icopilot 2.2.0

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 (203) hide show
  1. package/CHANGELOG.md +250 -0
  2. package/LICENSE +21 -0
  3. package/README.md +214 -0
  4. package/bin/icopilot.js +6 -0
  5. package/dist/acp/router.js +123 -0
  6. package/dist/acp/schema.js +53 -0
  7. package/dist/agents/aggregator.js +187 -0
  8. package/dist/agents/custom-agents.js +97 -0
  9. package/dist/agents/goal-driven.js +411 -0
  10. package/dist/agents/multi-repo.js +350 -0
  11. package/dist/agents/parallel-runner.js +181 -0
  12. package/dist/agents/router.js +144 -0
  13. package/dist/agents/self-heal.js +481 -0
  14. package/dist/agents/tdd-agent.js +278 -0
  15. package/dist/api/github-models.js +158 -0
  16. package/dist/bridge/ide-bridge.js +479 -0
  17. package/dist/cloud/routine-executor.js +34 -0
  18. package/dist/cloud/routine-scheduler.js +67 -0
  19. package/dist/cloud/routine-storage.js +297 -0
  20. package/dist/commands/acp-cmd.js +143 -0
  21. package/dist/commands/actions-cmd.js +624 -0
  22. package/dist/commands/agent-cmd.js +144 -0
  23. package/dist/commands/alias-cmd.js +132 -0
  24. package/dist/commands/bookmark-cmd.js +77 -0
  25. package/dist/commands/changelog-cmd.js +99 -0
  26. package/dist/commands/changes-cmd.js +120 -0
  27. package/dist/commands/clipboard-cmd.js +217 -0
  28. package/dist/commands/cloud-routine-cmd.js +265 -0
  29. package/dist/commands/codegen-cmd.js +544 -0
  30. package/dist/commands/compare-cmd.js +116 -0
  31. package/dist/commands/context-cmd.js +247 -0
  32. package/dist/commands/context-viz-cmd.js +43 -0
  33. package/dist/commands/conventions-cmd.js +116 -0
  34. package/dist/commands/cost-cmd.js +51 -0
  35. package/dist/commands/deps-cmd.js +294 -0
  36. package/dist/commands/diagram-cmd.js +658 -0
  37. package/dist/commands/diff-review-cmd.js +92 -0
  38. package/dist/commands/doc-cmd.js +412 -0
  39. package/dist/commands/doctor-cmd.js +152 -0
  40. package/dist/commands/editor-cmd.js +49 -0
  41. package/dist/commands/env-cmd.js +86 -0
  42. package/dist/commands/explain-cmd.js +78 -0
  43. package/dist/commands/explain-shell-cmd.js +22 -0
  44. package/dist/commands/explore-cmd.js +231 -0
  45. package/dist/commands/feedback-cmd.js +98 -0
  46. package/dist/commands/fix-cmd.js +17 -0
  47. package/dist/commands/generate-cmd.js +38 -0
  48. package/dist/commands/git-extra.js +197 -0
  49. package/dist/commands/git-log-cmd.js +98 -0
  50. package/dist/commands/git-undo-cmd.js +137 -0
  51. package/dist/commands/git.js +155 -0
  52. package/dist/commands/history-cmd.js +122 -0
  53. package/dist/commands/index-cmd.js +65 -0
  54. package/dist/commands/init-cmd.js +73 -0
  55. package/dist/commands/lint-cmd.js +133 -0
  56. package/dist/commands/memory-cmd.js +98 -0
  57. package/dist/commands/metrics-cmd.js +97 -0
  58. package/dist/commands/mode-prefix.js +30 -0
  59. package/dist/commands/multi-cmd.js +44 -0
  60. package/dist/commands/notify-cmd.js +204 -0
  61. package/dist/commands/profile-cmd.js +101 -0
  62. package/dist/commands/prompts.js +17 -0
  63. package/dist/commands/rag-cmd.js +60 -0
  64. package/dist/commands/readme-cmd.js +564 -0
  65. package/dist/commands/reasoning-cmd.js +34 -0
  66. package/dist/commands/refactor-cmd.js +96 -0
  67. package/dist/commands/release-cmd.js +450 -0
  68. package/dist/commands/repo-cmd.js +195 -0
  69. package/dist/commands/route-cmd.js +21 -0
  70. package/dist/commands/schedule-cmd.js +109 -0
  71. package/dist/commands/search-cmd.js +47 -0
  72. package/dist/commands/security-cmd.js +156 -0
  73. package/dist/commands/settings-cmd.js +238 -0
  74. package/dist/commands/skill-cmd.js +338 -0
  75. package/dist/commands/slash.js +2721 -0
  76. package/dist/commands/snippets-cmd.js +83 -0
  77. package/dist/commands/space-cmd.js +92 -0
  78. package/dist/commands/stash-cmd.js +156 -0
  79. package/dist/commands/stats-cmd.js +36 -0
  80. package/dist/commands/style-cmd.js +85 -0
  81. package/dist/commands/suggest-cmd.js +40 -0
  82. package/dist/commands/summary-cmd.js +138 -0
  83. package/dist/commands/task-cmd.js +58 -0
  84. package/dist/commands/team-memory-cmd.js +97 -0
  85. package/dist/commands/template-cmd.js +475 -0
  86. package/dist/commands/test-cmd.js +146 -0
  87. package/dist/commands/todo-cmd.js +172 -0
  88. package/dist/commands/tokens-cmd.js +277 -0
  89. package/dist/commands/trigger-cmd.js +147 -0
  90. package/dist/commands/undo-cmd.js +18 -0
  91. package/dist/commands/voice-cmd.js +89 -0
  92. package/dist/commands/watch-cmd.js +110 -0
  93. package/dist/commands/web-cmd.js +183 -0
  94. package/dist/commands/worktree-cmd.js +119 -0
  95. package/dist/config-profile.js +66 -0
  96. package/dist/config.js +288 -0
  97. package/dist/context/compactor.js +53 -0
  98. package/dist/context/dep-context.js +329 -0
  99. package/dist/context/file-refs.js +54 -0
  100. package/dist/context/git-context.js +229 -0
  101. package/dist/context/image-input.js +66 -0
  102. package/dist/context/memory.js +55 -0
  103. package/dist/context/persistent-memory.js +104 -0
  104. package/dist/context/pinned.js +96 -0
  105. package/dist/context/priority.js +150 -0
  106. package/dist/context/read-only.js +48 -0
  107. package/dist/context/smart-files.js +286 -0
  108. package/dist/context/team-memory.js +156 -0
  109. package/dist/extensions/loader.js +149 -0
  110. package/dist/extensions/marketplace.js +49 -0
  111. package/dist/extensions/slack-provider.js +181 -0
  112. package/dist/extensions/team.js +56 -0
  113. package/dist/extensions/teams-provider.js +222 -0
  114. package/dist/extensions/voice.js +18 -0
  115. package/dist/hooks/lifecycle.js +215 -0
  116. package/dist/hooks/precommit.js +463 -0
  117. package/dist/index/embeddings.js +23 -0
  118. package/dist/index/indexer.js +86 -0
  119. package/dist/index/retrieve.js +20 -0
  120. package/dist/index/store.js +95 -0
  121. package/dist/index.js +286 -0
  122. package/dist/intelligence/dead-code.js +457 -0
  123. package/dist/intelligence/error-watch.js +263 -0
  124. package/dist/intelligence/navigation.js +141 -0
  125. package/dist/intelligence/stack-trace.js +210 -0
  126. package/dist/intelligence/symbol-index.js +410 -0
  127. package/dist/knowledge/auto-memory.js +412 -0
  128. package/dist/knowledge/conventions.js +475 -0
  129. package/dist/knowledge/corrections.js +213 -0
  130. package/dist/knowledge/rag.js +450 -0
  131. package/dist/knowledge/style-learner.js +324 -0
  132. package/dist/logger.js +35 -0
  133. package/dist/mcp/client.js +144 -0
  134. package/dist/mcp/config.js +24 -0
  135. package/dist/mcp/index.js +89 -0
  136. package/dist/modes/auto-compact.js +20 -0
  137. package/dist/modes/autopilot.js +157 -0
  138. package/dist/modes/background.js +82 -0
  139. package/dist/modes/interactive.js +187 -0
  140. package/dist/modes/oneshot.js +36 -0
  141. package/dist/modes/tui.js +265 -0
  142. package/dist/modes/turn.js +342 -0
  143. package/dist/notifications/manager.js +107 -0
  144. package/dist/plugins/marketplace.js +244 -0
  145. package/dist/providers/custom-provider.js +298 -0
  146. package/dist/providers/local-model.js +121 -0
  147. package/dist/routing/profiles.js +44 -0
  148. package/dist/routing/router.js +18 -0
  149. package/dist/sandbox/container.js +151 -0
  150. package/dist/security/audit.js +237 -0
  151. package/dist/security/content-filter.js +449 -0
  152. package/dist/security/proxy.js +301 -0
  153. package/dist/security/retention.js +281 -0
  154. package/dist/security/roles.js +252 -0
  155. package/dist/server/api-server.js +679 -0
  156. package/dist/session/bookmarks.js +72 -0
  157. package/dist/session/cloud-session.js +291 -0
  158. package/dist/session/handoff.js +405 -0
  159. package/dist/session/manager.js +35 -0
  160. package/dist/session/session.js +296 -0
  161. package/dist/session/share.js +313 -0
  162. package/dist/session/undo-journal.js +91 -0
  163. package/dist/snippets/store.js +60 -0
  164. package/dist/spaces/space-config.js +156 -0
  165. package/dist/spaces/space.js +220 -0
  166. package/dist/stats/store.js +101 -0
  167. package/dist/tools/apply-patch.js +134 -0
  168. package/dist/tools/auto-check.js +218 -0
  169. package/dist/tools/diff-edit.js +150 -0
  170. package/dist/tools/diff-prompt.js +36 -0
  171. package/dist/tools/edit-file.js +66 -0
  172. package/dist/tools/file-ops.js +205 -0
  173. package/dist/tools/glob.js +17 -0
  174. package/dist/tools/grep.js +56 -0
  175. package/dist/tools/image.js +194 -0
  176. package/dist/tools/list-directory.js +228 -0
  177. package/dist/tools/memory.js +17 -0
  178. package/dist/tools/multi-edit.js +299 -0
  179. package/dist/tools/policy.js +95 -0
  180. package/dist/tools/registry.js +484 -0
  181. package/dist/tools/retry.js +74 -0
  182. package/dist/tools/run-in-terminal.js +162 -0
  183. package/dist/tools/safety.js +64 -0
  184. package/dist/tools/sandbox.js +15 -0
  185. package/dist/tools/search-symbols.js +212 -0
  186. package/dist/tools/shell.js +118 -0
  187. package/dist/tools/web.js +167 -0
  188. package/dist/ui/prompt.js +37 -0
  189. package/dist/ui/render.js +96 -0
  190. package/dist/ui/screen.js +13 -0
  191. package/dist/ui/theme.js +56 -0
  192. package/dist/util/browser.js +34 -0
  193. package/dist/util/completion.js +350 -0
  194. package/dist/util/cost.js +28 -0
  195. package/dist/util/keybindings.js +113 -0
  196. package/dist/util/lazy.js +26 -0
  197. package/dist/util/perf.js +25 -0
  198. package/dist/util/token-worker.js +11 -0
  199. package/dist/util/tokens.js +50 -0
  200. package/dist/workflows/builtins.js +128 -0
  201. package/dist/workflows/engine.js +496 -0
  202. package/dist/workflows/file-trigger.js +197 -0
  203. package/package.json +79 -0
