@thegitai/cli 1.0.0-beta.1 → 1.0.0-beta.10
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 +4 -3
- package/dist/bin/ai.js +18 -61
- package/dist/parsers/NOTICE +18 -0
- package/dist/src/api/auth.js +4 -2
- package/dist/src/api/browser-login.js +10 -28
- package/dist/src/api/chat.js +20 -9
- package/dist/src/api/http.js +32 -3
- package/dist/src/api/models.js +4 -2
- package/dist/src/artifact-policy.js +9 -0
- package/dist/src/cli-args.js +65 -0
- package/dist/src/client-environment.js +127 -0
- package/dist/src/colors.js +59 -0
- package/dist/src/core/clipboard.js +56 -0
- package/dist/src/edit-journal.js +39 -6
- package/dist/src/executor.js +28 -5
- package/dist/src/help-text.js +15 -1
- package/dist/src/markdown-renderer.js +1 -1
- package/dist/src/patcher.js +18 -1
- package/dist/src/scanner.js +67 -17
- package/dist/src/session-safety.js +64 -12
- package/dist/src/tool-executor.js +8 -0
- package/dist/src/tools/delete-file.js +1 -1
- package/dist/src/tools/index.js +2 -0
- package/dist/src/tools/patch-file.js +14 -1
- package/dist/src/tools/path-suggest.js +66 -0
- package/dist/src/tools/read-document.js +14 -3
- package/dist/src/tools/read-file.js +9 -0
- package/dist/src/tools/replace-document-text.js +242 -0
- package/dist/src/tools/restore-checkpoint.js +1 -1
- package/dist/src/tools/run-command.js +1 -1
- package/dist/src/tools/run-node-script.js +1 -1
- package/dist/src/tools/str-replace.js +14 -1
- package/dist/src/tools/undo-edit.js +7 -5
- package/dist/src/tools/write-file.js +14 -1
- package/dist/src/tree-sitter-runtime.js +14 -1
- package/dist/src/ui/repl.js +13 -1
- package/dist/src/ui/tui/bridge.js +2 -2
- package/dist/src/ui/tui/build-frame.js +18 -2
- package/dist/src/ui/tui/shell-input.js +9 -1
- package/dist/src/version.js +35 -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 +15 -16
package/README.md
CHANGED
|
@@ -9,7 +9,7 @@ reads code, edits files, and runs commands with your approval.
|
|
|
9
9
|
npm i -g @thegitai/cli
|
|
10
10
|
```
|
|
11
11
|
|
|
12
|
-
Requires Node.js 24.
|
|
12
|
+
Requires Node.js 24 or newer.
|
|
13
13
|
|
|
14
14
|
## Usage
|
|
15
15
|
|
|
@@ -20,11 +20,12 @@ ai login sign in via your browser (--no-browser for SSH/headless)
|
|
|
20
20
|
ai whoami show the signed-in account
|
|
21
21
|
ai --usage show account usage and reset times
|
|
22
22
|
ai logout sign out
|
|
23
|
+
ai --version print the version and exit
|
|
23
24
|
```
|
|
24
25
|
|
|
25
26
|
Run `ai --help` for sessions, modes, keys, and chat commands.
|
|
26
27
|
|
|
27
28
|
## License
|
|
28
29
|
|
|
29
|
-
Proprietary — see
|
|
30
|
-
copying and redistribution are not permitted.
|
|
30
|
+
Proprietary — see the LICENSE file included in this package. Source is
|
|
31
|
+
visible for inspection; copying and redistribution are not permitted.
|
package/dist/bin/ai.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
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';
|
|
@@ -13,65 +13,13 @@ import { runClientInteractive, shouldUseClientRatatuiShell, } from '../src/ui/re
|
|
|
13
13
|
import { appendPromptToFile } from '../src/ui/prompt-history-store.js';
|
|
14
14
|
import { formatSessionExitNotice } from '../src/session-exit.js';
|
|
15
15
|
import { formatUsageText } from '../src/usage.js';
|
|
16
|
+
import { formatVersionLine } from '../src/version.js';
|
|
17
|
+
import { parseArgs } from '../src/cli-args.js';
|
|
16
18
|
const DEFAULT_SERVER_URL = 'https://thegit.ai';
|
|
17
|
-
const AUTH_COMMANDS = new Set(['login', 'whoami', 'logout']);
|
|
18
19
|
const { auth, chat, models, sessions } = ServerApi;
|
|
19
20
|
function printUsage() {
|
|
20
21
|
console.log(formatCliHelpText({ color: process.stdout.isTTY === true }));
|
|
21
22
|
}
|
|
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
23
|
async function promptText(question, fallback = null) {
|
|
76
24
|
const rl = readline.createInterface({ input, output });
|
|
77
25
|
try {
|
|
@@ -88,17 +36,16 @@ function appendPromptHistory(prompt, env = process.env) {
|
|
|
88
36
|
}
|
|
89
37
|
async function runAuthCommand(command, args) {
|
|
90
38
|
if (command === 'login') {
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
const
|
|
39
|
+
// The public CLI always authenticates against the official TheGitAI host.
|
|
40
|
+
// There is intentionally no server/website override here — internal dev
|
|
41
|
+
// uses private tooling, not a customer-visible runtime override path.
|
|
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:'));
|
|
@@ -340,11 +287,21 @@ async function mainInteractive({ authConfig, projectIndex, serverModels, serverS
|
|
|
340
287
|
}
|
|
341
288
|
}
|
|
342
289
|
export async function main() {
|
|
343
|
-
const { autoYes, help, usage, command, commandArgs, session: sessionIdentifier, listSessions, prompt, } = parseArgs(process.argv);
|
|
290
|
+
const { autoYes, help, version, usage, command, commandArgs, session: sessionIdentifier, listSessions, unknownOption, prompt, } = parseArgs(process.argv);
|
|
291
|
+
if (version) {
|
|
292
|
+
console.log(formatVersionLine());
|
|
293
|
+
return;
|
|
294
|
+
}
|
|
344
295
|
if (help) {
|
|
345
296
|
printUsage();
|
|
346
297
|
return;
|
|
347
298
|
}
|
|
299
|
+
if (unknownOption) {
|
|
300
|
+
console.error(`Unknown option: ${unknownOption}`);
|
|
301
|
+
console.error("Run 'ai --help' to see available commands and options.");
|
|
302
|
+
process.exitCode = 2;
|
|
303
|
+
return;
|
|
304
|
+
}
|
|
348
305
|
if (command) {
|
|
349
306
|
await runAuthCommand(command, commandArgs);
|
|
350
307
|
return;
|
|
@@ -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/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, } 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) {
|
|
@@ -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,9 +2,8 @@ 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) {
|
|
@@ -13,29 +12,11 @@ function shutDownServer(server) {
|
|
|
13
12
|
server.closeAllConnections?.();
|
|
14
13
|
server.close();
|
|
15
14
|
}
|
|
16
|
-
function
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
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;
|
|
15
|
+
export function resolveWebsiteUrl() {
|
|
16
|
+
// The public CLI always signs in through the official TheGitAI website. There
|
|
17
|
+
// is no override path here so the published package cannot be pointed at a
|
|
18
|
+
// clone host.
|
|
19
|
+
return DEFAULT_WEBSITE_URL.replace(/\/+$/, '');
|
|
39
20
|
}
|
|
40
21
|
function defaultDeviceName() {
|
|
41
22
|
try {
|
|
@@ -73,14 +54,15 @@ const RESULT_PAGE = (heading, detail) => `<!doctype html><html><head><meta chars
|
|
|
73
54
|
`p{color:#9aa0a6}</style></head><body><div class="card"><h1>${heading}</h1>` +
|
|
74
55
|
`<p>${detail}</p></div></body></html>`;
|
|
75
56
|
async function exchangeCodeForToken({ serverUrl, code, codeVerifier, fetchImpl, }) {
|
|
57
|
+
const trace = createTraceContext();
|
|
76
58
|
const response = await fetchImpl(`${serverUrl}/v1/cli/auth/token`, {
|
|
77
59
|
method: 'POST',
|
|
78
|
-
headers: { 'content-type': 'application/json' },
|
|
60
|
+
headers: { 'content-type': 'application/json', ...trace.headers },
|
|
79
61
|
body: JSON.stringify({ code, code_verifier: codeVerifier }),
|
|
80
62
|
});
|
|
81
63
|
const data = (await readJsonResponse(response));
|
|
82
64
|
if (!response.ok) {
|
|
83
|
-
throw new
|
|
65
|
+
throw new ServerApiError(failureMessage(data, response.status), response.status, trace.traceId);
|
|
84
66
|
}
|
|
85
67
|
const token = String(data?.token ?? '').trim();
|
|
86
68
|
const customer = data?.customer;
|
|
@@ -103,7 +85,7 @@ async function exchangeCodeForToken({ serverUrl, code, codeVerifier, fetchImpl,
|
|
|
103
85
|
*/
|
|
104
86
|
export async function loginViaBrowser(options) {
|
|
105
87
|
const serverUrl = normalizeServerUrl(options.serverUrl ?? DEFAULT_SERVER_URL);
|
|
106
|
-
const websiteUrl = resolveWebsiteUrl(
|
|
88
|
+
const websiteUrl = resolveWebsiteUrl();
|
|
107
89
|
const fetchImpl = options.fetchImpl ?? globalThis.fetch;
|
|
108
90
|
const openBrowser = options.openBrowser ?? openUrl;
|
|
109
91
|
const onUrl = options.onUrl ?? (() => { });
|
package/dist/src/api/chat.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { createPromptCheckpoint, sanitizeSessionSafetyForServer, } from '../session-safety.js';
|
|
2
2
|
import { applySessionSnapshot, snapshotFromSession, } from '../session-store.js';
|
|
3
3
|
import { executeLocalToolCall } from '../tool-executor.js';
|
|
4
|
-
import { normalizeServerUrl, readErrorResponse, } from './http.js';
|
|
4
|
+
import { createTraceContext, normalizeServerUrl, readErrorResponse, } from './http.js';
|
|
5
|
+
import { collectClientEnvironment } from '../client-environment.js';
|
|
5
6
|
export class TurnCancelledError extends Error {
|
|
6
7
|
name = 'TurnCancelledError';
|
|
7
8
|
constructor(message = 'Turn cancelled.') {
|
|
@@ -12,10 +13,12 @@ export class ChatTurnFailedError extends Error {
|
|
|
12
13
|
name = 'ChatTurnFailedError';
|
|
13
14
|
category;
|
|
14
15
|
retryable;
|
|
15
|
-
|
|
16
|
-
|
|
16
|
+
traceId;
|
|
17
|
+
constructor(message, category = 'unknown_error', retryable = false, traceId = '') {
|
|
18
|
+
super(traceId ? `${message}\nTrace ID: ${traceId}` : message);
|
|
17
19
|
this.category = category;
|
|
18
20
|
this.retryable = retryable;
|
|
21
|
+
this.traceId = traceId;
|
|
19
22
|
}
|
|
20
23
|
}
|
|
21
24
|
export function isTurnCancelledError(error) {
|
|
@@ -139,17 +142,19 @@ function publicStatusMessage(data) {
|
|
|
139
142
|
return `Running ${toolName} locally...`;
|
|
140
143
|
return null;
|
|
141
144
|
}
|
|
142
|
-
async function postToolResult({ config, turnId, event, result, session, fetchImpl, }) {
|
|
145
|
+
async function postToolResult({ config, turnId, event, result, session, fetchImpl, traceId, }) {
|
|
143
146
|
const payload = {
|
|
144
147
|
toolCallId: event.call.id,
|
|
145
148
|
result,
|
|
146
149
|
toolState: toolStateFromSession(session),
|
|
147
150
|
};
|
|
151
|
+
const trace = createTraceContext(traceId);
|
|
148
152
|
const response = await fetchImpl(`${normalizeServerUrl(config.serverUrl)}/v1/chat/turn/${encodeURIComponent(turnId)}/tool-result`, {
|
|
149
153
|
method: 'POST',
|
|
150
154
|
headers: {
|
|
151
155
|
authorization: `Bearer ${config.token}`,
|
|
152
156
|
'content-type': 'application/json',
|
|
157
|
+
...trace.headers,
|
|
153
158
|
},
|
|
154
159
|
body: JSON.stringify(payload),
|
|
155
160
|
});
|
|
@@ -157,10 +162,10 @@ async function postToolResult({ config, turnId, event, result, session, fetchImp
|
|
|
157
162
|
return;
|
|
158
163
|
}
|
|
159
164
|
if (!response.ok) {
|
|
160
|
-
throw await readErrorResponse(response);
|
|
165
|
+
throw await readErrorResponse(response, trace.traceId);
|
|
161
166
|
}
|
|
162
167
|
}
|
|
163
|
-
async function executeAndPostToolResult({ config, projectIndex, session, event, input, fetchImpl, signal, }) {
|
|
168
|
+
async function executeAndPostToolResult({ config, projectIndex, session, event, input, fetchImpl, signal, traceId, }) {
|
|
164
169
|
const turnId = String(event?.turnId ?? '').trim();
|
|
165
170
|
if (!turnId || !event?.call?.id || !event.call.name) {
|
|
166
171
|
throw new Error('Server emitted an invalid tool-call event.');
|
|
@@ -189,13 +194,14 @@ async function executeAndPostToolResult({ config, projectIndex, session, event,
|
|
|
189
194
|
result: rawResult,
|
|
190
195
|
session,
|
|
191
196
|
fetchImpl,
|
|
197
|
+
traceId,
|
|
192
198
|
});
|
|
193
199
|
}
|
|
194
200
|
finally {
|
|
195
201
|
session.turnState.id = previousTurnId;
|
|
196
202
|
}
|
|
197
203
|
}
|
|
198
|
-
async function consumeTurnStream({ response, config, projectIndex, session, input, fetchImpl, signal, }) {
|
|
204
|
+
async function consumeTurnStream({ response, config, projectIndex, session, input, fetchImpl, signal, traceId, }) {
|
|
199
205
|
if (!response.body) {
|
|
200
206
|
throw new Error('Server returned an empty chat stream.');
|
|
201
207
|
}
|
|
@@ -224,6 +230,7 @@ async function consumeTurnStream({ response, config, projectIndex, session, inpu
|
|
|
224
230
|
input,
|
|
225
231
|
fetchImpl,
|
|
226
232
|
signal,
|
|
233
|
+
traceId,
|
|
227
234
|
});
|
|
228
235
|
return;
|
|
229
236
|
}
|
|
@@ -243,7 +250,7 @@ async function consumeTurnStream({ response, config, projectIndex, session, inpu
|
|
|
243
250
|
if (event.event === 'cancelled') {
|
|
244
251
|
throw new TurnCancelledError(message);
|
|
245
252
|
}
|
|
246
|
-
throw new ChatTurnFailedError(message, typeof event.data?.category === 'string' ? event.data.category : 'unknown_error', Boolean(event.data?.retryable));
|
|
253
|
+
throw new ChatTurnFailedError(message, typeof event.data?.category === 'string' ? event.data.category : 'unknown_error', Boolean(event.data?.retryable), typeof event.data?.traceId === 'string' ? event.data.traceId : traceId);
|
|
247
254
|
}
|
|
248
255
|
}
|
|
249
256
|
while (true) {
|
|
@@ -282,11 +289,13 @@ export async function sendServerUserMessage({ config, projectIndex, session, inp
|
|
|
282
289
|
modelId: session.modelId,
|
|
283
290
|
session: snapshotForServer(session),
|
|
284
291
|
input,
|
|
292
|
+
clientEnvironment: collectClientEnvironment({ env: session.env }),
|
|
285
293
|
imageAttachments,
|
|
286
294
|
maxToolSteps: session.maxToolSteps,
|
|
287
295
|
autoYes: session.autoYes,
|
|
288
296
|
agentMode: session.agentMode,
|
|
289
297
|
};
|
|
298
|
+
const trace = createTraceContext();
|
|
290
299
|
const preTurnHistoryLength = session.history.length;
|
|
291
300
|
const preserveOnAbort = () => preserveCancelledTurnInput(session, input);
|
|
292
301
|
if (signal?.aborted) {
|
|
@@ -302,12 +311,13 @@ export async function sendServerUserMessage({ config, projectIndex, session, inp
|
|
|
302
311
|
accept: 'text/event-stream',
|
|
303
312
|
authorization: `Bearer ${config.token}`,
|
|
304
313
|
'content-type': 'application/json',
|
|
314
|
+
...trace.headers,
|
|
305
315
|
},
|
|
306
316
|
body: JSON.stringify(request),
|
|
307
317
|
signal,
|
|
308
318
|
});
|
|
309
319
|
if (!response.ok) {
|
|
310
|
-
throw await readErrorResponse(response);
|
|
320
|
+
throw await readErrorResponse(response, trace.traceId);
|
|
311
321
|
}
|
|
312
322
|
const result = await consumeTurnStream({
|
|
313
323
|
response,
|
|
@@ -317,6 +327,7 @@ export async function sendServerUserMessage({ config, projectIndex, session, inp
|
|
|
317
327
|
input,
|
|
318
328
|
fetchImpl,
|
|
319
329
|
signal,
|
|
330
|
+
traceId: trace.traceId,
|
|
320
331
|
});
|
|
321
332
|
applySessionSnapshot(session, result.snapshot, { preserveAgentMode: true });
|
|
322
333
|
return {
|
package/dist/src/api/http.js
CHANGED
|
@@ -1,4 +1,31 @@
|
|
|
1
|
+
import { randomUUID } from 'node:crypto';
|
|
1
2
|
const DEFAULT_SERVER_URL = 'https://thegit.ai';
|
|
3
|
+
export const TRACE_ID_HEADER = 'x-thegitai-trace-id';
|
|
4
|
+
export const CLIENT_HEADER = 'x-thegitai-client';
|
|
5
|
+
export const CLIENT_PLATFORM_HEADER = 'x-thegitai-client-platform';
|
|
6
|
+
export class ServerApiError extends Error {
|
|
7
|
+
status;
|
|
8
|
+
traceId;
|
|
9
|
+
constructor(message, status, traceId) {
|
|
10
|
+
super(traceId ? `${message}\nTrace ID: ${traceId}` : message);
|
|
11
|
+
this.name = 'ServerApiError';
|
|
12
|
+
this.status = status;
|
|
13
|
+
this.traceId = traceId;
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
export function createTraceId() {
|
|
17
|
+
return `tr_${randomUUID().replace(/-/g, '')}`;
|
|
18
|
+
}
|
|
19
|
+
export function createTraceContext(traceId = createTraceId()) {
|
|
20
|
+
return {
|
|
21
|
+
traceId,
|
|
22
|
+
headers: {
|
|
23
|
+
[TRACE_ID_HEADER]: traceId,
|
|
24
|
+
[CLIENT_HEADER]: 'cli',
|
|
25
|
+
[CLIENT_PLATFORM_HEADER]: `${process.platform}/${process.arch}`,
|
|
26
|
+
},
|
|
27
|
+
};
|
|
28
|
+
}
|
|
2
29
|
export function normalizeServerUrl(serverUrl) {
|
|
3
30
|
const normalized = String(serverUrl || DEFAULT_SERVER_URL)
|
|
4
31
|
.trim()
|
|
@@ -22,15 +49,17 @@ export async function readJsonResponse(response) {
|
|
|
22
49
|
export function failureMessage(data, status) {
|
|
23
50
|
return String(data?.error?.message ?? data?.message ?? `Request failed with ${status}`);
|
|
24
51
|
}
|
|
25
|
-
export async function readErrorResponse(response) {
|
|
52
|
+
export async function readErrorResponse(response, traceId = response.headers.get(TRACE_ID_HEADER) ?? '') {
|
|
26
53
|
const data = await readJsonResponse(response);
|
|
27
|
-
return new
|
|
54
|
+
return new ServerApiError(failureMessage(data, response.status), response.status, traceId);
|
|
28
55
|
}
|
|
29
56
|
export async function authorizedJson({ config, path, method = 'GET', body = null, headers = {}, fetchImpl = globalThis.fetch, }) {
|
|
57
|
+
const trace = createTraceContext();
|
|
30
58
|
const response = await fetchImpl(`${normalizeServerUrl(config.serverUrl)}${path}`, {
|
|
31
59
|
method,
|
|
32
60
|
headers: {
|
|
33
61
|
authorization: `Bearer ${config.token}`,
|
|
62
|
+
...trace.headers,
|
|
34
63
|
...headers,
|
|
35
64
|
...(body === null ? {} : { 'content-type': 'application/json' }),
|
|
36
65
|
},
|
|
@@ -38,7 +67,7 @@ export async function authorizedJson({ config, path, method = 'GET', body = null
|
|
|
38
67
|
});
|
|
39
68
|
const data = await readJsonResponse(response);
|
|
40
69
|
if (!response.ok) {
|
|
41
|
-
throw new
|
|
70
|
+
throw new ServerApiError(failureMessage(data, response.status), response.status, trace.traceId);
|
|
42
71
|
}
|
|
43
72
|
return data;
|
|
44
73
|
}
|
package/dist/src/api/models.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { existsSync, mkdirSync, readFileSync, writeFileSync, } from 'node:fs';
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
import { getClientStateDir } from '../client-state.js';
|
|
4
|
-
import { failureMessage, normalizeServerUrl, readJsonResponse, } from './http.js';
|
|
4
|
+
import { ServerApiError, createTraceContext, failureMessage, normalizeServerUrl, readJsonResponse, } from './http.js';
|
|
5
5
|
function sanitizeModelInfo(raw) {
|
|
6
6
|
if (!raw || typeof raw !== 'object') {
|
|
7
7
|
return null;
|
|
@@ -54,14 +54,16 @@ export function writeCachedServerModels(cache, env = process.env) {
|
|
|
54
54
|
});
|
|
55
55
|
}
|
|
56
56
|
export async function fetchServerModels({ config, fetchImpl = globalThis.fetch, }) {
|
|
57
|
+
const trace = createTraceContext();
|
|
57
58
|
const response = await fetchImpl(`${normalizeServerUrl(config.serverUrl)}/v1/models`, {
|
|
58
59
|
headers: {
|
|
59
60
|
authorization: `Bearer ${config.token}`,
|
|
61
|
+
...trace.headers,
|
|
60
62
|
},
|
|
61
63
|
});
|
|
62
64
|
const data = (await readJsonResponse(response));
|
|
63
65
|
if (!response.ok) {
|
|
64
|
-
throw new
|
|
66
|
+
throw new ServerApiError(failureMessage(data, response.status), response.status, trace.traceId);
|
|
65
67
|
}
|
|
66
68
|
const models = Array.isArray(data?.models)
|
|
67
69
|
? data.models.map(sanitizeModelInfo).filter(Boolean)
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
export const AUTH_COMMANDS = new Set(['login', 'whoami', 'logout']);
|
|
2
|
+
export function parseArgs(argv) {
|
|
3
|
+
const args = argv.slice(2);
|
|
4
|
+
const firstArg = args[0];
|
|
5
|
+
const command = firstArg && AUTH_COMMANDS.has(firstArg) ? firstArg : null;
|
|
6
|
+
const commandArgs = command ? args.slice(1) : [];
|
|
7
|
+
let autoYes = false;
|
|
8
|
+
let help = false;
|
|
9
|
+
let version = false;
|
|
10
|
+
let usage = false;
|
|
11
|
+
let session = null;
|
|
12
|
+
let listSessions = false;
|
|
13
|
+
let unknownOption = null;
|
|
14
|
+
const promptParts = [];
|
|
15
|
+
for (let i = 0; i < args.length; i++) {
|
|
16
|
+
const arg = args[i];
|
|
17
|
+
if (arg === '--yes' || arg === '-y') {
|
|
18
|
+
autoYes = true;
|
|
19
|
+
continue;
|
|
20
|
+
}
|
|
21
|
+
if ((arg === '--session' || arg === '--resume') && i + 1 < args.length) {
|
|
22
|
+
session = args[i + 1] ?? null;
|
|
23
|
+
i += 1;
|
|
24
|
+
continue;
|
|
25
|
+
}
|
|
26
|
+
if (arg === '--list-sessions') {
|
|
27
|
+
listSessions = true;
|
|
28
|
+
continue;
|
|
29
|
+
}
|
|
30
|
+
if (arg === '--help' || arg === '-h') {
|
|
31
|
+
help = true;
|
|
32
|
+
continue;
|
|
33
|
+
}
|
|
34
|
+
if (arg === '--version' || arg === '-v') {
|
|
35
|
+
version = true;
|
|
36
|
+
continue;
|
|
37
|
+
}
|
|
38
|
+
if (arg === '--usage') {
|
|
39
|
+
usage = true;
|
|
40
|
+
continue;
|
|
41
|
+
}
|
|
42
|
+
// An unrecognized dashed token is a mistyped flag, not prompt text. Without
|
|
43
|
+
// an auth subcommand (whose flags are parsed separately) it would otherwise
|
|
44
|
+
// be swept into the prompt and silently start a billable session. Flag the
|
|
45
|
+
// first one so the caller can fail fast instead. Quoted prompts are a single
|
|
46
|
+
// argv entry with spaces, so they never look like a bare option here.
|
|
47
|
+
if (command === null && unknownOption === null && /^-/.test(arg)) {
|
|
48
|
+
unknownOption = arg;
|
|
49
|
+
continue;
|
|
50
|
+
}
|
|
51
|
+
promptParts.push(arg);
|
|
52
|
+
}
|
|
53
|
+
return {
|
|
54
|
+
command,
|
|
55
|
+
commandArgs,
|
|
56
|
+
autoYes,
|
|
57
|
+
help,
|
|
58
|
+
version,
|
|
59
|
+
usage,
|
|
60
|
+
session,
|
|
61
|
+
listSessions,
|
|
62
|
+
unknownOption,
|
|
63
|
+
prompt: promptParts.join(' ').trim(),
|
|
64
|
+
};
|
|
65
|
+
}
|