@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/README.md
CHANGED
|
@@ -22,9 +22,11 @@ npx @letta-ai/dreams@latest onboard --json
|
|
|
22
22
|
7. returns a skill handoff (`status: "ready"`) with absolute `cli_path` and skill path
|
|
23
23
|
|
|
24
24
|
It does **not** check authentication or choose login/orient next steps. The
|
|
25
|
-
installed `dreams-setup` skill owns sequencing (authenticate → detect
|
|
26
|
-
init snapshot →
|
|
27
|
-
|
|
25
|
+
installed `dreams-setup` skill owns sequencing (authenticate → detect sources →
|
|
26
|
+
init snapshot → approve directories + install consented hooks → optional
|
|
27
|
+
background history import). SessionStart hooks call `dreams wake`; Stop hooks
|
|
28
|
+
call `dreams sync`. Repeated `onboard` calls are safe and repair local setup
|
|
29
|
+
only.
|
|
28
30
|
|
|
29
31
|
### Commands
|
|
30
32
|
|
|
@@ -34,7 +36,7 @@ and repair local setup only.
|
|
|
34
36
|
- `dreams status [--json]` — installation, active and previous CLI versions,
|
|
35
37
|
SQLite, setup skill, and authentication state.
|
|
36
38
|
- `dreams auth login [--json]` — OAuth device code sign-in (see Auth below).
|
|
37
|
-
- `dreams auth status [--json]` — authentication state; tokens are masked
|
|
39
|
+
- `dreams auth status [--json]` — authentication state; lists every discovered credential (`env` / `keychain` / `file`), which won and why, plus a hooks note that live sync inherits the harness process env; tokens are masked
|
|
38
40
|
(`at-let-…abcd`), never printed.
|
|
39
41
|
- `dreams auth logout [--json]` — best-effort revoke of Dreams' refresh token,
|
|
40
42
|
then clear stored credentials.
|
|
@@ -77,42 +79,56 @@ It is a separate internal Cloud operation with different credentials and data
|
|
|
77
79
|
cleanup semantics. `dreams dev reset test-workspace` fails without deleting
|
|
78
80
|
anything.
|
|
79
81
|
|
|
80
|
-
## Local sources (Claude Code)
|
|
82
|
+
## Local sources (Claude Code and Codex)
|
|
81
83
|
|
|
82
84
|
The CLI maintains a crash-safe local SQLite store (`~/.dreams/dreams.db`) that
|
|
83
|
-
discovers
|
|
84
|
-
they are sent to Letta Cloud.
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
- `dreams source
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
85
|
+
discovers coding-agent transcripts and prepares immutable upload segments before
|
|
86
|
+
they are sent to Letta Cloud. Harness behavior is selected with
|
|
87
|
+
`--source <type>` (`claude-code` default; `codex`). One invocation always
|
|
88
|
+
targets a single source — it never processes all sources at once.
|
|
89
|
+
|
|
90
|
+
- `dreams source detect [--source <type>] [--json]` — detect a harness from
|
|
91
|
+
filesystem facts only (`~/.claude` or `~/.codex`; no transcript reads) and
|
|
92
|
+
register an installation-scoped Cloud source in status `detected`. Idempotent;
|
|
93
|
+
does not scan or upload.
|
|
94
|
+
- `dreams source approve-dir <path> [--json]` (alias: `approve-root`) — approve a
|
|
95
|
+
work directory. Sources are **deny-by-default**: no transcript bytes are queued
|
|
96
|
+
until their working directory is under an approved folder (`.dreamignore`
|
|
97
|
+
still applies).
|
|
98
|
+
- `dreams source scan [--source <type>] [--json]` — discover sessions for the
|
|
99
|
+
selected source (Claude: `~/.claude/projects/...`; Codex:
|
|
100
|
+
`~/.codex/sessions/YYYY/MM/DD/rollout-*.jsonl`), read only complete new lines
|
|
101
|
+
after the durable cursor, and enqueue deterministic upload segments. Repeated
|
|
102
|
+
scans are idempotent.
|
|
103
|
+
- `dreams source list [--json]` — show the DB path, approved directories, sources,
|
|
104
|
+
and the pending upload queue.
|
|
99
105
|
- `dreams snapshots create --type <type> --file <path> --client-request-id <id> [--workspace-id <id>] [--json]`
|
|
100
106
|
— submit a skill-authored JSON snapshot (256 KiB UTF-8 object cap) to
|
|
101
107
|
`POST /v1/dreams/workspaces/{workspaceId}/snapshots`. Workspace defaults to the
|
|
102
108
|
local binding from `source detect`.
|
|
103
|
-
- `dreams
|
|
104
|
-
|
|
105
|
-
`
|
|
106
|
-
- `dreams
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
- `dreams sync
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
109
|
+
- `dreams hooks install [--source <type>] [--dry-run] [--json]` — install or
|
|
110
|
+
preview Claude Code / Codex live-sync hooks. SessionStart → `wake`, Stop →
|
|
111
|
+
`sync`. Prefer `--dry-run` for consent before writing.
|
|
112
|
+
- `dreams wake [--source <type>] [--json]` — SessionStart entrypoint: quiet
|
|
113
|
+
managed CLI/skill repair, then force-kick the sync worker for pending
|
|
114
|
+
live/backfill drain (no stdin session hint). Always exits 0 for hook safety.
|
|
115
|
+
- `dreams sync [--source <type>] [--force] [--json]` — Stop / live-turn
|
|
116
|
+
scheduler for one source: records a durable sync trigger (optional stdin
|
|
117
|
+
`session_id`) and runs a detached worker that live-scans tracked sessions and
|
|
118
|
+
uploads with a per-pass budget (~3h cadence unless `--force` or retry-due).
|
|
119
|
+
- `dreams sync pause|resume [--json]` — global pause makes hook wakes no-ops;
|
|
120
|
+
resume clears pause and requests one immediate sync (not per-source).
|
|
121
|
+
- `dreams sync status [--source <type>] [--json]` — per-source diagnostics
|
|
122
|
+
(`effective_state`, cadence/retry, worker). Codex status also reports
|
|
123
|
+
discovery health (`discovery.status` / reason codes) when rollout formats
|
|
124
|
+
drift.
|
|
125
|
+
- `dreams backfill start --since <RFC3339> [--source <type>] [--json]` — approve
|
|
126
|
+
a frozen `[since, until=now]` historical window for one source and run one
|
|
127
|
+
bounded local scan/queue pass (session frontier, newest unfinished sessions
|
|
128
|
+
first; no upload). Policies are source-isolated. Re-running the same active
|
|
129
|
+
`--since` is idempotent; a different active policy fails closed. After
|
|
130
|
+
`backfill start`, kick with `dreams wake --source …` so draining does not wait
|
|
131
|
+
for the next session.
|
|
116
132
|
|
|
117
133
|
Local cursors track a `queued_through_offset` (advanced as segments are queued)
|
|
118
134
|
separately from an `acked_offset` (advanced only after durable Cloud
|
|
@@ -150,6 +166,12 @@ masks them. `DREAMS_CREDENTIALS_BACKEND=file|keychain` forces a backend
|
|
|
150
166
|
when it is missing or expires within 5 minutes.
|
|
151
167
|
3. Otherwise: a structured not-authenticated error (`dreams auth login`).
|
|
152
168
|
|
|
169
|
+
`dreams auth status --json` reports that inventory explicitly: top-level
|
|
170
|
+
`source` / `access_token` remain the winner for this process; `credentials[]`
|
|
171
|
+
lists each backend with `present`, `selected`, and `reason`; `active` restates
|
|
172
|
+
the winner; `hooks` notes that coding-agent hooks inherit the harness
|
|
173
|
+
environment (a harness-set `LETTA_API_KEY` shadows stored OAuth).
|
|
174
|
+
|
|
153
175
|
**Refresh rotation.** Refreshes use `refresh_token_mode: "new"`: the server
|
|
154
176
|
always returns a *new* refresh token that revokes the old one for this
|
|
155
177
|
(user, client, device). The CLI persists the rotated token immediately and
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Codex SessionStart/Stop hook launcher for Dreams.
|
|
4
|
+
*
|
|
5
|
+
* Codex Stop requires exactly one valid JSON object on stdout when exiting 0.
|
|
6
|
+
* Dreams CLI JSON must never leak into that stream. This wrapper:
|
|
7
|
+
* 1. Forwards hook stdin to `dreams <wake|sync> --source codex`
|
|
8
|
+
* 2. Suppresses all CLI stdout/stderr
|
|
9
|
+
* 3. Always prints `{}` and exits 0 (scheduling failures must not interrupt Codex)
|
|
10
|
+
*
|
|
11
|
+
* Usage:
|
|
12
|
+
* node dreams-codex-hook.cjs <cli_path> [wake|sync]
|
|
13
|
+
* Default mode is `sync` (Stop / live turn). SessionStart should pass `wake`.
|
|
14
|
+
*/
|
|
15
|
+
"use strict";
|
|
16
|
+
|
|
17
|
+
var spawnSync = require("node:child_process").spawnSync;
|
|
18
|
+
var fs = require("node:fs");
|
|
19
|
+
var path = require("node:path");
|
|
20
|
+
|
|
21
|
+
function readStdinSync() {
|
|
22
|
+
try {
|
|
23
|
+
return fs.readFileSync(0);
|
|
24
|
+
} catch {
|
|
25
|
+
return Buffer.alloc(0);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function quoteCmdArg(value) {
|
|
30
|
+
return '"' + String(value).replace(/"/g, '""') + '"';
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function resolveMode(raw) {
|
|
34
|
+
if (raw === "wake" || raw === "sync") return raw;
|
|
35
|
+
return "sync";
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Resolve how to invoke the durable Dreams CLI.
|
|
40
|
+
* On Windows the stable launcher is `dreams.cmd`, which cannot be spawnSync'd
|
|
41
|
+
* with shell:false. Prefer the sibling Node controller (`launcher.cjs`) when
|
|
42
|
+
* present (managed install layout); otherwise fall back to ComSpec with
|
|
43
|
+
* explicitly quoted paths so spaces work.
|
|
44
|
+
*/
|
|
45
|
+
function resolveInvocation(cliPath, mode) {
|
|
46
|
+
var args = [mode, "--source", "codex"];
|
|
47
|
+
if (process.platform === "win32" && /\.(cmd|bat)$/i.test(cliPath)) {
|
|
48
|
+
var sibling = path.join(path.dirname(cliPath), "launcher.cjs");
|
|
49
|
+
try {
|
|
50
|
+
if (fs.statSync(sibling).isFile()) {
|
|
51
|
+
return { command: process.execPath, args: [sibling].concat(args), shell: false };
|
|
52
|
+
}
|
|
53
|
+
} catch {
|
|
54
|
+
// fall through to ComSpec
|
|
55
|
+
}
|
|
56
|
+
var cmdline = [quoteCmdArg(cliPath)].concat(args).join(" ");
|
|
57
|
+
return {
|
|
58
|
+
command: process.env.ComSpec || "cmd.exe",
|
|
59
|
+
args: ["/d", "/s", "/c", cmdline],
|
|
60
|
+
shell: false,
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
return { command: cliPath, args: args, shell: false };
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function main() {
|
|
67
|
+
var cliPath = process.argv[2];
|
|
68
|
+
var mode = resolveMode(process.argv[3]);
|
|
69
|
+
var stdin = mode === "wake" ? Buffer.alloc(0) : readStdinSync();
|
|
70
|
+
|
|
71
|
+
if (typeof cliPath === "string" && cliPath.trim() !== "") {
|
|
72
|
+
try {
|
|
73
|
+
var invocation = resolveInvocation(cliPath, mode);
|
|
74
|
+
spawnSync(invocation.command, invocation.args, {
|
|
75
|
+
input: stdin,
|
|
76
|
+
encoding: "buffer",
|
|
77
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
78
|
+
windowsHide: true,
|
|
79
|
+
shell: invocation.shell,
|
|
80
|
+
env: process.env,
|
|
81
|
+
});
|
|
82
|
+
} catch {
|
|
83
|
+
// Tolerate missing launcher / spawn failures — hook must not interrupt Codex.
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
process.stdout.write("{}\n");
|
|
88
|
+
process.exit(0);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
main();
|
package/bin/dreams.cjs
CHANGED
|
@@ -16,4 +16,25 @@ if (major < REQUIRED_MAJOR || (major === REQUIRED_MAJOR && minor < REQUIRED_MINO
|
|
|
16
16
|
process.exit(1);
|
|
17
17
|
}
|
|
18
18
|
|
|
19
|
+
// node:sqlite still emits ExperimentalWarning on load; suppress only that noise
|
|
20
|
+
// before requiring the CLI (which imports node:sqlite).
|
|
21
|
+
(function suppressSqliteExperimentalWarning() {
|
|
22
|
+
var originalEmit = process.emit;
|
|
23
|
+
process.emit = function (event) {
|
|
24
|
+
if (event === 'warning' && arguments.length > 1) {
|
|
25
|
+
var warning = arguments[1];
|
|
26
|
+
if (
|
|
27
|
+
warning &&
|
|
28
|
+
typeof warning === 'object' &&
|
|
29
|
+
warning.name === 'ExperimentalWarning' &&
|
|
30
|
+
typeof warning.message === 'string' &&
|
|
31
|
+
/SQLite is an experimental feature|node:sqlite/i.test(warning.message)
|
|
32
|
+
) {
|
|
33
|
+
return false;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
return originalEmit.apply(process, arguments);
|
|
37
|
+
};
|
|
38
|
+
})();
|
|
39
|
+
|
|
19
40
|
require('../dist/cli.js').main();
|
package/dist/auth/commands.js
CHANGED
|
@@ -97,20 +97,40 @@ async function commandAuthLoginUnlocked(json) {
|
|
|
97
97
|
}
|
|
98
98
|
}
|
|
99
99
|
function renderAuthStatus(info) {
|
|
100
|
+
const candidateLines = info.credentials.map((c) => {
|
|
101
|
+
const mark = c.selected ? "*" : " ";
|
|
102
|
+
const detail = c.present
|
|
103
|
+
? `${c.access_token ?? "(no access token)"}` + (c.expires_at ? ` exp=${c.expires_at}` : "")
|
|
104
|
+
: "(absent)";
|
|
105
|
+
return ` ${mark} ${c.source.padEnd(9)} ${detail}\n ${c.reason}`;
|
|
106
|
+
});
|
|
100
107
|
if (!info.authenticated) {
|
|
101
108
|
return [
|
|
102
109
|
`Not authenticated — run \`dreams auth login\` to connect this device.`,
|
|
103
110
|
``,
|
|
104
111
|
` Device id: ${info.device_id}`,
|
|
112
|
+
``,
|
|
113
|
+
`Credentials discovered:`,
|
|
114
|
+
...candidateLines,
|
|
115
|
+
``,
|
|
116
|
+
`Hooks: ${info.hooks.note}`,
|
|
105
117
|
].join("\n");
|
|
106
118
|
}
|
|
107
119
|
return [
|
|
108
120
|
`Authenticated via ${info.source}`,
|
|
121
|
+
info.active ? ` Why: ${info.active.reason}` : ``,
|
|
109
122
|
``,
|
|
110
123
|
` Access token: ${info.access_token ?? "(none — will refresh on next use)"}`,
|
|
111
124
|
` Expires at: ${info.expires_at ?? "(n/a — LETTA_API_KEY)"}`,
|
|
112
125
|
` Device id: ${info.device_id}`,
|
|
113
|
-
|
|
126
|
+
``,
|
|
127
|
+
`Credentials discovered (* = selected for this process):`,
|
|
128
|
+
...candidateLines,
|
|
129
|
+
``,
|
|
130
|
+
`Hooks: ${info.hooks.note}`,
|
|
131
|
+
]
|
|
132
|
+
.filter((line) => line !== undefined)
|
|
133
|
+
.join("\n");
|
|
114
134
|
}
|
|
115
135
|
function commandAuthStatusUnlocked(json) {
|
|
116
136
|
const info = (0, index_1.authStatusInfo)();
|
package/dist/auth/credentials.js
CHANGED
|
@@ -47,6 +47,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
47
47
|
exports.EXPIRY_MARGIN_MS = exports.NotAuthenticatedError = void 0;
|
|
48
48
|
exports.credentialsFilePath = credentialsFilePath;
|
|
49
49
|
exports.loadCredentials = loadCredentials;
|
|
50
|
+
exports.probeStoredCredentialBackends = probeStoredCredentialBackends;
|
|
50
51
|
exports.saveCredentials = saveCredentials;
|
|
51
52
|
exports.clearCredentials = clearCredentials;
|
|
52
53
|
exports.expiresWithin = expiresWithin;
|
|
@@ -166,6 +167,23 @@ function warnKeychainUnavailable() {
|
|
|
166
167
|
process.stderr.write("Warning: OS keychain unavailable — storing credentials in ~/.dreams/credentials.json (mode 0600).\n");
|
|
167
168
|
}
|
|
168
169
|
// --- public API ---------------------------------------------------------------
|
|
170
|
+
function credentialsFromKeychainRaw(refresh, accessRaw) {
|
|
171
|
+
let access_token = "";
|
|
172
|
+
let expires_at = EPOCH_ISO;
|
|
173
|
+
if (accessRaw) {
|
|
174
|
+
try {
|
|
175
|
+
const parsed = JSON.parse(accessRaw);
|
|
176
|
+
if (typeof parsed.access_token === "string")
|
|
177
|
+
access_token = parsed.access_token;
|
|
178
|
+
if (typeof parsed.expires_at === "string")
|
|
179
|
+
expires_at = parsed.expires_at;
|
|
180
|
+
}
|
|
181
|
+
catch {
|
|
182
|
+
// stale/foreign entry shape — treat access token as expired
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
return { access_token, refresh_token: refresh, expires_at };
|
|
186
|
+
}
|
|
169
187
|
/**
|
|
170
188
|
* Load stored credentials: keychain first, then the file fallback (covers
|
|
171
189
|
* installs where an earlier save fell back to the file).
|
|
@@ -173,25 +191,66 @@ function warnKeychainUnavailable() {
|
|
|
173
191
|
function loadCredentials() {
|
|
174
192
|
const refresh = keychainRead(REFRESH_ACCOUNT);
|
|
175
193
|
if (refresh) {
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
194
|
+
return {
|
|
195
|
+
credentials: credentialsFromKeychainRaw(refresh, keychainRead(ACCESS_ACCOUNT)),
|
|
196
|
+
source: "keychain",
|
|
197
|
+
};
|
|
198
|
+
}
|
|
199
|
+
const fromFile = fileRead();
|
|
200
|
+
return fromFile ? { credentials: fromFile, source: "file" } : null;
|
|
201
|
+
}
|
|
202
|
+
/**
|
|
203
|
+
* Inspect each stored backend independently for `auth status` inventory.
|
|
204
|
+
* Unlike `loadCredentials()`, this does not stop after keychain wins — file is
|
|
205
|
+
* still reported when both exist. Never throws (forced-keychain misconfig →
|
|
206
|
+
* unavailable).
|
|
207
|
+
*/
|
|
208
|
+
function probeStoredCredentialBackends() {
|
|
209
|
+
let keychain;
|
|
210
|
+
if (backendOverride() === "file") {
|
|
211
|
+
keychain = { status: "not_checked", credentials: null };
|
|
212
|
+
}
|
|
213
|
+
else {
|
|
214
|
+
try {
|
|
215
|
+
const ctor = requireKeyring();
|
|
216
|
+
if (!ctor) {
|
|
217
|
+
keychain = { status: "unavailable", credentials: null };
|
|
186
218
|
}
|
|
187
|
-
|
|
188
|
-
|
|
219
|
+
else {
|
|
220
|
+
let refresh = null;
|
|
221
|
+
let accessRaw = null;
|
|
222
|
+
try {
|
|
223
|
+
refresh = new ctor(KEYCHAIN_SERVICE, REFRESH_ACCOUNT).getPassword() ?? null;
|
|
224
|
+
}
|
|
225
|
+
catch {
|
|
226
|
+
refresh = null;
|
|
227
|
+
}
|
|
228
|
+
try {
|
|
229
|
+
accessRaw = new ctor(KEYCHAIN_SERVICE, ACCESS_ACCOUNT).getPassword() ?? null;
|
|
230
|
+
}
|
|
231
|
+
catch {
|
|
232
|
+
accessRaw = null;
|
|
233
|
+
}
|
|
234
|
+
if (refresh) {
|
|
235
|
+
keychain = {
|
|
236
|
+
status: "present",
|
|
237
|
+
credentials: credentialsFromKeychainRaw(refresh, accessRaw),
|
|
238
|
+
};
|
|
239
|
+
}
|
|
240
|
+
else {
|
|
241
|
+
keychain = { status: "absent", credentials: null };
|
|
242
|
+
}
|
|
189
243
|
}
|
|
190
244
|
}
|
|
191
|
-
|
|
245
|
+
catch {
|
|
246
|
+
keychain = { status: "unavailable", credentials: null };
|
|
247
|
+
}
|
|
192
248
|
}
|
|
193
249
|
const fromFile = fileRead();
|
|
194
|
-
|
|
250
|
+
const file = fromFile
|
|
251
|
+
? { status: "present", credentials: fromFile }
|
|
252
|
+
: { status: "absent", credentials: null };
|
|
253
|
+
return { keychain, file };
|
|
195
254
|
}
|
|
196
255
|
/** Persist credentials: keychain if usable, else the 0600 file (with a warning). */
|
|
197
256
|
function saveCredentials(credentials) {
|
package/dist/auth/index.js
CHANGED
|
@@ -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
|
-
|
|
46
|
-
|
|
47
|
-
|
|
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
|
-
|
|
50
|
-
expires_at: null,
|
|
51
|
-
device_id,
|
|
93
|
+
reason: "LETTA_API_KEY is set and takes highest precedence",
|
|
52
94
|
};
|
|
53
95
|
}
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
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:
|
|
60
|
-
source:
|
|
61
|
-
access_token:
|
|
62
|
-
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
|
}
|