tryharness 0.7.22 → 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 (64) hide show
  1. package/README.md +48 -8
  2. package/assets/opencode-plugin.js +48 -10
  3. package/assets/pi-extension.ts +106 -26
  4. package/cli.mjs +1281 -289
  5. package/lib/activity.mjs +197 -40
  6. package/lib/adapters.mjs +337 -54
  7. package/lib/agentnode.mjs +175 -35
  8. package/lib/atlas.mjs +604 -0
  9. package/lib/bip39-english.txt +2048 -0
  10. package/lib/cards.mjs +177 -39
  11. package/lib/claudemem.mjs +74 -18
  12. package/lib/commands.mjs +13 -5
  13. package/lib/context.mjs +628 -0
  14. package/lib/control-client.mjs +5 -2
  15. package/lib/coordinator.mjs +281 -56
  16. package/lib/daemon.mjs +5590 -838
  17. package/lib/desktop.mjs +149 -35
  18. package/lib/digest.mjs +81 -0
  19. package/lib/environment.mjs +180 -0
  20. package/lib/files.mjs +246 -13
  21. package/lib/firewall.mjs +288 -0
  22. package/lib/folders.mjs +223 -57
  23. package/lib/frames.mjs +333 -0
  24. package/lib/inference.mjs +663 -0
  25. package/lib/learn.mjs +182 -0
  26. package/lib/live.mjs +286 -61
  27. package/lib/localmodels.mjs +571 -0
  28. package/lib/mcp-stdio.mjs +912 -103
  29. package/lib/media.mjs +228 -0
  30. package/lib/members.json +1 -1
  31. package/lib/members.mjs +35 -9
  32. package/lib/models.mjs +342 -80
  33. package/lib/network.mjs +201 -42
  34. package/lib/notify.mjs +49 -12
  35. package/lib/nudge.mjs +190 -32
  36. package/lib/ocr.mjs +380 -0
  37. package/lib/onboard.mjs +321 -70
  38. package/lib/parsers.mjs +132 -41
  39. package/lib/paths.mjs +33 -10
  40. package/lib/personal.mjs +77 -0
  41. package/lib/phones.mjs +398 -51
  42. package/lib/places.mjs +566 -102
  43. package/lib/provenance.mjs +19 -4
  44. package/lib/receipts.mjs +82 -0
  45. package/lib/redact.mjs +730 -0
  46. package/lib/runner.mjs +347 -55
  47. package/lib/runtime-install.mjs +59 -17
  48. package/lib/screens.mjs +212 -0
  49. package/lib/sealed.mjs +160 -28
  50. package/lib/secrets.mjs +18 -5
  51. package/lib/service.mjs +173 -35
  52. package/lib/snippets.mjs +140 -0
  53. package/lib/speech.mjs +993 -0
  54. package/lib/store.mjs +1678 -301
  55. package/lib/swarm.mjs +598 -117
  56. package/lib/t3.mjs +400 -79
  57. package/lib/trails.mjs +401 -0
  58. package/lib/ui.mjs +95 -23
  59. package/lib/vectors.mjs +5 -2
  60. package/lib/wakes.mjs +284 -68
  61. package/lib/watches.mjs +362 -0
  62. package/lib/worktrees.mjs +186 -38
  63. package/package.json +6 -4
  64. package/lib/embedder.mjs +0 -156
