psyclaw 0.26.3 → 0.27.0

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 (42) hide show
  1. package/README.md +35 -8
  2. package/dist/src/adapters/pi/extension.js +274 -119
  3. package/dist/src/adapters/pi/extension.js.map +1 -1
  4. package/dist/src/branding.d.ts +9 -0
  5. package/dist/src/branding.js +23 -0
  6. package/dist/src/branding.js.map +1 -1
  7. package/dist/src/chat.d.ts +3 -0
  8. package/dist/src/chat.js +22 -6
  9. package/dist/src/chat.js.map +1 -1
  10. package/dist/src/cli.js +20 -8
  11. package/dist/src/cli.js.map +1 -1
  12. package/dist/src/index.d.ts +2 -0
  13. package/dist/src/index.js +2 -0
  14. package/dist/src/index.js.map +1 -1
  15. package/dist/src/setup.d.ts +1 -1
  16. package/dist/src/setup.js +12 -4
  17. package/dist/src/setup.js.map +1 -1
  18. package/dist/src/skills/recommended.d.ts +75 -0
  19. package/dist/src/skills/recommended.js +291 -0
  20. package/dist/src/skills/recommended.js.map +1 -0
  21. package/dist/src/style/cli-ui.js +2 -1
  22. package/dist/src/style/cli-ui.js.map +1 -1
  23. package/dist/src/telemetry/export.d.ts +24 -0
  24. package/dist/src/telemetry/export.js +264 -0
  25. package/dist/src/telemetry/export.js.map +1 -0
  26. package/dist/src/tui/skill-manager.d.ts +49 -0
  27. package/dist/src/tui/skill-manager.js +133 -0
  28. package/dist/src/tui/skill-manager.js.map +1 -0
  29. package/dist/src/updates/registry.d.ts +1 -0
  30. package/dist/src/updates/registry.js +19 -0
  31. package/dist/src/updates/registry.js.map +1 -1
  32. package/dist/src/updates/update.d.ts +32 -4
  33. package/dist/src/updates/update.js +163 -6
  34. package/dist/src/updates/update.js.map +1 -1
  35. package/package.json +4 -3
  36. package/scripts/rebrand-pi.mjs +16 -12
  37. package/skills/core/citation-audit/SKILL.md +9 -9
  38. package/skills/core/evidence-capture/SKILL.md +9 -9
  39. package/skills/core/manifest.json +15 -15
  40. package/skills/core/research-brief/SKILL.md +8 -8
  41. package/skills/core/research-intake/SKILL.md +9 -9
  42. package/skills/recommended/catalog.json +14 -13
package/README.md CHANGED
@@ -2,20 +2,20 @@
2
2
 
3
3
  PsyClaw 是面向社会科学研究的 Pi 工作台。它把研究项目、证据来源、Claim-Evidence 账本、完整性门禁、人工裁决、可恢复工作流和本地面板接到官方 Pi runtime 上;它不替代统计软件,也不会把未经核验的引用、结果或审稿意见写成事实。
4
4
 
5
- 当前版本:`0.26.3`。本仓库以 `psypi v0.4.1` 的完整工作台为功能基线完成改名;`psypi` 仓库保留为历史仓库,正式命令和后续发布均使用 `psyclaw`。
5
+ 当前版本:`0.27.0`。本仓库以 `psypi v0.4.1` 的完整工作台为功能基线完成改名;`psypi` 仓库保留为历史仓库,正式命令和后续发布均使用 `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.26.3
12
+ npm install -g psyclaw@0.27.0
13
13
  ```
14
14
 
15
15
  中国大陆网络较慢或无法访问官方源时:
16
16
 
17
17
  ```powershell
18
- npm install -g psyclaw@0.26.3 --registry=https://registry.npmmirror.com
18
+ npm install -g psyclaw@0.27.0 --registry=https://registry.npmmirror.com
19
19
  ```
20
20
 
21
21
  确认命令入口:
@@ -57,13 +57,23 @@ psyclaw brief
57
57
  | 创建人工裁决模板 | `psyclaw hitl init` |
58
58
  | 写研究移交记录 | `psyclaw handoff` |
59
59
  | 扫描本机其他 Agent | `psyclaw agents` |
60
- | 查看 Pi runtime 更新 | `psyclaw check-updates` |
61
- | 更新 Pi runtime | `psyclaw update --yes` |
60
+ | 查看 PsyClaw 和内置 Pi 更新 | `psyclaw check-updates` |
61
+ | 更新 PsyClaw 和内置 Pi | `psyclaw update` |
62
+ | 导出脱敏使用路径 | 在对话中输入 `/trace` |
62
63
  | 打开科研面板 | 在对话中输入 `/panel` |
