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/adapters.mjs CHANGED
@@ -11,48 +11,118 @@ export const AGENTS = MACHINE_RUNNERS;
11
11
  // join the machine's task board by launching the same stdio MCP under their own name.
12
12
  export const MCP_CLIENTS = ["grok-bot"];
13
13
  const MCP_CLIENT_APPS = {
14
- "grok-bot": { darwin: ["/Applications/Grok Bot.app", path.join(os.homedir(), "Applications", "Grok Bot.app")] },
14
+ "grok-bot": {
15
+ darwin: [
16
+ "/Applications/Grok Bot.app",
17
+ path.join(os.homedir(), "Applications", "Grok Bot.app"),
18
+ ],
19
+ },
15
20
  };
16
21
 
17
22
  // Agents that run in their own cloud but have an app here, where Open brings them forward. The app is checked to be that
18
23
  // agent's (its bundle id is in Info.plist, binary or XML), so another app with the same name is never opened as it.
19
24
  const AGENT_APPS = {
20
- muse: { name: "Muse", bundle: "com.meta.endo", darwin: ["/Applications/Muse.app", path.join(os.homedir(), "Applications", "Muse.app")] },
25
+ muse: {
26
+ name: "Muse",
27
+ bundle: "com.meta.endo",
28
+ darwin: [
29
+ "/Applications/Muse.app",
30
+ path.join(os.homedir(), "Applications", "Muse.app"),
31
+ ],
32
+ },
21
33
  };
