@agi-cli/server 0.1.148 → 0.1.150
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 +3 -3
- package/src/index.ts +4 -0
- package/src/openapi/schemas.ts +25 -1
- package/src/routes/auth.ts +500 -0
- package/src/routes/git/status.ts +9 -2
- package/src/routes/git/types.ts +13 -1
- package/src/routes/git/utils.ts +38 -1
- package/src/runtime/tools/approval.ts +5 -4
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@agi-cli/server",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.150",
|
|
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.
|
|
33
|
-
"@agi-cli/database": "0.1.
|
|
32
|
+
"@agi-cli/sdk": "0.1.150",
|
|
33
|
+
"@agi-cli/database": "0.1.150",
|
|
34
34
|
"drizzle-orm": "^0.44.5",
|
|
35
35
|
"hono": "^4.9.9",
|
|
36
36
|
"zod": "^4.1.8"
|
package/src/index.ts
CHANGED
|
@@ -18,6 +18,7 @@ import { registerBranchRoutes } from './routes/branch.ts';
|
|
|
18
18
|
import { registerResearchRoutes } from './routes/research.ts';
|
|
19
19
|
import { registerSessionApprovalRoute } from './routes/session-approval.ts';
|
|
20
20
|
import { registerSetuRoutes } from './routes/setu.ts';
|
|
21
|
+
import { registerAuthRoutes } from './routes/auth.ts';
|
|
21
22
|
import type { AgentConfigEntry } from './runtime/agent/registry.ts';
|
|
22
23
|
|
|
23
24
|
const globalTerminalManager = new TerminalManager();
|
|
@@ -72,6 +73,7 @@ function initApp() {
|
|
|
72
73
|
registerBranchRoutes(app);
|
|
73
74
|
registerResearchRoutes(app);
|
|
74
75
|
registerSetuRoutes(app);
|
|
76
|
+
registerAuthRoutes(app);
|
|
75
77
|
|
|
76
78
|
return app;
|
|
77
79
|
}
|
|
@@ -142,6 +144,7 @@ export function createStandaloneApp(_config?: StandaloneAppConfig) {
|
|
|
142
144
|
registerBranchRoutes(honoApp);
|
|
143
145
|
registerResearchRoutes(honoApp);
|
|
144
146
|
registerSetuRoutes(honoApp);
|
|
147
|
+
registerAuthRoutes(honoApp);
|
|
145
148
|
|
|
146
149
|
return honoApp;
|
|
147
150
|
}
|
|
@@ -240,6 +243,7 @@ export function createEmbeddedApp(config: EmbeddedAppConfig = {}) {
|
|
|
240
243
|
registerBranchRoutes(honoApp);
|
|
241
244
|
registerResearchRoutes(honoApp);
|
|
242
245
|
registerSetuRoutes(honoApp);
|
|
246
|
+
registerAuthRoutes(honoApp);
|
|
243
247
|
|
|
244
248
|
return honoApp;
|
|
245
249
|
}
|
package/src/openapi/schemas.ts
CHANGED
|
@@ -228,7 +228,12 @@ export const schemas = {
|
|
|
228
228
|
type: 'array',
|
|
229
229
|
items: { $ref: '#/components/schemas/GitFile' },
|
|
230
230
|
},
|
|
231
|
+
conflicted: {
|
|
232
|
+
type: 'array',
|
|
233
|
+
items: { $ref: '#/components/schemas/GitFile' },
|
|
234
|
+
},
|
|
231
235
|
hasChanges: { type: 'boolean' },
|
|
236
|
+
hasConflicts: { type: 'boolean' },
|
|
232
237
|
},
|
|
233
238
|
required: [
|
|
234
239
|
'branch',
|
|
@@ -237,7 +242,9 @@ export const schemas = {
|
|
|
237
242
|
'staged',
|
|
238
243
|
'unstaged',
|
|
239
244
|
'untracked',
|
|
245
|
+
'conflicted',
|
|
240
246
|
'hasChanges',
|
|
247
|
+
'hasConflicts',
|
|
241
248
|
],
|
|
242
249
|
},
|
|
243
250
|
GitFile: {
|
|
@@ -246,12 +253,29 @@ export const schemas = {
|
|
|
246
253
|
path: { type: 'string' },
|
|
247
254
|
status: {
|
|
248
255
|
type: 'string',
|
|
249
|
-
enum: [
|
|
256
|
+
enum: [
|
|
257
|
+
'modified',
|
|
258
|
+
'added',
|
|
259
|
+
'deleted',
|
|
260
|
+
'renamed',
|
|
261
|
+
'untracked',
|
|
262
|
+
'conflicted',
|
|
263
|
+
],
|
|
250
264
|
},
|
|
251
265
|
staged: { type: 'boolean' },
|
|
252
266
|
insertions: { type: 'integer' },
|
|
253
267
|
deletions: { type: 'integer' },
|
|
254
268
|
oldPath: { type: 'string' },
|
|
269
|
+
conflictType: {
|
|
270
|
+
type: 'string',
|
|
271
|
+
enum: [
|
|
272
|
+
'both-modified',
|
|
273
|
+
'deleted-by-us',
|
|
274
|
+
'deleted-by-them',
|
|
275
|
+
'both-added',
|
|
276
|
+
'both-deleted',
|
|
277
|
+
],
|
|
278
|
+
},
|
|
255
279
|
},
|
|
256
280
|
required: ['path', 'status', 'staged'],
|
|
257
281
|
},
|
|
@@ -0,0 +1,500 @@
|
|
|
1
|
+
import type { Hono } from 'hono';
|
|
2
|
+
import {
|
|
3
|
+
getAllAuth,
|
|
4
|
+
setAuth,
|
|
5
|
+
removeAuth,
|
|
6
|
+
ensureSetuWallet,
|
|
7
|
+
getSetuWallet,
|
|
8
|
+
importWallet,
|
|
9
|
+
loadConfig,
|
|
10
|
+
catalog,
|
|
11
|
+
getOnboardingComplete,
|
|
12
|
+
setOnboardingComplete,
|
|
13
|
+
authorize,
|
|
14
|
+
exchange,
|
|
15
|
+
authorizeWeb,
|
|
16
|
+
exchangeWeb,
|
|
17
|
+
authorizeOpenAI,
|
|
18
|
+
exchangeOpenAI,
|
|
19
|
+
type ProviderId,
|
|
20
|
+
} from '@agi-cli/sdk';
|
|
21
|
+
import { logger } from '@agi-cli/sdk';
|
|
22
|
+
import { serializeError } from '../runtime/errors/api-error.ts';
|
|
23
|
+
|
|
24
|
+
const oauthVerifiers = new Map<
|
|
25
|
+
string,
|
|
26
|
+
{ verifier: string; provider: string; createdAt: number; callbackUrl: string }
|
|
27
|
+
>();
|
|
28
|
+
|
|
29
|
+
setInterval(() => {
|
|
30
|
+
const now = Date.now();
|
|
31
|
+
for (const [key, value] of oauthVerifiers.entries()) {
|
|
32
|
+
if (now - value.createdAt > 10 * 60 * 1000) {
|
|
33
|
+
oauthVerifiers.delete(key);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
}, 60 * 1000);
|
|
37
|
+
|
|
38
|
+
export function registerAuthRoutes(app: Hono) {
|
|
39
|
+
app.get('/v1/auth/status', async (c) => {
|
|
40
|
+
try {
|
|
41
|
+
const projectRoot = process.cwd();
|
|
42
|
+
const auth = await getAllAuth(projectRoot);
|
|
43
|
+
const cfg = await loadConfig(projectRoot);
|
|
44
|
+
const onboardingComplete = await getOnboardingComplete(projectRoot);
|
|
45
|
+
const setuWallet = await getSetuWallet(projectRoot);
|
|
46
|
+
|
|
47
|
+
const providers: Record<
|
|
48
|
+
string,
|
|
49
|
+
{
|
|
50
|
+
configured: boolean;
|
|
51
|
+
type?: 'api' | 'oauth' | 'wallet';
|
|
52
|
+
label: string;
|
|
53
|
+
supportsOAuth: boolean;
|
|
54
|
+
modelCount: number;
|
|
55
|
+
costRange?: { min: number; max: number };
|
|
56
|
+
}
|
|
57
|
+
> = {};
|
|
58
|
+
|
|
59
|
+
for (const [id, entry] of Object.entries(catalog)) {
|
|
60
|
+
const providerAuth = auth[id as ProviderId];
|
|
61
|
+
const models = entry.models || [];
|
|
62
|
+
const costs = models
|
|
63
|
+
.map((m) => m.cost?.input)
|
|
64
|
+
.filter((c): c is number => c !== undefined);
|
|
65
|
+
|
|
66
|
+
providers[id] = {
|
|
67
|
+
configured: !!providerAuth,
|
|
68
|
+
type: providerAuth?.type,
|
|
69
|
+
label: entry.label || id,
|
|
70
|
+
supportsOAuth: id === 'anthropic' || id === 'openai',
|
|
71
|
+
modelCount: models.length,
|
|
72
|
+
costRange:
|
|
73
|
+
costs.length > 0
|
|
74
|
+
? {
|
|
75
|
+
min: Math.min(...costs),
|
|
76
|
+
max: Math.max(...costs),
|
|
77
|
+
}
|
|
78
|
+
: undefined,
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
return c.json({
|
|
83
|
+
onboardingComplete,
|
|
84
|
+
setu: setuWallet
|
|
85
|
+
? {
|
|
86
|
+
configured: true,
|
|
87
|
+
publicKey: setuWallet.publicKey,
|
|
88
|
+
}
|
|
89
|
+
: {
|
|
90
|
+
configured: false,
|
|
91
|
+
},
|
|
92
|
+
providers,
|
|
93
|
+
defaults: cfg.defaults,
|
|
94
|
+
});
|
|
95
|
+
} catch (error) {
|
|
96
|
+
logger.error('Failed to get auth status', error);
|
|
97
|
+
const errorResponse = serializeError(error);
|
|
98
|
+
return c.json(errorResponse, errorResponse.error.status || 500);
|
|
99
|
+
}
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
app.post('/v1/auth/setu/setup', async (c) => {
|
|
103
|
+
try {
|
|
104
|
+
const projectRoot = process.cwd();
|
|
105
|
+
const existing = await getSetuWallet(projectRoot);
|
|
106
|
+
const wallet = await ensureSetuWallet(projectRoot);
|
|
107
|
+
|
|
108
|
+
return c.json({
|
|
109
|
+
success: true,
|
|
110
|
+
publicKey: wallet.publicKey,
|
|
111
|
+
isNew: !existing,
|
|
112
|
+
});
|
|
113
|
+
} catch (error) {
|
|
114
|
+
logger.error('Failed to setup Setu wallet', error);
|
|
115
|
+
const errorResponse = serializeError(error);
|
|
116
|
+
return c.json(errorResponse, errorResponse.error.status || 500);
|
|
117
|
+
}
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
app.post('/v1/auth/setu/import', async (c) => {
|
|
121
|
+
try {
|
|
122
|
+
const { privateKey } = await c.req.json<{ privateKey: string }>();
|
|
123
|
+
|
|
124
|
+
if (!privateKey) {
|
|
125
|
+
return c.json({ error: 'Private key required' }, 400);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
try {
|
|
129
|
+
const wallet = importWallet(privateKey);
|
|
130
|
+
await setAuth(
|
|
131
|
+
'setu',
|
|
132
|
+
{ type: 'wallet', secret: privateKey },
|
|
133
|
+
undefined,
|
|
134
|
+
'global',
|
|
135
|
+
);
|
|
136
|
+
|
|
137
|
+
return c.json({
|
|
138
|
+
success: true,
|
|
139
|
+
publicKey: wallet.publicKey,
|
|
140
|
+
});
|
|
141
|
+
} catch {
|
|
142
|
+
return c.json({ error: 'Invalid private key format' }, 400);
|
|
143
|
+
}
|
|
144
|
+
} catch (error) {
|
|
145
|
+
logger.error('Failed to import Setu wallet', error);
|
|
146
|
+
const errorResponse = serializeError(error);
|
|
147
|
+
return c.json(errorResponse, errorResponse.error.status || 500);
|
|
148
|
+
}
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
app.post('/v1/auth/:provider', async (c) => {
|
|
152
|
+
try {
|
|
153
|
+
const provider = c.req.param('provider') as ProviderId;
|
|
154
|
+
const { apiKey } = await c.req.json<{ apiKey: string }>();
|
|
155
|
+
|
|
156
|
+
if (!catalog[provider]) {
|
|
157
|
+
return c.json({ error: 'Unknown provider' }, 400);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
if (!apiKey) {
|
|
161
|
+
return c.json({ error: 'API key required' }, 400);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
await setAuth(
|
|
165
|
+
provider,
|
|
166
|
+
{ type: 'api', key: apiKey },
|
|
167
|
+
undefined,
|
|
168
|
+
'global',
|
|
169
|
+
);
|
|
170
|
+
|
|
171
|
+
return c.json({ success: true, provider });
|
|
172
|
+
} catch (error) {
|
|
173
|
+
logger.error('Failed to add provider', error);
|
|
174
|
+
const errorResponse = serializeError(error);
|
|
175
|
+
return c.json(errorResponse, errorResponse.error.status || 500);
|
|
176
|
+
}
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
app.post('/v1/auth/:provider/oauth/url', async (c) => {
|
|
180
|
+
try {
|
|
181
|
+
const provider = c.req.param('provider');
|
|
182
|
+
const { mode = 'max' } = await c.req
|
|
183
|
+
.json<{ mode?: string }>()
|
|
184
|
+
.catch(() => ({}));
|
|
185
|
+
|
|
186
|
+
let url: string;
|
|
187
|
+
let verifier: string;
|
|
188
|
+
|
|
189
|
+
if (provider === 'anthropic') {
|
|
190
|
+
const result = await authorize(mode as 'max' | 'console');
|
|
191
|
+
url = result.url;
|
|
192
|
+
verifier = result.verifier;
|
|
193
|
+
} else if (provider === 'openai') {
|
|
194
|
+
return c.json(
|
|
195
|
+
{
|
|
196
|
+
error:
|
|
197
|
+
'OpenAI OAuth requires localhost callback. Use the redirect flow instead.',
|
|
198
|
+
},
|
|
199
|
+
400,
|
|
200
|
+
);
|
|
201
|
+
} else {
|
|
202
|
+
return c.json({ error: 'OAuth not supported for this provider' }, 400);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
const sessionId = crypto.randomUUID();
|
|
206
|
+
oauthVerifiers.set(sessionId, {
|
|
207
|
+
verifier,
|
|
208
|
+
provider,
|
|
209
|
+
createdAt: Date.now(),
|
|
210
|
+
callbackUrl: '',
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
return c.json({ url, sessionId, provider });
|
|
214
|
+
} catch (error) {
|
|
215
|
+
const message =
|
|
216
|
+
error instanceof Error ? error.message : 'OAuth initialization failed';
|
|
217
|
+
logger.error('OAuth URL generation failed', error);
|
|
218
|
+
return c.json({ error: message }, 500);
|
|
219
|
+
}
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
app.post('/v1/auth/:provider/oauth/exchange', async (c) => {
|
|
223
|
+
try {
|
|
224
|
+
const provider = c.req.param('provider');
|
|
225
|
+
const { code, sessionId } = await c.req.json<{
|
|
226
|
+
code: string;
|
|
227
|
+
sessionId: string;
|
|
228
|
+
}>();
|
|
229
|
+
|
|
230
|
+
if (!code || !sessionId) {
|
|
231
|
+
return c.json({ error: 'Code and sessionId required' }, 400);
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
if (!oauthVerifiers.has(sessionId)) {
|
|
235
|
+
return c.json({ error: 'Session expired or invalid' }, 400);
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
const { verifier } = oauthVerifiers.get(sessionId)!;
|
|
239
|
+
oauthVerifiers.delete(sessionId);
|
|
240
|
+
|
|
241
|
+
if (provider === 'anthropic') {
|
|
242
|
+
const tokens = await exchange(code, verifier);
|
|
243
|
+
await setAuth(
|
|
244
|
+
'anthropic',
|
|
245
|
+
{
|
|
246
|
+
type: 'oauth',
|
|
247
|
+
refresh: tokens.refresh,
|
|
248
|
+
access: tokens.access,
|
|
249
|
+
expires: tokens.expires,
|
|
250
|
+
},
|
|
251
|
+
undefined,
|
|
252
|
+
'global',
|
|
253
|
+
);
|
|
254
|
+
} else if (provider === 'openai') {
|
|
255
|
+
return c.json({ error: 'Use redirect flow for OpenAI' }, 400);
|
|
256
|
+
} else {
|
|
257
|
+
return c.json({ error: 'Unknown provider' }, 400);
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
return c.json({ success: true, provider });
|
|
261
|
+
} catch (error) {
|
|
262
|
+
const message =
|
|
263
|
+
error instanceof Error ? error.message : 'Token exchange failed';
|
|
264
|
+
logger.error('OAuth exchange failed', error);
|
|
265
|
+
return c.json({ error: message }, 500);
|
|
266
|
+
}
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
app.get('/v1/auth/:provider/oauth/start', async (c) => {
|
|
270
|
+
try {
|
|
271
|
+
const provider = c.req.param('provider');
|
|
272
|
+
const mode = c.req.query('mode') || 'max';
|
|
273
|
+
|
|
274
|
+
let url: string;
|
|
275
|
+
let verifier: string;
|
|
276
|
+
let callbackUrl = '';
|
|
277
|
+
|
|
278
|
+
if (provider === 'anthropic') {
|
|
279
|
+
const host = c.req.header('host') || 'localhost:3000';
|
|
280
|
+
const protocol = c.req.header('x-forwarded-proto') || 'http';
|
|
281
|
+
callbackUrl = `${protocol}://${host}/v1/auth/${provider}/oauth/callback`;
|
|
282
|
+
const result = authorizeWeb(mode as 'max' | 'console', callbackUrl);
|
|
283
|
+
url = result.url;
|
|
284
|
+
verifier = result.verifier;
|
|
285
|
+
} else if (provider === 'openai') {
|
|
286
|
+
const result = await authorizeOpenAI();
|
|
287
|
+
url = result.url;
|
|
288
|
+
verifier = result.verifier;
|
|
289
|
+
callbackUrl = 'localhost';
|
|
290
|
+
result
|
|
291
|
+
.waitForCallback()
|
|
292
|
+
.then(async (code) => {
|
|
293
|
+
const tokens = await exchangeOpenAI(code, verifier);
|
|
294
|
+
await setAuth(
|
|
295
|
+
'openai',
|
|
296
|
+
{
|
|
297
|
+
type: 'oauth',
|
|
298
|
+
refresh: tokens.refresh,
|
|
299
|
+
access: tokens.access,
|
|
300
|
+
expires: tokens.expires,
|
|
301
|
+
accountId: tokens.accountId,
|
|
302
|
+
idToken: tokens.idToken,
|
|
303
|
+
},
|
|
304
|
+
undefined,
|
|
305
|
+
'global',
|
|
306
|
+
);
|
|
307
|
+
result.close();
|
|
308
|
+
})
|
|
309
|
+
.catch(() => {
|
|
310
|
+
result.close();
|
|
311
|
+
});
|
|
312
|
+
} else {
|
|
313
|
+
return c.json({ error: 'OAuth not supported for this provider' }, 400);
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
const sessionId = crypto.randomUUID();
|
|
317
|
+
oauthVerifiers.set(sessionId, {
|
|
318
|
+
verifier,
|
|
319
|
+
provider,
|
|
320
|
+
createdAt: Date.now(),
|
|
321
|
+
callbackUrl,
|
|
322
|
+
});
|
|
323
|
+
|
|
324
|
+
c.header(
|
|
325
|
+
'Set-Cookie',
|
|
326
|
+
`oauth_session=${sessionId}; Path=/; HttpOnly; SameSite=Lax; Max-Age=600`,
|
|
327
|
+
);
|
|
328
|
+
|
|
329
|
+
return c.redirect(url);
|
|
330
|
+
} catch (error) {
|
|
331
|
+
const message =
|
|
332
|
+
error instanceof Error ? error.message : 'OAuth initialization failed';
|
|
333
|
+
logger.error('OAuth start failed', error);
|
|
334
|
+
return c.json({ error: message }, 500);
|
|
335
|
+
}
|
|
336
|
+
});
|
|
337
|
+
|
|
338
|
+
app.get('/v1/auth/:provider/oauth/callback', async (c) => {
|
|
339
|
+
try {
|
|
340
|
+
const provider = c.req.param('provider');
|
|
341
|
+
const code = c.req.query('code');
|
|
342
|
+
const fragment = c.req.query('fragment');
|
|
343
|
+
|
|
344
|
+
const cookies = c.req.header('Cookie') || '';
|
|
345
|
+
const sessionMatch = cookies.match(/oauth_session=([^;]+)/);
|
|
346
|
+
const sessionId = sessionMatch?.[1];
|
|
347
|
+
|
|
348
|
+
if (!sessionId || !oauthVerifiers.has(sessionId)) {
|
|
349
|
+
return c.html(
|
|
350
|
+
'<html><body><h1>Session expired</h1><p>Please close this window and try again.</p><script>setTimeout(() => window.close(), 3000);</script></body></html>',
|
|
351
|
+
);
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
const { verifier, callbackUrl } = oauthVerifiers.get(sessionId)!;
|
|
355
|
+
oauthVerifiers.delete(sessionId);
|
|
356
|
+
|
|
357
|
+
if (provider === 'anthropic') {
|
|
358
|
+
const fullCode = fragment ? `${code}#${fragment}` : code;
|
|
359
|
+
const tokens = await exchangeWeb(fullCode!, verifier, callbackUrl);
|
|
360
|
+
|
|
361
|
+
await setAuth(
|
|
362
|
+
'anthropic',
|
|
363
|
+
{
|
|
364
|
+
type: 'oauth',
|
|
365
|
+
refresh: tokens.refresh,
|
|
366
|
+
access: tokens.access,
|
|
367
|
+
expires: tokens.expires,
|
|
368
|
+
},
|
|
369
|
+
undefined,
|
|
370
|
+
'global',
|
|
371
|
+
);
|
|
372
|
+
} else if (provider === 'openai') {
|
|
373
|
+
return c.html(
|
|
374
|
+
'<html><body><h1>OpenAI uses localhost callback</h1><p>This route is not used for OpenAI. Please close this window.</p><script>setTimeout(() => window.close(), 3000);</script></body></html>',
|
|
375
|
+
);
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
return c.html(`
|
|
379
|
+
<html>
|
|
380
|
+
<head>
|
|
381
|
+
<title>Connected!</title>
|
|
382
|
+
<style>
|
|
383
|
+
body {
|
|
384
|
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
|
385
|
+
display: flex;
|
|
386
|
+
justify-content: center;
|
|
387
|
+
align-items: center;
|
|
388
|
+
height: 100vh;
|
|
389
|
+
margin: 0;
|
|
390
|
+
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
|
391
|
+
color: white;
|
|
392
|
+
}
|
|
393
|
+
.container {
|
|
394
|
+
text-align: center;
|
|
395
|
+
padding: 2rem;
|
|
396
|
+
background: rgba(255,255,255,0.1);
|
|
397
|
+
border-radius: 16px;
|
|
398
|
+
backdrop-filter: blur(10px);
|
|
399
|
+
}
|
|
400
|
+
.checkmark {
|
|
401
|
+
font-size: 4rem;
|
|
402
|
+
margin-bottom: 1rem;
|
|
403
|
+
}
|
|
404
|
+
h1 { margin: 0 0 0.5rem 0; }
|
|
405
|
+
p { margin: 0; opacity: 0.9; }
|
|
406
|
+
</style>
|
|
407
|
+
</head>
|
|
408
|
+
<body>
|
|
409
|
+
<div class="container">
|
|
410
|
+
<div class="checkmark">✓</div>
|
|
411
|
+
<h1>Connected!</h1>
|
|
412
|
+
<p>You can close this window.</p>
|
|
413
|
+
</div>
|
|
414
|
+
<script>
|
|
415
|
+
if (window.opener) {
|
|
416
|
+
window.opener.postMessage({ type: 'oauth-success', provider: '${provider}' }, '*');
|
|
417
|
+
}
|
|
418
|
+
setTimeout(() => window.close(), 1500);
|
|
419
|
+
</script>
|
|
420
|
+
</body>
|
|
421
|
+
</html>
|
|
422
|
+
`);
|
|
423
|
+
} catch (error) {
|
|
424
|
+
const message =
|
|
425
|
+
error instanceof Error ? error.message : 'Authentication failed';
|
|
426
|
+
logger.error('OAuth callback failed', error);
|
|
427
|
+
return c.html(`
|
|
428
|
+
<html>
|
|
429
|
+
<head>
|
|
430
|
+
<title>Error</title>
|
|
431
|
+
<style>
|
|
432
|
+
body {
|
|
433
|
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
|
434
|
+
display: flex;
|
|
435
|
+
justify-content: center;
|
|
436
|
+
align-items: center;
|
|
437
|
+
height: 100vh;
|
|
438
|
+
margin: 0;
|
|
439
|
+
background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%);
|
|
440
|
+
color: white;
|
|
441
|
+
}
|
|
442
|
+
.container {
|
|
443
|
+
text-align: center;
|
|
444
|
+
padding: 2rem;
|
|
445
|
+
background: rgba(255,255,255,0.1);
|
|
446
|
+
border-radius: 16px;
|
|
447
|
+
backdrop-filter: blur(10px);
|
|
448
|
+
}
|
|
449
|
+
.icon { font-size: 4rem; margin-bottom: 1rem; }
|
|
450
|
+
h1 { margin: 0 0 0.5rem 0; }
|
|
451
|
+
p { margin: 0; opacity: 0.9; }
|
|
452
|
+
</style>
|
|
453
|
+
</head>
|
|
454
|
+
<body>
|
|
455
|
+
<div class="container">
|
|
456
|
+
<div class="icon">✗</div>
|
|
457
|
+
<h1>Error</h1>
|
|
458
|
+
<p>${message}</p>
|
|
459
|
+
</div>
|
|
460
|
+
<script>
|
|
461
|
+
if (window.opener) {
|
|
462
|
+
window.opener.postMessage({ type: 'oauth-error', provider: '${c.req.param('provider')}', error: '${message}' }, '*');
|
|
463
|
+
}
|
|
464
|
+
setTimeout(() => window.close(), 3000);
|
|
465
|
+
</script>
|
|
466
|
+
</body>
|
|
467
|
+
</html>
|
|
468
|
+
`);
|
|
469
|
+
}
|
|
470
|
+
});
|
|
471
|
+
|
|
472
|
+
app.post('/v1/auth/onboarding/complete', async (c) => {
|
|
473
|
+
try {
|
|
474
|
+
await setOnboardingComplete();
|
|
475
|
+
return c.json({ success: true });
|
|
476
|
+
} catch (error) {
|
|
477
|
+
logger.error('Failed to complete onboarding', error);
|
|
478
|
+
const errorResponse = serializeError(error);
|
|
479
|
+
return c.json(errorResponse, errorResponse.error.status || 500);
|
|
480
|
+
}
|
|
481
|
+
});
|
|
482
|
+
|
|
483
|
+
app.delete('/v1/auth/:provider', async (c) => {
|
|
484
|
+
try {
|
|
485
|
+
const provider = c.req.param('provider') as ProviderId;
|
|
486
|
+
|
|
487
|
+
if (!catalog[provider]) {
|
|
488
|
+
return c.json({ error: 'Unknown provider' }, 400);
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
await removeAuth(provider, undefined, 'global');
|
|
492
|
+
|
|
493
|
+
return c.json({ success: true, provider });
|
|
494
|
+
} catch (error) {
|
|
495
|
+
logger.error('Failed to remove provider', error);
|
|
496
|
+
const errorResponse = serializeError(error);
|
|
497
|
+
return c.json(errorResponse, errorResponse.error.status || 500);
|
|
498
|
+
}
|
|
499
|
+
});
|
|
500
|
+
}
|
package/src/routes/git/status.ts
CHANGED
|
@@ -36,7 +36,7 @@ export function registerStatusRoute(app: Hono) {
|
|
|
36
36
|
{ cwd: gitRoot },
|
|
37
37
|
);
|
|
38
38
|
|
|
39
|
-
const { staged, unstaged, untracked } = parseGitStatus(
|
|
39
|
+
const { staged, unstaged, untracked, conflicted } = parseGitStatus(
|
|
40
40
|
statusOutput,
|
|
41
41
|
gitRoot,
|
|
42
42
|
);
|
|
@@ -46,7 +46,12 @@ export function registerStatusRoute(app: Hono) {
|
|
|
46
46
|
const branch = await getCurrentBranch(gitRoot);
|
|
47
47
|
|
|
48
48
|
const hasChanges =
|
|
49
|
-
staged.length > 0 ||
|
|
49
|
+
staged.length > 0 ||
|
|
50
|
+
unstaged.length > 0 ||
|
|
51
|
+
untracked.length > 0 ||
|
|
52
|
+
conflicted.length > 0;
|
|
53
|
+
|
|
54
|
+
const hasConflicts = conflicted.length > 0;
|
|
50
55
|
|
|
51
56
|
return c.json({
|
|
52
57
|
status: 'ok',
|
|
@@ -59,7 +64,9 @@ export function registerStatusRoute(app: Hono) {
|
|
|
59
64
|
staged,
|
|
60
65
|
unstaged,
|
|
61
66
|
untracked,
|
|
67
|
+
conflicted,
|
|
62
68
|
hasChanges,
|
|
69
|
+
hasConflicts,
|
|
63
70
|
},
|
|
64
71
|
});
|
|
65
72
|
} catch (error) {
|
package/src/routes/git/types.ts
CHANGED
|
@@ -1,12 +1,24 @@
|
|
|
1
1
|
export interface GitFile {
|
|
2
2
|
path: string;
|
|
3
3
|
absPath: string;
|
|
4
|
-
status:
|
|
4
|
+
status:
|
|
5
|
+
| 'modified'
|
|
6
|
+
| 'added'
|
|
7
|
+
| 'deleted'
|
|
8
|
+
| 'renamed'
|
|
9
|
+
| 'untracked'
|
|
10
|
+
| 'conflicted';
|
|
5
11
|
staged: boolean;
|
|
6
12
|
insertions?: number;
|
|
7
13
|
deletions?: number;
|
|
8
14
|
oldPath?: string;
|
|
9
15
|
isNew: boolean;
|
|
16
|
+
conflictType?:
|
|
17
|
+
| 'both-modified'
|
|
18
|
+
| 'deleted-by-us'
|
|
19
|
+
| 'deleted-by-them'
|
|
20
|
+
| 'both-added'
|
|
21
|
+
| 'both-deleted';
|
|
10
22
|
}
|
|
11
23
|
|
|
12
24
|
export interface GitRoot {
|
package/src/routes/git/utils.ts
CHANGED
|
@@ -121,6 +121,25 @@ function getStatusFromCodeV2(code: string): GitFile['status'] {
|
|
|
121
121
|
}
|
|
122
122
|
}
|
|
123
123
|
|
|
124
|
+
function getConflictType(xy: string): GitFile['conflictType'] {
|
|
125
|
+
switch (xy) {
|
|
126
|
+
case 'UU':
|
|
127
|
+
return 'both-modified';
|
|
128
|
+
case 'AA':
|
|
129
|
+
return 'both-added';
|
|
130
|
+
case 'DD':
|
|
131
|
+
return 'both-deleted';
|
|
132
|
+
case 'DU':
|
|
133
|
+
case 'UD':
|
|
134
|
+
return 'deleted-by-us';
|
|
135
|
+
case 'AU':
|
|
136
|
+
case 'UA':
|
|
137
|
+
return 'deleted-by-them';
|
|
138
|
+
default:
|
|
139
|
+
return 'both-modified';
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
124
143
|
export function parseGitStatus(
|
|
125
144
|
statusOutput: string,
|
|
126
145
|
gitRoot: string,
|
|
@@ -128,11 +147,13 @@ export function parseGitStatus(
|
|
|
128
147
|
staged: GitFile[];
|
|
129
148
|
unstaged: GitFile[];
|
|
130
149
|
untracked: GitFile[];
|
|
150
|
+
conflicted: GitFile[];
|
|
131
151
|
} {
|
|
132
152
|
const lines = statusOutput.trim().split('\n').filter(Boolean);
|
|
133
153
|
const staged: GitFile[] = [];
|
|
134
154
|
const unstaged: GitFile[] = [];
|
|
135
155
|
const untracked: GitFile[] = [];
|
|
156
|
+
const conflicted: GitFile[] = [];
|
|
136
157
|
|
|
137
158
|
for (const line of lines) {
|
|
138
159
|
if (line.startsWith('1 ') || line.startsWith('2 ')) {
|
|
@@ -174,10 +195,26 @@ export function parseGitStatus(
|
|
|
174
195
|
staged: false,
|
|
175
196
|
isNew: true,
|
|
176
197
|
});
|
|
198
|
+
} else if (line.startsWith('u ')) {
|
|
199
|
+
const parts = line.split(' ');
|
|
200
|
+
if (parts.length < 11) continue;
|
|
201
|
+
|
|
202
|
+
const xy = parts[1];
|
|
203
|
+
const path = parts.slice(10).join(' ');
|
|
204
|
+
const absPath = join(gitRoot, path);
|
|
205
|
+
|
|
206
|
+
conflicted.push({
|
|
207
|
+
path,
|
|
208
|
+
absPath,
|
|
209
|
+
status: 'conflicted',
|
|
210
|
+
staged: false,
|
|
211
|
+
isNew: false,
|
|
212
|
+
conflictType: getConflictType(xy),
|
|
213
|
+
});
|
|
177
214
|
}
|
|
178
215
|
}
|
|
179
216
|
|
|
180
|
-
return { staged, unstaged, untracked };
|
|
217
|
+
return { staged, unstaged, untracked, conflicted };
|
|
181
218
|
}
|
|
182
219
|
|
|
183
220
|
export async function getAheadBehind(
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { publish } from '../../events/bus.ts';
|
|
2
|
+
import { debugLog } from '../debug/index.ts';
|
|
2
3
|
|
|
3
4
|
export type ToolApprovalMode = 'auto' | 'dangerous' | 'all';
|
|
4
5
|
|
|
@@ -49,7 +50,7 @@ export async function requestApproval(
|
|
|
49
50
|
args: unknown,
|
|
50
51
|
timeoutMs = 120000,
|
|
51
52
|
): Promise<boolean> {
|
|
52
|
-
|
|
53
|
+
debugLog('[approval] requestApproval called', {
|
|
53
54
|
sessionId,
|
|
54
55
|
messageId,
|
|
55
56
|
callId,
|
|
@@ -67,7 +68,7 @@ export async function requestApproval(
|
|
|
67
68
|
};
|
|
68
69
|
|
|
69
70
|
pendingApprovals.set(callId, approval);
|
|
70
|
-
|
|
71
|
+
debugLog(
|
|
71
72
|
'[approval] Added to pendingApprovals, count:',
|
|
72
73
|
pendingApprovals.size,
|
|
73
74
|
);
|
|
@@ -106,7 +107,7 @@ export function resolveApproval(
|
|
|
106
107
|
callId: string,
|
|
107
108
|
approved: boolean,
|
|
108
109
|
): { ok: boolean; error?: string } {
|
|
109
|
-
|
|
110
|
+
debugLog('[approval] resolveApproval called', {
|
|
110
111
|
callId,
|
|
111
112
|
approved,
|
|
112
113
|
pendingCount: pendingApprovals.size,
|
|
@@ -114,7 +115,7 @@ export function resolveApproval(
|
|
|
114
115
|
});
|
|
115
116
|
const approval = pendingApprovals.get(callId);
|
|
116
117
|
if (!approval) {
|
|
117
|
-
|
|
118
|
+
debugLog('[approval] No pending approval found for callId:', callId);
|
|
118
119
|
return { ok: false, error: 'No pending approval found for this callId' };
|
|
119
120
|
}
|
|
120
121
|
|