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.
- package/README.md +35 -8
- package/dist/apps/panel/index.html +58 -15
- package/dist/src/adapters/pi/extension.js +322 -120
- package/dist/src/adapters/pi/extension.js.map +1 -1
- package/dist/src/branding.d.ts +9 -0
- package/dist/src/branding.js +24 -1
- package/dist/src/branding.js.map +1 -1
- package/dist/src/chat.d.ts +3 -0
- package/dist/src/chat.js +17 -6
- package/dist/src/chat.js.map +1 -1
- package/dist/src/cli.js +20 -8
- package/dist/src/cli.js.map +1 -1
- package/dist/src/core/docx.js +2 -2
- package/dist/src/core/docx.js.map +1 -1
- package/dist/src/index.d.ts +2 -0
- package/dist/src/index.js +2 -0
- package/dist/src/index.js.map +1 -1
- package/dist/src/panel/extension.js +3 -1
- package/dist/src/panel/extension.js.map +1 -1
- package/dist/src/panel/server.d.ts +2 -0
- package/dist/src/panel/server.js +66 -9
- package/dist/src/panel/server.js.map +1 -1
- package/dist/src/setup.d.ts +1 -1
- package/dist/src/setup.js +12 -4
- package/dist/src/setup.js.map +1 -1
- package/dist/src/skills/recommended.d.ts +84 -0
- package/dist/src/skills/recommended.js +339 -0
- package/dist/src/skills/recommended.js.map +1 -0
- package/dist/src/style/cli-ui.js +2 -1
- package/dist/src/style/cli-ui.js.map +1 -1
- package/dist/src/telemetry/export.d.ts +24 -0
- package/dist/src/telemetry/export.js +264 -0
- package/dist/src/telemetry/export.js.map +1 -0
- package/dist/src/tui/skill-manager.d.ts +49 -0
- package/dist/src/tui/skill-manager.js +133 -0
- package/dist/src/tui/skill-manager.js.map +1 -0
- package/dist/src/updates/registry.d.ts +1 -0
- package/dist/src/updates/registry.js +19 -0
- package/dist/src/updates/registry.js.map +1 -1
- package/dist/src/updates/update.d.ts +32 -4
- package/dist/src/updates/update.js +163 -6
- package/dist/src/updates/update.js.map +1 -1
- package/package.json +4 -3
- package/scripts/rebrand-pi.mjs +16 -12
- package/skills/core/citation-audit/SKILL.md +9 -9
- package/skills/core/evidence-capture/SKILL.md +9 -9
- package/skills/core/manifest.json +15 -15
- package/skills/core/research-brief/SKILL.md +8 -8
- package/skills/core/research-intake/SKILL.md +9 -9
- 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
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
const
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
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
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
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
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
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 (
|
|
231
|
-
ctx.
|
|
232
|
-
|
|
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:
|
|
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
|
-
|
|
242
|
-
|
|
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 === "
|
|
257
|
-
const
|
|
258
|
-
const
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
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
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
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: "
|
|
594
|
+
description: "打开 Skill 安装与启用管理页",
|
|
421
595
|
handler: async (args, ctx) => {
|
|
422
596
|
try {
|
|
423
|
-
await
|
|
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: "
|
|
606
|
+
description: "打开 MCP 配置与预检管理页",
|
|
433
607
|
handler: async (args, ctx) => {
|
|
434
608
|
try {
|
|
435
|
-
await
|
|
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
|
-
|
|
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
|
-
|
|
633
|
+
if (kind === "skill")
|
|
634
|
+
await showSkillManager(pi, "", ctx);
|
|
635
|
+
else
|
|
636
|
+
await showMcpManager("", ctx);
|
|
459
637
|
return;
|
|
460
638
|
}
|
|
461
|
-
|
|
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",
|