char-slop/berry

multipurpose self-modifiable agentic harness

git clone https://git.t4t.associates/char-slop/berry

Charlotte Somreach other machines over ssha35b3b6

main
5.2 KiB126 linesraw
1import type { Context } from "@earendil-works/chord";
2import { type FileError, getOrThrow } from "@earendil-works/pi-durable/env";
3import ignore, { type Ignore } from "ignore";
4import type { Environment } from "@berry/api/environment.ts";
5
6function unavailable(error: FileError): boolean {
7  return ["not_found", "not_directory", "permission_denied", "invalid"].includes(error.code);
8}
9
10function score(path: string, query: string): number {
11  const text = path.toLowerCase();
12  if (query.length > text.length) return -Infinity;
13  let previous = Array<number>(text.length).fill(-Infinity);
14  for (let q = 0; q < query.length; q++) {
15    const current = Array<number>(text.length).fill(-Infinity);
16    let best = -Infinity;
17    for (let i = 0; i < text.length; i++) {
18      if (text[i] === query[q]) {
19        const boundary = i === 0 || /[/._-]/u.test(text[i - 1]) ||
20          (path[i] !== text[i] && path[i - 1] === text[i - 1]);
21        current[i] = (q === 0 ? -i * 0.1 : Math.max(best, (previous[i - 1] ?? -Infinity) + 8)) +
22          1 + (boundary ? 6 : 0);
23      }
24      best = Math.max(best, previous[i]) - 1;
25    }
26    previous = current;
27  }
28  const quality = Math.max(...previous);
29  if (!Number.isFinite(quality)) return -Infinity;
30  const name = text.replace(/\/$/u, "").split("/").at(-1)!;
31  const rank = text === query || name === query
32    ? 3
33    : name.startsWith(query)
34    ? 2
35    : name.includes(query)
36    ? 1
37    : 0;
38  return rank * 1000 + quality - path.length * 0.01;
39}
40
41const WALK_WIDTH = 16;
42
43/** Whether `path` names a directory, through any links; `false` where nothing can be seen. */
44async function isDirectory(env: Environment, path: string, context: Context): Promise<boolean> {
45  const canonical = await env.canonicalPath(path, context);
46  const info = canonical.ok ? await env.fileInfo(canonical.value, context) : canonical;
47  if (info.ok) return info.value.kind === "directory";
48  if (unavailable(info.error)) return false;
49  throw info.error;
50}
51
52/** Paths extending a reference from the environment's working directory, with directories left open. */
53export async function references(
54  env: Environment,
55  prefix: string,
56  context: Context,
57): Promise<string[]> {
58  if (/\s/u.test(prefix) || prefix.includes("\u0000")) return [];
59  if (prefix === "~") return ["~/"];
60  let base = prefix.match(/^(?:~\/|\/|(?:\.\.?\/)+)/u)?.[0] ?? "";
61  let query = prefix.slice(base.length);
62  let directory = getOrThrow(await env.absolutePath(base || ".", context));
63  while (query.includes("/")) {
64    const part = query.slice(0, query.indexOf("/") + 1);
65    const next = getOrThrow(await env.joinPath([directory, part], context));
66    if (!(await isDirectory(env, next, context))) break;
67    base += part;
68    query = query.slice(part.length);
69    directory = next;
70  }
71  query = query.toLowerCase();
72  const matches: { path: string; score: number }[] = [];
73  type Rule = { root: string; matcher: Ignore };
74  type Visit = { path: string; current: string; rules: Rule[] };
75  async function visit({ path, current, rules: inherited }: Visit): Promise<Visit[]> {
76    const entries = await env.listDir(current, context);
77    if (!entries.ok) {
78      if (unavailable(entries.error)) return [];
79      throw entries.error;
80    }
81    let rules = inherited;
82    const gitignore = entries.value.find((entry) => entry.name === ".gitignore");
83    if (gitignore) {
84      const source = await env.readTextFile(gitignore.path, context);
85      if (source.ok) rules = [...rules, { root: path, matcher: ignore().add(source.value) }];
86      else if (!unavailable(source.error)) throw source.error;
87    }
88    const next: Visit[] = [];
89    for (const entry of entries.value) {
90      if (/\s/u.test(entry.name) || [".git", ".jj", "node_modules"].includes(entry.name)) continue;
91      const relative = path + entry.name;
92      const directory = entry.kind === "directory" ||
93        (entry.kind === "symlink" && await isDirectory(env, entry.path, context));
94      const candidate = relative + (directory ? "/" : "");
95      let ignored = false;
96      for (const rule of rules) {
97        const result = rule.matcher.test(candidate.slice(rule.root.length));
98        if (result.ignored) ignored = true;
99        else if (result.unignored) ignored = false;
100      }
101      if (ignored) continue;
102      const quality = query === "" ? 0 : score(candidate, query);
103      if (Number.isFinite(quality)) matches.push({ path: base + candidate, score: quality });
104      // explicit symlink directories can be browsed, but recursive search must not follow cycles
105      if (query !== "" && entry.kind === "directory") {
106        next.push({ path: candidate, current: entry.path, rules });
107      }
108    }
109    return next;
110  }
111  // several directories at once, as each can be a round trip to another machine
112  const pending: Visit[] = [{ path: "", current: directory, rules: [] }];
113  const running = new Set<Promise<void>>();
114  while (pending.length > 0 || running.size > 0) {
115    while (pending.length > 0 && running.size < WALK_WIDTH) {
116      const visiting: Promise<void> = visit(pending.pop()!).then((next) => {
117        running.delete(visiting);
118        pending.push(...next);
119      });
120      running.add(visiting);
121    }
122    await Promise.race(running);
123  }
124  return matches.sort((a, b) => b.score - a.score || a.path.localeCompare(b.path))
125    .slice(0, 50).map((match) => match.path);
126}