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.
- package/README.md +48 -8
- package/assets/opencode-plugin.js +48 -10
- package/assets/pi-extension.ts +106 -26
- package/cli.mjs +1281 -289
- package/lib/activity.mjs +197 -40
- package/lib/adapters.mjs +337 -54
- package/lib/agentnode.mjs +175 -35
- package/lib/atlas.mjs +604 -0
- package/lib/bip39-english.txt +2048 -0
- package/lib/cards.mjs +177 -39
- package/lib/claudemem.mjs +74 -18
- package/lib/commands.mjs +13 -5
- package/lib/context.mjs +628 -0
- package/lib/control-client.mjs +5 -2
- package/lib/coordinator.mjs +281 -56
- package/lib/daemon.mjs +5590 -838
- package/lib/desktop.mjs +149 -35
- package/lib/digest.mjs +81 -0
- package/lib/environment.mjs +180 -0
- package/lib/files.mjs +246 -13
- package/lib/firewall.mjs +288 -0
- package/lib/folders.mjs +223 -57
- package/lib/frames.mjs +333 -0
- package/lib/inference.mjs +663 -0
- package/lib/learn.mjs +182 -0
- package/lib/live.mjs +286 -61
- package/lib/localmodels.mjs +571 -0
- package/lib/mcp-stdio.mjs +912 -103
- package/lib/media.mjs +228 -0
- 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 +190 -32
- package/lib/ocr.mjs +380 -0
- package/lib/onboard.mjs +321 -70
- package/lib/parsers.mjs +132 -41
- package/lib/paths.mjs +33 -10
- package/lib/personal.mjs +77 -0
- package/lib/phones.mjs +398 -51
- package/lib/places.mjs +566 -102
- package/lib/provenance.mjs +19 -4
- package/lib/receipts.mjs +82 -0
- package/lib/redact.mjs +730 -0
- package/lib/runner.mjs +347 -55
- package/lib/runtime-install.mjs +59 -17
- package/lib/screens.mjs +212 -0
- package/lib/sealed.mjs +160 -28
- package/lib/secrets.mjs +18 -5
- package/lib/service.mjs +173 -35
- package/lib/snippets.mjs +140 -0
- package/lib/speech.mjs +993 -0
- package/lib/store.mjs +1678 -301
- package/lib/swarm.mjs +598 -117
- package/lib/t3.mjs +400 -79
- package/lib/trails.mjs +401 -0
- package/lib/ui.mjs +95 -23
- package/lib/vectors.mjs +5 -2
- package/lib/wakes.mjs +284 -68
- package/lib/watches.mjs +362 -0
- package/lib/worktrees.mjs +186 -38
- package/package.json +6 -4
- 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": {
|
|
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: {
|
|
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({
|
|
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(
|
|
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({
|
|
30
|
-
|
|
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({
|
|
36
|
-
|
|
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(
|
|
93
|
+
function installAsset(
|
|
94
|
+
source,
|
|
95
|
+
target,
|
|
96
|
+
{ runtimeRoot, nodePath = process.execPath }
|
|
97
|
+
) {
|
|
42
98
|
ensureDir(path.dirname(target));
|
|
43
|
-
const text = fs
|
|
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 {
|
|
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")
|
|
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(
|
|
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) =>
|
|
135
|
+
const isHarnessHook = (entry) =>
|
|
136
|
+
JSON.stringify(entry).includes("/lib/nudge.mjs");
|
|
66
137
|
|
|
67
|
-
export function installClaudeSessionHook({
|
|
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 =
|
|
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 = (
|
|
73
|
-
|
|
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 = (
|
|
76
|
-
|
|
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(
|
|
79
|
-
|
|
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(
|
|
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({
|
|
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 =
|
|
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 = (
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
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(
|
|
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]))
|
|
285
|
+
if (Array.isArray(hooks[event]))
|
|
286
|
+
hooks[event] = hooks[event].filter((entry) => !isHarnessHook(entry));
|
|
113
287
|
}
|
|
114
|
-
fs.writeFileSync(
|
|
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]))
|
|
300
|
+
if (Array.isArray(hooks[event]))
|
|
301
|
+
hooks[event] = hooks[event].filter((entry) => !isHarnessHook(entry));
|
|
124
302
|
}
|
|
125
|
-
fs.writeFileSync(
|
|
303
|
+
fs.writeFileSync(
|
|
304
|
+
file,
|
|
305
|
+
JSON.stringify({ ...settings, hooks }, null, 2) + "\n"
|
|
306
|
+
);
|
|
126
307
|
}
|
|
127
308
|
|
|
128
|
-
export function configureAgents({
|
|
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({
|
|
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({
|
|
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, [
|
|
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 {
|
|
148
|
-
|
|
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, [
|
|
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 {
|
|
154
|
-
|
|
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(
|
|
158
|
-
|
|
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(
|
|
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, [
|
|
163
|
-
|
|
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({
|
|
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") {
|
|
177
|
-
|
|
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 {
|
|
470
|
+
try {
|
|
471
|
+
fs.unlinkSync(target);
|
|
472
|
+
} catch (error) {
|
|
190
473
|
if (error?.code !== "ENOENT") throw error;
|
|
191
474
|
}
|
|
192
475
|
}
|