@giovannijecha/jecode 0.8.4 → 0.8.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (99) hide show
  1. package/README.md +28 -9
  2. package/dist/accounts.js +47 -10
  3. package/dist/atomic.js +24 -14
  4. package/dist/batch-view.js +27 -2
  5. package/dist/batch.js +39 -4
  6. package/dist/bounded-file.js +212 -0
  7. package/dist/cli-info.js +0 -1
  8. package/dist/commands.js +2 -0
  9. package/dist/config.js +16 -7
  10. package/dist/context/automatic.js +35 -0
  11. package/dist/context/compactor.js +32 -2
  12. package/dist/context/manual.js +20 -3
  13. package/dist/context/request-projection.js +130 -0
  14. package/dist/controller-request.js +33 -11
  15. package/dist/credential-commands.js +25 -9
  16. package/dist/credentials.js +56 -18
  17. package/dist/directory-anchor.js +91 -0
  18. package/dist/file-identity.js +12 -0
  19. package/dist/model-command.js +5 -4
  20. package/dist/openai-account-command.js +23 -10
  21. package/dist/openai-account.js +7 -5
  22. package/dist/openai-oauth-callback.js +13 -4
  23. package/dist/openai-oauth-tokens.js +9 -7
  24. package/dist/openai-oauth.js +8 -6
  25. package/dist/permission-command.js +1 -1
  26. package/dist/process-lease.js +329 -0
  27. package/dist/provider-commands.js +11 -37
  28. package/dist/provider-errors.js +31 -7
  29. package/dist/provider-label.js +9 -3
  30. package/dist/providers/anthropic-stream.js +4 -1
  31. package/dist/providers/anthropic-wire.js +8 -3
  32. package/dist/providers/anthropic.js +39 -20
  33. package/dist/providers/catalog.js +4 -4
  34. package/dist/providers/failure.js +181 -0
  35. package/dist/providers/http.js +82 -23
  36. package/dist/providers/index.js +1 -5
  37. package/dist/providers/ollama-context.js +42 -0
  38. package/dist/providers/ollama-endpoint.js +7 -34
  39. package/dist/providers/ollama-stream.js +5 -1
  40. package/dist/providers/ollama.js +36 -158
  41. package/dist/providers/openai-codex.js +71 -45
  42. package/dist/providers/openai-stream.js +26 -2
  43. package/dist/providers/openai.js +51 -25
  44. package/dist/providers/sse.js +52 -8
  45. package/dist/request-identity.js +32 -0
  46. package/dist/sessions/bucket.js +55 -0
  47. package/dist/sessions/catalog-io.js +162 -0
  48. package/dist/sessions/catalog.js +3 -1
  49. package/dist/sessions/codec-messages.js +122 -0
  50. package/dist/sessions/codec-transcript.js +93 -0
  51. package/dist/sessions/codec-values.js +52 -0
  52. package/dist/sessions/codec.js +4 -257
  53. package/dist/sessions/files.js +158 -0
  54. package/dist/sessions/lease.js +132 -49
  55. package/dist/sessions/load.js +90 -0
  56. package/dist/sessions/runtime.js +15 -8
  57. package/dist/sessions/snapshot.js +33 -0
  58. package/dist/sessions/store.js +183 -378
  59. package/dist/settings-command.js +10 -5
  60. package/dist/settings.js +75 -22
  61. package/dist/stable-directory.js +148 -0
  62. package/dist/start.js +1 -2
  63. package/dist/store-lock.js +68 -84
  64. package/dist/tools/args.js +2 -2
  65. package/dist/tools/file-read.js +192 -0
  66. package/dist/tools/file-summary.js +9 -0
  67. package/dist/tools/{fs.js → file-write.js} +5 -171
  68. package/dist/tools/glob.js +107 -0
  69. package/dist/tools/index.js +2 -1
  70. package/dist/tools/search.js +66 -205
  71. package/dist/tools/text-boundary.js +7 -33
  72. package/dist/tui/app-workflows.js +9 -334
  73. package/dist/tui/approve.js +5 -3
  74. package/dist/tui/blocks.js +8 -7
  75. package/dist/tui/command-workflow.js +106 -0
  76. package/dist/tui/components/command-menu.js +7 -10
  77. package/dist/tui/components/footer.js +1 -1
  78. package/dist/tui/components/menu.js +74 -43
  79. package/dist/tui/components/messages.js +16 -9
  80. package/dist/tui/components/tool-evidence.js +107 -0
  81. package/dist/tui/components/tool-motion.js +32 -0
  82. package/dist/tui/components/tool.js +48 -202
  83. package/dist/tui/feedback.js +4 -0
  84. package/dist/tui/help.js +1 -1
  85. package/dist/tui/picker-layout.js +40 -0
  86. package/dist/tui/picker.js +7 -71
  87. package/dist/tui/session-view.js +7 -2
  88. package/dist/tui/tool-details.js +135 -0
  89. package/dist/tui/transcript-grammar.js +8 -1
  90. package/dist/tui/transcript-view.js +26 -112
  91. package/dist/tui/turn-workflow.js +264 -0
  92. package/dist/tui/turn.js +7 -140
  93. package/dist/tui/workflow-types.js +2 -0
  94. package/dist/tui/workspace.js +21 -7
  95. package/dist/user-store.js +23 -31
  96. package/package.json +14 -15
  97. package/dist/ollama-settings-command.js +0 -74
  98. package/dist/tools/ripgrep.js +0 -230
  99. package/dist/tui/motion.js +0 -32
