skill-viewer 0.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +54 -0
- package/dist/cli.js +1043 -0
- package/package.json +41 -0
- package/public/index.html +2219 -0
package/dist/cli.js
ADDED
|
@@ -0,0 +1,1043 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
var __create = Object.create;
|
|
4
|
+
var __defProp = Object.defineProperty;
|
|
5
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
6
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
7
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
8
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
9
|
+
var __copyProps = (to, from, except, desc) => {
|
|
10
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
11
|
+
for (let key of __getOwnPropNames(from))
|
|
12
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
13
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
14
|
+
}
|
|
15
|
+
return to;
|
|
16
|
+
};
|
|
17
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
18
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
19
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
20
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
21
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
22
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
23
|
+
mod
|
|
24
|
+
));
|
|
25
|
+
|
|
26
|
+
// src/cli.ts
|
|
27
|
+
var import_node_http = __toESM(require("http"));
|
|
28
|
+
var import_node_fs3 = require("fs");
|
|
29
|
+
var import_node_path3 = __toESM(require("path"));
|
|
30
|
+
|
|
31
|
+
// src/server.ts
|
|
32
|
+
var import_express = __toESM(require("express"));
|
|
33
|
+
var import_node_path2 = __toESM(require("path"));
|
|
34
|
+
var import_node_fs2 = __toESM(require("fs"));
|
|
35
|
+
|
|
36
|
+
// src/index.ts
|
|
37
|
+
var import_node_fs = __toESM(require("fs"));
|
|
38
|
+
var import_node_path = __toESM(require("path"));
|
|
39
|
+
var import_node_os = __toESM(require("os"));
|
|
40
|
+
|
|
41
|
+
// src/frontmatter.ts
|
|
42
|
+
var FM_REGEX = /^---\s*\n([\s\S]*?)\n---\s*\n?/;
|
|
43
|
+
function parseFrontmatter(content) {
|
|
44
|
+
content = content.replace(/\r\n/g, "\n");
|
|
45
|
+
const match = content.match(FM_REGEX);
|
|
46
|
+
if (!match) return {};
|
|
47
|
+
const result = {};
|
|
48
|
+
let lastKey = null;
|
|
49
|
+
for (const line of match[1].split("\n")) {
|
|
50
|
+
const isContinuation = /^\s+/.test(line) || lastKey !== null && line.indexOf(":") === -1;
|
|
51
|
+
if (isContinuation && lastKey !== null) {
|
|
52
|
+
result[lastKey] = result[lastKey] + "\n" + line.trimEnd();
|
|
53
|
+
continue;
|
|
54
|
+
}
|
|
55
|
+
const idx = line.indexOf(":");
|
|
56
|
+
if (idx === -1) continue;
|
|
57
|
+
const key = line.slice(0, idx).trim();
|
|
58
|
+
if (key === "") continue;
|
|
59
|
+
let value = line.slice(idx + 1).trim();
|
|
60
|
+
if (value.startsWith('"') && value.endsWith('"') || value.startsWith("'") && value.endsWith("'")) {
|
|
61
|
+
value = value.slice(1, -1);
|
|
62
|
+
}
|
|
63
|
+
result[key] = value;
|
|
64
|
+
lastKey = key;
|
|
65
|
+
}
|
|
66
|
+
return result;
|
|
67
|
+
}
|
|
68
|
+
function extractFrontmatterRaw(content) {
|
|
69
|
+
const match = content.match(FM_REGEX);
|
|
70
|
+
return match ? match[1] : "";
|
|
71
|
+
}
|
|
72
|
+
function stripFrontmatter(content) {
|
|
73
|
+
return content.replace(FM_REGEX, "");
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// src/index.ts
|
|
77
|
+
var AGENT_CONFIGS = [
|
|
78
|
+
{
|
|
79
|
+
id: "claude",
|
|
80
|
+
name: "Claude Code",
|
|
81
|
+
globalDir: import_node_path.default.join(import_node_os.default.homedir(), ".claude"),
|
|
82
|
+
projectDirName: ".claude",
|
|
83
|
+
fileExtensions: [".md"],
|
|
84
|
+
hasPlugins: true,
|
|
85
|
+
subdirs: ["skills", "commands"],
|
|
86
|
+
scanGlobalRoot: false
|
|
87
|
+
},
|
|
88
|
+
{
|
|
89
|
+
id: "gemini",
|
|
90
|
+
name: "Gemini CLI",
|
|
91
|
+
globalDir: import_node_path.default.join(import_node_os.default.homedir(), ".gemini"),
|
|
92
|
+
projectDirName: ".gemini",
|
|
93
|
+
fileExtensions: [".md", ".toml"],
|
|
94
|
+
hasPlugins: false,
|
|
95
|
+
subdirs: ["skills", "commands"],
|
|
96
|
+
scanGlobalRoot: false
|
|
97
|
+
},
|
|
98
|
+
{
|
|
99
|
+
id: "opencode",
|
|
100
|
+
name: "OpenCode",
|
|
101
|
+
globalDir: import_node_path.default.join(import_node_os.default.homedir(), ".config", "opencode"),
|
|
102
|
+
projectDirName: ".opencode",
|
|
103
|
+
fileExtensions: [".md"],
|
|
104
|
+
hasPlugins: false,
|
|
105
|
+
subdirs: ["skills", "commands", "agents"],
|
|
106
|
+
scanGlobalRoot: false
|
|
107
|
+
},
|
|
108
|
+
{
|
|
109
|
+
id: "codex",
|
|
110
|
+
name: "Codex CLI",
|
|
111
|
+
globalDir: import_node_path.default.join(import_node_os.default.homedir(), ".codex"),
|
|
112
|
+
projectDirName: ".codex",
|
|
113
|
+
fileExtensions: [".md"],
|
|
114
|
+
hasPlugins: false,
|
|
115
|
+
subdirs: ["skills"],
|
|
116
|
+
scanGlobalRoot: false
|
|
117
|
+
},
|
|
118
|
+
{
|
|
119
|
+
id: "cursor",
|
|
120
|
+
name: "Cursor",
|
|
121
|
+
globalDir: import_node_path.default.join(import_node_os.default.homedir(), ".cursor"),
|
|
122
|
+
projectDirName: ".cursor",
|
|
123
|
+
fileExtensions: [".md", ".mdc"],
|
|
124
|
+
hasPlugins: false,
|
|
125
|
+
subdirs: ["rules"],
|
|
126
|
+
scanGlobalRoot: false
|
|
127
|
+
},
|
|
128
|
+
{
|
|
129
|
+
id: "cline",
|
|
130
|
+
name: "Cline",
|
|
131
|
+
globalDir: import_node_path.default.join(import_node_os.default.homedir(), "Documents", "Cline", "Rules"),
|
|
132
|
+
projectDirName: ".clinerules",
|
|
133
|
+
fileExtensions: [".md", ".txt"],
|
|
134
|
+
hasPlugins: false,
|
|
135
|
+
subdirs: [],
|
|
136
|
+
scanGlobalRoot: true
|
|
137
|
+
}
|
|
138
|
+
];
|
|
139
|
+
function getAgentConfig(agentId) {
|
|
140
|
+
return AGENT_CONFIGS.find((a) => a.id === agentId) || AGENT_CONFIGS[0];
|
|
141
|
+
}
|
|
142
|
+
var activeAgent = AGENT_CONFIGS[0];
|
|
143
|
+
var CLAUDE_DIR = activeAgent.globalDir;
|
|
144
|
+
var PLUGINS_DIR = import_node_path.default.join(CLAUDE_DIR, "plugins", "cache", "claude-plugins-official");
|
|
145
|
+
var CUSTOM_SKILLS_DIR = import_node_path.default.join(CLAUDE_DIR, "skills");
|
|
146
|
+
var COMMANDS_DIR = import_node_path.default.join(CLAUDE_DIR, "commands");
|
|
147
|
+
function updateDirs(agent) {
|
|
148
|
+
CLAUDE_DIR = agent.globalDir;
|
|
149
|
+
if (agent.id === "claude") {
|
|
150
|
+
PLUGINS_DIR = import_node_path.default.join(CLAUDE_DIR, "plugins", "cache", "claude-plugins-official");
|
|
151
|
+
CUSTOM_SKILLS_DIR = import_node_path.default.join(CLAUDE_DIR, "skills");
|
|
152
|
+
COMMANDS_DIR = import_node_path.default.join(CLAUDE_DIR, "commands");
|
|
153
|
+
} else {
|
|
154
|
+
PLUGINS_DIR = "";
|
|
155
|
+
CUSTOM_SKILLS_DIR = CLAUDE_DIR;
|
|
156
|
+
COMMANDS_DIR = "";
|
|
157
|
+
for (const sub of agent.subdirs) {
|
|
158
|
+
const subDir = import_node_path.default.join(CLAUDE_DIR, sub);
|
|
159
|
+
if (sub === "commands") COMMANDS_DIR = subDir;
|
|
160
|
+
else if (sub === "rules") CUSTOM_SKILLS_DIR = subDir;
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
var KNOWN_TOOLS = /* @__PURE__ */ new Set([
|
|
165
|
+
"Agent",
|
|
166
|
+
"Bash",
|
|
167
|
+
"Edit",
|
|
168
|
+
"Read",
|
|
169
|
+
"Write",
|
|
170
|
+
"Glob",
|
|
171
|
+
"Grep",
|
|
172
|
+
"WebFetch",
|
|
173
|
+
"WebSearch",
|
|
174
|
+
"TaskCreate",
|
|
175
|
+
"TaskUpdate",
|
|
176
|
+
"TodoWrite",
|
|
177
|
+
"Skill",
|
|
178
|
+
"NotebookEdit",
|
|
179
|
+
"LSP"
|
|
180
|
+
]);
|
|
181
|
+
var SKILL_REF_RE = /\b[\w-]+:[\w-]+\b/g;
|
|
182
|
+
function exists(p) {
|
|
183
|
+
try {
|
|
184
|
+
import_node_fs.default.accessSync(p);
|
|
185
|
+
return true;
|
|
186
|
+
} catch {
|
|
187
|
+
return false;
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
function readdir(p) {
|
|
191
|
+
try {
|
|
192
|
+
return import_node_fs.default.readdirSync(p);
|
|
193
|
+
} catch {
|
|
194
|
+
return [];
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
function readText(p) {
|
|
198
|
+
return import_node_fs.default.readFileSync(p, "utf-8");
|
|
199
|
+
}
|
|
200
|
+
function isDir(p) {
|
|
201
|
+
try {
|
|
202
|
+
return import_node_fs.default.statSync(p).isDirectory();
|
|
203
|
+
} catch {
|
|
204
|
+
return false;
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
function isFile(p) {
|
|
208
|
+
try {
|
|
209
|
+
return import_node_fs.default.statSync(p).isFile();
|
|
210
|
+
} catch {
|
|
211
|
+
return false;
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
function mtime(p) {
|
|
215
|
+
try {
|
|
216
|
+
return import_node_fs.default.statSync(p).mtimeMs / 1e3;
|
|
217
|
+
} catch {
|
|
218
|
+
return 0;
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
function findLatestVersionDir(pluginDir) {
|
|
222
|
+
const entries = readdir(pluginDir).filter((name) => {
|
|
223
|
+
const full = import_node_path.default.join(pluginDir, name);
|
|
224
|
+
return isDir(full) && (name[0] >= "0" && name[0] <= "9" || name === "unknown");
|
|
225
|
+
});
|
|
226
|
+
if (entries.length === 0) return null;
|
|
227
|
+
entries.sort((a, b) => {
|
|
228
|
+
if (a === "unknown") return 1;
|
|
229
|
+
if (b === "unknown") return -1;
|
|
230
|
+
return b.localeCompare(a);
|
|
231
|
+
});
|
|
232
|
+
return import_node_path.default.join(pluginDir, entries[0]);
|
|
233
|
+
}
|
|
234
|
+
var SkillIndex = class {
|
|
235
|
+
skills = [];
|
|
236
|
+
byPath = /* @__PURE__ */ new Map();
|
|
237
|
+
projectDirs = /* @__PURE__ */ new Set();
|
|
238
|
+
getActiveAgent() {
|
|
239
|
+
return activeAgent;
|
|
240
|
+
}
|
|
241
|
+
setAgent(agentId) {
|
|
242
|
+
activeAgent = getAgentConfig(agentId);
|
|
243
|
+
updateDirs(activeAgent);
|
|
244
|
+
this.build();
|
|
245
|
+
}
|
|
246
|
+
build() {
|
|
247
|
+
this.skills = [];
|
|
248
|
+
this.byPath = /* @__PURE__ */ new Map();
|
|
249
|
+
if (activeAgent.id === "claude") {
|
|
250
|
+
this.indexPlugins();
|
|
251
|
+
this.indexCustomSkills();
|
|
252
|
+
this.indexCommands();
|
|
253
|
+
} else {
|
|
254
|
+
this.indexGenericGlobalDir();
|
|
255
|
+
}
|
|
256
|
+
for (const dir of this.projectDirs) {
|
|
257
|
+
this.indexProjectDir(dir);
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
addProjectDir(projectDir) {
|
|
261
|
+
const configDir = import_node_path.default.join(projectDir, activeAgent.projectDirName);
|
|
262
|
+
if (!exists(configDir) || !isDir(configDir)) return false;
|
|
263
|
+
this.projectDirs.add(projectDir);
|
|
264
|
+
this.indexProjectDir(projectDir);
|
|
265
|
+
return true;
|
|
266
|
+
}
|
|
267
|
+
removeProjectDir(projectDir) {
|
|
268
|
+
this.projectDirs.delete(projectDir);
|
|
269
|
+
const configDir = import_node_path.default.join(projectDir, activeAgent.projectDirName);
|
|
270
|
+
this.skills = this.skills.filter((s) => !s.path.startsWith(configDir + import_node_path.default.sep));
|
|
271
|
+
for (const [p] of this.byPath) {
|
|
272
|
+
if (p.startsWith(configDir + import_node_path.default.sep)) this.byPath.delete(p);
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
getProjectDirs() {
|
|
276
|
+
return [...this.projectDirs];
|
|
277
|
+
}
|
|
278
|
+
extractCrossReferences(content) {
|
|
279
|
+
const matches = content.match(SKILL_REF_RE) || [];
|
|
280
|
+
return [...new Set(matches)].sort();
|
|
281
|
+
}
|
|
282
|
+
extractToolReferences(content) {
|
|
283
|
+
const found = [];
|
|
284
|
+
for (const tool of KNOWN_TOOLS) {
|
|
285
|
+
const re = new RegExp(`\\b${tool}\\b`);
|
|
286
|
+
if (re.test(content)) found.push(tool);
|
|
287
|
+
}
|
|
288
|
+
return found.sort();
|
|
289
|
+
}
|
|
290
|
+
extractStructuralTags(content) {
|
|
291
|
+
const tags = [];
|
|
292
|
+
if (/^[ \t]*-[ \t]*\[[ xX]\]/m.test(content)) tags.push("has-checklist");
|
|
293
|
+
if (/examples?:|for example|e\.g\./i.test(content)) tags.push("has-examples");
|
|
294
|
+
if (/```/.test(content)) tags.push("has-code-blocks");
|
|
295
|
+
if (/^[ \t]*(\d+\.|step \d+)/im.test(content)) tags.push("has-steps");
|
|
296
|
+
if (/^\|.+\|/m.test(content)) tags.push("has-table");
|
|
297
|
+
if (/```\s*(mermaid|dot|plantuml)|graph (LR|TD|RL|BT)|digraph /i.test(content)) tags.push("has-diagram");
|
|
298
|
+
return tags;
|
|
299
|
+
}
|
|
300
|
+
indexSkill(filePath, sourceType, sourceName, skillDir) {
|
|
301
|
+
try {
|
|
302
|
+
const content = readText(filePath);
|
|
303
|
+
const frontmatter = parseFrontmatter(content);
|
|
304
|
+
const body = stripFrontmatter(content);
|
|
305
|
+
const mt = mtime(filePath);
|
|
306
|
+
const skill = {
|
|
307
|
+
name: frontmatter.name || (import_node_path.default.basename(filePath) === "SKILL.md" ? import_node_path.default.basename(import_node_path.default.dirname(filePath)) : import_node_path.default.basename(filePath, import_node_path.default.extname(filePath))),
|
|
308
|
+
description: frontmatter.description || "",
|
|
309
|
+
path: filePath,
|
|
310
|
+
sourceType,
|
|
311
|
+
sourceName,
|
|
312
|
+
filename: import_node_path.default.basename(filePath),
|
|
313
|
+
content: body,
|
|
314
|
+
frontmatter,
|
|
315
|
+
frontmatterRaw: extractFrontmatterRaw(content),
|
|
316
|
+
crossReferences: this.extractCrossReferences(content),
|
|
317
|
+
toolReferences: this.extractToolReferences(content),
|
|
318
|
+
structuralTags: this.extractStructuralTags(body),
|
|
319
|
+
wordCount: body.split(/\s+/).filter(Boolean).length,
|
|
320
|
+
mtime: mt,
|
|
321
|
+
skillDir
|
|
322
|
+
};
|
|
323
|
+
this.skills.push(skill);
|
|
324
|
+
this.byPath.set(filePath, skill);
|
|
325
|
+
} catch {
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
indexPlugins() {
|
|
329
|
+
if (!exists(PLUGINS_DIR)) return;
|
|
330
|
+
for (const pluginName of readdir(PLUGINS_DIR)) {
|
|
331
|
+
const pluginDir = import_node_path.default.join(PLUGINS_DIR, pluginName);
|
|
332
|
+
if (!isDir(pluginDir)) continue;
|
|
333
|
+
const latestVersion = findLatestVersionDir(pluginDir);
|
|
334
|
+
if (!latestVersion) continue;
|
|
335
|
+
const skillsDir = import_node_path.default.join(latestVersion, "skills");
|
|
336
|
+
if (!exists(skillsDir)) continue;
|
|
337
|
+
for (const skillName of readdir(skillsDir)) {
|
|
338
|
+
const skillDir = import_node_path.default.join(skillsDir, skillName);
|
|
339
|
+
if (!isDir(skillDir)) continue;
|
|
340
|
+
const skillFile = import_node_path.default.join(skillDir, "SKILL.md");
|
|
341
|
+
if (exists(skillFile)) {
|
|
342
|
+
this.indexSkill(skillFile, "plugin", pluginName, skillDir);
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
indexCustomSkills() {
|
|
348
|
+
if (!exists(CUSTOM_SKILLS_DIR)) return;
|
|
349
|
+
for (const name of readdir(CUSTOM_SKILLS_DIR)) {
|
|
350
|
+
const itemPath = import_node_path.default.join(CUSTOM_SKILLS_DIR, name);
|
|
351
|
+
if (isDir(itemPath)) {
|
|
352
|
+
const skillFile = import_node_path.default.join(itemPath, "SKILL.md");
|
|
353
|
+
if (exists(skillFile)) {
|
|
354
|
+
this.indexSkill(skillFile, "custom", "Custom Skills", itemPath);
|
|
355
|
+
}
|
|
356
|
+
} else if (isFile(itemPath) && itemPath.endsWith(".md")) {
|
|
357
|
+
this.indexSkill(itemPath, "custom", "Custom Skills", import_node_path.default.dirname(itemPath));
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
indexCommands() {
|
|
362
|
+
if (!exists(COMMANDS_DIR)) return;
|
|
363
|
+
for (const name of readdir(COMMANDS_DIR)) {
|
|
364
|
+
const filePath = import_node_path.default.join(COMMANDS_DIR, name);
|
|
365
|
+
if (isFile(filePath) && name.endsWith(".md")) {
|
|
366
|
+
this.indexSkill(filePath, "command", "Commands", COMMANDS_DIR);
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
hasMatchingExtension(filename) {
|
|
371
|
+
return activeAgent.fileExtensions.some((ext) => filename.endsWith(ext));
|
|
372
|
+
}
|
|
373
|
+
indexGenericGlobalDir() {
|
|
374
|
+
const globalDir = activeAgent.globalDir;
|
|
375
|
+
if (!exists(globalDir) || !isDir(globalDir)) return;
|
|
376
|
+
if (activeAgent.scanGlobalRoot) {
|
|
377
|
+
this.indexGenericDir(globalDir, "custom", activeAgent.name, true);
|
|
378
|
+
}
|
|
379
|
+
for (const sub of activeAgent.subdirs) {
|
|
380
|
+
const subDir = import_node_path.default.join(globalDir, sub);
|
|
381
|
+
if (exists(subDir) && isDir(subDir)) {
|
|
382
|
+
const sourceType = sub === "commands" ? "command" : "custom";
|
|
383
|
+
const sourceName = sub === "commands" ? "Commands" : activeAgent.name;
|
|
384
|
+
this.indexGenericDir(subDir, sourceType, sourceName, true);
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
indexGenericDir(dir, sourceType, sourceName, recursive) {
|
|
389
|
+
for (const name of readdir(dir)) {
|
|
390
|
+
if (name.startsWith(".")) continue;
|
|
391
|
+
const itemPath = import_node_path.default.join(dir, name);
|
|
392
|
+
if (isFile(itemPath) && this.hasMatchingExtension(name)) {
|
|
393
|
+
this.indexSkill(itemPath, sourceType, sourceName, dir);
|
|
394
|
+
} else if (recursive && isDir(itemPath)) {
|
|
395
|
+
this.indexGenericDir(itemPath, sourceType, sourceName, true);
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
indexProjectDir(projectDir) {
|
|
400
|
+
const configDir = import_node_path.default.join(projectDir, activeAgent.projectDirName);
|
|
401
|
+
const projectName = import_node_path.default.basename(projectDir);
|
|
402
|
+
if (activeAgent.id === "claude") {
|
|
403
|
+
const cmdsDir = import_node_path.default.join(configDir, "commands");
|
|
404
|
+
if (exists(cmdsDir) && isDir(cmdsDir)) {
|
|
405
|
+
for (const name of readdir(cmdsDir)) {
|
|
406
|
+
const filePath = import_node_path.default.join(cmdsDir, name);
|
|
407
|
+
if (isFile(filePath) && name.endsWith(".md")) {
|
|
408
|
+
this.indexSkill(filePath, "project", projectName, cmdsDir);
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
const skillsDir = import_node_path.default.join(configDir, "skills");
|
|
413
|
+
if (exists(skillsDir) && isDir(skillsDir)) {
|
|
414
|
+
for (const name of readdir(skillsDir)) {
|
|
415
|
+
const itemPath = import_node_path.default.join(skillsDir, name);
|
|
416
|
+
if (isDir(itemPath)) {
|
|
417
|
+
const skillFile = import_node_path.default.join(itemPath, "SKILL.md");
|
|
418
|
+
if (exists(skillFile)) {
|
|
419
|
+
this.indexSkill(skillFile, "project", projectName, itemPath);
|
|
420
|
+
}
|
|
421
|
+
} else if (isFile(itemPath) && itemPath.endsWith(".md")) {
|
|
422
|
+
this.indexSkill(itemPath, "project", projectName, skillsDir);
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
} else {
|
|
427
|
+
if (!exists(configDir) || !isDir(configDir)) return;
|
|
428
|
+
if (activeAgent.subdirs.length > 0) {
|
|
429
|
+
for (const sub of activeAgent.subdirs) {
|
|
430
|
+
const subDir = import_node_path.default.join(configDir, sub);
|
|
431
|
+
if (exists(subDir) && isDir(subDir)) {
|
|
432
|
+
const sourceType = sub === "commands" ? "command" : "project";
|
|
433
|
+
this.indexGenericDir(subDir, sourceType, projectName, true);
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
} else {
|
|
437
|
+
this.indexGenericDir(configDir, "project", projectName, false);
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
get(skillPath) {
|
|
442
|
+
return this.byPath.get(skillPath);
|
|
443
|
+
}
|
|
444
|
+
search(query) {
|
|
445
|
+
if (!query) return [];
|
|
446
|
+
const q = query.toLowerCase();
|
|
447
|
+
const results = [];
|
|
448
|
+
for (const skill of this.skills) {
|
|
449
|
+
const haystack = `${skill.name} ${skill.description} ${skill.content}`.toLowerCase();
|
|
450
|
+
if (!haystack.includes(q)) continue;
|
|
451
|
+
const idx = skill.content.toLowerCase().indexOf(q);
|
|
452
|
+
let snippet;
|
|
453
|
+
if (idx >= 0) {
|
|
454
|
+
const start = Math.max(0, idx - 80);
|
|
455
|
+
const end = Math.min(skill.content.length, idx + query.length + 80);
|
|
456
|
+
snippet = skill.content.slice(start, end).trim();
|
|
457
|
+
if (start > 0) snippet = "\u2026" + snippet;
|
|
458
|
+
if (end < skill.content.length) snippet += "\u2026";
|
|
459
|
+
} else {
|
|
460
|
+
snippet = skill.description;
|
|
461
|
+
}
|
|
462
|
+
results.push({
|
|
463
|
+
name: skill.name,
|
|
464
|
+
description: skill.description,
|
|
465
|
+
path: skill.path,
|
|
466
|
+
sourceType: skill.sourceType,
|
|
467
|
+
sourceName: skill.sourceName,
|
|
468
|
+
snippet,
|
|
469
|
+
structuralTags: skill.structuralTags
|
|
470
|
+
});
|
|
471
|
+
}
|
|
472
|
+
return results;
|
|
473
|
+
}
|
|
474
|
+
getGraph() {
|
|
475
|
+
const nodes = this.skills.map((s) => ({
|
|
476
|
+
id: s.path,
|
|
477
|
+
name: s.name,
|
|
478
|
+
sourceType: s.sourceType,
|
|
479
|
+
sourceName: s.sourceName,
|
|
480
|
+
wordCount: s.wordCount,
|
|
481
|
+
structuralTags: s.structuralTags
|
|
482
|
+
}));
|
|
483
|
+
const nameToPath = /* @__PURE__ */ new Map();
|
|
484
|
+
for (const s of this.skills) {
|
|
485
|
+
const refKey = `${s.sourceName.toLowerCase().replace(/ /g, "-")}:${s.name.toLowerCase()}`;
|
|
486
|
+
nameToPath.set(refKey, s.path);
|
|
487
|
+
nameToPath.set(s.name.toLowerCase(), s.path);
|
|
488
|
+
}
|
|
489
|
+
const edges = [];
|
|
490
|
+
const seen = /* @__PURE__ */ new Set();
|
|
491
|
+
for (const skill of this.skills) {
|
|
492
|
+
for (const ref of skill.crossReferences) {
|
|
493
|
+
const targetPath = nameToPath.get(ref.toLowerCase());
|
|
494
|
+
if (targetPath && targetPath !== skill.path) {
|
|
495
|
+
const key = `${skill.path}->${targetPath}`;
|
|
496
|
+
if (!seen.has(key)) {
|
|
497
|
+
seen.add(key);
|
|
498
|
+
edges.push({ source: skill.path, target: targetPath });
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
return { nodes, edges };
|
|
504
|
+
}
|
|
505
|
+
getHealth(skillPath) {
|
|
506
|
+
const skill = this.byPath.get(skillPath);
|
|
507
|
+
if (!skill) return null;
|
|
508
|
+
const ageDays = (Date.now() / 1e3 - skill.mtime) / 86400;
|
|
509
|
+
const completenessGaps = [];
|
|
510
|
+
if (!skill.description) completenessGaps.push("missing-description");
|
|
511
|
+
if (!skill.structuralTags.includes("has-examples")) completenessGaps.push("no-examples");
|
|
512
|
+
if (skill.wordCount < 50) completenessGaps.push("very-short");
|
|
513
|
+
return {
|
|
514
|
+
mtime: skill.mtime,
|
|
515
|
+
mtimeIso: new Date(skill.mtime * 1e3).toISOString(),
|
|
516
|
+
ageDays: Math.round(ageDays * 10) / 10,
|
|
517
|
+
wordCount: skill.wordCount,
|
|
518
|
+
completenessGaps,
|
|
519
|
+
toolCount: skill.toolReferences.length,
|
|
520
|
+
crossRefCount: skill.crossReferences.length,
|
|
521
|
+
structuralTags: skill.structuralTags
|
|
522
|
+
};
|
|
523
|
+
}
|
|
524
|
+
};
|
|
525
|
+
function getSources(index2) {
|
|
526
|
+
const sources = { plugins: [], custom: [], commands: [], projects: [] };
|
|
527
|
+
if (activeAgent.id === "claude") {
|
|
528
|
+
const pluginGroups = /* @__PURE__ */ new Map();
|
|
529
|
+
for (const s of index2.skills) {
|
|
530
|
+
if (s.sourceType !== "plugin") continue;
|
|
531
|
+
const group = pluginGroups.get(s.sourceName) || [];
|
|
532
|
+
group.push(s);
|
|
533
|
+
pluginGroups.set(s.sourceName, group);
|
|
534
|
+
}
|
|
535
|
+
for (const [name, skills] of pluginGroups) {
|
|
536
|
+
const skillsDir = import_node_path.default.dirname(skills[0].skillDir);
|
|
537
|
+
const version2 = import_node_path.default.basename(import_node_path.default.dirname(skillsDir));
|
|
538
|
+
sources.plugins.push({
|
|
539
|
+
name,
|
|
540
|
+
path: skillsDir,
|
|
541
|
+
count: skills.length,
|
|
542
|
+
version: version2,
|
|
543
|
+
skills: skills.map((s) => ({ name: s.name, path: s.path })).sort((a, b) => a.name.localeCompare(b.name))
|
|
544
|
+
});
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
const customSkills = index2.skills.filter((s) => s.sourceType === "custom");
|
|
548
|
+
if (customSkills.length > 0) {
|
|
549
|
+
const sourceName = activeAgent.id === "claude" ? "Custom Skills" : activeAgent.name;
|
|
550
|
+
const sourcePath = activeAgent.id === "claude" ? CUSTOM_SKILLS_DIR : activeAgent.globalDir;
|
|
551
|
+
sources.custom.push({
|
|
552
|
+
name: sourceName,
|
|
553
|
+
path: sourcePath,
|
|
554
|
+
count: customSkills.length,
|
|
555
|
+
files: customSkills.map((s) => ({ name: s.name, path: s.path })).sort((a, b) => a.name.localeCompare(b.name))
|
|
556
|
+
});
|
|
557
|
+
}
|
|
558
|
+
const commandSkills = index2.skills.filter((s) => s.sourceType === "command");
|
|
559
|
+
if (commandSkills.length > 0) {
|
|
560
|
+
sources.commands.push({
|
|
561
|
+
name: "Commands",
|
|
562
|
+
path: COMMANDS_DIR || import_node_path.default.join(activeAgent.globalDir, "commands"),
|
|
563
|
+
count: commandSkills.length,
|
|
564
|
+
files: commandSkills.map((s) => ({ name: s.name, path: s.path })).sort((a, b) => a.name.localeCompare(b.name))
|
|
565
|
+
});
|
|
566
|
+
}
|
|
567
|
+
for (const projectDir of index2.getProjectDirs()) {
|
|
568
|
+
const configDir = import_node_path.default.join(projectDir, activeAgent.projectDirName);
|
|
569
|
+
const projectSkills = index2.skills.filter((s) => s.path.startsWith(configDir + import_node_path.default.sep));
|
|
570
|
+
const commandCount = projectSkills.filter((s) => {
|
|
571
|
+
const rel = import_node_path.default.relative(configDir, s.path);
|
|
572
|
+
return rel.startsWith("commands" + import_node_path.default.sep);
|
|
573
|
+
}).length;
|
|
574
|
+
sources.projects.push({
|
|
575
|
+
name: import_node_path.default.basename(projectDir),
|
|
576
|
+
path: configDir,
|
|
577
|
+
projectDir,
|
|
578
|
+
commandCount,
|
|
579
|
+
skillCount: projectSkills.length - commandCount
|
|
580
|
+
});
|
|
581
|
+
}
|
|
582
|
+
return sources;
|
|
583
|
+
}
|
|
584
|
+
function indexedToSummary(skill) {
|
|
585
|
+
return {
|
|
586
|
+
name: skill.name,
|
|
587
|
+
description: skill.description,
|
|
588
|
+
path: skill.path,
|
|
589
|
+
filename: skill.filename,
|
|
590
|
+
skillDir: skill.skillDir
|
|
591
|
+
};
|
|
592
|
+
}
|
|
593
|
+
function getSkillsForSource(sourcePath, index2) {
|
|
594
|
+
const matched = index2.skills.filter((s) => s.path.startsWith(sourcePath + import_node_path.default.sep));
|
|
595
|
+
const skills = matched.map(indexedToSummary);
|
|
596
|
+
skills.sort((a, b) => a.name.localeCompare(b.name));
|
|
597
|
+
for (const s of skills) {
|
|
598
|
+
s.health = index2.getHealth(s.path) ?? void 0;
|
|
599
|
+
const indexed = index2.get(s.path);
|
|
600
|
+
if (indexed) {
|
|
601
|
+
s.toolReferences = indexed.toolReferences;
|
|
602
|
+
s.structuralTags = indexed.structuralTags;
|
|
603
|
+
}
|
|
604
|
+
}
|
|
605
|
+
return skills;
|
|
606
|
+
}
|
|
607
|
+
function buildTree(dirPath) {
|
|
608
|
+
const entries = [];
|
|
609
|
+
let items;
|
|
610
|
+
try {
|
|
611
|
+
items = import_node_fs.default.readdirSync(dirPath);
|
|
612
|
+
} catch {
|
|
613
|
+
return entries;
|
|
614
|
+
}
|
|
615
|
+
const dirs = items.filter((n) => !n.startsWith(".") && n !== "__pycache__" && isDir(import_node_path.default.join(dirPath, n))).sort();
|
|
616
|
+
const files = items.filter((n) => {
|
|
617
|
+
if (n.startsWith(".") && n !== ".claude-plugin") return false;
|
|
618
|
+
return isFile(import_node_path.default.join(dirPath, n));
|
|
619
|
+
}).sort();
|
|
620
|
+
for (const name of dirs) {
|
|
621
|
+
const full = import_node_path.default.join(dirPath, name);
|
|
622
|
+
entries.push({ name, path: full, type: "directory", children: buildTree(full) });
|
|
623
|
+
}
|
|
624
|
+
for (const name of files) {
|
|
625
|
+
const full = import_node_path.default.join(dirPath, name);
|
|
626
|
+
const stat = import_node_fs.default.statSync(full);
|
|
627
|
+
entries.push({ name, path: full, type: "file", size: stat.size, extension: import_node_path.default.extname(name) });
|
|
628
|
+
}
|
|
629
|
+
return entries;
|
|
630
|
+
}
|
|
631
|
+
function getAgentConfigs() {
|
|
632
|
+
return AGENT_CONFIGS;
|
|
633
|
+
}
|
|
634
|
+
function getWatchDirs() {
|
|
635
|
+
if (activeAgent.id === "claude") {
|
|
636
|
+
return [PLUGINS_DIR, CUSTOM_SKILLS_DIR, COMMANDS_DIR].filter((d) => exists(d) && isDir(d));
|
|
637
|
+
}
|
|
638
|
+
const dirs = [];
|
|
639
|
+
if (activeAgent.scanGlobalRoot && exists(activeAgent.globalDir) && isDir(activeAgent.globalDir)) {
|
|
640
|
+
dirs.push(activeAgent.globalDir);
|
|
641
|
+
}
|
|
642
|
+
for (const sub of activeAgent.subdirs) {
|
|
643
|
+
const subDir = import_node_path.default.join(activeAgent.globalDir, sub);
|
|
644
|
+
if (exists(subDir) && isDir(subDir)) dirs.push(subDir);
|
|
645
|
+
}
|
|
646
|
+
return dirs;
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
// src/server.ts
|
|
650
|
+
function extractWildcard(params) {
|
|
651
|
+
const val = params.path;
|
|
652
|
+
const raw = Array.isArray(val) ? val.join("/") : String(val);
|
|
653
|
+
return raw.startsWith("/") ? raw : "/" + raw;
|
|
654
|
+
}
|
|
655
|
+
function getReferenceFiles(skillPath) {
|
|
656
|
+
const skillDir = import_node_path2.default.dirname(skillPath);
|
|
657
|
+
const skillName = import_node_path2.default.basename(skillPath);
|
|
658
|
+
const refs = [];
|
|
659
|
+
let items;
|
|
660
|
+
try {
|
|
661
|
+
items = import_node_fs2.default.readdirSync(skillDir);
|
|
662
|
+
} catch {
|
|
663
|
+
return refs;
|
|
664
|
+
}
|
|
665
|
+
for (const name of items.sort()) {
|
|
666
|
+
if (name.startsWith(".")) continue;
|
|
667
|
+
const full = import_node_path2.default.join(skillDir, name);
|
|
668
|
+
try {
|
|
669
|
+
const stat = import_node_fs2.default.statSync(full);
|
|
670
|
+
if (stat.isFile() && name !== skillName && name.endsWith(".md")) {
|
|
671
|
+
refs.push({ name, path: full, type: "file" });
|
|
672
|
+
} else if (stat.isDirectory()) {
|
|
673
|
+
const children = [];
|
|
674
|
+
for (const sub of import_node_fs2.default.readdirSync(full).sort()) {
|
|
675
|
+
const subFull = import_node_path2.default.join(full, sub);
|
|
676
|
+
if (import_node_fs2.default.statSync(subFull).isFile() && sub.endsWith(".md")) {
|
|
677
|
+
children.push({ name: sub, path: subFull, type: "file" });
|
|
678
|
+
}
|
|
679
|
+
}
|
|
680
|
+
if (children.length > 0) {
|
|
681
|
+
refs.push({ name, path: full, type: "directory", children });
|
|
682
|
+
}
|
|
683
|
+
}
|
|
684
|
+
} catch {
|
|
685
|
+
continue;
|
|
686
|
+
}
|
|
687
|
+
}
|
|
688
|
+
return refs;
|
|
689
|
+
}
|
|
690
|
+
function isUnderAllowedRoot(resolvedPath, index2) {
|
|
691
|
+
const agent = index2.getActiveAgent();
|
|
692
|
+
const roots = [agent.globalDir, ...index2.getProjectDirs()];
|
|
693
|
+
return roots.some((root) => resolvedPath.startsWith(root + import_node_path2.default.sep) || resolvedPath === root);
|
|
694
|
+
}
|
|
695
|
+
function createApp(index2) {
|
|
696
|
+
const app2 = (0, import_express.default)();
|
|
697
|
+
const publicDir = import_node_path2.default.resolve(__dirname, "..", "public");
|
|
698
|
+
app2.get("/", (_req, res) => {
|
|
699
|
+
const html = import_node_fs2.default.readFileSync(import_node_path2.default.join(publicDir, "index.html"), "utf-8");
|
|
700
|
+
res.type("html").send(html);
|
|
701
|
+
});
|
|
702
|
+
app2.use(import_express.default.json());
|
|
703
|
+
app2.get("/api/agents", (_req, res) => {
|
|
704
|
+
const agents = getAgentConfigs().map((a) => ({
|
|
705
|
+
id: a.id,
|
|
706
|
+
name: a.name,
|
|
707
|
+
global_dir: a.globalDir,
|
|
708
|
+
project_dir_name: a.projectDirName
|
|
709
|
+
}));
|
|
710
|
+
const active = index2.getActiveAgent();
|
|
711
|
+
res.json({ agents, active_id: active.id });
|
|
712
|
+
});
|
|
713
|
+
app2.post("/api/agent", (req, res) => {
|
|
714
|
+
const agentId = req.body?.id;
|
|
715
|
+
if (!agentId) {
|
|
716
|
+
res.status(400).json({ error: "Missing agent id" });
|
|
717
|
+
return;
|
|
718
|
+
}
|
|
719
|
+
const configs = getAgentConfigs();
|
|
720
|
+
if (!configs.find((a) => a.id === agentId)) {
|
|
721
|
+
res.status(400).json({ error: `Unknown agent: ${agentId}` });
|
|
722
|
+
return;
|
|
723
|
+
}
|
|
724
|
+
index2.setAgent(agentId);
|
|
725
|
+
res.json({ ok: true, active_id: agentId, sources: getSources(index2) });
|
|
726
|
+
});
|
|
727
|
+
app2.get("/api/sources", (_req, res) => {
|
|
728
|
+
res.json(getSources(index2));
|
|
729
|
+
});
|
|
730
|
+
app2.post("/api/projects", (req, res) => {
|
|
731
|
+
const dir = req.body?.path;
|
|
732
|
+
if (!dir) {
|
|
733
|
+
res.status(400).json({ error: "Missing path" });
|
|
734
|
+
return;
|
|
735
|
+
}
|
|
736
|
+
const ok = index2.addProjectDir(dir);
|
|
737
|
+
if (!ok) {
|
|
738
|
+
const agent = index2.getActiveAgent();
|
|
739
|
+
res.status(400).json({ error: `No ${agent.projectDirName}/ directory found at that path` });
|
|
740
|
+
return;
|
|
741
|
+
}
|
|
742
|
+
res.json({ ok: true, sources: getSources(index2) });
|
|
743
|
+
});
|
|
744
|
+
app2.delete("/api/projects", (req, res) => {
|
|
745
|
+
const dir = req.body?.path;
|
|
746
|
+
if (!dir) {
|
|
747
|
+
res.status(400).json({ error: "Missing path" });
|
|
748
|
+
return;
|
|
749
|
+
}
|
|
750
|
+
index2.removeProjectDir(dir);
|
|
751
|
+
res.json({ ok: true, sources: getSources(index2) });
|
|
752
|
+
});
|
|
753
|
+
app2.get("/api/skills", (req, res) => {
|
|
754
|
+
const source = req.query.source;
|
|
755
|
+
if (!source) {
|
|
756
|
+
res.json([]);
|
|
757
|
+
return;
|
|
758
|
+
}
|
|
759
|
+
const skills = getSkillsForSource(source, index2).map((s) => ({
|
|
760
|
+
name: s.name,
|
|
761
|
+
description: s.description,
|
|
762
|
+
path: s.path,
|
|
763
|
+
filename: s.filename,
|
|
764
|
+
skill_dir: s.skillDir,
|
|
765
|
+
old: s.old,
|
|
766
|
+
health: s.health ? toSnakeHealth(s.health) : void 0,
|
|
767
|
+
tool_references: s.toolReferences,
|
|
768
|
+
structural_tags: s.structuralTags
|
|
769
|
+
}));
|
|
770
|
+
res.json(skills);
|
|
771
|
+
});
|
|
772
|
+
app2.get("/api/skill/*path", (req, res) => {
|
|
773
|
+
const skillPath = import_node_path2.default.resolve(extractWildcard(req.params));
|
|
774
|
+
if (!isUnderAllowedRoot(skillPath, index2)) {
|
|
775
|
+
res.status(403).json({ error: "Access denied" });
|
|
776
|
+
return;
|
|
777
|
+
}
|
|
778
|
+
if (!import_node_fs2.default.existsSync(skillPath)) {
|
|
779
|
+
res.status(404).json({ error: "Skill not found" });
|
|
780
|
+
return;
|
|
781
|
+
}
|
|
782
|
+
try {
|
|
783
|
+
const content = import_node_fs2.default.readFileSync(skillPath, "utf-8");
|
|
784
|
+
const frontmatter = parseFrontmatter(content);
|
|
785
|
+
const frontmatterRaw = extractFrontmatterRaw(content);
|
|
786
|
+
const body = stripFrontmatter(content);
|
|
787
|
+
const response = {
|
|
788
|
+
name: frontmatter.name || (import_node_path2.default.basename(skillPath) === "SKILL.md" ? import_node_path2.default.basename(import_node_path2.default.dirname(skillPath)) : import_node_path2.default.basename(skillPath, import_node_path2.default.extname(skillPath))),
|
|
789
|
+
description: frontmatter.description || "",
|
|
790
|
+
frontmatter,
|
|
791
|
+
frontmatter_raw: frontmatterRaw,
|
|
792
|
+
content: body,
|
|
793
|
+
path: skillPath,
|
|
794
|
+
references: getReferenceFiles(skillPath)
|
|
795
|
+
};
|
|
796
|
+
const indexed = index2.get(skillPath);
|
|
797
|
+
if (indexed) {
|
|
798
|
+
response.cross_references = indexed.crossReferences;
|
|
799
|
+
response.tool_references = indexed.toolReferences;
|
|
800
|
+
response.structural_tags = indexed.structuralTags;
|
|
801
|
+
response.word_count = indexed.wordCount;
|
|
802
|
+
response.health = toSnakeHealth(index2.getHealth(skillPath));
|
|
803
|
+
response.skill_dir = import_node_path2.default.basename(skillPath) === "SKILL.md" ? import_node_path2.default.dirname(skillPath) : null;
|
|
804
|
+
}
|
|
805
|
+
res.json(response);
|
|
806
|
+
} catch (e) {
|
|
807
|
+
res.status(500).json({ error: e instanceof Error ? e.message : String(e) });
|
|
808
|
+
}
|
|
809
|
+
});
|
|
810
|
+
app2.get("/api/reference/*path", (req, res) => {
|
|
811
|
+
const refPath = import_node_path2.default.resolve(extractWildcard(req.params));
|
|
812
|
+
if (!isUnderAllowedRoot(refPath, index2)) {
|
|
813
|
+
res.status(403).json({ error: "Access denied" });
|
|
814
|
+
return;
|
|
815
|
+
}
|
|
816
|
+
if (!import_node_fs2.default.existsSync(refPath)) {
|
|
817
|
+
res.status(404).json({ error: "Reference not found" });
|
|
818
|
+
return;
|
|
819
|
+
}
|
|
820
|
+
try {
|
|
821
|
+
const content = import_node_fs2.default.readFileSync(refPath, "utf-8");
|
|
822
|
+
res.json({ name: import_node_path2.default.basename(refPath), content, path: refPath });
|
|
823
|
+
} catch (e) {
|
|
824
|
+
res.status(500).json({ error: e instanceof Error ? e.message : String(e) });
|
|
825
|
+
}
|
|
826
|
+
});
|
|
827
|
+
app2.get("/api/search", (req, res) => {
|
|
828
|
+
const query = req.query.q || "";
|
|
829
|
+
const results = index2.search(query).map((r) => ({
|
|
830
|
+
name: r.name,
|
|
831
|
+
description: r.description,
|
|
832
|
+
path: r.path,
|
|
833
|
+
source_type: r.sourceType,
|
|
834
|
+
source_name: r.sourceName,
|
|
835
|
+
snippet: r.snippet,
|
|
836
|
+
structural_tags: r.structuralTags
|
|
837
|
+
}));
|
|
838
|
+
res.json(results);
|
|
839
|
+
});
|
|
840
|
+
app2.get("/api/graph", (_req, res) => {
|
|
841
|
+
const graph = index2.getGraph();
|
|
842
|
+
res.json({
|
|
843
|
+
nodes: graph.nodes.map((n) => ({
|
|
844
|
+
id: n.id,
|
|
845
|
+
name: n.name,
|
|
846
|
+
source_type: n.sourceType,
|
|
847
|
+
source_name: n.sourceName,
|
|
848
|
+
word_count: n.wordCount,
|
|
849
|
+
structural_tags: n.structuralTags
|
|
850
|
+
})),
|
|
851
|
+
edges: graph.edges
|
|
852
|
+
});
|
|
853
|
+
});
|
|
854
|
+
app2.get("/api/skill-tree/*path", (req, res) => {
|
|
855
|
+
const dirPath = import_node_path2.default.resolve(extractWildcard(req.params));
|
|
856
|
+
if (!isUnderAllowedRoot(dirPath, index2)) {
|
|
857
|
+
res.status(403).json({ error: "Access denied" });
|
|
858
|
+
return;
|
|
859
|
+
}
|
|
860
|
+
if (!import_node_fs2.default.existsSync(dirPath) || !import_node_fs2.default.statSync(dirPath).isDirectory()) {
|
|
861
|
+
res.status(404).json({ error: "Directory not found" });
|
|
862
|
+
return;
|
|
863
|
+
}
|
|
864
|
+
res.json({ root: dirPath, tree: buildTree(dirPath) });
|
|
865
|
+
});
|
|
866
|
+
app2.get("/api/health/*path", (req, res) => {
|
|
867
|
+
const skillPath = import_node_path2.default.resolve(extractWildcard(req.params));
|
|
868
|
+
if (!isUnderAllowedRoot(skillPath, index2)) {
|
|
869
|
+
res.status(403).json({ error: "Access denied" });
|
|
870
|
+
return;
|
|
871
|
+
}
|
|
872
|
+
const health = index2.getHealth(skillPath);
|
|
873
|
+
if (!health) {
|
|
874
|
+
res.status(404).json({ error: "Skill not found in index" });
|
|
875
|
+
return;
|
|
876
|
+
}
|
|
877
|
+
res.json(toSnakeHealth(health));
|
|
878
|
+
});
|
|
879
|
+
return app2;
|
|
880
|
+
}
|
|
881
|
+
function toSnakeHealth(h) {
|
|
882
|
+
return {
|
|
883
|
+
mtime: h.mtime,
|
|
884
|
+
mtime_iso: h.mtimeIso,
|
|
885
|
+
age_days: h.ageDays,
|
|
886
|
+
word_count: h.wordCount,
|
|
887
|
+
completeness_gaps: h.completenessGaps,
|
|
888
|
+
tool_count: h.toolCount,
|
|
889
|
+
cross_ref_count: h.crossRefCount,
|
|
890
|
+
structural_tags: h.structuralTags
|
|
891
|
+
};
|
|
892
|
+
}
|
|
893
|
+
|
|
894
|
+
// src/watcher.ts
|
|
895
|
+
var import_ws = require("ws");
|
|
896
|
+
var import_chokidar = __toESM(require("chokidar"));
|
|
897
|
+
function setupWebSocket(server2) {
|
|
898
|
+
const wss2 = new import_ws.WebSocketServer({ server: server2, path: "/ws" });
|
|
899
|
+
const clients = /* @__PURE__ */ new Set();
|
|
900
|
+
wss2.on("error", (err) => {
|
|
901
|
+
console.error("WebSocket server error:", err.message);
|
|
902
|
+
});
|
|
903
|
+
wss2.on("connection", (ws) => {
|
|
904
|
+
clients.add(ws);
|
|
905
|
+
ws.on("close", () => clients.delete(ws));
|
|
906
|
+
ws.on("error", () => clients.delete(ws));
|
|
907
|
+
});
|
|
908
|
+
function broadcast2(msg) {
|
|
909
|
+
const data = JSON.stringify(msg);
|
|
910
|
+
for (const ws of clients) {
|
|
911
|
+
try {
|
|
912
|
+
ws.send(data);
|
|
913
|
+
} catch {
|
|
914
|
+
clients.delete(ws);
|
|
915
|
+
}
|
|
916
|
+
}
|
|
917
|
+
}
|
|
918
|
+
return { broadcast: broadcast2, wss: wss2 };
|
|
919
|
+
}
|
|
920
|
+
function setupWatcher(index2, broadcast2) {
|
|
921
|
+
let watchedDirs = getWatchDirs();
|
|
922
|
+
const watcher2 = import_chokidar.default.watch(watchedDirs, {
|
|
923
|
+
persistent: true,
|
|
924
|
+
ignoreInitial: true,
|
|
925
|
+
awaitWriteFinish: { stabilityThreshold: 300 }
|
|
926
|
+
});
|
|
927
|
+
let debounceTimer = null;
|
|
928
|
+
function handleChange() {
|
|
929
|
+
if (debounceTimer) clearTimeout(debounceTimer);
|
|
930
|
+
debounceTimer = setTimeout(() => {
|
|
931
|
+
index2.build();
|
|
932
|
+
broadcast2({ type: "skill_changed" });
|
|
933
|
+
}, 500);
|
|
934
|
+
}
|
|
935
|
+
watcher2.on("change", handleChange);
|
|
936
|
+
watcher2.on("add", handleChange);
|
|
937
|
+
watcher2.on("unlink", handleChange);
|
|
938
|
+
watcher2.on("error", (err) => console.error("Watcher error:", err));
|
|
939
|
+
if (watchedDirs.length > 0) {
|
|
940
|
+
console.log(`Watching ${watchedDirs.length} directories for changes`);
|
|
941
|
+
}
|
|
942
|
+
return {
|
|
943
|
+
add: (dir) => watcher2.add(dir),
|
|
944
|
+
unwatch: (dir) => watcher2.unwatch(dir),
|
|
945
|
+
close: () => watcher2.close(),
|
|
946
|
+
switchAgent: () => {
|
|
947
|
+
if (watchedDirs.length > 0) watcher2.unwatch(watchedDirs);
|
|
948
|
+
watchedDirs = getWatchDirs();
|
|
949
|
+
if (watchedDirs.length > 0) {
|
|
950
|
+
watcher2.add(watchedDirs);
|
|
951
|
+
console.log(`Watching ${watchedDirs.length} directories for changes`);
|
|
952
|
+
}
|
|
953
|
+
}
|
|
954
|
+
};
|
|
955
|
+
}
|
|
956
|
+
|
|
957
|
+
// src/cli.ts
|
|
958
|
+
var version = JSON.parse((0, import_node_fs3.readFileSync)(import_node_path3.default.join(__dirname, "..", "package.json"), "utf-8")).version;
|
|
959
|
+
var args = process.argv.slice(2);
|
|
960
|
+
if (args.includes("--help") || args.includes("-h")) {
|
|
961
|
+
console.log(`skill-viewer v${version}
|
|
962
|
+
`);
|
|
963
|
+
console.log(`Browse and inspect Claude Code skills installed in ~/.claude/
|
|
964
|
+
`);
|
|
965
|
+
console.log(`Usage: skill-viewer [options]
|
|
966
|
+
`);
|
|
967
|
+
console.log(`Options:`);
|
|
968
|
+
console.log(` -h, --help Show this help message`);
|
|
969
|
+
console.log(` -v, --version Show version number
|
|
970
|
+
`);
|
|
971
|
+
console.log(`Environment variables:`);
|
|
972
|
+
console.log(` PORT Server port (default: 8080)`);
|
|
973
|
+
process.exit(0);
|
|
974
|
+
}
|
|
975
|
+
if (args.includes("--version") || args.includes("-v")) {
|
|
976
|
+
console.log(version);
|
|
977
|
+
process.exit(0);
|
|
978
|
+
}
|
|
979
|
+
var PORT = parseInt(process.env.PORT || "8080", 10);
|
|
980
|
+
var index = new SkillIndex();
|
|
981
|
+
index.build();
|
|
982
|
+
if (!(0, import_node_fs3.existsSync)(CLAUDE_DIR)) {
|
|
983
|
+
console.warn(`
|
|
984
|
+
Warning: ${CLAUDE_DIR} does not exist.`);
|
|
985
|
+
console.warn(`Install Claude Code and run it once to create this directory.
|
|
986
|
+
`);
|
|
987
|
+
} else if (index.skills.length === 0) {
|
|
988
|
+
console.warn(`
|
|
989
|
+
No skills found. Install a plugin or add custom skills to ~/.claude/skills/
|
|
990
|
+
`);
|
|
991
|
+
} else {
|
|
992
|
+
console.log(`Indexed ${index.skills.length} skills`);
|
|
993
|
+
}
|
|
994
|
+
var app = createApp(index);
|
|
995
|
+
var server = import_node_http.default.createServer(app);
|
|
996
|
+
var { broadcast, wss } = setupWebSocket(server);
|
|
997
|
+
var watcher = setupWatcher(index, broadcast);
|
|
998
|
+
var originalAdd = index.addProjectDir.bind(index);
|
|
999
|
+
var originalRemove = index.removeProjectDir.bind(index);
|
|
1000
|
+
var originalSetAgent = index.setAgent.bind(index);
|
|
1001
|
+
index.addProjectDir = (dir) => {
|
|
1002
|
+
const ok = originalAdd(dir);
|
|
1003
|
+
if (ok) watcher.add(dir);
|
|
1004
|
+
return ok;
|
|
1005
|
+
};
|
|
1006
|
+
index.removeProjectDir = (dir) => {
|
|
1007
|
+
originalRemove(dir);
|
|
1008
|
+
watcher.unwatch(dir);
|
|
1009
|
+
};
|
|
1010
|
+
index.setAgent = (agentId) => {
|
|
1011
|
+
originalSetAgent(agentId);
|
|
1012
|
+
watcher.switchAgent();
|
|
1013
|
+
};
|
|
1014
|
+
server.listen(PORT, () => {
|
|
1015
|
+
console.log(`Skill Viewer running at http://localhost:${PORT}`);
|
|
1016
|
+
});
|
|
1017
|
+
server.on("error", (err) => {
|
|
1018
|
+
if (err.code === "EADDRINUSE") {
|
|
1019
|
+
console.error(`
|
|
1020
|
+
Error: Port ${PORT} is already in use.
|
|
1021
|
+
`);
|
|
1022
|
+
console.error(`Try one of:`);
|
|
1023
|
+
console.error(` PORT=3000 npx skill-viewer`);
|
|
1024
|
+
console.error(` lsof -ti:${PORT} | xargs kill
|
|
1025
|
+
`);
|
|
1026
|
+
process.exit(1);
|
|
1027
|
+
}
|
|
1028
|
+
throw err;
|
|
1029
|
+
});
|
|
1030
|
+
var shuttingDown = false;
|
|
1031
|
+
function shutdown() {
|
|
1032
|
+
if (shuttingDown) {
|
|
1033
|
+
process.exit(0);
|
|
1034
|
+
}
|
|
1035
|
+
shuttingDown = true;
|
|
1036
|
+
console.log("Shutting down...");
|
|
1037
|
+
wss.clients.forEach((ws) => ws.terminate());
|
|
1038
|
+
wss.close();
|
|
1039
|
+
server.close();
|
|
1040
|
+
watcher.close().finally(() => process.exit(0));
|
|
1041
|
+
}
|
|
1042
|
+
process.on("SIGTERM", shutdown);
|
|
1043
|
+
process.on("SIGINT", shutdown);
|