@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/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.4';
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
- // PreToolUse hook 触发;返回 200 放行 / 412 阻断
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
- return json(res, 412, {
929
- ok: false,
930
- allow: false,
931
- reason: decision.reason,
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 Start local sidecar on 127.0.0.1:${DEFAULT_PORT}
1172
- (falls back to a random port if ${DEFAULT_PORT} is taken)
1173
- skillfm-local stop Stop the running sidecar
1174
- skillfm-local status Print current sidecar state
1175
- skillfm-local help Show this help
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 { runDoctor, renderDoctorReport } = await import('./doctor.js');
1260
- const report = await runDoctor(process.cwd());
1261
- if (process.argv.includes('--json')) {
1262
- console.log(JSON.stringify(report, null, 2));
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(renderDoctorReport(report));
1708
+ console.log(renderDoctorV3Report(report));
1709
+ }
1710
+ // 退出码:任意 fail 项 → 1(disable-hooks 模式只是预览,永远 0)
1711
+ if (disableHooksMode) {
1712
+ process.exit(0);
1266
1713
  }
1267
- const hasFail = report.items.some((i) => i.status === 'fail');
1268
- process.exit(hasFail ? 1 : 0);
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) }));