@thegitai/cli 1.0.0-preview.3 → 1.0.0-preview.30
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 +39 -6
- package/dist/bin/ai.js +142 -383
- package/dist/src/agent-mode.js +1 -6
- package/dist/src/api/auth.js +6 -4
- package/dist/src/api/browser-login.js +152 -37
- package/dist/src/api/chat.js +258 -38
- package/dist/src/api/contracts.js +55 -1
- package/dist/src/api/default-host.js +1 -0
- package/dist/src/api/http.js +69 -7
- package/dist/src/api/models.js +19 -10
- package/dist/src/background-jobs.js +2 -2
- package/dist/src/cli-args.js +19 -5
- package/dist/src/core/clipboard.js +7 -13
- package/dist/src/core/image-limits.js +56 -0
- package/dist/src/core/image-path-extractor.js +70 -3
- package/dist/src/core/session-image-store.js +199 -0
- package/dist/src/executor.js +25 -3
- package/dist/src/help-text.js +63 -16
- package/dist/src/permissions.js +243 -0
- package/dist/src/session-safety.js +0 -12
- package/dist/src/session-store.js +121 -20
- package/dist/src/session.js +14 -3
- package/dist/src/signin.js +58 -0
- package/dist/src/tool-executor.js +11 -46
- package/dist/src/tools/delete-file.js +15 -3
- package/dist/src/tools/index.js +13 -10
- package/dist/src/tools/patch-file.js +12 -26
- package/dist/src/tools/read-image-file.js +85 -0
- package/dist/src/tools/replace-document-text.js +28 -18
- package/dist/src/tools/restore-checkpoint.js +0 -1
- package/dist/src/tools/run-command.js +14 -71
- package/dist/src/tools/run-node-script.js +12 -81
- package/dist/src/tools/save-generated-image.js +120 -0
- package/dist/src/tools/str-replace.js +12 -26
- package/dist/src/tools/undo-edit.js +1 -6
- package/dist/src/tools/write-file.js +67 -11
- 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 +610 -164
- package/dist/src/ui/tui/bridge.js +10 -0
- package/dist/src/ui/tui/build-frame.js +452 -115
- package/dist/src/ui/tui/markdown-render.js +81 -73
- package/dist/src/ui/tui/shell-input.js +206 -63
- package/dist/src/ui/tui/terminal-theme.js +28 -0
- package/dist/src/ui/tui/terminal-title.js +3 -0
- package/dist/src/ui/tui/terminal-writes.js +48 -0
- package/dist/src/ui/tui/text.js +158 -4
- package/dist/src/ui/tui/user-input.js +568 -0
- package/dist/src/utils.js +9 -0
- package/package.json +29 -6
- package/dist/src/markdown-renderer.js +0 -112
- package/dist/src/project-index.js +0 -221
- package/dist/src/tools/code-intel.js +0 -472
- package/dist/src/tools/find-symbol.js +0 -70
- package/dist/src/tools/hover-symbol.js +0 -95
- package/dist/src/tools/list-symbols.js +0 -55
- package/dist/src/tools/search-code.js +0 -37
- package/dist/src/tools/signature-help.js +0 -118
package/dist/src/api/chat.js
CHANGED
|
@@ -1,11 +1,14 @@
|
|
|
1
1
|
import { drainBackgroundJobNotifications } from '../background-jobs.js';
|
|
2
2
|
import { createPromptCheckpoint, sanitizeSessionSafetyForServer, } from '../session-safety.js';
|
|
3
|
-
import { applySessionSnapshot, snapshotFromSession, } from '../session-store.js';
|
|
3
|
+
import { applySessionSnapshot, saveSessionState, snapshotFromSession, } from '../session-store.js';
|
|
4
4
|
import { executeLocalToolCall } from '../tool-executor.js';
|
|
5
|
-
import {
|
|
5
|
+
import { saveGeneratedImage } from '../tools/save-generated-image.js';
|
|
6
|
+
import { isUserInputQuestionArray } from './contracts.js';
|
|
7
|
+
import { createTraceContext, gatewayFailureCategory, normalizeServerUrl, readErrorResponse, } from './http.js';
|
|
6
8
|
import { collectClientEnvironment } from '../client-environment.js';
|
|
7
9
|
import { collectProjectOrientation } from '../project-orientation.js';
|
|
8
10
|
import { autoAttachImages } from '../core/image-path-extractor.js';
|
|
11
|
+
import { formatTurnFailureMarker } from '../turn-failure-marker.js';
|
|
9
12
|
export class TurnCancelledError extends Error {
|
|
10
13
|
name = 'TurnCancelledError';
|
|
11
14
|
constructor(message = 'Turn cancelled.') {
|
|
@@ -17,11 +20,13 @@ export class ChatTurnFailedError extends Error {
|
|
|
17
20
|
category;
|
|
18
21
|
retryable;
|
|
19
22
|
traceId;
|
|
20
|
-
|
|
23
|
+
partialSnapshot;
|
|
24
|
+
constructor(message, category = 'unknown_error', retryable = false, traceId = '', partialSnapshot) {
|
|
21
25
|
super(traceId ? `${message}\nTrace ID: ${traceId}` : message);
|
|
22
26
|
this.category = category;
|
|
23
27
|
this.retryable = retryable;
|
|
24
28
|
this.traceId = traceId;
|
|
29
|
+
this.partialSnapshot = partialSnapshot;
|
|
25
30
|
}
|
|
26
31
|
}
|
|
27
32
|
export function isTurnCancelledError(error) {
|
|
@@ -51,6 +56,7 @@ function parseSseBlock(block) {
|
|
|
51
56
|
return { event, data: text };
|
|
52
57
|
}
|
|
53
58
|
}
|
|
59
|
+
let toolStateSeqCounter = 0;
|
|
54
60
|
function toolStateFromSession(session) {
|
|
55
61
|
return {
|
|
56
62
|
autoYes: session.autoYes,
|
|
@@ -93,17 +99,30 @@ export function preserveCancelledTurnInput(session, input) {
|
|
|
93
99
|
return;
|
|
94
100
|
break;
|
|
95
101
|
}
|
|
96
|
-
session.history.push({
|
|
102
|
+
session.history.push({
|
|
103
|
+
role: 'user',
|
|
104
|
+
parts: [{ text }],
|
|
105
|
+
kind: 'turnStart',
|
|
106
|
+
userInput: text,
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
function appendTurnFailureMarker(session, category) {
|
|
110
|
+
session.history.push({
|
|
111
|
+
role: 'model',
|
|
112
|
+
parts: [{ text: formatTurnFailureMarker(category) }],
|
|
113
|
+
});
|
|
97
114
|
}
|
|
98
115
|
function preserveFailedTurnInput(session, input, category) {
|
|
99
116
|
const text = input.trim();
|
|
100
117
|
if (!text)
|
|
101
118
|
return;
|
|
102
|
-
session.history.push({ role: 'user', parts: [{ text }], kind: 'turnStart' });
|
|
103
119
|
session.history.push({
|
|
104
|
-
role: '
|
|
105
|
-
parts: [{ text
|
|
120
|
+
role: 'user',
|
|
121
|
+
parts: [{ text }],
|
|
122
|
+
kind: 'turnStart',
|
|
123
|
+
userInput: text,
|
|
106
124
|
});
|
|
125
|
+
appendTurnFailureMarker(session, category);
|
|
107
126
|
}
|
|
108
127
|
function historyHasToolCall(session, callId) {
|
|
109
128
|
return session.history.some((entry) => (entry.parts ?? []).some((part) => String(part?.functionCall?.id ?? '') === callId));
|
|
@@ -143,11 +162,19 @@ function publicStatusMessage(data) {
|
|
|
143
162
|
? event.toolName
|
|
144
163
|
: 'tool';
|
|
145
164
|
if (event.phase === 'thinking')
|
|
146
|
-
return '
|
|
165
|
+
return 'Exploring options...';
|
|
166
|
+
if (event.phase === 'analyzing_image') {
|
|
167
|
+
return (event.imageCount ?? 1) > 1 ? 'Analyzing images...' : 'Analyzing image...';
|
|
168
|
+
}
|
|
169
|
+
if (event.phase === 'generating_image') {
|
|
170
|
+
return 'Generating image...';
|
|
171
|
+
}
|
|
147
172
|
if (event.phase === 'running_tool')
|
|
148
173
|
return `Running ${toolName}...`;
|
|
149
174
|
if (event.phase === 'waiting_for_tool')
|
|
150
175
|
return `Running ${toolName} locally...`;
|
|
176
|
+
if (event.phase === 'waiting_for_user_input')
|
|
177
|
+
return 'Waiting for your input...';
|
|
151
178
|
return null;
|
|
152
179
|
}
|
|
153
180
|
function normalizeShellJobToolCall(call) {
|
|
@@ -184,6 +211,7 @@ async function postToolResult({ config, turnId, event, result, session, fetchImp
|
|
|
184
211
|
toolCallId: event.call.id,
|
|
185
212
|
result,
|
|
186
213
|
toolState: toolStateFromSession(session),
|
|
214
|
+
toolStateSeq: ++toolStateSeqCounter,
|
|
187
215
|
};
|
|
188
216
|
const trace = createTraceContext(traceId);
|
|
189
217
|
const response = await fetchImpl(`${normalizeServerUrl(config.serverUrl)}/v1/chat/turn/${encodeURIComponent(turnId)}/tool-result`, {
|
|
@@ -202,7 +230,76 @@ async function postToolResult({ config, turnId, event, result, session, fetchImp
|
|
|
202
230
|
throw await readErrorResponse(response, trace.traceId);
|
|
203
231
|
}
|
|
204
232
|
}
|
|
205
|
-
async function
|
|
233
|
+
async function postUserInputResult({ config, turnId, requestId, result, fetchImpl, traceId, }) {
|
|
234
|
+
const payload = {
|
|
235
|
+
requestId,
|
|
236
|
+
result,
|
|
237
|
+
};
|
|
238
|
+
const trace = createTraceContext(traceId);
|
|
239
|
+
const response = await fetchImpl(`${normalizeServerUrl(config.serverUrl)}/v1/chat/turn/${encodeURIComponent(turnId)}/user-input-result`, {
|
|
240
|
+
method: 'POST',
|
|
241
|
+
headers: {
|
|
242
|
+
authorization: `Bearer ${config.token}`,
|
|
243
|
+
'content-type': 'application/json',
|
|
244
|
+
...trace.headers,
|
|
245
|
+
},
|
|
246
|
+
body: JSON.stringify(payload),
|
|
247
|
+
});
|
|
248
|
+
if (response.status === 410) {
|
|
249
|
+
return;
|
|
250
|
+
}
|
|
251
|
+
if (!response.ok) {
|
|
252
|
+
throw await readErrorResponse(response, trace.traceId);
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
export async function postInterjection({ config, turnId, text, messageId, imageAttachments = [], fetchImpl = globalThis.fetch, traceId, }) {
|
|
256
|
+
const payload = {
|
|
257
|
+
text,
|
|
258
|
+
messageId,
|
|
259
|
+
...(imageAttachments.length > 0 ? { imageAttachments } : {}),
|
|
260
|
+
};
|
|
261
|
+
const trace = createTraceContext(traceId);
|
|
262
|
+
const response = await fetchImpl(`${normalizeServerUrl(config.serverUrl)}/v1/chat/turn/${encodeURIComponent(turnId)}/interject`, {
|
|
263
|
+
method: 'POST',
|
|
264
|
+
headers: {
|
|
265
|
+
authorization: `Bearer ${config.token}`,
|
|
266
|
+
'content-type': 'application/json',
|
|
267
|
+
...trace.headers,
|
|
268
|
+
},
|
|
269
|
+
body: JSON.stringify(payload),
|
|
270
|
+
});
|
|
271
|
+
if (response.status === 410) {
|
|
272
|
+
return 'stale';
|
|
273
|
+
}
|
|
274
|
+
if (!response.ok) {
|
|
275
|
+
throw await readErrorResponse(response, trace.traceId);
|
|
276
|
+
}
|
|
277
|
+
return 'delivered';
|
|
278
|
+
}
|
|
279
|
+
const turnIdOverrides = new WeakMap();
|
|
280
|
+
function enterServerTurnId(session, serverSessionTurnId) {
|
|
281
|
+
const active = turnIdOverrides.get(session);
|
|
282
|
+
if (active) {
|
|
283
|
+
active.depth += 1;
|
|
284
|
+
return;
|
|
285
|
+
}
|
|
286
|
+
turnIdOverrides.set(session, {
|
|
287
|
+
previousTurnId: session.turnState.id,
|
|
288
|
+
depth: 1,
|
|
289
|
+
});
|
|
290
|
+
session.turnState.id = serverSessionTurnId;
|
|
291
|
+
}
|
|
292
|
+
function exitServerTurnId(session) {
|
|
293
|
+
const active = turnIdOverrides.get(session);
|
|
294
|
+
if (!active)
|
|
295
|
+
return;
|
|
296
|
+
active.depth -= 1;
|
|
297
|
+
if (active.depth === 0) {
|
|
298
|
+
session.turnState.id = active.previousTurnId;
|
|
299
|
+
turnIdOverrides.delete(session);
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
async function executeAndPostToolResult({ config, session, event, input, fetchImpl, signal, traceId, }) {
|
|
206
303
|
const turnId = String(event?.turnId ?? '').trim();
|
|
207
304
|
if (!turnId || !event?.call?.id || !event.call.name) {
|
|
208
305
|
throw new Error('Server emitted an invalid tool-call event.');
|
|
@@ -210,17 +307,23 @@ async function executeAndPostToolResult({ config, projectIndex, session, event,
|
|
|
210
307
|
if (signal?.aborted) {
|
|
211
308
|
throw new TurnCancelledError();
|
|
212
309
|
}
|
|
213
|
-
const previousTurnId = session.turnState.id;
|
|
214
310
|
const serverSessionTurnId = String(event.sessionTurnId ?? '').trim();
|
|
215
311
|
if (serverSessionTurnId) {
|
|
216
|
-
session
|
|
312
|
+
enterServerTurnId(session, serverSessionTurnId);
|
|
217
313
|
if (!session.clientState.safety.checkpoints.some((checkpoint) => checkpoint.turnId === serverSessionTurnId)) {
|
|
218
314
|
createPromptCheckpoint(session.clientState.safety, 'prompt boundary', serverSessionTurnId);
|
|
219
315
|
}
|
|
220
316
|
}
|
|
221
317
|
try {
|
|
222
318
|
const call = normalizeShellJobToolCall(event.call);
|
|
223
|
-
const rawResult =
|
|
319
|
+
const rawResult = event.generatedImage
|
|
320
|
+
? saveGeneratedImage({
|
|
321
|
+
base64Data: event.generatedImage.base64Data,
|
|
322
|
+
mimeType: event.generatedImage.mimeType,
|
|
323
|
+
suggestedFilename: event.generatedImage.suggestedFilename ||
|
|
324
|
+
String(call.args?.filename ?? call.args?.file_name ?? ''),
|
|
325
|
+
})
|
|
326
|
+
: await executeLocalToolCall(session, call);
|
|
224
327
|
preserveCancelledTurnToolResult(session, input, { ...event, call }, rawResult);
|
|
225
328
|
if (signal?.aborted) {
|
|
226
329
|
throw new TurnCancelledError();
|
|
@@ -236,10 +339,12 @@ async function executeAndPostToolResult({ config, projectIndex, session, event,
|
|
|
236
339
|
});
|
|
237
340
|
}
|
|
238
341
|
finally {
|
|
239
|
-
|
|
342
|
+
if (serverSessionTurnId) {
|
|
343
|
+
exitServerTurnId(session);
|
|
344
|
+
}
|
|
240
345
|
}
|
|
241
346
|
}
|
|
242
|
-
async function consumeTurnStream({ response, config,
|
|
347
|
+
async function consumeTurnStream({ response, config, session, input, fetchImpl, signal, traceId, onTurnStart, onInterjectionDelivered, }) {
|
|
243
348
|
if (!response.body) {
|
|
244
349
|
throw new Error('Server returned an empty chat stream.');
|
|
245
350
|
}
|
|
@@ -249,8 +354,56 @@ async function consumeTurnStream({ response, config, projectIndex, session, inpu
|
|
|
249
354
|
const finalResult = {
|
|
250
355
|
current: null,
|
|
251
356
|
};
|
|
357
|
+
const pendingParallelTools = [];
|
|
358
|
+
let firstParallelFailure = null;
|
|
359
|
+
let rejectOnParallelFailure = null;
|
|
360
|
+
const parallelToolFailure = new Promise((_, reject) => {
|
|
361
|
+
rejectOnParallelFailure = reject;
|
|
362
|
+
});
|
|
363
|
+
parallelToolFailure.catch(() => { });
|
|
364
|
+
function recordParallelFailure(error) {
|
|
365
|
+
const failure = error ?? new Error('Local tool execution failed.');
|
|
366
|
+
if (firstParallelFailure == null) {
|
|
367
|
+
firstParallelFailure = failure;
|
|
368
|
+
rejectOnParallelFailure?.(failure);
|
|
369
|
+
}
|
|
370
|
+
return failure;
|
|
371
|
+
}
|
|
372
|
+
async function drainParallelTools() {
|
|
373
|
+
if (!pendingParallelTools.length)
|
|
374
|
+
return;
|
|
375
|
+
const pending = pendingParallelTools.splice(0);
|
|
376
|
+
const outcomes = await Promise.all(pending);
|
|
377
|
+
for (const outcome of outcomes) {
|
|
378
|
+
if (outcome != null)
|
|
379
|
+
throw outcome;
|
|
380
|
+
}
|
|
381
|
+
}
|
|
252
382
|
async function handleEvent(event) {
|
|
383
|
+
if (event.event === 'turn-start') {
|
|
384
|
+
const turnId = String(event.data?.turnId ?? '').trim();
|
|
385
|
+
if (turnId)
|
|
386
|
+
onTurnStart?.(turnId);
|
|
387
|
+
return;
|
|
388
|
+
}
|
|
389
|
+
if (event.event === 'interjection-delivered') {
|
|
390
|
+
onInterjectionDelivered?.(event.data);
|
|
391
|
+
return;
|
|
392
|
+
}
|
|
253
393
|
if (event.event === 'status') {
|
|
394
|
+
const data = event.data;
|
|
395
|
+
if (data?.phase === 'analyzing_image') {
|
|
396
|
+
session.onImageAnalysis?.(Math.max(1, Number(data.imageCount ?? 1) || 1));
|
|
397
|
+
session.onImageGeneration?.(false);
|
|
398
|
+
}
|
|
399
|
+
else if (data?.phase === 'generating_image') {
|
|
400
|
+
session.onImageGeneration?.(true);
|
|
401
|
+
session.onImageAnalysis?.(0);
|
|
402
|
+
}
|
|
403
|
+
else if (data?.phase) {
|
|
404
|
+
session.onImageAnalysis?.(0);
|
|
405
|
+
session.onImageGeneration?.(false);
|
|
406
|
+
}
|
|
254
407
|
const message = publicStatusMessage(event.data);
|
|
255
408
|
if (message)
|
|
256
409
|
session.onStatus(message);
|
|
@@ -260,11 +413,24 @@ async function consumeTurnStream({ response, config, projectIndex, session, inpu
|
|
|
260
413
|
return;
|
|
261
414
|
}
|
|
262
415
|
if (event.event === 'tool-call') {
|
|
416
|
+
const data = event.data;
|
|
417
|
+
if (data?.parallelSafe === true) {
|
|
418
|
+
pendingParallelTools.push(executeAndPostToolResult({
|
|
419
|
+
config,
|
|
420
|
+
session,
|
|
421
|
+
event: data,
|
|
422
|
+
input,
|
|
423
|
+
fetchImpl,
|
|
424
|
+
signal,
|
|
425
|
+
traceId,
|
|
426
|
+
}).then(() => null, (error) => recordParallelFailure(error)));
|
|
427
|
+
return;
|
|
428
|
+
}
|
|
429
|
+
await drainParallelTools();
|
|
263
430
|
await executeAndPostToolResult({
|
|
264
431
|
config,
|
|
265
|
-
projectIndex,
|
|
266
432
|
session,
|
|
267
|
-
event:
|
|
433
|
+
event: data,
|
|
268
434
|
input,
|
|
269
435
|
fetchImpl,
|
|
270
436
|
signal,
|
|
@@ -279,36 +445,75 @@ async function consumeTurnStream({ response, config, projectIndex, session, inpu
|
|
|
279
445
|
}
|
|
280
446
|
return;
|
|
281
447
|
}
|
|
448
|
+
if (event.event === 'user-input-request') {
|
|
449
|
+
await drainParallelTools();
|
|
450
|
+
const data = event.data;
|
|
451
|
+
const turnId = String(data?.turnId ?? '').trim();
|
|
452
|
+
const requestId = String(data?.requestId ?? '').trim();
|
|
453
|
+
if (!turnId ||
|
|
454
|
+
!requestId ||
|
|
455
|
+
!isUserInputQuestionArray(data?.questions)) {
|
|
456
|
+
throw new Error('Server emitted an invalid user-input request.');
|
|
457
|
+
}
|
|
458
|
+
if (!session.requestUserInput) {
|
|
459
|
+
throw new Error('Interactive user input is unavailable in this client.');
|
|
460
|
+
}
|
|
461
|
+
const result = await session.requestUserInput({ questions: data.questions }, signal);
|
|
462
|
+
if (signal?.aborted) {
|
|
463
|
+
throw new TurnCancelledError();
|
|
464
|
+
}
|
|
465
|
+
await postUserInputResult({
|
|
466
|
+
config,
|
|
467
|
+
turnId,
|
|
468
|
+
requestId,
|
|
469
|
+
result,
|
|
470
|
+
fetchImpl,
|
|
471
|
+
traceId,
|
|
472
|
+
});
|
|
473
|
+
return;
|
|
474
|
+
}
|
|
282
475
|
if (event.event === 'result') {
|
|
476
|
+
await drainParallelTools();
|
|
283
477
|
finalResult.current = event.data;
|
|
284
478
|
return;
|
|
285
479
|
}
|
|
286
480
|
if (event.event === 'cancelled' || event.event === 'error') {
|
|
481
|
+
await drainParallelTools().catch(() => { });
|
|
287
482
|
const message = String(event.data?.message ?? 'Server chat failed.');
|
|
288
483
|
if (event.event === 'cancelled') {
|
|
289
484
|
throw new TurnCancelledError(message);
|
|
290
485
|
}
|
|
291
|
-
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);
|
|
486
|
+
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, event.data?.snapshot);
|
|
292
487
|
}
|
|
293
488
|
}
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
489
|
+
try {
|
|
490
|
+
while (true) {
|
|
491
|
+
if (signal?.aborted) {
|
|
492
|
+
await reader.cancel().catch(() => { });
|
|
493
|
+
throw new TurnCancelledError();
|
|
494
|
+
}
|
|
495
|
+
const read = await Promise.race([reader.read(), parallelToolFailure]);
|
|
496
|
+
if (read.done)
|
|
497
|
+
break;
|
|
498
|
+
buffer += decoder.decode(read.value, { stream: true });
|
|
499
|
+
let separatorIndex = buffer.indexOf('\n\n');
|
|
500
|
+
while (separatorIndex !== -1) {
|
|
501
|
+
const block = buffer.slice(0, separatorIndex);
|
|
502
|
+
buffer = buffer.slice(separatorIndex + 2);
|
|
503
|
+
const event = parseSseBlock(block);
|
|
504
|
+
if (event)
|
|
505
|
+
await handleEvent(event);
|
|
506
|
+
separatorIndex = buffer.indexOf('\n\n');
|
|
507
|
+
}
|
|
311
508
|
}
|
|
509
|
+
await drainParallelTools();
|
|
510
|
+
}
|
|
511
|
+
catch (error) {
|
|
512
|
+
await reader.cancel().catch(() => { });
|
|
513
|
+
throw error;
|
|
514
|
+
}
|
|
515
|
+
finally {
|
|
516
|
+
await drainParallelTools().catch(() => { });
|
|
312
517
|
}
|
|
313
518
|
buffer += decoder.decode();
|
|
314
519
|
const tail = buffer.trim();
|
|
@@ -322,7 +527,7 @@ async function consumeTurnStream({ response, config, projectIndex, session, inpu
|
|
|
322
527
|
}
|
|
323
528
|
return finalResult.current;
|
|
324
529
|
}
|
|
325
|
-
export async function sendServerUserMessage({ config,
|
|
530
|
+
export async function sendServerUserMessage({ config, session, input, imageAttachments = [], fetchImpl = globalThis.fetch, signal, onTurnStart, onInterjectionDelivered, }) {
|
|
326
531
|
const autoAttach = autoAttachImages(input, session.rootDir, imageAttachments);
|
|
327
532
|
const requestImageAttachments = autoAttach.attachments.length > 0
|
|
328
533
|
? [...imageAttachments, ...autoAttach.attachments]
|
|
@@ -373,12 +578,13 @@ export async function sendServerUserMessage({ config, projectIndex, session, inp
|
|
|
373
578
|
const result = await consumeTurnStream({
|
|
374
579
|
response,
|
|
375
580
|
config,
|
|
376
|
-
projectIndex,
|
|
377
581
|
session,
|
|
378
582
|
input: requestInputBase,
|
|
379
583
|
fetchImpl,
|
|
380
584
|
signal,
|
|
381
585
|
traceId: trace.traceId,
|
|
586
|
+
onTurnStart,
|
|
587
|
+
onInterjectionDelivered,
|
|
382
588
|
});
|
|
383
589
|
applySessionSnapshot(session, result.snapshot, { preserveAgentMode: true });
|
|
384
590
|
return {
|
|
@@ -395,8 +601,22 @@ export async function sendServerUserMessage({ config, projectIndex, session, inp
|
|
|
395
601
|
? error
|
|
396
602
|
: new TurnCancelledError();
|
|
397
603
|
}
|
|
398
|
-
|
|
399
|
-
|
|
604
|
+
const category = error instanceof ChatTurnFailedError
|
|
605
|
+
? error.category
|
|
606
|
+
:
|
|
607
|
+
gatewayFailureCategory(error) ?? 'unknown_error';
|
|
608
|
+
const partial = error instanceof ChatTurnFailedError ? error.partialSnapshot : undefined;
|
|
609
|
+
if (partial) {
|
|
610
|
+
applySessionSnapshot(session, partial, { preserveAgentMode: true });
|
|
611
|
+
}
|
|
612
|
+
else {
|
|
613
|
+
session.history.length = preTurnHistoryLength;
|
|
614
|
+
preserveFailedTurnInput(session, requestInputBase, category);
|
|
615
|
+
}
|
|
616
|
+
try {
|
|
617
|
+
saveSessionState(session, session.env);
|
|
618
|
+
}
|
|
619
|
+
catch { }
|
|
400
620
|
throw error;
|
|
401
621
|
}
|
|
402
622
|
finally {
|
|
@@ -1 +1,55 @@
|
|
|
1
|
-
|
|
1
|
+
function nonEmptyString(value) {
|
|
2
|
+
return typeof value === 'string' && value.trim().length > 0;
|
|
3
|
+
}
|
|
4
|
+
const USER_INPUT_ID_PATTERN = /^[a-z][a-z0-9_]*$/;
|
|
5
|
+
export function isUserInputQuestionArray(value) {
|
|
6
|
+
if (!Array.isArray(value) || value.length < 1 || value.length > 4) {
|
|
7
|
+
return false;
|
|
8
|
+
}
|
|
9
|
+
const questionIds = new Set();
|
|
10
|
+
return value.every((question) => {
|
|
11
|
+
if (!question || typeof question !== 'object' || Array.isArray(question)) {
|
|
12
|
+
return false;
|
|
13
|
+
}
|
|
14
|
+
const candidate = question;
|
|
15
|
+
if (!nonEmptyString(candidate.id) ||
|
|
16
|
+
!USER_INPUT_ID_PATTERN.test(candidate.id) ||
|
|
17
|
+
questionIds.has(candidate.id) ||
|
|
18
|
+
!nonEmptyString(candidate.header) ||
|
|
19
|
+
Array.from(candidate.header).length > 12 ||
|
|
20
|
+
!nonEmptyString(candidate.question) ||
|
|
21
|
+
typeof candidate.multiSelect !== 'boolean' ||
|
|
22
|
+
!Array.isArray(candidate.options) ||
|
|
23
|
+
candidate.options.length < 2 ||
|
|
24
|
+
candidate.options.length > 4) {
|
|
25
|
+
return false;
|
|
26
|
+
}
|
|
27
|
+
questionIds.add(candidate.id);
|
|
28
|
+
const optionIds = new Set();
|
|
29
|
+
const optionsValid = candidate.options.every((option) => {
|
|
30
|
+
if (!option || typeof option !== 'object' || Array.isArray(option)) {
|
|
31
|
+
return false;
|
|
32
|
+
}
|
|
33
|
+
const item = option;
|
|
34
|
+
if (!nonEmptyString(item.id) ||
|
|
35
|
+
!USER_INPUT_ID_PATTERN.test(item.id) ||
|
|
36
|
+
optionIds.has(item.id) ||
|
|
37
|
+
!nonEmptyString(item.label) ||
|
|
38
|
+
!nonEmptyString(item.description)) {
|
|
39
|
+
return false;
|
|
40
|
+
}
|
|
41
|
+
optionIds.add(item.id);
|
|
42
|
+
return true;
|
|
43
|
+
});
|
|
44
|
+
if (!optionsValid) {
|
|
45
|
+
return false;
|
|
46
|
+
}
|
|
47
|
+
if (candidate.recommendedOptionId !== undefined &&
|
|
48
|
+
(!nonEmptyString(candidate.recommendedOptionId) ||
|
|
49
|
+
candidate.recommendedOptionId !==
|
|
50
|
+
candidate.options[0].id)) {
|
|
51
|
+
return false;
|
|
52
|
+
}
|
|
53
|
+
return true;
|
|
54
|
+
});
|
|
55
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export const DEFAULT_THEGITAI_HOST = 'https://thegit.ai';
|
package/dist/src/api/http.js
CHANGED
|
@@ -1,16 +1,18 @@
|
|
|
1
1
|
import { randomUUID } from 'node:crypto';
|
|
2
|
-
|
|
2
|
+
import { DEFAULT_THEGITAI_HOST } from './default-host.js';
|
|
3
3
|
export const TRACE_ID_HEADER = 'x-thegitai-trace-id';
|
|
4
4
|
export const CLIENT_HEADER = 'x-thegitai-client';
|
|
5
5
|
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() {
|
|
@@ -27,6 +29,14 @@ export function createTraceContext(traceId = createTraceId()) {
|
|
|
27
29
|
};
|
|
28
30
|
}
|
|
29
31
|
export const REQUEST_TIMEOUT_MS = 8000;
|
|
32
|
+
export const STARTUP_REQUEST_TIMEOUT_MS = 3000;
|
|
33
|
+
export const STARTUP_DEADLINE_MS = 10_000;
|
|
34
|
+
export const STARTUP_RETRY_BUDGET = {
|
|
35
|
+
retries: 3,
|
|
36
|
+
baseDelayMs: 200,
|
|
37
|
+
deadlineMs: STARTUP_DEADLINE_MS,
|
|
38
|
+
timeoutMs: STARTUP_REQUEST_TIMEOUT_MS,
|
|
39
|
+
};
|
|
30
40
|
const TRANSIENT_NETWORK_CODES = new Set([
|
|
31
41
|
'ECONNRESET',
|
|
32
42
|
'ECONNREFUSED',
|
|
@@ -58,7 +68,9 @@ export function isTransientNetworkError(error) {
|
|
|
58
68
|
}
|
|
59
69
|
return false;
|
|
60
70
|
}
|
|
61
|
-
export async function retryTransient(run, { retries = 2, baseDelayMs = 400 } = {}) {
|
|
71
|
+
export async function retryTransient(run, { retries = 2, baseDelayMs = 400, deadlineMs, now = () => Date.now(), } = {}) {
|
|
72
|
+
const startedAt = now();
|
|
73
|
+
const remainingMs = () => deadlineMs == null ? Infinity : deadlineMs - (now() - startedAt);
|
|
62
74
|
let attempt = 0;
|
|
63
75
|
for (;;) {
|
|
64
76
|
try {
|
|
@@ -68,13 +80,15 @@ export async function retryTransient(run, { retries = 2, baseDelayMs = 400 } = {
|
|
|
68
80
|
if (attempt >= retries || !isTransientNetworkError(error))
|
|
69
81
|
throw error;
|
|
70
82
|
const delayMs = baseDelayMs * 2 ** attempt;
|
|
83
|
+
if (remainingMs() <= delayMs)
|
|
84
|
+
throw error;
|
|
71
85
|
attempt += 1;
|
|
72
86
|
await new Promise((resolve) => setTimeout(resolve, delayMs));
|
|
73
87
|
}
|
|
74
88
|
}
|
|
75
89
|
}
|
|
76
90
|
export function normalizeServerUrl(serverUrl) {
|
|
77
|
-
const normalized = String(serverUrl ||
|
|
91
|
+
const normalized = String(serverUrl || DEFAULT_THEGITAI_HOST)
|
|
78
92
|
.trim()
|
|
79
93
|
.replace(/\/+$/, '');
|
|
80
94
|
if (!/^https?:\/\//i.test(normalized)) {
|
|
@@ -82,6 +96,41 @@ export function normalizeServerUrl(serverUrl) {
|
|
|
82
96
|
}
|
|
83
97
|
return normalized;
|
|
84
98
|
}
|
|
99
|
+
const MAX_NON_JSON_BODY_CHARS = 200;
|
|
100
|
+
const GATEWAY_STATUS_MESSAGES = {
|
|
101
|
+
502: 'Could not reach the TheGitAI server (bad gateway).',
|
|
102
|
+
503: 'The TheGitAI server is temporarily unavailable.',
|
|
103
|
+
504: 'The TheGitAI server did not respond in time (gateway timeout).',
|
|
104
|
+
520: 'The connection to the TheGitAI server failed (edge error 520).',
|
|
105
|
+
521: 'The TheGitAI server is not accepting connections (edge error 521).',
|
|
106
|
+
522: 'The connection to the TheGitAI server timed out (edge error 522).',
|
|
107
|
+
523: 'The TheGitAI server is unreachable (edge error 523).',
|
|
108
|
+
524: 'The TheGitAI server did not respond in time (edge error 524).',
|
|
109
|
+
};
|
|
110
|
+
function truncateByCodePoint(text, maxChars) {
|
|
111
|
+
if (text.length <= maxChars)
|
|
112
|
+
return text;
|
|
113
|
+
const points = Array.from(text);
|
|
114
|
+
if (points.length <= maxChars)
|
|
115
|
+
return text;
|
|
116
|
+
return `${points.slice(0, maxChars).join('').trimEnd()}…`;
|
|
117
|
+
}
|
|
118
|
+
export function nonJsonErrorMessage(body, status) {
|
|
119
|
+
const rayId = /Cloudflare Ray ID:\s*(?:<[^>]*>\s*)*([0-9a-f]{8,})/i.exec(body)?.[1];
|
|
120
|
+
const gateway = GATEWAY_STATUS_MESSAGES[status];
|
|
121
|
+
const summary = truncateByCodePoint(body
|
|
122
|
+
.replace(/<(script|style)\b[^>]*>[\s\S]*?<\/\1>/gi, ' ')
|
|
123
|
+
.replace(/<[^>]*>/g, ' ')
|
|
124
|
+
.replace(/\s+/g, ' ')
|
|
125
|
+
.trim(), MAX_NON_JSON_BODY_CHARS);
|
|
126
|
+
const base = gateway ?? (summary || `Request failed with ${status}`);
|
|
127
|
+
return rayId ? `${base} (Cloudflare Ray ID: ${rayId})` : base;
|
|
128
|
+
}
|
|
129
|
+
export function gatewayFailureCategory(error) {
|
|
130
|
+
if (!(error instanceof ServerApiError))
|
|
131
|
+
return null;
|
|
132
|
+
return error.status in GATEWAY_STATUS_MESSAGES ? 'gateway_error' : null;
|
|
133
|
+
}
|
|
85
134
|
export async function readJsonResponse(response) {
|
|
86
135
|
const text = await response.text();
|
|
87
136
|
if (!text.trim())
|
|
@@ -90,15 +139,28 @@ export async function readJsonResponse(response) {
|
|
|
90
139
|
return JSON.parse(text);
|
|
91
140
|
}
|
|
92
141
|
catch {
|
|
93
|
-
return {
|
|
142
|
+
return {
|
|
143
|
+
error: { message: nonJsonErrorMessage(text, response.status) },
|
|
144
|
+
};
|
|
94
145
|
}
|
|
95
146
|
}
|
|
96
147
|
export function failureMessage(data, status) {
|
|
97
148
|
return String(data?.error?.message ?? data?.message ?? `Request failed with ${status}`);
|
|
98
149
|
}
|
|
150
|
+
export function failureCode(data) {
|
|
151
|
+
return typeof data?.error?.code === 'string' ? data.error.code : '';
|
|
152
|
+
}
|
|
153
|
+
export function isAuthenticationError(error) {
|
|
154
|
+
return error instanceof ServerApiError && error.status === 401;
|
|
155
|
+
}
|
|
156
|
+
export function authenticationErrorMessage(error) {
|
|
157
|
+
return error.code === 'AUTH_TOKEN_EXPIRED'
|
|
158
|
+
? 'Your login expired after 48 hours of inactivity. Run `ai login` and resume this saved session.'
|
|
159
|
+
: 'Your login is no longer valid. Run `ai login` and resume this saved session.';
|
|
160
|
+
}
|
|
99
161
|
export async function readErrorResponse(response, traceId = response.headers.get(TRACE_ID_HEADER) ?? '') {
|
|
100
162
|
const data = await readJsonResponse(response);
|
|
101
|
-
return new ServerApiError(failureMessage(data, response.status), response.status, traceId);
|
|
163
|
+
return new ServerApiError(failureMessage(data, response.status), response.status, traceId, failureCode(data));
|
|
102
164
|
}
|
|
103
165
|
export async function authorizedJson({ config, path, method = 'GET', body = null, headers = {}, fetchImpl = globalThis.fetch, timeoutMs = REQUEST_TIMEOUT_MS, }) {
|
|
104
166
|
const trace = createTraceContext();
|
|
@@ -115,7 +177,7 @@ export async function authorizedJson({ config, path, method = 'GET', body = null
|
|
|
115
177
|
});
|
|
116
178
|
const data = await readJsonResponse(response);
|
|
117
179
|
if (!response.ok) {
|
|
118
|
-
throw new ServerApiError(failureMessage(data, response.status), response.status, trace.traceId);
|
|
180
|
+
throw new ServerApiError(failureMessage(data, response.status), response.status, trace.traceId, failureCode(data));
|
|
119
181
|
}
|
|
120
182
|
return data;
|
|
121
183
|
}
|