@ijfw/memory-server 1.3.0 → 1.4.0

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.
Files changed (64) hide show
  1. package/fixtures/team/book.json +47 -0
  2. package/fixtures/team/business.json +47 -0
  3. package/fixtures/team/content.json +47 -0
  4. package/fixtures/team/design.json +47 -0
  5. package/fixtures/team/mixed.json +59 -0
  6. package/fixtures/team/research.json +47 -0
  7. package/fixtures/team/software.json +47 -0
  8. package/package.json +1 -9
  9. package/src/active-extension-writer.js +116 -0
  10. package/src/blackboard.js +360 -0
  11. package/src/cli-run.js +91 -0
  12. package/src/codex-agents.js +177 -0
  13. package/src/compute/extract.js +3 -0
  14. package/src/compute/fts5.js +4 -4
  15. package/src/compute/graph-lock.js +0 -2
  16. package/src/compute/migrations/003-tier-semantic.js +3 -3
  17. package/src/compute/runner.js +44 -15
  18. package/src/compute/schema.sql +1 -1
  19. package/src/cross-orchestrator-cli.js +974 -13
  20. package/src/cross-orchestrator.js +9 -1
  21. package/src/dashboard-client.html +144 -1
  22. package/src/dashboard-server.js +75 -2
  23. package/src/design-intelligence.js +721 -0
  24. package/src/dispatch/colon-syntax.js +31 -3
  25. package/src/dispatch/domain-manifest.js +251 -0
  26. package/src/dispatch/extension.js +404 -0
  27. package/src/dispatch/override.js +221 -0
  28. package/src/dispatch-planner.js +1 -0
  29. package/src/dream/runner.mjs +3 -3
  30. package/src/extension-installer.js +1230 -0
  31. package/src/extension-manifest-schema.js +301 -0
  32. package/src/extension-signer.js +740 -0
  33. package/src/gate-result-formatter.js +95 -0
  34. package/src/gate-result-schema.js +274 -0
  35. package/src/gate-result.js +195 -0
  36. package/src/intent-router.js +2 -0
  37. package/src/lib/npm-view.js +1 -0
  38. package/src/memory/fts5.js +3 -3
  39. package/src/memory/migrations/002-tier-semantic.js +2 -2
  40. package/src/memory/staleness.js +1 -1
  41. package/src/memory/tier-promotion.js +6 -6
  42. package/src/memory/tokenize.js +1 -1
  43. package/src/memory-feedback.js +188 -0
  44. package/src/override-manifest-schema.js +146 -0
  45. package/src/override-resolver.js +699 -0
  46. package/src/override-use-registry.js +307 -0
  47. package/src/overrides/presets/academic.md +101 -0
  48. package/src/overrides/presets/book.md +87 -0
  49. package/src/overrides/presets/campaign.md +95 -0
  50. package/src/overrides/presets/screenplay.md +99 -0
  51. package/src/recovery/checkpoint.js +191 -0
  52. package/src/redactor.js +2 -0
  53. package/src/runtime-mediator.js +178 -0
  54. package/src/sandbox.js +17 -3
  55. package/src/server.js +94 -2
  56. package/src/swarm/dispatch-prompt.js +154 -0
  57. package/src/swarm/planner.js +399 -0
  58. package/src/swarm/review.js +136 -0
  59. package/src/swarm/worktree.js +239 -0
  60. package/src/team/generator.js +119 -0
  61. package/src/team/schemas.js +341 -0
  62. package/src/trident/dispatch.js +47 -0
  63. package/src/update-check.js +1 -1
  64. package/src/vectors.js +7 -8
