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.
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 +5268 -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 +384 -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/watches.mjs CHANGED
@@ -14,7 +14,11 @@ const USUAL_AFTER = 12;
14
14
  const MIN_EVERY_SECONDS = 5;
15
15
  const RUN_OUT_HOURS = 24;
16
16
 
17
- const median = (values) => { const sorted = [...values].sort((a, b) => a - b); const mid = sorted.length >> 1; return sorted.length % 2 ? sorted[mid] : (sorted[mid - 1] + sorted[mid]) / 2; };
17
+ const median = (values) => {
18
+ const sorted = [...values].sort((a, b) => a - b);
19
+ const mid = sorted.length >> 1;
20
+ return sorted.length % 2 ? sorted[mid] : (sorted[mid - 1] + sorted[mid]) / 2;
21
+ };
18
22
  const escape = (text) => text.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
19
23
 
20
24
  /**
@@ -24,15 +28,38 @@ const escape = (text) => text.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
24
28
  */
25
29
  export function readNumber(output, measure, now = Date.now()) {
26
30
  const text = String(output ?? "");
27
- if (measure.count) return text.split("\n").filter((line) => line.toLowerCase().includes(String(measure.count).toLowerCase())).length;
31
+ if (measure.count)
32
+ return text
33
+ .split("\n")
34
+ .filter((line) =>
35
+ line.toLowerCase().includes(String(measure.count).toLowerCase())
36
+ ).length;
28
37
  if (measure.age) {
29
- const found = [...text.matchAll(new RegExp(`["']?${escape(measure.age)}["']?\\s*[:=]\\s*"?([0-9][0-9:.TZ+-]*)`, "g"))].at(-1)?.[1];
38
+ const found = [
39
+ ...text.matchAll(
40
+ new RegExp(
41
+ `["']?${escape(measure.age)}["']?\\s*[:=]\\s*"?([0-9][0-9:.TZ+-]*)`,
42
+ "g"
43
+ )
44
+ ),
45
+ ].at(-1)?.[1];
30
46
  if (!found) return null;
31
- const at = /^\d+(?:\.\d+)?$/.test(found) ? Number(found) * (Number(found) < 1e12 ? 1000 : 1) : Date.parse(found);
32
- return Number.isFinite(at) ? Math.max(0, Math.round((now - at) / 1000)) : null;
47
+ const at = /^\d+(?:\.\d+)?$/.test(found)
48
+ ? Number(found) * (Number(found) < 1e12 ? 1000 : 1)
49
+ : Date.parse(found);
50
+ return Number.isFinite(at)
51
+ ? Math.max(0, Math.round((now - at) / 1000))
52
+ : null;
33
53
  }
34
54
  if (measure.field) {
35
- const found = [...text.matchAll(new RegExp(`["']?${escape(measure.field)}["']?\\s*[:=]\\s*"?(-?\\d+(?:\\.\\d+)?)`, "g"))].at(-1);
55
+ const found = [
56
+ ...text.matchAll(
57
+ new RegExp(
58
+ `["']?${escape(measure.field)}["']?\\s*[:=]\\s*"?(-?\\d+(?:\\.\\d+)?)`,
59
+ "g"
60
+ )
61
+ ),
62
+ ].at(-1);
36
63
  return found ? Number(found[1]) : null;
37
64
  }
38
65
  const found = text.match(/-?\d+(?:\.\d+)?(?=\D*$)/);
@@ -44,21 +71,36 @@ export function readNumber(output, measure, now = Date.now()) {
44
71
  * its history, once it has some), or, falling toward its floor, due to cross it within a day. null when it is fine.
45
72
  */
46
73
  export function offPattern(samples, { above = null, below = null } = {}) {
47
- const values = samples.map((sample) => sample.value).filter((value) => value !== null && Number.isFinite(value));
74
+ const values = samples
75
+ .map((sample) => sample.value)
76
+ .filter((value) => value !== null && Number.isFinite(value));
48
77
  const latest = samples.at(-1);
49
- if (!latest || latest.value === null) return latest?.error ? { kind: "failed", text: `its check failed: ${latest.error}` } : null;
50
- if (above !== null && latest.value > above) return { kind: "limit", text: `is ${latest.value}, above ${above}` };
51
- if (below !== null && latest.value < below) return { kind: "limit", text: `is ${latest.value}, below ${below}` };
78
+ if (!latest || latest.value === null)
79
+ return latest?.error
80
+ ? { kind: "failed", text: `its check failed: ${latest.error}` }
81
+ : null;
82
+ if (above !== null && latest.value > above)
83
+ return { kind: "limit", text: `is ${latest.value}, above ${above}` };
84
+ if (below !== null && latest.value < below)
85
+ return { kind: "limit", text: `is ${latest.value}, below ${below}` };
52
86
  if (below !== null && samples.length >= 4) {
53
87
  // Falling toward the floor: a straight line through the recent samples, and when it reaches it.
54
88
  const recent = samples.slice(-12).filter((sample) => sample.value !== null);
55
89
  const t0 = recent[0].at;
56
- const xs = recent.map((sample) => (sample.at - t0) / 3_600_000), ys = recent.map((sample) => sample.value);
57
- const mx = xs.reduce((a, b) => a + b, 0) / xs.length, my = ys.reduce((a, b) => a + b, 0) / ys.length;
58
- const slope = xs.reduce((sum, x, i) => sum + (x - mx) * (ys[i] - my), 0) / (xs.reduce((sum, x) => sum + (x - mx) ** 2, 0) || 1);
90
+ const xs = recent.map((sample) => (sample.at - t0) / 3_600_000),
91
+ ys = recent.map((sample) => sample.value);
92
+ const mx = xs.reduce((a, b) => a + b, 0) / xs.length,
93
+ my = ys.reduce((a, b) => a + b, 0) / ys.length;
94
+ const slope =
95
+ xs.reduce((sum, x, i) => sum + (x - mx) * (ys[i] - my), 0) /
96
+ (xs.reduce((sum, x) => sum + (x - mx) ** 2, 0) || 1);
59
97
  if (slope < 0) {
60
98
  const hours = (latest.value - below) / -slope;
61
- if (hours <= RUN_OUT_HOURS) return { kind: "run-out", text: `is falling by ${Math.abs(slope).toPrecision(2)} an hour and reaches ${below} in about ${hours < 1 ? `${Math.max(1, Math.round(hours * 60))} minutes` : `${Math.round(hours)} hour${Math.round(hours) === 1 ? "" : "s"}`}` };
99
+ if (hours <= RUN_OUT_HOURS)
100
+ return {
101
+ kind: "run-out",
102
+ text: `is falling by ${Math.abs(slope).toPrecision(2)} an hour and reaches ${below} in about ${hours < 1 ? `${Math.max(1, Math.round(hours * 60))} minutes` : `${Math.round(hours)} hour${Math.round(hours) === 1 ? "" : "s"}`}`,
103
+ };
62
104
  }
63
105
  }
64
106
  if (above === null && below === null && values.length > USUAL_AFTER) {
@@ -66,8 +108,15 @@ export function offPattern(samples, { above = null, below = null } = {}) {
66
108
  const middle = median(history);
67
109
  // A number that takes two values (a sawtooth, a flag) has no median deviation; the mean deviation stands in.
68
110
  const deviations = history.map((value) => Math.abs(value - middle));
69
- const spread = median(deviations) || deviations.reduce((a, b) => a + b, 0) / deviations.length || Math.max(1e-9, Math.abs(middle) * 0.05);
70
- if (Math.abs(latest.value - middle) > 6 * spread) return { kind: "unusual", text: `is ${latest.value}; it is usually about ${Number(middle.toPrecision(4))}` };
111
+ const spread =
112
+ median(deviations) ||
113
+ deviations.reduce((a, b) => a + b, 0) / deviations.length ||
114
+ Math.max(1e-9, Math.abs(middle) * 0.05);
115
+ if (Math.abs(latest.value - middle) > 6 * spread)
116
+ return {
117
+ kind: "unusual",
118
+ text: `is ${latest.value}; it is usually about ${Number(middle.toPrecision(4))}`,
119
+ };
71
120
  }
72
121
  return null;
73
122
  }
@@ -76,67 +125,210 @@ export function offPattern(samples, { above = null, below = null } = {}) {
76
125
  function runCheck(watch, shell = process.env.SHELL || "/bin/sh") {
77
126
  return new Promise((resolve) => {
78
127
  const started = performance.now();
79
- let out = "", err = "", done = false;
80
- const child = spawn(shell, ["-lc", watch.command], { cwd: watch.folder || undefined, stdio: ["ignore", "pipe", "pipe"] });
81
- const timer = setTimeout(() => { child.kill("SIGKILL"); finish(null, `it took longer than its ${watch.everySeconds} s interval`); }, watch.everySeconds * 1000);
82
- const finish = (output, error) => { if (done) return; done = true; clearTimeout(timer); resolve({ output, error, ms: Math.round(performance.now() - started) }); };
83
- child.stdout.on("data", (chunk) => { out = (out + chunk).slice(-200_000); });
84
- child.stderr.on("data", (chunk) => { err = (err + chunk).slice(-4_000); });
128
+ let out = "",
129
+ err = "",
130
+ done = false;
131
+ const child = spawn(shell, ["-lc", watch.command], {
132
+ cwd: watch.folder || undefined,
133
+ stdio: ["ignore", "pipe", "pipe"],
134
+ });
135
+ const timer = setTimeout(() => {
136
+ child.kill("SIGKILL");
137
+ finish(null, `it took longer than its ${watch.everySeconds} s interval`);
138
+ }, watch.everySeconds * 1000);
139
+ const finish = (output, error) => {
140
+ if (done) return;
141
+ done = true;
142
+ clearTimeout(timer);
143
+ resolve({ output, error, ms: Math.round(performance.now() - started) });
144
+ };
145
+ child.stdout.on("data", (chunk) => {
146
+ out = (out + chunk).slice(-200_000);
147
+ });
148
+ child.stderr.on("data", (chunk) => {
149
+ err = (err + chunk).slice(-4_000);
150
+ });
85
151
  child.on("error", (error) => finish(null, error.message));
86
- child.on("close", (code) => finish(code === 0 || out ? out : null, code === 0 || out ? null : (err.trim().split("\n").at(-1) || `it exited with code ${code}`)));
152
+ child.on("close", (code) =>
153
+ finish(
154
+ code === 0 || out ? out : null,
155
+ code === 0 || out
156
+ ? null
157
+ : err.trim().split("\n").at(-1) || `it exited with code ${code}`
158
+ )
159
+ );
87
160
  });
88
161
  }
89
162
 
90
- export function createWatches({ store, notify = () => {}, log = () => {}, run = runCheck }) {
91
- const list = () => { try { return JSON.parse(store.meta("watches") || "[]"); } catch { return []; } };
163
+ export function createWatches({
164
+ store,
165
+ notify = () => {},
166
+ log = () => {},
167
+ run = runCheck,
168
+ }) {
169
+ const list = () => {
170
+ try {
171
+ return JSON.parse(store.meta("watches") || "[]");
172
+ } catch {
173
+ return [];
174
+ }
175
+ };
92
176
  const save = (watches) => store.meta("watches", JSON.stringify(watches));
93
- const samplesOf = (id) => { try { return JSON.parse(store.meta(`watch-samples:${id}`) || "[]"); } catch { return []; } };
177
+ const samplesOf = (id) => {
178
+ try {
179
+ return JSON.parse(store.meta(`watch-samples:${id}`) || "[]");
180
+ } catch {
181
+ return [];
182
+ }
183
+ };
94
184
  const timers = new Map();
95
185
 
96
186
  const check = async (id) => {
97
187
  const watch = list().find((entry) => entry.id === id);
98
188
  if (!watch || watch.state !== "on") return null;
99
189
  const result = await run(watch);
100
- const value = result.output === null ? null : readNumber(result.output, watch.measure);
101
- const samples = [...samplesOf(id), { at: Date.now(), value, ms: result.ms, ...(result.error ? { error: result.error.slice(0, 300) } : value === null ? { error: "its output had no number to read" } : {}) }].slice(-HISTORY);
190
+ const value =
191
+ result.output === null ? null : readNumber(result.output, watch.measure);
192
+ const samples = [
193
+ ...samplesOf(id),
194
+ {
195
+ at: Date.now(),
196
+ value,
197
+ ms: result.ms,
198
+ ...(result.error
199
+ ? { error: result.error.slice(0, 300) }
200
+ : value === null
201
+ ? { error: "its output had no number to read" }
202
+ : {}),
203
+ },
204
+ ].slice(-HISTORY);
102
205
  store.meta(`watch-samples:${id}`, JSON.stringify(samples));
103
206
  const off = offPattern(samples, watch.measure);
104
207
  const current = list().find((entry) => entry.id === id);
105
208
  if (!current) return null;
106
209
  // Said once when it goes off and once when it is back; not on every check while it stays off.
107
210
  if (off && !current.off) {
108
- const alert = { at: new Date().toISOString(), text: `${current.name} ${off.text}.`, kind: off.kind };
109
- save(list().map((entry) => (entry.id === id ? { ...entry, off: alert } : entry)));
110
- store.meta("watch-alerts", JSON.stringify([...(JSON.parse(store.meta("watch-alerts") || "[]")), { watch: id, ...alert }].slice(-200)));
211
+ const alert = {
212
+ at: new Date().toISOString(),
213
+ text: `${current.name} ${off.text}.`,
214
+ kind: off.kind,
215
+ };
216
+ save(
217
+ list().map((entry) =>
218
+ entry.id === id ? { ...entry, off: alert } : entry
219
+ )
220
+ );
221
+ store.meta(
222
+ "watch-alerts",
223
+ JSON.stringify(
224
+ [
225
+ ...JSON.parse(store.meta("watch-alerts") || "[]"),
226
+ { watch: id, ...alert },
227
+ ].slice(-200)
228
+ )
229
+ );
111
230
  notify(alert.text, { watch: current, alert });
112
231
  log(`watch ${current.name}: ${off.text}`);
113
232
  } else if (!off && current.off) {
114
- save(list().map((entry) => (entry.id === id ? { ...entry, off: null } : entry)));
115
- notify(`${current.name} is back to normal.`, { watch: current, alert: null });
233
+ save(
234
+ list().map((entry) =>
235
+ entry.id === id ? { ...entry, off: null } : entry
236
+ )
237
+ );
238
+ notify(`${current.name} is back to normal.`, {
239
+ watch: current,
240
+ alert: null,
241
+ });
116
242
  }
117
243
  return { value, off, ms: result.ms };
118
244
  };
119
245
 
120
246
  const schedule = (watch) => {
121
247
  clearTimeout(timers.get(watch.id));
122
- if (watch.state !== "on") { timers.delete(watch.id); return; }
123
- const tick = () => { void check(watch.id).catch((error) => log(`watch ${watch.name}: ${error.message}`)).finally(() => { if (timers.has(watch.id)) timers.set(watch.id, setTimeout(tick, watch.everySeconds * 1000).unref?.() ?? null); }); };
248
+ if (watch.state !== "on") {
249
+ timers.delete(watch.id);
250
+ return;
251
+ }
252
+ const tick = () => {
253
+ void check(watch.id)
254
+ .catch((error) => log(`watch ${watch.name}: ${error.message}`))
255
+ .finally(() => {
256
+ if (timers.has(watch.id))
257
+ timers.set(
258
+ watch.id,
259
+ setTimeout(tick, watch.everySeconds * 1000).unref?.() ?? null
260
+ );
261
+ });
262
+ };
124
263
  timers.set(watch.id, setTimeout(tick, 0));
125
264
  };
126
265
 
127
266
  return {
128
- list: () => list().map((watch) => ({ ...watch, samples: samplesOf(watch.id).slice(-20) })),
129
- alerts: (since = "") => { try { return JSON.parse(store.meta("watch-alerts") || "[]").filter((alert) => alert.at > since); } catch { return []; } },
267
+ list: () =>
268
+ list().map((watch) => ({
269
+ ...watch,
270
+ samples: samplesOf(watch.id).slice(-20),
271
+ })),
272
+ alerts: (since = "") => {
273
+ try {
274
+ return JSON.parse(store.meta("watch-alerts") || "[]").filter(
275
+ (alert) => alert.at > since
276
+ );
277
+ } catch {
278
+ return [];
279
+ }
280
+ },
130
281
  /** A new watch: the person's starts at once; an agent's waits for the person's start. */
131
- add({ name, command, folder = null, everySeconds = 300, measure = {}, by = "user" }) {
132
- if (!String(name || "").trim() || !String(command || "").trim()) return { ok: false, error: "a watch needs a name and a command to read" };
133
- const every = Math.max(MIN_EVERY_SECONDS, Math.round(Number(everySeconds) || 300));
282
+ add({
283
+ name,
284
+ command,
285
+ folder = null,
286
+ everySeconds = 300,
287
+ measure = {},
288
+ by = "user",
289
+ }) {
290
+ if (!String(name || "").trim() || !String(command || "").trim())
291
+ return {
292
+ ok: false,
293
+ error: "a watch needs a name and a command to read",
294
+ };
295
+ const every = Math.max(
296
+ MIN_EVERY_SECONDS,
297
+ Math.round(Number(everySeconds) || 300)
298
+ );
134
299
  const clean = {
135
- ...(measure.field ? { field: String(measure.field).slice(0, 100) } : {}), ...(measure.count ? { count: String(measure.count).slice(0, 100) } : {}), ...(measure.age ? { age: String(measure.age).slice(0, 100) } : {}),
136
- above: Number.isFinite(Number(measure.above)) && measure.above !== null && measure.above !== "" ? Number(measure.above) : null,
137
- below: Number.isFinite(Number(measure.below)) && measure.below !== null && measure.below !== "" ? Number(measure.below) : null,
300
+ ...(measure.field
301
+ ? { field: String(measure.field).slice(0, 100) }
302
+ : {}),
303
+ ...(measure.count
304
+ ? { count: String(measure.count).slice(0, 100) }
305
+ : {}),
306
+ ...(measure.age ? { age: String(measure.age).slice(0, 100) } : {}),
307
+ above:
308
+ Number.isFinite(Number(measure.above)) &&
309
+ measure.above !== null &&
310
+ measure.above !== ""
311
+ ? Number(measure.above)
312
+ : null,
313
+ below:
314
+ Number.isFinite(Number(measure.below)) &&
315
+ measure.below !== null &&
316
+ measure.below !== ""
317
+ ? Number(measure.below)
318
+ : null,
319
+ };
320
+ const watch = {
321
+ id: randomUUID(),
322
+ name: String(name).trim().slice(0, 120),
323
+ command: String(command).slice(0, 2_000),
324
+ folder,
325
+ everySeconds: every,
326
+ measure: clean,
327
+ by,
328
+ state: by === "user" ? "on" : "suggested",
329
+ off: null,
330
+ createdAt: new Date().toISOString(),
138
331
  };
139
- const watch = { id: randomUUID(), name: String(name).trim().slice(0, 120), command: String(command).slice(0, 2_000), folder, everySeconds: every, measure: clean, by, state: by === "user" ? "on" : "suggested", off: null, createdAt: new Date().toISOString() };
140
332
  save([...list(), watch]);
141
333
  if (watch.state === "on") schedule(watch);
142
334
  return { ok: true, watch };
@@ -150,14 +342,21 @@ export function createWatches({ store, notify = () => {}, log = () => {}, run =
150
342
  return { ok: true, watch: on };
151
343
  },
152
344
  remove(id) {
153
- if (!list().some((entry) => entry.id === id)) return { ok: false, error: "no such watch" };
154
- clearTimeout(timers.get(id)); timers.delete(id);
345
+ if (!list().some((entry) => entry.id === id))
346
+ return { ok: false, error: "no such watch" };
347
+ clearTimeout(timers.get(id));
348
+ timers.delete(id);
155
349
  save(list().filter((entry) => entry.id !== id));
156
350
  store.meta(`watch-samples:${id}`, "[]");
157
351
  return { ok: true };
158
352
  },
159
353
  check,
160
- resume() { for (const watch of list()) if (watch.state === "on") schedule(watch); },
161
- stop() { for (const timer of timers.values()) clearTimeout(timer); timers.clear(); },
354
+ resume() {
355
+ for (const watch of list()) if (watch.state === "on") schedule(watch);
356
+ },
357
+ stop() {
358
+ for (const timer of timers.values()) clearTimeout(timer);
359
+ timers.clear();
360
+ },
162
361
  };
163
362
  }