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,463 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import { spawn } from 'node:child_process';
4
+ import simpleGit from 'simple-git';
5
+ import { activeProvider, client } from '../api/github-models.js';
6
+ import { detectLinters } from '../commands/lint-cmd.js';
7
+ import { scanFilesForSecrets } from '../commands/security-cmd.js';
8
+ import { detectTestFrameworks } from '../commands/test-cmd.js';
9
+ import { config } from '../config.js';
10
+ import { theme } from '../ui/theme.js';
11
+ const HOOK_MARKER = '# managed by icopilot pre-commit hook';
12
+ const PRECOMMIT_USAGE = [
13
+ theme.brand('Hook command'),
14
+ ` ${theme.hl('/hook install')} ${theme.dim('install the git pre-commit hook')}`,
15
+ ` ${theme.hl('/hook uninstall')} ${theme.dim('remove the git pre-commit hook')}`,
16
+ ` ${theme.hl('/hook run')} ${theme.dim('run configured checks now')}`,
17
+ ` ${theme.hl('/hook config')} ${theme.dim('show current hook config')}`,
18
+ ` ${theme.hl('/hook config enable|disable')} ${theme.dim('toggle the hook')}`,
19
+ ` ${theme.hl('/hook config fail-on <mode>')} ${theme.dim('set error, warning, or never')}`,
20
+ ` ${theme.hl('/hook config checks <list>')} ${theme.dim('replace checks (comma-separated)')}`,
21
+ ` ${theme.hl('/hook config add <check>')} ${theme.dim('add review, security, lint, or test')}`,
22
+ ` ${theme.hl('/hook config remove <check>')} ${theme.dim('remove a configured check')}`,
23
+ ` ${theme.hl('/hook config reset')} ${theme.dim('restore defaults')}`,
24
+ ].join('\n');
25
+ const PRECOMMIT_CONFIG_FILE = path.join('.icopilot', 'precommit.json');
26
+ const REVIEW_PROMPT = `You are doing a fast pre-commit code review.
27
+ Return "LGTM" if the staged diff is clean.
28
+ Otherwise return a compact bullet list of concrete issues only. Focus on bugs, regressions, security problems, and missing tests.`;
29
+ const VALID_CHECKS = ['review', 'security', 'lint', 'test'];
30
+ const DEFAULT_PRECOMMIT_CONFIG = {
31
+ enabled: true,
32
+ checks: ['review', 'security'],
33
+ failOn: 'error',
34
+ };
35
+ const MAX_FINDINGS = 20;
36
+ export function installHook(gitDir) {
37
+ const hooksDir = path.join(gitDir, 'hooks');
38
+ const hookPath = path.join(hooksDir, 'pre-commit');
39
+ fs.mkdirSync(hooksDir, { recursive: true });
40
+ fs.writeFileSync(hookPath, buildHookScript(), 'utf8');
41
+ try {
42
+ fs.chmodSync(hookPath, 0o755);
43
+ }
44
+ catch {
45
+ /* best effort on Windows */
46
+ }
47
+ }
48
+ export function uninstallHook(gitDir) {
49
+ fs.rmSync(path.join(gitDir, 'hooks', 'pre-commit'), { force: true });
50
+ }
51
+ export async function runPrecommitChecks(precommitConfig) {
52
+ return toPublicResult(await runPrecommitChecksInternal(normalizePrecommitConfig(precommitConfig), process.cwd()));
53
+ }
54
+ export function loadPrecommitConfig(cwd = config.cwd) {
55
+ const filePath = getPrecommitConfigPath(cwd);
56
+ try {
57
+ if (!fs.existsSync(filePath))
58
+ return { ...DEFAULT_PRECOMMIT_CONFIG, checks: [...DEFAULT_PRECOMMIT_CONFIG.checks] };
59
+ const parsed = JSON.parse(fs.readFileSync(filePath, 'utf8'));
60
+ return normalizePrecommitConfig(parsed);
61
+ }
62
+ catch {
63
+ return { ...DEFAULT_PRECOMMIT_CONFIG, checks: [...DEFAULT_PRECOMMIT_CONFIG.checks] };
64
+ }
65
+ }
66
+ export function savePrecommitConfig(next, cwd = config.cwd) {
67
+ const normalized = normalizePrecommitConfig(next);
68
+ const filePath = getPrecommitConfigPath(cwd);
69
+ fs.mkdirSync(path.dirname(filePath), { recursive: true });
70
+ fs.writeFileSync(filePath, `${JSON.stringify(normalized, null, 2)}\n`, 'utf8');
71
+ return normalized;
72
+ }
73
+ export async function hookCommand(args, cwd = config.cwd) {
74
+ const [subcommand, ...rest] = args;
75
+ const normalizedSubcommand = subcommand?.toLowerCase();
76
+ if (!normalizedSubcommand) {
77
+ return { output: `${PRECOMMIT_USAGE}\n`, exitCode: 0 };
78
+ }
79
+ switch (normalizedSubcommand) {
80
+ case 'install': {
81
+ const gitDir = await resolveGitDir(cwd);
82
+ installHook(gitDir);
83
+ const current = savePrecommitConfig(loadPrecommitConfig(cwd), cwd);
84
+ return {
85
+ output: `${theme.ok('✔ pre-commit hook installed')}\n` +
86
+ `${theme.dim(path.join(gitDir, 'hooks', 'pre-commit'))}\n` +
87
+ `${formatPrecommitConfig(current, cwd)}`,
88
+ exitCode: 0,
89
+ };
90
+ }
91
+ case 'uninstall': {
92
+ const gitDir = await resolveGitDir(cwd);
93
+ uninstallHook(gitDir);
94
+ return {
95
+ output: `${theme.ok('✔ pre-commit hook removed')}\n${theme.dim(path.join(gitDir, 'hooks', 'pre-commit'))}\n`,
96
+ exitCode: 0,
97
+ };
98
+ }
99
+ case 'run': {
100
+ const current = loadPrecommitConfig(cwd);
101
+ const result = await runPrecommitChecksInternal({ ...current, enabled: true }, cwd);
102
+ return {
103
+ output: formatPrecommitResult(result, current, cwd),
104
+ exitCode: result.passed ? 0 : 1,
105
+ };
106
+ }
107
+ case 'pre-commit': {
108
+ const current = loadPrecommitConfig(cwd);
109
+ if (!current.enabled) {
110
+ return {
111
+ output: `${theme.dim(`pre-commit hook disabled (${getPrecommitConfigPath(cwd)})`)}\n`,
112
+ exitCode: 0,
113
+ };
114
+ }
115
+ const result = await runPrecommitChecksInternal(current, cwd);
116
+ return {
117
+ output: formatPrecommitResult(result, current, cwd),
118
+ exitCode: result.passed ? 0 : 1,
119
+ };
120
+ }
121
+ case 'config':
122
+ return handleConfigCommand(rest, cwd);
123
+ default:
124
+ return {
125
+ output: `${theme.warn(`unknown hook subcommand: ${subcommand}`)}\n${PRECOMMIT_USAGE}\n`,
126
+ exitCode: 1,
127
+ };
128
+ }
129
+ }
130
+ function buildHookScript() {
131
+ return `#!/bin/sh
132
+ ${HOOK_MARKER}
133
+ if command -v icopilot >/dev/null 2>&1; then
134
+ exec icopilot hook pre-commit "$@"
135
+ fi
136
+
137
+ if command -v icli >/dev/null 2>&1; then
138
+ exec icli hook pre-commit "$@"
139
+ fi
140
+
141
+ echo "icopilot is not installed on PATH." >&2
142
+ exit 1
143
+ `;
144
+ }
145
+ function getPrecommitConfigPath(cwd) {
146
+ return path.join(cwd, PRECOMMIT_CONFIG_FILE);
147
+ }
148
+ function normalizePrecommitConfig(input) {
149
+ const candidate = input && typeof input === 'object' ? input : {};
150
+ const enabled = typeof candidate.enabled === 'boolean' ? candidate.enabled : DEFAULT_PRECOMMIT_CONFIG.enabled;
151
+ const failOn = candidate.failOn === 'error' || candidate.failOn === 'warning' || candidate.failOn === 'never'
152
+ ? candidate.failOn
153
+ : DEFAULT_PRECOMMIT_CONFIG.failOn;
154
+ const checks = Array.isArray(candidate.checks)
155
+ ? dedupeChecks(candidate.checks.filter(isCheckName))
156
+ : [...DEFAULT_PRECOMMIT_CONFIG.checks];
157
+ return {
158
+ enabled,
159
+ checks: checks.length ? checks : [...DEFAULT_PRECOMMIT_CONFIG.checks],
160
+ failOn,
161
+ };
162
+ }
163
+ function dedupeChecks(checks) {
164
+ return [...new Set(checks)];
165
+ }
166
+ function isCheckName(value) {
167
+ return typeof value === 'string' && VALID_CHECKS.includes(value);
168
+ }
169
+ async function handleConfigCommand(args, cwd) {
170
+ const current = loadPrecommitConfig(cwd);
171
+ const [action, ...rest] = args;
172
+ if (!action || action === 'show') {
173
+ return { output: formatPrecommitConfig(current, cwd), exitCode: 0 };
174
+ }
175
+ switch (action.toLowerCase()) {
176
+ case 'enable':
177
+ return {
178
+ output: `${theme.ok('✔ hook enabled')}\n${formatPrecommitConfig(savePrecommitConfig({ ...current, enabled: true }, cwd), cwd)}`,
179
+ exitCode: 0,
180
+ };
181
+ case 'disable':
182
+ return {
183
+ output: `${theme.ok('✔ hook disabled')}\n${formatPrecommitConfig(savePrecommitConfig({ ...current, enabled: false }, cwd), cwd)}`,
184
+ exitCode: 0,
185
+ };
186
+ case 'fail-on': {
187
+ const value = rest[0];
188
+ if (value !== 'error' && value !== 'warning' && value !== 'never') {
189
+ return {
190
+ output: `${theme.warn('usage: /hook config fail-on <error|warning|never>')}\n`,
191
+ exitCode: 1,
192
+ };
193
+ }
194
+ return {
195
+ output: `${theme.ok(`✔ failOn → ${value}`)}\n${formatPrecommitConfig(savePrecommitConfig({ ...current, failOn: value }, cwd), cwd)}`,
196
+ exitCode: 0,
197
+ };
198
+ }
199
+ case 'checks': {
200
+ const list = parseCheckList(rest.join(' '));
201
+ if (!list.length) {
202
+ return {
203
+ output: `${theme.warn('usage: /hook config checks <review,security,lint,test>')}\n`,
204
+ exitCode: 1,
205
+ };
206
+ }
207
+ return {
208
+ output: `${theme.ok(`✔ checks → ${list.join(', ')}`)}\n${formatPrecommitConfig(savePrecommitConfig({ ...current, checks: list }, cwd), cwd)}`,
209
+ exitCode: 0,
210
+ };
211
+ }
212
+ case 'add': {
213
+ const check = rest[0];
214
+ if (!isCheckName(check)) {
215
+ return {
216
+ output: `${theme.warn('usage: /hook config add <review|security|lint|test>')}\n`,
217
+ exitCode: 1,
218
+ };
219
+ }
220
+ return {
221
+ output: `${theme.ok(`✔ added ${check}`)}\n${formatPrecommitConfig(savePrecommitConfig({ ...current, checks: dedupeChecks([...current.checks, check]) }, cwd), cwd)}`,
222
+ exitCode: 0,
223
+ };
224
+ }
225
+ case 'remove': {
226
+ const check = rest[0];
227
+ if (!isCheckName(check)) {
228
+ return {
229
+ output: `${theme.warn('usage: /hook config remove <review|security|lint|test>')}\n`,
230
+ exitCode: 1,
231
+ };
232
+ }
233
+ return {
234
+ output: `${theme.ok(`✔ removed ${check}`)}\n${formatPrecommitConfig(savePrecommitConfig({ ...current, checks: current.checks.filter((entry) => entry !== check) }, cwd), cwd)}`,
235
+ exitCode: 0,
236
+ };
237
+ }
238
+ case 'reset':
239
+ return {
240
+ output: `${theme.ok('✔ hook config reset')}\n${formatPrecommitConfig(savePrecommitConfig(DEFAULT_PRECOMMIT_CONFIG, cwd), cwd)}`,
241
+ exitCode: 0,
242
+ };
243
+ default:
244
+ return {
245
+ output: `${theme.warn(`unknown config action: ${action}`)}\n${PRECOMMIT_USAGE}\n`,
246
+ exitCode: 1,
247
+ };
248
+ }
249
+ }
250
+ function parseCheckList(value) {
251
+ const items = value
252
+ .split(',')
253
+ .map((entry) => entry.trim().toLowerCase())
254
+ .filter(Boolean);
255
+ return dedupeChecks(items.filter(isCheckName));
256
+ }
257
+ async function runPrecommitChecksInternal(precommitConfig, cwd) {
258
+ if (!precommitConfig.enabled) {
259
+ return { passed: true, checks: [] };
260
+ }
261
+ const staged = await getStagedContext(cwd);
262
+ const checks = [];
263
+ for (const name of precommitConfig.checks) {
264
+ checks.push(await runCheck(name, staged));
265
+ }
266
+ return {
267
+ passed: evaluateOverallPass(checks, precommitConfig.failOn),
268
+ checks,
269
+ };
270
+ }
271
+ async function getStagedContext(cwd) {
272
+ const git = simpleGit({ baseDir: cwd });
273
+ const filesRaw = await git.diff(['--cached', '--name-only', '--diff-filter=ACMR']);
274
+ const files = filesRaw
275
+ .split(/\r?\n/)
276
+ .map((entry) => entry.trim())
277
+ .filter(Boolean);
278
+ const diff = files.length ? await git.diff(['--cached', '--unified=0', '--no-color']) : '';
279
+ return { cwd, files, diff };
280
+ }
281
+ async function runCheck(name, staged) {
282
+ switch (name) {
283
+ case 'review':
284
+ return runReviewCheck(staged);
285
+ case 'security':
286
+ return runSecurityCheck(staged);
287
+ case 'lint':
288
+ return runLintCheck(staged);
289
+ case 'test':
290
+ return runTestCheck(staged);
291
+ }
292
+ }
293
+ async function runReviewCheck(staged) {
294
+ const startedAt = Date.now();
295
+ if (!staged.diff.trim()) {
296
+ return finalizeCheck('review', true, [], startedAt, 'none');
297
+ }
298
+ try {
299
+ const provider = activeProvider();
300
+ const messages = [
301
+ { role: 'system', content: REVIEW_PROMPT },
302
+ {
303
+ role: 'user',
304
+ content: `Review this staged diff:\n\n${staged.diff.slice(0, 80_000)}`,
305
+ },
306
+ ];
307
+ const response = await client().chat.completions.create({
308
+ model: config.defaultModel,
309
+ messages,
310
+ temperature: 0.1,
311
+ ...(provider?.maxTokens ? { max_tokens: provider.maxTokens } : {}),
312
+ });
313
+ const content = response.choices[0]?.message?.content?.trim() ?? '';
314
+ if (!content || isCleanReview(content)) {
315
+ return finalizeCheck('review', true, [], startedAt, 'none');
316
+ }
317
+ return finalizeCheck('review', false, summarizeText(content), startedAt, 'warning');
318
+ }
319
+ catch (error) {
320
+ return finalizeCheck('review', false, [`review failed: ${String(error?.message || error)}`], startedAt, 'error');
321
+ }
322
+ }
323
+ function runSecurityCheck(staged) {
324
+ const startedAt = Date.now();
325
+ const findings = scanFilesForSecrets(staged.cwd, staged.files).map((finding) => `${finding.file}:${finding.line} ${finding.pattern} (${finding.severity})`);
326
+ return Promise.resolve(finalizeCheck('security', findings.length === 0, findings, startedAt, findings.length ? 'error' : 'none'));
327
+ }
328
+ async function runLintCheck(staged) {
329
+ const startedAt = Date.now();
330
+ const linters = detectLinters(staged.cwd);
331
+ if (!linters.length) {
332
+ return finalizeCheck('lint', false, ['No configured linter detected.'], startedAt, 'error');
333
+ }
334
+ const selection = buildLintCommand(linters, staged.files);
335
+ const result = await executeCommand(selection.command, staged.cwd);
336
+ const findings = result.code === 0 ? [] : summarizeText(result.output || 'Lint command failed.');
337
+ return finalizeCheck('lint', result.code === 0, findings, startedAt, result.code === 0 ? 'none' : 'error');
338
+ }
339
+ async function runTestCheck(staged) {
340
+ const startedAt = Date.now();
341
+ const frameworks = detectTestFrameworks(staged.cwd);
342
+ if (!frameworks.length) {
343
+ return finalizeCheck('test', false, ['No configured test runner detected.'], startedAt, 'error');
344
+ }
345
+ const selection = buildTestCommand(frameworks, staged.files);
346
+ const result = await executeCommand(selection.command, staged.cwd);
347
+ const findings = result.code === 0 ? [] : summarizeText(result.output || 'Test command failed.');
348
+ return finalizeCheck('test', result.code === 0, findings, startedAt, result.code === 0 ? 'none' : 'error');
349
+ }
350
+ function buildLintCommand(linters, stagedFiles) {
351
+ const preferred = linters.find((entry) => entry.name === 'npm-lint') ?? linters[0];
352
+ const lintableFiles = stagedFiles.filter((file) => /\.(?:[cm]?[jt]sx?|py|go|java|cs|php|rb)$/i.test(file));
353
+ if ((preferred.name === 'eslint' || preferred.name === 'eslint-config') && lintableFiles.length) {
354
+ return { command: ['npx', 'eslint', ...lintableFiles].map(quoteArg).join(' ') };
355
+ }
356
+ return { command: preferred.command };
357
+ }
358
+ function buildTestCommand(frameworks, stagedFiles) {
359
+ const testFiles = stagedFiles.filter((file) => /\.(?:test|spec)\.[cm]?[jt]sx?$/i.test(file));
360
+ const preferred = frameworks.find((entry) => entry.name === 'vitest') ??
361
+ frameworks.find((entry) => entry.name === 'jest') ??
362
+ frameworks[0];
363
+ if (preferred.name === 'vitest' && testFiles.length) {
364
+ return { command: ['npx', 'vitest', 'run', ...testFiles].map(quoteArg).join(' ') };
365
+ }
366
+ if (preferred.name === 'jest' && testFiles.length) {
367
+ return { command: ['npx', 'jest', ...testFiles].map(quoteArg).join(' ') };
368
+ }
369
+ return { command: preferred.command };
370
+ }
371
+ function executeCommand(command, cwd) {
372
+ return new Promise((resolve, reject) => {
373
+ const child = spawn(command, [], {
374
+ cwd,
375
+ shell: true,
376
+ windowsHide: true,
377
+ stdio: ['ignore', 'pipe', 'pipe'],
378
+ });
379
+ let output = '';
380
+ child.stdout.on('data', (chunk) => {
381
+ output += String(chunk);
382
+ });
383
+ child.stderr.on('data', (chunk) => {
384
+ output += String(chunk);
385
+ });
386
+ child.on('error', reject);
387
+ child.on('exit', (code) => {
388
+ resolve({ code: code ?? 1, output });
389
+ });
390
+ });
391
+ }
392
+ function summarizeText(text) {
393
+ return text
394
+ .split(/\r?\n/)
395
+ .map((line) => line.replace(/^\s*[-*]\s*/, '').trim())
396
+ .filter(Boolean)
397
+ .slice(0, MAX_FINDINGS);
398
+ }
399
+ function isCleanReview(content) {
400
+ return /^(?:lgtm|no issues found|looks good\b)/i.test(content.trim());
401
+ }
402
+ function evaluateOverallPass(checks, failOn) {
403
+ if (failOn === 'never')
404
+ return true;
405
+ if (failOn === 'warning')
406
+ return checks.every((check) => check.failureLevel === 'none');
407
+ return checks.every((check) => check.failureLevel !== 'error');
408
+ }
409
+ function finalizeCheck(name, passed, findings, startedAt, failureLevel) {
410
+ return {
411
+ name,
412
+ passed,
413
+ findings,
414
+ duration: Date.now() - startedAt,
415
+ failureLevel,
416
+ };
417
+ }
418
+ function toPublicResult(result) {
419
+ return {
420
+ passed: result.passed,
421
+ checks: result.checks.map(({ failureLevel: _failureLevel, ...check }) => check),
422
+ };
423
+ }
424
+ function formatPrecommitConfig(precommitConfig, cwd) {
425
+ const configPath = getPrecommitConfigPath(cwd);
426
+ return [
427
+ theme.brand('Pre-commit config'),
428
+ ` file: ${theme.hl(configPath)}`,
429
+ ` enabled: ${theme.hl(String(precommitConfig.enabled))}`,
430
+ ` failOn: ${theme.hl(precommitConfig.failOn)}`,
431
+ ` checks: ${theme.hl(precommitConfig.checks.join(', '))}`,
432
+ '',
433
+ ].join('\n');
434
+ }
435
+ function formatPrecommitResult(result, precommitConfig, cwd) {
436
+ const header = result.passed
437
+ ? theme.ok('✔ pre-commit checks passed')
438
+ : theme.err('✖ pre-commit checks failed');
439
+ const lines = [
440
+ header,
441
+ theme.dim(`config: ${getPrecommitConfigPath(cwd)} failOn=${precommitConfig.failOn}`),
442
+ '',
443
+ ];
444
+ if (result.checks.length === 0) {
445
+ lines.push(theme.dim('No checks ran.'));
446
+ }
447
+ for (const check of result.checks) {
448
+ lines.push(` ${check.passed ? theme.ok('✓') : theme.err('✗')} ${check.name} ${theme.dim(`(${check.duration}ms)`)}`);
449
+ for (const finding of check.findings) {
450
+ lines.push(` - ${finding}`);
451
+ }
452
+ }
453
+ lines.push('');
454
+ return `${lines.join('\n')}\n`;
455
+ }
456
+ async function resolveGitDir(cwd) {
457
+ const git = simpleGit({ baseDir: cwd });
458
+ const gitDir = (await git.raw(['rev-parse', '--git-dir'])).trim();
459
+ return path.resolve(cwd, gitDir);
460
+ }
461
+ function quoteArg(value) {
462
+ return `"${value.replace(/(["\\$`])/g, '\\$1')}"`;
463
+ }
@@ -0,0 +1,23 @@
1
+ import { client } from '../api/github-models.js';
2
+ const EMBEDDING_MODEL = 'text-embedding-3-small';
3
+ const BATCH_SIZE = 64;
4
+ export async function embed(texts) {
5
+ if (!texts.length)
6
+ return [];
7
+ const vectors = [];
8
+ for (let i = 0; i < texts.length; i += BATCH_SIZE) {
9
+ const input = texts.slice(i, i + BATCH_SIZE);
10
+ try {
11
+ const response = await client().embeddings.create({
12
+ model: EMBEDDING_MODEL,
13
+ input,
14
+ });
15
+ vectors.push(...response.data.map((item) => item.embedding));
16
+ }
17
+ catch (err) {
18
+ const message = err instanceof Error ? err.message : String(err);
19
+ throw new Error(`Embedding request failed: ${message}`);
20
+ }
21
+ }
22
+ return vectors;
23
+ }
@@ -0,0 +1,86 @@
1
+ import crypto from 'node:crypto';
2
+ import fs from 'node:fs/promises';
3
+ import path from 'node:path';
4
+ import fg from 'fast-glob';
5
+ import { embed } from './embeddings.js';
6
+ import { VectorStore } from './store.js';
7
+ const DEFAULT_INCLUDE = ['**/*.{ts,tsx,js,jsx,md,py,go,rs,java,rb,cs,json,yaml,yml,toml}'];
8
+ const DEFAULT_IGNORE = ['node_modules/**', 'dist/**', '.git/**', 'coverage/**'];
9
+ const MAX_FILE_BYTES = 256 * 1024;
10
+ const OVERLAP_CHARS = 200;
11
+ export async function buildIndex(cwd, opts = {}) {
12
+ const started = Date.now();
13
+ const outPath = path.join(cwd, '.icopilot', 'index.json');
14
+ const store = new VectorStore(outPath);
15
+ const existing = store.load().entries;
16
+ const existingByFile = new Map();
17
+ for (const entry of existing) {
18
+ const bucket = existingByFile.get(entry.file) ?? [];
19
+ bucket.push(entry);
20
+ existingByFile.set(entry.file, bucket);
21
+ }
22
+ const files = await fg(opts.include ?? DEFAULT_INCLUDE, {
23
+ cwd,
24
+ ignore: opts.ignore ?? DEFAULT_IGNORE,
25
+ onlyFiles: true,
26
+ dot: false,
27
+ });
28
+ const chunkChars = opts.chunkChars ?? 1500;
29
+ const retained = [];
30
+ const pending = [];
31
+ for (const file of files.sort()) {
32
+ const absolute = path.join(cwd, file);
33
+ const stat = await fs.stat(absolute);
34
+ if (stat.size > MAX_FILE_BYTES)
35
+ continue;
36
+ const content = await fs.readFile(absolute, 'utf8');
37
+ const sha = sha1(content);
38
+ const oldEntries = existingByFile.get(file);
39
+ if (oldEntries?.length && oldEntries.every((entry) => entry.sha === sha)) {
40
+ retained.push(...oldEntries);
41
+ continue;
42
+ }
43
+ const chunks = chunkText(content, chunkChars);
44
+ if (chunks.length)
45
+ pending.push({ file, sha, chunks });
46
+ }
47
+ const texts = pending.flatMap((item) => item.chunks);
48
+ const vectors = await embed(texts);
49
+ const fresh = [];
50
+ let vectorIndex = 0;
51
+ for (const item of pending) {
52
+ item.chunks.forEach((text, chunk) => {
53
+ fresh.push({
54
+ id: `${item.file}:${item.sha}:${chunk}`,
55
+ file: item.file,
56
+ chunk,
57
+ text,
58
+ vector: vectors[vectorIndex++],
59
+ sha: item.sha,
60
+ });
61
+ });
62
+ }
63
+ store.replaceAll([...retained, ...fresh]);
64
+ store.save();
65
+ return {
66
+ files: files.length,
67
+ chunks: fresh.length,
68
+ ms: Date.now() - started,
69
+ outPath,
70
+ };
71
+ }
72
+ function chunkText(text, chunkChars) {
73
+ const chunks = [];
74
+ const step = Math.max(1, chunkChars - OVERLAP_CHARS);
75
+ for (let start = 0; start < text.length; start += step) {
76
+ const chunk = text.slice(start, start + chunkChars).trim();
77
+ if (chunk)
78
+ chunks.push(chunk);
79
+ if (start + chunkChars >= text.length)
80
+ break;
81
+ }
82
+ return chunks;
83
+ }
84
+ function sha1(text) {
85
+ return crypto.createHash('sha1').update(text).digest('hex');
86
+ }
@@ -0,0 +1,20 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import { embed } from './embeddings.js';
4
+ import { VectorStore } from './store.js';
5
+ export async function retrieve(cwd, query, topK = 6) {
6
+ const indexPath = path.join(cwd, '.icopilot', 'index.json');
7
+ if (!fs.existsSync(indexPath))
8
+ return [];
9
+ const store = new VectorStore(indexPath);
10
+ store.load();
11
+ const [queryVec] = await embed([query]);
12
+ if (!queryVec)
13
+ return [];
14
+ return store.search(queryVec, topK).map(({ entry, score }) => ({
15
+ file: entry.file,
16
+ chunk: entry.chunk,
17
+ text: entry.text,
18
+ score,
19
+ }));
20
+ }
@@ -0,0 +1,95 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import { embed } from './embeddings.js';
4
+ export class VectorStore {
5
+ file;
6
+ data = { model: 'text-embedding-3-small', createdAt: '', entries: [] };
7
+ norms = new Map();
8
+ constructor(file) {
9
+ this.file = file;
10
+ }
11
+ get index() {
12
+ return this.data;
13
+ }
14
+ get entries() {
15
+ return this.data.entries;
16
+ }
17
+ load() {
18
+ if (!fs.existsSync(this.file)) {
19
+ this.data = { model: 'text-embedding-3-small', createdAt: '', entries: [] };
20
+ this.rebuildNorms();
21
+ return this.data;
22
+ }
23
+ const raw = fs.readFileSync(this.file, 'utf8');
24
+ const parsed = JSON.parse(raw);
25
+ this.data = {
26
+ model: parsed.model || 'text-embedding-3-small',
27
+ createdAt: parsed.createdAt || '',
28
+ entries: Array.isArray(parsed.entries) ? parsed.entries : [],
29
+ };
30
+ this.rebuildNorms();
31
+ return this.data;
32
+ }
33
+ save() {
34
+ fs.mkdirSync(path.dirname(this.file), { recursive: true });
35
+ this.data.createdAt = new Date().toISOString();
36
+ fs.writeFileSync(this.file, JSON.stringify(this.data, null, 2), 'utf8');
37
+ }
38
+ addAll(entries) {
39
+ const byId = new Map(this.data.entries.map((entry) => [entry.id, entry]));
40
+ for (const entry of entries) {
41
+ byId.set(entry.id, entry);
42
+ this.norms.set(entry.id, norm(entry.vector));
43
+ }
44
+ this.data.entries = [...byId.values()];
45
+ this.rebuildNorms();
46
+ }
47
+ replaceAll(entries) {
48
+ this.data.entries = [...entries];
49
+ this.rebuildNorms();
50
+ }
51
+ search(queryVec, topK = 8) {
52
+ const queryNorm = norm(queryVec);
53
+ return this.data.entries
54
+ .map((entry) => ({
55
+ entry,
56
+ score: cosine(queryVec, queryNorm, entry.vector, this.norms.get(entry.id) ?? norm(entry.vector)),
57
+ }))
58
+ .sort((a, b) => b.score - a.score)
59
+ .slice(0, topK);
60
+ }
61
+ rebuildNorms() {
62
+ this.norms = new Map(this.data.entries.map((entry) => [entry.id, norm(entry.vector)]));
63
+ }
64
+ }
65
+ function norm(vector) {
66
+ return Math.sqrt(vector.reduce((sum, value) => sum + value * value, 0));
67
+ }
68
+ function cosine(a, aNorm, b, bNorm) {
69
+ if (!aNorm || !bNorm)
70
+ return 0;
71
+ const len = Math.min(a.length, b.length);
72
+ let dot = 0;
73
+ for (let i = 0; i < len; i++)
74
+ dot += a[i] * b[i];
75
+ return dot / (aNorm * bNorm);
76
+ }
77
+ export async function searchIndex(cwd, query, topK = 6) {
78
+ const indexPath = path.join(cwd, '.icopilot', 'index.json');
79
+ if (!fs.existsSync(indexPath)) {
80
+ const error = new Error('No index found');
81
+ error.code = 'ENOENT';
82
+ throw error;
83
+ }
84
+ const store = new VectorStore(indexPath);
85
+ store.load();
86
+ const [queryVec] = await embed([query]);
87
+ if (!queryVec)
88
+ return [];
89
+ return store.search(queryVec, topK).map(({ entry, score }) => ({
90
+ file: entry.file,
91
+ chunk: entry.chunk,
92
+ text: entry.text,
93
+ score,
94
+ }));
95
+ }