@@ -182,6 +182,14 @@ function spawnCli(pick, request, timeoutMs, signal = null, env = process.env) {
182
182
  // Listen for external abort (runAc).
183
183
  if (signal) signal.addEventListener('abort', killAndAbort, { once: true });
184
184
 
185
+ // CLI children can exit before consuming stdin (for example auth failures,
186
+ // startup validation, or commands that reject piped input). Treat pipe
187
+ // errors as child stderr/exit evidence, not uncaught process exceptions.
188
+ proc.stdin.on('error', (err) => {
189
+ if (err?.code && !stderr.includes(err.code)) stderr += `${stderr ? '\n' : ''}stdin ${err.code}`;
190
+ });
191
+ proc.stdout.on('error', () => {});
192
+ proc.stderr.on('error', () => {});
185
193
  proc.stdout.on('data', (chunk) => { stdout += chunk.toString(); });
186
194
  proc.stderr.on('data', (chunk) => { stderr += chunk.toString(); });
187
195
  // Single-settlement guard: error + close can both fire on spawn failure.
@@ -200,7 +208,7 @@ function spawnCli(pick, request, timeoutMs, signal = null, env = process.env) {
200
208
  try {
201
209
  const flushed = proc.stdin.write(request);
202
210
  if (flushed) {
203
- proc.stdin.end();
211
+ try { proc.stdin.end(); } catch { /* stdin may close before end */ }
204
212
  } else {
205
213
  proc.stdin.once('drain', () => { try { proc.stdin.end(); } catch { /* */ } });
206
214
  }
@@ -228,6 +228,7 @@ tr:hover td{background:var(--surface)}
228
228
 
229
229
  <div class="sb-group-label"><span>Audits</span></div>
230
230
  <button class="sb-item" data-section="trident"><span class="sb-icon">&#9650;</span><span class="sb-label">Cross-AI Reviews</span></button>
231
+ <button class="sb-item" data-section="extensions"><span class="sb-icon">&#9881;</span><span class="sb-label">Extensions</span></button>
231
232
 
232
233
  <div class="sb-group-label"><span>Settings</span></div>
233
234
  <button class="sb-item" data-section="subs"><span class="sb-icon">&#9879;</span><span class="sb-label">Settings</span></button>
@@ -562,6 +563,19 @@ tr:hover td{background:var(--surface)}
562
563
  </div>
563
564
  </div>
564
565
 
566
+ <!-- ======== EXTENSIONS (W3/t15) ======== -->
567
+ <div class="section" data-section="extensions">
568
+ <div class="card">
569
+ <div class="ctitle"><span id="ext-count">Extensions</span></div>
570
+ <div id="extensions-content">
571
+ <div class="empty">
572
+ <div class="empty-icon">&#9881;</div>
573
+ <p>Loading extensions...</p>
574
+ </div>
575
+ </div>
576
+ </div>
577
+ </div>
578
+
565
579
  <!-- ======== SUBSCRIPTIONS ======== -->
566
580
  <div class="section" data-section="subs">
567
581
  <div class="sgrid">
@@ -626,7 +640,7 @@ tr:hover td{background:var(--surface)}
626
640
  // ====== LUCIDE ICONS (vendored, ISC license, https://lucide.dev) ======
627
641
  const ICONS = {"activity":"<svg class=\"icon\" xmlns=\"http://www.w3.org/2000/svg\" width=\"16\" height=\"16\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M22 12h-2.48a2 2 0 0 0-1.93 1.46l-2.35 8.36a.25.25 0 0 1-.48 0L9.24 2.18a.25.25 0 0 0-.48 0l-2.35 8.36A2 2 0 0 1 4.49 12H2\"/></svg>","bar-chart-2":"<svg class=\"icon\" xmlns=\"http://www.w3.org/2000/svg\" width=\"16\" height=\"16\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M5 21v-6\"/><path d=\"M12 21V3\"/><path d=\"M19 21V9\"/></svg>","clock":"<svg class=\"icon\" xmlns=\"http://www.w3.org/2000/svg\" width=\"16\" height=\"16\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><circle cx=\"12\" cy=\"12\" r=\"10\"/><path d=\"M12 6v6l4 2\"/></svg>","credit-card":"<svg class=\"icon\" xmlns=\"http://www.w3.org/2000/svg\" width=\"16\" height=\"16\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><rect width=\"20\" height=\"14\" x=\"2\" y=\"5\" rx=\"2\"/><line x1=\"2\" x2=\"22\" y1=\"10\" y2=\"10\"/></svg>","dollar-sign":"<svg class=\"icon\" xmlns=\"http://www.w3.org/2000/svg\" width=\"16\" height=\"16\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><line x1=\"12\" x2=\"12\" y1=\"2\" y2=\"22\"/><path d=\"M17 5H9.5a3.5 3.5 0 0 0 0 7h5a3.5 3.5 0 0 1 0 7H6\"/></svg>","file-text":"<svg class=\"icon\" xmlns=\"http://www.w3.org/2000/svg\" width=\"16\" height=\"16\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M6 22a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h8l6 6v12a2 2 0 0 1-2 2z\"/><path d=\"M14 2v6h6\"/><path d=\"M10 9H8\"/><path d=\"M16 13H8\"/><path d=\"M16 17H8\"/></svg>","folder-open":"<svg class=\"icon\" xmlns=\"http://www.w3.org/2000/svg\" width=\"16\" height=\"16\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"m6 14 1.5-2.9A2 2 0 0 1 9.24 10H20a2 2 0 0 1 1.94 2.5l-1.54 6a2 2 0 0 1-1.95 1.5H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h3.9a2 2 0 0 1 1.69.9l.81 1.2a2 2 0 0 0 1.67.9H18a2 2 0 0 1 2 2v2\"/></svg>","folder":"<svg class=\"icon\" xmlns=\"http://www.w3.org/2000/svg\" width=\"16\" height=\"16\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M20 20a2 2 0 0 0 2-2V8a2 2 0 0 0-2-2h-7.9a2 2 0 0 1-1.69-.9L9.6 3.9A2 2 0 0 0 7.93 3H4a2 2 0 0 0-2 2v13a2 2 0 0 0 2 2Z\"/></svg>","home":"<svg class=\"icon\" xmlns=\"http://www.w3.org/2000/svg\" width=\"16\" height=\"16\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M15 21v-8a1 1 0 0 0-1-1h-4a1 1 0 0 0-1 1v8\"/><path d=\"M3 10a2 2 0 0 1 .709-1.528l7-6a2 2 0 0 1 2.582 0l7 6A2 2 0 0 1 21 10v9a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z\"/></svg>","layers":"<svg class=\"icon\" xmlns=\"http://www.w3.org/2000/svg\" width=\"16\" height=\"16\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M12.83 2.18a2 2 0 0 0-1.66 0L2.6 6.08a1 1 0 0 0 0 1.83l8.58 3.91a2 2 0 0 0 1.66 0l8.58-3.9a1 1 0 0 0 0-1.83z\"/><path d=\"M2 12a1 1 0 0 0 .58.91l8.6 3.91a2 2 0 0 0 1.65 0l8.58-3.9A1 1 0 0 0 22 12\"/><path d=\"M2 17a1 1 0 0 0 .58.91l8.6 3.91a2 2 0 0 0 1.65 0l8.58-3.9A1 1 0 0 0 22 17\"/></svg>","search":"<svg class=\"icon\" xmlns=\"http://www.w3.org/2000/svg\" width=\"16\" height=\"16\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"m21 21-4.34-4.34\"/><circle cx=\"11\" cy=\"11\" r=\"8\"/></svg>","settings":"<svg class=\"icon\" xmlns=\"http://www.w3.org/2000/svg\" width=\"16\" height=\"16\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M9.671 4.136a2.34 2.34 0 0 1 4.659 0 2.34 2.34 0 0 0 3.319 1.915 2.34 2.34 0 0 1 2.33 4.033 2.34 2.34 0 0 0 0 3.831 2.34 2.34 0 0 1-2.33 4.033 2.34 2.34 0 0 0-3.319 1.915 2.34 2.34 0 0 1-4.659 0 2.34 2.34 0 0 0-3.32-1.915 2.34 2.34 0 0 1-2.33-4.033 2.34 2.34 0 0 0 0-3.831A2.34 2.34 0 0 1 6.35 6.051a2.34 2.34 0 0 0 3.319-1.915\"/><circle cx=\"12\" cy=\"12\" r=\"3\"/></svg>","shield":"<svg class=\"icon\" xmlns=\"http://www.w3.org/2000/svg\" width=\"16\" height=\"16\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M20 13c0 5-3.5 7.5-7.66 8.95a1 1 0 0 1-.67-.01C7.5 20.5 4 18 4 13V6a1 1 0 0 1 1-1c2 0 4.5-1.2 6.24-2.72a1.17 1.17 0 0 1 1.52 0C14.51 3.81 17 5 19 5a1 1 0 0 1 1 1z\"/></svg>","trending-up":"<svg class=\"icon\" xmlns=\"http://www.w3.org/2000/svg\" width=\"16\" height=\"16\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M16 7h6v6\"/><path d=\"m22 7-8.5 8.5-5-5L2 17\"/></svg>","users":"<svg class=\"icon\" xmlns=\"http://www.w3.org/2000/svg\" width=\"16\" height=\"16\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M16 21v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2\"/><path d=\"M16 3.128a4 4 0 0 1 0 7.744\"/><path d=\"M22 21v-2a4 4 0 0 0-3-3.87\"/><circle cx=\"9\" cy=\"7\" r=\"4\"/></svg>","zap":"<svg class=\"icon\" xmlns=\"http://www.w3.org/2000/svg\" width=\"16\" height=\"16\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M4 14a1 1 0 0 1-.78-1.63l9.9-10.2a.5.5 0 0 1 .86.46l-1.92 6.02A1 1 0 0 0 13 10h7a1 1 0 0 1 .78 1.63l-9.9 10.2a.5.5 0 0 1-.86-.46l1.92-6.02A1 1 0 0 0 11 14z\"/></svg>"};
628
642
 
629
- var ICON_MAP = {today:'home',live:'activity',window:'clock',spend:'dollar-sign',models:'layers',activity:'bar-chart-2',trend:'trending-up',memsearch:'search',sessions:'clock',handoffs:'file-text',allprojects:'folder',trident:'shield',subs:'credit-card',accounts:'users',display:'settings'};
643
+ var ICON_MAP = {today:'home',live:'activity',window:'clock',spend:'dollar-sign',models:'layers',activity:'bar-chart-2',trend:'trending-up',memsearch:'search',sessions:'clock',handoffs:'file-text',allprojects:'folder',trident:'shield',extensions:'package',subs:'credit-card',accounts:'users',display:'settings'};
630
644
 
631
645
  document.querySelectorAll('.sb-icon').forEach(function(el) {
632
646
  var sec = el.closest('.sb-item');
@@ -1161,6 +1175,134 @@ async function loadBlockUsage() {
1161
1175
  } catch(e) {}
1162
1176
  }
1163
1177
 
1178
+ // ====== EXTENSIONS (W3/t15) ======
1179
+ function _extScopeChipColor(scope) {
1180
+ // project = accent, org = surface, user = dim
1181
+ if (scope === 'project') return 'background:rgba(94,106,210,0.18);color:var(--accent)';
1182
+ if (scope === 'org') return 'background:var(--surface);color:var(--fg)';
1183
+ return 'background:var(--surface);color:var(--fg-dim)';
1184
+ }
1185
+ function _extVerdictChipColor(v) {
1186
+ var u = String(v || '').toUpperCase();
1187
+ if (u === 'PASS') return 'background:rgba(60,170,90,0.18);color:#3caa5a';
1188
+ if (u === 'CONDITIONAL') return 'background:rgba(220,180,60,0.18);color:#caa030';
1189
+ if (u === 'WARN') return 'background:rgba(220,180,60,0.18);color:#caa030';
1190
+ if (u === 'FLAG') return 'background:rgba(220,120,60,0.18);color:#c87038';
1191
+ if (u === 'FAIL') return 'background:rgba(220,60,60,0.18);color:#c84040';
1192
+ return 'background:var(--surface);color:var(--fg-dim)';
1193
+ }
1194
+ function _extChip(text, style) {
1195
+ var span = document.createElement('span');
1196
+ span.className = 'chip';
1197
+ span.setAttribute('style', style);
1198
+ span.textContent = text;
1199
+ return span;
1200
+ }
1201
+ function _extEmpty(content, msg, subMsg) {
1202
+ while (content.firstChild) content.removeChild(content.firstChild);
1203
+ var wrap = document.createElement('div');
1204
+ wrap.className = 'empty';
1205
+ var icon = document.createElement('div');
1206
+ icon.className = 'empty-icon';
1207
+ icon.textContent = '⚙'; // gear
1208
+ var p1 = document.createElement('p');
1209
+ p1.textContent = msg;
1210
+ var p2 = document.createElement('p');
1211
+ p2.setAttribute('style', 'margin-top:8px;font-size:12px;color:var(--fg-dim)');
1212
+ p2.textContent = subMsg;
1213
+ wrap.appendChild(icon);
1214
+ wrap.appendChild(p1);
1215
+ wrap.appendChild(p2);
1216
+ content.appendChild(wrap);
1217
+ }
1218
+ async function loadExtensions() {
1219
+ var content = document.getElementById('extensions-content');
1220
+ var countEl = document.getElementById('ext-count');
1221
+ if (!content) return;
1222
+ try {
1223
+ var r = await fetch('/api/extensions/health');
1224
+ var d = await r.json();
1225
+ var exts = Array.isArray(d.extensions) ? d.extensions : [];
1226
+ var staleCount = exts.filter(function(e) { return e.status === 'stale'; }).length;
1227
+
1228
+ if (countEl) {
1229
+ countEl.textContent = exts.length === 0
1230
+ ? 'Extensions'
1231
+ : 'Extensions (' + exts.length + ' installed' + (staleCount ? ', ' + staleCount + ' stale' : '') + ')';
1232
+ }
1233
+
1234
+ if (exts.length === 0) {
1235
+ _extEmpty(content, 'No extensions installed.',
1236
+ 'Install with `ijfw extension install <source>`. Extensions ship overrides, skills, and integrations.');
1237
+ return;
1238
+ }
1239
+
1240
+ while (content.firstChild) content.removeChild(content.firstChild);
1241
+
1242
+ if (staleCount > 0) {
1243
+ var banner = document.createElement('div');
1244
+ banner.setAttribute('style', 'padding:8px 12px;margin-bottom:12px;background:rgba(220,120,60,0.08);border-left:3px solid #c87038;border-radius:4px;font-size:13px');
1245
+ banner.textContent = '⚠ ' + staleCount + ' extension' + (staleCount === 1 ? '' : 's') +
1246
+ ' marked stale -- scope dir is missing. Run `ijfw extension reinstall` or remove the entry.';
1247
+ content.appendChild(banner);
1248
+ }
1249
+
1250
+ var table = document.createElement('table');
1251
+ table.setAttribute('aria-label', 'Installed extensions');
1252
+ var thead = document.createElement('thead');
1253
+ var trh = document.createElement('tr');
1254
+ ['Name','Version','Scope','Last Trident','Status'].forEach(function(h) {
1255
+ var th = document.createElement('th');
1256
+ th.textContent = h;
1257
+ trh.appendChild(th);
1258
+ });
1259
+ thead.appendChild(trh);
1260
+ table.appendChild(thead);
1261
+
1262
+ var tbody = document.createElement('tbody');
1263
+ exts.forEach(function(e) {
1264
+ var tr = document.createElement('tr');
1265
+ var tdName = document.createElement('td');
1266
+ var strong = document.createElement('strong');
1267
+ strong.textContent = e.name || '';
1268
+ tdName.appendChild(strong);
1269
+ tr.appendChild(tdName);
1270
+
1271
+ var tdVer = document.createElement('td');
1272
+ tdVer.textContent = e.version || '';
1273
+ tr.appendChild(tdVer);
1274
+
1275
+ var tdScope = document.createElement('td');
1276
+ tdScope.appendChild(_extChip(e.scope || 'unknown', _extScopeChipColor(e.scope)));
1277
+ tr.appendChild(tdScope);
1278
+
1279
+ var tdVerdict = document.createElement('td');
1280
+ var verdict = e.last_trident_verdict;
1281
+ tdVerdict.appendChild(_extChip(verdict == null ? '--' : String(verdict),
1282
+ _extVerdictChipColor(verdict)));
1283
+ tr.appendChild(tdVerdict);
1284
+
1285
+ var tdStatus = document.createElement('td');
1286
+ if (e.status === 'stale') {
1287
+ var staleChip = _extChip('stale', 'background:rgba(220,120,60,0.18);color:#c87038');
1288
+ staleChip.title = 'scope dir missing';
1289
+ tdStatus.appendChild(staleChip);
1290
+ } else {
1291
+ tdStatus.appendChild(_extChip('active', 'background:rgba(60,170,90,0.18);color:#3caa5a'));
1292
+ }
1293
+ tr.appendChild(tdStatus);
1294
+
1295
+ tbody.appendChild(tr);
1296
+ });
1297
+ table.appendChild(tbody);
1298
+ content.appendChild(table);
1299
+ } catch (err) {
1300
+ if (countEl) countEl.textContent = 'Extensions';
1301
+ _extEmpty(content, 'No extensions installed.',
1302
+ 'Registry unavailable -- this is normal before the first install.');
1303
+ }
1304
+ }
1305
+
1164
1306
  // ====== RUN ALL LOADERS ======
1165
1307
  document.addEventListener('DOMContentLoaded', function() {
1166
1308
  loadCostToday();
@@ -1174,6 +1316,7 @@ document.addEventListener('DOMContentLoaded', function() {
1174
1316
  loadConfig();
1175
1317
  loadTrendSparkline();
1176
1318
  loadBlockUsage();
1319
+ loadExtensions();
1177
1320
  });
1178
1321
  </script>
1179
1322
  </body>
@@ -20,6 +20,8 @@ import { computeValueDelivered } from './cost/savings.js';
20
20
  import { listMemoryFiles, listKnownProjects } from './memory/reader.js';
21
21
  import { searchMemory } from './memory/search.js';
22
22
  import { buildRecallCounts, mergeRecallCounts, topRecalled } from './memory/recall-counter.js';
23
+ import { PLACEHOLDER_HTML } from './design-companion.js';
24
+ import { listExtensions } from './extension-installer.js';
23
25
 
24
26
  const __dirname = dirname(fileURLToPath(import.meta.url));
25
27
  // REPO_ROOT: IJFW_PROJECT_ROOT override > user's interactive shell cwd (PWD) > process.cwd() fallback.
@@ -132,6 +134,24 @@ const PORT_WALK_MAX = 10; // walk up to 37891+PORT_WALK_MAX (37900)
132
134
  const BACKFILL_DEFAULT = 200;
133
135
  const BACKFILL_CAP = 50; // max observations sent on fresh connect (W4.6)
134
136
 
137
+ const DESIGN_LIVE_RELOAD_SCRIPT = `
138
+ <script>
139
+ (function(){
140
+ if (window.__ijfwDesignLiveReload) return;
141
+ window.__ijfwDesignLiveReload = true;
142
+ try {
143
+ var events = new EventSource('/design/stream');
144
+ events.addEventListener('reload', function(){ window.location.reload(); });
145
+ } catch (_) {}
146
+ })();
147
+ </script>`;
148
+
149
+ function injectDesignLiveReload(html) {
150
+ if (typeof html !== 'string' || html.includes('__ijfwDesignLiveReload')) return html;
151
+ if (/<\/body>/i.test(html)) return html.replace(/<\/body>/i, DESIGN_LIVE_RELOAD_SCRIPT + '\n</body>');
152
+ return html + DESIGN_LIVE_RELOAD_SCRIPT;
153
+ }
154
+
135
155
  // ---------- integer param validator ----------
136
156
  // Rejects numeric-prefix garbage like "10xyz" that parseInt accepts (W9-M2).
137
157
  // Returns null on invalid input; caller should respond with 400.
@@ -668,6 +688,35 @@ export async function startServer(options = {}) {
668
688
  res.end(JSON.stringify(config));
669
689
  }],
670
690
 
691
+ // ---------- extensions health (W3/t15) ----------
692
+ // Reads .ijfw/state/extension-registry.json (project) plus org/user via
693
+ // listExtensions(). Missing or malformed registry yields {extensions: []}
694
+ // (day-1 protection). ENOENT and JSON.parse errors are swallowed inside
695
+ // readRegistry(); this handler adds an outer try/catch as a final safety
696
+ // net so a malformed registry can never crash the dashboard.
697
+ ['/api/extensions/health', async (req, res) => {
698
+ try {
699
+ const extensions = await listExtensions(REPO_ROOT);
700
+ // Strip embedded manifest blobs -- the dashboard only needs the
701
+ // status summary fields, not the full manifest.
702
+ const out = (Array.isArray(extensions) ? extensions : []).map((e) => ({
703
+ name: e.name,
704
+ version: e.version,
705
+ scope: e.scope,
706
+ installed_at: e.installed_at || null,
707
+ status: e.status || 'stale',
708
+ last_trident_verdict: e.last_trident_verdict ?? null,
709
+ }));
710
+ res.writeHead(200, { 'Content-Type': 'application/json' });
711
+ res.end(JSON.stringify({ extensions: out }));
712
+ } catch (err) {
713
+ // ENOENT / missing / malformed -> 200 with empty list (day-1).
714
+ process.stderr.write(`[ijfw-mcp] /api/extensions/health: ${err && err.message ? err.message : err}\n`);
715
+ res.writeHead(200, { 'Content-Type': 'application/json' });
716
+ res.end(JSON.stringify({ extensions: [], error: 'malformed registry' }));
717
+ }
718
+ }],
719
+
671
720
  ['/api/value-delivered', (req, res, url) => {
672
721
  try {
673
722
  const platform = url.searchParams.get('platform') || 'claude';
@@ -699,6 +748,29 @@ export async function startServer(options = {}) {
699
748
  }],
700
749
 
701
750
  // ---------- design companion ----------
751
+ [/^\/design\/files\/[^/]+\.html$/, async (req, res, url) => {
752
+ const contentDir = join(homedir(), '.ijfw', 'design-companion', 'content');
753
+ mkdirSync(contentDir, { recursive: true });
754
+ const name = url.pathname.split('/').pop();
755
+ const filePath = join(contentDir, name);
756
+ let html = null;
757
+ try {
758
+ if (existsSync(filePath)) html = readFileSync(filePath, 'utf8');
759
+ } catch {}
760
+ if (!html) {
761
+ res.writeHead(404, { 'Content-Type': 'text/plain' });
762
+ res.end('404 Not Found');
763
+ return;
764
+ }
765
+ html = injectDesignLiveReload(html);
766
+ res.writeHead(200, {
767
+ 'Content-Type': 'text/html; charset=utf-8',
768
+ 'Cache-Control': 'no-store',
769
+ 'Content-Security-Policy': "default-src 'self' https: data:; style-src 'self' 'unsafe-inline' https:; script-src 'self' 'unsafe-inline' https:; img-src 'self' data: https:; font-src 'self' data: https:; connect-src 'self'",
770
+ });
771
+ res.end(html);
772
+ }],
773
+
702
774
  ['/design', async (req, res) => {
703
775
  const contentDir = join(homedir(), '.ijfw', 'design-companion', 'content');
704
776
  mkdirSync(contentDir, { recursive: true });
@@ -714,12 +786,13 @@ export async function startServer(options = {}) {
714
786
  }
715
787
  } catch {}
716
788
  if (!html) {
717
- html = `<!doctype html><html><body><pre>Design companion active. Push a design with: ijfw design push file.html</pre></body></html>`;
789
+ html = PLACEHOLDER_HTML;
718
790
  }
791
+ html = injectDesignLiveReload(html);
719
792
  res.writeHead(200, {
720
793
  'Content-Type': 'text/html; charset=utf-8',
721
794
  'Cache-Control': 'no-store',
722
- 'Content-Security-Policy': "default-src 'self'; style-src 'self' 'unsafe-inline'; script-src 'self' 'unsafe-inline'; img-src 'self' data:; connect-src 'self'",
795
+ 'Content-Security-Policy': "default-src 'self' https: data:; style-src 'self' 'unsafe-inline' https:; script-src 'self' 'unsafe-inline' https:; img-src 'self' data: https:; font-src 'self' data: https:; connect-src 'self'",
723
796
  });
724
797
  res.end(html);
725
798
  }],