@trops/dash-core 0.1.385 → 0.1.387

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.
@@ -28153,1027 +28153,6 @@ const pluginController$1 = {
28153
28153
 
28154
28154
  var pluginController_1 = pluginController$1;
28155
28155
 
28156
- /**
28157
- * cliController.js
28158
- *
28159
- * Manages Claude Code CLI (`claude -p`) as an alternative LLM backend.
28160
- * Spawns the CLI subprocess, parses stream-json NDJSON output, and emits
28161
- * the same LLM_STREAM_* events as the Anthropic SDK path.
28162
- *
28163
- * Users with a Claude Pro/Max subscription and Claude Code installed
28164
- * can use the Chat widget without a separate API key.
28165
- */
28166
-
28167
- const { spawn, execSync } = require$$7;
28168
- const {
28169
- LLM_STREAM_DELTA: LLM_STREAM_DELTA$2,
28170
- LLM_STREAM_TOOL_CALL: LLM_STREAM_TOOL_CALL$2,
28171
- LLM_STREAM_TOOL_RESULT: LLM_STREAM_TOOL_RESULT$2,
28172
- LLM_STREAM_COMPLETE: LLM_STREAM_COMPLETE$2,
28173
- LLM_STREAM_ERROR: LLM_STREAM_ERROR$2,
28174
- } = llmEvents$1;
28175
-
28176
- const IS_WINDOWS = process.platform === "win32";
28177
-
28178
- /**
28179
- * Cached shell PATH result (resolved once, reused for all spawns).
28180
- * Same pattern as mcpController.js.
28181
- */
28182
- let _shellPath = null;
28183
-
28184
- function getShellPath() {
28185
- if (_shellPath !== null) return _shellPath;
28186
-
28187
- // Windows: no POSIX login-shell trick — just use the inherited PATH,
28188
- // which is typically correct for GUI-launched Electron apps.
28189
- if (IS_WINDOWS) {
28190
- _shellPath = process.env.PATH || "";
28191
- return _shellPath;
28192
- }
28193
-
28194
- try {
28195
- const shell = process.env.SHELL || "/bin/bash";
28196
- _shellPath = execSync(`${shell} -ilc 'echo -n "$PATH"'`, {
28197
- encoding: "utf8",
28198
- timeout: 5000,
28199
- });
28200
- } catch {
28201
- _shellPath = process.env.PATH || "";
28202
- }
28203
-
28204
- return _shellPath;
28205
- }
28206
-
28207
- /**
28208
- * Cached CLI binary path (resolved once via `which` / `where`).
28209
- */
28210
- let _cliBinaryPath = undefined; // undefined = not yet checked
28211
-
28212
- function resolveCliBinary() {
28213
- if (_cliBinaryPath !== undefined) return _cliBinaryPath;
28214
-
28215
- try {
28216
- const fullPath = getShellPath();
28217
- // `where` on Windows, `which` everywhere else. `where` may list
28218
- // multiple matches on separate lines (e.g. claude.cmd + claude.ps1)
28219
- // — take the first hit.
28220
- const lookup = IS_WINDOWS ? "where claude" : "which claude";
28221
- const result = execSync(lookup, {
28222
- encoding: "utf8",
28223
- timeout: 5000,
28224
- env: { ...process.env, PATH: fullPath },
28225
- });
28226
- _cliBinaryPath = IS_WINDOWS
28227
- ? result
28228
- .split(/\r?\n/)
28229
- .find((l) => l.trim())
28230
- ?.trim() || null
28231
- : result.trim();
28232
- } catch {
28233
- _cliBinaryPath = null;
28234
- }
28235
-
28236
- return _cliBinaryPath;
28237
- }
28238
-
28239
- /**
28240
- * Active CLI processes for abort support.
28241
- * Map<requestId, ChildProcess>
28242
- */
28243
- const activeProcesses = new Map();
28244
-
28245
- /**
28246
- * Kill a child process and its descendants. On Windows, spawning with
28247
- * shell:true (needed for .cmd targets) means child.kill() only
28248
- * terminates the cmd.exe — the real CLI keeps running. Use taskkill
28249
- * with /T (tree) /F (force) to clean up.
28250
- */
28251
- function killChildTree(child) {
28252
- if (!child || child.killed || typeof child.pid !== "number") return;
28253
- if (IS_WINDOWS) {
28254
- try {
28255
- execSync(`taskkill /pid ${child.pid} /T /F`, {
28256
- stdio: "ignore",
28257
- timeout: 5000,
28258
- });
28259
- } catch {
28260
- // Fall back to plain kill — best-effort
28261
- try {
28262
- child.kill();
28263
- } catch {
28264
- /* ignore */
28265
- }
28266
- }
28267
- } else {
28268
- child.kill("SIGTERM");
28269
- }
28270
- }
28271
-
28272
- /**
28273
- * Session IDs for conversation continuity.
28274
- * Map<widgetUuid, sessionId>
28275
- */
28276
- const sessions = new Map();
28277
-
28278
- /**
28279
- * Send events safely to a window.
28280
- */
28281
- function safeSend(win, channel, data) {
28282
- if (win && !win.isDestroyed()) {
28283
- win.webContents.send(channel, data);
28284
- }
28285
- }
28286
-
28287
- const cliController$2 = {
28288
- /**
28289
- * isAvailable
28290
- * Check if the Claude Code CLI is installed and accessible.
28291
- *
28292
- * @returns {{ available: boolean, path?: string }}
28293
- */
28294
- isAvailable: () => {
28295
- const binaryPath = resolveCliBinary();
28296
- if (binaryPath) {
28297
- return { available: true, path: binaryPath };
28298
- }
28299
- return { available: false };
28300
- },
28301
-
28302
- /**
28303
- * sendMessage
28304
- * Stream a response from the Claude Code CLI with NDJSON parsing.
28305
- *
28306
- * @param {BrowserWindow} win - the window to send stream events to
28307
- * @param {string} requestId - unique ID for this request
28308
- * @param {object} params - { model, messages, systemPrompt, maxToolRounds, widgetUuid }
28309
- */
28310
- sendMessage: async (win, requestId, params) => {
28311
- const { model, messages, systemPrompt, widgetUuid, cwd } = params;
28312
-
28313
- const binaryPath = resolveCliBinary();
28314
- if (!binaryPath) {
28315
- safeSend(win, LLM_STREAM_ERROR$2, {
28316
- requestId,
28317
- error:
28318
- "Claude Code CLI not found. Install from https://claude.ai/download",
28319
- code: "CLI_NOT_FOUND",
28320
- });
28321
- return;
28322
- }
28323
-
28324
- // Build CLI args.
28325
- //
28326
- // --disable-slash-commands: prevent Claude from auto-triggering project
28327
- // skills by description match. When running in a project with a
28328
- // `.claude/skills/<name>/SKILL.md`, Claude would otherwise internalize
28329
- // that skill's content even when the host app's system prompt says not
28330
- // to — causing long reasoning loops or silent hangs. We pass only the
28331
- // caller's `systemPrompt` as context.
28332
- //
28333
- // --permission-mode bypassPermissions: in an embedded in-app assistant,
28334
- // the user has already opted into the configured MCP servers (they
28335
- // ran `claude mcp add` themselves). Prompting for tool-use approval
28336
- // on every call produces "I need permission to..." replies instead of
28337
- // actual actions. Bypassing matches the user's intent — if they
28338
- // didn't want the assistant to use a tool, they wouldn't have
28339
- // configured it.
28340
- //
28341
- // (We intentionally avoid `--bare` — it also disables keychain reads,
28342
- // which breaks OAuth login for users authenticated via `claude login`.)
28343
- const args = [
28344
- "-p",
28345
- "--disable-slash-commands",
28346
- "--permission-mode",
28347
- "bypassPermissions",
28348
- "--output-format",
28349
- "stream-json",
28350
- "--verbose",
28351
- ];
28352
-
28353
- if (model) {
28354
- args.push("--model", model);
28355
- }
28356
-
28357
- if (systemPrompt) {
28358
- args.push("--append-system-prompt", systemPrompt);
28359
- }
28360
-
28361
- // Resume existing session for conversation continuity
28362
- const sessionId = widgetUuid ? sessions.get(widgetUuid) : null;
28363
- if (sessionId) {
28364
- args.push("--resume", sessionId);
28365
- }
28366
-
28367
- // Extract the user message (last user message in the array)
28368
- const lastUserMsg = [...messages].reverse().find((m) => m.role === "user");
28369
- const userText =
28370
- typeof lastUserMsg?.content === "string"
28371
- ? lastUserMsg.content
28372
- : Array.isArray(lastUserMsg?.content)
28373
- ? lastUserMsg.content
28374
- .filter((b) => b.type === "text")
28375
- .map((b) => b.text)
28376
- .join("\n")
28377
- : "";
28378
-
28379
- if (!userText) {
28380
- safeSend(win, LLM_STREAM_ERROR$2, {
28381
- requestId,
28382
- error: "No user message to send.",
28383
- code: "CLI_ERROR",
28384
- });
28385
- return;
28386
- }
28387
-
28388
- try {
28389
- const fullPath = getShellPath();
28390
- const spawnOpts = {
28391
- env: { ...process.env, PATH: fullPath },
28392
- stdio: ["pipe", "pipe", "pipe"],
28393
- // On Windows, the Claude CLI is typically installed as claude.cmd
28394
- // (a batch wrapper). Node's child_process.spawn can't launch .cmd
28395
- // files directly without a shell — ENOENT otherwise.
28396
- shell: IS_WINDOWS,
28397
- };
28398
- if (cwd) {
28399
- const fs = require("fs");
28400
- if (!fs.existsSync(cwd)) {
28401
- fs.mkdirSync(cwd, { recursive: true });
28402
- }
28403
- spawnOpts.cwd = cwd;
28404
- }
28405
- const child = spawn(binaryPath, args, spawnOpts);
28406
-
28407
- activeProcesses.set(requestId, child);
28408
-
28409
- // Pipe user message via stdin (not visible in ps)
28410
- child.stdin.write(userText);
28411
- child.stdin.end();
28412
-
28413
- let stdoutBuffer = "";
28414
- let stderrBuffer = "";
28415
- let capturedSessionId = null;
28416
- let retried = false;
28417
-
28418
- // Track active tool calls for mapping results
28419
- const activeToolCalls = new Map();
28420
-
28421
- child.stdout.on("data", (chunk) => {
28422
- stdoutBuffer += chunk.toString();
28423
-
28424
- // Process complete lines
28425
- const lines = stdoutBuffer.split("\n");
28426
- stdoutBuffer = lines.pop(); // keep incomplete line in buffer
28427
-
28428
- for (const line of lines) {
28429
- if (!line.trim()) continue;
28430
-
28431
- let parsed;
28432
- try {
28433
- parsed = JSON.parse(line);
28434
- } catch {
28435
- console.warn("[cliController] Skipping invalid JSON line:", line);
28436
- continue;
28437
- }
28438
-
28439
- // Capture session ID from any message that has it
28440
- if (parsed.session_id && widgetUuid) {
28441
- capturedSessionId = parsed.session_id;
28442
- sessions.set(widgetUuid, capturedSessionId);
28443
- }
28444
-
28445
- // Map CLI stream-json events to IPC events
28446
- if (parsed.type === "content_block_delta") {
28447
- if (parsed.delta?.type === "text_delta" && parsed.delta.text) {
28448
- safeSend(win, LLM_STREAM_DELTA$2, {
28449
- requestId,
28450
- text: parsed.delta.text,
28451
- });
28452
- } else if (parsed.delta?.type === "input_json_delta") {
28453
- // Update tool input incrementally
28454
- const tc = activeToolCalls.get(parsed.index);
28455
- if (tc) {
28456
- tc.partialInput =
28457
- (tc.partialInput || "") + (parsed.delta.partial_json || "");
28458
- }
28459
- }
28460
- } else if (parsed.type === "content_block_start") {
28461
- if (parsed.content_block?.type === "tool_use") {
28462
- const toolBlock = parsed.content_block;
28463
- activeToolCalls.set(parsed.index, {
28464
- toolUseId: toolBlock.id,
28465
- toolName: toolBlock.name,
28466
- partialInput: "",
28467
- });
28468
- safeSend(win, LLM_STREAM_TOOL_CALL$2, {
28469
- requestId,
28470
- toolUseId: toolBlock.id,
28471
- toolName: toolBlock.name,
28472
- serverName: "Claude Code",
28473
- input: toolBlock.input || {},
28474
- });
28475
- }
28476
- } else if (parsed.type === "content_block_stop") {
28477
- // Tool call completed — try to parse the accumulated input
28478
- const tc = activeToolCalls.get(parsed.index);
28479
- if (tc && tc.partialInput) {
28480
- try {
28481
- tc.finalInput = JSON.parse(tc.partialInput);
28482
- } catch {
28483
- tc.finalInput = tc.partialInput;
28484
- }
28485
- }
28486
- } else if (parsed.type === "message_stop") {
28487
- // Individual message completed (may be followed by more in tool-use loops)
28488
- } else if (parsed.type === "result") {
28489
- // Final result — conversation complete
28490
- const content = [];
28491
- if (parsed.result) {
28492
- content.push({ type: "text", text: parsed.result });
28493
- }
28494
-
28495
- safeSend(win, LLM_STREAM_COMPLETE$2, {
28496
- requestId,
28497
- content,
28498
- stopReason: parsed.stop_reason || "end_turn",
28499
- usage: parsed.usage || {},
28500
- });
28501
- }
28502
- }
28503
- });
28504
-
28505
- child.stderr.on("data", (chunk) => {
28506
- stderrBuffer += chunk.toString();
28507
- });
28508
-
28509
- child.on("error", (err) => {
28510
- activeProcesses.delete(requestId);
28511
- safeSend(win, LLM_STREAM_ERROR$2, {
28512
- requestId,
28513
- error: `Failed to start Claude CLI: ${err.message}`,
28514
- code: "CLI_SPAWN_ERROR",
28515
- });
28516
- });
28517
-
28518
- child.on("close", (code) => {
28519
- activeProcesses.delete(requestId);
28520
-
28521
- // Process any remaining buffer
28522
- if (stdoutBuffer.trim()) {
28523
- try {
28524
- const parsed = JSON.parse(stdoutBuffer);
28525
- if (parsed.session_id && widgetUuid) {
28526
- sessions.set(widgetUuid, parsed.session_id);
28527
- }
28528
- if (parsed.type === "result") {
28529
- const content = [];
28530
- if (parsed.result) {
28531
- content.push({ type: "text", text: parsed.result });
28532
- }
28533
- safeSend(win, LLM_STREAM_COMPLETE$2, {
28534
- requestId,
28535
- content,
28536
- stopReason: parsed.stop_reason || "end_turn",
28537
- usage: parsed.usage || {},
28538
- });
28539
- return;
28540
- }
28541
- } catch {
28542
- // ignore
28543
- }
28544
- }
28545
-
28546
- if (code !== 0 && code !== null) {
28547
- // Check if resume failed and retry without it
28548
- if (sessionId && !retried && stderrBuffer.includes("session")) {
28549
- retried = true;
28550
- if (widgetUuid) sessions.delete(widgetUuid);
28551
- // Retry without --resume
28552
- cliController$2.sendMessage(win, requestId, {
28553
- ...params,
28554
- _retryWithoutResume: true,
28555
- });
28556
- return;
28557
- }
28558
-
28559
- // Check for auth errors
28560
- if (
28561
- stderrBuffer.includes("auth") ||
28562
- stderrBuffer.includes("login") ||
28563
- stderrBuffer.includes("not authenticated")
28564
- ) {
28565
- safeSend(win, LLM_STREAM_ERROR$2, {
28566
- requestId,
28567
- error:
28568
- "Claude Code CLI is not authenticated. Run `claude auth login` in your terminal.",
28569
- code: "CLI_AUTH_ERROR",
28570
- });
28571
- return;
28572
- }
28573
-
28574
- safeSend(win, LLM_STREAM_ERROR$2, {
28575
- requestId,
28576
- error: `Claude CLI exited with code ${code}${stderrBuffer ? ": " + stderrBuffer.slice(0, 500) : ""}`,
28577
- code: "CLI_ERROR",
28578
- });
28579
- }
28580
- });
28581
- } catch (err) {
28582
- activeProcesses.delete(requestId);
28583
- safeSend(win, LLM_STREAM_ERROR$2, {
28584
- requestId,
28585
- error: `Failed to start Claude CLI: ${err.message}`,
28586
- code: "CLI_SPAWN_ERROR",
28587
- });
28588
- }
28589
- },
28590
-
28591
- /**
28592
- * abortRequest
28593
- * Kill an in-flight CLI process.
28594
- *
28595
- * @param {string} requestId - the request to cancel
28596
- * @returns {{ success: boolean }}
28597
- */
28598
- abortRequest: (requestId) => {
28599
- const child = activeProcesses.get(requestId);
28600
- if (child) {
28601
- killChildTree(child);
28602
- activeProcesses.delete(requestId);
28603
- return { success: true };
28604
- }
28605
- return { success: false, message: "Request not found" };
28606
- },
28607
-
28608
- /**
28609
- * clearSession
28610
- * Remove the stored session ID for a widget (called on "New Chat").
28611
- *
28612
- * @param {string} widgetUuid - the widget whose session to clear
28613
- * @returns {{ success: boolean }}
28614
- */
28615
- clearSession: (widgetUuid) => {
28616
- if (widgetUuid && sessions.has(widgetUuid)) {
28617
- sessions.delete(widgetUuid);
28618
- return { success: true };
28619
- }
28620
- return { success: false };
28621
- },
28622
-
28623
- /**
28624
- * getSessionStatus
28625
- * Check if a CLI session exists and whether a process is active for a widget.
28626
- *
28627
- * @param {string} widgetUuid - the widget to check
28628
- * @returns {{ hasSession: boolean, sessionId?: string, isProcessActive: boolean }}
28629
- */
28630
- getSessionStatus: (widgetUuid) => {
28631
- const sessionId = widgetUuid ? sessions.get(widgetUuid) : null;
28632
- // Check if any active process belongs to this widget
28633
- let isProcessActive = false;
28634
- for (const [, child] of activeProcesses) {
28635
- if (!child.killed) {
28636
- isProcessActive = true;
28637
- break;
28638
- }
28639
- }
28640
- return {
28641
- hasSession: !!sessionId,
28642
- sessionId: sessionId || undefined,
28643
- isProcessActive,
28644
- };
28645
- },
28646
-
28647
- /**
28648
- * endSession
28649
- * Kill any active CLI process AND clear the session for a widget.
28650
- *
28651
- * @param {string} widgetUuid - the widget whose session to end
28652
- * @returns {{ success: boolean }}
28653
- */
28654
- endSession: (widgetUuid) => {
28655
- // Kill any active processes for this widget
28656
- for (const [reqId, child] of activeProcesses) {
28657
- if (reqId.startsWith(widgetUuid)) {
28658
- killChildTree(child);
28659
- activeProcesses.delete(reqId);
28660
- }
28661
- }
28662
- // Clear the session
28663
- if (widgetUuid && sessions.has(widgetUuid)) {
28664
- sessions.delete(widgetUuid);
28665
- }
28666
- return { success: true };
28667
- },
28668
- };
28669
-
28670
- var cliController_1 = cliController$2;
28671
-
28672
- /**
28673
- * toolDefinitions.js
28674
- *
28675
- * MCP tool schemas for dashboard/workspace operations and app stats.
28676
- * Each definition includes name, description, and JSON Schema inputSchema.
28677
- */
28678
-
28679
- const dashboardTools$1 = [
28680
- {
28681
- name: "list_dashboards",
28682
- description:
28683
- "List all dashboards with their IDs, names, and widget counts. Use this to discover existing dashboards before creating new ones or to find a dashboard ID for other operations.",
28684
- inputSchema: {
28685
- type: "object",
28686
- properties: {},
28687
- required: [],
28688
- },
28689
- },
28690
- {
28691
- name: "get_dashboard",
28692
- description:
28693
- "Get full details of a dashboard including layout, widgets, and theme. Omit dashboardId to get the active dashboard. Use this to inspect widget configurations or to understand the current layout before making changes.",
28694
- inputSchema: {
28695
- type: "object",
28696
- properties: {
28697
- dashboardId: {
28698
- type: "string",
28699
- description:
28700
- "Dashboard ID. Omit to get the currently active dashboard.",
28701
- },
28702
- },
28703
- required: [],
28704
- },
28705
- },
28706
- {
28707
- name: "create_dashboard",
28708
- description:
28709
- "Create a new dashboard with the given name. Defaults to a 1×1 grid layout if `layout` is omitted — the resulting dashboard has a single cell ready for a widget. Pass an explicit `layout` object to use different dimensions. Pass `layout: null` only if the caller specifically wants a layout-less container dashboard (rare — widgets cannot be added without further editing). Returns the dashboard ID.",
28710
- inputSchema: {
28711
- type: "object",
28712
- properties: {
28713
- name: {
28714
- type: "string",
28715
- description: "Display name for the new dashboard",
28716
- },
28717
- layout: {
28718
- type: "object",
28719
- description:
28720
- "Optional grid layout configuration. When provided, creates a grid dashboard instead of a simple container.",
28721
- properties: {
28722
- rows: {
28723
- type: "number",
28724
- description: "Number of rows (1-10)",
28725
- },
28726
- cols: {
28727
- type: "number",
28728
- description: "Number of columns (1-10)",
28729
- },
28730
- gap: {
28731
- type: "string",
28732
- description:
28733
- "Tailwind gap class (e.g. 'gap-2', 'gap-4'). Defaults to 'gap-2'.",
28734
- },
28735
- colModes: {
28736
- type: "object",
28737
- description:
28738
- "Per-row column sizing. Keys are row numbers (as strings), values are mode strings: 'equal', '1/4', '1/3', '1/2', '2/3'.",
28739
- },
28740
- },
28741
- required: ["rows", "cols"],
28742
- },
28743
- },
28744
- required: ["name"],
28745
- },
28746
- },
28747
- {
28748
- name: "delete_dashboard",
28749
- description:
28750
- "Delete a dashboard by ID. Cannot delete the last remaining dashboard. Use list_dashboards first to find the dashboard ID.",
28751
- inputSchema: {
28752
- type: "object",
28753
- properties: {
28754
- dashboardId: {
28755
- type: "string",
28756
- description: "ID of the dashboard to delete",
28757
- },
28758
- },
28759
- required: ["dashboardId"],
28760
- },
28761
- },
28762
- {
28763
- name: "get_app_stats",
28764
- description:
28765
- "Get application statistics: counts of dashboards, widgets, themes, and providers. Useful for understanding the current state of the app at a glance.",
28766
- inputSchema: {
28767
- type: "object",
28768
- properties: {},
28769
- required: [],
28770
- },
28771
- },
28772
- ];
28773
-
28774
- const widgetTools$1 = [
28775
- {
28776
- name: "add_widget",
28777
- description:
28778
- "Add a widget to a dashboard by its scoped component name. IMPORTANT: Use the exact scoped name from list_widgets or search_widgets (format: 'scope.package.WidgetName', e.g. 'trops.gong.GongCallSearch'). Can be called multiple times. Returns the widget instance ID for use with configure_widget. If the dashboard has a grid layout, you can specify row/col for explicit placement, or omit them to auto-place in the next empty cell.",
28779
- inputSchema: {
28780
- type: "object",
28781
- properties: {
28782
- dashboardId: {
28783
- type: "string",
28784
- description:
28785
- "Dashboard ID to add the widget to. Omit to use the active dashboard.",
28786
- },
28787
- widgetName: {
28788
- type: "string",
28789
- description:
28790
- "Scoped component name from list_widgets/search_widgets (e.g. 'trops.gong.GongCallSearch', 'trops.slack.SlackChannelFeed')",
28791
- },
28792
- row: {
28793
- type: "number",
28794
- description:
28795
- "Grid row to place the widget in (1-indexed). Must be used together with col. Requires a grid layout on the dashboard.",
28796
- },
28797
- col: {
28798
- type: "number",
28799
- description:
28800
- "Grid column to place the widget in (1-indexed). Must be used together with row.",
28801
- },
28802
- },
28803
- required: ["widgetName"],
28804
- },
28805
- },
28806
- {
28807
- name: "remove_widget",
28808
- description:
28809
- "Remove a widget instance from a dashboard by its ID. Use get_dashboard to find widget instance IDs.",
28810
- inputSchema: {
28811
- type: "object",
28812
- properties: {
28813
- dashboardId: {
28814
- type: "string",
28815
- description: "Dashboard ID. Omit to use the active dashboard.",
28816
- },
28817
- widgetId: {
28818
- type: "string",
28819
- description: "ID of the widget instance to remove",
28820
- },
28821
- },
28822
- required: ["widgetId"],
28823
- },
28824
- },
28825
- {
28826
- name: "configure_widget",
28827
- description:
28828
- "Update a widget's configuration. The config object is merged into the existing config (partial update). Use get_dashboard to see current widget configs and discover valid config keys.",
28829
- inputSchema: {
28830
- type: "object",
28831
- properties: {
28832
- dashboardId: {
28833
- type: "string",
28834
- description: "Dashboard ID. Omit to use the active dashboard.",
28835
- },
28836
- widgetId: {
28837
- type: "string",
28838
- description: "ID of the widget instance to configure",
28839
- },
28840
- config: {
28841
- type: "object",
28842
- description:
28843
- "Configuration object to merge into existing widget config",
28844
- },
28845
- },
28846
- required: ["widgetId", "config"],
28847
- },
28848
- },
28849
- {
28850
- name: "list_widgets",
28851
- description:
28852
- "List all available widgets from the registry. Returns scoped component names (e.g. 'trops.gong.GongCallSearch') that can be passed directly to add_widget. Each widget includes an 'installed' boolean — if true, use add_widget directly; if false, call install_widget first. Also includes description, provider requirements, and package info.",
28853
- inputSchema: {
28854
- type: "object",
28855
- properties: {},
28856
- required: [],
28857
- },
28858
- },
28859
- {
28860
- name: "search_widgets",
28861
- description:
28862
- "Search the widget registry by keyword. Returns matching widgets with scoped names (e.g. 'trops.slack.SlackChannelFeed') that can be passed directly to add_widget. Each widget includes an 'installed' boolean — if true, use add_widget directly; if false, call install_widget first. Also includes description and provider info.",
28863
- inputSchema: {
28864
- type: "object",
28865
- properties: {
28866
- query: {
28867
- type: "string",
28868
- description:
28869
- "Search keyword to match against widget names, descriptions, and tags",
28870
- },
28871
- },
28872
- required: ["query"],
28873
- },
28874
- },
28875
- {
28876
- name: "install_widget",
28877
- description:
28878
- "Install a widget package from the Dash registry. Requires registry authentication — the user must be signed in via Settings > Account in the Dash app. Use search_widgets first to find available packages, then install by package name (e.g., 'slack', 'gong', 'chat'). After installation, use add_widget to place it on a dashboard.",
28879
- inputSchema: {
28880
- type: "object",
28881
- properties: {
28882
- packageName: {
28883
- type: "string",
28884
- description:
28885
- "Package name from the registry (e.g., 'slack', 'gong', 'chat'). Use the 'package' field from search_widgets results.",
28886
- },
28887
- },
28888
- required: ["packageName"],
28889
- },
28890
- },
28891
- ];
28892
-
28893
- const themeTools$1 = [
28894
- {
28895
- name: "list_themes",
28896
- description:
28897
- "List all saved themes with their names and whether they are currently active. Use this to discover available themes before applying one.",
28898
- inputSchema: {
28899
- type: "object",
28900
- properties: {},
28901
- required: [],
28902
- },
28903
- },
28904
- {
28905
- name: "get_theme",
28906
- description:
28907
- "Get full details of a theme by name, including all color values and shade mappings. Use list_themes first to find theme names.",
28908
- inputSchema: {
28909
- type: "object",
28910
- properties: {
28911
- name: {
28912
- type: "string",
28913
- description: "Name of the theme to retrieve",
28914
- },
28915
- },
28916
- required: ["name"],
28917
- },
28918
- },
28919
- {
28920
- name: "create_theme",
28921
- description:
28922
- "Create a new theme from a colors object. Primary maps to buttons, links, and active states. Secondary maps to backgrounds, cards, and panels. Tertiary maps to accents, badges, and highlights. Example colors: { primary: '#3b82f6', secondary: '#10b981', tertiary: '#f59e0b' }. After creation, use apply_theme to activate it.",
28923
- inputSchema: {
28924
- type: "object",
28925
- properties: {
28926
- name: {
28927
- type: "string",
28928
- description: "Display name for the new theme",
28929
- },
28930
- colors: {
28931
- type: "object",
28932
- description:
28933
- "Theme colors object with role keys mapped to hex values or shade objects",
28934
- },
28935
- },
28936
- required: ["name", "colors"],
28937
- },
28938
- },
28939
- {
28940
- name: "create_theme_from_url",
28941
- description:
28942
- "Extract brand colors from a website URL and generate a matching theme. Loads the page in a hidden browser, extracts colors from meta tags, CSS variables, computed styles, and favicons, then maps them to theme roles. Works best with pages that have visible brand colors. Takes a few seconds to process. After creation, use apply_theme to activate it.",
28943
- inputSchema: {
28944
- type: "object",
28945
- properties: {
28946
- url: {
28947
- type: "string",
28948
- description:
28949
- "Website URL to extract colors from (must start with http:// or https://)",
28950
- },
28951
- name: {
28952
- type: "string",
28953
- description:
28954
- "Optional name for the theme. If omitted, a name is derived from the URL hostname.",
28955
- },
28956
- },
28957
- required: ["url"],
28958
- },
28959
- },
28960
- {
28961
- name: "apply_theme",
28962
- description:
28963
- "Apply a saved theme. Omit `dashboard` to set the app-wide default theme (affects every dashboard that doesn't have its own override). Pass `dashboard` (name or ID) to set that dashboard's theme override instead — useful when the user asks for a theme on a specific dashboard (e.g. 'apply ocean to my Sales dashboard'). The theme must already exist; use list_themes to see available themes or create one with create_theme / create_theme_from_url.",
28964
- inputSchema: {
28965
- type: "object",
28966
- properties: {
28967
- name: {
28968
- type: "string",
28969
- description: "Name of the theme to apply",
28970
- },
28971
- dashboard: {
28972
- type: "string",
28973
- description:
28974
- "Optional dashboard name or numeric ID. Omit for app-wide application.",
28975
- },
28976
- },
28977
- required: ["name"],
28978
- },
28979
- },
28980
- ];
28981
-
28982
- const guideTools$1 = [
28983
- {
28984
- name: "get_setup_guide",
28985
- description:
28986
- "Get a contextual setup guide for Dash. Returns step-by-step instructions for the requested topic. Call this when the user asks how to get started, what they can do, or needs help with a specific workflow.",
28987
- inputSchema: {
28988
- type: "object",
28989
- properties: {
28990
- topic: {
28991
- type: "string",
28992
- enum: ["dashboard", "theme", "provider", "widget", "overview"],
28993
- description:
28994
- "Topic to get help with. Use 'overview' or omit for a general capabilities guide.",
28995
- },
28996
- },
28997
- required: [],
28998
- },
28999
- },
29000
- ];
29001
-
29002
- const providerTools$1 = [
29003
- {
29004
- name: "list_providers",
29005
- description:
29006
- "List all configured providers with their names, types, and status. Credential secrets are never returned. Use this to check which services are already connected.",
29007
- inputSchema: {
29008
- type: "object",
29009
- properties: {},
29010
- required: [],
29011
- },
29012
- },
29013
- {
29014
- name: "add_provider",
29015
- description:
29016
- "Add a new provider configuration. Supports credential providers (API keys) and MCP providers (server connections with tool scoping). Credentials are encrypted at rest. Common types: 'github', 'slack', 'algolia', 'notion', 'openai'. Use list_providers first to check existing connections.",
29017
- inputSchema: {
29018
- type: "object",
29019
- properties: {
29020
- name: {
29021
- type: "string",
29022
- description:
29023
- "Unique display name for the provider (e.g. 'Algolia Production', 'Slack')",
29024
- },
29025
- type: {
29026
- type: "string",
29027
- description:
29028
- "Provider type identifier (e.g. 'algolia', 'slack', 'openai', 'github')",
29029
- },
29030
- providerClass: {
29031
- type: "string",
29032
- enum: ["credential", "mcp"],
29033
- description:
29034
- "Provider class: 'credential' for API key providers, 'mcp' for MCP server providers. Defaults to 'credential'.",
29035
- },
29036
- credentials: {
29037
- type: "object",
29038
- description:
29039
- "Credentials object (e.g. { apiKey: '...', appId: '...' }). Encrypted at rest, never returned in responses.",
29040
- },
29041
- mcpConfig: {
29042
- type: "object",
29043
- description:
29044
- "MCP server configuration (transport, command, args, envMapping). Only used when providerClass is 'mcp'.",
29045
- },
29046
- allowedTools: {
29047
- type: "array",
29048
- items: { type: "string" },
29049
- description:
29050
- "Optional list of allowed MCP tool names. Only used when providerClass is 'mcp'.",
29051
- },
29052
- },
29053
- required: ["name", "type", "credentials"],
29054
- },
29055
- },
29056
- {
29057
- name: "remove_provider",
29058
- description:
29059
- "Remove a provider by name. This deletes the provider and its stored credentials permanently.",
29060
- inputSchema: {
29061
- type: "object",
29062
- properties: {
29063
- name: {
29064
- type: "string",
29065
- description: "Name of the provider to remove",
29066
- },
29067
- },
29068
- required: ["name"],
29069
- },
29070
- },
29071
- ];
29072
-
29073
- const layoutTools$1 = [
29074
- {
29075
- name: "set_layout",
29076
- description:
29077
- "Set or replace the grid layout on a dashboard. Creates a LayoutGridContainer with the specified dimensions. Existing widgets in cells that fit the new grid are preserved; widgets outside the new bounds are orphaned (kept but unassigned). Use this to add a grid to an existing dashboard or to resize the grid.",
29078
- inputSchema: {
29079
- type: "object",
29080
- properties: {
29081
- dashboardId: {
29082
- type: "string",
29083
- description: "Dashboard ID. Omit to use the active dashboard.",
29084
- },
29085
- rows: {
29086
- type: "number",
29087
- description: "Number of rows (1-10)",
29088
- },
29089
- cols: {
29090
- type: "number",
29091
- description: "Number of columns (1-10)",
29092
- },
29093
- gap: {
29094
- type: "string",
29095
- description:
29096
- "Tailwind gap class (e.g. 'gap-2', 'gap-4'). Defaults to 'gap-2'.",
29097
- },
29098
- colModes: {
29099
- type: "object",
29100
- description:
29101
- "Per-row column sizing. Keys are row numbers (as strings), values are mode strings: 'equal', '1/4', '1/3', '1/2', '2/3'.",
29102
- },
29103
- },
29104
- required: ["rows", "cols"],
29105
- },
29106
- },
29107
- {
29108
- name: "update_layout",
29109
- description:
29110
- "Partially update the grid layout. Only specified properties change — omitted properties keep their current values. colModes is merged (not replaced). Widgets in removed rows/columns are orphaned. Dashboard must already have a grid layout.",
29111
- inputSchema: {
29112
- type: "object",
29113
- properties: {
29114
- dashboardId: {
29115
- type: "string",
29116
- description: "Dashboard ID. Omit to use the active dashboard.",
29117
- },
29118
- rows: {
29119
- type: "number",
29120
- description: "New number of rows (1-10). Omit to keep current.",
29121
- },
29122
- cols: {
29123
- type: "number",
29124
- description: "New number of columns (1-10). Omit to keep current.",
29125
- },
29126
- gap: {
29127
- type: "string",
29128
- description: "Tailwind gap class. Omit to keep current.",
29129
- },
29130
- colModes: {
29131
- type: "object",
29132
- description:
29133
- "Column sizing modes to merge. Set a key to null to reset that row to default.",
29134
- },
29135
- },
29136
- required: [],
29137
- },
29138
- },
29139
- {
29140
- name: "move_widget",
29141
- description:
29142
- "Move a widget to a different grid cell. If the target cell is occupied, the two widgets are swapped. The widget must already be placed in a grid cell. Use get_dashboard to find widget IDs and current positions.",
29143
- inputSchema: {
29144
- type: "object",
29145
- properties: {
29146
- dashboardId: {
29147
- type: "string",
29148
- description: "Dashboard ID. Omit to use the active dashboard.",
29149
- },
29150
- widgetId: {
29151
- type: "string",
29152
- description: "ID of the widget to move",
29153
- },
29154
- row: {
29155
- type: "number",
29156
- description: "Target row (1-indexed)",
29157
- },
29158
- col: {
29159
- type: "number",
29160
- description: "Target column (1-indexed)",
29161
- },
29162
- },
29163
- required: ["widgetId", "row", "col"],
29164
- },
29165
- },
29166
- ];
29167
-
29168
- var toolDefinitions$1 = {
29169
- dashboardTools: dashboardTools$1,
29170
- widgetTools: widgetTools$1,
29171
- themeTools: themeTools$1,
29172
- providerTools: providerTools$1,
29173
- guideTools: guideTools$1,
29174
- layoutTools: layoutTools$1,
29175
- };
29176
-
29177
28156
  var mcp = {};
29178
28157
 
29179
28158
  var server$1 = {};
@@ -49304,533 +48283,1635 @@ function jsonSchemaToZod$1(schema) {
49304
48283
  if (!required.includes(key)) {
49305
48284
  fieldSchema = fieldSchema.optional();
49306
48285
  }
49307
- shape[key] = fieldSchema;
49308
- }
48286
+ shape[key] = fieldSchema;
48287
+ }
48288
+
48289
+ return z$1.object(shape);
48290
+ }
48291
+
48292
+ var jsonSchemaToZod_1 = { jsonSchemaToZod: jsonSchemaToZod$1, jsonSchemaPropertyToZod };
48293
+
48294
+ /**
48295
+ * mcpDashServerController.js
48296
+ *
48297
+ * Manages the hosted MCP server that exposes Dash capabilities to external
48298
+ * LLM clients (Claude Desktop, ChatGPT, etc.) via Streamable HTTP transport.
48299
+ *
48300
+ * This is the MCP *server* — distinct from mcpController.js which is the
48301
+ * MCP *client* that connects to external tool servers for widgets.
48302
+ *
48303
+ * Architecture:
48304
+ * - Node https server bound to 127.0.0.1 (localhost only)
48305
+ * - Auto-generated self-signed TLS certificate for localhost
48306
+ * - StreamableHTTPServerTransport from @modelcontextprotocol/sdk
48307
+ * - McpServer registers tools and resources
48308
+ * - Bearer token authentication on all requests
48309
+ * - Rate limiting via token bucket (60 req/min)
48310
+ */
48311
+
48312
+ const https$2 = require$$8$1;
48313
+ const { randomUUID } = require$$1$5;
48314
+ const { BrowserWindow } = require$$0$1;
48315
+ const { McpServer } = mcp;
48316
+ const {
48317
+ StreamableHTTPServerTransport,
48318
+ } = streamableHttp;
48319
+
48320
+ const settingsController$3 = settingsController_1;
48321
+ const { getOrCreateCert } = tlsCert;
48322
+
48323
+ // Tool-name prefixes that indicate a mutation. After a successful call
48324
+ // to any of these, the renderer is notified via "dash-mcp:state-changed"
48325
+ // so it can refresh the relevant UI slice (themes, dashboards, widgets,
48326
+ // providers, etc.) without requiring a manual reload.
48327
+ const MUTATING_PREFIXES = [
48328
+ "create_",
48329
+ "add_",
48330
+ "remove_",
48331
+ "delete_",
48332
+ "update_",
48333
+ "apply_",
48334
+ "install_",
48335
+ "move_",
48336
+ "set_",
48337
+ "configure_",
48338
+ ];
48339
+
48340
+ function isMutatingTool(name) {
48341
+ return MUTATING_PREFIXES.some((p) => name.startsWith(p));
48342
+ }
48343
+
48344
+ function broadcastStateChanged(toolName, result) {
48345
+ // Best-effort parse of the tool's first text content block. MCP tool
48346
+ // results are of shape { content: [{ type: "text", text: "<json>" }] }.
48347
+ // Expose the parsed JSON as `result` so renderers can act on specifics
48348
+ // (e.g. the new dashboard ID from create_dashboard) without a round
48349
+ // trip back to fetch state.
48350
+ let parsed = null;
48351
+ try {
48352
+ const firstText = result?.content?.find?.((c) => c.type === "text")?.text;
48353
+ if (firstText) parsed = JSON.parse(firstText);
48354
+ } catch {
48355
+ /* leave null */
48356
+ }
48357
+ const payload = { toolName, result: parsed };
48358
+ for (const win of BrowserWindow.getAllWindows()) {
48359
+ if (!win.isDestroyed()) {
48360
+ try {
48361
+ win.webContents.send("dash-mcp:state-changed", payload);
48362
+ } catch {
48363
+ /* ignore */
48364
+ }
48365
+ }
48366
+ }
48367
+ }
48368
+
48369
+ // --- State ---
48370
+ let mcpServer = null;
48371
+ let httpsServer = null;
48372
+ let transport = null;
48373
+ let startTime = null;
48374
+ let connectionCount = 0;
48375
+ let activeWin = null;
48376
+
48377
+ // --- Rate Limiting ---
48378
+ const RATE_LIMIT = 60; // requests per minute
48379
+ const RATE_WINDOW = 60 * 1000; // 1 minute in ms
48380
+ const rateBuckets$1 = new Map(); // ip -> { count, resetAt }
48381
+
48382
+ function isRateLimited$1(ip) {
48383
+ const now = Date.now();
48384
+ let bucket = rateBuckets$1.get(ip);
48385
+ if (!bucket || now > bucket.resetAt) {
48386
+ bucket = { count: 0, resetAt: now + RATE_WINDOW };
48387
+ rateBuckets$1.set(ip, bucket);
48388
+ }
48389
+ bucket.count++;
48390
+ return bucket.count > RATE_LIMIT;
48391
+ }
48392
+
48393
+ // Clean up stale buckets periodically
48394
+ let cleanupInterval = null;
48395
+ function startCleanup() {
48396
+ if (cleanupInterval) return;
48397
+ cleanupInterval = setInterval(() => {
48398
+ const now = Date.now();
48399
+ for (const [ip, bucket] of rateBuckets$1) {
48400
+ if (now > bucket.resetAt) rateBuckets$1.delete(ip);
48401
+ }
48402
+ }, RATE_WINDOW);
48403
+ }
48404
+ function stopCleanup() {
48405
+ if (cleanupInterval) {
48406
+ clearInterval(cleanupInterval);
48407
+ cleanupInterval = null;
48408
+ }
48409
+ rateBuckets$1.clear();
48410
+ }
48411
+
48412
+ // --- Tool, Resource & Prompt Registration ---
48413
+ // These are populated by other modules (DASH-78, DASH-79, etc.)
48414
+ // Each entry: { name, description, inputSchema, handler }
48415
+ const registeredTools = [];
48416
+ const registeredResources = [];
48417
+ // Each entry: { name, description, args, handler }
48418
+ const registeredPrompts = [];
48419
+
48420
+ /**
48421
+ * Register a tool to be exposed via the MCP server.
48422
+ * Call this before starting the server (or restart after registering).
48423
+ */
48424
+ function registerTool$6(toolDef) {
48425
+ registeredTools.push(toolDef);
48426
+ }
48427
+
48428
+ /**
48429
+ * Register a resource to be exposed via the MCP server.
48430
+ */
48431
+ function registerResource$1(resourceDef) {
48432
+ registeredResources.push(resourceDef);
48433
+ }
48434
+
48435
+ /**
48436
+ * Register a prompt to be exposed via the MCP server.
48437
+ * Prompts are guided entry points that LLM clients display as suggested actions.
48438
+ */
48439
+ function registerPrompt$1(promptDef) {
48440
+ registeredPrompts.push(promptDef);
48441
+ }
48442
+
48443
+ const z = zod;
48444
+ const { jsonSchemaToZod } = jsonSchemaToZod_1;
48445
+
48446
+ /**
48447
+ * Apply all registered tools, resources, and prompts to the McpServer instance.
48448
+ */
48449
+ function applyRegistrations(server) {
48450
+ for (const tool of registeredTools) {
48451
+ const zodSchema = jsonSchemaToZod(tool.inputSchema);
48452
+ // Wrap mutating tool handlers so a successful invocation broadcasts
48453
+ // "dash-mcp:state-changed" to all renderer windows. Read-only tools
48454
+ // (list_, get_, search_) are passed through unwrapped.
48455
+ const mutating = isMutatingTool(tool.name);
48456
+ const handler = mutating
48457
+ ? async (...args) => {
48458
+ const result = await tool.handler(...args);
48459
+ if (result && !result.isError) {
48460
+ broadcastStateChanged(tool.name, result);
48461
+ }
48462
+ return result;
48463
+ }
48464
+ : tool.handler;
48465
+ // server.tool() expects a raw Zod shape (e.g. { name: z.string() }),
48466
+ // NOT a z.object() wrapper. Extract .shape from the Zod object.
48467
+ server.tool(tool.name, tool.description, zodSchema.shape || {}, handler);
48468
+ }
48469
+ for (const resource of registeredResources) {
48470
+ server.resource(
48471
+ resource.name,
48472
+ resource.uri,
48473
+ resource.metadata || {},
48474
+ resource.handler,
48475
+ );
48476
+ }
48477
+ for (const prompt of registeredPrompts) {
48478
+ if (prompt.args && Object.keys(prompt.args).length > 0) {
48479
+ // Prompt with arguments — use the 4-arg overload
48480
+ // Build a Zod-compatible arg schema from our plain arg definitions
48481
+ const shape = {};
48482
+ for (const [key, def] of Object.entries(prompt.args)) {
48483
+ shape[key] = def.required
48484
+ ? z.string().describe(def.description)
48485
+ : z.string().optional().describe(def.description);
48486
+ }
48487
+ server.prompt(prompt.name, prompt.description, shape, prompt.handler);
48488
+ } else {
48489
+ // Prompt with no arguments — use the 2-arg overload
48490
+ server.prompt(prompt.name, prompt.description, prompt.handler);
48491
+ }
48492
+ }
48493
+ }
48494
+
48495
+ // --- Settings Helpers ---
48496
+ function getMcpServerSettings(win) {
48497
+ const result = settingsController$3.getSettingsForApplication(win);
48498
+ const settings = result?.settings || {};
48499
+ return settings.mcpDashServer || {};
48500
+ }
48501
+
48502
+ function saveMcpServerSettings(win, mcpSettings) {
48503
+ const result = settingsController$3.getSettingsForApplication(win);
48504
+ const settings = result?.settings || {};
48505
+ settings.mcpDashServer = mcpSettings;
48506
+ settingsController$3.saveSettingsForApplication(win, settings);
48507
+ }
48508
+
48509
+ // --- App ID Resolution ---
48510
+ /**
48511
+ * Resolve the appId by scanning the userData/Dashboard directory for
48512
+ * subdirectories containing workspaces.json. Falls back to the default.
48513
+ */
48514
+ function resolveAppId() {
48515
+ const { app } = require$$0$1;
48516
+ const fs = require$$0$2;
48517
+ const path = require$$1$2;
48518
+ const dashboardDir = path.join(app.getPath("userData"), "Dashboard");
48519
+ try {
48520
+ const entries = fs.readdirSync(dashboardDir, { withFileTypes: true });
48521
+ for (const entry of entries) {
48522
+ if (entry.isDirectory()) {
48523
+ const wsFile = path.join(dashboardDir, entry.name, "workspaces.json");
48524
+ if (fs.existsSync(wsFile)) {
48525
+ return entry.name;
48526
+ }
48527
+ }
48528
+ }
48529
+ } catch (e) {
48530
+ // Directory may not exist yet
48531
+ }
48532
+ return "@trops/dash-electron";
48533
+ }
48534
+
48535
+ /**
48536
+ * Get the current server context (win + appId) for tool handlers.
48537
+ * Returns null if the server is not running.
48538
+ */
48539
+ function getServerContext() {
48540
+ if (!activeWin) return null;
48541
+ return { win: activeWin, appId: resolveAppId() };
48542
+ }
48543
+
48544
+ // --- Controller ---
48545
+ const mcpDashServerController$4 = {
48546
+ /**
48547
+ * Start the MCP Dash server.
48548
+ * @param {BrowserWindow} win
48549
+ * @param {Object} options - { port?: number }
48550
+ */
48551
+ startServer: async (win, options = {}) => {
48552
+ if (httpsServer) {
48553
+ return {
48554
+ success: false,
48555
+ error: "Server is already running",
48556
+ };
48557
+ }
48558
+
48559
+ try {
48560
+ const serverSettings = getMcpServerSettings(win);
48561
+ const port = options.port || serverSettings.port || 3141;
48562
+ const token =
48563
+ serverSettings.token || mcpDashServerController$4.getOrCreateToken(win);
48564
+
48565
+ // Create McpServer
48566
+ mcpServer = new McpServer({
48567
+ name: "dash-electron",
48568
+ version: "1.0.0",
48569
+ });
48570
+
48571
+ // Generate or load TLS certificate
48572
+ const { app } = require("electron");
48573
+ const path = require("path");
48574
+ const certsDir = path.join(app.getPath("userData"), "certs");
48575
+ const tlsCert = getOrCreateCert(certsDir);
48576
+
48577
+ // Apply registered tools and resources
48578
+ applyRegistrations(mcpServer);
48579
+
48580
+ // Create HTTPS server with auth and rate limiting
48581
+ httpsServer = https$2.createServer(
48582
+ { key: tlsCert.key, cert: tlsCert.cert },
48583
+ async (req, res) => {
48584
+ const ip = req.socket.remoteAddress || req.connection.remoteAddress;
48585
+
48586
+ // Rate limiting
48587
+ if (isRateLimited$1(ip)) {
48588
+ res.writeHead(429, { "Content-Type": "application/json" });
48589
+ res.end(JSON.stringify({ error: "Rate limit exceeded" }));
48590
+ return;
48591
+ }
48592
+
48593
+ // Bearer token auth
48594
+ const authHeader = req.headers.authorization;
48595
+ if (!authHeader || authHeader !== `Bearer ${token}`) {
48596
+ res.writeHead(401, { "Content-Type": "application/json" });
48597
+ res.end(JSON.stringify({ error: "Unauthorized" }));
48598
+ return;
48599
+ }
48600
+
48601
+ // Handle MCP requests on /mcp path
48602
+ if (req.url === "/mcp" || req.url?.startsWith("/mcp")) {
48603
+ try {
48604
+ // Stateless mode: create a fresh server + transport per request
48605
+ const reqServer = new McpServer({
48606
+ name: "dash-electron",
48607
+ version: "1.0.0",
48608
+ });
48609
+ applyRegistrations(reqServer);
48610
+ const reqTransport = new StreamableHTTPServerTransport({
48611
+ sessionIdGenerator: undefined,
48612
+ });
48613
+ await reqServer.connect(reqTransport);
48614
+ connectionCount++;
48615
+ await reqTransport.handleRequest(req, res);
48616
+ } catch (err) {
48617
+ console.error("[mcpDashServer] Error handling MCP request:", err);
48618
+ if (!res.headersSent) {
48619
+ res.writeHead(500, {
48620
+ "Content-Type": "application/json",
48621
+ });
48622
+ res.end(
48623
+ JSON.stringify({
48624
+ error: "Internal server error",
48625
+ }),
48626
+ );
48627
+ }
48628
+ }
48629
+ } else {
48630
+ // Health check endpoint
48631
+ if (req.url === "/health" && req.method === "GET") {
48632
+ res.writeHead(200, {
48633
+ "Content-Type": "application/json",
48634
+ });
48635
+ res.end(
48636
+ JSON.stringify({
48637
+ status: "ok",
48638
+ server: "dash-electron-mcp",
48639
+ version: "1.0.0",
48640
+ }),
48641
+ );
48642
+ return;
48643
+ }
48644
+ res.writeHead(404, { "Content-Type": "application/json" });
48645
+ res.end(JSON.stringify({ error: "Not found" }));
48646
+ }
48647
+ },
48648
+ );
48649
+
48650
+ // Bind to localhost only
48651
+ await new Promise((resolve, reject) => {
48652
+ httpsServer.on("error", (err) => {
48653
+ httpsServer = null;
48654
+ mcpServer = null;
48655
+ if (err.code === "EADDRINUSE") {
48656
+ reject(
48657
+ new Error(
48658
+ `Port ${port} is already in use. Choose a different port in Settings.`,
48659
+ ),
48660
+ );
48661
+ } else {
48662
+ reject(err);
48663
+ }
48664
+ });
48665
+ httpsServer.listen(port, "127.0.0.1", () => {
48666
+ resolve();
48667
+ });
48668
+ });
48669
+
48670
+ startTime = Date.now();
48671
+ connectionCount = 0;
48672
+ activeWin = win;
48673
+ startCleanup();
48674
+
48675
+ // Save enabled state
48676
+ saveMcpServerSettings(win, {
48677
+ ...serverSettings,
48678
+ enabled: true,
48679
+ port,
48680
+ token,
48681
+ });
48682
+
48683
+ console.log(
48684
+ `[mcpDashServer] Server started on https://127.0.0.1:${port}/mcp`,
48685
+ );
48686
+
48687
+ return {
48688
+ success: true,
48689
+ port,
48690
+ url: `https://127.0.0.1:${port}/mcp`,
48691
+ };
48692
+ } catch (err) {
48693
+ console.error("[mcpDashServer] Failed to start server:", err);
48694
+ httpsServer = null;
48695
+ mcpServer = null;
48696
+ return {
48697
+ success: false,
48698
+ error: err.message,
48699
+ };
48700
+ }
48701
+ },
48702
+
48703
+ /**
48704
+ * Stop the MCP Dash server.
48705
+ */
48706
+ stopServer: async (win) => {
48707
+ if (!httpsServer) {
48708
+ return { success: true, message: "Server was not running" };
48709
+ }
48710
+
48711
+ try {
48712
+ stopCleanup();
48713
+
48714
+ await new Promise((resolve) => {
48715
+ httpsServer.close(() => resolve());
48716
+ // Force close after 5 seconds
48717
+ setTimeout(() => resolve(), 5000);
48718
+ });
48719
+
48720
+ if (mcpServer) {
48721
+ try {
48722
+ await mcpServer.close();
48723
+ } catch (e) {
48724
+ // Ignore close errors
48725
+ }
48726
+ }
48727
+
48728
+ httpsServer = null;
48729
+ mcpServer = null;
48730
+ transport = null;
48731
+ startTime = null;
48732
+ connectionCount = 0;
48733
+ activeWin = null;
48734
+
48735
+ // Update settings
48736
+ if (win) {
48737
+ const serverSettings = getMcpServerSettings(win);
48738
+ saveMcpServerSettings(win, {
48739
+ ...serverSettings,
48740
+ enabled: false,
48741
+ });
48742
+ }
48743
+
48744
+ console.log("[mcpDashServer] Server stopped");
48745
+ return { success: true };
48746
+ } catch (err) {
48747
+ console.error("[mcpDashServer] Error stopping server:", err);
48748
+ return { success: false, error: err.message };
48749
+ }
48750
+ },
48751
+
48752
+ /**
48753
+ * Restart the server (stop + start).
48754
+ */
48755
+ restartServer: async (win, options = {}) => {
48756
+ await mcpDashServerController$4.stopServer(win);
48757
+ return mcpDashServerController$4.startServer(win, options);
48758
+ },
48759
+
48760
+ /**
48761
+ * Get server status.
48762
+ */
48763
+ getStatus: (win) => {
48764
+ const serverSettings = getMcpServerSettings(win);
48765
+ return {
48766
+ running: !!httpsServer,
48767
+ enabled: serverSettings.enabled || false,
48768
+ port: serverSettings.port || 3141,
48769
+ connectionCount,
48770
+ uptime: startTime ? Math.floor((Date.now() - startTime) / 1000) : 0,
48771
+ toolCount: registeredTools.length,
48772
+ resourceCount: registeredResources.length,
48773
+ };
48774
+ },
48775
+
48776
+ /**
48777
+ * Get or create the bearer token.
48778
+ */
48779
+ getOrCreateToken: (win) => {
48780
+ const serverSettings = getMcpServerSettings(win);
48781
+ if (serverSettings.token) {
48782
+ return serverSettings.token;
48783
+ }
48784
+ const token = randomUUID();
48785
+ saveMcpServerSettings(win, { ...serverSettings, token });
48786
+ return token;
48787
+ },
48788
+
48789
+ /**
48790
+ * Auto-start server if enabled in settings.
48791
+ * Called from dash-electron on app ready.
48792
+ */
48793
+ autoStart: async (win) => {
48794
+ const serverSettings = getMcpServerSettings(win);
48795
+ if (serverSettings.enabled) {
48796
+ console.log("[mcpDashServer] Auto-starting server...");
48797
+ return mcpDashServerController$4.startServer(win, {
48798
+ port: serverSettings.port,
48799
+ });
48800
+ }
48801
+ return { success: false, message: "Server not enabled" };
48802
+ },
49309
48803
 
49310
- return z$1.object(shape);
49311
- }
48804
+ // Expose registration functions for other controllers
48805
+ registerTool: registerTool$6,
48806
+ registerResource: registerResource$1,
48807
+ registerPrompt: registerPrompt$1,
48808
+ getServerContext,
48809
+ };
49312
48810
 
