psyclaw 0.30.15 → 0.30.17

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.
@@ -31,10 +31,11 @@ import { readPublishedVersions } from "../workflows/publish.js";
31
31
  export { verifyDoi } from "../core/doi.js";
32
32
  import { PSYCLAW_IDENTITY_PROMPT } from "../branding.js";
33
33
  import { sessionHelpDocument } from "../session/help.js";
34
- import { recommendedSkillTarget } from "../skills/recommended.js";
34
+ import { normalizeRecommendedSkillId, readMergedRecommendationState, readRecommendationState as readCanonicalRecommendationState, recommendedSkillTarget, saveRecommendationState, validateModelInstalledRecommendedSkill, } from "../skills/recommended.js";
35
35
  import { buildRecommendedSkillInstallTask } from "../skills/install-guidance.js";
36
36
  import { readUserSkillState, scanLocalSkills, setLocalSkillEnabled, userSkillId, } from "../skills/user-skills.js";
37
- import { loadVerifyChecklist, markVerifyItem, skipUnverifiedItems } from "../verify/checklist.js";
37
+ import { RuntimeMcpRegistry } from "../integrations/mcp-runtime.js";
38
+ import { defaultVerifyChecklist, loadVerifyChecklist, markVerifyItem, skipUnverifiedItems } from "../verify/checklist.js";
38
39
  import { readActiveAnalysisPlan } from "../analysis/plan.js";
39
40
  import { browserConfigForPreference, injectBrowserObservabilityConfig } from "../observability/config.js";
40
41
  import { readTelemetryPreference, telemetryPreferenceOptions, writeTelemetryPreference } from "../observability/preference.js";
