@agi-cli/server 0.1.65 → 0.1.67
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 +5 -4
- package/src/routes/config.ts +97 -65
- package/src/routes/git.ts +132 -31
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@agi-cli/server",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.67",
|
|
4
4
|
"description": "HTTP API server for AGI CLI",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./src/index.ts",
|
|
@@ -29,10 +29,11 @@
|
|
|
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.67",
|
|
33
|
+
"@agi-cli/database": "0.1.67",
|
|
34
34
|
"drizzle-orm": "^0.44.5",
|
|
35
|
-
"hono": "^4.9.9"
|
|
35
|
+
"hono": "^4.9.9",
|
|
36
|
+
"zod": "^4.1.8"
|
|
36
37
|
},
|
|
37
38
|
"devDependencies": {
|
|
38
39
|
"@types/bun": "latest",
|
package/src/routes/config.ts
CHANGED
|
@@ -4,6 +4,63 @@ import { catalog, type ProviderId, isProviderAuthorized } from '@agi-cli/sdk';
|
|
|
4
4
|
import { readdir } from 'node:fs/promises';
|
|
5
5
|
import { join, basename } from 'node:path';
|
|
6
6
|
import type { EmbeddedAppConfig } from '../index.ts';
|
|
7
|
+
import type { AGIConfig } from '@agi-cli/sdk';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Check if a provider is authorized in either embedded config or file-based config
|
|
11
|
+
*/
|
|
12
|
+
async function isProviderAuthorizedHybrid(
|
|
13
|
+
embeddedConfig: EmbeddedAppConfig | undefined,
|
|
14
|
+
fileConfig: AGIConfig,
|
|
15
|
+
provider: ProviderId,
|
|
16
|
+
): Promise<boolean> {
|
|
17
|
+
// Check embedded auth first
|
|
18
|
+
const hasEmbeddedAuth =
|
|
19
|
+
embeddedConfig?.provider === provider ||
|
|
20
|
+
(embeddedConfig?.auth && provider in embeddedConfig.auth);
|
|
21
|
+
|
|
22
|
+
if (hasEmbeddedAuth) {
|
|
23
|
+
return true;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// Fallback to file-based auth
|
|
27
|
+
return await isProviderAuthorized(fileConfig, provider);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Get all authorized providers from both embedded and file-based config
|
|
32
|
+
*/
|
|
33
|
+
async function getAuthorizedProviders(
|
|
34
|
+
embeddedConfig: EmbeddedAppConfig | undefined,
|
|
35
|
+
fileConfig: AGIConfig,
|
|
36
|
+
): Promise<ProviderId[]> {
|
|
37
|
+
const allProviders = Object.keys(catalog) as ProviderId[];
|
|
38
|
+
const authorizedProviders: ProviderId[] = [];
|
|
39
|
+
|
|
40
|
+
for (const provider of allProviders) {
|
|
41
|
+
const authorized = await isProviderAuthorizedHybrid(
|
|
42
|
+
embeddedConfig,
|
|
43
|
+
fileConfig,
|
|
44
|
+
provider,
|
|
45
|
+
);
|
|
46
|
+
if (authorized) {
|
|
47
|
+
authorizedProviders.push(provider);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
return authorizedProviders;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Get default value with embedded config taking priority over file config
|
|
56
|
+
*/
|
|
57
|
+
function getDefault<T>(
|
|
58
|
+
embeddedValue: T | undefined,
|
|
59
|
+
embeddedDefaultValue: T | undefined,
|
|
60
|
+
fileValue: T,
|
|
61
|
+
): T {
|
|
62
|
+
return embeddedValue ?? embeddedDefaultValue ?? fileValue;
|
|
63
|
+
}
|
|
7
64
|
|
|
8
65
|
export function registerConfigRoutes(app: Hono) {
|
|
9
66
|
// Get working directory info
|
|
@@ -46,37 +103,28 @@ export function registerConfigRoutes(app: Hono) {
|
|
|
46
103
|
const allAgents = Array.from(new Set([...embeddedAgents, ...fileAgents]));
|
|
47
104
|
|
|
48
105
|
// Providers: Check both embedded and file-based auth
|
|
49
|
-
const
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
// Check embedded auth first
|
|
54
|
-
const hasEmbeddedAuth =
|
|
55
|
-
embeddedConfig?.provider === provider ||
|
|
56
|
-
(embeddedConfig?.auth && provider in embeddedConfig.auth);
|
|
57
|
-
|
|
58
|
-
// Fallback to file-based auth
|
|
59
|
-
const hasFileAuth = await isProviderAuthorized(cfg, provider);
|
|
60
|
-
|
|
61
|
-
if (hasEmbeddedAuth || hasFileAuth) {
|
|
62
|
-
authorizedProviders.push(provider);
|
|
63
|
-
}
|
|
64
|
-
}
|
|
106
|
+
const authorizedProviders = await getAuthorizedProviders(
|
|
107
|
+
embeddedConfig,
|
|
108
|
+
cfg,
|
|
109
|
+
);
|
|
65
110
|
|
|
66
111
|
// Defaults: Embedded overrides file config
|
|
67
112
|
const defaults = {
|
|
68
|
-
agent:
|
|
69
|
-
embeddedConfig?.
|
|
70
|
-
embeddedConfig?.agent
|
|
113
|
+
agent: getDefault(
|
|
114
|
+
embeddedConfig?.agent,
|
|
115
|
+
embeddedConfig?.defaults?.agent,
|
|
71
116
|
cfg.defaults.agent,
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
embeddedConfig?.provider
|
|
117
|
+
),
|
|
118
|
+
provider: getDefault(
|
|
119
|
+
embeddedConfig?.provider,
|
|
120
|
+
embeddedConfig?.defaults?.provider,
|
|
75
121
|
cfg.defaults.provider,
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
embeddedConfig?.model
|
|
122
|
+
),
|
|
123
|
+
model: getDefault(
|
|
124
|
+
embeddedConfig?.model,
|
|
125
|
+
embeddedConfig?.defaults?.model,
|
|
79
126
|
cfg.defaults.model,
|
|
127
|
+
),
|
|
80
128
|
};
|
|
81
129
|
|
|
82
130
|
return c.json({
|
|
@@ -98,8 +146,11 @@ export function registerConfigRoutes(app: Hono) {
|
|
|
98
146
|
: ['general', 'build', 'plan'];
|
|
99
147
|
return c.json({
|
|
100
148
|
agents,
|
|
101
|
-
default:
|
|
102
|
-
embeddedConfig.agent
|
|
149
|
+
default: getDefault(
|
|
150
|
+
embeddedConfig.agent,
|
|
151
|
+
embeddedConfig.defaults?.agent,
|
|
152
|
+
'general',
|
|
153
|
+
),
|
|
103
154
|
});
|
|
104
155
|
}
|
|
105
156
|
|
|
@@ -140,22 +191,18 @@ export function registerConfigRoutes(app: Hono) {
|
|
|
140
191
|
|
|
141
192
|
return c.json({
|
|
142
193
|
providers,
|
|
143
|
-
default:
|
|
194
|
+
default: getDefault(
|
|
195
|
+
embeddedConfig.provider,
|
|
196
|
+
embeddedConfig.defaults?.provider,
|
|
197
|
+
undefined,
|
|
198
|
+
),
|
|
144
199
|
});
|
|
145
200
|
}
|
|
146
201
|
|
|
147
202
|
const projectRoot = c.req.query('project') || process.cwd();
|
|
148
203
|
const cfg = await loadConfig(projectRoot);
|
|
149
204
|
|
|
150
|
-
const
|
|
151
|
-
const authorizedProviders: ProviderId[] = [];
|
|
152
|
-
|
|
153
|
-
for (const provider of allProviders) {
|
|
154
|
-
const authorized = await isProviderAuthorized(cfg, provider);
|
|
155
|
-
if (authorized) {
|
|
156
|
-
authorizedProviders.push(provider);
|
|
157
|
-
}
|
|
158
|
-
}
|
|
205
|
+
const authorizedProviders = await getAuthorizedProviders(undefined, cfg);
|
|
159
206
|
|
|
160
207
|
return c.json({
|
|
161
208
|
providers: authorizedProviders,
|
|
@@ -170,36 +217,17 @@ export function registerConfigRoutes(app: Hono) {
|
|
|
170
217
|
| undefined;
|
|
171
218
|
const provider = c.req.param('provider') as ProviderId;
|
|
172
219
|
|
|
173
|
-
|
|
174
|
-
// Check if provider is authorized in embedded mode
|
|
175
|
-
const hasAuth =
|
|
176
|
-
embeddedConfig.provider === provider ||
|
|
177
|
-
(embeddedConfig.auth && provider in embeddedConfig.auth);
|
|
178
|
-
|
|
179
|
-
if (!hasAuth) {
|
|
180
|
-
return c.json({ error: 'Provider not authorized' }, 403);
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
const providerCatalog = catalog[provider];
|
|
184
|
-
if (!providerCatalog) {
|
|
185
|
-
return c.json({ error: 'Provider not found' }, 404);
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
return c.json({
|
|
189
|
-
models: providerCatalog.models.map((m) => ({
|
|
190
|
-
id: m.id,
|
|
191
|
-
label: m.label || m.id,
|
|
192
|
-
toolCall: m.toolCall,
|
|
193
|
-
reasoning: m.reasoning,
|
|
194
|
-
})),
|
|
195
|
-
default: embeddedConfig.model || embeddedConfig.defaults?.model,
|
|
196
|
-
});
|
|
197
|
-
}
|
|
198
|
-
|
|
220
|
+
// Always load file config for fallback auth check
|
|
199
221
|
const projectRoot = c.req.query('project') || process.cwd();
|
|
200
222
|
const cfg = await loadConfig(projectRoot);
|
|
201
223
|
|
|
202
|
-
|
|
224
|
+
// Check if provider is authorized (hybrid: embedded OR file-based)
|
|
225
|
+
const authorized = await isProviderAuthorizedHybrid(
|
|
226
|
+
embeddedConfig,
|
|
227
|
+
cfg,
|
|
228
|
+
provider,
|
|
229
|
+
);
|
|
230
|
+
|
|
203
231
|
if (!authorized) {
|
|
204
232
|
return c.json({ error: 'Provider not authorized' }, 403);
|
|
205
233
|
}
|
|
@@ -216,7 +244,11 @@ export function registerConfigRoutes(app: Hono) {
|
|
|
216
244
|
toolCall: m.toolCall,
|
|
217
245
|
reasoning: m.reasoning,
|
|
218
246
|
})),
|
|
219
|
-
default:
|
|
247
|
+
default: getDefault(
|
|
248
|
+
embeddedConfig?.model,
|
|
249
|
+
embeddedConfig?.defaults?.model,
|
|
250
|
+
cfg.defaults.model,
|
|
251
|
+
),
|
|
220
252
|
});
|
|
221
253
|
});
|
|
222
254
|
}
|
package/src/routes/git.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { Hono } from 'hono';
|
|
2
2
|
import { execFile } from 'node:child_process';
|
|
3
|
-
import { extname } from 'node:path';
|
|
3
|
+
import { extname, join } from 'node:path';
|
|
4
|
+
import { readFile } from 'node:fs/promises';
|
|
4
5
|
import { promisify } from 'node:util';
|
|
5
6
|
import { z } from 'zod';
|
|
6
7
|
import { generateText, resolveModel, type ProviderId } from '@agi-cli/sdk';
|
|
@@ -113,10 +114,13 @@ const gitPushSchema = z.object({
|
|
|
113
114
|
// Types
|
|
114
115
|
export interface GitFile {
|
|
115
116
|
path: string;
|
|
117
|
+
absPath: string; // NEW: Absolute filesystem path
|
|
116
118
|
status: 'modified' | 'added' | 'deleted' | 'renamed' | 'untracked';
|
|
117
119
|
staged: boolean;
|
|
118
120
|
insertions?: number;
|
|
119
121
|
deletions?: number;
|
|
122
|
+
oldPath?: string; // For renamed files
|
|
123
|
+
isNew: boolean; // NEW: True for untracked or newly added files
|
|
120
124
|
}
|
|
121
125
|
|
|
122
126
|
interface GitRoot {
|
|
@@ -149,7 +153,25 @@ async function validateAndGetGitRoot(
|
|
|
149
153
|
}
|
|
150
154
|
}
|
|
151
155
|
|
|
152
|
-
|
|
156
|
+
/**
|
|
157
|
+
* Check if a file is new/untracked (not in git index)
|
|
158
|
+
*/
|
|
159
|
+
async function checkIfNewFile(gitRoot: string, file: string): Promise<boolean> {
|
|
160
|
+
try {
|
|
161
|
+
// Check if file exists in git index or committed
|
|
162
|
+
await execFileAsync('git', ['ls-files', '--error-unmatch', file], {
|
|
163
|
+
cwd: gitRoot,
|
|
164
|
+
});
|
|
165
|
+
return false; // File exists in git
|
|
166
|
+
} catch {
|
|
167
|
+
return true; // File is new/untracked
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function parseGitStatus(
|
|
172
|
+
statusOutput: string,
|
|
173
|
+
gitRoot: string,
|
|
174
|
+
): {
|
|
153
175
|
staged: GitFile[];
|
|
154
176
|
unstaged: GitFile[];
|
|
155
177
|
untracked: GitFile[];
|
|
@@ -160,34 +182,50 @@ function parseGitStatus(statusOutput: string): {
|
|
|
160
182
|
const untracked: GitFile[] = [];
|
|
161
183
|
|
|
162
184
|
for (const line of lines) {
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
185
|
+
// Porcelain v2 format has different line types
|
|
186
|
+
if (line.startsWith('1 ') || line.startsWith('2 ')) {
|
|
187
|
+
// Regular changed entry: "1 XY sub <mH> <mI> <mW> <hH> <hI> <path>"
|
|
188
|
+
// XY is a 2-character field with staged (X) and unstaged (Y) status
|
|
189
|
+
const parts = line.split(' ');
|
|
190
|
+
if (parts.length < 9) continue;
|
|
191
|
+
|
|
192
|
+
const xy = parts[1]; // e.g., ".M", "M.", "MM", "A.", etc.
|
|
193
|
+
const x = xy[0]; // staged status
|
|
194
|
+
const y = xy[1]; // unstaged status
|
|
195
|
+
const path = parts.slice(8).join(' '); // Path can contain spaces
|
|
196
|
+
const absPath = join(gitRoot, path);
|
|
197
|
+
|
|
198
|
+
// Check if file is staged (X is not '.')
|
|
199
|
+
if (x !== '.') {
|
|
200
|
+
staged.push({
|
|
201
|
+
path,
|
|
202
|
+
absPath,
|
|
203
|
+
status: getStatusFromCodeV2(x),
|
|
204
|
+
staged: true,
|
|
205
|
+
isNew: x === 'A',
|
|
206
|
+
});
|
|
207
|
+
}
|
|
184
208
|
|
|
185
|
-
|
|
186
|
-
|
|
209
|
+
// Check if file is unstaged (Y is not '.')
|
|
210
|
+
if (y !== '.') {
|
|
211
|
+
unstaged.push({
|
|
212
|
+
path,
|
|
213
|
+
absPath,
|
|
214
|
+
status: getStatusFromCodeV2(y),
|
|
215
|
+
staged: false,
|
|
216
|
+
isNew: false,
|
|
217
|
+
});
|
|
218
|
+
}
|
|
219
|
+
} else if (line.startsWith('? ')) {
|
|
220
|
+
// Untracked file: "? <path>"
|
|
221
|
+
const path = line.slice(2);
|
|
222
|
+
const absPath = join(gitRoot, path);
|
|
187
223
|
untracked.push({
|
|
188
224
|
path,
|
|
225
|
+
absPath,
|
|
189
226
|
status: 'untracked',
|
|
190
227
|
staged: false,
|
|
228
|
+
isNew: true,
|
|
191
229
|
});
|
|
192
230
|
}
|
|
193
231
|
}
|
|
@@ -210,6 +248,23 @@ function getStatusFromCode(code: string): GitFile['status'] {
|
|
|
210
248
|
}
|
|
211
249
|
}
|
|
212
250
|
|
|
251
|
+
function getStatusFromCodeV2(code: string): GitFile['status'] {
|
|
252
|
+
switch (code) {
|
|
253
|
+
case 'M':
|
|
254
|
+
return 'modified';
|
|
255
|
+
case 'A':
|
|
256
|
+
return 'added';
|
|
257
|
+
case 'D':
|
|
258
|
+
return 'deleted';
|
|
259
|
+
case 'R':
|
|
260
|
+
return 'renamed';
|
|
261
|
+
case 'C':
|
|
262
|
+
return 'modified'; // Copied - treat as modified
|
|
263
|
+
default:
|
|
264
|
+
return 'modified';
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
|
|
213
268
|
async function getAheadBehind(
|
|
214
269
|
gitRoot: string,
|
|
215
270
|
): Promise<{ ahead: number; behind: number }> {
|
|
@@ -264,11 +319,14 @@ export function registerGitRoutes(app: Hono) {
|
|
|
264
319
|
// Get status
|
|
265
320
|
const { stdout: statusOutput } = await execFileAsync(
|
|
266
321
|
'git',
|
|
267
|
-
['status', '--porcelain=
|
|
322
|
+
['status', '--porcelain=v2'],
|
|
268
323
|
{ cwd: gitRoot },
|
|
269
324
|
);
|
|
270
325
|
|
|
271
|
-
const { staged, unstaged, untracked } = parseGitStatus(
|
|
326
|
+
const { staged, unstaged, untracked } = parseGitStatus(
|
|
327
|
+
statusOutput,
|
|
328
|
+
gitRoot,
|
|
329
|
+
);
|
|
272
330
|
|
|
273
331
|
// Get ahead/behind counts
|
|
274
332
|
const { ahead, behind } = await getAheadBehind(gitRoot);
|
|
@@ -286,6 +344,8 @@ export function registerGitRoutes(app: Hono) {
|
|
|
286
344
|
branch,
|
|
287
345
|
ahead,
|
|
288
346
|
behind,
|
|
347
|
+
gitRoot, // NEW: Expose git root path
|
|
348
|
+
workingDir: requestedPath, // NEW: Current working directory
|
|
289
349
|
staged,
|
|
290
350
|
unstaged,
|
|
291
351
|
untracked,
|
|
@@ -324,8 +384,46 @@ export function registerGitRoutes(app: Hono) {
|
|
|
324
384
|
}
|
|
325
385
|
|
|
326
386
|
const { gitRoot } = validation;
|
|
387
|
+
const absPath = join(gitRoot, query.file);
|
|
388
|
+
|
|
389
|
+
// Check if file is new/untracked
|
|
390
|
+
const isNewFile = await checkIfNewFile(gitRoot, query.file);
|
|
391
|
+
|
|
392
|
+
// For new files, read and return full content
|
|
393
|
+
if (isNewFile) {
|
|
394
|
+
try {
|
|
395
|
+
const content = await readFile(absPath, 'utf-8');
|
|
396
|
+
const lineCount = content.split('\n').length;
|
|
397
|
+
const language = inferLanguage(query.file);
|
|
398
|
+
|
|
399
|
+
return c.json({
|
|
400
|
+
status: 'ok',
|
|
401
|
+
data: {
|
|
402
|
+
file: query.file,
|
|
403
|
+
absPath,
|
|
404
|
+
diff: '', // Empty diff for new files
|
|
405
|
+
content, // NEW: Full file content
|
|
406
|
+
isNewFile: true, // NEW: Flag indicating this is a new file
|
|
407
|
+
isBinary: false,
|
|
408
|
+
insertions: lineCount,
|
|
409
|
+
deletions: 0,
|
|
410
|
+
language,
|
|
411
|
+
staged: !!query.staged, // NEW: Whether showing staged or unstaged
|
|
412
|
+
},
|
|
413
|
+
});
|
|
414
|
+
} catch (error) {
|
|
415
|
+
return c.json(
|
|
416
|
+
{
|
|
417
|
+
status: 'error',
|
|
418
|
+
error:
|
|
419
|
+
error instanceof Error ? error.message : 'Failed to read file',
|
|
420
|
+
},
|
|
421
|
+
500,
|
|
422
|
+
);
|
|
423
|
+
}
|
|
424
|
+
}
|
|
327
425
|
|
|
328
|
-
//
|
|
426
|
+
// For existing files, get diff output and stats
|
|
329
427
|
const diffArgs = query.staged
|
|
330
428
|
? ['diff', '--cached', '--', query.file]
|
|
331
429
|
: ['diff', '--', query.file];
|
|
@@ -370,11 +468,14 @@ export function registerGitRoutes(app: Hono) {
|
|
|
370
468
|
status: 'ok',
|
|
371
469
|
data: {
|
|
372
470
|
file: query.file,
|
|
471
|
+
absPath, // NEW: Absolute path
|
|
373
472
|
diff: diffText,
|
|
473
|
+
isNewFile: false, // NEW: Not a new file
|
|
474
|
+
isBinary: binary,
|
|
374
475
|
insertions,
|
|
375
476
|
deletions,
|
|
376
477
|
language,
|
|
377
|
-
|
|
478
|
+
staged: !!query.staged, // NEW: Whether showing staged or unstaged
|
|
378
479
|
},
|
|
379
480
|
});
|
|
380
481
|
} catch (error) {
|
|
@@ -568,10 +669,10 @@ export function registerGitRoutes(app: Hono) {
|
|
|
568
669
|
// Get file list for context
|
|
569
670
|
const { stdout: statusOutput } = await execFileAsync(
|
|
570
671
|
'git',
|
|
571
|
-
['status', '--porcelain=
|
|
672
|
+
['status', '--porcelain=v2'],
|
|
572
673
|
{ cwd: gitRoot },
|
|
573
674
|
);
|
|
574
|
-
const { staged } = parseGitStatus(statusOutput);
|
|
675
|
+
const { staged } = parseGitStatus(statusOutput, gitRoot);
|
|
575
676
|
const fileList = staged.map((f) => `${f.status}: ${f.path}`).join('\n');
|
|
576
677
|
|
|
577
678
|
// Load config to get provider settings
|