@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 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 source
26
- init snapshot → activate test connection). Repeated `onboard` calls are safe
27
- and repair local setup only.
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 Claude Code transcripts and prepares immutable upload segments before
84
- they are sent to Letta Cloud.
85
-
86
- - `dreams source detect [--json]` detect Claude Code from harness filesystem
87
- facts under `~/.claude` (no transcript reads) and register an installation-scoped
88
- Cloud source in status `detected`. Idempotent; does not scan or upload.
89
- - `dreams source approve-root <path> [--json]` approve a work root. Sources
90
- are **deny-by-default**: no transcript bytes are queued until their working
91
- directory is under an approved root.
92
- - `dreams source scan [--json]` discover top-level Claude Code sessions
93
- (`~/.claude/projects/<project>/<sessionId>.jsonl`), read only complete new
94
- lines after the durable cursor, and enqueue deterministic upload segments.
95
- Repeated scans are idempotent; `.dreamignore` and unapproved roots are honored
96
- before anything is enqueued.
97
- - `dreams source list [--json]` — show the DB path, approved roots, sources, and
98
- the pending upload queue.
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 upload [--json]` — authenticate, refresh source registration, and
104
- upload due queue entries. Upload accepts `detected` sources; Cloud promotes to
105
- `active` after durable acceptance.
106
- - `dreams sync [--force] [--json]` — hook-safe scheduler: records a durable sync
107
- trigger and runs a detached worker that live-scans tracked sessions and uploads
108
- with a per-pass budget (~3h cadence unless `--force` or retry-due).
109
- - `dreams sync pause|resume|status [--json]` — pause makes hook wakes no-ops;
110
- resume clears pause and requests one immediate sync.
111
- - `dreams backfill start --since <RFC3339> [--json]` approve a frozen
112
- `[since, until=now]` historical window and run one bounded local scan/queue
113
- pass (session frontier, newest unfinished sessions first; no upload).
114
- Re-running the same active `--since` is idempotent; a different active policy
115
- fails closed. Sync drains live first, then backfill by newest session mtime.
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();
@@ -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
- ].join("\n");
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)();
@@ -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
- let access_token = "";
177
- let expires_at = EPOCH_ISO;
178
- const accessRaw = keychainRead(ACCESS_ACCOUNT);
179
- if (accessRaw) {
180
- try {
181
- const parsed = JSON.parse(accessRaw);
182
- if (typeof parsed.access_token === "string")
183
- access_token = parsed.access_token;
184
- if (typeof parsed.expires_at === "string")
185
- expires_at = parsed.expires_at;
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
- catch {
188
- // stale/foreign entry shape — treat access token as expired
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
- return { credentials: { access_token, refresh_token: refresh, expires_at }, source: "keychain" };
245
+ catch {
246
+ keychain = { status: "unavailable", credentials: null };
247
+ }
192
248
  }
193
249
  const fromFile = fileRead();
194
- return fromFile ? { credentials: fromFile, source: "file" } : null;
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) {
@@ -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
- if (envKey) {
46
- return {
47
- authenticated: true,
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
- access_token: (0, credentials_1.maskToken)(envKey),
50
- expires_at: null,
51
- device_id,
93
+ reason: "LETTA_API_KEY is set and takes highest precedence",
52
94
  };
53
95
  }
54
- const stored = (0, credentials_1.loadCredentials)();
55
- if (!stored) {
56
- return { authenticated: false, source: null, access_token: null, expires_at: null, device_id };
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: 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,
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
  }