@thegitai/cli 1.0.0-beta.8 → 1.0.0-preview.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 (55) hide show
  1. package/README.md +36 -2
  2. package/dist/bin/ai.js +135 -26
  3. package/dist/parsers/NOTICE +18 -0
  4. package/dist/src/agent-mode.js +5 -0
  5. package/dist/src/api/auth.js +3 -3
  6. package/dist/src/api/browser-login.js +3 -38
  7. package/dist/src/api/chat.js +57 -11
  8. package/dist/src/api/http.js +49 -1
  9. package/dist/src/api/models.js +26 -20
  10. package/dist/src/artifact-policy.js +3 -0
  11. package/dist/src/background-jobs.js +410 -0
  12. package/dist/src/cli-args.js +0 -5
  13. package/dist/src/client-environment.js +2 -0
  14. package/dist/src/colors.js +50 -0
  15. package/dist/src/core/clipboard.js +19 -0
  16. package/dist/src/core/image-path-extractor.js +144 -0
  17. package/dist/src/executor.js +48 -12
  18. package/dist/src/help-text.js +11 -6
  19. package/dist/src/markdown-renderer.js +1 -1
  20. package/dist/src/patcher.js +1 -3
  21. package/dist/src/scanner.js +50 -12
  22. package/dist/src/scratch-dir.js +57 -0
  23. package/dist/src/secret-preview.js +0 -10
  24. package/dist/src/session-safety.js +0 -19
  25. package/dist/src/session-store.js +0 -1
  26. package/dist/src/todo-list.js +106 -0
  27. package/dist/src/tool-executor.js +159 -18
  28. package/dist/src/tools/delete-file.js +1 -1
  29. package/dist/src/tools/index.js +6 -0
  30. package/dist/src/tools/patch-file.js +3 -2
  31. package/dist/src/tools/path-suggest.js +81 -8
  32. package/dist/src/tools/read-document.js +2 -2
  33. package/dist/src/tools/read-file.js +14 -7
  34. package/dist/src/tools/replace-document-text.js +3 -11
  35. package/dist/src/tools/restore-checkpoint.js +1 -1
  36. package/dist/src/tools/run-command.js +83 -16
  37. package/dist/src/tools/run-node-script.js +3 -1
  38. package/dist/src/tools/shell-job-kill.js +48 -0
  39. package/dist/src/tools/shell-job-output.js +51 -0
  40. package/dist/src/tools/str-replace.js +3 -2
  41. package/dist/src/tools/undo-edit.js +1 -1
  42. package/dist/src/tools/update-todos.js +27 -0
  43. package/dist/src/tools/write-file.js +1 -1
  44. package/dist/src/tree-sitter-runtime.js +8 -1
  45. package/dist/src/ui/repl.js +313 -23
  46. package/dist/src/ui/tui/bridge.js +0 -4
  47. package/dist/src/ui/tui/build-frame.js +220 -24
  48. package/dist/src/ui/tui/shell-input.js +33 -4
  49. package/dist/src/ui/tui/terminal-title.js +81 -0
  50. package/dist/src/version.js +0 -6
  51. package/dist/vendor/web-tree-sitter/LICENSE +21 -0
  52. package/dist/vendor/web-tree-sitter/NOTICE +13 -0
  53. package/dist/vendor/web-tree-sitter/web-tree-sitter.cjs +4063 -0
  54. package/dist/vendor/web-tree-sitter/web-tree-sitter.wasm +0 -0
  55. package/package.json +14 -15
@@ -1,4 +1,5 @@
1
- import chalk from 'chalk';
1
+ import chalk from '../colors.js';
2
+ import { startBackgroundJob } from '../background-jobs.js';
2
3
  import { getBlockedCommandReason, runCommand, } from '../executor.js';
3
4
  import { syncIndexFromDisk } from '../project-index.js';
4
5
  import { isTuiMode } from '../runtime-mode.js';
