@thegitai/cli 1.0.0-preview.5 → 1.0.0-preview.51

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 (76) hide show
  1. package/README.md +93 -4
  2. package/dist/bin/ai.js +142 -112
  3. package/dist/bin/browser-host.js +265 -0
  4. package/dist/src/agent-mode.js +32 -7
  5. package/dist/src/api/auth.js +10 -4
  6. package/dist/src/api/browser-login.js +149 -37
  7. package/dist/src/api/chat.js +680 -166
  8. package/dist/src/api/contracts.js +55 -1
  9. package/dist/src/api/default-host.js +1 -0
  10. package/dist/src/api/http.js +151 -7
  11. package/dist/src/api/models.js +29 -10
  12. package/dist/src/api/startup-handshake.js +36 -0
  13. package/dist/src/api/turn-recovery.js +189 -0
  14. package/dist/src/background-jobs.js +4 -2
  15. package/dist/src/browser/bridge.js +262 -0
  16. package/dist/src/browser/framing.js +42 -0
  17. package/dist/src/browser/native-host.js +219 -0
  18. package/dist/src/browser/protocol.js +46 -0
  19. package/dist/src/browser/session-bridge.js +130 -0
  20. package/dist/src/cli-args.js +19 -5
  21. package/dist/src/client-environment.js +2 -0
  22. package/dist/src/core/clipboard.js +7 -13
  23. package/dist/src/core/image-limits.js +56 -0
  24. package/dist/src/core/image-path-extractor.js +70 -3
  25. package/dist/src/core/session-image-store.js +246 -0
  26. package/dist/src/edit-journal.js +33 -2
  27. package/dist/src/executor.js +25 -3
  28. package/dist/src/help-text.js +112 -21
  29. package/dist/src/patcher.js +43 -4
  30. package/dist/src/permissions.js +288 -0
  31. package/dist/src/preferences.js +39 -0
  32. package/dist/src/quota-notice.js +35 -0
  33. package/dist/src/session-safety.js +0 -12
  34. package/dist/src/session-store.js +121 -20
  35. package/dist/src/session.js +19 -4
  36. package/dist/src/signin.js +58 -0
  37. package/dist/src/tool-executor.js +13 -46
  38. package/dist/src/tools/browser.js +641 -0
  39. package/dist/src/tools/delete-file.js +20 -4
  40. package/dist/src/tools/index.js +32 -10
  41. package/dist/src/tools/patch-file.js +12 -26
  42. package/dist/src/tools/read-file.js +5 -0
  43. package/dist/src/tools/read-image-file.js +85 -0
  44. package/dist/src/tools/replace-document-text.js +28 -18
  45. package/dist/src/tools/restore-checkpoint.js +0 -1
  46. package/dist/src/tools/run-command.js +14 -71
  47. package/dist/src/tools/run-node-script.js +12 -81
  48. package/dist/src/tools/save-generated-image.js +120 -0
  49. package/dist/src/tools/str-replace.js +12 -26
  50. package/dist/src/tools/undo-edit.js +1 -6
  51. package/dist/src/tools/write-file.js +67 -11
  52. package/dist/src/ui/prompt-history-store.js +1 -1
  53. package/dist/src/ui/repl.js +949 -279
  54. package/dist/src/ui/slash-commands.js +121 -0
  55. package/dist/src/ui/tui/attention.js +25 -0
  56. package/dist/src/ui/tui/bridge.js +38 -5
  57. package/dist/src/ui/tui/build-frame.js +686 -223
  58. package/dist/src/ui/tui/composer-layout.js +76 -0
  59. package/dist/src/ui/tui/desktop-notification.js +118 -0
  60. package/dist/src/ui/tui/markdown-render.js +11 -2
  61. package/dist/src/ui/tui/shell-input.js +256 -73
  62. package/dist/src/ui/tui/shortcuts.js +61 -0
  63. package/dist/src/ui/tui/terminal-theme.js +28 -0
  64. package/dist/src/ui/tui/user-input.js +593 -0
  65. package/dist/src/update-check.js +205 -0
  66. package/dist/src/usage.js +5 -2
  67. package/dist/src/utils.js +13 -0
  68. package/dist/src/version.js +52 -2
  69. package/package.json +22 -9
  70. package/dist/src/project-index.js +0 -233
  71. package/dist/src/tools/code-intel.js +0 -472
  72. package/dist/src/tools/find-symbol.js +0 -70
  73. package/dist/src/tools/hover-symbol.js +0 -95
  74. package/dist/src/tools/list-symbols.js +0 -55
  75. package/dist/src/tools/search-code.js +0 -37
  76. package/dist/src/tools/signature-help.js +0 -118
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # TheGitAI — AI coding agent for your terminal
2
2
 
