skill-tree 0.0.1 → 0.1.1
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/README.md +324 -1
- package/dist/chunk-5QGJJCVX.mjs +15392 -0
- package/dist/chunk-Q6SFFUDU.mjs +15874 -0
- package/dist/cli/index.d.mts +1 -0
- package/dist/cli/index.d.ts +1 -0
- package/dist/cli/index.js +15285 -0
- package/dist/cli/index.mjs +3569 -0
- package/dist/index.d.mts +7112 -4
- package/dist/index.d.ts +7112 -4
- package/dist/index.js +16005 -3
- package/dist/index.mjs +248 -3
- package/package.json +32 -6
|
@@ -0,0 +1,3569 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
FilesystemStorageAdapter,
|
|
4
|
+
GitSyncAdapter,
|
|
5
|
+
SkillBank,
|
|
6
|
+
SkillGraphServer,
|
|
7
|
+
SkillTreeMcpServer,
|
|
8
|
+
VERSION,
|
|
9
|
+
createDefaultSyncConfig
|
|
10
|
+
} from "../chunk-Q6SFFUDU.mjs";
|
|
11
|
+
|
|
12
|
+
// src/cli/index.ts
|
|
13
|
+
import { Command as Command24 } from "commander";
|
|
14
|
+
|
|
15
|
+
// src/config/types.ts
|
|
16
|
+
var DEFAULT_CONFIG = {
|
|
17
|
+
storage: {
|
|
18
|
+
type: "sqlite",
|
|
19
|
+
path: "~/.skill-tree/skills.db"
|
|
20
|
+
},
|
|
21
|
+
indexer: {
|
|
22
|
+
sources: [],
|
|
23
|
+
batch_size: 10,
|
|
24
|
+
min_confidence: 0.7
|
|
25
|
+
},
|
|
26
|
+
matching: {
|
|
27
|
+
embedding_model: "text-embedding-3-small",
|
|
28
|
+
similarity_threshold: 0.8
|
|
29
|
+
},
|
|
30
|
+
sync: {
|
|
31
|
+
auto_check: false,
|
|
32
|
+
check_interval_hours: 24,
|
|
33
|
+
conflict_resolution: "prompt"
|
|
34
|
+
},
|
|
35
|
+
cli: {
|
|
36
|
+
output_format: "table",
|
|
37
|
+
color: true,
|
|
38
|
+
quiet: false
|
|
39
|
+
}
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
// src/config/loader.ts
|
|
43
|
+
import * as fs from "fs";
|
|
44
|
+
import * as path from "path";
|
|
45
|
+
import * as os from "os";
|
|
46
|
+
var ENV_MAPPINGS = {
|
|
47
|
+
GITHUB_TOKEN: "indexer.github_token",
|
|
48
|
+
ANTHROPIC_API_KEY: "indexer.anthropic_key",
|
|
49
|
+
SKILL_TREE_STORAGE_TYPE: "storage.type",
|
|
50
|
+
SKILL_TREE_STORAGE_PATH: "storage.path",
|
|
51
|
+
SKILL_TREE_OUTPUT_FORMAT: "cli.output_format",
|
|
52
|
+
SKILL_TREE_QUIET: "cli.quiet",
|
|
53
|
+
SKILL_TREE_NO_COLOR: "cli.color"
|
|
54
|
+
};
|
|
55
|
+
function getConfigDir() {
|
|
56
|
+
return path.join(os.homedir(), ".skill-tree");
|
|
57
|
+
}
|
|
58
|
+
function getConfigPath() {
|
|
59
|
+
return path.join(getConfigDir(), "config.yaml");
|
|
60
|
+
}
|
|
61
|
+
function expandPath(filePath) {
|
|
62
|
+
if (filePath.startsWith("~/")) {
|
|
63
|
+
return path.join(os.homedir(), filePath.slice(2));
|
|
64
|
+
}
|
|
65
|
+
if (filePath.startsWith("~")) {
|
|
66
|
+
return path.join(os.homedir(), filePath.slice(1));
|
|
67
|
+
}
|
|
68
|
+
return filePath;
|
|
69
|
+
}
|
|
70
|
+
function substituteEnvVars(value) {
|
|
71
|
+
let result = value.replace(/\$\{([^}]+)\}/g, (_, varName) => {
|
|
72
|
+
return process.env[varName] || "";
|
|
73
|
+
});
|
|
74
|
+
result = result.replace(/\$([A-Z_][A-Z0-9_]*)/gi, (_, varName) => {
|
|
75
|
+
return process.env[varName] || "";
|
|
76
|
+
});
|
|
77
|
+
return result;
|
|
78
|
+
}
|
|
79
|
+
function substituteEnvVarsInObject(obj) {
|
|
80
|
+
if (typeof obj === "string") {
|
|
81
|
+
return substituteEnvVars(obj);
|
|
82
|
+
}
|
|
83
|
+
if (Array.isArray(obj)) {
|
|
84
|
+
return obj.map((item) => substituteEnvVarsInObject(item));
|
|
85
|
+
}
|
|
86
|
+
if (obj !== null && typeof obj === "object") {
|
|
87
|
+
const result = {};
|
|
88
|
+
for (const [key, value] of Object.entries(obj)) {
|
|
89
|
+
result[key] = substituteEnvVarsInObject(value);
|
|
90
|
+
}
|
|
91
|
+
return result;
|
|
92
|
+
}
|
|
93
|
+
return obj;
|
|
94
|
+
}
|
|
95
|
+
function setNestedProperty(obj, path6, value) {
|
|
96
|
+
const parts = path6.split(".");
|
|
97
|
+
let current = obj;
|
|
98
|
+
for (let i = 0; i < parts.length - 1; i++) {
|
|
99
|
+
const part = parts[i];
|
|
100
|
+
if (!(part in current)) {
|
|
101
|
+
current[part] = {};
|
|
102
|
+
}
|
|
103
|
+
current = current[part];
|
|
104
|
+
}
|
|
105
|
+
current[parts[parts.length - 1]] = value;
|
|
106
|
+
}
|
|
107
|
+
function deepMerge(target, source) {
|
|
108
|
+
const result = { ...target };
|
|
109
|
+
for (const key of Object.keys(source)) {
|
|
110
|
+
const sourceValue = source[key];
|
|
111
|
+
const targetValue = result[key];
|
|
112
|
+
if (sourceValue !== void 0 && typeof sourceValue === "object" && sourceValue !== null && !Array.isArray(sourceValue) && typeof targetValue === "object" && targetValue !== null && !Array.isArray(targetValue)) {
|
|
113
|
+
result[key] = deepMerge(
|
|
114
|
+
targetValue,
|
|
115
|
+
sourceValue
|
|
116
|
+
);
|
|
117
|
+
} else if (sourceValue !== void 0) {
|
|
118
|
+
result[key] = sourceValue;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
return result;
|
|
122
|
+
}
|
|
123
|
+
function parseSimpleYaml(content) {
|
|
124
|
+
const result = {};
|
|
125
|
+
const lines = content.split("\n");
|
|
126
|
+
const stack = [{ indent: -1, obj: result }];
|
|
127
|
+
for (const line of lines) {
|
|
128
|
+
const trimmed = line.trim();
|
|
129
|
+
if (!trimmed || trimmed.startsWith("#")) {
|
|
130
|
+
continue;
|
|
131
|
+
}
|
|
132
|
+
const indent = line.search(/\S/);
|
|
133
|
+
while (stack.length > 1 && stack[stack.length - 1].indent >= indent) {
|
|
134
|
+
stack.pop();
|
|
135
|
+
}
|
|
136
|
+
const parent = stack[stack.length - 1].obj;
|
|
137
|
+
const colonIndex = trimmed.indexOf(":");
|
|
138
|
+
if (colonIndex === -1) continue;
|
|
139
|
+
const key = trimmed.slice(0, colonIndex).trim();
|
|
140
|
+
let value = trimmed.slice(colonIndex + 1).trim();
|
|
141
|
+
if (value === "") {
|
|
142
|
+
const newObj = {};
|
|
143
|
+
parent[key] = newObj;
|
|
144
|
+
stack.push({ indent, obj: newObj });
|
|
145
|
+
} else if (value.startsWith("[") && value.endsWith("]")) {
|
|
146
|
+
const items = value.slice(1, -1).split(",").map((s) => s.trim()).filter(Boolean);
|
|
147
|
+
parent[key] = items.map((item) => {
|
|
148
|
+
if (item.startsWith('"') && item.endsWith('"') || item.startsWith("'") && item.endsWith("'")) {
|
|
149
|
+
return item.slice(1, -1);
|
|
150
|
+
}
|
|
151
|
+
if (item === "true") return true;
|
|
152
|
+
if (item === "false") return false;
|
|
153
|
+
const num = Number(item);
|
|
154
|
+
if (!isNaN(num)) return num;
|
|
155
|
+
return item;
|
|
156
|
+
});
|
|
157
|
+
} else {
|
|
158
|
+
if (value.startsWith('"') && value.endsWith('"') || value.startsWith("'") && value.endsWith("'")) {
|
|
159
|
+
value = value.slice(1, -1);
|
|
160
|
+
}
|
|
161
|
+
if (value === "true") {
|
|
162
|
+
parent[key] = true;
|
|
163
|
+
} else if (value === "false") {
|
|
164
|
+
parent[key] = false;
|
|
165
|
+
} else {
|
|
166
|
+
const num = Number(value);
|
|
167
|
+
if (!isNaN(num) && value !== "") {
|
|
168
|
+
parent[key] = num;
|
|
169
|
+
} else {
|
|
170
|
+
parent[key] = value;
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
return result;
|
|
176
|
+
}
|
|
177
|
+
function loadConfigFromFile(configPath) {
|
|
178
|
+
const expandedPath = expandPath(configPath);
|
|
179
|
+
if (!fs.existsSync(expandedPath)) {
|
|
180
|
+
return null;
|
|
181
|
+
}
|
|
182
|
+
try {
|
|
183
|
+
const content = fs.readFileSync(expandedPath, "utf-8");
|
|
184
|
+
const parsed = parseSimpleYaml(content);
|
|
185
|
+
return substituteEnvVarsInObject(parsed);
|
|
186
|
+
} catch (error) {
|
|
187
|
+
console.error(`Warning: Failed to parse config file ${configPath}:`, error);
|
|
188
|
+
return null;
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
function loadConfigFromEnv() {
|
|
192
|
+
const config2 = {};
|
|
193
|
+
for (const [envVar, configPath] of Object.entries(ENV_MAPPINGS)) {
|
|
194
|
+
const value = process.env[envVar];
|
|
195
|
+
if (value !== void 0) {
|
|
196
|
+
if (envVar === "SKILL_TREE_NO_COLOR") {
|
|
197
|
+
setNestedProperty(config2, configPath, value !== "true" && value !== "1");
|
|
198
|
+
} else if (envVar === "SKILL_TREE_QUIET") {
|
|
199
|
+
setNestedProperty(config2, configPath, value === "true" || value === "1");
|
|
200
|
+
} else {
|
|
201
|
+
setNestedProperty(config2, configPath, value);
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
return config2;
|
|
206
|
+
}
|
|
207
|
+
var ConfigLoader = class {
|
|
208
|
+
constructor(configPath) {
|
|
209
|
+
this.loaded = false;
|
|
210
|
+
this.configPath = configPath || getConfigPath();
|
|
211
|
+
this.config = { ...DEFAULT_CONFIG };
|
|
212
|
+
}
|
|
213
|
+
/**
|
|
214
|
+
* Load configuration from all sources
|
|
215
|
+
* Priority (highest to lowest):
|
|
216
|
+
* 1. Environment variables
|
|
217
|
+
* 2. Config file
|
|
218
|
+
* 3. Default values
|
|
219
|
+
*/
|
|
220
|
+
load() {
|
|
221
|
+
if (this.loaded) {
|
|
222
|
+
return this.config;
|
|
223
|
+
}
|
|
224
|
+
let config2 = deepMerge({}, DEFAULT_CONFIG);
|
|
225
|
+
const fileConfig = loadConfigFromFile(this.configPath);
|
|
226
|
+
if (fileConfig) {
|
|
227
|
+
config2 = deepMerge(config2, fileConfig);
|
|
228
|
+
}
|
|
229
|
+
const envConfig = loadConfigFromEnv();
|
|
230
|
+
config2 = deepMerge(config2, envConfig);
|
|
231
|
+
config2.storage.path = expandPath(config2.storage.path);
|
|
232
|
+
this.config = config2;
|
|
233
|
+
this.loaded = true;
|
|
234
|
+
return this.config;
|
|
235
|
+
}
|
|
236
|
+
/**
|
|
237
|
+
* Get current configuration
|
|
238
|
+
*/
|
|
239
|
+
getConfig() {
|
|
240
|
+
if (!this.loaded) {
|
|
241
|
+
return this.load();
|
|
242
|
+
}
|
|
243
|
+
return this.config;
|
|
244
|
+
}
|
|
245
|
+
/**
|
|
246
|
+
* Override configuration with partial values
|
|
247
|
+
*/
|
|
248
|
+
override(overrides) {
|
|
249
|
+
this.config = deepMerge(this.getConfig(), overrides);
|
|
250
|
+
return this.config;
|
|
251
|
+
}
|
|
252
|
+
/**
|
|
253
|
+
* Get a specific config value by path
|
|
254
|
+
*/
|
|
255
|
+
get(path6) {
|
|
256
|
+
const parts = path6.split(".");
|
|
257
|
+
let current = this.getConfig();
|
|
258
|
+
for (const part of parts) {
|
|
259
|
+
if (current === null || typeof current !== "object") {
|
|
260
|
+
return void 0;
|
|
261
|
+
}
|
|
262
|
+
current = current[part];
|
|
263
|
+
}
|
|
264
|
+
return current;
|
|
265
|
+
}
|
|
266
|
+
/**
|
|
267
|
+
* Check if config file exists
|
|
268
|
+
*/
|
|
269
|
+
configFileExists() {
|
|
270
|
+
return fs.existsSync(expandPath(this.configPath));
|
|
271
|
+
}
|
|
272
|
+
/**
|
|
273
|
+
* Create default config file
|
|
274
|
+
*/
|
|
275
|
+
createDefaultConfigFile() {
|
|
276
|
+
const configDir = path.dirname(expandPath(this.configPath));
|
|
277
|
+
if (!fs.existsSync(configDir)) {
|
|
278
|
+
fs.mkdirSync(configDir, { recursive: true });
|
|
279
|
+
}
|
|
280
|
+
const configContent = `# Skill Tree Configuration
|
|
281
|
+
# Generated by skill-tree
|
|
282
|
+
|
|
283
|
+
storage:
|
|
284
|
+
type: sqlite
|
|
285
|
+
path: ~/.skill-tree/skills.db
|
|
286
|
+
|
|
287
|
+
indexer:
|
|
288
|
+
# GitHub token for API access (or use GITHUB_TOKEN env var)
|
|
289
|
+
github_token: \${GITHUB_TOKEN}
|
|
290
|
+
# Anthropic API key for classification (or use ANTHROPIC_API_KEY env var)
|
|
291
|
+
anthropic_key: \${ANTHROPIC_API_KEY}
|
|
292
|
+
sources: []
|
|
293
|
+
batch_size: 10
|
|
294
|
+
min_confidence: 0.7
|
|
295
|
+
|
|
296
|
+
matching:
|
|
297
|
+
embedding_model: text-embedding-3-small
|
|
298
|
+
similarity_threshold: 0.8
|
|
299
|
+
|
|
300
|
+
sync:
|
|
301
|
+
auto_check: false
|
|
302
|
+
check_interval_hours: 24
|
|
303
|
+
conflict_resolution: prompt
|
|
304
|
+
|
|
305
|
+
cli:
|
|
306
|
+
output_format: table
|
|
307
|
+
color: true
|
|
308
|
+
quiet: false
|
|
309
|
+
`;
|
|
310
|
+
fs.writeFileSync(expandPath(this.configPath), configContent);
|
|
311
|
+
}
|
|
312
|
+
};
|
|
313
|
+
var globalConfig = null;
|
|
314
|
+
function getConfigLoader(configPath) {
|
|
315
|
+
if (!globalConfig) {
|
|
316
|
+
globalConfig = new ConfigLoader(configPath);
|
|
317
|
+
}
|
|
318
|
+
return globalConfig;
|
|
319
|
+
}
|
|
320
|
+
function loadConfig(configPath) {
|
|
321
|
+
return getConfigLoader(configPath).load();
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
// src/cli/commands/list.ts
|
|
325
|
+
import { Command } from "commander";
|
|
326
|
+
|
|
327
|
+
// src/cli/utils/paths.ts
|
|
328
|
+
import * as fs2 from "fs";
|
|
329
|
+
import * as path2 from "path";
|
|
330
|
+
import * as os2 from "os";
|
|
331
|
+
var DEFAULT_PATHS = [
|
|
332
|
+
".claude/skills",
|
|
333
|
+
// Project local (Claude)
|
|
334
|
+
".agent/skills",
|
|
335
|
+
// Project local (universal)
|
|
336
|
+
"~/.claude/skills",
|
|
337
|
+
// User global (Claude)
|
|
338
|
+
"~/.agent/skills"
|
|
339
|
+
// User global (universal)
|
|
340
|
+
];
|
|
341
|
+
function expandHome(p) {
|
|
342
|
+
if (p.startsWith("~/")) {
|
|
343
|
+
return path2.join(os2.homedir(), p.slice(2));
|
|
344
|
+
}
|
|
345
|
+
return p;
|
|
346
|
+
}
|
|
347
|
+
function dirExists(p) {
|
|
348
|
+
try {
|
|
349
|
+
return fs2.statSync(p).isDirectory();
|
|
350
|
+
} catch {
|
|
351
|
+
return false;
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
function resolveSkillPath(explicitPath) {
|
|
355
|
+
if (explicitPath) {
|
|
356
|
+
const resolved = expandHome(explicitPath);
|
|
357
|
+
return path2.resolve(resolved);
|
|
358
|
+
}
|
|
359
|
+
const envPath = process.env.SKILL_TREE_PATH;
|
|
360
|
+
if (envPath) {
|
|
361
|
+
return path2.resolve(expandHome(envPath));
|
|
362
|
+
}
|
|
363
|
+
for (const defaultPath of DEFAULT_PATHS) {
|
|
364
|
+
const resolved = path2.resolve(expandHome(defaultPath));
|
|
365
|
+
if (dirExists(resolved)) {
|
|
366
|
+
return resolved;
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
return path2.resolve(".claude/skills");
|
|
370
|
+
}
|
|
371
|
+
function ensureDir(dir) {
|
|
372
|
+
if (!dirExists(dir)) {
|
|
373
|
+
fs2.mkdirSync(dir, { recursive: true });
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
// src/cli/utils/skillbank.ts
|
|
378
|
+
async function createSkillBankFromOptions(options) {
|
|
379
|
+
const skillPath = resolveSkillPath(options.path);
|
|
380
|
+
ensureDir(skillPath);
|
|
381
|
+
const skillBank = new SkillBank({
|
|
382
|
+
storage: {
|
|
383
|
+
type: "filesystem",
|
|
384
|
+
basePath: skillPath,
|
|
385
|
+
openSkillsCompatible: true
|
|
386
|
+
}
|
|
387
|
+
});
|
|
388
|
+
await skillBank.initialize();
|
|
389
|
+
return skillBank;
|
|
390
|
+
}
|
|
391
|
+
function getSkillPath(options) {
|
|
392
|
+
return resolveSkillPath(options.path);
|
|
393
|
+
}
|
|
394
|
+
var getSkillBank = createSkillBankFromOptions;
|
|
395
|
+
|
|
396
|
+
// src/cli/utils/output.ts
|
|
397
|
+
import chalk from "chalk";
|
|
398
|
+
function formatSkillLine(skill) {
|
|
399
|
+
const status = formatStatus(skill.status);
|
|
400
|
+
const version = chalk.dim(`v${skill.version}`);
|
|
401
|
+
const id = chalk.bold(skill.id);
|
|
402
|
+
const name = skill.name !== skill.id ? chalk.dim(` (${skill.name})`) : "";
|
|
403
|
+
const tags = skill.tags.length > 0 ? chalk.dim(` [${skill.tags.join(", ")}]`) : "";
|
|
404
|
+
return `${status} ${id}${name} ${version}${tags}`;
|
|
405
|
+
}
|
|
406
|
+
function formatStatus(status) {
|
|
407
|
+
switch (status) {
|
|
408
|
+
case "active":
|
|
409
|
+
return chalk.green("\u25CF");
|
|
410
|
+
case "draft":
|
|
411
|
+
return chalk.yellow("\u25CB");
|
|
412
|
+
case "deprecated":
|
|
413
|
+
return chalk.red("\u2717");
|
|
414
|
+
case "experimental":
|
|
415
|
+
return chalk.blue("\u25D0");
|
|
416
|
+
default:
|
|
417
|
+
return chalk.gray("?");
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
function formatSkillDetail(skill) {
|
|
421
|
+
const lines = [];
|
|
422
|
+
lines.push(chalk.bold(`${skill.name} v${skill.version}`));
|
|
423
|
+
lines.push(chalk.dim("\u2500".repeat(50)));
|
|
424
|
+
lines.push(`${chalk.dim("Status:")} ${formatStatusLabel(skill.status)}`);
|
|
425
|
+
lines.push(`${chalk.dim("Author:")} ${skill.author}`);
|
|
426
|
+
if (skill.tags.length > 0) {
|
|
427
|
+
lines.push(`${chalk.dim("Tags:")} ${skill.tags.join(", ")}`);
|
|
428
|
+
}
|
|
429
|
+
if (skill.parentVersion) {
|
|
430
|
+
lines.push(`${chalk.dim("Parent:")} v${skill.parentVersion}`);
|
|
431
|
+
}
|
|
432
|
+
if (skill.derivedFrom && skill.derivedFrom.length > 0) {
|
|
433
|
+
lines.push(`${chalk.dim("Derived:")} ${skill.derivedFrom.join(", ")}`);
|
|
434
|
+
}
|
|
435
|
+
const { usageCount, successRate } = skill.metrics;
|
|
436
|
+
if (usageCount > 0) {
|
|
437
|
+
lines.push(`${chalk.dim("Usage:")} ${usageCount} times, ${Math.round(successRate * 100)}% success`);
|
|
438
|
+
}
|
|
439
|
+
lines.push("");
|
|
440
|
+
lines.push(chalk.dim("Description"));
|
|
441
|
+
lines.push(chalk.dim("\u2500".repeat(50)));
|
|
442
|
+
lines.push(skill.description);
|
|
443
|
+
lines.push("");
|
|
444
|
+
lines.push(chalk.dim("Problem"));
|
|
445
|
+
lines.push(chalk.dim("\u2500".repeat(50)));
|
|
446
|
+
lines.push(skill.problem);
|
|
447
|
+
lines.push("");
|
|
448
|
+
if (skill.triggerConditions.length > 0) {
|
|
449
|
+
lines.push(chalk.dim("Trigger Conditions"));
|
|
450
|
+
lines.push(chalk.dim("\u2500".repeat(50)));
|
|
451
|
+
for (const trigger of skill.triggerConditions) {
|
|
452
|
+
const desc = trigger.description ? ` - ${trigger.description}` : "";
|
|
453
|
+
lines.push(` ${chalk.cyan(trigger.type)}: ${trigger.value}${chalk.dim(desc)}`);
|
|
454
|
+
}
|
|
455
|
+
lines.push("");
|
|
456
|
+
}
|
|
457
|
+
lines.push(chalk.dim("Solution"));
|
|
458
|
+
lines.push(chalk.dim("\u2500".repeat(50)));
|
|
459
|
+
lines.push(skill.solution);
|
|
460
|
+
lines.push("");
|
|
461
|
+
if (skill.verification) {
|
|
462
|
+
lines.push(chalk.dim("Verification"));
|
|
463
|
+
lines.push(chalk.dim("\u2500".repeat(50)));
|
|
464
|
+
lines.push(skill.verification);
|
|
465
|
+
lines.push("");
|
|
466
|
+
}
|
|
467
|
+
if (skill.notes) {
|
|
468
|
+
lines.push(chalk.dim("Notes"));
|
|
469
|
+
lines.push(chalk.dim("\u2500".repeat(50)));
|
|
470
|
+
lines.push(skill.notes);
|
|
471
|
+
}
|
|
472
|
+
return lines.join("\n");
|
|
473
|
+
}
|
|
474
|
+
function formatStatusLabel(status) {
|
|
475
|
+
switch (status) {
|
|
476
|
+
case "active":
|
|
477
|
+
return chalk.green("active");
|
|
478
|
+
case "draft":
|
|
479
|
+
return chalk.yellow("draft");
|
|
480
|
+
case "deprecated":
|
|
481
|
+
return chalk.red("deprecated");
|
|
482
|
+
case "experimental":
|
|
483
|
+
return chalk.blue("experimental");
|
|
484
|
+
default:
|
|
485
|
+
return chalk.gray(status);
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
function formatVersionHistory(versions) {
|
|
489
|
+
const lines = [];
|
|
490
|
+
lines.push(chalk.bold("Version History"));
|
|
491
|
+
lines.push(chalk.dim("\u2500".repeat(50)));
|
|
492
|
+
for (const v of versions.reverse()) {
|
|
493
|
+
const date = v.createdAt.toISOString().split("T")[0];
|
|
494
|
+
const hash = chalk.dim(`#${v.contentHash.slice(0, 7)}`);
|
|
495
|
+
const changelog = v.changelog ? ` - ${v.changelog}` : "";
|
|
496
|
+
lines.push(` ${chalk.cyan(`v${v.version}`)} ${chalk.dim(date)} ${hash}${changelog}`);
|
|
497
|
+
}
|
|
498
|
+
return lines.join("\n");
|
|
499
|
+
}
|
|
500
|
+
function formatDiff(versionA, versionB, changes) {
|
|
501
|
+
const lines = [];
|
|
502
|
+
lines.push(chalk.bold(`Diff: v${versionA} \u2192 v${versionB}`));
|
|
503
|
+
lines.push(chalk.dim("\u2500".repeat(50)));
|
|
504
|
+
if (changes.modified.length === 0 && changes.added.length === 0 && changes.removed.length === 0) {
|
|
505
|
+
lines.push(chalk.dim("No changes"));
|
|
506
|
+
return lines.join("\n");
|
|
507
|
+
}
|
|
508
|
+
for (const mod of changes.modified) {
|
|
509
|
+
lines.push(chalk.yellow(`~ ${mod.field}`));
|
|
510
|
+
lines.push(chalk.red(` - ${truncate(mod.oldValue, 60)}`));
|
|
511
|
+
lines.push(chalk.green(` + ${truncate(mod.newValue, 60)}`));
|
|
512
|
+
}
|
|
513
|
+
for (const add of changes.added) {
|
|
514
|
+
lines.push(chalk.green(`+ ${add.type}: ${add.value}`));
|
|
515
|
+
}
|
|
516
|
+
for (const rem of changes.removed) {
|
|
517
|
+
lines.push(chalk.red(`- ${rem.type}: ${rem.value}`));
|
|
518
|
+
}
|
|
519
|
+
return lines.join("\n");
|
|
520
|
+
}
|
|
521
|
+
function formatStats(stats) {
|
|
522
|
+
const lines = [];
|
|
523
|
+
lines.push(chalk.bold("Skill Bank Statistics"));
|
|
524
|
+
lines.push(chalk.dim("\u2500".repeat(50)));
|
|
525
|
+
lines.push(`${chalk.dim("Total skills:")} ${stats.totalSkills}`);
|
|
526
|
+
lines.push("");
|
|
527
|
+
lines.push(chalk.dim("By Status:"));
|
|
528
|
+
lines.push(` ${chalk.green("\u25CF")} Active: ${stats.byStatus.active}`);
|
|
529
|
+
lines.push(` ${chalk.yellow("\u25CB")} Draft: ${stats.byStatus.draft}`);
|
|
530
|
+
lines.push(` ${chalk.blue("\u25D0")} Experimental: ${stats.byStatus.experimental}`);
|
|
531
|
+
lines.push(` ${chalk.red("\u2717")} Deprecated: ${stats.byStatus.deprecated}`);
|
|
532
|
+
lines.push("");
|
|
533
|
+
if (Object.keys(stats.byTag).length > 0) {
|
|
534
|
+
lines.push(chalk.dim("By Tag:"));
|
|
535
|
+
const sortedTags = Object.entries(stats.byTag).sort((a, b) => b[1] - a[1]);
|
|
536
|
+
for (const [tag, count] of sortedTags.slice(0, 10)) {
|
|
537
|
+
lines.push(` ${tag}: ${count}`);
|
|
538
|
+
}
|
|
539
|
+
lines.push("");
|
|
540
|
+
}
|
|
541
|
+
if (stats.totalUsage > 0) {
|
|
542
|
+
lines.push(chalk.dim("Usage:"));
|
|
543
|
+
lines.push(` Total uses: ${stats.totalUsage}`);
|
|
544
|
+
lines.push(` Avg success: ${Math.round(stats.avgSuccessRate * 100)}%`);
|
|
545
|
+
}
|
|
546
|
+
return lines.join("\n");
|
|
547
|
+
}
|
|
548
|
+
function printError(message) {
|
|
549
|
+
console.error(chalk.red(`Error: ${message}`));
|
|
550
|
+
}
|
|
551
|
+
function printSuccess(message) {
|
|
552
|
+
console.log(chalk.green(`\u2713 ${message}`));
|
|
553
|
+
}
|
|
554
|
+
function printWarning(message) {
|
|
555
|
+
console.log(chalk.yellow(`\u26A0 ${message}`));
|
|
556
|
+
}
|
|
557
|
+
function printInfo(message) {
|
|
558
|
+
console.log(chalk.dim(message));
|
|
559
|
+
}
|
|
560
|
+
function truncate(text, maxLength) {
|
|
561
|
+
const oneLine = text.replace(/\n/g, " ");
|
|
562
|
+
if (oneLine.length <= maxLength) return oneLine;
|
|
563
|
+
return oneLine.slice(0, maxLength - 3) + "...";
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
// src/cli/commands/list.ts
|
|
567
|
+
var listCommand = new Command("list").description("List all skills").option("-s, --status <status>", "Filter by status (active, draft, deprecated, experimental)").option("-t, --tag <tag>", "Filter by tag").option("-a, --author <author>", "Filter by author").action(async (options, command) => {
|
|
568
|
+
const globalOpts = command.optsWithGlobals();
|
|
569
|
+
try {
|
|
570
|
+
const skillBank = await createSkillBankFromOptions(globalOpts);
|
|
571
|
+
const filter = {};
|
|
572
|
+
if (options.status) {
|
|
573
|
+
filter.status = [options.status];
|
|
574
|
+
}
|
|
575
|
+
if (options.tag) {
|
|
576
|
+
filter.tags = [options.tag];
|
|
577
|
+
}
|
|
578
|
+
if (options.author) {
|
|
579
|
+
filter.author = options.author;
|
|
580
|
+
}
|
|
581
|
+
const skills = await skillBank.listSkills(
|
|
582
|
+
Object.keys(filter).length > 0 ? filter : void 0
|
|
583
|
+
);
|
|
584
|
+
if (globalOpts.json) {
|
|
585
|
+
console.log(JSON.stringify(skills, null, 2));
|
|
586
|
+
return;
|
|
587
|
+
}
|
|
588
|
+
if (skills.length === 0) {
|
|
589
|
+
printInfo(`No skills found in ${getSkillPath(globalOpts)}`);
|
|
590
|
+
return;
|
|
591
|
+
}
|
|
592
|
+
if (!globalOpts.quiet) {
|
|
593
|
+
printInfo(`Skills in ${getSkillPath(globalOpts)}:
|
|
594
|
+
`);
|
|
595
|
+
}
|
|
596
|
+
for (const skill of skills) {
|
|
597
|
+
console.log(formatSkillLine(skill));
|
|
598
|
+
}
|
|
599
|
+
if (!globalOpts.quiet) {
|
|
600
|
+
console.log();
|
|
601
|
+
printInfo(`${skills.length} skill(s)`);
|
|
602
|
+
}
|
|
603
|
+
} catch (error) {
|
|
604
|
+
printError(error.message);
|
|
605
|
+
process.exit(1);
|
|
606
|
+
}
|
|
607
|
+
});
|
|
608
|
+
|
|
609
|
+
// src/cli/commands/show.ts
|
|
610
|
+
import { Command as Command2 } from "commander";
|
|
611
|
+
var showCommand = new Command2("show").description("Show skill details").argument("<id>", "Skill ID").option("-v, --version <version>", "Show specific version").action(async (id, options, command) => {
|
|
612
|
+
const globalOpts = command.optsWithGlobals();
|
|
613
|
+
try {
|
|
614
|
+
const skillBank = await createSkillBankFromOptions(globalOpts);
|
|
615
|
+
const skill = await skillBank.getSkill(id, options.version);
|
|
616
|
+
if (!skill) {
|
|
617
|
+
printError(`Skill not found: ${id}${options.version ? `@${options.version}` : ""}`);
|
|
618
|
+
process.exit(1);
|
|
619
|
+
}
|
|
620
|
+
if (globalOpts.json) {
|
|
621
|
+
console.log(JSON.stringify(skill, null, 2));
|
|
622
|
+
return;
|
|
623
|
+
}
|
|
624
|
+
console.log(formatSkillDetail(skill));
|
|
625
|
+
} catch (error) {
|
|
626
|
+
printError(error.message);
|
|
627
|
+
process.exit(1);
|
|
628
|
+
}
|
|
629
|
+
});
|
|
630
|
+
|
|
631
|
+
// src/cli/commands/search.ts
|
|
632
|
+
import { Command as Command3 } from "commander";
|
|
633
|
+
var searchCommand = new Command3("search").description("Search skills by text").argument("<query>", "Search query").action(async (query, options, command) => {
|
|
634
|
+
const globalOpts = command.optsWithGlobals();
|
|
635
|
+
try {
|
|
636
|
+
const skillBank = await createSkillBankFromOptions(globalOpts);
|
|
637
|
+
const skills = await skillBank.searchSkills(query);
|
|
638
|
+
if (globalOpts.json) {
|
|
639
|
+
console.log(JSON.stringify(skills, null, 2));
|
|
640
|
+
return;
|
|
641
|
+
}
|
|
642
|
+
if (skills.length === 0) {
|
|
643
|
+
printInfo(`No skills found matching "${query}"`);
|
|
644
|
+
return;
|
|
645
|
+
}
|
|
646
|
+
if (!globalOpts.quiet) {
|
|
647
|
+
printInfo(`Results for "${query}":
|
|
648
|
+
`);
|
|
649
|
+
}
|
|
650
|
+
for (const skill of skills) {
|
|
651
|
+
console.log(formatSkillLine(skill));
|
|
652
|
+
}
|
|
653
|
+
if (!globalOpts.quiet) {
|
|
654
|
+
console.log();
|
|
655
|
+
printInfo(`${skills.length} result(s)`);
|
|
656
|
+
}
|
|
657
|
+
} catch (error) {
|
|
658
|
+
printError(error.message);
|
|
659
|
+
process.exit(1);
|
|
660
|
+
}
|
|
661
|
+
});
|
|
662
|
+
|
|
663
|
+
// src/cli/commands/stats.ts
|
|
664
|
+
import { Command as Command4 } from "commander";
|
|
665
|
+
var statsCommand = new Command4("stats").description("Show skill bank statistics").action(async (options, command) => {
|
|
666
|
+
const globalOpts = command.optsWithGlobals();
|
|
667
|
+
try {
|
|
668
|
+
const skillBank = await createSkillBankFromOptions(globalOpts);
|
|
669
|
+
const stats = await skillBank.getStats();
|
|
670
|
+
if (globalOpts.json) {
|
|
671
|
+
console.log(JSON.stringify(stats, null, 2));
|
|
672
|
+
return;
|
|
673
|
+
}
|
|
674
|
+
console.log(formatStats(stats));
|
|
675
|
+
} catch (error) {
|
|
676
|
+
printError(error.message);
|
|
677
|
+
process.exit(1);
|
|
678
|
+
}
|
|
679
|
+
});
|
|
680
|
+
|
|
681
|
+
// src/cli/commands/versions.ts
|
|
682
|
+
import { Command as Command5 } from "commander";
|
|
683
|
+
var versionsCommand = new Command5("versions").description("Show version history for a skill").argument("<id>", "Skill ID").action(async (id, options, command) => {
|
|
684
|
+
const globalOpts = command.optsWithGlobals();
|
|
685
|
+
try {
|
|
686
|
+
const skillBank = await createSkillBankFromOptions(globalOpts);
|
|
687
|
+
const versions = await skillBank.getVersionHistory(id);
|
|
688
|
+
if (versions.length === 0) {
|
|
689
|
+
printInfo(`No version history found for: ${id}`);
|
|
690
|
+
return;
|
|
691
|
+
}
|
|
692
|
+
if (globalOpts.json) {
|
|
693
|
+
console.log(JSON.stringify(versions, null, 2));
|
|
694
|
+
return;
|
|
695
|
+
}
|
|
696
|
+
console.log(formatVersionHistory(versions));
|
|
697
|
+
} catch (error) {
|
|
698
|
+
printError(error.message);
|
|
699
|
+
process.exit(1);
|
|
700
|
+
}
|
|
701
|
+
});
|
|
702
|
+
|
|
703
|
+
// src/cli/commands/diff.ts
|
|
704
|
+
import { Command as Command6 } from "commander";
|
|
705
|
+
var diffCommand = new Command6("diff").description("Compare two versions of a skill").argument("<id>", "Skill ID").argument("<version-a>", "First version").argument("<version-b>", "Second version").action(async (id, versionA, versionB, options, command) => {
|
|
706
|
+
const globalOpts = command.optsWithGlobals();
|
|
707
|
+
try {
|
|
708
|
+
const skillBank = await createSkillBankFromOptions(globalOpts);
|
|
709
|
+
const diff = await skillBank.compareVersions(id, versionA, versionB);
|
|
710
|
+
if (globalOpts.json) {
|
|
711
|
+
console.log(JSON.stringify(diff, null, 2));
|
|
712
|
+
return;
|
|
713
|
+
}
|
|
714
|
+
console.log(formatDiff(diff.versionA, diff.versionB, diff.changes));
|
|
715
|
+
} catch (error) {
|
|
716
|
+
printError(error.message);
|
|
717
|
+
process.exit(1);
|
|
718
|
+
}
|
|
719
|
+
});
|
|
720
|
+
|
|
721
|
+
// src/cli/commands/rollback.ts
|
|
722
|
+
import { Command as Command7 } from "commander";
|
|
723
|
+
var rollbackCommand = new Command7("rollback").description("Rollback a skill to a previous version").argument("<id>", "Skill ID").requiredOption("--to <version>", "Version to rollback to").action(async (id, options, command) => {
|
|
724
|
+
const globalOpts = command.optsWithGlobals();
|
|
725
|
+
try {
|
|
726
|
+
const skillBank = await createSkillBankFromOptions(globalOpts);
|
|
727
|
+
const skill = await skillBank.rollbackSkill(id, options.to);
|
|
728
|
+
if (globalOpts.json) {
|
|
729
|
+
console.log(JSON.stringify(skill, null, 2));
|
|
730
|
+
return;
|
|
731
|
+
}
|
|
732
|
+
printSuccess(`Rolled back ${id} to v${options.to} (new version: v${skill.version})`);
|
|
733
|
+
} catch (error) {
|
|
734
|
+
printError(error.message);
|
|
735
|
+
process.exit(1);
|
|
736
|
+
}
|
|
737
|
+
});
|
|
738
|
+
|
|
739
|
+
// src/cli/commands/fork.ts
|
|
740
|
+
import { Command as Command8 } from "commander";
|
|
741
|
+
var forkCommand = new Command8("fork").description("Fork a skill to create a variant").argument("<id>", "Skill ID to fork from").requiredOption("--new-id <id>", "ID for the new forked skill").requiredOption("--reason <reason>", "Reason for forking").option("--name <name>", "Name for the forked skill").option("--from-version <version>", "Version to fork from (defaults to latest)").action(async (id, options, command) => {
|
|
742
|
+
const globalOpts = command.optsWithGlobals();
|
|
743
|
+
try {
|
|
744
|
+
const skillBank = await createSkillBankFromOptions(globalOpts);
|
|
745
|
+
const forked = await skillBank.forkSkill(id, {
|
|
746
|
+
newId: options.newId,
|
|
747
|
+
newName: options.name,
|
|
748
|
+
reason: options.reason,
|
|
749
|
+
fromVersion: options.fromVersion
|
|
750
|
+
});
|
|
751
|
+
if (globalOpts.json) {
|
|
752
|
+
console.log(JSON.stringify(forked, null, 2));
|
|
753
|
+
return;
|
|
754
|
+
}
|
|
755
|
+
printSuccess(`Forked ${id} \u2192 ${forked.id} (v${forked.version})`);
|
|
756
|
+
} catch (error) {
|
|
757
|
+
printError(error.message);
|
|
758
|
+
process.exit(1);
|
|
759
|
+
}
|
|
760
|
+
});
|
|
761
|
+
|
|
762
|
+
// src/cli/commands/deprecate.ts
|
|
763
|
+
import { Command as Command9 } from "commander";
|
|
764
|
+
var deprecateCommand = new Command9("deprecate").description("Mark a skill as deprecated").argument("<id>", "Skill ID").action(async (id, options, command) => {
|
|
765
|
+
const globalOpts = command.optsWithGlobals();
|
|
766
|
+
try {
|
|
767
|
+
const skillBank = await createSkillBankFromOptions(globalOpts);
|
|
768
|
+
const skill = await skillBank.deprecateSkill(id);
|
|
769
|
+
if (globalOpts.json) {
|
|
770
|
+
console.log(JSON.stringify(skill, null, 2));
|
|
771
|
+
return;
|
|
772
|
+
}
|
|
773
|
+
printSuccess(`Deprecated: ${id} (v${skill.version})`);
|
|
774
|
+
} catch (error) {
|
|
775
|
+
printError(error.message);
|
|
776
|
+
process.exit(1);
|
|
777
|
+
}
|
|
778
|
+
});
|
|
779
|
+
|
|
780
|
+
// src/cli/commands/activate.ts
|
|
781
|
+
import { Command as Command10 } from "commander";
|
|
782
|
+
var activateCommand = new Command10("activate").description("Mark a skill as active").argument("<id>", "Skill ID").action(async (id, options, command) => {
|
|
783
|
+
const globalOpts = command.optsWithGlobals();
|
|
784
|
+
try {
|
|
785
|
+
const skillBank = await createSkillBankFromOptions(globalOpts);
|
|
786
|
+
const skill = await skillBank.getSkill(id);
|
|
787
|
+
if (!skill) {
|
|
788
|
+
printError(`Skill not found: ${id}`);
|
|
789
|
+
process.exit(1);
|
|
790
|
+
}
|
|
791
|
+
skill.status = "active";
|
|
792
|
+
skill.updatedAt = /* @__PURE__ */ new Date();
|
|
793
|
+
await skillBank.saveSkill(skill);
|
|
794
|
+
if (globalOpts.json) {
|
|
795
|
+
console.log(JSON.stringify(skill, null, 2));
|
|
796
|
+
return;
|
|
797
|
+
}
|
|
798
|
+
printSuccess(`Activated: ${id} (v${skill.version})`);
|
|
799
|
+
} catch (error) {
|
|
800
|
+
printError(error.message);
|
|
801
|
+
process.exit(1);
|
|
802
|
+
}
|
|
803
|
+
});
|
|
804
|
+
|
|
805
|
+
// src/cli/commands/delete.ts
|
|
806
|
+
import { Command as Command11 } from "commander";
|
|
807
|
+
var deleteCommand = new Command11("delete").description("Delete a skill").argument("<id>", "Skill ID").option("-f, --force", "Skip confirmation").option("-v, --version <version>", "Delete specific version only").action(async (id, options, command) => {
|
|
808
|
+
const globalOpts = command.optsWithGlobals();
|
|
809
|
+
try {
|
|
810
|
+
const skillBank = await createSkillBankFromOptions(globalOpts);
|
|
811
|
+
const skill = await skillBank.getSkill(id, options.version);
|
|
812
|
+
if (!skill) {
|
|
813
|
+
printError(`Skill not found: ${id}${options.version ? `@${options.version}` : ""}`);
|
|
814
|
+
process.exit(1);
|
|
815
|
+
}
|
|
816
|
+
if (!options.force) {
|
|
817
|
+
printWarning(`This will permanently delete ${id}${options.version ? `@${options.version}` : " and all versions"}`);
|
|
818
|
+
printWarning("Use --force to confirm");
|
|
819
|
+
process.exit(1);
|
|
820
|
+
}
|
|
821
|
+
const deleted = await skillBank.deleteSkill(id, options.version);
|
|
822
|
+
if (!deleted) {
|
|
823
|
+
printError(`Failed to delete: ${id}`);
|
|
824
|
+
process.exit(1);
|
|
825
|
+
}
|
|
826
|
+
if (globalOpts.json) {
|
|
827
|
+
console.log(JSON.stringify({ deleted: true, id, version: options.version }));
|
|
828
|
+
return;
|
|
829
|
+
}
|
|
830
|
+
printSuccess(`Deleted: ${id}${options.version ? `@${options.version}` : ""}`);
|
|
831
|
+
} catch (error) {
|
|
832
|
+
printError(error.message);
|
|
833
|
+
process.exit(1);
|
|
834
|
+
}
|
|
835
|
+
});
|
|
836
|
+
|
|
837
|
+
// src/cli/commands/export.ts
|
|
838
|
+
import { Command as Command12 } from "commander";
|
|
839
|
+
import * as fs3 from "fs";
|
|
840
|
+
var exportCommand = new Command12("export").description("Export all skills to JSON").option("-o, --output <file>", "Output file path (defaults to stdout)").action(async (options, command) => {
|
|
841
|
+
const globalOpts = command.optsWithGlobals();
|
|
842
|
+
try {
|
|
843
|
+
const skillBank = await createSkillBankFromOptions(globalOpts);
|
|
844
|
+
const skills = await skillBank.exportAll();
|
|
845
|
+
const json = JSON.stringify(skills, null, 2);
|
|
846
|
+
if (options.output) {
|
|
847
|
+
fs3.writeFileSync(options.output, json, "utf-8");
|
|
848
|
+
if (!globalOpts.quiet) {
|
|
849
|
+
printSuccess(`Exported ${skills.length} skill(s) to ${options.output}`);
|
|
850
|
+
}
|
|
851
|
+
} else {
|
|
852
|
+
console.log(json);
|
|
853
|
+
}
|
|
854
|
+
} catch (error) {
|
|
855
|
+
printError(error.message);
|
|
856
|
+
process.exit(1);
|
|
857
|
+
}
|
|
858
|
+
});
|
|
859
|
+
|
|
860
|
+
// src/cli/commands/import.ts
|
|
861
|
+
import { Command as Command13 } from "commander";
|
|
862
|
+
import * as fs4 from "fs";
|
|
863
|
+
|
|
864
|
+
// src/import/converter.ts
|
|
865
|
+
var DEFAULT_SECTION_PATTERNS = {
|
|
866
|
+
problem: [
|
|
867
|
+
/^#+\s*(?:problem|issue|what\s+problem|the\s+problem)/i,
|
|
868
|
+
/^#+\s*(?:why|motivation|background)/i
|
|
869
|
+
],
|
|
870
|
+
solution: [
|
|
871
|
+
/^#+\s*(?:solution|how\s+to|implementation|approach)/i,
|
|
872
|
+
/^#+\s*(?:usage|instructions|steps)/i
|
|
873
|
+
],
|
|
874
|
+
verification: [
|
|
875
|
+
/^#+\s*(?:verification|verify|testing|test|validate)/i,
|
|
876
|
+
/^#+\s*(?:how\s+to\s+verify|checking)/i
|
|
877
|
+
],
|
|
878
|
+
examples: [
|
|
879
|
+
/^#+\s*(?:examples?|sample|demo)/i,
|
|
880
|
+
/^#+\s*(?:use\s+cases?|scenarios?)/i
|
|
881
|
+
],
|
|
882
|
+
notes: [
|
|
883
|
+
/^#+\s*(?:notes?|caveats?|warnings?|tips?)/i,
|
|
884
|
+
/^#+\s*(?:additional|edge\s+cases?|considerations?)/i
|
|
885
|
+
],
|
|
886
|
+
triggers: [
|
|
887
|
+
/^#+\s*(?:triggers?|when\s+to\s+use|conditions?)/i,
|
|
888
|
+
/^#+\s*(?:activation|applies\s+when)/i
|
|
889
|
+
]
|
|
890
|
+
};
|
|
891
|
+
function parseMarkdownSections(content) {
|
|
892
|
+
const lines = content.split("\n");
|
|
893
|
+
const sections = {
|
|
894
|
+
remainingContent: ""
|
|
895
|
+
};
|
|
896
|
+
let currentSection = "remaining";
|
|
897
|
+
let currentContent = [];
|
|
898
|
+
const flushSection = () => {
|
|
899
|
+
const text = currentContent.join("\n").trim();
|
|
900
|
+
if (!text) return;
|
|
901
|
+
switch (currentSection) {
|
|
902
|
+
case "problem":
|
|
903
|
+
sections.problem = text;
|
|
904
|
+
break;
|
|
905
|
+
case "solution":
|
|
906
|
+
sections.solution = text;
|
|
907
|
+
break;
|
|
908
|
+
case "verification":
|
|
909
|
+
sections.verification = text;
|
|
910
|
+
break;
|
|
911
|
+
case "examples":
|
|
912
|
+
sections.examples = parseExamples(text);
|
|
913
|
+
break;
|
|
914
|
+
case "notes":
|
|
915
|
+
sections.notes = text;
|
|
916
|
+
break;
|
|
917
|
+
case "triggers":
|
|
918
|
+
sections.triggers = parseTriggerList(text);
|
|
919
|
+
break;
|
|
920
|
+
case "remaining":
|
|
921
|
+
sections.remainingContent += (sections.remainingContent ? "\n\n" : "") + text;
|
|
922
|
+
break;
|
|
923
|
+
}
|
|
924
|
+
currentContent = [];
|
|
925
|
+
};
|
|
926
|
+
for (const line of lines) {
|
|
927
|
+
let matchedSection = null;
|
|
928
|
+
for (const [section, sectionPatterns] of Object.entries(DEFAULT_SECTION_PATTERNS)) {
|
|
929
|
+
for (const pattern of sectionPatterns) {
|
|
930
|
+
if (pattern.test(line)) {
|
|
931
|
+
matchedSection = section;
|
|
932
|
+
break;
|
|
933
|
+
}
|
|
934
|
+
}
|
|
935
|
+
if (matchedSection) break;
|
|
936
|
+
}
|
|
937
|
+
if (matchedSection) {
|
|
938
|
+
flushSection();
|
|
939
|
+
currentSection = matchedSection;
|
|
940
|
+
} else {
|
|
941
|
+
currentContent.push(line);
|
|
942
|
+
}
|
|
943
|
+
}
|
|
944
|
+
flushSection();
|
|
945
|
+
return sections;
|
|
946
|
+
}
|
|
947
|
+
function parseExamples(text) {
|
|
948
|
+
const examples = [];
|
|
949
|
+
const exampleBlocks = text.split(/^###\s+/m).filter(Boolean);
|
|
950
|
+
for (const block of exampleBlocks) {
|
|
951
|
+
const lines = block.split("\n");
|
|
952
|
+
const scenario = lines[0]?.trim() || "Example";
|
|
953
|
+
const content = lines.slice(1).join("\n");
|
|
954
|
+
const beforeMatch = content.match(/(?:before|input|given)[:\s]*\n?([\s\S]*?)(?=(?:after|output|then|result)|$)/i);
|
|
955
|
+
const afterMatch = content.match(/(?:after|output|then|result)[:\s]*\n?([\s\S]*?)$/i);
|
|
956
|
+
if (beforeMatch || afterMatch) {
|
|
957
|
+
examples.push({
|
|
958
|
+
scenario,
|
|
959
|
+
before: beforeMatch?.[1]?.trim() || "",
|
|
960
|
+
after: afterMatch?.[1]?.trim() || content.trim()
|
|
961
|
+
});
|
|
962
|
+
} else {
|
|
963
|
+
examples.push({
|
|
964
|
+
scenario,
|
|
965
|
+
before: "",
|
|
966
|
+
after: content.trim()
|
|
967
|
+
});
|
|
968
|
+
}
|
|
969
|
+
}
|
|
970
|
+
if (examples.length === 0 && text.trim()) {
|
|
971
|
+
examples.push({
|
|
972
|
+
scenario: "Example usage",
|
|
973
|
+
before: "",
|
|
974
|
+
after: text.trim()
|
|
975
|
+
});
|
|
976
|
+
}
|
|
977
|
+
return examples;
|
|
978
|
+
}
|
|
979
|
+
function parseTriggerList(text) {
|
|
980
|
+
const triggers = [];
|
|
981
|
+
const listItems = text.match(/^[\s]*[-*•]\s*(.+)$/gm) || text.match(/^[\s]*\d+\.\s*(.+)$/gm);
|
|
982
|
+
if (listItems) {
|
|
983
|
+
for (const item of listItems) {
|
|
984
|
+
const cleaned = item.replace(/^[\s]*[-*•\d.]+\s*/, "").trim();
|
|
985
|
+
if (cleaned) triggers.push(cleaned);
|
|
986
|
+
}
|
|
987
|
+
} else {
|
|
988
|
+
triggers.push(...text.split("\n").map((t) => t.trim()).filter(Boolean));
|
|
989
|
+
}
|
|
990
|
+
return triggers;
|
|
991
|
+
}
|
|
992
|
+
function inferTriggerType(text) {
|
|
993
|
+
const lower = text.toLowerCase();
|
|
994
|
+
if (lower.includes("error") || lower.includes("exception") || lower.includes("fail")) {
|
|
995
|
+
return { type: "error", value: text, description: "Triggered by error conditions" };
|
|
996
|
+
}
|
|
997
|
+
if (lower.includes("pattern") || lower.includes("regex") || lower.includes("match")) {
|
|
998
|
+
return { type: "pattern", value: text };
|
|
999
|
+
}
|
|
1000
|
+
if (lower.includes("when") || lower.includes("context") || lower.includes("if ")) {
|
|
1001
|
+
return { type: "context", value: text };
|
|
1002
|
+
}
|
|
1003
|
+
return { type: "keyword", value: text };
|
|
1004
|
+
}
|
|
1005
|
+
function extractKeywordsFromDescription(description) {
|
|
1006
|
+
const keywords = [];
|
|
1007
|
+
const techPatterns = [
|
|
1008
|
+
/\b(react|vue|angular|svelte|next\.?js|nuxt)\b/gi,
|
|
1009
|
+
/\b(node\.?js|deno|bun|python|rust|go|java|typescript|javascript)\b/gi,
|
|
1010
|
+
/\b(docker|kubernetes|k8s|aws|gcp|azure)\b/gi,
|
|
1011
|
+
/\b(git|github|gitlab|npm|yarn|pnpm)\b/gi,
|
|
1012
|
+
/\b(sql|postgres|mysql|mongodb|redis)\b/gi,
|
|
1013
|
+
/\b(api|rest|graphql|grpc)\b/gi,
|
|
1014
|
+
/\b(test|testing|jest|vitest|pytest)\b/gi
|
|
1015
|
+
];
|
|
1016
|
+
for (const pattern of techPatterns) {
|
|
1017
|
+
const matches = description.match(pattern);
|
|
1018
|
+
if (matches) {
|
|
1019
|
+
keywords.push(...matches.map((m) => m.toLowerCase()));
|
|
1020
|
+
}
|
|
1021
|
+
}
|
|
1022
|
+
return [...new Set(keywords)];
|
|
1023
|
+
}
|
|
1024
|
+
function extractTriggerConditions(skill, sections) {
|
|
1025
|
+
const conditions = [];
|
|
1026
|
+
if (sections?.triggers) {
|
|
1027
|
+
for (const trigger of sections.triggers) {
|
|
1028
|
+
conditions.push(inferTriggerType(trigger));
|
|
1029
|
+
}
|
|
1030
|
+
}
|
|
1031
|
+
const keywords = extractKeywordsFromDescription(skill.description);
|
|
1032
|
+
for (const keyword of keywords) {
|
|
1033
|
+
if (!conditions.some((c) => c.value.toLowerCase() === keyword.toLowerCase())) {
|
|
1034
|
+
conditions.push({
|
|
1035
|
+
type: "keyword",
|
|
1036
|
+
value: keyword,
|
|
1037
|
+
description: "Keyword from description"
|
|
1038
|
+
});
|
|
1039
|
+
}
|
|
1040
|
+
}
|
|
1041
|
+
if (skill.tags) {
|
|
1042
|
+
for (const tag of skill.tags.slice(0, 5)) {
|
|
1043
|
+
if (!conditions.some((c) => c.value.toLowerCase() === tag.toLowerCase())) {
|
|
1044
|
+
conditions.push({
|
|
1045
|
+
type: "keyword",
|
|
1046
|
+
value: tag,
|
|
1047
|
+
description: "Tag from classification"
|
|
1048
|
+
});
|
|
1049
|
+
}
|
|
1050
|
+
}
|
|
1051
|
+
}
|
|
1052
|
+
if (conditions.length === 0) {
|
|
1053
|
+
conditions.push({
|
|
1054
|
+
type: "context",
|
|
1055
|
+
value: skill.description.substring(0, 100),
|
|
1056
|
+
description: "Generated from skill description"
|
|
1057
|
+
});
|
|
1058
|
+
}
|
|
1059
|
+
return conditions;
|
|
1060
|
+
}
|
|
1061
|
+
function convertIndexerSkill(indexerSkill) {
|
|
1062
|
+
const warnings = [];
|
|
1063
|
+
const sections = parseMarkdownSections(indexerSkill.content);
|
|
1064
|
+
const hasStructuredContent = !!(sections.problem || sections.solution || sections.verification || sections.examples?.length);
|
|
1065
|
+
if (!hasStructuredContent) {
|
|
1066
|
+
warnings.push("No structured content sections found, using raw content");
|
|
1067
|
+
}
|
|
1068
|
+
const triggerConditions = extractTriggerConditions(indexerSkill, sections);
|
|
1069
|
+
let examples = sections.examples || [];
|
|
1070
|
+
if (examples.length === 0) {
|
|
1071
|
+
examples = [{
|
|
1072
|
+
scenario: "Usage example",
|
|
1073
|
+
before: "",
|
|
1074
|
+
after: indexerSkill.content.substring(0, 500)
|
|
1075
|
+
}];
|
|
1076
|
+
warnings.push("No examples found, created placeholder");
|
|
1077
|
+
}
|
|
1078
|
+
const problem = sections.problem || indexerSkill.description;
|
|
1079
|
+
const solution = sections.solution || indexerSkill.content;
|
|
1080
|
+
const verification = sections.verification || "Verify the skill output meets expectations";
|
|
1081
|
+
if (!sections.problem) warnings.push("No problem section found, using description");
|
|
1082
|
+
if (!sections.solution) warnings.push("No solution section found, using full content");
|
|
1083
|
+
if (!sections.verification) warnings.push("No verification section found, using default");
|
|
1084
|
+
const tags = [...indexerSkill.tags || []];
|
|
1085
|
+
if (indexerSkill.sourceRepo) {
|
|
1086
|
+
const repoName = indexerSkill.sourceRepo.split("/").pop();
|
|
1087
|
+
if (repoName && !tags.includes(repoName)) {
|
|
1088
|
+
tags.push(repoName);
|
|
1089
|
+
}
|
|
1090
|
+
}
|
|
1091
|
+
const status = indexerSkill.status === "indexed" ? "active" : indexerSkill.status === "raw" ? "draft" : "draft";
|
|
1092
|
+
let notes = sections.notes || void 0;
|
|
1093
|
+
if (indexerSkill.classificationReasoning) {
|
|
1094
|
+
const classificationNote = `
|
|
1095
|
+
|
|
1096
|
+
---
|
|
1097
|
+
Classification: ${indexerSkill.primaryPath?.join(" > ") || "uncategorized"}
|
|
1098
|
+
Reasoning: ${indexerSkill.classificationReasoning}`;
|
|
1099
|
+
notes = (notes || "") + classificationNote;
|
|
1100
|
+
}
|
|
1101
|
+
const skill = {
|
|
1102
|
+
id: indexerSkill.slug,
|
|
1103
|
+
name: indexerSkill.displayName || indexerSkill.name,
|
|
1104
|
+
version: indexerSkill.version,
|
|
1105
|
+
description: indexerSkill.description,
|
|
1106
|
+
problem,
|
|
1107
|
+
triggerConditions,
|
|
1108
|
+
solution,
|
|
1109
|
+
verification,
|
|
1110
|
+
examples,
|
|
1111
|
+
notes,
|
|
1112
|
+
author: indexerSkill.author,
|
|
1113
|
+
tags,
|
|
1114
|
+
createdAt: new Date(indexerSkill.scrapedAt),
|
|
1115
|
+
updatedAt: new Date(indexerSkill.updatedAt),
|
|
1116
|
+
status,
|
|
1117
|
+
metrics: {
|
|
1118
|
+
usageCount: 0,
|
|
1119
|
+
successRate: 0,
|
|
1120
|
+
feedbackScores: []
|
|
1121
|
+
},
|
|
1122
|
+
source: {
|
|
1123
|
+
type: "imported",
|
|
1124
|
+
location: indexerSkill.sourceUrl,
|
|
1125
|
+
importedAt: /* @__PURE__ */ new Date()
|
|
1126
|
+
},
|
|
1127
|
+
// Extended fields for indexed skills
|
|
1128
|
+
taxonomy: indexerSkill.primaryPath ? {
|
|
1129
|
+
primaryPath: indexerSkill.primaryPath,
|
|
1130
|
+
secondaryPaths: indexerSkill.secondaryPaths,
|
|
1131
|
+
confidence: indexerSkill.confidence
|
|
1132
|
+
} : void 0,
|
|
1133
|
+
externalSource: {
|
|
1134
|
+
url: indexerSkill.sourceUrl,
|
|
1135
|
+
repo: indexerSkill.sourceRepo,
|
|
1136
|
+
scrapedAt: new Date(indexerSkill.scrapedAt)
|
|
1137
|
+
}
|
|
1138
|
+
};
|
|
1139
|
+
return { skill, warnings, hasStructuredContent };
|
|
1140
|
+
}
|
|
1141
|
+
function convertIndexerSkills(skills) {
|
|
1142
|
+
const results = [];
|
|
1143
|
+
const converted = [];
|
|
1144
|
+
for (const skill of skills) {
|
|
1145
|
+
const result = convertIndexerSkill(skill);
|
|
1146
|
+
results.push(result);
|
|
1147
|
+
converted.push(result.skill);
|
|
1148
|
+
}
|
|
1149
|
+
return {
|
|
1150
|
+
skills: converted,
|
|
1151
|
+
results,
|
|
1152
|
+
stats: {
|
|
1153
|
+
total: results.length,
|
|
1154
|
+
withStructuredContent: results.filter((r) => r.hasStructuredContent).length,
|
|
1155
|
+
withWarnings: results.filter((r) => r.warnings.length > 0).length
|
|
1156
|
+
}
|
|
1157
|
+
};
|
|
1158
|
+
}
|
|
1159
|
+
function parseIndexerExport(content) {
|
|
1160
|
+
const data = JSON.parse(content);
|
|
1161
|
+
let indexerSkills;
|
|
1162
|
+
let exportMetadata;
|
|
1163
|
+
if (Array.isArray(data)) {
|
|
1164
|
+
indexerSkills = data;
|
|
1165
|
+
} else if (data.skills && Array.isArray(data.skills)) {
|
|
1166
|
+
indexerSkills = data.skills;
|
|
1167
|
+
exportMetadata = {
|
|
1168
|
+
exportedAt: data.exportedAt,
|
|
1169
|
+
originalStats: data.stats
|
|
1170
|
+
};
|
|
1171
|
+
} else {
|
|
1172
|
+
throw new Error("Invalid indexer export format: expected array or object with skills array");
|
|
1173
|
+
}
|
|
1174
|
+
const { skills, stats } = convertIndexerSkills(indexerSkills);
|
|
1175
|
+
return { skills, stats, exportMetadata };
|
|
1176
|
+
}
|
|
1177
|
+
|
|
1178
|
+
// src/import/detect.ts
|
|
1179
|
+
function isSkillTreeSkill(obj) {
|
|
1180
|
+
if (typeof obj !== "object" || obj === null) return false;
|
|
1181
|
+
const skill = obj;
|
|
1182
|
+
return typeof skill.id === "string" && typeof skill.name === "string" && typeof skill.version === "string" && typeof skill.problem === "string" && typeof skill.solution === "string" && Array.isArray(skill.triggerConditions) && typeof skill.metrics === "object";
|
|
1183
|
+
}
|
|
1184
|
+
function isIndexerSkill(obj) {
|
|
1185
|
+
if (typeof obj !== "object" || obj === null) return false;
|
|
1186
|
+
const skill = obj;
|
|
1187
|
+
return typeof skill.slug === "string" && typeof skill.sourceRepo === "string" && typeof skill.sourcePath === "string" && typeof skill.sourceUrl === "string" && typeof skill.content === "string" && typeof skill.scrapedAt === "string" && (skill.status === "raw" || skill.status === "indexed" || skill.status === "failed");
|
|
1188
|
+
}
|
|
1189
|
+
function isIndexerExport(obj) {
|
|
1190
|
+
if (typeof obj !== "object" || obj === null) return false;
|
|
1191
|
+
const exp = obj;
|
|
1192
|
+
return typeof exp.exportedAt === "string" && Array.isArray(exp.skills);
|
|
1193
|
+
}
|
|
1194
|
+
function detectFormat(data) {
|
|
1195
|
+
if (Array.isArray(data)) {
|
|
1196
|
+
if (data.length === 0) {
|
|
1197
|
+
return { format: "unknown", confidence: 0, details: "Empty array" };
|
|
1198
|
+
}
|
|
1199
|
+
const first = data[0];
|
|
1200
|
+
if (isIndexerSkill(first)) {
|
|
1201
|
+
return { format: "indexer", confidence: 0.95, details: "Array of indexer skills" };
|
|
1202
|
+
}
|
|
1203
|
+
if (isSkillTreeSkill(first)) {
|
|
1204
|
+
return { format: "skill-tree", confidence: 0.95, details: "Array of skill-tree skills" };
|
|
1205
|
+
}
|
|
1206
|
+
return { format: "unknown", confidence: 0.3, details: "Array of unknown objects" };
|
|
1207
|
+
}
|
|
1208
|
+
if (typeof data === "object" && data !== null) {
|
|
1209
|
+
if (isIndexerExport(data)) {
|
|
1210
|
+
return { format: "indexer-export", confidence: 0.98, details: "Indexer export with metadata" };
|
|
1211
|
+
}
|
|
1212
|
+
if (isIndexerSkill(data)) {
|
|
1213
|
+
return { format: "indexer", confidence: 0.9, details: "Single indexer skill" };
|
|
1214
|
+
}
|
|
1215
|
+
if (isSkillTreeSkill(data)) {
|
|
1216
|
+
return { format: "skill-tree", confidence: 0.9, details: "Single skill-tree skill" };
|
|
1217
|
+
}
|
|
1218
|
+
}
|
|
1219
|
+
return { format: "unknown", confidence: 0, details: "Unrecognized format" };
|
|
1220
|
+
}
|
|
1221
|
+
function detectFormatFromString(content) {
|
|
1222
|
+
try {
|
|
1223
|
+
const data = JSON.parse(content);
|
|
1224
|
+
return detectFormat(data);
|
|
1225
|
+
} catch {
|
|
1226
|
+
return { format: "unknown", confidence: 0, details: "Invalid JSON" };
|
|
1227
|
+
}
|
|
1228
|
+
}
|
|
1229
|
+
function isLikelyIndexerFormat(content) {
|
|
1230
|
+
const result = detectFormatFromString(content);
|
|
1231
|
+
return result.format === "indexer" || result.format === "indexer-export";
|
|
1232
|
+
}
|
|
1233
|
+
|
|
1234
|
+
// src/cli/commands/import.ts
|
|
1235
|
+
var importCommand = new Command13("import").description("Import skills from JSON file").argument("<file>", "JSON file to import").option("--from-indexer", "Import from skill-indexer export format").option("--auto-detect", "Auto-detect format (default: true)", true).action(async (file, options, command) => {
|
|
1236
|
+
const globalOpts = command.optsWithGlobals();
|
|
1237
|
+
try {
|
|
1238
|
+
if (!fs4.existsSync(file)) {
|
|
1239
|
+
printError(`File not found: ${file}`);
|
|
1240
|
+
process.exit(1);
|
|
1241
|
+
}
|
|
1242
|
+
const content = fs4.readFileSync(file, "utf-8");
|
|
1243
|
+
let skills;
|
|
1244
|
+
const isIndexerFormat = options.fromIndexer || options.autoDetect !== false && isLikelyIndexerFormat(content);
|
|
1245
|
+
if (isIndexerFormat) {
|
|
1246
|
+
if (!globalOpts.quiet) {
|
|
1247
|
+
printInfo("Detected skill-indexer format, converting...");
|
|
1248
|
+
}
|
|
1249
|
+
try {
|
|
1250
|
+
const { skills: converted, stats, exportMetadata } = parseIndexerExport(content);
|
|
1251
|
+
skills = converted;
|
|
1252
|
+
if (!globalOpts.quiet) {
|
|
1253
|
+
printInfo(`Converted ${stats.total} skills (${stats.withStructuredContent} with structured content)`);
|
|
1254
|
+
if (exportMetadata) {
|
|
1255
|
+
printInfo(`Original export from: ${exportMetadata.exportedAt}`);
|
|
1256
|
+
}
|
|
1257
|
+
}
|
|
1258
|
+
} catch (err) {
|
|
1259
|
+
printError(`Failed to parse indexer export: ${err.message}`);
|
|
1260
|
+
process.exit(1);
|
|
1261
|
+
}
|
|
1262
|
+
} else {
|
|
1263
|
+
try {
|
|
1264
|
+
const parsed = JSON.parse(content);
|
|
1265
|
+
skills = Array.isArray(parsed) ? parsed : [parsed];
|
|
1266
|
+
} catch {
|
|
1267
|
+
printError("Invalid JSON file");
|
|
1268
|
+
process.exit(1);
|
|
1269
|
+
}
|
|
1270
|
+
for (const skill of skills) {
|
|
1271
|
+
skill.createdAt = new Date(skill.createdAt);
|
|
1272
|
+
skill.updatedAt = new Date(skill.updatedAt);
|
|
1273
|
+
if (skill.metrics.lastUsed) {
|
|
1274
|
+
skill.metrics.lastUsed = new Date(skill.metrics.lastUsed);
|
|
1275
|
+
}
|
|
1276
|
+
if (skill.source?.importedAt) {
|
|
1277
|
+
skill.source.importedAt = new Date(skill.source.importedAt);
|
|
1278
|
+
}
|
|
1279
|
+
if (skill.externalSource?.scrapedAt) {
|
|
1280
|
+
skill.externalSource.scrapedAt = new Date(skill.externalSource.scrapedAt);
|
|
1281
|
+
}
|
|
1282
|
+
}
|
|
1283
|
+
}
|
|
1284
|
+
const skillBank = await createSkillBankFromOptions(globalOpts);
|
|
1285
|
+
const result = await skillBank.importSkills(skills);
|
|
1286
|
+
if (globalOpts.json) {
|
|
1287
|
+
console.log(JSON.stringify({
|
|
1288
|
+
...result,
|
|
1289
|
+
format: isIndexerFormat ? "indexer" : "skill-tree"
|
|
1290
|
+
}, null, 2));
|
|
1291
|
+
return;
|
|
1292
|
+
}
|
|
1293
|
+
printSuccess(`Imported ${result.imported} skill(s)`);
|
|
1294
|
+
if (result.failed > 0) {
|
|
1295
|
+
printWarning(`Failed to import ${result.failed} skill(s)`);
|
|
1296
|
+
}
|
|
1297
|
+
} catch (error) {
|
|
1298
|
+
printError(error.message);
|
|
1299
|
+
process.exit(1);
|
|
1300
|
+
}
|
|
1301
|
+
});
|
|
1302
|
+
|
|
1303
|
+
// src/cli/commands/indexer/index.ts
|
|
1304
|
+
import { Command as Command20 } from "commander";
|
|
1305
|
+
|
|
1306
|
+
// src/cli/commands/indexer/scrape.ts
|
|
1307
|
+
import { Command as Command14 } from "commander";
|
|
1308
|
+
import { spawn } from "child_process";
|
|
1309
|
+
|
|
1310
|
+
// src/services/indexer.ts
|
|
1311
|
+
import * as path3 from "path";
|
|
1312
|
+
import * as fs5 from "fs";
|
|
1313
|
+
function hasIndexerSupport(storage) {
|
|
1314
|
+
const s = storage;
|
|
1315
|
+
return typeof s.addRelationship === "function" && typeof s.getTaxonomyTree === "function";
|
|
1316
|
+
}
|
|
1317
|
+
var IndexerService = class {
|
|
1318
|
+
constructor(config2 = {}, skillBank) {
|
|
1319
|
+
this.initialized = false;
|
|
1320
|
+
this.globalConfig = loadConfig();
|
|
1321
|
+
this.serviceConfig = {
|
|
1322
|
+
githubToken: config2.githubToken || this.globalConfig.indexer.github_token,
|
|
1323
|
+
anthropicApiKey: config2.anthropicApiKey || this.globalConfig.indexer.anthropic_key,
|
|
1324
|
+
batchSize: config2.batchSize || this.globalConfig.indexer.batch_size,
|
|
1325
|
+
minConfidence: config2.minConfidence || this.globalConfig.indexer.min_confidence,
|
|
1326
|
+
concurrency: config2.concurrency || 3,
|
|
1327
|
+
cacheTtlSeconds: config2.cacheTtlSeconds || 3600,
|
|
1328
|
+
databasePath: config2.databasePath,
|
|
1329
|
+
cacheDir: config2.cacheDir,
|
|
1330
|
+
claudeModel: config2.claudeModel
|
|
1331
|
+
};
|
|
1332
|
+
this.skillBank = skillBank;
|
|
1333
|
+
}
|
|
1334
|
+
/**
|
|
1335
|
+
* Get effective configuration
|
|
1336
|
+
*/
|
|
1337
|
+
getConfig() {
|
|
1338
|
+
return { ...this.serviceConfig };
|
|
1339
|
+
}
|
|
1340
|
+
/**
|
|
1341
|
+
* Initialize the service (lazy load scraper modules)
|
|
1342
|
+
*/
|
|
1343
|
+
async initialize() {
|
|
1344
|
+
if (this.initialized) return;
|
|
1345
|
+
try {
|
|
1346
|
+
const possiblePaths = [
|
|
1347
|
+
// Relative to this file in dist
|
|
1348
|
+
path3.resolve(__dirname, "../../scraper/dist"),
|
|
1349
|
+
// Relative to project root
|
|
1350
|
+
path3.resolve(process.cwd(), "scraper/dist"),
|
|
1351
|
+
// Absolute paths from config
|
|
1352
|
+
this.serviceConfig.cacheDir ? path3.resolve(this.serviceConfig.cacheDir, "../scraper/dist") : null
|
|
1353
|
+
].filter(Boolean);
|
|
1354
|
+
let scraperBasePath = null;
|
|
1355
|
+
for (const basePath of possiblePaths) {
|
|
1356
|
+
const scraperIndex = path3.join(basePath, "scraper/index.js");
|
|
1357
|
+
if (fs5.existsSync(scraperIndex)) {
|
|
1358
|
+
scraperBasePath = basePath;
|
|
1359
|
+
break;
|
|
1360
|
+
}
|
|
1361
|
+
}
|
|
1362
|
+
if (!scraperBasePath) {
|
|
1363
|
+
throw new Error("Scraper modules not found. Run `cd scraper && npm run build` first.");
|
|
1364
|
+
}
|
|
1365
|
+
const scraperPath = path3.join(scraperBasePath, "scraper/index.js");
|
|
1366
|
+
const indexerPath = path3.join(scraperBasePath, "indexer/index.js");
|
|
1367
|
+
const databasePath = path3.join(scraperBasePath, "database/index.js");
|
|
1368
|
+
this.scraperModule = await import(
|
|
1369
|
+
/* webpackIgnore: true */
|
|
1370
|
+
scraperPath
|
|
1371
|
+
);
|
|
1372
|
+
this.indexerModule = await import(
|
|
1373
|
+
/* webpackIgnore: true */
|
|
1374
|
+
indexerPath
|
|
1375
|
+
);
|
|
1376
|
+
this.databaseModule = await import(
|
|
1377
|
+
/* webpackIgnore: true */
|
|
1378
|
+
databasePath
|
|
1379
|
+
);
|
|
1380
|
+
if (this.databaseModule.createDatabase) {
|
|
1381
|
+
const dbPath = this.serviceConfig.databasePath || path3.join(process.cwd(), "scraper/data/skills.db");
|
|
1382
|
+
this.db = this.databaseModule.createDatabase(dbPath);
|
|
1383
|
+
}
|
|
1384
|
+
this.initialized = true;
|
|
1385
|
+
} catch (err) {
|
|
1386
|
+
console.warn(`Scraper modules not available: ${err.message}`);
|
|
1387
|
+
console.warn("Some indexer features will be limited.");
|
|
1388
|
+
this.initialized = true;
|
|
1389
|
+
}
|
|
1390
|
+
}
|
|
1391
|
+
/**
|
|
1392
|
+
* Check if the indexer is available
|
|
1393
|
+
*/
|
|
1394
|
+
isAvailable() {
|
|
1395
|
+
return this.initialized && !!this.scraperModule;
|
|
1396
|
+
}
|
|
1397
|
+
/**
|
|
1398
|
+
* Check if running in degraded mode (no scraper modules)
|
|
1399
|
+
*/
|
|
1400
|
+
isDegradedMode() {
|
|
1401
|
+
return this.initialized && !this.scraperModule;
|
|
1402
|
+
}
|
|
1403
|
+
/**
|
|
1404
|
+
* Scrape skills from GitHub sources
|
|
1405
|
+
*/
|
|
1406
|
+
async scrape(sources, options) {
|
|
1407
|
+
await this.initialize();
|
|
1408
|
+
if (!this.scraperModule || !this.db) {
|
|
1409
|
+
throw new Error(
|
|
1410
|
+
"Scraper not available. Build the scraper module first: cd scraper && npm run build"
|
|
1411
|
+
);
|
|
1412
|
+
}
|
|
1413
|
+
const result = {
|
|
1414
|
+
discovered: 0,
|
|
1415
|
+
scraped: 0,
|
|
1416
|
+
skipped: 0,
|
|
1417
|
+
failed: 0,
|
|
1418
|
+
unchanged: 0,
|
|
1419
|
+
errors: []
|
|
1420
|
+
};
|
|
1421
|
+
try {
|
|
1422
|
+
const scraper = this.scraperModule.createScraper({
|
|
1423
|
+
githubToken: this.serviceConfig.githubToken,
|
|
1424
|
+
cacheDir: this.serviceConfig.cacheDir,
|
|
1425
|
+
cacheTtlSeconds: this.serviceConfig.cacheTtlSeconds
|
|
1426
|
+
});
|
|
1427
|
+
for (const source of sources) {
|
|
1428
|
+
try {
|
|
1429
|
+
let skills;
|
|
1430
|
+
if (source.type === "awesome-list") {
|
|
1431
|
+
skills = await scraper.scrapeAwesomeList(source.url, {
|
|
1432
|
+
force: options?.force
|
|
1433
|
+
});
|
|
1434
|
+
} else {
|
|
1435
|
+
skills = await scraper.scrapeRepository(source.url, {
|
|
1436
|
+
force: options?.force
|
|
1437
|
+
});
|
|
1438
|
+
}
|
|
1439
|
+
result.discovered += skills.length;
|
|
1440
|
+
for (const skill of skills) {
|
|
1441
|
+
try {
|
|
1442
|
+
const existing = this.db.getSkillBySlug?.(skill.slug);
|
|
1443
|
+
if (existing && !options?.force) {
|
|
1444
|
+
result.unchanged++;
|
|
1445
|
+
} else {
|
|
1446
|
+
this.db.saveSkill?.(skill);
|
|
1447
|
+
result.scraped++;
|
|
1448
|
+
}
|
|
1449
|
+
} catch (err) {
|
|
1450
|
+
result.failed++;
|
|
1451
|
+
result.errors.push(`Failed to save skill ${skill.slug}: ${err.message}`);
|
|
1452
|
+
}
|
|
1453
|
+
}
|
|
1454
|
+
} catch (err) {
|
|
1455
|
+
result.failed++;
|
|
1456
|
+
result.errors.push(`Failed to scrape ${source.url}: ${err.message}`);
|
|
1457
|
+
}
|
|
1458
|
+
}
|
|
1459
|
+
} catch (err) {
|
|
1460
|
+
result.errors.push(`Scrape failed: ${err.message}`);
|
|
1461
|
+
}
|
|
1462
|
+
return result;
|
|
1463
|
+
}
|
|
1464
|
+
/**
|
|
1465
|
+
* Classify unindexed skills using AI
|
|
1466
|
+
*/
|
|
1467
|
+
async classify(options) {
|
|
1468
|
+
await this.initialize();
|
|
1469
|
+
if (!this.indexerModule || !this.db) {
|
|
1470
|
+
throw new Error(
|
|
1471
|
+
"Indexer not available. Build the scraper module first: cd scraper && npm run build"
|
|
1472
|
+
);
|
|
1473
|
+
}
|
|
1474
|
+
const result = {
|
|
1475
|
+
indexed: 0,
|
|
1476
|
+
skipped: 0,
|
|
1477
|
+
failed: 0,
|
|
1478
|
+
errors: []
|
|
1479
|
+
};
|
|
1480
|
+
try {
|
|
1481
|
+
const indexer = this.indexerModule.createIndexer({
|
|
1482
|
+
anthropicApiKey: this.serviceConfig.anthropicApiKey,
|
|
1483
|
+
model: this.serviceConfig.claudeModel || "claude-sonnet-4-20250514",
|
|
1484
|
+
minConfidence: this.serviceConfig.minConfidence
|
|
1485
|
+
});
|
|
1486
|
+
let skills;
|
|
1487
|
+
if (options?.skillId) {
|
|
1488
|
+
const skill = this.db.getSkillBySlug?.(options.skillId);
|
|
1489
|
+
skills = skill ? [skill] : [];
|
|
1490
|
+
} else if (options?.all) {
|
|
1491
|
+
skills = this.db.getAllSkills?.() || [];
|
|
1492
|
+
} else {
|
|
1493
|
+
skills = this.db.getSkillsByStatus?.("raw") || [];
|
|
1494
|
+
}
|
|
1495
|
+
const batchSize = this.serviceConfig.batchSize || 10;
|
|
1496
|
+
for (let i = 0; i < skills.length; i += batchSize) {
|
|
1497
|
+
const batch = skills.slice(i, i + batchSize);
|
|
1498
|
+
for (const skill of batch) {
|
|
1499
|
+
if (skill.status === "indexed" && !options?.all) {
|
|
1500
|
+
result.skipped++;
|
|
1501
|
+
continue;
|
|
1502
|
+
}
|
|
1503
|
+
try {
|
|
1504
|
+
const classification = await indexer.classifySkill(skill);
|
|
1505
|
+
skill.status = "indexed";
|
|
1506
|
+
skill.primaryPath = classification.primaryPath;
|
|
1507
|
+
skill.secondaryPaths = classification.secondaryPaths;
|
|
1508
|
+
skill.confidence = classification.confidence;
|
|
1509
|
+
skill.classificationReasoning = classification.reasoning;
|
|
1510
|
+
skill.indexedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
1511
|
+
this.db.saveSkill?.(skill);
|
|
1512
|
+
result.indexed++;
|
|
1513
|
+
} catch (err) {
|
|
1514
|
+
result.failed++;
|
|
1515
|
+
result.errors.push(`Failed to classify ${skill.slug}: ${err.message}`);
|
|
1516
|
+
}
|
|
1517
|
+
}
|
|
1518
|
+
}
|
|
1519
|
+
} catch (err) {
|
|
1520
|
+
result.errors.push(`Classification failed: ${err.message}`);
|
|
1521
|
+
}
|
|
1522
|
+
return result;
|
|
1523
|
+
}
|
|
1524
|
+
/**
|
|
1525
|
+
* Detect relationships between skills
|
|
1526
|
+
*/
|
|
1527
|
+
async detectRelationships(options) {
|
|
1528
|
+
await this.initialize();
|
|
1529
|
+
const result = {
|
|
1530
|
+
detected: 0,
|
|
1531
|
+
skipped: 0,
|
|
1532
|
+
errors: []
|
|
1533
|
+
};
|
|
1534
|
+
if (this.skillBank) {
|
|
1535
|
+
try {
|
|
1536
|
+
const storage = this.skillBank.getStorage();
|
|
1537
|
+
if (hasIndexerSupport(storage)) {
|
|
1538
|
+
const skills = await this.skillBank.listSkills();
|
|
1539
|
+
const targetSkills = options?.skillId ? skills.filter((s) => s.id === options.skillId) : skills;
|
|
1540
|
+
for (const skill of targetSkills) {
|
|
1541
|
+
const relationships = this.detectSkillRelationships(skill, skills);
|
|
1542
|
+
for (const rel of relationships) {
|
|
1543
|
+
try {
|
|
1544
|
+
await storage.addRelationship(
|
|
1545
|
+
skill.id,
|
|
1546
|
+
rel.targetSkillId,
|
|
1547
|
+
rel.type,
|
|
1548
|
+
rel.confidence,
|
|
1549
|
+
rel.reasoning
|
|
1550
|
+
);
|
|
1551
|
+
result.detected++;
|
|
1552
|
+
} catch (err) {
|
|
1553
|
+
result.skipped++;
|
|
1554
|
+
}
|
|
1555
|
+
}
|
|
1556
|
+
}
|
|
1557
|
+
}
|
|
1558
|
+
} catch (err) {
|
|
1559
|
+
result.errors.push(`Relationship detection failed: ${err.message}`);
|
|
1560
|
+
}
|
|
1561
|
+
return result;
|
|
1562
|
+
}
|
|
1563
|
+
if (!this.db) {
|
|
1564
|
+
throw new Error(
|
|
1565
|
+
"Neither SkillBank nor scraper database available."
|
|
1566
|
+
);
|
|
1567
|
+
}
|
|
1568
|
+
try {
|
|
1569
|
+
const skills = this.db.getAllSkills?.() || [];
|
|
1570
|
+
const targetSkills = options?.skillId ? skills.filter((s) => s.slug === options.skillId) : skills.filter((s) => s.status === "indexed");
|
|
1571
|
+
for (const skill of targetSkills) {
|
|
1572
|
+
const relationships = this.detectSkillRelationships(skill, skills);
|
|
1573
|
+
for (const rel of relationships) {
|
|
1574
|
+
try {
|
|
1575
|
+
this.db.saveRelationship?.({
|
|
1576
|
+
sourceSkillId: skill.id,
|
|
1577
|
+
...rel
|
|
1578
|
+
});
|
|
1579
|
+
result.detected++;
|
|
1580
|
+
} catch (err) {
|
|
1581
|
+
result.skipped++;
|
|
1582
|
+
}
|
|
1583
|
+
}
|
|
1584
|
+
}
|
|
1585
|
+
} catch (err) {
|
|
1586
|
+
result.errors.push(`Relationship detection failed: ${err.message}`);
|
|
1587
|
+
}
|
|
1588
|
+
return result;
|
|
1589
|
+
}
|
|
1590
|
+
/**
|
|
1591
|
+
* Detect relationships for a single skill
|
|
1592
|
+
*/
|
|
1593
|
+
detectSkillRelationships(skill, allSkills) {
|
|
1594
|
+
const relationships = [];
|
|
1595
|
+
const skillId = skill.id || skill.slug;
|
|
1596
|
+
const skillContent = `${skill.name} ${skill.description} ${skill.problem || ""} ${skill.solution || ""} ${skill.content || ""}`.toLowerCase();
|
|
1597
|
+
for (const other of allSkills) {
|
|
1598
|
+
const otherId = other.id || other.slug;
|
|
1599
|
+
if (otherId === skillId) continue;
|
|
1600
|
+
const otherName = (other.name || "").toLowerCase();
|
|
1601
|
+
const dependencyPatterns = ["requires", "depends on", "uses", "needs", "builds on"];
|
|
1602
|
+
for (const pattern of dependencyPatterns) {
|
|
1603
|
+
if (skillContent.includes(`${pattern} ${otherName}`)) {
|
|
1604
|
+
relationships.push({
|
|
1605
|
+
targetSkillId: otherId,
|
|
1606
|
+
type: "depends_on",
|
|
1607
|
+
confidence: 0.7,
|
|
1608
|
+
reasoning: `Content mentions "${pattern} ${other.name}"`
|
|
1609
|
+
});
|
|
1610
|
+
break;
|
|
1611
|
+
}
|
|
1612
|
+
}
|
|
1613
|
+
const extensionPatterns = ["extends", "improves", "enhances", "builds upon"];
|
|
1614
|
+
for (const pattern of extensionPatterns) {
|
|
1615
|
+
if (skillContent.includes(`${pattern} ${otherName}`)) {
|
|
1616
|
+
relationships.push({
|
|
1617
|
+
targetSkillId: otherId,
|
|
1618
|
+
type: "extends",
|
|
1619
|
+
confidence: 0.7,
|
|
1620
|
+
reasoning: `Content mentions "${pattern} ${other.name}"`
|
|
1621
|
+
});
|
|
1622
|
+
break;
|
|
1623
|
+
}
|
|
1624
|
+
}
|
|
1625
|
+
const alternativePatterns = ["alternative to", "instead of", "replacement for"];
|
|
1626
|
+
for (const pattern of alternativePatterns) {
|
|
1627
|
+
if (skillContent.includes(`${pattern} ${otherName}`)) {
|
|
1628
|
+
relationships.push({
|
|
1629
|
+
targetSkillId: otherId,
|
|
1630
|
+
type: "alternative",
|
|
1631
|
+
confidence: 0.6,
|
|
1632
|
+
reasoning: `Content mentions "${pattern} ${other.name}"`
|
|
1633
|
+
});
|
|
1634
|
+
break;
|
|
1635
|
+
}
|
|
1636
|
+
}
|
|
1637
|
+
const skillPath = skill.taxonomy?.primaryPath || skill.primaryPath || [];
|
|
1638
|
+
const otherPath = other.taxonomy?.primaryPath || other.primaryPath || [];
|
|
1639
|
+
if (skillPath.length >= 2 && otherPath.length >= 2) {
|
|
1640
|
+
if (skillPath[0] === otherPath[0] && skillPath[1] === otherPath[1]) {
|
|
1641
|
+
relationships.push({
|
|
1642
|
+
targetSkillId: otherId,
|
|
1643
|
+
type: "related",
|
|
1644
|
+
confidence: 0.5,
|
|
1645
|
+
reasoning: `Same taxonomy category: ${skillPath.slice(0, 2).join(" > ")}`
|
|
1646
|
+
});
|
|
1647
|
+
}
|
|
1648
|
+
}
|
|
1649
|
+
}
|
|
1650
|
+
return relationships;
|
|
1651
|
+
}
|
|
1652
|
+
/**
|
|
1653
|
+
* Get taxonomy tree
|
|
1654
|
+
*/
|
|
1655
|
+
async getTaxonomyTree(rootPath) {
|
|
1656
|
+
await this.initialize();
|
|
1657
|
+
if (this.skillBank) {
|
|
1658
|
+
const storage = this.skillBank.getStorage();
|
|
1659
|
+
if (hasIndexerSupport(storage)) {
|
|
1660
|
+
const nodes = await storage.getTaxonomyTree(rootPath);
|
|
1661
|
+
return this.wrapTreeNodes(nodes, rootPath);
|
|
1662
|
+
}
|
|
1663
|
+
}
|
|
1664
|
+
const skills = this.skillBank ? await this.skillBank.listSkills() : this.db?.getAllSkills?.() || [];
|
|
1665
|
+
return this.buildTaxonomyTreeFromSkills(skills, rootPath);
|
|
1666
|
+
}
|
|
1667
|
+
/**
|
|
1668
|
+
* Wrap tree nodes returned from SQLite storage into a root node
|
|
1669
|
+
*/
|
|
1670
|
+
wrapTreeNodes(nodes, rootPath) {
|
|
1671
|
+
const root = {
|
|
1672
|
+
id: "root",
|
|
1673
|
+
name: rootPath?.join(" > ") || "All Skills",
|
|
1674
|
+
path: rootPath || [],
|
|
1675
|
+
skillCount: 0,
|
|
1676
|
+
children: []
|
|
1677
|
+
};
|
|
1678
|
+
for (const node of nodes) {
|
|
1679
|
+
root.children.push(this.convertToTaxonomyNode(node));
|
|
1680
|
+
root.skillCount += this.countNodeSkills(node);
|
|
1681
|
+
}
|
|
1682
|
+
return root;
|
|
1683
|
+
}
|
|
1684
|
+
/**
|
|
1685
|
+
* Convert storage tree node to TaxonomyNode format
|
|
1686
|
+
*/
|
|
1687
|
+
convertToTaxonomyNode(node) {
|
|
1688
|
+
return {
|
|
1689
|
+
id: node.id,
|
|
1690
|
+
name: node.name,
|
|
1691
|
+
path: Array.isArray(node.path) ? node.path : (node.path || "").split("/"),
|
|
1692
|
+
skillCount: node.skillCount || 0,
|
|
1693
|
+
children: (node.children || []).map((child) => this.convertToTaxonomyNode(child))
|
|
1694
|
+
};
|
|
1695
|
+
}
|
|
1696
|
+
/**
|
|
1697
|
+
* Count total skills in a node tree
|
|
1698
|
+
*/
|
|
1699
|
+
countNodeSkills(node) {
|
|
1700
|
+
let count = node.skillCount || 0;
|
|
1701
|
+
for (const child of node.children || []) {
|
|
1702
|
+
count += this.countNodeSkills(child);
|
|
1703
|
+
}
|
|
1704
|
+
return count;
|
|
1705
|
+
}
|
|
1706
|
+
/**
|
|
1707
|
+
* Build taxonomy tree from flat nodes
|
|
1708
|
+
*/
|
|
1709
|
+
buildTaxonomyTree(nodes, rootPath) {
|
|
1710
|
+
const root = {
|
|
1711
|
+
id: "root",
|
|
1712
|
+
name: rootPath?.join(" > ") || "All Skills",
|
|
1713
|
+
path: rootPath || [],
|
|
1714
|
+
skillCount: 0,
|
|
1715
|
+
children: []
|
|
1716
|
+
};
|
|
1717
|
+
const nodeMap = /* @__PURE__ */ new Map();
|
|
1718
|
+
nodeMap.set("root", root);
|
|
1719
|
+
for (const node of nodes) {
|
|
1720
|
+
const taxNode = {
|
|
1721
|
+
id: node.id,
|
|
1722
|
+
name: node.name,
|
|
1723
|
+
path: node.path,
|
|
1724
|
+
skillCount: node.skillCount || 0,
|
|
1725
|
+
children: []
|
|
1726
|
+
};
|
|
1727
|
+
nodeMap.set(node.id, taxNode);
|
|
1728
|
+
}
|
|
1729
|
+
for (const node of nodes) {
|
|
1730
|
+
const taxNode = nodeMap.get(node.id);
|
|
1731
|
+
const parentId = node.parentId || "root";
|
|
1732
|
+
const parent = nodeMap.get(parentId);
|
|
1733
|
+
if (parent) {
|
|
1734
|
+
parent.children.push(taxNode);
|
|
1735
|
+
parent.skillCount += taxNode.skillCount;
|
|
1736
|
+
}
|
|
1737
|
+
}
|
|
1738
|
+
return root;
|
|
1739
|
+
}
|
|
1740
|
+
/**
|
|
1741
|
+
* Build taxonomy tree from skills
|
|
1742
|
+
*/
|
|
1743
|
+
buildTaxonomyTreeFromSkills(skills, rootPath) {
|
|
1744
|
+
const root = {
|
|
1745
|
+
id: "root",
|
|
1746
|
+
name: rootPath?.join(" > ") || "All Skills",
|
|
1747
|
+
path: rootPath || [],
|
|
1748
|
+
skillCount: 0,
|
|
1749
|
+
children: []
|
|
1750
|
+
};
|
|
1751
|
+
const nodeMap = /* @__PURE__ */ new Map();
|
|
1752
|
+
for (const skill of skills) {
|
|
1753
|
+
const taxonomy = skill.taxonomy || (skill.primaryPath ? { primaryPath: skill.primaryPath } : null);
|
|
1754
|
+
if (!taxonomy?.primaryPath) continue;
|
|
1755
|
+
const skillPath = taxonomy.primaryPath;
|
|
1756
|
+
if (rootPath && rootPath.length > 0) {
|
|
1757
|
+
let matches = true;
|
|
1758
|
+
for (let i = 0; i < rootPath.length; i++) {
|
|
1759
|
+
if (skillPath[i] !== rootPath[i]) {
|
|
1760
|
+
matches = false;
|
|
1761
|
+
break;
|
|
1762
|
+
}
|
|
1763
|
+
}
|
|
1764
|
+
if (!matches) continue;
|
|
1765
|
+
}
|
|
1766
|
+
let currentPath = [];
|
|
1767
|
+
let parent = root;
|
|
1768
|
+
for (const segment of skillPath) {
|
|
1769
|
+
currentPath = [...currentPath, segment];
|
|
1770
|
+
const pathKey = currentPath.join("/");
|
|
1771
|
+
let node = nodeMap.get(pathKey);
|
|
1772
|
+
if (!node) {
|
|
1773
|
+
node = {
|
|
1774
|
+
id: pathKey,
|
|
1775
|
+
name: segment,
|
|
1776
|
+
path: [...currentPath],
|
|
1777
|
+
skillCount: 0,
|
|
1778
|
+
children: []
|
|
1779
|
+
};
|
|
1780
|
+
nodeMap.set(pathKey, node);
|
|
1781
|
+
parent.children.push(node);
|
|
1782
|
+
}
|
|
1783
|
+
parent = node;
|
|
1784
|
+
}
|
|
1785
|
+
parent.skillCount++;
|
|
1786
|
+
root.skillCount++;
|
|
1787
|
+
}
|
|
1788
|
+
return root;
|
|
1789
|
+
}
|
|
1790
|
+
/**
|
|
1791
|
+
* Get indexer statistics
|
|
1792
|
+
*/
|
|
1793
|
+
async getStats() {
|
|
1794
|
+
await this.initialize();
|
|
1795
|
+
if (this.skillBank) {
|
|
1796
|
+
const skills = await this.skillBank.listSkills();
|
|
1797
|
+
const storage = this.skillBank.getStorage();
|
|
1798
|
+
let taxonomyNodes = 0;
|
|
1799
|
+
let relationships = 0;
|
|
1800
|
+
let sources = 0;
|
|
1801
|
+
if (storage) {
|
|
1802
|
+
if (hasIndexerSupport(storage)) {
|
|
1803
|
+
const tree = await storage.getTaxonomyTree();
|
|
1804
|
+
taxonomyNodes = this.countTaxonomyNodes(tree);
|
|
1805
|
+
if (storage.getRelationships) {
|
|
1806
|
+
for (const skill of skills) {
|
|
1807
|
+
const rels = await storage.getRelationships(skill.id);
|
|
1808
|
+
relationships += rels.length;
|
|
1809
|
+
}
|
|
1810
|
+
}
|
|
1811
|
+
}
|
|
1812
|
+
const sourceSet = /* @__PURE__ */ new Set();
|
|
1813
|
+
for (const skill of skills) {
|
|
1814
|
+
if (skill.externalSource?.repo) {
|
|
1815
|
+
sourceSet.add(skill.externalSource.repo);
|
|
1816
|
+
}
|
|
1817
|
+
if (skill.source?.type === "imported" && skill.externalSource?.url) {
|
|
1818
|
+
sourceSet.add(skill.externalSource.url);
|
|
1819
|
+
}
|
|
1820
|
+
}
|
|
1821
|
+
sources = sourceSet.size;
|
|
1822
|
+
}
|
|
1823
|
+
const indexed = skills.filter(
|
|
1824
|
+
(s) => s.status === "active" && (s.taxonomy || s.source?.type === "imported")
|
|
1825
|
+
).length;
|
|
1826
|
+
return {
|
|
1827
|
+
totalSkills: skills.length,
|
|
1828
|
+
indexedSkills: indexed,
|
|
1829
|
+
rawSkills: skills.filter((s) => s.status === "draft").length,
|
|
1830
|
+
failedSkills: skills.filter((s) => s.status === "deprecated").length,
|
|
1831
|
+
taxonomyNodes,
|
|
1832
|
+
relationships,
|
|
1833
|
+
sources
|
|
1834
|
+
};
|
|
1835
|
+
}
|
|
1836
|
+
if (this.db) {
|
|
1837
|
+
const stats = this.db.getStats?.() || {};
|
|
1838
|
+
return {
|
|
1839
|
+
totalSkills: stats.totalSkills || 0,
|
|
1840
|
+
indexedSkills: stats.indexedSkills || 0,
|
|
1841
|
+
rawSkills: stats.rawSkills || 0,
|
|
1842
|
+
failedSkills: stats.failedSkills || 0,
|
|
1843
|
+
taxonomyNodes: stats.taxonomyNodes || 0,
|
|
1844
|
+
relationships: stats.relationships || 0,
|
|
1845
|
+
sources: stats.sources || 0
|
|
1846
|
+
};
|
|
1847
|
+
}
|
|
1848
|
+
return {
|
|
1849
|
+
totalSkills: 0,
|
|
1850
|
+
indexedSkills: 0,
|
|
1851
|
+
rawSkills: 0,
|
|
1852
|
+
failedSkills: 0,
|
|
1853
|
+
taxonomyNodes: 0,
|
|
1854
|
+
relationships: 0,
|
|
1855
|
+
sources: 0
|
|
1856
|
+
};
|
|
1857
|
+
}
|
|
1858
|
+
/**
|
|
1859
|
+
* Count nodes in taxonomy tree
|
|
1860
|
+
*/
|
|
1861
|
+
countTaxonomyNodes(nodes) {
|
|
1862
|
+
let count = 0;
|
|
1863
|
+
for (const node of nodes) {
|
|
1864
|
+
count++;
|
|
1865
|
+
if (node.children) {
|
|
1866
|
+
count += this.countTaxonomyNodes(node.children);
|
|
1867
|
+
}
|
|
1868
|
+
}
|
|
1869
|
+
return count;
|
|
1870
|
+
}
|
|
1871
|
+
/**
|
|
1872
|
+
* Scrape and index skills directly into SkillBank
|
|
1873
|
+
* This is the streamlined workflow for integrated mode
|
|
1874
|
+
*/
|
|
1875
|
+
async scrapeAndIndex(sources, options) {
|
|
1876
|
+
if (!this.skillBank) {
|
|
1877
|
+
throw new Error("SkillBank required for scrapeAndIndex. Use integrated mode.");
|
|
1878
|
+
}
|
|
1879
|
+
await this.initialize();
|
|
1880
|
+
const skillsAdded = [];
|
|
1881
|
+
const scraped = await this.scrape(sources, { force: options?.force });
|
|
1882
|
+
let indexed = { indexed: 0, skipped: 0, failed: 0, errors: [] };
|
|
1883
|
+
if (options?.autoClassify !== false) {
|
|
1884
|
+
indexed = await this.classify({ all: options?.force });
|
|
1885
|
+
}
|
|
1886
|
+
if (this.db) {
|
|
1887
|
+
const skills = this.db.getAllSkills?.() || [];
|
|
1888
|
+
for (const skill of skills) {
|
|
1889
|
+
if (skill.status !== "indexed" && !options?.force) continue;
|
|
1890
|
+
try {
|
|
1891
|
+
const converted = convertIndexerSkill(skill);
|
|
1892
|
+
await this.skillBank.addSkill(converted.skill);
|
|
1893
|
+
skillsAdded.push(converted.skill.id);
|
|
1894
|
+
} catch (err) {
|
|
1895
|
+
indexed.errors.push(`Failed to import ${skill.slug}: ${err.message}`);
|
|
1896
|
+
}
|
|
1897
|
+
}
|
|
1898
|
+
}
|
|
1899
|
+
let relationships = { detected: 0, skipped: 0, errors: [] };
|
|
1900
|
+
if (options?.detectRelationships !== false) {
|
|
1901
|
+
relationships = await this.detectRelationships();
|
|
1902
|
+
}
|
|
1903
|
+
return {
|
|
1904
|
+
scraped,
|
|
1905
|
+
indexed,
|
|
1906
|
+
relationships,
|
|
1907
|
+
skillsAdded
|
|
1908
|
+
};
|
|
1909
|
+
}
|
|
1910
|
+
/**
|
|
1911
|
+
* Import skills from indexer database into SkillBank
|
|
1912
|
+
*/
|
|
1913
|
+
async importFromIndexerDb(options) {
|
|
1914
|
+
if (!this.skillBank) {
|
|
1915
|
+
throw new Error("SkillBank not configured. Use integrated mode.");
|
|
1916
|
+
}
|
|
1917
|
+
await this.initialize();
|
|
1918
|
+
if (!this.db) {
|
|
1919
|
+
throw new Error(
|
|
1920
|
+
"Scraper database not available. Build the scraper module first."
|
|
1921
|
+
);
|
|
1922
|
+
}
|
|
1923
|
+
const result = { imported: 0, failed: 0, skills: [] };
|
|
1924
|
+
try {
|
|
1925
|
+
let skills;
|
|
1926
|
+
if (options?.status) {
|
|
1927
|
+
skills = this.db.getSkillsByStatus?.(options.status) || [];
|
|
1928
|
+
} else {
|
|
1929
|
+
skills = this.db.getAllSkills?.() || [];
|
|
1930
|
+
}
|
|
1931
|
+
if (options?.limit) {
|
|
1932
|
+
skills = skills.slice(0, options.limit);
|
|
1933
|
+
}
|
|
1934
|
+
for (const skill of skills) {
|
|
1935
|
+
try {
|
|
1936
|
+
const converted = convertIndexerSkill(skill);
|
|
1937
|
+
await this.skillBank.addSkill(converted.skill);
|
|
1938
|
+
result.imported++;
|
|
1939
|
+
result.skills.push(converted.skill);
|
|
1940
|
+
} catch (err) {
|
|
1941
|
+
result.failed++;
|
|
1942
|
+
}
|
|
1943
|
+
}
|
|
1944
|
+
} catch (err) {
|
|
1945
|
+
throw new Error(`Import failed: ${err.message}`);
|
|
1946
|
+
}
|
|
1947
|
+
return result;
|
|
1948
|
+
}
|
|
1949
|
+
/**
|
|
1950
|
+
* Get default skill sources from config
|
|
1951
|
+
*/
|
|
1952
|
+
getDefaultSources() {
|
|
1953
|
+
const configSources = this.globalConfig.indexer.sources;
|
|
1954
|
+
if (configSources.length > 0) {
|
|
1955
|
+
return configSources.map((url) => ({
|
|
1956
|
+
type: "awesome-list",
|
|
1957
|
+
url
|
|
1958
|
+
}));
|
|
1959
|
+
}
|
|
1960
|
+
return [
|
|
1961
|
+
{ type: "awesome-list", url: "https://github.com/VoltAgent/awesome-agent-skills" }
|
|
1962
|
+
];
|
|
1963
|
+
}
|
|
1964
|
+
/**
|
|
1965
|
+
* Close database connections
|
|
1966
|
+
*/
|
|
1967
|
+
async close() {
|
|
1968
|
+
if (this.db && typeof this.db.close === "function") {
|
|
1969
|
+
this.db.close();
|
|
1970
|
+
}
|
|
1971
|
+
this.db = void 0;
|
|
1972
|
+
this.initialized = false;
|
|
1973
|
+
}
|
|
1974
|
+
};
|
|
1975
|
+
function createIntegratedIndexer(skillBank, config2 = {}) {
|
|
1976
|
+
return new IndexerService(config2, skillBank);
|
|
1977
|
+
}
|
|
1978
|
+
|
|
1979
|
+
// src/cli/commands/indexer/scrape.ts
|
|
1980
|
+
var scrapeCommand = new Command14("scrape").description("Scrape skills from GitHub sources").argument("[url]", "Repository or awesome-list URL").option("-d, --discover", "Discover from default sources").option("-f, --force", "Force scrape even if no changes detected").option("--standalone", "Use standalone skillindexer CLI (fallback)").option("--import", "Auto-import scraped skills into skill-tree", true).action(async (url, options, command) => {
|
|
1981
|
+
const globalOpts = command.optsWithGlobals();
|
|
1982
|
+
if (options.standalone) {
|
|
1983
|
+
await runStandaloneMode(url, options, globalOpts);
|
|
1984
|
+
return;
|
|
1985
|
+
}
|
|
1986
|
+
try {
|
|
1987
|
+
const skillBank = await getSkillBank(globalOpts);
|
|
1988
|
+
const indexer = createIntegratedIndexer(skillBank, {
|
|
1989
|
+
githubToken: process.env.GITHUB_TOKEN,
|
|
1990
|
+
anthropicApiKey: process.env.ANTHROPIC_API_KEY
|
|
1991
|
+
});
|
|
1992
|
+
let sources;
|
|
1993
|
+
if (url) {
|
|
1994
|
+
sources = [{
|
|
1995
|
+
type: url.includes("awesome") ? "awesome-list" : "repository",
|
|
1996
|
+
url
|
|
1997
|
+
}];
|
|
1998
|
+
} else if (options.discover) {
|
|
1999
|
+
sources = indexer.getDefaultSources();
|
|
2000
|
+
} else {
|
|
2001
|
+
printError("Please provide a URL or use --discover to scrape from default sources");
|
|
2002
|
+
process.exit(1);
|
|
2003
|
+
}
|
|
2004
|
+
if (!globalOpts.quiet) {
|
|
2005
|
+
printInfo(`Scraping ${sources.length} source(s)...`);
|
|
2006
|
+
for (const source of sources) {
|
|
2007
|
+
printInfo(` - ${source.url} (${source.type})`);
|
|
2008
|
+
}
|
|
2009
|
+
printInfo("");
|
|
2010
|
+
}
|
|
2011
|
+
const result = await indexer.scrape(sources, { force: options.force });
|
|
2012
|
+
if (globalOpts.json) {
|
|
2013
|
+
console.log(JSON.stringify(result, null, 2));
|
|
2014
|
+
} else if (!globalOpts.quiet) {
|
|
2015
|
+
printSuccess("Scrape completed");
|
|
2016
|
+
console.log(` Discovered: ${result.discovered}`);
|
|
2017
|
+
console.log(` Scraped: ${result.scraped}`);
|
|
2018
|
+
console.log(` Unchanged: ${result.unchanged}`);
|
|
2019
|
+
console.log(` Skipped: ${result.skipped}`);
|
|
2020
|
+
console.log(` Failed: ${result.failed}`);
|
|
2021
|
+
if (result.errors.length > 0) {
|
|
2022
|
+
printWarning("\nErrors:");
|
|
2023
|
+
for (const error of result.errors) {
|
|
2024
|
+
console.log(` - ${error}`);
|
|
2025
|
+
}
|
|
2026
|
+
}
|
|
2027
|
+
}
|
|
2028
|
+
if (options.import && result.scraped > 0) {
|
|
2029
|
+
if (!globalOpts.quiet) {
|
|
2030
|
+
printInfo("\nImporting scraped skills into skill-tree...");
|
|
2031
|
+
}
|
|
2032
|
+
try {
|
|
2033
|
+
const importResult = await indexer.importFromIndexerDb({ status: "indexed" });
|
|
2034
|
+
if (!globalOpts.quiet) {
|
|
2035
|
+
printSuccess(`Imported ${importResult.imported} skills`);
|
|
2036
|
+
if (importResult.failed > 0) {
|
|
2037
|
+
printWarning(`Failed to import ${importResult.failed} skills`);
|
|
2038
|
+
}
|
|
2039
|
+
}
|
|
2040
|
+
} catch (err) {
|
|
2041
|
+
printWarning(`Auto-import failed: ${err.message}`);
|
|
2042
|
+
printInfo("You can manually import with: skill-tree import");
|
|
2043
|
+
}
|
|
2044
|
+
}
|
|
2045
|
+
await indexer.close();
|
|
2046
|
+
} catch (err) {
|
|
2047
|
+
const errorMessage = err.message;
|
|
2048
|
+
if (errorMessage.includes("Scraper not available") || errorMessage.includes("not found")) {
|
|
2049
|
+
printWarning("IndexerService not available, falling back to standalone CLI...");
|
|
2050
|
+
await runStandaloneMode(url, options, globalOpts);
|
|
2051
|
+
return;
|
|
2052
|
+
}
|
|
2053
|
+
printError(errorMessage);
|
|
2054
|
+
process.exit(1);
|
|
2055
|
+
}
|
|
2056
|
+
});
|
|
2057
|
+
async function runStandaloneMode(url, options, globalOpts) {
|
|
2058
|
+
if (!globalOpts.quiet) {
|
|
2059
|
+
printInfo("Using standalone skillindexer CLI...");
|
|
2060
|
+
printInfo("");
|
|
2061
|
+
}
|
|
2062
|
+
const args = ["scrape"];
|
|
2063
|
+
if (url) args.push(url);
|
|
2064
|
+
if (options.discover) args.push("--discover");
|
|
2065
|
+
if (options.force) args.push("--force");
|
|
2066
|
+
try {
|
|
2067
|
+
await runSkillIndexer(args, globalOpts.quiet ?? false);
|
|
2068
|
+
} catch (err) {
|
|
2069
|
+
printError(err.message);
|
|
2070
|
+
printWarning("");
|
|
2071
|
+
printWarning("If skillindexer is not installed, you can:");
|
|
2072
|
+
printWarning(" 1. cd scraper && npm install && npm run build");
|
|
2073
|
+
printWarning(" 2. npm link (in scraper directory)");
|
|
2074
|
+
printWarning("");
|
|
2075
|
+
printWarning("Or use the export/import workflow:");
|
|
2076
|
+
printWarning(" skillindexer scrape --discover");
|
|
2077
|
+
printWarning(" skillindexer export-skilltree -o skills.json");
|
|
2078
|
+
printWarning(" skill-tree import skills.json");
|
|
2079
|
+
process.exit(1);
|
|
2080
|
+
}
|
|
2081
|
+
}
|
|
2082
|
+
async function runSkillIndexer(args, quiet) {
|
|
2083
|
+
return new Promise((resolve3, reject) => {
|
|
2084
|
+
const proc = spawn("skillindexer", args, {
|
|
2085
|
+
stdio: quiet ? "pipe" : "inherit",
|
|
2086
|
+
shell: true
|
|
2087
|
+
});
|
|
2088
|
+
proc.on("error", (err) => {
|
|
2089
|
+
reject(new Error(`Failed to run skillindexer: ${err.message}`));
|
|
2090
|
+
});
|
|
2091
|
+
proc.on("close", (code) => {
|
|
2092
|
+
if (code === 0) {
|
|
2093
|
+
resolve3();
|
|
2094
|
+
} else {
|
|
2095
|
+
reject(new Error(`skillindexer exited with code ${code}`));
|
|
2096
|
+
}
|
|
2097
|
+
});
|
|
2098
|
+
});
|
|
2099
|
+
}
|
|
2100
|
+
|
|
2101
|
+
// src/cli/commands/indexer/classify.ts
|
|
2102
|
+
import { Command as Command15 } from "commander";
|
|
2103
|
+
import { spawn as spawn2 } from "child_process";
|
|
2104
|
+
var classifyCommand = new Command15("classify").description("Classify unindexed skills using AI").option("-s, --skill <id>", "Classify specific skill by ID").option("--all", "Re-classify all skills (including already indexed)").option("--standalone", "Use standalone skillindexer CLI (fallback)").action(async (options, command) => {
|
|
2105
|
+
const globalOpts = command.optsWithGlobals();
|
|
2106
|
+
if (!process.env.ANTHROPIC_API_KEY) {
|
|
2107
|
+
printError("ANTHROPIC_API_KEY environment variable is required");
|
|
2108
|
+
process.exit(1);
|
|
2109
|
+
}
|
|
2110
|
+
if (options.standalone) {
|
|
2111
|
+
await runStandaloneMode2(options, globalOpts);
|
|
2112
|
+
return;
|
|
2113
|
+
}
|
|
2114
|
+
try {
|
|
2115
|
+
const skillBank = await getSkillBank(globalOpts);
|
|
2116
|
+
const indexer = createIntegratedIndexer(skillBank, {
|
|
2117
|
+
anthropicApiKey: process.env.ANTHROPIC_API_KEY
|
|
2118
|
+
});
|
|
2119
|
+
if (!globalOpts.quiet) {
|
|
2120
|
+
if (options.skill) {
|
|
2121
|
+
printInfo(`Classifying skill: ${options.skill}`);
|
|
2122
|
+
} else if (options.all) {
|
|
2123
|
+
printInfo("Re-classifying all skills...");
|
|
2124
|
+
} else {
|
|
2125
|
+
printInfo("Classifying unindexed skills...");
|
|
2126
|
+
}
|
|
2127
|
+
printInfo("");
|
|
2128
|
+
}
|
|
2129
|
+
const result = await indexer.classify({
|
|
2130
|
+
skillId: options.skill,
|
|
2131
|
+
all: options.all
|
|
2132
|
+
});
|
|
2133
|
+
if (globalOpts.json) {
|
|
2134
|
+
console.log(JSON.stringify(result, null, 2));
|
|
2135
|
+
} else if (!globalOpts.quiet) {
|
|
2136
|
+
printSuccess("Classification completed");
|
|
2137
|
+
console.log(` Indexed: ${result.indexed}`);
|
|
2138
|
+
console.log(` Skipped: ${result.skipped}`);
|
|
2139
|
+
console.log(` Failed: ${result.failed}`);
|
|
2140
|
+
if (result.errors.length > 0) {
|
|
2141
|
+
printWarning("\nErrors:");
|
|
2142
|
+
for (const error of result.errors) {
|
|
2143
|
+
console.log(` - ${error}`);
|
|
2144
|
+
}
|
|
2145
|
+
}
|
|
2146
|
+
}
|
|
2147
|
+
await indexer.close();
|
|
2148
|
+
} catch (err) {
|
|
2149
|
+
const errorMessage = err.message;
|
|
2150
|
+
if (errorMessage.includes("not available") || errorMessage.includes("not found")) {
|
|
2151
|
+
printWarning("IndexerService not available, falling back to standalone CLI...");
|
|
2152
|
+
await runStandaloneMode2(options, globalOpts);
|
|
2153
|
+
return;
|
|
2154
|
+
}
|
|
2155
|
+
printError(errorMessage);
|
|
2156
|
+
process.exit(1);
|
|
2157
|
+
}
|
|
2158
|
+
});
|
|
2159
|
+
async function runStandaloneMode2(options, globalOpts) {
|
|
2160
|
+
if (!globalOpts.quiet) {
|
|
2161
|
+
printInfo("Using standalone skillindexer CLI...");
|
|
2162
|
+
printInfo("");
|
|
2163
|
+
}
|
|
2164
|
+
const args = ["index"];
|
|
2165
|
+
if (options.skill) args.push("--skill", options.skill);
|
|
2166
|
+
if (options.all) args.push("--all");
|
|
2167
|
+
try {
|
|
2168
|
+
await runSkillIndexer2(args, globalOpts.quiet ?? false);
|
|
2169
|
+
} catch (err) {
|
|
2170
|
+
printError(err.message);
|
|
2171
|
+
printWarning("");
|
|
2172
|
+
printWarning("Make sure skillindexer is installed and ANTHROPIC_API_KEY is set.");
|
|
2173
|
+
process.exit(1);
|
|
2174
|
+
}
|
|
2175
|
+
}
|
|
2176
|
+
async function runSkillIndexer2(args, quiet) {
|
|
2177
|
+
return new Promise((resolve3, reject) => {
|
|
2178
|
+
const proc = spawn2("skillindexer", args, {
|
|
2179
|
+
stdio: quiet ? "pipe" : "inherit",
|
|
2180
|
+
shell: true
|
|
2181
|
+
});
|
|
2182
|
+
proc.on("error", (err) => {
|
|
2183
|
+
reject(new Error(`Failed to run skillindexer: ${err.message}`));
|
|
2184
|
+
});
|
|
2185
|
+
proc.on("close", (code) => {
|
|
2186
|
+
if (code === 0) {
|
|
2187
|
+
resolve3();
|
|
2188
|
+
} else {
|
|
2189
|
+
reject(new Error(`skillindexer exited with code ${code}`));
|
|
2190
|
+
}
|
|
2191
|
+
});
|
|
2192
|
+
});
|
|
2193
|
+
}
|
|
2194
|
+
|
|
2195
|
+
// src/cli/commands/indexer/taxonomy.ts
|
|
2196
|
+
import { Command as Command16 } from "commander";
|
|
2197
|
+
import { spawn as spawn3 } from "child_process";
|
|
2198
|
+
var taxonomyCommand = new Command16("taxonomy").description("Browse the taxonomy tree").argument("[path]", 'Subtree path (e.g., "Development/Python")').option("-i, --interactive", "Interactive browsing mode").option("--standalone", "Use standalone skillindexer CLI (fallback)").action(async (pathArg, options, command) => {
|
|
2199
|
+
const globalOpts = command.optsWithGlobals();
|
|
2200
|
+
if (options.interactive || options.standalone) {
|
|
2201
|
+
await runStandaloneMode3(pathArg, options, globalOpts);
|
|
2202
|
+
return;
|
|
2203
|
+
}
|
|
2204
|
+
try {
|
|
2205
|
+
const skillBank = await getSkillBank(globalOpts);
|
|
2206
|
+
const indexer = createIntegratedIndexer(skillBank);
|
|
2207
|
+
const rootPath = pathArg ? pathArg.split("/").filter(Boolean) : void 0;
|
|
2208
|
+
const tree = await indexer.getTaxonomyTree(rootPath);
|
|
2209
|
+
if (globalOpts.json) {
|
|
2210
|
+
console.log(JSON.stringify(tree, null, 2));
|
|
2211
|
+
} else {
|
|
2212
|
+
printTaxonomyTree(tree, 0);
|
|
2213
|
+
}
|
|
2214
|
+
await indexer.close();
|
|
2215
|
+
} catch (err) {
|
|
2216
|
+
const errorMessage = err.message;
|
|
2217
|
+
if (errorMessage.includes("not available") || errorMessage.includes("not found")) {
|
|
2218
|
+
printWarning("IndexerService not available, falling back to standalone CLI...");
|
|
2219
|
+
await runStandaloneMode3(pathArg, options, globalOpts);
|
|
2220
|
+
return;
|
|
2221
|
+
}
|
|
2222
|
+
printError(errorMessage);
|
|
2223
|
+
process.exit(1);
|
|
2224
|
+
}
|
|
2225
|
+
});
|
|
2226
|
+
function printTaxonomyTree(node, indent) {
|
|
2227
|
+
const prefix = " ".repeat(indent);
|
|
2228
|
+
const countStr = node.skillCount > 0 ? ` (${node.skillCount})` : "";
|
|
2229
|
+
if (indent === 0) {
|
|
2230
|
+
console.log(`${node.name}${countStr}`);
|
|
2231
|
+
} else {
|
|
2232
|
+
console.log(`${prefix}\u251C\u2500 ${node.name}${countStr}`);
|
|
2233
|
+
}
|
|
2234
|
+
for (const child of node.children) {
|
|
2235
|
+
printTaxonomyTree(child, indent + 1);
|
|
2236
|
+
}
|
|
2237
|
+
}
|
|
2238
|
+
async function runStandaloneMode3(pathArg, options, globalOpts) {
|
|
2239
|
+
if (!globalOpts.quiet) {
|
|
2240
|
+
printInfo("Using standalone skillindexer CLI...");
|
|
2241
|
+
printInfo("");
|
|
2242
|
+
}
|
|
2243
|
+
const args = ["tree"];
|
|
2244
|
+
if (pathArg) args.push(pathArg);
|
|
2245
|
+
if (options.interactive) args.push("--interactive");
|
|
2246
|
+
try {
|
|
2247
|
+
await runSkillIndexer3(args, globalOpts.quiet ?? false);
|
|
2248
|
+
} catch (err) {
|
|
2249
|
+
printError(err.message);
|
|
2250
|
+
printWarning("");
|
|
2251
|
+
printWarning("Make sure skillindexer is installed and has indexed skills.");
|
|
2252
|
+
printWarning("Run: skill-tree index scrape --discover && skill-tree index classify");
|
|
2253
|
+
process.exit(1);
|
|
2254
|
+
}
|
|
2255
|
+
}
|
|
2256
|
+
async function runSkillIndexer3(args, quiet) {
|
|
2257
|
+
return new Promise((resolve3, reject) => {
|
|
2258
|
+
const proc = spawn3("skillindexer", args, {
|
|
2259
|
+
stdio: quiet ? "pipe" : "inherit",
|
|
2260
|
+
shell: true
|
|
2261
|
+
});
|
|
2262
|
+
proc.on("error", (err) => {
|
|
2263
|
+
reject(new Error(`Failed to run skillindexer: ${err.message}`));
|
|
2264
|
+
});
|
|
2265
|
+
proc.on("close", (code) => {
|
|
2266
|
+
if (code === 0) {
|
|
2267
|
+
resolve3();
|
|
2268
|
+
} else {
|
|
2269
|
+
reject(new Error(`skillindexer exited with code ${code}`));
|
|
2270
|
+
}
|
|
2271
|
+
});
|
|
2272
|
+
});
|
|
2273
|
+
}
|
|
2274
|
+
|
|
2275
|
+
// src/cli/commands/indexer/relationships.ts
|
|
2276
|
+
import { Command as Command17 } from "commander";
|
|
2277
|
+
import { spawn as spawn4 } from "child_process";
|
|
2278
|
+
var relationshipsCommand = new Command17("relationships").description("Detect relationships between indexed skills").option("-s, --skill <id>", "Detect relationships for specific skill").option("--use-ai", "Use AI for relationship reasoning (slower, more accurate)").option("--clear", "Clear existing relationships before detection").option("--standalone", "Use standalone skillindexer CLI (fallback)").action(async (options, command) => {
|
|
2279
|
+
const globalOpts = command.optsWithGlobals();
|
|
2280
|
+
if (options.standalone || options.useAi) {
|
|
2281
|
+
await runStandaloneMode4(options, globalOpts);
|
|
2282
|
+
return;
|
|
2283
|
+
}
|
|
2284
|
+
try {
|
|
2285
|
+
const skillBank = await getSkillBank(globalOpts);
|
|
2286
|
+
const indexer = createIntegratedIndexer(skillBank);
|
|
2287
|
+
if (!globalOpts.quiet) {
|
|
2288
|
+
if (options.skill) {
|
|
2289
|
+
printInfo(`Detecting relationships for skill: ${options.skill}`);
|
|
2290
|
+
} else {
|
|
2291
|
+
printInfo("Detecting relationships between all skills...");
|
|
2292
|
+
}
|
|
2293
|
+
printInfo("");
|
|
2294
|
+
}
|
|
2295
|
+
const result = await indexer.detectRelationships({
|
|
2296
|
+
skillId: options.skill,
|
|
2297
|
+
useAi: options.useAi
|
|
2298
|
+
});
|
|
2299
|
+
if (globalOpts.json) {
|
|
2300
|
+
console.log(JSON.stringify(result, null, 2));
|
|
2301
|
+
} else if (!globalOpts.quiet) {
|
|
2302
|
+
printSuccess("Relationship detection completed");
|
|
2303
|
+
console.log(` Detected: ${result.detected}`);
|
|
2304
|
+
console.log(` Skipped: ${result.skipped}`);
|
|
2305
|
+
if (result.errors.length > 0) {
|
|
2306
|
+
printWarning("\nErrors:");
|
|
2307
|
+
for (const error of result.errors) {
|
|
2308
|
+
console.log(` - ${error}`);
|
|
2309
|
+
}
|
|
2310
|
+
}
|
|
2311
|
+
}
|
|
2312
|
+
await indexer.close();
|
|
2313
|
+
} catch (err) {
|
|
2314
|
+
const errorMessage = err.message;
|
|
2315
|
+
if (errorMessage.includes("not available") || errorMessage.includes("not found")) {
|
|
2316
|
+
printWarning("IndexerService not available, falling back to standalone CLI...");
|
|
2317
|
+
await runStandaloneMode4(options, globalOpts);
|
|
2318
|
+
return;
|
|
2319
|
+
}
|
|
2320
|
+
printError(errorMessage);
|
|
2321
|
+
process.exit(1);
|
|
2322
|
+
}
|
|
2323
|
+
});
|
|
2324
|
+
async function runStandaloneMode4(options, globalOpts) {
|
|
2325
|
+
if (!globalOpts.quiet) {
|
|
2326
|
+
printInfo("Using standalone skillindexer CLI...");
|
|
2327
|
+
printInfo("");
|
|
2328
|
+
}
|
|
2329
|
+
const args = ["detect-relationships"];
|
|
2330
|
+
if (options.skill) args.push("--skill", options.skill);
|
|
2331
|
+
if (options.useAi) args.push("--use-ai");
|
|
2332
|
+
if (options.clear) args.push("--clear");
|
|
2333
|
+
try {
|
|
2334
|
+
await runSkillIndexer4(args, globalOpts.quiet ?? false);
|
|
2335
|
+
} catch (err) {
|
|
2336
|
+
printError(err.message);
|
|
2337
|
+
printWarning("");
|
|
2338
|
+
printWarning("Make sure skillindexer is installed and has indexed skills.");
|
|
2339
|
+
process.exit(1);
|
|
2340
|
+
}
|
|
2341
|
+
}
|
|
2342
|
+
async function runSkillIndexer4(args, quiet) {
|
|
2343
|
+
return new Promise((resolve3, reject) => {
|
|
2344
|
+
const proc = spawn4("skillindexer", args, {
|
|
2345
|
+
stdio: quiet ? "pipe" : "inherit",
|
|
2346
|
+
shell: true
|
|
2347
|
+
});
|
|
2348
|
+
proc.on("error", (err) => {
|
|
2349
|
+
reject(new Error(`Failed to run skillindexer: ${err.message}`));
|
|
2350
|
+
});
|
|
2351
|
+
proc.on("close", (code) => {
|
|
2352
|
+
if (code === 0) {
|
|
2353
|
+
resolve3();
|
|
2354
|
+
} else {
|
|
2355
|
+
reject(new Error(`skillindexer exited with code ${code}`));
|
|
2356
|
+
}
|
|
2357
|
+
});
|
|
2358
|
+
});
|
|
2359
|
+
}
|
|
2360
|
+
|
|
2361
|
+
// src/cli/commands/indexer/stats.ts
|
|
2362
|
+
import { Command as Command18 } from "commander";
|
|
2363
|
+
import { spawn as spawn5 } from "child_process";
|
|
2364
|
+
var indexerStatsCommand = new Command18("stats").description("Show indexer database statistics").option("--standalone", "Use standalone skillindexer CLI (fallback)").action(async (options, command) => {
|
|
2365
|
+
const globalOpts = command.optsWithGlobals();
|
|
2366
|
+
if (options.standalone) {
|
|
2367
|
+
await runStandaloneMode5(globalOpts);
|
|
2368
|
+
return;
|
|
2369
|
+
}
|
|
2370
|
+
try {
|
|
2371
|
+
const skillBank = await getSkillBank(globalOpts);
|
|
2372
|
+
const indexer = createIntegratedIndexer(skillBank);
|
|
2373
|
+
const stats = await indexer.getStats();
|
|
2374
|
+
if (globalOpts.json) {
|
|
2375
|
+
console.log(JSON.stringify(stats, null, 2));
|
|
2376
|
+
} else {
|
|
2377
|
+
console.log("Skill-Tree Statistics");
|
|
2378
|
+
console.log("=".repeat(40));
|
|
2379
|
+
console.log("");
|
|
2380
|
+
console.log("Skills:");
|
|
2381
|
+
console.log(` Total: ${stats.totalSkills}`);
|
|
2382
|
+
console.log(` Indexed: ${stats.indexedSkills}`);
|
|
2383
|
+
console.log(` Draft: ${stats.rawSkills}`);
|
|
2384
|
+
console.log(` Deprecated: ${stats.failedSkills}`);
|
|
2385
|
+
console.log("");
|
|
2386
|
+
console.log("Taxonomy:");
|
|
2387
|
+
console.log(` Nodes: ${stats.taxonomyNodes}`);
|
|
2388
|
+
console.log("");
|
|
2389
|
+
console.log("Relationships:");
|
|
2390
|
+
console.log(` Total: ${stats.relationships}`);
|
|
2391
|
+
console.log("");
|
|
2392
|
+
console.log("Sources:");
|
|
2393
|
+
console.log(` Unique: ${stats.sources}`);
|
|
2394
|
+
}
|
|
2395
|
+
await indexer.close();
|
|
2396
|
+
} catch (err) {
|
|
2397
|
+
const errorMessage = err.message;
|
|
2398
|
+
if (errorMessage.includes("not available") || errorMessage.includes("not found")) {
|
|
2399
|
+
printWarning("IndexerService not available, falling back to standalone CLI...");
|
|
2400
|
+
await runStandaloneMode5(globalOpts);
|
|
2401
|
+
return;
|
|
2402
|
+
}
|
|
2403
|
+
printError(errorMessage);
|
|
2404
|
+
process.exit(1);
|
|
2405
|
+
}
|
|
2406
|
+
});
|
|
2407
|
+
async function runStandaloneMode5(globalOpts) {
|
|
2408
|
+
if (!globalOpts.quiet) {
|
|
2409
|
+
printInfo("Using standalone skillindexer CLI...");
|
|
2410
|
+
printInfo("");
|
|
2411
|
+
}
|
|
2412
|
+
try {
|
|
2413
|
+
await runSkillIndexer5(["stats"], globalOpts.quiet ?? false);
|
|
2414
|
+
} catch (err) {
|
|
2415
|
+
printError(err.message);
|
|
2416
|
+
printWarning("");
|
|
2417
|
+
printWarning("Make sure skillindexer is installed.");
|
|
2418
|
+
process.exit(1);
|
|
2419
|
+
}
|
|
2420
|
+
}
|
|
2421
|
+
async function runSkillIndexer5(args, quiet) {
|
|
2422
|
+
return new Promise((resolve3, reject) => {
|
|
2423
|
+
const proc = spawn5("skillindexer", args, {
|
|
2424
|
+
stdio: quiet ? "pipe" : "inherit",
|
|
2425
|
+
shell: true
|
|
2426
|
+
});
|
|
2427
|
+
proc.on("error", (err) => {
|
|
2428
|
+
reject(new Error(`Failed to run skillindexer: ${err.message}`));
|
|
2429
|
+
});
|
|
2430
|
+
proc.on("close", (code) => {
|
|
2431
|
+
if (code === 0) {
|
|
2432
|
+
resolve3();
|
|
2433
|
+
} else {
|
|
2434
|
+
reject(new Error(`skillindexer exited with code ${code}`));
|
|
2435
|
+
}
|
|
2436
|
+
});
|
|
2437
|
+
});
|
|
2438
|
+
}
|
|
2439
|
+
|
|
2440
|
+
// src/cli/commands/indexer/sync.ts
|
|
2441
|
+
import { Command as Command19 } from "commander";
|
|
2442
|
+
import * as fs6 from "fs";
|
|
2443
|
+
import * as path4 from "path";
|
|
2444
|
+
import * as os3 from "os";
|
|
2445
|
+
import { spawn as spawn6 } from "child_process";
|
|
2446
|
+
|
|
2447
|
+
// src/services/sync.ts
|
|
2448
|
+
var SyncService = class {
|
|
2449
|
+
constructor(skillBank, config2 = {}) {
|
|
2450
|
+
this.syncStates = /* @__PURE__ */ new Map();
|
|
2451
|
+
this.skillBank = skillBank;
|
|
2452
|
+
this.globalConfig = loadConfig();
|
|
2453
|
+
this.config = {
|
|
2454
|
+
githubToken: config2.githubToken || this.globalConfig.indexer.github_token,
|
|
2455
|
+
autoSyncInterval: config2.autoSyncInterval || 0,
|
|
2456
|
+
conflictResolution: config2.conflictResolution || this.globalConfig.sync.conflict_resolution
|
|
2457
|
+
};
|
|
2458
|
+
}
|
|
2459
|
+
/**
|
|
2460
|
+
* Initialize sync service
|
|
2461
|
+
* Loads sync state from storage if available
|
|
2462
|
+
*/
|
|
2463
|
+
async initialize() {
|
|
2464
|
+
const storage = this.skillBank.getStorage();
|
|
2465
|
+
if (storage?.getSyncStates) {
|
|
2466
|
+
try {
|
|
2467
|
+
const states = await storage.getSyncStates();
|
|
2468
|
+
for (const state of states) {
|
|
2469
|
+
const record = state;
|
|
2470
|
+
if (record.skillId && record.lastSynced) {
|
|
2471
|
+
const skillId = record.skillId;
|
|
2472
|
+
this.syncStates.set(skillId, {
|
|
2473
|
+
skillId,
|
|
2474
|
+
sourceUrl: record.sourceUrl || "",
|
|
2475
|
+
sourceEtag: record.sourceEtag,
|
|
2476
|
+
lastSynced: new Date(record.lastSynced),
|
|
2477
|
+
localVersion: record.localVersion || "0.0.0",
|
|
2478
|
+
remoteVersion: record.remoteVersion,
|
|
2479
|
+
status: record.status || "synced"
|
|
2480
|
+
});
|
|
2481
|
+
}
|
|
2482
|
+
}
|
|
2483
|
+
} catch {
|
|
2484
|
+
}
|
|
2485
|
+
}
|
|
2486
|
+
}
|
|
2487
|
+
/**
|
|
2488
|
+
* Get sync state for a skill
|
|
2489
|
+
*/
|
|
2490
|
+
getSyncState(skillId) {
|
|
2491
|
+
return this.syncStates.get(skillId);
|
|
2492
|
+
}
|
|
2493
|
+
/**
|
|
2494
|
+
* Set sync state for a skill
|
|
2495
|
+
*/
|
|
2496
|
+
async setSyncState(skillId, state) {
|
|
2497
|
+
this.syncStates.set(skillId, state);
|
|
2498
|
+
const storage = this.skillBank.getStorage();
|
|
2499
|
+
if (storage?.saveSyncState) {
|
|
2500
|
+
try {
|
|
2501
|
+
await storage.saveSyncState({
|
|
2502
|
+
remote: state.sourceUrl,
|
|
2503
|
+
syncedAt: state.lastSynced,
|
|
2504
|
+
commitHash: state.sourceEtag
|
|
2505
|
+
});
|
|
2506
|
+
} catch {
|
|
2507
|
+
}
|
|
2508
|
+
}
|
|
2509
|
+
}
|
|
2510
|
+
/**
|
|
2511
|
+
* Check for updates on all imported skills
|
|
2512
|
+
*/
|
|
2513
|
+
async checkForUpdates() {
|
|
2514
|
+
const results = [];
|
|
2515
|
+
const skills = await this.skillBank.listSkills();
|
|
2516
|
+
for (const skill of skills) {
|
|
2517
|
+
if (!skill.externalSource?.url) continue;
|
|
2518
|
+
const result = await this.checkSkillForUpdates(skill);
|
|
2519
|
+
results.push(result);
|
|
2520
|
+
}
|
|
2521
|
+
this.lastCheck = /* @__PURE__ */ new Date();
|
|
2522
|
+
return results;
|
|
2523
|
+
}
|
|
2524
|
+
/**
|
|
2525
|
+
* Check a single skill for updates
|
|
2526
|
+
*/
|
|
2527
|
+
async checkSkillForUpdates(skill) {
|
|
2528
|
+
const syncState = this.syncStates.get(skill.id);
|
|
2529
|
+
if (!skill.externalSource?.url) {
|
|
2530
|
+
return {
|
|
2531
|
+
skillId: skill.id,
|
|
2532
|
+
skillName: skill.name,
|
|
2533
|
+
status: "synced",
|
|
2534
|
+
localVersion: skill.version,
|
|
2535
|
+
hasRemoteChanges: false,
|
|
2536
|
+
hasLocalChanges: false
|
|
2537
|
+
};
|
|
2538
|
+
}
|
|
2539
|
+
const hasLocalChanges = syncState ? skill.version !== syncState.localVersion : false;
|
|
2540
|
+
let hasRemoteChanges = false;
|
|
2541
|
+
let remoteVersion;
|
|
2542
|
+
if (skill.externalSource.url.includes("github.com")) {
|
|
2543
|
+
try {
|
|
2544
|
+
const remoteInfo = await this.fetchRemoteInfo(skill.externalSource.url);
|
|
2545
|
+
hasRemoteChanges = remoteInfo.etag !== syncState?.sourceEtag;
|
|
2546
|
+
remoteVersion = remoteInfo.version;
|
|
2547
|
+
} catch {
|
|
2548
|
+
}
|
|
2549
|
+
}
|
|
2550
|
+
let status;
|
|
2551
|
+
if (hasLocalChanges && hasRemoteChanges) {
|
|
2552
|
+
status = "conflict";
|
|
2553
|
+
} else if (hasLocalChanges) {
|
|
2554
|
+
status = "local-modified";
|
|
2555
|
+
} else if (hasRemoteChanges) {
|
|
2556
|
+
status = "remote-modified";
|
|
2557
|
+
} else {
|
|
2558
|
+
status = "synced";
|
|
2559
|
+
}
|
|
2560
|
+
return {
|
|
2561
|
+
skillId: skill.id,
|
|
2562
|
+
skillName: skill.name,
|
|
2563
|
+
status,
|
|
2564
|
+
localVersion: skill.version,
|
|
2565
|
+
remoteVersion,
|
|
2566
|
+
lastSynced: syncState?.lastSynced,
|
|
2567
|
+
hasRemoteChanges,
|
|
2568
|
+
hasLocalChanges
|
|
2569
|
+
};
|
|
2570
|
+
}
|
|
2571
|
+
/**
|
|
2572
|
+
* Fetch remote info from GitHub
|
|
2573
|
+
*/
|
|
2574
|
+
async fetchRemoteInfo(url) {
|
|
2575
|
+
const match = url.match(/github\.com\/([^/]+)\/([^/]+)\/blob\/([^/]+)\/(.+)/);
|
|
2576
|
+
if (!match) {
|
|
2577
|
+
throw new Error("Invalid GitHub URL");
|
|
2578
|
+
}
|
|
2579
|
+
const [, owner, repo, branch, filePath] = match;
|
|
2580
|
+
const apiUrl = `https://api.github.com/repos/${owner}/${repo}/contents/${filePath}?ref=${branch}`;
|
|
2581
|
+
const headers = {
|
|
2582
|
+
"Accept": "application/vnd.github.v3+json",
|
|
2583
|
+
"User-Agent": "skill-tree-sync"
|
|
2584
|
+
};
|
|
2585
|
+
if (this.config.githubToken) {
|
|
2586
|
+
headers["Authorization"] = `Bearer ${this.config.githubToken}`;
|
|
2587
|
+
}
|
|
2588
|
+
const response = await fetch(apiUrl, { headers });
|
|
2589
|
+
if (!response.ok) {
|
|
2590
|
+
throw new Error(`GitHub API error: ${response.status}`);
|
|
2591
|
+
}
|
|
2592
|
+
const data = await response.json();
|
|
2593
|
+
const etag = response.headers.get("etag") || data.sha;
|
|
2594
|
+
let version;
|
|
2595
|
+
if (data.content) {
|
|
2596
|
+
const content = Buffer.from(data.content, "base64").toString("utf-8");
|
|
2597
|
+
const versionMatch = content.match(/version:\s*['"]?(\d+\.\d+\.\d+)['"]?/i);
|
|
2598
|
+
if (versionMatch) {
|
|
2599
|
+
version = versionMatch[1];
|
|
2600
|
+
}
|
|
2601
|
+
}
|
|
2602
|
+
return { etag, version, content: data.content };
|
|
2603
|
+
}
|
|
2604
|
+
/**
|
|
2605
|
+
* Pull remote changes for all skills that need updates
|
|
2606
|
+
*/
|
|
2607
|
+
async pullRemoteChanges(options) {
|
|
2608
|
+
const result = {
|
|
2609
|
+
pulled: 0,
|
|
2610
|
+
skipped: 0,
|
|
2611
|
+
conflicts: 0,
|
|
2612
|
+
errors: [],
|
|
2613
|
+
updates: []
|
|
2614
|
+
};
|
|
2615
|
+
let skills = await this.skillBank.listSkills();
|
|
2616
|
+
if (options?.skillIds) {
|
|
2617
|
+
skills = skills.filter((s) => options.skillIds.includes(s.id));
|
|
2618
|
+
}
|
|
2619
|
+
for (const skill of skills) {
|
|
2620
|
+
if (!skill.externalSource?.url) {
|
|
2621
|
+
result.skipped++;
|
|
2622
|
+
continue;
|
|
2623
|
+
}
|
|
2624
|
+
try {
|
|
2625
|
+
const checkResult = await this.checkSkillForUpdates(skill);
|
|
2626
|
+
if (checkResult.status === "synced") {
|
|
2627
|
+
result.skipped++;
|
|
2628
|
+
continue;
|
|
2629
|
+
}
|
|
2630
|
+
if (checkResult.status === "conflict") {
|
|
2631
|
+
const resolution = options?.conflictResolution || this.config.conflictResolution;
|
|
2632
|
+
if (resolution === "manual") {
|
|
2633
|
+
result.conflicts++;
|
|
2634
|
+
continue;
|
|
2635
|
+
} else if (resolution === "local") {
|
|
2636
|
+
result.skipped++;
|
|
2637
|
+
continue;
|
|
2638
|
+
}
|
|
2639
|
+
}
|
|
2640
|
+
if (checkResult.status === "local-modified") {
|
|
2641
|
+
result.skipped++;
|
|
2642
|
+
continue;
|
|
2643
|
+
}
|
|
2644
|
+
if (!options?.dryRun) {
|
|
2645
|
+
const updated = await this.pullSkillUpdate(skill);
|
|
2646
|
+
if (updated) {
|
|
2647
|
+
result.pulled++;
|
|
2648
|
+
result.updates.push({
|
|
2649
|
+
skillId: skill.id,
|
|
2650
|
+
previousVersion: skill.version,
|
|
2651
|
+
newVersion: updated.version
|
|
2652
|
+
});
|
|
2653
|
+
}
|
|
2654
|
+
} else {
|
|
2655
|
+
result.pulled++;
|
|
2656
|
+
}
|
|
2657
|
+
} catch (err) {
|
|
2658
|
+
result.errors.push(`Failed to sync ${skill.id}: ${err.message}`);
|
|
2659
|
+
}
|
|
2660
|
+
}
|
|
2661
|
+
return result;
|
|
2662
|
+
}
|
|
2663
|
+
/**
|
|
2664
|
+
* Pull update for a single skill
|
|
2665
|
+
*/
|
|
2666
|
+
async pullSkillUpdate(skill) {
|
|
2667
|
+
if (!skill.externalSource?.url) return null;
|
|
2668
|
+
try {
|
|
2669
|
+
const remoteInfo = await this.fetchRemoteInfo(skill.externalSource.url);
|
|
2670
|
+
if (!remoteInfo.content) return null;
|
|
2671
|
+
const content = Buffer.from(remoteInfo.content, "base64").toString("utf-8");
|
|
2672
|
+
const updatedSkill = {
|
|
2673
|
+
...skill,
|
|
2674
|
+
version: remoteInfo.version || skill.version,
|
|
2675
|
+
updatedAt: /* @__PURE__ */ new Date(),
|
|
2676
|
+
externalSource: {
|
|
2677
|
+
...skill.externalSource,
|
|
2678
|
+
etag: remoteInfo.etag
|
|
2679
|
+
}
|
|
2680
|
+
};
|
|
2681
|
+
await this.skillBank.saveSkill(updatedSkill);
|
|
2682
|
+
await this.setSyncState(skill.id, {
|
|
2683
|
+
skillId: skill.id,
|
|
2684
|
+
sourceUrl: skill.externalSource.url,
|
|
2685
|
+
sourceEtag: remoteInfo.etag,
|
|
2686
|
+
lastSynced: /* @__PURE__ */ new Date(),
|
|
2687
|
+
localVersion: updatedSkill.version,
|
|
2688
|
+
remoteVersion: remoteInfo.version,
|
|
2689
|
+
status: "synced"
|
|
2690
|
+
});
|
|
2691
|
+
return updatedSkill;
|
|
2692
|
+
} catch {
|
|
2693
|
+
return null;
|
|
2694
|
+
}
|
|
2695
|
+
}
|
|
2696
|
+
/**
|
|
2697
|
+
* Mark a skill as synced (after manual resolution)
|
|
2698
|
+
*/
|
|
2699
|
+
async markAsSynced(skillId) {
|
|
2700
|
+
const skill = await this.skillBank.getSkill(skillId);
|
|
2701
|
+
if (!skill || !skill.externalSource?.url) return;
|
|
2702
|
+
await this.setSyncState(skillId, {
|
|
2703
|
+
skillId,
|
|
2704
|
+
sourceUrl: skill.externalSource.url,
|
|
2705
|
+
sourceEtag: skill.externalSource.etag,
|
|
2706
|
+
lastSynced: /* @__PURE__ */ new Date(),
|
|
2707
|
+
localVersion: skill.version,
|
|
2708
|
+
status: "synced"
|
|
2709
|
+
});
|
|
2710
|
+
}
|
|
2711
|
+
/**
|
|
2712
|
+
* Get sync status summary
|
|
2713
|
+
*/
|
|
2714
|
+
async getStatusSummary() {
|
|
2715
|
+
const skills = await this.skillBank.listSkills();
|
|
2716
|
+
const summary = {
|
|
2717
|
+
total: 0,
|
|
2718
|
+
synced: 0,
|
|
2719
|
+
localModified: 0,
|
|
2720
|
+
remoteModified: 0,
|
|
2721
|
+
conflicts: 0,
|
|
2722
|
+
neverSynced: 0,
|
|
2723
|
+
lastCheck: this.lastCheck
|
|
2724
|
+
};
|
|
2725
|
+
for (const skill of skills) {
|
|
2726
|
+
if (!skill.externalSource?.url) continue;
|
|
2727
|
+
summary.total++;
|
|
2728
|
+
const state = this.syncStates.get(skill.id);
|
|
2729
|
+
if (!state) {
|
|
2730
|
+
summary.neverSynced++;
|
|
2731
|
+
} else {
|
|
2732
|
+
switch (state.status) {
|
|
2733
|
+
case "synced":
|
|
2734
|
+
summary.synced++;
|
|
2735
|
+
break;
|
|
2736
|
+
case "local-modified":
|
|
2737
|
+
summary.localModified++;
|
|
2738
|
+
break;
|
|
2739
|
+
case "remote-modified":
|
|
2740
|
+
summary.remoteModified++;
|
|
2741
|
+
break;
|
|
2742
|
+
case "conflict":
|
|
2743
|
+
summary.conflicts++;
|
|
2744
|
+
break;
|
|
2745
|
+
}
|
|
2746
|
+
}
|
|
2747
|
+
}
|
|
2748
|
+
return summary;
|
|
2749
|
+
}
|
|
2750
|
+
/**
|
|
2751
|
+
* Detect conflicts between local and remote versions
|
|
2752
|
+
*/
|
|
2753
|
+
async detectConflicts() {
|
|
2754
|
+
const conflicts = [];
|
|
2755
|
+
const skills = await this.skillBank.listSkills();
|
|
2756
|
+
for (const skill of skills) {
|
|
2757
|
+
const state = this.syncStates.get(skill.id);
|
|
2758
|
+
if (state?.status === "conflict" && skill.externalSource?.url) {
|
|
2759
|
+
conflicts.push({
|
|
2760
|
+
skill,
|
|
2761
|
+
localChanges: [`Version changed: ${state.localVersion} -> ${skill.version}`],
|
|
2762
|
+
remoteChanges: state.remoteVersion ? [`Remote version: ${state.remoteVersion}`] : ["Remote content changed"]
|
|
2763
|
+
});
|
|
2764
|
+
}
|
|
2765
|
+
}
|
|
2766
|
+
return conflicts;
|
|
2767
|
+
}
|
|
2768
|
+
/**
|
|
2769
|
+
* Register a skill as imported (sets initial sync state)
|
|
2770
|
+
*/
|
|
2771
|
+
async registerImportedSkill(skill) {
|
|
2772
|
+
if (!skill.externalSource?.url) return;
|
|
2773
|
+
await this.setSyncState(skill.id, {
|
|
2774
|
+
skillId: skill.id,
|
|
2775
|
+
sourceUrl: skill.externalSource.url,
|
|
2776
|
+
sourceEtag: skill.externalSource.etag,
|
|
2777
|
+
lastSynced: /* @__PURE__ */ new Date(),
|
|
2778
|
+
localVersion: skill.version,
|
|
2779
|
+
status: "synced"
|
|
2780
|
+
});
|
|
2781
|
+
}
|
|
2782
|
+
};
|
|
2783
|
+
function createSyncService(skillBank, config2) {
|
|
2784
|
+
return new SyncService(skillBank, config2);
|
|
2785
|
+
}
|
|
2786
|
+
|
|
2787
|
+
// src/cli/commands/indexer/sync.ts
|
|
2788
|
+
var syncCommand = new Command19("sync").description("Sync imported skills with remote sources").option("--status", "Show sync status for all imported skills").option("--pull", "Pull remote changes for skills that need updates").option("--dry-run", "Preview changes without making them").option("--force", "Force sync even for conflicting skills").option("--resolve <strategy>", "Conflict resolution: local, remote, manual", "manual").option("--skill <id>", "Sync specific skill only").option("--import", "Import from indexer export (legacy mode)").option("--export-path <path>", "Path to indexer export file (for --import)").option("--indexed-only", "Only sync indexed skills (for --import)").action(async (options, command) => {
|
|
2789
|
+
const globalOpts = command.optsWithGlobals();
|
|
2790
|
+
try {
|
|
2791
|
+
if (options.import) {
|
|
2792
|
+
await runLegacyImport(options, globalOpts);
|
|
2793
|
+
return;
|
|
2794
|
+
}
|
|
2795
|
+
const skillBank = await getSkillBank(globalOpts);
|
|
2796
|
+
const syncService = createSyncService(skillBank, {
|
|
2797
|
+
githubToken: process.env.GITHUB_TOKEN,
|
|
2798
|
+
conflictResolution: options.resolve
|
|
2799
|
+
});
|
|
2800
|
+
await syncService.initialize();
|
|
2801
|
+
if (options.status) {
|
|
2802
|
+
if (!globalOpts.quiet) {
|
|
2803
|
+
printInfo("Checking sync status...");
|
|
2804
|
+
}
|
|
2805
|
+
const results = await syncService.checkForUpdates();
|
|
2806
|
+
const summary = await syncService.getStatusSummary();
|
|
2807
|
+
if (globalOpts.json) {
|
|
2808
|
+
console.log(JSON.stringify({ results, summary }, null, 2));
|
|
2809
|
+
return;
|
|
2810
|
+
}
|
|
2811
|
+
console.log("");
|
|
2812
|
+
console.log("Sync Status Summary");
|
|
2813
|
+
console.log("=".repeat(40));
|
|
2814
|
+
console.log(` Total imported skills: ${summary.total}`);
|
|
2815
|
+
console.log(` Synced: ${summary.synced}`);
|
|
2816
|
+
console.log(` Local modified: ${summary.localModified}`);
|
|
2817
|
+
console.log(` Remote modified: ${summary.remoteModified}`);
|
|
2818
|
+
console.log(` Conflicts: ${summary.conflicts}`);
|
|
2819
|
+
console.log(` Never synced: ${summary.neverSynced}`);
|
|
2820
|
+
if (summary.lastCheck) {
|
|
2821
|
+
console.log(` Last check: ${summary.lastCheck.toISOString()}`);
|
|
2822
|
+
}
|
|
2823
|
+
const needsAction = results.filter((r) => r.status !== "synced");
|
|
2824
|
+
if (needsAction.length > 0) {
|
|
2825
|
+
console.log("");
|
|
2826
|
+
console.log("Skills needing attention:");
|
|
2827
|
+
for (const result of needsAction) {
|
|
2828
|
+
const icon = result.status === "conflict" ? "\u26A0\uFE0F" : result.status === "remote-modified" ? "\u2193" : result.status === "local-modified" ? "\u2191" : "?";
|
|
2829
|
+
console.log(` ${icon} ${result.skillName} (${result.skillId})`);
|
|
2830
|
+
console.log(` Status: ${result.status}`);
|
|
2831
|
+
console.log(` Local version: ${result.localVersion}`);
|
|
2832
|
+
if (result.remoteVersion) {
|
|
2833
|
+
console.log(` Remote version: ${result.remoteVersion}`);
|
|
2834
|
+
}
|
|
2835
|
+
}
|
|
2836
|
+
}
|
|
2837
|
+
return;
|
|
2838
|
+
}
|
|
2839
|
+
if (options.pull) {
|
|
2840
|
+
if (!globalOpts.quiet) {
|
|
2841
|
+
if (options.dryRun) {
|
|
2842
|
+
printInfo("Checking for remote changes (dry run)...");
|
|
2843
|
+
} else {
|
|
2844
|
+
printInfo("Pulling remote changes...");
|
|
2845
|
+
}
|
|
2846
|
+
}
|
|
2847
|
+
const pullResult = await syncService.pullRemoteChanges({
|
|
2848
|
+
skillIds: options.skill ? [options.skill] : void 0,
|
|
2849
|
+
conflictResolution: options.force ? "remote" : options.resolve,
|
|
2850
|
+
dryRun: options.dryRun
|
|
2851
|
+
});
|
|
2852
|
+
if (globalOpts.json) {
|
|
2853
|
+
console.log(JSON.stringify(pullResult, null, 2));
|
|
2854
|
+
return;
|
|
2855
|
+
}
|
|
2856
|
+
if (options.dryRun) {
|
|
2857
|
+
console.log("");
|
|
2858
|
+
console.log("Would pull the following updates:");
|
|
2859
|
+
console.log(` Skills to update: ${pullResult.pulled}`);
|
|
2860
|
+
console.log(` Skipped: ${pullResult.skipped}`);
|
|
2861
|
+
console.log(` Conflicts: ${pullResult.conflicts}`);
|
|
2862
|
+
} else {
|
|
2863
|
+
printSuccess("Sync completed");
|
|
2864
|
+
console.log(` Pulled: ${pullResult.pulled}`);
|
|
2865
|
+
console.log(` Skipped: ${pullResult.skipped}`);
|
|
2866
|
+
console.log(` Conflicts: ${pullResult.conflicts}`);
|
|
2867
|
+
if (pullResult.updates.length > 0) {
|
|
2868
|
+
console.log("");
|
|
2869
|
+
console.log("Updated skills:");
|
|
2870
|
+
for (const update of pullResult.updates) {
|
|
2871
|
+
console.log(` - ${update.skillId}: ${update.previousVersion} -> ${update.newVersion}`);
|
|
2872
|
+
}
|
|
2873
|
+
}
|
|
2874
|
+
}
|
|
2875
|
+
if (pullResult.errors.length > 0) {
|
|
2876
|
+
printWarning("\nErrors:");
|
|
2877
|
+
for (const error of pullResult.errors) {
|
|
2878
|
+
console.log(` - ${error}`);
|
|
2879
|
+
}
|
|
2880
|
+
}
|
|
2881
|
+
return;
|
|
2882
|
+
}
|
|
2883
|
+
printInfo("Usage:");
|
|
2884
|
+
console.log(" skill-tree index sync --status Show sync status");
|
|
2885
|
+
console.log(" skill-tree index sync --pull Pull remote changes");
|
|
2886
|
+
console.log(" skill-tree index sync --import Import from indexer export");
|
|
2887
|
+
console.log("");
|
|
2888
|
+
console.log("Options:");
|
|
2889
|
+
console.log(" --dry-run Preview changes");
|
|
2890
|
+
console.log(" --force Force sync for conflicts");
|
|
2891
|
+
console.log(" --resolve <strategy> Conflict resolution: local, remote, manual");
|
|
2892
|
+
console.log(" --skill <id> Sync specific skill");
|
|
2893
|
+
} catch (err) {
|
|
2894
|
+
printError(err.message);
|
|
2895
|
+
process.exit(1);
|
|
2896
|
+
}
|
|
2897
|
+
});
|
|
2898
|
+
async function runLegacyImport(options, globalOpts) {
|
|
2899
|
+
let exportPath = options.exportPath;
|
|
2900
|
+
if (!exportPath) {
|
|
2901
|
+
if (!globalOpts.quiet) {
|
|
2902
|
+
printInfo("Exporting skills from indexer...");
|
|
2903
|
+
}
|
|
2904
|
+
exportPath = path4.join(os3.tmpdir(), `skill-tree-sync-${Date.now()}.json`);
|
|
2905
|
+
const args = ["export-skilltree", "-o", exportPath, "-f", "json"];
|
|
2906
|
+
if (options.indexedOnly) args.push("--indexed-only");
|
|
2907
|
+
await runSkillIndexer6(args, true);
|
|
2908
|
+
if (!fs6.existsSync(exportPath)) {
|
|
2909
|
+
printError("Failed to export skills from indexer");
|
|
2910
|
+
process.exit(1);
|
|
2911
|
+
}
|
|
2912
|
+
}
|
|
2913
|
+
if (!globalOpts.quiet) {
|
|
2914
|
+
printInfo(`Reading export from ${exportPath}...`);
|
|
2915
|
+
}
|
|
2916
|
+
const content = fs6.readFileSync(exportPath, "utf-8");
|
|
2917
|
+
const { skills, stats } = parseIndexerExport(content);
|
|
2918
|
+
if (!globalOpts.quiet) {
|
|
2919
|
+
printInfo(`Found ${stats.total} skills (${stats.withStructuredContent} with structured content)`);
|
|
2920
|
+
}
|
|
2921
|
+
if (options.dryRun) {
|
|
2922
|
+
console.log("");
|
|
2923
|
+
console.log("Would import the following skills:");
|
|
2924
|
+
for (const skill of skills.slice(0, 20)) {
|
|
2925
|
+
console.log(` - ${skill.id} (${skill.name})`);
|
|
2926
|
+
}
|
|
2927
|
+
if (skills.length > 20) {
|
|
2928
|
+
console.log(` ... and ${skills.length - 20} more`);
|
|
2929
|
+
}
|
|
2930
|
+
return;
|
|
2931
|
+
}
|
|
2932
|
+
const skillBank = await getSkillBank(globalOpts);
|
|
2933
|
+
const result = await skillBank.importSkills(skills);
|
|
2934
|
+
if (!options.exportPath && exportPath) {
|
|
2935
|
+
try {
|
|
2936
|
+
fs6.unlinkSync(exportPath);
|
|
2937
|
+
} catch {
|
|
2938
|
+
}
|
|
2939
|
+
}
|
|
2940
|
+
if (globalOpts.json) {
|
|
2941
|
+
console.log(JSON.stringify(result, null, 2));
|
|
2942
|
+
return;
|
|
2943
|
+
}
|
|
2944
|
+
printSuccess(`Synced ${result.imported} skills into SkillBank`);
|
|
2945
|
+
if (result.failed > 0) {
|
|
2946
|
+
printWarning(`Failed to sync ${result.failed} skills`);
|
|
2947
|
+
}
|
|
2948
|
+
}
|
|
2949
|
+
async function runSkillIndexer6(args, quiet) {
|
|
2950
|
+
return new Promise((resolve3, reject) => {
|
|
2951
|
+
const proc = spawn6("skillindexer", args, {
|
|
2952
|
+
stdio: quiet ? "pipe" : "inherit",
|
|
2953
|
+
shell: true
|
|
2954
|
+
});
|
|
2955
|
+
proc.on("error", (err) => {
|
|
2956
|
+
reject(new Error(`Failed to run skillindexer: ${err.message}`));
|
|
2957
|
+
});
|
|
2958
|
+
proc.on("close", (code) => {
|
|
2959
|
+
if (code === 0) {
|
|
2960
|
+
resolve3();
|
|
2961
|
+
} else {
|
|
2962
|
+
reject(new Error(`skillindexer exited with code ${code}`));
|
|
2963
|
+
}
|
|
2964
|
+
});
|
|
2965
|
+
});
|
|
2966
|
+
}
|
|
2967
|
+
|
|
2968
|
+
// src/cli/commands/indexer/index.ts
|
|
2969
|
+
var indexerCommand = new Command20("index").description("Skill indexer - discover and classify skills from GitHub").addCommand(scrapeCommand).addCommand(classifyCommand).addCommand(taxonomyCommand).addCommand(relationshipsCommand).addCommand(indexerStatsCommand).addCommand(syncCommand);
|
|
2970
|
+
|
|
2971
|
+
// src/cli/commands/config.ts
|
|
2972
|
+
import { Command as Command21 } from "commander";
|
|
2973
|
+
import * as fs7 from "fs";
|
|
2974
|
+
var configCommand = new Command21("config").description("View and manage configuration");
|
|
2975
|
+
configCommand.command("show").description("Show current configuration").option("--path <path>", "Path to specific config value (e.g., storage.type)").action((options) => {
|
|
2976
|
+
const globalOpts = configCommand.parent?.opts() || {};
|
|
2977
|
+
const configPath = globalOpts.config;
|
|
2978
|
+
const loader = new ConfigLoader(configPath);
|
|
2979
|
+
const config2 = loader.load();
|
|
2980
|
+
if (options.path) {
|
|
2981
|
+
const value = loader.get(options.path);
|
|
2982
|
+
if (value === void 0) {
|
|
2983
|
+
console.error(`Config path not found: ${options.path}`);
|
|
2984
|
+
process.exit(1);
|
|
2985
|
+
}
|
|
2986
|
+
if (globalOpts.json) {
|
|
2987
|
+
console.log(JSON.stringify(value, null, 2));
|
|
2988
|
+
} else {
|
|
2989
|
+
console.log(value);
|
|
2990
|
+
}
|
|
2991
|
+
} else {
|
|
2992
|
+
if (globalOpts.json) {
|
|
2993
|
+
console.log(JSON.stringify(config2, null, 2));
|
|
2994
|
+
} else {
|
|
2995
|
+
console.log("Current Configuration:");
|
|
2996
|
+
console.log("=".repeat(40));
|
|
2997
|
+
console.log(`Config file: ${expandPath(configPath || getConfigPath())}`);
|
|
2998
|
+
console.log("");
|
|
2999
|
+
printConfig(config2, 0);
|
|
3000
|
+
}
|
|
3001
|
+
}
|
|
3002
|
+
});
|
|
3003
|
+
configCommand.command("init").description("Create default configuration file").option("-f, --force", "Overwrite existing config file").action((options) => {
|
|
3004
|
+
const globalOpts = configCommand.parent?.opts() || {};
|
|
3005
|
+
const configPath = expandPath(globalOpts.config || getConfigPath());
|
|
3006
|
+
if (fs7.existsSync(configPath) && !options.force) {
|
|
3007
|
+
console.error(`Config file already exists: ${configPath}`);
|
|
3008
|
+
console.error("Use --force to overwrite");
|
|
3009
|
+
process.exit(1);
|
|
3010
|
+
}
|
|
3011
|
+
const loader = new ConfigLoader(configPath);
|
|
3012
|
+
loader.createDefaultConfigFile();
|
|
3013
|
+
if (!(globalOpts.quiet ?? false)) {
|
|
3014
|
+
console.log(`Created config file: ${configPath}`);
|
|
3015
|
+
}
|
|
3016
|
+
});
|
|
3017
|
+
configCommand.command("path").description("Show configuration file path").action(() => {
|
|
3018
|
+
const globalOpts = configCommand.parent?.opts() || {};
|
|
3019
|
+
const configPath = globalOpts.config || getConfigPath();
|
|
3020
|
+
console.log(expandPath(configPath));
|
|
3021
|
+
});
|
|
3022
|
+
configCommand.command("edit").description("Open configuration file in editor").action(() => {
|
|
3023
|
+
const globalOpts = configCommand.parent?.opts() || {};
|
|
3024
|
+
const configPath = expandPath(globalOpts.config || getConfigPath());
|
|
3025
|
+
if (!fs7.existsSync(configPath)) {
|
|
3026
|
+
const loader = new ConfigLoader(configPath);
|
|
3027
|
+
loader.createDefaultConfigFile();
|
|
3028
|
+
}
|
|
3029
|
+
const editor = process.env.EDITOR || process.env.VISUAL || "nano";
|
|
3030
|
+
import("child_process").then(({ spawn: spawn7 }) => {
|
|
3031
|
+
const child = spawn7(editor, [configPath], {
|
|
3032
|
+
stdio: "inherit",
|
|
3033
|
+
shell: true
|
|
3034
|
+
});
|
|
3035
|
+
child.on("exit", (code) => {
|
|
3036
|
+
process.exit(code || 0);
|
|
3037
|
+
});
|
|
3038
|
+
}).catch((error) => {
|
|
3039
|
+
console.error("Failed to open editor:", error.message);
|
|
3040
|
+
process.exit(1);
|
|
3041
|
+
});
|
|
3042
|
+
});
|
|
3043
|
+
configCommand.command("defaults").description("Show default configuration values").action(() => {
|
|
3044
|
+
const globalOpts = configCommand.parent?.opts() || {};
|
|
3045
|
+
if (globalOpts.json) {
|
|
3046
|
+
console.log(JSON.stringify(DEFAULT_CONFIG, null, 2));
|
|
3047
|
+
} else {
|
|
3048
|
+
console.log("Default Configuration:");
|
|
3049
|
+
console.log("=".repeat(40));
|
|
3050
|
+
printConfig(DEFAULT_CONFIG, 0);
|
|
3051
|
+
}
|
|
3052
|
+
});
|
|
3053
|
+
configCommand.command("validate").description("Validate configuration file").action(() => {
|
|
3054
|
+
const globalOpts = configCommand.parent?.opts() || {};
|
|
3055
|
+
const configPath = expandPath(globalOpts.config || getConfigPath());
|
|
3056
|
+
if (!fs7.existsSync(configPath)) {
|
|
3057
|
+
console.error(`Config file not found: ${configPath}`);
|
|
3058
|
+
process.exit(1);
|
|
3059
|
+
}
|
|
3060
|
+
try {
|
|
3061
|
+
const loader = new ConfigLoader(configPath);
|
|
3062
|
+
const config2 = loader.load();
|
|
3063
|
+
const errors = [];
|
|
3064
|
+
if (!config2.storage.type) {
|
|
3065
|
+
errors.push("storage.type is required");
|
|
3066
|
+
}
|
|
3067
|
+
if (!config2.storage.path) {
|
|
3068
|
+
errors.push("storage.path is required");
|
|
3069
|
+
}
|
|
3070
|
+
if (!["sqlite", "filesystem", "memory"].includes(config2.storage.type)) {
|
|
3071
|
+
errors.push(`storage.type must be one of: sqlite, filesystem, memory`);
|
|
3072
|
+
}
|
|
3073
|
+
if (config2.indexer.min_confidence < 0 || config2.indexer.min_confidence > 1) {
|
|
3074
|
+
errors.push("indexer.min_confidence must be between 0 and 1");
|
|
3075
|
+
}
|
|
3076
|
+
if (config2.indexer.batch_size < 1) {
|
|
3077
|
+
errors.push("indexer.batch_size must be at least 1");
|
|
3078
|
+
}
|
|
3079
|
+
if (config2.matching.similarity_threshold < 0 || config2.matching.similarity_threshold > 1) {
|
|
3080
|
+
errors.push("matching.similarity_threshold must be between 0 and 1");
|
|
3081
|
+
}
|
|
3082
|
+
if (errors.length > 0) {
|
|
3083
|
+
console.error("Configuration errors:");
|
|
3084
|
+
for (const error of errors) {
|
|
3085
|
+
console.error(` - ${error}`);
|
|
3086
|
+
}
|
|
3087
|
+
process.exit(1);
|
|
3088
|
+
}
|
|
3089
|
+
if (!(globalOpts.quiet ?? false)) {
|
|
3090
|
+
console.log("Configuration is valid");
|
|
3091
|
+
}
|
|
3092
|
+
} catch (error) {
|
|
3093
|
+
console.error("Failed to parse config file:", error.message);
|
|
3094
|
+
process.exit(1);
|
|
3095
|
+
}
|
|
3096
|
+
});
|
|
3097
|
+
function printConfig(obj, indent) {
|
|
3098
|
+
const prefix = " ".repeat(indent);
|
|
3099
|
+
if (typeof obj !== "object" || obj === null) {
|
|
3100
|
+
console.log(`${prefix}${obj}`);
|
|
3101
|
+
return;
|
|
3102
|
+
}
|
|
3103
|
+
if (Array.isArray(obj)) {
|
|
3104
|
+
if (obj.length === 0) {
|
|
3105
|
+
console.log(`${prefix}[]`);
|
|
3106
|
+
} else {
|
|
3107
|
+
for (const item of obj) {
|
|
3108
|
+
console.log(`${prefix}- ${item}`);
|
|
3109
|
+
}
|
|
3110
|
+
}
|
|
3111
|
+
return;
|
|
3112
|
+
}
|
|
3113
|
+
for (const [key, value] of Object.entries(obj)) {
|
|
3114
|
+
if (typeof value === "object" && value !== null && !Array.isArray(value)) {
|
|
3115
|
+
console.log(`${prefix}${key}:`);
|
|
3116
|
+
printConfig(value, indent + 1);
|
|
3117
|
+
} else if (Array.isArray(value)) {
|
|
3118
|
+
console.log(`${prefix}${key}:`);
|
|
3119
|
+
printConfig(value, indent + 1);
|
|
3120
|
+
} else {
|
|
3121
|
+
const displayValue = value === void 0 ? "(not set)" : value;
|
|
3122
|
+
console.log(`${prefix}${key}: ${displayValue}`);
|
|
3123
|
+
}
|
|
3124
|
+
}
|
|
3125
|
+
}
|
|
3126
|
+
|
|
3127
|
+
// src/cli/commands/sync.ts
|
|
3128
|
+
import { Command as Command22 } from "commander";
|
|
3129
|
+
import * as path5 from "path";
|
|
3130
|
+
import * as fs8 from "fs";
|
|
3131
|
+
function getSyncConfigPath(basePath) {
|
|
3132
|
+
return path5.join(basePath, ".skillbank", "sync-config.json");
|
|
3133
|
+
}
|
|
3134
|
+
async function loadSyncConfig(basePath) {
|
|
3135
|
+
const configPath = getSyncConfigPath(basePath);
|
|
3136
|
+
try {
|
|
3137
|
+
const content = await fs8.promises.readFile(configPath, "utf-8");
|
|
3138
|
+
return JSON.parse(content);
|
|
3139
|
+
} catch {
|
|
3140
|
+
return null;
|
|
3141
|
+
}
|
|
3142
|
+
}
|
|
3143
|
+
async function saveSyncConfig(basePath, config2) {
|
|
3144
|
+
const configPath = getSyncConfigPath(basePath);
|
|
3145
|
+
await fs8.promises.mkdir(path5.dirname(configPath), { recursive: true });
|
|
3146
|
+
await fs8.promises.writeFile(configPath, JSON.stringify(config2, null, 2), "utf-8");
|
|
3147
|
+
}
|
|
3148
|
+
async function createSyncAdapter(basePath, globalOpts) {
|
|
3149
|
+
const config2 = await loadSyncConfig(basePath);
|
|
3150
|
+
if (!config2) {
|
|
3151
|
+
printError('Sync not configured. Run "skill-tree sync init" first.');
|
|
3152
|
+
return null;
|
|
3153
|
+
}
|
|
3154
|
+
const adapter = new GitSyncAdapter({
|
|
3155
|
+
repoPath: basePath,
|
|
3156
|
+
config: config2
|
|
3157
|
+
});
|
|
3158
|
+
await adapter.initialize();
|
|
3159
|
+
return adapter;
|
|
3160
|
+
}
|
|
3161
|
+
var syncCommand2 = new Command22("sync").description("Multi-agent skill synchronization").addCommand(initCommand()).addCommand(statusCommand()).addCommand(pullCommand()).addCommand(pushCommand()).addCommand(conflictsCommand()).addCommand(resolveCommand());
|
|
3162
|
+
function initCommand() {
|
|
3163
|
+
return new Command22("init").description("Initialize sync configuration").requiredOption("-r, --remote <url>", "Remote repository URL").requiredOption("-a, --agent <id>", "Agent ID (slug format)").option("-b, --branch <branch>", "Branch to sync with", "main").option("-n, --name <name>", "Agent name (human-readable)").option("-e, --env <environment>", "Environment (production, staging, etc.)").action(async (options, command) => {
|
|
3164
|
+
const globalOpts = command.optsWithGlobals();
|
|
3165
|
+
const basePath = getSkillPath(globalOpts);
|
|
3166
|
+
try {
|
|
3167
|
+
const existing = await loadSyncConfig(basePath);
|
|
3168
|
+
if (existing) {
|
|
3169
|
+
printWarning("Sync already configured. Overwriting...");
|
|
3170
|
+
}
|
|
3171
|
+
const config2 = createDefaultSyncConfig(options.remote, options.agent, {
|
|
3172
|
+
branch: options.branch,
|
|
3173
|
+
agentName: options.name,
|
|
3174
|
+
environment: options.env
|
|
3175
|
+
});
|
|
3176
|
+
const gitDir = path5.join(basePath, ".git");
|
|
3177
|
+
if (!fs8.existsSync(gitDir)) {
|
|
3178
|
+
printError(`Not a git repository: ${basePath}`);
|
|
3179
|
+
printInfo("Initialize a git repository first with: git init");
|
|
3180
|
+
process.exit(1);
|
|
3181
|
+
}
|
|
3182
|
+
await saveSyncConfig(basePath, config2);
|
|
3183
|
+
printSuccess("Sync configured successfully!");
|
|
3184
|
+
console.log();
|
|
3185
|
+
printInfo(`Remote: ${options.remote}`);
|
|
3186
|
+
printInfo(`Branch: ${options.branch}`);
|
|
3187
|
+
printInfo(`Agent ID: ${options.agent}`);
|
|
3188
|
+
if (options.name) {
|
|
3189
|
+
printInfo(`Agent Name: ${options.name}`);
|
|
3190
|
+
}
|
|
3191
|
+
if (options.env) {
|
|
3192
|
+
printInfo(`Environment: ${options.env}`);
|
|
3193
|
+
}
|
|
3194
|
+
console.log();
|
|
3195
|
+
printInfo('Run "skill-tree sync pull" to fetch skills from remote.');
|
|
3196
|
+
} catch (error) {
|
|
3197
|
+
printError(error.message);
|
|
3198
|
+
process.exit(1);
|
|
3199
|
+
}
|
|
3200
|
+
});
|
|
3201
|
+
}
|
|
3202
|
+
function statusCommand() {
|
|
3203
|
+
return new Command22("status").description("Show sync status").action(async (_options, command) => {
|
|
3204
|
+
const globalOpts = command.optsWithGlobals();
|
|
3205
|
+
const basePath = getSkillPath(globalOpts);
|
|
3206
|
+
try {
|
|
3207
|
+
const adapter = await createSyncAdapter(basePath, globalOpts);
|
|
3208
|
+
if (!adapter) return;
|
|
3209
|
+
const status = await adapter.status();
|
|
3210
|
+
if (globalOpts.json) {
|
|
3211
|
+
console.log(JSON.stringify(status, null, 2));
|
|
3212
|
+
return;
|
|
3213
|
+
}
|
|
3214
|
+
console.log();
|
|
3215
|
+
printInfo(`Branch: ${status.branch}`);
|
|
3216
|
+
printInfo(`Remote: ${status.remoteUrl}`);
|
|
3217
|
+
console.log();
|
|
3218
|
+
if (!status.connected) {
|
|
3219
|
+
printWarning("Not connected to remote");
|
|
3220
|
+
} else {
|
|
3221
|
+
if (status.localAhead > 0) {
|
|
3222
|
+
printWarning(`Your branch is ahead by ${status.localAhead} commit(s)`);
|
|
3223
|
+
}
|
|
3224
|
+
if (status.remoteBehind > 0) {
|
|
3225
|
+
printWarning(`Your branch is behind by ${status.remoteBehind} commit(s)`);
|
|
3226
|
+
}
|
|
3227
|
+
if (status.localAhead === 0 && status.remoteBehind === 0) {
|
|
3228
|
+
printSuccess("Up to date with remote");
|
|
3229
|
+
}
|
|
3230
|
+
}
|
|
3231
|
+
if (status.modifiedSkills.length > 0) {
|
|
3232
|
+
console.log();
|
|
3233
|
+
printInfo(`Modified skills (${status.modifiedSkills.length}):`);
|
|
3234
|
+
for (const skillId of status.modifiedSkills) {
|
|
3235
|
+
console.log(` - ${skillId}`);
|
|
3236
|
+
}
|
|
3237
|
+
}
|
|
3238
|
+
if (status.pendingConflicts > 0) {
|
|
3239
|
+
console.log();
|
|
3240
|
+
printWarning(`Pending conflicts: ${status.pendingConflicts}`);
|
|
3241
|
+
printInfo('Run "skill-tree sync conflicts" to see details');
|
|
3242
|
+
}
|
|
3243
|
+
if (status.lastSync) {
|
|
3244
|
+
console.log();
|
|
3245
|
+
printInfo(`Last sync: ${status.lastSync.toLocaleString()}`);
|
|
3246
|
+
}
|
|
3247
|
+
} catch (error) {
|
|
3248
|
+
printError(error.message);
|
|
3249
|
+
process.exit(1);
|
|
3250
|
+
}
|
|
3251
|
+
});
|
|
3252
|
+
}
|
|
3253
|
+
function pullCommand() {
|
|
3254
|
+
return new Command22("pull").description("Pull skills from remote").option("-s, --skills <ids>", "Specific skill IDs (comma-separated)").option("-f, --force", "Force overwrite local changes").option("-d, --dry-run", "Preview changes without applying").action(async (options, command) => {
|
|
3255
|
+
const globalOpts = command.optsWithGlobals();
|
|
3256
|
+
const basePath = getSkillPath(globalOpts);
|
|
3257
|
+
try {
|
|
3258
|
+
const adapter = await createSyncAdapter(basePath, globalOpts);
|
|
3259
|
+
if (!adapter) return;
|
|
3260
|
+
const skillIds = options.skills?.split(",").map((s) => s.trim());
|
|
3261
|
+
if (options.dryRun) {
|
|
3262
|
+
printInfo("Dry run - no changes will be applied");
|
|
3263
|
+
console.log();
|
|
3264
|
+
}
|
|
3265
|
+
const result = await adapter.pull({
|
|
3266
|
+
skillIds,
|
|
3267
|
+
force: options.force,
|
|
3268
|
+
dryRun: options.dryRun
|
|
3269
|
+
});
|
|
3270
|
+
if (globalOpts.json) {
|
|
3271
|
+
console.log(JSON.stringify(result, null, 2));
|
|
3272
|
+
return;
|
|
3273
|
+
}
|
|
3274
|
+
if (result.pulled.length === 0 && result.conflicts.length === 0) {
|
|
3275
|
+
printSuccess("Already up to date");
|
|
3276
|
+
return;
|
|
3277
|
+
}
|
|
3278
|
+
if (result.pulled.length > 0) {
|
|
3279
|
+
console.log();
|
|
3280
|
+
printSuccess(`Pulled ${result.pulled.length} skill(s):`);
|
|
3281
|
+
for (const change of result.pulled) {
|
|
3282
|
+
const icon = change.changeType === "added" ? "+" : change.changeType === "deleted" ? "-" : "~";
|
|
3283
|
+
console.log(` ${icon} ${change.skillId}`);
|
|
3284
|
+
}
|
|
3285
|
+
}
|
|
3286
|
+
if (result.autoMerged.length > 0) {
|
|
3287
|
+
console.log();
|
|
3288
|
+
printInfo(`Auto-merged ${result.autoMerged.length} skill(s):`);
|
|
3289
|
+
for (const merge of result.autoMerged) {
|
|
3290
|
+
console.log(` ~ ${merge.skillId}`);
|
|
3291
|
+
}
|
|
3292
|
+
}
|
|
3293
|
+
if (result.conflicts.length > 0) {
|
|
3294
|
+
console.log();
|
|
3295
|
+
printWarning(`Conflicts (${result.conflicts.length}):`);
|
|
3296
|
+
for (const conflict of result.conflicts) {
|
|
3297
|
+
console.log(` ! ${conflict.skillId}`);
|
|
3298
|
+
for (const field of conflict.conflictingFields) {
|
|
3299
|
+
console.log(` - ${field}`);
|
|
3300
|
+
}
|
|
3301
|
+
}
|
|
3302
|
+
console.log();
|
|
3303
|
+
printInfo('Run "skill-tree sync conflicts" to resolve');
|
|
3304
|
+
}
|
|
3305
|
+
if (result.errors.length > 0) {
|
|
3306
|
+
console.log();
|
|
3307
|
+
printError(`Errors (${result.errors.length}):`);
|
|
3308
|
+
for (const err of result.errors) {
|
|
3309
|
+
console.log(` - ${err.skillId || "general"}: ${err.message}`);
|
|
3310
|
+
}
|
|
3311
|
+
}
|
|
3312
|
+
} catch (error) {
|
|
3313
|
+
printError(error.message);
|
|
3314
|
+
process.exit(1);
|
|
3315
|
+
}
|
|
3316
|
+
});
|
|
3317
|
+
}
|
|
3318
|
+
function pushCommand() {
|
|
3319
|
+
return new Command22("push").description("Push skills to remote").option("-s, --skills <ids>", "Specific skill IDs (comma-separated)").option("-m, --message <msg>", "Custom commit message").option("-f, --force", "Force push (overwrites remote)").action(async (options, command) => {
|
|
3320
|
+
const globalOpts = command.optsWithGlobals();
|
|
3321
|
+
const basePath = getSkillPath(globalOpts);
|
|
3322
|
+
try {
|
|
3323
|
+
const adapter = await createSyncAdapter(basePath, globalOpts);
|
|
3324
|
+
if (!adapter) return;
|
|
3325
|
+
const skillIds = options.skills?.split(",").map((s) => s.trim());
|
|
3326
|
+
if (options.force) {
|
|
3327
|
+
printWarning("Force push enabled - this may overwrite remote changes!");
|
|
3328
|
+
}
|
|
3329
|
+
const result = await adapter.push({
|
|
3330
|
+
skillIds,
|
|
3331
|
+
message: options.message,
|
|
3332
|
+
force: options.force
|
|
3333
|
+
});
|
|
3334
|
+
if (globalOpts.json) {
|
|
3335
|
+
console.log(JSON.stringify(result, null, 2));
|
|
3336
|
+
return;
|
|
3337
|
+
}
|
|
3338
|
+
if (result.pushed.length === 0 && result.errors.length === 0) {
|
|
3339
|
+
printInfo("Nothing to push");
|
|
3340
|
+
return;
|
|
3341
|
+
}
|
|
3342
|
+
if (result.pushed.length > 0) {
|
|
3343
|
+
console.log();
|
|
3344
|
+
printSuccess(`Pushed ${result.pushed.length} skill(s):`);
|
|
3345
|
+
for (const change of result.pushed) {
|
|
3346
|
+
const icon = change.changeType === "added" ? "+" : "~";
|
|
3347
|
+
console.log(` ${icon} ${change.skillId}`);
|
|
3348
|
+
}
|
|
3349
|
+
}
|
|
3350
|
+
if (result.errors.length > 0) {
|
|
3351
|
+
console.log();
|
|
3352
|
+
if (result.errors.some((e) => e.type === "conflict")) {
|
|
3353
|
+
printWarning("Push rejected - remote has changes");
|
|
3354
|
+
printInfo('Run "skill-tree sync pull" first, then try again');
|
|
3355
|
+
} else {
|
|
3356
|
+
printError(`Errors (${result.errors.length}):`);
|
|
3357
|
+
for (const err of result.errors) {
|
|
3358
|
+
console.log(` - ${err.skillId || "general"}: ${err.message}`);
|
|
3359
|
+
}
|
|
3360
|
+
}
|
|
3361
|
+
}
|
|
3362
|
+
} catch (error) {
|
|
3363
|
+
printError(error.message);
|
|
3364
|
+
process.exit(1);
|
|
3365
|
+
}
|
|
3366
|
+
});
|
|
3367
|
+
}
|
|
3368
|
+
function conflictsCommand() {
|
|
3369
|
+
return new Command22("conflicts").description("List pending conflicts").option("-v, --verbose", "Show detailed conflict information").action(async (options, command) => {
|
|
3370
|
+
const globalOpts = command.optsWithGlobals();
|
|
3371
|
+
const basePath = getSkillPath(globalOpts);
|
|
3372
|
+
try {
|
|
3373
|
+
const adapter = await createSyncAdapter(basePath, globalOpts);
|
|
3374
|
+
if (!adapter) return;
|
|
3375
|
+
const conflicts = await adapter.listConflicts();
|
|
3376
|
+
if (globalOpts.json) {
|
|
3377
|
+
console.log(JSON.stringify(conflicts, null, 2));
|
|
3378
|
+
return;
|
|
3379
|
+
}
|
|
3380
|
+
if (conflicts.length === 0) {
|
|
3381
|
+
printSuccess("No pending conflicts");
|
|
3382
|
+
return;
|
|
3383
|
+
}
|
|
3384
|
+
console.log();
|
|
3385
|
+
printWarning(`${conflicts.length} conflict(s) pending resolution:`);
|
|
3386
|
+
console.log();
|
|
3387
|
+
for (const conflict of conflicts) {
|
|
3388
|
+
console.log(` ${conflict.skillId}`);
|
|
3389
|
+
console.log(` Detected: ${conflict.detectedAt.toLocaleString()}`);
|
|
3390
|
+
console.log(` Local version: ${conflict.localVersion.version}`);
|
|
3391
|
+
console.log(` Remote version: ${conflict.remoteVersion.version}`);
|
|
3392
|
+
console.log(` Conflicting fields:`);
|
|
3393
|
+
for (const field of conflict.conflictingFields) {
|
|
3394
|
+
console.log(` - ${field}`);
|
|
3395
|
+
}
|
|
3396
|
+
if (options.verbose && conflict.fieldConflicts.length > 0) {
|
|
3397
|
+
console.log(` Details:`);
|
|
3398
|
+
for (const fc of conflict.fieldConflicts) {
|
|
3399
|
+
console.log(` ${fc.field}: ${fc.reason}`);
|
|
3400
|
+
}
|
|
3401
|
+
}
|
|
3402
|
+
if (conflict.suggestedResolution) {
|
|
3403
|
+
console.log(
|
|
3404
|
+
` Suggested resolution available (confidence: ${Math.round(conflict.confidence * 100)}%)`
|
|
3405
|
+
);
|
|
3406
|
+
}
|
|
3407
|
+
console.log();
|
|
3408
|
+
}
|
|
3409
|
+
printInfo("To resolve a conflict:");
|
|
3410
|
+
console.log(" skill-tree sync resolve <skill-id> --accept-local");
|
|
3411
|
+
console.log(" skill-tree sync resolve <skill-id> --accept-remote");
|
|
3412
|
+
console.log(" skill-tree sync resolve <skill-id> --accept-merged");
|
|
3413
|
+
} catch (error) {
|
|
3414
|
+
printError(error.message);
|
|
3415
|
+
process.exit(1);
|
|
3416
|
+
}
|
|
3417
|
+
});
|
|
3418
|
+
}
|
|
3419
|
+
function resolveCommand() {
|
|
3420
|
+
return new Command22("resolve").description("Resolve a conflict").argument("<skill-id>", "Skill ID to resolve").option("-l, --accept-local", "Accept local version").option("-r, --accept-remote", "Accept remote version").option("-m, --accept-merged", "Accept auto-merged suggestion").action(async (skillId, options, command) => {
|
|
3421
|
+
const globalOpts = command.optsWithGlobals();
|
|
3422
|
+
const basePath = getSkillPath(globalOpts);
|
|
3423
|
+
try {
|
|
3424
|
+
const adapter = await createSyncAdapter(basePath, globalOpts);
|
|
3425
|
+
if (!adapter) return;
|
|
3426
|
+
let strategy;
|
|
3427
|
+
if (options.acceptLocal) {
|
|
3428
|
+
strategy = "accept-local";
|
|
3429
|
+
} else if (options.acceptRemote) {
|
|
3430
|
+
strategy = "accept-remote";
|
|
3431
|
+
} else if (options.acceptMerged) {
|
|
3432
|
+
strategy = "accept-merged";
|
|
3433
|
+
} else {
|
|
3434
|
+
printError("Specify a resolution strategy:");
|
|
3435
|
+
console.log(" --accept-local Keep your local version");
|
|
3436
|
+
console.log(" --accept-remote Accept the remote version");
|
|
3437
|
+
console.log(" --accept-merged Accept the auto-merged suggestion");
|
|
3438
|
+
process.exit(1);
|
|
3439
|
+
return;
|
|
3440
|
+
}
|
|
3441
|
+
await adapter.resolveConflict(skillId, { strategy });
|
|
3442
|
+
printSuccess(`Resolved conflict for ${skillId} using ${strategy}`);
|
|
3443
|
+
printInfo('The resolved version has been staged. Run "skill-tree sync push" when ready.');
|
|
3444
|
+
} catch (error) {
|
|
3445
|
+
printError(error.message);
|
|
3446
|
+
process.exit(1);
|
|
3447
|
+
}
|
|
3448
|
+
});
|
|
3449
|
+
}
|
|
3450
|
+
|
|
3451
|
+
// src/cli/commands/mcp.ts
|
|
3452
|
+
import { Command as Command23 } from "commander";
|
|
3453
|
+
import * as readline from "readline";
|
|
3454
|
+
var mcpCommand = new Command23("mcp").description("Start MCP server over stdio (JSON-RPC 2.0)").option("--agent-can-set-loadout", "Allow agent to set loadout via criteria", false).option("--max-expanded <n>", "Maximum number of expanded skills", "5").action(async (options, command) => {
|
|
3455
|
+
const globalOpts = command.optsWithGlobals();
|
|
3456
|
+
const skillPath = resolveSkillPath(globalOpts.path);
|
|
3457
|
+
ensureDir(skillPath);
|
|
3458
|
+
const storage = new FilesystemStorageAdapter({ basePath: skillPath });
|
|
3459
|
+
await storage.initialize();
|
|
3460
|
+
const graphServer = new SkillGraphServer(storage, {
|
|
3461
|
+
agentCanModify: true,
|
|
3462
|
+
agentCanSetLoadout: globalOpts.agentCanSetLoadout ?? false,
|
|
3463
|
+
agentCanSwitchProfile: true,
|
|
3464
|
+
requireApproval: false,
|
|
3465
|
+
autoExpandOnUse: true,
|
|
3466
|
+
autoExpandRelated: false,
|
|
3467
|
+
maxExpanded: parseInt(globalOpts.maxExpanded || "5", 10)
|
|
3468
|
+
});
|
|
3469
|
+
await graphServer.initialize();
|
|
3470
|
+
const mcpServer = new SkillTreeMcpServer(graphServer, {
|
|
3471
|
+
dynamicTools: false
|
|
3472
|
+
});
|
|
3473
|
+
const rl = readline.createInterface({ input: process.stdin, terminal: false });
|
|
3474
|
+
let buffer = "";
|
|
3475
|
+
rl.on("line", async (line) => {
|
|
3476
|
+
buffer += line;
|
|
3477
|
+
let msg;
|
|
3478
|
+
try {
|
|
3479
|
+
msg = JSON.parse(buffer);
|
|
3480
|
+
buffer = "";
|
|
3481
|
+
} catch {
|
|
3482
|
+
return;
|
|
3483
|
+
}
|
|
3484
|
+
const { id, method, params } = msg;
|
|
3485
|
+
try {
|
|
3486
|
+
let result;
|
|
3487
|
+
switch (method) {
|
|
3488
|
+
case "initialize":
|
|
3489
|
+
result = {
|
|
3490
|
+
protocolVersion: "2024-11-05",
|
|
3491
|
+
capabilities: { tools: {} },
|
|
3492
|
+
serverInfo: mcpServer.getServerInfo()
|
|
3493
|
+
};
|
|
3494
|
+
break;
|
|
3495
|
+
case "notifications/initialized":
|
|
3496
|
+
return;
|
|
3497
|
+
case "tools/list":
|
|
3498
|
+
result = {
|
|
3499
|
+
tools: mcpServer.getTools().map((t) => ({
|
|
3500
|
+
name: t.name,
|
|
3501
|
+
description: t.description,
|
|
3502
|
+
inputSchema: t.inputSchema
|
|
3503
|
+
}))
|
|
3504
|
+
};
|
|
3505
|
+
break;
|
|
3506
|
+
case "tools/call": {
|
|
3507
|
+
const { name, arguments: args } = params;
|
|
3508
|
+
const toolResult = await mcpServer.executeTool(name, args || {});
|
|
3509
|
+
result = {
|
|
3510
|
+
content: [
|
|
3511
|
+
{
|
|
3512
|
+
type: "text",
|
|
3513
|
+
text: JSON.stringify(toolResult, null, 2)
|
|
3514
|
+
}
|
|
3515
|
+
],
|
|
3516
|
+
isError: !toolResult.success
|
|
3517
|
+
};
|
|
3518
|
+
break;
|
|
3519
|
+
}
|
|
3520
|
+
case "ping":
|
|
3521
|
+
result = {};
|
|
3522
|
+
break;
|
|
3523
|
+
default:
|
|
3524
|
+
respond(id, null, { code: -32601, message: `Method not found: ${method}` });
|
|
3525
|
+
return;
|
|
3526
|
+
}
|
|
3527
|
+
respond(id, result);
|
|
3528
|
+
} catch (err) {
|
|
3529
|
+
respond(id, null, { code: -32603, message: String(err) });
|
|
3530
|
+
}
|
|
3531
|
+
});
|
|
3532
|
+
function respond(id, result, error) {
|
|
3533
|
+
if (id === void 0) return;
|
|
3534
|
+
const response = { jsonrpc: "2.0", id };
|
|
3535
|
+
if (error) {
|
|
3536
|
+
response.error = error;
|
|
3537
|
+
} else {
|
|
3538
|
+
response.result = result;
|
|
3539
|
+
}
|
|
3540
|
+
process.stdout.write(JSON.stringify(response) + "\n");
|
|
3541
|
+
}
|
|
3542
|
+
process.on("SIGTERM", () => {
|
|
3543
|
+
mcpServer.dispose();
|
|
3544
|
+
process.exit(0);
|
|
3545
|
+
});
|
|
3546
|
+
});
|
|
3547
|
+
|
|
3548
|
+
// src/cli/index.ts
|
|
3549
|
+
var program = new Command24();
|
|
3550
|
+
var config = loadConfig();
|
|
3551
|
+
program.name("skill-tree").description("Management CLI for agent skills").version(VERSION).option("-p, --path <dir>", "Skills directory path", config.storage.path).option("-c, --config <file>", "Config file path", getConfigPath()).option("--json", "Output as JSON", config.cli.output_format === "json").option("-q, --quiet", "Suppress non-essential output", config.cli.quiet).option("--no-color", "Disable colored output", !config.cli.color);
|
|
3552
|
+
program.addCommand(listCommand);
|
|
3553
|
+
program.addCommand(showCommand);
|
|
3554
|
+
program.addCommand(searchCommand);
|
|
3555
|
+
program.addCommand(statsCommand);
|
|
3556
|
+
program.addCommand(versionsCommand);
|
|
3557
|
+
program.addCommand(diffCommand);
|
|
3558
|
+
program.addCommand(rollbackCommand);
|
|
3559
|
+
program.addCommand(forkCommand);
|
|
3560
|
+
program.addCommand(deprecateCommand);
|
|
3561
|
+
program.addCommand(activateCommand);
|
|
3562
|
+
program.addCommand(deleteCommand);
|
|
3563
|
+
program.addCommand(exportCommand);
|
|
3564
|
+
program.addCommand(importCommand);
|
|
3565
|
+
program.addCommand(indexerCommand);
|
|
3566
|
+
program.addCommand(configCommand);
|
|
3567
|
+
program.addCommand(syncCommand2);
|
|
3568
|
+
program.addCommand(mcpCommand);
|
|
3569
|
+
program.parse();
|