@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.
- package/README.md +93 -4
- package/dist/bin/ai.js +142 -112
- package/dist/bin/browser-host.js +265 -0
- package/dist/src/agent-mode.js +32 -7
- package/dist/src/api/auth.js +10 -4
- package/dist/src/api/browser-login.js +149 -37
- package/dist/src/api/chat.js +680 -166
- package/dist/src/api/contracts.js +55 -1
- package/dist/src/api/default-host.js +1 -0
- package/dist/src/api/http.js +151 -7
- package/dist/src/api/models.js +29 -10
- package/dist/src/api/startup-handshake.js +36 -0
- package/dist/src/api/turn-recovery.js +189 -0
- package/dist/src/background-jobs.js +4 -2
- package/dist/src/browser/bridge.js +262 -0
- package/dist/src/browser/framing.js +42 -0
- package/dist/src/browser/native-host.js +219 -0
- package/dist/src/browser/protocol.js +46 -0
- package/dist/src/browser/session-bridge.js +130 -0
- package/dist/src/cli-args.js +19 -5
- package/dist/src/client-environment.js +2 -0
- package/dist/src/core/clipboard.js +7 -13
- package/dist/src/core/image-limits.js +56 -0
- package/dist/src/core/image-path-extractor.js +70 -3
- package/dist/src/core/session-image-store.js +246 -0
- package/dist/src/edit-journal.js +33 -2
- package/dist/src/executor.js +25 -3
- package/dist/src/help-text.js +112 -21
- package/dist/src/patcher.js +43 -4
- package/dist/src/permissions.js +288 -0
- package/dist/src/preferences.js +39 -0
- package/dist/src/quota-notice.js +35 -0
- package/dist/src/session-safety.js +0 -12
- package/dist/src/session-store.js +121 -20
- package/dist/src/session.js +19 -4
- package/dist/src/signin.js +58 -0
- package/dist/src/tool-executor.js +13 -46
- package/dist/src/tools/browser.js +641 -0
- package/dist/src/tools/delete-file.js +20 -4
- package/dist/src/tools/index.js +32 -10
- package/dist/src/tools/patch-file.js +12 -26
- package/dist/src/tools/read-file.js +5 -0
- package/dist/src/tools/read-image-file.js +85 -0
- package/dist/src/tools/replace-document-text.js +28 -18
- package/dist/src/tools/restore-checkpoint.js +0 -1
- package/dist/src/tools/run-command.js +14 -71
- package/dist/src/tools/run-node-script.js +12 -81
- package/dist/src/tools/save-generated-image.js +120 -0
- package/dist/src/tools/str-replace.js +12 -26
- package/dist/src/tools/undo-edit.js +1 -6
- package/dist/src/tools/write-file.js +67 -11
- package/dist/src/ui/prompt-history-store.js +1 -1
- package/dist/src/ui/repl.js +949 -279
- package/dist/src/ui/slash-commands.js +121 -0
- package/dist/src/ui/tui/attention.js +25 -0
- package/dist/src/ui/tui/bridge.js +38 -5
- package/dist/src/ui/tui/build-frame.js +686 -223
- package/dist/src/ui/tui/composer-layout.js +76 -0
- package/dist/src/ui/tui/desktop-notification.js +118 -0
- package/dist/src/ui/tui/markdown-render.js +11 -2
- package/dist/src/ui/tui/shell-input.js +256 -73
- package/dist/src/ui/tui/shortcuts.js +61 -0
- package/dist/src/ui/tui/terminal-theme.js +28 -0
- package/dist/src/ui/tui/user-input.js +593 -0
- package/dist/src/update-check.js +205 -0
- package/dist/src/usage.js +5 -2
- package/dist/src/utils.js +13 -0
- package/dist/src/version.js +52 -2
- package/package.json +22 -9
- package/dist/src/project-index.js +0 -233
- package/dist/src/tools/code-intel.js +0 -472
- package/dist/src/tools/find-symbol.js +0 -70
- package/dist/src/tools/hover-symbol.js +0 -95
- package/dist/src/tools/list-symbols.js +0 -55
- package/dist/src/tools/search-code.js +0 -37
- 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
|
|
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
|
-
|
|
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
|
|
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
|
|
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 {
|
|
7
|
-
import {
|
|
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 {
|
|
10
|
-
import {
|
|
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
|
-
|
|
24
|
-
const
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
68
|
+
async function ensureCliAuthConfig() {
|
|
81
69
|
const config = auth.readCliAuthConfig();
|
|
82
|
-
if (
|
|
83
|
-
|
|
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
|
|
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,
|
|
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
|
|
148
|
+
if (command && command !== 'login') {
|
|
149
|
+
await runAuthCommand(command);
|
|
158
150
|
return;
|
|
159
151
|
}
|
|
160
152
|
if (usage) {
|
|
161
|
-
const authConfig =
|
|
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 ??
|
|
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 =
|
|
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
|
-
|
|
188
|
-
serverModels =
|
|
197
|
+
if (modelsOutcome.status === 'fulfilled') {
|
|
198
|
+
serverModels = modelsOutcome.value;
|
|
189
199
|
}
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
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
|
-
|
|
201
|
-
whoami =
|
|
212
|
+
if (whoamiOutcome.status === 'fulfilled') {
|
|
213
|
+
whoami = whoamiOutcome.value;
|
|
202
214
|
}
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
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
|
-
|
|
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
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
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
|
-
|
|
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
|
});
|