@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.
Files changed (101) hide show
  1. package/LICENSE +20 -0
  2. package/README.md +30 -0
  3. package/dist/bin/ai.js +438 -0
  4. package/dist/parsers/tree-sitter-c-sharp.wasm +0 -0
  5. package/dist/parsers/tree-sitter-c.wasm +0 -0
  6. package/dist/parsers/tree-sitter-cpp.wasm +0 -0
  7. package/dist/parsers/tree-sitter-css.wasm +0 -0
  8. package/dist/parsers/tree-sitter-go.wasm +0 -0
  9. package/dist/parsers/tree-sitter-html.wasm +0 -0
  10. package/dist/parsers/tree-sitter-java.wasm +0 -0
  11. package/dist/parsers/tree-sitter-javascript.wasm +0 -0
  12. package/dist/parsers/tree-sitter-objc.wasm +0 -0
  13. package/dist/parsers/tree-sitter-php.wasm +0 -0
  14. package/dist/parsers/tree-sitter-python.wasm +0 -0
  15. package/dist/parsers/tree-sitter-ruby.wasm +0 -0
  16. package/dist/parsers/tree-sitter-rust.wasm +0 -0
  17. package/dist/parsers/tree-sitter-tsx.wasm +0 -0
  18. package/dist/parsers/tree-sitter-typescript.wasm +0 -0
  19. package/dist/src/agent-mode.js +142 -0
  20. package/dist/src/api/auth.js +81 -0
  21. package/dist/src/api/browser-login.js +184 -0
  22. package/dist/src/api/chat.js +346 -0
  23. package/dist/src/api/contracts.js +1 -0
  24. package/dist/src/api/http.js +44 -0
  25. package/dist/src/api/index.js +11 -0
  26. package/dist/src/api/models.js +110 -0
  27. package/dist/src/api/sessions.js +72 -0
  28. package/dist/src/artifact-policy.js +207 -0
  29. package/dist/src/client-state.js +14 -0
  30. package/dist/src/core/clipboard.js +208 -0
  31. package/dist/src/core/open-url.js +32 -0
  32. package/dist/src/edit-journal.js +133 -0
  33. package/dist/src/executor.js +924 -0
  34. package/dist/src/extractors/cpp.js +18 -0
  35. package/dist/src/extractors/csharp.js +16 -0
  36. package/dist/src/extractors/css.js +12 -0
  37. package/dist/src/extractors/go.js +27 -0
  38. package/dist/src/extractors/index.js +52 -0
  39. package/dist/src/extractors/java.js +14 -0
  40. package/dist/src/extractors/javascript.js +33 -0
  41. package/dist/src/extractors/objc.js +14 -0
  42. package/dist/src/extractors/php.js +20 -0
  43. package/dist/src/extractors/python.js +11 -0
  44. package/dist/src/extractors/ruby.js +13 -0
  45. package/dist/src/extractors/rust.js +17 -0
  46. package/dist/src/extractors/utils.js +58 -0
  47. package/dist/src/help-text.js +125 -0
  48. package/dist/src/markdown-renderer.js +112 -0
  49. package/dist/src/patcher.js +279 -0
  50. package/dist/src/project-index.js +221 -0
  51. package/dist/src/repo-map-languages.js +100 -0
  52. package/dist/src/runtime-mode.js +35 -0
  53. package/dist/src/scanner.js +362 -0
  54. package/dist/src/secret-preview.js +137 -0
  55. package/dist/src/session-exit.js +17 -0
  56. package/dist/src/session-safety.js +1012 -0
  57. package/dist/src/session-store.js +266 -0
  58. package/dist/src/session.js +93 -0
  59. package/dist/src/tool-executor.js +188 -0
  60. package/dist/src/tools/code-intel.js +472 -0
  61. package/dist/src/tools/delete-file.js +27 -0
  62. package/dist/src/tools/exec-utils.js +17 -0
  63. package/dist/src/tools/find-symbol.js +70 -0
  64. package/dist/src/tools/get-diagnostics.js +22 -0
  65. package/dist/src/tools/grep-code.js +331 -0
  66. package/dist/src/tools/hover-symbol.js +95 -0
  67. package/dist/src/tools/index.js +73 -0
  68. package/dist/src/tools/list-checkpoints.js +11 -0
  69. package/dist/src/tools/list-directories.js +16 -0
  70. package/dist/src/tools/list-files.js +13 -0
  71. package/dist/src/tools/list-session-edits.js +9 -0
  72. package/dist/src/tools/list-symbols.js +55 -0
  73. package/dist/src/tools/patch-file.js +88 -0
  74. package/dist/src/tools/path-listing.js +83 -0
  75. package/dist/src/tools/read-document.js +111 -0
  76. package/dist/src/tools/read-file.js +109 -0
  77. package/dist/src/tools/restore-checkpoint.js +100 -0
  78. package/dist/src/tools/ripgrep.js +29 -0
  79. package/dist/src/tools/run-command.js +94 -0
  80. package/dist/src/tools/run-node-script.js +210 -0
  81. package/dist/src/tools/search-code.js +37 -0
  82. package/dist/src/tools/shell-diagnostics.js +707 -0
  83. package/dist/src/tools/signature-help.js +118 -0
  84. package/dist/src/tools/str-replace.js +193 -0
  85. package/dist/src/tools/types.js +1 -0
  86. package/dist/src/tools/undo-edit.js +202 -0
  87. package/dist/src/tools/write-file.js +59 -0
  88. package/dist/src/tree-sitter-runtime.js +135 -0
  89. package/dist/src/types.js +1 -0
  90. package/dist/src/ui/paste-collapse.js +22 -0
  91. package/dist/src/ui/prompt-history-store.js +96 -0
  92. package/dist/src/ui/repl.js +2238 -0
  93. package/dist/src/ui/tui/bridge.js +175 -0
  94. package/dist/src/ui/tui/build-frame.js +718 -0
  95. package/dist/src/ui/tui/markdown-render.js +455 -0
  96. package/dist/src/ui/tui/shell-input.js +488 -0
  97. package/dist/src/ui/tui/text.js +30 -0
  98. package/dist/src/ui/tui/types.js +1 -0
  99. package/dist/src/usage.js +47 -0
  100. package/dist/src/utils.js +38 -0
  101. 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
@@ -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
+ }