psyclaw 0.27.12 → 0.27.14

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.
Files changed (40) hide show
  1. package/README.md +11 -5
  2. package/dist/apps/panel/index.html +1 -1
  3. package/dist/src/adapters/pi/extension.js +28 -42
  4. package/dist/src/adapters/pi/extension.js.map +1 -1
  5. package/dist/src/analysis/hooks.js +12 -12
  6. package/dist/src/analysis/hooks.js.map +1 -1
  7. package/dist/src/branding.js +2 -2
  8. package/dist/src/branding.js.map +1 -1
  9. package/dist/src/cli.js +1 -12
  10. package/dist/src/cli.js.map +1 -1
  11. package/dist/src/index.d.ts +0 -1
  12. package/dist/src/index.js +0 -1
  13. package/dist/src/index.js.map +1 -1
  14. package/dist/src/integrations/mcp-runtime.js +3 -1
  15. package/dist/src/integrations/mcp-runtime.js.map +1 -1
  16. package/dist/src/panel/server.js +4 -0
  17. package/dist/src/panel/server.js.map +1 -1
  18. package/dist/src/setup.js +32 -10
  19. package/dist/src/setup.js.map +1 -1
  20. package/dist/src/skills/contracts.d.ts +4 -4
  21. package/dist/src/skills/preflight.d.ts +3 -3
  22. package/dist/src/skills/preflight.js +3 -3
  23. package/dist/src/skills/recommended.d.ts +1 -0
  24. package/dist/src/skills/recommended.js +9 -8
  25. package/dist/src/skills/recommended.js.map +1 -1
  26. package/dist/src/skills/registry.js +23 -37
  27. package/dist/src/skills/registry.js.map +1 -1
  28. package/dist/src/style/cli-ui.js +2 -2
  29. package/dist/src/style/cli-ui.js.map +1 -1
  30. package/dist/src/tui/provider-picker.js +3 -3
  31. package/dist/src/tui/provider-picker.js.map +1 -1
  32. package/dist/src/updates/update.js +3 -3
  33. package/dist/src/updates/update.js.map +1 -1
  34. package/dist/src/wizard.js +8 -4
  35. package/dist/src/wizard.js.map +1 -1
  36. package/package.json +1 -1
  37. package/skills/recommended/catalog.json +47 -0
  38. package/dist/src/telemetry/export.d.ts +0 -58
  39. package/dist/src/telemetry/export.js +0 -428
  40. package/dist/src/telemetry/export.js.map +0 -1
package/README.md CHANGED
@@ -2,20 +2,26 @@
2
2
 
3
3
  PsyClaw 是面向社会科学研究的智能体工作台。它把研究项目、证据来源、Claim-Evidence 账本、完整性门禁、人工裁决、可恢复工作流和本地面板接到内置运行时上;它不替代统计软件,也不会把未经核验的引用、结果或审稿意见写成事实。
4
4
 
5
- 当前版本:`0.27.12`。正式命令、用户配置目录和后续发布统一使用 `psyclaw`。
5
+ 当前版本:`0.27.14`。正式命令、用户配置目录和后续发布统一使用 `psyclaw`。
6
6
 
7
7
  ## 安装
8
8
 
9
9
  需要 Node.js `>=22.19.0`。官方 npm 源:
10
10
 
11
11
  ```powershell
12
- npm install -g psyclaw@0.27.12
12
+ npm install -g psyclaw@0.27.14
13
+ ```
14
+
15
+ 如果本机 npm 配置把 registry 误写成带有 `~/` 的地址,请显式指定官方源:
16
+
17
+ ```bash
18
+ npm install -g psyclaw@0.27.14 --registry=https://registry.npmjs.org/
13
19
  ```
14
20
 
15
21
  中国大陆网络较慢或无法访问官方源时:
16
22
 
17
23
  ```powershell
18
- npm install -g psyclaw@0.27.12 --registry=https://registry.npmmirror.com
24
+ npm install -g psyclaw@0.27.14 --registry=https://registry.npmmirror.com
19
25
  ```
20
26
 
21
27
  确认命令入口:
@@ -63,7 +69,7 @@ psyclaw brief
63
69
  | 扫描本机其他 Agent | `psyclaw agents` |
