@ottocode/server 0.1.265 → 0.1.267
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/package.json +3 -3
- package/src/routes/auth/copilot.ts +699 -0
- package/src/routes/auth/oauth.ts +578 -0
- package/src/routes/auth/onboarding.ts +45 -0
- package/src/routes/auth/providers.ts +189 -0
- package/src/routes/auth/service.ts +167 -0
- package/src/routes/auth/state.ts +23 -0
- package/src/routes/auth/status.ts +203 -0
- package/src/routes/auth/wallet.ts +229 -0
- package/src/routes/auth.ts +12 -2080
- package/src/routes/config/models-service.ts +411 -0
- package/src/routes/config/models.ts +6 -426
- package/src/routes/config/providers-service.ts +237 -0
- package/src/routes/config/providers.ts +10 -242
- package/src/routes/files/handlers.ts +297 -0
- package/src/routes/files/service.ts +313 -0
- package/src/routes/files.ts +12 -608
- package/src/routes/git/commit-service.ts +207 -0
- package/src/routes/git/commit.ts +6 -220
- package/src/routes/git/remote-service.ts +116 -0
- package/src/routes/git/remote.ts +8 -115
- package/src/routes/git/staging-service.ts +111 -0
- package/src/routes/git/staging.ts +10 -205
- package/src/routes/mcp/auth.ts +338 -0
- package/src/routes/mcp/lifecycle.ts +263 -0
- package/src/routes/mcp/servers.ts +212 -0
- package/src/routes/mcp/service.ts +664 -0
- package/src/routes/mcp/state.ts +13 -0
- package/src/routes/mcp.ts +6 -1233
- package/src/routes/ottorouter/billing.ts +593 -0
- package/src/routes/ottorouter/service.ts +92 -0
- package/src/routes/ottorouter/topup.ts +301 -0
- package/src/routes/ottorouter/wallet.ts +370 -0
- package/src/routes/ottorouter.ts +6 -1319
- package/src/routes/research/service.ts +339 -0
- package/src/routes/research.ts +12 -390
- package/src/routes/sessions/crud.ts +563 -0
- package/src/routes/sessions/queue.ts +242 -0
- package/src/routes/sessions/retry.ts +121 -0
- package/src/routes/sessions/service.ts +768 -0
- package/src/routes/sessions/share.ts +434 -0
- package/src/routes/sessions.ts +8 -1977
- package/src/routes/skills/service.ts +221 -0
- package/src/routes/skills/spec.ts +309 -0
- package/src/routes/skills.ts +31 -909
- package/src/routes/terminals/service.ts +326 -0
- package/src/routes/terminals.ts +19 -295
- package/src/routes/tunnel/service.ts +217 -0
- package/src/routes/tunnel.ts +29 -219
- package/src/runtime/agent/registry-prompts.ts +147 -0
- package/src/runtime/agent/registry.ts +6 -124
- package/src/runtime/agent/runner-errors.ts +116 -0
- package/src/runtime/agent/runner-reminders.ts +45 -0
- package/src/runtime/agent/runner-setup-model.ts +75 -0
- package/src/runtime/agent/runner-setup-prompt.ts +185 -0
- package/src/runtime/agent/runner-setup-tools.ts +103 -0
- package/src/runtime/agent/runner-setup-utils.ts +21 -0
- package/src/runtime/agent/runner-setup.ts +54 -288
- package/src/runtime/agent/runner-telemetry.ts +112 -0
- package/src/runtime/agent/runner-text.ts +108 -0
- package/src/runtime/agent/runner-tool-observer.ts +86 -0
- package/src/runtime/agent/runner.ts +79 -378
- package/src/runtime/prompt/builder.ts +5 -1
- package/src/runtime/prompt/capabilities.ts +13 -8
- package/src/runtime/provider/custom.ts +73 -0
- package/src/runtime/provider/index.ts +2 -85
- package/src/runtime/provider/reasoning-builders.ts +280 -0
- package/src/runtime/provider/reasoning.ts +67 -264
- package/src/tools/adapter/events.ts +116 -0
- package/src/tools/adapter/execution.ts +160 -0
- package/src/tools/adapter/pending.ts +37 -0
- package/src/tools/adapter/persistence.ts +166 -0
- package/src/tools/adapter/results.ts +97 -0
- package/src/tools/adapter.ts +124 -451
|
@@ -0,0 +1,326 @@
|
|
|
1
|
+
import type { Context } from 'hono';
|
|
2
|
+
import { streamSSE } from 'hono/streaming';
|
|
3
|
+
import type { TerminalManager } from '@ottocode/sdk';
|
|
4
|
+
import { logger } from '@ottocode/sdk';
|
|
5
|
+
|
|
6
|
+
export function listTerminals(terminalManager: TerminalManager) {
|
|
7
|
+
const terminals = terminalManager.list();
|
|
8
|
+
return {
|
|
9
|
+
terminals: terminals.map((terminal) => terminal.toJSON()),
|
|
10
|
+
count: terminals.length,
|
|
11
|
+
};
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export async function createTerminal(
|
|
15
|
+
c: Context,
|
|
16
|
+
terminalManager: TerminalManager,
|
|
17
|
+
) {
|
|
18
|
+
try {
|
|
19
|
+
const body = await c.req.json();
|
|
20
|
+
const { command, args, purpose, cwd, title } = body;
|
|
21
|
+
|
|
22
|
+
if (!command || !purpose) {
|
|
23
|
+
return c.json({ error: 'command and purpose are required' }, 400);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
let resolvedCommand = command;
|
|
27
|
+
if (command === 'bash' || command === 'sh' || command === 'shell') {
|
|
28
|
+
resolvedCommand =
|
|
29
|
+
process.platform === 'win32'
|
|
30
|
+
? process.env.COMSPEC || 'cmd.exe'
|
|
31
|
+
: process.env.SHELL || '/bin/bash';
|
|
32
|
+
}
|
|
33
|
+
const resolvedCwd = cwd || process.cwd();
|
|
34
|
+
|
|
35
|
+
const terminal = terminalManager.create({
|
|
36
|
+
command: resolvedCommand,
|
|
37
|
+
args: args || [],
|
|
38
|
+
purpose,
|
|
39
|
+
cwd: resolvedCwd,
|
|
40
|
+
createdBy: 'user',
|
|
41
|
+
title,
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
return c.json({
|
|
45
|
+
terminalId: terminal.id,
|
|
46
|
+
pid: terminal.pid,
|
|
47
|
+
purpose: terminal.purpose,
|
|
48
|
+
command: terminal.command,
|
|
49
|
+
});
|
|
50
|
+
} catch (error) {
|
|
51
|
+
logger.error('Error creating terminal', error);
|
|
52
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
53
|
+
return c.json({ error: message }, 500);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function getTerminal(c: Context, terminalManager: TerminalManager) {
|
|
58
|
+
const id = c.req.param('id');
|
|
59
|
+
const terminal = terminalManager.get(id);
|
|
60
|
+
|
|
61
|
+
if (!terminal) {
|
|
62
|
+
return c.json({ error: 'Terminal not found' }, 404);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
return c.json({ terminal: terminal.toJSON() });
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export function createTerminalWebSocketHandler(
|
|
69
|
+
terminalManager: TerminalManager,
|
|
70
|
+
id: string,
|
|
71
|
+
) {
|
|
72
|
+
let onData: ((data: string) => void) | null = null;
|
|
73
|
+
let onExit: ((exitCode: number) => void) | null = null;
|
|
74
|
+
|
|
75
|
+
return {
|
|
76
|
+
onOpen(
|
|
77
|
+
_event: unknown,
|
|
78
|
+
ws: {
|
|
79
|
+
send: (data: string) => void;
|
|
80
|
+
close: (code?: number, reason?: string) => void;
|
|
81
|
+
},
|
|
82
|
+
) {
|
|
83
|
+
const terminal = terminalManager.get(id);
|
|
84
|
+
if (!terminal) {
|
|
85
|
+
ws.close(4004, 'Terminal not found');
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const history = terminal.read();
|
|
90
|
+
for (const chunk of history) {
|
|
91
|
+
ws.send(chunk);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
onData = (data: string) => {
|
|
95
|
+
try {
|
|
96
|
+
ws.send(data);
|
|
97
|
+
} catch {
|
|
98
|
+
// ws may be closed
|
|
99
|
+
}
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
onExit = (exitCode: number) => {
|
|
103
|
+
try {
|
|
104
|
+
ws.send(JSON.stringify({ type: 'exit', exitCode }));
|
|
105
|
+
ws.close(1000, 'Process exited');
|
|
106
|
+
} catch {
|
|
107
|
+
// ws may already be closed
|
|
108
|
+
}
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
terminal.onData(onData);
|
|
112
|
+
terminal.onExit(onExit);
|
|
113
|
+
|
|
114
|
+
if (terminal.status === 'exited') {
|
|
115
|
+
onExit(terminal.exitCode ?? 0);
|
|
116
|
+
}
|
|
117
|
+
},
|
|
118
|
+
onMessage(event: { data: unknown }, _ws: unknown) {
|
|
119
|
+
const terminal = terminalManager.get(id);
|
|
120
|
+
if (!terminal) return;
|
|
121
|
+
|
|
122
|
+
const raw = event.data;
|
|
123
|
+
const message =
|
|
124
|
+
typeof raw === 'string'
|
|
125
|
+
? raw
|
|
126
|
+
: raw instanceof ArrayBuffer
|
|
127
|
+
? new TextDecoder().decode(raw)
|
|
128
|
+
: String(raw);
|
|
129
|
+
|
|
130
|
+
if (message.startsWith('{')) {
|
|
131
|
+
try {
|
|
132
|
+
const msg = JSON.parse(message);
|
|
133
|
+
if (msg.type === 'resize' && msg.cols > 0 && msg.rows > 0) {
|
|
134
|
+
terminal.resize(msg.cols, msg.rows);
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
} catch {
|
|
138
|
+
// not JSON, treat as input
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
terminal.write(message);
|
|
143
|
+
},
|
|
144
|
+
onClose() {
|
|
145
|
+
const terminal = terminalManager.get(id);
|
|
146
|
+
if (terminal) {
|
|
147
|
+
if (onData) terminal.removeDataListener(onData);
|
|
148
|
+
if (onExit) terminal.removeExitListener(onExit);
|
|
149
|
+
}
|
|
150
|
+
onData = null;
|
|
151
|
+
onExit = null;
|
|
152
|
+
},
|
|
153
|
+
onError() {
|
|
154
|
+
const terminal = terminalManager.get(id);
|
|
155
|
+
if (terminal) {
|
|
156
|
+
if (onData) terminal.removeDataListener(onData);
|
|
157
|
+
if (onExit) terminal.removeExitListener(onExit);
|
|
158
|
+
}
|
|
159
|
+
onData = null;
|
|
160
|
+
onExit = null;
|
|
161
|
+
},
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
export async function handleTerminalOutput(
|
|
166
|
+
c: Context,
|
|
167
|
+
terminalManager: TerminalManager,
|
|
168
|
+
) {
|
|
169
|
+
const id = c.req.param('id');
|
|
170
|
+
const terminal = terminalManager.get(id);
|
|
171
|
+
|
|
172
|
+
if (!terminal) {
|
|
173
|
+
return c.json({ error: 'Terminal not found' }, 404);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
const activeTerminal = terminal;
|
|
177
|
+
|
|
178
|
+
return streamSSE(c, async (stream) => {
|
|
179
|
+
const skipHistory = c.req.query('skipHistory') === 'true';
|
|
180
|
+
if (!skipHistory) {
|
|
181
|
+
const history = activeTerminal.read();
|
|
182
|
+
for (const line of history) {
|
|
183
|
+
await stream.write(
|
|
184
|
+
`data: ${JSON.stringify({ type: 'data', line })}\n\n`,
|
|
185
|
+
);
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
const sendEvent = async (payload: Record<string, unknown>) => {
|
|
190
|
+
try {
|
|
191
|
+
await stream.write(`data: ${JSON.stringify(payload)}\n\n`);
|
|
192
|
+
} catch (error) {
|
|
193
|
+
logger.error('SSE error writing event', error, { id });
|
|
194
|
+
}
|
|
195
|
+
};
|
|
196
|
+
|
|
197
|
+
const onData = (line: string) => {
|
|
198
|
+
void sendEvent({ type: 'data', line });
|
|
199
|
+
};
|
|
200
|
+
|
|
201
|
+
let resolveStream: (() => void) | null = null;
|
|
202
|
+
let finished = false;
|
|
203
|
+
|
|
204
|
+
const hb = setInterval(async () => {
|
|
205
|
+
try {
|
|
206
|
+
await stream.write(`: hb ${Date.now()}\n\n`);
|
|
207
|
+
} catch {
|
|
208
|
+
clearInterval(hb);
|
|
209
|
+
}
|
|
210
|
+
}, 15000);
|
|
211
|
+
|
|
212
|
+
function cleanup() {
|
|
213
|
+
activeTerminal.removeDataListener(onData);
|
|
214
|
+
activeTerminal.removeExitListener(onExit);
|
|
215
|
+
c.req.raw.signal.removeEventListener('abort', onAbort);
|
|
216
|
+
clearInterval(hb);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
function finish() {
|
|
220
|
+
if (finished) {
|
|
221
|
+
return;
|
|
222
|
+
}
|
|
223
|
+
finished = true;
|
|
224
|
+
cleanup();
|
|
225
|
+
resolveStream?.();
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
async function onExit(exitCode: number) {
|
|
229
|
+
try {
|
|
230
|
+
await sendEvent({ type: 'exit', exitCode });
|
|
231
|
+
} finally {
|
|
232
|
+
stream.close();
|
|
233
|
+
finish();
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
function onAbort() {
|
|
238
|
+
stream.close();
|
|
239
|
+
finish();
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
terminal.onData(onData);
|
|
243
|
+
terminal.onExit(onExit);
|
|
244
|
+
|
|
245
|
+
c.req.raw.signal.addEventListener('abort', onAbort, { once: true });
|
|
246
|
+
|
|
247
|
+
const waitForClose = new Promise<void>((resolve) => {
|
|
248
|
+
resolveStream = resolve;
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
if (terminal.status === 'exited') {
|
|
252
|
+
void onExit(terminal.exitCode ?? 0);
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
await waitForClose;
|
|
256
|
+
});
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
export async function sendTerminalInput(
|
|
260
|
+
c: Context,
|
|
261
|
+
terminalManager: TerminalManager,
|
|
262
|
+
) {
|
|
263
|
+
const id = c.req.param('id');
|
|
264
|
+
const terminal = terminalManager.get(id);
|
|
265
|
+
|
|
266
|
+
if (!terminal) {
|
|
267
|
+
return c.json({ error: 'Terminal not found' }, 404);
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
try {
|
|
271
|
+
const body = await c.req.json();
|
|
272
|
+
const { input } = body;
|
|
273
|
+
|
|
274
|
+
if (!input) {
|
|
275
|
+
return c.json({ error: 'input is required' }, 400);
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
terminal.write(input);
|
|
279
|
+
return c.json({ success: true });
|
|
280
|
+
} catch (error) {
|
|
281
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
282
|
+
return c.json({ error: message }, 500);
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
export async function killTerminal(
|
|
287
|
+
c: Context,
|
|
288
|
+
terminalManager: TerminalManager,
|
|
289
|
+
) {
|
|
290
|
+
const id = c.req.param('id');
|
|
291
|
+
|
|
292
|
+
try {
|
|
293
|
+
await terminalManager.kill(id);
|
|
294
|
+
return c.json({ success: true });
|
|
295
|
+
} catch (error) {
|
|
296
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
297
|
+
return c.json({ error: message }, 500);
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
export async function resizeTerminal(
|
|
302
|
+
c: Context,
|
|
303
|
+
terminalManager: TerminalManager,
|
|
304
|
+
) {
|
|
305
|
+
const id = c.req.param('id');
|
|
306
|
+
const terminal = terminalManager.get(id);
|
|
307
|
+
|
|
308
|
+
if (!terminal) {
|
|
309
|
+
return c.json({ error: 'Terminal not found' }, 404);
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
try {
|
|
313
|
+
const body = await c.req.json();
|
|
314
|
+
const { cols, rows } = body;
|
|
315
|
+
|
|
316
|
+
if (!cols || !rows || cols < 1 || rows < 1) {
|
|
317
|
+
return c.json({ error: 'valid cols and rows are required' }, 400);
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
terminal.resize(cols, rows);
|
|
321
|
+
return c.json({ success: true });
|
|
322
|
+
} catch (error) {
|
|
323
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
324
|
+
return c.json({ error: message }, 500);
|
|
325
|
+
}
|
|
326
|
+
}
|
package/src/routes/terminals.ts
CHANGED
|
@@ -1,10 +1,17 @@
|
|
|
1
|
-
import type { Context } from 'hono';
|
|
2
1
|
import type { Hono } from 'hono';
|
|
3
|
-
import { streamSSE } from 'hono/streaming';
|
|
4
2
|
import type { TerminalManager } from '@ottocode/sdk';
|
|
5
|
-
import { logger } from '@ottocode/sdk';
|
|
6
3
|
import { upgradeWebSocket } from '../ws.ts';
|
|
7
4
|
import { openApiRoute } from '../openapi/route.ts';
|
|
5
|
+
import {
|
|
6
|
+
createTerminal,
|
|
7
|
+
createTerminalWebSocketHandler,
|
|
8
|
+
getTerminal,
|
|
9
|
+
handleTerminalOutput,
|
|
10
|
+
killTerminal,
|
|
11
|
+
listTerminals,
|
|
12
|
+
resizeTerminal,
|
|
13
|
+
sendTerminalInput,
|
|
14
|
+
} from './terminals/service.ts';
|
|
8
15
|
|
|
9
16
|
export function registerTerminalsRoutes(
|
|
10
17
|
app: Hono,
|
|
@@ -42,13 +49,7 @@ export function registerTerminalsRoutes(
|
|
|
42
49
|
},
|
|
43
50
|
},
|
|
44
51
|
},
|
|
45
|
-
|
|
46
|
-
const terminals = terminalManager.list();
|
|
47
|
-
return c.json({
|
|
48
|
-
terminals: terminals.map((t) => t.toJSON()),
|
|
49
|
-
count: terminals.length,
|
|
50
|
-
});
|
|
51
|
-
},
|
|
52
|
+
(c) => c.json(listTerminals(terminalManager)),
|
|
52
53
|
);
|
|
53
54
|
|
|
54
55
|
openApiRoute(
|
|
@@ -122,45 +123,7 @@ export function registerTerminalsRoutes(
|
|
|
122
123
|
},
|
|
123
124
|
},
|
|
124
125
|
},
|
|
125
|
-
|
|
126
|
-
try {
|
|
127
|
-
const body = await c.req.json();
|
|
128
|
-
const { command, args, purpose, cwd, title } = body;
|
|
129
|
-
|
|
130
|
-
if (!command || !purpose) {
|
|
131
|
-
return c.json({ error: 'command and purpose are required' }, 400);
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
let resolvedCommand = command;
|
|
135
|
-
if (command === 'bash' || command === 'sh' || command === 'shell') {
|
|
136
|
-
resolvedCommand =
|
|
137
|
-
process.platform === 'win32'
|
|
138
|
-
? process.env.COMSPEC || 'cmd.exe'
|
|
139
|
-
: process.env.SHELL || '/bin/bash';
|
|
140
|
-
}
|
|
141
|
-
const resolvedCwd = cwd || process.cwd();
|
|
142
|
-
|
|
143
|
-
const terminal = terminalManager.create({
|
|
144
|
-
command: resolvedCommand,
|
|
145
|
-
args: args || [],
|
|
146
|
-
purpose,
|
|
147
|
-
cwd: resolvedCwd,
|
|
148
|
-
createdBy: 'user',
|
|
149
|
-
title,
|
|
150
|
-
});
|
|
151
|
-
|
|
152
|
-
return c.json({
|
|
153
|
-
terminalId: terminal.id,
|
|
154
|
-
pid: terminal.pid,
|
|
155
|
-
purpose: terminal.purpose,
|
|
156
|
-
command: terminal.command,
|
|
157
|
-
});
|
|
158
|
-
} catch (error) {
|
|
159
|
-
logger.error('Error creating terminal', error);
|
|
160
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
161
|
-
return c.json({ error: message }, 500);
|
|
162
|
-
}
|
|
163
|
-
},
|
|
126
|
+
(c) => createTerminal(c, terminalManager),
|
|
164
127
|
);
|
|
165
128
|
|
|
166
129
|
openApiRoute(
|
|
@@ -202,16 +165,7 @@ export function registerTerminalsRoutes(
|
|
|
202
165
|
},
|
|
203
166
|
},
|
|
204
167
|
},
|
|
205
|
-
|
|
206
|
-
const id = c.req.param('id');
|
|
207
|
-
const terminal = terminalManager.get(id);
|
|
208
|
-
|
|
209
|
-
if (!terminal) {
|
|
210
|
-
return c.json({ error: 'Terminal not found' }, 404);
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
return c.json({ terminal: terminal.toJSON() });
|
|
214
|
-
},
|
|
168
|
+
(c) => getTerminal(c, terminalManager),
|
|
215
169
|
);
|
|
216
170
|
|
|
217
171
|
openApiRoute(
|
|
@@ -238,186 +192,10 @@ export function registerTerminalsRoutes(
|
|
|
238
192
|
},
|
|
239
193
|
upgradeWebSocket((c) => {
|
|
240
194
|
const id = c.req.param('id');
|
|
241
|
-
|
|
242
|
-
let onData: ((data: string) => void) | null = null;
|
|
243
|
-
let onExit: ((exitCode: number) => void) | null = null;
|
|
244
|
-
|
|
245
|
-
return {
|
|
246
|
-
onOpen(_event, ws) {
|
|
247
|
-
const terminal = terminalManager.get(id);
|
|
248
|
-
if (!terminal) {
|
|
249
|
-
ws.close(4004, 'Terminal not found');
|
|
250
|
-
return;
|
|
251
|
-
}
|
|
252
|
-
|
|
253
|
-
const history = terminal.read();
|
|
254
|
-
for (const chunk of history) {
|
|
255
|
-
ws.send(chunk);
|
|
256
|
-
}
|
|
257
|
-
|
|
258
|
-
onData = (data: string) => {
|
|
259
|
-
try {
|
|
260
|
-
ws.send(data);
|
|
261
|
-
} catch {
|
|
262
|
-
// ws may be closed
|
|
263
|
-
}
|
|
264
|
-
};
|
|
265
|
-
|
|
266
|
-
onExit = (exitCode: number) => {
|
|
267
|
-
try {
|
|
268
|
-
ws.send(JSON.stringify({ type: 'exit', exitCode }));
|
|
269
|
-
ws.close(1000, 'Process exited');
|
|
270
|
-
} catch {
|
|
271
|
-
// ws may already be closed
|
|
272
|
-
}
|
|
273
|
-
};
|
|
274
|
-
|
|
275
|
-
terminal.onData(onData);
|
|
276
|
-
terminal.onExit(onExit);
|
|
277
|
-
|
|
278
|
-
if (terminal.status === 'exited') {
|
|
279
|
-
onExit(terminal.exitCode ?? 0);
|
|
280
|
-
}
|
|
281
|
-
},
|
|
282
|
-
onMessage(event, _ws) {
|
|
283
|
-
const terminal = terminalManager.get(id);
|
|
284
|
-
if (!terminal) return;
|
|
285
|
-
|
|
286
|
-
const raw = event.data;
|
|
287
|
-
const message =
|
|
288
|
-
typeof raw === 'string'
|
|
289
|
-
? raw
|
|
290
|
-
: raw instanceof ArrayBuffer
|
|
291
|
-
? new TextDecoder().decode(raw)
|
|
292
|
-
: String(raw);
|
|
293
|
-
|
|
294
|
-
if (message.startsWith('{')) {
|
|
295
|
-
try {
|
|
296
|
-
const msg = JSON.parse(message);
|
|
297
|
-
if (msg.type === 'resize' && msg.cols > 0 && msg.rows > 0) {
|
|
298
|
-
terminal.resize(msg.cols, msg.rows);
|
|
299
|
-
return;
|
|
300
|
-
}
|
|
301
|
-
} catch {
|
|
302
|
-
// not JSON, treat as input
|
|
303
|
-
}
|
|
304
|
-
}
|
|
305
|
-
|
|
306
|
-
terminal.write(message);
|
|
307
|
-
},
|
|
308
|
-
onClose() {
|
|
309
|
-
const terminal = terminalManager.get(id);
|
|
310
|
-
if (terminal) {
|
|
311
|
-
if (onData) terminal.removeDataListener(onData);
|
|
312
|
-
if (onExit) terminal.removeExitListener(onExit);
|
|
313
|
-
}
|
|
314
|
-
onData = null;
|
|
315
|
-
onExit = null;
|
|
316
|
-
},
|
|
317
|
-
onError() {
|
|
318
|
-
const terminal = terminalManager.get(id);
|
|
319
|
-
if (terminal) {
|
|
320
|
-
if (onData) terminal.removeDataListener(onData);
|
|
321
|
-
if (onExit) terminal.removeExitListener(onExit);
|
|
322
|
-
}
|
|
323
|
-
onData = null;
|
|
324
|
-
onExit = null;
|
|
325
|
-
},
|
|
326
|
-
};
|
|
195
|
+
return createTerminalWebSocketHandler(terminalManager, id);
|
|
327
196
|
}),
|
|
328
197
|
);
|
|
329
198
|
|
|
330
|
-
const handleTerminalOutput = async (c: Context) => {
|
|
331
|
-
const id = c.req.param('id');
|
|
332
|
-
const terminal = terminalManager.get(id);
|
|
333
|
-
|
|
334
|
-
if (!terminal) {
|
|
335
|
-
return c.json({ error: 'Terminal not found' }, 404);
|
|
336
|
-
}
|
|
337
|
-
|
|
338
|
-
const activeTerminal = terminal;
|
|
339
|
-
|
|
340
|
-
return streamSSE(c, async (stream) => {
|
|
341
|
-
const skipHistory = c.req.query('skipHistory') === 'true';
|
|
342
|
-
if (!skipHistory) {
|
|
343
|
-
const history = activeTerminal.read();
|
|
344
|
-
for (const line of history) {
|
|
345
|
-
await stream.write(
|
|
346
|
-
`data: ${JSON.stringify({ type: 'data', line })}\n\n`,
|
|
347
|
-
);
|
|
348
|
-
}
|
|
349
|
-
}
|
|
350
|
-
|
|
351
|
-
const sendEvent = async (payload: Record<string, unknown>) => {
|
|
352
|
-
try {
|
|
353
|
-
await stream.write(`data: ${JSON.stringify(payload)}\n\n`);
|
|
354
|
-
} catch (error) {
|
|
355
|
-
logger.error('SSE error writing event', error, { id });
|
|
356
|
-
}
|
|
357
|
-
};
|
|
358
|
-
|
|
359
|
-
const onData = (line: string) => {
|
|
360
|
-
void sendEvent({ type: 'data', line });
|
|
361
|
-
};
|
|
362
|
-
|
|
363
|
-
let resolveStream: (() => void) | null = null;
|
|
364
|
-
let finished = false;
|
|
365
|
-
|
|
366
|
-
const hb = setInterval(async () => {
|
|
367
|
-
try {
|
|
368
|
-
await stream.write(`: hb ${Date.now()}\n\n`);
|
|
369
|
-
} catch {
|
|
370
|
-
clearInterval(hb);
|
|
371
|
-
}
|
|
372
|
-
}, 15000);
|
|
373
|
-
|
|
374
|
-
function cleanup() {
|
|
375
|
-
activeTerminal.removeDataListener(onData);
|
|
376
|
-
activeTerminal.removeExitListener(onExit);
|
|
377
|
-
c.req.raw.signal.removeEventListener('abort', onAbort);
|
|
378
|
-
clearInterval(hb);
|
|
379
|
-
}
|
|
380
|
-
|
|
381
|
-
function finish() {
|
|
382
|
-
if (finished) {
|
|
383
|
-
return;
|
|
384
|
-
}
|
|
385
|
-
finished = true;
|
|
386
|
-
cleanup();
|
|
387
|
-
resolveStream?.();
|
|
388
|
-
}
|
|
389
|
-
|
|
390
|
-
async function onExit(exitCode: number) {
|
|
391
|
-
try {
|
|
392
|
-
await sendEvent({ type: 'exit', exitCode });
|
|
393
|
-
} finally {
|
|
394
|
-
stream.close();
|
|
395
|
-
finish();
|
|
396
|
-
}
|
|
397
|
-
}
|
|
398
|
-
|
|
399
|
-
function onAbort() {
|
|
400
|
-
stream.close();
|
|
401
|
-
finish();
|
|
402
|
-
}
|
|
403
|
-
|
|
404
|
-
terminal.onData(onData);
|
|
405
|
-
terminal.onExit(onExit);
|
|
406
|
-
|
|
407
|
-
c.req.raw.signal.addEventListener('abort', onAbort, { once: true });
|
|
408
|
-
|
|
409
|
-
const waitForClose = new Promise<void>((resolve) => {
|
|
410
|
-
resolveStream = resolve;
|
|
411
|
-
});
|
|
412
|
-
|
|
413
|
-
if (terminal.status === 'exited') {
|
|
414
|
-
void onExit(terminal.exitCode ?? 0);
|
|
415
|
-
}
|
|
416
|
-
|
|
417
|
-
await waitForClose;
|
|
418
|
-
});
|
|
419
|
-
};
|
|
420
|
-
|
|
421
199
|
openApiRoute(
|
|
422
200
|
app,
|
|
423
201
|
{
|
|
@@ -449,7 +227,7 @@ export function registerTerminalsRoutes(
|
|
|
449
227
|
},
|
|
450
228
|
},
|
|
451
229
|
},
|
|
452
|
-
handleTerminalOutput,
|
|
230
|
+
(c) => handleTerminalOutput(c, terminalManager),
|
|
453
231
|
);
|
|
454
232
|
openApiRoute(
|
|
455
233
|
app,
|
|
@@ -478,7 +256,7 @@ export function registerTerminalsRoutes(
|
|
|
478
256
|
},
|
|
479
257
|
},
|
|
480
258
|
},
|
|
481
|
-
handleTerminalOutput,
|
|
259
|
+
(c) => handleTerminalOutput(c, terminalManager),
|
|
482
260
|
);
|
|
483
261
|
|
|
484
262
|
openApiRoute(
|
|
@@ -534,29 +312,7 @@ export function registerTerminalsRoutes(
|
|
|
534
312
|
},
|
|
535
313
|
},
|
|
536
314
|
},
|
|
537
|
-
|
|
538
|
-
const id = c.req.param('id');
|
|
539
|
-
const terminal = terminalManager.get(id);
|
|
540
|
-
|
|
541
|
-
if (!terminal) {
|
|
542
|
-
return c.json({ error: 'Terminal not found' }, 404);
|
|
543
|
-
}
|
|
544
|
-
|
|
545
|
-
try {
|
|
546
|
-
const body = await c.req.json();
|
|
547
|
-
const { input } = body;
|
|
548
|
-
|
|
549
|
-
if (!input) {
|
|
550
|
-
return c.json({ error: 'input is required' }, 400);
|
|
551
|
-
}
|
|
552
|
-
|
|
553
|
-
terminal.write(input);
|
|
554
|
-
return c.json({ success: true });
|
|
555
|
-
} catch (error) {
|
|
556
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
557
|
-
return c.json({ error: message }, 500);
|
|
558
|
-
}
|
|
559
|
-
},
|
|
315
|
+
(c) => sendTerminalInput(c, terminalManager),
|
|
560
316
|
);
|
|
561
317
|
|
|
562
318
|
openApiRoute(
|
|
@@ -595,17 +351,7 @@ export function registerTerminalsRoutes(
|
|
|
595
351
|
},
|
|
596
352
|
},
|
|
597
353
|
},
|
|
598
|
-
|
|
599
|
-
const id = c.req.param('id');
|
|
600
|
-
|
|
601
|
-
try {
|
|
602
|
-
await terminalManager.kill(id);
|
|
603
|
-
return c.json({ success: true });
|
|
604
|
-
} catch (error) {
|
|
605
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
606
|
-
return c.json({ error: message }, 500);
|
|
607
|
-
}
|
|
608
|
-
},
|
|
354
|
+
(c) => killTerminal(c, terminalManager),
|
|
609
355
|
);
|
|
610
356
|
|
|
611
357
|
openApiRoute(
|
|
@@ -656,28 +402,6 @@ export function registerTerminalsRoutes(
|
|
|
656
402
|
'404': { description: 'Terminal not found' },
|
|
657
403
|
},
|
|
658
404
|
},
|
|
659
|
-
|
|
660
|
-
const id = c.req.param('id');
|
|
661
|
-
const terminal = terminalManager.get(id);
|
|
662
|
-
|
|
663
|
-
if (!terminal) {
|
|
664
|
-
return c.json({ error: 'Terminal not found' }, 404);
|
|
665
|
-
}
|
|
666
|
-
|
|
667
|
-
try {
|
|
668
|
-
const body = await c.req.json();
|
|
669
|
-
const { cols, rows } = body;
|
|
670
|
-
|
|
671
|
-
if (!cols || !rows || cols < 1 || rows < 1) {
|
|
672
|
-
return c.json({ error: 'valid cols and rows are required' }, 400);
|
|
673
|
-
}
|
|
674
|
-
|
|
675
|
-
terminal.resize(cols, rows);
|
|
676
|
-
return c.json({ success: true });
|
|
677
|
-
} catch (error) {
|
|
678
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
679
|
-
return c.json({ error: message }, 500);
|
|
680
|
-
}
|
|
681
|
-
},
|
|
405
|
+
(c) => resizeTerminal(c, terminalManager),
|
|
682
406
|
);
|
|
683
407
|
}
|