@heytherevibin/skillforge 0.10.0 → 0.11.7
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/CHANGELOG.md +53 -0
- package/CONTRIBUTING.md +5 -3
- package/README.md +37 -345
- package/RELEASING.md +8 -7
- package/STRATEGY.md +2 -2
- package/bin/cli.js +297 -52
- package/ci/test-user-env-profile.cjs +65 -0
- package/docs/README.md +14 -0
- package/docs/architecture-and-data.md +90 -0
- package/docs/cli-reference.md +57 -0
- package/docs/environment-and-configuration.md +76 -0
- package/docs/getting-started.md +88 -0
- package/docs/mcp-integration.md +75 -0
- package/docs/troubleshooting.md +50 -0
- package/lib/templates/claude-code-skillforge-global.md +3 -3
- package/lib/templates/cursor-skillforge-global.md +6 -2
- package/lib/user-env-profile.js +141 -0
- package/package.json +3 -2
- package/python/app/agent_cli.py +334 -0
- package/python/app/explain_route.py +170 -0
- package/python/app/health_cli.py +13 -0
- package/python/app/main.py +131 -48
- package/python/app/materialize.py +150 -68
- package/python/app/mcp_contract.py +2 -1
- package/python/app/mcp_operator.py +252 -0
- package/python/app/mcp_server.py +290 -118
- package/python/app/npm_pkg_version.py +38 -0
- package/python/app/pick_diversify.py +51 -0
- package/python/app/replay_cli.py +145 -0
- package/python/app/route_cli.py +251 -87
- package/python/app/route_cli_pick.py +35 -0
- package/python/app/route_policies.py +18 -3
- package/python/app/route_quality.py +70 -1
- package/python/app/router_llm.py +85 -0
- package/python/app/router_mode.py +21 -0
- package/python/app/routing_signals.py +7 -1
- package/python/app/skill_manifest.py +67 -0
- package/python/app/skills_author_cli.py +117 -0
- package/python/app/tips_cli.py +37 -0
- package/python/app/tools_cli.py +276 -0
- package/python/fixtures/route_eval/smoke.json +5 -0
- package/python/requirements.txt +1 -0
- package/python/tests/test_capabilities_bundle.py +33 -0
- package/python/tests/test_materialize_hosts.py +108 -0
- package/python/tests/test_mcp_contract.py +1 -1
- package/python/tests/test_mcp_initialize_clientinfo.py +26 -0
- package/python/tests/test_mcp_operator.py +84 -0
- package/python/tests/test_npm_pkg_version.py +21 -0
- package/python/tests/test_pick_diversify.py +47 -0
- package/python/tests/test_replay_cli.py +31 -0
- package/python/tests/test_route_cli_pick.py +25 -0
- package/python/tests/test_route_policies.py +29 -0
- package/python/tests/test_route_quality.py +72 -0
- package/python/tests/test_router_llm.py +63 -0
- package/python/tests/test_router_mode_env.py +21 -0
- package/python/tests/test_routing_signals.py +20 -0
- package/python/tests/test_skill_manifest.py +48 -0
- package/python/tests/test_tools_cli.py +69 -0
package/bin/cli.js
CHANGED
|
@@ -6,15 +6,20 @@
|
|
|
6
6
|
* skillforge, skillforge --help Show help (primary path: MCP, not a web app)
|
|
7
7
|
* skillforge mcp MCP stdio server (Claude / Cursor / …)
|
|
8
8
|
* skillforge events [--watch] [--limit=N] Print SQLite routing events
|
|
9
|
+
* skillforge replay [--session-id] Chronological SQLite event replay
|
|
10
|
+
* skillforge tools <cmd> [--json] MCP tool parity (see skillforge tools -h)
|
|
11
|
+
* skillforge tips Short MCP + terminal cheatsheet
|
|
12
|
+
* skillforge agent [--prompt=…] Terminal chat agent (OpenAI-compatible API tools)
|
|
9
13
|
* skillforge route [words…] [--prompt=…] Same routing as MCP route_skills (terminal)
|
|
10
14
|
* skillforge index --project-root=… Chunk/embed repo files for project RAG
|
|
11
15
|
* skillforge health [--quick] [--json] Preflight: paths, catalog, optional router load
|
|
12
16
|
* skillforge route-eval --fixture=… Embedding-only regression cases (CI-friendly)
|
|
13
17
|
* skillforge weights export|import Snapshot learned weights (JSON)
|
|
14
18
|
* skillforge install One-time Python venv + deps (+ Cursor /skillforge when detected)
|
|
19
|
+
* skillforge config path|init|validate … ~/.skillforge/env (dotenv-style profile + linter)
|
|
15
20
|
* skillforge hosts init [--force] Install global /skillforge for Cursor + Claude Code (no Python setup)
|
|
16
21
|
* skillforge cursor init [--force] Same as hosts init (alias)
|
|
17
|
-
* skillforge skills …
|
|
22
|
+
* skillforge skills list|add|remove|init|lint … ; pack … ; reset
|
|
18
23
|
*/
|
|
19
24
|
|
|
20
25
|
const path = require('path');
|
|
@@ -22,6 +27,7 @@ const fs = require('fs');
|
|
|
22
27
|
const { spawn, spawnSync } = require('child_process');
|
|
23
28
|
const os = require('os');
|
|
24
29
|
const packs = require('../lib/packs');
|
|
30
|
+
const userEnvProfile = require('../lib/user-env-profile');
|
|
25
31
|
|
|
26
32
|
const PKG_ROOT = path.resolve(__dirname, '..');
|
|
27
33
|
const PKG = require(path.join(PKG_ROOT, 'package.json'));
|
|
@@ -34,6 +40,8 @@ const USER_SKILLS_DIR = path.join(CONFIG_DIR, 'skills');
|
|
|
34
40
|
/** Bearer-token file for the removed HTTP API (<=0.6.x); deleted on first CLI use. */
|
|
35
41
|
const LEGACY_AUTH_FILE = path.join(CONFIG_DIR, 'auth.json');
|
|
36
42
|
const SETUP_MARKER = path.join(CONFIG_DIR, '.setup-complete');
|
|
43
|
+
/** User-owned KEY=VALUE profile (merged before process.env — shell / MCP host overrides). */
|
|
44
|
+
const USER_ENV_PATH = path.join(CONFIG_DIR, 'env');
|
|
37
45
|
|
|
38
46
|
const args = process.argv.slice(2);
|
|
39
47
|
const cmd = args[0];
|
|
@@ -179,7 +187,9 @@ function setupIfNeeded() {
|
|
|
179
187
|
}
|
|
180
188
|
|
|
181
189
|
function buildEnv(extra = {}) {
|
|
190
|
+
const { vars: profile } = userEnvProfile.readUserEnvProfileFromFile(USER_ENV_PATH);
|
|
182
191
|
return {
|
|
192
|
+
...profile,
|
|
183
193
|
...process.env,
|
|
184
194
|
SKILLFORGE_BUNDLED_SKILLS: path.join(PKG_ROOT, 'skills'),
|
|
185
195
|
SKILLFORGE_USER_SKILLS: USER_SKILLS_DIR,
|
|
@@ -190,10 +200,121 @@ function buildEnv(extra = {}) {
|
|
|
190
200
|
};
|
|
191
201
|
}
|
|
192
202
|
|
|
203
|
+
function printUserEnvProfilePathLine() {
|
|
204
|
+
process.stdout.write(USER_ENV_PATH + '\n');
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
function runInitUserEnv(templateForce) {
|
|
208
|
+
ensureDirs();
|
|
209
|
+
if (fs.existsSync(USER_ENV_PATH) && !templateForce) {
|
|
210
|
+
err(`${USER_ENV_PATH} already exists.`);
|
|
211
|
+
log(c.dim(' Use --force to replace it with the template (you will lose current contents).'));
|
|
212
|
+
process.exit(1);
|
|
213
|
+
}
|
|
214
|
+
const template =
|
|
215
|
+
'# Skillforge user environment profile (~/.skillforge/env).\n' +
|
|
216
|
+
'# Loaded for every Skillforge subprocess (CLI and `skillforge mcp`).\n' +
|
|
217
|
+
'# Merge order: entries here first, then process/MCP host env overwrites duplicates; SKILLFORGE_BUNDLED_SKILLS,\n' +
|
|
218
|
+
'# SKILLFORGE_USER_SKILLS, SKILLFORGE_DB_PATH, and PYTHONPATH are always finalized by Skillforge.\n' +
|
|
219
|
+
'# Omit secrets from VCS; chmod 600 is recommended.\n#\n' +
|
|
220
|
+
'# Examples (uncomment and set):\n' +
|
|
221
|
+
'# ANTHROPIC_API_KEY=\n' +
|
|
222
|
+
'# SKILLFORGE_ROUTER_MODE=host\n' +
|
|
223
|
+
'# OPENAI_API_BASE=http://127.0.0.1:11434/v1\n' +
|
|
224
|
+
'# SKILLFORGE_ROUTER_LLM_BACKEND=openai_compatible\n' +
|
|
225
|
+
'# SKILLFORGE_AGENT_MODEL=llama3.2\n' +
|
|
226
|
+
'\n';
|
|
227
|
+
fs.writeFileSync(USER_ENV_PATH, template, 'utf8');
|
|
228
|
+
if (!isWindows()) {
|
|
229
|
+
try {
|
|
230
|
+
fs.chmodSync(USER_ENV_PATH, 0o600);
|
|
231
|
+
} catch (_) {
|
|
232
|
+
/* chmod optional */
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
ok(`Wrote commented template:\n${c.dim(` ${USER_ENV_PATH}`)}`);
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
function runValidateEnvProfileCmd() {
|
|
239
|
+
const r = userEnvProfile.readUserEnvProfileFromFile(USER_ENV_PATH);
|
|
240
|
+
if (r.missingFile) {
|
|
241
|
+
info('Optional profile file not created yet.');
|
|
242
|
+
log(c.dim(` Path: ${USER_ENV_PATH}`));
|
|
243
|
+
log(c.dim(' scaffold: skillforge config init'));
|
|
244
|
+
process.exit(0);
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
let nErr = 0;
|
|
248
|
+
let nWarn = 0;
|
|
249
|
+
for (const issue of r.issues) {
|
|
250
|
+
const loc = issue.line ? ` ${c.dim(`(line ${issue.line})`)}` : '';
|
|
251
|
+
if (issue.level === 'error') {
|
|
252
|
+
nErr += 1;
|
|
253
|
+
err(`${issue.message}${loc}`);
|
|
254
|
+
} else {
|
|
255
|
+
nWarn += 1;
|
|
256
|
+
log(c.yellow('⚠'), `${issue.message}${loc}`);
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
if (nErr === 0 && nWarn === 0) {
|
|
261
|
+
ok(`Profile syntax OK — ${USER_ENV_PATH}`);
|
|
262
|
+
process.exit(0);
|
|
263
|
+
}
|
|
264
|
+
if (nErr === 0) {
|
|
265
|
+
ok(`Validated — ${nWarn} warning(s) only.`);
|
|
266
|
+
process.exit(0);
|
|
267
|
+
}
|
|
268
|
+
log(c.dim(` Correct ${USER_ENV_PATH}, then run skillforge config validate again.`));
|
|
269
|
+
process.exit(1);
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
function runConfigCmd() {
|
|
273
|
+
const sub = args[1];
|
|
274
|
+
if (
|
|
275
|
+
sub === undefined ||
|
|
276
|
+
sub === '--help' ||
|
|
277
|
+
sub === '-h'
|
|
278
|
+
) {
|
|
279
|
+
log(c.bold('Usage'));
|
|
280
|
+
log(c.dim(' skillforge config path'));
|
|
281
|
+
log(c.dim(' skillforge config init [--force]'));
|
|
282
|
+
log(c.dim(' skillforge config validate\n'));
|
|
283
|
+
log(c.bold('Description'));
|
|
284
|
+
log(
|
|
285
|
+
c.dim(
|
|
286
|
+
' path Print ~/.skillforge/env (dotenv KEY=value; optional export; # comments).\n' +
|
|
287
|
+
' init Create a commented template (--force replaces an existing profile).\n' +
|
|
288
|
+
' validate Lint the profile — errors exit 1; missing file exits 0 (optional).',
|
|
289
|
+
),
|
|
290
|
+
);
|
|
291
|
+
process.exit(sub === undefined ? 1 : 0);
|
|
292
|
+
}
|
|
293
|
+
if (sub === 'path') {
|
|
294
|
+
printUserEnvProfilePathLine();
|
|
295
|
+
return;
|
|
296
|
+
}
|
|
297
|
+
if (sub === 'init') {
|
|
298
|
+
const templateForce =
|
|
299
|
+
args.includes('--force');
|
|
300
|
+
runInitUserEnv(templateForce);
|
|
301
|
+
log(c.dim(' Run skillforge config validate after editing.'));
|
|
302
|
+
return;
|
|
303
|
+
}
|
|
304
|
+
if (sub === 'validate') {
|
|
305
|
+
runValidateEnvProfileCmd();
|
|
306
|
+
return;
|
|
307
|
+
}
|
|
308
|
+
err(`Unknown config subcommand: ${sub}`);
|
|
309
|
+
log(c.dim(' Try: path, init, validate'));
|
|
310
|
+
process.exit(1);
|
|
311
|
+
}
|
|
312
|
+
|
|
193
313
|
function printMcpConfig() {
|
|
194
314
|
setupIfNeeded();
|
|
195
315
|
const useLocal = args.includes('--local');
|
|
196
316
|
const withKey = args.includes('--with-anthropic');
|
|
317
|
+
const withEnv = args.includes('--with-env');
|
|
197
318
|
const cliJs = path.join(PKG_ROOT, 'bin', 'cli.js');
|
|
198
319
|
/** @type {Record<string, unknown>} */
|
|
199
320
|
const entry = useLocal
|
|
@@ -206,20 +327,32 @@ function printMcpConfig() {
|
|
|
206
327
|
args: ['-y', NPM_PKG_NAME, 'mcp'],
|
|
207
328
|
};
|
|
208
329
|
if (withKey) {
|
|
209
|
-
entry.env = {
|
|
330
|
+
entry.env = {
|
|
331
|
+
SKILLFORGE_ROUTER_MODE: 'auto',
|
|
332
|
+
ANTHROPIC_API_KEY: 'sk-ant-…',
|
|
333
|
+
};
|
|
334
|
+
} else if (withEnv) {
|
|
335
|
+
entry.env = {
|
|
336
|
+
SKILLFORGE_ROUTER_MODE: 'host',
|
|
337
|
+
};
|
|
210
338
|
}
|
|
211
339
|
const out = { mcpServers: { skillforge: entry } };
|
|
212
340
|
process.stdout.write(JSON.stringify(out, null, 2) + '\n');
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
)
|
|
217
|
-
)
|
|
341
|
+
let note =
|
|
342
|
+
'Merge into ~/.cursor/mcp.json, Claude Desktop config, etc. --local uses this package checkout. ' +
|
|
343
|
+
'Routing: default host (two-step picked_names); --with-anthropic ⇒ SKILLFORGE_ROUTER_MODE=auto plus ANTHROPIC_API_KEY placeholder. ' +
|
|
344
|
+
'--with-env adds server.env SKILLFORGE_ROUTER_MODE=host explicitly (combine with ~/.skillforge/env for secrets).';
|
|
345
|
+
if (withKey && withEnv) {
|
|
346
|
+
note += ' When both --with-env and --with-anthropic are passed, the emitted env matches --with-anthropic only.';
|
|
347
|
+
}
|
|
348
|
+
process.stderr.write(c.dim(`${note}\n`));
|
|
218
349
|
}
|
|
219
350
|
|
|
220
351
|
function runMcpServer() {
|
|
221
352
|
setupIfNeeded();
|
|
222
|
-
const env = buildEnv(
|
|
353
|
+
const env = buildEnv({
|
|
354
|
+
SKILLFORGE_TRANSPORT: 'mcp',
|
|
355
|
+
});
|
|
223
356
|
// MCP JSON-RPC must own stdout. This process must not log to stdout after this point.
|
|
224
357
|
const proc = spawn(venvPython(), ['-m', 'app.mcp_server'], {
|
|
225
358
|
stdio: ['inherit', 'inherit', 'inherit'],
|
|
@@ -240,6 +373,55 @@ function runEventsCmd() {
|
|
|
240
373
|
proc.on('exit', (code) => process.exit(code ?? 0));
|
|
241
374
|
}
|
|
242
375
|
|
|
376
|
+
function runReplayCmd() {
|
|
377
|
+
setupIfNeeded();
|
|
378
|
+
const sub = args.slice(1);
|
|
379
|
+
const proc = spawn(venvPython(), ['-m', 'app.replay_cli', ...sub], {
|
|
380
|
+
stdio: 'inherit',
|
|
381
|
+
env: buildEnv(),
|
|
382
|
+
});
|
|
383
|
+
proc.on('exit', (code) => process.exit(code ?? 0));
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
function runTipsCmd() {
|
|
387
|
+
setupIfNeeded();
|
|
388
|
+
const proc = spawn(venvPython(), ['-m', 'app.tips_cli'], {
|
|
389
|
+
stdio: 'inherit',
|
|
390
|
+
env: buildEnv(),
|
|
391
|
+
});
|
|
392
|
+
proc.on('exit', (code) => process.exit(code ?? 0));
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
function runToolsCmd() {
|
|
396
|
+
setupIfNeeded();
|
|
397
|
+
const sub = args.slice(2);
|
|
398
|
+
const proc = spawn(venvPython(), ['-m', 'app.tools_cli', ...sub], {
|
|
399
|
+
stdio: 'inherit',
|
|
400
|
+
env: buildEnv(),
|
|
401
|
+
});
|
|
402
|
+
proc.on('exit', (code) => process.exit(code ?? 0));
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
function runAgentCmd() {
|
|
406
|
+
setupIfNeeded();
|
|
407
|
+
// args = argv after 'skillforge' — ["agent", ...], same pattern as route (slice after subcommand).
|
|
408
|
+
const sub = args.slice(1);
|
|
409
|
+
const proc = spawn(venvPython(), ['-m', 'app.agent_cli', ...sub], {
|
|
410
|
+
stdio: 'inherit',
|
|
411
|
+
env: buildEnv(),
|
|
412
|
+
});
|
|
413
|
+
proc.on('exit', (code) => process.exit(code ?? 0));
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
function runSkillsAuthorCmd(mode, argv) {
|
|
417
|
+
setupIfNeeded();
|
|
418
|
+
const proc = spawn(venvPython(), ['-m', 'app.skills_author_cli', mode, ...argv], {
|
|
419
|
+
stdio: 'inherit',
|
|
420
|
+
env: buildEnv(),
|
|
421
|
+
});
|
|
422
|
+
proc.on('exit', (code) => process.exit(code ?? 0));
|
|
423
|
+
}
|
|
424
|
+
|
|
243
425
|
function runRouteCmd() {
|
|
244
426
|
setupIfNeeded();
|
|
245
427
|
const sub = args.slice(1);
|
|
@@ -378,55 +560,101 @@ function runHostsInit() {
|
|
|
378
560
|
}
|
|
379
561
|
|
|
380
562
|
function showHelp() {
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
${c.
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
skillforge
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
563
|
+
const colW = 48;
|
|
564
|
+
const row = (cmd, desc) => {
|
|
565
|
+
const s = `${cmd}`;
|
|
566
|
+
const padLen = Math.max(2, colW - s.length);
|
|
567
|
+
const pad = ' '.repeat(padLen);
|
|
568
|
+
return ` ${c.cyan(s)}${pad}${desc}`;
|
|
569
|
+
};
|
|
570
|
+
const sec = title => `\n${c.bold(title)}\n${c.dim(' ' + '─'.repeat(72))}`;
|
|
571
|
+
log('');
|
|
572
|
+
log(c.bold(`Skillforge`) + ` ${c.dim('local SKILL.md orchestration')}`);
|
|
573
|
+
log(
|
|
574
|
+
`${c.dim('Version')} ${PKG_VERSION}${c.dim(' · ')}Enterprise automation and MCP hosts share the same Python engine.`,
|
|
575
|
+
);
|
|
576
|
+
log(`${c.dim('State directory')} ${CONFIG_DIR}`);
|
|
577
|
+
log(
|
|
578
|
+
`\n${c.dim('PRIMARY INTEGRATION (production workloads):')} ${c.bold('stdio MCP')} — ${c.dim('configure')} ${c.cyan('skillforge mcp config')} ${c.dim(', then restart the IDE / agent.')}`,
|
|
579
|
+
);
|
|
580
|
+
log(
|
|
581
|
+
`${c.dim('TERMINAL (operators, scripting, CI):')} ${c.dim('routing, observability, and')} ${c.cyan('skillforge tools')} ${c.dim('(MCP tool parity).')}`,
|
|
582
|
+
);
|
|
583
|
+
|
|
584
|
+
log(sec('Model Context Protocol'));
|
|
585
|
+
log(row('skillforge mcp', 'Start JSON-RPC MCP server over stdio (Cursor, Claude Desktop, compatible hosts)'));
|
|
586
|
+
log(
|
|
587
|
+
row(
|
|
588
|
+
'skillforge mcp config [--local] [--with-anthropic] [--with-env]',
|
|
589
|
+
'Emit MCP host JSON (--with-env adds SKILLFORGE_ROUTER_MODE=host)',
|
|
590
|
+
),
|
|
591
|
+
);
|
|
592
|
+
log(` ${c.dim('Default routing')} SKILLFORGE_ROUTER_MODE=host · two-step shortlist then picked_names`);
|
|
593
|
+
log(
|
|
594
|
+
` ${c.dim('Minimal npx host entry')} ${JSON.stringify({
|
|
595
|
+
mcpServers: {
|
|
596
|
+
skillforge: { command: 'npx', args: ['-y', NPM_PKG_NAME, 'mcp'] },
|
|
597
|
+
},
|
|
598
|
+
})}`,
|
|
599
|
+
);
|
|
600
|
+
|
|
601
|
+
log(sec('Routing & context'));
|
|
602
|
+
log(row('skillforge agent [--prompt TEXT]', 'Standalone chat agent · OpenAI-compatible API + MCP tool handlers'));
|
|
603
|
+
log(row('skillforge route [TEXT…]', 'Interactive / scripted routing · --json · -i · --explain (see route --help)'));
|
|
604
|
+
log(row('skillforge index --project-root=…', 'Project text index RAG chunks (SQLite project_chunks)'));
|
|
605
|
+
log(row('skillforge tips', 'Short operator reference (routing, env, MCP host mode)'));
|
|
606
|
+
|
|
607
|
+
log(sec('MCP tool parity (CLI)'));
|
|
608
|
+
log(row('skillforge tools …', 'Subcommands mirror MCP tools (same handlers)'));
|
|
609
|
+
log(
|
|
610
|
+
row(
|
|
611
|
+
'skillforge tools --help',
|
|
612
|
+
'search · explain · get · catalog · feedback · disable · referenced',
|
|
613
|
+
),
|
|
614
|
+
);
|
|
615
|
+
log(
|
|
616
|
+
` ${c.dim('└')} ${c.dim('materialize · bootstrap · capabilities · router-status · index-status · weights-snapshot · events-recent')}`,
|
|
617
|
+
);
|
|
618
|
+
log(row('skillforge tools … --json', 'Raw tool envelope (content + _meta) for automation'));
|
|
619
|
+
|
|
620
|
+
log(sec('Observability & diagnostics'));
|
|
621
|
+
log(row('skillforge events …', '--watch routing / usage SQLite tail'));
|
|
622
|
+
log(row('skillforge replay …', 'Chronological event timeline (--session-id, --json)'));
|
|
623
|
+
log(row('skillforge health …', 'Preflight: paths · catalog (--quick skips embed load)'));
|
|
624
|
+
log(row('skillforge route-eval …', 'Fixture embedding regression harness (CI)'));
|
|
625
|
+
log(row('skillforge weights export|import …', 'Portable learned weights snapshot'));
|
|
626
|
+
|
|
627
|
+
log(sec('Catalog & authoring'));
|
|
628
|
+
log(row('skillforge skills list|add|remove|init|lint …', 'Filesystem skill trees + scaffold + manifest lint'));
|
|
629
|
+
log(row('skillforge pack install|list|update|remove …', 'Git-hosted skill bundles'));
|
|
630
|
+
|
|
631
|
+
log(sec('Setup & lifecycle'));
|
|
632
|
+
log(row('skillforge install [--force-cursor]', 'Python venv + deps + optional Cursor / Claude Code slash templates'));
|
|
633
|
+
log(row('skillforge config path|init|validate …', 'Stable ~/.skillforge/env (dotenv linter: config validate · README)'));
|
|
634
|
+
log(row('skillforge hosts init · skillforge cursor init', 'Rewrite managed /skillforge host commands (--force)'));
|
|
635
|
+
log(row('skillforge reset', 'Drop SQLite learning + event history'));
|
|
636
|
+
|
|
637
|
+
log(`\n${c.bold('First run')} ${c.cyan('skillforge install')} ${c.dim('or')} ${c.cyan(`npx -y ${NPM_PKG_NAME} install`)}`);
|
|
638
|
+
log(
|
|
639
|
+
c.dim(
|
|
640
|
+
' Auto-provisions ~/.skillforge/venv · optional Cursor + Claude Code /skillforge commands ' +
|
|
641
|
+
'(SKILLFORGE_SKIP_CURSOR_SETUP, SKILLFORGE_SKIP_CLAUDE_CODE_SETUP).',
|
|
642
|
+
),
|
|
643
|
+
);
|
|
644
|
+
log(`\n${c.bold('Documentation')} README.md + docs/ · npm/GitHub (${NPM_PKG_NAME})`);
|
|
423
645
|
}
|
|
424
646
|
|
|
425
647
|
// ---- main ----
|
|
426
648
|
async function main() {
|
|
427
649
|
dropLegacyAuthJsonIfPresent();
|
|
428
650
|
|
|
429
|
-
if (
|
|
651
|
+
if (cmd === 'help') {
|
|
652
|
+
showHelp();
|
|
653
|
+
return;
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
const wantsCliHelp = args.includes('--help') || args.includes('-h');
|
|
657
|
+
if (wantsCliHelp && (cmd === undefined || cmd === '--help' || cmd === '-h')) {
|
|
430
658
|
showHelp();
|
|
431
659
|
return;
|
|
432
660
|
}
|
|
@@ -438,6 +666,18 @@ async function main() {
|
|
|
438
666
|
case 'events':
|
|
439
667
|
runEventsCmd();
|
|
440
668
|
break;
|
|
669
|
+
case 'replay':
|
|
670
|
+
runReplayCmd();
|
|
671
|
+
break;
|
|
672
|
+
case 'tips':
|
|
673
|
+
runTipsCmd();
|
|
674
|
+
break;
|
|
675
|
+
case 'tools':
|
|
676
|
+
runToolsCmd();
|
|
677
|
+
break;
|
|
678
|
+
case 'agent':
|
|
679
|
+
runAgentCmd();
|
|
680
|
+
break;
|
|
441
681
|
case 'route':
|
|
442
682
|
runRouteCmd();
|
|
443
683
|
break;
|
|
@@ -486,14 +726,19 @@ async function main() {
|
|
|
486
726
|
case 'reset':
|
|
487
727
|
reset();
|
|
488
728
|
break;
|
|
729
|
+
case 'config':
|
|
730
|
+
runConfigCmd();
|
|
731
|
+
break;
|
|
489
732
|
case 'skills': {
|
|
490
733
|
const sub = args[1];
|
|
491
734
|
if (sub === 'list') skillsList();
|
|
492
735
|
else if (sub === 'add') skillsAdd(args[2]);
|
|
493
736
|
else if (sub === 'remove' || sub === 'rm') skillsRemove(args[2]);
|
|
737
|
+
else if (sub === 'init') runSkillsAuthorCmd('init', args.slice(2));
|
|
738
|
+
else if (sub === 'lint') runSkillsAuthorCmd('lint', args.slice(2));
|
|
494
739
|
else {
|
|
495
740
|
err(`Unknown skills subcommand: ${sub}`);
|
|
496
|
-
log(c.dim(' Try: list, add, remove'));
|
|
741
|
+
log(c.dim(' Try: list, add, remove, init, lint'));
|
|
497
742
|
process.exit(1);
|
|
498
743
|
}
|
|
499
744
|
break;
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
const os = require('os');
|
|
6
|
+
const test = require('node:test');
|
|
7
|
+
const assert = require('node:assert/strict');
|
|
8
|
+
|
|
9
|
+
const {
|
|
10
|
+
parseUserEnvProfileText,
|
|
11
|
+
readUserEnvProfileFromFile,
|
|
12
|
+
} = require('../lib/user-env-profile');
|
|
13
|
+
|
|
14
|
+
test('parse basic KEY=value pairs', () => {
|
|
15
|
+
const { vars, issues } = parseUserEnvProfileText('A=1\nB=two');
|
|
16
|
+
assert.equal(vars.A, '1');
|
|
17
|
+
assert.equal(vars.B, 'two');
|
|
18
|
+
assert.equal(issues.length, 0);
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
test('export prefix and stripping quotes', () => {
|
|
22
|
+
const { vars, issues } = parseUserEnvProfileText(`export Z="quoted"\nQ='single'`);
|
|
23
|
+
assert.equal(vars.Z, 'quoted');
|
|
24
|
+
assert.equal(vars.Q, 'single');
|
|
25
|
+
assert.equal(issues.length, 0);
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
test('duplicate KEY warns last wins', () => {
|
|
29
|
+
const { vars, issues } = parseUserEnvProfileText('A=1\nA=2\n');
|
|
30
|
+
assert.equal(vars.A, '2');
|
|
31
|
+
assert.ok(issues.some(i => i.message.includes('duplicate')));
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
test('invalid KEY name error does not bind', () => {
|
|
35
|
+
const { vars, issues } = parseUserEnvProfileText('1BAD=x');
|
|
36
|
+
assert.equal(vars['1BAD'], undefined);
|
|
37
|
+
assert.ok(issues.some(i => i.level === 'error'));
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
test('bare line warns', () => {
|
|
41
|
+
const { issues } = parseUserEnvProfileText('not-a-pair');
|
|
42
|
+
assert.ok(issues.some(i => i.level === 'warning' && i.message.includes('dotenv')));
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
test('readUserEnvProfileFromFile missing yields missingFile', () => {
|
|
46
|
+
const p = path.join(os.tmpdir(), `sf-env-missing-${Math.random().toString(36).slice(2)}`);
|
|
47
|
+
fs.rmSync(p, { force: true });
|
|
48
|
+
const r = readUserEnvProfileFromFile(p);
|
|
49
|
+
assert.equal(r.missingFile, true);
|
|
50
|
+
assert.deepEqual(r.vars, {});
|
|
51
|
+
assert.deepEqual(r.issues, []);
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
test('readUserEnvProfileFromFile parses file', () => {
|
|
55
|
+
const p = path.join(os.tmpdir(), `sf-env-ok-${Math.random().toString(36).slice(2)}`);
|
|
56
|
+
fs.writeFileSync(p, 'HELLO_CI=world\n', 'utf8');
|
|
57
|
+
try {
|
|
58
|
+
const r = readUserEnvProfileFromFile(p);
|
|
59
|
+
assert.ok(!r.missingFile);
|
|
60
|
+
assert.equal(r.vars.HELLO_CI, 'world');
|
|
61
|
+
assert.equal(r.issues.length, 0);
|
|
62
|
+
} finally {
|
|
63
|
+
fs.unlinkSync(p);
|
|
64
|
+
}
|
|
65
|
+
});
|
package/docs/README.md
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# Skillforge documentation
|
|
2
|
+
|
|
3
|
+
These guides supplement the repo root **[README](../README.md)** (overview and badges). Follow them **in order** the first time you install Skillforge.
|
|
4
|
+
|
|
5
|
+
| Order | Guide | What you learn |
|
|
6
|
+
|:-----:|-------|----------------|
|
|
7
|
+
| 1 | [Getting started](getting-started.md) | Install, MCP wiring preflight, first commands |
|
|
8
|
+
| 2 | [Environment & configuration](environment-and-configuration.md) | Layers: `~/.skillforge/env`, MCP `env`, env variable reference |
|
|
9
|
+
| 3 | [MCP integration](mcp-integration.md) | Router modes (`host`, `auto`, …), JSON snippet, MCP tools summary |
|
|
10
|
+
| 4 | [CLI reference](cli-reference.md) | Every `skillforge` subcommand cluster and parity with MCP |
|
|
11
|
+
| 5 | [Architecture & data](architecture-and-data.md) | Routing pipeline, SQLite layout, policies, project RAG, skills/packs |
|
|
12
|
+
| 6 | [Troubleshooting](troubleshooting.md) | Common failures (tools missing, npm CI, policies JSON) |
|
|
13
|
+
|
|
14
|
+
**Elsewhere:** **Current release (`main`):** **`0.11.7`** — [CHANGELOG](../CHANGELOG.md) · [SECURITY](../SECURITY.md) · [RELEASING](../RELEASING.md) · [CONTRIBUTING](../CONTRIBUTING.md) · [STRATEGY](../STRATEGY.md).
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
# Architecture & data
|
|
2
|
+
|
|
3
|
+
Skillforge stitches three layers:
|
|
4
|
+
|
|
5
|
+
1. **Node bootstrap** (**`bin/cli.js`**) — ensures **`~/.skillforge`**, wires env, forks Python.
|
|
6
|
+
2. **Python orchestration** (**`python/app/main.py`** + satellite modules) — embeddings, **`Router`**, policies, MCP surfaces.
|
|
7
|
+
3. **Filesystem catalog** (**`skills/`**) — bundled SKILL.md corpus + **`~/.skillforge/skills/`** overlays + optional **`pack`** trees.
|
|
8
|
+
|
|
9
|
+
ASCII overview:
|
|
10
|
+
|
|
11
|
+
```
|
|
12
|
+
MCP IDE host ──stdin/stdout JSON-RPC──► skillforge mcp ──► app.mcp_server
|
|
13
|
+
│
|
|
14
|
+
CLI / systemd / CI ──► bin/cli.js ───────────────────────► app.*_cli (+ shared Router)
|
|
15
|
+
│
|
|
16
|
+
SQLite stores per global (~/.skillforge/data) vs project (.skillforge) roots
|
|
17
|
+
sentence-transformers (default MiniLM family) ─ skill card embeddings ─► shortlists
|
|
18
|
+
Optional Anthropic Async client when router modes demand Haiku rerank/final picks
|
|
19
|
+
Standalone OpenAIRouterLLM when transport ≠ MCP AND SKILLFORGE_ROUTER_LLM_BACKEND=openai_compatible
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
### Routing stages (mental model)
|
|
23
|
+
|
|
24
|
+
```
|
|
25
|
+
prompt (+ optional fused conversation overlays)
|
|
26
|
+
→ encode embedding query (skills + hybrid sparse boosts)
|
|
27
|
+
→ merge learned weights / overlay boosts / exclusions (project policies)
|
|
28
|
+
→ top-K candidates
|
|
29
|
+
→ optional LLM rerank + final picks (embedding-only / Haiku / host pick / openai_compatible)
|
|
30
|
+
→ chunk context + optional fusion w/ project_chunks
|
|
31
|
+
→ markdown payload + MCP _meta auditing
|
|
32
|
+
→ SQLite telemetry (sessions, events, weights, optional project_chunks)
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
**Re-route guard:** **`SKILLFORGE_REROUTE_THRESHOLD`** hysteresis when picks swing hard.
|
|
36
|
+
|
|
37
|
+
### Policy + overlay ingestion
|
|
38
|
+
|
|
39
|
+
**`load_route_policies_config`** merges:
|
|
40
|
+
|
|
41
|
+
1. **`SKILLFORGE_ROUTE_POLICIES`** (**inline JSON**) — malformed JSON ⇒ stderr warning · empty rules fallback.
|
|
42
|
+
2. **`SKILLFORGE_ROUTE_POLICIES_FILE`** path.
|
|
43
|
+
3. **`<project>/.skillforge/policies.json`**
|
|
44
|
+
4. **`<project>/skillforge-policies.json`**
|
|
45
|
+
|
|
46
|
+
Policy JSON optionally embeds **`rules`**, **`exclude_skills`** / boosts / **`project_notes`**. Overlay notes deliberately **never** activate without **`project_root`**.
|
|
47
|
+
|
|
48
|
+
### Data directories
|
|
49
|
+
|
|
50
|
+
#### Global (**`~/.skillforge`**)
|
|
51
|
+
|
|
52
|
+
```
|
|
53
|
+
~/.skillforge/
|
|
54
|
+
├── env # Optional dotenv-style operator profile (config commands)
|
|
55
|
+
├── venv/ # Managed Python toolchain
|
|
56
|
+
├── data/orchestrator.db
|
|
57
|
+
├── skills/ # Operator-authored skills
|
|
58
|
+
├── packs/ # Expanded pack artefacts
|
|
59
|
+
├── .setup-complete # Marker after install completes
|
|
60
|
+
└── ...
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
#### Per project (**`<repo>/.skillforge`** once routing/indexing attaches **`project_root`**)
|
|
64
|
+
|
|
65
|
+
```
|
|
66
|
+
<repo>/.skillforge/
|
|
67
|
+
├── orchestrator.db # SQLite (sessions/events/weights/chunks overlay)
|
|
68
|
+
├── policies.json # Optional overlays (alternate: repo-root manifest)
|
|
69
|
+
├── last_route.json # Debugging snapshot from CLI routing
|
|
70
|
+
└── ...
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
### Learning + portability
|
|
74
|
+
|
|
75
|
+
- **`skill_weights`** table mutated by MCP **`route_skills`**, **`skill_feedback`**, etc.
|
|
76
|
+
- Export/import symmetry via **`skillforge weights export|import`** (JSON payloads align with MCP **`weights_snapshot`**).
|
|
77
|
+
|
|
78
|
+
### Project RAG ingestion
|
|
79
|
+
|
|
80
|
+
Steps:
|
|
81
|
+
|
|
82
|
+
```bash
|
|
83
|
+
skillforge index --project-root=/absolute/path/to/repo
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
Then MCP / CLI **`include_project_rag`** toggles fused retrieval guarded by **`project_index.py`** (embedding dimension mismatches skipped defensively).
|
|
87
|
+
|
|
88
|
+
### Bundled skills gate
|
|
89
|
+
|
|
90
|
+
**`ci/bundle-gate.json`** field **`minSkillMdFiles`** is the CI-enforced minimum **`skills/**/**/SKILL.md`** count (see `.github/workflows/ci.yml` → **Verify skills bundle**).
|