@@ -12,9 +13,10 @@ export async function runShellCommand(context, args) {
12
13
  if (!command) {
13
14
  return { ok: false, error: 'command is required' };
14
15
  }
16
+ const runInBackground = args.background === true;
15
17
  const hasTimeout = typeof args.timeout_ms === 'number' && args.timeout_ms > 0;
16
18
  const repoHint = buildNestedGitHint(rootDir, command);
17
- const blockedReason = getBlockedCommandReason(command, hasTimeout, rootDir);
19
+ const blockedReason = getBlockedCommandReason(command, hasTimeout || runInBackground, rootDir);
18
20
  if (blockedReason) {
19
21
  const error = blockedReason;
20
22
  if (!isTuiMode())
@@ -37,7 +39,9 @@ export async function runShellCommand(context, args) {
37
39
  error: 'confirmCommand is required when autoYes is false',
38
40
  };
39
41
  }
40
- const approved = await confirmCommand(command);
42
+ const approved = await confirmCommand(runInBackground
43
+ ? `${command}\n\nRuns as a managed background job until it exits or is killed.`
44
+ : command);
41
45
  if (!approved) {
42
46
  if (!isTuiMode())
43
47
  console.log(chalk.dim(` ⏭ Skipped: ${command}`));
@@ -45,10 +49,13 @@ export async function runShellCommand(context, args) {
45
49
  ok: false,
46
50
  skipped: true,
47
51
  command,
48
- error: 'User declined command execution',
52
+ error: 'User declined command execution. Do not rerun this command or try a broader variant of it; either continue without it or ask the user one specific question about how to proceed.',
49
53
  };
50
54
  }
51
55
  }
