wisp-agents 0.8.0 → 0.9.0
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/CHANGELOG.md +20 -1
- package/README.md +28 -12
- package/bin/wisp.js +1 -1
- package/package.json +2 -2
- package/server/adapters/claude.js +3 -1
- package/server/adapters/grok.js +251 -0
- package/server/adapters/index.js +2 -1
- package/server/adapters/opencode.js +3 -1
- package/server/index.js +169 -18
- package/server/machines.js +672 -0
- package/server/mcp-ask.js +1 -1
- package/server/mcp-wisp.js +2 -2
- package/server/openapi.js +17 -2
- package/server/remote.js +8 -4
- package/server/runner.js +2 -0
- package/server/store.js +11 -6
- package/server/subagents.js +2 -1
- package/skills/wisp/SKILL.md +2 -2
- package/web/css/app.css +166 -39
- package/web/index.html +5 -0
- package/web/js/app.js +603 -80
- package/web/logos/grok.svg +1 -0
package/server/index.js
CHANGED
|
@@ -22,6 +22,7 @@ import { createWorktree, worktreeSource, ensureWorktree, removeWorktree, worktre
|
|
|
22
22
|
import { normalizeRecipe, findRecipe, recipeView, fill, positional } from './recipes.js';
|
|
23
23
|
import { Triggers, normalizeTrigger, triggerView } from './triggers.js';
|
|
24
24
|
import { Remote } from './remote.js';
|
|
25
|
+
import { Machines, splitId, qualify, sshHosts } from './machines.js';
|
|
25
26
|
import { usage } from './usage.js';
|
|
26
27
|
import { normalizeTeam, normalizePreset, delegationReport } from './subagents.js';
|
|
27
28
|
|
|
@@ -87,6 +88,30 @@ export async function startServer({ port = 4477, host = '127.0.0.1', handleSigna
|
|
|
87
88
|
runner.on('finished', (f) => broadcast('finished', f));
|
|
88
89
|
runner.on('finished', ({ session }) => notifyWebhooks(store, session));
|
|
89
90
|
runner.on('finished', ({ session }) => remote.notify(store.get(session.id), session).catch((err) => console.warn(`wisp: phone: ${err.message}`)));
|
|
91
|
+
|
|
92
|
+
// Machines: other computers running their own wisp, reached over SSH. Their sessions show up
|
|
93
|
+
// here as <id>@<machine>; their events are relayed into this stream.
|
|
94
|
+
const machines = new Machines({
|
|
95
|
+
store,
|
|
96
|
+
version: VERSION,
|
|
97
|
+
apiVersion: API_VERSION,
|
|
98
|
+
onChange: () => broadcast('machines', machines.view()),
|
|
99
|
+
onEvent: (event, data) => {
|
|
100
|
+
broadcast(event, data);
|
|
101
|
+
if (event === 'finished' && remote.on) notifyFromMachine(data.session).catch((err) => console.warn(`wisp: phone: ${err.message}`));
|
|
102
|
+
},
|
|
103
|
+
});
|
|
104
|
+
async function notifyFromMachine(summary) {
|
|
105
|
+
const at = splitId(summary.id);
|
|
106
|
+
const s = machines.session(at.machine, await machines.json(at.machine, 'GET', `/sessions/${at.id}`));
|
|
107
|
+
await remote.notify(s, summary);
|
|
108
|
+
}
|
|
109
|
+
// Attachments are uploaded here; a machine gets its own copies before a launch or reply.
|
|
110
|
+
const pushUploads = async (mid, ids) => {
|
|
111
|
+
const out = [];
|
|
112
|
+
for (const u of resolveAttachments(ids)) out.push((await machines.upload(mid, u)).id);
|
|
113
|
+
return out;
|
|
114
|
+
};
|
|
90
115
|
setInterval(() => broadcast('ping', {}), 25_000).unref();
|
|
91
116
|
|
|
92
117
|
const telemetry = new Telemetry(store);
|
|
@@ -150,7 +175,8 @@ export async function startServer({ port = 4477, host = '127.0.0.1', handleSigna
|
|
|
150
175
|
// runs need one each, or the harnesses would edit the same files.
|
|
151
176
|
const comparing = runners.length > 1;
|
|
152
177
|
const wantTree = body.worktree ?? (comparing || (mode !== 'plan' && d.worktree !== 'off'));
|
|
153
|
-
|
|
178
|
+
// groupId: one launch sent to several machines (or here and there) shows as one task.
|
|
179
|
+
const groupId = /^[a-f0-9]{12}$/.test(body.groupId || '') ? body.groupId : comparing ? newId() : null;
|
|
154
180
|
const attachments = resolveAttachments(body.attachments);
|
|
155
181
|
const preset = body.preset && store.config.teams.find((t) => t.id === body.preset);
|
|
156
182
|
if (body.preset && !preset && !body.subagents) throw bad(`no such subagent preset: ${body.preset}`);
|
|
@@ -177,6 +203,27 @@ export async function startServer({ port = 4477, host = '127.0.0.1', handleSigna
|
|
|
177
203
|
return runners.map((r, i) => runner.create({ id: ids[i], prompt: body.prompt, cwd, ...r, mode, attachments, worktree: trees[i], groupId, origin, subagents }));
|
|
178
204
|
}
|
|
179
205
|
|
|
206
|
+
// Where a launch runs: body.machine, else a project folder that only exists on a machine
|
|
207
|
+
// (recipes and schedules keep just the path). null = here.
|
|
208
|
+
function machineFor(body) {
|
|
209
|
+
if (body.machine) return machines.get(String(body.machine)).id;
|
|
210
|
+
const cwd = String(body.cwd || '');
|
|
211
|
+
if (!cwd || fs.existsSync(expand(cwd))) return null;
|
|
212
|
+
return store.config.projects.find((p) => p.machine && (cwd === p.path || cwd.startsWith(`${p.path}/`)))?.machine || null;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// launch(), here or on a machine. Resolves with summaries (a compare launch has several).
|
|
216
|
+
async function launchAny(body, origin = null) {
|
|
217
|
+
const mid = machineFor(body);
|
|
218
|
+
if (!mid) return (await launch(body, origin)).map((x) => summarize(x, store));
|
|
219
|
+
const out = await machines.launch(mid, { ...body, attachments: await pushUploads(mid, body.attachments), ...(origin && { origin }) });
|
|
220
|
+
const known = store.projectFor(out.cwd, mid);
|
|
221
|
+
(known || store.addProject(out.cwd, { machine: mid })).lastUsedAt = Date.now();
|
|
222
|
+
store.saveConfig();
|
|
223
|
+
if (!known) broadcast('config', {});
|
|
224
|
+
return (out.group || [out]).map((x) => machines.summary(mid, x));
|
|
225
|
+
}
|
|
226
|
+
|
|
180
227
|
// A follow-up from anywhere (UI, CLI, phone). An archived session may have lost its worktree.
|
|
181
228
|
async function replyTo(s, text, attachments = [], queue) {
|
|
182
229
|
await ensureWorktree(s.worktree);
|
|
@@ -194,29 +241,31 @@ export async function startServer({ port = 4477, host = '127.0.0.1', handleSigna
|
|
|
194
241
|
};
|
|
195
242
|
|
|
196
243
|
// Recipes and triggers.
|
|
197
|
-
const runRecipe = async (r, { params = {}, words, cwd, agent, model, effort, mode } = {}) => {
|
|
244
|
+
const runRecipe = async (r, { params = {}, words, cwd, machine, agent, model, effort, mode } = {}) => {
|
|
198
245
|
const values = words ? { ...positional(r.prompt, words), ...params } : params;
|
|
199
|
-
const [s] = await
|
|
246
|
+
const [s] = await launchAny({ prompt: fill(r.prompt, values), cwd: r.cwd || cwd, machine, agent: agent || r.agent || undefined, model: model ?? (r.model || undefined), effort: effort || r.effort || undefined, mode: mode || r.mode || undefined }, { kind: 'recipe', id: r.id, name: r.name });
|
|
200
247
|
r.runs = (r.runs || 0) + 1;
|
|
201
248
|
r.lastRunAt = Date.now();
|
|
202
249
|
store.saveConfig();
|
|
203
250
|
broadcast('config', {});
|
|
204
251
|
return s;
|
|
205
252
|
};
|
|
206
|
-
const triggers = new Triggers({ store, launch: async (body, origin) => (await
|
|
253
|
+
const triggers = new Triggers({ store, launch: async (body, origin) => (await launchAny(body, origin))[0], onChange: () => broadcast('config', {}) });
|
|
207
254
|
|
|
208
255
|
// Phone: answers and follow-ups coming back from ntfy / Telegram.
|
|
209
256
|
const remote = new Remote({
|
|
210
257
|
store,
|
|
211
258
|
onChange: () => broadcast('remote', {}),
|
|
212
259
|
act: async ({ sessionId, kind, ref, index, text }) => {
|
|
213
|
-
const
|
|
214
|
-
if (
|
|
260
|
+
const ops = await sessionOps(sessionId).catch((err) => ({ error: err.message }));
|
|
261
|
+
if (ops?.error) return `Can’t reach it: ${ops.error}`;
|
|
262
|
+
if (!ops) return 'That session is gone.';
|
|
263
|
+
const { s } = ops;
|
|
215
264
|
if (kind === 'answer') {
|
|
216
265
|
if (!s.ask || s.ask.id !== ref) return 'That question was already answered.';
|
|
217
266
|
const answer = s.ask.options[index];
|
|
218
267
|
if (answer === undefined) return 'Unknown option.';
|
|
219
|
-
|
|
268
|
+
await ops.answer(answer);
|
|
220
269
|
return `✓ Answered: ${answer}`;
|
|
221
270
|
}
|
|
222
271
|
if (kind === 'reply') {
|
|
@@ -226,19 +275,35 @@ export async function startServer({ port = 4477, host = '127.0.0.1', handleSigna
|
|
|
226
275
|
const o = q?.options?.[index];
|
|
227
276
|
const msg = typeof o === 'string' ? o : o?.reply || o?.label;
|
|
228
277
|
if (!msg) return 'Unknown option.';
|
|
229
|
-
await
|
|
278
|
+
await ops.reply(msg);
|
|
230
279
|
return `✓ Sent: ${typeof o === 'string' ? o : o.label || msg}`;
|
|
231
280
|
}
|
|
232
281
|
if (kind === 'done') {
|
|
233
282
|
if (s.turns.at(-1)?.id !== ref && ref) return 'There is newer work on that session; open wisp to review it.';
|
|
234
|
-
return
|
|
283
|
+
return (await ops.done()) ? '✓ Marked done' : 'It is still working.';
|
|
235
284
|
}
|
|
236
285
|
if (s.agent === 'wisp') return 'That is the tour; it can’t take follow-ups.';
|
|
237
|
-
const r = await
|
|
286
|
+
const r = await ops.reply(String(text || '').trim());
|
|
238
287
|
return r.answered ? '✓ Answered' : r.interrupted ? '✓ Sent; the agent picked it up mid-run' : r.queued ? '✓ Queued for when it finishes' : '✓ Sent';
|
|
239
288
|
},
|
|
240
289
|
});
|
|
241
290
|
|
|
291
|
+
// What the phone can do to a session, here or on a machine: { s, answer, reply, done } or null.
|
|
292
|
+
async function sessionOps(sessionId) {
|
|
293
|
+
const at = splitId(sessionId);
|
|
294
|
+
if (!at) {
|
|
295
|
+
const s = store.get(sessionId);
|
|
296
|
+
return s && { s, answer: (a) => runner.answer(s, a), reply: (t) => replyTo(s, t), done: () => markDone(s) };
|
|
297
|
+
}
|
|
298
|
+
const call = (method, p, body) => machines.json(at.machine, method, `/sessions/${at.id}${p}`, body);
|
|
299
|
+
const s = await call('GET', '').catch((err) => {
|
|
300
|
+
if (err.status === 404) return null;
|
|
301
|
+
throw err;
|
|
302
|
+
});
|
|
303
|
+
const live = s && (s.state === 'running' || s.state === 'waiting');
|
|
304
|
+
return s && { s: machines.session(at.machine, s), answer: (a) => call('POST', '/answer', { text: a }), reply: (t) => call('POST', '/reply', { text: t }), done: async () => !live && !!(await call('PATCH', '', { state: 'done' })) };
|
|
305
|
+
}
|
|
306
|
+
|
|
242
307
|
// Ship it: follow open PRs (checks, reviews, merges) so their cards stay current.
|
|
243
308
|
const refreshPr = async (s) => {
|
|
244
309
|
if (!s.pr || !s.worktree) return s.pr;
|
|
@@ -391,6 +456,10 @@ export async function startServer({ port = 4477, host = '127.0.0.1', handleSigna
|
|
|
391
456
|
fs.writeFileSync(path.join(uploadsDir, `${uid}.json`), JSON.stringify(meta));
|
|
392
457
|
return send(res, 201, meta);
|
|
393
458
|
}
|
|
459
|
+
if (id && m === 'GET' && splitId(id)) {
|
|
460
|
+
const at = splitId(id);
|
|
461
|
+
return pipeFrom(res, await machines.stream(at.machine, 'GET', `/uploads/${at.id}`));
|
|
462
|
+
}
|
|
394
463
|
if (id && m === 'GET') {
|
|
395
464
|
const u = uploads.get(id);
|
|
396
465
|
if (!u || !fs.existsSync(u.path)) return send(res, 404, { error: 'not found' });
|
|
@@ -486,8 +555,9 @@ export async function startServer({ port = 4477, host = '127.0.0.1', handleSigna
|
|
|
486
555
|
}
|
|
487
556
|
if (action === 'run' && m === 'POST') {
|
|
488
557
|
const body = await readJson(req);
|
|
489
|
-
|
|
490
|
-
|
|
558
|
+
// machine: run it in a project there (r.cwd empty, or a path on that machine).
|
|
559
|
+
const s = await runRecipe(r, { params: body.params || {}, words: Array.isArray(body.words) ? body.words.map(String) : undefined, cwd: body.cwd, machine: body.machine || undefined, agent: body.agent, model: body.model, effort: body.effort, mode: body.mode });
|
|
560
|
+
return send(res, 201, s);
|
|
491
561
|
}
|
|
492
562
|
}
|
|
493
563
|
|
|
@@ -518,7 +588,7 @@ export async function startServer({ port = 4477, host = '127.0.0.1', handleSigna
|
|
|
518
588
|
}
|
|
519
589
|
if (action === 'run' && m === 'POST') {
|
|
520
590
|
const r = await triggers.runNow(t);
|
|
521
|
-
const started =
|
|
591
|
+
const started = Array.isArray(r) ? r : r ? [r] : [];
|
|
522
592
|
return send(res, 200, { started, trigger: triggerView(t) });
|
|
523
593
|
}
|
|
524
594
|
}
|
|
@@ -543,6 +613,11 @@ export async function startServer({ port = 4477, host = '127.0.0.1', handleSigna
|
|
|
543
613
|
if (!id && m === 'GET') return send(res, 200, store.config.projects);
|
|
544
614
|
if (!id && m === 'POST') {
|
|
545
615
|
const body = await readJson(req);
|
|
616
|
+
if (body.machine) {
|
|
617
|
+
const p = await machines.addProject(String(body.machine), String(body.path || '').trim(), body.name);
|
|
618
|
+
broadcast('config', {});
|
|
619
|
+
return send(res, 201, p);
|
|
620
|
+
}
|
|
546
621
|
const dir = path.resolve(String(body.path || '').replace(/^~(?=$|[\\/])/, os.homedir()));
|
|
547
622
|
if (!fs.existsSync(dir) || !fs.statSync(dir).isDirectory()) return send(res, 400, { error: `Not a folder: ${dir}` });
|
|
548
623
|
const p = store.addProject(dir, { name: body.name });
|
|
@@ -607,13 +682,18 @@ export async function startServer({ port = 4477, host = '127.0.0.1', handleSigna
|
|
|
607
682
|
return send(res, 200, entries);
|
|
608
683
|
}
|
|
609
684
|
|
|
685
|
+
if (root === 'machines') return machinesApi(req, res, url, id, action);
|
|
686
|
+
|
|
610
687
|
if (root === 'sessions') {
|
|
688
|
+
if (id && splitId(id)) return machineSessionApi(req, res, url, splitId(id), action);
|
|
611
689
|
if (!id) {
|
|
612
690
|
if (m === 'GET') {
|
|
613
691
|
const q = Object.fromEntries(url.searchParams);
|
|
614
692
|
const cwd = q.cwd && path.resolve(q.cwd.replace(/^~(?=$|[\\/])/, os.homedir()));
|
|
615
693
|
// Subagent sessions belong to their lead; ?children=1 lists them too.
|
|
616
|
-
|
|
694
|
+
// Sessions on machines too (the last ones seen, for a machine that's offline).
|
|
695
|
+
const here = store.all().filter((x) => !x.parentId || q.children).map((x) => summarize(x, store));
|
|
696
|
+
let list = [...here, ...(await machines.sessions(q))].sort((a, b) => b.updatedAt - a.updatedAt);
|
|
617
697
|
if (q.state) list = list.filter((x) => q.state.split(',').includes(x.state));
|
|
618
698
|
if (q.agent) list = list.filter((x) => x.agent === q.agent);
|
|
619
699
|
if (q.project) list = list.filter((x) => x.projectId === q.project || x.project === q.project);
|
|
@@ -623,9 +703,12 @@ export async function startServer({ port = 4477, host = '127.0.0.1', handleSigna
|
|
|
623
703
|
return send(res, 200, list);
|
|
624
704
|
}
|
|
625
705
|
if (m === 'POST') {
|
|
626
|
-
const
|
|
627
|
-
|
|
628
|
-
|
|
706
|
+
const body = await readJson(req);
|
|
707
|
+
// A machine's wisp hands on where a recipe or trigger launch came from.
|
|
708
|
+
const o = body.origin;
|
|
709
|
+
const origin = o && (o.kind === 'recipe' || o.kind === 'trigger') ? { kind: o.kind, id: String(o.id || ''), name: String(o.name || '') } : null;
|
|
710
|
+
const out = await launchAny(body, origin);
|
|
711
|
+
return send(res, 201, out.length > 1 ? { ...out[0], group: out } : out[0]);
|
|
629
712
|
}
|
|
630
713
|
}
|
|
631
714
|
|
|
@@ -868,6 +951,64 @@ export async function startServer({ port = 4477, host = '127.0.0.1', handleSigna
|
|
|
868
951
|
send(res, 404, { error: 'not found' });
|
|
869
952
|
}
|
|
870
953
|
|
|
954
|
+
// /api/machines: add, connect, and browse folders on other computers.
|
|
955
|
+
async function machinesApi(req, res, url, id, action) {
|
|
956
|
+
const m = req.method;
|
|
957
|
+
const one = (mid) => machines.view().find((x) => x.id === mid);
|
|
958
|
+
if (!id && m === 'GET') return send(res, 200, machines.view());
|
|
959
|
+
if (!id && m === 'POST') {
|
|
960
|
+
const body = await readJson(req);
|
|
961
|
+
const x = machines.add(body);
|
|
962
|
+
if (body.connect !== false) await machines.connect(x.id);
|
|
963
|
+
return send(res, 201, one(x.id));
|
|
964
|
+
}
|
|
965
|
+
if (id === 'ssh-hosts' && m === 'GET') return send(res, 200, sshHosts());
|
|
966
|
+
machines.get(id); // 404s
|
|
967
|
+
if (!action && m === 'PATCH') return send(res, 200, (await machines.update(id, await readJson(req)), one(id)));
|
|
968
|
+
if (!action && m === 'DELETE') {
|
|
969
|
+
machines.remove(id);
|
|
970
|
+
broadcast('config', {});
|
|
971
|
+
return send(res, 200, { ok: true });
|
|
972
|
+
}
|
|
973
|
+
if (action === 'connect' && m === 'POST') return send(res, 200, await machines.connect(id));
|
|
974
|
+
if (action === 'disconnect' && m === 'POST') return send(res, 200, (machines.disconnect(id), one(id)));
|
|
975
|
+
// Stop wisp over there (its agents too); restart starts this version in its place.
|
|
976
|
+
if (action === 'stop' && m === 'POST') return send(res, 200, (await machines.stopRemote(id, { restart: !!(await readJson(req)).restart }), one(id)));
|
|
977
|
+
if (action === 'fs' && m === 'GET') return send(res, 200, await machines.json(id, 'GET', `/fs?q=${encodeURIComponent(url.searchParams.get('q') || '')}`));
|
|
978
|
+
if (action === 'discover' && m === 'GET') {
|
|
979
|
+
const known = new Set(store.config.projects.filter((p) => p.machine === id).map((p) => p.path));
|
|
980
|
+
return send(res, 200, (await machines.json(id, 'GET', '/projects/discover')).filter((f) => !known.has(f.path)));
|
|
981
|
+
}
|
|
982
|
+
send(res, 404, { error: 'not found' });
|
|
983
|
+
}
|
|
984
|
+
|
|
985
|
+
// /api/sessions/<id>@<machine>/…: passed through to that machine's wisp, ids rewritten.
|
|
986
|
+
async function machineSessionApi(req, res, url, at, action) {
|
|
987
|
+
const m = req.method;
|
|
988
|
+
const mid = at.machine;
|
|
989
|
+
const sub = `/sessions/${at.id}${action ? `/${action}` : ''}${url.search}`;
|
|
990
|
+
if ((action === 'file' && m === 'GET') || (action === 'presentation' && url.searchParams.get('format') === 'markdown')) return pipeFrom(res, await machines.stream(mid, 'GET', sub));
|
|
991
|
+
// Take over: a terminal here, signed in over there, resuming the agent in its folder.
|
|
992
|
+
if (action === 'takeover') {
|
|
993
|
+
const { command } = await machines.json(mid, 'GET', `/sessions/${at.id}/takeover`);
|
|
994
|
+
const argv = machines.terminalArgv(mid, command);
|
|
995
|
+
if (m === 'POST') {
|
|
996
|
+
const s = await machines.json(mid, 'GET', `/sessions/${at.id}`);
|
|
997
|
+
if (s.state === 'running' || s.state === 'waiting') await machines.json(mid, 'POST', `/sessions/${at.id}/stop`, {});
|
|
998
|
+
openInTerminal({ cwd: os.homedir(), argv, terminal: store.config.defaults.terminal || 'auto', custom: store.config.defaults.terminalCommand });
|
|
999
|
+
telemetry.track('takeover', { agent: s.agent, terminal: store.config.defaults.terminal || 'auto', machine: true });
|
|
1000
|
+
}
|
|
1001
|
+
return send(res, 200, { command: argv.map(shq).join(' ') });
|
|
1002
|
+
}
|
|
1003
|
+
const body = m === 'GET' ? undefined : await readJson(req);
|
|
1004
|
+
if (action === 'reply' && body?.attachments?.length) body.attachments = await pushUploads(mid, body.attachments);
|
|
1005
|
+
const out = await machines.json(mid, m, sub, body, action === 'wait' ? { timeout: (MAX_WAIT_S + 30) * 1000 } : undefined);
|
|
1006
|
+
if (!action && m === 'GET') return send(res, 200, machines.session(mid, out));
|
|
1007
|
+
if ((!action && m === 'PATCH') || action === 'wait') return send(res, 200, machines.summary(mid, out));
|
|
1008
|
+
if (out && typeof out === 'object' && out.sessionId) out.sessionId = qualify(out.sessionId, mid);
|
|
1009
|
+
return send(res, 200, out);
|
|
1010
|
+
}
|
|
1011
|
+
|
|
871
1012
|
function serveStatic(res, pathname) {
|
|
872
1013
|
const p = path.normalize(path.join(WEB, pathname === '/' ? 'index.html' : pathname));
|
|
873
1014
|
if (!p.startsWith(WEB) || !fs.existsSync(p) || fs.statSync(p).isDirectory()) {
|
|
@@ -888,6 +1029,7 @@ export async function startServer({ port = 4477, host = '127.0.0.1', handleSigna
|
|
|
888
1029
|
retro.stop();
|
|
889
1030
|
triggers.stop();
|
|
890
1031
|
remote.stop();
|
|
1032
|
+
machines.stop(); // the tunnels go; wisp over there keeps running, agents and all
|
|
891
1033
|
clearInterval(prTimer);
|
|
892
1034
|
for (const s of store.all()) runner.stop(s);
|
|
893
1035
|
store.flush();
|
|
@@ -907,6 +1049,7 @@ export async function startServer({ port = 4477, host = '127.0.0.1', handleSigna
|
|
|
907
1049
|
updater.start();
|
|
908
1050
|
triggers.start();
|
|
909
1051
|
remote.start();
|
|
1052
|
+
machines.start();
|
|
910
1053
|
|
|
911
1054
|
if (telemetry.enabled) {
|
|
912
1055
|
if (store.config.defaults.telemetryNotice !== 'seen') console.log('wisp sends anonymous usage stats and error reports (no prompts, paths or code). Opt out: Settings, or WISP_TELEMETRY=0 / DO_NOT_TRACK=1.');
|
|
@@ -915,7 +1058,7 @@ export async function startServer({ port = 4477, host = '127.0.0.1', handleSigna
|
|
|
915
1058
|
.catch(() => {});
|
|
916
1059
|
}
|
|
917
1060
|
|
|
918
|
-
return { url: `http://localhost:${port}`, server, store, runner, shutdown, telemetry, triggers, remote };
|
|
1061
|
+
return { url: `http://localhost:${port}`, server, store, runner, shutdown, telemetry, triggers, remote, machines };
|
|
919
1062
|
}
|
|
920
1063
|
|
|
921
1064
|
// POST { event, session, markdown } to each configured webhook (Slack bridges, CI, notifiers).
|
|
@@ -935,6 +1078,14 @@ function notifyWebhooks(store, summary) {
|
|
|
935
1078
|
}
|
|
936
1079
|
}
|
|
937
1080
|
|
|
1081
|
+
// Pass a machine's raw response (images, markdown, uploads) straight through.
|
|
1082
|
+
function pipeFrom(res, r) {
|
|
1083
|
+
const headers = { 'content-type': r.headers['content-type'] || 'application/octet-stream' };
|
|
1084
|
+
if (r.headers['content-length']) headers['content-length'] = r.headers['content-length'];
|
|
1085
|
+
res.writeHead(r.statusCode, headers);
|
|
1086
|
+
r.pipe(res);
|
|
1087
|
+
}
|
|
1088
|
+
|
|
938
1089
|
function send(res, status, body) {
|
|
939
1090
|
res.writeHead(status, { 'content-type': 'application/json' });
|
|
940
1091
|
res.end(JSON.stringify(body));
|