@thegitai/cli 1.0.0-preview.1 → 1.0.0-preview.11
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 +16 -4
- package/dist/bin/ai.js +57 -287
- package/dist/src/api/auth.js +2 -2
- package/dist/src/api/browser-login.js +72 -3
- package/dist/src/api/chat.js +147 -30
- package/dist/src/api/http.js +16 -3
- package/dist/src/api/models.js +9 -4
- package/dist/src/help-text.js +19 -7
- package/dist/src/patcher.js +96 -9
- package/dist/src/project-index.js +13 -1
- package/dist/src/project-orientation.js +99 -0
- package/dist/src/scratch-dir.js +51 -33
- package/dist/src/session-store.js +52 -20
- package/dist/src/session.js +8 -0
- package/dist/src/tool-executor.js +38 -6
- package/dist/src/tools/delete-file.js +22 -4
- package/dist/src/tools/patch-file.js +30 -5
- package/dist/src/tools/read-file.js +3 -1
- package/dist/src/tools/replace-document-text.js +7 -1
- package/dist/src/tools/run-command.js +37 -19
- package/dist/src/tools/run-node-script.js +24 -4
- package/dist/src/tools/str-replace.js +30 -5
- package/dist/src/tools/write-file.js +25 -5
- 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 +197 -51
- package/dist/src/ui/tui/bridge.js +3 -0
- package/dist/src/ui/tui/build-frame.js +179 -82
- package/dist/src/ui/tui/markdown-render.js +72 -73
- package/dist/src/ui/tui/shell-input.js +42 -13
- 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/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,16 @@ 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
|
+
|
|
29
41
|
## Visible to-do list
|
|
30
42
|
|
|
31
43
|
For larger multi-step tasks, the agent keeps a compact to-do list on screen so
|
package/dist/bin/ai.js
CHANGED
|
@@ -4,20 +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
18
|
const DEFAULT_SERVER_URL = 'https://thegit.ai';
|
|
22
19
|
const { auth, chat, models, sessions } = ServerApi;
|
|
23
20
|
function printUsage() {
|
|
@@ -111,54 +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 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
|
-
}
|
|
140
|
-
function makeConfirmCommand(session) {
|
|
141
|
-
return async (command) => {
|
|
142
|
-
console.log(chalk.bold(`\nCommand approval needed:\n${command}\n`));
|
|
143
|
-
const answer = (await promptText('Approve? [y]es/[a]ll/[n]o', 'n')).toLowerCase();
|
|
144
|
-
if (answer === 'a' || answer === 'all') {
|
|
145
|
-
session.autoYes = true;
|
|
146
|
-
return true;
|
|
147
|
-
}
|
|
148
|
-
return answer === 'y' || answer === 'yes';
|
|
149
|
-
};
|
|
150
|
-
}
|
|
151
|
-
function makeConfirmPatch(session) {
|
|
152
|
-
return async (filePath) => {
|
|
153
|
-
const answer = (await promptText(`Apply patch to ${filePath}? [y]es/[a]ll/[n]o`, 'n')).toLowerCase();
|
|
154
|
-
if (answer === 'a' || answer === 'all') {
|
|
155
|
-
session.autoYes = true;
|
|
156
|
-
return true;
|
|
157
|
-
}
|
|
158
|
-
return answer === 'y' || answer === 'yes';
|
|
159
|
-
};
|
|
115
|
+
return (serverModels?.models.find((model) => model.id === modelId)?.label ??
|
|
116
|
+
`Model ${modelId}`);
|
|
160
117
|
}
|
|
161
118
|
async function saveSessionBoth({ session, serverSessionClient, }) {
|
|
119
|
+
if (!sessionHasUserMessage(session))
|
|
120
|
+
return;
|
|
162
121
|
saveSessionState(session);
|
|
163
122
|
try {
|
|
164
123
|
await serverSessionClient.save(session);
|
|
@@ -167,196 +126,18 @@ async function saveSessionBoth({ session, serverSessionClient, }) {
|
|
|
167
126
|
console.error(chalk.yellow(`Warning: session save failed: ${error.message}`));
|
|
168
127
|
}
|
|
169
128
|
}
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
return {
|
|
183
|
-
selected: models.validateServerModel(selected, serverModels),
|
|
184
|
-
serverModels,
|
|
185
|
-
};
|
|
186
|
-
}
|
|
187
|
-
async function promptForResumeSession(rootDir, serverModels) {
|
|
188
|
-
const sessions = listSessionMetadata(rootDir);
|
|
189
|
-
if (!sessions.length) {
|
|
190
|
-
console.log(chalk.dim('No saved sessions for this repo.\n'));
|
|
191
|
-
return null;
|
|
192
|
-
}
|
|
193
|
-
printSessionList(rootDir, sessions, serverModels);
|
|
194
|
-
console.log();
|
|
195
|
-
const identifier = (await promptText('Session id or name', '')).trim();
|
|
196
|
-
if (!identifier) {
|
|
197
|
-
return null;
|
|
198
|
-
}
|
|
199
|
-
return loadSessionSnapshot(rootDir, identifier);
|
|
200
|
-
}
|
|
201
|
-
async function runTurn({ authConfig, projectIndex, serverSessionClient, session, inputText, }) {
|
|
202
|
-
const prompt = String(inputText ?? '').trim();
|
|
203
|
-
if (!prompt)
|
|
204
|
-
return;
|
|
205
|
-
appendPromptHistory(prompt, session.env);
|
|
206
|
-
const result = await chat.sendServerUserMessage({
|
|
207
|
-
config: authConfig,
|
|
208
|
-
projectIndex,
|
|
209
|
-
session,
|
|
210
|
-
input: prompt,
|
|
211
|
-
});
|
|
212
|
-
if (result.text) {
|
|
213
|
-
console.log(`\n${chalk.green('TheGitAI>')}`);
|
|
214
|
-
console.log(renderMarkdownForTerminal(result.text));
|
|
215
|
-
console.log();
|
|
216
|
-
}
|
|
217
|
-
await saveSessionBoth({ session, serverSessionClient });
|
|
218
|
-
}
|
|
219
|
-
async function mainInteractive({ authConfig, projectIndex, serverModels, serverSessionClient, session, usageText, initialPrompt, }) {
|
|
220
|
-
if (initialPrompt) {
|
|
221
|
-
console.log(chalk.dim(`Prompt: "${initialPrompt}"`));
|
|
222
|
-
await runTurn({
|
|
223
|
-
authConfig,
|
|
224
|
-
projectIndex,
|
|
225
|
-
serverSessionClient,
|
|
226
|
-
session,
|
|
227
|
-
inputText: initialPrompt,
|
|
228
|
-
});
|
|
229
|
-
}
|
|
230
|
-
while (true) {
|
|
231
|
-
const inputText = await promptText('you');
|
|
232
|
-
const trimmed = inputText.trim();
|
|
233
|
-
if (!trimmed)
|
|
234
|
-
continue;
|
|
235
|
-
if (trimmed === '/exit')
|
|
236
|
-
return;
|
|
237
|
-
if (trimmed === '/help') {
|
|
238
|
-
console.log(renderMarkdownForTerminal(formatInteractiveHelpText()));
|
|
239
|
-
continue;
|
|
240
|
-
}
|
|
241
|
-
if (trimmed === '/usage') {
|
|
242
|
-
console.log(await usageText());
|
|
243
|
-
continue;
|
|
244
|
-
}
|
|
245
|
-
if (trimmed === '/clear') {
|
|
246
|
-
clearConversation(session);
|
|
247
|
-
await saveSessionBoth({ session, serverSessionClient });
|
|
248
|
-
console.log(chalk.dim('Conversation cleared.\n'));
|
|
249
|
-
continue;
|
|
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
|
-
}
|
|
313
|
-
if (trimmed === '/resume') {
|
|
314
|
-
const snapshot = await promptForResumeSession(session.rootDir, serverModels);
|
|
315
|
-
if (!snapshot) {
|
|
316
|
-
console.log(chalk.dim('Resume cancelled.\n'));
|
|
317
|
-
continue;
|
|
318
|
-
}
|
|
319
|
-
applySessionSnapshot(session, snapshot);
|
|
320
|
-
setBackgroundJobSession(session.sessionId);
|
|
321
|
-
await saveSessionBoth({ session, serverSessionClient });
|
|
322
|
-
console.log(chalk.dim(`Resumed session${session.sessionName ? ` "${session.sessionName}"` : ''} (${session.sessionId})\n`));
|
|
323
|
-
continue;
|
|
324
|
-
}
|
|
325
|
-
if (trimmed === '/model' || trimmed.startsWith('/model ')) {
|
|
326
|
-
const inline = trimmed.slice('/model'.length).trim();
|
|
327
|
-
let serverModels;
|
|
328
|
-
let selected = null;
|
|
329
|
-
if (inline) {
|
|
330
|
-
serverModels = await models.fetchServerModels({ config: authConfig });
|
|
331
|
-
selected = models.validateServerModel(inline, serverModels);
|
|
332
|
-
}
|
|
333
|
-
else {
|
|
334
|
-
const response = await promptForModelSelection(session.modelId, authConfig);
|
|
335
|
-
serverModels = response.serverModels;
|
|
336
|
-
selected = response.selected;
|
|
337
|
-
}
|
|
338
|
-
if (!selected) {
|
|
339
|
-
console.log(chalk.dim('Model selection cancelled.\n'));
|
|
340
|
-
continue;
|
|
341
|
-
}
|
|
342
|
-
session.modelId = selected;
|
|
343
|
-
models.updateSelectedModelCache({
|
|
344
|
-
config: authConfig,
|
|
345
|
-
selectedModelId: selected,
|
|
346
|
-
serverModels,
|
|
347
|
-
});
|
|
348
|
-
await saveSessionBoth({ session, serverSessionClient });
|
|
349
|
-
console.log(chalk.dim(`Switched to ${modelLabel(serverModels, selected)}.\n`));
|
|
350
|
-
continue;
|
|
351
|
-
}
|
|
352
|
-
await runTurn({
|
|
353
|
-
authConfig,
|
|
354
|
-
projectIndex,
|
|
355
|
-
serverSessionClient,
|
|
356
|
-
session,
|
|
357
|
-
inputText: trimmed,
|
|
358
|
-
});
|
|
359
|
-
}
|
|
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;
|
|
360
141
|
}
|
|
361
142
|
export async function main() {
|
|
362
143
|
const { autoYes, help, version, usage, command, commandArgs, session: sessionIdentifier, listSessions, unknownOption, prompt, } = parseArgs(process.argv);
|
|
@@ -384,6 +165,22 @@ export async function main() {
|
|
|
384
165
|
return;
|
|
385
166
|
}
|
|
386
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
|
+
}
|
|
387
184
|
const authConfig = requireCliAuthConfig();
|
|
388
185
|
const serverSessionClient = sessions.createServerSessionClient({ config: authConfig });
|
|
389
186
|
const cachedModels = models.selectCacheForServer(models.readCachedServerModels(), authConfig.serverUrl);
|
|
@@ -426,13 +223,6 @@ export async function main() {
|
|
|
426
223
|
if (offlineNotice) {
|
|
427
224
|
console.error(chalk.yellow(`⚠ Couldn't reach TheGitAI (${offlineNotice}). Starting with cached settings — it will reconnect on your next message.`));
|
|
428
225
|
}
|
|
429
|
-
if (listSessions) {
|
|
430
|
-
printSessionList(rootDir, listSessionMetadata(rootDir), serverModels);
|
|
431
|
-
return;
|
|
432
|
-
}
|
|
433
|
-
const sourceSnapshot = sessionIdentifier
|
|
434
|
-
? loadSessionSnapshot(rootDir, sessionIdentifier)
|
|
435
|
-
: null;
|
|
436
226
|
const selectedModelId = models.selectServerModel({
|
|
437
227
|
requestedModelId: sourceSnapshot?.modelId ?? null,
|
|
438
228
|
cached: cachedModels,
|
|
@@ -448,11 +238,8 @@ export async function main() {
|
|
|
448
238
|
autoYes,
|
|
449
239
|
modelId: selectedModelId,
|
|
450
240
|
});
|
|
451
|
-
session.confirmCommand = makeConfirmCommand(session);
|
|
452
|
-
session.confirmPatch = makeConfirmPatch(session);
|
|
453
241
|
if (sourceSnapshot) {
|
|
454
242
|
applySessionSnapshot(session, sourceSnapshot);
|
|
455
|
-
await saveSessionBoth({ session, serverSessionClient });
|
|
456
243
|
}
|
|
457
244
|
const projectIndex = createIndex({
|
|
458
245
|
rootDir,
|
|
@@ -468,44 +255,27 @@ export async function main() {
|
|
|
468
255
|
},
|
|
469
256
|
});
|
|
470
257
|
const initialPrompt = prompt || undefined;
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
printSessionExit(session);
|
|
485
|
-
return;
|
|
486
|
-
}
|
|
487
|
-
printStartupBanner(rootDir, modelLabel(serverModels, session.modelId), session.autoYes);
|
|
488
|
-
printSessionStartup(session);
|
|
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
|
-
}
|
|
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);
|
|
507
271
|
}
|
|
508
272
|
main().catch((error) => {
|
|
509
|
-
|
|
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
|
+
}
|
|
510
280
|
process.exit(1);
|
|
511
281
|
});
|
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, {
|