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.
- package/CHANGELOG.md +250 -0
- package/LICENSE +21 -0
- package/README.md +214 -0
- package/bin/icopilot.js +6 -0
- package/dist/acp/router.js +123 -0
- package/dist/acp/schema.js +53 -0
- package/dist/agents/aggregator.js +187 -0
- package/dist/agents/custom-agents.js +97 -0
- package/dist/agents/goal-driven.js +411 -0
- package/dist/agents/multi-repo.js +350 -0
- package/dist/agents/parallel-runner.js +181 -0
- package/dist/agents/router.js +144 -0
- package/dist/agents/self-heal.js +481 -0
- package/dist/agents/tdd-agent.js +278 -0
- package/dist/api/github-models.js +158 -0
- package/dist/bridge/ide-bridge.js +479 -0
- package/dist/cloud/routine-executor.js +34 -0
- package/dist/cloud/routine-scheduler.js +67 -0
- package/dist/cloud/routine-storage.js +297 -0
- package/dist/commands/acp-cmd.js +143 -0
- package/dist/commands/actions-cmd.js +624 -0
- package/dist/commands/agent-cmd.js +144 -0
- package/dist/commands/alias-cmd.js +132 -0
- package/dist/commands/bookmark-cmd.js +77 -0
- package/dist/commands/changelog-cmd.js +99 -0
- package/dist/commands/changes-cmd.js +120 -0
- package/dist/commands/clipboard-cmd.js +217 -0
- package/dist/commands/cloud-routine-cmd.js +265 -0
- package/dist/commands/codegen-cmd.js +544 -0
- package/dist/commands/compare-cmd.js +116 -0
- package/dist/commands/context-cmd.js +247 -0
- package/dist/commands/context-viz-cmd.js +43 -0
- package/dist/commands/conventions-cmd.js +116 -0
- package/dist/commands/cost-cmd.js +51 -0
- package/dist/commands/deps-cmd.js +294 -0
- package/dist/commands/diagram-cmd.js +658 -0
- package/dist/commands/diff-review-cmd.js +92 -0
- package/dist/commands/doc-cmd.js +412 -0
- package/dist/commands/doctor-cmd.js +152 -0
- package/dist/commands/editor-cmd.js +49 -0
- package/dist/commands/env-cmd.js +86 -0
- package/dist/commands/explain-cmd.js +78 -0
- package/dist/commands/explain-shell-cmd.js +22 -0
- package/dist/commands/explore-cmd.js +231 -0
- package/dist/commands/feedback-cmd.js +98 -0
- package/dist/commands/fix-cmd.js +17 -0
- package/dist/commands/generate-cmd.js +38 -0
- package/dist/commands/git-extra.js +197 -0
- package/dist/commands/git-log-cmd.js +98 -0
- package/dist/commands/git-undo-cmd.js +137 -0
- package/dist/commands/git.js +155 -0
- package/dist/commands/history-cmd.js +122 -0
- package/dist/commands/index-cmd.js +65 -0
- package/dist/commands/init-cmd.js +73 -0
- package/dist/commands/lint-cmd.js +133 -0
- package/dist/commands/memory-cmd.js +98 -0
- package/dist/commands/metrics-cmd.js +97 -0
- package/dist/commands/mode-prefix.js +30 -0
- package/dist/commands/multi-cmd.js +44 -0
- package/dist/commands/notify-cmd.js +204 -0
- package/dist/commands/profile-cmd.js +101 -0
- package/dist/commands/prompts.js +17 -0
- package/dist/commands/rag-cmd.js +60 -0
- package/dist/commands/readme-cmd.js +564 -0
- package/dist/commands/reasoning-cmd.js +34 -0
- package/dist/commands/refactor-cmd.js +96 -0
- package/dist/commands/release-cmd.js +450 -0
- package/dist/commands/repo-cmd.js +195 -0
- package/dist/commands/route-cmd.js +21 -0
- package/dist/commands/schedule-cmd.js +109 -0
- package/dist/commands/search-cmd.js +47 -0
- package/dist/commands/security-cmd.js +156 -0
- package/dist/commands/settings-cmd.js +238 -0
- package/dist/commands/skill-cmd.js +338 -0
- package/dist/commands/slash.js +2721 -0
- package/dist/commands/snippets-cmd.js +83 -0
- package/dist/commands/space-cmd.js +92 -0
- package/dist/commands/stash-cmd.js +156 -0
- package/dist/commands/stats-cmd.js +36 -0
- package/dist/commands/style-cmd.js +85 -0
- package/dist/commands/suggest-cmd.js +40 -0
- package/dist/commands/summary-cmd.js +138 -0
- package/dist/commands/task-cmd.js +58 -0
- package/dist/commands/team-memory-cmd.js +97 -0
- package/dist/commands/template-cmd.js +475 -0
- package/dist/commands/test-cmd.js +146 -0
- package/dist/commands/todo-cmd.js +172 -0
- package/dist/commands/tokens-cmd.js +277 -0
- package/dist/commands/trigger-cmd.js +147 -0
- package/dist/commands/undo-cmd.js +18 -0
- package/dist/commands/voice-cmd.js +89 -0
- package/dist/commands/watch-cmd.js +110 -0
- package/dist/commands/web-cmd.js +183 -0
- package/dist/commands/worktree-cmd.js +119 -0
- package/dist/config-profile.js +66 -0
- package/dist/config.js +288 -0
- package/dist/context/compactor.js +53 -0
- package/dist/context/dep-context.js +329 -0
- package/dist/context/file-refs.js +54 -0
- package/dist/context/git-context.js +229 -0
- package/dist/context/image-input.js +66 -0
- package/dist/context/memory.js +55 -0
- package/dist/context/persistent-memory.js +104 -0
- package/dist/context/pinned.js +96 -0
- package/dist/context/priority.js +150 -0
- package/dist/context/read-only.js +48 -0
- package/dist/context/smart-files.js +286 -0
- package/dist/context/team-memory.js +156 -0
- package/dist/extensions/loader.js +149 -0
- package/dist/extensions/marketplace.js +49 -0
- package/dist/extensions/slack-provider.js +181 -0
- package/dist/extensions/team.js +56 -0
- package/dist/extensions/teams-provider.js +222 -0
- package/dist/extensions/voice.js +18 -0
- package/dist/hooks/lifecycle.js +215 -0
- package/dist/hooks/precommit.js +463 -0
- package/dist/index/embeddings.js +23 -0
- package/dist/index/indexer.js +86 -0
- package/dist/index/retrieve.js +20 -0
- package/dist/index/store.js +95 -0
- package/dist/index.js +286 -0
- package/dist/intelligence/dead-code.js +457 -0
- package/dist/intelligence/error-watch.js +263 -0
- package/dist/intelligence/navigation.js +141 -0
- package/dist/intelligence/stack-trace.js +210 -0
- package/dist/intelligence/symbol-index.js +410 -0
- package/dist/knowledge/auto-memory.js +412 -0
- package/dist/knowledge/conventions.js +475 -0
- package/dist/knowledge/corrections.js +213 -0
- package/dist/knowledge/rag.js +450 -0
- package/dist/knowledge/style-learner.js +324 -0
- package/dist/logger.js +35 -0
- package/dist/mcp/client.js +144 -0
- package/dist/mcp/config.js +24 -0
- package/dist/mcp/index.js +89 -0
- package/dist/modes/auto-compact.js +20 -0
- package/dist/modes/autopilot.js +157 -0
- package/dist/modes/background.js +82 -0
- package/dist/modes/interactive.js +187 -0
- package/dist/modes/oneshot.js +36 -0
- package/dist/modes/tui.js +265 -0
- package/dist/modes/turn.js +342 -0
- package/dist/notifications/manager.js +107 -0
- package/dist/plugins/marketplace.js +244 -0
- package/dist/providers/custom-provider.js +298 -0
- package/dist/providers/local-model.js +121 -0
- package/dist/routing/profiles.js +44 -0
- package/dist/routing/router.js +18 -0
- package/dist/sandbox/container.js +151 -0
- package/dist/security/audit.js +237 -0
- package/dist/security/content-filter.js +449 -0
- package/dist/security/proxy.js +301 -0
- package/dist/security/retention.js +281 -0
- package/dist/security/roles.js +252 -0
- package/dist/server/api-server.js +679 -0
- package/dist/session/bookmarks.js +72 -0
- package/dist/session/cloud-session.js +291 -0
- package/dist/session/handoff.js +405 -0
- package/dist/session/manager.js +35 -0
- package/dist/session/session.js +296 -0
- package/dist/session/share.js +313 -0
- package/dist/session/undo-journal.js +91 -0
- package/dist/snippets/store.js +60 -0
- package/dist/spaces/space-config.js +156 -0
- package/dist/spaces/space.js +220 -0
- package/dist/stats/store.js +101 -0
- package/dist/tools/apply-patch.js +134 -0
- package/dist/tools/auto-check.js +218 -0
- package/dist/tools/diff-edit.js +150 -0
- package/dist/tools/diff-prompt.js +36 -0
- package/dist/tools/edit-file.js +66 -0
- package/dist/tools/file-ops.js +205 -0
- package/dist/tools/glob.js +17 -0
- package/dist/tools/grep.js +56 -0
- package/dist/tools/image.js +194 -0
- package/dist/tools/list-directory.js +228 -0
- package/dist/tools/memory.js +17 -0
- package/dist/tools/multi-edit.js +299 -0
- package/dist/tools/policy.js +95 -0
- package/dist/tools/registry.js +484 -0
- package/dist/tools/retry.js +74 -0
- package/dist/tools/run-in-terminal.js +162 -0
- package/dist/tools/safety.js +64 -0
- package/dist/tools/sandbox.js +15 -0
- package/dist/tools/search-symbols.js +212 -0
- package/dist/tools/shell.js +118 -0
- package/dist/tools/web.js +167 -0
- package/dist/ui/prompt.js +37 -0
- package/dist/ui/render.js +96 -0
- package/dist/ui/screen.js +13 -0
- package/dist/ui/theme.js +56 -0
- package/dist/util/browser.js +34 -0
- package/dist/util/completion.js +350 -0
- package/dist/util/cost.js +28 -0
- package/dist/util/keybindings.js +113 -0
- package/dist/util/lazy.js +26 -0
- package/dist/util/perf.js +25 -0
- package/dist/util/token-worker.js +11 -0
- package/dist/util/tokens.js +50 -0
- package/dist/workflows/builtins.js +128 -0
- package/dist/workflows/engine.js +496 -0
- package/dist/workflows/file-trigger.js +197 -0
- package/package.json +79 -0
|
@@ -0,0 +1,286 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import simpleGit from 'simple-git';
|
|
4
|
+
import { config } from '../config.js';
|
|
5
|
+
const DEFAULT_MAX_FILES = 10;
|
|
6
|
+
const SMALL_FILE_BYTES = 16 * 1024;
|
|
7
|
+
const DEFAULT_IGNORES = new Set(['.git', 'node_modules', 'dist', 'coverage', '.vitest']);
|
|
8
|
+
const STOP_WORDS = new Set([
|
|
9
|
+
'a',
|
|
10
|
+
'an',
|
|
11
|
+
'and',
|
|
12
|
+
'by',
|
|
13
|
+
'file',
|
|
14
|
+
'find',
|
|
15
|
+
'for',
|
|
16
|
+
'from',
|
|
17
|
+
'in',
|
|
18
|
+
'of',
|
|
19
|
+
'on',
|
|
20
|
+
'or',
|
|
21
|
+
'relevant',
|
|
22
|
+
'show',
|
|
23
|
+
'the',
|
|
24
|
+
'to',
|
|
25
|
+
'with',
|
|
26
|
+
]);
|
|
27
|
+
const SHORT_KEYWORDS = new Set(['c', 'go', 'js', 'md', 'py', 'ts']);
|
|
28
|
+
const EXTENSION_KEYWORDS = [
|
|
29
|
+
{ extensions: ['.ts', '.mts', '.cts'], keywords: ['ts', 'typescript', 'type-safe'] },
|
|
30
|
+
{ extensions: ['.tsx'], keywords: ['tsx', 'react', 'component', 'typescript'] },
|
|
31
|
+
{ extensions: ['.js', '.mjs', '.cjs'], keywords: ['js', 'javascript', 'node'] },
|
|
32
|
+
{ extensions: ['.json'], keywords: ['json', 'config', 'configuration', 'manifest'] },
|
|
33
|
+
{ extensions: ['.md'], keywords: ['docs', 'documentation', 'markdown', 'md', 'readme'] },
|
|
34
|
+
{ extensions: ['.yml', '.yaml'], keywords: ['workflow', 'yaml', 'yml', 'config'] },
|
|
35
|
+
];
|
|
36
|
+
export class SmartFileSelector {
|
|
37
|
+
cwd;
|
|
38
|
+
constructor(cwd = config.cwd) {
|
|
39
|
+
this.cwd = cwd;
|
|
40
|
+
}
|
|
41
|
+
async selectRelevant(query, options = {}) {
|
|
42
|
+
const maxFiles = Math.max(1, options.maxFiles ?? DEFAULT_MAX_FILES);
|
|
43
|
+
const keywords = extractKeywords(query);
|
|
44
|
+
const normalizedQuery = normalizeForMatch(query);
|
|
45
|
+
if (!keywords.length && !normalizedQuery) {
|
|
46
|
+
return [];
|
|
47
|
+
}
|
|
48
|
+
const candidates = await collectCandidateFiles(this.cwd, options);
|
|
49
|
+
if (!candidates.length) {
|
|
50
|
+
return [];
|
|
51
|
+
}
|
|
52
|
+
const recentPaths = options.preferRecent === false ? new Set() : await getRecentPaths(this.cwd);
|
|
53
|
+
const fallbackRecentPaths = recentPaths.size || options.preferRecent === false
|
|
54
|
+
? new Set()
|
|
55
|
+
: getFallbackRecentPaths(candidates, maxFiles);
|
|
56
|
+
return candidates
|
|
57
|
+
.map((candidate) => scoreCandidate(candidate, keywords, normalizedQuery, recentPaths, fallbackRecentPaths))
|
|
58
|
+
.filter((candidate) => candidate !== null)
|
|
59
|
+
.sort((left, right) => right.score - left.score || left.path.localeCompare(right.path))
|
|
60
|
+
.slice(0, maxFiles);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
async function collectCandidateFiles(cwd, options) {
|
|
64
|
+
const gitignoreRules = readGitignoreRules(cwd);
|
|
65
|
+
const matcher = options.filePattern ? createGlobMatcher(options.filePattern) : null;
|
|
66
|
+
const candidates = [];
|
|
67
|
+
async function walk(dir) {
|
|
68
|
+
let entries;
|
|
69
|
+
try {
|
|
70
|
+
entries = await fs.promises.readdir(dir, { withFileTypes: true });
|
|
71
|
+
}
|
|
72
|
+
catch {
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
await Promise.all(entries.map(async (entry) => {
|
|
76
|
+
const absolutePath = path.join(dir, entry.name);
|
|
77
|
+
const relativePath = normalizeSlashes(path.relative(cwd, absolutePath));
|
|
78
|
+
if (!relativePath) {
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
if (shouldIgnore(relativePath, entry.isDirectory(), gitignoreRules)) {
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
if (entry.isDirectory()) {
|
|
85
|
+
await walk(absolutePath);
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
if (!entry.isFile()) {
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
if (!options.includeTests && isTestFile(relativePath)) {
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
if (matcher && !matcher.test(relativePath)) {
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
try {
|
|
98
|
+
const stats = await fs.promises.stat(absolutePath);
|
|
99
|
+
candidates.push({ path: relativePath, size: stats.size, mtimeMs: stats.mtimeMs });
|
|
100
|
+
}
|
|
101
|
+
catch {
|
|
102
|
+
// Ignore unreadable files.
|
|
103
|
+
}
|
|
104
|
+
}));
|
|
105
|
+
}
|
|
106
|
+
await walk(cwd);
|
|
107
|
+
return candidates;
|
|
108
|
+
}
|
|
109
|
+
function scoreCandidate(candidate, keywords, normalizedQuery, recentPaths, fallbackRecentPaths) {
|
|
110
|
+
const normalizedPath = normalizeForMatch(candidate.path);
|
|
111
|
+
const ext = path.extname(candidate.path).toLowerCase();
|
|
112
|
+
const fileName = path.basename(candidate.path);
|
|
113
|
+
const normalizedFileName = normalizeForMatch(fileName);
|
|
114
|
+
const normalizedBaseName = normalizeForMatch(path.basename(candidate.path, ext));
|
|
115
|
+
const componentTokens = new Set(splitComponents(candidate.path));
|
|
116
|
+
const reasons = [];
|
|
117
|
+
let score = 0;
|
|
118
|
+
const exactMatchTerms = [normalizedQuery, ...keywords].filter(Boolean);
|
|
119
|
+
const baseNameTokens = normalizedBaseName.split('-').filter(Boolean);
|
|
120
|
+
const exactMatch = exactMatchTerms.some((term) => normalizedFileName === term || normalizedBaseName === term) ||
|
|
121
|
+
(baseNameTokens.length > 0 && baseNameTokens.every((token) => keywords.includes(token)));
|
|
122
|
+
if (exactMatch) {
|
|
123
|
+
score += 10;
|
|
124
|
+
reasons.push('exact filename match');
|
|
125
|
+
}
|
|
126
|
+
const matchedPathKeywords = keywords.filter((keyword) => {
|
|
127
|
+
if (componentTokens.has(keyword)) {
|
|
128
|
+
return true;
|
|
129
|
+
}
|
|
130
|
+
return normalizedPath.includes(keyword);
|
|
131
|
+
});
|
|
132
|
+
if (matchedPathKeywords.length) {
|
|
133
|
+
score += 5;
|
|
134
|
+
reasons.push(`path component match (${unique(matchedPathKeywords).join(', ')})`);
|
|
135
|
+
}
|
|
136
|
+
if (isExtensionRelevant(ext, candidate.path, keywords)) {
|
|
137
|
+
score += 3;
|
|
138
|
+
reasons.push(`extension relevance (${ext || 'no extension'})`);
|
|
139
|
+
}
|
|
140
|
+
if (recentPaths.has(candidate.path) || fallbackRecentPaths.has(candidate.path)) {
|
|
141
|
+
score += 2;
|
|
142
|
+
reasons.push('recently modified');
|
|
143
|
+
}
|
|
144
|
+
if (candidate.size <= SMALL_FILE_BYTES) {
|
|
145
|
+
score += 1;
|
|
146
|
+
reasons.push('small file bonus');
|
|
147
|
+
}
|
|
148
|
+
return score > 0 ? { path: candidate.path, score, reason: reasons.join('; ') } : null;
|
|
149
|
+
}
|
|
150
|
+
async function getRecentPaths(cwd) {
|
|
151
|
+
try {
|
|
152
|
+
const git = simpleGit({ baseDir: cwd });
|
|
153
|
+
const output = await git.raw(['log', '-n', '1', '--name-only', '--format=']);
|
|
154
|
+
return new Set(output
|
|
155
|
+
.split(/\r?\n/)
|
|
156
|
+
.map((line) => normalizeSlashes(line.trim()))
|
|
157
|
+
.filter(Boolean));
|
|
158
|
+
}
|
|
159
|
+
catch {
|
|
160
|
+
return new Set();
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
function getFallbackRecentPaths(candidates, maxFiles) {
|
|
164
|
+
const count = Math.min(candidates.length, Math.max(maxFiles, 3));
|
|
165
|
+
return new Set([...candidates]
|
|
166
|
+
.sort((left, right) => right.mtimeMs - left.mtimeMs)
|
|
167
|
+
.slice(0, count)
|
|
168
|
+
.map((candidate) => candidate.path));
|
|
169
|
+
}
|
|
170
|
+
function readGitignoreRules(cwd) {
|
|
171
|
+
const gitignorePath = path.join(cwd, '.gitignore');
|
|
172
|
+
if (!fs.existsSync(gitignorePath)) {
|
|
173
|
+
return [];
|
|
174
|
+
}
|
|
175
|
+
try {
|
|
176
|
+
return fs
|
|
177
|
+
.readFileSync(gitignorePath, 'utf8')
|
|
178
|
+
.split(/\r?\n/)
|
|
179
|
+
.map((line) => line.trim())
|
|
180
|
+
.filter((line) => line && !line.startsWith('#'));
|
|
181
|
+
}
|
|
182
|
+
catch {
|
|
183
|
+
return [];
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
function shouldIgnore(relativePath, isDirectory, rules) {
|
|
187
|
+
if (!relativePath || relativePath.startsWith('..')) {
|
|
188
|
+
return false;
|
|
189
|
+
}
|
|
190
|
+
const normalizedPath = normalizeSlashes(relativePath);
|
|
191
|
+
const segments = normalizedPath.split('/');
|
|
192
|
+
if (segments.some((segment) => DEFAULT_IGNORES.has(segment))) {
|
|
193
|
+
return true;
|
|
194
|
+
}
|
|
195
|
+
let ignored = false;
|
|
196
|
+
for (const rawRule of rules) {
|
|
197
|
+
const negated = rawRule.startsWith('!');
|
|
198
|
+
const rule = negated ? rawRule.slice(1) : rawRule;
|
|
199
|
+
if (!rule) {
|
|
200
|
+
continue;
|
|
201
|
+
}
|
|
202
|
+
if (matchesGitignoreRule(normalizedPath, segments, isDirectory, rule)) {
|
|
203
|
+
ignored = !negated;
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
return ignored;
|
|
207
|
+
}
|
|
208
|
+
function matchesGitignoreRule(normalizedPath, segments, isDirectory, rule) {
|
|
209
|
+
const normalizedRule = normalizeSlashes(rule).replace(/^\/+/, '');
|
|
210
|
+
const directoryOnly = normalizedRule.endsWith('/');
|
|
211
|
+
const bareRule = normalizedRule.replace(/\/+$/, '');
|
|
212
|
+
if (!bareRule) {
|
|
213
|
+
return false;
|
|
214
|
+
}
|
|
215
|
+
if (directoryOnly && !isDirectory && bareRule === normalizedPath) {
|
|
216
|
+
return false;
|
|
217
|
+
}
|
|
218
|
+
if (!hasGlob(bareRule) && !bareRule.includes('/')) {
|
|
219
|
+
return segments.includes(bareRule);
|
|
220
|
+
}
|
|
221
|
+
if (!hasGlob(bareRule)) {
|
|
222
|
+
return normalizedPath === bareRule || normalizedPath.startsWith(`${bareRule}/`);
|
|
223
|
+
}
|
|
224
|
+
return createGlobMatcher(bareRule).test(normalizedPath);
|
|
225
|
+
}
|
|
226
|
+
function createGlobMatcher(pattern) {
|
|
227
|
+
const normalizedPattern = normalizeSlashes(pattern).replace(/^\/+/, '');
|
|
228
|
+
const source = normalizedPattern
|
|
229
|
+
.replace(/[|\\{}()[\]^$+.]/g, '\\$&')
|
|
230
|
+
.replace(/\*\*/g, '::DOUBLE_STAR::')
|
|
231
|
+
.replace(/\*/g, '[^/]*')
|
|
232
|
+
.replace(/\?/g, '[^/]')
|
|
233
|
+
.replace(/::DOUBLE_STAR::/g, '.*');
|
|
234
|
+
return new RegExp(`^${source}$`, 'i');
|
|
235
|
+
}
|
|
236
|
+
function hasGlob(value) {
|
|
237
|
+
return /[*?[\]]/.test(value);
|
|
238
|
+
}
|
|
239
|
+
function extractKeywords(query) {
|
|
240
|
+
return unique(query
|
|
241
|
+
.toLowerCase()
|
|
242
|
+
.split(/[^a-z0-9.\-_]+/)
|
|
243
|
+
.map((part) => normalizeForMatch(part))
|
|
244
|
+
.filter((part) => {
|
|
245
|
+
if (!part)
|
|
246
|
+
return false;
|
|
247
|
+
if (STOP_WORDS.has(part))
|
|
248
|
+
return false;
|
|
249
|
+
return part.length > 1 || SHORT_KEYWORDS.has(part);
|
|
250
|
+
}));
|
|
251
|
+
}
|
|
252
|
+
function isExtensionRelevant(ext, filePath, keywords) {
|
|
253
|
+
const lowerPath = filePath.toLowerCase();
|
|
254
|
+
if (keywords.some((keyword) => keyword === ext.replace(/^\./, '') || keyword === lowerPath.split('.').slice(1).join('.'))) {
|
|
255
|
+
return true;
|
|
256
|
+
}
|
|
257
|
+
return EXTENSION_KEYWORDS.some((entry) => {
|
|
258
|
+
return (entry.extensions.includes(ext) &&
|
|
259
|
+
entry.keywords.some((keyword) => keywords.includes(normalizeForMatch(keyword))));
|
|
260
|
+
});
|
|
261
|
+
}
|
|
262
|
+
function isTestFile(filePath) {
|
|
263
|
+
const normalizedPath = normalizeSlashes(filePath).toLowerCase();
|
|
264
|
+
return (normalizedPath.includes('/tests/') ||
|
|
265
|
+
normalizedPath.includes('/__tests__/') ||
|
|
266
|
+
/(?:^|\/)[^.]+\.(test|spec)\.[^/]+$/.test(normalizedPath));
|
|
267
|
+
}
|
|
268
|
+
function splitComponents(filePath) {
|
|
269
|
+
return unique(normalizeSlashes(filePath)
|
|
270
|
+
.toLowerCase()
|
|
271
|
+
.split(/[/.\-_]+/)
|
|
272
|
+
.map((part) => part.trim())
|
|
273
|
+
.filter(Boolean));
|
|
274
|
+
}
|
|
275
|
+
function normalizeForMatch(value) {
|
|
276
|
+
return value
|
|
277
|
+
.toLowerCase()
|
|
278
|
+
.replace(/[^a-z0-9]+/g, '-')
|
|
279
|
+
.replace(/^-+|-+$/g, '');
|
|
280
|
+
}
|
|
281
|
+
function normalizeSlashes(value) {
|
|
282
|
+
return value.split(path.sep).join('/');
|
|
283
|
+
}
|
|
284
|
+
function unique(values) {
|
|
285
|
+
return [...new Set(values)];
|
|
286
|
+
}
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
export const TEAM_MEMORY_FILE = '.icopilot/team-memory.md';
|
|
4
|
+
const FILE_HEADER = `# Team memory
|
|
5
|
+
|
|
6
|
+
<!-- Shared team memory for conventions, decisions, tips, and warnings. -->
|
|
7
|
+
`;
|
|
8
|
+
export class TeamMemory {
|
|
9
|
+
filePath = null;
|
|
10
|
+
entries = [];
|
|
11
|
+
load(projectRoot) {
|
|
12
|
+
this.filePath = path.join(projectRoot, '.icopilot', 'team-memory.md');
|
|
13
|
+
this.entries = readEntries(this.filePath);
|
|
14
|
+
return this.list();
|
|
15
|
+
}
|
|
16
|
+
add(entry) {
|
|
17
|
+
this.assertLoaded();
|
|
18
|
+
const normalized = normalizeEntry(entry);
|
|
19
|
+
this.entries = this.entries.filter((candidate) => candidate.id !== normalized.id);
|
|
20
|
+
this.entries.push(normalized);
|
|
21
|
+
this.persist();
|
|
22
|
+
}
|
|
23
|
+
remove(id) {
|
|
24
|
+
this.assertLoaded();
|
|
25
|
+
const normalizedId = id.trim();
|
|
26
|
+
if (!normalizedId)
|
|
27
|
+
return;
|
|
28
|
+
this.entries = this.entries.filter((entry) => entry.id !== normalizedId);
|
|
29
|
+
this.persist();
|
|
30
|
+
}
|
|
31
|
+
list() {
|
|
32
|
+
return this.entries.map(cloneEntry);
|
|
33
|
+
}
|
|
34
|
+
render() {
|
|
35
|
+
if (this.entries.length === 0)
|
|
36
|
+
return '';
|
|
37
|
+
return [
|
|
38
|
+
'## Team memory',
|
|
39
|
+
...this.entries.map((entry) => {
|
|
40
|
+
const meta = [entry.category, entry.author?.trim(), entry.date?.trim()].filter((value) => Boolean(value));
|
|
41
|
+
return `- [${meta.join(' • ')}] ${entry.content}`;
|
|
42
|
+
}),
|
|
43
|
+
].join('\n');
|
|
44
|
+
}
|
|
45
|
+
search(query) {
|
|
46
|
+
const normalized = query.trim().toLowerCase();
|
|
47
|
+
if (!normalized)
|
|
48
|
+
return this.list();
|
|
49
|
+
return this.entries
|
|
50
|
+
.filter((entry) => [entry.id, entry.category, entry.content, entry.author, entry.date]
|
|
51
|
+
.filter((value) => typeof value === 'string')
|
|
52
|
+
.some((value) => value.toLowerCase().includes(normalized)))
|
|
53
|
+
.map(cloneEntry);
|
|
54
|
+
}
|
|
55
|
+
assertLoaded() {
|
|
56
|
+
if (!this.filePath)
|
|
57
|
+
throw new Error('Team memory must be loaded before use.');
|
|
58
|
+
}
|
|
59
|
+
persist() {
|
|
60
|
+
if (!this.filePath)
|
|
61
|
+
return;
|
|
62
|
+
fs.mkdirSync(path.dirname(this.filePath), { recursive: true });
|
|
63
|
+
fs.writeFileSync(this.filePath, serializeEntries(this.entries), 'utf8');
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
function readEntries(filePath) {
|
|
67
|
+
try {
|
|
68
|
+
if (!fs.existsSync(filePath) || !fs.statSync(filePath).isFile())
|
|
69
|
+
return [];
|
|
70
|
+
const text = fs.readFileSync(filePath, 'utf8');
|
|
71
|
+
return parseEntries(text);
|
|
72
|
+
}
|
|
73
|
+
catch {
|
|
74
|
+
return [];
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
function parseEntries(markdown) {
|
|
78
|
+
const entries = [];
|
|
79
|
+
const normalized = markdown.replace(/\r\n/g, '\n');
|
|
80
|
+
const lines = normalized.split('\n');
|
|
81
|
+
for (let index = 0; index < lines.length; index += 1) {
|
|
82
|
+
const line = lines[index];
|
|
83
|
+
if (!line.startsWith('## '))
|
|
84
|
+
continue;
|
|
85
|
+
const heading = line.slice(3).trim();
|
|
86
|
+
const bodyLines = [];
|
|
87
|
+
index += 1;
|
|
88
|
+
while (index < lines.length && !lines[index].startsWith('## ')) {
|
|
89
|
+
bodyLines.push(lines[index]);
|
|
90
|
+
index += 1;
|
|
91
|
+
}
|
|
92
|
+
const entry = parseSection(heading, bodyLines.join('\n'));
|
|
93
|
+
if (entry)
|
|
94
|
+
entries.push(entry);
|
|
95
|
+
if (index < lines.length && lines[index].startsWith('## ')) {
|
|
96
|
+
index -= 1;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
return entries;
|
|
100
|
+
}
|
|
101
|
+
function parseSection(heading, body) {
|
|
102
|
+
const match = body.trim().match(/^<!--\s*\n?([\s\S]*?)\n?-->\s*([\s\S]*)$/);
|
|
103
|
+
if (!match)
|
|
104
|
+
return null;
|
|
105
|
+
const metadata = parseMetadata(match[1] ?? '');
|
|
106
|
+
const category = metadata.category;
|
|
107
|
+
const content = (match[2] ?? '').trim();
|
|
108
|
+
if (!isCategory(category) || !content)
|
|
109
|
+
return null;
|
|
110
|
+
const id = typeof metadata.id === 'string' && metadata.id.trim() ? metadata.id.trim() : heading;
|
|
111
|
+
if (!id)
|
|
112
|
+
return null;
|
|
113
|
+
return normalizeEntry({
|
|
114
|
+
id,
|
|
115
|
+
category,
|
|
116
|
+
content,
|
|
117
|
+
author: metadata.author,
|
|
118
|
+
date: metadata.date,
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
function parseMetadata(block) {
|
|
122
|
+
const metadata = {};
|
|
123
|
+
for (const line of block.split(/\r?\n/)) {
|
|
124
|
+
const match = line.match(/^\s*([a-zA-Z][\w-]*)\s*:\s*(.*?)\s*$/);
|
|
125
|
+
if (!match)
|
|
126
|
+
continue;
|
|
127
|
+
metadata[match[1].toLowerCase()] = match[2];
|
|
128
|
+
}
|
|
129
|
+
return metadata;
|
|
130
|
+
}
|
|
131
|
+
function serializeEntries(entries) {
|
|
132
|
+
const blocks = entries.map((entry) => {
|
|
133
|
+
const metadataLines = [`id: ${entry.id}`, `category: ${entry.category}`];
|
|
134
|
+
if (entry.author?.trim())
|
|
135
|
+
metadataLines.push(`author: ${entry.author.trim()}`);
|
|
136
|
+
if (entry.date?.trim())
|
|
137
|
+
metadataLines.push(`date: ${entry.date.trim()}`);
|
|
138
|
+
return [`## ${entry.id}`, '<!--', ...metadataLines, '-->', entry.content.trim()].join('\n');
|
|
139
|
+
});
|
|
140
|
+
return `${[FILE_HEADER.trimEnd(), ...blocks].join('\n\n').trimEnd()}\n`;
|
|
141
|
+
}
|
|
142
|
+
function normalizeEntry(entry) {
|
|
143
|
+
return {
|
|
144
|
+
id: entry.id.trim(),
|
|
145
|
+
category: entry.category,
|
|
146
|
+
content: entry.content.trim(),
|
|
147
|
+
author: entry.author?.trim() || undefined,
|
|
148
|
+
date: entry.date?.trim() || undefined,
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
function cloneEntry(entry) {
|
|
152
|
+
return { ...entry };
|
|
153
|
+
}
|
|
154
|
+
function isCategory(value) {
|
|
155
|
+
return value === 'convention' || value === 'decision' || value === 'tip' || value === 'warning';
|
|
156
|
+
}
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import os from 'node:os';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { theme } from '../ui/theme.js';
|
|
5
|
+
const EXTENSIONS_DIR = path.join('.icopilot', 'extensions');
|
|
6
|
+
const HANDLER_FILE = 'index.js';
|
|
7
|
+
export function discoverExtensions(cwd) {
|
|
8
|
+
const userRoot = path.join(os.homedir(), EXTENSIONS_DIR);
|
|
9
|
+
const projectRoot = path.join(cwd, EXTENSIONS_DIR);
|
|
10
|
+
const discovered = new Map();
|
|
11
|
+
for (const root of [userRoot, projectRoot]) {
|
|
12
|
+
for (const extension of discoverExtensionsInRoot(root)) {
|
|
13
|
+
discovered.set(extension.name.toLowerCase(), extension);
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
return [...discovered.values()].sort((a, b) => a.name.localeCompare(b.name));
|
|
17
|
+
}
|
|
18
|
+
export function loadExtensionManifest(extDir) {
|
|
19
|
+
const manifestPath = path.join(extDir, 'manifest.json');
|
|
20
|
+
try {
|
|
21
|
+
const parsed = JSON.parse(fs.readFileSync(manifestPath, 'utf8'));
|
|
22
|
+
return isExtensionManifest(parsed) ? parsed : null;
|
|
23
|
+
}
|
|
24
|
+
catch {
|
|
25
|
+
return null;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
export function listExtensions(cwd) {
|
|
29
|
+
const extensions = discoverExtensions(cwd);
|
|
30
|
+
if (extensions.length === 0)
|
|
31
|
+
return theme.dim('No extensions discovered.\n');
|
|
32
|
+
const lines = extensions.map((extension) => {
|
|
33
|
+
const scope = extension.path.startsWith(path.join(cwd, '.icopilot'))
|
|
34
|
+
? theme.ok('project')
|
|
35
|
+
: theme.dim('user');
|
|
36
|
+
const toolCount = extension.tools?.length ?? 0;
|
|
37
|
+
const commandCount = extension.commands?.length ?? 0;
|
|
38
|
+
return ` ${theme.hl(extension.name)} ${theme.dim(`v${extension.version}`)} ${theme.dim(`(${scope})`)} ${extension.description}${theme.dim(` [tools:${toolCount} commands:${commandCount}]`)}`;
|
|
39
|
+
});
|
|
40
|
+
return `${theme.brand('Extensions')}\n${lines.join('\n')}\n`;
|
|
41
|
+
}
|
|
42
|
+
export function extensionCommand(args, cwd) {
|
|
43
|
+
const [subcommandRaw, ...rest] = args;
|
|
44
|
+
const subcommand = (subcommandRaw || 'list').toLowerCase();
|
|
45
|
+
switch (subcommand) {
|
|
46
|
+
case 'list':
|
|
47
|
+
return listExtensions(cwd);
|
|
48
|
+
case 'info':
|
|
49
|
+
return infoExtension(rest.join(' ').trim(), cwd);
|
|
50
|
+
case 'reload': {
|
|
51
|
+
const count = discoverExtensions(cwd).length;
|
|
52
|
+
return theme.ok(`✔ reloaded ${count} extension${count === 1 ? '' : 's'}\n`);
|
|
53
|
+
}
|
|
54
|
+
default:
|
|
55
|
+
return theme.warn('usage: /extension list|info <name>|reload\n');
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
function infoExtension(name, cwd) {
|
|
59
|
+
if (!name)
|
|
60
|
+
return theme.warn('usage: /extension info <name>\n');
|
|
61
|
+
const extension = discoverExtensions(cwd).find((candidate) => candidate.name.localeCompare(name, undefined, { sensitivity: 'accent' }) === 0);
|
|
62
|
+
if (!extension)
|
|
63
|
+
return theme.warn(`extension not found: ${name}\n`);
|
|
64
|
+
const lines = [
|
|
65
|
+
`${theme.brand('Extension')} ${theme.hl(extension.name)} ${theme.dim(`v${extension.version}`)}`,
|
|
66
|
+
` ${extension.description}`,
|
|
67
|
+
` ${theme.dim('path:')} ${extension.path}`,
|
|
68
|
+
];
|
|
69
|
+
if ((extension.tools?.length ?? 0) > 0) {
|
|
70
|
+
lines.push(` ${theme.ok('tools')}`);
|
|
71
|
+
for (const tool of extension.tools ?? []) {
|
|
72
|
+
lines.push(` ${theme.hl(tool.name)} ${theme.dim('→')} ${tool.description}`);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
if ((extension.commands?.length ?? 0) > 0) {
|
|
76
|
+
lines.push(` ${theme.ok('commands')}`);
|
|
77
|
+
for (const command of extension.commands ?? []) {
|
|
78
|
+
lines.push(` ${theme.hl(command.name)} ${theme.dim('→')} ${command.description}`);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
return `${lines.join('\n')}\n`;
|
|
82
|
+
}
|
|
83
|
+
function discoverExtensionsInRoot(root) {
|
|
84
|
+
if (!fs.existsSync(root))
|
|
85
|
+
return [];
|
|
86
|
+
const entries = fs.readdirSync(root, { withFileTypes: true });
|
|
87
|
+
const discovered = [];
|
|
88
|
+
for (const entry of entries) {
|
|
89
|
+
if (!entry.isDirectory())
|
|
90
|
+
continue;
|
|
91
|
+
const extDir = path.join(root, entry.name);
|
|
92
|
+
const manifest = loadExtensionManifest(extDir);
|
|
93
|
+
if (!manifest)
|
|
94
|
+
continue;
|
|
95
|
+
discovered.push(extendManifest(extDir, manifest));
|
|
96
|
+
}
|
|
97
|
+
return discovered;
|
|
98
|
+
}
|
|
99
|
+
function extendManifest(extDir, manifest) {
|
|
100
|
+
const handler = path.join(extDir, HANDLER_FILE);
|
|
101
|
+
return {
|
|
102
|
+
...manifest,
|
|
103
|
+
tools: manifest.tools?.map((tool) => ({ ...tool, handler })),
|
|
104
|
+
commands: manifest.commands?.map((command) => ({ ...command, handler })),
|
|
105
|
+
path: extDir,
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
function isExtensionManifest(value) {
|
|
109
|
+
if (!value || typeof value !== 'object' || Array.isArray(value))
|
|
110
|
+
return false;
|
|
111
|
+
const manifest = value;
|
|
112
|
+
return (isNonEmptyString(manifest.name) &&
|
|
113
|
+
isNonEmptyString(manifest.version) &&
|
|
114
|
+
isNonEmptyString(manifest.description) &&
|
|
115
|
+
isToolManifestArray(manifest.tools) &&
|
|
116
|
+
isCommandManifestArray(manifest.commands));
|
|
117
|
+
}
|
|
118
|
+
function isToolManifestArray(value) {
|
|
119
|
+
if (value === undefined)
|
|
120
|
+
return true;
|
|
121
|
+
if (!Array.isArray(value))
|
|
122
|
+
return false;
|
|
123
|
+
return value.every((tool) => {
|
|
124
|
+
if (!tool || typeof tool !== 'object' || Array.isArray(tool))
|
|
125
|
+
return false;
|
|
126
|
+
const entry = tool;
|
|
127
|
+
return (isNonEmptyString(entry.name) &&
|
|
128
|
+
isNonEmptyString(entry.description) &&
|
|
129
|
+
isPlainObject(entry.parameters));
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
function isCommandManifestArray(value) {
|
|
133
|
+
if (value === undefined)
|
|
134
|
+
return true;
|
|
135
|
+
if (!Array.isArray(value))
|
|
136
|
+
return false;
|
|
137
|
+
return value.every((command) => {
|
|
138
|
+
if (!command || typeof command !== 'object' || Array.isArray(command))
|
|
139
|
+
return false;
|
|
140
|
+
const entry = command;
|
|
141
|
+
return isNonEmptyString(entry.name) && isNonEmptyString(entry.description);
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
function isNonEmptyString(value) {
|
|
145
|
+
return typeof value === 'string' && value.trim().length > 0;
|
|
146
|
+
}
|
|
147
|
+
function isPlainObject(value) {
|
|
148
|
+
return Boolean(value) && typeof value === 'object' && !Array.isArray(value);
|
|
149
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import fs from 'node:fs/promises';
|
|
2
|
+
import os from 'node:os';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
export class LocalPluginCatalog {
|
|
5
|
+
file;
|
|
6
|
+
constructor(file = path.join(os.homedir(), '.icopilot', 'plugins.json')) {
|
|
7
|
+
this.file = file;
|
|
8
|
+
}
|
|
9
|
+
async list() {
|
|
10
|
+
try {
|
|
11
|
+
const raw = await fs.readFile(this.file, 'utf8');
|
|
12
|
+
const parsed = JSON.parse(raw);
|
|
13
|
+
if (!Array.isArray(parsed))
|
|
14
|
+
return [];
|
|
15
|
+
return parsed.filter(isPluginEntry);
|
|
16
|
+
}
|
|
17
|
+
catch (err) {
|
|
18
|
+
if (err?.code === 'ENOENT')
|
|
19
|
+
return [];
|
|
20
|
+
throw err;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
async search(query) {
|
|
24
|
+
const q = query.trim().toLowerCase();
|
|
25
|
+
const entries = await this.list();
|
|
26
|
+
if (!q)
|
|
27
|
+
return entries;
|
|
28
|
+
return entries.filter((entry) => [entry.name, entry.description, entry.install, entry.homepage || '']
|
|
29
|
+
.join('\n')
|
|
30
|
+
.toLowerCase()
|
|
31
|
+
.includes(q));
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
function isPluginEntry(value) {
|
|
35
|
+
if (!value || typeof value !== 'object')
|
|
36
|
+
return false;
|
|
37
|
+
const entry = value;
|
|
38
|
+
return (typeof entry.name === 'string' &&
|
|
39
|
+
typeof entry.description === 'string' &&
|
|
40
|
+
typeof entry.install === 'string' &&
|
|
41
|
+
(entry.homepage === undefined || typeof entry.homepage === 'string'));
|
|
42
|
+
}
|
|
43
|
+
let _catalog = new LocalPluginCatalog();
|
|
44
|
+
export function registerPluginCatalog(c) {
|
|
45
|
+
_catalog = c;
|
|
46
|
+
}
|
|
47
|
+
export function getPluginCatalog() {
|
|
48
|
+
return _catalog;
|
|
49
|
+
}
|