@skillfm/local 2.0.5 → 2.0.6
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/dist/agent-hints.d.ts.map +1 -1
- package/dist/agent-hints.js +29 -14
- package/dist/agent-hints.js.map +1 -1
- package/dist/doctor.d.ts +26 -0
- package/dist/doctor.d.ts.map +1 -1
- package/dist/doctor.js +184 -0
- package/dist/doctor.js.map +1 -1
- package/dist/guard/bin.js +0 -0
- package/dist/guard/cli.d.ts.map +1 -1
- package/dist/guard/cli.js +26 -0
- package/dist/guard/cli.js.map +1 -1
- package/dist/index.js +467 -19
- package/dist/index.js.map +1 -1
- package/package.json +3 -2
- package/dist/lang.d.ts +0 -21
- package/dist/lang.d.ts.map +0 -1
- package/dist/lang.js +0 -62
- package/dist/lang.js.map +0 -1
package/dist/index.js
CHANGED
|
@@ -17,8 +17,11 @@ import { homedir } from 'node:os';
|
|
|
17
17
|
import { probeSoulFile, buildConsentPayload, writeSkillfmBlock, removeSkillfmBlock, verifySkillfmBlock, markDeclined, hasDeclined, } from './soul.js';
|
|
18
18
|
import { guardState } from './guard/state.js';
|
|
19
19
|
import { agentHint } from './agent-hints.js';
|
|
20
|
+
import { buildElicitationError, buildElicitationId, buildIsErrorResult, buildRejectEnvelope, } from './mcp-output/builder.js';
|
|
21
|
+
import { MCP_OUTPUT_FEATURES, MCP_OUTPUT_VERSION, } from './mcp-output/types.js';
|
|
22
|
+
import { renderNotFoundPage, renderReviewPage, reviewStore, } from './mcp-output/deny-review.js';
|
|
20
23
|
const PKG_NAME = '@skillfm/local';
|
|
21
|
-
const PKG_VERSION = '0.2.
|
|
24
|
+
const PKG_VERSION = '0.2.6';
|
|
22
25
|
// OAuth endpoints live at API root (not under /api/v1), so we keep two base URLs.
|
|
23
26
|
const DEFAULT_API_ROOT = process.env.SKILLFM_API_ROOT || 'https://api.skillfm.ai';
|
|
24
27
|
const DEFAULT_API_BASE_URL = process.env.SKILLFM_API_URL || `${DEFAULT_API_ROOT}/api/v1`;
|
|
@@ -259,6 +262,17 @@ function json(res, status, body) {
|
|
|
259
262
|
});
|
|
260
263
|
res.end(payload);
|
|
261
264
|
}
|
|
265
|
+
function html(res, status, body) {
|
|
266
|
+
res.writeHead(status, {
|
|
267
|
+
'Content-Type': 'text/html; charset=utf-8',
|
|
268
|
+
'Content-Length': Buffer.byteLength(body),
|
|
269
|
+
});
|
|
270
|
+
res.end(body);
|
|
271
|
+
}
|
|
272
|
+
function buildSidecarBaseUrl() {
|
|
273
|
+
const port = parseInt(process.env.SKILLFM_LOCAL_PORT || '19821', 10);
|
|
274
|
+
return `http://127.0.0.1:${port}`;
|
|
275
|
+
}
|
|
262
276
|
const routes = {
|
|
263
277
|
'GET /health': async (_req, res) => {
|
|
264
278
|
json(res, 200, { ok: true, service: PKG_NAME, version: PKG_VERSION });
|
|
@@ -912,12 +926,33 @@ const routes = {
|
|
|
912
926
|
},
|
|
913
927
|
// --------------------------------------------------------------------
|
|
914
928
|
// GET /internal/guard/check?session_id=<id>&tool=<name>&harness=<name>
|
|
915
|
-
//
|
|
929
|
+
// &format=native|mcp-error|mcp-result|mcp-envelope
|
|
930
|
+
// &task_id=<id>&request_id=<id>&redline=<S1..S9>&tier=<A|B|C>
|
|
931
|
+
//
|
|
932
|
+
// PreToolUse hook 触发;返回 200 放行 / 412 阻断(native)
|
|
933
|
+
// 或 200 + MCP-style envelope(mcp-* formats)
|
|
934
|
+
//
|
|
935
|
+
// M9.5 升级:format 参数支持 4 种输出
|
|
936
|
+
// - native (默认,向后兼容):旧 412 + { reason, hint }
|
|
937
|
+
// - mcp-error : URLElicitationRequiredError(-32042) only
|
|
938
|
+
// - mcp-result : { result, isError:true } only
|
|
939
|
+
// - mcp-envelope : 双发({ error, result })
|
|
916
940
|
// --------------------------------------------------------------------
|
|
917
941
|
'GET /internal/guard/check': async (req, res) => {
|
|
918
942
|
const url = new URL(req.url || '/', 'http://x');
|
|
919
943
|
const session_id = url.searchParams.get('session_id') || '';
|
|
920
944
|
const tool = url.searchParams.get('tool') || undefined;
|
|
945
|
+
const format = (url.searchParams.get('format') || 'native');
|
|
946
|
+
const task_id = url.searchParams.get('task_id') || `task-${Date.now().toString(36)}`;
|
|
947
|
+
const request_id_raw = url.searchParams.get('request_id');
|
|
948
|
+
const request_id = request_id_raw
|
|
949
|
+
? /^\d+$/.test(request_id_raw)
|
|
950
|
+
? parseInt(request_id_raw, 10)
|
|
951
|
+
: request_id_raw
|
|
952
|
+
: null;
|
|
953
|
+
const redline = url.searchParams.get('redline') || undefined;
|
|
954
|
+
const tierRaw = url.searchParams.get('tier');
|
|
955
|
+
const tier = tierRaw === 'A' || tierRaw === 'B' || tierRaw === 'C' ? tierRaw : undefined;
|
|
921
956
|
if (!session_id) {
|
|
922
957
|
return json(res, 400, { ok: false, error: 'session_id required' });
|
|
923
958
|
}
|
|
@@ -925,14 +960,76 @@ const routes = {
|
|
|
925
960
|
if (decision.allow) {
|
|
926
961
|
return json(res, 200, { ok: true, allow: true, reason: decision.reason });
|
|
927
962
|
}
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
963
|
+
// 拒绝路径
|
|
964
|
+
if (format === 'native') {
|
|
965
|
+
return json(res, 412, {
|
|
966
|
+
ok: false,
|
|
967
|
+
allow: false,
|
|
968
|
+
reason: decision.reason,
|
|
969
|
+
hint: decision.hint,
|
|
970
|
+
});
|
|
971
|
+
}
|
|
972
|
+
const rejectInput = {
|
|
973
|
+
task_id,
|
|
974
|
+
reason: decision.reason || 'guard 校验未通过',
|
|
932
975
|
hint: decision.hint,
|
|
976
|
+
tool,
|
|
977
|
+
tier,
|
|
978
|
+
redline,
|
|
979
|
+
sidecarBaseUrl: buildSidecarBaseUrl(),
|
|
980
|
+
requestId: request_id,
|
|
981
|
+
};
|
|
982
|
+
// 提前生成 elicitation_id,登记到 reviewStore,让 envelope 与 store index 配对
|
|
983
|
+
const elicitation_id = buildElicitationId(task_id);
|
|
984
|
+
reviewStore.open(rejectInput, elicitation_id);
|
|
985
|
+
if (format === 'mcp-error') {
|
|
986
|
+
return json(res, 200, buildElicitationError(rejectInput, elicitation_id));
|
|
987
|
+
}
|
|
988
|
+
if (format === 'mcp-result') {
|
|
989
|
+
return json(res, 200, buildIsErrorResult(rejectInput));
|
|
990
|
+
}
|
|
991
|
+
// mcp-envelope (双发)
|
|
992
|
+
return json(res, 200, buildRejectEnvelope(rejectInput, elicitation_id));
|
|
993
|
+
},
|
|
994
|
+
// --------------------------------------------------------------------
|
|
995
|
+
// GET /internal/mcp/probe
|
|
996
|
+
// doctor 用:暴露 sidecar MCP 输出层版本和能力
|
|
997
|
+
// --------------------------------------------------------------------
|
|
998
|
+
'GET /internal/mcp/probe': async (_req, res) => {
|
|
999
|
+
return json(res, 200, {
|
|
1000
|
+
ok: true,
|
|
1001
|
+
output_version: MCP_OUTPUT_VERSION,
|
|
1002
|
+
features: MCP_OUTPUT_FEATURES,
|
|
1003
|
+
formats: ['native', 'mcp-error', 'mcp-result', 'mcp-envelope'],
|
|
933
1004
|
});
|
|
934
1005
|
},
|
|
935
1006
|
// --------------------------------------------------------------------
|
|
1007
|
+
// GET /internal/mcp/reviews
|
|
1008
|
+
// debug:列出当前 pending / decided 的审阅记录
|
|
1009
|
+
// --------------------------------------------------------------------
|
|
1010
|
+
'GET /internal/mcp/reviews': async (_req, res) => {
|
|
1011
|
+
return json(res, 200, { ok: true, reviews: reviewStore.list() });
|
|
1012
|
+
},
|
|
1013
|
+
// --------------------------------------------------------------------
|
|
1014
|
+
// GET /internal/mcp/elicitation/poll?elicitationId=<id>
|
|
1015
|
+
// M9.5 L2-4: SkillFM MCP bridge 用来 polling 等审阅完成。
|
|
1016
|
+
// 返回 { status: 'pending' | 'complete' | 'not-found', payload?, decision? }
|
|
1017
|
+
// --------------------------------------------------------------------
|
|
1018
|
+
'GET /internal/mcp/elicitation/poll': async (req, res) => {
|
|
1019
|
+
const url = new URL(req.url || '/', 'http://x');
|
|
1020
|
+
const eid = url.searchParams.get('elicitationId') || '';
|
|
1021
|
+
if (!eid)
|
|
1022
|
+
return json(res, 400, { ok: false, error: 'elicitationId required' });
|
|
1023
|
+
const rec = reviewStore.getByElicitationId(eid);
|
|
1024
|
+
if (!rec)
|
|
1025
|
+
return json(res, 200, { status: 'not-found' });
|
|
1026
|
+
if (rec.decision === 'pending')
|
|
1027
|
+
return json(res, 200, { status: 'pending' });
|
|
1028
|
+
const { buildElicitationCompletePayload } = await import('./mcp-output/deny-review.js');
|
|
1029
|
+
const payload = buildElicitationCompletePayload(rec);
|
|
1030
|
+
return json(res, 200, { status: 'complete', decision: rec.decision, payload });
|
|
1031
|
+
},
|
|
1032
|
+
// --------------------------------------------------------------------
|
|
936
1033
|
// POST /internal/guard/post-tool-use
|
|
937
1034
|
// body: { session_id, tool?, outcome? }
|
|
938
1035
|
// PostToolUse hook;仅 audit,warn-only,永远 200。
|
|
@@ -967,6 +1064,99 @@ async function handleRequest(req, res) {
|
|
|
967
1064
|
}
|
|
968
1065
|
const method = req.method || 'GET';
|
|
969
1066
|
const url = (req.url || '/').split('?')[0];
|
|
1067
|
+
// ---- SSE: /internal/mcp/elicitation/stream?elicitationId=<id> ----
|
|
1068
|
+
if (url === '/internal/mcp/elicitation/stream' && method === 'GET') {
|
|
1069
|
+
const u = new URL(req.url || '/', 'http://x');
|
|
1070
|
+
const eid = u.searchParams.get('elicitationId') || '';
|
|
1071
|
+
if (!eid) {
|
|
1072
|
+
res.writeHead(400);
|
|
1073
|
+
res.end('elicitationId required');
|
|
1074
|
+
return;
|
|
1075
|
+
}
|
|
1076
|
+
res.writeHead(200, {
|
|
1077
|
+
'Content-Type': 'text/event-stream',
|
|
1078
|
+
'Cache-Control': 'no-cache, no-transform',
|
|
1079
|
+
Connection: 'keep-alive',
|
|
1080
|
+
});
|
|
1081
|
+
// 已 decided:立即 push + 关闭
|
|
1082
|
+
const existing = reviewStore.getByElicitationId(eid);
|
|
1083
|
+
if (existing && existing.decision !== 'pending') {
|
|
1084
|
+
const { buildElicitationCompletePayload } = await import('./mcp-output/deny-review.js');
|
|
1085
|
+
const payload = buildElicitationCompletePayload(existing);
|
|
1086
|
+
res.write(`event: elicitation-complete\n`);
|
|
1087
|
+
res.write(`data: ${JSON.stringify(payload)}\n\n`);
|
|
1088
|
+
res.end();
|
|
1089
|
+
return;
|
|
1090
|
+
}
|
|
1091
|
+
// 订阅 — 每 15s 发 keepalive 注释,事件来了发完即关
|
|
1092
|
+
const onComplete = (payload) => {
|
|
1093
|
+
try {
|
|
1094
|
+
res.write(`event: elicitation-complete\n`);
|
|
1095
|
+
res.write(`data: ${JSON.stringify(payload.payload)}\n\n`);
|
|
1096
|
+
}
|
|
1097
|
+
catch {
|
|
1098
|
+
// ignore
|
|
1099
|
+
}
|
|
1100
|
+
finally {
|
|
1101
|
+
try {
|
|
1102
|
+
res.end();
|
|
1103
|
+
}
|
|
1104
|
+
catch {
|
|
1105
|
+
// ignore
|
|
1106
|
+
}
|
|
1107
|
+
clearInterval(keepAlive);
|
|
1108
|
+
reviewStore.off(`elicitation:${eid}`, onComplete);
|
|
1109
|
+
}
|
|
1110
|
+
};
|
|
1111
|
+
const keepAlive = setInterval(() => {
|
|
1112
|
+
try {
|
|
1113
|
+
res.write(`: keepalive\n\n`);
|
|
1114
|
+
}
|
|
1115
|
+
catch {
|
|
1116
|
+
clearInterval(keepAlive);
|
|
1117
|
+
reviewStore.off(`elicitation:${eid}`, onComplete);
|
|
1118
|
+
}
|
|
1119
|
+
}, 15_000);
|
|
1120
|
+
reviewStore.on(`elicitation:${eid}`, onComplete);
|
|
1121
|
+
req.on('close', () => {
|
|
1122
|
+
clearInterval(keepAlive);
|
|
1123
|
+
reviewStore.off(`elicitation:${eid}`, onComplete);
|
|
1124
|
+
});
|
|
1125
|
+
return;
|
|
1126
|
+
}
|
|
1127
|
+
// ---- prefix route: /deny-review/:task_id [/approve|/cancel|/modify] ----
|
|
1128
|
+
if (url.startsWith('/deny-review/')) {
|
|
1129
|
+
const tail = url.slice('/deny-review/'.length);
|
|
1130
|
+
const parts = tail.split('/').filter(Boolean);
|
|
1131
|
+
const task_id = decodeURIComponent(parts[0] || '');
|
|
1132
|
+
const action = parts[1] || ''; // '' | 'approve' | 'cancel' | 'modify'
|
|
1133
|
+
if (!task_id) {
|
|
1134
|
+
res.writeHead(400);
|
|
1135
|
+
res.end('task_id required');
|
|
1136
|
+
return;
|
|
1137
|
+
}
|
|
1138
|
+
if (method === 'GET' && !action) {
|
|
1139
|
+
const rec = reviewStore.get(task_id);
|
|
1140
|
+
if (!rec)
|
|
1141
|
+
return html(res, 404, renderNotFoundPage(task_id));
|
|
1142
|
+
return html(res, 200, renderReviewPage(rec));
|
|
1143
|
+
}
|
|
1144
|
+
if (method === 'POST' && (action === 'approve' || action === 'cancel' || action === 'modify')) {
|
|
1145
|
+
const decision = action === 'approve'
|
|
1146
|
+
? 'approved'
|
|
1147
|
+
: action === 'cancel'
|
|
1148
|
+
? 'cancelled'
|
|
1149
|
+
: 'modified';
|
|
1150
|
+
const rec = reviewStore.decide(task_id, decision);
|
|
1151
|
+
if (!rec) {
|
|
1152
|
+
return json(res, 404, { ok: false, error: 'task_not_found', task_id });
|
|
1153
|
+
}
|
|
1154
|
+
return json(res, 200, { ok: true, decision: rec.decision, task_id });
|
|
1155
|
+
}
|
|
1156
|
+
res.writeHead(405);
|
|
1157
|
+
res.end('method not allowed');
|
|
1158
|
+
return;
|
|
1159
|
+
}
|
|
970
1160
|
const key = `${method} ${url}`;
|
|
971
1161
|
const handler = routes[key];
|
|
972
1162
|
if (!handler) {
|
|
@@ -1168,11 +1358,22 @@ function cmdHelp() {
|
|
|
1168
1358
|
console.log(`${PKG_NAME} v${PKG_VERSION}
|
|
1169
1359
|
|
|
1170
1360
|
Usage:
|
|
1171
|
-
skillfm-local start
|
|
1172
|
-
|
|
1173
|
-
skillfm-local stop
|
|
1174
|
-
skillfm-local status
|
|
1175
|
-
skillfm-local
|
|
1361
|
+
skillfm-local start Start local sidecar on 127.0.0.1:${DEFAULT_PORT}
|
|
1362
|
+
(falls back to a random port if ${DEFAULT_PORT} is taken)
|
|
1363
|
+
skillfm-local stop Stop the running sidecar
|
|
1364
|
+
skillfm-local status Print current sidecar state
|
|
1365
|
+
skillfm-local init [--v3] [--host=<id>]
|
|
1366
|
+
Project-level init: write SKILL.md priming + harness
|
|
1367
|
+
PreToolUse hook (Layer 1 + Layer 3). --v3 = three-layer
|
|
1368
|
+
architecture (recommended); omit for v2 flat (legacy).
|
|
1369
|
+
skillfm-local bootstrap [--host=<id>] [--json]
|
|
1370
|
+
⭐ One-line full setup: verify sidecar + activation,
|
|
1371
|
+
then run init --v3 in current cwd. Recommended for
|
|
1372
|
+
first-time agents after activation.
|
|
1373
|
+
skillfm-local doctor [--v2] [--disable-hooks] [--json]
|
|
1374
|
+
Three-layer self-check (Layer 1 SKILL.md / Layer 2 MCP
|
|
1375
|
+
reject / Layer 3 hook), or v0.2 layered defense check.
|
|
1376
|
+
skillfm-local help Show this help
|
|
1176
1377
|
|
|
1177
1378
|
Service discovery:
|
|
1178
1379
|
${LOCAL_SETTINGS_FILE} JSON with {url, pid, started_at}
|
|
@@ -1205,6 +1406,9 @@ const main = async () => {
|
|
|
1205
1406
|
case 'init':
|
|
1206
1407
|
await cmdInit();
|
|
1207
1408
|
break;
|
|
1409
|
+
case 'bootstrap':
|
|
1410
|
+
await cmdBootstrap();
|
|
1411
|
+
break;
|
|
1208
1412
|
case 'doctor':
|
|
1209
1413
|
await cmdDoctor();
|
|
1210
1414
|
break;
|
|
@@ -1225,6 +1429,8 @@ const main = async () => {
|
|
|
1225
1429
|
async function cmdInit() {
|
|
1226
1430
|
const { detectHarness } = await import('./harness/detector.js');
|
|
1227
1431
|
const { initHarness } = await import('./harness/writers.js');
|
|
1432
|
+
const { detectPrimaryHost, installAll } = await import('./harness/kernels/registry.js');
|
|
1433
|
+
const { writeProjectSkillMd, writeUserScopeSkillMd } = await import('./skill-md/writer.js');
|
|
1228
1434
|
const args = process.argv.slice(3);
|
|
1229
1435
|
const flags = {};
|
|
1230
1436
|
for (let i = 0; i < args.length; i += 1) {
|
|
@@ -1247,25 +1453,267 @@ async function cmdInit() {
|
|
|
1247
1453
|
const detected = detectHarness({});
|
|
1248
1454
|
const harness = (typeof flags.harness === 'string' && flags.harness) || detected.harness;
|
|
1249
1455
|
const lang = (typeof flags.lang === 'string' && flags.lang) || 'auto';
|
|
1456
|
+
const apply = flags['dry-run'] !== true;
|
|
1457
|
+
const v3 = flags['v3'] === true || flags['kernels'] === true;
|
|
1458
|
+
// ---- v3 路径(推荐):3 层抽象 — Layer 1 SKILL.md + Layer 3 kernel registry
|
|
1459
|
+
if (v3) {
|
|
1460
|
+
const host = typeof flags.host === 'string'
|
|
1461
|
+
? { host: flags.host, source: 'forced', reason: 'user --host flag' }
|
|
1462
|
+
: detectPrimaryHost();
|
|
1463
|
+
// Layer 1: SKILL.md
|
|
1464
|
+
const skillMd = writeProjectSkillMd({ cwd: process.cwd(), lang });
|
|
1465
|
+
let userSkillMd = null;
|
|
1466
|
+
if (host.host === 'claude-code' && flags['copy-to-user'] === true) {
|
|
1467
|
+
userSkillMd = writeUserScopeSkillMd(lang);
|
|
1468
|
+
}
|
|
1469
|
+
// Layer 3: kernel adapters
|
|
1470
|
+
const layer3 = await installAll({
|
|
1471
|
+
host,
|
|
1472
|
+
cwd: process.cwd(),
|
|
1473
|
+
apply,
|
|
1474
|
+
forceKernel: typeof flags.kernel === 'string' ? flags.kernel : undefined,
|
|
1475
|
+
alsoInstallMcpHosts: typeof flags['also-mcp-hosts'] === 'string'
|
|
1476
|
+
? flags['also-mcp-hosts'].split(',').map((s) => s.trim()).filter(Boolean)
|
|
1477
|
+
: undefined,
|
|
1478
|
+
});
|
|
1479
|
+
if (flags['json'] === true) {
|
|
1480
|
+
console.log(JSON.stringify({
|
|
1481
|
+
ok: true,
|
|
1482
|
+
architecture: 'v3-three-layer',
|
|
1483
|
+
detect: detected,
|
|
1484
|
+
host,
|
|
1485
|
+
layer1_skill_md: { project: skillMd, user: userSkillMd },
|
|
1486
|
+
layer3_kernels: layer3,
|
|
1487
|
+
}, null, 2));
|
|
1488
|
+
return;
|
|
1489
|
+
}
|
|
1490
|
+
// 人类可读输出
|
|
1491
|
+
const lines = [];
|
|
1492
|
+
lines.push(`✓ 检测 host: ${host.host}${host.source !== 'detected' ? ` (${host.source})` : ''}`);
|
|
1493
|
+
if (host.reason)
|
|
1494
|
+
lines.push(` reason: ${host.reason}`);
|
|
1495
|
+
lines.push('');
|
|
1496
|
+
// Layer 1
|
|
1497
|
+
const skillBadge = skillMd.action === 'created'
|
|
1498
|
+
? '已创建'
|
|
1499
|
+
: skillMd.action === 'replaced'
|
|
1500
|
+
? '已升级'
|
|
1501
|
+
: skillMd.action === 'appended'
|
|
1502
|
+
? '已追加'
|
|
1503
|
+
: '已就绪';
|
|
1504
|
+
lines.push(`✓ Layer 1: SKILL.md (${skillBadge}) — ${skillMd.path}`);
|
|
1505
|
+
if (userSkillMd) {
|
|
1506
|
+
lines.push(`✓ Layer 1: 用户级 SKILL.md (${userSkillMd.action}) — ${userSkillMd.path}`);
|
|
1507
|
+
}
|
|
1508
|
+
lines.push('');
|
|
1509
|
+
// Layer 2 — 提示用户 sidecar MCP reject 是否在跑(可选探测)
|
|
1510
|
+
lines.push(`ℹ Layer 2: MCP Protocol-Reject 由 sidecar 输出层提供`);
|
|
1511
|
+
lines.push(` → 检查 \`skillfm doctor\` Layer 2 是否 ✅ (output_version=v2)`);
|
|
1512
|
+
lines.push('');
|
|
1513
|
+
// Layer 3
|
|
1514
|
+
if (layer3.matched.length === 0) {
|
|
1515
|
+
lines.push(`⚠ Layer 3: 当前 host=${host.host} 未匹配任何 kernel`);
|
|
1516
|
+
lines.push(` → 仅 Layer 1+2 生效;如需 Layer 3 强制,加 --kernel=<hook-file|deny-pipeline|mcp-only>`);
|
|
1517
|
+
}
|
|
1518
|
+
else {
|
|
1519
|
+
for (const result of layer3.results) {
|
|
1520
|
+
const fileSummary = result.files
|
|
1521
|
+
.map((f) => `${f.action} ${f.path}`)
|
|
1522
|
+
.join(', ');
|
|
1523
|
+
lines.push(`✓ Layer 3 [${result.kernel}] host=${result.host}: ${fileSummary || '(无文件改动)'}`);
|
|
1524
|
+
for (const w of result.warnings)
|
|
1525
|
+
lines.push(` ⚠ ${w}`);
|
|
1526
|
+
}
|
|
1527
|
+
}
|
|
1528
|
+
lines.push('');
|
|
1529
|
+
// 顶层 warnings
|
|
1530
|
+
if (layer3.warnings.length > 0) {
|
|
1531
|
+
lines.push('⚠ 整体提示:');
|
|
1532
|
+
for (const w of layer3.warnings)
|
|
1533
|
+
lines.push(` - ${w}`);
|
|
1534
|
+
lines.push('');
|
|
1535
|
+
}
|
|
1536
|
+
// mcp-only 主动联动提示
|
|
1537
|
+
if (layer3.matched.includes('mcp-only')) {
|
|
1538
|
+
lines.push('🔗 mcp-only 内核强制力依赖 Layer 2:');
|
|
1539
|
+
lines.push(' - 确保 sidecar 在跑(skillfm start)');
|
|
1540
|
+
lines.push(' - 确保 sidecar 输出层 ≥ v2(URLElicitationRequiredError)');
|
|
1541
|
+
lines.push(' - 跑 `skillfm doctor` 验证三层全部就绪');
|
|
1542
|
+
lines.push('');
|
|
1543
|
+
}
|
|
1544
|
+
lines.push(`下一步:`);
|
|
1545
|
+
lines.push(` - skillfm start # 启 sidecar (如未启)`);
|
|
1546
|
+
lines.push(` - skillfm doctor # 三层架构自检`);
|
|
1547
|
+
lines.push(` - 在 ${host.host} 内开新 session 验证 priming + hook 生效`);
|
|
1548
|
+
console.log(lines.join('\n'));
|
|
1549
|
+
return;
|
|
1550
|
+
}
|
|
1551
|
+
// ---- v2 路径(兼容):单 harness × 单模板
|
|
1250
1552
|
const report = initHarness({
|
|
1251
1553
|
harness,
|
|
1252
1554
|
cwd: process.cwd(),
|
|
1253
1555
|
lang,
|
|
1254
1556
|
llmOnly: flags['llm-only'] === true,
|
|
1255
1557
|
});
|
|
1256
|
-
console.log(JSON.stringify({ ok: true, detect: detected, report }, null, 2));
|
|
1558
|
+
console.log(JSON.stringify({ ok: true, architecture: 'v2-flat', detect: detected, report }, null, 2));
|
|
1559
|
+
}
|
|
1560
|
+
// ============================================================================
|
|
1561
|
+
// cmdBootstrap — 一行命令完整安装:start + activate (auto) + init --v3
|
|
1562
|
+
// ============================================================================
|
|
1563
|
+
/**
|
|
1564
|
+
* 解决"激活后只写 2 个文件"的 UX 缺陷。
|
|
1565
|
+
*
|
|
1566
|
+
* 流程:
|
|
1567
|
+
* 1. 检测 sidecar 是否在跑;不在跑则启
|
|
1568
|
+
* 2. 检测是否已激活(~/.skillfm/config.json brain_key);未激活则提示用户走 OAuth
|
|
1569
|
+
* (bootstrap 不能自动完成 OAuth — 用户必须在浏览器手动授权)
|
|
1570
|
+
* 3. 已激活后立即触发 init --v3 在 cwd 写 SKILL.md + harness hook + MCP 注册
|
|
1571
|
+
*
|
|
1572
|
+
* 与 cmdStart 区别:start 只起 sidecar;bootstrap 是 start + 强引导激活 + 项目级 init。
|
|
1573
|
+
*
|
|
1574
|
+
* 使用:
|
|
1575
|
+
* npx -y -p @skillfm/local skillfm-local bootstrap
|
|
1576
|
+
* npx -y -p @skillfm/local skillfm-local bootstrap --host=cursor
|
|
1577
|
+
* npx -y -p @skillfm/local skillfm-local bootstrap --json
|
|
1578
|
+
*/
|
|
1579
|
+
async function cmdBootstrap() {
|
|
1580
|
+
const args = process.argv.slice(3);
|
|
1581
|
+
const flags = {};
|
|
1582
|
+
for (let i = 0; i < args.length; i += 1) {
|
|
1583
|
+
const t = args[i];
|
|
1584
|
+
if (t.startsWith('--')) {
|
|
1585
|
+
const eq = t.indexOf('=');
|
|
1586
|
+
if (eq > 0)
|
|
1587
|
+
flags[t.slice(2, eq)] = t.slice(eq + 1);
|
|
1588
|
+
else {
|
|
1589
|
+
const nxt = args[i + 1];
|
|
1590
|
+
if (nxt !== undefined && !nxt.startsWith('--')) {
|
|
1591
|
+
flags[t.slice(2)] = nxt;
|
|
1592
|
+
i += 1;
|
|
1593
|
+
}
|
|
1594
|
+
else
|
|
1595
|
+
flags[t.slice(2)] = true;
|
|
1596
|
+
}
|
|
1597
|
+
}
|
|
1598
|
+
}
|
|
1599
|
+
const asJson = flags['json'] === true;
|
|
1600
|
+
const lines = [];
|
|
1601
|
+
const log = (msg) => {
|
|
1602
|
+
if (!asJson)
|
|
1603
|
+
console.log(msg);
|
|
1604
|
+
lines.push(msg);
|
|
1605
|
+
};
|
|
1606
|
+
// 1. sidecar 状态
|
|
1607
|
+
const existing = readLocalSettings();
|
|
1608
|
+
const sidecarRunning = existing && isPidAlive(existing.pid);
|
|
1609
|
+
if (sidecarRunning) {
|
|
1610
|
+
log(`✓ sidecar 已在 ${existing.url} 运行 (PID ${existing.pid})`);
|
|
1611
|
+
}
|
|
1612
|
+
else {
|
|
1613
|
+
log(`⚠ sidecar 未在跑,请先启动:npx -y -p @skillfm/local skillfm-local start`);
|
|
1614
|
+
log(` bootstrap 不会自动启 sidecar(避免子进程孤儿化);请单独跑 start 后再 bootstrap。`);
|
|
1615
|
+
if (asJson) {
|
|
1616
|
+
console.log(JSON.stringify({ ok: false, reason: 'sidecar_not_running', steps: lines }, null, 2));
|
|
1617
|
+
}
|
|
1618
|
+
process.exit(1);
|
|
1619
|
+
}
|
|
1620
|
+
// 2. 激活状态
|
|
1621
|
+
const cfg = readConfig();
|
|
1622
|
+
const activated = Boolean(cfg.agentToken);
|
|
1623
|
+
if (!activated) {
|
|
1624
|
+
log(`⚠ 尚未激活 — brain_key 未保存。`);
|
|
1625
|
+
log(` 请走 OAuth 流程:`);
|
|
1626
|
+
log(` curl -sS -X POST ${existing.url}/activate/start`);
|
|
1627
|
+
log(` 用户在浏览器完成授权后,每 5s 轮询:`);
|
|
1628
|
+
log(` curl -sS -X POST ${existing.url}/activate/poll`);
|
|
1629
|
+
log(` 激活成功后再次跑 bootstrap 完成项目级 init。`);
|
|
1630
|
+
if (asJson) {
|
|
1631
|
+
console.log(JSON.stringify({ ok: false, reason: 'not_activated', steps: lines }, null, 2));
|
|
1632
|
+
}
|
|
1633
|
+
process.exit(1);
|
|
1634
|
+
}
|
|
1635
|
+
log(`✓ 已激活 (brain_key 长度 ${cfg.agentToken.length} chars)`);
|
|
1636
|
+
// 3. 项目级 init --v3
|
|
1637
|
+
log(``);
|
|
1638
|
+
log(`正在执行 init --v3 完成项目级三层防御注入...`);
|
|
1639
|
+
// 直接复用 cmdInit 内 v3 路径的实现(detectPrimaryHost + writeProjectSkillMd + installAll)
|
|
1640
|
+
const { detectPrimaryHost, installAll } = await import('./harness/kernels/registry.js');
|
|
1641
|
+
const { writeProjectSkillMd } = await import('./skill-md/writer.js');
|
|
1642
|
+
const host = typeof flags.host === 'string'
|
|
1643
|
+
? { host: flags.host, source: 'forced', reason: 'bootstrap --host flag' }
|
|
1644
|
+
: detectPrimaryHost();
|
|
1645
|
+
const lang = (typeof flags.lang === 'string' && flags.lang) || 'auto';
|
|
1646
|
+
const skillMd = writeProjectSkillMd({ cwd: process.cwd(), lang });
|
|
1647
|
+
log(`✓ Layer 1 SKILL.md (${skillMd.action}) — ${skillMd.path}`);
|
|
1648
|
+
const layer3 = await installAll({
|
|
1649
|
+
host,
|
|
1650
|
+
cwd: process.cwd(),
|
|
1651
|
+
apply: true,
|
|
1652
|
+
forceKernel: typeof flags.kernel === 'string' ? flags.kernel : undefined,
|
|
1653
|
+
});
|
|
1654
|
+
if (layer3.matched.length === 0) {
|
|
1655
|
+
log(`⚠ Layer 3: host=${host.host} 未匹配任何 kernel;仅 Layer 1+2 生效`);
|
|
1656
|
+
}
|
|
1657
|
+
else {
|
|
1658
|
+
for (const result of layer3.results) {
|
|
1659
|
+
const fileSummary = result.files.map((f) => `${f.action} ${f.path}`).join(', ');
|
|
1660
|
+
log(`✓ Layer 3 [${result.kernel}] host=${result.host}: ${fileSummary || '(无文件改动)'}`);
|
|
1661
|
+
}
|
|
1662
|
+
}
|
|
1663
|
+
log(``);
|
|
1664
|
+
log(`✅ Bootstrap 完成。三层防御已就绪:`);
|
|
1665
|
+
log(` Layer 1 SKILL.md priming → ./SKILL.md`);
|
|
1666
|
+
log(` Layer 2 MCP Protocol-Reject → sidecar 输出层 v2`);
|
|
1667
|
+
log(` Layer 3 ${layer3.matched.join('+') || '(unknown host, layer 1+2 only)'}`);
|
|
1668
|
+
log(``);
|
|
1669
|
+
log(`下一步:跑 \`skillfm doctor\` 自检三层全部就绪`);
|
|
1670
|
+
if (asJson) {
|
|
1671
|
+
console.log(JSON.stringify({
|
|
1672
|
+
ok: true,
|
|
1673
|
+
architecture: 'v3-three-layer',
|
|
1674
|
+
sidecar: { url: existing.url, pid: existing.pid },
|
|
1675
|
+
activated: true,
|
|
1676
|
+
host,
|
|
1677
|
+
layer1_skill_md: skillMd,
|
|
1678
|
+
layer3_kernels: layer3,
|
|
1679
|
+
steps: lines,
|
|
1680
|
+
}, null, 2));
|
|
1681
|
+
}
|
|
1257
1682
|
}
|
|
1258
1683
|
async function cmdDoctor() {
|
|
1259
|
-
const
|
|
1260
|
-
const
|
|
1261
|
-
|
|
1262
|
-
|
|
1684
|
+
const flags = process.argv.slice(3);
|
|
1685
|
+
const useV2 = flags.includes('--v2');
|
|
1686
|
+
const asJson = flags.includes('--json');
|
|
1687
|
+
const disableHooksMode = flags.includes('--disable-hooks');
|
|
1688
|
+
if (useV2) {
|
|
1689
|
+
const { runDoctor, renderDoctorReport } = await import('./doctor.js');
|
|
1690
|
+
const report = await runDoctor(process.cwd());
|
|
1691
|
+
if (asJson)
|
|
1692
|
+
console.log(JSON.stringify(report, null, 2));
|
|
1693
|
+
else
|
|
1694
|
+
console.log(renderDoctorReport(report));
|
|
1695
|
+
const hasFail = report.items.some((i) => i.status === 'fail');
|
|
1696
|
+
process.exit(hasFail ? 1 : 0);
|
|
1697
|
+
}
|
|
1698
|
+
// v3 默认(三层抽象架构)
|
|
1699
|
+
const { runDoctorV3, renderDoctorV3Report, renderDisableHooksReport } = await import('./doctor.js');
|
|
1700
|
+
const report = await runDoctorV3(process.cwd());
|
|
1701
|
+
if (asJson) {
|
|
1702
|
+
console.log(JSON.stringify(disableHooksMode ? { ...report, mode: 'disable-hooks-preview' } : report, null, 2));
|
|
1703
|
+
}
|
|
1704
|
+
else if (disableHooksMode) {
|
|
1705
|
+
console.log(renderDisableHooksReport(report));
|
|
1263
1706
|
}
|
|
1264
1707
|
else {
|
|
1265
|
-
console.log(
|
|
1708
|
+
console.log(renderDoctorV3Report(report));
|
|
1709
|
+
}
|
|
1710
|
+
// 退出码:任意 fail 项 → 1(disable-hooks 模式只是预览,永远 0)
|
|
1711
|
+
if (disableHooksMode) {
|
|
1712
|
+
process.exit(0);
|
|
1266
1713
|
}
|
|
1267
|
-
const
|
|
1268
|
-
|
|
1714
|
+
const layer1HasFail = report.layer1.items.some((i) => i.status === 'fail');
|
|
1715
|
+
const layer2Fail = report.layer2.diagnose.level === 'absent';
|
|
1716
|
+
process.exit(layer1HasFail || layer2Fail ? 1 : 0);
|
|
1269
1717
|
}
|
|
1270
1718
|
main().catch((e) => {
|
|
1271
1719
|
console.error(JSON.stringify({ ok: false, error: e?.message || String(e) }));
|