wisp-agents 0.15.0 → 0.17.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 +55 -1
- package/README.md +29 -13
- package/bin/wisp.js +3 -2
- package/package.json +3 -3
- package/server/adapters/antigravity.js +7 -3
- package/server/adapters/claude.js +79 -55
- package/server/adapters/codex.js +5 -2
- package/server/adapters/common.js +14 -34
- package/server/adapters/cursor.js +4 -2
- package/server/adapters/grok.js +17 -2
- package/server/adapters/index.js +25 -2
- package/server/adapters/opencode.js +78 -12
- package/server/checkpoint.js +12 -68
- package/server/client.js +6 -4
- package/server/git.js +9 -4
- package/server/handoff.js +71 -0
- package/server/http/respond.js +59 -0
- package/server/http/router.js +64 -0
- package/server/http/static.js +36 -0
- package/server/index.js +138 -1216
- package/server/machines/bootstrap.js +56 -0
- package/server/machines/hosts.js +127 -0
- package/server/machines.js +7 -179
- package/server/mcp-ask.js +113 -8
- package/server/mcp-wisp.js +17 -5
- package/server/mentions.js +39 -0
- package/server/openapi.js +36 -4
- package/server/paths.js +32 -0
- package/server/preferences.js +2 -6
- package/server/proc.js +54 -0
- package/server/protocol.js +8 -1
- package/server/recipes.js +2 -4
- package/server/remote.js +14 -14
- package/server/render.js +3 -1
- package/server/router.js +7 -3
- package/server/routes/auto.js +29 -0
- package/server/routes/automations.js +92 -0
- package/server/routes/internal.js +176 -0
- package/server/routes/machines.js +43 -0
- package/server/routes/meta.js +76 -0
- package/server/routes/proxy.js +47 -0
- package/server/routes/sessions.js +351 -0
- package/server/routes/settings.js +144 -0
- package/server/routes/ship.js +61 -0
- package/server/routes/uploads.js +19 -0
- package/server/runner.js +216 -49
- package/server/services/auto.js +67 -0
- package/server/services/feedback.js +53 -0
- package/server/services/install.js +69 -0
- package/server/services/launch.js +127 -0
- package/server/services/phone.js +65 -0
- package/server/services/ship.js +82 -0
- package/server/services/telemetry.js +34 -0
- package/server/services/uploads.js +59 -0
- package/server/services/webhooks.js +28 -0
- package/server/store.js +13 -5
- package/server/subagents.js +5 -4
- package/server/telemetry.js +1 -1
- package/server/terminal.js +2 -1
- package/server/triggers.js +5 -7
- package/server/update.js +4 -3
- package/server/usage.js +94 -0
- package/server/util.js +42 -0
- package/server/welcome.js +3 -3
- package/web/css/auto.css +29 -0
- package/web/css/automations.css +71 -0
- package/web/css/base.css +83 -0
- package/web/css/blocks.css +461 -0
- package/web/css/composer.css +84 -0
- package/web/css/feed.css +235 -0
- package/web/css/launcher.css +204 -0
- package/web/css/layout.css +61 -0
- package/web/css/machines.css +125 -0
- package/web/css/modals.css +176 -0
- package/web/css/palette.css +79 -0
- package/web/css/responsive.css +35 -0
- package/web/css/ship.css +95 -0
- package/web/css/subagents.css +130 -0
- package/web/css/tokens.css +115 -0
- package/web/index.html +17 -1
- package/web/js/app.js +28 -5237
- package/web/js/blocks.js +46 -0
- package/web/js/core/api.js +25 -0
- package/web/js/core/bus.js +28 -0
- package/web/js/core/dom.js +7 -0
- package/web/js/core/format.js +27 -0
- package/web/js/core/state.js +43 -0
- package/web/js/features/actions.js +344 -0
- package/web/js/features/attachments.js +115 -0
- package/web/js/features/auto.js +54 -0
- package/web/js/features/automations.js +305 -0
- package/web/js/features/commands.js +123 -0
- package/web/js/features/compare.js +105 -0
- package/web/js/features/composer.js +315 -0
- package/web/js/features/feed.js +400 -0
- package/web/js/features/feedback.js +127 -0
- package/web/js/features/keys.js +83 -0
- package/web/js/features/launcher.js +561 -0
- package/web/js/features/live.js +91 -0
- package/web/js/features/machines.js +313 -0
- package/web/js/features/memory.js +154 -0
- package/web/js/features/mentions.js +125 -0
- package/web/js/features/notify.js +36 -0
- package/web/js/features/phone.js +124 -0
- package/web/js/features/places.js +38 -0
- package/web/js/features/projects.js +284 -0
- package/web/js/features/runner-picker.js +233 -0
- package/web/js/features/runners.js +163 -0
- package/web/js/features/scroll.js +75 -0
- package/web/js/features/settings.js +390 -0
- package/web/js/features/setup.js +94 -0
- package/web/js/features/ship.js +122 -0
- package/web/js/features/team.js +386 -0
- package/web/js/features/turns.js +432 -0
- package/web/js/features/update.js +48 -0
- package/web/js/features/usage.js +75 -0
- package/web/js/mention-text.js +59 -0
- package/web/js/ui/kbd.js +46 -0
- package/web/js/ui/overlay.js +38 -0
- package/web/js/ui/popover.js +52 -0
- package/web/js/ui/toast.js +23 -0
- package/web/css/app.css +0 -1910
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
// Bridge for the MCP servers running inside an agent process (ask_user, approve, delegate, wisp_stats,
|
|
2
|
+
// wisp_sessions, wisp_session). Every call
|
|
3
|
+
// carries the per-run token wisp handed the agent; nothing else may use these.
|
|
4
|
+
|
|
5
|
+
import { adapters } from '../adapters/index.js';
|
|
6
|
+
import { plainMentions } from '../mentions.js';
|
|
7
|
+
import { blocksToMarkdown, sessionToMarkdown } from '../render.js';
|
|
8
|
+
import { summarize } from '../runner.js';
|
|
9
|
+
import { delegationReport } from '../subagents.js';
|
|
10
|
+
import { toolStats, toolStatsText } from '../usage.js';
|
|
11
|
+
import { clip } from '../util.js';
|
|
12
|
+
|
|
13
|
+
export function register(r, app) {
|
|
14
|
+
const { store, runner, telemetry } = app;
|
|
15
|
+
// The token check, then the handler; a wrong token is a 403 whatever the route.
|
|
16
|
+
const guarded = (fn) => (c) => (c.req.headers['x-wisp-token'] !== app.token ? c.send(403, { error: 'bad token' }) : fn(c));
|
|
17
|
+
const opts = { doc: false };
|
|
18
|
+
const lead = (c) => store.get(c.params.id);
|
|
19
|
+
|
|
20
|
+
r.route(
|
|
21
|
+
'POST',
|
|
22
|
+
'/internal/ask',
|
|
23
|
+
guarded(async (c) => {
|
|
24
|
+
const body = await c.body();
|
|
25
|
+
const s = store.get(body.session);
|
|
26
|
+
if (!s) return c.send(404, { error: 'no such session' });
|
|
27
|
+
telemetry.track('ask_user', { agent: s.agent });
|
|
28
|
+
c.send(200, { id: runner.ask(s, body) });
|
|
29
|
+
}),
|
|
30
|
+
opts,
|
|
31
|
+
);
|
|
32
|
+
r.route('GET', '/internal/ask/:id', guarded((c) => c.send(200, runner.pollAnswer(c.params.id))), opts);
|
|
33
|
+
|
|
34
|
+
// A live approval (the harness's permission prompt): { decision } at once, or an { id } to poll
|
|
35
|
+
// like ask_user, whose answer carries the decision.
|
|
36
|
+
r.route(
|
|
37
|
+
'POST',
|
|
38
|
+
'/internal/approve',
|
|
39
|
+
guarded(async (c) => {
|
|
40
|
+
const body = await c.body();
|
|
41
|
+
const s = store.get(body.session);
|
|
42
|
+
if (!s) return c.send(404, { error: 'no such session' });
|
|
43
|
+
const out = runner.requestApproval(s, body);
|
|
44
|
+
if (out.id) telemetry.track('approval', { agent: s.agent });
|
|
45
|
+
c.send(200, out);
|
|
46
|
+
}),
|
|
47
|
+
opts,
|
|
48
|
+
);
|
|
49
|
+
|
|
50
|
+
// delegate: POST starts tasks on the lead's subagents; GET reports on them (done once all finished).
|
|
51
|
+
r.route(
|
|
52
|
+
'POST',
|
|
53
|
+
'/internal/delegate/:id',
|
|
54
|
+
guarded(async (c) => {
|
|
55
|
+
const s = lead(c);
|
|
56
|
+
if (!s) return c.send(404, { error: 'no such session' });
|
|
57
|
+
const body = await c.body();
|
|
58
|
+
const delegations = runner.delegate(s, body.tasks);
|
|
59
|
+
telemetry.track('delegate', { agent: s.agent, tasks: delegations.length, harnesses: [...new Set(delegations.filter((d) => d.agent).map((d) => d.agent))] });
|
|
60
|
+
c.send(200, { delegations });
|
|
61
|
+
}),
|
|
62
|
+
opts,
|
|
63
|
+
);
|
|
64
|
+
r.route(
|
|
65
|
+
'GET',
|
|
66
|
+
'/internal/delegate/:id',
|
|
67
|
+
guarded((c) => {
|
|
68
|
+
const s = lead(c);
|
|
69
|
+
if (!s) return c.send(404, { error: 'no such session' });
|
|
70
|
+
const ids = (c.query.get('ids') || '').split(',').filter(Boolean);
|
|
71
|
+
const list = runner.delegations(s, ids);
|
|
72
|
+
if (!list.length) return c.send(200, { done: true, text: 'No delegated tasks to report on.' });
|
|
73
|
+
const running = ({ d, child }) => child?.turns.find((t) => t.id === d.turnId)?.status === 'running';
|
|
74
|
+
c.send(200, { done: !list.some(running), text: list.map(({ d, child }) => delegationReport(d, child, adapters)).join('\n\n---\n\n') });
|
|
75
|
+
}),
|
|
76
|
+
opts,
|
|
77
|
+
);
|
|
78
|
+
|
|
79
|
+
// wisp_stats: read-only tool counts, spend and success across sessions. project=current is the
|
|
80
|
+
// asking session's project.
|
|
81
|
+
r.route(
|
|
82
|
+
'GET',
|
|
83
|
+
'/internal/stats/:id',
|
|
84
|
+
guarded((c) => {
|
|
85
|
+
const s = lead(c);
|
|
86
|
+
if (!s) return c.send(404, { error: 'no such session' });
|
|
87
|
+
const projectFor = (x) => store.projectFor(x.cwd);
|
|
88
|
+
let project = c.query.get('project') || '';
|
|
89
|
+
if (project === 'current') project = projectFor(s)?.id || String(s.cwd || '').split(/[\\/]/).pop();
|
|
90
|
+
const stats = toolStats(store.all(), { days: Number(c.query.get('days')) || 30, project: projectFor, filter: { agent: c.query.get('agent') || '', project } });
|
|
91
|
+
telemetry.track('wisp_stats', { agent: s.agent });
|
|
92
|
+
c.send(200, { stats, text: toolStatsText(stats) });
|
|
93
|
+
}),
|
|
94
|
+
opts,
|
|
95
|
+
);
|
|
96
|
+
// wisp_sessions: the user's other tasks (not the asker, its subagents or the tour), newest first.
|
|
97
|
+
r.route(
|
|
98
|
+
'GET',
|
|
99
|
+
'/internal/sessions/:id',
|
|
100
|
+
guarded((c) => {
|
|
101
|
+
const s = lead(c);
|
|
102
|
+
if (!s) return c.send(404, { error: 'no such session' });
|
|
103
|
+
const q = Object.fromEntries(c.query);
|
|
104
|
+
let project = q.project || '';
|
|
105
|
+
if (project === 'current') project = store.projectFor(s.cwd)?.id || String(s.cwd || '').split(/[\\/]/).pop();
|
|
106
|
+
const states = q.state ? q.state.split(',').map((x) => x.trim()) : null;
|
|
107
|
+
const words = String(q.query || '').toLowerCase().split(/\s+/).filter(Boolean);
|
|
108
|
+
const all = store
|
|
109
|
+
.all()
|
|
110
|
+
.filter((x) => x.id !== s.id && !x.parentId && x.agent !== 'wisp' && x.id !== s.parentId)
|
|
111
|
+
.map((x) => summarize(x, store))
|
|
112
|
+
.filter((x) => (q.archived === 'true' || !x.archived) && (!states || states.includes(x.state)))
|
|
113
|
+
.filter((x) => !project || x.projectId === project || x.project === project)
|
|
114
|
+
.filter((x) => words.every((w) => `${x.title} ${x.preview}`.toLowerCase().includes(w)))
|
|
115
|
+
.sort((a, b) => b.updatedAt - a.updatedAt);
|
|
116
|
+
const list = all.slice(0, Math.min(Math.max(Number(q.limit) || 20, 1), 100));
|
|
117
|
+
telemetry.track('wisp_sessions', { agent: s.agent });
|
|
118
|
+
const text = list.length
|
|
119
|
+
? `${all.length} task${all.length === 1 ? '' : 's'}${all.length > list.length ? `, the newest ${list.length}` : ''}. Read one with wisp_session.\n\n${list.map(sessionLine).join('\n')}`
|
|
120
|
+
: 'No tasks match.';
|
|
121
|
+
c.send(200, { text });
|
|
122
|
+
}),
|
|
123
|
+
opts,
|
|
124
|
+
);
|
|
125
|
+
|
|
126
|
+
// wisp_session: one task's requests and a turn's report (?of=<id>&turn=<1-based>).
|
|
127
|
+
r.route(
|
|
128
|
+
'GET',
|
|
129
|
+
'/internal/session/:id',
|
|
130
|
+
guarded((c) => {
|
|
131
|
+
const s = lead(c);
|
|
132
|
+
if (!s) return c.send(404, { error: 'no such session' });
|
|
133
|
+
const id = String(c.query.get('of') || '').trim().replace(/^wisp:/, '');
|
|
134
|
+
const x = store.get(id);
|
|
135
|
+
telemetry.track('wisp_session', { agent: s.agent });
|
|
136
|
+
if (!x) return c.send(200, { text: `No wisp task \`${id}\` on this computer. It may have been deleted, or it ran on another machine. List tasks with wisp_sessions.` });
|
|
137
|
+
c.send(200, { text: sessionText(x, store, c.query.get('turn')) });
|
|
138
|
+
}),
|
|
139
|
+
opts,
|
|
140
|
+
);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
const age = (t) => {
|
|
144
|
+
const m = Math.round((Date.now() - t) / 60_000);
|
|
145
|
+
return m < 1 ? 'just now' : m < 60 ? `${m}m ago` : m < 48 * 60 ? `${Math.round(m / 60)}h ago` : `${Math.round(m / 1440)}d ago`;
|
|
146
|
+
};
|
|
147
|
+
|
|
148
|
+
const sessionLine = (x) =>
|
|
149
|
+
`- \`${x.id}\` **${x.title}** — ${x.archived ? 'archived, ' : ''}${x.state}, ${x.agent}${x.model ? ` (${x.model})` : ''}, ${x.project}, ${age(x.updatedAt)}${x.state === 'running' && x.live ? `: ${x.live}` : x.preview ? `: ${clip(x.preview, 160)}` : ''}`;
|
|
150
|
+
|
|
151
|
+
export function sessionText(x, store, turnArg) {
|
|
152
|
+
const sum = summarize(x, store);
|
|
153
|
+
const n = Number(turnArg);
|
|
154
|
+
const turn = turnArg === null || turnArg === undefined || turnArg === '' || !n ? x.turns.at(-1) : x.turns[n - 1];
|
|
155
|
+
const head = [
|
|
156
|
+
`# ${x.title}`,
|
|
157
|
+
`Task \`${x.id}\` · ${sum.archived ? 'archived · ' : ''}${x.state} · ${x.agent}${x.model ? ` (${x.model})` : ''} · project ${sum.project} · updated ${age(x.updatedAt)}`,
|
|
158
|
+
`Folder: \`${x.worktree?.path || x.cwd}\`${x.worktree ? ` (worktree, branch \`${x.worktree.branch}\`)` : ''}${x.pr ? ` · PR #${x.pr.number} (${String(x.pr.state).toLowerCase()}): ${x.pr.url}` : ''}`,
|
|
159
|
+
];
|
|
160
|
+
const asks = x.turns.map((t, i) => `${i + 1}. ${clip(plainMentions(t.prompt).replace(/\s+/g, ' '), 300)}${t.status === 'running' ? ' _(running)_' : t.status === 'error' ? ' _(failed)_' : t.status === 'stopped' ? ' _(stopped)_' : ''}`);
|
|
161
|
+
if (!turn) return [...head, '', `Turn ${turnArg} doesn't exist; this task has ${x.turns.length}.`, '', '## Requests', ...asks].join('\n');
|
|
162
|
+
const i = x.turns.indexOf(turn);
|
|
163
|
+
const report = turn === x.turns.at(-1) ? sessionToMarkdown(x) : blocksToMarkdown(turn.presentation?.blocks, turn);
|
|
164
|
+
const files = (turn.changes || []).filter((ch) => !ch.reverted).map((ch) => `\`${ch.file}\``);
|
|
165
|
+
return [
|
|
166
|
+
...head,
|
|
167
|
+
'',
|
|
168
|
+
`## Requests (${x.turns.length})`,
|
|
169
|
+
...asks,
|
|
170
|
+
'',
|
|
171
|
+
`## Report for turn ${i + 1}${x.turns.length > 1 ? ` of ${x.turns.length} (pass turn to read another)` : ''}`,
|
|
172
|
+
'',
|
|
173
|
+
report || '_No report yet._',
|
|
174
|
+
...(files.length ? ['', `Files edited in this turn: ${files.join(', ')}`] : []),
|
|
175
|
+
].join('\n');
|
|
176
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
// /api/machines: add, connect, and browse folders on other computers.
|
|
2
|
+
|
|
3
|
+
import { sshHosts, discoverHosts } from '../machines.js';
|
|
4
|
+
|
|
5
|
+
export function register(r, app) {
|
|
6
|
+
const { store, machines } = app;
|
|
7
|
+
const one = (mid) => machines.view().find((x) => x.id === mid);
|
|
8
|
+
// A machine route: 404s for an unknown id before doing anything.
|
|
9
|
+
const known = (fn) => (c) => (machines.get(c.params.id), fn(c, c.params.id));
|
|
10
|
+
|
|
11
|
+
r.route('GET', '/machines', (c) => c.send(200, machines.view()));
|
|
12
|
+
r.route('POST', '/machines', async (c) => {
|
|
13
|
+
const body = await c.body();
|
|
14
|
+
const x = machines.add(body);
|
|
15
|
+
if (body.connect !== false) await machines.connect(x.id);
|
|
16
|
+
c.send(201, one(x.id));
|
|
17
|
+
});
|
|
18
|
+
r.route('GET', '/machines/ssh-hosts', (c) => c.send(200, sshHosts()));
|
|
19
|
+
r.route('GET', '/machines/discover', async (c) => c.send(200, await discoverHosts()));
|
|
20
|
+
r.route('PATCH', '/machines/:id', known(async (c, id) => c.send(200, (await machines.update(id, await c.body()), one(id)))));
|
|
21
|
+
r.route(
|
|
22
|
+
'DELETE',
|
|
23
|
+
'/machines/:id',
|
|
24
|
+
known((c, id) => {
|
|
25
|
+
machines.remove(id);
|
|
26
|
+
app.broadcast('config', {});
|
|
27
|
+
c.send(200, { ok: true });
|
|
28
|
+
}),
|
|
29
|
+
);
|
|
30
|
+
r.route('POST', '/machines/:id/connect', known(async (c, id) => c.send(200, await machines.connect(id))));
|
|
31
|
+
r.route('POST', '/machines/:id/disconnect', known((c, id) => c.send(200, (machines.disconnect(id), one(id)))));
|
|
32
|
+
// Stop wisp over there (its agents too); restart starts this version in its place.
|
|
33
|
+
r.route('POST', '/machines/:id/stop', known(async (c, id) => c.send(200, (await machines.stopRemote(id, { restart: !!(await c.body()).restart }), one(id)))));
|
|
34
|
+
r.route('GET', '/machines/:id/fs', known(async (c, id) => c.send(200, await machines.json(id, 'GET', `/fs?q=${encodeURIComponent(c.query.get('q') || '')}`))));
|
|
35
|
+
r.route(
|
|
36
|
+
'GET',
|
|
37
|
+
'/machines/:id/discover',
|
|
38
|
+
known(async (c, id) => {
|
|
39
|
+
const have = new Set(store.config.projects.filter((p) => p.machine === id).map((p) => p.path));
|
|
40
|
+
c.send(200, (await machines.json(id, 'GET', '/projects/discover')).filter((f) => !have.has(f.path)));
|
|
41
|
+
}),
|
|
42
|
+
);
|
|
43
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
// The server itself: live events, health, self-update, the API description, installed
|
|
2
|
+
// harnesses, folder suggestions, usage and feedback.
|
|
3
|
+
|
|
4
|
+
import fs from 'node:fs';
|
|
5
|
+
import path from 'node:path';
|
|
6
|
+
import { openapi } from '../openapi.js';
|
|
7
|
+
import { usage } from '../usage.js';
|
|
8
|
+
import { expandHome, VERSION } from '../util.js';
|
|
9
|
+
|
|
10
|
+
export const API_VERSION = 1;
|
|
11
|
+
|
|
12
|
+
export function register(r, app) {
|
|
13
|
+
const { store, runner, updater, telemetry, machines, port } = app;
|
|
14
|
+
|
|
15
|
+
r.route('GET', '/events', (c) => app.events.attach(c.req, c.res));
|
|
16
|
+
|
|
17
|
+
r.route('GET', '/health', (c) => c.send(200, { ok: true, name: 'wisp', version: VERSION, apiVersion: API_VERSION, pid: process.pid, url: `http://127.0.0.1:${port}` }));
|
|
18
|
+
|
|
19
|
+
// New wisp-agents on npm? POST installs it and relaunches; running agents are stopped, so the
|
|
20
|
+
// client must pass force once it has confirmed that with the user.
|
|
21
|
+
r.route('GET', '/update', async (c) => {
|
|
22
|
+
if (c.query.has('check')) return c.send(200, await updater.check());
|
|
23
|
+
updater.maybeCheck(); // the UI asks on focus and reconnect: a day-old check gets refreshed
|
|
24
|
+
c.send(200, updater.view());
|
|
25
|
+
});
|
|
26
|
+
r.route('POST', '/update', async (c) => {
|
|
27
|
+
const body = await c.body();
|
|
28
|
+
if (runner.live.size && !body.force) return c.send(409, { error: 'agents are running', running: runner.live.size });
|
|
29
|
+
await updater.install();
|
|
30
|
+
c.send(202, updater.view());
|
|
31
|
+
telemetry.track('update', { from: updater.state.current, to: updater.state.latest, method: updater.method });
|
|
32
|
+
setTimeout(() => (app.shutdown(), process.exit(0)), 300);
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
r.route('GET', '/openapi.json', (c) => c.send(200, openapi({ version: VERSION, port })), { doc: false });
|
|
36
|
+
|
|
37
|
+
r.route('GET', '/agents', async (c) => c.send(200, await app.auto.detect({ fresh: c.query.has('fresh') })));
|
|
38
|
+
// Re-check: detect harnesses and models again, here and on every online machine.
|
|
39
|
+
r.route('POST', '/agents/recheck', async (c) => {
|
|
40
|
+
const [agents] = await Promise.all([app.auto.detect({ fresh: true }), machines.recheckAgents()]);
|
|
41
|
+
c.send(200, agents);
|
|
42
|
+
});
|
|
43
|
+
// No harness yet: install one (opencode) without leaving wisp. Answers once it's ready.
|
|
44
|
+
r.route('POST', '/agents/install', async (c) => {
|
|
45
|
+
const { id } = await c.body();
|
|
46
|
+
c.send(200, await app.installer.run(String(id || '')));
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
// Directory suggestions for the launcher's project picker.
|
|
50
|
+
r.route(
|
|
51
|
+
'GET',
|
|
52
|
+
'/fs',
|
|
53
|
+
(c) => {
|
|
54
|
+
const raw = expandHome(c.query.get('q') || '');
|
|
55
|
+
const trailing = /[\\/]$/.test(raw); // a trailing backslash counts too (Windows)
|
|
56
|
+
const dir = trailing ? raw : path.dirname(raw);
|
|
57
|
+
const prefix = trailing ? '' : path.basename(raw).toLowerCase();
|
|
58
|
+
let entries = [];
|
|
59
|
+
try {
|
|
60
|
+
entries = fs
|
|
61
|
+
.readdirSync(dir, { withFileTypes: true })
|
|
62
|
+
.filter((e) => e.isDirectory() && !e.name.startsWith('.') && e.name.toLowerCase().startsWith(prefix))
|
|
63
|
+
.slice(0, 12)
|
|
64
|
+
.map((e) => path.join(dir, e.name));
|
|
65
|
+
} catch {}
|
|
66
|
+
c.send(200, entries);
|
|
67
|
+
},
|
|
68
|
+
{ doc: false },
|
|
69
|
+
);
|
|
70
|
+
|
|
71
|
+
// Spend and outcomes. ?days=7|30|90
|
|
72
|
+
r.route('GET', '/usage', (c) => c.send(200, usage(store.all(), { days: Number(c.query.get('days')) || 30, project: (s) => store.projectFor(s.cwd) })));
|
|
73
|
+
|
|
74
|
+
// Feedback for the wisp team: thumbs on a turn and/or a note. { rating?, text?, sessionId?, turnId? }
|
|
75
|
+
r.route('POST', '/feedback', async (c) => c.send(200, await app.sendFeedback(await c.body())));
|
|
76
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
// /api/sessions/<id>@<machine>/…: passed through to that machine's wisp, ids rewritten.
|
|
2
|
+
// Registered before the session routes, so a qualified id never reaches them.
|
|
3
|
+
|
|
4
|
+
import os from 'node:os';
|
|
5
|
+
import { pipeFrom } from '../http/respond.js';
|
|
6
|
+
import { splitId, qualify } from '../machines.js';
|
|
7
|
+
import { openInTerminal, shq } from '../terminal.js';
|
|
8
|
+
import { MAX_WAIT_S } from './sessions.js';
|
|
9
|
+
|
|
10
|
+
const METHODS = ['GET', 'POST', 'PUT', 'PATCH', 'DELETE'];
|
|
11
|
+
|
|
12
|
+
export function register(r, app) {
|
|
13
|
+
const { store, machines, telemetry } = app;
|
|
14
|
+
const remote = { when: (p) => !!splitId(p.id), doc: false };
|
|
15
|
+
|
|
16
|
+
async function proxy(c) {
|
|
17
|
+
const at = splitId(c.params.id);
|
|
18
|
+
const { action } = c.params;
|
|
19
|
+
const m = c.req.method;
|
|
20
|
+
const mid = at.machine;
|
|
21
|
+
const sub = `/sessions/${at.id}${action ? `/${action}` : ''}${c.url.search}`;
|
|
22
|
+
if ((action === 'file' && m === 'GET') || (action === 'presentation' && c.query.get('format') === 'markdown')) return pipeFrom(c.res, await machines.stream(mid, 'GET', sub));
|
|
23
|
+
// Take over: a terminal here, signed in over there, resuming the agent in its folder.
|
|
24
|
+
if (action === 'takeover') {
|
|
25
|
+
const { command } = await machines.json(mid, 'GET', `/sessions/${at.id}/takeover`);
|
|
26
|
+
const argv = machines.terminalArgv(mid, command);
|
|
27
|
+
if (m === 'POST') {
|
|
28
|
+
const s = await machines.json(mid, 'GET', `/sessions/${at.id}`);
|
|
29
|
+
if (s.state === 'running' || s.state === 'waiting') await machines.json(mid, 'POST', `/sessions/${at.id}/stop`, {});
|
|
30
|
+
const d = store.config.defaults;
|
|
31
|
+
openInTerminal({ cwd: os.homedir(), argv, terminal: d.terminal || 'auto', custom: d.terminalCommand });
|
|
32
|
+
telemetry.track('takeover', { agent: s.agent, terminal: d.terminal || 'auto', machine: true });
|
|
33
|
+
}
|
|
34
|
+
return c.send(200, { command: argv.map(shq).join(' ') });
|
|
35
|
+
}
|
|
36
|
+
const body = m === 'GET' ? undefined : await c.body();
|
|
37
|
+
if (action === 'reply' && body?.attachments?.length) body.attachments = await app.launcher.pushUploads(mid, body.attachments);
|
|
38
|
+
const out = await machines.json(mid, m, sub, body, action === 'wait' ? { timeout: (MAX_WAIT_S + 30) * 1000 } : undefined);
|
|
39
|
+
if (!action && m === 'GET') return c.send(200, machines.session(mid, out));
|
|
40
|
+
if ((!action && m === 'PATCH') || action === 'wait') return c.send(200, machines.summary(mid, out));
|
|
41
|
+
if (out && typeof out === 'object' && out.sessionId) out.sessionId = qualify(out.sessionId, mid);
|
|
42
|
+
c.send(200, out);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
r.route(METHODS, '/sessions/:id', proxy, remote);
|
|
46
|
+
r.route(METHODS, '/sessions/:id/:action', proxy, remote);
|
|
47
|
+
}
|