privateer-agent 0.15.0 → 0.15.2

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.
@@ -64,9 +64,9 @@ const VERSION: string = (() => {
64
64
  // The Privateer mark: our symbol — a padlock (with a keyhole) fused into an anchor,
65
65
  // "bring your own model" meets lock-and-key privacy — drawn from the app's logo. It's
66
66
  // rendered with terminal HALF-BLOCKS, so each text row packs TWO pixel rows. The mark is
67
- // a SINGLE colour (every set pixel is the accent), so a cell never needs two different
67
+ // a SINGLE colour (every set pixel is WHITE), so a cell never needs two different
68
68
  // colours: both pixels set → a full block "█", top only → "▀", bottom only → "▄", none →
69
- // a space — all painted with the accent as FOREGROUND, no background cells at all. That
69
+ // a space — all painted with WHITE as FOREGROUND, no background cells at all. That
70
70
  // makes the mark inherit the theme's ink (dark on light, light on dark) with one colour,
71
71
  // and sidesteps the old bg-bleed hazard entirely. Every built line is MARK_W visible
72
72
  // cells wide (SGR escapes don't count), so the text column beside it stays aligned. To
@@ -91,9 +91,9 @@ const PIXELS = [
91
91
  ".OO..OO..OO.", "..OO.OO.OO..",
92
92
  "..OOOOOOOO..", "...OOOOOO...", "....OOOO....", ".....OO.....",
93
93
  ];
94
- // Build the mark for a given palette (the accent is the ink). Cheap — called once per
94
+ // Build the mark for a given palette (WHITE is the ink). Cheap — called once per
95
95
  // header factory invocation, i.e. once per theme, not per frame. Each cell resets SGR so
96
- // the accent can never bleed into the row padding the framer adds after it.
96
+ // the colour can never bleed into the row padding the framer adds after it.
97
97
  function buildMark(p: Palette): string[] {
98
98
  const rows: string[] = [];
99
99
  for (let r = 0; r < PIXELS.length; r += 2) {
@@ -103,9 +103,9 @@ function buildMark(p: Palette): string[] {
103
103
  for (let x = 0; x < MARK_W; x++) {
104
104
  const t = top[x] === "O";
105
105
  const b = bot[x] === "O";
106
- if (t && b) line += `${p.ACCENT}█${p.RESET}`;
107
- else if (t) line += `${p.ACCENT}▀${p.RESET}`;
108
- else if (b) line += `${p.ACCENT}▄${p.RESET}`;
106
+ if (t && b) line += `${p.WHITE}█${p.RESET}`;
107
+ else if (t) line += `${p.WHITE}▀${p.RESET}`;
108
+ else if (b) line += `${p.WHITE}▄${p.RESET}`;
109
109
  else line += " ";
110
110
  }
111
111
  rows.push(line);
@@ -277,7 +277,7 @@ function contextLine(p: Palette): string {
277
277
  const cost = truncated
278
278
  ? `${p.YELLOW} · too big, ${fmtBytes(loadedBytes)} loaded · ${p.INK}/context${p.RESET}`
279
279
  : `${p.DIM} · ~${tok} tok/turn${p.RESET}`;
280
- return `${p.GREEN}⚓${p.DIM} ${p.RESET}${p.INK}${nearest}${p.RESET}${more}${cost}`;
280
+ return `${p.WHITE}⚓${p.DIM} ${p.RESET}${p.INK}${nearest}${p.RESET}${more}${cost}`;
281
281
  }
282
282
 
283
283
  // ── "What's New" — a tiny in-banner changelog ────────────────────────────────
@@ -314,7 +314,7 @@ function renderBanner(width: number, p: Palette, mark: string[], modelProvider?:
314
314
  // sits beside the lock body (not the shackle); the rest follows in reading order.
315
315
  const text: string[] = [
316
316
  "",
317
- `${p.BOLD}${p.ACCENT}✻ ${p.ACCENT}P${p.INK}RIVATEER${p.RESET}${p.DIM} privateer-agent ${p.INK}v${VERSION}${p.RESET}`,
317
+ `${p.BOLD}${p.SKY}✻ ${p.SKY}P${p.INK}RIVATEER${p.RESET}${p.DIM} privateer-agent ${p.INK}v${VERSION}${p.RESET}`,
318
318
  `${p.DIM}Chart your own course privately.${p.RESET}`,
319
319
  "",
320
320
  accountLine(p, modelProvider),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "privateer-agent",
3
- "version": "0.15.0",
3
+ "version": "0.15.2",
4
4
  "description": "Privacy-first terminal coding agent — bring your own model across 20 providers (Anthropic, OpenAI, OpenRouter, Google, local Ollama…). Safe-by-default permissions, MCP, sub-agents, workflows, and verifiable TEE inference. Built on the Pi toolkit.",
5
5
  "type": "module",
6
6
  "license": "MIT",
package/src/cli/chat.ts CHANGED
@@ -336,16 +336,22 @@ async function main() {
336
336
 
337
337
  async function runTurn(text: string, remote: boolean, echo = true): Promise<void> {
338
338
  if (turnActive) {
339
- console.log(`\n${DIM}(busy — a turn is already running)${RESET}`);
340
- // Say it to the DRIVER too, not just to this terminal's own console. An
341
- // app-driven prompt refused here used to vanish: the app had already echoed
342
- // the message into its feed and lit the thinking pill (submit() in
343
- // RemoteDriveContext does both before transmitting), and the only trace of
344
- // the refusal was a dim line on a terminal nobody was looking at. The driver
345
- // saw their own message sit under a spinner forever. A notice does not close
346
- // their turn, and must not — a turn IS running, and it is the one they are
347
- // watching — but it tells them this message was not added to it.
348
- if (remote) relay?.sendNotice("Not sent — this terminal is already running a turn. Wait for it to finish, or press Stop.");
339
+ // A message sent while a turn runs STEERS it, the way typing into a running
340
+ // session does in any agent: Pi delivers it after the tool calls in flight and
341
+ // before the next model call. Refusing it ("busy") left the driver no way to
342
+ // correct course short of Stop, which throws the turn's work away.
343
+ if (remote) console.log(`\n${DIM}⟿ [app] ${text.slice(0, 80)}${RESET}`);
344
+ try {
345
+ const mentions = await resolveMentions(text, cwd);
346
+ await session.steer(mentions.text, mentions.images.length ? mentions.images : undefined);
347
+ console.log(`${DIM}(steering the running turn)${RESET}`);
348
+ } catch (e) {
349
+ // An extension command can't be queued (Pi refuses it mid-stream). Say so to
350
+ // whoever sent it — the app has already echoed the message into its feed.
351
+ const m = `Not sent — ${(e as Error).message || "this can't be added to a running turn"}. Wait for the turn to finish, or press Stop.`;
352
+ console.log(`\n${DIM}${m}${RESET}`);
353
+ if (remote) relay?.sendNotice(m);
354
+ }
349
355
  return;
350
356
  }
351
357
  turnActive = true;
@@ -375,6 +381,12 @@ async function main() {
375
381
  if (remote) relay?.sendNotice(m);
376
382
  }
377
383
  await session.prompt(mentions.text, mentions.images.length ? { images: mentions.images } : undefined);
384
+ // A steer that landed after the agent's last check (while its final reply was
385
+ // streaming) sits in Pi's queue with nothing left to pick it up. Run it now
386
+ // rather than leave the driver's message unanswered until some later prompt.
387
+ for (let q = session.clearQueue?.(); q && q.steering.length + q.followUp.length > 0; q = session.clearQueue?.()) {
388
+ await session.prompt([...q.steering, ...q.followUp].join("\n\n"));
389
+ }
378
390
  } catch (e) {
379
391
  failure = (e as Error).message || "The turn failed.";
380
392
  console.log(`\n${RED}${failure}${RESET}`);
@@ -1,10 +1,10 @@
1
1
  {
2
2
  "$comment": "The banner's What's New list, newest first: the two to four things a returning user should notice, not a changelog. `reviewedFor` is the release this list was last looked at for. A release can't ship until it equals package.json's version (scripts/check-whats-new.mjs, run by `npm test` and by release.yml before anything builds), so every release, patches included, is a deliberate look at this list. If nothing new is worth a line, bumping `reviewedFor` alone is the answer. `since` is the release an entry arrived in. `cmd` is shown in the accent colour.",
3
- "reviewedFor": "0.15.0",
3
+ "reviewedFor": "0.15.2",
4
4
  "items": [
5
+ { "text": "Driving from the app: a message sent mid-turn steers it, and questions survive a dropped link", "since": "0.15.1" },
5
6
  { "text": "MCP on Pi's built-in support — sign OAuth connectors back in with", "cmd": "/mcp", "since": "0.15.0" },
6
7
  { "text": "Sub-agents are back: hand work to bounded parallel agents", "since": "0.15.0" },
7
- { "text": "API keys can expire and carry their own credit limit — set them in the app", "since": "0.14.6" },
8
- { "text": "Save to Cargo with no app attached — it lands on your next app open", "since": "0.14.4" }
8
+ { "text": "API keys can expire and carry their own credit limit — set them in the app", "since": "0.14.6" }
9
9
  ]
10
10
  }
@@ -227,6 +227,485 @@ export const MCP_CATALOG: CatalogEntry[] = [
227
227
  needs: "oauth",
228
228
  },
229
229
 
230
+ // ── More remote OAuth servers (added 2026-10-20) ───────────────────────────
231
+ // Mirrors the block of the same name in treeview client/components/mcpCatalog.ts,
232
+ // minus the rows whose registration refuses a loopback redirect (Calendly). Each
233
+ // was probed on 2026-10-20: initialize → 401, OAuth discovery, and a dynamic
234
+ // client registration with http://127.0.0.1:<port>/callback. A full consent →
235
+ // tools/list round trip has not been run.
236
+ {
237
+ // Distinct id from the stdio `notion` entry: the two coexist, and this one
238
+ // needs no integration token.
239
+ id: "notion-remote",
240
+ name: "notion-remote",
241
+ label: "Notion",
242
+ blurb: "Pages, databases, and search. Sign in via browser.",
243
+ transport: "http",
244
+ url: "https://mcp.notion.com/mcp",
245
+ oauth: true,
246
+ needs: "oauth",
247
+ },
248
+ {
249
+ id: "supabase-remote",
250
+ name: "supabase-remote",
251
+ label: "Supabase",
252
+ blurb: "Projects, tables, and SQL. Sign in via browser.",
253
+ transport: "http",
254
+ url: "https://mcp.supabase.com/mcp",
255
+ oauth: true,
256
+ needs: "oauth",
257
+ },
258
+ {
259
+ id: "gitlab",
260
+ name: "gitlab",
261
+ label: "GitLab",
262
+ blurb: "Projects, issues, and merge requests. Sign in via browser.",
263
+ transport: "http",
264
+ url: "https://gitlab.com/api/v4/mcp",
265
+ oauth: true,
266
+ needs: "oauth",
267
+ },
268
+ {
269
+ id: "vercel",
270
+ name: "vercel",
271
+ label: "Vercel",
272
+ blurb: "Deployments, logs, and projects. Sign in via browser.",
273
+ transport: "http",
274
+ url: "https://mcp.vercel.com/",
275
+ oauth: true,
276
+ needs: "oauth",
277
+ },
278
+ {
279
+ id: "netlify",
280
+ name: "netlify",
281
+ label: "Netlify",
282
+ blurb: "Sites, deploys, and forms. Sign in via browser.",
283
+ transport: "http",
284
+ url: "https://netlify-mcp.netlify.app/mcp",
285
+ oauth: true,
286
+ needs: "oauth",
287
+ },
288
+ {
289
+ id: "cloudflare",
290
+ name: "cloudflare",
291
+ label: "Cloudflare",
292
+ blurb: "Your whole Cloudflare account through its API. Sign in via browser.",
293
+ transport: "http",
294
+ url: "https://mcp.cloudflare.com/mcp",
295
+ oauth: true,
296
+ needs: "oauth",
297
+ },
298
+ {
299
+ id: "cloudflare-workers",
300
+ name: "cloudflare-workers",
301
+ label: "Cloudflare Workers",
302
+ blurb: "Workers, KV, R2, and D1. Sign in via browser.",
303
+ transport: "http",
304
+ url: "https://bindings.mcp.cloudflare.com/mcp",
305
+ oauth: true,
306
+ needs: "oauth",
307
+ },
308
+ {
309
+ id: "cloudflare-observability",
310
+ name: "cloudflare-observability",
311
+ label: "Cloudflare Observability",
312
+ blurb: "Worker logs and analytics. Sign in via browser.",
313
+ transport: "http",
314
+ url: "https://observability.mcp.cloudflare.com/mcp",
315
+ oauth: true,
316
+ needs: "oauth",
317
+ },
318
+ {
319
+ id: "neon",
320
+ name: "neon",
321
+ label: "Neon",
322
+ blurb: "Serverless Postgres projects, branches, and SQL. Sign in via browser.",
323
+ transport: "http",
324
+ url: "https://mcp.neon.tech/mcp",
325
+ oauth: true,
326
+ needs: "oauth",
327
+ },
328
+ {
329
+ id: "prisma",
330
+ name: "prisma",
331
+ label: "Prisma Postgres",
332
+ blurb: "Databases, migrations, and queries. Sign in via browser.",
333
+ transport: "http",
334
+ url: "https://mcp.prisma.io/mcp",
335
+ oauth: true,
336
+ needs: "oauth",
337
+ },
338
+ {
339
+ id: "instantdb",
340
+ name: "instantdb",
341
+ label: "InstantDB",
342
+ blurb: "Apps, schemas, and data. Sign in via browser.",
343
+ transport: "http",
344
+ url: "https://mcp.instantdb.com/mcp",
345
+ oauth: true,
346
+ needs: "oauth",
347
+ },
348
+ {
349
+ id: "buildkite",
350
+ name: "buildkite",
351
+ label: "Buildkite",
352
+ blurb: "Pipelines, builds, and jobs. Sign in via browser.",
353
+ transport: "http",
354
+ url: "https://mcp.buildkite.com/mcp",
355
+ oauth: true,
356
+ needs: "oauth",
357
+ },
358
+ {
359
+ id: "grafana",
360
+ name: "grafana",
361
+ label: "Grafana",
362
+ blurb: "Dashboards, metrics, and alerts. Sign in via browser.",
363
+ transport: "http",
364
+ url: "https://mcp.grafana.com/mcp",
365
+ oauth: true,
366
+ needs: "oauth",
367
+ },
368
+ {
369
+ id: "honeycomb",
370
+ name: "honeycomb",
371
+ label: "Honeycomb",
372
+ blurb: "Traces, queries, and SLOs. Sign in via browser.",
373
+ transport: "http",
374
+ url: "https://mcp.honeycomb.io/mcp",
375
+ oauth: true,
376
+ needs: "oauth",
377
+ },
378
+ {
379
+ id: "posthog",
380
+ name: "posthog",
381
+ label: "PostHog",
382
+ blurb: "Product analytics, flags, and errors. Sign in via browser.",
383
+ transport: "http",
384
+ url: "https://mcp.posthog.com/mcp",
385
+ oauth: true,
386
+ needs: "oauth",
387
+ },
388
+ {
389
+ id: "amplitude",
390
+ name: "amplitude",
391
+ label: "Amplitude",
392
+ blurb: "Charts, cohorts, and experiments. Sign in via browser.",
393
+ transport: "http",
394
+ url: "https://mcp.amplitude.com/mcp",
395
+ oauth: true,
396
+ needs: "oauth",
397
+ },
398
+ {
399
+ id: "mixpanel",
400
+ name: "mixpanel",
401
+ label: "Mixpanel",
402
+ blurb: "Events, funnels, and reports. Sign in via browser.",
403
+ transport: "http",
404
+ url: "https://mcp.mixpanel.com/mcp",
405
+ oauth: true,
406
+ needs: "oauth",
407
+ },
408
+ {
409
+ id: "semgrep",
410
+ name: "semgrep",
411
+ label: "Semgrep",
412
+ blurb: "Scan code for security issues. Sign in via browser.",
413
+ transport: "http",
414
+ url: "https://mcp.semgrep.ai/mcp",
415
+ oauth: true,
416
+ needs: "oauth",
417
+ },
418
+ {
419
+ id: "port",
420
+ name: "port",
421
+ label: "Port",
422
+ blurb: "Your software catalog and developer portal. Sign in via browser.",
423
+ transport: "http",
424
+ url: "https://mcp.port.io/v1",
425
+ oauth: true,
426
+ needs: "oauth",
427
+ },
428
+ {
429
+ id: "stytch",
430
+ name: "stytch",
431
+ label: "Stytch",
432
+ blurb: "Auth projects, users, and settings. Sign in via browser.",
433
+ transport: "http",
434
+ url: "https://mcp.stytch.dev/mcp",
435
+ oauth: true,
436
+ needs: "oauth",
437
+ },
438
+ {
439
+ id: "globalping",
440
+ name: "globalping",
441
+ label: "Globalping",
442
+ blurb: "Ping, traceroute, and DNS from probes worldwide. Sign in via browser.",
443
+ transport: "http",
444
+ url: "https://mcp.globalping.dev/mcp",
445
+ oauth: true,
446
+ needs: "oauth",
447
+ },
448
+ {
449
+ id: "apify",
450
+ name: "apify",
451
+ label: "Apify",
452
+ blurb: "Run scrapers and actors. Sign in via browser.",
453
+ transport: "http",
454
+ url: "https://mcp.apify.com",
455
+ oauth: true,
456
+ needs: "oauth",
457
+ },
458
+ {
459
+ id: "parallel-search",
460
+ name: "parallel-search",
461
+ label: "Parallel Search",
462
+ blurb: "Web search built for agents. Sign in via browser.",
463
+ transport: "http",
464
+ url: "https://search-mcp.parallel.ai/mcp",
465
+ oauth: true,
466
+ needs: "oauth",
467
+ },
468
+ {
469
+ id: "clickup",
470
+ name: "clickup",
471
+ label: "ClickUp",
472
+ blurb: "Tasks, docs, and spaces. Sign in via browser.",
473
+ transport: "http",
474
+ url: "https://mcp.clickup.com/mcp",
475
+ oauth: true,
476
+ needs: "oauth",
477
+ },
478
+ {
479
+ id: "monday",
480
+ name: "monday",
481
+ label: "monday.com",
482
+ blurb: "Boards, items, and updates. Sign in via browser.",
483
+ transport: "http",
484
+ url: "https://mcp.monday.com/mcp",
485
+ oauth: true,
486
+ needs: "oauth",
487
+ },
488
+ {
489
+ id: "todoist",
490
+ name: "todoist",
491
+ label: "Todoist",
492
+ blurb: "Tasks, projects, and due dates. Sign in via browser.",
493
+ transport: "http",
494
+ url: "https://ai.todoist.net/mcp",
495
+ oauth: true,
496
+ needs: "oauth",
497
+ },
498
+ {
499
+ id: "airtable",
500
+ name: "airtable",
501
+ label: "Airtable",
502
+ blurb: "Bases, tables, and records. Sign in via browser.",
503
+ transport: "http",
504
+ url: "https://mcp.airtable.com/mcp",
505
+ oauth: true,
506
+ needs: "oauth",
507
+ },
508
+ {
509
+ id: "miro",
510
+ name: "miro",
511
+ label: "Miro",
512
+ blurb: "Boards, sticky notes, and diagrams. Sign in via browser.",
513
+ transport: "http",
514
+ url: "https://mcp.miro.com/",
515
+ oauth: true,
516
+ needs: "oauth",
517
+ },
518
+ {
519
+ id: "canva",
520
+ name: "canva",
521
+ label: "Canva",
522
+ blurb: "Find, create, and export designs. Sign in via browser.",
523
+ transport: "http",
524
+ url: "https://mcp.canva.com/mcp",
525
+ oauth: true,
526
+ needs: "oauth",
527
+ },
528
+ {
529
+ id: "webflow",
530
+ name: "webflow",
531
+ label: "Webflow",
532
+ blurb: "Sites, pages, and CMS collections. Sign in via browser.",
533
+ transport: "http",
534
+ url: "https://mcp.webflow.com/mcp",
535
+ oauth: true,
536
+ needs: "oauth",
537
+ },
538
+ {
539
+ id: "wix",
540
+ name: "wix",
541
+ label: "Wix",
542
+ blurb: "Sites, stores, and bookings. Sign in via browser.",
543
+ transport: "http",
544
+ url: "https://mcp.wix.com/mcp",
545
+ oauth: true,
546
+ needs: "oauth",
547
+ },
548
+ {
549
+ id: "cloudinary",
550
+ name: "cloudinary",
551
+ label: "Cloudinary",
552
+ blurb: "Upload, search, and transform media assets. Sign in via browser.",
553
+ transport: "http",
554
+ url: "https://asset-management.mcp.cloudinary.com/mcp",
555
+ oauth: true,
556
+ needs: "oauth",
557
+ },
558
+ {
559
+ id: "attio",
560
+ name: "attio",
561
+ label: "Attio",
562
+ blurb: "CRM records, lists, and notes. Sign in via browser.",
563
+ transport: "http",
564
+ url: "https://mcp.attio.com/mcp",
565
+ oauth: true,
566
+ needs: "oauth",
567
+ },
568
+ {
569
+ id: "intercom",
570
+ name: "intercom",
571
+ label: "Intercom",
572
+ blurb: "Conversations, contacts, and tickets. Sign in via browser.",
573
+ transport: "http",
574
+ url: "https://mcp.intercom.com/mcp",
575
+ oauth: true,
576
+ needs: "oauth",
577
+ },
578
+ {
579
+ id: "zapier",
580
+ name: "zapier",
581
+ label: "Zapier",
582
+ blurb: "Run actions across thousands of apps. Sign in via browser.",
583
+ transport: "http",
584
+ url: "https://mcp.zapier.com/api/mcp/mcp",
585
+ oauth: true,
586
+ needs: "oauth",
587
+ },
588
+ {
589
+ id: "paypal",
590
+ name: "paypal",
591
+ label: "PayPal",
592
+ blurb: "Invoices, orders, and transactions. Sign in via browser.",
593
+ transport: "http",
594
+ url: "https://mcp.paypal.com/mcp",
595
+ oauth: true,
596
+ needs: "oauth",
597
+ },
598
+ {
599
+ id: "square",
600
+ name: "square",
601
+ label: "Square",
602
+ blurb: "Payments, catalog, and orders. Sign in via browser.",
603
+ transport: "http",
604
+ url: "https://mcp.squareup.com/mcp",
605
+ oauth: true,
606
+ needs: "oauth",
607
+ },
608
+ {
609
+ id: "ramp",
610
+ name: "ramp",
611
+ label: "Ramp",
612
+ blurb: "Spend, cards, and reimbursements. Sign in via browser.",
613
+ transport: "http",
614
+ url: "https://ramp-mcp-remote.ramp.com/mcp",
615
+ oauth: true,
616
+ needs: "oauth",
617
+ },
618
+ {
619
+ id: "morningstar",
620
+ name: "morningstar",
621
+ label: "Morningstar",
622
+ blurb: "Fund and stock research data. Sign in via browser.",
623
+ transport: "http",
624
+ url: "https://mcp.morningstar.com/mcp",
625
+ oauth: true,
626
+ needs: "oauth",
627
+ },
628
+ {
629
+ id: "thoughtspot",
630
+ name: "thoughtspot",
631
+ label: "ThoughtSpot",
632
+ blurb: "Ask questions of your analytics data. Sign in via browser.",
633
+ transport: "http",
634
+ url: "https://agent.thoughtspot.app/mcp",
635
+ oauth: true,
636
+ needs: "oauth",
637
+ },
638
+ {
639
+ id: "jam",
640
+ name: "jam",
641
+ label: "Jam",
642
+ blurb: "Bug reports with recordings and logs. Sign in via browser.",
643
+ transport: "http",
644
+ url: "https://mcp.jam.dev/mcp",
645
+ oauth: true,
646
+ needs: "oauth",
647
+ },
648
+ {
649
+ id: "granola",
650
+ name: "granola",
651
+ label: "Granola",
652
+ blurb: "Your meeting notes. Sign in via browser.",
653
+ transport: "http",
654
+ url: "https://mcp.granola.ai/mcp",
655
+ oauth: true,
656
+ needs: "oauth",
657
+ },
658
+ {
659
+ id: "fathom",
660
+ name: "fathom",
661
+ label: "Fathom",
662
+ blurb: "Meeting recordings, transcripts, and summaries. Sign in via browser.",
663
+ transport: "http",
664
+ url: "https://api.fathom.ai/mcp",
665
+ oauth: true,
666
+ needs: "oauth",
667
+ },
668
+ {
669
+ id: "fireflies",
670
+ name: "fireflies",
671
+ label: "Fireflies.ai",
672
+ blurb: "Meeting transcripts and summaries. Sign in via browser.",
673
+ transport: "http",
674
+ url: "https://api.fireflies.ai/mcp",
675
+ oauth: true,
676
+ needs: "oauth",
677
+ },
678
+ {
679
+ id: "otter",
680
+ name: "otter",
681
+ label: "Otter.ai",
682
+ blurb: "Meeting transcripts and notes. Sign in via browser.",
683
+ transport: "http",
684
+ url: "https://mcp.otter.ai/mcp",
685
+ oauth: true,
686
+ needs: "oauth",
687
+ },
688
+ {
689
+ id: "read-ai",
690
+ name: "read-ai",
691
+ label: "Read AI",
692
+ blurb: "Meeting reports and recaps. Sign in via browser.",
693
+ transport: "http",
694
+ url: "https://api.read.ai/mcp",
695
+ oauth: true,
696
+ needs: "oauth",
697
+ },
698
+ {
699
+ id: "tldv",
700
+ name: "tldv",
701
+ label: "tl;dv",
702
+ blurb: "Meeting recordings and highlights. Sign in via browser.",
703
+ transport: "http",
704
+ url: "https://mcp.tldv.io/mcp",
705
+ oauth: true,
706
+ needs: "oauth",
707
+ },
708
+
230
709
  // ── Local, token-authorized ─────────────────────────────────────────────────
231
710
  {
232
711
  id: "brave-search",
@@ -138,7 +138,14 @@ export async function createLiveTaskSession(spec: TaskSpec, deps: LiveTaskDeps):
138
138
  };
139
139
 
140
140
  const runTurn = async (text: string): Promise<void> => {
141
- if (turnActive || stopped) return;
141
+ if (stopped) return;
142
+ // A message sent while a turn runs steers it (delivered before the next model call)
143
+ // rather than being dropped — the driver must be able to correct course mid-turn.
144
+ if (turnActive) {
145
+ try { await session.steer(text); }
146
+ catch (e) { bridge.sendNotice(`Not sent — ${(e as Error).message || "this can't be added to a running turn"}.`); }
147
+ return;
148
+ }
142
149
  turnActive = true;
143
150
  let failure: string | undefined;
144
151
  lastAnswer = ""; // keep the CLOSING answer, not the whole session
@@ -160,6 +167,10 @@ export async function createLiveTaskSession(spec: TaskSpec, deps: LiveTaskDeps):
160
167
  `Use the save_attachment tool with the ref number to write one to disk.]`
161
168
  : "";
162
169
  await session.prompt(text + note);
170
+ // A steer that arrived after the agent's last check would otherwise sit unread.
171
+ for (let q = session.clearQueue?.(); q && q.steering.length + q.followUp.length > 0; q = session.clearQueue?.()) {
172
+ await session.prompt([...q.steering, ...q.followUp].join("\n\n"));
173
+ }
163
174
  } catch (e) {
164
175
  turnErrored = true;
165
176
  failure = (e as Error).message || "The turn failed.";