@agi-cli/server 0.1.89 → 0.1.90

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.89",
3
+ "version": "0.1.90",
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.89",
33
- "@agi-cli/database": "0.1.89",
32
+ "@agi-cli/sdk": "0.1.90",
33
+ "@agi-cli/database": "0.1.90",
34
34
  "drizzle-orm": "^0.44.5",
35
35
  "hono": "^4.9.9",
36
36
  "zod": "^4.1.8"
package/src/index.ts CHANGED
@@ -1,6 +1,8 @@
1
1
  import { Hono } from 'hono';
2
2
  import { cors } from 'hono/cors';
3
3
  import type { ProviderId, AuthInfo } from '@agi-cli/sdk';
4
+ import { TerminalManager } from '@agi-cli/sdk';
5
+ import { setTerminalManager } from '@agi-cli/sdk';
4
6
  import { registerRootRoutes } from './routes/root.ts';
5
7
  import { registerOpenApiRoute } from './routes/openapi.ts';
6
8
  import { registerSessionsRoutes } from './routes/sessions.ts';
@@ -10,8 +12,12 @@ import { registerAskRoutes } from './routes/ask.ts';
10
12
  import { registerConfigRoutes } from './routes/config/index.ts';
11
13
  import { registerFilesRoutes } from './routes/files.ts';
12
14
  import { registerGitRoutes } from './routes/git/index.ts';
15
+ import { registerTerminalsRoutes } from './routes/terminals.ts';
13
16
  import type { AgentConfigEntry } from './runtime/agent-registry.ts';
14
17
 
