psyclaw 0.26.3 → 0.27.4

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 (50) hide show
  1. package/README.md +35 -8
  2. package/dist/apps/panel/index.html +58 -15
  3. package/dist/src/adapters/pi/extension.js +322 -120
  4. package/dist/src/adapters/pi/extension.js.map +1 -1
  5. package/dist/src/branding.d.ts +9 -0
  6. package/dist/src/branding.js +24 -1
  7. package/dist/src/branding.js.map +1 -1
  8. package/dist/src/chat.d.ts +3 -0
  9. package/dist/src/chat.js +17 -6
  10. package/dist/src/chat.js.map +1 -1
  11. package/dist/src/cli.js +20 -8
  12. package/dist/src/cli.js.map +1 -1
  13. package/dist/src/core/docx.js +2 -2
  14. package/dist/src/core/docx.js.map +1 -1
  15. package/dist/src/index.d.ts +2 -0
  16. package/dist/src/index.js +2 -0
  17. package/dist/src/index.js.map +1 -1
  18. package/dist/src/panel/extension.js +3 -1
  19. package/dist/src/panel/extension.js.map +1 -1
  20. package/dist/src/panel/server.d.ts +2 -0
  21. package/dist/src/panel/server.js +66 -9
  22. package/dist/src/panel/server.js.map +1 -1
  23. package/dist/src/setup.d.ts +1 -1
  24. package/dist/src/setup.js +12 -4
  25. package/dist/src/setup.js.map +1 -1
  26. package/dist/src/skills/recommended.d.ts +84 -0
  27. package/dist/src/skills/recommended.js +339 -0
  28. package/dist/src/skills/recommended.js.map +1 -0
  29. package/dist/src/style/cli-ui.js +2 -1
  30. package/dist/src/style/cli-ui.js.map +1 -1
  31. package/dist/src/telemetry/export.d.ts +24 -0
  32. package/dist/src/telemetry/export.js +264 -0
  33. package/dist/src/telemetry/export.js.map +1 -0
  34. package/dist/src/tui/skill-manager.d.ts +49 -0
  35. package/dist/src/tui/skill-manager.js +133 -0
  36. package/dist/src/tui/skill-manager.js.map +1 -0
  37. package/dist/src/updates/registry.d.ts +1 -0
  38. package/dist/src/updates/registry.js +19 -0
  39. package/dist/src/updates/registry.js.map +1 -1
  40. package/dist/src/updates/update.d.ts +32 -4
  41. package/dist/src/updates/update.js +163 -6
  42. package/dist/src/updates/update.js.map +1 -1
  43. package/package.json +4 -3
  44. package/scripts/rebrand-pi.mjs +16 -12
  45. package/skills/core/citation-audit/SKILL.md +9 -9
  46. package/skills/core/evidence-capture/SKILL.md +9 -9
  47. package/skills/core/manifest.json +15 -15
  48. package/skills/core/research-brief/SKILL.md +8 -8
  49. package/skills/core/research-intake/SKILL.md +9 -9
  50. package/skills/recommended/catalog.json +163 -43
@@ -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, enabledRecommendedSkillPaths, normalizeRecommendedSkillId, readRecommendationState, readRecommendedCatalog, recommendedSkillTarget, saveRecommendationState, validateModelInstalledRecommendedSkill, } 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
  }
@@ -147,125 +136,303 @@ async function recommendedItems(kind) {
147
136
  const value = JSON.parse(await readFile(path, "utf8"));
148
137
  return {
149
138
  items: Array.isArray(value.items) ? value.items.filter((item) => Boolean(item && typeof item === "object")) : [],
139
+ externalTools: Array.isArray(value.externalTools) ? value.externalTools.filter((item) => Boolean(item && typeof item === "object")) : [],
150
140
  installPrep: Array.isArray(value.installPrep) ? value.installPrep.filter((item) => Boolean(item && typeof item === "object")) : [],
151
141
  };
152
142
  }
153
143
  catch { /* try next candidate */ }
154
144
  }
