@thegitai/cli 1.0.0-preview.3 → 1.0.0-preview.30
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 +39 -6
- package/dist/bin/ai.js +142 -383
- package/dist/src/agent-mode.js +1 -6
- package/dist/src/api/auth.js +6 -4
- package/dist/src/api/browser-login.js +152 -37
- package/dist/src/api/chat.js +258 -38
- package/dist/src/api/contracts.js +55 -1
- package/dist/src/api/default-host.js +1 -0
- package/dist/src/api/http.js +69 -7
- package/dist/src/api/models.js +19 -10
- package/dist/src/background-jobs.js +2 -2
- package/dist/src/cli-args.js +19 -5
- package/dist/src/core/clipboard.js +7 -13
- package/dist/src/core/image-limits.js +56 -0
- package/dist/src/core/image-path-extractor.js +70 -3
- package/dist/src/core/session-image-store.js +199 -0
- package/dist/src/executor.js +25 -3
- package/dist/src/help-text.js +63 -16
- package/dist/src/permissions.js +243 -0
- package/dist/src/session-safety.js +0 -12
- package/dist/src/session-store.js +121 -20
- package/dist/src/session.js +14 -3
- package/dist/src/signin.js +58 -0
- package/dist/src/tool-executor.js +11 -46
- package/dist/src/tools/delete-file.js +15 -3
- package/dist/src/tools/index.js +13 -10
- package/dist/src/tools/patch-file.js +12 -26
- package/dist/src/tools/read-image-file.js +85 -0
- package/dist/src/tools/replace-document-text.js +28 -18
- package/dist/src/tools/restore-checkpoint.js +0 -1
- package/dist/src/tools/run-command.js +14 -71
- package/dist/src/tools/run-node-script.js +12 -81
- package/dist/src/tools/save-generated-image.js +120 -0
- package/dist/src/tools/str-replace.js +12 -26
- package/dist/src/tools/undo-edit.js +1 -6
- package/dist/src/tools/write-file.js +67 -11
- 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 +610 -164
- package/dist/src/ui/tui/bridge.js +10 -0
- package/dist/src/ui/tui/build-frame.js +452 -115
- package/dist/src/ui/tui/markdown-render.js +81 -73
- package/dist/src/ui/tui/shell-input.js +206 -63
- package/dist/src/ui/tui/terminal-theme.js +28 -0
- package/dist/src/ui/tui/terminal-title.js +3 -0
- package/dist/src/ui/tui/terminal-writes.js +48 -0
- package/dist/src/ui/tui/text.js +158 -4
- package/dist/src/ui/tui/user-input.js +568 -0
- package/dist/src/utils.js +9 -0
- package/package.json +29 -6
- package/dist/src/markdown-renderer.js +0 -112
- package/dist/src/project-index.js +0 -221
- package/dist/src/tools/code-intel.js +0 -472
- package/dist/src/tools/find-symbol.js +0 -70
- package/dist/src/tools/hover-symbol.js +0 -95
- package/dist/src/tools/list-symbols.js +0 -55
- package/dist/src/tools/search-code.js +0 -37
- package/dist/src/tools/signature-help.js +0 -118
|
@@ -1,17 +1,31 @@
|
|
|
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';
|
|
6
|
-
|
|
7
|
-
const
|
|
8
|
-
const DEFAULT_TIMEOUT_MS = 5 * 60 * 1000;
|
|
7
|
+
import { ServerApiError, createTraceContext, failureCode, failureMessage, normalizeServerUrl, readJsonResponse, } from './http.js';
|
|
8
|
+
import { DEFAULT_THEGITAI_HOST } from './default-host.js';
|
|
9
|
+
const DEFAULT_TIMEOUT_MS = 10 * 60 * 1000;
|
|
9
10
|
function shutDownServer(server) {
|
|
10
11
|
server.closeAllConnections?.();
|
|
11
12
|
server.close();
|
|
12
13
|
}
|
|
14
|
+
export class SignInCancelledError extends Error {
|
|
15
|
+
constructor() {
|
|
16
|
+
super('Sign-in cancelled.');
|
|
17
|
+
this.name = 'SignInCancelledError';
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
export function isSignInCancelled(error) {
|
|
21
|
+
return error instanceof SignInCancelledError;
|
|
22
|
+
}
|
|
23
|
+
function isAbortError(error) {
|
|
24
|
+
const err = error;
|
|
25
|
+
return err?.name === 'AbortError' || err?.code === 'ABORT_ERR';
|
|
26
|
+
}
|
|
13
27
|
export function resolveWebsiteUrl() {
|
|
14
|
-
return
|
|
28
|
+
return DEFAULT_THEGITAI_HOST.replace(/\/+$/, '');
|
|
15
29
|
}
|
|
16
30
|
function defaultDeviceName() {
|
|
17
31
|
try {
|
|
@@ -21,6 +35,73 @@ function defaultDeviceName() {
|
|
|
21
35
|
return os.hostname();
|
|
22
36
|
}
|
|
23
37
|
}
|
|
38
|
+
function readLinuxOsPrettyName() {
|
|
39
|
+
try {
|
|
40
|
+
const content = readFileSync('/etc/os-release', 'utf8');
|
|
41
|
+
return content.match(/^PRETTY_NAME="?([^"\n]*)"?$/m)?.[1]?.trim() ?? '';
|
|
42
|
+
}
|
|
43
|
+
catch {
|
|
44
|
+
return '';
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
function macArchLabel() {
|
|
48
|
+
try {
|
|
49
|
+
if (process.arch === 'arm64')
|
|
50
|
+
return 'Apple Silicon';
|
|
51
|
+
if (os.cpus().some((cpu) => cpu.model.includes('Apple'))) {
|
|
52
|
+
return 'Apple Silicon';
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
catch {
|
|
56
|
+
}
|
|
57
|
+
return 'Intel';
|
|
58
|
+
}
|
|
59
|
+
export function describeOperatingSystem() {
|
|
60
|
+
try {
|
|
61
|
+
if (process.platform === 'linux') {
|
|
62
|
+
const base = readLinuxOsPrettyName() || `Linux ${os.release()}`;
|
|
63
|
+
return process.arch === 'arm64' ? `${base}, ARM64` : base;
|
|
64
|
+
}
|
|
65
|
+
if (process.platform === 'darwin') {
|
|
66
|
+
let version = '';
|
|
67
|
+
try {
|
|
68
|
+
version = execSync('sw_vers -productVersion', {
|
|
69
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
70
|
+
})
|
|
71
|
+
.toString()
|
|
72
|
+
.trim();
|
|
73
|
+
}
|
|
74
|
+
catch {
|
|
75
|
+
}
|
|
76
|
+
return `${version ? `macOS ${version}` : 'macOS'}, ${macArchLabel()}`;
|
|
77
|
+
}
|
|
78
|
+
if (process.platform === 'win32') {
|
|
79
|
+
let label = '';
|
|
80
|
+
try {
|
|
81
|
+
label = os.version();
|
|
82
|
+
}
|
|
83
|
+
catch {
|
|
84
|
+
}
|
|
85
|
+
const build = Number(os.release().split('.')[2] ?? '0');
|
|
86
|
+
if (build >= 22000)
|
|
87
|
+
label = label.replace(/Windows 10/i, 'Windows 11');
|
|
88
|
+
const base = label || `Windows ${os.release()}`;
|
|
89
|
+
return process.arch === 'arm64' ? `${base}, ARM64` : base;
|
|
90
|
+
}
|
|
91
|
+
return `${process.platform} ${os.release()}`;
|
|
92
|
+
}
|
|
93
|
+
catch {
|
|
94
|
+
return process.platform;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
export function withOperatingSystemInfo(name) {
|
|
98
|
+
const trimmed = name.trim();
|
|
99
|
+
const osLabel = describeOperatingSystem();
|
|
100
|
+
const combined = osLabel && !trimmed.includes(osLabel)
|
|
101
|
+
? `${trimmed} (${osLabel})`
|
|
102
|
+
: trimmed;
|
|
103
|
+
return combined.slice(0, 180);
|
|
104
|
+
}
|
|
24
105
|
export function generatePkce() {
|
|
25
106
|
const verifier = crypto.randomBytes(32).toString('base64url');
|
|
26
107
|
const challenge = crypto
|
|
@@ -33,12 +114,8 @@ function buildAuthUrl(websiteUrl, params) {
|
|
|
33
114
|
const url = new URL(`${websiteUrl}/cli-auth`);
|
|
34
115
|
url.searchParams.set('code_challenge', params.codeChallenge);
|
|
35
116
|
url.searchParams.set('device_name', params.deviceName);
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
if (params.state)
|
|
39
|
-
url.searchParams.set('state', params.state);
|
|
40
|
-
if (params.paste)
|
|
41
|
-
url.searchParams.set('mode', 'paste');
|
|
117
|
+
url.searchParams.set('redirect_uri', params.redirectUri);
|
|
118
|
+
url.searchParams.set('state', params.state);
|
|
42
119
|
return url.toString();
|
|
43
120
|
}
|
|
44
121
|
const RESULT_PAGE = (heading, detail) => `<!doctype html><html><head><meta charset="utf-8"><title>TheGitAI CLI</title>` +
|
|
@@ -56,7 +133,7 @@ async function exchangeCodeForToken({ serverUrl, code, codeVerifier, fetchImpl,
|
|
|
56
133
|
});
|
|
57
134
|
const data = (await readJsonResponse(response));
|
|
58
135
|
if (!response.ok) {
|
|
59
|
-
throw new ServerApiError(failureMessage(data, response.status), response.status, trace.traceId);
|
|
136
|
+
throw new ServerApiError(failureMessage(data, response.status), response.status, trace.traceId, failureCode(data));
|
|
60
137
|
}
|
|
61
138
|
const token = String(data?.token ?? '').trim();
|
|
62
139
|
const customer = data?.customer;
|
|
@@ -71,36 +148,38 @@ async function exchangeCodeForToken({ serverUrl, code, codeVerifier, fetchImpl,
|
|
|
71
148
|
customer,
|
|
72
149
|
};
|
|
73
150
|
}
|
|
151
|
+
async function readPastedResult({ promptCode, signal, serverUrl, codeVerifier, fetchImpl, onPasteRejected, }) {
|
|
152
|
+
while (!signal.aborted) {
|
|
153
|
+
const code = (await promptCode(signal)).trim();
|
|
154
|
+
if (code) {
|
|
155
|
+
try {
|
|
156
|
+
return await exchangeCodeForToken({ serverUrl, code, codeVerifier, fetchImpl });
|
|
157
|
+
}
|
|
158
|
+
catch (error) {
|
|
159
|
+
if (signal.aborted)
|
|
160
|
+
break;
|
|
161
|
+
onPasteRejected?.(error.message);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
await new Promise((resolve) => setImmediate(resolve));
|
|
165
|
+
}
|
|
166
|
+
return await new Promise(() => { });
|
|
167
|
+
}
|
|
74
168
|
export async function loginViaBrowser(options) {
|
|
75
|
-
const serverUrl = normalizeServerUrl(options.serverUrl ??
|
|
169
|
+
const serverUrl = normalizeServerUrl(options.serverUrl ?? DEFAULT_THEGITAI_HOST);
|
|
76
170
|
const websiteUrl = resolveWebsiteUrl();
|
|
77
171
|
const fetchImpl = options.fetchImpl ?? globalThis.fetch;
|
|
78
172
|
const openBrowser = options.openBrowser ?? openUrl;
|
|
79
173
|
const onUrl = options.onUrl ?? (() => { });
|
|
80
|
-
const deviceName = options.deviceName ?? defaultDeviceName();
|
|
174
|
+
const deviceName = withOperatingSystemInfo(options.deviceName ?? defaultDeviceName());
|
|
81
175
|
const { verifier, challenge } = generatePkce();
|
|
82
|
-
if (options.noBrowser) {
|
|
83
|
-
const authUrl = buildAuthUrl(websiteUrl, {
|
|
84
|
-
codeChallenge: challenge,
|
|
85
|
-
deviceName,
|
|
86
|
-
paste: true,
|
|
87
|
-
});
|
|
88
|
-
onUrl(authUrl);
|
|
89
|
-
if (!options.promptCode) {
|
|
90
|
-
throw new Error('No way to read the authorization code in this context.');
|
|
91
|
-
}
|
|
92
|
-
const code = (await options.promptCode()).trim();
|
|
93
|
-
if (!code) {
|
|
94
|
-
throw new Error('No authorization code was entered.');
|
|
95
|
-
}
|
|
96
|
-
return exchangeCodeForToken({ serverUrl, code, codeVerifier: verifier, fetchImpl });
|
|
97
|
-
}
|
|
98
176
|
const state = crypto.randomBytes(16).toString('base64url');
|
|
99
177
|
const server = http.createServer();
|
|
178
|
+
let timer;
|
|
100
179
|
const codePromise = new Promise((resolve, reject) => {
|
|
101
|
-
|
|
180
|
+
timer = setTimeout(() => {
|
|
102
181
|
shutDownServer(server);
|
|
103
|
-
reject(new Error('
|
|
182
|
+
reject(new Error('Sign-in timed out. Authorization codes last 10 minutes — run `ai` again to start over.'));
|
|
104
183
|
}, options.timeoutMs ?? DEFAULT_TIMEOUT_MS);
|
|
105
184
|
server.on('request', (req, res) => {
|
|
106
185
|
const requestUrl = new URL(req.url ?? '/', 'http://127.0.0.1');
|
|
@@ -113,7 +192,7 @@ export async function loginViaBrowser(options) {
|
|
|
113
192
|
const returnedState = requestUrl.searchParams.get('state') ?? '';
|
|
114
193
|
if (!code || returnedState !== state) {
|
|
115
194
|
res.writeHead(400, { 'content-type': 'text/html', connection: 'close' });
|
|
116
|
-
res.end(RESULT_PAGE('Login failed', 'The request could not be verified. Please run ai
|
|
195
|
+
res.end(RESULT_PAGE('Login failed', 'The request could not be verified. Please run `ai` again.'));
|
|
117
196
|
clearTimeout(timer);
|
|
118
197
|
shutDownServer(server);
|
|
119
198
|
reject(new Error('The login callback could not be verified.'));
|
|
@@ -143,8 +222,44 @@ export async function loginViaBrowser(options) {
|
|
|
143
222
|
state,
|
|
144
223
|
});
|
|
145
224
|
onUrl(authUrl);
|
|
146
|
-
await openBrowser(authUrl).catch(() => false);
|
|
147
|
-
options.
|
|
148
|
-
|
|
149
|
-
|
|
225
|
+
const opened = await openBrowser(authUrl).catch(() => false);
|
|
226
|
+
options.onBrowserOpen?.(opened);
|
|
227
|
+
let raceSettled = false;
|
|
228
|
+
const untilSettled = (promise) => promise.catch((error) => {
|
|
229
|
+
if (raceSettled)
|
|
230
|
+
return new Promise(() => { });
|
|
231
|
+
throw error;
|
|
232
|
+
});
|
|
233
|
+
const pasteAbort = new AbortController();
|
|
234
|
+
const routes = [
|
|
235
|
+
untilSettled(codePromise.then((code) => exchangeCodeForToken({ serverUrl, code, codeVerifier: verifier, fetchImpl }))),
|
|
236
|
+
];
|
|
237
|
+
if (options.promptCode) {
|
|
238
|
+
const pasted = readPastedResult({
|
|
239
|
+
promptCode: options.promptCode,
|
|
240
|
+
signal: pasteAbort.signal,
|
|
241
|
+
serverUrl,
|
|
242
|
+
codeVerifier: verifier,
|
|
243
|
+
fetchImpl,
|
|
244
|
+
onPasteRejected: options.onPasteRejected,
|
|
245
|
+
}).catch((error) => {
|
|
246
|
+
if (!raceSettled && isAbortError(error))
|
|
247
|
+
throw new SignInCancelledError();
|
|
248
|
+
if (!raceSettled) {
|
|
249
|
+
return new Promise(() => { });
|
|
250
|
+
}
|
|
251
|
+
throw error;
|
|
252
|
+
});
|
|
253
|
+
routes.push(untilSettled(pasted));
|
|
254
|
+
}
|
|
255
|
+
try {
|
|
256
|
+
return await Promise.race(routes);
|
|
257
|
+
}
|
|
258
|
+
finally {
|
|
259
|
+
raceSettled = true;
|
|
260
|
+
pasteAbort.abort();
|
|
261
|
+
if (timer)
|
|
262
|
+
clearTimeout(timer);
|
|
263
|
+
shutDownServer(server);
|
|
264
|
+
}
|
|
150
265
|
}
|