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/network.mjs CHANGED
@@ -7,14 +7,25 @@
7
7
  const SEEN = (id) => `relay_seen:${id}`;
8
8
  const AUTO_SEND_DEFAULT = "relay_auto_send_default";
9
9
  const DECISIONS = new Set(["accept", "decline", "block"]);
10
- const handleOf = (identity) => String(identity || "").split("/")[0].toLowerCase();
10
+ const handleOf = (identity) =>
11
+ String(identity || "")
12
+ .split("/")[0]
13
+ .toLowerCase();
11
14
  const agentOf = (identity) => String(identity || "").split("/")[1] || "";
12
15
 
13
- export function createNetwork({ store, askHarness, relayRequest, openRelayRequest, fingerprint, myKey }) {
16
+ export function createNetwork({
17
+ store,
18
+ askHarness,
19
+ relayRequest,
20
+ openRelayRequest,
21
+ fingerprint,
22
+ myKey,
23
+ }) {
14
24
  // Harness answers for the server's half; a refusal comes back as a plain error the person can read.
15
25
  const harness = async (input) => {
16
26
  const answer = await askHarness("relay_network", input);
17
- if (!answer.ok) throw new Error(answer.error || "Harness refused the request");
27
+ if (!answer.ok)
28
+ throw new Error(answer.error || "Harness refused the request");
18
29
  const { ok: _ok, ...rest } = answer;
19
30
  return rest;
20
31
  };
@@ -23,7 +34,9 @@ export function createNetwork({ store, askHarness, relayRequest, openRelayReques
23
34
  // a message from a person), nothing (open), the other side (a request not answered yet), or nobody (ended).
24
35
  const conversations = async () => {
25
36
  const server = await harness({ op: "threads" }).catch(() => null);
26
- const local = new Map(store.relayThreads().map((row) => [row.task_id, row]));
37
+ const local = new Map(
38
+ store.relayThreads().map((row) => [row.task_id, row])
39
+ );
27
40
  const items = [];
28
41
  for (const thread of server?.threads ?? []) {
29
42
  const here = local.get(thread.id) || null;
@@ -33,45 +46,150 @@ export function createNetwork({ store, askHarness, relayRequest, openRelayReques
33
46
  let keyPrint = null;
34
47
  // A sealed request's subject: opened on this computer, with the other computer's key to compare.
35
48
  if (thread.sealed && !about) {
36
- const opened = await openRelayRequest({ sealed: thread.sealed, from: `${peer.handle}/${peer.agent}` }).catch(() => null);
37
- if (opened) { about = opened.about; keyPrint = opened.fingerprint; }
49
+ const opened = await openRelayRequest({
50
+ sealed: thread.sealed,
51
+ from: `${peer.handle}/${peer.agent}`,
52
+ }).catch(() => null);
53
+ if (opened) {
54
+ about = opened.about;
55
+ keyPrint = opened.fingerprint;
56
+ }
38
57
  }
39
- items.push(item({ id: thread.id, role: thread.role, kind: thread.kind || (thread.target?.agent === "person" ? "person" : "agent"), status: thread.status, about,
40
- peer: { handle: peer.handle, agent: peer.agent }, mine: thread.role === "asker" ? thread.asker.agent : thread.target.agent,
41
- askerSince: thread.role === "target" ? thread.asker.since : null, sealed: Boolean(thread.sealed), fingerprint: keyPrint, createdAt: thread.createdAt, here }));
58
+ items.push(
59
+ item({
60
+ id: thread.id,
61
+ role: thread.role,
62
+ kind:
63
+ thread.kind ||
64
+ (thread.target?.agent === "person" ? "person" : "agent"),
65
+ status: thread.status,
66
+ about,
67
+ peer: { handle: peer.handle, agent: peer.agent },
68
+ mine:
69
+ thread.role === "asker" ? thread.asker.agent : thread.target.agent,
70
+ askerSince: thread.role === "target" ? thread.asker.since : null,
71
+ sealed: Boolean(thread.sealed),
72
+ fingerprint: keyPrint,
73
+ createdAt: thread.createdAt,
74
+ here,
75
+ })
76
+ );
42
77
  }
43
78
  // Threads this computer holds that Harness no longer lists (older ones, or Harness out of reach).
44
79
  for (const here of local.values()) {
45
- items.push(item({ id: here.task_id, role: here.role, kind: agentOf(here.with_agent) === "person" ? "person" : "agent", status: here.closed_at ? "closed" : "accepted", about: here.objective,
46
- peer: { handle: handleOf(here.with_agent), agent: agentOf(here.with_agent) }, mine: here.target_agent === "user" ? "person" : here.target_agent,
47
- askerSince: null, sealed: false, fingerprint: null, createdAt: here.opened_at, here }));
80
+ items.push(
81
+ item({
82
+ id: here.task_id,
83
+ role: here.role,
84
+ kind: agentOf(here.with_agent) === "person" ? "person" : "agent",
85
+ status: here.closed_at ? "closed" : "accepted",
86
+ about: here.objective,
87
+ peer: {
88
+ handle: handleOf(here.with_agent),
89
+ agent: agentOf(here.with_agent),
90
+ },
91
+ mine: here.target_agent === "user" ? "person" : here.target_agent,
92
+ askerSince: null,
93
+ sealed: false,
94
+ fingerprint: null,
95
+ createdAt: here.opened_at,
96
+ here,
97
+ })
98
+ );
48
99
  }
49
- items.sort((a, b) => String(b.updatedAt).localeCompare(String(a.updatedAt)));
50
- return { ok: true, connected: Boolean(server), items, needsYou: items.filter((entry) => entry.group === "needs_you").length };
100
+ items.sort((a, b) =>
101
+ String(b.updatedAt).localeCompare(String(a.updatedAt))
102
+ );
103
+ return {
104
+ ok: true,
105
+ connected: Boolean(server),
106
+ items,
107
+ needsYou: items.filter((entry) => entry.group === "needs_you").length,
108
+ };
51
109
  };
52
110
 
53
- const item = ({ id, role, kind, status, about, peer, mine, askerSince, sealed, fingerprint: keyPrint, createdAt, here }) => {
111
+ const item = ({
112
+ id,
113
+ role,
114
+ kind,
115
+ status,
116
+ about,
117
+ peer,
118
+ mine,
119
+ askerSince,
120
+ sealed,
121
+ fingerprint: keyPrint,
122
+ createdAt,
123
+ here,
124
+ }) => {
54
125
  const seen = Number(store.meta(SEEN(id)) || 0);
55
- const thread = here ? {
56
- open: !here.closed_at, autoSend: here.auto_send, held: here.held, unread: here.last_in_seq > seen, messages: here.messages, outcome: here.outcome,
57
- last: here.last ? { direction: here.last.direction, sender: here.last.sender, text: String(here.last.text || "").slice(0, 160), state: here.last.state, at: here.last.created_at } : null,
58
- } : null;
59
- const reason = role === "target" && status === "requested" ? "request"
60
- : thread?.open && thread.held ? "held"
61
- : thread?.open && kind === "person" && thread.unread ? "message"
126
+ const thread = here
127
+ ? {
128
+ open: !here.closed_at,
129
+ autoSend: here.auto_send,
130
+ held: here.held,
131
+ unread: here.last_in_seq > seen,
132
+ messages: here.messages,
133
+ outcome: here.outcome,
134
+ last: here.last
135
+ ? {
136
+ direction: here.last.direction,
137
+ sender: here.last.sender,
138
+ text: String(here.last.text || "").slice(0, 160),
139
+ state: here.last.state,
140
+ at: here.last.created_at,
141
+ }
142
+ : null,
143
+ }
62
144
  : null;
63
- const group = reason ? "needs_you"
64
- : thread ? (thread.open ? "open" : "ended")
65
- : status === "requested" ? "waiting"
66
- : status === "accepted" || status === "accepted_pending" ? "open"
67
- : "ended";
68
- return { id, role, kind, status, about, peer, mine, askerSince, sealed, fingerprint: keyPrint, group, reason, thread, createdAt, updatedAt: here?.updated_at || createdAt };
145
+ const reason =
146
+ role === "target" && status === "requested"
147
+ ? "request"
148
+ : thread?.open && thread.held
149
+ ? "held"
150
+ : thread?.open && kind === "person" && thread.unread
151
+ ? "message"
152
+ : null;
153
+ const group = reason
154
+ ? "needs_you"
155
+ : thread
156
+ ? thread.open
157
+ ? "open"
158
+ : "ended"
159
+ : status === "requested"
160
+ ? "waiting"
161
+ : status === "accepted" || status === "accepted_pending"
162
+ ? "open"
163
+ : "ended";
164
+ return {
165
+ id,
166
+ role,
167
+ kind,
168
+ status,
169
+ about,
170
+ peer,
171
+ mine,
172
+ askerSince,
173
+ sealed,
174
+ fingerprint: keyPrint,
175
+ group,
176
+ reason,
177
+ thread,
178
+ createdAt,
179
+ updatedAt: here?.updated_at || createdAt,
180
+ };
69
181
  };
70
182
 
71
183
  // A refusal from Harness or the other computer comes back as the answer, in its own words.
72
- const answered = (promise) => ({ pending: promise.catch((error) => ({ ok: false, error: error.message })) });
184
+ const answered = (promise) => ({
185
+ pending: promise.catch((error) => ({ ok: false, error: error.message })),
186
+ });
73
187
  const command = (request, agent) => {
74
- if (agent !== "user") return { ok: false, error: "only the person talks to other swarms from here" };
188
+ if (agent !== "user")
189
+ return {
190
+ ok: false,
191
+ error: "only the person talks to other swarms from here",
192
+ };
75
193
  const id = String(request.id || "");
76
194
  switch (request.command) {
77
195
  case "board.relay.list":
@@ -79,40 +197,81 @@ export function createNetwork({ store, askHarness, relayRequest, openRelayReques
79
197
  // The server's half of Swarms (card, search, contacts, blocks): what the dashboard asks Harness directly.
80
198
  case "board.relay.network": {
81
199
  const { command: _command, agent: _agent, ...input } = request;
82
- if (input.op === "decide" || input.op === "threads") return { ok: false, error: "use board.relay.decide and board.relay.list" };
83
- return answered(harness(input).then((answer) => ({ ok: true, ...answer })));
200
+ if (input.op === "decide" || input.op === "threads")
201
+ return {
202
+ ok: false,
203
+ error: "use board.relay.decide and board.relay.list",
204
+ };
205
+ return answered(
206
+ harness(input).then((answer) => ({ ok: true, ...answer }))
207
+ );
84
208
  }
85
209
  case "board.relay.decide":
86
- if (!DECISIONS.has(String(request.decision))) return { ok: false, error: "decision is accept, decline or block" };
87
- return answered(harness({ op: "decide", id, decision: String(request.decision) }).then((answer) => ({ ok: true, ...answer })));
210
+ if (!DECISIONS.has(String(request.decision)))
211
+ return { ok: false, error: "decision is accept, decline or block" };
212
+ return answered(
213
+ harness({
214
+ op: "decide",
215
+ id,
216
+ decision: String(request.decision),
217
+ }).then((answer) => ({ ok: true, ...answer }))
218
+ );
88
219
  // A request from this computer: to one of their listed agents from one of the person's, or to the person themselves.
89
220
  case "board.relay.request": {
90
- const handle = String(request.handle || "").toLowerCase().replace(/^@/, "");
221
+ const handle = String(request.handle || "")
222
+ .toLowerCase()
223
+ .replace(/^@/, "");
91
224
  const toAgent = String(request.toAgent || "person");
92
- const fromAgent = toAgent === "person" ? "person" : String(request.fromAgent || "");
93
- return answered(relayRequest({ to: `${handle}/${toAgent}`, fromAgent, about: request.about, brief: request.brief, refs: request.refs }).then((made) => ({ ok: true, ...made })));
225
+ const fromAgent =
226
+ toAgent === "person" ? "person" : String(request.fromAgent || "");
227
+ return answered(
228
+ relayRequest({
229
+ to: `${handle}/${toAgent}`,
230
+ fromAgent,
231
+ about: request.about,
232
+ brief: request.brief,
233
+ refs: request.refs,
234
+ }).then((made) => ({ ok: true, ...made }))
235
+ );
94
236
  }
95
237
  case "board.relay.seen": {
96
238
  const thread = store.getThread(id);
97
239
  if (!thread) return { ok: false, error: "no thread on this task" };
98
- const last = store.threadMessages(id).filter((message) => message.direction === "in").at(-1);
240
+ const last = store
241
+ .threadMessages(id)
242
+ .filter((message) => message.direction === "in")
243
+ .at(-1);
99
244
  store.meta(SEEN(id), String(last?.seq ?? 0));
100
245
  return { ok: true };
101
246
  }
102
247
  // Whether replies the person's agents write wait for the person on a new thread (the default) or send on their own.
103
248
  case "board.relay.defaults":
104
- if (typeof request.autoSend === "boolean") store.meta(AUTO_SEND_DEFAULT, request.autoSend ? "1" : "0");
249
+ if (typeof request.autoSend === "boolean")
250
+ store.meta(AUTO_SEND_DEFAULT, request.autoSend ? "1" : "0");
105
251
  return { ok: true, autoSend: store.meta(AUTO_SEND_DEFAULT) === "1" };
106
252
  case "board.relay.peers":
107
- return answered(Promise.all(store.relayPeers().map(async (peer) => ({ handle: peer.handle, pinnedAt: peer.pinned_at, fingerprint: await fingerprint(peer.public_key) })))
108
- .then(async (peers) => ({ ok: true, peers, mine: myKey() ? await fingerprint(myKey()) : null })));
253
+ return answered(
254
+ Promise.all(
255
+ store.relayPeers().map(async (peer) => ({
256
+ handle: peer.handle,
257
+ pinnedAt: peer.pinned_at,
258
+ fingerprint: await fingerprint(peer.public_key),
259
+ }))
260
+ ).then(async (peers) => ({
261
+ ok: true,
262
+ peers,
263
+ mine: myKey() ? await fingerprint(myKey()) : null,
264
+ }))
265
+ );
109
266
  default:
110
267
  return { ok: false, error: `unknown command ${request.command}` };
111
268
  }
112
269
  };
113
270
 
114
271
  /** A thread just opened on this computer starts with the person's default for their agents' replies. */
115
- const opened = (taskId) => { if (store.meta(AUTO_SEND_DEFAULT) === "1") store.setAutoSend(taskId, true); };
272
+ const opened = (taskId) => {
273
+ if (store.meta(AUTO_SEND_DEFAULT) === "1") store.setAutoSend(taskId, true);
274
+ };
116
275
 
117
276
  return { command, opened };
118
277
  }
package/lib/notify.mjs CHANGED
@@ -9,24 +9,46 @@ import { spawn } from "node:child_process";
9
9
  const TITLE = "Harness";
10
10
 
11
11
  function command(platform, title, body, env) {
12
- if (env.HARNESS_NOTIFY_COMMAND) return { command: env.HARNESS_NOTIFY_COMMAND, args: [title, body] };
12
+ if (env.HARNESS_NOTIFY_COMMAND)
13
+ return { command: env.HARNESS_NOTIFY_COMMAND, args: [title, body] };
13
14
  if (platform === "darwin") {
14
- const quote = (value) => String(value).replaceAll("\\", "\\\\").replaceAll('"', '\\"');
15
- return { command: "osascript", args: ["-e", `display notification "${quote(body)}" with title "${quote(title)}"`] };
15
+ const quote = (value) =>
16
+ String(value).replaceAll("\\", "\\\\").replaceAll('"', '\\"');
17
+ return {
18
+ command: "osascript",
19
+ args: [
20
+ "-e",
21
+ `display notification "${quote(body)}" with title "${quote(title)}"`,
22
+ ],
23
+ };
16
24
  }
17
- if (platform === "linux") return { command: "notify-send", args: [title, body] };
25
+ if (platform === "linux")
26
+ return { command: "notify-send", args: [title, body] };
18
27
  if (platform === "win32") {
19
28
  const quote = (value) => String(value).replaceAll("'", "''");
20
- return { command: "powershell", args: ["-NoProfile", "-Command", `[void][System.Reflection.Assembly]::LoadWithPartialName('System.Windows.Forms'); $n = New-Object System.Windows.Forms.NotifyIcon; $n.Icon = [System.Drawing.SystemIcons]::Information; $n.Visible = $true; $n.ShowBalloonTip(8000, '${quote(title)}', '${quote(body)}', 'Info'); Start-Sleep -Seconds 9; $n.Dispose()`] };
29
+ return {
30
+ command: "powershell",
31
+ args: [
32
+ "-NoProfile",
33
+ "-Command",
34
+ `[void][System.Reflection.Assembly]::LoadWithPartialName('System.Windows.Forms'); $n = New-Object System.Windows.Forms.NotifyIcon; $n.Icon = [System.Drawing.SystemIcons]::Information; $n.Visible = $true; $n.ShowBalloonTip(8000, '${quote(title)}', '${quote(body)}', 'Info'); Start-Sleep -Seconds 9; $n.Dispose()`,
35
+ ],
36
+ };
21
37
  }
22
38
  return null;
23
39
  }
24
40
 
25
- export function notifyDesktop(body, { title = TITLE, platform = process.platform, env = process.env } = {}) {
41
+ export function notifyDesktop(
42
+ body,
43
+ { title = TITLE, platform = process.platform, env = process.env } = {}
44
+ ) {
26
45
  const call = command(platform, title, String(body).slice(0, 240), env);
27
46
  if (!call) return false;
28
47
  try {
29
- const child = spawn(call.command, call.args, { stdio: "ignore", detached: true });
48
+ const child = spawn(call.command, call.args, {
49
+ stdio: "ignore",
50
+ detached: true,
51
+ });
30
52
  child.on("error", () => {});
31
53
  child.unref();
32
54
  return true;
@@ -39,14 +61,29 @@ export function notifyDesktop(body, { title = TITLE, platform = process.platform
39
61
  export function notificationFor(task, { previous = null } = {}) {
40
62
  if (!task) return null;
41
63
  const objective = String(task.objective || "").slice(0, 80);
42
- const wantsRun = task.status === "open" && task.run_workspace && !task.run_authorized_at;
43
- if (wantsRun && !(previous && previous.status === "open" && previous.run_workspace && !previous.run_authorized_at)) {
64
+ const wantsRun =
65
+ task.status === "open" && task.run_workspace && !task.run_authorized_at;
66
+ if (
67
+ wantsRun &&
68
+ !(
69
+ previous &&
70
+ previous.status === "open" &&
71
+ previous.run_workspace &&
72
+ !previous.run_authorized_at
73
+ )
74
+ ) {
44
75
  return `${task.origin_agent} wants ${task.target_agent} to run: ${objective}. Authorize with tryharness board run ${String(task.id).slice(0, 8)}`;
45
76
  }
46
- if (task.status === "needs_input" && (task.holder === "user" || task.origin_agent === "user") && previous?.status !== "needs_input") {
77
+ if (
78
+ task.status === "needs_input" &&
79
+ (task.holder === "user" || task.origin_agent === "user") &&
80
+ previous?.status !== "needs_input"
81
+ ) {
47
82
  return `${task.claimed_by} has a question on: ${objective}`;
48
83
  }
49
- if (task.status === "unknown" && previous?.status !== "unknown") return `The outcome of ${task.target_agent}'s run is unknown: ${objective}`;
50
- if (task.status === "open" && task.target_agent === "harness" && !previous) return `${task.origin_agent} posted for Harness: ${objective}`;
84
+ if (task.status === "unknown" && previous?.status !== "unknown")
85
+ return `The outcome of ${task.target_agent}'s run is unknown: ${objective}`;
86
+ if (task.status === "open" && task.target_agent === "harness" && !previous)
87
+ return `${task.origin_agent} posted for Harness: ${objective}`;
51
88
  return null;
52
89
  }