tryharness 0.7.23 → 0.7.26
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.
- package/assets/opencode-plugin.js +48 -11
- package/assets/pi-extension.ts +105 -26
- package/cli.mjs +1238 -322
- package/lib/activity.mjs +197 -40
- package/lib/adapters.mjs +337 -54
- package/lib/agentnode.mjs +175 -35
- package/lib/atlas.mjs +383 -83
- package/lib/cards.mjs +174 -40
- package/lib/claudemem.mjs +74 -18
- package/lib/commands.mjs +13 -5
- package/lib/context.mjs +409 -71
- package/lib/control-client.mjs +5 -2
- package/lib/coordinator.mjs +274 -57
- package/lib/daemon.mjs +5268 -1024
- package/lib/desktop.mjs +149 -35
- package/lib/digest.mjs +54 -13
- package/lib/environment.mjs +103 -21
- package/lib/files.mjs +246 -13
- package/lib/firewall.mjs +169 -35
- package/lib/folders.mjs +179 -36
- package/lib/frames.mjs +214 -40
- package/lib/inference.mjs +463 -126
- package/lib/learn.mjs +78 -16
- package/lib/live.mjs +286 -61
- package/lib/localmodels.mjs +424 -83
- package/lib/mcp-stdio.mjs +868 -151
- package/lib/media.mjs +138 -28
- package/lib/members.json +1 -1
- package/lib/members.mjs +35 -9
- package/lib/models.mjs +342 -80
- package/lib/network.mjs +201 -42
- package/lib/notify.mjs +49 -12
- package/lib/nudge.mjs +184 -33
- package/lib/ocr.mjs +223 -60
- package/lib/onboard.mjs +321 -70
- package/lib/parsers.mjs +132 -41
- package/lib/paths.mjs +33 -10
- package/lib/personal.mjs +51 -13
- package/lib/phones.mjs +384 -60
- package/lib/places.mjs +555 -107
- package/lib/provenance.mjs +19 -4
- package/lib/receipts.mjs +51 -11
- package/lib/redact.mjs +512 -116
- package/lib/runner.mjs +345 -55
- package/lib/runtime-install.mjs +55 -14
- package/lib/screens.mjs +154 -38
- package/lib/sealed.mjs +157 -28
- package/lib/secrets.mjs +18 -5
- package/lib/service.mjs +170 -34
- package/lib/snippets.mjs +94 -18
- package/lib/speech.mjs +688 -175
- package/lib/store.mjs +1654 -307
- package/lib/swarm.mjs +596 -117
- package/lib/t3.mjs +400 -79
- package/lib/trails.mjs +285 -61
- package/lib/ui.mjs +95 -23
- package/lib/vectors.mjs +5 -2
- package/lib/wakes.mjs +284 -68
- package/lib/watches.mjs +247 -48
- package/lib/worktrees.mjs +186 -38
- 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 =
|
|
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 =
|
|
13
|
-
|
|
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 =
|
|
16
|
-
|
|
17
|
-
const
|
|
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) => {
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
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) || [])]
|
|
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")
|
|
33
|
-
|
|
34
|
-
if (EDIT_TOOLS.has(tool) && typeof input
|
|
35
|
-
|
|
36
|
-
|
|
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))
|
|
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(
|
|
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))
|
|
57
|
-
|
|
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 =
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
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 = [
|
|
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 =
|
|
154
|
+
call.error =
|
|
155
|
+
Boolean(error) || (call.exitCode !== null && call.exitCode !== 0);
|
|
74
156
|
};
|
|
75
|
-
const lines = readline.createInterface({
|
|
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) {
|
|
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 = (
|
|
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"))
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
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)
|
|
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") {
|
|
100
|
-
|
|
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 (
|
|
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 (
|
|
105
|
-
|
|
106
|
-
|
|
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 (
|
|
110
|
-
|
|
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)
|
|
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 =
|
|
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(
|
|
127
|
-
|
|
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 (
|
|
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({
|
|
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))
|
|
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)
|
|
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)
|
|
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) => {
|
|
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) {
|
|
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) {
|
|
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(
|
|
379
|
+
const before = trail.usage.filter(
|
|
380
|
+
(row) => !editing || firstEditAt === null || (row.at ?? 0) < firstEditAt
|
|
381
|
+
);
|
|
171
382
|
return {
|
|
172
|
-
edited: [...edited],
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
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 =
|
|
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("│", "|"),
|
|
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),
|
|
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) {
|
|
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) => {
|
|
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")
|
|
43
|
-
|
|
44
|
-
if (this.state === "cancel"
|
|
45
|
-
|
|
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({
|
|
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)
|
|
76
|
-
|
|
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) => {
|
|
107
|
+
start: (text) => {
|
|
108
|
+
spin.start(text);
|
|
109
|
+
process.once("SIGINT", quit);
|
|
110
|
+
},
|
|
79
111
|
message: (text) => spin.message(text),
|
|
80
|
-
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) => {
|
|
90
|
-
|
|
91
|
-
|
|
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("┌"),
|
|
96
|
-
|
|
97
|
-
|
|
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) => {
|
|
100
|
-
|
|
101
|
-
|
|
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)
|
|
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)
|
|
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;
|