@thegitai/cli 1.0.0-preview.3 → 1.0.0-preview.31

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 (58) hide show
  1. package/README.md +39 -6
  2. package/dist/bin/ai.js +142 -383
  3. package/dist/src/agent-mode.js +1 -6
  4. package/dist/src/api/auth.js +6 -4
  5. package/dist/src/api/browser-login.js +152 -37
  6. package/dist/src/api/chat.js +258 -38
  7. package/dist/src/api/contracts.js +55 -1
  8. package/dist/src/api/default-host.js +1 -0
  9. package/dist/src/api/http.js +69 -7
  10. package/dist/src/api/models.js +19 -10
  11. package/dist/src/background-jobs.js +2 -2
  12. package/dist/src/cli-args.js +19 -5
  13. package/dist/src/core/clipboard.js +7 -13
  14. package/dist/src/core/image-limits.js +56 -0
  15. package/dist/src/core/image-path-extractor.js +70 -3
  16. package/dist/src/core/session-image-store.js +199 -0
  17. package/dist/src/executor.js +25 -3
  18. package/dist/src/help-text.js +67 -18
  19. package/dist/src/permissions.js +243 -0
  20. package/dist/src/session-safety.js +0 -12
  21. package/dist/src/session-store.js +121 -20
  22. package/dist/src/session.js +14 -3
  23. package/dist/src/signin.js +58 -0
  24. package/dist/src/tool-executor.js +11 -46
  25. package/dist/src/tools/delete-file.js +15 -3
  26. package/dist/src/tools/index.js +13 -10
  27. package/dist/src/tools/patch-file.js +12 -26
  28. package/dist/src/tools/read-image-file.js +85 -0
  29. package/dist/src/tools/replace-document-text.js +28 -18
  30. package/dist/src/tools/restore-checkpoint.js +0 -1
  31. package/dist/src/tools/run-command.js +14 -71
  32. package/dist/src/tools/run-node-script.js +12 -81
  33. package/dist/src/tools/save-generated-image.js +120 -0
  34. package/dist/src/tools/str-replace.js +12 -26
  35. package/dist/src/tools/undo-edit.js +1 -6
  36. package/dist/src/tools/write-file.js +67 -11
  37. package/dist/src/turn-failure-marker.js +11 -0
  38. package/dist/src/ui/prompt-history-store.js +1 -1
  39. package/dist/src/ui/repl.js +649 -164
  40. package/dist/src/ui/tui/bridge.js +10 -0
  41. package/dist/src/ui/tui/build-frame.js +453 -115
  42. package/dist/src/ui/tui/markdown-render.js +81 -73
  43. package/dist/src/ui/tui/shell-input.js +206 -63
  44. package/dist/src/ui/tui/terminal-theme.js +28 -0
  45. package/dist/src/ui/tui/terminal-title.js +3 -0
  46. package/dist/src/ui/tui/terminal-writes.js +48 -0
  47. package/dist/src/ui/tui/text.js +158 -4
  48. package/dist/src/ui/tui/user-input.js +568 -0
  49. package/dist/src/utils.js +9 -0
  50. package/package.json +29 -6
  51. package/dist/src/markdown-renderer.js +0 -112
  52. package/dist/src/project-index.js +0 -221
  53. package/dist/src/tools/code-intel.js +0 -472
  54. package/dist/src/tools/find-symbol.js +0 -70
  55. package/dist/src/tools/hover-symbol.js +0 -95
  56. package/dist/src/tools/list-symbols.js +0 -55
  57. package/dist/src/tools/search-code.js +0 -37
  58. package/dist/src/tools/signature-help.js +0 -118
package/dist/bin/ai.js CHANGED
@@ -1,72 +1,48 @@
1
1
  #!/usr/bin/env node
2
2
  import chalk from '../src/colors.js';
