@agi-cli/server 0.1.56 → 0.1.57

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": "@agi-cli/server",
3
- "version": "0.1.56",
3
+ "version": "0.1.57",
4
4
  "description": "HTTP API server for AGI CLI",
5
5
  "type": "module",
6
6
  "main": "./src/index.ts",
@@ -29,8 +29,8 @@
29
29
  "typecheck": "tsc --noEmit"
30
30
  },
31
31
  "dependencies": {
32
- "@agi-cli/sdk": "0.1.56",
33
- "@agi-cli/database": "0.1.56",
32
+ "@agi-cli/sdk": "0.1.57",
33
+ "@agi-cli/database": "0.1.57",
34
34
  "drizzle-orm": "^0.44.5",
35
35
  "hono": "^4.9.9"
36
36
  },
package/src/index.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import { Hono } from 'hono';
2
2
  import { cors } from 'hono/cors';
3
- import type { ProviderId } from '@agi-cli/sdk';
3
+ import type { ProviderId, AuthInfo } from '@agi-cli/sdk';
4
4
  import { registerRootRoutes } from './routes/root.ts';
5
5
  import { registerOpenApiRoute } from './routes/openapi.ts';
6
6
  import { registerSessionsRoutes } from './routes/sessions.ts';
@@ -9,6 +9,7 @@ import { registerSessionStreamRoute } from './routes/session-stream.ts';
9
9
  import { registerAskRoutes } from './routes/ask.ts';
10
10
  import { registerConfigRoutes } from './routes/config.ts';
11
11
  import { registerGitRoutes } from './routes/git.ts';
12
+ import type { AgentConfigEntry } from './runtime/agent-registry.ts';
12
13
 
