@thegitai/cli 1.0.0-preview.1 → 1.0.0-preview.11

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 (36) hide show
  1. package/README.md +16 -4
  2. package/dist/bin/ai.js +57 -287
  3. package/dist/src/api/auth.js +2 -2
  4. package/dist/src/api/browser-login.js +72 -3
  5. package/dist/src/api/chat.js +147 -30
  6. package/dist/src/api/http.js +16 -3
  7. package/dist/src/api/models.js +9 -4
  8. package/dist/src/help-text.js +19 -7
  9. package/dist/src/patcher.js +96 -9
  10. package/dist/src/project-index.js +13 -1
  11. package/dist/src/project-orientation.js +99 -0
  12. package/dist/src/scratch-dir.js +51 -33
  13. package/dist/src/session-store.js +52 -20
  14. package/dist/src/session.js +8 -0
  15. package/dist/src/tool-executor.js +38 -6
  16. package/dist/src/tools/delete-file.js +22 -4
  17. package/dist/src/tools/patch-file.js +30 -5
  18. package/dist/src/tools/read-file.js +3 -1
  19. package/dist/src/tools/replace-document-text.js +7 -1
  20. package/dist/src/tools/run-command.js +37 -19
  21. package/dist/src/tools/run-node-script.js +24 -4
  22. package/dist/src/tools/str-replace.js +30 -5
  23. package/dist/src/tools/write-file.js +25 -5
  24. package/dist/src/turn-failure-marker.js +11 -0
  25. package/dist/src/ui/prompt-history-store.js +1 -1
  26. package/dist/src/ui/repl.js +197 -51
  27. package/dist/src/ui/tui/bridge.js +3 -0
  28. package/dist/src/ui/tui/build-frame.js +179 -82
  29. package/dist/src/ui/tui/markdown-render.js +72 -73
  30. package/dist/src/ui/tui/shell-input.js +42 -13
  31. package/dist/src/ui/tui/terminal-title.js +3 -0
  32. package/dist/src/ui/tui/terminal-writes.js +48 -0
  33. package/dist/src/ui/tui/text.js +158 -4
  34. package/dist/src/utils.js +9 -0
  35. package/package.json +18 -6
  36. package/dist/src/markdown-renderer.js +0 -112
package/README.md CHANGED
@@ -1,8 +1,10 @@
1
- # TheGitAI
1
+ # TheGitAI — AI coding agent for your terminal
2
2
 
3
- Interactive terminal coding agent. Talk to your repo in plain English — it reads
4
- your code, makes edits, runs the commands a task needs, and keeps long-running
5
- processes going while you work, all with your approval.
3
+ TheGitAI is an AI coding agent for your terminal. It indexes your repository,
4
+ writes and edits files, runs commands, and builds features with you.
5
+
6
+ Talk to your repo in plain English. TheGitAI can keep long-running processes
7
+ going while you work, all with your approval.
6
8
 
7
9
  ## Install
8
10
 
@@ -26,6 +28,16 @@ ai --version print the version and exit
26
28
 
27
29
  Run `ai --help` for sessions, modes, keys, and chat commands.
28
30
 
31
+ Coding sessions require an interactive terminal on stdin and stdout. Piped
32
+ one-shot prompts are not supported. Saved history is local to the repo and can
33
+ always be viewed or resumed from that computer. Continuing it through the
34
+ service requires the TheGitAI account used for that session. Local sessions can
35
+ also be listed while signed out or offline.
36
+
37
+ CLI login tokens use a rolling 24-hour inactivity timeout. If one expires during
38
+ any server request, the CLI saves the local session, removes the expired
39
+ credential, and asks you to run `ai login` before resuming.
40
+
29
41
  ## Visible to-do list
30
42
 
31
43
  For larger multi-step tasks, the agent keeps a compact to-do list on screen so
package/dist/bin/ai.js CHANGED
@@ -4,20 +4,17 @@ 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';
6
6
  import { loginViaBrowser } from '../src/api/browser-login.js';
