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,55 @@
1
+ import fs from 'node:fs';
2
+ import os from 'node:os';
3
+ import path from 'node:path';
4
+ import { PersistentMemory } from './persistent-memory.js';
5
+ import { TeamMemory } from './team-memory.js';
6
+ const MAX_MEMORY_BYTES = 16 * 1024;
7
+ function readMemory(file) {
8
+ try {
9
+ if (!fs.existsSync(file) || !fs.statSync(file).isFile())
10
+ return null;
11
+ const text = fs.readFileSync(file, 'utf8').slice(0, MAX_MEMORY_BYTES).trim();
12
+ return text || null;
13
+ }
14
+ catch {
15
+ return null;
16
+ }
17
+ }
18
+ export function loadMemoryBlock(cwd) {
19
+ const project = readMemory(path.join(cwd, '.icopilot', 'memory.md'));
20
+ const team = readTeamMemory(cwd);
21
+ const global = readMemory(path.join(os.homedir(), '.icopilot', 'memory.md'));
22
+ const persistent = readPersistentMemory(cwd);
23
+ const sections = [];
24
+ if (project)
25
+ sections.push(`## Project memory\n${project}`);
26
+ if (team)
27
+ sections.push(team);
28
+ if (global)
29
+ sections.push(`## Global memory\n${global}`);
30
+ if (persistent)
31
+ sections.push(persistent);
32
+ return sections.length ? sections.join('\n\n') : null;
33
+ }
34
+ function readPersistentMemory(cwd) {
35
+ try {
36
+ const memory = new PersistentMemory();
37
+ memory.load(memory.getProjectId(cwd));
38
+ const rendered = memory.render().slice(0, MAX_MEMORY_BYTES).trim();
39
+ return rendered || null;
40
+ }
41
+ catch {
42
+ return null;
43
+ }
44
+ }
45
+ function readTeamMemory(cwd) {
46
+ try {
47
+ const memory = new TeamMemory();
48
+ memory.load(cwd);
49
+ const rendered = memory.render().slice(0, MAX_MEMORY_BYTES).trim();
50
+ return rendered || null;
51
+ }
52
+ catch {
53
+ return null;
54
+ }
55
+ }
@@ -0,0 +1,104 @@
1
+ import crypto from 'node:crypto';
2
+ import fs from 'node:fs';
3
+ import os from 'node:os';
4
+ import path from 'node:path';
5
+ export class PersistentMemory {
6
+ storePath;
7
+ entries = new Map();
8
+ constructor(storePath = path.join(os.homedir(), '.icopilot', 'memory')) {
9
+ this.storePath = resolveStorePath(storePath);
10
+ }
11
+ load(projectId) {
12
+ this.entries.clear();
13
+ const filePath = this.filePath(projectId);
14
+ try {
15
+ if (!fs.existsSync(filePath) || !fs.statSync(filePath).isFile())
16
+ return;
17
+ const parsed = JSON.parse(fs.readFileSync(filePath, 'utf8'));
18
+ if (!Array.isArray(parsed))
19
+ return;
20
+ for (const item of parsed) {
21
+ const entry = parseMemoryEntry(item);
22
+ if (!entry)
23
+ continue;
24
+ this.entries.set(entry.key, entry);
25
+ }
26
+ }
27
+ catch {
28
+ this.entries.clear();
29
+ }
30
+ }
31
+ save(projectId) {
32
+ fs.mkdirSync(this.storePath, { recursive: true });
33
+ const filePath = this.filePath(projectId);
34
+ const payload = JSON.stringify(this.recall(), null, 2);
35
+ fs.writeFileSync(filePath, `${payload}\n`, 'utf8');
36
+ }
37
+ remember(key, value, source = 'user') {
38
+ const normalizedKey = key.trim();
39
+ const normalizedValue = value.trim();
40
+ if (!normalizedKey || !normalizedValue)
41
+ return;
42
+ const existing = this.entries.get(normalizedKey);
43
+ this.entries.set(normalizedKey, {
44
+ key: normalizedKey,
45
+ value: normalizedValue,
46
+ addedAt: existing?.addedAt ?? new Date().toISOString(),
47
+ source,
48
+ });
49
+ }
50
+ forget(key) {
51
+ return this.entries.delete(key.trim());
52
+ }
53
+ recall(query) {
54
+ const entries = [...this.entries.values()]
55
+ .map((entry) => ({ ...entry }))
56
+ .sort((a, b) => a.key.localeCompare(b.key));
57
+ const normalizedQuery = query?.trim().toLowerCase();
58
+ if (!normalizedQuery)
59
+ return entries;
60
+ return entries.filter((entry) => entry.key.toLowerCase().includes(normalizedQuery) ||
61
+ entry.value.toLowerCase().includes(normalizedQuery));
62
+ }
63
+ render() {
64
+ const entries = this.recall();
65
+ if (entries.length === 0)
66
+ return '';
67
+ return [
68
+ '## Persistent project memory',
69
+ ...entries.map((entry) => `- ${entry.key}: ${entry.value}`),
70
+ ].join('\n');
71
+ }
72
+ getProjectId(cwd) {
73
+ const normalized = path.resolve(cwd);
74
+ const stable = process.platform === 'win32' ? normalized.toLowerCase() : normalized;
75
+ return crypto.createHash('sha256').update(stable).digest('hex');
76
+ }
77
+ filePath(projectId) {
78
+ return path.join(this.storePath, `${projectId}.json`);
79
+ }
80
+ }
81
+ function parseMemoryEntry(value) {
82
+ if (!value || typeof value !== 'object')
83
+ return null;
84
+ const entry = value;
85
+ if (typeof entry.key !== 'string' ||
86
+ typeof entry.value !== 'string' ||
87
+ typeof entry.addedAt !== 'string' ||
88
+ (entry.source !== 'user' && entry.source !== 'auto')) {
89
+ return null;
90
+ }
91
+ return {
92
+ key: entry.key,
93
+ value: entry.value,
94
+ addedAt: entry.addedAt,
95
+ source: entry.source,
96
+ };
97
+ }
98
+ function resolveStorePath(storePath) {
99
+ if (storePath === '~')
100
+ return os.homedir();
101
+ if (/^~[\\/]/.test(storePath))
102
+ return path.join(os.homedir(), storePath.slice(2));
103
+ return path.resolve(storePath);
104
+ }
@@ -0,0 +1,96 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import { countTokensSync } from '../util/tokens.js';
4
+ export class PinnedContext {
5
+ files;
6
+ constructor(files = []) {
7
+ this.files = [...files];
8
+ }
9
+ add(filePath, cwd) {
10
+ const resolvedPath = path.resolve(cwd, filePath);
11
+ try {
12
+ if (!fs.statSync(resolvedPath).isFile())
13
+ return null;
14
+ const content = fs.readFileSync(resolvedPath, 'utf8');
15
+ const pinnedFile = {
16
+ path: resolvedPath,
17
+ addedAt: new Date().toISOString(),
18
+ tokens: countTokensSync(content),
19
+ };
20
+ const existingIndex = this.files.findIndex((file) => file.path === resolvedPath);
21
+ if (existingIndex >= 0) {
22
+ this.files[existingIndex] = pinnedFile;
23
+ }
24
+ else {
25
+ this.files.push(pinnedFile);
26
+ }
27
+ return pinnedFile;
28
+ }
29
+ catch {
30
+ return null;
31
+ }
32
+ }
33
+ remove(filePath) {
34
+ const normalizedTarget = path.normalize(filePath);
35
+ const nextFiles = this.files.filter((file) => path.normalize(file.path) !== normalizedTarget);
36
+ const removed = nextFiles.length !== this.files.length;
37
+ this.files = nextFiles;
38
+ return removed;
39
+ }
40
+ list() {
41
+ return [...this.files];
42
+ }
43
+ clear() {
44
+ const count = this.files.length;
45
+ this.files = [];
46
+ return count;
47
+ }
48
+ render() {
49
+ if (!this.files.length)
50
+ return '';
51
+ const parts = ['### Pinned context files'];
52
+ for (const file of this.files) {
53
+ parts.push('');
54
+ parts.push(`#### ${file.path}`);
55
+ try {
56
+ const content = fs.readFileSync(file.path, 'utf8');
57
+ const language = path.extname(file.path).replace(/^\./, '');
58
+ parts.push(`\`\`\`${language}`);
59
+ parts.push(content);
60
+ parts.push('```');
61
+ }
62
+ catch {
63
+ parts.push('_[error: unable to read file]_');
64
+ }
65
+ }
66
+ return parts.join('\n');
67
+ }
68
+ totalTokens() {
69
+ return this.files.reduce((total, file) => total + file.tokens, 0);
70
+ }
71
+ toJSON() {
72
+ return this.list();
73
+ }
74
+ static fromJSON(data) {
75
+ if (!Array.isArray(data))
76
+ return new PinnedContext();
77
+ const files = data.flatMap((entry) => {
78
+ if (!entry || typeof entry !== 'object')
79
+ return [];
80
+ const candidate = entry;
81
+ if (typeof candidate.path !== 'string' ||
82
+ typeof candidate.addedAt !== 'string' ||
83
+ typeof candidate.tokens !== 'number') {
84
+ return [];
85
+ }
86
+ return [
87
+ {
88
+ path: candidate.path,
89
+ addedAt: candidate.addedAt,
90
+ tokens: candidate.tokens,
91
+ },
92
+ ];
93
+ });
94
+ return new PinnedContext(files);
95
+ }
96
+ }
@@ -0,0 +1,150 @@
1
+ const PINNED_BONUS = 100;
2
+ const RECENTLY_MENTIONED_BONUS = 50;
3
+ const KEYWORD_OVERLAP_BONUS = 30;
4
+ const RECENTLY_MODIFIED_BONUS = 20;
5
+ const SMALL_FILE_BONUS = 10;
6
+ const TEAM_MEMORY_BONUS = 15;
7
+ const DEPENDENCY_PROXIMITY_BONUS = 25;
8
+ const SMALL_SOURCE_TOKEN_LIMIT = 400;
9
+ export class PriorityScorer {
10
+ score(sources, query) {
11
+ const queryKeywords = extractKeywords(query);
12
+ return sources
13
+ .map((source) => this.scoreSource(source, queryKeywords))
14
+ .sort(compareScoredSources);
15
+ }
16
+ selectWithinBudget(scored, tokenBudget) {
17
+ const selected = [];
18
+ let usedTokens = 0;
19
+ for (const source of scored) {
20
+ const pinned = isPinnedSource(source);
21
+ const nextTokens = usedTokens + source.tokens;
22
+ if (!pinned && nextTokens > tokenBudget)
23
+ continue;
24
+ selected.push(source);
25
+ usedTokens = nextTokens;
26
+ }
27
+ return selected;
28
+ }
29
+ scoreSource(source, queryKeywords) {
30
+ let score = 0;
31
+ const reasons = [];
32
+ if (isPinnedSource(source)) {
33
+ score += PINNED_BONUS;
34
+ reasons.push('pinned source');
35
+ }
36
+ if (hasTruthyFlag(source.metadata, ['recentlyMentioned', 'mentionedRecently', 'recentMention'])) {
37
+ score += RECENTLY_MENTIONED_BONUS;
38
+ reasons.push('recently mentioned in conversation');
39
+ }
40
+ if (hasKeywordOverlap(source, queryKeywords)) {
41
+ score += KEYWORD_OVERLAP_BONUS;
42
+ reasons.push('query keyword overlap');
43
+ }
44
+ if (isRecentlyModified(source)) {
45
+ score += RECENTLY_MODIFIED_BONUS;
46
+ reasons.push('recently modified');
47
+ }
48
+ if (source.tokens > 0 && source.tokens <= SMALL_SOURCE_TOKEN_LIMIT) {
49
+ score += SMALL_FILE_BONUS;
50
+ reasons.push('small source bonus');
51
+ }
52
+ if (source.type === 'team') {
53
+ score += TEAM_MEMORY_BONUS;
54
+ reasons.push('team memory');
55
+ }
56
+ if (hasDependencyProximity(source.metadata)) {
57
+ score += DEPENDENCY_PROXIMITY_BONUS;
58
+ reasons.push('dependency proximity');
59
+ }
60
+ return {
61
+ ...source,
62
+ score,
63
+ reasons,
64
+ };
65
+ }
66
+ }
67
+ export function buildContextWindow(sources, query, budget) {
68
+ const scorer = new PriorityScorer();
69
+ const scored = scorer.score(sources, query);
70
+ const selected = scorer.selectWithinBudget(scored, budget);
71
+ return selected
72
+ .map((source) => [
73
+ `### [${source.type}] ${source.id}`,
74
+ `score: ${source.score}`,
75
+ `reasons: ${source.reasons.join(', ') || 'none'}`,
76
+ source.content,
77
+ ].join('\n'))
78
+ .join('\n\n');
79
+ }
80
+ function compareScoredSources(left, right) {
81
+ return right.score - left.score || left.tokens - right.tokens || left.id.localeCompare(right.id);
82
+ }
83
+ function isPinnedSource(source) {
84
+ return source.type === 'pinned' || hasTruthyFlag(source.metadata, ['pinned', 'alwaysInclude']);
85
+ }
86
+ function isRecentlyModified(source) {
87
+ if (source.type === 'git')
88
+ return true;
89
+ return (hasTruthyFlag(source.metadata, ['recentlyModified', 'gitModified']) ||
90
+ isFreshTimestamp(source.metadata?.updatedAt));
91
+ }
92
+ function hasKeywordOverlap(source, queryKeywords) {
93
+ if (queryKeywords.size === 0)
94
+ return false;
95
+ const sourceKeywords = extractKeywords([
96
+ source.id,
97
+ source.content,
98
+ ...collectTextMetadata(source.metadata),
99
+ ...collectKeywordMetadata(source.metadata),
100
+ ].join(' '));
101
+ for (const keyword of queryKeywords) {
102
+ if (sourceKeywords.has(keyword))
103
+ return true;
104
+ }
105
+ return false;
106
+ }
107
+ function hasDependencyProximity(metadata) {
108
+ if (!metadata)
109
+ return false;
110
+ if (hasTruthyFlag(metadata, ['dependencyProximity', 'nearDependency']))
111
+ return true;
112
+ const distance = metadata.dependencyDistance;
113
+ return (typeof distance === 'number' && Number.isFinite(distance) && distance >= 0 && distance <= 2);
114
+ }
115
+ function hasTruthyFlag(metadata, keys) {
116
+ if (!metadata)
117
+ return false;
118
+ return keys.some((key) => Boolean(metadata[key]));
119
+ }
120
+ function isFreshTimestamp(value) {
121
+ if (typeof value !== 'string')
122
+ return false;
123
+ const timestamp = Date.parse(value);
124
+ if (Number.isNaN(timestamp))
125
+ return false;
126
+ return Date.now() - timestamp <= 7 * 24 * 60 * 60 * 1000;
127
+ }
128
+ function extractKeywords(text) {
129
+ return new Set(text
130
+ .toLowerCase()
131
+ .split(/[^a-z0-9_]+/i)
132
+ .map((part) => part.trim())
133
+ .filter((part) => part.length >= 3));
134
+ }
135
+ function collectTextMetadata(metadata) {
136
+ if (!metadata)
137
+ return [];
138
+ const values = [];
139
+ for (const key of ['path', 'title', 'summary', 'module', 'dependency']) {
140
+ const value = metadata[key];
141
+ if (typeof value === 'string' && value.trim())
142
+ values.push(value);
143
+ }
144
+ return values;
145
+ }
146
+ function collectKeywordMetadata(metadata) {
147
+ if (!metadata || !Array.isArray(metadata.keywords))
148
+ return [];
149
+ return metadata.keywords.filter((value) => typeof value === 'string');
150
+ }
@@ -0,0 +1,48 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import { config } from '../config.js';
4
+ const readOnlyFiles = new Set();
5
+ export function addReadOnly(filePath) {
6
+ const resolvedPath = path.resolve(config.cwd, filePath);
7
+ if (!fs.existsSync(resolvedPath) || !fs.statSync(resolvedPath).isFile()) {
8
+ throw new Error(`file not found: ${resolvedPath}`);
9
+ }
10
+ readOnlyFiles.add(path.normalize(resolvedPath));
11
+ return resolvedPath;
12
+ }
13
+ export function removeReadOnly(filePath) {
14
+ return readOnlyFiles.delete(path.normalize(path.resolve(config.cwd, filePath)));
15
+ }
16
+ export function isReadOnly(filePath) {
17
+ return readOnlyFiles.has(path.normalize(path.resolve(config.cwd, filePath)));
18
+ }
19
+ export function getReadOnlyFiles() {
20
+ return [...readOnlyFiles].sort((left, right) => left.localeCompare(right));
21
+ }
22
+ export function getReadOnlyContext() {
23
+ const files = getReadOnlyFiles();
24
+ if (!files.length)
25
+ return '';
26
+ const parts = [
27
+ '### Read-only context files',
28
+ '',
29
+ 'These files are available for context only. Do not modify them.',
30
+ ];
31
+ for (const file of files) {
32
+ parts.push('', `#### ${file} (read-only)`);
33
+ try {
34
+ const content = fs.readFileSync(file, 'utf8');
35
+ const language = path.extname(file).replace(/^\./, '');
36
+ parts.push(`\`\`\`${language}`);
37
+ parts.push(content);
38
+ parts.push('```');
39
+ }
40
+ catch {
41
+ parts.push('_[error: unable to read file]_');
42
+ }
43
+ }
44
+ return parts.join('\n');
45
+ }
46
+ export function clearReadOnly() {
47
+ readOnlyFiles.clear();
48
+ }