@thegitai/cli 1.0.0-beta.1
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/LICENSE +20 -0
- package/README.md +30 -0
- package/dist/bin/ai.js +438 -0
- package/dist/parsers/tree-sitter-c-sharp.wasm +0 -0
- package/dist/parsers/tree-sitter-c.wasm +0 -0
- package/dist/parsers/tree-sitter-cpp.wasm +0 -0
- package/dist/parsers/tree-sitter-css.wasm +0 -0
- package/dist/parsers/tree-sitter-go.wasm +0 -0
- package/dist/parsers/tree-sitter-html.wasm +0 -0
- package/dist/parsers/tree-sitter-java.wasm +0 -0
- package/dist/parsers/tree-sitter-javascript.wasm +0 -0
- package/dist/parsers/tree-sitter-objc.wasm +0 -0
- package/dist/parsers/tree-sitter-php.wasm +0 -0
- package/dist/parsers/tree-sitter-python.wasm +0 -0
- package/dist/parsers/tree-sitter-ruby.wasm +0 -0
- package/dist/parsers/tree-sitter-rust.wasm +0 -0
- package/dist/parsers/tree-sitter-tsx.wasm +0 -0
- package/dist/parsers/tree-sitter-typescript.wasm +0 -0
- package/dist/src/agent-mode.js +142 -0
- package/dist/src/api/auth.js +81 -0
- package/dist/src/api/browser-login.js +184 -0
- package/dist/src/api/chat.js +346 -0
- package/dist/src/api/contracts.js +1 -0
- package/dist/src/api/http.js +44 -0
- package/dist/src/api/index.js +11 -0
- package/dist/src/api/models.js +110 -0
- package/dist/src/api/sessions.js +72 -0
- package/dist/src/artifact-policy.js +207 -0
- package/dist/src/client-state.js +14 -0
- package/dist/src/core/clipboard.js +208 -0
- package/dist/src/core/open-url.js +32 -0
- package/dist/src/edit-journal.js +133 -0
- package/dist/src/executor.js +924 -0
- package/dist/src/extractors/cpp.js +18 -0
- package/dist/src/extractors/csharp.js +16 -0
- package/dist/src/extractors/css.js +12 -0
- package/dist/src/extractors/go.js +27 -0
- package/dist/src/extractors/index.js +52 -0
- package/dist/src/extractors/java.js +14 -0
- package/dist/src/extractors/javascript.js +33 -0
- package/dist/src/extractors/objc.js +14 -0
- package/dist/src/extractors/php.js +20 -0
- package/dist/src/extractors/python.js +11 -0
- package/dist/src/extractors/ruby.js +13 -0
- package/dist/src/extractors/rust.js +17 -0
- package/dist/src/extractors/utils.js +58 -0
- package/dist/src/help-text.js +125 -0
- package/dist/src/markdown-renderer.js +112 -0
- package/dist/src/patcher.js +279 -0
- package/dist/src/project-index.js +221 -0
- package/dist/src/repo-map-languages.js +100 -0
- package/dist/src/runtime-mode.js +35 -0
- package/dist/src/scanner.js +362 -0
- package/dist/src/secret-preview.js +137 -0
- package/dist/src/session-exit.js +17 -0
- package/dist/src/session-safety.js +1012 -0
- package/dist/src/session-store.js +266 -0
- package/dist/src/session.js +93 -0
- package/dist/src/tool-executor.js +188 -0
- package/dist/src/tools/code-intel.js +472 -0
- package/dist/src/tools/delete-file.js +27 -0
- package/dist/src/tools/exec-utils.js +17 -0
- package/dist/src/tools/find-symbol.js +70 -0
- package/dist/src/tools/get-diagnostics.js +22 -0
- package/dist/src/tools/grep-code.js +331 -0
- package/dist/src/tools/hover-symbol.js +95 -0
- package/dist/src/tools/index.js +73 -0
- package/dist/src/tools/list-checkpoints.js +11 -0
- package/dist/src/tools/list-directories.js +16 -0
- package/dist/src/tools/list-files.js +13 -0
- package/dist/src/tools/list-session-edits.js +9 -0
- package/dist/src/tools/list-symbols.js +55 -0
- package/dist/src/tools/patch-file.js +88 -0
- package/dist/src/tools/path-listing.js +83 -0
- package/dist/src/tools/read-document.js +111 -0
- package/dist/src/tools/read-file.js +109 -0
- package/dist/src/tools/restore-checkpoint.js +100 -0
- package/dist/src/tools/ripgrep.js +29 -0
- package/dist/src/tools/run-command.js +94 -0
- package/dist/src/tools/run-node-script.js +210 -0
- package/dist/src/tools/search-code.js +37 -0
- package/dist/src/tools/shell-diagnostics.js +707 -0
- package/dist/src/tools/signature-help.js +118 -0
- package/dist/src/tools/str-replace.js +193 -0
- package/dist/src/tools/types.js +1 -0
- package/dist/src/tools/undo-edit.js +202 -0
- package/dist/src/tools/write-file.js +59 -0
- package/dist/src/tree-sitter-runtime.js +135 -0
- package/dist/src/types.js +1 -0
- package/dist/src/ui/paste-collapse.js +22 -0
- package/dist/src/ui/prompt-history-store.js +96 -0
- package/dist/src/ui/repl.js +2238 -0
- package/dist/src/ui/tui/bridge.js +175 -0
- package/dist/src/ui/tui/build-frame.js +718 -0
- package/dist/src/ui/tui/markdown-render.js +455 -0
- package/dist/src/ui/tui/shell-input.js +488 -0
- package/dist/src/ui/tui/text.js +30 -0
- package/dist/src/ui/tui/types.js +1 -0
- package/dist/src/usage.js +47 -0
- package/dist/src/utils.js +38 -0
- package/package.json +38 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
TheGitAI Client — Source-Visible Proprietary License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 TheGitAI
|
|
4
|
+
|
|
5
|
+
This source code is made available for inspection and trust verification only.
|
|
6
|
+
The following rights are NOT granted:
|
|
7
|
+
|
|
8
|
+
- Copying or redistribution of this code, in whole or in part
|
|
9
|
+
- Commercial use or resale
|
|
10
|
+
- Derivative works or modifications for distribution
|
|
11
|
+
- Sublicensing
|
|
12
|
+
|
|
13
|
+
SECURITY NOTICE: This client is untrusted for enforcement purposes. All
|
|
14
|
+
authentication, authorization, and security-sensitive decisions are enforced
|
|
15
|
+
server-side. The server's enforcement is authoritative regardless of any
|
|
16
|
+
client-side values.
|
|
17
|
+
|
|
18
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
19
|
+
IMPLIED. IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR
|
|
20
|
+
OTHER LIABILITY ARISING FROM THE USE OF THIS SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# TheGitAI
|
|
2
|
+
|
|
3
|
+
Interactive terminal coding agent. Talk to your repo in plain English — it
|
|
4
|
+
reads code, edits files, and runs commands with your approval.
|
|
5
|
+
|
|
6
|
+
## Install
|
|
7
|
+
|
|
8
|
+
```bash
|
|
9
|
+
npm i -g @thegitai/cli
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
Requires Node.js 24.
|
|
13
|
+
|
|
14
|
+
## Usage
|
|
15
|
+
|
|
16
|
+
```text
|
|
17
|
+
ai start an interactive session in the current repo
|
|
18
|
+
ai "<request>" start a session with <request> as the first message
|
|
19
|
+
ai login sign in via your browser (--no-browser for SSH/headless)
|
|
20
|
+
ai whoami show the signed-in account
|
|
21
|
+
ai --usage show account usage and reset times
|
|
22
|
+
ai logout sign out
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
Run `ai --help` for sessions, modes, keys, and chat commands.
|
|
26
|
+
|
|
27
|
+
## License
|
|
28
|
+
|
|
29
|
+
Proprietary — see [LICENSE](./LICENSE). Source is visible for inspection;
|
|
30
|
+
copying and redistribution are not permitted.
|
package/dist/bin/ai.js
ADDED
|
@@ -0,0 +1,438 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import chalk from 'chalk';
|
|
3
|
+
import { stdin as input, stdout as output } from 'node:process';
|
|
4
|
+
import readline from 'node:readline/promises';
|
|
5
|
+
import { ServerApi } from '../src/api/index.js';
|
|
6
|
+
import { loginViaBrowser } from '../src/api/browser-login.js';
|
|
7
|
+
import { formatCliHelpText, formatInteractiveHelpText, } from '../src/help-text.js';
|
|
8
|
+
import { renderMarkdownForTerminal } from '../src/markdown-renderer.js';
|
|
9
|
+
import { createIndex } from '../src/project-index.js';
|
|
10
|
+
import { createSession, clearConversation } from '../src/session.js';
|
|
11
|
+
import { applySessionSnapshot, listSessionMetadata, loadSessionSnapshot, saveSessionState, } from '../src/session-store.js';
|
|
12
|
+
import { runClientInteractive, shouldUseClientRatatuiShell, } from '../src/ui/repl.js';
|
|
13
|
+
import { appendPromptToFile } from '../src/ui/prompt-history-store.js';
|
|
14
|
+
import { formatSessionExitNotice } from '../src/session-exit.js';
|
|
15
|
+
import { formatUsageText } from '../src/usage.js';
|
|
16
|
+
const DEFAULT_SERVER_URL = 'https://thegit.ai';
|
|
17
|
+
const AUTH_COMMANDS = new Set(['login', 'whoami', 'logout']);
|
|
18
|
+
const { auth, chat, models, sessions } = ServerApi;
|
|
19
|
+
function printUsage() {
|
|
20
|
+
console.log(formatCliHelpText({ color: process.stdout.isTTY === true }));
|
|
21
|
+
}
|
|
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
|
+
async function promptText(question, fallback = null) {
|
|
76
|
+
const rl = readline.createInterface({ input, output });
|
|
77
|
+
try {
|
|
78
|
+
const suffix = fallback ? ` (${fallback})` : '';
|
|
79
|
+
const answer = await rl.question(`${question}${suffix}: `);
|
|
80
|
+
return answer.trim() || fallback || '';
|
|
81
|
+
}
|
|
82
|
+
finally {
|
|
83
|
+
rl.close();
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
function appendPromptHistory(prompt, env = process.env) {
|
|
87
|
+
appendPromptToFile(prompt, env);
|
|
88
|
+
}
|
|
89
|
+
async function runAuthCommand(command, args) {
|
|
90
|
+
if (command === 'login') {
|
|
91
|
+
const serverUrl = commandFlagValue(args, '--server') ??
|
|
92
|
+
process.env.THEGITAI_SERVER_URL ??
|
|
93
|
+
DEFAULT_SERVER_URL;
|
|
94
|
+
const websiteUrl = commandFlagValue(args, '--website') ?? undefined;
|
|
95
|
+
const noBrowser = args.includes('--no-browser');
|
|
96
|
+
console.log(chalk.dim(noBrowser
|
|
97
|
+
? 'Sign in on the website, then paste the authorization code here.'
|
|
98
|
+
: 'Opening your browser to sign in…'));
|
|
99
|
+
const result = await loginViaBrowser({
|
|
100
|
+
serverUrl,
|
|
101
|
+
websiteUrl,
|
|
102
|
+
noBrowser,
|
|
103
|
+
onUrl: (url) => {
|
|
104
|
+
console.log(chalk.dim(noBrowser ? 'Open this URL to sign in:' : 'If your browser did not open, visit:'));
|
|
105
|
+
console.log(` ${url}`);
|
|
106
|
+
},
|
|
107
|
+
onWaiting: () => console.log(chalk.dim('Waiting for you to finish signing in…')),
|
|
108
|
+
promptCode: noBrowser
|
|
109
|
+
? () => promptText('Paste the authorization code')
|
|
110
|
+
: undefined,
|
|
111
|
+
deviceName: process.env.THEGITAI_DEVICE_NAME?.trim() || undefined,
|
|
112
|
+
});
|
|
113
|
+
auth.writeCliAuthConfig(result);
|
|
114
|
+
console.log(chalk.green(`✓ Logged in as ${result.customer.email}`));
|
|
115
|
+
console.log(chalk.dim(`Server: ${result.serverUrl}`));
|
|
116
|
+
console.log(chalk.dim('You can close the browser tab. Run `ai` in a repo to start.'));
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
const config = auth.readCliAuthConfig();
|
|
120
|
+
if (!config) {
|
|
121
|
+
throw new Error('Not logged in. Run `ai login` first.');
|
|
122
|
+
}
|
|
123
|
+
if (command === 'whoami') {
|
|
124
|
+
const customer = await auth.fetchWhoami({ config });
|
|
125
|
+
console.log(chalk.green(customer.email));
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
if (command === 'logout') {
|
|
129
|
+
await auth.logoutFromServer({ config });
|
|
130
|
+
auth.clearCliAuthConfig();
|
|
131
|
+
console.log(chalk.green('Logged out.'));
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
throw new Error(`Unknown auth command: ${command}`);
|
|
135
|
+
}
|
|
136
|
+
function requireCliAuthConfig() {
|
|
137
|
+
const config = auth.readCliAuthConfig();
|
|
138
|
+
if (!config) {
|
|
139
|
+
throw new Error('Not logged in. Run `ai login` first.');
|
|
140
|
+
}
|
|
141
|
+
return config;
|
|
142
|
+
}
|
|
143
|
+
function formatSessionName(name) {
|
|
144
|
+
return name ? `"${name}"` : '(unnamed)';
|
|
145
|
+
}
|
|
146
|
+
function printSessionList(rootDir, sessions, serverModels) {
|
|
147
|
+
console.log(chalk.bold(`Saved sessions for ${rootDir}`));
|
|
148
|
+
if (!sessions.length) {
|
|
149
|
+
console.log(chalk.dim(' No saved sessions for this repo.'));
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
for (const session of sessions) {
|
|
153
|
+
const updated = new Date(session.updatedAt).toLocaleString();
|
|
154
|
+
const label = `${formatSessionName(session.name)} ${chalk.dim(session.id)}`;
|
|
155
|
+
const detail = [
|
|
156
|
+
modelLabel(serverModels, session.modelId),
|
|
157
|
+
`${session.messageCount} messages`,
|
|
158
|
+
`updated ${updated}`,
|
|
159
|
+
].join(' · ');
|
|
160
|
+
console.log(` ${label}`);
|
|
161
|
+
console.log(chalk.dim(` ${detail}`));
|
|
162
|
+
if (session.lastUserMessage) {
|
|
163
|
+
console.log(chalk.dim(` ${session.lastUserMessage}`));
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
function printStartupBanner(rootDir, modelLabel, autoYes) {
|
|
168
|
+
console.log(chalk.dim(`Project: ${rootDir}`));
|
|
169
|
+
console.log(chalk.dim(`Model: ${modelLabel}`));
|
|
170
|
+
if (autoYes) {
|
|
171
|
+
console.log(chalk.dim('Auto-confirm: enabled'));
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
function printSessionStartup(session) {
|
|
175
|
+
console.log(chalk.dim(`Session: ${session.sessionName ? `${session.sessionName} ` : ''}${session.sessionId}`));
|
|
176
|
+
}
|
|
177
|
+
function printSessionExit(session) {
|
|
178
|
+
console.log(chalk.dim(`\n${formatSessionExitNotice(session.sessionId)}\n`));
|
|
179
|
+
}
|
|
180
|
+
function modelLabel(serverModels, modelId) {
|
|
181
|
+
return (serverModels.models.find((model) => model.id === modelId)?.label ??
|
|
182
|
+
'Unknown model');
|
|
183
|
+
}
|
|
184
|
+
function makeConfirmCommand(session) {
|
|
185
|
+
return async (command) => {
|
|
186
|
+
console.log(chalk.bold(`\nCommand approval needed:\n${command}\n`));
|
|
187
|
+
const answer = (await promptText('Approve? [y]es/[a]ll/[n]o', 'n')).toLowerCase();
|
|
188
|
+
if (answer === 'a' || answer === 'all') {
|
|
189
|
+
session.autoYes = true;
|
|
190
|
+
return true;
|
|
191
|
+
}
|
|
192
|
+
return answer === 'y' || answer === 'yes';
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
function makeConfirmPatch(session) {
|
|
196
|
+
return async (filePath) => {
|
|
197
|
+
const answer = (await promptText(`Apply patch to ${filePath}? [y]es/[a]ll/[n]o`, 'n')).toLowerCase();
|
|
198
|
+
if (answer === 'a' || answer === 'all') {
|
|
199
|
+
session.autoYes = true;
|
|
200
|
+
return true;
|
|
201
|
+
}
|
|
202
|
+
return answer === 'y' || answer === 'yes';
|
|
203
|
+
};
|
|
204
|
+
}
|
|
205
|
+
async function saveSessionBoth({ session, serverSessionClient, }) {
|
|
206
|
+
saveSessionState(session);
|
|
207
|
+
try {
|
|
208
|
+
await serverSessionClient.save(session);
|
|
209
|
+
}
|
|
210
|
+
catch (error) {
|
|
211
|
+
console.error(chalk.yellow(`Warning: session save failed: ${error.message}`));
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
async function promptForModelSelection(currentModelId, authConfig) {
|
|
215
|
+
const serverModels = await models.fetchServerModels({ config: authConfig });
|
|
216
|
+
console.log(chalk.bold('\nAvailable models'));
|
|
217
|
+
for (const model of serverModels.models) {
|
|
218
|
+
const marker = model.id === currentModelId ? '*' : ' ';
|
|
219
|
+
console.log(`${marker} ${model.id} ${model.label}`);
|
|
220
|
+
}
|
|
221
|
+
console.log();
|
|
222
|
+
const selected = (await promptText('Model id', '')).trim() || null;
|
|
223
|
+
if (!selected) {
|
|
224
|
+
return { selected: null, serverModels };
|
|
225
|
+
}
|
|
226
|
+
return {
|
|
227
|
+
selected: models.validateServerModel(selected, serverModels),
|
|
228
|
+
serverModels,
|
|
229
|
+
};
|
|
230
|
+
}
|
|
231
|
+
async function promptForResumeSession(rootDir, serverModels) {
|
|
232
|
+
const sessions = listSessionMetadata(rootDir);
|
|
233
|
+
if (!sessions.length) {
|
|
234
|
+
console.log(chalk.dim('No saved sessions for this repo.\n'));
|
|
235
|
+
return null;
|
|
236
|
+
}
|
|
237
|
+
printSessionList(rootDir, sessions, serverModels);
|
|
238
|
+
console.log();
|
|
239
|
+
const identifier = (await promptText('Session id or name', '')).trim();
|
|
240
|
+
if (!identifier) {
|
|
241
|
+
return null;
|
|
242
|
+
}
|
|
243
|
+
return loadSessionSnapshot(rootDir, identifier);
|
|
244
|
+
}
|
|
245
|
+
async function runTurn({ authConfig, projectIndex, serverSessionClient, session, inputText, }) {
|
|
246
|
+
const prompt = String(inputText ?? '').trim();
|
|
247
|
+
if (!prompt)
|
|
248
|
+
return;
|
|
249
|
+
appendPromptHistory(prompt, session.env);
|
|
250
|
+
const result = await chat.sendServerUserMessage({
|
|
251
|
+
config: authConfig,
|
|
252
|
+
projectIndex,
|
|
253
|
+
session,
|
|
254
|
+
input: prompt,
|
|
255
|
+
});
|
|
256
|
+
if (result.text) {
|
|
257
|
+
console.log(`\n${chalk.green('TheGitAI>')}`);
|
|
258
|
+
console.log(renderMarkdownForTerminal(result.text));
|
|
259
|
+
console.log();
|
|
260
|
+
}
|
|
261
|
+
await saveSessionBoth({ session, serverSessionClient });
|
|
262
|
+
}
|
|
263
|
+
async function mainInteractive({ authConfig, projectIndex, serverModels, serverSessionClient, session, usageText, initialPrompt, }) {
|
|
264
|
+
if (initialPrompt) {
|
|
265
|
+
console.log(chalk.dim(`Prompt: "${initialPrompt}"`));
|
|
266
|
+
await runTurn({
|
|
267
|
+
authConfig,
|
|
268
|
+
projectIndex,
|
|
269
|
+
serverSessionClient,
|
|
270
|
+
session,
|
|
271
|
+
inputText: initialPrompt,
|
|
272
|
+
});
|
|
273
|
+
}
|
|
274
|
+
while (true) {
|
|
275
|
+
const inputText = await promptText('you');
|
|
276
|
+
const trimmed = inputText.trim();
|
|
277
|
+
if (!trimmed)
|
|
278
|
+
continue;
|
|
279
|
+
if (trimmed === '/exit')
|
|
280
|
+
return;
|
|
281
|
+
if (trimmed === '/help') {
|
|
282
|
+
console.log(renderMarkdownForTerminal(formatInteractiveHelpText()));
|
|
283
|
+
continue;
|
|
284
|
+
}
|
|
285
|
+
if (trimmed === '/usage') {
|
|
286
|
+
console.log(await usageText());
|
|
287
|
+
continue;
|
|
288
|
+
}
|
|
289
|
+
if (trimmed === '/clear') {
|
|
290
|
+
clearConversation(session);
|
|
291
|
+
await saveSessionBoth({ session, serverSessionClient });
|
|
292
|
+
console.log(chalk.dim('Conversation cleared.\n'));
|
|
293
|
+
continue;
|
|
294
|
+
}
|
|
295
|
+
if (trimmed === '/resume') {
|
|
296
|
+
const snapshot = await promptForResumeSession(session.rootDir, serverModels);
|
|
297
|
+
if (!snapshot) {
|
|
298
|
+
console.log(chalk.dim('Resume cancelled.\n'));
|
|
299
|
+
continue;
|
|
300
|
+
}
|
|
301
|
+
applySessionSnapshot(session, snapshot);
|
|
302
|
+
await saveSessionBoth({ session, serverSessionClient });
|
|
303
|
+
console.log(chalk.dim(`Resumed session${session.sessionName ? ` "${session.sessionName}"` : ''} (${session.sessionId})\n`));
|
|
304
|
+
continue;
|
|
305
|
+
}
|
|
306
|
+
if (trimmed === '/model' || trimmed.startsWith('/model ')) {
|
|
307
|
+
const inline = trimmed.slice('/model'.length).trim();
|
|
308
|
+
let serverModels;
|
|
309
|
+
let selected = null;
|
|
310
|
+
if (inline) {
|
|
311
|
+
serverModels = await models.fetchServerModels({ config: authConfig });
|
|
312
|
+
selected = models.validateServerModel(inline, serverModels);
|
|
313
|
+
}
|
|
314
|
+
else {
|
|
315
|
+
const response = await promptForModelSelection(session.modelId, authConfig);
|
|
316
|
+
serverModels = response.serverModels;
|
|
317
|
+
selected = response.selected;
|
|
318
|
+
}
|
|
319
|
+
if (!selected) {
|
|
320
|
+
console.log(chalk.dim('Model selection cancelled.\n'));
|
|
321
|
+
continue;
|
|
322
|
+
}
|
|
323
|
+
session.modelId = selected;
|
|
324
|
+
models.updateSelectedModelCache({
|
|
325
|
+
config: authConfig,
|
|
326
|
+
selectedModelId: selected,
|
|
327
|
+
serverModels,
|
|
328
|
+
});
|
|
329
|
+
await saveSessionBoth({ session, serverSessionClient });
|
|
330
|
+
console.log(chalk.dim(`Switched to ${modelLabel(serverModels, selected)}.\n`));
|
|
331
|
+
continue;
|
|
332
|
+
}
|
|
333
|
+
await runTurn({
|
|
334
|
+
authConfig,
|
|
335
|
+
projectIndex,
|
|
336
|
+
serverSessionClient,
|
|
337
|
+
session,
|
|
338
|
+
inputText: trimmed,
|
|
339
|
+
});
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
export async function main() {
|
|
343
|
+
const { autoYes, help, usage, command, commandArgs, session: sessionIdentifier, listSessions, prompt, } = parseArgs(process.argv);
|
|
344
|
+
if (help) {
|
|
345
|
+
printUsage();
|
|
346
|
+
return;
|
|
347
|
+
}
|
|
348
|
+
if (command) {
|
|
349
|
+
await runAuthCommand(command, commandArgs);
|
|
350
|
+
return;
|
|
351
|
+
}
|
|
352
|
+
if (usage) {
|
|
353
|
+
const authConfig = requireCliAuthConfig();
|
|
354
|
+
console.log(formatUsageText(await auth.fetchWhoamiResponse({ config: authConfig })));
|
|
355
|
+
return;
|
|
356
|
+
}
|
|
357
|
+
const rootDir = process.cwd();
|
|
358
|
+
const authConfig = requireCliAuthConfig();
|
|
359
|
+
const serverSessionClient = sessions.createServerSessionClient({ config: authConfig });
|
|
360
|
+
const cachedModels = models.readCachedServerModels();
|
|
361
|
+
const serverModels = await models.fetchServerModels({ config: authConfig });
|
|
362
|
+
const whoami = await auth.fetchWhoamiResponse({ config: authConfig });
|
|
363
|
+
if (listSessions) {
|
|
364
|
+
printSessionList(rootDir, listSessionMetadata(rootDir), serverModels);
|
|
365
|
+
return;
|
|
366
|
+
}
|
|
367
|
+
const sourceSnapshot = sessionIdentifier
|
|
368
|
+
? loadSessionSnapshot(rootDir, sessionIdentifier)
|
|
369
|
+
: null;
|
|
370
|
+
const selectedModelId = models.selectServerModel({
|
|
371
|
+
requestedModelId: sourceSnapshot?.modelId ?? null,
|
|
372
|
+
cached: cachedModels,
|
|
373
|
+
serverModels,
|
|
374
|
+
});
|
|
375
|
+
models.updateSelectedModelCache({
|
|
376
|
+
config: authConfig,
|
|
377
|
+
selectedModelId,
|
|
378
|
+
serverModels,
|
|
379
|
+
});
|
|
380
|
+
const session = createSession({
|
|
381
|
+
rootDir,
|
|
382
|
+
autoYes,
|
|
383
|
+
modelId: selectedModelId,
|
|
384
|
+
});
|
|
385
|
+
session.confirmCommand = makeConfirmCommand(session);
|
|
386
|
+
session.confirmPatch = makeConfirmPatch(session);
|
|
387
|
+
if (sourceSnapshot) {
|
|
388
|
+
applySessionSnapshot(session, sourceSnapshot);
|
|
389
|
+
await saveSessionBoth({ session, serverSessionClient });
|
|
390
|
+
}
|
|
391
|
+
const projectIndex = createIndex({
|
|
392
|
+
rootDir,
|
|
393
|
+
onStatus: (message) => {
|
|
394
|
+
if (message.trim()) {
|
|
395
|
+
console.log(chalk.dim(` ${message}`));
|
|
396
|
+
}
|
|
397
|
+
},
|
|
398
|
+
onContextLog: (message) => {
|
|
399
|
+
if (message.trim()) {
|
|
400
|
+
console.log(chalk.cyan(` ${message}`));
|
|
401
|
+
}
|
|
402
|
+
},
|
|
403
|
+
});
|
|
404
|
+
const initialPrompt = prompt || undefined;
|
|
405
|
+
if (shouldUseClientRatatuiShell()) {
|
|
406
|
+
await runClientInteractive({
|
|
407
|
+
appendPromptHistory: (value) => appendPromptHistory(value, session.env),
|
|
408
|
+
authConfig,
|
|
409
|
+
debugUi: whoami.debugUi,
|
|
410
|
+
projectIndex,
|
|
411
|
+
serverModels,
|
|
412
|
+
serverSessionClient,
|
|
413
|
+
session,
|
|
414
|
+
initialPrompt,
|
|
415
|
+
usageText: async () => formatUsageText(await auth.fetchWhoamiResponse({ config: authConfig })),
|
|
416
|
+
});
|
|
417
|
+
await saveSessionBoth({ session, serverSessionClient });
|
|
418
|
+
printSessionExit(session);
|
|
419
|
+
return;
|
|
420
|
+
}
|
|
421
|
+
printStartupBanner(rootDir, modelLabel(serverModels, session.modelId), session.autoYes);
|
|
422
|
+
printSessionStartup(session);
|
|
423
|
+
await mainInteractive({
|
|
424
|
+
authConfig,
|
|
425
|
+
projectIndex,
|
|
426
|
+
serverModels,
|
|
427
|
+
serverSessionClient,
|
|
428
|
+
session,
|
|
429
|
+
usageText: async () => formatUsageText(await auth.fetchWhoamiResponse({ config: authConfig })),
|
|
430
|
+
initialPrompt,
|
|
431
|
+
});
|
|
432
|
+
await saveSessionBoth({ session, serverSessionClient });
|
|
433
|
+
printSessionExit(session);
|
|
434
|
+
}
|
|
435
|
+
main().catch((error) => {
|
|
436
|
+
console.error(chalk.red(`\n✖ Error: ${error.message}\n`));
|
|
437
|
+
process.exit(1);
|
|
438
|
+
});
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
export const AGENT_MODES = ['default', 'auto-accept', 'plan'];
|
|
2
|
+
const PLAN_MODE_TOOL_NAMES = new Set([
|
|
3
|
+
'search_code',
|
|
4
|
+
'list_files',
|
|
5
|
+
'list_directories',
|
|
6
|
+
'read_file',
|
|
7
|
+
'grep_code',
|
|
8
|
+
'find_symbol',
|
|
9
|
+
'list_symbols',
|
|
10
|
+
'hover_symbol',
|
|
11
|
+
'signature_help',
|
|
12
|
+
'read_document',
|
|
13
|
+
'analyze_image',
|
|
14
|
+
'run_command',
|
|
15
|
+
]);
|
|
16
|
+
const PLAN_MODE_RUN_COMMAND_NAMES = new Set([
|
|
17
|
+
'pwd',
|
|
18
|
+
'ls',
|
|
19
|
+
'find',
|
|
20
|
+
'fd',
|
|
21
|
+
'rg',
|
|
22
|
+
'grep',
|
|
23
|
+
'cat',
|
|
24
|
+
'sed',
|
|
25
|
+
'nl',
|
|
26
|
+
'head',
|
|
27
|
+
'tail',
|
|
28
|
+
'wc',
|
|
29
|
+
'jq',
|
|
30
|
+
]);
|
|
31
|
+
const PLAN_MODE_RUN_COMMAND_ACTION = 'Plan mode is read-only inspection only. Use read_file, read_document, grep_code, list_files, list_directories, symbol tools, or a bounded file/directory reading command. Switch to Default mode for tests, builds, diagnostics, installs, project execution, network probes, or edits.';
|
|
32
|
+
export function normalizeAgentMode(value) {
|
|
33
|
+
return AGENT_MODES.includes(value)
|
|
34
|
+
? value
|
|
35
|
+
: 'default';
|
|
36
|
+
}
|
|
37
|
+
export function nextAgentMode(mode) {
|
|
38
|
+
if (mode === 'default')
|
|
39
|
+
return 'auto-accept';
|
|
40
|
+
if (mode === 'auto-accept')
|
|
41
|
+
return 'plan';
|
|
42
|
+
return 'default';
|
|
43
|
+
}
|
|
44
|
+
export function agentModeAllowsTool(mode, toolName) {
|
|
45
|
+
return mode !== 'plan' || PLAN_MODE_TOOL_NAMES.has(toolName);
|
|
46
|
+
}
|
|
47
|
+
function getUnquotedShellText(command) {
|
|
48
|
+
let quote = null;
|
|
49
|
+
let escaped = false;
|
|
50
|
+
let text = '';
|
|
51
|
+
for (const char of String(command)) {
|
|
52
|
+
if (escaped) {
|
|
53
|
+
escaped = false;
|
|
54
|
+
text += ' ';
|
|
55
|
+
continue;
|
|
56
|
+
}
|
|
57
|
+
if (char === '\\' && quote !== "'") {
|
|
58
|
+
escaped = true;
|
|
59
|
+
text += ' ';
|
|
60
|
+
continue;
|
|
61
|
+
}
|
|
62
|
+
if (quote) {
|
|
63
|
+
if (char === quote)
|
|
64
|
+
quote = null;
|
|
65
|
+
text += ' ';
|
|
66
|
+
continue;
|
|
67
|
+
}
|
|
68
|
+
if (char === "'" || char === '"' || char === '`') {
|
|
69
|
+
quote = char;
|
|
70
|
+
text += ' ';
|
|
71
|
+
continue;
|
|
72
|
+
}
|
|
73
|
+
text += char;
|
|
74
|
+
}
|
|
75
|
+
return text;
|
|
76
|
+
}
|
|
77
|
+
function planModeRunCommandBlockReason(command) {
|
|
78
|
+
const raw = String(command);
|
|
79
|
+
const text = getUnquotedShellText(command).trim();
|
|
80
|
+
if (!text)
|
|
81
|
+
return 'Plan mode run_command requires a command.';
|
|
82
|
+
if (/[<>]/.test(text) ||
|
|
83
|
+
/\$\(/.test(text) ||
|
|
84
|
+
/\bfind\b[\s\S]*(?:\s-delete\b|\s-exec\b)/.test(text) ||
|
|
85
|
+
/\bsed\b[\s\S]*(?:^|\s|["'])-i(?:\b|[A-Za-z])/.test(raw)) {
|
|
86
|
+
return PLAN_MODE_RUN_COMMAND_ACTION;
|
|
87
|
+
}
|
|
88
|
+
const segments = text
|
|
89
|
+
.split(/\s*(?:&&|\|\||;|\|)\s*/g)
|
|
90
|
+
.map((segment) => segment.trim())
|
|
91
|
+
.filter(Boolean);
|
|
92
|
+
if (segments.length === 0)
|
|
93
|
+
return 'Plan mode run_command requires a command.';
|
|
94
|
+
for (const segment of segments) {
|
|
95
|
+
const tokens = segment.match(/\S+/g) ?? [];
|
|
96
|
+
let commandName = '';
|
|
97
|
+
for (const token of tokens) {
|
|
98
|
+
if (/^[A-Za-z_][A-Za-z0-9_]*=/.test(token))
|
|
99
|
+
continue;
|
|
100
|
+
if (token === 'command')
|
|
101
|
+
continue;
|
|
102
|
+
commandName = token;
|
|
103
|
+
break;
|
|
104
|
+
}
|
|
105
|
+
commandName = commandName.split('/').pop() ?? commandName;
|
|
106
|
+
if (!PLAN_MODE_RUN_COMMAND_NAMES.has(commandName)) {
|
|
107
|
+
return PLAN_MODE_RUN_COMMAND_ACTION;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
return null;
|
|
111
|
+
}
|
|
112
|
+
export function buildAgentModeToolBlockedResult(mode, call) {
|
|
113
|
+
if (mode !== 'plan')
|
|
114
|
+
return null;
|
|
115
|
+
if (!PLAN_MODE_TOOL_NAMES.has(call.name)) {
|
|
116
|
+
return buildPlanModeToolBlockedResult(call.name);
|
|
117
|
+
}
|
|
118
|
+
if (call.name !== 'run_command')
|
|
119
|
+
return null;
|
|
120
|
+
const command = String(call.args?.command ?? call.args?.cmd ?? '');
|
|
121
|
+
const reason = planModeRunCommandBlockReason(command);
|
|
122
|
+
return reason ? buildPlanModeToolBlockedResult(call.name, reason) : null;
|
|
123
|
+
}
|
|
124
|
+
export function buildPlanModeToolBlockedResult(toolName, action = 'Plan mode is read-only inspection only. Ask any needed question, use allowed inspection tools, or tell the user to switch modes before execution.') {
|
|
125
|
+
return {
|
|
126
|
+
ok: false,
|
|
127
|
+
failureCategory: 'policy_blocked',
|
|
128
|
+
failureDetails: {
|
|
129
|
+
category: 'policy_blocked',
|
|
130
|
+
tool: toolName,
|
|
131
|
+
action,
|
|
132
|
+
},
|
|
133
|
+
error: `Plan mode blocked ${toolName}. ${action}`,
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
export function agentModeLabel(mode) {
|
|
137
|
+
if (mode === 'auto-accept')
|
|
138
|
+
return 'Auto-Accept';
|
|
139
|
+
if (mode === 'plan')
|
|
140
|
+
return 'Plan · read-only';
|
|
141
|
+
return 'Default';
|
|
142
|
+
}
|