@corsair-dev/studio 0.1.2 → 0.1.4

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.
@@ -341,6 +341,594 @@ var exchangeOAuth = async (ctx) => {
341
341
  return { ok: true };
342
342
  };
343
343
 
344
+ // src/server/handlers/chat.ts
345
+ import { buildCorsairToolDefs } from "@corsair-dev/mcp";
346
+ import { streamText, tool } from "ai";
347
+ import { z } from "zod";
348
+
349
+ // src/server/chat-store.ts
350
+ import Database from "better-sqlite3";
351
+ import { Kysely, SqliteDialect } from "kysely";
352
+
353
+ // src/server/model-pricing.ts
354
+ var MODEL_PRICES = {
355
+ "gpt-4o": { input: 2.5, output: 10 },
356
+ "gpt-4o-2024-11-20": { input: 2.5, output: 10 },
357
+ "gpt-4o-2024-08-06": { input: 2.5, output: 10 },
358
+ "gpt-4o-mini": { input: 0.15, output: 0.6 },
359
+ "gpt-4-turbo": { input: 10, output: 30 },
360
+ "gpt-4": { input: 30, output: 60 },
361
+ "gpt-3.5-turbo": { input: 0.5, output: 1.5 },
362
+ "gpt-4.1": { input: 2, output: 8 },
363
+ "gpt-4.1-mini": { input: 0.4, output: 1.6 },
364
+ "gpt-4.1-nano": { input: 0.1, output: 0.4 },
365
+ "gpt-5": { input: 1.25, output: 10 },
366
+ "gpt-5-mini": { input: 0.25, output: 2 },
367
+ "gpt-5-nano": { input: 0.05, output: 0.4 },
368
+ o1: { input: 15, output: 60 },
369
+ "o1-mini": { input: 3, output: 12 },
370
+ "o1-preview": { input: 15, output: 60 },
371
+ "o3-mini": { input: 1.1, output: 4.4 },
372
+ o3: { input: 2, output: 8 },
373
+ "o4-mini": { input: 1.1, output: 4.4 },
374
+ "claude-opus-4-5": { input: 15, output: 75 },
375
+ "claude-opus-4-7": { input: 15, output: 75 },
376
+ "claude-opus-4-1": { input: 15, output: 75 },
377
+ "claude-sonnet-4-5": { input: 3, output: 15 },
378
+ "claude-sonnet-4-6": { input: 3, output: 15 },
379
+ "claude-sonnet-4": { input: 3, output: 15 },
380
+ "claude-haiku-4-5": { input: 1, output: 5 },
381
+ "claude-3-7-sonnet-latest": { input: 3, output: 15 },
382
+ "claude-3-5-sonnet-latest": { input: 3, output: 15 },
383
+ "claude-3-5-sonnet-20241022": { input: 3, output: 15 },
384
+ "claude-3-5-haiku-latest": { input: 0.8, output: 4 },
385
+ "claude-3-5-haiku-20241022": { input: 0.8, output: 4 },
386
+ "claude-3-opus-latest": { input: 15, output: 75 },
387
+ "claude-3-haiku-20240307": { input: 0.25, output: 1.25 },
388
+ "gemini-2.5-pro": { input: 1.25, output: 10 },
389
+ "gemini-2.5-flash": { input: 0.3, output: 2.5 },
390
+ "gemini-2.5-flash-lite": { input: 0.1, output: 0.4 },
391
+ "gemini-2.0-flash": { input: 0.1, output: 0.4 },
392
+ "gemini-2.0-flash-001": { input: 0.1, output: 0.4 },
393
+ "gemini-2.0-flash-lite": { input: 0.075, output: 0.3 },
394
+ "gemini-2.0-pro-exp": { input: 1.25, output: 5 },
395
+ "gemini-1.5-pro": { input: 1.25, output: 5 },
396
+ "gemini-1.5-pro-latest": { input: 1.25, output: 5 },
397
+ "gemini-1.5-flash": { input: 0.075, output: 0.3 },
398
+ "gemini-1.5-flash-latest": { input: 0.075, output: 0.3 },
399
+ "gemini-1.5-flash-8b": { input: 0.0375, output: 0.15 },
400
+ "llama-3.3-70b-versatile": { input: 0.59, output: 0.79 },
401
+ "llama-3.1-70b-versatile": { input: 0.59, output: 0.79 },
402
+ "llama-3.1-8b-instant": { input: 0.05, output: 0.08 },
403
+ "llama-3.2-1b-preview": { input: 0.04, output: 0.04 },
404
+ "llama-3.2-3b-preview": { input: 0.06, output: 0.06 },
405
+ "llama-3.2-11b-vision-preview": { input: 0.18, output: 0.18 },
406
+ "llama-3.2-90b-vision-preview": { input: 0.9, output: 0.9 },
407
+ "mixtral-8x7b-32768": { input: 0.24, output: 0.24 },
408
+ "gemma2-9b-it": { input: 0.2, output: 0.2 },
409
+ "deepseek-r1-distill-llama-70b": { input: 0.75, output: 0.99 },
410
+ "deepseek-chat": { input: 0.27, output: 1.1 },
411
+ "deepseek-reasoner": { input: 0.55, output: 2.19 },
412
+ "mistral-large-latest": { input: 2, output: 6 },
413
+ "mistral-small-latest": { input: 0.2, output: 0.6 },
414
+ "codestral-latest": { input: 0.3, output: 0.9 },
415
+ "grok-2": { input: 2, output: 10 },
416
+ "grok-2-mini": { input: 0.3, output: 0.5 },
417
+ "grok-3": { input: 3, output: 15 },
418
+ "grok-3-mini": { input: 0.3, output: 0.5 },
419
+ "grok-4": { input: 5, output: 25 }
420
+ };
421
+ function normalizeId(id) {
422
+ return id.toLowerCase().replace(/^models\//, "").replace(/^anthropic\//, "").replace(/^openai\//, "").replace(/^google\//, "").replace(/^groq\//, "").trim();
423
+ }
424
+ function candidateIds(modelId) {
425
+ const norm = normalizeId(modelId);
426
+ const variants = /* @__PURE__ */ new Set([modelId, norm]);
427
+ const stripDate = norm.replace(/-\d{8}$/, "").replace(/-\d{4}-\d{2}-\d{2}$/, "");
428
+ if (stripDate !== norm) variants.add(stripDate);
429
+ const stripVersion = norm.replace(/-(?:00\d|0?\d{2,3})$/, "");
430
+ if (stripVersion !== norm) variants.add(stripVersion);
431
+ const stripLatest = norm.replace(/-latest$/, "");
432
+ if (stripLatest !== norm) variants.add(stripLatest);
433
+ const stripPreview = norm.replace(/-preview$/, "").replace(/-exp$/, "");
434
+ if (stripPreview !== norm) variants.add(stripPreview);
435
+ return Array.from(variants);
436
+ }
437
+ var PRICES_NORMALIZED = (() => {
438
+ const map = {};
439
+ for (const [k, v] of Object.entries(MODEL_PRICES)) {
440
+ map[normalizeId(k)] = v;
441
+ }
442
+ return map;
443
+ })();
444
+ function getModelPrice(modelId) {
445
+ if (MODEL_PRICES[modelId]) return MODEL_PRICES[modelId];
446
+ for (const candidate of candidateIds(modelId)) {
447
+ const direct = PRICES_NORMALIZED[candidate];
448
+ if (direct) return direct;
449
+ }
450
+ const norm = normalizeId(modelId);
451
+ let bestMatch = null;
452
+ for (const [key, price] of Object.entries(PRICES_NORMALIZED)) {
453
+ if (norm.startsWith(key) || key.startsWith(norm)) {
454
+ if (!bestMatch || key.length > bestMatch.key.length) {
455
+ bestMatch = { key, price };
456
+ }
457
+ }
458
+ }
459
+ return bestMatch?.price ?? null;
460
+ }
461
+ function computeCost(modelId, usage) {
462
+ const price = getModelPrice(modelId);
463
+ if (!price) {
464
+ console.warn(`[corsair:pricing] no price entry for model "${modelId}"`);
465
+ return null;
466
+ }
467
+ const input = usage.inputTokens / 1e6 * price.input;
468
+ const output = usage.outputTokens / 1e6 * price.output;
469
+ return input + output;
470
+ }
471
+
472
+ // src/server/chat-store.ts
473
+ var sqlite = new Database(":memory:");
474
+ var db = new Kysely({
475
+ dialect: new SqliteDialect({ database: sqlite })
476
+ });
477
+ var schemaReady = initSchema();
478
+ async function initSchema() {
479
+ await db.schema.createTable("chats").ifNotExists().addColumn("id", "text", (col) => col.primaryKey()).addColumn("title", "text", (col) => col.notNull()).addColumn("created_at", "integer", (col) => col.notNull()).execute();
480
+ await db.schema.createTable("chat_messages").ifNotExists().addColumn("id", "text", (col) => col.primaryKey()).addColumn("chat_id", "text", (col) => col.notNull()).addColumn("role", "text", (col) => col.notNull()).addColumn("blocks", "text", (col) => col.notNull()).addColumn("error", "text").addColumn("seq", "integer", (col) => col.notNull()).addColumn("usage", "text").execute();
481
+ }
482
+ var _seq = 0;
483
+ function getTextFromBlocks(blocks) {
484
+ return blocks.filter(
485
+ (block) => block.type === "text"
486
+ ).map((block) => block.content).join("");
487
+ }
488
+ function parseRawUsage(raw) {
489
+ if (!raw) return null;
490
+ try {
491
+ const parsed = JSON.parse(raw);
492
+ if (typeof parsed.model !== "string" || typeof parsed.inputTokens !== "number" || typeof parsed.outputTokens !== "number" || typeof parsed.totalTokens !== "number") {
493
+ return null;
494
+ }
495
+ return {
496
+ model: parsed.model,
497
+ inputTokens: parsed.inputTokens,
498
+ outputTokens: parsed.outputTokens,
499
+ totalTokens: parsed.totalTokens
500
+ };
501
+ } catch {
502
+ return null;
503
+ }
504
+ }
505
+ function withCost(raw) {
506
+ return {
507
+ ...raw,
508
+ cost: computeCost(raw.model, raw)
509
+ };
510
+ }
511
+ function aggregateUsage(rows) {
512
+ const total = {
513
+ inputTokens: 0,
514
+ outputTokens: 0,
515
+ totalTokens: 0,
516
+ cost: 0,
517
+ hasUnknownCost: false
518
+ };
519
+ for (const u of rows) {
520
+ if (!u) continue;
521
+ total.inputTokens += u.inputTokens;
522
+ total.outputTokens += u.outputTokens;
523
+ total.totalTokens += u.totalTokens;
524
+ if (u.cost === null) {
525
+ total.hasUnknownCost = true;
526
+ } else {
527
+ total.cost += u.cost;
528
+ }
529
+ }
530
+ return total;
531
+ }
532
+ async function createChat() {
533
+ await schemaReady;
534
+ const id = crypto.randomUUID();
535
+ const created_at = Date.now();
536
+ const title = "New chat";
537
+ await db.insertInto("chats").values({ id, title, created_at }).execute();
538
+ return {
539
+ id,
540
+ title,
541
+ created_at,
542
+ usage_total: aggregateUsage([])
543
+ };
544
+ }
545
+ async function listChats() {
546
+ await schemaReady;
547
+ const chats = await db.selectFrom("chats").select(["id", "title", "created_at"]).orderBy("created_at", "desc").execute();
548
+ const messageRows = await db.selectFrom("chat_messages").select(["chat_id", "usage"]).execute();
549
+ const usageByChat = /* @__PURE__ */ new Map();
550
+ for (const row of messageRows) {
551
+ const raw = parseRawUsage(row.usage);
552
+ if (!raw) continue;
553
+ const list = usageByChat.get(row.chat_id) ?? [];
554
+ list.push(withCost(raw));
555
+ usageByChat.set(row.chat_id, list);
556
+ }
557
+ return chats.map((chat) => ({
558
+ id: chat.id,
559
+ title: chat.title,
560
+ created_at: chat.created_at,
561
+ usage_total: aggregateUsage(usageByChat.get(chat.id) ?? [])
562
+ }));
563
+ }
564
+ async function chatExists(chatId) {
565
+ await schemaReady;
566
+ const row = await db.selectFrom("chats").select("id").where("id", "=", chatId).executeTakeFirst();
567
+ return !!row;
568
+ }
569
+ async function getMessages(chatId) {
570
+ await schemaReady;
571
+ const rows = await db.selectFrom("chat_messages").select(["id", "chat_id", "role", "blocks", "error", "usage"]).where("chat_id", "=", chatId).orderBy("seq", "asc").execute();
572
+ return rows.map((row) => {
573
+ const raw = parseRawUsage(row.usage);
574
+ return {
575
+ id: row.id,
576
+ chat_id: row.chat_id,
577
+ role: row.role,
578
+ blocks: JSON.parse(row.blocks),
579
+ error: row.error,
580
+ usage: raw ? withCost(raw) : null
581
+ };
582
+ });
583
+ }
584
+ async function appendMessage(chatId, id, role, blocks, options = {}) {
585
+ await schemaReady;
586
+ await db.insertInto("chat_messages").values({
587
+ id,
588
+ chat_id: chatId,
589
+ role,
590
+ blocks: JSON.stringify(blocks),
591
+ error: options.error ?? null,
592
+ seq: ++_seq,
593
+ usage: options.usage ? JSON.stringify(options.usage) : null
594
+ }).execute();
595
+ if (role === "user") {
596
+ const row = await db.selectFrom("chats").select("title").where("id", "=", chatId).executeTakeFirst();
597
+ if (row?.title === "New chat") {
598
+ const text = getTextFromBlocks(blocks);
599
+ const newTitle = text.slice(0, 60).trim();
600
+ if (newTitle) {
601
+ await db.updateTable("chats").set({ title: newTitle }).where("id", "=", chatId).execute();
602
+ }
603
+ }
604
+ }
605
+ }
606
+
607
+ // src/server/handlers/chat.ts
608
+ function isTextBlock(block) {
609
+ return block.type === "text";
610
+ }
611
+ function isCompletedToolBlock(block) {
612
+ return block.type === "tool" && block.result !== void 0;
613
+ }
614
+ function isPendingToolBlock(block) {
615
+ return block.type === "tool" && block.result === void 0;
616
+ }
617
+ function joinTextBlocks(blocks) {
618
+ return blocks.map((block) => block.content).join("");
619
+ }
620
+ function toToolArgs(args) {
621
+ if (args && typeof args === "object") {
622
+ const toolArgs = {};
623
+ for (const [key, value] of Object.entries(args)) {
624
+ toolArgs[key] = value;
625
+ }
626
+ return toolArgs;
627
+ }
628
+ return {};
629
+ }
630
+ function makeTextPart(block) {
631
+ return { type: "text", text: block.content };
632
+ }
633
+ function makeToolCallPart(toolCallId, block) {
634
+ return {
635
+ type: "tool-call",
636
+ toolCallId,
637
+ toolName: block.name,
638
+ args: toToolArgs(block.args)
639
+ };
640
+ }
641
+ function makeToolResultPart(toolCallId, block) {
642
+ return {
643
+ type: "tool-result",
644
+ toolCallId,
645
+ toolName: block.name,
646
+ result: block.result
647
+ };
648
+ }
649
+ function findPendingToolBlockIndex(blocks, toolName) {
650
+ return blocks.findLastIndex(
651
+ (block) => isPendingToolBlock(block) && block.name === toolName
652
+ );
653
+ }
654
+ function toAiMessages(stored) {
655
+ const result = [];
656
+ for (const msg of stored) {
657
+ const textBlocks = msg.blocks.filter(isTextBlock);
658
+ const text = joinTextBlocks(textBlocks);
659
+ if (msg.role === "user") {
660
+ result.push({ role: "user", content: text });
661
+ continue;
662
+ }
663
+ const toolBlocks = msg.blocks.filter(isCompletedToolBlock);
664
+ if (toolBlocks.length === 0) {
665
+ result.push({ role: "assistant", content: text });
666
+ continue;
667
+ }
668
+ const toolCallIds = toolBlocks.map(() => crypto.randomUUID());
669
+ const assistantParts = textBlocks.map(makeTextPart);
670
+ const toolParts = [];
671
+ for (const [index, block] of toolBlocks.entries()) {
672
+ const toolCallId = toolCallIds[index];
673
+ if (!toolCallId) {
674
+ continue;
675
+ }
676
+ assistantParts.push(makeToolCallPart(toolCallId, block));
677
+ toolParts.push(makeToolResultPart(toolCallId, block));
678
+ }
679
+ result.push({ role: "assistant", content: assistantParts });
680
+ result.push({ role: "tool", content: toolParts });
681
+ }
682
+ return result;
683
+ }
684
+ function buildAiTools(corsairClient) {
685
+ const defs = buildCorsairToolDefs({ corsair: corsairClient, setup: false });
686
+ const tools = {};
687
+ for (const def of defs) {
688
+ tools[def.name] = tool({
689
+ description: def.description,
690
+ parameters: z.object(def.shape),
691
+ execute: async (args) => {
692
+ const result = await def.handler(args);
693
+ const texts = result.content.filter((c) => c.type === "text");
694
+ if (result.isError) {
695
+ throw new Error(texts.map((c) => c.text).join("\n"));
696
+ }
697
+ const text = texts[0]?.text ?? "";
698
+ try {
699
+ return JSON.parse(text);
700
+ } catch {
701
+ return text;
702
+ }
703
+ }
704
+ });
705
+ }
706
+ return tools;
707
+ }
708
+ async function resolveModel() {
709
+ const override = process.env.CORSAIR_CHAT_MODEL;
710
+ console.log(
711
+ "[corsair:chat] resolving model \u2014 OPENAI_API_KEY:",
712
+ !!process.env.OPENAI_API_KEY,
713
+ "| ANTHROPIC_API_KEY:",
714
+ !!process.env.ANTHROPIC_API_KEY,
715
+ "| CORSAIR_CHAT_MODEL:",
716
+ override ?? "(default)"
717
+ );
718
+ if (process.env.OPENAI_API_KEY) {
719
+ const { openai } = await import("@ai-sdk/openai");
720
+ const modelId = override ?? "gpt-4o-mini";
721
+ console.log("[corsair:chat] using openai:", modelId);
722
+ return { model: openai(modelId), modelId };
723
+ }
724
+ if (process.env.ANTHROPIC_API_KEY) {
725
+ const { anthropic } = await import("@ai-sdk/anthropic");
726
+ const modelId = override ?? "claude-sonnet-4-6";
727
+ console.log("[corsair:chat] using anthropic:", modelId);
728
+ return { model: anthropic(modelId), modelId };
729
+ }
730
+ if (process.env.GOOGLE_GENERATIVE_AI_API_KEY) {
731
+ const { google } = await import("@ai-sdk/google");
732
+ const modelId = override ?? "gemini-2.0-flash";
733
+ return { model: google(modelId), modelId };
734
+ }
735
+ if (process.env.GROQ_API_KEY) {
736
+ const { createGroq } = await import("@ai-sdk/groq");
737
+ const modelId = override ?? "llama-3.3-70b-versatile";
738
+ return { model: createGroq()(modelId), modelId };
739
+ }
740
+ throw new Error(
741
+ "No AI provider configured. Set one of: OPENAI_API_KEY, ANTHROPIC_API_KEY, GOOGLE_GENERATIVE_AI_API_KEY, or GROQ_API_KEY."
742
+ );
743
+ }
744
+ function errorMessage(err) {
745
+ return err instanceof Error ? err.message : String(err);
746
+ }
747
+ function isObject(value) {
748
+ return typeof value === "object" && value !== null;
749
+ }
750
+ function parseRawUsage2(raw) {
751
+ if (!isObject(raw)) return void 0;
752
+ const usage = {};
753
+ if (typeof raw.promptTokens === "number")
754
+ usage.promptTokens = raw.promptTokens;
755
+ if (typeof raw.completionTokens === "number")
756
+ usage.completionTokens = raw.completionTokens;
757
+ if (typeof raw.totalTokens === "number") usage.totalTokens = raw.totalTokens;
758
+ return usage;
759
+ }
760
+ function parseStreamPart(raw) {
761
+ if (!isObject(raw) || typeof raw.type !== "string") {
762
+ return null;
763
+ }
764
+ const textDelta = typeof raw.textDelta === "string" ? raw.textDelta : void 0;
765
+ const toolName = typeof raw.toolName === "string" ? raw.toolName : void 0;
766
+ let responseModelId;
767
+ if (isObject(raw.response) && typeof raw.response.modelId === "string") {
768
+ responseModelId = raw.response.modelId;
769
+ }
770
+ return {
771
+ type: raw.type,
772
+ textDelta,
773
+ toolName,
774
+ args: raw.args,
775
+ result: raw.result,
776
+ usage: parseRawUsage2(raw.usage),
777
+ responseModelId
778
+ };
779
+ }
780
+ function buildRawMessageUsage(modelId, raw) {
781
+ if (!raw) return null;
782
+ const inputTokens = raw.promptTokens ?? 0;
783
+ const outputTokens = raw.completionTokens ?? 0;
784
+ const totalTokens = raw.totalTokens ?? inputTokens + outputTokens;
785
+ if (inputTokens === 0 && outputTokens === 0 && totalTokens === 0) return null;
786
+ return { model: modelId, inputTokens, outputTokens, totalTokens };
787
+ }
788
+ var chatHandler = async (ctx) => {
789
+ console.log("[corsair:chat] request received");
790
+ const body = await readJsonBody(ctx.req);
791
+ const tenant = body.tenant ? String(body.tenant) : void 0;
792
+ const chatId = body.chatId ? String(body.chatId) : void 0;
793
+ const newUserText = body.message ? String(body.message) : void 0;
794
+ const storedHistory = chatId && await chatExists(chatId) ? await getMessages(chatId) : [];
795
+ const messages = toAiMessages(storedHistory);
796
+ if (newUserText) {
797
+ messages.push({ role: "user", content: newUserText });
798
+ }
799
+ console.log(
800
+ "[corsair:chat] history:",
801
+ storedHistory.length,
802
+ "stored msgs \u2192",
803
+ messages.length,
804
+ "AI msgs | tenant:",
805
+ tenant ?? "(none)",
806
+ "| chatId:",
807
+ chatId ?? "(none)"
808
+ );
809
+ let resolved;
810
+ try {
811
+ resolved = await resolveModel();
812
+ } catch (err) {
813
+ const message = errorMessage(err);
814
+ console.error("[corsair:chat] model resolution failed:", message);
815
+ ctx.res.writeHead(400, { "content-type": "application/json" });
816
+ ctx.res.end(JSON.stringify({ error: message }));
817
+ return;
818
+ }
819
+ const handle = await ctx.getCorsair();
820
+ const client = handle.resolveClient(tenant);
821
+ const tools = buildAiTools(client);
822
+ const userMsgId = crypto.randomUUID();
823
+ if (chatId && await chatExists(chatId) && newUserText) {
824
+ try {
825
+ await appendMessage(chatId, userMsgId, "user", [
826
+ { type: "text", content: newUserText }
827
+ ]);
828
+ } catch (err) {
829
+ console.error("[corsair:chat] failed to save user message:", err);
830
+ }
831
+ }
832
+ ctx.res.writeHead(200, {
833
+ "content-type": "text/event-stream",
834
+ "cache-control": "no-cache",
835
+ connection: "keep-alive"
836
+ });
837
+ const send = (data) => {
838
+ ctx.res.write(`data: ${JSON.stringify(data)}
839
+
840
+ `);
841
+ };
842
+ const assistantBlocks = [];
843
+ let finalRawUsage = null;
844
+ console.log("[corsair:chat] starting stream");
845
+ try {
846
+ const result = streamText({
847
+ model: resolved.model,
848
+ system: "You are a helpful assistant with access to Corsair tools. Use the tools to answer questions about the user's integrations and data. Use list_operations to get the exact endpoint names.",
849
+ messages,
850
+ tools,
851
+ maxSteps: 10
852
+ });
853
+ for await (const raw of result.fullStream) {
854
+ const part = parseStreamPart(raw);
855
+ if (!part) {
856
+ continue;
857
+ }
858
+ if (part.type === "text-delta") {
859
+ send({ type: "text", text: part.textDelta });
860
+ const last = assistantBlocks[assistantBlocks.length - 1];
861
+ if (last?.type === "text") {
862
+ last.content += part.textDelta ?? "";
863
+ } else {
864
+ assistantBlocks.push({ type: "text", content: part.textDelta ?? "" });
865
+ }
866
+ } else if (part.type === "tool-call") {
867
+ send({ type: "tool-start", name: part.toolName, args: part.args });
868
+ assistantBlocks.push({
869
+ type: "tool",
870
+ name: part.toolName ?? "",
871
+ args: part.args
872
+ });
873
+ } else if (part.type === "tool-result") {
874
+ send({ type: "tool-end", name: part.toolName, result: part.result });
875
+ const idx = findPendingToolBlockIndex(assistantBlocks, part.toolName);
876
+ const block = idx >= 0 ? assistantBlocks[idx] : void 0;
877
+ if (block && isPendingToolBlock(block)) {
878
+ block.result = part.result;
879
+ }
880
+ } else if (part.type === "finish") {
881
+ const effectiveModelId = part.responseModelId ?? resolved.modelId;
882
+ const raw2 = buildRawMessageUsage(effectiveModelId, part.usage);
883
+ console.log(
884
+ "[corsair:chat] finish \u2014 model:",
885
+ effectiveModelId,
886
+ "| usage:",
887
+ part.usage
888
+ );
889
+ if (raw2) {
890
+ finalRawUsage = raw2;
891
+ send({ type: "usage", usage: withCost(raw2) });
892
+ }
893
+ send({ type: "done" });
894
+ }
895
+ }
896
+ } catch (err) {
897
+ const message = errorMessage(err);
898
+ console.error("[corsair:chat] stream error:", message);
899
+ send({ type: "error", message });
900
+ } finally {
901
+ if (chatId && await chatExists(chatId) && assistantBlocks.length > 0) {
902
+ try {
903
+ await appendMessage(
904
+ chatId,
905
+ crypto.randomUUID(),
906
+ "assistant",
907
+ assistantBlocks,
908
+ { usage: finalRawUsage ?? void 0 }
909
+ );
910
+ } catch (err) {
911
+ console.error("[corsair:chat] failed to save assistant message:", err);
912
+ }
913
+ }
914
+ console.log("[corsair:chat] done");
915
+ ctx.res.end();
916
+ }
917
+ };
918
+
919
+ // src/server/handlers/chats.ts
920
+ var listChatsHandler = async () => {
921
+ return { chats: await listChats() };
922
+ };
923
+ var createChatHandler = async () => {
924
+ return { chat: await createChat() };
925
+ };
926
+ var getChatMessagesHandler = async (ctx) => {
927
+ const chatId = ctx.url.searchParams.get("chatId");
928
+ if (!chatId) throw new Error("chatId is required");
929
+ return { messages: await getMessages(chatId) };
930
+ };
931
+
344
932
  // src/server/handlers/credentials-internal.ts
345
933
  var BASE_FIELDS = {
346
934
  oauth_2: {
@@ -533,9 +1121,9 @@ function getKyselyDb(database) {
533
1121
  var listDbTables = async (ctx) => {
534
1122
  const { internal } = await ctx.getCorsair();
535
1123
  if (!internal.database) throw new Error("No database configured.");
536
- const db = getKyselyDb(internal.database);
537
- if (!db) throw new Error("Could not access kysely db handle.");
538
- const existing = await db.introspection.getTables();
1124
+ const db2 = getKyselyDb(internal.database);
1125
+ if (!db2) throw new Error("Could not access kysely db handle.");
1126
+ const existing = await db2.introspection.getTables();
539
1127
  const existingNames = new Set(existing.map((t) => t.name));
540
1128
  return {
541
1129
  core: CORE_TABLES.filter((t) => existingNames.has(t)),
@@ -551,14 +1139,14 @@ var listDbRows = async (ctx) => {
551
1139
  if (!table) throw new Error("Missing table.");
552
1140
  const { internal } = await ctx.getCorsair();
553
1141
  if (!internal.database) throw new Error("No database configured.");
554
- const db = getKyselyDb(internal.database);
555
- if (!db) throw new Error("Could not access kysely db handle.");
1142
+ const db2 = getKyselyDb(internal.database);
1143
+ if (!db2) throw new Error("Could not access kysely db handle.");
556
1144
  let rows;
557
1145
  try {
558
- const q = db.selectFrom(table).selectAll().limit(limit).offset(offset);
1146
+ const q = db2.selectFrom(table).selectAll().limit(limit).offset(offset);
559
1147
  rows = await q.orderBy("created_at", "desc").execute();
560
1148
  } catch {
561
- rows = await db.selectFrom(table).selectAll().limit(limit).offset(offset).execute();
1149
+ rows = await db2.selectFrom(table).selectAll().limit(limit).offset(offset).execute();
562
1150
  }
563
1151
  const safeRows = rows.map(redactSensitive);
564
1152
  return { rows: safeRows, limit, offset };
@@ -580,9 +1168,9 @@ var queryEntityData = async (ctx) => {
580
1168
  if (!entity) throw new Error("Missing entity.");
581
1169
  const { internal } = await ctx.getCorsair();
582
1170
  if (!internal.database) throw new Error("No database configured.");
583
- const db = getKyselyDb(internal.database);
584
- if (!db) throw new Error("Could not access kysely db handle.");
585
- const base = db.selectFrom("corsair_entities as e").innerJoin("corsair_accounts as a", "a.id", "e.account_id").innerJoin("corsair_integrations as i", "i.id", "a.integration_id").where("a.tenant_id", "=", tenant).where("i.name", "=", integration).where("e.entity_type", "=", entity);
1171
+ const db2 = getKyselyDb(internal.database);
1172
+ if (!db2) throw new Error("Could not access kysely db handle.");
1173
+ const base = db2.selectFrom("corsair_entities as e").innerJoin("corsair_accounts as a", "a.id", "e.account_id").innerJoin("corsair_integrations as i", "i.id", "a.integration_id").where("a.tenant_id", "=", tenant).where("i.name", "=", integration).where("e.entity_type", "=", entity);
586
1174
  let rowsRaw = [];
587
1175
  let hasMore = false;
588
1176
  let total = 0;
@@ -682,14 +1270,14 @@ function collectPrimitiveValues(value) {
682
1270
  var listPermissions = async (ctx) => {
683
1271
  const { internal } = await ctx.getCorsair();
684
1272
  if (!internal.database) throw new Error("No database configured.");
685
- const db = getKyselyDb(internal.database);
686
- if (!db) throw new Error("Could not access kysely db handle.");
1273
+ const db2 = getKyselyDb(internal.database);
1274
+ if (!db2) throw new Error("Could not access kysely db handle.");
687
1275
  const limit = Math.min(
688
1276
  Math.max(Number(ctx.url.searchParams.get("limit") ?? 100), 1),
689
1277
  500
690
1278
  );
691
1279
  try {
692
- const rows = await db.selectFrom("corsair_permissions").selectAll().limit(limit).offset(0).orderBy("created_at", "desc").execute();
1280
+ const rows = await db2.selectFrom("corsair_permissions").selectAll().limit(limit).offset(0).orderBy("created_at", "desc").execute();
693
1281
  return { rows };
694
1282
  } catch (err) {
695
1283
  return { rows: [], note: err.message };
@@ -809,19 +1397,19 @@ var setupPlugin = async (ctx) => {
809
1397
  if (!hasAuthConfig(internal, pluginId)) {
810
1398
  throw new Error(`Plugin '${pluginId}' has no credential setup.`);
811
1399
  }
812
- const db = asDb(internal.database);
1400
+ const db2 = asDb(internal.database);
813
1401
  const now = /* @__PURE__ */ new Date();
814
- let integration = await db.selectFrom("corsair_integrations").selectAll().where("name", "=", pluginId).executeTakeFirst();
1402
+ let integration = await db2.selectFrom("corsair_integrations").selectAll().where("name", "=", pluginId).executeTakeFirst();
815
1403
  if (!integration) {
816
1404
  const id = randomUUID();
817
- await db.insertInto("corsair_integrations").values({
1405
+ await db2.insertInto("corsair_integrations").values({
818
1406
  id,
819
1407
  name: pluginId,
820
1408
  config: {},
821
1409
  created_at: now,
822
1410
  updated_at: now
823
1411
  }).execute();
824
- integration = await db.selectFrom("corsair_integrations").selectAll().where("id", "=", id).executeTakeFirst();
1412
+ integration = await db2.selectFrom("corsair_integrations").selectAll().where("id", "=", id).executeTakeFirst();
825
1413
  }
826
1414
  if (!integration) {
827
1415
  throw new Error(`Failed to create integration for '${pluginId}'.`);
@@ -830,9 +1418,9 @@ var setupPlugin = async (ctx) => {
830
1418
  if (typeof integrationId !== "string") {
831
1419
  throw new Error(`Invalid integration row for '${pluginId}'.`);
832
1420
  }
833
- let account = await db.selectFrom("corsair_accounts").selectAll().where("tenant_id", "=", tenantId).where("integration_id", "=", integrationId).executeTakeFirst();
1421
+ let account = await db2.selectFrom("corsair_accounts").selectAll().where("tenant_id", "=", tenantId).where("integration_id", "=", integrationId).executeTakeFirst();
834
1422
  if (!account) {
835
- await db.insertInto("corsair_accounts").values({
1423
+ await db2.insertInto("corsair_accounts").values({
836
1424
  id: randomUUID(),
837
1425
  tenant_id: tenantId,
838
1426
  integration_id: integrationId,
@@ -840,7 +1428,7 @@ var setupPlugin = async (ctx) => {
840
1428
  created_at: now,
841
1429
  updated_at: now
842
1430
  }).execute();
843
- account = await db.selectFrom("corsair_accounts").selectAll().where("tenant_id", "=", tenantId).where("integration_id", "=", integrationId).executeTakeFirst();
1431
+ account = await db2.selectFrom("corsair_accounts").selectAll().where("tenant_id", "=", tenantId).where("integration_id", "=", integrationId).executeTakeFirst();
844
1432
  }
845
1433
  const rootKeys = instance.keys ?? null;
846
1434
  const integrationNamespace = rootKeys?.[pluginId] ?? null;
@@ -948,8 +1536,8 @@ var listTenants = async (ctx) => {
948
1536
  return { tenants: ["default"] };
949
1537
  }
950
1538
  if (!internal.database) throw new Error("No database configured.");
951
- const db = asDb2(internal.database);
952
- const rows = await db.selectFrom("corsair_accounts").selectAll().execute();
1539
+ const db2 = asDb2(internal.database);
1540
+ const rows = await db2.selectFrom("corsair_accounts").selectAll().execute();
953
1541
  const ids = /* @__PURE__ */ new Set(["default"]);
954
1542
  for (const row of rows) {
955
1543
  const tenantId = row.tenant_id;
@@ -973,13 +1561,13 @@ var createTenant = async (ctx) => {
973
1561
  throw new Error("Multi-tenancy is not enabled for this Corsair instance.");
974
1562
  }
975
1563
  if (!internal.database) throw new Error("No database configured.");
976
- const db = asDb2(internal.database);
1564
+ const db2 = asDb2(internal.database);
977
1565
  const now = /* @__PURE__ */ new Date();
978
1566
  const authPluginIds = getAuthPluginIds(internal);
979
1567
  if (authPluginIds.length === 0) {
980
1568
  return { ok: true, created: false };
981
1569
  }
982
- const integrations = await db.selectFrom("corsair_integrations").selectAll().execute();
1570
+ const integrations = await db2.selectFrom("corsair_integrations").selectAll().execute();
983
1571
  const integrationByName = /* @__PURE__ */ new Map();
984
1572
  for (const row of integrations) {
985
1573
  const name = row.name;
@@ -995,10 +1583,10 @@ var createTenant = async (ctx) => {
995
1583
  created_at: now,
996
1584
  updated_at: now
997
1585
  };
998
- await db.insertInto("corsair_integrations").values(row).execute();
1586
+ await db2.insertInto("corsair_integrations").values(row).execute();
999
1587
  integrationByName.set(pluginId, row);
1000
1588
  }
1001
- const existingAccounts = await db.selectFrom("corsair_accounts").selectAll().where("tenant_id", "=", tenantId).execute();
1589
+ const existingAccounts = await db2.selectFrom("corsair_accounts").selectAll().where("tenant_id", "=", tenantId).execute();
1002
1590
  const accountByIntegrationId = /* @__PURE__ */ new Map();
1003
1591
  for (const row of existingAccounts) {
1004
1592
  const integrationId = row.integration_id;
@@ -1016,7 +1604,7 @@ var createTenant = async (ctx) => {
1016
1604
  if (!existing) {
1017
1605
  createdAny = true;
1018
1606
  pluginsMissingDek.add(pluginId);
1019
- await db.insertInto("corsair_accounts").values({
1607
+ await db2.insertInto("corsair_accounts").values({
1020
1608
  id: randomUUID2(),
1021
1609
  tenant_id: tenantId,
1022
1610
  integration_id: integrationId,
@@ -1066,7 +1654,15 @@ var routes = [
1066
1654
  { method: "GET", path: "/api/db/tables", handler: listDbTables },
1067
1655
  { method: "POST", path: "/api/db/rows", handler: listDbRows },
1068
1656
  { method: "POST", path: "/api/db/entities/query", handler: queryEntityData },
1069
- { method: "GET", path: "/api/db/permissions", handler: listPermissions }
1657
+ { method: "GET", path: "/api/db/permissions", handler: listPermissions },
1658
+ { method: "GET", path: "/api/chats", handler: listChatsHandler },
1659
+ { method: "POST", path: "/api/chats", handler: createChatHandler },
1660
+ {
1661
+ method: "GET",
1662
+ path: "/api/chats/messages",
1663
+ handler: getChatMessagesHandler
1664
+ },
1665
+ { method: "POST", path: "/api/chat", handler: chatHandler }
1070
1666
  ];
1071
1667
  async function handleApi(req, res, baseCtx) {
1072
1668
  const url = new URL(req.url ?? "/", "http://localhost");