@ottocode/server 0.1.228 → 0.1.231
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/index.ts +1 -2
- package/src/openapi/paths/ask.ts +11 -0
- package/src/openapi/paths/config.ts +17 -0
- package/src/openapi/paths/messages.ts +6 -0
- package/src/openapi/schemas.ts +6 -0
- package/src/routes/ask.ts +8 -0
- package/src/routes/config/defaults.ts +13 -1
- package/src/routes/config/main.ts +7 -0
- package/src/routes/config/models.ts +2 -0
- package/src/routes/mcp.ts +62 -58
- package/src/routes/session-messages.ts +6 -1
- package/src/routes/session-stream.ts +46 -45
- package/src/routes/sessions.ts +4 -1
- package/src/routes/terminals.ts +15 -3
- package/src/routes/tunnel.ts +7 -3
- package/src/runtime/agent/runner-setup.ts +43 -34
- package/src/runtime/agent/runner.ts +171 -8
- package/src/runtime/ask/service.ts +16 -0
- package/src/runtime/debug/turn-dump.ts +330 -0
- package/src/runtime/message/history-builder.ts +99 -91
- package/src/runtime/message/service.ts +16 -2
- package/src/runtime/prompt/builder.ts +8 -6
- package/src/runtime/provider/reasoning.ts +291 -0
- package/src/runtime/session/queue.ts +2 -0
- package/src/runtime/tools/guards.ts +52 -4
- package/src/tools/adapter.ts +87 -8
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ottocode/server",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.231",
|
|
4
4
|
"description": "HTTP API server for ottocode",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./src/index.ts",
|
|
@@ -49,8 +49,8 @@
|
|
|
49
49
|
"typecheck": "tsc --noEmit"
|
|
50
50
|
},
|
|
51
51
|
"dependencies": {
|
|
52
|
-
"@ottocode/sdk": "0.1.
|
|
53
|
-
"@ottocode/database": "0.1.
|
|
52
|
+
"@ottocode/sdk": "0.1.231",
|
|
53
|
+
"@ottocode/database": "0.1.231",
|
|
54
54
|
"drizzle-orm": "^0.44.5",
|
|
55
55
|
"hono": "^4.9.9",
|
|
56
56
|
"zod": "^4.3.6"
|
package/src/index.ts
CHANGED
|
@@ -108,7 +108,6 @@ export type StandaloneAppConfig = {
|
|
|
108
108
|
export function createStandaloneApp(_config?: StandaloneAppConfig) {
|
|
109
109
|
const honoApp = new Hono();
|
|
110
110
|
|
|
111
|
-
// Enable CORS for localhost and local network access
|
|
112
111
|
honoApp.use(
|
|
113
112
|
'*',
|
|
114
113
|
cors({
|
|
@@ -192,6 +191,7 @@ export type EmbeddedAppConfig = {
|
|
|
192
191
|
model?: string;
|
|
193
192
|
agent?: string;
|
|
194
193
|
toolApproval?: 'auto' | 'dangerous' | 'all';
|
|
194
|
+
fullWidthContent?: boolean;
|
|
195
195
|
};
|
|
196
196
|
/** Additional CORS origins for proxies/Tailscale (e.g., ['https://myapp.ts.net', 'https://example.com']) */
|
|
197
197
|
corsOrigins?: string[];
|
|
@@ -214,7 +214,6 @@ export function createEmbeddedApp(config: EmbeddedAppConfig = {}) {
|
|
|
214
214
|
await next();
|
|
215
215
|
});
|
|
216
216
|
|
|
217
|
-
// Enable CORS for localhost and local network access
|
|
218
217
|
honoApp.use(
|
|
219
218
|
'*',
|
|
220
219
|
cors({
|
package/src/openapi/paths/ask.ts
CHANGED
|
@@ -35,6 +35,17 @@ export const askPaths = {
|
|
|
35
35
|
description:
|
|
36
36
|
'Optional model override for the selected provider.',
|
|
37
37
|
},
|
|
38
|
+
reasoningText: {
|
|
39
|
+
type: 'boolean',
|
|
40
|
+
description:
|
|
41
|
+
'Enable extended thinking / reasoning for models that support it.',
|
|
42
|
+
},
|
|
43
|
+
reasoningLevel: {
|
|
44
|
+
type: 'string',
|
|
45
|
+
enum: ['minimal', 'low', 'medium', 'high', 'max', 'xhigh'],
|
|
46
|
+
description:
|
|
47
|
+
'Optional reasoning intensity override for supported providers/models.',
|
|
48
|
+
},
|
|
38
49
|
sessionId: {
|
|
39
50
|
type: 'string',
|
|
40
51
|
description: 'Send the prompt to a specific session.',
|
|
@@ -178,7 +178,12 @@ export const configPaths = {
|
|
|
178
178
|
agent: { type: 'string' },
|
|
179
179
|
provider: { type: 'string' },
|
|
180
180
|
model: { type: 'string' },
|
|
181
|
+
fullWidthContent: { type: 'boolean' },
|
|
181
182
|
reasoningText: { type: 'boolean' },
|
|
183
|
+
reasoningLevel: {
|
|
184
|
+
type: 'string',
|
|
185
|
+
enum: ['minimal', 'low', 'medium', 'high', 'max', 'xhigh'],
|
|
186
|
+
},
|
|
182
187
|
scope: {
|
|
183
188
|
type: 'string',
|
|
184
189
|
enum: ['global', 'local'],
|
|
@@ -204,7 +209,19 @@ export const configPaths = {
|
|
|
204
209
|
agent: { type: 'string' },
|
|
205
210
|
provider: { type: 'string' },
|
|
206
211
|
model: { type: 'string' },
|
|
212
|
+
fullWidthContent: { type: 'boolean' },
|
|
207
213
|
reasoningText: { type: 'boolean' },
|
|
214
|
+
reasoningLevel: {
|
|
215
|
+
type: 'string',
|
|
216
|
+
enum: [
|
|
217
|
+
'minimal',
|
|
218
|
+
'low',
|
|
219
|
+
'medium',
|
|
220
|
+
'high',
|
|
221
|
+
'max',
|
|
222
|
+
'xhigh',
|
|
223
|
+
],
|
|
224
|
+
},
|
|
208
225
|
},
|
|
209
226
|
required: ['agent', 'provider', 'model'],
|
|
210
227
|
},
|
|
@@ -72,6 +72,12 @@ export const messagesPaths = {
|
|
|
72
72
|
description:
|
|
73
73
|
'Enable extended thinking / reasoning for models that support it.',
|
|
74
74
|
},
|
|
75
|
+
reasoningLevel: {
|
|
76
|
+
type: 'string',
|
|
77
|
+
enum: ['minimal', 'low', 'medium', 'high', 'max', 'xhigh'],
|
|
78
|
+
description:
|
|
79
|
+
'Reasoning intensity level for providers/models that support it.',
|
|
80
|
+
},
|
|
75
81
|
},
|
|
76
82
|
},
|
|
77
83
|
},
|
package/src/openapi/schemas.ts
CHANGED
|
@@ -73,6 +73,7 @@ export const schemas = {
|
|
|
73
73
|
additionalProperties: { type: 'integer' },
|
|
74
74
|
nullable: true,
|
|
75
75
|
},
|
|
76
|
+
isRunning: { type: 'boolean' },
|
|
76
77
|
},
|
|
77
78
|
required: ['id', 'agent', 'provider', 'model', 'projectPath', 'createdAt'],
|
|
78
79
|
},
|
|
@@ -195,7 +196,12 @@ export const schemas = {
|
|
|
195
196
|
agent: { type: 'string' },
|
|
196
197
|
provider: { $ref: '#/components/schemas/Provider' },
|
|
197
198
|
model: { type: 'string' },
|
|
199
|
+
fullWidthContent: { type: 'boolean' },
|
|
198
200
|
reasoningText: { type: 'boolean' },
|
|
201
|
+
reasoningLevel: {
|
|
202
|
+
type: 'string',
|
|
203
|
+
enum: ['minimal', 'low', 'medium', 'high', 'max', 'xhigh'],
|
|
204
|
+
},
|
|
199
205
|
},
|
|
200
206
|
required: ['agent', 'provider', 'model'],
|
|
201
207
|
},
|
package/src/routes/ask.ts
CHANGED
|
@@ -79,6 +79,14 @@ export function registerAskRoutes(app: Hono) {
|
|
|
79
79
|
agent: typeof body.agent === 'string' ? body.agent : undefined,
|
|
80
80
|
provider: typeof body.provider === 'string' ? body.provider : undefined,
|
|
81
81
|
model: typeof body.model === 'string' ? body.model : undefined,
|
|
82
|
+
reasoningText:
|
|
83
|
+
typeof body.reasoningText === 'boolean'
|
|
84
|
+
? body.reasoningText
|
|
85
|
+
: undefined,
|
|
86
|
+
reasoningLevel:
|
|
87
|
+
typeof body.reasoningLevel === 'string'
|
|
88
|
+
? (body.reasoningLevel as AskServerRequest['reasoningLevel'])
|
|
89
|
+
: undefined,
|
|
82
90
|
sessionId:
|
|
83
91
|
typeof body.sessionId === 'string' ? body.sessionId : undefined,
|
|
84
92
|
last: Boolean(body.last),
|
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
import type { Hono } from 'hono';
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
setConfig,
|
|
4
|
+
loadConfig,
|
|
5
|
+
type ProviderId,
|
|
6
|
+
type ReasoningLevel,
|
|
7
|
+
} from '@ottocode/sdk';
|
|
3
8
|
import { logger } from '@ottocode/sdk';
|
|
4
9
|
import { serializeError } from '../../runtime/errors/api-error.ts';
|
|
5
10
|
|
|
@@ -14,7 +19,9 @@ export function registerDefaultsRoute(app: Hono) {
|
|
|
14
19
|
toolApproval?: 'auto' | 'dangerous' | 'all';
|
|
15
20
|
guidedMode?: boolean;
|
|
16
21
|
reasoningText?: boolean;
|
|
22
|
+
reasoningLevel?: ReasoningLevel;
|
|
17
23
|
theme?: string;
|
|
24
|
+
fullWidthContent?: boolean;
|
|
18
25
|
scope?: 'global' | 'local';
|
|
19
26
|
}>();
|
|
20
27
|
|
|
@@ -26,7 +33,9 @@ export function registerDefaultsRoute(app: Hono) {
|
|
|
26
33
|
toolApproval: 'auto' | 'dangerous' | 'all';
|
|
27
34
|
guidedMode: boolean;
|
|
28
35
|
reasoningText: boolean;
|
|
36
|
+
reasoningLevel: ReasoningLevel;
|
|
29
37
|
theme: string;
|
|
38
|
+
fullWidthContent: boolean;
|
|
30
39
|
}> = {};
|
|
31
40
|
|
|
32
41
|
if (body.agent) updates.agent = body.agent;
|
|
@@ -36,7 +45,10 @@ export function registerDefaultsRoute(app: Hono) {
|
|
|
36
45
|
if (body.guidedMode !== undefined) updates.guidedMode = body.guidedMode;
|
|
37
46
|
if (body.reasoningText !== undefined)
|
|
38
47
|
updates.reasoningText = body.reasoningText;
|
|
48
|
+
if (body.reasoningLevel) updates.reasoningLevel = body.reasoningLevel;
|
|
39
49
|
if (body.theme) updates.theme = body.theme;
|
|
50
|
+
if (body.fullWidthContent !== undefined)
|
|
51
|
+
updates.fullWidthContent = body.fullWidthContent;
|
|
40
52
|
|
|
41
53
|
await setConfig(scope, updates, projectRoot);
|
|
42
54
|
|
|
@@ -61,7 +61,14 @@ export function registerMainConfigRoute(app: Hono) {
|
|
|
61
61
|
) as 'auto' | 'dangerous' | 'all',
|
|
62
62
|
guidedMode: cfg.defaults.guidedMode ?? false,
|
|
63
63
|
reasoningText: cfg.defaults.reasoningText ?? true,
|
|
64
|
+
reasoningLevel: cfg.defaults.reasoningLevel ?? 'high',
|
|
64
65
|
theme: cfg.defaults.theme,
|
|
66
|
+
fullWidthContent:
|
|
67
|
+
getDefault(
|
|
68
|
+
undefined,
|
|
69
|
+
embeddedConfig?.defaults?.fullWidthContent,
|
|
70
|
+
cfg.defaults.fullWidthContent,
|
|
71
|
+
) ?? false,
|
|
65
72
|
};
|
|
66
73
|
|
|
67
74
|
return c.json({
|
|
@@ -138,6 +138,7 @@ export function registerModelsRoutes(app: Hono) {
|
|
|
138
138
|
reasoningText: m.reasoningText,
|
|
139
139
|
vision: m.modalities?.input?.includes('image') ?? false,
|
|
140
140
|
attachment: m.attachment ?? false,
|
|
141
|
+
free: m.cost?.input === 0 && m.cost?.output === 0,
|
|
141
142
|
})),
|
|
142
143
|
default: getDefault(
|
|
143
144
|
embeddedConfig?.model,
|
|
@@ -205,6 +206,7 @@ export function registerModelsRoutes(app: Hono) {
|
|
|
205
206
|
reasoningText: m.reasoningText,
|
|
206
207
|
vision: m.modalities?.input?.includes('image') ?? false,
|
|
207
208
|
attachment: m.attachment ?? false,
|
|
209
|
+
free: m.cost?.input === 0 && m.cost?.output === 0,
|
|
208
210
|
})),
|
|
209
211
|
};
|
|
210
212
|
}
|
package/src/routes/mcp.ts
CHANGED
|
@@ -1,36 +1,21 @@
|
|
|
1
1
|
import type { Hono } from 'hono';
|
|
2
2
|
import {
|
|
3
|
+
COPILOT_MCP_SCOPE,
|
|
3
4
|
getMCPManager,
|
|
5
|
+
getCopilotMCPOAuthKey,
|
|
6
|
+
getStoredCopilotMCPToken,
|
|
4
7
|
initializeMCP,
|
|
8
|
+
isGitHubCopilotUrl,
|
|
5
9
|
loadMCPConfig,
|
|
6
10
|
getGlobalConfigDir,
|
|
7
11
|
MCPClientWrapper,
|
|
12
|
+
OAuthCredentialStore,
|
|
8
13
|
addMCPServerToConfig,
|
|
9
14
|
removeMCPServerFromConfig,
|
|
10
15
|
} from '@ottocode/sdk';
|
|
11
|
-
import {
|
|
12
|
-
authorizeCopilot,
|
|
13
|
-
pollForCopilotTokenOnce,
|
|
14
|
-
getAuth,
|
|
15
|
-
setAuth,
|
|
16
|
-
} from '@ottocode/sdk';
|
|
16
|
+
import { authorizeCopilot, pollForCopilotTokenOnce } from '@ottocode/sdk';
|
|
17
17
|
|
|
18
|
-
const
|
|
19
|
-
'api.githubcopilot.com',
|
|
20
|
-
'copilot-proxy.githubusercontent.com',
|
|
21
|
-
];
|
|
22
|
-
|
|
23
|
-
function isGitHubCopilotUrl(url?: string): boolean {
|
|
24
|
-
if (!url) return false;
|
|
25
|
-
try {
|
|
26
|
-
const parsed = new URL(url);
|
|
27
|
-
return GITHUB_COPILOT_HOSTS.some(
|
|
28
|
-
(h) => parsed.hostname === h || parsed.hostname.endsWith(`.${h}`),
|
|
29
|
-
);
|
|
30
|
-
} catch {
|
|
31
|
-
return false;
|
|
32
|
-
}
|
|
33
|
-
}
|
|
18
|
+
const copilotMCPOAuthStore = new OAuthCredentialStore();
|
|
34
19
|
|
|
35
20
|
const copilotMCPSessions = new Map<
|
|
36
21
|
string,
|
|
@@ -184,13 +169,14 @@ export function registerMCPRoutes(app: Hono) {
|
|
|
184
169
|
);
|
|
185
170
|
|
|
186
171
|
if (isGitHubCopilotUrl(serverConfig.url) && !status?.connected) {
|
|
187
|
-
const
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
172
|
+
const existingAuth = await getStoredCopilotMCPToken(
|
|
173
|
+
copilotMCPOAuthStore,
|
|
174
|
+
name,
|
|
175
|
+
serverConfig.scope ?? 'global',
|
|
176
|
+
projectRoot,
|
|
177
|
+
);
|
|
192
178
|
|
|
193
|
-
if (!existingAuth || existingAuth.
|
|
179
|
+
if (!existingAuth.token || existingAuth.needsReauth) {
|
|
194
180
|
const deviceData = await authorizeCopilot({ mcp: true });
|
|
195
181
|
const sessionId = crypto.randomUUID();
|
|
196
182
|
copilotMCPSessions.set(sessionId, {
|
|
@@ -256,14 +242,13 @@ export function registerMCPRoutes(app: Hono) {
|
|
|
256
242
|
|
|
257
243
|
if (isGitHubCopilotUrl(serverConfig.url)) {
|
|
258
244
|
try {
|
|
259
|
-
const
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
) {
|
|
245
|
+
const existingAuth = await getStoredCopilotMCPToken(
|
|
246
|
+
copilotMCPOAuthStore,
|
|
247
|
+
name,
|
|
248
|
+
serverConfig.scope ?? 'global',
|
|
249
|
+
projectRoot,
|
|
250
|
+
);
|
|
251
|
+
if (existingAuth.token && !existingAuth.needsReauth) {
|
|
267
252
|
return c.json({
|
|
268
253
|
ok: true,
|
|
269
254
|
name,
|
|
@@ -334,29 +319,31 @@ export function registerMCPRoutes(app: Hono) {
|
|
|
334
319
|
const result = await pollForCopilotTokenOnce(session.deviceCode);
|
|
335
320
|
if (result.status === 'complete') {
|
|
336
321
|
copilotMCPSessions.delete(sessionId);
|
|
337
|
-
await setAuth(
|
|
338
|
-
'copilot',
|
|
339
|
-
{
|
|
340
|
-
type: 'oauth',
|
|
341
|
-
refresh: result.accessToken,
|
|
342
|
-
access: result.accessToken,
|
|
343
|
-
expires: 0,
|
|
344
|
-
scopes:
|
|
345
|
-
'repo read:org read:packages gist notifications read:project security_events',
|
|
346
|
-
},
|
|
347
|
-
undefined,
|
|
348
|
-
'global',
|
|
349
|
-
);
|
|
350
322
|
const projectRoot = process.cwd();
|
|
351
323
|
const config = await loadMCPConfig(projectRoot, getGlobalConfigDir());
|
|
352
324
|
const serverConfig = config.servers.find((s) => s.name === name);
|
|
325
|
+
if (!serverConfig) {
|
|
326
|
+
return c.json(
|
|
327
|
+
{ ok: false, error: `Server "${name}" not found` },
|
|
328
|
+
404,
|
|
329
|
+
);
|
|
330
|
+
}
|
|
331
|
+
await copilotMCPOAuthStore.saveTokens(
|
|
332
|
+
getCopilotMCPOAuthKey(
|
|
333
|
+
name,
|
|
334
|
+
serverConfig.scope ?? 'global',
|
|
335
|
+
projectRoot,
|
|
336
|
+
),
|
|
337
|
+
{
|
|
338
|
+
access_token: result.accessToken,
|
|
339
|
+
scope: COPILOT_MCP_SCOPE,
|
|
340
|
+
},
|
|
341
|
+
);
|
|
353
342
|
let mcpMgr = getMCPManager();
|
|
354
|
-
if (
|
|
355
|
-
|
|
356
|
-
mcpMgr = await initializeMCP({ servers: [] }, projectRoot);
|
|
357
|
-
}
|
|
358
|
-
await mcpMgr.restartServer(serverConfig);
|
|
343
|
+
if (!mcpMgr) {
|
|
344
|
+
mcpMgr = await initializeMCP({ servers: [] }, projectRoot);
|
|
359
345
|
}
|
|
346
|
+
await mcpMgr.restartServer(serverConfig);
|
|
360
347
|
mcpMgr = getMCPManager();
|
|
361
348
|
const status = mcpMgr
|
|
362
349
|
? (await mcpMgr.getStatusAsync()).find((s) => s.name === name)
|
|
@@ -421,8 +408,13 @@ export function registerMCPRoutes(app: Hono) {
|
|
|
421
408
|
|
|
422
409
|
if (serverConfig && isGitHubCopilotUrl(serverConfig.url)) {
|
|
423
410
|
try {
|
|
424
|
-
const auth = await
|
|
425
|
-
|
|
411
|
+
const auth = await getStoredCopilotMCPToken(
|
|
412
|
+
copilotMCPOAuthStore,
|
|
413
|
+
name,
|
|
414
|
+
serverConfig.scope ?? 'global',
|
|
415
|
+
projectRoot,
|
|
416
|
+
);
|
|
417
|
+
const authenticated = !!auth.token && !auth.needsReauth;
|
|
426
418
|
return c.json({ authenticated, authType: 'copilot-device' });
|
|
427
419
|
} catch {
|
|
428
420
|
return c.json({ authenticated: false, authType: 'copilot-device' });
|
|
@@ -450,10 +442,22 @@ export function registerMCPRoutes(app: Hono) {
|
|
|
450
442
|
|
|
451
443
|
if (serverConfig && isGitHubCopilotUrl(serverConfig.url)) {
|
|
452
444
|
try {
|
|
453
|
-
const
|
|
454
|
-
|
|
445
|
+
const key = getCopilotMCPOAuthKey(
|
|
446
|
+
name,
|
|
447
|
+
serverConfig.scope ?? 'global',
|
|
448
|
+
projectRoot,
|
|
449
|
+
);
|
|
450
|
+
await copilotMCPOAuthStore.clearServer(key);
|
|
451
|
+
if (key !== name) {
|
|
452
|
+
await copilotMCPOAuthStore.clearServer(name);
|
|
453
|
+
}
|
|
455
454
|
const manager = getMCPManager();
|
|
456
455
|
if (manager) {
|
|
456
|
+
await manager.clearAuthData(
|
|
457
|
+
name,
|
|
458
|
+
serverConfig.scope ?? 'global',
|
|
459
|
+
projectRoot,
|
|
460
|
+
);
|
|
457
461
|
await manager.stopServer(name);
|
|
458
462
|
}
|
|
459
463
|
return c.json({ ok: true, name });
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { Hono } from 'hono';
|
|
2
|
-
import { loadConfig } from '@ottocode/sdk';
|
|
2
|
+
import { loadConfig, type ReasoningLevel } from '@ottocode/sdk';
|
|
3
3
|
import { getDb } from '@ottocode/database';
|
|
4
4
|
import { messages, messageParts, sessions } from '@ottocode/database/schema';
|
|
5
5
|
import { eq, inArray } from 'drizzle-orm';
|
|
@@ -124,6 +124,10 @@ export function registerSessionMessagesRoutes(app: Hono) {
|
|
|
124
124
|
|
|
125
125
|
const reasoning =
|
|
126
126
|
body?.reasoningText ?? cfg.defaults.reasoningText ?? false;
|
|
127
|
+
const reasoningLevel =
|
|
128
|
+
(body?.reasoningLevel as ReasoningLevel | undefined) ??
|
|
129
|
+
cfg.defaults.reasoningLevel ??
|
|
130
|
+
'high';
|
|
127
131
|
|
|
128
132
|
// Validate model capabilities if tools are allowed for this agent
|
|
129
133
|
const wantsToolCalls = true; // agent toolset may be non-empty
|
|
@@ -158,6 +162,7 @@ export function registerSessionMessagesRoutes(app: Hono) {
|
|
|
158
162
|
oneShot: Boolean(body?.oneShot),
|
|
159
163
|
userContext,
|
|
160
164
|
reasoningText: reasoning,
|
|
165
|
+
reasoningLevel,
|
|
161
166
|
images,
|
|
162
167
|
files,
|
|
163
168
|
});
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { Context } from 'hono';
|
|
1
2
|
import type { Hono } from 'hono';
|
|
2
3
|
import { subscribe } from '../events/bus.ts';
|
|
3
4
|
import type { OttoEvent } from '../events/types.ts';
|
|
@@ -8,54 +9,54 @@ function safeStringify(obj: unknown): string {
|
|
|
8
9
|
);
|
|
9
10
|
}
|
|
10
11
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
});
|
|
19
|
-
|
|
20
|
-
const encoder = new TextEncoder();
|
|
12
|
+
function handleSessionStream(c: Context) {
|
|
13
|
+
const sessionId = c.req.param('id');
|
|
14
|
+
const headers = new Headers({
|
|
15
|
+
'Content-Type': 'text/event-stream',
|
|
16
|
+
'Cache-Control': 'no-cache, no-transform',
|
|
17
|
+
Connection: 'keep-alive',
|
|
18
|
+
});
|
|
21
19
|
|
|
22
|
-
|
|
23
|
-
start(controller) {
|
|
24
|
-
const write = (evt: OttoEvent) => {
|
|
25
|
-
let line: string;
|
|
26
|
-
try {
|
|
27
|
-
line =
|
|
28
|
-
`event: ${evt.type}\n` +
|
|
29
|
-
`data: ${safeStringify(evt.payload ?? {})}\n\n`;
|
|
30
|
-
} catch {
|
|
31
|
-
line = `event: ${evt.type}\ndata: {}\n\n`;
|
|
32
|
-
}
|
|
33
|
-
controller.enqueue(encoder.encode(line));
|
|
34
|
-
};
|
|
35
|
-
const unsubscribe = subscribe(sessionId, write);
|
|
36
|
-
// Initial ping
|
|
37
|
-
controller.enqueue(encoder.encode(`: connected ${sessionId}\n\n`));
|
|
38
|
-
// Heartbeat every 5s to prevent idle timeout (Bun default is 10s)
|
|
39
|
-
const hb = setInterval(() => {
|
|
40
|
-
try {
|
|
41
|
-
controller.enqueue(encoder.encode(`: hb ${Date.now()}\n\n`));
|
|
42
|
-
} catch {
|
|
43
|
-
// Controller might be closed
|
|
44
|
-
clearInterval(hb);
|
|
45
|
-
}
|
|
46
|
-
}, 5000);
|
|
20
|
+
const encoder = new TextEncoder();
|
|
47
21
|
|
|
48
|
-
|
|
49
|
-
|
|
22
|
+
const stream = new ReadableStream<Uint8Array>({
|
|
23
|
+
start(controller) {
|
|
24
|
+
const write = (evt: OttoEvent) => {
|
|
25
|
+
let line: string;
|
|
26
|
+
try {
|
|
27
|
+
line =
|
|
28
|
+
`event: ${evt.type}\n` +
|
|
29
|
+
`data: ${safeStringify(evt.payload ?? {})}\n\n`;
|
|
30
|
+
} catch {
|
|
31
|
+
line = `event: ${evt.type}\ndata: {}\n\n`;
|
|
32
|
+
}
|
|
33
|
+
controller.enqueue(encoder.encode(line));
|
|
34
|
+
};
|
|
35
|
+
const unsubscribe = subscribe(sessionId, write);
|
|
36
|
+
controller.enqueue(encoder.encode(`: connected ${sessionId}\n\n`));
|
|
37
|
+
const hb = setInterval(() => {
|
|
38
|
+
try {
|
|
39
|
+
controller.enqueue(encoder.encode(`: hb ${Date.now()}\n\n`));
|
|
40
|
+
} catch {
|
|
50
41
|
clearInterval(hb);
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
controller.close();
|
|
54
|
-
} catch {}
|
|
55
|
-
});
|
|
56
|
-
},
|
|
57
|
-
});
|
|
42
|
+
}
|
|
43
|
+
}, 5000);
|
|
58
44
|
|
|
59
|
-
|
|
45
|
+
const signal = c.req.raw?.signal as AbortSignal | undefined;
|
|
46
|
+
signal?.addEventListener('abort', () => {
|
|
47
|
+
clearInterval(hb);
|
|
48
|
+
unsubscribe();
|
|
49
|
+
try {
|
|
50
|
+
controller.close();
|
|
51
|
+
} catch {}
|
|
52
|
+
});
|
|
53
|
+
},
|
|
60
54
|
});
|
|
55
|
+
|
|
56
|
+
return new Response(stream, { headers });
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export function registerSessionStreamRoute(app: Hono) {
|
|
60
|
+
app.get('/v1/sessions/:id/stream', handleSessionStream);
|
|
61
|
+
app.post('/v1/sessions/:id/stream', handleSessionStream);
|
|
61
62
|
}
|
package/src/routes/sessions.ts
CHANGED
|
@@ -15,6 +15,7 @@ import { resolveAgentConfig } from '../runtime/agent/registry.ts';
|
|
|
15
15
|
import { createSession as createSessionRow } from '../runtime/session/manager.ts';
|
|
16
16
|
import { serializeError } from '../runtime/errors/api-error.ts';
|
|
17
17
|
import { logger } from '@ottocode/sdk';
|
|
18
|
+
import { getRunnerState } from '../runtime/session/queue.ts';
|
|
18
19
|
|
|
19
20
|
export function registerSessionsRoutes(app: Hono) {
|
|
20
21
|
// List sessions
|
|
@@ -53,7 +54,9 @@ export function registerSessionsRoutes(app: Hono) {
|
|
|
53
54
|
} catch {}
|
|
54
55
|
}
|
|
55
56
|
const { toolCountsJson: _toolCountsJson, ...rest } = r;
|
|
56
|
-
|
|
57
|
+
const isRunning = getRunnerState(r.id)?.running ?? false;
|
|
58
|
+
const base = counts ? { ...rest, toolCounts: counts } : rest;
|
|
59
|
+
return { ...base, isRunning };
|
|
57
60
|
});
|
|
58
61
|
return c.json({
|
|
59
62
|
items: normalized,
|
package/src/routes/terminals.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { Context } from 'hono';
|
|
1
2
|
import type { Hono } from 'hono';
|
|
2
3
|
import { streamSSE } from 'hono/streaming';
|
|
3
4
|
import type { TerminalManager } from '@ottocode/sdk';
|
|
@@ -77,7 +78,7 @@ export function registerTerminalsRoutes(
|
|
|
77
78
|
return c.json({ terminal: terminal.toJSON() });
|
|
78
79
|
});
|
|
79
80
|
|
|
80
|
-
|
|
81
|
+
const handleTerminalOutput = async (c: Context) => {
|
|
81
82
|
const id = c.req.param('id');
|
|
82
83
|
logger.debug('SSE client connecting to terminal', { id });
|
|
83
84
|
const terminal = terminalManager.get(id);
|
|
@@ -91,7 +92,6 @@ export function registerTerminalsRoutes(
|
|
|
91
92
|
|
|
92
93
|
return streamSSE(c, async (stream) => {
|
|
93
94
|
logger.debug('SSE stream started for terminal', { id });
|
|
94
|
-
// Send historical buffer first (unless skipHistory is set)
|
|
95
95
|
const skipHistory = c.req.query('skipHistory') === 'true';
|
|
96
96
|
if (!skipHistory) {
|
|
97
97
|
const history = activeTerminal.read();
|
|
@@ -121,10 +121,19 @@ export function registerTerminalsRoutes(
|
|
|
121
121
|
let resolveStream: (() => void) | null = null;
|
|
122
122
|
let finished = false;
|
|
123
123
|
|
|
124
|
+
const hb = setInterval(async () => {
|
|
125
|
+
try {
|
|
126
|
+
await stream.write(`: hb ${Date.now()}\n\n`);
|
|
127
|
+
} catch {
|
|
128
|
+
clearInterval(hb);
|
|
129
|
+
}
|
|
130
|
+
}, 15000);
|
|
131
|
+
|
|
124
132
|
function cleanup() {
|
|
125
133
|
activeTerminal.removeDataListener(onData);
|
|
126
134
|
activeTerminal.removeExitListener(onExit);
|
|
127
135
|
c.req.raw.signal.removeEventListener('abort', onAbort);
|
|
136
|
+
clearInterval(hb);
|
|
128
137
|
}
|
|
129
138
|
|
|
130
139
|
function finish() {
|
|
@@ -168,7 +177,10 @@ export function registerTerminalsRoutes(
|
|
|
168
177
|
|
|
169
178
|
await waitForClose;
|
|
170
179
|
});
|
|
171
|
-
}
|
|
180
|
+
};
|
|
181
|
+
|
|
182
|
+
app.get('/v1/terminals/:id/output', handleTerminalOutput);
|
|
183
|
+
app.post('/v1/terminals/:id/output', handleTerminalOutput);
|
|
172
184
|
|
|
173
185
|
app.post('/v1/terminals/:id/input', async (c) => {
|
|
174
186
|
const id = c.req.param('id');
|
package/src/routes/tunnel.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { Hono } from 'hono';
|
|
2
|
+
import type { Context } from 'hono';
|
|
2
3
|
import { streamSSE } from 'hono/streaming';
|
|
3
4
|
import {
|
|
4
5
|
OttoTunnel,
|
|
@@ -159,8 +160,8 @@ export function registerTunnelRoutes(app: Hono) {
|
|
|
159
160
|
}
|
|
160
161
|
});
|
|
161
162
|
|
|
162
|
-
|
|
163
|
-
return streamSSE(c, async (stream) => {
|
|
163
|
+
const handleTunnelStream = async (c: Context) => {
|
|
164
|
+
return streamSSE(c as Context, async (stream) => {
|
|
164
165
|
const sendEvent = async (data: Record<string, unknown>) => {
|
|
165
166
|
try {
|
|
166
167
|
await stream.write(`data: ${JSON.stringify(data)}\n\n`);
|
|
@@ -202,7 +203,10 @@ export function registerTunnelRoutes(app: Hono) {
|
|
|
202
203
|
|
|
203
204
|
clearInterval(interval);
|
|
204
205
|
});
|
|
205
|
-
}
|
|
206
|
+
};
|
|
207
|
+
|
|
208
|
+
app.get('/v1/tunnel/stream', handleTunnelStream);
|
|
209
|
+
app.post('/v1/tunnel/stream', handleTunnelStream);
|
|
206
210
|
}
|
|
207
211
|
|
|
208
212
|
export function stopActiveTunnel() {
|