@agi-cli/server 0.1.76 → 0.1.78

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.76",
3
+ "version": "0.1.78",
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.76",
33
- "@agi-cli/database": "0.1.76",
32
+ "@agi-cli/sdk": "0.1.78",
33
+ "@agi-cli/database": "0.1.78",
34
34
  "drizzle-orm": "^0.44.5",
35
35
  "hono": "^4.9.9",
36
36
  "zod": "^4.1.8"
@@ -1,12 +1,18 @@
1
1
  import type { Hono } from 'hono';
2
2
  import { loadConfig } from '@agi-cli/sdk';
3
- import { catalog, type ProviderId, isProviderAuthorized } from '@agi-cli/sdk';
3
+ import {
4
+ catalog,
5
+ type ProviderId,
6
+ isProviderAuthorized,
7
+ getGlobalAgentsDir,
8
+ } from '@agi-cli/sdk';
4
9
  import { readdir } from 'node:fs/promises';
5
10
  import { join, basename } from 'node:path';
6
11
  import type { EmbeddedAppConfig } from '../index.ts';
7
12
  import type { AGIConfig } from '@agi-cli/sdk';
8
13
  import { logger } from '../runtime/logger.ts';
9
14
  import { serializeError } from '../runtime/api-error.ts';
15
+ import { loadAgentsConfig } from '../runtime/agent-registry.ts';
10
16
 