3
- import { stdin as input, stdout as output } from 'node:process';
4
- import readline from 'node:readline/promises';
5
3
  import { ServerApi } from '../src/api/index.js';
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';
10
- 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';
4
+ import { DEFAULT_THEGITAI_HOST } from '../src/api/default-host.js';
5
+ import { isSignInCancelled } from '../src/api/browser-login.js';
6
+ import { runSignIn } from '../src/signin.js';
7
+ import { STARTUP_RETRY_BUDGET, authenticationErrorMessage, isAuthenticationError, isTransientNetworkError, } from '../src/api/http.js';
8
+ import { formatCliHelpText } from '../src/help-text.js';
9
+ import { createSession } from '../src/session.js';
10
+ import { applySessionSnapshot, listSessionMetadata, loadSessionSnapshot, saveSessionState, sessionHasUserMessage, } from '../src/session-store.js';
11
+ import { runClientInteractive } from '../src/ui/repl.js';
14
12
  import { appendPromptToFile } from '../src/ui/prompt-history-store.js';
15
13
  import { formatSessionExitNotice } from '../src/session-exit.js';
16
14
  import { formatUsageText } from '../src/usage.js';
17
15
  import { formatVersionLine } from '../src/version.js';
18
16
  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
- import { setScratchSession } from '../src/scratch-dir.js';
22
- const DEFAULT_SERVER_URL = 'https://thegit.ai';
23
17
  const { auth, chat, models, sessions } = ServerApi;
24
18
  function printUsage() {
25
19
  console.log(formatCliHelpText({ color: process.stdout.isTTY === true }));
26
20
  }
