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.
- 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 +5248 -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 +382 -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/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) => {
|
|
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)
|
|
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 = [
|
|
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)
|
|
32
|
-
|
|
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 = [
|
|
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
|
|
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)
|
|
50
|
-
|
|
51
|
-
|
|
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),
|
|
57
|
-
|
|
58
|
-
const
|
|
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)
|
|
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 =
|
|
70
|
-
|
|
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 = "",
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
const
|
|
83
|
-
|
|
84
|
-
|
|
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) =>
|
|
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({
|
|
91
|
-
|
|
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) => {
|
|
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 =
|
|
101
|
-
|
|
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 = {
|
|
109
|
-
|
|
110
|
-
|
|
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(
|
|
115
|
-
|
|
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") {
|
|
123
|
-
|
|
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: () =>
|
|
129
|
-
|
|
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({
|
|
132
|
-
|
|
133
|
-
|
|
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
|
|
136
|
-
|
|
137
|
-
|
|
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))
|
|
154
|
-
|
|
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() {
|
|
161
|
-
|
|
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
|
}
|