@prismer/runtime 1.9.0-hotfix.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (39) hide show
  1. package/README.md +288 -0
  2. package/dist/adapter-registry-EMZFKFYK.mjs +8 -0
  3. package/dist/adapter-registry-EMZFKFYK.mjs.map +1 -0
  4. package/dist/artifacts-uploader-N2HNBSTW.mjs +159 -0
  5. package/dist/artifacts-uploader-N2HNBSTW.mjs.map +1 -0
  6. package/dist/auto-register-M4YSPJS6.mjs +204 -0
  7. package/dist/auto-register-M4YSPJS6.mjs.map +1 -0
  8. package/dist/bin/prismer.js +14988 -0
  9. package/dist/chunk-BJTO5JO5.mjs +11 -0
  10. package/dist/chunk-BJTO5JO5.mjs.map +1 -0
  11. package/dist/chunk-JIEDFDVI.mjs +148 -0
  12. package/dist/chunk-JIEDFDVI.mjs.map +1 -0
  13. package/dist/chunk-NDNX2G6O.mjs +68 -0
  14. package/dist/chunk-NDNX2G6O.mjs.map +1 -0
  15. package/dist/chunk-POWV475F.mjs +45 -0
  16. package/dist/chunk-POWV475F.mjs.map +1 -0
  17. package/dist/chunk-VTFKZAUY.mjs +68 -0
  18. package/dist/chunk-VTFKZAUY.mjs.map +1 -0
  19. package/dist/dispatch-mux-MW4HHICS.mjs +8 -0
  20. package/dist/dispatch-mux-MW4HHICS.mjs.map +1 -0
  21. package/dist/dispatch-rpc-J7H6KC2V.mjs +78 -0
  22. package/dist/dispatch-rpc-J7H6KC2V.mjs.map +1 -0
  23. package/dist/fs-rpc-OSHJZYK6.mjs +58 -0
  24. package/dist/fs-rpc-OSHJZYK6.mjs.map +1 -0
  25. package/dist/heartbeat-loop-H2LAI3V5.mjs +95 -0
  26. package/dist/heartbeat-loop-H2LAI3V5.mjs.map +1 -0
  27. package/dist/icon +21 -0
  28. package/dist/index.d.mts +2063 -0
  29. package/dist/index.d.ts +2063 -0
  30. package/dist/index.js +10013 -0
  31. package/dist/index.js.map +1 -0
  32. package/dist/index.mjs +8807 -0
  33. package/dist/index.mjs.map +1 -0
  34. package/dist/mode-b-DXJ7FJAL.mjs +152 -0
  35. package/dist/mode-b-DXJ7FJAL.mjs.map +1 -0
  36. package/dist/registry-ZYU2HDFL.mjs +12 -0
  37. package/dist/registry-ZYU2HDFL.mjs.map +1 -0
  38. package/dist/smallicon +6 -0
  39. package/package.json +72 -0
