@thegitai/cli 1.0.0-beta.2 → 1.0.0-beta.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.
Files changed (55) hide show
  1. package/README.md +37 -2
  2. package/dist/bin/ai.js +148 -75
  3. package/dist/parsers/NOTICE +18 -0
  4. package/dist/src/agent-mode.js +5 -0
  5. package/dist/src/api/auth.js +6 -4
  6. package/dist/src/api/browser-login.js +7 -41
  7. package/dist/src/api/chat.js +77 -20
  8. package/dist/src/api/http.js +81 -4
  9. package/dist/src/api/models.js +26 -18
  10. package/dist/src/artifact-policy.js +12 -0
  11. package/dist/src/background-jobs.js +410 -0
  12. package/dist/src/cli-args.js +60 -0
  13. package/dist/src/client-environment.js +129 -0
  14. package/dist/src/colors.js +50 -0
  15. package/dist/src/core/clipboard.js +75 -0
  16. package/dist/src/core/image-path-extractor.js +144 -0
  17. package/dist/src/edit-journal.js +39 -6
  18. package/dist/src/executor.js +48 -12
  19. package/dist/src/help-text.js +24 -5
  20. package/dist/src/markdown-renderer.js +1 -1
  21. package/dist/src/patcher.js +17 -2
  22. package/dist/src/scanner.js +58 -17
  23. package/dist/src/scratch-dir.js +57 -0
  24. package/dist/src/secret-preview.js +0 -10
  25. package/dist/src/session-safety.js +64 -31
  26. package/dist/src/session-store.js +0 -1
  27. package/dist/src/todo-list.js +106 -0
  28. package/dist/src/tool-executor.js +164 -18
  29. package/dist/src/tools/delete-file.js +1 -1
  30. package/dist/src/tools/index.js +8 -0
  31. package/dist/src/tools/patch-file.js +16 -2
  32. package/dist/src/tools/path-suggest.js +139 -0
  33. package/dist/src/tools/read-document.js +15 -4
  34. package/dist/src/tools/read-file.js +23 -7
  35. package/dist/src/tools/replace-document-text.js +234 -0
  36. package/dist/src/tools/restore-checkpoint.js +1 -1
  37. package/dist/src/tools/run-command.js +83 -16
  38. package/dist/src/tools/run-node-script.js +3 -1
  39. package/dist/src/tools/shell-job-kill.js +48 -0
  40. package/dist/src/tools/shell-job-output.js +51 -0
  41. package/dist/src/tools/str-replace.js +16 -2
  42. package/dist/src/tools/undo-edit.js +7 -5
  43. package/dist/src/tools/update-todos.js +27 -0
  44. package/dist/src/tools/write-file.js +14 -1
  45. package/dist/src/tree-sitter-runtime.js +8 -1
  46. package/dist/src/ui/repl.js +315 -24
  47. package/dist/src/ui/tui/bridge.js +2 -6
  48. package/dist/src/ui/tui/build-frame.js +224 -25
  49. package/dist/src/ui/tui/shell-input.js +42 -5
  50. package/dist/src/version.js +29 -0
  51. package/dist/vendor/web-tree-sitter/LICENSE +21 -0
  52. package/dist/vendor/web-tree-sitter/NOTICE +13 -0
  53. package/dist/vendor/web-tree-sitter/web-tree-sitter.cjs +4063 -0
  54. package/dist/vendor/web-tree-sitter/web-tree-sitter.wasm +0 -0
  55. package/package.json +14 -15
