@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.
- package/dist/claudeAuth.js +130 -0
- package/dist/index.js +52 -0
- package/package.json +1 -1
|
@@ -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.
|