package/lib/activity.mjs CHANGED
@@ -32,7 +32,9 @@ const base = (token) => path.basename(String(token || "")).replace(/^-/, "");
32
32
  export function agentOf(command) {
33
33
  if (/\/@anthropic-ai\/claude-code\//.test(command)) return "claude-code";
34
34
  const [first, second] = String(command).trim().split(/\s+/);
35
- const name = RUNTIMES.has(base(first).replace(/\d+$/, "")) ? base(second) : base(first);
35
+ const name = RUNTIMES.has(base(first).replace(/\d+$/, ""))
36
+ ? base(second)
37
+ : base(first);
36
38
  return AGENT_NAMES[name] || null;
37
39
  }
38
40
 
@@ -40,16 +42,31 @@ export function agentOf(command) {
40
42
  export function parsePs(text) {
41
43
  const rows = [];
42
44
  for (const line of String(text).split("\n")) {
43
- const match = line.match(/^\s*(\d+)\s+(\d+)\s+([\d.]+)\s+(\w{3}\s+\w{3}\s+\d+\s+[\d:]+\s+\d{4})\s+(.*)$/);
45
+ const match = line.match(
46
+ /^\s*(\d+)\s+(\d+)\s+([\d.]+)\s+(\w{3}\s+\w{3}\s+\d+\s+[\d:]+\s+\d{4})\s+(.*)$/
47
+ );
44
48
  if (!match) continue;
45
49
  const startedAt = Date.parse(match[4]);
46
- if (Number.isFinite(startedAt)) rows.push({ pid: Number(match[1]), ppid: Number(match[2]), cpu: Number(match[3]), startedAt, command: match[5] });
50
+ if (Number.isFinite(startedAt))
51
+ rows.push({
52
+ pid: Number(match[1]),
53
+ ppid: Number(match[2]),
54
+ cpu: Number(match[3]),
55
+ startedAt,
56
+ command: match[5],
57
+ });
47
58
  }
48
59
  return rows;
49
60
  }
50
61
 
51
- const stamp = (value) => (typeof value === "number" ? value : Date.parse(value || "")) || null;
52
- const textOf = (content) => (typeof content === "string" ? content : Array.isArray(content) ? content.map((part) => part?.text || "").join("") : "");
62
+ const stamp = (value) =>
63
+ (typeof value === "number" ? value : Date.parse(value || "")) || null;
64
+ const textOf = (content) =>
65
+ typeof content === "string"
66
+ ? content
67
+ : Array.isArray(content)
68
+ ? content.map((part) => part?.text || "").join("")
69
+ : "";
53
70
 
54
71
  /** Whether a transcript's last turn is still open, and when it started, from its newest rows. */
55
72
  export function turnOf(agent, rows) {
@@ -57,24 +74,38 @@ export function turnOf(agent, rows) {
57
74
  const row = rows[index];
58
75
  if (agent === "codex") {
59
76
  const kind = row.type === "event_msg" ? row.payload?.type : null;
60
- if (kind === "task_started") return { open: true, startedAt: stamp(row.timestamp) };
61
- if (kind === "task_complete" || kind === "turn_aborted") return { open: false };
77
+ if (kind === "task_started")
78
+ return { open: true, startedAt: stamp(row.timestamp) };
79
+ if (kind === "task_complete" || kind === "turn_aborted")
80
+ return { open: false };
62
81
  } else if (agent === "pi") {
63
82
  const message = row.type === "message" ? row.message : null;
64
83
  if (!message) continue;
65
- if (message.role === "user") return { open: true, startedAt: stamp(row.timestamp ?? message.timestamp) };
66
- if (message.role === "assistant" && message.stopReason !== "toolUse") return { open: false };
84
+ if (message.role === "user")
85
+ return {
86
+ open: true,
87
+ startedAt: stamp(row.timestamp ?? message.timestamp),
88
+ };
89
+ if (message.role === "assistant" && message.stopReason !== "toolUse")
90
+ return { open: false };
67
91
  } else {
68
- if (row.isMeta || (row.type !== "user" && row.type !== "assistant")) continue;
92
+ if (row.isMeta || (row.type !== "user" && row.type !== "assistant"))
93
+ continue;
69
94
  const content = row.message?.content;
70
95
  if (row.type === "assistant") {
71
96
  if (row.message?.stop_reason === "end_turn") return { open: false };
72
97
  continue;
73
98
  }
74
- if (Array.isArray(content) && content.some((part) => part?.type === "tool_result")) continue;
99
+ if (
100
+ Array.isArray(content) &&
101
+ content.some((part) => part?.type === "tool_result")
102
+ )
103
+ continue;
75
104
  const text = textOf(content);
76
- if (text.startsWith("[Request interrupted by user")) return { open: false };
77
- if (text.startsWith("<local-command") || text.startsWith("<command-")) continue;
105
+ if (text.startsWith("[Request interrupted by user"))
106
+ return { open: false };
107
+ if (text.startsWith("<local-command") || text.startsWith("<command-"))
108
+ continue;
78
109
  return { open: true, startedAt: stamp(row.timestamp) };
79
110
  }
80
111
  }
@@ -91,7 +122,14 @@ function tailRows(file) {
91
122
  fs.readSync(fd, buffer, 0, length, size - length);
92
123
  const lines = buffer.toString("utf8").split("\n");
93
124
  if (length < size) lines.shift();
94
- return lines.flatMap((line) => { try { const row = JSON.parse(line); return row && typeof row === "object" ? [row] : []; } catch { return []; } });
125
+ return lines.flatMap((line) => {
126
+ try {
127
+ const row = JSON.parse(line);
128
+ return row && typeof row === "object" ? [row] : [];
129
+ } catch {
130
+ return [];
131
+ }
132
+ });
95
133
  } catch {
96
134
  return [];
97
135
  } finally {
@@ -99,24 +137,49 @@ function tailRows(file) {
99
137
  }
100
138
  }
101
139
 
102
- const stat = (file) => { try { return { file, mtime: fs.statSync(file).mtimeMs }; } catch { return null; } };
140
+ const stat = (file) => {
141
+ try {
142
+ return { file, mtime: fs.statSync(file).mtimeMs };
143
+ } catch {
144
+ return null;
145
+ }
146
+ };
103
147
 
104
- const exec = (file, args) => new Promise((resolve) => {
105
- execFile(file, args, { encoding: "utf8", maxBuffer: 16 << 20, timeout: 10_000, env: { ...process.env, LC_ALL: "C" } }, (error, stdout) => resolve(error && !stdout ? "" : stdout));
106
- });
148
+ const exec = (file, args) =>
149
+ new Promise((resolve) => {
150
+ execFile(
151
+ file,
152
+ args,
153
+ {
154
+ encoding: "utf8",
155
+ maxBuffer: 16 << 20,
156
+ timeout: 10_000,
157
+ env: { ...process.env, LC_ALL: "C" },
158
+ },
159
+ (error, stdout) => resolve(error && !stdout ? "" : stdout)
160
+ );
161
+ });
107
162
 
108
163
  // Each process's working directory: Claude Code keeps a session's transcript under it.
109
164
  async function workingDirectories(pids) {
110
165
  const found = new Map();
111
166
  if (!pids.length) return found;
112
167
  if (process.platform === "linux") {
113
- for (const pid of pids) { try { found.set(pid, fs.readlinkSync(`/proc/${pid}/cwd`)); } catch {} }
168
+ for (const pid of pids) {
169
+ try {
170
+ found.set(pid, fs.readlinkSync(`/proc/${pid}/cwd`));
171
+ } catch {}
172
+ }
114
173
  return found;
115
174
  }
116
- const lsof = ["/usr/sbin/lsof", "/usr/bin/lsof"].find((file) => fs.existsSync(file));
175
+ const lsof = ["/usr/sbin/lsof", "/usr/bin/lsof"].find((file) =>
176
+ fs.existsSync(file)
177
+ );
117
178
  if (!lsof) return found;
118
179
  let pid = null;
119
- for (const line of (await exec(lsof, ["-a", "-d", "cwd", "-p", pids.join(","), "-Fpn"])).split("\n")) {
180
+ for (const line of (
181
+ await exec(lsof, ["-a", "-d", "cwd", "-p", pids.join(","), "-Fpn"])
182
+ ).split("\n")) {
120
183
  if (line.startsWith("p")) pid = Number(line.slice(1));
121
184
  else if (line.startsWith("n") && pid) found.set(pid, line.slice(1));
122
185
  }
@@ -127,40 +190,111 @@ async function workingDirectories(pids) {
127
190
  * Judges the machine from what was read. `processes` are ps rows, `sessions` are transcripts
128
191
  * ({ agent, file, mtime, turn, owners: pids of the live agent processes it may belong to }).
129
192
  */
130
- export function judgeActivity({ processes, sessions, runs = [], queued = 0, now = Date.now() }) {
193
+ export function judgeActivity({
194
+ processes,
195
+ sessions,
196
+ runs = [],
197
+ queued = 0,
198
+ now = Date.now(),
199
+ }) {
131
200
  const children = new Map();
132
- for (const row of processes) children.set(row.ppid, [...(children.get(row.ppid) || []), row]);
201
+ for (const row of processes)
202
+ children.set(row.ppid, [...(children.get(row.ppid) || []), row]);
133
203
  const descendants = (pid) => {
134
204
  const found = [];
135
205
  const pending = [...(children.get(pid) || [])];
136
- while (pending.length) { const row = pending.pop(); found.push(row); pending.push(...(children.get(row.pid) || [])); }
206
+ while (pending.length) {
207
+ const row = pending.pop();
208
+ found.push(row);
209
+ pending.push(...(children.get(row.pid) || []));
210
+ }
137
211
  return found;
138
212
  };
139
- const reasons = runs.map((run) => ({ kind: "run", agent: run.agent, id: run.id }));
213
+ const reasons = runs.map((run) => ({
214
+ kind: "run",
215
+ agent: run.agent,
216
+ id: run.id,
217
+ }));
140
218
  if (queued) reasons.push({ kind: "queued", count: queued });
141
219
  const waiting = [];
142
220
  for (const session of sessions) {
143
221
  if (!session.turn.open) continue;
144
222
  const started = session.turn.startedAt || 0;
145
- const ranCommand = (row) => { const name = base(row.command.split(/\s+/)[0]); return session.agent === "claude-code" ? SHELLS.has(name) : !HELPERS.has(name); };
146
- const command = session.owners.flatMap(descendants).find((row) => row.startedAt >= started && ranCommand(row));
147
- if (command) reasons.push({ kind: "command", agent: session.agent, file: session.file, command: command.command.slice(0, 120) });
148
- else if (now - session.mtime < QUIET_MS) reasons.push({ kind: "turn", agent: session.agent, file: session.file });
149
- else waiting.push({ agent: session.agent, file: session.file, since: new Date(session.mtime).toISOString() });
223
+ const ranCommand = (row) => {
224
+ const name = base(row.command.split(/\s+/)[0]);
225
+ return session.agent === "claude-code"
226
+ ? SHELLS.has(name)
227
+ : !HELPERS.has(name);
228
+ };
229
+ const command = session.owners
230
+ .flatMap(descendants)
231
+ .find((row) => row.startedAt >= started && ranCommand(row));
232
+ if (command)
233
+ reasons.push({
234
+ kind: "command",
235
+ agent: session.agent,
236
+ file: session.file,
237
+ command: command.command.slice(0, 120),
238
+ });
239
+ else if (now - session.mtime < QUIET_MS)
240
+ reasons.push({ kind: "turn", agent: session.agent, file: session.file });
241
+ else
242
+ waiting.push({
243
+ agent: session.agent,
244
+ file: session.file,
245
+ since: new Date(session.mtime).toISOString(),
246
+ });
150
247
  }
151
248
  for (const agentRow of processes.filter((row) => agentOf(row.command))) {
152
249
  const hot = descendants(agentRow.pid).find((row) => row.cpu >= CPU_BUSY);
153
- if (hot) reasons.push({ kind: "background", agent: agentOf(agentRow.command), pid: agentRow.pid, command: hot.command.slice(0, 120), cpu: hot.cpu });
250
+ if (hot)
251
+ reasons.push({
252
+ kind: "background",
253
+ agent: agentOf(agentRow.command),
254
+ pid: agentRow.pid,
255
+ command: hot.command.slice(0, 120),
256
+ cpu: hot.cpu,
257
+ });
154
258
  }
155
- return { busy: reasons.length > 0, reasons, waiting, sessions: sessions.map((session) => ({ agent: session.agent, file: session.file, open: session.turn.open })), checkedAt: new Date(now).toISOString() };
259
+ return {
260
+ busy: reasons.length > 0,
261
+ reasons,
262
+ waiting,
263
+ sessions: sessions.map((session) => ({
264
+ agent: session.agent,
265
+ file: session.file,
266
+ open: session.turn.open,
267
+ })),
268
+ checkedAt: new Date(now).toISOString(),
269
+ };
156
270
  }
157
271
 
158
272
  /** Reads this machine's agent processes and transcripts and judges them. */
159
- export async function machineActivity({ runs = [], queued = 0, agents = ["claude-code", "codex", "pi"], now = Date.now() } = {}) {
160
- const processes = process.platform === "win32" ? [] : parsePs(await exec(PS, ["-axww", "-o", "pid=,ppid=,pcpu=,lstart=,command="]));
273
+ export async function machineActivity({
274
+ runs = [],
275
+ queued = 0,
276
+ agents = ["claude-code", "codex", "pi"],
277
+ now = Date.now(),
278
+ } = {}) {
279
+ const processes =
280
+ process.platform === "win32"
281
+ ? []
282
+ : parsePs(
283
+ await exec(PS, ["-axww", "-o", "pid=,ppid=,pcpu=,lstart=,command="])
284
+ );
161
285
  const live = processes.filter((row) => agents.includes(agentOf(row.command)));
162
- const claudeRecords = (pid) => historyRoots("claude-code").some((root) => fs.existsSync(path.join(root, "..", "sessions", `${pid}.json`)));
163
- const cwds = await workingDirectories(live.filter((row) => agentOf(row.command) === "claude-code" && !claudeRecords(row.pid)).map((row) => row.pid));
286
+ const claudeRecords = (pid) =>
287
+ historyRoots("claude-code").some((root) =>
288
+ fs.existsSync(path.join(root, "..", "sessions", `${pid}.json`))
289
+ );
290
+ const cwds = await workingDirectories(
291
+ live
292
+ .filter(
293
+ (row) =>
294
+ agentOf(row.command) === "claude-code" && !claudeRecords(row.pid)
295
+ )
296
+ .map((row) => row.pid)
297
+ );
164
298
  const sessions = [];
165
299
  for (const agent of agents) {
166
300
  const owners = live.filter((row) => agentOf(row.command) === agent);
@@ -176,16 +310,39 @@ export async function machineActivity({ runs = [], queued = 0, agents = ["claude
176
310
  const cwd = record?.cwd || cwds.get(row.pid);
177
311
  if (!cwd) continue;
178
312
  const folder = path.join(root, cwd.replace(/[^a-zA-Z0-9]/g, "-"));
179
- const items = record?.sessionId ? [stat(path.join(folder, `${record.sessionId}.jsonl`))].filter(Boolean) : listJsonl(folder).filter((item) => item.mtime >= row.startedAt);
180
- for (const item of items) sessions.push({ agent, file: item.file, mtime: item.mtime, turn: turnOf(agent, tailRows(item.file)), owners: [row.pid] });
313
+ const items = record?.sessionId
314
+ ? [stat(path.join(folder, `${record.sessionId}.jsonl`))].filter(
315
+ Boolean
316
+ )
317
+ : listJsonl(folder).filter((item) => item.mtime >= row.startedAt);
318
+ for (const item of items)
319
+ sessions.push({
320
+ agent,
321
+ file: item.file,
322
+ mtime: item.mtime,
323
+ turn: turnOf(agent, tailRows(item.file)),
324
+ owners: [row.pid],
325
+ });
181
326
  }
182
327
  } else {
183
328
  for (const item of listJsonl(root)) {
184
- if (item.mtime >= since) sessions.push({ agent, file: item.file, mtime: item.mtime, turn: turnOf(agent, tailRows(item.file)), owners: owners.map((row) => row.pid) });
329
+ if (item.mtime >= since)
330
+ sessions.push({
331
+ agent,
332
+ file: item.file,
333
+ mtime: item.mtime,
334
+ turn: turnOf(agent, tailRows(item.file)),
335
+ owners: owners.map((row) => row.pid),
336
+ });
185
337
  }
186
338
  }
187
339
  }
188
340
  }
189
341
  // Every agent process, whether or not it is working: the desktop app never pauses the app hosting one.
190
- return { ...judgeActivity({ processes, sessions, runs, queued, now }), agentPids: processes.filter((row) => agentOf(row.command)).map((row) => row.pid) };
342
+ return {
343
+ ...judgeActivity({ processes, sessions, runs, queued, now }),
344
+ agentPids: processes
345
+ .filter((row) => agentOf(row.command))
346
+ .map((row) => row.pid),
347
+ };
191
348
  }