@thegitai/cli 1.0.0-beta.2 → 1.0.0-beta.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 +37 -2
- package/dist/bin/ai.js +148 -75
- package/dist/parsers/NOTICE +18 -0
- package/dist/src/agent-mode.js +5 -0
- package/dist/src/api/auth.js +6 -4
- package/dist/src/api/browser-login.js +7 -41
- package/dist/src/api/chat.js +77 -20
- package/dist/src/api/http.js +81 -4
- package/dist/src/api/models.js +26 -18
- package/dist/src/artifact-policy.js +12 -0
- package/dist/src/background-jobs.js +410 -0
- package/dist/src/cli-args.js +60 -0
- package/dist/src/client-environment.js +129 -0
- package/dist/src/colors.js +50 -0
- package/dist/src/core/clipboard.js +75 -0
- package/dist/src/core/image-path-extractor.js +144 -0
- package/dist/src/edit-journal.js +39 -6
- package/dist/src/executor.js +48 -12
- package/dist/src/help-text.js +24 -5
- package/dist/src/markdown-renderer.js +1 -1
- package/dist/src/patcher.js +17 -2
- package/dist/src/scanner.js +58 -17
- package/dist/src/scratch-dir.js +57 -0
- package/dist/src/secret-preview.js +0 -10
- package/dist/src/session-safety.js +64 -31
- package/dist/src/session-store.js +0 -1
- package/dist/src/todo-list.js +106 -0
- package/dist/src/tool-executor.js +164 -18
- package/dist/src/tools/delete-file.js +1 -1
- package/dist/src/tools/index.js +8 -0
- package/dist/src/tools/patch-file.js +16 -2
- package/dist/src/tools/path-suggest.js +139 -0
- package/dist/src/tools/read-document.js +15 -4
- package/dist/src/tools/read-file.js +23 -7
- package/dist/src/tools/replace-document-text.js +234 -0
- package/dist/src/tools/restore-checkpoint.js +1 -1
- package/dist/src/tools/run-command.js +83 -16
- package/dist/src/tools/run-node-script.js +3 -1
- 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 +16 -2
- package/dist/src/tools/undo-edit.js +7 -5
- package/dist/src/tools/update-todos.js +27 -0
- package/dist/src/tools/write-file.js +14 -1
- package/dist/src/tree-sitter-runtime.js +8 -1
- package/dist/src/ui/repl.js +315 -24
- package/dist/src/ui/tui/bridge.js +2 -6
- package/dist/src/ui/tui/build-frame.js +225 -25
- package/dist/src/ui/tui/shell-input.js +42 -5
- package/dist/src/version.js +29 -0
- 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 +14 -15
package/README.md
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
# TheGitAI
|
|
2
2
|
|
|
3
|
-
Interactive terminal coding agent. Talk to your repo in plain English — it
|
|
4
|
-
|
|
3
|
+
Interactive terminal coding agent. Talk to your repo in plain English — it reads
|
|
4
|
+
your code, makes edits, runs the commands a task needs, and keeps long-running
|
|
5
|
+
processes going while you work, all with your approval.
|
|
5
6
|
|
|
6
7
|
## Install
|
|
7
8
|
|
|
@@ -20,10 +21,44 @@ ai login sign in via your browser (--no-browser for SSH/headless)
|
|
|
20
21
|
ai whoami show the signed-in account
|
|
21
22
|
ai --usage show account usage and reset times
|
|
22
23
|
ai logout sign out
|
|
24
|
+
ai --version print the version and exit
|
|
23
25
|
```
|
|
24
26
|
|
|
25
27
|
Run `ai --help` for sessions, modes, keys, and chat commands.
|
|
26
28
|
|
|
29
|
+
## Visible to-do list
|
|
30
|
+
|
|
31
|
+
For larger multi-step tasks, the agent keeps a compact to-do list on screen so
|
|
32
|
+
you can see what it plans to do, what it is working on right now, and what is
|
|
33
|
+
already done.
|
|
34
|
+
|
|
35
|
+
- While the agent works, the list sits at the bottom of the live **Working**
|
|
36
|
+
area, right above where you type, and updates as steps start and finish —
|
|
37
|
+
one step in progress at a time — so it stays visible even when tool output
|
|
38
|
+
above it runs long.
|
|
39
|
+
- When the turn ends, a final snapshot of the list stays readable in the
|
|
40
|
+
transcript, and the footer shows a small progress chip (e.g. `◐ 4/6 to-dos`)
|
|
41
|
+
while steps remain open.
|
|
42
|
+
- The agent's current reasoning stays visible too, right below the list, in a
|
|
43
|
+
compact one-line form so it doesn't compete with the list for space.
|
|
44
|
+
- The list is managed entirely by the agent; simple one-step requests skip it.
|
|
45
|
+
|
|
46
|
+
## Background jobs
|
|
47
|
+
|
|
48
|
+
Some commands are meant to keep running — a dev server, a file watcher, a local
|
|
49
|
+
API. TheGitAI runs these as **background jobs**, so the agent can start a server,
|
|
50
|
+
work against it live, and stop it when the task is done, all in one session.
|
|
51
|
+
|
|
52
|
+
- Each job's block in the transcript updates on its own with a live tail of its
|
|
53
|
+
latest output, and a compact indicator keeps you aware of what's still running.
|
|
54
|
+
- Type `/jobs` to open a picker: **↑ / ↓** to move, **Enter** to expand a job and
|
|
55
|
+
read its output inline, **k** to stop it, **Esc** to close.
|
|
56
|
+
- `/jobs output <id>` prints a job's full output, and `/jobs kill <id>` stops one
|
|
57
|
+
by id.
|
|
58
|
+
- When you end the session, TheGitAI stops its background jobs for you.
|
|
59
|
+
|
|
60
|
+
Full documentation: <https://thegit.ai/docs>
|
|
61
|
+
|
|
27
62
|
## License
|
|
28
63
|
|
|
29
64
|
Proprietary — see the LICENSE file included in this package. Source is
|
package/dist/bin/ai.js
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
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 { isTransientNetworkError } from '../src/api/http.js';
|
|
7
8
|
import { formatCliHelpText, formatInteractiveHelpText, } from '../src/help-text.js';
|
|
8
9
|
import { renderMarkdownForTerminal } from '../src/markdown-renderer.js';
|
|
9
10
|
import { createIndex } from '../src/project-index.js';
|
|
@@ -13,65 +14,15 @@ import { runClientInteractive, shouldUseClientRatatuiShell, } from '../src/ui/re
|
|
|
13
14
|
import { appendPromptToFile } from '../src/ui/prompt-history-store.js';
|
|
14
15
|
import { formatSessionExitNotice } from '../src/session-exit.js';
|
|
15
16
|
import { formatUsageText } from '../src/usage.js';
|
|
17
|
+
import { formatVersionLine } from '../src/version.js';
|
|
18
|
+
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';
|
|
16
21
|
const DEFAULT_SERVER_URL = 'https://thegit.ai';
|
|
17
|
-
const AUTH_COMMANDS = new Set(['login', 'whoami', 'logout']);
|
|
18
22
|
const { auth, chat, models, sessions } = ServerApi;
|
|
19
23
|
function printUsage() {
|
|
20
24
|
console.log(formatCliHelpText({ color: process.stdout.isTTY === true }));
|
|
21
25
|
}
|
|
22
|
-
export function parseArgs(argv) {
|
|
23
|
-
const args = argv.slice(2);
|
|
24
|
-
const firstArg = args[0];
|
|
25
|
-
const command = firstArg && AUTH_COMMANDS.has(firstArg) ? firstArg : null;
|
|
26
|
-
const commandArgs = command ? args.slice(1) : [];
|
|
27
|
-
let autoYes = false;
|
|
28
|
-
let help = false;
|
|
29
|
-
let usage = false;
|
|
30
|
-
let session = null;
|
|
31
|
-
let listSessions = false;
|
|
32
|
-
const promptParts = [];
|
|
33
|
-
for (let i = 0; i < args.length; i++) {
|
|
34
|
-
const arg = args[i];
|
|
35
|
-
if (arg === '--yes' || arg === '-y') {
|
|
36
|
-
autoYes = true;
|
|
37
|
-
continue;
|
|
38
|
-
}
|
|
39
|
-
if ((arg === '--session' || arg === '--resume') && i + 1 < args.length) {
|
|
40
|
-
session = args[i + 1] ?? null;
|
|
41
|
-
i += 1;
|
|
42
|
-
continue;
|
|
43
|
-
}
|
|
44
|
-
if (arg === '--list-sessions') {
|
|
45
|
-
listSessions = true;
|
|
46
|
-
continue;
|
|
47
|
-
}
|
|
48
|
-
if (arg === '--help' || arg === '-h') {
|
|
49
|
-
help = true;
|
|
50
|
-
continue;
|
|
51
|
-
}
|
|
52
|
-
if (arg === '--usage') {
|
|
53
|
-
usage = true;
|
|
54
|
-
continue;
|
|
55
|
-
}
|
|
56
|
-
promptParts.push(arg);
|
|
57
|
-
}
|
|
58
|
-
return {
|
|
59
|
-
command,
|
|
60
|
-
commandArgs,
|
|
61
|
-
autoYes,
|
|
62
|
-
help,
|
|
63
|
-
usage,
|
|
64
|
-
session,
|
|
65
|
-
listSessions,
|
|
66
|
-
prompt: promptParts.join(' ').trim(),
|
|
67
|
-
};
|
|
68
|
-
}
|
|
69
|
-
function commandFlagValue(args, name) {
|
|
70
|
-
const index = args.indexOf(name);
|
|
71
|
-
if (index === -1)
|
|
72
|
-
return null;
|
|
73
|
-
return args[index + 1] ?? null;
|
|
74
|
-
}
|
|
75
26
|
async function promptText(question, fallback = null) {
|
|
76
27
|
const rl = readline.createInterface({ input, output });
|
|
77
28
|
try {
|
|
@@ -88,17 +39,13 @@ function appendPromptHistory(prompt, env = process.env) {
|
|
|
88
39
|
}
|
|
89
40
|
async function runAuthCommand(command, args) {
|
|
90
41
|
if (command === 'login') {
|
|
91
|
-
const serverUrl =
|
|
92
|
-
process.env.THEGITAI_SERVER_URL ??
|
|
93
|
-
DEFAULT_SERVER_URL;
|
|
94
|
-
const websiteUrl = commandFlagValue(args, '--website') ?? undefined;
|
|
42
|
+
const serverUrl = DEFAULT_SERVER_URL;
|
|
95
43
|
const noBrowser = args.includes('--no-browser');
|
|
96
44
|
console.log(chalk.dim(noBrowser
|
|
97
45
|
? 'Sign in on the website, then paste the authorization code here.'
|
|
98
46
|
: 'Opening your browser to sign in…'));
|
|
99
47
|
const result = await loginViaBrowser({
|
|
100
48
|
serverUrl,
|
|
101
|
-
websiteUrl,
|
|
102
49
|
noBrowser,
|
|
103
50
|
onUrl: (url) => {
|
|
104
51
|
console.log(chalk.dim(noBrowser ? 'Open this URL to sign in:' : 'If your browser did not open, visit:'));
|
|
@@ -181,6 +128,15 @@ function modelLabel(serverModels, modelId) {
|
|
|
181
128
|
return (serverModels.models.find((model) => model.id === modelId)?.label ??
|
|
182
129
|
'Unknown model');
|
|
183
130
|
}
|
|
131
|
+
function formatJobElapsedMs(ms) {
|
|
132
|
+
const totalSeconds = Math.max(0, Math.floor(ms / 1000));
|
|
133
|
+
if (totalSeconds < 60)
|
|
134
|
+
return `${totalSeconds}s`;
|
|
135
|
+
const minutes = Math.floor(totalSeconds / 60);
|
|
136
|
+
if (minutes < 60)
|
|
137
|
+
return `${minutes}m${String(totalSeconds % 60).padStart(2, '0')}s`;
|
|
138
|
+
return `${Math.floor(minutes / 60)}h${String(minutes % 60).padStart(2, '0')}m`;
|
|
139
|
+
}
|
|
184
140
|
function makeConfirmCommand(session) {
|
|
185
141
|
return async (command) => {
|
|
186
142
|
console.log(chalk.bold(`\nCommand approval needed:\n${command}\n`));
|
|
@@ -292,6 +248,68 @@ async function mainInteractive({ authConfig, projectIndex, serverModels, serverS
|
|
|
292
248
|
console.log(chalk.dim('Conversation cleared.\n'));
|
|
293
249
|
continue;
|
|
294
250
|
}
|
|
251
|
+
if (trimmed === '/jobs' || trimmed.startsWith('/jobs ')) {
|
|
252
|
+
const jobsArgs = trimmed.slice('/jobs'.length).trim();
|
|
253
|
+
const killMatch = jobsArgs.match(/^kill\s+(\S+)$/);
|
|
254
|
+
if (killMatch) {
|
|
255
|
+
const jobId = killMatch[1];
|
|
256
|
+
const killed = await killBackgroundJob(jobId);
|
|
257
|
+
await collectBackgroundJobUiKillMutations({
|
|
258
|
+
session,
|
|
259
|
+
projectIndex,
|
|
260
|
+
jobId,
|
|
261
|
+
result: killed,
|
|
262
|
+
});
|
|
263
|
+
if (!killed.ok) {
|
|
264
|
+
console.log(chalk.red(killed.error ?? 'Background job kill failed.'));
|
|
265
|
+
}
|
|
266
|
+
else if (killed.alreadyFinished) {
|
|
267
|
+
console.log(chalk.dim(`${jobId} had already finished.`));
|
|
268
|
+
}
|
|
269
|
+
continue;
|
|
270
|
+
}
|
|
271
|
+
const outputMatch = jobsArgs.match(/^output\s+(\S+)$/);
|
|
272
|
+
if (outputMatch) {
|
|
273
|
+
const jobId = outputMatch[1];
|
|
274
|
+
await collectBackgroundJobUiOutputMutations({
|
|
275
|
+
session,
|
|
276
|
+
projectIndex,
|
|
277
|
+
jobId,
|
|
278
|
+
});
|
|
279
|
+
const job = listBackgroundJobs().find((candidate) => candidate.id === jobId);
|
|
280
|
+
const buffered = getJobBufferedOutput(jobId);
|
|
281
|
+
if (!job || !buffered) {
|
|
282
|
+
console.log(chalk.red(`Unknown background job id: ${jobId}`));
|
|
283
|
+
continue;
|
|
284
|
+
}
|
|
285
|
+
if (buffered.droppedChars > 0) {
|
|
286
|
+
console.log(chalk.dim(`... (${buffered.droppedChars} chars of older output dropped) ...`));
|
|
287
|
+
}
|
|
288
|
+
console.log(buffered.output || chalk.dim('(no output captured)'));
|
|
289
|
+
continue;
|
|
290
|
+
}
|
|
291
|
+
if (jobsArgs) {
|
|
292
|
+
console.log(chalk.dim('Usage: /jobs — list · /jobs output <id> — full output · /jobs kill <id> — kill'));
|
|
293
|
+
continue;
|
|
294
|
+
}
|
|
295
|
+
const jobsList = listBackgroundJobs();
|
|
296
|
+
if (!jobsList.length) {
|
|
297
|
+
console.log(chalk.dim('No background jobs in this session.'));
|
|
298
|
+
continue;
|
|
299
|
+
}
|
|
300
|
+
for (const job of jobsList) {
|
|
301
|
+
const elapsed = formatJobElapsedMs((job.endedAt ?? Date.now()) - job.startedAt);
|
|
302
|
+
const stateText = job.status === 'running'
|
|
303
|
+
? `running · ${elapsed}`
|
|
304
|
+
: job.status === 'killed'
|
|
305
|
+
? `killed · ran ${elapsed}`
|
|
306
|
+
: job.status === 'error'
|
|
307
|
+
? 'failed to start'
|
|
308
|
+
: `exited (code ${job.exitCode ?? 1}) · ran ${elapsed}`;
|
|
309
|
+
console.log(`${job.id} · ${stateText}\n $ ${job.command}`);
|
|
310
|
+
}
|
|
311
|
+
continue;
|
|
312
|
+
}
|
|
295
313
|
if (trimmed === '/resume') {
|
|
296
314
|
const snapshot = await promptForResumeSession(session.rootDir, serverModels);
|
|
297
315
|
if (!snapshot) {
|
|
@@ -299,6 +317,7 @@ async function mainInteractive({ authConfig, projectIndex, serverModels, serverS
|
|
|
299
317
|
continue;
|
|
300
318
|
}
|
|
301
319
|
applySessionSnapshot(session, snapshot);
|
|
320
|
+
setBackgroundJobSession(session.sessionId);
|
|
302
321
|
await saveSessionBoth({ session, serverSessionClient });
|
|
303
322
|
console.log(chalk.dim(`Resumed session${session.sessionName ? ` "${session.sessionName}"` : ''} (${session.sessionId})\n`));
|
|
304
323
|
continue;
|
|
@@ -340,11 +359,21 @@ async function mainInteractive({ authConfig, projectIndex, serverModels, serverS
|
|
|
340
359
|
}
|
|
341
360
|
}
|
|
342
361
|
export async function main() {
|
|
343
|
-
const { autoYes, help, usage, command, commandArgs, session: sessionIdentifier, listSessions, prompt, } = parseArgs(process.argv);
|
|
362
|
+
const { autoYes, help, version, usage, command, commandArgs, session: sessionIdentifier, listSessions, unknownOption, prompt, } = parseArgs(process.argv);
|
|
363
|
+
if (version) {
|
|
364
|
+
console.log(formatVersionLine());
|
|
365
|
+
return;
|
|
366
|
+
}
|
|
344
367
|
if (help) {
|
|
345
368
|
printUsage();
|
|
346
369
|
return;
|
|
347
370
|
}
|
|
371
|
+
if (unknownOption) {
|
|
372
|
+
console.error(`Unknown option: ${unknownOption}`);
|
|
373
|
+
console.error("Run 'ai --help' to see available commands and options.");
|
|
374
|
+
process.exitCode = 2;
|
|
375
|
+
return;
|
|
376
|
+
}
|
|
348
377
|
if (command) {
|
|
349
378
|
await runAuthCommand(command, commandArgs);
|
|
350
379
|
return;
|
|
@@ -357,9 +386,46 @@ export async function main() {
|
|
|
357
386
|
const rootDir = process.cwd();
|
|
358
387
|
const authConfig = requireCliAuthConfig();
|
|
359
388
|
const serverSessionClient = sessions.createServerSessionClient({ config: authConfig });
|
|
360
|
-
const cachedModels = models.readCachedServerModels();
|
|
361
|
-
|
|
362
|
-
|
|
389
|
+
const cachedModels = models.selectCacheForServer(models.readCachedServerModels(), authConfig.serverUrl);
|
|
390
|
+
let offlineNotice = null;
|
|
391
|
+
let serverModels;
|
|
392
|
+
try {
|
|
393
|
+
serverModels = await models.fetchServerModels({ config: authConfig });
|
|
394
|
+
}
|
|
395
|
+
catch (error) {
|
|
396
|
+
if (isTransientNetworkError(error) && cachedModels?.models.length) {
|
|
397
|
+
serverModels = { models: cachedModels.models };
|
|
398
|
+
offlineNotice = error?.message ? String(error.message) : 'network error';
|
|
399
|
+
}
|
|
400
|
+
else {
|
|
401
|
+
throw error;
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
let whoami;
|
|
405
|
+
try {
|
|
406
|
+
whoami = await auth.fetchWhoamiResponse({ config: authConfig });
|
|
407
|
+
}
|
|
408
|
+
catch (error) {
|
|
409
|
+
if (isTransientNetworkError(error)) {
|
|
410
|
+
whoami = {
|
|
411
|
+
customer: {
|
|
412
|
+
id: '',
|
|
413
|
+
uuid: '',
|
|
414
|
+
email: authConfig.email,
|
|
415
|
+
customer_type: authConfig.customerType ?? 'USER',
|
|
416
|
+
scopes: [],
|
|
417
|
+
},
|
|
418
|
+
debugUi: { showSessionId: false },
|
|
419
|
+
};
|
|
420
|
+
offlineNotice ??= error?.message ? String(error.message) : 'network error';
|
|
421
|
+
}
|
|
422
|
+
else {
|
|
423
|
+
throw error;
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
if (offlineNotice) {
|
|
427
|
+
console.error(chalk.yellow(`⚠ Couldn't reach TheGitAI (${offlineNotice}). Starting with cached settings — it will reconnect on your next message.`));
|
|
428
|
+
}
|
|
363
429
|
if (listSessions) {
|
|
364
430
|
printSessionList(rootDir, listSessionMetadata(rootDir), serverModels);
|
|
365
431
|
return;
|
|
@@ -420,17 +486,24 @@ export async function main() {
|
|
|
420
486
|
}
|
|
421
487
|
printStartupBanner(rootDir, modelLabel(serverModels, session.modelId), session.autoYes);
|
|
422
488
|
printSessionStartup(session);
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
489
|
+
setBackgroundJobSession(session.sessionId);
|
|
490
|
+
try {
|
|
491
|
+
await mainInteractive({
|
|
492
|
+
authConfig,
|
|
493
|
+
projectIndex,
|
|
494
|
+
serverModels,
|
|
495
|
+
serverSessionClient,
|
|
496
|
+
session,
|
|
497
|
+
usageText: async () => formatUsageText(await auth.fetchWhoamiResponse({ config: authConfig })),
|
|
498
|
+
initialPrompt,
|
|
499
|
+
});
|
|
500
|
+
await saveSessionBoth({ session, serverSessionClient });
|
|
501
|
+
printSessionExit(session);
|
|
502
|
+
}
|
|
503
|
+
finally {
|
|
504
|
+
killAllBackgroundJobs({ sessionId: session.sessionId, remove: true });
|
|
505
|
+
setBackgroundJobSession(null);
|
|
506
|
+
}
|
|
434
507
|
}
|
|
435
508
|
main().catch((error) => {
|
|
436
509
|
console.error(chalk.red(`\n✖ Error: ${error.message}\n`));
|
|
@@ -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 { authorizedJson, failureMessage, normalizeServerUrl, readJsonResponse, } from './http.js';
|
|
4
|
+
import { ServerApiError, authorizedJson, createTraceContext, 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
|
}
|
|
@@ -68,14 +68,16 @@ export async function fetchWhoamiResponse({ config, fetchImpl = globalThis.fetch
|
|
|
68
68
|
};
|
|
69
69
|
}
|
|
70
70
|
export async function logoutFromServer({ config, fetchImpl = globalThis.fetch, }) {
|
|
71
|
+
const trace = createTraceContext();
|
|
71
72
|
const response = await fetchImpl(`${normalizeServerUrl(config.serverUrl)}/v1/auth/logout`, {
|
|
72
73
|
method: 'POST',
|
|
73
74
|
headers: {
|
|
74
75
|
authorization: `Bearer ${config.token}`,
|
|
76
|
+
...trace.headers,
|
|
75
77
|
},
|
|
76
78
|
});
|
|
77
79
|
if (!response.ok && response.status !== 401) {
|
|
78
80
|
const data = (await readJsonResponse(response));
|
|
79
|
-
throw new
|
|
81
|
+
throw new ServerApiError(failureMessage(data, response.status), response.status, trace.traceId);
|
|
80
82
|
}
|
|
81
83
|
}
|
|
@@ -2,40 +2,16 @@ import crypto from 'node:crypto';
|
|
|
2
2
|
import http from 'node:http';
|
|
3
3
|
import os from 'node:os';
|
|
4
4
|
import { openUrl } from '../core/open-url.js';
|
|
5
|
-
import { failureMessage, normalizeServerUrl, readJsonResponse, } from './http.js';
|
|
5
|
+
import { ServerApiError, createTraceContext, failureMessage, normalizeServerUrl, readJsonResponse, } from './http.js';
|
|
6
6
|
const DEFAULT_WEBSITE_URL = 'https://thegit.ai';
|
|
7
|
-
const DEFAULT_DEV_WEBSITE_URL = 'http://localhost:3002';
|
|
8
7
|
const DEFAULT_SERVER_URL = 'https://thegit.ai';
|
|
9
8
|
const DEFAULT_TIMEOUT_MS = 5 * 60 * 1000;
|
|
10
9
|
function shutDownServer(server) {
|
|
11
|
-
// Drop any lingering (keep-alive) connections so the event loop empties and
|
|
12
|
-
// the CLI exits instead of hanging after a successful login.
|
|
13
10
|
server.closeAllConnections?.();
|
|
14
11
|
server.close();
|
|
15
12
|
}
|
|
16
|
-
function
|
|
17
|
-
|
|
18
|
-
return false;
|
|
19
|
-
try {
|
|
20
|
-
const host = new URL(url).hostname;
|
|
21
|
-
return host === 'localhost' || host === '127.0.0.1' || host === '::1';
|
|
22
|
-
}
|
|
23
|
-
catch {
|
|
24
|
-
return false;
|
|
25
|
-
}
|
|
26
|
-
}
|
|
27
|
-
export function resolveWebsiteUrl(websiteUrl, env = process.env, serverUrl) {
|
|
28
|
-
const explicit = String(websiteUrl ?? '').trim() ||
|
|
29
|
-
String(env.THEGITAI_WEBSITE_URL ?? '').trim();
|
|
30
|
-
// With no explicit override, point at production — unless we're clearly in
|
|
31
|
-
// local dev (talking to a localhost server), in which case default to the
|
|
32
|
-
// local website so `ai login` works without any flags or env vars.
|
|
33
|
-
const value = explicit || (isLocalhostUrl(serverUrl) ? DEFAULT_DEV_WEBSITE_URL : DEFAULT_WEBSITE_URL);
|
|
34
|
-
const normalized = value.replace(/\/+$/, '');
|
|
35
|
-
if (!/^https?:\/\//i.test(normalized)) {
|
|
36
|
-
throw new Error('Website URL must start with http:// or https://.');
|
|
37
|
-
}
|
|
38
|
-
return normalized;
|
|
13
|
+
export function resolveWebsiteUrl() {
|
|
14
|
+
return DEFAULT_WEBSITE_URL.replace(/\/+$/, '');
|
|
39
15
|
}
|
|
40
16
|
function defaultDeviceName() {
|
|
41
17
|
try {
|
|
@@ -45,7 +21,6 @@ function defaultDeviceName() {
|
|
|
45
21
|
return os.hostname();
|
|
46
22
|
}
|
|
47
23
|
}
|
|
48
|
-
/** PKCE (RFC 7636, S256): a random verifier and its SHA-256 challenge. */
|
|
49
24
|
export function generatePkce() {
|
|
50
25
|
const verifier = crypto.randomBytes(32).toString('base64url');
|
|
51
26
|
const challenge = crypto
|
|
@@ -73,14 +48,15 @@ const RESULT_PAGE = (heading, detail) => `<!doctype html><html><head><meta chars
|
|
|
73
48
|
`p{color:#9aa0a6}</style></head><body><div class="card"><h1>${heading}</h1>` +
|
|
74
49
|
`<p>${detail}</p></div></body></html>`;
|
|
75
50
|
async function exchangeCodeForToken({ serverUrl, code, codeVerifier, fetchImpl, }) {
|
|
51
|
+
const trace = createTraceContext();
|
|
76
52
|
const response = await fetchImpl(`${serverUrl}/v1/cli/auth/token`, {
|
|
77
53
|
method: 'POST',
|
|
78
|
-
headers: { 'content-type': 'application/json' },
|
|
54
|
+
headers: { 'content-type': 'application/json', ...trace.headers },
|
|
79
55
|
body: JSON.stringify({ code, code_verifier: codeVerifier }),
|
|
80
56
|
});
|
|
81
57
|
const data = (await readJsonResponse(response));
|
|
82
58
|
if (!response.ok) {
|
|
83
|
-
throw new
|
|
59
|
+
throw new ServerApiError(failureMessage(data, response.status), response.status, trace.traceId);
|
|
84
60
|
}
|
|
85
61
|
const token = String(data?.token ?? '').trim();
|
|
86
62
|
const customer = data?.customer;
|
|
@@ -95,15 +71,9 @@ async function exchangeCodeForToken({ serverUrl, code, codeVerifier, fetchImpl,
|
|
|
95
71
|
customer,
|
|
96
72
|
};
|
|
97
73
|
}
|
|
98
|
-
/**
|
|
99
|
-
* Browser-based login. Starts a loopback server so the website can redirect the
|
|
100
|
-
* one-time code back automatically; the code is then exchanged for a token
|
|
101
|
-
* using the PKCE verifier. With `noBrowser`, the user pastes the code instead.
|
|
102
|
-
* The CLI never sees the user's credentials.
|
|
103
|
-
*/
|
|
104
74
|
export async function loginViaBrowser(options) {
|
|
105
75
|
const serverUrl = normalizeServerUrl(options.serverUrl ?? DEFAULT_SERVER_URL);
|
|
106
|
-
const websiteUrl = resolveWebsiteUrl(
|
|
76
|
+
const websiteUrl = resolveWebsiteUrl();
|
|
107
77
|
const fetchImpl = options.fetchImpl ?? globalThis.fetch;
|
|
108
78
|
const openBrowser = options.openBrowser ?? openUrl;
|
|
109
79
|
const onUrl = options.onUrl ?? (() => { });
|
|
@@ -115,8 +85,6 @@ export async function loginViaBrowser(options) {
|
|
|
115
85
|
deviceName,
|
|
116
86
|
paste: true,
|
|
117
87
|
});
|
|
118
|
-
// Headless mode: only print the URL for the user to open on another device.
|
|
119
|
-
// Never launch a browser here — that is the whole point of --no-browser.
|
|
120
88
|
onUrl(authUrl);
|
|
121
89
|
if (!options.promptCode) {
|
|
122
90
|
throw new Error('No way to read the authorization code in this context.');
|
|
@@ -143,8 +111,6 @@ export async function loginViaBrowser(options) {
|
|
|
143
111
|
}
|
|
144
112
|
const code = requestUrl.searchParams.get('code') ?? '';
|
|
145
113
|
const returnedState = requestUrl.searchParams.get('state') ?? '';
|
|
146
|
-
// `Connection: close` plus closeAllConnections() ensures the browser's
|
|
147
|
-
// keep-alive socket is torn down so the process can exit after login.
|
|
148
114
|
if (!code || returnedState !== state) {
|
|
149
115
|
res.writeHead(400, { 'content-type': 'text/html', connection: 'close' });
|
|
150
116
|
res.end(RESULT_PAGE('Login failed', 'The request could not be verified. Please run ai login again.'));
|