155
- return { items: [], installPrep: [] };
145
+ return { items: [], externalTools: [], installPrep: [] };
156
146
  }
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;
147
+ function skillScopeLabel(scope) {
148
+ return scope === "user" ? "系统目录(所有项目)" : "项目目录(仅当前项目)";
149
+ }
150
+ async function skillManagerRows(root, state) {
151
+ const catalog = await readRecommendedCatalog();
152
+ return Promise.all(catalog.items.filter((item) => item.kind === "skill").map(async (item) => {
153
+ const id = normalizeRecommendedSkillId(item.id);
154
+ const scope = state.skillScopes?.[id] ?? "project";
155
+ try {
156
+ await validateModelInstalledRecommendedSkill(root, id, scope);
157
+ return {
158
+ id,
159
+ name: item.name,
160
+ description: String(item.description ?? ""),
161
+ ...(item.sourceRef === undefined ? {} : { sourceRef: item.sourceRef }),
162
+ installed: true,
163
+ enabled: state.skills.includes(id),
164
+ scope,
165
+ blocked: false,
166
+ };
173
167
  }
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;
168
+ catch {
169
+ return {
170
+ id,
171
+ name: item.name,
172
+ description: String(item.description ?? ""),
173
+ ...(item.sourceRef === undefined ? {} : { sourceRef: item.sourceRef }),
174
+ installed: false,
175
+ enabled: false,
176
+ scope,
177
+ blocked: typeof item.sourceRef !== "string" || !/^https:\/\//.test(item.sourceRef),
178
+ reason: typeof item.sourceRef === "string" && /^https:\/\//.test(item.sourceRef)
179
+ ? "尚未安装;按 Enter 选择安装范围并交给当前模型处理"
180
+ : "没有可交给模型检查的来源网址",
181
+ };
215
182
  }
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");
183
+ }));
184
+ }
185
+ async function mcpManagerRows(state) {
186
+ const { items, installPrep } = await recommendedItems("mcp");
187
+ return items.map((item) => {
188
+ const id = String(item.id ?? "");
189
+ const plan = installPrep.find((candidate) => candidate.id === id);
190
+ const blocked = plan?.status === "blocked";
191
+ const dependencies = Array.isArray(plan?.dependencies)
192
+ ? plan.dependencies.filter((value) => typeof value === "string")
193
+ : [];
194
+ return {
195
+ id,
196
+ name: String(item.name ?? id),
197
+ description: String(item.description ?? ""),
198
+ ...(typeof item.sourceRef === "string" ? { sourceRef: item.sourceRef } : {}),
199
+ enabled: state.mcp.includes(id),
200
+ blocked,
201
+ ...(typeof plan?.blockedReason === "string" ? { reason: plan.blockedReason } : {}),
202
+ details: [
203
+ `传输:${String(item.transport ?? "未声明")} · 风险:${String(item.risk ?? "未声明")}`,
204
+ `版本:${String(plan?.ref ?? "未固定")} · 许可证:${String(plan?.license ?? "未核验")}`,
205
+ `依赖:${dependencies.length > 0 ? dependencies.join(", ") : "无已声明依赖"}`,
206
+ ],
207
+ };
208
+ });
209
+ }
210
+ function skillRowLabel(row) {
211
+ const state = row.blocked ? "× 阻断" : row.enabled ? "● 已启用" : row.installed ? "○ 未启用" : "↓ 未安装";
212
+ return `${state} ${row.name} [${row.id}]`;
213
+ }
214
+ async function setRecommendedSkillEnabled(root, requestedId, enabled, requestedScope) {
215
+ const id = normalizeRecommendedSkillId(requestedId);
216
+ const state = await readRecommendationState(root);
217
+ const scope = requestedScope ?? state.skillScopes?.[id] ?? "project";
218
+ if (enabled)
219
+ await validateModelInstalledRecommendedSkill(root, id, scope);
220
+ const current = new Set(state.skills);
221
+ if (enabled)
222
+ current.add(id);
223
+ else
224
+ current.delete(id);
225
+ state.skills = [...current];
226
+ state.skillScopes = { ...(state.skillScopes ?? {}), [id]: scope };
227
+ await saveRecommendationState(root, state);
228
+ }
229
+ function skillManagerItems(rows) {
230
+ return [
231
+ ...coreSkillNames().map((name) => ({
232
+ id: `core:${name}`,
233
+ name,
234
+ description: "PsyClaw 核心研究流程 Skill。",
235
+ status: "core",
236
+ sourceRef: "PsyClaw core",
237
+ })),
238
+ ...rows.map((row) => ({
239
+ id: row.id,
240
+ name: row.name,
241
+ description: row.description,
242
+ status: row.blocked ? "blocked" : row.enabled ? "enabled" : row.installed ? "disabled" : "missing",
243
+ ...(row.sourceRef === undefined ? {} : { sourceRef: row.sourceRef }),
244
+ details: [`安装位置:${skillScopeLabel(row.scope)}`],
245
+ ...(row.reason === undefined ? {} : { reason: row.reason }),
246
+ })),
247
+ ];
248
+ }
249
+ async function openSkillManager(ctx, rows) {
250
+ return ctx.ui.custom((tui, theme, keybindings, done) => (new SkillManagerComponent(skillManagerItems(rows), tui, theme, keybindings, done)));
251
+ }
252
+ function mcpManagerItems(rows) {
253
+ return rows.map((row) => ({
254
+ id: row.id,
255
+ name: row.name,
256
+ description: row.description,
257
+ status: row.blocked ? "blocked" : row.enabled ? "enabled" : "disabled",
258
+ configuredEnabled: row.enabled,
259
+ ...(row.sourceRef === undefined ? {} : { sourceRef: row.sourceRef }),
260
+ ...(row.reason === undefined ? {} : { reason: row.reason }),
261
+ details: row.details,
262
+ }));
263
+ }
264
+ async function openMcpManager(ctx, rows) {
265
+ return ctx.ui.custom((tui, theme, keybindings, done) => (new SkillManagerComponent(mcpManagerItems(rows), tui, theme, keybindings, done, {
266
+ title: "MCP 管理",
267
+ itemLabel: "MCP",
268
+ footer: "↑/↓ 移动 · Space 开启/关闭项目配置 · Enter 查看预检 · Esc 关闭",
269
+ enterAction: "install",
270
+ enabledText: "项目配置已开启;运行时仍需通过安装、信任和工具策略检查",
271
+ disabledText: "项目配置已关闭",
272
+ blockedText: "预检阻断;若配置曾开启,可按 Space 关闭",
273
+ })));
274
+ }
275
+ async function setRecommendedMcpEnabled(root, id, enabled) {
276
+ const state = await readRecommendationState(root);
277
+ const rows = await mcpManagerRows(state);
278
+ const row = rows.find((candidate) => candidate.id === id);
279
+ if (!row)
280
+ throw new Error(`未找到推荐 MCP: ${id}`);
281
+ if (enabled && row.blocked)
282
+ throw new Error(row.reason ?? `MCP 预检未通过: ${id}`);
283
+ const current = new Set(state.mcp);
284
+ if (enabled)
285
+ current.add(id);
286
+ else
287
+ current.delete(id);
288
+ state.mcp = [...current];
289
+ await saveRecommendationState(root, state);
290
+ }
291
+ function mcpInstallPlan(id, items, installPrep) {
292
+ const item = items.find((candidate) => candidate.id === id);
293
+ if (!item)
294
+ throw new Error(`未找到推荐 MCP: ${id}`);
295
+ const plan = installPrep.find((candidate) => candidate.id === id);
296
+ return {
297
+ schemaVersion: "psyclaw/recommendation-install/v1",
298
+ kind: "mcp",
299
+ id,
300
+ name: item.name,
301
+ sourceRef: item.sourceRef,
302
+ plan: plan ?? null,
303
+ status: plan?.status ?? "review-required",
304
+ next: "这里只展示固定来源、版本、许可证和依赖预检;完成受信任安装与注册后,MCP 才能在运行时调用。",
305
+ };
306
+ }
307
+ async function showMcpManager(args, ctx) {
308
+ const [verb, id] = args.trim().split(/\s+/).filter(Boolean);
309
+ if (verb && !["status", "enable", "disable", "install"].includes(verb)) {
310
+ throw new Error("Usage: /mcp [status|enable <id>|disable <id>|install <id>]");
311
+ }
312
+ if ((verb === "enable" || verb === "disable" || verb === "install") && !id) {
313
+ throw new Error(`Usage: /mcp ${verb} <id>`);
314
+ }
315
+ const catalog = await recommendedItems("mcp");
316
+ if (verb === "install") {
317
+ ctx.ui.notify(JSON.stringify(mcpInstallPlan(id, catalog.items, catalog.installPrep), null, 2), "info");
318
+ return;
319
+ }
320
+ if (verb === "enable" || verb === "disable") {
321
+ await setRecommendedMcpEnabled(ctx.cwd, id, verb === "enable");
322
+ ctx.ui.notify(`MCP ${id} 的项目配置已${verb === "enable" ? "开启" : "关闭"};重启后重新检查安装、信任和工具策略`, "info");
323
+ return;
324
+ }
325
+ if (!ctx.hasUI || typeof ctx.ui.custom !== "function" || verb === "status") {
326
+ const rows = await mcpManagerRows(await readRecommendationState(ctx.cwd));
327
+ ctx.ui.notify(rows.map((row) => `${row.blocked ? "[!]" : row.enabled ? "[on]" : "[off]"} ${row.id} — ${row.name}${row.reason ? ` — ${row.reason}` : ""}`).join("\n"), "info");
328
+ return;
329
+ }
330
+ while (true) {
331
+ const rows = await mcpManagerRows(await readRecommendationState(ctx.cwd));
332
+ const action = await openMcpManager(ctx, rows);
333
+ if (action.type === "close")
228
334
  return;
335
+ const row = rows.find((candidate) => candidate.id === action.id);
336
+ if (!row)
337
+ continue;
338
+ if (action.type === "install") {
339
+ ctx.ui.notify(JSON.stringify(mcpInstallPlan(row.id, catalog.items, catalog.installPrep), null, 2), "info");
340
+ continue;
229
341
  }
230
- if (actionChoice.startsWith("🌐")) {
231
- ctx.ui.notify(`来源仓库: ${String(selectedItem.sourceRef ?? "无")}`, "info");
232
- return;
342
+ if (action.type === "toggle") {
343
+ await setRecommendedMcpEnabled(ctx.cwd, row.id, action.enabled);
344
+ ctx.ui.notify(`${row.name} 的项目配置已${action.enabled ? "开启" : "关闭"};重启后重新检查运行时可用性`, "info");
233
345
  }
346
+ }
347
+ }
348
+ function modelSkillInstallTask(root, row, scope) {
349
+ const target = recommendedSkillTarget(root, row.id, scope);
350
+ return [
351
+ `安装推荐 Skill:${row.name} (${row.id})。`,
352
+ `来源网址:${row.sourceRef}`,
353
+ `安装位置:${skillScopeLabel(scope)}。`,
354
+ `唯一允许的最终目标目录:${target}`,
355
+ "请使用当前会话的工具检查来源仓库并完成安装。不要写入其他 Skill 目录,不要修改 data/raw、.git 或研究产物。",
356
+ "目标目录最终必须直接包含有效 SKILL.md(YAML frontmatter 至少包含 name 和 description),不得包含 .git、符号链接、凭据或二进制大文件。",
357
+ "如果仓库包含多个 Skill,只安装与此推荐项相符的部分;如果它不是 Skill 或无法合理适配,停止并说明原因,不要伪造 SKILL.md。",
358
+ "安装完成后检查目标目录结构,并提醒用户执行 /skills 启用该项,再执行 /reload。",
359
+ ].join("\n");
360
+ }
361
+ async function chooseSkillScope(ctx) {
362
+ const projectLabel = skillScopeLabel("project");
363
+ const systemLabel = skillScopeLabel("user");
364
+ const selected = await ctx.ui.select("选择 Skill 安装位置", [projectLabel, systemLabel]);
365
+ if (selected === projectLabel)
366
+ return "project";
367
+ if (selected === systemLabel)
368
+ return "user";
369
+ return undefined;
370
+ }
371
+ async function queueModelSkillInstall(pi, ctx, row) {
372
+ if (!row.sourceRef)
373
+ throw new Error(`推荐 Skill 没有来源网址: ${row.id}`);
374
+ const scope = await chooseSkillScope(ctx);
375
+ if (!scope)
234
376
  return;
377
+ const approved = await ctx.ui.confirm("交给当前模型安装 Skill?", `${row.name}\n来源:${row.sourceRef}\n安装位置:${skillScopeLabel(scope)}\n目标目录:${recommendedSkillTarget(ctx.cwd, row.id, scope)}\n模型将检查仓库并使用文件与命令工具完成安装。`);
378
+ if (!approved)
379
+ return;
380
+ const state = await readRecommendationState(ctx.cwd);
381
+ state.skillScopes = { ...(state.skillScopes ?? {}), [row.id]: scope };
382
+ await saveRecommendationState(ctx.cwd, state);
383
+ pi.sendUserMessage(modelSkillInstallTask(ctx.cwd, row, scope), ctx.isIdle() ? {} : { deliverAs: "followUp" });
384
+ ctx.ui.notify(`已将 ${row.name} 的安装任务交给当前模型,目标为${skillScopeLabel(scope)}。安装完成后请在 /skills 中启用,再执行 /reload。`, "info");
385
+ }
386
+ async function showSkillManager(pi, args, ctx) {
387
+ const action = args.trim().split(/\s+/).filter(Boolean);
388
+ const verb = action[0];
389
+ const id = action[1];
390
+ if (verb && !["status", "enable", "disable", "install"].includes(verb)) {
391
+ throw new Error("Usage: /skills [status|enable <id>|disable <id>|install <id>]");
235
392
  }
236
- // 2. 命令行显式参数调用
237
393
  if ((verb === "enable" || verb === "disable" || verb === "install") && !id) {
238
- throw new Error(`Usage: /${kind} ${verb} <id>`);
394
+ throw new Error(`Usage: /skills ${verb} <id>`);
395
+ }
396
+ if (verb === "install") {
397
+ const row = (await skillManagerRows(ctx.cwd, await readRecommendationState(ctx.cwd))).find((candidate) => candidate.id === normalizeRecommendedSkillId(id));
398
+ if (!row)
399
+ throw new Error(`未找到推荐 Skill: ${id}`);
400
+ await queueModelSkillInstall(pi, ctx, row);
401
+ return;
239
402
  }
240
403
  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");
404
+ await setRecommendedSkillEnabled(ctx.cwd, id, verb === "enable");
405
+ ctx.ui.notify(`${verb === "enable" ? "已启用" : "已停用"} ${normalizeRecommendedSkillId(id)}。请运行 /reload 重新加载。`, "info");
254
406
  return;
255
407
  }
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");
408
+ if (!ctx.hasUI || typeof ctx.ui.custom !== "function" || verb === "status") {
409
+ const state = await readRecommendationState(ctx.cwd);
410
+ const rows = await skillManagerRows(ctx.cwd, state);
411
+ const lines = [
412
+ ...coreSkillNames().map((name) => `● 内置 ${name}`),
413
+ ...rows.map((row) => `${skillRowLabel(row)}${row.reason ? ` — ${row.reason}` : ""}`),
414
+ ];
415
+ ctx.ui.notify(lines.join("\n"), "info");
262
416
  return;
263
417
  }
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");
418
+ while (true) {
419
+ const state = await readRecommendationState(ctx.cwd);
420
+ const rows = await skillManagerRows(ctx.cwd, state);
421
+ const action = await openSkillManager(ctx, rows);
422
+ if (action.type === "close")
423
+ return;
424
+ const row = rows.find((candidate) => candidate.id === action.id);
425
+ if (!row)
426
+ continue;
427
+ if (action.type === "install") {
428
+ await queueModelSkillInstall(pi, ctx, row);
429
+ return;
430
+ }
431
+ if (action.type === "toggle") {
432
+ await setRecommendedSkillEnabled(ctx.cwd, row.id, action.enabled);
433
+ ctx.ui.notify(`已${action.enabled ? "启用" : "停用"} ${row.name}。请运行 /reload 重新加载。`, "info");
434
+ }
435
+ }
269
436
  }
