@trops/dash-core 0.1.48 → 0.1.50

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.
@@ -22,7 +22,8 @@ var require$$2$4 = require('algoliasearch');
22
22
  var require$$3$2 = require('node:path');
23
23
  var require$$0$3 = require('openai');
24
24
  require('live-plugin-manager');
25
- var require$$0$4 = require('@anthropic-ai/sdk');
25
+ var require$$0$5 = require('@anthropic-ai/sdk');
26
+ var require$$0$4 = require('child_process');
26
27
  var require$$2$6 = require('os');
27
28
  var require$$3$3 = require('adm-zip');
28
29
  var require$$4$1 = require('url');
@@ -525,23 +526,31 @@ var openaiEvents$1 = {
525
526
  const LLM_SEND_MESSAGE$1 = "llm-send-message";
526
527
  const LLM_ABORT_REQUEST$1 = "llm-abort-request";
527
528
  const LLM_LIST_CONNECTED_TOOLS$1 = "llm-list-connected-tools";
529
+ const LLM_CHECK_CLI_AVAILABLE$1 = "llm-check-cli-available";
530
+ const LLM_CLEAR_CLI_SESSION$1 = "llm-clear-cli-session";
531
+ const LLM_CLI_SESSION_STATUS$1 = "llm-cli-session-status";
532
+ const LLM_CLI_END_SESSION$1 = "llm-cli-end-session";
528
533
 
529
534
  // --- Main → Renderer (send) ---
530
- const LLM_STREAM_DELTA$2 = "llm-stream-delta";
531
- const LLM_STREAM_TOOL_CALL$2 = "llm-stream-tool-call";
532
- const LLM_STREAM_TOOL_RESULT$2 = "llm-stream-tool-result";
533
- const LLM_STREAM_COMPLETE$2 = "llm-stream-complete";
534
- const LLM_STREAM_ERROR$2 = "llm-stream-error";
535
+ const LLM_STREAM_DELTA$3 = "llm-stream-delta";
536
+ const LLM_STREAM_TOOL_CALL$3 = "llm-stream-tool-call";
537
+ const LLM_STREAM_TOOL_RESULT$3 = "llm-stream-tool-result";
538
+ const LLM_STREAM_COMPLETE$3 = "llm-stream-complete";
539
+ const LLM_STREAM_ERROR$3 = "llm-stream-error";
535
540
 
536
541
  var llmEvents$1 = {
537
542
  LLM_SEND_MESSAGE: LLM_SEND_MESSAGE$1,
538
543
  LLM_ABORT_REQUEST: LLM_ABORT_REQUEST$1,
539
544
  LLM_LIST_CONNECTED_TOOLS: LLM_LIST_CONNECTED_TOOLS$1,
540
- LLM_STREAM_DELTA: LLM_STREAM_DELTA$2,
541
- LLM_STREAM_TOOL_CALL: LLM_STREAM_TOOL_CALL$2,
542
- LLM_STREAM_TOOL_RESULT: LLM_STREAM_TOOL_RESULT$2,
543
- LLM_STREAM_COMPLETE: LLM_STREAM_COMPLETE$2,
544
- LLM_STREAM_ERROR: LLM_STREAM_ERROR$2,
545
+ LLM_CHECK_CLI_AVAILABLE: LLM_CHECK_CLI_AVAILABLE$1,
546
+ LLM_CLEAR_CLI_SESSION: LLM_CLEAR_CLI_SESSION$1,
547
+ LLM_STREAM_DELTA: LLM_STREAM_DELTA$3,
548
+ LLM_STREAM_TOOL_CALL: LLM_STREAM_TOOL_CALL$3,
549
+ LLM_STREAM_TOOL_RESULT: LLM_STREAM_TOOL_RESULT$3,
550
+ LLM_STREAM_COMPLETE: LLM_STREAM_COMPLETE$3,
551
+ LLM_STREAM_ERROR: LLM_STREAM_ERROR$3,
552
+ LLM_CLI_SESSION_STATUS: LLM_CLI_SESSION_STATUS$1,
553
+ LLM_CLI_END_SESSION: LLM_CLI_END_SESSION$1,
545
554
  };
546
555
 
547
556
  /**
@@ -4809,28 +4818,28 @@ const fs$5 = require$$2;
4809
4818
  /**
4810
4819
  * Cached shell PATH result (resolved once, reused for all spawns).
4811
4820
  */
4812
- let _shellPath = null;
4821
+ let _shellPath$1 = null;
4813
4822
 
4814
4823
  /**
4815
4824
  * Get the user's full shell PATH (including nvm, homebrew, volta, etc.).
4816
4825
  * Electron GUI apps on macOS don't inherit the shell PATH, so we
4817
4826
  * resolve it once by invoking a login shell.
4818
4827
  */
4819
- function getShellPath() {
4820
- if (_shellPath !== null) return _shellPath;
4828
+ function getShellPath$1() {
4829
+ if (_shellPath$1 !== null) return _shellPath$1;
4821
4830
 
4822
4831
  try {
4823
4832
  const { execSync } = require("child_process");
4824
4833
  const shell = process.env.SHELL || "/bin/bash";
4825
- _shellPath = execSync(`${shell} -ilc 'echo -n "$PATH"'`, {
4834
+ _shellPath$1 = execSync(`${shell} -ilc 'echo -n "$PATH"'`, {
4826
4835
  encoding: "utf8",
4827
4836
  timeout: 5000,
4828
4837
  });
4829
4838
  } catch {
4830
- _shellPath = process.env.PATH || "";
4839
+ _shellPath$1 = process.env.PATH || "";
4831
4840
  }
4832
4841
 
4833
- return _shellPath;
4842
+ return _shellPath$1;
4834
4843
  }
4835
4844
 
4836
4845
  /**
@@ -4948,7 +4957,7 @@ const mcpController$2 = {
4948
4957
  const env = { ...process.env };
4949
4958
  // Ensure full shell PATH is available (Electron GUI apps
4950
4959
  // on macOS don't inherit nvm/homebrew paths)
4951
- env.PATH = getShellPath();
4960
+ env.PATH = getShellPath$1();
4952
4961
  if (mcpConfig.envMapping && credentials) {
4953
4962
  Object.entries(mcpConfig.envMapping).forEach(
4954
4963
  ([envVar, credentialKey]) => {
@@ -6317,6 +6326,438 @@ const pluginController$1 = {
6317
6326
 
6318
6327
  var pluginController_1 = pluginController$1;
6319
6328
 
6329
+ /**
6330
+ * cliController.js
6331
+ *
6332
+ * Manages Claude Code CLI (`claude -p`) as an alternative LLM backend.
6333
+ * Spawns the CLI subprocess, parses stream-json NDJSON output, and emits
6334
+ * the same LLM_STREAM_* events as the Anthropic SDK path.
6335
+ *
6336
+ * Users with a Claude Pro/Max subscription and Claude Code installed
6337
+ * can use the Chat widget without a separate API key.
6338
+ */
6339
+
6340
+ const { spawn, execSync } = require$$0$4;
6341
+ const {
6342
+ LLM_STREAM_DELTA: LLM_STREAM_DELTA$2,
6343
+ LLM_STREAM_TOOL_CALL: LLM_STREAM_TOOL_CALL$2,
6344
+ LLM_STREAM_TOOL_RESULT: LLM_STREAM_TOOL_RESULT$2,
6345
+ LLM_STREAM_COMPLETE: LLM_STREAM_COMPLETE$2,
6346
+ LLM_STREAM_ERROR: LLM_STREAM_ERROR$2,
6347
+ } = llmEvents$1;
6348
+
6349
+ /**
6350
+ * Cached shell PATH result (resolved once, reused for all spawns).
6351
+ * Same pattern as mcpController.js.
6352
+ */
6353
+ let _shellPath = null;
6354
+
6355
+ function getShellPath() {
6356
+ if (_shellPath !== null) return _shellPath;
6357
+
6358
+ try {
6359
+ const shell = process.env.SHELL || "/bin/bash";
6360
+ _shellPath = execSync(`${shell} -ilc 'echo -n "$PATH"'`, {
6361
+ encoding: "utf8",
6362
+ timeout: 5000,
6363
+ });
6364
+ } catch {
6365
+ _shellPath = process.env.PATH || "";
6366
+ }
6367
+
6368
+ return _shellPath;
6369
+ }
6370
+
6371
+ /**
6372
+ * Cached CLI binary path (resolved once via `which claude`).
6373
+ */
6374
+ let _cliBinaryPath = undefined; // undefined = not yet checked
6375
+
6376
+ function resolveCliBinary() {
6377
+ if (_cliBinaryPath !== undefined) return _cliBinaryPath;
6378
+
6379
+ try {
6380
+ const fullPath = getShellPath();
6381
+ _cliBinaryPath = execSync("which claude", {
6382
+ encoding: "utf8",
6383
+ timeout: 5000,
6384
+ env: { ...process.env, PATH: fullPath },
6385
+ }).trim();
6386
+ } catch {
6387
+ _cliBinaryPath = null;
6388
+ }
6389
+
6390
+ return _cliBinaryPath;
6391
+ }
6392
+
6393
+ /**
6394
+ * Active CLI processes for abort support.
6395
+ * Map<requestId, ChildProcess>
6396
+ */
6397
+ const activeProcesses = new Map();
6398
+
6399
+ /**
6400
+ * Session IDs for conversation continuity.
6401
+ * Map<widgetUuid, sessionId>
6402
+ */
6403
+ const sessions = new Map();
6404
+
6405
+ /**
6406
+ * Send events safely to a window.
6407
+ */
6408
+ function safeSend(win, channel, data) {
6409
+ if (win && !win.isDestroyed()) {
6410
+ win.webContents.send(channel, data);
6411
+ }
6412
+ }
6413
+
6414
+ const cliController$2 = {
6415
+ /**
6416
+ * isAvailable
6417
+ * Check if the Claude Code CLI is installed and accessible.
6418
+ *
6419
+ * @returns {{ available: boolean, path?: string }}
6420
+ */
6421
+ isAvailable: () => {
6422
+ const binaryPath = resolveCliBinary();
6423
+ if (binaryPath) {
6424
+ return { available: true, path: binaryPath };
6425
+ }
6426
+ return { available: false };
6427
+ },
6428
+
6429
+ /**
6430
+ * sendMessage
6431
+ * Stream a response from the Claude Code CLI with NDJSON parsing.
6432
+ *
6433
+ * @param {BrowserWindow} win - the window to send stream events to
6434
+ * @param {string} requestId - unique ID for this request
6435
+ * @param {object} params - { model, messages, systemPrompt, maxToolRounds, widgetUuid }
6436
+ */
6437
+ sendMessage: async (win, requestId, params) => {
6438
+ const { model, messages, systemPrompt, widgetUuid } = params;
6439
+
6440
+ const binaryPath = resolveCliBinary();
6441
+ if (!binaryPath) {
6442
+ safeSend(win, LLM_STREAM_ERROR$2, {
6443
+ requestId,
6444
+ error:
6445
+ "Claude Code CLI not found. Install from https://claude.ai/download",
6446
+ code: "CLI_NOT_FOUND",
6447
+ });
6448
+ return;
6449
+ }
6450
+
6451
+ // Build CLI args
6452
+ const args = ["-p", "--output-format", "stream-json", "--verbose"];
6453
+
6454
+ if (model) {
6455
+ args.push("--model", model);
6456
+ }
6457
+
6458
+ if (systemPrompt) {
6459
+ args.push("--append-system-prompt", systemPrompt);
6460
+ }
6461
+
6462
+ // Resume existing session for conversation continuity
6463
+ const sessionId = widgetUuid ? sessions.get(widgetUuid) : null;
6464
+ if (sessionId) {
6465
+ args.push("--resume", sessionId);
6466
+ }
6467
+
6468
+ // Extract the user message (last user message in the array)
6469
+ const lastUserMsg = [...messages].reverse().find((m) => m.role === "user");
6470
+ const userText =
6471
+ typeof lastUserMsg?.content === "string"
6472
+ ? lastUserMsg.content
6473
+ : Array.isArray(lastUserMsg?.content)
6474
+ ? lastUserMsg.content
6475
+ .filter((b) => b.type === "text")
6476
+ .map((b) => b.text)
6477
+ .join("\n")
6478
+ : "";
6479
+
6480
+ if (!userText) {
6481
+ safeSend(win, LLM_STREAM_ERROR$2, {
6482
+ requestId,
6483
+ error: "No user message to send.",
6484
+ code: "CLI_ERROR",
6485
+ });
6486
+ return;
6487
+ }
6488
+
6489
+ try {
6490
+ const fullPath = getShellPath();
6491
+ const child = spawn(binaryPath, args, {
6492
+ env: { ...process.env, PATH: fullPath },
6493
+ stdio: ["pipe", "pipe", "pipe"],
6494
+ });
6495
+
6496
+ activeProcesses.set(requestId, child);
6497
+
6498
+ // Pipe user message via stdin (not visible in ps)
6499
+ child.stdin.write(userText);
6500
+ child.stdin.end();
6501
+
6502
+ let stdoutBuffer = "";
6503
+ let stderrBuffer = "";
6504
+ let capturedSessionId = null;
6505
+ let retried = false;
6506
+
6507
+ // Track active tool calls for mapping results
6508
+ const activeToolCalls = new Map();
6509
+
6510
+ child.stdout.on("data", (chunk) => {
6511
+ stdoutBuffer += chunk.toString();
6512
+
6513
+ // Process complete lines
6514
+ const lines = stdoutBuffer.split("\n");
6515
+ stdoutBuffer = lines.pop(); // keep incomplete line in buffer
6516
+
6517
+ for (const line of lines) {
6518
+ if (!line.trim()) continue;
6519
+
6520
+ let parsed;
6521
+ try {
6522
+ parsed = JSON.parse(line);
6523
+ } catch {
6524
+ console.warn("[cliController] Skipping invalid JSON line:", line);
6525
+ continue;
6526
+ }
6527
+
6528
+ // Capture session ID from any message that has it
6529
+ if (parsed.session_id && widgetUuid) {
6530
+ capturedSessionId = parsed.session_id;
6531
+ sessions.set(widgetUuid, capturedSessionId);
6532
+ }
6533
+
6534
+ // Map CLI stream-json events to IPC events
6535
+ if (parsed.type === "content_block_delta") {
6536
+ if (parsed.delta?.type === "text_delta" && parsed.delta.text) {
6537
+ safeSend(win, LLM_STREAM_DELTA$2, {
6538
+ requestId,
6539
+ text: parsed.delta.text,
6540
+ });
6541
+ } else if (parsed.delta?.type === "input_json_delta") {
6542
+ // Update tool input incrementally
6543
+ const tc = activeToolCalls.get(parsed.index);
6544
+ if (tc) {
6545
+ tc.partialInput =
6546
+ (tc.partialInput || "") + (parsed.delta.partial_json || "");
6547
+ }
6548
+ }
6549
+ } else if (parsed.type === "content_block_start") {
6550
+ if (parsed.content_block?.type === "tool_use") {
6551
+ const toolBlock = parsed.content_block;
6552
+ activeToolCalls.set(parsed.index, {
6553
+ toolUseId: toolBlock.id,
6554
+ toolName: toolBlock.name,
6555
+ partialInput: "",
6556
+ });
6557
+ safeSend(win, LLM_STREAM_TOOL_CALL$2, {
6558
+ requestId,
6559
+ toolUseId: toolBlock.id,
6560
+ toolName: toolBlock.name,
6561
+ serverName: "Claude Code",
6562
+ input: toolBlock.input || {},
6563
+ });
6564
+ }
6565
+ } else if (parsed.type === "content_block_stop") {
6566
+ // Tool call completed — try to parse the accumulated input
6567
+ const tc = activeToolCalls.get(parsed.index);
6568
+ if (tc && tc.partialInput) {
6569
+ try {
6570
+ tc.finalInput = JSON.parse(tc.partialInput);
6571
+ } catch {
6572
+ tc.finalInput = tc.partialInput;
6573
+ }
6574
+ }
6575
+ } else if (parsed.type === "message_stop") {
6576
+ // Individual message completed (may be followed by more in tool-use loops)
6577
+ } else if (parsed.type === "result") {
6578
+ // Final result — conversation complete
6579
+ const content = [];
6580
+ if (parsed.result) {
6581
+ content.push({ type: "text", text: parsed.result });
6582
+ }
6583
+
6584
+ safeSend(win, LLM_STREAM_COMPLETE$2, {
6585
+ requestId,
6586
+ content,
6587
+ stopReason: parsed.stop_reason || "end_turn",
6588
+ usage: parsed.usage || {},
6589
+ });
6590
+ }
6591
+ }
6592
+ });
6593
+
6594
+ child.stderr.on("data", (chunk) => {
6595
+ stderrBuffer += chunk.toString();
6596
+ });
6597
+
6598
+ child.on("error", (err) => {
6599
+ activeProcesses.delete(requestId);
6600
+ safeSend(win, LLM_STREAM_ERROR$2, {
6601
+ requestId,
6602
+ error: `Failed to start Claude CLI: ${err.message}`,
6603
+ code: "CLI_SPAWN_ERROR",
6604
+ });
6605
+ });
6606
+
6607
+ child.on("close", (code) => {
6608
+ activeProcesses.delete(requestId);
6609
+
6610
+ // Process any remaining buffer
6611
+ if (stdoutBuffer.trim()) {
6612
+ try {
6613
+ const parsed = JSON.parse(stdoutBuffer);
6614
+ if (parsed.session_id && widgetUuid) {
6615
+ sessions.set(widgetUuid, parsed.session_id);
6616
+ }
6617
+ if (parsed.type === "result") {
6618
+ const content = [];
6619
+ if (parsed.result) {
6620
+ content.push({ type: "text", text: parsed.result });
6621
+ }
6622
+ safeSend(win, LLM_STREAM_COMPLETE$2, {
6623
+ requestId,
6624
+ content,
6625
+ stopReason: parsed.stop_reason || "end_turn",
6626
+ usage: parsed.usage || {},
6627
+ });
6628
+ return;
6629
+ }
6630
+ } catch {
6631
+ // ignore
6632
+ }
6633
+ }
6634
+
6635
+ if (code !== 0 && code !== null) {
6636
+ // Check if resume failed and retry without it
6637
+ if (sessionId && !retried && stderrBuffer.includes("session")) {
6638
+ retried = true;
6639
+ if (widgetUuid) sessions.delete(widgetUuid);
6640
+ // Retry without --resume
6641
+ cliController$2.sendMessage(win, requestId, {
6642
+ ...params,
6643
+ _retryWithoutResume: true,
6644
+ });
6645
+ return;
6646
+ }
6647
+
6648
+ // Check for auth errors
6649
+ if (
6650
+ stderrBuffer.includes("auth") ||
6651
+ stderrBuffer.includes("login") ||
6652
+ stderrBuffer.includes("not authenticated")
6653
+ ) {
6654
+ safeSend(win, LLM_STREAM_ERROR$2, {
6655
+ requestId,
6656
+ error:
6657
+ "Claude Code CLI is not authenticated. Run `claude auth login` in your terminal.",
6658
+ code: "CLI_AUTH_ERROR",
6659
+ });
6660
+ return;
6661
+ }
6662
+
6663
+ safeSend(win, LLM_STREAM_ERROR$2, {
6664
+ requestId,
6665
+ error: `Claude CLI exited with code ${code}${stderrBuffer ? ": " + stderrBuffer.slice(0, 500) : ""}`,
6666
+ code: "CLI_ERROR",
6667
+ });
6668
+ }
6669
+ });
6670
+ } catch (err) {
6671
+ activeProcesses.delete(requestId);
6672
+ safeSend(win, LLM_STREAM_ERROR$2, {
6673
+ requestId,
6674
+ error: `Failed to start Claude CLI: ${err.message}`,
6675
+ code: "CLI_SPAWN_ERROR",
6676
+ });
6677
+ }
6678
+ },
6679
+
6680
+ /**
6681
+ * abortRequest
6682
+ * Kill an in-flight CLI process.
6683
+ *
6684
+ * @param {string} requestId - the request to cancel
6685
+ * @returns {{ success: boolean }}
6686
+ */
6687
+ abortRequest: (requestId) => {
6688
+ const child = activeProcesses.get(requestId);
6689
+ if (child) {
6690
+ child.kill("SIGTERM");
6691
+ activeProcesses.delete(requestId);
6692
+ return { success: true };
6693
+ }
6694
+ return { success: false, message: "Request not found" };
6695
+ },
6696
+
6697
+ /**
6698
+ * clearSession
6699
+ * Remove the stored session ID for a widget (called on "New Chat").
6700
+ *
6701
+ * @param {string} widgetUuid - the widget whose session to clear
6702
+ * @returns {{ success: boolean }}
6703
+ */
6704
+ clearSession: (widgetUuid) => {
6705
+ if (widgetUuid && sessions.has(widgetUuid)) {
6706
+ sessions.delete(widgetUuid);
6707
+ return { success: true };
6708
+ }
6709
+ return { success: false };
6710
+ },
6711
+
6712
+ /**
6713
+ * getSessionStatus
6714
+ * Check if a CLI session exists and whether a process is active for a widget.
6715
+ *
6716
+ * @param {string} widgetUuid - the widget to check
6717
+ * @returns {{ hasSession: boolean, sessionId?: string, isProcessActive: boolean }}
6718
+ */
6719
+ getSessionStatus: (widgetUuid) => {
6720
+ const sessionId = widgetUuid ? sessions.get(widgetUuid) : null;
6721
+ // Check if any active process belongs to this widget
6722
+ let isProcessActive = false;
6723
+ for (const [, child] of activeProcesses) {
6724
+ if (!child.killed) {
6725
+ isProcessActive = true;
6726
+ break;
6727
+ }
6728
+ }
6729
+ return {
6730
+ hasSession: !!sessionId,
6731
+ sessionId: sessionId || undefined,
6732
+ isProcessActive,
6733
+ };
6734
+ },
6735
+
6736
+ /**
6737
+ * endSession
6738
+ * Kill any active CLI process AND clear the session for a widget.
6739
+ *
6740
+ * @param {string} widgetUuid - the widget whose session to end
6741
+ * @returns {{ success: boolean }}
6742
+ */
6743
+ endSession: (widgetUuid) => {
6744
+ // Kill any active processes for this widget
6745
+ for (const [reqId, child] of activeProcesses) {
6746
+ if (reqId.startsWith(widgetUuid)) {
6747
+ child.kill("SIGTERM");
6748
+ activeProcesses.delete(reqId);
6749
+ }
6750
+ }
6751
+ // Clear the session
6752
+ if (widgetUuid && sessions.has(widgetUuid)) {
6753
+ sessions.delete(widgetUuid);
6754
+ }
6755
+ return { success: true };
6756
+ },
6757
+ };
6758
+
6759
+ var cliController_1 = cliController$2;
6760
+
6320
6761
  /**
6321
6762
  * llmController.js
6322
6763
  *
@@ -6328,8 +6769,9 @@ var pluginController_1 = pluginController$1;
6328
6769
  * per-request, receiving the full messages array each time.
6329
6770
  */
6330
6771
 
6331
- const Anthropic = require$$0$4;
6772
+ const Anthropic = require$$0$5;
6332
6773
  const mcpController$1 = mcpController_1;
6774
+ const cliController$1 = cliController_1;
6333
6775
  const {
6334
6776
  LLM_STREAM_DELTA: LLM_STREAM_DELTA$1,
6335
6777
  LLM_STREAM_TOOL_CALL: LLM_STREAM_TOOL_CALL$1,
@@ -6398,6 +6840,11 @@ const llmController$1 = {
6398
6840
  * @param {object} params - { apiKey, model, messages, tools, toolServerMap, systemPrompt, maxToolRounds }
6399
6841
  */
6400
6842
  sendMessage: async (win, requestId, params) => {
6843
+ // Route to CLI backend if specified
6844
+ if (params.backend === "claude-code") {
6845
+ return cliController$1.sendMessage(win, requestId, params);
6846
+ }
6847
+
6401
6848
  const {
6402
6849
  apiKey,
6403
6850
  model = "claude-sonnet-4-20250514",
@@ -6621,7 +7068,8 @@ const llmController$1 = {
6621
7068
  activeRequests.delete(requestId);
6622
7069
  return { success: true };
6623
7070
  }
6624
- return { success: false, message: "Request not found" };
7071
+ // Fallback to CLI controller
7072
+ return cliController$1.abortRequest(requestId);
6625
7073
  },
6626
7074
  };
6627
7075
 
@@ -7889,6 +8337,10 @@ const {
7889
8337
  LLM_SEND_MESSAGE,
7890
8338
  LLM_ABORT_REQUEST,
7891
8339
  LLM_LIST_CONNECTED_TOOLS,
8340
+ LLM_CHECK_CLI_AVAILABLE,
8341
+ LLM_CLEAR_CLI_SESSION,
8342
+ LLM_CLI_SESSION_STATUS,
8343
+ LLM_CLI_END_SESSION,
7892
8344
  LLM_STREAM_DELTA,
7893
8345
  LLM_STREAM_TOOL_CALL,
7894
8346
  LLM_STREAM_TOOL_RESULT,
@@ -7926,51 +8378,129 @@ const llmApi$2 = {
7926
8378
  */
7927
8379
  listConnectedTools: () => ipcRenderer$2.invoke(LLM_LIST_CONNECTED_TOOLS),
7928
8380
 
8381
+ /**
8382
+ * checkCliAvailable
8383
+ * Check if the Claude Code CLI is installed and accessible.
8384
+ *
8385
+ * @returns {Promise<{ available: boolean, path?: string }>}
8386
+ */
8387
+ checkCliAvailable: () => ipcRenderer$2.invoke(LLM_CHECK_CLI_AVAILABLE),
8388
+
8389
+ /**
8390
+ * clearCliSession
8391
+ * Clear the CLI conversation session for a widget (for "New Chat").
8392
+ *
8393
+ * @param {string} widgetUuid - the widget whose session to clear
8394
+ * @returns {Promise<{ success: boolean }>}
8395
+ */
8396
+ clearCliSession: (widgetUuid) =>
8397
+ ipcRenderer$2.invoke(LLM_CLEAR_CLI_SESSION, { widgetUuid }),
8398
+
8399
+ /**
8400
+ * getCliSessionStatus
8401
+ * Check if a CLI session is active for a widget.
8402
+ *
8403
+ * @param {string} widgetUuid - the widget to check
8404
+ * @returns {Promise<{ hasSession: boolean, sessionId?: string, isProcessActive: boolean }>}
8405
+ */
8406
+ getCliSessionStatus: (widgetUuid) =>
8407
+ ipcRenderer$2.invoke(LLM_CLI_SESSION_STATUS, { widgetUuid }),
8408
+
8409
+ /**
8410
+ * endCliSession
8411
+ * Kill any active CLI process AND clear the session for a widget.
8412
+ *
8413
+ * @param {string} widgetUuid - the widget whose session to end
8414
+ * @returns {Promise<{ success: boolean }>}
8415
+ */
8416
+ endCliSession: (widgetUuid) =>
8417
+ ipcRenderer$2.invoke(LLM_CLI_END_SESSION, { widgetUuid }),
8418
+
7929
8419
  // --- Stream event listeners ---
8420
+ // Each on* method returns the wrapped callback so callers can remove
8421
+ // their own listener without nuking listeners from other widgets.
7930
8422
 
7931
8423
  /**
7932
8424
  * onStreamDelta
7933
8425
  * Listen for text chunks as they stream in.
8426
+ * @returns {Function} wrapped callback for use with removeStreamListener
7934
8427
  */
7935
8428
  onStreamDelta: (callback) => {
7936
- ipcRenderer$2.on(LLM_STREAM_DELTA, (_event, data) => callback(data));
8429
+ const wrapped = (_event, data) => callback(data);
8430
+ ipcRenderer$2.on(LLM_STREAM_DELTA, wrapped);
8431
+ return wrapped;
7937
8432
  },
7938
8433
 
7939
8434
  /**
7940
8435
  * onStreamToolCall
7941
8436
  * Listen for tool call notifications.
8437
+ * @returns {Function} wrapped callback for use with removeStreamListener
7942
8438
  */
7943
8439
  onStreamToolCall: (callback) => {
7944
- ipcRenderer$2.on(LLM_STREAM_TOOL_CALL, (_event, data) => callback(data));
8440
+ const wrapped = (_event, data) => callback(data);
8441
+ ipcRenderer$2.on(LLM_STREAM_TOOL_CALL, wrapped);
8442
+ return wrapped;
7945
8443
  },
7946
8444
 
7947
8445
  /**
7948
8446
  * onStreamToolResult
7949
8447
  * Listen for tool result notifications.
8448
+ * @returns {Function} wrapped callback for use with removeStreamListener
7950
8449
  */
7951
8450
  onStreamToolResult: (callback) => {
7952
- ipcRenderer$2.on(LLM_STREAM_TOOL_RESULT, (_event, data) => callback(data));
8451
+ const wrapped = (_event, data) => callback(data);
8452
+ ipcRenderer$2.on(LLM_STREAM_TOOL_RESULT, wrapped);
8453
+ return wrapped;
7953
8454
  },
7954
8455
 
7955
8456
  /**
7956
8457
  * onStreamComplete
7957
8458
  * Listen for stream completion (final response).
8459
+ * @returns {Function} wrapped callback for use with removeStreamListener
7958
8460
  */
7959
8461
  onStreamComplete: (callback) => {
7960
- ipcRenderer$2.on(LLM_STREAM_COMPLETE, (_event, data) => callback(data));
8462
+ const wrapped = (_event, data) => callback(data);
8463
+ ipcRenderer$2.on(LLM_STREAM_COMPLETE, wrapped);
8464
+ return wrapped;
7961
8465
  },
7962
8466
 
7963
8467
  /**
7964
8468
  * onStreamError
7965
8469
  * Listen for stream errors.
8470
+ * @returns {Function} wrapped callback for use with removeStreamListener
7966
8471
  */
7967
8472
  onStreamError: (callback) => {
7968
- ipcRenderer$2.on(LLM_STREAM_ERROR, (_event, data) => callback(data));
8473
+ const wrapped = (_event, data) => callback(data);
8474
+ ipcRenderer$2.on(LLM_STREAM_ERROR, wrapped);
8475
+ return wrapped;
8476
+ },
8477
+
8478
+ /**
8479
+ * removeStreamListener
8480
+ * Remove a specific stream listener by channel and callback reference.
8481
+ *
8482
+ * @param {string} channel - the IPC channel name
8483
+ * @param {Function} wrapped - the callback returned by on*
8484
+ */
8485
+ removeStreamListener: (channel, wrapped) => {
8486
+ ipcRenderer$2.removeListener(channel, wrapped);
8487
+ },
8488
+
8489
+ /**
8490
+ * Stream channel constants for use with removeStreamListener.
8491
+ */
8492
+ streamChannels: {
8493
+ delta: LLM_STREAM_DELTA,
8494
+ toolCall: LLM_STREAM_TOOL_CALL,
8495
+ toolResult: LLM_STREAM_TOOL_RESULT,
8496
+ complete: LLM_STREAM_COMPLETE,
8497
+ error: LLM_STREAM_ERROR,
7969
8498
  },
7970
8499
 
7971
8500
  /**
7972
8501
  * removeAllStreamListeners
7973
- * Clean up all LLM stream listeners.
8502
+ * Clean up ALL LLM stream listeners (global).
8503
+ * Prefer removeStreamListener for scoped cleanup.
7974
8504
  */
7975
8505
  removeAllStreamListeners: () => {
7976
8506
  ipcRenderer$2.removeAllListeners(LLM_STREAM_DELTA);
@@ -9553,6 +10083,7 @@ const openaiController = openaiController_1;
9553
10083
  const menuItemsController = menuItemsController_1;
9554
10084
  const pluginController = pluginController_1;
9555
10085
  const llmController = llmController_1;
10086
+ const cliController = cliController_1;
9556
10087
 
9557
10088
  // --- Utils ---
9558
10089
  const clientCache = requireClientCache();
@@ -9609,6 +10140,7 @@ var electron = {
9609
10140
  menuItemsController,
9610
10141
  pluginController,
9611
10142
  llmController,
10143
+ cliController,
9612
10144
 
9613
10145
  // Controller functions (flat) — spread for convenient destructuring
9614
10146
  ...controllers,