psyclaw 0.27.6 → 0.27.7
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 +7 -7
- package/dist/apps/panel/index.html +11 -11
- package/dist/src/adapters/pi/extension.js +336 -67
- package/dist/src/adapters/pi/extension.js.map +1 -1
- package/dist/src/cli.js +1 -4
- package/dist/src/cli.js.map +1 -1
- package/dist/src/index.d.ts +1 -0
- package/dist/src/index.js +1 -0
- package/dist/src/index.js.map +1 -1
- package/dist/src/integrations/mcp-runtime.d.ts +13 -0
- package/dist/src/integrations/mcp-runtime.js +48 -0
- package/dist/src/integrations/mcp-runtime.js.map +1 -1
- package/dist/src/panel/events.d.ts +2 -1
- package/dist/src/panel/events.js +2 -5
- package/dist/src/panel/events.js.map +1 -1
- package/dist/src/panel/server.js +28 -8
- package/dist/src/panel/server.js.map +1 -1
- package/dist/src/project/bootstrap.d.ts +17 -0
- package/dist/src/project/bootstrap.js +87 -1
- package/dist/src/project/bootstrap.js.map +1 -1
- package/dist/src/skills/user-skills.d.ts +57 -0
- package/dist/src/skills/user-skills.js +294 -0
- package/dist/src/skills/user-skills.js.map +1 -0
- package/dist/src/style/cli-ui.js +1 -1
- package/dist/src/style/cli-ui.js.map +1 -1
- package/dist/src/telemetry/export.d.ts +8 -6
- package/dist/src/telemetry/export.js +81 -18
- package/dist/src/telemetry/export.js.map +1 -1
- package/dist/src/tui/skill-manager.d.ts +3 -0
- package/dist/src/tui/skill-manager.js +11 -1
- package/dist/src/tui/skill-manager.js.map +1 -1
- package/package.json +1 -1
- package/scripts/rebrand-pi.mjs +66 -4
- package/skills/core/citation-audit/SKILL.md +9 -9
- package/skills/core/evidence-capture/SKILL.md +9 -9
- package/skills/core/research-brief/SKILL.md +8 -8
- package/skills/core/research-intake/SKILL.md +9 -9
|
@@ -6,14 +6,14 @@ import { atomicWriteFile } from "../../project/jsonl.js";
|
|
|
6
6
|
import { RunEventLog } from "../../panel/events.js";
|
|
7
7
|
import { readProject } from "../../research/ledger.js";
|
|
8
8
|
import { join } from "node:path";
|
|
9
|
-
import { mkdir, readFile } from "node:fs/promises";
|
|
10
|
-
import { homedir } from "node:os";
|
|
9
|
+
import { lstat, mkdir, readFile } from "node:fs/promises";
|
|
11
10
|
import { PROVIDER_PRESETS, saveProviderConfig } from "../../setup.js";
|
|
12
11
|
import { dirname } from "node:path";
|
|
13
12
|
import { fileURLToPath } from "node:url";
|
|
14
13
|
import { coreSkillNames, enabledRecommendedSkillPaths, normalizeRecommendedSkillId, readRecommendationState, readRecommendedCatalog, recommendedSkillTarget, saveRecommendationState, validateModelInstalledRecommendedSkill, } from "../../skills/recommended.js";
|
|
15
14
|
import { SkillManagerComponent } from "../../tui/skill-manager.js";
|
|
16
|
-
import {
|
|
15
|
+
import { enabledLocalSkillPaths, enabledLocalPromptPaths, readUserSkillState, scanLocalSkills, setLocalSkillEnabled, setLocalSkillsEnabled, skillNamesInPaths, userSkillId, } from "../../skills/user-skills.js";
|
|
16
|
+
import { RuntimeMcpRegistry, setUserMcpConfigEnabled, } from "../../integrations/mcp-runtime.js";
|
|
17
17
|
import { ProviderPickerComponent, SecretInputComponent, } from "../../tui/provider-picker.js";
|
|
18
18
|
const PARADIGMS = new Set([
|
|
19
19
|
"survey-observational",
|
|
@@ -165,28 +165,32 @@ async function readControlledRun(root) {
|
|
|
165
165
|
try {
|
|
166
166
|
const value = JSON.parse(await readFile(await controlledRunPath(root), "utf8"));
|
|
167
167
|
return value.schemaVersion === "psyclaw/controlled-run/v1" && value.status === "active" && typeof value.projectId === "string" && typeof value.objective === "string" && typeof value.activatedAt === "string"
|
|
168
|
-
? value
|
|
168
|
+
? { ...value, selectedSkills: Array.isArray(value.selectedSkills) ? value.selectedSkills.filter((item) => typeof item === "string") : [] }
|
|
169
169
|
: null;
|
|
170
170
|
}
|
|
171
171
|
catch {
|
|
172
172
|
return null;
|
|
173
173
|
}
|
|
174
174
|
}
|
|
175
|
-
async function activateControlledRun(root, projectId, objective) {
|
|
175
|
+
async function activateControlledRun(root, projectId, objective, selectedSkills) {
|
|
176
176
|
const state = {
|
|
177
177
|
schemaVersion: "psyclaw/controlled-run/v1",
|
|
178
178
|
projectId,
|
|
179
179
|
objective,
|
|
180
|
+
selectedSkills: [...new Set(selectedSkills)],
|
|
180
181
|
activatedAt: new Date().toISOString(),
|
|
181
182
|
status: "active",
|
|
182
183
|
};
|
|
183
184
|
await atomicWriteFile(await controlledRunPath(root), `${JSON.stringify(state, null, 2)}\n`);
|
|
184
185
|
return state;
|
|
185
186
|
}
|
|
186
|
-
function controlledRunRequest(objective) {
|
|
187
|
+
function controlledRunRequest(objective, selectedSkills) {
|
|
187
188
|
return [
|
|
188
189
|
"PsyClaw 受控研究流程已由用户通过 /run 明确启动。先读取 .psyclaw/project.json、.psyclaw/controlled-run.json、notes/research-spec.md 和 notes/plan.md。",
|
|
189
190
|
`本次目标:${objective}`,
|
|
191
|
+
selectedSkills.length > 0
|
|
192
|
+
? `本次运行由用户选择的优化 Skill:${selectedSkills.join(", ")}。在相关任务中优先加载并遵循这些 Skill;同名 Skill 只加载一次。`
|
|
193
|
+
: "本次运行未指定额外 Skill,使用 PsyClaw 默认研究流程。不要删除、停用或改写已安装的其他 Skill。",
|
|
190
194
|
"实际推进当前尚未完成的研究任务;先形成或更新符合学术规范的 Markdown 分析报告,不能只描述计划。分析报告必须清楚区分数据来源、统计结果、文献证据、限制与尚未核验内容。",
|
|
191
195
|
"分析报告完成后,下一步不是直接询问是否导出 DOCX,而是询问用户是否据此撰写论文。",
|
|
192
196
|
"若用户选择撰写论文,再单独询问是否先进行文献调研,并说明该阶段可能花费较长时间;提示用户可以指定已启用的 Skill,或者选择 PsyClaw 默认的文献调研方式。不要在获得答复前自动开始长时调研。",
|
|
@@ -196,6 +200,53 @@ function controlledRunRequest(objective) {
|
|
|
196
200
|
"如果用户在未完成文献调研、全文写作或评审前要求导出 DOCX,允许导出当前分析报告,但必须明确标为遵循学术规范的分析报告,不得称为论文。",
|
|
197
201
|
].join("\n");
|
|
198
202
|
}
|
|
203
|
+
function parseRunArgs(args) {
|
|
204
|
+
const value = args.trim();
|
|
205
|
+
if (!value.startsWith("--skills"))
|
|
206
|
+
return { objective: value };
|
|
207
|
+
const match = value.match(/^--skills(?:=|\s+)([^\s]+)(?:\s+([\s\S]*))?$/);
|
|
208
|
+
if (!match)
|
|
209
|
+
throw new Error("Usage: /run [--skills skill-a,skill-b] [objective]");
|
|
210
|
+
return {
|
|
211
|
+
objective: (match[2] ?? "").trim(),
|
|
212
|
+
requestedSkills: [...new Set(match[1].split(",").map((item) => item.trim()).filter(Boolean))],
|
|
213
|
+
};
|
|
214
|
+
}
|
|
215
|
+
async function selectableRunSkills(root) {
|
|
216
|
+
const state = await readRecommendationState(root);
|
|
217
|
+
const rows = await skillManagerRows(root, state);
|
|
218
|
+
const available = [
|
|
219
|
+
...coreSkillNames().map((name) => ({ id: name, name })),
|
|
220
|
+
...rows.filter((row) => row.installed && row.enabled && !row.blocked).map((row) => ({ id: row.id, name: row.name })),
|
|
221
|
+
];
|
|
222
|
+
const seen = new Set();
|
|
223
|
+
return available.filter((skill) => {
|
|
224
|
+
const key = skill.name.trim().toLocaleLowerCase();
|
|
225
|
+
if (!key || seen.has(key))
|
|
226
|
+
return false;
|
|
227
|
+
seen.add(key);
|
|
228
|
+
return true;
|
|
229
|
+
});
|
|
230
|
+
}
|
|
231
|
+
function resolveRunSkills(requested, available) {
|
|
232
|
+
const lookup = new Map();
|
|
233
|
+
for (const skill of available) {
|
|
234
|
+
lookup.set(skill.id.toLocaleLowerCase(), skill.name);
|
|
235
|
+
lookup.set(skill.name.toLocaleLowerCase(), skill.name);
|
|
236
|
+
}
|
|
237
|
+
const selected = [];
|
|
238
|
+
const unknown = [];
|
|
239
|
+
for (const value of requested) {
|
|
240
|
+
const name = lookup.get(value.toLocaleLowerCase());
|
|
241
|
+
if (!name)
|
|
242
|
+
unknown.push(value);
|
|
243
|
+
else if (!selected.some((item) => item.toLocaleLowerCase() === name.toLocaleLowerCase()))
|
|
244
|
+
selected.push(name);
|
|
245
|
+
}
|
|
246
|
+
if (unknown.length > 0)
|
|
247
|
+
throw new Error(`本次运行不可用的 Skill:${unknown.join(", ")}。请先通过 /skills 安装或启用。`);
|
|
248
|
+
return selected;
|
|
249
|
+
}
|
|
199
250
|
async function recommendedItems(kind) {
|
|
200
251
|
const file = kind === "skills" ? "catalog.json" : "mcp-catalog.json";
|
|
201
252
|
const moduleDir = dirname(fileURLToPath(import.meta.url));
|
|
@@ -219,11 +270,13 @@ async function recommendedItems(kind) {
|
|
|
219
270
|
return { items: [], externalTools: [], installPrep: [] };
|
|
220
271
|
}
|
|
221
272
|
function skillScopeLabel(scope) {
|
|
273
|
+
if (scope === "local")
|
|
274
|
+
return "用户本地目录(所有项目可发现)";
|
|
222
275
|
return scope === "user" ? "系统目录(所有项目)" : "项目目录(仅当前项目)";
|
|
223
276
|
}
|
|
224
277
|
async function skillManagerRows(root, state) {
|
|
225
278
|
const catalog = await readRecommendedCatalog();
|
|
226
|
-
|
|
279
|
+
const recommendedRows = await Promise.all(catalog.items.filter((item) => item.kind === "skill").map(async (item) => {
|
|
227
280
|
const id = normalizeRecommendedSkillId(item.id);
|
|
228
281
|
const scope = state.skillScopes?.[id] ?? "project";
|
|
229
282
|
try {
|
|
@@ -239,9 +292,21 @@ async function skillManagerRows(root, state) {
|
|
|
239
292
|
enabled: state.skills.includes(id),
|
|
240
293
|
scope,
|
|
241
294
|
blocked: false,
|
|
295
|
+
source: "recommended",
|
|
242
296
|
};
|
|
243
297
|
}
|
|
244
|
-
catch {
|
|
298
|
+
catch (validationError) {
|
|
299
|
+
// A managed target that exists on disk but fails validation (e.g. a URL
|
|
300
|
+
// install whose manifest/hash does not match the pinned catalog) is still
|
|
301
|
+
// shown as installed so the user sees it; enabling it only takes effect
|
|
302
|
+
// once the install is repaired through /install.
|
|
303
|
+
let onDisk = false;
|
|
304
|
+
try {
|
|
305
|
+
const target = recommendedSkillTarget(root, id, scope);
|
|
306
|
+
const stat = await lstat(target);
|
|
307
|
+
onDisk = stat.isDirectory();
|
|
308
|
+
}
|
|
309
|
+
catch { /* target missing */ }
|
|
245
310
|
return {
|
|
246
311
|
id,
|
|
247
312
|
name: item.name,
|
|
@@ -249,20 +314,50 @@ async function skillManagerRows(root, state) {
|
|
|
249
314
|
...(item.sourceRef === undefined ? {} : { sourceRef: item.sourceRef }),
|
|
250
315
|
...(typeof item.installHint === "string" ? { installHint: item.installHint } : {}),
|
|
251
316
|
collection: item.skillLayout === "collection",
|
|
252
|
-
installed:
|
|
253
|
-
enabled:
|
|
317
|
+
installed: onDisk,
|
|
318
|
+
enabled: onDisk && state.skills.includes(id),
|
|
254
319
|
scope,
|
|
255
320
|
blocked: typeof item.sourceRef !== "string" || !/^https:\/\//.test(item.sourceRef),
|
|
256
|
-
reason:
|
|
257
|
-
? "
|
|
258
|
-
: "
|
|
321
|
+
reason: onDisk
|
|
322
|
+
? `已检测到目录但未通过来源/许可/哈希校验${state.skills.includes(id) ? "(当前配置为启用,但校验失败不会加载)" : ""};可按 Enter 重新安装修复`
|
|
323
|
+
: typeof item.sourceRef === "string" && /^https:\/\//.test(item.sourceRef)
|
|
324
|
+
? "尚未安装;按 Enter 选择安装范围并交给当前模型处理"
|
|
325
|
+
: "没有可交给模型检查的来源网址",
|
|
326
|
+
source: "recommended",
|
|
259
327
|
};
|
|
260
328
|
}
|
|
261
329
|
}));
|
|
330
|
+
// Merge user-installed local skills from the expanded discovery roots so the
|
|
331
|
+
// management page shows them and lets the user toggle each one. Managed
|
|
332
|
+
// recommended installs are excluded here — they are shown through their
|
|
333
|
+
// recommended rows above (including on-disk-but-unvalidated installs).
|
|
334
|
+
const userState = await readUserSkillState(root);
|
|
335
|
+
const disabled = new Set(userState.disabled);
|
|
336
|
+
const localSkills = await scanLocalSkills(root, { includeManaged: false });
|
|
337
|
+
const localRows = localSkills.map((skill) => ({
|
|
338
|
+
id: userSkillId(skill.name),
|
|
339
|
+
name: skill.name,
|
|
340
|
+
description: skill.description,
|
|
341
|
+
sourceRef: skill.path,
|
|
342
|
+
collection: false,
|
|
343
|
+
installed: true,
|
|
344
|
+
enabled: !disabled.has(skill.name),
|
|
345
|
+
scope: "local",
|
|
346
|
+
blocked: false,
|
|
347
|
+
source: "local",
|
|
348
|
+
duplicatePaths: skill.duplicatePaths,
|
|
349
|
+
}));
|
|
350
|
+
// A catalog skill that is actually installed is shown once through its
|
|
351
|
+
// recommended row (which carries source/trust info); purely local skills —
|
|
352
|
+
// including a local copy of a catalog skill that failed managed validation —
|
|
353
|
+
// are appended so they stay visible and toggleable.
|
|
354
|
+
const installedRecommendedNames = new Set(recommendedRows.filter((row) => row.installed).map((row) => row.name.toLocaleLowerCase()));
|
|
355
|
+
const uniqueLocalRows = localRows.filter((row) => !installedRecommendedNames.has(row.name.toLocaleLowerCase()));
|
|
356
|
+
return [...recommendedRows, ...uniqueLocalRows];
|
|
262
357
|
}
|
|
263
|
-
async function mcpManagerRows(state) {
|
|
358
|
+
async function mcpManagerRows(root, state, runtime) {
|
|
264
359
|
const { items, installPrep } = await recommendedItems("mcp");
|
|
265
|
-
|
|
360
|
+
const recommendedRows = items.map((item) => {
|
|
266
361
|
const id = String(item.id ?? "");
|
|
267
362
|
const plan = installPrep.find((candidate) => candidate.id === id);
|
|
268
363
|
const dependencies = Array.isArray(plan?.dependencies)
|
|
@@ -274,6 +369,7 @@ async function mcpManagerRows(state) {
|
|
|
274
369
|
description: String(item.description ?? ""),
|
|
275
370
|
...(typeof item.sourceRef === "string" ? { sourceRef: item.sourceRef } : {}),
|
|
276
371
|
enabled: state.mcp.includes(id),
|
|
372
|
+
source: "recommended",
|
|
277
373
|
details: [
|
|
278
374
|
`传输:${String(item.transport ?? "未声明")} · 风险:${String(item.risk ?? "未声明")}`,
|
|
279
375
|
`参考版本:${String(plan?.ref ?? "由模型读取来源确定")} · 许可证:${String(plan?.license ?? "由模型读取来源确定")}`,
|
|
@@ -281,6 +377,31 @@ async function mcpManagerRows(state) {
|
|
|
281
377
|
],
|
|
282
378
|
};
|
|
283
379
|
});
|
|
380
|
+
// Merge user-configured MCP servers (`.psyclaw/mcp/*.json` and
|
|
381
|
+
// `~/.psyclaw/mcp/*.json`) so the management page shows them too, even when
|
|
382
|
+
// disabled. User rows win over a same-id recommended row.
|
|
383
|
+
const userRows = (await runtime.listUserConfigs(root)).map((entry) => ({
|
|
384
|
+
id: entry.id,
|
|
385
|
+
name: entry.name,
|
|
386
|
+
description: `用户配置:${entry.command}`,
|
|
387
|
+
enabled: entry.enabled,
|
|
388
|
+
source: "user",
|
|
389
|
+
userEntry: entry,
|
|
390
|
+
details: [
|
|
391
|
+
`传输:stdio · 范围:${entry.scope === "user" ? "用户级" : "项目级"}`,
|
|
392
|
+
`配置:${entry.path}`,
|
|
393
|
+
],
|
|
394
|
+
}));
|
|
395
|
+
const merged = new Map(recommendedRows.map((row) => [row.id, row]));
|
|
396
|
+
for (const row of userRows)
|
|
397
|
+
merged.set(row.id, row);
|
|
398
|
+
return [...merged.values()].sort((left, right) => {
|
|
399
|
+
if (left.source === "user" && right.source !== "user")
|
|
400
|
+
return -1;
|
|
401
|
+
if (right.source === "user" && left.source !== "user")
|
|
402
|
+
return 1;
|
|
403
|
+
return left.id.localeCompare(right.id);
|
|
404
|
+
});
|
|
284
405
|
}
|
|
285
406
|
function skillRowLabel(row) {
|
|
286
407
|
const state = row.blocked ? "× 阻断" : row.enabled ? "● 已启用" : row.installed ? "○ 未启用" : "↓ 未安装";
|
|
@@ -316,7 +437,12 @@ function skillManagerItems(rows) {
|
|
|
316
437
|
description: row.description,
|
|
317
438
|
status: row.blocked ? "blocked" : row.enabled ? "enabled" : row.installed ? "disabled" : "missing",
|
|
318
439
|
...(row.sourceRef === undefined ? {} : { sourceRef: row.sourceRef }),
|
|
319
|
-
details: [
|
|
440
|
+
details: [
|
|
441
|
+
`安装位置:${skillScopeLabel(row.scope)}`,
|
|
442
|
+
...(row.duplicatePaths && row.duplicatePaths.length > 0
|
|
443
|
+
? [`已忽略 ${row.duplicatePaths.length} 个同名来源`]
|
|
444
|
+
: []),
|
|
445
|
+
],
|
|
320
446
|
...(row.reason === undefined ? {} : { reason: row.reason }),
|
|
321
447
|
})),
|
|
322
448
|
];
|
|
@@ -339,18 +465,14 @@ async function openMcpManager(ctx, rows) {
|
|
|
339
465
|
return ctx.ui.custom((tui, theme, keybindings, done) => (new SkillManagerComponent(mcpManagerItems(rows), tui, theme, keybindings, done, {
|
|
340
466
|
title: "MCP 管理",
|
|
341
467
|
itemLabel: "MCP",
|
|
342
|
-
footer: "↑/↓ 移动 · Space
|
|
468
|
+
footer: "↑/↓ 移动 · Space 开启/关闭 · a 全部开启 · d 全部关闭 · Enter 交给模型安装并配置 · Esc 关闭",
|
|
343
469
|
enterAction: "install",
|
|
344
|
-
enabledText: "
|
|
345
|
-
disabledText: "
|
|
470
|
+
enabledText: "已开启;Enter 可让当前模型重新安装或修复推荐配置",
|
|
471
|
+
disabledText: "已关闭",
|
|
346
472
|
})));
|
|
347
473
|
}
|
|
348
474
|
async function setRecommendedMcpEnabled(root, id, enabled) {
|
|
349
475
|
const state = await readRecommendationState(root);
|
|
350
|
-
const rows = await mcpManagerRows(state);
|
|
351
|
-
const row = rows.find((candidate) => candidate.id === id);
|
|
352
|
-
if (!row)
|
|
353
|
-
throw new Error(`未找到推荐 MCP: ${id}`);
|
|
354
476
|
const current = new Set(state.mcp);
|
|
355
477
|
if (enabled)
|
|
356
478
|
current.add(id);
|
|
@@ -359,6 +481,9 @@ async function setRecommendedMcpEnabled(root, id, enabled) {
|
|
|
359
481
|
state.mcp = [...current];
|
|
360
482
|
await saveRecommendationState(root, state);
|
|
361
483
|
}
|
|
484
|
+
async function setUserMcpEnabled(entry, enabled) {
|
|
485
|
+
await setUserMcpConfigEnabled(entry, enabled);
|
|
486
|
+
}
|
|
362
487
|
function modelMcpInstallTask(root, row, plan) {
|
|
363
488
|
const suggestedCommand = typeof plan?.command === "string" ? plan.command : "请根据来源仓库的最新安装说明确定";
|
|
364
489
|
const suggestedTarget = typeof plan?.target === "string" ? plan.target : `.psyclaw/mcp/${row.id}.json`;
|
|
@@ -381,48 +506,86 @@ async function queueModelMcpInstall(pi, ctx, row, plan) {
|
|
|
381
506
|
pi.sendUserMessage(modelMcpInstallTask(ctx.cwd, row, plan), ctx.isIdle() ? {} : { deliverAs: "followUp" });
|
|
382
507
|
ctx.ui.notify(`已将 ${row.name} 的下载、安装和配置任务交给当前模型。模型完成并确认可启动后,请执行 /reload。`, "info");
|
|
383
508
|
}
|
|
384
|
-
async function showMcpManager(pi, args, ctx) {
|
|
509
|
+
async function showMcpManager(pi, args, ctx, runtime) {
|
|
385
510
|
const [verb, id] = args.trim().split(/\s+/).filter(Boolean);
|
|
386
|
-
if (verb && !["status", "enable", "disable", "install"].includes(verb)) {
|
|
387
|
-
throw new Error("Usage: /mcp [status|enable <id>|disable <id>|install <id>]");
|
|
511
|
+
if (verb && !["status", "enable", "disable", "enable-all", "disable-all", "install"].includes(verb)) {
|
|
512
|
+
throw new Error("Usage: /mcp [status|enable <id>|disable <id>|enable-all|disable-all|install <id>]");
|
|
388
513
|
}
|
|
389
514
|
if ((verb === "enable" || verb === "disable" || verb === "install") && !id) {
|
|
390
515
|
throw new Error(`Usage: /mcp ${verb} <id>`);
|
|
391
516
|
}
|
|
392
517
|
const catalog = await recommendedItems("mcp");
|
|
393
518
|
if (verb === "install") {
|
|
394
|
-
const rows = await mcpManagerRows(await readRecommendationState(ctx.cwd));
|
|
519
|
+
const rows = await mcpManagerRows(ctx.cwd, await readRecommendationState(ctx.cwd), runtime);
|
|
395
520
|
const row = rows.find((candidate) => candidate.id === id);
|
|
396
521
|
if (!row)
|
|
397
522
|
throw new Error(`未找到推荐 MCP: ${id}`);
|
|
523
|
+
if (row.source !== "recommended")
|
|
524
|
+
throw new Error(`MCP ${id} 已是用户配置,无需再次安装`);
|
|
398
525
|
await queueModelMcpInstall(pi, ctx, row, catalog.installPrep.find((candidate) => candidate.id === row.id));
|
|
399
526
|
return;
|
|
400
527
|
}
|
|
528
|
+
if (verb === "enable-all" || verb === "disable-all") {
|
|
529
|
+
const enabled = verb === "enable-all";
|
|
530
|
+
const rows = await mcpManagerRows(ctx.cwd, await readRecommendationState(ctx.cwd), runtime);
|
|
531
|
+
for (const row of rows) {
|
|
532
|
+
if (row.source === "user" && row.userEntry)
|
|
533
|
+
await setUserMcpEnabled(row.userEntry, enabled);
|
|
534
|
+
else
|
|
535
|
+
await setRecommendedMcpEnabled(ctx.cwd, row.id, enabled);
|
|
536
|
+
}
|
|
537
|
+
ctx.ui.notify(`已批量${enabled ? "开启" : "关闭"}全部 MCP 配置;重启后重新检查运行时可用性`, "info");
|
|
538
|
+
return;
|
|
539
|
+
}
|
|
401
540
|
if (verb === "enable" || verb === "disable") {
|
|
402
|
-
await
|
|
403
|
-
|
|
541
|
+
const rows = await mcpManagerRows(ctx.cwd, await readRecommendationState(ctx.cwd), runtime);
|
|
542
|
+
const row = rows.find((candidate) => candidate.id === id);
|
|
543
|
+
if (!row)
|
|
544
|
+
throw new Error(`未找到 MCP: ${id}`);
|
|
545
|
+
if (row.source === "user" && row.userEntry)
|
|
546
|
+
await setUserMcpEnabled(row.userEntry, verb === "enable");
|
|
547
|
+
else
|
|
548
|
+
await setRecommendedMcpEnabled(ctx.cwd, row.id, verb === "enable");
|
|
549
|
+
ctx.ui.notify(`MCP ${id} 已${verb === "enable" ? "开启" : "关闭"};重启后重新检查安装、信任和工具策略`, "info");
|
|
404
550
|
return;
|
|
405
551
|
}
|
|
406
552
|
if (!ctx.hasUI || typeof ctx.ui.custom !== "function" || verb === "status") {
|
|
407
|
-
const rows = await mcpManagerRows(await readRecommendationState(ctx.cwd));
|
|
408
|
-
ctx.ui.notify(rows.map((row) => `${row.enabled ? "[on]" : "[off]"} ${row.id} — ${row.name}`).join("\n"), "info");
|
|
553
|
+
const rows = await mcpManagerRows(ctx.cwd, await readRecommendationState(ctx.cwd), runtime);
|
|
554
|
+
ctx.ui.notify(rows.map((row) => `${row.enabled ? "[on]" : "[off]"} ${row.id} — ${row.name}${row.source === "user" ? "(用户配置)" : ""}`).join("\n"), "info");
|
|
409
555
|
return;
|
|
410
556
|
}
|
|
411
557
|
while (true) {
|
|
412
|
-
const rows = await mcpManagerRows(await readRecommendationState(ctx.cwd));
|
|
558
|
+
const rows = await mcpManagerRows(ctx.cwd, await readRecommendationState(ctx.cwd), runtime);
|
|
413
559
|
const action = await openMcpManager(ctx, rows);
|
|
414
560
|
if (action.type === "close")
|
|
415
561
|
return;
|
|
562
|
+
if (action.type === "toggle-all") {
|
|
563
|
+
for (const row of rows) {
|
|
564
|
+
if (row.source === "user" && row.userEntry)
|
|
565
|
+
await setUserMcpEnabled(row.userEntry, action.enabled);
|
|
566
|
+
else
|
|
567
|
+
await setRecommendedMcpEnabled(ctx.cwd, row.id, action.enabled);
|
|
568
|
+
}
|
|
569
|
+
ctx.ui.notify(`已批量${action.enabled ? "开启" : "关闭"}全部 MCP 配置;重启后重新检查运行时可用性`, "info");
|
|
570
|
+
continue;
|
|
571
|
+
}
|
|
416
572
|
const row = rows.find((candidate) => candidate.id === action.id);
|
|
417
573
|
if (!row)
|
|
418
574
|
continue;
|
|
419
575
|
if (action.type === "install") {
|
|
576
|
+
if (row.source !== "recommended") {
|
|
577
|
+
ctx.ui.notify(`${row.name} 已是用户配置,无需再次安装`, "info");
|
|
578
|
+
continue;
|
|
579
|
+
}
|
|
420
580
|
await queueModelMcpInstall(pi, ctx, row, catalog.installPrep.find((candidate) => candidate.id === row.id));
|
|
421
581
|
return;
|
|
422
582
|
}
|
|
423
583
|
if (action.type === "toggle") {
|
|
424
|
-
|
|
425
|
-
|
|
584
|
+
if (row.source === "user" && row.userEntry)
|
|
585
|
+
await setUserMcpEnabled(row.userEntry, action.enabled);
|
|
586
|
+
else
|
|
587
|
+
await setRecommendedMcpEnabled(ctx.cwd, row.id, action.enabled);
|
|
588
|
+
ctx.ui.notify(`${row.name} 已${action.enabled ? "开启" : "关闭"};重启后重新检查运行时可用性`, "info");
|
|
426
589
|
}
|
|
427
590
|
}
|
|
428
591
|
}
|
|
@@ -471,8 +634,8 @@ async function showSkillManager(pi, args, ctx) {
|
|
|
471
634
|
const action = args.trim().split(/\s+/).filter(Boolean);
|
|
472
635
|
const verb = action[0];
|
|
473
636
|
const id = action[1];
|
|
474
|
-
if (verb && !["status", "enable", "disable", "install"].includes(verb)) {
|
|
475
|
-
throw new Error("Usage: /skills [status|enable <id>|disable <id>|install <id>]");
|
|
637
|
+
if (verb && !["status", "enable", "disable", "enable-all", "disable-all", "install"].includes(verb)) {
|
|
638
|
+
throw new Error("Usage: /skills [status|enable <id>|disable <id>|enable-all|disable-all|install <id>]");
|
|
476
639
|
}
|
|
477
640
|
if ((verb === "enable" || verb === "disable" || verb === "install") && !id) {
|
|
478
641
|
throw new Error(`Usage: /skills ${verb} <id>`);
|
|
@@ -484,9 +647,44 @@ async function showSkillManager(pi, args, ctx) {
|
|
|
484
647
|
await queueModelSkillInstall(pi, ctx, row);
|
|
485
648
|
return;
|
|
486
649
|
}
|
|
650
|
+
if (verb === "enable-all" || verb === "disable-all") {
|
|
651
|
+
const enabled = verb === "enable-all";
|
|
652
|
+
const state = await readRecommendationState(ctx.cwd);
|
|
653
|
+
const rows = await skillManagerRows(ctx.cwd, state);
|
|
654
|
+
const managed = rows.filter((row) => row.source === "recommended" && row.installed && !row.blocked);
|
|
655
|
+
const locals = rows.filter((row) => row.source === "local");
|
|
656
|
+
const failed = [];
|
|
657
|
+
for (const row of managed) {
|
|
658
|
+
try {
|
|
659
|
+
await setRecommendedSkillEnabled(ctx.cwd, row.id, enabled);
|
|
660
|
+
}
|
|
661
|
+
catch {
|
|
662
|
+
if (enabled)
|
|
663
|
+
failed.push(row.name);
|
|
664
|
+
}
|
|
665
|
+
}
|
|
666
|
+
if (locals.length > 0) {
|
|
667
|
+
await setLocalSkillsEnabled(ctx.cwd, locals.map((row) => row.name), enabled);
|
|
668
|
+
}
|
|
669
|
+
ctx.ui.notify(enabled && failed.length > 0
|
|
670
|
+
? `已批量启用(${failed.join("、")} 启用失败)。请运行 /reload 重新加载。`
|
|
671
|
+
: `已批量${enabled ? "启用" : "停用"}可管理的 Skill。请运行 /reload 重新加载。`, enabled && failed.length > 0 ? "warning" : "info");
|
|
672
|
+
return;
|
|
673
|
+
}
|
|
487
674
|
if (verb === "enable" || verb === "disable") {
|
|
488
|
-
|
|
489
|
-
|
|
675
|
+
const requested = normalizeRecommendedSkillId(id);
|
|
676
|
+
const state = await readRecommendationState(ctx.cwd);
|
|
677
|
+
const rows = await skillManagerRows(ctx.cwd, state);
|
|
678
|
+
const row = rows.find((candidate) => candidate.id === requested || candidate.name === requested || candidate.id === userSkillId(requested));
|
|
679
|
+
if (!row)
|
|
680
|
+
throw new Error(`未找到 Skill: ${id}`);
|
|
681
|
+
if (row.source === "local") {
|
|
682
|
+
await setLocalSkillEnabled(ctx.cwd, row.name, verb === "enable");
|
|
683
|
+
}
|
|
684
|
+
else {
|
|
685
|
+
await setRecommendedSkillEnabled(ctx.cwd, row.id, verb === "enable");
|
|
686
|
+
}
|
|
687
|
+
ctx.ui.notify(`${verb === "enable" ? "已启用" : "已停用"} ${row.name}。请运行 /reload 重新加载。`, "info");
|
|
490
688
|
return;
|
|
491
689
|
}
|
|
492
690
|
if (!ctx.hasUI || typeof ctx.ui.custom !== "function" || verb === "status") {
|
|
@@ -505,6 +703,42 @@ async function showSkillManager(pi, args, ctx) {
|
|
|
505
703
|
const action = await openSkillManager(ctx, rows);
|
|
506
704
|
if (action.type === "close")
|
|
507
705
|
return;
|
|
706
|
+
if (action.type === "toggle-all") {
|
|
707
|
+
const managed = rows.filter((row) => row.source === "recommended" && row.installed && !row.blocked);
|
|
708
|
+
const locals = rows.filter((row) => row.source === "local");
|
|
709
|
+
if (action.enabled) {
|
|
710
|
+
const failed = [];
|
|
711
|
+
for (const row of managed) {
|
|
712
|
+
try {
|
|
713
|
+
await setRecommendedSkillEnabled(ctx.cwd, row.id, true);
|
|
714
|
+
}
|
|
715
|
+
catch {
|
|
716
|
+
failed.push(row.name);
|
|
717
|
+
}
|
|
718
|
+
}
|
|
719
|
+
if (locals.length > 0) {
|
|
720
|
+
const ids = locals.map((row) => row.name);
|
|
721
|
+
await setLocalSkillsEnabled(ctx.cwd, ids, true);
|
|
722
|
+
}
|
|
723
|
+
ctx.ui.notify(failed.length > 0
|
|
724
|
+
? `已启用全部可管理的 Skill(${failed.join("、")} 启用失败:可能未通过来源、许可或依赖预检)。请运行 /reload 重新加载。`
|
|
725
|
+
: `已启用全部可管理的 Skill。请运行 /reload 重新加载。`, failed.length > 0 ? "warning" : "info");
|
|
726
|
+
}
|
|
727
|
+
else {
|
|
728
|
+
for (const row of managed) {
|
|
729
|
+
try {
|
|
730
|
+
await setRecommendedSkillEnabled(ctx.cwd, row.id, false);
|
|
731
|
+
}
|
|
732
|
+
catch { /* already disabled */ }
|
|
733
|
+
}
|
|
734
|
+
if (locals.length > 0) {
|
|
735
|
+
const ids = locals.map((row) => row.name);
|
|
736
|
+
await setLocalSkillsEnabled(ctx.cwd, ids, false);
|
|
737
|
+
}
|
|
738
|
+
ctx.ui.notify("已停用全部可管理的 Skill。请运行 /reload 重新加载。", "info");
|
|
739
|
+
}
|
|
740
|
+
continue;
|
|
741
|
+
}
|
|
508
742
|
const row = rows.find((candidate) => candidate.id === action.id);
|
|
509
743
|
if (!row)
|
|
510
744
|
continue;
|
|
@@ -513,8 +747,14 @@ async function showSkillManager(pi, args, ctx) {
|
|
|
513
747
|
return;
|
|
514
748
|
}
|
|
515
749
|
if (action.type === "toggle") {
|
|
516
|
-
|
|
517
|
-
|
|
750
|
+
if (row.source === "local") {
|
|
751
|
+
await setLocalSkillEnabled(ctx.cwd, row.name, action.enabled);
|
|
752
|
+
ctx.ui.notify(`已${action.enabled ? "启用" : "停用"}本地 Skill ${row.name}。请运行 /reload 重新加载。`, "info");
|
|
753
|
+
}
|
|
754
|
+
else {
|
|
755
|
+
await setRecommendedSkillEnabled(ctx.cwd, row.id, action.enabled);
|
|
756
|
+
ctx.ui.notify(`已${action.enabled ? "启用" : "停用"} ${row.name}。请运行 /reload 重新加载。`, "info");
|
|
757
|
+
}
|
|
518
758
|
}
|
|
519
759
|
}
|
|
520
760
|
}
|
|
@@ -622,17 +862,17 @@ export default function psyclawExtension(pi) {
|
|
|
622
862
|
const enabled = await enabledRecommendedSkillPaths(event.cwd);
|
|
623
863
|
for (const warning of enabled.warnings)
|
|
624
864
|
ctx.ui.notify(`PsyClaw Skill: ${warning}`, "warning");
|
|
625
|
-
const
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
865
|
+
const local = await enabledLocalSkillPaths(event.cwd, {
|
|
866
|
+
excludedNames: [...coreSkillNames(), ...await skillNamesInPaths(enabled.paths)],
|
|
867
|
+
});
|
|
868
|
+
for (const warning of local.warnings)
|
|
869
|
+
ctx.ui.notify(`PsyClaw Skill: ${warning}`, "warning");
|
|
870
|
+
// Return only existing Skill directories. Same-name local/core candidates
|
|
871
|
+
// are resolved before Pi sees them, so harmless collisions stay silent.
|
|
872
|
+
return {
|
|
873
|
+
skillPaths: [...enabled.paths, ...local.paths],
|
|
874
|
+
promptPaths: await enabledLocalPromptPaths(event.cwd),
|
|
875
|
+
};
|
|
636
876
|
});
|
|
637
877
|
if (!legacyTestApi && typeof pi.on === "function")
|
|
638
878
|
pi.on("session_shutdown", () => runtimeMcps.close());
|
|
@@ -657,18 +897,47 @@ export default function psyclawExtension(pi) {
|
|
|
657
897
|
});
|
|
658
898
|
if (!legacyTestApi)
|
|
659
899
|
pi.registerCommand("run", {
|
|
660
|
-
description: "
|
|
900
|
+
description: "启动受控研究流程,可用 --skills a,b 选择本次优化 Skill",
|
|
661
901
|
handler: async (args, ctx) => {
|
|
662
902
|
try {
|
|
663
|
-
const
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
903
|
+
const parsed = parseRunArgs(args);
|
|
904
|
+
let project;
|
|
905
|
+
try {
|
|
906
|
+
project = await readProject(ctx.cwd);
|
|
907
|
+
}
|
|
908
|
+
catch (error) {
|
|
909
|
+
// No `.psyclaw/project.json` (or it is corrupt): fall back to the
|
|
910
|
+
// compliant analysis documents when present, so a project prepared
|
|
911
|
+
// outside the /init flow can be analyzed directly.
|
|
912
|
+
const { bootstrapProjectFromAnalysisDocs } = await import("../../project/bootstrap.js");
|
|
913
|
+
try {
|
|
914
|
+
project = await bootstrapProjectFromAnalysisDocs(ctx.cwd);
|
|
915
|
+
}
|
|
916
|
+
catch (bootstrapError) {
|
|
917
|
+
ctx.ui.notify(bootstrapError instanceof Error ? bootstrapError.message : String(bootstrapError), "warning");
|
|
918
|
+
return;
|
|
919
|
+
}
|
|
920
|
+
ctx.ui.notify("检测到合规的分析文档,已自动补齐研究项目记录并启动受控流程。", "info");
|
|
921
|
+
}
|
|
922
|
+
const available = await selectableRunSkills(ctx.cwd);
|
|
923
|
+
let requestedSkills = parsed.requestedSkills;
|
|
924
|
+
if (requestedSkills === undefined && ctx.hasUI && typeof ctx.ui.input === "function") {
|
|
925
|
+
const answer = await ctx.ui.input("选择本次 Run 使用的 Skill(逗号分隔,可留空)", available.map((skill) => skill.name).join(", "));
|
|
926
|
+
if (answer === undefined) {
|
|
927
|
+
ctx.ui.notify("已取消启动受控研究流程", "info");
|
|
928
|
+
return;
|
|
929
|
+
}
|
|
930
|
+
requestedSkills = answer.split(",").map((item) => item.trim()).filter(Boolean);
|
|
931
|
+
}
|
|
932
|
+
const selectedSkills = resolveRunSkills(requestedSkills ?? [], available);
|
|
933
|
+
const objective = parsed.objective || project.goal;
|
|
934
|
+
await activateControlledRun(ctx.cwd, project.id, objective, selectedSkills);
|
|
935
|
+
pi.appendEntry("psyclaw:controlled-run", { projectId: project.id, objective, selectedSkills, activatedAt: new Date().toISOString() });
|
|
936
|
+
pi.sendUserMessage(controlledRunRequest(objective, selectedSkills), ctx.isIdle() ? {} : { deliverAs: "followUp" });
|
|
937
|
+
ctx.ui.notify(`受控研究流程已启动${selectedSkills.length > 0 ? `;本次使用 Skill:${selectedSkills.join(", ")}` : ";使用默认 Skill"}。分析完成后会先询问是否撰写论文。`, "info");
|
|
669
938
|
}
|
|
670
|
-
catch {
|
|
671
|
-
ctx
|
|
939
|
+
catch (error) {
|
|
940
|
+
await notifyError(ctx, error);
|
|
672
941
|
}
|
|
673
942
|
},
|
|
674
943
|
});
|
|
@@ -831,7 +1100,7 @@ export default function psyclawExtension(pi) {
|
|
|
831
1100
|
description: "打开 MCP 安装与配置管理页",
|
|
832
1101
|
handler: async (args, ctx) => {
|
|
833
1102
|
try {
|
|
834
|
-
await showMcpManager(pi, args, ctx);
|
|
1103
|
+
await showMcpManager(pi, args, ctx, runtimeMcps);
|
|
835
1104
|
}
|
|
836
1105
|
catch (error) {
|
|
837
1106
|
await notifyError(ctx, error);
|
|
@@ -858,13 +1127,13 @@ export default function psyclawExtension(pi) {
|
|
|
858
1127
|
if (kind === "skill")
|
|
859
1128
|
await showSkillManager(pi, "", ctx);
|
|
860
1129
|
else
|
|
861
|
-
await showMcpManager(pi, "", ctx);
|
|
1130
|
+
await showMcpManager(pi, "", ctx, runtimeMcps);
|
|
862
1131
|
return;
|
|
863
1132
|
}
|
|
864
1133
|
if (kind === "skill")
|
|
865
1134
|
await showSkillManager(pi, `install ${id}`, ctx);
|
|
866
1135
|
else
|
|
867
|
-
await showMcpManager(pi, `install ${id}`, ctx);
|
|
1136
|
+
await showMcpManager(pi, `install ${id}`, ctx, runtimeMcps);
|
|
868
1137
|
}
|
|
869
1138
|
catch (error) {
|
|
870
1139
|
await notifyError(ctx, error);
|
|
@@ -972,18 +1241,18 @@ export default function psyclawExtension(pi) {
|
|
|
972
1241
|
},
|
|
973
1242
|
});
|
|
974
1243
|
const traceCommand = {
|
|
975
|
-
description: "
|
|
1244
|
+
description: "导出包含正文与工具参数的完整使用路径,供 Langfuse 或 LangSmith 分析",
|
|
976
1245
|
handler: async (args, ctx) => {
|
|
977
1246
|
try {
|
|
978
1247
|
if (args.trim())
|
|
979
|
-
throw new Error("Usage: /
|
|
1248
|
+
throw new Error("Usage: /export");
|
|
980
1249
|
const result = await exportTraces({ root: ctx.cwd });
|
|
981
1250
|
ctx.ui.notify([
|
|
982
1251
|
"使用路径已导出(未上传)",
|
|
983
1252
|
`文件:${result.output}`,
|
|
984
1253
|
`轨迹:${result.traces}`,
|
|
985
1254
|
`步骤:${result.spans}`,
|
|
986
|
-
"
|
|
1255
|
+
"包含对话正文、工具参数、原始 ID 与绝对路径,便于排查各环节问题;请勿将导出文件提交到公开仓库。",
|
|
987
1256
|
].join("\n"), "info");
|
|
988
1257
|
}
|
|
989
1258
|
catch (error) {
|
|
@@ -991,7 +1260,7 @@ export default function psyclawExtension(pi) {
|
|
|
991
1260
|
}
|
|
992
1261
|
},
|
|
993
1262
|
};
|
|
994
|
-
pi.registerCommand("
|
|
1263
|
+
pi.registerCommand("export", traceCommand);
|
|
995
1264
|
if (typeof pi.registerTool === "function") {
|
|
996
1265
|
pi.registerTool({
|
|
997
1266
|
name: "psyclaw_mcp",
|