synabun 2.0.0-beta → 2.0.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.
- package/CHANGELOG.md +8 -0
- package/THIRD-PARTY-LICENSES.md +5 -0
- package/launcher.mjs +40 -0
- package/lib/claude-hooks.js +61 -8
- package/lib/client-config-repair.js +4 -3
- package/lib/data-home-migration.js +4 -0
- package/lib/desktop-pwa.js +206 -0
- package/lib/external-tools.js +221 -0
- package/lib/first-launch-protection.js +5 -0
- package/lib/packaged-runtime.js +110 -0
- package/lib/start-launcher.js +1309 -0
- package/neural-interface/i18n/en.json +41 -2
- package/neural-interface/i18n/pt-BR.json +41 -2
- package/neural-interface/lib/assistant-envelope.js +9 -6
- package/neural-interface/lib/augmented-path.js +23 -3
- package/neural-interface/lib/browser-language.js +83 -0
- package/neural-interface/lib/claude-agent-bridge.js +54 -147
- package/neural-interface/lib/claude-executable.js +241 -0
- package/neural-interface/lib/claude-model-catalog.js +52 -91
- package/neural-interface/lib/native-binary-runtime.js +69 -337
- package/neural-interface/lib/native-loop-providers.js +30 -44
- package/neural-interface/lib/offline-page.js +54 -0
- package/neural-interface/lib/session-title-generator.js +5 -0
- package/neural-interface/lib/synabun-update-plan.js +19 -0
- package/neural-interface/package-lock.json +0 -251
- package/neural-interface/public/install-app.html +31 -0
- package/neural-interface/public/manifest.json +2 -0
- package/neural-interface/public/offline.html +401 -66
- package/neural-interface/public/onboarding.html +252 -146
- package/neural-interface/public/shared/assistant/asst-mascot.js +12 -10
- package/neural-interface/public/shared/assistant/asst-rack-styles.js +0 -3
- package/neural-interface/public/shared/assistant/asst-render.js +58 -49
- package/neural-interface/public/shared/assistant/asst-styles.js +4 -2
- package/neural-interface/public/shared/command-runner-model.js +30 -0
- package/neural-interface/public/shared/html-shell.js +9 -5
- package/neural-interface/public/shared/pwa-install-page.js +34 -0
- package/neural-interface/public/shared/pwa-install.css +12 -0
- package/neural-interface/public/shared/pwa-install.js +28 -0
- package/neural-interface/public/shared/settings/settings-rules.js +160 -0
- package/neural-interface/public/shared/start-bridge.js +583 -0
- package/neural-interface/public/shared/styleguide/sg-colors.js +5 -2
- package/neural-interface/public/shared/styleguide/sg-import-export.js +4 -3
- package/neural-interface/public/shared/styleguide/sg-kit.js +4 -1
- package/neural-interface/public/shared/styleguide/sg-panel.js +9 -1
- package/neural-interface/public/shared/styleguide/sg-typography.js +1 -0
- package/neural-interface/public/shared/styles.css +801 -392
- package/neural-interface/public/shared/term-ready.js +307 -0
- package/neural-interface/public/shared/ui-claude-panel.js +3 -3
- package/neural-interface/public/shared/ui-command-runner.js +251 -468
- package/neural-interface/public/shared/ui-judgments.js +1 -1
- package/neural-interface/public/shared/ui-loading.js +221 -10
- package/neural-interface/public/shared/ui-menubar.js +11 -27
- package/neural-interface/public/shared/ui-settings.js +36 -59
- package/neural-interface/public/shared/ui-terminal.js +130 -144
- package/neural-interface/public/sw.js +3 -1
- package/neural-interface/public/variant/2d/main.js +2 -1
- package/neural-interface/public/variant/3d/main.js +2 -1
- package/neural-interface/scripts/rebuild-pty.js +15 -12
- package/neural-interface/server.js +220 -185
- package/package.json +13 -1
- package/postinstall.js +11 -81
- package/setup.js +72 -4
- package/docs/browser-v2.md +0 -109
- package/docs/hook-integrity.md +0 -89
- package/docs/judgments.md +0 -182
- package/docs/style-guide.md +0 -246
- package/scripts/audit-claude-hook-usage.mjs +0 -12
- package/scripts/audit-memory-duplicates.mjs +0 -44
- package/scripts/measure-claude-hook-overhead.mjs +0 -77
- package/scripts/recover-user-config.mjs +0 -254
|
@@ -539,7 +539,7 @@
|
|
|
539
539
|
.mcp-other-section.open .mcp-other-chevron { transform: rotate(180deg); }
|
|
540
540
|
.mcp-other-body { padding: 0 14px 12px; }
|
|
541
541
|
|
|
542
|
-
/* ── Rules (Step 5 option, Step 6
|
|
542
|
+
/* ── Rules (Step 5 option, Step 6 installer) ── */
|
|
543
543
|
.rules-option {
|
|
544
544
|
display: flex;
|
|
545
545
|
align-items: center;
|
|
@@ -550,12 +550,37 @@
|
|
|
550
550
|
cursor: pointer;
|
|
551
551
|
user-select: none;
|
|
552
552
|
}
|
|
553
|
+
/* A step whose height follows what the installer reports: on a short window it scrolls (its parts keep their
|
|
554
|
+
height instead of being squeezed) rather than being cut off. */
|
|
555
|
+
.step[data-step="6"] { max-height: calc(90vh - 58px); }
|
|
556
|
+
.step[data-step="6"] > * { flex-shrink: 0; }
|
|
557
|
+
.step[data-step="6"] .subtitle { margin-bottom: 16px; }
|
|
558
|
+
/* The step's one primary action: what it does, then the button. */
|
|
559
|
+
.rules-install-all {
|
|
560
|
+
display: flex;
|
|
561
|
+
align-items: center;
|
|
562
|
+
gap: 14px;
|
|
563
|
+
padding: 12px 14px;
|
|
564
|
+
margin-bottom: 12px;
|
|
565
|
+
border: 1px solid var(--accent-border);
|
|
566
|
+
border-radius: var(--r-card);
|
|
567
|
+
background: var(--accent-bg);
|
|
568
|
+
}
|
|
569
|
+
.rules-install-all .ria-text { flex: 1 1 0; min-width: 0; font-size: 12px; color: var(--t-primary); line-height: 1.5; }
|
|
570
|
+
.rules-install-all .action-btn { flex-shrink: 0; padding: 9px 18px; font-size: 12px; white-space: nowrap; }
|
|
571
|
+
/* What the installer answered, and where the rules live. */
|
|
572
|
+
.rules-result { margin-bottom: 12px; font-size: 12px; color: var(--t-primary); line-height: 1.5; overflow-wrap: anywhere; }
|
|
573
|
+
.rules-result.ok .rr-message::before { content: '\2713'; margin-right: 8px; color: var(--green); }
|
|
574
|
+
.rules-result.fail .rr-message { color: var(--red); }
|
|
575
|
+
.rules-result .rr-where { margin-top: 4px; font-size: 11px; color: var(--t-muted); }
|
|
553
576
|
.rules-list {
|
|
554
577
|
display: flex;
|
|
555
578
|
flex-direction: column;
|
|
556
579
|
gap: 8px;
|
|
557
580
|
margin-bottom: 12px;
|
|
558
581
|
}
|
|
582
|
+
.rules-list:empty { display: none; }
|
|
583
|
+
.rules-list .mcp-toggle-row.has-note { align-items: flex-start; }
|
|
559
584
|
.rules-list .mtr-state {
|
|
560
585
|
margin-left: 6px;
|
|
561
586
|
font-size: 10px;
|
|
@@ -563,8 +588,9 @@
|
|
|
563
588
|
color: var(--t-muted);
|
|
564
589
|
}
|
|
565
590
|
.rules-list .mtr-state.ok { color: var(--green); }
|
|
591
|
+
.rules-list .mtr-note { margin-top: 4px; font-size: 11px; color: var(--t-secondary); line-height: 1.45; overflow-wrap: anywhere; }
|
|
566
592
|
.rules-actions { display: flex; gap: 6px; flex-shrink: 0; }
|
|
567
|
-
.rules-actions button {
|
|
593
|
+
.rules-actions button, .rules-note button {
|
|
568
594
|
padding: 5px 10px;
|
|
569
595
|
border-radius: 8px;
|
|
570
596
|
border: 1px solid var(--b-light);
|
|
@@ -576,11 +602,16 @@
|
|
|
576
602
|
white-space: nowrap;
|
|
577
603
|
transition: all var(--ease);
|
|
578
604
|
}
|
|
579
|
-
.rules-actions button:hover { background: var(--s-medium); color: var(--t-bright); border-color: var(--b-hover); }
|
|
580
|
-
.rules-actions button
|
|
605
|
+
.rules-actions button:hover, .rules-note button:hover { background: var(--s-medium); color: var(--t-bright); border-color: var(--b-hover); }
|
|
606
|
+
.rules-actions button.primary { border-color: var(--accent-border); background: var(--s-active); color: var(--t-bright); font-weight: 600; }
|
|
607
|
+
.rules-actions button:disabled, .rules-note button:disabled { opacity: 0.4; cursor: default; }
|
|
581
608
|
.rules-note { font-size: 11px; color: var(--t-muted); margin-bottom: 12px; line-height: 1.5; }
|
|
609
|
+
.rules-note button { margin-left: 8px; }
|
|
610
|
+
/* Under the installer: the text SynaBun cannot install, to paste by hand. */
|
|
611
|
+
.rules-manual-label { margin: 4px 0 8px; font-size: 10px; color: var(--t-muted); text-transform: uppercase; letter-spacing: 0.5px; }
|
|
582
612
|
.rules-extra { margin-top: 8px; }
|
|
583
613
|
#rulesViewCode, #rulesCursorCode, #rulesCoexistCode { max-height: 160px; }
|
|
614
|
+
#rulesViewCode { margin-bottom: 12px; }
|
|
584
615
|
|
|
585
616
|
/* ── Dependency List ── */
|
|
586
617
|
.dep-list {
|
|
@@ -1188,14 +1219,14 @@
|
|
|
1188
1219
|
<span class="mascot-toast" id="mascotToast"></span>
|
|
1189
1220
|
</div>
|
|
1190
1221
|
<h2>AI Tool Integration</h2>
|
|
1191
|
-
<p class="subtitle">Toggle on the AI coding tools you use. SynaBun registers itself with each one automatically.</p>
|
|
1222
|
+
<p class="subtitle">Toggle on the AI coding tools you use. SynaBun registers itself with each one and installs its rules automatically.</p>
|
|
1192
1223
|
<div class="mcp-toggle-list" id="mcpToggleList"></div>
|
|
1193
1224
|
<label class="rules-option" for="rulesInstallOpt">
|
|
1194
1225
|
<span class="key-backup-checkbox-wrap">
|
|
1195
1226
|
<input type="checkbox" id="rulesInstallOpt" checked>
|
|
1196
1227
|
<span class="key-backup-checkmark"><svg viewBox="0 0 24 24" width="12" height="12" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"><polyline points="20 6 9 17 4 12"/></svg></span>
|
|
1197
1228
|
</span>
|
|
1198
|
-
<span>Install SynaBun's rules
|
|
1229
|
+
<span>Install SynaBun's rules with each tool I connect</span>
|
|
1199
1230
|
</label>
|
|
1200
1231
|
<div class="mcp-other-section" id="mcpOtherSection">
|
|
1201
1232
|
<div class="mcp-other-header" id="mcpOtherHeader">
|
|
@@ -1213,27 +1244,31 @@
|
|
|
1213
1244
|
</div>
|
|
1214
1245
|
</div>
|
|
1215
1246
|
|
|
1216
|
-
<!-- Step 6: SynaBun rules -->
|
|
1247
|
+
<!-- Step 6: Install SynaBun rules. The installer leads; the copy boxes at the bottom are only for what it cannot write. -->
|
|
1217
1248
|
<div class="step right" data-step="6">
|
|
1218
|
-
<div
|
|
1219
|
-
|
|
1220
|
-
|
|
1249
|
+
<div class="step-eyes"></div>
|
|
1250
|
+
<h2>Install SynaBun Rules</h2>
|
|
1251
|
+
<p class="subtitle">SynaBun installs its rules where each connected tool loads them for every project, and keeps them up to date. There is nothing to paste.</p>
|
|
1252
|
+
<div class="rules-note" id="rulesNote" role="status" style="display:none;"></div>
|
|
1253
|
+
<div class="rules-install-all" id="rulesInstallAll" style="display:none;">
|
|
1254
|
+
<div class="ria-text" id="rulesInstallAllHelp"></div>
|
|
1255
|
+
<button type="button" class="action-btn" id="rulesInstallAllBtn" aria-describedby="rulesInstallAllHelp"></button>
|
|
1221
1256
|
</div>
|
|
1222
|
-
<
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
<div class="rules-list" id="rulesList"></div>
|
|
1226
|
-
<div class="code-copy-wrap" id="rulesViewWrap" style="display:none;margin-bottom:12px;">
|
|
1227
|
-
<div class="instructions-preview" id="rulesViewCode"></div>
|
|
1228
|
-
<span class="code-copy-label">copy</span>
|
|
1257
|
+
<div class="rules-result" id="rulesResult" role="status" style="display:none;">
|
|
1258
|
+
<div class="rr-message" id="rulesResultMessage"></div>
|
|
1259
|
+
<div class="rr-where" id="rulesResultWhere"></div>
|
|
1229
1260
|
</div>
|
|
1261
|
+
<div class="rules-list" id="rulesList"></div>
|
|
1262
|
+
<div class="instructions-preview" id="rulesViewCode" role="region" tabindex="0" style="display:none;"></div>
|
|
1263
|
+
<div class="rules-note" id="rulesIdle" style="display:none;"></div>
|
|
1264
|
+
<div class="rules-manual-label">Manual setup</div>
|
|
1230
1265
|
<div class="mcp-other-section" id="rulesCursorSection">
|
|
1231
1266
|
<div class="mcp-other-header" id="rulesCursorHeader">
|
|
1232
|
-
<span>Cursor (
|
|
1267
|
+
<span>Cursor (its rules are pasted by hand)</span>
|
|
1233
1268
|
<svg class="mcp-other-chevron" viewBox="0 0 24 24" width="14" height="14"><polyline points="6 9 12 15 18 9" fill="none" stroke="currentColor" stroke-width="2"/></svg>
|
|
1234
1269
|
</div>
|
|
1235
1270
|
<div class="mcp-other-body" id="rulesCursorBody" style="display:none;">
|
|
1236
|
-
<div style="font-size:12px;color:var(--t-secondary);margin-bottom:6px;">Cursor keeps User Rules inside the app.
|
|
1271
|
+
<div id="rulesCursorHelp" style="font-size:12px;color:var(--t-secondary);margin-bottom:6px;">Cursor keeps its User Rules inside the app, so SynaBun cannot install them. Copy the rules and paste them into Cursor Settings → Rules.</div>
|
|
1237
1272
|
<div class="code-copy-wrap" id="rulesCursorWrap">
|
|
1238
1273
|
<div class="instructions-preview" id="rulesCursorCode"></div>
|
|
1239
1274
|
<span class="code-copy-label">copy</span>
|
|
@@ -1246,7 +1281,7 @@
|
|
|
1246
1281
|
<svg class="mcp-other-chevron" viewBox="0 0 24 24" width="14" height="14"><polyline points="6 9 12 15 18 9" fill="none" stroke="currentColor" stroke-width="2"/></svg>
|
|
1247
1282
|
</div>
|
|
1248
1283
|
<div class="mcp-other-body" id="rulesCoexistBody" style="display:none;">
|
|
1249
|
-
<div style="font-size:12px;color:var(--t-secondary);margin-bottom:6px;">Copy this, name the other tool in it, and paste it into your instructions file. SynaBun never installs it.</div>
|
|
1284
|
+
<div id="rulesCoexistHelp" style="font-size:12px;color:var(--t-secondary);margin-bottom:6px;">Copy this, name the other tool in it, and paste it into your instructions file. SynaBun never installs it.</div>
|
|
1250
1285
|
<div class="code-copy-wrap" id="rulesCoexistWrap">
|
|
1251
1286
|
<div class="instructions-preview" id="rulesCoexistCode"></div>
|
|
1252
1287
|
<span class="code-copy-label">copy</span>
|
|
@@ -1261,7 +1296,7 @@
|
|
|
1261
1296
|
<h2 style="text-align:center;margin-bottom:12px;">Setup Complete!</h2>
|
|
1262
1297
|
<ul class="checklist" id="checklist"></ul>
|
|
1263
1298
|
<div id="restartNotice" style="margin:16px 0 0;padding:12px 14px;background:rgba(255,255,255,0.04);border:1px solid rgba(255,255,255,0.10);border-radius:var(--r-card);font-size:12px;color:var(--t-primary);line-height:1.5;">
|
|
1264
|
-
<span style="color:var(--t-bright);font-weight:600;">Restart required</span> — Relaunch <span id="restartToolName" style="color:var(--t-bright);font-weight:500;"></span> for the MCP changes to be picked up.
|
|
1299
|
+
<span style="color:var(--t-bright);font-weight:600;">Restart required</span> — Relaunch <span id="restartToolName" style="color:var(--t-bright);font-weight:500;"></span> for the MCP changes and SynaBun's rules to be picked up.
|
|
1265
1300
|
</div>
|
|
1266
1301
|
<div style="text-align:center;margin-top:16px;">
|
|
1267
1302
|
<button class="action-btn" id="launchBtn" style="font-size:15px;padding:12px 32px;">Launch Neural Interface</button>
|
|
@@ -1371,12 +1406,21 @@
|
|
|
1371
1406
|
|
|
1372
1407
|
<script type="module">
|
|
1373
1408
|
|
|
1409
|
+
// The rules step is the installer of Settings → AI connections: the same client calls and the same reading
|
|
1410
|
+
// of the status (see STEP 6). Both modules are import-free, so the wizard still loads nothing of the app.
|
|
1411
|
+
import { fetchRulesStatus, fetchRulesTextWithFallback, installRules, installAllRules } from './shared/api.js';
|
|
1412
|
+
import {
|
|
1413
|
+
describeRulesHost, describeRulesInstall, describeRulesInstallAll, describeRulesInPlace, rulesInstallPlan, catalogueTranslator,
|
|
1414
|
+
} from './shared/settings/settings-rules.js';
|
|
1415
|
+
|
|
1374
1416
|
// ═══════════════════════════════════════════
|
|
1375
1417
|
// STATE
|
|
1376
1418
|
// ═══════════════════════════════════════════
|
|
1377
1419
|
|
|
1378
1420
|
const TOTAL_STEPS = 8;
|
|
1379
1421
|
let currentStep = 0;
|
|
1422
|
+
// Step 6: a connected tool still has no rules. Leaving the step then is a skip, and the Next button says so.
|
|
1423
|
+
let rulesPending = false;
|
|
1380
1424
|
|
|
1381
1425
|
const wizardState = {
|
|
1382
1426
|
collectionName: 'claude_memory',
|
|
@@ -1386,6 +1430,8 @@ const wizardState = {
|
|
|
1386
1430
|
enabledTools: [], // tool ids toggled on via Step 5 install toggles
|
|
1387
1431
|
dataHome: '',
|
|
1388
1432
|
packageRoot: '',
|
|
1433
|
+
mcpCommand: '',
|
|
1434
|
+
mcpArgs: [],
|
|
1389
1435
|
buildReady: false,
|
|
1390
1436
|
mcpWritten: false,
|
|
1391
1437
|
projects: [],
|
|
@@ -1479,7 +1525,8 @@ function updateNav() {
|
|
|
1479
1525
|
$prevBtn.disabled = currentStep === 0;
|
|
1480
1526
|
|
|
1481
1527
|
const labels = ['Begin', 'Next', 'Next', 'Next', 'Next', 'Next', 'Next', 'Finish'];
|
|
1482
|
-
|
|
1528
|
+
const label = currentStep === 6 && rulesPending ? 'Skip' : (labels[currentStep] || 'Next');
|
|
1529
|
+
$nextBtn.textContent = label + ' \u2192';
|
|
1483
1530
|
|
|
1484
1531
|
if (currentStep === TOTAL_STEPS - 1) {
|
|
1485
1532
|
$nextBtn.style.display = 'none';
|
|
@@ -2541,7 +2588,11 @@ function getGenericMcpJson() {
|
|
|
2541
2588
|
const mcpPath = pkgRoot + '/mcp-server/run.mjs';
|
|
2542
2589
|
const envPath = dataHome + '/.env';
|
|
2543
2590
|
const env = { DOTENV_PATH: envPath, SYNABUN_DATA_HOME: dataHome, MEMORY_DATA_DIR: dataHome + '/mcp-data' };
|
|
2544
|
-
|
|
2591
|
+
// A packaged application is started through its own executable; the server says which.
|
|
2592
|
+
const packaged = wizardState.mcpCommand && wizardState.mcpCommand !== 'node';
|
|
2593
|
+
const command = packaged ? wizardState.mcpCommand : 'node';
|
|
2594
|
+
const args = packaged ? wizardState.mcpArgs : [mcpPath];
|
|
2595
|
+
return JSON.stringify({ mcpServers: { SynaBun: { command, args, env } } }, null, 2);
|
|
2545
2596
|
}
|
|
2546
2597
|
|
|
2547
2598
|
// "Other providers" collapsible — shows the generic MCP JSON for paste-based setups.
|
|
@@ -2576,156 +2627,207 @@ document.getElementById('mcpCodeWrap').addEventListener('click', () => {
|
|
|
2576
2627
|
});
|
|
2577
2628
|
|
|
2578
2629
|
// ═══════════════════════════════════════════
|
|
2579
|
-
// STEP 6: SynaBun rules
|
|
2630
|
+
// STEP 6: Install SynaBun rules
|
|
2580
2631
|
// ═══════════════════════════════════════════
|
|
2581
|
-
//
|
|
2582
|
-
//
|
|
2583
|
-
//
|
|
2632
|
+
// The installer, as Settings → AI connections → SynaBun rules shows it: one
|
|
2633
|
+
// button that installs the rules for every connected tool, then one row per
|
|
2634
|
+
// tool with where its rules live, what state they are in (GET /api/setup/rules)
|
|
2635
|
+
// and the action that state calls for, and under them what the installer
|
|
2636
|
+
// answered. The calls are the shared client's (shared/api.js). What a state is
|
|
2637
|
+
// called, which action it gets and every sentence about it come from
|
|
2638
|
+
// shared/settings/settings-rules.js and the Settings strings (i18n/en.json), so
|
|
2639
|
+
// the two surfaces cannot drift apart. Nothing is pasted in this step: the two
|
|
2640
|
+
// boxes under "Manual setup" hold what SynaBun cannot install (Cursor's User
|
|
2641
|
+
// Rules, the coexistence snippet). Nothing here has to be clicked to continue.
|
|
2584
2642
|
|
|
2585
2643
|
const RESTART_HINT = 'Restart SynaBun to finish this update.';
|
|
2586
|
-
const
|
|
2587
|
-
installed: 'Installed', newer: 'Installed by a newer SynaBun', outdated: 'Update available', modified: 'Edited', conflict: 'Conflict',
|
|
2588
|
-
error: 'Error', shadowed: 'Shadowed', 'not-installed': 'Not installed',
|
|
2589
|
-
};
|
|
2590
|
-
const RULES_PRIMARY = { 'not-installed': 'Install', outdated: 'Update', modified: 'Replace' };
|
|
2644
|
+
const RULES_SETTINGS_PATH = 'Settings → AI connections → SynaBun rules';
|
|
2591
2645
|
let rulesStatus = null; // null: the route is missing or failed
|
|
2646
|
+
let rulesTx = null; // the Settings strings; null until they are loaded
|
|
2647
|
+
let rulesReport = null; // what the last install answered: { message, note, ok }
|
|
2648
|
+
let rulesBusy = false; // an install is on its way: every button of the step waits
|
|
2649
|
+
let rulesViewHost = ''; // the tool whose rules text is open under the list
|
|
2592
2650
|
const rulesTextCache = {};
|
|
2593
2651
|
|
|
2594
|
-
|
|
2652
|
+
// The wizard's names for the tools (the ones Step 5 shows), for every sentence that names one.
|
|
2653
|
+
const RULES_TOOL_NAMES = Object.fromEntries(TOOLS.map(t => [t.id, t.name]));
|
|
2654
|
+
const ruleToolNames = (hosts) => hosts.map(host => RULES_TOOL_NAMES[host] || host).join(', ');
|
|
2655
|
+
|
|
2656
|
+
// The words of this step are the Settings ones. One request; a failed one is tried again on the next visit.
|
|
2657
|
+
async function loadRulesStrings() {
|
|
2658
|
+
if (rulesTx) return rulesTx;
|
|
2595
2659
|
try {
|
|
2596
|
-
const res = await fetch('/
|
|
2597
|
-
if (
|
|
2598
|
-
|
|
2599
|
-
|
|
2600
|
-
} catch { return null; }
|
|
2660
|
+
const res = await fetch('/i18n/en.json');
|
|
2661
|
+
if (res.ok) rulesTx = catalogueTranslator(await res.json());
|
|
2662
|
+
} catch { /* the step says so and offers to try again */ }
|
|
2663
|
+
return rulesTx;
|
|
2601
2664
|
}
|
|
2602
2665
|
|
|
2603
2666
|
// The rendered text for a host, from the rules route or the older read-only endpoint.
|
|
2604
2667
|
async function loadRulesText(host) {
|
|
2605
|
-
if (rulesTextCache[host])
|
|
2606
|
-
|
|
2607
|
-
try {
|
|
2608
|
-
const res = await fetch(`/api/setup/rules/${host}/text`);
|
|
2609
|
-
if (res.ok) text = (await res.json()).text || '';
|
|
2610
|
-
} catch { /* try the older endpoint */ }
|
|
2611
|
-
if (!text) {
|
|
2612
|
-
const format = host === 'opencode' ? 'generic' : host;
|
|
2613
|
-
const res = await fetch(`/api/claude-code/ruleset?format=${format}`);
|
|
2614
|
-
const data = res.ok ? await res.json() : {};
|
|
2615
|
-
text = data.ruleset || '';
|
|
2616
|
-
}
|
|
2617
|
-
if (!text) throw new Error('Rules text not available');
|
|
2618
|
-
rulesTextCache[host] = text;
|
|
2619
|
-
return text;
|
|
2620
|
-
}
|
|
2621
|
-
|
|
2622
|
-
function showRulesToast(msg, duration = 2600) {
|
|
2623
|
-
const toast = document.getElementById('rulesToast');
|
|
2624
|
-
if (!toast) return;
|
|
2625
|
-
toast.textContent = msg;
|
|
2626
|
-
toast.classList.add('show');
|
|
2627
|
-
clearTimeout(toast._timer);
|
|
2628
|
-
toast._timer = setTimeout(() => toast.classList.remove('show'), duration);
|
|
2668
|
+
if (!rulesTextCache[host]) rulesTextCache[host] = await fetchRulesTextWithFallback(host);
|
|
2669
|
+
return rulesTextCache[host];
|
|
2629
2670
|
}
|
|
2630
2671
|
|
|
2631
2672
|
function enabledRuleTools() {
|
|
2632
2673
|
return wizardState.enabledTools.map(id => TOOLS.find(t => t.id === id)).filter(Boolean);
|
|
2633
2674
|
}
|
|
2634
2675
|
|
|
2635
|
-
function
|
|
2636
|
-
const
|
|
2637
|
-
const
|
|
2638
|
-
const
|
|
2676
|
+
function renderRulesStep() {
|
|
2677
|
+
const el = (id) => document.getElementById(id);
|
|
2678
|
+
const tx = rulesTx;
|
|
2679
|
+
const plan = rulesInstallPlan(rulesStatus);
|
|
2680
|
+
const ready = !!tx && !!rulesStatus;
|
|
2681
|
+
|
|
2682
|
+
// What stands in the way, if anything: the step's own text, the installer, or no connected tool.
|
|
2683
|
+
const note = !tx ? 'This step could not load its text. Check that SynaBun is running, then try again.'
|
|
2684
|
+
: !rulesStatus ? `The rules installer did not answer. ${RESTART_HINT} The rules can also be installed later in ${RULES_SETTINGS_PATH}.`
|
|
2685
|
+
: !plan.connected.length ? 'No tool is connected yet. Connect one in the previous step and its rules are installed with it.'
|
|
2686
|
+
: '';
|
|
2687
|
+
const $note = el('rulesNote');
|
|
2688
|
+
$note.textContent = note;
|
|
2689
|
+
if (note && !ready) {
|
|
2690
|
+
const retry = document.createElement('button');
|
|
2691
|
+
retry.type = 'button';
|
|
2692
|
+
retry.textContent = 'Try again';
|
|
2693
|
+
retry.disabled = rulesBusy;
|
|
2694
|
+
retry.addEventListener('click', () => onRulesetStep());
|
|
2695
|
+
$note.appendChild(retry);
|
|
2696
|
+
}
|
|
2697
|
+
$note.style.display = note ? '' : 'none';
|
|
2698
|
+
|
|
2699
|
+
// The primary action, while a connected tool still has rules to install or update.
|
|
2700
|
+
rulesPending = ready && plan.pending.length > 0;
|
|
2701
|
+
el('rulesInstallAll').style.display = rulesPending ? '' : 'none';
|
|
2702
|
+
if (tx) {
|
|
2703
|
+
el('rulesInstallAllHelp').textContent = tx('settings.redesign.control.set002.help');
|
|
2704
|
+
el('rulesInstallAllBtn').textContent = tx('settings.redesign.control.set002.label');
|
|
2705
|
+
el('rulesCursorHelp').textContent = tx('settings.redesign.setup.cursorHelp');
|
|
2706
|
+
el('rulesCoexistHelp').textContent = tx('settings.redesign.setup.coexistHelp');
|
|
2707
|
+
}
|
|
2708
|
+
el('rulesInstallAllBtn').disabled = rulesBusy;
|
|
2709
|
+
|
|
2710
|
+
// The result: what the last install answered, or, with nothing left to install, the rules that are in place
|
|
2711
|
+
// (the toggles of Step 5 installed them) and the file each tool reads.
|
|
2712
|
+
const report = rulesReport || (ready && !rulesPending ? describeRulesInPlace(rulesStatus, { tx, labels: RULES_TOOL_NAMES }) : null);
|
|
2713
|
+
const $result = el('rulesResult');
|
|
2714
|
+
$result.style.display = report ? '' : 'none';
|
|
2715
|
+
if (report) {
|
|
2716
|
+
$result.className = 'rules-result ' + (report.ok === false ? 'fail' : 'ok');
|
|
2717
|
+
el('rulesResultMessage').textContent = report.message;
|
|
2718
|
+
el('rulesResultWhere').textContent = report.note || '';
|
|
2719
|
+
el('rulesResultWhere').style.display = report.note ? '' : 'none';
|
|
2720
|
+
}
|
|
2721
|
+
|
|
2722
|
+
// One row per tool that has rules or is connected. Without a status only the tools connected in Step 5 are known.
|
|
2723
|
+
const $list = el('rulesList');
|
|
2639
2724
|
$list.innerHTML = '';
|
|
2640
|
-
|
|
2641
|
-
const
|
|
2642
|
-
|
|
2643
|
-
|
|
2644
|
-
|
|
2645
|
-
|
|
2646
|
-
|
|
2647
|
-
for (const tool of tools) {
|
|
2648
|
-
const info = rulesStatus && rulesStatus.hosts ? rulesStatus.hosts[tool.id] : null;
|
|
2649
|
-
const state = info ? info.state : '';
|
|
2650
|
-
const good = state === 'installed' || state === 'newer';
|
|
2651
|
-
// A file without SynaBun's markers is the user's own: the server refuses to replace it, so no button.
|
|
2652
|
-
const primary = info && info.detected !== false && info.replaceable !== false ? (RULES_PRIMARY[state] || '') : '';
|
|
2653
|
-
const detail = !info ? RESTART_HINT
|
|
2654
|
-
: info.detected === false ? `${tool.name} was not found on this machine`
|
|
2655
|
-
: ((['conflict', 'error', 'shadowed'].includes(state) || info.replaceable === false) && (info.error || info.detail)) || info.path || '';
|
|
2656
|
-
|
|
2725
|
+
const idle = [];
|
|
2726
|
+
for (const tool of (tx ? (rulesStatus ? TOOLS : enabledRuleTools()) : [])) {
|
|
2727
|
+
const info = rulesStatus?.hosts?.[tool.id];
|
|
2728
|
+
const view = describeRulesHost(info, { label: tool.name, version: rulesStatus?.version, tx });
|
|
2729
|
+
// Rules are for a tool that talks to SynaBun. One that is not connected and has none is only named, under
|
|
2730
|
+
// the list: connecting it in Step 5 installs them.
|
|
2731
|
+
if (view.known && view.state === 'not-installed' && !plan.connected.includes(tool.id)) { idle.push(tool.name); continue; }
|
|
2657
2732
|
const row = document.createElement('div');
|
|
2658
|
-
row.className = 'mcp-toggle-row' + (good ? ' connected' : '');
|
|
2733
|
+
row.className = 'mcp-toggle-row' + (view.good ? ' connected' : '') + (view.note ? ' has-note' : '');
|
|
2659
2734
|
row.dataset.host = tool.id;
|
|
2660
2735
|
row.innerHTML = `
|
|
2661
2736
|
<div class="mtr-body">
|
|
2662
|
-
<div class="mtr-name">${tool.name}<span class="mtr-state${good ? ' ok' : ''}" data-role="state"></span></div>
|
|
2737
|
+
<div class="mtr-name">${tool.name}<span class="mtr-state${view.good ? ' ok' : ''}" data-role="state"></span></div>
|
|
2663
2738
|
<div class="mtr-path" data-role="path"></div>
|
|
2739
|
+
<div class="mtr-note" data-role="note" style="display:none"></div>
|
|
2664
2740
|
</div>
|
|
2665
2741
|
<div class="rules-actions">
|
|
2666
|
-
${
|
|
2667
|
-
<button type="button" data-role="view"
|
|
2668
|
-
<button type="button" data-role="copy">Copy</button>
|
|
2742
|
+
${view.action ? '<button type="button" class="primary" data-role="primary"></button>' : ''}
|
|
2743
|
+
${view.known ? '<button type="button" data-role="view"></button>' : ''}
|
|
2669
2744
|
</div>
|
|
2670
2745
|
`;
|
|
2671
|
-
row.querySelector('[data-role="state"]').textContent =
|
|
2746
|
+
row.querySelector('[data-role="state"]').textContent = view.badge;
|
|
2672
2747
|
const $path = row.querySelector('[data-role="path"]');
|
|
2673
|
-
$path.textContent =
|
|
2674
|
-
$path.title =
|
|
2675
|
-
row.querySelector('[data-role="
|
|
2676
|
-
|
|
2677
|
-
|
|
2748
|
+
$path.textContent = view.pathText;
|
|
2749
|
+
$path.title = view.path;
|
|
2750
|
+
const $rowNote = row.querySelector('[data-role="note"]');
|
|
2751
|
+
$rowNote.textContent = view.note;
|
|
2752
|
+
$rowNote.style.display = view.note ? '' : 'none';
|
|
2753
|
+
const $primary = row.querySelector('[data-role="primary"]');
|
|
2754
|
+
if ($primary) {
|
|
2755
|
+
$primary.textContent = view.actionLabel;
|
|
2756
|
+
$primary.disabled = rulesBusy;
|
|
2757
|
+
$primary.addEventListener('click', () => installToolRules(tool));
|
|
2758
|
+
}
|
|
2759
|
+
const $view = row.querySelector('[data-role="view"]');
|
|
2760
|
+
if ($view) {
|
|
2761
|
+
$view.textContent = tx(rulesViewHost === tool.id ? 'settings.redesign.setup.hide' : 'settings.redesign.setup.view');
|
|
2762
|
+
$view.setAttribute('aria-expanded', String(rulesViewHost === tool.id));
|
|
2763
|
+
$view.setAttribute('aria-controls', 'rulesViewCode');
|
|
2764
|
+
$view.addEventListener('click', () => viewRules(tool));
|
|
2765
|
+
}
|
|
2678
2766
|
$list.appendChild(row);
|
|
2679
2767
|
}
|
|
2768
|
+
el('rulesViewCode').style.display = rulesViewHost && $list.querySelector(`[data-host="${rulesViewHost}"] [data-role="view"]`) ? '' : 'none';
|
|
2769
|
+
// The other tools, in one line. With no tool connected at all, the note at the top already says it.
|
|
2770
|
+
const $idle = el('rulesIdle');
|
|
2771
|
+
$idle.textContent = idle.length && plan.connected.length ? `${tx('settings.redesign.setup.notConnected')}: ${idle.join(', ')}. Connecting a tool in the previous step installs its rules.` : '';
|
|
2772
|
+
$idle.style.display = $idle.textContent ? '' : 'none';
|
|
2773
|
+
|
|
2774
|
+
updateNav();
|
|
2680
2775
|
requestAnimationFrame(() => resizeViewport(currentStep));
|
|
2681
2776
|
}
|
|
2682
2777
|
|
|
2683
|
-
|
|
2684
|
-
|
|
2685
|
-
if (
|
|
2686
|
-
|
|
2778
|
+
// Run one install, keep what it answered for the result line, then read the status again.
|
|
2779
|
+
async function runRulesInstall(work) {
|
|
2780
|
+
if (rulesBusy) return;
|
|
2781
|
+
rulesBusy = true;
|
|
2782
|
+
renderRulesStep();
|
|
2687
2783
|
try {
|
|
2688
|
-
|
|
2689
|
-
method: 'POST',
|
|
2690
|
-
headers: { 'Content-Type': 'application/json' },
|
|
2691
|
-
body: JSON.stringify({ force }),
|
|
2692
|
-
});
|
|
2693
|
-
const data = await res.json().catch(() => ({}));
|
|
2694
|
-
if (!res.ok || data.ok === false) showRulesToast(data.error || (res.status === 404 ? RESTART_HINT : `${tool.name} rules were not installed`), 5000);
|
|
2695
|
-
else showRulesToast(`${tool.name} rules installed`);
|
|
2784
|
+
rulesReport = await work();
|
|
2696
2785
|
} catch (err) {
|
|
2697
|
-
|
|
2698
|
-
|
|
2699
|
-
|
|
2700
|
-
|
|
2786
|
+
// The server's own sentence (an edited copy, damaged markers, another SynaBun at work). A route that is not
|
|
2787
|
+
// there is the server that was not restarted.
|
|
2788
|
+
const message = err?.message === 'HTTP 404' ? RESTART_HINT : (err?.message || rulesTx('settings.redesign.setup.rulesActionFailed'));
|
|
2789
|
+
rulesReport = { message, note: '', ok: false };
|
|
2790
|
+
}
|
|
2791
|
+
rulesStatus = await fetchRulesStatus();
|
|
2792
|
+
rulesBusy = false;
|
|
2793
|
+
renderRulesStep();
|
|
2794
|
+
}
|
|
2795
|
+
|
|
2796
|
+
// "Install for all": every connected tool, by the installer's own rule. Same call as the button in Settings.
|
|
2797
|
+
function installAllToolRules() {
|
|
2798
|
+
runRulesInstall(async () => {
|
|
2799
|
+
const report = describeRulesInstallAll(await installAllRules(), { tx: rulesTx, labels: RULES_TOOL_NAMES });
|
|
2800
|
+
return { message: report.message, note: report.note, ok: report.failed.length === 0 };
|
|
2801
|
+
});
|
|
2802
|
+
}
|
|
2803
|
+
|
|
2804
|
+
// One tool's Install, Update or Replace. Replace is behind the confirm that says the edited copy is backed up.
|
|
2805
|
+
function installToolRules(tool) {
|
|
2806
|
+
const view = describeRulesHost(rulesStatus?.hosts?.[tool.id], { label: tool.name, version: rulesStatus?.version, tx: rulesTx });
|
|
2807
|
+
if (!view.action) return;
|
|
2808
|
+
if (view.force && !confirm(rulesTx('settings.redesign.setup.replaceYourEditedRulesWithSynabun', { label: tool.name }))) return;
|
|
2809
|
+
delete rulesTextCache[tool.id];
|
|
2810
|
+
runRulesInstall(async () => {
|
|
2811
|
+
const result = await installRules(tool.id, { force: view.force });
|
|
2812
|
+
return { message: describeRulesInstall(result, view.state, { label: tool.name, tx: rulesTx }), note: '', ok: true };
|
|
2813
|
+
});
|
|
2701
2814
|
}
|
|
2702
2815
|
|
|
2703
|
-
|
|
2704
|
-
|
|
2816
|
+
// View shows the exact text a tool receives, under the list; a second press hides it.
|
|
2817
|
+
async function viewRules(tool) {
|
|
2705
2818
|
const $code = document.getElementById('rulesViewCode');
|
|
2706
|
-
if (
|
|
2707
|
-
|
|
2819
|
+
if (rulesViewHost === tool.id) {
|
|
2820
|
+
rulesViewHost = '';
|
|
2708
2821
|
} else {
|
|
2709
|
-
try { $code.textContent = await loadRulesText(
|
|
2710
|
-
catch { $code.textContent =
|
|
2711
|
-
$
|
|
2712
|
-
|
|
2822
|
+
try { $code.textContent = await loadRulesText(tool.id); }
|
|
2823
|
+
catch { $code.textContent = rulesTx('settings.redesign.setup.couldNotLoadTheRules', { label: tool.name, RESTART_HINT }); }
|
|
2824
|
+
$code.setAttribute('aria-label', rulesTx('settings.redesign.setup.previewRules', { name: tool.name }));
|
|
2825
|
+
rulesViewHost = tool.id;
|
|
2713
2826
|
}
|
|
2714
|
-
|
|
2827
|
+
renderRulesStep();
|
|
2715
2828
|
}
|
|
2716
2829
|
|
|
2717
|
-
|
|
2718
|
-
let text;
|
|
2719
|
-
try { text = await loadRulesText(host); }
|
|
2720
|
-
catch { showRulesToast(`Could not load the ${name} rules. ${RESTART_HINT}`, 5000); return; }
|
|
2721
|
-
try {
|
|
2722
|
-
await navigator.clipboard.writeText(text);
|
|
2723
|
-
showRulesToast(`${name} rules copied`);
|
|
2724
|
-
} catch {
|
|
2725
|
-
// A refused clipboard is the browser's doing; restarting SynaBun would not help.
|
|
2726
|
-
showRulesToast('Could not copy: the browser refused access to the clipboard.', 5000);
|
|
2727
|
-
}
|
|
2728
|
-
}
|
|
2830
|
+
document.getElementById('rulesInstallAllBtn').addEventListener('click', () => installAllToolRules());
|
|
2729
2831
|
|
|
2730
2832
|
// A click-to-copy code box (the pattern of the MCP JSON box in Step 5). The
|
|
2731
2833
|
// label says "copied!" only once the clipboard took the text; a browser that
|
|
@@ -2747,7 +2849,7 @@ function wireRulesCopyBox(wrapId, codeId) {
|
|
|
2747
2849
|
});
|
|
2748
2850
|
}
|
|
2749
2851
|
|
|
2750
|
-
// A collapsible copy-only section: loads
|
|
2852
|
+
// A collapsible copy-only section, for text SynaBun cannot install: loads it the first time it opens.
|
|
2751
2853
|
function wireRulesExtra(sectionId, headerId, bodyId, wrapId, codeId, host, name) {
|
|
2752
2854
|
const $section = document.getElementById(sectionId);
|
|
2753
2855
|
const $body = document.getElementById(bodyId);
|
|
@@ -2768,15 +2870,10 @@ function wireRulesExtra(sectionId, headerId, bodyId, wrapId, codeId, host, name)
|
|
|
2768
2870
|
wireRulesExtra('rulesCursorSection', 'rulesCursorHeader', 'rulesCursorBody', 'rulesCursorWrap', 'rulesCursorCode', 'cursor', 'Cursor rules');
|
|
2769
2871
|
wireRulesExtra('rulesCoexistSection', 'rulesCoexistHeader', 'rulesCoexistBody', 'rulesCoexistWrap', 'rulesCoexistCode', 'coexistence', 'coexistence snippet');
|
|
2770
2872
|
|
|
2771
|
-
document.getElementById('rulesViewWrap').addEventListener('click', () => {
|
|
2772
|
-
const $wrap = document.getElementById('rulesViewWrap');
|
|
2773
|
-
const tool = TOOLS.find(t => t.id === $wrap.dataset.host);
|
|
2774
|
-
if (tool) copyRules(tool.id, tool.name);
|
|
2775
|
-
});
|
|
2776
|
-
|
|
2777
2873
|
async function onRulesetStep() {
|
|
2778
|
-
|
|
2779
|
-
|
|
2874
|
+
rulesReport = null;
|
|
2875
|
+
[rulesStatus] = await Promise.all([fetchRulesStatus(), loadRulesStrings()]);
|
|
2876
|
+
renderRulesStep();
|
|
2780
2877
|
if (currentStep === 6) resizeViewport(6);
|
|
2781
2878
|
}
|
|
2782
2879
|
|
|
@@ -2805,18 +2902,23 @@ async function onCompleteStep() {
|
|
|
2805
2902
|
} catch { /* non-critical — user can enable via Settings later */ }
|
|
2806
2903
|
}
|
|
2807
2904
|
|
|
2808
|
-
// Rules count as installed when every connected tool has its copy in place.
|
|
2809
|
-
|
|
2810
|
-
|
|
2811
|
-
const
|
|
2812
|
-
|
|
2905
|
+
// Rules count as installed when every connected tool has its copy in place. When they are not, the item says
|
|
2906
|
+
// for which tools and where the installer is from now on.
|
|
2907
|
+
rulesStatus = await fetchRulesStatus();
|
|
2908
|
+
const rulesPlan = rulesInstallPlan(rulesStatus);
|
|
2909
|
+
const rulesMissing = rulesPlan.connected.filter(host => !rulesPlan.inPlace.includes(host));
|
|
2910
|
+
const rulesInstalled = rulesPlan.connected.length > 0 && rulesMissing.length === 0;
|
|
2911
|
+
const rulesLabel = rulesInstalled ? 'Rules installed'
|
|
2912
|
+
: !rulesStatus ? `Rules not installed: the installer did not answer (${RULES_SETTINGS_PATH})`
|
|
2913
|
+
: !rulesPlan.connected.length ? 'Rules not installed: no tool is connected'
|
|
2914
|
+
: `Rules not installed for ${ruleToolNames(rulesMissing)} (${RULES_SETTINGS_PATH})`;
|
|
2813
2915
|
|
|
2814
2916
|
const items = [
|
|
2815
2917
|
{ label: 'Memory name set', done: !!wizardState.collectionName },
|
|
2816
2918
|
{ label: 'Dependencies installed & MCP built', done: wizardState.buildReady },
|
|
2817
2919
|
{ label: 'Projects registered', done: (wizardState.projects || []).length > 0 },
|
|
2818
2920
|
{ label: 'AI tool integration enabled', done: wizardState.enabledTools.length > 0 || wizardState.mcpWritten },
|
|
2819
|
-
{ label:
|
|
2921
|
+
{ label: rulesLabel, done: rulesInstalled },
|
|
2820
2922
|
];
|
|
2821
2923
|
|
|
2822
2924
|
const $list = document.getElementById('checklist');
|
|
@@ -2853,6 +2955,10 @@ async function init() {
|
|
|
2853
2955
|
if (data.packageRoot) {
|
|
2854
2956
|
wizardState.packageRoot = data.packageRoot;
|
|
2855
2957
|
}
|
|
2958
|
+
if (data.mcpCommand && Array.isArray(data.mcpArgs)) {
|
|
2959
|
+
wizardState.mcpCommand = data.mcpCommand;
|
|
2960
|
+
wizardState.mcpArgs = data.mcpArgs;
|
|
2961
|
+
}
|
|
2856
2962
|
} catch {}
|
|
2857
2963
|
|
|
2858
2964
|
// Enable the entrance animation only after every top-level initializer has
|