@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.
- package/dist/electron/index.js +1703 -1461
- package/dist/electron/index.js.map +1 -1
- package/package.json +1 -1
package/dist/electron/index.js
CHANGED
|
@@ -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
|
-
|
|
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
|
|
48811
|
+
var mcpDashServerController_1 = mcpDashServerController$4;
|
|
49314
48812
|
|
|
49315
48813
|
/**
|
|
49316
|
-
*
|
|
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
|
-
*
|
|
49322
|
-
*
|
|
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
|
-
*
|
|
49325
|
-
*
|
|
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
|
|
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
|
-
|
|
49339
|
-
|
|
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
|
|
49342
|
-
const { getOrCreateCert } = tlsCert;
|
|
48833
|
+
const IS_WINDOWS = process.platform === "win32";
|
|
49343
48834
|
|
|
49344
|
-
|
|
49345
|
-
|
|
49346
|
-
|
|
49347
|
-
|
|
49348
|
-
|
|
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
|
|
49362
|
-
|
|
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
|
|
49374
|
-
|
|
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
|
-
|
|
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
|
-
|
|
49391
|
-
|
|
49392
|
-
|
|
49393
|
-
let
|
|
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
|
-
|
|
49399
|
-
|
|
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
|
-
|
|
49404
|
-
|
|
49405
|
-
|
|
49406
|
-
|
|
49407
|
-
|
|
49408
|
-
|
|
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
|
-
|
|
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
|
-
*
|
|
49443
|
-
*
|
|
48897
|
+
* Active CLI processes for abort support.
|
|
48898
|
+
* Map<requestId, ChildProcess>
|
|
49444
48899
|
*/
|
|
49445
|
-
|
|
49446
|
-
registeredTools.push(toolDef);
|
|
49447
|
-
}
|
|
48900
|
+
const activeProcesses = new Map();
|
|
49448
48901
|
|
|
49449
48902
|
/**
|
|
49450
|
-
*
|
|
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
|
|
49453
|
-
|
|
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
|
-
*
|
|
49458
|
-
*
|
|
48930
|
+
* Session IDs for conversation continuity.
|
|
48931
|
+
* Map<widgetUuid, sessionId>
|
|
49459
48932
|
*/
|
|
49460
|
-
|
|
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
|
-
*
|
|
48936
|
+
* Send events safely to a window.
|
|
49469
48937
|
*/
|
|
49470
|
-
function
|
|
49471
|
-
|
|
49472
|
-
|
|
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
|
-
|
|
49517
|
-
|
|
49518
|
-
|
|
49519
|
-
|
|
49520
|
-
|
|
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
|
-
|
|
49524
|
-
|
|
49525
|
-
|
|
49526
|
-
|
|
49527
|
-
|
|
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
|
-
|
|
49531
|
-
|
|
49532
|
-
|
|
49533
|
-
|
|
49534
|
-
|
|
49535
|
-
|
|
49536
|
-
|
|
49537
|
-
|
|
49538
|
-
|
|
49539
|
-
|
|
49540
|
-
|
|
49541
|
-
|
|
49542
|
-
|
|
49543
|
-
|
|
49544
|
-
|
|
49545
|
-
|
|
49546
|
-
|
|
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
|
-
|
|
49558
|
-
|
|
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
|
-
|
|
49566
|
-
|
|
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
|
-
|
|
49581
|
-
|
|
49582
|
-
|
|
49583
|
-
|
|
49584
|
-
|
|
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
|
-
|
|
49587
|
-
|
|
49588
|
-
|
|
49589
|
-
|
|
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
|
-
|
|
49593
|
-
const
|
|
49594
|
-
const
|
|
49595
|
-
|
|
49596
|
-
|
|
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
|
-
|
|
49599
|
-
applyRegistrations(mcpServer);
|
|
49108
|
+
activeProcesses.set(requestId, child);
|
|
49600
49109
|
|
|
49601
|
-
//
|
|
49602
|
-
|
|
49603
|
-
|
|
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
|
-
|
|
49608
|
-
|
|
49609
|
-
|
|
49610
|
-
|
|
49611
|
-
|
|
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
|
-
//
|
|
49615
|
-
|
|
49616
|
-
|
|
49617
|
-
|
|
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
|
-
//
|
|
49623
|
-
if (
|
|
49624
|
-
|
|
49625
|
-
|
|
49626
|
-
|
|
49627
|
-
|
|
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
|
-
|
|
49631
|
-
|
|
49632
|
-
|
|
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
|
-
|
|
49635
|
-
|
|
49636
|
-
|
|
49637
|
-
|
|
49638
|
-
|
|
49639
|
-
|
|
49640
|
-
|
|
49641
|
-
|
|
49642
|
-
|
|
49643
|
-
|
|
49644
|
-
|
|
49645
|
-
|
|
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
|
-
//
|
|
49652
|
-
|
|
49653
|
-
|
|
49654
|
-
|
|
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
|
-
|
|
49666
|
-
|
|
49242
|
+
} catch {
|
|
49243
|
+
// ignore
|
|
49667
49244
|
}
|
|
49668
|
-
}
|
|
49669
|
-
);
|
|
49245
|
+
}
|
|
49670
49246
|
|
|
49671
|
-
|
|
49672
|
-
|
|
49673
|
-
|
|
49674
|
-
|
|
49675
|
-
|
|
49676
|
-
|
|
49677
|
-
|
|
49678
|
-
|
|
49679
|
-
|
|
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
|
-
|
|
49692
|
-
|
|
49693
|
-
|
|
49694
|
-
|
|
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
|
-
|
|
49697
|
-
|
|
49698
|
-
|
|
49699
|
-
|
|
49700
|
-
|
|
49701
|
-
|
|
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
|
-
|
|
49705
|
-
|
|
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
|
-
|
|
49709
|
-
|
|
49710
|
-
|
|
49711
|
-
|
|
49712
|
-
|
|
49713
|
-
|
|
49714
|
-
|
|
49715
|
-
|
|
49716
|
-
|
|
49717
|
-
|
|
49718
|
-
|
|
49719
|
-
|
|
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
|
-
|
|
49726
|
-
|
|
49727
|
-
|
|
49728
|
-
|
|
49729
|
-
|
|
49730
|
-
|
|
49731
|
-
|
|
49732
|
-
|
|
49733
|
-
|
|
49734
|
-
|
|
49735
|
-
|
|
49736
|
-
|
|
49737
|
-
|
|
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
|
-
|
|
49775
|
-
|
|
49776
|
-
|
|
49777
|
-
|
|
49778
|
-
|
|
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
|
-
|
|
49783
|
-
|
|
49784
|
-
|
|
49785
|
-
|
|
49786
|
-
|
|
49787
|
-
|
|
49788
|
-
|
|
49789
|
-
|
|
49790
|
-
|
|
49791
|
-
|
|
49792
|
-
|
|
49793
|
-
|
|
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
|
-
|
|
49799
|
-
|
|
49800
|
-
|
|
49801
|
-
|
|
49802
|
-
|
|
49803
|
-
|
|
49804
|
-
|
|
49805
|
-
|
|
49806
|
-
|
|
49807
|
-
|
|
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
|
-
|
|
49812
|
-
|
|
49813
|
-
|
|
49814
|
-
|
|
49815
|
-
|
|
49816
|
-
|
|
49817
|
-
|
|
49818
|
-
|
|
49819
|
-
|
|
49820
|
-
|
|
49821
|
-
|
|
49822
|
-
|
|
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
|
-
|
|
49826
|
-
|
|
49827
|
-
|
|
49828
|
-
|
|
49829
|
-
|
|
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
|
/**
|