@thegitai/cli 1.0.0-preview.2 → 1.0.0-preview.20
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 +32 -4
- package/dist/bin/ai.js +57 -291
- package/dist/src/agent-mode.js +1 -1
- package/dist/src/api/auth.js +2 -2
- package/dist/src/api/browser-login.js +72 -3
- package/dist/src/api/chat.js +232 -33
- package/dist/src/api/contracts.js +55 -1
- package/dist/src/api/http.js +16 -3
- package/dist/src/api/models.js +9 -4
- package/dist/src/executor.js +1 -1
- package/dist/src/help-text.js +51 -11
- package/dist/src/permissions.js +243 -0
- package/dist/src/project-index.js +13 -1
- package/dist/src/session-store.js +57 -20
- package/dist/src/session.js +14 -3
- package/dist/src/tool-executor.js +2 -2
- package/dist/src/tools/delete-file.js +14 -0
- package/dist/src/tools/patch-file.js +12 -16
- package/dist/src/tools/replace-document-text.js +28 -18
- package/dist/src/tools/run-command.js +13 -27
- package/dist/src/tools/run-node-script.js +11 -26
- package/dist/src/tools/str-replace.js +12 -16
- package/dist/src/tools/write-file.js +66 -0
- 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 +569 -151
- package/dist/src/ui/tui/bridge.js +10 -0
- package/dist/src/ui/tui/build-frame.js +535 -159
- package/dist/src/ui/tui/markdown-render.js +81 -73
- package/dist/src/ui/tui/shell-input.js +155 -45
- 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 +18 -6
- package/dist/src/markdown-renderer.js +0 -112
package/dist/src/api/chat.js
CHANGED
|
@@ -1,11 +1,13 @@
|
|
|
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 { isUserInputQuestionArray } from './contracts.js';
|
|
5
6
|
import { createTraceContext, normalizeServerUrl, readErrorResponse, } from './http.js';
|
|
6
7
|
import { collectClientEnvironment } from '../client-environment.js';
|
|
7
8
|
import { collectProjectOrientation } from '../project-orientation.js';
|
|
8
9
|
import { autoAttachImages } from '../core/image-path-extractor.js';
|
|
10
|
+
import { formatTurnFailureMarker } from '../turn-failure-marker.js';
|
|
9
11
|
export class TurnCancelledError extends Error {
|
|
10
12
|
name = 'TurnCancelledError';
|
|
11
13
|
constructor(message = 'Turn cancelled.') {
|
|
@@ -17,11 +19,13 @@ export class ChatTurnFailedError extends Error {
|
|
|
17
19
|
category;
|
|
18
20
|
retryable;
|
|
19
21
|
traceId;
|
|
20
|
-
|
|
22
|
+
partialSnapshot;
|
|
23
|
+
constructor(message, category = 'unknown_error', retryable = false, traceId = '', partialSnapshot) {
|
|
21
24
|
super(traceId ? `${message}\nTrace ID: ${traceId}` : message);
|
|
22
25
|
this.category = category;
|
|
23
26
|
this.retryable = retryable;
|
|
24
27
|
this.traceId = traceId;
|
|
28
|
+
this.partialSnapshot = partialSnapshot;
|
|
25
29
|
}
|
|
26
30
|
}
|
|
27
31
|
export function isTurnCancelledError(error) {
|
|
@@ -51,6 +55,7 @@ function parseSseBlock(block) {
|
|
|
51
55
|
return { event, data: text };
|
|
52
56
|
}
|
|
53
57
|
}
|
|
58
|
+
let toolStateSeqCounter = 0;
|
|
54
59
|
function toolStateFromSession(session) {
|
|
55
60
|
return {
|
|
56
61
|
autoYes: session.autoYes,
|
|
@@ -93,17 +98,30 @@ export function preserveCancelledTurnInput(session, input) {
|
|
|
93
98
|
return;
|
|
94
99
|
break;
|
|
95
100
|
}
|
|
96
|
-
session.history.push({
|
|
101
|
+
session.history.push({
|
|
102
|
+
role: 'user',
|
|
103
|
+
parts: [{ text }],
|
|
104
|
+
kind: 'turnStart',
|
|
105
|
+
userInput: text,
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
function appendTurnFailureMarker(session, category) {
|
|
109
|
+
session.history.push({
|
|
110
|
+
role: 'model',
|
|
111
|
+
parts: [{ text: formatTurnFailureMarker(category) }],
|
|
112
|
+
});
|
|
97
113
|
}
|
|
98
114
|
function preserveFailedTurnInput(session, input, category) {
|
|
99
115
|
const text = input.trim();
|
|
100
116
|
if (!text)
|
|
101
117
|
return;
|
|
102
|
-
session.history.push({ role: 'user', parts: [{ text }], kind: 'turnStart' });
|
|
103
118
|
session.history.push({
|
|
104
|
-
role: '
|
|
105
|
-
parts: [{ text
|
|
119
|
+
role: 'user',
|
|
120
|
+
parts: [{ text }],
|
|
121
|
+
kind: 'turnStart',
|
|
122
|
+
userInput: text,
|
|
106
123
|
});
|
|
124
|
+
appendTurnFailureMarker(session, category);
|
|
107
125
|
}
|
|
108
126
|
function historyHasToolCall(session, callId) {
|
|
109
127
|
return session.history.some((entry) => (entry.parts ?? []).some((part) => String(part?.functionCall?.id ?? '') === callId));
|
|
@@ -143,11 +161,16 @@ function publicStatusMessage(data) {
|
|
|
143
161
|
? event.toolName
|
|
144
162
|
: 'tool';
|
|
145
163
|
if (event.phase === 'thinking')
|
|
146
|
-
return '
|
|
164
|
+
return 'Exploring options...';
|
|
165
|
+
if (event.phase === 'analyzing_image') {
|
|
166
|
+
return (event.imageCount ?? 1) > 1 ? 'Analyzing images...' : 'Analyzing image...';
|
|
167
|
+
}
|
|
147
168
|
if (event.phase === 'running_tool')
|
|
148
169
|
return `Running ${toolName}...`;
|
|
149
170
|
if (event.phase === 'waiting_for_tool')
|
|
150
171
|
return `Running ${toolName} locally...`;
|
|
172
|
+
if (event.phase === 'waiting_for_user_input')
|
|
173
|
+
return 'Waiting for your input...';
|
|
151
174
|
return null;
|
|
152
175
|
}
|
|
153
176
|
function normalizeShellJobToolCall(call) {
|
|
@@ -184,6 +207,7 @@ async function postToolResult({ config, turnId, event, result, session, fetchImp
|
|
|
184
207
|
toolCallId: event.call.id,
|
|
185
208
|
result,
|
|
186
209
|
toolState: toolStateFromSession(session),
|
|
210
|
+
toolStateSeq: ++toolStateSeqCounter,
|
|
187
211
|
};
|
|
188
212
|
const trace = createTraceContext(traceId);
|
|
189
213
|
const response = await fetchImpl(`${normalizeServerUrl(config.serverUrl)}/v1/chat/turn/${encodeURIComponent(turnId)}/tool-result`, {
|
|
@@ -202,6 +226,71 @@ async function postToolResult({ config, turnId, event, result, session, fetchImp
|
|
|
202
226
|
throw await readErrorResponse(response, trace.traceId);
|
|
203
227
|
}
|
|
204
228
|
}
|
|
229
|
+
async function postUserInputResult({ config, turnId, requestId, result, fetchImpl, traceId, }) {
|
|
230
|
+
const payload = {
|
|
231
|
+
requestId,
|
|
232
|
+
result,
|
|
233
|
+
};
|
|
234
|
+
const trace = createTraceContext(traceId);
|
|
235
|
+
const response = await fetchImpl(`${normalizeServerUrl(config.serverUrl)}/v1/chat/turn/${encodeURIComponent(turnId)}/user-input-result`, {
|
|
236
|
+
method: 'POST',
|
|
237
|
+
headers: {
|
|
238
|
+
authorization: `Bearer ${config.token}`,
|
|
239
|
+
'content-type': 'application/json',
|
|
240
|
+
...trace.headers,
|
|
241
|
+
},
|
|
242
|
+
body: JSON.stringify(payload),
|
|
243
|
+
});
|
|
244
|
+
if (response.status === 410) {
|
|
245
|
+
return;
|
|
246
|
+
}
|
|
247
|
+
if (!response.ok) {
|
|
248
|
+
throw await readErrorResponse(response, trace.traceId);
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
export async function postInterjection({ config, turnId, text, messageId, fetchImpl = globalThis.fetch, traceId, }) {
|
|
252
|
+
const payload = { text, messageId };
|
|
253
|
+
const trace = createTraceContext(traceId);
|
|
254
|
+
const response = await fetchImpl(`${normalizeServerUrl(config.serverUrl)}/v1/chat/turn/${encodeURIComponent(turnId)}/interject`, {
|
|
255
|
+
method: 'POST',
|
|
256
|
+
headers: {
|
|
257
|
+
authorization: `Bearer ${config.token}`,
|
|
258
|
+
'content-type': 'application/json',
|
|
259
|
+
...trace.headers,
|
|
260
|
+
},
|
|
261
|
+
body: JSON.stringify(payload),
|
|
262
|
+
});
|
|
263
|
+
if (response.status === 410) {
|
|
264
|
+
return 'stale';
|
|
265
|
+
}
|
|
266
|
+
if (!response.ok) {
|
|
267
|
+
throw await readErrorResponse(response, trace.traceId);
|
|
268
|
+
}
|
|
269
|
+
return 'delivered';
|
|
270
|
+
}
|
|
271
|
+
const turnIdOverrides = new WeakMap();
|
|
272
|
+
function enterServerTurnId(session, serverSessionTurnId) {
|
|
273
|
+
const active = turnIdOverrides.get(session);
|
|
274
|
+
if (active) {
|
|
275
|
+
active.depth += 1;
|
|
276
|
+
return;
|
|
277
|
+
}
|
|
278
|
+
turnIdOverrides.set(session, {
|
|
279
|
+
previousTurnId: session.turnState.id,
|
|
280
|
+
depth: 1,
|
|
281
|
+
});
|
|
282
|
+
session.turnState.id = serverSessionTurnId;
|
|
283
|
+
}
|
|
284
|
+
function exitServerTurnId(session) {
|
|
285
|
+
const active = turnIdOverrides.get(session);
|
|
286
|
+
if (!active)
|
|
287
|
+
return;
|
|
288
|
+
active.depth -= 1;
|
|
289
|
+
if (active.depth === 0) {
|
|
290
|
+
session.turnState.id = active.previousTurnId;
|
|
291
|
+
turnIdOverrides.delete(session);
|
|
292
|
+
}
|
|
293
|
+
}
|
|
205
294
|
async function executeAndPostToolResult({ config, projectIndex, session, event, input, fetchImpl, signal, traceId, }) {
|
|
206
295
|
const turnId = String(event?.turnId ?? '').trim();
|
|
207
296
|
if (!turnId || !event?.call?.id || !event.call.name) {
|
|
@@ -210,10 +299,9 @@ async function executeAndPostToolResult({ config, projectIndex, session, event,
|
|
|
210
299
|
if (signal?.aborted) {
|
|
211
300
|
throw new TurnCancelledError();
|
|
212
301
|
}
|
|
213
|
-
const previousTurnId = session.turnState.id;
|
|
214
302
|
const serverSessionTurnId = String(event.sessionTurnId ?? '').trim();
|
|
215
303
|
if (serverSessionTurnId) {
|
|
216
|
-
session
|
|
304
|
+
enterServerTurnId(session, serverSessionTurnId);
|
|
217
305
|
if (!session.clientState.safety.checkpoints.some((checkpoint) => checkpoint.turnId === serverSessionTurnId)) {
|
|
218
306
|
createPromptCheckpoint(session.clientState.safety, 'prompt boundary', serverSessionTurnId);
|
|
219
307
|
}
|
|
@@ -236,10 +324,12 @@ async function executeAndPostToolResult({ config, projectIndex, session, event,
|
|
|
236
324
|
});
|
|
237
325
|
}
|
|
238
326
|
finally {
|
|
239
|
-
|
|
327
|
+
if (serverSessionTurnId) {
|
|
328
|
+
exitServerTurnId(session);
|
|
329
|
+
}
|
|
240
330
|
}
|
|
241
331
|
}
|
|
242
|
-
async function consumeTurnStream({ response, config, projectIndex, session, input, fetchImpl, signal, traceId, }) {
|
|
332
|
+
async function consumeTurnStream({ response, config, projectIndex, session, input, fetchImpl, signal, traceId, onTurnStart, onInterjectionDelivered, }) {
|
|
243
333
|
if (!response.body) {
|
|
244
334
|
throw new Error('Server returned an empty chat stream.');
|
|
245
335
|
}
|
|
@@ -249,8 +339,50 @@ async function consumeTurnStream({ response, config, projectIndex, session, inpu
|
|
|
249
339
|
const finalResult = {
|
|
250
340
|
current: null,
|
|
251
341
|
};
|
|
342
|
+
const pendingParallelTools = [];
|
|
343
|
+
let firstParallelFailure = null;
|
|
344
|
+
let rejectOnParallelFailure = null;
|
|
345
|
+
const parallelToolFailure = new Promise((_, reject) => {
|
|
346
|
+
rejectOnParallelFailure = reject;
|
|
347
|
+
});
|
|
348
|
+
parallelToolFailure.catch(() => { });
|
|
349
|
+
function recordParallelFailure(error) {
|
|
350
|
+
const failure = error ?? new Error('Local tool execution failed.');
|
|
351
|
+
if (firstParallelFailure == null) {
|
|
352
|
+
firstParallelFailure = failure;
|
|
353
|
+
rejectOnParallelFailure?.(failure);
|
|
354
|
+
}
|
|
355
|
+
return failure;
|
|
356
|
+
}
|
|
357
|
+
async function drainParallelTools() {
|
|
358
|
+
if (!pendingParallelTools.length)
|
|
359
|
+
return;
|
|
360
|
+
const pending = pendingParallelTools.splice(0);
|
|
361
|
+
const outcomes = await Promise.all(pending);
|
|
362
|
+
for (const outcome of outcomes) {
|
|
363
|
+
if (outcome != null)
|
|
364
|
+
throw outcome;
|
|
365
|
+
}
|
|
366
|
+
}
|
|
252
367
|
async function handleEvent(event) {
|
|
368
|
+
if (event.event === 'turn-start') {
|
|
369
|
+
const turnId = String(event.data?.turnId ?? '').trim();
|
|
370
|
+
if (turnId)
|
|
371
|
+
onTurnStart?.(turnId);
|
|
372
|
+
return;
|
|
373
|
+
}
|
|
374
|
+
if (event.event === 'interjection-delivered') {
|
|
375
|
+
onInterjectionDelivered?.(event.data);
|
|
376
|
+
return;
|
|
377
|
+
}
|
|
253
378
|
if (event.event === 'status') {
|
|
379
|
+
const data = event.data;
|
|
380
|
+
if (data?.phase === 'analyzing_image') {
|
|
381
|
+
session.onImageAnalysis?.(Math.max(1, Number(data.imageCount ?? 1) || 1));
|
|
382
|
+
}
|
|
383
|
+
else if (data?.phase) {
|
|
384
|
+
session.onImageAnalysis?.(0);
|
|
385
|
+
}
|
|
254
386
|
const message = publicStatusMessage(event.data);
|
|
255
387
|
if (message)
|
|
256
388
|
session.onStatus(message);
|
|
@@ -260,11 +392,26 @@ async function consumeTurnStream({ response, config, projectIndex, session, inpu
|
|
|
260
392
|
return;
|
|
261
393
|
}
|
|
262
394
|
if (event.event === 'tool-call') {
|
|
395
|
+
const data = event.data;
|
|
396
|
+
if (data?.parallelSafe === true) {
|
|
397
|
+
pendingParallelTools.push(executeAndPostToolResult({
|
|
398
|
+
config,
|
|
399
|
+
projectIndex,
|
|
400
|
+
session,
|
|
401
|
+
event: data,
|
|
402
|
+
input,
|
|
403
|
+
fetchImpl,
|
|
404
|
+
signal,
|
|
405
|
+
traceId,
|
|
406
|
+
}).then(() => null, (error) => recordParallelFailure(error)));
|
|
407
|
+
return;
|
|
408
|
+
}
|
|
409
|
+
await drainParallelTools();
|
|
263
410
|
await executeAndPostToolResult({
|
|
264
411
|
config,
|
|
265
412
|
projectIndex,
|
|
266
413
|
session,
|
|
267
|
-
event:
|
|
414
|
+
event: data,
|
|
268
415
|
input,
|
|
269
416
|
fetchImpl,
|
|
270
417
|
signal,
|
|
@@ -279,36 +426,75 @@ async function consumeTurnStream({ response, config, projectIndex, session, inpu
|
|
|
279
426
|
}
|
|
280
427
|
return;
|
|
281
428
|
}
|
|
429
|
+
if (event.event === 'user-input-request') {
|
|
430
|
+
await drainParallelTools();
|
|
431
|
+
const data = event.data;
|
|
432
|
+
const turnId = String(data?.turnId ?? '').trim();
|
|
433
|
+
const requestId = String(data?.requestId ?? '').trim();
|
|
434
|
+
if (!turnId ||
|
|
435
|
+
!requestId ||
|
|
436
|
+
!isUserInputQuestionArray(data?.questions)) {
|
|
437
|
+
throw new Error('Server emitted an invalid user-input request.');
|
|
438
|
+
}
|
|
439
|
+
if (!session.requestUserInput) {
|
|
440
|
+
throw new Error('Interactive user input is unavailable in this client.');
|
|
441
|
+
}
|
|
442
|
+
const result = await session.requestUserInput({ questions: data.questions }, signal);
|
|
443
|
+
if (signal?.aborted) {
|
|
444
|
+
throw new TurnCancelledError();
|
|
445
|
+
}
|
|
446
|
+
await postUserInputResult({
|
|
447
|
+
config,
|
|
448
|
+
turnId,
|
|
449
|
+
requestId,
|
|
450
|
+
result,
|
|
451
|
+
fetchImpl,
|
|
452
|
+
traceId,
|
|
453
|
+
});
|
|
454
|
+
return;
|
|
455
|
+
}
|
|
282
456
|
if (event.event === 'result') {
|
|
457
|
+
await drainParallelTools();
|
|
283
458
|
finalResult.current = event.data;
|
|
284
459
|
return;
|
|
285
460
|
}
|
|
286
461
|
if (event.event === 'cancelled' || event.event === 'error') {
|
|
462
|
+
await drainParallelTools().catch(() => { });
|
|
287
463
|
const message = String(event.data?.message ?? 'Server chat failed.');
|
|
288
464
|
if (event.event === 'cancelled') {
|
|
289
465
|
throw new TurnCancelledError(message);
|
|
290
466
|
}
|
|
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);
|
|
467
|
+
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
468
|
}
|
|
293
469
|
}
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
470
|
+
try {
|
|
471
|
+
while (true) {
|
|
472
|
+
if (signal?.aborted) {
|
|
473
|
+
await reader.cancel().catch(() => { });
|
|
474
|
+
throw new TurnCancelledError();
|
|
475
|
+
}
|
|
476
|
+
const read = await Promise.race([reader.read(), parallelToolFailure]);
|
|
477
|
+
if (read.done)
|
|
478
|
+
break;
|
|
479
|
+
buffer += decoder.decode(read.value, { stream: true });
|
|
480
|
+
let separatorIndex = buffer.indexOf('\n\n');
|
|
481
|
+
while (separatorIndex !== -1) {
|
|
482
|
+
const block = buffer.slice(0, separatorIndex);
|
|
483
|
+
buffer = buffer.slice(separatorIndex + 2);
|
|
484
|
+
const event = parseSseBlock(block);
|
|
485
|
+
if (event)
|
|
486
|
+
await handleEvent(event);
|
|
487
|
+
separatorIndex = buffer.indexOf('\n\n');
|
|
488
|
+
}
|
|
311
489
|
}
|
|
490
|
+
await drainParallelTools();
|
|
491
|
+
}
|
|
492
|
+
catch (error) {
|
|
493
|
+
await reader.cancel().catch(() => { });
|
|
494
|
+
throw error;
|
|
495
|
+
}
|
|
496
|
+
finally {
|
|
497
|
+
await drainParallelTools().catch(() => { });
|
|
312
498
|
}
|
|
313
499
|
buffer += decoder.decode();
|
|
314
500
|
const tail = buffer.trim();
|
|
@@ -322,7 +508,7 @@ async function consumeTurnStream({ response, config, projectIndex, session, inpu
|
|
|
322
508
|
}
|
|
323
509
|
return finalResult.current;
|
|
324
510
|
}
|
|
325
|
-
export async function sendServerUserMessage({ config, projectIndex, session, input, imageAttachments = [], fetchImpl = globalThis.fetch, signal, }) {
|
|
511
|
+
export async function sendServerUserMessage({ config, projectIndex, session, input, imageAttachments = [], fetchImpl = globalThis.fetch, signal, onTurnStart, onInterjectionDelivered, }) {
|
|
326
512
|
const autoAttach = autoAttachImages(input, session.rootDir, imageAttachments);
|
|
327
513
|
const requestImageAttachments = autoAttach.attachments.length > 0
|
|
328
514
|
? [...imageAttachments, ...autoAttach.attachments]
|
|
@@ -379,6 +565,8 @@ export async function sendServerUserMessage({ config, projectIndex, session, inp
|
|
|
379
565
|
fetchImpl,
|
|
380
566
|
signal,
|
|
381
567
|
traceId: trace.traceId,
|
|
568
|
+
onTurnStart,
|
|
569
|
+
onInterjectionDelivered,
|
|
382
570
|
});
|
|
383
571
|
applySessionSnapshot(session, result.snapshot, { preserveAgentMode: true });
|
|
384
572
|
return {
|
|
@@ -395,8 +583,19 @@ export async function sendServerUserMessage({ config, projectIndex, session, inp
|
|
|
395
583
|
? error
|
|
396
584
|
: new TurnCancelledError();
|
|
397
585
|
}
|
|
398
|
-
|
|
399
|
-
|
|
586
|
+
const category = error instanceof ChatTurnFailedError ? error.category : 'unknown_error';
|
|
587
|
+
const partial = error instanceof ChatTurnFailedError ? error.partialSnapshot : undefined;
|
|
588
|
+
if (partial) {
|
|
589
|
+
applySessionSnapshot(session, partial, { preserveAgentMode: true });
|
|
590
|
+
}
|
|
591
|
+
else {
|
|
592
|
+
session.history.length = preTurnHistoryLength;
|
|
593
|
+
preserveFailedTurnInput(session, requestInputBase, category);
|
|
594
|
+
}
|
|
595
|
+
try {
|
|
596
|
+
saveSessionState(session, session.env);
|
|
597
|
+
}
|
|
598
|
+
catch { }
|
|
400
599
|
throw error;
|
|
401
600
|
}
|
|
402
601
|
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
|
+
}
|
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() {
|
|
@@ -96,9 +98,20 @@ export async function readJsonResponse(response) {
|
|
|
96
98
|
export function failureMessage(data, status) {
|
|
97
99
|
return String(data?.error?.message ?? data?.message ?? `Request failed with ${status}`);
|
|
98
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
|
+
}
|
|
99
112
|
export async function readErrorResponse(response, traceId = response.headers.get(TRACE_ID_HEADER) ?? '') {
|
|
100
113
|
const data = await readJsonResponse(response);
|
|
101
|
-
return new ServerApiError(failureMessage(data, response.status), response.status, traceId);
|
|
114
|
+
return new ServerApiError(failureMessage(data, response.status), response.status, traceId, failureCode(data));
|
|
102
115
|
}
|
|
103
116
|
export async function authorizedJson({ config, path, method = 'GET', body = null, headers = {}, fetchImpl = globalThis.fetch, timeoutMs = REQUEST_TIMEOUT_MS, }) {
|
|
104
117
|
const trace = createTraceContext();
|
|
@@ -115,7 +128,7 @@ export async function authorizedJson({ config, path, method = 'GET', body = null
|
|
|
115
128
|
});
|
|
116
129
|
const data = await readJsonResponse(response);
|
|
117
130
|
if (!response.ok) {
|
|
118
|
-
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));
|
|
119
132
|
}
|
|
120
133
|
return data;
|
|
121
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 { REQUEST_TIMEOUT_MS, ServerApiError, createTraceContext, failureMessage, normalizeServerUrl, readJsonResponse, retryTransient, } 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');
|
|
@@ -70,7 +75,7 @@ export async function fetchServerModels({ config, fetchImpl = globalThis.fetch,
|
|
|
70
75
|
});
|
|
71
76
|
const data = (await readJsonResponse(response));
|
|
72
77
|
if (!response.ok) {
|
|
73
|
-
throw new ServerApiError(failureMessage(data, response.status), response.status, trace.traceId);
|
|
78
|
+
throw new ServerApiError(failureMessage(data, response.status), response.status, trace.traceId, failureCode(data));
|
|
74
79
|
}
|
|
75
80
|
const models = Array.isArray(data?.models)
|
|
76
81
|
? data.models.map(sanitizeModelInfo).filter(Boolean)
|
package/dist/src/executor.js
CHANGED
|
@@ -596,7 +596,7 @@ export function commandUsesSudo(command) {
|
|
|
596
596
|
}
|
|
597
597
|
export function sudoPromptFromTail(text) {
|
|
598
598
|
const tail = text.slice(-1000).replace(/\x1b\[[0-9;?]*[A-Za-z]/g, '');
|
|
599
|
-
const match = tail.match(/(?:\[sudo\][^\r\n]*password[^\r\n]*:
|
|
599
|
+
const match = tail.match(/(?:\[sudo\][^\r\n]*password[^\r\n]*: ?|\[?sudo[^\r\n]*password[^\r\n]*: ?|password[^\r\n]*: ?)$/i);
|
|
600
600
|
return match?.[0] ?? null;
|
|
601
601
|
}
|
|
602
602
|
function isSudoPromptLine(text) {
|
package/dist/src/help-text.js
CHANGED
|
@@ -22,6 +22,7 @@ const HELP_MARKDOWN = [
|
|
|
22
22
|
'',
|
|
23
23
|
'- `ai` — start an interactive chat session in the current repo',
|
|
24
24
|
'- `ai "<request>"` — start an interactive session with `<request>` as the first message',
|
|
25
|
+
'- Coding sessions require terminal stdin and stdout; piped prompts are not supported.',
|
|
25
26
|
'',
|
|
26
27
|
'## Auth',
|
|
27
28
|
'',
|
|
@@ -35,8 +36,8 @@ const HELP_MARKDOWN = [
|
|
|
35
36
|
'',
|
|
36
37
|
'- `ai --list-sessions` — list saved sessions for this repo',
|
|
37
38
|
'- `ai --session <id|name>` — resume a saved session by id or name',
|
|
38
|
-
'- Sessions are stored locally and
|
|
39
|
-
'
|
|
39
|
+
'- Sessions are stored locally and can be listed or resumed in the same repo.',
|
|
40
|
+
' Continuing one requires the TheGitAI account used for that session.',
|
|
40
41
|
'',
|
|
41
42
|
'## Options',
|
|
42
43
|
'',
|
|
@@ -46,7 +47,9 @@ const HELP_MARKDOWN = [
|
|
|
46
47
|
'',
|
|
47
48
|
'## Modes',
|
|
48
49
|
'',
|
|
49
|
-
'- Default — asks before
|
|
50
|
+
'- Default — asks before creating, editing or deleting files and before',
|
|
51
|
+
' running commands. Each answer can be remembered for the rest of the',
|
|
52
|
+
' session; see Safety & approvals.',
|
|
50
53
|
'- Auto-Accept — approves shell commands and file edits for the session.',
|
|
51
54
|
'- Plan — read-only; the agent can inspect, ask typed questions, and plan,',
|
|
52
55
|
' with file-reading shell commands only. It will not edit files or run tests,',
|
|
@@ -56,7 +59,15 @@ const HELP_MARKDOWN = [
|
|
|
56
59
|
'## Keys & clipboard',
|
|
57
60
|
'',
|
|
58
61
|
'- **Enter** sends • **Shift+Tab** cycles modes • **Esc** cancels the turn •',
|
|
59
|
-
' **Ctrl+C**
|
|
62
|
+
' **Ctrl+C** clears the composer or the queued message, and quits once there',
|
|
63
|
+
' is nothing left to clear. These are the same on macOS, Linux, and Windows.',
|
|
64
|
+
'- **While the agent is working**, Enter queues your message and locks the',
|
|
65
|
+
' composer. Press **Enter** again to send it into the running turn: the agent',
|
|
66
|
+
' picks it up at its next step instead of waiting for the turn to finish, and',
|
|
67
|
+
' the Your messages panel tracks it from queued to delivered. **↑** brings it',
|
|
68
|
+
' back for editing and **Esc**/**Ctrl+C** discards it; both unlock the',
|
|
69
|
+
' composer for the next message. Messages with images cannot be sent mid-turn',
|
|
70
|
+
' and are sent with the next prompt instead.',
|
|
60
71
|
`- **Paste** into the composer with your terminal's paste shortcut (\`${PASTE_SHORTCUT}\``,
|
|
61
72
|
' on this system) or by right-clicking the composer.',
|
|
62
73
|
'- **Copy** from the transcript by dragging to select; double-click copies a',
|
|
@@ -77,14 +88,33 @@ const HELP_MARKDOWN = [
|
|
|
77
88
|
' browse them, press Enter to expand one and read its output, k to stop it',
|
|
78
89
|
'- `/jobs output <id>` — print one job\'s full captured output',
|
|
79
90
|
'- `/jobs kill <id>` — stop one background job',
|
|
80
|
-
'- `/
|
|
91
|
+
'- `/new` — start a new conversation; this session remains saved',
|
|
81
92
|
'- `/exit` — quit the session',
|
|
82
93
|
'',
|
|
83
94
|
'## Safety & approvals',
|
|
84
95
|
'',
|
|
85
|
-
'- TheGitAI asks before
|
|
86
|
-
'
|
|
87
|
-
'
|
|
96
|
+
'- In Default mode TheGitAI asks before it creates a file, edits an existing',
|
|
97
|
+
' file, deletes a file, or runs a command. These are four separate',
|
|
98
|
+
' permissions: allowing edits does not allow deletions, and allowing file',
|
|
99
|
+
' changes does not allow commands.',
|
|
100
|
+
'- At each prompt: **↑/↓** moves between choices, **Enter** confirms the',
|
|
101
|
+
' highlighted one, and **Esc** denies. Deny is selected by default.',
|
|
102
|
+
' Single-letter shortcuts were removed on purpose: a prompt can appear',
|
|
103
|
+
' while you are typing, and a stray letter must never approve anything.',
|
|
104
|
+
'- Every prompt offers Approve once, an "Always allow" for that kind of',
|
|
105
|
+
' action, and Deny. A command also offers to remember just its prefix — for',
|
|
106
|
+
' example approving `npm test -- --watch=false` can allow `npm test`.',
|
|
107
|
+
'- A prefix is only offered when the command shows a plain verb, as in',
|
|
108
|
+
' `npm test` or `git status`. Commands with no verb (`ls -la`), interpreters',
|
|
109
|
+
' and wrappers (`bash -c`, `python -c`, `env`, `timeout`, `make`), and',
|
|
110
|
+
' destructive or network binaries (`rm`, `sudo`, `curl`) are never offered',
|
|
111
|
+
' one — a bare binary grant would mean "anything this program can do".',
|
|
112
|
+
' `npm run` must name its script, so the grant is `npm run build`.',
|
|
113
|
+
'- A remembered prefix never covers a command that chains, pipes, redirects,',
|
|
114
|
+
' or substitutes, so allowing `npm test` can never green-light',
|
|
115
|
+
' `npm test && rm -rf build`.',
|
|
116
|
+
'- Everything you allow lasts for the current session only and is forgotten',
|
|
117
|
+
' when it ends. `/new` clears it immediately.',
|
|
88
118
|
'- If an approved `sudo` command needs a password, the TUI shows the exact',
|
|
89
119
|
' command and keeps the password masked and local.',
|
|
90
120
|
'- `-y` / `--yes` at startup auto-approves every shell command and file',
|
|
@@ -105,7 +135,10 @@ const HELP_MARKDOWN = [
|
|
|
105
135
|
'- Auth or permission errors → run `ai whoami` to confirm the signed-in',
|
|
106
136
|
' account.',
|
|
107
137
|
'- Usage or quota errors → run `ai --usage`.',
|
|
108
|
-
'-
|
|
138
|
+
'- Signed in with the wrong credentials → `ai logout`, then `ai login` with',
|
|
139
|
+
' the account you intended to use.',
|
|
140
|
+
'- A local session was used with a different sign-in → sign in with the',
|
|
141
|
+
' account you used for that session or start a new session.',
|
|
109
142
|
'- For anything else, re-run the command and report the printed error',
|
|
110
143
|
' message — there is no client-side debug mode by design.',
|
|
111
144
|
].join('\n');
|
|
@@ -126,8 +159,15 @@ export function formatInteractiveHelpText() {
|
|
|
126
159
|
return HELP_MARKDOWN;
|
|
127
160
|
}
|
|
128
161
|
export function formatCliHelpText({ color = false } = {}) {
|
|
129
|
-
if (!color)
|
|
130
|
-
return HELP_MARKDOWN
|
|
162
|
+
if (!color) {
|
|
163
|
+
return HELP_MARKDOWN.split('\n')
|
|
164
|
+
.map((line) => line
|
|
165
|
+
.replace(/^#{1,6}\s+/, '')
|
|
166
|
+
.replace(/^-\s+/, ' ')
|
|
167
|
+
.replace(/`([^`]+)`/g, '$1')
|
|
168
|
+
.replace(/\*\*([^*]+)\*\*/g, '$1'))
|
|
169
|
+
.join('\n');
|
|
170
|
+
}
|
|
131
171
|
return HELP_MARKDOWN.split('\n')
|
|
132
172
|
.map((line) => {
|
|
133
173
|
const heading = line.match(/^(#{1,6})\s+(.*)$/);
|