@@ -53,6 +54,61 @@ async function readRecommendationState(root) {
53
54
  async function writeRecommendationState(root, state) {
54
55
  await atomicWriteFile(await assertSafeProjectPath(root, ".psyclaw/recommendations.json"), `${JSON.stringify({ ...state, skills: [...new Set(state.skills)].sort(), mcp: [...new Set(state.mcp)].sort() }, null, 2)}\n`);
55
56
  }
57
+ /** Resolve Skill installed/enabled the same way the TUI /skill manager does. */
58
+ async function panelRecommendedSkillStatuses(root) {
59
+ const state = await readMergedRecommendationState(root);
60
+ const catalog = await recommendedSkills();
61
+ return Promise.all((catalog.items ?? []).map(async (item) => {
62
+ const id = normalizeRecommendedSkillId(String(item.id ?? ""));
63
+ const name = typeof item.name === "string" ? item.name : id;
64
+ const scope = state.skillScopes?.[id] ?? "project";
65
+ const desired = state.skills.includes(id);
66
+ try {
67
+ await validateModelInstalledRecommendedSkill(root, id, scope);
68
+ return { id, name, installed: true, enabled: desired, scope };
69
+ }
70
+ catch {
71
+ let onDisk = false;
72
+ try {
73
+ const target = recommendedSkillTarget(root, id, scope);
74
+ const stat = await lstat(target);
75
+ onDisk = stat.isDirectory();
76
+ }
77
+ catch { /* missing */ }
78
+ if (!onDisk && scope === "project") {
79
+ try {
80
+ const userTarget = recommendedSkillTarget(root, id, "user");
81
+ const stat = await lstat(userTarget);
82
+ if (stat.isDirectory()) {
83
+ return { id, name, installed: true, enabled: desired, scope: "user" };
84
+ }
85
+ }
86
+ catch { /* missing */ }
87
+ }
88
+ return { id, name, installed: onDisk, enabled: onDisk && desired, scope };
89
+ }
90
+ }));
91
+ }
92
+ /** Resolve MCP installed/enabled from config files + recommendation desire. */
93
+ async function panelRecommendedMcpStatuses(root) {
94
+ const state = await readMergedRecommendationState(root);
95
+ const catalog = await recommendedMcps();
96
+ const configured = await new RuntimeMcpRegistry().listUserConfigs(root);
97
+ const byId = new Map();
98
+ for (const entry of configured) {
99
+ const previous = byId.get(entry.id);
100
+ if (!previous || entry.enabled)
101
+ byId.set(entry.id, { enabled: entry.enabled });
102
+ }
103
+ return (catalog.items ?? []).map((item) => {
104
+ const id = String(item.id ?? "");
105
+ const name = typeof item.name === "string" ? item.name : id;
106
+ const installed = byId.has(id);
107
+ const desired = state.mcp.includes(id);
108
+ // Match TUI recommended rows: installed = config present; enabled = desire.
109
+ return { id, name, installed, enabled: installed && desired };
110
+ });
111
+ }
56
112
  /** Bundled capabilities, listed separately from the downloadable ecosystem. */
57
113
  const CORE_SKILLS = [
58
114
  { id: "academic-grill", name: "学术追问" },
@@ -404,7 +460,7 @@ async function panelStats(root) {
404
460
  const files = await listProjectFiles(root);
405
461
  return { schemaVersion: "psyclaw/panel-stats/v1", runs: (await listRuns(root)).length, evidence: await countLines(paths.evidence), claims: await countLines(paths.claims), auditEvents: await countLines(paths.audit), trackedFiles: files.length, outputs: files.filter((path) => path.startsWith("outputs/")).length, generatedAt: new Date().toISOString() };
406
462
  }
407
- const PANEL_FILE_ROOTS = ["notes/", "paper/", "docs/", "outputs/", "logs/"];
463
+ const PANEL_FILE_ROOTS = ["notes/", "paper/", "docs/", "outputs/", "logs/", "analysis/results/"];
408
464
  const PANEL_FILE_EXTENSIONS = /\.(md|markdown|txt|json|csv)$/i;
409
465
  function panelFileAllowed(relative) {
410
466
  const normalized = relative.replaceAll("\\", "/");
@@ -1232,16 +1288,11 @@ export function createPanelServer(root, options = {}) {
1232
1288
  return;
1233
1289
  }
1234
1290
  if (url.pathname === "/api/enabled-capabilities") {
1235
- const state = await readRecommendationState(root);
1236
1291
  const disabled = await readDisabledCapabilities(root);
1237
- const [skills, mcps] = await Promise.all([recommendedSkills(), recommendedMcps()]);
1238
- const skillIds = new Set((skills.items ?? []).map((item) => String(item.id ?? "")));
1239
- const mcpIds = new Set((mcps.items ?? []).map((item) => String(item.id ?? "")));
1240
- const nameOf = (kind, id) => {
1241
- const catalog = kind === "skill" ? skills : mcps;
1242
- const item = (catalog.items ?? []).find((candidate) => candidate.id === id);
1243
- return typeof item?.name === "string" ? item.name : id;
1244
- };
1292
+ const [skillStatuses, mcpStatuses] = await Promise.all([
1293
+ panelRecommendedSkillStatuses(root),
1294
+ panelRecommendedMcpStatuses(root),
1295
+ ]);
1245
1296
  // User-installed (non-catalog) skills from the expanded discovery
1246
1297
  // roots, with their individual enable state.
1247
1298
  const userSkillState = await readUserSkillState(root);
@@ -1252,6 +1303,7 @@ export function createPanelServer(root, options = {}) {
1252
1303
  description: skill.description,
1253
1304
  path: skill.path,
1254
1305
  enabled: !disabledUserSkills.has(skill.name),
1306
+ installed: true,
1255
1307
  }));
1256
1308
  response.writeHead(200, { "content-type": "application/json" });
1257
1309
  response.end(JSON.stringify({
@@ -1259,9 +1311,10 @@ export function createPanelServer(root, options = {}) {
1259
1311
  coreSkills: CORE_SKILLS.map((skill) => ({
1260
1312
  ...skill,
1261
1313
  enabled: "locked" in skill && skill.locked ? true : !disabled.coreSkills.includes(skill.id),
1314
+ installed: true,
1262
1315
  })),
1263
- skills: state.skills.filter((id) => skillIds.has(id)).map((id) => ({ id, name: nameOf("skill", id), enabled: true })),
1264
- mcp: state.mcp.filter((id) => mcpIds.has(id)).map((id) => ({ id, name: nameOf("mcp", id), enabled: true })),
1316
+ skills: skillStatuses,
1317
+ mcp: mcpStatuses,
1265
1318
  localSkills,
1266
1319
  }));
1267
1320
  return;
@@ -1304,7 +1357,15 @@ export function createPanelServer(root, options = {}) {
1304
1357
  }
1305
1358
  if (url.pathname === "/api/crosscheck") {
1306
1359
  if (request.method === "GET") {
1307
- const checklist = await loadVerifyChecklist(root);
1360
+ const existing = await loadVerifyChecklist(root);
1361
+ const defaults = defaultVerifyChecklist();
1362
+ const checklist = {
1363
+ ...existing,
1364
+ items: [
1365
+ ...existing.items,
1366
+ ...defaults.items.filter((item) => !existing.items.some((row) => row.id === item.id)),
1367
+ ],
1368
+ };
1308
1369
  response.writeHead(200, { "content-type": "application/json" });
1309
1370
  response.end(JSON.stringify({ schemaVersion: "psyclaw/crosscheck/v1", checklist }));
1310
1371
  return;
@@ -1552,14 +1613,29 @@ export function createPanelServer(root, options = {}) {
1552
1613
  response.end(JSON.stringify({ schemaVersion: "psyclaw/local-skill-state-receipt/v1", ok: true, id, enabled: body.enabled, disabled: state.disabled }));
1553
1614
  return;
1554
1615
  }
1555
- const state = await readRecommendationState(root);
1616
+ const state = await readCanonicalRecommendationState(root);
1617
+ if (kind === "skills" && body.enabled) {
1618
+ const scope = state.skillScopes?.[id] ?? "project";
1619
+ try {
1620
+ await validateModelInstalledRecommendedSkill(root, id, scope);
1621
+ }
1622
+ catch {
1623
+ throw new Error(`Skill 尚未安装到本地,无法启用:${id}。请先安装或重新安装。`);
1624
+ }
1625
+ }
1626
+ if (kind === "mcp" && body.enabled) {
1627
+ const configs = await new RuntimeMcpRegistry().listUserConfigs(root);
1628
+ if (!configs.some((entry) => entry.id === id)) {
1629
+ throw new Error(`MCP 尚未安装到本地,无法启用:${id}。请先安装或重新安装。`);
1630
+ }
1631
+ }
1556
1632
  const values = new Set(state[kind]);
1557
1633
  if (body.enabled)
1558
1634
  values.add(id);
1559
1635
  else
1560
1636
  values.delete(id);
1561
1637
  state[kind] = [...values];
1562
- await writeRecommendationState(root, state);
1638
+ await saveRecommendationState(root, state);
1563
1639
  response.writeHead(200, { "content-type": "application/json" });
1564
1640
  response.end(JSON.stringify({ schemaVersion: "psyclaw/recommendation-state-receipt/v1", ok: true, state }));
1565
1641
  return;