56
+ if (runInBackground) {
57
+ return runBackgroundCommand(context, command, args.timeout_ms, repoHint);
58
+ }
52
59
  const result = await runCommand(command, rootDir, {
53
60
  requestSudoPassword,
54
61
  timeout: typeof args.timeout_ms === 'number' && args.timeout_ms > 0 ? args.timeout_ms : undefined,
@@ -67,19 +74,9 @@ export async function runShellCommand(context, args) {
67
74
  if (repoSync.added || repoSync.modified || repoSync.removed) {
68
75
  onStatus(`Synced repo state after command (${repoSync.added} added, ${repoSync.modified} modified, ${repoSync.removed} removed).`);
69
76
  }
70
- // Redact connection-string passwords so shell output (e.g. `cat .env`,
71
- // `printenv`) cannot leak them into history or telemetry.
72
- let output = typeof result.output === 'string'
73
- ? redactConnectionStringCredentials(result.output)
77
+ const output = typeof result.output === 'string'
78
+ ? boundCommandOutput(redactConnectionStringCredentials(result.output))
74
79
  : result.output;
75
- if (typeof output === 'string' && output.length > MAX_OUTPUT_CHARS) {
76
- const headSize = Math.floor(MAX_OUTPUT_CHARS * 0.2);
77
- const tailSize = MAX_OUTPUT_CHARS - headSize;
78
- output =
79
- output.slice(0, headSize) +
80
- `\n\n... (${output.length - headSize - tailSize} chars truncated) ...\n\n` +
81
- output.slice(-tailSize);
82
- }
83
80
  return {
84
81
  ok: result.exitCode === 0,
85
82
  command,
@@ -92,3 +89,73 @@ export async function runShellCommand(context, args) {
92
89
  repoHint,
93
90
  };
94
91
  }
92
+ export function boundCommandOutput(output) {
93
+ if (output.length <= MAX_OUTPUT_CHARS)
94
+ return output;
95
+ const headSize = Math.floor(MAX_OUTPUT_CHARS * 0.2);
96
+ const tailSize = MAX_OUTPUT_CHARS - headSize;
97
+ return (output.slice(0, headSize) +
98
+ `\n\n... (${output.length - headSize - tailSize} chars truncated) ...\n\n` +
99
+ output.slice(-tailSize));
100
+ }
101
+ async function runBackgroundCommand(context, command, timeoutMs, repoHint) {
102
+ const { rootDir, projectIndex, onStatus } = context;
103
+ const started = await startBackgroundJob(command, rootDir, {
104
+ startupWaitMs: typeof timeoutMs === 'number' && timeoutMs > 0 ? timeoutMs : undefined,
105
+ sessionId: context.sessionId,
106
+ });
107
+ if (!started.ok || !started.snapshot) {
108
+ if (!isTuiMode())
109
+ console.log(chalk.red(`\n ✖ ${started.error}`));
110
+ return {
111
+ ok: false,
112
+ blocked: true,
113
+ command,
114
+ error: started.error ?? 'Background job failed to start.',
115
+ repoHint,
116
+ };
117
+ }
118
+ const snapshot = started.snapshot;
119
+ const repoSync = projectIndex.initialized
120
+ ? await syncIndexFromDisk(projectIndex)
121
+ : {
122
+ added: 0,
123
+ modified: 0,
124
+ removed: 0,
125
+ indexedChunks: 0,
126
+ retrievalTokensUsed: 0,
127
+ };
128
+ invalidateShellDiagnosticsCache(rootDir);
129
+ if (repoSync.added || repoSync.modified || repoSync.removed) {
130
+ onStatus(`Synced repo state after command (${repoSync.added} added, ${repoSync.modified} modified, ${repoSync.removed} removed).`);
131
+ }
132
+ const output = boundCommandOutput(redactConnectionStringCredentials(started.startupOutput ?? '').trim());
133
+ if (snapshot.status === 'running') {
134
+ return {
135
+ ok: true,
136
+ backgrounded: true,
137
+ command,
138
+ jobId: snapshot.id,
139
+ status: 'running',
140
+ pid: snapshot.pid,
141
+ output,
142
+ note: `Background job ${snapshot.id} is running. Use shell_job_output to poll status and new output, and shell_job_kill to stop it.`,
143
+ repoSync,
144
+ retrievalTokensUsed: repoSync.retrievalTokensUsed,
145
+ repoHint,
146
+ };
147
+ }
148
+ return {
149
+ ok: snapshot.exitCode === 0,
150
+ backgrounded: true,
151
+ command,
152
+ jobId: snapshot.id,
153
+ status: snapshot.status,
154
+ exitCode: snapshot.exitCode,
155
+ output,
156
+ note: `Background job ${snapshot.id} finished during the startup window.`,
157
+ repoSync,
158
+ retrievalTokensUsed: repoSync.retrievalTokensUsed,
159
+ repoHint,
160
+ };
161
+ }
@@ -1,8 +1,9 @@
1
- import chalk from 'chalk';
1
+ import chalk from '../colors.js';
2
2
  import { execFileSync, spawn } from 'node:child_process';
3
3
  import { syncIndexFromDisk } from '../project-index.js';
4
4
  import { isTuiMode } from '../runtime-mode.js';
5
5
  import { buildDeferredShellDiagnostics, invalidateShellDiagnosticsCache, } from './shell-diagnostics.js';
6
+ import { ensureSessionScratchDir } from '../scratch-dir.js';
6
7
  const DEFAULT_TIMEOUT = 5 * 60 * 1000;
7
8
  const MAX_OUTPUT_CHARS = 4000;
8
9
  const MAX_CAPTURE_CHARS = 1024 * 1024;
@@ -99,6 +100,7 @@ function executeNodeScript(rootDir, script, timeout) {
99
100
  npm_config_progress: 'false',
100
101
  npm_config_fund: 'false',
101
102
  npm_config_audit: 'false',
103
+ THEGITAI_SCRATCH_DIR: ensureSessionScratchDir(),
102
104
  },
103
105
  stdio: ['pipe', 'pipe', 'pipe'],
104
106
  });
@@ -0,0 +1,48 @@
1
+ import { killBackgroundJob } from '../background-jobs.js';
2
+ import { redactConnectionStringCredentials } from '../secret-preview.js';
3
+ const MAX_JOB_TOOL_OUTPUT_CHARS = 64 * 1024;
4
+ function boundJobToolOutput(output) {
5
+ if (output.length <= MAX_JOB_TOOL_OUTPUT_CHARS)
6
+ return output;
7
+ const headSize = Math.floor(MAX_JOB_TOOL_OUTPUT_CHARS * 0.2);
8
+ const tailSize = MAX_JOB_TOOL_OUTPUT_CHARS - headSize;
9
+ return (output.slice(0, headSize) +
10
+ `\n\n... (${output.length - headSize - tailSize} chars truncated) ...\n\n` +
11
+ output.slice(-tailSize));
12
+ }
13
+ export async function shellJobKill(context, args) {
14
+ const jobId = String(args.job_id ?? '').trim();
15
+ if (!jobId) {
16
+ return {
17
+ ok: false,
18
+ error: 'job_id is required',
19
+ failureCategory: 'missing_required_argument',
20
+ };
21
+ }
22
+ const result = await killBackgroundJob(jobId, {
23
+ sessionId: context.sessionId,
24
+ });
25
+ if (!result.ok || !result.snapshot) {
26
+ return {
27
+ ok: false,
28
+ error: result.error ?? 'Background job lookup failed.',
29
+ failureCategory: 'not_found',
30
+ };
31
+ }
32
+ const snapshot = result.snapshot;
33
+ let finalOutput = redactConnectionStringCredentials(result.finalOutput ?? '');
34
+ if (result.droppedChars) {
35
+ finalOutput = `... (${result.droppedChars} chars of older output dropped) ...\n${finalOutput}`;
36
+ }
37
+ finalOutput = boundJobToolOutput(finalOutput);
38
+ return {
39
+ ok: true,
40
+ jobId: snapshot.id,
41
+ command: snapshot.command,
42
+ status: snapshot.status,
43
+ exitCode: snapshot.exitCode,
44
+ alreadyFinished: result.alreadyFinished === true,
45
+ ranMs: (snapshot.endedAt ?? Date.now()) - snapshot.startedAt,
46
+ finalOutput,
47
+ };
48
+ }
@@ -0,0 +1,51 @@
1
+ import { MAX_JOB_WAIT_MS, readBackgroundJobOutput, } from '../background-jobs.js';
2
+ import { redactConnectionStringCredentials } from '../secret-preview.js';
3
+ const MAX_JOB_TOOL_OUTPUT_CHARS = 64 * 1024;
4
+ function boundJobToolOutput(output) {
5
+ if (output.length <= MAX_JOB_TOOL_OUTPUT_CHARS)
6
+ return output;
7
+ const headSize = Math.floor(MAX_JOB_TOOL_OUTPUT_CHARS * 0.2);
8
+ const tailSize = MAX_JOB_TOOL_OUTPUT_CHARS - headSize;
9
+ return (output.slice(0, headSize) +
10
+ `\n\n... (${output.length - headSize - tailSize} chars truncated) ...\n\n` +
11
+ output.slice(-tailSize));
12
+ }
13
+ export async function shellJobOutput(context, args) {
14
+ const jobId = String(args.job_id ?? '').trim();
15
+ if (!jobId) {
16
+ return {
17
+ ok: false,
18
+ error: 'job_id is required',
19
+ failureCategory: 'missing_required_argument',
20
+ };
21
+ }
22
+ const waitMs = typeof args.wait_ms === 'number' && args.wait_ms > 0
23
+ ? Math.min(args.wait_ms, MAX_JOB_WAIT_MS)
24
+ : 0;
25
+ const result = await readBackgroundJobOutput(jobId, {
26
+ waitMs,
27
+ sessionId: context.sessionId,
28
+ });
29
+ if (!result.ok || !result.snapshot) {
30
+ return {
31
+ ok: false,
32
+ error: result.error ?? 'Background job lookup failed.',
33
+ failureCategory: 'not_found',
34
+ };
35
+ }
36
+ const snapshot = result.snapshot;
37
+ let newOutput = redactConnectionStringCredentials(result.newOutput ?? '');
38
+ if (result.droppedChars) {
39
+ newOutput = `... (${result.droppedChars} chars of older output dropped) ...\n${newOutput}`;
40
+ }
41
+ newOutput = boundJobToolOutput(newOutput);
42
+ return {
43
+ ok: true,
44
+ jobId: snapshot.id,
45
+ command: snapshot.command,
46
+ status: snapshot.status,
47
+ exitCode: snapshot.exitCode,
48
+ elapsedMs: (snapshot.endedAt ?? Date.now()) - snapshot.startedAt,
49
+ newOutput,
50
+ };
51
+ }
@@ -1,7 +1,8 @@
1
- import chalk from 'chalk';
1
+ import chalk from '../colors.js';
2
2
  import path from 'node:path';
3
3
  import { normalizeProjectRelativePath } from '../artifact-policy.js';
4
4
  import { readProjectFile, writeProjectFile } from '../patcher.js';
5
+ import { repairFilePath } from './path-suggest.js';
5
6
  import { upsertIndexFile } from '../project-index.js';
6
7
  import { isTuiMode } from '../runtime-mode.js';
7
8
  import { getCurrentFileHash, resolveRedactionTokens } from '../session-safety.js';
@@ -76,7 +77,7 @@ function buildStrReplacePreview(oldString, newString) {
76
77
  }
77
78
  export async function strReplace(context, args) {
78
79
  const { rootDir, projectIndex, autoYes, confirmPatch } = context;
79
- const filePath = String(args.filePath ?? args.file_path ?? '').trim();
80
+ const filePath = repairFilePath(rootDir, String(args.filePath ?? args.file_path ?? '').trim());
80
81
  let oldString = typeof args.old_string === 'string'
81
82
  ? args.old_string
82
83
  : typeof args.oldString === 'string'
@@ -1,4 +1,4 @@
1
- import chalk from 'chalk';
1
+ import chalk from '../colors.js';
2
2
  import { hashStoredContent, readFileEditSnapshot, storedContentBuffer, } from '../edit-journal.js';
3
3
  import { deleteProjectFile, writeProjectFile, writeProjectFileBuffer, } from '../patcher.js';
4
4
  import { removeIndexFile, upsertIndexFile, } from '../project-index.js';
@@ -0,0 +1,27 @@
1
+ import { extractTodosArg, replaceTodos } from '../todo-list.js';
2
+ export function updateTodos(_context, args) {
3
+ const rawTodos = extractTodosArg(args);
4
+ if (rawTodos === undefined) {
5
+ return {
6
+ ok: false,
7
+ error: 'todos is required (pass [] to clear the list).',
8
+ failureCategory: 'missing_required_argument',
9
+ };
10
+ }
11
+ const result = replaceTodos(rawTodos);
12
+ if (!result.ok) {
13
+ return {
14
+ ok: false,
15
+ error: result.error,
16
+ failureCategory: 'invalid_argument',
17
+ };
18
+ }
19
+ const { snapshot, normalizations } = result;
20
+ return {
21
+ ok: true,
22
+ todos: snapshot.items,
23
+ completedCount: snapshot.completedCount,
24
+ totalCount: snapshot.totalCount,
25
+ ...(normalizations.length ? { normalizations } : {}),
26
+ };
27
+ }
@@ -1,4 +1,4 @@
1
- import chalk from 'chalk';
1
+ import chalk from '../colors.js';
2
2
  import path from 'node:path';
3
3
  import { normalizeProjectRelativePath } from '../artifact-policy.js';
4
4
  import { writeProjectFile } from '../patcher.js';
@@ -5,8 +5,15 @@ import { fileURLToPath } from 'node:url';
5
5
  import { addSignatureForNode } from './extractors/index.js';
6
6
  import { getRepoMapLanguageForFile, } from './repo-map-languages.js';
7
7
  const require = createRequire(import.meta.url);
8
- const TreeSitter = require('web-tree-sitter');
9
8
  const __dirname = path.dirname(fileURLToPath(import.meta.url));
9
+ function resolveVendoredTreeSitter() {
10
+ const candidates = [
11
+ path.resolve(__dirname, '..', 'vendor', 'web-tree-sitter', 'web-tree-sitter.cjs'),
12
+ path.resolve(__dirname, '..', '..', 'vendor', 'web-tree-sitter', 'web-tree-sitter.cjs'),
13
+ ];
14
+ return candidates.find((candidate) => existsSync(candidate)) ?? candidates[0];
15
+ }
16
+ const TreeSitter = require(resolveVendoredTreeSitter());
10
17
  let parserInitPromise = null;
11
18
  const parserCache = Object.create(null);
12
19
  const languageCache = Object.create(null);