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,329 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ const STATIC_IMPORT_RE = /\bimport\s+(?:type\s+)?(?:[\s\S]*?\s+from\s+)?['"]([^'"]+)['"]/g;
4
+ const DYNAMIC_IMPORT_RE = /\bimport\s*\(\s*['"]([^'"]+)['"]\s*\)/g;
5
+ const REQUIRE_RE = /\brequire\s*\(\s*['"]([^'"]+)['"]\s*\)/g;
6
+ const RESOLVABLE_EXTENSIONS = ['.ts', '.tsx', '.js', '.jsx'];
7
+ export class DependencyResolver {
8
+ cwd;
9
+ tsconfigPath;
10
+ importCache = new Map();
11
+ nearestTsConfigCache = new Map();
12
+ tsConfigCache = new Map();
13
+ constructor(options = {}) {
14
+ this.cwd = path.resolve(options.cwd ?? process.cwd());
15
+ this.tsconfigPath = options.tsconfigPath
16
+ ? path.resolve(this.cwd, options.tsconfigPath)
17
+ : undefined;
18
+ }
19
+ resolveImports(filePath) {
20
+ const resolvedFile = this.resolveInputFile(filePath);
21
+ if (!resolvedFile)
22
+ return [];
23
+ const cached = this.importCache.get(resolvedFile);
24
+ if (cached)
25
+ return [...cached];
26
+ const source = this.readFile(resolvedFile);
27
+ if (source === null) {
28
+ this.importCache.set(resolvedFile, []);
29
+ return [];
30
+ }
31
+ const imports = dedupe(extractImportSpecifiers(source)
32
+ .map((specifier) => this.resolveSpecifier(specifier, resolvedFile))
33
+ .filter((value) => Boolean(value)));
34
+ this.importCache.set(resolvedFile, imports);
35
+ return [...imports];
36
+ }
37
+ getRelatedFiles(filePath, depth = 1) {
38
+ if (depth < 1)
39
+ return [];
40
+ const entryFile = this.resolveInputFile(filePath);
41
+ if (!entryFile)
42
+ return [];
43
+ const seen = new Set([entryFile]);
44
+ const related = [];
45
+ const walk = (currentFile, remainingDepth) => {
46
+ if (remainingDepth < 1)
47
+ return;
48
+ for (const importedFile of this.resolveImports(currentFile)) {
49
+ if (seen.has(importedFile))
50
+ continue;
51
+ seen.add(importedFile);
52
+ related.push(importedFile);
53
+ walk(importedFile, remainingDepth - 1);
54
+ }
55
+ };
56
+ walk(entryFile, depth);
57
+ return related;
58
+ }
59
+ buildDependencyGraph(entryFile) {
60
+ const rootFile = this.resolveInputFile(entryFile);
61
+ const nodes = new Map();
62
+ if (!rootFile)
63
+ return { nodes };
64
+ const visited = new Set();
65
+ const walk = (currentFile) => {
66
+ if (visited.has(currentFile))
67
+ return;
68
+ visited.add(currentFile);
69
+ const imports = this.resolveImports(currentFile);
70
+ nodes.set(currentFile, imports);
71
+ for (const importedFile of imports) {
72
+ walk(importedFile);
73
+ }
74
+ };
75
+ walk(rootFile);
76
+ return { nodes };
77
+ }
78
+ resolveInputFile(filePath) {
79
+ const absolutePath = path.isAbsolute(filePath) ? filePath : path.resolve(this.cwd, filePath);
80
+ return this.resolveWithExtensions(absolutePath);
81
+ }
82
+ resolveSpecifier(specifier, importerFile) {
83
+ if (!specifier)
84
+ return null;
85
+ if (specifier.startsWith('.') || path.isAbsolute(specifier)) {
86
+ return this.resolveWithExtensions(path.resolve(path.dirname(importerFile), specifier));
87
+ }
88
+ const tsConfig = this.getTsConfigForFile(importerFile);
89
+ if (tsConfig) {
90
+ for (const [pattern, targets] of Object.entries(tsConfig.paths)) {
91
+ const match = matchPathAlias(specifier, pattern);
92
+ if (!match.matches)
93
+ continue;
94
+ for (const target of targets) {
95
+ const replaced = target.includes('*') ? target.replace('*', match.value) : target;
96
+ const candidate = path.resolve(tsConfig.baseUrl ?? tsConfig.tsconfigDir, replaced);
97
+ const resolved = this.resolveWithExtensions(candidate);
98
+ if (resolved)
99
+ return resolved;
100
+ }
101
+ }
102
+ }
103
+ return isNodeModulesSpecifier(specifier) ? null : this.resolveWithExtensions(specifier);
104
+ }
105
+ resolveWithExtensions(candidatePath) {
106
+ const normalizedPath = path.resolve(candidatePath);
107
+ if (normalizedPath.includes(`${path.sep}node_modules${path.sep}`))
108
+ return null;
109
+ const candidates = new Set();
110
+ const extension = path.extname(normalizedPath);
111
+ const basePath = extension ? normalizedPath.slice(0, -extension.length) : normalizedPath;
112
+ candidates.add(normalizedPath);
113
+ if (extension) {
114
+ for (const knownExtension of RESOLVABLE_EXTENSIONS) {
115
+ candidates.add(`${basePath}${knownExtension}`);
116
+ }
117
+ }
118
+ else {
119
+ for (const knownExtension of RESOLVABLE_EXTENSIONS) {
120
+ candidates.add(`${normalizedPath}${knownExtension}`);
121
+ }
122
+ }
123
+ for (const knownExtension of RESOLVABLE_EXTENSIONS) {
124
+ candidates.add(path.join(normalizedPath, `index${knownExtension}`));
125
+ if (extension) {
126
+ candidates.add(path.join(basePath, `index${knownExtension}`));
127
+ }
128
+ }
129
+ for (const candidate of candidates) {
130
+ try {
131
+ if (fs.statSync(candidate).isFile()) {
132
+ return path.resolve(candidate);
133
+ }
134
+ }
135
+ catch {
136
+ // Ignore missing candidates.
137
+ }
138
+ }
139
+ return null;
140
+ }
141
+ getTsConfigForFile(filePath) {
142
+ const tsconfigPath = this.findNearestTsConfig(path.dirname(filePath));
143
+ if (!tsconfigPath)
144
+ return null;
145
+ return this.loadTsConfig(tsconfigPath);
146
+ }
147
+ findNearestTsConfig(startDir) {
148
+ if (this.tsconfigPath)
149
+ return fs.existsSync(this.tsconfigPath) ? this.tsconfigPath : null;
150
+ const normalizedStart = path.resolve(startDir);
151
+ const cached = this.nearestTsConfigCache.get(normalizedStart);
152
+ if (cached !== undefined)
153
+ return cached;
154
+ let currentDir = normalizedStart;
155
+ // eslint-disable-next-line no-constant-condition
156
+ while (true) {
157
+ const candidate = path.join(currentDir, 'tsconfig.json');
158
+ if (fs.existsSync(candidate) && fs.statSync(candidate).isFile()) {
159
+ this.nearestTsConfigCache.set(normalizedStart, candidate);
160
+ return candidate;
161
+ }
162
+ const parentDir = path.dirname(currentDir);
163
+ if (parentDir === currentDir)
164
+ break;
165
+ currentDir = parentDir;
166
+ }
167
+ this.nearestTsConfigCache.set(normalizedStart, null);
168
+ return null;
169
+ }
170
+ loadTsConfig(tsconfigPath) {
171
+ const normalizedPath = path.resolve(tsconfigPath);
172
+ const cached = this.tsConfigCache.get(normalizedPath);
173
+ if (cached)
174
+ return cached;
175
+ const loaded = this.loadRawTsConfig(normalizedPath, new Set());
176
+ const compilerOptions = loaded.compilerOptions ?? {};
177
+ const options = {
178
+ baseUrl: compilerOptions.baseUrl
179
+ ? path.resolve(path.dirname(normalizedPath), compilerOptions.baseUrl)
180
+ : undefined,
181
+ paths: compilerOptions.paths ?? {},
182
+ tsconfigDir: path.dirname(normalizedPath),
183
+ };
184
+ this.tsConfigCache.set(normalizedPath, options);
185
+ return options;
186
+ }
187
+ loadRawTsConfig(tsconfigPath, visited) {
188
+ const normalizedPath = path.resolve(tsconfigPath);
189
+ if (visited.has(normalizedPath))
190
+ return {};
191
+ visited.add(normalizedPath);
192
+ const localConfig = parseJsoncFile(normalizedPath) ?? {};
193
+ const inheritedPath = resolveExtendsPath(localConfig.extends, normalizedPath);
194
+ if (!inheritedPath)
195
+ return localConfig;
196
+ const parentConfig = this.loadRawTsConfig(inheritedPath, visited);
197
+ return mergeTsConfigs(parentConfig, localConfig);
198
+ }
199
+ readFile(filePath) {
200
+ try {
201
+ return fs.readFileSync(filePath, 'utf8');
202
+ }
203
+ catch {
204
+ return null;
205
+ }
206
+ }
207
+ }
208
+ function extractImportSpecifiers(source) {
209
+ const specifiers = [];
210
+ collectMatches(STATIC_IMPORT_RE, source, specifiers);
211
+ collectMatches(DYNAMIC_IMPORT_RE, source, specifiers);
212
+ collectMatches(REQUIRE_RE, source, specifiers);
213
+ return specifiers;
214
+ }
215
+ function collectMatches(pattern, source, output) {
216
+ pattern.lastIndex = 0;
217
+ let match;
218
+ while ((match = pattern.exec(source))) {
219
+ output.push(match[1]);
220
+ }
221
+ }
222
+ function dedupe(values) {
223
+ return [...new Set(values)];
224
+ }
225
+ function isNodeModulesSpecifier(specifier) {
226
+ return !specifier.startsWith('.') && !path.isAbsolute(specifier);
227
+ }
228
+ function matchPathAlias(specifier, pattern) {
229
+ if (!pattern.includes('*')) {
230
+ return { matches: specifier === pattern, value: '' };
231
+ }
232
+ const [prefix, suffix] = pattern.split('*');
233
+ if (!specifier.startsWith(prefix) || !specifier.endsWith(suffix)) {
234
+ return { matches: false, value: '' };
235
+ }
236
+ return {
237
+ matches: true,
238
+ value: specifier.slice(prefix.length, specifier.length - suffix.length),
239
+ };
240
+ }
241
+ function parseJsoncFile(filePath) {
242
+ try {
243
+ const raw = fs.readFileSync(filePath, 'utf8');
244
+ return JSON.parse(stripTrailingCommas(stripJsonComments(raw)));
245
+ }
246
+ catch {
247
+ return null;
248
+ }
249
+ }
250
+ function stripJsonComments(input) {
251
+ let output = '';
252
+ let inString = false;
253
+ let stringDelimiter = '';
254
+ let escapeNext = false;
255
+ let i = 0;
256
+ while (i < input.length) {
257
+ const current = input[i];
258
+ const next = input[i + 1];
259
+ if (inString) {
260
+ output += current;
261
+ if (escapeNext) {
262
+ escapeNext = false;
263
+ }
264
+ else if (current === '\\') {
265
+ escapeNext = true;
266
+ }
267
+ else if (current === stringDelimiter) {
268
+ inString = false;
269
+ stringDelimiter = '';
270
+ }
271
+ i += 1;
272
+ continue;
273
+ }
274
+ if (current === '"' || current === "'") {
275
+ inString = true;
276
+ stringDelimiter = current;
277
+ output += current;
278
+ i += 1;
279
+ continue;
280
+ }
281
+ if (current === '/' && next === '/') {
282
+ while (i < input.length && input[i] !== '\n')
283
+ i += 1;
284
+ continue;
285
+ }
286
+ if (current === '/' && next === '*') {
287
+ i += 2;
288
+ while (i < input.length && !(input[i] === '*' && input[i + 1] === '/'))
289
+ i += 1;
290
+ i += 2;
291
+ continue;
292
+ }
293
+ output += current;
294
+ i += 1;
295
+ }
296
+ return output;
297
+ }
298
+ function stripTrailingCommas(input) {
299
+ return input.replace(/,\s*([}\]])/g, '$1');
300
+ }
301
+ function resolveExtendsPath(extendsValue, tsconfigPath) {
302
+ if (!extendsValue)
303
+ return null;
304
+ if (!extendsValue.startsWith('.') && !path.isAbsolute(extendsValue))
305
+ return null;
306
+ const candidate = path.isAbsolute(extendsValue)
307
+ ? extendsValue
308
+ : path.resolve(path.dirname(tsconfigPath), extendsValue);
309
+ if (fs.existsSync(candidate) && fs.statSync(candidate).isFile())
310
+ return candidate;
311
+ const withJsonExtension = candidate.endsWith('.json') ? candidate : `${candidate}.json`;
312
+ if (fs.existsSync(withJsonExtension) && fs.statSync(withJsonExtension).isFile())
313
+ return withJsonExtension;
314
+ return null;
315
+ }
316
+ function mergeTsConfigs(baseConfig, overrideConfig) {
317
+ return {
318
+ ...baseConfig,
319
+ ...overrideConfig,
320
+ compilerOptions: {
321
+ ...(baseConfig.compilerOptions ?? {}),
322
+ ...(overrideConfig.compilerOptions ?? {}),
323
+ paths: {
324
+ ...(baseConfig.compilerOptions?.paths ?? {}),
325
+ ...(overrideConfig.compilerOptions?.paths ?? {}),
326
+ },
327
+ },
328
+ };
329
+ }
@@ -0,0 +1,54 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import { config } from '../config.js';
4
+ const FILE_REF_RE = /(^|\s)@([^\s@`'"<>]+)/g;
5
+ const MAX_BYTES = 256 * 1024; // 256KB per file
6
+ export function parseFileRefs(input) {
7
+ const out = [];
8
+ const seen = new Set();
9
+ let m;
10
+ FILE_REF_RE.lastIndex = 0;
11
+ while ((m = FILE_REF_RE.exec(input))) {
12
+ const rel = m[2];
13
+ if (seen.has(rel))
14
+ continue;
15
+ seen.add(rel);
16
+ const abs = path.resolve(config.cwd, rel);
17
+ const ref = { raw: `@${rel}`, rel, abs };
18
+ try {
19
+ const st = fs.statSync(abs);
20
+ if (!st.isFile()) {
21
+ ref.error = 'not a file';
22
+ }
23
+ else if (st.size > MAX_BYTES) {
24
+ ref.error = `file too large (${st.size} bytes; cap ${MAX_BYTES})`;
25
+ ref.content = fs.readFileSync(abs, 'utf8').slice(0, MAX_BYTES);
26
+ }
27
+ else {
28
+ ref.content = fs.readFileSync(abs, 'utf8');
29
+ }
30
+ }
31
+ catch (e) {
32
+ ref.error = e?.code || e?.message || 'read error';
33
+ }
34
+ out.push(ref);
35
+ }
36
+ return out;
37
+ }
38
+ /** Build a system-style context block from file refs. */
39
+ export function renderFileRefBlock(refs) {
40
+ if (!refs.length)
41
+ return null;
42
+ const parts = ['### Referenced files'];
43
+ for (const r of refs) {
44
+ if (r.error && !r.content) {
45
+ parts.push(`\n#### ${r.rel}\n_[error: ${r.error}]_`);
46
+ continue;
47
+ }
48
+ const lang = path.extname(r.rel).replace(/^\./, '') || '';
49
+ parts.push(`\n#### ${r.rel}\n\`\`\`${lang}\n${r.content}\n\`\`\``);
50
+ if (r.error)
51
+ parts.push(`_[note: ${r.error}]_`);
52
+ }
53
+ return parts.join('\n');
54
+ }
@@ -0,0 +1,229 @@
1
+ import { execSync } from 'node:child_process';
2
+ import { config } from '../config.js';
3
+ const DEFAULT_RECENT_COMMITS = 5;
4
+ const DEFAULT_CONTEXT_LIMIT = 8;
5
+ const MAX_DIFF_CHARS = 4_000;
6
+ export class GitContextProvider {
7
+ cwd;
8
+ constructor(cwd = config.cwd) {
9
+ this.cwd = cwd;
10
+ }
11
+ async getRecentlyModified(options = {}) {
12
+ if (!this.isRepository())
13
+ return [];
14
+ const args = [
15
+ 'log',
16
+ '--name-status',
17
+ '--format=format:',
18
+ `-n ${Math.max(1, options.commits ?? DEFAULT_RECENT_COMMITS)}`,
19
+ ];
20
+ if (options.since)
21
+ args.push(`--since=${quote(options.since)}`);
22
+ if (options.author)
23
+ args.push(`--author=${quote(options.author)}`);
24
+ if (options.paths?.length)
25
+ args.push(`-- ${options.paths.map(quote).join(' ')}`);
26
+ const output = this.runGit(args.join(' '));
27
+ return dedupeGitFiles(parseNameStatusOutput(output));
28
+ }
29
+ async getStagedFiles() {
30
+ return this.getDiffFiles('--cached');
31
+ }
32
+ async getUnstagedFiles() {
33
+ if (!this.isRepository())
34
+ return [];
35
+ const output = this.runGit('status --porcelain=v1');
36
+ const files = output
37
+ .split(/\r?\n/)
38
+ .map((line) => line.trimEnd())
39
+ .filter(Boolean)
40
+ .flatMap((line) => parsePorcelainLine(line, 'unstaged'));
41
+ return Promise.resolve(dedupeGitFiles(files.map((file) => ({
42
+ ...file,
43
+ diff: file.status === 'added' ? undefined : this.getFileDiff('', file.path),
44
+ }))));
45
+ }
46
+ async getBranchDiff(base) {
47
+ if (!this.isRepository())
48
+ return [];
49
+ const resolvedBase = base || this.detectBaseBranch();
50
+ const range = `${resolvedBase}...HEAD`;
51
+ const output = this.runGit(`diff --name-status ${quote(range)}`);
52
+ const files = parseNameStatusOutput(output).map((file) => ({
53
+ ...file,
54
+ diff: this.getFileDiff(quote(range), file.path),
55
+ }));
56
+ return dedupeGitFiles(files);
57
+ }
58
+ async getBlameContext(file, line) {
59
+ if (!this.isRepository()) {
60
+ throw new Error(`Not a git repository: ${this.cwd}`);
61
+ }
62
+ const safeLine = Math.max(1, Math.floor(line));
63
+ const output = this.runGit(`blame -L ${safeLine},${safeLine} --porcelain -- ${quote(file)}`);
64
+ return parseBlameOutput(output);
65
+ }
66
+ async getSessionContextFiles() {
67
+ if (!this.isRepository())
68
+ return [];
69
+ const [staged, unstaged, branchDiff] = await Promise.all([
70
+ this.getStagedFiles(),
71
+ this.getUnstagedFiles(),
72
+ this.getBranchDiff().catch(() => []),
73
+ ]);
74
+ const merged = dedupeGitFiles([...staged, ...unstaged, ...branchDiff]).slice(0, DEFAULT_CONTEXT_LIMIT);
75
+ if (merged.length > 0)
76
+ return merged;
77
+ return (await this.getRecentlyModified({ commits: 3 })).slice(0, DEFAULT_CONTEXT_LIMIT);
78
+ }
79
+ async getDiffFiles(diffMode) {
80
+ if (!this.isRepository())
81
+ return [];
82
+ const output = this.runGit(`diff ${diffMode} --name-status`.trim());
83
+ const files = parseNameStatusOutput(output).map((file) => ({
84
+ ...file,
85
+ diff: this.getFileDiff(diffMode, file.path),
86
+ }));
87
+ return dedupeGitFiles(files);
88
+ }
89
+ getFileDiff(diffMode, filePath) {
90
+ try {
91
+ const output = this.runGit(`diff ${diffMode} -- ${quote(filePath)}`.trim());
92
+ return output.trim() ? output.trim() : undefined;
93
+ }
94
+ catch {
95
+ return undefined;
96
+ }
97
+ }
98
+ detectBaseBranch() {
99
+ try {
100
+ const remoteHead = this.runGit('symbolic-ref refs/remotes/origin/HEAD').trim();
101
+ const match = remoteHead.match(/refs\/remotes\/origin\/(.+)/);
102
+ if (match?.[1])
103
+ return match[1].trim();
104
+ }
105
+ catch {
106
+ /* fall through */
107
+ }
108
+ for (const branch of ['main', 'master', 'develop']) {
109
+ try {
110
+ this.runGit(`rev-parse --verify ${quote(branch)}`);
111
+ return branch;
112
+ }
113
+ catch {
114
+ /* keep trying */
115
+ }
116
+ }
117
+ return 'HEAD~1';
118
+ }
119
+ isRepository() {
120
+ try {
121
+ return this.runGit('rev-parse --is-inside-work-tree').trim() === 'true';
122
+ }
123
+ catch {
124
+ return false;
125
+ }
126
+ }
127
+ runGit(args) {
128
+ return execSync(`git ${args}`, {
129
+ cwd: this.cwd,
130
+ encoding: 'utf8',
131
+ stdio: ['ignore', 'pipe', 'pipe'],
132
+ });
133
+ }
134
+ }
135
+ export function renderGitContextBlock(files) {
136
+ if (!files.length)
137
+ return '';
138
+ const parts = ['### Git context', '', 'Recently modified files to keep in mind:'];
139
+ for (const file of files) {
140
+ parts.push(`- ${file.status}: ${file.path}`);
141
+ if (file.diff?.trim()) {
142
+ parts.push('');
143
+ parts.push('```diff');
144
+ parts.push(truncateDiff(file.diff.trim()));
145
+ parts.push('```');
146
+ parts.push('');
147
+ }
148
+ }
149
+ return parts.join('\n').trim();
150
+ }
151
+ function truncateDiff(diff) {
152
+ return diff.length > MAX_DIFF_CHARS ? `${diff.slice(0, MAX_DIFF_CHARS)}\n... [truncated]` : diff;
153
+ }
154
+ function parseNameStatusOutput(output) {
155
+ return output
156
+ .split(/\r?\n/)
157
+ .map((line) => line.trim())
158
+ .filter(Boolean)
159
+ .flatMap((line) => {
160
+ const parts = line.split('\t').filter(Boolean);
161
+ if (parts.length < 2)
162
+ return [];
163
+ const [rawStatus, ...rawPaths] = parts;
164
+ const targetPath = rawPaths.at(-1);
165
+ if (!targetPath)
166
+ return [];
167
+ return [{ path: targetPath, status: normalizeStatus(rawStatus) }];
168
+ });
169
+ }
170
+ function parsePorcelainLine(line, mode) {
171
+ const indexStatus = line[0] ?? ' ';
172
+ const workTreeStatus = line[1] ?? ' ';
173
+ const rawPath = line.slice(3).trim();
174
+ const filePath = rawPath.includes(' -> ') ? rawPath.split(' -> ').at(-1)?.trim() : rawPath;
175
+ if (!filePath)
176
+ return [];
177
+ if (line.startsWith('??')) {
178
+ return [{ path: filePath, status: 'added' }];
179
+ }
180
+ if (mode === 'unstaged' && workTreeStatus !== ' ') {
181
+ return [{ path: filePath, status: normalizeStatus(workTreeStatus) }];
182
+ }
183
+ if (mode === 'unstaged' && indexStatus === 'U') {
184
+ return [{ path: filePath, status: 'modified' }];
185
+ }
186
+ return [];
187
+ }
188
+ function normalizeStatus(status) {
189
+ const normalized = status.trim().charAt(0);
190
+ if (normalized === 'A' || normalized === '?')
191
+ return 'added';
192
+ if (normalized === 'D')
193
+ return 'deleted';
194
+ return 'modified';
195
+ }
196
+ function parseBlameOutput(output) {
197
+ const lines = output.split(/\r?\n/);
198
+ const commit = lines[0]?.split(' ')[0]?.trim();
199
+ const author = extractBlameField(lines, 'author') || 'Unknown';
200
+ const message = extractBlameField(lines, 'summary') || '';
201
+ const authorTime = Number(extractBlameField(lines, 'author-time') || '0');
202
+ return {
203
+ author,
204
+ date: authorTime > 0 ? new Date(authorTime * 1000).toISOString() : '',
205
+ commit: commit || '',
206
+ message,
207
+ };
208
+ }
209
+ function extractBlameField(lines, field) {
210
+ const prefix = `${field} `;
211
+ return lines
212
+ .find((line) => line.startsWith(prefix))
213
+ ?.slice(prefix.length)
214
+ .trim();
215
+ }
216
+ function dedupeGitFiles(files) {
217
+ const seen = new Set();
218
+ const result = [];
219
+ for (const file of files) {
220
+ if (seen.has(file.path))
221
+ continue;
222
+ seen.add(file.path);
223
+ result.push(file);
224
+ }
225
+ return result;
226
+ }
227
+ function quote(value) {
228
+ return JSON.stringify(value);
229
+ }
@@ -0,0 +1,66 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ const IMAGE_EXTENSION_PATTERN = /\.(png|jpe?g|gif|webp|svg)$/i;
4
+ const IMAGE_PATH_PATTERN = /"([^"\r\n]+\.(?:png|jpe?g|gif|webp|svg))"|'([^'\r\n]+\.(?:png|jpe?g|gif|webp|svg))'|((?:[A-Za-z]:[\\/]|\.{1,2}[\\/]|~[\\/]|\/)?[^\s"'`<>]+\.(?:png|jpe?g|gif|webp|svg))/gi;
5
+ const MIME_TYPES = {
6
+ '.png': 'image/png',
7
+ '.jpg': 'image/jpeg',
8
+ '.jpeg': 'image/jpeg',
9
+ '.gif': 'image/gif',
10
+ '.webp': 'image/webp',
11
+ '.svg': 'image/svg+xml',
12
+ };
13
+ const VISION_MODEL_PATTERNS = [
14
+ /^gpt-4o(?:$|-)/i,
15
+ /^gpt-4\.1(?:$|-)/i,
16
+ /^gpt-4-turbo(?:$|-)/i,
17
+ /^gpt-4-vision(?:$|-)/i,
18
+ /^o4(?:$|-)/i,
19
+ /^claude-(?:3|3\.5|3\.7|sonnet-4|opus-4|haiku-4)/i,
20
+ /^gemini/i,
21
+ ];
22
+ export function detectImagePaths(message) {
23
+ const paths = [];
24
+ const seen = new Set();
25
+ for (const match of message.matchAll(IMAGE_PATH_PATTERN)) {
26
+ const candidate = normalizeDetectedPath(match[1] ?? match[2] ?? match[3] ?? '');
27
+ if (!candidate)
28
+ continue;
29
+ const key = candidate.toLowerCase();
30
+ if (seen.has(key))
31
+ continue;
32
+ seen.add(key);
33
+ paths.push(candidate);
34
+ }
35
+ return paths;
36
+ }
37
+ export function encodeImageToBase64(filePath) {
38
+ const resolvedPath = path.resolve(filePath);
39
+ const extension = path.extname(resolvedPath).toLowerCase();
40
+ const mimeType = MIME_TYPES[extension];
41
+ if (!mimeType) {
42
+ throw new Error(`unsupported image type: ${filePath}`);
43
+ }
44
+ const base64 = fs.readFileSync(resolvedPath).toString('base64');
45
+ return { base64, mimeType };
46
+ }
47
+ export function buildImageContent(filePaths) {
48
+ return filePaths.map((filePath) => {
49
+ const { base64, mimeType } = encodeImageToBase64(filePath);
50
+ return {
51
+ type: 'image_url',
52
+ image_url: {
53
+ url: `data:${mimeType};base64,${base64}`,
54
+ },
55
+ };
56
+ });
57
+ }
58
+ export function isVisionCapableModel(model) {
59
+ return VISION_MODEL_PATTERNS.some((pattern) => pattern.test(model.trim()));
60
+ }
61
+ function normalizeDetectedPath(rawValue) {
62
+ const value = rawValue.trim().replace(/[),.;:!?]+$/g, '');
63
+ if (!value || /^(?:https?:|data:)/i.test(value))
64
+ return null;
65
+ return IMAGE_EXTENSION_PATTERN.test(value) ? value : null;
66
+ }