270
437
  /** Run meta-analysis with the staged progress captured into a buffer (no TTY). */
271
438
  async function runMetaAnalysisStaged(root, target, nStudies) {
@@ -365,6 +532,13 @@ const WORKFLOW_RUNNERS = {
365
532
  export default function psyclawExtension(pi) {
366
533
  const developerCommands = process.env.PSYCLAW_DEVELOPER_COMMANDS === "1";
367
534
  const legacyTestApi = typeof pi.registerTool !== "function";
535
+ if (!legacyTestApi && typeof pi.on === "function")
536
+ pi.on("resources_discover", async (event, ctx) => {
537
+ const enabled = await enabledRecommendedSkillPaths(event.cwd);
538
+ for (const warning of enabled.warnings)
539
+ ctx.ui.notify(`PsyClaw Skill: ${warning}`, "warning");
540
+ return { skillPaths: enabled.paths };
541
+ });
368
542
  pi.registerCommand("research", {
369
543
  description: "创建可追溯的研究项目",
370
544
  handler: async (args, ctx) => {
@@ -417,10 +591,10 @@ export default function psyclawExtension(pi) {
417
591
  });
418
592
  if (!legacyTestApi)
419
593
  pi.registerCommand("skills", {
420
- description: "查看并启用/禁用推荐 Skill",
594
+ description: "打开 Skill 安装与启用管理页",
421
595
  handler: async (args, ctx) => {
422
596
  try {
423
- await showRecommended("skills", args, ctx);
597
+ await showSkillManager(pi, args, ctx);
424
598
  }
425
599
  catch (error) {
426
600
  await notifyError(ctx, error);
@@ -429,10 +603,10 @@ export default function psyclawExtension(pi) {
429
603
  });
430
604
  if (!legacyTestApi)
431
605
  pi.registerCommand("mcp", {
432
- description: "查看并启用/禁用推荐 MCP",
606
+ description: "打开 MCP 配置与预检管理页",
433
607
  handler: async (args, ctx) => {
434
608
  try {
435
- await showRecommended("mcp", args, ctx);
609
+ await showMcpManager(args, ctx);
436
610
  }
437
611
  catch (error) {
438
612
  await notifyError(ctx, error);
@@ -449,16 +623,23 @@ export default function psyclawExtension(pi) {
449
623
  const [skills, mcps] = await Promise.all([recommendedItems("skills"), recommendedItems("mcp")]);
450
624
  const skillLines = skills.items.slice(0, 8).map((item) => `Skill: ${String(item.id)} — ${String(item.name)}`);
451
625
  const mcpLines = mcps.items.slice(0, 8).map((item) => `MCP: ${String(item.id)} — ${String(item.name)}`);
452
- ctx.ui.notify(["推荐安装入口", "", ...skillLines, ...mcpLines, "", "安装:/install skill|mcp <id>", "管理:/skills 或 /mcp", "也可以打开 /panel 查看推荐页面"].join("\n"), "info");
626
+ const toolLines = skills.externalTools.slice(0, 8).map((item) => `外部工具: ${String(item.name)} — ${String(item.sourceRef ?? "请查看项目文档")}`);
627
+ ctx.ui.notify(["推荐安装入口", "", ...skillLines, ...mcpLines, "", ...toolLines, "", "安装:/install skill|mcp <id>", "管理:/skills 或 /mcp", "外部工具不通过 Skill 安装器安装", "也可以打开 /panel 查看推荐页面"].join("\n"), "info");
453
628
  return;
454
629
  }
455
630
  if (kind !== "skill" && kind !== "mcp")
456
631
  throw new Error("Usage: /install skill|mcp <id>");
457
632
  if (!id) {
458
- await showRecommended(kind === "skill" ? "skills" : "mcp", "", ctx);
633
+ if (kind === "skill")
634
+ await showSkillManager(pi, "", ctx);
635
+ else
636
+ await showMcpManager("", ctx);
459
637
  return;
460
638
  }
461
- await showRecommended(kind === "skill" ? "skills" : "mcp", `install ${id}`, ctx);
639
+ if (kind === "skill")
640
+ await showSkillManager(pi, `install ${id}`, ctx);
641
+ else
642
+ await showMcpManager(`install ${id}`, ctx);
462
643
  }
463
644
  catch (error) {
464
645
  await notifyError(ctx, error);
@@ -529,6 +710,27 @@ export default function psyclawExtension(pi) {
529
710
  ctx.ui.notify(`启动横幅宠物已${action === "on" ? "开启" : "关闭"},下次启动生效`, "info");
530
711
  },
531
712
  });
713
+ const traceCommand = {
714
+ description: "导出脱敏使用路径,供 Langfuse 或 LangSmith 分析",
715
+ handler: async (args, ctx) => {
716
+ try {
717
+ if (args.trim())
718
+ throw new Error("Usage: /trace");
719
+ const result = await exportTraces({ root: ctx.cwd });
720
+ ctx.ui.notify([
721
+ "使用路径已导出(未上传)",
722
+ `文件:${result.output}`,
723
+ `轨迹:${result.traces}`,
724
+ `步骤:${result.spans}`,
725
+ "隐私:不含对话正文、工具参数、原始 ID 或绝对路径",
726
+ ].join("\n"), "info");
727
+ }
728
+ catch (error) {
729
+ await notifyError(ctx, error);
730
+ }
731
+ },
732
+ };
733
+ pi.registerCommand("trace", traceCommand);
532
734
  if (typeof pi.registerTool === "function") {
533
735
  pi.registerTool({
534
736
  name: "psyclaw_skill",