@@ -0,0 +1,192 @@
1
+ // Bounded workspace file reads and stable directory listings.
2
+ import * as path from "node:path";
3
+ import { withStableFile } from "../bounded-file.js";
4
+ import { readStableDirectory } from "../stable-directory.js";
5
+ import { optionalInt, optionalString, requireString } from "./args.js";
6
+ import { resolveExistingInRoot } from "./paths.js";
7
+ import { leadingText } from "./text-boundary.js";
8
+ import { count, plural } from "./file-summary.js";
9
+ const MAX_READ_CHARS = 60_000;
10
+ const MAX_LIST_CHARS = 60_000;
11
+ const MAX_LIST_ENTRIES = 2_000;
12
+ const READ_CHUNK_BYTES = 64 * 1024;
13
+ const MAX_READ_SCAN_BYTES = 16 * 1024 * 1024;
14
+ export const readFile = {
15
+ name: "read_file",
16
+ description: "Read a regular UTF-8 text file inside the workspace. Optionally start at a line " +
17
+ "(1-based) and cap how many lines come back. Large files are truncated.",
18
+ dangerous: false,
19
+ concurrency: "shared",
20
+ input: {
21
+ type: "object",
22
+ properties: {
23
+ path: { type: "string", description: "Path relative to the workspace root." },
24
+ offset: { type: "integer", description: "First line to return, 1-based." },
25
+ limit: { type: "integer", description: "How many lines to return." },
26
+ },
27
+ required: ["path"],
28
+ },
29
+ async run(args, ctx) {
30
+ return runReadFile(args, ctx);
31
+ },
32
+ };
33
+ export async function runReadFile(args, ctx, dependencies = {}) {
34
+ const root = await resolveExistingInRoot(ctx.root, ".");
35
+ const target = await resolveExistingInRoot(root, requireString(args, "path"));
36
+ const offset = optionalInt(args, "offset");
37
+ const limit = optionalInt(args, "limit");
38
+ const { text, truncated, scanCapped } = await readRange(root, target, offset, limit, ctx.signal, dependencies);
39
+ if (truncated) {
40
+ return {
41
+ output: `${text}\n\n[truncated at ${MAX_READ_CHARS} characters — read a narrower range]`,
42
+ summary: `truncated at ${MAX_READ_CHARS} characters`,
43
+ };
44
+ }
45
+ if (scanCapped) {
46
+ const notice = `[read stopped after scanning ${MAX_READ_SCAN_BYTES} bytes — use a smaller offset or search_text]`;
47
+ return {
48
+ output: text === "" ? notice : `${text}\n\n${notice}`,
49
+ summary: `scan capped at ${MAX_READ_SCAN_BYTES} bytes`,
50
+ };
51
+ }
52
+ if (text === "")
53
+ return { output: "[file is empty]", summary: "empty" };
54
+ return { output: text, summary: `${count(text, "line")}` };
55
+ }
56
+ export const listDir = {
57
+ name: "list_dir",
58
+ description: "List the entries of a directory inside the workspace. Directories end with a slash.",
59
+ dangerous: false,
60
+ concurrency: "shared",
61
+ input: {
62
+ type: "object",
63
+ properties: {
64
+ path: { type: "string", description: "Directory relative to the workspace root. Defaults to the root." },
65
+ },
66
+ required: [],
67
+ },
68
+ async run(args, ctx) {
69
+ return runListDir(args, ctx);
70
+ },
71
+ };
72
+ export async function runListDir(args, ctx, dependencies = {}) {
73
+ const root = await resolveExistingInRoot(ctx.root, ".");
74
+ const requested = optionalString(args, "path");
75
+ const relative = requested === undefined || requested.trim() === "" ? "." : requested;
76
+ const target = await resolveExistingInRoot(root, relative);
77
+ const inspected = await readStableDirectory(root, target, {
78
+ maxEntries: MAX_LIST_ENTRIES + 1,
79
+ signal: ctx.signal,
80
+ beforeOpen: dependencies.beforeOpen,
81
+ });
82
+ const entries = [];
83
+ let chars = 0;
84
+ let truncated = inspected.capped;
85
+ for (const entry of inspected.entries) {
86
+ const label = entry.kind === "directory" ? `${entry.name}/` : entry.name;
87
+ const separator = entries.length === 0 ? 0 : 1;
88
+ if (entries.length >= MAX_LIST_ENTRIES ||
89
+ chars + separator + label.length > MAX_LIST_CHARS) {
90
+ truncated = true;
91
+ break;
92
+ }
93
+ entries.push(label);
94
+ chars += separator + label.length;
95
+ }
96
+ if (entries.length === 0)
97
+ return { output: "[empty directory]", summary: "empty" };
98
+ const listing = entries.join("\n");
99
+ if (truncated) {
100
+ return {
101
+ output: `${listing}\n\n[truncated after ${entries.length} entries]`,
102
+ summary: `${entries.length}+ entries`,
103
+ };
104
+ }
105
+ return { output: listing, summary: plural(entries.length, "entry", "entries") };
106
+ }
107
+ async function readRange(root, target, offset, limit, signal, dependencies) {
108
+ throwIfAborted(signal);
109
+ const firstLine = Math.max(1, offset ?? 1);
110
+ const lineCount = limit === undefined ? undefined : Math.max(0, limit);
111
+ const endLine = lineCount === undefined ? Number.POSITIVE_INFINITY : firstLine + lineCount;
112
+ if (lineCount === 0)
113
+ return { text: "", truncated: false, scanCapped: false };
114
+ const result = await withStableFile(target, {
115
+ label: "read file",
116
+ signal,
117
+ beforeOpen: dependencies.beforeOpen,
118
+ }, async (handle, opened) => {
119
+ const decoder = new TextDecoder();
120
+ let text = "";
121
+ let line = 1;
122
+ let stopped = false;
123
+ let truncated = false;
124
+ let reachedEnd = false;
125
+ const selected = (at) => at >= firstLine && at < endLine;
126
+ const append = (fragment) => {
127
+ if (fragment === "")
128
+ return;
129
+ const room = MAX_READ_CHARS - text.length;
130
+ if (fragment.length > room) {
131
+ text += leadingText(fragment, room);
132
+ truncated = true;
133
+ stopped = true;
134
+ return;
135
+ }
136
+ text += fragment;
137
+ };
138
+ const consume = (chunk) => {
139
+ let start = 0;
140
+ while (!stopped && start < chunk.length) {
141
+ const newline = chunk.indexOf("\n", start);
142
+ const end = newline === -1 ? chunk.length : newline;
143
+ if (selected(line))
144
+ append(chunk.slice(start, end));
145
+ if (stopped || newline === -1)
146
+ return;
147
+ const nextLine = line + 1;
148
+ if (selected(line) && selected(nextLine))
149
+ append("\n");
150
+ line = nextLine;
151
+ if (line >= endLine) {
152
+ stopped = true;
153
+ return;
154
+ }
155
+ start = newline + 1;
156
+ }
157
+ };
158
+ let position = 0;
159
+ const buffer = Buffer.allocUnsafe(READ_CHUNK_BYTES);
160
+ while (!stopped && position < MAX_READ_SCAN_BYTES) {
161
+ throwIfAborted(signal);
162
+ const length = Math.min(buffer.length, MAX_READ_SCAN_BYTES - position);
163
+ const { bytesRead } = await handle.read(buffer, 0, length, position);
164
+ throwIfAborted(signal);
165
+ if (bytesRead === 0) {
166
+ reachedEnd = true;
167
+ break;
168
+ }
169
+ position += bytesRead;
170
+ consume(decoder.decode(buffer.subarray(0, bytesRead), { stream: true }));
171
+ }
172
+ if (!stopped && BigInt(position) >= opened.size)
173
+ reachedEnd = true;
174
+ if (!stopped && reachedEnd)
175
+ consume(decoder.decode());
176
+ return {
177
+ text,
178
+ truncated,
179
+ scanCapped: !stopped && !reachedEnd && position >= MAX_READ_SCAN_BYTES,
180
+ };
181
+ });
182
+ const confirmed = await resolveExistingInRoot(root, target);
183
+ if (path.relative(target, confirmed) !== "") {
184
+ throw new Error("read file changed while it was being read");
185
+ }
186
+ return result;
187
+ }
188
+ function throwIfAborted(signal) {
189
+ if (signal?.aborted !== true)
190
+ return;
191
+ throw signal.reason instanceof Error ? signal.reason : new Error("interrupted");
192
+ }
@@ -0,0 +1,9 @@
1
+ // Shared line and entry counts in filesystem tool summaries.
2
+ export function count(text, noun) {
3
+ if (text === "")
4
+ return "empty";
5
+ return plural(text.split("\n").length, noun, `${noun}s`);
6
+ }
7
+ export function plural(n, one, many) {
8
+ return `${n} ${n === 1 ? one : many}`;
9
+ }
@@ -1,94 +1,12 @@
1
- // Filesystem tools: read, list, write, edit.
2
- import { constants } from "node:fs";
1
+ // Atomic file mutations share preview validation and exact replacement rules.
3
2
  import * as fs from "node:fs/promises";