@@ -0,0 +1,162 @@
1
+ import { spawn } from 'node:child_process';
2
+ import path from 'node:path';
3
+ import { config } from '../config.js';
4
+ import { assertSandbox } from './sandbox.js';
5
+ const DEFAULT_TIMEOUT_MS = 30_000;
6
+ const MAX_OUTPUT_BYTES = 10 * 1024;
7
+ const TRUNCATION_NOTE = '\n…[output truncated after 10KB]';
8
+ export const runInTerminalSchema = {
9
+ type: 'function',
10
+ function: {
11
+ name: 'run_in_terminal',
12
+ description: 'Run a terminal command immediately with streamed stdout/stderr, optional cwd/env overrides, and timeout protection.',
13
+ parameters: {
14
+ type: 'object',
15
+ properties: {
16
+ command: {
17
+ type: 'string',
18
+ description: 'Command line to execute.',
19
+ },
20
+ cwd: {
21
+ type: 'string',
22
+ description: 'Optional working directory, relative to the current workspace unless absolute.',
23
+ },
24
+ timeout: {
25
+ type: 'number',
26
+ description: 'Optional timeout in milliseconds. Defaults to 30000.',
27
+ default: DEFAULT_TIMEOUT_MS,
28
+ },
29
+ env: {
30
+ type: 'object',
31
+ additionalProperties: { type: 'string' },
32
+ description: 'Optional environment variable overrides.',
33
+ },
34
+ },
35
+ required: ['command'],
36
+ },
37
+ },
38
+ };
39
+ export async function runInTerminal(args) {
40
+ const cwd = path.resolve(config.cwd, args.cwd || '.');
41
+ const timeoutMs = normalizeTimeout(args.timeout);
42
+ const env = {
43
+ ...process.env,
44
+ ...sanitizeEnv(args.env),
45
+ };
46
+ try {
47
+ assertSandbox(cwd, config.cwd);
48
+ }
49
+ catch (error) {
50
+ const message = error instanceof Error ? error.message : String(error);
51
+ return {
52
+ stdout: '',
53
+ stderr: message,
54
+ exitCode: -1,
55
+ timedOut: false,
56
+ truncated: false,
57
+ };
58
+ }
59
+ return new Promise((resolve) => {
60
+ const child = spawn(args.command, {
61
+ cwd,
62
+ env,
63
+ shell: true,
64
+ stdio: ['ignore', 'pipe', 'pipe'],
65
+ });
66
+ let stdout = '';
67
+ let stderr = '';
68
+ let capturedBytes = 0;
69
+ let timedOut = false;
70
+ let truncated = false;
71
+ let settled = false;
72
+ const timeoutId = setTimeout(() => {
73
+ timedOut = true;
74
+ child.kill();
75
+ }, timeoutMs);
76
+ const onStdout = (chunk) => {
77
+ const text = chunk.toString();
78
+ if (!config.jsonOutput)
79
+ process.stdout.write(text);
80
+ const result = appendOutput(stdout, text, capturedBytes, truncated);
81
+ stdout = result.output;
82
+ capturedBytes = result.capturedBytes;
83
+ truncated = result.truncated;
84
+ };
85
+ const onStderr = (chunk) => {
86
+ const text = chunk.toString();
87
+ if (!config.jsonOutput)
88
+ process.stderr.write(text);
89
+ const result = appendOutput(stderr, text, capturedBytes, truncated);
90
+ stderr = result.output;
91
+ capturedBytes = result.capturedBytes;
92
+ truncated = result.truncated;
93
+ };
94
+ child.stdout?.on('data', onStdout);
95
+ child.stderr?.on('data', onStderr);
96
+ child.once('error', (error) => {
97
+ if (settled)
98
+ return;
99
+ settled = true;
100
+ clearTimeout(timeoutId);
101
+ const result = appendOutput(stderr, error.message, capturedBytes, truncated);
102
+ resolve({
103
+ stdout,
104
+ stderr: result.output,
105
+ exitCode: -1,
106
+ timedOut,
107
+ truncated: result.truncated,
108
+ });
109
+ });
110
+ child.once('close', (code) => {
111
+ if (settled)
112
+ return;
113
+ settled = true;
114
+ clearTimeout(timeoutId);
115
+ resolve({
116
+ stdout,
117
+ stderr,
118
+ exitCode: code,
119
+ timedOut,
120
+ truncated,
121
+ });
122
+ });
123
+ });
124
+ }
125
+ function sanitizeEnv(env) {
126
+ if (!env)
127
+ return {};
128
+ return Object.fromEntries(Object.entries(env).filter((entry) => typeof entry[1] === 'string'));
129
+ }
130
+ function normalizeTimeout(timeout) {
131
+ if (typeof timeout !== 'number' || !Number.isFinite(timeout) || timeout <= 0) {
132
+ return DEFAULT_TIMEOUT_MS;
133
+ }
134
+ return Math.floor(timeout);
135
+ }
136
+ function appendOutput(current, chunk, capturedBytes, alreadyTruncated) {
137
+ if (alreadyTruncated || chunk.length === 0) {
138
+ return { output: current, capturedBytes, truncated: alreadyTruncated };
139
+ }
140
+ const availableBytes = MAX_OUTPUT_BYTES - capturedBytes;
141
+ if (availableBytes <= 0) {
142
+ return { output: withTruncationNote(current), capturedBytes, truncated: true };
143
+ }
144
+ const chunkBytes = Buffer.byteLength(chunk);
145
+ if (chunkBytes <= availableBytes) {
146
+ return {
147
+ output: current + chunk,
148
+ capturedBytes: capturedBytes + chunkBytes,
149
+ truncated: false,
150
+ };
151
+ }
152
+ const chunkBuffer = Buffer.from(chunk);
153
+ const visible = chunkBuffer.subarray(0, availableBytes).toString('utf8');
154
+ return {
155
+ output: withTruncationNote(current + visible),
156
+ capturedBytes: MAX_OUTPUT_BYTES,
157
+ truncated: true,
158
+ };
159
+ }
160
+ function withTruncationNote(output) {
161
+ return output.endsWith(TRUNCATION_NOTE) ? output : output + TRUNCATION_NOTE;
162
+ }
@@ -0,0 +1,64 @@
1
+ import { theme } from '../ui/theme.js';
2
+ const SAFE_CHECK = {
3
+ dangerous: false,
4
+ level: 'safe',
5
+ reason: '',
6
+ };
7
+ const FORK_BOMB_PATTERN = /:\s*\(\)\s*\{\s*:\s*\|\s*:\s*&\s*\}\s*;\s*:/;
8
+ const ROOT_DELETE_PATTERN = /\brm\s+-rf\b[^\n\r;|&]*?(?:^|\s)(?:\/(?:\s|$)|~(?:\s|$)|["']\/["']|["']~["'])/i;
9
+ const RECURSIVE_DELETE_PATTERN = /\brm\s+-rf\b/i;
10
+ const SQL_DESTRUCTIVE_PATTERN = /\b(?:drop\s+table|drop\s+database|truncate(?:\s+table)?)\b/i;
11
+ const FORCE_PUSH_PATTERN = /\bgit\s+push\b[^\n\r;|&]*(?:\s--force(?:-with-lease)?\b|\s-f\b)/i;
12
+ const CHMOD_WORLD_WRITABLE_PATTERN = /\bchmod\b[^\n\r;|&]*\b777\b/i;
13
+ const DEV_NULL_REDIRECT_PATTERN = /(?:^|\s)(?:\d*>)\s*\/dev\/null\b/;
14
+ const IMPORTANT_COMMAND_PATTERN = /\b(?:rm|git|chmod|chown|dd|mkfs(?:\.\w+)?|curl|wget|docker|kubectl|npm|pnpm|yarn|node|psql|mysql|sqlite3|systemctl|service)\b/i;
15
+ const PIPE_TO_SHELL_PATTERN = /\b(?:curl|wget)\b[^\n\r;|&]*\|\s*(?:sh|bash|zsh|ksh|fish)\b/i;
16
+ const MKFS_PATTERN = /\bmkfs(?:\.\w+)?\b/i;
17
+ const DD_TO_DEVICE_PATTERN = /\bdd\b[^\n\r;|&]*\bif=[^\s]+[^\n\r;|&]*\bof=\/dev\/\S+/i;
18
+ export function checkCommandSafety(command) {
19
+ const normalized = command.trim();
20
+ if (!normalized)
21
+ return SAFE_CHECK;
22
+ if (FORK_BOMB_PATTERN.test(normalized)) {
23
+ return critical('fork bomb can exhaust system resources');
24
+ }
25
+ if (ROOT_DELETE_PATTERN.test(normalized)) {
26
+ return critical('recursive delete of root/home');
27
+ }
28
+ if (SQL_DESTRUCTIVE_PATTERN.test(normalized)) {
29
+ return critical('destructive SQL command detected');
30
+ }
31
+ if (MKFS_PATTERN.test(normalized) || DD_TO_DEVICE_PATTERN.test(normalized)) {
32
+ return critical('disk formatting or raw device overwrite detected');
33
+ }
34
+ if (RECURSIVE_DELETE_PATTERN.test(normalized)) {
35
+ return warn('recursive delete may remove many files');
36
+ }
37
+ if (FORCE_PUSH_PATTERN.test(normalized)) {
38
+ return warn('force push may overwrite remote history');
39
+ }
40
+ if (CHMOD_WORLD_WRITABLE_PATTERN.test(normalized)) {
41
+ return warn('world-writable permissions');
42
+ }
43
+ if (DEV_NULL_REDIRECT_PATTERN.test(normalized) && IMPORTANT_COMMAND_PATTERN.test(normalized)) {
44
+ return warn('output redirection may hide failures from an important command');
45
+ }
46
+ if (PIPE_TO_SHELL_PATTERN.test(normalized)) {
47
+ return warn('piping remote scripts to a shell can execute untrusted code');
48
+ }
49
+ return SAFE_CHECK;
50
+ }
51
+ export function formatSafetyWarning(check) {
52
+ if (!check.dangerous || check.level === 'safe')
53
+ return '';
54
+ if (check.level === 'critical') {
55
+ return theme.err(`Critical: ${check.reason}`);
56
+ }
57
+ return theme.warn(`Warning: ${check.reason}`);
58
+ }
59
+ function warn(reason) {
60
+ return { dangerous: true, level: 'warn', reason };
61
+ }
62
+ function critical(reason) {
63
+ return { dangerous: true, level: 'critical', reason };
64
+ }
@@ -0,0 +1,15 @@
1
+ import path from 'node:path';
2
+ export function isSandboxed() {
3
+ return process.env.ICOPILOT_SANDBOX === '1';
4
+ }
5
+ export function pathInSandbox(absPath, cwdRoot) {
6
+ const resolvedPath = path.resolve(absPath);
7
+ const resolvedRoot = path.resolve(cwdRoot);
8
+ const rel = path.relative(resolvedRoot, resolvedPath);
9
+ return rel === '' || (!rel.startsWith('..') && !path.isAbsolute(rel));
10
+ }
11
+ export function assertSandbox(absPath, cwdRoot) {
12
+ if (isSandboxed() && !pathInSandbox(absPath, cwdRoot)) {
13
+ throw new Error(`sandbox violation: ${path.resolve(absPath)} outside ${path.resolve(cwdRoot)}`);
14
+ }
15
+ }
@@ -0,0 +1,212 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import fg from 'fast-glob';
4
+ import { config } from '../config.js';
5
+ import { assertSandbox } from './sandbox.js';
6
+ const RESULT_LIMIT = 50;
7
+ const DEFAULT_FILE_PATTERNS = [
8
+ '**/*.{ts,tsx,mts,cts}',
9
+ '**/*.{js,jsx,mjs,cjs}',
10
+ '**/*.py',
11
+ '**/*.go',
12
+ '**/*.rs',
13
+ ];
14
+ const PATTERN_LIBRARY = {
15
+ typescript: [
16
+ {
17
+ type: 'function',
18
+ regex: /^\s*(?:export\s+)?(?:default\s+)?(?:async\s+)?function\s+(?<name>[A-Za-z_$][\w$]*)\b/,
19
+ },
20
+ {
21
+ type: 'class',
22
+ regex: /^\s*(?:export\s+)?(?:default\s+)?class\s+(?<name>[A-Za-z_$][\w$]*)\b/,
23
+ },
24
+ {
25
+ type: 'interface',
26
+ regex: /^\s*(?:export\s+)?(?:default\s+)?interface\s+(?<name>[A-Za-z_$][\w$]*)\b/,
27
+ },
28
+ { type: 'type', regex: /^\s*(?:export\s+)?type\s+(?<name>[A-Za-z_$][\w$]*)\b/ },
29
+ {
30
+ type: 'variable',
31
+ regex: /^\s*(?:export\s+)?(?:const|let|var)\s+(?<name>[A-Za-z_$][\w$]*)\b/,
32
+ },
33
+ ],
34
+ javascript: [
35
+ {
36
+ type: 'function',
37
+ regex: /^\s*(?:export\s+)?(?:default\s+)?(?:async\s+)?function\s+(?<name>[A-Za-z_$][\w$]*)\b/,
38
+ },
39
+ {
40
+ type: 'class',
41
+ regex: /^\s*(?:export\s+)?(?:default\s+)?class\s+(?<name>[A-Za-z_$][\w$]*)\b/,
42
+ },
43
+ {
44
+ type: 'variable',
45
+ regex: /^\s*(?:export\s+)?(?:const|let|var)\s+(?<name>[A-Za-z_$][\w$]*)\b/,
46
+ },
47
+ ],
48
+ python: [
49
+ { type: 'function', regex: /^\s*def\s+(?<name>[A-Za-z_][\w]*)\b/ },
50
+ { type: 'class', regex: /^\s*class\s+(?<name>[A-Za-z_][\w]*)\b/ },
51
+ { type: 'variable', regex: /^(?<name>[A-Za-z_][\w]*)\s*(?::[^=]+)?=/ },
52
+ ],
53
+ go: [
54
+ { type: 'function', regex: /^\s*func\s*(?:\([^)]*\)\s*)?(?<name>[A-Za-z_][\w]*)\s*\(/ },
55
+ { type: 'interface', regex: /^\s*type\s+(?<name>[A-Za-z_][\w]*)\s+interface\b/ },
56
+ { type: 'type', regex: /^\s*type\s+(?<name>[A-Za-z_][\w]*)\s+(?!interface\b).+/ },
57
+ { type: 'variable', regex: /^\s*(?:var|const)\s+(?<name>[A-Za-z_][\w]*)\b/ },
58
+ ],
59
+ rust: [
60
+ { type: 'function', regex: /^\s*(?:pub\s+)?(?:async\s+)?fn\s+(?<name>[A-Za-z_][\w]*)\b/ },
61
+ { type: 'interface', regex: /^\s*(?:pub\s+)?trait\s+(?<name>[A-Za-z_][\w]*)\b/ },
62
+ { type: 'type', regex: /^\s*(?:pub\s+)?(?:struct|enum|type)\s+(?<name>[A-Za-z_][\w]*)\b/ },
63
+ {
64
+ type: 'variable',
65
+ regex: /^\s*(?:pub\s+)?(?:const|static)\s+(?:mut\s+)?(?<name>[A-Za-z_][\w]*)\b/,
66
+ },
67
+ ],
68
+ };
69
+ export const searchSymbolsSchema = {
70
+ type: 'function',
71
+ function: {
72
+ name: 'search_symbols',
73
+ description: 'Search source files for symbol declarations matching a regex query.',
74
+ parameters: {
75
+ type: 'object',
76
+ properties: {
77
+ query: {
78
+ type: 'string',
79
+ description: 'Regular expression used to match symbol names or declaration signatures.',
80
+ },
81
+ filePattern: {
82
+ type: 'string',
83
+ description: 'Optional fast-glob file pattern to narrow the search scope.',
84
+ },
85
+ type: {
86
+ type: 'string',
87
+ enum: ['function', 'class', 'variable', 'interface', 'type', 'all'],
88
+ description: 'Optional symbol kind filter.',
89
+ },
90
+ },
91
+ required: ['query'],
92
+ },
93
+ },
94
+ };
95
+ export async function searchSymbols(args) {
96
+ const root = path.resolve(config.cwd);
97
+ assertSandbox(root, config.cwd);
98
+ const query = compileQuery(args.query);
99
+ const typeFilter = args.type ?? 'all';
100
+ const files = await fg(args.filePattern ? [args.filePattern] : [...DEFAULT_FILE_PATTERNS], {
101
+ cwd: root,
102
+ onlyFiles: true,
103
+ dot: false,
104
+ unique: true,
105
+ ignore: ['**/node_modules/**', '**/dist/**', '**/.git/**', ...loadGitIgnorePatterns(root)],
106
+ });
107
+ const results = [];
108
+ for (const file of files) {
109
+ if (results.length >= RESULT_LIMIT)
110
+ break;
111
+ const language = detectLanguage(file);
112
+ if (!language)
113
+ continue;
114
+ const matches = searchFile(path.join(root, file), path.relative(config.cwd, path.join(root, file)), language, query, typeFilter);
115
+ for (const match of matches) {
116
+ results.push(match);
117
+ if (results.length >= RESULT_LIMIT)
118
+ break;
119
+ }
120
+ }
121
+ return JSON.stringify(results);
122
+ }
123
+ function searchFile(absoluteFile, relativeFile, language, query, typeFilter) {
124
+ let content;
125
+ try {
126
+ content = fs.readFileSync(absoluteFile, 'utf8');
127
+ }
128
+ catch {
129
+ return [];
130
+ }
131
+ const patterns = PATTERN_LIBRARY[language];
132
+ const results = [];
133
+ const lines = content.split(/\r?\n/);
134
+ for (let index = 0; index < lines.length; index += 1) {
135
+ const line = lines[index] ?? '';
136
+ for (const pattern of patterns) {
137
+ if (typeFilter !== 'all' && pattern.type !== typeFilter)
138
+ continue;
139
+ const match = pattern.regex.exec(line);
140
+ const name = match?.groups?.name;
141
+ if (!name)
142
+ continue;
143
+ const signature = line.trim();
144
+ if (!query.test(name) && !query.test(signature))
145
+ continue;
146
+ results.push({
147
+ name,
148
+ type: pattern.type,
149
+ file: relativeFile,
150
+ line: index + 1,
151
+ signature,
152
+ });
153
+ break;
154
+ }
155
+ }
156
+ return results;
157
+ }
158
+ function compileQuery(query) {
159
+ try {
160
+ return new RegExp(query);
161
+ }
162
+ catch {
163
+ return new RegExp(escapeRegex(query));
164
+ }
165
+ }
166
+ function detectLanguage(file) {
167
+ const extension = path.extname(file).toLowerCase();
168
+ switch (extension) {
169
+ case '.ts':
170
+ case '.tsx':
171
+ case '.mts':
172
+ case '.cts':
173
+ return 'typescript';
174
+ case '.js':
175
+ case '.jsx':
176
+ case '.mjs':
177
+ case '.cjs':
178
+ return 'javascript';
179
+ case '.py':
180
+ return 'python';
181
+ case '.go':
182
+ return 'go';
183
+ case '.rs':
184
+ return 'rust';
185
+ default:
186
+ return null;
187
+ }
188
+ }
189
+ function escapeRegex(value) {
190
+ return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
191
+ }
192
+ function loadGitIgnorePatterns(root) {
193
+ const gitIgnorePath = path.join(root, '.gitignore');
194
+ if (!fs.existsSync(gitIgnorePath))
195
+ return [];
196
+ try {
197
+ return fs
198
+ .readFileSync(gitIgnorePath, 'utf8')
199
+ .split(/\r?\n/)
200
+ .map((line) => line.trim())
201
+ .filter((line) => line !== '' && !line.startsWith('#') && !line.startsWith('!'))
202
+ .flatMap((line) => {
203
+ const normalized = line.replace(/^\.\//, '').replace(/^\/+/, '');
204
+ if (normalized.endsWith('/'))
205
+ return [normalized, `${normalized}**`];
206
+ return [normalized];
207
+ });
208
+ }
209
+ catch {
210
+ return [];
211
+ }
212
+ }
@@ -0,0 +1,118 @@
1
+ import { spawn } from 'node:child_process';
2
+ import path from 'node:path';
3
+ import { confirm, input } from '@inquirer/prompts';
4
+ import { config } from '../config.js';
5
+ import { theme } from '../ui/theme.js';
6
+ import { toolMemory } from './memory.js';
7
+ import { loadPolicy, shellCommandAllowed } from './policy.js';
8
+ import { assertSandbox } from './sandbox.js';
9
+ import { checkCommandSafety } from './safety.js';
10
+ /**
11
+ * Propose a shell command, require confirmation, then run it.
12
+ * Output is streamed to the terminal AND captured for return.
13
+ */
14
+ export async function proposeAndRun(cmd, opts = {}) {
15
+ const cwd = path.resolve(opts.cwd || config.cwd);
16
+ const policy = loadPolicy(config.cwd);
17
+ if (!shellCommandAllowed(cmd, policy)) {
18
+ process.stdout.write(theme.err(' policy denied\n'));
19
+ return { ran: false, exitCode: null, stdout: '', stderr: 'policy denied' };
20
+ }
21
+ try {
22
+ assertSandbox(cwd, config.cwd);
23
+ }
24
+ catch (e) {
25
+ const message = e?.message || String(e);
26
+ process.stdout.write(theme.err(` ${message}\n`));
27
+ return { ran: false, exitCode: null, stdout: '', stderr: message };
28
+ }
29
+ if (!config.quiet && !config.jsonOutput) {
30
+ process.stdout.write('\n' + theme.badge('SHELL') + '\n');
31
+ if (opts.explain)
32
+ process.stdout.write(theme.dim(opts.explain) + '\n');
33
+ process.stdout.write(theme.hl(' $ ') + cmd + '\n');
34
+ process.stdout.write(theme.dim(` cwd: ${cwd}\n`));
35
+ }
36
+ const safety = checkCommandSafety(cmd);
37
+ const remembered = toolMemory.isShellRemembered(cmd);
38
+ let ok = false;
39
+ if (config.autoApprove && safety.level !== 'critical') {
40
+ ok = true;
41
+ }
42
+ else if (safety.level === 'critical') {
43
+ ok = await approveCriticalCommand(safety.reason).catch(() => false);
44
+ }
45
+ else if (remembered && safety.level === 'safe') {
46
+ ok = true;
47
+ }
48
+ else {
49
+ ok = await approveCommand(safety.reason, safety.level === 'warn');
50
+ }
51
+ if (!ok) {
52
+ if (!config.jsonOutput)
53
+ process.stdout.write(theme.warn(' skipped.\n'));
54
+ return { ran: false, exitCode: null, stdout: '', stderr: '' };
55
+ }
56
+ if (!config.autoApprove && !remembered) {
57
+ const remember = await confirm({
58
+ message: 'Remember this command for the session?',
59
+ default: false,
60
+ }).catch(() => false);
61
+ if (remember)
62
+ toolMemory.rememberShell(cmd);
63
+ }
64
+ return runCaptured(cmd, cwd);
65
+ }
66
+ async function approveCommand(reason, warned) {
67
+ if (warned && !config.quiet && !config.jsonOutput) {
68
+ process.stdout.write(theme.warn(` Warning: ${reason}\n`));
69
+ }
70
+ return confirm({
71
+ message: 'Run this command?',
72
+ default: false,
73
+ }).catch(() => false);
74
+ }
75
+ async function approveCriticalCommand(reason) {
76
+ if (config.autoApprove) {
77
+ if (!config.quiet && !config.jsonOutput) {
78
+ process.stdout.write(theme.err(` blocked critical command: ${reason}\n`));
79
+ }
80
+ return false;
81
+ }
82
+ process.stdout.write(theme.err(' !!! CRITICAL COMMAND WARNING !!!\n'));
83
+ process.stdout.write(theme.err(` Reason: ${reason}\n`));
84
+ const answer = await input({
85
+ message: 'Type "yes" to run this critical command:',
86
+ default: '',
87
+ }).catch(() => '');
88
+ return answer.trim() === 'yes';
89
+ }
90
+ function runCaptured(cmd, cwd) {
91
+ return new Promise((resolve) => {
92
+ const isWin = process.platform === 'win32';
93
+ const shell = isWin ? 'powershell.exe' : 'bash';
94
+ const args = isWin ? ['-NoProfile', '-Command', cmd] : ['-lc', cmd];
95
+ const child = spawn(shell, args, { cwd, stdio: ['ignore', 'pipe', 'pipe'] });
96
+ let stdout = '';
97
+ let stderr = '';
98
+ child.stdout.on('data', (d) => {
99
+ const s = d.toString();
100
+ stdout += s;
101
+ if (!config.jsonOutput)
102
+ process.stdout.write(s);
103
+ });
104
+ child.stderr.on('data', (d) => {
105
+ const s = d.toString();
106
+ stderr += s;
107
+ if (!config.jsonOutput)
108
+ process.stderr.write(s);
109
+ });
110
+ child.on('close', (code) => {
111
+ resolve({ ran: true, exitCode: code, stdout, stderr });
112
+ });
113
+ child.on('error', (err) => {
114
+ stderr += String(err);
115
+ resolve({ ran: true, exitCode: -1, stdout, stderr });
116
+ });
117
+ });
118
+ }