p-spore 0.1.0
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 +97 -0
- package/dist/agents/agent-manager.d.ts +57 -0
- package/dist/agents/agent-manager.d.ts.map +1 -0
- package/dist/agents/agent-manager.js +559 -0
- package/dist/agents/agent-manager.js.map +1 -0
- package/dist/agents/agent-tools.d.ts +4 -0
- package/dist/agents/agent-tools.d.ts.map +1 -0
- package/dist/agents/agent-tools.js +111 -0
- package/dist/agents/agent-tools.js.map +1 -0
- package/dist/agents/types.d.ts +52 -0
- package/dist/agents/types.d.ts.map +1 -0
- package/dist/agents/types.js +2 -0
- package/dist/agents/types.js.map +1 -0
- package/dist/application/agent-application.d.ts +89 -0
- package/dist/application/agent-application.d.ts.map +1 -0
- package/dist/application/agent-application.js +542 -0
- package/dist/application/agent-application.js.map +1 -0
- package/dist/automation/automation-tools.d.ts +4 -0
- package/dist/automation/automation-tools.d.ts.map +1 -0
- package/dist/automation/automation-tools.js +35 -0
- package/dist/automation/automation-tools.js.map +1 -0
- package/dist/automation/cron-adapter.d.ts +11 -0
- package/dist/automation/cron-adapter.d.ts.map +1 -0
- package/dist/automation/cron-adapter.js +48 -0
- package/dist/automation/cron-adapter.js.map +1 -0
- package/dist/automation/daemon-run-executor.d.ts +18 -0
- package/dist/automation/daemon-run-executor.d.ts.map +1 -0
- package/dist/automation/daemon-run-executor.js +69 -0
- package/dist/automation/daemon-run-executor.js.map +1 -0
- package/dist/automation/job-store.d.ts +12 -0
- package/dist/automation/job-store.d.ts.map +1 -0
- package/dist/automation/job-store.js +185 -0
- package/dist/automation/job-store.js.map +1 -0
- package/dist/automation/run-ledger.d.ts +25 -0
- package/dist/automation/run-ledger.d.ts.map +1 -0
- package/dist/automation/run-ledger.js +148 -0
- package/dist/automation/run-ledger.js.map +1 -0
- package/dist/automation/scheduler.d.ts +36 -0
- package/dist/automation/scheduler.d.ts.map +1 -0
- package/dist/automation/scheduler.js +300 -0
- package/dist/automation/scheduler.js.map +1 -0
- package/dist/automation/types.d.ts +81 -0
- package/dist/automation/types.d.ts.map +1 -0
- package/dist/automation/types.js +2 -0
- package/dist/automation/types.js.map +1 -0
- package/dist/cli/main.d.ts +4 -0
- package/dist/cli/main.d.ts.map +1 -0
- package/dist/cli/main.js +768 -0
- package/dist/cli/main.js.map +1 -0
- package/dist/config/config.d.ts +52 -0
- package/dist/config/config.d.ts.map +1 -0
- package/dist/config/config.js +345 -0
- package/dist/config/config.js.map +1 -0
- package/dist/daemon/daemon.d.ts +30 -0
- package/dist/daemon/daemon.d.ts.map +1 -0
- package/dist/daemon/daemon.js +437 -0
- package/dist/daemon/daemon.js.map +1 -0
- package/dist/daemon/supervisor.d.ts +18 -0
- package/dist/daemon/supervisor.d.ts.map +1 -0
- package/dist/daemon/supervisor.js +37 -0
- package/dist/daemon/supervisor.js.map +1 -0
- package/dist/desktop/daemon-bridge.d.ts +32 -0
- package/dist/desktop/daemon-bridge.d.ts.map +1 -0
- package/dist/desktop/daemon-bridge.js +204 -0
- package/dist/desktop/daemon-bridge.js.map +1 -0
- package/dist/desktop/daemon-supervisor.d.ts +2 -0
- package/dist/desktop/daemon-supervisor.d.ts.map +1 -0
- package/dist/desktop/daemon-supervisor.js +2 -0
- package/dist/desktop/daemon-supervisor.js.map +1 -0
- package/dist/desktop/ipc-contract.d.ts +153 -0
- package/dist/desktop/ipc-contract.d.ts.map +1 -0
- package/dist/desktop/ipc-contract.js +219 -0
- package/dist/desktop/ipc-contract.js.map +1 -0
- package/dist/desktop/main.d.ts +2 -0
- package/dist/desktop/main.d.ts.map +1 -0
- package/dist/desktop/main.js +594 -0
- package/dist/desktop/main.js.map +1 -0
- package/dist/desktop/preload.cjs +32 -0
- package/dist/desktop/renderer/index.html +284 -0
- package/dist/desktop/renderer/renderer.d.ts +2 -0
- package/dist/desktop/renderer/renderer.d.ts.map +1 -0
- package/dist/desktop/renderer/renderer.js +895 -0
- package/dist/desktop/renderer/renderer.js.map +1 -0
- package/dist/desktop/renderer/store.d.ts +33 -0
- package/dist/desktop/renderer/store.d.ts.map +1 -0
- package/dist/desktop/renderer/store.js +144 -0
- package/dist/desktop/renderer/store.js.map +1 -0
- package/dist/desktop/renderer/styles.css +540 -0
- package/dist/desktop/security.d.ts +5 -0
- package/dist/desktop/security.d.ts.map +1 -0
- package/dist/desktop/security.js +49 -0
- package/dist/desktop/security.js.map +1 -0
- package/dist/desktop/smoke-fixture.d.ts +3 -0
- package/dist/desktop/smoke-fixture.d.ts.map +1 -0
- package/dist/desktop/smoke-fixture.js +223 -0
- package/dist/desktop/smoke-fixture.js.map +1 -0
- package/dist/desktop/validation.d.ts +8 -0
- package/dist/desktop/validation.d.ts.map +1 -0
- package/dist/desktop/validation.js +341 -0
- package/dist/desktop/validation.js.map +1 -0
- package/dist/domain/runtime.d.ts +116 -0
- package/dist/domain/runtime.d.ts.map +1 -0
- package/dist/domain/runtime.js +2 -0
- package/dist/domain/runtime.js.map +1 -0
- package/dist/gateway/local-gateway.d.ts +23 -0
- package/dist/gateway/local-gateway.d.ts.map +1 -0
- package/dist/gateway/local-gateway.js +265 -0
- package/dist/gateway/local-gateway.js.map +1 -0
- package/dist/kernel/loop.d.ts +10 -0
- package/dist/kernel/loop.d.ts.map +1 -0
- package/dist/kernel/loop.js +41 -0
- package/dist/kernel/loop.js.map +1 -0
- package/dist/kernel/reducer.d.ts +3 -0
- package/dist/kernel/reducer.d.ts.map +1 -0
- package/dist/kernel/reducer.js +102 -0
- package/dist/kernel/reducer.js.map +1 -0
- package/dist/kernel/state.d.ts +93 -0
- package/dist/kernel/state.d.ts.map +1 -0
- package/dist/kernel/state.js +15 -0
- package/dist/kernel/state.js.map +1 -0
- package/dist/mcp/mcp-manager.d.ts +55 -0
- package/dist/mcp/mcp-manager.d.ts.map +1 -0
- package/dist/mcp/mcp-manager.js +591 -0
- package/dist/mcp/mcp-manager.js.map +1 -0
- package/dist/mcp/mcp-tools.d.ts +4 -0
- package/dist/mcp/mcp-tools.d.ts.map +1 -0
- package/dist/mcp/mcp-tools.js +53 -0
- package/dist/mcp/mcp-tools.js.map +1 -0
- package/dist/mcp/types.d.ts +73 -0
- package/dist/mcp/types.d.ts.map +1 -0
- package/dist/mcp/types.js +2 -0
- package/dist/mcp/types.js.map +1 -0
- package/dist/memory/bm25.d.ts +14 -0
- package/dist/memory/bm25.d.ts.map +1 -0
- package/dist/memory/bm25.js +40 -0
- package/dist/memory/bm25.js.map +1 -0
- package/dist/memory/context-loader.d.ts +26 -0
- package/dist/memory/context-loader.d.ts.map +1 -0
- package/dist/memory/context-loader.js +97 -0
- package/dist/memory/context-loader.js.map +1 -0
- package/dist/memory/memory-service.d.ts +30 -0
- package/dist/memory/memory-service.d.ts.map +1 -0
- package/dist/memory/memory-service.js +133 -0
- package/dist/memory/memory-service.js.map +1 -0
- package/dist/memory/memory-tools.d.ts +4 -0
- package/dist/memory/memory-tools.d.ts.map +1 -0
- package/dist/memory/memory-tools.js +49 -0
- package/dist/memory/memory-tools.js.map +1 -0
- package/dist/memory/search-service.d.ts +38 -0
- package/dist/memory/search-service.d.ts.map +1 -0
- package/dist/memory/search-service.js +463 -0
- package/dist/memory/search-service.js.map +1 -0
- package/dist/memory/tokenizer.d.ts +5 -0
- package/dist/memory/tokenizer.d.ts.map +1 -0
- package/dist/memory/tokenizer.js +42 -0
- package/dist/memory/tokenizer.js.map +1 -0
- package/dist/notifications/notification-service.d.ts +47 -0
- package/dist/notifications/notification-service.d.ts.map +1 -0
- package/dist/notifications/notification-service.js +163 -0
- package/dist/notifications/notification-service.js.map +1 -0
- package/dist/notifications/notification-tools.d.ts +4 -0
- package/dist/notifications/notification-tools.d.ts.map +1 -0
- package/dist/notifications/notification-tools.js +31 -0
- package/dist/notifications/notification-tools.js.map +1 -0
- package/dist/pi/pi-runtime-adapter.d.ts +38 -0
- package/dist/pi/pi-runtime-adapter.d.ts.map +1 -0
- package/dist/pi/pi-runtime-adapter.js +483 -0
- package/dist/pi/pi-runtime-adapter.js.map +1 -0
- package/dist/plugins/extension-manager.d.ts +32 -0
- package/dist/plugins/extension-manager.d.ts.map +1 -0
- package/dist/plugins/extension-manager.js +562 -0
- package/dist/plugins/extension-manager.js.map +1 -0
- package/dist/plugins/resource-inspector.d.ts +18 -0
- package/dist/plugins/resource-inspector.d.ts.map +1 -0
- package/dist/plugins/resource-inspector.js +386 -0
- package/dist/plugins/resource-inspector.js.map +1 -0
- package/dist/plugins/types.d.ts +96 -0
- package/dist/plugins/types.d.ts.map +1 -0
- package/dist/plugins/types.js +2 -0
- package/dist/plugins/types.js.map +1 -0
- package/dist/policy/approval-service.d.ts +68 -0
- package/dist/policy/approval-service.d.ts.map +1 -0
- package/dist/policy/approval-service.js +349 -0
- package/dist/policy/approval-service.js.map +1 -0
- package/dist/policy/policy.d.ts +26 -0
- package/dist/policy/policy.d.ts.map +1 -0
- package/dist/policy/policy.js +228 -0
- package/dist/policy/policy.js.map +1 -0
- package/dist/protocol/client.d.ts +25 -0
- package/dist/protocol/client.d.ts.map +1 -0
- package/dist/protocol/client.js +188 -0
- package/dist/protocol/client.js.map +1 -0
- package/dist/protocol/errors.d.ts +5 -0
- package/dist/protocol/errors.d.ts.map +1 -0
- package/dist/protocol/errors.js +9 -0
- package/dist/protocol/errors.js.map +1 -0
- package/dist/protocol/schema.d.ts +71 -0
- package/dist/protocol/schema.d.ts.map +1 -0
- package/dist/protocol/schema.js +155 -0
- package/dist/protocol/schema.js.map +1 -0
- package/dist/settings/llm-settings-service.d.ts +9 -0
- package/dist/settings/llm-settings-service.d.ts.map +1 -0
- package/dist/settings/llm-settings-service.js +199 -0
- package/dist/settings/llm-settings-service.js.map +1 -0
- package/dist/settings/llm-settings.d.ts +29 -0
- package/dist/settings/llm-settings.d.ts.map +1 -0
- package/dist/settings/llm-settings.js +113 -0
- package/dist/settings/llm-settings.js.map +1 -0
- package/dist/shell/shell-service.d.ts +88 -0
- package/dist/shell/shell-service.d.ts.map +1 -0
- package/dist/shell/shell-service.js +459 -0
- package/dist/shell/shell-service.js.map +1 -0
- package/dist/storage/atomic.d.ts +4 -0
- package/dist/storage/atomic.d.ts.map +1 -0
- package/dist/storage/atomic.js +49 -0
- package/dist/storage/atomic.js.map +1 -0
- package/dist/storage/daemon-lock.d.ts +17 -0
- package/dist/storage/daemon-lock.d.ts.map +1 -0
- package/dist/storage/daemon-lock.js +97 -0
- package/dist/storage/daemon-lock.js.map +1 -0
- package/dist/storage/home.d.ts +29 -0
- package/dist/storage/home.d.ts.map +1 -0
- package/dist/storage/home.js +108 -0
- package/dist/storage/home.js.map +1 -0
- package/dist/storage/jsonl.d.ts +2 -0
- package/dist/storage/jsonl.d.ts.map +1 -0
- package/dist/storage/jsonl.js +45 -0
- package/dist/storage/jsonl.js.map +1 -0
- package/dist/tools/core-tools.d.ts +16 -0
- package/dist/tools/core-tools.d.ts.map +1 -0
- package/dist/tools/core-tools.js +283 -0
- package/dist/tools/core-tools.js.map +1 -0
- package/dist/ui/shared/presentation.d.ts +46 -0
- package/dist/ui/shared/presentation.d.ts.map +1 -0
- package/dist/ui/shared/presentation.js +81 -0
- package/dist/ui/shared/presentation.js.map +1 -0
- package/dist/ui/shared/protocol-state.d.ts +30 -0
- package/dist/ui/shared/protocol-state.d.ts.map +1 -0
- package/dist/ui/shared/protocol-state.js +165 -0
- package/dist/ui/shared/protocol-state.js.map +1 -0
- package/dist/ui/tui/commands.d.ts +2 -0
- package/dist/ui/tui/commands.d.ts.map +1 -0
- package/dist/ui/tui/commands.js +33 -0
- package/dist/ui/tui/commands.js.map +1 -0
- package/dist/ui/tui/store.d.ts +32 -0
- package/dist/ui/tui/store.d.ts.map +1 -0
- package/dist/ui/tui/store.js +62 -0
- package/dist/ui/tui/store.js.map +1 -0
- package/dist/ui/tui/tui-app.d.ts +21 -0
- package/dist/ui/tui/tui-app.d.ts.map +1 -0
- package/dist/ui/tui/tui-app.js +643 -0
- package/dist/ui/tui/tui-app.js.map +1 -0
- package/dist/ui/tui/view.d.ts +29 -0
- package/dist/ui/tui/view.d.ts.map +1 -0
- package/dist/ui/tui/view.js +255 -0
- package/dist/ui/tui/view.js.map +1 -0
- package/dist/version.d.ts +5 -0
- package/dist/version.d.ts.map +1 -0
- package/dist/version.js +5 -0
- package/dist/version.js.map +1 -0
- package/package.json +62 -0
package/dist/cli/main.js
ADDED
|
@@ -0,0 +1,768 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { execFile, spawn } from "node:child_process";
|
|
3
|
+
import { realpathSync } from "node:fs";
|
|
4
|
+
import { access, readFile } from "node:fs/promises";
|
|
5
|
+
import { createRequire } from "node:module";
|
|
6
|
+
import { fileURLToPath } from "node:url";
|
|
7
|
+
import { promisify } from "node:util";
|
|
8
|
+
import { PsporeDaemon } from "../daemon/daemon.js";
|
|
9
|
+
import { DaemonSupervisor } from "../daemon/supervisor.js";
|
|
10
|
+
import { ProtocolClient } from "../protocol/client.js";
|
|
11
|
+
import { readResourceTrustSummary } from "../plugins/extension-manager.js";
|
|
12
|
+
import { resolvePsporeHome } from "../config/config.js";
|
|
13
|
+
import { ShellService } from "../shell/shell-service.js";
|
|
14
|
+
import { LlmSettingsService } from "../settings/llm-settings-service.js";
|
|
15
|
+
import { defaultModelFor, MODEL_APIS, PROVIDER_OPTIONS, THINKING_LEVELS, } from "../settings/llm-settings.js";
|
|
16
|
+
import { initializePsporeHome, loadConfig, psporePaths } from "../storage/home.js";
|
|
17
|
+
import { PsporeTuiApp } from "../ui/tui/tui-app.js";
|
|
18
|
+
import { PI_VERSION, PSPORE_VERSION } from "../version.js";
|
|
19
|
+
const execFileAsync = promisify(execFile);
|
|
20
|
+
export async function runCli(argv = process.argv.slice(2)) {
|
|
21
|
+
const parsed = parseGlobalOptions(argv);
|
|
22
|
+
const home = resolvePsporeHome(parsed.home);
|
|
23
|
+
const command = parsed.args[0] ?? "help";
|
|
24
|
+
const args = parsed.args.slice(1);
|
|
25
|
+
switch (command) {
|
|
26
|
+
case "help":
|
|
27
|
+
case "--help":
|
|
28
|
+
case "-h":
|
|
29
|
+
process.stdout.write(helpText());
|
|
30
|
+
return 0;
|
|
31
|
+
case "version":
|
|
32
|
+
case "--version":
|
|
33
|
+
case "-v":
|
|
34
|
+
process.stdout.write(`pSpore ${PSPORE_VERSION} (Pi ${PI_VERSION}, Node ${process.versions.node})\n`);
|
|
35
|
+
return 0;
|
|
36
|
+
case "init": {
|
|
37
|
+
requireNoArgs(args, "init");
|
|
38
|
+
const result = await initializePsporeHome(home);
|
|
39
|
+
process.stdout.write(result.created.length > 0
|
|
40
|
+
? `Initialized pSpore home at ${home}\nCreated ${result.created.length} files.\n`
|
|
41
|
+
: `pSpore home is already initialized at ${home}; no files overwritten.\n`);
|
|
42
|
+
return 0;
|
|
43
|
+
}
|
|
44
|
+
case "daemon":
|
|
45
|
+
requireNoArgs(args, "daemon");
|
|
46
|
+
return runDaemon(home);
|
|
47
|
+
case "status":
|
|
48
|
+
requireNoArgs(args, "status");
|
|
49
|
+
return printStatus(home);
|
|
50
|
+
case "doctor":
|
|
51
|
+
requireNoArgs(args, "doctor");
|
|
52
|
+
return doctor(home);
|
|
53
|
+
case "tui":
|
|
54
|
+
requireNoArgs(args, "tui");
|
|
55
|
+
return runTui(home);
|
|
56
|
+
case "desktop":
|
|
57
|
+
requireNoArgs(args, "desktop");
|
|
58
|
+
return runDesktop(home);
|
|
59
|
+
case "config":
|
|
60
|
+
return runConfigCommand(home, args);
|
|
61
|
+
case "jobs":
|
|
62
|
+
requireNoArgs(args, "jobs");
|
|
63
|
+
return printProtocolList(home, "jobs.list");
|
|
64
|
+
case "runs":
|
|
65
|
+
requireNoArgs(args, "runs");
|
|
66
|
+
return printProtocolList(home, "runs.list");
|
|
67
|
+
case "notifications":
|
|
68
|
+
requireNoArgs(args, "notifications");
|
|
69
|
+
return printProtocolList(home, "notifications.list");
|
|
70
|
+
case "agents":
|
|
71
|
+
requireNoArgs(args, "agents");
|
|
72
|
+
return printProtocolList(home, "agents.list");
|
|
73
|
+
case "agent":
|
|
74
|
+
return runAgentCommand(home, args);
|
|
75
|
+
case "mcp":
|
|
76
|
+
return runMcpCommand(home, args);
|
|
77
|
+
case "resources":
|
|
78
|
+
requireNoArgs(args, "resources");
|
|
79
|
+
return printResourceList(home, "resources.list");
|
|
80
|
+
case "skills":
|
|
81
|
+
requireNoArgs(args, "skills");
|
|
82
|
+
return printResourceList(home, "skills.list");
|
|
83
|
+
case "plugins":
|
|
84
|
+
requireNoArgs(args, "plugins");
|
|
85
|
+
return printResourceList(home, "plugins.list");
|
|
86
|
+
case "resource":
|
|
87
|
+
return runResourceCommand(home, args);
|
|
88
|
+
case "schedule":
|
|
89
|
+
if (args.length !== 2 || args[0] !== "put")
|
|
90
|
+
throw new CliError("schedule usage: schedule put JOB.json");
|
|
91
|
+
return putJobFile(home, args[1]);
|
|
92
|
+
case "run":
|
|
93
|
+
if (args.length !== 1)
|
|
94
|
+
throw new CliError("run requires exactly one job ID");
|
|
95
|
+
return runJob(home, args[0]);
|
|
96
|
+
case "cancel":
|
|
97
|
+
if (args.length !== 1)
|
|
98
|
+
throw new CliError("cancel requires exactly one run ID");
|
|
99
|
+
return cancelRun(home, args[0]);
|
|
100
|
+
case "prompt":
|
|
101
|
+
if (args.length === 0 || !args.join(" ").trim())
|
|
102
|
+
throw new CliError("prompt requires text");
|
|
103
|
+
return runPrompt(home, args.join(" "));
|
|
104
|
+
default:
|
|
105
|
+
throw new CliError(`Unknown command: ${command}\n\n${helpText()}`);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
async function runDaemon(home) {
|
|
109
|
+
const daemon = await PsporeDaemon.start({ home });
|
|
110
|
+
process.stdout.write(`pSpore daemon ${daemon.kernel.snapshot().daemon.phase} (PID ${process.pid})\nSocket: ${daemon.paths.socket}\n`);
|
|
111
|
+
let stopping = false;
|
|
112
|
+
const stop = () => {
|
|
113
|
+
if (stopping)
|
|
114
|
+
return;
|
|
115
|
+
stopping = true;
|
|
116
|
+
void daemon.stop().catch(() => undefined);
|
|
117
|
+
};
|
|
118
|
+
process.once("SIGINT", stop);
|
|
119
|
+
process.once("SIGTERM", stop);
|
|
120
|
+
try {
|
|
121
|
+
await daemon.whenStopped();
|
|
122
|
+
return 0;
|
|
123
|
+
}
|
|
124
|
+
finally {
|
|
125
|
+
process.off("SIGINT", stop);
|
|
126
|
+
process.off("SIGTERM", stop);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
async function printStatus(home) {
|
|
130
|
+
const client = await connectFromEndpoint(home, "status");
|
|
131
|
+
try {
|
|
132
|
+
const status = await client.command("status.get", {});
|
|
133
|
+
const sessionCount = Object.keys(status.sessions).length;
|
|
134
|
+
const active = Object.values(status.sessions).filter((session) => session.status === "running").length;
|
|
135
|
+
process.stdout.write([
|
|
136
|
+
`Daemon: ${status.daemon.phase}`,
|
|
137
|
+
`Instance: ${status.daemon.instanceId ?? "unknown"}`,
|
|
138
|
+
`Revision: ${status.revision}`,
|
|
139
|
+
`Sessions: ${sessionCount} (${active} running)`,
|
|
140
|
+
`Pending approvals: ${Object.keys(status.approvals).length}`,
|
|
141
|
+
`Runs: ${Object.keys(status.runs).length} (${Object.values(status.runs).filter((run) => run.status === "running").length} running)`,
|
|
142
|
+
`MCP servers: ${Object.keys(status.mcpServers).length} (${Object.values(status.mcpServers).filter((server) => server.status === "ready").length} ready)`,
|
|
143
|
+
`Resources: ${Object.keys(status.resources).length} (${Object.values(status.resources).filter((resource) => resource.status === "active" || resource.status === "ready").length} active)`,
|
|
144
|
+
].join("\n") + "\n");
|
|
145
|
+
return 0;
|
|
146
|
+
}
|
|
147
|
+
finally {
|
|
148
|
+
await client.close();
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
async function doctor(home) {
|
|
152
|
+
const checks = [];
|
|
153
|
+
const nodeMajor = Number(process.versions.node.split(".")[0]);
|
|
154
|
+
checks.push({ name: "Node", ok: nodeMajor === 24, detail: process.versions.node });
|
|
155
|
+
try {
|
|
156
|
+
const config = await loadConfig(home);
|
|
157
|
+
checks.push({ name: "Config", ok: true, detail: `${home}/config.json` });
|
|
158
|
+
checks.push({
|
|
159
|
+
name: "Model",
|
|
160
|
+
ok: config.model !== null,
|
|
161
|
+
detail: config.model ? `${config.model.provider}/${config.model.model}` : "not configured",
|
|
162
|
+
});
|
|
163
|
+
checks.push({
|
|
164
|
+
name: "MCP config",
|
|
165
|
+
ok: true,
|
|
166
|
+
detail: `${config.mcpServers.length} configured (${config.mcpServers.filter((server) => server.enabled).length} enabled)`,
|
|
167
|
+
});
|
|
168
|
+
const resourceTrust = await readResourceTrustSummary(psporePaths(home, config.runtime.workspace).resourceTrust);
|
|
169
|
+
checks.push({
|
|
170
|
+
name: "Resource trust",
|
|
171
|
+
ok: true,
|
|
172
|
+
detail: `${resourceTrust.trusted} trusted, ${resourceTrust.disabled} disabled (${resourceTrust.skills} skills, ${resourceTrust.hostPlugins} host plugins)`,
|
|
173
|
+
});
|
|
174
|
+
const shell = new ShellService({
|
|
175
|
+
artifactRoot: psporePaths(home, config.runtime.workspace).artifacts,
|
|
176
|
+
...(config.shell.executable === undefined ? {} : { configuredShell: config.shell.executable }),
|
|
177
|
+
});
|
|
178
|
+
const resolved = await shell.initialize();
|
|
179
|
+
checks.push({ name: "Shell", ok: true, detail: `${resolved.path} (${resolved.source})` });
|
|
180
|
+
}
|
|
181
|
+
catch (error) {
|
|
182
|
+
checks.push({ name: "Config", ok: false, detail: toError(error).message });
|
|
183
|
+
}
|
|
184
|
+
try {
|
|
185
|
+
const result = await execFileAsync("rg", ["--version"], { encoding: "utf8" });
|
|
186
|
+
checks.push({ name: "ripgrep", ok: true, detail: result.stdout.split("\n")[0] ?? "available" });
|
|
187
|
+
}
|
|
188
|
+
catch (error) {
|
|
189
|
+
checks.push({ name: "ripgrep", ok: false, detail: toError(error).message });
|
|
190
|
+
}
|
|
191
|
+
try {
|
|
192
|
+
const endpoint = await endpointFor(home);
|
|
193
|
+
const client = new ProtocolClient({ socketPath: endpoint.socket, clientId: "doctor" });
|
|
194
|
+
await client.connect();
|
|
195
|
+
await client.close();
|
|
196
|
+
checks.push({ name: "Daemon", ok: true, detail: `${endpoint.state} at ${endpoint.socket}` });
|
|
197
|
+
}
|
|
198
|
+
catch (error) {
|
|
199
|
+
checks.push({ name: "Daemon", ok: false, detail: toError(error).message });
|
|
200
|
+
}
|
|
201
|
+
for (const check of checks)
|
|
202
|
+
process.stdout.write(`${check.ok ? "OK" : "FAIL"} ${check.name}: ${check.detail}\n`);
|
|
203
|
+
return checks.every((check) => check.ok || check.name === "Model" || check.name === "Daemon") ? 0 : 1;
|
|
204
|
+
}
|
|
205
|
+
async function runTui(home) {
|
|
206
|
+
if (!process.stdin.isTTY || !process.stdout.isTTY) {
|
|
207
|
+
throw new CliError("tui requires an interactive TTY; use `pspore status` for a non-interactive check");
|
|
208
|
+
}
|
|
209
|
+
const { paths } = await initializePsporeHome(home);
|
|
210
|
+
const supervisor = new DaemonSupervisor({ home });
|
|
211
|
+
await supervisor.start();
|
|
212
|
+
const settings = new LlmSettingsService(home, paths);
|
|
213
|
+
try {
|
|
214
|
+
await new PsporeTuiApp({
|
|
215
|
+
socketPath: paths.socket,
|
|
216
|
+
settings,
|
|
217
|
+
onSettingsSaved: async () => {
|
|
218
|
+
if (supervisor.ownership !== "owned") {
|
|
219
|
+
return {
|
|
220
|
+
applied: false,
|
|
221
|
+
restartRequired: true,
|
|
222
|
+
message: "Settings saved; restart the external daemon to apply them",
|
|
223
|
+
};
|
|
224
|
+
}
|
|
225
|
+
await supervisor.stop();
|
|
226
|
+
await supervisor.start();
|
|
227
|
+
return { applied: true, restartRequired: false, message: "Settings saved; the TUI daemon is restarting" };
|
|
228
|
+
},
|
|
229
|
+
}).run();
|
|
230
|
+
return 0;
|
|
231
|
+
}
|
|
232
|
+
finally {
|
|
233
|
+
await supervisor.stop();
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
async function runDesktop(home) {
|
|
237
|
+
const entry = fileURLToPath(new URL("../desktop/main.js", import.meta.url));
|
|
238
|
+
try {
|
|
239
|
+
await access(entry);
|
|
240
|
+
}
|
|
241
|
+
catch {
|
|
242
|
+
throw new CliError(`Electron entry is not built at ${entry}; run \`npm run build\` first`);
|
|
243
|
+
}
|
|
244
|
+
const require = createRequire(import.meta.url);
|
|
245
|
+
let electron;
|
|
246
|
+
try {
|
|
247
|
+
electron = require("electron");
|
|
248
|
+
}
|
|
249
|
+
catch {
|
|
250
|
+
throw new CliError("Electron is not included in the npm CLI package; use the pSpore AppImage for the desktop client");
|
|
251
|
+
}
|
|
252
|
+
if (typeof electron !== "string")
|
|
253
|
+
throw new CliError("The local Electron executable could not be resolved");
|
|
254
|
+
return new Promise((resolve, reject) => {
|
|
255
|
+
const child = spawn(electron, [entry, "--home", home], { stdio: "inherit", env: process.env });
|
|
256
|
+
child.once("error", reject);
|
|
257
|
+
child.once("exit", (code, signal) => {
|
|
258
|
+
if (signal)
|
|
259
|
+
reject(new CliError(`Electron exited after signal ${signal}`));
|
|
260
|
+
else
|
|
261
|
+
resolve(code ?? 1);
|
|
262
|
+
});
|
|
263
|
+
});
|
|
264
|
+
}
|
|
265
|
+
async function runConfigCommand(home, args) {
|
|
266
|
+
const { paths } = await initializePsporeHome(home);
|
|
267
|
+
const service = new LlmSettingsService(home, paths);
|
|
268
|
+
const action = args[0] ?? "show";
|
|
269
|
+
if (action === "show") {
|
|
270
|
+
if (args.length > 1)
|
|
271
|
+
throw new CliError("config show does not accept arguments");
|
|
272
|
+
printModelSettings(await service.get());
|
|
273
|
+
return 0;
|
|
274
|
+
}
|
|
275
|
+
const current = await service.get();
|
|
276
|
+
let update;
|
|
277
|
+
if (action === "set") {
|
|
278
|
+
const provider = parseConfigProvider(args[1]);
|
|
279
|
+
if (provider === "custom-openai") {
|
|
280
|
+
const model = args[2];
|
|
281
|
+
const baseUrl = args[3];
|
|
282
|
+
if (!model || !baseUrl || args.length > 6) {
|
|
283
|
+
throw new CliError("config set usage: config set custom-openai MODEL BASE_URL [chat|responses] [THINKING]");
|
|
284
|
+
}
|
|
285
|
+
update = settingsUpdate(current, {
|
|
286
|
+
provider,
|
|
287
|
+
model,
|
|
288
|
+
baseUrl,
|
|
289
|
+
api: parseConfigApi(args[4] ?? "chat"),
|
|
290
|
+
thinking: parseConfigThinking(args[5] ?? current.thinking),
|
|
291
|
+
customModel: true,
|
|
292
|
+
});
|
|
293
|
+
}
|
|
294
|
+
else {
|
|
295
|
+
if (args.length > 4)
|
|
296
|
+
throw new CliError("config set usage: config set PROVIDER [MODEL] [THINKING]");
|
|
297
|
+
update = settingsUpdate(current, {
|
|
298
|
+
provider,
|
|
299
|
+
model: args[2] ?? defaultModelFor(provider),
|
|
300
|
+
thinking: parseConfigThinking(args[3] ?? current.thinking),
|
|
301
|
+
baseUrl: "",
|
|
302
|
+
api: "provider-default",
|
|
303
|
+
customModel: false,
|
|
304
|
+
});
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
else if (action === "provider") {
|
|
308
|
+
const provider = parseConfigProvider(args[1]);
|
|
309
|
+
if (provider === "custom-openai")
|
|
310
|
+
throw new CliError("Use `config endpoint BASE_URL` to select custom-openai");
|
|
311
|
+
if (args.length !== 2)
|
|
312
|
+
throw new CliError("config provider usage: config provider deepseek|openai");
|
|
313
|
+
update = settingsUpdate(current, {
|
|
314
|
+
provider,
|
|
315
|
+
model: defaultModelFor(provider),
|
|
316
|
+
baseUrl: "",
|
|
317
|
+
api: "provider-default",
|
|
318
|
+
customModel: false,
|
|
319
|
+
});
|
|
320
|
+
}
|
|
321
|
+
else if (action === "model") {
|
|
322
|
+
const model = args[1];
|
|
323
|
+
if (!model || args.length !== 2)
|
|
324
|
+
throw new CliError("config model requires exactly one MODEL");
|
|
325
|
+
update = settingsUpdate(current, { model });
|
|
326
|
+
}
|
|
327
|
+
else if (action === "endpoint") {
|
|
328
|
+
const baseUrl = args[1];
|
|
329
|
+
if (!baseUrl || args.length > 3)
|
|
330
|
+
throw new CliError("config endpoint usage: config endpoint BASE_URL [chat|responses]");
|
|
331
|
+
update = settingsUpdate(current, {
|
|
332
|
+
provider: "custom-openai",
|
|
333
|
+
model: current.provider === "custom-openai" ? current.model : defaultModelFor("custom-openai"),
|
|
334
|
+
baseUrl,
|
|
335
|
+
api: parseConfigApi(args[2] ?? "chat"),
|
|
336
|
+
customModel: true,
|
|
337
|
+
});
|
|
338
|
+
}
|
|
339
|
+
else if (action === "thinking") {
|
|
340
|
+
if (args.length !== 2)
|
|
341
|
+
throw new CliError(`config thinking usage: config thinking ${THINKING_LEVELS.join("|")}`);
|
|
342
|
+
update = settingsUpdate(current, { thinking: parseConfigThinking(args[1]) });
|
|
343
|
+
}
|
|
344
|
+
else if (action === "key") {
|
|
345
|
+
if (args.length !== 1)
|
|
346
|
+
throw new CliError("API keys are never accepted as command arguments; use `config key` and enter it securely");
|
|
347
|
+
const apiKey = await readSecretInput();
|
|
348
|
+
if (!apiKey)
|
|
349
|
+
throw new CliError("API key cannot be empty");
|
|
350
|
+
update = settingsUpdate(current, { apiKey });
|
|
351
|
+
}
|
|
352
|
+
else if (action === "clear-key") {
|
|
353
|
+
if (args.length !== 1)
|
|
354
|
+
throw new CliError("config clear-key does not accept arguments");
|
|
355
|
+
update = settingsUpdate(current, { clearApiKey: true });
|
|
356
|
+
}
|
|
357
|
+
else {
|
|
358
|
+
throw new CliError("config usage: show | set | provider | model | endpoint | thinking | key | clear-key");
|
|
359
|
+
}
|
|
360
|
+
const saved = await service.save(update);
|
|
361
|
+
printModelSettings(saved);
|
|
362
|
+
process.stdout.write("Saved. Restart an existing daemon to apply the new model settings.\n");
|
|
363
|
+
return 0;
|
|
364
|
+
}
|
|
365
|
+
function printModelSettings(settings) {
|
|
366
|
+
process.stdout.write([
|
|
367
|
+
`Provider: ${settings.provider}`,
|
|
368
|
+
`Model: ${settings.model}`,
|
|
369
|
+
`Base URL: ${settings.baseUrl || "provider default"}`,
|
|
370
|
+
`API: ${settings.api}`,
|
|
371
|
+
`Thinking: ${settings.thinking}`,
|
|
372
|
+
`API key: ${settings.hasApiKey ? "configured (hidden)" : "not configured"}`,
|
|
373
|
+
].join("\n") + "\n");
|
|
374
|
+
}
|
|
375
|
+
async function readSecretInput() {
|
|
376
|
+
if (!process.stdin.isTTY) {
|
|
377
|
+
let value = "";
|
|
378
|
+
for await (const chunk of process.stdin) {
|
|
379
|
+
value += String(chunk);
|
|
380
|
+
if (value.length > 16_386)
|
|
381
|
+
throw new CliError("API key input exceeds 16384 characters");
|
|
382
|
+
}
|
|
383
|
+
return value.replace(/\r?\n$/, "");
|
|
384
|
+
}
|
|
385
|
+
process.stdout.write("API key (hidden): ");
|
|
386
|
+
process.stdin.setRawMode(true);
|
|
387
|
+
process.stdin.resume();
|
|
388
|
+
return new Promise((resolve, reject) => {
|
|
389
|
+
let value = "";
|
|
390
|
+
const finish = (error) => {
|
|
391
|
+
process.stdin.off("data", onData);
|
|
392
|
+
process.stdin.setRawMode(false);
|
|
393
|
+
process.stdin.pause();
|
|
394
|
+
process.stdout.write("\n");
|
|
395
|
+
if (error)
|
|
396
|
+
reject(error);
|
|
397
|
+
else
|
|
398
|
+
resolve(value);
|
|
399
|
+
};
|
|
400
|
+
const onData = (chunk) => {
|
|
401
|
+
const data = String(chunk);
|
|
402
|
+
if (data.includes("\u0003"))
|
|
403
|
+
return finish(new CliError("API key input cancelled"));
|
|
404
|
+
if (data.includes("\r") || data.includes("\n"))
|
|
405
|
+
return finish();
|
|
406
|
+
if (data === "\u007f" || data === "\b") {
|
|
407
|
+
value = [...value].slice(0, -1).join("");
|
|
408
|
+
return;
|
|
409
|
+
}
|
|
410
|
+
const pasted = data.startsWith("\u001b[200~") && data.endsWith("\u001b[201~")
|
|
411
|
+
? data.slice(6, -6)
|
|
412
|
+
: data;
|
|
413
|
+
const printable = [...pasted].filter((character) => character >= " " && character <= "~").join("");
|
|
414
|
+
if (value.length + printable.length > 16_384)
|
|
415
|
+
return finish(new CliError("API key input exceeds 16384 characters"));
|
|
416
|
+
value += printable;
|
|
417
|
+
};
|
|
418
|
+
process.stdin.on("data", onData);
|
|
419
|
+
});
|
|
420
|
+
}
|
|
421
|
+
function parseConfigProvider(value) {
|
|
422
|
+
const normalized = value?.trim().toLowerCase();
|
|
423
|
+
if (!normalized || !PROVIDER_OPTIONS.includes(normalized)) {
|
|
424
|
+
throw new CliError("Provider must be deepseek, openai, or custom-openai");
|
|
425
|
+
}
|
|
426
|
+
return normalized;
|
|
427
|
+
}
|
|
428
|
+
function parseConfigThinking(value) {
|
|
429
|
+
const normalized = value?.trim().toLowerCase();
|
|
430
|
+
if (!normalized || !THINKING_LEVELS.includes(normalized)) {
|
|
431
|
+
throw new CliError(`Thinking must be one of: ${THINKING_LEVELS.join(", ")}`);
|
|
432
|
+
}
|
|
433
|
+
return normalized;
|
|
434
|
+
}
|
|
435
|
+
function parseConfigApi(value) {
|
|
436
|
+
const normalized = value.trim().toLowerCase();
|
|
437
|
+
const api = normalized === "chat" ? "openai-completions" : normalized === "responses" ? "openai-responses" : normalized;
|
|
438
|
+
if (!MODEL_APIS.includes(api) || (api !== "openai-completions" && api !== "openai-responses")) {
|
|
439
|
+
throw new CliError("Custom OpenAI API must be chat/openai-completions or responses/openai-responses");
|
|
440
|
+
}
|
|
441
|
+
return api;
|
|
442
|
+
}
|
|
443
|
+
function settingsUpdate(settings, changes) {
|
|
444
|
+
return {
|
|
445
|
+
provider: changes.provider ?? settings.provider,
|
|
446
|
+
model: changes.model ?? settings.model,
|
|
447
|
+
baseUrl: changes.baseUrl ?? settings.baseUrl,
|
|
448
|
+
api: changes.api ?? settings.api,
|
|
449
|
+
thinking: changes.thinking ?? settings.thinking,
|
|
450
|
+
customModel: changes.customModel ?? settings.customModel,
|
|
451
|
+
...(changes.apiKey ? { apiKey: changes.apiKey } : {}),
|
|
452
|
+
clearApiKey: changes.clearApiKey ?? false,
|
|
453
|
+
};
|
|
454
|
+
}
|
|
455
|
+
async function runPrompt(home, text) {
|
|
456
|
+
const client = await connectFromEndpoint(home, "prompt");
|
|
457
|
+
let resolveSettled;
|
|
458
|
+
let printedDelta = false;
|
|
459
|
+
const settled = new Promise((resolve) => {
|
|
460
|
+
resolveSettled = resolve;
|
|
461
|
+
});
|
|
462
|
+
const unsubscribe = client.subscribe((event) => {
|
|
463
|
+
if (event.type === "message.delta" && typeof event.payload["delta"] === "string") {
|
|
464
|
+
printedDelta = true;
|
|
465
|
+
process.stdout.write(event.payload["delta"]);
|
|
466
|
+
}
|
|
467
|
+
if (event.type === "run.settled")
|
|
468
|
+
resolveSettled();
|
|
469
|
+
});
|
|
470
|
+
const abort = () => void client.command("session.abort", { reason: "CLI interrupted" }, { sessionId: "main" }).catch(() => undefined);
|
|
471
|
+
process.once("SIGINT", abort);
|
|
472
|
+
try {
|
|
473
|
+
await client.command("session.prompt", { text, mode: "prompt" }, { sessionId: "main" });
|
|
474
|
+
await settled;
|
|
475
|
+
const snapshot = await client.command("session.snapshot", {}, { sessionId: "main" });
|
|
476
|
+
if (!snapshot)
|
|
477
|
+
throw new CliError("Session disappeared before completion");
|
|
478
|
+
const finalText = snapshot.messages.at(-1)?.text;
|
|
479
|
+
if (printedDelta)
|
|
480
|
+
process.stdout.write("\n");
|
|
481
|
+
else
|
|
482
|
+
process.stdout.write(`${finalText ?? ""}\n`);
|
|
483
|
+
return 0;
|
|
484
|
+
}
|
|
485
|
+
finally {
|
|
486
|
+
process.off("SIGINT", abort);
|
|
487
|
+
unsubscribe();
|
|
488
|
+
await client.close();
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
async function printProtocolList(home, command) {
|
|
492
|
+
const client = await connectFromEndpoint(home, command.split(".")[0] ?? "list");
|
|
493
|
+
try {
|
|
494
|
+
const values = await client.command(command, {});
|
|
495
|
+
process.stdout.write(`${JSON.stringify(values, null, 2)}\n`);
|
|
496
|
+
return 0;
|
|
497
|
+
}
|
|
498
|
+
finally {
|
|
499
|
+
await client.close();
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
async function runJob(home, jobId) {
|
|
503
|
+
const client = await connectFromEndpoint(home, "run");
|
|
504
|
+
try {
|
|
505
|
+
const run = await client.command("job.run", { jobId });
|
|
506
|
+
process.stdout.write(`${run.id} ${run.status}${run.reason ? `: ${run.reason}` : run.error ? `: ${run.error}` : ""}\n`);
|
|
507
|
+
return run.status === "succeeded" ? 0 : 1;
|
|
508
|
+
}
|
|
509
|
+
finally {
|
|
510
|
+
await client.close();
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
async function putJobFile(home, path) {
|
|
514
|
+
let value;
|
|
515
|
+
try {
|
|
516
|
+
value = JSON.parse(await readFile(path, "utf8"));
|
|
517
|
+
}
|
|
518
|
+
catch (error) {
|
|
519
|
+
throw new CliError(`Cannot read job JSON ${path}: ${toError(error).message}`);
|
|
520
|
+
}
|
|
521
|
+
if (!isRecord(value) || typeof value["id"] !== "string" || !value["id"].trim()) {
|
|
522
|
+
throw new CliError("Job JSON must contain a non-empty id");
|
|
523
|
+
}
|
|
524
|
+
const client = await connectFromEndpoint(home, "schedule");
|
|
525
|
+
try {
|
|
526
|
+
const jobs = await client.command("jobs.list", {});
|
|
527
|
+
const current = jobs.find((job) => job.id === value["id"]);
|
|
528
|
+
const updated = await client.command("job.put", {
|
|
529
|
+
job: value,
|
|
530
|
+
expectedSha256: current?.sourceSha256 ?? null,
|
|
531
|
+
});
|
|
532
|
+
process.stdout.write(`${updated.id} ${current ? "updated" : "created"} ${updated.sourceSha256 ?? ""}\n`);
|
|
533
|
+
return 0;
|
|
534
|
+
}
|
|
535
|
+
finally {
|
|
536
|
+
await client.close();
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
async function runAgentCommand(home, args) {
|
|
540
|
+
const action = args[0];
|
|
541
|
+
const client = await connectFromEndpoint(home, "agent");
|
|
542
|
+
try {
|
|
543
|
+
let result;
|
|
544
|
+
if (action === "spawn") {
|
|
545
|
+
const objective = args.slice(1).join(" ").trim();
|
|
546
|
+
if (!objective)
|
|
547
|
+
throw new CliError("agent spawn requires an objective");
|
|
548
|
+
const config = await loadConfig(home);
|
|
549
|
+
result = await client.command("agent.spawn", {
|
|
550
|
+
parentId: "main",
|
|
551
|
+
objective,
|
|
552
|
+
cwd: config.runtime.workspace,
|
|
553
|
+
workspaceMode: "shared-read",
|
|
554
|
+
budget: {
|
|
555
|
+
maxTurns: 8,
|
|
556
|
+
maxToolCalls: 24,
|
|
557
|
+
maxInputTokens: 50_000,
|
|
558
|
+
maxOutputTokens: 8_000,
|
|
559
|
+
maxChildren: 0,
|
|
560
|
+
},
|
|
561
|
+
});
|
|
562
|
+
}
|
|
563
|
+
else if (action === "send") {
|
|
564
|
+
const agentId = args[1];
|
|
565
|
+
const message = args.slice(2).join(" ").trim();
|
|
566
|
+
if (!agentId || !message)
|
|
567
|
+
throw new CliError("agent send requires AGENT_ID and MESSAGE");
|
|
568
|
+
result = await client.command("agent.send", { agentId, message, mode: "next" });
|
|
569
|
+
}
|
|
570
|
+
else if (action === "cancel") {
|
|
571
|
+
const agentId = args[1];
|
|
572
|
+
if (!agentId || args.length !== 2)
|
|
573
|
+
throw new CliError("agent cancel requires exactly one AGENT_ID");
|
|
574
|
+
result = await client.command("agent.cancel", { agentId, reason: "cancelled from CLI" });
|
|
575
|
+
}
|
|
576
|
+
else if (action === "join") {
|
|
577
|
+
const agentIds = args.slice(1);
|
|
578
|
+
if (agentIds.length === 0)
|
|
579
|
+
throw new CliError("agent join requires at least one AGENT_ID");
|
|
580
|
+
result = await client.command("agent.join", { agentIds, timeoutMs: 300_000 });
|
|
581
|
+
}
|
|
582
|
+
else {
|
|
583
|
+
throw new CliError("agent usage: agent spawn OBJECTIVE | send ID MESSAGE | join ID... | cancel ID");
|
|
584
|
+
}
|
|
585
|
+
process.stdout.write(`${JSON.stringify(result, null, 2)}\n`);
|
|
586
|
+
return 0;
|
|
587
|
+
}
|
|
588
|
+
finally {
|
|
589
|
+
await client.close();
|
|
590
|
+
}
|
|
591
|
+
}
|
|
592
|
+
async function runMcpCommand(home, args) {
|
|
593
|
+
const action = args[0] ?? "list";
|
|
594
|
+
const client = await connectFromEndpoint(home, "mcp");
|
|
595
|
+
try {
|
|
596
|
+
let result;
|
|
597
|
+
if (action === "list") {
|
|
598
|
+
if (args.length > 1)
|
|
599
|
+
throw new CliError("mcp list does not accept arguments");
|
|
600
|
+
result = await client.command("mcp.servers", {});
|
|
601
|
+
}
|
|
602
|
+
else if (action === "tools") {
|
|
603
|
+
if (args.length > 1)
|
|
604
|
+
throw new CliError("mcp tools does not accept arguments");
|
|
605
|
+
result = await client.command("mcp.tools", {});
|
|
606
|
+
}
|
|
607
|
+
else if (action === "discover" || action === "ping") {
|
|
608
|
+
const serverId = args[1];
|
|
609
|
+
if (!serverId || args.length !== 2)
|
|
610
|
+
throw new CliError(`mcp ${action} requires exactly one SERVER_ID`);
|
|
611
|
+
result = await client.command(action === "discover" ? "mcp.discover" : "mcp.ping", { serverId });
|
|
612
|
+
}
|
|
613
|
+
else {
|
|
614
|
+
throw new CliError("mcp usage: mcp list | tools | discover SERVER_ID | ping SERVER_ID");
|
|
615
|
+
}
|
|
616
|
+
process.stdout.write(`${JSON.stringify(result, null, 2)}\n`);
|
|
617
|
+
return 0;
|
|
618
|
+
}
|
|
619
|
+
finally {
|
|
620
|
+
await client.close();
|
|
621
|
+
}
|
|
622
|
+
}
|
|
623
|
+
async function printResourceList(home, command) {
|
|
624
|
+
const client = await connectFromEndpoint(home, command.split(".")[0] ?? "resources");
|
|
625
|
+
try {
|
|
626
|
+
const result = await client.command(command, {});
|
|
627
|
+
process.stdout.write(`${JSON.stringify(result, null, 2)}\n`);
|
|
628
|
+
return 0;
|
|
629
|
+
}
|
|
630
|
+
finally {
|
|
631
|
+
await client.close();
|
|
632
|
+
}
|
|
633
|
+
}
|
|
634
|
+
async function runResourceCommand(home, args) {
|
|
635
|
+
const action = args[0];
|
|
636
|
+
const client = await connectFromEndpoint(home, "resource");
|
|
637
|
+
try {
|
|
638
|
+
let result;
|
|
639
|
+
if (action === "inspect") {
|
|
640
|
+
const stagingPath = args[1];
|
|
641
|
+
if (!stagingPath || args.length !== 2)
|
|
642
|
+
throw new CliError("resource inspect requires exactly one STAGING_PATH");
|
|
643
|
+
result = await client.command("resources.inspect", { stagingPath });
|
|
644
|
+
}
|
|
645
|
+
else if (action === "install") {
|
|
646
|
+
const stagingPath = args[1];
|
|
647
|
+
const expectedDigest = args[2];
|
|
648
|
+
if (!stagingPath || !expectedDigest || args.length !== 3) {
|
|
649
|
+
throw new CliError("resource install requires STAGING_PATH and the exact inspected SHA256");
|
|
650
|
+
}
|
|
651
|
+
result = await client.command("resources.install", { stagingPath, expectedDigest });
|
|
652
|
+
}
|
|
653
|
+
else if (action === "disable") {
|
|
654
|
+
const kind = args[1];
|
|
655
|
+
const name = args[2];
|
|
656
|
+
const expectedDigest = args[3];
|
|
657
|
+
if ((kind !== "skill" && kind !== "host-plugin") || !name || !expectedDigest || args.length !== 4) {
|
|
658
|
+
throw new CliError("resource disable requires KIND(skill|host-plugin), NAME, and exact SHA256");
|
|
659
|
+
}
|
|
660
|
+
result = await client.command("resources.disable", { kind, name, expectedDigest });
|
|
661
|
+
}
|
|
662
|
+
else {
|
|
663
|
+
throw new CliError("resource usage: resource inspect PATH | install PATH SHA256 | disable KIND NAME SHA256");
|
|
664
|
+
}
|
|
665
|
+
process.stdout.write(`${JSON.stringify(result, null, 2)}\n`);
|
|
666
|
+
return 0;
|
|
667
|
+
}
|
|
668
|
+
finally {
|
|
669
|
+
await client.close();
|
|
670
|
+
}
|
|
671
|
+
}
|
|
672
|
+
async function cancelRun(home, runId) {
|
|
673
|
+
const client = await connectFromEndpoint(home, "cancel");
|
|
674
|
+
try {
|
|
675
|
+
const run = await client.command("run.cancel", { runId });
|
|
676
|
+
process.stdout.write(`${run.id} ${run.status}\n`);
|
|
677
|
+
return 0;
|
|
678
|
+
}
|
|
679
|
+
finally {
|
|
680
|
+
await client.close();
|
|
681
|
+
}
|
|
682
|
+
}
|
|
683
|
+
async function connectFromEndpoint(home, purpose) {
|
|
684
|
+
const endpoint = await endpointFor(home);
|
|
685
|
+
const client = new ProtocolClient({ socketPath: endpoint.socket, clientId: `${purpose}-${process.pid}` });
|
|
686
|
+
try {
|
|
687
|
+
await client.connect();
|
|
688
|
+
return client;
|
|
689
|
+
}
|
|
690
|
+
catch (error) {
|
|
691
|
+
await client.close().catch(() => undefined);
|
|
692
|
+
throw new CliError(`Cannot connect to pSpore daemon at ${endpoint.socket}: ${toError(error).message}`);
|
|
693
|
+
}
|
|
694
|
+
}
|
|
695
|
+
async function endpointFor(home) {
|
|
696
|
+
const path = psporePaths(home).endpoint;
|
|
697
|
+
let value;
|
|
698
|
+
try {
|
|
699
|
+
value = JSON.parse(await readFile(path, "utf8"));
|
|
700
|
+
}
|
|
701
|
+
catch (error) {
|
|
702
|
+
throw new CliError(`No daemon endpoint at ${path}; start it with \`pspore daemon\``);
|
|
703
|
+
}
|
|
704
|
+
if (!isRecord(value) || value["version"] !== 1 || typeof value["socket"] !== "string" || typeof value["state"] !== "string") {
|
|
705
|
+
throw new CliError(`Invalid daemon endpoint file: ${path}`);
|
|
706
|
+
}
|
|
707
|
+
return { socket: value["socket"], state: value["state"] };
|
|
708
|
+
}
|
|
709
|
+
function parseGlobalOptions(argv) {
|
|
710
|
+
const args = [];
|
|
711
|
+
let home;
|
|
712
|
+
for (let index = 0; index < argv.length; index++) {
|
|
713
|
+
const argument = argv[index];
|
|
714
|
+
if (argument === "--home") {
|
|
715
|
+
const value = argv[++index];
|
|
716
|
+
if (!value)
|
|
717
|
+
throw new CliError("--home requires a path");
|
|
718
|
+
home = value;
|
|
719
|
+
}
|
|
720
|
+
else if (argument?.startsWith("--home=")) {
|
|
721
|
+
home = argument.slice("--home=".length);
|
|
722
|
+
if (!home)
|
|
723
|
+
throw new CliError("--home requires a path");
|
|
724
|
+
}
|
|
725
|
+
else if (argument !== undefined) {
|
|
726
|
+
args.push(argument);
|
|
727
|
+
}
|
|
728
|
+
}
|
|
729
|
+
return { ...(home === undefined ? {} : { home }), args };
|
|
730
|
+
}
|
|
731
|
+
function requireNoArgs(args, command) {
|
|
732
|
+
if (args.length > 0)
|
|
733
|
+
throw new CliError(`${command} does not accept positional arguments`);
|
|
734
|
+
}
|
|
735
|
+
function helpText() {
|
|
736
|
+
return `pSpore ${PSPORE_VERSION} — Linux local agent core\n\nUsage: pspore [--home PATH] <command>\n\nCommands:\n init Initialize private config/workspace/state directories\n daemon Run the foreground daemon\n tui Open the reconnecting terminal client and start a local daemon when needed\n desktop Open the secure Electron 43 client\n config show Show model settings without exposing the API key\n config set ... Configure DeepSeek, OpenAI, or a custom OpenAI endpoint\n config key Read an API key from hidden TTY input or stdin\n config clear-key Remove the current provider's managed API key\n prompt TEXT Send one prompt and wait for completion\n jobs List loaded cron jobs\n runs List durable automation runs\n notifications List durable local notifications\n agents List child agents and their current status\n agent spawn TEXT Spawn a bounded shared-read child agent\n agent send ID TEXT\n Send a queued message to a child agent\n agent join ID... Wait for one or more child agents\n agent cancel ID Cancel a child agent and its descendants\n mcp list List configured MCP health without connecting\n mcp tools List schemas activated in this daemon process\n mcp discover ID Lazily connect and activate one trusted server\n mcp ping ID Check one trusted MCP server connection\n resources List trusted skills and host plugins\n skills List trusted skills and activation state\n plugins List trusted host plugins and activation state\n resource inspect PATH\n Inspect one directory under workspace/staging without executing it\n resource install PATH SHA256\n Trust and install only the exact inspected content digest\n resource disable KIND NAME SHA256\n Revoke trust without deleting installed content\n schedule put FILE\n Atomically create/update a cron job from JSON\n run JOB_ID Trigger one job now and wait for completion\n cancel RUN_ID Cancel a queued or active run\n status Show daemon and session status\n doctor Check Node, config, model, shell, ripgrep, resource trust, MCP, and daemon\n version Print pinned runtime versions\n help Show this help\n`;
|
|
737
|
+
}
|
|
738
|
+
class CliError extends Error {
|
|
739
|
+
constructor(message) {
|
|
740
|
+
super(message);
|
|
741
|
+
this.name = "CliError";
|
|
742
|
+
}
|
|
743
|
+
}
|
|
744
|
+
function isRecord(value) {
|
|
745
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
746
|
+
}
|
|
747
|
+
function toError(error) {
|
|
748
|
+
return error instanceof Error ? error : new Error(String(error));
|
|
749
|
+
}
|
|
750
|
+
if (isEntrypoint(process.argv[1], import.meta.url)) {
|
|
751
|
+
runCli().then((code) => {
|
|
752
|
+
process.exitCode = code;
|
|
753
|
+
}, (error) => {
|
|
754
|
+
process.stderr.write(`pSpore: ${toError(error).message}\n`);
|
|
755
|
+
process.exitCode = 1;
|
|
756
|
+
});
|
|
757
|
+
}
|
|
758
|
+
export function isEntrypoint(argvPath, moduleUrl) {
|
|
759
|
+
if (!argvPath)
|
|
760
|
+
return false;
|
|
761
|
+
try {
|
|
762
|
+
return realpathSync(argvPath) === fileURLToPath(moduleUrl);
|
|
763
|
+
}
|
|
764
|
+
catch {
|
|
765
|
+
return false;
|
|
766
|
+
}
|
|
767
|
+
}
|
|
768
|
+
//# sourceMappingURL=main.js.map
|