@thegitai/cli 1.0.0-beta.4 → 1.0.0-beta.5

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.
@@ -1,7 +1,7 @@
1
1
  import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync, } from 'node:fs';
2
2
  import path from 'node:path';
3
3
  import { getClientStateDir } from '../client-state.js';
4
- import { authorizedJson, failureMessage, normalizeServerUrl, readJsonResponse, } from './http.js';
4
+ import { ServerApiError, authorizedJson, createTraceContext, failureMessage, normalizeServerUrl, readJsonResponse, } from './http.js';
5
5
  export function getAuthConfigPath(env = process.env) {
6
6
  const configured = String(env.THEGITAI_AUTH_CONFIG ?? '').trim();
7
7
  if (configured) {
@@ -68,14 +68,16 @@ export async function fetchWhoamiResponse({ config, fetchImpl = globalThis.fetch
68
68
  };
69
69
  }
70
70
  export async function logoutFromServer({ config, fetchImpl = globalThis.fetch, }) {
71
+ const trace = createTraceContext();
71
72
  const response = await fetchImpl(`${normalizeServerUrl(config.serverUrl)}/v1/auth/logout`, {
72
73
  method: 'POST',
73
74
  headers: {
74
75
  authorization: `Bearer ${config.token}`,
76
+ ...trace.headers,
75
77
  },
76
78
  });
77
79
  if (!response.ok && response.status !== 401) {
78
80
  const data = (await readJsonResponse(response));
79
- throw new Error(failureMessage(data, response.status));
81
+ throw new ServerApiError(failureMessage(data, response.status), response.status, trace.traceId);
80
82
  }
81
83
  }
@@ -2,7 +2,7 @@ import crypto from 'node:crypto';
2
2
  import http from 'node:http';
3
3
  import os from 'node:os';
4
4
  import { openUrl } from '../core/open-url.js';
5
- import { failureMessage, normalizeServerUrl, readJsonResponse, } from './http.js';
5
+ import { ServerApiError, createTraceContext, failureMessage, normalizeServerUrl, readJsonResponse, } from './http.js';
6
6
  const DEFAULT_WEBSITE_URL = 'https://thegit.ai';
7
7
  const DEFAULT_DEV_WEBSITE_URL = 'http://localhost:3002';
8
8
  const DEFAULT_SERVER_URL = 'https://thegit.ai';
@@ -73,14 +73,15 @@ const RESULT_PAGE = (heading, detail) => `<!doctype html><html><head><meta chars
73
73
  `p{color:#9aa0a6}</style></head><body><div class="card"><h1>${heading}</h1>` +
74
74
  `<p>${detail}</p></div></body></html>`;
75
75
  async function exchangeCodeForToken({ serverUrl, code, codeVerifier, fetchImpl, }) {
76
+ const trace = createTraceContext();
76
77
  const response = await fetchImpl(`${serverUrl}/v1/cli/auth/token`, {
77
78
  method: 'POST',
78
- headers: { 'content-type': 'application/json' },
79
+ headers: { 'content-type': 'application/json', ...trace.headers },
79
80
  body: JSON.stringify({ code, code_verifier: codeVerifier }),
80
81
  });
81
82
  const data = (await readJsonResponse(response));
82
83
  if (!response.ok) {
83
- throw new Error(failureMessage(data, response.status));
84
+ throw new ServerApiError(failureMessage(data, response.status), response.status, trace.traceId);
84
85
  }
85
86
  const token = String(data?.token ?? '').trim();
86
87
  const customer = data?.customer;
@@ -1,7 +1,7 @@
1
1
  import { createPromptCheckpoint, sanitizeSessionSafetyForServer, } from '../session-safety.js';
2
2
  import { applySessionSnapshot, snapshotFromSession, } from '../session-store.js';
3
3
  import { executeLocalToolCall } from '../tool-executor.js';
4
- import { normalizeServerUrl, readErrorResponse, } from './http.js';
4
+ import { createTraceContext, normalizeServerUrl, readErrorResponse, } from './http.js';
5
5
  export class TurnCancelledError extends Error {
6
6
  name = 'TurnCancelledError';
7
7
  constructor(message = 'Turn cancelled.') {
@@ -12,10 +12,12 @@ export class ChatTurnFailedError extends Error {
12
12
  name = 'ChatTurnFailedError';
13
13
  category;
14
14
  retryable;
15
- constructor(message, category = 'unknown_error', retryable = false) {
16
- super(message);
15
+ traceId;
16
+ constructor(message, category = 'unknown_error', retryable = false, traceId = '') {
17
+ super(traceId ? `${message}\nTrace ID: ${traceId}` : message);
17
18
  this.category = category;
18
19
  this.retryable = retryable;
20
+ this.traceId = traceId;
19
21
  }
20
22
  }
21
23
  export function isTurnCancelledError(error) {
@@ -139,17 +141,19 @@ function publicStatusMessage(data) {
139
141
  return `Running ${toolName} locally...`;
140
142
  return null;
141
143
  }
142
- async function postToolResult({ config, turnId, event, result, session, fetchImpl, }) {
144
+ async function postToolResult({ config, turnId, event, result, session, fetchImpl, traceId, }) {
143
145
  const payload = {
144
146
  toolCallId: event.call.id,
145
147
  result,
146
148
  toolState: toolStateFromSession(session),
147
149
  };
150
+ const trace = createTraceContext(traceId);
148
151
  const response = await fetchImpl(`${normalizeServerUrl(config.serverUrl)}/v1/chat/turn/${encodeURIComponent(turnId)}/tool-result`, {
149
152
  method: 'POST',
150
153
  headers: {
151
154
  authorization: `Bearer ${config.token}`,
152
155
  'content-type': 'application/json',
156
+ ...trace.headers,
153
157
  },
154
158
  body: JSON.stringify(payload),
155
159
  });
@@ -157,10 +161,10 @@ async function postToolResult({ config, turnId, event, result, session, fetchImp
157
161
  return;
158
162
  }
159
163
  if (!response.ok) {
160
- throw await readErrorResponse(response);
164
+ throw await readErrorResponse(response, trace.traceId);
161
165
  }
162
166
  }
163
- async function executeAndPostToolResult({ config, projectIndex, session, event, input, fetchImpl, signal, }) {
167
+ async function executeAndPostToolResult({ config, projectIndex, session, event, input, fetchImpl, signal, traceId, }) {
164
168
  const turnId = String(event?.turnId ?? '').trim();
165
169
  if (!turnId || !event?.call?.id || !event.call.name) {
166
170
  throw new Error('Server emitted an invalid tool-call event.');
@@ -189,13 +193,14 @@ async function executeAndPostToolResult({ config, projectIndex, session, event,
189
193
  result: rawResult,
190
194
  session,
191
195
  fetchImpl,
196
+ traceId,
192
197
  });
193
198
  }
194
199
  finally {
195
200
  session.turnState.id = previousTurnId;
196
201
  }
197
202
  }
198
- async function consumeTurnStream({ response, config, projectIndex, session, input, fetchImpl, signal, }) {
203
+ async function consumeTurnStream({ response, config, projectIndex, session, input, fetchImpl, signal, traceId, }) {
199
204
  if (!response.body) {
200
205
  throw new Error('Server returned an empty chat stream.');
201
206
  }
@@ -224,6 +229,7 @@ async function consumeTurnStream({ response, config, projectIndex, session, inpu
224
229
  input,
225
230
  fetchImpl,
226
231
  signal,
232
+ traceId,
227
233
  });
228
234
  return;
229
235
  }
@@ -243,7 +249,7 @@ async function consumeTurnStream({ response, config, projectIndex, session, inpu
243
249
  if (event.event === 'cancelled') {
244
250
  throw new TurnCancelledError(message);
245
251
  }
246
- throw new ChatTurnFailedError(message, typeof event.data?.category === 'string' ? event.data.category : 'unknown_error', Boolean(event.data?.retryable));
252
+ 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
253
  }
248
254
  }
249
255
  while (true) {
@@ -287,6 +293,7 @@ export async function sendServerUserMessage({ config, projectIndex, session, inp
287
293
  autoYes: session.autoYes,
288
294
  agentMode: session.agentMode,
289
295
  };
296
+ const trace = createTraceContext();
290
297
  const preTurnHistoryLength = session.history.length;
291
298
  const preserveOnAbort = () => preserveCancelledTurnInput(session, input);
292
299
  if (signal?.aborted) {
@@ -302,12 +309,13 @@ export async function sendServerUserMessage({ config, projectIndex, session, inp
302
309
  accept: 'text/event-stream',
303
310
  authorization: `Bearer ${config.token}`,
304
311
  'content-type': 'application/json',
312
+ ...trace.headers,
305
313
  },
306
314
  body: JSON.stringify(request),
307
315
  signal,
308
316
  });
309
317
  if (!response.ok) {
310
- throw await readErrorResponse(response);
318
+ throw await readErrorResponse(response, trace.traceId);
311
319
  }
312
320
  const result = await consumeTurnStream({
313
321
  response,
@@ -317,6 +325,7 @@ export async function sendServerUserMessage({ config, projectIndex, session, inp
317
325
  input,
318
326
  fetchImpl,
319
327
  signal,
328
+ traceId: trace.traceId,
320
329
  });
321
330
  applySessionSnapshot(session, result.snapshot, { preserveAgentMode: true });
322
331
  return {
@@ -1,4 +1,31 @@
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
+ }
2
29
  export function normalizeServerUrl(serverUrl) {
3
30
  const normalized = String(serverUrl || DEFAULT_SERVER_URL)
4
31
  .trim()
@@ -22,15 +49,17 @@ export async function readJsonResponse(response) {
22
49
  export function failureMessage(data, status) {
23
50
  return String(data?.error?.message ?? data?.message ?? `Request failed with ${status}`);
24
51
  }
25
- export async function readErrorResponse(response) {
52
+ export async function readErrorResponse(response, traceId = response.headers.get(TRACE_ID_HEADER) ?? '') {
26
53
  const data = await readJsonResponse(response);
27
- return new Error(failureMessage(data, response.status));
54
+ return new ServerApiError(failureMessage(data, response.status), response.status, traceId);
28
55
  }
29
56
  export async function authorizedJson({ config, path, method = 'GET', body = null, headers = {}, fetchImpl = globalThis.fetch, }) {
57
+ const trace = createTraceContext();
30
58
  const response = await fetchImpl(`${normalizeServerUrl(config.serverUrl)}${path}`, {
31
59
  method,
32
60
  headers: {
33
61
  authorization: `Bearer ${config.token}`,
62
+ ...trace.headers,
34
63
  ...headers,
35
64
  ...(body === null ? {} : { 'content-type': 'application/json' }),
36
65
  },
@@ -38,7 +67,7 @@ export async function authorizedJson({ config, path, method = 'GET', body = null
38
67
  });
39
68
  const data = await readJsonResponse(response);
40
69
  if (!response.ok) {
41
- throw new Error(failureMessage(data, response.status));
70
+ throw new ServerApiError(failureMessage(data, response.status), response.status, trace.traceId);
42
71
  }
43
72
  return data;
44
73
  }
@@ -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 { ServerApiError, createTraceContext, failureMessage, normalizeServerUrl, readJsonResponse, } from './http.js';
5
5
  function sanitizeModelInfo(raw) {
6
6
  if (!raw || typeof raw !== 'object') {
7
7
  return null;
@@ -54,14 +54,16 @@ export function writeCachedServerModels(cache, env = process.env) {
54
54
  });
55
55
  }
56
56
  export async function fetchServerModels({ config, fetchImpl = globalThis.fetch, }) {
57
+ const trace = createTraceContext();
57
58
  const response = await fetchImpl(`${normalizeServerUrl(config.serverUrl)}/v1/models`, {
58
59
  headers: {
59
60
  authorization: `Bearer ${config.token}`,
61
+ ...trace.headers,
60
62
  },
61
63
  });
62
64
  const data = (await readJsonResponse(response));
63
65
  if (!response.ok) {
64
- throw new Error(failureMessage(data, response.status));
66
+ throw new ServerApiError(failureMessage(data, response.status), response.status, trace.traceId);
65
67
  }
66
68
  const models = Array.isArray(data?.models)
67
69
  ? data.models.map(sanitizeModelInfo).filter(Boolean)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@thegitai/cli",
3
- "version": "1.0.0-beta.4",
3
+ "version": "1.0.0-beta.5",
4
4
  "description": "TheGitAI CLI client (source-visible, proprietary)",
5
5
  "license": "SEE LICENSE IN LICENSE",
6
6
  "homepage": "https://thegit.ai",
@@ -27,10 +27,10 @@
27
27
  "web-tree-sitter": "^0.26.6"
28
28
  },
29
29
  "optionalDependencies": {
30
- "@thegitai/tui-darwin-arm64": "1.0.0-beta.4",
31
- "@thegitai/tui-darwin-x64": "1.0.0-beta.4",
32
- "@thegitai/tui-linux-x64": "1.0.0-beta.4",
33
- "@thegitai/tui-win32-x64": "1.0.0-beta.4"
30
+ "@thegitai/tui-darwin-arm64": "1.0.0-beta.5",
31
+ "@thegitai/tui-darwin-x64": "1.0.0-beta.5",
32
+ "@thegitai/tui-linux-x64": "1.0.0-beta.5",
33
+ "@thegitai/tui-win32-x64": "1.0.0-beta.5"
34
34
  },
35
35
  "publishConfig": {
36
36
  "access": "public"