7
- import { isTransientNetworkError } from '../src/api/http.js';
8
- import { formatCliHelpText, formatInteractiveHelpText, } from '../src/help-text.js';
9
- import { renderMarkdownForTerminal } from '../src/markdown-renderer.js';
7
+ import { authenticationErrorMessage, isAuthenticationError, isTransientNetworkError, } from '../src/api/http.js';
8
+ import { formatCliHelpText } from '../src/help-text.js';
10
9
  import { createIndex } from '../src/project-index.js';
11
- import { createSession, clearConversation } from '../src/session.js';
12
- import { applySessionSnapshot, listSessionMetadata, loadSessionSnapshot, saveSessionState, } from '../src/session-store.js';
13
- import { runClientInteractive, shouldUseClientRatatuiShell, } from '../src/ui/repl.js';
10
+ import { createSession } from '../src/session.js';
11
+ import { applySessionSnapshot, listSessionMetadata, loadSessionSnapshot, saveSessionState, sessionHasUserMessage, } from '../src/session-store.js';
12
+ import { runClientInteractive } from '../src/ui/repl.js';
14
13
  import { appendPromptToFile } from '../src/ui/prompt-history-store.js';
15
14
  import { formatSessionExitNotice } from '../src/session-exit.js';
16
15
  import { formatUsageText } from '../src/usage.js';
17
16
  import { formatVersionLine } from '../src/version.js';
18
17
  import { parseArgs } from '../src/cli-args.js';
19
- import { getJobBufferedOutput, killBackgroundJob, killAllBackgroundJobs, listBackgroundJobs, setBackgroundJobSession, } from '../src/background-jobs.js';
20
- import { collectBackgroundJobUiKillMutations, collectBackgroundJobUiOutputMutations, } from '../src/tool-executor.js';
21
18
  const DEFAULT_SERVER_URL = 'https://thegit.ai';
22
19
  const { auth, chat, models, sessions } = ServerApi;
23
20
  function printUsage() {
@@ -111,54 +108,16 @@ function printSessionList(rootDir, sessions, serverModels) {
111
108
  }
112
109
  }
113
110
  }
114
- function printStartupBanner(rootDir, modelLabel, autoYes) {
115
- console.log(chalk.dim(`Project: ${rootDir}`));
116
- console.log(chalk.dim(`Model: ${modelLabel}`));
117
- if (autoYes) {
118
- console.log(chalk.dim('Auto-confirm: enabled'));
119
- }
120
- }
121
- function printSessionStartup(session) {
122
- console.log(chalk.dim(`Session: ${session.sessionName ? `${session.sessionName} ` : ''}${session.sessionId}`));
123
- }
124
111
  function printSessionExit(session) {
125
112
  console.log(chalk.dim(`\n${formatSessionExitNotice(session.sessionId)}\n`));
126
113
  }
127
114
  function modelLabel(serverModels, modelId) {
128
- return (serverModels.models.find((model) => model.id === modelId)?.label ??
129
- 'Unknown model');
130
- }
131
- function formatJobElapsedMs(ms) {
132
- const totalSeconds = Math.max(0, Math.floor(ms / 1000));
133
- if (totalSeconds < 60)
134
- return `${totalSeconds}s`;
135
- const minutes = Math.floor(totalSeconds / 60);
136
- if (minutes < 60)
137
- return `${minutes}m${String(totalSeconds % 60).padStart(2, '0')}s`;
138
- return `${Math.floor(minutes / 60)}h${String(minutes % 60).padStart(2, '0')}m`;
139
- }
140
- function makeConfirmCommand(session) {
141
- return async (command) => {
142
- console.log(chalk.bold(`\nCommand approval needed:\n${command}\n`));
143
- const answer = (await promptText('Approve? [y]es/[a]ll/[n]o', 'n')).toLowerCase();
144
- if (answer === 'a' || answer === 'all') {
145
- session.autoYes = true;
146
- return true;
147
- }
148
- return answer === 'y' || answer === 'yes';
149
- };
150
- }
151
- function makeConfirmPatch(session) {
152
- return async (filePath) => {
153
- const answer = (await promptText(`Apply patch to ${filePath}? [y]es/[a]ll/[n]o`, 'n')).toLowerCase();
154
- if (answer === 'a' || answer === 'all') {
155
- session.autoYes = true;
156
- return true;
157
- }
158
- return answer === 'y' || answer === 'yes';
159
- };
115
+ return (serverModels?.models.find((model) => model.id === modelId)?.label ??
116
+ `Model ${modelId}`);
160
117
  }
