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.
- package/README.md +48 -8
- package/assets/opencode-plugin.js +48 -10
- package/assets/pi-extension.ts +106 -26
- package/cli.mjs +1281 -289
- package/lib/activity.mjs +197 -40
- package/lib/adapters.mjs +337 -54
- package/lib/agentnode.mjs +175 -35
- package/lib/atlas.mjs +604 -0
- package/lib/bip39-english.txt +2048 -0
- package/lib/cards.mjs +177 -39
- package/lib/claudemem.mjs +74 -18
- package/lib/commands.mjs +13 -5
- package/lib/context.mjs +628 -0
- package/lib/control-client.mjs +5 -2
- package/lib/coordinator.mjs +281 -56
- package/lib/daemon.mjs +5590 -838
- package/lib/desktop.mjs +149 -35
- package/lib/digest.mjs +81 -0
- package/lib/environment.mjs +180 -0
- package/lib/files.mjs +246 -13
- package/lib/firewall.mjs +288 -0
- package/lib/folders.mjs +223 -57
- package/lib/frames.mjs +333 -0
- package/lib/inference.mjs +663 -0
- package/lib/learn.mjs +182 -0
- package/lib/live.mjs +286 -61
- package/lib/localmodels.mjs +571 -0
- package/lib/mcp-stdio.mjs +912 -103
- package/lib/media.mjs +228 -0
- 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 +190 -32
- package/lib/ocr.mjs +380 -0
- package/lib/onboard.mjs +321 -70
- package/lib/parsers.mjs +132 -41
- package/lib/paths.mjs +33 -10
- package/lib/personal.mjs +77 -0
- package/lib/phones.mjs +398 -51
- package/lib/places.mjs +566 -102
- package/lib/provenance.mjs +19 -4
- package/lib/receipts.mjs +82 -0
- package/lib/redact.mjs +730 -0
- package/lib/runner.mjs +347 -55
- package/lib/runtime-install.mjs +59 -17
- package/lib/screens.mjs +212 -0
- package/lib/sealed.mjs +160 -28
- package/lib/secrets.mjs +18 -5
- package/lib/service.mjs +173 -35
- package/lib/snippets.mjs +140 -0
- package/lib/speech.mjs +993 -0
- package/lib/store.mjs +1678 -301
- package/lib/swarm.mjs +598 -117
- package/lib/t3.mjs +400 -79
- package/lib/trails.mjs +401 -0
- package/lib/ui.mjs +95 -23
- package/lib/vectors.mjs +5 -2
- package/lib/wakes.mjs +284 -68
- package/lib/watches.mjs +362 -0
- package/lib/worktrees.mjs +186 -38
- package/package.json +6 -4
- 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+$/, ""))
|
|
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(
|
|
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))
|
|
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) =>
|
|
52
|
-
|
|
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")
|
|
61
|
-
|
|
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")
|
|
66
|
-
|
|
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"))
|
|
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 (
|
|
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"))
|
|
77
|
-
|
|
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) => {
|
|
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) => {
|
|
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) =>
|
|
105
|
-
|
|
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) {
|
|
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) =>
|
|
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 (
|
|
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({
|
|
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)
|
|
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) {
|
|
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) => ({
|
|
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) => {
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
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)
|
|
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 {
|
|
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({
|
|
160
|
-
|
|
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) =>
|
|
163
|
-
|
|
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
|
|
180
|
-
|
|
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)
|
|
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 {
|
|
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
|
}
|