@agi-cli/server 0.1.119 → 0.1.120

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.119",
3
+ "version": "0.1.120",
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.119",
33
- "@agi-cli/database": "0.1.119",
32
+ "@agi-cli/sdk": "0.1.120",
33
+ "@agi-cli/database": "0.1.120",
34
34
  "drizzle-orm": "^0.44.5",
35
35
  "hono": "^4.9.9",
36
36
  "zod": "^4.1.8"
package/src/index.ts CHANGED
@@ -14,6 +14,7 @@ import { registerFilesRoutes } from './routes/files.ts';
14
14
  import { registerGitRoutes } from './routes/git/index.ts';
15
15
  import { registerTerminalsRoutes } from './routes/terminals.ts';
16
16
  import { registerSessionFilesRoutes } from './routes/session-files.ts';
17
+ import { registerBranchRoutes } from './routes/branch.ts';
17
18
  import type { AgentConfigEntry } from './runtime/agent-registry.ts';
18
19
 
19
20
  const globalTerminalManager = new TerminalManager();
@@ -64,6 +65,7 @@ function initApp() {
64
65
  registerGitRoutes(app);
65
66
  registerTerminalsRoutes(app, globalTerminalManager);
66
67
  registerSessionFilesRoutes(app);
68
+ registerBranchRoutes(app);
67
69
 
68
70
  return app;
69
71
  }
@@ -130,6 +132,7 @@ export function createStandaloneApp(_config?: StandaloneAppConfig) {
130
132
  registerGitRoutes(honoApp);
131
133
  registerTerminalsRoutes(honoApp, globalTerminalManager);
132
134
  registerSessionFilesRoutes(honoApp);
135
+ registerBranchRoutes(honoApp);
133
136
 
134
137
  return honoApp;
135
138
  }
@@ -224,6 +227,7 @@ export function createEmbeddedApp(config: EmbeddedAppConfig = {}) {
224
227
  registerGitRoutes(honoApp);
225
228
  registerTerminalsRoutes(honoApp, globalTerminalManager);
226
229
  registerSessionFilesRoutes(honoApp);
230
+ registerBranchRoutes(honoApp);
227
231
 
228
232
  return honoApp;
229
233
  }
