tryharness 0.7.23 → 0.7.25

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 (61) hide show
  1. package/assets/opencode-plugin.js +48 -11
  2. package/assets/pi-extension.ts +105 -26
  3. package/cli.mjs +1238 -322
  4. package/lib/activity.mjs +197 -40
  5. package/lib/adapters.mjs +337 -54
  6. package/lib/agentnode.mjs +175 -35
  7. package/lib/atlas.mjs +383 -83
  8. package/lib/cards.mjs +174 -40
  9. package/lib/claudemem.mjs +74 -18
  10. package/lib/commands.mjs +13 -5
  11. package/lib/context.mjs +409 -71
  12. package/lib/control-client.mjs +5 -2
  13. package/lib/coordinator.mjs +274 -57
  14. package/lib/daemon.mjs +5248 -1024
  15. package/lib/desktop.mjs +149 -35
  16. package/lib/digest.mjs +54 -13
  17. package/lib/environment.mjs +103 -21
  18. package/lib/files.mjs +246 -13
  19. package/lib/firewall.mjs +169 -35
  20. package/lib/folders.mjs +179 -36
  21. package/lib/frames.mjs +214 -40
  22. package/lib/inference.mjs +463 -126
  23. package/lib/learn.mjs +78 -16
  24. package/lib/live.mjs +286 -61
  25. package/lib/localmodels.mjs +424 -83
  26. package/lib/mcp-stdio.mjs +868 -151
  27. package/lib/media.mjs +138 -28
  28. package/lib/members.json +1 -1
  29. package/lib/members.mjs +35 -9
  30. package/lib/models.mjs +342 -80
  31. package/lib/network.mjs +201 -42
  32. package/lib/notify.mjs +49 -12
  33. package/lib/nudge.mjs +184 -33
  34. package/lib/ocr.mjs +223 -60
  35. package/lib/onboard.mjs +321 -70
  36. package/lib/parsers.mjs +132 -41
  37. package/lib/paths.mjs +33 -10
  38. package/lib/personal.mjs +51 -13
  39. package/lib/phones.mjs +382 -60
  40. package/lib/places.mjs +555 -107
  41. package/lib/provenance.mjs +19 -4
  42. package/lib/receipts.mjs +51 -11
  43. package/lib/redact.mjs +512 -116
  44. package/lib/runner.mjs +345 -55
  45. package/lib/runtime-install.mjs +55 -14
  46. package/lib/screens.mjs +154 -38
  47. package/lib/sealed.mjs +157 -28
  48. package/lib/secrets.mjs +18 -5
  49. package/lib/service.mjs +170 -34
  50. package/lib/snippets.mjs +94 -18
  51. package/lib/speech.mjs +688 -175
  52. package/lib/store.mjs +1654 -307
  53. package/lib/swarm.mjs +596 -117
  54. package/lib/t3.mjs +400 -79
  55. package/lib/trails.mjs +285 -61
  56. package/lib/ui.mjs +95 -23
  57. package/lib/vectors.mjs +5 -2
  58. package/lib/wakes.mjs +284 -68
  59. package/lib/watches.mjs +247 -48
  60. package/lib/worktrees.mjs +186 -38
  61. package/package.json +3 -3
package/lib/trails.mjs CHANGED
@@ -7,43 +7,92 @@ import readline from "node:readline";
7
7
  // call used. This is the record the context engine learns from (what a run needed against what its brief carried) and
8
8
  // the evidence receipts check claims against. Read only; the parsers never decide anything about intent.
9
9
 
