@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.
@@ -10,13 +10,17 @@ exports.commandSourceScan = commandSourceScan;
10
10
  exports.commandUpload = commandUpload;
11
11
  exports.commandSourceApproveRoot = commandSourceApproveRoot;
12
12
  exports.commandSourceList = commandSourceList;
13
+ exports.commandSourceDetect = commandSourceDetect;
13
14
  exports.commandSource = commandSource;
14
15
  const store_1 = require("../store");
16
+ const upload_1 = require("../cloud/upload");
15
17
  const config_1 = require("./config");
16
18
  const db_1 = require("./db");
19
+ const detect_1 = require("./detect");
17
20
  const leases_1 = require("./leases");
18
21
  const scan_1 = require("./scan");
19
- const upload_1 = require("../cloud/upload");
22
+ const source_adapters_1 = require("./source-adapters");
23
+ const state_lock_1 = require("./state-lock");
20
24
  function open() {
21
25
  const installation = (0, store_1.loadOrCreateInstallation)();
22
26
  const db = (0, db_1.openDb)({
@@ -31,7 +35,7 @@ function emit(json, payload, human) {
31
35
  else
32
36
  console.log(human());
33
37
  }
34
- /** Optional override for the per-segment byte cap (advanced/testing); ignored if not a positive integer. */
38
+ /** Optional override for the soft segment packing target (advanced/testing). */
35
39
  function maxSegmentBytesOverride() {
36
40
  const raw = process.env.DREAMS_MAX_SEGMENT_BYTES;
37
41
  if (!raw)
@@ -39,7 +43,15 @@ function maxSegmentBytesOverride() {
39
43
  const value = Number(raw);
40
44
  return Number.isSafeInteger(value) && value > 0 ? value : undefined;
41
45
  }
42
- function commandSourceScan(json) {
46
+ /** Optional override for the hard per-record ceiling (advanced/testing). */
47
+ function maxRecordBytesOverride() {
48
+ const raw = process.env.DREAMS_MAX_RECORD_BYTES;
49
+ if (!raw)
50
+ return undefined;
51
+ const value = Number(raw);
52
+ return Number.isSafeInteger(value) && value > 0 ? value : undefined;
53
+ }
54
+ function commandSourceScanUnlocked(json, adapter) {
43
55
  const { db, installationId } = open();
44
56
  try {
45
57
  const owner = (0, leases_1.acquireLease)(db, leases_1.SOURCE_LEASE);
@@ -49,7 +61,13 @@ function commandSourceScan(json) {
49
61
  }
50
62
  let summary;
51
63
  try {
52
- summary = (0, scan_1.reconcile)(db, { installationId, leaseOwner: owner, maxSegmentBytes: maxSegmentBytesOverride() });
64
+ summary = (0, scan_1.reconcile)(db, {
65
+ installationId,
66
+ leaseOwner: owner,
67
+ adapter,
68
+ maxSegmentBytes: maxSegmentBytesOverride(),
69
+ maxRecordBytes: maxRecordBytesOverride(),
70
+ });
53
71
  }
54
72
  finally {
55
73
  (0, leases_1.releaseLease)(db, leases_1.SOURCE_LEASE, owner);
@@ -72,7 +90,7 @@ function commandSourceScan(json) {
72
90
  (0, db_1.closeDb)();
73
91
  }
74
92
  }
75
- async function commandUpload(json) {
93
+ async function commandUploadUnlocked(json, adapter) {
76
94
  const { db, installationId } = open();
77
95
  try {
78
96
  const owner = (0, leases_1.acquireLease)(db, leases_1.SOURCE_LEASE);
@@ -82,7 +100,11 @@ async function commandUpload(json) {
82
100
  }
83
101
  let summary;
84
102
  try {
85
- summary = await (0, upload_1.runUpload)(db, { installationId, leaseOwner: owner });
103
+ summary = await (0, upload_1.runUpload)(db, {
104
+ installationId,
105
+ leaseOwner: owner,
106
+ adapter,
107
+ });
86
108
  }
87
109
  finally {
88
110
  (0, leases_1.releaseLease)(db, leases_1.SOURCE_LEASE, owner);
@@ -108,36 +130,43 @@ async function commandUpload(json) {
108
130
  (0, db_1.closeDb)();
109
131
  }
110
132
  }
111
- function commandSourceApproveRoot(json, root) {
133
+ function commandSourceApproveRootUnlocked(json, root) {
112
134
  if (!root) {
113
- process.stderr.write("Usage: dreams source approve-root <path>\n");
135
+ process.stderr.write("Usage: dreams source approve-dir <path>\n");
114
136
  return 1;
115
137
  }
116
138
  const { db } = open();
117
139
  try {
118
140
  const roots = (0, config_1.approveRoot)(db, root);
119
- 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)"}`);
120
142
  return 0;
121
143
  }
122
144
  finally {
123
145
  (0, db_1.closeDb)();
124
146
  }
125
147
  }
126
- function commandSourceList(json) {
148
+ function commandSourceListUnlocked(json) {
127
149
  const { db } = open();
128
150
  try {
129
151
  const roots = (0, config_1.getApprovedRoots)(db);
130
152
  const sources = db
131
- .prepare("SELECT id, source_type, source_key, display_name, status FROM sources ORDER BY id")
153
+ .prepare("SELECT id, source_type, source_key, display_name, status, server_source_id FROM sources ORDER BY id")
132
154
  .all();
133
155
  const queue = (0, scan_1.listPendingQueue)(db);
134
- 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
+ }, () => [
135
164
  `Dream local sources`,
136
165
  ``,
137
- ` DB: ${(0, db_1.dbFilePath)()}`,
138
- ` Approved roots: ${roots.length ? roots.join(", ") : "(none — deny by default)"}`,
139
- ` Sources: ${sources.length}`,
140
- ` 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}`,
141
170
  ].join("\n"));
142
171
  return 0;
143
172
  }
@@ -145,16 +174,121 @@ function commandSourceList(json) {
145
174
  (0, db_1.closeDb)();
146
175
  }
147
176
  }
148
- function commandSource(subcommand, positional, json) {
177
+ async function commandSourceDetectUnlocked(json, adapter) {
178
+ const { db, installationId } = open();
179
+ try {
180
+ const owner = (0, leases_1.acquireLease)(db, leases_1.SOURCE_LEASE);
181
+ if (owner === null) {
182
+ emit(json, { status: "busy", reason: "another source operation holds the local lease" }, () => "Another Dreams source operation is in progress; skipping.");
183
+ return 0;
184
+ }
185
+ let result;
186
+ try {
187
+ result = await (0, detect_1.runSourceDetect)(db, {
188
+ installationId,
189
+ leaseOwner: owner,
190
+ adapter,
191
+ });
192
+ }
193
+ finally {
194
+ (0, leases_1.releaseLease)(db, leases_1.SOURCE_LEASE, owner);
195
+ }
196
+ emit(json, result, () => [
197
+ result.status === "ok" ? "Source detection complete" : "Source detection failed",
198
+ ``,
199
+ ` Harness: ${result.harness}${result.harness_present ? "" : " (not found)"}`,
200
+ ` Local source: ${result.local_source_id ?? "(none)"}`,
201
+ ` Server source: ${result.server_source_id ?? "(none)"}`,
202
+ ` Workspace: ${result.workspace_id ?? "(none)"}`,
203
+ ` Source status: ${result.source_status ?? "(none)"}`,
204
+ ` Registration: ${result.registration}`,
205
+ result.message ? ` Message: ${result.message}` : ``,
206
+ ].filter((line) => line !== ``).join("\n"));
207
+ return result.status === "ok" ? 0 : 1;
208
+ }
209
+ finally {
210
+ (0, db_1.closeDb)();
211
+ }
212
+ }
213
+ function stateBusy(json) {
214
+ emit(json, { status: "busy", reason: "another Dreams command holds the local state lock" }, () => "Another Dreams command is using local state; skipping.");
215
+ return 0;
216
+ }
217
+ function commandSourceScan(json, sourceType) {
218
+ const adapter = (0, source_adapters_1.resolveSourceAdapter)(sourceType);
219
+ const lock = (0, state_lock_1.acquireLocalStateLock)();
220
+ if (lock === null)
221
+ return stateBusy(json);
222
+ try {
223
+ return commandSourceScanUnlocked(json, adapter);
224
+ }
225
+ finally {
226
+ (0, state_lock_1.releaseLocalStateLock)(lock);
227
+ }
228
+ }
229
+ async function commandUpload(json, sourceType) {
230
+ const adapter = (0, source_adapters_1.resolveSourceAdapter)(sourceType);
231
+ const lock = (0, state_lock_1.acquireLocalStateLock)();
232
+ if (lock === null)
233
+ return stateBusy(json);
234
+ try {
235
+ return await commandUploadUnlocked(json, adapter);
236
+ }
237
+ finally {
238
+ (0, state_lock_1.releaseLocalStateLock)(lock);
239
+ }
240
+ }
241
+ function commandSourceApproveRoot(json, root) {
242
+ const lock = (0, state_lock_1.acquireLocalStateLock)();
243
+ if (lock === null)
244
+ return stateBusy(json);
245
+ try {
246
+ return commandSourceApproveRootUnlocked(json, root);
247
+ }
248
+ finally {
249
+ (0, state_lock_1.releaseLocalStateLock)(lock);
250
+ }
251
+ }
252
+ function commandSourceList(json) {
253
+ const lock = (0, state_lock_1.acquireLocalStateLock)();
254
+ if (lock === null)
255
+ return stateBusy(json);
256
+ try {
257
+ return commandSourceListUnlocked(json);
258
+ }
259
+ finally {
260
+ (0, state_lock_1.releaseLocalStateLock)(lock);
261
+ }
262
+ }
263
+ async function commandSourceDetect(json, sourceType) {
264
+ const adapter = (0, source_adapters_1.resolveSourceAdapter)(sourceType);
265
+ const lock = (0, state_lock_1.acquireLocalStateLock)();
266
+ if (lock === null)
267
+ return stateBusy(json);
268
+ try {
269
+ return await commandSourceDetectUnlocked(json, adapter);
270
+ }
271
+ finally {
272
+ (0, state_lock_1.releaseLocalStateLock)(lock);
273
+ }
274
+ }
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
+ }
149
280
  switch (subcommand) {
281
+ case "detect":
282
+ return commandSourceDetect(json, sourceType);
150
283
  case "scan":
151
- return commandSourceScan(json);
284
+ return commandSourceScan(json, sourceType);
152
285
  case "approve-root":
286
+ case "approve-dir":
153
287
  return commandSourceApproveRoot(json, positional);
154
288
  case "list":
155
289
  return commandSourceList(json);
156
290
  default:
157
- process.stderr.write(`Unknown source subcommand: ${subcommand ?? "(none)"} — expected 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`);
158
292
  return 1;
159
293
  }
160
294
  }
package/dist/local/db.js CHANGED
@@ -188,6 +188,74 @@ const MIGRATIONS = [
188
188
  // Due-row selection: pending rows for a source, ordered by priority then time.
189
189
  `CREATE INDEX idx_upload_queue_due ON upload_queue (source_id, state, priority, next_attempt_at)`,
190
190
  ],
191
+ [
192
+ // Hook-safe sync worker ([LET-10237]): durable trigger coalescing + runtime status.
193
+ `CREATE TABLE sync_triggers (
194
+ source_id TEXT PRIMARY KEY REFERENCES sources(id) ON DELETE CASCADE,
195
+ requested_gen INTEGER NOT NULL DEFAULT 0,
196
+ processed_gen INTEGER NOT NULL DEFAULT 0,
197
+ forced INTEGER NOT NULL DEFAULT 0,
198
+ session_hints TEXT,
199
+ updated_at TEXT NOT NULL
200
+ )`,
201
+ `CREATE TABLE sync_runtime (
202
+ id INTEGER PRIMARY KEY CHECK (id = 1),
203
+ paused INTEGER NOT NULL DEFAULT 0,
204
+ state TEXT NOT NULL DEFAULT 'idle',
205
+ last_attempt_at TEXT,
206
+ last_success_at TEXT,
207
+ last_error_at TEXT,
208
+ last_error_code TEXT,
209
+ last_error_message TEXT,
210
+ next_due_at TEXT,
211
+ updated_at TEXT NOT NULL
212
+ )`,
213
+ `INSERT INTO sync_runtime (id, paused, state, updated_at) VALUES (1, 0, 'idle', datetime('now'))`,
214
+ ],
215
+ [
216
+ // Backfill session frontier ([LET-10239]): per-generation completion for an
217
+ // approved [since_at, until_at] window. Mtime orders unfinished sessions only.
218
+ // Rows are scoped by policy_id so a later window cannot inherit stale completes.
219
+ `CREATE TABLE backfill_frontier (
220
+ policy_id TEXT NOT NULL,
221
+ source_id TEXT NOT NULL REFERENCES sources(id) ON DELETE CASCADE,
222
+ session_id TEXT NOT NULL,
223
+ generation INTEGER NOT NULL,
224
+ source_file_id INTEGER REFERENCES source_files(id) ON DELETE SET NULL,
225
+ mtime_ms INTEGER,
226
+ status TEXT NOT NULL DEFAULT 'pending',
227
+ completed_at TEXT,
228
+ updated_at TEXT NOT NULL,
229
+ PRIMARY KEY (policy_id, session_id, generation)
230
+ )`,
231
+ `CREATE INDEX idx_backfill_frontier_unfinished
232
+ ON backfill_frontier (policy_id, status, mtime_ms)`,
233
+ ],
234
+ [
235
+ // Per-source sync schedule ([LET-10314]): cadence/retry isolation across adapters.
236
+ // Global pause/running remain on sync_runtime; due state lives on sync_triggers.
237
+ `ALTER TABLE sync_triggers ADD COLUMN state TEXT NOT NULL DEFAULT 'idle'`,
238
+ `ALTER TABLE sync_triggers ADD COLUMN last_attempt_at TEXT`,
239
+ `ALTER TABLE sync_triggers ADD COLUMN last_success_at TEXT`,
240
+ `ALTER TABLE sync_triggers ADD COLUMN last_error_at TEXT`,
241
+ `ALTER TABLE sync_triggers ADD COLUMN last_error_code TEXT`,
242
+ `ALTER TABLE sync_triggers ADD COLUMN last_error_message TEXT`,
243
+ `ALTER TABLE sync_triggers ADD COLUMN next_due_at TEXT`,
244
+ // Preserve any pre-migration global cadence onto the default Claude trigger row.
245
+ `UPDATE sync_triggers
246
+ SET next_due_at = (SELECT next_due_at FROM sync_runtime WHERE id = 1),
247
+ last_attempt_at = (SELECT last_attempt_at FROM sync_runtime WHERE id = 1),
248
+ last_success_at = (SELECT last_success_at FROM sync_runtime WHERE id = 1),
249
+ last_error_at = (SELECT last_error_at FROM sync_runtime WHERE id = 1),
250
+ last_error_code = (SELECT last_error_code FROM sync_runtime WHERE id = 1),
251
+ last_error_message = (SELECT last_error_message FROM sync_runtime WHERE id = 1),
252
+ state = COALESCE((SELECT state FROM sync_runtime WHERE id = 1), 'idle')
253
+ WHERE source_id = 'src_claude-code'`,
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
+ ],
191
259
  ];
192
260
  exports.SCHEMA_VERSION = MIGRATIONS.length;
193
261
  function tx(db, fn) {
@@ -0,0 +1,148 @@
1
+ "use strict";
2
+ /**
3
+ * `dreams source detect` — register a harness as a detected Cloud source
4
+ * without reading transcripts, approving roots, scanning, or uploading.
5
+ *
6
+ * CLI defaults to the Claude adapter; pass an explicit adapter for Codex
7
+ * (`dreams source detect --source codex`) ([LET-10313] / [LET-10315]).
8
+ */
9
+ Object.defineProperty(exports, "__esModule", { value: true });
10
+ exports.detectClaudeCodeHarness = detectClaudeCodeHarness;
11
+ exports.localClaudeSourceId = localClaudeSourceId;
12
+ exports.ensureLocalDetectedSource = ensureLocalDetectedSource;
13
+ exports.runSourceDetect = runSourceDetect;
14
+ const index_1 = require("../auth/index");
15
+ const client_1 = require("../cloud/client");
16
+ const version_1 = require("../version");
17
+ const skill_1 = require("../skill");
18
+ const bindings_1 = require("./bindings");
19
+ const claude_1 = require("./claude");
20
+ const codex_1 = require("./codex");
21
+ const db_1 = require("./db");
22
+ const leases_1 = require("./leases");
23
+ /**
24
+ * Deterministic harness-owned filesystem facts only. Never opens transcript
25
+ * files or walks session contents.
26
+ */
27
+ function detectClaudeCodeHarness(home) {
28
+ const result = (0, claude_1.detectClaudeCodeHarness)(home);
29
+ return { present: result.present, harness: claude_1.claudeSourceAdapter.sourceType, evidence: result.evidence };
30
+ }
31
+ function localClaudeSourceId() {
32
+ return claude_1.claudeSourceAdapter.sourceId;
33
+ }
34
+ /** Ensure the local sources row exists without touching queues or files. */
35
+ function ensureLocalDetectedSource(db, owner, adapter) {
36
+ const id = adapter.sourceId;
37
+ return (0, db_1.tx)(db, () => {
38
+ (0, leases_1.assertLeaseHeld)(db, leases_1.SOURCE_LEASE, owner);
39
+ const existing = db.prepare("SELECT id FROM sources WHERE id = ?").get(id);
40
+ if (!existing) {
41
+ const now = (0, db_1.nowIso)();
42
+ db.prepare(`INSERT INTO sources (id, source_type, source_key, display_name, adapter_version, status, created_at, updated_at)
43
+ VALUES (?, ?, ?, ?, ?, 'detected', ?, ?)`).run(id, adapter.sourceType, adapter.sourceKey, adapter.displayName, adapter.adapterVersion, now, now);
44
+ }
45
+ return id;
46
+ });
47
+ }
48
+ async function runSourceDetect(db, options) {
49
+ const adapter = options.adapter;
50
+ const detection = adapter.detect();
51
+ if (!detection.present) {
52
+ return {
53
+ status: "error",
54
+ harness: adapter.sourceType,
55
+ harness_present: false,
56
+ evidence: [],
57
+ local_source_id: null,
58
+ server_source_id: null,
59
+ workspace_id: null,
60
+ source_status: null,
61
+ registration: "skipped",
62
+ message: adapter.sourceType === claude_1.claudeSourceAdapter.sourceType
63
+ ? "Claude Code was not detected under ~/.claude"
64
+ : adapter.sourceType === codex_1.CODEX_SOURCE_TYPE
65
+ ? "Codex was not detected under ~/.codex"
66
+ : `${adapter.displayName} was not detected`,
67
+ };
68
+ }
69
+ const localId = ensureLocalDetectedSource(db, options.leaseOwner, adapter);
70
+ const before = db.prepare("SELECT server_source_id, status FROM sources WHERE id = ?").get(localId);
71
+ let registration;
72
+ try {
73
+ registration = await (0, client_1.registerSource)({
74
+ sourceType: adapter.sourceType,
75
+ sourceKey: adapter.sourceKey,
76
+ displayName: adapter.displayName,
77
+ installationId: options.installationId,
78
+ adapterVersion: adapter.adapterVersion,
79
+ // Required for detection-only create: Cloud defaults omitted status to active.
80
+ status: "detected",
81
+ cliVersion: (0, version_1.packageVersion)(),
82
+ skillVersion: (0, skill_1.installedSkillVersion)(),
83
+ platform: process.platform,
84
+ });
85
+ }
86
+ catch (error) {
87
+ if (error instanceof index_1.NotAuthenticatedError) {
88
+ return {
89
+ status: "error",
90
+ harness: adapter.sourceType,
91
+ harness_present: true,
92
+ evidence: detection.evidence,
93
+ local_source_id: localId,
94
+ server_source_id: before.server_source_id,
95
+ workspace_id: null,
96
+ source_status: before.status,
97
+ registration: "skipped",
98
+ message: "not authenticated: run `dreams auth login`",
99
+ };
100
+ }
101
+ if (error instanceof client_1.CloudAuthError) {
102
+ return {
103
+ status: "error",
104
+ harness: adapter.sourceType,
105
+ harness_present: true,
106
+ evidence: detection.evidence,
107
+ local_source_id: localId,
108
+ server_source_id: before.server_source_id,
109
+ workspace_id: null,
110
+ source_status: before.status,
111
+ registration: "skipped",
112
+ message: `authentication failed: ${error.message}`,
113
+ };
114
+ }
115
+ if (error instanceof client_1.CloudError) {
116
+ return {
117
+ status: "error",
118
+ harness: adapter.sourceType,
119
+ harness_present: true,
120
+ evidence: detection.evidence,
121
+ local_source_id: localId,
122
+ server_source_id: before.server_source_id,
123
+ workspace_id: null,
124
+ source_status: before.status,
125
+ registration: "skipped",
126
+ message: `could not register source: ${error.message}`,
127
+ };
128
+ }
129
+ throw error;
130
+ }
131
+ const persisted = (0, bindings_1.persistSourceBindings)(db, localId, {
132
+ serverSourceId: registration.serverSourceId,
133
+ status: registration.status,
134
+ workspaceId: registration.workspaceId,
135
+ }, options.leaseOwner);
136
+ return {
137
+ status: "ok",
138
+ harness: adapter.sourceType,
139
+ harness_present: true,
140
+ evidence: detection.evidence,
141
+ local_source_id: localId,
142
+ server_source_id: persisted.serverSourceId,
143
+ workspace_id: persisted.workspaceId,
144
+ source_status: persisted.status,
145
+ registration: before.server_source_id ? "refreshed" : "created",
146
+ message: null,
147
+ };
148
+ }
@@ -0,0 +1,6 @@
1
+ "use strict";
2
+ /**
3
+ * Harness-agnostic session discovery shape ([LET-10313]).
4
+ * Parsing and filesystem layout stay inside each source adapter.
5
+ */
6
+ Object.defineProperty(exports, "__esModule", { value: true });
@@ -0,0 +1,102 @@
1
+ "use strict";
2
+ /**
3
+ * `dreams hooks install [--dry-run] [--source <type>] [--json]`
4
+ *
5
+ * CLI owns the mutation; the setup skill owns consent and showing the dry-run.
6
+ */
7
+ Object.defineProperty(exports, "__esModule", { value: true });
8
+ exports.commandHooksInstall = commandHooksInstall;
9
+ const claude_1 = require("./claude");
10
+ const claude_hooks_1 = require("./claude-hooks");
11
+ const codex_1 = require("./codex");
12
+ const codex_hooks_1 = require("./codex-hooks");
13
+ const source_adapters_1 = require("./source-adapters");
14
+ const skill_1 = require("../skill");
15
+ const managed_install_1 = require("../managed-install");
16
+ function emit(json, payload, human) {
17
+ if (json)
18
+ console.log(JSON.stringify(payload, null, 2));
19
+ else
20
+ console.log(human());
21
+ }
22
+ function commandHooksInstall(options) {
23
+ let sources;
24
+ try {
25
+ if (options.sourceType) {
26
+ const adapter = (0, source_adapters_1.resolveSourceAdapter)(options.sourceType);
27
+ if (adapter.sourceType !== "claude-code" && adapter.sourceType !== "codex") {
28
+ process.stderr.write(`hooks install does not support source type ${adapter.sourceType}\n`);
29
+ return 1;
30
+ }
31
+ sources = [adapter.sourceType];
32
+ }
33
+ else {
34
+ sources = ["claude-code", "codex"];
35
+ }
36
+ }
37
+ catch (error) {
38
+ process.stderr.write(`${error instanceof Error ? error.message : String(error)}\n`);
39
+ return 1;
40
+ }
41
+ const cliPath = (0, managed_install_1.stableCliPath)();
42
+ const results = [];
43
+ const humanLines = [
44
+ options.dryRun ? "Dreams hooks install (dry-run — no files written)" : "Dreams hooks install",
45
+ "",
46
+ ` CLI path: ${cliPath}`,
47
+ ];
48
+ for (const source of sources) {
49
+ if (source === "claude-code") {
50
+ const present = (0, claude_1.detectClaudeCodeHarness)().present;
51
+ if (!present && options.sourceType === undefined) {
52
+ humanLines.push("", "Claude Code: harness not detected — skipped");
53
+ results.push({ source: "claude-code", status: "skipped", reason: "harness_not_detected" });
54
+ continue;
55
+ }
56
+ const plan = (0, claude_hooks_1.planClaudeHooksInstall)({ cliPath });
57
+ if (!options.dryRun)
58
+ (0, claude_hooks_1.installClaudeHooks)({ cliPath });
59
+ results.push({
60
+ source: "claude-code",
61
+ status: options.dryRun ? "dry_run" : plan.changed ? "updated" : "unchanged",
62
+ settings_path: plan.settings_path,
63
+ changed: plan.changed,
64
+ session_start_command: plan.session_start_command,
65
+ stop_command: plan.stop_command,
66
+ next_contents: options.dryRun ? plan.next_contents : undefined,
67
+ });
68
+ humanLines.push("", `Claude Code (${options.dryRun ? "dry-run" : plan.changed ? "updated" : "unchanged"}):`, ` Settings: ${plan.settings_path}`, ` SessionStart → wake: ${plan.session_start_command}`, ` Stop → sync: ${plan.stop_command}`);
69
+ continue;
70
+ }
71
+ const present = (0, codex_1.detectCodexHarness)().present;
72
+ if (!present && options.sourceType === undefined) {
73
+ humanLines.push("", "Codex: harness not detected — skipped");
74
+ results.push({ source: "codex", status: "skipped", reason: "harness_not_detected" });
75
+ continue;
76
+ }
77
+ const hookScriptPath = (0, skill_1.codexSyncHookScriptPath)();
78
+ const plan = (0, codex_hooks_1.planCodexHooksInstall)({ cliPath, hookScriptPath });
79
+ if (!options.dryRun)
80
+ (0, codex_hooks_1.installCodexHooks)({ cliPath, hookScriptPath });
81
+ results.push({
82
+ source: "codex",
83
+ status: options.dryRun ? "dry_run" : plan.hooks_changed || plan.config_changed ? "updated" : "unchanged",
84
+ hooks_path: plan.hooks_path,
85
+ config_path: plan.config_path,
86
+ hooks_changed: plan.hooks_changed,
87
+ config_changed: plan.config_changed,
88
+ session_start_command: plan.session_start_command,
89
+ stop_command: plan.stop_command,
90
+ next_hooks_contents: options.dryRun ? plan.next_hooks_contents : undefined,
91
+ next_config_contents: options.dryRun ? plan.next_config_contents : undefined,
92
+ });
93
+ humanLines.push("", `Codex (${options.dryRun ? "dry-run" : plan.hooks_changed || plan.config_changed ? "updated" : "unchanged"}):`, ` Hooks: ${plan.hooks_path}`, ` Config: ${plan.config_path}`, ` SessionStart → wake: ${plan.session_start_command}`, ` Stop → sync: ${plan.stop_command}`, options.dryRun ? " (Also ensures [features].hooks = true in config.toml)" : "");
94
+ }
95
+ emit(options.json, {
96
+ status: "ok",
97
+ dry_run: options.dryRun,
98
+ cli_path: cliPath,
99
+ results,
100
+ }, () => humanLines.filter((line) => line !== "").join("\n"));
101
+ return 0;
102
+ }
@@ -42,7 +42,7 @@ var __importStar = (this && this.__importStar) || (function () {
42
42
  };
43
43
  })();
44
44
  Object.defineProperty(exports, "__esModule", { value: true });
45
- exports.LeaseLostError = exports.SOURCE_LEASE = void 0;
45
+ exports.LeaseLostError = exports.SYNC_WORKER_LEASE = exports.SOURCE_LEASE = void 0;
46
46
  exports.assertLeaseHeld = assertLeaseHeld;
47
47
  exports.renewLease = renewLease;
48
48
  exports.acquireLease = acquireLease;
@@ -57,6 +57,8 @@ const DEFAULT_TTL_MS = 30_000;
57
57
  * against this one lease so they never interleave writes.
58
58
  */
59
59
  exports.SOURCE_LEASE = "source";
60
+ /** Election lease for the detached sync worker so only one worker runs at a time. */
61
+ exports.SYNC_WORKER_LEASE = "sync-worker";
60
62
  /** Thrown when a write is attempted after the lease was lost/reclaimed. */
61
63
  class LeaseLostError extends Error {
62
64
  constructor(name) {