@agi-cli/server 0.1.67 → 0.1.68

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.67",
3
+ "version": "0.1.68",
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.67",
33
- "@agi-cli/database": "0.1.67",
32
+ "@agi-cli/sdk": "0.1.68",
33
+ "@agi-cli/database": "0.1.68",
34
34
  "drizzle-orm": "^0.44.5",
35
35
  "hono": "^4.9.9",
36
36
  "zod": "^4.1.8"
package/src/index.ts CHANGED
@@ -230,3 +230,12 @@ export {
230
230
  type BuiltinAgent,
231
231
  type BuiltinTool,
232
232
  } from './presets.ts';
233
+
234
+ // Export debug state management
235
+ export {
236
+ setDebugEnabled,
237
+ isDebugEnabled,
238
+ setTraceEnabled,
239
+ isTraceEnabled,
240
+ } from './runtime/debug-state.ts';
241
+ export { logger } from './runtime/logger.ts';
package/src/routes/ask.ts CHANGED
@@ -4,8 +4,10 @@ import type {
4
4
  InjectableConfig,
5
5
  InjectableCredentials,
6
6
  } from '../runtime/ask-service.ts';
7
- import { AskServiceError, handleAskRequest } from '../runtime/ask-service.ts';
7
+ import { handleAskRequest } from '../runtime/ask-service.ts';
8
8
  import type { EmbeddedAppConfig } from '../index.ts';
9
+ import { serializeError } from '../runtime/api-error.ts';
10
+ import { logger } from '../runtime/logger.ts';
9
11
 
10
12
  export function registerAskRoutes(app: Hono) {
11
13
  app.post('/v1/ask', async (c) => {
@@ -104,11 +106,9 @@ export function registerAskRoutes(app: Hono) {
104
106
  const response = await handleAskRequest(request);
105
107
  return c.json(response, 202);
106
108
  } catch (err) {
107
- if (err instanceof AskServiceError) {
108
- return c.json({ error: err.message }, err.status as never);
109
- }
110
- const message = err instanceof Error ? err.message : String(err);
111
- return c.json({ error: message }, 400);
109
+ logger.error('Ask request failed', err);
110
+ const errorResponse = serializeError(err);
111
+ return c.json(errorResponse, errorResponse.error.status || 400);
112
112
  }
113
113
  });
114
114
  }
@@ -5,6 +5,8 @@ import { readdir } from 'node:fs/promises';
5
5
  import { join, basename } from 'node:path';
6
6
  import type { EmbeddedAppConfig } from '../index.ts';
7
7
  import type { AGIConfig } from '@agi-cli/sdk';
8
+ import { logger } from '../runtime/logger.ts';
9
+ import { serializeError } from '../runtime/api-error.ts';
8
10
 