10
- const PATH_TOKEN = /(?:\.{0,2}\/)?[\w@+-][\w@+.-]*(?:\/[\w@+.-]+)+|[\w@+-][\w@+-]*\.[A-Za-z][A-Za-z0-9]{0,7}\b/g;
10
+ const PATH_TOKEN =
11
+ /(?:\.{0,2}\/)?[\w@+-][\w@+.-]*(?:\/[\w@+.-]+)+|[\w@+-][\w@+-]*\.[A-Za-z][A-Za-z0-9]{0,7}\b/g;
11
12
  const PATCH_FILE = /\*\*\* (?:Update|Add|Delete) File: ([^\n"\\]+)/g;
12
- const REDIRECT_FILE = /(?:^|[\s;&|])(?:>>?|tee(?:\s+-a)?)\s+(["']?)([^\s"'|;&<>]+)\1/g;
13
- const SED_INPLACE = /\bsed\s+(?:-[a-zA-Z]*i[a-zA-Z]*(?:\s+''|\s+"")?|--in-place)\b[^|;&]*?\s(["']?)([^\s"'|;&]+)\1\s*(?:$|[|;&])/gm;
13
+ const REDIRECT_FILE =
14
+ /(?:^|[\s;&|])(?:>>?|tee(?:\s+-a)?)\s+(["']?)([^\s"'|;&<>]+)\1/g;
15
+ const SED_INPLACE =
16
+ /\bsed\s+(?:-[a-zA-Z]*i[a-zA-Z]*(?:\s+''|\s+"")?|--in-place)\b[^|;&]*?\s(["']?)([^\s"'|;&]+)\1\s*(?:$|[|;&])/gm;
14
17
  // Test runners by their command, for receipts: a command's own exit code is the evidence, never the agent's words.
15
- const TEST_COMMAND = /\b(?:npm(?:\s+run)?\s+(?:test|smoke|e2e|eval|lint|typecheck)|npx\s+(?:jest|vitest|tsx\s+tests\/|playwright\s+test)|jest|vitest|pytest|cargo\s+(?:test|check|clippy)|go\s+test|forge\s+test|bun\s+test|deno\s+test|swift\s+test|mvn\s+test|gradlew?\s+test|make\s+(?:test|check)|node\s+(?:--test\b|[\w./-]*\btests?\/)|tsc(?:\s|$))/;
16
- const EDIT_TOOLS = new Set(["Edit", "Write", "MultiEdit", "NotebookEdit", "edit", "write", "apply_patch"]);
17
- const BRIEF_SKIP = /^<(?:command-|local-command|system-reminder|task-notification|user-prompt-submit-hook)|^Caveat:/;
18
+ const TEST_COMMAND =
19
+ /\b(?:npm(?:\s+run)?\s+(?:test|smoke|e2e|eval|lint|typecheck)|npx\s+(?:jest|vitest|tsx\s+tests\/|playwright\s+test)|jest|vitest|pytest|cargo\s+(?:test|check|clippy)|go\s+test|forge\s+test|bun\s+test|deno\s+test|swift\s+test|mvn\s+test|gradlew?\s+test|make\s+(?:test|check)|node\s+(?:--test\b|[\w./-]*\btests?\/)|tsc(?:\s|$))/;
20
+ const EDIT_TOOLS = new Set([
21
+ "Edit",
22
+ "Write",
23
+ "MultiEdit",
24
+ "NotebookEdit",
25
+ "edit",
26
+ "write",
27
+ "apply_patch",
28
+ ]);
29
+ const BRIEF_SKIP =
30
+ /^<(?:command-|local-command|system-reminder|task-notification|user-prompt-submit-hook)|^Caveat:/;
18
31
 
19
- const parseJson = (line) => { try { const value = JSON.parse(line); return value && typeof value === "object" ? value : null; } catch { return null; } };
20
- const textOf = (content) => typeof content === "string" ? content
21
- : Array.isArray(content) ? content.map((part) => (typeof part?.text === "string" ? part.text : "")).filter(Boolean).join("\n") : "";
22
- const ms = (value) => { const at = typeof value === "number" ? value * (value < 1e12 ? 1000 : 1) : Date.parse(value); return Number.isFinite(at) ? at : null; };
32
+ const parseJson = (line) => {
33
+ try {
34
+ const value = JSON.parse(line);
35
+ return value && typeof value === "object" ? value : null;
36
+ } catch {
37
+ return null;
38
+ }
39
+ };
40
+ const textOf = (content) =>
41
+ typeof content === "string"
42
+ ? content
43
+ : Array.isArray(content)
44
+ ? content
45
+ .map((part) => (typeof part?.text === "string" ? part.text : ""))
46
+ .filter(Boolean)
47
+ .join("\n")
48
+ : "";
49
+ const ms = (value) => {
50
+ const at =
51
+ typeof value === "number"
52
+ ? value * (value < 1e12 ? 1000 : 1)
53
+ : Date.parse(value);
54
+ return Number.isFinite(at) ? at : null;
55
+ };
23
56
 
24
57
  /** Path-like words in a tool call's input or output, as written (relative or absolute). */
25
58
  export function pathTokens(text) {
26
- return [...new Set(String(text).match(PATH_TOKEN) || [])].map((token) => token.replace(/[.,:;)\]}'"`]+$/, "")).filter((token) => token.length > 2 && token.length < 300);
59
+ return [...new Set(String(text).match(PATH_TOKEN) || [])]
60
+ .map((token) => token.replace(/[.,:;)\]}'"`]+$/, ""))
61
+ .filter((token) => token.length > 2 && token.length < 300);
27
62
  }
28
63
 
29
64
  /** Files a tool call changed: edit tools' own paths, patch headers, sed -i, redirects and tee. */
30
65
  export function editedPaths(tool, input) {
31
66
  const out = new Set();
32
- if (EDIT_TOOLS.has(tool) && typeof input?.file_path === "string") out.add(input.file_path);
33
- if (EDIT_TOOLS.has(tool) && typeof input === "string") { const found = input.match(/"(?:file_path|path)":"([^"]+)"/); if (found && !found[1].endsWith("…")) out.add(found[1]); }
34
- if (EDIT_TOOLS.has(tool) && typeof input?.path === "string") out.add(input.path);
35
- if (tool === "NotebookEdit" && typeof input?.notebook_path === "string") out.add(input.notebook_path);
36
- const text = typeof input === "string" ? input : JSON.stringify(input ?? "").replace(/\\n/g, "\n").replace(/\\"/g, "\"");
67
+ if (EDIT_TOOLS.has(tool) && typeof input?.file_path === "string")
68
+ out.add(input.file_path);
69
+ if (EDIT_TOOLS.has(tool) && typeof input === "string") {
70
+ const found = input.match(/"(?:file_path|path)":"([^"]+)"/);
71
+ if (found && !found[1].endsWith("…")) out.add(found[1]);
72
+ }
73
+ if (EDIT_TOOLS.has(tool) && typeof input?.path === "string")
74
+ out.add(input.path);
75
+ if (tool === "NotebookEdit" && typeof input?.notebook_path === "string")
76
+ out.add(input.notebook_path);
77
+ const text =
78
+ typeof input === "string"
79
+ ? input
80
+ : JSON.stringify(input ?? "")
81
+ .replace(/\\n/g, "\n")
82
+ .replace(/\\"/g, '"');
37
83
  for (const match of text.matchAll(PATCH_FILE)) out.add(match[1].trim());
38
84
  for (const match of text.matchAll(SED_INPLACE)) out.add(match[2]);
39
- for (const match of text.matchAll(REDIRECT_FILE)) if (!/^\/dev\/|^&\d$/.test(match[2])) out.add(match[2]);
85
+ for (const match of text.matchAll(REDIRECT_FILE))
86
+ if (!/^\/dev\/|^&\d$/.test(match[2])) out.add(match[2]);
40
87
  return [...out];
41
88
  }
42
89
 
43
90
  export const isTestCommand = (command) => TEST_COMMAND.test(String(command));
44
91
 
45
92
  function exitCodeOf(text) {
46
- const found = String(text).match(/"?exit_code"?\s*[:=]\s*(-?\d+)|Exit code:?\s*(-?\d+)|Process exited with code (-?\d+)/i);
93
+ const found = String(text).match(
94
+ /"?exit_code"?\s*[:=]\s*(-?\d+)|Exit code:?\s*(-?\d+)|Process exited with code (-?\d+)/i
95
+ );
47
96
  return found ? Number(found[1] ?? found[2] ?? found[3]) : null;
48
97
  }
49
98
 
@@ -53,14 +102,44 @@ function exitCodeOf(text) {
53
102
  * per model call { at, input, cached, output }; turns are the person's messages and stops, { at, interrupted }. Claude Code and Codex are read in full; other agents fall back to none.
54
103
  */
55
104
  export async function readTrail(agent, file) {
56
- if (!["claude-code", "codex"].includes(agent) || !fs.existsSync(file)) return null;
57
- const trail = { agent, sessionId: null, cwd: null, brief: null, startedAt: null, endedAt: null, calls: [], usage: [], turns: [] };
105
+ if (!["claude-code", "codex"].includes(agent) || !fs.existsSync(file))
106
+ return null;
107
+ const trail = {
108
+ agent,
109
+ sessionId: null,
110
+ cwd: null,
111
+ brief: null,
112
+ startedAt: null,
113
+ endedAt: null,
114
+ calls: [],
115
+ usage: [],
116
+ turns: [],
117
+ };
58
118
  const byId = new Map();
59
119
  const add = (at, tool, input, id) => {
60
- const text = typeof input === "string" ? input : JSON.stringify(input ?? {});
61
- const command = typeof input?.command === "string" ? input.command : Array.isArray(input?.command) ? input.command.join(" ") : typeof input?.cmd === "string" ? input.cmd
62
- : [...text.matchAll(/cmd\s*:\s*"((?:[^"\\]|\\.)*)"/g)].map((match) => match[1]).join("\n") || null;
63
- const call = { at, tool, input: text.slice(0, 4_000), named: pathTokens(text), edited: editedPaths(tool, input), command, exitCode: null, error: false, output: "" };
120
+ const text =
121
+ typeof input === "string" ? input : JSON.stringify(input ?? {});
122
+ const command =
123
+ typeof input?.command === "string"
124
+ ? input.command
125
+ : Array.isArray(input?.command)
126
+ ? input.command.join(" ")
127
+ : typeof input?.cmd === "string"
128
+ ? input.cmd
129
+ : [...text.matchAll(/cmd\s*:\s*"((?:[^"\\]|\\.)*)"/g)]
130
+ .map((match) => match[1])
131
+ .join("\n") || null;
132
+ const call = {
133
+ at,
134
+ tool,
135
+ input: text.slice(0, 4_000),
136
+ named: pathTokens(text),
137
+ edited: editedPaths(tool, input),
138
+ command,
139
+ exitCode: null,
140
+ error: false,
141
+ output: "",
142
+ };
64
143
  trail.calls.push(call);
65
144
  if (id) byId.set(id, call);
66
145
  };
@@ -68,75 +147,189 @@ export async function readTrail(agent, file) {
68
147
  const call = byId.get(id);
69
148
  if (!call) return;
70
149
  call.output = String(text).slice(0, 4_000);
71
- call.named = [...new Set([...call.named, ...pathTokens(String(text).slice(0, 20_000))])];
150
+ call.named = [
151
+ ...new Set([...call.named, ...pathTokens(String(text).slice(0, 20_000))]),
152
+ ];
72
153
  call.exitCode = exitCodeOf(text);
73
- call.error = Boolean(error) || (call.exitCode !== null && call.exitCode !== 0);
154
+ call.error =
155
+ Boolean(error) || (call.exitCode !== null && call.exitCode !== 0);
74
156
  };
75
- const lines = readline.createInterface({ input: fs.createReadStream(file, { encoding: "utf8" }), crlfDelay: Infinity });
157
+ const lines = readline.createInterface({
158
+ input: fs.createReadStream(file, { encoding: "utf8" }),
159
+ crlfDelay: Infinity,
160
+ });
76
161
  for await (const line of lines) {
77
162
  const row = parseJson(line);
78
163
  if (!row) continue;
79
164
  const at = ms(row.timestamp ?? row.payload?.timestamp);
80
- if (at) { trail.startedAt ??= at; trail.endedAt = at; }
165
+ if (at) {
166
+ trail.startedAt ??= at;
167
+ trail.endedAt = at;
168
+ }
81
169
  if (agent === "claude-code") {
82
170
  trail.sessionId ??= row.sessionId ?? null;
83
171
  trail.cwd ??= row.cwd ?? null;
84
172
  const content = row.message?.content;
85
173
  if (row.type === "user" && !row.isMeta) {
86
- const words = (typeof content === "string" ? content : Array.isArray(content) && !content.some((part) => part?.type === "tool_result") ? textOf(content) : "").trim();
174
+ const words = (
175
+ typeof content === "string"
176
+ ? content
177
+ : Array.isArray(content) &&
178
+ !content.some((part) => part?.type === "tool_result")
179
+ ? textOf(content)
180
+ : ""
181
+ ).trim();
87
182
  // The person's turns: each message starts one; Esc leaves "[Request interrupted by user...]" where it stopped one.
88
- if (words.startsWith("[Request interrupted by user")) trail.turns.push({ at, interrupted: true });
89
- else if (words && !BRIEF_SKIP.test(words)) { trail.turns.push({ at, interrupted: false }); trail.brief ??= words; }
90
- }
91
- if (Array.isArray(content)) for (const part of content) {
92
- if (part?.type === "tool_use") add(at, part.name, part.input, part.id);
93
- if (part?.type === "tool_result") result(part.tool_use_id, textOf(part.content) || JSON.stringify(row.toolUseResult ?? ""), part.is_error);
183
+ if (words.startsWith("[Request interrupted by user"))
184
+ trail.turns.push({ at, interrupted: true });
185
+ else if (words && !BRIEF_SKIP.test(words)) {
186
+ trail.turns.push({ at, interrupted: false });
187
+ trail.brief ??= words;
188
+ }
94
189
  }
190
+ if (Array.isArray(content))
191
+ for (const part of content) {
192
+ if (part?.type === "tool_use")
193
+ add(at, part.name, part.input, part.id);
194
+ if (part?.type === "tool_result")
195
+ result(
196
+ part.tool_use_id,
197
+ textOf(part.content) || JSON.stringify(row.toolUseResult ?? ""),
198
+ part.is_error
199
+ );
200
+ }
95
201
  const usage = row.type === "assistant" ? row.message?.usage : null;
96
- if (usage) trail.usage.push({ at, input: (usage.input_tokens || 0) + (usage.cache_creation_input_tokens || 0) + (usage.cache_read_input_tokens || 0), cached: usage.cache_read_input_tokens || 0, output: usage.output_tokens || 0 });
202
+ if (usage)
203
+ trail.usage.push({
204
+ at,
205
+ input:
206
+ (usage.input_tokens || 0) +
207
+ (usage.cache_creation_input_tokens || 0) +
208
+ (usage.cache_read_input_tokens || 0),
209
+ cached: usage.cache_read_input_tokens || 0,
210
+ output: usage.output_tokens || 0,
211
+ });
97
212
  } else {
98
213
  const payload = row.payload ?? {};
99
- if (row.type === "session_meta") { trail.sessionId ??= payload.id ?? null; trail.cwd ??= payload.cwd ?? null; }
100
- if (row.type === "response_item" && payload.type === "message" && payload.role === "user") {
214
+ if (row.type === "session_meta") {
215
+ trail.sessionId ??= payload.id ?? null;
216
+ trail.cwd ??= payload.cwd ?? null;
217
+ }
218
+ if (
219
+ row.type === "response_item" &&
220
+ payload.type === "message" &&
221
+ payload.role === "user"
222
+ ) {
101
223
  const words = textOf(payload.content).trim();
102
- if (words && !/^<(?:environment_context|user_instructions|permissions|turn_aborted)/.test(words) && !words.startsWith("# AGENTS.md")) { trail.turns.push({ at, interrupted: false }); trail.brief ??= words; }
224
+ if (
225
+ words &&
226
+ !/^<(?:environment_context|user_instructions|permissions|turn_aborted)/.test(
227
+ words
228
+ ) &&
229
+ !words.startsWith("# AGENTS.md")
230
+ ) {
231
+ trail.turns.push({ at, interrupted: false });
232
+ trail.brief ??= words;
233
+ }
103
234
  }
104
- if (row.type === "event_msg" && payload.type === "turn_aborted" && payload.reason === "interrupted") trail.turns.push({ at, interrupted: true });
105
- if (row.type === "response_item" && ["function_call", "custom_tool_call", "local_shell_call"].includes(payload.type)) {
106
- const input = payload.input ?? parseJson(payload.arguments) ?? payload.arguments ?? payload.action;
235
+ if (
236
+ row.type === "event_msg" &&
237
+ payload.type === "turn_aborted" &&
238
+ payload.reason === "interrupted"
239
+ )
240
+ trail.turns.push({ at, interrupted: true });
241
+ if (
242
+ row.type === "response_item" &&
243
+ ["function_call", "custom_tool_call", "local_shell_call"].includes(
244
+ payload.type
245
+ )
246
+ ) {
247
+ const input =
248
+ payload.input ??
249
+ parseJson(payload.arguments) ??
250
+ payload.arguments ??
251
+ payload.action;
107
252
  add(at, payload.name ?? payload.type, input, payload.call_id);
108
253
  }
109
- if (row.type === "response_item" && ["function_call_output", "custom_tool_call_output"].includes(payload.type)) {
110
- const text = typeof payload.output === "string" ? payload.output : textOf(payload.output) || JSON.stringify(payload.output ?? "");
254
+ if (
255
+ row.type === "response_item" &&
256
+ ["function_call_output", "custom_tool_call_output"].includes(
257
+ payload.type
258
+ )
259
+ ) {
260
+ const text =
261
+ typeof payload.output === "string"
262
+ ? payload.output
263
+ : textOf(payload.output) || JSON.stringify(payload.output ?? "");
111
264
  result(payload.call_id, text, false);
112
265
  }
113
- if (row.type === "token_usage_record" && payload.usage) trail.usage.push({ at, input: payload.usage.input_tokens || 0, cached: payload.usage.cached_input_tokens || 0, output: payload.usage.output_tokens || 0 });
266
+ if (row.type === "token_usage_record" && payload.usage)
267
+ trail.usage.push({
268
+ at,
269
+ input: payload.usage.input_tokens || 0,
270
+ cached: payload.usage.cached_input_tokens || 0,
271
+ output: payload.usage.output_tokens || 0,
272
+ });
114
273
  }
115
274
  }
116
275
  return trail;
117
276
  }
118
277
 
119
- const RESULT_EDIT = /(?:The file (\S+) has been (?:updated|created)|File created successfully at: (\S+?)[\s)\]])/g;
278
+ const RESULT_EDIT =
279
+ /(?:The file (\S+) has been (?:updated|created)|File created successfully at: (\S+?)[\s)\]])/g;
120
280
 
121
281
  /**
122
282
  * A trail rebuilt from the relay's own copy of a session (store.mjs keeps every message with tool calls clipped), for
123
283
  * sessions whose source file the agent has since deleted. It has the brief, the calls with the paths they named and the
124
284
  * files they changed (the edit tools' own confirmations carry the whole path), but no token counts or timestamps.
125
285
  */
126
- export function trailFromTranscript(agent, transcript, { cwd = null, startedAt = null } = {}) {
127
- const trail = { agent, sessionId: null, cwd, brief: null, startedAt, endedAt: startedAt, calls: [], usage: [] };
286
+ export function trailFromTranscript(
287
+ agent,
288
+ transcript,
289
+ { cwd = null, startedAt = null } = {}
290
+ ) {
291
+ const trail = {
292
+ agent,
293
+ sessionId: null,
294
+ cwd,
295
+ brief: null,
296
+ startedAt,
297
+ endedAt: startedAt,
298
+ calls: [],
299
+ usage: [],
300
+ };
128
301
  for (const block of String(transcript).split(/\n\n(?=(?:User|Agent): )/)) {
129
302
  const user = block.startsWith("User: ");
130
303
  const text = block.replace(/^(?:User|Agent): /, "");
131
- if (user && !trail.brief && !text.startsWith("[tool result") && !BRIEF_SKIP.test(text.trim()) && !text.includes("<local-command-caveat>")) trail.brief = text.trim();
304
+ if (
305
+ user &&
306
+ !trail.brief &&
307
+ !text.startsWith("[tool result") &&
308
+ !BRIEF_SKIP.test(text.trim()) &&
309
+ !text.includes("<local-command-caveat>")
310
+ )
311
+ trail.brief = text.trim();
132
312
  for (const match of text.matchAll(/\[tool: (\S+) ([\s\S]*?)\](?=\n|$)/g)) {
133
- trail.calls.push({ at: null, tool: match[1], input: match[2], named: pathTokens(match[2]), edited: editedPaths(match[1], match[2]).filter((file) => !file.endsWith("…")), command: null, exitCode: null, error: false, output: "" });
313
+ trail.calls.push({
314
+ at: null,
315
+ tool: match[1],
316
+ input: match[2],
317
+ named: pathTokens(match[2]),
318
+ edited: editedPaths(match[1], match[2]).filter(
319
+ (file) => !file.endsWith("…")
320
+ ),
321
+ command: null,
322
+ exitCode: null,
323
+ error: false,
324
+ output: "",
325
+ });
134
326
  }
135
327
  if (user && text.startsWith("[tool result")) {
136
328
  const last = trail.calls.at(-1);
137
329
  if (!last) continue;
138
330
  last.named = [...new Set([...last.named, ...pathTokens(text)])];
139
- for (const match of text.matchAll(RESULT_EDIT)) last.edited = [...new Set([...last.edited, match[1] || match[2]])];
331
+ for (const match of text.matchAll(RESULT_EDIT))
332
+ last.edited = [...new Set([...last.edited, match[1] || match[2]])];
140
333
  }
141
334
  }
142
335
  return trail;
@@ -144,9 +337,13 @@ export function trailFromTranscript(agent, transcript, { cwd = null, startedAt =
144
337
 
145
338
  /** A path as written in a call, made relative to root when it lies inside it; null when it points elsewhere. */
146
339
  export function insideRoot(token, root, cwd = root) {
147
- const absolute = path.isAbsolute(token) ? token : path.resolve(cwd || root, token);
340
+ const absolute = path.isAbsolute(token)
341
+ ? token
342
+ : path.resolve(cwd || root, token);
148
343
  const rel = path.relative(root, absolute);
149
- return rel && !rel.startsWith("..") && !path.isAbsolute(rel) ? rel.split(path.sep).join("/") : null;
344
+ return rel && !rel.startsWith("..") && !path.isAbsolute(rel)
345
+ ? rel.split(path.sep).join("/")
346
+ : null;
150
347
  }
151
348
 
152
349
  /**
@@ -155,7 +352,10 @@ export function insideRoot(token, root, cwd = root) {
155
352
  * counts the calls and tokens spent before that edit, which is what a better brief can save.
156
353
  */
157
354
  export function needs(trail, root, files) {
158
- const known = (token) => { const rel = insideRoot(token, root, trail.cwd); return rel && files.has(rel) ? rel : null; };
355
+ const known = (token) => {
356
+ const rel = insideRoot(token, root, trail.cwd);
357
+ return rel && files.has(rel) ? rel : null;
358
+ };
159
359
  const edited = new Set();
160
360
  const read = new Set();
161
361
  let firstEditAt = null;
@@ -163,15 +363,39 @@ export function needs(trail, root, files) {
163
363
  let callsBefore = 0;
164
364
  for (const call of trail.calls) {
165
365
  const changed = call.edited.map(known).filter(Boolean);
166
- if (changed.length && !editing) { editing = true; firstEditAt = call.at; }
366
+ if (changed.length && !editing) {
367
+ editing = true;
368
+ firstEditAt = call.at;
369
+ }
167
370
  for (const file of changed) edited.add(file);
168
- if (!editing) { callsBefore += 1; for (const token of call.named) { const file = known(token); if (file) read.add(file); } }
371
+ if (!editing) {
372
+ callsBefore += 1;
373
+ for (const token of call.named) {
374
+ const file = known(token);
375
+ if (file) read.add(file);
376
+ }
377
+ }
169
378
  }
170
- const before = trail.usage.filter((row) => !editing || firstEditAt === null || (row.at ?? 0) < firstEditAt);
379
+ const before = trail.usage.filter(
380
+ (row) => !editing || firstEditAt === null || (row.at ?? 0) < firstEditAt
381
+ );
171
382
  return {
172
- edited: [...edited], read: [...read], firstEditAt,
173
- beforeFirstEdit: { calls: callsBefore, modelCalls: before.length, inputTokens: before.reduce((sum, row) => sum + row.input, 0),
174
- uncachedInputTokens: before.reduce((sum, row) => sum + Math.max(0, row.input - row.cached), 0), outputTokens: before.reduce((sum, row) => sum + row.output, 0),
175
- ms: firstEditAt !== null && trail.startedAt !== null ? firstEditAt - trail.startedAt : null },
383
+ edited: [...edited],
384
+ read: [...read],
385
+ firstEditAt,
386
+ beforeFirstEdit: {
387
+ calls: callsBefore,
388
+ modelCalls: before.length,
389
+ inputTokens: before.reduce((sum, row) => sum + row.input, 0),
390
+ uncachedInputTokens: before.reduce(
391
+ (sum, row) => sum + Math.max(0, row.input - row.cached),
392
+ 0
393
+ ),
394
+ outputTokens: before.reduce((sum, row) => sum + row.output, 0),
395
+ ms:
396
+ firstEditAt !== null && trail.startedAt !== null
397
+ ? firstEditAt - trail.startedAt
398
+ : null,
399
+ },
176
400
  };
177
401
  }
package/lib/ui.mjs CHANGED
@@ -6,9 +6,17 @@ import pc from "picocolors";
6
6
  // menus, spinners while waiting. createUi draws it; scriptedUi answers from a list and keeps what a person would have
7
7
  // seen, for tests. Prompts return null when the person cancels (Ctrl-C), so setup can skip that part and go on.
8
8
 
9
- const unicode = process.platform !== "win32" || Boolean(process.env.WT_SESSION) || process.env.TERM_PROGRAM === "vscode";
9
+ const unicode =
10
+ process.platform !== "win32" ||
11
+ Boolean(process.env.WT_SESSION) ||
12
+ process.env.TERM_PROGRAM === "vscode";
10
13
  const glyph = (fancy, plain) => (unicode ? fancy : plain);
11
- const BAR = glyph("│", "|"), END = glyph("└", "—"), ACTIVE = glyph("◆", "*"), DONE = glyph("◇", "o"), STOP = glyph("■", "x"), WARN = glyph("▲", "!");
14
+ const BAR = glyph("│", "|"),
15
+ END = glyph("└", "—"),
16
+ ACTIVE = glyph("◆", "*"),
17
+ DONE = glyph("◇", "o"),
18
+ STOP = glyph("■", "x"),
19
+ WARN = glyph("▲", "!");
12
20
 
13
21
  const visible = (text) => text.replace(/\x1b\[[0-9;]*m/g, "").length;
14
22
 
@@ -16,10 +24,15 @@ const visible = (text) => text.replace(/\x1b\[[0-9;]*m/g, "").length;
16
24
  export function wrap(text, width) {
17
25
  return text.split("\n").flatMap((line) => {
18
26
  if (!line.trim()) return [""];
19
- const indent = " ".repeat(line.length - line.trimStart().length), room = Math.max(20, width - indent.length), rows = [];
27
+ const indent = " ".repeat(line.length - line.trimStart().length),
28
+ room = Math.max(20, width - indent.length),
29
+ rows = [];
20
30
  let row = "";
21
31
  for (const word of line.trim().split(" ")) {
22
- if (row && visible(row) + 1 + visible(word) > room) { rows.push(row); row = word; } else row = row ? `${row} ${word}` : word;
32
+ if (row && visible(row) + 1 + visible(word) > room) {
33
+ rows.push(row);
34
+ row = word;
35
+ } else row = row ? `${row} ${word}` : word;
23
36
  }
24
37
  return [...rows, row].map((part) => `${indent}${part}`);
25
38
  });
@@ -33,16 +46,24 @@ export const columns = () => process.stdout.columns || 100;
33
46
  async function waitOrType({ message, placeholder, until }) {
34
47
  const stop = new AbortController();
35
48
  let done = null;
36
- void until.then((result) => { done = result; stop.abort(); });
49
+ void until.then((result) => {
50
+ done = result;
51
+ stop.abort();
52
+ });
37
53
  const value = await new TextPrompt({
38
54
  placeholder,
39
55
  signal: stop.signal,
40
56
  render() {
41
57
  const head = (symbol) => `${pc.gray(BAR)}\n${symbol} ${message}\n`;
42
- if (this.state === "submit") return `${head(pc.green(DONE))}${pc.gray(BAR)} ${pc.dim(this.value || "keep waiting")}`;
43
- if (this.state === "cancel" && done) return `${pc.gray(BAR)}\n${done.ok ? pc.green(DONE) : pc.yellow(WARN)} ${done.line}`;
44
- if (this.state === "cancel") return `${head(pc.red(STOP))}${pc.gray(BAR)} ${pc.dim("stopped")}`;
45
- const shown = this.value ? this.valueWithCursor : `${pc.inverse(placeholder[0])}${pc.dim(placeholder.slice(1))}`;
58
+ if (this.state === "submit")
59
+ return `${head(pc.green(DONE))}${pc.gray(BAR)} ${pc.dim(this.value || "keep waiting")}`;
60
+ if (this.state === "cancel" && done)
61
+ return `${pc.gray(BAR)}\n${done.ok ? pc.green(DONE) : pc.yellow(WARN)} ${done.line}`;
62
+ if (this.state === "cancel")
63
+ return `${head(pc.red(STOP))}${pc.gray(BAR)} ${pc.dim("stopped")}`;
64
+ const shown = this.value
65
+ ? this.valueWithCursor
66
+ : `${pc.inverse(placeholder[0])}${pc.dim(placeholder.slice(1))}`;
46
67
  return `${head(pc.cyan(ACTIVE))}${pc.cyan(BAR)} ${shown}\n${pc.cyan(END)}\n`;
47
68
  },
48
69
  }).prompt();
@@ -51,7 +72,9 @@ async function waitOrType({ message, placeholder, until }) {
51
72
  return { value: String(value ?? "").trim() };
52
73
  }
53
74
 
54
- export function createUi({ interactive = Boolean(process.stdin.isTTY && process.stdout.isTTY) } = {}) {
75
+ export function createUi({
76
+ interactive = Boolean(process.stdin.isTTY && process.stdout.isTTY),
77
+ } = {}) {
55
78
  const answer = (value) => (p.isCancel(value) ? null : value);
56
79
  // clack does not wrap; a long line would run back to the left edge, off the rail.
57
80
  const fit = (text) => wrap(text, columns() - 4).join("\n");
@@ -72,12 +95,24 @@ export function createUi({ interactive = Boolean(process.stdin.isTTY && process.
72
95
  // clack's spinner stops on Ctrl-C but keeps the process alive; setup ends there instead. Without a terminal (piped
73
96
  // or logged) it is two plain lines, not animation frames.
74
97
  spinner: () => {
75
- if (!process.stdout.isTTY) return { start: (text) => p.log.message(fit(text)), message: () => {}, stop: (text, code) => (code ? p.log.warn : p.log.step)(fit(text)) };
76
- const spin = p.spinner(), quit = () => process.exit(130);
98
+ if (!process.stdout.isTTY)
99
+ return {
100
+ start: (text) => p.log.message(fit(text)),
101
+ message: () => {},
102
+ stop: (text, code) => (code ? p.log.warn : p.log.step)(fit(text)),
103
+ };
104
+ const spin = p.spinner(),
105
+ quit = () => process.exit(130);
77
106
  return {
78
- start: (text) => { spin.start(text); process.once("SIGINT", quit); },
107
+ start: (text) => {
108
+ spin.start(text);
109
+ process.once("SIGINT", quit);
110
+ },
79
111
  message: (text) => spin.message(text),
80
- stop: (text, code) => { process.removeListener("SIGINT", quit); spin.stop(text, code); },
112
+ stop: (text, code) => {
113
+ process.removeListener("SIGINT", quit);
114
+ spin.stop(text, code);
115
+ },
81
116
  };
82
117
  },
83
118
  waitOrType,
@@ -86,19 +121,56 @@ export function createUi({ interactive = Boolean(process.stdin.isTTY && process.
86
121
 
87
122
  /** The same interface answered from `answers` in order (undefined takes the default); `lines` is what was shown. */
88
123
  export function scriptedUi(answers = [], lines = []) {
89
- const say = (symbol) => (text) => { lines.push(...`${symbol} ${text}`.split("\n")); };
90
- const next = (fallback) => { const value = answers.shift(); return value === undefined ? fallback : value; };
91
- const ask = async (opts) => { lines.push(`? ${opts.message}`); const value = next(null); if (value !== null) lines.push(` ${value}`); return value; };
124
+ const say = (symbol) => (text) => {
125
+ lines.push(...`${symbol} ${text}`.split("\n"));
126
+ };
127
+ const next = (fallback) => {
128
+ const value = answers.shift();
129
+ return value === undefined ? fallback : value;
130
+ };
131
+ const ask = async (opts) => {
132
+ lines.push(`? ${opts.message}`);
133
+ const value = next(null);
134
+ if (value !== null) lines.push(` ${value}`);
135
+ return value;
136
+ };
92
137
  return {
93
138
  interactive: true,
94
139
  lines,
95
- intro: say("┌"), outro: say("└"), step: say("◇"), success: say("◆"), info: say("●"), warn: say("▲"), message: say("│"),
96
- note: (text, title) => { lines.push(`[${title}]`, ...text.split("\n")); },
97
- select: async (opts) => { lines.push(`? ${opts.message}`, ...opts.options.map((option) => ` ${option.label}${option.hint ? ` (${option.hint})` : ""}`)); return next(opts.initialValue ?? opts.options[0].value); },
140
+ intro: say("┌"),
141
+ outro: say("└"),
142
+ step: say("◇"),
143
+ success: say("◆"),
144
+ info: say("●"),
145
+ warn: say("▲"),
146
+ message: say("│"),
147
+ note: (text, title) => {
148
+ lines.push(`[${title}]`, ...text.split("\n"));
149
+ },
150
+ select: async (opts) => {
151
+ lines.push(
152
+ `? ${opts.message}`,
153
+ ...opts.options.map(
154
+ (option) =>
155
+ ` ${option.label}${option.hint ? ` (${option.hint})` : ""}`
156
+ )
157
+ );
158
+ return next(opts.initialValue ?? opts.options[0].value);
159
+ },
98
160
  text: ask,
99
- password: async (opts) => { lines.push(`? ${opts.message}`); return next(null); },
100
- confirm: async (opts) => { lines.push(`? ${opts.message}`); return next(opts.initialValue ?? true); },
101
- spinner: () => ({ start: say("◒"), message: () => {}, stop: (text, code = 0) => say(code ? "▲" : "◇")(text) }),
161
+ password: async (opts) => {
162
+ lines.push(`? ${opts.message}`);
163
+ return next(null);
164
+ },
165
+ confirm: async (opts) => {
166
+ lines.push(`? ${opts.message}`);
167
+ return next(opts.initialValue ?? true);
168
+ },
169
+ spinner: () => ({
170
+ start: say("◒"),
171
+ message: () => {},
172
+ stop: (text, code = 0) => say(code ? "▲" : "◇")(text),
173
+ }),
102
174
  waitOrType: async ({ message, until }) => {
103
175
  lines.push(`? ${message}`);
104
176
  const value = next("");
package/lib/vectors.mjs CHANGED
@@ -3,7 +3,8 @@
3
3
  // entries, not millions, so a scan is the right index until one outgrows it.
4
4
 
5
5
  export function cosine(a, b) {
6
- if (a.length !== b.length) throw new Error(`vector lengths differ: ${a.length} and ${b.length}`);
6
+ if (a.length !== b.length)
7
+ throw new Error(`vector lengths differ: ${a.length} and ${b.length}`);
7
8
  let dot = 0;
8
9
  let na = 0;
9
10
  let nb = 0;
@@ -36,7 +37,9 @@ export function toBlob(vector) {
36
37
 
37
38
  /** Buffer (or Uint8Array) from SQLite -> Float32Array (a copy). */
38
39
  export function fromBlob(blob) {
39
- const bytes = Buffer.isBuffer(blob) ? blob : Buffer.from(blob.buffer, blob.byteOffset, blob.byteLength);
40
+ const bytes = Buffer.isBuffer(blob)
41
+ ? blob
42
+ : Buffer.from(blob.buffer, blob.byteOffset, blob.byteLength);
40
43
  const out = new Float32Array(bytes.length / 4);
41
44
  for (let i = 0; i < out.length; i += 1) out[i] = bytes.readFloatLE(i * 4);
42
45
  return out;