11
17
  /**
12
18
  * Check if a provider is authorized in either embedded config or file-based config
@@ -64,6 +70,63 @@ function getDefault<T>(
64
70
  return embeddedValue ?? embeddedDefaultValue ?? fileValue;
65
71
  }
66
72
 
73
+ /**
74
+ * Discover all agents from all sources:
75
+ * - Built-in agents (general, build, plan)
76
+ * - agents.json (global + local)
77
+ * - Agent files in .agi/agents/ (global + local)
78
+ */
79
+ async function discoverAllAgents(projectRoot: string): Promise<string[]> {
80
+ const builtInAgents = ['general', 'build', 'plan'];
81
+ const agentSet = new Set<string>(builtInAgents);
82
+
83
+ // Load agents from agents.json (global + local merged)
84
+ try {
85
+ const agentsJson = await loadAgentsConfig(projectRoot);
86
+ for (const agentName of Object.keys(agentsJson)) {
87
+ if (agentName.trim()) {
88
+ agentSet.add(agentName);
89
+ }
90
+ }
91
+ } catch (err) {
92
+ logger.debug('Failed to load agents.json', err);
93
+ }
94
+
95
+ // Discover custom agent files from local .agi/agents/
96
+ try {
97
+ const localAgentsPath = join(projectRoot, '.agi', 'agents');
98
+ const localFiles = await readdir(localAgentsPath).catch(() => []);
99
+ for (const file of localFiles) {
100
+ if (file.endsWith('.txt') || file.endsWith('.md')) {
101
+ const agentName = file.replace(/\.(txt|md)$/, '');
102
+ if (agentName.trim()) {
103
+ agentSet.add(agentName);
104
+ }
105
+ }
106
+ }
107
+ } catch (err) {
108
+ logger.debug('Failed to read local agents directory', err);
109
+ }
110
+
111
+ // Discover custom agent files from global ~/.config/agi/agents/
112
+ try {
113
+ const globalAgentsPath = getGlobalAgentsDir();
114
+ const globalFiles = await readdir(globalAgentsPath).catch(() => []);
115
+ for (const file of globalFiles) {
116
+ if (file.endsWith('.txt') || file.endsWith('.md')) {
117
+ const agentName = file.replace(/\.(txt|md)$/, '');
118
+ if (agentName.trim()) {
119
+ agentSet.add(agentName);
120
+ }
121
+ }
122
+ }
123
+ } catch (err) {
124
+ logger.debug('Failed to read global agents directory', err);
125
+ }
126
+
127
+ return Array.from(agentSet).sort();
128
+ }
129
+
67
130
  export function registerConfigRoutes(app: Hono) {
68
131
  // Get working directory info
69
132
  app.get('/v1/config/cwd', (c) => {
@@ -93,26 +156,20 @@ export function registerConfigRoutes(app: Hono) {
93
156
  const cfg = await loadConfig(projectRoot);
94
157
 
95
158
  // 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);
159
+ let allAgents: string[];
160
+
161
+ if (embeddedConfig?.agents) {
162
+ // Embedded mode: use embedded agents + file agents
163
+ const embeddedAgents = Object.keys(embeddedConfig.agents);
164
+ const fileAgents = await discoverAllAgents(cfg.projectRoot);
165
+ allAgents = Array.from(
166
+ new Set([...embeddedAgents, ...fileAgents]),
167
+ ).sort();
168
+ } else {
169
+ // File mode: discover all agents
170
+ allAgents = await discoverAllAgents(cfg.projectRoot);
107
171
  }
108
172
 
109
- // Agents: Embedded custom agents + file-based agents
110
- const fileAgents = [...builtInAgents, ...customAgents];
111
- const embeddedAgents = embeddedConfig?.agents
112
- ? Object.keys(embeddedConfig.agents)
113
- : [];
114
- const allAgents = Array.from(new Set([...embeddedAgents, ...fileAgents]));
115
-
116
173
  // Providers: Check both embedded and file-based auth
117
174
  const authorizedProviders = await getAuthorizedProviders(
118
175
  embeddedConfig,
@@ -174,26 +231,12 @@ export function registerConfigRoutes(app: Hono) {
174
231
  const projectRoot = c.req.query('project') || process.cwd();
175
232
  const cfg = await loadConfig(projectRoot);
176
233
 
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', ''));
234
+ const allAgents = await discoverAllAgents(cfg.projectRoot);
185
235
 
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
- }
236
+ return c.json({
237
+ agents: allAgents,
238
+ default: cfg.defaults.agent,
239
+ });
197
240
  } catch (error) {
198
241
  logger.error('Failed to get agents', error);
199
242
  const errorResponse = serializeError(error);
@@ -288,4 +331,57 @@ export function registerConfigRoutes(app: Hono) {
288
331
  return c.json(errorResponse, errorResponse.error.status || 500);
289
332
  }
290
333
  });
334
+
335
+ // Get all models grouped by provider
336
+ app.get('/v1/config/models', async (c) => {
337
+ try {
338
+ const embeddedConfig = c.get('embeddedConfig') as
339
+ | EmbeddedAppConfig
340
+ | undefined;
341
+
342
+ const projectRoot = c.req.query('project') || process.cwd();
343
+ const cfg = await loadConfig(projectRoot);
344
+
345
+ // Get all authorized providers
346
+ const authorizedProviders = await getAuthorizedProviders(
347
+ embeddedConfig,
348
+ cfg,
349
+ );
350
+
351
+ // Build models map
352
+ const modelsMap: Record<
353
+ string,
354
+ {
355
+ label: string;
356
+ models: Array<{
357
+ id: string;
358
+ label: string;
359
+ toolCall?: boolean;
360
+ reasoning?: boolean;
361
+ }>;
362
+ }
363
+ > = {};
364
+
365
+ for (const provider of authorizedProviders) {
366
+ const providerCatalog = catalog[provider];
367
+ if (providerCatalog) {
368
+ modelsMap[provider] = {
369
+ label: providerCatalog.label || provider,
370
+ models: providerCatalog.models.map((m) => ({
371
+ id: m.id,
372
+ label: m.label || m.id,
373
+ toolCall: m.toolCall,
374
+ reasoning: m.reasoning,
375
+ })),
376
+ };
377
+ }
378
+ }
379
+
380
+ return c.json(modelsMap);
381
+ } catch (error) {
382
+ logger.error('Failed to get all models', error);
383
+ const errorResponse = serializeError(error);
384
+ return c.json(errorResponse, errorResponse.error.status || 500);
385
+ }
386
+ });
291
387
  }
@@ -4,7 +4,7 @@ import { getDb } from '@agi-cli/database';
4
4
  import { sessions } from '@agi-cli/database/schema';
5
5
  import { desc, eq } from 'drizzle-orm';
6
6
  import type { ProviderId } from '@agi-cli/sdk';
7
- import { isProviderId } from '@agi-cli/sdk';
7
+ import { isProviderId, catalog } from '@agi-cli/sdk';
8
8
  import { resolveAgentConfig } from '../runtime/agent-registry.ts';
9
9
  import { createSession as createSessionRow } from '../runtime/session-manager.ts';
10
10
  import { serializeError } from '../runtime/api-error.ts';
@@ -80,6 +80,117 @@ export function registerSessionsRoutes(app: Hono) {
80
80
  }
81
81
  });
82
82
 