4
3
  import * as path from "node:path";
5
- import { optionalBool, optionalInt, optionalString, requireString } from "./args.js";
6
- import { assertDirectWritableInRoot, displayPath, resolveDirectWritableInRoot, resolveExistingInRoot, } from "./paths.js";
7
- import { assertEditableText, assertReplacementFits, leadingText, MAX_EDITABLE_CHARS, MAX_EDITABLE_LINES, readEditableText, } from "./text-boundary.js";
8
4
  import { atomicWrite } from "../atomic.js";
9
- const MAX_READ_CHARS = 60_000;
10
- const MAX_LIST_CHARS = 60_000;
11
- const MAX_LIST_ENTRIES = 2_000;
12
- const READ_CHUNK_BYTES = 64 * 1024;
5
+ import { optionalBool, requireString } from "./args.js";
6
+ import { assertDirectWritableInRoot, displayPath, resolveDirectWritableInRoot, resolveExistingInRoot, } from "./paths.js";
7
+ import { assertEditableText, assertReplacementFits, MAX_EDITABLE_CHARS, MAX_EDITABLE_LINES, readEditableText, } from "./text-boundary.js";
8
+ import { count, plural } from "./file-summary.js";
13
9
  const DEFAULT_MUTATION_DEPENDENCIES = { atomicWrite };
