@1presence/bridge 0.61.0 → 0.62.0

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,130 @@
1
+ import { spawn } from 'node:child_process';
2
+ import { createInterface } from 'node:readline';
3
+ /**
4
+ * Parse the stdout of `claude auth status --json`. Extracts the first JSON object
5
+ * (defensive against any banner/log noise around it). Returns null when there is
6
+ * no parseable object or no boolean `loggedIn` field — i.e. an output shape we
7
+ * don't recognise, which the caller treats as "unknown, don't block".
8
+ * Exported for unit testing.
9
+ */
10
+ export function parseAuthStatus(stdout) {
11
+ const start = stdout.indexOf('{');
12
+ const end = stdout.lastIndexOf('}');
13
+ if (start === -1 || end === -1 || end < start)
14
+ return null;
15
+ try {
16
+ const j = JSON.parse(stdout.slice(start, end + 1));
17
+ if (typeof j['loggedIn'] !== 'boolean')
18
+ return null;
19
+ return {
20
+ loggedIn: j['loggedIn'],
21
+ email: typeof j['email'] === 'string' ? j['email'] : undefined,
22
+ authMethod: typeof j['authMethod'] === 'string' ? j['authMethod'] : undefined,
23
+ subscriptionType: typeof j['subscriptionType'] === 'string' ? j['subscriptionType'] : undefined,
24
+ orgName: typeof j['orgName'] === 'string' ? j['orgName'] : undefined,
25
+ };
26
+ }
27
+ catch {
28
+ return null;
29
+ }
30
+ }
31
+ /**
32
+ * Ask the local `claude` CLI whether it's signed in via `claude auth status
33
+ * --json`. Zero-cost — no model turn runs. Resolves the parsed status, or null
34
+ * when we can't determine it (CLI not installed, too old for `auth status`,
35
+ * errored, or timed out). null means "unknown": a probe we can't trust must
36
+ * never gate startup — the first turn's error path still guides the user if auth
37
+ * is genuinely broken.
38
+ */
39
+ export function probeClaudeAuth(timeoutMs = 8000) {
40
+ return new Promise((resolve) => {
41
+ let settled = false;
42
+ const finish = (v) => { if (!settled) {
43
+ settled = true;
44
+ resolve(v);
45
+ } };
46
+ let proc;
47
+ try {
48
+ proc = spawn('claude', ['auth', 'status', '--json'], { stdio: ['ignore', 'pipe', 'ignore'] });
49
+ }
50
+ catch {
51
+ finish(null);
52
+ return;
53
+ }
54
+ const timer = setTimeout(() => { try {
55
+ proc.kill('SIGKILL');
56
+ }
57
+ catch { /* already gone */ } finish(null); }, timeoutMs);
58
+ let out = '';
59
+ proc.stdout?.on('data', (c) => { out += c.toString('utf-8'); });
60
+ proc.on('error', () => { clearTimeout(timer); finish(null); });
61
+ proc.on('close', () => { clearTimeout(timer); finish(parseAuthStatus(out)); });
62
+ });
63
+ }
64
+ /**
65
+ * Launch the local Claude Code subscription sign-in (`claude auth login
66
+ * --claudeai`) with inherited stdio, so the user completes the browser / paste
67
+ * flow right here. Resolves true when the child exits (any exit code — the caller
68
+ * re-probes to learn the outcome), or false if it can't be spawned at all (CLI
69
+ * missing / too old for `auth login`), in which case the poll below still carries
70
+ * a sign-in the user does in another terminal.
71
+ */
72
+ export function launchClaudeLogin() {
73
+ return new Promise((resolve) => {
74
+ let proc;
75
+ try {
76
+ proc = spawn('claude', ['auth', 'login', '--claudeai'], { stdio: 'inherit' });
77
+ }
78
+ catch {
79
+ resolve(false);
80
+ return;
81
+ }
82
+ proc.on('error', () => resolve(false));
83
+ proc.on('close', () => resolve(true));
84
+ });
85
+ }
86
+ const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
87
+ /**
88
+ * Poll `claude auth status` until it reports signed-in, or the timeout elapses.
89
+ * This is also what catches a sign-in the user completes in ANOTHER terminal
90
+ * window: on macOS the credential lives in the Keychain, which no process can
91
+ * watch, so polling is the only cross-platform detector. The Agent SDK re-reads
92
+ * credentials on every query(), so once this returns true the very next turn
93
+ * works with no bridge restart. `onWaiting` fires once, the first time we find
94
+ * the user still isn't signed in (so the caller can print a waiting line without
95
+ * spamming it). Returns true if signed in, false on timeout.
96
+ */
97
+ export async function waitForClaudeLogin(opts = {}) {
98
+ const intervalMs = opts.intervalMs ?? 3000;
99
+ const timeoutMs = opts.timeoutMs ?? 5 * 60 * 1000;
100
+ const deadline = Date.now() + timeoutMs;
101
+ let announced = false;
102
+ for (;;) {
103
+ const status = await probeClaudeAuth();
104
+ if (status?.loggedIn)
105
+ return true;
106
+ if (Date.now() >= deadline)
107
+ return false;
108
+ if (!announced) {
109
+ opts.onWaiting?.();
110
+ announced = true;
111
+ }
112
+ await sleep(intervalMs);
113
+ }
114
+ }
115
+ /**
116
+ * Minimal Y/n prompt. Returns the boolean answer; empty input returns `def`.
117
+ * Caller is responsible for only invoking this when stdin is a TTY.
118
+ */
119
+ export function promptYesNo(question, def) {
120
+ return new Promise((resolve) => {
121
+ const rl = createInterface({ input: process.stdin, output: process.stdout });
122
+ rl.question(question, (answer) => {
123
+ rl.close();
124
+ const a = answer.trim().toLowerCase();
125
+ if (a === '')
126
+ return resolve(def);
127
+ resolve(a === 'y' || a === 'yes');
128
+ });
129
+ });
130
+ }
package/dist/index.js CHANGED
@@ -9,6 +9,7 @@ import { query } from '@anthropic-ai/claude-agent-sdk';
9
9
  import { getValidAuth, ensureFreshToken, forceRefreshToken, isTokenValid, AuthCancelledError } from './auth.js';
