clementine-agent 1.18.80 → 1.18.82

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.
@@ -100,6 +100,21 @@ export declare class PersonalAssistant {
100
100
  }>;
101
101
  updatedAt: string;
102
102
  };
103
+ /**
104
+ * PRD Phase 2.1: clear the cached status for one server so the next query
105
+ * repopulates it from a fresh handshake. The SDK manages connections
106
+ * internally; we don't have a direct "reconnect now" hook, but invalidating
107
+ * the cached entry tells the dashboard to render 'pending' and resets any
108
+ * stale error/auth state. Returns the post-clear cached snapshot.
109
+ */
110
+ invalidateMcpStatus(serverName: string): {
111
+ servers: Array<{
112
+ name: string;
113
+ status: string;
114
+ }>;
115
+ updatedAt: string;
116
+ cleared: boolean;
117
+ };
103
118
  /** Inject a background work result into the session as silent follow-up context. */
104
119
  injectPendingContext(sessionKey: string, userPrompt: string, result: string): void;
105
120
  private initMemoryStore;
@@ -810,6 +810,21 @@ export class PersonalAssistant {
810
810
  getMcpStatus() {
811
811
  return { servers: this._lastMcpStatus, updatedAt: this._lastMcpStatusTime };
812
812
  }
813
+ /**
814
+ * PRD Phase 2.1: clear the cached status for one server so the next query
815
+ * repopulates it from a fresh handshake. The SDK manages connections
816
+ * internally; we don't have a direct "reconnect now" hook, but invalidating
817
+ * the cached entry tells the dashboard to render 'pending' and resets any
818
+ * stale error/auth state. Returns the post-clear cached snapshot.
819
+ */
820
+ invalidateMcpStatus(serverName) {
821
+ const beforeLen = this._lastMcpStatus.length;
822
+ this._lastMcpStatus = this._lastMcpStatus.filter((s) => s.name !== serverName);
823
+ const cleared = this._lastMcpStatus.length < beforeLen;
824
+ if (cleared)
825
+ this._lastMcpStatusTime = new Date().toISOString();
826
+ return { servers: this._lastMcpStatus, updatedAt: this._lastMcpStatusTime, cleared };
827
+ }
813
828
  /** Inject a background work result into the session as silent follow-up context. */
814
829
  injectPendingContext(sessionKey, userPrompt, result) {
815
830
  const pending = this.pendingContext.get(sessionKey) ?? [];
@@ -9266,6 +9266,31 @@ If the tool returns nothing or errors, return an empty array \`[]\`.`,
9266
9266
  app.get('/api/mcp-status', gwHandler(async (gw, _req, res) => {
9267
9267
  res.json(gw.getMcpStatus());
9268
9268
  }));
9269
+ // PRD Phase 2.1: Reconnect a single MCP server. Clears the cached status so
9270
+ // the next query handshake repopulates it. The SDK doesn't expose a direct
9271
+ // reconnect call, so this is the closest equivalent: kick the cache.
9272
+ app.post('/api/mcp-servers/:name/reconnect', gwHandler(async (gw, req, res) => {
9273
+ const rawName = req.params.name;
9274
+ const name = Array.isArray(rawName) ? rawName[0] : rawName;
9275
+ if (!name) {
9276
+ res.status(400).json({ ok: false, error: 'name required' });
9277
+ return;
9278
+ }
9279
+ try {
9280
+ const result = gw.invalidateMcpStatus(String(name));
9281
+ res.json({
9282
+ ok: true,
9283
+ cleared: result.cleared,
9284
+ message: result.cleared
9285
+ ? `Reconnect queued for "${name}" — status will refresh on the next query.`
9286
+ : `"${name}" had no cached status to clear; next query will populate it.`,
9287
+ status: result,
9288
+ });
9289
+ }
9290
+ catch (err) {
9291
+ res.status(500).json({ ok: false, error: String(err) });
9292
+ }
9293
+ }));
9269
9294
  // ── Self-Improvement API ─────────────────────────────────────────
9270
9295
  // ── MCP Server Management API ───────────────────────────────────────
9271
9296
  app.get('/api/mcp-servers', (_req, res) => {
@@ -16356,14 +16381,18 @@ if('serviceWorker' in navigator){navigator.serviceWorker.getRegistrations().then
16356
16381
 
16357
16382
  <!-- ═══ Tasks Page — single unified surface ═══ -->
16358
16383
  <div class="page" id="page-build">
16359
- <!-- Sub-tabs hidden by default. Cron is the single surface; multi-step
16360
- workflows (formerly "Tricks") are still accessible via deep-link
16361
- ?tab=workflows or by toggling .show-workflows-tabs on the page. -->
16362
- <div id="build-tabs" style="display:none;gap:4px;padding:8px 18px 0;background:var(--bg-secondary);border-bottom:1px solid var(--border);flex-shrink:0">
16384
+ <!-- PRD Phase 2: top-level tab strip within the Tasks domain.
16385
+ Tasks (default) + Tools & MCP catalog. Workflows still reachable
16386
+ via deep-link ?tab=workflows for power users with existing
16387
+ multi-step workflows. -->
16388
+ <div id="build-tabs" style="display:flex;gap:4px;padding:8px 18px 0;background:var(--bg-secondary);border-bottom:1px solid var(--border);flex-shrink:0">
16363
16389
  <button class="build-tab-btn active" data-build-tab="crons" onclick="switchBuildTab('crons')" style="padding:8px 14px;border-radius:6px 6px 0 0;border:none;background:transparent;color:var(--text-primary);font-size:13px;font-weight:500;cursor:pointer;border-bottom:2px solid transparent">
16364
16390
  <span style="margin-right:6px">📅</span>Tasks <span id="build-tab-cron-count" style="display:none;margin-left:4px;font-size:10px;background:var(--bg-tertiary);padding:1px 6px;border-radius:999px;color:var(--text-muted)">0</span>
16365
16391
  </button>
16366
- <button class="build-tab-btn" data-build-tab="workflows" onclick="switchBuildTab('workflows')" style="padding:8px 14px;border-radius:6px 6px 0 0;border:none;background:transparent;color:var(--text-secondary);font-size:13px;font-weight:500;cursor:pointer;border-bottom:2px solid transparent">
16392
+ <button class="build-tab-btn" data-build-tab="toolsmcp" onclick="switchBuildTab('toolsmcp')" style="padding:8px 14px;border-radius:6px 6px 0 0;border:none;background:transparent;color:var(--text-secondary);font-size:13px;font-weight:500;cursor:pointer;border-bottom:2px solid transparent">
16393
+ <span style="margin-right:6px">🧰</span>Tools &amp; MCP <span id="build-tab-toolsmcp-count" style="display:none;margin-left:4px;font-size:10px;background:var(--bg-tertiary);padding:1px 6px;border-radius:999px;color:var(--text-muted)">0</span>
16394
+ </button>
16395
+ <button class="build-tab-btn" data-build-tab="workflows" onclick="switchBuildTab('workflows')" style="display:none;padding:8px 14px;border-radius:6px 6px 0 0;border:none;background:transparent;color:var(--text-secondary);font-size:13px;font-weight:500;cursor:pointer;border-bottom:2px solid transparent">
16367
16396
  <span style="margin-right:6px">🔧</span>Workflows <span id="build-tab-workflows-count" style="display:none;margin-left:4px;font-size:10px;background:var(--bg-tertiary);padding:1px 6px;border-radius:999px;color:var(--text-muted)">0</span>
16368
16397
  </button>
16369
16398
  </div>
@@ -16375,6 +16404,12 @@ if('serviceWorker' in navigator){navigator.serviceWorker.getRegistrations().then
16375
16404
  <div id="build-tab-crons" style="display:none;flex:1;min-height:0;overflow-y:auto;padding:18px;background:var(--bg-primary)">
16376
16405
  <div id="panel-cron"><div class="empty-state" style="padding:24px;color:var(--text-muted)">Loading scheduled tasks…</div></div>
16377
16406
  </div>
16407
+ <!-- ── PRD Phase 2: Tools & MCP catalog ────────────────────────────────
16408
+ Read-only foundation in 1.18.81. Future slices: per-tool bindings,
16409
+ Reconnect/Toggle/Edit actions, Approval Mode + Max-auto-runs config. -->
16410
+ <div id="build-tab-toolsmcp" style="display:none;flex:1;min-height:0;overflow-y:auto;padding:18px;background:var(--bg-primary)">
16411
+ <div id="panel-toolsmcp"><div class="empty-state" style="padding:24px;color:var(--text-muted)">Loading Tools &amp; MCP catalog…</div></div>
16412
+ </div>
16378
16413
  <!-- Tricks (workflows) tab — existing RoutinesUI surface ─────────────────── -->
16379
16414
  <div id="build-tab-workflows" style="display:none;flex:1;min-height:0;display:flex;flex-direction:column">
16380
16415
  <!-- Toolbar -->
@@ -20309,6 +20344,77 @@ if('serviceWorker' in navigator){navigator.serviceWorker.getRegistrations().then
20309
20344
 
20310
20345
  <!-- (legacy standalone Preview modal removed in 1.18.70 — preview now lives as a tab inside the cron modal) -->
20311
20346
 
20347
+ <!-- ═══ MCP Server Edit Modal — PRD Phase 2.1 ═══ -->
20348
+ <div class="modal-overlay" id="mcp-edit-modal">
20349
+ <div class="modal" style="max-width:640px;width:96vw">
20350
+ <div class="modal-header">
20351
+ <h3 id="mcp-edit-title">Edit MCP Server</h3>
20352
+ <button class="btn-ghost btn-sm" onclick="closeMcpServerEditModal()">&times;</button>
20353
+ </div>
20354
+ <div class="modal-body" style="padding:18px">
20355
+ <div id="mcp-edit-readonly-note" style="display:none;margin-bottom:14px;padding:10px 12px;border-radius:6px;background:rgba(245,158,11,0.10);border:1px solid rgba(245,158,11,0.30);color:var(--yellow);font-size:12px">
20356
+ ⚠ Auto-detected server. Edits to this config aren't persisted by Clementine — change it in the source file (Claude Desktop config, Claude Code settings, or extensions). Use the toggle on the catalog card to enable/disable.
20357
+ </div>
20358
+ <div class="form-group">
20359
+ <label class="form-label">Name</label>
20360
+ <input type="text" id="mcp-edit-name">
20361
+ <div class="form-hint">Identifier — cannot be renamed via this dialog.</div>
20362
+ </div>
20363
+ <div class="form-row">
20364
+ <div class="form-group">
20365
+ <label class="form-label">Transport</label>
20366
+ <select id="mcp-edit-type" onchange="syncMcpEditTransportRows()">
20367
+ <option value="stdio">stdio (local process)</option>
20368
+ <option value="http">http</option>
20369
+ <option value="sse">sse</option>
20370
+ </select>
20371
+ </div>
20372
+ <div class="form-group">
20373
+ <label class="form-label" style="display:flex;align-items:center;gap:8px">
20374
+ <input type="checkbox" id="mcp-edit-enabled"> Enabled
20375
+ </label>
20376
+ <div class="form-hint">Disable to remove this server from every task without deleting it.</div>
20377
+ </div>
20378
+ </div>
20379
+ <div class="form-group">
20380
+ <label class="form-label">Description</label>
20381
+ <input type="text" id="mcp-edit-description" placeholder="What this server does">
20382
+ </div>
20383
+ <!-- stdio-only fields -->
20384
+ <div id="mcp-edit-stdio-rows">
20385
+ <div class="form-group">
20386
+ <label class="form-label">Command</label>
20387
+ <input type="text" id="mcp-edit-command" placeholder="e.g. npx, python, ./bin/server">
20388
+ </div>
20389
+ <div class="form-group">
20390
+ <label class="form-label">Args <span style="color:var(--text-muted);font-weight:normal">(one per line)</span></label>
20391
+ <textarea id="mcp-edit-args" rows="3" placeholder="--port&#10;3001" style="font-family:'JetBrains Mono',monospace;font-size:11px"></textarea>
20392
+ </div>
20393
+ <div class="form-group">
20394
+ <label class="form-label">Env <span style="color:var(--text-muted);font-weight:normal">(JSON object, optional)</span></label>
20395
+ <textarea id="mcp-edit-env" rows="3" placeholder='{ "API_KEY": "..." }' style="font-family:'JetBrains Mono',monospace;font-size:11px"></textarea>
20396
+ </div>
20397
+ </div>
20398
+ <!-- http/sse fields -->
20399
+ <div id="mcp-edit-http-rows" style="display:none">
20400
+ <div class="form-group">
20401
+ <label class="form-label">URL</label>
20402
+ <input type="text" id="mcp-edit-url" placeholder="https://example.com/mcp">
20403
+ </div>
20404
+ <div class="form-group">
20405
+ <label class="form-label">Headers <span style="color:var(--text-muted);font-weight:normal">(JSON object, optional)</span></label>
20406
+ <textarea id="mcp-edit-headers" rows="3" placeholder='{ "Authorization": "Bearer ..." }' style="font-family:'JetBrains Mono',monospace;font-size:11px"></textarea>
20407
+ </div>
20408
+ </div>
20409
+ </div>
20410
+ <div class="modal-footer">
20411
+ <span style="flex:1"></span>
20412
+ <button onclick="closeMcpServerEditModal()">Close</button>
20413
+ <button class="btn-primary" id="mcp-edit-save" onclick="saveMcpServerEdit()">Save</button>
20414
+ </div>
20415
+ </div>
20416
+ </div>
20417
+
20312
20418
  <!-- ═══ Goal Modal ═══ -->
20313
20419
  <div class="modal-overlay" id="goal-modal">
20314
20420
  <div class="modal" style="width:520px">
@@ -21148,12 +21254,27 @@ function switchBuildTab(tab) {
21148
21254
  var workPane = document.getElementById('build-tab-workflows');
21149
21255
  var cronPane = document.getElementById('build-tab-crons');
21150
21256
  var tplPane = document.getElementById('build-tab-templates');
21257
+ var toolsmcpPane = document.getElementById('build-tab-toolsmcp');
21151
21258
  var headerStrip = document.getElementById('build-header-strip');
21152
21259
  var usagePanel = document.getElementById('build-usage-panel');
21153
21260
  var newBtn = document.getElementById('builder-new-btn');
21154
21261
  // Always close any open workflow when changing tabs — switching context
21155
21262
  // is a clean slate, not a stale node hanging on the canvas.
21156
21263
  if (typeof closeBuilderCanvas === 'function') closeBuilderCanvas();
21264
+ // Default: hide the Tools & MCP pane unless we're explicitly on it.
21265
+ if (toolsmcpPane && tab !== 'toolsmcp') toolsmcpPane.style.display = 'none';
21266
+ if (tab === 'toolsmcp') {
21267
+ // PRD Phase 2: Tools & MCP catalog. Read-only foundation in 1.18.81.
21268
+ if (workPane) workPane.style.display = 'none';
21269
+ if (cronPane) cronPane.style.display = 'none';
21270
+ if (tplPane) tplPane.style.display = 'none';
21271
+ if (toolsmcpPane) toolsmcpPane.style.display = 'block';
21272
+ if (headerStrip) headerStrip.style.display = 'none';
21273
+ if (usagePanel) usagePanel.style.display = 'none';
21274
+ if (newBtn) newBtn.style.display = 'none';
21275
+ if (typeof refreshToolsMcpCatalog === 'function') refreshToolsMcpCatalog();
21276
+ return;
21277
+ }
21157
21278
  if (tab === 'templates') {
21158
21279
  if (workPane) workPane.style.display = 'none';
21159
21280
  if (cronPane) cronPane.style.display = 'none';
@@ -23532,6 +23653,301 @@ function renderRunningCard(item) {
23532
23653
  + '</div></div>';
23533
23654
  }
23534
23655
 
23656
+ // ── PRD Phase 2: Tools & MCP catalog ──────────────────────────────────
23657
+ // Read-only foundation in 1.18.81. Renders the four-card taxonomy:
23658
+ // • Built-in — Claude SDK native tools (Read/Write/Bash/etc.)
23659
+ // • Custom MCP — in-process SDK MCP servers
23660
+ // • Shell command — CLI wrappers
23661
+ // • External MCP — stdio / sse / http MCP servers
23662
+ // Pulls from /api/mcp-status (live status) + /api/mcp-servers (config).
23663
+ // Future slices wire Reconnect/Toggle/Edit + the McpToolBinding modal.
23664
+ async function refreshToolsMcpCatalog() {
23665
+ var panel = document.getElementById('panel-toolsmcp');
23666
+ if (!panel) return;
23667
+ panel.innerHTML = '<div class="empty-state" style="padding:24px;color:var(--text-muted)">Loading Tools &amp; MCP catalog…</div>';
23668
+ var statusMap = {};
23669
+ var servers = [];
23670
+ try {
23671
+ var sR = await apiFetch('/api/mcp-status');
23672
+ var statusJson = await sR.json();
23673
+ statusMap = statusJson || {};
23674
+ } catch (e) { /* status is optional — servers still render without it */ }
23675
+ try {
23676
+ var lR = await apiFetch('/api/mcp-servers');
23677
+ var lJson = await lR.json();
23678
+ servers = (lJson && lJson.servers) || [];
23679
+ } catch (e) {
23680
+ panel.innerHTML = '<div class="empty-state" style="padding:24px;color:var(--red)">Failed to load MCP servers: ' + esc(String(e)) + '</div>';
23681
+ return;
23682
+ }
23683
+ var tabCount = document.getElementById('build-tab-toolsmcp-count');
23684
+ if (tabCount) {
23685
+ tabCount.textContent = servers.length;
23686
+ tabCount.style.display = servers.length > 0 ? '' : 'none';
23687
+ }
23688
+ // Bucket servers into the four PRD categories. The existing
23689
+ // ManagedMcpServer type doesn't have an explicit "kind" field, so we
23690
+ // infer: stdio with a known shell binary → 'shell', stdio bundled with
23691
+ // clementine → 'builtin', stdio external command → 'external_stdio',
23692
+ // http/sse → 'external_remote'. The bucket keys map to the PRD's four
23693
+ // taxonomy cards.
23694
+ var buckets = { builtin: [], custom: [], shell: [], external: [] };
23695
+ for (var i = 0; i < servers.length; i++) {
23696
+ var s = servers[i];
23697
+ var name = s.name || '';
23698
+ var type = s.type || 'stdio';
23699
+ var cmd = s.command || '';
23700
+ var kind;
23701
+ // The clementine-tools server is an in-process bundle
23702
+ if (name === 'clementine-tools' || name === 'kernel') kind = 'builtin';
23703
+ else if (type === 'http' || type === 'sse') kind = 'external';
23704
+ else if (/^(sf|gh|gcloud|kubectl|docker|aws|az|terraform)$/.test(cmd) || /\\b(sf|gh|gcloud|kubectl)$/.test(cmd)) kind = 'shell';
23705
+ else kind = 'external'; // default for stdio external MCP
23706
+ buckets[kind].push(s);
23707
+ }
23708
+ var html = '';
23709
+ // Header strip
23710
+ html += '<div style="margin-bottom:18px"><h2 style="margin:0 0 4px;font-size:18px;font-weight:600;color:var(--text-primary)">Tools &amp; MCP catalog</h2>'
23711
+ + '<div style="font-size:12px;color:var(--text-muted)">'+ esc(servers.length) +' MCP server' + (servers.length === 1 ? '' : 's') + ' configured. Click any task in the Tasks tab to bind specific tools to that task.</div></div>';
23712
+ // Four-card taxonomy. Each section is a labeled bucket of cards.
23713
+ var sections = [
23714
+ { key: 'builtin', label: 'Built-in', desc: 'Claude SDK native tools — always available to every task at the agent profile\\x27s permission tier.' },
23715
+ { key: 'custom', label: 'Custom in-process MCP', desc: 'MCP servers defined in clementine\\x27s code, loaded inside the daemon process.' },
23716
+ { key: 'shell', label: 'Shell commands', desc: 'Local CLI binaries (sf, gh, gcloud…) wrapped as MCP servers.' },
23717
+ { key: 'external', label: 'External MCP servers', desc: 'Third-party MCP servers reached over stdio, SSE, or HTTP.' },
23718
+ ];
23719
+ for (var k = 0; k < sections.length; k++) {
23720
+ var sec = sections[k];
23721
+ var bucket = buckets[sec.key] || [];
23722
+ html += '<div style="margin-bottom:24px">';
23723
+ html += '<div style="display:flex;align-items:baseline;gap:10px;margin-bottom:10px">'
23724
+ + '<h3 style="margin:0;font-size:14px;font-weight:600;color:var(--text-primary)">' + esc(sec.label) + '</h3>'
23725
+ + '<span style="font-size:11px;color:var(--text-muted);font-weight:500">' + bucket.length + '</span>'
23726
+ + '</div>';
23727
+ html += '<div style="font-size:11px;color:var(--text-muted);margin-bottom:12px">' + esc(sec.desc) + '</div>';
23728
+ if (bucket.length === 0) {
23729
+ html += '<div class="empty-state" style="padding:14px;color:var(--text-muted);font-size:12px;background:var(--bg-secondary);border:1px dashed var(--border);border-radius:6px">No servers in this bucket.</div>';
23730
+ } else {
23731
+ html += '<div style="display:grid;grid-template-columns:repeat(auto-fill,minmax(280px,1fr));gap:12px">';
23732
+ for (var b = 0; b < bucket.length; b++) html += renderMcpCatalogCard(bucket[b], statusMap);
23733
+ html += '</div>';
23734
+ }
23735
+ html += '</div>';
23736
+ }
23737
+ panel.innerHTML = html;
23738
+ }
23739
+
23740
+ // Render one MCP server card. Status pill colors mirror the PRD's five
23741
+ // states (connected / failed / needs-auth / pending / disabled). The
23742
+ // statusMap shape comes from gw.getMcpStatus() — varies a bit between
23743
+ // SDK versions; we defensively probe for connected/healthy fields.
23744
+ function renderMcpCatalogCard(server, statusMap) {
23745
+ var name = server.name || '(unnamed)';
23746
+ var transport = server.type || 'stdio';
23747
+ var enabled = server.enabled !== false;
23748
+ var status = statusMap && statusMap[name];
23749
+ var statusKind, statusLabel, statusColor;
23750
+ if (!enabled) { statusKind = 'disabled'; statusLabel = 'disabled'; statusColor = 'var(--text-muted)'; }
23751
+ else if (status && (status.connected === true || status.status === 'connected' || status.healthy === true)) { statusKind = 'connected'; statusLabel = 'connected'; statusColor = 'var(--green)'; }
23752
+ else if (status && (status.needsAuth === true || status.status === 'needs-auth')) { statusKind = 'needsauth'; statusLabel = 'needs auth'; statusColor = 'var(--yellow)'; }
23753
+ else if (status && (status.connected === false || status.status === 'failed' || status.error)) { statusKind = 'failed'; statusLabel = 'failed'; statusColor = 'var(--red)'; }
23754
+ else { statusKind = 'pending'; statusLabel = 'pending'; statusColor = 'var(--text-muted)'; }
23755
+ var toolCount = (status && (status.toolCount != null ? status.toolCount : (Array.isArray(status.tools) ? status.tools.length : null))) || (Array.isArray(server.exposedTools) ? server.exposedTools.length : null);
23756
+ var lastChecked = status && (status.lastCheckedAt || status.checkedAt || status.updatedAt);
23757
+ var src = server.source === 'auto-detected' ? 'auto-detected' : 'user-configured';
23758
+ var lastError = status && status.error;
23759
+ var html = ''
23760
+ + '<div style="background:var(--bg-secondary);border:1px solid var(--border);border-radius:8px;padding:14px;display:flex;flex-direction:column;gap:8px">'
23761
+ + '<div style="display:flex;align-items:center;gap:8px">'
23762
+ + '<div style="font-size:13px;font-weight:600;color:var(--text-primary);flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap" title="' + esc(name) + '">' + esc(name) + '</div>'
23763
+ + '<span style="display:inline-flex;align-items:center;gap:5px;font-size:10px;font-weight:500;color:' + statusColor + ';background:' + statusColor + '20;padding:2px 8px;border-radius:999px;text-transform:uppercase;letter-spacing:0.04em">'
23764
+ + '<span style="display:inline-block;width:6px;height:6px;border-radius:50%;background:' + statusColor + '"></span>' + esc(statusLabel)
23765
+ + '</span>'
23766
+ + '</div>'
23767
+ + '<div style="display:flex;align-items:center;gap:10px;font-size:11px;color:var(--text-muted)">'
23768
+ + '<span style="text-transform:uppercase;letter-spacing:0.04em">' + esc(transport) + '</span>'
23769
+ + '<span>·</span>'
23770
+ + '<span>' + esc(src) + '</span>'
23771
+ + (toolCount != null ? '<span>·</span><span>' + esc(toolCount) + ' tool' + (toolCount === 1 ? '' : 's') + '</span>' : '')
23772
+ + '</div>'
23773
+ + (server.description ? '<div style="font-size:12px;color:var(--text-secondary);line-height:1.45">' + esc(String(server.description).slice(0, 240)) + '</div>' : '')
23774
+ + (lastError ? '<div style="font-size:11px;color:var(--red);background:rgba(239,68,68,0.06);padding:6px 8px;border-radius:4px;word-break:break-word">' + esc(String(lastError).slice(0, 200)) + '</div>' : '')
23775
+ + (lastChecked ? '<div style="font-size:11px;color:var(--text-muted)">Checked ' + esc(timeAgo(lastChecked)) + '</div>' : '')
23776
+ + '<div style="display:flex;gap:6px;margin-top:4px;flex-wrap:wrap">'
23777
+ + '<button class="btn-sm" onclick="toggleMcpServerEnabled(\\x27' + jsStr(name) + '\\x27,' + (enabled ? 'false' : 'true') + ')" title="' + (enabled ? 'Disable this MCP server for all tasks' : 'Enable this MCP server') + '">' + (enabled ? 'Disable' : 'Enable') + '</button>'
23778
+ + '<button class="btn-sm" onclick="reconnectMcpServer(\\x27' + jsStr(name) + '\\x27)" title="Clear cached status — next query will reconnect">Reconnect</button>'
23779
+ + '<button class="btn-sm" onclick="openMcpServerEditModal(\\x27' + jsStr(name) + '\\x27)" title="View or edit this server\\x27s config">Edit</button>'
23780
+ + '</div>'
23781
+ + '</div>';
23782
+ return html;
23783
+ }
23784
+
23785
+ // PRD Phase 2.1: Reconnect — invalidate cached status server-side, refresh
23786
+ // the catalog so the user sees the pending pill until the next query
23787
+ // handshake repopulates it.
23788
+ async function reconnectMcpServer(name) {
23789
+ try {
23790
+ var r = await apiFetch('/api/mcp-servers/' + encodeURIComponent(name) + '/reconnect', { method: 'POST' });
23791
+ var d = await r.json();
23792
+ if (!r.ok || d.ok === false) {
23793
+ toast('Reconnect failed: ' + (d.error || 'unknown'), 'error');
23794
+ return;
23795
+ }
23796
+ toast(d.message || 'Reconnect queued.', 'info');
23797
+ refreshToolsMcpCatalog();
23798
+ } catch (e) {
23799
+ toast('Reconnect failed: ' + String(e), 'error');
23800
+ }
23801
+ }
23802
+
23803
+ // PRD Phase 2.1: Edit modal. User-managed servers get an editable config
23804
+ // form; auto-detected servers render the same fields read-only with a note
23805
+ // pointing the user at the underlying config file.
23806
+ async function openMcpServerEditModal(name) {
23807
+ // Pull the latest server config — don't trust whatever was on the rendered card.
23808
+ var server;
23809
+ try {
23810
+ var r = await apiFetch('/api/mcp-servers');
23811
+ var d = await r.json();
23812
+ server = (d && d.servers || []).find(function(s) { return s.name === name; });
23813
+ } catch (e) {
23814
+ toast('Failed to load server: ' + String(e), 'error');
23815
+ return;
23816
+ }
23817
+ if (!server) { toast('Server "' + name + '" not found', 'error'); return; }
23818
+ var modal = document.getElementById('mcp-edit-modal');
23819
+ if (!modal) { toast('Edit modal missing from DOM', 'error'); return; }
23820
+ document.getElementById('mcp-edit-title').textContent = 'Edit: ' + name;
23821
+ var roNote = document.getElementById('mcp-edit-readonly-note');
23822
+ var isReadOnly = server.source === 'auto-detected';
23823
+ if (roNote) roNote.style.display = isReadOnly ? '' : 'none';
23824
+ // Set fields
23825
+ document.getElementById('mcp-edit-name').value = server.name || '';
23826
+ document.getElementById('mcp-edit-name').disabled = true; // never rename via this path
23827
+ document.getElementById('mcp-edit-type').value = server.type || 'stdio';
23828
+ document.getElementById('mcp-edit-type').disabled = isReadOnly;
23829
+ document.getElementById('mcp-edit-description').value = server.description || '';
23830
+ document.getElementById('mcp-edit-description').disabled = isReadOnly;
23831
+ document.getElementById('mcp-edit-enabled').checked = server.enabled !== false;
23832
+ document.getElementById('mcp-edit-enabled').disabled = isReadOnly;
23833
+ document.getElementById('mcp-edit-command').value = server.command || '';
23834
+ document.getElementById('mcp-edit-command').disabled = isReadOnly;
23835
+ document.getElementById('mcp-edit-args').value = Array.isArray(server.args) ? server.args.join('\\n') : '';
23836
+ document.getElementById('mcp-edit-args').disabled = isReadOnly;
23837
+ document.getElementById('mcp-edit-url').value = server.url || '';
23838
+ document.getElementById('mcp-edit-url').disabled = isReadOnly;
23839
+ document.getElementById('mcp-edit-headers').value = server.headers && Object.keys(server.headers).length ? JSON.stringify(server.headers, null, 2) : '';
23840
+ document.getElementById('mcp-edit-headers').disabled = isReadOnly;
23841
+ document.getElementById('mcp-edit-env').value = server.env && Object.keys(server.env).length ? JSON.stringify(server.env, null, 2) : '';
23842
+ document.getElementById('mcp-edit-env').disabled = isReadOnly;
23843
+ // Show the right transport fields
23844
+ syncMcpEditTransportRows();
23845
+ // Save button hidden for read-only auto-detected servers.
23846
+ var saveBtn = document.getElementById('mcp-edit-save');
23847
+ if (saveBtn) saveBtn.style.display = isReadOnly ? 'none' : '';
23848
+ modal.classList.add('show');
23849
+ }
23850
+
23851
+ function closeMcpServerEditModal() {
23852
+ var modal = document.getElementById('mcp-edit-modal');
23853
+ if (modal) modal.classList.remove('show');
23854
+ }
23855
+
23856
+ // Show only the row matching the selected transport (stdio vs http/sse).
23857
+ function syncMcpEditTransportRows() {
23858
+ var t = (document.getElementById('mcp-edit-type') || {}).value || 'stdio';
23859
+ var stdioRow = document.getElementById('mcp-edit-stdio-rows');
23860
+ var httpRow = document.getElementById('mcp-edit-http-rows');
23861
+ if (stdioRow) stdioRow.style.display = (t === 'stdio') ? '' : 'none';
23862
+ if (httpRow) httpRow.style.display = (t === 'http' || t === 'sse') ? '' : 'none';
23863
+ }
23864
+
23865
+ async function saveMcpServerEdit() {
23866
+ var name = (document.getElementById('mcp-edit-name') || {}).value;
23867
+ if (!name) return;
23868
+ var type = document.getElementById('mcp-edit-type').value;
23869
+ var description = document.getElementById('mcp-edit-description').value.trim();
23870
+ var enabled = !!document.getElementById('mcp-edit-enabled').checked;
23871
+ var commandRaw = document.getElementById('mcp-edit-command').value.trim();
23872
+ var argsRaw = document.getElementById('mcp-edit-args').value;
23873
+ var url = document.getElementById('mcp-edit-url').value.trim();
23874
+ var headersRaw = document.getElementById('mcp-edit-headers').value.trim();
23875
+ var envRaw = document.getElementById('mcp-edit-env').value.trim();
23876
+
23877
+ var headers, env;
23878
+ if (headersRaw) {
23879
+ try {
23880
+ headers = JSON.parse(headersRaw);
23881
+ if (!headers || typeof headers !== 'object' || Array.isArray(headers)) throw new Error('Headers must be a JSON object');
23882
+ } catch (e) {
23883
+ toast('Headers JSON invalid: ' + (e.message || String(e)), 'error');
23884
+ document.getElementById('mcp-edit-headers').focus();
23885
+ return;
23886
+ }
23887
+ }
23888
+ if (envRaw) {
23889
+ try {
23890
+ env = JSON.parse(envRaw);
23891
+ if (!env || typeof env !== 'object' || Array.isArray(env)) throw new Error('Env must be a JSON object');
23892
+ } catch (e) {
23893
+ toast('Env JSON invalid: ' + (e.message || String(e)), 'error');
23894
+ document.getElementById('mcp-edit-env').focus();
23895
+ return;
23896
+ }
23897
+ }
23898
+ var args = argsRaw.split(/\\r?\\n/).map(function(s){ return s.trim(); }).filter(Boolean);
23899
+ var body = { type: type, description: description, enabled: enabled };
23900
+ if (type === 'stdio') {
23901
+ if (!commandRaw) { toast('Command is required for stdio transport', 'error'); document.getElementById('mcp-edit-command').focus(); return; }
23902
+ body.command = commandRaw;
23903
+ body.args = args;
23904
+ if (env) body.env = env;
23905
+ } else {
23906
+ if (!url) { toast('URL is required for ' + type + ' transport', 'error'); document.getElementById('mcp-edit-url').focus(); return; }
23907
+ body.url = url;
23908
+ if (headers) body.headers = headers;
23909
+ }
23910
+ try {
23911
+ var r = await apiFetch('/api/mcp-servers/' + encodeURIComponent(name), {
23912
+ method: 'PUT',
23913
+ headers: { 'Content-Type': 'application/json' },
23914
+ body: JSON.stringify(body),
23915
+ });
23916
+ var d = await r.json();
23917
+ if (!r.ok || d.error) {
23918
+ toast('Save failed: ' + (d.error || 'unknown'), 'error');
23919
+ return;
23920
+ }
23921
+ toast('Saved.', 'success');
23922
+ closeMcpServerEditModal();
23923
+ refreshToolsMcpCatalog();
23924
+ } catch (e) {
23925
+ toast('Save failed: ' + String(e), 'error');
23926
+ }
23927
+ }
23928
+
23929
+ // PUT helper for the Toggle button. Lazy: re-fetches the catalog after
23930
+ // the round-trip so the new state is reflected. Future slice will swap
23931
+ // to optimistic update + rollback on error.
23932
+ async function toggleMcpServerEnabled(name, nextEnabled) {
23933
+ try {
23934
+ var r = await apiFetch('/api/mcp-servers/' + encodeURIComponent(name), {
23935
+ method: 'PUT',
23936
+ headers: { 'Content-Type': 'application/json' },
23937
+ body: JSON.stringify({ enabled: nextEnabled }),
23938
+ });
23939
+ var d = await r.json();
23940
+ if (!r.ok || d.error) {
23941
+ toast('Toggle failed: ' + (d.error || 'unknown'), 'error');
23942
+ return;
23943
+ }
23944
+ toast(name + ' is now ' + (nextEnabled ? 'enabled' : 'disabled'), 'success');
23945
+ refreshToolsMcpCatalog();
23946
+ } catch (e) {
23947
+ toast('Toggle failed: ' + String(e), 'error');
23948
+ }
23949
+ }
23950
+
23535
23951
  async function refreshCron() {
23536
23952
  try {
23537
23953
  // Fetch operations + cross-job recent runs in parallel for the three-zone
@@ -255,6 +255,15 @@ export declare class Gateway {
255
255
  }>;
256
256
  updatedAt: string;
257
257
  };
258
+ /** PRD Phase 2.1: thin pass-through for the dashboard's Reconnect button. */
259
+ invalidateMcpStatus(serverName: string): {
260
+ servers: Array<{
261
+ name: string;
262
+ status: string;
263
+ }>;
264
+ updatedAt: string;
265
+ cleared: boolean;
266
+ };
258
267
  getPresenceInfo(sessionKey: string): {
259
268
  model: string;
260
269
  project: string | null;
@@ -2241,6 +2241,10 @@ export class Gateway {
2241
2241
  getMcpStatus() {
2242
2242
  return this.assistant.getMcpStatus();
2243
2243
  }
2244
+ /** PRD Phase 2.1: thin pass-through for the dashboard's Reconnect button. */
2245
+ invalidateMcpStatus(serverName) {
2246
+ return this.assistant.invalidateMcpStatus(serverName);
2247
+ }
2244
2248
  getPresenceInfo(sessionKey) {
2245
2249
  const sess = this.sessions.get(sessionKey);
2246
2250
  const modelName = sess?.model
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "clementine-agent",
3
- "version": "1.18.80",
3
+ "version": "1.18.82",
4
4
  "description": "Clementine — Personal AI Assistant (TypeScript)",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",