14
- export const readFile = {
15
- name: "read_file",
16
- description: "Read a regular UTF-8 text file inside the workspace. Optionally start at a line " +
17
- "(1-based) and cap how many lines come back. Large files are truncated.",
18
- dangerous: false,
19
- concurrency: "shared",
20
- input: {
21
- type: "object",
22
- properties: {
23
- path: { type: "string", description: "Path relative to the workspace root." },
24
- offset: { type: "integer", description: "First line to return, 1-based." },
25
- limit: { type: "integer", description: "How many lines to return." },
26
- },
27
- required: ["path"],
28
- },
29
- async run(args, ctx) {
30
- const root = await resolveExistingInRoot(ctx.root, ".");
31
- const target = await resolveExistingInRoot(root, requireString(args, "path"));
32
- const offset = optionalInt(args, "offset");
33
- const limit = optionalInt(args, "limit");
34
- const { text, truncated } = await readRange(target, offset, limit, ctx.signal);
35
- if (truncated) {
36
- return {
37
- output: `${text}\n\n[truncated at ${MAX_READ_CHARS} characters — read a narrower range]`,
38
- summary: `truncated at ${MAX_READ_CHARS} characters`,
39
- };
40
- }
41
- if (text === "")
42
- return { output: "[file is empty]", summary: "empty" };
43
- return { output: text, summary: `${count(text, "line")}` };
44
- },
45
- };
46
- export const listDir = {
47
- name: "list_dir",
48
- description: "List the entries of a directory inside the workspace. Directories end with a slash.",
49
- dangerous: false,
50
- concurrency: "shared",
51
- input: {
52
- type: "object",
53
- properties: {
54
- path: { type: "string", description: "Directory relative to the workspace root. Defaults to the root." },
55
- },
56
- required: [],
57
- },
58
- async run(args, ctx) {
59
- const root = await resolveExistingInRoot(ctx.root, ".");
60
- const requested = optionalString(args, "path");
61
- const relative = requested === undefined || requested.trim() === "" ? "." : requested;
62
- const target = await resolveExistingInRoot(root, relative);
63
- const entries = [];
64
- let chars = 0;
65
- let truncated = false;
66
- const directory = await fs.opendir(target);
67
- for await (const entry of directory) {
68
- const label = entry.isDirectory() ? `${entry.name}/` : entry.name;
69
- const separator = entries.length === 0 ? 0 : 1;
70
- if (entries.length >= MAX_LIST_ENTRIES ||
71
- chars + separator + label.length > MAX_LIST_CHARS) {
72
- truncated = true;
73
- break;
74
- }
75
- entries.push(label);
76
- chars += separator + label.length;
77
- }
78
- if (entries.length === 0)
79
- return { output: "[empty directory]", summary: "empty" };
80
- const listing = entries
81
- .sort((a, b) => a.localeCompare(b))
82
- .join("\n");
83
- if (truncated) {
84
- return {
85
- output: `${listing}\n\n[truncated after ${entries.length} entries]`,
86
- summary: `${entries.length}+ entries`,
87
- };
88
- }
89
- return { output: listing, summary: plural(entries.length, "entry", "entries") };
90
- },
91
- };
92
10
  export const writeFile = {
93
11
  name: "write_file",
94
12
  description: "Create a file, or replace its entire contents. Parent directories are " +
@@ -204,82 +122,6 @@ export async function runEditFile(args, ctx, dependencies = DEFAULT_MUTATION_DEP
204
122
  summary: plural(made, "replacement", "replacements"),
205
123
  };
206
124
  }
