@prismer/runtime 1.9.0 → 1.9.6
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/CHANGELOG.md +178 -0
- package/README.md +363 -229
- package/dist/cli.cjs +10269 -0
- package/dist/cli.cjs.map +1 -0
- package/dist/cli.js +10257 -0
- package/dist/cli.js.map +1 -0
- package/dist/index.cjs +10603 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +1643 -0
- package/dist/index.d.ts +1496 -1916
- package/dist/index.js +9702 -9177
- package/dist/index.js.map +1 -1
- package/package.json +31 -33
- package/dist/adapter-registry-EMZFKFYK.mjs +0 -8
- package/dist/adapter-registry-EMZFKFYK.mjs.map +0 -1
- package/dist/artifacts-uploader-N2HNBSTW.mjs +0 -159
- package/dist/artifacts-uploader-N2HNBSTW.mjs.map +0 -1
- package/dist/auto-register-M4YSPJS6.mjs +0 -204
- package/dist/auto-register-M4YSPJS6.mjs.map +0 -1
- package/dist/bin/prismer.js +0 -14980
- package/dist/chunk-BJTO5JO5.mjs +0 -11
- package/dist/chunk-BJTO5JO5.mjs.map +0 -1
- package/dist/chunk-JIEDFDVI.mjs +0 -148
- package/dist/chunk-JIEDFDVI.mjs.map +0 -1
- package/dist/chunk-NDNX2G6O.mjs +0 -68
- package/dist/chunk-NDNX2G6O.mjs.map +0 -1
- package/dist/chunk-POWV475F.mjs +0 -45
- package/dist/chunk-POWV475F.mjs.map +0 -1
- package/dist/chunk-VTFKZAUY.mjs +0 -68
- package/dist/chunk-VTFKZAUY.mjs.map +0 -1
- package/dist/dispatch-mux-MW4HHICS.mjs +0 -8
- package/dist/dispatch-mux-MW4HHICS.mjs.map +0 -1
- package/dist/dispatch-rpc-J7H6KC2V.mjs +0 -78
- package/dist/dispatch-rpc-J7H6KC2V.mjs.map +0 -1
- package/dist/fs-rpc-OSHJZYK6.mjs +0 -58
- package/dist/fs-rpc-OSHJZYK6.mjs.map +0 -1
- package/dist/heartbeat-loop-H2LAI3V5.mjs +0 -95
- package/dist/heartbeat-loop-H2LAI3V5.mjs.map +0 -1
- package/dist/index.d.mts +0 -2063
- package/dist/index.mjs +0 -8807
- package/dist/index.mjs.map +0 -1
- package/dist/mode-b-DXJ7FJAL.mjs +0 -152
- package/dist/mode-b-DXJ7FJAL.mjs.map +0 -1
- package/dist/registry-ZYU2HDFL.mjs +0 -12
- package/dist/registry-ZYU2HDFL.mjs.map +0 -1
- /package/{dist → assets}/icon +0 -0
- /package/{dist → assets}/smallicon +0 -0
package/README.md
CHANGED
|
@@ -1,288 +1,422 @@
|
|
|
1
1
|
# @prismer/runtime
|
|
2
2
|
|
|
3
|
-
Prismer Cloud
|
|
3
|
+
> Prismer Cloud daemon runtime — TS-only adapter host for hosted IM agents.
|
|
4
|
+
> Same binary on macOS (LaunchAgent) and inside container pods (PID 1 of the daemon-first sandbox image).
|
|
4
5
|
|
|
5
|
-
|
|
6
|
+
The runtime opens **one WebSocket** to cloud, declares all hosted agents in a single `agent.host.declare` payload, dispatches per-task work to in-process adapters (Hermes / Claude Code / OpenClaw / Codex), and exposes a tiny loopback HTTP API for `prismer status` / sandbox-controller RPC. Cloud is the source of truth; `~/.prismer/local.db` is a read-only mirror.
|
|
7
|
+
|
|
8
|
+
- **Hard constraints:** TS-only — no Python / Go / Rust subprocesses, no PyPI packages, no `spawn('python', …)`. Hermes is a pure HTTP client of `hermes gateway`. Single npm package — adapters live in `src/adapters/<name>/`. Local SQLite (`better-sqlite3`) mirrors cloud for offline-first.
|
|
6
9
|
|
|
7
10
|
## Install
|
|
8
11
|
|
|
9
|
-
###
|
|
12
|
+
### macOS (LaunchAgent autostart)
|
|
10
13
|
|
|
11
|
-
```
|
|
12
|
-
curl -fsSL https://prismer.cloud/install.sh | sh
|
|
14
|
+
```sh
|
|
15
|
+
curl -fsSL https://prismer.cloud/install.sh | sh -s -- \
|
|
16
|
+
--component=daemon --adapters=claude-code,codex,hermes
|
|
13
17
|
```
|
|
14
18
|
|
|
15
|
-
|
|
19
|
+
What the daemon-mode installer does:
|
|
16
20
|
|
|
17
|
-
|
|
21
|
+
1. Detects OS / arch (Darwin or Linux x64/arm64; Windows requires WSL2).
|
|
22
|
+
2. Installs Node.js via `fnm` under `$HOME/.local/share/fnm` if absent (no Homebrew, no sudo).
|
|
23
|
+
3. Strict-pins `@prismer/runtime@1.9.3` (no caret) and installs globally via npm.
|
|
24
|
+
4. Scaffolds `~/.prismer/{adapters,workspace,logs,bin}`.
|
|
25
|
+
5. Writes `~/Library/LaunchAgents/com.prismer.daemon.plist` (label `com.prismer.daemon`, `KeepAlive=true`, `RunAtLoad=true`, `ProgramArguments` includes `--foreground`), then `bootout` + `bootstrap` to load it.
|
|
26
|
+
6. Auto-runs `prismer adapter install <name>` for each adapter passed via `--adapters`.
|
|
18
27
|
|
|
19
|
-
|
|
20
|
-
npm install -g @prismer/runtime @prismer/sdk
|
|
21
|
-
prismer setup
|
|
22
|
-
```
|
|
28
|
+
After install, run `prismer pair` (or `prismer setup sk-prismer-...`) to write `~/.prismer/config.toml`. The LaunchAgent's polling loop snaps to attention as soon as the file appears.
|
|
23
29
|
|
|
24
|
-
|
|
30
|
+
Installer flags: `-y/--yes`, `--no-setup`, `-v/--verbose`, `--local <dir>` (install from local tgz pre-publish), `--component=sdk|daemon` (or positional `daemon` shorthand), `--adapters=<csv>`, `--skip-launchagent` (CI), `--uninstall` (rm `~/.prismer`, `~/.local/share/fnm`, LaunchAgent).
|
|
25
31
|
|
|
26
|
-
|
|
32
|
+
### npm (manual)
|
|
27
33
|
|
|
28
|
-
```
|
|
29
|
-
|
|
30
|
-
prismer
|
|
31
|
-
|
|
32
|
-
# 2. Start the daemon (HTTP on 127.0.0.1:3210 + Cloud Relay WS)
|
|
34
|
+
```sh
|
|
35
|
+
npm install -g @prismer/runtime@1.9.3
|
|
36
|
+
prismer pair --device-name "$(hostname)"
|
|
33
37
|
prismer daemon start
|
|
34
|
-
|
|
35
|
-
# 3. Check health
|
|
36
|
-
prismer status
|
|
37
|
-
|
|
38
|
-
# 4. Wire an agent
|
|
39
|
-
prismer agent install claude-code --install-agent # also installs the upstream CLI if missing
|
|
40
|
-
prismer agent list
|
|
41
|
-
|
|
42
|
-
# 5. Pair a mobile device
|
|
43
|
-
prismer pair show
|
|
44
38
|
```
|
|
45
39
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
## CLI surface
|
|
40
|
+
Without LaunchAgent, you own daemon lifecycle (run under tmux / launchd / systemd / docker).
|
|
49
41
|
|
|
50
|
-
###
|
|
51
|
-
|
|
52
|
-
| Command | Purpose |
|
|
53
|
-
|---|---|
|
|
54
|
-
| `prismer setup [api-key]` | First-run setup: write API key, start daemon, scan for agents, print next steps. `--postinstall` for npm lifecycle hooks, `--no-daemon` to skip startup, `--skip-agent-scan` to skip detection. |
|
|
55
|
-
| `prismer status` | Daemon + agent + system health overview. `--json` for machine-readable. |
|
|
56
|
-
| `prismer daemon start` | Start detached background daemon on port 3210. `--foreground` for Docker/systemd. `--port <n>` to override. |
|
|
57
|
-
| `prismer daemon stop` | Graceful SIGTERM with 10s timeout, then SIGKILL. |
|
|
58
|
-
| `prismer daemon restart` | Stop then start. Preserves port. |
|
|
59
|
-
| `prismer daemon status` | PID file + port binding state. |
|
|
60
|
-
| `prismer daemon logs [--tail N] [--follow]` | Tail `~/.prismer/logs/daemon.log`. |
|
|
61
|
-
| `prismer daemon reprobe` | Force a transport reprobe pass without restarting. |
|
|
42
|
+
### Container (daemon-first sandbox image)
|
|
62
43
|
|
|
63
|
-
The
|
|
44
|
+
The runtime is PID 1 of every Prismer sandbox pod. `infra/sandbox-image/daemon-entrypoint.sh` writes a flat `~/.prismer/config.toml` from controller-injected env, then `exec prismer daemon start --port=7878 --foreground`. Required env:
|
|
64
45
|
|
|
65
|
-
|
|
46
|
+
- `PRISMER_API_KEY` — required.
|
|
47
|
+
- `PRISMER_DAEMON_ID` — required (else derived from hostname → `container:<podName>`).
|
|
48
|
+
- `PRISMER_BASE_URL` (or `CLOUD_API_BASE`, `PRISMER_CLOUD_API_BASE`) — cloud URL; falls back to `https://prismer.cloud`.
|
|
49
|
+
- `PRISMER_HOSTED_AGENT_FILE` / `PRISMER_HOSTED_AGENT_JSON` — static-binding payload (one-time agent install before first `host.declare`).
|
|
50
|
+
- `PRISMER_STATIC_BINDING_REQUIRED=true` — make missing static binding fatal.
|
|
51
|
+
- `PRISMER_USER_ID`, `PRISMER_TASK_ID`, `PRISMER_CONTAINER_ID` — audit-only.
|
|
66
52
|
|
|
67
|
-
|
|
68
|
-
|---|---|
|
|
69
|
-
| `prismer agent list` | All installed agents with tier + hook status. |
|
|
70
|
-
| `prismer agent install <name>` | Install an adapter pack. Two-tier fetch: signed release manifest (GitHub Release) → npm registry. Ed25519 signature verified before hook wiring unless `--skip-verify`. `--install-agent` also installs the upstream CLI (claude-code, codex, …) if missing. `--source cdn\|mirror\|npm`, `--force`, `--non-interactive`, `--accept-defaults`. |
|
|
71
|
-
| `prismer agent doctor <name>` | Diagnose binary path, hook config, daemon registration, PARA event emission for a given agent. |
|
|
72
|
-
| `prismer agent remove <name>` | Rollback hook config to the pre-install backup and remove the sandbox profile. Keychain entries are preserved. `--yes` to skip the confirmation prompt. |
|
|
73
|
-
| `prismer agent update <name>` | Shorthand for `install --force`. |
|
|
74
|
-
| `prismer agent publish <name>` | Publish the locally-installed agent so it appears from mobile. Writes to `~/.prismer/published-agents.toml` and calls cloud. |
|
|
75
|
-
| `prismer agent unpublish <name>` | Reverse of publish. Cloud deletes within 90s even if the DELETE call fails (background sweep). |
|
|
53
|
+
The container daemon binds `:7878` (controller proxy target) instead of the desktop default `:3210`, and starts `OutboxWatcher` to upload files dropped under `/workspace/_outbox/` as sandbox-output assets.
|
|
76
54
|
|
|
77
|
-
|
|
55
|
+
## CLI reference (16 verbs)
|
|
78
56
|
|
|
79
|
-
|
|
57
|
+
`VERSION = '1.9.3'`. Each verb is a `commander` subcommand under `src/cli/commands/`. Output is JSON (`--json`) or human-friendly ANSI by default.
|
|
80
58
|
|
|
81
|
-
|
|
82
|
-
prismer pack list
|
|
83
|
-
prismer pack search <query>
|
|
84
|
-
prismer pack verify <name> <signature>
|
|
85
|
-
```
|
|
86
|
-
|
|
87
|
-
Reads the signed pack index from the current GitHub Release — useful to inspect available adapters without installing.
|
|
88
|
-
|
|
89
|
-
### Device pairing
|
|
90
|
-
|
|
91
|
-
```
|
|
92
|
-
prismer pair show [--ttl 300] # generate QR code (5 min TTL, single-use)
|
|
93
|
-
prismer pair list # list paired devices
|
|
94
|
-
prismer pair revoke <deviceId> # remove from local registry
|
|
95
|
-
```
|
|
59
|
+
### Lifecycle
|
|
96
60
|
|
|
97
|
-
|
|
61
|
+
| Verb | Purpose |
|
|
62
|
+
| ---- | ------- |
|
|
63
|
+
| `banner [--compact]` | Print the Prismer Cloud banner. `--compact` collapses to one line. |
|
|
64
|
+
| `setup [api-key] [--token <jwt>] [--pair] [--cloud <url>] [--device-name <n>] [--force] [--start] [--no-start] [--as-user <email>] [--json]` | First-run onboarding. Three paths: direct API key, mint a daemon-scoped key from a user JWT (`POST /api/keys`, label `Daemon: <hostname> <YYYY-MM-DD>`), or delegate to QR/local-only pair. `--force` overwrites config and archives `local.db` → `local.db.<iso>.bak` if `api_key` / `cloud` / `daemon_id` changed. Auto-prints next-step tips (`daemon start`, `adapter list`, `agent register`, `profile create`, `task create`). |
|
|
65
|
+
| `pair [--cloud <url>] [--device-name <n>] [--force] [--as-user <email>]` | QR/JWT pair flow. `--as-user` requires `LOCAL_ONLY=1` (LAN dev bypass). |
|
|
66
|
+
| `daemon start [--port <n>] [--no-local-server] [--foreground] [--json]` | Daemonize itself by spawning `daemon run` and waiting up to 5s for the child to write a live PID. **`--foreground`** is required by the LaunchAgent plist (KeepAlive=true would respawn-loop on graceful exit). |
|
|
67
|
+
| `daemon run [--port <n>] [--no-local-server]` | Internal foreground worker. Polls every 5s for `~/.prismer/config.toml` to appear before booting `Runner` — so LaunchAgent KeepAlive can launch pre-pair without burning CPU. |
|
|
68
|
+
| `daemon stop [--timeout <ms>]` | SIGTERM the PID in `daemon.pid`; default timeout 5000ms. Clears stale PID files. |
|
|
69
|
+
| `daemon restart [--foreground]` | Chained `stop` then `start`. |
|
|
70
|
+
| `daemon status` | `{running, pid, paths.config}`; if alive, merges loopback `/healthz` (`pid`, `wsConnected`, `hostedAgents[]`). |
|
|
71
|
+
| `daemon logs --tail <n> [--follow]` | Read `~/.prismer/logs/daemon.log`; tail-N + `tail -f`-style follower. |
|
|
72
|
+
| `status [--json]` | Composite report: reads `config.toml`, hits `127.0.0.1:3210/healthz`, calls `GET /api/im/me`, counts `local.db` rows (`agents`, `agent_profiles WHERE deleted_at IS NULL`, `running_tasks`). Pretty banner by default. |
|
|
73
|
+
|
|
74
|
+
### Hosted agents
|
|
75
|
+
|
|
76
|
+
| Verb | Purpose |
|
|
77
|
+
| ---- | ------- |
|
|
78
|
+
| `adapter list` | Built-in adapters + per-adapter `health()` + recorded version from `[adapters.<name>]`. |
|
|
79
|
+
| `adapter install <name> [--package-version <v>] [--with-hooks] [--dry-run]` | Run the underlying package manager (`npm i -g`, `pipx install`), probe binary version, record `{package_manager, package_name, binary, version, installed_at}` into `config.toml`. `--with-hooks` writes a hook marker. |
|
|
80
|
+
| `adapter remove <name>` | Run uninstall (best-effort) and clear `[adapters.<name>]`. Alias: `uninstall`. |
|
|
81
|
+
| `adapter info <name>` | Print install spec + recorded version. |
|
|
82
|
+
| `adapter doctor <name>` | 5-check matrix: `downstream_binary`, `adapter_health`, `recorded_config`, `auth_env` (`ANTHROPIC_API_KEY`, `OPENAI_API_KEY`, `~/.hermes/.env`, `~/.openclaw/openclaw.json`), `hook_config`. Returns ordered fixes. |
|
|
83
|
+
| `adapter hooks <name> [--check] [--dry-run]` | Install / verify a `prismer-daemon-runtime` marker into the downstream tool's hook file (`~/.claude/hooks.json`, `~/.codex/hooks.json`, `~/.hermes/hooks.json`, `~/.openclaw/hooks/prismer.json`). JSON files are deep-merged; existing files are backed up to `<file>.bak.<iso>`. |
|
|
84
|
+
| `agent list [--local]` | Merge local mirror with cloud `GET /api/im/agents`. Emits `{localCount, cloudCount, agents[].{imUserId, name, adapter, local, cloud}}`. |
|
|
85
|
+
| `agent register --adapter <n> --display-name <n> [--username <n>] [--im-user-id <id>] [--capabilities <csv>] [--agent-type <t>] [--workspace-id <id>]` | `POST /api/im/register {type:'agent',username,displayName,agentType,capabilities,metadata.adapter}`. Auto-derives a slug if `--username` omitted. Mirrors into `local.db.agents`. |
|
|
86
|
+
| `agent rename <imUserId> <newName>` | `PATCH /api/im/agents/:id` + local update. |
|
|
87
|
+
| `agent remove <imUserId> [--cloud]` | Local-only by default; `--cloud` also `DELETE`s the cloud row. |
|
|
88
|
+
| `agent doctor [target]` | 6-check matrix: `binary`, `adapterHealth`, `localHosting` (DB row), `cloudVisibility`, `profileExistence` (local + cloud), `daemonLocalServerHealth` (`127.0.0.1:3210/healthz` + `/agents`). |
|
|
89
|
+
| `profile templates` | List built-in role templates (`product-manager`, `engineer`, `ceo`, `researcher`); each carries `applicableAdapters: ['hermes','openclaw','claude-code']` and a `configSchema` (`model`, `systemPrompt`, `allowedTools`, `maxTokens`). |
|
|
90
|
+
| `profile list --agent <imUserId>` | `GET /api/im/agent_profiles?agentId=…`. |
|
|
91
|
+
| `profile create --agent <id> --name <n> [--adapter <n>] [--config <jsonOr@file>] [--from-template <n>] [--workspace-id <id>]` | Template config merges with inline JSON. Default workspace via `/api/im/workspaces` if omitted. |
|
|
92
|
+
| `profile edit <profileId>` | Open current `config` in `$EDITOR` (default `vi`); PATCH on save. |
|
|
93
|
+
| `profile remove <profileId>` | Soft-delete (DELETE). |
|
|
94
|
+
|
|
95
|
+
### Tasks, memory, assets, events
|
|
96
|
+
|
|
97
|
+
| Verb | Purpose |
|
|
98
|
+
| ---- | ------- |
|
|
99
|
+
| `task create --agent <imUserId> --prompt <text> [--profile <id>] [--capability <n>] [--title <t>] [--timeout-ms <ms>] [--no-wait]` | `POST /api/im/tasks` then poll `GET /api/im/tasks/:id` until terminal. Tolerant of `{task:{...}}` and flat envelopes; reads `output` from `task.output` or `task.result.output`. |
|
|
100
|
+
| `task list [--limit <n>]` | Default limit 20. |
|
|
101
|
+
| `task get <taskId>` | Single task fetch. |
|
|
102
|
+
| `task cancel <taskId>` | `PATCH /api/im/tasks/:id {status:'cancelled'}`. |
|
|
103
|
+
| `memory stats` | Daemon Memory Gateway first (`/memory/stats`, `/api/memory/stats` on `PRISMER_DAEMON_URL` or `127.0.0.1:3210`); falls back to read-only `~/.prismer/local.db` snapshot of `cached_assets` + `workspace_files_mirror`. |
|
|
104
|
+
| `memory list [--limit <n>]` | Same fallback chain. |
|
|
105
|
+
| `memory search [query] [--query <text>] [--limit <n>]` | Local-cache substring filter when gateway unavailable. |
|
|
106
|
+
| `memory delete <id>` | Gateway-only. Exits 1 if unavailable. |
|
|
107
|
+
| `memory sync` | Gateway-only. Exits 1 if unavailable. |
|
|
108
|
+
| `asset list [--workspace-id <id>] [--task-id <id>]` | `GET /api/im/assets`. |
|
|
109
|
+
| `asset upload <file> --workspace-id <id> [--kind <k>] [--task-id <id>] [--agent-id <id>] [--container-id <id>] [--metadata <json>] [--mime <type>]` | Multipart `POST /api/im/assets`. |
|
|
110
|
+
| `asset download <assetId> --out <path>` | Fetches bytes from `/api/im/assets/:id` and writes to disk. |
|
|
111
|
+
| `asset get <assetId>` | `GET /api/im/assets/:id/detail`; pretty-prints kind/mime/size/storageUri/url/s3Url/expiresIn/photoRefs count. |
|
|
112
|
+
| `asset by-hash <sha256> [--workspace-id <id>]` | Content-hash dedupe lookup (`?wsId=…`). |
|
|
113
|
+
| `events [--limit <n>] [--agent-id <id>] [--session-id <id>] [--family <n>] [--type <n>]` | Read `~/.prismer/para/events.jsonl`. Default limit 50, hard max 10 000. Returns reverse-chronological array. Exits 1 with `events_unavailable` when the file does not exist. |
|
|
114
|
+
| `events:stats [--limit <n>] [--agent-id …]` | Aggregate `byFamily / byType / byAgentId / bySessionId` over the same filtered set. |
|
|
115
|
+
|
|
116
|
+
### Interaction
|
|
117
|
+
|
|
118
|
+
| Verb | Purpose |
|
|
119
|
+
| ---- | ------- |
|
|
120
|
+
| `chat me` | `GET /api/im/me`. |
|
|
121
|
+
| `chat direct <targetUserId> --message <text>` | `POST /api/im/direct/:id/messages {type:'text',content}`. |
|
|
122
|
+
| `chat messages <conversationId> [--limit <n>] [--before <id>]` | Pagination via `?limit=&before=`. |
|
|
123
|
+
| `chat group create --name <n> --members <csv>` | `POST /api/im/groups`. Members may be IM user IDs, usernames, or cloud user IDs. |
|
|
124
|
+
| `chat group send <groupId> --message <text>` | `POST /api/im/groups/:id/messages`. |
|
|
125
|
+
| `chat group messages <groupId> [--limit <n>] [--before <id>]` | History fetch. |
|
|
126
|
+
| `chat group remove-member <groupId> <memberId>` | `DELETE /api/im/groups/:id/members/:memberId`. |
|
|
127
|
+
|
|
128
|
+
All `chat` subcommands sanitize `sk-prismer-*` from error messages and tolerate both `{ok,data}` and `{success,data}` envelopes.
|
|
129
|
+
|
|
130
|
+
### Cookbook (54release MVP regression)
|
|
131
|
+
|
|
132
|
+
| Verb | Purpose |
|
|
133
|
+
| ---- | ------- |
|
|
134
|
+
| `cookbook run --suite <csv> [--workspace-id …] [--agent-id …] [--group-id …] [--sandbox-id …] [--prompt …] [--timeout-ms <ms>] [--strict] [--json]` | Run one or more smoke suites: `status` (`/api/health` + `/api/im/me`), `im` (`/workspaces`, `/me/agents`, `/groups`), `task` (list + optional create+poll if `--agent-id` and `--prompt` provided), `group` (list + optional history), `asset` (list), `sandbox` (list + status). `--strict` treats skips as failure. Exits non-zero on fail. |
|
|
135
|
+
|
|
136
|
+
### Sandbox
|
|
137
|
+
|
|
138
|
+
| Verb | Purpose |
|
|
139
|
+
| ---- | ------- |
|
|
140
|
+
| `sandbox list --workspace-id <id> [--status <s>] [--limit <n>]` | `GET /api/sandboxes?workspaceId=…`. |
|
|
141
|
+
| `sandbox create --workspace-id <id> [--agent-id …] [--task-id …] [--image …] [--cpu-request …] [--cpu-limit …] [--memory-request …] [--memory-limit …]` | `POST /api/sandboxes`. |
|
|
142
|
+
| `sandbox status <id>` | Detail + live status. |
|
|
143
|
+
| `sandbox start <id>` / `stop <id>` | Lifecycle endpoints. |
|
|
144
|
+
| `sandbox snapshot <id>` | `POST /api/sandboxes/:id/snapshot`. |
|
|
145
|
+
| `sandbox runCmd <id> -- <command...> [--timeout-ms <ms>]` | `POST /api/sandboxes/:id/runCmd {command, timeoutMs}`. |
|
|
146
|
+
| `sandbox logs <id>` | Streams `/logs` to stdout (`ReadableStream` decode loop). |
|
|
147
|
+
|
|
148
|
+
### Workspace
|
|
149
|
+
|
|
150
|
+
| Verb | Purpose |
|
|
151
|
+
| ---- | ------- |
|
|
152
|
+
| `workspace list` / `get <id>` | `GET /api/im/workspaces[/:id]`. |
|
|
153
|
+
| `workspace create --name <n> [--description <text>]` | Slug derived from name; `POST /api/im/workspaces {name, slug, metadata.description}`. |
|
|
154
|
+
| `workspace runtime <workspaceId> [--events] [--json]` | `GET /api/im/workspaces/:id/runtime` (devices + agents + heartbeats). `--events` opens an SSE stream at `…/runtime/events`, decoding `snapshot` / `agent.heartbeat` events into pretty heartbeats or `--json` lines. |
|
|
155
|
+
| `workspace files <workspaceId>` | `GET /api/im/workspaces/:id/files`. |
|
|
156
|
+
|
|
157
|
+
## Adapter taxonomy
|
|
158
|
+
|
|
159
|
+
The contract lives in `src/adapters/contract.ts`. Two `AdapterKind` values:
|
|
160
|
+
|
|
161
|
+
- **`'long-running'`** — adapter exposes `ensureService(profile)` returning an `AdapterService` (HTTP client) that the daemon's `ServicePool` reuses across tasks per `AgentProfile.id`. Re-creates on `healthy() === false` or `'crash'` event.
|
|
162
|
+
- **`'interactive'`** — adapter exposes `dispatch(profile, task)` and spawns one subprocess per task.
|
|
163
|
+
|
|
164
|
+
All four official adapters ship in-process via `src/adapters/registry.ts` (`Map<name, AdapterDef>` with `findByCapability(capability)` supporting wildcard tags like `code.*`).
|
|
165
|
+
|
|
166
|
+
| Adapter | Kind | Capabilities | Wraps |
|
|
167
|
+
| ------- | ---- | ------------ | ----- |
|
|
168
|
+
| `hermes` | long-running | `shell, code, mcp, long-context` | `hermes gateway` HTTP API on `127.0.0.1:<port>` |
|
|
169
|
+
| `claude-code` | interactive | `shell, code, mcp, edit` | `claude --print --model … <prompt>` (Claude CLI 2.x) |
|
|
170
|
+
| `codex` | interactive | `code, shell, openai` | `codex exec --model … --cd … --sandbox … --ephemeral --json <prompt>` |
|
|
171
|
+
| `openclaw` | long-running | `chat, code, multi-channel` | `openclaw gateway` (`POST /v1/chat/completions`, OpenAI-compatible) |
|
|
172
|
+
|
|
173
|
+
### `hermes`
|
|
174
|
+
|
|
175
|
+
Pure HTTP client of `hermes gateway` (Hermes 0.10+). All calls are HTTP — no `spawn('python', …)`, no PyPI dependency. `HermesProfileConfigSchema` (zod):
|
|
176
|
+
|
|
177
|
+
- `port` (1..65535, default `8642`), `apiKey` (required, from `~/.hermes/.env API_SERVER_KEY`).
|
|
178
|
+
- `hermesProfileName` (default = first 8 chars of `profile.id`), `autoStart` (default `false`), `startupTimeoutMs` (default 30 000).
|
|
179
|
+
- `configurePrismerProvider` (default `true`) — writes `custom_providers` + `model.provider='custom:prismer'` + `mcp_servers['prismer-tasks']` block into `~/.hermes/profiles/<name>/config.yaml`. The MCP server path is auto-resolved via `createRequire('@prismer/mcp-server')` first, then a repo-relative dev fallback.
|
|
180
|
+
- `installPrismerMcpServer` (default `true`), `prismerMcpServerPath` (override).
|
|
181
|
+
- `model` (default `us-kimi-k2.5`), `prismerProviderName` (default `prismer`), `prismerProviderBaseUrl` (default `$PRISMER_BASE_URL/api/v1`), `prismerApiKeyEnv` (default `PRISMER_API_KEY`, regex `^[A-Za-z_][A-Za-z0-9_]*$`).
|
|
182
|
+
- `mirrorNativeKanban` (default `true`) — mirrors Prismer `work_item` projections into Hermes Kanban as triage cards via `hermes -p <profile> kanban create … --triage --idempotency-key prismer:<parentTaskId>`.
|
|
183
|
+
- `mirrorNativeGoals` (default `true`) — writes Prismer standing objectives into Hermes' SQLite `state_meta["goal:<sessionId>"]` directly.
|
|
184
|
+
- `nativeMirrorTimeoutMs` (default 2000).
|
|
185
|
+
|
|
186
|
+
Dispatch flow:
|
|
187
|
+
|
|
188
|
+
1. (autostart only) spawn `hermes -p <profile> gateway run` with `API_SERVER_ENABLED=true`, `API_SERVER_KEY`, `API_SERVER_PORT`, `API_SERVER_HOST=127.0.0.1`.
|
|
189
|
+
2. Pre-bridge native Kanban + native Goals (best-effort, `nativeMirrorTimeoutMs` cap).
|
|
190
|
+
3. `POST /v1/runs {input, instructions, session_id, stream:true}` → `{run_id}`.
|
|
191
|
+
4. Stream `GET /v1/runs/:id/events` (SSE). Events: `message.delta` (concat to deltas), `run.completed` (final output), `run.failed` (throw), `tool.started` / `tool.completed` (forward to `task.onProgress` with `{kind:'tool', event, tool, summary, preview, duration, error, arguments, result}`), `reasoning.available` (forward as `{kind:'reasoning'}` without bumping progress).
|
|
192
|
+
5. On abort, `POST /v1/runs/:id/stop`.
|
|
193
|
+
6. Result includes `metadata.hermes.{status, lastSyncedAt, kanban, goals, runId, baseUrl, model}` for cloud-side bridge persistence (PATCH-merged onto `IMTask.metadata.bridge.hermes`).
|
|
98
194
|
|
|
99
|
-
###
|
|
195
|
+
### `claude-code`
|
|
100
196
|
|
|
101
|
-
|
|
197
|
+
`claude --print --model <m> [--system-prompt …] [--allowed-tools …] <prompt>` per dispatch. `--cwd` removed (uses `child_process.spawn` cwd); prompt is positional; `--headless` was renamed to `--print` in claude CLI 2.x. Config: `cwd` (required), `model` (default `'sonnet'`), `systemPrompt`, `envVars`, `mcpServers`, `allowedTools`, `maxTurns` (default 20), `baseURL`, `apiKeyRef` (`env:NAME` or `keychain:NAME` — keychain only on darwin via `security find-generic-password`), `route` (`'default'|'prismer'|'omniroute'`). `stdio: ['ignore', 'pipe', 'pipe']` to close child stdin (claude ≥ 2.1.128 prints "no stdin data received in 3s, proceeding without it" otherwise). `task.signal` → `child.kill('SIGTERM')`; `task.timeoutMs` → SIGTERM via `setTimeout`.
|
|
102
198
|
|
|
103
|
-
|
|
104
|
-
prismer register <username> # create a Prismer identity
|
|
105
|
-
prismer config show|set
|
|
106
|
-
prismer token refresh
|
|
199
|
+
### `codex`
|
|
107
200
|
|
|
108
|
-
|
|
109
|
-
prismer parse / parse-status / parse-result
|
|
110
|
-
prismer recall / discover # memory + agent discovery
|
|
111
|
-
```
|
|
201
|
+
`codex exec --model <m> --cd <cwd> --sandbox <level> --ephemeral --json <prompt>`. Config: `cwd` (required), `model` (default `codex-mini-latest`), `sandbox` (`'read-only'|'workspace-write'|'danger-full-access'`, default `workspace-write`), `systemPrompt` (prepended to user prompt — Codex CLI has no `--system-prompt` flag as of 2026-05), `envVars`, `apiKeyEnv` (default `OPENAI_API_KEY`). `parseCodexOutput` reads JSONL stdout backwards looking for `{"type":"message"|"assistant"|"output", "content"|"message"|"text": string}`; falls back to raw stdout. Outputs are capped at 64 KiB with `\n…[truncated]` suffix.
|
|
112
202
|
|
|
113
|
-
###
|
|
203
|
+
### `openclaw`
|
|
114
204
|
|
|
115
|
-
|
|
116
|
-
prismer task ... # Task Router (v1.9.0 cross-agent task routing)
|
|
117
|
-
prismer memory ... # Memory Gateway (hexagonal cloud/local ports)
|
|
118
|
-
prismer memory key-backup / key-recover / key-fingerprint # Shamir recovery
|
|
119
|
-
prismer evolution ... # Evolution Gateway
|
|
120
|
-
prismer tier:set <agent-id> <tier> # 1..7
|
|
121
|
-
prismer tier:get <agent-id>
|
|
122
|
-
prismer session export <sessionId> # copy ~/.prismer/trace/<id>.jsonl.zst
|
|
123
|
-
prismer events [--agent-id --session-id --family --type]
|
|
124
|
-
prismer events:stats
|
|
125
|
-
prismer permissions:test --tier N # inspect permission evaluation
|
|
126
|
-
```
|
|
205
|
+
HTTP-only client of `openclaw gateway`. `POST /v1/chat/completions` (OpenAI-compatible, non-streaming). Config: `port` (default `18789`), `apiKey` (bearer token from `gateway.auth.bearerTokens`), `model` (default `'openclaw'` — OpenClaw uses this to pick which agent answers, NOT the underlying LLM; primary LLM is set in `openclaw.json`). Includes a defensive strip of OpenClaw 2026.4.x's `Cannot read properties of undefined (reading '...')` stderr-bleed prefix that contaminates assistant content.
|
|
127
206
|
|
|
128
|
-
|
|
207
|
+
## Daemon architecture
|
|
129
208
|
|
|
130
209
|
```
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
210
|
+
┌──────────────────────────────────────────────────────────┐
|
|
211
|
+
│ Cloud (REST + WS multiplexed across hosted agents) │
|
|
212
|
+
└─────────────┬──────────────────────────┬─────────────────┘
|
|
213
|
+
│ WS ?token=<sk-prismer> │ REST CloudClient
|
|
214
|
+
│ │
|
|
215
|
+
┌─────▼─────┐ ┌──────────┐ ┌───▼─────────────────┐
|
|
216
|
+
│ WsClient ├──▶ dispatch ├──▶ adapter.dispatch / │
|
|
217
|
+
│ (1 conn) │ │ router │ │ ServicePool.ensure │
|
|
218
|
+
└─────┬─────┘ └─────┬────┘ └─────────┬───────────┘
|
|
219
|
+
│ │ │
|
|
220
|
+
host.acked runtimeRoute long-running
|
|
221
|
+
host.declare ='shell' → adapters reused
|
|
222
|
+
task.dispatch shell-executor per AgentProfile.id
|
|
223
|
+
│
|
|
224
|
+
┌──────▼─────────────────────────────────────┐
|
|
225
|
+
│ Runner (main loop, ~960 LOC) │
|
|
226
|
+
│ - LocalDb (better-sqlite3 ~/.prismer/local.db) │
|
|
227
|
+
│ - AssetCache (LRU, 5 GiB default) │
|
|
228
|
+
│ - UriResolver (prismer:// → file://) │
|
|
229
|
+
│ - SyncQueue + SyncWorker │
|
|
230
|
+
│ - LocalServer (127.0.0.1:3210 / :7878) │
|
|
231
|
+
│ - OutboxWatcher (container only) │
|
|
232
|
+
│ - 30s heartbeat re-declare │
|
|
233
|
+
│ - 60s stuck-task reaper │
|
|
234
|
+
└────────────────────────────────────────────┘
|
|
134
235
|
```
|
|
135
236
|
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
[
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
237
|
+
### `Runner` boot sequence (`src/daemon/runner.ts`)
|
|
238
|
+
|
|
239
|
+
1. Load `config.toml`; export `PRISMER_BASE_URL` + `PRISMER_API_KEY` so adapter children inherit the same provider.
|
|
240
|
+
2. Open SQLite, init asset cache, URI resolver, adapter registry, `ServicePool`.
|
|
241
|
+
3. **Static-binding preload** — `installStaticHostedAgentFromEnv` reads `PRISMER_HOSTED_AGENT_FILE` / `PRISMER_HOSTED_AGENT_JSON` and seeds one `agents` + `agent_profiles` row before the first `agent.host.declare`. `PRISMER_STATIC_BINDING_REQUIRED=1` makes a missing payload fatal.
|
|
242
|
+
4. Load remaining locally-registered agents from DB.
|
|
243
|
+
5. Spin `SyncWorker` (local-write → cloud-flush queue: PATCH/POST/DELETE per resource type, 4xx-other dropped, 5xx/408/429/0 retried).
|
|
244
|
+
6. Open WS; wire handlers.
|
|
245
|
+
7. (Container only — `PRISMER_CONTAINER_ID` set or `PRISMER_RUNTIME_MODE=container`) start `OutboxWatcher` on `/workspace/_outbox/` (uploads as sandbox-output assets, marks staged in `_uploaded/`).
|
|
246
|
+
8. Bind `LocalServer` (unless `--no-local-server`).
|
|
247
|
+
9. Start a 30s heartbeat that re-sends `agent.host.declare` so the cloud's 90s `sweepTimedOut()` doesn't flip status back offline.
|
|
248
|
+
10. Start a 60s **stuck-task reaper**: any `runningTasks` entry past `max(timeoutMs, 5min)` gets `ctrl.abort()` + a daemon-side `task.dispatch.reply {ok:false, error.code:'daemon_task_timeout'}` sent over WS — safety valve for upstream LLM gateway half-closes.
|
|
249
|
+
|
|
250
|
+
### Dispatch (`src/daemon/dispatch.ts`)
|
|
251
|
+
|
|
252
|
+
Pure function that runs a single task:
|
|
253
|
+
|
|
254
|
+
1. Resolve `AgentProfile` (cloud REST — by `profileId` or, if blank, the most-recently-created profile for `agentImUserId`).
|
|
255
|
+
2. Look up adapter; reply `{ok:false, error.code:'adapter_unhealthy'}` if missing.
|
|
256
|
+
3. Rewrite `prismer://` URIs in `prompt` and `context[].content` via `UriResolver`, pinning resolved hashes.
|
|
257
|
+
4. `composePrompt(...)` — concat context history + current message; trim oldest first when total chars exceed `contextMaxChars` (default 8000); always keep at least one entry.
|
|
258
|
+
5. **Memory context** — `GET /api/im/memory/digest?maxLines=120&maxBytes=4000` → prepend `[Memory Context]` block when non-empty.
|
|
259
|
+
6. **Goal context** — `GET /api/im/tasks?workspaceId=<ws>&limit=100`, filter for `metadata.kind==='goal' || intent==='standing_objective'`, dedupe by assignment, sort by `updatedAt`, take top 4 → prepend `[Active Goals]` block.
|
|
260
|
+
7. Resolve `profile.config.systemPrompt` (warn loudly if non-string — without this, role-template-driven agents silently regress to defaults).
|
|
261
|
+
8. Build `TaskInput { taskId, prompt, metadata: {...payload.metadata, conversationId, prismerGoals, systemPrompt?, prismerObservability}, timeoutMs, signal, onProgress }`. `onProgress` re-emits as `task.dispatch.progress` over WS.
|
|
262
|
+
9. Long-running: `servicePool.ensureService(profile, adapter).dispatch(taskInput)`. Interactive: `adapter.dispatch(profile, taskInput)`.
|
|
263
|
+
10. Build `task.dispatch.reply { taskId, ok, output, error, metrics }`.
|
|
264
|
+
11. **Bridge metadata writeback** — if `result.metadata.hermes` present, PATCH `/api/im/tasks/:id` merging onto `metadata.bridge.hermes` (with `lastSyncedAt`).
|
|
265
|
+
12. **Observability writeback** — PATCH `metadata.observability` with `{identity, memory, goals, lastSyncedAt}`.
|
|
266
|
+
13. Always unpin all resolved hashes.
|
|
267
|
+
|
|
268
|
+
### Daemon-local shell (`src/daemon/shell-executor.ts`)
|
|
269
|
+
|
|
270
|
+
`runtimeRoute='shell'` (or `metadata.execution.kind === 'shell'`) routes the dispatch to `executeShellDispatch` instead of an adapter. `ShellExecutionConfig` (resolved from `[shell]` block in `config.toml`):
|
|
271
|
+
|
|
272
|
+
- `enabled` (default `false`; `PRISMER_SHELL_ENABLED=true` env override).
|
|
273
|
+
- `defaultCwd` (default `process.cwd()`).
|
|
274
|
+
- `maxTimeoutMs` (default 60s, hard max 30 min).
|
|
275
|
+
- `maxOutputBytes` (default 256 KiB, hard max 5 MiB).
|
|
276
|
+
- `allowedWorkspaces?` (allow-list).
|
|
277
|
+
- `shell` (`bash|zsh|sh`, default `bash`).
|
|
278
|
+
|
|
279
|
+
Spawns `<shell> -lc <command>` with `payload.metadata.execution.cwd|env|shell` overrides; streams stdout/stderr → `task.dispatch.progress` events with `detail.{stream, chunk, sequence, truncated}`. Output (capped) is rendered as `$ <cmd>\ncwd: …\nshell: …\nexitCode: N\n[stdout]\n…\n[stderr]\n…`. Structured error codes: `shell_disabled`, `shell_workspace_not_allowed`, `shell_command_required`, `shell_cwd_missing`, `shell_spawn_failed`, `shell_timeout`, `task_cancelled`, `shell_exit_nonzero`. SIGTERM → 2s grace → SIGKILL on timeout/abort.
|
|
280
|
+
|
|
281
|
+
### `WsClient` (`src/daemon/ws-client.ts`)
|
|
282
|
+
|
|
283
|
+
WS to cloud, `?token=<apiKey>`. Exponential backoff 1s → 60s, capped at 30 attempts before 5min cooldown ("degraded"). Special close codes: `1000` normal (no reconnect), `4001` AUTH (no reconnect, emit `auth-failed`). Events: `open|message|close|error|drop|auth-failed|degraded|reconnect-scheduled`.
|
|
284
|
+
|
|
285
|
+
## WS protocol (cloud ↔ daemon)
|
|
286
|
+
|
|
287
|
+
Mirror types in `src/types/im-events.ts` (canonical chain: `src/im/types/im-events.ts` → SDK → runtime).
|
|
288
|
+
|
|
289
|
+
### Daemon → cloud
|
|
290
|
+
|
|
291
|
+
| Event | Payload |
|
|
292
|
+
| ----- | ------- |
|
|
293
|
+
| `agent.host.declare` | `{daemonId, daemonVersion, platform: 'darwin'|'linux'|'win32', agents: [{imUserId, name, adapterName, capabilities, profiles:[{id,version}]}]}`. Sent on `'authenticated'` ack and every 30s. Cloud stamps `daemonId` into `IMAgentCard.metadata` so the workspace runtime view groups daemon-declared agents under the right device; cloud also writes a Redis presence record at `runtime:device:<wsId>:<daemonId>` (TTL 90s). |
|
|
294
|
+
| `task.dispatch.reply` | `{taskId, ok, output?, error?, metrics?}`; `requestId` echoes the original request envelope. |
|
|
295
|
+
| `task.dispatch.progress` | `{taskId, progress: 0..1, message?, detail?}` — incremental. |
|
|
296
|
+
| `agent.status.changed` | `{agentImUserId, status, activeProfileId?, runningTaskIds?}` — emitted on agent state shifts. |
|
|
297
|
+
|
|
298
|
+
### Cloud → daemon
|
|
299
|
+
|
|
300
|
+
| Event | Payload |
|
|
301
|
+
| ----- | ------- |
|
|
302
|
+
| `host.acked` | `{workspaceId, syncCursor: {workspaces, agent_profiles}, profilesToSync: string[]}` — first reply post-declare; cloud also fires `redispatchPending(userId)` once per WS connection so any tasks accumulated while offline come back through `task.dispatch.request`. |
|
|
303
|
+
| `task.dispatch.request` | `{taskId, agentImUserId?, targetDaemonId?, profileId, capability, prompt, runtimeRoute?, metadata?, timeoutMs?, context?: TaskDispatchContextEntry[], conversationId?}`. `runtimeRoute` accepts `'agent' | 'sandbox' | 'shell'`. Per-`taskId` dedupe in `Runner.runningTasks` is what makes redispatch resilient to cloud's heartbeat-redeclare loop. |
|
|
304
|
+
| `task.cancel` | `{taskId, reason?}` → `runningTasks.get(taskId)?.ctrl.abort()`. |
|
|
305
|
+
| `agent.changed` | `{agentImUserId, fields: {displayName?, capabilities?}}` → patch in-memory. |
|
|
306
|
+
| `agent_profile.changed` | `{profileId, version}` → re-fetch via REST + redeclare. |
|
|
307
|
+
| `workspace.changed` | `{workspaceId, updatedAt}` → re-fetch. |
|
|
308
|
+
| `workspace_file.changed` | `{workspaceId, path, operation: 'create'|'update'|'delete', assetId?, contentHash?, version}` → upsert/delete `workspace_files_mirror`. |
|
|
309
|
+
|
|
310
|
+
The server-side `'authenticated'` ack from cloud triggers the first `agent.host.declare` (declaring on `open` is racy because cloud auth is an async DB lookup).
|
|
311
|
+
|
|
312
|
+
## Hermes long-running setup
|
|
313
|
+
|
|
314
|
+
Operators run `hermes` in their own Python venv. Sample `~/.hermes/profiles/<name>/config.yaml` (auto-written by the adapter when `configurePrismerProvider: true`):
|
|
315
|
+
|
|
316
|
+
```yaml
|
|
317
|
+
custom_providers:
|
|
318
|
+
prismer:
|
|
319
|
+
type: openai
|
|
320
|
+
base_url: https://prismer.cloud/api/v1
|
|
321
|
+
api_key_env: PRISMER_API_KEY
|
|
322
|
+
model:
|
|
323
|
+
provider: custom:prismer
|
|
324
|
+
name: us-kimi-k2.5
|
|
325
|
+
mcp_servers:
|
|
326
|
+
prismer-tasks:
|
|
327
|
+
command: node
|
|
328
|
+
args:
|
|
329
|
+
- /usr/local/lib/node_modules/@prismer/mcp-server/dist/index.js
|
|
165
330
|
```
|
|
166
331
|
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
Relevant environment variables:
|
|
170
|
-
|
|
171
|
-
| Var | Default | Purpose |
|
|
172
|
-
|---|---|---|
|
|
173
|
-
| `PRISMER_API_KEY` | — | Seeds `prismer setup` and daemon identity. |
|
|
174
|
-
| `PRISMER_BASE_URL` | `https://prismer.cloud` | Cloud API base for all daemon + CLI calls. |
|
|
175
|
-
| `PRISMER_API_BASE` | `https://prismer.cloud/api/im` | IM / PARA endpoints. |
|
|
176
|
-
| `PRISMER_MASTER_PASSPHRASE` | — | Enables the encrypted-file keychain backend when no system keychain is available. |
|
|
177
|
-
| `PRISMER_SKIP_POSTINSTALL` | — | Set to `1` to suppress `npm install` postinstall setup. |
|
|
178
|
-
| `PRISMER_FORCE_POSTINSTALL` | — | Set to `1` to run postinstall even on non-global installs. |
|
|
179
|
-
| `NO_COLOR` | — | Disable ANSI colour. |
|
|
180
|
-
|
|
181
|
-
### Keychain backends
|
|
182
|
-
|
|
183
|
-
The runtime probes backends in order:
|
|
184
|
-
|
|
185
|
-
1. **macOS Keychain** (`security` binary). Values written via `-X <hex>` so plaintext never appears in `ps aux`.
|
|
186
|
-
2. **libsecret** via `secret-tool` (Linux GNOME/KDE).
|
|
187
|
-
3. **pass** (`pass` store, optional GPG-backed).
|
|
188
|
-
4. **Encrypted-file fallback** — XChaCha20-Poly1305 with a key derived from `PRISMER_MASTER_PASSPHRASE`. Only enabled when the env var is set.
|
|
189
|
-
|
|
190
|
-
## Architecture
|
|
332
|
+
Then start the gateway with the API server platform enabled:
|
|
191
333
|
|
|
334
|
+
```sh
|
|
335
|
+
API_SERVER_ENABLED=true \
|
|
336
|
+
API_SERVER_KEY=<apiKey> \
|
|
337
|
+
API_SERVER_PORT=8642 \
|
|
338
|
+
API_SERVER_HOST=127.0.0.1 \
|
|
339
|
+
hermes -p <profile> gateway run
|
|
192
340
|
```
|
|
193
|
-
prismer CLI (bin/prismer.ts)
|
|
194
|
-
│
|
|
195
|
-
HTTP 127.0.0.1:3210
|
|
196
|
-
│
|
|
197
|
-
┌────────────────▼─────────────────┐
|
|
198
|
-
│ Daemon (daemon/runner.ts) │
|
|
199
|
-
│ ├─ HTTP server (daemon-http.ts) │
|
|
200
|
-
│ ├─ Multi-path transport │
|
|
201
|
-
│ │ ├─ WS to Cloud Relay │
|
|
202
|
-
│ │ └─ LAN probe │
|
|
203
|
-
│ ├─ Agent supervisor │
|
|
204
|
-
│ ├─ Events tailer (PARA → cloud) │
|
|
205
|
-
│ ├─ Outbox (SQLite WAL) │
|
|
206
|
-
│ └─ Memory Gateway (SQLite FTS5) │
|
|
207
|
-
└────────────────┬─────────────────┘
|
|
208
|
-
│
|
|
209
|
-
Installed agents (hooks)
|
|
210
|
-
claude-code, codex, openclaw, hermes
|
|
211
|
-
```
|
|
212
|
-
|
|
213
|
-
### Data directory layout (`~/.prismer/`)
|
|
214
|
-
|
|
215
|
-
| Path | Contents |
|
|
216
|
-
|---|---|
|
|
217
|
-
| `config.toml` | User config (see above). |
|
|
218
|
-
| `daemon.pid` | Atomic PID file. Owned by the running daemon. |
|
|
219
|
-
| `daemon.port` | Bound port (default 3210). |
|
|
220
|
-
| `daemon.starts.json` | Start-attempt log for crash-loop detection. |
|
|
221
|
-
| `logs/daemon.log` | Plain-text log output; tailed by `prismer daemon logs`. |
|
|
222
|
-
| `outbox.sqlite` | Ordered event buffer (WAL mode) for Cloud Relay. |
|
|
223
|
-
| `memory.sqlite` | Local Memory Gateway store with FTS5 indices. |
|
|
224
|
-
| `trace/<sessionId>.jsonl.zst` | Per-session PARA traces emitted by L8 adapters. |
|
|
225
|
-
| `para/events.jsonl` | Live PARA event stream tailed by `events-tailer`. |
|
|
226
|
-
| `sandbox/<agent>.sb` | macOS seatbelt sandbox profiles for installed agents. |
|
|
227
|
-
| `data/paired-devices.json` | Local device pairing registry. |
|
|
228
|
-
| `published-agents.toml` | Mirror of cloud-published agents. |
|
|
229
341
|
|
|
230
|
-
|
|
342
|
+
A plain `hermes gateway` without these env vars starts only configured messaging platforms and will never bind the `/v1/runs` HTTP API. Set `autoStart: true` on the AgentProfile to let the daemon spawn this command on first dispatch.
|
|
231
343
|
|
|
232
|
-
|
|
233
|
-
- **API key hygiene** — Keys are passed to child processes via named FDs or keychain reads, not via argv. macOS `security` uses `-X` hex encoding. Config writes should be `chmod 0o600` (see Known Issues).
|
|
234
|
-
- **Sandbox profiles** — macOS seatbelt / Linux AppArmor / bwrap. Generated from adapter manifests during install, removed on uninstall.
|
|
235
|
-
- **Device pairing** — Offer tokens are short-lived (5 min), single-use, exchanged over TLS to Cloud Relay. Revocation currently only prunes the local registry (see Known Issues).
|
|
344
|
+
The adapter mirrors Prismer projections into Hermes' native surfaces:
|
|
236
345
|
|
|
237
|
-
|
|
346
|
+
- **Kanban** — `hermes -p <profile> kanban create … --triage --idempotency-key prismer:<parentTaskId>`. Triage avoids duplicate execution; Prismer's `agent_run` remains the executable source of truth.
|
|
347
|
+
- **Goals** — direct write into Hermes' SQLite `state_meta["goal:<sessionId>"]` (Hermes has no public goals REST/CLI surface).
|
|
238
348
|
|
|
239
|
-
|
|
349
|
+
Bridge results are persisted on `IMTask.metadata.bridge.hermes` via the daemon's PATCH-merge writeback step.
|
|
240
350
|
|
|
241
|
-
|
|
242
|
-
- `prismer agent remove` does not prompt for confirmation; `--yes` is declared but unread.
|
|
243
|
-
- `prismer migrate` runs destructive steps on TTYs with no confirmation.
|
|
244
|
-
- `prismer pair revoke` is local-only; the server-side binding is not invalidated.
|
|
245
|
-
- `getAuthToken()` in `migrate-luminclaw-memory.ts` returns empty — the import flow requires authentication to be wired.
|
|
246
|
-
- Version strings in `install-agent.ts` success/already-installed paths are hardcoded to `1.9.0` instead of being read from the pack manifest.
|
|
247
|
-
- libsecret backend collapses "not found" and transient failures into `null` — the macOS backend distinguishes these.
|
|
248
|
-
- `daemon start` poll timeout of 2s can cause false-positive "failed to start" entries on slow machines and trigger the crash-loop guard.
|
|
351
|
+
## LocalServer HTTP API (loopback only)
|
|
249
352
|
|
|
250
|
-
|
|
353
|
+
`src/daemon/local-server.ts` binds `127.0.0.1:<port>`. Desktop default `:3210`, container `:7878`. CORS allows `*` / `GET,POST,OPTIONS` / `Content-Type,Authorization`.
|
|
251
354
|
|
|
252
|
-
|
|
355
|
+
| Method | Path | Purpose |
|
|
356
|
+
| ------ | ---- | ------- |
|
|
357
|
+
| OPTIONS | `*` | CORS preflight |
|
|
358
|
+
| GET | `/healthz` | `{status:'ok', daemonId, cloudBaseUrl, workspaceId, pid, startedAt, wsConnected, hostedAgents, observability}` |
|
|
359
|
+
| GET | `/agents` | `{agents: [{imUserId, name, adapterName}]}` |
|
|
360
|
+
| GET | `/tasks/running` | `{taskIds: string[]}` |
|
|
361
|
+
| POST | `/v1/runs` | Sandbox controller daemon-dispatch ack — `202 {runId, status:'accepted', taskId}`. Optional `payload.shellCommand` triggers async `bash -c <cmd>` with `cwd=/workspace` (escape hatch). |
|
|
362
|
+
| POST | `/v1/agents/install` | **Cloud → daemon agent install RPC**: validates body via `validateInstallAgentPayload`, calls `runner.installHostedAgent` → upsert `agents` + `agent_profiles` rows, reload, send `agent.host.declare`, return `{ok, daemonId, installedAgent, hostedAgents}`. |
|
|
363
|
+
| POST | `/v1/snapshot` | FS-manifest snapshot — walks `snapshotRoot` (default `/workspace`), computes per-file `{path, sha256, sizeBytes, mtime}`. Skips dotfiles + `_outbox/_uploaded`. 404 if root missing. |
|
|
253
364
|
|
|
254
|
-
|
|
255
|
-
# Build
|
|
256
|
-
npm run build # tsup bundle + copy icon/smallicon
|
|
365
|
+
The sandbox-controller's `installAgent` proxy at `POST /:id/installAgent` validates body via `InstallAgentSchema` (zod), resolves pod IP via `orchestrator.getContainerIp(id)`, and forwards to `http://<podIP>:7878/v1/agents/install`. The earlier `daemonDispatch` proxy at `POST /:id/daemonDispatch` forwards `{taskId, adapter?, prompt?, env?}` to `:7878/v1/runs`.
|
|
257
366
|
|
|
258
|
-
|
|
259
|
-
npm run typecheck
|
|
367
|
+
## Configuration
|
|
260
368
|
|
|
261
|
-
|
|
262
|
-
|
|
369
|
+
| Env var | Default | Purpose |
|
|
370
|
+
| ------- | ------- | ------- |
|
|
371
|
+
| `PRISMER_HOME` | `~/.prismer` | Daemon home (config, db, logs, adapter installs) |
|
|
372
|
+
| `PRISMER_BASE_URL` | from `config.toml` `cloud_api_base` | Cloud URL (LAN dev override) |
|
|
373
|
+
| `PRISMER_API_KEY` | from `config.toml` `api_key` | Cloud API key (also exported to adapter children) |
|
|
374
|
+
| `PRISMER_HOSTED_AGENT_FILE` | (empty) | Static-binding payload path (k8s/container) |
|
|
375
|
+
| `PRISMER_HOSTED_AGENT_JSON` | (empty) | Inline JSON variant |
|
|
376
|
+
| `PRISMER_STATIC_BINDING_REQUIRED` | `false` | Treat missing static binding as fatal |
|
|
377
|
+
| `PRISMER_CONTAINER_ID` | (empty) | Activates `OutboxWatcher` |
|
|
378
|
+
| `PRISMER_RUNTIME_MODE` | (empty) | `container` activates `OutboxWatcher` (alt to `PRISMER_CONTAINER_ID`) |
|
|
379
|
+
| `PRISMER_DAEMON_URL` | `http://127.0.0.1:3210` | Loopback URL used by `prismer memory` and `agent doctor` |
|
|
380
|
+
| `PRISMER_SHELL_ENABLED` | `false` | Override `[shell].enabled` in `config.toml` |
|
|
381
|
+
| `LOCAL_ONLY` | (empty) | `=1` enables `--as-user` pair bypass |
|
|
382
|
+
| `PRISMER_AUTH_TOKEN` | (empty) | Fallback for `prismer setup --token <jwt>` |
|
|
383
|
+
| `EDITOR` | `vi` | Used by `prismer profile edit` |
|
|
384
|
+
|
|
385
|
+
`config.toml` schema (TOML, written by `setup` / `pair`):
|
|
263
386
|
|
|
264
|
-
|
|
265
|
-
|
|
387
|
+
```toml
|
|
388
|
+
api_key = "sk-prismer-..."
|
|
389
|
+
cloud_api_base = "https://cloud.prismer.dev"
|
|
390
|
+
daemon_id = "..."
|
|
391
|
+
|
|
392
|
+
[adapters.claude-code]
|
|
393
|
+
package_manager = "npm"
|
|
394
|
+
package_name = "@anthropic-ai/claude-code"
|
|
395
|
+
binary = "/usr/local/bin/claude"
|
|
396
|
+
version = "2.1.128"
|
|
397
|
+
installed_at = "2026-05-07T00:00:00.000Z"
|
|
398
|
+
|
|
399
|
+
[shell]
|
|
400
|
+
enabled = false
|
|
401
|
+
maxTimeoutMs = 60000
|
|
402
|
+
maxOutputBytes = 262144
|
|
403
|
+
shell = "bash"
|
|
266
404
|
```
|
|
267
405
|
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
```
|
|
271
|
-
sdk/build/pack.sh --scope all --clean # tarball all SDKs
|
|
272
|
-
sdk/build/verify.sh --scope all # version parity + compile
|
|
273
|
-
public/install.sh --local-artifacts # install from local tarballs
|
|
274
|
-
```
|
|
406
|
+
## Troubleshooting
|
|
275
407
|
|
|
276
|
-
|
|
408
|
+
- **LaunchAgent respawn loop after install** — the plist must include `--foreground` in `ProgramArguments`. v1.9.3 installs do this automatically; if you're upgrading from 1.8.x re-run `curl -fsSL https://prismer.cloud/install.sh | sh -s -- --component=daemon` (the installer does `bootout` + `bootstrap` to reload).
|
|
409
|
+
- **Daemon shows "waiting for ~/.prismer/config.toml"** — that's expected behavior pre-pair. Run `prismer pair` (or `prismer setup sk-prismer-...`) and the daemon snaps to attention within 5s.
|
|
410
|
+
- **Cloud unreachable** — `prismer status` will print the failing base URL. Common causes: stale `cloud_api_base` after a tenant move (run `prismer setup --force <new-key> --cloud <url>`), or the API key was rotated (mint a new one with `prismer setup --token <jwt>`).
|
|
411
|
+
- **Agent shows under `__unbound__` device in workspace runtime view** — the daemon's `agent.host.declare` is not stamping `daemonId`. Confirm you're on `@prismer/runtime@1.9.3` (the `daemonId` payload field landed in 1.9.3).
|
|
412
|
+
- **`claude --print` runs report "no stdin data received in 3s"** — fixed in 1.9.3 (`stdio: ['ignore', 'pipe', 'pipe']`). Older runtimes will surface the warning but the dispatch still completes.
|
|
413
|
+
- **Tasks dispatch but `@`-mention messages never reach the agent** — cloud-side `SHADOW_JOIN_FAILED` rollback. The runtime's `agent.host.declare` requires every declared agent to auto-join all its conversation rooms; check daemon logs for the rollback line and re-register the agent (`prismer agent register …`) to fix the cloud-side row.
|
|
414
|
+
- **Stuck task that never replies** — the 60s reaper aborts and replies `{ok:false, error.code:'daemon_task_timeout'}` after `max(timeoutMs, 5min)`. Cloud will mark the task `failed` accordingly. Inspect via `prismer task get <id>`.
|
|
277
415
|
|
|
278
|
-
##
|
|
416
|
+
## Versioning
|
|
279
417
|
|
|
280
|
-
|
|
281
|
-
- Cloud dashboard: https://prismer.cloud
|
|
282
|
-
- Issues: https://github.com/Prismer-AI/PrismerCloud/issues
|
|
283
|
-
- SDK: [`@prismer/sdk`](https://www.npmjs.com/package/@prismer/sdk)
|
|
284
|
-
- Plugins: [`@prismer/claude-code-plugin`](https://www.npmjs.com/package/@prismer/claude-code-plugin), [`@prismer/openclaw-channel`](https://www.npmjs.com/package/@prismer/openclaw-channel)
|
|
418
|
+
> See [CHANGELOG.md](./CHANGELOG.md). Current: **v1.9.3** (2026-05-07).
|
|
285
419
|
|
|
286
420
|
## License
|
|
287
421
|
|
|
288
|
-
MIT
|
|
422
|
+
MIT.
|