3
- TheGitAI is an AI coding agent for your terminal. It indexes your repository,
3
+ TheGitAI is an AI coding agent for your terminal. It reads and searches your repository,
4
4
  writes and edits files, runs commands, and builds features with you.
5
5
 
6
6
  Talk to your repo in plain English. TheGitAI can keep long-running processes
@@ -12,14 +12,26 @@ going while you work, all with your approval.
12
12
  npm i -g @thegitai/cli
13
13
  ```
14
14
 
15
- Requires Node.js 24 or newer.
15
+ Supports Node.js 22 and newer. Node.js 24 is recommended.
16
+
17
+ This installs the `ai` command. If `ai` is already taken by another command on
18
+ your machine, `thegitai` starts the same CLI.
19
+
20
+ ## Updates
21
+
22
+ `ai` checks for a newer release each time it starts, and offers to install it
23
+ for you. TheGitAI works best on the latest version, and an older CLI may not
24
+ work optimally. **Update now** is already selected, so Enter takes it — the CLI
25
+ installs the update and asks you to start `ai` again, so the whole session runs
26
+ one version — while ↑/↓ moves to **Skip** to go on with the version you have. Skipping lasts for that session
27
+ only; the next start asks again. Ctrl+C quits without installing anything.
16
28
 
17
29
  ## Usage
18
30
 
19
31
  ```text
20
- ai start an interactive session in the current repo
32
+ ai sign in if needed, then start a session in the current repo
21
33
  ai "<request>" start a session with <request> as the first message
22
- ai login sign in via your browser (--no-browser for SSH/headless)
34
+ ai login the same as `ai`; kept for muscle memory
23
35
  ai whoami show the signed-in account
24
36
  ai --usage show account usage and reset times
25
37
  ai logout sign out
@@ -34,6 +46,83 @@ always be viewed or resumed from that computer. Continuing it through the
34
46
  service requires the TheGitAI account used for that session. Local sessions can
35
47
  also be listed while signed out or offline.
36
48
 
