harness-everything 0.16.4 → 0.16.6

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 (36) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/CHANGELOG.md +12 -0
  4. package/ci/mechanism-2am-openai-packaged-helper-runtime.test.js +110 -0
  5. package/ci/mechanism-2an-environment-detection-readonly.test.js +113 -0
  6. package/ci/skill-routing-check.js +1 -1
  7. package/docs/workflows/environment-detection.md +15 -7
  8. package/environment-detection/SKILL.md +15 -16
  9. package/harness-everything/SKILL.md +1 -1
  10. package/harness-everything/scripts/bootstrap.js +4 -3
  11. package/harness-everything/scripts/runtime-paths.js +37 -0
  12. package/harness-everything/scripts/self-heal.js +31 -8
  13. package/harness-everything/scripts/tier-router.js +2 -6
  14. package/hooks/scripts/lib/platforms/claude.js +3 -3
  15. package/opencode-plugin/plugin.json +1 -1
  16. package/package.json +1 -1
  17. package/plugins/harness-everything/.codex-plugin/plugin.json +1 -1
  18. package/plugins/harness-everything/hooks/scripts/lib/platforms/claude.js +3 -3
  19. package/plugins/harness-everything/plugin.json +1 -1
  20. package/plugins/harness-everything/scripts/installer.js +629 -0
  21. package/plugins/harness-everything/scripts/lib/claude-hooks.js +84 -0
  22. package/plugins/harness-everything/scripts/lib/execution-contract.js +70 -0
  23. package/plugins/harness-everything/scripts/lib/gitignore.js +199 -0
  24. package/plugins/harness-everything/scripts/lib/manifest.js +142 -0
  25. package/plugins/harness-everything/scripts/lib/path-boundary.js +90 -0
  26. package/plugins/harness-everything/scripts/lib/prompts.js +241 -0
  27. package/plugins/harness-everything/scripts/lib/skills.js +314 -0
  28. package/plugins/harness-everything/scripts/lib/workspace.js +290 -0
  29. package/plugins/harness-everything/skills/environment-detection/SKILL.md +15 -16
  30. package/plugins/harness-everything/skills/harness-everything/SKILL.md +1 -1
  31. package/plugins/harness-everything/skills/harness-everything/scripts/bootstrap.js +4 -3
  32. package/plugins/harness-everything/skills/harness-everything/scripts/runtime-paths.js +37 -0
  33. package/plugins/harness-everything/skills/harness-everything/scripts/self-heal.js +31 -8
  34. package/plugins/harness-everything/skills/harness-everything/scripts/tier-router.js +2 -6
  35. package/scripts/installer.js +12 -4
  36. package/scripts/sync-openai-plugin.js +5 -2
