@agi-cli/server 0.1.55 → 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 +3 -3
- package/src/index.ts +45 -5
- package/src/presets.ts +81 -0
- package/src/routes/ask.ts +61 -6
- package/src/routes/config.ts +103 -5
- package/src/routes/git.ts +68 -25
- package/src/routes/session-stream.ts +8 -3
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@agi-cli/server",
|
|
3
|
-
"version": "0.1.
|
|
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.
|
|
33
|
-
"@agi-cli/database": "0.1.
|
|
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
|
|
122
|
-
|
|
123
|
-
|
|
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(
|
|
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
|
-
|
|
84
|
+
skipFileConfig ||
|
|
85
|
+
(typeof body.skipFileConfig === 'boolean'
|
|
33
86
|
? body.skipFileConfig
|
|
34
|
-
:
|
|
87
|
+
: false),
|
|
35
88
|
config:
|
|
36
|
-
|
|
89
|
+
injectableConfig ||
|
|
90
|
+
(body.config && typeof body.config === 'object'
|
|
37
91
|
? (body.config as InjectableConfig)
|
|
38
|
-
: undefined,
|
|
92
|
+
: undefined),
|
|
39
93
|
credentials:
|
|
40
|
-
|
|
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,
|
package/src/routes/config.ts
CHANGED
|
@@ -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
|
-
|
|
39
|
-
|
|
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:
|
|
83
|
+
agents: allAgents,
|
|
46
84
|
providers: authorizedProviders,
|
|
47
|
-
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
|
|
103
|
-
|
|
104
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
407
|
-
|
|
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
|
-
|
|
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
|
-
|
|
29
|
-
|
|
30
|
-
|
|
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', () => {
|