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,475 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import fg from 'fast-glob';
4
+ import { parse, stringify } from 'yaml';
5
+ const CONVENTION_PATTERNS = [
6
+ 'src/**/*.{ts,tsx,js,jsx,mjs,cjs}',
7
+ 'tests/**/*.{ts,tsx,js,jsx,mjs,cjs}',
8
+ '*.ts',
9
+ '*.tsx',
10
+ '*.js',
11
+ '*.jsx',
12
+ '*.mjs',
13
+ '*.cjs',
14
+ ];
15
+ const CONVENTION_IGNORE = [
16
+ '**/node_modules/**',
17
+ '**/dist/**',
18
+ '**/.git/**',
19
+ '**/.icopilot/**',
20
+ '**/coverage/**',
21
+ ];
22
+ const BUILTIN_MODULES = new Set([
23
+ 'assert',
24
+ 'buffer',
25
+ 'child_process',
26
+ 'crypto',
27
+ 'events',
28
+ 'fs',
29
+ 'http',
30
+ 'https',
31
+ 'net',
32
+ 'os',
33
+ 'path',
34
+ 'process',
35
+ 'stream',
36
+ 'timers',
37
+ 'tty',
38
+ 'url',
39
+ 'util',
40
+ 'zlib',
41
+ ]);
42
+ export class ConventionManager {
43
+ set;
44
+ constructor(initialSet = { name: 'Project conventions', conventions: [] }) {
45
+ this.set = normalizeConventionSet(initialSet, initialSet.name || 'Project conventions');
46
+ }
47
+ load(rootDir) {
48
+ const filePath = resolveConventionPath(rootDir);
49
+ if (!fs.existsSync(filePath)) {
50
+ this.set = emptyConventionSet(rootDir);
51
+ return this.getConventionSet();
52
+ }
53
+ const parsed = parse(fs.readFileSync(filePath, 'utf8'));
54
+ this.set = normalizeConventionSet(parsed, emptyConventionSet(rootDir).name);
55
+ return this.getConventionSet();
56
+ }
57
+ detect(rootDir) {
58
+ const stats = collectDetectionStats(listConventionFiles(rootDir));
59
+ const detected = [];
60
+ if (stats.semicolonsAlways > 0 && stats.semicolonsAlways >= stats.semicolonsNever) {
61
+ detected.push({
62
+ id: 'use-semicolons',
63
+ name: 'Use semicolons',
64
+ description: 'Terminate statements with semicolons.',
65
+ rule: 'End statements with semicolons.',
66
+ example: 'const answer = 42;',
67
+ severity: 'required',
68
+ });
69
+ }
70
+ if (stats.singleQuotes > 0 || stats.doubleQuotes > 0) {
71
+ const prefersSingle = stats.singleQuotes >= stats.doubleQuotes;
72
+ detected.push({
73
+ id: prefersSingle ? 'prefer-single-quotes' : 'prefer-double-quotes',
74
+ name: prefersSingle ? 'Prefer single quotes' : 'Prefer double quotes',
75
+ description: prefersSingle
76
+ ? 'Use single-quoted strings unless escaping would be noisier.'
77
+ : 'Use double-quoted strings unless escaping would be noisier.',
78
+ rule: prefersSingle ? 'Use single quotes for strings.' : 'Use double quotes for strings.',
79
+ example: prefersSingle ? "const label = 'ready';" : 'const label = "ready";',
80
+ severity: 'recommended',
81
+ });
82
+ }
83
+ if (stats.esmImports > 0) {
84
+ detected.push({
85
+ id: 'use-esm-imports',
86
+ name: 'Use ESM imports',
87
+ description: 'Prefer native ES module imports and exports.',
88
+ rule: 'Use import/export syntax instead of require/module.exports.',
89
+ example: "import fs from 'node:fs';",
90
+ severity: 'required',
91
+ });
92
+ }
93
+ if (stats.nodeProtocolImports > 0 &&
94
+ stats.nodeProtocolImports >= Math.max(1, Math.floor(stats.builtinImports / 2))) {
95
+ detected.push({
96
+ id: 'prefer-node-protocol-imports',
97
+ name: 'Prefer node: protocol imports',
98
+ description: 'Use the node: protocol for Node.js built-in modules.',
99
+ rule: 'Import built-in modules using the node: protocol.',
100
+ example: "import path from 'node:path';",
101
+ severity: 'recommended',
102
+ });
103
+ }
104
+ if (stats.typeImports > 0) {
105
+ detected.push({
106
+ id: 'prefer-type-imports',
107
+ name: 'Prefer type-only imports',
108
+ description: 'Use import type for TypeScript-only type imports when possible.',
109
+ rule: 'Prefer import type for type-only dependencies.',
110
+ example: "import type { Session } from './session.js';",
111
+ severity: 'recommended',
112
+ });
113
+ }
114
+ if (stats.vitestImports > 0) {
115
+ detected.push({
116
+ id: 'use-vitest-for-tests',
117
+ name: 'Use Vitest for tests',
118
+ description: 'Project tests are written with Vitest.',
119
+ rule: 'Write unit tests with Vitest APIs and imports.',
120
+ example: "import { describe, expect, it } from 'vitest';",
121
+ severity: 'recommended',
122
+ });
123
+ }
124
+ return detected.sort(compareConventions);
125
+ }
126
+ add(convention) {
127
+ const normalized = normalizeConvention(convention);
128
+ const next = this.set.conventions.filter((entry) => entry.id !== normalized.id);
129
+ next.push(normalized);
130
+ this.set = {
131
+ name: this.set.name,
132
+ conventions: next.sort(compareConventions),
133
+ };
134
+ }
135
+ remove(id) {
136
+ const normalizedId = slugify(id);
137
+ this.set = {
138
+ name: this.set.name,
139
+ conventions: this.set.conventions.filter((entry) => entry.id !== normalizedId),
140
+ };
141
+ }
142
+ check(code) {
143
+ const violations = [];
144
+ for (const convention of this.set.conventions) {
145
+ violations.push(...checkConvention(code, convention));
146
+ }
147
+ return violations.sort((left, right) => (left.line ?? Number.MAX_SAFE_INTEGER) - (right.line ?? Number.MAX_SAFE_INTEGER));
148
+ }
149
+ toPromptContext() {
150
+ if (this.set.conventions.length === 0)
151
+ return '';
152
+ return [
153
+ `Follow the ${this.set.name} when generating or editing code:`,
154
+ ...this.set.conventions.map((convention) => {
155
+ const example = convention.example ? ` Example: ${convention.example}` : '';
156
+ return `- [${convention.severity}] ${convention.name}: ${convention.rule}${example}`;
157
+ }),
158
+ ].join('\n');
159
+ }
160
+ save(rootDir) {
161
+ const filePath = resolveConventionPath(rootDir);
162
+ fs.mkdirSync(path.dirname(filePath), { recursive: true });
163
+ fs.writeFileSync(filePath, `${stringify(this.set).trimEnd()}\n`, 'utf8');
164
+ }
165
+ getConventionSet() {
166
+ return {
167
+ name: this.set.name,
168
+ conventions: this.set.conventions.map((convention) => ({ ...convention })),
169
+ };
170
+ }
171
+ }
172
+ export function listConventionFiles(rootDir) {
173
+ return fg.sync(CONVENTION_PATTERNS, {
174
+ cwd: rootDir,
175
+ absolute: true,
176
+ onlyFiles: true,
177
+ unique: true,
178
+ ignore: CONVENTION_IGNORE,
179
+ });
180
+ }
181
+ export function resolveConventionPath(rootDir) {
182
+ return path.join(rootDir, '.icopilot', 'conventions.yaml');
183
+ }
184
+ export function loadConventionSet(rootDir) {
185
+ const filePath = resolveConventionPath(rootDir);
186
+ if (!fs.existsSync(filePath))
187
+ return null;
188
+ const manager = new ConventionManager();
189
+ return manager.load(rootDir);
190
+ }
191
+ export function loadConventionPromptContext(rootDir) {
192
+ const filePath = resolveConventionPath(rootDir);
193
+ if (!fs.existsSync(filePath))
194
+ return null;
195
+ try {
196
+ const manager = new ConventionManager();
197
+ manager.load(rootDir);
198
+ return manager.toPromptContext();
199
+ }
200
+ catch {
201
+ return null;
202
+ }
203
+ }
204
+ function emptyConventionSet(rootDir) {
205
+ const baseName = path.basename(rootDir) || 'project';
206
+ return {
207
+ name: `${baseName} conventions`,
208
+ conventions: [],
209
+ };
210
+ }
211
+ function normalizeConventionSet(value, fallbackName) {
212
+ if (!value || typeof value !== 'object' || Array.isArray(value)) {
213
+ return { name: fallbackName, conventions: [] };
214
+ }
215
+ const record = value;
216
+ const name = typeof record.name === 'string' && record.name.trim() ? record.name.trim() : fallbackName;
217
+ const conventions = Array.isArray(record.conventions)
218
+ ? record.conventions
219
+ .filter((entry) => entry && typeof entry === 'object')
220
+ .map((entry) => normalizeConvention(entry))
221
+ .sort(compareConventions)
222
+ : [];
223
+ return { name, conventions };
224
+ }
225
+ function normalizeConvention(value) {
226
+ const severity = normalizeSeverity(value.severity);
227
+ const name = value.name.trim();
228
+ const description = value.description.trim();
229
+ const rule = value.rule.trim();
230
+ if (!name || !description || !rule) {
231
+ throw new Error('convention name, description, and rule are required');
232
+ }
233
+ return {
234
+ id: slugify(value.id || value.name),
235
+ name,
236
+ description,
237
+ rule,
238
+ example: value.example?.trim() || undefined,
239
+ severity,
240
+ };
241
+ }
242
+ function normalizeSeverity(value) {
243
+ if (value === 'required' || value === 'recommended' || value === 'optional')
244
+ return value;
245
+ return 'recommended';
246
+ }
247
+ function slugify(value) {
248
+ return (value
249
+ .trim()
250
+ .toLowerCase()
251
+ .replace(/[^a-z0-9]+/g, '-')
252
+ .replace(/^-+|-+$/g, '') || 'convention');
253
+ }
254
+ function compareConventions(left, right) {
255
+ return (severityRank(left.severity) - severityRank(right.severity) ||
256
+ left.name.localeCompare(right.name));
257
+ }
258
+ function severityRank(severity) {
259
+ switch (severity) {
260
+ case 'required':
261
+ return 0;
262
+ case 'recommended':
263
+ return 1;
264
+ case 'optional':
265
+ default:
266
+ return 2;
267
+ }
268
+ }
269
+ function collectDetectionStats(files) {
270
+ const stats = {
271
+ singleQuotes: 0,
272
+ doubleQuotes: 0,
273
+ semicolonsAlways: 0,
274
+ semicolonsNever: 0,
275
+ esmImports: 0,
276
+ nodeProtocolImports: 0,
277
+ builtinImports: 0,
278
+ typeImports: 0,
279
+ vitestImports: 0,
280
+ };
281
+ for (const file of files) {
282
+ const source = fs.readFileSync(file, 'utf8');
283
+ stats.singleQuotes += source.match(/'([^'\\]|\\.)*'/g)?.length ?? 0;
284
+ stats.doubleQuotes += source.match(/"([^"\\]|\\.)*"/g)?.length ?? 0;
285
+ stats.esmImports += source.match(/^\s*(?:import|export)\s/gm)?.length ?? 0;
286
+ stats.nodeProtocolImports += source.match(/from\s+['"]node:[^'"]+['"]/g)?.length ?? 0;
287
+ stats.typeImports += source.match(/^\s*import\s+type\b/gm)?.length ?? 0;
288
+ stats.vitestImports += source.match(/from\s+['"]vitest['"]/g)?.length ?? 0;
289
+ for (const line of source.split(/\r?\n/)) {
290
+ const trimmed = line.trim();
291
+ if (!looksLikeStatement(trimmed))
292
+ continue;
293
+ if (/;\s*(?:\/\/.*)?$/.test(trimmed)) {
294
+ stats.semicolonsAlways += 1;
295
+ }
296
+ else {
297
+ stats.semicolonsNever += 1;
298
+ }
299
+ }
300
+ for (const match of source.matchAll(/(?:from\s+['"]|require\(\s*['"])([^'"]+)(?:['"]\s*\)?)/g)) {
301
+ const moduleName = match[1];
302
+ if (!moduleName || moduleName.startsWith('node:'))
303
+ continue;
304
+ if (BUILTIN_MODULES.has(moduleName)) {
305
+ stats.builtinImports += 1;
306
+ }
307
+ }
308
+ }
309
+ return stats;
310
+ }
311
+ function checkConvention(code, convention) {
312
+ const lowered = `${convention.id} ${convention.name} ${convention.description} ${convention.rule}`.toLowerCase();
313
+ if (lowered.includes('forbid:') || lowered.includes('forbid-regex:')) {
314
+ return checkForbiddenPattern(code, convention);
315
+ }
316
+ if (lowered.includes('require:') || lowered.includes('require-regex:')) {
317
+ return checkRequiredPattern(code, convention);
318
+ }
319
+ if (lowered.includes('semicolon')) {
320
+ return checkSemicolons(code, convention);
321
+ }
322
+ if (lowered.includes('single quote')) {
323
+ return checkStringQuotes(code, convention, 'single');
324
+ }
325
+ if (lowered.includes('double quote')) {
326
+ return checkStringQuotes(code, convention, 'double');
327
+ }
328
+ if (lowered.includes('esm') || lowered.includes('import/export syntax')) {
329
+ return checkEsmImports(code, convention);
330
+ }
331
+ if (lowered.includes('node:') || lowered.includes('built-in modules using the node: protocol')) {
332
+ return checkNodeProtocolImports(code, convention);
333
+ }
334
+ if (lowered.includes('vitest')) {
335
+ return checkVitest(code, convention);
336
+ }
337
+ return [];
338
+ }
339
+ function checkForbiddenPattern(code, convention) {
340
+ const expression = extractRulePattern(convention.rule, ['forbid:', 'forbid-regex:']);
341
+ if (!expression)
342
+ return [];
343
+ const regex = toRegExp(expression);
344
+ if (!regex)
345
+ return [];
346
+ const violations = [];
347
+ for (const match of code.matchAll(regex)) {
348
+ const index = match.index ?? 0;
349
+ violations.push({
350
+ convention,
351
+ line: lineNumberAt(code, index),
352
+ description: `Forbidden pattern matched: ${match[0]}`,
353
+ });
354
+ }
355
+ return violations;
356
+ }
357
+ function checkRequiredPattern(code, convention) {
358
+ const expression = extractRulePattern(convention.rule, ['require:', 'require-regex:']);
359
+ if (!expression)
360
+ return [];
361
+ const regex = toRegExp(expression);
362
+ if (!regex || regex.test(code))
363
+ return [];
364
+ return [
365
+ {
366
+ convention,
367
+ description: `Required pattern was not found: ${expression}`,
368
+ },
369
+ ];
370
+ }
371
+ function checkSemicolons(code, convention) {
372
+ const violations = [];
373
+ const lines = code.split(/\r?\n/);
374
+ for (let index = 0; index < lines.length; index++) {
375
+ const trimmed = lines[index]?.trim() ?? '';
376
+ if (!looksLikeStatement(trimmed) || /;\s*(?:\/\/.*)?$/.test(trimmed))
377
+ continue;
378
+ violations.push({
379
+ convention,
380
+ line: index + 1,
381
+ description: 'Statement should end with a semicolon.',
382
+ });
383
+ }
384
+ return violations;
385
+ }
386
+ function checkStringQuotes(code, convention, preferred) {
387
+ const regex = preferred === 'single' ? /"([^"\\]|\\.)*"/g : /'([^'\\]|\\.)*'/g;
388
+ const violations = [];
389
+ for (const match of code.matchAll(regex)) {
390
+ const index = match.index ?? 0;
391
+ violations.push({
392
+ convention,
393
+ line: lineNumberAt(code, index),
394
+ description: `Use ${preferred} quotes for strings.`,
395
+ });
396
+ }
397
+ return violations;
398
+ }
399
+ function checkEsmImports(code, convention) {
400
+ const violations = [];
401
+ for (const match of code.matchAll(/\brequire\(\s*['"][^'"]+['"]\s*\)|\bmodule\.exports\b|\bexports\.[A-Za-z0-9_]+/g)) {
402
+ const index = match.index ?? 0;
403
+ violations.push({
404
+ convention,
405
+ line: lineNumberAt(code, index),
406
+ description: 'Use import/export syntax instead of CommonJS module patterns.',
407
+ });
408
+ }
409
+ return violations;
410
+ }
411
+ function checkNodeProtocolImports(code, convention) {
412
+ const violations = [];
413
+ for (const match of code.matchAll(/(?:from\s+['"]|require\(\s*['"])([^'"]+)(?:['"]\s*\)?)/g)) {
414
+ const moduleName = match[1];
415
+ if (!moduleName || moduleName.startsWith('node:') || !BUILTIN_MODULES.has(moduleName))
416
+ continue;
417
+ const index = match.index ?? 0;
418
+ violations.push({
419
+ convention,
420
+ line: lineNumberAt(code, index),
421
+ description: `Import built-in module "${moduleName}" via the node: protocol.`,
422
+ });
423
+ }
424
+ return violations;
425
+ }
426
+ function checkVitest(code, convention) {
427
+ const violations = [];
428
+ for (const match of code.matchAll(/from\s+['"](?:@jest\/globals|jest|mocha)['"]/g)) {
429
+ const index = match.index ?? 0;
430
+ violations.push({
431
+ convention,
432
+ line: lineNumberAt(code, index),
433
+ description: 'Use Vitest imports for project tests.',
434
+ });
435
+ }
436
+ return violations;
437
+ }
438
+ function extractRulePattern(rule, prefixes) {
439
+ const lowered = rule.toLowerCase();
440
+ for (const prefix of prefixes) {
441
+ const start = lowered.indexOf(prefix);
442
+ if (start === -1)
443
+ continue;
444
+ return rule.slice(start + prefix.length).trim();
445
+ }
446
+ return null;
447
+ }
448
+ function toRegExp(expression) {
449
+ if (!expression)
450
+ return null;
451
+ const literal = expression.match(/^\/(.+)\/([a-z]*)$/i);
452
+ try {
453
+ if (literal) {
454
+ return new RegExp(literal[1], literal[2].includes('g') ? literal[2] : `${literal[2]}g`);
455
+ }
456
+ return new RegExp(expression, 'g');
457
+ }
458
+ catch {
459
+ return null;
460
+ }
461
+ }
462
+ function lineNumberAt(text, index) {
463
+ return text.slice(0, index).split(/\r?\n/).length;
464
+ }
465
+ function looksLikeStatement(line) {
466
+ if (!line)
467
+ return false;
468
+ if (line.startsWith('//') || line.startsWith('/*') || line.startsWith('*'))
469
+ return false;
470
+ if (/^[{}[\],]+$/.test(line))
471
+ return false;
472
+ if (/[{:,]$/.test(line))
473
+ return false;
474
+ return true;
475
+ }
@@ -0,0 +1,213 @@
1
+ import { randomUUID } from 'node:crypto';
2
+ import fs from 'node:fs';
3
+ import os from 'node:os';
4
+ import path from 'node:path';
5
+ export class CorrectionMemory {
6
+ storePath;
7
+ corrections = [];
8
+ constructor(storePath = resolveCorrectionsPath()) {
9
+ this.storePath = storePath;
10
+ }
11
+ add(correction) {
12
+ const normalized = normalizeDraft(correction);
13
+ if (!normalized)
14
+ return;
15
+ const existing = this.corrections.find((entry) => isSameCorrection(entry, normalized));
16
+ if (existing) {
17
+ existing.pattern = normalized.pattern;
18
+ existing.wrongBehavior = normalized.wrongBehavior;
19
+ existing.correctBehavior = normalized.correctBehavior;
20
+ existing.category = normalized.category;
21
+ existing.timestamp = Date.now();
22
+ existing.frequency += 1;
23
+ return;
24
+ }
25
+ this.corrections.push({
26
+ id: randomUUID(),
27
+ timestamp: Date.now(),
28
+ frequency: 1,
29
+ ...normalized,
30
+ });
31
+ }
32
+ remove(id) {
33
+ const normalizedId = id.trim();
34
+ if (!normalizedId)
35
+ return;
36
+ this.corrections = this.corrections.filter((entry) => entry.id !== normalizedId);
37
+ }
38
+ list() {
39
+ return sortCorrections(this.corrections).map(cloneCorrection);
40
+ }
41
+ search(query) {
42
+ const normalizedQuery = query.trim().toLowerCase();
43
+ if (!normalizedQuery)
44
+ return this.list();
45
+ return this.list().filter((entry) => searchableText(entry).includes(normalizedQuery));
46
+ }
47
+ getRelevant(context) {
48
+ const normalizedContext = context.trim().toLowerCase();
49
+ if (!normalizedContext)
50
+ return this.list().slice(0, 5);
51
+ const scored = this.corrections
52
+ .map((entry) => ({ entry, score: scoreCorrection(entry, normalizedContext) }))
53
+ .filter((candidate) => candidate.score >= 2)
54
+ .sort((left, right) => {
55
+ if (right.score !== left.score)
56
+ return right.score - left.score;
57
+ if (right.entry.frequency !== left.entry.frequency) {
58
+ return right.entry.frequency - left.entry.frequency;
59
+ }
60
+ return right.entry.timestamp - left.entry.timestamp;
61
+ })
62
+ .slice(0, 5)
63
+ .map((candidate) => cloneCorrection(candidate.entry));
64
+ return scored;
65
+ }
66
+ toPromptContext() {
67
+ return formatCorrectionsAsPrompt(this.list());
68
+ }
69
+ incrementFrequency(id) {
70
+ const normalizedId = id.trim();
71
+ if (!normalizedId)
72
+ return;
73
+ const correction = this.corrections.find((entry) => entry.id === normalizedId);
74
+ if (!correction)
75
+ return;
76
+ correction.frequency += 1;
77
+ correction.timestamp = Date.now();
78
+ }
79
+ save() {
80
+ fs.mkdirSync(path.dirname(this.storePath), { recursive: true });
81
+ fs.writeFileSync(this.storePath, `${JSON.stringify(this.list(), null, 2)}\n`, 'utf8');
82
+ }
83
+ load() {
84
+ this.corrections = [];
85
+ if (!fs.existsSync(this.storePath) || !fs.statSync(this.storePath).isFile())
86
+ return;
87
+ try {
88
+ const parsed = JSON.parse(fs.readFileSync(this.storePath, 'utf8'));
89
+ if (!Array.isArray(parsed))
90
+ return;
91
+ this.corrections = parsed.flatMap((entry) => {
92
+ const normalized = normalizeStoredCorrection(entry);
93
+ return normalized ? [normalized] : [];
94
+ });
95
+ }
96
+ catch {
97
+ this.corrections = [];
98
+ }
99
+ }
100
+ }
101
+ export function resolveCorrectionsPath() {
102
+ const configured = process.env.ICOPILOT_CORRECTIONS_PATH ||
103
+ path.join(os.homedir(), '.icopilot', 'corrections.json');
104
+ if (configured === '~')
105
+ return os.homedir();
106
+ if (/^~[\\/]/.test(configured))
107
+ return path.join(os.homedir(), configured.slice(2));
108
+ return path.resolve(configured);
109
+ }
110
+ export function formatCorrectionsAsPrompt(corrections) {
111
+ if (corrections.length === 0)
112
+ return '';
113
+ const lines = corrections.map((entry) => {
114
+ const qualifier = entry.pattern.toLowerCase() === entry.wrongBehavior.toLowerCase()
115
+ ? ''
116
+ : ` when the request matches "${entry.pattern}"`;
117
+ return `- Do NOT ${entry.wrongBehavior}${qualifier}. Instead, ${entry.correctBehavior}.`;
118
+ });
119
+ return ['User corrections to obey:', ...lines].join('\n');
120
+ }
121
+ export function loadCorrectionPromptContext(context) {
122
+ const memory = new CorrectionMemory();
123
+ memory.load();
124
+ const relevant = memory.getRelevant(context);
125
+ if (relevant.length === 0)
126
+ return null;
127
+ return formatCorrectionsAsPrompt(relevant);
128
+ }
129
+ function cloneCorrection(correction) {
130
+ return { ...correction };
131
+ }
132
+ function normalizeDraft(correction) {
133
+ const pattern = correction.pattern.trim();
134
+ const wrongBehavior = correction.wrongBehavior.trim();
135
+ const correctBehavior = correction.correctBehavior.trim();
136
+ const category = correction.category.trim();
137
+ if (!pattern || !wrongBehavior || !correctBehavior || !category)
138
+ return null;
139
+ return {
140
+ pattern,
141
+ wrongBehavior,
142
+ correctBehavior,
143
+ category,
144
+ };
145
+ }
146
+ function normalizeStoredCorrection(value) {
147
+ if (!value || typeof value !== 'object' || Array.isArray(value))
148
+ return null;
149
+ const record = value;
150
+ const draft = normalizeDraft({
151
+ pattern: typeof record.pattern === 'string' ? record.pattern : '',
152
+ wrongBehavior: typeof record.wrongBehavior === 'string' ? record.wrongBehavior : '',
153
+ correctBehavior: typeof record.correctBehavior === 'string' ? record.correctBehavior : '',
154
+ category: typeof record.category === 'string' ? record.category : '',
155
+ });
156
+ if (!draft || typeof record.id !== 'string')
157
+ return null;
158
+ return {
159
+ id: record.id,
160
+ timestamp: Number.isFinite(record.timestamp) ? Number(record.timestamp) : Date.now(),
161
+ frequency: Number.isFinite(record.frequency) ? Math.max(1, Number(record.frequency)) : 1,
162
+ ...draft,
163
+ };
164
+ }
165
+ function searchableText(correction) {
166
+ return [
167
+ correction.pattern,
168
+ correction.wrongBehavior,
169
+ correction.correctBehavior,
170
+ correction.category,
171
+ ]
172
+ .join('\n')
173
+ .toLowerCase();
174
+ }
175
+ function tokenize(value) {
176
+ return value
177
+ .toLowerCase()
178
+ .split(/[^a-z0-9]+/)
179
+ .filter(Boolean);
180
+ }
181
+ function scoreCorrection(correction, context) {
182
+ let score = 0;
183
+ const haystack = searchableText(correction);
184
+ const pattern = correction.pattern.toLowerCase();
185
+ if (context.includes(pattern) || pattern.includes(context)) {
186
+ score += 10;
187
+ }
188
+ for (const token of tokenize(context)) {
189
+ if (token.length < 4)
190
+ continue;
191
+ if (pattern.includes(token)) {
192
+ score += 3;
193
+ continue;
194
+ }
195
+ if (haystack.includes(token)) {
196
+ score += 1;
197
+ }
198
+ }
199
+ return score;
200
+ }
201
+ function sortCorrections(corrections) {
202
+ return [...corrections].sort((left, right) => {
203
+ if (right.frequency !== left.frequency)
204
+ return right.frequency - left.frequency;
205
+ return right.timestamp - left.timestamp;
206
+ });
207
+ }
208
+ function isSameCorrection(left, right) {
209
+ return (left.pattern.toLowerCase() === right.pattern.toLowerCase() &&
210
+ left.wrongBehavior.toLowerCase() === right.wrongBehavior.toLowerCase() &&
211
+ left.correctBehavior.toLowerCase() === right.correctBehavior.toLowerCase() &&
212
+ left.category.toLowerCase() === right.category.toLowerCase());
213
+ }