49313
- var jsonSchemaToZod_1 = { jsonSchemaToZod: jsonSchemaToZod$1, jsonSchemaPropertyToZod };
48811
+ var mcpDashServerController_1 = mcpDashServerController$4;
49314
48812
 
49315
48813
  /**
49316
- * mcpDashServerController.js
49317
- *
49318
- * Manages the hosted MCP server that exposes Dash capabilities to external
49319
- * LLM clients (Claude Desktop, ChatGPT, etc.) via Streamable HTTP transport.
48814
+ * cliController.js
49320
48815
  *
49321
- * This is the MCP *server* distinct from mcpController.js which is the
49322
- * MCP *client* that connects to external tool servers for widgets.
48816
+ * Manages Claude Code CLI (`claude -p`) as an alternative LLM backend.
48817
+ * Spawns the CLI subprocess, parses stream-json NDJSON output, and emits
48818
+ * the same LLM_STREAM_* events as the Anthropic SDK path.
49323
48819
  *
49324
- * Architecture:
49325
- * - Node https server bound to 127.0.0.1 (localhost only)
49326
- * - Auto-generated self-signed TLS certificate for localhost
49327
- * - StreamableHTTPServerTransport from @modelcontextprotocol/sdk
49328
- * - McpServer registers tools and resources
49329
- * - Bearer token authentication on all requests
49330
- * - Rate limiting via token bucket (60 req/min)
48820
+ * Users with a Claude Pro/Max subscription and Claude Code installed
48821
+ * can use the Chat widget without a separate API key.
49331
48822
  */