10
10
  import { spawnClaude, killAll, cancelConversation, setVerbose, setDebug, paint, SECTION_COLORS, getCliVersion, getRateLimitWindow } from './claude.js';
11
11
  import { ensureModelChoice } from './config.js';
12
+ import { probeClaudeAuth, launchClaudeLogin, waitForClaudeLogin, promptYesNo } from './claudeAuth.js';
12
13
  import { checkAndUpdate } from './update.js';
13
14
  import { makeBridgeAccumulator, postSaveTurn } from './accumulator.js';
14
15
  import { writeSpool, deleteSpool, listSpool } from './outbox.js';
@@ -759,6 +760,54 @@ function scheduleReconnect(closeCode, retryDelay) {
759
760
  }, delay);
760
761
  }
761
762
  // ─── Main ─────────────────────────────────────────────────────────────────────
763
+ // Check the LOCAL Claude Code sign-in (the user's own claude.ai subscription
764
+ // OAuth, distinct from the 1Presence gateway login above) before connecting.
765
+ // Local Mode drives Claude Code through the Agent SDK, so if Claude Code isn't
766
+ // signed in every turn fails with the `local_auth` code (and pages ops). Catch
767
+ // it up front: probe, and if signed out, offer to sign in here — polling also
768
+ // picks up a sign-in the user does in another terminal, and the SDK re-reads
769
+ // credentials each turn, so no restart is needed once signed in. Any uncertainty
770
+ // (old CLI without `auth status`, probe error) is treated as "don't block".
771
+ async function ensureClaudeCodeLogin() {
772
+ const status = await probeClaudeAuth();
773
+ if (status === null)
774
+ return; // unknown — don't gate startup on a probe we can't trust
775
+ if (status.loggedIn) {
776
+ const who = status.email ? ` as ${status.email}` : '';
777
+ const plan = status.subscriptionType ? ` (${status.subscriptionType})` : '';
778
+ console.log(paint(SECTION_COLORS.assistant, `✓ Claude Code signed in${who}${plan}`));
779
+ return;
780
+ }
781
+ console.log(paint(SECTION_COLORS.result, '\n⚠ Your local Claude Code isn’t signed in.'));
782
+ console.log(' Local Mode runs on your own Claude subscription — Claude Code has to be signed in on this machine, or every message will fail.');
783
+ if (!process.stdin.isTTY) {
784
+ console.log(' Not an interactive terminal, so I can’t prompt. Sign in with `claude auth login`, then start the bridge again.\n');
785
+ return;
786
+ }
787
+ const yes = await promptYesNo(' Log in to Claude now? (Y/n) ', true);
788
+ if (!yes) {
789
+ console.log(' Skipping sign-in. Messages will fail until you run `claude auth login` — sign in any time and resend.\n');
790
+ return;
791
+ }
792
+ // Best-effort: open the sign-in right here. If the CLI is too old for
793
+ // `auth login`, this returns false and the poll still catches a sign-in the
794
+ // user does in another terminal.
795
+ const launched = await launchClaudeLogin();
796
+ if (!launched) {
797
+ console.log(' Couldn’t open sign-in automatically. In another terminal run: claude auth login');
798
+ }
799
+ const ok = await waitForClaudeLogin({
800
+ onWaiting: () => console.log(paint('90', ' Waiting for sign-in… finish it in the browser (or run `claude auth login` in any terminal). Ctrl+C to abort.')),
801
+ });
802
+ if (ok) {
803
+ const s = await probeClaudeAuth();
804
+ const who = s?.email ? ` as ${s.email}` : '';
805
+ console.log(paint(SECTION_COLORS.assistant, `✓ Signed in${who} — continuing.\n`));
806
+ }
807
+ else {
808
+ console.log(paint(SECTION_COLORS.result, ' Still not signed in — starting anyway. Sign in any time and resend your message.\n'));
809
+ }
810
+ }
762
811
  async function main() {
763
812
  console.log(`1Presence Bridge v${version}\n`);
764
813
  if (VERBOSE) {
@@ -778,6 +827,9 @@ async function main() {
778
827
  // ~/.1presence/config.json). In a non-TTY environment this is a no-op and
779
828
  // Claude Code's own default is used.
780
829
  await ensureModelChoice();
830
+ // Local Claude Code sign-in check (see fn comment). Runs before setup/connect
831
+ // so a signed-out user is guided in up front rather than hitting a failed turn.
832
+ await ensureClaudeCodeLogin();
781
833
  // Write system prompt + MCP config. If this fails the bridge is dead in the
782
834
  // water — surface the underlying error rather than letting it bubble up as
783
835
  // a generic "Fatal:" with no context.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@1presence/bridge",
3
- "version": "0.61.0",
3
+ "version": "0.62.0",
4
4
  "description": "Run 1Presence on your Mac and use your Claude.ai Pro subscription from any device",
5
5
  "type": "module",
6
6
  "bin": {