@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.
- package/README.md +111 -35
- package/bin/dreams-codex-hook.cjs +91 -0
- package/bin/dreams.cjs +21 -0
- package/dist/auth/commands.js +67 -5
- package/dist/auth/credentials.js +101 -22
- package/dist/auth/index.js +106 -13
- package/dist/cli.js +239 -13
- package/dist/cloud/client.js +89 -5
- package/dist/cloud/upload.js +150 -70
- package/dist/development-reset.js +359 -0
- package/dist/launcher.js +311 -0
- package/dist/local/backfill.js +625 -0
- package/dist/local/bindings.js +30 -0
- package/dist/local/claude-hooks.js +169 -0
- package/dist/local/claude.js +130 -176
- package/dist/local/codex-hooks.js +235 -0
- package/dist/local/codex.js +510 -0
- package/dist/local/commands.js +153 -19
- package/dist/local/db.js +68 -0
- package/dist/local/detect.js +148 -0
- package/dist/local/discovered.js +6 -0
- package/dist/local/hooks-install.js +102 -0
- package/dist/local/leases.js +3 -1
- package/dist/local/scan.js +99 -17
- package/dist/local/source-adapter.js +8 -0
- package/dist/local/source-adapters.js +71 -0
- package/dist/local/state-lock.js +88 -0
- package/dist/local/sync-control.js +432 -0
- package/dist/local/transcript-bytes.js +204 -0
- package/dist/managed-install.js +74 -86
- package/dist/onboard.js +26 -72
- package/dist/skill.js +373 -16
- package/dist/snapshots.js +210 -0
- package/dist/suppress-sqlite-warning.js +34 -0
- package/dist/sync.js +950 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -15,50 +15,120 @@ npx @letta-ai/dreams@latest onboard --json
|
|
|
15
15
|
|
|
16
16
|
1. verifies Node >= 22.13 through the bin preflight
|
|
17
17
|
2. installs the exact CLI version under `~/.dreams/cli/<version>/`
|
|
18
|
-
3.
|
|
18
|
+
3. activates it through the stable launcher at `~/.dreams/bin/dreams`
|
|
19
19
|
4. creates or preserves the installation id in `~/.dreams/installation.json`
|
|
20
|
-
5. installs `~/.claude/skills/dreams-setup
|
|
20
|
+
5. installs `~/.claude/skills/dreams-setup/` (skill + init snapshot template)
|
|
21
21
|
6. initializes and migrates `~/.dreams/dreams.db`
|
|
22
|
-
7. returns
|
|
22
|
+
7. returns a skill handoff (`status: "ready"`) with absolute `cli_path` and skill path
|
|
23
23
|
|
|
24
|
-
It does not
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
24
|
+
It does **not** check authentication or choose login/orient next steps. The
|
|
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
|
|
|
31
|
-
- `dreams onboard [--json]` — install or repair local setup and
|
|
32
|
-
setup
|
|
33
|
-
- `dreams
|
|
34
|
-
|
|
33
|
+
- `dreams onboard [--json]` — install or repair local setup and hand off to the
|
|
34
|
+
setup skill.
|
|
35
|
+
- `dreams rollback [--json]` — reactivate the previous completed CLI version.
|
|
36
|
+
- `dreams status [--json]` — installation, active and previous CLI versions,
|
|
37
|
+
SQLite, setup skill, and authentication state.
|
|
35
38
|
- `dreams auth login [--json]` — OAuth device code sign-in (see Auth below).
|
|
36
|
-
- `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
|
|
37
40
|
(`at-let-…abcd`), never printed.
|
|
38
41
|
- `dreams auth logout [--json]` — best-effort revoke of Dreams' refresh token,
|
|
39
42
|
then clear stored credentials.
|
|
43
|
+
- `dreams dev reset local [--confirm] [--json]` — preview or remove only local
|
|
44
|
+
SQLite and blob state.
|
|
45
|
+
- `dreams dev reset identity [--confirm] [--json]` — preview or remove local
|
|
46
|
+
state, stored Dreams credentials, and the installation identity.
|
|
40
47
|
|
|
41
48
|
Local state lives in `~/.dreams/`. Requires Node >= 22.13, where built-in
|
|
42
49
|
`node:sqlite` no longer requires the `--experimental-sqlite` flag.
|
|
43
50
|
|
|
44
|
-
|
|
51
|
+
### CLI upgrades and rollback
|
|
52
|
+
|
|
53
|
+
Installed CLI versions are immutable under `~/.dreams/cli/<version>/`. The
|
|
54
|
+
atomic `~/.dreams/cli/activation.json` record selects the active version and
|
|
55
|
+
retains one previous version. The stable launcher reads that record rather than
|
|
56
|
+
pointing directly at a version.
|
|
57
|
+
|
|
58
|
+
Run `dreams rollback` to reactivate the previous completed version. The launcher
|
|
59
|
+
also rolls back automatically when an active CLI cannot reach its startup
|
|
60
|
+
readiness marker, then retries the original command once. Normal command errors
|
|
61
|
+
after readiness, including invalid arguments, authentication failures, and
|
|
62
|
+
network failures, never trigger rollback.
|
|
63
|
+
|
|
64
|
+
### Development reset
|
|
65
|
+
|
|
66
|
+
Development resets are preview-only until `--confirm` is provided. `local`
|
|
67
|
+
removes `dreams.db`, its SQLite sidecars, and `~/.dreams/blobs/`; it preserves
|
|
68
|
+
credentials and the installation identity. `identity` removes those local files
|
|
69
|
+
plus Dreams' stored credentials and `installation.json`, so the next `onboard`
|
|
70
|
+
mints a new device identity. Both scopes preserve installed CLI versions, the
|
|
71
|
+
stable launcher, the setup skill, and every Cloud resource. Confirmed resets
|
|
72
|
+
hold the same local-state lock as onboarding, auth, source, and upload commands,
|
|
73
|
+
so they fail instead of deleting files another process is using. If
|
|
74
|
+
`DREAMS_CREDENTIALS_BACKEND=file` is forced, identity reset clears the file and
|
|
75
|
+
explicitly reports that the OS keychain was not checked.
|
|
76
|
+
|
|
77
|
+
Test-workspace cleanup is intentionally not available through this local CLI.
|
|
78
|
+
It is a separate internal Cloud operation with different credentials and data
|
|
79
|
+
cleanup semantics. `dreams dev reset test-workspace` fails without deleting
|
|
80
|
+
anything.
|
|
81
|
+
|
|
82
|
+
## Local sources (Claude Code and Codex)
|
|
45
83
|
|
|
46
84
|
The CLI maintains a crash-safe local SQLite store (`~/.dreams/dreams.db`) that
|
|
47
|
-
discovers
|
|
48
|
-
they are sent to Letta Cloud.
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
- `dreams
|
|
61
|
-
|
|
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.
|
|
105
|
+
- `dreams snapshots create --type <type> --file <path> --client-request-id <id> [--workspace-id <id>] [--json]`
|
|
106
|
+
— submit a skill-authored JSON snapshot (256 KiB UTF-8 object cap) to
|
|
107
|
+
`POST /v1/dreams/workspaces/{workspaceId}/snapshots`. Workspace defaults to the
|
|
108
|
+
local binding from `source detect`.
|
|
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.
|
|
62
132
|
|
|
63
133
|
Local cursors track a `queued_through_offset` (advanced as segments are queued)
|
|
64
134
|
separately from an `acked_offset` (advanced only after durable Cloud
|
|
@@ -96,6 +166,12 @@ masks them. `DREAMS_CREDENTIALS_BACKEND=file|keychain` forces a backend
|
|
|
96
166
|
when it is missing or expires within 5 minutes.
|
|
97
167
|
3. Otherwise: a structured not-authenticated error (`dreams auth login`).
|
|
98
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
|
+
|
|
99
175
|
**Refresh rotation.** Refreshes use `refresh_token_mode: "new"`: the server
|
|
100
176
|
always returns a *new* refresh token that revokes the old one for this
|
|
101
177
|
(user, client, device). The CLI persists the rotated token immediately and
|
|
@@ -127,20 +203,20 @@ npm test # build + node:test suite (uses a temp HOME)
|
|
|
127
203
|
## Testing the npx bootstrap via tarball (no publish)
|
|
128
204
|
|
|
129
205
|
```sh
|
|
130
|
-
npm pack # produces letta-ai-dreams-0.0.
|
|
206
|
+
npm pack # produces letta-ai-dreams-0.0.3.tgz
|
|
131
207
|
cd "$(mktemp -d)"
|
|
132
|
-
npx -y -p /absolute/path/to/dreams/letta-ai-dreams-0.0.
|
|
208
|
+
npx -y -p /absolute/path/to/dreams/letta-ai-dreams-0.0.3.tgz dreams onboard
|
|
133
209
|
```
|
|
134
210
|
|
|
135
211
|
(The `-p <tarball> dreams` form is required: a bare `npx <tarball>` treats the
|
|
136
212
|
path as a script to execute, not a package to install.)
|
|
137
213
|
|
|
138
214
|
`onboard` writes to `~/.dreams/` and installs
|
|
139
|
-
`~/.claude/skills/dreams-setup
|
|
140
|
-
for a sandboxed run.
|
|
215
|
+
`~/.claude/skills/dreams-setup/` (including the init snapshot template). Set
|
|
216
|
+
`HOME` to a temporary directory for a sandboxed run.
|
|
141
217
|
|
|
142
218
|
## Deferred
|
|
143
219
|
|
|
144
|
-
-
|
|
145
|
-
-
|
|
146
|
-
-
|
|
220
|
+
- Shared-memory repository generation and review
|
|
221
|
+
- Hook installation and default Dream schedule creation
|
|
222
|
+
- `dreams backfill start` (skill optional background history import)
|
|
@@ -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
|
@@ -7,8 +7,8 @@
|
|
|
7
7
|
* {"status":"authenticated",…} object once the user approves in the browser.
|
|
8
8
|
*/
|
|
9
9
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
10
|
-
exports.commandAuthLogin = commandAuthLogin;
|
|
11
10
|
exports.renderAuthStatus = renderAuthStatus;
|
|
11
|
+
exports.commandAuthLogin = commandAuthLogin;
|
|
12
12
|
exports.commandAuthStatus = commandAuthStatus;
|
|
13
13
|
exports.commandAuthLogout = commandAuthLogout;
|
|
14
14
|
const browser_1 = require("./browser");
|
|
@@ -17,12 +17,13 @@ const index_1 = require("./index");
|
|
|
17
17
|
const credentials_1 = require("./credentials");
|
|
18
18
|
const oauth_refresh_1 = require("./oauth-refresh");
|
|
19
19
|
const oauth_1 = require("./oauth");
|
|
20
|
+
const state_lock_1 = require("../local/state-lock");
|
|
20
21
|
/** `auth login` short-circuits when the stored token is valid for >7 days. */
|
|
21
22
|
const REAUTH_MARGIN_MS = 7 * 24 * 60 * 60 * 1000;
|
|
22
23
|
function printJsonLine(value) {
|
|
23
24
|
console.log(JSON.stringify(value));
|
|
24
25
|
}
|
|
25
|
-
async function
|
|
26
|
+
async function commandAuthLoginUnlocked(json) {
|
|
26
27
|
const stored = (0, credentials_1.loadCredentials)();
|
|
27
28
|
if (stored && stored.credentials.access_token && !(0, credentials_1.expiresWithin)(stored.credentials, REAUTH_MARGIN_MS)) {
|
|
28
29
|
if (json) {
|
|
@@ -96,22 +97,42 @@ async function commandAuthLogin(json) {
|
|
|
96
97
|
}
|
|
97
98
|
}
|
|
98
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
|
+
});
|
|
99
107
|
if (!info.authenticated) {
|
|
100
108
|
return [
|
|
101
109
|
`Not authenticated — run \`dreams auth login\` to connect this device.`,
|
|
102
110
|
``,
|
|
103
111
|
` Device id: ${info.device_id}`,
|
|
112
|
+
``,
|
|
113
|
+
`Credentials discovered:`,
|
|
114
|
+
...candidateLines,
|
|
115
|
+
``,
|
|
116
|
+
`Hooks: ${info.hooks.note}`,
|
|
104
117
|
].join("\n");
|
|
105
118
|
}
|
|
106
119
|
return [
|
|
107
120
|
`Authenticated via ${info.source}`,
|
|
121
|
+
info.active ? ` Why: ${info.active.reason}` : ``,
|
|
108
122
|
``,
|
|
109
123
|
` Access token: ${info.access_token ?? "(none — will refresh on next use)"}`,
|
|
110
124
|
` Expires at: ${info.expires_at ?? "(n/a — LETTA_API_KEY)"}`,
|
|
111
125
|
` Device id: ${info.device_id}`,
|
|
112
|
-
|
|
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");
|
|
113
134
|
}
|
|
114
|
-
function
|
|
135
|
+
function commandAuthStatusUnlocked(json) {
|
|
115
136
|
const info = (0, index_1.authStatusInfo)();
|
|
116
137
|
if (json) {
|
|
117
138
|
console.log(JSON.stringify(info, null, 2));
|
|
@@ -121,7 +142,7 @@ function commandAuthStatus(json) {
|
|
|
121
142
|
}
|
|
122
143
|
return 0;
|
|
123
144
|
}
|
|
124
|
-
async function
|
|
145
|
+
async function commandAuthLogoutUnlocked(json) {
|
|
125
146
|
const stored = (0, credentials_1.loadCredentials)();
|
|
126
147
|
if (!stored) {
|
|
127
148
|
if (json)
|
|
@@ -139,3 +160,44 @@ async function commandAuthLogout(json) {
|
|
|
139
160
|
console.log("Logged out: refresh token revoked and local credentials cleared.");
|
|
140
161
|
return 0;
|
|
141
162
|
}
|
|
163
|
+
function stateBusy(json) {
|
|
164
|
+
const message = "Another Dreams command is using local state. Try again after it finishes.";
|
|
165
|
+
if (json)
|
|
166
|
+
printJsonLine({ status: "error", code: "local_state_busy", message });
|
|
167
|
+
else
|
|
168
|
+
process.stderr.write(message + "\n");
|
|
169
|
+
return 1;
|
|
170
|
+
}
|
|
171
|
+
async function commandAuthLogin(json) {
|
|
172
|
+
const lock = (0, state_lock_1.acquireLocalStateLock)();
|
|
173
|
+
if (lock === null)
|
|
174
|
+
return stateBusy(json);
|
|
175
|
+
try {
|
|
176
|
+
return await commandAuthLoginUnlocked(json);
|
|
177
|
+
}
|
|
178
|
+
finally {
|
|
179
|
+
(0, state_lock_1.releaseLocalStateLock)(lock);
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
function commandAuthStatus(json) {
|
|
183
|
+
const lock = (0, state_lock_1.acquireLocalStateLock)();
|
|
184
|
+
if (lock === null)
|
|
185
|
+
return stateBusy(json);
|
|
186
|
+
try {
|
|
187
|
+
return commandAuthStatusUnlocked(json);
|
|
188
|
+
}
|
|
189
|
+
finally {
|
|
190
|
+
(0, state_lock_1.releaseLocalStateLock)(lock);
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
async function commandAuthLogout(json) {
|
|
194
|
+
const lock = (0, state_lock_1.acquireLocalStateLock)();
|
|
195
|
+
if (lock === null)
|
|
196
|
+
return stateBusy(json);
|
|
197
|
+
try {
|
|
198
|
+
return await commandAuthLogoutUnlocked(json);
|
|
199
|
+
}
|
|
200
|
+
finally {
|
|
201
|
+
(0, state_lock_1.releaseLocalStateLock)(lock);
|
|
202
|
+
}
|
|
203
|
+
}
|
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;
|
|
@@ -108,15 +109,26 @@ function keychainRead(account) {
|
|
|
108
109
|
return null; // no entry, or keychain unusable — fall through to file
|
|
109
110
|
}
|
|
110
111
|
}
|
|
111
|
-
function keychainDelete(account) {
|
|
112
|
+
function keychainDelete(account, strict) {
|
|
112
113
|
const ctor = keyringCtor();
|
|
113
|
-
if (!ctor)
|
|
114
|
-
|
|
114
|
+
if (!ctor) {
|
|
115
|
+
if (backendOverride() === "file")
|
|
116
|
+
return "not_checked_file_backend";
|
|
117
|
+
if (strict) {
|
|
118
|
+
throw new Error("Could not verify OS keychain cleanup because @napi-rs/keyring is unavailable. " +
|
|
119
|
+
"Rerun where keychain support is available.");
|
|
120
|
+
}
|
|
121
|
+
return "unavailable";
|
|
122
|
+
}
|
|
115
123
|
try {
|
|
116
124
|
new ctor(KEYCHAIN_SERVICE, account).deleteCredential();
|
|
125
|
+
return "cleared";
|
|
117
126
|
}
|
|
118
|
-
catch {
|
|
119
|
-
|
|
127
|
+
catch (error) {
|
|
128
|
+
if (strict) {
|
|
129
|
+
throw new Error(`Could not clear Dreams credentials from the OS keychain: ${error instanceof Error ? error.message : String(error)}`);
|
|
130
|
+
}
|
|
131
|
+
return "unavailable";
|
|
120
132
|
}
|
|
121
133
|
}
|
|
122
134
|
// --- file backend ------------------------------------------------------------
|
|
@@ -155,6 +167,23 @@ function warnKeychainUnavailable() {
|
|
|
155
167
|
process.stderr.write("Warning: OS keychain unavailable — storing credentials in ~/.dreams/credentials.json (mode 0600).\n");
|
|
156
168
|
}
|
|
157
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
|
+
}
|
|
158
187
|
/**
|
|
159
188
|
* Load stored credentials: keychain first, then the file fallback (covers
|
|
160
189
|
* installs where an earlier save fell back to the file).
|
|
@@ -162,25 +191,66 @@ function warnKeychainUnavailable() {
|
|
|
162
191
|
function loadCredentials() {
|
|
163
192
|
const refresh = keychainRead(REFRESH_ACCOUNT);
|
|
164
193
|
if (refresh) {
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
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 };
|
|
175
218
|
}
|
|
176
|
-
|
|
177
|
-
|
|
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
|
+
}
|
|
178
243
|
}
|
|
179
244
|
}
|
|
180
|
-
|
|
245
|
+
catch {
|
|
246
|
+
keychain = { status: "unavailable", credentials: null };
|
|
247
|
+
}
|
|
181
248
|
}
|
|
182
249
|
const fromFile = fileRead();
|
|
183
|
-
|
|
250
|
+
const file = fromFile
|
|
251
|
+
? { status: "present", credentials: fromFile }
|
|
252
|
+
: { status: "absent", credentials: null };
|
|
253
|
+
return { keychain, file };
|
|
184
254
|
}
|
|
185
255
|
/** Persist credentials: keychain if usable, else the 0600 file (with a warning). */
|
|
186
256
|
function saveCredentials(credentials) {
|
|
@@ -202,10 +272,19 @@ function saveCredentials(credentials) {
|
|
|
202
272
|
fileWrite(credentials);
|
|
203
273
|
}
|
|
204
274
|
/** Clear both backends (login/logout/invalid_grant paths). */
|
|
205
|
-
function clearCredentials() {
|
|
206
|
-
|
|
207
|
-
keychainDelete(
|
|
275
|
+
function clearCredentials(options = {}) {
|
|
276
|
+
const strict = options.strictKeychain === true;
|
|
277
|
+
const refresh = keychainDelete(REFRESH_ACCOUNT, strict);
|
|
278
|
+
const access = keychainDelete(ACCESS_ACCOUNT, strict);
|
|
208
279
|
fs.rmSync(credentialsFilePath(), { force: true });
|
|
280
|
+
return {
|
|
281
|
+
file: "cleared",
|
|
282
|
+
keychain: refresh === "not_checked_file_backend" || access === "not_checked_file_backend"
|
|
283
|
+
? "not_checked_file_backend"
|
|
284
|
+
: refresh === "cleared" && access === "cleared"
|
|
285
|
+
? "cleared"
|
|
286
|
+
: "unavailable",
|
|
287
|
+
};
|
|
209
288
|
}
|
|
210
289
|
function expiresWithin(credentials, marginMs = exports.EXPIRY_MARGIN_MS) {
|
|
211
290
|
const at = Date.parse(credentials.expires_at);
|