@laitszkin/apollo-toolkit 3.13.2 → 3.14.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 (154) hide show
  1. package/AGENTS.md +7 -7
  2. package/CHANGELOG.md +27 -0
  3. package/CLAUDE.md +8 -8
  4. package/analyse-app-logs/SKILL.md +3 -3
  5. package/bin/apollo-toolkit.ts +7 -0
  6. package/codex/codex-memory-manager/SKILL.md +2 -2
  7. package/codex/learn-skill-from-conversations/SKILL.md +3 -3
  8. package/dist/bin/apollo-toolkit.d.ts +2 -0
  9. package/dist/bin/apollo-toolkit.js +7 -0
  10. package/dist/lib/cli.d.ts +41 -0
  11. package/dist/lib/cli.js +655 -0
  12. package/dist/lib/installer.d.ts +59 -0
  13. package/dist/lib/installer.js +404 -0
  14. package/dist/lib/tool-runner.d.ts +19 -0
  15. package/dist/lib/tool-runner.js +536 -0
  16. package/dist/lib/tools/architecture.d.ts +2 -0
  17. package/dist/lib/tools/architecture.js +34 -0
  18. package/dist/lib/tools/create-specs.d.ts +2 -0
  19. package/dist/lib/tools/create-specs.js +175 -0
  20. package/dist/lib/tools/docs-to-voice.d.ts +2 -0
  21. package/dist/lib/tools/docs-to-voice.js +705 -0
  22. package/dist/lib/tools/enforce-video-aspect-ratio.d.ts +2 -0
  23. package/dist/lib/tools/enforce-video-aspect-ratio.js +312 -0
  24. package/dist/lib/tools/extract-conversations.d.ts +2 -0
  25. package/dist/lib/tools/extract-conversations.js +105 -0
  26. package/dist/lib/tools/extract-pdf-text.d.ts +2 -0
  27. package/dist/lib/tools/extract-pdf-text.js +92 -0
  28. package/dist/lib/tools/filter-logs.d.ts +2 -0
  29. package/dist/lib/tools/filter-logs.js +94 -0
  30. package/dist/lib/tools/find-github-issues.d.ts +2 -0
  31. package/dist/lib/tools/find-github-issues.js +176 -0
  32. package/dist/lib/tools/generate-storyboard-images.d.ts +2 -0
  33. package/dist/lib/tools/generate-storyboard-images.js +419 -0
  34. package/dist/lib/tools/log-cli-utils.d.ts +35 -0
  35. package/dist/lib/tools/log-cli-utils.js +233 -0
  36. package/dist/lib/tools/open-github-issue.d.ts +2 -0
  37. package/dist/lib/tools/open-github-issue.js +750 -0
  38. package/dist/lib/tools/read-github-issue.d.ts +2 -0
  39. package/dist/lib/tools/read-github-issue.js +134 -0
  40. package/dist/lib/tools/render-error-book.d.ts +2 -0
  41. package/dist/lib/tools/render-error-book.js +265 -0
  42. package/dist/lib/tools/render-katex.d.ts +2 -0
  43. package/dist/lib/tools/render-katex.js +294 -0
  44. package/dist/lib/tools/review-threads.d.ts +2 -0
  45. package/dist/lib/tools/review-threads.js +491 -0
  46. package/dist/lib/tools/search-logs.d.ts +2 -0
  47. package/dist/lib/tools/search-logs.js +164 -0
  48. package/dist/lib/tools/sync-memory-index.d.ts +2 -0
  49. package/dist/lib/tools/sync-memory-index.js +113 -0
  50. package/dist/lib/tools/validate-openai-agent-config.d.ts +2 -0
  51. package/dist/lib/tools/validate-openai-agent-config.js +184 -0
  52. package/dist/lib/tools/validate-skill-frontmatter.d.ts +2 -0
  53. package/dist/lib/tools/validate-skill-frontmatter.js +118 -0
  54. package/dist/lib/types.d.ts +82 -0
  55. package/dist/lib/types.js +2 -0
  56. package/dist/lib/updater.d.ts +34 -0
  57. package/dist/lib/updater.js +112 -0
  58. package/dist/lib/utils/format.d.ts +2 -0
  59. package/dist/lib/utils/format.js +6 -0
  60. package/dist/lib/utils/terminal.d.ts +12 -0
  61. package/dist/lib/utils/terminal.js +26 -0
  62. package/docs-to-voice/SKILL.md +0 -1
  63. package/generate-spec/SKILL.md +1 -1
  64. package/katex/SKILL.md +1 -2
  65. package/lib/cli.ts +780 -0
  66. package/lib/installer.ts +466 -0
  67. package/lib/tool-runner.ts +561 -0
  68. package/lib/tools/architecture.ts +34 -0
  69. package/lib/tools/create-specs.ts +204 -0
  70. package/lib/tools/docs-to-voice.ts +799 -0
  71. package/lib/tools/enforce-video-aspect-ratio.ts +368 -0
  72. package/lib/tools/extract-conversations.ts +114 -0
  73. package/lib/tools/extract-pdf-text.ts +99 -0
  74. package/lib/tools/filter-logs.ts +118 -0
  75. package/lib/tools/find-github-issues.ts +211 -0
  76. package/lib/tools/generate-storyboard-images.ts +455 -0
  77. package/lib/tools/log-cli-utils.ts +262 -0
  78. package/lib/tools/open-github-issue.ts +930 -0
  79. package/lib/tools/read-github-issue.ts +179 -0
  80. package/lib/tools/render-error-book.ts +300 -0
  81. package/lib/tools/render-katex.ts +325 -0
  82. package/lib/tools/review-threads.ts +590 -0
  83. package/lib/tools/search-logs.ts +200 -0
  84. package/lib/tools/sync-memory-index.ts +114 -0
  85. package/lib/tools/validate-openai-agent-config.ts +209 -0
  86. package/lib/tools/validate-skill-frontmatter.ts +124 -0
  87. package/lib/types.ts +90 -0
  88. package/lib/updater.ts +165 -0
  89. package/lib/utils/format.ts +7 -0
  90. package/lib/utils/terminal.ts +22 -0
  91. package/open-github-issue/SKILL.md +2 -2
  92. package/optimise-skill/SKILL.md +1 -1
  93. package/package.json +13 -4
  94. package/resources/project-architecture/assets/architecture.css +764 -0
  95. package/resources/project-architecture/assets/viewer.client.js +144 -0
  96. package/resources/project-architecture/index.html +42 -0
  97. package/review-spec-related-changes/SKILL.md +1 -1
  98. package/solve-issues-found-during-review/SKILL.md +2 -1
  99. package/tsconfig.json +28 -0
  100. package/analyse-app-logs/scripts/__pycache__/filter_logs_by_time.cpython-312.pyc +0 -0
  101. package/analyse-app-logs/scripts/__pycache__/log_cli_utils.cpython-312.pyc +0 -0
  102. package/analyse-app-logs/scripts/__pycache__/search_logs.cpython-312.pyc +0 -0
  103. package/analyse-app-logs/scripts/filter_logs_by_time.py +0 -64
  104. package/analyse-app-logs/scripts/log_cli_utils.py +0 -112
  105. package/analyse-app-logs/scripts/search_logs.py +0 -137
  106. package/analyse-app-logs/tests/test_filter_logs_by_time.py +0 -95
  107. package/analyse-app-logs/tests/test_search_logs.py +0 -100
  108. package/codex/codex-memory-manager/scripts/extract_recent_conversations.py +0 -369
  109. package/codex/codex-memory-manager/scripts/sync_memory_index.py +0 -130
  110. package/codex/codex-memory-manager/tests/test_extract_recent_conversations.py +0 -177
  111. package/codex/codex-memory-manager/tests/test_memory_template.py +0 -37
  112. package/codex/codex-memory-manager/tests/test_sync_memory_index.py +0 -84
  113. package/codex/learn-skill-from-conversations/scripts/extract_recent_conversations.py +0 -369
  114. package/codex/learn-skill-from-conversations/tests/test_extract_recent_conversations.py +0 -177
  115. package/docs-to-voice/scripts/__pycache__/docs_to_voice.cpython-312.pyc +0 -0
  116. package/docs-to-voice/scripts/docs_to_voice.py +0 -1385
  117. package/docs-to-voice/scripts/docs_to_voice.sh +0 -11
  118. package/docs-to-voice/tests/test_docs_to_voice_api_max_chars.py +0 -210
  119. package/docs-to-voice/tests/test_docs_to_voice_sentence_timeline.py +0 -115
  120. package/docs-to-voice/tests/test_docs_to_voice_settings.py +0 -43
  121. package/docs-to-voice/tests/test_docs_to_voice_shell_wrapper.py +0 -51
  122. package/docs-to-voice/tests/test_docs_to_voice_speech_rate.py +0 -57
  123. package/generate-spec/scripts/__pycache__/create-specscpython-312.pyc +0 -0
  124. package/generate-spec/scripts/create-specs +0 -215
  125. package/generate-spec/tests/test_create_specs.py +0 -200
  126. package/init-project-html/scripts/architecture-bootstrap-render.js +0 -16
  127. package/init-project-html/scripts/architecture.js +0 -296
  128. package/katex/scripts/__pycache__/render_katex.cpython-312.pyc +0 -0
  129. package/katex/scripts/render_katex.py +0 -247
  130. package/katex/scripts/render_katex.sh +0 -11
  131. package/katex/tests/test_render_katex.py +0 -174
  132. package/learning-error-book/scripts/render_error_book_json_to_pdf.py +0 -590
  133. package/learning-error-book/tests/test_render_error_book_json_to_pdf.py +0 -134
  134. package/open-github-issue/scripts/__pycache__/open_github_issue.cpython-312.pyc +0 -0
  135. package/open-github-issue/scripts/open_github_issue.py +0 -705
  136. package/open-github-issue/tests/test_open_github_issue.py +0 -381
  137. package/openai-text-to-image-storyboard/scripts/generate_storyboard_images.py +0 -763
  138. package/openai-text-to-image-storyboard/tests/test_generate_storyboard_images.py +0 -177
  139. package/read-github-issue/scripts/__pycache__/find_issues.cpython-312.pyc +0 -0
  140. package/read-github-issue/scripts/__pycache__/read_issue.cpython-312.pyc +0 -0
  141. package/read-github-issue/scripts/find_issues.py +0 -148
  142. package/read-github-issue/scripts/read_issue.py +0 -108
  143. package/read-github-issue/tests/test_find_issues.py +0 -127
  144. package/read-github-issue/tests/test_read_issue.py +0 -109
  145. package/resolve-review-comments/scripts/__pycache__/review_threads.cpython-312.pyc +0 -0
  146. package/resolve-review-comments/scripts/review_threads.py +0 -425
  147. package/resolve-review-comments/tests/test_review_threads.py +0 -74
  148. package/scripts/validate_openai_agent_config.py +0 -209
  149. package/scripts/validate_skill_frontmatter.py +0 -131
  150. package/text-to-short-video/scripts/__pycache__/enforce_video_aspect_ratio.cpython-312.pyc +0 -0
  151. package/text-to-short-video/scripts/enforce_video_aspect_ratio.py +0 -350
  152. package/text-to-short-video/tests/test_enforce_video_aspect_ratio.py +0 -194
  153. package/weekly-financial-event-report/scripts/extract_pdf_text_pdfkit.swift +0 -99
  154. package/weekly-financial-event-report/tests/test_extract_pdf_text_pdfkit.py +0 -64
