@1presence/bridge 0.52.0 → 0.55.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.
Files changed (2) hide show
  1. package/dist/index.js +96 -0
  2. package/package.json +1 -1
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,6 +219,89 @@ 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
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
@@ -499,6 +583,18 @@ function connect(auth, retryDelay = 1000) {
499
583
  console.log(`[bridge] ✕ stopped conversation ${msg.conversationId}`);
500
584
  return;
501
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
+ }
502
598
  if (msg.type !== 'message' || !msg.conversationId || !msg.text)
503
599
  return;
504
600
  const { conversationId, text, sessionId, history, vaultFileOpen, clientCapabilities, syncedFolders, agentSlug, model } = msg;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@1presence/bridge",
3
- "version": "0.52.0",
3
+ "version": "0.55.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": {