@letta-ai/dreams 0.0.1 → 0.0.4

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.
@@ -38,28 +38,121 @@ async function getAuthHeader() {
38
38
  const { token } = await resolveAccessToken();
39
39
  return { authorization: `Bearer ${token}` };
40
40
  }
41
+ const HOOKS_NOTE = "Hooks invoke `dreams sync` with the coding-agent harness process environment. " +
42
+ "If that environment sets LETTA_API_KEY, it wins over keychain/file — verify the " +
43
+ "credential hooks will use (harness env), not only an interactive CLI shell.";
44
+ function maskedStored(credentials) {
45
+ if (!credentials) {
46
+ return { access_token: null, expires_at: null, has_refresh_token: false };
47
+ }
48
+ return {
49
+ access_token: credentials.access_token ? (0, credentials_1.maskToken)(credentials.access_token) : null,
50
+ expires_at: credentials.expires_at,
51
+ has_refresh_token: true,
52
+ };
53
+ }
54
+ function keychainReason(status, selected) {
55
+ if (status === "not_checked") {
56
+ return "not checked because DREAMS_CREDENTIALS_BACKEND=file";
57
+ }
58
+ if (status === "unavailable") {
59
+ return "OS keychain unavailable (@napi-rs/keyring missing or unreadable)";
60
+ }
61
+ if (status === "absent") {
62
+ return "no Dreams refresh token in OS keychain";
63
+ }
64
+ if (selected) {
65
+ return "selected: highest-precedence stored credential (no LETTA_API_KEY)";
66
+ }
67
+ return "present but shadowed by LETTA_API_KEY";
68
+ }
69
+ function fileReason(present, selected, keychainPresent, envPresent) {
70
+ if (!present)
71
+ return "no credentials file at ~/.dreams/credentials.json";
72
+ if (selected) {
73
+ return "selected: file fallback (no LETTA_API_KEY; keychain empty or not used)";
74
+ }
75
+ if (envPresent)
76
+ return "present but shadowed by LETTA_API_KEY";
77
+ if (keychainPresent)
78
+ return "present but shadowed by keychain for stored-credential resolution";
79
+ return "present but not selected";
80
+ }
41
81
  /** Snapshot of auth state for `dreams auth status` and `dreams status`. Never refreshes. */