9
11
  /**
10
12
  * Check if a provider is authorized in either embedded config or file-based config
@@ -65,190 +67,225 @@ function getDefault<T>(
65
67
  export function registerConfigRoutes(app: Hono) {
66
68
  // Get working directory info
67
69
  app.get('/v1/config/cwd', (c) => {
68
- const cwd = process.cwd();
69
- const dirName = basename(cwd);
70
- return c.json({
71
- cwd,
72
- dirName,
73
- });
70
+ try {
71
+ const cwd = process.cwd();
72
+ const dirName = basename(cwd);
73
+ return c.json({
74
+ cwd,
75
+ dirName,
76
+ });
77
+ } catch (error) {
78
+ logger.error('Failed to get current working directory', error);
79
+ const errorResponse = serializeError(error);
80
+ return c.json(errorResponse, errorResponse.error.status || 500);
81
+ }
74
82
  });
75
83
 
76
84
  // Get full config (agents, providers, models, defaults)
77
85
  app.get('/v1/config', async (c) => {
78
- const projectRoot = c.req.query('project') || process.cwd();
79
- const embeddedConfig = c.get('embeddedConfig') as
80
- | EmbeddedAppConfig
81
- | undefined;
82
-
83
- // Always load file config as base/fallback
84
- const cfg = await loadConfig(projectRoot);
85
-
86
- // Hybrid mode: Merge embedded config with file config
87
- const builtInAgents = ['general', 'build', 'plan'];
88
- let customAgents: string[] = [];
89
-
90
86
  try {
91
- const customAgentsPath = join(cfg.projectRoot, '.agi', 'agents');
92
- const files = await readdir(customAgentsPath).catch(() => []);
93
- customAgents = files
94
- .filter((f) => f.endsWith('.txt'))
95
- .map((f) => f.replace('.txt', ''));
96
- } catch {}
97
-
98
- // Agents: Embedded custom agents + file-based agents
99
- const fileAgents = [...builtInAgents, ...customAgents];
100
- const embeddedAgents = embeddedConfig?.agents
101
- ? Object.keys(embeddedConfig.agents)
102
- : [];
103
- const allAgents = Array.from(new Set([...embeddedAgents, ...fileAgents]));
104
-
105
- // Providers: Check both embedded and file-based auth
106
- const authorizedProviders = await getAuthorizedProviders(
107
- embeddedConfig,
108
- cfg,
109
- );
110
-
111
- // Defaults: Embedded overrides file config
112
- const defaults = {
113
- agent: getDefault(
114
- embeddedConfig?.agent,
115
- embeddedConfig?.defaults?.agent,
116
- cfg.defaults.agent,
117
- ),
118
- provider: getDefault(
119
- embeddedConfig?.provider,
120
- embeddedConfig?.defaults?.provider,
121
- cfg.defaults.provider,
122
- ),
123
- model: getDefault(
124
- embeddedConfig?.model,
125
- embeddedConfig?.defaults?.model,
126
- cfg.defaults.model,
127
- ),
128
- };
129
-
130
- return c.json({
131
- agents: allAgents,
132
- providers: authorizedProviders,
133
- defaults,
134
- });
135
- });
136
-
137
- // Get available agents
138
- app.get('/v1/config/agents', async (c) => {
139
- const embeddedConfig = c.get('embeddedConfig') as
140
- | EmbeddedAppConfig
141
- | undefined;
142
-
143
- if (embeddedConfig) {
144
- const agents = embeddedConfig.agents
87
+ const projectRoot = c.req.query('project') || process.cwd();
88
+ const embeddedConfig = c.get('embeddedConfig') as
89
+ | EmbeddedAppConfig
90
+ | undefined;
91
+
92
+ // Always load file config as base/fallback
93
+ const cfg = await loadConfig(projectRoot);
94
+
95
+ // Hybrid mode: Merge embedded config with file config
96
+ const builtInAgents = ['general', 'build', 'plan'];
97
+ let customAgents: string[] = [];
98
+
99
+ try {
100
+ const customAgentsPath = join(cfg.projectRoot, '.agi', 'agents');
101
+ const files = await readdir(customAgentsPath).catch(() => []);
102
+ customAgents = files
103
+ .filter((f) => f.endsWith('.txt'))
104
+ .map((f) => f.replace('.txt', ''));
105
+ } catch (err) {
106
+ logger.debug('Failed to read custom agents directory', err);
107
+ }
108
+
109
+ // Agents: Embedded custom agents + file-based agents
110
+ const fileAgents = [...builtInAgents, ...customAgents];
111
+ const embeddedAgents = embeddedConfig?.agents
145
112
  ? Object.keys(embeddedConfig.agents)
146
- : ['general', 'build', 'plan'];
147
- return c.json({
148
- agents,
149
- default: getDefault(
150
- embeddedConfig.agent,
151
- embeddedConfig.defaults?.agent,
152
- 'general',
113
+ : [];
114
+ const allAgents = Array.from(new Set([...embeddedAgents, ...fileAgents]));
115
+
116
+ // Providers: Check both embedded and file-based auth
117
+ const authorizedProviders = await getAuthorizedProviders(
118
+ embeddedConfig,
119
+ cfg,
120
+ );
121
+
122
+ // Defaults: Embedded overrides file config
123
+ const defaults = {
124
+ agent: getDefault(
125
+ embeddedConfig?.agent,
126
+ embeddedConfig?.defaults?.agent,
127
+ cfg.defaults.agent,
153
128
  ),
129
+ provider: getDefault(
130
+ embeddedConfig?.provider,
131
+ embeddedConfig?.defaults?.provider,
132
+ cfg.defaults.provider,
133
+ ),
134
+ model: getDefault(
135
+ embeddedConfig?.model,
136
+ embeddedConfig?.defaults?.model,
137
+ cfg.defaults.model,
138
+ ),
139
+ };
140
+
141
+ return c.json({
142
+ agents: allAgents,
143
+ providers: authorizedProviders,
144
+ defaults,
154
145
  });
146
+ } catch (error) {
147
+ logger.error('Failed to load config', error);
148
+ const errorResponse = serializeError(error);
149
+ return c.json(errorResponse, errorResponse.error.status || 500);
155
150
  }
151
+ });
156
152
 
157
- const projectRoot = c.req.query('project') || process.cwd();
158
- const cfg = await loadConfig(projectRoot);
159
-
160
- const builtInAgents = ['general', 'build', 'plan'];
161
-
153
+ // Get available agents
154
+ app.get('/v1/config/agents', async (c) => {
162
155
  try {
163
- const customAgentsPath = join(cfg.projectRoot, '.agi', 'agents');
164
- const files = await readdir(customAgentsPath).catch(() => []);
165
- const customAgents = files
166
- .filter((f) => f.endsWith('.txt'))
167
- .map((f) => f.replace('.txt', ''));
168
-
169
- return c.json({
170
- agents: [...builtInAgents, ...customAgents],
171
- default: cfg.defaults.agent,
172
- });
173
- } catch {
174
- return c.json({
175
- agents: builtInAgents,
176
- default: cfg.defaults.agent,
177
- });
156
+ const embeddedConfig = c.get('embeddedConfig') as
157
+ | EmbeddedAppConfig
158
+ | undefined;
159
+
160
+ if (embeddedConfig) {
161
+ const agents = embeddedConfig.agents
162
+ ? Object.keys(embeddedConfig.agents)
163
+ : ['general', 'build', 'plan'];
164
+ return c.json({
165
+ agents,
166
+ default: getDefault(
167
+ embeddedConfig.agent,
168
+ embeddedConfig.defaults?.agent,
169
+ 'general',
170
+ ),
171
+ });
172
+ }
173
+
174
+ const projectRoot = c.req.query('project') || process.cwd();
175
+ const cfg = await loadConfig(projectRoot);
176
+
177
+ const builtInAgents = ['general', 'build', 'plan'];
178
+
179
+ try {
180
+ const customAgentsPath = join(cfg.projectRoot, '.agi', 'agents');
181
+ const files = await readdir(customAgentsPath).catch(() => []);
182
+ const customAgents = files
183
+ .filter((f) => f.endsWith('.txt'))
184
+ .map((f) => f.replace('.txt', ''));
185
+
186
+ return c.json({
187
+ agents: [...builtInAgents, ...customAgents],
188
+ default: cfg.defaults.agent,
189
+ });
190
+ } catch (err) {
191
+ logger.debug('Failed to read custom agents directory', err);
192
+ return c.json({
193
+ agents: builtInAgents,
194
+ default: cfg.defaults.agent,
195
+ });
196
+ }
197
+ } catch (error) {
198
+ logger.error('Failed to get agents', error);
199
+ const errorResponse = serializeError(error);
200
+ return c.json(errorResponse, errorResponse.error.status || 500);
178
201
  }
179
202
  });
180
203
 
181
204
  // Get available providers (only authorized ones)
182
205
  app.get('/v1/config/providers', async (c) => {
183
- const embeddedConfig = c.get('embeddedConfig') as
184
- | EmbeddedAppConfig
185
- | undefined;
186
-
187
- if (embeddedConfig) {
188
- const providers = embeddedConfig.auth
189
- ? (Object.keys(embeddedConfig.auth) as ProviderId[])
190
- : [embeddedConfig.provider];
206
+ try {
207
+ const embeddedConfig = c.get('embeddedConfig') as
208
+ | EmbeddedAppConfig
209
+ | undefined;
210
+
211
+ if (embeddedConfig) {
212
+ const providers = embeddedConfig.auth
213
+ ? (Object.keys(embeddedConfig.auth) as ProviderId[])
214
+ : [embeddedConfig.provider];
215
+
216
+ return c.json({
217
+ providers,
218
+ default: getDefault(
219
+ embeddedConfig.provider,
220
+ embeddedConfig.defaults?.provider,
221
+ undefined,
222
+ ),
223
+ });
224
+ }
225
+
226
+ const projectRoot = c.req.query('project') || process.cwd();
227
+ const cfg = await loadConfig(projectRoot);
228
+
229
+ const authorizedProviders = await getAuthorizedProviders(undefined, cfg);
191
230
 
192
231
  return c.json({
193
- providers,
194
- default: getDefault(
195
- embeddedConfig.provider,
196
- embeddedConfig.defaults?.provider,
197
- undefined,
198
- ),
232
+ providers: authorizedProviders,
233
+ default: cfg.defaults.provider,
199
234
  });
235
+ } catch (error) {
236
+ logger.error('Failed to get providers', error);
237
+ const errorResponse = serializeError(error);
238
+ return c.json(errorResponse, errorResponse.error.status || 500);
200
239
  }
201
-
202
- const projectRoot = c.req.query('project') || process.cwd();
203
- const cfg = await loadConfig(projectRoot);
204
-
205
- const authorizedProviders = await getAuthorizedProviders(undefined, cfg);
206
-
207
- return c.json({
208
- providers: authorizedProviders,
209
- default: cfg.defaults.provider,
210
- });
211
240
  });
212
241
 
213
242
  // Get available models for a provider
214
243
  app.get('/v1/config/providers/:provider/models', async (c) => {
215
- const embeddedConfig = c.get('embeddedConfig') as
216
- | EmbeddedAppConfig
217
- | undefined;
218
- const provider = c.req.param('provider') as ProviderId;
219
-
220
- // Always load file config for fallback auth check
221
- const projectRoot = c.req.query('project') || process.cwd();
222
- const cfg = await loadConfig(projectRoot);
223
-
224
- // Check if provider is authorized (hybrid: embedded OR file-based)
225
- const authorized = await isProviderAuthorizedHybrid(
226
- embeddedConfig,
227
- cfg,
228
- provider,
229
- );
230
-
231
- if (!authorized) {
232
- return c.json({ error: 'Provider not authorized' }, 403);
233
- }
244
+ try {
245
+ const embeddedConfig = c.get('embeddedConfig') as
246
+ | EmbeddedAppConfig
247
+ | undefined;
248
+ const provider = c.req.param('provider') as ProviderId;
249
+
250
+ // Always load file config for fallback auth check
251
+ const projectRoot = c.req.query('project') || process.cwd();
252
+ const cfg = await loadConfig(projectRoot);
253
+
254
+ // Check if provider is authorized (hybrid: embedded OR file-based)
255
+ const authorized = await isProviderAuthorizedHybrid(
256
+ embeddedConfig,
257
+ cfg,
258
+ provider,
259
+ );
260
+
261
+ if (!authorized) {
262
+ logger.warn('Provider not authorized', { provider });
263
+ return c.json({ error: 'Provider not authorized' }, 403);
264
+ }
265
+
266
+ const providerCatalog = catalog[provider];
267
+ if (!providerCatalog) {
268
+ logger.warn('Provider not found in catalog', { provider });
269
+ return c.json({ error: 'Provider not found' }, 404);
270
+ }
234
271
 
235
- const providerCatalog = catalog[provider];
236
- if (!providerCatalog) {
237
- return c.json({ error: 'Provider not found' }, 404);
272
+ return c.json({
273
+ models: providerCatalog.models.map((m) => ({
274
+ id: m.id,
275
+ label: m.label || m.id,
276
+ toolCall: m.toolCall,
277
+ reasoning: m.reasoning,
278
+ })),
279
+ default: getDefault(
280
+ embeddedConfig?.model,
281
+ embeddedConfig?.defaults?.model,
282
+ cfg.defaults.model,
283
+ ),
284
+ });
285
+ } catch (error) {
286
+ logger.error('Failed to get provider models', error);
287
+ const errorResponse = serializeError(error);
288
+ return c.json(errorResponse, errorResponse.error.status || 500);
238
289
  }
239
-
240
- return c.json({
241
- models: providerCatalog.models.map((m) => ({
242
- id: m.id,
243
- label: m.label || m.id,
244
- toolCall: m.toolCall,
245
- reasoning: m.reasoning,
246
- })),
247
- default: getDefault(
248
- embeddedConfig?.model,
249
- embeddedConfig?.defaults?.model,
250
- cfg.defaults.model,
251
- ),
252
- });
253
290
  });
254
291
  }
package/src/routes/git.ts CHANGED
@@ -4,8 +4,11 @@ import { extname, join } from 'node:path';
4
4
  import { readFile } from 'node:fs/promises';
5
5
  import { promisify } from 'node:util';
6
6
  import { z } from 'zod';
7
- import { generateText, resolveModel, type ProviderId } from '@agi-cli/sdk';
8
- import { loadConfig } from '@agi-cli/sdk';
7
+ import { generateText } from 'ai';
8
+ import type { ProviderId } from '@agi-cli/sdk';
9
+ import { loadConfig, getAuth } from '@agi-cli/sdk';
10
+ import { resolveModel } from '../runtime/provider.ts';
11
+ import { getProviderSpoofPrompt } from '../runtime/prompt.ts';
9
12
 
10
13
  const execFileAsync = promisify(execFile);
11
14
 
@@ -233,7 +236,7 @@ function parseGitStatus(
233
236
  return { staged, unstaged, untracked };
234
237
  }
235
238
 
236
- function getStatusFromCode(code: string): GitFile['status'] {
239
+ function _getStatusFromCode(code: string): GitFile['status'] {
237
240
  switch (code) {
238
241
  case 'M':
239
242
  return 'modified';
@@ -678,16 +681,22 @@ export function registerGitRoutes(app: Hono) {
678
681
  // Load config to get provider settings
679
682
  const config = await loadConfig();
680
683
 
681
- // Use a simple model for quick commit message generation
682
- const provider = config.defaults?.provider || 'anthropic';
683
- const model = await resolveModel(
684
- provider as ProviderId,
685
- config.defaults?.model,
686
- undefined,
687
- );
684
+ // Use the default provider and model for quick commit message generation
685
+ const provider = (config.defaults?.provider || 'anthropic') as ProviderId;
686
+ const modelId = config.defaults?.model || 'claude-3-5-sonnet-20241022';
687
+
688
+ // Check if we need OAuth spoof prompt (same as runner)
689
+ const auth = await getAuth(provider, config.projectRoot);
690
+ const needsSpoof = auth?.type === 'oauth';
691
+ const spoofPrompt = needsSpoof
692
+ ? getProviderSpoofPrompt(provider)
693
+ : undefined;
694
+
695
+ // Resolve model with proper authentication (3-level fallback: OAuth, API key, env var)
696
+ const model = await resolveModel(provider, modelId, config);
688
697
 
689
698
  // Generate commit message using AI
690
- const prompt = `Generate a concise, conventional commit message for these git changes.
699
+ const userPrompt = `Generate a concise, conventional commit message for these git changes.
691
700
 
692
701
  Staged files:
693
702
  ${fileList}
@@ -705,12 +714,15 @@ Guidelines:
705
714
 
706
715
  Commit message:`;
707
716
 
717
+ // Use spoof prompt as system if OAuth, otherwise use normal system prompt
718
+ const systemPrompt = spoofPrompt
719
+ ? spoofPrompt
720
+ : 'You are a helpful assistant that generates git commit messages.';
721
+
708
722
  const { text } = await generateText({
709
- provider: provider as ProviderId,
710
- model: model.id,
711
- systemPrompt:
712
- 'You are a helpful assistant that generates git commit messages.',
713
- prompt,
723
+ model,
724
+ system: systemPrompt,
725
+ prompt: userPrompt,
714
726
  maxTokens: 200,
715
727
  });
716
728