83
+ // Update session preferences
84
+ app.patch('/v1/sessions/:sessionId', async (c) => {
85
+ try {
86
+ const sessionId = c.req.param('sessionId');
87
+ const projectRoot = c.req.query('project') || process.cwd();
88
+ const cfg = await loadConfig(projectRoot);
89
+ const db = await getDb(cfg.projectRoot);
90
+
91
+ const body = (await c.req.json().catch(() => ({}))) as Record<
92
+ string,
93
+ unknown
94
+ >;
95
+
96
+ // Fetch existing session
97
+ const existingRows = await db
98
+ .select()
99
+ .from(sessions)
100
+ .where(eq(sessions.id, sessionId))
101
+ .limit(1);
102
+
103
+ if (!existingRows.length) {
104
+ return c.json({ error: 'Session not found' }, 404);
105
+ }
106
+
107
+ const existingSession = existingRows[0];
108
+
109
+ // Verify session belongs to current project
110
+ if (existingSession.projectPath !== cfg.projectRoot) {
111
+ return c.json({ error: 'Session not found in this project' }, 404);
112
+ }
113
+
114
+ // Prepare update data
115
+ const updates: {
116
+ agent?: string;
117
+ provider?: string;
118
+ model?: string;
119
+ lastActiveAt?: number;
120
+ } = {
121
+ lastActiveAt: Date.now(),
122
+ };
123
+
124
+ // Validate agent if provided
125
+ if (typeof body.agent === 'string') {
126
+ const agentName = body.agent.trim();
127
+ if (agentName) {
128
+ // Agent validation: check if it exists via resolveAgentConfig
129
+ try {
130
+ await resolveAgentConfig(cfg.projectRoot, agentName);
131
+ updates.agent = agentName;
132
+ } catch (err) {
133
+ logger.warn('Invalid agent provided', { agent: agentName, err });
134
+ return c.json({ error: `Invalid agent: ${agentName}` }, 400);
135
+ }
136
+ }
137
+ }
138
+
139
+ // Validate provider if provided
140
+ if (typeof body.provider === 'string') {
141
+ const providerName = body.provider.trim();
142
+ if (providerName && isProviderId(providerName)) {
143
+ updates.provider = providerName;
144
+ } else if (providerName) {
145
+ return c.json({ error: `Invalid provider: ${providerName}` }, 400);
146
+ }
147
+ }
148
+
149
+ // Validate model if provided (and optionally verify it belongs to provider)
150
+ if (typeof body.model === 'string') {
151
+ const modelName = body.model.trim();
152
+ if (modelName) {
153
+ const targetProvider = (updates.provider ||
154
+ existingSession.provider) as ProviderId;
155
+
156
+ // Check if model exists for the provider
157
+ const providerCatalog = catalog[targetProvider];
158
+ if (providerCatalog) {
159
+ const modelExists = providerCatalog.models.some(
160
+ (m) => m.id === modelName,
161
+ );
162
+ if (!modelExists) {
163
+ return c.json(
164
+ {
165
+ error: `Model "${modelName}" not found for provider "${targetProvider}"`,
166
+ },
167
+ 400,
168
+ );
169
+ }
170
+ }
171
+
172
+ updates.model = modelName;
173
+ }
174
+ }
175
+
176
+ // Perform update
177
+ await db.update(sessions).set(updates).where(eq(sessions.id, sessionId));
178
+
179
+ // Return updated session
180
+ const updatedRows = await db
181
+ .select()
182
+ .from(sessions)
183
+ .where(eq(sessions.id, sessionId))
184
+ .limit(1);
185
+
186
+ return c.json(updatedRows[0]);
187
+ } catch (err) {
188
+ logger.error('Failed to update session', err);
189
+ const errorResponse = serializeError(err);
190
+ return c.json(errorResponse, errorResponse.error.status || 500);
191
+ }
192
+ });
193
+
83
194
  // Abort session stream
84
195
  app.delete('/v1/sessions/:sessionId/abort', async (c) => {
85
196
  const sessionId = c.req.param('sessionId');
@@ -45,9 +45,11 @@ export function debug(message: string, meta?: Record<string, unknown>): void {
45
45
  }
46
46
 
47
47
  /**
48
- * Log informational messages
48
+ * Log informational messages (only when debug or trace mode is enabled)
49
49
  */
50
50
  export function info(message: string, meta?: Record<string, unknown>): void {
51
+ if (!isDebugEnabled() && !isTraceEnabled()) return;
52
+
51
53
  try {
52
54
  if (meta && Object.keys(meta).length > 0) {
53
55
  console.log(`[info] ${message}`, meta);