@@ -0,0 +1,466 @@
1
+ import fs from 'node:fs';
2
+ import fsp from 'node:fs/promises';
3
+ import os from 'node:os';
4
+ import path from 'node:path';
5
+ import type { InstallMode, InstallTarget, ManifestData, SyncResult } from './types';
6
+
7
+ export interface TargetDefinition {
8
+ id: InstallMode;
9
+ label: string;
10
+ description: string;
11
+ }
12
+
13
+ export const TARGET_DEFINITIONS: readonly TargetDefinition[] = Object.freeze([
14
+ { id: 'codex', label: 'Codex', description: '~/.codex/skills' },
15
+ { id: 'openclaw', label: 'OpenClaw', description: '~/.openclaw/workspace*/skills' },
16
+ { id: 'trae', label: 'Trae', description: '~/.trae/skills' },
17
+ { id: 'agents', label: 'Agents', description: '~/.agents/skills' },
18
+ { id: 'claude-code', label: 'Claude Code', description: '~/.claude/skills' },
19
+ ]);
20
+
21
+ export const VALID_MODES: readonly InstallMode[] = TARGET_DEFINITIONS.map(({ id }) => id);
22
+ const COPY_FILES = new Set(['AGENTS.md', 'CHANGELOG.md', 'LICENSE', 'README.md', 'package.json']);
23
+ const COPY_DIRS = new Set<string>();
24
+ export const MANIFEST_FILENAME = '.apollo-toolkit-manifest.json';
25
+
26
+ export function resolveHomeDirectory(env: NodeJS.ProcessEnv = process.env): string {
27
+ return env.HOME || env.USERPROFILE || os.homedir();
28
+ }
29
+
30
+ export function expandUserPath(inputPath: string, env: NodeJS.ProcessEnv = process.env): string {
31
+ if (!inputPath) return inputPath;
32
+ if (inputPath === '~') return resolveHomeDirectory(env);
33
+ if (inputPath.startsWith('~/') || inputPath.startsWith('~\\')) {
34
+ return path.join(resolveHomeDirectory(env), inputPath.slice(2));
35
+ }
36
+ return inputPath;
37
+ }
38
+
39
+ export function resolveToolkitHome(env: NodeJS.ProcessEnv = process.env): string {
40
+ if (env.APOLLO_TOOLKIT_HOME) {
41
+ return path.resolve(expandUserPath(env.APOLLO_TOOLKIT_HOME, env));
42
+ }
43
+ return path.join(resolveHomeDirectory(env), '.apollo-toolkit');
44
+ }
45
+
46
+ export function normalizeModes(inputModes: string[]): InstallMode[] {
47
+ const modes: InstallMode[] = [];
48
+ for (const rawMode of inputModes) {
49
+ const mode = String(rawMode).toLowerCase();
50
+ if (mode === 'all') {
51
+ for (const candidate of VALID_MODES) {
52
+ if (!modes.includes(candidate)) {
53
+ modes.push(candidate);
54
+ }
55
+ }
56
+ continue;
57
+ }
58
+ if (!(VALID_MODES as readonly string[]).includes(mode)) {
59
+ throw new Error(`Invalid mode: ${rawMode}`);
60
+ }
61
+ if (!modes.includes(mode as InstallMode)) {
62
+ modes.push(mode as InstallMode);
63
+ }
64
+ }
65
+ return modes;
66
+ }
67
+
68
+ export async function listSkillNames(rootDir: string, modes: InstallMode[] = []): Promise<string[]> {
69
+ const entries = await fsp.readdir(rootDir, { withFileTypes: true });
70
+ const skillNames = new Set<string>();
71
+
72
+ for (const entry of entries) {
73
+ if (!entry.isDirectory()) continue;
74
+ if (fs.existsSync(path.join(rootDir, entry.name, 'SKILL.md'))) {
75
+ skillNames.add(entry.name);
76
+ }
77
+ }
78
+
79
+ if (modes.includes('codex')) {
80
+ const codexDir = path.join(rootDir, 'codex');
81
+ if (fs.existsSync(codexDir)) {
82
+ const codexEntries = await fsp.readdir(codexDir, { withFileTypes: true });
83
+ for (const entry of codexEntries) {
84
+ if (entry.isDirectory() && fs.existsSync(path.join(codexDir, entry.name, 'SKILL.md'))) {
85
+ skillNames.add(entry.name);
86
+ }
87
+ }
88
+ }
89
+ }
90
+
91
+ return [...skillNames].sort();
92
+ }
93
+
94
+ export async function listCodexSkillNames(rootDir: string): Promise<string[]> {
95
+ const codexDir = path.join(rootDir, 'codex');
96
+ if (!fs.existsSync(codexDir)) return [];
97
+
98
+ const entries = await fsp.readdir(codexDir, { withFileTypes: true });
99
+ return entries
100
+ .filter((entry) => entry.isDirectory() && fs.existsSync(path.join(codexDir, entry.name, 'SKILL.md')))
101
+ .map((entry) => entry.name)
102
+ .sort();
103
+ }
104
+
105
+ export async function readManifest(targetRoot: string): Promise<ManifestData | null> {
106
+ const manifestPath = path.join(targetRoot, MANIFEST_FILENAME);
107
+ try {
108
+ const raw = await fsp.readFile(manifestPath, 'utf8');
109
+ return JSON.parse(raw);
110
+ } catch {
111
+ return null;
112
+ }
113
+ }
114
+
115
+ function isSafeSkillName(skillName: string): boolean {
116
+ return typeof skillName === 'string'
117
+ && skillName.length > 0
118
+ && !skillName.includes('\0')
119
+ && !skillName.includes('/')
120
+ && !skillName.includes('\\')
121
+ && !path.isAbsolute(skillName)
122
+ && skillName !== '.'
123
+ && skillName !== '..';
124
+ }
125
+
126
+ function getManifestSkillNames(manifest: ManifestData): string[] {
127
+ return [...new Set([
128
+ ...(Array.isArray(manifest.historicalSkills) ? manifest.historicalSkills : []),
129
+ ...(Array.isArray(manifest.skills) ? manifest.skills : []),
130
+ ])].filter(isSafeSkillName).sort();
131
+ }
132
+
133
+ export async function writeManifest(
134
+ targetRoot: string,
135
+ { version, linkMode, skills, previousSkills = [] }: { version: string; linkMode: string; skills: string[]; previousSkills?: string[] },
136
+ ): Promise<void> {
137
+ const historicalSkills = [...new Set([...previousSkills, ...skills])].sort();
138
+ const manifest: ManifestData = {
139
+ version,
140
+ installedAt: new Date().toISOString(),
141
+ linkMode,
142
+ skills: [...skills].sort(),
143
+ historicalSkills,
144
+ };
145
+ await fsp.mkdir(targetRoot, { recursive: true });
146
+ await fsp.writeFile(
147
+ path.join(targetRoot, MANIFEST_FILENAME),
148
+ `${JSON.stringify(manifest, null, 2)}\n`,
149
+ 'utf8',
150
+ );
151
+ }
152
+
153
+ export async function listAllKnownSkillNames({ toolkitHome, modes = [], env = process.env }: { toolkitHome: string; modes?: InstallMode[]; env?: NodeJS.ProcessEnv }): Promise<string[]> {
154
+ const allNames = new Set<string>();
155
+ const currentSkills = await listSkillNames(toolkitHome, modes).catch(() => []);
156
+ for (const name of currentSkills) allNames.add(name);
157
+
158
+ const targets = await getUninstallTargetRoots(modes, env);
159
+ for (const target of targets) {
160
+ if (!target.root) continue;
161
+ const manifest = await readManifest(target.root);
162
+ if (manifest && manifest.historicalSkills) {
163
+ for (const name of getManifestSkillNames(manifest)) {
164
+ allNames.add(name);
165
+ }
166
+ }
167
+ }
168
+ return [...allNames].sort();
169
+ }
170
+
171
+ function getTargetSkillNames({ targetMode, sharedSkillNames, codexSkillNames, includeExclusiveSkills = false }: {
172
+ targetMode: string;
173
+ sharedSkillNames: string[];
174
+ codexSkillNames: string[];
175
+ includeExclusiveSkills?: boolean;
176
+ }): string[] {
177
+ const includeCodexSkills = targetMode === 'codex' || includeExclusiveSkills;
178
+ if (!includeCodexSkills || codexSkillNames.length === 0) return sharedSkillNames;
179
+ return [...new Set([...sharedSkillNames, ...codexSkillNames])].sort();
180
+ }
181
+
182
+ function resolveInstallSourcePath({ toolkitHome, targetMode, skillName, codexSkillNames }: {
183
+ toolkitHome: string;
184
+ targetMode: string;
185
+ skillName: string;
186
+ codexSkillNames: string[];
187
+ }): string {
188
+ if (targetMode === 'codex' && codexSkillNames.includes(skillName)) {
189
+ return path.join(toolkitHome, 'codex', skillName);
190
+ }
191
+ return path.join(toolkitHome, skillName);
192
+ }
193
+
194
+ function shouldCopyEntry(sourceRoot: string, entry: fs.Dirent): boolean {
195
+ if (entry.isFile()) return COPY_FILES.has(entry.name);
196
+ if (!entry.isDirectory()) return false;
197
+ if (COPY_DIRS.has(entry.name)) return true;
198
+ return fs.existsSync(path.join(sourceRoot, entry.name, 'SKILL.md'));
199
+ }
200
+
201
+ function shouldCopyCodexContainer({ sourceRoot, entry, modes = [] }: {
202
+ sourceRoot: string;
203
+ entry: fs.Dirent;
204
+ modes?: InstallMode[];
205
+ }): boolean {
206
+ if (entry.name !== 'codex' || !entry.isDirectory() || !modes.includes('codex')) return false;
207
+ const codexDir = path.join(sourceRoot, entry.name);
208
+ if (!fs.existsSync(codexDir)) return false;
209
+ const childNames = fs.readdirSync(codexDir);
210
+ return childNames.some((childName) => fs.existsSync(path.join(codexDir, childName, 'SKILL.md')));
211
+ }
212
+
213
+ async function stageToolkitContents({ sourceRoot, destinationRoot, version, modes = [] }: {
214
+ sourceRoot: string;
215
+ destinationRoot: string;
216
+ version: string;
217
+ modes?: InstallMode[];
218
+ }): Promise<string[]> {
219
+ const entries = await fsp.readdir(sourceRoot, { withFileTypes: true });
220
+ const copiedEntries: string[] = [];
221
+ await fsp.mkdir(destinationRoot, { recursive: true });
222
+
223
+ for (const entry of entries) {
224
+ if (!shouldCopyEntry(sourceRoot, entry) && !shouldCopyCodexContainer({ sourceRoot, entry, modes })) continue;
225
+ const sourcePath = path.join(sourceRoot, entry.name);
226
+ const destinationPath = path.join(destinationRoot, entry.name);
227
+ await fsp.cp(sourcePath, destinationPath, { recursive: true, force: true });
228
+ copiedEntries.push(entry.name);
229
+ }
230
+
231
+ const metadata = { version, installedAt: new Date().toISOString(), source: 'npm-package' };
232
+ await fsp.writeFile(
233
+ path.join(destinationRoot, '.apollo-toolkit-install.json'),
234
+ `${JSON.stringify(metadata, null, 2)}\n`,
235
+ 'utf8',
236
+ );
237
+ return copiedEntries.sort();
238
+ }
239
+
240
+ export async function syncToolkitHome({ sourceRoot, toolkitHome, version, modes = [] }: {
241
+ sourceRoot: string;
242
+ toolkitHome: string;
243
+ version: string;
244
+ modes?: InstallMode[];
245
+ }): Promise<SyncResult & { toolkitHome: string; skillNames: string[] }> {
246
+ const parentDir = path.dirname(toolkitHome);
247
+ const tempDir = path.join(parentDir, `.apollo-toolkit.tmp-${process.pid}-${Date.now()}`);
248
+ const previousSkillNames = await listSkillNames(toolkitHome, modes).catch(() => []);
249
+
250
+ await fsp.rm(tempDir, { recursive: true, force: true });
251
+ await stageToolkitContents({ sourceRoot, destinationRoot: tempDir, version, modes });
252
+
253
+ const stat = await fsp.lstat(toolkitHome).catch(() => null);
254
+ if (stat && !stat.isDirectory()) {
255
+ throw new Error(`Apollo Toolkit home exists but is not a directory: ${toolkitHome}`);
256
+ }
257
+
258
+ await fsp.rm(toolkitHome, { recursive: true, force: true });
259
+ await fsp.mkdir(parentDir, { recursive: true });
260
+ await fsp.rename(tempDir, toolkitHome);
261
+
262
+ return {
263
+ toolkitHome,
264
+ previousSkillNames,
265
+ skillNames: await listSkillNames(toolkitHome, modes),
266
+ };
267
+ }
268
+
269
+ export async function getTargetRoots(modes: string[], env: NodeJS.ProcessEnv = process.env): Promise<InstallTarget[]> {
270
+ const homeDir = resolveHomeDirectory(env);
271
+ const targets: InstallTarget[] = [];
272
+
273
+ for (const mode of normalizeModes(modes)) {
274
+ if (mode === 'codex') {
275
+ targets.push({
276
+ id: mode,
277
+ label: 'Codex',
278
+ root: env.CODEX_SKILLS_DIR
279
+ ? path.resolve(expandUserPath(env.CODEX_SKILLS_DIR, env))
280
+ : path.join(homeDir, '.codex', 'skills'),
281
+ });
282
+ continue;
283
+ }
284
+ if (mode === 'trae') {
285
+ targets.push({
286
+ id: mode,
287
+ label: 'Trae',
288
+ root: env.TRAE_SKILLS_DIR
289
+ ? path.resolve(expandUserPath(env.TRAE_SKILLS_DIR, env))
290
+ : path.join(homeDir, '.trae', 'skills'),
291
+ });
292
+ continue;
293
+ }
294
+ if (mode === 'agents') {
295
+ targets.push({
296
+ id: mode,
297
+ label: 'Agents',
298
+ root: env.AGENTS_SKILLS_DIR
299
+ ? path.resolve(expandUserPath(env.AGENTS_SKILLS_DIR, env))
300
+ : path.join(homeDir, '.agents', 'skills'),
301
+ });
302
+ continue;
303
+ }
304
+ if (mode === 'openclaw') {
305
+ const openclawHome = env.OPENCLAW_HOME
306
+ ? path.resolve(expandUserPath(env.OPENCLAW_HOME, env))
307
+ : path.join(homeDir, '.openclaw');
308
+ const entries = await fsp.readdir(openclawHome, { withFileTypes: true }).catch(() => []);
309
+ const workspaceNames = entries
310
+ .filter((entry) => entry.isDirectory() && entry.name.startsWith('workspace'))
311
+ .map((entry) => entry.name)
312
+ .sort();
313
+ if (workspaceNames.length === 0) {
314
+ throw new Error(`No workspace directories found under: ${openclawHome}`);
315
+ }
316
+ for (const workspaceName of workspaceNames) {
317
+ targets.push({
318
+ id: mode,
319
+ label: `OpenClaw (${workspaceName})`,
320
+ root: path.join(openclawHome, workspaceName, 'skills'),
321
+ });
322
+ }
323
+ continue;
324
+ }
325
+ if (mode === 'claude-code') {
326
+ targets.push({
327
+ id: mode,
328
+ label: 'Claude Code',
329
+ root: env.CLAUDE_CODE_SKILLS_DIR
330
+ ? path.resolve(expandUserPath(env.CLAUDE_CODE_SKILLS_DIR, env))
331
+ : path.join(homeDir, '.claude', 'skills'),
332
+ });
333
+ continue;
334
+ }
335
+ }
336
+ return targets;
337
+ }
338
+
339
+ export async function getUninstallTargetRoots(modes: string[] = [...VALID_MODES], env: NodeJS.ProcessEnv = process.env): Promise<InstallTarget[]> {
340
+ const targets: InstallTarget[] = [];
341
+ for (const mode of normalizeModes(modes)) {
342
+ try {
343
+ targets.push(...await getTargetRoots([mode], env));
344
+ } catch {
345
+ // Uninstall is best-effort across agents
346
+ }
347
+ }
348
+ return targets;
349
+ }
350
+
351
+ async function ensureDirectory(dirPath: string): Promise<void> {
352
+ await fsp.mkdir(dirPath, { recursive: true });
353
+ }
354
+
355
+ async function replaceWithCopy(sourcePath: string, targetPath: string): Promise<void> {
356
+ await fsp.rm(targetPath, { recursive: true, force: true });
357
+ await ensureDirectory(path.dirname(targetPath));
358
+ await fsp.cp(sourcePath, targetPath, { recursive: true, force: true });
359
+ }
360
+
361
+ async function replaceWithSymlink(sourcePath: string, targetPath: string): Promise<void> {
362
+ await fsp.rm(targetPath, { recursive: true, force: true });
363
+ await ensureDirectory(path.dirname(targetPath));
364
+ await fsp.symlink(sourcePath, targetPath, process.platform === 'win32' ? 'junction' : 'dir');
365
+ }
366
+
367
+ export async function installLinks({ toolkitHome, modes, env = process.env, previousSkillNames = [], linkMode = 'copy', includeExclusiveSkills = false }: {
368
+ toolkitHome: string;
369
+ modes: InstallMode[];
370
+ env?: NodeJS.ProcessEnv;
371
+ previousSkillNames?: string[];
372
+ linkMode?: 'copy' | 'symlink';
373
+ includeExclusiveSkills?: boolean;
374
+ }): Promise<{ skillNames: string[]; targets: InstallTarget[]; copiedPaths: any[]; linkMode: string }> {
375
+ const normalizedModes = normalizeModes(modes);
376
+ const codexSkillNames = (normalizedModes.includes('codex') || includeExclusiveSkills)
377
+ ? await listCodexSkillNames(toolkitHome)
378
+ : [];
379
+ const sharedSkillNames = await listSkillNames(toolkitHome);
380
+ const skillNames = normalizedModes.includes('codex')
381
+ ? [...new Set([...sharedSkillNames, ...codexSkillNames])].sort()
382
+ : sharedSkillNames;
383
+ const targets = await getTargetRoots(normalizedModes, env);
384
+ const copiedPaths: any[] = [];
385
+
386
+ for (const target of targets) {
387
+ const targetSkillNames = getTargetSkillNames({
388
+ targetMode: target.id,
389
+ sharedSkillNames,
390
+ codexSkillNames,
391
+ includeExclusiveSkills,
392
+ });
393
+
394
+ const existingManifest = await readManifest(target.root!);
395
+ const allPreviousSkills = existingManifest
396
+ ? [...new Set([...getManifestSkillNames(existingManifest), ...previousSkillNames.filter(isSafeSkillName)])]
397
+ : previousSkillNames.filter(isSafeSkillName);
398
+
399
+ const staleSkillNames = allPreviousSkills.filter(
400
+ (skillName) => !targetSkillNames.includes(skillName),
401
+ );
402
+
403
+ await ensureDirectory(target.root!);
404
+ for (const staleSkillName of staleSkillNames) {
405
+ await fsp.rm(path.join(target.root!, staleSkillName), { recursive: true, force: true });
406
+ }
407
+ for (const skillName of targetSkillNames) {
408
+ const sourcePath = resolveInstallSourcePath({
409
+ toolkitHome,
410
+ targetMode: target.id,
411
+ skillName,
412
+ codexSkillNames,
413
+ });
414
+ const targetPath = path.join(target.root!, skillName);
415
+
416
+ if (linkMode === 'symlink') {
417
+ await replaceWithSymlink(sourcePath, targetPath);
418
+ } else {
419
+ await replaceWithCopy(sourcePath, targetPath);
420
+ }
421
+ copiedPaths.push({ target: target.label, path: targetPath, skillName, linkMode });
422
+ }
423
+
424
+ await writeManifest(target.root!, {
425
+ version: existingManifest?.version || 'unknown',
426
+ linkMode,
427
+ skills: targetSkillNames,
428
+ previousSkills: allPreviousSkills,
429
+ });
430
+ }
431
+
432
+ return { skillNames, targets, copiedPaths, linkMode };
433
+ }
434
+
435
+ export async function uninstallSkills({ env = process.env, modes = null }: { env?: NodeJS.ProcessEnv; modes?: InstallMode[] | null } = {}): Promise<{ target: string; root: string; removedSkills: string[] }[]> {
436
+ const normalizedModes = modes ? normalizeModes(modes) : [...VALID_MODES];
437
+ const targets = await getUninstallTargetRoots(normalizedModes, env);
438
+ const results: { target: string; root: string; removedSkills: string[] }[] = [];
439
+
440
+ for (const target of targets) {
441
+ const manifest = await readManifest(target.root!);
442
+ if (!manifest) continue;
443
+
444
+ const skillNames = getManifestSkillNames(manifest);
445
+ const removedSkills: string[] = [];
446
+ for (const skillName of skillNames) {
447
+ const skillPath = path.join(target.root!, skillName);
448
+ try {
449
+ await fsp.rm(skillPath, { recursive: true, force: true });
450
+ removedSkills.push(skillName);
451
+ } catch {
452
+ // skip
453
+ }
454
+ }
455
+
456
+ try {
457
+ await fsp.rm(path.join(target.root!, MANIFEST_FILENAME), { force: true });
458
+ } catch {
459
+ // ok
460
+ }
461
+
462
+ results.push({ target: target.label, root: target.root!, removedSkills });
463
+ }
464
+
465
+ return results;
466
+ }