osborn 0.9.86 → 0.9.88
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 +75 -8
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -201,6 +201,22 @@ let leaveRoomHook = null;
|
|
|
201
201
|
// as the existing fetch-log/save-log flow so we don't ship Supabase credentials
|
|
202
202
|
// to the Fly machine.
|
|
203
203
|
let bugReportHook = null;
|
|
204
|
+
const canvasClients = new Set();
|
|
205
|
+
let latestCanvasShow = null; // last 'show' so a reconnecting canvas resyncs its visual
|
|
206
|
+
function pushCanvas(evt) {
|
|
207
|
+
if (evt.kind === 'show')
|
|
208
|
+
latestCanvasShow = evt;
|
|
209
|
+
const line = `data: ${JSON.stringify(evt)}\n\n`;
|
|
210
|
+
for (const res of canvasClients) {
|
|
211
|
+
try {
|
|
212
|
+
res.write(line);
|
|
213
|
+
}
|
|
214
|
+
catch {
|
|
215
|
+
canvasClients.delete(res);
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
console.log(`🖼️ canvas ${evt.kind}: ${evt.kind === 'say' ? evt.text.slice(0, 60) : evt.mode} → ${canvasClients.size} client(s)`);
|
|
219
|
+
}
|
|
204
220
|
function startApiServer(workingDir, port) {
|
|
205
221
|
const server = createServer(async (req, res) => {
|
|
206
222
|
// CORS headers for cloud frontend
|
|
@@ -476,6 +492,50 @@ function startApiServer(workingDir, port) {
|
|
|
476
492
|
console.log('[events] SSE client connected');
|
|
477
493
|
return;
|
|
478
494
|
}
|
|
495
|
+
// ── Meeting canvas: SSE stream the Recall webpage subscribes to ──────────
|
|
496
|
+
// The canvas page (frontend /meeting-canvas) opens this as an EventSource.
|
|
497
|
+
// Recall renders that page as the bot's camera+mic, so whatever we push here
|
|
498
|
+
// becomes what the meeting sees + hears. On connect we resync the current
|
|
499
|
+
// visual so a page reload doesn't blank the bot's camera.
|
|
500
|
+
if (req.method === 'GET' && url.pathname === '/canvas-stream') {
|
|
501
|
+
res.writeHead(200, {
|
|
502
|
+
'Content-Type': 'text/event-stream',
|
|
503
|
+
'Cache-Control': 'no-cache',
|
|
504
|
+
'Connection': 'keep-alive',
|
|
505
|
+
'X-Accel-Buffering': 'no',
|
|
506
|
+
});
|
|
507
|
+
res.write(`: canvas connected ${new Date().toISOString()}\n\n`);
|
|
508
|
+
res.write(`data: ${JSON.stringify(latestCanvasShow ?? { kind: 'show', mode: 'idle' })}\n\n`);
|
|
509
|
+
canvasClients.add(res);
|
|
510
|
+
const hb = setInterval(() => { try {
|
|
511
|
+
res.write(`: ping ${Date.now()}\n\n`);
|
|
512
|
+
}
|
|
513
|
+
catch { } }, 10_000);
|
|
514
|
+
req.on('close', () => { clearInterval(hb); canvasClients.delete(res); console.log('[canvas] client disconnected'); });
|
|
515
|
+
console.log(`[canvas] client connected (${canvasClients.size} total)`);
|
|
516
|
+
return;
|
|
517
|
+
}
|
|
518
|
+
// ── Meeting canvas: control endpoint (director / agent tool) ─────────────
|
|
519
|
+
// POST /canvas { kind:'say', text } | { kind:'show', mode, title?, items?, url?, text? }
|
|
520
|
+
if (req.method === 'POST' && url.pathname === '/canvas') {
|
|
521
|
+
let body = '';
|
|
522
|
+
req.on('data', (c) => { body += c; });
|
|
523
|
+
req.on('end', () => {
|
|
524
|
+
try {
|
|
525
|
+
const evt = JSON.parse(body || '{}');
|
|
526
|
+
if (evt.kind !== 'say' && evt.kind !== 'show')
|
|
527
|
+
throw new Error("kind must be 'say' or 'show'");
|
|
528
|
+
pushCanvas(evt);
|
|
529
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
530
|
+
res.end(JSON.stringify({ ok: true, clients: canvasClients.size }));
|
|
531
|
+
}
|
|
532
|
+
catch (e) {
|
|
533
|
+
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
534
|
+
res.end(JSON.stringify({ error: e.message }));
|
|
535
|
+
}
|
|
536
|
+
});
|
|
537
|
+
return;
|
|
538
|
+
}
|
|
479
539
|
// GET /sessions/export — stream a gzipped tar of ~/.claude/projects/ AND
|
|
480
540
|
// ~/.claude/skills/ to the client. Both directories ship in one archive so
|
|
481
541
|
// a sync covers conversations (projects/) and learned skills together —
|
|
@@ -4625,15 +4685,22 @@ async function main() {
|
|
|
4625
4685
|
(process.env.FLY_APP_NAME
|
|
4626
4686
|
? `https://${process.env.FLY_APP_NAME}.fly.dev`
|
|
4627
4687
|
: `http://localhost:${apiPort}`);
|
|
4628
|
-
//
|
|
4629
|
-
//
|
|
4630
|
-
//
|
|
4631
|
-
//
|
|
4688
|
+
// Transcript: Recall captures meeting audio internally; we get it
|
|
4689
|
+
// live via the realtime webhook (+ REST poll backstop). Output:
|
|
4690
|
+
// the bot casts the meeting canvas as its camera+mic (below), so
|
|
4691
|
+
// it can show visuals and speak into the meeting on demand.
|
|
4632
4692
|
await sendToFrontend({ type: 'meeting_joining', message: 'Osborn is joining your meeting...' });
|
|
4633
|
-
//
|
|
4634
|
-
//
|
|
4635
|
-
//
|
|
4636
|
-
|
|
4693
|
+
// Cast target = the bot's camera+mic webpage (Recall output_media).
|
|
4694
|
+
// DEFAULT is the meeting canvas (frontend /meeting-canvas), pointed
|
|
4695
|
+
// at THIS agent's public URL so it can subscribe to /canvas-stream
|
|
4696
|
+
// and become the bot's face (visuals) + voice (TTS it speaks). An
|
|
4697
|
+
// explicit castUrl from the frontend or OSBORN_MEETING_CAST_URL
|
|
4698
|
+
// overrides it (e.g. to cast a live feed or a seeded site instead).
|
|
4699
|
+
const frontendUrl = (process.env.OSBORN_FRONTEND_URL || 'https://www.voice-native.com').replace(/\/$/, '');
|
|
4700
|
+
const canvasUrl = /^https:\/\//.test(webhookBase)
|
|
4701
|
+
? `${frontendUrl}/meeting-canvas?agent=${encodeURIComponent(webhookBase)}`
|
|
4702
|
+
: undefined;
|
|
4703
|
+
const castUrl = data.castUrl || process.env.OSBORN_MEETING_CAST_URL || canvasUrl;
|
|
4637
4704
|
const botId = await recallJoin.joinMeeting(meetingUrl, webhookBase, { castUrl });
|
|
4638
4705
|
const sessionId = currentLLM?.sessionId || currentResumeSessionId || 'default';
|
|
4639
4706
|
recallJoin.registerBot(botId, sessionId);
|