63
64
  | 查看或切换 Provider | 在对话中输入 `/provider` 或 `/provider <id>` |
64
65
  | 管理启动横幅宠物 | `/pet status`、`/pet on`、`/pet off`(默认关闭) |
65
66
 
66
- `psyclaw update` 只更新捆绑的 Pi runtime;它不是从 Git 拉取 PsyClaw 源码。全局安装用户通过 `npm install -g psyclaw@<version>` 获取 PsyClaw 新版本;本仓库开发者直接修改源码并构建。
67
+ `psyclaw update` 会更新 PsyClaw 整包,并同时安装该版本锁定、验证过的内置 Pi;它不会更新或删除系统中单独安装的 `pi` 命令。在源码仓库中运行时,命令会停止并提示通过 Git 更新,不会覆盖本地修改。
68
+ 仅查看更新计划而不执行时,使用 `psyclaw update --check`。旧的 `--yes` 参数仍兼容,但不再需要。
69
+
70
+ ### 导出使用路径
71
+
72
+ ```bash
73
+ /trace
74
+ ```
75
+
76
+ 该斜杠命令直接在 PsyClaw 对话界面中使用,从当前项目的工作流日志和对应 Pi 会话中生成 OTLP/HTTP JSON,用于通过 OpenTelemetry Collector 导入 Langfuse 或 LangSmith。导出器只保留步骤类型、父子关系、时间、结果状态和版本;不包含对话正文、工具参数/输出、研究内容、原始 ID 或绝对路径。命令只写本地文件,不会自动上传。终端兼容入口仍为 `psyclaw traces export`。
67
77
 
68
78
  ## 开发者本地安装
69
79
 
@@ -74,7 +84,7 @@ npm link
74
84
  psyclaw --help
75
85
  ```
76
86
 
77
- 开发阶段不需要执行 `psyclaw update`。它会更新 Pi 依赖,不会同步本仓库代码。
87
+ 开发阶段不需要执行 `psyclaw update`。请通过 Git 同步仓库,再运行 `pnpm install` 和 `pnpm build`。
78
88
 
79
89
  ## 从旧 Python 版迁移
80
90
 
@@ -89,12 +99,29 @@ where.exe psyclaw
89
99
 
90
100
  ```powershell
91
101
  npm uninstall -g psyclaw
92
- npm install -g psyclaw@0.26.3
102
+ npm install -g psyclaw@0.27.0
93
103
  psyclaw --help