49332
48823
 
49333
- const https$2 = require$$8$1;
49334
- const { randomUUID } = require$$1$5;
49335
- const { BrowserWindow } = require$$0$1;
49336
- const { McpServer } = mcp;
48824
+ const { spawn, execSync } = require$$7;
49337
48825
  const {
49338
- StreamableHTTPServerTransport,
49339
- } = streamableHttp;
48826
+ LLM_STREAM_DELTA: LLM_STREAM_DELTA$2,
48827
+ LLM_STREAM_TOOL_CALL: LLM_STREAM_TOOL_CALL$2,
48828
+ LLM_STREAM_TOOL_RESULT: LLM_STREAM_TOOL_RESULT$2,
48829
+ LLM_STREAM_COMPLETE: LLM_STREAM_COMPLETE$2,
48830
+ LLM_STREAM_ERROR: LLM_STREAM_ERROR$2,
48831
+ } = llmEvents$1;
49340
48832
 
49341
- const settingsController$3 = settingsController_1;
49342
- const { getOrCreateCert } = tlsCert;
48833
+ const IS_WINDOWS = process.platform === "win32";
49343
48834
 
49344
- // Tool-name prefixes that indicate a mutation. After a successful call
49345
- // to any of these, the renderer is notified via "dash-mcp:state-changed"
49346
- // so it can refresh the relevant UI slice (themes, dashboards, widgets,
49347
- // providers, etc.) without requiring a manual reload.
49348
- const MUTATING_PREFIXES = [
49349
- "create_",
49350
- "add_",
49351
- "remove_",
49352
- "delete_",
49353
- "update_",
49354
- "apply_",
49355
- "install_",
49356
- "move_",
49357
- "set_",
49358
- "configure_",
49359
- ];
48835
+ /**
48836
+ * Cached shell PATH result (resolved once, reused for all spawns).
48837
+ * Same pattern as mcpController.js.
48838
+ */
48839
+ let _shellPath = null;
49360
48840
 
