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,338 @@
1
+ import { execFileSync } from 'node:child_process';
2
+ import fs from 'node:fs';
3
+ import os from 'node:os';
4
+ import path from 'node:path';
5
+ import { fileURLToPath } from 'node:url';
6
+ import { theme } from '../ui/theme.js';
7
+ const SKILLS_ENV = 'ICOPILOT_SKILLS_PATH';
8
+ const SKILL_USAGE = [
9
+ theme.brand('Skill command'),
10
+ ` ${theme.hl('/skill list')} ${theme.dim('show saved skills')}`,
11
+ ` ${theme.hl('/skill add <path-or-url>')} ${theme.dim('add a skill from a file, directory, or URL')}`,
12
+ ` ${theme.hl('/skill remove <name>')} ${theme.dim('remove a skill')}`,
13
+ ` ${theme.hl('/skill activate <name>')} ${theme.dim('activate a skill')}`,
14
+ ` ${theme.hl('/skill deactivate <name>')} ${theme.dim('deactivate a skill')}`,
15
+ ].join('\n');
16
+ export function loadSkills() {
17
+ return loadSkillStore().skills;
18
+ }
19
+ export function saveSkills(skills) {
20
+ const store = loadSkillStore();
21
+ const next = {
22
+ configPath: store.configPath,
23
+ skills: [...skills].filter(isSkill).sort((left, right) => left.name.localeCompare(right.name)),
24
+ };
25
+ fs.mkdirSync(path.dirname(next.configPath), { recursive: true });
26
+ fs.writeFileSync(next.configPath, `${JSON.stringify(next, null, 2)}\n`, 'utf8');
27
+ }
28
+ export function addSkill(source) {
29
+ const raw = source.trim();
30
+ if (!raw)
31
+ return { error: 'usage: /skill add <path-or-url>' };
32
+ try {
33
+ const current = loadSkills();
34
+ const resolved = resolveSkillSource(raw);
35
+ const content = readResolvedSourceContent(resolved);
36
+ const name = uniqueSkillName(skillNameFromSource(resolved.path), current);
37
+ const duplicate = current.find((skill) => skill.path === resolved.path ||
38
+ skill.name.localeCompare(name, undefined, { sensitivity: 'accent' }) === 0);
39
+ if (duplicate) {
40
+ return { error: `skill already exists: ${duplicate.name}` };
41
+ }
42
+ const skill = {
43
+ name,
44
+ description: skillDescriptionFromContent(content, resolved),
45
+ source: resolved.source,
46
+ path: resolved.path,
47
+ active: true,
48
+ };
49
+ saveSkills([...current, skill]);
50
+ return skill;
51
+ }
52
+ catch (error) {
53
+ return { error: error.message };
54
+ }
55
+ }
56
+ export function removeSkill(name) {
57
+ const target = name.trim();
58
+ if (!target)
59
+ return false;
60
+ const current = loadSkills();
61
+ const next = current.filter((skill) => skill.name.localeCompare(target, undefined, { sensitivity: 'accent' }) !== 0);
62
+ if (next.length === current.length)
63
+ return false;
64
+ saveSkills(next);
65
+ return true;
66
+ }
67
+ export function listSkills() {
68
+ const skills = loadSkills();
69
+ if (skills.length === 0)
70
+ return theme.dim('No skills saved.\n');
71
+ const lines = skills.map((skill) => {
72
+ const status = skill.active ? theme.ok('active') : theme.dim('inactive');
73
+ return ` ${theme.hl(skill.name)} [${status}] ${theme.dim(skill.source)} ${theme.dim('—')} ${skill.description}`;
74
+ });
75
+ return `${theme.brand('Skills')}\n${lines.join('\n')}\n`;
76
+ }
77
+ export function activateSkill(name) {
78
+ return setSkillActiveState(name, true);
79
+ }
80
+ export function deactivateSkill(name) {
81
+ return setSkillActiveState(name, false);
82
+ }
83
+ export function getActiveSkillContext() {
84
+ const active = loadSkills().filter((skill) => skill.active);
85
+ if (active.length === 0)
86
+ return '';
87
+ const sections = [];
88
+ for (const skill of active) {
89
+ try {
90
+ const content = readSkillContent(skill).trim();
91
+ if (!content)
92
+ continue;
93
+ sections.push(`## Skill: ${skill.name}\n${content}`);
94
+ }
95
+ catch {
96
+ continue;
97
+ }
98
+ }
99
+ return sections.join('\n\n');
100
+ }
101
+ export function skillCommand(args) {
102
+ const [subcommandRaw, ...rest] = args;
103
+ const subcommand = (subcommandRaw || '').toLowerCase();
104
+ switch (subcommand) {
105
+ case '':
106
+ return `${SKILL_USAGE}\n`;
107
+ case 'list':
108
+ return listSkills();
109
+ case 'add': {
110
+ const source = rest.join(' ').trim();
111
+ if (!source)
112
+ return theme.warn('usage: /skill add <path-or-url>\n');
113
+ const result = addSkill(source);
114
+ if ('error' in result)
115
+ return theme.err(`skill: ${result.error}\n`);
116
+ return theme.ok(`✔ added skill ${result.name} (${result.source})\n`);
117
+ }
118
+ case 'remove':
119
+ case 'delete':
120
+ case 'rm': {
121
+ const [name] = rest;
122
+ if (!name)
123
+ return theme.warn('usage: /skill remove <name>\n');
124
+ return removeSkill(name)
125
+ ? theme.ok(`✔ removed skill ${name}\n`)
126
+ : theme.warn(`skill not found: ${name}\n`);
127
+ }
128
+ case 'activate': {
129
+ const [name] = rest;
130
+ if (!name)
131
+ return theme.warn('usage: /skill activate <name>\n');
132
+ return activateSkill(name)
133
+ ? theme.ok(`✔ activated skill ${name}\n`)
134
+ : theme.warn(`skill not found: ${name}\n`);
135
+ }
136
+ case 'deactivate': {
137
+ const [name] = rest;
138
+ if (!name)
139
+ return theme.warn('usage: /skill deactivate <name>\n');
140
+ return deactivateSkill(name)
141
+ ? theme.ok(`✔ deactivated skill ${name}\n`)
142
+ : theme.warn(`skill not found: ${name}\n`);
143
+ }
144
+ default:
145
+ return theme.warn('usage: /skill [list|add|remove|activate|deactivate]\n');
146
+ }
147
+ }
148
+ function loadSkillStore() {
149
+ const configPath = skillsPath();
150
+ if (!fs.existsSync(configPath)) {
151
+ return { skills: [], configPath };
152
+ }
153
+ try {
154
+ const parsed = JSON.parse(fs.readFileSync(configPath, 'utf8'));
155
+ if (Array.isArray(parsed)) {
156
+ return {
157
+ skills: parsed.filter(isSkill).sort((left, right) => left.name.localeCompare(right.name)),
158
+ configPath,
159
+ };
160
+ }
161
+ if (parsed &&
162
+ typeof parsed === 'object' &&
163
+ Array.isArray(parsed.skills)) {
164
+ return {
165
+ skills: parsed.skills
166
+ .filter(isSkill)
167
+ .sort((left, right) => left.name.localeCompare(right.name)),
168
+ configPath,
169
+ };
170
+ }
171
+ }
172
+ catch {
173
+ return { skills: [], configPath };
174
+ }
175
+ return { skills: [], configPath };
176
+ }
177
+ function skillsPath() {
178
+ return process.env[SKILLS_ENV] || path.join(os.homedir(), '.icopilot', 'skills.json');
179
+ }
180
+ function setSkillActiveState(name, active) {
181
+ const target = name.trim();
182
+ if (!target)
183
+ return false;
184
+ const current = loadSkills();
185
+ let changed = false;
186
+ const next = current.map((skill) => {
187
+ if (skill.name.localeCompare(target, undefined, { sensitivity: 'accent' }) !== 0)
188
+ return skill;
189
+ changed = true;
190
+ return { ...skill, active };
191
+ });
192
+ if (!changed)
193
+ return false;
194
+ saveSkills(next);
195
+ return true;
196
+ }
197
+ function resolveSkillSource(source) {
198
+ if (isLikelyUrl(source)) {
199
+ return { source: 'url', path: normalizeUrl(source) };
200
+ }
201
+ const resolvedPath = path.resolve(source);
202
+ if (!fs.existsSync(resolvedPath)) {
203
+ throw new Error(`skill source not found: ${source}`);
204
+ }
205
+ const stats = fs.statSync(resolvedPath);
206
+ if (stats.isDirectory())
207
+ return { source: 'directory', path: resolvedPath };
208
+ if (stats.isFile())
209
+ return { source: 'file', path: resolvedPath };
210
+ throw new Error(`unsupported skill source: ${source}`);
211
+ }
212
+ function normalizeUrl(source) {
213
+ const url = new URL(source);
214
+ if (url.protocol === 'http:' || url.protocol === 'https:' || url.protocol === 'file:') {
215
+ return url.toString();
216
+ }
217
+ throw new Error(`unsupported URL protocol: ${url.protocol}`);
218
+ }
219
+ function isLikelyUrl(source) {
220
+ if (/^[a-zA-Z]:[\\/]/.test(source) || source.startsWith('\\\\')) {
221
+ return false;
222
+ }
223
+ try {
224
+ const url = new URL(source);
225
+ return url.protocol === 'http:' || url.protocol === 'https:' || url.protocol === 'file:';
226
+ }
227
+ catch {
228
+ return false;
229
+ }
230
+ }
231
+ function readSkillContent(skill) {
232
+ return readResolvedSourceContent({ source: skill.source, path: skill.path });
233
+ }
234
+ function readResolvedSourceContent(source) {
235
+ switch (source.source) {
236
+ case 'file':
237
+ return readTextFile(source.path);
238
+ case 'directory':
239
+ return readDirectory(source.path);
240
+ case 'url':
241
+ return readUrl(source.path);
242
+ }
243
+ }
244
+ function readUrl(urlValue) {
245
+ const url = new URL(urlValue);
246
+ if (url.protocol === 'file:') {
247
+ return readTextFile(fileURLToPath(url));
248
+ }
249
+ return execFileSync('curl', ['-fsSL', urlValue], {
250
+ encoding: 'utf8',
251
+ maxBuffer: 10 * 1024 * 1024,
252
+ }).trim();
253
+ }
254
+ function readDirectory(dirPath) {
255
+ const files = listFilesRecursively(dirPath);
256
+ const sections = files
257
+ .map((filePath) => {
258
+ const content = readTextFile(filePath).trim();
259
+ if (!content)
260
+ return null;
261
+ const relative = path.relative(dirPath, filePath) || path.basename(filePath);
262
+ return `### File: ${relative}\n${content}`;
263
+ })
264
+ .filter((section) => Boolean(section));
265
+ return sections.join('\n\n');
266
+ }
267
+ function listFilesRecursively(dirPath) {
268
+ const files = [];
269
+ const entries = fs.readdirSync(dirPath, { withFileTypes: true });
270
+ for (const entry of entries.sort((left, right) => left.name.localeCompare(right.name))) {
271
+ const entryPath = path.join(dirPath, entry.name);
272
+ if (entry.isDirectory()) {
273
+ files.push(...listFilesRecursively(entryPath));
274
+ continue;
275
+ }
276
+ if (entry.isFile())
277
+ files.push(entryPath);
278
+ }
279
+ return files;
280
+ }
281
+ function readTextFile(filePath) {
282
+ const buffer = fs.readFileSync(filePath);
283
+ if (buffer.includes(0))
284
+ return '';
285
+ return buffer.toString('utf8').trim();
286
+ }
287
+ function skillNameFromSource(source) {
288
+ if (isLikelyUrl(source)) {
289
+ const url = new URL(source);
290
+ if (url.protocol === 'file:') {
291
+ return basenameWithoutExt(fileURLToPath(url));
292
+ }
293
+ return basenameWithoutExt(url.pathname) || sanitizeName(url.hostname);
294
+ }
295
+ return basenameWithoutExt(source);
296
+ }
297
+ function uniqueSkillName(name, skills) {
298
+ const base = sanitizeName(name || 'skill');
299
+ let candidate = base;
300
+ let counter = 2;
301
+ while (skills.some((skill) => skill.name.localeCompare(candidate, undefined, { sensitivity: 'accent' }) === 0)) {
302
+ candidate = `${base}-${counter}`;
303
+ counter += 1;
304
+ }
305
+ return candidate;
306
+ }
307
+ function basenameWithoutExt(value) {
308
+ const parsed = path.parse(value);
309
+ return sanitizeName(parsed.name || parsed.base || 'skill');
310
+ }
311
+ function sanitizeName(value) {
312
+ const normalized = value.trim().replace(/\.[^.]+$/, '');
313
+ const cleaned = normalized.replace(/[^a-z0-9_-]+/gi, '-').replace(/^-+|-+$/g, '');
314
+ return cleaned || 'skill';
315
+ }
316
+ function skillDescriptionFromContent(content, source) {
317
+ const firstMeaningful = content
318
+ .split(/\r?\n/)
319
+ .map((line) => line.trim())
320
+ .find((line) => line.length > 0);
321
+ if (firstMeaningful) {
322
+ return firstMeaningful.replace(/^#{1,6}\s*/, '').slice(0, 120);
323
+ }
324
+ return source.source === 'directory'
325
+ ? `Directory skill from ${source.path}`
326
+ : `Skill from ${source.path}`;
327
+ }
328
+ function isSkill(value) {
329
+ if (!value || typeof value !== 'object')
330
+ return false;
331
+ const skill = value;
332
+ return (typeof skill.name === 'string' &&
333
+ typeof skill.description === 'string' &&
334
+ (skill.source === 'file' || skill.source === 'url' || skill.source === 'directory') &&
335
+ typeof skill.path === 'string' &&
336
+ typeof skill.active === 'boolean' &&
337
+ skill.name.trim().length > 0);
338
+ }