@agi-cli/server 0.1.113 → 0.1.115

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.113",
3
+ "version": "0.1.115",
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.113",
33
- "@agi-cli/database": "0.1.113",
32
+ "@agi-cli/sdk": "0.1.115",
33
+ "@agi-cli/database": "0.1.115",
34
34
  "drizzle-orm": "^0.44.5",
35
35
  "hono": "^4.9.9",
36
36
  "zod": "^4.1.8"
package/src/index.ts CHANGED
@@ -13,6 +13,7 @@ import { registerConfigRoutes } from './routes/config/index.ts';
13
13
  import { registerFilesRoutes } from './routes/files.ts';
14
14
  import { registerGitRoutes } from './routes/git/index.ts';
15
15
  import { registerTerminalsRoutes } from './routes/terminals.ts';
16
+ import { registerSessionFilesRoutes } from './routes/session-files.ts';
16
17
  import type { AgentConfigEntry } from './runtime/agent-registry.ts';
17
18
 
18
19
  const globalTerminalManager = new TerminalManager();
@@ -62,6 +63,7 @@ function initApp() {
62
63
  registerFilesRoutes(app);
63
64
  registerGitRoutes(app);
64
65
  registerTerminalsRoutes(app, globalTerminalManager);
66
+ registerSessionFilesRoutes(app);
65
67
 
66
68
  return app;
67
69
  }
@@ -127,6 +129,7 @@ export function createStandaloneApp(_config?: StandaloneAppConfig) {
127
129
  registerFilesRoutes(honoApp);
128
130
  registerGitRoutes(honoApp);
129
131
  registerTerminalsRoutes(honoApp, globalTerminalManager);
132
+ registerSessionFilesRoutes(honoApp);
130
133
 
131
134
  return honoApp;
132
135
  }
@@ -220,6 +223,7 @@ export function createEmbeddedApp(config: EmbeddedAppConfig = {}) {
220
223
  registerFilesRoutes(honoApp);
221
224
  registerGitRoutes(honoApp);
222
225
  registerTerminalsRoutes(honoApp, globalTerminalManager);
226
+ registerSessionFilesRoutes(honoApp);
223
227
 
224
228
  return honoApp;
225
229
  }