@@ -0,0 +1,106 @@
1
+ import type { Hono } from 'hono';
2
+ import { loadConfig } from '@agi-cli/sdk';
3
+ import { getDb } from '@agi-cli/database';
4
+ import { isProviderId, logger } from '@agi-cli/sdk';
5
+ import {
6
+ createBranch,
7
+ listBranches,
8
+ getParentSession,
9
+ } from '../runtime/branch.ts';
10
+ import { serializeError } from '../runtime/api-error.ts';
11
+
12
+ export function registerBranchRoutes(app: Hono) {
13
+ app.post('/v1/sessions/:sessionId/branch', async (c) => {
14
+ try {
15
+ const sessionId = c.req.param('sessionId');
16
+ const projectRoot = c.req.query('project') || process.cwd();
17
+ const cfg = await loadConfig(projectRoot);
18
+ const db = await getDb(cfg.projectRoot);
19
+
20
+ const body = (await c.req.json().catch(() => ({}))) as Record<
21
+ string,
22
+ unknown
23
+ >;
24
+
25
+ const fromMessageId = body.fromMessageId;
26
+ if (typeof fromMessageId !== 'string' || !fromMessageId.trim()) {
27
+ return c.json({ error: 'fromMessageId is required' }, 400);
28
+ }
29
+
30
+ const provider =
31
+ typeof body.provider === 'string' && isProviderId(body.provider)
32
+ ? body.provider
33
+ : undefined;
34
+
35
+ const model =
36
+ typeof body.model === 'string' && body.model.trim()
37
+ ? body.model.trim()
38
+ : undefined;
39
+
40
+ const agent =
41
+ typeof body.agent === 'string' && body.agent.trim()
42
+ ? body.agent.trim()
43
+ : undefined;
44
+
45
+ const title =
46
+ typeof body.title === 'string' && body.title.trim()
47
+ ? body.title.trim()
48
+ : undefined;
49
+
50
+ const result = await createBranch({
51
+ db,
52
+ parentSessionId: sessionId,
53
+ fromMessageId: fromMessageId.trim(),
54
+ provider,
55
+ model,
56
+ agent,
57
+ title,
58
+ projectPath: cfg.projectRoot,
59
+ });
60
+
61
+ return c.json(result, 201);
62
+ } catch (err) {
63
+ logger.error('Failed to create branch', err);
64
+ const errorResponse = serializeError(err);
65
+ return c.json(errorResponse, errorResponse.error.status || 400);
66
+ }
67
+ });
68
+
69
+ app.get('/v1/sessions/:sessionId/branches', async (c) => {
70
+ try {
71
+ const sessionId = c.req.param('sessionId');
72
+ const projectRoot = c.req.query('project') || process.cwd();
73
+ const cfg = await loadConfig(projectRoot);
74
+ const db = await getDb(cfg.projectRoot);
75
+
76
+ const branches = await listBranches(db, sessionId, cfg.projectRoot);
77
+
78
+ return c.json({ branches });
79
+ } catch (err) {
80
+ logger.error('Failed to list branches', err);
81
+ const errorResponse = serializeError(err);
82
+ return c.json(errorResponse, errorResponse.error.status || 500);
83
+ }
84
+ });
85
+
86
+ app.get('/v1/sessions/:sessionId/parent', async (c) => {
87
+ try {
88
+ const sessionId = c.req.param('sessionId');
89
+ const projectRoot = c.req.query('project') || process.cwd();
90
+ const cfg = await loadConfig(projectRoot);
91
+ const db = await getDb(cfg.projectRoot);
92
+
93
+ const parent = await getParentSession(db, sessionId, cfg.projectRoot);
94
+
95
+ if (!parent) {
96
+ return c.json({ parent: null });
97
+ }
98
+
99
+ return c.json({ parent });
100
+ } catch (err) {
101
+ logger.error('Failed to get parent session', err);
102
+ const errorResponse = serializeError(err);
103
+ return c.json(errorResponse, errorResponse.error.status || 500);
104
+ }
105
+ });
106
+ }
@@ -0,0 +1,277 @@
1
+ import { eq, asc } from 'drizzle-orm';
2
+ import type { DB } from '@agi-cli/database';
3
+ import { sessions, messages, messageParts } from '@agi-cli/database/schema';
4
+ import { publish } from '../events/bus.ts';
5
+ import type { ProviderId } from '@agi-cli/sdk';
6
+
7
+ type SessionRow = typeof sessions.$inferSelect;
8
+
9
+ export type CreateBranchInput = {
10
+ db: DB;
11
+ parentSessionId: string;
12
+ fromMessageId: string;
13
+ provider?: ProviderId;
14
+ model?: string;
15
+ agent?: string;
16
+ title?: string;
17
+ projectPath: string;
18
+ };
19
+
20
+ export type BranchResult = {
21
+ session: SessionRow;
22
+ parentSessionId: string;
23
+ branchPointMessageId: string;
24
+ copiedMessages: number;
25
+ copiedParts: number;
26
+ };
27
+
28
+ export async function createBranch({
29
+ db,
30
+ parentSessionId,
31
+ fromMessageId,
32
+ provider,
33
+ model,
34
+ agent,
35
+ title,
36
+ projectPath,
37
+ }: CreateBranchInput): Promise<BranchResult> {
38
+ const parentRows = await db
39
+ .select()
40
+ .from(sessions)
41
+ .where(eq(sessions.id, parentSessionId));
42
+
43
+ if (!parentRows.length) {
44
+ throw new Error('Parent session not found');
45
+ }
46
+
47
+ const parent = parentRows[0];
48
+
49
+ if (parent.projectPath !== projectPath) {
50
+ throw new Error('Parent session not found in this project');
51
+ }
52
+
53
+ const branchPointRows = await db
54
+ .select()
55
+ .from(messages)
56
+ .where(eq(messages.id, fromMessageId));
57
+
58
+ if (!branchPointRows.length) {
59
+ throw new Error('Branch point message not found');
60
+ }
61
+
62
+ const branchPoint = branchPointRows[0];
63
+
64
+ if (branchPoint.sessionId !== parentSessionId) {
65
+ throw new Error('Branch point message does not belong to parent session');
66
+ }
67
+
68
+ const allMessages = await db
69
+ .select()
70
+ .from(messages)
71
+ .where(eq(messages.sessionId, parentSessionId))
72
+ .orderBy(asc(messages.createdAt));
73
+
74
+ const branchPointIndex = allMessages.findIndex((m) => m.id === fromMessageId);
75
+ if (branchPointIndex === -1) {
76
+ throw new Error('Branch point message not found in session');
77
+ }
78
+
79
+ const messagesToCopy = allMessages.slice(0, branchPointIndex + 1);
80
+
81
+ const newSessionId = crypto.randomUUID();
82
+ const now = Date.now();
83
+
84
+ const newSession: typeof sessions.$inferInsert = {
85
+ id: newSessionId,
86
+ title: title || `Branch of ${parent.title || 'Untitled'}`,
87
+ agent: agent || parent.agent,
88
+ provider: provider || parent.provider,
89
+ model: model || parent.model,
90
+ projectPath: parent.projectPath,
91
+ createdAt: now,
92
+ lastActiveAt: now,
93
+ parentSessionId,
94
+ branchPointMessageId: fromMessageId,
95
+ sessionType: 'branch',
96
+ };
97
+
98
+ await db.insert(sessions).values(newSession);
99
+
100
+ const messageIdMap = new Map<string, string>();
101
+ let copiedParts = 0;
102
+
103
+ for (const msg of messagesToCopy) {
104
+ const newMessageId = crypto.randomUUID();
105
+ messageIdMap.set(msg.id, newMessageId);
106
+
107
+ const newMessage: typeof messages.$inferInsert = {
108
+ id: newMessageId,
109
+ sessionId: newSessionId,
110
+ role: msg.role,
111
+ status: msg.status,
112
+ agent: msg.agent,
113
+ provider: msg.provider,
114
+ model: msg.model,
115
+ createdAt: msg.createdAt,
116
+ completedAt: msg.completedAt,
117
+ latencyMs: msg.latencyMs,
118
+ promptTokens: msg.promptTokens,
119
+ completionTokens: msg.completionTokens,
120
+ totalTokens: msg.totalTokens,
121
+ cachedInputTokens: msg.cachedInputTokens,
122
+ reasoningTokens: msg.reasoningTokens,
123
+ error: msg.error,
124
+ errorType: msg.errorType,
125
+ errorDetails: msg.errorDetails,
126
+ isAborted: msg.isAborted,
127
+ };
128
+
129
+ await db.insert(messages).values(newMessage);
130
+
131
+ const parts = await db
132
+ .select()
133
+ .from(messageParts)
134
+ .where(eq(messageParts.messageId, msg.id))
135
+ .orderBy(asc(messageParts.index));
136
+
137
+ for (const part of parts) {
138
+ const newPart: typeof messageParts.$inferInsert = {
139
+ id: crypto.randomUUID(),
140
+ messageId: newMessageId,
141
+ index: part.index,
142
+ stepIndex: part.stepIndex,
143
+ type: part.type,
144
+ content: part.content,
145
+ agent: part.agent,
146
+ provider: part.provider,
147
+ model: part.model,
148
+ startedAt: part.startedAt,
149
+ completedAt: part.completedAt,
150
+ compactedAt: part.compactedAt,
151
+ toolName: part.toolName,
152
+ toolCallId: part.toolCallId,
153
+ toolDurationMs: part.toolDurationMs,
154
+ };
155
+
156
+ await db.insert(messageParts).values(newPart);
157
+ copiedParts++;
158
+ }
159
+ }
160
+
161
+ const result: SessionRow = {
162
+ ...newSession,
163
+ totalInputTokens: null,
164
+ totalOutputTokens: null,
165
+ totalCachedTokens: null,
166
+ totalReasoningTokens: null,
167
+ totalToolTimeMs: null,
168
+ toolCountsJson: null,
169
+ contextSummary: null,
170
+ lastCompactedAt: null,
171
+ };
172
+
173
+ publish({
174
+ type: 'session.created',
175
+ sessionId: newSessionId,
176
+ payload: result,
177
+ });
178
+
179
+ return {
180
+ session: result,
181
+ parentSessionId,
182
+ branchPointMessageId: fromMessageId,
183
+ copiedMessages: messagesToCopy.length,
184
+ copiedParts,
185
+ };
186
+ }
187
+
188
+ export type ListBranchesResult = Array<{
189
+ session: SessionRow;
190
+ branchPointMessageId: string | null;
191
+ branchPointPreview: string | null;
192
+ createdAt: number;
193
+ }>;
194
+
195
+ export async function listBranches(
196
+ db: DB,
197
+ sessionId: string,
198
+ projectPath: string,
199
+ ): Promise<ListBranchesResult> {
200
+ const branches = await db
201
+ .select()
202
+ .from(sessions)
203
+ .where(eq(sessions.parentSessionId, sessionId))
204
+ .orderBy(asc(sessions.createdAt));
205
+
206
+ const results: ListBranchesResult = [];
207
+
208
+ for (const branch of branches) {
209
+ if (branch.projectPath !== projectPath) continue;
210
+
211
+ let preview: string | null = null;
212
+
213
+ if (branch.branchPointMessageId) {
214
+ const msgRows = await db
215
+ .select()
216
+ .from(messages)
217
+ .where(eq(messages.id, branch.branchPointMessageId));
218
+
219
+ if (msgRows.length > 0) {
220
+ const parts = await db
221
+ .select()
222
+ .from(messageParts)
223
+ .where(eq(messageParts.messageId, branch.branchPointMessageId))
224
+ .orderBy(asc(messageParts.index));
225
+
226
+ for (const part of parts) {
227
+ if (part.type === 'text') {
228
+ try {
229
+ const content = JSON.parse(part.content || '{}');
230
+ if (content.text) {
231
+ preview = content.text.slice(0, 100);
232
+ break;
233
+ }
234
+ } catch {}
235
+ }
236
+ }
237
+ }
238
+ }
239
+
240
+ results.push({
241
+ session: branch,
242
+ branchPointMessageId: branch.branchPointMessageId,
243
+ branchPointPreview: preview,
244
+ createdAt: branch.createdAt,
245
+ });
246
+ }
247
+
248
+ return results;
249
+ }
250
+
251
+ export async function getParentSession(
252
+ db: DB,
253
+ sessionId: string,
254
+ projectPath: string,
255
+ ): Promise<SessionRow | null> {
256
+ const sessionRows = await db
257
+ .select()
258
+ .from(sessions)
259
+ .where(eq(sessions.id, sessionId));
260
+
261
+ if (!sessionRows.length) return null;
262
+
263
+ const session = sessionRows[0];
264
+ if (!session.parentSessionId) return null;
265
+
266
+ const parentRows = await db
267
+ .select()
268
+ .from(sessions)
269
+ .where(eq(sessions.id, session.parentSessionId));
270
+
271
+ if (!parentRows.length) return null;
272
+
273
+ const parent = parentRows[0];
274
+ if (parent.projectPath !== projectPath) return null;
275
+
276
+ return parent;
277
+ }