@agi-cli/server 0.1.76 → 0.1.77
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/routes/config.ts +134 -38
- package/src/routes/sessions.ts +112 -1
- package/src/runtime/logger.ts +3 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@agi-cli/server",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.77",
|
|
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.77",
|
|
33
|
+
"@agi-cli/database": "0.1.77",
|
|
34
34
|
"drizzle-orm": "^0.44.5",
|
|
35
35
|
"hono": "^4.9.9",
|
|
36
36
|
"zod": "^4.1.8"
|
package/src/routes/config.ts
CHANGED
|
@@ -1,12 +1,18 @@
|
|
|
1
1
|
import type { Hono } from 'hono';
|
|
2
2
|
import { loadConfig } from '@agi-cli/sdk';
|
|
3
|
-
import {
|
|
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
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
const
|
|
101
|
-
const
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
}
|
|
106
|
-
|
|
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
|
|
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
|
-
|
|
187
|
-
|
|
188
|
-
|
|
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
|
}
|
package/src/routes/sessions.ts
CHANGED
|
@@ -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');
|
package/src/runtime/logger.ts
CHANGED
|
@@ -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);
|