49
+ Signing in opens your browser. The same screen also prints a URL you can open on
50
+ any other device and a box for an authorization code, so SSH and headless
51
+ machines need no separate command: open the URL wherever you have a browser,
52
+ choose "On another computer" on that page, and paste the code it shows.
53
+
54
+ CLI login tokens use a rolling 48-hour inactivity timeout. If one expires during
55
+ any server request, the CLI removes the expired credential and signs you in
56
+ again on the next run.
57
+
58
+ ## When the connection drops
59
+
60
+ A dropped connection does not end your turn. The CLI shows
61
+ `Connection interrupted · retrying in 10s · retry 1/3` under the working timer,
62
+ waits ten seconds, and reconnects to the same turn — it never starts the request
63
+ over, and a command, edit or browser action that already ran is not run again.
64
+ It tries three times. Once it is back, the turn simply continues and a short
65
+ `Connection restored` note stays in the transcript.
66
+
67
+ The same applies when you start `ai` while TheGitAI is briefly unavailable:
68
+ startup shows `Couldn't reach TheGitAI (…) · retrying in 10s · retry 1/3` and
69
+ waits up to three times before giving up.
70
+
71
+ If TheGitAI itself restarted while it was working, the turn cannot resume
72
+ where it was. It continues instead: the CLI sends your request once more with
73
+ the steps that already finished on your machine and everything you told it
74
+ during the turn, and the work picks up from there — a command that already ran
75
+ is not run again, and a correction you made still stands — with a short
76
+ `Server restarted` note in the transcript.
77
+
78
+ Press Esc at any point to stop the turn instead. If all three retries fail, the
79
+ turn ends with an error that names what was interrupted, notes that some actions
80
+ may already have completed, and leaves anything you had queued in the composer
81
+ rather than sending it on top of a turn whose outcome is unknown.
82
+
83
+ ## Chrome browser control
84
+
85
+ The TheGitAI Chrome extension is separately installed. After installing it,
86
+ start `ai` normally and ask about your current tab or a website task. It uses
87
+ your existing Chrome profile, website sessions, and terminal login. Connection
88
+ is automatic; no setup/start command, second sign-in, or manual tab attachment
89
+ is required. Tabs used by the agent join a TheGitAI tab group in an existing
90
+ window. The conversation stays in your terminal.
91
+
92
+ Default mode asks before using a website and can remember that site for the
93
+ session. Bypass all permissions allows browser actions; Plan mode reads pages
94
+ without clicking, typing, or navigating. Use Stop/Resume in the extension popup to
95
+ pause browser control, or Disable to disconnect it. Chrome's protected pages,
96
+ including browser settings and extension management, cannot be controlled.
97
+
98
+ The extension is currently a development download and is not yet listed in the
99
+ Chrome Web Store. Its version and distribution are independent of this npm
100
+ package. Ubuntu with Chrome is the initial validation target.
101
+
102
+ ## Structured questions
103
+
104
+
105
+ The agent can pause its current turn to ask up to four related questions in one
106
+ form. Use **↑ / ↓**, the displayed option number, or **Enter** to choose, and
107
+ **← / →** to move between questions. **Something else / add details** is the
108
+ numbered final option; focus it and type, paste, or press **Enter** to add a
109
+ single-line note; **Enter** commits it and moves on the way any other choice
110
+ does — to the next question, or to **Submit answers** on the last one — without
111
+ submitting the form. On single-select questions it is mutually exclusive with
112
+ the listed choices, and **↑ / ↓** leaves its editor when the note is empty.
113
+ Multi-select questions toggle choices. The last question has an explicit
114
+ **Submit answers** row. **Esc** closes an open note first and otherwise cancels
115
+ the form; **Ctrl+C** cancels the whole turn.
116
+
117
+ Only the option you are on shows its description, so the whole form — every
118
+ option, the note row, **Submit answers**, and the key hints — stays on screen
119
+ without scrolling. Move between options to read the others.
120
+
121
+ Answers return to the same turn, and completed question/answer records remain
122
+ readable when the session is resumed. Default, Bypass all permissions, and Plan
123
+ modes all support the form; Bypass all permissions does not choose answers for
124
+ you.
125
+
37
126
  ## Visible to-do list
38
127
 
39
128
  For larger multi-step tasks, the agent keeps a compact to-do list on screen so
package/dist/bin/ai.js CHANGED
@@ -1,68 +1,52 @@
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';
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 { ServerApiError, authenticationErrorMessage, isAuthenticationError, isTransientNetworkError, } from '../src/api/http.js';
8
+ import { loadStartupHandshake } from '../src/api/startup-handshake.js';
9
+ import { TURN_RECOVERY_DELAY_MS, TURN_RECOVERY_RETRIES, } from '../src/api/turn-recovery.js';
8
10
  import { formatCliHelpText } from '../src/help-text.js';
