@1presence/bridge 0.51.0 → 0.54.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/claude.js CHANGED
@@ -234,7 +234,7 @@ async function* promptStream(messages) {
234
234
  }
235
235
  // ─── Spawn (drive one turn through the SDK) ──────────────────────────────────────
236
236
  export function spawnClaude(params) {
237
- const { conversationId, presenceSessionId, text, uid, history, vaultFileOpen, clientCapabilities, syncedFolders, onEvent, onDone, onError, onNotice } = params;
237
+ const { conversationId, presenceSessionId, text, uid, history, vaultFileOpen, clientCapabilities, syncedFolders, model: perTurnModel, onEvent, onDone, onError, onNotice } = params;
238
238
  const systemPromptPath = join(tmpdir(), `agent-${uid}.md`);
239
239
  const mcpConfigPath = join(tmpdir(), `mcp-${uid}.json`);
240
240
  if (verbose) {
@@ -337,7 +337,7 @@ export function spawnClaude(params) {
337
337
  if (!safeEnv['MAX_MCP_OUTPUT_TOKENS']) {
338
338
  safeEnv['MAX_MCP_OUTPUT_TOKENS'] = '200000';
339
339
  }
340
- const pinnedModel = getBridgeModel();
340
+ const pinnedModel = perTurnModel ?? getBridgeModel();
341
341
  // Process one translated raw stream-json event: bookkeeping + forward. Mirrors
342
342
  // the old CLI stdout parser so the gateway/accumulator see identical shapes.
343
343
  // Returns false when the event must be suppressed (errors) or the turn was
package/dist/index.js CHANGED
@@ -5,6 +5,7 @@ import { tmpdir } from 'os';
5
5
  import { join, dirname } from 'path';
6
6
  import { fileURLToPath } from 'url';
7
7
  import { createRequire } from 'module';
8
+ import { query } from '@anthropic-ai/claude-agent-sdk';
8
9
  import { getValidAuth, ensureFreshToken, forceRefreshToken, isTokenValid, AuthCancelledError } from './auth.js';
9
10
  import { spawnClaude, killAll, cancelConversation, setVerbose, setDebug, paint, SECTION_COLORS } from './claude.js';
10
11
  import { ensureModelChoice } from './config.js';
@@ -218,8 +219,91 @@ const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/
218
219
  function isUuid(value) {
219
220
  return UUID_RE.test(value);
220
221
  }
222
+ // ─── Bridge vision reconstruction (no MCP, no chat turn) ──────────────────────
223
+ //
224
+ // A lightweight path for doc reproduction: the gateway sends a signed GCS URL +
225
+ // system prompt. We download the document and call query() with NO MCP tools —
226
+ // just a direct vision call using the user's claude.ai subscription. The whole
227
+ // reply is collected into one string and sent back as doc_recon_response.
228
+ async function handleDocReconRequest(req) {
229
+ const send = (payload) => {
230
+ if (currentWs?.readyState === WebSocket.OPEN)
231
+ currentWs.send(JSON.stringify(payload));
232
+ };
233
+ let base64;
234
+ let mediaType;
235
+ try {
236
+ const res = await fetch(req.sourceUrl);
237
+ if (!res.ok)
238
+ throw new Error(`download failed: ${res.status}`);
239
+ const buf = await res.arrayBuffer();
240
+ base64 = Buffer.from(buf).toString('base64');
241
+ mediaType = req.sourceContentType || 'application/pdf';
242
+ }
243
+ catch (err) {
244
+ console.error(`[bridge] doc_recon: download failed (reqId: ${req.reqId}): ${err.message}`);
245
+ send({ type: 'doc_recon_response', reqId: req.reqId, error: `download failed: ${err.message}` });
246
+ return;
247
+ }
248
+ const isImage = mediaType.startsWith('image/');
249
+ const sourceBlock = isImage
250
+ ? { type: 'image', source: { type: 'base64', media_type: mediaType, data: base64 } }
251
+ : { type: 'document', source: { type: 'base64', media_type: 'application/pdf', data: base64 } };
252
+ // Strip API key so this uses the user's claude.ai subscription (same as every
253
+ // bridge turn). Spread the rest of the env through — options.env REPLACES.
254
+ const { ANTHROPIC_API_KEY: _stripped, ...safeEnv } = process.env;
255
+ if (!safeEnv['MAX_MCP_OUTPUT_TOKENS'])
256
+ safeEnv['MAX_MCP_OUTPUT_TOKENS'] = '200000';
257
+ const abort = new AbortController();
258
+ let assistantText = '';
259
+ const options = {
260
+ systemPrompt: req.systemPrompt,
261
+ mcpServers: {},
262
+ strictMcpConfig: true,
263
+ settingSources: [],
264
+ allowedTools: [],
265
+ tools: [],
266
+ cwd: join(tmpdir(), '1presence-bridge'),
267
+ abortController: abort,
268
+ includePartialMessages: false,
269
+ permissionMode: 'default',
270
+ env: safeEnv,
271
+ };
272
+ // Cast through `unknown[]` like buildPromptMessages in claude.ts — the SDK's
273
+ // discriminated content-block union won't accept the inferred-`string` `type`
274
+ // discriminants on these literals, but the shape is correct at runtime.
275
+ const promptMessages = [{
276
+ type: 'user',
277
+ message: { role: 'user', content: [sourceBlock, { type: 'text', text: req.userText }] },
278
+ parent_tool_use_id: null,
279
+ }];
280
+ async function* promptGen() { for (const m of promptMessages)
281
+ yield m; }
282
+ try {
283
+ console.log(paint('90', `[bridge] doc_recon: reqId ${req.reqId} — reconstructing`));
284
+ for await (const m of query({ prompt: promptGen(), options })) {
285
+ if (m.isReplay)
286
+ continue;
287
+ if (m.type === 'assistant') {
288
+ const am = m;
289
+ for (const block of (am.message?.content ?? [])) {
290
+ if (block.type === 'text' && block.text)
291
+ assistantText += block.text;
292
+ }
293
+ }
294
+ }
295
+ console.log(paint('90', `[bridge] doc_recon: reqId ${req.reqId} — done (${assistantText.length} chars)`));
296
+ send({ type: 'doc_recon_response', reqId: req.reqId, text: assistantText });
297
+ }
298
+ catch (err) {
299
+ if (!abort.signal.aborted) {
300
+ console.error(`[bridge] doc_recon: query failed (reqId: ${req.reqId}): ${err.message}`);
301
+ send({ type: 'doc_recon_response', reqId: req.reqId, error: err.message });
302
+ }
303
+ }
304
+ }
221
305
  // ─── Handle a single incoming message (token refresh + spawn) ─────────────────
222
- async function handleMessage(conversationId, text, sessionId, history, auth, vaultFileOpen, clientCapabilities, syncedFolders, agentSlug) {
306
+ async function handleMessage(conversationId, text, sessionId, history, auth, vaultFileOpen, clientCapabilities, syncedFolders, agentSlug, model) {
223
307
  // Refresh JWT if <10 min remaining before spawning Claude
224
308
  let activeAuth = auth;
225
309
  try {
@@ -322,6 +406,7 @@ async function handleMessage(conversationId, text, sessionId, history, auth, vau
322
406
  vaultFileOpen,
323
407
  clientCapabilities,
324
408
  syncedFolders,
409
+ model,
325
410
  onEvent: (event) => {
326
411
  accumulator.consume(event);
327
412
  if (!responding && event['type'] === 'assistant') {
@@ -498,14 +583,26 @@ function connect(auth, retryDelay = 1000) {
498
583
  console.log(`[bridge] ✕ stopped conversation ${msg.conversationId}`);
499
584
  return;
500
585
  }
586
+ if (msg.type === 'doc_recon_request') {
587
+ const req = msg;
588
+ if (req.reqId && req.systemPrompt && req.sourceUrl) {
589
+ handleDocReconRequest(req).catch((err) => {
590
+ console.error(`[bridge] doc_recon: unhandled error (reqId: ${req.reqId}): ${err.message}`);
591
+ if (currentWs?.readyState === WebSocket.OPEN) {
592
+ currentWs.send(JSON.stringify({ type: 'doc_recon_response', reqId: req.reqId, error: err.message }));
593
+ }
594
+ });
595
+ }
596
+ return;
597
+ }
501
598
  if (msg.type !== 'message' || !msg.conversationId || !msg.text)
502
599
  return;
503
- const { conversationId, text, sessionId, history, vaultFileOpen, clientCapabilities, syncedFolders, agentSlug } = msg;
600
+ const { conversationId, text, sessionId, history, vaultFileOpen, clientCapabilities, syncedFolders, agentSlug, model } = msg;
504
601
  const ts = new Date().toLocaleTimeString();
505
602
  const hist = Array.isArray(history) ? history : [];
506
603
  console.log(`[${ts}] ▶ ${text}${hist.length ? ` (history: ${hist.length} turn${hist.length === 1 ? '' : 's'})` : ''}`);
507
604
  startTurnTimer();
508
- handleMessage(conversationId, text, sessionId ?? null, hist, auth, vaultFileOpen, clientCapabilities, syncedFolders, agentSlug).catch((err) => {
605
+ handleMessage(conversationId, text, sessionId ?? null, hist, auth, vaultFileOpen, clientCapabilities, syncedFolders, agentSlug, model).catch((err) => {
509
606
  stopTurnTimer();
510
607
  console.error(`[bridge] handleMessage error: ${err.message}`);
511
608
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@1presence/bridge",
3
- "version": "0.51.0",
3
+ "version": "0.54.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": {