18
+ const globalTerminalManager = new TerminalManager();
19
+ setTerminalManager(globalTerminalManager);
20
+
15
21
  function initApp() {
16
22
  const app = new Hono();
17
23
 
@@ -55,6 +61,7 @@ function initApp() {
55
61
  registerConfigRoutes(app);
56
62
  registerFilesRoutes(app);
57
63
  registerGitRoutes(app);
64
+ registerTerminalsRoutes(app, globalTerminalManager);
58
65
 
59
66
  return app;
60
67
  }
@@ -119,6 +126,7 @@ export function createStandaloneApp(_config?: StandaloneAppConfig) {
119
126
  registerConfigRoutes(honoApp);
120
127
  registerFilesRoutes(honoApp);
121
128
  registerGitRoutes(honoApp);
129
+ registerTerminalsRoutes(honoApp, globalTerminalManager);
122
130
 
123
131
  return honoApp;
124
132
  }
@@ -211,6 +219,7 @@ export function createEmbeddedApp(config: EmbeddedAppConfig = {}) {
211
219
  registerConfigRoutes(honoApp);
212
220
  registerFilesRoutes(honoApp);
213
221
  registerGitRoutes(honoApp);
222
+ registerTerminalsRoutes(honoApp, globalTerminalManager);
214
223
 
215
224
  return honoApp;
216
225
  }
@@ -0,0 +1,226 @@
1
+ export const terminalsPath = {
2
+ '/v1/terminals': {
3
+ get: {
4
+ operationId: 'getTerminals',
5
+ summary: 'List all terminals',
6
+ description: 'Get a list of all active terminal sessions',
7
+ responses: {
8
+ '200': {
9
+ description: 'List of terminals',
10
+ content: {
11
+ 'application/json': {
12
+ schema: {
13
+ type: 'object',
14
+ properties: {
15
+ terminals: {
16
+ type: 'array',
17
+ items: {
18
+ $ref: '#/components/schemas/Terminal',
19
+ },
20
+ },
21
+ count: {
22
+ type: 'integer',
23
+ },
24
+ },
25
+ },
26
+ },
27
+ },
28
+ },
29
+ },
30
+ },
31
+ post: {
32
+ operationId: 'postTerminals',
33
+ summary: 'Create a new terminal',
34
+ description: 'Spawn a new terminal process',
35
+ requestBody: {
36
+ required: true,
37
+ content: {
38
+ 'application/json': {
39
+ schema: {
40
+ type: 'object',
41
+ required: ['command', 'purpose'],
42
+ properties: {
43
+ command: {
44
+ type: 'string',
45
+ description: 'Command to execute',
46
+ },
47
+ args: {
48
+ type: 'array',
49
+ items: { type: 'string' },
50
+ description: 'Command arguments',
51
+ },
52
+ purpose: {
53
+ type: 'string',
54
+ description: 'Description of terminal purpose',
55
+ },
56
+ cwd: {
57
+ type: 'string',
58
+ description: 'Working directory',
59
+ },
60
+ title: {
61
+ type: 'string',
62
+ description: 'Terminal title',
63
+ },
64
+ },
65
+ },
66
+ },
67
+ },
68
+ },
69
+ responses: {
70
+ '200': {
71
+ description: 'Terminal created',
72
+ content: {
73
+ 'application/json': {
74
+ schema: {
75
+ type: 'object',
76
+ properties: {
77
+ terminalId: { type: 'string' },
78
+ pid: { type: 'integer' },
79
+ purpose: { type: 'string' },
80
+ command: { type: 'string' },
81
+ },
82
+ },
83
+ },
84
+ },
85
+ },
86
+ },
87
+ },
88
+ },
89
+ '/v1/terminals/{id}': {
90
+ get: {
91
+ operationId: 'getTerminalsById',
92
+ summary: 'Get terminal details',
93
+ description: 'Get information about a specific terminal',
94
+ parameters: [
95
+ {
96
+ name: 'id',
97
+ in: 'path',
98
+ required: true,
99
+ schema: { type: 'string' },
100
+ },
101
+ ],
102
+ responses: {
103
+ '200': {
104
+ description: 'Terminal details',
105
+ content: {
106
+ 'application/json': {
107
+ schema: {
108
+ type: 'object',
109
+ properties: {
110
+ terminal: {
111
+ $ref: '#/components/schemas/Terminal',
112
+ },
113
+ },
114
+ },
115
+ },
116
+ },
117
+ },
118
+ '404': {
119
+ description: 'Terminal not found',
120
+ },
121
+ },
122
+ },
123
+ delete: {
124
+ operationId: 'deleteTerminalsById',
125
+ summary: 'Kill terminal',
126
+ description: 'Terminate a running terminal process',
127
+ parameters: [
128
+ {
129
+ name: 'id',
130
+ in: 'path',
131
+ required: true,
132
+ schema: { type: 'string' },
133
+ },
134
+ ],
135
+ responses: {
136
+ '200': {
137
+ description: 'Terminal killed',
138
+ content: {
139
+ 'application/json': {
140
+ schema: {
141
+ type: 'object',
142
+ properties: {
143
+ success: { type: 'boolean' },
144
+ },
145
+ },
146
+ },
147
+ },
148
+ },
149
+ },
150
+ },
151
+ },
152
+ '/v1/terminals/{id}/output': {
153
+ get: {
154
+ operationId: 'getTerminalsByIdOutput',
155
+ summary: 'Stream terminal output',
156
+ description: 'Get real-time terminal output via SSE',
157
+ parameters: [
158
+ {
159
+ name: 'id',
160
+ in: 'path',
161
+ required: true,
162
+ schema: { type: 'string' },
163
+ },
164
+ ],
165
+ responses: {
166
+ '200': {
167
+ description: 'SSE stream of terminal output',
168
+ content: {
169
+ 'text/event-stream': {
170
+ schema: {
171
+ type: 'string',
172
+ },
173
+ },
174
+ },
175
+ },
176
+ },
177
+ },
178
+ },
179
+ '/v1/terminals/{id}/input': {
180
+ post: {
181
+ operationId: 'postTerminalsByIdInput',
182
+ summary: 'Send input to terminal',
183
+ description: 'Write data to terminal stdin',
184
+ parameters: [
185
+ {
186
+ name: 'id',
187
+ in: 'path',
188
+ required: true,
189
+ schema: { type: 'string' },
190
+ },
191
+ ],
192
+ requestBody: {
193
+ required: true,
194
+ content: {
195
+ 'application/json': {
196
+ schema: {
197
+ type: 'object',
198
+ required: ['input'],
199
+ properties: {
200
+ input: {
201
+ type: 'string',
202
+ description: 'Input to send to terminal',
203
+ },
204
+ },
205
+ },
206
+ },
207
+ },
208
+ },
209
+ responses: {
210
+ '200': {
211
+ description: 'Input sent',
212
+ content: {
213
+ 'application/json': {
214
+ schema: {
215
+ type: 'object',
216
+ properties: {
217
+ success: { type: 'boolean' },
218
+ },
219
+ },
220
+ },
221
+ },
222
+ },
223
+ },
224
+ },
225
+ },
226
+ };
@@ -290,4 +290,30 @@ export const schemas = {
290
290
  },
291
291
  required: ['hash', 'message', 'filesChanged', 'insertions', 'deletions'],
292
292
  },