9
- import { createIndex } from '../src/project-index.js';
10
- import { createSession } from '../src/session.js';
11
- import { applySessionSnapshot, listSessionMetadata, loadSessionSnapshot, saveSessionState, } from '../src/session-store.js';
11
+ import { createSession, disposeSession } from '../src/session.js';
12
+ import { applySessionSnapshot, listSessionMetadata, loadSessionSnapshot, saveSessionState, sessionHasUserMessage, } from '../src/session-store.js';
12
13
  import { runClientInteractive } from '../src/ui/repl.js';
14
+ import { startBrowserDiscoveryInBackground } from '../src/browser/session-bridge.js';
13
15
  import { appendPromptToFile } from '../src/ui/prompt-history-store.js';
14
16
  import { formatSessionExitNotice } from '../src/session-exit.js';
15
17
  import { formatUsageText } from '../src/usage.js';
16
- import { formatVersionLine } from '../src/version.js';
18
+ import { getCliVersion, formatVersionLine } from '../src/version.js';
19
+ import { offerUpdateAtStartup } from '../src/update-check.js';
17
20
  import { parseArgs } from '../src/cli-args.js';
18
- const DEFAULT_SERVER_URL = 'https://thegit.ai';
19
21
  const { auth, chat, models, sessions } = ServerApi;
20
22
  function printUsage() {
21
23
  console.log(formatCliHelpText({ color: process.stdout.isTTY === true }));
22
24
  }
