@letta-ai/dreams 0.0.1 → 0.0.3

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/dist/skill.js CHANGED
@@ -35,6 +35,8 @@ var __importStar = (this && this.__importStar) || (function () {
35
35
  Object.defineProperty(exports, "__esModule", { value: true });
36
36
  exports.skillDir = skillDir;
37
37
  exports.skillFilePath = skillFilePath;
38
+ exports.initSnapshotTemplatePath = initSnapshotTemplatePath;
39
+ exports.claudeHooksReferencePath = claudeHooksReferencePath;
38
40
  exports.installedSkillVersion = installedSkillVersion;
39
41
  exports.installSkill = installSkill;
40
42
  const crypto = __importStar(require("node:crypto"));
@@ -50,6 +52,12 @@ function skillDir() {
50
52
  function skillFilePath() {
51
53
  return path.join(skillDir(), "SKILL.md");
52
54
  }
55
+ function initSnapshotTemplatePath() {
56
+ return path.join(skillDir(), "references", "init-snapshot.template.json");
57
+ }
58
+ function claudeHooksReferencePath() {
59
+ return path.join(skillDir(), "references", "hooks-claude.md");
60
+ }
53
61
  function skillContent(version) {
54
62
  return `---
55
63
  name: dreams-setup
@@ -59,28 +67,276 @@ version: ${version}
59
67
 
60
68
  # Dreams setup
61
69
 
62
- Start or repair the local setup with:
70
+ CLI owns side effects. This skill owns sequencing and user conversation. There is no durable resume pointer: in a new conversation, start at step 1 and skip work that durable facts show is already satisfied. Never blindly repeat mutations. Never print or summarize access tokens, refresh tokens, credential files, or keychain contents.
71
+
72
+ ## Step 0 — Local bootstrap (optional repair)
73
+
74
+ If the durable CLI or this skill may be missing, run:
63
75
 
64
76
  \`\`\`bash
65
77
  npx @letta-ai/dreams@latest onboard --json
66
78
  \`\`\`
67
79
 
68
- \`onboard\` installs an exact CLI version at a stable local path, creates or reuses the installation identity, installs this skill, initializes local SQLite, and checks authentication. It does not log in or run \`orient\` itself.
80
+ \`onboard\` installs or repairs the durable CLI, installation identity, this skill (including reference files), and local SQLite. It does **not** check authentication or choose the next setup step. On success it returns \`status: "ready"\` with absolute \`cli_path\` and \`skill.path\`. Use that \`cli_path\` for later Dreams commands and for harness hook commands.
81
+
82
+ ## Step 1 — Authenticate
83
+
84
+ 1. Run \`dreams auth status --json\`.
85
+ 2. If unauthenticated, run \`dreams auth login --json\`, wait for browser approval (JSON Lines: \`awaiting_approval\` then \`authenticated\`), then re-check \`auth status\`.
86
+ 3. Prefer an existing usable credential; do not force a new login when status is already authenticated.
87
+
88
+ ## Step 2 — Detect and register source
89
+
90
+ Run:
91
+
92
+ \`\`\`bash
93
+ dreams source detect --json
94
+ \`\`\`
95
+
96
+ This detects Claude Code from harness filesystem facts under \`~/.claude\` only (no transcript reads), registers the installation-scoped source with Cloud as \`detected\`, and persists workspace/source ids locally. It is idempotent: re-running refreshes metadata and must not regress lifecycle state. Do not scan, approve roots, or upload in this step.
97
+
98
+ ## Step 3 — Init snapshot
99
+
100
+ 1. Read the reference template at \`references/init-snapshot.template.json\` in this skill directory.
101
+ 2. Gather workspace facts with the user (repos, harnesses, skills, identity signals, teammates, approved roots intent). Prefer asking over inventing.
102
+ 3. Write a concrete JSON object (do **not** include teammate \`role\` or \`scope.mode\`; keep \`scope.approved_roots\`).
103
+ 4. Show the JSON to the user and obtain explicit approval.
104
+ 5. Submit with the narrow CLI transport (not raw HTTP). Workspace id comes from the local binding created in step 2 (override with \`--workspace-id\` only if needed):
105
+
106
+ \`\`\`bash
107
+ dreams snapshots create --type init --file /path/to/approved-init.json --client-request-id <unique-id> --json
108
+ \`\`\`
109
+
110
+ Reuse the same \`client-request-id\` only when retrying the exact same approved payload. Never resubmit a new or edited snapshot without showing it and getting approval again. Snapshot submit stores the fact; it does not create the shared-memory repository.
69
111
 
70
- ## Follow the response
112
+ ## Step 4 — Install live sync hooks (Claude Code)
71
113
 
72
- 1. Parse the single JSON document from stdout.
73
- 2. Briefly explain \`agent_instructions\` to the user without pasting raw JSON.
74
- 3. Run the exact command in \`next_action.command\`.
75
- 4. Use the absolute \`cli_path\` from the response for later Dreams commands.
114
+ Wire Claude Code hooks so live turns wake \`dreams sync\` through the durable CLI path from onboard (\`cli_path\`). There is **no** \`dreams hooks install\` command — you edit \`~/.claude/settings.json\` using \`references/hooks-claude.md\`.
76
115
 
77
- When \`state\` is \`awaiting_auth\`, run the returned \`auth login --json\` command and wait for the user to approve in the browser. That login command emits JSON Lines: first \`awaiting_approval\`, then \`authenticated\`. After it succeeds, run the stable CLI's \`onboard --json\` command again.
116
+ **Consent first:** before editing global harness config, explicitly explain that this enables **ongoing transcript sync** on future Claude Code sessions, and obtain the user's approval. Do not install hooks without that consent. Hooks go in **before** root activation so live coverage has no setup gap.
78
117
 
79
- When \`state\` is \`ready_for_orient\`, run the returned \`orient\` command.
118
+ Then:
80
119
 
81
- Repeated \`onboard\` calls are safe. They repair missing local setup and return the same next action until authentication state changes.
120
+ 1. Read \`references/hooks-claude.md\`.
121
+ 2. Substitute the absolute \`cli_path\` from onboard into every hook command (quote the path).
122
+ 3. Choose the **platform-appropriate** command from the reference (POSIX vs PowerShell). Do not install \`>/dev/null\` redirects on Windows.
123
+ 4. Merge **idempotently** into \`~/.claude/settings.json\`: if a Dreams sync hook is already present (command contains \`# dreams-sync-hook\` or the same \`cli_path\` + \`sync\`), **update that handler in place** and leave unrelated hooks untouched — never append duplicates.
124
+ 5. Confirm through the durable launcher: \`"<cli_path>" sync status --json\` (hooks themselves should remain silent and exit 0). Read \`sync.effective_state\` (\`paused\` / \`running\` / \`pending\` / \`not_due\` / \`degraded\` / \`idle\`).
82
125
 
83
- Never print or summarize access tokens, refresh tokens, credential files, or keychain contents.
126
+ Rules:
127
+
128
+ - Call the durable \`cli_path\` with \`sync\` (pipe hook stdin). Never \`npx\`, never \`jq\`.
129
+ - Suppress stdout/stderr and force exit 0 so harnesses do not treat sync JSON as hook-control output.
130
+ - Install Claude Code \`SessionStart\` + \`Stop\` only. **Codex hooks are deferred** until Dreams has a Codex source adapter (today's sync/live hints are Claude-only).
131
+
132
+ ### Hook diagnostics & repair
133
+
134
+ When live sync seems broken, use this ladder (do **not** put version checks inside the hot hook command):
135
+
136
+ 1. Verify the durable launcher itself: \`"<cli_path>" status --json\` (expects a ready CLI + skill paths). A wrong or nonexistent path means Dreams never starts, so **no** sync diagnostics can be recorded — re-run \`npx @letta-ai/dreams@latest onboard --json\` and re-substitute the new absolute \`cli_path\` into hooks.
137
+ 2. Interpret local sync diagnostics: \`"<cli_path>" sync status --json\`
138
+ - \`effective_state: "paused"\` → expected no-op until \`sync resume\`
139
+ - \`not_due\` → cadence/backoff (including a retained trigger waiting for \`next_due_at\`); quiet hook wakes are expected
140
+ - \`pending\` → trigger is runnable now (forced, cadence due, or retry due) but no worker lease is held yet
141
+ - \`running\` → detached worker lease is active
142
+ - \`degraded\` → inspect \`last_error_code\` / \`last_error_message\` (structured \`spawn_failed\` when the parent could not detach a worker)
143
+ - \`idle\` → no pause, no pending trigger, and nothing waiting on cadence
144
+ 3. Silence from hooks is **required** (exit 0, no stdout/stderr). Silence means “don’t interrupt Claude,” **not** proof of a successful Cloud upload.
145
+
146
+ ## Step 5 — Activate live sync
147
+
148
+ With user consent for privacy roots:
149
+
150
+ 1. Approve privacy roots: \`"<cli_path>" source approve-root "<path>" --json\` (repeat as needed). Deny-by-default: nothing leaves the machine until roots are approved.
151
+ 2. Wake live-only scheduling through the durable launcher:
152
+ \`"<cli_path>" sync --force --json\`
153
+ This records a durable trigger and may spawn the detached worker. It does **not** run an unrestricted historical scan — live mode only touches already-tracked sessions plus a validated current-session hint from hooks. Do **not** run \`dreams source scan\` here (it performs unrestricted historical discovery under approved roots). Do **not** invoke \`dreams upload\` separately during activation — \`sync\` owns live-first upload ordering for whatever is already queued.
154
+ 3. Treat this wake as a **local scheduling check**. Step 2's \`source detect\` already exercised Cloud registration; the first real hook-driven upload (when the user has an active Claude session under an approved root) confirms the end-to-end path. Do not present \`sync --force\` as a Cloud smoke test — on a fresh device with nothing queued it may perform no network I/O.
155
+
156
+ ## Step 6 — Optional: import recent history in the background
157
+
158
+ Only if the user wants older transcripts included. Historical import is **optional**, runs in the **background**, and drains in **incremental bounded slices** across later \`dreams sync\` wakes (including SessionStart/Stop). It does **not** need to finish in one process — sleep, offline time, and restarts are fine; work resumes from durable local state. That steadiness is why a wider window is a reasonable choice: choose the history that is useful, not only what might finish tonight.
159
+
160
+ When offering this step, use calm language. Watching the first background sync settle is a reassuring sign that imports are moving; it is not a separate scary connection ritual.
161
+
162
+ If the user opts in:
163
+
164
+ 1. Agree a start bound. Describe it accurately as **sessions modified since \`<RFC3339>\` through now** (the CLI freezes \`until_at = now\`). Selection is by transcript-file mtime for whole sessions — not per-message timestamps inside a file.
165
+ 2. Obtain explicit consent to queue and upload those historical transcript bytes (approved roots + \`.dreamignore\` still apply). Do not start an open-ended “import everything” without a bound.
166
+ 3. Initialize the frozen window and queue one bounded local slice:
167
+ \`"<cli_path>" backfill start --since <RFC3339> --json\`
168
+ 4. Immediately wake the sync worker so draining does not wait for a future hook:
169
+ \`"<cli_path>" sync --force --json\`
170
+ 5. Explain what happens next: live turns stay first; remaining capacity steadily continues the historical frontier in the background (newer sessions in the window tend to be imported before older ones; bytes within a session still move forward). Users can pause or resume without discarding queued bytes or frontier progress:
171
+ \`"<cli_path>" sync pause\`
172
+ \`"<cli_path>" sync resume\`
173
+ \`"<cli_path>" sync status --json\` shows \`effective_state\` while import runs.
174
+
175
+ ## Re-entry
176
+
177
+ If unsure what is already done, inspect durable facts (\`dreams auth status\`, \`dreams source list\`, \`dreams status\`, \`dreams sync status\`) and continue at the first incomplete step. Repeated \`onboard\`, \`source detect\`, approved snapshot retries, and idempotent hook merges are safe when they follow the rules above. Never “catch up” by running unrestricted \`source scan\` after hooks are installed — use live sync and, if desired, bounded background backfill instead.
178
+ `;
179
+ }
180
+ function initSnapshotTemplate() {
181
+ return `${JSON.stringify({
182
+ $comment: "Non-enforced reference template for Dream init snapshots (LET-9833). Cloud accepts any JSON object + 256 KiB cap; this documents the expected Init fields for the skill/CLI. Do NOT include role on teammates, and do NOT include scope.mode — keep scope.approved_roots.",
183
+ repositories: [
184
+ {
185
+ display_name: "my-repo",
186
+ git_remote_sanitized: "github.com/acme/my-repo",
187
+ primary_language: "typescript",
188
+ build_system: "nx",
189
+ instruction_files: [{ name: "AGENTS.md", size_bytes: 1024 }],
190
+ skill_names: ["deploy"],
191
+ letta_agent_count: 2,
192
+ letta_memory_artifact_count: 0,
193
+ allowlisted_file_names: [],
194
+ },
195
+ ],
196
+ harnesses: [{ name: "claude-code", version: "1.2.3" }],
197
+ skills: [{ name: "deploy", description: "deploys things" }],
198
+ identity_signals: [{ type: "git_email", value: "dev@acme.com" }],
199
+ transcript_inventory: [{ source_key: "sess-1", session_count: 3, segment_count: 10 }],
200
+ teammates: [
201
+ {
202
+ display_name: "Alex",
203
+ identity_signals: [{ type: "git_email", value: "alex@acme.com" }],
204
+ },
205
+ ],
206
+ scope: { approved_roots: [] },
207
+ cli_version: "0.0.3",
208
+ }, null, 2)}\n`;
209
+ }
210
+ function claudeHooksReference() {
211
+ return `# Claude Code — Dreams live sync hooks
212
+
213
+ Install **globally** in \`~/.claude/settings.json\` (user scope). Merge into the existing \`hooks\` object; never replace the whole file or delete unrelated hook entries.
214
+
215
+ ## Goal
216
+
217
+ Wake the durable Dreams CLI on \`SessionStart\` (recover pending work after crashes/restarts) and \`Stop\` (after each live turn). Pipe the hook JSON stdin into \`dreams sync\` so the current \`session_id\` can be recorded as a live hint.
218
+
219
+ ## Platform-specific commands
220
+
221
+ Replace \`CLI_PATH\` with the absolute \`cli_path\` from \`dreams onboard --json\`.
222
+
223
+ ### macOS / Linux (bash)
224
+
225
+ Always quote the launcher path (it may contain spaces):
226
+
227
+ \`\`\`bash
228
+ "CLI_PATH" sync >/dev/null 2>&1 || true # dreams-sync-hook
229
+ \`\`\`
230
+
231
+ ### Windows (PowerShell)
232
+
233
+ Use Claude's \`shell: "powershell"\` so the command is not run through bash redirects:
234
+
235
+ \`\`\`powershell
236
+ & 'CLI_PATH' sync *> $null; exit 0 # dreams-sync-hook
237
+ \`\`\`
238
+
239
+ Rules:
240
+
241
+ - Pipe stdin from the harness into that command (Claude already provides JSON on stdin to command hooks).
242
+ - Do **not** use \`npx\`, \`jq\`, or shell JSON parsing.
243
+ - Suppress stdout/stderr and force exit 0. Sync JSON must not enter Claude context.
244
+ - Keep the \`# dreams-sync-hook\` marker. On re-run, **update** the existing Dreams handler in place (including switching POSIX ↔ PowerShell when the host OS differs) instead of appending another copy.
245
+
246
+ ## Example merge fragment (macOS / Linux)
247
+
248
+ \`\`\`json
249
+ {
250
+ "hooks": {
251
+ "SessionStart": [
252
+ {
253
+ "hooks": [
254
+ {
255
+ "type": "command",
256
+ "command": "\\"CLI_PATH\\" sync >/dev/null 2>&1 || true # dreams-sync-hook"
257
+ }
258
+ ]
259
+ }
260
+ ],
261
+ "Stop": [
262
+ {
263
+ "hooks": [
264
+ {
265
+ "type": "command",
266
+ "command": "\\"CLI_PATH\\" sync >/dev/null 2>&1 || true # dreams-sync-hook"
267
+ }
268
+ ]
269
+ }
270
+ ]
271
+ }
272
+ }
273
+ \`\`\`
274
+
275
+ ## Example merge fragment (Windows)
276
+
277
+ \`\`\`json
278
+ {
279
+ "hooks": {
280
+ "SessionStart": [
281
+ {
282
+ "hooks": [
283
+ {
284
+ "type": "command",
285
+ "shell": "powershell",
286
+ "command": "& 'CLI_PATH' sync *> $null; exit 0 # dreams-sync-hook"
287
+ }
288
+ ]
289
+ }
290
+ ],
291
+ "Stop": [
292
+ {
293
+ "hooks": [
294
+ {
295
+ "type": "command",
296
+ "shell": "powershell",
297
+ "command": "& 'CLI_PATH' sync *> $null; exit 0 # dreams-sync-hook"
298
+ }
299
+ ]
300
+ }
301
+ ]
302
+ }
303
+ }
304
+ \`\`\`
305
+
306
+ ## Idempotent merge checklist
307
+
308
+ 1. Obtain explicit user consent for ongoing transcript sync (see skill Step 4).
309
+ 2. Read \`~/.claude/settings.json\` (create \`{}\` if absent).
310
+ 3. Preserve every existing top-level key and every non-Dreams hook matcher/handler.
311
+ 4. For \`SessionStart\` and \`Stop\`, find a handler containing \`dreams-sync-hook\` (or the same \`CLI_PATH\` + \` sync\`) and update it in place; otherwise append one host-appropriate Dreams handler.
312
+ 5. Write the file back atomically.
313
+ 6. Optional check through the durable launcher:
314
+ - macOS / Linux: \`"CLI_PATH" sync status --json\`
315
+ - Windows PowerShell: \`& 'CLI_PATH' sync status --json\`
316
+
317
+ ## Notes
318
+
319
+ ### No-op contracts
320
+
321
+ - Hook commands must stay **silent** and **exit 0** (including when sync is paused or not due). That silence protects Claude’s context; it is **not** proof the Cloud upload succeeded.
322
+ - \`dreams sync pause\` / \`dreams sync resume\` toggle hook wakes without uninstalling handlers.
323
+ - Distinguish expected no-ops from failure with \`dreams sync status --json\`:
324
+ - \`effective_state: "paused"\` or \`"not_due"\` → expected quiet wake
325
+ - \`pending\` / \`running\` → runnable work recorded or worker in flight
326
+ - \`idle\` → nothing waiting
327
+ - \`degraded\` + structured \`last_error_code\` (e.g. \`spawn_failed\`) → failure
328
+
329
+ ### Robustness
330
+
331
+ - \`session_id\` in hook stdin is an **optional** live hint. Malformed/missing stdin must still issue a generic \`sync\` wake for tracked sessions (live mode — no silent historical ingest).
332
+ - Always quote the durable absolute launcher path, suppress stdout/stderr, and force exit 0.
333
+ - Preserve unrelated handlers; update the marked Dreams handler (\`# dreams-sync-hook\`) **in place** — never append duplicates.
334
+ - Do not put version checks in the hot hook path. If the launcher path is wrong/nonexistent, Dreams never starts and cannot record diagnostics — repair with \`dreams onboard --json\` / \`dreams status --json\` and re-substitute \`cli_path\`.
335
+
336
+ ### Other
337
+
338
+ - Users can pause hooks without uninstalling: \`dreams sync pause\` / \`dreams sync resume\`.
339
+ - Codex hook wiring is intentionally deferred until Dreams ships a Codex source adapter.
84
340
  `;
85
341
  }
86
342
  function readFile(file) {
@@ -127,12 +383,21 @@ function installedSkillVersion() {
127
383
  const match = text.match(/^version:\s*(\S+)\s*$/m);
128
384
  return match ? match[1] : null;
129
385
  }
386
+ function desiredArtifacts() {
387
+ return [
388
+ { path: skillFilePath(), contents: skillContent((0, version_1.packageVersion)()) },
389
+ { path: initSnapshotTemplatePath(), contents: initSnapshotTemplate() },
390
+ { path: claudeHooksReferencePath(), contents: claudeHooksReference() },
391
+ ];
392
+ }
130
393
  function installSkill() {
131
394
  removeLegacyGeneratedSkill();
132
- const desired = skillContent((0, version_1.packageVersion)());
133
- const existing = readFile(skillFilePath());
134
- if (existing === desired)
395
+ const artifacts = desiredArtifacts();
396
+ const existing = artifacts.map((artifact) => readFile(artifact.path));
397
+ const allSame = artifacts.every((artifact, index) => existing[index] === artifact.contents);
398
+ if (allSame)
135
399
  return "unchanged";
136
- writeAtomic(skillFilePath(), desired);
137
- return existing === null ? "installed" : "updated";
400
+ for (const artifact of artifacts)
401
+ writeAtomic(artifact.path, artifact.contents);
402
+ return existing[0] === null ? "installed" : "updated";
138
403
  }
@@ -0,0 +1,210 @@
1
+ "use strict";
2
+ /**
3
+ * `dreams snapshots create` — narrow CLI transport for skill-authored snapshots.
4
+ *
5
+ * Keeps credential handling, base URL selection, size checking, JSON-object
6
+ * validation, and idempotency keys inside the CLI. The skill authors the
7
+ * payload and obtains user approval before invoking this command.
8
+ *
9
+ * Workspace id is taken from the local binding written by `source detect`
10
+ * (or upload registration), matching the Cloud path
11
+ * POST /v1/dreams/workspaces/{workspaceId}/snapshots.
12
+ */
13
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
14
+ if (k2 === undefined) k2 = k;
15
+ var desc = Object.getOwnPropertyDescriptor(m, k);
16
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
17
+ desc = { enumerable: true, get: function() { return m[k]; } };
18
+ }
19
+ Object.defineProperty(o, k2, desc);
20
+ }) : (function(o, m, k, k2) {
21
+ if (k2 === undefined) k2 = k;
22
+ o[k2] = m[k];
23
+ }));
24
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
25
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
26
+ }) : function(o, v) {
27
+ o["default"] = v;
28
+ });
29
+ var __importStar = (this && this.__importStar) || (function () {
30
+ var ownKeys = function(o) {
31
+ ownKeys = Object.getOwnPropertyNames || function (o) {
32
+ var ar = [];
33
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
34
+ return ar;
35
+ };
36
+ return ownKeys(o);
37
+ };
38
+ return function (mod) {
39
+ if (mod && mod.__esModule) return mod;
40
+ var result = {};
41
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
42
+ __setModuleDefault(result, mod);
43
+ return result;
44
+ };
45
+ })();
46
+ Object.defineProperty(exports, "__esModule", { value: true });
47
+ exports.commandSnapshotsCreate = commandSnapshotsCreate;
48
+ exports.commandSnapshots = commandSnapshots;
49
+ const fs = __importStar(require("node:fs"));
50
+ const index_1 = require("./auth/index");
51
+ const client_1 = require("./cloud/client");
52
+ const db_1 = require("./local/db");
53
+ const state_lock_1 = require("./local/state-lock");
54
+ const store_1 = require("./store");
55
+ const MAX_SNAPSHOT_BYTES = 256 * 1024;
56
+ const MAX_TYPE_LEN = 64;
57
+ const MAX_CLIENT_REQUEST_ID_LEN = 128;
58
+ const TYPE_RE = /^[a-z][a-z0-9_-]*$/;
59
+ const CLIENT_REQUEST_ID_RE = /^[A-Za-z0-9._:-]+$/;
60
+ function emit(json, payload, human) {
61
+ if (json)
62
+ console.log(JSON.stringify(payload, null, 2));
63
+ else
64
+ console.log(human());
65
+ }
66
+ function validateType(type) {
67
+ if (!TYPE_RE.test(type) || type.length > MAX_TYPE_LEN) {
68
+ return `invalid --type (expected <=${MAX_TYPE_LEN} chars matching ${TYPE_RE})`;
69
+ }
70
+ return null;
71
+ }
72
+ function validateClientRequestId(id) {
73
+ if (!CLIENT_REQUEST_ID_RE.test(id) || id.length > MAX_CLIENT_REQUEST_ID_LEN) {
74
+ return `invalid --client-request-id (expected <=${MAX_CLIENT_REQUEST_ID_LEN} chars matching ${CLIENT_REQUEST_ID_RE})`;
75
+ }
76
+ return null;
77
+ }
78
+ function loadPayload(file) {
79
+ let raw;
80
+ try {
81
+ raw = fs.readFileSync(file, "utf8");
82
+ }
83
+ catch (error) {
84
+ return { ok: false, message: `could not read --file: ${error instanceof Error ? error.message : String(error)}` };
85
+ }
86
+ const bytes = Buffer.byteLength(raw, "utf8");
87
+ if (bytes > MAX_SNAPSHOT_BYTES) {
88
+ return { ok: false, message: `snapshot exceeds ${MAX_SNAPSHOT_BYTES} byte UTF-8 cap (${bytes} bytes)` };
89
+ }
90
+ let parsed;
91
+ try {
92
+ parsed = JSON.parse(raw);
93
+ }
94
+ catch (error) {
95
+ return { ok: false, message: `snapshot is not valid JSON: ${error instanceof Error ? error.message : String(error)}` };
96
+ }
97
+ if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
98
+ return { ok: false, message: "snapshot payload must be a JSON object" };
99
+ }
100
+ return { ok: true, data: parsed, bytes };
101
+ }
102
+ function resolveWorkspaceId(explicit) {
103
+ if (explicit)
104
+ return explicit;
105
+ const installation = (0, store_1.loadOrCreateInstallation)();
106
+ const db = (0, db_1.openDb)({
107
+ installationId: installation.installation_id,
108
+ installationCreatedAt: installation.created_at,
109
+ });
110
+ try {
111
+ const row = db.prepare("SELECT workspace_id FROM workspace_bindings WHERE id = 1").get();
112
+ return row?.workspace_id ?? null;
113
+ }
114
+ finally {
115
+ (0, db_1.closeDb)();
116
+ }
117
+ }
118
+ async function commandSnapshotsCreate(args) {
119
+ if (!args.type || !args.file || !args.clientRequestId) {
120
+ process.stderr.write("Usage: dreams snapshots create --type <type> --file <path> --client-request-id <id> [--workspace-id <id>] [--json]\n");
121
+ return 1;
122
+ }
123
+ const typeError = validateType(args.type);
124
+ if (typeError) {
125
+ emit(args.json, { status: "error", code: "invalid_type", message: typeError }, () => typeError);
126
+ return 1;
127
+ }
128
+ const idError = validateClientRequestId(args.clientRequestId);
129
+ if (idError) {
130
+ emit(args.json, { status: "error", code: "invalid_client_request_id", message: idError }, () => idError);
131
+ return 1;
132
+ }
133
+ const loaded = loadPayload(args.file);
134
+ if (!loaded.ok) {
135
+ emit(args.json, { status: "error", code: "invalid_payload", message: loaded.message }, () => loaded.message);
136
+ return 1;
137
+ }
138
+ const lock = (0, state_lock_1.acquireLocalStateLock)();
139
+ if (lock === null) {
140
+ const message = "Another Dreams command is using local state. Try again after it finishes.";
141
+ emit(args.json, { status: "error", code: "local_state_busy", message }, () => message);
142
+ return 1;
143
+ }
144
+ try {
145
+ const workspaceId = resolveWorkspaceId(args.workspaceId);
146
+ if (!workspaceId) {
147
+ const message = "no workspace binding found; run `dreams source detect` first (or pass --workspace-id)";
148
+ emit(args.json, { status: "error", code: "workspace_unresolved", message }, () => message);
149
+ return 1;
150
+ }
151
+ try {
152
+ const result = await (0, client_1.createSnapshot)({
153
+ workspaceId,
154
+ type: args.type,
155
+ clientRequestId: args.clientRequestId,
156
+ data: loaded.data,
157
+ });
158
+ emit(args.json, {
159
+ status: "ok",
160
+ id: result.id,
161
+ type: result.type,
162
+ workspace_id: result.workspaceId,
163
+ client_request_id: result.clientRequestId,
164
+ snapshot_status: result.status,
165
+ created_at: result.createdAt,
166
+ bytes: loaded.bytes,
167
+ }, () => [
168
+ "Snapshot stored",
169
+ "",
170
+ ` Id: ${result.id}`,
171
+ ` Type: ${result.type}`,
172
+ ` Workspace: ${result.workspaceId}`,
173
+ ` Client request id: ${result.clientRequestId}`,
174
+ ` Status: ${result.status}`,
175
+ ` Bytes: ${loaded.bytes}`,
176
+ ].join("\n"));
177
+ return 0;
178
+ }
179
+ catch (error) {
180
+ if (error instanceof index_1.NotAuthenticatedError) {
181
+ const message = "not authenticated: run `dreams auth login`";
182
+ emit(args.json, { status: "error", code: "not_authenticated", message }, () => message);
183
+ return 1;
184
+ }
185
+ if (error instanceof client_1.CloudAuthError) {
186
+ const message = `authentication failed: ${error.message}`;
187
+ emit(args.json, { status: "error", code: error.code ?? "auth_failed", message }, () => message);
188
+ return 1;
189
+ }
190
+ if (error instanceof client_1.CloudError) {
191
+ const message = error.message;
192
+ emit(args.json, { status: "error", code: error.retryable ? "retryable" : "cloud_error", message, retryable: error.retryable }, () => message);
193
+ return 1;
194
+ }
195
+ throw error;
196
+ }
197
+ }
198
+ finally {
199
+ (0, state_lock_1.releaseLocalStateLock)(lock);
200
+ }
201
+ }
202
+ async function commandSnapshots(subcommand, args) {
203
+ switch (subcommand) {
204
+ case "create":
205
+ return commandSnapshotsCreate(args);
206
+ default:
207
+ process.stderr.write(`Unknown snapshots subcommand: ${subcommand ?? "(none)"} — expected create\n`);
208
+ return 1;
209
+ }
210
+ }