13
14
  function initApp() {
14
15
  const app = new Hono();
@@ -117,16 +118,48 @@ export function createStandaloneApp(_config?: StandaloneAppConfig) {
117
118
  return honoApp;
118
119
  }
119
120
 
121
+ /**
122
+ * Embedded app configuration with hybrid fallback:
123
+ * 1. Injected config (highest priority)
124
+ * 2. Environment variables
125
+ * 3. auth.json/config.json files (fallback)
126
+ *
127
+ * All fields are optional - if not provided, falls back to files/env
128
+ */
120
129
  export type EmbeddedAppConfig = {
121
- provider: ProviderId;
122
- model: string;
123
- apiKey: string;
130
+ /** Primary provider (optional - falls back to config.json or env) */
131
+ provider?: ProviderId;
132
+ /** Primary model (optional - falls back to config.json) */
133
+ model?: string;
134
+ /** Primary API key (optional - falls back to env vars or auth.json) */
135
+ apiKey?: string;
136
+ /** Default agent (optional - falls back to config.json) */
124
137
  agent?: string;
138
+ /** Multi-provider auth (optional - falls back to auth.json) */
139
+ auth?: Record<string, { apiKey: string } | AuthInfo>;
140
+ /** Custom agents (optional - falls back to .agi/agents/) */
141
+ agents?: Record<
142
+ string,
143
+ Omit<AgentConfigEntry, 'tools'> & { tools?: readonly string[] | string[] }
144
+ >;
145
+ /** Default settings (optional - falls back to config.json) */
146
+ defaults?: {
147
+ provider?: ProviderId;
148
+ model?: string;
149
+ agent?: string;
150
+ };
125
151
  };
126
152
 
127
- export function createEmbeddedApp(_config: EmbeddedAppConfig) {
153
+ export function createEmbeddedApp(config: EmbeddedAppConfig = {}) {
128
154
  const honoApp = new Hono();
129
155
 
156
+ // Store injected config in Hono context for routes to access
157
+ // Config can be empty - routes will fall back to files/env
158
+ honoApp.use('*', async (c, next) => {
159
+ c.set('embeddedConfig', config);
160
+ await next();
161
+ });
162
+
130
163
  // Enable CORS for all localhost ports (for web UI on random ports)
131
164
  honoApp.use(
132
165
  '*',
@@ -163,6 +196,7 @@ export function createEmbeddedApp(_config: EmbeddedAppConfig) {
163
196
  registerSessionMessagesRoutes(honoApp);
164
197
  registerSessionStreamRoute(honoApp);
165
198
  registerAskRoutes(honoApp);
199
+ registerConfigRoutes(honoApp);
166
200
  registerGitRoutes(honoApp);
167
201
 
168
202
  return honoApp;
@@ -181,3 +215,9 @@ export {
181
215
  } from './runtime/ask-service.ts';
182
216
  export { registerSessionsRoutes } from './routes/sessions.ts';
183
217
  export { registerAskRoutes } from './routes/ask.ts';
218
+ export {
219
+ BUILTIN_AGENTS,
220
+ BUILTIN_TOOLS,
221
+ type BuiltinAgent,
222
+ type BuiltinTool,
223
+ } from './presets.ts';
package/src/presets.ts ADDED
@@ -0,0 +1,81 @@
1
+ import AGENT_BUILD from '@agi-cli/sdk/prompts/agents/build.txt' with {
2
+ type: 'text',
3
+ };
4
+ import AGENT_PLAN from '@agi-cli/sdk/prompts/agents/plan.txt' with {
5
+ type: 'text',
6
+ };
7
+ import AGENT_GENERAL from '@agi-cli/sdk/prompts/agents/general.txt' with {
8
+ type: 'text',
9
+ };
10
+
11
+ export const BUILTIN_AGENTS = {
12
+ build: {
13
+ prompt: AGENT_BUILD,
14
+ tools: [
15
+ 'read',
16
+ 'write',
17
+ 'ls',
18
+ 'tree',
19
+ 'bash',
20
+ 'update_plan',
21
+ 'grep',
22
+ 'git_status',
23
+ 'git_diff',
24
+ 'ripgrep',
25
+ 'apply_patch',
26
+ 'websearch',
27
+ 'progress_update',
28
+ 'finish',
29
+ ] as string[],
30
+ },
31
+ plan: {
32
+ prompt: AGENT_PLAN,
33
+ tools: [
34
+ 'read',
35
+ 'ls',
36
+ 'tree',
37
+ 'ripgrep',
38
+ 'update_plan',
39
+ 'websearch',
40
+ 'progress_update',
41
+ 'finish',
42
+ ] as string[],
43
+ },
44
+ general: {
45
+ prompt: AGENT_GENERAL,
46
+ tools: [
47
+ 'read',
48
+ 'write',
49
+ 'ls',
50
+ 'tree',
51
+ 'bash',
52
+ 'ripgrep',
53
+ 'websearch',
54
+ 'update_plan',
55
+ 'progress_update',
56
+ 'finish',
57
+ ] as string[],
58
+ },
59
+ };
60
+
61
+ export const BUILTIN_TOOLS = [
62
+ 'read',
63
+ 'write',
64
+ 'ls',
65
+ 'tree',
66
+ 'bash',
67
+ 'grep',
68
+ 'ripgrep',
69
+ 'git_status',
70
+ 'git_diff',
71
+ 'git_commit',
72
+ 'apply_patch',
73
+ 'update_plan',
74
+ 'edit',
75
+ 'websearch',
76
+ 'progress_update',
77
+ 'finish',
78
+ ] as const;
79
+
80
+ export type BuiltinAgent = keyof typeof BUILTIN_AGENTS;
81
+ export type BuiltinTool = (typeof BUILTIN_TOOLS)[number];
package/src/routes/ask.ts CHANGED
@@ -5,6 +5,7 @@ import type {
5
5
  InjectableCredentials,
6
6
  } from '../runtime/ask-service.ts';
7
7
  import { AskServiceError, handleAskRequest } from '../runtime/ask-service.ts';
8
+ import type { EmbeddedAppConfig } from '../index.ts';
8
9
 
9
10
  export function registerAskRoutes(app: Hono) {
10
11
  app.post('/v1/ask', async (c) => {
@@ -18,6 +19,57 @@ export function registerAskRoutes(app: Hono) {
18
19
  return c.json({ error: 'Prompt is required.' }, 400);
19
20
  }
20
21
 
22
+ const embeddedConfig = c.get('embeddedConfig') as
23
+ | EmbeddedAppConfig
24
+ | undefined;
25
+
26
+ // Hybrid fallback: Use embedded config if provided, otherwise fall back to files/env
27
+ let injectableConfig: InjectableConfig | undefined;
28
+ let injectableCredentials: InjectableCredentials | undefined;
29
+ let skipFileConfig = false;
30
+
31
+ if (embeddedConfig && Object.keys(embeddedConfig).length > 0) {
32
+ // Has embedded config - build injectable config from it
33
+ const hasDefaults =
34
+ embeddedConfig.defaults ||
35
+ embeddedConfig.provider ||
36
+ embeddedConfig.model ||
37
+ embeddedConfig.agent;
38
+
39
+ if (hasDefaults) {
40
+ injectableConfig = {
41
+ defaults: embeddedConfig.defaults || {
42
+ agent: embeddedConfig.agent,
43
+ provider: embeddedConfig.provider,
44
+ model: embeddedConfig.model,
45
+ },
46
+ };
47
+ }
48
+
49
+ // Convert embedded auth to injectable credentials
50
+ const hasAuth = embeddedConfig.auth || embeddedConfig.apiKey;
51
+ if (hasAuth) {
52
+ if (embeddedConfig.auth) {
53
+ injectableCredentials = {};
54
+ for (const [provider, auth] of Object.entries(embeddedConfig.auth)) {
55
+ if ('apiKey' in auth) {
56
+ injectableCredentials[provider] = { apiKey: auth.apiKey };
57
+ } else {
58
+ injectableCredentials[provider] = auth;
59
+ }
60
+ }
61
+ } else if (embeddedConfig.apiKey && embeddedConfig.provider) {
62
+ injectableCredentials = {
63
+ [embeddedConfig.provider]: { apiKey: embeddedConfig.apiKey },
64
+ };
65
+ }
66
+
67
+ // Only skip file config if we have credentials injected
68
+ skipFileConfig = true;
69
+ }
70
+ // If no auth provided, skipFileConfig stays false -> will use ensureProviderEnv -> auth.json fallback
71
+ }
72
+
21
73
  const request: AskServerRequest = {
22
74
  projectRoot,
23
75
  prompt,
@@ -29,17 +81,20 @@ export function registerAskRoutes(app: Hono) {
29
81
  last: Boolean(body.last),
30
82
  jsonMode: Boolean(body.jsonMode),
31
83
  skipFileConfig:
32
- typeof body.skipFileConfig === 'boolean'
84
+ skipFileConfig ||
85
+ (typeof body.skipFileConfig === 'boolean'
33
86
  ? body.skipFileConfig
34
- : undefined,
87
+ : false),
35
88
  config:
36
- body.config && typeof body.config === 'object'
89
+ injectableConfig ||
90
+ (body.config && typeof body.config === 'object'
37
91
  ? (body.config as InjectableConfig)
38
- : undefined,
92
+ : undefined),
39
93
  credentials:
40
- body.credentials && typeof body.credentials === 'object'
94
+ injectableCredentials ||
95
+ (body.credentials && typeof body.credentials === 'object'
41
96
  ? (body.credentials as InjectableCredentials)
42
- : undefined,
97
+ : undefined),
43
98
  agentPrompt:
44
99
  typeof body.agentPrompt === 'string' ? body.agentPrompt : undefined,
45
100
  tools: Array.isArray(body.tools) ? body.tools : undefined,
@@ -3,6 +3,7 @@ import { loadConfig } from '@agi-cli/sdk';
3
3
  import { catalog, type ProviderId, isProviderAuthorized } from '@agi-cli/sdk';
4
4
  import { readdir } from 'node:fs/promises';
5
5
  import { join, basename } from 'node:path';
6
+ import type { EmbeddedAppConfig } from '../index.ts';
6
7
 
7
8
  export function registerConfigRoutes(app: Hono) {
8
9
  // Get working directory info
@@ -18,8 +19,14 @@ export function registerConfigRoutes(app: Hono) {
18
19
  // Get full config (agents, providers, models, defaults)
19
20
  app.get('/v1/config', async (c) => {
20
21
  const projectRoot = c.req.query('project') || process.cwd();
22
+ const embeddedConfig = c.get('embeddedConfig') as
23
+ | EmbeddedAppConfig
24
+ | undefined;
25
+
26
+ // Always load file config as base/fallback
21
27
  const cfg = await loadConfig(projectRoot);
22
28
 
29
+ // Hybrid mode: Merge embedded config with file config
23
30
  const builtInAgents = ['general', 'build', 'plan'];
24
31
  let customAgents: string[] = [];
25
32
 
@@ -31,25 +38,71 @@ export function registerConfigRoutes(app: Hono) {
31
38
  .map((f) => f.replace('.txt', ''));
32
39
  } catch {}
33
40
 
41
+ // Agents: Embedded custom agents + file-based agents
42
+ const fileAgents = [...builtInAgents, ...customAgents];
43
+ const embeddedAgents = embeddedConfig?.agents
44
+ ? Object.keys(embeddedConfig.agents)
45
+ : [];
46
+ const allAgents = Array.from(new Set([...embeddedAgents, ...fileAgents]));
47
+
48
+ // Providers: Check both embedded and file-based auth
34
49
  const allProviders = Object.keys(catalog) as ProviderId[];
35
50
  const authorizedProviders: ProviderId[] = [];
36
51
 
37
52
  for (const provider of allProviders) {
38
- const authorized = await isProviderAuthorized(cfg, provider);
39
- if (authorized) {
53
+ // Check embedded auth first
54
+ const hasEmbeddedAuth =
55
+ embeddedConfig?.provider === provider ||
56
+ (embeddedConfig?.auth && provider in embeddedConfig.auth);
57
+
58
+ // Fallback to file-based auth
59
+ const hasFileAuth = await isProviderAuthorized(cfg, provider);
60
+
61
+ if (hasEmbeddedAuth || hasFileAuth) {
40
62
  authorizedProviders.push(provider);
41
63
  }
42
64
  }
43
65
 
66
+ // Defaults: Embedded overrides file config
67
+ const defaults = {
68
+ agent:
69
+ embeddedConfig?.defaults?.agent ||
70
+ embeddedConfig?.agent ||
71
+ cfg.defaults.agent,
72
+ provider:
73
+ embeddedConfig?.defaults?.provider ||
74
+ embeddedConfig?.provider ||
75
+ cfg.defaults.provider,
76
+ model:
77
+ embeddedConfig?.defaults?.model ||
78
+ embeddedConfig?.model ||
79
+ cfg.defaults.model,
80
+ };
81
+
44
82
  return c.json({
45
- agents: [...builtInAgents, ...customAgents],
83
+ agents: allAgents,
46
84
  providers: authorizedProviders,
47
- defaults: cfg.defaults,
85
+ defaults,
48
86
  });
49
87
  });
50
88
 
51
89
  // Get available agents
52
90
  app.get('/v1/config/agents', async (c) => {
91
+ const embeddedConfig = c.get('embeddedConfig') as
92
+ | EmbeddedAppConfig
93
+ | undefined;
94
+
95
+ if (embeddedConfig) {
96
+ const agents = embeddedConfig.agents
97
+ ? Object.keys(embeddedConfig.agents)
98
+ : ['general', 'build', 'plan'];
99
+ return c.json({
100
+ agents,
101
+ default:
102
+ embeddedConfig.agent || embeddedConfig.defaults?.agent || 'general',
103
+ });
104
+ }
105
+
53
106
  const projectRoot = c.req.query('project') || process.cwd();
54
107
  const cfg = await loadConfig(projectRoot);
55
108
 
@@ -76,6 +129,21 @@ export function registerConfigRoutes(app: Hono) {
76
129
 
77
130
  // Get available providers (only authorized ones)
78
131
  app.get('/v1/config/providers', async (c) => {
132
+ const embeddedConfig = c.get('embeddedConfig') as
133
+ | EmbeddedAppConfig
134
+ | undefined;
135
+
136
+ if (embeddedConfig) {
137
+ const providers = embeddedConfig.auth
138
+ ? (Object.keys(embeddedConfig.auth) as ProviderId[])
139
+ : [embeddedConfig.provider];
140
+
141
+ return c.json({
142
+ providers,
143
+ default: embeddedConfig.defaults?.provider || embeddedConfig.provider,
144
+ });
145
+ }
146
+
79
147
  const projectRoot = c.req.query('project') || process.cwd();
80
148
  const cfg = await loadConfig(projectRoot);
81
149
 
@@ -97,9 +165,39 @@ export function registerConfigRoutes(app: Hono) {
97
165
 
98
166
  // Get available models for a provider
99
167
  app.get('/v1/config/providers/:provider/models', async (c) => {
168
+ const embeddedConfig = c.get('embeddedConfig') as
169
+ | EmbeddedAppConfig
170
+ | undefined;
171
+ const provider = c.req.param('provider') as ProviderId;
172
+
173
+ if (embeddedConfig) {
174
+ // Check if provider is authorized in embedded mode
175
+ const hasAuth =
176
+ embeddedConfig.provider === provider ||
177
+ (embeddedConfig.auth && provider in embeddedConfig.auth);
178
+
179
+ if (!hasAuth) {
180
+ return c.json({ error: 'Provider not authorized' }, 403);
181
+ }
182
+
183
+ const providerCatalog = catalog[provider];
184
+ if (!providerCatalog) {
185
+ return c.json({ error: 'Provider not found' }, 404);
186
+ }
187
+
188
+ return c.json({
189
+ models: providerCatalog.models.map((m) => ({
190
+ id: m.id,
191
+ label: m.label || m.id,
192
+ toolCall: m.toolCall,
193
+ reasoning: m.reasoning,
194
+ })),
195
+ default: embeddedConfig.model || embeddedConfig.defaults?.model,
196
+ });
197
+ }
198
+
100
199
  const projectRoot = c.req.query('project') || process.cwd();
101
200
  const cfg = await loadConfig(projectRoot);
102
- const provider = c.req.param('provider') as ProviderId;
103
201
 
104
202
  const authorized = await isProviderAuthorized(cfg, provider);
105
203
  if (!authorized) {
package/src/routes/git.ts CHANGED
@@ -90,8 +90,18 @@ function detectLanguage(filePath: string): string {
90
90
  return languageMap[ext] || 'plaintext';
91
91
  }
92
92
 
93
+ // Helper function to check if path is in a git repository
94
+ async function isGitRepository(path: string): Promise<boolean> {
95
+ try {
96
+ await execFileAsync('git', ['rev-parse', '--git-dir'], { cwd: path });
97
+ return true;
98
+ } catch {
99
+ return false;
100
+ }
101
+ }
102
+
93
103
  // Helper function to find git root directory
94
- async function findGitRoot(startPath: string): Promise<string> {
104
+ async function findGitRoot(startPath: string): Promise<string | null> {
95
105
  try {
96
106
  const { stdout } = await execFileAsync(
97
107
  'git',
@@ -99,10 +109,28 @@ async function findGitRoot(startPath: string): Promise<string> {
99
109
  { cwd: startPath },
100
110
  );
101
111
  return stdout.trim();
102
- } catch (_error) {
103
- // If not in a git repository, return the original path
104
- return startPath;
112
+ } catch {
113
+ return null;
114
+ }
115
+ }
116
+
117
+ // Helper to validate git repo and get root
118
+ async function validateAndGetGitRoot(
119
+ path: string,
120
+ ): Promise<{ gitRoot: string } | { error: string; code: string }> {
121
+ if (!(await isGitRepository(path))) {
122
+ return { error: 'Not a git repository', code: 'NOT_A_GIT_REPO' };
123
+ }
124
+
125
+ const gitRoot = await findGitRoot(path);
126
+ if (!gitRoot) {
127
+ return {
128
+ error: 'Could not find git repository root',
129
+ code: 'GIT_ROOT_NOT_FOUND',
130
+ };
105
131
  }
132
+
133
+ return { gitRoot };
106
134
  }
107
135
 
108
136
  // Git status parsing
@@ -231,7 +259,16 @@ export function registerGitRoutes(app: Hono) {
231
259
  });
232
260
 
233
261
  const requestedPath = query.project || process.cwd();
234
- const gitRoot = await findGitRoot(requestedPath);
262
+
263
+ const validation = await validateAndGetGitRoot(requestedPath);
264
+ if ('error' in validation) {
265
+ return c.json(
266
+ { status: 'error', error: validation.error, code: validation.code },
267
+ 400,
268
+ );
269
+ }
270
+
271
+ const { gitRoot } = validation;
235
272
 
236
273
  // Get git status
237
274
  const { stdout: statusOutput } = await execFileAsync(
@@ -290,12 +327,12 @@ export function registerGitRoutes(app: Hono) {
290
327
  data: status,
291
328
  });
292
329
  } catch (error) {
293
- console.error('Git status error:', error);
330
+ const errorMessage =
331
+ error instanceof Error ? error.message : 'Failed to get git status';
294
332
  return c.json(
295
333
  {
296
334
  status: 'error',
297
- error:
298
- error instanceof Error ? error.message : 'Failed to get git status',
335
+ error: errorMessage,
299
336
  },
300
337
  500,
301
338
  );
@@ -312,7 +349,16 @@ export function registerGitRoutes(app: Hono) {
312
349
  });
313
350
 
314
351
  const requestedPath = query.project || process.cwd();
315
- const gitRoot = await findGitRoot(requestedPath);
352
+
353
+ const validation = await validateAndGetGitRoot(requestedPath);
354
+ if ('error' in validation) {
355
+ return c.json(
356
+ { status: 'error', error: validation.error, code: validation.code },
357
+ 400,
358
+ );
359
+ }
360
+
361
+ const { gitRoot } = validation;
316
362
  const file = query.file;
317
363
  const staged = query.staged;
318
364
 
@@ -349,7 +395,6 @@ export function registerGitRoutes(app: Hono) {
349
395
  insertions = lines.length;
350
396
  deletions = 0;
351
397
  } catch (err) {
352
- console.error('Error reading new file:', err);
353
398
  diffOutput = `Error reading file: ${err instanceof Error ? err.message : 'Unknown error'}`;
354
399
  }
355
400
  } else {
@@ -403,15 +448,9 @@ export function registerGitRoutes(app: Hono) {
403
448
  },
404
449
  });
405
450
  } catch (error) {
406
- console.error('Git diff error:', error);
407
- return c.json(
408
- {
409
- status: 'error',
410
- error:
411
- error instanceof Error ? error.message : 'Failed to get git diff',
412
- },
413
- 500,
414
- );
451
+ const errorMessage =
452
+ error instanceof Error ? error.message : 'Failed to get git diff';
453
+ return c.json({ status: 'error', error: errorMessage }, 500);
415
454
  }
416
455
  });
417
456
 
@@ -504,7 +543,6 @@ Generate only the commit message, nothing else.`;
504
543
  },
505
544
  });
506
545
  } catch (error) {
507
- console.error('Generate commit message error:', error);
508
546
  return c.json(
509
547
  {
510
548
  status: 'error',
@@ -538,7 +576,6 @@ Generate only the commit message, nothing else.`;
538
576
  },
539
577
  });
540
578
  } catch (error) {
541
- console.error('Git stage error:', error);
542
579
  return c.json(
543
580
  {
544
581
  status: 'error',
@@ -579,7 +616,6 @@ Generate only the commit message, nothing else.`;
579
616
  },
580
617
  });
581
618
  } catch (error) {
582
- console.error('Git unstage error:', error);
583
619
  return c.json(
584
620
  {
585
621
  status: 'error',
@@ -656,7 +692,6 @@ Generate only the commit message, nothing else.`;
656
692
  },
657
693
  });
658
694
  } catch (error) {
659
- console.error('Git commit error:', error);
660
695
  return c.json(
661
696
  {
662
697
  status: 'error',
@@ -675,7 +710,16 @@ Generate only the commit message, nothing else.`;
675
710
  });
676
711
 
677
712
  const requestedPath = query.project || process.cwd();
678
- const gitRoot = await findGitRoot(requestedPath);
713
+
714
+ const validation = await validateAndGetGitRoot(requestedPath);
715
+ if ('error' in validation) {
716
+ return c.json(
717
+ { status: 'error', error: validation.error, code: validation.code },
718
+ 400,
719
+ );
720
+ }
721
+
722
+ const { gitRoot } = validation;
679
723
 
680
724
  const branch = await getCurrentBranch(gitRoot);
681
725
  const { ahead, behind } = await getAheadBehind(gitRoot);
@@ -720,7 +764,6 @@ Generate only the commit message, nothing else.`;
720
764
  },
721
765
  });
722
766
  } catch (error) {
723
- console.error('Git branch error:', error);
724
767
  return c.json(
725
768
  {
726
769
  status: 'error',
@@ -24,10 +24,15 @@ export function registerSessionStreamRoute(app: Hono) {
24
24
  const unsubscribe = subscribe(sessionId, write);
25
25
  // Initial ping
26
26
  controller.enqueue(encoder.encode(`: connected ${sessionId}\n\n`));
27
+ // Heartbeat every 5s to prevent idle timeout (Bun default is 10s)
27
28
  const hb = setInterval(() => {
28
- // Comment line as heartbeat to keep connection alive
29
- controller.enqueue(encoder.encode(`: hb ${Date.now()}\n\n`));
30
- }, 15000);
29
+ try {
30
+ controller.enqueue(encoder.encode(`: hb ${Date.now()}\n\n`));
31
+ } catch {
32
+ // Controller might be closed
33
+ clearInterval(hb);
34
+ }
35
+ }, 5000);
31
36
 
32
37
  const signal = c.req.raw?.signal as AbortSignal | undefined;
33
38
  signal?.addEventListener('abort', () => {