@letta-ai/dreams 0.0.3 → 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.
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  /**
3
- * Codex source adapter — discovery and harness detection ([LET-10314]).
3
+ * Codex source adapter — discovery and harness detection ([LET-10314] / [LET-10317]).
4
4
  *
5
5
  * On-disk layout (fail closed when unrecognized):
6
6
  * ~/.codex/sessions/YYYY/MM/DD/rollout-<timestamp>-<sessionId>.jsonl
@@ -18,7 +18,8 @@
18
18
  * `codex-rs/hooks/schema/generated/session-start.command.input.schema.json`)
19
19
  * Pin: https://github.com/openai/codex/tree/fb6aad9ae34116128e537696c24e35fe6548e1c2
20
20
  *
21
- * Subagent rollouts are skipped. Paths stay private to this module.
21
+ * Subagent rollouts are skipped. Classification shares one path with discovery
22
+ * so status diagnostics and ingest agree. Paths stay private to this module.
22
23
  * Registration lives in `source-adapters.ts` (not the default).
23
24
  */
24
25
  var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
@@ -59,6 +60,8 @@ exports.codexSourceAdapter = exports.CODEX_DISPLAY_NAME = exports.CODEX_SOURCE_I
59
60
  exports.codexSessionsDir = codexSessionsDir;
60
61
  exports.validateCodexSessionId = validateCodexSessionId;
61
62
  exports.detectCodexHarness = detectCodexHarness;
63
+ exports.classifyCodexRollout = classifyCodexRollout;
64
+ exports.inspectCodexDiscovery = inspectCodexDiscovery;
62
65
  exports.discoverSessions = discoverSessions;
63
66
  const fs = __importStar(require("node:fs"));
64
67
  const os = __importStar(require("node:os"));
@@ -74,6 +77,9 @@ const SESSION_ID_RE = /^[A-Za-z0-9][A-Za-z0-9_-]{0,127}$/;
74
77
  const HEAD_READ_BYTES = 256 * 1024;
75
78
  /** Timestamp uses hyphens instead of colons: YYYY-MM-DDTHH-MM-SS. */
76
79
  const ROLLOUT_RE = /^rollout-\d{4}-\d{2}-\d{2}T\d{2}-\d{2}-\d{2}-([A-Za-z0-9][A-Za-z0-9_-]{0,127})\.jsonl$/;
80
+ /** Broader candidate gate for diagnostics — drifted names still count as candidates. */
81
+ const ROLLOUT_CANDIDATE_RE = /^rollout-.*\.jsonl$/i;
82
+ const MAX_EXAMPLES = 5;
77
83
  function codexSessionsDir() {
78
84
  return path.join(os.homedir(), ".codex", "sessions");
79
85
  }
@@ -116,27 +122,43 @@ function detectCodexHarness(home = os.homedir()) {
116
122
  }
117
123
  return { present: true, evidence };
118
124
  }
119
- function readFirstRecord(filePath) {
125
+ function readFirstRecordDetailed(filePath) {
120
126
  let fd;
121
127
  try {
122
128
  fd = fs.openSync(filePath, "r");
123
129
  const buf = Buffer.alloc(HEAD_READ_BYTES);
124
130
  const read = fs.readSync(fd, buf, 0, HEAD_READ_BYTES, 0);
131
+ if (read === 0)
132
+ return { ok: false, reason: "empty" };
125
133
  const head = buf.subarray(0, read);
126
134
  const nl = head.indexOf(0x0a);
127
- if (nl === -1)
128
- return null;
129
- const line = head.subarray(0, nl).toString("utf8").trim();
135
+ const lineBytes = nl === -1 ? head : head.subarray(0, nl);
136
+ const line = lineBytes.toString("utf8").trim();
130
137
  if (line === "")
131
- return null;
132
- return JSON.parse(line);
138
+ return { ok: false, reason: "empty" };
139
+ try {
140
+ const parsed = JSON.parse(line);
141
+ if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
142
+ return { ok: false, reason: "unsupported_metadata" };
143
+ }
144
+ return { ok: true, record: parsed };
145
+ }
146
+ catch {
147
+ return { ok: false, reason: "malformed_json" };
148
+ }
133
149
  }
134
150
  catch {
135
- return null;
151
+ return { ok: false, reason: "unreadable" };
136
152
  }
137
153
  finally {
138
- if (fd !== undefined)
139
- fs.closeSync(fd);
154
+ if (fd !== undefined) {
155
+ try {
156
+ fs.closeSync(fd);
157
+ }
158
+ catch {
159
+ // ignore
160
+ }
161
+ }
140
162
  }
141
163
  }