161
118
  async function saveSessionBoth({ session, serverSessionClient, }) {
119
+ if (!sessionHasUserMessage(session))
120
+ return;
162
121
  saveSessionState(session);
163
122
  try {
164
123
  await serverSessionClient.save(session);
@@ -167,196 +126,18 @@ async function saveSessionBoth({ session, serverSessionClient, }) {
167
126
  console.error(chalk.yellow(`Warning: session save failed: ${error.message}`));
168
127
  }
169
128
  }
170
- async function promptForModelSelection(currentModelId, authConfig) {
171
- const serverModels = await models.fetchServerModels({ config: authConfig });
172
- console.log(chalk.bold('\nAvailable models'));
173
- for (const model of serverModels.models) {
174
- const marker = model.id === currentModelId ? '*' : ' ';
175
- console.log(`${marker} ${model.id} ${model.label}`);
176
- }
177
- console.log();
178
- const selected = (await promptText('Model id', '')).trim() || null;
179
- if (!selected) {
180
- return { selected: null, serverModels };
181
- }
182
- return {
183
- selected: models.validateServerModel(selected, serverModels),
184
- serverModels,
185
- };
186
- }
187
- async function promptForResumeSession(rootDir, serverModels) {
188
- const sessions = listSessionMetadata(rootDir);
189
- if (!sessions.length) {
190
- console.log(chalk.dim('No saved sessions for this repo.\n'));
191
- return null;
192
- }
193
- printSessionList(rootDir, sessions, serverModels);
194
- console.log();
195
- const identifier = (await promptText('Session id or name', '')).trim();
196
- if (!identifier) {
197
- return null;
198
- }
199
- return loadSessionSnapshot(rootDir, identifier);
200
- }
201
- async function runTurn({ authConfig, projectIndex, serverSessionClient, session, inputText, }) {
202
- const prompt = String(inputText ?? '').trim();
203
- if (!prompt)
204
- return;
205
- appendPromptHistory(prompt, session.env);
206
- const result = await chat.sendServerUserMessage({
207
- config: authConfig,
208
- projectIndex,
209
- session,
210
- input: prompt,
211
- });
212
- if (result.text) {
213
- console.log(`\n${chalk.green('TheGitAI>')}`);
214
- console.log(renderMarkdownForTerminal(result.text));
215
- console.log();
216
- }
217
- await saveSessionBoth({ session, serverSessionClient });
218
- }
219
- async function mainInteractive({ authConfig, projectIndex, serverModels, serverSessionClient, session, usageText, initialPrompt, }) {
220
- if (initialPrompt) {
221
- console.log(chalk.dim(`Prompt: "${initialPrompt}"`));
222
- await runTurn({
223
- authConfig,
224
- projectIndex,
225
- serverSessionClient,
226
- session,
227
- inputText: initialPrompt,
228
- });
229
- }
230
- while (true) {
231
- const inputText = await promptText('you');
232
- const trimmed = inputText.trim();
233
- if (!trimmed)
234
- continue;
235
- if (trimmed === '/exit')
236
- return;
237
- if (trimmed === '/help') {
238
- console.log(renderMarkdownForTerminal(formatInteractiveHelpText()));
239
- continue;
240
- }
241
- if (trimmed === '/usage') {
242
- console.log(await usageText());
243
- continue;
244
- }
245
- if (trimmed === '/clear') {
246
- clearConversation(session);
247
- await saveSessionBoth({ session, serverSessionClient });
248
- console.log(chalk.dim('Conversation cleared.\n'));
249
- continue;
250
- }
251
- if (trimmed === '/jobs' || trimmed.startsWith('/jobs ')) {
252
- const jobsArgs = trimmed.slice('/jobs'.length).trim();
253
- const killMatch = jobsArgs.match(/^kill\s+(\S+)$/);
254
- if (killMatch) {
255
- const jobId = killMatch[1];
256
- const killed = await killBackgroundJob(jobId);
257
- await collectBackgroundJobUiKillMutations({
258
- session,
259
- projectIndex,
260
- jobId,
261
- result: killed,
262
- });
263
- if (!killed.ok) {
264
- console.log(chalk.red(killed.error ?? 'Background job kill failed.'));
265
- }
266
- else if (killed.alreadyFinished) {
267
- console.log(chalk.dim(`${jobId} had already finished.`));
268
- }
269
- continue;
270
- }
271
- const outputMatch = jobsArgs.match(/^output\s+(\S+)$/);
272
- if (outputMatch) {
273
- const jobId = outputMatch[1];
274
- await collectBackgroundJobUiOutputMutations({
275
- session,
276
- projectIndex,
277
- jobId,
278
- });
279
- const job = listBackgroundJobs().find((candidate) => candidate.id === jobId);
280
- const buffered = getJobBufferedOutput(jobId);
281
- if (!job || !buffered) {
282
- console.log(chalk.red(`Unknown background job id: ${jobId}`));
283
- continue;
284
- }
285
- if (buffered.droppedChars > 0) {
286
- console.log(chalk.dim(`... (${buffered.droppedChars} chars of older output dropped) ...`));
287
- }
288
- console.log(buffered.output || chalk.dim('(no output captured)'));
289
- continue;
290
- }
291
- if (jobsArgs) {
292
- console.log(chalk.dim('Usage: /jobs — list · /jobs output <id> — full output · /jobs kill <id> — kill'));
293
- continue;
294
- }
295
- const jobsList = listBackgroundJobs();
296
- if (!jobsList.length) {
297
- console.log(chalk.dim('No background jobs in this session.'));
298
- continue;
299
- }
300
- for (const job of jobsList) {
301
- const elapsed = formatJobElapsedMs((job.endedAt ?? Date.now()) - job.startedAt);
302
- const stateText = job.status === 'running'
303
- ? `running · ${elapsed}`
304
- : job.status === 'killed'
305
- ? `killed · ran ${elapsed}`
306
- : job.status === 'error'
307
- ? 'failed to start'
308
- : `exited (code ${job.exitCode ?? 1}) · ran ${elapsed}`;
309
- console.log(`${job.id} · ${stateText}\n $ ${job.command}`);
310
- }
311
- continue;
312
- }
313
- if (trimmed === '/resume') {
314
- const snapshot = await promptForResumeSession(session.rootDir, serverModels);
315
- if (!snapshot) {
316
- console.log(chalk.dim('Resume cancelled.\n'));
317
- continue;
318
- }
319
- applySessionSnapshot(session, snapshot);
320
- setBackgroundJobSession(session.sessionId);
321
- await saveSessionBoth({ session, serverSessionClient });
322
- console.log(chalk.dim(`Resumed session${session.sessionName ? ` "${session.sessionName}"` : ''} (${session.sessionId})\n`));
323
- continue;
324
- }
325
- if (trimmed === '/model' || trimmed.startsWith('/model ')) {
326
- const inline = trimmed.slice('/model'.length).trim();
327
- let serverModels;
328
- let selected = null;
329
- if (inline) {
330
- serverModels = await models.fetchServerModels({ config: authConfig });
331
- selected = models.validateServerModel(inline, serverModels);
332
- }
333
- else {
334
- const response = await promptForModelSelection(session.modelId, authConfig);
335
- serverModels = response.serverModels;
336
- selected = response.selected;
337
- }
338
- if (!selected) {
339
- console.log(chalk.dim('Model selection cancelled.\n'));
340
- continue;
341
- }
342
- session.modelId = selected;
343
- models.updateSelectedModelCache({
344
- config: authConfig,
345
- selectedModelId: selected,
346
- serverModels,
347
- });
348
- await saveSessionBoth({ session, serverSessionClient });
349
- console.log(chalk.dim(`Switched to ${modelLabel(serverModels, selected)}.\n`));
350
- continue;
351
- }
352
- await runTurn({
353
- authConfig,
354
- projectIndex,
355
- serverSessionClient,
356
- session,
357
- inputText: trimmed,
358
- });
359
- }
129
+ function requireInteractiveTerminal() {
130
+ if (process.stdin.isTTY !== true) {
131
+ console.error('Error: stdin is not a terminal');
132
+ process.exitCode = 1;
133
+ return false;
134
+ }
135
+ if (process.stdout.isTTY !== true) {
136
+ console.error('Error: stdout is not a terminal');
137
+ process.exitCode = 1;
138
+ return false;
139
+ }
140
+ return true;
360
141
  }
361
142
  export async function main() {
362
143
  const { autoYes, help, version, usage, command, commandArgs, session: sessionIdentifier, listSessions, unknownOption, prompt, } = parseArgs(process.argv);
@@ -384,6 +165,22 @@ export async function main() {
384
165
  return;
385
166
  }
386
167
  const rootDir = process.cwd();
168
+ if (listSessions) {
169
+ const activeServerUrl = auth.readCliAuthConfig()?.serverUrl ?? DEFAULT_SERVER_URL;
170
+ printSessionList(rootDir, listSessionMetadata(rootDir), models.selectCacheForServer(models.readCachedServerModels(), activeServerUrl));
171
+ return;
172
+ }
173
+ const sourceSnapshot = sessionIdentifier
174
+ ? loadSessionSnapshot(rootDir, sessionIdentifier)
175
+ : null;
176
+ if (sessionIdentifier && !sourceSnapshot) {
177
+ console.error(`Error: No saved session named or identified by "${sessionIdentifier}" is available for this repo. Run \`ai --list-sessions\`.`);
178
+ process.exitCode = 1;
179
+ return;
180
+ }
181
+ if (!requireInteractiveTerminal()) {
182
+ return;
183
+ }
387
184
  const authConfig = requireCliAuthConfig();
388
185
  const serverSessionClient = sessions.createServerSessionClient({ config: authConfig });
389
186
  const cachedModels = models.selectCacheForServer(models.readCachedServerModels(), authConfig.serverUrl);
@@ -426,13 +223,6 @@ export async function main() {
426
223
  if (offlineNotice) {
427
224
  console.error(chalk.yellow(`⚠ Couldn't reach TheGitAI (${offlineNotice}). Starting with cached settings — it will reconnect on your next message.`));
428
225
  }
429
- if (listSessions) {
430
- printSessionList(rootDir, listSessionMetadata(rootDir), serverModels);
431
- return;
432
- }
433
- const sourceSnapshot = sessionIdentifier
434
- ? loadSessionSnapshot(rootDir, sessionIdentifier)
435
- : null;
436
226
  const selectedModelId = models.selectServerModel({
437
227
  requestedModelId: sourceSnapshot?.modelId ?? null,
438
228
  cached: cachedModels,
@@ -448,11 +238,8 @@ export async function main() {
448
238
  autoYes,
449
239
  modelId: selectedModelId,
450
240
  });
451
- session.confirmCommand = makeConfirmCommand(session);
452
- session.confirmPatch = makeConfirmPatch(session);
453
241
  if (sourceSnapshot) {
454
242
  applySessionSnapshot(session, sourceSnapshot);
455
- await saveSessionBoth({ session, serverSessionClient });
456
243
  }
457
244
  const projectIndex = createIndex({
458
245
  rootDir,
@@ -468,44 +255,27 @@ export async function main() {
468
255
  },
469
256
  });
470
257
  const initialPrompt = prompt || undefined;
471
- if (shouldUseClientRatatuiShell()) {
472
- await runClientInteractive({
473
- appendPromptHistory: (value) => appendPromptHistory(value, session.env),
474
- authConfig,
475
- debugUi: whoami.debugUi,
476
- projectIndex,
477
- serverModels,
478
- serverSessionClient,
479
- session,
480
- initialPrompt,
481
- usageText: async () => formatUsageText(await auth.fetchWhoamiResponse({ config: authConfig })),
482
- });
483
- await saveSessionBoth({ session, serverSessionClient });
484
- printSessionExit(session);
485
- return;
486
- }
487
- printStartupBanner(rootDir, modelLabel(serverModels, session.modelId), session.autoYes);
488
- printSessionStartup(session);
489
- setBackgroundJobSession(session.sessionId);
490
- try {
491
- await mainInteractive({
492
- authConfig,
493
- projectIndex,
494
- serverModels,
495
- serverSessionClient,
496
- session,
497
- usageText: async () => formatUsageText(await auth.fetchWhoamiResponse({ config: authConfig })),
498
- initialPrompt,
499
- });
500
- await saveSessionBoth({ session, serverSessionClient });
501
- printSessionExit(session);
502
- }
503
- finally {
504
- killAllBackgroundJobs({ sessionId: session.sessionId, remove: true });
505
- setBackgroundJobSession(null);
506
- }
258
+ await runClientInteractive({
259
+ appendPromptHistory: (value) => appendPromptHistory(value, session.env),
260
+ authConfig,
261
+ debugUi: whoami.debugUi,
262
+ projectIndex,
263
+ serverModels,
264
+ serverSessionClient,
265
+ session,
266
+ initialPrompt,
267
+ usageText: async () => formatUsageText(await auth.fetchWhoamiResponse({ config: authConfig })),
268
+ });
269
+ await saveSessionBoth({ session, serverSessionClient });
270
+ printSessionExit(session);
507
271
  }
508
272
  main().catch((error) => {
509
- console.error(chalk.red(`\n✖ Error: ${error.message}\n`));
273
+ if (isAuthenticationError(error)) {
274
+ auth.clearCliAuthConfig();
275
+ console.error(chalk.red(`\n✖ Error: ${authenticationErrorMessage(error)}\n`));
276
+ }
277
+ else {
278
+ console.error(chalk.red(`\n✖ Error: ${error.message}\n`));
279
+ }
510
280
  process.exit(1);
511
281
  });
@@ -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 { ServerApiError, authorizedJson, createTraceContext, failureMessage, normalizeServerUrl, readJsonResponse, retryTransient, } from './http.js';
4
+ import { ServerApiError, authorizedJson, createTraceContext, failureCode, failureMessage, normalizeServerUrl, readJsonResponse, retryTransient, } 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) {
@@ -78,6 +78,6 @@ export async function logoutFromServer({ config, fetchImpl = globalThis.fetch, }
78
78
  });
79
79
  if (!response.ok && response.status !== 401) {
80
80
  const data = (await readJsonResponse(response));
81
- throw new ServerApiError(failureMessage(data, response.status), response.status, trace.traceId);
81
+ throw new ServerApiError(failureMessage(data, response.status), response.status, trace.traceId, failureCode(data));
82
82
  }
83
83
  }
@@ -1,8 +1,10 @@
1
+ import { execSync } from 'node:child_process';
1
2
  import crypto from 'node:crypto';
3
+ import { readFileSync } from 'node:fs';
2
4
  import http from 'node:http';
3
5
  import os from 'node:os';
4
6
  import { openUrl } from '../core/open-url.js';
5
- import { ServerApiError, createTraceContext, failureMessage, normalizeServerUrl, readJsonResponse, } from './http.js';
7
+ import { ServerApiError, createTraceContext, failureCode, failureMessage, normalizeServerUrl, readJsonResponse, } from './http.js';
6
8
  const DEFAULT_WEBSITE_URL = 'https://thegit.ai';
7
9
  const DEFAULT_SERVER_URL = 'https://thegit.ai';
8
10
  const DEFAULT_TIMEOUT_MS = 5 * 60 * 1000;
@@ -21,6 +23,73 @@ function defaultDeviceName() {
21
23
  return os.hostname();
22
24
  }
23
25
  }
26
+ function readLinuxOsPrettyName() {
27
+ try {
28
+ const content = readFileSync('/etc/os-release', 'utf8');
29
+ return content.match(/^PRETTY_NAME="?([^"\n]*)"?$/m)?.[1]?.trim() ?? '';
30
+ }
31
+ catch {
32
+ return '';
33
+ }
34
+ }
35
+ function macArchLabel() {
36
+ try {
37
+ if (process.arch === 'arm64')
38
+ return 'Apple Silicon';
39
+ if (os.cpus().some((cpu) => cpu.model.includes('Apple'))) {
40
+ return 'Apple Silicon';
41
+ }
42
+ }
43
+ catch {
44
+ }
45
+ return 'Intel';
46
+ }
47
+ export function describeOperatingSystem() {
48
+ try {
49
+ if (process.platform === 'linux') {
50
+ const base = readLinuxOsPrettyName() || `Linux ${os.release()}`;
51
+ return process.arch === 'arm64' ? `${base}, ARM64` : base;
52
+ }
53
+ if (process.platform === 'darwin') {
54
+ let version = '';
55
+ try {
56
+ version = execSync('sw_vers -productVersion', {
57
+ stdio: ['ignore', 'pipe', 'ignore'],
58
+ })
59
+ .toString()
60
+ .trim();
61
+ }
62
+ catch {
63
+ }
64
+ return `${version ? `macOS ${version}` : 'macOS'}, ${macArchLabel()}`;
65
+ }
66
+ if (process.platform === 'win32') {
67
+ let label = '';
68
+ try {
69
+ label = os.version();
70
+ }
71
+ catch {
72
+ }
73
+ const build = Number(os.release().split('.')[2] ?? '0');
74
+ if (build >= 22000)
75
+ label = label.replace(/Windows 10/i, 'Windows 11');
76
+ const base = label || `Windows ${os.release()}`;
77
+ return process.arch === 'arm64' ? `${base}, ARM64` : base;
78
+ }
79
+ return `${process.platform} ${os.release()}`;
80
+ }
81
+ catch {
82
+ return process.platform;
83
+ }
84
+ }
85
+ export function withOperatingSystemInfo(name) {
86
+ const trimmed = name.trim();
87
+ const osLabel = describeOperatingSystem();
88
+ const combined = osLabel && !trimmed.includes(osLabel)
89
+ ? `${trimmed} (${osLabel})`
90
+ : trimmed;
91
+ return combined.slice(0, 180);
92
+ }
24
93
  export function generatePkce() {
25
94
  const verifier = crypto.randomBytes(32).toString('base64url');
26
95
  const challenge = crypto
@@ -56,7 +125,7 @@ async function exchangeCodeForToken({ serverUrl, code, codeVerifier, fetchImpl,
56
125
  });
57
126
  const data = (await readJsonResponse(response));
58
127
  if (!response.ok) {
59
- throw new ServerApiError(failureMessage(data, response.status), response.status, trace.traceId);
128
+ throw new ServerApiError(failureMessage(data, response.status), response.status, trace.traceId, failureCode(data));
60
129
  }
61
130
  const token = String(data?.token ?? '').trim();
62
131
  const customer = data?.customer;
@@ -77,7 +146,7 @@ export async function loginViaBrowser(options) {
77
146
  const fetchImpl = options.fetchImpl ?? globalThis.fetch;
78
147
  const openBrowser = options.openBrowser ?? openUrl;
79
148
  const onUrl = options.onUrl ?? (() => { });
80
- const deviceName = options.deviceName ?? defaultDeviceName();
149
+ const deviceName = withOperatingSystemInfo(options.deviceName ?? defaultDeviceName());
81
150
  const { verifier, challenge } = generatePkce();
82
151
  if (options.noBrowser) {
83
152
  const authUrl = buildAuthUrl(websiteUrl, {