23
- async function promptText(question, fallback = null) {
24
- const rl = readline.createInterface({ input, output });
25
- try {
26
- const suffix = fallback ? ` (${fallback})` : '';
27
- const answer = await rl.question(`${question}${suffix}: `);
28
- return answer.trim() || fallback || '';
29
- }
30
- finally {
31
- rl.close();
25
+ function unreachableReason(error) {
26
+ const err = error;
27
+ const code = err?.code ?? err?.cause?.code;
28
+ if (err?.name === 'TimeoutError' || err?.name === 'AbortError') {
29
+ return 'network timeout';
32
30
  }
31
+ if (code === 'ENOTFOUND' || code === 'EAI_AGAIN')
32
+ return 'DNS lookup failed';
33
+ if (code === 'ECONNREFUSED')
34
+ return 'connection refused';
35
+ if (code === 'ECONNRESET' || code === 'EPIPE')
36
+ return 'connection reset';
37
+ return err?.message ? String(err.message) : 'network error';
33
38
  }
34
39
  function appendPromptHistory(prompt, env = process.env) {
35
40
  appendPromptToFile(prompt, env);
36
41
  }
37
- async function runAuthCommand(command, args) {
38
- if (command === 'login') {
39
- const serverUrl = DEFAULT_SERVER_URL;
40
- const noBrowser = args.includes('--no-browser');
41
- console.log(chalk.dim(noBrowser
42
- ? 'Sign in on the website, then paste the authorization code here.'
43
- : 'Opening your browser to sign in…'));
44
- const result = await loginViaBrowser({
45
- serverUrl,
46
- noBrowser,
47
- onUrl: (url) => {
48
- console.log(chalk.dim(noBrowser ? 'Open this URL to sign in:' : 'If your browser did not open, visit:'));
49
- console.log(` ${url}`);
50
- },
51
- onWaiting: () => console.log(chalk.dim('Waiting for you to finish signing in…')),
52
- promptCode: noBrowser
53
- ? () => promptText('Paste the authorization code')
54
- : undefined,
55
- deviceName: process.env.THEGITAI_DEVICE_NAME?.trim() || undefined,
56
- });
57
- auth.writeCliAuthConfig(result);
58
- console.log(chalk.green(`✓ Logged in as ${result.customer.email}`));
59
- console.log(chalk.dim(`Server: ${result.serverUrl}`));
60
- console.log(chalk.dim('You can close the browser tab. Run `ai` in a repo to start.'));
61
- return;
62
- }
42
+ async function runAuthCommand(command) {
63
43
  const config = auth.readCliAuthConfig();
64
44
  if (!config) {
65
- throw new Error('Not logged in. Run `ai login` first.');
45
+ if (command === 'logout') {
46
+ console.log(chalk.green('Already signed out.'));
47
+ return;
48
+ }
49
+ throw new Error('Not signed in. Run `ai` to sign in.');
66
50
  }
67
51
  if (command === 'whoami') {
68
52
  const customer = await auth.fetchWhoami({ config });
@@ -70,19 +54,25 @@ async function runAuthCommand(command, args) {
70
54
  return;
71
55
  }
72
56
  if (command === 'logout') {
73
- await auth.logoutFromServer({ config });
57
+ try {
58
+ await auth.logoutFromServer({ config });
59
+ }
60
+ catch {
61
+ }
74
62
  auth.clearCliAuthConfig();
75
63
  console.log(chalk.green('Logged out.'));
76
64
  return;
77
65
  }
78
66
  throw new Error(`Unknown auth command: ${command}`);
79
67
  }
80
- function requireCliAuthConfig() {
68
+ async function ensureCliAuthConfig() {
81
69
  const config = auth.readCliAuthConfig();
82
- if (!config) {
83
- throw new Error('Not logged in. Run `ai login` first.');
70
+ if (config)
71
+ return config;
72
+ if (process.stdin.isTTY !== true || process.stdout.isTTY !== true) {
73
+ throw new Error('Not signed in. Run `ai login` on a terminal to sign in.');
84
74
  }
85
- return config;
75
+ return await runSignIn();
86
76
  }
87
77
  function formatSessionName(name) {
88
78
  return name ? `"${name}"` : '(unnamed)';
@@ -116,6 +106,8 @@ function modelLabel(serverModels, modelId) {
116
106
  `Model ${modelId}`);
117
107
  }
118
108
  async function saveSessionBoth({ session, serverSessionClient, }) {
109
+ if (!sessionHasUserMessage(session))
110
+ return;
119
111
  saveSessionState(session);
120
112
  try {
121
113
  await serverSessionClient.save(session);
@@ -138,7 +130,7 @@ function requireInteractiveTerminal() {
138
130
  return true;
139
131
  }
140
132
  export async function main() {
141
- const { autoYes, help, version, usage, command, commandArgs, session: sessionIdentifier, listSessions, unknownOption, prompt, } = parseArgs(process.argv);
133
+ const { autoYes, help, version, usage, command, session: sessionIdentifier, listSessions, unknownOption, prompt, } = parseArgs(process.argv);
142
134
  if (version) {
143
135
  console.log(formatVersionLine());
144
136
  return;
@@ -153,18 +145,18 @@ export async function main() {
153
145
  process.exitCode = 2;
154
146
  return;
155
147
  }
156
- if (command) {
157
- await runAuthCommand(command, commandArgs);
148
+ if (command && command !== 'login') {
149
+ await runAuthCommand(command);
158
150
  return;
159
151
  }
160
152
  if (usage) {
161
- const authConfig = requireCliAuthConfig();
153
+ const authConfig = await ensureCliAuthConfig();
162
154
  console.log(formatUsageText(await auth.fetchWhoamiResponse({ config: authConfig })));
163
155
  return;
164
156
  }
165
157
  const rootDir = process.cwd();
166
158
  if (listSessions) {
167
- const activeServerUrl = auth.readCliAuthConfig()?.serverUrl ?? DEFAULT_SERVER_URL;
159
+ const activeServerUrl = auth.readCliAuthConfig()?.serverUrl ?? DEFAULT_THEGITAI_HOST;
168
160
  printSessionList(rootDir, listSessionMetadata(rootDir), models.selectCacheForServer(models.readCachedServerModels(), activeServerUrl));
169
161
  return;
170
162
  }
@@ -179,48 +171,77 @@ export async function main() {
179
171
  if (!requireInteractiveTerminal()) {
180
172
  return;
181
173
  }
182
- const authConfig = requireCliAuthConfig();
174
+ const authConfig = await ensureCliAuthConfig();
183
175
  const serverSessionClient = sessions.createServerSessionClient({ config: authConfig });
184
176
  const cachedModels = models.selectCacheForServer(models.readCachedServerModels(), authConfig.serverUrl);
177
+ const { models: modelsOutcome, whoami: whoamiOutcome } = await loadStartupHandshake({
178
+ config: authConfig,
179
+ canStartOffline: Boolean(cachedModels?.models.length),
180
+ onStatus: (status, lastFailure) => {
181
+ if (status.phase === 'waiting') {
182
+ const reason = lastFailure instanceof ServerApiError
183
+ ? `HTTP ${lastFailure.status}`
184
+ : unreachableReason(lastFailure);
185
+ console.error(chalk.yellow(`⚠ Couldn't reach TheGitAI (${reason}) · retrying in ${TURN_RECOVERY_DELAY_MS / 1000}s · retry ${status.retry}/${TURN_RECOVERY_RETRIES}`));
186
+ }
187
+ else if (status.phase === 'reconnecting') {
188
+ console.error(chalk.dim(` Reconnecting · retry ${status.retry}/${TURN_RECOVERY_RETRIES}`));
189
+ }
190
+ else if (status.phase === 'restored') {
191
+ console.error(chalk.green('✓ Reconnected to TheGitAI'));
192
+ }
193
+ },
194
+ });
185
195
  let offlineNotice = null;
186
196
  let serverModels;
187
- try {
188
- serverModels = await models.fetchServerModels({ config: authConfig });
197
+ if (modelsOutcome.status === 'fulfilled') {
198
+ serverModels = modelsOutcome.value;
189
199
  }
190
- catch (error) {
191
- if (isTransientNetworkError(error) && cachedModels?.models.length) {
192
- serverModels = { models: cachedModels.models };
193
- offlineNotice = error?.message ? String(error.message) : 'network error';
194
- }
195
- else {
196
- throw error;
197
- }
200
+ else if (isTransientNetworkError(modelsOutcome.reason) &&
201
+ cachedModels?.models.length) {
202
+ serverModels = { models: cachedModels.models };
203
+ offlineNotice = unreachableReason(modelsOutcome.reason);
204
+ }
205
+ else if (isTransientNetworkError(modelsOutcome.reason)) {
206
+ throw new Error(`Couldn't reach TheGitAI to load your models (${unreachableReason(modelsOutcome.reason)}).\nCheck your internet connection and run \`ai\` again.`);
207
+ }
208
+ else {
209
+ throw modelsOutcome.reason;
198
210
  }
199
211
  let whoami;
200
- try {
201
- whoami = await auth.fetchWhoamiResponse({ config: authConfig });
212
+ if (whoamiOutcome.status === 'fulfilled') {
213
+ whoami = whoamiOutcome.value;
202
214
  }
203
- catch (error) {
204
- if (isTransientNetworkError(error)) {
205
- whoami = {
206
- customer: {
207
- id: '',
208
- uuid: '',
209
- email: authConfig.email,
210
- customer_type: authConfig.customerType ?? 'USER',
211
- scopes: [],
212
- },
213
- debugUi: { showSessionId: false },
214
- };
215
- offlineNotice ??= error?.message ? String(error.message) : 'network error';
216
- }
217
- else {
218
- throw error;
219
- }
215
+ else if (isTransientNetworkError(whoamiOutcome.reason)) {
216
+ whoami = {
217
+ customer: {
218
+ id: '',
219
+ uuid: '',
220
+ email: authConfig.email,
221
+ customer_type: authConfig.customerType ?? 'USER',
222
+ scopes: [],
223
+ },
224
+ debugUi: { showSessionId: false },
225
+ };
226
+ offlineNotice ??= unreachableReason(whoamiOutcome.reason);
227
+ }
228
+ else {
229
+ throw whoamiOutcome.reason;
220
230
  }
221
231
  if (offlineNotice) {
222
232
  console.error(chalk.yellow(`⚠ Couldn't reach TheGitAI (${offlineNotice}). Starting with cached settings — it will reconnect on your next message.`));
223
233
  }
234
+ const updateOutcome = await offerUpdateAtStartup({
235
+ currentVersion: getCliVersion(),
236
+ latestVersion: whoami.release?.latestVersion ?? null,
237
+ });
238
+ if (updateOutcome === 'updated') {
239
+ return;
240
+ }
241
+ if (updateOutcome === 'cancelled') {
242
+ process.exitCode = 130;
243
+ return;
244
+ }
224
245
  const selectedModelId = models.selectServerModel({
225
246
  requestedModelId: sourceSnapshot?.modelId ?? null,
226
247
  cached: cachedModels,
@@ -239,35 +260,44 @@ export async function main() {
239
260
  if (sourceSnapshot) {
240
261
  applySessionSnapshot(session, sourceSnapshot);
241
262
  }
242
- const projectIndex = createIndex({
243
- rootDir,
244
- onStatus: (message) => {
245
- if (message.trim()) {
246
- console.log(chalk.dim(` ${message}`));
247
- }
248
- },
249
- onContextLog: (message) => {
250
- if (message.trim()) {
251
- console.log(chalk.cyan(` ${message}`));
252
- }
253
- },
254
- });
263
+ startBrowserDiscoveryInBackground({ cwd: rootDir, env: session.env });
255
264
  const initialPrompt = prompt || undefined;
256
- await runClientInteractive({
257
- appendPromptHistory: (value) => appendPromptHistory(value, session.env),
258
- authConfig,
259
- debugUi: whoami.debugUi,
260
- projectIndex,
261
- serverModels,
262
- serverSessionClient,
263
- session,
264
- initialPrompt,
265
- usageText: async () => formatUsageText(await auth.fetchWhoamiResponse({ config: authConfig })),
266
- });
267
- await saveSessionBoth({ session, serverSessionClient });
268
- printSessionExit(session);
265
+ try {
266
+ const outcome = await runClientInteractive({
267
+ appendPromptHistory: (value) => appendPromptHistory(value, session.env),
268
+ authConfig,
269
+ debugUi: whoami.debugUi,
270
+ serverModels,
271
+ serverSessionClient,
272
+ session,
273
+ initialPrompt,
274
+ usageText: async () => formatUsageText(await auth.fetchWhoamiResponse({ config: authConfig })),
275
+ });
276
+ if (outcome.signedOut) {
277
+ if (sessionHasUserMessage(session)) {
278
+ saveSessionState(session);
279
+ }
280
+ console.log(chalk.green('\n✓ Logged out.\n'));
281
+ return;
282
+ }
283
+ await saveSessionBoth({ session, serverSessionClient });
284
+ printSessionExit(session);
285
+ }
286
+ finally {
287
+ await disposeSession(session);
288
+ }
269
289
  }
270
290
  main().catch((error) => {
271
- console.error(chalk.red(`\n✖ Error: ${error.message}\n`));
291
+ if (isSignInCancelled(error)) {
292
+ console.error(chalk.dim('\nSign-in cancelled. Nothing was saved.\n'));
293
+ process.exit(130);
294
+ }
295
+ if (isAuthenticationError(error)) {
296
+ auth.clearCliAuthConfig();
297
+ console.error(chalk.red(`\n✖ Error: ${authenticationErrorMessage(error)}\n`));
298
+ }
299
+ else {
300
+ console.error(chalk.red(`\n✖ Error: ${error.message}\n`));
301
+ }
272
302
  process.exit(1);
273
303
  });