142
164
  function isSubagentMeta(payload) {
@@ -158,36 +180,119 @@ function statSignals(filePath) {
158
180
  const mtime = Number.isFinite(stat.mtimeMs) ? Math.floor(stat.mtimeMs) : null;
159
181
  return { size: stat.size, inode, birthtime_ms: birthtime, mtime_ms: mtime };
160
182
  }
183
+ function homeRelative(filePath, home = os.homedir()) {
184
+ const homeReal = (() => {
185
+ try {
186
+ return (0, config_1.canonicalize)(home);
187
+ }
188
+ catch {
189
+ return path.resolve(home);
190
+ }
191
+ })();
192
+ const fileReal = (() => {
193
+ try {
194
+ return (0, config_1.canonicalize)(filePath);
195
+ }
196
+ catch {
197
+ return path.resolve(filePath);
198
+ }
199
+ })();
200
+ if (fileReal === homeReal || fileReal.startsWith(homeReal + path.sep)) {
201
+ return path.join("~", path.relative(homeReal, fileReal));
202
+ }
203
+ return path.basename(filePath);
204
+ }
161
205
  /**
162
- * Enumerate top-level Codex rollout files. Malformed, truncated, escaped,
163
- * identity-mismatched, and subagent files are skipped (fail closed).
164
- *
165
- * Optional `sessionsDir` is Codex-private (tests / direct callers).
206
+ * Classify one rollout candidate. Shared by discovery ingest and status
207
+ * diagnostics a "supported" result is exactly what `discoverSessions` emits.
166
208
  */
167
- function discoverSessions(sessionsDir = codexSessionsDir()) {
168
- const sessions = [];
169
- let rootReal;
209
+ function classifyCodexRollout(filePath, filenameId, options) {
210
+ const basename = path.basename(filePath);
211
+ const relative_path = homeRelative(filePath, options.home);
212
+ if (!validateCodexSessionId(filenameId)) {
213
+ return { kind: "unsupported", reason: "invalid_session_id", basename, relative_path };
214
+ }
215
+ let realPath;
170
216
  try {
171
- rootReal = (0, config_1.canonicalize)(sessionsDir);
172
- if (!fs.statSync(rootReal).isDirectory())
173
- return sessions;
217
+ realPath = (0, config_1.canonicalize)(filePath);
174
218
  }
175
219
  catch {
176
- return sessions;
220
+ return { kind: "unsupported", reason: "unreadable", basename, relative_path };
221
+ }
222
+ if (!underRoot(options.sessionsRootReal, realPath)) {
223
+ return { kind: "excluded", reason: "escaped_path", basename, relative_path };
224
+ }
225
+ const first = readFirstRecordDetailed(filePath);
226
+ if (!first.ok) {
227
+ return { kind: "unsupported", reason: first.reason, basename, relative_path };
228
+ }
229
+ const record = first.record;
230
+ if (record.type !== "session_meta") {
231
+ return { kind: "unsupported", reason: "unsupported_metadata", basename, relative_path };
232
+ }
233
+ const payload = record.payload;
234
+ if (!payload || typeof payload !== "object" || Array.isArray(payload)) {
235
+ return { kind: "unsupported", reason: "unsupported_metadata", basename, relative_path };
236
+ }
237
+ const meta = payload;
238
+ if (isSubagentMeta(meta)) {
239
+ return { kind: "excluded", reason: "subagent", basename, relative_path };
240
+ }
241
+ const sessionId = typeof meta.id === "string" ? meta.id : typeof meta.session_id === "string" ? meta.session_id : null;
242
+ if (!sessionId) {
243
+ return { kind: "unsupported", reason: "unsupported_metadata", basename, relative_path };
244
+ }
245
+ if (sessionId !== filenameId) {
246
+ return { kind: "unsupported", reason: "session_id_mismatch", basename, relative_path };
177
247
  }
248
+ if (typeof meta.session_id === "string" && typeof meta.id === "string" && meta.session_id !== meta.id) {
249
+ return { kind: "unsupported", reason: "session_id_mismatch", basename, relative_path };
250
+ }
251
+ if (!validateCodexSessionId(sessionId)) {
252
+ return { kind: "unsupported", reason: "invalid_session_id", basename, relative_path };
253
+ }
254
+ let cwd = null;
255
+ if (typeof meta.cwd === "string") {
256
+ try {
257
+ cwd = (0, config_1.canonicalize)(meta.cwd);
258
+ }
259
+ catch {
260
+ cwd = null;
261
+ }
262
+ }
263
+ try {
264
+ const signals = statSignals(filePath);
265
+ return {
266
+ kind: "supported",
267
+ session: {
268
+ path: filePath,
269
+ real_path: realPath,
270
+ session_id: sessionId,
271
+ cwd,
272
+ size: signals.size,
273
+ inode: signals.inode,
274
+ birthtime_ms: signals.birthtime_ms,
275
+ mtime_ms: signals.mtime_ms,
276
+ first_chunk_fp: (0, transcript_bytes_1.firstChunkFingerprint)(filePath),
277
+ },
278
+ };
279
+ }
280
+ catch {
281
+ return { kind: "unsupported", reason: "unreadable", basename, relative_path };
282
+ }
283
+ }
284
+ function* walkRolloutCandidates(sessionsRootReal) {
178
285
  let yearEntries;
179
286
  try {
180
- yearEntries = fs.readdirSync(rootReal, { withFileTypes: true });
287
+ yearEntries = fs.readdirSync(sessionsRootReal, { withFileTypes: true });
181
288
  }
182
289
  catch {
183
- return sessions;
290
+ return;
184
291
  }
185
292
  for (const yearEntry of yearEntries) {
186
- if (!yearEntry.isDirectory())
187
- continue;
188
- if (!/^\d{4}$/.test(yearEntry.name))
293
+ if (!yearEntry.isDirectory() || !/^\d{4}$/.test(yearEntry.name))
189
294
  continue;
190
- const yearDir = path.join(rootReal, yearEntry.name);
295
+ const yearDir = path.join(sessionsRootReal, yearEntry.name);
191
296
  let monthEntries;
192
297
  try {
193
298
  monthEntries = fs.readdirSync(yearDir, { withFileTypes: true });
@@ -196,9 +301,7 @@ function discoverSessions(sessionsDir = codexSessionsDir()) {
196
301
  continue;
197
302
  }
198
303
  for (const monthEntry of monthEntries) {
199
- if (!monthEntry.isDirectory())
200
- continue;
201
- if (!/^\d{2}$/.test(monthEntry.name))
304
+ if (!monthEntry.isDirectory() || !/^\d{2}$/.test(monthEntry.name))
202
305
  continue;
203
306
  const monthDir = path.join(yearDir, monthEntry.name);
204
307
  let dayEntries;
@@ -209,9 +312,7 @@ function discoverSessions(sessionsDir = codexSessionsDir()) {
209
312
  continue;
210
313
  }
211
314
  for (const dayEntry of dayEntries) {
212
- if (!dayEntry.isDirectory())
213
- continue;
214
- if (!/^\d{2}$/.test(dayEntry.name))
315
+ if (!dayEntry.isDirectory() || !/^\d{2}$/.test(dayEntry.name))
215
316
  continue;
216
317
  const dayDir = path.join(monthDir, dayEntry.name);
217
318
  let files;
@@ -224,80 +325,176 @@ function discoverSessions(sessionsDir = codexSessionsDir()) {
224
325
  for (const file of files) {
225
326
  if (!file.isFile())
226
327
  continue;
227
- const match = ROLLOUT_RE.exec(file.name);
228
- if (!match)
229
- continue;
230
- const filenameId = match[1];
231
- if (!validateCodexSessionId(filenameId))
328
+ if (!ROLLOUT_CANDIDATE_RE.test(file.name))
232
329
  continue;
233
- const filePath = path.join(dayDir, file.name);
234
- try {
235
- let realPath;
236
- try {
237
- realPath = (0, config_1.canonicalize)(filePath);
238
- }
239
- catch {
240
- continue;
241
- }
242
- if (!underRoot(rootReal, realPath))
243
- continue;
244
- const record = readFirstRecord(filePath);
245
- if (!record)
246
- continue;
247
- if (record.type !== "session_meta")
248
- continue;
249
- const payload = record.payload;
250
- if (!payload || typeof payload !== "object" || Array.isArray(payload))
251
- continue;
252
- const meta = payload;
253
- if (isSubagentMeta(meta))
254
- continue;
255
- // Prefer payload.id; accept payload.session_id when present (same ThreadId).
256
- const sessionId = typeof meta.id === "string"
257
- ? meta.id
258
- : typeof meta.session_id === "string"
259
- ? meta.session_id
260
- : null;
261
- if (!sessionId || sessionId !== filenameId)
262
- continue;
263
- if (typeof meta.session_id === "string" &&
264
- typeof meta.id === "string" &&
265
- meta.session_id !== meta.id) {
266
- continue;
267
- }
268
- if (!validateCodexSessionId(sessionId))
269
- continue;
270
- let cwd = null;
271
- if (typeof meta.cwd === "string") {
272
- try {
273
- cwd = (0, config_1.canonicalize)(meta.cwd);
274
- }
275
- catch {
276
- // Unresolvable cwd → discover but deny-by-default at policy gate.
277
- cwd = null;
278
- }
279
- }
280
- const signals = statSignals(filePath);
281
- sessions.push({
282
- path: filePath,
283
- real_path: realPath,
284
- session_id: sessionId,
285
- cwd,
286
- size: signals.size,
287
- inode: signals.inode,
288
- birthtime_ms: signals.birthtime_ms,
289
- mtime_ms: signals.mtime_ms,
290
- first_chunk_fp: (0, transcript_bytes_1.firstChunkFingerprint)(filePath),
291
- });
330
+ const match = ROLLOUT_RE.exec(file.name);
331
+ if (match) {
332
+ yield { filePath: path.join(dayDir, file.name), filenameId: match[1], supportedName: true };
292
333
  }
293
- catch {
294
- // Fail closed for this candidate only never abort the whole pass.
295
- continue;
334
+ else {
335
+ yield { filePath: path.join(dayDir, file.name), filenameId: null, supportedName: false };
296
336
  }
297
337
  }
298
338
  }
299
339
  }
300
340
  }
341
+ }
342
+ function actionableDriftMessage() {
343
+ return [
344
+ "Codex rollout files were found but none use a supported format.",
345
+ "Repair/upgrade with `npx @letta-ai/dreams@latest onboard --json`, then rerun `dreams sync status --source codex --json`.",
346
+ "If still unsupported, preserve a sample rollout basename and report the new format.",
347
+ ].join(" ");
348
+ }
349
+ /**
350
+ * Walk Codex rollout candidates and classify each. Never throws for bad files.
351
+ */
352
+ function inspectCodexDiscovery(sessionsDir = codexSessionsDir(), home = os.homedir()) {
353
+ const harness = detectCodexHarness(home);
354
+ const by_reason = {};
355
+ const examples = [];
356
+ let candidates = 0;
357
+ let supported = 0;
358
+ let unsupported = 0;
359
+ let excluded_subagent = 0;
360
+ let excluded_other = 0;
361
+ if (!harness.present) {
362
+ return {
363
+ harness_present: false,
364
+ sessions_dir_present: false,
365
+ candidates: 0,
366
+ supported: 0,
367
+ unsupported: 0,
368
+ excluded_subagent: 0,
369
+ excluded_other: 0,
370
+ by_reason: {},
371
+ examples: [],
372
+ status: "absent",
373
+ message: "Codex was not detected under ~/.codex",
374
+ };
375
+ }
376
+ let sessionsRootReal;
377
+ let sessionsDirPresent = false;
378
+ try {
379
+ sessionsRootReal = (0, config_1.canonicalize)(sessionsDir);
380
+ sessionsDirPresent = fs.statSync(sessionsRootReal).isDirectory();
381
+ }
382
+ catch {
383
+ return {
384
+ harness_present: true,
385
+ sessions_dir_present: false,
386
+ candidates: 0,
387
+ supported: 0,
388
+ unsupported: 0,
389
+ excluded_subagent: 0,
390
+ excluded_other: 0,
391
+ by_reason: {},
392
+ examples: [],
393
+ status: "empty",
394
+ message: "Codex is present but no sessions directory was found",
395
+ };
396
+ }
397
+ if (!sessionsDirPresent) {
398
+ return {
399
+ harness_present: true,
400
+ sessions_dir_present: false,
401
+ candidates: 0,
402
+ supported: 0,
403
+ unsupported: 0,
404
+ excluded_subagent: 0,
405
+ excluded_other: 0,
406
+ by_reason: {},
407
+ examples: [],
408
+ status: "empty",
409
+ message: "Codex is present but no sessions directory was found",
410
+ };
411
+ }
412
+ for (const { filePath, filenameId, supportedName } of walkRolloutCandidates(sessionsRootReal)) {
413
+ candidates++;
414
+ const result = supportedName
415
+ ? classifyCodexRollout(filePath, filenameId, { sessionsRootReal, home })
416
+ : {
417
+ kind: "unsupported",
418
+ reason: "invalid_session_id",
419
+ basename: path.basename(filePath),
420
+ relative_path: homeRelative(filePath, home),
421
+ };
422
+ if (result.kind === "supported") {
423
+ supported++;
424
+ continue;
425
+ }
426
+ if (result.kind === "excluded") {
427
+ if (result.reason === "subagent")
428
+ excluded_subagent++;
429
+ else
430
+ excluded_other++;
431
+ continue;
432
+ }
433
+ unsupported++;
434
+ by_reason[result.reason] = (by_reason[result.reason] ?? 0) + 1;
435
+ if (examples.length < MAX_EXAMPLES) {
436
+ examples.push({
437
+ reason: result.reason,
438
+ basename: result.basename,
439
+ relative_path: result.relative_path,
440
+ });
441
+ }
442
+ }
443
+ let status = "ok";
444
+ let message = null;
445
+ if (candidates === 0) {
446
+ status = "empty";
447
+ message = "Codex is present but no rollout files were found";
448
+ }
449
+ else if (supported === 0 && unsupported > 0) {
450
+ status = "warning";
451
+ message = actionableDriftMessage();
452
+ }
453
+ else if (supported > 0 && unsupported > 0) {
454
+ status = "warning";
455
+ message =
456
+ "Some Codex rollout files use an unsupported format and were skipped. " +
457
+ "Rerun `dreams sync status --source codex --json` after upgrading Dreams; " +
458
+ "supported sessions continue to sync.";
459
+ }
460
+ return {
461
+ harness_present: true,
462
+ sessions_dir_present: true,
463
+ candidates,
464
+ supported,
465
+ unsupported,
466
+ excluded_subagent,
467
+ excluded_other,
468
+ by_reason,
469
+ examples,
470
+ status,
471
+ message,
472
+ };
473
+ }
474
+ /**
475
+ * Enumerate top-level Codex rollout files. Malformed, truncated, escaped,
476
+ * identity-mismatched, and subagent files are skipped (fail closed).
477
+ *
478
+ * Optional `sessionsDir` is Codex-private (tests / direct callers).
479
+ */
480
+ function discoverSessions(sessionsDir = codexSessionsDir()) {
481
+ const sessions = [];
482
+ let rootReal;
483
+ try {
484
+ rootReal = (0, config_1.canonicalize)(sessionsDir);
485
+ if (!fs.statSync(rootReal).isDirectory())
486
+ return sessions;
487
+ }
488
+ catch {
489
+ return sessions;
490
+ }
491
+ for (const { filePath, filenameId, supportedName } of walkRolloutCandidates(rootReal)) {
492
+ if (!supportedName || filenameId === null)
493
+ continue;
494
+ const result = classifyCodexRollout(filePath, filenameId, { sessionsRootReal: rootReal });
495
+ if (result.kind === "supported")
496
+ sessions.push(result.session);
497
+ }
301
498
  sessions.sort((a, b) => a.path.localeCompare(b.path));
302
499
  return sessions;
303
500
  }
@@ -51,7 +51,7 @@ function maxRecordBytesOverride() {
51
51
  const value = Number(raw);
52
52
  return Number.isSafeInteger(value) && value > 0 ? value : undefined;
53
53
  }
54
- function commandSourceScanUnlocked(json) {
54
+ function commandSourceScanUnlocked(json, adapter) {
55
55
  const { db, installationId } = open();
56
56
  try {
57
57
  const owner = (0, leases_1.acquireLease)(db, leases_1.SOURCE_LEASE);
@@ -64,7 +64,7 @@ function commandSourceScanUnlocked(json) {
64
64
  summary = (0, scan_1.reconcile)(db, {
65
65
  installationId,
66
66
  leaseOwner: owner,
67
- adapter: (0, source_adapters_1.defaultSourceAdapter)(),
67
+ adapter,
68
68
  maxSegmentBytes: maxSegmentBytesOverride(),
69
69
  maxRecordBytes: maxRecordBytesOverride(),
70
70
  });
@@ -90,7 +90,7 @@ function commandSourceScanUnlocked(json) {
90
90
  (0, db_1.closeDb)();
91
91
  }
92
92
  }
93
- async function commandUploadUnlocked(json) {
93
+ async function commandUploadUnlocked(json, adapter) {
94
94
  const { db, installationId } = open();
95
95
  try {
96
96
  const owner = (0, leases_1.acquireLease)(db, leases_1.SOURCE_LEASE);
@@ -103,7 +103,7 @@ async function commandUploadUnlocked(json) {
103
103
  summary = await (0, upload_1.runUpload)(db, {
104
104
  installationId,
105
105
  leaseOwner: owner,
106
- adapter: (0, source_adapters_1.defaultSourceAdapter)(),
106
+ adapter,
107
107
  });
108
108
  }
109
109
  finally {
@@ -132,13 +132,13 @@ async function commandUploadUnlocked(json) {
132
132
  }
133
133
  function commandSourceApproveRootUnlocked(json, root) {
134
134
  if (!root) {
135
- process.stderr.write("Usage: dreams source approve-root <path>\n");
135
+ process.stderr.write("Usage: dreams source approve-dir <path>\n");
136
136
  return 1;
137
137
  }
138
138
  const { db } = open();
139
139
  try {
140
140
  const roots = (0, config_1.approveRoot)(db, root);
141
- emit(json, { status: "ok", approved_roots: roots }, () => `Approved roots:\n${roots.map((r) => ` ${r}`).join("\n") || " (none)"}`);
141
+ emit(json, { status: "ok", approved_roots: roots, approved_directories: roots }, () => `Approved directories:\n${roots.map((r) => ` ${r}`).join("\n") || " (none)"}`);
142
142
  return 0;
143
143
  }
144
144
  finally {
@@ -153,13 +153,20 @@ function commandSourceListUnlocked(json) {
153
153
  .prepare("SELECT id, source_type, source_key, display_name, status, server_source_id FROM sources ORDER BY id")
154
154
  .all();
155
155
  const queue = (0, scan_1.listPendingQueue)(db);
156
- emit(json, { status: "ok", db_path: (0, db_1.dbFilePath)(), approved_roots: roots, sources, pending_uploads: queue }, () => [
156
+ emit(json, {
157
+ status: "ok",
158
+ db_path: (0, db_1.dbFilePath)(),
159
+ approved_roots: roots,
160
+ approved_directories: roots,
161
+ sources,
162
+ pending_uploads: queue,
163
+ }, () => [
157
164
  `Dream local sources`,
158
165
  ``,
159
- ` DB: ${(0, db_1.dbFilePath)()}`,
160
- ` Approved roots: ${roots.length ? roots.join(", ") : "(none — deny by default)"}`,
161
- ` Sources: ${sources.length}`,
162
- ` Pending uploads: ${queue.length}`,
166
+ ` DB: ${(0, db_1.dbFilePath)()}`,
167
+ ` Approved directories: ${roots.length ? roots.join(", ") : "(none — deny by default)"}`,
168
+ ` Sources: ${sources.length}`,
169
+ ` Pending uploads: ${queue.length}`,
163
170
  ].join("\n"));
164
171
  return 0;
165
172
  }
@@ -167,12 +174,12 @@ function commandSourceListUnlocked(json) {
167
174
  (0, db_1.closeDb)();
168
175
  }
169
176
  }
170
- async function commandSourceDetectUnlocked(json) {
177
+ async function commandSourceDetectUnlocked(json, adapter) {
171
178
  const { db, installationId } = open();
172
179
  try {
173
180
  const owner = (0, leases_1.acquireLease)(db, leases_1.SOURCE_LEASE);
174
181
  if (owner === null) {
175
- emit(json, { status: "busy", reason: "another source operation holds the local lease" }, () => "Another dreams source operation is in progress; skipping.");
182
+ emit(json, { status: "busy", reason: "another source operation holds the local lease" }, () => "Another Dreams source operation is in progress; skipping.");
176
183
  return 0;
177
184
  }
178
185
  let result;
@@ -180,7 +187,7 @@ async function commandSourceDetectUnlocked(json) {
180
187
  result = await (0, detect_1.runSourceDetect)(db, {
181
188
  installationId,
182
189
  leaseOwner: owner,
183
- adapter: (0, source_adapters_1.defaultSourceAdapter)(),
190
+ adapter,
184
191
  });
185
192
  }
186
193
  finally {
@@ -207,23 +214,25 @@ function stateBusy(json) {
207
214
  emit(json, { status: "busy", reason: "another Dreams command holds the local state lock" }, () => "Another Dreams command is using local state; skipping.");
208
215
  return 0;
209
216
  }
210
- function commandSourceScan(json) {
217
+ function commandSourceScan(json, sourceType) {
218
+ const adapter = (0, source_adapters_1.resolveSourceAdapter)(sourceType);
211
219
  const lock = (0, state_lock_1.acquireLocalStateLock)();
212
220
  if (lock === null)
213
221
  return stateBusy(json);
214
222
  try {
215
- return commandSourceScanUnlocked(json);
223
+ return commandSourceScanUnlocked(json, adapter);
216
224
  }
217
225
  finally {
218
226
  (0, state_lock_1.releaseLocalStateLock)(lock);
219
227
  }
220
228
  }
221
- async function commandUpload(json) {
229
+ async function commandUpload(json, sourceType) {
230
+ const adapter = (0, source_adapters_1.resolveSourceAdapter)(sourceType);
222
231
  const lock = (0, state_lock_1.acquireLocalStateLock)();
223
232
  if (lock === null)
224
233
  return stateBusy(json);
225
234
  try {
226
- return await commandUploadUnlocked(json);
235
+ return await commandUploadUnlocked(json, adapter);
227
236
  }
228
237
  finally {
229
238
  (0, state_lock_1.releaseLocalStateLock)(lock);
@@ -251,29 +260,35 @@ function commandSourceList(json) {
251
260
  (0, state_lock_1.releaseLocalStateLock)(lock);
252
261
  }
253
262
  }
254
- async function commandSourceDetect(json) {
263
+ async function commandSourceDetect(json, sourceType) {
264
+ const adapter = (0, source_adapters_1.resolveSourceAdapter)(sourceType);
255
265
  const lock = (0, state_lock_1.acquireLocalStateLock)();
256
266
  if (lock === null)
257
267
  return stateBusy(json);
258
268
  try {
259
- return await commandSourceDetectUnlocked(json);
269
+ return await commandSourceDetectUnlocked(json, adapter);
260
270
  }
261
271
  finally {
262
272
  (0, state_lock_1.releaseLocalStateLock)(lock);
263
273
  }
264
274
  }
265
- function commandSource(subcommand, positional, json) {
275
+ function commandSource(subcommand, positional, json, sourceType) {
276
+ if (sourceType !== undefined && subcommand !== "detect" && subcommand !== "scan") {
277
+ process.stderr.write(`--source is only valid with \`dreams source detect\` or \`dreams source scan\`\n`);
278
+ return 1;
279
+ }
266
280
  switch (subcommand) {
267
281
  case "detect":
268
- return commandSourceDetect(json);
282
+ return commandSourceDetect(json, sourceType);
269
283
  case "scan":
270
- return commandSourceScan(json);
284
+ return commandSourceScan(json, sourceType);
271
285
  case "approve-root":
286
+ case "approve-dir":
272
287
  return commandSourceApproveRoot(json, positional);
273
288
  case "list":
274
289
  return commandSourceList(json);
275
290
  default:
276
- process.stderr.write(`Unknown source subcommand: ${subcommand ?? "(none)"} — expected detect, scan, approve-root, or list\n`);
291
+ process.stderr.write(`Unknown source subcommand: ${subcommand ?? "(none)"} — expected detect, scan, approve-root, approve-dir, or list\n`);
277
292
  return 1;
278
293
  }
279
294
  }
package/dist/local/db.js CHANGED
@@ -252,6 +252,10 @@ const MIGRATIONS = [
252
252
  state = COALESCE((SELECT state FROM sync_runtime WHERE id = 1), 'idle')
253
253
  WHERE source_id = 'src_claude-code'`,
254
254
  ],
255
+ [
256
+ // Track which source currently owns the singleton worker ([LET-10317]).
257
+ `ALTER TABLE sync_runtime ADD COLUMN active_source_id TEXT`,
258
+ ],
255
259
  ];
256
260
  exports.SCHEMA_VERSION = MIGRATIONS.length;
257
261
  function tx(db, fn) {