@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ottocode/server",
3
- "version": "0.1.228",
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.228",
53
- "@ottocode/database": "0.1.228",
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({
@@ -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
  },
@@ -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 { setConfig, loadConfig, type ProviderId } from '@ottocode/sdk';
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 GITHUB_COPILOT_HOSTS = [
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 MCP_SCOPES =
188
- 'repo read:org read:packages gist notifications read:project security_events';
189
- const existingAuth = await getAuth('copilot');
190
- const hasMCPScopes =
191
- existingAuth?.type === 'oauth' && existingAuth.scopes === MCP_SCOPES;
172
+ const existingAuth = await getStoredCopilotMCPToken(
173
+ copilotMCPOAuthStore,
174
+ name,
175
+ serverConfig.scope ?? 'global',
176
+ projectRoot,
177
+ );
192
178
 
193
- if (!existingAuth || existingAuth.type !== 'oauth' || !hasMCPScopes) {
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 MCP_SCOPES =
260
- 'repo read:org read:packages gist notifications read:project security_events';
261
- const existingAuth = await getAuth('copilot');
262
- if (
263
- existingAuth?.type === 'oauth' &&
264
- existingAuth.refresh &&
265
- existingAuth.scopes === MCP_SCOPES
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 (serverConfig) {
355
- if (!mcpMgr) {
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 getAuth('copilot');
425
- const authenticated = auth?.type === 'oauth' && !!auth.refresh;
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 { removeAuth } = await import('@ottocode/sdk');
454
- await removeAuth('copilot');
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
- export function registerSessionStreamRoute(app: Hono) {
12
- app.get('/v1/sessions/:id/stream', async (c) => {
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
- });
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
- 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
- // 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
- const signal = c.req.raw?.signal as AbortSignal | undefined;
49
- signal?.addEventListener('abort', () => {
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
- unsubscribe();
52
- try {
53
- controller.close();
54
- } catch {}
55
- });
56
- },
57
- });
42
+ }
43
+ }, 5000);
58
44
 
59
- return new Response(stream, { headers });
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
  }
@@ -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
- return counts ? { ...rest, toolCounts: counts } : rest;
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,
@@ -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
- app.get('/v1/terminals/:id/output', async (c) => {
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');
@@ -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
- app.get('/v1/tunnel/stream', async (c) => {
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() {