@@ -0,0 +1,241 @@
1
+ // Terminal TUI primitives used by the interactive installer flow. No install
2
+ // or filesystem logic lives here - purely input/output.
3
+ const readline = require('readline');
4
+
5
+ function interactiveSelect(items, pageSize = items.length) {
6
+ return new Promise((resolve) => {
7
+ let cursor = 0;
8
+ let scrollIndex = 0;
9
+ let visibleItems = [];
10
+ const isScrollable = items.length > pageSize;
11
+
12
+ function render() {
13
+ readline.cursorTo(process.stdout, 0);
14
+
15
+ if (isScrollable) {
16
+ if (cursor >= scrollIndex + pageSize) {
17
+ scrollIndex = cursor - pageSize + 1;
18
+ } else if (cursor < scrollIndex) {
19
+ scrollIndex = cursor;
20
+ }
21
+ }
22
+
23
+ visibleItems = items.slice(scrollIndex, scrollIndex + pageSize);
24
+ const writeLines = [];
25
+
26
+ if (isScrollable) {
27
+ if (scrollIndex > 0) {
28
+ writeLines.push(` ▲ (more above)`);
29
+ } else {
30
+ writeLines.push(``); // Spacer
31
+ }
32
+ }
33
+
34
+ for (let i = 0; i < visibleItems.length; i++) {
35
+ const itemIndex = scrollIndex + i;
36
+ const item = visibleItems[i];
37
+ const isSelected = item.checked ? '[x]' : '[ ]';
38
+ const isCurrent = itemIndex === cursor ? '> ' : ' ';
39
+ const desc = item.description ? ` - ${item.description}` : '';
40
+ const lineText = `${isCurrent}${isSelected} ${item.name || item.id}${desc}`;
41
+ const maxCols = process.stdout.columns || 80;
42
+ const truncated = lineText.length > maxCols - 5
43
+ ? lineText.slice(0, maxCols - 8) + '...'
44
+ : lineText;
45
+ writeLines.push(truncated);
46
+ }
47
+
48
+ if (isScrollable) {
49
+ if (scrollIndex + pageSize < items.length) {
50
+ writeLines.push(` ▼ (more below)`);
51
+ } else {
52
+ writeLines.push(``); // Spacer
53
+ }
54
+ }
55
+
56
+ writeLines.push(``);
57
+ writeLines.push(`(Use Up/Down Arrow keys to navigate, Space to toggle, Enter to confirm)`);
58
+
59
+ for (const line of writeLines) {
60
+ readline.clearLine(process.stdout, 0);
61
+ process.stdout.write(`${line}\n`);
62
+ }
63
+ }
64
+
65
+ readline.emitKeypressEvents(process.stdin);
66
+ if (process.stdin.isTTY) {
67
+ process.stdin.setRawMode(true);
68
+ }
69
+ process.stdin.resume();
70
+
71
+ // Hide cursor
72
+ process.stdout.write('[?25l');
73
+
74
+ render();
75
+
76
+ function cleanup() {
77
+ // Show cursor again
78
+ process.stdout.write('[?25h');
79
+ process.stdin.removeListener('keypress', handleKeypress);
80
+ if (process.stdin.isTTY) {
81
+ process.stdin.setRawMode(false);
82
+ }
83
+ process.stdin.pause();
84
+ }
85
+
86
+ function handleKeypress(str, key) {
87
+ if (key) {
88
+ if (key.ctrl && key.name === 'c') {
89
+ cleanup();
90
+ process.exit(130);
91
+ }
92
+
93
+ const linesToMove = pageSize + (isScrollable ? 4 : 2);
94
+
95
+ if (key.name === 'up') {
96
+ cursor = (cursor - 1 + items.length) % items.length;
97
+ readline.moveCursor(process.stdout, 0, -linesToMove);
98
+ render();
99
+ } else if (key.name === 'down') {
100
+ cursor = (cursor + 1) % items.length;
101
+ readline.moveCursor(process.stdout, 0, -linesToMove);
102
+ render();
103
+ } else if (key.name === 'space' || str === ' ') {
104
+ const itemIndex = cursor;
105
+ items[itemIndex].checked = !items[itemIndex].checked;
106
+ readline.moveCursor(process.stdout, 0, -linesToMove);
107
+ render();
108
+ } else if (key.name === 'return' || key.name === 'enter') {
109
+ cleanup();
110
+ readline.moveCursor(process.stdout, 0, -linesToMove);
111
+ for (let i = 0; i < linesToMove; i++) {
112
+ readline.clearLine(process.stdout, 0);
113
+ process.stdout.write('\n');
114
+ }
115
+ readline.moveCursor(process.stdout, 0, -linesToMove);
116
+
117
+ for (let i = 0; i < visibleItems.length; i++) {
118
+ readline.clearLine(process.stdout, 0);
119
+ const item = visibleItems[i];
120
+ const isSelected = item.checked ? '✔' : ' ';
121
+ process.stdout.write(` ${isSelected} ${item.name || item.id}\n`);
122
+ }
123
+ if (isScrollable) {
124
+ readline.clearLine(process.stdout, 0);
125
+ process.stdout.write('\n');
126
+ }
127
+ readline.clearLine(process.stdout, 0);
128
+ process.stdout.write('\n');
129
+ resolve(items);
130
+ }
131
+ }
132
+ }
133
+
134
+ process.stdin.on('keypress', handleKeypress);
135
+ });
136
+ }
137
+
138
+ async function askQuestion(query) {
139
+ const rl = readline.createInterface({
140
+ input: process.stdin,
141
+ output: process.stdout,
142
+ });
143
+ return new Promise(resolve => {
144
+ rl.question(query, answer => {
145
+ rl.close();
146
+ resolve(answer.trim().toLowerCase());
147
+ });
148
+ });
149
+ }
150
+
151
+ function interactiveSingleSelect(items) {
152
+ return new Promise((resolve) => {
153
+ let cursor = 0;
154
+ if (!items.some(i => i.checked)) {
155
+ items[0].checked = true;
156
+ }
157
+
158
+ function render() {
159
+ readline.cursorTo(process.stdout, 0);
160
+ for (let i = 0; i < items.length; i++) {
161
+ const item = items[i];
162
+ const isSelected = item.checked ? '[x]' : '[ ]';
163
+ const isCurrent = i === cursor ? '> ' : ' ';
164
+ readline.clearLine(process.stdout, 0);
165
+ process.stdout.write(`${isCurrent}${isSelected} ${item.name}\n`);
166
+ }
167
+ readline.clearLine(process.stdout, 0);
168
+ process.stdout.write(`\n`);
169
+ readline.clearLine(process.stdout, 0);
170
+ process.stdout.write(`(Use Up/Down Arrow keys to navigate, Space to select, Enter to confirm)\n`);
171
+ }
172
+
173
+ readline.emitKeypressEvents(process.stdin);
174
+ if (process.stdin.isTTY) {
175
+ process.stdin.setRawMode(true);
176
+ }
177
+ process.stdin.resume();
178
+ process.stdout.write('[?25l');
179
+ render();
180
+
181
+ function cleanup() {
182
+ process.stdout.write('[?25h');
183
+ process.stdin.removeListener('keypress', handleKeypress);
184
+ if (process.stdin.isTTY) {
185
+ process.stdin.setRawMode(false);
186
+ }
187
+ process.stdin.pause();
188
+ }
189
+
190
+ function handleKeypress(str, key) {
191
+ if (key) {
192
+ if (key.ctrl && key.name === 'c') {
193
+ cleanup();
194
+ process.exit(130);
195
+ }
196
+
197
+ const linesToMove = items.length + 2;
198
+
199
+ if (key.name === 'up') {
200
+ cursor = (cursor - 1 + items.length) % items.length;
201
+ readline.moveCursor(process.stdout, 0, -linesToMove);
202
+ render();
203
+ } else if (key.name === 'down') {
204
+ cursor = (cursor + 1) % items.length;
205
+ readline.moveCursor(process.stdout, 0, -linesToMove);
206
+ render();
207
+ } else if (key.name === 'space' || str === ' ') {
208
+ items.forEach((item, idx) => {
209
+ item.checked = (idx === cursor);
210
+ });
211
+ readline.moveCursor(process.stdout, 0, -linesToMove);
212
+ render();
213
+ } else if (key.name === 'return' || key.name === 'enter') {
214
+ items.forEach((item, idx) => {
215
+ item.checked = (idx === cursor);
216
+ });
217
+ cleanup();
218
+ readline.moveCursor(process.stdout, 0, -linesToMove);
219
+ for (let i = 0; i < linesToMove; i++) {
220
+ readline.clearLine(process.stdout, 0);
221
+ process.stdout.write('\n');
222
+ }
223
+ readline.moveCursor(process.stdout, 0, -linesToMove);
224
+
225
+ for (let i = 0; i < items.length; i++) {
226
+ readline.clearLine(process.stdout, 0);
227
+ const item = items[i];
228
+ const isSelected = item.checked ? '✔' : ' ';
229
+ process.stdout.write(` ${isSelected} ${item.name}\n`);
230
+ }
231
+ readline.clearLine(process.stdout, 0);
232
+ process.stdout.write('\n');
233
+ resolve(items.find(item => item.checked).id);
234
+ }
235
+ }
236
+ }
237
+ process.stdin.on('keypress', handleKeypress);
238
+ });
239
+ }
240
+
241
+ module.exports = { interactiveSelect, interactiveSingleSelect, askQuestion };
@@ -0,0 +1,314 @@
1
+ // Skill discovery, copy, and manifest-aware install/uninstall bookkeeping.
2
+ const fs = require('fs');
3
+ const path = require('path');
4
+ const manifest = require('./manifest');
5
+
6
+ function copyDir(src, dest) {
7
+ fs.mkdirSync(dest, { recursive: true });
8
+ const entries = fs.readdirSync(src, { withFileTypes: true });
9
+ for (const entry of entries) {
10
+ const srcPath = path.join(src, entry.name);
11
+ const destPath = path.join(dest, entry.name);
12
+ if (entry.isDirectory()) copyDir(srcPath, destPath);
13
+ else fs.copyFileSync(srcPath, destPath);
14
+ }
15
+ }
16
+
17
+ function isLinkPath(p) {
18
+ try { return fs.lstatSync(p).isSymbolicLink(); } catch (e) { return false; }
19
+ }
20
+
21
+ function pathPresent(p) {
22
+ return isLinkPath(p) || fs.existsSync(p);
23
+ }
24
+
25
+ function removePathSafely(p) {
26
+ if (pathPresent(p)) fs.rmSync(p, { recursive: true, force: true });
27
+ }
28
+
29
+ // `.agents/skills/` is the canonical per-scope store used for deduplication.
30
+ // Some hosts consume that path natively (for example Codex project skills),
31
+ // while others link from their own native directories (Continue/Hermes).
32
+ function getCanonicalSkillsDir({ isGlobal, workspaceRoot, userHome }) {
33
+ return path.join(isGlobal ? userHome : workspaceRoot, '.agents', 'skills');
34
+ }
35
+
36
+ function getAvailableSkills(harnessSourceDir) {
37
+ const dirs = fs.readdirSync(harnessSourceDir, { withFileTypes: true });
38
+ const skills = [];
39
+ for (const dir of dirs) {
40
+ if (dir.isDirectory() && !dir.name.startsWith('.') && !['bin', 'docs', 'hooks', 'scripts', 'node_modules'].includes(dir.name)) {
41
+ const skillPath = path.join(harnessSourceDir, dir.name, 'SKILL.md');
42
+ if (fs.existsSync(skillPath)) skills.push(dir.name);
43
+ }
44
+ }
45
+ return skills.sort();
46
+ }
47
+
48
+ function parseFrontmatter(skillMdPath) {
49
+ if (!fs.existsSync(skillMdPath)) return null;
50
+ try {
51
+ const content = fs.readFileSync(skillMdPath, 'utf8');
52
+ const match = content.match(/^---\r?\n([\s\S]*?)\r?\n---/);
53
+ if (!match) return null;
54
+ const yaml = match[1];
55
+ const get = (key) => {
56
+ const m = yaml.match(new RegExp(`^${key}:\\s*(.*)$`, 'm'));
57
+ return m ? m[1].trim().replace(/^['"]|['"]$/g, '') : '';
58
+ };
59
+ const getMeta = (key) => {
60
+ const m = yaml.match(new RegExp(`^[ \\t]+${key}:\\s*(.*)$`, 'm'));
61
+ return m ? m[1].trim().replace(/^['"]|['"]$/g, '') : '';
62
+ };
63
+ return {
64
+ name: get('name'),
65
+ description: get('description'),
66
+ author: getMeta('author') || get('author'),
67
+ version: getMeta('version') || get('version'),
68
+ };
69
+ } catch (e) {
70
+ return null;
71
+ }
72
+ }
73
+
74
+ function getSkillInfo(harnessSourceDir, skillDirName) {
75
+ const fm = parseFrontmatter(path.join(harnessSourceDir, skillDirName, 'SKILL.md'));
76
+ if (!fm) return null;
77
+ return { id: skillDirName, name: fm.name || skillDirName, description: fm.description || '' };
78
+ }
79
+
80
+ function isHarnessSkillDir(dirPath) {
81
+ const fm = parseFrontmatter(path.join(dirPath, 'SKILL.md'));
82
+ return !!fm && fm.author === manifest.HARNESS_AUTHOR;
83
+ }
84
+
85
+ function ensureCanonicalSkill({ canonicalDir, skillName, harnessSourceDir }) {
86
+ const dest = path.join(canonicalDir, skillName);
87
+ removePathSafely(dest);
88
+ copyDir(path.join(harnessSourceDir, skillName), dest);
89
+ return dest;
90
+ }
91
+
92
+ function linkOrCopySkill({ dest, canonicalSkillDir, harnessSourceDir, skillName, linkMode }) {
93
+ removePathSafely(dest);
94
+ fs.mkdirSync(path.dirname(dest), { recursive: true });
95
+
96
+ if (linkMode === 'copy') {
97
+ copyDir(path.join(harnessSourceDir, skillName), dest);
98
+ return { kind: 'copy' };
99
+ }
100
+
101
+ const symlinkType = process.platform === 'win32' ? 'junction' : 'dir';
102
+ try {
103
+ fs.symlinkSync(canonicalSkillDir, dest, symlinkType);
104
+ return { kind: symlinkType === 'junction' ? 'junction' : 'symlink', canonicalPath: canonicalSkillDir };
105
+ } catch (e) {
106
+ if (linkMode === 'symlink') throw new Error(`Failed to create symlink at ${dest}: ${e.message}`);
107
+ copyDir(path.join(harnessSourceDir, skillName), dest);
108
+ return { kind: 'copy' };
109
+ }
110
+ }
111
+
112
+ function installSkillsToTargets({ chosenSkills, targetDirs, harnessSourceDir, packageVersion, canonicalDir, linkMode = 'auto' }) {
113
+ const useCanonical = !!canonicalDir && linkMode !== 'copy';
114
+ const canonicalSkillDirs = {};
115
+ if (useCanonical) {
116
+ for (const skillName of chosenSkills) {
117
+ canonicalSkillDirs[skillName] = ensureCanonicalSkill({ canonicalDir, skillName, harnessSourceDir });
118
+ }
119
+ }
120
+
121
+ for (const target of targetDirs) {
122
+ fs.mkdirSync(target.path, { recursive: true });
123
+ const refSrc = path.join(harnessSourceDir, 'references');
124
+ if (fs.existsSync(refSrc)) {
125
+ const refDest = path.join(path.dirname(target.manifestPath), 'references');
126
+ if (fs.existsSync(refDest)) fs.rmSync(refDest, { recursive: true, force: true });
127
+ copyDir(refSrc, refDest);
128
+ }
129
+
130
+ const targetIsCanonical = useCanonical && path.resolve(target.path) === path.resolve(canonicalDir);
131
+ for (const skillName of chosenSkills) {
132
+ const dest = path.join(target.path, skillName);
133
+ let result;
134
+ if (!useCanonical) {
135
+ removePathSafely(dest);
136
+ copyDir(path.join(harnessSourceDir, skillName), dest);
137
+ result = { kind: 'copy' };
138
+ } else if (targetIsCanonical) {
139
+ result = { kind: 'copy' };
140
+ } else {
141
+ result = linkOrCopySkill({ dest, canonicalSkillDir: canonicalSkillDirs[skillName], harnessSourceDir, skillName, linkMode });
142
+ }
143
+ manifest.recordSkillInstall(target.manifestPath, packageVersion, skillName, dest, result);
144
+ const suffix = result.kind === 'copy' ? '' : ` (${result.kind} -> ${path.relative(path.dirname(dest), result.canonicalPath)})`;
145
+ console.log(` ✅ Installed skill: ${target.label}${skillName}/${suffix}`);
146
+ }
147
+ }
148
+ }
149
+
150
+ function installAgentsToTarget({ target, harnessSourceDir, packageVersion }) {
151
+ const sourceDir = path.join(harnessSourceDir, 'fable-mode', 'agents');
152
+ if (!fs.existsSync(sourceDir) || !target || !target.path || !target.manifestPath) return;
153
+
154
+ fs.mkdirSync(target.path, { recursive: true });
155
+ const trackedPaths = new Set((manifest.readManifest(target.manifestPath).agents || []).map(agent => agent.filePath));
156
+ for (const entry of fs.readdirSync(sourceDir, { withFileTypes: true })) {
157
+ if (!entry.isFile() || !entry.name.endsWith('.md')) continue;
158
+ const sourcePath = path.join(sourceDir, entry.name);
159
+ const destPath = path.join(target.path, entry.name);
160
+ if (fs.existsSync(destPath)) {
161
+ const source = fs.readFileSync(sourcePath, 'utf8');
162
+ const existing = fs.readFileSync(destPath, 'utf8');
163
+ if (source !== existing) {
164
+ console.warn(` ⚠️ Preserved existing agent without overwrite: ${destPath}`);
165
+ continue;
166
+ }
167
+ if (!trackedPaths.has(destPath)) {
168
+ console.warn(` ⚠️ Preserved pre-existing identical agent without claiming ownership: ${destPath}`);
169
+ continue;
170
+ }
171
+ } else {
172
+ fs.copyFileSync(sourcePath, destPath);
173
+ console.log(` ✅ Installed agent: ${target.label}${entry.name}`);
174
+ }
175
+ const agentInfo = parseFrontmatter(sourcePath);
176
+ const agentName = (agentInfo && agentInfo.name) || entry.name.replace(/\.md$/i, '');
177
+ manifest.recordAgentInstall(target.manifestPath, packageVersion, agentName, destPath);
178
+ }
179
+ }
180
+
181
+ function manifestTrackedSkills(manifestPath, scopeLabel) {
182
+ const data = manifest.readManifest(manifestPath);
183
+ const results = [];
184
+ for (const entry of data.skills || []) {
185
+ const linked = isLinkPath(entry.dirPath);
186
+ if (!linked && (!fs.existsSync(entry.dirPath) || !isHarnessSkillDir(entry.dirPath))) continue;
187
+ results.push({
188
+ id: entry.id,
189
+ scope: scopeLabel,
190
+ dirPath: entry.dirPath,
191
+ parentPath: path.dirname(entry.dirPath),
192
+ manifestPath,
193
+ canonicalPath: entry.canonicalPath || null,
194
+ });
195
+ }
196
+ return results;
197
+ }
198
+
199
+ function getInstalledSkills(workspaceRoot, userHome, opts = {}) {
200
+ const includeLegacy = opts.includeLegacy !== false;
201
+ const results = [];
202
+ const globalAgentsDir = path.join(userHome, '.agents');
203
+
204
+ const manifestHomes = [
205
+ { home: path.join(workspaceRoot, '.claude'), scope: 'local (Claude)' },
206
+ { home: path.join(workspaceRoot, '.cursor'), scope: 'local (Cursor)' },
207
+ { home: path.join(workspaceRoot, '.github'), scope: 'local (Copilot)' },
208
+ { home: path.join(workspaceRoot, '.codex'), scope: 'local (Codex)' },
209
+ { home: path.join(workspaceRoot, '.continue'), scope: 'local (Continue)' },
210
+ { home: path.join(workspaceRoot, '.hermes'), scope: 'local (Hermes)' },
211
+ { home: path.join(userHome, '.claude'), scope: 'global' },
212
+ { home: globalAgentsDir, scope: 'global' },
213
+ { home: path.join(userHome, '.continue'), scope: 'global' },
214
+ { home: path.join(userHome, '.hermes'), scope: 'global' },
215
+ ];
216
+ for (const { home, scope } of manifestHomes) {
217
+ results.push(...manifestTrackedSkills(manifest.getManifestPath(home), scope));
218
+ }
219
+
220
+ if (!includeLegacy) return results;
221
+
222
+ const legacyDirs = [
223
+ { path: path.join(workspaceRoot, '.harness', 'skills'), scope: 'local (legacy .harness)', manifestPath: manifest.getManifestPath(path.join(workspaceRoot, '.claude')) },
224
+ { path: path.join(workspaceRoot, '.claude', 'harness-everything', 'skills'), scope: 'local (Claude legacy erroneous path)', manifestPath: manifest.getManifestPath(path.join(workspaceRoot, '.claude')) },
225
+ { path: path.join(workspaceRoot, '.cursor', 'skills'), scope: 'local (Cursor)', manifestPath: manifest.getManifestPath(path.join(workspaceRoot, '.cursor')) },
226
+ { path: path.join(workspaceRoot, '.github', 'skills'), scope: 'local (Copilot)', manifestPath: manifest.getManifestPath(path.join(workspaceRoot, '.github')) },
227
+ { path: path.join(workspaceRoot, '.codex', 'skills'), scope: 'local (Codex legacy .codex path)', manifestPath: manifest.getManifestPath(path.join(workspaceRoot, '.codex')) },
228
+ { path: path.join(workspaceRoot, '.agents', 'skills'), scope: 'local (shared Agent Skills)', manifestPath: manifest.getManifestPath(path.join(workspaceRoot, '.codex')) },
229
+ { path: path.join(workspaceRoot, '.continue', 'skills'), scope: 'local (Continue)', manifestPath: manifest.getManifestPath(path.join(workspaceRoot, '.continue')) },
230
+ { path: path.join(globalAgentsDir, 'skills'), scope: 'global', manifestPath: manifest.getManifestPath(globalAgentsDir) },
231
+ { path: path.join(userHome, '.continue', 'skills'), scope: 'global', manifestPath: manifest.getManifestPath(path.join(userHome, '.continue')) },
232
+ { path: path.join(userHome, '.hermes', 'skills'), scope: 'global', manifestPath: manifest.getManifestPath(path.join(userHome, '.hermes')) },
233
+ ];
234
+
235
+ const known = new Set(results.flatMap(r => r.canonicalPath ? [r.dirPath, r.canonicalPath] : [r.dirPath]));
236
+ for (const item of legacyDirs) {
237
+ if (!fs.existsSync(item.path)) continue;
238
+ try {
239
+ const entries = fs.readdirSync(item.path, { withFileTypes: true });
240
+ for (const entry of entries) {
241
+ if (!entry.isDirectory()) continue;
242
+ const dirPath = path.join(item.path, entry.name);
243
+ if (known.has(dirPath)) continue;
244
+ if (isHarnessSkillDir(dirPath)) {
245
+ results.push({ id: entry.name, scope: item.scope, dirPath, parentPath: item.path, manifestPath: item.manifestPath });
246
+ known.add(dirPath);
247
+ }
248
+ }
249
+ } catch (e) {
250
+ // Ignore unreadable fallback directories.
251
+ }
252
+ }
253
+ return results;
254
+ }
255
+
256
+ function getInstalledAgents(workspaceRoot, userHome) {
257
+ const homes = [
258
+ { home: path.join(workspaceRoot, '.claude'), scope: 'local (Claude)' },
259
+ { home: path.join(userHome, '.claude'), scope: 'global (Claude)' },
260
+ ];
261
+ const results = [];
262
+ for (const { home, scope } of homes) {
263
+ const manifestPath = manifest.getManifestPath(home);
264
+ const data = manifest.readManifest(manifestPath);
265
+ for (const entry of data.agents || []) {
266
+ if (entry.filePath && fs.existsSync(entry.filePath)) results.push({ ...entry, scope, manifestPath });
267
+ }
268
+ }
269
+ return results;
270
+ }
271
+
272
+ function removeSkill(entry, ctx = {}) {
273
+ const wasLink = isLinkPath(entry.dirPath);
274
+ removePathSafely(entry.dirPath);
275
+ if (entry.manifestPath) manifest.removeSkillFromManifest(entry.manifestPath, entry.dirPath);
276
+
277
+ if (!ctx.workspaceRoot || !ctx.userHome) return;
278
+
279
+ if (wasLink && entry.canonicalPath) {
280
+ const stillInUse = getInstalledSkills(ctx.workspaceRoot, ctx.userHome, { includeLegacy: false }).some(
281
+ s => s.dirPath === entry.canonicalPath || s.canonicalPath === entry.canonicalPath
282
+ );
283
+ if (!stillInUse) removePathSafely(entry.canonicalPath);
284
+ } else if (!wasLink) {
285
+ const dependents = getInstalledSkills(ctx.workspaceRoot, ctx.userHome, { includeLegacy: false }).filter(
286
+ s => s.canonicalPath === entry.dirPath
287
+ );
288
+ for (const dep of dependents) {
289
+ removePathSafely(dep.dirPath);
290
+ if (dep.manifestPath) manifest.removeSkillFromManifest(dep.manifestPath, dep.dirPath);
291
+ }
292
+ }
293
+ }
294
+
295
+ function removeAgent(entry) {
296
+ if (entry.filePath && fs.existsSync(entry.filePath)) fs.unlinkSync(entry.filePath);
297
+ if (entry.manifestPath) manifest.removeAgentFromManifest(entry.manifestPath, entry.filePath);
298
+ }
299
+
300
+ module.exports = {
301
+ copyDir,
302
+ getAvailableSkills,
303
+ getSkillInfo,
304
+ isHarnessSkillDir,
305
+ isLinkPath,
306
+ pathPresent,
307
+ getCanonicalSkillsDir,
308
+ installSkillsToTargets,
309
+ installAgentsToTarget,
310
+ getInstalledSkills,
311
+ getInstalledAgents,
312
+ removeSkill,
313
+ removeAgent,
314
+ };