neuro-cli 4.1.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 (175) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +510 -0
  3. package/dist/advisor/advisor.d.ts +50 -0
  4. package/dist/advisor/advisor.js +178 -0
  5. package/dist/agents/base.d.ts +62 -0
  6. package/dist/agents/base.js +215 -0
  7. package/dist/agents/orchestrator.d.ts +46 -0
  8. package/dist/agents/orchestrator.js +192 -0
  9. package/dist/agents/team.d.ts +51 -0
  10. package/dist/agents/team.js +210 -0
  11. package/dist/api/models.d.ts +23 -0
  12. package/dist/api/models.js +514 -0
  13. package/dist/api/ollama.d.ts +153 -0
  14. package/dist/api/ollama.js +751 -0
  15. package/dist/api/openrouter.d.ts +55 -0
  16. package/dist/api/openrouter.js +223 -0
  17. package/dist/commands/commands.d.ts +43 -0
  18. package/dist/commands/commands.js +308 -0
  19. package/dist/config/config.d.ts +8 -0
  20. package/dist/config/config.js +311 -0
  21. package/dist/context/compaction.d.ts +54 -0
  22. package/dist/context/compaction.js +251 -0
  23. package/dist/context/custom-agents.d.ts +107 -0
  24. package/dist/context/custom-agents.js +397 -0
  25. package/dist/context/custom-tools.d.ts +120 -0
  26. package/dist/context/custom-tools.js +564 -0
  27. package/dist/context/git-checkpoint.d.ts +52 -0
  28. package/dist/context/git-checkpoint.js +240 -0
  29. package/dist/context/neuro-md.d.ts +48 -0
  30. package/dist/context/neuro-md.js +202 -0
  31. package/dist/context/neuroignore.d.ts +102 -0
  32. package/dist/context/neuroignore.js +441 -0
  33. package/dist/context/repo-map.d.ts +38 -0
  34. package/dist/context/repo-map.js +220 -0
  35. package/dist/context/skill-standard.d.ts +262 -0
  36. package/dist/context/skill-standard.js +1156 -0
  37. package/dist/context/skill-system.d.ts +75 -0
  38. package/dist/context/skill-system.js +578 -0
  39. package/dist/context/tree-sitter.d.ts +99 -0
  40. package/dist/context/tree-sitter.js +1956 -0
  41. package/dist/core/acp.d.ts +325 -0
  42. package/dist/core/acp.js +1498 -0
  43. package/dist/core/api-server.d.ts +143 -0
  44. package/dist/core/api-server.js +550 -0
  45. package/dist/core/approval.d.ts +81 -0
  46. package/dist/core/approval.js +432 -0
  47. package/dist/core/auto-compact.d.ts +127 -0
  48. package/dist/core/auto-compact.js +436 -0
  49. package/dist/core/auto-mode.d.ts +232 -0
  50. package/dist/core/auto-mode.js +831 -0
  51. package/dist/core/background-session.d.ts +166 -0
  52. package/dist/core/background-session.js +696 -0
  53. package/dist/core/cicd.d.ts +257 -0
  54. package/dist/core/cicd.js +1443 -0
  55. package/dist/core/cloud-sync.d.ts +156 -0
  56. package/dist/core/cloud-sync.js +582 -0
  57. package/dist/core/code-review.d.ts +132 -0
  58. package/dist/core/code-review.js +1191 -0
  59. package/dist/core/completion.d.ts +49 -0
  60. package/dist/core/completion.js +384 -0
  61. package/dist/core/context.d.ts +38 -0
  62. package/dist/core/context.js +144 -0
  63. package/dist/core/diff-preview.d.ts +35 -0
  64. package/dist/core/diff-preview.js +173 -0
  65. package/dist/core/doom-loop.d.ts +51 -0
  66. package/dist/core/doom-loop.js +179 -0
  67. package/dist/core/engine.d.ts +183 -0
  68. package/dist/core/engine.js +942 -0
  69. package/dist/core/extended-thinking.d.ts +103 -0
  70. package/dist/core/extended-thinking.js +269 -0
  71. package/dist/core/fallback.d.ts +54 -0
  72. package/dist/core/fallback.js +104 -0
  73. package/dist/core/git-worktree.d.ts +139 -0
  74. package/dist/core/git-worktree.js +614 -0
  75. package/dist/core/headless.d.ts +30 -0
  76. package/dist/core/headless.js +101 -0
  77. package/dist/core/i18n.d.ts +83 -0
  78. package/dist/core/i18n.js +739 -0
  79. package/dist/core/linting.d.ts +129 -0
  80. package/dist/core/linting.js +699 -0
  81. package/dist/core/model-router.d.ts +109 -0
  82. package/dist/core/model-router.js +581 -0
  83. package/dist/core/multi-model.d.ts +243 -0
  84. package/dist/core/multi-model.js +1099 -0
  85. package/dist/core/multi-session.d.ts +144 -0
  86. package/dist/core/multi-session.js +442 -0
  87. package/dist/core/multimodal.d.ts +125 -0
  88. package/dist/core/multimodal.js +286 -0
  89. package/dist/core/observability.d.ts +93 -0
  90. package/dist/core/observability.js +737 -0
  91. package/dist/core/os-sandbox.d.ts +122 -0
  92. package/dist/core/os-sandbox.js +1193 -0
  93. package/dist/core/outcome-grading.d.ts +228 -0
  94. package/dist/core/outcome-grading.js +1123 -0
  95. package/dist/core/output-styles.d.ts +57 -0
  96. package/dist/core/output-styles.js +382 -0
  97. package/dist/core/parallel-agents.d.ts +183 -0
  98. package/dist/core/parallel-agents.js +563 -0
  99. package/dist/core/plugin-bundle.d.ts +236 -0
  100. package/dist/core/plugin-bundle.js +887 -0
  101. package/dist/core/plugin-sdk.d.ts +139 -0
  102. package/dist/core/plugin-sdk.js +273 -0
  103. package/dist/core/prompt-cache.d.ts +163 -0
  104. package/dist/core/prompt-cache.js +599 -0
  105. package/dist/core/sandbox.d.ts +127 -0
  106. package/dist/core/sandbox.js +369 -0
  107. package/dist/core/scheduled-tasks.d.ts +151 -0
  108. package/dist/core/scheduled-tasks.js +502 -0
  109. package/dist/core/security-scanner.d.ts +160 -0
  110. package/dist/core/security-scanner.js +1494 -0
  111. package/dist/core/session.d.ts +83 -0
  112. package/dist/core/session.js +269 -0
  113. package/dist/core/shell-completion.d.ts +51 -0
  114. package/dist/core/shell-completion.js +674 -0
  115. package/dist/core/smart-monitor.d.ts +146 -0
  116. package/dist/core/smart-monitor.js +1199 -0
  117. package/dist/core/spec-driven.d.ts +233 -0
  118. package/dist/core/spec-driven.js +1485 -0
  119. package/dist/core/spending-warnings.d.ts +123 -0
  120. package/dist/core/spending-warnings.js +456 -0
  121. package/dist/core/sub-agent.d.ts +298 -0
  122. package/dist/core/sub-agent.js +1023 -0
  123. package/dist/core/telemetry.d.ts +157 -0
  124. package/dist/core/telemetry.js +412 -0
  125. package/dist/core/terminal-ux.d.ts +134 -0
  126. package/dist/core/terminal-ux.js +649 -0
  127. package/dist/core/testing.d.ts +146 -0
  128. package/dist/core/testing.js +1200 -0
  129. package/dist/core/types.d.ts +439 -0
  130. package/dist/core/types.js +6 -0
  131. package/dist/core/undo-redo.d.ts +112 -0
  132. package/dist/core/undo-redo.js +290 -0
  133. package/dist/core/updater.d.ts +159 -0
  134. package/dist/core/updater.js +608 -0
  135. package/dist/core/vim-mode.d.ts +151 -0
  136. package/dist/core/vim-mode.js +771 -0
  137. package/dist/core/voice.d.ts +137 -0
  138. package/dist/core/voice.js +538 -0
  139. package/dist/core/web-dashboard.d.ts +109 -0
  140. package/dist/core/web-dashboard.js +484 -0
  141. package/dist/hooks/hooks.d.ts +74 -0
  142. package/dist/hooks/hooks.js +160 -0
  143. package/dist/hooks/llm-evaluator.d.ts +165 -0
  144. package/dist/hooks/llm-evaluator.js +560 -0
  145. package/dist/index.d.ts +3 -0
  146. package/dist/index.js +1186 -0
  147. package/dist/lsp/lsp-manager.d.ts +63 -0
  148. package/dist/lsp/lsp-manager.js +351 -0
  149. package/dist/mcp/client.d.ts +133 -0
  150. package/dist/mcp/client.js +684 -0
  151. package/dist/mcp/mcp-apps.d.ts +70 -0
  152. package/dist/mcp/mcp-apps.js +1007 -0
  153. package/dist/tools/bash.d.ts +5 -0
  154. package/dist/tools/bash.js +195 -0
  155. package/dist/tools/browser.d.ts +92 -0
  156. package/dist/tools/browser.js +1570 -0
  157. package/dist/tools/extended.d.ts +6 -0
  158. package/dist/tools/extended.js +191 -0
  159. package/dist/tools/file.d.ts +10 -0
  160. package/dist/tools/file.js +382 -0
  161. package/dist/tools/github.d.ts +389 -0
  162. package/dist/tools/github.js +759 -0
  163. package/dist/tools/index.d.ts +9 -0
  164. package/dist/tools/index.js +40 -0
  165. package/dist/tools/memory.d.ts +6 -0
  166. package/dist/tools/memory.js +197 -0
  167. package/dist/tools/registry.d.ts +29 -0
  168. package/dist/tools/registry.js +64 -0
  169. package/dist/tools/web.d.ts +6 -0
  170. package/dist/tools/web.js +150 -0
  171. package/dist/ui/renderer.d.ts +97 -0
  172. package/dist/ui/renderer.js +279 -0
  173. package/dist/ui/theme.d.ts +27 -0
  174. package/dist/ui/theme.js +106 -0
  175. package/package.json +83 -0