94
104
  ```
95
105
 
96
106
  Python 的卸载器提示 `No files were found to uninstall` 时,不要手工删除不明目录;先用上述命令定位旧入口,再在对应 Python 环境中修复或移除它。
97
107
 
108
+ ## 卸载
109
+
110
+ 仅卸载 PsyClaw 程序,保留用户配置和历史会话:
111
+
112
+ ```bash
113
+ npm uninstall -g psyclaw
114
+ ```
115
+
116
+ 完全卸载 PsyClaw(同时删除模型配置、用户设置和历史会话):
117
+
118
+ ```bash
119
+ npm uninstall -g psyclaw
120
+ node -e 'const fs=require("node:fs"),os=require("node:os"),path=require("node:path");const target=path.join(os.homedir(),`.psy${"pi"}`);if(path.dirname(target)!==os.homedir())throw new Error("unsafe PsyClaw data path");fs.rmSync(target,{recursive:true,force:true});console.log(`removed ${target}`)'
121
+ ```
122
+
123
+ 上述命令不会删除研究项目目录,因为其中可能包含论文、证据、原始数据和分析产物。如需删除某个项目,应单独确认该项目的准确路径后处理。单独全局安装的 `@earendil-works/pi-coding-agent` 不属于 PsyClaw 用户数据,上述命令不会删除它。
124
+
98
125
  ## 文档
99
126
 
100
127
  - [PsyClaw v0.26.0 使用白皮书](docs/PsyClaw使用白皮书_v0.26.0.md)
@@ -1,6 +1,6 @@
1
1
  import { getAgentDir } from "@earendil-works/pi-coding-agent";
2
2
  import { Type } from "typebox";
3
- import { asProject, bootstrapProject, projectPaths, runOfflineBrief, runInstitutionalFulltext, runLiteratureReview, runExpertReview, runAnalysisDelegation, runWritingReview, runMetaAnalysis, createStageRunner, publishManuscript, recordCitationUse, writeHandoff } from "../../index.js";
3
+ import { asProject, bootstrapProject, exportTraces, projectPaths, runOfflineBrief, runInstitutionalFulltext, runLiteratureReview, runExpertReview, runAnalysisDelegation, runWritingReview, runMetaAnalysis, createStageRunner, publishManuscript, recordCitationUse, 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";
@@ -10,6 +10,8 @@ import { mkdir, readFile } from "node:fs/promises";
10
10
  import { PROVIDER_PRESETS } from "../../setup.js";
11
11
  import { dirname } from "node:path";
12
12
  import { fileURLToPath } from "node:url";
13
+ import { coreSkillNames, installRecommendedSkill, normalizeRecommendedSkillId, readRecommendationState, readRecommendedCatalog, saveRecommendationState, validateInstalledRecommendedSkill, } from "../../skills/recommended.js";
14
+ import { SkillManagerComponent } from "../../tui/skill-manager.js";
13
15
  const PARADIGMS = new Set([
14
16
  "survey-observational",
15
17
  "qualitative-thematic",
@@ -44,19 +46,6 @@ async function notifyError(ctx, error) {
44
46
  }
45
47
  const activeAgentRuns = new Set();
46
48
  const CORE_SKILLS = new Set(["research-intake", "evidence-capture", "citation-audit", "research-brief"]);
47
- async function recommendationState(root) {
48
- try {
49
- const value = JSON.parse(await readFile(join(root, ".psyclaw", "recommendations.json"), "utf8"));
50
- return { schemaVersion: "psyclaw/recommendation-state/v1", skills: Array.isArray(value.skills) ? value.skills.filter((id) => typeof id === "string") : [], mcp: Array.isArray(value.mcp) ? value.mcp.filter((id) => typeof id === "string") : [] };
51
- }
52
- catch {
53
- return { schemaVersion: "psyclaw/recommendation-state/v1", skills: [], mcp: [] };
54
- }
55
- }
56
- async function saveRecommendationState(root, state) {
57
- await mkdir(join(root, ".psyclaw"), { recursive: true });
58
- await atomicWriteFile(join(root, ".psyclaw", "recommendations.json"), `${JSON.stringify({ ...state, skills: [...new Set(state.skills)].sort(), mcp: [...new Set(state.mcp)].sort() }, null, 2)}\n`);
59
- }
60
49
  function coreSkillPath(name) {
61
50
  return join(dirname(fileURLToPath(import.meta.url)), "..", "..", "..", "..", "skills", "core", name, "SKILL.md");
62
51
  }
@@ -154,118 +143,257 @@ async function recommendedItems(kind) {
154
143
  }
155
144
  return { items: [], installPrep: [] };
156
145
  }
157
- async function showRecommended(kind, args, ctx) {
158
- const { items, installPrep } = await recommendedItems(kind);
159
- const state = await recommendationState(ctx.cwd);
160
- const stateKey = kind === "skills" ? "skill" : "mcp";
161
- const stateField = kind === "skills" ? "skills" : "mcp";
162
- const action = args.trim().split(/\s+/).filter(Boolean);
163
- const verb = action[0];
164
- const id = action[1];
165
- if (verb && !["status", "enable", "disable", "install", "select"].includes(verb)) {
166
- throw new Error(`Usage: /${kind} [status|enable <id>|disable <id>|install <id>]`);
167
- }
168
- // 1. 无参数或显式 select 时,如果处于交互 TUI 模式,则启动键盘上下键选择器
169
- if ((!verb || verb === "select") && ctx.hasUI && typeof ctx.ui.select === "function") {
170
- if (!items.length) {
171
- ctx.ui.notify(`暂无推荐${kind === "skills" ? " Skill" : " MCP"}`, "info");
172
- return;
146
+ async function skillManagerRows(root, state) {
147
+ const catalog = await readRecommendedCatalog();
148
+ return Promise.all(catalog.items.filter((item) => item.kind === "skill").map(async (item) => {
149
+ const id = normalizeRecommendedSkillId(item.id);
150
+ try {
151
+ await validateInstalledRecommendedSkill(root, id);
152
+ return {
153
+ id,
154
+ name: item.name,
155
+ description: String(item.description ?? ""),
156
+ ...(item.sourceRef === undefined ? {} : { sourceRef: item.sourceRef }),
157
+ installed: true,
158
+ enabled: state.skills.includes(id),
159
+ blocked: false,
160
+ };
173
161
  }
174
- const enabled = new Set(state[stateField]);
175
- const optionLabels = items.map((item) => {
176
- const isEnabled = enabled.has(String(item.id));
177
- const mark = isEnabled ? "● [已启用]" : "○ [未启用]";
178
- const stage = item.stage ? ` [${String(item.stage)}]` : "";
179
- const desc = item.description ? ` — ${String(item.description)}` : "";
180
- return `${mark} ${String(item.name)}${stage}${desc}`;
181
- });
182
- const CANCEL_LABEL = "↩️ 取消 / 返回";
183
- const selected = await ctx.ui.select(`选择推荐 ${kind === "skills" ? "Skill" : "MCP"}(使用键盘 ↑/↓ 移动光标,Enter 确认):`, [...optionLabels, CANCEL_LABEL]);
184
- if (!selected || selected === CANCEL_LABEL)
185
- return;
186
- const selectedIndex = optionLabels.indexOf(selected);
187
- if (selectedIndex < 0 || selectedIndex >= items.length)
188
- return;
189
- const selectedItem = items[selectedIndex];
190
- if (!selectedItem)
191
- return;
192
- const targetId = String(selectedItem.id);
193
- const isCurrentlyEnabled = enabled.has(targetId);
194
- const actionChoice = await ctx.ui.select(`管理推荐 ${kind === "skills" ? "Skill" : "MCP"}: ${String(selectedItem.name)} (${targetId})`, [
195
- isCurrentlyEnabled ? `🔴 禁用 (Disable ${String(selectedItem.name)})` : `🟢 启用 (Enable ${String(selectedItem.name)})`,
196
- `📦 查看安装预检与依赖计划 (Install Plan)`,
197
- `🌐 查看来源仓库 (${String(selectedItem.sourceRef ?? "无")})`,
198
- `↩️ 取消`,
199
- ]);
200
- if (!actionChoice || actionChoice === "↩️ 取消")
201
- return;
202
- if (actionChoice.startsWith("🔴") || actionChoice.startsWith("🟢")) {
203
- const nextAction = isCurrentlyEnabled ? "disable" : "enable";
204
- if (await ctx.ui.confirm(`${nextAction === "enable" ? "启用" : "禁用"}推荐${stateKey === "skill" ? " Skill" : " MCP"}`, `${String(selectedItem.name)} 的状态将写入当前项目配置。`)) {
205
- const current = new Set(state[stateField]);
206
- if (nextAction === "enable")
207
- current.add(targetId);
208
- else
209
- current.delete(targetId);
210
- state[stateField] = [...current];
211
- await saveRecommendationState(ctx.cwd, state);
212
- ctx.ui.notify(`${nextAction === "enable" ? "已启用" : "已禁用"} ${String(selectedItem.name)}(/reload 后生效;打开 /panel 可可视化管理)`, "info");
213
- }
214
- return;
162
+ catch (error) {
163
+ const plan = catalog.installPrep.find((candidate) => normalizeRecommendedSkillId(candidate.id) === id);
164
+ const installable = plan?.sourceKind === "github" && typeof plan.sourceUrl === "string" &&
165
+ typeof plan.skillPath === "string" && typeof plan.skillName === "string" &&
166
+ typeof plan.ref === "string" && !["unknown", "NOASSERTION"].includes(plan.license);
167
+ return {
168
+ id,
169
+ name: item.name,
170
+ description: String(item.description ?? ""),
171
+ ...(item.sourceRef === undefined ? {} : { sourceRef: item.sourceRef }),
172
+ installed: false,
173
+ enabled: false,
174
+ blocked: !installable,
175
+ reason: installable ? "尚未安装" : (plan?.blockedReason ?? (error instanceof Error ? error.message : String(error))),
176
+ };
215
177
  }
216
- if (actionChoice.startsWith("📦")) {
217
- const plan = installPrep.find((candidate) => candidate.id === targetId);
218
- ctx.ui.notify(JSON.stringify({
219
- schemaVersion: "psyclaw/recommendation-install/v1",
220
- kind: stateKey,
221
- id: targetId,
222
- name: selectedItem.name,
223
- sourceRef: selectedItem.sourceRef,
224
- plan: plan ?? null,
225
- status: plan?.status ?? "review-required",
226
- next: "安装预检与启用是两个步骤;打开 /panel 查看完整计划,安装完成后再执行启用。",
227
- }, null, 2), "info");
178
+ }));
179
+ }
180
+ async function mcpManagerRows(state) {
181
+ const { items, installPrep } = await recommendedItems("mcp");
182
+ return items.map((item) => {
183
+ const id = String(item.id ?? "");
184
+ const plan = installPrep.find((candidate) => candidate.id === id);
185
+ const blocked = plan?.status === "blocked";
186
+ const dependencies = Array.isArray(plan?.dependencies)
187
+ ? plan.dependencies.filter((value) => typeof value === "string")
188
+ : [];
189
+ return {
190
+ id,
191
+ name: String(item.name ?? id),
192
+ description: String(item.description ?? ""),
193
+ ...(typeof item.sourceRef === "string" ? { sourceRef: item.sourceRef } : {}),
194
+ enabled: state.mcp.includes(id),
195
+ blocked,
196
+ ...(typeof plan?.blockedReason === "string" ? { reason: plan.blockedReason } : {}),
197
+ details: [
198
+ `传输:${String(item.transport ?? "未声明")} · 风险:${String(item.risk ?? "未声明")}`,
199
+ `版本:${String(plan?.ref ?? "未固定")} · 许可证:${String(plan?.license ?? "未核验")}`,
200
+ `依赖:${dependencies.length > 0 ? dependencies.join(", ") : "无已声明依赖"}`,
201
+ ],
202
+ };
203
+ });
204
+ }
205
+ function skillRowLabel(row) {
206
+ const state = row.blocked ? "× 阻断" : row.enabled ? "● 已启用" : row.installed ? "○ 未启用" : "↓ 未安装";
207
+ return `${state} ${row.name} [${row.id}]`;
208
+ }
209
+ async function setRecommendedSkillEnabled(root, requestedId, enabled) {
210
+ const id = normalizeRecommendedSkillId(requestedId);
211
+ if (enabled)
212
+ await validateInstalledRecommendedSkill(root, id);
213
+ const state = await readRecommendationState(root);
214
+ const current = new Set(state.skills);
215
+ if (enabled)
216
+ current.add(id);
217
+ else
218
+ current.delete(id);
219
+ state.skills = [...current];
220
+ await saveRecommendationState(root, state);
221
+ }
222
+ function skillManagerItems(rows) {
223
+ return [
224
+ ...coreSkillNames().map((name) => ({
225
+ id: `core:${name}`,
226
+ name,
227
+ description: "PsyClaw 核心研究流程 Skill。",
228
+ status: "core",
229
+ sourceRef: "PsyClaw core",
230
+ })),
231
+ ...rows.map((row) => ({
232
+ id: row.id,
233
+ name: row.name,
234
+ description: row.description,
235
+ status: row.blocked ? "blocked" : row.enabled ? "enabled" : row.installed ? "disabled" : "missing",
236
+ ...(row.sourceRef === undefined ? {} : { sourceRef: row.sourceRef }),
237
+ ...(row.reason === undefined ? {} : { reason: row.reason }),
238
+ })),
239
+ ];
240
+ }
241
+ async function openSkillManager(ctx, rows) {
242
+ return ctx.ui.custom((tui, theme, keybindings, done) => (new SkillManagerComponent(skillManagerItems(rows), tui, theme, keybindings, done)));
243
+ }
244
+ function mcpManagerItems(rows) {
245
+ return rows.map((row) => ({
246
+ id: row.id,
247
+ name: row.name,
248
+ description: row.description,
249
+ status: row.blocked ? "blocked" : row.enabled ? "enabled" : "disabled",
250
+ configuredEnabled: row.enabled,
251
+ ...(row.sourceRef === undefined ? {} : { sourceRef: row.sourceRef }),
252
+ ...(row.reason === undefined ? {} : { reason: row.reason }),
253
+ details: row.details,
254
+ }));
255
+ }
256
+ async function openMcpManager(ctx, rows) {
257
+ return ctx.ui.custom((tui, theme, keybindings, done) => (new SkillManagerComponent(mcpManagerItems(rows), tui, theme, keybindings, done, {
258
+ title: "MCP 管理",
259
+ itemLabel: "MCP",
260
+ footer: "↑/↓ 移动 · Space 开启/关闭项目配置 · Enter 查看预检 · Esc 关闭",
261
+ enterAction: "install",
262
+ enabledText: "项目配置已开启;运行时仍需通过安装、信任和工具策略检查",
263
+ disabledText: "项目配置已关闭",
264
+ blockedText: "预检阻断;若配置曾开启,可按 Space 关闭",
265
+ })));
266
+ }
267
+ async function setRecommendedMcpEnabled(root, id, enabled) {
268
+ const state = await readRecommendationState(root);
269
+ const rows = await mcpManagerRows(state);
270
+ const row = rows.find((candidate) => candidate.id === id);
271
+ if (!row)
272
+ throw new Error(`未找到推荐 MCP: ${id}`);
273
+ if (enabled && row.blocked)
274
+ throw new Error(row.reason ?? `MCP 预检未通过: ${id}`);
275
+ const current = new Set(state.mcp);
276
+ if (enabled)
277
+ current.add(id);
278
+ else
279
+ current.delete(id);
280
+ state.mcp = [...current];
281
+ await saveRecommendationState(root, state);
282
+ }
283
+ function mcpInstallPlan(id, items, installPrep) {
284
+ const item = items.find((candidate) => candidate.id === id);
285
+ if (!item)
286
+ throw new Error(`未找到推荐 MCP: ${id}`);
287
+ const plan = installPrep.find((candidate) => candidate.id === id);
288
+ return {
289
+ schemaVersion: "psyclaw/recommendation-install/v1",
290
+ kind: "mcp",
291
+ id,
292
+ name: item.name,
293
+ sourceRef: item.sourceRef,
294
+ plan: plan ?? null,
295
+ status: plan?.status ?? "review-required",
296
+ next: "这里只展示固定来源、版本、许可证和依赖预检;完成受信任安装与注册后,MCP 才能在运行时调用。",
297
+ };
298
+ }
299
+ async function showMcpManager(args, ctx) {
300
+ const [verb, id] = args.trim().split(/\s+/).filter(Boolean);
301
+ if (verb && !["status", "enable", "disable", "install"].includes(verb)) {
302
+ throw new Error("Usage: /mcp [status|enable <id>|disable <id>|install <id>]");
303
+ }
304
+ if ((verb === "enable" || verb === "disable" || verb === "install") && !id) {
305
+ throw new Error(`Usage: /mcp ${verb} <id>`);
306
+ }
307
+ const catalog = await recommendedItems("mcp");
308
+ if (verb === "install") {
309
+ ctx.ui.notify(JSON.stringify(mcpInstallPlan(id, catalog.items, catalog.installPrep), null, 2), "info");
310
+ return;
311
+ }
312
+ if (verb === "enable" || verb === "disable") {
313
+ await setRecommendedMcpEnabled(ctx.cwd, id, verb === "enable");
314
+ ctx.ui.notify(`MCP ${id} 的项目配置已${verb === "enable" ? "开启" : "关闭"};重启后重新检查安装、信任和工具策略`, "info");
315
+ return;
316
+ }
317
+ if (!ctx.hasUI || typeof ctx.ui.custom !== "function" || verb === "status") {
318
+ const rows = await mcpManagerRows(await readRecommendationState(ctx.cwd));
319
+ ctx.ui.notify(rows.map((row) => `${row.blocked ? "[!]" : row.enabled ? "[on]" : "[off]"} ${row.id} — ${row.name}${row.reason ? ` — ${row.reason}` : ""}`).join("\n"), "info");
320
+ return;
321
+ }
322
+ while (true) {
323
+ const rows = await mcpManagerRows(await readRecommendationState(ctx.cwd));
324
+ const action = await openMcpManager(ctx, rows);
325
+ if (action.type === "close")
228
326
  return;
327
+ const row = rows.find((candidate) => candidate.id === action.id);
328
+ if (!row)
329
+ continue;
330
+ if (action.type === "install") {
331
+ ctx.ui.notify(JSON.stringify(mcpInstallPlan(row.id, catalog.items, catalog.installPrep), null, 2), "info");
332
+ continue;
229
333
  }
230
- if (actionChoice.startsWith("🌐")) {
231
- ctx.ui.notify(`来源仓库: ${String(selectedItem.sourceRef ?? "无")}`, "info");
232
- return;
334
+ if (action.type === "toggle") {
335
+ await setRecommendedMcpEnabled(ctx.cwd, row.id, action.enabled);
336
+ ctx.ui.notify(`${row.name} 的项目配置已${action.enabled ? "开启" : "关闭"};重启后重新检查运行时可用性`, "info");
233
337
  }
234
- return;
235
338
  }
236
- // 2. 命令行显式参数调用
339
+ }
340
+ async function showSkillManager(args, ctx) {
341
+ const action = args.trim().split(/\s+/).filter(Boolean);
342
+ const verb = action[0];
343
+ const id = action[1];
344
+ if (verb && !["status", "enable", "disable", "install"].includes(verb)) {
345
+ throw new Error("Usage: /skills [status|enable <id>|disable <id>|install <id>]");
346
+ }
237
347
  if ((verb === "enable" || verb === "disable" || verb === "install") && !id) {
238
- throw new Error(`Usage: /${kind} ${verb} <id>`);
348
+ throw new Error(`Usage: /skills ${verb} <id>`);
349
+ }
350
+ if (verb === "install") {
351
+ const result = await installRecommendedSkill(ctx.cwd, id);
352
+ ctx.ui.notify(`${result.installed ? "已安装" : "已经安装"} ${result.skillName}\n${result.path}\n安装不会自动启用,请返回 /skills 开启。`, "info");
353
+ return;
239
354
  }
240
355
  if (verb === "enable" || verb === "disable") {
241
- const item = items.find((candidate) => candidate.id === id);
242
- if (!item)
243
- throw new Error(`未找到推荐${stateKey === "skill" ? " Skill" : " MCP"}: ${id}`);
244
- if (!ctx.hasUI || !(await ctx.ui.confirm(`${verb === "enable" ? "启用" : "禁用"}推荐${stateKey === "skill" ? " Skill" : " MCP"}`, `${String(item.name)} 的状态将写入当前项目配置。`)))
245
- return;
246
- const current = new Set(state[stateField]);
247
- if (verb === "enable")
248
- current.add(id);
249
- else
250
- current.delete(id);
251
- state[stateField] = [...current];
252
- await saveRecommendationState(ctx.cwd, state);
253
- ctx.ui.notify(`${verb === "enable" ? "已启用" : "已禁用"} ${String(item.name)}(/reload 后生效;打开 /panel 可可视化管理)`, "info");
356
+ await setRecommendedSkillEnabled(ctx.cwd, id, verb === "enable");
357
+ ctx.ui.notify(`${verb === "enable" ? "已启用" : "已停用"} ${normalizeRecommendedSkillId(id)},下次启动 PsyClaw 生效`, "info");
254
358
  return;
255
359
  }
256
- if (verb === "install") {
257
- const item = items.find((candidate) => candidate.id === id);
258
- const plan = installPrep.find((candidate) => candidate.id === id);
259
- if (!item)
260
- throw new Error(`未找到推荐${kind === "skills" ? " Skill" : " MCP"}: ${id}`);
261
- ctx.ui.notify(JSON.stringify({ schemaVersion: "psyclaw/recommendation-install/v1", kind: stateKey, id, name: item.name, sourceRef: item.sourceRef, plan: plan ?? null, status: plan?.status ?? "review-required", next: "安装预检与启用是两个步骤;打开 /panel 查看完整计划,安装完成后再执行启用。" }, null, 2), "info");
360
+ if (!ctx.hasUI || typeof ctx.ui.custom !== "function" || verb === "status") {
361
+ const state = await readRecommendationState(ctx.cwd);
362
+ const rows = await skillManagerRows(ctx.cwd, state);
363
+ const lines = [
364
+ ...coreSkillNames().map((name) => `● 内置 ${name}`),
365
+ ...rows.map((row) => `${skillRowLabel(row)}${row.reason ? ` — ${row.reason}` : ""}`),
366
+ ];
367
+ ctx.ui.notify(lines.join("\n"), "info");
262
368
  return;
263
369
  }
264
- // 3. 非交互模式或 status 命令回退输出
265
- const enabled = new Set(state[stateField]);
266
- const lines = items.map((item) => `${enabled.has(String(item.id)) ? "[on]" : "[off]"} ${String(item.id)} — ${String(item.name)} (/${kind} enable|disable ${String(item.id)})`);
267
- lines.push("", "可视化管理:打开 /panel(推荐 + 现有 Skill/MCP 的启用/停用)");
268
- ctx.ui.notify(lines.length ? lines.join("\n") : `暂无推荐${kind === "skills" ? " Skill" : " MCP"}`, "info");
370
+ while (true) {
371
+ const state = await readRecommendationState(ctx.cwd);
372
+ const rows = await skillManagerRows(ctx.cwd, state);
373
+ const action = await openSkillManager(ctx, rows);
374
+ if (action.type === "close")
375
+ return;
376
+ const row = rows.find((candidate) => candidate.id === action.id);
377
+ if (!row)
378
+ continue;
379
+ if (action.type === "install") {
380
+ const approved = await ctx.ui.confirm("安装推荐 Skill?", `${row.name}\n固定来源:${row.sourceRef ?? row.id}\n安装到当前项目,安装完成后仍由你决定是否启用。`);
381
+ if (!approved)
382
+ continue;
383
+ try {
384
+ const result = await installRecommendedSkill(ctx.cwd, row.id);
385
+ ctx.ui.notify(`${result.installed ? "安装完成" : "已经安装"}:${result.skillName}`, "info");
386
+ }
387
+ catch (error) {
388
+ await notifyError(ctx, error);
389
+ }
390
+ continue;
391
+ }
392
+ if (action.type === "toggle") {
393
+ await setRecommendedSkillEnabled(ctx.cwd, row.id, action.enabled);
394
+ ctx.ui.notify(`已${action.enabled ? "启用" : "停用"} ${row.name},重启 PsyClaw 后生效`, "info");
395
+ }
396
+ }
269
397
  }
270
398
  /** Run meta-analysis with the staged progress captured into a buffer (no TTY). */
271
399
  async function runMetaAnalysisStaged(root, target, nStudies) {
@@ -417,10 +545,10 @@ export default function psyclawExtension(pi) {
417
545
  });
418
546
  if (!legacyTestApi)
419
547
  pi.registerCommand("skills", {
420
- description: "查看并启用/禁用推荐 Skill",
548
+ description: "打开 Skill 安装与启用管理页",
421
549
  handler: async (args, ctx) => {
422
550
  try {
423
- await showRecommended("skills", args, ctx);
551
+ await showSkillManager(args, ctx);
424
552
  }
425
553
  catch (error) {
426
554
  await notifyError(ctx, error);
@@ -429,10 +557,10 @@ export default function psyclawExtension(pi) {
429
557
  });
430
558
  if (!legacyTestApi)
431
559
  pi.registerCommand("mcp", {
432
- description: "查看并启用/禁用推荐 MCP",
560
+ description: "打开 MCP 配置与预检管理页",
433
561
  handler: async (args, ctx) => {
434
562
  try {
435
- await showRecommended("mcp", args, ctx);
563
+ await showMcpManager(args, ctx);
436
564
  }
437
565
  catch (error) {
438
566
  await notifyError(ctx, error);
@@ -455,10 +583,16 @@ export default function psyclawExtension(pi) {
455
583
  if (kind !== "skill" && kind !== "mcp")
456
584
  throw new Error("Usage: /install skill|mcp <id>");
457
585
  if (!id) {
458
- await showRecommended(kind === "skill" ? "skills" : "mcp", "", ctx);
586
+ if (kind === "skill")
587
+ await showSkillManager("", ctx);
588
+ else
589
+ await showMcpManager("", ctx);
459
590
  return;
460
591
  }
461
- await showRecommended(kind === "skill" ? "skills" : "mcp", `install ${id}`, ctx);
592
+ if (kind === "skill")
593
+ await showSkillManager(`install ${id}`, ctx);
594
+ else
595
+ await showMcpManager(`install ${id}`, ctx);
462
596
  }
463
597
  catch (error) {
464
598
  await notifyError(ctx, error);
@@ -529,6 +663,27 @@ export default function psyclawExtension(pi) {
529
663
  ctx.ui.notify(`启动横幅宠物已${action === "on" ? "开启" : "关闭"},下次启动生效`, "info");
530
664
  },
531
665
  });
666
+ const traceCommand = {
667
+ description: "导出脱敏使用路径,供 Langfuse 或 LangSmith 分析",
668
+ handler: async (args, ctx) => {
669
+ try {
670
+ if (args.trim())
671
+ throw new Error("Usage: /trace");
672
+ const result = await exportTraces({ root: ctx.cwd });
673
+ ctx.ui.notify([
674
+ "使用路径已导出(未上传)",
675
+ `文件:${result.output}`,
676
+ `轨迹:${result.traces}`,
677
+ `步骤:${result.spans}`,
678
+ "隐私:不含对话正文、工具参数、原始 ID 或绝对路径",
679
+ ].join("\n"), "info");
680
+ }
681
+ catch (error) {
682
+ await notifyError(ctx, error);
683
+ }
684
+ },
685
+ };
686
+ pi.registerCommand("trace", traceCommand);
532
687
  if (typeof pi.registerTool === "function") {
533
688
  pi.registerTool({
534
689
  name: "psyclaw_skill",