@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.
@@ -0,0 +1,92 @@
1
+ "use strict";
2
+ /**
3
+ * Cross-process lock around refresh + persist. Refresh-token rotation revokes
4
+ * the old token, so two dreams processes on one machine must not interleave
5
+ * rotate-then-persist. Simple lockfile: exclusive create, stale after 30s.
6
+ */
7
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
8
+ if (k2 === undefined) k2 = k;
9
+ var desc = Object.getOwnPropertyDescriptor(m, k);
10
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
11
+ desc = { enumerable: true, get: function() { return m[k]; } };
12
+ }
13
+ Object.defineProperty(o, k2, desc);
14
+ }) : (function(o, m, k, k2) {
15
+ if (k2 === undefined) k2 = k;
16
+ o[k2] = m[k];
17
+ }));
18
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
19
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
20
+ }) : function(o, v) {
21
+ o["default"] = v;
22
+ });
23
+ var __importStar = (this && this.__importStar) || (function () {
24
+ var ownKeys = function(o) {
25
+ ownKeys = Object.getOwnPropertyNames || function (o) {
26
+ var ar = [];
27
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
28
+ return ar;
29
+ };
30
+ return ownKeys(o);
31
+ };
32
+ return function (mod) {
33
+ if (mod && mod.__esModule) return mod;
34
+ var result = {};
35
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
36
+ __setModuleDefault(result, mod);
37
+ return result;
38
+ };
39
+ })();
40
+ Object.defineProperty(exports, "__esModule", { value: true });
41
+ exports.lockFilePath = lockFilePath;
42
+ exports.withAuthLock = withAuthLock;
43
+ const fs = __importStar(require("node:fs"));
44
+ const path = __importStar(require("node:path"));
45
+ const store_1 = require("../store");
46
+ const STALE_MS = 30_000;
47
+ const RETRY_MS = 100;
48
+ const ACQUIRE_TIMEOUT_MS = 45_000;
49
+ function lockFilePath() {
50
+ return path.join((0, store_1.dreamsDir)(), "auth.lock");
51
+ }
52
+ function sleep(ms) {
53
+ return new Promise((resolve) => setTimeout(resolve, ms));
54
+ }
55
+ async function acquireLock() {
56
+ const file = lockFilePath();
57
+ fs.mkdirSync(path.dirname(file), { recursive: true });
58
+ const deadline = Date.now() + ACQUIRE_TIMEOUT_MS;
59
+ for (;;) {
60
+ try {
61
+ fs.writeFileSync(file, `${process.pid}\n`, { flag: "wx" });
62
+ return;
63
+ }
64
+ catch (error) {
65
+ if (error.code !== "EEXIST")
66
+ throw error;
67
+ }
68
+ try {
69
+ const ageMs = Date.now() - fs.statSync(file).mtimeMs;
70
+ if (ageMs > STALE_MS) {
71
+ fs.rmSync(file, { force: true }); // holder died — break the stale lock
72
+ continue;
73
+ }
74
+ }
75
+ catch {
76
+ continue; // lock vanished between attempts — retry immediately
77
+ }
78
+ if (Date.now() > deadline) {
79
+ throw new Error(`Timed out waiting for auth lock at ${file}`);
80
+ }
81
+ await sleep(RETRY_MS);
82
+ }
83
+ }
84
+ async function withAuthLock(fn) {
85
+ await acquireLock();
86
+ try {
87
+ return await fn();
88
+ }
89
+ finally {
90
+ fs.rmSync(lockFilePath(), { force: true });
91
+ }
92
+ }
@@ -0,0 +1,68 @@
1
+ "use strict";
2
+ /**
3
+ * Access-token refresh with rotation handling.
4
+ *
5
+ * In-process concurrency: singleflight keyed by refreshToken+deviceId
6
+ * (mirrors letta-code's oauth-refresh.ts) so N concurrent callers produce
7
+ * exactly one network refresh. Cross-process: rotation is scoped per
8
+ * device_id, and a lockfile guards rotate-then-persist on this machine.
9
+ */
10
+ Object.defineProperty(exports, "__esModule", { value: true });
11
+ exports.credentialsFromToken = credentialsFromToken;
12
+ exports.refreshCredentialsSingleFlight = refreshCredentialsSingleFlight;
13
+ const credentials_1 = require("./credentials");
14
+ const lock_1 = require("./lock");
15
+ const oauth_1 = require("./oauth");
16
+ const inFlightRefreshes = new Map();
17
+ function credentialsFromToken(token, previousRefreshToken) {
18
+ return {
19
+ access_token: token.access_token,
20
+ // refresh_token_mode "new" always rotates; keep the old one only if the
21
+ // server unexpectedly omits a replacement.
22
+ refresh_token: token.refresh_token || previousRefreshToken,
23
+ expires_at: new Date(Date.now() + token.expires_in * 1000).toISOString(),
24
+ };
25
+ }
26
+ async function refreshCredentialsSingleFlight(credentials, deviceId) {
27
+ const key = `${credentials.refresh_token}\0${deviceId}`;
28
+ const existing = inFlightRefreshes.get(key);
29
+ if (existing) {
30
+ return await existing;
31
+ }
32
+ const pending = refreshAndPersist(credentials, deviceId);
33
+ inFlightRefreshes.set(key, pending);
34
+ try {
35
+ return await pending;
36
+ }
37
+ finally {
38
+ if (inFlightRefreshes.get(key) === pending) {
39
+ inFlightRefreshes.delete(key);
40
+ }
41
+ }
42
+ }
43
+ async function refreshAndPersist(credentials, deviceId) {
44
+ return (0, lock_1.withAuthLock)(async () => {
45
+ // Another process may have rotated while we waited on the lock.
46
+ const current = (0, credentials_1.loadCredentials)();
47
+ const effective = current?.credentials ?? credentials;
48
+ if (effective.access_token && !(0, credentials_1.expiresWithin)(effective)) {
49
+ return effective;
50
+ }
51
+ try {
52
+ const token = await (0, oauth_1.refreshAccessToken)(effective.refresh_token, deviceId);
53
+ const rotated = credentialsFromToken(token, effective.refresh_token);
54
+ // The server just revoked the old refresh token: persist the new one
55
+ // immediately and atomically, before anyone uses the new access token.
56
+ (0, credentials_1.saveCredentials)(rotated);
57
+ return rotated;
58
+ }
59
+ catch (error) {
60
+ if (error instanceof oauth_1.OAuthRefreshError && error.oauthCode === "invalid_grant") {
61
+ // Stale rotated token (revoked elsewhere) — stored credentials are dead.
62
+ (0, credentials_1.clearCredentials)();
63
+ throw new credentials_1.NotAuthenticatedError("Stored credentials are no longer valid. Run `dreams auth login` to reconnect.");
64
+ }
65
+ throw error;
66
+ }
67
+ });
68
+ }
@@ -0,0 +1,193 @@
1
+ "use strict";
2
+ /**
3
+ * OAuth 2.0 Device Code Flow against Letta Cloud.
4
+ *
5
+ * Protocol behavior mirrors letta-code's src/auth/oauth.ts (same endpoints,
6
+ * bodies, and error codes) but is reimplemented here on node builtins only —
7
+ * letta-code exports nothing and its credentials must not be shared (refresh
8
+ * rotation is scoped per device_id, so each CLI needs its own device identity).
9
+ */
10
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
11
+ if (k2 === undefined) k2 = k;
12
+ var desc = Object.getOwnPropertyDescriptor(m, k);
13
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
14
+ desc = { enumerable: true, get: function() { return m[k]; } };
15
+ }
16
+ Object.defineProperty(o, k2, desc);
17
+ }) : (function(o, m, k, k2) {
18
+ if (k2 === undefined) k2 = k;
19
+ o[k2] = m[k];
20
+ }));
21
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
22
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
23
+ }) : function(o, v) {
24
+ o["default"] = v;
25
+ });
26
+ var __importStar = (this && this.__importStar) || (function () {
27
+ var ownKeys = function(o) {
28
+ ownKeys = Object.getOwnPropertyNames || function (o) {
29
+ var ar = [];
30
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
31
+ return ar;
32
+ };
33
+ return ownKeys(o);
34
+ };
35
+ return function (mod) {
36
+ if (mod && mod.__esModule) return mod;
37
+ var result = {};
38
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
39
+ __setModuleDefault(result, mod);
40
+ return result;
41
+ };
42
+ })();
43
+ Object.defineProperty(exports, "__esModule", { value: true });
44
+ exports.OAuthRefreshError = exports.CLIENT_ID = void 0;
45
+ exports.authBaseUrl = authBaseUrl;
46
+ exports.apiBaseUrl = apiBaseUrl;
47
+ exports.deviceName = deviceName;
48
+ exports.requestDeviceCode = requestDeviceCode;
49
+ exports.pollForToken = pollForToken;
50
+ exports.refreshAccessToken = refreshAccessToken;
51
+ exports.revokeToken = revokeToken;
52
+ const os = __importStar(require("node:os"));
53
+ /**
54
+ * Letta's public OAuth client (no secret; verified public client server-side).
55
+ * Shared with letta-code — safe because token rotation is scoped per
56
+ * (user, client, device) and Dreams always sends its own device_id.
57
+ */
58
+ exports.CLIENT_ID = "ci-let-724dea7e98f4af6f8f370f4b1466200c";
59
+ function authBaseUrl() {
60
+ return process.env.LETTA_AUTH_BASE_URL || "https://app.letta.com";
61
+ }
62
+ function apiBaseUrl() {
63
+ return process.env.LETTA_BASE_URL || "https://api.letta.com";
64
+ }
65
+ function deviceName() {
66
+ return `Dreams CLI (${os.hostname()})`;
67
+ }
68
+ class OAuthRefreshError extends Error {
69
+ oauthCode;
70
+ status;
71
+ constructor(message, options = {}) {
72
+ super(message);
73
+ this.name = "OAuthRefreshError";
74
+ this.oauthCode = options.oauthCode;
75
+ this.status = options.status;
76
+ }
77
+ }
78
+ exports.OAuthRefreshError = OAuthRefreshError;
79
+ async function readErrorBody(response) {
80
+ try {
81
+ return (await response.json());
82
+ }
83
+ catch {
84
+ return { error: `http_${response.status}` };
85
+ }
86
+ }
87
+ /** Wrap undici's bare "fetch failed" into something actionable. */
88
+ async function postJson(url, body) {
89
+ try {
90
+ return await fetch(url, {
91
+ method: "POST",
92
+ headers: { "Content-Type": "application/json" },
93
+ body: JSON.stringify(body),
94
+ });
95
+ }
96
+ catch (error) {
97
+ const cause = error.cause;
98
+ const detail = cause?.code || cause?.message || (error instanceof Error ? error.message : String(error));
99
+ throw new Error(`Could not reach Letta auth servers at ${authBaseUrl()} (${detail}). Check your network, proxy, or VPN settings.`);
100
+ }
101
+ }
102
+ /** Device Code Flow — step 1: mint a device + user code pair. */
103
+ async function requestDeviceCode() {
104
+ const response = await postJson(`${authBaseUrl()}/api/oauth/device/code`, {
105
+ client_id: exports.CLIENT_ID,
106
+ });
107
+ if (!response.ok) {
108
+ const error = await readErrorBody(response);
109
+ throw new Error(`Failed to request device code: ${error.error_description || error.error}`);
110
+ }
111
+ return (await response.json());
112
+ }
113
+ function realSleep(ms) {
114
+ return new Promise((resolve) => setTimeout(resolve, ms));
115
+ }
116
+ /**
117
+ * Device Code Flow — step 2: poll the token endpoint until the user approves.
118
+ * Handles authorization_pending (keep polling), slow_down (+5s to the
119
+ * interval), access_denied, and expired_token per RFC 8628.
120
+ */
121
+ async function pollForToken(options) {
122
+ const sleep = options.sleep ?? realSleep;
123
+ const expiresInMs = (options.expiresInSeconds ?? 900) * 1000;
124
+ let pollIntervalMs = (options.intervalSeconds ?? 5) * 1000;
125
+ const startTime = Date.now();
126
+ while (Date.now() - startTime < expiresInMs) {
127
+ await sleep(pollIntervalMs);
128
+ const response = await postJson(`${authBaseUrl()}/api/oauth/token`, {
129
+ grant_type: "urn:ietf:params:oauth:grant-type:device_code",
130
+ client_id: exports.CLIENT_ID,
131
+ device_code: options.deviceCode,
132
+ device_id: options.deviceId,
133
+ device_name: deviceName(),
134
+ });
135
+ const result = (await response.json());
136
+ if (response.ok)
137
+ return result;
138
+ if (result.error === "authorization_pending") {
139
+ options.onPending?.();
140
+ continue;
141
+ }
142
+ if (result.error === "slow_down") {
143
+ pollIntervalMs += 5000; // server asked us to back off
144
+ continue;
145
+ }
146
+ if (result.error === "access_denied") {
147
+ throw new Error("Authorization was denied in the browser.");
148
+ }
149
+ if (result.error === "expired_token") {
150
+ throw new Error("Device code expired before approval. Run `dreams auth login` again.");
151
+ }
152
+ throw new Error(`OAuth error: ${result.error_description || result.error}`);
153
+ }
154
+ throw new Error("Timed out waiting for authorization. Run `dreams auth login` again.");
155
+ }
156
+ /**
157
+ * Exchange a refresh token for a new access token. The server ALWAYS rotates:
158
+ * with refresh_token_mode "new" the response carries a new refresh_token that
159
+ * revokes the old one for this (user, client, device) — callers must persist
160
+ * it before using the new access token.
161
+ */
162
+ async function refreshAccessToken(refreshToken, deviceId) {
163
+ const response = await postJson(`${authBaseUrl()}/api/oauth/token`, {
164
+ grant_type: "refresh_token",
165
+ client_id: exports.CLIENT_ID,
166
+ refresh_token: refreshToken,
167
+ refresh_token_mode: "new",
168
+ device_id: deviceId,
169
+ device_name: deviceName(),
170
+ });
171
+ if (!response.ok) {
172
+ const error = await readErrorBody(response);
173
+ throw new OAuthRefreshError(`Failed to refresh access token: ${error.error_description || error.error}`, { oauthCode: error.error, status: response.status });
174
+ }
175
+ return (await response.json());
176
+ }
177
+ /** Revoke a refresh token (logout). Best-effort: warns on stderr, never throws. */
178
+ async function revokeToken(refreshToken) {
179
+ try {
180
+ const response = await postJson(`${authBaseUrl()}/api/oauth/revoke`, {
181
+ client_id: exports.CLIENT_ID,
182
+ token: refreshToken,
183
+ token_type_hint: "refresh_token",
184
+ });
185
+ if (!response.ok) {
186
+ const error = await readErrorBody(response);
187
+ process.stderr.write(`Warning: failed to revoke refresh token: ${error.error_description || error.error}\n`);
188
+ }
189
+ }
190
+ catch (error) {
191
+ process.stderr.write(`Warning: failed to revoke refresh token: ${String(error)}\n`);
192
+ }
193
+ }
package/dist/cli.js ADDED
@@ -0,0 +1,149 @@
1
+ "use strict";
2
+ /** @letta-ai/dreams command-line entry point. */
3
+ Object.defineProperty(exports, "__esModule", { value: true });
4
+ exports.main = main;
5
+ const index_1 = require("./auth/index");
6
+ const commands_1 = require("./auth/commands");
7
+ const commands_2 = require("./local/commands");
8
+ const db_1 = require("./local/db");
9
+ const managed_install_1 = require("./managed-install");
10
+ const onboard_1 = require("./onboard");
11
+ const skill_1 = require("./skill");
12
+ const store_1 = require("./store");
13
+ const version_1 = require("./version");
14
+ const USAGE = `Usage: dreams <command> [options]
15
+
16
+ Commands:
17
+ onboard Install Dreams locally and return the next setup action
18
+ status Show installation, skill, CLI, database, and auth state
19
+ auth login Connect this device to Letta (OAuth device code flow)
20
+ auth status Show authentication state (never prints tokens)
21
+ auth logout Revoke Dreams' refresh token and clear stored credentials
22
+ source scan Discover Claude Code sessions and build the local upload queue
23
+ source approve-root <path> Approve a work root so its transcripts become eligible
24
+ source list Show local sources, approved roots, and the pending queue
25
+ upload Upload pending queued objects to Letta Cloud
26
+
27
+ Options:
28
+ --json Machine-readable JSON output
29
+ --version Print the CLI version
30
+ --help Show this help
31
+ `;
32
+ const COMMANDS_WITH_SUBCOMMANDS = new Set(["auth", "source"]);
33
+ function parseArgs(argv) {
34
+ const parsed = {
35
+ positionals: [],
36
+ json: false,
37
+ help: false,
38
+ version: false,
39
+ unknown: [],
40
+ };
41
+ for (const arg of argv) {
42
+ if (arg === "--json")
43
+ parsed.json = true;
44
+ else if (arg === "--help" || arg === "-h")
45
+ parsed.help = true;
46
+ else if (arg === "--version" || arg === "-v")
47
+ parsed.version = true;
48
+ else if (arg.startsWith("-"))
49
+ parsed.unknown.push(arg);
50
+ else
51
+ parsed.positionals.push(arg);
52
+ }
53
+ return parsed;
54
+ }
55
+ function commandStatus(json) {
56
+ const installation = (0, store_1.loadOrCreateInstallation)();
57
+ const auth = (0, index_1.authStatusInfo)();
58
+ const status = {
59
+ installation_id: installation.installation_id,
60
+ installation_created_at: installation.created_at,
61
+ state: auth.authenticated ? "authenticated" : "unauthenticated",
62
+ cli_path: (0, managed_install_1.stableCliPath)(),
63
+ database_path: (0, db_1.dbFilePath)(),
64
+ skill_version: (0, skill_1.installedSkillVersion)(),
65
+ skill_path: (0, skill_1.skillFilePath)(),
66
+ cli_version: (0, version_1.packageVersion)(),
67
+ auth,
68
+ };
69
+ if (json) {
70
+ console.log(JSON.stringify(status, null, 2));
71
+ }
72
+ else {
73
+ const authLine = auth.authenticated
74
+ ? `yes (via ${auth.source}, token ${auth.access_token ?? "pending refresh"}${auth.expires_at ? `, expires ${auth.expires_at}` : ""})`
75
+ : "no (run `dreams onboard`)";
76
+ console.log([
77
+ "Dreams status",
78
+ "",
79
+ ` Installation id: ${status.installation_id}`,
80
+ ` Created at: ${status.installation_created_at}`,
81
+ ` State: ${status.state}`,
82
+ ` Durable CLI: ${status.cli_path}`,
83
+ ` Skill installed: ${status.skill_version ? `v${status.skill_version} at ${status.skill_path}` : "no"}`,
84
+ ` Authenticated: ${authLine}`,
85
+ ` Database: ${status.database_path}`,
86
+ ` CLI version: ${status.cli_version}`,
87
+ ].join("\n"));
88
+ }
89
+ return 0;
90
+ }
91
+ function commandAuth(subcommand, json) {
92
+ switch (subcommand) {
93
+ case "login":
94
+ return (0, commands_1.commandAuthLogin)(json);
95
+ case "status":
96
+ return (0, commands_1.commandAuthStatus)(json);
97
+ case "logout":
98
+ return (0, commands_1.commandAuthLogout)(json);
99
+ default:
100
+ process.stderr.write(`Unknown auth subcommand: ${subcommand ?? "(none)"} — expected login, status, or logout\n\n${USAGE}`);
101
+ return 1;
102
+ }
103
+ }
104
+ async function runCli() {
105
+ const args = parseArgs(process.argv.slice(2));
106
+ const command = args.positionals[0];
107
+ const subcommand = args.positionals[1];
108
+ if (args.version) {
109
+ console.log((0, version_1.packageVersion)());
110
+ return 0;
111
+ }
112
+ if (args.help || command === undefined) {
113
+ process.stdout.write(USAGE);
114
+ return args.help ? 0 : 1;
115
+ }
116
+ if (args.unknown.length > 0) {
117
+ process.stderr.write(`Unknown argument(s): ${args.unknown.join(", ")}\n\n${USAGE}`);
118
+ return 1;
119
+ }
120
+ // Single-verb commands take no positional arguments beyond the command itself.
121
+ if (!COMMANDS_WITH_SUBCOMMANDS.has(command) && args.positionals.length > 1) {
122
+ process.stderr.write(`Unexpected argument(s): ${args.positionals.slice(1).join(", ")}\n\n${USAGE}`);
123
+ return 1;
124
+ }
125
+ switch (command) {
126
+ case "onboard":
127
+ return (0, onboard_1.commandOnboard)(args.json);
128
+ case "status":
129
+ return commandStatus(args.json);
130
+ case "auth":
131
+ return commandAuth(subcommand, args.json);
132
+ case "source":
133
+ return (0, commands_2.commandSource)(subcommand, args.positionals[2], args.json);
134
+ case "upload":
135
+ return (0, commands_2.commandUpload)(args.json);
136
+ default:
137
+ process.stderr.write(`Unknown command: ${command}\n\n${USAGE}`);
138
+ return 1;
139
+ }
140
+ }
141
+ function main() {
142
+ runCli().then((code) => process.exit(code), (error) => {
143
+ process.stderr.write(`${error instanceof Error ? error.message : String(error)}\n`);
144
+ process.exit(1);
145
+ });
146
+ }
147
+ if (require.main === module) {
148
+ main();
149
+ }