@@ -0,0 +1,1156 @@
1
+ /**
2
+ * NeuroCLI — SKILL.md Standard Compliance System
3
+ *
4
+ * Implements the agentskills.io specification for SKILL.md files.
5
+ * Provides auto-discovery, parsing, activation, registry, and
6
+ * cross-tool compatibility with Claude Code, Codex CLI, and GitHub Copilot.
7
+ *
8
+ * Uses only Node.js built-in modules (fs, path, os, crypto, https, http, url).
9
+ */
10
+ import * as fs from "fs";
11
+ import * as path from "path";
12
+ import * as os from "os";
13
+ import * as crypto from "crypto";
14
+ import * as https from "https";
15
+ import * as http from "http";
16
+ import { URL } from "url";
17
+ // ---------------------------------------------------------------------------
18
+ // Constants
19
+ // ---------------------------------------------------------------------------
20
+ /** File name for skill definitions */
21
+ const SKILL_FILENAME = "SKILL.md";
22
+ /** Maximum allowed length for skill names */
23
+ const MAX_NAME_LENGTH = 64;
24
+ /** Regex for valid skill names: lowercase, digits, hyphens; must start with letter */
25
+ const VALID_NAME_REGEX = /^[a-z][a-z0-9-]{0,63}$/;
26
+ /** Default global skills directory relative to home */
27
+ const DEFAULT_GLOBAL_SKILLS_DIR = ".neuro/skills";
28
+ /** Default project-level skills directory */
29
+ const DEFAULT_PROJECT_SKILLS_DIR = ".neuro/skills";
30
+ /** Maximum number of nested directories to traverse during discovery */
31
+ const MAX_DISCOVERY_DEPTH = 5;
32
+ /** Cache TTL in milliseconds (5 minutes) */
33
+ const CACHE_TTL_MS = 5 * 60 * 1000;
34
+ /** Neuro skill registry base URL */
35
+ const REGISTRY_BASE_URL = "https://registry.agentskills.io";
36
+ // ---------------------------------------------------------------------------
37
+ // YAML Frontmatter Parser
38
+ // ---------------------------------------------------------------------------
39
+ /**
40
+ * Minimal YAML frontmatter parser that handles the subset of YAML used in
41
+ * SKILL.md files. Supports scalars (strings, numbers, booleans) and
42
+ * inline arrays (["a", "b"]). Does NOT depend on any external library.
43
+ */
44
+ function parseFrontmatter(raw) {
45
+ const trimmed = raw.trimStart();
46
+ if (!trimmed.startsWith("---")) {
47
+ return { metadata: {}, body: raw };
48
+ }
49
+ // Find the closing ---
50
+ const firstDelimiterEnd = trimmed.indexOf("\n", 0);
51
+ if (firstDelimiterEnd === -1) {
52
+ return { metadata: {}, body: raw };
53
+ }
54
+ const secondDelimiter = trimmed.indexOf("\n---", firstDelimiterEnd);
55
+ if (secondDelimiter === -1) {
56
+ return { metadata: {}, body: raw };
57
+ }
58
+ const frontmatterText = trimmed.slice(firstDelimiterEnd + 1, secondDelimiter);
59
+ const body = trimmed.slice(secondDelimiter + 4).trimStart();
60
+ const metadata = {};
61
+ // Parse line by line — handles top-level keys only (spec compliant)
62
+ const lines = frontmatterText.split("\n");
63
+ for (const line of lines) {
64
+ const colonIdx = line.indexOf(":");
65
+ if (colonIdx === -1)
66
+ continue;
67
+ const key = line.slice(0, colonIdx).trim();
68
+ let value = line.slice(colonIdx + 1).trim();
69
+ if (value === "") {
70
+ continue;
71
+ }
72
+ // Inline array: ["a", "b", "c"]
73
+ if (typeof value === "string" && value.startsWith("[") && value.endsWith("]")) {
74
+ value = parseInlineArray(value);
75
+ }
76
+ // Quoted string
77
+ else if (typeof value === "string") {
78
+ value = unquote(value);
79
+ }
80
+ // Numeric
81
+ else if (typeof value === "string" && /^\d+(\.\d+)?$/.test(value)) {
82
+ value = Number(value);
83
+ }
84
+ // Boolean
85
+ else if (value === "true") {
86
+ value = true;
87
+ }
88
+ else if (value === "false") {
89
+ value = false;
90
+ }
91
+ metadata[key] = value;
92
+ }
93
+ return { metadata, body };
94
+ }
95
+ /**
96
+ * Parse a YAML inline array like `["react", "nextjs", "frontend"]`.
97
+ */
98
+ function parseInlineArray(raw) {
99
+ const inner = raw.slice(1, -1).trim();
100
+ if (inner === "")
101
+ return [];
102
+ const items = [];
103
+ let current = "";
104
+ let inQuote = null;
105
+ for (let i = 0; i < inner.length; i++) {
106
+ const ch = inner[i];
107
+ if (inQuote) {
108
+ if (ch === inQuote) {
109
+ inQuote = null;
110
+ }
111
+ else {
112
+ current += ch;
113
+ }
114
+ }
115
+ else if (ch === '"' || ch === "'") {
116
+ inQuote = ch;
117
+ }
118
+ else if (ch === ",") {
119
+ items.push(current.trim());
120
+ current = "";
121
+ }
122
+ else {
123
+ current += ch;
124
+ }
125
+ }
126
+ if (current.trim()) {
127
+ items.push(current.trim());
128
+ }
129
+ return items.map((s) => unquote(s));
130
+ }
131
+ /**
132
+ * Remove surrounding quotes from a string value.
133
+ */
134
+ function unquote(value) {
135
+ if ((value.startsWith('"') && value.endsWith('"')) ||
136
+ (value.startsWith("'") && value.endsWith("'"))) {
137
+ return value.slice(1, -1);
138
+ }
139
+ return value;
140
+ }
141
+ // ---------------------------------------------------------------------------
142
+ // Cross-Tool Compatibility Adapters
143
+ // ---------------------------------------------------------------------------
144
+ /**
145
+ * Adapter for Claude Code skill format.
146
+ * Claude Code uses a CLAUDE.md convention with similar frontmatter.
147
+ */
148
+ function adaptFromClaudeCode(raw, filePath) {
149
+ const warnings = [];
150
+ const errors = [];
151
+ // Claude Code files may not have structured frontmatter; attempt extraction
152
+ const { metadata, body } = parseFrontmatter(raw);
153
+ const name = metadata.name ||
154
+ path.basename(path.dirname(filePath)).toLowerCase().replace(/\s+/g, "-");
155
+ if (!VALID_NAME_REGEX.test(name)) {
156
+ errors.push(`Invalid skill name "${name}". Must match ${VALID_NAME_REGEX.source}`);
157
+ }
158
+ const description = metadata.description ||
159
+ metadata.trigger ||
160
+ body.split("\n").find((l) => l.trim().length > 0) ||
161
+ "";
162
+ return {
163
+ skill: {
164
+ name,
165
+ description,
166
+ version: metadata.version || undefined,
167
+ author: metadata.author || undefined,
168
+ tags: Array.isArray(metadata.tags) ? metadata.tags : undefined,
169
+ tools: Array.isArray(metadata.tools) ? metadata.tools : undefined,
170
+ priority: typeof metadata.priority === "number" ? metadata.priority : 50,
171
+ invoke: metadata.invoke || "auto",
172
+ load: metadata.load || "on-demand",
173
+ body,
174
+ filePath,
175
+ source: "project",
176
+ },
177
+ warnings,
178
+ errors,
179
+ };
180
+ }
181
+ /**
182
+ * Adapter for Codex CLI skill format.
183
+ * Codex CLI uses codex.md / AGENTS.md files.
184
+ */
185
+ function adaptFromCodexCli(raw, filePath) {
186
+ const { metadata, body } = parseFrontmatter(raw);
187
+ const warnings = [];
188
+ const errors = [];
189
+ const name = metadata.name ||
190
+ path.basename(path.dirname(filePath)).toLowerCase().replace(/\s+/g, "-");
191
+ if (!VALID_NAME_REGEX.test(name)) {
192
+ errors.push(`Invalid skill name "${name}". Must match ${VALID_NAME_REGEX.source}`);
193
+ }
194
+ // Codex CLI uses "triggers" (plural) or "when" as activation condition
195
+ const description = metadata.description ||
196
+ metadata.triggers ||
197
+ metadata.when ||
198
+ "";
199
+ return {
200
+ skill: {
201
+ name,
202
+ description,
203
+ version: metadata.version || undefined,
204
+ author: metadata.author || undefined,
205
+ tags: Array.isArray(metadata.tags) ? metadata.tags : undefined,
206
+ tools: Array.isArray(metadata.tools) ? metadata.tools : undefined,
207
+ priority: typeof metadata.priority === "number" ? metadata.priority : 50,
208
+ invoke: metadata.invoke || "auto",
209
+ load: metadata.load || "on-demand",
210
+ body,
211
+ filePath,
212
+ source: "project",
213
+ },
214
+ warnings,
215
+ errors,
216
+ };
217
+ }
218
+ /**
219
+ * Adapter for GitHub Copilot skill format.
220
+ * Copilot uses .github/copilot-skills/ with .md files.
221
+ */
222
+ function adaptFromCopilot(raw, filePath) {
223
+ const { metadata, body } = parseFrontmatter(raw);
224
+ const warnings = [];
225
+ const errors = [];
226
+ const name = metadata.name ||
227
+ path.basename(filePath, ".md").toLowerCase().replace(/\s+/g, "-");
228
+ if (!VALID_NAME_REGEX.test(name)) {
229
+ errors.push(`Invalid skill name "${name}". Must match ${VALID_NAME_REGEX.source}`);
230
+ }
231
+ // Copilot uses "appliesTo" or "whenToSuggest" as activation condition
232
+ const description = metadata.description ||
233
+ metadata.appliesTo ||
234
+ metadata.whenToSuggest ||
235
+ "";
236
+ return {
237
+ skill: {
238
+ name,
239
+ description,
240
+ version: metadata.version || undefined,
241
+ author: metadata.author || undefined,
242
+ tags: Array.isArray(metadata.tags) ? metadata.tags : undefined,
243
+ tools: Array.isArray(metadata.tools) ? metadata.tools : undefined,
244
+ priority: typeof metadata.priority === "number" ? metadata.priority : 50,
245
+ invoke: metadata.invoke || "auto",
246
+ load: metadata.load || "on-demand",
247
+ body,
248
+ filePath,
249
+ source: "project",
250
+ },
251
+ warnings,
252
+ errors,
253
+ };
254
+ }
255
+ // ---------------------------------------------------------------------------
256
+ // Utility helpers
257
+ // ---------------------------------------------------------------------------
258
+ /**
259
+ * Walk a directory tree up to `maxDepth` levels, returning all files named
260
+ * `SKILL_FILENAME` or recognized cross-tool skill files.
261
+ */
262
+ function walkForSkills(dir, maxDepth, currentDepth = 0) {
263
+ const results = [];
264
+ if (currentDepth > maxDepth)
265
+ return results;
266
+ if (!fs.existsSync(dir))
267
+ return results;
268
+ let entries;
269
+ try {
270
+ entries = fs.readdirSync(dir, { withFileTypes: true });
271
+ }
272
+ catch {
273
+ return results;
274
+ }
275
+ for (const entry of entries) {
276
+ const fullPath = path.join(dir, entry.name);
277
+ if (entry.isFile()) {
278
+ // Standard SKILL.md
279
+ if (entry.name === SKILL_FILENAME) {
280
+ results.push(fullPath);
281
+ }
282
+ // Cross-tool: Claude Code
283
+ else if (entry.name === "CLAUDE.md") {
284
+ results.push(fullPath);
285
+ }
286
+ // Cross-tool: Codex CLI
287
+ else if (entry.name === "codex.md" || entry.name === "AGENTS.md") {
288
+ results.push(fullPath);
289
+ }
290
+ }
291
+ else if (entry.isDirectory()) {
292
+ // Recurse into subdirectories (e.g. nested skill folders)
293
+ if (!entry.name.startsWith(".") && entry.name !== "node_modules") {
294
+ results.push(...walkForSkills(fullPath, maxDepth, currentDepth + 1));
295
+ }
296
+ }
297
+ }
298
+ return results;
299
+ }
300
+ /**
301
+ * Determine the source type based on file path.
302
+ */
303
+ function determineSource(filePath, globalDir, projectDir) {
304
+ const normalized = path.resolve(filePath);
305
+ if (normalized.startsWith(path.resolve(globalDir))) {
306
+ return "global";
307
+ }
308
+ return "project";
309
+ }
310
+ function detectFormat(filePath) {
311
+ const base = path.basename(filePath);
312
+ switch (base) {
313
+ case "CLAUDE.md":
314
+ return "claude";
315
+ case "codex.md":
316
+ case "AGENTS.md":
317
+ return "codex";
318
+ default:
319
+ // Check for GitHub Copilot path pattern
320
+ if (filePath.includes(path.join(".github", "copilot-skills"))) {
321
+ return "copilot";
322
+ }
323
+ return "neuro";
324
+ }
325
+ }
326
+ /**
327
+ * Compute SHA-256 checksum of a string.
328
+ */
329
+ function checksum(content) {
330
+ return crypto.createHash("sha256").update(content, "utf8").digest("hex");
331
+ }
332
+ /**
333
+ * Ensure a directory exists, creating it recursively if needed.
334
+ */
335
+ function ensureDir(dir) {
336
+ if (!fs.existsSync(dir)) {
337
+ fs.mkdirSync(dir, { recursive: true });
338
+ }
339
+ }
340
+ /**
341
+ * HTTP/HTTPS GET request that returns the body as a string.
342
+ * Works with both http and https protocols using only built-in modules.
343
+ */
344
+ function httpGet(urlStr) {
345
+ return new Promise((resolve, reject) => {
346
+ let client;
347
+ try {
348
+ const parsed = new URL(urlStr);
349
+ client = parsed.protocol === "https:" ? https : http;
350
+ }
351
+ catch (err) {
352
+ reject(new Error(`Invalid URL: ${urlStr}`));
353
+ return;
354
+ }
355
+ client
356
+ .get(urlStr, { timeout: 15000 }, (res) => {
357
+ if (res.statusCode && res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) {
358
+ // Follow redirect
359
+ httpGet(res.headers.location).then(resolve).catch(reject);
360
+ return;
361
+ }
362
+ if (res.statusCode && (res.statusCode < 200 || res.statusCode >= 300)) {
363
+ reject(new Error(`HTTP ${res.statusCode} for ${urlStr}`));
364
+ return;
365
+ }
366
+ const chunks = [];
367
+ res.on("data", (chunk) => chunks.push(chunk));
368
+ res.on("end", () => resolve(Buffer.concat(chunks).toString("utf8")));
369
+ res.on("error", reject);
370
+ })
371
+ .on("error", reject)
372
+ .on("timeout", function () {
373
+ this.destroy();
374
+ reject(new Error(`Request timeout for ${urlStr}`));
375
+ });
376
+ });
377
+ }
378
+ /**
379
+ * HTTP POST request that sends a JSON body and returns the response.
380
+ */
381
+ function httpPost(urlStr, body) {
382
+ return new Promise((resolve, reject) => {
383
+ let client;
384
+ try {
385
+ const parsed = new URL(urlStr);
386
+ client = parsed.protocol === "https:" ? https : http;
387
+ }
388
+ catch (err) {
389
+ reject(new Error(`Invalid URL: ${urlStr}`));
390
+ return;
391
+ }
392
+ const payload = JSON.stringify(body);
393
+ const options = {
394
+ method: "POST",
395
+ headers: {
396
+ "Content-Type": "application/json",
397
+ "Content-Length": Buffer.byteLength(payload),
398
+ },
399
+ timeout: 15000,
400
+ };
401
+ const req = client.request(urlStr, options, (res) => {
402
+ if (res.statusCode && (res.statusCode < 200 || res.statusCode >= 300)) {
403
+ const chunks = [];
404
+ res.on("data", (chunk) => chunks.push(chunk));
405
+ res.on("end", () => {
406
+ reject(new Error(`HTTP ${res.statusCode}: ${Buffer.concat(chunks).toString("utf8")}`));
407
+ });
408
+ return;
409
+ }
410
+ const chunks = [];
411
+ res.on("data", (chunk) => chunks.push(chunk));
412
+ res.on("end", () => resolve(Buffer.concat(chunks).toString("utf8")));
413
+ res.on("error", reject);
414
+ });
415
+ req.on("error", reject);
416
+ req.on("timeout", () => {
417
+ req.destroy();
418
+ reject(new Error(`Request timeout for ${urlStr}`));
419
+ });
420
+ req.write(payload);
421
+ req.end();
422
+ });
423
+ }
424
+ // ---------------------------------------------------------------------------
425
+ // SkillStandard Class
426
+ // ---------------------------------------------------------------------------
427
+ /**
428
+ * Core class implementing the agentskills.io SKILL.md standard compliance
429
+ * system for NeuroCLI.
430
+ *
431
+ * Handles discovery, parsing, activation, installation, search, publishing,
432
+ * and cross-tool compatibility.
433
+ */
434
+ export class SkillStandard {
435
+ globalSkillsDir;
436
+ projectSkillsDir;
437
+ enableCache;
438
+ cacheTtlMs;
439
+ registryBaseUrl;
440
+ /** In-memory skill definitions, keyed by name */
441
+ skills = new Map();
442
+ /** Compact listing cache for agent activation */
443
+ compactListing = [];
444
+ /** Internal cache */
445
+ cache = null;
446
+ /** Local registry index (file-backed) */
447
+ registryIndex = new Map();
448
+ /** Path to the local registry file */
449
+ registryFilePath;
450
+ /** Whether discovery has been run at least once */
451
+ discovered = false;
452
+ constructor(options = {}) {
453
+ this.globalSkillsDir =
454
+ options.globalSkillsDir ||
455
+ path.join(os.homedir(), DEFAULT_GLOBAL_SKILLS_DIR);
456
+ this.projectSkillsDir =
457
+ options.projectSkillsDir || DEFAULT_PROJECT_SKILLS_DIR;
458
+ this.enableCache = options.enableCache ?? true;
459
+ this.cacheTtlMs = options.cacheTtlMs ?? CACHE_TTL_MS;
460
+ this.registryBaseUrl = options.registryBaseUrl || REGISTRY_BASE_URL;
461
+ this.registryFilePath = path.join(this.globalSkillsDir, "..", "registry.json");
462
+ }
463
+ // -----------------------------------------------------------------------
464
+ // Public API
465
+ // -----------------------------------------------------------------------
466
+ /**
467
+ * Discover all skills from both global and project directories.
468
+ * Reads ONLY the `name` and `description` from each skill's YAML
469
+ * frontmatter for the compact listing, but also caches full definitions.
470
+ *
471
+ * @param projectRoot - The root directory of the current project
472
+ * @returns Array of compact skill entries for agent activation
473
+ */
474
+ discoverSkills(projectRoot) {
475
+ // Return cached if still valid
476
+ if (this.enableCache && this.cache && Date.now() - this.cache.timestamp < this.cacheTtlMs) {
477
+ return this.cache.compactListing;
478
+ }
479
+ this.skills.clear();
480
+ this.compactListing = [];
481
+ const globalDir = this.globalSkillsDir;
482
+ const localDir = path.resolve(projectRoot, this.projectSkillsDir);
483
+ // Discover from global directory
484
+ const globalFiles = walkForSkills(globalDir, MAX_DISCOVERY_DEPTH);
485
+ for (const file of globalFiles) {
486
+ const result = this.parseSkillMd(file);
487
+ if (result.skill && result.errors.length === 0) {
488
+ result.skill.source = "global";
489
+ this.skills.set(result.skill.name, result.skill);
490
+ }
491
+ }
492
+ // Discover from project directory
493
+ const projectFiles = walkForSkills(localDir, MAX_DISCOVERY_DEPTH);
494
+ for (const file of projectFiles) {
495
+ const result = this.parseSkillMd(file);
496
+ if (result.skill && result.errors.length === 0) {
497
+ // Project skills override global skills with the same name
498
+ result.skill.source = "project";
499
+ this.skills.set(result.skill.name, result.skill);
500
+ }
501
+ }
502
+ // Also check for GitHub Copilot skill files in .github/copilot-skills/
503
+ const copilotDir = path.resolve(projectRoot, ".github/copilot-skills");
504
+ const copilotFiles = walkForSkills(copilotDir, MAX_DISCOVERY_DEPTH);
505
+ for (const file of copilotFiles) {
506
+ const result = this.parseSkillMd(file);
507
+ if (result.skill && result.errors.length === 0) {
508
+ result.skill.source = "project";
509
+ this.skills.set(result.skill.name, result.skill);
510
+ }
511
+ }
512
+ // Build compact listing — sorted by priority descending, then name ascending
513
+ this.compactListing = Array.from(this.skills.values())
514
+ .map((s) => ({
515
+ name: s.name,
516
+ description: s.description,
517
+ priority: s.priority ?? 50,
518
+ invoke: s.invoke ?? "auto",
519
+ load: s.load ?? "on-demand",
520
+ }))
521
+ .sort((a, b) => {
522
+ if (b.priority !== a.priority)
523
+ return b.priority - a.priority;
524
+ return a.name.localeCompare(b.name);
525
+ });
526
+ // Update cache
527
+ if (this.enableCache) {
528
+ this.cache = {
529
+ compactListing: this.compactListing,
530
+ skills: new Map(this.skills),
531
+ timestamp: Date.now(),
532
+ };
533
+ }
534
+ this.discovered = true;
535
+ return this.compactListing;
536
+ }
537
+ /**
538
+ * Parse a SKILL.md file according to the agentskills.io specification.
539
+ * Supports cross-tool format detection and automatic adaptation.
540
+ *
541
+ * @param filePath - Absolute path to the SKILL.md (or compatible) file
542
+ * @returns Parse result with skill definition, warnings, and errors
543
+ */
544
+ parseSkillMd(filePath) {
545
+ const warnings = [];
546
+ const errors = [];
547
+ // Read file
548
+ let raw;
549
+ try {
550
+ raw = fs.readFileSync(filePath, "utf8");
551
+ }
552
+ catch (err) {
553
+ return {
554
+ skill: null,
555
+ warnings,
556
+ errors: [
557
+ `Failed to read skill file at ${filePath}: ${err.message}`,
558
+ ],
559
+ };
560
+ }
561
+ // Detect format and adapt
562
+ const format = detectFormat(filePath);
563
+ let result;
564
+ switch (format) {
565
+ case "claude":
566
+ result = adaptFromClaudeCode(raw, filePath);
567
+ warnings.push(`Adapted from Claude Code format (CLAUDE.md). Some fields may need review.`);
568
+ break;
569
+ case "codex":
570
+ result = adaptFromCodexCli(raw, filePath);
571
+ warnings.push(`Adapted from Codex CLI format (${path.basename(filePath)}). Some fields may need review.`);
572
+ break;
573
+ case "copilot":
574
+ result = adaptFromCopilot(raw, filePath);
575
+ warnings.push(`Adapted from GitHub Copilot format. Some fields may need review.`);
576
+ break;
577
+ case "neuro":
578
+ default:
579
+ result = this.parseNeuroFormat(raw, filePath);
580
+ break;
581
+ }
582
+ // Merge warnings
583
+ warnings.push(...result.warnings);
584
+ // Validate required fields
585
+ if (result.skill) {
586
+ // Validate name
587
+ if (!result.skill.name) {
588
+ errors.push("Required field 'name' is missing.");
589
+ result.skill = null;
590
+ }
591
+ else if (!VALID_NAME_REGEX.test(result.skill.name)) {
592
+ errors.push(`Invalid skill name "${result.skill.name}". Must be lowercase, start with a letter, contain only letters, digits, and hyphens, and be at most ${MAX_NAME_LENGTH} characters. Pattern: ${VALID_NAME_REGEX.source}`);
593
+ result.skill = null;
594
+ }
595
+ // Validate description
596
+ if (result.skill && !result.skill.description) {
597
+ errors.push("Required field 'description' is missing.");
598
+ result.skill = null;
599
+ }
600
+ // Validate version format if present
601
+ if (result.skill && result.skill.version) {
602
+ if (!/^\d+\.\d+\.\d+/.test(result.skill.version)) {
603
+ warnings.push(`Version "${result.skill.version}" does not follow semver (major.minor.patch).`);
604
+ }
605
+ }
606
+ // Validate priority range
607
+ if (result.skill && result.skill.priority !== undefined) {
608
+ if (result.skill.priority < 0 || result.skill.priority > 100) {
609
+ warnings.push(`Priority ${result.skill.priority} is outside the recommended 0-100 range. Clamping.`);
610
+ result.skill.priority = Math.max(0, Math.min(100, result.skill.priority));
611
+ }
612
+ }
613
+ // Set defaults
614
+ if (result.skill) {
615
+ result.skill.priority = result.skill.priority ?? 50;
616
+ result.skill.invoke = result.skill.invoke ?? "auto";
617
+ result.skill.load = result.skill.load ?? "on-demand";
618
+ result.skill.source = determineSource(filePath, this.globalSkillsDir, this.projectSkillsDir);
619
+ }
620
+ }
621
+ return {
622
+ skill: result.skill,
623
+ warnings,
624
+ errors: [...errors, ...result.errors],
625
+ };
626
+ }
627
+ /**
628
+ * Get the compact listing of name+description pairs for agent activation.
629
+ * This is the minimal information the agent sees to decide which skill
630
+ * to activate. Must call discoverSkills() first.
631
+ *
632
+ * @returns Array of compact skill entries
633
+ */
634
+ getCompactListing() {
635
+ if (!this.discovered) {
636
+ return [];
637
+ }
638
+ return this.compactListing;
639
+ }
640
+ /**
641
+ * Activate a skill if its description matches the current context or prompt.
642
+ * For "auto" invoke skills, the description is used as a trigger condition.
643
+ * For "explicit" invoke skills, the skill name must be directly referenced.
644
+ *
645
+ * @param name - The skill name to activate
646
+ * @param prompt - The current user prompt or context for matching
647
+ * @returns The full skill definition if activated, or null
648
+ */
649
+ activateSkill(name, prompt) {
650
+ const skill = this.skills.get(name);
651
+ if (!skill)
652
+ return null;
653
+ // For explicit invoke, only activate if the skill name is directly
654
+ // referenced in the prompt
655
+ if (skill.invoke === "explicit") {
656
+ const namePatterns = [
657
+ name,
658
+ name.replace(/-/g, " "),
659
+ name.replace(/-/g, "_"),
660
+ ];
661
+ const promptLower = prompt.toLowerCase();
662
+ const isReferenced = namePatterns.some((p) => promptLower.includes(p.toLowerCase()));
663
+ if (!isReferenced)
664
+ return null;
665
+ }
666
+ // For auto invoke, check if the description matches the prompt context.
667
+ // Use a simple keyword-overlap heuristic with a threshold.
668
+ if (skill.invoke === "auto") {
669
+ const descriptionKeywords = extractKeywords(skill.description);
670
+ const promptKeywords = extractKeywords(prompt);
671
+ if (descriptionKeywords.length > 0) {
672
+ const overlap = descriptionKeywords.filter((k) => promptKeywords.includes(k));
673
+ // Require at least 30% keyword overlap, or at least 1 match for
674
+ // short descriptions
675
+ const threshold = Math.max(1, Math.ceil(descriptionKeywords.length * 0.3));
676
+ if (overlap.length < threshold)
677
+ return null;
678
+ }
679
+ }
680
+ return skill;
681
+ }
682
+ /**
683
+ * Install a skill from a URL or local path.
684
+ *
685
+ * @param source - URL (http/https) or local file path to the SKILL.md
686
+ * @param options - Installation options
687
+ * @returns The installed skill definition, or null on failure
688
+ */
689
+ async installSkill(source, options = {}) {
690
+ const errors = [];
691
+ let content;
692
+ // Determine if source is a URL or local path
693
+ if (source.startsWith("http://") || source.startsWith("https://")) {
694
+ try {
695
+ content = await httpGet(source);
696
+ }
697
+ catch (err) {
698
+ return {
699
+ skill: null,
700
+ errors: [
701
+ `Failed to fetch skill from ${source}: ${err.message}`,
702
+ ],
703
+ };
704
+ }
705
+ }
706
+ else {
707
+ // Local path
708
+ const resolvedPath = path.resolve(source);
709
+ if (!fs.existsSync(resolvedPath)) {
710
+ return {
711
+ skill: null,
712
+ errors: [`Local path does not exist: ${resolvedPath}`],
713
+ };
714
+ }
715
+ try {
716
+ content = fs.readFileSync(resolvedPath, "utf8");
717
+ }
718
+ catch (err) {
719
+ return {
720
+ skill: null,
721
+ errors: [
722
+ `Failed to read local file at ${resolvedPath}: ${err.message}`,
723
+ ],
724
+ };
725
+ }
726
+ }
727
+ // Parse the content
728
+ const tempPath = path.join(os.tmpdir(), `neuro-skill-install-${Date.now()}.md`);
729
+ try {
730
+ fs.writeFileSync(tempPath, content, "utf8");
731
+ const parseResult = this.parseSkillMd(tempPath);
732
+ if (parseResult.errors.length > 0 || !parseResult.skill) {
733
+ return {
734
+ skill: null,
735
+ errors: [...parseResult.errors, "Failed to parse skill content."],
736
+ };
737
+ }
738
+ const skillName = options.name || parseResult.skill.name;
739
+ if (!VALID_NAME_REGEX.test(skillName)) {
740
+ return {
741
+ skill: null,
742
+ errors: [
743
+ `Invalid skill name "${skillName}". Must match ${VALID_NAME_REGEX.source}`,
744
+ ],
745
+ };
746
+ }
747
+ // Determine target directory
748
+ const targetDir = options.global
749
+ ? path.join(this.globalSkillsDir, skillName)
750
+ : path.join(path.resolve(options.projectRoot || process.cwd()), this.projectSkillsDir, skillName);
751
+ ensureDir(targetDir);
752
+ const targetPath = path.join(targetDir, SKILL_FILENAME);
753
+ // If content came from a different format, re-serialize to standard SKILL.md
754
+ const format = detectFormat(source);
755
+ if (format !== "neuro" && parseResult.skill) {
756
+ content = serializeToStandardFormat(parseResult.skill);
757
+ }
758
+ fs.writeFileSync(targetPath, content, "utf8");
759
+ // Re-parse from final location
760
+ const finalResult = this.parseSkillMd(targetPath);
761
+ if (finalResult.skill) {
762
+ finalResult.skill.source = options.global ? "global" : "project";
763
+ this.skills.set(finalResult.skill.name, finalResult.skill);
764
+ this.invalidateCache();
765
+ // Update registry index
766
+ this.updateRegistryEntry(finalResult.skill, source);
767
+ }
768
+ return { skill: finalResult.skill, errors: [] };
769
+ }
770
+ finally {
771
+ // Clean up temp file
772
+ try {
773
+ fs.unlinkSync(tempPath);
774
+ }
775
+ catch {
776
+ // Ignore cleanup errors
777
+ }
778
+ }
779
+ }
780
+ /**
781
+ * Search available skills from the registry.
782
+ * Searches both the local registry index and the remote registry.
783
+ *
784
+ * @param query - Search query string
785
+ * @returns Array of matching registry entries
786
+ */
787
+ async searchSkills(query) {
788
+ const results = [];
789
+ const queryLower = query.toLowerCase();
790
+ const queryTerms = queryLower.split(/\s+/);
791
+ // Search local registry first
792
+ this.loadRegistryIndex();
793
+ for (const entry of this.registryIndex.values()) {
794
+ if (this.matchesQuery(entry, queryTerms)) {
795
+ results.push(entry);
796
+ }
797
+ }
798
+ // Search remote registry
799
+ try {
800
+ const remoteResults = await this.searchRemoteRegistry(query);
801
+ for (const entry of remoteResults) {
802
+ // Don't duplicate entries already found locally
803
+ if (!results.some((r) => r.name === entry.name)) {
804
+ // Mark installation status
805
+ entry.installed = this.skills.has(entry.name);
806
+ results.push(entry);
807
+ }
808
+ }
809
+ }
810
+ catch {
811
+ // Remote registry unavailable — return local results only
812
+ }
813
+ // Sort by relevance: installed first, then by name
814
+ results.sort((a, b) => {
815
+ if (a.installed !== b.installed)
816
+ return a.installed ? -1 : 1;
817
+ return a.name.localeCompare(b.name);
818
+ });
819
+ return results;
820
+ }
821
+ /**
822
+ * Publish a skill to the registry.
823
+ *
824
+ * @param skillPath - Path to the SKILL.md file or the skill directory
825
+ * @returns The registry entry created, or null on failure
826
+ */
827
+ async publishSkill(skillPath) {
828
+ const errors = [];
829
+ // Resolve path
830
+ const resolved = path.resolve(skillPath);
831
+ let filePath;
832
+ if (fs.statSync(resolved).isDirectory()) {
833
+ filePath = path.join(resolved, SKILL_FILENAME);
834
+ }
835
+ else {
836
+ filePath = resolved;
837
+ }
838
+ if (!fs.existsSync(filePath)) {
839
+ return {
840
+ entry: null,
841
+ errors: [`Skill file not found at ${filePath}`],
842
+ };
843
+ }
844
+ // Parse the skill
845
+ const parseResult = this.parseSkillMd(filePath);
846
+ if (parseResult.errors.length > 0 || !parseResult.skill) {
847
+ return {
848
+ entry: null,
849
+ errors: [...parseResult.errors, "Cannot publish invalid skill."],
850
+ };
851
+ }
852
+ const skill = parseResult.skill;
853
+ // Read raw content for checksum
854
+ const content = fs.readFileSync(filePath, "utf8");
855
+ const entry = {
856
+ name: skill.name,
857
+ description: skill.description,
858
+ version: skill.version || "1.0.0",
859
+ author: skill.author || "unknown",
860
+ tags: skill.tags || [],
861
+ source: filePath,
862
+ checksum: checksum(content),
863
+ addedAt: new Date().toISOString(),
864
+ installed: true,
865
+ };
866
+ // Attempt remote publish
867
+ try {
868
+ await httpPost(`${this.registryBaseUrl}/api/v1/skills`, {
869
+ name: entry.name,
870
+ description: entry.description,
871
+ version: entry.version,
872
+ author: entry.author,
873
+ tags: entry.tags,
874
+ checksum: entry.checksum,
875
+ content,
876
+ });
877
+ }
878
+ catch (err) {
879
+ errors.push(`Remote publish failed: ${err.message}. Skill saved to local registry only.`);
880
+ }
881
+ // Update local registry
882
+ this.loadRegistryIndex();
883
+ this.registryIndex.set(entry.name, entry);
884
+ this.saveRegistryIndex();
885
+ return { entry, errors };
886
+ }
887
+ /**
888
+ * Get the full content of an activated skill.
889
+ * This should be called after activateSkill() returns a match.
890
+ *
891
+ * @param name - The skill name
892
+ * @returns The full skill definition including the Markdown body, or null
893
+ */
894
+ getSkillContent(name) {
895
+ const skill = this.skills.get(name);
896
+ if (!skill)
897
+ return null;
898
+ // Re-read from disk to ensure we have the latest content
899
+ try {
900
+ const raw = fs.readFileSync(skill.filePath, "utf8");
901
+ const result = this.parseSkillMd(skill.filePath);
902
+ if (result.skill) {
903
+ // Update the cached version
904
+ this.skills.set(name, result.skill);
905
+ return result.skill;
906
+ }
907
+ }
908
+ catch {
909
+ // Fall back to cached version
910
+ }
911
+ return skill;
912
+ }
913
+ /**
914
+ * List all installed skills with their current status.
915
+ *
916
+ * @returns Array of skill definitions with status information
917
+ */
918
+ listInstalled() {
919
+ return Array.from(this.skills.values()).map((skill) => ({
920
+ ...skill,
921
+ loaded: true,
922
+ format: detectFormat(skill.filePath),
923
+ }));
924
+ }
925
+ // -----------------------------------------------------------------------
926
+ // Private Methods
927
+ // -----------------------------------------------------------------------
928
+ /**
929
+ * Parse a standard Neuro SKILL.md file.
930
+ */
931
+ parseNeuroFormat(raw, filePath) {
932
+ const warnings = [];
933
+ const errors = [];
934
+ const { metadata, body } = parseFrontmatter(raw);
935
+ const name = metadata.name;
936
+ const description = metadata.description;
937
+ if (!name) {
938
+ errors.push("Required field 'name' is missing from frontmatter.");
939
+ }
940
+ if (!description) {
941
+ errors.push("Required field 'description' is missing from frontmatter.");
942
+ }
943
+ if (errors.length > 0) {
944
+ return {
945
+ skill: null,
946
+ warnings,
947
+ errors,
948
+ };
949
+ }
950
+ const skill = {
951
+ name: name,
952
+ description: description,
953
+ version: metadata.version || undefined,
954
+ author: metadata.author || undefined,
955
+ tags: Array.isArray(metadata.tags) ? metadata.tags : undefined,
956
+ tools: Array.isArray(metadata.tools) ? metadata.tools : undefined,
957
+ priority: typeof metadata.priority === "number" ? metadata.priority : undefined,
958
+ invoke: metadata.invoke || undefined,
959
+ load: metadata.load || undefined,
960
+ body,
961
+ filePath,
962
+ source: "project", // Will be overridden by caller
963
+ };
964
+ return { skill, warnings, errors };
965
+ }
966
+ /**
967
+ * Invalidate the discovery cache.
968
+ */
969
+ invalidateCache() {
970
+ this.cache = null;
971
+ this.discovered = false;
972
+ this.compactListing = [];
973
+ }
974
+ /**
975
+ * Load the local registry index from disk.
976
+ */
977
+ loadRegistryIndex() {
978
+ this.registryIndex.clear();
979
+ if (!fs.existsSync(this.registryFilePath))
980
+ return;
981
+ try {
982
+ const raw = fs.readFileSync(this.registryFilePath, "utf8");
983
+ const data = JSON.parse(raw);
984
+ for (const entry of data) {
985
+ // Verify installation status
986
+ const skillDir = path.join(this.globalSkillsDir, entry.name);
987
+ entry.installed = fs.existsSync(path.join(skillDir, SKILL_FILENAME));
988
+ this.registryIndex.set(entry.name, entry);
989
+ }
990
+ }
991
+ catch {
992
+ // Corrupted or empty registry — start fresh
993
+ }
994
+ }
995
+ /**
996
+ * Save the local registry index to disk.
997
+ */
998
+ saveRegistryIndex() {
999
+ ensureDir(path.dirname(this.registryFilePath));
1000
+ const data = Array.from(this.registryIndex.values());
1001
+ fs.writeFileSync(this.registryFilePath, JSON.stringify(data, null, 2), "utf8");
1002
+ }
1003
+ /**
1004
+ * Update a single registry entry.
1005
+ */
1006
+ updateRegistryEntry(skill, source) {
1007
+ this.loadRegistryIndex();
1008
+ try {
1009
+ const content = fs.readFileSync(skill.filePath, "utf8");
1010
+ const entry = {
1011
+ name: skill.name,
1012
+ description: skill.description,
1013
+ version: skill.version || "1.0.0",
1014
+ author: skill.author || "unknown",
1015
+ tags: skill.tags || [],
1016
+ source,
1017
+ checksum: checksum(content),
1018
+ addedAt: new Date().toISOString(),
1019
+ installed: true,
1020
+ };
1021
+ this.registryIndex.set(skill.name, entry);
1022
+ this.saveRegistryIndex();
1023
+ }
1024
+ catch {
1025
+ // Silently fail — non-critical
1026
+ }
1027
+ }
1028
+ /**
1029
+ * Search the remote skill registry.
1030
+ */
1031
+ async searchRemoteRegistry(query) {
1032
+ const url = `${this.registryBaseUrl}/api/v1/skills/search?q=${encodeURIComponent(query)}`;
1033
+ const raw = await httpGet(url);
1034
+ const data = JSON.parse(raw);
1035
+ if (Array.isArray(data)) {
1036
+ return data;
1037
+ }
1038
+ if (data && Array.isArray(data.results)) {
1039
+ return data.results;
1040
+ }
1041
+ return [];
1042
+ }
1043
+ /**
1044
+ * Check if a registry entry matches the given query terms.
1045
+ */
1046
+ matchesQuery(entry, queryTerms) {
1047
+ const searchable = [
1048
+ entry.name,
1049
+ entry.description,
1050
+ entry.author,
1051
+ ...entry.tags,
1052
+ ]
1053
+ .join(" ")
1054
+ .toLowerCase();
1055
+ return queryTerms.every((term) => searchable.includes(term));
1056
+ }
1057
+ }
1058
+ // ---------------------------------------------------------------------------
1059
+ // Standalone utility functions
1060
+ // ---------------------------------------------------------------------------
1061
+ /**
1062
+ * Extract meaningful keywords from a text string.
1063
+ * Removes stop words and normalizes for comparison.
1064
+ */
1065
+ function extractKeywords(text) {
1066
+ const stopWords = new Set([
1067
+ "a", "an", "and", "are", "as", "at", "be", "by", "do", "for", "from",
1068
+ "has", "have", "how", "i", "if", "in", "is", "it", "its", "just",
1069
+ "me", "my", "no", "not", "of", "on", "or", "our", "out", "own",
1070
+ "say", "she", "so", "than", "that", "the", "their", "them", "then",
1071
+ "there", "these", "they", "this", "to", "up", "us", "very", "was",
1072
+ "we", "what", "when", "which", "who", "will", "with", "would", "you",
1073
+ "your", "can", "could", "should", "shall", "may", "might", "must",
1074
+ ]);
1075
+ return text
1076
+ .toLowerCase()
1077
+ .replace(/[^a-z0-9\s-]/g, " ")
1078
+ .split(/[\s-]+/)
1079
+ .filter((word) => word.length > 1 && !stopWords.has(word));
1080
+ }
1081
+ /**
1082
+ * Serialize a StandardSkillDefinition back into a valid SKILL.md format.
1083
+ * Used when converting cross-tool formats to the standard format.
1084
+ */
1085
+ function serializeToStandardFormat(skill) {
1086
+ const lines = ["---"];
1087
+ lines.push(`name: ${skill.name}`);
1088
+ lines.push(`description: "${skill.description.replace(/"/g, '\\"')}"`);
1089
+ if (skill.version)
1090
+ lines.push(`version: "${skill.version}"`);
1091
+ if (skill.author)
1092
+ lines.push(`author: "${skill.author}"`);
1093
+ if (skill.tags && skill.tags.length > 0) {
1094
+ lines.push(`tags: [${skill.tags.map((t) => `"${t}"`).join(", ")}]`);
1095
+ }
1096
+ if (skill.tools && skill.tools.length > 0) {
1097
+ lines.push(`tools: [${skill.tools.map((t) => `"${t}"`).join(", ")}]`);
1098
+ }
1099
+ if (skill.priority !== undefined)
1100
+ lines.push(`priority: ${skill.priority}`);
1101
+ if (skill.invoke)
1102
+ lines.push(`invoke: ${skill.invoke}`);
1103
+ if (skill.load)
1104
+ lines.push(`load: ${skill.load}`);
1105
+ lines.push("---");
1106
+ lines.push("");
1107
+ if (skill.body) {
1108
+ lines.push(skill.body);
1109
+ }
1110
+ return lines.join("\n");
1111
+ }
1112
+ /**
1113
+ * Create a new SKILL.md file at the specified path.
1114
+ * Convenience function for bootstrapping new skills.
1115
+ */
1116
+ export function createSkillFile(dirPath, definition) {
1117
+ ensureDir(dirPath);
1118
+ const skill = {
1119
+ ...definition,
1120
+ body: definition.body || `# ${definition.name}\n\nSkill content goes here.\n`,
1121
+ filePath: "",
1122
+ source: "project",
1123
+ };
1124
+ const content = serializeToStandardFormat(skill);
1125
+ const filePath = path.join(dirPath, SKILL_FILENAME);
1126
+ fs.writeFileSync(filePath, content, "utf8");
1127
+ return filePath;
1128
+ }
1129
+ /**
1130
+ * Validate a skill name against the specification requirements.
1131
+ */
1132
+ export function validateSkillName(name) {
1133
+ const errors = [];
1134
+ if (!name) {
1135
+ errors.push("Name is required.");
1136
+ }
1137
+ else {
1138
+ if (name.length > MAX_NAME_LENGTH) {
1139
+ errors.push(`Name exceeds maximum length of ${MAX_NAME_LENGTH} characters (got ${name.length}).`);
1140
+ }
1141
+ if (!/^[a-z]/.test(name)) {
1142
+ errors.push("Name must start with a lowercase letter.");
1143
+ }
1144
+ if (/[^a-z0-9-]/.test(name)) {
1145
+ errors.push("Name must contain only lowercase letters, digits, and hyphens.");
1146
+ }
1147
+ if (name.endsWith("-")) {
1148
+ errors.push("Name must not end with a hyphen.");
1149
+ }
1150
+ if (name.includes("--")) {
1151
+ errors.push("Name must not contain consecutive hyphens.");
1152
+ }
1153
+ }
1154
+ return { valid: errors.length === 0, errors };
1155
+ }
1156
+ //# sourceMappingURL=skill-standard.js.map