64
70
  | 查看 PsyClaw 和内置运行时更新 | `psyclaw check-updates` |
65
71
  | 更新 PsyClaw 和内置运行时 | `psyclaw update` |
66
- | 导出完整使用路径 | 在对话中输入 `/export` |
72
+ | 导出会话 | 使用 Pi 内置的 `/export` |
67
73
  | 打开科研面板 | 在对话中输入 `/panel` |
68
74
  | 查看或切换 Provider | 在对话中输入 `/provider` 或 `/provider <id>` |
69
75
  | 管理启动横幅宠物 | `/pet status`、`/pet on`、`/pet off`(默认关闭) |
@@ -82,7 +88,7 @@ psyclaw brief
82
88
  /export
83
89
  ```
84
90
 
85
- 该斜杠命令直接在 PsyClaw 对话界面中使用,从当前项目的工作流日志和对应会话中生成 `outputs/psyclaw-export.otlp.json`,用于通过 OpenTelemetry Collector 导入 Langfuse 或 LangSmith。导出文件包含对话正文、工具参数、研究内容、原始 ID 与绝对路径,只写入本地,不会自动上传;不要将其提交到公开仓库。终端入口为 `psyclaw export`。Panel 不再读取实时 Trace,只展示 `outputs/` 下已经显式生成的 Export 文件。
91
+ `/export` 由 Pi 内置命令提供。PsyClaw 不重复实现会话导出或 Trace 运行时。
86
92
 
87
93
  ## 开发者本地安装
88
94
 
@@ -65,7 +65,7 @@ function selectTree(button){document.querySelectorAll(".tree-item").forEach(item
65
65
  async function openFile(path,button){selectTree(button);$("viewer-path").textContent=path;$("viewer-kind").textContent="读取中";$("viewer-body").className="viewer-body empty";$("viewer-body").textContent="读取中...";try{const file=await json(`/api/project-file?path=${encodeURIComponent(path)}`);$("viewer-kind").textContent=file.format;$("viewer-body").className="viewer-body";$("viewer-body").innerHTML=file.format==="markdown"?`<article class="markdown">${renderMarkdown(file.content)}</article>`:`<pre class="raw">${esc(file.format==="json"?JSON.stringify(JSON.parse(file.content),null,2):file.content)}</pre>`}catch(error){$("viewer-body").className="viewer-body empty";$("viewer-body").textContent=error.message}}
66
66
  async function loadProject(){const files=await json("/api/project-files");state.files=files.files||[];renderTree();const goal=document.querySelector('[data-file="notes/goal.md"]')||document.querySelector("[data-file]");if(goal)goal.click()}
67
67
 
68
- async function loadEcosystem(){try{const[skills,mcps,enabled]=await Promise.all([json("/api/recommended-skills"),json("/api/recommended-mcps"),json("/api/enabled-capabilities")]);const skillSet=new Set((enabled.skills||[]).map(item=>item.id)),mcpSet=new Set((enabled.mcp||[]).map(item=>item.id));state.eco=[...(enabled.coreSkills||[]).map(item=>({...item,kind:"core",description:"PsyClaw 内置 Skill",sourceRef:"bundled",enabled:item.enabled,installed:true})),...(skills.items||[]).map(item=>({...item,kind:"skill",enabled:skillSet.has(item.id),installed:skillSet.has(item.id)})),...(enabled.localSkills||[]).map(item=>({...item,kind:"local",sourceRef:item.path||"用户目录",installed:true})),...(mcps.items||[]).map(item=>({...item,kind:"mcp",enabled:mcpSet.has(item.id),installed:mcpSet.has(item.id)})),...(skills.externalTools||[]).map(item=>({...item,kind:"external",enabled:false,installed:false}))];renderEcosystem()}catch(error){$("ecosystem-body").innerHTML=`<tr><td colspan="5">${esc(error.message)}</td></tr>`}}
68
+ async function loadEcosystem(){try{const[skills,mcps,enabled]=await Promise.all([json("/api/recommended-skills"),json("/api/recommended-mcps"),json("/api/enabled-capabilities")]);const skillSet=new Set((enabled.skills||[]).map(item=>item.id)),mcpSet=new Set((enabled.mcp||[]).map(item=>item.id));state.eco=[...(enabled.coreSkills||[]).map(item=>({...item,kind:"core",description:"PsyClaw 内置 Skill",sourceRef:"bundled",enabled:item.enabled,installed:true})),...(skills.items||[]).map(item=>({...item,kind:"skill",enabled:skillSet.has(item.id),installed:skillSet.has(item.id)})),...(skills.plugins||[]).map(item=>({...item,kind:"plugin",enabled:false,installed:false})),...(enabled.localSkills||[]).map(item=>({...item,kind:"local",sourceRef:item.path||"用户目录",installed:true})),...(mcps.items||[]).map(item=>({...item,kind:"mcp",enabled:mcpSet.has(item.id),installed:mcpSet.has(item.id)})),...(skills.externalTools||[]).map(item=>({...item,kind:"external",enabled:false,installed:false}))];renderEcosystem()}catch(error){$("ecosystem-body").innerHTML=`<tr><td colspan="5">${esc(error.message)}</td></tr>`}}
69
69
  function renderEcosystem(){const query=$("eco-search").value.trim().toLowerCase(),kind=$("eco-kind").value,status=$("eco-status").value;const items=state.eco.filter(item=>(kind==="all"||item.kind===kind)&&(status==="all"||(status==="enabled"?item.enabled:!item.enabled))&&`${item.name} ${item.description||""}`.toLowerCase().includes(query));$("ecosystem-body").innerHTML=items.map(item=>`<tr><td><div class="name">${esc(item.name)}</div><div class="subtle">${esc(item.description||"")}</div></td><td>${esc(item.kind==="core"?"Core Skill":item.kind==="local"?"用户 Skill":item.kind.toUpperCase())}</td><td>${/^https?:/.test(item.sourceRef||"")?`<a href="${esc(item.sourceRef)}" target="_blank" rel="noreferrer">来源</a>`:esc(item.sourceRef||"bundled")}</td><td><span class="tag ${item.enabled?"ok":""}">${item.enabled?"已启用":item.kind==="external"?"外部工具":"未启用"}</span></td><td>${item.kind==="external"?"—":item.enabled?`<button class="button small" onclick="toggleCapability('${item.kind}','${esc(item.id)}',false)">停用</button>`:item.kind==="local"?`<button class="button small primary" onclick="toggleCapability('local','${esc(item.id)}',true)">启用</button>`:`<button class="button small primary" onclick="installCapability('${item.kind}','${esc(item.id)}')">${item.kind==="core"?"启用":"安装"}</button>`}</td></tr>`).join("")||'<tr><td colspan="5">没有匹配项</td></tr>'}
70
70
  async function toggleCapability(kind,id,enabled){await json("/api/recommendation-state",{method:"POST",headers:{"content-type":"application/json"},body:JSON.stringify({kind:kind==="core"?"core-skill":kind==="skill"?"skills":kind==="local"?"local-skill":kind,id,enabled})});await loadEcosystem()}
71
71
  async function installCapability(kind,id){if(kind==="core"){await toggleCapability(kind,id,true);return}const scope=kind==="skill"?(confirm("安装到系统目录?\n确定:所有项目使用;取消:仅当前项目使用。")?"user":"project"):undefined;if(!confirm(`确认安装 ${id}?安装完成后需要 /reload。`))return;const result=await json("/api/install/execute",{method:"POST",headers:{"content-type":"application/json"},body:JSON.stringify({kind,id,scope,approved:true,actor:"researcher"})});alert(result.message||(result.ok?"安装任务已处理,请执行 /reload。":"安装未完成"));await loadEcosystem()}
@@ -1,6 +1,6 @@
1
1
  import { getAgentDir } from "@earendil-works/pi-coding-agent";
2
2
  import { Type } from "typebox";
3
- import { appendApproval, approvalInputDigest, approvePrimaryDocument, asProject, bootstrapProject, DEFAULT_RUN_APPROVAL_NODES, exportTraces, PRIMARY_PLAN_DOCUMENTS, primaryPlanApprovalStatus, projectPaths, runOfflineBrief, runInstitutionalFulltext, runLiteratureReview, runExpertReview, runAnalysisDelegation, runWritingReview, runMetaAnalysis, createStageRunner, exportAcademicDocument, recordCitationUse, runParallelPeerReview, writeHandoff } from "../../index.js";
3
+ import { appendApproval, approvalInputDigest, approvePrimaryDocument, asProject, bootstrapProject, DEFAULT_RUN_APPROVAL_NODES, PRIMARY_PLAN_DOCUMENTS, primaryPlanApprovalStatus, projectPaths, runOfflineBrief, runInstitutionalFulltext, runLiteratureReview, runExpertReview, runAnalysisDelegation, runWritingReview, runMetaAnalysis, createStageRunner, exportAcademicDocument, recordCitationUse, runParallelPeerReview, writeHandoff } from "../../index.js";
4
4
  import { runPlanWithPi } from "../../orchestration/pi-executor.js";
5
5
  import { atomicWriteFile } from "../../project/jsonl.js";
6
6
  import { RunEventLog } from "../../panel/events.js";
@@ -14,7 +14,7 @@ import { coreSkillNames, enabledRecommendedSkillPaths, normalizeRecommendedSkill
14
14
  import { SkillManagerComponent } from "../../tui/skill-manager.js";
15
15
  import { enabledLocalSkillPaths, enabledLocalPromptPaths, readUserSkillState, scanLocalSkills, setLocalSkillEnabled, setLocalSkillsEnabled, skillNamesInPaths, installLocalSkill, userSkillId, } from "../../skills/user-skills.js";
16
16
  import { RuntimeMcpRegistry, setUserMcpConfigEnabled, } from "../../integrations/mcp-runtime.js";
17
- import { SecretInputComponent, } from "../../tui/provider-picker.js";
17
+ import { SecretInputComponent, ProviderPickerComponent, } from "../../tui/provider-picker.js";
18
18
  const PARADIGMS = new Set([
19
19
  "survey-observational",
20
20
  "qualitative-thematic",
@@ -56,7 +56,6 @@ async function runPluginCommand(args) {
56
56
  throw new Error(`不支持的 Plugin 操作:${args[0] ?? ""}`);
57
57
  }
58
58
  const activeAgentRuns = new Set();
59
- const pendingInitApprovals = new Set();
60
59
  const activeApprovalDialogs = new Set();
61
60
  const CORE_SKILLS = new Set(["academic-grill", "research-intake", "evidence-capture", "citation-audit", "research-brief"]);
62
61
  function coreSkillPath(name) {
@@ -120,10 +119,14 @@ async function saveDefaultModel(provider, model) {
120
119
  await atomicWriteFile(path, `${JSON.stringify({ ...settings, defaultProvider: provider, defaultModel: model }, null, 2)}\n`);
121
120
  }
122
121
  async function pickProviderItem(ctx, title, items) {
123
- const labels = items.map((item) => `${item.current ? "* " : ""}${item.label} (${item.id})`);
124
- const selected = await ctx.ui.select(title, labels, { timeout: 60_000 });
125
- const index = selected === undefined ? -1 : labels.indexOf(selected);
126
- return index < 0 ? undefined : items[index].id;
122
+ if (typeof ctx.ui.custom !== "function") {
123
+ const labels = items.map((item) => `${item.current ? "* " : ""}${item.label} (${item.id})`);
124
+ const selected = await ctx.ui.select(title, labels, { timeout: 60_000 });
125
+ const index = selected === undefined ? -1 : labels.indexOf(selected);
126
+ return index < 0 ? undefined : items[index].id;
127
+ }
128
+ const selected = await ctx.ui.custom((tui, theme, keybindings, done) => new ProviderPickerComponent(title, items, tui, theme, keybindings, done));
129
+ return selected.type === "select" ? selected.id : undefined;
127
130
  }
128
131
  async function promptProviderKey(ctx, providerName, envName) {
129
132
  const result = await ctx.ui.custom((tui, theme, keybindings, done) => new SecretInputComponent(`配置 ${providerName}`, envName, tui, theme, keybindings, done));
@@ -354,13 +357,14 @@ async function recommendedItems(kind) {
354
357
  const value = JSON.parse(await readFile(path, "utf8"));
355
358
  return {
356
359
  items: Array.isArray(value.items) ? value.items.filter((item) => Boolean(item && typeof item === "object")) : [],
360
+ plugins: Array.isArray(value.plugins) ? value.plugins.filter((item) => Boolean(item && typeof item === "object")) : [],
357
361
  externalTools: Array.isArray(value.externalTools) ? value.externalTools.filter((item) => Boolean(item && typeof item === "object")) : [],
358
362
  installPrep: Array.isArray(value.installPrep) ? value.installPrep.filter((item) => Boolean(item && typeof item === "object")) : [],
359
363
  };
360
364
  }
361
365
  catch { /* try next candidate */ }
362
366
  }
363
- return { items: [], externalTools: [], installPrep: [] };
367
+ return { items: [], plugins: [], externalTools: [], installPrep: [] };
364
368
  }
365
369
  function skillScopeLabel(scope) {
366
370
  if (scope === "local")
@@ -1000,13 +1004,6 @@ export default function psyclawExtension(pi) {
1000
1004
  });
1001
1005
  return approved ? undefined : { block: true, terminate: true, reason: "用户拒绝该执行步骤或审批超时" };
1002
1006
  });
1003
- if (!legacyTestApi && typeof pi.on === "function")
1004
- pi.on("agent_end", async (_event, ctx) => {
1005
- if (!pendingInitApprovals.has(ctx.cwd) || !(await planDocumentsReady(ctx.cwd)))
1006
- return;
1007
- pendingInitApprovals.delete(ctx.cwd);
1008
- await reviewPrimaryDocuments(ctx);
1009
- });
1010
1007
  pi.registerCommand("init", {
1011
1008
  description: "初始化可追溯的研究项目",
1012
1009
  handler: async (args, ctx) => {
@@ -1014,9 +1011,8 @@ export default function psyclawExtension(pi) {
1014
1011
  const parsed = parseInitArgs(args);
1015
1012
  const project = await bootstrapProject({ root: ctx.cwd, ...parsed });
1016
1013
  if (typeof pi.sendUserMessage === "function") {
1017
- pendingInitApprovals.add(ctx.cwd);
1018
1014
  pi.sendUserMessage(academicGrillRequest(project.goal, "init"), ctx.isIdle() ? {} : { deliverAs: "followUp" });
1019
- ctx.ui.notify(`研究项目已初始化:${project.id}(${project.paradigm})。学术追问完成并生成主要计划文档后会进入审批;全部批准后才能 /run。`, "info");
1015
+ ctx.ui.notify(`研究项目已初始化:${project.id}(${project.paradigm})。当前保持普通对话;如需进入受控研究流程,请明确运行 /run。`, "info");
1020
1016
  }
1021
1017
  else {
1022
1018
  ctx.ui.notify(`研究项目已初始化:${project.id}(${project.paradigm})。使用 /run 启动受控研究流程。`, "info");
@@ -1066,8 +1062,11 @@ export default function psyclawExtension(pi) {
1066
1062
  const planApproval = await primaryPlanApprovalStatus(ctx.cwd);
1067
1063
  if (!planApproval.ok) {
1068
1064
  const missing = planApproval.documents.filter((item) => !item.approved).map((item) => item.path);
1069
- ctx.ui.notify(`主要计划尚未批准或批准后已修改:${missing.join("、")}。请运行 /approve。`, "warning");
1070
- return;
1065
+ // `/run` is the sole switch into the controlled workflow. A plan
1066
+ // can still be incomplete when execution starts; keep the status
1067
+ // visible and let the model repair it through the normal workflow
1068
+ // instead of forcing a separate approval command first.
1069
+ ctx.ui.notify(`检测到计划尚未确认或已修改:${missing.join("、")}。将由受控流程继续处理;如需先人工确认,可运行 /approve。`, "warning");
1071
1070
  }
1072
1071
  const available = await selectableRunSkills(ctx.cwd);
1073
1072
  let requestedSkills = parsed.requestedSkills;
@@ -1329,8 +1328,9 @@ export default function psyclawExtension(pi) {
1329
1328
  const [skills, mcps] = await Promise.all([recommendedItems("skills"), recommendedItems("mcp")]);
1330
1329
  const skillLines = skills.items.slice(0, 8).map((item) => `Skill: ${String(item.id)} — ${String(item.name)}`);
1331
1330
  const mcpLines = mcps.items.slice(0, 8).map((item) => `MCP: ${String(item.id)} — ${String(item.name)}`);
1331
+ const pluginLines = skills.plugins.slice(0, 8).map((item) => `Plugin: ${String(item.name)} — ${String(item.sourceRef ?? "请查看项目文档")}`);
1332
1332
  const toolLines = skills.externalTools.slice(0, 8).map((item) => `外部工具: ${String(item.name)} — ${String(item.sourceRef ?? "请查看项目文档")}`);
1333
- ctx.ui.notify(["推荐安装入口", "", ...skillLines, ...mcpLines, "", ...toolLines, "", "安装:/install skill|mcp <id>", "管理:/skill 或 /mcp", "外部工具不通过 Skill 安装器安装", "也可以打开 /panel 查看推荐页面"].join("\n"), "info");
1333
+ ctx.ui.notify(["推荐安装入口", "", ...skillLines, ...pluginLines, ...mcpLines, "", ...toolLines, "", "安装:/install skill|mcp <id>", "Plugin:/plugin install <source>", "管理:/skill 或 /mcp", "外部工具不通过 Skill 安装器安装", "也可以打开 /panel 查看推荐页面"].join("\n"), "info");
1334
1334
  return;
1335
1335
  }
1336
1336
  if (kind !== "skill" && kind !== "mcp")
@@ -1415,9 +1415,16 @@ export default function psyclawExtension(pi) {
1415
1415
  }
1416
1416
  if (preset && ctx.hasUI && typeof ctx.ui.custom === "function") {
1417
1417
  const credential = await providerCredentialSource(preset);
1418
- const key = credential === "missing" ? await promptProviderKey(ctx, preset.name, preset.apiKeyEnv) : "";
1418
+ // Always show the credential step when switching a built-in
1419
+ // provider. An empty submission deliberately keeps an existing
1420
+ // environment/auth-store credential; this makes the flow explicit
1421
+ // and avoids silently skipping the key screen on another machine.
1422
+ const key = await promptProviderKey(ctx, preset.name, preset.apiKeyEnv);
1419
1423
  if (key === undefined)
1420
1424
  return;
1425
+ if (!key && credential === "missing") {
1426
+ throw new Error(`未找到 ${preset.apiKeyEnv};请输入 API Key 后再继续`);
1427
+ }
1421
1428
  await saveProviderConfig({ ...preset, ...(key ? { apiKey: key } : {}) });
1422
1429
  const refreshed = await Promise.race([
1423
1430
  ctx.modelRegistry.refresh().then(() => true),
@@ -1460,27 +1467,6 @@ export default function psyclawExtension(pi) {
1460
1467
  ctx.ui.notify(`启动横幅宠物已${action === "on" ? "开启" : "关闭"},下次启动生效`, "info");
1461
1468
  },
1462
1469
  });
1463
- const traceCommand = {
1464
- description: "导出包含正文与工具参数的完整使用路径,供 Langfuse 或 LangSmith 分析",
1465
- handler: async (args, ctx) => {
1466
- try {
1467
- if (args.trim())
1468
- throw new Error("Usage: /export");
1469
- const result = await exportTraces({ root: ctx.cwd });
1470
- ctx.ui.notify([
1471
- "使用路径已导出(未上传)",
1472
- `文件:${result.output}`,
1473
- `轨迹:${result.traces}`,
1474
- `步骤:${result.spans}`,
1475
- "包含对话正文、工具参数、原始 ID 与绝对路径,便于排查各环节问题;请勿将导出文件提交到公开仓库。",
1476
- ].join("\n"), "info");
1477
- }
1478
- catch (error) {
1479
- await notifyError(ctx, error);
1480
- }
1481
- },
1482
- };
1483
- pi.registerCommand("export", traceCommand);
1484
1470
  if (typeof pi.registerTool === "function") {
1485
1471
  pi.registerTool({
1486
1472
  name: "psyclaw_mcp",