49361
- function isMutatingTool(name) {
49362
- return MUTATING_PREFIXES.some((p) => name.startsWith(p));
49363
- }
48841
+ function getShellPath() {
48842
+ if (_shellPath !== null) return _shellPath;
48843
+
48844
+ // Windows: no POSIX login-shell trick — just use the inherited PATH,
48845
+ // which is typically correct for GUI-launched Electron apps.
48846
+ if (IS_WINDOWS) {
48847
+ _shellPath = process.env.PATH || "";
48848
+ return _shellPath;
48849
+ }
49364
48850
 
49365
- function broadcastStateChanged(toolName, result) {
49366
- // Best-effort parse of the tool's first text content block. MCP tool
49367
- // results are of shape { content: [{ type: "text", text: "<json>" }] }.
49368
- // Expose the parsed JSON as `result` so renderers can act on specifics
49369
- // (e.g. the new dashboard ID from create_dashboard) without a round
49370
- // trip back to fetch state.
49371
- let parsed = null;
49372
48851
  try {
49373
- const firstText = result?.content?.find?.((c) => c.type === "text")?.text;
49374
- if (firstText) parsed = JSON.parse(firstText);
48852
+ const shell = process.env.SHELL || "/bin/bash";
48853
+ _shellPath = execSync(`${shell} -ilc 'echo -n "$PATH"'`, {
48854
+ encoding: "utf8",
48855
+ timeout: 5000,
48856
+ });
49375
48857
  } catch {
49376
- /* leave null */
49377
- }
49378
- const payload = { toolName, result: parsed };
49379
- for (const win of BrowserWindow.getAllWindows()) {
49380
- if (!win.isDestroyed()) {
49381
- try {
49382
- win.webContents.send("dash-mcp:state-changed", payload);
49383
- } catch {
49384
- /* ignore */
49385
- }
49386
- }
48858
+ _shellPath = process.env.PATH || "";
49387
48859
  }
48860
+
48861
+ return _shellPath;
49388
48862
  }
49389
48863
 
49390
- // --- State ---
49391
- let mcpServer = null;
49392
- let httpsServer = null;
49393
- let transport = null;
49394
- let startTime = null;
49395
- let connectionCount = 0;
49396
- let activeWin = null;
48864
+ /**
48865
+ * Cached CLI binary path (resolved once via `which` / `where`).
48866
+ */
48867
+ let _cliBinaryPath = undefined; // undefined = not yet checked
49397
48868
 
49398
- // --- Rate Limiting ---
49399
- const RATE_LIMIT = 60; // requests per minute
49400
- const RATE_WINDOW = 60 * 1000; // 1 minute in ms
49401
- const rateBuckets$1 = new Map(); // ip -> { count, resetAt }
48869
+ function resolveCliBinary() {
48870
+ if (_cliBinaryPath !== undefined) return _cliBinaryPath;
49402
48871
 
49403
- function isRateLimited$1(ip) {
49404
- const now = Date.now();
49405
- let bucket = rateBuckets$1.get(ip);
49406
- if (!bucket || now > bucket.resetAt) {
49407
- bucket = { count: 0, resetAt: now + RATE_WINDOW };
49408
- rateBuckets$1.set(ip, bucket);
48872
+ try {
48873
+ const fullPath = getShellPath();
48874
+ // `where` on Windows, `which` everywhere else. `where` may list
48875
+ // multiple matches on separate lines (e.g. claude.cmd + claude.ps1)
48876
+ // take the first hit.
48877
+ const lookup = IS_WINDOWS ? "where claude" : "which claude";
48878
+ const result = execSync(lookup, {
48879
+ encoding: "utf8",
48880
+ timeout: 5000,
48881
+ env: { ...process.env, PATH: fullPath },
48882
+ });
48883
+ _cliBinaryPath = IS_WINDOWS
48884
+ ? result
48885
+ .split(/\r?\n/)
48886
+ .find((l) => l.trim())
48887
+ ?.trim() || null
48888
+ : result.trim();
48889
+ } catch {
48890
+ _cliBinaryPath = null;
49409
48891
  }
49410
- bucket.count++;
49411
- return bucket.count > RATE_LIMIT;
49412
- }
49413
48892
 
49414
- // Clean up stale buckets periodically
49415
- let cleanupInterval = null;
49416
- function startCleanup() {
49417
- if (cleanupInterval) return;
49418
- cleanupInterval = setInterval(() => {
49419
- const now = Date.now();
49420
- for (const [ip, bucket] of rateBuckets$1) {
49421
- if (now > bucket.resetAt) rateBuckets$1.delete(ip);
49422
- }
49423
- }, RATE_WINDOW);
49424
- }
49425
- function stopCleanup() {
49426
- if (cleanupInterval) {
49427
- clearInterval(cleanupInterval);
49428
- cleanupInterval = null;
49429
- }
49430
- rateBuckets$1.clear();
48893
+ return _cliBinaryPath;
49431
48894
  }
49432
48895
 
49433
- // --- Tool, Resource & Prompt Registration ---
49434
- // These are populated by other modules (DASH-78, DASH-79, etc.)
49435
- // Each entry: { name, description, inputSchema, handler }
49436
- const registeredTools = [];
49437
- const registeredResources = [];
49438
- // Each entry: { name, description, args, handler }
49439
- const registeredPrompts = [];
49440
-
49441
48896
  /**
49442
- * Register a tool to be exposed via the MCP server.
49443
- * Call this before starting the server (or restart after registering).
48897
+ * Active CLI processes for abort support.
48898
+ * Map<requestId, ChildProcess>
49444
48899
  */
49445
- function registerTool$6(toolDef) {
49446
- registeredTools.push(toolDef);
49447
- }
48900
+ const activeProcesses = new Map();
49448
48901
 
49449
48902
  /**
49450
- * Register a resource to be exposed via the MCP server.
48903
+ * Kill a child process and its descendants. On Windows, spawning with
48904
+ * shell:true (needed for .cmd targets) means child.kill() only
48905
+ * terminates the cmd.exe — the real CLI keeps running. Use taskkill
48906
+ * with /T (tree) /F (force) to clean up.
49451
48907
  */
49452
- function registerResource$1(resourceDef) {
49453
- registeredResources.push(resourceDef);
48908
+ function killChildTree(child) {
48909
+ if (!child || child.killed || typeof child.pid !== "number") return;
48910
+ if (IS_WINDOWS) {
48911
+ try {
48912
+ execSync(`taskkill /pid ${child.pid} /T /F`, {
48913
+ stdio: "ignore",
48914
+ timeout: 5000,
48915
+ });
48916
+ } catch {
48917
+ // Fall back to plain kill — best-effort
48918
+ try {
48919
+ child.kill();
48920
+ } catch {
48921
+ /* ignore */
48922
+ }
48923
+ }
48924
+ } else {
48925
+ child.kill("SIGTERM");
48926
+ }
49454
48927
  }
49455
48928
 
49456
48929
  /**
49457
- * Register a prompt to be exposed via the MCP server.
49458
- * Prompts are guided entry points that LLM clients display as suggested actions.
48930
+ * Session IDs for conversation continuity.
48931
+ * Map<widgetUuid, sessionId>
49459
48932
  */
49460
- function registerPrompt$1(promptDef) {
49461
- registeredPrompts.push(promptDef);
49462
- }
49463
-
49464
- const z = zod;
49465
- const { jsonSchemaToZod } = jsonSchemaToZod_1;
48933
+ const sessions = new Map();
49466
48934
 
49467
48935
  /**
49468
- * Apply all registered tools, resources, and prompts to the McpServer instance.
48936
+ * Send events safely to a window.
49469
48937
  */
49470
- function applyRegistrations(server) {
49471
- for (const tool of registeredTools) {
49472
- const zodSchema = jsonSchemaToZod(tool.inputSchema);
49473
- // Wrap mutating tool handlers so a successful invocation broadcasts
49474
- // "dash-mcp:state-changed" to all renderer windows. Read-only tools
49475
- // (list_, get_, search_) are passed through unwrapped.
49476
- const mutating = isMutatingTool(tool.name);
49477
- const handler = mutating
49478
- ? async (...args) => {
49479
- const result = await tool.handler(...args);
49480
- if (result && !result.isError) {
49481
- broadcastStateChanged(tool.name, result);
49482
- }
49483
- return result;
49484
- }
49485
- : tool.handler;
49486
- // server.tool() expects a raw Zod shape (e.g. { name: z.string() }),
49487
- // NOT a z.object() wrapper. Extract .shape from the Zod object.
49488
- server.tool(tool.name, tool.description, zodSchema.shape || {}, handler);
49489
- }
49490
- for (const resource of registeredResources) {
49491
- server.resource(
49492
- resource.name,
49493
- resource.uri,
49494
- resource.metadata || {},
49495
- resource.handler,
49496
- );
49497
- }
49498
- for (const prompt of registeredPrompts) {
49499
- if (prompt.args && Object.keys(prompt.args).length > 0) {
49500
- // Prompt with arguments — use the 4-arg overload
49501
- // Build a Zod-compatible arg schema from our plain arg definitions
49502
- const shape = {};
49503
- for (const [key, def] of Object.entries(prompt.args)) {
49504
- shape[key] = def.required
49505
- ? z.string().describe(def.description)
49506
- : z.string().optional().describe(def.description);
49507
- }
49508
- server.prompt(prompt.name, prompt.description, shape, prompt.handler);
49509
- } else {
49510
- // Prompt with no arguments — use the 2-arg overload
49511
- server.prompt(prompt.name, prompt.description, prompt.handler);
49512
- }
48938
+ function safeSend(win, channel, data) {
48939
+ if (win && !win.isDestroyed()) {
48940
+ win.webContents.send(channel, data);
49513
48941
  }
49514
48942
  }
49515
48943
 
49516
- // --- Settings Helpers ---
49517
- function getMcpServerSettings(win) {
49518
- const result = settingsController$3.getSettingsForApplication(win);
49519
- const settings = result?.settings || {};
49520
- return settings.mcpDashServer || {};
49521
- }
48944
+ const cliController$2 = {
48945
+ /**
48946
+ * isAvailable
48947
+ * Check if the Claude Code CLI is installed and accessible.
48948
+ *
48949
+ * @returns {{ available: boolean, path?: string }}
48950
+ */
48951
+ isAvailable: () => {
48952
+ const binaryPath = resolveCliBinary();
48953
+ if (binaryPath) {
48954
+ return { available: true, path: binaryPath };
48955
+ }
48956
+ return { available: false };
48957
+ },
49522
48958
 
49523
- function saveMcpServerSettings(win, mcpSettings) {
49524
- const result = settingsController$3.getSettingsForApplication(win);
49525
- const settings = result?.settings || {};
49526
- settings.mcpDashServer = mcpSettings;
49527
- settingsController$3.saveSettingsForApplication(win, settings);
49528
- }
48959
+ /**
48960
+ * sendMessage
48961
+ * Stream a response from the Claude Code CLI with NDJSON parsing.
48962
+ *
48963
+ * @param {BrowserWindow} win - the window to send stream events to
48964
+ * @param {string} requestId - unique ID for this request
48965
+ * @param {object} params - { model, messages, systemPrompt, maxToolRounds, widgetUuid }
48966
+ */
48967
+ sendMessage: async (win, requestId, params) => {
48968
+ const { model, messages, systemPrompt, widgetUuid, cwd } = params;
49529
48969
 
49530
- // --- App ID Resolution ---
49531
- /**
49532
- * Resolve the appId by scanning the userData/Dashboard directory for
49533
- * subdirectories containing workspaces.json. Falls back to the default.
49534
- */
49535
- function resolveAppId() {
49536
- const { app } = require$$0$1;
49537
- const fs = require$$0$2;
49538
- const path = require$$1$2;
49539
- const dashboardDir = path.join(app.getPath("userData"), "Dashboard");
49540
- try {
49541
- const entries = fs.readdirSync(dashboardDir, { withFileTypes: true });
49542
- for (const entry of entries) {
49543
- if (entry.isDirectory()) {
49544
- const wsFile = path.join(dashboardDir, entry.name, "workspaces.json");
49545
- if (fs.existsSync(wsFile)) {
49546
- return entry.name;
48970
+ const binaryPath = resolveCliBinary();
48971
+ if (!binaryPath) {
48972
+ safeSend(win, LLM_STREAM_ERROR$2, {
48973
+ requestId,
48974
+ error:
48975
+ "Claude Code CLI not found. Install from https://claude.ai/download",
48976
+ code: "CLI_NOT_FOUND",
48977
+ });
48978
+ return;
48979
+ }
48980
+
48981
+ // Build CLI args.
48982
+ //
48983
+ // --disable-slash-commands: prevent Claude from auto-triggering project
48984
+ // skills by description match. When running in a project with a
48985
+ // `.claude/skills/<name>/SKILL.md`, Claude would otherwise internalize
48986
+ // that skill's content even when the host app's system prompt says not
48987
+ // to — causing long reasoning loops or silent hangs. We pass only the
48988
+ // caller's `systemPrompt` as context.
48989
+ //
48990
+ // --permission-mode bypassPermissions: in an embedded in-app assistant,
48991
+ // the user has already opted into the configured MCP servers (they
48992
+ // ran `claude mcp add` themselves). Prompting for tool-use approval
48993
+ // on every call produces "I need permission to..." replies instead of
48994
+ // actual actions. Bypassing matches the user's intent — if they
48995
+ // didn't want the assistant to use a tool, they wouldn't have
48996
+ // configured it.
48997
+ //
48998
+ // (We intentionally avoid `--bare` — it also disables keychain reads,
48999
+ // which breaks OAuth login for users authenticated via `claude login`.)
49000
+ const args = [
49001
+ "-p",
49002
+ "--disable-slash-commands",
49003
+ "--permission-mode",
49004
+ "bypassPermissions",
49005
+ "--output-format",
49006
+ "stream-json",
49007
+ "--verbose",
49008
+ ];
49009
+
49010
+ // Auto-wire the hosted Dash MCP server so the assistant can use Dash
49011
+ // tools (apply_theme, create_dashboard, add_widget, etc.) without
49012
+ // the user running `claude mcp add dash ...` themselves. We pass an
49013
+ // inline --mcp-config every spawn; merges with any user-configured
49014
+ // MCPs so their other tools (github, slack, etc.) remain available.
49015
+ //
49016
+ // Prereqs: the Dash MCP server is running and has issued a bearer
49017
+ // token. If either is missing (server disabled, first launch before
49018
+ // auto-start completes), we silently skip — the assistant still
49019
+ // works for non-Dash queries, and the setup banner remains visible
49020
+ // as a manual fallback.
49021
+ try {
49022
+ const mcpDashServerController = mcpDashServerController_1;
49023
+ const status = mcpDashServerController.getStatus?.(win);
49024
+ if (status?.running) {
49025
+ const token = mcpDashServerController.getOrCreateToken?.(win);
49026
+ if (token) {
49027
+ const port = status.port || 3141;
49028
+ const mcpConfig = JSON.stringify({
49029
+ mcpServers: {
49030
+ dash: {
49031
+ type: "stdio",
49032
+ command: "npx",
49033
+ args: [
49034
+ "mcp-remote",
49035
+ `https://127.0.0.1:${port}/mcp`,
49036
+ "--header",
49037
+ `Authorization: Bearer ${token}`,
49038
+ ],
49039
+ env: { NODE_TLS_REJECT_UNAUTHORIZED: "0" },
49040
+ },
49041
+ },
49042
+ });
49043
+ args.push("--mcp-config", mcpConfig);
49547
49044
  }
49548
49045
  }
49046
+ } catch (err) {
49047
+ // Non-fatal: log and continue without Dash MCP.
49048
+ console.warn(
49049
+ "[cliController] Failed to inject Dash MCP config:",
49050
+ err?.message,
49051
+ );
49549
49052
  }
49550
- } catch (e) {
49551
- // Directory may not exist yet
49552
- }
49553
- return "@trops/dash-electron";
49554
- }
49555
49053
 
49556
- /**
49557
- * Get the current server context (win + appId) for tool handlers.
49558
- * Returns null if the server is not running.
49559
- */
49560
- function getServerContext() {
49561
- if (!activeWin) return null;
49562
- return { win: activeWin, appId: resolveAppId() };
49563
- }
49054
+ if (model) {
49055
+ args.push("--model", model);
49056
+ }
49564
49057
 
49565
- // --- Controller ---
49566
- const mcpDashServerController$4 = {
49567
- /**
49568
- * Start the MCP Dash server.
49569
- * @param {BrowserWindow} win
49570
- * @param {Object} options - { port?: number }
49571
- */
49572
- startServer: async (win, options = {}) => {
49573
- if (httpsServer) {
49574
- return {
49575
- success: false,
49576
- error: "Server is already running",
49577
- };
49058
+ if (systemPrompt) {
49059
+ args.push("--append-system-prompt", systemPrompt);
49578
49060
  }
49579
49061
 
49580
- try {
49581
- const serverSettings = getMcpServerSettings(win);
49582
- const port = options.port || serverSettings.port || 3141;
49583
- const token =
49584
- serverSettings.token || mcpDashServerController$4.getOrCreateToken(win);
49062
+ // Resume existing session for conversation continuity
49063
+ const sessionId = widgetUuid ? sessions.get(widgetUuid) : null;
49064
+ if (sessionId) {
49065
+ args.push("--resume", sessionId);
49066
+ }
49585
49067
 
49586
- // Create McpServer
49587
- mcpServer = new McpServer({
49588
- name: "dash-electron",
49589
- version: "1.0.0",
49068
+ // Extract the user message (last user message in the array)
49069
+ const lastUserMsg = [...messages].reverse().find((m) => m.role === "user");
49070
+ const userText =
49071
+ typeof lastUserMsg?.content === "string"
49072
+ ? lastUserMsg.content
49073
+ : Array.isArray(lastUserMsg?.content)
49074
+ ? lastUserMsg.content
49075
+ .filter((b) => b.type === "text")
49076
+ .map((b) => b.text)
49077
+ .join("\n")
49078
+ : "";
49079
+
49080
+ if (!userText) {
49081
+ safeSend(win, LLM_STREAM_ERROR$2, {
49082
+ requestId,
49083
+ error: "No user message to send.",
49084
+ code: "CLI_ERROR",
49590
49085
  });
49086
+ return;
49087
+ }
49591
49088
 
49592
- // Generate or load TLS certificate
49593
- const { app } = require("electron");
49594
- const path = require("path");
49595
- const certsDir = path.join(app.getPath("userData"), "certs");
49596
- const tlsCert = getOrCreateCert(certsDir);
49089
+ try {
49090
+ const fullPath = getShellPath();
49091
+ const spawnOpts = {
49092
+ env: { ...process.env, PATH: fullPath },
49093
+ stdio: ["pipe", "pipe", "pipe"],
49094
+ // On Windows, the Claude CLI is typically installed as claude.cmd
49095
+ // (a batch wrapper). Node's child_process.spawn can't launch .cmd
49096
+ // files directly without a shell — ENOENT otherwise.
49097
+ shell: IS_WINDOWS,
49098
+ };
49099
+ if (cwd) {
49100
+ const fs = require("fs");
49101
+ if (!fs.existsSync(cwd)) {
49102
+ fs.mkdirSync(cwd, { recursive: true });
49103
+ }
49104
+ spawnOpts.cwd = cwd;
49105
+ }
49106
+ const child = spawn(binaryPath, args, spawnOpts);
49597
49107
 
49598
- // Apply registered tools and resources
49599
- applyRegistrations(mcpServer);
49108
+ activeProcesses.set(requestId, child);
49600
49109
 
49601
- // Create HTTPS server with auth and rate limiting
49602
- httpsServer = https$2.createServer(
49603
- { key: tlsCert.key, cert: tlsCert.cert },
49604
- async (req, res) => {
49605
- const ip = req.socket.remoteAddress || req.connection.remoteAddress;
49110
+ // Pipe user message via stdin (not visible in ps)
49111
+ child.stdin.write(userText);
49112
+ child.stdin.end();
49606
49113
 
49607
- // Rate limiting
49608
- if (isRateLimited$1(ip)) {
49609
- res.writeHead(429, { "Content-Type": "application/json" });
49610
- res.end(JSON.stringify({ error: "Rate limit exceeded" }));
49611
- return;
49114
+ let stdoutBuffer = "";
49115
+ let stderrBuffer = "";
49116
+ let capturedSessionId = null;
49117
+ let retried = false;
49118
+
49119
+ // Track active tool calls for mapping results
49120
+ const activeToolCalls = new Map();
49121
+
49122
+ child.stdout.on("data", (chunk) => {
49123
+ stdoutBuffer += chunk.toString();
49124
+
49125
+ // Process complete lines
49126
+ const lines = stdoutBuffer.split("\n");
49127
+ stdoutBuffer = lines.pop(); // keep incomplete line in buffer
49128
+
49129
+ for (const line of lines) {
49130
+ if (!line.trim()) continue;
49131
+
49132
+ let parsed;
49133
+ try {
49134
+ parsed = JSON.parse(line);
49135
+ } catch {
49136
+ console.warn("[cliController] Skipping invalid JSON line:", line);
49137
+ continue;
49612
49138
  }
49613
49139
 
49614
- // Bearer token auth
49615
- const authHeader = req.headers.authorization;
49616
- if (!authHeader || authHeader !== `Bearer ${token}`) {
49617
- res.writeHead(401, { "Content-Type": "application/json" });
49618
- res.end(JSON.stringify({ error: "Unauthorized" }));
49619
- return;
49140
+ // Capture session ID from any message that has it
49141
+ if (parsed.session_id && widgetUuid) {
49142
+ capturedSessionId = parsed.session_id;
49143
+ sessions.set(widgetUuid, capturedSessionId);
49620
49144
  }
49621
49145
 
49622
- // Handle MCP requests on /mcp path
49623
- if (req.url === "/mcp" || req.url?.startsWith("/mcp")) {
49624
- try {
49625
- // Stateless mode: create a fresh server + transport per request
49626
- const reqServer = new McpServer({
49627
- name: "dash-electron",
49628
- version: "1.0.0",
49146
+ // Map CLI stream-json events to IPC events
49147
+ if (parsed.type === "content_block_delta") {
49148
+ if (parsed.delta?.type === "text_delta" && parsed.delta.text) {
49149
+ safeSend(win, LLM_STREAM_DELTA$2, {
49150
+ requestId,
49151
+ text: parsed.delta.text,
49629
49152
  });
49630
- applyRegistrations(reqServer);
49631
- const reqTransport = new StreamableHTTPServerTransport({
49632
- sessionIdGenerator: undefined,
49153
+ } else if (parsed.delta?.type === "input_json_delta") {
49154
+ // Update tool input incrementally
49155
+ const tc = activeToolCalls.get(parsed.index);
49156
+ if (tc) {
49157
+ tc.partialInput =
49158
+ (tc.partialInput || "") + (parsed.delta.partial_json || "");
49159
+ }
49160
+ }
49161
+ } else if (parsed.type === "content_block_start") {
49162
+ if (parsed.content_block?.type === "tool_use") {
49163
+ const toolBlock = parsed.content_block;
49164
+ activeToolCalls.set(parsed.index, {
49165
+ toolUseId: toolBlock.id,
49166
+ toolName: toolBlock.name,
49167
+ partialInput: "",
49633
49168
  });
49634
- await reqServer.connect(reqTransport);
49635
- connectionCount++;
49636
- await reqTransport.handleRequest(req, res);
49637
- } catch (err) {
49638
- console.error("[mcpDashServer] Error handling MCP request:", err);
49639
- if (!res.headersSent) {
49640
- res.writeHead(500, {
49641
- "Content-Type": "application/json",
49642
- });
49643
- res.end(
49644
- JSON.stringify({
49645
- error: "Internal server error",
49646
- }),
49647
- );
49169
+ safeSend(win, LLM_STREAM_TOOL_CALL$2, {
49170
+ requestId,
49171
+ toolUseId: toolBlock.id,
49172
+ toolName: toolBlock.name,
49173
+ serverName: "Claude Code",
49174
+ input: toolBlock.input || {},
49175
+ });
49176
+ }
49177
+ } else if (parsed.type === "content_block_stop") {
49178
+ // Tool call completed — try to parse the accumulated input
49179
+ const tc = activeToolCalls.get(parsed.index);
49180
+ if (tc && tc.partialInput) {
49181
+ try {
49182
+ tc.finalInput = JSON.parse(tc.partialInput);
49183
+ } catch {
49184
+ tc.finalInput = tc.partialInput;
49648
49185
  }
49649
49186
  }
49650
- } else {
49651
- // Health check endpoint
49652
- if (req.url === "/health" && req.method === "GET") {
49653
- res.writeHead(200, {
49654
- "Content-Type": "application/json",
49187
+ } else if (parsed.type === "message_stop") {
49188
+ // Individual message completed (may be followed by more in tool-use loops)
49189
+ } else if (parsed.type === "result") {
49190
+ // Final result — conversation complete
49191
+ const content = [];
49192
+ if (parsed.result) {
49193
+ content.push({ type: "text", text: parsed.result });
49194
+ }
49195
+
49196
+ safeSend(win, LLM_STREAM_COMPLETE$2, {
49197
+ requestId,
49198
+ content,
49199
+ stopReason: parsed.stop_reason || "end_turn",
49200
+ usage: parsed.usage || {},
49201
+ });
49202
+ }
49203
+ }
49204
+ });
49205
+
49206
+ child.stderr.on("data", (chunk) => {
49207
+ stderrBuffer += chunk.toString();
49208
+ });
49209
+
49210
+ child.on("error", (err) => {
49211
+ activeProcesses.delete(requestId);
49212
+ safeSend(win, LLM_STREAM_ERROR$2, {
49213
+ requestId,
49214
+ error: `Failed to start Claude CLI: ${err.message}`,
49215
+ code: "CLI_SPAWN_ERROR",
49216
+ });
49217
+ });
49218
+
49219
+ child.on("close", (code) => {
49220
+ activeProcesses.delete(requestId);
49221
+
49222
+ // Process any remaining buffer
49223
+ if (stdoutBuffer.trim()) {
49224
+ try {
49225
+ const parsed = JSON.parse(stdoutBuffer);
49226
+ if (parsed.session_id && widgetUuid) {
49227
+ sessions.set(widgetUuid, parsed.session_id);
49228
+ }
49229
+ if (parsed.type === "result") {
49230
+ const content = [];
49231
+ if (parsed.result) {
49232
+ content.push({ type: "text", text: parsed.result });
49233
+ }
49234
+ safeSend(win, LLM_STREAM_COMPLETE$2, {
49235
+ requestId,
49236
+ content,
49237
+ stopReason: parsed.stop_reason || "end_turn",
49238
+ usage: parsed.usage || {},
49655
49239
  });
49656
- res.end(
49657
- JSON.stringify({
49658
- status: "ok",
49659
- server: "dash-electron-mcp",
49660
- version: "1.0.0",
49661
- }),
49662
- );
49663
49240
  return;
49664
49241
  }
49665
- res.writeHead(404, { "Content-Type": "application/json" });
49666
- res.end(JSON.stringify({ error: "Not found" }));
49242
+ } catch {
49243
+ // ignore
49667
49244
  }
49668
- },
49669
- );
49245
+ }
49670
49246
 
49671
- // Bind to localhost only
49672
- await new Promise((resolve, reject) => {
49673
- httpsServer.on("error", (err) => {
49674
- httpsServer = null;
49675
- mcpServer = null;
49676
- if (err.code === "EADDRINUSE") {
49677
- reject(
49678
- new Error(
49679
- `Port ${port} is already in use. Choose a different port in Settings.`,
49680
- ),
49681
- );
49682
- } else {
49683
- reject(err);
49247
+ if (code !== 0 && code !== null) {
49248
+ // Check if resume failed and retry without it
49249
+ if (sessionId && !retried && stderrBuffer.includes("session")) {
49250
+ retried = true;
49251
+ if (widgetUuid) sessions.delete(widgetUuid);
49252
+ // Retry without --resume
49253
+ cliController$2.sendMessage(win, requestId, {
49254
+ ...params,
49255
+ _retryWithoutResume: true,
49256
+ });
49257
+ return;
49684
49258
  }
49685
- });
49686
- httpsServer.listen(port, "127.0.0.1", () => {
49687
- resolve();
49688
- });
49689
- });
49690
49259
 
49691
- startTime = Date.now();
49692
- connectionCount = 0;
49693
- activeWin = win;
49694
- startCleanup();
49260
+ // Check for auth errors
49261
+ if (
49262
+ stderrBuffer.includes("auth") ||
49263
+ stderrBuffer.includes("login") ||
49264
+ stderrBuffer.includes("not authenticated")
49265
+ ) {
49266
+ safeSend(win, LLM_STREAM_ERROR$2, {
49267
+ requestId,
49268
+ error:
49269
+ "Claude Code CLI is not authenticated. Run `claude auth login` in your terminal.",
49270
+ code: "CLI_AUTH_ERROR",
49271
+ });
49272
+ return;
49273
+ }
49695
49274
 
49696
- // Save enabled state
49697
- saveMcpServerSettings(win, {
49698
- ...serverSettings,
49699
- enabled: true,
49700
- port,
49701
- token,
49275
+ safeSend(win, LLM_STREAM_ERROR$2, {
49276
+ requestId,
49277
+ error: `Claude CLI exited with code ${code}${stderrBuffer ? ": " + stderrBuffer.slice(0, 500) : ""}`,
49278
+ code: "CLI_ERROR",
49279
+ });
49280
+ }
49281
+ });
49282
+ } catch (err) {
49283
+ activeProcesses.delete(requestId);
49284
+ safeSend(win, LLM_STREAM_ERROR$2, {
49285
+ requestId,
49286
+ error: `Failed to start Claude CLI: ${err.message}`,
49287
+ code: "CLI_SPAWN_ERROR",
49702
49288
  });
49289
+ }
49290
+ },
49703
49291
 
49704
- console.log(
49705
- `[mcpDashServer] Server started on https://127.0.0.1:${port}/mcp`,
49706
- );
49292
+ /**
49293
+ * abortRequest
49294
+ * Kill an in-flight CLI process.
49295
+ *
49296
+ * @param {string} requestId - the request to cancel
49297
+ * @returns {{ success: boolean }}
49298
+ */
49299
+ abortRequest: (requestId) => {
49300
+ const child = activeProcesses.get(requestId);
49301
+ if (child) {
49302
+ killChildTree(child);
49303
+ activeProcesses.delete(requestId);
49304
+ return { success: true };
49305
+ }
49306
+ return { success: false, message: "Request not found" };
49307
+ },
49707
49308
 
49708
- return {
49709
- success: true,
49710
- port,
49711
- url: `https://127.0.0.1:${port}/mcp`,
49712
- };
49713
- } catch (err) {
49714
- console.error("[mcpDashServer] Failed to start server:", err);
49715
- httpsServer = null;
49716
- mcpServer = null;
49717
- return {
49718
- success: false,
49719
- error: err.message,
49720
- };
49309
+ /**
49310
+ * clearSession
49311
+ * Remove the stored session ID for a widget (called on "New Chat").
49312
+ *
49313
+ * @param {string} widgetUuid - the widget whose session to clear
49314
+ * @returns {{ success: boolean }}
49315
+ */
49316
+ clearSession: (widgetUuid) => {
49317
+ if (widgetUuid && sessions.has(widgetUuid)) {
49318
+ sessions.delete(widgetUuid);
49319
+ return { success: true };
49320
+ }
49321
+ return { success: false };
49322
+ },
49323
+
49324
+ /**
49325
+ * getSessionStatus
49326
+ * Check if a CLI session exists and whether a process is active for a widget.
49327
+ *
49328
+ * @param {string} widgetUuid - the widget to check
49329
+ * @returns {{ hasSession: boolean, sessionId?: string, isProcessActive: boolean }}
49330
+ */
49331
+ getSessionStatus: (widgetUuid) => {
49332
+ const sessionId = widgetUuid ? sessions.get(widgetUuid) : null;
49333
+ // Check if any active process belongs to this widget
49334
+ let isProcessActive = false;
49335
+ for (const [, child] of activeProcesses) {
49336
+ if (!child.killed) {
49337
+ isProcessActive = true;
49338
+ break;
49339
+ }
49340
+ }
49341
+ return {
49342
+ hasSession: !!sessionId,
49343
+ sessionId: sessionId || undefined,
49344
+ isProcessActive,
49345
+ };
49346
+ },
49347
+
49348
+ /**
49349
+ * endSession
49350
+ * Kill any active CLI process AND clear the session for a widget.
49351
+ *
49352
+ * @param {string} widgetUuid - the widget whose session to end
49353
+ * @returns {{ success: boolean }}
49354
+ */
49355
+ endSession: (widgetUuid) => {
49356
+ // Kill any active processes for this widget
49357
+ for (const [reqId, child] of activeProcesses) {
49358
+ if (reqId.startsWith(widgetUuid)) {
49359
+ killChildTree(child);
49360
+ activeProcesses.delete(reqId);
49361
+ }
49362
+ }
49363
+ // Clear the session
49364
+ if (widgetUuid && sessions.has(widgetUuid)) {
49365
+ sessions.delete(widgetUuid);
49721
49366
  }
49367
+ return { success: true };
49368
+ },
49369
+ };
49370
+
49371
+ var cliController_1 = cliController$2;
49372
+
49373
+ /**
49374
+ * toolDefinitions.js
49375
+ *
49376
+ * MCP tool schemas for dashboard/workspace operations and app stats.
49377
+ * Each definition includes name, description, and JSON Schema inputSchema.
49378
+ */
49379
+
49380
+ const dashboardTools$1 = [
49381
+ {
49382
+ name: "list_dashboards",
49383
+ description:
49384
+ "List all dashboards with their IDs, names, and widget counts. Use this to discover existing dashboards before creating new ones or to find a dashboard ID for other operations.",
49385
+ inputSchema: {
49386
+ type: "object",
49387
+ properties: {},
49388
+ required: [],
49389
+ },
49390
+ },
49391
+ {
49392
+ name: "get_dashboard",
49393
+ description:
49394
+ "Get full details of a dashboard including layout, widgets, and theme. Omit dashboardId to get the active dashboard. Use this to inspect widget configurations or to understand the current layout before making changes.",
49395
+ inputSchema: {
49396
+ type: "object",
49397
+ properties: {
49398
+ dashboardId: {
49399
+ type: "string",
49400
+ description:
49401
+ "Dashboard ID. Omit to get the currently active dashboard.",
49402
+ },
49403
+ },
49404
+ required: [],
49405
+ },
49406
+ },
49407
+ {
49408
+ name: "create_dashboard",
49409
+ description:
49410
+ "Create a new dashboard with the given name. Defaults to a 1×1 grid layout if `layout` is omitted — the resulting dashboard has a single cell ready for a widget. Pass an explicit `layout` object to use different dimensions. Pass `layout: null` only if the caller specifically wants a layout-less container dashboard (rare — widgets cannot be added without further editing). Returns the dashboard ID.",
49411
+ inputSchema: {
49412
+ type: "object",
49413
+ properties: {
49414
+ name: {
49415
+ type: "string",
49416
+ description: "Display name for the new dashboard",
49417
+ },
49418
+ layout: {
49419
+ type: "object",
49420
+ description:
49421
+ "Optional grid layout configuration. When provided, creates a grid dashboard instead of a simple container.",
49422
+ properties: {
49423
+ rows: {
49424
+ type: "number",
49425
+ description: "Number of rows (1-10)",
49426
+ },
49427
+ cols: {
49428
+ type: "number",
49429
+ description: "Number of columns (1-10)",
49430
+ },
49431
+ gap: {
49432
+ type: "string",
49433
+ description:
49434
+ "Tailwind gap class (e.g. 'gap-2', 'gap-4'). Defaults to 'gap-2'.",
49435
+ },
49436
+ colModes: {
49437
+ type: "object",
49438
+ description:
49439
+ "Per-row column sizing. Keys are row numbers (as strings), values are mode strings: 'equal', '1/4', '1/3', '1/2', '2/3'.",
49440
+ },
49441
+ },
49442
+ required: ["rows", "cols"],
49443
+ },
49444
+ },
49445
+ required: ["name"],
49446
+ },
49447
+ },
49448
+ {
49449
+ name: "delete_dashboard",
49450
+ description:
49451
+ "Delete a dashboard by ID. Cannot delete the last remaining dashboard. Use list_dashboards first to find the dashboard ID.",
49452
+ inputSchema: {
49453
+ type: "object",
49454
+ properties: {
49455
+ dashboardId: {
49456
+ type: "string",
49457
+ description: "ID of the dashboard to delete",
49458
+ },
49459
+ },
49460
+ required: ["dashboardId"],
49461
+ },
49462
+ },
49463
+ {
49464
+ name: "get_app_stats",
49465
+ description:
49466
+ "Get application statistics: counts of dashboards, widgets, themes, and providers. Useful for understanding the current state of the app at a glance.",
49467
+ inputSchema: {
49468
+ type: "object",
49469
+ properties: {},
49470
+ required: [],
49471
+ },
49472
+ },
49473
+ {
49474
+ name: "search_registry_dashboards",
49475
+ description:
49476
+ "Search the online Dash registry for pre-built dashboard templates. Returns matching dashboards with their names, descriptions, and the list of widgets they require. Useful when the user asks for a dashboard by topic (e.g. 'find me a sales dashboard'). If `compatibleWidgetsOnly` is true, only dashboards whose required widgets are ALL already installed are returned — safe to install without additional widget downloads. Otherwise, include dashboards that may require pulling in new widget packages first.",
49477
+ inputSchema: {
49478
+ type: "object",
49479
+ properties: {
49480
+ query: {
49481
+ type: "string",
49482
+ description:
49483
+ "Search keyword to match against dashboard names, descriptions, and tags",
49484
+ },
49485
+ compatibleWidgetsOnly: {
49486
+ type: "boolean",
49487
+ description:
49488
+ "When true, restrict results to dashboards whose required widgets are already installed. Defaults to false (returns all matches).",
49489
+ },
49490
+ },
49491
+ required: ["query"],
49492
+ },
49493
+ },
49494
+ ];
49495
+
49496
+ const widgetTools$1 = [
49497
+ {
49498
+ name: "add_widget",
49499
+ description:
49500
+ "Add a widget to a dashboard by its scoped component name. IMPORTANT: Use the exact scoped name from list_widgets or search_widgets (format: 'scope.package.WidgetName', e.g. 'trops.gong.GongCallSearch'). Can be called multiple times. Returns the widget instance ID for use with configure_widget. If the dashboard has a grid layout, you can specify row/col for explicit placement, or omit them to auto-place in the next empty cell.",
49501
+ inputSchema: {
49502
+ type: "object",
49503
+ properties: {
49504
+ dashboardId: {
49505
+ type: "string",
49506
+ description:
49507
+ "Dashboard ID to add the widget to. Omit to use the active dashboard.",
49508
+ },
49509
+ widgetName: {
49510
+ type: "string",
49511
+ description:
49512
+ "Scoped component name from list_widgets/search_widgets (e.g. 'trops.gong.GongCallSearch', 'trops.slack.SlackChannelFeed')",
49513
+ },
49514
+ row: {
49515
+ type: "number",
49516
+ description:
49517
+ "Grid row to place the widget in (1-indexed). Must be used together with col. Requires a grid layout on the dashboard.",
49518
+ },
49519
+ col: {
49520
+ type: "number",
49521
+ description:
49522
+ "Grid column to place the widget in (1-indexed). Must be used together with row.",
49523
+ },
49524
+ },
49525
+ required: ["widgetName"],
49526
+ },
49527
+ },
49528
+ {
49529
+ name: "remove_widget",
49530
+ description:
49531
+ "Remove a widget instance from a dashboard by its ID. Use get_dashboard to find widget instance IDs.",
49532
+ inputSchema: {
49533
+ type: "object",
49534
+ properties: {
49535
+ dashboardId: {
49536
+ type: "string",
49537
+ description: "Dashboard ID. Omit to use the active dashboard.",
49538
+ },
49539
+ widgetId: {
49540
+ type: "string",
49541
+ description: "ID of the widget instance to remove",
49542
+ },
49543
+ },
49544
+ required: ["widgetId"],
49545
+ },
49546
+ },
49547
+ {
49548
+ name: "configure_widget",
49549
+ description:
49550
+ "Update a widget's configuration. The config object is merged into the existing config (partial update). Use get_dashboard to see current widget configs and discover valid config keys.",
49551
+ inputSchema: {
49552
+ type: "object",
49553
+ properties: {
49554
+ dashboardId: {
49555
+ type: "string",
49556
+ description: "Dashboard ID. Omit to use the active dashboard.",
49557
+ },
49558
+ widgetId: {
49559
+ type: "string",
49560
+ description: "ID of the widget instance to configure",
49561
+ },
49562
+ config: {
49563
+ type: "object",
49564
+ description:
49565
+ "Configuration object to merge into existing widget config",
49566
+ },
49567
+ },
49568
+ required: ["widgetId", "config"],
49569
+ },
49570
+ },
49571
+ {
49572
+ name: "list_widgets",
49573
+ description:
49574
+ "List all available widgets from the registry. Returns scoped component names (e.g. 'trops.gong.GongCallSearch') that can be passed directly to add_widget. Each widget includes an 'installed' boolean — if true, use add_widget directly; if false, call install_widget first. Also includes description, provider requirements, and package info.",
49575
+ inputSchema: {
49576
+ type: "object",
49577
+ properties: {},
49578
+ required: [],
49579
+ },
49580
+ },
49581
+ {
49582
+ name: "search_widgets",
49583
+ description:
49584
+ "Search the widget registry by keyword. Returns matching widgets with scoped names (e.g. 'trops.slack.SlackChannelFeed') that can be passed directly to add_widget. Each widget includes an 'installed' boolean — if true, use add_widget directly; if false, call install_widget first. Also includes description and provider info.",
49585
+ inputSchema: {
49586
+ type: "object",
49587
+ properties: {
49588
+ query: {
49589
+ type: "string",
49590
+ description:
49591
+ "Search keyword to match against widget names, descriptions, and tags",
49592
+ },
49593
+ },
49594
+ required: ["query"],
49595
+ },
49722
49596
  },
49723
-
49724
- /**
49725
- * Stop the MCP Dash server.
49726
- */
49727
- stopServer: async (win) => {
49728
- if (!httpsServer) {
49729
- return { success: true, message: "Server was not running" };
49730
- }
49731
-
49732
- try {
49733
- stopCleanup();
49734
-
49735
- await new Promise((resolve) => {
49736
- httpsServer.close(() => resolve());
49737
- // Force close after 5 seconds
49738
- setTimeout(() => resolve(), 5000);
49739
- });
49740
-
49741
- if (mcpServer) {
49742
- try {
49743
- await mcpServer.close();
49744
- } catch (e) {
49745
- // Ignore close errors
49746
- }
49747
- }
49748
-
49749
- httpsServer = null;
49750
- mcpServer = null;
49751
- transport = null;
49752
- startTime = null;
49753
- connectionCount = 0;
49754
- activeWin = null;
49755
-
49756
- // Update settings
49757
- if (win) {
49758
- const serverSettings = getMcpServerSettings(win);
49759
- saveMcpServerSettings(win, {
49760
- ...serverSettings,
49761
- enabled: false,
49762
- });
49763
- }
49764
-
49765
- console.log("[mcpDashServer] Server stopped");
49766
- return { success: true };
49767
- } catch (err) {
49768
- console.error("[mcpDashServer] Error stopping server:", err);
49769
- return { success: false, error: err.message };
49770
- }
49597
+ {
49598
+ name: "install_widget",
49599
+ description:
49600
+ "Install a widget package from the Dash registry. Requires registry authentication — the user must be signed in via Settings > Account in the Dash app. Use search_widgets first to find available packages, then install by package name (e.g., 'slack', 'gong', 'chat'). After installation, use add_widget to place it on a dashboard.",
49601
+ inputSchema: {
49602
+ type: "object",
49603
+ properties: {
49604
+ packageName: {
49605
+ type: "string",
49606
+ description:
49607
+ "Package name from the registry (e.g., 'slack', 'gong', 'chat'). Use the 'package' field from search_widgets results.",
49608
+ },
49609
+ },
49610
+ required: ["packageName"],
49611
+ },
49771
49612
  },
49613
+ ];
49772
49614
 
49773
- /**
49774
- * Restart the server (stop + start).
49775
- */
49776
- restartServer: async (win, options = {}) => {
49777
- await mcpDashServerController$4.stopServer(win);
49778
- return mcpDashServerController$4.startServer(win, options);
49615
+ const themeTools$1 = [
49616
+ {
49617
+ name: "list_themes",
49618
+ description:
49619
+ "List all saved themes with their names and whether they are currently active. Use this to discover available themes before applying one.",
49620
+ inputSchema: {
49621
+ type: "object",
49622
+ properties: {},
49623
+ required: [],
49624
+ },
49625
+ },
49626
+ {
49627
+ name: "get_theme",
49628
+ description:
49629
+ "Get full details of a theme by name, including all color values and shade mappings. Use list_themes first to find theme names.",
49630
+ inputSchema: {
49631
+ type: "object",
49632
+ properties: {
49633
+ name: {
49634
+ type: "string",
49635
+ description: "Name of the theme to retrieve",
49636
+ },
49637
+ },
49638
+ required: ["name"],
49639
+ },
49640
+ },
49641
+ {
49642
+ name: "create_theme",
49643
+ description:
49644
+ "Create a new theme from a colors object. Primary maps to buttons, links, and active states. Secondary maps to backgrounds, cards, and panels. Tertiary maps to accents, badges, and highlights. Example colors: { primary: '#3b82f6', secondary: '#10b981', tertiary: '#f59e0b' }. After creation, use apply_theme to activate it.",
49645
+ inputSchema: {
49646
+ type: "object",
49647
+ properties: {
49648
+ name: {
49649
+ type: "string",
49650
+ description: "Display name for the new theme",
49651
+ },
49652
+ colors: {
49653
+ type: "object",
49654
+ description:
49655
+ "Theme colors object with role keys mapped to hex values or shade objects",
49656
+ },
49657
+ },
49658
+ required: ["name", "colors"],
49659
+ },
49660
+ },
49661
+ {
49662
+ name: "create_theme_from_url",
49663
+ description:
49664
+ "Extract brand colors from a website URL and generate a matching theme. Loads the page in a hidden browser, extracts colors from meta tags, CSS variables, computed styles, and favicons, then maps them to theme roles. Works best with pages that have visible brand colors. Takes a few seconds to process. After creation, use apply_theme to activate it.",
49665
+ inputSchema: {
49666
+ type: "object",
49667
+ properties: {
49668
+ url: {
49669
+ type: "string",
49670
+ description:
49671
+ "Website URL to extract colors from (must start with http:// or https://)",
49672
+ },
49673
+ name: {
49674
+ type: "string",
49675
+ description:
49676
+ "Optional name for the theme. If omitted, a name is derived from the URL hostname.",
49677
+ },
49678
+ },
49679
+ required: ["url"],
49680
+ },
49681
+ },
49682
+ {
49683
+ name: "apply_theme",
49684
+ description:
49685
+ "Apply a saved theme. Omit `dashboard` to set the app-wide default theme (affects every dashboard that doesn't have its own override). Pass `dashboard` (name or ID) to set that dashboard's theme override instead — useful when the user asks for a theme on a specific dashboard (e.g. 'apply ocean to my Sales dashboard'). The theme must already exist; use list_themes to see available themes or create one with create_theme / create_theme_from_url.",
49686
+ inputSchema: {
49687
+ type: "object",
49688
+ properties: {
49689
+ name: {
49690
+ type: "string",
49691
+ description: "Name of the theme to apply",
49692
+ },
49693
+ dashboard: {
49694
+ type: "string",
49695
+ description:
49696
+ "Optional dashboard name or numeric ID. Omit for app-wide application.",
49697
+ },
49698
+ },
49699
+ required: ["name"],
49700
+ },
49701
+ },
49702
+ {
49703
+ name: "search_registry_themes",
49704
+ description:
49705
+ "Search the online Dash registry for themes by keyword. Returns matching theme packages with their names, descriptions, and preview metadata. Useful when the user asks for a theme style (e.g. 'find me a dark purple theme') and the local `list_themes` set doesn't have a good match. Each result includes an `installed` boolean — if false, call `install_registry_theme` to pull it in, then `apply_theme` to activate.",
49706
+ inputSchema: {
49707
+ type: "object",
49708
+ properties: {
49709
+ query: {
49710
+ type: "string",
49711
+ description:
49712
+ "Search keyword to match against theme names, descriptions, and tags",
49713
+ },
49714
+ },
49715
+ required: ["query"],
49716
+ },
49779
49717
  },
49718
+ ];
49780
49719
 
49781
- /**
49782
- * Get server status.
49783
- */
49784
- getStatus: (win) => {
49785
- const serverSettings = getMcpServerSettings(win);
49786
- return {
49787
- running: !!httpsServer,
49788
- enabled: serverSettings.enabled || false,
49789
- port: serverSettings.port || 3141,
49790
- connectionCount,
49791
- uptime: startTime ? Math.floor((Date.now() - startTime) / 1000) : 0,
49792
- toolCount: registeredTools.length,
49793
- resourceCount: registeredResources.length,
49794
- };
49720
+ const guideTools$1 = [
49721
+ {
49722
+ name: "get_setup_guide",
49723
+ description:
49724
+ "Get a contextual setup guide for Dash. Returns step-by-step instructions for the requested topic. Call this when the user asks how to get started, what they can do, or needs help with a specific workflow.",
49725
+ inputSchema: {
49726
+ type: "object",
49727
+ properties: {
49728
+ topic: {
49729
+ type: "string",
49730
+ enum: ["dashboard", "theme", "provider", "widget", "overview"],
49731
+ description:
49732
+ "Topic to get help with. Use 'overview' or omit for a general capabilities guide.",
49733
+ },
49734
+ },
49735
+ required: [],
49736
+ },
49795
49737
  },
49738
+ ];
49796
49739
 
49797
- /**
49798
- * Get or create the bearer token.
49799
- */
49800
- getOrCreateToken: (win) => {
49801
- const serverSettings = getMcpServerSettings(win);
49802
- if (serverSettings.token) {
49803
- return serverSettings.token;
49804
- }
49805
- const token = randomUUID();
49806
- saveMcpServerSettings(win, { ...serverSettings, token });
49807
- return token;
49740
+ const providerTools$1 = [
49741
+ {
49742
+ name: "list_providers",
49743
+ description:
49744
+ "List all configured providers with their names, types, and status. Credential secrets are never returned. Use this to check which services are already connected.",
49745
+ inputSchema: {
49746
+ type: "object",
49747
+ properties: {},
49748
+ required: [],
49749
+ },
49750
+ },
49751
+ {
49752
+ name: "add_provider",
49753
+ description:
49754
+ "Add a new provider configuration. Supports credential providers (API keys) and MCP providers (server connections with tool scoping). Credentials are encrypted at rest. Common types: 'github', 'slack', 'algolia', 'notion', 'openai'. Use list_providers first to check existing connections.",
49755
+ inputSchema: {
49756
+ type: "object",
49757
+ properties: {
49758
+ name: {
49759
+ type: "string",
49760
+ description:
49761
+ "Unique display name for the provider (e.g. 'Algolia Production', 'Slack')",
49762
+ },
49763
+ type: {
49764
+ type: "string",
49765
+ description:
49766
+ "Provider type identifier (e.g. 'algolia', 'slack', 'openai', 'github')",
49767
+ },
49768
+ providerClass: {
49769
+ type: "string",
49770
+ enum: ["credential", "mcp"],
49771
+ description:
49772
+ "Provider class: 'credential' for API key providers, 'mcp' for MCP server providers. Defaults to 'credential'.",
49773
+ },
49774
+ credentials: {
49775
+ type: "object",
49776
+ description:
49777
+ "Credentials object (e.g. { apiKey: '...', appId: '...' }). Encrypted at rest, never returned in responses.",
49778
+ },
49779
+ mcpConfig: {
49780
+ type: "object",
49781
+ description:
49782
+ "MCP server configuration (transport, command, args, envMapping). Only used when providerClass is 'mcp'.",
49783
+ },
49784
+ allowedTools: {
49785
+ type: "array",
49786
+ items: { type: "string" },
49787
+ description:
49788
+ "Optional list of allowed MCP tool names. Only used when providerClass is 'mcp'.",
49789
+ },
49790
+ },
49791
+ required: ["name", "type", "credentials"],
49792
+ },
49793
+ },
49794
+ {
49795
+ name: "remove_provider",
49796
+ description:
49797
+ "Remove a provider by name. This deletes the provider and its stored credentials permanently.",
49798
+ inputSchema: {
49799
+ type: "object",
49800
+ properties: {
49801
+ name: {
49802
+ type: "string",
49803
+ description: "Name of the provider to remove",
49804
+ },
49805
+ },
49806
+ required: ["name"],
49807
+ },
49808
49808
  },
49809
+ ];
49809
49810
 
49810
- /**
49811
- * Auto-start server if enabled in settings.
49812
- * Called from dash-electron on app ready.
49813
- */
49814
- autoStart: async (win) => {
49815
- const serverSettings = getMcpServerSettings(win);
49816
- if (serverSettings.enabled) {
49817
- console.log("[mcpDashServer] Auto-starting server...");
49818
- return mcpDashServerController$4.startServer(win, {
49819
- port: serverSettings.port,
49820
- });
49821
- }
49822
- return { success: false, message: "Server not enabled" };
49811
+ const layoutTools$1 = [
49812
+ {
49813
+ name: "set_layout",
49814
+ description:
49815
+ "Set or replace the grid layout on a dashboard. Creates a LayoutGridContainer with the specified dimensions. Existing widgets in cells that fit the new grid are preserved; widgets outside the new bounds are orphaned (kept but unassigned). Use this to add a grid to an existing dashboard or to resize the grid.",
49816
+ inputSchema: {
49817
+ type: "object",
49818
+ properties: {
49819
+ dashboardId: {
49820
+ type: "string",
49821
+ description: "Dashboard ID. Omit to use the active dashboard.",
49822
+ },
49823
+ rows: {
49824
+ type: "number",
49825
+ description: "Number of rows (1-10)",
49826
+ },
49827
+ cols: {
49828
+ type: "number",
49829
+ description: "Number of columns (1-10)",
49830
+ },
49831
+ gap: {
49832
+ type: "string",
49833
+ description:
49834
+ "Tailwind gap class (e.g. 'gap-2', 'gap-4'). Defaults to 'gap-2'.",
49835
+ },
49836
+ colModes: {
49837
+ type: "object",
49838
+ description:
49839
+ "Per-row column sizing. Keys are row numbers (as strings), values are mode strings: 'equal', '1/4', '1/3', '1/2', '2/3'.",
49840
+ },
49841
+ },
49842
+ required: ["rows", "cols"],
49843
+ },
49844
+ },
49845
+ {
49846
+ name: "update_layout",
49847
+ description:
49848
+ "Partially update the grid layout. Only specified properties change — omitted properties keep their current values. colModes is merged (not replaced). Widgets in removed rows/columns are orphaned. Dashboard must already have a grid layout.",
49849
+ inputSchema: {
49850
+ type: "object",
49851
+ properties: {
49852
+ dashboardId: {
49853
+ type: "string",
49854
+ description: "Dashboard ID. Omit to use the active dashboard.",
49855
+ },
49856
+ rows: {
49857
+ type: "number",
49858
+ description: "New number of rows (1-10). Omit to keep current.",
49859
+ },
49860
+ cols: {
49861
+ type: "number",
49862
+ description: "New number of columns (1-10). Omit to keep current.",
49863
+ },
49864
+ gap: {
49865
+ type: "string",
49866
+ description: "Tailwind gap class. Omit to keep current.",
49867
+ },
49868
+ colModes: {
49869
+ type: "object",
49870
+ description:
49871
+ "Column sizing modes to merge. Set a key to null to reset that row to default.",
49872
+ },
49873
+ },
49874
+ required: [],
49875
+ },
49876
+ },
49877
+ {
49878
+ name: "move_widget",
49879
+ description:
49880
+ "Move a widget to a different grid cell. If the target cell is occupied, the two widgets are swapped. The widget must already be placed in a grid cell. Use get_dashboard to find widget IDs and current positions.",
49881
+ inputSchema: {
49882
+ type: "object",
49883
+ properties: {
49884
+ dashboardId: {
49885
+ type: "string",
49886
+ description: "Dashboard ID. Omit to use the active dashboard.",
49887
+ },
49888
+ widgetId: {
49889
+ type: "string",
49890
+ description: "ID of the widget to move",
49891
+ },
49892
+ row: {
49893
+ type: "number",
49894
+ description: "Target row (1-indexed)",
49895
+ },
49896
+ col: {
49897
+ type: "number",
49898
+ description: "Target column (1-indexed)",
49899
+ },
49900
+ },
49901
+ required: ["widgetId", "row", "col"],
49902
+ },
49823
49903
  },
49904
+ ];
49824
49905
 
49825
- // Expose registration functions for other controllers
49826
- registerTool: registerTool$6,
49827
- registerResource: registerResource$1,
49828
- registerPrompt: registerPrompt$1,
49829
- getServerContext,
49906
+ var toolDefinitions$1 = {
49907
+ dashboardTools: dashboardTools$1,
49908
+ widgetTools: widgetTools$1,
49909
+ themeTools: themeTools$1,
49910
+ providerTools: providerTools$1,
49911
+ guideTools: guideTools$1,
49912
+ layoutTools: layoutTools$1,
49830
49913
  };
49831
49914
 
49832
- var mcpDashServerController_1 = mcpDashServerController$4;
49833
-
49834
49915
  var widgetRegistry$1 = {exports: {}};
49835
49916
 
49836
49917
  var dynamicWidgetLoader$3 = {exports: {}};
@@ -60782,6 +60863,159 @@ async function handleApplyTheme$1({ name, dashboard }) {
60782
60863
  };
60783
60864
  }
60784
60865
 
60866
+ /**
60867
+ * search_registry_themes — Search the online Dash registry for themes by
60868
+ * keyword. Companion to list_themes (which lists already-saved themes).
60869
+ */
60870
+ async function handleSearchRegistryThemes$1({ query }) {
60871
+ if (!query || typeof query !== "string" || !query.trim()) {
60872
+ return {
60873
+ content: [
60874
+ {
60875
+ type: "text",
60876
+ text: JSON.stringify({
60877
+ error: "query is required and must be a non-empty string",
60878
+ }),
60879
+ },
60880
+ ],
60881
+ isError: true,
60882
+ };
60883
+ }
60884
+
60885
+ try {
60886
+ const result = await registryController$2.searchThemes(query.trim());
60887
+ const packages = result.packages || [];
60888
+
60889
+ // Build a set of locally-saved theme names so the LLM knows which
60890
+ // registry themes are already available.
60891
+ let installedNames = new Set();
60892
+ try {
60893
+ const { win, appId } = requireContext$1();
60894
+ const local = themeController$4.listThemesForApplication(win, appId);
60895
+ const themeMap = local?.themes || {};
60896
+ installedNames = new Set(Object.keys(themeMap));
60897
+ } catch {
60898
+ /* best-effort — continue with empty set if context unavailable */
60899
+ }
60900
+
60901
+ const themes = packages.map((pkg) => ({
60902
+ name: pkg.name,
60903
+ scope: pkg.scope || null,
60904
+ displayName: pkg.displayName || pkg.name,
60905
+ description: pkg.description || "",
60906
+ icon: pkg.icon || null,
60907
+ installed: installedNames.has(pkg.name),
60908
+ preview: pkg.preview || null,
60909
+ }));
60910
+
60911
+ return {
60912
+ content: [
60913
+ {
60914
+ type: "text",
60915
+ text: JSON.stringify(
60916
+ { query: query.trim(), themes, count: themes.length },
60917
+ null,
60918
+ 2,
60919
+ ),
60920
+ },
60921
+ ],
60922
+ };
60923
+ } catch (err) {
60924
+ return {
60925
+ content: [
60926
+ {
60927
+ type: "text",
60928
+ text: JSON.stringify({
60929
+ error: `Failed to search theme registry: ${err.message}`,
60930
+ }),
60931
+ },
60932
+ ],
60933
+ isError: true,
60934
+ };
60935
+ }
60936
+ }
60937
+
60938
+ /**
60939
+ * search_registry_dashboards — Search the online Dash registry for
60940
+ * pre-built dashboard templates.
60941
+ */
60942
+ async function handleSearchRegistryDashboards$1({
60943
+ query,
60944
+ compatibleWidgetsOnly = false,
60945
+ }) {
60946
+ if (!query || typeof query !== "string" || !query.trim()) {
60947
+ return {
60948
+ content: [
60949
+ {
60950
+ type: "text",
60951
+ text: JSON.stringify({
60952
+ error: "query is required and must be a non-empty string",
60953
+ }),
60954
+ },
60955
+ ],
60956
+ isError: true,
60957
+ };
60958
+ }
60959
+
60960
+ try {
60961
+ // If compatibility filter requested, compute the list of widget
60962
+ // scoped IDs the user currently has installed. The registry
60963
+ // filter in searchDashboards then prunes dashboards whose required
60964
+ // widgets aren't all present.
60965
+ let filters = {};
60966
+ if (compatibleWidgetsOnly) {
60967
+ const installedPkgs = getInstalledPackageNames();
60968
+ filters.compatibleWidgets = Array.from(installedPkgs);
60969
+ }
60970
+
60971
+ const result = await registryController$2.searchDashboards(
60972
+ query.trim(),
60973
+ filters,
60974
+ );
60975
+ const packages = result.packages || [];
60976
+
60977
+ const dashboards = packages.map((pkg) => ({
60978
+ name: pkg.name,
60979
+ scope: pkg.scope || null,
60980
+ displayName: pkg.displayName || pkg.name,
60981
+ description: pkg.description || "",
60982
+ icon: pkg.icon || null,
60983
+ requiredWidgets: pkg.requiredWidgets || pkg.widgets || [],
60984
+ preview: pkg.preview || null,
60985
+ }));
60986
+
60987
+ return {
60988
+ content: [
60989
+ {
60990
+ type: "text",
60991
+ text: JSON.stringify(
60992
+ {
60993
+ query: query.trim(),
60994
+ compatibleWidgetsOnly,
60995
+ dashboards,
60996
+ count: dashboards.length,
60997
+ },
60998
+ null,
60999
+ 2,
61000
+ ),
61001
+ },
61002
+ ],
61003
+ };
61004
+ } catch (err) {
61005
+ return {
61006
+ content: [
61007
+ {
61008
+ type: "text",
61009
+ text: JSON.stringify({
61010
+ error: `Failed to search dashboard registry: ${err.message}`,
61011
+ }),
61012
+ },
61013
+ ],
61014
+ isError: true,
61015
+ };
61016
+ }
61017
+ }
61018
+
60785
61019
  // --- Provider Tool Handlers ---
60786
61020
 
60787
61021
  const { PROVIDER_LIST_COMPLETE } = events$8;
@@ -61647,6 +61881,8 @@ var toolHandlers$1 = {
61647
61881
  handleCreateTheme: handleCreateTheme$1,
61648
61882
  handleCreateThemeFromUrl: handleCreateThemeFromUrl$1,
61649
61883
  handleApplyTheme: handleApplyTheme$1,
61884
+ handleSearchRegistryThemes: handleSearchRegistryThemes$1,
61885
+ handleSearchRegistryDashboards: handleSearchRegistryDashboards$1,
61650
61886
  handleListProviders: handleListProviders$1,
61651
61887
  handleAddProvider: handleAddProvider$1,
61652
61888
  handleRemoveProvider: handleRemoveProvider$1,
@@ -61715,6 +61951,8 @@ const dashToolHandlerMap = {
61715
61951
  create_theme: toolHandlers.handleCreateTheme,
61716
61952
  create_theme_from_url: toolHandlers.handleCreateThemeFromUrl,
61717
61953
  apply_theme: toolHandlers.handleApplyTheme,
61954
+ search_registry_themes: toolHandlers.handleSearchRegistryThemes,
61955
+ search_registry_dashboards: toolHandlers.handleSearchRegistryDashboards,
61718
61956
  list_providers: toolHandlers.handleListProviders,
61719
61957
  add_provider: toolHandlers.handleAddProvider,
61720
61958
  remove_provider: toolHandlers.handleRemoveProvider,
@@ -75031,6 +75269,7 @@ const {
75031
75269
  handleCreateDashboard,
75032
75270
  handleDeleteDashboard,
75033
75271
  handleGetAppStats,
75272
+ handleSearchRegistryDashboards,
75034
75273
  } = toolHandlers$1;
75035
75274
 
75036
75275
  // Map tool names to handler functions
@@ -75040,6 +75279,7 @@ const handlerMap$7 = {
75040
75279
  create_dashboard: handleCreateDashboard,
75041
75280
  delete_dashboard: handleDeleteDashboard,
75042
75281
  get_app_stats: handleGetAppStats,
75282
+ search_registry_dashboards: handleSearchRegistryDashboards,
75043
75283
  };
75044
75284
 
75045
75285
  /**
@@ -75131,6 +75371,7 @@ const {
75131
75371
  handleCreateTheme,
75132
75372
  handleCreateThemeFromUrl,
75133
75373
  handleApplyTheme,
75374
+ handleSearchRegistryThemes,
75134
75375
  } = toolHandlers$1;
75135
75376
 
75136
75377
  // Map tool names to handler functions
@@ -75140,6 +75381,7 @@ const handlerMap$5 = {
75140
75381
  create_theme: handleCreateTheme,
75141
75382
  create_theme_from_url: handleCreateThemeFromUrl,
75142
75383
  apply_theme: handleApplyTheme,
75384
+ search_registry_themes: handleSearchRegistryThemes,
75143
75385
  };
75144
75386
 
75145
75387
  /**