27
- async function promptText(question, fallback = null) {
28
- const rl = readline.createInterface({ input, output });
29
- try {
30
- const suffix = fallback ? ` (${fallback})` : '';
31
- const answer = await rl.question(`${question}${suffix}: `);
32
- return answer.trim() || fallback || '';
33
- }
34
- finally {
35
- rl.close();
36
- }
21
+ function unreachableReason(error) {
22
+ const err = error;
23
+ const code = err?.code ?? err?.cause?.code;
24
+ if (err?.name === 'TimeoutError' || err?.name === 'AbortError') {
25
+ return 'network timeout';
26
+ }
27
+ if (code === 'ENOTFOUND' || code === 'EAI_AGAIN')
28
+ return 'DNS lookup failed';
29
+ if (code === 'ECONNREFUSED')
30
+ return 'connection refused';
31
+ if (code === 'ECONNRESET' || code === 'EPIPE')
32
+ return 'connection reset';
33
+ return err?.message ? String(err.message) : 'network error';
37
34
  }
38
35
  function appendPromptHistory(prompt, env = process.env) {
39
36
  appendPromptToFile(prompt, env);
40
37
  }
41
- async function runAuthCommand(command, args) {
42
- if (command === 'login') {
43
- const serverUrl = DEFAULT_SERVER_URL;
44
- const noBrowser = args.includes('--no-browser');
45
- console.log(chalk.dim(noBrowser
46
- ? 'Sign in on the website, then paste the authorization code here.'
47
- : 'Opening your browser to sign in…'));
48
- const result = await loginViaBrowser({
49
- serverUrl,
50
- noBrowser,
51
- onUrl: (url) => {
52
- console.log(chalk.dim(noBrowser ? 'Open this URL to sign in:' : 'If your browser did not open, visit:'));
53
- console.log(` ${url}`);
54
- },
55
- onWaiting: () => console.log(chalk.dim('Waiting for you to finish signing in…')),
56
- promptCode: noBrowser
57
- ? () => promptText('Paste the authorization code')
58
- : undefined,
59
- deviceName: process.env.THEGITAI_DEVICE_NAME?.trim() || undefined,
60
- });
61
- auth.writeCliAuthConfig(result);
62
- console.log(chalk.green(`✓ Logged in as ${result.customer.email}`));
63
- console.log(chalk.dim(`Server: ${result.serverUrl}`));
64
- console.log(chalk.dim('You can close the browser tab. Run `ai` in a repo to start.'));
65
- return;
66
- }
38
+ async function runAuthCommand(command) {
67
39
  const config = auth.readCliAuthConfig();
68
40
  if (!config) {
69
- throw new Error('Not logged in. Run `ai login` first.');
41
+ if (command === 'logout') {
42
+ console.log(chalk.green('Already signed out.'));
43
+ return;
44
+ }
45
+ throw new Error('Not signed in. Run `ai` to sign in.');
70
46
  }
71
47
  if (command === 'whoami') {
72
48
  const customer = await auth.fetchWhoami({ config });
@@ -74,19 +50,25 @@ async function runAuthCommand(command, args) {
74
50
  return;
75
51
  }
76
52
  if (command === 'logout') {
77
- await auth.logoutFromServer({ config });
53
+ try {
54
+ await auth.logoutFromServer({ config });
55
+ }
56
+ catch {
57
+ }
78
58
  auth.clearCliAuthConfig();
79
59
  console.log(chalk.green('Logged out.'));
80
60
  return;
81
61
  }
82
62
  throw new Error(`Unknown auth command: ${command}`);
83
63
  }
84
- function requireCliAuthConfig() {
64
+ async function ensureCliAuthConfig() {
85
65
  const config = auth.readCliAuthConfig();
86
- if (!config) {
87
- throw new Error('Not logged in. Run `ai login` first.');
66
+ if (config)
67
+ return config;
68
+ if (process.stdin.isTTY !== true || process.stdout.isTTY !== true) {
69
+ throw new Error('Not signed in. Run `ai login` on a terminal to sign in.');
88
70
  }
89
- return config;
71
+ return await runSignIn();
90
72
  }
91
73
  function formatSessionName(name) {
92
74
  return name ? `"${name}"` : '(unnamed)';
@@ -112,54 +94,16 @@ function printSessionList(rootDir, sessions, serverModels) {
112
94
  }
113
95
  }
114
96
  }
115
- function printStartupBanner(rootDir, modelLabel, autoYes) {
116
- console.log(chalk.dim(`Project: ${rootDir}`));
117
- console.log(chalk.dim(`Model: ${modelLabel}`));
118
- if (autoYes) {
119
- console.log(chalk.dim('Auto-confirm: enabled'));
120
- }
121
- }
122
- function printSessionStartup(session) {
123
- console.log(chalk.dim(`Session: ${session.sessionName ? `${session.sessionName} ` : ''}${session.sessionId}`));
124
- }
125
97
  function printSessionExit(session) {
126
98
  console.log(chalk.dim(`\n${formatSessionExitNotice(session.sessionId)}\n`));
127
99
  }
128
100
  function modelLabel(serverModels, modelId) {
129
- return (serverModels.models.find((model) => model.id === modelId)?.label ??
130
- 'Unknown model');
131
- }
132
- function formatJobElapsedMs(ms) {
133
- const totalSeconds = Math.max(0, Math.floor(ms / 1000));
134
- if (totalSeconds < 60)
135
- return `${totalSeconds}s`;
136
- const minutes = Math.floor(totalSeconds / 60);
137
- if (minutes < 60)
138
- return `${minutes}m${String(totalSeconds % 60).padStart(2, '0')}s`;
139
- return `${Math.floor(minutes / 60)}h${String(minutes % 60).padStart(2, '0')}m`;
140
- }
141
- function makeConfirmCommand(session) {
142
- return async (command) => {
143
- console.log(chalk.bold(`\nCommand approval needed:\n${command}\n`));
144
- const answer = (await promptText('Approve? [y]es/[a]ll/[n]o', 'n')).toLowerCase();
145
- if (answer === 'a' || answer === 'all') {
146
- session.autoYes = true;
147
- return true;
148
- }
149
- return answer === 'y' || answer === 'yes';
150
- };
151
- }
152
- function makeConfirmPatch(session) {
153
- return async (filePath) => {
154
- const answer = (await promptText(`Apply patch to ${filePath}? [y]es/[a]ll/[n]o`, 'n')).toLowerCase();
155
- if (answer === 'a' || answer === 'all') {
156
- session.autoYes = true;
157
- return true;
158
- }
159
- return answer === 'y' || answer === 'yes';
160
- };
101
+ return (serverModels?.models.find((model) => model.id === modelId)?.label ??
102
+ `Model ${modelId}`);
161
103
  }
162
104
  async function saveSessionBoth({ session, serverSessionClient, }) {
105
+ if (!sessionHasUserMessage(session))
106
+ return;
163
107
  saveSessionState(session);
164
108
  try {
165
109
  await serverSessionClient.save(session);
@@ -168,200 +112,21 @@ async function saveSessionBoth({ session, serverSessionClient, }) {
168
112
  console.error(chalk.yellow(`Warning: session save failed: ${error.message}`));
169
113
  }
170
114
  }
171
- async function promptForModelSelection(currentModelId, authConfig) {
172
- const serverModels = await models.fetchServerModels({ config: authConfig });
173
- console.log(chalk.bold('\nAvailable models'));
174
- for (const model of serverModels.models) {
175
- const marker = model.id === currentModelId ? '*' : ' ';
176
- console.log(`${marker} ${model.id} ${model.label}`);
177
- }
178
- console.log();
179
- const selected = (await promptText('Model id', '')).trim() || null;
180
- if (!selected) {
181
- return { selected: null, serverModels };
182
- }
183
- return {
184
- selected: models.validateServerModel(selected, serverModels),
185
- serverModels,
186
- };
187
- }
188
- async function promptForResumeSession(rootDir, serverModels) {
189
- const sessions = listSessionMetadata(rootDir);
190
- if (!sessions.length) {
191
- console.log(chalk.dim('No saved sessions for this repo.\n'));
192
- return null;
193
- }
194
- printSessionList(rootDir, sessions, serverModels);
195
- console.log();
196
- const identifier = (await promptText('Session id or name', '')).trim();
197
- if (!identifier) {
198
- return null;
199
- }
200
- return loadSessionSnapshot(rootDir, identifier);
201
- }
202
- async function runTurn({ authConfig, projectIndex, serverSessionClient, session, inputText, }) {
203
- const prompt = String(inputText ?? '').trim();
204
- if (!prompt)
205
- return;
206
- appendPromptHistory(prompt, session.env);
207
- const result = await chat.sendServerUserMessage({
208
- config: authConfig,
209
- projectIndex,
210
- session,
211
- input: prompt,
212
- });
213
- if (result.text) {
214
- console.log(`\n${chalk.green('TheGitAI>')}`);
215
- console.log(renderMarkdownForTerminal(result.text));
216
- console.log();
217
- }
218
- await saveSessionBoth({ session, serverSessionClient });
219
- }
220
- async function mainInteractive({ authConfig, projectIndex, serverModels, serverSessionClient, session, usageText, initialPrompt, }) {
221
- if (initialPrompt) {
222
- console.log(chalk.dim(`Prompt: "${initialPrompt}"`));
223
- await runTurn({
224
- authConfig,
225
- projectIndex,
226
- serverSessionClient,
227
- session,
228
- inputText: initialPrompt,
229
- });
230
- }
231
- while (true) {
232
- const inputText = await promptText('you');
233
- const trimmed = inputText.trim();
234
- if (!trimmed)
235
- continue;
236
- if (trimmed === '/exit')
237
- return;
238
- if (trimmed === '/help') {
239
- console.log(renderMarkdownForTerminal(formatInteractiveHelpText()));
240
- continue;
241
- }
242
- if (trimmed === '/usage') {
243
- console.log(await usageText());
244
- continue;
245
- }
246
- if (trimmed === '/clear') {
247
- clearConversation(session);
248
- await saveSessionBoth({ session, serverSessionClient });
249
- console.log(chalk.dim('Conversation cleared.\n'));
250
- continue;
251
- }
252
- if (trimmed === '/jobs' || trimmed.startsWith('/jobs ')) {
253
- const jobsArgs = trimmed.slice('/jobs'.length).trim();
254
- const killMatch = jobsArgs.match(/^kill\s+(\S+)$/);
255
- if (killMatch) {
256
- const jobId = killMatch[1];
257
- const killed = await killBackgroundJob(jobId);
258
- await collectBackgroundJobUiKillMutations({
259
- session,
260
- projectIndex,
261
- jobId,
262
- result: killed,
263
- });
264
- if (!killed.ok) {
265
- console.log(chalk.red(killed.error ?? 'Background job kill failed.'));
266
- }
267
- else if (killed.alreadyFinished) {
268
- console.log(chalk.dim(`${jobId} had already finished.`));
269
- }
270
- continue;
271
- }
272
- const outputMatch = jobsArgs.match(/^output\s+(\S+)$/);
273
- if (outputMatch) {
274
- const jobId = outputMatch[1];
275
- await collectBackgroundJobUiOutputMutations({
276
- session,
277
- projectIndex,
278
- jobId,
279
- });
280
- const job = listBackgroundJobs().find((candidate) => candidate.id === jobId);
281
- const buffered = getJobBufferedOutput(jobId);
282
- if (!job || !buffered) {
283
- console.log(chalk.red(`Unknown background job id: ${jobId}`));
284
- continue;
285
- }
286
- if (buffered.droppedChars > 0) {
287
- console.log(chalk.dim(`... (${buffered.droppedChars} chars of older output dropped) ...`));
288
- }
289
- console.log(buffered.output || chalk.dim('(no output captured)'));
290
- continue;
291
- }
292
- if (jobsArgs) {
293
- console.log(chalk.dim('Usage: /jobs — list · /jobs output <id> — full output · /jobs kill <id> — kill'));
294
- continue;
295
- }
296
- const jobsList = listBackgroundJobs();
297
- if (!jobsList.length) {
298
- console.log(chalk.dim('No background jobs in this session.'));
299
- continue;
300
- }
301
- for (const job of jobsList) {
302
- const elapsed = formatJobElapsedMs((job.endedAt ?? Date.now()) - job.startedAt);
303
- const stateText = job.status === 'running'
304
- ? `running · ${elapsed}`
305
- : job.status === 'killed'
306
- ? `killed · ran ${elapsed}`
307
- : job.status === 'error'
308
- ? 'failed to start'
309
- : `exited (code ${job.exitCode ?? 1}) · ran ${elapsed}`;
310
- console.log(`${job.id} · ${stateText}\n $ ${job.command}`);
311
- }
312
- continue;
313
- }
314
- if (trimmed === '/resume') {
315
- const snapshot = await promptForResumeSession(session.rootDir, serverModels);
316
- if (!snapshot) {
317
- console.log(chalk.dim('Resume cancelled.\n'));
318
- continue;
319
- }
320
- applySessionSnapshot(session, snapshot);
321
- setBackgroundJobSession(session.sessionId);
322
- setScratchSession(session.sessionId);
323
- await saveSessionBoth({ session, serverSessionClient });
324
- console.log(chalk.dim(`Resumed session${session.sessionName ? ` "${session.sessionName}"` : ''} (${session.sessionId})\n`));
325
- continue;
326
- }
327
- if (trimmed === '/model' || trimmed.startsWith('/model ')) {
328
- const inline = trimmed.slice('/model'.length).trim();
329
- let serverModels;
330
- let selected = null;
331
- if (inline) {
332
- serverModels = await models.fetchServerModels({ config: authConfig });
333
- selected = models.validateServerModel(inline, serverModels);
334
- }
335
- else {
336
- const response = await promptForModelSelection(session.modelId, authConfig);
337
- serverModels = response.serverModels;
338
- selected = response.selected;
339
- }
340
- if (!selected) {
341
- console.log(chalk.dim('Model selection cancelled.\n'));
342
- continue;
343
- }
344
- session.modelId = selected;
345
- models.updateSelectedModelCache({
346
- config: authConfig,
347
- selectedModelId: selected,
348
- serverModels,
349
- });
350
- await saveSessionBoth({ session, serverSessionClient });
351
- console.log(chalk.dim(`Switched to ${modelLabel(serverModels, selected)}.\n`));
352
- continue;
353
- }
354
- await runTurn({
355
- authConfig,
356
- projectIndex,
357
- serverSessionClient,
358
- session,
359
- inputText: trimmed,
360
- });
361
- }
115
+ function requireInteractiveTerminal() {
116
+ if (process.stdin.isTTY !== true) {
117
+ console.error('Error: stdin is not a terminal');
118
+ process.exitCode = 1;
119
+ return false;
120
+ }
121
+ if (process.stdout.isTTY !== true) {
122
+ console.error('Error: stdout is not a terminal');
123
+ process.exitCode = 1;
124
+ return false;
125
+ }
126
+ return true;
362
127
  }
363
128
  export async function main() {
364
- const { autoYes, help, version, usage, command, commandArgs, session: sessionIdentifier, listSessions, unknownOption, prompt, } = parseArgs(process.argv);
129
+ const { autoYes, help, version, usage, command, session: sessionIdentifier, listSessions, unknownOption, prompt, } = parseArgs(process.argv);
365
130
  if (version) {
366
131
  console.log(formatVersionLine());
367
132
  return;
@@ -376,65 +141,84 @@ export async function main() {
376
141
  process.exitCode = 2;
377
142
  return;
378
143
  }
379
- if (command) {
380
- await runAuthCommand(command, commandArgs);
144
+ if (command && command !== 'login') {
145
+ await runAuthCommand(command);
381
146
  return;
382
147
  }
383
148
  if (usage) {
384
- const authConfig = requireCliAuthConfig();
149
+ const authConfig = await ensureCliAuthConfig();
385
150
  console.log(formatUsageText(await auth.fetchWhoamiResponse({ config: authConfig })));
386
151
  return;
387
152
  }
388
153
  const rootDir = process.cwd();
389
- const authConfig = requireCliAuthConfig();
154
+ if (listSessions) {
155
+ const activeServerUrl = auth.readCliAuthConfig()?.serverUrl ?? DEFAULT_THEGITAI_HOST;
156
+ printSessionList(rootDir, listSessionMetadata(rootDir), models.selectCacheForServer(models.readCachedServerModels(), activeServerUrl));
157
+ return;
158
+ }
159
+ const sourceSnapshot = sessionIdentifier
160
+ ? loadSessionSnapshot(rootDir, sessionIdentifier)
161
+ : null;
162
+ if (sessionIdentifier && !sourceSnapshot) {
163
+ console.error(`Error: No saved session named or identified by "${sessionIdentifier}" is available for this repo. Run \`ai --list-sessions\`.`);
164
+ process.exitCode = 1;
165
+ return;
166
+ }
167
+ if (!requireInteractiveTerminal()) {
168
+ return;
169
+ }
170
+ const authConfig = await ensureCliAuthConfig();
390
171
  const serverSessionClient = sessions.createServerSessionClient({ config: authConfig });
391
172
  const cachedModels = models.selectCacheForServer(models.readCachedServerModels(), authConfig.serverUrl);
173
+ const [modelsOutcome, whoamiOutcome] = await Promise.allSettled([
174
+ models.fetchServerModels({
175
+ config: authConfig,
176
+ budget: STARTUP_RETRY_BUDGET,
177
+ }),
178
+ auth.fetchWhoamiResponse({
179
+ config: authConfig,
180
+ budget: STARTUP_RETRY_BUDGET,
181
+ }),
182
+ ]);
392
183
  let offlineNotice = null;
393
184
  let serverModels;
394
- try {
395
- serverModels = await models.fetchServerModels({ config: authConfig });
185
+ if (modelsOutcome.status === 'fulfilled') {
186
+ serverModels = modelsOutcome.value;
396
187
  }
397
- catch (error) {
398
- if (isTransientNetworkError(error) && cachedModels?.models.length) {
399
- serverModels = { models: cachedModels.models };
400
- offlineNotice = error?.message ? String(error.message) : 'network error';
401
- }
402
- else {
403
- throw error;
404
- }
188
+ else if (isTransientNetworkError(modelsOutcome.reason) &&
189
+ cachedModels?.models.length) {
190
+ serverModels = { models: cachedModels.models };
191
+ offlineNotice = unreachableReason(modelsOutcome.reason);
192
+ }
193
+ else if (isTransientNetworkError(modelsOutcome.reason)) {
194
+ throw new Error(`Couldn't reach TheGitAI to load your models (${unreachableReason(modelsOutcome.reason)}).\nCheck your internet connection and run \`ai\` again.`);
195
+ }
196
+ else {
197
+ throw modelsOutcome.reason;
405
198
  }
406
199
  let whoami;
407
- try {
408
- whoami = await auth.fetchWhoamiResponse({ config: authConfig });
200
+ if (whoamiOutcome.status === 'fulfilled') {
201
+ whoami = whoamiOutcome.value;
202
+ }
203
+ else if (isTransientNetworkError(whoamiOutcome.reason)) {
204
+ whoami = {
205
+ customer: {
206
+ id: '',
207
+ uuid: '',
208
+ email: authConfig.email,
209
+ customer_type: authConfig.customerType ?? 'USER',
210
+ scopes: [],
211
+ },
212
+ debugUi: { showSessionId: false },
213
+ };
214
+ offlineNotice ??= unreachableReason(whoamiOutcome.reason);
409
215
  }
410
- catch (error) {
411
- if (isTransientNetworkError(error)) {
412
- whoami = {
413
- customer: {
414
- id: '',
415
- uuid: '',
416
- email: authConfig.email,
417
- customer_type: authConfig.customerType ?? 'USER',
418
- scopes: [],
419
- },
420
- debugUi: { showSessionId: false },
421
- };
422
- offlineNotice ??= error?.message ? String(error.message) : 'network error';
423
- }
424
- else {
425
- throw error;
426
- }
216
+ else {
217
+ throw whoamiOutcome.reason;
427
218
  }
428
219
  if (offlineNotice) {
429
220
  console.error(chalk.yellow(`⚠ Couldn't reach TheGitAI (${offlineNotice}). Starting with cached settings — it will reconnect on your next message.`));
430
221
  }
431
- if (listSessions) {
432
- printSessionList(rootDir, listSessionMetadata(rootDir), serverModels);
433
- return;
434
- }
435
- const sourceSnapshot = sessionIdentifier
436
- ? loadSessionSnapshot(rootDir, sessionIdentifier)
437
- : null;
438
222
  const selectedModelId = models.selectServerModel({
439
223
  requestedModelId: sourceSnapshot?.modelId ?? null,
440
224
  cached: cachedModels,
@@ -450,66 +234,41 @@ export async function main() {
450
234
  autoYes,
451
235
  modelId: selectedModelId,
452
236
  });
453
- session.confirmCommand = makeConfirmCommand(session);
454
- session.confirmPatch = makeConfirmPatch(session);
455
237
  if (sourceSnapshot) {
456
238
  applySessionSnapshot(session, sourceSnapshot);
457
- await saveSessionBoth({ session, serverSessionClient });
458
239
  }
459
- setScratchSession(session.sessionId);
460
- const projectIndex = createIndex({
461
- rootDir,
462
- onStatus: (message) => {
463
- if (message.trim()) {
464
- console.log(chalk.dim(` ${message}`));
465
- }
466
- },
467
- onContextLog: (message) => {
468
- if (message.trim()) {
469
- console.log(chalk.cyan(` ${message}`));
470
- }
471
- },
472
- });
473
240
  const initialPrompt = prompt || undefined;
474
- if (shouldUseClientRatatuiShell()) {
475
- await runClientInteractive({
476
- appendPromptHistory: (value) => appendPromptHistory(value, session.env),
477
- authConfig,
478
- debugUi: whoami.debugUi,
479
- projectIndex,
480
- serverModels,
481
- serverSessionClient,
482
- session,
483
- initialPrompt,
484
- usageText: async () => formatUsageText(await auth.fetchWhoamiResponse({ config: authConfig })),
485
- });
486
- await saveSessionBoth({ session, serverSessionClient });
487
- printSessionExit(session);
241
+ const outcome = await runClientInteractive({
242
+ appendPromptHistory: (value) => appendPromptHistory(value, session.env),
243
+ authConfig,
244
+ debugUi: whoami.debugUi,
245
+ serverModels,
246
+ serverSessionClient,
247
+ session,
248
+ initialPrompt,
249
+ usageText: async () => formatUsageText(await auth.fetchWhoamiResponse({ config: authConfig })),
250
+ });
251
+ if (outcome.signedOut) {
252
+ if (sessionHasUserMessage(session)) {
253
+ saveSessionState(session);
254
+ }
255
+ console.log(chalk.green('\n✓ Logged out.\n'));
488
256
  return;
489
257
  }
490
- printStartupBanner(rootDir, modelLabel(serverModels, session.modelId), session.autoYes);
491
- printSessionStartup(session);
492
- setBackgroundJobSession(session.sessionId);
493
- try {
494
- await mainInteractive({
495
- authConfig,
496
- projectIndex,
497
- serverModels,
498
- serverSessionClient,
499
- session,
500
- usageText: async () => formatUsageText(await auth.fetchWhoamiResponse({ config: authConfig })),
501
- initialPrompt,
502
- });
503
- await saveSessionBoth({ session, serverSessionClient });
504
- printSessionExit(session);
505
- }
506
- finally {
507
- killAllBackgroundJobs({ sessionId: session.sessionId, remove: true });
508
- setBackgroundJobSession(null);
509
- setScratchSession(null);
510
- }
258
+ await saveSessionBoth({ session, serverSessionClient });
259
+ printSessionExit(session);
511
260
  }
512
261
  main().catch((error) => {
513
- console.error(chalk.red(`\n✖ Error: ${error.message}\n`));
262
+ if (isSignInCancelled(error)) {
263
+ console.error(chalk.dim('\nSign-in cancelled. Nothing was saved.\n'));
264
+ process.exit(130);
265
+ }
266
+ if (isAuthenticationError(error)) {
267
+ auth.clearCliAuthConfig();
268
+ console.error(chalk.red(`\n✖ Error: ${authenticationErrorMessage(error)}\n`));
269
+ }
270
+ else {
271
+ console.error(chalk.red(`\n✖ Error: ${error.message}\n`));
272
+ }
514
273
  process.exit(1);
515
274
  });
@@ -1,14 +1,9 @@
1
1
  export const AGENT_MODES = ['default', 'auto-accept', 'plan'];
2
2
  const PLAN_MODE_TOOL_NAMES = new Set([
3
- 'search_code',
4
3
  'list_files',
5
4
  'list_directories',
6
5
  'read_file',
7
6
  'grep_code',
8
- 'find_symbol',
9
- 'list_symbols',
10
- 'hover_symbol',
11
- 'signature_help',
12
7
  'read_document',
13
8
  'analyze_image',
14
9
  'run_command',
@@ -46,7 +41,7 @@ export function nextAgentMode(mode) {
46
41
  export function agentModeAllowsTool(mode, toolName) {
47
42
  return mode !== 'plan' || PLAN_MODE_TOOL_NAMES.has(toolName);
48
43
  }
49
- function getUnquotedShellText(command) {
44
+ export function getUnquotedShellText(command) {
50
45
  let quote = null;
51
46
  let escaped = false;
52
47
  let text = '';
@@ -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) {
@@ -50,12 +50,14 @@ export async function fetchWhoami({ config, fetchImpl = globalThis.fetch, }) {
50
50
  const data = await fetchWhoamiResponse({ config, fetchImpl });
51
51
  return data.customer;
52
52
  }
53
- export async function fetchWhoamiResponse({ config, fetchImpl = globalThis.fetch, }) {
53
+ export async function fetchWhoamiResponse({ config, fetchImpl = globalThis.fetch, budget = {}, }) {
54
+ const { timeoutMs, ...ladder } = budget;
54
55
  const data = (await retryTransient(() => authorizedJson({
55
56
  config,
56
57
  path: '/v1/auth/whoami',
57
58
  fetchImpl,
58
- })));
59
+ ...(timeoutMs == null ? {} : { timeoutMs }),
60
+ }), ladder));
59
61
  if (!data?.customer?.email) {
60
62
  throw new Error('Server returned an invalid whoami response.');
61
63
  }
@@ -78,6 +80,6 @@ export async function logoutFromServer({ config, fetchImpl = globalThis.fetch, }
78
80
  });
79
81
  if (!response.ok && response.status !== 401) {
80
82
  const data = (await readJsonResponse(response));
81
- throw new ServerApiError(failureMessage(data, response.status), response.status, trace.traceId);
83
+ throw new ServerApiError(failureMessage(data, response.status), response.status, trace.traceId, failureCode(data));
82
84
  }
83
85
  }