@thegitai/cli 1.0.0-beta.1 → 1.0.0-beta.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (45) hide show
  1. package/README.md +4 -3
  2. package/dist/bin/ai.js +18 -61
  3. package/dist/parsers/NOTICE +18 -0
  4. package/dist/src/api/auth.js +4 -2
  5. package/dist/src/api/browser-login.js +10 -28
  6. package/dist/src/api/chat.js +20 -9
  7. package/dist/src/api/http.js +32 -3
  8. package/dist/src/api/models.js +4 -2
  9. package/dist/src/artifact-policy.js +9 -0
  10. package/dist/src/cli-args.js +65 -0
  11. package/dist/src/client-environment.js +127 -0
  12. package/dist/src/colors.js +59 -0
  13. package/dist/src/core/clipboard.js +56 -0
  14. package/dist/src/edit-journal.js +39 -6
  15. package/dist/src/executor.js +28 -5
  16. package/dist/src/help-text.js +15 -1
  17. package/dist/src/markdown-renderer.js +1 -1
  18. package/dist/src/patcher.js +18 -1
  19. package/dist/src/scanner.js +67 -17
  20. package/dist/src/session-safety.js +64 -12
  21. package/dist/src/tool-executor.js +8 -0
  22. package/dist/src/tools/delete-file.js +1 -1
  23. package/dist/src/tools/index.js +2 -0
  24. package/dist/src/tools/patch-file.js +14 -1
  25. package/dist/src/tools/path-suggest.js +66 -0
  26. package/dist/src/tools/read-document.js +14 -3
  27. package/dist/src/tools/read-file.js +9 -0
  28. package/dist/src/tools/replace-document-text.js +242 -0
  29. package/dist/src/tools/restore-checkpoint.js +1 -1
  30. package/dist/src/tools/run-command.js +1 -1
  31. package/dist/src/tools/run-node-script.js +1 -1
  32. package/dist/src/tools/str-replace.js +14 -1
  33. package/dist/src/tools/undo-edit.js +7 -5
  34. package/dist/src/tools/write-file.js +14 -1
  35. package/dist/src/tree-sitter-runtime.js +14 -1
  36. package/dist/src/ui/repl.js +13 -1
  37. package/dist/src/ui/tui/bridge.js +2 -2
  38. package/dist/src/ui/tui/build-frame.js +18 -2
  39. package/dist/src/ui/tui/shell-input.js +9 -1
  40. package/dist/src/version.js +35 -0
  41. package/dist/vendor/web-tree-sitter/LICENSE +21 -0
  42. package/dist/vendor/web-tree-sitter/NOTICE +13 -0
  43. package/dist/vendor/web-tree-sitter/web-tree-sitter.cjs +4063 -0
  44. package/dist/vendor/web-tree-sitter/web-tree-sitter.wasm +0 -0
  45. package/package.json +15 -16
package/README.md CHANGED
@@ -9,7 +9,7 @@ reads code, edits files, and runs commands with your approval.
9
9
  npm i -g @thegitai/cli
10
10
  ```
11
11
 
12
- Requires Node.js 24.
12
+ Requires Node.js 24 or newer.
13
13
 
14
14
  ## Usage
15
15
 
@@ -20,11 +20,12 @@ ai login sign in via your browser (--no-browser for SSH/headless)
20
20
  ai whoami show the signed-in account
21
21
  ai --usage show account usage and reset times
22
22
  ai logout sign out
23
+ ai --version print the version and exit
23
24
  ```
24
25
 
25
26
  Run `ai --help` for sessions, modes, keys, and chat commands.
26
27
 
27
28
  ## License
28
29
 
29
- Proprietary — see [LICENSE](./LICENSE). Source is visible for inspection;
30
- copying and redistribution are not permitted.
30
+ Proprietary — see the LICENSE file included in this package. Source is
31
+ visible for inspection; copying and redistribution are not permitted.
package/dist/bin/ai.js CHANGED
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import chalk from 'chalk';
2
+ import chalk from '../src/colors.js';
3
3
  import { stdin as input, stdout as output } from 'node:process';
4
4
  import readline from 'node:readline/promises';
5
5
  import { ServerApi } from '../src/api/index.js';
@@ -13,65 +13,13 @@ import { runClientInteractive, shouldUseClientRatatuiShell, } from '../src/ui/re
13
13
  import { appendPromptToFile } from '../src/ui/prompt-history-store.js';
14
14
  import { formatSessionExitNotice } from '../src/session-exit.js';