42
82
  function authStatusInfo() {
43
83
  const device_id = (0, store_1.loadOrCreateInstallation)().installation_id;
44
84
  const envKey = process.env.LETTA_API_KEY;
45
- if (envKey) {
46
- return {
47
- authenticated: true,
85
+ const envPresent = typeof envKey === "string" && envKey.length > 0;
86
+ const probes = (0, credentials_1.probeStoredCredentialBackends)();
87
+ const keychainPresent = probes.keychain.status === "present";
88
+ const filePresent = probes.file.status === "present";
89
+ let active = null;
90
+ if (envPresent) {
91
+ active = {
48
92
  source: "env",
49
- access_token: (0, credentials_1.maskToken)(envKey),
50
- expires_at: null,
51
- device_id,
93
+ reason: "LETTA_API_KEY is set and takes highest precedence",
52
94
  };
53
95
  }
54
- const stored = (0, credentials_1.loadCredentials)();
55
- if (!stored) {
56
- return { authenticated: false, source: null, access_token: null, expires_at: null, device_id };
96
+ else if (keychainPresent) {
97
+ active = {
98
+ source: "keychain",
99
+ reason: "no LETTA_API_KEY; OS keychain has a Dreams refresh token",
100
+ };
101
+ }
102
+ else if (filePresent) {
103
+ active = {
104
+ source: "file",
105
+ reason: "no LETTA_API_KEY; credentials file present (keychain empty or unavailable)",
106
+ };
57
107
  }
108
+ const keychainMasked = maskedStored(probes.keychain.credentials);
109
+ const fileMasked = maskedStored(probes.file.credentials);
110
+ const credentials = [
111
+ {
112
+ source: "env",
113
+ present: envPresent,
114
+ selected: active?.source === "env",
115
+ access_token: envPresent && envKey ? (0, credentials_1.maskToken)(envKey) : null,
116
+ expires_at: null,
117
+ has_refresh_token: false,
118
+ reason: envPresent
119
+ ? "highest precedence; used for CLI Cloud calls in this process and any child that inherits this environment"
120
+ : "LETTA_API_KEY is unset in this process",
121
+ },
122
+ {
123
+ source: "keychain",
124
+ present: keychainPresent,
125
+ selected: active?.source === "keychain",
126
+ access_token: keychainMasked.access_token,
127
+ expires_at: keychainMasked.expires_at,
128
+ has_refresh_token: keychainMasked.has_refresh_token,
129
+ reason: keychainReason(probes.keychain.status, active?.source === "keychain"),
130
+ },
131
+ {
132
+ source: "file",
133
+ present: filePresent,
134
+ selected: active?.source === "file",
135
+ access_token: fileMasked.access_token,
136
+ expires_at: fileMasked.expires_at,
137
+ has_refresh_token: fileMasked.has_refresh_token,
138
+ reason: fileReason(filePresent, active?.source === "file", keychainPresent, envPresent),
139
+ },
140
+ ];
141
+ const winner = active
142
+ ? credentials.find((c) => c.source === active.source)
143
+ : null;
58
144
  return {
59
- authenticated: true,
60
- source: stored.source,
61
- access_token: stored.credentials.access_token ? (0, credentials_1.maskToken)(stored.credentials.access_token) : null,
62
- expires_at: stored.credentials.expires_at,
145
+ authenticated: active !== null,
146
+ source: active?.source ?? null,
147
+ access_token: winner?.access_token ?? null,
148
+ expires_at: winner?.expires_at ?? null,
63
149
  device_id,
150
+ active,
151
+ credentials,
152
+ hooks: {
153
+ inherits_process_env: true,
154
+ effective_source_in_this_process: active?.source ?? null,
155
+ note: HOOKS_NOTE,
156
+ },
64
157
  };
65
158
  }
package/dist/cli.js CHANGED
@@ -2,49 +2,158 @@
2
2
  /** @letta-ai/dreams command-line entry point. */
3
3
  Object.defineProperty(exports, "__esModule", { value: true });
4
4
  exports.main = main;
5
+ // Must be first: filters node:sqlite ExperimentalWarning before other imports load it.
6
+ require("./suppress-sqlite-warning");
5
7
  const index_1 = require("./auth/index");
6
8
  const commands_1 = require("./auth/commands");
9
+ const development_reset_1 = require("./development-reset");
7
10
  const commands_2 = require("./local/commands");
8
11
  const db_1 = require("./local/db");
12
+ const state_lock_1 = require("./local/state-lock");
13
+ const launcher_1 = require("./launcher");
9
14
  const managed_install_1 = require("./managed-install");
10
15
  const onboard_1 = require("./onboard");
16
+ const snapshots_1 = require("./snapshots");
11
17
  const skill_1 = require("./skill");
12
18
  const store_1 = require("./store");
19
+ const backfill_1 = require("./local/backfill");
20
+ const hooks_install_1 = require("./local/hooks-install");
21
+ const sync_1 = require("./sync");
13
22
  const version_1 = require("./version");
14
23
  const USAGE = `Usage: dreams <command> [options]
15
24
 
16
25
  Commands:
17
- onboard Install Dreams locally and return the next setup action
26
+ onboard Install Dreams locally and hand off to the setup skill
27
+ rollback Reactivate the previous installed CLI version
18
28
  status Show installation, skill, CLI, database, and auth state
19
29
  auth login Connect this device to Letta (OAuth device code flow)
20
30
  auth status Show authentication state (never prints tokens)
21
31
  auth logout Revoke Dreams' refresh token and clear stored credentials
22
- source scan Discover Claude Code sessions and build the local upload queue
23
- source approve-root <path> Approve a work root so its transcripts become eligible
24
- source list Show local sources, approved roots, and the pending queue
32
+ dev reset local Preview/reset local SQLite and blob state
33
+ dev reset identity Preview/reset local state, credentials, and device identity
34
+ source detect Detect a harness and register a detected Cloud source
35
+ source scan Discover sessions and build the local upload queue
36
+ source approve-root <path> Approve a work directory so its transcripts become eligible
37
+ source approve-dir <path> Alias for approve-root (preferred user-facing name)
38
+ source list Show local sources, approved directories, and the pending queue
39
+ snapshots create Submit a skill-authored snapshot JSON file to Cloud
40
+ hooks install Install or preview harness live-sync hooks (Claude/Codex)
41
+ wake SessionStart housekeeping + kick pending sync/backfill
42
+ sync Schedule a live-turn sync worker (Stop hook; then active backfill)
43
+ sync pause|resume|status Pause/resume hooks or show sync diagnostics
44
+ backfill start Queue a bounded historical backfill window (local only)
25
45
  upload Upload pending queued objects to Letta Cloud
26
46
 
27
47
  Options:
28
- --json Machine-readable JSON output
29
- --version Print the CLI version
30
- --help Show this help
48
+ --json Machine-readable JSON output
49
+ --confirm Apply a development reset preview
50
+ --dry-run Preview hooks install without writing files
51
+ --force Bypass sync cadence (never bypasses pause/privacy)
52
+ --source <type> Source adapter (claude-code default; codex) for sync/wake, source detect/scan, upload, backfill, hooks install
53
+ --since <RFC3339> Backfill window start (with backfill start)
54
+ --type <type> Snapshot type (with snapshots create)
55
+ --file <path> Snapshot JSON file (with snapshots create)
56
+ --client-request-id <id> Idempotency key (with snapshots create)
57
+ --workspace-id <id> Override workspace id (defaults to local binding)
58
+ --version Print the CLI version
59
+ --help Show this help
31
60
  `;
32
- const COMMANDS_WITH_SUBCOMMANDS = new Set(["auth", "source"]);
61
+ const COMMANDS_WITH_SUBCOMMANDS = new Set(["auth", "dev", "source", "snapshots", "sync", "backfill", "hooks"]);
33
62
  function parseArgs(argv) {
34
63
  const parsed = {
35
64
  positionals: [],
36
65
  json: false,
66
+ confirm: false,
67
+ dryRun: false,
37
68
  help: false,
38
69
  version: false,
70
+ force: false,
71
+ worker: false,
72
+ source: undefined,
73
+ since: undefined,
74
+ type: undefined,
75
+ file: undefined,
76
+ clientRequestId: undefined,
77
+ workspaceId: undefined,
39
78
  unknown: [],
40
79
  };
41
- for (const arg of argv) {
80
+ for (let i = 0; i < argv.length; i++) {
81
+ const arg = argv[i];
42
82
  if (arg === "--json")
43
83
  parsed.json = true;
84
+ else if (arg === "--confirm")
85
+ parsed.confirm = true;
86
+ else if (arg === "--dry-run")
87
+ parsed.dryRun = true;
88
+ else if (arg === "--force")
89
+ parsed.force = true;
90
+ else if (arg === "--worker")
91
+ parsed.worker = true;
92
+ else if (arg === "--source") {
93
+ const value = argv[++i];
94
+ if (value === undefined || value.trim() === "")
95
+ parsed.unknown.push("--source");
96
+ else
97
+ parsed.source = value;
98
+ }
99
+ else if (arg.startsWith("--source=")) {
100
+ const value = arg.slice("--source=".length);
101
+ if (value.trim() === "")
102
+ parsed.unknown.push("--source=");
103
+ else
104
+ parsed.source = value;
105
+ }
44
106
  else if (arg === "--help" || arg === "-h")
45
107
  parsed.help = true;
46
108
  else if (arg === "--version" || arg === "-v")
47
109
  parsed.version = true;
110
+ else if (arg === "--since") {
111
+ const value = argv[++i];
112
+ if (value === undefined)
113
+ parsed.unknown.push("--since");
114
+ else
115
+ parsed.since = value;
116
+ }
117
+ else if (arg.startsWith("--since="))
118
+ parsed.since = arg.slice("--since=".length);
119
+ else if (arg === "--type") {
120
+ const value = argv[++i];
121
+ if (value === undefined)
122
+ parsed.unknown.push("--type");
123
+ else
124
+ parsed.type = value;
125
+ }
126
+ else if (arg.startsWith("--type="))
127
+ parsed.type = arg.slice("--type=".length);
128
+ else if (arg === "--file") {
129
+ const value = argv[++i];
130
+ if (value === undefined)
131
+ parsed.unknown.push("--file");
132
+ else
133
+ parsed.file = value;
134
+ }
135
+ else if (arg.startsWith("--file="))
136
+ parsed.file = arg.slice("--file=".length);
137
+ else if (arg === "--client-request-id") {
138
+ const value = argv[++i];
139
+ if (value === undefined)
140
+ parsed.unknown.push("--client-request-id");
141
+ else
142
+ parsed.clientRequestId = value;
143
+ }
144
+ else if (arg.startsWith("--client-request-id=")) {
145
+ parsed.clientRequestId = arg.slice("--client-request-id=".length);
146
+ }
147
+ else if (arg === "--workspace-id") {
148
+ const value = argv[++i];
149
+ if (value === undefined)
150
+ parsed.unknown.push("--workspace-id");
151
+ else
152
+ parsed.workspaceId = value;
153
+ }
154
+ else if (arg.startsWith("--workspace-id=")) {
155
+ parsed.workspaceId = arg.slice("--workspace-id=".length);
156
+ }
48
157
  else if (arg.startsWith("-"))
49
158
  parsed.unknown.push(arg);
50
159
  else
@@ -52,14 +161,17 @@ function parseArgs(argv) {
52
161
  }
53
162
  return parsed;
54
163
  }
55
- function commandStatus(json) {
164
+ function commandStatusUnlocked(json) {
56
165
  const installation = (0, store_1.loadOrCreateInstallation)();
57
166
  const auth = (0, index_1.authStatusInfo)();
167
+ const activation = (0, launcher_1.readActivation)();
58
168
  const status = {
59
169
  installation_id: installation.installation_id,
60
170
  installation_created_at: installation.created_at,
61
171
  state: auth.authenticated ? "authenticated" : "unauthenticated",
62
172
  cli_path: (0, managed_install_1.stableCliPath)(),
173
+ active_cli_version: activation?.active_version ?? null,
174
+ previous_cli_version: activation?.previous_version ?? null,
63
175
  database_path: (0, db_1.dbFilePath)(),
64
176
  skill_version: (0, skill_1.installedSkillVersion)(),
65
177
  skill_path: (0, skill_1.skillFilePath)(),
@@ -72,7 +184,7 @@ function commandStatus(json) {
72
184
  else {
73
185
  const authLine = auth.authenticated
74
186
  ? `yes (via ${auth.source}, token ${auth.access_token ?? "pending refresh"}${auth.expires_at ? `, expires ${auth.expires_at}` : ""})`
75
- : "no (run `dreams onboard`)";
187
+ : "no (run `dreams auth login`)";
76
188
  console.log([
77
189
  "Dreams status",
78
190
  "",
@@ -80,6 +192,8 @@ function commandStatus(json) {
80
192
  ` Created at: ${status.installation_created_at}`,
81
193
  ` State: ${status.state}`,
82
194
  ` Durable CLI: ${status.cli_path}`,
195
+ ` Active version: ${status.active_cli_version ?? "(not activated)"}`,
196
+ ` Previous version:${status.previous_cli_version ? ` ${status.previous_cli_version}` : " (none)"}`,
83
197
  ` Skill installed: ${status.skill_version ? `v${status.skill_version} at ${status.skill_path}` : "no"}`,
84
198
  ` Authenticated: ${authLine}`,
85
199
  ` Database: ${status.database_path}`,
@@ -88,6 +202,23 @@ function commandStatus(json) {
88
202
  }
89
203
  return 0;
90
204
  }
205
+ async function commandStatus(json) {
206
+ const lock = await (0, state_lock_1.waitForLocalStateLock)();
207
+ if (lock === null) {
208
+ const message = "Another Dreams command is using local state. Try again after it finishes.";
209
+ if (json)
210
+ console.log(JSON.stringify({ status: "error", code: "local_state_busy", message }));
211
+ else
212
+ process.stderr.write(message + "\n");
213
+ return 1;
214
+ }
215
+ try {
216
+ return commandStatusUnlocked(json);
217
+ }
218
+ finally {
219
+ (0, state_lock_1.releaseLocalStateLock)(lock);
220
+ }
221
+ }
91
222
  function commandAuth(subcommand, json) {
92
223
  switch (subcommand) {
93
224
  case "login":
@@ -101,6 +232,17 @@ function commandAuth(subcommand, json) {
101
232
  return 1;
102
233
  }
103
234
  }
235
+ function commandDevelopment(positionals, confirmed, json) {
236
+ if (positionals[1] !== "reset") {
237
+ process.stderr.write(`Unknown dev subcommand: ${positionals[1] ?? "(none)"} — expected reset\n\n${USAGE}`);
238
+ return 1;
239
+ }
240
+ if (positionals.length > 3) {
241
+ process.stderr.write(`Unexpected argument(s): ${positionals.slice(3).join(", ")}\n\n${USAGE}`);
242
+ return 1;
243
+ }
244
+ return (0, development_reset_1.commandDevelopmentReset)(positionals[2], confirmed, json);
245
+ }
104
246
  async function runCli() {
105
247
  const args = parseArgs(process.argv.slice(2));
106
248
  const command = args.positionals[0];
@@ -113,6 +255,36 @@ async function runCli() {
113
255
  process.stdout.write(USAGE);
114
256
  return args.help ? 0 : 1;
115
257
  }
258
+ if (args.confirm && (command !== "dev" || subcommand !== "reset")) {
259
+ process.stderr.write(`--confirm is only valid with \`dreams dev reset\`\n\n${USAGE}`);
260
+ return 1;
261
+ }
262
+ if ((args.type || args.file || args.clientRequestId || args.workspaceId) && command !== "snapshots") {
263
+ process.stderr.write(`Snapshot options are only valid with \`dreams snapshots create\`\n\n${USAGE}`);
264
+ return 1;
265
+ }
266
+ if ((args.force || args.worker) && command !== "sync") {
267
+ process.stderr.write(`--force/--worker are only valid with \`dreams sync\`\n\n${USAGE}`);
268
+ return 1;
269
+ }
270
+ if (args.dryRun && command !== "hooks") {
271
+ process.stderr.write(`--dry-run is only valid with \`dreams hooks install\`\n\n${USAGE}`);
272
+ return 1;
273
+ }
274
+ if (args.source &&
275
+ command !== "sync" &&
276
+ command !== "wake" &&
277
+ command !== "upload" &&
278
+ command !== "hooks" &&
279
+ !(command === "source" && (subcommand === "detect" || subcommand === "scan")) &&
280
+ !(command === "backfill" && subcommand === "start")) {
281
+ process.stderr.write(`--source is only valid with \`dreams sync\`, \`dreams wake\`, \`dreams hooks install\`, \`dreams source detect\`, \`dreams source scan\`, \`dreams upload\`, or \`dreams backfill start\`\n\n${USAGE}`);
282
+ return 1;
283
+ }
284
+ if (args.since && command !== "backfill") {
285
+ process.stderr.write(`--since is only valid with \`dreams backfill start\`\n\n${USAGE}`);
286
+ return 1;
287
+ }
116
288
  if (args.unknown.length > 0) {
117
289
  process.stderr.write(`Unknown argument(s): ${args.unknown.join(", ")}\n\n${USAGE}`);
118
290
  return 1;
@@ -122,23 +294,77 @@ async function runCli() {
122
294
  process.stderr.write(`Unexpected argument(s): ${args.positionals.slice(1).join(", ")}\n\n${USAGE}`);
123
295
  return 1;
124
296
  }
297
+ if (command === "snapshots" && args.positionals.length > 2) {
298
+ process.stderr.write(`Unexpected argument(s): ${args.positionals.slice(2).join(", ")}\n\n${USAGE}`);
299
+ return 1;
300
+ }
301
+ if (command === "sync" && args.positionals.length > 2) {
302
+ process.stderr.write(`Unexpected argument(s): ${args.positionals.slice(2).join(", ")}\n\n${USAGE}`);
303
+ return 1;
304
+ }
305
+ if (command === "backfill" && args.positionals.length > 2) {
306
+ process.stderr.write(`Unexpected argument(s): ${args.positionals.slice(2).join(", ")}\n\n${USAGE}`);
307
+ return 1;
308
+ }
309
+ if (command === "hooks" && args.positionals.length > 2) {
310
+ process.stderr.write(`Unexpected argument(s): ${args.positionals.slice(2).join(", ")}\n\n${USAGE}`);
311
+ return 1;
312
+ }
313
+ if (command === "wake" && args.positionals.length > 1) {
314
+ process.stderr.write(`Unexpected argument(s): ${args.positionals.slice(1).join(", ")}\n\n${USAGE}`);
315
+ return 1;
316
+ }
125
317
  switch (command) {
126
318
  case "onboard":
127
319
  return (0, onboard_1.commandOnboard)(args.json);
320
+ case "rollback":
321
+ return (0, launcher_1.commandRollback)(args.json);
128
322
  case "status":
129
323
  return commandStatus(args.json);
130
324
  case "auth":
131
325
  return commandAuth(subcommand, args.json);
326
+ case "dev":
327
+ return commandDevelopment(args.positionals, args.confirm, args.json);
132
328
  case "source":
133
- return (0, commands_2.commandSource)(subcommand, args.positionals[2], args.json);
329
+ return (0, commands_2.commandSource)(subcommand, args.positionals[2], args.json, args.source);
330
+ case "snapshots":
331
+ return (0, snapshots_1.commandSnapshots)(subcommand, {
332
+ type: args.type,
333
+ file: args.file,
334
+ clientRequestId: args.clientRequestId,
335
+ workspaceId: args.workspaceId,
336
+ json: args.json,
337
+ });
338
+ case "hooks":
339
+ if (subcommand !== "install") {
340
+ process.stderr.write(`Unknown hooks subcommand: ${subcommand ?? "(none)"} — expected install\n\n${USAGE}`);
341
+ return 1;
342
+ }
343
+ return (0, hooks_install_1.commandHooksInstall)({ json: args.json, dryRun: args.dryRun, sourceType: args.source });
344
+ case "wake":
345
+ return (0, sync_1.commandWake)({ json: args.json, sourceType: args.source });
346
+ case "sync":
347
+ return (0, sync_1.commandSync)(subcommand, {
348
+ json: args.json,
349
+ force: args.force,
350
+ worker: args.worker,
351
+ sourceType: args.source,
352
+ });
353
+ case "backfill":
354
+ if (subcommand !== "start") {
355
+ process.stderr.write(`Unknown backfill subcommand: ${subcommand ?? "(none)"} — expected start\n\n${USAGE}`);
356
+ return 1;
357
+ }
358
+ return (0, backfill_1.commandBackfillStart)({ since: args.since, json: args.json, sourceType: args.source });
134
359
  case "upload":
135
- return (0, commands_2.commandUpload)(args.json);
360
+ return (0, commands_2.commandUpload)(args.json, args.source);
136
361
  default:
137
362
  process.stderr.write(`Unknown command: ${command}\n\n${USAGE}`);
138
363
  return 1;
139
364
  }
140
365
  }
141
366
  function main() {
367
+ (0, launcher_1.signalLauncherReady)();
142
368
  runCli().then((code) => process.exit(code), (error) => {
143
369
  process.stderr.write(`${error instanceof Error ? error.message : String(error)}\n`);
144
370
  process.exit(1);
@@ -42,8 +42,11 @@ var __importStar = (this && this.__importStar) || (function () {
42
42
  };
43
43
  })();
44
44
  Object.defineProperty(exports, "__esModule", { value: true });
45
- exports.CloudError = exports.CloudAuthError = void 0;
45
+ exports.CLOUD_UPLOAD_MAX_CONTENT_CHARS = exports.CloudError = exports.CloudAuthError = void 0;
46
+ exports.setCloudUploadMaxContentCharsForTests = setCloudUploadMaxContentCharsForTests;
47
+ exports.encodeUploadContent = encodeUploadContent;
46
48
  exports.registerSource = registerSource;
49
+ exports.createSnapshot = createSnapshot;
47
50
  exports.uploadObject = uploadObject;
48
51
  const zlib = __importStar(require("node:zlib"));
49
52
  const index_1 = require("../auth/index");
@@ -71,6 +74,33 @@ exports.CloudError = CloudError;
71
74
  // Comfortably below the source lease TTL (30s) so a stalled request cannot
72
75
  // outlive the lease and let another process reclaim it mid-flight.
73
76
  const REQUEST_TIMEOUT_MS = 20_000;
77
+ /**
78
+ * Cloud `createUploadBodySchema.content` max: base64 of gzip'd bytes (not the
79
+ * uncompressed transcript). Enforced locally so oversized payloads quarantine
80
+ * without a wasted round trip ([Amelia] two-limit model).
81
+ */
82
+ exports.CLOUD_UPLOAD_MAX_CONTENT_CHARS = 8 * 1024 * 1024;
83
+ let cloudUploadMaxContentCharsForTests = null;
84
+ /** Test-only override for the encoded upload size gate. Pass null to clear. */
85
+ function setCloudUploadMaxContentCharsForTests(value) {
86
+ cloudUploadMaxContentCharsForTests = value;
87
+ }
88
+ function cloudUploadMaxContentChars() {
89
+ return cloudUploadMaxContentCharsForTests ?? exports.CLOUD_UPLOAD_MAX_CONTENT_CHARS;
90
+ }
91
+ /** Gzip + base64 encode transcript bytes and enforce the Cloud content char cap. */
92
+ function encodeUploadContent(bytes, maxContentChars = cloudUploadMaxContentChars()) {
93
+ const content = zlib.gzipSync(bytes).toString("base64");
94
+ if (content.length > maxContentChars) {
95
+ return {
96
+ ok: false,
97
+ code: "payload_too_large",
98
+ message: `gzip+base64 upload content is ${content.length} chars; Cloud maximum is ${maxContentChars}`,
99
+ encodedLength: content.length,
100
+ };
101
+ }
102
+ return { ok: true, content, encodedLength: content.length };
103
+ }
74
104
  function errorDetail(error) {
75
105
  const cause = error.cause;
76
106
  return cause?.code || cause?.message || (error instanceof Error ? error.message : String(error));
@@ -144,7 +174,7 @@ function classifyAuthOrRetry(status, json, path) {
144
174
  * id, so it is also how the CLI resolves the workspace.
145
175
  */
146
176
  async function registerSource(input) {
147
- const r = await request("POST", "/v1/dreams/sources", {
177
+ const body = {
148
178
  source_type: input.sourceType,
149
179
  source_key: input.sourceKey,
150
180
  display_name: input.displayName,
@@ -152,7 +182,16 @@ async function registerSource(input) {
152
182
  adapter_version: input.adapterVersion,
153
183
  privacy_config_hash: input.privacyConfigHash ?? null,
154
184
  backfill_policy: input.backfillPolicy ?? null,
155
- });
185
+ };
186
+ if (input.status != null)
187
+ body.status = input.status;
188
+ if (input.cliVersion != null)
189
+ body.cli_version = input.cliVersion;
190
+ if (input.skillVersion != null)
191
+ body.skill_version = input.skillVersion;
192
+ if (input.platform != null)
193
+ body.platform = input.platform;
194
+ const r = await request("POST", "/v1/dreams/sources", body);
156
195
  if (r.status !== 200 && r.status !== 201)
157
196
  classifyAuthOrRetry(r.status, r.json, "sources");
158
197
  const id = r.json?.id;
@@ -164,14 +203,59 @@ async function registerSource(input) {
164
203
  workspaceId: typeof r.json?.workspace_id === "string" ? r.json.workspace_id : null,
165
204
  };
166
205
  }
206
+ /**
207
+ * Narrow transport for skill-authored snapshots ([11] / LET-9833).
208
+ * Validation beyond JSON-object + size belongs on the server.
209
+ *
210
+ * Cloud contract: POST /v1/dreams/workspaces/{workspaceId}/snapshots
211
+ * with body `{ type, data, client_request_id }`.
212
+ */
213
+ async function createSnapshot(input) {
214
+ const path = `/v1/dreams/workspaces/${encodeURIComponent(input.workspaceId)}/snapshots`;
215
+ const r = await request("POST", path, {
216
+ type: input.type,
217
+ client_request_id: input.clientRequestId,
218
+ data: input.data,
219
+ });
220
+ if (r.status !== 200 && r.status !== 201)
221
+ classifyAuthOrRetry(r.status, r.json, "snapshots");
222
+ const id = r.json?.id;
223
+ if (typeof id !== "string")
224
+ throw new CloudError("snapshot response missing id", { retryable: true });
225
+ return {
226
+ id,
227
+ type: typeof r.json?.type === "string" ? r.json.type : input.type,
228
+ workspaceId: input.workspaceId,
229
+ clientRequestId: typeof r.json?.client_request_id === "string" ? r.json.client_request_id : input.clientRequestId,
230
+ status: typeof r.json?.status === "string" ? r.json.status : "stored",
231
+ createdAt: typeof r.json?.created_at === "string" ? r.json.created_at : null,
232
+ };
233
+ }
167
234
  async function uploadObject(input) {
168
- const content = zlib.gzipSync(input.bytes).toString("base64");
235
+ // Prefer a preflight-encoded body; otherwise encode here (defense in depth).
236
+ const gated = input.encodedContent !== undefined
237
+ ? input.encodedContent.length > cloudUploadMaxContentChars()
238
+ ? {
239
+ ok: false,
240
+ code: "payload_too_large",
241
+ message: `gzip+base64 upload content is ${input.encodedContent.length} chars; Cloud maximum is ${cloudUploadMaxContentChars()}`,
242
+ encodedLength: input.encodedContent.length,
243
+ }
244
+ : {
245
+ ok: true,
246
+ content: input.encodedContent,
247
+ encodedLength: input.encodedContent.length,
248
+ }
249
+ : encodeUploadContent(input.bytes);
250
+ if (!gated.ok) {
251
+ return { kind: "quarantine", code: gated.code, message: gated.message };
252
+ }
169
253
  const r = await request("POST", "/v1/dreams/uploads", {
170
254
  source_id: input.sourceId,
171
255
  client_upload_id: input.clientUploadId,
172
256
  content_sha256: input.contentSha256,
173
257
  content_encoding: "gzip+base64",
174
- content,
258
+ content: gated.content,
175
259
  source_schema: input.sourceSchema,
176
260
  source_metadata: input.sourceMetadata,
177
261
  priority: input.priority,