psyclaw 0.27.5 → 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 +9 -7
- package/dist/apps/panel/index.html +11 -11
- package/dist/src/adapters/pi/extension.js +383 -55
- package/dist/src/adapters/pi/extension.js.map +1 -1
- package/dist/src/chat.js +2 -2
- package/dist/src/chat.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 +26 -0
- package/dist/src/integrations/mcp-runtime.js +154 -0
- package/dist/src/integrations/mcp-runtime.js.map +1 -0
- 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,12 +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";
|
|
9
|
+
import { lstat, mkdir, readFile } from "node:fs/promises";
|
|
10
10
|
import { PROVIDER_PRESETS, saveProviderConfig } from "../../setup.js";
|
|
11
11
|
import { dirname } from "node:path";
|
|
12
12
|
import { fileURLToPath } from "node:url";
|
|
13
13
|
import { coreSkillNames, enabledRecommendedSkillPaths, normalizeRecommendedSkillId, readRecommendationState, readRecommendedCatalog, recommendedSkillTarget, saveRecommendationState, validateModelInstalledRecommendedSkill, } from "../../skills/recommended.js";
|
|
14
14
|
import { SkillManagerComponent } from "../../tui/skill-manager.js";
|
|
15
|
+
import { enabledLocalSkillPaths, enabledLocalPromptPaths, readUserSkillState, scanLocalSkills, setLocalSkillEnabled, setLocalSkillsEnabled, skillNamesInPaths, userSkillId, } from "../../skills/user-skills.js";
|
|
16
|
+
import { RuntimeMcpRegistry, setUserMcpConfigEnabled, } from "../../integrations/mcp-runtime.js";
|
|
15
17
|
import { ProviderPickerComponent, SecretInputComponent, } from "../../tui/provider-picker.js";
|
|
16
18
|
const PARADIGMS = new Set([
|
|
17
19
|
"survey-observational",
|
|
@@ -163,28 +165,32 @@ async function readControlledRun(root) {
|
|
|
163
165
|
try {
|
|
164
166
|
const value = JSON.parse(await readFile(await controlledRunPath(root), "utf8"));
|
|
165
167
|
return value.schemaVersion === "psyclaw/controlled-run/v1" && value.status === "active" && typeof value.projectId === "string" && typeof value.objective === "string" && typeof value.activatedAt === "string"
|
|
166
|
-
? value
|
|
168
|
+
? { ...value, selectedSkills: Array.isArray(value.selectedSkills) ? value.selectedSkills.filter((item) => typeof item === "string") : [] }
|
|
167
169
|
: null;
|
|
168
170
|
}
|
|
169
171
|
catch {
|
|
170
172
|
return null;
|
|
171
173
|
}
|
|
172
174
|
}
|
|
173
|
-
async function activateControlledRun(root, projectId, objective) {
|
|
175
|
+
async function activateControlledRun(root, projectId, objective, selectedSkills) {
|
|
174
176
|
const state = {
|
|
175
177
|
schemaVersion: "psyclaw/controlled-run/v1",
|
|
176
178
|
projectId,
|
|
177
179
|
objective,
|
|
180
|
+
selectedSkills: [...new Set(selectedSkills)],
|
|
178
181
|
activatedAt: new Date().toISOString(),
|
|
179
182
|
status: "active",
|
|
180
183
|
};
|
|
181
184
|
await atomicWriteFile(await controlledRunPath(root), `${JSON.stringify(state, null, 2)}\n`);
|
|
182
185
|
return state;
|
|
183
186
|
}
|
|
184
|
-
function controlledRunRequest(objective) {
|
|
187
|
+
function controlledRunRequest(objective, selectedSkills) {
|
|
185
188
|
return [
|
|
186
189
|
"PsyClaw 受控研究流程已由用户通过 /run 明确启动。先读取 .psyclaw/project.json、.psyclaw/controlled-run.json、notes/research-spec.md 和 notes/plan.md。",
|
|
187
190
|
`本次目标:${objective}`,
|
|
191
|
+
selectedSkills.length > 0
|
|
192
|
+
? `本次运行由用户选择的优化 Skill:${selectedSkills.join(", ")}。在相关任务中优先加载并遵循这些 Skill;同名 Skill 只加载一次。`
|
|
193
|
+
: "本次运行未指定额外 Skill,使用 PsyClaw 默认研究流程。不要删除、停用或改写已安装的其他 Skill。",
|
|
188
194
|
"实际推进当前尚未完成的研究任务;先形成或更新符合学术规范的 Markdown 分析报告,不能只描述计划。分析报告必须清楚区分数据来源、统计结果、文献证据、限制与尚未核验内容。",
|
|
189
195
|
"分析报告完成后,下一步不是直接询问是否导出 DOCX,而是询问用户是否据此撰写论文。",
|
|
190
196
|
"若用户选择撰写论文,再单独询问是否先进行文献调研,并说明该阶段可能花费较长时间;提示用户可以指定已启用的 Skill,或者选择 PsyClaw 默认的文献调研方式。不要在获得答复前自动开始长时调研。",
|
|
@@ -194,6 +200,53 @@ function controlledRunRequest(objective) {
|
|
|
194
200
|
"如果用户在未完成文献调研、全文写作或评审前要求导出 DOCX,允许导出当前分析报告,但必须明确标为遵循学术规范的分析报告,不得称为论文。",
|
|
195
201
|
].join("\n");
|
|
196
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
|
+
}
|
|
197
250
|
async function recommendedItems(kind) {
|
|
198
251
|
const file = kind === "skills" ? "catalog.json" : "mcp-catalog.json";
|
|
199
252
|
const moduleDir = dirname(fileURLToPath(import.meta.url));
|
|
@@ -217,11 +270,13 @@ async function recommendedItems(kind) {
|
|
|
217
270
|
return { items: [], externalTools: [], installPrep: [] };
|
|
218
271
|
}
|
|
219
272
|
function skillScopeLabel(scope) {
|
|
273
|
+
if (scope === "local")
|
|
274
|
+
return "用户本地目录(所有项目可发现)";
|
|
220
275
|
return scope === "user" ? "系统目录(所有项目)" : "项目目录(仅当前项目)";
|
|
221
276
|
}
|
|
222
277
|
async function skillManagerRows(root, state) {
|
|
223
278
|
const catalog = await readRecommendedCatalog();
|
|
224
|
-
|
|
279
|
+
const recommendedRows = await Promise.all(catalog.items.filter((item) => item.kind === "skill").map(async (item) => {
|
|
225
280
|
const id = normalizeRecommendedSkillId(item.id);
|
|
226
281
|
const scope = state.skillScopes?.[id] ?? "project";
|
|
227
282
|
try {
|
|
@@ -237,9 +292,21 @@ async function skillManagerRows(root, state) {
|
|
|
237
292
|
enabled: state.skills.includes(id),
|
|
238
293
|
scope,
|
|
239
294
|
blocked: false,
|
|
295
|
+
source: "recommended",
|
|
240
296
|
};
|
|
241
297
|
}
|
|
242
|
-
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 */ }
|
|
243
310
|
return {
|
|
244
311
|
id,
|
|
245
312
|
name: item.name,
|
|
@@ -247,20 +314,50 @@ async function skillManagerRows(root, state) {
|
|
|
247
314
|
...(item.sourceRef === undefined ? {} : { sourceRef: item.sourceRef }),
|
|
248
315
|
...(typeof item.installHint === "string" ? { installHint: item.installHint } : {}),
|
|
249
316
|
collection: item.skillLayout === "collection",
|
|
250
|
-
installed:
|
|
251
|
-
enabled:
|
|
317
|
+
installed: onDisk,
|
|
318
|
+
enabled: onDisk && state.skills.includes(id),
|
|
252
319
|
scope,
|
|
253
320
|
blocked: typeof item.sourceRef !== "string" || !/^https:\/\//.test(item.sourceRef),
|
|
254
|
-
reason:
|
|
255
|
-
? "
|
|
256
|
-
: "
|
|
321
|
+
reason: onDisk
|
|
322
|
+
? `已检测到目录但未通过来源/许可/哈希校验${state.skills.includes(id) ? "(当前配置为启用,但校验失败不会加载)" : ""};可按 Enter 重新安装修复`
|
|
323
|
+
: typeof item.sourceRef === "string" && /^https:\/\//.test(item.sourceRef)
|
|
324
|
+
? "尚未安装;按 Enter 选择安装范围并交给当前模型处理"
|
|
325
|
+
: "没有可交给模型检查的来源网址",
|
|
326
|
+
source: "recommended",
|
|
257
327
|
};
|
|
258
328
|
}
|
|
259
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];
|
|
260
357
|
}
|
|
261
|
-
async function mcpManagerRows(state) {
|
|
358
|
+
async function mcpManagerRows(root, state, runtime) {
|
|
262
359
|
const { items, installPrep } = await recommendedItems("mcp");
|
|
263
|
-
|
|
360
|
+
const recommendedRows = items.map((item) => {
|
|
264
361
|
const id = String(item.id ?? "");
|
|
265
362
|
const plan = installPrep.find((candidate) => candidate.id === id);
|
|
266
363
|
const dependencies = Array.isArray(plan?.dependencies)
|
|
@@ -272,6 +369,7 @@ async function mcpManagerRows(state) {
|
|
|
272
369
|
description: String(item.description ?? ""),
|
|
273
370
|
...(typeof item.sourceRef === "string" ? { sourceRef: item.sourceRef } : {}),
|
|
274
371
|
enabled: state.mcp.includes(id),
|
|
372
|
+
source: "recommended",
|
|
275
373
|
details: [
|
|
276
374
|
`传输:${String(item.transport ?? "未声明")} · 风险:${String(item.risk ?? "未声明")}`,
|
|
277
375
|
`参考版本:${String(plan?.ref ?? "由模型读取来源确定")} · 许可证:${String(plan?.license ?? "由模型读取来源确定")}`,
|
|
@@ -279,6 +377,31 @@ async function mcpManagerRows(state) {
|
|
|
279
377
|
],
|
|
280
378
|
};
|
|
281
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
|
+
});
|
|
282
405
|
}
|
|
283
406
|
function skillRowLabel(row) {
|
|
284
407
|
const state = row.blocked ? "× 阻断" : row.enabled ? "● 已启用" : row.installed ? "○ 未启用" : "↓ 未安装";
|
|
@@ -314,7 +437,12 @@ function skillManagerItems(rows) {
|
|
|
314
437
|
description: row.description,
|
|
315
438
|
status: row.blocked ? "blocked" : row.enabled ? "enabled" : row.installed ? "disabled" : "missing",
|
|
316
439
|
...(row.sourceRef === undefined ? {} : { sourceRef: row.sourceRef }),
|
|
317
|
-
details: [
|
|
440
|
+
details: [
|
|
441
|
+
`安装位置:${skillScopeLabel(row.scope)}`,
|
|
442
|
+
...(row.duplicatePaths && row.duplicatePaths.length > 0
|
|
443
|
+
? [`已忽略 ${row.duplicatePaths.length} 个同名来源`]
|
|
444
|
+
: []),
|
|
445
|
+
],
|
|
318
446
|
...(row.reason === undefined ? {} : { reason: row.reason }),
|
|
319
447
|
})),
|
|
320
448
|
];
|
|
@@ -337,18 +465,14 @@ async function openMcpManager(ctx, rows) {
|
|
|
337
465
|
return ctx.ui.custom((tui, theme, keybindings, done) => (new SkillManagerComponent(mcpManagerItems(rows), tui, theme, keybindings, done, {
|
|
338
466
|
title: "MCP 管理",
|
|
339
467
|
itemLabel: "MCP",
|
|
340
|
-
footer: "↑/↓ 移动 · Space
|
|
468
|
+
footer: "↑/↓ 移动 · Space 开启/关闭 · a 全部开启 · d 全部关闭 · Enter 交给模型安装并配置 · Esc 关闭",
|
|
341
469
|
enterAction: "install",
|
|
342
|
-
enabledText: "
|
|
343
|
-
disabledText: "
|
|
470
|
+
enabledText: "已开启;Enter 可让当前模型重新安装或修复推荐配置",
|
|
471
|
+
disabledText: "已关闭",
|
|
344
472
|
})));
|
|
345
473
|
}
|
|
346
474
|
async function setRecommendedMcpEnabled(root, id, enabled) {
|
|
347
475
|
const state = await readRecommendationState(root);
|
|
348
|
-
const rows = await mcpManagerRows(state);
|
|
349
|
-
const row = rows.find((candidate) => candidate.id === id);
|
|
350
|
-
if (!row)
|
|
351
|
-
throw new Error(`未找到推荐 MCP: ${id}`);
|
|
352
476
|
const current = new Set(state.mcp);
|
|
353
477
|
if (enabled)
|
|
354
478
|
current.add(id);
|
|
@@ -357,6 +481,9 @@ async function setRecommendedMcpEnabled(root, id, enabled) {
|
|
|
357
481
|
state.mcp = [...current];
|
|
358
482
|
await saveRecommendationState(root, state);
|
|
359
483
|
}
|
|
484
|
+
async function setUserMcpEnabled(entry, enabled) {
|
|
485
|
+
await setUserMcpConfigEnabled(entry, enabled);
|
|
486
|
+
}
|
|
360
487
|
function modelMcpInstallTask(root, row, plan) {
|
|
361
488
|
const suggestedCommand = typeof plan?.command === "string" ? plan.command : "请根据来源仓库的最新安装说明确定";
|
|
362
489
|
const suggestedTarget = typeof plan?.target === "string" ? plan.target : `.psyclaw/mcp/${row.id}.json`;
|
|
@@ -379,48 +506,86 @@ async function queueModelMcpInstall(pi, ctx, row, plan) {
|
|
|
379
506
|
pi.sendUserMessage(modelMcpInstallTask(ctx.cwd, row, plan), ctx.isIdle() ? {} : { deliverAs: "followUp" });
|
|
380
507
|
ctx.ui.notify(`已将 ${row.name} 的下载、安装和配置任务交给当前模型。模型完成并确认可启动后,请执行 /reload。`, "info");
|
|
381
508
|
}
|
|
382
|
-
async function showMcpManager(pi, args, ctx) {
|
|
509
|
+
async function showMcpManager(pi, args, ctx, runtime) {
|
|
383
510
|
const [verb, id] = args.trim().split(/\s+/).filter(Boolean);
|
|
384
|
-
if (verb && !["status", "enable", "disable", "install"].includes(verb)) {
|
|
385
|
-
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>]");
|
|
386
513
|
}
|
|
387
514
|
if ((verb === "enable" || verb === "disable" || verb === "install") && !id) {
|
|
388
515
|
throw new Error(`Usage: /mcp ${verb} <id>`);
|
|
389
516
|
}
|
|
390
517
|
const catalog = await recommendedItems("mcp");
|
|
391
518
|
if (verb === "install") {
|
|
392
|
-
const rows = await mcpManagerRows(await readRecommendationState(ctx.cwd));
|
|
519
|
+
const rows = await mcpManagerRows(ctx.cwd, await readRecommendationState(ctx.cwd), runtime);
|
|
393
520
|
const row = rows.find((candidate) => candidate.id === id);
|
|
394
521
|
if (!row)
|
|
395
522
|
throw new Error(`未找到推荐 MCP: ${id}`);
|
|
523
|
+
if (row.source !== "recommended")
|
|
524
|
+
throw new Error(`MCP ${id} 已是用户配置,无需再次安装`);
|
|
396
525
|
await queueModelMcpInstall(pi, ctx, row, catalog.installPrep.find((candidate) => candidate.id === row.id));
|
|
397
526
|
return;
|
|
398
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
|
+
}
|
|
399
540
|
if (verb === "enable" || verb === "disable") {
|
|
400
|
-
await
|
|
401
|
-
|
|
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");
|
|
402
550
|
return;
|
|
403
551
|
}
|
|
404
552
|
if (!ctx.hasUI || typeof ctx.ui.custom !== "function" || verb === "status") {
|
|
405
|
-
const rows = await mcpManagerRows(await readRecommendationState(ctx.cwd));
|
|
406
|
-
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");
|
|
407
555
|
return;
|
|
408
556
|
}
|
|
409
557
|
while (true) {
|
|
410
|
-
const rows = await mcpManagerRows(await readRecommendationState(ctx.cwd));
|
|
558
|
+
const rows = await mcpManagerRows(ctx.cwd, await readRecommendationState(ctx.cwd), runtime);
|
|
411
559
|
const action = await openMcpManager(ctx, rows);
|
|
412
560
|
if (action.type === "close")
|
|
413
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
|
+
}
|
|
414
572
|
const row = rows.find((candidate) => candidate.id === action.id);
|
|
415
573
|
if (!row)
|
|
416
574
|
continue;
|
|
417
575
|
if (action.type === "install") {
|
|
576
|
+
if (row.source !== "recommended") {
|
|
577
|
+
ctx.ui.notify(`${row.name} 已是用户配置,无需再次安装`, "info");
|
|
578
|
+
continue;
|
|
579
|
+
}
|
|
418
580
|
await queueModelMcpInstall(pi, ctx, row, catalog.installPrep.find((candidate) => candidate.id === row.id));
|
|
419
581
|
return;
|
|
420
582
|
}
|
|
421
583
|
if (action.type === "toggle") {
|
|
422
|
-
|
|
423
|
-
|
|
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");
|
|
424
589
|
}
|
|
425
590
|
}
|
|
426
591
|
}
|
|
@@ -469,8 +634,8 @@ async function showSkillManager(pi, args, ctx) {
|
|
|
469
634
|
const action = args.trim().split(/\s+/).filter(Boolean);
|
|
470
635
|
const verb = action[0];
|
|
471
636
|
const id = action[1];
|
|
472
|
-
if (verb && !["status", "enable", "disable", "install"].includes(verb)) {
|
|
473
|
-
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>]");
|
|
474
639
|
}
|
|
475
640
|
if ((verb === "enable" || verb === "disable" || verb === "install") && !id) {
|
|
476
641
|
throw new Error(`Usage: /skills ${verb} <id>`);
|
|
@@ -482,9 +647,44 @@ async function showSkillManager(pi, args, ctx) {
|
|
|
482
647
|
await queueModelSkillInstall(pi, ctx, row);
|
|
483
648
|
return;
|
|
484
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
|
+
}
|
|
485
674
|
if (verb === "enable" || verb === "disable") {
|
|
486
|
-
|
|
487
|
-
|
|
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");
|
|
488
688
|
return;
|
|
489
689
|
}
|
|
490
690
|
if (!ctx.hasUI || typeof ctx.ui.custom !== "function" || verb === "status") {
|
|
@@ -503,6 +703,42 @@ async function showSkillManager(pi, args, ctx) {
|
|
|
503
703
|
const action = await openSkillManager(ctx, rows);
|
|
504
704
|
if (action.type === "close")
|
|
505
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
|
+
}
|
|
506
742
|
const row = rows.find((candidate) => candidate.id === action.id);
|
|
507
743
|
if (!row)
|
|
508
744
|
continue;
|
|
@@ -511,8 +747,14 @@ async function showSkillManager(pi, args, ctx) {
|
|
|
511
747
|
return;
|
|
512
748
|
}
|
|
513
749
|
if (action.type === "toggle") {
|
|
514
|
-
|
|
515
|
-
|
|
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
|
+
}
|
|
516
758
|
}
|
|
517
759
|
}
|
|
518
760
|
}
|
|
@@ -614,13 +856,26 @@ const WORKFLOW_RUNNERS = {
|
|
|
614
856
|
export default function psyclawExtension(pi) {
|
|
615
857
|
const developerCommands = process.env.PSYCLAW_DEVELOPER_COMMANDS === "1";
|
|
616
858
|
const legacyTestApi = typeof pi.registerTool !== "function";
|
|
859
|
+
const runtimeMcps = new RuntimeMcpRegistry();
|
|
617
860
|
if (!legacyTestApi && typeof pi.on === "function")
|
|
618
861
|
pi.on("resources_discover", async (event, ctx) => {
|
|
619
862
|
const enabled = await enabledRecommendedSkillPaths(event.cwd);
|
|
620
863
|
for (const warning of enabled.warnings)
|
|
621
864
|
ctx.ui.notify(`PsyClaw Skill: ${warning}`, "warning");
|
|
622
|
-
|
|
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
|
+
};
|
|
623
876
|
});
|
|
877
|
+
if (!legacyTestApi && typeof pi.on === "function")
|
|
878
|
+
pi.on("session_shutdown", () => runtimeMcps.close());
|
|
624
879
|
pi.registerCommand("init", {
|
|
625
880
|
description: "初始化可追溯的研究项目",
|
|
626
881
|
handler: async (args, ctx) => {
|
|
@@ -642,18 +897,47 @@ export default function psyclawExtension(pi) {
|
|
|
642
897
|
});
|
|
643
898
|
if (!legacyTestApi)
|
|
644
899
|
pi.registerCommand("run", {
|
|
645
|
-
description: "
|
|
900
|
+
description: "启动受控研究流程,可用 --skills a,b 选择本次优化 Skill",
|
|
646
901
|
handler: async (args, ctx) => {
|
|
647
902
|
try {
|
|
648
|
-
const
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
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");
|
|
654
938
|
}
|
|
655
|
-
catch {
|
|
656
|
-
ctx
|
|
939
|
+
catch (error) {
|
|
940
|
+
await notifyError(ctx, error);
|
|
657
941
|
}
|
|
658
942
|
},
|
|
659
943
|
});
|
|
@@ -799,12 +1083,24 @@ export default function psyclawExtension(pi) {
|
|
|
799
1083
|
}
|
|
800
1084
|
},
|
|
801
1085
|
});
|
|
1086
|
+
if (!legacyTestApi)
|
|
1087
|
+
pi.registerCommand("skill", {
|
|
1088
|
+
description: "调用任意已加载 Skill(/skill <name> [任务])",
|
|
1089
|
+
handler: async (args, ctx) => {
|
|
1090
|
+
const [name, ...rest] = args.trim().split(/\s+/).filter(Boolean);
|
|
1091
|
+
if (!name || !/^[a-z0-9-]+$/.test(name)) {
|
|
1092
|
+
ctx.ui.notify("Usage: /skill <name> [task];也可直接使用 /skill:<name>", "info");
|
|
1093
|
+
return;
|
|
1094
|
+
}
|
|
1095
|
+
pi.sendUserMessage(`/skill:${name}${rest.length ? ` ${rest.join(" ")}` : ""}`, ctx.isIdle() ? {} : { deliverAs: "followUp" });
|
|
1096
|
+
},
|
|
1097
|
+
});
|
|
802
1098
|
if (!legacyTestApi)
|
|
803
1099
|
pi.registerCommand("mcp", {
|
|
804
1100
|
description: "打开 MCP 安装与配置管理页",
|
|
805
1101
|
handler: async (args, ctx) => {
|
|
806
1102
|
try {
|
|
807
|
-
await showMcpManager(pi, args, ctx);
|
|
1103
|
+
await showMcpManager(pi, args, ctx, runtimeMcps);
|
|
808
1104
|
}
|
|
809
1105
|
catch (error) {
|
|
810
1106
|
await notifyError(ctx, error);
|
|
@@ -831,13 +1127,13 @@ export default function psyclawExtension(pi) {
|
|
|
831
1127
|
if (kind === "skill")
|
|
832
1128
|
await showSkillManager(pi, "", ctx);
|
|
833
1129
|
else
|
|
834
|
-
await showMcpManager(pi, "", ctx);
|
|
1130
|
+
await showMcpManager(pi, "", ctx, runtimeMcps);
|
|
835
1131
|
return;
|
|
836
1132
|
}
|
|
837
1133
|
if (kind === "skill")
|
|
838
1134
|
await showSkillManager(pi, `install ${id}`, ctx);
|
|
839
1135
|
else
|
|
840
|
-
await showMcpManager(pi, `install ${id}`, ctx);
|
|
1136
|
+
await showMcpManager(pi, `install ${id}`, ctx, runtimeMcps);
|
|
841
1137
|
}
|
|
842
1138
|
catch (error) {
|
|
843
1139
|
await notifyError(ctx, error);
|
|
@@ -945,18 +1241,18 @@ export default function psyclawExtension(pi) {
|
|
|
945
1241
|
},
|
|
946
1242
|
});
|
|
947
1243
|
const traceCommand = {
|
|
948
|
-
description: "
|
|
1244
|
+
description: "导出包含正文与工具参数的完整使用路径,供 Langfuse 或 LangSmith 分析",
|
|
949
1245
|
handler: async (args, ctx) => {
|
|
950
1246
|
try {
|
|
951
1247
|
if (args.trim())
|
|
952
|
-
throw new Error("Usage: /
|
|
1248
|
+
throw new Error("Usage: /export");
|
|
953
1249
|
const result = await exportTraces({ root: ctx.cwd });
|
|
954
1250
|
ctx.ui.notify([
|
|
955
1251
|
"使用路径已导出(未上传)",
|
|
956
1252
|
`文件:${result.output}`,
|
|
957
1253
|
`轨迹:${result.traces}`,
|
|
958
1254
|
`步骤:${result.spans}`,
|
|
959
|
-
"
|
|
1255
|
+
"包含对话正文、工具参数、原始 ID 与绝对路径,便于排查各环节问题;请勿将导出文件提交到公开仓库。",
|
|
960
1256
|
].join("\n"), "info");
|
|
961
1257
|
}
|
|
962
1258
|
catch (error) {
|
|
@@ -964,8 +1260,40 @@ export default function psyclawExtension(pi) {
|
|
|
964
1260
|
}
|
|
965
1261
|
},
|
|
966
1262
|
};
|
|
967
|
-
pi.registerCommand("
|
|
1263
|
+
pi.registerCommand("export", traceCommand);
|
|
968
1264
|
if (typeof pi.registerTool === "function") {
|
|
1265
|
+
pi.registerTool({
|
|
1266
|
+
name: "psyclaw_mcp",
|
|
1267
|
+
label: "MCP tools",
|
|
1268
|
+
description: "List and call tools from MCP servers enabled in .psyclaw/mcp/*.json. Use action=list first, then action=call with the exact server and tool names.",
|
|
1269
|
+
promptSnippet: "Discover and call enabled MCP servers such as MNE through PsyClaw.",
|
|
1270
|
+
parameters: Type.Object({
|
|
1271
|
+
action: Type.Union([Type.Literal("list"), Type.Literal("call")]),
|
|
1272
|
+
server: Type.Optional(Type.String({ description: "Configured MCP server id, for example mne-mcp" })),
|
|
1273
|
+
tool: Type.Optional(Type.String({ description: "Exact MCP tool name returned by action=list" })),
|
|
1274
|
+
input: Type.Optional(Type.Record(Type.String(), Type.Unknown())),
|
|
1275
|
+
}),
|
|
1276
|
+
executionMode: "sequential",
|
|
1277
|
+
async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
|
|
1278
|
+
try {
|
|
1279
|
+
if (params.action === "list") {
|
|
1280
|
+
const servers = await runtimeMcps.list(ctx.cwd, params.server);
|
|
1281
|
+
return { content: [{ type: "text", text: JSON.stringify({ servers }, null, 2) }], details: { action: "list", servers: servers.length } };
|
|
1282
|
+
}
|
|
1283
|
+
if (!params.server || !params.tool)
|
|
1284
|
+
throw new Error("server and tool are required for action=call");
|
|
1285
|
+
const result = await runtimeMcps.call(ctx.cwd, params.server, params.tool, params.input ?? {});
|
|
1286
|
+
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }], details: { action: "call", server: params.server, tool: params.tool } };
|
|
1287
|
+
}
|
|
1288
|
+
catch (error) {
|
|
1289
|
+
return {
|
|
1290
|
+
content: [{ type: "text", text: `MCP call failed: ${error instanceof Error ? error.message : String(error)}` }],
|
|
1291
|
+
details: { action: params.action, status: "failed" },
|
|
1292
|
+
isError: true,
|
|
1293
|
+
};
|
|
1294
|
+
}
|
|
1295
|
+
},
|
|
1296
|
+
});
|
|
969
1297
|
pi.registerTool({
|
|
970
1298
|
name: "psyclaw_skill",
|
|
971
1299
|
label: "Research skill",
|