293
+ Terminal: {
294
+ type: 'object',
295
+ properties: {
296
+ id: { type: 'string' },
297
+ pid: { type: 'integer' },
298
+ command: { type: 'string' },
299
+ args: {
300
+ type: 'array',
301
+ items: { type: 'string' },
302
+ },
303
+ cwd: { type: 'string' },
304
+ purpose: { type: 'string' },
305
+ createdBy: {
306
+ type: 'string',
307
+ enum: ['user', 'llm'],
308
+ },
309
+ title: { type: 'string' },
310
+ status: {
311
+ type: 'string',
312
+ enum: ['running', 'exited'],
313
+ },
314
+ exitCode: { type: 'integer' },
315
+ createdAt: { type: 'string', format: 'date-time' },
316
+ uptime: { type: 'integer' },
317
+ },
318
+ },
293
319
  } as const;
@@ -7,6 +7,8 @@ import { sessionsPaths } from './paths/sessions';
7
7
  import { streamPaths } from './paths/stream';
8
8
  import { schemas } from './schemas';
9
9
 
10
+ import { terminalsPath } from './paths/terminals';
11
+
10
12
  export function getOpenAPISpec() {
11
13
  const spec = {
12
14
  openapi: '3.0.3',
@@ -24,6 +26,7 @@ export function getOpenAPISpec() {
24
26
  { name: 'config' },
25
27
  { name: 'files' },
26
28
  { name: 'git' },
29
+ { name: 'terminals' },
27
30
  ],
28
31
  paths: {
29
32
  ...askPaths,
@@ -33,6 +36,7 @@ export function getOpenAPISpec() {
33
36
  ...configPaths,
34
37
  ...filesPaths,
35
38
  ...gitPaths,
39
+ ...terminalsPath,
36
40
  },
37
41
  components: {
38
42
  schemas,
package/src/presets.ts CHANGED
@@ -19,6 +19,7 @@ export const BUILTIN_AGENTS = {
19
19
  'bash',
20
20
  'update_plan',
21
21
  'grep',
22
+ 'terminal',
22
23
  'git_status',
23
24
  'git_diff',
24
25
  'ripgrep',
@@ -50,6 +51,7 @@ export const BUILTIN_AGENTS = {
50
51
  'tree',
51
52
  'bash',
52
53
  'ripgrep',
54
+ 'terminal',
53
55
  'websearch',
54
56
  'update_plan',
55
57
  'progress_update',
@@ -64,6 +66,7 @@ export const BUILTIN_TOOLS = [
64
66
  'ls',
65
67
  'tree',
66
68
  'bash',
69
+ 'terminal',
67
70
  'grep',
68
71
  'ripgrep',
69
72
  'git_status',
@@ -0,0 +1,201 @@
1
+ import type { Hono } from 'hono';
2
+ import { streamSSE } from 'hono/streaming';
3
+ import type { TerminalManager } from '@agi-cli/sdk';
4
+
5
+ export function registerTerminalsRoutes(
6
+ app: Hono,
7
+ terminalManager: TerminalManager,
8
+ ) {
9
+ app.get('/v1/terminals', async (c) => {
10
+ const terminals = terminalManager.list();
11
+ return c.json({
12
+ terminals: terminals.map((t) => t.toJSON()),
13
+ count: terminals.length,
14
+ });
15
+ });
16
+
17
+ app.post('/v1/terminals', async (c) => {
18
+ try {
19
+ console.log('[API] POST /v1/terminals called');
20
+ const body = await c.req.json();
21
+ console.log('[API] Request body:', body);
22
+ const { command, args, purpose, cwd, title } = body;
23
+
24
+ if (!command || !purpose) {
25
+ return c.json({ error: 'command and purpose are required' }, 400);
26
+ }
27
+
28
+ let resolvedCommand = command;
29
+ if (command === 'bash' || command === 'sh' || command === 'shell') {
30
+ resolvedCommand = process.env.SHELL || '/bin/bash';
31
+ }
32
+ const resolvedCwd = cwd || process.cwd();
33
+
34
+ console.log('[API] Creating terminal with:', {
35
+ command: resolvedCommand,
36
+ args,
37
+ purpose,
38
+ cwd: resolvedCwd,
39
+ });
40
+
41
+ const terminal = terminalManager.create({
42
+ command: resolvedCommand,
43
+ args: args || [],
44
+ purpose,
45
+ cwd: resolvedCwd,
46
+ createdBy: 'user',
47
+ title,
48
+ });
49
+
50
+ console.log('[API] Terminal created successfully:', terminal.id);
51
+
52
+ return c.json({
53
+ terminalId: terminal.id,
54
+ pid: terminal.pid,
55
+ purpose: terminal.purpose,
56
+ command: terminal.command,
57
+ });
58
+ } catch (error) {
59
+ console.error('[API] Error creating terminal:', error);
60
+ console.error(
61
+ '[API] Error stack:',
62
+ error instanceof Error ? error.stack : 'No stack',
63
+ );
64
+ const message = error instanceof Error ? error.message : String(error);
65
+ return c.json({ error: message }, 500);
66
+ }
67
+ });
68
+
69
+ app.get('/v1/terminals/:id', async (c) => {
70
+ const id = c.req.param('id');
71
+ const terminal = terminalManager.get(id);
72
+
73
+ if (!terminal) {
74
+ return c.json({ error: 'Terminal not found' }, 404);
75
+ }
76
+
77
+ return c.json({ terminal: terminal.toJSON() });
78
+ });
79
+
80
+ app.get('/v1/terminals/:id/output', async (c) => {
81
+ const id = c.req.param('id');
82
+ console.log('[SSE] Client connecting to terminal:', id);
83
+ const terminal = terminalManager.get(id);
84
+
85
+ if (!terminal) {
86
+ console.error('[SSE] Terminal not found:', id);
87
+ return c.json({ error: 'Terminal not found' }, 404);
88
+ }
89
+
90
+ return streamSSE(c, async (stream) => {
91
+ console.log('[SSE] Stream started for terminal:', id);
92
+ // Send historical buffer first (unless skipHistory is set)
93
+ const skipHistory = c.req.query('skipHistory') === 'true';
94
+ if (!skipHistory) {
95
+ const history = terminal.read();
96
+ console.log('[SSE] Sending history, lines:', history.length);
97
+ for (const line of history) {
98
+ await stream.write(
99
+ `data: ${JSON.stringify({ type: 'data', line })}\n\n`,
100
+ );
101
+ }
102
+ }
103
+
104
+ const sendEvent = async (payload: Record<string, unknown>) => {
105
+ try {
106
+ await stream.write(`data: ${JSON.stringify(payload)}\n\n`);
107
+ } catch (error) {
108
+ console.error('[SSE] Error writing event:', error);
109
+ }
110
+ };
111
+
112
+ const onData = (line: string) => {
113
+ void sendEvent({ type: 'data', line });
114
+ };
115
+
116
+ let resolveStream: (() => void) | null = null;
117
+ let finished = false;
118
+
119
+ function cleanup() {
120
+ terminal.removeDataListener(onData);
121
+ terminal.removeExitListener(onExit);
122
+ c.req.raw.signal.removeEventListener('abort', onAbort);
123
+ }
124
+
125
+ function finish() {
126
+ if (finished) {
127
+ return;
128
+ }
129
+ finished = true;
130
+ cleanup();
131
+ resolveStream?.();
132
+ }
133
+
134
+ async function onExit(exitCode: number) {
135
+ try {
136
+ await sendEvent({ type: 'exit', exitCode });
137
+ } finally {
138
+ stream.close();
139
+ finish();
140
+ }
141
+ }
142
+
143
+ function onAbort() {
144
+ console.log('[SSE] Client disconnected:', terminal.id);
145
+ stream.close();
146
+ finish();
147
+ }
148
+
149
+ terminal.onData(onData);
150
+ terminal.onExit(onExit);
151
+
152
+ c.req.raw.signal.addEventListener('abort', onAbort, { once: true });
153
+
154
+ const waitForClose = new Promise<void>((resolve) => {
155
+ resolveStream = resolve;
156
+ });
157
+
158
+ if (terminal.status === 'exited') {
159
+ void onExit(terminal.exitCode ?? 0);
160
+ }
161
+
162
+ await waitForClose;
163
+ });
164
+ });
165
+
166
+ app.post('/v1/terminals/:id/input', async (c) => {
167
+ const id = c.req.param('id');
168
+ const terminal = terminalManager.get(id);
169
+
170
+ if (!terminal) {
171
+ return c.json({ error: 'Terminal not found' }, 404);
172
+ }
173
+
174
+ try {
175
+ const body = await c.req.json();
176
+ const { input } = body;
177
+
178
+ if (!input) {
179
+ return c.json({ error: 'input is required' }, 400);
180
+ }
181
+
182
+ terminal.write(input);
183
+ return c.json({ success: true });
184
+ } catch (error) {
185
+ const message = error instanceof Error ? error.message : String(error);
186
+ return c.json({ error: message }, 500);
187
+ }
188
+ });
189
+
190
+ app.delete('/v1/terminals/:id', async (c) => {
191
+ const id = c.req.param('id');
192
+
193
+ try {
194
+ await terminalManager.kill(id);
195
+ return c.json({ success: true });
196
+ } catch (error) {
197
+ const message = error instanceof Error ? error.message : String(error);
198
+ return c.json({ error: message }, 500);
199
+ }
200
+ });
201
+ }
@@ -122,6 +122,7 @@ const defaultToolExtras: Record<string, string[]> = {
122
122
  'glob',
123
123
  'ripgrep',
124
124
  'git_status',
125
+ 'terminal',
125
126
  'apply_patch',
126
127
  'websearch',
127
128
  ],
@@ -327,7 +327,8 @@ async function touchSessionLastActive(args: {
327
327
  await db
328
328
  .update(sessions)
329
329
  .set({ updatedAt: Date.now() })
330
- .where(eq(sessions.id, sessionId));
330
+ .where(eq(sessions.id, sessionId))
331
+ .run();
331
332
  } catch (err) {
332
333
  debugLog('[touchSessionLastActive] Error:', err);
333
334
  }
@@ -11,6 +11,7 @@ import ANTHROPIC_SPOOF_PROMPT from '@agi-cli/sdk/prompts/providers/anthropicSpoo
11
11
  type: 'text',
12
12
  };
13
13
 
14
+ import { getTerminalManager } from '@agi-cli/sdk';
14
15
  export async function composeSystemPrompt(options: {
15
16
  provider: string;
16
17
  model?: string;
@@ -72,6 +73,15 @@ export async function composeSystemPrompt(options: {
72
73
  parts.push(userContextBlock);
73
74
  }
74
75
 
76
+ // Add terminal context if available
77
+ const terminalManager = getTerminalManager();
78
+ if (terminalManager) {
79
+ const terminalContext = terminalManager.getContext();
80
+ if (terminalContext) {
81
+ parts.push(terminalContext);
82
+ }
83
+ }
84
+
75
85
  const composed = parts.filter(Boolean).join('\n\n').trim();
76
86
  if (composed) return composed;
77
87