@thegitai/cli 1.0.0-beta.9 → 1.0.0-preview.10
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 +49 -3
- package/dist/bin/ai.js +83 -197
- package/dist/parsers/NOTICE +18 -0
- package/dist/src/agent-mode.js +5 -0
- package/dist/src/api/auth.js +4 -4
- package/dist/src/api/browser-login.js +72 -19
- package/dist/src/api/chat.js +182 -35
- package/dist/src/api/http.js +65 -4
- package/dist/src/api/models.js +33 -22
- package/dist/src/artifact-policy.js +3 -0
- package/dist/src/background-jobs.js +410 -0
- package/dist/src/cli-args.js +0 -5
- package/dist/src/client-environment.js +2 -0
- package/dist/src/colors.js +50 -0
- package/dist/src/core/clipboard.js +19 -0
- package/dist/src/core/image-path-extractor.js +144 -0
- package/dist/src/executor.js +48 -12
- package/dist/src/help-text.js +30 -13
- package/dist/src/patcher.js +97 -12
- package/dist/src/project-index.js +13 -1
- package/dist/src/project-orientation.js +99 -0
- package/dist/src/scanner.js +50 -12
- package/dist/src/scratch-dir.js +75 -0
- package/dist/src/secret-preview.js +0 -10
- package/dist/src/session-safety.js +0 -19
- package/dist/src/session-store.js +52 -21
- package/dist/src/session.js +8 -0
- package/dist/src/todo-list.js +106 -0
- package/dist/src/tool-executor.js +194 -21
- package/dist/src/tools/delete-file.js +23 -5
- package/dist/src/tools/index.js +6 -0
- package/dist/src/tools/patch-file.js +33 -7
- package/dist/src/tools/path-suggest.js +81 -8
- package/dist/src/tools/read-document.js +2 -2
- package/dist/src/tools/read-file.js +17 -8
- package/dist/src/tools/replace-document-text.js +10 -12
- package/dist/src/tools/restore-checkpoint.js +1 -1
- package/dist/src/tools/run-command.js +109 -24
- package/dist/src/tools/run-node-script.js +27 -5
- package/dist/src/tools/shell-job-kill.js +48 -0
- package/dist/src/tools/shell-job-output.js +51 -0
- package/dist/src/tools/str-replace.js +33 -7
- package/dist/src/tools/undo-edit.js +1 -1
- package/dist/src/tools/update-todos.js +27 -0
- package/dist/src/tools/write-file.js +26 -6
- package/dist/src/tree-sitter-runtime.js +8 -1
- 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 +500 -71
- package/dist/src/ui/tui/bridge.js +3 -4
- package/dist/src/ui/tui/build-frame.js +393 -100
- package/dist/src/ui/tui/markdown-render.js +72 -73
- package/dist/src/ui/tui/shell-input.js +75 -17
- package/dist/src/ui/tui/terminal-title.js +84 -0
- package/dist/src/ui/tui/terminal-writes.js +48 -0
- package/dist/src/ui/tui/text.js +158 -4
- package/dist/src/utils.js +9 -0
- package/dist/src/version.js +0 -6
- package/dist/vendor/web-tree-sitter/LICENSE +21 -0
- package/dist/vendor/web-tree-sitter/NOTICE +13 -0
- package/dist/vendor/web-tree-sitter/web-tree-sitter.cjs +4063 -0
- package/dist/vendor/web-tree-sitter/web-tree-sitter.wasm +0 -0
- package/package.json +27 -16
- package/dist/src/markdown-renderer.js +0 -112
package/README.md
CHANGED
|
@@ -1,7 +1,10 @@
|
|
|
1
|
-
# TheGitAI
|
|
1
|
+
# TheGitAI — AI coding agent for your terminal
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
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.
|
|
5
8
|
|
|
6
9
|
## Install
|
|
7
10
|
|
|
@@ -25,6 +28,49 @@ ai --version print the version and exit
|
|
|
25
28
|
|
|
26
29
|
Run `ai --help` for sessions, modes, keys, and chat commands.
|
|
27
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
|
+
## Visible to-do list
|
|
42
|
+
|
|
43
|
+
For larger multi-step tasks, the agent keeps a compact to-do list on screen so
|
|
44
|
+
you can see what it plans to do, what it is working on right now, and what is
|
|
45
|
+
already done.
|
|
46
|
+
|
|
47
|
+
- While the agent works, the list sits at the bottom of the live **Working**
|
|
48
|
+
area, right above where you type, and updates as steps start and finish —
|
|
49
|
+
one step in progress at a time — so it stays visible even when tool output
|
|
50
|
+
above it runs long.
|
|
51
|
+
- When the turn ends, a final snapshot of the list stays readable in the
|
|
52
|
+
transcript, and the footer shows a small progress chip (e.g. `◐ 4/6 to-dos`)
|
|
53
|
+
while steps remain open.
|
|
54
|
+
- The agent's current reasoning stays visible too, right below the list, in a
|
|
55
|
+
compact one-line form so it doesn't compete with the list for space.
|
|
56
|
+
- The list is managed entirely by the agent; simple one-step requests skip it.
|
|
57
|
+
|
|
58
|
+
## Background jobs
|
|
59
|
+
|
|
60
|
+
Some commands are meant to keep running — a dev server, a file watcher, a local
|
|
61
|
+
API. TheGitAI runs these as **background jobs**, so the agent can start a server,
|
|
62
|
+
work against it live, and stop it when the task is done, all in one session.
|
|
63
|
+
|
|
64
|
+
- Each job's block in the transcript updates on its own with a live tail of its
|
|
65
|
+
latest output, and a compact indicator keeps you aware of what's still running.
|
|
66
|
+
- Type `/jobs` to open a picker: **↑ / ↓** to move, **Enter** to expand a job and
|
|
67
|
+
read its output inline, **k** to stop it, **Esc** to close.
|
|
68
|
+
- `/jobs output <id>` prints a job's full output, and `/jobs kill <id>` stops one
|
|
69
|
+
by id.
|
|
70
|
+
- When you end the session, TheGitAI stops its background jobs for you.
|
|
71
|
+
|
|
72
|
+
Full documentation: <https://thegit.ai/docs>
|
|
73
|
+
|
|
28
74
|
## License
|
|
29
75
|
|
|
30
76
|
Proprietary — see the LICENSE file included in this package. Source is
|
package/dist/bin/ai.js
CHANGED
|
@@ -1,15 +1,15 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import chalk from '
|
|
2
|
+
import chalk from '../src/colors.js';
|
|
3
3
|
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 {
|
|
8
|
-
import {
|
|
7
|
+
import { authenticationErrorMessage, isAuthenticationError, isTransientNetworkError, } from '../src/api/http.js';
|
|
8
|
+
import { formatCliHelpText } from '../src/help-text.js';
|
|
9
9
|
import { createIndex } from '../src/project-index.js';
|
|
10
|
-
import { createSession
|
|
11
|
-
import { applySessionSnapshot, listSessionMetadata, loadSessionSnapshot, saveSessionState, } from '../src/session-store.js';
|
|
12
|
-
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';
|
|
13
13
|
import { appendPromptToFile } from '../src/ui/prompt-history-store.js';
|
|
14
14
|
import { formatSessionExitNotice } from '../src/session-exit.js';
|
|
15
15
|
import { formatUsageText } from '../src/usage.js';
|
|
@@ -36,9 +36,6 @@ function appendPromptHistory(prompt, env = process.env) {
|
|
|
36
36
|
}
|
|
37
37
|
async function runAuthCommand(command, args) {
|
|
38
38
|
if (command === 'login') {
|
|
39
|
-
// The public CLI always authenticates against the official TheGitAI host.
|
|
40
|
-
// There is intentionally no server/website override here — internal dev
|
|
41
|
-
// uses private tooling, not a customer-visible runtime override path.
|
|
42
39
|
const serverUrl = DEFAULT_SERVER_URL;
|
|
43
40
|
const noBrowser = args.includes('--no-browser');
|
|
44
41
|
console.log(chalk.dim(noBrowser
|
|
@@ -111,45 +108,16 @@ function printSessionList(rootDir, sessions, serverModels) {
|
|
|
111
108
|
}
|
|
112
109
|
}
|
|
113
110
|
}
|
|
114
|
-
function printStartupBanner(rootDir, modelLabel, autoYes) {
|
|
115
|
-
console.log(chalk.dim(`Project: ${rootDir}`));
|
|
116
|
-
console.log(chalk.dim(`Model: ${modelLabel}`));
|
|
117
|
-
if (autoYes) {
|
|
118
|
-
console.log(chalk.dim('Auto-confirm: enabled'));
|
|
119
|
-
}
|
|
120
|
-
}
|
|
121
|
-
function printSessionStartup(session) {
|
|
122
|
-
console.log(chalk.dim(`Session: ${session.sessionName ? `${session.sessionName} ` : ''}${session.sessionId}`));
|
|
123
|
-
}
|
|
124
111
|
function printSessionExit(session) {
|
|
125
112
|
console.log(chalk.dim(`\n${formatSessionExitNotice(session.sessionId)}\n`));
|
|
126
113
|
}
|
|
127
114
|
function modelLabel(serverModels, modelId) {
|
|
128
|
-
return (serverModels
|
|
129
|
-
|
|
130
|
-
}
|
|
131
|
-
function makeConfirmCommand(session) {
|
|
132
|
-
return async (command) => {
|
|
133
|
-
console.log(chalk.bold(`\nCommand approval needed:\n${command}\n`));
|
|
134
|
-
const answer = (await promptText('Approve? [y]es/[a]ll/[n]o', 'n')).toLowerCase();
|
|
135
|
-
if (answer === 'a' || answer === 'all') {
|
|
136
|
-
session.autoYes = true;
|
|
137
|
-
return true;
|
|
138
|
-
}
|
|
139
|
-
return answer === 'y' || answer === 'yes';
|
|
140
|
-
};
|
|
141
|
-
}
|
|
142
|
-
function makeConfirmPatch(session) {
|
|
143
|
-
return async (filePath) => {
|
|
144
|
-
const answer = (await promptText(`Apply patch to ${filePath}? [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
|
-
};
|
|
115
|
+
return (serverModels?.models.find((model) => model.id === modelId)?.label ??
|
|
116
|
+
`Model ${modelId}`);
|
|
151
117
|
}
|
|
152
118
|
async function saveSessionBoth({ session, serverSessionClient, }) {
|
|
119
|
+
if (!sessionHasUserMessage(session))
|
|
120
|
+
return;
|
|
153
121
|
saveSessionState(session);
|
|
154
122
|
try {
|
|
155
123
|
await serverSessionClient.save(session);
|
|
@@ -158,133 +126,18 @@ async function saveSessionBoth({ session, serverSessionClient, }) {
|
|
|
158
126
|
console.error(chalk.yellow(`Warning: session save failed: ${error.message}`));
|
|
159
127
|
}
|
|
160
128
|
}
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
console.log(`${marker} ${model.id} ${model.label}`);
|
|
167
|
-
}
|
|
168
|
-
console.log();
|
|
169
|
-
const selected = (await promptText('Model id', '')).trim() || null;
|
|
170
|
-
if (!selected) {
|
|
171
|
-
return { selected: null, serverModels };
|
|
172
|
-
}
|
|
173
|
-
return {
|
|
174
|
-
selected: models.validateServerModel(selected, serverModels),
|
|
175
|
-
serverModels,
|
|
176
|
-
};
|
|
177
|
-
}
|
|
178
|
-
async function promptForResumeSession(rootDir, serverModels) {
|
|
179
|
-
const sessions = listSessionMetadata(rootDir);
|
|
180
|
-
if (!sessions.length) {
|
|
181
|
-
console.log(chalk.dim('No saved sessions for this repo.\n'));
|
|
182
|
-
return null;
|
|
183
|
-
}
|
|
184
|
-
printSessionList(rootDir, sessions, serverModels);
|
|
185
|
-
console.log();
|
|
186
|
-
const identifier = (await promptText('Session id or name', '')).trim();
|
|
187
|
-
if (!identifier) {
|
|
188
|
-
return null;
|
|
189
|
-
}
|
|
190
|
-
return loadSessionSnapshot(rootDir, identifier);
|
|
191
|
-
}
|
|
192
|
-
async function runTurn({ authConfig, projectIndex, serverSessionClient, session, inputText, }) {
|
|
193
|
-
const prompt = String(inputText ?? '').trim();
|
|
194
|
-
if (!prompt)
|
|
195
|
-
return;
|
|
196
|
-
appendPromptHistory(prompt, session.env);
|
|
197
|
-
const result = await chat.sendServerUserMessage({
|
|
198
|
-
config: authConfig,
|
|
199
|
-
projectIndex,
|
|
200
|
-
session,
|
|
201
|
-
input: prompt,
|
|
202
|
-
});
|
|
203
|
-
if (result.text) {
|
|
204
|
-
console.log(`\n${chalk.green('TheGitAI>')}`);
|
|
205
|
-
console.log(renderMarkdownForTerminal(result.text));
|
|
206
|
-
console.log();
|
|
207
|
-
}
|
|
208
|
-
await saveSessionBoth({ session, serverSessionClient });
|
|
209
|
-
}
|
|
210
|
-
async function mainInteractive({ authConfig, projectIndex, serverModels, serverSessionClient, session, usageText, initialPrompt, }) {
|
|
211
|
-
if (initialPrompt) {
|
|
212
|
-
console.log(chalk.dim(`Prompt: "${initialPrompt}"`));
|
|
213
|
-
await runTurn({
|
|
214
|
-
authConfig,
|
|
215
|
-
projectIndex,
|
|
216
|
-
serverSessionClient,
|
|
217
|
-
session,
|
|
218
|
-
inputText: initialPrompt,
|
|
219
|
-
});
|
|
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;
|
|
220
134
|
}
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
continue;
|
|
226
|
-
if (trimmed === '/exit')
|
|
227
|
-
return;
|
|
228
|
-
if (trimmed === '/help') {
|
|
229
|
-
console.log(renderMarkdownForTerminal(formatInteractiveHelpText()));
|
|
230
|
-
continue;
|
|
231
|
-
}
|
|
232
|
-
if (trimmed === '/usage') {
|
|
233
|
-
console.log(await usageText());
|
|
234
|
-
continue;
|
|
235
|
-
}
|
|
236
|
-
if (trimmed === '/clear') {
|
|
237
|
-
clearConversation(session);
|
|
238
|
-
await saveSessionBoth({ session, serverSessionClient });
|
|
239
|
-
console.log(chalk.dim('Conversation cleared.\n'));
|
|
240
|
-
continue;
|
|
241
|
-
}
|
|
242
|
-
if (trimmed === '/resume') {
|
|
243
|
-
const snapshot = await promptForResumeSession(session.rootDir, serverModels);
|
|
244
|
-
if (!snapshot) {
|
|
245
|
-
console.log(chalk.dim('Resume cancelled.\n'));
|
|
246
|
-
continue;
|
|
247
|
-
}
|
|
248
|
-
applySessionSnapshot(session, snapshot);
|
|
249
|
-
await saveSessionBoth({ session, serverSessionClient });
|
|
250
|
-
console.log(chalk.dim(`Resumed session${session.sessionName ? ` "${session.sessionName}"` : ''} (${session.sessionId})\n`));
|
|
251
|
-
continue;
|
|
252
|
-
}
|
|
253
|
-
if (trimmed === '/model' || trimmed.startsWith('/model ')) {
|
|
254
|
-
const inline = trimmed.slice('/model'.length).trim();
|
|
255
|
-
let serverModels;
|
|
256
|
-
let selected = null;
|
|
257
|
-
if (inline) {
|
|
258
|
-
serverModels = await models.fetchServerModels({ config: authConfig });
|
|
259
|
-
selected = models.validateServerModel(inline, serverModels);
|
|
260
|
-
}
|
|
261
|
-
else {
|
|
262
|
-
const response = await promptForModelSelection(session.modelId, authConfig);
|
|
263
|
-
serverModels = response.serverModels;
|
|
264
|
-
selected = response.selected;
|
|
265
|
-
}
|
|
266
|
-
if (!selected) {
|
|
267
|
-
console.log(chalk.dim('Model selection cancelled.\n'));
|
|
268
|
-
continue;
|
|
269
|
-
}
|
|
270
|
-
session.modelId = selected;
|
|
271
|
-
models.updateSelectedModelCache({
|
|
272
|
-
config: authConfig,
|
|
273
|
-
selectedModelId: selected,
|
|
274
|
-
serverModels,
|
|
275
|
-
});
|
|
276
|
-
await saveSessionBoth({ session, serverSessionClient });
|
|
277
|
-
console.log(chalk.dim(`Switched to ${modelLabel(serverModels, selected)}.\n`));
|
|
278
|
-
continue;
|
|
279
|
-
}
|
|
280
|
-
await runTurn({
|
|
281
|
-
authConfig,
|
|
282
|
-
projectIndex,
|
|
283
|
-
serverSessionClient,
|
|
284
|
-
session,
|
|
285
|
-
inputText: trimmed,
|
|
286
|
-
});
|
|
135
|
+
if (process.stdout.isTTY !== true) {
|
|
136
|
+
console.error('Error: stdout is not a terminal');
|
|
137
|
+
process.exitCode = 1;
|
|
138
|
+
return false;
|
|
287
139
|
}
|
|
140
|
+
return true;
|
|
288
141
|
}
|
|
289
142
|
export async function main() {
|
|
290
143
|
const { autoYes, help, version, usage, command, commandArgs, session: sessionIdentifier, listSessions, unknownOption, prompt, } = parseArgs(process.argv);
|
|
@@ -312,18 +165,64 @@ export async function main() {
|
|
|
312
165
|
return;
|
|
313
166
|
}
|
|
314
167
|
const rootDir = process.cwd();
|
|
315
|
-
const authConfig = requireCliAuthConfig();
|
|
316
|
-
const serverSessionClient = sessions.createServerSessionClient({ config: authConfig });
|
|
317
|
-
const cachedModels = models.readCachedServerModels();
|
|
318
|
-
const serverModels = await models.fetchServerModels({ config: authConfig });
|
|
319
|
-
const whoami = await auth.fetchWhoamiResponse({ config: authConfig });
|
|
320
168
|
if (listSessions) {
|
|
321
|
-
|
|
169
|
+
const activeServerUrl = auth.readCliAuthConfig()?.serverUrl ?? DEFAULT_SERVER_URL;
|
|
170
|
+
printSessionList(rootDir, listSessionMetadata(rootDir), models.selectCacheForServer(models.readCachedServerModels(), activeServerUrl));
|
|
322
171
|
return;
|
|
323
172
|
}
|
|
324
173
|
const sourceSnapshot = sessionIdentifier
|
|
325
174
|
? loadSessionSnapshot(rootDir, sessionIdentifier)
|
|
326
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
|
+
}
|
|
184
|
+
const authConfig = requireCliAuthConfig();
|
|
185
|
+
const serverSessionClient = sessions.createServerSessionClient({ config: authConfig });
|
|
186
|
+
const cachedModels = models.selectCacheForServer(models.readCachedServerModels(), authConfig.serverUrl);
|
|
187
|
+
let offlineNotice = null;
|
|
188
|
+
let serverModels;
|
|
189
|
+
try {
|
|
190
|
+
serverModels = await models.fetchServerModels({ config: authConfig });
|
|
191
|
+
}
|
|
192
|
+
catch (error) {
|
|
193
|
+
if (isTransientNetworkError(error) && cachedModels?.models.length) {
|
|
194
|
+
serverModels = { models: cachedModels.models };
|
|
195
|
+
offlineNotice = error?.message ? String(error.message) : 'network error';
|
|
196
|
+
}
|
|
197
|
+
else {
|
|
198
|
+
throw error;
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
let whoami;
|
|
202
|
+
try {
|
|
203
|
+
whoami = await auth.fetchWhoamiResponse({ config: authConfig });
|
|
204
|
+
}
|
|
205
|
+
catch (error) {
|
|
206
|
+
if (isTransientNetworkError(error)) {
|
|
207
|
+
whoami = {
|
|
208
|
+
customer: {
|
|
209
|
+
id: '',
|
|
210
|
+
uuid: '',
|
|
211
|
+
email: authConfig.email,
|
|
212
|
+
customer_type: authConfig.customerType ?? 'USER',
|
|
213
|
+
scopes: [],
|
|
214
|
+
},
|
|
215
|
+
debugUi: { showSessionId: false },
|
|
216
|
+
};
|
|
217
|
+
offlineNotice ??= error?.message ? String(error.message) : 'network error';
|
|
218
|
+
}
|
|
219
|
+
else {
|
|
220
|
+
throw error;
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
if (offlineNotice) {
|
|
224
|
+
console.error(chalk.yellow(`⚠ Couldn't reach TheGitAI (${offlineNotice}). Starting with cached settings — it will reconnect on your next message.`));
|
|
225
|
+
}
|
|
327
226
|
const selectedModelId = models.selectServerModel({
|
|
328
227
|
requestedModelId: sourceSnapshot?.modelId ?? null,
|
|
329
228
|
cached: cachedModels,
|
|
@@ -339,11 +238,8 @@ export async function main() {
|
|
|
339
238
|
autoYes,
|
|
340
239
|
modelId: selectedModelId,
|
|
341
240
|
});
|
|
342
|
-
session.confirmCommand = makeConfirmCommand(session);
|
|
343
|
-
session.confirmPatch = makeConfirmPatch(session);
|
|
344
241
|
if (sourceSnapshot) {
|
|
345
242
|
applySessionSnapshot(session, sourceSnapshot);
|
|
346
|
-
await saveSessionBoth({ session, serverSessionClient });
|
|
347
243
|
}
|
|
348
244
|
const projectIndex = createIndex({
|
|
349
245
|
rootDir,
|
|
@@ -359,37 +255,27 @@ export async function main() {
|
|
|
359
255
|
},
|
|
360
256
|
});
|
|
361
257
|
const initialPrompt = prompt || undefined;
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
appendPromptHistory: (value) => appendPromptHistory(value, session.env),
|
|
365
|
-
authConfig,
|
|
366
|
-
debugUi: whoami.debugUi,
|
|
367
|
-
projectIndex,
|
|
368
|
-
serverModels,
|
|
369
|
-
serverSessionClient,
|
|
370
|
-
session,
|
|
371
|
-
initialPrompt,
|
|
372
|
-
usageText: async () => formatUsageText(await auth.fetchWhoamiResponse({ config: authConfig })),
|
|
373
|
-
});
|
|
374
|
-
await saveSessionBoth({ session, serverSessionClient });
|
|
375
|
-
printSessionExit(session);
|
|
376
|
-
return;
|
|
377
|
-
}
|
|
378
|
-
printStartupBanner(rootDir, modelLabel(serverModels, session.modelId), session.autoYes);
|
|
379
|
-
printSessionStartup(session);
|
|
380
|
-
await mainInteractive({
|
|
258
|
+
await runClientInteractive({
|
|
259
|
+
appendPromptHistory: (value) => appendPromptHistory(value, session.env),
|
|
381
260
|
authConfig,
|
|
261
|
+
debugUi: whoami.debugUi,
|
|
382
262
|
projectIndex,
|
|
383
263
|
serverModels,
|
|
384
264
|
serverSessionClient,
|
|
385
265
|
session,
|
|
386
|
-
usageText: async () => formatUsageText(await auth.fetchWhoamiResponse({ config: authConfig })),
|
|
387
266
|
initialPrompt,
|
|
267
|
+
usageText: async () => formatUsageText(await auth.fetchWhoamiResponse({ config: authConfig })),
|
|
388
268
|
});
|
|
389
269
|
await saveSessionBoth({ session, serverSessionClient });
|
|
390
270
|
printSessionExit(session);
|
|
391
271
|
}
|
|
392
272
|
main().catch((error) => {
|
|
393
|
-
|
|
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
|
+
}
|
|
394
280
|
process.exit(1);
|
|
395
281
|
});
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
Tree-sitter grammar parsers
|
|
2
|
+
===========================
|
|
3
|
+
|
|
4
|
+
The `tree-sitter-*.wasm` files in this directory are precompiled tree-sitter
|
|
5
|
+
grammar parsers, vendored so the published `@thegitai/cli` package ships local
|
|
6
|
+
code intelligence without a runtime dependency. They are used unmodified, only
|
|
7
|
+
as parsing inputs to the vendored web-tree-sitter runtime
|
|
8
|
+
(see ../vendor/web-tree-sitter/NOTICE).
|
|
9
|
+
|
|
10
|
+
Each grammar is the work of its respective tree-sitter grammar project and is
|
|
11
|
+
distributed under that project's license (the tree-sitter grammars are
|
|
12
|
+
MIT-licensed). Grammars included:
|
|
13
|
+
|
|
14
|
+
c, c-sharp, cpp, css, go, html, java, javascript, objc, php, python, ruby,
|
|
15
|
+
rust, tsx, typescript
|
|
16
|
+
|
|
17
|
+
Upstream organization: https://github.com/tree-sitter
|
|
18
|
+
Individual grammars: https://github.com/tree-sitter/tree-sitter-<language>
|
package/dist/src/agent-mode.js
CHANGED
|
@@ -12,6 +12,8 @@ const PLAN_MODE_TOOL_NAMES = new Set([
|
|
|
12
12
|
'read_document',
|
|
13
13
|
'analyze_image',
|
|
14
14
|
'run_command',
|
|
15
|
+
'shell_job_output',
|
|
16
|
+
'update_todos',
|
|
15
17
|
]);
|
|
16
18
|
const PLAN_MODE_RUN_COMMAND_NAMES = new Set([
|
|
17
19
|
'pwd',
|
|
@@ -117,6 +119,9 @@ export function buildAgentModeToolBlockedResult(mode, call) {
|
|
|
117
119
|
}
|
|
118
120
|
if (call.name !== 'run_command')
|
|
119
121
|
return null;
|
|
122
|
+
if (call.args?.background === true) {
|
|
123
|
+
return buildPlanModeToolBlockedResult(call.name, PLAN_MODE_RUN_COMMAND_ACTION);
|
|
124
|
+
}
|
|
120
125
|
const command = String(call.args?.command ?? call.args?.cmd ?? '');
|
|
121
126
|
const reason = planModeRunCommandBlockReason(command);
|
|
122
127
|
return reason ? buildPlanModeToolBlockedResult(call.name, reason) : null;
|
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, } 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) {
|
|
@@ -51,11 +51,11 @@ export async function fetchWhoami({ config, fetchImpl = globalThis.fetch, }) {
|
|
|
51
51
|
return data.customer;
|
|
52
52
|
}
|
|
53
53
|
export async function fetchWhoamiResponse({ config, fetchImpl = globalThis.fetch, }) {
|
|
54
|
-
const data = (await authorizedJson({
|
|
54
|
+
const data = (await retryTransient(() => authorizedJson({
|
|
55
55
|
config,
|
|
56
56
|
path: '/v1/auth/whoami',
|
|
57
57
|
fetchImpl,
|
|
58
|
-
}));
|
|
58
|
+
})));
|
|
59
59
|
if (!data?.customer?.email) {
|
|
60
60
|
throw new Error('Server returned an invalid whoami response.');
|
|
61
61
|
}
|
|
@@ -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,21 +1,18 @@
|
|
|
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;
|
|
9
11
|
function shutDownServer(server) {
|
|
10
|
-
// Drop any lingering (keep-alive) connections so the event loop empties and
|
|
11
|
-
// the CLI exits instead of hanging after a successful login.
|
|
12
12
|
server.closeAllConnections?.();
|
|
13
13
|
server.close();
|
|
14
14
|
}
|
|
15
15
|
export function resolveWebsiteUrl() {
|
|
16
|
-
// The public CLI always signs in through the official TheGitAI website. There
|
|
17
|
-
// is no override path here so the published package cannot be pointed at a
|
|
18
|
-
// clone host.
|
|
19
16
|
return DEFAULT_WEBSITE_URL.replace(/\/+$/, '');
|
|
20
17
|
}
|
|
21
18
|
function defaultDeviceName() {
|
|
@@ -26,7 +23,73 @@ function defaultDeviceName() {
|
|
|
26
23
|
return os.hostname();
|
|
27
24
|
}
|
|
28
25
|
}
|
|
29
|
-
|
|
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
|
+
}
|
|
30
93
|
export function generatePkce() {
|
|
31
94
|
const verifier = crypto.randomBytes(32).toString('base64url');
|
|
32
95
|
const challenge = crypto
|
|
@@ -62,7 +125,7 @@ async function exchangeCodeForToken({ serverUrl, code, codeVerifier, fetchImpl,
|
|
|
62
125
|
});
|
|
63
126
|
const data = (await readJsonResponse(response));
|
|
64
127
|
if (!response.ok) {
|
|
65
|
-
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));
|
|
66
129
|
}
|
|
67
130
|
const token = String(data?.token ?? '').trim();
|
|
68
131
|
const customer = data?.customer;
|
|
@@ -77,19 +140,13 @@ async function exchangeCodeForToken({ serverUrl, code, codeVerifier, fetchImpl,
|
|
|
77
140
|
customer,
|
|
78
141
|
};
|
|
79
142
|
}
|
|
80
|
-
/**
|
|
81
|
-
* Browser-based login. Starts a loopback server so the website can redirect the
|
|
82
|
-
* one-time code back automatically; the code is then exchanged for a token
|
|
83
|
-
* using the PKCE verifier. With `noBrowser`, the user pastes the code instead.
|
|
84
|
-
* The CLI never sees the user's credentials.
|
|
85
|
-
*/
|
|
86
143
|
export async function loginViaBrowser(options) {
|
|
87
144
|
const serverUrl = normalizeServerUrl(options.serverUrl ?? DEFAULT_SERVER_URL);
|
|
88
145
|
const websiteUrl = resolveWebsiteUrl();
|
|
89
146
|
const fetchImpl = options.fetchImpl ?? globalThis.fetch;
|
|
90
147
|
const openBrowser = options.openBrowser ?? openUrl;
|
|
91
148
|
const onUrl = options.onUrl ?? (() => { });
|
|
92
|
-
const deviceName = options.deviceName ?? defaultDeviceName();
|
|
149
|
+
const deviceName = withOperatingSystemInfo(options.deviceName ?? defaultDeviceName());
|
|
93
150
|
const { verifier, challenge } = generatePkce();
|
|
94
151
|
if (options.noBrowser) {
|
|
95
152
|
const authUrl = buildAuthUrl(websiteUrl, {
|
|
@@ -97,8 +154,6 @@ export async function loginViaBrowser(options) {
|
|
|
97
154
|
deviceName,
|
|
98
155
|
paste: true,
|
|
99
156
|
});
|
|
100
|
-
// Headless mode: only print the URL for the user to open on another device.
|
|
101
|
-
// Never launch a browser here — that is the whole point of --no-browser.
|
|
102
157
|
onUrl(authUrl);
|
|
103
158
|
if (!options.promptCode) {
|
|
104
159
|
throw new Error('No way to read the authorization code in this context.');
|
|
@@ -125,8 +180,6 @@ export async function loginViaBrowser(options) {
|
|
|
125
180
|
}
|
|
126
181
|
const code = requestUrl.searchParams.get('code') ?? '';
|
|
127
182
|
const returnedState = requestUrl.searchParams.get('state') ?? '';
|
|
128
|
-
// `Connection: close` plus closeAllConnections() ensures the browser's
|
|
129
|
-
// keep-alive socket is torn down so the process can exit after login.
|
|
130
183
|
if (!code || returnedState !== state) {
|
|
131
184
|
res.writeHead(400, { 'content-type': 'text/html', connection: 'close' });
|
|
132
185
|
res.end(RESULT_PAGE('Login failed', 'The request could not be verified. Please run ai login again.'));
|