@@ -1,7 +1,10 @@
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
- import { normalizeServerUrl, readErrorResponse, } from './http.js';
5
+ import { createTraceContext, normalizeServerUrl, readErrorResponse, } from './http.js';
6
+ import { collectClientEnvironment } from '../client-environment.js';
7
+ import { autoAttachImages } from '../core/image-path-extractor.js';
5
8
  export class TurnCancelledError extends Error {
6
9
  name = 'TurnCancelledError';
7
10
  constructor(message = 'Turn cancelled.') {
@@ -12,10 +15,12 @@ export class ChatTurnFailedError extends Error {
12
15
  name = 'ChatTurnFailedError';
13
16
  category;
14
17
  retryable;
15
- constructor(message, category = 'unknown_error', retryable = false) {
16
- super(message);
18
+ traceId;
19
+ constructor(message, category = 'unknown_error', retryable = false, traceId = '') {
20
+ super(traceId ? `${message}\nTrace ID: ${traceId}` : message);
17
21
  this.category = category;
18
22
  this.retryable = retryable;
23
+ this.traceId = traceId;
19
24
  }
20
25
  }
21
26
  export function isTurnCancelledError(error) {
@@ -60,6 +65,11 @@ function snapshotForServer(session) {
60
65
  snapshot.clientState.safety = sanitizeSessionSafetyForServer(snapshot.clientState.safety);
61
66
  return snapshot;
62
67
  }
68
+ function imageAttachmentsForServer(attachments) {
69
+ return (attachments ?? []).map(({ filePath, ...attachment }) => attachment.source === 'file' && filePath
70
+ ? { ...attachment, filePath }
71
+ : attachment);
72
+ }
63
73
  function userHistoryText(entry) {
64
74
  return (entry.parts ?? [])
65
75
  .map((part) => (typeof part?.text === 'string' ? part.text : ''))
@@ -139,17 +149,48 @@ function publicStatusMessage(data) {
139
149
  return `Running ${toolName} locally...`;
140
150
  return null;
141
151
  }
142
- async function postToolResult({ config, turnId, event, result, session, fetchImpl, }) {
152
+ function normalizeShellJobToolCall(call) {
153
+ if (call.name !== 'shell_job_output' && call.name !== 'shell_job_kill') {
154
+ return call;
155
+ }
156
+ const args = call.args && typeof call.args === 'object' && !Array.isArray(call.args)
157
+ ? { ...call.args }
158
+ : {};
159
+ let changed = false;
160
+ if (args.job_id === undefined) {
161
+ const alias = args.jobId ?? args.id;
162
+ if (alias !== undefined) {
163
+ args.job_id = alias;
164
+ delete args.jobId;
165
+ delete args.id;
166
+ changed = true;
167
+ }
168
+ }
169
+ if (call.name === 'shell_job_output' && args.wait_ms === undefined) {
170
+ const alias = args.waitMs ?? args.wait ?? args.wait_millis;
171
+ if (alias !== undefined) {
172
+ args.wait_ms = alias;
173
+ delete args.waitMs;
174
+ delete args.wait;
175
+ delete args.wait_millis;
176
+ changed = true;
177
+ }
178
+ }
179
+ return changed ? { ...call, args } : call;
180
+ }
181
+ async function postToolResult({ config, turnId, event, result, session, fetchImpl, traceId, }) {
143
182
  const payload = {
144
183
  toolCallId: event.call.id,
145
184
  result,
146
185
  toolState: toolStateFromSession(session),
147
186
  };
187
+ const trace = createTraceContext(traceId);
148
188
  const response = await fetchImpl(`${normalizeServerUrl(config.serverUrl)}/v1/chat/turn/${encodeURIComponent(turnId)}/tool-result`, {
149
189
  method: 'POST',
150
190
  headers: {
151
191
  authorization: `Bearer ${config.token}`,
152
192
  'content-type': 'application/json',
193
+ ...trace.headers,
153
194
  },
154
195
  body: JSON.stringify(payload),
155
196
  });
@@ -157,10 +198,10 @@ async function postToolResult({ config, turnId, event, result, session, fetchImp
157
198
  return;
158
199
  }
159
200
  if (!response.ok) {
160
- throw await readErrorResponse(response);
201
+ throw await readErrorResponse(response, trace.traceId);
161
202
  }
162
203
  }
163
- async function executeAndPostToolResult({ config, projectIndex, session, event, input, fetchImpl, signal, }) {
204
+ async function executeAndPostToolResult({ config, projectIndex, session, event, input, fetchImpl, signal, traceId, }) {
164
205
  const turnId = String(event?.turnId ?? '').trim();
165
206
  if (!turnId || !event?.call?.id || !event.call.name) {
166
207
  throw new Error('Server emitted an invalid tool-call event.');
@@ -177,8 +218,9 @@ async function executeAndPostToolResult({ config, projectIndex, session, event,
177
218
  }
178
219
  }
179
220
  try {
180
- const rawResult = await executeLocalToolCall({ projectIndex }, session, event.call);
181
- preserveCancelledTurnToolResult(session, input, event, rawResult);
221
+ const call = normalizeShellJobToolCall(event.call);
222
+ const rawResult = await executeLocalToolCall({ projectIndex }, session, call);
223
+ preserveCancelledTurnToolResult(session, input, { ...event, call }, rawResult);
182
224
  if (signal?.aborted) {
183
225
  throw new TurnCancelledError();
184
226
  }
@@ -189,13 +231,14 @@ async function executeAndPostToolResult({ config, projectIndex, session, event,
189
231
  result: rawResult,
190
232
  session,
191
233
  fetchImpl,
234
+ traceId,
192
235
  });
193
236
  }
194
237
  finally {
195
238
  session.turnState.id = previousTurnId;
196
239
  }
197
240
  }
198
- async function consumeTurnStream({ response, config, projectIndex, session, input, fetchImpl, signal, }) {
241
+ async function consumeTurnStream({ response, config, projectIndex, session, input, fetchImpl, signal, traceId, }) {
199
242
  if (!response.body) {
200
243
  throw new Error('Server returned an empty chat stream.');
201
244
  }
@@ -224,6 +267,7 @@ async function consumeTurnStream({ response, config, projectIndex, session, inpu
224
267
  input,
225
268
  fetchImpl,
226
269
  signal,
270
+ traceId,
227
271
  });
228
272
  return;
229
273
  }
@@ -243,7 +287,7 @@ async function consumeTurnStream({ response, config, projectIndex, session, inpu
243
287
  if (event.event === 'cancelled') {
244
288
  throw new TurnCancelledError(message);
245
289
  }
246
- throw new ChatTurnFailedError(message, typeof event.data?.category === 'string' ? event.data.category : 'unknown_error', Boolean(event.data?.retryable));
290
+ 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);
247
291
  }
248
292
  }
249
293
  while (true) {
@@ -278,17 +322,31 @@ async function consumeTurnStream({ response, config, projectIndex, session, inpu
278
322
  return finalResult.current;
279
323
  }
280
324
  export async function sendServerUserMessage({ config, projectIndex, session, input, imageAttachments = [], fetchImpl = globalThis.fetch, signal, }) {
325
+ const autoAttach = autoAttachImages(input, session.rootDir, imageAttachments);
326
+ const requestImageAttachments = autoAttach.attachments.length > 0
327
+ ? [...imageAttachments, ...autoAttach.attachments]
328
+ : imageAttachments;
329
+ const requestInputBase = autoAttach.attachments.length > 0 ? autoAttach.sanitizedInput : input;
330
+ const backgroundJobUpdate = drainBackgroundJobNotifications({
331
+ sessionId: session.sessionId,
332
+ });
333
+ for (const err of autoAttach.errors) {
334
+ session.onStatus(`Image: ${err}`);
335
+ }
281
336
  const request = {
282
337
  modelId: session.modelId,
283
338
  session: snapshotForServer(session),
284
- input,
285
- imageAttachments,
339
+ input: requestInputBase,
340
+ backgroundJobUpdate: backgroundJobUpdate || undefined,
341
+ clientEnvironment: collectClientEnvironment({ env: session.env }),
342
+ imageAttachments: imageAttachmentsForServer(requestImageAttachments),
286
343
  maxToolSteps: session.maxToolSteps,
287
344
  autoYes: session.autoYes,
288
345
  agentMode: session.agentMode,
289
346
  };
347
+ const trace = createTraceContext();
290
348
  const preTurnHistoryLength = session.history.length;
291
- const preserveOnAbort = () => preserveCancelledTurnInput(session, input);
349
+ const preserveOnAbort = () => preserveCancelledTurnInput(session, requestInputBase);
292
350
  if (signal?.aborted) {
293
351
  preserveOnAbort();
294
352
  }
@@ -302,21 +360,23 @@ export async function sendServerUserMessage({ config, projectIndex, session, inp
302
360
  accept: 'text/event-stream',
303
361
  authorization: `Bearer ${config.token}`,
304
362
  'content-type': 'application/json',
363
+ ...trace.headers,
305
364
  },
306
365
  body: JSON.stringify(request),
307
366
  signal,
308
367
  });
309
368
  if (!response.ok) {
310
- throw await readErrorResponse(response);
369
+ throw await readErrorResponse(response, trace.traceId);
311
370
  }
312
371
  const result = await consumeTurnStream({
313
372
  response,
314
373
  config,
315
374
  projectIndex,
316
375
  session,
317
- input,
376
+ input: requestInputBase,
318
377
  fetchImpl,
319
378
  signal,
379
+ traceId: trace.traceId,
320
380
  });
321
381
  applySessionSnapshot(session, result.snapshot, { preserveAgentMode: true });
322
382
  return {
@@ -328,16 +388,13 @@ export async function sendServerUserMessage({ config, projectIndex, session, inp
328
388
  }
329
389
  catch (error) {
330
390
  if (isTurnCancelledError(error)) {
331
- preserveCancelledTurnInput(session, input);
391
+ preserveCancelledTurnInput(session, requestInputBase);
332
392
  throw error instanceof TurnCancelledError
333
393
  ? error
334
394
  : new TurnCancelledError();
335
395
  }
336
- // Non-cancel failures (e.g. upstream connection errors) must not leave
337
- // speculative cancelled-turn entries in history — otherwise the next
338
- // request replays a malformed transcript to the server.
339
396
  session.history.length = preTurnHistoryLength;
340
- preserveFailedTurnInput(session, input, error instanceof ChatTurnFailedError ? error.category : 'unknown_error');
397
+ preserveFailedTurnInput(session, requestInputBase, error instanceof ChatTurnFailedError ? error.category : 'unknown_error');
341
398
  throw error;
342
399
  }
343
400
  finally {
@@ -1,4 +1,78 @@
1
+ import { randomUUID } from 'node:crypto';
1
2
  const DEFAULT_SERVER_URL = 'https://thegit.ai';
3
+ export const TRACE_ID_HEADER = 'x-thegitai-trace-id';
4
+ export const CLIENT_HEADER = 'x-thegitai-client';
5
+ export const CLIENT_PLATFORM_HEADER = 'x-thegitai-client-platform';
6
+ export class ServerApiError extends Error {
7
+ status;
8
+ traceId;
9
+ constructor(message, status, traceId) {
10
+ super(traceId ? `${message}\nTrace ID: ${traceId}` : message);
11
+ this.name = 'ServerApiError';
12
+ this.status = status;
13
+ this.traceId = traceId;
14
+ }
15
+ }
16
+ export function createTraceId() {
17
+ return `tr_${randomUUID().replace(/-/g, '')}`;
18
+ }
19
+ export function createTraceContext(traceId = createTraceId()) {
20
+ return {
21
+ traceId,
22
+ headers: {
23
+ [TRACE_ID_HEADER]: traceId,
24
+ [CLIENT_HEADER]: 'cli',
25
+ [CLIENT_PLATFORM_HEADER]: `${process.platform}/${process.arch}`,
26
+ },
27
+ };
28
+ }
29
+ export const REQUEST_TIMEOUT_MS = 8000;
30
+ const TRANSIENT_NETWORK_CODES = new Set([
31
+ 'ECONNRESET',
32
+ 'ECONNREFUSED',
33
+ 'ETIMEDOUT',
34
+ 'EAI_AGAIN',
35
+ 'ENOTFOUND',
36
+ 'ENETUNREACH',
37
+ 'EHOSTUNREACH',
38
+ 'EPIPE',
39
+ 'UND_ERR_CONNECT_TIMEOUT',
40
+ 'UND_ERR_SOCKET',
41
+ 'UND_ERR_HEADERS_TIMEOUT',
42
+ 'UND_ERR_BODY_TIMEOUT',
43
+ ]);
44
+ export function isTransientNetworkError(error) {
45
+ if (error instanceof ServerApiError)
46
+ return false;
47
+ if (!error || typeof error !== 'object')
48
+ return false;
49
+ const err = error;
50
+ if (err.name === 'AbortError' || err.name === 'TimeoutError')
51
+ return true;
52
+ const code = err.code ?? err.cause?.code;
53
+ if (code) {
54
+ return TRANSIENT_NETWORK_CODES.has(code);
55
+ }
56
+ if (error instanceof TypeError && /fetch failed/i.test(err.message ?? '')) {
57
+ return true;
58
+ }
59
+ return false;
60
+ }
61
+ export async function retryTransient(run, { retries = 2, baseDelayMs = 400 } = {}) {
62
+ let attempt = 0;
63
+ for (;;) {
64
+ try {
65
+ return await run();
66
+ }
67
+ catch (error) {
68
+ if (attempt >= retries || !isTransientNetworkError(error))
69
+ throw error;
70
+ const delayMs = baseDelayMs * 2 ** attempt;
71
+ attempt += 1;
72
+ await new Promise((resolve) => setTimeout(resolve, delayMs));
73
+ }
74
+ }
75
+ }
2
76
  export function normalizeServerUrl(serverUrl) {
3
77
  const normalized = String(serverUrl || DEFAULT_SERVER_URL)
4
78
  .trim()
@@ -22,23 +96,26 @@ export async function readJsonResponse(response) {
22
96
  export function failureMessage(data, status) {
23
97
  return String(data?.error?.message ?? data?.message ?? `Request failed with ${status}`);
24
98
  }
25
- export async function readErrorResponse(response) {
99
+ export async function readErrorResponse(response, traceId = response.headers.get(TRACE_ID_HEADER) ?? '') {
26
100
  const data = await readJsonResponse(response);
27
- return new Error(failureMessage(data, response.status));
101
+ return new ServerApiError(failureMessage(data, response.status), response.status, traceId);
28
102
  }
29
- export async function authorizedJson({ config, path, method = 'GET', body = null, headers = {}, fetchImpl = globalThis.fetch, }) {
103
+ export async function authorizedJson({ config, path, method = 'GET', body = null, headers = {}, fetchImpl = globalThis.fetch, timeoutMs = REQUEST_TIMEOUT_MS, }) {
104
+ const trace = createTraceContext();
30
105
  const response = await fetchImpl(`${normalizeServerUrl(config.serverUrl)}${path}`, {
31
106
  method,
32
107
  headers: {
33
108
  authorization: `Bearer ${config.token}`,
109
+ ...trace.headers,
34
110
  ...headers,
35
111
  ...(body === null ? {} : { 'content-type': 'application/json' }),
36
112
  },
37
113
  body: body === null ? undefined : JSON.stringify(body),
114
+ signal: AbortSignal.timeout(timeoutMs),
38
115
  });
39
116
  const data = await readJsonResponse(response);
40
117
  if (!response.ok) {
41
- throw new Error(failureMessage(data, response.status));
118
+ throw new ServerApiError(failureMessage(data, response.status), response.status, trace.traceId);
42
119
  }
43
120
  return data;
44
121
  }
@@ -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 { failureMessage, normalizeServerUrl, readJsonResponse, } from './http.js';
4
+ import { REQUEST_TIMEOUT_MS, ServerApiError, createTraceContext, failureMessage, normalizeServerUrl, readJsonResponse, retryTransient, } from './http.js';
5
5
  function sanitizeModelInfo(raw) {
6
6
  if (!raw || typeof raw !== 'object') {
7
7
  return null;
@@ -45,6 +45,11 @@ export function readCachedServerModels(env = process.env) {
45
45
  return null;
46
46
  }
47
47
  }
48
+ export function selectCacheForServer(cached, serverUrl) {
49
+ if (!cached)
50
+ return null;
51
+ return cached.serverUrl === normalizeServerUrl(serverUrl) ? cached : null;
52
+ }
48
53
  export function writeCachedServerModels(cache, env = process.env) {
49
54
  const filePath = getModelsCachePath(env);
50
55
  mkdirSync(path.dirname(filePath), { recursive: true, mode: 0o700 });
@@ -54,24 +59,27 @@ export function writeCachedServerModels(cache, env = process.env) {
54
59
  });
55
60
  }
56
61
  export async function fetchServerModels({ config, fetchImpl = globalThis.fetch, }) {
57
- const response = await fetchImpl(`${normalizeServerUrl(config.serverUrl)}/v1/models`, {
58
- headers: {
59
- authorization: `Bearer ${config.token}`,
60
- },
62
+ return retryTransient(async () => {
63
+ const trace = createTraceContext();
64
+ const response = await fetchImpl(`${normalizeServerUrl(config.serverUrl)}/v1/models`, {
65
+ headers: {
66
+ authorization: `Bearer ${config.token}`,
67
+ ...trace.headers,
68
+ },
69
+ signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
70
+ });
71
+ const data = (await readJsonResponse(response));
72
+ if (!response.ok) {
73
+ throw new ServerApiError(failureMessage(data, response.status), response.status, trace.traceId);
74
+ }
75
+ const models = Array.isArray(data?.models)
76
+ ? data.models.map(sanitizeModelInfo).filter(Boolean)
77
+ : [];
78
+ if (models.length === 0) {
79
+ throw new Error('Server returned an invalid model list.');
80
+ }
81
+ return { models };
61
82
  });
62
- const data = (await readJsonResponse(response));
63
- if (!response.ok) {
64
- throw new Error(failureMessage(data, response.status));
65
- }
66
- const models = Array.isArray(data?.models)
67
- ? data.models.map(sanitizeModelInfo).filter(Boolean)
68
- : [];
69
- if (models.length === 0) {
70
- throw new Error('Server returned an invalid model list.');
71
- }
72
- return {
73
- models,
74
- };
75
83
  }
76
84
  export function selectServerModel({ requestedModelId, cached, serverModels, }) {
77
85
  const supportedIds = new Set(serverModels.models.map((model) => model.id));
@@ -37,6 +37,15 @@ export const BINARY_ARTIFACT_EXTENSIONS = createArtifactNameSet([
37
37
  '.bz2',
38
38
  '.7z',
39
39
  '.pdf',
40
+ '.doc',
41
+ '.docx',
42
+ '.docm',
43
+ '.dot',
44
+ '.dotx',
45
+ '.dotm',
46
+ '.xls',
47
+ '.xlsx',
48
+ '.xlsm',
40
49
  '.exe',
41
50
  '.dll',
42
51
  '.so',
@@ -145,6 +154,8 @@ const SENSITIVE_BASENAME_PATTERNS = [
145
154
  /^\.?pypirc$/i,
146
155
  /^credentials(?:\..*)?$/i,
147
156
  /^secrets?(?:\..*)?$/i,
157
+ /^service[-_]?account(?:\..*)?\.json$/i,
158
+ /^.*credentials.*\.json$/i,
148
159
  ];
149
160
  const SENSITIVE_PATH_PATTERNS = [
150
161
  /(^|[/\\])\.aws[/\\]credentials$/i,
@@ -152,6 +163,7 @@ const SENSITIVE_PATH_PATTERNS = [
152
163
  /(^|[/\\])credentials?([._-]|$)/i,
153
164
  /(^|[/\\])secrets?([._-]|$)/i,
154
165
  /(^|[/\\])private[-_]?key([._-]|$)/i,
166
+ /(^|[/\\])service[-_]?account/i,
155
167
  /\.(?:pem|key|p12|pfx)$/i,
156
168
  ];
157
169
  export function normalizeArtifactPath(relPath) {