@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.
- package/dist/index.js +96 -0
- 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;
|