22
- export function detectAgentApps({ platform = process.platform, exists = fs.existsSync, read = (file) => fs.readFileSync(file, "latin1") } = {}) {
34
+ export function detectAgentApps({
35
+ platform = process.platform,
36
+ exists = fs.existsSync,
37
+ read = (file) => fs.readFileSync(file, "latin1"),
38
+ } = {}) {
23
39
  return Object.entries(AGENT_APPS).flatMap(([agent, app]) => {
24
- const found = (app[platform] || []).find((candidate) => exists(candidate) && (() => { try { return read(path.join(candidate, "Contents", "Info.plist")).includes(app.bundle); } catch { return false; } })());
40
+ const found = (app[platform] || []).find(
41
+ (candidate) =>
42
+ exists(candidate) &&
43
+ (() => {
44
+ try {
45
+ return read(
46
+ path.join(candidate, "Contents", "Info.plist")
47
+ ).includes(app.bundle);
48
+ } catch {
49
+ return false;
50
+ }
51
+ })()
52
+ );
25
53
  return found ? [{ agent, name: app.name, path: found }] : [];
26
54
  });
27
55
  }
28
56
 
29
- export function detectMcpClients({ platform = process.platform, exists = fs.existsSync } = {}) {
30
- return MCP_CLIENTS.map((client) => ({ client, path: (MCP_CLIENT_APPS[client][platform] || []).find((candidate) => exists(candidate)) || null }));
57
+ export function detectMcpClients({
58
+ platform = process.platform,
59
+ exists = fs.existsSync,
60
+ } = {}) {
61
+ return MCP_CLIENTS.map((client) => ({
62
+ client,
63
+ path:
64
+ (MCP_CLIENT_APPS[client][platform] || []).find((candidate) =>
65
+ exists(candidate)
66
+ ) || null,
67
+ }));
31
68
  }
32
69
 
33
70
  // Grok Bot keeps its MCP server list in its account and writes it only from its own settings
34
71
  // (a one-server JSON paste); there is no file on disk to write. The snippet is the setup.
35
- export function mcpClientSnippet({ runtimeRoot, client, nodePath = process.execPath }) {
36
- return { mcpServers: { harness: { command: nodePath, args: [path.join(runtimeRoot, "lib", "mcp-stdio.mjs"), "--agent", client] } } };
72
+ export function mcpClientSnippet({
73
+ runtimeRoot,
74
+ client,
75
+ nodePath = process.execPath,
76
+ }) {
77
+ return {
78
+ mcpServers: {
79
+ harness: {
80
+ command: nodePath,
81
+ args: [
82
+ path.join(runtimeRoot, "lib", "mcp-stdio.mjs"),
83
+ "--agent",
84
+ client,
85
+ ],
86
+ },
87
+ },
88
+ };
37
89
  }
38
90
 
39
91
  // An extension or plugin runs inside its agent and starts Harness from the runtime and Node setup used, as the hooks
40
92
  // and MCP entries written here do.
41
- function installAsset(source, target, { runtimeRoot, nodePath = process.execPath }) {
93
+ function installAsset(
94
+ source,
95
+ target,
96
+ { runtimeRoot, nodePath = process.execPath }
97
+ ) {
42
98
  ensureDir(path.dirname(target));
43
- const text = fs.readFileSync(source, "utf8").replaceAll('"__HARNESS_RUNTIME__"', JSON.stringify(runtimeRoot)).replaceAll('"__HARNESS_NODE__"', JSON.stringify(nodePath));
99
+ const text = fs
100
+ .readFileSync(source, "utf8")
101
+ .replaceAll('"__HARNESS_RUNTIME__"', JSON.stringify(runtimeRoot))
102
+ .replaceAll('"__HARNESS_NODE__"', JSON.stringify(nodePath));
44
103
  fs.writeFileSync(target, text, { mode: 0o600 });
45
- try { fs.chmodSync(target, 0o600); } catch {}
104
+ try {
105
+ fs.chmodSync(target, 0o600);
106
+ } catch {}
46
107
  }
47
108
 
48
109
  function diagnostic(result) {
49
- return (result.stderr || result.stdout || result.error || "command failed").replace(/\s+/g, " ").trim().slice(0, 240);
110
+ return (result.stderr || result.stdout || result.error || "command failed")
111
+ .replace(/\s+/g, " ")
112
+ .trim()
113
+ .slice(0, 240);
50
114
  }
51
115
 
52
116
  export function integrationAssets(homeDir = os.homedir()) {
53
117
  return {
54
118
  pi: path.join(homeDir, ".pi", "agent", "extensions", "tryharness.ts"),
55
- opencode: path.join(homeDir, ".config", "opencode", "plugins", "tryharness.js"),
119
+ opencode: path.join(
120
+ homeDir,
121
+ ".config",
122
+ "opencode",
123
+ "plugins",
124
+ "tryharness.js"
125
+ ),
56
126
  };
57
127
  }
58
128
 
@@ -62,23 +132,74 @@ export function integrationAssets(homeDir = os.homedir()) {
62
132
  export function claudeSettingsPath(homeDir = os.homedir()) {
63
133
  return path.join(homeDir, ".claude", "settings.json");
64
134
  }
65
- const isHarnessHook = (entry) => JSON.stringify(entry).includes("/lib/nudge.mjs");
135
+ const isHarnessHook = (entry) =>
136
+ JSON.stringify(entry).includes("/lib/nudge.mjs");
66
137
 
67
- export function installClaudeSessionHook({ runtimeRoot, homeDir = os.homedir(), nodePath = process.execPath }) {
138
+ export function installClaudeSessionHook({
139
+ runtimeRoot,
140
+ homeDir = os.homedir(),
141
+ nodePath = process.execPath,
142
+ }) {
68
143
  const file = claudeSettingsPath(homeDir);
69
144
  const settings = readJson(file, {});
70
- const hooks = settings.hooks && typeof settings.hooks === "object" ? settings.hooks : {};
145
+ const hooks =
146
+ settings.hooks && typeof settings.hooks === "object" ? settings.hooks : {};
71
147
  const nudge = JSON.stringify(path.join(runtimeRoot, "lib", "nudge.mjs"));
72
- const starts = (Array.isArray(hooks.SessionStart) ? hooks.SessionStart : []).filter((entry) => !isHarnessHook(entry));
73
- starts.push({ hooks: [{ type: "command", command: `${JSON.stringify(nodePath)} ${nudge} --agent claude-code`, timeout: 10 }] });
148
+ const starts = (
149
+ Array.isArray(hooks.SessionStart) ? hooks.SessionStart : []
150
+ ).filter((entry) => !isHarnessHook(entry));
151
+ starts.push({
152
+ hooks: [
153
+ {
154
+ type: "command",
155
+ command: `${JSON.stringify(nodePath)} ${nudge} --agent claude-code`,
156
+ timeout: 10,
157
+ },
158
+ ],
159
+ });
74
160
  // Mid-session: on every prompt, one line only when the queue changed since the session last saw it.
75
- const prompts = (Array.isArray(hooks.UserPromptSubmit) ? hooks.UserPromptSubmit : []).filter((entry) => !isHarnessHook(entry));
76
- prompts.push({ hooks: [{ type: "command", command: `${JSON.stringify(nodePath)} ${nudge} --agent claude-code --if-changed`, timeout: 10 }] });
161
+ const prompts = (
162
+ Array.isArray(hooks.UserPromptSubmit) ? hooks.UserPromptSubmit : []
163
+ ).filter((entry) => !isHarnessHook(entry));
164
+ prompts.push({
165
+ hooks: [
166
+ {
167
+ type: "command",
168
+ command: `${JSON.stringify(nodePath)} ${nudge} --agent claude-code --if-changed`,
169
+ timeout: 10,
170
+ },
171
+ ],
172
+ });
77
173
  // When a turn ends: a message the person sent this session from Harness keeps it going.
78
- const stops = (Array.isArray(hooks.Stop) ? hooks.Stop : []).filter((entry) => !isHarnessHook(entry));
79
- stops.push({ hooks: [{ type: "command", command: `${JSON.stringify(nodePath)} ${nudge} --agent claude-code --stop`, timeout: 10 }] });
174
+ const stops = (Array.isArray(hooks.Stop) ? hooks.Stop : []).filter(
175
+ (entry) => !isHarnessHook(entry)
176
+ );
177
+ stops.push({
178
+ hooks: [
179
+ {
180
+ type: "command",
181
+ command: `${JSON.stringify(nodePath)} ${nudge} --agent claude-code --stop`,
182
+ timeout: 10,
183
+ },
184
+ ],
185
+ });
80
186
  ensureDir(path.dirname(file));
81
- fs.writeFileSync(file, JSON.stringify({ ...settings, hooks: { ...hooks, SessionStart: starts, UserPromptSubmit: prompts, Stop: stops } }, null, 2) + "\n");
187
+ fs.writeFileSync(
188
+ file,
189
+ JSON.stringify(
190
+ {
191
+ ...settings,
192
+ hooks: {
193
+ ...hooks,
194
+ SessionStart: starts,
195
+ UserPromptSubmit: prompts,
196
+ Stop: stops,
197
+ },
198
+ },
199
+ null,
200
+ 2
201
+ ) + "\n"
202
+ );
82
203
  }
83
204
 
84
205
  // Codex reads the same hook shape from ~/.codex/hooks.json (SessionStart, UserPromptSubmit and Stop,
@@ -88,19 +209,71 @@ export function codexHooksPath(homeDir = os.homedir()) {
88
209
  return path.join(homeDir, ".codex", "hooks.json");
89
210
  }
90
211
 
91
- export function installCodexHooks({ runtimeRoot, homeDir = os.homedir(), nodePath = process.execPath }) {
212
+ export function installCodexHooks({
213
+ runtimeRoot,
214
+ homeDir = os.homedir(),
215
+ nodePath = process.execPath,
216
+ }) {
92
217
  const file = codexHooksPath(homeDir);
93
218
  const settings = readJson(file, {});
94
- const hooks = settings.hooks && typeof settings.hooks === "object" ? settings.hooks : {};
219
+ const hooks =
220
+ settings.hooks && typeof settings.hooks === "object" ? settings.hooks : {};
95
221
  const nudge = JSON.stringify(path.join(runtimeRoot, "lib", "nudge.mjs"));
96
- const starts = (Array.isArray(hooks.SessionStart) ? hooks.SessionStart : []).filter((entry) => !isHarnessHook(entry));
97
- starts.push({ matcher: "startup|resume", hooks: [{ type: "command", command: `${JSON.stringify(nodePath)} ${nudge} --agent codex`, statusMessage: "Reading the Harness board", timeout: 10 }] });
98
- const prompts = (Array.isArray(hooks.UserPromptSubmit) ? hooks.UserPromptSubmit : []).filter((entry) => !isHarnessHook(entry));
99
- prompts.push({ hooks: [{ type: "command", command: `${JSON.stringify(nodePath)} ${nudge} --agent codex --if-changed`, timeout: 10 }] });
100
- const stops = (Array.isArray(hooks.Stop) ? hooks.Stop : []).filter((entry) => !isHarnessHook(entry));
101
- stops.push({ hooks: [{ type: "command", command: `${JSON.stringify(nodePath)} ${nudge} --agent codex --stop`, timeout: 10 }] });
222
+ const starts = (
223
+ Array.isArray(hooks.SessionStart) ? hooks.SessionStart : []
224
+ ).filter((entry) => !isHarnessHook(entry));
225
+ starts.push({
226
+ matcher: "startup|resume",
227
+ hooks: [
228
+ {
229
+ type: "command",
230
+ command: `${JSON.stringify(nodePath)} ${nudge} --agent codex`,
231
+ statusMessage: "Reading the Harness board",
232
+ timeout: 10,
233
+ },
234
+ ],
235
+ });
236
+ const prompts = (
237
+ Array.isArray(hooks.UserPromptSubmit) ? hooks.UserPromptSubmit : []
238
+ ).filter((entry) => !isHarnessHook(entry));
239
+ prompts.push({
240
+ hooks: [
241
+ {
242
+ type: "command",
243
+ command: `${JSON.stringify(nodePath)} ${nudge} --agent codex --if-changed`,
244
+ timeout: 10,
245
+ },
246
+ ],
247
+ });
248
+ const stops = (Array.isArray(hooks.Stop) ? hooks.Stop : []).filter(
249
+ (entry) => !isHarnessHook(entry)
250
+ );
251
+ stops.push({
252
+ hooks: [
253
+ {
254
+ type: "command",
255
+ command: `${JSON.stringify(nodePath)} ${nudge} --agent codex --stop`,
256
+ timeout: 10,
257
+ },
258
+ ],
259
+ });
102
260
  ensureDir(path.dirname(file));
103
- fs.writeFileSync(file, JSON.stringify({ ...settings, hooks: { ...hooks, SessionStart: starts, UserPromptSubmit: prompts, Stop: stops } }, null, 2) + "\n");
261
+ fs.writeFileSync(
262
+ file,
263
+ JSON.stringify(
264
+ {
265
+ ...settings,
266
+ hooks: {
267
+ ...hooks,
268
+ SessionStart: starts,
269
+ UserPromptSubmit: prompts,
270
+ Stop: stops,
271
+ },
272
+ },
273
+ null,
274
+ 2
275
+ ) + "\n"
276
+ );
104
277
  }
105
278
 
106
279
  export function removeCodexHooks(homeDir = os.homedir()) {
@@ -109,9 +282,13 @@ export function removeCodexHooks(homeDir = os.homedir()) {
109
282
  if (!settings?.hooks) return;
110
283
  const hooks = { ...settings.hooks };
111
284
  for (const event of ["SessionStart", "UserPromptSubmit", "Stop"]) {
112
- if (Array.isArray(hooks[event])) hooks[event] = hooks[event].filter((entry) => !isHarnessHook(entry));
285
+ if (Array.isArray(hooks[event]))
286
+ hooks[event] = hooks[event].filter((entry) => !isHarnessHook(entry));
113
287
  }
114
- fs.writeFileSync(file, JSON.stringify({ ...settings, hooks }, null, 2) + "\n");
288
+ fs.writeFileSync(
289
+ file,
290
+ JSON.stringify({ ...settings, hooks }, null, 2) + "\n"
291
+ );
115
292
  }
116
293
 
117
294
  export function removeClaudeSessionHook(homeDir = os.homedir()) {
@@ -120,12 +297,22 @@ export function removeClaudeSessionHook(homeDir = os.homedir()) {
120
297
  if (!settings?.hooks) return;
121
298
  const hooks = { ...settings.hooks };
122
299
  for (const event of ["SessionStart", "UserPromptSubmit", "Stop"]) {
123
- if (Array.isArray(hooks[event])) hooks[event] = hooks[event].filter((entry) => !isHarnessHook(entry));
300
+ if (Array.isArray(hooks[event]))
301
+ hooks[event] = hooks[event].filter((entry) => !isHarnessHook(entry));
124
302
  }
125
- fs.writeFileSync(file, JSON.stringify({ ...settings, hooks }, null, 2) + "\n");
303
+ fs.writeFileSync(
304
+ file,
305
+ JSON.stringify({ ...settings, hooks }, null, 2) + "\n"
306
+ );
126
307
  }
127
308
 
128
- export function configureAgents({ runtimeRoot, selected = AGENTS, homeDir = os.homedir(), detected = detectAgents(), execute = run }) {
309
+ export function configureAgents({
310
+ runtimeRoot,
311
+ selected = AGENTS,
312
+ homeDir = os.homedir(),
313
+ detected = detectAgents(),
314
+ execute = run,
315
+ }) {
129
316
  const byAgent = new Map(detected.map((entry) => [entry.agent, entry]));
130
317
  const mcpPath = path.join(runtimeRoot, "lib", "mcp-stdio.mjs");
131
318
  const assets = integrationAssets(homeDir);
@@ -133,34 +320,119 @@ export function configureAgents({ runtimeRoot, selected = AGENTS, homeDir = os.h
133
320
  for (const agent of AGENTS) {
134
321
  const found = byAgent.get(agent);
135
322
  if (!found?.path) {
136
- states.push({ agent, installed: false, configured: false, detail: "not installed" });
323
+ states.push({
324
+ agent,
325
+ installed: false,
326
+ configured: false,
327
+ detail: "not installed",
328
+ });
137
329
  continue;
138
330
  }
139
331
  if (!selected.includes(agent)) {
140
- states.push({ agent, installed: true, configured: false, detail: "not selected" });
332
+ states.push({
333
+ agent,
334
+ installed: true,
335
+ configured: false,
336
+ detail: "not selected",
337
+ });
141
338
  continue;
142
339
  }
143
340
  if (agent === "claude-code") {
144
341
  execute(found.path, ["mcp", "remove", "--scope", "user", "harness"]);
145
- const added = execute(found.path, ["mcp", "add", "--scope", "user", "harness", "--", process.execPath, mcpPath, "--agent", agent]);
342
+ const added = execute(found.path, [
343
+ "mcp",
344
+ "add",
345
+ "--scope",
346
+ "user",
347
+ "harness",
348
+ "--",
349
+ process.execPath,
350
+ mcpPath,
351
+ "--agent",
352
+ agent,
353
+ ]);
146
354
  let hooked = true;
147
- try { installClaudeSessionHook({ runtimeRoot, homeDir }); } catch { hooked = false; }
148
- states.push({ agent, installed: true, configured: added.ok, detail: added.ok ? (hooked ? "MCP and session-start nudge configured" : "MCP configured; the session-start hook could not be written") : diagnostic(added) });
355
+ try {
356
+ installClaudeSessionHook({ runtimeRoot, homeDir });
357
+ } catch {
358
+ hooked = false;
359
+ }
360
+ states.push({
361
+ agent,
362
+ installed: true,
363
+ configured: added.ok,
364
+ detail: added.ok
365
+ ? hooked
366
+ ? "MCP and session-start nudge configured"
367
+ : "MCP configured; the session-start hook could not be written"
368
+ : diagnostic(added),
369
+ });
149
370
  } else if (agent === "codex") {
150
371
  execute(found.path, ["mcp", "remove", "harness"]);
151
- const added = execute(found.path, ["mcp", "add", "harness", "--", process.execPath, mcpPath, "--agent", agent]);
372
+ const added = execute(found.path, [
373
+ "mcp",
374
+ "add",
375
+ "harness",
376
+ "--",
377
+ process.execPath,
378
+ mcpPath,
379
+ "--agent",
380
+ agent,
381
+ ]);
152
382
  let hooked = true;
153
- try { installCodexHooks({ runtimeRoot, homeDir }); } catch { hooked = false; }
154
- states.push({ agent, installed: true, configured: added.ok, detail: added.ok ? (hooked ? "MCP and session hooks configured; in Codex run /hooks once to trust them" : "MCP configured; the session hooks could not be written") : diagnostic(added) });
383
+ try {
384
+ installCodexHooks({ runtimeRoot, homeDir });
385
+ } catch {
386
+ hooked = false;
387
+ }
388
+ states.push({
389
+ agent,
390
+ installed: true,
391
+ configured: added.ok,
392
+ detail: added.ok
393
+ ? hooked
394
+ ? "MCP and session hooks configured; in Codex run /hooks once to trust them"
395
+ : "MCP configured; the session hooks could not be written"
396
+ : diagnostic(added),
397
+ });
155
398
  } else if (agent === "pi") {
156
399
  // pi has no MCP client: the extension starts the same stdio server and gives pi its tools.
157
- installAsset(path.join(runtimeRoot, "assets", "pi-extension.ts"), assets.pi, { runtimeRoot });
158
- states.push({ agent, installed: true, configured: fs.existsSync(assets.pi), detail: "extension installed" });
400
+ installAsset(
401
+ path.join(runtimeRoot, "assets", "pi-extension.ts"),
402
+ assets.pi,
403
+ { runtimeRoot }
404
+ );
405
+ states.push({
406
+ agent,
407
+ installed: true,
408
+ configured: fs.existsSync(assets.pi),
409
+ detail: "extension installed",
410
+ });
159
411
  } else {
160
- installAsset(path.join(runtimeRoot, "assets", "opencode-plugin.js"), assets.opencode, { runtimeRoot });
412
+ installAsset(
413
+ path.join(runtimeRoot, "assets", "opencode-plugin.js"),
414
+ assets.opencode,
415
+ { runtimeRoot }
416
+ );
161
417
  // The same stdio server as Claude Code and Codex. opencode has no `mcp remove`; `mcp add` replaces the entry.
162
- const added = execute(found.path, ["mcp", "add", "harness", "--", process.execPath, mcpPath, "--agent", agent]);
163
- states.push({ agent, installed: true, configured: added.ok, detail: added.ok ? "MCP and plugin configured" : `plugin installed; MCP: ${diagnostic(added)}` });
418
+ const added = execute(found.path, [
419
+ "mcp",
420
+ "add",
421
+ "harness",
422
+ "--",
423
+ process.execPath,
424
+ mcpPath,
425
+ "--agent",
426
+ agent,
427
+ ]);
428
+ states.push({
429
+ agent,
430
+ installed: true,
431
+ configured: added.ok,
432
+ detail: added.ok
433
+ ? "MCP and plugin configured"
434
+ : `plugin installed; MCP: ${diagnostic(added)}`,
435
+ });
164
436
  }
165
437
  }
166
438
  return states;
@@ -170,11 +442,20 @@ export function agentHealth(states) {
170
442
  return Object.fromEntries(states.map((state) => [state.agent, state]));
171
443
  }
172
444
 
173
- export function removeAgentConfigurations({ homeDir = os.homedir(), detected = detectAgents(), execute = run } = {}) {
445
+ export function removeAgentConfigurations({
446
+ homeDir = os.homedir(),
447
+ detected = detectAgents(),
448
+ execute = run,
449
+ } = {}) {
174
450
  for (const found of detected) {
175
451
  if (!found.path) continue;
176
- if (found.agent === "claude-code") { execute(found.path, ["mcp", "remove", "--scope", "user", "harness"]); removeClaudeSessionHook(homeDir); }
177
- else if (found.agent === "codex") { execute(found.path, ["mcp", "remove", "harness"]); removeCodexHooks(homeDir); }
452
+ if (found.agent === "claude-code") {
453
+ execute(found.path, ["mcp", "remove", "--scope", "user", "harness"]);
454
+ removeClaudeSessionHook(homeDir);
455
+ } else if (found.agent === "codex") {
456
+ execute(found.path, ["mcp", "remove", "harness"]);
457
+ removeCodexHooks(homeDir);
458
+ }
178
459
  }
179
460
  // opencode has no `mcp remove`, so its entry comes out of the config file `mcp add` wrote it to.
180
461
  for (const name of ["opencode.json", "opencode.jsonc"]) {
@@ -186,7 +467,9 @@ export function removeAgentConfigurations({ homeDir = os.homedir(), detected = d
186
467
  }
187
468
  const assets = integrationAssets(homeDir);
188
469
  for (const target of Object.values(assets)) {
189
- try { fs.unlinkSync(target); } catch (error) {
470
+ try {
471
+ fs.unlinkSync(target);
472
+ } catch (error) {
190
473
  if (error?.code !== "ENOENT") throw error;
191
474
  }
192
475
  }