@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.
- package/README.md +56 -34
- package/bin/dreams-codex-hook.cjs +91 -0
- package/bin/dreams.cjs +21 -0
- package/dist/auth/commands.js +21 -1
- package/dist/auth/credentials.js +73 -14
- package/dist/auth/index.js +106 -13
- package/dist/cli.js +48 -12
- package/dist/local/backfill.js +20 -3
- package/dist/local/claude-hooks.js +169 -0
- package/dist/local/claude.js +76 -20
- package/dist/local/codex-hooks.js +235 -0
- package/dist/local/codex.js +298 -101
- package/dist/local/commands.js +39 -24
- package/dist/local/db.js +4 -0
- package/dist/local/detect.js +6 -2
- package/dist/local/hooks-install.js +102 -0
- package/dist/local/source-adapters.js +13 -0
- package/dist/local/sync-control.js +73 -22
- package/dist/skill.js +202 -110
- package/dist/suppress-sqlite-warning.js +34 -0
- package/dist/sync.js +191 -31
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -2,6 +2,8 @@
|
|
|
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");
|
|
7
9
|
const development_reset_1 = require("./development-reset");
|
|
@@ -15,6 +17,7 @@ const snapshots_1 = require("./snapshots");
|
|
|
15
17
|
const skill_1 = require("./skill");
|
|
16
18
|
const store_1 = require("./store");
|
|
17
19
|
const backfill_1 = require("./local/backfill");
|
|
20
|
+
const hooks_install_1 = require("./local/hooks-install");
|
|
18
21
|
const sync_1 = require("./sync");
|
|
19
22
|
const version_1 = require("./version");
|
|
20
23
|
const USAGE = `Usage: dreams <command> [options]
|
|
@@ -28,12 +31,15 @@ Commands:
|
|
|
28
31
|
auth logout Revoke Dreams' refresh token and clear stored credentials
|
|
29
32
|
dev reset local Preview/reset local SQLite and blob state
|
|
30
33
|
dev reset identity Preview/reset local state, credentials, and device identity
|
|
31
|
-
source detect Detect
|
|
32
|
-
source scan Discover
|
|
33
|
-
source approve-root <path> Approve a work
|
|
34
|
-
source
|
|
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
|
|
35
39
|
snapshots create Submit a skill-authored snapshot JSON file to Cloud
|
|
36
|
-
|
|
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)
|
|
37
43
|
sync pause|resume|status Pause/resume hooks or show sync diagnostics
|
|
38
44
|
backfill start Queue a bounded historical backfill window (local only)
|
|
39
45
|
upload Upload pending queued objects to Letta Cloud
|
|
@@ -41,8 +47,9 @@ Commands:
|
|
|
41
47
|
Options:
|
|
42
48
|
--json Machine-readable JSON output
|
|
43
49
|
--confirm Apply a development reset preview
|
|
50
|
+
--dry-run Preview hooks install without writing files
|
|
44
51
|
--force Bypass sync cadence (never bypasses pause/privacy)
|
|
45
|
-
--source <type>
|
|
52
|
+
--source <type> Source adapter (claude-code default; codex) for sync/wake, source detect/scan, upload, backfill, hooks install
|
|
46
53
|
--since <RFC3339> Backfill window start (with backfill start)
|
|
47
54
|
--type <type> Snapshot type (with snapshots create)
|
|
48
55
|
--file <path> Snapshot JSON file (with snapshots create)
|
|
@@ -51,12 +58,13 @@ Options:
|
|
|
51
58
|
--version Print the CLI version
|
|
52
59
|
--help Show this help
|
|
53
60
|
`;
|
|
54
|
-
const COMMANDS_WITH_SUBCOMMANDS = new Set(["auth", "dev", "source", "snapshots", "sync", "backfill"]);
|
|
61
|
+
const COMMANDS_WITH_SUBCOMMANDS = new Set(["auth", "dev", "source", "snapshots", "sync", "backfill", "hooks"]);
|
|
55
62
|
function parseArgs(argv) {
|
|
56
63
|
const parsed = {
|
|
57
64
|
positionals: [],
|
|
58
65
|
json: false,
|
|
59
66
|
confirm: false,
|
|
67
|
+
dryRun: false,
|
|
60
68
|
help: false,
|
|
61
69
|
version: false,
|
|
62
70
|
force: false,
|
|
@@ -75,6 +83,8 @@ function parseArgs(argv) {
|
|
|
75
83
|
parsed.json = true;
|
|
76
84
|
else if (arg === "--confirm")
|
|
77
85
|
parsed.confirm = true;
|
|
86
|
+
else if (arg === "--dry-run")
|
|
87
|
+
parsed.dryRun = true;
|
|
78
88
|
else if (arg === "--force")
|
|
79
89
|
parsed.force = true;
|
|
80
90
|
else if (arg === "--worker")
|
|
@@ -257,8 +267,18 @@ async function runCli() {
|
|
|
257
267
|
process.stderr.write(`--force/--worker are only valid with \`dreams sync\`\n\n${USAGE}`);
|
|
258
268
|
return 1;
|
|
259
269
|
}
|
|
260
|
-
if (args.
|
|
261
|
-
process.stderr.write(`--
|
|
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}`);
|
|
262
282
|
return 1;
|
|
263
283
|
}
|
|
264
284
|
if (args.since && command !== "backfill") {
|
|
@@ -286,6 +306,14 @@ async function runCli() {
|
|
|
286
306
|
process.stderr.write(`Unexpected argument(s): ${args.positionals.slice(2).join(", ")}\n\n${USAGE}`);
|
|
287
307
|
return 1;
|
|
288
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
|
+
}
|
|
289
317
|
switch (command) {
|
|
290
318
|
case "onboard":
|
|
291
319
|
return (0, onboard_1.commandOnboard)(args.json);
|
|
@@ -298,7 +326,7 @@ async function runCli() {
|
|
|
298
326
|
case "dev":
|
|
299
327
|
return commandDevelopment(args.positionals, args.confirm, args.json);
|
|
300
328
|
case "source":
|
|
301
|
-
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);
|
|
302
330
|
case "snapshots":
|
|
303
331
|
return (0, snapshots_1.commandSnapshots)(subcommand, {
|
|
304
332
|
type: args.type,
|
|
@@ -307,6 +335,14 @@ async function runCli() {
|
|
|
307
335
|
workspaceId: args.workspaceId,
|
|
308
336
|
json: args.json,
|
|
309
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 });
|
|
310
346
|
case "sync":
|
|
311
347
|
return (0, sync_1.commandSync)(subcommand, {
|
|
312
348
|
json: args.json,
|
|
@@ -319,9 +355,9 @@ async function runCli() {
|
|
|
319
355
|
process.stderr.write(`Unknown backfill subcommand: ${subcommand ?? "(none)"} — expected start\n\n${USAGE}`);
|
|
320
356
|
return 1;
|
|
321
357
|
}
|
|
322
|
-
return (0, backfill_1.commandBackfillStart)({ since: args.since, json: args.json });
|
|
358
|
+
return (0, backfill_1.commandBackfillStart)({ since: args.since, json: args.json, sourceType: args.source });
|
|
323
359
|
case "upload":
|
|
324
|
-
return (0, commands_2.commandUpload)(args.json);
|
|
360
|
+
return (0, commands_2.commandUpload)(args.json, args.source);
|
|
325
361
|
default:
|
|
326
362
|
process.stderr.write(`Unknown command: ${command}\n\n${USAGE}`);
|
|
327
363
|
return 1;
|
package/dist/local/backfill.js
CHANGED
|
@@ -517,6 +517,21 @@ function commandBackfillStart(options) {
|
|
|
517
517
|
process.stderr.write("dreams backfill start requires --since <RFC3339>\n");
|
|
518
518
|
return 1;
|
|
519
519
|
}
|
|
520
|
+
// Resolve before any installation/DB/policy mutation (unknown/empty fail closed).
|
|
521
|
+
let adapter;
|
|
522
|
+
try {
|
|
523
|
+
adapter = (0, source_adapters_1.resolveSourceAdapter)(options.sourceType);
|
|
524
|
+
}
|
|
525
|
+
catch (error) {
|
|
526
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
527
|
+
if (options.json) {
|
|
528
|
+
console.log(JSON.stringify({ status: "error", schema_version: 1, code: "invalid_argument", message }));
|
|
529
|
+
}
|
|
530
|
+
else {
|
|
531
|
+
process.stderr.write(`${message}\n`);
|
|
532
|
+
}
|
|
533
|
+
return 1;
|
|
534
|
+
}
|
|
520
535
|
const lock = (0, state_lock_1.acquireLocalStateLock)();
|
|
521
536
|
if (lock === null) {
|
|
522
537
|
if (options.json) {
|
|
@@ -548,7 +563,7 @@ function commandBackfillStart(options) {
|
|
|
548
563
|
installationId: installation.installation_id,
|
|
549
564
|
leaseOwner: owner,
|
|
550
565
|
since: options.since,
|
|
551
|
-
adapter
|
|
566
|
+
adapter,
|
|
552
567
|
});
|
|
553
568
|
}
|
|
554
569
|
finally {
|
|
@@ -557,6 +572,8 @@ function commandBackfillStart(options) {
|
|
|
557
572
|
const payload = {
|
|
558
573
|
schema_version: 1,
|
|
559
574
|
status: result.status,
|
|
575
|
+
source: adapter.sourceType,
|
|
576
|
+
source_id: adapter.sourceId,
|
|
560
577
|
policy: result.policy,
|
|
561
578
|
sessions_in_window: result.sessions_in_window,
|
|
562
579
|
sessions_selected: result.sessions_selected,
|
|
@@ -568,7 +585,7 @@ function commandBackfillStart(options) {
|
|
|
568
585
|
console.log(JSON.stringify(payload, null, 2));
|
|
569
586
|
else {
|
|
570
587
|
console.log([
|
|
571
|
-
`Backfill ${result.status} for ${result.policy.since_at} → ${result.policy.until_at}`,
|
|
588
|
+
`Backfill ${result.status} for ${adapter.sourceType} (${result.policy.since_at} → ${result.policy.until_at})`,
|
|
572
589
|
"",
|
|
573
590
|
` Sessions in window: ${result.sessions_in_window}`,
|
|
574
591
|
` Sessions scanned: ${result.sessions_selected}`,
|
|
@@ -576,7 +593,7 @@ function commandBackfillStart(options) {
|
|
|
576
593
|
` Frontier complete: ${result.frontier_complete}`,
|
|
577
594
|
` Segments enqueued: ${result.scan.segments_enqueued}`,
|
|
578
595
|
"",
|
|
579
|
-
|
|
596
|
+
`Local queue only — wake sync to upload: dreams sync --source ${adapter.sourceType} --force`,
|
|
580
597
|
].join("\n"));
|
|
581
598
|
}
|
|
582
599
|
return 0;
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Claude Code live-sync hook helpers — merge Dreams SessionStart/Stop into
|
|
4
|
+
* ~/.claude/settings.json without clobbering unrelated handlers.
|
|
5
|
+
*
|
|
6
|
+
* SessionStart → `dreams wake` (housekeeping + pending drain)
|
|
7
|
+
* Stop → `dreams sync` (live turn, stdin session hint)
|
|
8
|
+
*/
|
|
9
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
12
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
13
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
14
|
+
}
|
|
15
|
+
Object.defineProperty(o, k2, desc);
|
|
16
|
+
}) : (function(o, m, k, k2) {
|
|
17
|
+
if (k2 === undefined) k2 = k;
|
|
18
|
+
o[k2] = m[k];
|
|
19
|
+
}));
|
|
20
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
21
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
22
|
+
}) : function(o, v) {
|
|
23
|
+
o["default"] = v;
|
|
24
|
+
});
|
|
25
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
26
|
+
var ownKeys = function(o) {
|
|
27
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
28
|
+
var ar = [];
|
|
29
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
30
|
+
return ar;
|
|
31
|
+
};
|
|
32
|
+
return ownKeys(o);
|
|
33
|
+
};
|
|
34
|
+
return function (mod) {
|
|
35
|
+
if (mod && mod.__esModule) return mod;
|
|
36
|
+
var result = {};
|
|
37
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
38
|
+
__setModuleDefault(result, mod);
|
|
39
|
+
return result;
|
|
40
|
+
};
|
|
41
|
+
})();
|
|
42
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
43
|
+
exports.CLAUDE_HOOK_MARKER = void 0;
|
|
44
|
+
exports.claudeSettingsPath = claudeSettingsPath;
|
|
45
|
+
exports.buildClaudeHookCommandPosix = buildClaudeHookCommandPosix;
|
|
46
|
+
exports.buildClaudeHookCommandPowerShell = buildClaudeHookCommandPowerShell;
|
|
47
|
+
exports.buildClaudeHookCommand = buildClaudeHookCommand;
|
|
48
|
+
exports.mergeClaudeSettingsJson = mergeClaudeSettingsJson;
|
|
49
|
+
exports.planClaudeHooksInstall = planClaudeHooksInstall;
|
|
50
|
+
exports.installClaudeHooks = installClaudeHooks;
|
|
51
|
+
const fs = __importStar(require("node:fs"));
|
|
52
|
+
const os = __importStar(require("node:os"));
|
|
53
|
+
const path = __importStar(require("node:path"));
|
|
54
|
+
exports.CLAUDE_HOOK_MARKER = "dreams-sync-hook";
|
|
55
|
+
function claudeSettingsPath(home = os.homedir()) {
|
|
56
|
+
return path.join(home, ".claude", "settings.json");
|
|
57
|
+
}
|
|
58
|
+
/** POSIX bash command for Claude Code command hooks. */
|
|
59
|
+
function buildClaudeHookCommandPosix(cliPath, mode) {
|
|
60
|
+
return `"${cliPath}" ${mode} >/dev/null 2>&1 || true # ${exports.CLAUDE_HOOK_MARKER}`;
|
|
61
|
+
}
|
|
62
|
+
/** PowerShell command for Claude Code command hooks on Windows. */
|
|
63
|
+
function buildClaudeHookCommandPowerShell(cliPath, mode) {
|
|
64
|
+
const quoted = cliPath.replace(/'/g, "''");
|
|
65
|
+
return `& '${quoted}' ${mode} *> $null; exit 0 # ${exports.CLAUDE_HOOK_MARKER}`;
|
|
66
|
+
}
|
|
67
|
+
function buildClaudeHookCommand(cliPath, mode, platform = process.platform) {
|
|
68
|
+
return platform === "win32"
|
|
69
|
+
? buildClaudeHookCommandPowerShell(cliPath, mode)
|
|
70
|
+
: buildClaudeHookCommandPosix(cliPath, mode);
|
|
71
|
+
}
|
|
72
|
+
function isDreamsClaudeHandler(command) {
|
|
73
|
+
if (typeof command !== "string")
|
|
74
|
+
return false;
|
|
75
|
+
// Prefer the marker; also recognize older Dreams sync-only handlers by cli+sync.
|
|
76
|
+
return (command.includes(exports.CLAUDE_HOOK_MARKER) ||
|
|
77
|
+
(/\bsync\b/.test(command) && (command.includes("dreams") || command.includes(">/dev/null") || command.includes("*> $null"))));
|
|
78
|
+
}
|
|
79
|
+
function upsertEventHandler(matchers, command, platform) {
|
|
80
|
+
const next = matchers.map((matcher) => ({ ...matcher, hooks: [...(matcher.hooks ?? [])] }));
|
|
81
|
+
let updated = false;
|
|
82
|
+
for (const matcher of next) {
|
|
83
|
+
const hooks = matcher.hooks ?? [];
|
|
84
|
+
for (let i = 0; i < hooks.length; i++) {
|
|
85
|
+
if (isDreamsClaudeHandler(hooks[i]?.command)) {
|
|
86
|
+
const handler = { ...hooks[i], type: "command", command };
|
|
87
|
+
if (platform === "win32")
|
|
88
|
+
handler.shell = "powershell";
|
|
89
|
+
else
|
|
90
|
+
delete handler.shell;
|
|
91
|
+
hooks[i] = handler;
|
|
92
|
+
updated = true;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
matcher.hooks = hooks;
|
|
96
|
+
}
|
|
97
|
+
if (updated)
|
|
98
|
+
return next;
|
|
99
|
+
const handler = { type: "command", command };
|
|
100
|
+
if (platform === "win32")
|
|
101
|
+
handler.shell = "powershell";
|
|
102
|
+
return [...next, { hooks: [handler] }];
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* Merge Dreams Claude handlers into settings.json.
|
|
106
|
+
* SessionStart uses wake; Stop uses sync.
|
|
107
|
+
*/
|
|
108
|
+
function mergeClaudeSettingsJson(existingRaw, cliPath, platform = process.platform) {
|
|
109
|
+
const wakeCmd = buildClaudeHookCommand(cliPath, "wake", platform);
|
|
110
|
+
const syncCmd = buildClaudeHookCommand(cliPath, "sync", platform);
|
|
111
|
+
let doc = {};
|
|
112
|
+
if (existingRaw !== null && existingRaw.trim() !== "") {
|
|
113
|
+
const parsed = JSON.parse(existingRaw);
|
|
114
|
+
if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
115
|
+
throw new Error("claude settings.json must be a JSON object");
|
|
116
|
+
}
|
|
117
|
+
doc = { ...parsed };
|
|
118
|
+
}
|
|
119
|
+
const hooksValue = doc.hooks;
|
|
120
|
+
const hooks = hooksValue && typeof hooksValue === "object" && !Array.isArray(hooksValue)
|
|
121
|
+
? { ...hooksValue }
|
|
122
|
+
: {};
|
|
123
|
+
const sessionStart = Array.isArray(hooks.SessionStart) ? hooks.SessionStart : [];
|
|
124
|
+
const stop = Array.isArray(hooks.Stop) ? hooks.Stop : [];
|
|
125
|
+
hooks.SessionStart = upsertEventHandler(sessionStart, wakeCmd, platform);
|
|
126
|
+
hooks.Stop = upsertEventHandler(stop, syncCmd, platform);
|
|
127
|
+
doc.hooks = hooks;
|
|
128
|
+
return `${JSON.stringify(doc, null, 2)}\n`;
|
|
129
|
+
}
|
|
130
|
+
function planClaudeHooksInstall(options) {
|
|
131
|
+
const home = options.home ?? os.homedir();
|
|
132
|
+
const platform = options.platform ?? process.platform;
|
|
133
|
+
const settingsPath = claudeSettingsPath(home);
|
|
134
|
+
let existing = null;
|
|
135
|
+
try {
|
|
136
|
+
existing = fs.readFileSync(settingsPath, "utf8");
|
|
137
|
+
}
|
|
138
|
+
catch {
|
|
139
|
+
existing = null;
|
|
140
|
+
}
|
|
141
|
+
const next = mergeClaudeSettingsJson(existing, options.cliPath, platform);
|
|
142
|
+
return {
|
|
143
|
+
settings_path: settingsPath,
|
|
144
|
+
changed: existing !== next,
|
|
145
|
+
session_start_command: buildClaudeHookCommand(options.cliPath, "wake", platform),
|
|
146
|
+
stop_command: buildClaudeHookCommand(options.cliPath, "sync", platform),
|
|
147
|
+
next_contents: next,
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
function installClaudeHooks(options) {
|
|
151
|
+
const plan = planClaudeHooksInstall(options);
|
|
152
|
+
if (plan.changed) {
|
|
153
|
+
fs.mkdirSync(path.dirname(plan.settings_path), { recursive: true });
|
|
154
|
+
const temporary = `${plan.settings_path}.tmp-${process.pid}-${Math.random().toString(16).slice(2)}`;
|
|
155
|
+
try {
|
|
156
|
+
fs.writeFileSync(temporary, plan.next_contents, { encoding: "utf8" });
|
|
157
|
+
fs.renameSync(temporary, plan.settings_path);
|
|
158
|
+
}
|
|
159
|
+
finally {
|
|
160
|
+
try {
|
|
161
|
+
fs.rmSync(temporary, { force: true });
|
|
162
|
+
}
|
|
163
|
+
catch {
|
|
164
|
+
// ignore
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
return plan;
|
|
169
|
+
}
|
package/dist/local/claude.js
CHANGED
|
@@ -5,9 +5,12 @@
|
|
|
5
5
|
* Discovery rule (filesystem shape is authoritative for the pilot):
|
|
6
6
|
* - only DIRECT children of ~/.claude/projects/<encoded-project>/
|
|
7
7
|
* - filename must be a UUID-like `<sessionId>.jsonl`
|
|
8
|
-
* -
|
|
8
|
+
* - a complete head record's `sessionId` must agree with the filename
|
|
9
|
+
* - `cwd` is taken from the first head record that carries a string `cwd`
|
|
10
|
+
* for that session (modern Claude transcripts often open with mode /
|
|
11
|
+
* ai-title / etc. before the first cwd-bearing record)
|
|
9
12
|
* - never recurse; the `subagents/` directory is excluded outright
|
|
10
|
-
* - `isSidechain: true`
|
|
13
|
+
* - `isSidechain: true` on any matching head record excludes the file
|
|
11
14
|
* `parentUuid` is intentionally NOT used — ordinary main-session records also
|
|
12
15
|
* carry parent relationships.
|
|
13
16
|
*
|
|
@@ -107,28 +110,84 @@ function detectClaudeCodeHarness(home = os.homedir()) {
|
|
|
107
110
|
}
|
|
108
111
|
return { present: true, evidence };
|
|
109
112
|
}
|
|
110
|
-
/**
|
|
111
|
-
|
|
113
|
+
/**
|
|
114
|
+
* Read up to HEAD_READ_BYTES and derive session identity + cwd from complete
|
|
115
|
+
* JSONL records in that window. Modern Claude Code transcripts often open with
|
|
116
|
+
* metadata lines (mode, ai-title, …) that omit `cwd`; the first cwd-bearing
|
|
117
|
+
* record may appear several lines later (still well within the head window).
|
|
118
|
+
*
|
|
119
|
+
* Returns null when the head has no complete line, no matching sessionId, a
|
|
120
|
+
* mismatched sessionId, or is unreadable / malformed beyond recovery.
|
|
121
|
+
*/
|
|
122
|
+
function readHeadSessionFacts(filePath, expectedSessionId) {
|
|
112
123
|
let fd;
|
|
113
124
|
try {
|
|
114
125
|
fd = fs.openSync(filePath, "r");
|
|
115
126
|
const buf = Buffer.alloc(HEAD_READ_BYTES);
|
|
116
127
|
const read = fs.readSync(fd, buf, 0, HEAD_READ_BYTES, 0);
|
|
128
|
+
if (read === 0)
|
|
129
|
+
return null;
|
|
117
130
|
const head = buf.subarray(0, read);
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
131
|
+
// Only complete lines — a trailing partial record in the head is ignored.
|
|
132
|
+
const text = head.toString("utf8");
|
|
133
|
+
const lines = text.split("\n");
|
|
134
|
+
// If the buffer did not end on a newline, the final split piece is partial.
|
|
135
|
+
const completeLines = head[head.length - 1] === 0x0a ? lines : lines.slice(0, -1);
|
|
136
|
+
if (completeLines.length === 0)
|
|
123
137
|
return null;
|
|
124
|
-
|
|
138
|
+
let sawMatchingSession = false;
|
|
139
|
+
let isSidechain = false;
|
|
140
|
+
let cwd = null;
|
|
141
|
+
for (const raw of completeLines) {
|
|
142
|
+
const line = raw.trim();
|
|
143
|
+
if (line === "")
|
|
144
|
+
continue;
|
|
145
|
+
let record;
|
|
146
|
+
try {
|
|
147
|
+
const parsed = JSON.parse(line);
|
|
148
|
+
if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed))
|
|
149
|
+
continue;
|
|
150
|
+
record = parsed;
|
|
151
|
+
}
|
|
152
|
+
catch {
|
|
153
|
+
continue;
|
|
154
|
+
}
|
|
155
|
+
const sessionId = typeof record.sessionId === "string" ? record.sessionId : null;
|
|
156
|
+
if (sessionId !== null) {
|
|
157
|
+
if (sessionId !== expectedSessionId) {
|
|
158
|
+
return null; // filename must agree with every sessionId in the head
|
|
159
|
+
}
|
|
160
|
+
sawMatchingSession = true;
|
|
161
|
+
if (record.isSidechain === true)
|
|
162
|
+
isSidechain = true;
|
|
163
|
+
}
|
|
164
|
+
// First cwd-bearing record wins. Modern transcripts put cwd on a later
|
|
165
|
+
// user/assistant line; some preamble lines omit sessionId entirely.
|
|
166
|
+
if (cwd === null && typeof record.cwd === "string" && record.cwd.length > 0) {
|
|
167
|
+
try {
|
|
168
|
+
cwd = (0, config_1.canonicalize)(record.cwd);
|
|
169
|
+
}
|
|
170
|
+
catch {
|
|
171
|
+
cwd = null;
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
if (!sawMatchingSession)
|
|
176
|
+
return null;
|
|
177
|
+
return { sessionId: expectedSessionId, cwd, isSidechain };
|
|
125
178
|
}
|
|
126
179
|
catch {
|
|
127
180
|
return null;
|
|
128
181
|
}
|
|
129
182
|
finally {
|
|
130
|
-
if (fd !== undefined)
|
|
131
|
-
|
|
183
|
+
if (fd !== undefined) {
|
|
184
|
+
try {
|
|
185
|
+
fs.closeSync(fd);
|
|
186
|
+
}
|
|
187
|
+
catch {
|
|
188
|
+
// ignore
|
|
189
|
+
}
|
|
190
|
+
}
|
|
132
191
|
}
|
|
133
192
|
}
|
|
134
193
|
function statSignals(filePath) {
|
|
@@ -176,20 +235,17 @@ function discoverSessions(projectsDir = claudeProjectsDir()) {
|
|
|
176
235
|
if (!UUID_RE.test(base))
|
|
177
236
|
continue;
|
|
178
237
|
const filePath = path.join(projectDir, file.name);
|
|
179
|
-
const
|
|
180
|
-
if (!
|
|
238
|
+
const facts = readHeadSessionFacts(filePath, base);
|
|
239
|
+
if (!facts)
|
|
181
240
|
continue;
|
|
182
|
-
if (
|
|
241
|
+
if (facts.isSidechain)
|
|
183
242
|
continue;
|
|
184
|
-
const sessionId = typeof record.sessionId === "string" ? record.sessionId : null;
|
|
185
|
-
if (sessionId !== base)
|
|
186
|
-
continue; // filename must agree with the transcript
|
|
187
243
|
const signals = statSignals(filePath);
|
|
188
244
|
sessions.push({
|
|
189
245
|
path: filePath,
|
|
190
246
|
real_path: (0, config_1.canonicalize)(filePath),
|
|
191
|
-
session_id: sessionId,
|
|
192
|
-
cwd:
|
|
247
|
+
session_id: facts.sessionId,
|
|
248
|
+
cwd: facts.cwd,
|
|
193
249
|
size: signals.size,
|
|
194
250
|
inode: signals.inode,
|
|
195
251
|
birthtime_ms: signals.birthtime_ms,
|