@thegitai/cli 1.0.0-beta.9 → 1.0.0-preview.10
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/README.md +49 -3
- package/dist/bin/ai.js +83 -197
- package/dist/parsers/NOTICE +18 -0
- package/dist/src/agent-mode.js +5 -0
- package/dist/src/api/auth.js +4 -4
- package/dist/src/api/browser-login.js +72 -19
- package/dist/src/api/chat.js +182 -35
- package/dist/src/api/http.js +65 -4
- package/dist/src/api/models.js +33 -22
- package/dist/src/artifact-policy.js +3 -0
- package/dist/src/background-jobs.js +410 -0
- package/dist/src/cli-args.js +0 -5
- package/dist/src/client-environment.js +2 -0
- package/dist/src/colors.js +50 -0
- package/dist/src/core/clipboard.js +19 -0
- package/dist/src/core/image-path-extractor.js +144 -0
- package/dist/src/executor.js +48 -12
- package/dist/src/help-text.js +30 -13
- package/dist/src/patcher.js +97 -12
- package/dist/src/project-index.js +13 -1
- package/dist/src/project-orientation.js +99 -0
- package/dist/src/scanner.js +50 -12
- package/dist/src/scratch-dir.js +75 -0
- package/dist/src/secret-preview.js +0 -10
- package/dist/src/session-safety.js +0 -19
- package/dist/src/session-store.js +52 -21
- package/dist/src/session.js +8 -0
- package/dist/src/todo-list.js +106 -0
- package/dist/src/tool-executor.js +194 -21
- package/dist/src/tools/delete-file.js +23 -5
- package/dist/src/tools/index.js +6 -0
- package/dist/src/tools/patch-file.js +33 -7
- package/dist/src/tools/path-suggest.js +81 -8
- package/dist/src/tools/read-document.js +2 -2
- package/dist/src/tools/read-file.js +17 -8
- package/dist/src/tools/replace-document-text.js +10 -12
- package/dist/src/tools/restore-checkpoint.js +1 -1
- package/dist/src/tools/run-command.js +109 -24
- package/dist/src/tools/run-node-script.js +27 -5
- package/dist/src/tools/shell-job-kill.js +48 -0
- package/dist/src/tools/shell-job-output.js +51 -0
- package/dist/src/tools/str-replace.js +33 -7
- package/dist/src/tools/undo-edit.js +1 -1
- package/dist/src/tools/update-todos.js +27 -0
- package/dist/src/tools/write-file.js +26 -6
- package/dist/src/tree-sitter-runtime.js +8 -1
- package/dist/src/turn-failure-marker.js +11 -0
- package/dist/src/ui/prompt-history-store.js +1 -1
- package/dist/src/ui/repl.js +500 -71
- package/dist/src/ui/tui/bridge.js +3 -4
- package/dist/src/ui/tui/build-frame.js +393 -100
- package/dist/src/ui/tui/markdown-render.js +72 -73
- package/dist/src/ui/tui/shell-input.js +75 -17
- package/dist/src/ui/tui/terminal-title.js +84 -0
- package/dist/src/ui/tui/terminal-writes.js +48 -0
- package/dist/src/ui/tui/text.js +158 -4
- package/dist/src/utils.js +9 -0
- package/dist/src/version.js +0 -6
- package/dist/vendor/web-tree-sitter/LICENSE +21 -0
- package/dist/vendor/web-tree-sitter/NOTICE +13 -0
- package/dist/vendor/web-tree-sitter/web-tree-sitter.cjs +4063 -0
- package/dist/vendor/web-tree-sitter/web-tree-sitter.wasm +0 -0
- package/package.json +27 -16
- package/dist/src/markdown-renderer.js +0 -112
package/dist/src/api/chat.js
CHANGED
|
@@ -1,8 +1,12 @@
|
|
|
1
|
+
import { drainBackgroundJobNotifications } from '../background-jobs.js';
|
|
1
2
|
import { createPromptCheckpoint, sanitizeSessionSafetyForServer, } from '../session-safety.js';
|
|
2
3
|
import { applySessionSnapshot, snapshotFromSession, } from '../session-store.js';
|
|
3
4
|
import { executeLocalToolCall } from '../tool-executor.js';
|
|
4
5
|
import { createTraceContext, normalizeServerUrl, readErrorResponse, } from './http.js';
|
|
5
6
|
import { collectClientEnvironment } from '../client-environment.js';
|
|
7
|
+
import { collectProjectOrientation } from '../project-orientation.js';
|
|
8
|
+
import { autoAttachImages } from '../core/image-path-extractor.js';
|
|
9
|
+
import { formatTurnFailureMarker } from '../turn-failure-marker.js';
|
|
6
10
|
export class TurnCancelledError extends Error {
|
|
7
11
|
name = 'TurnCancelledError';
|
|
8
12
|
constructor(message = 'Turn cancelled.') {
|
|
@@ -48,6 +52,7 @@ function parseSseBlock(block) {
|
|
|
48
52
|
return { event, data: text };
|
|
49
53
|
}
|
|
50
54
|
}
|
|
55
|
+
let toolStateSeqCounter = 0;
|
|
51
56
|
function toolStateFromSession(session) {
|
|
52
57
|
return {
|
|
53
58
|
autoYes: session.autoYes,
|
|
@@ -63,6 +68,11 @@ function snapshotForServer(session) {
|
|
|
63
68
|
snapshot.clientState.safety = sanitizeSessionSafetyForServer(snapshot.clientState.safety);
|
|
64
69
|
return snapshot;
|
|
65
70
|
}
|
|
71
|
+
function imageAttachmentsForServer(attachments) {
|
|
72
|
+
return (attachments ?? []).map(({ filePath, ...attachment }) => attachment.source === 'file' && filePath
|
|
73
|
+
? { ...attachment, filePath }
|
|
74
|
+
: attachment);
|
|
75
|
+
}
|
|
66
76
|
function userHistoryText(entry) {
|
|
67
77
|
return (entry.parts ?? [])
|
|
68
78
|
.map((part) => (typeof part?.text === 'string' ? part.text : ''))
|
|
@@ -85,16 +95,26 @@ export function preserveCancelledTurnInput(session, input) {
|
|
|
85
95
|
return;
|
|
86
96
|
break;
|
|
87
97
|
}
|
|
88
|
-
session.history.push({
|
|
98
|
+
session.history.push({
|
|
99
|
+
role: 'user',
|
|
100
|
+
parts: [{ text }],
|
|
101
|
+
kind: 'turnStart',
|
|
102
|
+
userInput: text,
|
|
103
|
+
});
|
|
89
104
|
}
|
|
90
105
|
function preserveFailedTurnInput(session, input, category) {
|
|
91
106
|
const text = input.trim();
|
|
92
107
|
if (!text)
|
|
93
108
|
return;
|
|
94
|
-
session.history.push({
|
|
109
|
+
session.history.push({
|
|
110
|
+
role: 'user',
|
|
111
|
+
parts: [{ text }],
|
|
112
|
+
kind: 'turnStart',
|
|
113
|
+
userInput: text,
|
|
114
|
+
});
|
|
95
115
|
session.history.push({
|
|
96
116
|
role: 'model',
|
|
97
|
-
parts: [{ text:
|
|
117
|
+
parts: [{ text: formatTurnFailureMarker(category) }],
|
|
98
118
|
});
|
|
99
119
|
}
|
|
100
120
|
function historyHasToolCall(session, callId) {
|
|
@@ -136,17 +156,50 @@ function publicStatusMessage(data) {
|
|
|
136
156
|
: 'tool';
|
|
137
157
|
if (event.phase === 'thinking')
|
|
138
158
|
return 'Thinking...';
|
|
159
|
+
if (event.phase === 'analyzing_image') {
|
|
160
|
+
return (event.imageCount ?? 1) > 1 ? 'Analyzing images...' : 'Analyzing image...';
|
|
161
|
+
}
|
|
139
162
|
if (event.phase === 'running_tool')
|
|
140
163
|
return `Running ${toolName}...`;
|
|
141
164
|
if (event.phase === 'waiting_for_tool')
|
|
142
165
|
return `Running ${toolName} locally...`;
|
|
143
166
|
return null;
|
|
144
167
|
}
|
|
168
|
+
function normalizeShellJobToolCall(call) {
|
|
169
|
+
if (call.name !== 'shell_job_output' && call.name !== 'shell_job_kill') {
|
|
170
|
+
return call;
|
|
171
|
+
}
|
|
172
|
+
const args = call.args && typeof call.args === 'object' && !Array.isArray(call.args)
|
|
173
|
+
? { ...call.args }
|
|
174
|
+
: {};
|
|
175
|
+
let changed = false;
|
|
176
|
+
if (args.job_id === undefined) {
|
|
177
|
+
const alias = args.jobId ?? args.id;
|
|
178
|
+
if (alias !== undefined) {
|
|
179
|
+
args.job_id = alias;
|
|
180
|
+
delete args.jobId;
|
|
181
|
+
delete args.id;
|
|
182
|
+
changed = true;
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
if (call.name === 'shell_job_output' && args.wait_ms === undefined) {
|
|
186
|
+
const alias = args.waitMs ?? args.wait ?? args.wait_millis;
|
|
187
|
+
if (alias !== undefined) {
|
|
188
|
+
args.wait_ms = alias;
|
|
189
|
+
delete args.waitMs;
|
|
190
|
+
delete args.wait;
|
|
191
|
+
delete args.wait_millis;
|
|
192
|
+
changed = true;
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
return changed ? { ...call, args } : call;
|
|
196
|
+
}
|
|
145
197
|
async function postToolResult({ config, turnId, event, result, session, fetchImpl, traceId, }) {
|
|
146
198
|
const payload = {
|
|
147
199
|
toolCallId: event.call.id,
|
|
148
200
|
result,
|
|
149
201
|
toolState: toolStateFromSession(session),
|
|
202
|
+
toolStateSeq: ++toolStateSeqCounter,
|
|
150
203
|
};
|
|
151
204
|
const trace = createTraceContext(traceId);
|
|
152
205
|
const response = await fetchImpl(`${normalizeServerUrl(config.serverUrl)}/v1/chat/turn/${encodeURIComponent(turnId)}/tool-result`, {
|
|
@@ -165,6 +218,29 @@ async function postToolResult({ config, turnId, event, result, session, fetchImp
|
|
|
165
218
|
throw await readErrorResponse(response, trace.traceId);
|
|
166
219
|
}
|
|
167
220
|
}
|
|
221
|
+
const turnIdOverrides = new WeakMap();
|
|
222
|
+
function enterServerTurnId(session, serverSessionTurnId) {
|
|
223
|
+
const active = turnIdOverrides.get(session);
|
|
224
|
+
if (active) {
|
|
225
|
+
active.depth += 1;
|
|
226
|
+
return;
|
|
227
|
+
}
|
|
228
|
+
turnIdOverrides.set(session, {
|
|
229
|
+
previousTurnId: session.turnState.id,
|
|
230
|
+
depth: 1,
|
|
231
|
+
});
|
|
232
|
+
session.turnState.id = serverSessionTurnId;
|
|
233
|
+
}
|
|
234
|
+
function exitServerTurnId(session) {
|
|
235
|
+
const active = turnIdOverrides.get(session);
|
|
236
|
+
if (!active)
|
|
237
|
+
return;
|
|
238
|
+
active.depth -= 1;
|
|
239
|
+
if (active.depth === 0) {
|
|
240
|
+
session.turnState.id = active.previousTurnId;
|
|
241
|
+
turnIdOverrides.delete(session);
|
|
242
|
+
}
|
|
243
|
+
}
|
|
168
244
|
async function executeAndPostToolResult({ config, projectIndex, session, event, input, fetchImpl, signal, traceId, }) {
|
|
169
245
|
const turnId = String(event?.turnId ?? '').trim();
|
|
170
246
|
if (!turnId || !event?.call?.id || !event.call.name) {
|
|
@@ -173,17 +249,17 @@ async function executeAndPostToolResult({ config, projectIndex, session, event,
|
|
|
173
249
|
if (signal?.aborted) {
|
|
174
250
|
throw new TurnCancelledError();
|
|
175
251
|
}
|
|
176
|
-
const previousTurnId = session.turnState.id;
|
|
177
252
|
const serverSessionTurnId = String(event.sessionTurnId ?? '').trim();
|
|
178
253
|
if (serverSessionTurnId) {
|
|
179
|
-
session
|
|
254
|
+
enterServerTurnId(session, serverSessionTurnId);
|
|
180
255
|
if (!session.clientState.safety.checkpoints.some((checkpoint) => checkpoint.turnId === serverSessionTurnId)) {
|
|
181
256
|
createPromptCheckpoint(session.clientState.safety, 'prompt boundary', serverSessionTurnId);
|
|
182
257
|
}
|
|
183
258
|
}
|
|
184
259
|
try {
|
|
185
|
-
const
|
|
186
|
-
|
|
260
|
+
const call = normalizeShellJobToolCall(event.call);
|
|
261
|
+
const rawResult = await executeLocalToolCall({ projectIndex }, session, call);
|
|
262
|
+
preserveCancelledTurnToolResult(session, input, { ...event, call }, rawResult);
|
|
187
263
|
if (signal?.aborted) {
|
|
188
264
|
throw new TurnCancelledError();
|
|
189
265
|
}
|
|
@@ -198,7 +274,9 @@ async function executeAndPostToolResult({ config, projectIndex, session, event,
|
|
|
198
274
|
});
|
|
199
275
|
}
|
|
200
276
|
finally {
|
|
201
|
-
|
|
277
|
+
if (serverSessionTurnId) {
|
|
278
|
+
exitServerTurnId(session);
|
|
279
|
+
}
|
|
202
280
|
}
|
|
203
281
|
}
|
|
204
282
|
async function consumeTurnStream({ response, config, projectIndex, session, input, fetchImpl, signal, traceId, }) {
|
|
@@ -211,8 +289,40 @@ async function consumeTurnStream({ response, config, projectIndex, session, inpu
|
|
|
211
289
|
const finalResult = {
|
|
212
290
|
current: null,
|
|
213
291
|
};
|
|
292
|
+
const pendingParallelTools = [];
|
|
293
|
+
let firstParallelFailure = null;
|
|
294
|
+
let rejectOnParallelFailure = null;
|
|
295
|
+
const parallelToolFailure = new Promise((_, reject) => {
|
|
296
|
+
rejectOnParallelFailure = reject;
|
|
297
|
+
});
|
|
298
|
+
parallelToolFailure.catch(() => { });
|
|
299
|
+
function recordParallelFailure(error) {
|
|
300
|
+
const failure = error ?? new Error('Local tool execution failed.');
|
|
301
|
+
if (firstParallelFailure == null) {
|
|
302
|
+
firstParallelFailure = failure;
|
|
303
|
+
rejectOnParallelFailure?.(failure);
|
|
304
|
+
}
|
|
305
|
+
return failure;
|
|
306
|
+
}
|
|
307
|
+
async function drainParallelTools() {
|
|
308
|
+
if (!pendingParallelTools.length)
|
|
309
|
+
return;
|
|
310
|
+
const pending = pendingParallelTools.splice(0);
|
|
311
|
+
const outcomes = await Promise.all(pending);
|
|
312
|
+
for (const outcome of outcomes) {
|
|
313
|
+
if (outcome != null)
|
|
314
|
+
throw outcome;
|
|
315
|
+
}
|
|
316
|
+
}
|
|
214
317
|
async function handleEvent(event) {
|
|
215
318
|
if (event.event === 'status') {
|
|
319
|
+
const data = event.data;
|
|
320
|
+
if (data?.phase === 'analyzing_image') {
|
|
321
|
+
session.onImageAnalysis?.(Math.max(1, Number(data.imageCount ?? 1) || 1));
|
|
322
|
+
}
|
|
323
|
+
else if (data?.phase) {
|
|
324
|
+
session.onImageAnalysis?.(0);
|
|
325
|
+
}
|
|
216
326
|
const message = publicStatusMessage(event.data);
|
|
217
327
|
if (message)
|
|
218
328
|
session.onStatus(message);
|
|
@@ -222,11 +332,26 @@ async function consumeTurnStream({ response, config, projectIndex, session, inpu
|
|
|
222
332
|
return;
|
|
223
333
|
}
|
|
224
334
|
if (event.event === 'tool-call') {
|
|
335
|
+
const data = event.data;
|
|
336
|
+
if (data?.parallelSafe === true) {
|
|
337
|
+
pendingParallelTools.push(executeAndPostToolResult({
|
|
338
|
+
config,
|
|
339
|
+
projectIndex,
|
|
340
|
+
session,
|
|
341
|
+
event: data,
|
|
342
|
+
input,
|
|
343
|
+
fetchImpl,
|
|
344
|
+
signal,
|
|
345
|
+
traceId,
|
|
346
|
+
}).then(() => null, (error) => recordParallelFailure(error)));
|
|
347
|
+
return;
|
|
348
|
+
}
|
|
349
|
+
await drainParallelTools();
|
|
225
350
|
await executeAndPostToolResult({
|
|
226
351
|
config,
|
|
227
352
|
projectIndex,
|
|
228
353
|
session,
|
|
229
|
-
event:
|
|
354
|
+
event: data,
|
|
230
355
|
input,
|
|
231
356
|
fetchImpl,
|
|
232
357
|
signal,
|
|
@@ -242,10 +367,12 @@ async function consumeTurnStream({ response, config, projectIndex, session, inpu
|
|
|
242
367
|
return;
|
|
243
368
|
}
|
|
244
369
|
if (event.event === 'result') {
|
|
370
|
+
await drainParallelTools();
|
|
245
371
|
finalResult.current = event.data;
|
|
246
372
|
return;
|
|
247
373
|
}
|
|
248
374
|
if (event.event === 'cancelled' || event.event === 'error') {
|
|
375
|
+
await drainParallelTools().catch(() => { });
|
|
249
376
|
const message = String(event.data?.message ?? 'Server chat failed.');
|
|
250
377
|
if (event.event === 'cancelled') {
|
|
251
378
|
throw new TurnCancelledError(message);
|
|
@@ -253,24 +380,34 @@ async function consumeTurnStream({ response, config, projectIndex, session, inpu
|
|
|
253
380
|
throw new ChatTurnFailedError(message, typeof event.data?.category === 'string' ? event.data.category : 'unknown_error', Boolean(event.data?.retryable), typeof event.data?.traceId === 'string' ? event.data.traceId : traceId);
|
|
254
381
|
}
|
|
255
382
|
}
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
383
|
+
try {
|
|
384
|
+
while (true) {
|
|
385
|
+
if (signal?.aborted) {
|
|
386
|
+
await reader.cancel().catch(() => { });
|
|
387
|
+
throw new TurnCancelledError();
|
|
388
|
+
}
|
|
389
|
+
const read = await Promise.race([reader.read(), parallelToolFailure]);
|
|
390
|
+
if (read.done)
|
|
391
|
+
break;
|
|
392
|
+
buffer += decoder.decode(read.value, { stream: true });
|
|
393
|
+
let separatorIndex = buffer.indexOf('\n\n');
|
|
394
|
+
while (separatorIndex !== -1) {
|
|
395
|
+
const block = buffer.slice(0, separatorIndex);
|
|
396
|
+
buffer = buffer.slice(separatorIndex + 2);
|
|
397
|
+
const event = parseSseBlock(block);
|
|
398
|
+
if (event)
|
|
399
|
+
await handleEvent(event);
|
|
400
|
+
separatorIndex = buffer.indexOf('\n\n');
|
|
401
|
+
}
|
|
273
402
|
}
|
|
403
|
+
await drainParallelTools();
|
|
404
|
+
}
|
|
405
|
+
catch (error) {
|
|
406
|
+
await reader.cancel().catch(() => { });
|
|
407
|
+
throw error;
|
|
408
|
+
}
|
|
409
|
+
finally {
|
|
410
|
+
await drainParallelTools().catch(() => { });
|
|
274
411
|
}
|
|
275
412
|
buffer += decoder.decode();
|
|
276
413
|
const tail = buffer.trim();
|
|
@@ -285,19 +422,32 @@ async function consumeTurnStream({ response, config, projectIndex, session, inpu
|
|
|
285
422
|
return finalResult.current;
|
|
286
423
|
}
|
|
287
424
|
export async function sendServerUserMessage({ config, projectIndex, session, input, imageAttachments = [], fetchImpl = globalThis.fetch, signal, }) {
|
|
425
|
+
const autoAttach = autoAttachImages(input, session.rootDir, imageAttachments);
|
|
426
|
+
const requestImageAttachments = autoAttach.attachments.length > 0
|
|
427
|
+
? [...imageAttachments, ...autoAttach.attachments]
|
|
428
|
+
: imageAttachments;
|
|
429
|
+
const requestInputBase = autoAttach.attachments.length > 0 ? autoAttach.sanitizedInput : input;
|
|
430
|
+
const backgroundJobUpdate = drainBackgroundJobNotifications({
|
|
431
|
+
sessionId: session.sessionId,
|
|
432
|
+
});
|
|
433
|
+
for (const err of autoAttach.errors) {
|
|
434
|
+
session.onStatus(`Image: ${err}`);
|
|
435
|
+
}
|
|
288
436
|
const request = {
|
|
289
437
|
modelId: session.modelId,
|
|
290
438
|
session: snapshotForServer(session),
|
|
291
|
-
input,
|
|
439
|
+
input: requestInputBase,
|
|
440
|
+
backgroundJobUpdate: backgroundJobUpdate || undefined,
|
|
292
441
|
clientEnvironment: collectClientEnvironment({ env: session.env }),
|
|
293
|
-
|
|
442
|
+
projectOrientation: collectProjectOrientation(session.rootDir) ?? undefined,
|
|
443
|
+
imageAttachments: imageAttachmentsForServer(requestImageAttachments),
|
|
294
444
|
maxToolSteps: session.maxToolSteps,
|
|
295
445
|
autoYes: session.autoYes,
|
|
296
446
|
agentMode: session.agentMode,
|
|
297
447
|
};
|
|
298
448
|
const trace = createTraceContext();
|
|
299
449
|
const preTurnHistoryLength = session.history.length;
|
|
300
|
-
const preserveOnAbort = () => preserveCancelledTurnInput(session,
|
|
450
|
+
const preserveOnAbort = () => preserveCancelledTurnInput(session, requestInputBase);
|
|
301
451
|
if (signal?.aborted) {
|
|
302
452
|
preserveOnAbort();
|
|
303
453
|
}
|
|
@@ -324,7 +474,7 @@ export async function sendServerUserMessage({ config, projectIndex, session, inp
|
|
|
324
474
|
config,
|
|
325
475
|
projectIndex,
|
|
326
476
|
session,
|
|
327
|
-
input,
|
|
477
|
+
input: requestInputBase,
|
|
328
478
|
fetchImpl,
|
|
329
479
|
signal,
|
|
330
480
|
traceId: trace.traceId,
|
|
@@ -339,16 +489,13 @@ export async function sendServerUserMessage({ config, projectIndex, session, inp
|
|
|
339
489
|
}
|
|
340
490
|
catch (error) {
|
|
341
491
|
if (isTurnCancelledError(error)) {
|
|
342
|
-
preserveCancelledTurnInput(session,
|
|
492
|
+
preserveCancelledTurnInput(session, requestInputBase);
|
|
343
493
|
throw error instanceof TurnCancelledError
|
|
344
494
|
? error
|
|
345
495
|
: new TurnCancelledError();
|
|
346
496
|
}
|
|
347
|
-
// Non-cancel failures (e.g. upstream connection errors) must not leave
|
|
348
|
-
// speculative cancelled-turn entries in history — otherwise the next
|
|
349
|
-
// request replays a malformed transcript to the server.
|
|
350
497
|
session.history.length = preTurnHistoryLength;
|
|
351
|
-
preserveFailedTurnInput(session,
|
|
498
|
+
preserveFailedTurnInput(session, requestInputBase, error instanceof ChatTurnFailedError ? error.category : 'unknown_error');
|
|
352
499
|
throw error;
|
|
353
500
|
}
|
|
354
501
|
finally {
|
package/dist/src/api/http.js
CHANGED
|
@@ -6,11 +6,13 @@ export const CLIENT_PLATFORM_HEADER = 'x-thegitai-client-platform';
|
|
|
6
6
|
export class ServerApiError extends Error {
|
|
7
7
|
status;
|
|
8
8
|
traceId;
|
|
9
|
-
|
|
9
|
+
code;
|
|
10
|
+
constructor(message, status, traceId, code = '') {
|
|
10
11
|
super(traceId ? `${message}\nTrace ID: ${traceId}` : message);
|
|
11
12
|
this.name = 'ServerApiError';
|
|
12
13
|
this.status = status;
|
|
13
14
|
this.traceId = traceId;
|
|
15
|
+
this.code = code;
|
|
14
16
|
}
|
|
15
17
|
}
|
|
16
18
|
export function createTraceId() {
|
|
@@ -26,6 +28,53 @@ export function createTraceContext(traceId = createTraceId()) {
|
|
|
26
28
|
},
|
|
27
29
|
};
|
|
28
30
|
}
|
|
31
|
+
export const REQUEST_TIMEOUT_MS = 8000;
|
|
32
|
+
const TRANSIENT_NETWORK_CODES = new Set([
|
|
33
|
+
'ECONNRESET',
|
|
34
|
+
'ECONNREFUSED',
|
|
35
|
+
'ETIMEDOUT',
|
|
36
|
+
'EAI_AGAIN',
|
|
37
|
+
'ENOTFOUND',
|
|
38
|
+
'ENETUNREACH',
|
|
39
|
+
'EHOSTUNREACH',
|
|
40
|
+
'EPIPE',
|
|
41
|
+
'UND_ERR_CONNECT_TIMEOUT',
|
|
42
|
+
'UND_ERR_SOCKET',
|
|
43
|
+
'UND_ERR_HEADERS_TIMEOUT',
|
|
44
|
+
'UND_ERR_BODY_TIMEOUT',
|
|
45
|
+
]);
|
|
46
|
+
export function isTransientNetworkError(error) {
|
|
47
|
+
if (error instanceof ServerApiError)
|
|
48
|
+
return false;
|
|
49
|
+
if (!error || typeof error !== 'object')
|
|
50
|
+
return false;
|
|
51
|
+
const err = error;
|
|
52
|
+
if (err.name === 'AbortError' || err.name === 'TimeoutError')
|
|
53
|
+
return true;
|
|
54
|
+
const code = err.code ?? err.cause?.code;
|
|
55
|
+
if (code) {
|
|
56
|
+
return TRANSIENT_NETWORK_CODES.has(code);
|
|
57
|
+
}
|
|
58
|
+
if (error instanceof TypeError && /fetch failed/i.test(err.message ?? '')) {
|
|
59
|
+
return true;
|
|
60
|
+
}
|
|
61
|
+
return false;
|
|
62
|
+
}
|
|
63
|
+
export async function retryTransient(run, { retries = 2, baseDelayMs = 400 } = {}) {
|
|
64
|
+
let attempt = 0;
|
|
65
|
+
for (;;) {
|
|
66
|
+
try {
|
|
67
|
+
return await run();
|
|
68
|
+
}
|
|
69
|
+
catch (error) {
|
|
70
|
+
if (attempt >= retries || !isTransientNetworkError(error))
|
|
71
|
+
throw error;
|
|
72
|
+
const delayMs = baseDelayMs * 2 ** attempt;
|
|
73
|
+
attempt += 1;
|
|
74
|
+
await new Promise((resolve) => setTimeout(resolve, delayMs));
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
29
78
|
export function normalizeServerUrl(serverUrl) {
|
|
30
79
|
const normalized = String(serverUrl || DEFAULT_SERVER_URL)
|
|
31
80
|
.trim()
|
|
@@ -49,11 +98,22 @@ export async function readJsonResponse(response) {
|
|
|
49
98
|
export function failureMessage(data, status) {
|
|
50
99
|
return String(data?.error?.message ?? data?.message ?? `Request failed with ${status}`);
|
|
51
100
|
}
|
|
101
|
+
export function failureCode(data) {
|
|
102
|
+
return typeof data?.error?.code === 'string' ? data.error.code : '';
|
|
103
|
+
}
|
|
104
|
+
export function isAuthenticationError(error) {
|
|
105
|
+
return error instanceof ServerApiError && error.status === 401;
|
|
106
|
+
}
|
|
107
|
+
export function authenticationErrorMessage(error) {
|
|
108
|
+
return error.code === 'AUTH_TOKEN_EXPIRED'
|
|
109
|
+
? 'Your login expired after 24 hours of inactivity. Run `ai login` and resume this saved session.'
|
|
110
|
+
: 'Your login is no longer valid. Run `ai login` and resume this saved session.';
|
|
111
|
+
}
|
|
52
112
|
export async function readErrorResponse(response, traceId = response.headers.get(TRACE_ID_HEADER) ?? '') {
|
|
53
113
|
const data = await readJsonResponse(response);
|
|
54
|
-
return new ServerApiError(failureMessage(data, response.status), response.status, traceId);
|
|
114
|
+
return new ServerApiError(failureMessage(data, response.status), response.status, traceId, failureCode(data));
|
|
55
115
|
}
|
|
56
|
-
export async function authorizedJson({ config, path, method = 'GET', body = null, headers = {}, fetchImpl = globalThis.fetch, }) {
|
|
116
|
+
export async function authorizedJson({ config, path, method = 'GET', body = null, headers = {}, fetchImpl = globalThis.fetch, timeoutMs = REQUEST_TIMEOUT_MS, }) {
|
|
57
117
|
const trace = createTraceContext();
|
|
58
118
|
const response = await fetchImpl(`${normalizeServerUrl(config.serverUrl)}${path}`, {
|
|
59
119
|
method,
|
|
@@ -64,10 +124,11 @@ export async function authorizedJson({ config, path, method = 'GET', body = null
|
|
|
64
124
|
...(body === null ? {} : { 'content-type': 'application/json' }),
|
|
65
125
|
},
|
|
66
126
|
body: body === null ? undefined : JSON.stringify(body),
|
|
127
|
+
signal: AbortSignal.timeout(timeoutMs),
|
|
67
128
|
});
|
|
68
129
|
const data = await readJsonResponse(response);
|
|
69
130
|
if (!response.ok) {
|
|
70
|
-
throw new ServerApiError(failureMessage(data, response.status), response.status, trace.traceId);
|
|
131
|
+
throw new ServerApiError(failureMessage(data, response.status), response.status, trace.traceId, failureCode(data));
|
|
71
132
|
}
|
|
72
133
|
return data;
|
|
73
134
|
}
|
package/dist/src/api/models.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { existsSync, mkdirSync, readFileSync, writeFileSync, } from 'node:fs';
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
import { getClientStateDir } from '../client-state.js';
|
|
4
|
-
import { ServerApiError, createTraceContext, failureMessage, normalizeServerUrl, readJsonResponse, } from './http.js';
|
|
4
|
+
import { REQUEST_TIMEOUT_MS, ServerApiError, createTraceContext, failureCode, failureMessage, normalizeServerUrl, readJsonResponse, retryTransient, } from './http.js';
|
|
5
5
|
function sanitizeModelInfo(raw) {
|
|
6
6
|
if (!raw || typeof raw !== 'object') {
|
|
7
7
|
return null;
|
|
@@ -9,10 +9,15 @@ function sanitizeModelInfo(raw) {
|
|
|
9
9
|
const value = raw;
|
|
10
10
|
const id = Number(value.id);
|
|
11
11
|
const label = String(value.label ?? '').trim();
|
|
12
|
-
|
|
12
|
+
const costRating = Number(value.costRating);
|
|
13
|
+
const description = String(value.description ?? '').trim();
|
|
14
|
+
if (!Number.isInteger(id) || id <= 0 || !label || !isCostRating(costRating)) {
|
|
13
15
|
return null;
|
|
14
16
|
}
|
|
15
|
-
return { id, label };
|
|
17
|
+
return { id, label, costRating, description };
|
|
18
|
+
}
|
|
19
|
+
function isCostRating(value) {
|
|
20
|
+
return Number.isInteger(value) && value >= 1 && value <= 3;
|
|
16
21
|
}
|
|
17
22
|
export function getModelsCachePath(env = process.env) {
|
|
18
23
|
return path.join(getClientStateDir(env), 'models.json');
|
|
@@ -45,6 +50,11 @@ export function readCachedServerModels(env = process.env) {
|
|
|
45
50
|
return null;
|
|
46
51
|
}
|
|
47
52
|
}
|
|
53
|
+
export function selectCacheForServer(cached, serverUrl) {
|
|
54
|
+
if (!cached)
|
|
55
|
+
return null;
|
|
56
|
+
return cached.serverUrl === normalizeServerUrl(serverUrl) ? cached : null;
|
|
57
|
+
}
|
|
48
58
|
export function writeCachedServerModels(cache, env = process.env) {
|
|
49
59
|
const filePath = getModelsCachePath(env);
|
|
50
60
|
mkdirSync(path.dirname(filePath), { recursive: true, mode: 0o700 });
|
|
@@ -54,26 +64,27 @@ export function writeCachedServerModels(cache, env = process.env) {
|
|
|
54
64
|
});
|
|
55
65
|
}
|
|
56
66
|
export async function fetchServerModels({ config, fetchImpl = globalThis.fetch, }) {
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
67
|
+
return retryTransient(async () => {
|
|
68
|
+
const trace = createTraceContext();
|
|
69
|
+
const response = await fetchImpl(`${normalizeServerUrl(config.serverUrl)}/v1/models`, {
|
|
70
|
+
headers: {
|
|
71
|
+
authorization: `Bearer ${config.token}`,
|
|
72
|
+
...trace.headers,
|
|
73
|
+
},
|
|
74
|
+
signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
|
|
75
|
+
});
|
|
76
|
+
const data = (await readJsonResponse(response));
|
|
77
|
+
if (!response.ok) {
|
|
78
|
+
throw new ServerApiError(failureMessage(data, response.status), response.status, trace.traceId, failureCode(data));
|
|
79
|
+
}
|
|
80
|
+
const models = Array.isArray(data?.models)
|
|
81
|
+
? data.models.map(sanitizeModelInfo).filter(Boolean)
|
|
82
|
+
: [];
|
|
83
|
+
if (models.length === 0) {
|
|
84
|
+
throw new Error('Server returned an invalid model list.');
|
|
85
|
+
}
|
|
86
|
+
return { models };
|
|
63
87
|
});
|
|
64
|
-
const data = (await readJsonResponse(response));
|
|
65
|
-
if (!response.ok) {
|
|
66
|
-
throw new ServerApiError(failureMessage(data, response.status), response.status, trace.traceId);
|
|
67
|
-
}
|
|
68
|
-
const models = Array.isArray(data?.models)
|
|
69
|
-
? data.models.map(sanitizeModelInfo).filter(Boolean)
|
|
70
|
-
: [];
|
|
71
|
-
if (models.length === 0) {
|
|
72
|
-
throw new Error('Server returned an invalid model list.');
|
|
73
|
-
}
|
|
74
|
-
return {
|
|
75
|
-
models,
|
|
76
|
-
};
|
|
77
88
|
}
|
|
78
89
|
export function selectServerModel({ requestedModelId, cached, serverModels, }) {
|
|
79
90
|
const supportedIds = new Set(serverModels.models.map((model) => model.id));
|
|
@@ -154,6 +154,8 @@ const SENSITIVE_BASENAME_PATTERNS = [
|
|
|
154
154
|
/^\.?pypirc$/i,
|
|
155
155
|
/^credentials(?:\..*)?$/i,
|
|
156
156
|
/^secrets?(?:\..*)?$/i,
|
|
157
|
+
/^service[-_]?account(?:\..*)?\.json$/i,
|
|
158
|
+
/^.*credentials.*\.json$/i,
|
|
157
159
|
];
|
|
158
160
|
const SENSITIVE_PATH_PATTERNS = [
|
|
159
161
|
/(^|[/\\])\.aws[/\\]credentials$/i,
|
|
@@ -161,6 +163,7 @@ const SENSITIVE_PATH_PATTERNS = [
|
|
|
161
163
|
/(^|[/\\])credentials?([._-]|$)/i,
|
|
162
164
|
/(^|[/\\])secrets?([._-]|$)/i,
|
|
163
165
|
/(^|[/\\])private[-_]?key([._-]|$)/i,
|
|
166
|
+
/(^|[/\\])service[-_]?account/i,
|
|
164
167
|
/\.(?:pem|key|p12|pfx)$/i,
|
|
165
168
|
];
|
|
166
169
|
export function normalizeArtifactPath(relPath) {
|