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
@@ -8,8 +8,11 @@ import { pathToFileURL } from "node:url";
8
8
  // Filled in by setup: the runtime the other agents' Harness server runs from.
9
9
  const RUNTIME = "__HARNESS_RUNTIME__";
10
10
  const AGENT = "opencode";
11
- const harness = () => import(pathToFileURL(path.join(RUNTIME, "lib", "nudge.mjs")).href);
12
- const socketPath = async () => (await import(pathToFileURL(path.join(RUNTIME, "lib", "paths.mjs")).href)).paths.socket;
11
+ const harness = () =>
12
+ import(pathToFileURL(path.join(RUNTIME, "lib", "nudge.mjs")).href);
13
+ const socketPath = async () =>
14
+ (await import(pathToFileURL(path.join(RUNTIME, "lib", "paths.mjs")).href))
15
+ .paths.socket;
13
16
 
14
17
  async function notify(message) {
15
18
  try {
@@ -28,8 +31,17 @@ const pids = [process.pid, process.ppid];
28
31
  let counter = 0;
29
32
  const BASE62 = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
30
33
  const partId = () => {
31
- const time = ((BigInt(Date.now()) * 0x1000n) + BigInt(++counter % 0x1000)) & 0xffffffffffffn;
32
- return "prt_" + time.toString(16).padStart(12, "0") + Array.from(crypto.getRandomValues(new Uint8Array(14)), (byte) => BASE62[byte % 62]).join("");
34
+ const time =
35
+ (BigInt(Date.now()) * 0x1000n + BigInt(++counter % 0x1000)) &
36
+ 0xffffffffffffn;
37
+ return (
38
+ "prt_" +
39
+ time.toString(16).padStart(12, "0") +
40
+ Array.from(
41
+ crypto.getRandomValues(new Uint8Array(14)),
42
+ (byte) => BASE62[byte % 62]
43
+ ).join("")
44
+ );
33
45
  };
34
46
 
35
47
  export const TryHarnessPlugin = async ({ directory, client }) => ({
@@ -48,22 +60,47 @@ export const TryHarnessPlugin = async ({ directory, client }) => ({
48
60
  const messageID = output.message?.id || input?.messageID;
49
61
  if (!messageID) return;
50
62
  const messages = await board.takeMessages(AGENT, sessionId);
51
- const nudge = first ? await board.boardNudge(AGENT) : await board.boardNudgeIfChanged(AGENT);
63
+ const nudge = first
64
+ ? await board.boardNudge(AGENT)
65
+ : await board.boardNudgeIfChanged(AGENT);
52
66
  // Marked synthetic: the model reads them, and they are not the words typed here.
53
- for (const text of [messages.length ? board.messagesText(messages) : "", nudge]) {
54
- if (text) output.parts.push({ id: partId(), sessionID: sessionId, messageID, type: "text", text, synthetic: true });
67
+ for (const text of [
68
+ messages.length ? board.messagesText(messages) : "",
69
+ nudge,
70
+ ]) {
71
+ if (text)
72
+ output.parts.push({
73
+ id: partId(),
74
+ sessionID: sessionId,
75
+ messageID,
76
+ type: "text",
77
+ text,
78
+ synthetic: true,
79
+ });
55
80
  }
56
81
  },
57
82
  event: async ({ event }) => {
58
83
  if (event?.type !== "session.idle") return;
59
- const sessionId = event?.properties?.sessionID || event?.properties?.sessionId;
84
+ const sessionId =
85
+ event?.properties?.sessionID || event?.properties?.sessionId;
60
86
  if (!sessionId) return;
61
87
  await notify({ command: "sync", agent: AGENT, sessionId, cwd: directory });
62
88
  // When a turn ends, a message waiting from Harness starts the next one (Claude Code's Stop hook does the same).
63
89
  const board = await harness().catch(() => null);
64
90
  const messages = board ? await board.takeMessages(AGENT, sessionId) : [];
65
- const send = client?.session?.promptAsync?.bind(client.session) ?? client?.session?.prompt?.bind(client.session);
66
- if (messages.length && send) await send({ path: { id: sessionId }, body: { parts: [{ type: "text", text: board.messagesText(messages) }] } }).catch(() => {});
67
- else if (board && !messages.length) await board.noteTurn(AGENT, { session_id: sessionId, cwd: directory }, pids);
91
+ const send =
92
+ client?.session?.promptAsync?.bind(client.session) ??
93
+ client?.session?.prompt?.bind(client.session);
94
+ if (messages.length && send)
95
+ await send({
96
+ path: { id: sessionId },
97
+ body: { parts: [{ type: "text", text: board.messagesText(messages) }] },
98
+ }).catch(() => {});
99
+ else if (board && !messages.length)
100
+ await board.noteTurn(
101
+ AGENT,
102
+ { session_id: sessionId, cwd: directory },
103
+ pids
104
+ );
68
105
  },
69
106
  });
@@ -11,14 +11,19 @@ import { pathToFileURL } from "node:url";
11
11
  const RUNTIME = "__HARNESS_RUNTIME__";
12
12
  const NODE = "__HARNESS_NODE__";
13
13
  const AGENT = "pi";
14
- const harness = () => import(pathToFileURL(path.join(RUNTIME, "lib", "nudge.mjs")).href);
14
+ const harness = () =>
15
+ import(pathToFileURL(path.join(RUNTIME, "lib", "nudge.mjs")).href);
15
16
 
16
17
  // The server, one per session: JSON-RPC over its stdin and stdout.
17
18
  let server = null;
18
19
  let nextId = 1;
19
20
  function start() {
20
21
  if (server) return server;
21
- const child = spawn(NODE, [path.join(RUNTIME, "lib", "mcp-stdio.mjs"), "--agent", AGENT], { stdio: ["pipe", "pipe", "ignore"] });
22
+ const child = spawn(
23
+ NODE,
24
+ [path.join(RUNTIME, "lib", "mcp-stdio.mjs"), "--agent", AGENT],
25
+ { stdio: ["pipe", "pipe", "ignore"] }
26
+ );
22
27
  const pending = new Map();
23
28
  let buffer = "";
24
29
  child.stdout.setEncoding("utf8");
@@ -28,14 +33,19 @@ function start() {
28
33
  buffer = lines.pop();
29
34
  for (const line of lines) {
30
35
  let message;
31
- try { message = JSON.parse(line); } catch { continue; }
36
+ try {
37
+ message = JSON.parse(line);
38
+ } catch {
39
+ continue;
40
+ }
32
41
  pending.get(message.id)?.(message);
33
42
  pending.delete(message.id);
34
43
  }
35
44
  });
36
45
  const stopped = () => {
37
46
  if (server?.child === child) server = null;
38
- for (const settle of pending.values()) settle({ error: { message: "the Harness server stopped" } });
47
+ for (const settle of pending.values())
48
+ settle({ error: { message: "the Harness server stopped" } });
39
49
  pending.clear();
40
50
  };
41
51
  child.on("exit", stopped);
@@ -47,34 +57,61 @@ function rpc(method, params = {}) {
47
57
  const { child, pending } = start();
48
58
  const id = nextId++;
49
59
  return new Promise((resolve, reject) => {
50
- pending.set(id, (message) => (message.error ? reject(new Error(message.error.message)) : resolve(message.result)));
51
- child.stdin.write(JSON.stringify({ jsonrpc: "2.0", id, method, params }) + "\n");
60
+ pending.set(id, (message) =>
61
+ message.error
62
+ ? reject(new Error(message.error.message))
63
+ : resolve(message.result)
64
+ );
65
+ child.stdin.write(
66
+ JSON.stringify({ jsonrpc: "2.0", id, method, params }) + "\n"
67
+ );
52
68
  });
53
69
  }
54
70
 
55
71
  let typeApi = null;
56
72
  async function types() {
57
73
  if (typeApi) return typeApi;
58
- try { typeApi = (await import("@sinclair/typebox")).Type; }
59
- catch { const loaded = await import("typebox"); typeApi = loaded.Type || loaded; }
74
+ try {
75
+ typeApi = (await import("@sinclair/typebox")).Type;
76
+ } catch {
77
+ const loaded = await import("typebox");
78
+ typeApi = loaded.Type || loaded;
79
+ }
60
80
  return typeApi;
61
81
  }
62
82
 
63
83
  function toType(schema, Type) {
64
84
  if (!schema || typeof schema !== "object") return Type.Any();
65
- const opts = typeof schema.description === "string" ? { description: schema.description } : {};
66
- if (Array.isArray(schema.enum) && schema.enum.length) return Type.Union(schema.enum.map((value) => Type.Literal(value)), opts);
67
- if (Array.isArray(schema.anyOf || schema.oneOf)) return Type.Union((schema.anyOf || schema.oneOf).map((entry) => toType(entry, Type)), opts);
85
+ const opts =
86
+ typeof schema.description === "string"
87
+ ? { description: schema.description }
88
+ : {};
89
+ if (Array.isArray(schema.enum) && schema.enum.length)
90
+ return Type.Union(
91
+ schema.enum.map((value) => Type.Literal(value)),
92
+ opts
93
+ );
94
+ if (Array.isArray(schema.anyOf || schema.oneOf))
95
+ return Type.Union(
96
+ (schema.anyOf || schema.oneOf).map((entry) => toType(entry, Type)),
97
+ opts
98
+ );
68
99
  if (schema.type === "object") {
69
100
  const required = new Set(schema.required || []);
70
101
  const properties = {};
71
102
  for (const [name, child] of Object.entries(schema.properties || {})) {
72
103
  const converted = toType(child, Type);
73
- properties[name] = required.has(name) ? converted : Type.Optional(converted);
104
+ properties[name] = required.has(name)
105
+ ? converted
106
+ : Type.Optional(converted);
74
107
  }
75
- return Type.Object(properties, { ...opts, additionalProperties: schema.additionalProperties === true });
108
+ return Type.Object(properties, {
109
+ ...opts,
110
+ additionalProperties: schema.additionalProperties === true,
111
+ });
76
112
  }
77
- if (schema.type === "array") return Type.Array(toType(schema.items, Type), opts);
113
+ if (schema.type === "array")
114
+ return Type.Array(toType(schema.items, Type), opts);
78
115
  if (schema.type === "string") return Type.String(opts);
79
116
  if (schema.type === "integer") return Type.Integer(opts);
80
117
  if (schema.type === "number") return Type.Number(opts);
@@ -91,32 +128,61 @@ export default function tryHarness(pi) {
91
128
  const Type = await types();
92
129
  const listing = await rpc("tools/list");
93
130
  for (const def of listing?.tools || []) {
94
- if (!def?.name || def.name === "report_session" || installed.has(def.name)) continue;
131
+ if (
132
+ !def?.name ||
133
+ def.name === "report_session" ||
134
+ installed.has(def.name)
135
+ )
136
+ continue;
95
137
  pi.registerTool({
96
138
  name: def.name,
97
139
  label: "Harness: " + def.name.replaceAll("_", " "),
98
- description: def.description || "Use this connected Harness capability.",
140
+ description:
141
+ def.description || "Use this connected Harness capability.",
99
142
  parameters: toType(def.inputSchema, Type),
100
143
  async execute(_id, params) {
101
- const result = await rpc("tools/call", { name: def.name, arguments: params || {} });
102
- const content = result?.content || [{ type: "text", text: "Harness returned no content." }];
144
+ const result = await rpc("tools/call", {
145
+ name: def.name,
146
+ arguments: params || {},
147
+ });
148
+ const content = result?.content || [
149
+ { type: "text", text: "Harness returned no content." },
150
+ ];
103
151
  // pi marks a tool failed when it throws.
104
- if (result?.isError) throw new Error(content.map((part) => part.text || "").join("\n") || "Harness call failed.");
152
+ if (result?.isError)
153
+ throw new Error(
154
+ content.map((part) => part.text || "").join("\n") ||
155
+ "Harness call failed."
156
+ );
105
157
  return { content, details: undefined };
106
158
  },
107
159
  });
108
160
  installed.add(def.name);
109
161
  }
110
162
  ctx?.ui?.notify?.("Harness connected", "info");
111
- })().catch((error) => { connecting = null; ctx?.ui?.notify?.("Harness unavailable: " + error?.message, "warning"); });
163
+ })().catch((error) => {
164
+ connecting = null;
165
+ ctx?.ui?.notify?.("Harness unavailable: " + error?.message, "warning");
166
+ });
112
167
  return connecting;
113
168
  };
114
- const where = (ctx) => ({ session_id: ctx?.sessionManager?.getSessionId?.(), cwd: ctx?.cwd || process.cwd() });
169
+ const where = (ctx) => ({
170
+ session_id: ctx?.sessionManager?.getSessionId?.(),
171
+ cwd: ctx?.cwd || process.cwd(),
172
+ });
115
173
  const pids = [process.pid, process.ppid];
116
174
  const tell = (nudge, ctx) => {
117
175
  if (!nudge) return;
118
176
  // Into the model's context when pi offers it; always visible to the person.
119
- if (typeof pi.sendMessage === "function") { try { pi.sendMessage({ customType: "harness-board", content: nudge, display: true }); } catch {} }
177
+ if (typeof pi.sendMessage === "function") {
178
+ try {
179
+ pi.sendMessage({
180
+ customType: "harness-board",
181
+ content: nudge,
182
+ display: true,
183
+ });
184
+ } catch {}
185
+ }
120
186
  ctx?.ui?.notify?.(nudge.split("\n").slice(0, 2).join(" "), "info");
121
187
  };
122
188
  pi.on("session_start", async (_event, ctx) => {
@@ -132,7 +198,9 @@ export default function tryHarness(pi) {
132
198
  // What the person sent this session from Harness (board.live.say), taken once, as Claude Code's hooks take it.
133
199
  const said = async (board, ctx) => {
134
200
  const sessionId = where(ctx).session_id;
135
- const messages = sessionId ? await board.takeMessages(AGENT, sessionId) : [];
201
+ const messages = sessionId
202
+ ? await board.takeMessages(AGENT, sessionId)
203
+ : [];
136
204
  return messages.length ? board.messagesText(messages) : "";
137
205
  };
138
206
  pi.on("before_agent_start", async (event, ctx) => {
@@ -140,7 +208,9 @@ export default function tryHarness(pi) {
140
208
  const board = await harness().catch(() => null);
141
209
  if (!board) return undefined;
142
210
  await board.notePrompt(AGENT, where(ctx), pids);
143
- const text = [await said(board, ctx), await board.boardNudge(AGENT)].filter(Boolean).join("\n\n");
211
+ const text = [await said(board, ctx), await board.boardNudge(AGENT)]
212
+ .filter(Boolean)
213
+ .join("\n\n");
144
214
  if (!text || typeof event?.systemPrompt !== "string") return undefined;
145
215
  return { systemPrompt: `${event.systemPrompt}\n\n${text}` };
146
216
  });
@@ -156,6 +226,15 @@ export default function tryHarness(pi) {
156
226
  const board = await harness().catch(() => null);
157
227
  if (board) tell(await board.boardNudgeIfChanged(AGENT), ctx);
158
228
  });
159
- pi.on("session_shutdown", () => { server?.child.kill(); server = null; });
160
- pi.registerCommand("harness-reconnect", { description: "Reconnect Harness", handler: async (_args, ctx) => { connecting = null; await connect(ctx); } });
229
+ pi.on("session_shutdown", () => {
230
+ server?.child.kill();
231
+ server = null;
232
+ });
233
+ pi.registerCommand("harness-reconnect", {
234
+ description: "Reconnect Harness",
235
+ handler: async (_args, ctx) => {
236
+ connecting = null;
237
+ await connect(ctx);
238
+ },
239
+ });
161
240
  }