@agi-cli/server 0.1.55

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.
@@ -0,0 +1,474 @@
1
+ import { providerIds } from '@agi-cli/sdk';
2
+
3
+ export function getOpenAPISpec() {
4
+ const spec = {
5
+ openapi: '3.0.3',
6
+ info: {
7
+ title: 'AGI Server API',
8
+ version: '0.1.0',
9
+ description:
10
+ 'Server-side API for AGI sessions, messages, and streaming events. All AI work runs on the server. Streaming uses SSE.',
11
+ },
12
+ tags: [
13
+ { name: 'sessions' },
14
+ { name: 'messages' },
15
+ { name: 'stream' },
16
+ { name: 'ask' },
17
+ ],
18
+ paths: {
19
+ '/v1/ask': {
20
+ post: {
21
+ tags: ['ask'],
22
+ operationId: 'ask',
23
+ summary: 'Send a prompt using the ask service',
24
+ description:
25
+ 'Streamlined endpoint used by the CLI to send prompts and receive assistant responses. Creates sessions as needed and reuses the last session when requested.',
26
+ parameters: [projectQueryParam()],
27
+ requestBody: {
28
+ required: true,
29
+ content: {
30
+ 'application/json': {
31
+ schema: {
32
+ type: 'object',
33
+ required: ['prompt'],
34
+ properties: {
35
+ prompt: {
36
+ type: 'string',
37
+ description: 'User prompt to send to the assistant.',
38
+ },
39
+ agent: {
40
+ type: 'string',
41
+ description:
42
+ 'Optional agent name to use for this request.',
43
+ },
44
+ provider: {
45
+ $ref: '#/components/schemas/Provider',
46
+ description:
47
+ 'Optional provider override. When omitted the agent and config defaults apply.',
48
+ },
49
+ model: {
50
+ type: 'string',
51
+ description:
52
+ 'Optional model override for the selected provider.',
53
+ },
54
+ sessionId: {
55
+ type: 'string',
56
+ description: 'Send the prompt to a specific session.',
57
+ },
58
+ last: {
59
+ type: 'boolean',
60
+ description:
61
+ 'If true, reuse the most recent session for the project.',
62
+ },
63
+ jsonMode: {
64
+ type: 'boolean',
65
+ description:
66
+ 'Request structured JSON output when supported by the agent.',
67
+ },
68
+ },
69
+ },
70
+ },
71
+ },
72
+ },
73
+ responses: {
74
+ 202: {
75
+ description: 'Accepted',
76
+ content: {
77
+ 'application/json': {
78
+ schema: { $ref: '#/components/schemas/AskResponse' },
79
+ },
80
+ },
81
+ },
82
+ 400: errorResponse(),
83
+ },
84
+ },
85
+ },
86
+ '/v1/sessions': {
87
+ get: {
88
+ tags: ['sessions'],
89
+ operationId: 'listSessions',
90
+ summary: 'List sessions',
91
+ parameters: [projectQueryParam()],
92
+ responses: {
93
+ 200: {
94
+ description: 'OK',
95
+ content: {
96
+ 'application/json': {
97
+ schema: {
98
+ type: 'array',
99
+ items: { $ref: '#/components/schemas/Session' },
100
+ },
101
+ },
102
+ },
103
+ },
104
+ },
105
+ },
106
+ post: {
107
+ tags: ['sessions'],
108
+ operationId: 'createSession',
109
+ summary: 'Create a new session',
110
+ parameters: [projectQueryParam()],
111
+ requestBody: {
112
+ required: false,
113
+ content: {
114
+ 'application/json': {
115
+ schema: {
116
+ type: 'object',
117
+ properties: {
118
+ title: { type: 'string', nullable: true },
119
+ agent: { type: 'string' },
120
+ provider: { $ref: '#/components/schemas/Provider' },
121
+ model: { type: 'string' },
122
+ },
123
+ },
124
+ },
125
+ },
126
+ },
127
+ responses: {
128
+ 201: {
129
+ description: 'Created',
130
+ content: {
131
+ 'application/json': {
132
+ schema: { $ref: '#/components/schemas/Session' },
133
+ },
134
+ },
135
+ },
136
+ 400: errorResponse(),
137
+ },
138
+ },
139
+ },
140
+ '/v1/sessions/{id}/messages': {
141
+ get: {
142
+ tags: ['messages'],
143
+ operationId: 'listMessages',
144
+ summary: 'List messages for a session',
145
+ parameters: [projectQueryParam(), sessionIdParam(), withoutParam()],
146
+ responses: {
147
+ 200: {
148
+ description: 'OK',
149
+ content: {
150
+ 'application/json': {
151
+ schema: {
152
+ type: 'array',
153
+ items: {
154
+ allOf: [
155
+ { $ref: '#/components/schemas/Message' },
156
+ {
157
+ type: 'object',
158
+ properties: {
159
+ parts: {
160
+ type: 'array',
161
+ items: {
162
+ $ref: '#/components/schemas/MessagePart',
163
+ },
164
+ },
165
+ },
166
+ required: [],
167
+ },
168
+ ],
169
+ },
170
+ },
171
+ },
172
+ },
173
+ },
174
+ },
175
+ },
176
+ post: {
177
+ tags: ['messages'],
178
+ operationId: 'createMessage',
179
+ summary: 'Send a user message and enqueue assistant run',
180
+ parameters: [projectQueryParam(), sessionIdParam()],
181
+ requestBody: {
182
+ required: true,
183
+ content: {
184
+ 'application/json': {
185
+ schema: {
186
+ type: 'object',
187
+ required: ['content'],
188
+ properties: {
189
+ content: { type: 'string' },
190
+ agent: {
191
+ type: 'string',
192
+ description: 'Agent name. Defaults to config if omitted.',
193
+ },
194
+ provider: { $ref: '#/components/schemas/Provider' },
195
+ model: { type: 'string' },
196
+ },
197
+ },
198
+ },
199
+ },
200
+ },
201
+ responses: {
202
+ 202: {
203
+ description: 'Accepted',
204
+ content: {
205
+ 'application/json': {
206
+ schema: {
207
+ type: 'object',
208
+ properties: { messageId: { type: 'string' } },
209
+ required: ['messageId'],
210
+ },
211
+ },
212
+ },
213
+ },
214
+ 400: errorResponse(),
215
+ },
216
+ },
217
+ },
218
+ '/v1/sessions/{id}/stream': {
219
+ get: {
220
+ tags: ['stream'],
221
+ operationId: 'subscribeSessionStream',
222
+ summary: 'Subscribe to session event stream (SSE)',
223
+ parameters: [projectQueryParam(), sessionIdParam()],
224
+ responses: {
225
+ 200: {
226
+ description: 'text/event-stream',
227
+ content: {
228
+ 'text/event-stream': {
229
+ schema: {
230
+ type: 'string',
231
+ description:
232
+ 'SSE event stream. Events include session.created, message.created, message.part.delta, tool.call, tool.delta, tool.result, message.completed, error.',
233
+ },
234
+ },
235
+ },
236
+ },
237
+ },
238
+ },
239
+ },
240
+ },
241
+ components: {
242
+ schemas: {
243
+ Provider: {
244
+ type: 'string',
245
+ enum: providerIds,
246
+ },
247
+ AskResponse: {
248
+ type: 'object',
249
+ properties: {
250
+ sessionId: { type: 'string' },
251
+ header: { $ref: '#/components/schemas/AskResponseHeader' },
252
+ provider: { $ref: '#/components/schemas/Provider' },
253
+ model: { type: 'string' },
254
+ agent: { type: 'string' },
255
+ assistantMessageId: { type: 'string' },
256
+ message: {
257
+ $ref: '#/components/schemas/AskResponseMessage',
258
+ nullable: true,
259
+ description:
260
+ 'Present when the request created a new session or reused the last session for the project.',
261
+ },
262
+ },
263
+ required: [
264
+ 'sessionId',
265
+ 'header',
266
+ 'provider',
267
+ 'model',
268
+ 'agent',
269
+ 'assistantMessageId',
270
+ ],
271
+ },
272
+ AskResponseHeader: {
273
+ type: 'object',
274
+ properties: {
275
+ sessionId: { type: 'string' },
276
+ agent: { type: 'string', nullable: true },
277
+ provider: {
278
+ $ref: '#/components/schemas/Provider',
279
+ nullable: true,
280
+ },
281
+ model: { type: 'string', nullable: true },
282
+ },
283
+ required: ['sessionId'],
284
+ },
285
+ AskResponseMessage: {
286
+ type: 'object',
287
+ properties: {
288
+ kind: { type: 'string', enum: ['created', 'last'] },
289
+ sessionId: { type: 'string' },
290
+ },
291
+ required: ['kind', 'sessionId'],
292
+ },
293
+ Session: {
294
+ type: 'object',
295
+ properties: {
296
+ id: { type: 'string' },
297
+ title: { type: 'string', nullable: true },
298
+ agent: { type: 'string' },
299
+ provider: { $ref: '#/components/schemas/Provider' },
300
+ model: { type: 'string' },
301
+ projectPath: { type: 'string' },
302
+ createdAt: { type: 'integer', format: 'int64' },
303
+ lastActiveAt: { type: 'integer', format: 'int64', nullable: true },
304
+ totalInputTokens: { type: 'integer', nullable: true },
305
+ totalOutputTokens: { type: 'integer', nullable: true },
306
+ totalToolTimeMs: { type: 'integer', nullable: true },
307
+ toolCounts: {
308
+ type: 'object',
309
+ additionalProperties: { type: 'integer' },
310
+ nullable: true,
311
+ },
312
+ },
313
+ required: [
314
+ 'id',
315
+ 'agent',
316
+ 'provider',
317
+ 'model',
318
+ 'projectPath',
319
+ 'createdAt',
320
+ ],
321
+ },
322
+ Message: {
323
+ type: 'object',
324
+ properties: {
325
+ id: { type: 'string' },
326
+ sessionId: { type: 'string' },
327
+ role: {
328
+ type: 'string',
329
+ enum: ['system', 'user', 'assistant', 'tool'],
330
+ },
331
+ status: { type: 'string', enum: ['pending', 'complete', 'error'] },
332
+ agent: { type: 'string' },
333
+ provider: { $ref: '#/components/schemas/Provider' },
334
+ model: { type: 'string' },
335
+ createdAt: { type: 'integer', format: 'int64' },
336
+ completedAt: { type: 'integer', format: 'int64', nullable: true },
337
+ latencyMs: { type: 'integer', nullable: true },
338
+ promptTokens: { type: 'integer', nullable: true },
339
+ completionTokens: { type: 'integer', nullable: true },
340
+ totalTokens: { type: 'integer', nullable: true },
341
+ error: { type: 'string', nullable: true },
342
+ },
343
+ required: [
344
+ 'id',
345
+ 'sessionId',
346
+ 'role',
347
+ 'status',
348
+ 'agent',
349
+ 'provider',
350
+ 'model',
351
+ 'createdAt',
352
+ ],
353
+ },
354
+ MessagePart: {
355
+ type: 'object',
356
+ properties: {
357
+ id: { type: 'string' },
358
+ messageId: { type: 'string' },
359
+ index: { type: 'integer', format: 'int64' },
360
+ type: {
361
+ type: 'string',
362
+ enum: ['text', 'tool_call', 'tool_result', 'image', 'error'],
363
+ },
364
+ content: {
365
+ type: 'string',
366
+ description:
367
+ 'JSON-encoded content. For text: {"text": string}. For tool_call: {"name": string, "args": object}. For tool_result: {"name": string, "result"?: any, "artifact"?: Artifact}.',
368
+ },
369
+ agent: { type: 'string' },
370
+ provider: { $ref: '#/components/schemas/Provider' },
371
+ model: { type: 'string' },
372
+ startedAt: { type: 'integer', format: 'int64', nullable: true },
373
+ completedAt: { type: 'integer', format: 'int64', nullable: true },
374
+ toolName: { type: 'string', nullable: true },
375
+ toolCallId: { type: 'string', nullable: true },
376
+ toolDurationMs: { type: 'integer', nullable: true },
377
+ },
378
+ required: [
379
+ 'id',
380
+ 'messageId',
381
+ 'index',
382
+ 'type',
383
+ 'content',
384
+ 'agent',
385
+ 'provider',
386
+ 'model',
387
+ ],
388
+ },
389
+ Artifact: {
390
+ oneOf: [
391
+ { $ref: '#/components/schemas/FileDiffArtifact' },
392
+ { $ref: '#/components/schemas/FileArtifact' },
393
+ ],
394
+ },
395
+ FileDiffArtifact: {
396
+ type: 'object',
397
+ properties: {
398
+ kind: { type: 'string', enum: ['file_diff'] },
399
+ patchFormat: { type: 'string', enum: ['unified'] },
400
+ patch: { type: 'string' },
401
+ summary: {
402
+ type: 'object',
403
+ properties: {
404
+ files: { type: 'integer' },
405
+ additions: { type: 'integer' },
406
+ deletions: { type: 'integer' },
407
+ },
408
+ additionalProperties: false,
409
+ },
410
+ },
411
+ required: ['kind', 'patchFormat', 'patch'],
412
+ },
413
+ FileArtifact: {
414
+ type: 'object',
415
+ properties: {
416
+ kind: { type: 'string', enum: ['file'] },
417
+ path: { type: 'string' },
418
+ mime: { type: 'string' },
419
+ size: { type: 'integer' },
420
+ sha256: { type: 'string' },
421
+ },
422
+ required: ['kind', 'path'],
423
+ },
424
+ },
425
+ },
426
+ } as const;
427
+ return spec;
428
+ }
429
+
430
+ function projectQueryParam() {
431
+ return {
432
+ in: 'query',
433
+ name: 'project',
434
+ required: false,
435
+ schema: { type: 'string' },
436
+ description:
437
+ 'Project root override (defaults to current working directory).',
438
+ } as const;
439
+ }
440
+
441
+ function sessionIdParam() {
442
+ return {
443
+ in: 'path',
444
+ name: 'id',
445
+ required: true,
446
+ schema: { type: 'string' },
447
+ } as const;
448
+ }
449
+
450
+ function withoutParam() {
451
+ return {
452
+ in: 'query',
453
+ name: 'without',
454
+ required: false,
455
+ schema: { type: 'string', enum: ['parts'] },
456
+ description:
457
+ 'Exclude parts from the response. By default, parts are included.',
458
+ } as const;
459
+ }
460
+
461
+ function errorResponse() {
462
+ return {
463
+ description: 'Bad Request',
464
+ content: {
465
+ 'application/json': {
466
+ schema: {
467
+ type: 'object',
468
+ properties: { error: { type: 'string' } },
469
+ required: ['error'],
470
+ },
471
+ },
472
+ },
473
+ } as const;
474
+ }
@@ -0,0 +1,59 @@
1
+ import type { Hono } from 'hono';
2
+ import type {
3
+ AskServerRequest,
4
+ InjectableConfig,
5
+ InjectableCredentials,
6
+ } from '../runtime/ask-service.ts';
7
+ import { AskServiceError, handleAskRequest } from '../runtime/ask-service.ts';
8
+
9
+ export function registerAskRoutes(app: Hono) {
10
+ app.post('/v1/ask', async (c) => {
11
+ const projectRoot = c.req.query('project') || process.cwd();
12
+ const body = (await c.req.json().catch(() => ({}))) as Record<
13
+ string,
14
+ unknown
15
+ >;
16
+ const prompt = typeof body.prompt === 'string' ? body.prompt : '';
17
+ if (!prompt.trim().length) {
18
+ return c.json({ error: 'Prompt is required.' }, 400);
19
+ }
20
+
21
+ const request: AskServerRequest = {
22
+ projectRoot,
23
+ prompt,
24
+ agent: typeof body.agent === 'string' ? body.agent : undefined,
25
+ provider: typeof body.provider === 'string' ? body.provider : undefined,
26
+ model: typeof body.model === 'string' ? body.model : undefined,
27
+ sessionId:
28
+ typeof body.sessionId === 'string' ? body.sessionId : undefined,
29
+ last: Boolean(body.last),
30
+ jsonMode: Boolean(body.jsonMode),
31
+ skipFileConfig:
32
+ typeof body.skipFileConfig === 'boolean'
33
+ ? body.skipFileConfig
34
+ : undefined,
35
+ config:
36
+ body.config && typeof body.config === 'object'
37
+ ? (body.config as InjectableConfig)
38
+ : undefined,
39
+ credentials:
40
+ body.credentials && typeof body.credentials === 'object'
41
+ ? (body.credentials as InjectableCredentials)
42
+ : undefined,
43
+ agentPrompt:
44
+ typeof body.agentPrompt === 'string' ? body.agentPrompt : undefined,
45
+ tools: Array.isArray(body.tools) ? body.tools : undefined,
46
+ };
47
+
48
+ try {
49
+ const response = await handleAskRequest(request);
50
+ return c.json(response, 202);
51
+ } catch (err) {
52
+ if (err instanceof AskServiceError) {
53
+ return c.json({ error: err.message }, err.status as never);
54
+ }
55
+ const message = err instanceof Error ? err.message : String(err);
56
+ return c.json({ error: message }, 400);
57
+ }
58
+ });
59
+ }
@@ -0,0 +1,124 @@
1
+ import type { Hono } from 'hono';
2
+ import { loadConfig } from '@agi-cli/sdk';
3
+ import { catalog, type ProviderId, isProviderAuthorized } from '@agi-cli/sdk';
4
+ import { readdir } from 'node:fs/promises';
5
+ import { join, basename } from 'node:path';
6
+
7
+ export function registerConfigRoutes(app: Hono) {
8
+ // Get working directory info
9
+ app.get('/v1/config/cwd', (c) => {
10
+ const cwd = process.cwd();
11
+ const dirName = basename(cwd);
12
+ return c.json({
13
+ cwd,
14
+ dirName,
15
+ });
16
+ });
17
+
18
+ // Get full config (agents, providers, models, defaults)
19
+ app.get('/v1/config', async (c) => {
20
+ const projectRoot = c.req.query('project') || process.cwd();
21
+ const cfg = await loadConfig(projectRoot);
22
+
23
+ const builtInAgents = ['general', 'build', 'plan'];
24
+ let customAgents: string[] = [];
25
+
26
+ try {
27
+ const customAgentsPath = join(cfg.projectRoot, '.agi', 'agents');
28
+ const files = await readdir(customAgentsPath).catch(() => []);
29
+ customAgents = files
30
+ .filter((f) => f.endsWith('.txt'))
31
+ .map((f) => f.replace('.txt', ''));
32
+ } catch {}
33
+
34
+ const allProviders = Object.keys(catalog) as ProviderId[];
35
+ const authorizedProviders: ProviderId[] = [];
36
+
37
+ for (const provider of allProviders) {
38
+ const authorized = await isProviderAuthorized(cfg, provider);
39
+ if (authorized) {
40
+ authorizedProviders.push(provider);
41
+ }
42
+ }
43
+
44
+ return c.json({
45
+ agents: [...builtInAgents, ...customAgents],
46
+ providers: authorizedProviders,
47
+ defaults: cfg.defaults,
48
+ });
49
+ });
50
+
51
+ // Get available agents
52
+ app.get('/v1/config/agents', async (c) => {
53
+ const projectRoot = c.req.query('project') || process.cwd();
54
+ const cfg = await loadConfig(projectRoot);
55
+
56
+ const builtInAgents = ['general', 'build', 'plan'];
57
+
58
+ try {
59
+ const customAgentsPath = join(cfg.projectRoot, '.agi', 'agents');
60
+ const files = await readdir(customAgentsPath).catch(() => []);
61
+ const customAgents = files
62
+ .filter((f) => f.endsWith('.txt'))
63
+ .map((f) => f.replace('.txt', ''));
64
+
65
+ return c.json({
66
+ agents: [...builtInAgents, ...customAgents],
67
+ default: cfg.defaults.agent,
68
+ });
69
+ } catch {
70
+ return c.json({
71
+ agents: builtInAgents,
72
+ default: cfg.defaults.agent,
73
+ });
74
+ }
75
+ });
76
+
77
+ // Get available providers (only authorized ones)
78
+ app.get('/v1/config/providers', async (c) => {
79
+ const projectRoot = c.req.query('project') || process.cwd();
80
+ const cfg = await loadConfig(projectRoot);
81
+
82
+ const allProviders = Object.keys(catalog) as ProviderId[];
83
+ const authorizedProviders: ProviderId[] = [];
84
+
85
+ for (const provider of allProviders) {
86
+ const authorized = await isProviderAuthorized(cfg, provider);
87
+ if (authorized) {
88
+ authorizedProviders.push(provider);
89
+ }
90
+ }
91
+
92
+ return c.json({
93
+ providers: authorizedProviders,
94
+ default: cfg.defaults.provider,
95
+ });
96
+ });
97
+
98
+ // Get available models for a provider
99
+ app.get('/v1/config/providers/:provider/models', async (c) => {
100
+ const projectRoot = c.req.query('project') || process.cwd();
101
+ const cfg = await loadConfig(projectRoot);
102
+ const provider = c.req.param('provider') as ProviderId;
103
+
104
+ const authorized = await isProviderAuthorized(cfg, provider);
105
+ if (!authorized) {
106
+ return c.json({ error: 'Provider not authorized' }, 403);
107
+ }
108
+
109
+ const providerCatalog = catalog[provider];
110
+ if (!providerCatalog) {
111
+ return c.json({ error: 'Provider not found' }, 404);
112
+ }
113
+
114
+ return c.json({
115
+ models: providerCatalog.models.map((m) => ({
116
+ id: m.id,
117
+ label: m.label || m.id,
118
+ toolCall: m.toolCall,
119
+ reasoning: m.reasoning,
120
+ })),
121
+ default: cfg.defaults.model,
122
+ });
123
+ });
124
+ }