207
- async function readRange(target, offset, limit, signal) {
208
- throwIfAborted(signal);
209
- const firstLine = Math.max(1, offset ?? 1);
210
- const lineCount = limit === undefined ? undefined : Math.max(0, limit);
211
- const endLine = lineCount === undefined ? Number.POSITIVE_INFINITY : firstLine + lineCount;
212
- if (lineCount === 0)
213
- return { text: "", truncated: false };
214
- if (!(await fs.lstat(target)).isFile())
215
- throw new Error("path must be a regular file");
216
- // O_NONBLOCK prevents a path swapped to a FIFO between lstat and open from
217
- // waiting forever for a writer. The handle stat closes that race before any
218
- // content is accepted.
219
- const flags = process.platform === "win32"
220
- ? "r"
221
- : constants.O_RDONLY | (constants.O_NONBLOCK ?? 0);
222
- const handle = await fs.open(target, flags);
223
- const decoder = new TextDecoder();
224
- let text = "";
225
- let line = 1;
226
- let stopped = false;
227
- let truncated = false;
228
- const selected = (at) => at >= firstLine && at < endLine;
229
- const append = (fragment) => {
230
- if (fragment === "")
231
- return;
232
- const room = MAX_READ_CHARS - text.length;
233
- if (fragment.length > room) {
234
- text += leadingText(fragment, room);
235
- truncated = true;
236
- stopped = true;
237
- return;
238
- }
239
- text += fragment;
240
- };
241
- const consume = (chunk) => {
242
- let start = 0;
243
- while (!stopped && start < chunk.length) {
244
- const newline = chunk.indexOf("\n", start);
245
- const end = newline === -1 ? chunk.length : newline;
246
- if (selected(line))
247
- append(chunk.slice(start, end));
248
- if (stopped || newline === -1)
249
- return;
250
- const nextLine = line + 1;
251
- if (selected(line) && selected(nextLine))
252
- append("\n");
253
- line = nextLine;
254
- if (line >= endLine) {
255
- stopped = true;
256
- return;
257
- }
258
- start = newline + 1;
259
- }
260
- };
261
- try {
262
- if (!(await handle.stat()).isFile())
263
- throw new Error("path must be a regular file");
264
- let position = 0;
265
- const buffer = Buffer.allocUnsafe(READ_CHUNK_BYTES);
266
- while (!stopped) {
267
- throwIfAborted(signal);
268
- const { bytesRead } = await handle.read(buffer, 0, buffer.length, position);
269
- throwIfAborted(signal);
270
- if (bytesRead === 0)
271
- break;
272
- position += bytesRead;
273
- consume(decoder.decode(buffer.subarray(0, bytesRead), { stream: true }));
274
- }
275
- if (!stopped)
276
- consume(decoder.decode());
277
- }
278
- finally {
279
- await handle.close();
280
- }
281
- return { text, truncated };
282
- }
283
125
  function throwIfAborted(signal) {
284
126
  if (signal?.aborted !== true)
285
127
  return;
@@ -348,11 +190,3 @@ function changedFile(operation, previewed) {
348
190
  const when = previewed ? "after the preview" : "while preparing the change";
349
191
  return new Error(`file changed ${when} — inspect it and retry the ${operation}`);
350
192
  }
351
- function count(text, noun) {
352
- if (text === "")
353
- return "empty";
354
- return plural(text.split("\n").length, noun, `${noun}s`);
355
- }
356
- function plural(n, one, many) {
357
- return `${n} ${n === 1 ? one : many}`;
358
- }
@@ -0,0 +1,107 @@
1
+ // Bounded workspace glob matching without regular-expression backtracking.
2
+ import * as path from "node:path";
3
+ const MAX_GLOB_CHARS = 512;
4
+ export function glob(pattern) {
5
+ const normalized = pattern.replace(/\\/g, "/");
6
+ if (normalized.length > MAX_GLOB_CHARS) {
7
+ throw new Error(`"pattern" must be at most ${MAX_GLOB_CHARS} characters`);
8
+ }
9
+ const tokens = tokenizeGlob(normalized.toLowerCase());
10
+ const basenameOnly = !normalized.includes("/");
11
+ return (relative) => {
12
+ const candidate = relative.replace(/\\/g, "/");
13
+ const target = (basenameOnly ? path.posix.basename(candidate) : candidate).toLowerCase();
14
+ return matchGlob(tokens, Array.from(target));
15
+ };
16
+ }
17
+ function tokenizeGlob(pattern) {
18
+ const chars = Array.from(pattern);
19
+ const tokens = [];
20
+ let index = 0;
21
+ while (index < chars.length) {
22
+ const char = chars[index];
23
+ if (char === "*") {
24
+ let end = index + 1;
25
+ while (chars[end] === "*")
26
+ end++;
27
+ if (end - index >= 2) {
28
+ if (chars[end] === "/") {
29
+ tokens.push({ kind: "globdir-start" }, { kind: "globdir-body" });
30
+ index = end + 1;
31
+ }
32
+ else {
33
+ tokens.push({ kind: "globstar" });
34
+ index = end;
35
+ }
36
+ }
37
+ else {
38
+ tokens.push({ kind: "star" });
39
+ index = end;
40
+ }
41
+ continue;
42
+ }
43
+ tokens.push(char === "?" ? { kind: "one" } : { kind: "literal", value: char });
44
+ index++;
45
+ }
46
+ return tokens;
47
+ }
48
+ /** Thompson-style wildcard matching: O(pattern × path), with no regex backtracking. */
49
+ function matchGlob(tokens, text) {
50
+ let states = epsilonClosure(new Set([0]), tokens);
51
+ for (const char of text) {
52
+ const next = new Set();
53
+ for (const state of states) {
54
+ const token = tokens[state];
55
+ if (token === undefined)
56
+ continue;
57
+ switch (token.kind) {
58
+ case "literal":
59
+ if (token.value === char)
60
+ next.add(state + 1);
61
+ break;
62
+ case "one":
63
+ if (char !== "/")
64
+ next.add(state + 1);
65
+ break;
66
+ case "star":
67
+ if (char !== "/")
68
+ next.add(state);
69
+ break;
70
+ case "globstar":
71
+ next.add(state);
72
+ break;
73
+ case "globdir-body":
74
+ next.add(state);
75
+ if (char === "/")
76
+ next.add(state + 1);
77
+ break;
78
+ case "globdir-start":
79
+ break;
80
+ }
81
+ }
82
+ states = epsilonClosure(next, tokens);
83
+ if (states.size === 0)
84
+ return false;
85
+ }
86
+ return epsilonClosure(states, tokens).has(tokens.length);
87
+ }
88
+ function epsilonClosure(seed, tokens) {
89
+ const states = new Set(seed);
90
+ const pending = [...seed];
91
+ while (pending.length > 0) {
92
+ const state = pending.pop();
93
+ const token = tokens[state];
94
+ const targets = token?.kind === "globdir-start"
95
+ ? [state + 1, state + 2]
96
+ : token?.kind === "star" || token?.kind === "globstar"
97
+ ? [state + 1]
98
+ : [];
99
+ for (const target of targets) {
100
+ if (states.has(target))
101
+ continue;
102
+ states.add(target);
103
+ pending.push(target);
104
+ }
105
+ }
106
+ return states;
107
+ }
@@ -1,5 +1,6 @@
1
1
  // The tool registry, and the one place a tool actually gets run.
2
- import { editFile, listDir, readFile, writeFile } from "./fs.js";
2
+ import { listDir, readFile } from "./file-read.js";
3
+ import { editFile, writeFile } from "./file-write.js";
3
4
  import { runCommand } from "./shell.js";
4
5
  import { findFiles, searchText } from "./search.js";
5
6
  export function builtinTools() {