package/README.md ADDED
@@ -0,0 +1,288 @@
1
+ # @prismer/runtime
2
+
3
+ Prismer Cloud v1.9.0 daemon runtime and `prismer` CLI.
4
+
5
+ This package hosts the local Prismer daemon, the HTTP control plane on port 3210, the multi-path transport to Cloud Relay (WebSocket), the agent supervisor and PARA adapter registry, and the `prismer` command-line interface. It is one of the three core v1.9.0 tracks — see [`docs/version190/02-architecture.md`](https://github.com/Prismer-AI/PrismerCloud/blob/main/docs/version190/02-architecture.md) for the full design.
6
+
7
+ ## Install
8
+
9
+ ### Bootstrap installer (recommended)
10
+
11
+ ```
12
+ curl -fsSL https://prismer.cloud/install.sh | sh
13
+ ```
14
+
15
+ Auto-detects Node via `fnm`, installs `@prismer/sdk` and `@prismer/runtime` globally, launches the daemon, and runs first-time setup. macOS and Linux, no sudo, no Homebrew required.
16
+
17
+ ### Manual (npm)
18
+
19
+ ```
20
+ npm install -g @prismer/runtime @prismer/sdk
21
+ prismer setup
22
+ ```
23
+
24
+ Requires Node.js 20+. `@prismer/sdk` is required — the runtime mounts the SDK's `register / config / token / send / load / search / parse / recall / discover` commands via `@prismer/sdk/cli`.
25
+
26
+ ## Quick start
27
+
28
+ ```
29
+ # 1. Provide an API key (persists to ~/.prismer/config.toml)
30
+ prismer setup sk-prismer-live-...
31
+
32
+ # 2. Start the daemon (HTTP on 127.0.0.1:3210 + Cloud Relay WS)
33
+ 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
+ ```
45
+
46
+ `PRISMER_API_KEY=sk-prismer-... prismer setup` works equivalently for Docker and CI — no browser OAuth round-trip is required when the key is supplied out-of-band.
47
+
48
+ ## CLI surface
49
+
50
+ ### Lifecycle
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. |
62
+
63
+ The daemon writes its PID to `~/.prismer/daemon.pid` atomically with `O_CREAT | O_EXCL`. A crash-loop guard in `daemon.starts.json` blocks rapid restart storms (3 failures in 10 minutes → 5-minute cooldown).
64
+
65
+ ### Agent control
66
+
67
+ | Command | Purpose |
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). |
76
+
77
+ Shipped adapters: `claude-code`, `codex`, `openclaw`, `hermes`. Each ships via npm with an Ed25519-signed manifest attached to the matching GitHub Release. See [`docs/version190/15-cli-design.md`](https://github.com/Prismer-AI/PrismerCloud/blob/main/docs/version190/15-cli-design.md) §15.6 for pack schema and signing flow.
78
+
79
+ ### Pack registry
80
+
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
+ ```
96
+
97
+ Pairing offers use 5-minute TTL tokens exchanged via Cloud Relay. The CLI polls the daemon for pair completion and writes the resulting device record to `~/.prismer/data/paired-devices.json`.
98
+
99
+ ### Domain commands (via SDK)
100
+
101
+ Mounted at runtime via `@prismer/sdk/cli`:
102
+
103
+ ```
104
+ prismer register <username> # create a Prismer identity
105
+ prismer config show|set
106
+ prismer token refresh
107
+
108
+ prismer send / load / search # IM + context shortcuts
109
+ prismer parse / parse-status / parse-result
110
+ prismer recall / discover # memory + agent discovery
111
+ ```
112
+
113
+ ### Domain commands (native)
114
+
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
+ ```
127
+
128
+ ### Migration
129
+
130
+ ```
131
+ prismer migrate # v1.8 → v1.9 upgrade (api key → keychain, hooks → daemon)
132
+ prismer migrate luminclaw-memory # import luminclaw local memory to Memory Gateway
133
+ prismer migrate-secrets # sweep config.toml plaintext → keychain
134
+ ```
135
+
136
+ `migrate-secrets --dry-run` previews without touching files or the keychain.
137
+
138
+ ## Global flags
139
+
140
+ Applied before Commander parses the command tree — all subcommands honour them:
141
+
142
+ | Flag | Effect |
143
+ |---|---|
144
+ | `--json` | Machine-readable single-line JSON output. Mutually exclusive with `--follow` on log streaming. |
145
+ | `--quiet` | Suppress non-error output. |
146
+ | `--color` / `--no-color` | Force colour on / off. `NO_COLOR` env is also honoured. |
147
+
148
+ All error-path JSON conforms to the Prismer API envelope: `{ success: false, error: { code, message }, ... }`.
149
+
150
+ ## Configuration
151
+
152
+ Config lives at `~/.prismer/config.toml`:
153
+
154
+ ```toml
155
+ [default]
156
+ api_key = "sk-prismer-live-..."
157
+ environment = "production"
158
+ base_url = "https://prismer.cloud"
159
+
160
+ [daemon]
161
+ id = "daemon:abc123..."
162
+
163
+ [user]
164
+ id = "user:abc123..."
165
+ ```
166
+
167
+ Resolution order for any value: **explicit flag > environment variable > config.toml > keychain placeholder > default**. `sk-prismer-...` API keys may also live in the keychain under service `prismer-config/default.api_key` and be referenced from the TOML via `$KEYRING:prismer-config/default.api_key`.
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
191
+
192
+ ```
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
+
230
+ ## Security
231
+
232
+ - **Ed25519 signed packs** — Adapter manifests are attached to the matching GitHub Release and carry detached Ed25519 signatures. `install-agent.ts` verifies before wiring hooks; npm-source fallback is treated as lower trust and signalled to the user. `--skip-verify` is allowed for offline / dev.
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).
236
+
237
+ ## Known issues
238
+
239
+ The CLI path is under active hardening. Tracked items:
240
+
241
+ - `install-agent.ts` catches Ed25519 signature failure in the CDN branch and falls through to the npm tier — a tampered pack can be silently downgraded. **Fixing imminently.**
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.
249
+
250
+ Fixes land in v1.9.1. Open an issue or PR at [github.com/Prismer-AI/PrismerCloud](https://github.com/Prismer-AI/PrismerCloud).
251
+
252
+ ## Development
253
+
254
+ ```
255
+ # Build
256
+ npm run build # tsup bundle + copy icon/smallicon
257
+
258
+ # Type check only
259
+ npm run typecheck
260
+
261
+ # Run the vitest suite
262
+ npm test
263
+
264
+ # Clean dist
265
+ npm run clean
266
+ ```
267
+
268
+ `@prismer/*` dependencies resolve from the npm registry. Fresh clones of the closed-source repo cannot `npm install` until upstream packages publish — use the monorepo build scripts instead:
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
+ ```
275
+
276
+ Release is driven from the source repo via `sdk/build/release.sh --scope prismer-cloud`, which syncs into the open-source mirror before tagging and publish. Hotfixes use `sdk/build/hotfix.sh @prismer/runtime 1.9.0.N` and do **not** bump the root `/VERSION`.
277
+
278
+ ## Links
279
+
280
+ - Design: [`docs/version190/02-architecture.md`](https://github.com/Prismer-AI/PrismerCloud/blob/main/docs/version190/02-architecture.md), [`docs/version190/15-cli-design.md`](https://github.com/Prismer-AI/PrismerCloud/blob/main/docs/version190/15-cli-design.md)
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)
285
+
286
+ ## License
287
+
288
+ MIT
@@ -0,0 +1,8 @@
1
+ import {
2
+ AdapterRegistry
3
+ } from "./chunk-VTFKZAUY.mjs";
4
+ import "./chunk-BJTO5JO5.mjs";
5
+ export {
6
+ AdapterRegistry
7
+ };
8
+ //# sourceMappingURL=adapter-registry-EMZFKFYK.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":[],"sourcesContent":[],"mappings":"","names":[]}
@@ -0,0 +1,159 @@
1
+ import "./chunk-BJTO5JO5.mjs";
2
+
3
+ // src/artifacts-uploader.ts
4
+ import * as fs from "fs";
5
+ import * as path from "path";
6
+ var MAX_SIMPLE_UPLOAD_BYTES = 10 * 1024 * 1024;
7
+ var ArtifactUploader = class {
8
+ apiKey;
9
+ cloudApiBase;
10
+ fetchImpl;
11
+ constructor(opts) {
12
+ if (!opts.apiKey) throw new Error("ArtifactUploader: apiKey required");
13
+ if (!opts.cloudApiBase) throw new Error("ArtifactUploader: cloudApiBase required");
14
+ this.apiKey = opts.apiKey;
15
+ this.cloudApiBase = opts.cloudApiBase.replace(/\/+$/, "");
16
+ this.fetchImpl = opts.fetchImpl ?? globalThis.fetch;
17
+ if (typeof this.fetchImpl !== "function") {
18
+ throw new Error("ArtifactUploader: fetch is not available \u2014 pass fetchImpl");
19
+ }
20
+ }
21
+ /**
22
+ * Upload one artifact for the given task. Returns:
23
+ * - `{ ok:true, uploadId, cdnUrl }` on success.
24
+ * - `{ ok:false, error }` on any failure — never throws.
25
+ */
26
+ async upload(taskId, artifactPath, options = {}) {
27
+ if (!taskId) return { ok: false, error: "taskId required" };
28
+ if (!artifactPath) return { ok: false, error: "artifactPath required" };
29
+ let buffer;
30
+ let stat;
31
+ try {
32
+ stat = fs.statSync(artifactPath);
33
+ if (!stat.isFile()) {
34
+ return { ok: false, error: `artifact is not a file: ${artifactPath}` };
35
+ }
36
+ if (stat.size > MAX_SIMPLE_UPLOAD_BYTES) {
37
+ return { ok: false, error: `artifact_too_large:${stat.size}` };
38
+ }
39
+ buffer = await fs.promises.readFile(artifactPath);
40
+ } catch (err) {
41
+ return { ok: false, error: `read_failed: ${err.message}` };
42
+ }
43
+ const fileName = options.fileName ?? path.basename(artifactPath);
44
+ const mimeType = options.mimeType ?? guessMime(fileName);
45
+ let presigned;
46
+ try {
47
+ const res = await this.fetchImpl(`${this.cloudApiBase}/api/im/artifacts/presign`, {
48
+ method: "POST",
49
+ headers: {
50
+ "Content-Type": "application/json",
51
+ Authorization: `Bearer ${this.apiKey}`
52
+ },
53
+ body: JSON.stringify({
54
+ taskId,
55
+ fileName,
56
+ fileSize: stat.size,
57
+ mimeType
58
+ })
59
+ });
60
+ if (!res.ok) {
61
+ const text = await safeText(res);
62
+ return { ok: false, error: `presign_${res.status}: ${text}` };
63
+ }
64
+ const body = await res.json();
65
+ if (!body.ok || !body.data) {
66
+ return { ok: false, error: `presign_invalid_response: ${body.error ?? "no data"}` };
67
+ }
68
+ presigned = body.data;
69
+ } catch (err) {
70
+ return { ok: false, error: `presign_threw: ${err.message}` };
71
+ }
72
+ try {
73
+ const form = new FormData();
74
+ for (const [k, v] of Object.entries(presigned.fields ?? {})) {
75
+ form.append(k, v);
76
+ }
77
+ const uploadUrl = presigned.url.startsWith("http") ? presigned.url : `${this.cloudApiBase}${presigned.url}`;
78
+ const blob = new Blob([buffer], { type: mimeType });
79
+ form.append("file", blob, fileName);
80
+ const headers = {};
81
+ if (!presigned.url.startsWith("http")) {
82
+ headers["Authorization"] = `Bearer ${this.apiKey}`;
83
+ }
84
+ const res = await this.fetchImpl(uploadUrl, {
85
+ method: "POST",
86
+ headers,
87
+ body: form
88
+ });
89
+ if (!res.ok) {
90
+ const text = await safeText(res);
91
+ return { ok: false, error: `upload_${res.status}: ${text}` };
92
+ }
93
+ } catch (err) {
94
+ return { ok: false, error: `upload_threw: ${err.message}` };
95
+ }
96
+ try {
97
+ const res = await this.fetchImpl(`${this.cloudApiBase}/api/im/artifacts/confirm`, {
98
+ method: "POST",
99
+ headers: {
100
+ "Content-Type": "application/json",
101
+ Authorization: `Bearer ${this.apiKey}`
102
+ },
103
+ body: JSON.stringify({ uploadId: presigned.uploadId })
104
+ });
105
+ if (!res.ok) {
106
+ const text = await safeText(res);
107
+ return { ok: false, error: `confirm_${res.status}: ${text}` };
108
+ }
109
+ const body = await res.json();
110
+ if (!body.ok || !body.data) {
111
+ return { ok: false, error: `confirm_invalid_response: ${body.error ?? "no data"}` };
112
+ }
113
+ return { ok: true, uploadId: body.data.uploadId, cdnUrl: body.data.cdnUrl };
114
+ } catch (err) {
115
+ return { ok: false, error: `confirm_threw: ${err.message}` };
116
+ }
117
+ }
118
+ };
119
+ async function safeText(res) {
120
+ try {
121
+ return (await res.text()).slice(0, 500);
122
+ } catch {
123
+ return "<no body>";
124
+ }
125
+ }
126
+ function guessMime(fileName) {
127
+ const ext = path.extname(fileName).toLowerCase();
128
+ switch (ext) {
129
+ case ".png":
130
+ return "image/png";
131
+ case ".jpg":
132
+ case ".jpeg":
133
+ return "image/jpeg";
134
+ case ".gif":
135
+ return "image/gif";
136
+ case ".webp":
137
+ return "image/webp";
138
+ case ".pdf":
139
+ return "application/pdf";
140
+ case ".json":
141
+ return "application/json";
142
+ case ".txt":
143
+ return "text/plain";
144
+ case ".md":
145
+ return "text/markdown";
146
+ case ".zip":
147
+ return "application/zip";
148
+ case ".html":
149
+ return "text/html";
150
+ case ".csv":
151
+ return "text/csv";
152
+ default:
153
+ return "application/octet-stream";
154
+ }
155
+ }
156
+ export {
157
+ ArtifactUploader
158
+ };
159
+ //# sourceMappingURL=artifacts-uploader-N2HNBSTW.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/artifacts-uploader.ts"],"sourcesContent":["/**\n * Prismer Runtime — Artifact Uploader (v1.9.x Task 2).\n *\n * After an adapter returns from `task.dispatch` with `artifacts: [{path, …}]`,\n * the daemon calls `ArtifactUploader.upload(taskId, path)` for each artifact.\n *\n * The uploader is intentionally defensive — failures at any step return a\n * structured `{ ok:false, error }` rather than throwing, so they never block\n * the rpc.response back to the cloud. The cloud already sees the artifact\n * paths in the dispatch result; the upload is best-effort enrichment that\n * lets iOS Library show downloadable files instead of agent-local paths.\n *\n * Three-step flow:\n * 1. POST /api/im/artifacts/presign → { uploadId, url, fields }\n * 2. POST <url> (multipart form) → S3 (or dev-upload) accepts bytes\n * 3. POST /api/im/artifacts/confirm → { uploadId, cdnUrl, … }\n *\n * The presign envelope includes both a presigned-POST `url` (S3) AND a set\n * of form fields that must be sent verbatim. We post them as multipart/form-data\n * with `file` last (S3 spec requirement).\n */\n\nimport * as fs from 'node:fs';\nimport * as path from 'node:path';\n\n/**\n * Cloud-side simple-upload limit (`FileService.presign` enforces 10 MiB). We\n * mirror it client-side so the daemon doesn't buffer hundreds of MB into\n * memory just to be rejected by presign — a 500MB log dump from an adapter\n * would OOM the daemon long before the cloud got a chance to say no.\n */\nconst MAX_SIMPLE_UPLOAD_BYTES = 10 * 1024 * 1024;\n\nexport interface ArtifactUploaderOptions {\n /** Cloud API key (sk-prismer-…) for the calling user/agent. */\n apiKey: string;\n /** Cloud API base URL, e.g. \"https://prismer.cloud\" or \"https://cloud.prismer.dev\". */\n cloudApiBase: string;\n /** Optional fetch impl override — useful for tests. Defaults to globalThis.fetch. */\n fetchImpl?: typeof fetch;\n}\n\nexport interface ArtifactUploadOptions {\n /** Override the file name sent in the presign request. Defaults to basename(path). */\n fileName?: string;\n /** Override the MIME type. Defaults to application/octet-stream when unknown. */\n mimeType?: string;\n}\n\nexport type ArtifactUploadResult =\n | { ok: true; uploadId: string; cdnUrl: string }\n | { ok: false; error: string };\n\ninterface PresignResponse {\n uploadId: string;\n url: string;\n fields: Record<string, string>;\n expiresAt: string;\n taskId: string;\n}\n\ninterface ConfirmResponse {\n uploadId: string;\n cdnUrl: string;\n fileName: string;\n fileSize: number;\n mimeType: string;\n sha256: string | null;\n cost: number;\n}\n\nexport class ArtifactUploader {\n private readonly apiKey: string;\n private readonly cloudApiBase: string;\n private readonly fetchImpl: typeof fetch;\n\n constructor(opts: ArtifactUploaderOptions) {\n if (!opts.apiKey) throw new Error('ArtifactUploader: apiKey required');\n if (!opts.cloudApiBase) throw new Error('ArtifactUploader: cloudApiBase required');\n this.apiKey = opts.apiKey;\n this.cloudApiBase = opts.cloudApiBase.replace(/\\/+$/, '');\n this.fetchImpl = opts.fetchImpl ?? (globalThis.fetch as typeof fetch);\n if (typeof this.fetchImpl !== 'function') {\n throw new Error('ArtifactUploader: fetch is not available — pass fetchImpl');\n }\n }\n\n /**\n * Upload one artifact for the given task. Returns:\n * - `{ ok:true, uploadId, cdnUrl }` on success.\n * - `{ ok:false, error }` on any failure — never throws.\n */\n async upload(\n taskId: string,\n artifactPath: string,\n options: ArtifactUploadOptions = {},\n ): Promise<ArtifactUploadResult> {\n if (!taskId) return { ok: false, error: 'taskId required' };\n if (!artifactPath) return { ok: false, error: 'artifactPath required' };\n\n // 0. Stat the file (and read into memory — artifacts in v1.9 are <= 10MB\n // per the simple-upload limit; multipart streaming is Sprint C+).\n //\n // We stat FIRST and refuse oversized files before the read, so a 500MB\n // adapter dump can't OOM the daemon. The cloud presign enforces the\n // same limit, but only after we've buffered the whole file.\n let buffer: Buffer;\n let stat: fs.Stats;\n try {\n stat = fs.statSync(artifactPath);\n if (!stat.isFile()) {\n return { ok: false, error: `artifact is not a file: ${artifactPath}` };\n }\n if (stat.size > MAX_SIMPLE_UPLOAD_BYTES) {\n return { ok: false, error: `artifact_too_large:${stat.size}` };\n }\n // Async read to keep the event loop responsive for other tasks the\n // daemon is concurrently dispatching.\n buffer = await fs.promises.readFile(artifactPath);\n } catch (err) {\n return { ok: false, error: `read_failed: ${(err as Error).message}` };\n }\n\n const fileName = options.fileName ?? path.basename(artifactPath);\n const mimeType = options.mimeType ?? guessMime(fileName);\n\n // 1. Presign.\n let presigned: PresignResponse;\n try {\n const res = await this.fetchImpl(`${this.cloudApiBase}/api/im/artifacts/presign`, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n Authorization: `Bearer ${this.apiKey}`,\n },\n body: JSON.stringify({\n taskId,\n fileName,\n fileSize: stat.size,\n mimeType,\n }),\n });\n if (!res.ok) {\n const text = await safeText(res);\n return { ok: false, error: `presign_${res.status}: ${text}` };\n }\n const body = (await res.json()) as { ok?: boolean; data?: PresignResponse; error?: string };\n if (!body.ok || !body.data) {\n return { ok: false, error: `presign_invalid_response: ${body.error ?? 'no data'}` };\n }\n presigned = body.data;\n } catch (err) {\n return { ok: false, error: `presign_threw: ${(err as Error).message}` };\n }\n\n // 2. POST the file to the presigned URL. The fields object is sent first\n // as form fields, then `file` last (S3 presigned-POST requirement).\n try {\n const form = new FormData();\n for (const [k, v] of Object.entries(presigned.fields ?? {})) {\n form.append(k, v);\n }\n // Resolve the upload URL: presign returns a relative path in dev mode\n // (e.g. `/api/im/files/dev-upload/<id>`). Make it absolute against\n // cloudApiBase so the same flow works for dev + S3.\n const uploadUrl = presigned.url.startsWith('http')\n ? presigned.url\n : `${this.cloudApiBase}${presigned.url}`;\n\n // `Buffer` is BodyInit-compatible via Blob (works on Node 20+).\n const blob = new Blob([buffer], { type: mimeType });\n form.append('file', blob, fileName);\n\n // Dev-upload route requires an Authorization header. S3 presigned POST\n // does NOT — including it is harmless because S3 ignores unknown\n // headers, but we omit the auth header for non-relative URLs to avoid\n // accidental signature interference.\n const headers: Record<string, string> = {};\n if (!presigned.url.startsWith('http')) {\n headers['Authorization'] = `Bearer ${this.apiKey}`;\n }\n\n const res = await this.fetchImpl(uploadUrl, {\n method: 'POST',\n headers,\n body: form,\n });\n if (!res.ok) {\n const text = await safeText(res);\n return { ok: false, error: `upload_${res.status}: ${text}` };\n }\n } catch (err) {\n return { ok: false, error: `upload_threw: ${(err as Error).message}` };\n }\n\n // 3. Confirm.\n try {\n const res = await this.fetchImpl(`${this.cloudApiBase}/api/im/artifacts/confirm`, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n Authorization: `Bearer ${this.apiKey}`,\n },\n body: JSON.stringify({ uploadId: presigned.uploadId }),\n });\n if (!res.ok) {\n const text = await safeText(res);\n return { ok: false, error: `confirm_${res.status}: ${text}` };\n }\n const body = (await res.json()) as { ok?: boolean; data?: ConfirmResponse; error?: string };\n if (!body.ok || !body.data) {\n return { ok: false, error: `confirm_invalid_response: ${body.error ?? 'no data'}` };\n }\n return { ok: true, uploadId: body.data.uploadId, cdnUrl: body.data.cdnUrl };\n } catch (err) {\n return { ok: false, error: `confirm_threw: ${(err as Error).message}` };\n }\n }\n}\n\n// ─── Helpers ────────────────────────────────────────────\n\nasync function safeText(res: Response): Promise<string> {\n try {\n return (await res.text()).slice(0, 500);\n } catch {\n return '<no body>';\n }\n}\n\n/** Minimal MIME guess — covers the common artifact extensions. */\nfunction guessMime(fileName: string): string {\n const ext = path.extname(fileName).toLowerCase();\n switch (ext) {\n case '.png':\n return 'image/png';\n case '.jpg':\n case '.jpeg':\n return 'image/jpeg';\n case '.gif':\n return 'image/gif';\n case '.webp':\n return 'image/webp';\n case '.pdf':\n return 'application/pdf';\n case '.json':\n return 'application/json';\n case '.txt':\n return 'text/plain';\n case '.md':\n return 'text/markdown';\n case '.zip':\n return 'application/zip';\n case '.html':\n return 'text/html';\n case '.csv':\n return 'text/csv';\n default:\n return 'application/octet-stream';\n }\n}\n"],"mappings":"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","names":[]}