15
15
  import { formatUsageText } from '../src/usage.js';
16
+ import { formatVersionLine } from '../src/version.js';
17
+ import { parseArgs } from '../src/cli-args.js';
16
18
  const DEFAULT_SERVER_URL = 'https://thegit.ai';
17
- const AUTH_COMMANDS = new Set(['login', 'whoami', 'logout']);
18
19
  const { auth, chat, models, sessions } = ServerApi;
19
20
  function printUsage() {
20
21
  console.log(formatCliHelpText({ color: process.stdout.isTTY === true }));
21
22
  }
22
- export function parseArgs(argv) {
23
- const args = argv.slice(2);
24
- const firstArg = args[0];
25
- const command = firstArg && AUTH_COMMANDS.has(firstArg) ? firstArg : null;
26
- const commandArgs = command ? args.slice(1) : [];
27
- let autoYes = false;
28
- let help = false;
29
- let usage = false;
30
- let session = null;
31
- let listSessions = false;
32
- const promptParts = [];
33
- for (let i = 0; i < args.length; i++) {
34
- const arg = args[i];
35
- if (arg === '--yes' || arg === '-y') {
36
- autoYes = true;
37
- continue;
38
- }
39
- if ((arg === '--session' || arg === '--resume') && i + 1 < args.length) {
40
- session = args[i + 1] ?? null;
41
- i += 1;
42
- continue;
43
- }
44
- if (arg === '--list-sessions') {
45
- listSessions = true;
46
- continue;
47
- }
48
- if (arg === '--help' || arg === '-h') {
49
- help = true;
50
- continue;
51
- }
52
- if (arg === '--usage') {
53
- usage = true;
54
- continue;
55
- }
56
- promptParts.push(arg);
57
- }
58
- return {
59
- command,
60
- commandArgs,
61
- autoYes,
62
- help,
63
- usage,
64
- session,
65
- listSessions,
66
- prompt: promptParts.join(' ').trim(),
67
- };
68
- }
69
- function commandFlagValue(args, name) {
70
- const index = args.indexOf(name);
71
- if (index === -1)
72
- return null;
73
- return args[index + 1] ?? null;
74
- }
75
23
  async function promptText(question, fallback = null) {
76
24
  const rl = readline.createInterface({ input, output });
77
25
  try {
@@ -88,17 +36,16 @@ function appendPromptHistory(prompt, env = process.env) {
88
36
  }
89
37
  async function runAuthCommand(command, args) {
90
38
  if (command === 'login') {
91
- const serverUrl = commandFlagValue(args, '--server') ??
92
- process.env.THEGITAI_SERVER_URL ??
93
- DEFAULT_SERVER_URL;
94
- const websiteUrl = commandFlagValue(args, '--website') ?? undefined;
39
+ // The public CLI always authenticates against the official TheGitAI host.
40
+ // There is intentionally no server/website override here — internal dev
41
+ // uses private tooling, not a customer-visible runtime override path.
42
+ const serverUrl = DEFAULT_SERVER_URL;
95
43
  const noBrowser = args.includes('--no-browser');
96
44
  console.log(chalk.dim(noBrowser
97
45
  ? 'Sign in on the website, then paste the authorization code here.'
98
46
  : 'Opening your browser to sign in…'));
99
47
  const result = await loginViaBrowser({
100
48
  serverUrl,
101
- websiteUrl,
102
49
  noBrowser,
103
50
  onUrl: (url) => {
104
51
  console.log(chalk.dim(noBrowser ? 'Open this URL to sign in:' : 'If your browser did not open, visit:'));
@@ -340,11 +287,21 @@ async function mainInteractive({ authConfig, projectIndex, serverModels, serverS
340
287
  }
341
288
  }
342
289
  export async function main() {
343
- const { autoYes, help, usage, command, commandArgs, session: sessionIdentifier, listSessions, prompt, } = parseArgs(process.argv);
290
+ const { autoYes, help, version, usage, command, commandArgs, session: sessionIdentifier, listSessions, unknownOption, prompt, } = parseArgs(process.argv);
291
+ if (version) {
292
+ console.log(formatVersionLine());
293
+ return;
294
+ }
344
295
  if (help) {
345
296
  printUsage();
346
297
  return;
347
298
  }
299
+ if (unknownOption) {
300
+ console.error(`Unknown option: ${unknownOption}`);
301
+ console.error("Run 'ai --help' to see available commands and options.");
302
+ process.exitCode = 2;
303
+ return;
304
+ }
348
305
  if (command) {
349
306
  await runAuthCommand(command, commandArgs);
350
307
  return;
@@ -0,0 +1,18 @@
1
+ Tree-sitter grammar parsers
2
+ ===========================
3
+
4
+ The `tree-sitter-*.wasm` files in this directory are precompiled tree-sitter
5
+ grammar parsers, vendored so the published `@thegitai/cli` package ships local
6
+ code intelligence without a runtime dependency. They are used unmodified, only
7
+ as parsing inputs to the vendored web-tree-sitter runtime
8
+ (see ../vendor/web-tree-sitter/NOTICE).
9
+
10
+ Each grammar is the work of its respective tree-sitter grammar project and is
11
+ distributed under that project's license (the tree-sitter grammars are
12
+ MIT-licensed). Grammars included:
13
+
14
+ c, c-sharp, cpp, css, go, html, java, javascript, objc, php, python, ruby,
15
+ rust, tsx, typescript
16
+
17
+ Upstream organization: https://github.com/tree-sitter
18
+ Individual grammars: https://github.com/tree-sitter/tree-sitter-<language>
@@ -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,9 +2,8 @@ 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
- const DEFAULT_DEV_WEBSITE_URL = 'http://localhost:3002';
8
7
  const DEFAULT_SERVER_URL = 'https://thegit.ai';
9
8
  const DEFAULT_TIMEOUT_MS = 5 * 60 * 1000;
10
9
  function shutDownServer(server) {
@@ -13,29 +12,11 @@ function shutDownServer(server) {
13
12
  server.closeAllConnections?.();
14
13
  server.close();
15
14
  }
16
- function isLocalhostUrl(url) {
17
- if (!url)
18
- return false;
19
- try {
20
- const host = new URL(url).hostname;
21
- return host === 'localhost' || host === '127.0.0.1' || host === '::1';
22
- }
23
- catch {
24
- return false;
25
- }
26
- }
27
- export function resolveWebsiteUrl(websiteUrl, env = process.env, serverUrl) {
28
- const explicit = String(websiteUrl ?? '').trim() ||
29
- String(env.THEGITAI_WEBSITE_URL ?? '').trim();
30
- // With no explicit override, point at production — unless we're clearly in
31
- // local dev (talking to a localhost server), in which case default to the
32
- // local website so `ai login` works without any flags or env vars.
33
- const value = explicit || (isLocalhostUrl(serverUrl) ? DEFAULT_DEV_WEBSITE_URL : DEFAULT_WEBSITE_URL);
34
- const normalized = value.replace(/\/+$/, '');
35
- if (!/^https?:\/\//i.test(normalized)) {
36
- throw new Error('Website URL must start with http:// or https://.');
37
- }
38
- return normalized;
15
+ export function resolveWebsiteUrl() {
16
+ // The public CLI always signs in through the official TheGitAI website. There
17
+ // is no override path here so the published package cannot be pointed at a
18
+ // clone host.
19
+ return DEFAULT_WEBSITE_URL.replace(/\/+$/, '');
39
20
  }
40
21
  function defaultDeviceName() {
41
22
  try {
@@ -73,14 +54,15 @@ const RESULT_PAGE = (heading, detail) => `<!doctype html><html><head><meta chars
73
54
  `p{color:#9aa0a6}</style></head><body><div class="card"><h1>${heading}</h1>` +
74
55
  `<p>${detail}</p></div></body></html>`;
75
56
  async function exchangeCodeForToken({ serverUrl, code, codeVerifier, fetchImpl, }) {
57
+ const trace = createTraceContext();
76
58
  const response = await fetchImpl(`${serverUrl}/v1/cli/auth/token`, {
77
59
  method: 'POST',
78
- headers: { 'content-type': 'application/json' },
60
+ headers: { 'content-type': 'application/json', ...trace.headers },
79
61
  body: JSON.stringify({ code, code_verifier: codeVerifier }),
80
62
  });
81
63
  const data = (await readJsonResponse(response));
82
64
  if (!response.ok) {
83
- throw new Error(failureMessage(data, response.status));
65
+ throw new ServerApiError(failureMessage(data, response.status), response.status, trace.traceId);
84
66
  }
85
67
  const token = String(data?.token ?? '').trim();
86
68
  const customer = data?.customer;
@@ -103,7 +85,7 @@ async function exchangeCodeForToken({ serverUrl, code, codeVerifier, fetchImpl,
103
85
  */
104
86
  export async function loginViaBrowser(options) {
105
87
  const serverUrl = normalizeServerUrl(options.serverUrl ?? DEFAULT_SERVER_URL);
106
- const websiteUrl = resolveWebsiteUrl(options.websiteUrl, process.env, serverUrl);
88
+ const websiteUrl = resolveWebsiteUrl();
107
89
  const fetchImpl = options.fetchImpl ?? globalThis.fetch;
108
90
  const openBrowser = options.openBrowser ?? openUrl;
109
91
  const onUrl = options.onUrl ?? (() => { });
@@ -1,7 +1,8 @@
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
+ import { collectClientEnvironment } from '../client-environment.js';
5
6
  export class TurnCancelledError extends Error {
6
7
  name = 'TurnCancelledError';
7
8
  constructor(message = 'Turn cancelled.') {
@@ -12,10 +13,12 @@ export class ChatTurnFailedError extends Error {
12
13
  name = 'ChatTurnFailedError';
13
14
  category;
14
15
  retryable;
15
- constructor(message, category = 'unknown_error', retryable = false) {
16
- super(message);
16
+ traceId;
17
+ constructor(message, category = 'unknown_error', retryable = false, traceId = '') {
18
+ super(traceId ? `${message}\nTrace ID: ${traceId}` : message);
17
19
  this.category = category;
18
20
  this.retryable = retryable;
21
+ this.traceId = traceId;
19
22
  }
20
23
  }
21
24
  export function isTurnCancelledError(error) {
@@ -139,17 +142,19 @@ function publicStatusMessage(data) {
139
142
  return `Running ${toolName} locally...`;
140
143
  return null;
141
144
  }
142
- async function postToolResult({ config, turnId, event, result, session, fetchImpl, }) {
145
+ async function postToolResult({ config, turnId, event, result, session, fetchImpl, traceId, }) {
143
146
  const payload = {
144
147
  toolCallId: event.call.id,
145
148
  result,
146
149
  toolState: toolStateFromSession(session),
147
150
  };
151
+ const trace = createTraceContext(traceId);
148
152
  const response = await fetchImpl(`${normalizeServerUrl(config.serverUrl)}/v1/chat/turn/${encodeURIComponent(turnId)}/tool-result`, {
149
153
  method: 'POST',
150
154
  headers: {
151
155
  authorization: `Bearer ${config.token}`,
152
156
  'content-type': 'application/json',
157
+ ...trace.headers,
153
158
  },
154
159
  body: JSON.stringify(payload),
155
160
  });
@@ -157,10 +162,10 @@ async function postToolResult({ config, turnId, event, result, session, fetchImp
157
162
  return;
158
163
  }
159
164
  if (!response.ok) {
160
- throw await readErrorResponse(response);
165
+ throw await readErrorResponse(response, trace.traceId);
161
166
  }
162
167
  }
163
- async function executeAndPostToolResult({ config, projectIndex, session, event, input, fetchImpl, signal, }) {
168
+ async function executeAndPostToolResult({ config, projectIndex, session, event, input, fetchImpl, signal, traceId, }) {
164
169
  const turnId = String(event?.turnId ?? '').trim();
165
170
  if (!turnId || !event?.call?.id || !event.call.name) {
166
171
  throw new Error('Server emitted an invalid tool-call event.');
@@ -189,13 +194,14 @@ async function executeAndPostToolResult({ config, projectIndex, session, event,
189
194
  result: rawResult,
190
195
  session,
191
196
  fetchImpl,
197
+ traceId,
192
198
  });
193
199
  }
194
200
  finally {
195
201
  session.turnState.id = previousTurnId;
196
202
  }
197
203
  }
198
- async function consumeTurnStream({ response, config, projectIndex, session, input, fetchImpl, signal, }) {
204
+ async function consumeTurnStream({ response, config, projectIndex, session, input, fetchImpl, signal, traceId, }) {
199
205
  if (!response.body) {
200
206
  throw new Error('Server returned an empty chat stream.');
201
207
  }
@@ -224,6 +230,7 @@ async function consumeTurnStream({ response, config, projectIndex, session, inpu
224
230
  input,
225
231
  fetchImpl,
226
232
  signal,
233
+ traceId,
227
234
  });
228
235
  return;
229
236
  }
@@ -243,7 +250,7 @@ async function consumeTurnStream({ response, config, projectIndex, session, inpu
243
250
  if (event.event === 'cancelled') {
244
251
  throw new TurnCancelledError(message);
245
252
  }
246
- throw new ChatTurnFailedError(message, typeof event.data?.category === 'string' ? event.data.category : 'unknown_error', Boolean(event.data?.retryable));
253
+ 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
254
  }
248
255
  }
249
256
  while (true) {
@@ -282,11 +289,13 @@ export async function sendServerUserMessage({ config, projectIndex, session, inp
282
289
  modelId: session.modelId,
283
290
  session: snapshotForServer(session),
284
291
  input,
292
+ clientEnvironment: collectClientEnvironment({ env: session.env }),
285
293
  imageAttachments,
286
294
  maxToolSteps: session.maxToolSteps,
287
295
  autoYes: session.autoYes,
288
296
  agentMode: session.agentMode,
289
297
  };
298
+ const trace = createTraceContext();
290
299
  const preTurnHistoryLength = session.history.length;
291
300
  const preserveOnAbort = () => preserveCancelledTurnInput(session, input);
292
301
  if (signal?.aborted) {
@@ -302,12 +311,13 @@ export async function sendServerUserMessage({ config, projectIndex, session, inp
302
311
  accept: 'text/event-stream',
303
312
  authorization: `Bearer ${config.token}`,
304
313
  'content-type': 'application/json',
314
+ ...trace.headers,
305
315
  },
306
316
  body: JSON.stringify(request),
307
317
  signal,
308
318
  });
309
319
  if (!response.ok) {
310
- throw await readErrorResponse(response);
320
+ throw await readErrorResponse(response, trace.traceId);
311
321
  }
312
322
  const result = await consumeTurnStream({
313
323
  response,
@@ -317,6 +327,7 @@ export async function sendServerUserMessage({ config, projectIndex, session, inp
317
327
  input,
318
328
  fetchImpl,
319
329
  signal,
330
+ traceId: trace.traceId,
320
331
  });
321
332
  applySessionSnapshot(session, result.snapshot, { preserveAgentMode: true });
322
333
  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)
@@ -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',
@@ -0,0 +1,65 @@
1
+ export const AUTH_COMMANDS = new Set(['login', 'whoami', 'logout']);
2
+ export function parseArgs(argv) {
3
+ const args = argv.slice(2);
4
+ const firstArg = args[0];
5
+ const command = firstArg && AUTH_COMMANDS.has(firstArg) ? firstArg : null;
6
+ const commandArgs = command ? args.slice(1) : [];
7
+ let autoYes = false;
8
+ let help = false;
9
+ let version = false;
10
+ let usage = false;
11
+ let session = null;
12
+ let listSessions = false;
13
+ let unknownOption = null;
14
+ const promptParts = [];
15
+ for (let i = 0; i < args.length; i++) {
16
+ const arg = args[i];
17
+ if (arg === '--yes' || arg === '-y') {
18
+ autoYes = true;
19
+ continue;
20
+ }
21
+ if ((arg === '--session' || arg === '--resume') && i + 1 < args.length) {
22
+ session = args[i + 1] ?? null;
23
+ i += 1;
24
+ continue;
25
+ }
26
+ if (arg === '--list-sessions') {
27
+ listSessions = true;
28
+ continue;
29
+ }
30
+ if (arg === '--help' || arg === '-h') {
31
+ help = true;
32
+ continue;
33
+ }
34
+ if (arg === '--version' || arg === '-v') {
35
+ version = true;
36
+ continue;
37
+ }
38
+ if (arg === '--usage') {
39
+ usage = true;
40
+ continue;
41
+ }
42
+ // An unrecognized dashed token is a mistyped flag, not prompt text. Without
43
+ // an auth subcommand (whose flags are parsed separately) it would otherwise
44
+ // be swept into the prompt and silently start a billable session. Flag the
45
+ // first one so the caller can fail fast instead. Quoted prompts are a single
46
+ // argv entry with spaces, so they never look like a bare option here.
47
+ if (command === null && unknownOption === null && /^-/.test(arg)) {
48
+ unknownOption = arg;
49
+ continue;
50
+ }
51
+ promptParts.push(arg);
52
+ }
53
+ return {
54
+ command,
55
+ commandArgs,
56
+ autoYes,
57
+ help,
58
+ version,
59
+ usage,
60
+ session,
61
+ listSessions,
62
+ unknownOption,
63
+ prompt: promptParts.join(' ').trim(),
64
+ };
65
+ }