@thegitai/cli 1.0.0-preview.3 → 1.0.0-preview.30
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 +39 -6
- package/dist/bin/ai.js +142 -383
- package/dist/src/agent-mode.js +1 -6
- package/dist/src/api/auth.js +6 -4
- package/dist/src/api/browser-login.js +152 -37
- package/dist/src/api/chat.js +258 -38
- package/dist/src/api/contracts.js +55 -1
- package/dist/src/api/default-host.js +1 -0
- package/dist/src/api/http.js +69 -7
- package/dist/src/api/models.js +19 -10
- package/dist/src/background-jobs.js +2 -2
- package/dist/src/cli-args.js +19 -5
- 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 +199 -0
- package/dist/src/executor.js +25 -3
- package/dist/src/help-text.js +63 -16
- package/dist/src/permissions.js +243 -0
- package/dist/src/session-safety.js +0 -12
- package/dist/src/session-store.js +121 -20
- package/dist/src/session.js +14 -3
- package/dist/src/signin.js +58 -0
- package/dist/src/tool-executor.js +11 -46
- package/dist/src/tools/delete-file.js +15 -3
- package/dist/src/tools/index.js +13 -10
- package/dist/src/tools/patch-file.js +12 -26
- 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/turn-failure-marker.js +11 -0
- package/dist/src/ui/prompt-history-store.js +1 -1
- package/dist/src/ui/repl.js +610 -164
- package/dist/src/ui/tui/bridge.js +10 -0
- package/dist/src/ui/tui/build-frame.js +452 -115
- package/dist/src/ui/tui/markdown-render.js +81 -73
- package/dist/src/ui/tui/shell-input.js +206 -63
- package/dist/src/ui/tui/terminal-theme.js +28 -0
- package/dist/src/ui/tui/terminal-title.js +3 -0
- package/dist/src/ui/tui/terminal-writes.js +48 -0
- package/dist/src/ui/tui/text.js +158 -4
- package/dist/src/ui/tui/user-input.js +568 -0
- package/dist/src/utils.js +9 -0
- package/package.json +29 -6
- package/dist/src/markdown-renderer.js +0 -112
- package/dist/src/project-index.js +0 -221
- 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/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 {
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
10
|
-
import {
|
|
11
|
-
import { createSession
|
|
12
|
-
import { applySessionSnapshot, listSessionMetadata, loadSessionSnapshot, saveSessionState, } from '../src/session-store.js';
|
|
13
|
-
import { runClientInteractive
|
|
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
|
-
|
|
28
|
-
const
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
64
|
+
async function ensureCliAuthConfig() {
|
|
85
65
|
const config = auth.readCliAuthConfig();
|
|
86
|
-
if (
|
|
87
|
-
|
|
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
|
|
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
|
|
130
|
-
|
|
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
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
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,
|
|
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
|
|
144
|
+
if (command && command !== 'login') {
|
|
145
|
+
await runAuthCommand(command);
|
|
381
146
|
return;
|
|
382
147
|
}
|
|
383
148
|
if (usage) {
|
|
384
|
-
const authConfig =
|
|
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
|
-
|
|
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
|
-
|
|
395
|
-
serverModels =
|
|
185
|
+
if (modelsOutcome.status === 'fulfilled') {
|
|
186
|
+
serverModels = modelsOutcome.value;
|
|
396
187
|
}
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
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
|
-
|
|
408
|
-
whoami =
|
|
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
|
-
|
|
411
|
-
|
|
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
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
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
|
-
|
|
491
|
-
|
|
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
|
-
|
|
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
|
});
|
package/dist/src/agent-mode.js
CHANGED
|
@@ -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 = '';
|
package/dist/src/api/auth.js
CHANGED
|
@@ -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
|
}
|