@@ -0,0 +1,387 @@
1
+ import type { Hono } from 'hono';
2
+ import { loadConfig } from '@agi-cli/sdk';
3
+ import { getDb } from '@agi-cli/database';
4
+ import { messages, messageParts, sessions } from '@agi-cli/database/schema';
5
+ import { eq, and, inArray } from 'drizzle-orm';
6
+ import { serializeError } from '../runtime/api-error.ts';
7
+ import { logger } from '@agi-cli/sdk';
8
+
9
+ const FILE_EDIT_TOOLS = [
10
+ 'Write',
11
+ 'ApplyPatch',
12
+ 'Edit',
13
+ 'write',
14
+ 'apply_patch',
15
+ 'edit',
16
+ ];
17
+
18
+ interface FileOperation {
19
+ path: string;
20
+ operation: 'write' | 'patch' | 'edit' | 'create';
21
+ timestamp: number;
22
+ toolCallId: string;
23
+ toolName: string;
24
+ patch?: string;
25
+ content?: string;
26
+ artifact?: {
27
+ kind: string;
28
+ patch?: string;
29
+ summary?: { additions: number; deletions: number };
30
+ };
31
+ }
32
+
33
+ interface SessionFile {
34
+ path: string;
35
+ operations: FileOperation[];
36
+ operationCount: number;
37
+ firstModified: number;
38
+ lastModified: number;
39
+ }
40
+
41
+ interface ToolResultData {
42
+ path?: string;
43
+ args?: Record<string, unknown>;
44
+ files?: Array<string | { path: string }>;
45
+ result?: {
46
+ ok?: boolean;
47
+ artifact?: {
48
+ kind?: string;
49
+ patch?: string;
50
+ summary?: { additions?: number; deletions?: number };
51
+ };
52
+ };
53
+ artifact?: {
54
+ kind?: string;
55
+ patch?: string;
56
+ summary?: { additions?: number; deletions?: number };
57
+ };
58
+ patch?: string;
59
+ }
60
+
61
+ function extractFilePathFromToolCall(
62
+ toolName: string,
63
+ content: unknown,
64
+ ): string | null {
65
+ if (!content || typeof content !== 'object') return null;
66
+
67
+ const c = content as Record<string, unknown>;
68
+ const args = c.args as Record<string, unknown> | undefined;
69
+
70
+ const name = toolName.toLowerCase();
71
+
72
+ if (name === 'write' || name === 'edit') {
73
+ if (args && typeof args.path === 'string') return args.path;
74
+ if (typeof c.path === 'string') return c.path;
75
+ }
76
+
77
+ if (name === 'applypatch' || name === 'apply_patch') {
78
+ const patch = args?.patch ?? c.patch;
79
+ if (typeof patch === 'string') {
80
+ const matches = [
81
+ ...patch.matchAll(/\*\*\* (?:Update|Add|Delete) File: (.+)/g),
82
+ ];
83
+ if (matches.length > 0) return matches[0][1].trim();
84
+ const unifiedMatch = patch.match(/^(?:---|\+\+\+) [ab]\/(.+)$/m);
85
+ if (unifiedMatch) return unifiedMatch[1].trim();
86
+ }
87
+ if (args && typeof args.path === 'string') return args.path;
88
+ if (typeof c.path === 'string') return c.path;
89
+ }
90
+
91
+ return null;
92
+ }
93
+
94
+ function extractPatchFromToolCall(
95
+ toolName: string,
96
+ content: unknown,
97
+ ): string | undefined {
98
+ if (!content || typeof content !== 'object') return undefined;
99
+
100
+ const c = content as Record<string, unknown>;
101
+ const args = c.args as Record<string, unknown> | undefined;
102
+ const name = toolName.toLowerCase();
103
+
104
+ if (name === 'applypatch' || name === 'apply_patch') {
105
+ const patch = args?.patch ?? c.patch;
106
+ if (typeof patch === 'string') return patch;
107
+ }
108
+
109
+ return undefined;
110
+ }
111
+
112
+ function extractContentFromToolCall(
113
+ toolName: string,
114
+ content: unknown,
115
+ ): string | undefined {
116
+ if (!content || typeof content !== 'object') return undefined;
117
+
118
+ const c = content as Record<string, unknown>;
119
+ const args = c.args as Record<string, unknown> | undefined;
120
+ const name = toolName.toLowerCase();
121
+
122
+ if (name === 'write') {
123
+ const writeContent = args?.content ?? c.content;
124
+ if (typeof writeContent === 'string') return writeContent;
125
+ }
126
+
127
+ return undefined;
128
+ }
129
+
130
+ function extractFilesFromToolResult(
131
+ toolName: string,
132
+ content: unknown,
133
+ ): string[] {
134
+ if (!content || typeof content !== 'object') return [];
135
+
136
+ const c = content as ToolResultData;
137
+ const files: string[] = [];
138
+
139
+ if (typeof c.path === 'string') {
140
+ files.push(c.path);
141
+ }
142
+
143
+ const args = c.args;
144
+ if (args && typeof args.path === 'string' && !files.includes(args.path)) {
145
+ files.push(args.path);
146
+ }
147
+
148
+ if (Array.isArray(c.files)) {
149
+ for (const f of c.files) {
150
+ if (typeof f === 'string' && !files.includes(f)) files.push(f);
151
+ if (f && typeof f === 'object' && typeof f.path === 'string') {
152
+ if (!files.includes(f.path)) files.push(f.path);
153
+ }
154
+ }
155
+ }
156
+
157
+ const name = toolName.toLowerCase();
158
+ if (name === 'applypatch' || name === 'apply_patch') {
159
+ const patch =
160
+ c.patch ??
161
+ (args?.patch as string | undefined) ??
162
+ c.result?.artifact?.patch;
163
+ if (typeof patch === 'string') {
164
+ const matches = patch.matchAll(
165
+ /\*\*\* (?:Update|Add|Delete) File: (.+)/g,
166
+ );
167
+ for (const match of matches) {
168
+ const fp = match[1].trim();
169
+ if (!files.includes(fp)) files.push(fp);
170
+ }
171
+ }
172
+ }
173
+
174
+ return files;
175
+ }
176
+
177
+ function extractDataFromToolResult(
178
+ toolName: string,
179
+ content: unknown,
180
+ ): {
181
+ patch?: string;
182
+ writeContent?: string;
183
+ artifact?: FileOperation['artifact'];
184
+ } {
185
+ if (!content || typeof content !== 'object') return {};
186
+
187
+ const c = content as ToolResultData;
188
+ const args = c.args as Record<string, unknown> | undefined;
189
+ const name = toolName.toLowerCase();
190
+
191
+ let patch: string | undefined;
192
+ let writeContent: string | undefined;
193
+ let artifact: FileOperation['artifact'] | undefined;
194
+
195
+ if (name === 'applypatch' || name === 'apply_patch') {
196
+ patch = (args?.patch as string | undefined) ?? c.patch;
197
+ }
198
+
199
+ if (name === 'write') {
200
+ writeContent = args?.content as string | undefined;
201
+ }
202
+
203
+ const rawArtifact = c.result?.artifact ?? c.artifact;
204
+ if (rawArtifact && typeof rawArtifact === 'object') {
205
+ artifact = {
206
+ kind: rawArtifact.kind || 'unknown',
207
+ patch: rawArtifact.patch,
208
+ summary: rawArtifact.summary
209
+ ? {
210
+ additions: rawArtifact.summary.additions || 0,
211
+ deletions: rawArtifact.summary.deletions || 0,
212
+ }
213
+ : undefined,
214
+ };
215
+ }
216
+
217
+ return { patch, writeContent, artifact };
218
+ }
219
+
220
+ function getOperationType(
221
+ toolName: string,
222
+ ): 'write' | 'patch' | 'edit' | 'create' {
223
+ const name = toolName.toLowerCase();
224
+ if (name === 'write') return 'write';
225
+ if (name === 'applypatch' || name === 'apply_patch') return 'patch';
226
+ if (name === 'edit') return 'edit';
227
+ return 'write';
228
+ }
229
+
230
+ export function registerSessionFilesRoutes(app: Hono) {
231
+ app.get('/v1/sessions/:sessionId/files', async (c) => {
232
+ try {
233
+ const sessionId = c.req.param('sessionId');
234
+ const projectRoot = c.req.query('project') || process.cwd();
235
+ const cfg = await loadConfig(projectRoot);
236
+ const db = await getDb(cfg.projectRoot);
237
+
238
+ const sessionRows = await db
239
+ .select()
240
+ .from(sessions)
241
+ .where(eq(sessions.id, sessionId))
242
+ .limit(1);
243
+
244
+ if (!sessionRows.length) {
245
+ return c.json({ error: 'Session not found' }, 404);
246
+ }
247
+
248
+ const messageRows = await db
249
+ .select({ id: messages.id })
250
+ .from(messages)
251
+ .where(eq(messages.sessionId, sessionId));
252
+
253
+ const messageIds = messageRows.map((m) => m.id);
254
+
255
+ if (!messageIds.length) {
256
+ return c.json({
257
+ files: [],
258
+ totalFiles: 0,
259
+ totalOperations: 0,
260
+ });
261
+ }
262
+
263
+ const parts = await db
264
+ .select()
265
+ .from(messageParts)
266
+ .where(
267
+ and(
268
+ inArray(messageParts.messageId, messageIds),
269
+ inArray(messageParts.toolName, FILE_EDIT_TOOLS),
270
+ ),
271
+ );
272
+
273
+ const fileOperationsMap = new Map<string, FileOperation[]>();
274
+ const toolCallDataMap = new Map<
275
+ string,
276
+ { patch?: string; content?: string }
277
+ >();
278
+
279
+ for (const part of parts) {
280
+ if (!part.toolName) continue;
281
+
282
+ let content: unknown;
283
+ try {
284
+ content = JSON.parse(part.content);
285
+ } catch {
286
+ continue;
287
+ }
288
+
289
+ if (part.type === 'tool_call') {
290
+ const callId = part.toolCallId || part.id;
291
+ const patch = extractPatchFromToolCall(part.toolName, content);
292
+ const writeContent = extractContentFromToolCall(
293
+ part.toolName,
294
+ content,
295
+ );
296
+
297
+ toolCallDataMap.set(callId, { patch, content: writeContent });
298
+
299
+ const path = extractFilePathFromToolCall(part.toolName, content);
300
+ if (path) {
301
+ const operation: FileOperation = {
302
+ path,
303
+ operation: getOperationType(part.toolName),
304
+ timestamp: part.startedAt || Date.now(),
305
+ toolCallId: callId,
306
+ toolName: part.toolName,
307
+ patch,
308
+ content: writeContent,
309
+ };
310
+
311
+ const existing = fileOperationsMap.get(path) || [];
312
+ const isDuplicate = existing.some(
313
+ (op) => op.toolCallId === operation.toolCallId,
314
+ );
315
+ if (!isDuplicate) {
316
+ existing.push(operation);
317
+ fileOperationsMap.set(path, existing);
318
+ }
319
+ }
320
+ } else if (part.type === 'tool_result') {
321
+ const filePaths = extractFilesFromToolResult(part.toolName, content);
322
+ const { patch, writeContent, artifact } = extractDataFromToolResult(
323
+ part.toolName,
324
+ content,
325
+ );
326
+ const callId = part.toolCallId || part.id;
327
+ const callData = toolCallDataMap.get(callId);
328
+
329
+ for (const filePath of filePaths) {
330
+ if (!filePath) continue;
331
+
332
+ const existing = fileOperationsMap.get(filePath) || [];
333
+ const existingOp = existing.find((op) => op.toolCallId === callId);
334
+
335
+ if (existingOp) {
336
+ existingOp.artifact = artifact;
337
+ existingOp.timestamp = part.completedAt || existingOp.timestamp;
338
+ if (!existingOp.patch && patch) {
339
+ existingOp.patch = patch;
340
+ }
341
+ if (!existingOp.content && writeContent) {
342
+ existingOp.content = writeContent;
343
+ }
344
+ } else {
345
+ const operation: FileOperation = {
346
+ path: filePath,
347
+ operation: getOperationType(part.toolName),
348
+ timestamp: part.completedAt || part.startedAt || Date.now(),
349
+ toolCallId: callId,
350
+ toolName: part.toolName,
351
+ patch: callData?.patch ?? patch,
352
+ content: callData?.content ?? writeContent,
353
+ artifact,
354
+ };
355
+ existing.push(operation);
356
+ fileOperationsMap.set(filePath, existing);
357
+ }
358
+ }
359
+ }
360
+ }
361
+
362
+ const files: SessionFile[] = [];
363
+ for (const [path, operations] of fileOperationsMap) {
364
+ operations.sort((a, b) => a.timestamp - b.timestamp);
365
+ files.push({
366
+ path,
367
+ operations,
368
+ operationCount: operations.length,
369
+ firstModified: operations[0]?.timestamp || 0,
370
+ lastModified: operations[operations.length - 1]?.timestamp || 0,
371
+ });
372
+ }
373
+
374
+ files.sort((a, b) => b.lastModified - a.lastModified);
375
+
376
+ return c.json({
377
+ files,
378
+ totalFiles: files.length,
379
+ totalOperations: parts.length,
380
+ });
381
+ } catch (error) {
382
+ logger.error('Failed to get session files', error);
383
+ const errorResponse = serializeError(error);
384
+ return c.json(errorResponse, errorResponse.error.status || 500);
385
+ }
386
+ });
387
+ }