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/onboard.mjs
CHANGED
|
@@ -1,6 +1,11 @@
|
|
|
1
1
|
import { spawnSync } from "node:child_process";
|
|
2
2
|
import pc from "picocolors";
|
|
3
|
-
import {
|
|
3
|
+
import {
|
|
4
|
+
detectLocalAgents,
|
|
5
|
+
setupHermes,
|
|
6
|
+
setupOpenClaw,
|
|
7
|
+
localWakes,
|
|
8
|
+
} from "./wakes.mjs";
|
|
4
9
|
import { columns, createUi, wrap } from "./ui.mjs";
|
|
5
10
|
|
|
6
11
|
// "Your swarm" in setup: every agent class with how Harness reaches it, the recommended way already chosen, and a real
|
|
@@ -8,13 +13,19 @@ import { columns, createUi, wrap } from "./ui.mjs";
|
|
|
8
13
|
// `npx tryharness agents`. Without a terminal to answer in, it only says what is set up and how to finish.
|
|
9
14
|
|
|
10
15
|
const SETUP = { openclaw: setupOpenClaw, hermes: setupHermes };
|
|
11
|
-
const LABEL = {
|
|
12
|
-
|
|
16
|
+
const LABEL = {
|
|
17
|
+
openclaw: "OpenClaw",
|
|
18
|
+
hermes: "Hermes",
|
|
19
|
+
"grok-bot": "Grok Bot",
|
|
20
|
+
};
|
|
21
|
+
const later = (agent) =>
|
|
22
|
+
`Later: ${pc.cyan(`npx tryharness agents setup ${agent}`)}`;
|
|
13
23
|
|
|
14
24
|
// The Harness mark (src/main/home/landing/walkthrough/BrandMark.tsx) in box characters: dim corners, the bright one in
|
|
15
25
|
// Harness teal, the wordmark beside it. Plain when the output is not a terminal or NO_COLOR is set.
|
|
16
26
|
export function banner(version, { color = pc.isColorSupported } = {}) {
|
|
17
|
-
const c = pc.createColors(color),
|
|
27
|
+
const c = pc.createColors(color),
|
|
28
|
+
teal = (text) => (color ? `\x1b[38;5;36m${text}\x1b[39m` : text);
|
|
18
29
|
const rows = [
|
|
19
30
|
[c.dim("┌───"), " ", teal("───┐")],
|
|
20
31
|
[c.dim("│"), " ", teal("│")],
|
|
@@ -26,34 +37,90 @@ export function banner(version, { color = pc.isColorSupported } = {}) {
|
|
|
26
37
|
[c.dim("│"), " ", c.dim("│")],
|
|
27
38
|
[c.dim("└───"), " ", c.dim("───┘")],
|
|
28
39
|
];
|
|
29
|
-
const side = {
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
40
|
+
const side = {
|
|
41
|
+
3: c.bold("harness relay"),
|
|
42
|
+
4: "your personal swarm",
|
|
43
|
+
5: c.dim(`setup ${version}`),
|
|
44
|
+
};
|
|
45
|
+
return rows
|
|
46
|
+
.map((parts, index) => {
|
|
47
|
+
const line = parts.join("");
|
|
48
|
+
const width = line.replace(/\x1b\[[0-9;]*m/g, "").length;
|
|
49
|
+
return side[index]
|
|
50
|
+
? ` ${line}${" ".repeat(23 - width)}${side[index]}`
|
|
51
|
+
: ` ${line}`.trimEnd();
|
|
52
|
+
})
|
|
53
|
+
.join("\n");
|
|
35
54
|
}
|
|
36
55
|
|
|
37
56
|
const copy = (text) => {
|
|
38
|
-
const tool =
|
|
39
|
-
|
|
57
|
+
const tool =
|
|
58
|
+
process.platform === "darwin"
|
|
59
|
+
? "pbcopy"
|
|
60
|
+
: process.platform === "win32"
|
|
61
|
+
? "clip"
|
|
62
|
+
: "xclip";
|
|
63
|
+
const result = spawnSync(
|
|
64
|
+
tool,
|
|
65
|
+
tool === "xclip" ? ["-selection", "clipboard"] : [],
|
|
66
|
+
{ input: text }
|
|
67
|
+
);
|
|
40
68
|
return result.status === 0;
|
|
41
69
|
};
|
|
42
70
|
|
|
43
71
|
/** Post a setup check for the agent and wait for its report: the same path real work takes. */
|
|
44
|
-
export async function testAgent({
|
|
72
|
+
export async function testAgent({
|
|
73
|
+
agent,
|
|
74
|
+
daemonRequest,
|
|
75
|
+
ui = createUi(),
|
|
76
|
+
waitMs = 10 * 60_000,
|
|
77
|
+
sleep = (ms) => new Promise((r) => setTimeout(r, ms)),
|
|
78
|
+
}) {
|
|
45
79
|
const name = LABEL[agent] || agent;
|
|
46
|
-
const posted = await daemonRequest({
|
|
47
|
-
|
|
48
|
-
|
|
80
|
+
const posted = await daemonRequest({
|
|
81
|
+
command: "board.post",
|
|
82
|
+
agent: "user",
|
|
83
|
+
objective: "Setup check from Harness: reply ready",
|
|
84
|
+
brief:
|
|
85
|
+
"Harness is checking that it can wake you. Claim this task and report it done with the single word: ready. Do nothing else.",
|
|
86
|
+
targetAgent: agent,
|
|
87
|
+
});
|
|
88
|
+
if (!posted?.ok) {
|
|
89
|
+
ui.warn(
|
|
90
|
+
`The test task could not be posted: ${posted?.error || "no answer"}`
|
|
91
|
+
);
|
|
92
|
+
return false;
|
|
93
|
+
}
|
|
94
|
+
const started = Date.now(),
|
|
95
|
+
spin = ui.spinner();
|
|
49
96
|
spin.start(`Waiting for ${name} to answer a test task`);
|
|
50
97
|
while (Date.now() - started < waitMs) {
|
|
51
98
|
await sleep(2_000);
|
|
52
|
-
const task = (
|
|
53
|
-
|
|
54
|
-
|
|
99
|
+
const task = (
|
|
100
|
+
await daemonRequest({
|
|
101
|
+
command: "board.get",
|
|
102
|
+
agent: "user",
|
|
103
|
+
id: posted.task.id,
|
|
104
|
+
}).catch(() => null)
|
|
105
|
+
)?.task;
|
|
106
|
+
if (task?.status === "done") {
|
|
107
|
+
spin.stop(
|
|
108
|
+
`${name} answered in ${Math.round((Date.now() - started) / 1000)}s. Ready.`
|
|
109
|
+
);
|
|
110
|
+
return true;
|
|
111
|
+
}
|
|
112
|
+
if (task && ["failed", "needs_input"].includes(task.status)) {
|
|
113
|
+
spin.stop(
|
|
114
|
+
`${name} answered, but the task is ${task.status}${task.result ? `: ${task.result}` : ""}`,
|
|
115
|
+
2
|
|
116
|
+
);
|
|
117
|
+
return false;
|
|
118
|
+
}
|
|
55
119
|
}
|
|
56
|
-
spin.stop(
|
|
120
|
+
spin.stop(
|
|
121
|
+
`No answer after ${Math.round(waitMs / 60_000)} minutes. Check that ${name} is running, then: npx tryharness agents test ${agent}`,
|
|
122
|
+
2
|
|
123
|
+
);
|
|
57
124
|
return false;
|
|
58
125
|
}
|
|
59
126
|
|
|
@@ -61,21 +128,50 @@ async function setupLocal({ entry, runtimeRoot, ui, daemonRequest }) {
|
|
|
61
128
|
const name = LABEL[entry.agent];
|
|
62
129
|
const run = async () => {
|
|
63
130
|
const result = SETUP[entry.agent]({ runtimeRoot });
|
|
64
|
-
if (!result.ok) {
|
|
131
|
+
if (!result.ok) {
|
|
132
|
+
ui.warn(result.detail);
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
65
135
|
ui.success(result.detail);
|
|
66
136
|
await testAgent({ agent: entry.agent, daemonRequest, ui });
|
|
67
137
|
};
|
|
68
138
|
if (localWakes()[entry.agent]) {
|
|
69
|
-
const health = (
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
if (!
|
|
139
|
+
const health = (
|
|
140
|
+
await daemonRequest({ command: "wake.local" }).catch(() => null)
|
|
141
|
+
)?.agents?.[entry.agent];
|
|
142
|
+
if (!health || health.answering) {
|
|
143
|
+
ui.success(`${name} is woken on this computer by its own webhook`);
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
ui.warn(
|
|
147
|
+
`${name} is not answering wakes (${health.lastFailure}). Check that it is running.`
|
|
148
|
+
);
|
|
149
|
+
if (
|
|
150
|
+
!ui.interactive ||
|
|
151
|
+
!(await ui.confirm({
|
|
152
|
+
message: `Set ${name} up again?`,
|
|
153
|
+
initialValue: true,
|
|
154
|
+
}))
|
|
155
|
+
) {
|
|
156
|
+
ui.info(later(entry.agent));
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
73
159
|
await run();
|
|
74
160
|
return;
|
|
75
161
|
}
|
|
76
|
-
if (!ui.interactive) {
|
|
77
|
-
|
|
78
|
-
|
|
162
|
+
if (!ui.interactive) {
|
|
163
|
+
ui.info(`${name} found. ${later(entry.agent)}`);
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
166
|
+
ui.step(
|
|
167
|
+
`${name} found on this computer\n${pc.dim(`Harness adds itself to ${name}'s MCP servers and turns on its webhook with a key made here${entry.agent === "openclaw" ? ", then restarts its gateway" : ""}. Nothing to copy.`)}`
|
|
168
|
+
);
|
|
169
|
+
if (
|
|
170
|
+
!(await ui.confirm({ message: `Set up ${name} now?`, initialValue: true }))
|
|
171
|
+
) {
|
|
172
|
+
ui.info(`Skipped. ${later(entry.agent)}`);
|
|
173
|
+
return;
|
|
174
|
+
}
|
|
79
175
|
await run();
|
|
80
176
|
}
|
|
81
177
|
|
|
@@ -89,64 +185,190 @@ async function waitFor(check, waitMs = 10 * 60_000, everyMs = 3_000) {
|
|
|
89
185
|
}
|
|
90
186
|
|
|
91
187
|
async function setupGrok({ ui, daemonRequest, clipboard }) {
|
|
92
|
-
const list = () =>
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
188
|
+
const list = () =>
|
|
189
|
+
daemonRequest({ command: "wake.server", input: { action: "list" } });
|
|
190
|
+
const listed = await list().catch((error) => ({
|
|
191
|
+
ok: false,
|
|
192
|
+
error: error.message,
|
|
193
|
+
}));
|
|
194
|
+
if (!listed.ok) {
|
|
195
|
+
ui.warn(
|
|
196
|
+
`Harness could not be reached (${listed.error}). ${later("grok-bot")}`
|
|
197
|
+
);
|
|
198
|
+
return;
|
|
199
|
+
}
|
|
200
|
+
const current = (listed.wakes || []).find(
|
|
201
|
+
(wake) => wake.agent === "grok-bot"
|
|
202
|
+
);
|
|
96
203
|
if (current) {
|
|
97
|
-
const how =
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
204
|
+
const how =
|
|
205
|
+
current.channel === "email"
|
|
206
|
+
? `email at ${current.address}`
|
|
207
|
+
: "its webhook";
|
|
208
|
+
if (current.answering) {
|
|
209
|
+
ui.success(`Grok Bot is woken by ${how}`);
|
|
210
|
+
return;
|
|
211
|
+
}
|
|
212
|
+
ui.warn(
|
|
213
|
+
`Grok Bot is not answering wakes by ${how} (${current.last_failure})`
|
|
214
|
+
);
|
|
215
|
+
if (
|
|
216
|
+
!ui.interactive ||
|
|
217
|
+
!(await ui.confirm({ message: "Set it up again?", initialValue: true }))
|
|
218
|
+
) {
|
|
219
|
+
ui.info(later("grok-bot"));
|
|
220
|
+
return;
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
if (!ui.interactive) {
|
|
224
|
+
ui.info(`Grok Bot is not set up. ${later("grok-bot")}`);
|
|
225
|
+
return;
|
|
101
226
|
}
|
|
102
|
-
if (!ui.interactive) { ui.info(`Grok Bot is not set up. ${later("grok-bot")}`); return; }
|
|
103
227
|
const channel = await ui.select({
|
|
104
228
|
message: "How should Harness wake Grok Bot?",
|
|
105
229
|
initialValue: "email",
|
|
106
230
|
options: [
|
|
107
|
-
{
|
|
108
|
-
|
|
231
|
+
{
|
|
232
|
+
value: "email",
|
|
233
|
+
label: "Email",
|
|
234
|
+
hint: "recommended · about 23s · nothing to copy back",
|
|
235
|
+
},
|
|
236
|
+
{
|
|
237
|
+
value: "webhook",
|
|
238
|
+
label: "Webhook",
|
|
239
|
+
hint: "about 17s · you paste its URL and key here",
|
|
240
|
+
},
|
|
109
241
|
{ value: "skip", label: "Not now" },
|
|
110
242
|
],
|
|
111
243
|
});
|
|
112
|
-
if (!channel || channel === "skip") {
|
|
244
|
+
if (!channel || channel === "skip") {
|
|
245
|
+
ui.info(`Skipped. ${later("grok-bot")}`);
|
|
246
|
+
return;
|
|
247
|
+
}
|
|
113
248
|
const message = listed.messages?.["grok-bot"]?.[channel];
|
|
114
|
-
if (!message) {
|
|
115
|
-
|
|
249
|
+
if (!message) {
|
|
250
|
+
ui.warn(
|
|
251
|
+
"Harness did not send the setup message. Update tryharness and try again."
|
|
252
|
+
);
|
|
253
|
+
return;
|
|
254
|
+
}
|
|
255
|
+
const setup = (listed.setup || []).find(
|
|
256
|
+
(entry) => entry.agent === "grok-bot"
|
|
257
|
+
);
|
|
116
258
|
const connected = Boolean(setup) && !setup.missing?.includes("connection");
|
|
117
259
|
const copied = clipboard(message);
|
|
118
|
-
ui.note(
|
|
119
|
-
|
|
120
|
-
|
|
260
|
+
ui.note(
|
|
261
|
+
wrap(message, Math.max(40, columns() - 8)).join("\n"),
|
|
262
|
+
copied
|
|
263
|
+
? "Paste this into Grok · it is on your clipboard"
|
|
264
|
+
: "Paste this into Grok"
|
|
265
|
+
);
|
|
266
|
+
if (connected)
|
|
267
|
+
ui.success(
|
|
268
|
+
`Grok is already connected to Harness as Grok Bot${setup.old_approval ? `\n${pc.dim("approved before it could register its own address, so it gives you its address instead")}` : ""}`
|
|
269
|
+
);
|
|
270
|
+
else
|
|
271
|
+
ui.info(
|
|
272
|
+
"Grok opens Harness's approval page. Name the connection Grok Bot and approve it."
|
|
273
|
+
);
|
|
121
274
|
|
|
122
275
|
const save = async (input, done) => {
|
|
123
|
-
const saved = await daemonRequest({
|
|
124
|
-
|
|
276
|
+
const saved = await daemonRequest({
|
|
277
|
+
command: "wake.server",
|
|
278
|
+
input: { action: "save", agent: "grok-bot", ...input },
|
|
279
|
+
});
|
|
280
|
+
if (!saved.ok) {
|
|
281
|
+
ui.warn(
|
|
282
|
+
`${saved.error}. Try again: npx tryharness agents setup grok-bot`
|
|
283
|
+
);
|
|
284
|
+
return false;
|
|
285
|
+
}
|
|
125
286
|
ui.success(done(saved));
|
|
126
287
|
return true;
|
|
127
288
|
};
|
|
128
289
|
if (channel === "webhook") {
|
|
129
|
-
ui.info(
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
const
|
|
133
|
-
|
|
134
|
-
|
|
290
|
+
ui.info(
|
|
291
|
+
"In Grok, open Routines, then Harness wake, and copy its webhook URL and key."
|
|
292
|
+
);
|
|
293
|
+
const address = await ui.text({
|
|
294
|
+
message: "Grok's webhook URL",
|
|
295
|
+
placeholder: "https://",
|
|
296
|
+
validate: (value) =>
|
|
297
|
+
/^https:\/\/\S+$/.test(value.trim())
|
|
298
|
+
? undefined
|
|
299
|
+
: "Paste the https URL from Grok's routine",
|
|
300
|
+
});
|
|
301
|
+
if (!address) {
|
|
302
|
+
ui.info(`Skipped. ${later("grok-bot")}`);
|
|
303
|
+
return;
|
|
304
|
+
}
|
|
305
|
+
const key = await ui.password({
|
|
306
|
+
message: "Its key",
|
|
307
|
+
validate: (value) =>
|
|
308
|
+
value.trim() ? undefined : "Paste the key from Grok's routine",
|
|
309
|
+
});
|
|
310
|
+
if (!key) {
|
|
311
|
+
ui.info(`Skipped. ${later("grok-bot")}`);
|
|
312
|
+
return;
|
|
313
|
+
}
|
|
314
|
+
if (
|
|
315
|
+
!(await save(
|
|
316
|
+
{ channel: "webhook", address: address.trim(), key },
|
|
317
|
+
() =>
|
|
318
|
+
"Saved. The key is stored sealed on Harness and never shown again."
|
|
319
|
+
))
|
|
320
|
+
)
|
|
321
|
+
return;
|
|
135
322
|
} else {
|
|
136
|
-
const ownAddress = (value) =>
|
|
323
|
+
const ownAddress = (value) =>
|
|
324
|
+
/^[^\s@]+@mail\.grokbot\.com$/i.test(value.trim())
|
|
325
|
+
? undefined
|
|
326
|
+
: "Grok's address ends in @mail.grokbot.com";
|
|
137
327
|
let address = "";
|
|
138
328
|
if (setup?.old_approval) {
|
|
139
|
-
address =
|
|
140
|
-
|
|
329
|
+
address =
|
|
330
|
+
(await ui.text({
|
|
331
|
+
message: "Grok's email address",
|
|
332
|
+
placeholder: "name@mail.grokbot.com",
|
|
333
|
+
validate: ownAddress,
|
|
334
|
+
})) ?? "";
|
|
335
|
+
if (!address) {
|
|
336
|
+
ui.info(`Skipped. ${later("grok-bot")}`);
|
|
337
|
+
return;
|
|
338
|
+
}
|
|
141
339
|
} else {
|
|
142
340
|
// Grok registers its own address; the person may type the one Grok gives instead, and the prompt closes when it registers.
|
|
143
341
|
let settled = false;
|
|
144
|
-
const registered = waitFor(
|
|
145
|
-
|
|
146
|
-
|
|
342
|
+
const registered = waitFor(
|
|
343
|
+
async () =>
|
|
344
|
+
settled ||
|
|
345
|
+
((await list().catch(() => null))?.wakes || []).some(
|
|
346
|
+
(wake) => wake.agent === "grok-bot"
|
|
347
|
+
)
|
|
348
|
+
).then((ok) =>
|
|
349
|
+
ok
|
|
350
|
+
? { ok, line: "Grok registered its address" }
|
|
351
|
+
: {
|
|
352
|
+
ok,
|
|
353
|
+
line: "Grok has not registered yet. When it has: npx tryharness agents test grok-bot",
|
|
354
|
+
}
|
|
355
|
+
);
|
|
356
|
+
const answer = await ui.waitOrType({
|
|
357
|
+
message: "Waiting for Grok to register its address",
|
|
358
|
+
placeholder: "or type the address Grok gives you",
|
|
359
|
+
until: registered,
|
|
360
|
+
});
|
|
147
361
|
let outcome = answer.done;
|
|
148
|
-
if (answer.cancelled) {
|
|
149
|
-
|
|
362
|
+
if (answer.cancelled) {
|
|
363
|
+
settled = true;
|
|
364
|
+
ui.info(`Stopped waiting. ${later("grok-bot")}`);
|
|
365
|
+
return;
|
|
366
|
+
}
|
|
367
|
+
if (answer.value && ownAddress(answer.value)) {
|
|
368
|
+
settled = true;
|
|
369
|
+
ui.warn(`${ownAddress(answer.value)}. ${later("grok-bot")}`);
|
|
370
|
+
return;
|
|
371
|
+
}
|
|
150
372
|
if (answer.value) address = answer.value;
|
|
151
373
|
else if (!outcome) {
|
|
152
374
|
const spin = ui.spinner();
|
|
@@ -157,25 +379,54 @@ async function setupGrok({ ui, daemonRequest, clipboard }) {
|
|
|
157
379
|
settled = true;
|
|
158
380
|
if (outcome && !outcome.ok) return;
|
|
159
381
|
}
|
|
160
|
-
if (
|
|
382
|
+
if (
|
|
383
|
+
address &&
|
|
384
|
+
!(await save(
|
|
385
|
+
{ channel: "email", address: address.trim() },
|
|
386
|
+
(saved) =>
|
|
387
|
+
`Saved. Harness wakes Grok at ${saved.wake?.address ?? address.trim()}`
|
|
388
|
+
))
|
|
389
|
+
)
|
|
390
|
+
return;
|
|
161
391
|
}
|
|
162
392
|
await testAgent({ agent: "grok-bot", daemonRequest, ui });
|
|
163
393
|
}
|
|
164
394
|
|
|
165
395
|
/** The whole "Your swarm" step. `only` limits it to one agent (npx tryharness agents setup <agent>). */
|
|
166
|
-
export async function setupSwarm({
|
|
396
|
+
export async function setupSwarm({
|
|
397
|
+
runtimeRoot,
|
|
398
|
+
states = [],
|
|
399
|
+
daemonRequest,
|
|
400
|
+
ui = createUi(),
|
|
401
|
+
only = null,
|
|
402
|
+
clipboard = copy,
|
|
403
|
+
}) {
|
|
167
404
|
const want = (agent) => !only || only === agent;
|
|
168
405
|
if (!only && states.length) {
|
|
169
406
|
const width = Math.max(...states.map((state) => state.agent.length));
|
|
170
|
-
const rows = states.map(
|
|
171
|
-
|
|
407
|
+
const rows = states.map(
|
|
408
|
+
(state) =>
|
|
409
|
+
`${!state.installed ? pc.dim("–") : state.configured ? pc.green("✓") : pc.yellow("!")} ${state.agent.padEnd(width)} ${pc.dim(state.detail)}`
|
|
410
|
+
);
|
|
411
|
+
ui.step(
|
|
412
|
+
`Coding agents on this computer ${pc.dim("· Harness starts them when work is posted")}\n${rows.join("\n")}`
|
|
413
|
+
);
|
|
172
414
|
}
|
|
173
|
-
const local = detectLocalAgents().filter(
|
|
174
|
-
|
|
175
|
-
|
|
415
|
+
const local = detectLocalAgents().filter(
|
|
416
|
+
(entry) => entry.found && want(entry.agent)
|
|
417
|
+
);
|
|
418
|
+
for (const entry of local)
|
|
419
|
+
await setupLocal({ entry, runtimeRoot, ui, daemonRequest });
|
|
420
|
+
if (only && LABEL[only] && only !== "grok-bot" && !local.length)
|
|
421
|
+
ui.warn(`${LABEL[only]} was not found on this computer.`);
|
|
176
422
|
if (want("grok-bot")) {
|
|
177
|
-
ui.step(
|
|
423
|
+
ui.step(
|
|
424
|
+
`Grok Bot ${pc.dim("· runs in its own cloud, woken by email or webhook")}`
|
|
425
|
+
);
|
|
178
426
|
await setupGrok({ ui, daemonRequest, clipboard });
|
|
179
427
|
}
|
|
180
|
-
if (!only)
|
|
428
|
+
if (!only)
|
|
429
|
+
ui.info(
|
|
430
|
+
`Bankr and Quotient run on Harness ${pc.dim("· nothing to set up")}`
|
|
431
|
+
);
|
|
181
432
|
}
|