mercury-agent 0.8.5 → 0.8.7

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.
@@ -11,7 +11,10 @@ import {
11
11
  import { createRequire } from "node:module";
12
12
  import { homedir, tmpdir } from "node:os";
13
13
  import { delimiter, dirname, join } from "node:path";
14
- import { getPiAuthCredential } from "mercury-agent/storage/pi-auth";
14
+ import {
15
+ getPiAuthCredential,
16
+ parseOAuthTokenEnv,
17
+ } from "mercury-agent/storage/pi-auth";
15
18
 
16
19
  const KNOWLEDGE_DIR = "knowledge";
17
20
  const VAULT_DIRS = ["people", "projects", "references", "daily", "episodes", "weekly", "monthly", "templates"];
@@ -422,10 +425,25 @@ export default function (mercury: {
422
425
  if (process.env.MERCURY_ANTHROPIC_API_KEY) {
423
426
  env.ANTHROPIC_API_KEY = process.env.MERCURY_ANTHROPIC_API_KEY;
424
427
  }
428
+ // Console-provisioned agents carry a JSON credential blob here, not a bare
429
+ // token — same handling as container-runner, via the shared parser.
430
+ let oauthBlobCorrupt = false;
425
431
  if (process.env.MERCURY_ANTHROPIC_OAUTH_TOKEN) {
426
- env.ANTHROPIC_API_KEY = process.env.MERCURY_ANTHROPIC_OAUTH_TOKEN;
432
+ const parsed = parseOAuthTokenEnv(
433
+ process.env.MERCURY_ANTHROPIC_OAUTH_TOKEN,
434
+ );
435
+ if (parsed.status === "token") {
436
+ env.ANTHROPIC_API_KEY = parsed.token;
437
+ } else if (parsed.status === "blob") {
438
+ env.ANTHROPIC_OAUTH_TOKEN = parsed.access;
439
+ } else if (parsed.status === "corrupt-blob") {
440
+ oauthBlobCorrupt = true;
441
+ }
442
+ // "empty" — whitespace-only value, treat as unset and fall through
443
+ }
444
+ if (env.ANTHROPIC_API_KEY || env.ANTHROPIC_OAUTH_TOKEN) {
445
+ return { ok: true, env };
427
446
  }
428
- if (env.ANTHROPIC_API_KEY) return { ok: true, env };
429
447
 
430
448
  // Spawned pi inherits process.env, so an unprefixed host key also works.
431
449
  if (process.env.ANTHROPIC_API_KEY || process.env.ANTHROPIC_OAUTH_TOKEN) {
@@ -450,8 +468,9 @@ export default function (mercury: {
450
468
 
451
469
  // Fail fast, mirroring the container-runner guard: without a credential
452
470
  // every pi spawn exits 1, one per space per pending date, every run.
453
- const reason =
454
- cred.status === "refresh-failed"
471
+ const reason = oauthBlobCorrupt
472
+ ? "MERCURY_ANTHROPIC_OAUTH_TOKEN holds a corrupt credential blob — re-provision it or set MERCURY_ANTHROPIC_API_KEY"
473
+ : cred.status === "refresh-failed"
455
474
  ? `Anthropic OAuth refresh failed for ${authPath} — re-authenticate on the host (run mercury auth login from the project directory that owns that file) or set MERCURY_ANTHROPIC_API_KEY`
456
475
  : `no Anthropic credential configured (checked ${authPath}) — run mercury auth login or set MERCURY_ANTHROPIC_API_KEY / MERCURY_ANTHROPIC_OAUTH_TOKEN`;
457
476
  return { ok: false, reason };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mercury-agent",
3
- "version": "0.8.5",
3
+ "version": "0.8.7",
4
4
  "description": "Personal AI assistant for chat platforms (WhatsApp, Slack, Discord, Telegram)",
5
5
  "license": "MIT",
6
6
  "author": "Avishai Tsabari",
@@ -33,6 +33,34 @@ function writeAuthFile(authPath: string, auth: AuthFile): void {
33
33
  fs.chmodSync(authPath, 0o600);
34
34
  }
35
35
 
36
+ export type OAuthTokenEnvValue =
37
+ | { status: "token"; token: string }
38
+ | { status: "blob"; access: string }
39
+ | { status: "corrupt-blob" }
40
+ /** Whitespace-only value — treat the variable as unset. */
41
+ | { status: "empty" };
42
+
43
+ /**
44
+ * Interpret the raw value of MERCURY_ANTHROPIC_OAUTH_TOKEN. Console-provisioned
45
+ * agents receive a full credential blob ({"access":"...","refresh":"...",
46
+ * "expires":...}) rather than a bare token, so any code spawning pi on the host
47
+ * must extract the access token instead of passing the value through verbatim.
48
+ */
49
+ export function parseOAuthTokenEnv(raw: string): OAuthTokenEnvValue {
50
+ const trimmed = raw.trim();
51
+ if (!trimmed) return { status: "empty" };
52
+ if (!trimmed.startsWith("{")) return { status: "token", token: trimmed };
53
+ try {
54
+ const blob = JSON.parse(trimmed) as { access?: unknown };
55
+ if (typeof blob.access === "string" && blob.access) {
56
+ return { status: "blob", access: blob.access };
57
+ }
58
+ } catch {
59
+ // fall through to corrupt-blob
60
+ }
61
+ return { status: "corrupt-blob" };
62
+ }
63
+
36
64
  export type PiAuthCredential =
37
65
  | { status: "ok"; apiKey: string }
38
66
  /** No usable oauth entry (or an env override takes precedence). */
@@ -40,6 +68,10 @@ export type PiAuthCredential =
40
68
  /** An oauth entry exists but could not be turned into a usable key. */
41
69
  | { status: "refresh-failed"; error?: Error };
42
70
 
71
+ // Deduplicate concurrent OAuth refreshes — refresh tokens are single-use, so
72
+ // two parallel calls with the same token race and one always fails.
73
+ const inflightRefresh = new Map<string, Promise<PiAuthCredential>>();
74
+
43
75
  export async function getPiAuthCredential(options: {
44
76
  provider: string;
45
77
  authPath: string;
@@ -55,6 +87,25 @@ export async function getPiAuthCredential(options: {
55
87
  return { status: "none" };
56
88
  }
57
89
 
90
+ // Coalesce concurrent refreshes for the same auth file so only one
91
+ // token-endpoint call is made; the rest share its result.
92
+ const key = options.authPath;
93
+ const existing = inflightRefresh.get(key);
94
+ if (existing) return existing;
95
+
96
+ const promise = doGetPiAuthCredential(options);
97
+ inflightRefresh.set(key, promise);
98
+ try {
99
+ return await promise;
100
+ } finally {
101
+ inflightRefresh.delete(key);
102
+ }
103
+ }
104
+
105
+ async function doGetPiAuthCredential(options: {
106
+ provider: string;
107
+ authPath: string;
108
+ }): Promise<PiAuthCredential> {
58
109
  const authPath = options.authPath;
59
110
  const auth = readAuthFile(authPath);
60
111