clementine-agent 1.18.81 → 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) => {
@@ -20319,6 +20344,77 @@ if('serviceWorker' in navigator){navigator.serviceWorker.getRegistrations().then
20319
20344
 
20320
20345
  <!-- (legacy standalone Preview modal removed in 1.18.70 — preview now lives as a tab inside the cron modal) -->
20321
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
+
20322
20418
  <!-- ═══ Goal Modal ═══ -->
20323
20419
  <div class="modal-overlay" id="goal-modal">
20324
20420
  <div class="modal" style="width:520px">
@@ -23677,14 +23773,159 @@ function renderMcpCatalogCard(server, statusMap) {
23677
23773
  + (server.description ? '<div style="font-size:12px;color:var(--text-secondary);line-height:1.45">' + esc(String(server.description).slice(0, 240)) + '</div>' : '')
23678
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>' : '')
23679
23775
  + (lastChecked ? '<div style="font-size:11px;color:var(--text-muted)">Checked ' + esc(timeAgo(lastChecked)) + '</div>' : '')
23680
- + '<div style="display:flex;gap:6px;margin-top:4px">'
23776
+ + '<div style="display:flex;gap:6px;margin-top:4px;flex-wrap:wrap">'
23681
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>'
23682
- + '<button class="btn-sm" disabled title="Edit + Reconnect coming in the next slice" style="opacity:0.55;cursor:not-allowed">Edit</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>'
23683
23780
  + '</div>'
23684
23781
  + '</div>';
23685
23782
  return html;
23686
23783
  }
23687
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
+
23688
23929
  // PUT helper for the Toggle button. Lazy: re-fetches the catalog after
23689
23930
  // the round-trip so the new state is reflected. Future slice will swap
23690
23931
  // to optimistic update + rollback on error.
@@ -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.81",
3
+ "version": "1.18.82",
4
4
  "description": "Clementine — Personal AI Assistant (TypeScript)",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",