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