@thegitai/cli 1.0.0-preview.23 → 1.0.0-preview.25
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/dist/bin/ai.js +54 -30
- package/dist/src/api/auth.js +4 -2
- package/dist/src/api/http.js +13 -1
- package/dist/src/api/models.js +17 -8
- package/dist/src/ui/repl.js +3 -1
- package/package.json +5 -5
package/dist/bin/ai.js
CHANGED
|
@@ -4,7 +4,7 @@ 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 { authenticationErrorMessage, isAuthenticationError, isTransientNetworkError, } from '../src/api/http.js';
|
|
7
|
+
import { STARTUP_RETRY_BUDGET, authenticationErrorMessage, isAuthenticationError, isTransientNetworkError, } from '../src/api/http.js';
|
|
8
8
|
import { formatCliHelpText } from '../src/help-text.js';
|
|
9
9
|
import { createIndex } from '../src/project-index.js';
|
|
10
10
|
import { createSession } from '../src/session.js';
|
|
@@ -20,6 +20,20 @@ const { auth, chat, models, sessions } = ServerApi;
|
|
|
20
20
|
function printUsage() {
|
|
21
21
|
console.log(formatCliHelpText({ color: process.stdout.isTTY === true }));
|
|
22
22
|
}
|
|
23
|
+
function unreachableReason(error) {
|
|
24
|
+
const err = error;
|
|
25
|
+
const code = err?.code ?? err?.cause?.code;
|
|
26
|
+
if (err?.name === 'TimeoutError' || err?.name === 'AbortError') {
|
|
27
|
+
return 'network timeout';
|
|
28
|
+
}
|
|
29
|
+
if (code === 'ENOTFOUND' || code === 'EAI_AGAIN')
|
|
30
|
+
return 'DNS lookup failed';
|
|
31
|
+
if (code === 'ECONNREFUSED')
|
|
32
|
+
return 'connection refused';
|
|
33
|
+
if (code === 'ECONNRESET' || code === 'EPIPE')
|
|
34
|
+
return 'connection reset';
|
|
35
|
+
return err?.message ? String(err.message) : 'network error';
|
|
36
|
+
}
|
|
23
37
|
async function promptText(question, fallback = null) {
|
|
24
38
|
const rl = readline.createInterface({ input, output });
|
|
25
39
|
try {
|
|
@@ -184,41 +198,51 @@ export async function main() {
|
|
|
184
198
|
const authConfig = requireCliAuthConfig();
|
|
185
199
|
const serverSessionClient = sessions.createServerSessionClient({ config: authConfig });
|
|
186
200
|
const cachedModels = models.selectCacheForServer(models.readCachedServerModels(), authConfig.serverUrl);
|
|
201
|
+
const [modelsOutcome, whoamiOutcome] = await Promise.allSettled([
|
|
202
|
+
models.fetchServerModels({
|
|
203
|
+
config: authConfig,
|
|
204
|
+
budget: STARTUP_RETRY_BUDGET,
|
|
205
|
+
}),
|
|
206
|
+
auth.fetchWhoamiResponse({
|
|
207
|
+
config: authConfig,
|
|
208
|
+
budget: STARTUP_RETRY_BUDGET,
|
|
209
|
+
}),
|
|
210
|
+
]);
|
|
187
211
|
let offlineNotice = null;
|
|
188
212
|
let serverModels;
|
|
189
|
-
|
|
190
|
-
serverModels =
|
|
213
|
+
if (modelsOutcome.status === 'fulfilled') {
|
|
214
|
+
serverModels = modelsOutcome.value;
|
|
191
215
|
}
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
216
|
+
else if (isTransientNetworkError(modelsOutcome.reason) &&
|
|
217
|
+
cachedModels?.models.length) {
|
|
218
|
+
serverModels = { models: cachedModels.models };
|
|
219
|
+
offlineNotice = unreachableReason(modelsOutcome.reason);
|
|
220
|
+
}
|
|
221
|
+
else if (isTransientNetworkError(modelsOutcome.reason)) {
|
|
222
|
+
throw new Error(`Couldn't reach TheGitAI to load your models (${unreachableReason(modelsOutcome.reason)}).\nCheck your internet connection and run \`ai\` again.`);
|
|
223
|
+
}
|
|
224
|
+
else {
|
|
225
|
+
throw modelsOutcome.reason;
|
|
200
226
|
}
|
|
201
227
|
let whoami;
|
|
202
|
-
|
|
203
|
-
whoami =
|
|
228
|
+
if (whoamiOutcome.status === 'fulfilled') {
|
|
229
|
+
whoami = whoamiOutcome.value;
|
|
204
230
|
}
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
throw error;
|
|
221
|
-
}
|
|
231
|
+
else if (isTransientNetworkError(whoamiOutcome.reason)) {
|
|
232
|
+
whoami = {
|
|
233
|
+
customer: {
|
|
234
|
+
id: '',
|
|
235
|
+
uuid: '',
|
|
236
|
+
email: authConfig.email,
|
|
237
|
+
customer_type: authConfig.customerType ?? 'USER',
|
|
238
|
+
scopes: [],
|
|
239
|
+
},
|
|
240
|
+
debugUi: { showSessionId: false },
|
|
241
|
+
};
|
|
242
|
+
offlineNotice ??= unreachableReason(whoamiOutcome.reason);
|
|
243
|
+
}
|
|
244
|
+
else {
|
|
245
|
+
throw whoamiOutcome.reason;
|
|
222
246
|
}
|
|
223
247
|
if (offlineNotice) {
|
|
224
248
|
console.error(chalk.yellow(`⚠ Couldn't reach TheGitAI (${offlineNotice}). Starting with cached settings — it will reconnect on your next message.`));
|
package/dist/src/api/auth.js
CHANGED
|
@@ -50,12 +50,14 @@ export async function fetchWhoami({ config, fetchImpl = globalThis.fetch, }) {
|
|
|
50
50
|
const data = await fetchWhoamiResponse({ config, fetchImpl });
|
|
51
51
|
return data.customer;
|
|
52
52
|
}
|
|
53
|
-
export async function fetchWhoamiResponse({ config, fetchImpl = globalThis.fetch, }) {
|
|
53
|
+
export async function fetchWhoamiResponse({ config, fetchImpl = globalThis.fetch, budget = {}, }) {
|
|
54
|
+
const { timeoutMs, ...ladder } = budget;
|
|
54
55
|
const data = (await retryTransient(() => authorizedJson({
|
|
55
56
|
config,
|
|
56
57
|
path: '/v1/auth/whoami',
|
|
57
58
|
fetchImpl,
|
|
58
|
-
|
|
59
|
+
...(timeoutMs == null ? {} : { timeoutMs }),
|
|
60
|
+
}), ladder));
|
|
59
61
|
if (!data?.customer?.email) {
|
|
60
62
|
throw new Error('Server returned an invalid whoami response.');
|
|
61
63
|
}
|
package/dist/src/api/http.js
CHANGED
|
@@ -29,6 +29,14 @@ export function createTraceContext(traceId = createTraceId()) {
|
|
|
29
29
|
};
|
|
30
30
|
}
|
|
31
31
|
export const REQUEST_TIMEOUT_MS = 8000;
|
|
32
|
+
export const STARTUP_REQUEST_TIMEOUT_MS = 3000;
|
|
33
|
+
export const STARTUP_DEADLINE_MS = 10_000;
|
|
34
|
+
export const STARTUP_RETRY_BUDGET = {
|
|
35
|
+
retries: 3,
|
|
36
|
+
baseDelayMs: 200,
|
|
37
|
+
deadlineMs: STARTUP_DEADLINE_MS,
|
|
38
|
+
timeoutMs: STARTUP_REQUEST_TIMEOUT_MS,
|
|
39
|
+
};
|
|
32
40
|
const TRANSIENT_NETWORK_CODES = new Set([
|
|
33
41
|
'ECONNRESET',
|
|
34
42
|
'ECONNREFUSED',
|
|
@@ -60,7 +68,9 @@ export function isTransientNetworkError(error) {
|
|
|
60
68
|
}
|
|
61
69
|
return false;
|
|
62
70
|
}
|
|
63
|
-
export async function retryTransient(run, { retries = 2, baseDelayMs = 400 } = {}) {
|
|
71
|
+
export async function retryTransient(run, { retries = 2, baseDelayMs = 400, deadlineMs, now = () => Date.now(), } = {}) {
|
|
72
|
+
const startedAt = now();
|
|
73
|
+
const remainingMs = () => deadlineMs == null ? Infinity : deadlineMs - (now() - startedAt);
|
|
64
74
|
let attempt = 0;
|
|
65
75
|
for (;;) {
|
|
66
76
|
try {
|
|
@@ -70,6 +80,8 @@ export async function retryTransient(run, { retries = 2, baseDelayMs = 400 } = {
|
|
|
70
80
|
if (attempt >= retries || !isTransientNetworkError(error))
|
|
71
81
|
throw error;
|
|
72
82
|
const delayMs = baseDelayMs * 2 ** attempt;
|
|
83
|
+
if (remainingMs() <= delayMs)
|
|
84
|
+
throw error;
|
|
73
85
|
attempt += 1;
|
|
74
86
|
await new Promise((resolve) => setTimeout(resolve, delayMs));
|
|
75
87
|
}
|
package/dist/src/api/models.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { existsSync, mkdirSync, readFileSync, writeFileSync, } from 'node:fs';
|
|
1
|
+
import { existsSync, mkdirSync, readFileSync, renameSync, rmSync, writeFileSync, } from 'node:fs';
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
import { getClientStateDir } from '../client-state.js';
|
|
4
4
|
import { REQUEST_TIMEOUT_MS, ServerApiError, createTraceContext, failureCode, failureMessage, normalizeServerUrl, readJsonResponse, retryTransient, } from './http.js';
|
|
@@ -58,12 +58,21 @@ export function selectCacheForServer(cached, serverUrl) {
|
|
|
58
58
|
export function writeCachedServerModels(cache, env = process.env) {
|
|
59
59
|
const filePath = getModelsCachePath(env);
|
|
60
60
|
mkdirSync(path.dirname(filePath), { recursive: true, mode: 0o700 });
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
61
|
+
const tempPath = `${filePath}.${process.pid}.tmp`;
|
|
62
|
+
try {
|
|
63
|
+
writeFileSync(tempPath, `${JSON.stringify(cache, null, 2)}\n`, {
|
|
64
|
+
encoding: 'utf8',
|
|
65
|
+
mode: 0o600,
|
|
66
|
+
});
|
|
67
|
+
renameSync(tempPath, filePath);
|
|
68
|
+
}
|
|
69
|
+
catch (error) {
|
|
70
|
+
rmSync(tempPath, { force: true });
|
|
71
|
+
throw error;
|
|
72
|
+
}
|
|
65
73
|
}
|
|
66
|
-
export async function fetchServerModels({ config, fetchImpl = globalThis.fetch, }) {
|
|
74
|
+
export async function fetchServerModels({ config, fetchImpl = globalThis.fetch, budget = {}, }) {
|
|
75
|
+
const { timeoutMs = REQUEST_TIMEOUT_MS, ...ladder } = budget;
|
|
67
76
|
return retryTransient(async () => {
|
|
68
77
|
const trace = createTraceContext();
|
|
69
78
|
const response = await fetchImpl(`${normalizeServerUrl(config.serverUrl)}/v1/models`, {
|
|
@@ -71,7 +80,7 @@ export async function fetchServerModels({ config, fetchImpl = globalThis.fetch,
|
|
|
71
80
|
authorization: `Bearer ${config.token}`,
|
|
72
81
|
...trace.headers,
|
|
73
82
|
},
|
|
74
|
-
signal: AbortSignal.timeout(
|
|
83
|
+
signal: AbortSignal.timeout(timeoutMs),
|
|
75
84
|
});
|
|
76
85
|
const data = (await readJsonResponse(response));
|
|
77
86
|
if (!response.ok) {
|
|
@@ -84,7 +93,7 @@ export async function fetchServerModels({ config, fetchImpl = globalThis.fetch,
|
|
|
84
93
|
throw new Error('Server returned an invalid model list.');
|
|
85
94
|
}
|
|
86
95
|
return { models };
|
|
87
|
-
});
|
|
96
|
+
}, ladder);
|
|
88
97
|
}
|
|
89
98
|
export function selectServerModel({ requestedModelId, cached, serverModels, }) {
|
|
90
99
|
const supportedIds = new Set(serverModels.models.map((model) => model.id));
|
package/dist/src/ui/repl.js
CHANGED
|
@@ -504,6 +504,8 @@ function formatToolResultState(result) {
|
|
|
504
504
|
return 'Blocked';
|
|
505
505
|
if (result?.ok === true)
|
|
506
506
|
return 'OK';
|
|
507
|
+
if (Number.isInteger(result?.httpStatus))
|
|
508
|
+
return String(result.httpStatus);
|
|
507
509
|
return 'Failed';
|
|
508
510
|
}
|
|
509
511
|
function formatRunCommandResultState(result) {
|
|
@@ -660,7 +662,7 @@ function buildFileChangeEntry(event) {
|
|
|
660
662
|
title: `Edited ${filePath}${summary}`,
|
|
661
663
|
};
|
|
662
664
|
}
|
|
663
|
-
function buildWorkingToolEntry(event) {
|
|
665
|
+
export function buildWorkingToolEntry(event) {
|
|
664
666
|
const { call, result } = event;
|
|
665
667
|
const error = typeof result?.error === 'string' && result.error.trim()
|
|
666
668
|
? `\n${truncate(result.error.trim(), 180)}`
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@thegitai/cli",
|
|
3
|
-
"version": "1.0.0-preview.
|
|
3
|
+
"version": "1.0.0-preview.25",
|
|
4
4
|
"description": "TheGitAI is an AI coding agent for your terminal. It indexes your repository, writes and edits files, runs commands, and builds features with you.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"ai",
|
|
@@ -37,10 +37,10 @@
|
|
|
37
37
|
"@lydell/node-pty-linux-x64": "1.1.0",
|
|
38
38
|
"@lydell/node-pty-win32-arm64": "1.1.0",
|
|
39
39
|
"@lydell/node-pty-win32-x64": "1.1.0",
|
|
40
|
-
"@thegitai/tui-darwin-arm64": "1.0.0-preview.
|
|
41
|
-
"@thegitai/tui-darwin-x64": "1.0.0-preview.
|
|
42
|
-
"@thegitai/tui-linux-x64": "1.0.0-preview.
|
|
43
|
-
"@thegitai/tui-win32-x64": "1.0.0-preview.
|
|
40
|
+
"@thegitai/tui-darwin-arm64": "1.0.0-preview.25",
|
|
41
|
+
"@thegitai/tui-darwin-x64": "1.0.0-preview.25",
|
|
42
|
+
"@thegitai/tui-linux-x64": "1.0.0-preview.25",
|
|
43
|
+
"@thegitai/tui-win32-x64": "1.0.0-preview.25",
|
|
44
44
|
"@vscode/ripgrep": "1.18.0"
|
|
45
45
|
},
|
|
46
46
|
"publishConfig": {
|