psyclaw 0.25.2 → 0.26.1
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 +111 -60
- package/{apps → dist/apps}/panel/index.html +50 -28
- package/dist/src/adapters/pi/extension.d.ts +19 -0
- package/dist/src/adapters/pi/extension.js +735 -0
- package/dist/src/adapters/pi/extension.js.map +1 -0
- package/dist/src/adapters/pi/model.d.ts +72 -0
- package/dist/src/adapters/pi/model.js +152 -0
- package/dist/src/adapters/pi/model.js.map +1 -0
- package/dist/src/adapters/pi/resources.d.ts +23 -0
- package/dist/src/adapters/pi/resources.js +54 -0
- package/dist/src/adapters/pi/resources.js.map +1 -0
- package/dist/src/adapters/pi/rpc.d.ts +58 -0
- package/dist/src/adapters/pi/rpc.js +253 -0
- package/dist/src/adapters/pi/rpc.js.map +1 -0
- package/dist/src/adapters/pi/session.d.ts +30 -0
- package/dist/src/adapters/pi/session.js +64 -0
- package/dist/src/adapters/pi/session.js.map +1 -0
- package/dist/src/agents/catalog.d.ts +30 -0
- package/dist/src/agents/catalog.js +118 -0
- package/dist/src/agents/catalog.js.map +1 -0
- package/dist/src/agents/discover.d.ts +28 -0
- package/dist/src/agents/discover.js +127 -0
- package/dist/src/agents/discover.js.map +1 -0
- package/dist/src/agents/import.d.ts +67 -0
- package/dist/src/agents/import.js +789 -0
- package/dist/src/agents/import.js.map +1 -0
- package/dist/src/analysis/hooks.d.ts +81 -0
- package/dist/src/analysis/hooks.js +139 -0
- package/dist/src/analysis/hooks.js.map +1 -0
- package/dist/src/analysis/pre-registration.d.ts +18 -0
- package/dist/src/analysis/pre-registration.js +86 -0
- package/dist/src/analysis/pre-registration.js.map +1 -0
- package/dist/src/branding.d.ts +44 -0
- package/dist/src/branding.js +128 -0
- package/dist/src/branding.js.map +1 -0
- package/dist/src/chat.d.ts +16 -0
- package/dist/src/chat.js +95 -0
- package/dist/src/chat.js.map +1 -0
- package/dist/src/cli.d.ts +2 -0
- package/dist/src/cli.js +316 -0
- package/dist/src/cli.js.map +1 -0
- package/dist/src/core/citations.d.ts +44 -0
- package/dist/src/core/citations.js +48 -0
- package/dist/src/core/citations.js.map +1 -0
- package/dist/src/core/contracts.d.ts +119 -0
- package/dist/src/core/contracts.js +2 -0
- package/dist/src/core/contracts.js.map +1 -0
- package/dist/src/core/docx.d.ts +11 -0
- package/dist/src/core/docx.js +46 -0
- package/dist/src/core/docx.js.map +1 -0
- package/dist/src/core/doi.d.ts +41 -0
- package/dist/src/core/doi.js +82 -0
- package/dist/src/core/doi.js.map +1 -0
- package/dist/src/core/evidence-policy.d.ts +24 -0
- package/dist/src/core/evidence-policy.js +308 -0
- package/dist/src/core/evidence-policy.js.map +1 -0
- package/dist/src/core/filekind.d.ts +15 -0
- package/dist/src/core/filekind.js +82 -0
- package/dist/src/core/filekind.js.map +1 -0
- package/dist/src/core/hash.d.ts +2 -0
- package/dist/src/core/hash.js +17 -0
- package/dist/src/core/hash.js.map +1 -0
- package/dist/src/core/pricing.d.ts +38 -0
- package/dist/src/core/pricing.js +110 -0
- package/dist/src/core/pricing.js.map +1 -0
- package/dist/src/core/redact.d.ts +12 -0
- package/dist/src/core/redact.js +48 -0
- package/dist/src/core/redact.js.map +1 -0
- package/dist/src/core/references.d.ts +55 -0
- package/dist/src/core/references.js +102 -0
- package/dist/src/core/references.js.map +1 -0
- package/dist/src/core/schemas.d.ts +162 -0
- package/dist/src/core/schemas.js +160 -0
- package/dist/src/core/schemas.js.map +1 -0
- package/dist/src/extension.d.ts +1 -0
- package/dist/src/extension.js +2 -0
- package/dist/src/extension.js.map +1 -0
- package/dist/src/index.d.ts +72 -0
- package/dist/src/index.js +73 -0
- package/dist/src/index.js.map +1 -0
- package/dist/src/install/installer.d.ts +147 -0
- package/dist/src/install/installer.js +721 -0
- package/dist/src/install/installer.js.map +1 -0
- package/dist/src/integrations/contracts.d.ts +35 -0
- package/dist/src/integrations/contracts.js +27 -0
- package/dist/src/integrations/contracts.js.map +1 -0
- package/dist/src/integrations/mcp.d.ts +155 -0
- package/dist/src/integrations/mcp.js +826 -0
- package/dist/src/integrations/mcp.js.map +1 -0
- package/dist/src/knowledge-map/contracts.d.ts +20 -0
- package/dist/src/knowledge-map/contracts.js +2 -0
- package/dist/src/knowledge-map/contracts.js.map +1 -0
- package/dist/src/knowledge-map/index.d.ts +4 -0
- package/dist/src/knowledge-map/index.js +39 -0
- package/dist/src/knowledge-map/index.js.map +1 -0
- package/dist/src/literature/contracts.d.ts +106 -0
- package/dist/src/literature/contracts.js +2 -0
- package/dist/src/literature/contracts.js.map +1 -0
- package/dist/src/literature/fulltext.d.ts +7 -0
- package/dist/src/literature/fulltext.js +24 -0
- package/dist/src/literature/fulltext.js.map +1 -0
- package/dist/src/literature/knowledge-map.d.ts +2 -0
- package/dist/src/literature/knowledge-map.js +20 -0
- package/dist/src/literature/knowledge-map.js.map +1 -0
- package/dist/src/literature/ledger.d.ts +3 -0
- package/dist/src/literature/ledger.js +17 -0
- package/dist/src/literature/ledger.js.map +1 -0
- package/dist/src/literature/metadata.d.ts +6 -0
- package/dist/src/literature/metadata.js +32 -0
- package/dist/src/literature/metadata.js.map +1 -0
- package/dist/src/memory/store.d.ts +22 -0
- package/dist/src/memory/store.js +73 -0
- package/dist/src/memory/store.js.map +1 -0
- package/dist/src/orchestration/contracts.d.ts +61 -0
- package/dist/src/orchestration/contracts.js +2 -0
- package/dist/src/orchestration/contracts.js.map +1 -0
- package/dist/src/orchestration/pi-executor.d.ts +28 -0
- package/dist/src/orchestration/pi-executor.js +169 -0
- package/dist/src/orchestration/pi-executor.js.map +1 -0
- package/dist/src/orchestration/runner.d.ts +196 -0
- package/dist/src/orchestration/runner.js +991 -0
- package/dist/src/orchestration/runner.js.map +1 -0
- package/dist/src/orchestration/scheduler.d.ts +8 -0
- package/dist/src/orchestration/scheduler.js +335 -0
- package/dist/src/orchestration/scheduler.js.map +1 -0
- package/dist/src/panel/events.d.ts +20 -0
- package/dist/src/panel/events.js +93 -0
- package/dist/src/panel/events.js.map +1 -0
- package/dist/src/panel/extension.d.ts +8 -0
- package/dist/src/panel/extension.js +115 -0
- package/dist/src/panel/extension.js.map +1 -0
- package/dist/src/panel/literature-map.d.ts +26 -0
- package/dist/src/panel/literature-map.js +34 -0
- package/dist/src/panel/literature-map.js.map +1 -0
- package/dist/src/panel/projection.d.ts +51 -0
- package/dist/src/panel/projection.js +288 -0
- package/dist/src/panel/projection.js.map +1 -0
- package/dist/src/panel/server.d.ts +46 -0
- package/dist/src/panel/server.js +1610 -0
- package/dist/src/panel/server.js.map +1 -0
- package/dist/src/panel/usage.d.ts +53 -0
- package/dist/src/panel/usage.js +150 -0
- package/dist/src/panel/usage.js.map +1 -0
- package/dist/src/project/bootstrap.d.ts +10 -0
- package/dist/src/project/bootstrap.js +83 -0
- package/dist/src/project/bootstrap.js.map +1 -0
- package/dist/src/project/hitl.d.ts +12 -0
- package/dist/src/project/hitl.js +43 -0
- package/dist/src/project/hitl.js.map +1 -0
- package/dist/src/project/jsonl.d.ts +16 -0
- package/dist/src/project/jsonl.js +93 -0
- package/dist/src/project/jsonl.js.map +1 -0
- package/dist/src/project/manuscript.d.ts +21 -0
- package/dist/src/project/manuscript.js +74 -0
- package/dist/src/project/manuscript.js.map +1 -0
- package/dist/src/project/paths.d.ts +38 -0
- package/dist/src/project/paths.js +166 -0
- package/dist/src/project/paths.js.map +1 -0
- package/dist/src/project/versions.d.ts +10 -0
- package/dist/src/project/versions.js +14 -0
- package/dist/src/project/versions.js.map +1 -0
- package/dist/src/psyclaw-theme.d.ts +88 -0
- package/dist/src/psyclaw-theme.js +89 -0
- package/dist/src/psyclaw-theme.js.map +1 -0
- package/dist/src/research/brief.d.ts +11 -0
- package/dist/src/research/brief.js +81 -0
- package/dist/src/research/brief.js.map +1 -0
- package/dist/src/research/ledger.d.ts +12 -0
- package/dist/src/research/ledger.js +43 -0
- package/dist/src/research/ledger.js.map +1 -0
- package/dist/src/run.d.ts +20 -0
- package/dist/src/run.js +34 -0
- package/dist/src/run.js.map +1 -0
- package/dist/src/setup.d.ts +54 -0
- package/dist/src/setup.js +221 -0
- package/dist/src/setup.js.map +1 -0
- package/dist/src/skills/contracts.d.ts +105 -0
- package/dist/src/skills/contracts.js +3 -0
- package/dist/src/skills/contracts.js.map +1 -0
- package/dist/src/skills/preflight.d.ts +10 -0
- package/dist/src/skills/preflight.js +39 -0
- package/dist/src/skills/preflight.js.map +1 -0
- package/dist/src/skills/registry.d.ts +28 -0
- package/dist/src/skills/registry.js +540 -0
- package/dist/src/skills/registry.js.map +1 -0
- package/dist/src/style/artifact.d.ts +6 -0
- package/dist/src/style/artifact.js +61 -0
- package/dist/src/style/artifact.js.map +1 -0
- package/dist/src/style/cli-ui.d.ts +41 -0
- package/dist/src/style/cli-ui.js +119 -0
- package/dist/src/style/cli-ui.js.map +1 -0
- package/dist/src/style/contracts.d.ts +34 -0
- package/dist/src/style/contracts.js +2 -0
- package/dist/src/style/contracts.js.map +1 -0
- package/dist/src/style/registry.d.ts +16 -0
- package/dist/src/style/registry.js +50 -0
- package/dist/src/style/registry.js.map +1 -0
- package/dist/src/trust/manifest.d.ts +42 -0
- package/dist/src/trust/manifest.js +53 -0
- package/dist/src/trust/manifest.js.map +1 -0
- package/dist/src/trust/sbom.d.ts +54 -0
- package/dist/src/trust/sbom.js +77 -0
- package/dist/src/trust/sbom.js.map +1 -0
- package/dist/src/tui/app.d.ts +3 -0
- package/dist/src/tui/app.js +103 -0
- package/dist/src/tui/app.js.map +1 -0
- package/dist/src/tui/model.d.ts +13 -0
- package/dist/src/tui/model.js +25 -0
- package/dist/src/tui/model.js.map +1 -0
- package/dist/src/updates/check.d.ts +43 -0
- package/dist/src/updates/check.js +68 -0
- package/dist/src/updates/check.js.map +1 -0
- package/dist/src/updates/manifest.d.ts +19 -0
- package/dist/src/updates/manifest.js +58 -0
- package/dist/src/updates/manifest.js.map +1 -0
- package/dist/src/updates/registry.d.ts +21 -0
- package/dist/src/updates/registry.js +82 -0
- package/dist/src/updates/registry.js.map +1 -0
- package/dist/src/updates/skills.d.ts +15 -0
- package/dist/src/updates/skills.js +39 -0
- package/dist/src/updates/skills.js.map +1 -0
- package/dist/src/updates/status.d.ts +7 -0
- package/dist/src/updates/status.js +37 -0
- package/dist/src/updates/status.js.map +1 -0
- package/dist/src/updates/update.d.ts +43 -0
- package/dist/src/updates/update.js +128 -0
- package/dist/src/updates/update.js.map +1 -0
- package/dist/src/wizard.d.ts +7 -0
- package/dist/src/wizard.js +90 -0
- package/dist/src/wizard.js.map +1 -0
- package/dist/src/workflows/analysis-delegation.d.ts +3 -0
- package/dist/src/workflows/analysis-delegation.js +96 -0
- package/dist/src/workflows/analysis-delegation.js.map +1 -0
- package/dist/src/workflows/expert-review-runner.d.ts +29 -0
- package/dist/src/workflows/expert-review-runner.js +35 -0
- package/dist/src/workflows/expert-review-runner.js.map +1 -0
- package/dist/src/workflows/expert-review.d.ts +140 -0
- package/dist/src/workflows/expert-review.js +111 -0
- package/dist/src/workflows/expert-review.js.map +1 -0
- package/dist/src/workflows/institutional-fulltext.d.ts +16 -0
- package/dist/src/workflows/institutional-fulltext.js +110 -0
- package/dist/src/workflows/institutional-fulltext.js.map +1 -0
- package/dist/src/workflows/literature-review.d.ts +3 -0
- package/dist/src/workflows/literature-review.js +76 -0
- package/dist/src/workflows/literature-review.js.map +1 -0
- package/dist/src/workflows/meta-analysis.d.ts +38 -0
- package/dist/src/workflows/meta-analysis.js +185 -0
- package/dist/src/workflows/meta-analysis.js.map +1 -0
- package/dist/src/workflows/publish.d.ts +51 -0
- package/dist/src/workflows/publish.js +153 -0
- package/dist/src/workflows/publish.js.map +1 -0
- package/dist/src/workflows/spec.d.ts +51 -0
- package/dist/src/workflows/spec.js +157 -0
- package/dist/src/workflows/spec.js.map +1 -0
- package/dist/src/workflows/style-contract.d.ts +24 -0
- package/dist/src/workflows/style-contract.js +23 -0
- package/dist/src/workflows/style-contract.js.map +1 -0
- package/dist/src/workflows/writing-review.d.ts +15 -0
- package/dist/src/workflows/writing-review.js +82 -0
- package/dist/src/workflows/writing-review.js.map +1 -0
- package/package.json +20 -8
- package/scripts/rebrand-pi.mjs +3 -3
- package/scripts/__pycache__/build_dist.cpython-314.pyc +0 -0
- package/scripts/__pycache__/build_whitepaper_docx.cpython-314.pyc +0 -0
- package/scripts/build_preprint_docx.py +0 -729
- package/scripts/build_preprint_figures.py +0 -238
- package/scripts/build_three_line_table_fixture.py +0 -56
- package/scripts/build_whitepaper_docx.py +0 -768
- package/scripts/check-branding.mjs +0 -159
- package/scripts/copy-panel.mjs +0 -11
- package/scripts/verify_docx.py +0 -41
|
@@ -0,0 +1,735 @@
|
|
|
1
|
+
import { Type } from "typebox";
|
|
2
|
+
import { asProject, bootstrapProject, projectPaths, runOfflineBrief, runInstitutionalFulltext, runLiteratureReview, runExpertReview, runAnalysisDelegation, runWritingReview, runMetaAnalysis, createStageRunner, publishManuscript, recordCitationUse, writeHandoff } from "../../index.js";
|
|
3
|
+
import { runPlanWithPi } from "../../orchestration/pi-executor.js";
|
|
4
|
+
import { atomicWriteFile } from "../../project/jsonl.js";
|
|
5
|
+
import { RunEventLog } from "../../panel/events.js";
|
|
6
|
+
import { readProject } from "../../research/ledger.js";
|
|
7
|
+
import { join } from "node:path";
|
|
8
|
+
import { mkdir, readFile } from "node:fs/promises";
|
|
9
|
+
import { dirname } from "node:path";
|
|
10
|
+
import { fileURLToPath } from "node:url";
|
|
11
|
+
const PARADIGMS = new Set([
|
|
12
|
+
"survey-observational",
|
|
13
|
+
"qualitative-thematic",
|
|
14
|
+
"experimental",
|
|
15
|
+
"quasi-experimental",
|
|
16
|
+
"longitudinal-panel",
|
|
17
|
+
"meta-analysis",
|
|
18
|
+
"ethnographic",
|
|
19
|
+
"historical-documentary",
|
|
20
|
+
"policy-legal",
|
|
21
|
+
"mixed-methods",
|
|
22
|
+
]);
|
|
23
|
+
function parseResearchArgs(args) {
|
|
24
|
+
const trimmed = args.trim();
|
|
25
|
+
const match = trimmed.match(/^--paradigm(?:=|\s+)(\S+)(?:\s+([\s\S]*))?$/i);
|
|
26
|
+
if (trimmed.startsWith("--")) {
|
|
27
|
+
const paradigm = match?.[1];
|
|
28
|
+
const goal = match?.[2]?.trim();
|
|
29
|
+
if (!match || !paradigm || !goal) {
|
|
30
|
+
throw new Error("Usage: /research --paradigm survey-observational <research goal>");
|
|
31
|
+
}
|
|
32
|
+
if (!PARADIGMS.has(paradigm))
|
|
33
|
+
throw new Error(`Unsupported paradigm: ${paradigm}`);
|
|
34
|
+
return { paradigm, goal };
|
|
35
|
+
}
|
|
36
|
+
if (!trimmed)
|
|
37
|
+
throw new Error("Usage: /research --paradigm survey-observational <research goal>");
|
|
38
|
+
return { paradigm: "survey-observational", goal: trimmed };
|
|
39
|
+
}
|
|
40
|
+
async function notifyError(ctx, error) {
|
|
41
|
+
ctx.ui.notify(error instanceof Error ? error.message : String(error), "error");
|
|
42
|
+
}
|
|
43
|
+
const activeAgentRuns = new Set();
|
|
44
|
+
const CORE_SKILLS = new Set(["research-intake", "evidence-capture", "citation-audit", "research-brief"]);
|
|
45
|
+
async function recommendationState(root) {
|
|
46
|
+
try {
|
|
47
|
+
const value = JSON.parse(await readFile(join(root, ".psyclaw", "recommendations.json"), "utf8"));
|
|
48
|
+
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") : [] };
|
|
49
|
+
}
|
|
50
|
+
catch {
|
|
51
|
+
return { schemaVersion: "psyclaw/recommendation-state/v1", skills: [], mcp: [] };
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
async function saveRecommendationState(root, state) {
|
|
55
|
+
await mkdir(join(root, ".psyclaw"), { recursive: true });
|
|
56
|
+
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`);
|
|
57
|
+
}
|
|
58
|
+
function coreSkillPath(name) {
|
|
59
|
+
return join(dirname(fileURLToPath(import.meta.url)), "..", "..", "..", "..", "skills", "core", name, "SKILL.md");
|
|
60
|
+
}
|
|
61
|
+
function providerEnvironment(provider) {
|
|
62
|
+
const envName = provider === "deepseek"
|
|
63
|
+
? "DEEPSEEK_API_KEY"
|
|
64
|
+
: provider === "openai"
|
|
65
|
+
? "OPENAI_API_KEY"
|
|
66
|
+
: provider === "anthropic"
|
|
67
|
+
? "ANTHROPIC_API_KEY"
|
|
68
|
+
: undefined;
|
|
69
|
+
if (!envName)
|
|
70
|
+
return {};
|
|
71
|
+
const value = process.env[envName];
|
|
72
|
+
return value === undefined ? {} : { [envName]: value };
|
|
73
|
+
}
|
|
74
|
+
function modelSummary(ctx) {
|
|
75
|
+
const current = ctx.model ? `${ctx.model.provider}/${ctx.model.id}` : "none";
|
|
76
|
+
const available = ctx.modelRegistry.getAll().map((model) => `${model.provider}/${model.id}`);
|
|
77
|
+
return `current=${current}; available=${available.length > 0 ? available.slice(0, 12).join(", ") : "none"}`;
|
|
78
|
+
}
|
|
79
|
+
function parseModelRef(args) {
|
|
80
|
+
const value = args.trim();
|
|
81
|
+
const slash = value.indexOf("/");
|
|
82
|
+
const parts = slash > 0 ? [value.slice(0, slash), value.slice(slash + 1)] : value.split(/\s+/, 2);
|
|
83
|
+
if (parts.length !== 2 || !parts[0] || !parts[1] || !/^[A-Za-z0-9._:-]+$/.test(parts[0]) || !/^[A-Za-z0-9._:/-]+$/.test(parts[1])) {
|
|
84
|
+
throw new Error("Usage: /model [provider/model]");
|
|
85
|
+
}
|
|
86
|
+
return { provider: parts[0], id: parts[1] };
|
|
87
|
+
}
|
|
88
|
+
function researchTaskPlan(runId, objective) {
|
|
89
|
+
return {
|
|
90
|
+
schemaVersion: "psyclaw/plan/v1",
|
|
91
|
+
runId,
|
|
92
|
+
tasks: [{
|
|
93
|
+
id: "agent-task-1",
|
|
94
|
+
role: "researcher",
|
|
95
|
+
objective,
|
|
96
|
+
deps: [],
|
|
97
|
+
ownedPaths: ["notes"],
|
|
98
|
+
parallelSafe: true,
|
|
99
|
+
inputs: [],
|
|
100
|
+
outputs: [],
|
|
101
|
+
completionContract: { requiredArtifacts: [], requiredReceiptEffects: [], mustPassGates: [] },
|
|
102
|
+
}],
|
|
103
|
+
budget: { maxTurns: 4, maxWorkers: 1 },
|
|
104
|
+
horizon: { strategy: "hierarchical-plan-act-reflect", maxIterations: 12, reflectionEvery: 1 },
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
async function recommendedItems(kind) {
|
|
108
|
+
const file = kind === "skills" ? "catalog.json" : "mcp-catalog.json";
|
|
109
|
+
const moduleDir = dirname(fileURLToPath(import.meta.url));
|
|
110
|
+
const candidates = [
|
|
111
|
+
join(moduleDir, "..", "..", "..", "..", "skills", "recommended", file),
|
|
112
|
+
join(moduleDir, "..", "..", "..", "skills", "recommended", file),
|
|
113
|
+
join(moduleDir, "..", "..", "skills", "recommended", file),
|
|
114
|
+
join(process.cwd(), "skills", "recommended", file),
|
|
115
|
+
];
|
|
116
|
+
for (const path of candidates) {
|
|
117
|
+
try {
|
|
118
|
+
const value = JSON.parse(await readFile(path, "utf8"));
|
|
119
|
+
return {
|
|
120
|
+
items: Array.isArray(value.items) ? value.items.filter((item) => Boolean(item && typeof item === "object")) : [],
|
|
121
|
+
installPrep: Array.isArray(value.installPrep) ? value.installPrep.filter((item) => Boolean(item && typeof item === "object")) : [],
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
catch { /* try next candidate */ }
|
|
125
|
+
}
|
|
126
|
+
return { items: [], installPrep: [] };
|
|
127
|
+
}
|
|
128
|
+
async function showRecommended(kind, args, ctx) {
|
|
129
|
+
const { items, installPrep } = await recommendedItems(kind);
|
|
130
|
+
const state = await recommendationState(ctx.cwd);
|
|
131
|
+
const stateKey = kind === "skills" ? "skill" : "mcp";
|
|
132
|
+
const stateField = kind === "skills" ? "skills" : "mcp";
|
|
133
|
+
const action = args.trim().split(/\s+/).filter(Boolean);
|
|
134
|
+
const verb = action[0];
|
|
135
|
+
const id = action[1];
|
|
136
|
+
if (verb && !["status", "enable", "disable", "install", "select"].includes(verb)) {
|
|
137
|
+
throw new Error(`Usage: /${kind} [status|enable <id>|disable <id>|install <id>]`);
|
|
138
|
+
}
|
|
139
|
+
// 1. 无参数或显式 select 时,如果处于交互 TUI 模式,则启动键盘上下键选择器
|
|
140
|
+
if ((!verb || verb === "select") && ctx.hasUI && typeof ctx.ui.select === "function") {
|
|
141
|
+
if (!items.length) {
|
|
142
|
+
ctx.ui.notify(`暂无推荐${kind === "skills" ? " Skill" : " MCP"}`, "info");
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
const enabled = new Set(state[stateField]);
|
|
146
|
+
const optionLabels = items.map((item) => {
|
|
147
|
+
const isEnabled = enabled.has(String(item.id));
|
|
148
|
+
const mark = isEnabled ? "● [已启用]" : "○ [未启用]";
|
|
149
|
+
const stage = item.stage ? ` [${String(item.stage)}]` : "";
|
|
150
|
+
const desc = item.description ? ` — ${String(item.description)}` : "";
|
|
151
|
+
return `${mark} ${String(item.name)}${stage}${desc}`;
|
|
152
|
+
});
|
|
153
|
+
const CANCEL_LABEL = "↩️ 取消 / 返回";
|
|
154
|
+
const selected = await ctx.ui.select(`选择推荐 ${kind === "skills" ? "Skill" : "MCP"}(使用键盘 ↑/↓ 移动光标,Enter 确认):`, [...optionLabels, CANCEL_LABEL]);
|
|
155
|
+
if (!selected || selected === CANCEL_LABEL)
|
|
156
|
+
return;
|
|
157
|
+
const selectedIndex = optionLabels.indexOf(selected);
|
|
158
|
+
if (selectedIndex < 0 || selectedIndex >= items.length)
|
|
159
|
+
return;
|
|
160
|
+
const selectedItem = items[selectedIndex];
|
|
161
|
+
if (!selectedItem)
|
|
162
|
+
return;
|
|
163
|
+
const targetId = String(selectedItem.id);
|
|
164
|
+
const isCurrentlyEnabled = enabled.has(targetId);
|
|
165
|
+
const actionChoice = await ctx.ui.select(`管理推荐 ${kind === "skills" ? "Skill" : "MCP"}: ${String(selectedItem.name)} (${targetId})`, [
|
|
166
|
+
isCurrentlyEnabled ? `🔴 禁用 (Disable ${String(selectedItem.name)})` : `🟢 启用 (Enable ${String(selectedItem.name)})`,
|
|
167
|
+
`📦 查看安装预检与依赖计划 (Install Plan)`,
|
|
168
|
+
`🌐 查看来源仓库 (${String(selectedItem.sourceRef ?? "无")})`,
|
|
169
|
+
`↩️ 取消`,
|
|
170
|
+
]);
|
|
171
|
+
if (!actionChoice || actionChoice === "↩️ 取消")
|
|
172
|
+
return;
|
|
173
|
+
if (actionChoice.startsWith("🔴") || actionChoice.startsWith("🟢")) {
|
|
174
|
+
const nextAction = isCurrentlyEnabled ? "disable" : "enable";
|
|
175
|
+
if (await ctx.ui.confirm(`${nextAction === "enable" ? "启用" : "禁用"}推荐${stateKey === "skill" ? " Skill" : " MCP"}`, `${String(selectedItem.name)} 的状态将写入当前项目配置。`)) {
|
|
176
|
+
const current = new Set(state[stateField]);
|
|
177
|
+
if (nextAction === "enable")
|
|
178
|
+
current.add(targetId);
|
|
179
|
+
else
|
|
180
|
+
current.delete(targetId);
|
|
181
|
+
state[stateField] = [...current];
|
|
182
|
+
await saveRecommendationState(ctx.cwd, state);
|
|
183
|
+
ctx.ui.notify(`${nextAction === "enable" ? "已启用" : "已禁用"} ${String(selectedItem.name)}(/reload 后生效;打开 /panel 可可视化管理)`, "info");
|
|
184
|
+
}
|
|
185
|
+
return;
|
|
186
|
+
}
|
|
187
|
+
if (actionChoice.startsWith("📦")) {
|
|
188
|
+
const plan = installPrep.find((candidate) => candidate.id === targetId);
|
|
189
|
+
ctx.ui.notify(JSON.stringify({
|
|
190
|
+
schemaVersion: "psyclaw/recommendation-install/v1",
|
|
191
|
+
kind: stateKey,
|
|
192
|
+
id: targetId,
|
|
193
|
+
name: selectedItem.name,
|
|
194
|
+
sourceRef: selectedItem.sourceRef,
|
|
195
|
+
plan: plan ?? null,
|
|
196
|
+
status: plan?.status ?? "review-required",
|
|
197
|
+
next: "安装预检与启用是两个步骤;打开 /panel 查看完整计划,安装完成后再执行启用。",
|
|
198
|
+
}, null, 2), "info");
|
|
199
|
+
return;
|
|
200
|
+
}
|
|
201
|
+
if (actionChoice.startsWith("🌐")) {
|
|
202
|
+
ctx.ui.notify(`来源仓库: ${String(selectedItem.sourceRef ?? "无")}`, "info");
|
|
203
|
+
return;
|
|
204
|
+
}
|
|
205
|
+
return;
|
|
206
|
+
}
|
|
207
|
+
// 2. 命令行显式参数调用
|
|
208
|
+
if ((verb === "enable" || verb === "disable" || verb === "install") && !id) {
|
|
209
|
+
throw new Error(`Usage: /${kind} ${verb} <id>`);
|
|
210
|
+
}
|
|
211
|
+
if (verb === "enable" || verb === "disable") {
|
|
212
|
+
const item = items.find((candidate) => candidate.id === id);
|
|
213
|
+
if (!item)
|
|
214
|
+
throw new Error(`未找到推荐${stateKey === "skill" ? " Skill" : " MCP"}: ${id}`);
|
|
215
|
+
if (!ctx.hasUI || !(await ctx.ui.confirm(`${verb === "enable" ? "启用" : "禁用"}推荐${stateKey === "skill" ? " Skill" : " MCP"}`, `${String(item.name)} 的状态将写入当前项目配置。`)))
|
|
216
|
+
return;
|
|
217
|
+
const current = new Set(state[stateField]);
|
|
218
|
+
if (verb === "enable")
|
|
219
|
+
current.add(id);
|
|
220
|
+
else
|
|
221
|
+
current.delete(id);
|
|
222
|
+
state[stateField] = [...current];
|
|
223
|
+
await saveRecommendationState(ctx.cwd, state);
|
|
224
|
+
ctx.ui.notify(`${verb === "enable" ? "已启用" : "已禁用"} ${String(item.name)}(/reload 后生效;打开 /panel 可可视化管理)`, "info");
|
|
225
|
+
return;
|
|
226
|
+
}
|
|
227
|
+
if (verb === "install") {
|
|
228
|
+
const item = items.find((candidate) => candidate.id === id);
|
|
229
|
+
const plan = installPrep.find((candidate) => candidate.id === id);
|
|
230
|
+
if (!item)
|
|
231
|
+
throw new Error(`未找到推荐${kind === "skills" ? " Skill" : " MCP"}: ${id}`);
|
|
232
|
+
ctx.ui.notify(JSON.stringify({ schemaVersion: "psyclaw/recommendation-install/v1", kind: stateKey, id, name: item.name, sourceRef: item.sourceRef, plan: plan ?? null, status: plan?.status ?? "review-required", next: "安装预检与启用是两个步骤;打开 /panel 查看完整计划,安装完成后再执行启用。" }, null, 2), "info");
|
|
233
|
+
return;
|
|
234
|
+
}
|
|
235
|
+
// 3. 非交互模式或 status 命令回退输出
|
|
236
|
+
const enabled = new Set(state[stateField]);
|
|
237
|
+
const lines = items.map((item) => `${enabled.has(String(item.id)) ? "[on]" : "[off]"} ${String(item.id)} — ${String(item.name)} (/${kind} enable|disable ${String(item.id)})`);
|
|
238
|
+
lines.push("", "可视化管理:打开 /panel(推荐 + 现有 Skill/MCP 的启用/停用)");
|
|
239
|
+
ctx.ui.notify(lines.length ? lines.join("\n") : `暂无推荐${kind === "skills" ? " Skill" : " MCP"}`, "info");
|
|
240
|
+
}
|
|
241
|
+
/** Run meta-analysis with the staged progress captured into a buffer (no TTY). */
|
|
242
|
+
async function runMetaAnalysisStaged(root, target, nStudies) {
|
|
243
|
+
const stages = [];
|
|
244
|
+
const result = await runMetaAnalysis(root, {
|
|
245
|
+
target,
|
|
246
|
+
nStudies,
|
|
247
|
+
runner: createStageRunner({ write: (line) => { stages.push(line); }, color: false }),
|
|
248
|
+
});
|
|
249
|
+
return { result, stages };
|
|
250
|
+
}
|
|
251
|
+
/** Shape a workflow result as a tool response the agent can reason over. */
|
|
252
|
+
function workflowToolResult(workflow, result, stages) {
|
|
253
|
+
return {
|
|
254
|
+
content: [{
|
|
255
|
+
type: "text",
|
|
256
|
+
text: JSON.stringify({
|
|
257
|
+
schemaVersion: "psyclaw/workflow-result/v1",
|
|
258
|
+
workflow,
|
|
259
|
+
status: result.verdict === "pass" ? "completed" : "blocked",
|
|
260
|
+
verdict: result.verdict,
|
|
261
|
+
runId: result.runId,
|
|
262
|
+
stages,
|
|
263
|
+
gates: result.gates.map((gate) => ({ id: gate.gateId, ok: gate.ok, severity: gate.severity, reason: gate.reason })),
|
|
264
|
+
outputs: result.outputPaths,
|
|
265
|
+
next: result.verdict === "pass"
|
|
266
|
+
? "Review the outputs before relying on them; statistics stay delegated to R (metafor) until the effect-size dataset is complete."
|
|
267
|
+
: "Resolve the blocked gates (e.g. supply data/clean/effects.csv), then rerun the workflow.",
|
|
268
|
+
}, null, 2),
|
|
269
|
+
}],
|
|
270
|
+
details: { workflow, runId: result.runId, verdict: result.verdict, outputPaths: result.outputPaths, gateCount: result.gates.length },
|
|
271
|
+
};
|
|
272
|
+
}
|
|
273
|
+
function workflowToolError(workflow, error) {
|
|
274
|
+
return {
|
|
275
|
+
content: [{ type: "text", text: `Workflow ${workflow} failed: ${error instanceof Error ? error.message : String(error)}` }],
|
|
276
|
+
details: { workflow, status: "failed" },
|
|
277
|
+
};
|
|
278
|
+
}
|
|
279
|
+
async function runMetaAnalysisTool(root, target, nStudies) {
|
|
280
|
+
try {
|
|
281
|
+
const { result, stages } = await runMetaAnalysisStaged(root, target, nStudies);
|
|
282
|
+
return workflowToolResult("meta-analysis", result, stages);
|
|
283
|
+
}
|
|
284
|
+
catch (error) {
|
|
285
|
+
return workflowToolError("meta-analysis", error);
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
/**
|
|
289
|
+
* Explicit workflow dispatch shared by the workbench's direct (`workflow`
|
|
290
|
+
* parameter) and natural-language routed paths, so every workflow result has
|
|
291
|
+
* the same `psyclaw/workflow-result/v1` contract.
|
|
292
|
+
*/
|
|
293
|
+
export async function runWorkflowTool(root, workflow, target, nStudies) {
|
|
294
|
+
const id = workflow.trim();
|
|
295
|
+
if (id === "meta-analysis") {
|
|
296
|
+
const cleanedTarget = target?.trim();
|
|
297
|
+
if (!cleanedTarget) {
|
|
298
|
+
return {
|
|
299
|
+
content: [{ type: "text", text: JSON.stringify({ schemaVersion: "psyclaw/workflow-result/v1", workflow: id, status: "blocked", verdict: "blocked", reason: "meta-analysis requires a target, e.g. online-learning-engagement" }, null, 2) }],
|
|
300
|
+
details: { workflow: id, status: "blocked" },
|
|
301
|
+
};
|
|
302
|
+
}
|
|
303
|
+
const count = Math.min(200, Math.max(2, Math.round(nStudies ?? 20)));
|
|
304
|
+
return runMetaAnalysisTool(root, cleanedTarget, count);
|
|
305
|
+
}
|
|
306
|
+
const runner = WORKFLOW_RUNNERS[id];
|
|
307
|
+
if (!runner) {
|
|
308
|
+
return {
|
|
309
|
+
content: [{ type: "text", text: JSON.stringify({ schemaVersion: "psyclaw/workflow-result/v1", workflow: id, status: "blocked", verdict: "blocked", reason: `Unknown workflow: ${id}; expected meta-analysis, literature-review, analysis-delegation, writing-review, or expert-review` }, null, 2) }],
|
|
310
|
+
details: { workflow: id, status: "blocked" },
|
|
311
|
+
};
|
|
312
|
+
}
|
|
313
|
+
try {
|
|
314
|
+
const result = await runner(root);
|
|
315
|
+
return workflowToolResult(id, result, []);
|
|
316
|
+
}
|
|
317
|
+
catch (error) {
|
|
318
|
+
return workflowToolError(id, error);
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
/** Turn a natural-language meta-analysis request into a searchable target. */
|
|
322
|
+
export function cleanMetaTarget(request) {
|
|
323
|
+
const cleaned = request
|
|
324
|
+
.replace(/请|帮我|麻烦|对|针对|进行|做|执行|开展|一下|元分析|系统综述|系统评价|meta[- ]?analysis|meta分析|效应量|森林图/gi, "")
|
|
325
|
+
.replace(/\b(of|for|on|the|a|an|and|with|in|to)\b/gi, " ")
|
|
326
|
+
.replace(/\s+/g, " ")
|
|
327
|
+
.trim();
|
|
328
|
+
return cleaned || request.trim();
|
|
329
|
+
}
|
|
330
|
+
const WORKFLOW_RUNNERS = {
|
|
331
|
+
"literature-review": runLiteratureReview,
|
|
332
|
+
"analysis-delegation": runAnalysisDelegation,
|
|
333
|
+
"writing-review": runWritingReview,
|
|
334
|
+
"expert-review": runExpertReview,
|
|
335
|
+
};
|
|
336
|
+
export default function psyclawExtension(pi) {
|
|
337
|
+
const developerCommands = process.env.PSYCLAW_DEVELOPER_COMMANDS === "1";
|
|
338
|
+
const legacyTestApi = typeof pi.registerTool !== "function";
|
|
339
|
+
pi.registerCommand("research", {
|
|
340
|
+
description: "Bootstrap an evidence-grounded psyclaw research project",
|
|
341
|
+
handler: async (args, ctx) => {
|
|
342
|
+
try {
|
|
343
|
+
const parsed = parseResearchArgs(args);
|
|
344
|
+
const project = await bootstrapProject({ root: ctx.cwd, ...parsed });
|
|
345
|
+
ctx.ui.notify(`Initialized ${project.id} (${project.paradigm})`, "info");
|
|
346
|
+
}
|
|
347
|
+
catch (error) {
|
|
348
|
+
await notifyError(ctx, error);
|
|
349
|
+
}
|
|
350
|
+
},
|
|
351
|
+
});
|
|
352
|
+
if (developerCommands || legacyTestApi)
|
|
353
|
+
pi.registerCommand("verify", {
|
|
354
|
+
description: "Write a machine-readable psyclaw handoff checkpoint",
|
|
355
|
+
handler: async (_args, ctx) => {
|
|
356
|
+
try {
|
|
357
|
+
const paths = projectPaths(ctx.cwd);
|
|
358
|
+
const project = await import("node:fs/promises").then(({ readFile }) => readFile(paths.project, "utf8").then((text) => asProject(JSON.parse(text))));
|
|
359
|
+
await writeHandoff(ctx.cwd, {
|
|
360
|
+
projectId: project.id,
|
|
361
|
+
runId: `run_${Date.now()}`,
|
|
362
|
+
goal: project.goal,
|
|
363
|
+
completed: ["project bootstrap"],
|
|
364
|
+
verified: ["project.json exists"],
|
|
365
|
+
blocked: ["evidence ledger has not been reviewed"],
|
|
366
|
+
nextSteps: ["import a local source and create Claim-Evidence links"],
|
|
367
|
+
verificationCommands: ["pnpm typecheck", "pnpm test"],
|
|
368
|
+
generatedAt: new Date().toISOString(),
|
|
369
|
+
});
|
|
370
|
+
ctx.ui.notify("Wrote notes/HANDOFF.md and notes/handoff.json", "info");
|
|
371
|
+
}
|
|
372
|
+
catch (error) {
|
|
373
|
+
await notifyError(ctx, error);
|
|
374
|
+
}
|
|
375
|
+
},
|
|
376
|
+
});
|
|
377
|
+
pi.registerCommand("brief", {
|
|
378
|
+
description: "Run the offline evidence-gated research brief",
|
|
379
|
+
handler: async (_args, ctx) => {
|
|
380
|
+
try {
|
|
381
|
+
const result = await runOfflineBrief(ctx.cwd);
|
|
382
|
+
ctx.ui.notify(result.verdict === "pass" ? `Brief ready: ${result.briefPath}` : "Brief blocked by evidence gates", result.verdict === "pass" ? "info" : "warning");
|
|
383
|
+
}
|
|
384
|
+
catch (error) {
|
|
385
|
+
await notifyError(ctx, error);
|
|
386
|
+
}
|
|
387
|
+
},
|
|
388
|
+
});
|
|
389
|
+
if (!legacyTestApi)
|
|
390
|
+
pi.registerCommand("skills", {
|
|
391
|
+
description: "查看并启用/禁用推荐 Skill",
|
|
392
|
+
handler: async (args, ctx) => {
|
|
393
|
+
try {
|
|
394
|
+
await showRecommended("skills", args, ctx);
|
|
395
|
+
}
|
|
396
|
+
catch (error) {
|
|
397
|
+
await notifyError(ctx, error);
|
|
398
|
+
}
|
|
399
|
+
},
|
|
400
|
+
});
|
|
401
|
+
if (!legacyTestApi)
|
|
402
|
+
pi.registerCommand("mcp", {
|
|
403
|
+
description: "查看并启用/禁用推荐 MCP",
|
|
404
|
+
handler: async (args, ctx) => {
|
|
405
|
+
try {
|
|
406
|
+
await showRecommended("mcp", args, ctx);
|
|
407
|
+
}
|
|
408
|
+
catch (error) {
|
|
409
|
+
await notifyError(ctx, error);
|
|
410
|
+
}
|
|
411
|
+
},
|
|
412
|
+
});
|
|
413
|
+
if (!legacyTestApi)
|
|
414
|
+
pi.registerCommand("install", {
|
|
415
|
+
description: "查看推荐 Skill/MCP,或生成安装预检计划",
|
|
416
|
+
handler: async (args, ctx) => {
|
|
417
|
+
try {
|
|
418
|
+
const [kind, id] = args.trim().split(/\s+/, 2);
|
|
419
|
+
if (!kind) {
|
|
420
|
+
const [skills, mcps] = await Promise.all([recommendedItems("skills"), recommendedItems("mcp")]);
|
|
421
|
+
const skillLines = skills.items.slice(0, 8).map((item) => `Skill: ${String(item.id)} — ${String(item.name)}`);
|
|
422
|
+
const mcpLines = mcps.items.slice(0, 8).map((item) => `MCP: ${String(item.id)} — ${String(item.name)}`);
|
|
423
|
+
ctx.ui.notify(["推荐安装入口", "", ...skillLines, ...mcpLines, "", "安装:/install skill|mcp <id>", "管理:/skills 或 /mcp", "也可以打开 /panel 查看推荐页面"].join("\n"), "info");
|
|
424
|
+
return;
|
|
425
|
+
}
|
|
426
|
+
if (kind !== "skill" && kind !== "mcp")
|
|
427
|
+
throw new Error("Usage: /install skill|mcp <id>");
|
|
428
|
+
if (!id) {
|
|
429
|
+
await showRecommended(kind === "skill" ? "skills" : "mcp", "", ctx);
|
|
430
|
+
return;
|
|
431
|
+
}
|
|
432
|
+
await showRecommended(kind === "skill" ? "skills" : "mcp", `install ${id}`, ctx);
|
|
433
|
+
}
|
|
434
|
+
catch (error) {
|
|
435
|
+
await notifyError(ctx, error);
|
|
436
|
+
}
|
|
437
|
+
},
|
|
438
|
+
});
|
|
439
|
+
if (typeof pi.registerTool === "function") {
|
|
440
|
+
pi.registerTool({
|
|
441
|
+
name: "psyclaw_skill",
|
|
442
|
+
label: "Research skill",
|
|
443
|
+
description: "Load one trusted bundled psyclaw core skill and make its use visible to the user. Use this instead of directly reading a core SKILL.md file.",
|
|
444
|
+
parameters: Type.Object({
|
|
445
|
+
name: Type.String({ description: "Core skill name: research-intake, evidence-capture, citation-audit, or research-brief" }),
|
|
446
|
+
purpose: Type.Optional(Type.String({ description: "Short user-facing reason for using this skill now" })),
|
|
447
|
+
}),
|
|
448
|
+
executionMode: "sequential",
|
|
449
|
+
async execute(_toolCallId, params) {
|
|
450
|
+
const name = params.name.trim();
|
|
451
|
+
if (!CORE_SKILLS.has(name)) {
|
|
452
|
+
return {
|
|
453
|
+
content: [{ type: "text", text: `Skill not available in the trusted core pack: ${name}` }],
|
|
454
|
+
details: { status: "blocked", name },
|
|
455
|
+
};
|
|
456
|
+
}
|
|
457
|
+
try {
|
|
458
|
+
const instructions = await readFile(coreSkillPath(name), "utf8");
|
|
459
|
+
return {
|
|
460
|
+
content: [{ type: "text", text: `正在使用 Skill:${name}${params.purpose ? `(${params.purpose})` : ""}\n\n${instructions}` }],
|
|
461
|
+
details: { status: "active", name, ...(params.purpose === undefined ? {} : { purpose: params.purpose }) },
|
|
462
|
+
};
|
|
463
|
+
}
|
|
464
|
+
catch (error) {
|
|
465
|
+
return {
|
|
466
|
+
content: [{ type: "text", text: `Skill could not be loaded: ${error instanceof Error ? error.message : String(error)}` }],
|
|
467
|
+
details: { status: "failed", name },
|
|
468
|
+
};
|
|
469
|
+
}
|
|
470
|
+
},
|
|
471
|
+
});
|
|
472
|
+
pi.registerTool({
|
|
473
|
+
name: "psyclaw_workbench",
|
|
474
|
+
label: "Research workbench",
|
|
475
|
+
description: "Primary psyclaw workbench for durable research state, evidence tracking, academic analysis/report contracts, human approval, and recoverable workflows. Infer the workflow from natural language, or pass an explicit `workflow` id to run it directly. Workflows: meta-analysis (real OpenAlex literature search, effect-size dataset contract at data/clean/effects.csv, R metafor delegation for REML / I² / Egger / forest — psyclaw never fabricates statistics), literature-review, analysis-delegation, writing-review, expert-review, institutional-fulltext. For paper/full-text requests call this first; for data analysis or academic reports route through research-intake -> evidence-capture -> citation-audit -> research-brief and record figure/document deliverables. Do not handle credentials.",
|
|
476
|
+
parameters: Type.Object({
|
|
477
|
+
request: Type.String({ description: "The user's research task in natural language" }),
|
|
478
|
+
identifier: Type.Optional(Type.String({ description: "DOI, publisher URL, or exact paper title when relevant" })),
|
|
479
|
+
workflow: Type.Optional(Type.String({ description: "Explicit workflow id to run directly: meta-analysis, literature-review, analysis-delegation, writing-review, or expert-review (otherwise inferred from the request)" })),
|
|
480
|
+
target: Type.Optional(Type.String({ description: "Research target topic; required for meta-analysis (e.g. online-learning-engagement)" })),
|
|
481
|
+
nStudies: Type.Optional(Type.Number({ description: "Studies to search for meta-analysis (default 20, clamped to 2-200)" })),
|
|
482
|
+
}),
|
|
483
|
+
executionMode: "sequential",
|
|
484
|
+
async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
|
|
485
|
+
try {
|
|
486
|
+
const request = params.request.trim();
|
|
487
|
+
const combined = `${request} ${params.identifier ?? ""}`;
|
|
488
|
+
const explicitWorkflow = params.workflow?.trim();
|
|
489
|
+
if (explicitWorkflow) {
|
|
490
|
+
return runWorkflowTool(ctx.cwd, explicitWorkflow, params.target, params.nStudies);
|
|
491
|
+
}
|
|
492
|
+
const metaAnalysisIntent = /meta[- ]?analysis|meta分析|元分析|系统综述|系统评价|效应量|forest plot|funnel|publication bias/i.test(combined);
|
|
493
|
+
const publishIntent = /publish|finalize|发布|导出|定稿|投稿|export (to )?(docx|word|apa)|docx export|apa ?7/i.test(combined);
|
|
494
|
+
const knowledgeMapIntent = /knowledge[ -]?map|literature[ -]?map|literature review|文献综述|知识图谱|知识地图|文献地图/i.test(combined);
|
|
495
|
+
const expertReviewIntent = /expert review|peer review|reviewer|专家评审|同行评审|审稿/i.test(combined);
|
|
496
|
+
const journalArtifactIntent = /journal style|journal format|publication[- ]ready|figure artifact|期刊格式|期刊风格|图表产物|投稿图表/i.test(combined);
|
|
497
|
+
const fulltextIntent = !knowledgeMapIntent && !expertReviewIntent && /\b(doi|paper|article|full.?text|pdf|publisher|institution)\b|学校|机构|论文|全文|下载/i.test(combined);
|
|
498
|
+
const academicReportIntent = journalArtifactIntent || /分析|数据集|统计|报告|学术|论文|word|docx|markdown|图表|可视化|csv|sav|dataset|report/i.test(combined);
|
|
499
|
+
if (metaAnalysisIntent) {
|
|
500
|
+
return runWorkflowTool(ctx.cwd, "meta-analysis", cleanMetaTarget(request), 20);
|
|
501
|
+
}
|
|
502
|
+
if (knowledgeMapIntent) {
|
|
503
|
+
return runWorkflowTool(ctx.cwd, "literature-review");
|
|
504
|
+
}
|
|
505
|
+
if (expertReviewIntent) {
|
|
506
|
+
return runWorkflowTool(ctx.cwd, "expert-review");
|
|
507
|
+
}
|
|
508
|
+
if (publishIntent) {
|
|
509
|
+
try {
|
|
510
|
+
const result = await publishManuscript(ctx.cwd, { name: "论文初稿", exportDocx: true });
|
|
511
|
+
return {
|
|
512
|
+
content: [{ type: "text", text: JSON.stringify({
|
|
513
|
+
schemaVersion: "psyclaw/publish-result/v1",
|
|
514
|
+
workflow: "publish-manuscript",
|
|
515
|
+
status: "completed",
|
|
516
|
+
markdownPath: result.markdownPath,
|
|
517
|
+
docxPath: result.docxPath,
|
|
518
|
+
markdownSha256: result.markdownSha256,
|
|
519
|
+
docxSha256: result.docxSha256,
|
|
520
|
+
evidenceIds: result.evidenceIds,
|
|
521
|
+
next: "手稿已发布到 paper/(panel 与文档清单会自动识别)。如需再修改,直接编辑 paper/论文初稿.md 或继续在 panel 手稿编辑器修改后重新发布。",
|
|
522
|
+
}, null, 2) }],
|
|
523
|
+
details: { workflow: "publish-manuscript", markdownPath: result.markdownPath, docxPath: result.docxPath, evidenceIds: result.evidenceIds },
|
|
524
|
+
};
|
|
525
|
+
}
|
|
526
|
+
catch (error) {
|
|
527
|
+
return workflowToolError("publish-manuscript", error);
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
if (academicReportIntent && (!fulltextIntent || journalArtifactIntent)) {
|
|
531
|
+
return {
|
|
532
|
+
content: [{ type: "text", text: JSON.stringify({
|
|
533
|
+
schemaVersion: "psyclaw/academic-report-plan/v1",
|
|
534
|
+
workflow: "academic-analysis-report",
|
|
535
|
+
status: "plan-required",
|
|
536
|
+
requiredSkills: ["research-intake", "evidence-capture", "citation-audit", "research-brief"],
|
|
537
|
+
requiredDeliverables: ["analysis-plan", "source-backed-reference-list", "publication-ready-figures-or-reproducible-plot-script", "markdown-report", "docx-report"],
|
|
538
|
+
documentExport: {
|
|
539
|
+
primary: "pandoc",
|
|
540
|
+
fallback: "python-docx script only after a failed Pandoc installation",
|
|
541
|
+
missingDependencyAction: "detect package manager, request side-effect approval, install with bash, verify version, and write a structured receipt",
|
|
542
|
+
},
|
|
543
|
+
dependencyReceipt: {
|
|
544
|
+
schemaVersion: "psyclaw/dependency-receipt/v1",
|
|
545
|
+
requiredFields: ["dependency", "requestedVersion", "resolvedVersion", "packageManager", "command", "approvedAt", "verifiedAt", "status"],
|
|
546
|
+
secretPolicy: "never record credentials, tokens, or environment values",
|
|
547
|
+
},
|
|
548
|
+
costNotice: {
|
|
549
|
+
confirmBeforeStart: true,
|
|
550
|
+
message: "文献检索与多源核验、全稿写作、DOCX 导出会消耗较多 token 与时间(长会话下缓存重读是主要费用)。开始前将告知预估 token/费用/时间,并可在更小范围或分段会话中执行。",
|
|
551
|
+
},
|
|
552
|
+
message: "Proceed through the four core skills and keep the report blocked until citations, figures, and document export are accounted for.",
|
|
553
|
+
}, null, 2) }],
|
|
554
|
+
details: { status: "plan-required", workflow: "academic-analysis-report" },
|
|
555
|
+
};
|
|
556
|
+
}
|
|
557
|
+
if (!fulltextIntent) {
|
|
558
|
+
return {
|
|
559
|
+
content: [{ type: "text", text: JSON.stringify({
|
|
560
|
+
schemaVersion: "psyclaw/workbench-routing/v1",
|
|
561
|
+
status: "conversation-first",
|
|
562
|
+
message: "This request does not yet require a durable psyclaw workflow. Continue conversationally and invoke the workbench when an artifact, evidence record, approval, or resumable run is needed.",
|
|
563
|
+
}, null, 2) }],
|
|
564
|
+
details: { status: "conversation-first", request },
|
|
565
|
+
};
|
|
566
|
+
}
|
|
567
|
+
const result = await runInstitutionalFulltext(ctx.cwd, params.identifier ?? request);
|
|
568
|
+
return {
|
|
569
|
+
content: [{ type: "text", text: JSON.stringify({
|
|
570
|
+
schemaVersion: "psyclaw/workbench-result/v1",
|
|
571
|
+
workflow: "institutional-fulltext",
|
|
572
|
+
status: "awaiting-human-approval",
|
|
573
|
+
verdict: result.verdict,
|
|
574
|
+
runId: result.runId,
|
|
575
|
+
outputs: result.outputPaths,
|
|
576
|
+
next: "Ask the user to complete institutional login in the visible browser and confirm the exact article before any download.",
|
|
577
|
+
}, null, 2) }],
|
|
578
|
+
details: { runId: result.runId, verdict: result.verdict, outputPaths: result.outputPaths, approval: "pending" },
|
|
579
|
+
};
|
|
580
|
+
}
|
|
581
|
+
catch (error) {
|
|
582
|
+
return {
|
|
583
|
+
content: [{ type: "text", text: `Institutional full-text planning failed: ${error instanceof Error ? error.message : String(error)}` }],
|
|
584
|
+
details: { status: "failed" },
|
|
585
|
+
};
|
|
586
|
+
}
|
|
587
|
+
},
|
|
588
|
+
});
|
|
589
|
+
pi.registerTool({
|
|
590
|
+
name: "psyclaw_cite",
|
|
591
|
+
label: "Record a citation use",
|
|
592
|
+
description: "Record one in-text citation while writing a manuscript: the DOI reference (verified against the archive) plus the reason it was cited and the sentence context. Call this every time a citation is added to the paper so the reference archive stays traceable — which citation appears where, why it was chosen, and whether its metadata was verified (docs/文档规范.md §8).",
|
|
593
|
+
parameters: Type.Object({
|
|
594
|
+
doi: Type.String({ description: "DOI of the cited reference" }),
|
|
595
|
+
reason: Type.String({ description: "One-line reason this source is cited at this position (e.g. 支持:社会支持缓冲压力假说)" }),
|
|
596
|
+
context: Type.String({ description: "The sentence/context in the manuscript where the citation appears" }),
|
|
597
|
+
section: Type.Optional(Type.String({ description: "Manuscript section, e.g. 1 引言" })),
|
|
598
|
+
}),
|
|
599
|
+
executionMode: "sequential",
|
|
600
|
+
async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
|
|
601
|
+
try {
|
|
602
|
+
const result = await recordCitationUse(ctx.cwd, {
|
|
603
|
+
doi: params.doi,
|
|
604
|
+
reason: params.reason,
|
|
605
|
+
context: params.context,
|
|
606
|
+
...(params.section === undefined || !params.section.trim() ? {} : { section: params.section }),
|
|
607
|
+
});
|
|
608
|
+
const ref = result.reference;
|
|
609
|
+
return {
|
|
610
|
+
content: [{ type: "text", text: JSON.stringify({
|
|
611
|
+
schemaVersion: "psyclaw/citation-use/v1",
|
|
612
|
+
citationId: result.record.citationId,
|
|
613
|
+
doi: result.record.doi,
|
|
614
|
+
verified: result.record.verified,
|
|
615
|
+
reason: result.record.reason,
|
|
616
|
+
referenceTitle: ref?.title ?? null,
|
|
617
|
+
archived: ref !== null,
|
|
618
|
+
next: result.record.verified
|
|
619
|
+
? "引用已双源核验并写入存档(含引用原因)。继续写作时保持每次引用都登记用途。"
|
|
620
|
+
: "该 DOI 未能完成双源核验,引用用途已如实登记(verified=false);请在最终交付前补核验或替换来源。",
|
|
621
|
+
}, null, 2) }],
|
|
622
|
+
details: { citationId: result.record.citationId, doi: result.record.doi, verified: result.record.verified },
|
|
623
|
+
};
|
|
624
|
+
}
|
|
625
|
+
catch (error) {
|
|
626
|
+
return {
|
|
627
|
+
content: [{ type: "text", text: `记录引用失败:${error instanceof Error ? error.message : String(error)}` }],
|
|
628
|
+
details: { status: "failed" },
|
|
629
|
+
};
|
|
630
|
+
}
|
|
631
|
+
},
|
|
632
|
+
});
|
|
633
|
+
}
|
|
634
|
+
if (developerCommands || legacyTestApi)
|
|
635
|
+
pi.registerCommand("model", {
|
|
636
|
+
description: "List or switch the active Pi model without exposing credentials",
|
|
637
|
+
handler: async (args, ctx) => {
|
|
638
|
+
try {
|
|
639
|
+
if (!args.trim()) {
|
|
640
|
+
ctx.ui.notify(modelSummary(ctx), "info");
|
|
641
|
+
return;
|
|
642
|
+
}
|
|
643
|
+
const ref = parseModelRef(args);
|
|
644
|
+
const model = ctx.modelRegistry.find(ref.provider, ref.id);
|
|
645
|
+
if (!model)
|
|
646
|
+
throw new Error(`Model not found: ${ref.provider}/${ref.id}`);
|
|
647
|
+
const changed = await pi.setModel(model);
|
|
648
|
+
if (!changed)
|
|
649
|
+
throw new Error("Model authentication is not configured");
|
|
650
|
+
ctx.ui.notify(`Model selected: ${ref.provider}/${ref.id}`, "info");
|
|
651
|
+
}
|
|
652
|
+
catch (error) {
|
|
653
|
+
await notifyError(ctx, error);
|
|
654
|
+
}
|
|
655
|
+
},
|
|
656
|
+
});
|
|
657
|
+
if (developerCommands || legacyTestApi)
|
|
658
|
+
pi.registerCommand("agents", {
|
|
659
|
+
description: "Run a human-approved, read-only Pi research worker",
|
|
660
|
+
handler: async (args, ctx) => {
|
|
661
|
+
const objective = args.trim();
|
|
662
|
+
if (!objective) {
|
|
663
|
+
ctx.ui.notify("Usage: /agents <bounded read-only research task>", "info");
|
|
664
|
+
return;
|
|
665
|
+
}
|
|
666
|
+
if (objective.length > 4_000) {
|
|
667
|
+
ctx.ui.notify("Agent task is too long; split it into smaller bounded tasks", "error");
|
|
668
|
+
return;
|
|
669
|
+
}
|
|
670
|
+
if (!ctx.hasUI) {
|
|
671
|
+
ctx.ui.notify("Multi-agent execution requires an explicit interactive approval", "warning");
|
|
672
|
+
return;
|
|
673
|
+
}
|
|
674
|
+
// Do not let the runner create a partial `.psyclaw/runs` tree for an
|
|
675
|
+
// uninitialized or corrupt project. `/research` (or `psyclaw init`) is
|
|
676
|
+
// the explicit state-creation step.
|
|
677
|
+
try {
|
|
678
|
+
await readProject(ctx.cwd);
|
|
679
|
+
}
|
|
680
|
+
catch {
|
|
681
|
+
ctx.ui.notify("Initialize a psyclaw project with /research before running /agents", "warning");
|
|
682
|
+
return;
|
|
683
|
+
}
|
|
684
|
+
const approved = await ctx.ui.confirm("Run read-only research worker?", "The worker runs in a separate Pi process with extensions, skills, context files, and mutating tools disabled.");
|
|
685
|
+
if (!approved) {
|
|
686
|
+
ctx.ui.notify("Agent run canceled", "info");
|
|
687
|
+
return;
|
|
688
|
+
}
|
|
689
|
+
if (activeAgentRuns.has(ctx.cwd)) {
|
|
690
|
+
ctx.ui.notify("A psyclaw agent run is already active for this project", "warning");
|
|
691
|
+
return;
|
|
692
|
+
}
|
|
693
|
+
activeAgentRuns.add(ctx.cwd);
|
|
694
|
+
try {
|
|
695
|
+
const runId = `pi_agent_${Date.now()}`;
|
|
696
|
+
const plan = researchTaskPlan(runId, objective);
|
|
697
|
+
await mkdir(join(ctx.cwd, ".psyclaw", "plans"), { recursive: true });
|
|
698
|
+
await atomicWriteFile(join(ctx.cwd, ".psyclaw", "plans", `${runId}.json`), `${JSON.stringify(plan, null, 2)}\n`);
|
|
699
|
+
const eventLog = new RunEventLog(ctx.cwd, runId);
|
|
700
|
+
const result = await runPlanWithPi(plan, {
|
|
701
|
+
cwd: ctx.cwd,
|
|
702
|
+
agentDir: join(ctx.cwd, ".psyclaw", "pi-agent"),
|
|
703
|
+
...(ctx.model?.provider === undefined ? {} : { provider: ctx.model.provider }),
|
|
704
|
+
...(ctx.model?.id === undefined ? {} : { model: ctx.model.id }),
|
|
705
|
+
env: providerEnvironment(ctx.model?.provider),
|
|
706
|
+
root: ctx.cwd,
|
|
707
|
+
pauseRequested: async () => {
|
|
708
|
+
try {
|
|
709
|
+
await import("node:fs/promises").then(({ access }) => access(join(ctx.cwd, ".psyclaw", "runs", `${runId}.pause`)));
|
|
710
|
+
return true;
|
|
711
|
+
}
|
|
712
|
+
catch {
|
|
713
|
+
return false;
|
|
714
|
+
}
|
|
715
|
+
},
|
|
716
|
+
onEvent: async (event) => { await eventLog.append(event); },
|
|
717
|
+
});
|
|
718
|
+
pi.appendEntry("psyclaw:agent-run", {
|
|
719
|
+
runId,
|
|
720
|
+
status: result.status,
|
|
721
|
+
diagnostics: result.diagnostics,
|
|
722
|
+
recordedAt: new Date().toISOString(),
|
|
723
|
+
});
|
|
724
|
+
ctx.ui.notify(`Agent run ${result.status}: ${result.diagnostics.join("; ") || "verified"}`, result.status === "completed" ? "info" : "warning");
|
|
725
|
+
}
|
|
726
|
+
catch (error) {
|
|
727
|
+
await notifyError(ctx, error);
|
|
728
|
+
}
|
|
729
|
+
finally {
|
|
730
|
+
activeAgentRuns.delete(ctx.cwd);
|
|
731
|
+
}
|
|
732
|
+
},
|
|
733
|
+
});
|
|
734
|
+
}
|
|
735
|
+
//# sourceMappingURL=extension.js.map
|