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,218 @@
1
+ import { spawn } from 'node:child_process';
2
+ import path from 'node:path';
3
+ import { detectLinters } from '../commands/lint-cmd.js';
4
+ import { detectTestFrameworks } from '../commands/test-cmd.js';
5
+ import { config } from '../config.js';
6
+ export const AUTO_FIX_MAX_RETRIES = 3;
7
+ export function getAutoCheckConfig() {
8
+ return {
9
+ autoLint: config.autoLint,
10
+ autoTest: config.autoTest,
11
+ autoFix: config.autoFix,
12
+ lintCmd: config.lintCmd.trim() || undefined,
13
+ testCmd: config.testCmd.trim() || undefined,
14
+ };
15
+ }
16
+ export function detectAutoLintCommand(cwd = config.cwd) {
17
+ const configured = config.lintCmd.trim();
18
+ if (configured)
19
+ return configured;
20
+ const linters = detectLinters(cwd);
21
+ const preferred = linters.find((entry) => entry.name === 'eslint' || entry.name === 'eslint-config') ??
22
+ linters.find((entry) => entry.name === 'npm-lint') ??
23
+ linters[0];
24
+ return preferred?.command;
25
+ }
26
+ export function detectAutoTestCommand(cwd = config.cwd) {
27
+ const configured = config.testCmd.trim();
28
+ if (configured)
29
+ return configured;
30
+ const frameworks = detectTestFrameworks(cwd);
31
+ const preferred = frameworks.find((entry) => entry.name === 'npm-test') ??
32
+ frameworks.find((entry) => entry.name === 'vitest') ??
33
+ frameworks.find((entry) => entry.name === 'jest') ??
34
+ frameworks[0];
35
+ return preferred?.command;
36
+ }
37
+ export async function runAutoLint(changedFiles) {
38
+ const normalizedFiles = normalizeFiles(changedFiles);
39
+ if (!normalizedFiles.length) {
40
+ return { passed: true, output: 'No changed files to lint.', fixable: false };
41
+ }
42
+ const lintableFiles = normalizedFiles.filter((file) => /\.(?:[cm]?[jt]sx?|py|go|java|cs|php|rb)$/iu.test(file));
43
+ if (!lintableFiles.length) {
44
+ return { passed: true, output: 'No lintable changed files detected.', fixable: false };
45
+ }
46
+ const command = buildLintCommand(lintableFiles, config.cwd, getAutoCheckConfig());
47
+ if (!command) {
48
+ return {
49
+ passed: false,
50
+ output: 'No configured linter detected for auto-lint.',
51
+ fixable: false,
52
+ };
53
+ }
54
+ const result = await executeShellCommand(command, config.cwd);
55
+ return toAutoCheckResult(result, { missingCommand: false });
56
+ }
57
+ export async function runAutoTest() {
58
+ const command = buildTestCommand(config.cwd, getAutoCheckConfig());
59
+ if (!command) {
60
+ return {
61
+ passed: false,
62
+ output: 'No configured test runner detected for auto-test.',
63
+ fixable: false,
64
+ };
65
+ }
66
+ const result = await executeShellCommand(command, config.cwd);
67
+ return toAutoCheckResult(result, { missingCommand: false });
68
+ }
69
+ export function extractChangedFilesFromToolResult(toolName, args, output) {
70
+ const parsed = tryParseJson(output);
71
+ switch (toolName) {
72
+ case 'write_file':
73
+ return parsed?.wrote && typeof args.path === 'string' ? [args.path] : [];
74
+ case 'write_files':
75
+ return parsed?.wrote && Array.isArray(args.items)
76
+ ? args.items
77
+ .map((item) => item && typeof item === 'object'
78
+ ? String(item.path ?? '')
79
+ : '')
80
+ .filter(Boolean)
81
+ : [];
82
+ case 'edit_file':
83
+ return parsed?.ok && typeof args.path === 'string' ? [args.path] : [];
84
+ case 'multi_edit':
85
+ return Array.isArray(parsed?.applied)
86
+ ? parsed.applied.filter((entry) => typeof entry === 'string')
87
+ : [];
88
+ case 'apply_patch':
89
+ return Array.isArray(parsed?.applied)
90
+ ? parsed.applied
91
+ .map((entry) => (typeof entry?.path === 'string' ? entry.path : ''))
92
+ .filter(Boolean)
93
+ : [];
94
+ default:
95
+ return [];
96
+ }
97
+ }
98
+ export function extractAutoLintResult(output) {
99
+ const parsed = tryParseJson(output);
100
+ const candidate = parsed?.autoLint;
101
+ if (!candidate || typeof candidate !== 'object')
102
+ return undefined;
103
+ if (typeof candidate.passed !== 'boolean' || typeof candidate.output !== 'string')
104
+ return undefined;
105
+ return {
106
+ passed: candidate.passed,
107
+ output: candidate.output,
108
+ fixable: Boolean(candidate.fixable),
109
+ };
110
+ }
111
+ export function formatAutoCheckResult(kind, result, changedFiles = []) {
112
+ const header = kind === 'lint' ? 'AUTO LINT' : 'AUTO TEST';
113
+ const label = changedFiles.length ? ` ${changedFiles.join(', ')}` : '';
114
+ const summary = result.passed ? 'passed' : 'failed';
115
+ const detail = result.output.trim();
116
+ return [`${header}${label}: ${summary}`, detail].filter(Boolean).join('\n');
117
+ }
118
+ export function buildAutoFixPrompt(kind, result, attempt, changedFiles = []) {
119
+ const scope = changedFiles.length ? `Files: ${changedFiles.join(', ')}\n` : '';
120
+ return [
121
+ `Automatic ${kind} failed after your recent edits.`,
122
+ `Retry ${attempt}/${AUTO_FIX_MAX_RETRIES}.`,
123
+ scope.trimEnd(),
124
+ 'Fix the reported problem using the available file-editing tools, then stop so the check can re-run.',
125
+ '',
126
+ result.output.trim(),
127
+ ]
128
+ .filter(Boolean)
129
+ .join('\n');
130
+ }
131
+ function buildLintCommand(lintableFiles, cwd, overrides) {
132
+ const configured = overrides.lintCmd?.trim();
133
+ if (configured)
134
+ return injectFiles(configured, lintableFiles);
135
+ const linters = detectLinters(cwd);
136
+ if (!linters.length)
137
+ return undefined;
138
+ const explicitEslint = linters.some((entry) => entry.name === 'eslint' || entry.name === 'eslint-config');
139
+ if (explicitEslint) {
140
+ return ['npx', 'eslint', ...lintableFiles].map(quoteArg).join(' ');
141
+ }
142
+ const preferred = linters.find((entry) => entry.name === 'npm-lint') ?? linters[0];
143
+ return injectFiles(preferred.command, lintableFiles);
144
+ }
145
+ function buildTestCommand(cwd, overrides) {
146
+ const configured = overrides.testCmd?.trim();
147
+ if (configured)
148
+ return configured;
149
+ const frameworks = detectTestFrameworks(cwd);
150
+ if (!frameworks.length)
151
+ return undefined;
152
+ const preferred = frameworks.find((entry) => entry.name === 'npm-test') ??
153
+ frameworks.find((entry) => entry.name === 'vitest') ??
154
+ frameworks.find((entry) => entry.name === 'jest') ??
155
+ frameworks[0];
156
+ return preferred?.command;
157
+ }
158
+ async function executeShellCommand(command, cwd) {
159
+ return new Promise((resolve) => {
160
+ const isWin = process.platform === 'win32';
161
+ const shell = isWin ? process.env.ComSpec || 'cmd.exe' : 'bash';
162
+ const args = isWin ? ['/d', '/s', '/c', command] : ['-lc', command];
163
+ const child = spawn(shell, args, {
164
+ cwd,
165
+ stdio: ['ignore', 'pipe', 'pipe'],
166
+ windowsHide: true,
167
+ });
168
+ let output = '';
169
+ child.stdout.on('data', (chunk) => {
170
+ output += String(chunk);
171
+ });
172
+ child.stderr.on('data', (chunk) => {
173
+ output += String(chunk);
174
+ });
175
+ child.on('close', (code) => {
176
+ resolve({ exitCode: code ?? -1, output });
177
+ });
178
+ child.on('error', (error) => {
179
+ resolve({ exitCode: -1, output: `${output}${error.message}` });
180
+ });
181
+ });
182
+ }
183
+ function toAutoCheckResult(result, options) {
184
+ const output = result.output.trim();
185
+ const commandMissing = options.missingCommand ||
186
+ /(?:is not recognized as|not found|No configured .* detected|could not find)/iu.test(output);
187
+ return {
188
+ passed: result.exitCode === 0,
189
+ output: output || (result.exitCode === 0 ? 'Check passed.' : 'Check failed with no output.'),
190
+ fixable: result.exitCode !== 0 && !commandMissing,
191
+ };
192
+ }
193
+ function injectFiles(command, files) {
194
+ if (!files.length)
195
+ return command;
196
+ const fileArgs = files.map(quoteArg).join(' ');
197
+ if (command.includes('{files}')) {
198
+ return command.replace(/\{files\}/gu, fileArgs);
199
+ }
200
+ if (/^(?:npm|pnpm|yarn)\s+/iu.test(command)) {
201
+ return `${command} -- ${fileArgs}`;
202
+ }
203
+ return `${command} ${fileArgs}`;
204
+ }
205
+ function normalizeFiles(files) {
206
+ return [...new Set(files.map((file) => path.normalize(file)).filter(Boolean))];
207
+ }
208
+ function quoteArg(value) {
209
+ return `"${value.replace(/(["\\$`])/gu, '\\$1')}"`;
210
+ }
211
+ function tryParseJson(value) {
212
+ try {
213
+ return JSON.parse(value);
214
+ }
215
+ catch {
216
+ return null;
217
+ }
218
+ }
@@ -0,0 +1,150 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import { config } from '../config.js';
4
+ const DIFF_BLOCK_PATTERN = /<<<<<<< SEARCH\r?\nfilepath:\s*(.+?)\r?\n([\s\S]*?)\r?\n=======\r?\n([\s\S]*?)\r?\n>>>>>>> REPLACE/g;
5
+ export function parseDiffBlocks(content) {
6
+ const blocks = [];
7
+ for (const match of content.matchAll(DIFF_BLOCK_PATTERN)) {
8
+ const [, filePath = '', search = '', replace = ''] = match;
9
+ const normalizedPath = filePath.trim();
10
+ if (!normalizedPath)
11
+ continue;
12
+ blocks.push({
13
+ filePath: normalizedPath,
14
+ search: normalizeEol(search),
15
+ replace: normalizeEol(replace),
16
+ });
17
+ }
18
+ return blocks;
19
+ }
20
+ export function applyDiffBlocks(blocks) {
21
+ const results = [];
22
+ const states = new Map();
23
+ for (const block of blocks) {
24
+ try {
25
+ const state = getFileState(states, block.filePath);
26
+ state.content = applyBlock(state.content, block);
27
+ fs.writeFileSync(state.absPath, restoreEol(state.content, state.eol), 'utf8');
28
+ results.push({ filePath: block.filePath, success: true });
29
+ }
30
+ catch (error) {
31
+ results.push({
32
+ filePath: block.filePath,
33
+ success: false,
34
+ error: error instanceof Error ? error.message : String(error),
35
+ });
36
+ }
37
+ }
38
+ return results;
39
+ }
40
+ function getFileState(states, filePath) {
41
+ const cached = states.get(filePath);
42
+ if (cached)
43
+ return cached;
44
+ const absPath = path.resolve(config.cwd, filePath);
45
+ if (!fs.existsSync(absPath)) {
46
+ throw new Error(`file not found: ${filePath}`);
47
+ }
48
+ const raw = fs.readFileSync(absPath, 'utf8');
49
+ const state = {
50
+ absPath,
51
+ eol: raw.includes('\r\n') ? '\r\n' : '\n',
52
+ content: normalizeEol(raw),
53
+ };
54
+ states.set(filePath, state);
55
+ return state;
56
+ }
57
+ function applyBlock(content, block) {
58
+ if (!block.search.trim()) {
59
+ throw new Error(`search text must not be empty for ${block.filePath}`);
60
+ }
61
+ const exactMatches = findAllOccurrences(content, block.search);
62
+ if (exactMatches.length === 1) {
63
+ const index = exactMatches[0];
64
+ return content.slice(0, index) + block.replace + content.slice(index + block.search.length);
65
+ }
66
+ if (exactMatches.length > 1) {
67
+ throw new Error(`search text matched multiple locations in ${block.filePath}`);
68
+ }
69
+ const fuzzyMatch = findFuzzyMatch(content, block.search);
70
+ if (!fuzzyMatch) {
71
+ throw new Error(`search text not found in ${block.filePath}`);
72
+ }
73
+ if (fuzzyMatch.ambiguous) {
74
+ throw new Error(`search text matched multiple locations in ${block.filePath}`);
75
+ }
76
+ const lines = content.split('\n');
77
+ const replacementLines = splitReplacementLines(block.replace);
78
+ return [
79
+ ...lines.slice(0, fuzzyMatch.startLine),
80
+ ...replacementLines,
81
+ ...lines.slice(fuzzyMatch.endLine),
82
+ ].join('\n');
83
+ }
84
+ function findAllOccurrences(source, target) {
85
+ if (!target)
86
+ return [];
87
+ const matches = [];
88
+ let offset = 0;
89
+ while (offset <= source.length) {
90
+ const index = source.indexOf(target, offset);
91
+ if (index === -1)
92
+ break;
93
+ matches.push(index);
94
+ offset = index + target.length;
95
+ }
96
+ return matches;
97
+ }
98
+ function findFuzzyMatch(content, search) {
99
+ const fileLines = content.split('\n');
100
+ const searchLines = stripBlankEdges(search.split('\n'));
101
+ if (searchLines.length === 0)
102
+ return null;
103
+ const normalizedSearch = searchLines.map(normalizeComparableLine);
104
+ const maxExtraLines = Math.max(4, search.split('\n').length - searchLines.length + 4);
105
+ const matches = new Map();
106
+ for (let start = 0; start < fileLines.length; start += 1) {
107
+ const minEnd = start + searchLines.length;
108
+ const maxEnd = Math.min(fileLines.length, minEnd + maxExtraLines);
109
+ for (let end = minEnd; end <= maxEnd; end += 1) {
110
+ const candidate = stripBlankEdges(fileLines.slice(start, end));
111
+ if (candidate.length !== normalizedSearch.length)
112
+ continue;
113
+ const normalizedCandidate = candidate.map(normalizeComparableLine);
114
+ if (normalizedCandidate.every((line, index) => line === normalizedSearch[index])) {
115
+ matches.set(`${start}:${end}`, { startLine: start, endLine: end });
116
+ }
117
+ }
118
+ }
119
+ if (matches.size === 0)
120
+ return null;
121
+ if (matches.size > 1) {
122
+ const [first] = matches.values();
123
+ return first ? { ...first, ambiguous: true } : null;
124
+ }
125
+ const [match] = matches.values();
126
+ return match ? { ...match, ambiguous: false } : null;
127
+ }
128
+ function splitReplacementLines(replace) {
129
+ if (replace === '')
130
+ return [];
131
+ return replace.split('\n');
132
+ }
133
+ function stripBlankEdges(lines) {
134
+ let start = 0;
135
+ let end = lines.length;
136
+ while (start < end && lines[start]?.trim() === '')
137
+ start += 1;
138
+ while (end > start && lines[end - 1]?.trim() === '')
139
+ end -= 1;
140
+ return lines.slice(start, end);
141
+ }
142
+ function normalizeComparableLine(line) {
143
+ return line.replace(/\s+/g, '');
144
+ }
145
+ function normalizeEol(value) {
146
+ return value.replace(/\r\n/g, '\n');
147
+ }
148
+ function restoreEol(value, eol) {
149
+ return eol === '\n' ? value : value.replace(/\n/g, '\r\n');
150
+ }
@@ -0,0 +1,36 @@
1
+ const BASE_SYSTEM_PROMPT = `You are iCopilot, a terminal-native coding assistant powered by GitHub Models.
2
+
3
+ Operating principles:
4
+ - Be concise. Default to short, direct answers; expand only when warranted.
5
+ - Render code in fenced blocks with the correct language tag.
6
+ - When you propose any change to the user's machine, use a tool call:
7
+ • run_shell for shell commands
8
+ • write_file for creating/overwriting files
9
+ • read_file for reading files
10
+ - Never claim to have run a command or written a file unless a tool call returned success.
11
+ - When the user references files with @path, those files are already injected; do not re-read them with read_file.
12
+ - Prefer surgical edits. State assumptions explicitly.`;
13
+ const DIFF_EDIT_SYSTEM_PROMPT = `${BASE_SYSTEM_PROMPT}
14
+
15
+ When you need to propose direct code edits in plain text, prefer SEARCH/REPLACE blocks over whole-file rewrites:
16
+ <<<<<<< SEARCH
17
+ filepath: src/example.ts
18
+ old code here that exists in file
19
+ =======
20
+ new replacement code
21
+ >>>>>>> REPLACE
22
+
23
+ Rules:
24
+ - Use one SEARCH/REPLACE block per localized change. Multiple blocks per file are allowed.
25
+ - The filepath line is required in every block.
26
+ - SEARCH content should be copied from the current file and include enough nearby context to be unique.
27
+ - Keep edits minimal; do not rewrite the whole file unless the user explicitly asks for that.`;
28
+ export function getDiffEditSystemPrompt() {
29
+ return DIFF_EDIT_SYSTEM_PROMPT;
30
+ }
31
+ export function getWholeFileSystemPrompt() {
32
+ return BASE_SYSTEM_PROMPT;
33
+ }
34
+ export function getEditFormatPrompt(format) {
35
+ return format === 'whole' ? getWholeFileSystemPrompt() : getDiffEditSystemPrompt();
36
+ }
@@ -0,0 +1,66 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import { config } from '../config.js';
4
+ import { proposeWrite } from './file-ops.js';
5
+ export async function editFileTool(args) {
6
+ const startLine = Number(args.startLine);
7
+ const endLine = Number(args.endLine);
8
+ const newLines = args.newContent.split('\n');
9
+ const linesReplaced = Number.isInteger(startLine) && Number.isInteger(endLine) ? endLine - startLine + 1 : 0;
10
+ try {
11
+ if (!Number.isInteger(startLine)) {
12
+ return JSON.stringify(result(false, linesReplaced, newLines.length, 'startLine must be an integer'));
13
+ }
14
+ if (!Number.isInteger(endLine)) {
15
+ return JSON.stringify(result(false, linesReplaced, newLines.length, 'endLine must be an integer'));
16
+ }
17
+ if (startLine < 1) {
18
+ return JSON.stringify(result(false, linesReplaced, newLines.length, 'startLine must be >= 1'));
19
+ }
20
+ if (endLine < startLine) {
21
+ return JSON.stringify(result(false, linesReplaced, newLines.length, 'endLine must be >= startLine'));
22
+ }
23
+ const abs = path.resolve(config.cwd, args.path);
24
+ if (!fs.existsSync(abs)) {
25
+ return JSON.stringify(result(false, linesReplaced, newLines.length, `file not found: ${args.path}`));
26
+ }
27
+ const current = fs.readFileSync(abs, 'utf8');
28
+ const lines = current.split('\n');
29
+ if (endLine > lines.length) {
30
+ return JSON.stringify(result(false, linesReplaced, newLines.length, `line range ${startLine}-${endLine} is out of bounds for ${lines.length}-line file`));
31
+ }
32
+ const updated = [...lines.slice(0, startLine - 1), ...newLines, ...lines.slice(endLine)].join('\n');
33
+ const write = await proposeWrite(args.path, updated);
34
+ if (!write.wrote) {
35
+ return JSON.stringify(result(false, linesReplaced, newLines.length, write.error || 'edit rejected'));
36
+ }
37
+ return JSON.stringify(result(true, linesReplaced, newLines.length, undefined, write.autoLint));
38
+ }
39
+ catch (e) {
40
+ const message = e instanceof Error ? e.message : String(e);
41
+ return JSON.stringify(result(false, linesReplaced, newLines.length, message));
42
+ }
43
+ }
44
+ export const EDIT_FILE_SCHEMA = {
45
+ type: 'function',
46
+ function: {
47
+ name: 'edit_file',
48
+ description: 'Edit specific lines of a file. More efficient than rewriting the whole file.',
49
+ parameters: {
50
+ type: 'object',
51
+ properties: {
52
+ path: { type: 'string' },
53
+ startLine: { type: 'number' },
54
+ endLine: { type: 'number' },
55
+ newContent: { type: 'string' },
56
+ },
57
+ required: ['path', 'startLine', 'endLine', 'newContent'],
58
+ },
59
+ },
60
+ };
61
+ function result(ok, linesReplaced, newLineCount, error, autoLint) {
62
+ const base = error
63
+ ? { ok, linesReplaced, newLineCount, error }
64
+ : { ok, linesReplaced, newLineCount };
65
+ return autoLint ? { ...base, autoLint } : base;
66
+ }
@@ -0,0 +1,205 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import { createPatch } from 'diff';
4
+ import { confirm } from '@inquirer/prompts';
5
+ import { config } from '../config.js';
6
+ import { theme } from '../ui/theme.js';
7
+ import { formatAutoCheckResult, runAutoLint } from './auto-check.js';
8
+ import { toolMemory } from './memory.js';
9
+ import { loadPolicy, writePathAllowed } from './policy.js';
10
+ import { assertSandbox } from './sandbox.js';
11
+ import { isReadOnly } from '../context/read-only.js';
12
+ import { hookManager } from '../hooks/lifecycle.js';
13
+ /** Show a unified diff and ask the user before writing the file. */
14
+ export async function proposeWrite(relPath, newContent) {
15
+ const abs = path.resolve(config.cwd, relPath);
16
+ const denied = ensureWriteAllowed(abs);
17
+ if (denied) {
18
+ process.stdout.write(theme.err(` ${denied}\n`));
19
+ return { wrote: false, path: abs, bytes: 0, error: denied };
20
+ }
21
+ let old = '';
22
+ let exists = false;
23
+ try {
24
+ old = fs.readFileSync(abs, 'utf8');
25
+ exists = true;
26
+ }
27
+ catch {
28
+ /* new file */
29
+ }
30
+ const patch = createPatch(relPath, old, newContent, exists ? 'current' : 'empty', 'proposed');
31
+ if (!config.quiet && !config.jsonOutput) {
32
+ process.stdout.write('\n' + theme.badge('WRITE') + ` ${relPath}\n`);
33
+ process.stdout.write(colorizePatch(patch) + '\n');
34
+ }
35
+ const remembered = toolMemory.isWriteRemembered(abs);
36
+ const ok = config.autoApprove ||
37
+ remembered ||
38
+ (await confirm({
39
+ message: exists ? 'Apply this patch?' : 'Create this new file?',
40
+ default: false,
41
+ }).catch(() => false));
42
+ if (!ok) {
43
+ if (!config.jsonOutput)
44
+ process.stdout.write(theme.warn(' skipped.\n'));
45
+ return { wrote: false, path: abs, bytes: 0 };
46
+ }
47
+ if (!config.autoApprove && !remembered) {
48
+ const remember = await confirm({
49
+ message: 'Remember this write path for the session?',
50
+ default: false,
51
+ }).catch(() => false);
52
+ if (remember)
53
+ toolMemory.rememberWrite(abs);
54
+ }
55
+ fs.mkdirSync(path.dirname(abs), { recursive: true });
56
+ fs.writeFileSync(abs, newContent, 'utf8');
57
+ await hookManager.emit('fileChanged', {
58
+ cwd: config.cwd,
59
+ path: relPath,
60
+ absolutePath: abs,
61
+ bytes: Buffer.byteLength(newContent),
62
+ });
63
+ if (!config.jsonOutput)
64
+ process.stdout.write(theme.ok(` ✔ wrote ${relPath}\n`));
65
+ const autoLint = await maybeRunAutoLint([relPath]);
66
+ return {
67
+ wrote: true,
68
+ path: abs,
69
+ bytes: Buffer.byteLength(newContent),
70
+ ...(autoLint ? { autoLint } : {}),
71
+ };
72
+ }
73
+ export async function proposeWriteBatch(items) {
74
+ const prepared = [];
75
+ for (const item of items) {
76
+ const abs = path.resolve(config.cwd, item.path);
77
+ const denied = ensureWriteAllowed(abs);
78
+ if (denied) {
79
+ process.stdout.write(theme.err(` ${denied}\n`));
80
+ return { wrote: false, results: [{ wrote: false, path: abs, bytes: 0, error: denied }] };
81
+ }
82
+ let old = '';
83
+ let exists = false;
84
+ try {
85
+ old = fs.readFileSync(abs, 'utf8');
86
+ exists = true;
87
+ }
88
+ catch {
89
+ /* new file */
90
+ }
91
+ prepared.push({ relPath: item.path, abs, content: item.content, old, exists });
92
+ }
93
+ if (!config.quiet && !config.jsonOutput) {
94
+ process.stdout.write('\n' + theme.badge('WRITE BATCH') + ` ${prepared.length} files\n`);
95
+ for (const item of prepared) {
96
+ const patch = createPatch(item.relPath, item.old, item.content, item.exists ? 'current' : 'empty', 'proposed');
97
+ process.stdout.write(colorizePatch(patch) + '\n');
98
+ }
99
+ }
100
+ const remembered = prepared.every((item) => toolMemory.isWriteRemembered(item.abs));
101
+ const ok = config.autoApprove ||
102
+ remembered ||
103
+ (await confirm({
104
+ message: 'Apply all patches?',
105
+ default: false,
106
+ }).catch(() => false));
107
+ if (!ok) {
108
+ if (!config.jsonOutput)
109
+ process.stdout.write(theme.warn(' skipped.\n'));
110
+ return {
111
+ wrote: false,
112
+ results: prepared.map((item) => ({ wrote: false, path: item.abs, bytes: 0 })),
113
+ };
114
+ }
115
+ if (!config.autoApprove && !remembered) {
116
+ const remember = await confirm({
117
+ message: 'Remember these write paths for the session?',
118
+ default: false,
119
+ }).catch(() => false);
120
+ if (remember)
121
+ prepared.forEach((item) => toolMemory.rememberWrite(item.abs));
122
+ }
123
+ const results = [];
124
+ const written = [];
125
+ try {
126
+ for (const item of prepared) {
127
+ fs.mkdirSync(path.dirname(item.abs), { recursive: true });
128
+ fs.writeFileSync(item.abs, item.content, 'utf8');
129
+ await hookManager.emit('fileChanged', {
130
+ cwd: config.cwd,
131
+ path: item.relPath,
132
+ absolutePath: item.abs,
133
+ bytes: Buffer.byteLength(item.content),
134
+ });
135
+ written.push(item);
136
+ results.push({ wrote: true, path: item.abs, bytes: Buffer.byteLength(item.content) });
137
+ }
138
+ const autoLint = await maybeRunAutoLint(prepared.map((item) => item.relPath));
139
+ return { wrote: true, results, ...(autoLint ? { autoLint } : {}) };
140
+ }
141
+ catch (e) {
142
+ const error = e?.message || String(e);
143
+ for (const item of written.reverse()) {
144
+ try {
145
+ if (item.exists)
146
+ fs.writeFileSync(item.abs, item.old, 'utf8');
147
+ else if (fs.existsSync(item.abs))
148
+ fs.unlinkSync(item.abs);
149
+ }
150
+ catch {
151
+ /* best-effort rollback */
152
+ }
153
+ }
154
+ return {
155
+ wrote: false,
156
+ results: [
157
+ ...results.map((result) => ({ ...result, wrote: false })),
158
+ { wrote: false, path: '', bytes: 0, error },
159
+ ],
160
+ };
161
+ }
162
+ }
163
+ export function readFileSafe(relPath) {
164
+ const abs = path.resolve(config.cwd, relPath);
165
+ assertSandbox(abs, config.cwd);
166
+ return fs.readFileSync(abs, 'utf8');
167
+ }
168
+ export function ensureWriteAllowed(abs) {
169
+ try {
170
+ assertSandbox(abs, config.cwd);
171
+ }
172
+ catch (e) {
173
+ return e?.message || String(e);
174
+ }
175
+ if (isReadOnly(abs))
176
+ return 'read-only file';
177
+ if (!writePathAllowed(abs, loadPolicy(config.cwd), config.cwd))
178
+ return 'policy denied';
179
+ return undefined;
180
+ }
181
+ function colorizePatch(p) {
182
+ return p
183
+ .split('\n')
184
+ .map((l) => {
185
+ if (l.startsWith('+++') || l.startsWith('---'))
186
+ return theme.dim(l);
187
+ if (l.startsWith('+'))
188
+ return theme.ok(l);
189
+ if (l.startsWith('-'))
190
+ return theme.err(l);
191
+ if (l.startsWith('@@'))
192
+ return theme.hl(l);
193
+ return l;
194
+ })
195
+ .join('\n');
196
+ }
197
+ async function maybeRunAutoLint(changedFiles) {
198
+ if (!config.autoLint)
199
+ return undefined;
200
+ const result = await runAutoLint(changedFiles);
201
+ if (!config.quiet && !config.jsonOutput) {
202
+ process.stdout.write(`${theme.dim(formatAutoCheckResult('lint', result, changedFiles))}\n`);
203
+ }
204
+ return result;
205
+ }
@@ -0,0 +1,17 @@
1
+ import path from 'node:path';
2
+ import fg from 'fast-glob';
3
+ import { config } from '../config.js';
4
+ import { assertSandbox } from './sandbox.js';
5
+ export async function globTool(args) {
6
+ const root = path.resolve(config.cwd, args.cwd || '.');
7
+ assertSandbox(root, config.cwd);
8
+ const files = await fg(args.pattern, {
9
+ cwd: root,
10
+ onlyFiles: true,
11
+ dot: true,
12
+ ignore: args.ignore || ['**/node_modules/**', '**/dist/**', '**/.git/**'],
13
+ });
14
+ return JSON.stringify({
15
+ files: files.map((file) => path.relative(config.cwd, path.join(root, file))),
16
+ });
17
+ }