@letta-ai/dreams 0.0.1
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 +146 -0
- package/bin/dreams.cjs +19 -0
- package/dist/auth/browser.js +43 -0
- package/dist/auth/commands.js +141 -0
- package/dist/auth/credentials.js +222 -0
- package/dist/auth/index.js +65 -0
- package/dist/auth/lock.js +92 -0
- package/dist/auth/oauth-refresh.js +68 -0
- package/dist/auth/oauth.js +193 -0
- package/dist/cli.js +149 -0
- package/dist/cloud/client.js +223 -0
- package/dist/cloud/upload.js +384 -0
- package/dist/local/blobs.js +190 -0
- package/dist/local/claude.js +315 -0
- package/dist/local/commands.js +160 -0
- package/dist/local/config.js +126 -0
- package/dist/local/db.js +265 -0
- package/dist/local/dreamignore.js +124 -0
- package/dist/local/ids.js +78 -0
- package/dist/local/leases.js +117 -0
- package/dist/local/scan.js +475 -0
- package/dist/managed-install.js +200 -0
- package/dist/onboard.js +131 -0
- package/dist/skill.js +138 -0
- package/dist/store.js +133 -0
- package/dist/version.js +48 -0
- package/package.json +32 -0
package/README.md
ADDED
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
# @letta-ai/dreams
|
|
2
|
+
|
|
3
|
+
CLI for **Dream** — Letta's shared organizational memory built from
|
|
4
|
+
coding-agent transcripts.
|
|
5
|
+
|
|
6
|
+
## Setup
|
|
7
|
+
|
|
8
|
+
Start setup from a coding agent with:
|
|
9
|
+
|
|
10
|
+
```sh
|
|
11
|
+
npx @letta-ai/dreams@latest onboard --json
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
`onboard`:
|
|
15
|
+
|
|
16
|
+
1. verifies Node >= 22.13 through the bin preflight
|
|
17
|
+
2. installs the exact CLI version under `~/.dreams/cli/<version>/`
|
|
18
|
+
3. creates or preserves the stable launcher at `~/.dreams/bin/dreams`
|
|
19
|
+
4. creates or preserves the installation id in `~/.dreams/installation.json`
|
|
20
|
+
5. installs `~/.claude/skills/dreams-setup/SKILL.md`
|
|
21
|
+
6. initializes and migrates `~/.dreams/dreams.db`
|
|
22
|
+
7. returns the next agent action based on authentication state
|
|
23
|
+
|
|
24
|
+
It does not start login or `orient` itself. An unauthenticated response directs
|
|
25
|
+
the agent to run `auth login` and then rerun `onboard`; an authenticated
|
|
26
|
+
response directs the agent to run `orient`. Repeated calls are safe and return
|
|
27
|
+
the same action until authentication state changes.
|
|
28
|
+
|
|
29
|
+
### Commands
|
|
30
|
+
|
|
31
|
+
- `dreams onboard [--json]` — install or repair local setup and return the next
|
|
32
|
+
setup action.
|
|
33
|
+
- `dreams status [--json]` — installation, durable CLI, SQLite, setup skill,
|
|
34
|
+
and authentication state.
|
|
35
|
+
- `dreams auth login [--json]` — OAuth device code sign-in (see Auth below).
|
|
36
|
+
- `dreams auth status [--json]` — authentication state; tokens are masked
|
|
37
|
+
(`at-let-…abcd`), never printed.
|
|
38
|
+
- `dreams auth logout [--json]` — best-effort revoke of Dreams' refresh token,
|
|
39
|
+
then clear stored credentials.
|
|
40
|
+
|
|
41
|
+
Local state lives in `~/.dreams/`. Requires Node >= 22.13, where built-in
|
|
42
|
+
`node:sqlite` no longer requires the `--experimental-sqlite` flag.
|
|
43
|
+
|
|
44
|
+
## Local sources (Claude Code)
|
|
45
|
+
|
|
46
|
+
The CLI maintains a crash-safe local SQLite store (`~/.dreams/dreams.db`) that
|
|
47
|
+
discovers Claude Code transcripts and prepares immutable upload segments before
|
|
48
|
+
they are sent to Letta Cloud.
|
|
49
|
+
|
|
50
|
+
- `dreams source approve-root <path> [--json]` — approve a work root. Sources
|
|
51
|
+
are **deny-by-default**: no transcript bytes are queued until their working
|
|
52
|
+
directory is under an approved root.
|
|
53
|
+
- `dreams source scan [--json]` — discover top-level Claude Code sessions
|
|
54
|
+
(`~/.claude/projects/<project>/<sessionId>.jsonl`), read only complete new
|
|
55
|
+
lines after the durable cursor, and enqueue deterministic upload segments.
|
|
56
|
+
Repeated scans are idempotent; `.dreamignore` and unapproved roots are honored
|
|
57
|
+
before anything is enqueued.
|
|
58
|
+
- `dreams source list [--json]` — show the DB path, approved roots, sources, and
|
|
59
|
+
the pending upload queue.
|
|
60
|
+
- `dreams upload [--json]` — authenticate, register the source, and upload due
|
|
61
|
+
queue entries to Letta Cloud.
|
|
62
|
+
|
|
63
|
+
Local cursors track a `queued_through_offset` (advanced as segments are queued)
|
|
64
|
+
separately from an `acked_offset` (advanced only after durable Cloud
|
|
65
|
+
acceptance, in a later step).
|
|
66
|
+
|
|
67
|
+
## Auth
|
|
68
|
+
|
|
69
|
+
`dreams auth login` runs Letta's **OAuth 2.0 Device Code Flow** against
|
|
70
|
+
`https://app.letta.com` (override with `LETTA_AUTH_BASE_URL`; the API host is
|
|
71
|
+
`https://api.letta.com`, override with `LETTA_BASE_URL`):
|
|
72
|
+
|
|
73
|
+
1. The CLI requests a device + user code and prints a verification URL.
|
|
74
|
+
2. The user approves in the browser; the CLI polls the token endpoint
|
|
75
|
+
(handling `authorization_pending`, `slow_down`, `access_denied`,
|
|
76
|
+
`expired_token`) until it receives an access token (`at-let-…`, ~30 days)
|
|
77
|
+
and a refresh token (`rt-let-…`, 1 year).
|
|
78
|
+
3. In `--json` mode, `auth login` emits **two JSON lines** on stdout: first
|
|
79
|
+
`{"status":"awaiting_approval",…}` (show the user the URL/code), then
|
|
80
|
+
`{"status":"authenticated",…}` after approval.
|
|
81
|
+
|
|
82
|
+
**Storage.** Credentials go to the OS keychain via `@napi-rs/keyring`
|
|
83
|
+
(an *optional* dependency — the CLI keeps zero hard runtime deps), service
|
|
84
|
+
`letta-dreams`, accounts `refresh-token` and `access-token`. When the keychain
|
|
85
|
+
is unavailable, the CLI falls back to `~/.dreams/credentials.json`, written
|
|
86
|
+
atomically with mode 0600 (a one-line stderr warning notes the fallback).
|
|
87
|
+
Tokens are never written to local setup state or logs, and status output always
|
|
88
|
+
masks them. `DREAMS_CREDENTIALS_BACKEND=file|keychain` forces a backend
|
|
89
|
+
(tests use `file`).
|
|
90
|
+
|
|
91
|
+
**Precedence** when resolving an auth header (`getAuthHeader()` in
|
|
92
|
+
`src/auth/index.ts`):
|
|
93
|
+
|
|
94
|
+
1. `LETTA_API_KEY` env var — used verbatim, never refreshed, never stored.
|
|
95
|
+
2. Stored Dreams credentials — the access token is refreshed automatically
|
|
96
|
+
when it is missing or expires within 5 minutes.
|
|
97
|
+
3. Otherwise: a structured not-authenticated error (`dreams auth login`).
|
|
98
|
+
|
|
99
|
+
**Refresh rotation.** Refreshes use `refresh_token_mode: "new"`: the server
|
|
100
|
+
always returns a *new* refresh token that revokes the old one for this
|
|
101
|
+
(user, client, device). The CLI persists the rotated token immediately and
|
|
102
|
+
atomically before using the new access token, singleflights concurrent
|
|
103
|
+
in-process refreshes, and holds a lockfile (`~/.dreams/auth.lock`, stale
|
|
104
|
+
after 30s) so two dreams processes on one machine cannot interleave
|
|
105
|
+
rotate-then-persist. A refresh failing with `invalid_grant` (stale rotated
|
|
106
|
+
token) clears stored credentials.
|
|
107
|
+
|
|
108
|
+
**Why Dreams does not reuse letta-code's keychain entries.** Dreams shares
|
|
109
|
+
letta-code's *public client id*, but deliberately keeps its own credentials
|
|
110
|
+
(service `letta-dreams` vs `letta-code`) and its own `device_id` (the Dreams
|
|
111
|
+
`installation_id`). Rotation is scoped per (user, client, device): if both
|
|
112
|
+
CLIs shared one refresh token, each refresh by one tool would revoke the
|
|
113
|
+
token out from under the other (rotation collision). Separate device ids
|
|
114
|
+
guarantee the two token chains never collide, and `dreams auth logout` can
|
|
115
|
+
never revoke letta-code's session.
|
|
116
|
+
|
|
117
|
+
## Development
|
|
118
|
+
|
|
119
|
+
```sh
|
|
120
|
+
npm install
|
|
121
|
+
npm run dev -- onboard --json # run from source via tsx
|
|
122
|
+
npm run typecheck
|
|
123
|
+
npm run build # compile to dist/
|
|
124
|
+
npm test # build + node:test suite (uses a temp HOME)
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
## Testing the npx bootstrap via tarball (no publish)
|
|
128
|
+
|
|
129
|
+
```sh
|
|
130
|
+
npm pack # produces letta-ai-dreams-0.0.1.tgz
|
|
131
|
+
cd "$(mktemp -d)"
|
|
132
|
+
npx -y -p /absolute/path/to/dreams/letta-ai-dreams-0.0.1.tgz dreams onboard
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
(The `-p <tarball> dreams` form is required: a bare `npx <tarball>` treats the
|
|
136
|
+
path as a script to execute, not a package to install.)
|
|
137
|
+
|
|
138
|
+
`onboard` writes to `~/.dreams/` and installs
|
|
139
|
+
`~/.claude/skills/dreams-setup/SKILL.md`. Set `HOME` to a temporary directory
|
|
140
|
+
for a sandboxed run.
|
|
141
|
+
|
|
142
|
+
## Deferred
|
|
143
|
+
|
|
144
|
+
- `orient` and its Cloud-backed setup actions
|
|
145
|
+
- Shared-memory generation and review
|
|
146
|
+
- Automated source configuration and historical backfill
|
package/bin/dreams.cjs
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Preflight Node version check. This shim must stay ES5-safe so it can run
|
|
3
|
+
// (and fail gracefully) on old Node versions before any modern syntax loads.
|
|
4
|
+
// Node >= 22.13 is required: that is where node:sqlite stopped needing the
|
|
5
|
+
// --experimental-sqlite flag, which the local source store depends on.
|
|
6
|
+
var REQUIRED_MAJOR = 22;
|
|
7
|
+
var REQUIRED_MINOR = 13;
|
|
8
|
+
|
|
9
|
+
var version = process.versions.node;
|
|
10
|
+
var parts = version.split('.');
|
|
11
|
+
var major = parseInt(parts[0], 10) || 0;
|
|
12
|
+
var minor = parseInt(parts[1], 10) || 0;
|
|
13
|
+
|
|
14
|
+
if (major < REQUIRED_MAJOR || (major === REQUIRED_MAJOR && minor < REQUIRED_MINOR)) {
|
|
15
|
+
console.error('Dream requires Node 22.13+ — found v' + version + '. Please upgrade.');
|
|
16
|
+
process.exit(1);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
require('../dist/cli.js').main();
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.openCommand = openCommand;
|
|
4
|
+
exports.openBrowser = openBrowser;
|
|
5
|
+
const node_child_process_1 = require("node:child_process");
|
|
6
|
+
/** Platform command for opening a URL in the default browser. */
|
|
7
|
+
function openCommand(platform) {
|
|
8
|
+
switch (platform) {
|
|
9
|
+
case "darwin":
|
|
10
|
+
return { cmd: "open", args: [] };
|
|
11
|
+
case "win32":
|
|
12
|
+
// `start` is a cmd builtin; empty title arg guards URLs with ampersands.
|
|
13
|
+
return { cmd: "cmd", args: ["/c", "start", '""'] };
|
|
14
|
+
case "linux":
|
|
15
|
+
return { cmd: "xdg-open", args: [] };
|
|
16
|
+
default:
|
|
17
|
+
return null;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Best-effort: open the verification URL in the user's default browser.
|
|
22
|
+
* Never throws, never blocks — the printed URL is always the fallback.
|
|
23
|
+
* Suppressed by DREAMS_NO_BROWSER=1 (CI, ssh sessions, tests).
|
|
24
|
+
*/
|
|
25
|
+
function openBrowser(url) {
|
|
26
|
+
if (process.env.DREAMS_NO_BROWSER === "1")
|
|
27
|
+
return false;
|
|
28
|
+
const command = openCommand(process.platform);
|
|
29
|
+
if (!command)
|
|
30
|
+
return false;
|
|
31
|
+
try {
|
|
32
|
+
const child = (0, node_child_process_1.spawn)(command.cmd, [...command.args, url], {
|
|
33
|
+
detached: true,
|
|
34
|
+
stdio: "ignore",
|
|
35
|
+
});
|
|
36
|
+
child.on("error", () => { });
|
|
37
|
+
child.unref();
|
|
38
|
+
return true;
|
|
39
|
+
}
|
|
40
|
+
catch {
|
|
41
|
+
return false;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Command handlers for `dreams auth login|status|logout`.
|
|
4
|
+
*
|
|
5
|
+
* JSON mode of `auth login` is JSON-lines on stdout (agents drive this):
|
|
6
|
+
* one {"status":"awaiting_approval",…} object immediately, then a second
|
|
7
|
+
* {"status":"authenticated",…} object once the user approves in the browser.
|
|
8
|
+
*/
|
|
9
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
10
|
+
exports.commandAuthLogin = commandAuthLogin;
|
|
11
|
+
exports.renderAuthStatus = renderAuthStatus;
|
|
12
|
+
exports.commandAuthStatus = commandAuthStatus;
|
|
13
|
+
exports.commandAuthLogout = commandAuthLogout;
|
|
14
|
+
const browser_1 = require("./browser");
|
|
15
|
+
const store_1 = require("../store");
|
|
16
|
+
const index_1 = require("./index");
|
|
17
|
+
const credentials_1 = require("./credentials");
|
|
18
|
+
const oauth_refresh_1 = require("./oauth-refresh");
|
|
19
|
+
const oauth_1 = require("./oauth");
|
|
20
|
+
/** `auth login` short-circuits when the stored token is valid for >7 days. */
|
|
21
|
+
const REAUTH_MARGIN_MS = 7 * 24 * 60 * 60 * 1000;
|
|
22
|
+
function printJsonLine(value) {
|
|
23
|
+
console.log(JSON.stringify(value));
|
|
24
|
+
}
|
|
25
|
+
async function commandAuthLogin(json) {
|
|
26
|
+
const stored = (0, credentials_1.loadCredentials)();
|
|
27
|
+
if (stored && stored.credentials.access_token && !(0, credentials_1.expiresWithin)(stored.credentials, REAUTH_MARGIN_MS)) {
|
|
28
|
+
if (json) {
|
|
29
|
+
printJsonLine({ status: "already_authenticated", expires_at: stored.credentials.expires_at });
|
|
30
|
+
}
|
|
31
|
+
else {
|
|
32
|
+
console.log(`Already authenticated (access token valid until ${stored.credentials.expires_at}).\n` +
|
|
33
|
+
`Run \`dreams auth logout\` first to connect a different account.`);
|
|
34
|
+
}
|
|
35
|
+
return 0;
|
|
36
|
+
}
|
|
37
|
+
const installation = (0, store_1.loadOrCreateInstallation)();
|
|
38
|
+
try {
|
|
39
|
+
const device = await (0, oauth_1.requestDeviceCode)();
|
|
40
|
+
const verifyUrl = device.verification_uri_complete || device.verification_uri;
|
|
41
|
+
const opened = (0, browser_1.openBrowser)(verifyUrl);
|
|
42
|
+
if (json) {
|
|
43
|
+
printJsonLine({
|
|
44
|
+
status: "awaiting_approval",
|
|
45
|
+
verification_uri: device.verification_uri,
|
|
46
|
+
verification_uri_complete: device.verification_uri_complete ?? null,
|
|
47
|
+
user_code: device.user_code,
|
|
48
|
+
expires_in: device.expires_in,
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
else {
|
|
52
|
+
console.log([
|
|
53
|
+
``,
|
|
54
|
+
`To connect this device to Letta, open:`,
|
|
55
|
+
``,
|
|
56
|
+
` ${verifyUrl}`,
|
|
57
|
+
``,
|
|
58
|
+
`and confirm the code: ${device.user_code}`,
|
|
59
|
+
``,
|
|
60
|
+
opened ? `(opened in your browser)` : ``,
|
|
61
|
+
`Waiting for approval (expires in ${Math.round(device.expires_in / 60)} minutes)...`,
|
|
62
|
+
].join("\n"));
|
|
63
|
+
}
|
|
64
|
+
let pendingPolls = 0;
|
|
65
|
+
const token = await (0, oauth_1.pollForToken)({
|
|
66
|
+
deviceCode: device.device_code,
|
|
67
|
+
deviceId: installation.installation_id,
|
|
68
|
+
intervalSeconds: device.interval,
|
|
69
|
+
expiresInSeconds: device.expires_in,
|
|
70
|
+
onPending: () => {
|
|
71
|
+
pendingPolls += 1;
|
|
72
|
+
if (!json && pendingPolls % 6 === 0)
|
|
73
|
+
console.log("Still waiting for approval...");
|
|
74
|
+
},
|
|
75
|
+
});
|
|
76
|
+
const credentials = (0, oauth_refresh_1.credentialsFromToken)(token, "");
|
|
77
|
+
(0, credentials_1.saveCredentials)(credentials);
|
|
78
|
+
if (json) {
|
|
79
|
+
printJsonLine({ status: "authenticated", expires_at: credentials.expires_at });
|
|
80
|
+
}
|
|
81
|
+
else {
|
|
82
|
+
console.log(`\nAuthenticated. Access token expires ${credentials.expires_at}.`);
|
|
83
|
+
if (process.env.LETTA_API_KEY) {
|
|
84
|
+
process.stderr.write("Note: LETTA_API_KEY is set and takes precedence over stored credentials.\n");
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
return 0;
|
|
88
|
+
}
|
|
89
|
+
catch (error) {
|
|
90
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
91
|
+
if (json)
|
|
92
|
+
printJsonLine({ status: "error", message });
|
|
93
|
+
else
|
|
94
|
+
process.stderr.write(`Login failed: ${message}\n`);
|
|
95
|
+
return 1;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
function renderAuthStatus(info) {
|
|
99
|
+
if (!info.authenticated) {
|
|
100
|
+
return [
|
|
101
|
+
`Not authenticated — run \`dreams auth login\` to connect this device.`,
|
|
102
|
+
``,
|
|
103
|
+
` Device id: ${info.device_id}`,
|
|
104
|
+
].join("\n");
|
|
105
|
+
}
|
|
106
|
+
return [
|
|
107
|
+
`Authenticated via ${info.source}`,
|
|
108
|
+
``,
|
|
109
|
+
` Access token: ${info.access_token ?? "(none — will refresh on next use)"}`,
|
|
110
|
+
` Expires at: ${info.expires_at ?? "(n/a — LETTA_API_KEY)"}`,
|
|
111
|
+
` Device id: ${info.device_id}`,
|
|
112
|
+
].join("\n");
|
|
113
|
+
}
|
|
114
|
+
function commandAuthStatus(json) {
|
|
115
|
+
const info = (0, index_1.authStatusInfo)();
|
|
116
|
+
if (json) {
|
|
117
|
+
console.log(JSON.stringify(info, null, 2));
|
|
118
|
+
}
|
|
119
|
+
else {
|
|
120
|
+
console.log(renderAuthStatus(info));
|
|
121
|
+
}
|
|
122
|
+
return 0;
|
|
123
|
+
}
|
|
124
|
+
async function commandAuthLogout(json) {
|
|
125
|
+
const stored = (0, credentials_1.loadCredentials)();
|
|
126
|
+
if (!stored) {
|
|
127
|
+
if (json)
|
|
128
|
+
printJsonLine({ status: "not_authenticated" });
|
|
129
|
+
else
|
|
130
|
+
console.log("No stored credentials to clear.");
|
|
131
|
+
return 0;
|
|
132
|
+
}
|
|
133
|
+
// Best-effort revoke of Dreams' own refresh token (never letta-code's).
|
|
134
|
+
await (0, oauth_1.revokeToken)(stored.credentials.refresh_token);
|
|
135
|
+
(0, credentials_1.clearCredentials)();
|
|
136
|
+
if (json)
|
|
137
|
+
printJsonLine({ status: "logged_out" });
|
|
138
|
+
else
|
|
139
|
+
console.log("Logged out: refresh token revoked and local credentials cleared.");
|
|
140
|
+
return 0;
|
|
141
|
+
}
|
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Credential storage for the Dreams CLI.
|
|
4
|
+
*
|
|
5
|
+
* Primary backend: OS keychain via @napi-rs/keyring (an OPTIONAL dependency —
|
|
6
|
+
* loaded with a try/require guard), service "letta-dreams", accounts
|
|
7
|
+
* "refresh-token" and "access-token" (the latter stores JSON with expires_at).
|
|
8
|
+
* Fallback: ~/.dreams/credentials.json written atomically with mode 0600.
|
|
9
|
+
*
|
|
10
|
+
* Tokens must NEVER be written to local setup state or logs. This module never
|
|
11
|
+
* touches letta-code's keychain entries (its service name is "letta-code").
|
|
12
|
+
*/
|
|
13
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
14
|
+
if (k2 === undefined) k2 = k;
|
|
15
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
16
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
17
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
18
|
+
}
|
|
19
|
+
Object.defineProperty(o, k2, desc);
|
|
20
|
+
}) : (function(o, m, k, k2) {
|
|
21
|
+
if (k2 === undefined) k2 = k;
|
|
22
|
+
o[k2] = m[k];
|
|
23
|
+
}));
|
|
24
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
25
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
26
|
+
}) : function(o, v) {
|
|
27
|
+
o["default"] = v;
|
|
28
|
+
});
|
|
29
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
30
|
+
var ownKeys = function(o) {
|
|
31
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
32
|
+
var ar = [];
|
|
33
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
34
|
+
return ar;
|
|
35
|
+
};
|
|
36
|
+
return ownKeys(o);
|
|
37
|
+
};
|
|
38
|
+
return function (mod) {
|
|
39
|
+
if (mod && mod.__esModule) return mod;
|
|
40
|
+
var result = {};
|
|
41
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
42
|
+
__setModuleDefault(result, mod);
|
|
43
|
+
return result;
|
|
44
|
+
};
|
|
45
|
+
})();
|
|
46
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
47
|
+
exports.EXPIRY_MARGIN_MS = exports.NotAuthenticatedError = void 0;
|
|
48
|
+
exports.credentialsFilePath = credentialsFilePath;
|
|
49
|
+
exports.loadCredentials = loadCredentials;
|
|
50
|
+
exports.saveCredentials = saveCredentials;
|
|
51
|
+
exports.clearCredentials = clearCredentials;
|
|
52
|
+
exports.expiresWithin = expiresWithin;
|
|
53
|
+
exports.maskToken = maskToken;
|
|
54
|
+
const fs = __importStar(require("node:fs"));
|
|
55
|
+
const path = __importStar(require("node:path"));
|
|
56
|
+
const store_1 = require("../store");
|
|
57
|
+
class NotAuthenticatedError extends Error {
|
|
58
|
+
code = "not_authenticated";
|
|
59
|
+
constructor(message = "Not authenticated. Run `dreams auth login` to connect this device.") {
|
|
60
|
+
super(message);
|
|
61
|
+
this.name = "NotAuthenticatedError";
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
exports.NotAuthenticatedError = NotAuthenticatedError;
|
|
65
|
+
const KEYCHAIN_SERVICE = "letta-dreams";
|
|
66
|
+
const REFRESH_ACCOUNT = "refresh-token";
|
|
67
|
+
const ACCESS_ACCOUNT = "access-token";
|
|
68
|
+
/** Refresh when the access token is missing or expires within 5 minutes. */
|
|
69
|
+
exports.EXPIRY_MARGIN_MS = 5 * 60 * 1000;
|
|
70
|
+
const EPOCH_ISO = new Date(0).toISOString();
|
|
71
|
+
let cachedKeyringCtor;
|
|
72
|
+
function requireKeyring() {
|
|
73
|
+
if (cachedKeyringCtor === undefined) {
|
|
74
|
+
try {
|
|
75
|
+
// Optional dependency: may be missing (install skipped/failed) — fall back.
|
|
76
|
+
const mod = require("@napi-rs/keyring");
|
|
77
|
+
cachedKeyringCtor = mod.Entry ?? null;
|
|
78
|
+
}
|
|
79
|
+
catch {
|
|
80
|
+
cachedKeyringCtor = null;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
return cachedKeyringCtor;
|
|
84
|
+
}
|
|
85
|
+
function backendOverride() {
|
|
86
|
+
const value = process.env.DREAMS_CREDENTIALS_BACKEND;
|
|
87
|
+
return value === "file" || value === "keychain" ? value : null;
|
|
88
|
+
}
|
|
89
|
+
/** null means "do not use the keychain" (forced file backend or module absent). */
|
|
90
|
+
function keyringCtor() {
|
|
91
|
+
const override = backendOverride();
|
|
92
|
+
if (override === "file")
|
|
93
|
+
return null;
|
|
94
|
+
const ctor = requireKeyring();
|
|
95
|
+
if (override === "keychain" && ctor === null) {
|
|
96
|
+
throw new Error("DREAMS_CREDENTIALS_BACKEND=keychain but @napi-rs/keyring is unavailable");
|
|
97
|
+
}
|
|
98
|
+
return ctor;
|
|
99
|
+
}
|
|
100
|
+
function keychainRead(account) {
|
|
101
|
+
const ctor = keyringCtor();
|
|
102
|
+
if (!ctor)
|
|
103
|
+
return null;
|
|
104
|
+
try {
|
|
105
|
+
return new ctor(KEYCHAIN_SERVICE, account).getPassword() ?? null;
|
|
106
|
+
}
|
|
107
|
+
catch {
|
|
108
|
+
return null; // no entry, or keychain unusable — fall through to file
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
function keychainDelete(account) {
|
|
112
|
+
const ctor = keyringCtor();
|
|
113
|
+
if (!ctor)
|
|
114
|
+
return;
|
|
115
|
+
try {
|
|
116
|
+
new ctor(KEYCHAIN_SERVICE, account).deleteCredential();
|
|
117
|
+
}
|
|
118
|
+
catch {
|
|
119
|
+
// already gone or keychain unusable
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
// --- file backend ------------------------------------------------------------
|
|
123
|
+
function credentialsFilePath() {
|
|
124
|
+
return path.join((0, store_1.dreamsDir)(), "credentials.json");
|
|
125
|
+
}
|
|
126
|
+
function fileRead() {
|
|
127
|
+
let parsed;
|
|
128
|
+
try {
|
|
129
|
+
parsed = JSON.parse(fs.readFileSync(credentialsFilePath(), "utf8"));
|
|
130
|
+
}
|
|
131
|
+
catch {
|
|
132
|
+
return null;
|
|
133
|
+
}
|
|
134
|
+
if (typeof parsed.refresh_token !== "string" || parsed.refresh_token === "")
|
|
135
|
+
return null;
|
|
136
|
+
return {
|
|
137
|
+
access_token: typeof parsed.access_token === "string" ? parsed.access_token : "",
|
|
138
|
+
refresh_token: parsed.refresh_token,
|
|
139
|
+
expires_at: typeof parsed.expires_at === "string" ? parsed.expires_at : EPOCH_ISO,
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
/** Atomic write (temp + rename) with owner-only permissions. */
|
|
143
|
+
function fileWrite(credentials) {
|
|
144
|
+
const file = credentialsFilePath();
|
|
145
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
146
|
+
const tmp = `${file}.tmp`;
|
|
147
|
+
fs.writeFileSync(tmp, JSON.stringify(credentials, null, 2) + "\n", { mode: 0o600 });
|
|
148
|
+
fs.renameSync(tmp, file);
|
|
149
|
+
}
|
|
150
|
+
let warnedKeychainUnavailable = false;
|
|
151
|
+
function warnKeychainUnavailable() {
|
|
152
|
+
if (warnedKeychainUnavailable)
|
|
153
|
+
return;
|
|
154
|
+
warnedKeychainUnavailable = true;
|
|
155
|
+
process.stderr.write("Warning: OS keychain unavailable — storing credentials in ~/.dreams/credentials.json (mode 0600).\n");
|
|
156
|
+
}
|
|
157
|
+
// --- public API ---------------------------------------------------------------
|
|
158
|
+
/**
|
|
159
|
+
* Load stored credentials: keychain first, then the file fallback (covers
|
|
160
|
+
* installs where an earlier save fell back to the file).
|
|
161
|
+
*/
|
|
162
|
+
function loadCredentials() {
|
|
163
|
+
const refresh = keychainRead(REFRESH_ACCOUNT);
|
|
164
|
+
if (refresh) {
|
|
165
|
+
let access_token = "";
|
|
166
|
+
let expires_at = EPOCH_ISO;
|
|
167
|
+
const accessRaw = keychainRead(ACCESS_ACCOUNT);
|
|
168
|
+
if (accessRaw) {
|
|
169
|
+
try {
|
|
170
|
+
const parsed = JSON.parse(accessRaw);
|
|
171
|
+
if (typeof parsed.access_token === "string")
|
|
172
|
+
access_token = parsed.access_token;
|
|
173
|
+
if (typeof parsed.expires_at === "string")
|
|
174
|
+
expires_at = parsed.expires_at;
|
|
175
|
+
}
|
|
176
|
+
catch {
|
|
177
|
+
// stale/foreign entry shape — treat access token as expired
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
return { credentials: { access_token, refresh_token: refresh, expires_at }, source: "keychain" };
|
|
181
|
+
}
|
|
182
|
+
const fromFile = fileRead();
|
|
183
|
+
return fromFile ? { credentials: fromFile, source: "file" } : null;
|
|
184
|
+
}
|
|
185
|
+
/** Persist credentials: keychain if usable, else the 0600 file (with a warning). */
|
|
186
|
+
function saveCredentials(credentials) {
|
|
187
|
+
const ctor = keyringCtor();
|
|
188
|
+
if (ctor) {
|
|
189
|
+
try {
|
|
190
|
+
new ctor(KEYCHAIN_SERVICE, REFRESH_ACCOUNT).setPassword(credentials.refresh_token);
|
|
191
|
+
new ctor(KEYCHAIN_SERVICE, ACCESS_ACCOUNT).setPassword(JSON.stringify({ access_token: credentials.access_token, expires_at: credentials.expires_at }));
|
|
192
|
+
// Remove any stale file copy so there is a single source of truth.
|
|
193
|
+
fs.rmSync(credentialsFilePath(), { force: true });
|
|
194
|
+
return;
|
|
195
|
+
}
|
|
196
|
+
catch {
|
|
197
|
+
// keychain present but unusable (locked, headless, …) — fall back
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
if (backendOverride() !== "file")
|
|
201
|
+
warnKeychainUnavailable();
|
|
202
|
+
fileWrite(credentials);
|
|
203
|
+
}
|
|
204
|
+
/** Clear both backends (login/logout/invalid_grant paths). */
|
|
205
|
+
function clearCredentials() {
|
|
206
|
+
keychainDelete(REFRESH_ACCOUNT);
|
|
207
|
+
keychainDelete(ACCESS_ACCOUNT);
|
|
208
|
+
fs.rmSync(credentialsFilePath(), { force: true });
|
|
209
|
+
}
|
|
210
|
+
function expiresWithin(credentials, marginMs = exports.EXPIRY_MARGIN_MS) {
|
|
211
|
+
const at = Date.parse(credentials.expires_at);
|
|
212
|
+
if (Number.isNaN(at))
|
|
213
|
+
return true;
|
|
214
|
+
return at - Date.now() <= marginMs;
|
|
215
|
+
}
|
|
216
|
+
/** Mask a token for display: `at-let-…abcd`. Never print tokens unmasked. */
|
|
217
|
+
function maskToken(token) {
|
|
218
|
+
const prefixMatch = token.match(/^[a-z]{2}-let-/);
|
|
219
|
+
const prefix = prefixMatch ? prefixMatch[0] : token.slice(0, 3);
|
|
220
|
+
const suffix = token.length >= prefix.length + 8 ? token.slice(-4) : "";
|
|
221
|
+
return `${prefix}…${suffix}`;
|
|
222
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Auth entry point for the Dreams CLI.
|
|
4
|
+
*
|
|
5
|
+
* Precedence for resolving an auth header:
|
|
6
|
+
* 1. LETTA_API_KEY env var — used verbatim; never refreshed, never stored.
|
|
7
|
+
* 2. Stored Dreams credentials — refreshed when missing/expiring within 5 min.
|
|
8
|
+
* 3. Otherwise: NotAuthenticatedError telling the caller to run
|
|
9
|
+
* `dreams auth login`.
|
|
10
|
+
*/
|
|
11
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
12
|
+
exports.NotAuthenticatedError = void 0;
|
|
13
|
+
exports.resolveAccessToken = resolveAccessToken;
|
|
14
|
+
exports.getAuthHeader = getAuthHeader;
|
|
15
|
+
exports.authStatusInfo = authStatusInfo;
|
|
16
|
+
const store_1 = require("../store");
|
|
17
|
+
const credentials_1 = require("./credentials");
|
|
18
|
+
Object.defineProperty(exports, "NotAuthenticatedError", { enumerable: true, get: function () { return credentials_1.NotAuthenticatedError; } });
|
|
19
|
+
const oauth_refresh_1 = require("./oauth-refresh");
|
|
20
|
+
async function resolveAccessToken() {
|
|
21
|
+
const envKey = process.env.LETTA_API_KEY;
|
|
22
|
+
if (envKey) {
|
|
23
|
+
return { token: envKey, source: "env" };
|
|
24
|
+
}
|
|
25
|
+
const stored = (0, credentials_1.loadCredentials)();
|
|
26
|
+
if (!stored) {
|
|
27
|
+
throw new credentials_1.NotAuthenticatedError();
|
|
28
|
+
}
|
|
29
|
+
if (stored.credentials.access_token && !(0, credentials_1.expiresWithin)(stored.credentials)) {
|
|
30
|
+
return { token: stored.credentials.access_token, source: stored.source };
|
|
31
|
+
}
|
|
32
|
+
const deviceId = (0, store_1.loadOrCreateInstallation)().installation_id;
|
|
33
|
+
const refreshed = await (0, oauth_refresh_1.refreshCredentialsSingleFlight)(stored.credentials, deviceId);
|
|
34
|
+
return { token: refreshed.access_token, source: stored.source };
|
|
35
|
+
}
|
|
36
|
+
/** Auth header for the (future) upload client. */
|
|
37
|
+
async function getAuthHeader() {
|
|
38
|
+
const { token } = await resolveAccessToken();
|
|
39
|
+
return { authorization: `Bearer ${token}` };
|
|
40
|
+
}
|
|
41
|
+
/** Snapshot of auth state for `dreams auth status` and `dreams status`. Never refreshes. */
|
|
42
|
+
function authStatusInfo() {
|
|
43
|
+
const device_id = (0, store_1.loadOrCreateInstallation)().installation_id;
|
|
44
|
+
const envKey = process.env.LETTA_API_KEY;
|
|
45
|
+
if (envKey) {
|
|
46
|
+
return {
|
|
47
|
+
authenticated: true,
|
|
48
|
+
source: "env",
|
|
49
|
+
access_token: (0, credentials_1.maskToken)(envKey),
|
|
50
|
+
expires_at: null,
|
|
51
|
+
device_id,
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
const stored = (0, credentials_1.loadCredentials)();
|
|
55
|
+
if (!stored) {
|
|
56
|
+
return { authenticated: false, source: null, access_token: null, expires_at: null, device_id };
|
|
57
|
+
}
|
|
58
|
+
return {
|
|
59
|
+
authenticated: true,
|
|
60
|
+
source: stored.source,
|
|
61
|
+
access_token: stored.credentials.access_token ? (0, credentials_1.maskToken)(stored.credentials.access_token) : null,
|
|
62
|
+
expires_at: stored.credentials.expires_at,
|
|
63
|
+
device_id,
|
|
64
|
+
};
|
|
65
|
+
}
|