@thegitai/cli 1.0.0-preview.3 → 1.0.0-preview.31

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.
Files changed (58) hide show
  1. package/README.md +39 -6
  2. package/dist/bin/ai.js +142 -383
  3. package/dist/src/agent-mode.js +1 -6
  4. package/dist/src/api/auth.js +6 -4
  5. package/dist/src/api/browser-login.js +152 -37
  6. package/dist/src/api/chat.js +258 -38
  7. package/dist/src/api/contracts.js +55 -1
  8. package/dist/src/api/default-host.js +1 -0
  9. package/dist/src/api/http.js +69 -7
  10. package/dist/src/api/models.js +19 -10
  11. package/dist/src/background-jobs.js +2 -2
  12. package/dist/src/cli-args.js +19 -5
  13. package/dist/src/core/clipboard.js +7 -13
  14. package/dist/src/core/image-limits.js +56 -0
  15. package/dist/src/core/image-path-extractor.js +70 -3
  16. package/dist/src/core/session-image-store.js +199 -0
  17. package/dist/src/executor.js +25 -3
  18. package/dist/src/help-text.js +67 -18
  19. package/dist/src/permissions.js +243 -0
  20. package/dist/src/session-safety.js +0 -12
  21. package/dist/src/session-store.js +121 -20
  22. package/dist/src/session.js +14 -3
  23. package/dist/src/signin.js +58 -0
  24. package/dist/src/tool-executor.js +11 -46
  25. package/dist/src/tools/delete-file.js +15 -3
  26. package/dist/src/tools/index.js +13 -10
  27. package/dist/src/tools/patch-file.js +12 -26
  28. package/dist/src/tools/read-image-file.js +85 -0
  29. package/dist/src/tools/replace-document-text.js +28 -18
  30. package/dist/src/tools/restore-checkpoint.js +0 -1
  31. package/dist/src/tools/run-command.js +14 -71
  32. package/dist/src/tools/run-node-script.js +12 -81
  33. package/dist/src/tools/save-generated-image.js +120 -0
  34. package/dist/src/tools/str-replace.js +12 -26
  35. package/dist/src/tools/undo-edit.js +1 -6
  36. package/dist/src/tools/write-file.js +67 -11
  37. package/dist/src/turn-failure-marker.js +11 -0
  38. package/dist/src/ui/prompt-history-store.js +1 -1
  39. package/dist/src/ui/repl.js +649 -164
  40. package/dist/src/ui/tui/bridge.js +10 -0
  41. package/dist/src/ui/tui/build-frame.js +453 -115
  42. package/dist/src/ui/tui/markdown-render.js +81 -73
  43. package/dist/src/ui/tui/shell-input.js +206 -63
  44. package/dist/src/ui/tui/terminal-theme.js +28 -0
  45. package/dist/src/ui/tui/terminal-title.js +3 -0
  46. package/dist/src/ui/tui/terminal-writes.js +48 -0
  47. package/dist/src/ui/tui/text.js +158 -4
  48. package/dist/src/ui/tui/user-input.js +568 -0
  49. package/dist/src/utils.js +9 -0
  50. package/package.json +29 -6
  51. package/dist/src/markdown-renderer.js +0 -112
  52. package/dist/src/project-index.js +0 -221
  53. package/dist/src/tools/code-intel.js +0 -472
  54. package/dist/src/tools/find-symbol.js +0 -70
  55. package/dist/src/tools/hover-symbol.js +0 -95
  56. package/dist/src/tools/list-symbols.js +0 -55
  57. package/dist/src/tools/search-code.js +0 -37
  58. 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
- const DEFAULT_WEBSITE_URL = 'https://thegit.ai';
7
- const DEFAULT_SERVER_URL = 'https://thegit.ai';
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 DEFAULT_WEBSITE_URL.replace(/\/+$/, '');
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
- if (params.redirectUri)
37
- url.searchParams.set('redirect_uri', params.redirectUri);
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 ?? DEFAULT_SERVER_URL);
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
- const timer = setTimeout(() => {
180
+ timer = setTimeout(() => {
102
181
  shutDownServer(server);
103
- reject(new Error('Timed out waiting for the browser login to complete.'));
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 login again.'));
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.onWaiting?.();
148
- const code = await codePromise;
149
- return exchangeCodeForToken({ serverUrl, code, codeVerifier: verifier, fetchImpl });
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
  }