@thegitai/cli 1.0.0-preview.2 → 1.0.0-preview.21
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 +32 -4
- package/dist/bin/ai.js +57 -291
- package/dist/src/agent-mode.js +1 -1
- package/dist/src/api/auth.js +2 -2
- package/dist/src/api/browser-login.js +72 -3
- package/dist/src/api/chat.js +236 -33
- package/dist/src/api/contracts.js +55 -1
- package/dist/src/api/http.js +16 -3
- package/dist/src/api/models.js +9 -4
- 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 +1 -1
- package/dist/src/help-text.js +51 -11
- package/dist/src/permissions.js +243 -0
- package/dist/src/project-index.js +13 -1
- package/dist/src/session-store.js +119 -20
- package/dist/src/session.js +14 -3
- package/dist/src/tool-executor.js +2 -2
- package/dist/src/tools/delete-file.js +14 -0
- package/dist/src/tools/index.js +2 -0
- package/dist/src/tools/patch-file.js +12 -16
- package/dist/src/tools/read-image-file.js +85 -0
- package/dist/src/tools/replace-document-text.js +28 -18
- package/dist/src/tools/run-command.js +13 -27
- package/dist/src/tools/run-node-script.js +11 -26
- package/dist/src/tools/str-replace.js +12 -16
- package/dist/src/tools/write-file.js +66 -0
- 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 +579 -154
- package/dist/src/ui/tui/bridge.js +10 -0
- package/dist/src/ui/tui/build-frame.js +535 -159
- 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 +18 -6
- package/dist/src/markdown-renderer.js +0 -112
package/README.md
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
|
-
# TheGitAI
|
|
1
|
+
# TheGitAI — AI coding agent for your terminal
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
3
|
+
TheGitAI is an AI coding agent for your terminal. It indexes your repository,
|
|
4
|
+
writes and edits files, runs commands, and builds features with you.
|
|
5
|
+
|
|
6
|
+
Talk to your repo in plain English. TheGitAI can keep long-running processes
|
|
7
|
+
going while you work, all with your approval.
|
|
6
8
|
|
|
7
9
|
## Install
|
|
8
10
|
|
|
@@ -26,6 +28,32 @@ ai --version print the version and exit
|
|
|
26
28
|
|
|
27
29
|
Run `ai --help` for sessions, modes, keys, and chat commands.
|
|
28
30
|
|
|
31
|
+
Coding sessions require an interactive terminal on stdin and stdout. Piped
|
|
32
|
+
one-shot prompts are not supported. Saved history is local to the repo and can
|
|
33
|
+
always be viewed or resumed from that computer. Continuing it through the
|
|
34
|
+
service requires the TheGitAI account used for that session. Local sessions can
|
|
35
|
+
also be listed while signed out or offline.
|
|
36
|
+
|
|
37
|
+
CLI login tokens use a rolling 24-hour inactivity timeout. If one expires during
|
|
38
|
+
any server request, the CLI saves the local session, removes the expired
|
|
39
|
+
credential, and asks you to run `ai login` before resuming.
|
|
40
|
+
|
|
41
|
+
## Structured questions
|
|
42
|
+
|
|
43
|
+
The agent can pause its current turn to ask up to four related questions in one
|
|
44
|
+
form. Use **↑ / ↓**, the displayed option number, or **Enter** to choose, and
|
|
45
|
+
**← / →** to move between questions. **Something else / add details** is the
|
|
46
|
+
numbered final option; focus it and type, paste, or press **Enter** to add a
|
|
47
|
+
single-line note. On single-select questions it is mutually exclusive with the
|
|
48
|
+
listed choices, and **↑ / ↓** leaves its editor when the note is empty.
|
|
49
|
+
Multi-select questions toggle choices. The last question has an explicit
|
|
50
|
+
**Submit answers** row. **Esc** closes an open note first and otherwise cancels
|
|
51
|
+
the form; **Ctrl+C** cancels the whole turn.
|
|
52
|
+
|
|
53
|
+
Answers return to the same turn, and completed question/answer records remain
|
|
54
|
+
readable when the session is resumed. Default, Auto-Accept, and Plan modes all
|
|
55
|
+
support the form; Auto-Accept does not choose answers for you.
|
|
56
|
+
|
|
29
57
|
## Visible to-do list
|
|
30
58
|
|
|
31
59
|
For larger multi-step tasks, the agent keeps a compact to-do list on screen so
|
package/dist/bin/ai.js
CHANGED
|
@@ -4,21 +4,17 @@ import { stdin as input, stdout as output } from 'node:process';
|
|
|
4
4
|
import readline from 'node:readline/promises';
|
|
5
5
|
import { ServerApi } from '../src/api/index.js';
|
|
6
6
|
import { loginViaBrowser } from '../src/api/browser-login.js';
|
|
7
|
-
import { isTransientNetworkError } from '../src/api/http.js';
|
|
8
|
-
import { formatCliHelpText
|
|
9
|
-
import { renderMarkdownForTerminal } from '../src/markdown-renderer.js';
|
|
7
|
+
import { authenticationErrorMessage, isAuthenticationError, isTransientNetworkError, } from '../src/api/http.js';
|
|
8
|
+
import { formatCliHelpText } from '../src/help-text.js';
|
|
10
9
|
import { createIndex } from '../src/project-index.js';
|
|
11
|
-
import { createSession
|
|
12
|
-
import { applySessionSnapshot, listSessionMetadata, loadSessionSnapshot, saveSessionState, } from '../src/session-store.js';
|
|
13
|
-
import { runClientInteractive
|
|
10
|
+
import { createSession } from '../src/session.js';
|
|
11
|
+
import { applySessionSnapshot, listSessionMetadata, loadSessionSnapshot, saveSessionState, sessionHasUserMessage, } from '../src/session-store.js';
|
|
12
|
+
import { runClientInteractive } from '../src/ui/repl.js';
|
|
14
13
|
import { appendPromptToFile } from '../src/ui/prompt-history-store.js';
|
|
15
14
|
import { formatSessionExitNotice } from '../src/session-exit.js';
|
|
16
15
|
import { formatUsageText } from '../src/usage.js';
|
|
17
16
|
import { formatVersionLine } from '../src/version.js';
|
|
18
17
|
import { parseArgs } from '../src/cli-args.js';
|
|
19
|
-
import { getJobBufferedOutput, killBackgroundJob, killAllBackgroundJobs, listBackgroundJobs, setBackgroundJobSession, } from '../src/background-jobs.js';
|
|
20
|
-
import { collectBackgroundJobUiKillMutations, collectBackgroundJobUiOutputMutations, } from '../src/tool-executor.js';
|
|
21
|
-
import { setScratchSession } from '../src/scratch-dir.js';
|
|
22
18
|
const DEFAULT_SERVER_URL = 'https://thegit.ai';
|
|
23
19
|
const { auth, chat, models, sessions } = ServerApi;
|
|
24
20
|
function printUsage() {
|
|
@@ -112,54 +108,16 @@ function printSessionList(rootDir, sessions, serverModels) {
|
|
|
112
108
|
}
|
|
113
109
|
}
|
|
114
110
|
}
|
|
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
111
|
function printSessionExit(session) {
|
|
126
112
|
console.log(chalk.dim(`\n${formatSessionExitNotice(session.sessionId)}\n`));
|
|
127
113
|
}
|
|
128
114
|
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
|
-
};
|
|
115
|
+
return (serverModels?.models.find((model) => model.id === modelId)?.label ??
|
|
116
|
+
`Model ${modelId}`);
|
|
161
117
|
}
|
|
162
118
|
async function saveSessionBoth({ session, serverSessionClient, }) {
|
|
119
|
+
if (!sessionHasUserMessage(session))
|
|
120
|
+
return;
|
|
163
121
|
saveSessionState(session);
|
|
164
122
|
try {
|
|
165
123
|
await serverSessionClient.save(session);
|
|
@@ -168,197 +126,18 @@ async function saveSessionBoth({ session, serverSessionClient, }) {
|
|
|
168
126
|
console.error(chalk.yellow(`Warning: session save failed: ${error.message}`));
|
|
169
127
|
}
|
|
170
128
|
}
|
|
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
|
-
}
|
|
129
|
+
function requireInteractiveTerminal() {
|
|
130
|
+
if (process.stdin.isTTY !== true) {
|
|
131
|
+
console.error('Error: stdin is not a terminal');
|
|
132
|
+
process.exitCode = 1;
|
|
133
|
+
return false;
|
|
134
|
+
}
|
|
135
|
+
if (process.stdout.isTTY !== true) {
|
|
136
|
+
console.error('Error: stdout is not a terminal');
|
|
137
|
+
process.exitCode = 1;
|
|
138
|
+
return false;
|
|
139
|
+
}
|
|
140
|
+
return true;
|
|
362
141
|
}
|
|
363
142
|
export async function main() {
|
|
364
143
|
const { autoYes, help, version, usage, command, commandArgs, session: sessionIdentifier, listSessions, unknownOption, prompt, } = parseArgs(process.argv);
|
|
@@ -386,6 +165,22 @@ export async function main() {
|
|
|
386
165
|
return;
|
|
387
166
|
}
|
|
388
167
|
const rootDir = process.cwd();
|
|
168
|
+
if (listSessions) {
|
|
169
|
+
const activeServerUrl = auth.readCliAuthConfig()?.serverUrl ?? DEFAULT_SERVER_URL;
|
|
170
|
+
printSessionList(rootDir, listSessionMetadata(rootDir), models.selectCacheForServer(models.readCachedServerModels(), activeServerUrl));
|
|
171
|
+
return;
|
|
172
|
+
}
|
|
173
|
+
const sourceSnapshot = sessionIdentifier
|
|
174
|
+
? loadSessionSnapshot(rootDir, sessionIdentifier)
|
|
175
|
+
: null;
|
|
176
|
+
if (sessionIdentifier && !sourceSnapshot) {
|
|
177
|
+
console.error(`Error: No saved session named or identified by "${sessionIdentifier}" is available for this repo. Run \`ai --list-sessions\`.`);
|
|
178
|
+
process.exitCode = 1;
|
|
179
|
+
return;
|
|
180
|
+
}
|
|
181
|
+
if (!requireInteractiveTerminal()) {
|
|
182
|
+
return;
|
|
183
|
+
}
|
|
389
184
|
const authConfig = requireCliAuthConfig();
|
|
390
185
|
const serverSessionClient = sessions.createServerSessionClient({ config: authConfig });
|
|
391
186
|
const cachedModels = models.selectCacheForServer(models.readCachedServerModels(), authConfig.serverUrl);
|
|
@@ -428,13 +223,6 @@ export async function main() {
|
|
|
428
223
|
if (offlineNotice) {
|
|
429
224
|
console.error(chalk.yellow(`⚠ Couldn't reach TheGitAI (${offlineNotice}). Starting with cached settings — it will reconnect on your next message.`));
|
|
430
225
|
}
|
|
431
|
-
if (listSessions) {
|
|
432
|
-
printSessionList(rootDir, listSessionMetadata(rootDir), serverModels);
|
|
433
|
-
return;
|
|
434
|
-
}
|
|
435
|
-
const sourceSnapshot = sessionIdentifier
|
|
436
|
-
? loadSessionSnapshot(rootDir, sessionIdentifier)
|
|
437
|
-
: null;
|
|
438
226
|
const selectedModelId = models.selectServerModel({
|
|
439
227
|
requestedModelId: sourceSnapshot?.modelId ?? null,
|
|
440
228
|
cached: cachedModels,
|
|
@@ -450,13 +238,9 @@ export async function main() {
|
|
|
450
238
|
autoYes,
|
|
451
239
|
modelId: selectedModelId,
|
|
452
240
|
});
|
|
453
|
-
session.confirmCommand = makeConfirmCommand(session);
|
|
454
|
-
session.confirmPatch = makeConfirmPatch(session);
|
|
455
241
|
if (sourceSnapshot) {
|
|
456
242
|
applySessionSnapshot(session, sourceSnapshot);
|
|
457
|
-
await saveSessionBoth({ session, serverSessionClient });
|
|
458
243
|
}
|
|
459
|
-
setScratchSession(session.sessionId);
|
|
460
244
|
const projectIndex = createIndex({
|
|
461
245
|
rootDir,
|
|
462
246
|
onStatus: (message) => {
|
|
@@ -471,45 +255,27 @@ export async function main() {
|
|
|
471
255
|
},
|
|
472
256
|
});
|
|
473
257
|
const initialPrompt = prompt || undefined;
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
printSessionExit(session);
|
|
488
|
-
return;
|
|
489
|
-
}
|
|
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 runClientInteractive({
|
|
259
|
+
appendPromptHistory: (value) => appendPromptHistory(value, session.env),
|
|
260
|
+
authConfig,
|
|
261
|
+
debugUi: whoami.debugUi,
|
|
262
|
+
projectIndex,
|
|
263
|
+
serverModels,
|
|
264
|
+
serverSessionClient,
|
|
265
|
+
session,
|
|
266
|
+
initialPrompt,
|
|
267
|
+
usageText: async () => formatUsageText(await auth.fetchWhoamiResponse({ config: authConfig })),
|
|
268
|
+
});
|
|
269
|
+
await saveSessionBoth({ session, serverSessionClient });
|
|
270
|
+
printSessionExit(session);
|
|
511
271
|
}
|
|
512
272
|
main().catch((error) => {
|
|
513
|
-
|
|
273
|
+
if (isAuthenticationError(error)) {
|
|
274
|
+
auth.clearCliAuthConfig();
|
|
275
|
+
console.error(chalk.red(`\n✖ Error: ${authenticationErrorMessage(error)}\n`));
|
|
276
|
+
}
|
|
277
|
+
else {
|
|
278
|
+
console.error(chalk.red(`\n✖ Error: ${error.message}\n`));
|
|
279
|
+
}
|
|
514
280
|
process.exit(1);
|
|
515
281
|
});
|
package/dist/src/agent-mode.js
CHANGED
|
@@ -46,7 +46,7 @@ export function nextAgentMode(mode) {
|
|
|
46
46
|
export function agentModeAllowsTool(mode, toolName) {
|
|
47
47
|
return mode !== 'plan' || PLAN_MODE_TOOL_NAMES.has(toolName);
|
|
48
48
|
}
|
|
49
|
-
function getUnquotedShellText(command) {
|
|
49
|
+
export function getUnquotedShellText(command) {
|
|
50
50
|
let quote = null;
|
|
51
51
|
let escaped = false;
|
|
52
52
|
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) {
|
|
@@ -78,6 +78,6 @@ export async function logoutFromServer({ config, fetchImpl = globalThis.fetch, }
|
|
|
78
78
|
});
|
|
79
79
|
if (!response.ok && response.status !== 401) {
|
|
80
80
|
const data = (await readJsonResponse(response));
|
|
81
|
-
throw new ServerApiError(failureMessage(data, response.status), response.status, trace.traceId);
|
|
81
|
+
throw new ServerApiError(failureMessage(data, response.status), response.status, trace.traceId, failureCode(data));
|
|
82
82
|
}
|
|
83
83
|
}
|
|
@@ -1,8 +1,10 @@
|
|
|
1
|
+
import { execSync } from 'node:child_process';
|
|
1
2
|
import crypto from 'node:crypto';
|
|
3
|
+
import { readFileSync } from 'node:fs';
|
|
2
4
|
import http from 'node:http';
|
|
3
5
|
import os from 'node:os';
|
|
4
6
|
import { openUrl } from '../core/open-url.js';
|
|
5
|
-
import { ServerApiError, createTraceContext, failureMessage, normalizeServerUrl, readJsonResponse, } from './http.js';
|
|
7
|
+
import { ServerApiError, createTraceContext, failureCode, failureMessage, normalizeServerUrl, readJsonResponse, } from './http.js';
|
|
6
8
|
const DEFAULT_WEBSITE_URL = 'https://thegit.ai';
|
|
7
9
|
const DEFAULT_SERVER_URL = 'https://thegit.ai';
|
|
8
10
|
const DEFAULT_TIMEOUT_MS = 5 * 60 * 1000;
|
|
@@ -21,6 +23,73 @@ function defaultDeviceName() {
|
|
|
21
23
|
return os.hostname();
|
|
22
24
|
}
|
|
23
25
|
}
|
|
26
|
+
function readLinuxOsPrettyName() {
|
|
27
|
+
try {
|
|
28
|
+
const content = readFileSync('/etc/os-release', 'utf8');
|
|
29
|
+
return content.match(/^PRETTY_NAME="?([^"\n]*)"?$/m)?.[1]?.trim() ?? '';
|
|
30
|
+
}
|
|
31
|
+
catch {
|
|
32
|
+
return '';
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
function macArchLabel() {
|
|
36
|
+
try {
|
|
37
|
+
if (process.arch === 'arm64')
|
|
38
|
+
return 'Apple Silicon';
|
|
39
|
+
if (os.cpus().some((cpu) => cpu.model.includes('Apple'))) {
|
|
40
|
+
return 'Apple Silicon';
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
catch {
|
|
44
|
+
}
|
|
45
|
+
return 'Intel';
|
|
46
|
+
}
|
|
47
|
+
export function describeOperatingSystem() {
|
|
48
|
+
try {
|
|
49
|
+
if (process.platform === 'linux') {
|
|
50
|
+
const base = readLinuxOsPrettyName() || `Linux ${os.release()}`;
|
|
51
|
+
return process.arch === 'arm64' ? `${base}, ARM64` : base;
|
|
52
|
+
}
|
|
53
|
+
if (process.platform === 'darwin') {
|
|
54
|
+
let version = '';
|
|
55
|
+
try {
|
|
56
|
+
version = execSync('sw_vers -productVersion', {
|
|
57
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
58
|
+
})
|
|
59
|
+
.toString()
|
|
60
|
+
.trim();
|
|
61
|
+
}
|
|
62
|
+
catch {
|
|
63
|
+
}
|
|
64
|
+
return `${version ? `macOS ${version}` : 'macOS'}, ${macArchLabel()}`;
|
|
65
|
+
}
|
|
66
|
+
if (process.platform === 'win32') {
|
|
67
|
+
let label = '';
|
|
68
|
+
try {
|
|
69
|
+
label = os.version();
|
|
70
|
+
}
|
|
71
|
+
catch {
|
|
72
|
+
}
|
|
73
|
+
const build = Number(os.release().split('.')[2] ?? '0');
|
|
74
|
+
if (build >= 22000)
|
|
75
|
+
label = label.replace(/Windows 10/i, 'Windows 11');
|
|
76
|
+
const base = label || `Windows ${os.release()}`;
|
|
77
|
+
return process.arch === 'arm64' ? `${base}, ARM64` : base;
|
|
78
|
+
}
|
|
79
|
+
return `${process.platform} ${os.release()}`;
|
|
80
|
+
}
|
|
81
|
+
catch {
|
|
82
|
+
return process.platform;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
export function withOperatingSystemInfo(name) {
|
|
86
|
+
const trimmed = name.trim();
|
|
87
|
+
const osLabel = describeOperatingSystem();
|
|
88
|
+
const combined = osLabel && !trimmed.includes(osLabel)
|
|
89
|
+
? `${trimmed} (${osLabel})`
|
|
90
|
+
: trimmed;
|
|
91
|
+
return combined.slice(0, 180);
|
|
92
|
+
}
|
|
24
93
|
export function generatePkce() {
|
|
25
94
|
const verifier = crypto.randomBytes(32).toString('base64url');
|
|
26
95
|
const challenge = crypto
|
|
@@ -56,7 +125,7 @@ async function exchangeCodeForToken({ serverUrl, code, codeVerifier, fetchImpl,
|
|
|
56
125
|
});
|
|
57
126
|
const data = (await readJsonResponse(response));
|
|
58
127
|
if (!response.ok) {
|
|
59
|
-
throw new ServerApiError(failureMessage(data, response.status), response.status, trace.traceId);
|
|
128
|
+
throw new ServerApiError(failureMessage(data, response.status), response.status, trace.traceId, failureCode(data));
|
|
60
129
|
}
|
|
61
130
|
const token = String(data?.token ?? '').trim();
|
|
62
131
|
const customer = data?.customer;
|
|
@@ -77,7 +146,7 @@ export async function loginViaBrowser(options) {
|
|
|
77
146
|
const fetchImpl = options.fetchImpl ?? globalThis.fetch;
|
|
78
147
|
const openBrowser = options.openBrowser ?? openUrl;
|
|
79
148
|
const onUrl = options.onUrl ?? (() => { });
|
|
80
|
-
const deviceName = options.deviceName ?? defaultDeviceName();
|
|
149
|
+
const deviceName = withOperatingSystemInfo(options.deviceName ?? defaultDeviceName());
|
|
81
150
|
const { verifier, challenge } = generatePkce();
|
|
82
151
|
if (options.noBrowser) {
|
|
83
152
|
const authUrl = buildAuthUrl(websiteUrl, {
|