@dollhousemcp/mcp-server 2.0.15 → 2.0.17

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 (53) hide show
  1. package/CHANGELOG.md +10 -0
  2. package/README.md.backup +18 -0
  3. package/dist/elements/base/BaseElementManager.d.ts.map +1 -1
  4. package/dist/elements/base/BaseElementManager.js +17 -1
  5. package/dist/elements/memories/MemoryManager.d.ts.map +1 -1
  6. package/dist/elements/memories/MemoryManager.js +13 -2
  7. package/dist/generated/version.d.ts +2 -2
  8. package/dist/generated/version.js +3 -3
  9. package/dist/handlers/ElementCRUDHandler.d.ts.map +1 -1
  10. package/dist/handlers/ElementCRUDHandler.js +7 -3
  11. package/dist/handlers/element-crud/createElement.d.ts.map +1 -1
  12. package/dist/handlers/element-crud/createElement.js +6 -2
  13. package/dist/handlers/element-crud/editElement.d.ts.map +1 -1
  14. package/dist/handlers/element-crud/editElement.js +6 -2
  15. package/dist/handlers/element-crud/helpers.d.ts +2 -0
  16. package/dist/handlers/element-crud/helpers.d.ts.map +1 -1
  17. package/dist/handlers/element-crud/helpers.js +21 -2
  18. package/dist/handlers/mcp-aql/IntrospectionResolver.d.ts.map +1 -1
  19. package/dist/handlers/mcp-aql/IntrospectionResolver.js +34 -7
  20. package/dist/handlers/mcp-aql/MCPAQLHandler.d.ts.map +1 -1
  21. package/dist/handlers/mcp-aql/MCPAQLHandler.js +51 -20
  22. package/dist/handlers/mcp-aql/OperationSchema.d.ts.map +1 -1
  23. package/dist/handlers/mcp-aql/OperationSchema.js +6 -4
  24. package/dist/handlers/mcp-aql/evaluatePermission.d.ts +2 -1
  25. package/dist/handlers/mcp-aql/evaluatePermission.d.ts.map +1 -1
  26. package/dist/handlers/mcp-aql/evaluatePermission.js +23 -11
  27. package/dist/handlers/mcp-aql/policies/ElementPolicies.d.ts +8 -0
  28. package/dist/handlers/mcp-aql/policies/ElementPolicies.d.ts.map +1 -1
  29. package/dist/handlers/mcp-aql/policies/ElementPolicies.js +26 -1
  30. package/dist/handlers/strategies/BaseActivationStrategy.d.ts.map +1 -1
  31. package/dist/handlers/strategies/BaseActivationStrategy.js +12 -3
  32. package/dist/handlers/strategies/PersonaActivationStrategy.js +2 -2
  33. package/dist/utils/permissionHooks.d.ts +74 -0
  34. package/dist/utils/permissionHooks.d.ts.map +1 -0
  35. package/dist/utils/permissionHooks.js +771 -0
  36. package/dist/web/public/index.html +12 -6
  37. package/dist/web/public/permissions.css +11 -0
  38. package/dist/web/public/permissions.js +78 -35
  39. package/dist/web/public/setup.css +172 -1
  40. package/dist/web/public/setup.js +644 -38
  41. package/dist/web/routes/permissionRoutes.d.ts.map +1 -1
  42. package/dist/web/routes/permissionRoutes.js +95 -27
  43. package/dist/web/routes/setupRoutes.d.ts +4 -0
  44. package/dist/web/routes/setupRoutes.d.ts.map +1 -1
  45. package/dist/web/routes/setupRoutes.js +122 -39
  46. package/package.json +8 -1
  47. package/scripts/pretooluse-codex.sh +6 -0
  48. package/scripts/pretooluse-cursor.sh +6 -0
  49. package/scripts/pretooluse-dollhouse.sh +110 -0
  50. package/scripts/pretooluse-gemini.sh +6 -0
  51. package/scripts/pretooluse-vscode.sh +163 -0
  52. package/scripts/pretooluse-windsurf.sh +168 -0
  53. package/server.json +2 -2
@@ -11,22 +11,165 @@
11
11
  // ── Config builders ────────────────────────────────────────────────────
12
12
 
13
13
  const PKG = '@dollhousemcp/mcp-server';
14
+ const HOOKS_DIR = '~/.dollhouse/hooks';
15
+ const HOOK_BASE_SCRIPT_PATH = `${HOOKS_DIR}/pretooluse-dollhouse.sh`;
14
16
 
15
17
  /** Platform registry — drives config generation AND panel rendering */
16
18
  const PLATFORMS = [
17
19
  // Claude Desktop & Claude Code panels are handwritten in HTML (unique structure)
18
20
  { id: 'claude-desktop', rootKey: 'mcpServers' },
19
- { id: 'claude-code', rootKey: 'mcpServers', cli: 'claude' },
21
+ { id: 'claude-code', rootKey: 'mcpServers', cli: 'claude', hookSupport: 'verified', hookCommand: `bash ${HOOK_BASE_SCRIPT_PATH}`, hookConfigPath: '<code>~/.claude/settings.json</code>' },
20
22
  // These panels are generated from this data by renderGeneratedPanels()
21
- { id: 'cursor', rootKey: 'mcpServers', installClient: 'cursor', openClient: 'cursor', configPath: '<code>.cursor/mcp.json</code> in your project, or <code>~/.cursor/mcp.json</code> for all projects', hint: 'Or configure via Settings &gt; MCP Servers in the Cursor UI.' },
22
- { id: 'vscode', rootKey: 'servers', installClient: 'vscode', configPath: '<code>.vscode/mcp.json</code> in your workspace', hint: 'VS Code uses <code>"servers"</code>, not <code>"mcpServers"</code>.' },
23
- { id: 'codex', rootKey: 'mcpServers', installClient: 'codex', openClient: 'codex', cli: 'codex', toml: true, tomlPath: '<code>~/.codex/config.toml</code> (Codex uses TOML, not JSON)' },
24
- { id: 'gemini', rootKey: 'mcpServers', installClient: 'gemini-cli', openClient: 'gemini-cli', cli: 'gemini', configPath: '<code>~/.gemini/settings.json</code> or <code>.gemini/settings.json</code> in your project' },
25
- { id: 'windsurf', rootKey: 'mcpServers', installClient: 'windsurf', openClient: 'windsurf', configPath: '<code>~/.codeium/windsurf/mcp_config.json</code>', hint: 'Or click the MCPs icon in the Cascade panel &gt; Configure.' },
26
- { id: 'cline', rootKey: 'mcpServers', installClient: 'cline', configPath: '<code>cline_mcp_settings.json</code> via Cline\'s top nav &gt; Configure &gt; Advanced MCP Settings' },
27
- { id: 'lmstudio', rootKey: 'mcpServers', openClient: 'lmstudio', configPath: '<code>~/.lmstudio/mcp.json</code> (or open via Program tab &gt; Install &gt; Edit mcp.json)', hint: 'Restart LM Studio after saving.' },
23
+ { id: 'cursor', rootKey: 'mcpServers', installClient: 'cursor', openClient: 'cursor', configPath: '<code>.cursor/mcp.json</code> in your project, or <code>~/.cursor/mcp.json</code> for all projects', hint: 'Or configure via Settings &gt; MCP Servers in the Cursor UI.', hookSupport: 'partial', hookCommand: `bash ${HOOKS_DIR}/pretooluse-cursor.sh`, hookConfigPath: '<code>.cursor/hooks.json</code> in your project, or <code>~/.cursor/hooks.json</code> for all projects' },
24
+ { id: 'vscode', rootKey: 'servers', installClient: 'vscode', configPath: '<code>.vscode/mcp.json</code> in your workspace', hint: 'VS Code uses <code>"servers"</code>, not <code>"mcpServers"</code>.', hookSupport: 'partial', hookCommand: `bash ${HOOKS_DIR}/pretooluse-vscode.sh`, hookConfigPath: '<code>~/.copilot/hooks/dollhouse-permissions.json</code> plus <code>chat.hookFilesLocations</code> in VS Code user settings' },
25
+ { id: 'codex', rootKey: 'mcpServers', installClient: 'codex', openClient: 'codex', cli: 'codex', toml: true, tomlPath: '<code>~/.codex/config.toml</code> (Codex uses TOML, not JSON)', hookSupport: 'partial', hookCommand: `bash ${HOOKS_DIR}/pretooluse-codex.sh`, hookConfigPath: '<code>~/.codex/hooks.json</code> and <code>~/.codex/config.toml</code>' },
26
+ { id: 'gemini', rootKey: 'mcpServers', installClient: 'gemini-cli', openClient: 'gemini-cli', cli: 'gemini', configPath: '<code>~/.gemini/settings.json</code> or <code>.gemini/settings.json</code> in your project', hookSupport: 'partial', hookCommand: `bash ${HOOKS_DIR}/pretooluse-gemini.sh`, hookConfigPath: '<code>~/.gemini/settings.json</code> or <code>.gemini/settings.json</code> in your project' },
27
+ { id: 'windsurf', rootKey: 'mcpServers', installClient: 'windsurf', openClient: 'windsurf', configPath: '<code>~/.codeium/windsurf/mcp_config.json</code>', hint: 'Or click the MCPs icon in the Cascade panel &gt; Configure.', hookSupport: 'partial', hookCommand: `bash ${HOOKS_DIR}/pretooluse-windsurf.sh`, hookConfigPath: '<code>~/.codeium/windsurf/hooks.json</code> or <code>.windsurf/hooks.json</code> in your project' },
28
+ { id: 'cline', rootKey: 'mcpServers', installClient: 'cline', openClient: 'cline', configPath: 'Cline stores MCP servers in <code>cline_mcp_settings.json</code> inside its extension settings. Use Configure Now or open the file directly.' },
29
+ { id: 'lmstudio', rootKey: 'mcpServers', installClient: 'lmstudio', openClient: 'lmstudio', configPath: '<code>~/.lmstudio/mcp.json</code> (or open via Program tab &gt; Install &gt; Edit mcp.json)', hint: 'Restart LM Studio after saving.' },
28
30
  ];
29
31
 
32
+ const HOOK_BASE_SCRIPT = `#!/bin/bash
33
+ # pretooluse-dollhouse.sh — shared hook bridge for DollhouseMCP
34
+
35
+ PORT_FILE="$HOME/.dollhouse/run/permission-server.port"
36
+ HOOK_PLATFORM="\${DOLLHOUSE_HOOK_PLATFORM:-claude_code}"
37
+
38
+ [[ -f "$PORT_FILE" ]] || exit 0
39
+ PORT=$(cat "$PORT_FILE" 2>/dev/null)
40
+ [[ "$PORT" =~ ^[0-9]+$ ]] || exit 0
41
+
42
+ INPUT=$(cat)
43
+ TOOL_NAME=$(echo "$INPUT" | jq -r '.tool_name // .toolName // .tool // .name // empty' 2>/dev/null)
44
+ TOOL_INPUT=$(echo "$INPUT" | jq -c '.tool_input // .toolInput // .input // {}' 2>/dev/null)
45
+ [[ -n "$TOOL_NAME" ]] || exit 0
46
+
47
+ PAYLOAD=$(jq -cn \\
48
+ --arg tool_name "$TOOL_NAME" \\
49
+ --arg platform "$HOOK_PLATFORM" \\
50
+ --arg session_id "\${DOLLHOUSE_SESSION_ID:-}" \\
51
+ --argjson input "$TOOL_INPUT" \\
52
+ '{ tool_name: $tool_name, input: $input, platform: $platform }
53
+ + (if ($session_id | length) > 0 then { session_id: $session_id } else {} end)')
54
+
55
+ RESPONSE=$(curl -s --max-time 5 -X POST "http://127.0.0.1:$PORT/api/evaluate_permission" \\
56
+ -H "Content-Type: application/json" \\
57
+ -d "$PAYLOAD" 2>/dev/null)
58
+
59
+ [[ -n "$RESPONSE" ]] && echo "$RESPONSE"
60
+ exit 0`;
61
+
62
+ const buildHookWrapperScript = (platform) => `#!/bin/bash
63
+ # pretooluse-${platform}.sh — manual hook wrapper for DollhouseMCP
64
+
65
+ export DOLLHOUSE_HOOK_PLATFORM="${platform}"
66
+ SCRIPT_DIR="$(cd "$(dirname "\${BASH_SOURCE[0]}")" && pwd)"
67
+ exec bash "$SCRIPT_DIR/pretooluse-dollhouse.sh"`;
68
+
69
+ const CLAUDE_CODE_HOOK_SETTINGS = `{
70
+ "hooks": {
71
+ "PreToolUse": [
72
+ {
73
+ "matcher": "*",
74
+ "hooks": [
75
+ {
76
+ "type": "command",
77
+ "command": "bash ${HOOK_BASE_SCRIPT_PATH}"
78
+ }
79
+ ]
80
+ }
81
+ ]
82
+ }
83
+ }`;
84
+
85
+ const GEMINI_HOOK_SETTINGS = `{
86
+ "hooks": {
87
+ "BeforeTool": [
88
+ {
89
+ "matcher": ".*",
90
+ "hooks": [
91
+ {
92
+ "type": "command",
93
+ "command": "bash ${HOOKS_DIR}/pretooluse-gemini.sh"
94
+ }
95
+ ]
96
+ }
97
+ ]
98
+ }
99
+ }`;
100
+
101
+ const CODEX_HOOK_SETTINGS = `{
102
+ "hooks": {
103
+ "PreToolUse": [
104
+ {
105
+ "matcher": "Bash",
106
+ "hooks": [
107
+ {
108
+ "type": "command",
109
+ "command": "bash ${HOOKS_DIR}/pretooluse-codex.sh",
110
+ "statusMessage": "Checking Bash permissions"
111
+ }
112
+ ]
113
+ }
114
+ ]
115
+ }
116
+ }`;
117
+
118
+ const CURSOR_HOOK_SETTINGS = `{
119
+ "version": 1,
120
+ "hooks": {
121
+ "preToolUse": [
122
+ {
123
+ "type": "command",
124
+ "command": "bash ${HOOKS_DIR}/pretooluse-cursor.sh",
125
+ "matcher": ".*"
126
+ }
127
+ ]
128
+ }
129
+ }`;
130
+
131
+ const VSCODE_HOOK_SETTINGS = `{
132
+ "hooks": {
133
+ "PreToolUse": [
134
+ {
135
+ "matcher": "*",
136
+ "hooks": [
137
+ {
138
+ "type": "command",
139
+ "command": "bash ${HOOKS_DIR}/pretooluse-vscode.sh"
140
+ }
141
+ ]
142
+ }
143
+ ]
144
+ }
145
+ }`;
146
+
147
+ const VSCODE_HOOK_LOCATIONS_SETTINGS = `{
148
+ "chat.hookFilesLocations": {
149
+ "~/.copilot/hooks": true
150
+ }
151
+ }`;
152
+
153
+ const WINDSURF_HOOK_SETTINGS = `{
154
+ "hooks": {
155
+ "pre_run_command": [
156
+ {
157
+ "type": "command",
158
+ "command": "bash ${HOOKS_DIR}/pretooluse-windsurf.sh"
159
+ }
160
+ ],
161
+ "pre_mcp_tool_use": [
162
+ {
163
+ "type": "command",
164
+ "command": "bash ${HOOKS_DIR}/pretooluse-windsurf.sh"
165
+ }
166
+ ]
167
+ }
168
+ }`;
169
+
170
+ const CODEX_HOOK_FEATURES_TOML = `[features]
171
+ codex_hooks = true`;
172
+
30
173
  /** Build a JSON config block for a given npx command string */
31
174
  function jsonConfig(rootKey, npxCmd) {
32
175
  const parts = npxCmd.split(' ');
@@ -140,10 +283,11 @@
140
283
 
141
284
  if (prereq) prereq.hidden = method !== 'global';
142
285
  if (mcpbSection) mcpbSection.hidden = method !== 'global';
143
- if (channelToggle) channelToggle.hidden = method === 'global';
286
+ if (channelToggle) channelToggle.hidden = method !== 'npx';
144
287
 
145
- updateAllConfigs(method);
288
+ updateAllConfigs(method === 'permissions' ? 'npx' : method);
146
289
  updateInstallButtonLabels();
290
+ updateSetupModeSections();
147
291
  updateDetectionState();
148
292
  };
149
293
 
@@ -154,7 +298,7 @@
154
298
  // Sync initial visibility — if the browser restored a non-default
155
299
  // active button (e.g. pinned was selected before reload), apply
156
300
  // the hidden state now without waiting for a click.
157
- if (channelToggle) channelToggle.hidden = currentMethod === 'global';
301
+ if (channelToggle) channelToggle.hidden = currentMethod !== 'npx';
158
302
  };
159
303
 
160
304
  // ── Channel selector ──────────────────────────────────────────────────
@@ -221,6 +365,24 @@
221
365
  }
222
366
  };
223
367
 
368
+ const updateSetupModeSections = () => {
369
+ document.querySelectorAll('[data-setup-modes]').forEach((section) => {
370
+ const modes = (section.dataset.setupModes || '')
371
+ .split(/\s+/)
372
+ .filter(Boolean);
373
+ section.hidden = !modes.includes(currentMethod);
374
+ });
375
+
376
+ const permissionsIntro = document.getElementById('setup-permissions-intro');
377
+ if (permissionsIntro) {
378
+ permissionsIntro.hidden = currentMethod !== 'permissions';
379
+ }
380
+
381
+ document.querySelectorAll('.setup-installed-notice').forEach((notice) => {
382
+ notice.hidden = currentMethod === 'permissions';
383
+ });
384
+ };
385
+
224
386
  /** Update a single code block's displayed code and copy button */
225
387
  const updateCodeBlock = (block, config) => {
226
388
  if (!block || !config) return;
@@ -385,6 +547,32 @@
385
547
  : `${msg}. Try the manual config below.`;
386
548
  };
387
549
 
550
+ const buildInstallPayload = (client) => {
551
+ const payload = { client };
552
+ if (currentMethod === 'global' && pinnedVersion && pinnedVersion !== 'latest') {
553
+ payload.version = pinnedVersion;
554
+ } else if (currentChannel !== 'latest') {
555
+ payload.channel = currentChannel;
556
+ }
557
+ return payload;
558
+ };
559
+
560
+ const applyInstallSuccessState = (btn, status, data, verified) => {
561
+ btn.textContent = 'Installed';
562
+ btn.classList.remove('is-loading');
563
+ btn.classList.add('is-success');
564
+ if (!status) return;
565
+
566
+ if (data.hookInstall?.supported && !data.hookInstall?.configured && data.hookInstall?.assetsPrepared) {
567
+ status.textContent = 'Configured MCP server. Dollhouse hook assets were also prepared; finish manual permission setup in Permissions & Security.';
568
+ } else {
569
+ status.textContent = verified
570
+ ? 'Verified — config written. Restart the application to activate.'
571
+ : 'Restart the application to activate.';
572
+ }
573
+ status.classList.add('is-success');
574
+ };
575
+
388
576
  /** Handle Configure Now button click */
389
577
  const handleInstallClick = async (btn) => {
390
578
  const client = btn.dataset.installClient;
@@ -402,17 +590,10 @@
402
590
  }
403
591
 
404
592
  try {
405
- const payload = { client };
406
- if (currentMethod === 'global' && pinnedVersion && pinnedVersion !== 'latest') {
407
- payload.version = pinnedVersion;
408
- } else if (currentChannel !== 'latest') {
409
- payload.channel = currentChannel;
410
- }
411
-
412
593
  const res = await DollhouseAuth.apiFetch('/api/setup/install', {
413
594
  method: 'POST',
414
595
  headers: { 'Content-Type': 'application/json' },
415
- body: JSON.stringify(payload),
596
+ body: JSON.stringify(buildInstallPayload(client)),
416
597
  });
417
598
 
418
599
  const data = await res.json();
@@ -426,19 +607,72 @@
426
607
  await fetchDetection();
427
608
  updateDetectionState();
428
609
  const verified = detectedConfigs[clientToPlatformReverse[client]]?.installed;
610
+ applyInstallSuccessState(btn, status, data, verified);
429
611
 
430
- btn.textContent = 'Installed';
612
+ // Show the completion banner after any successful install
613
+ showCompletionBanner(client);
614
+ } catch (err) {
615
+ btn.textContent = originalText;
616
+ btn.disabled = false;
617
+ btn.classList.remove('is-loading');
618
+ if (status) {
619
+ status.textContent = formatInstallError(err);
620
+ status.classList.add('is-error');
621
+ }
622
+ }
623
+ };
624
+
625
+ const updatePermissionInstallButton = (btn, detected) => {
626
+ if (!btn || btn.classList.contains('is-success')) return;
627
+
628
+ if (detected?.hookInstalled) {
629
+ btn.textContent = 'Permissions enabled';
630
+ btn.disabled = true;
631
+ btn.classList.add('is-match');
632
+ return;
633
+ }
634
+
635
+ btn.textContent = 'Configure Now';
636
+ btn.disabled = false;
637
+ btn.classList.remove('is-match');
638
+ };
639
+
640
+ const handlePermissionInstallClick = async (btn) => {
641
+ const client = btn.dataset.permissionInstallClient;
642
+ if (!client) return;
643
+
644
+ const status = document.querySelector(`[data-permission-install-status="${client}"]`);
645
+ const originalText = btn.textContent;
646
+
647
+ btn.disabled = true;
648
+ btn.textContent = 'Configuring...';
649
+ btn.classList.add('is-loading');
650
+ if (status) {
651
+ status.textContent = '';
652
+ status.className = 'setup-install-status';
653
+ }
654
+
655
+ try {
656
+ const res = await DollhouseAuth.apiFetch('/api/setup/install', {
657
+ method: 'POST',
658
+ headers: { 'Content-Type': 'application/json' },
659
+ body: JSON.stringify({ client }),
660
+ });
661
+
662
+ const data = await res.json();
663
+ if (!data.success) throw new Error(data.error || 'Installation failed');
664
+
665
+ await fetchDetection();
666
+ updateDetectionState();
667
+
668
+ btn.textContent = 'Permissions enabled';
431
669
  btn.classList.remove('is-loading');
432
670
  btn.classList.add('is-success');
671
+
433
672
  if (status) {
434
- status.textContent = verified
435
- ? 'Verified — config written. Restart the application to activate.'
436
- : 'Restart the application to activate.';
673
+ status.textContent = data.hookInstall?.message || 'Permissions are enabled. Restart the client if it is already running.';
437
674
  status.classList.add('is-success');
438
675
  }
439
-
440
- // Show the completion banner after any successful install
441
- showCompletionBanner(client);
442
676
  } catch (err) {
443
677
  btn.textContent = originalText;
444
678
  btn.disabled = false;
@@ -551,6 +785,19 @@
551
785
  });
552
786
  };
553
787
 
788
+ const initPermissionInstallButtons = () => {
789
+ document.querySelectorAll('.setup-permission-install-btn').forEach((btn) => {
790
+ btn.addEventListener('click', () => handlePermissionInstallClick(btn));
791
+ const client = btn.dataset.permissionInstallClient;
792
+ const status = document.querySelector(`[data-permission-install-status="${client}"]`);
793
+ if (status) {
794
+ const statusId = `permission-install-status-${client}`;
795
+ status.id = statusId;
796
+ btn.setAttribute('aria-describedby', statusId);
797
+ }
798
+ });
799
+ };
800
+
554
801
  // ── Open config file buttons ───────────────────────────────────────────
555
802
 
556
803
  /** Handle Open config file button click */
@@ -712,19 +959,205 @@
712
959
  * Called on init and whenever the method toggle changes.
713
960
  */
714
961
  const updateDetectionState = () => {
715
- for (const platformId of Object.values(clientToPlatform)) {
962
+ const platformIds = new Set(['claude-desktop', ...PLATFORMS.map((platform) => platform.id)]);
963
+ for (const platformId of platformIds) {
716
964
  updatePlatformDetectionState(platformId);
717
965
  }
718
966
  };
719
967
 
968
+ const PERMISSION_PLATFORM_LABELS = {
969
+ 'claude-desktop': 'Claude Desktop',
970
+ 'claude-code': 'Claude Code',
971
+ cursor: 'Cursor',
972
+ vscode: 'VS Code',
973
+ codex: 'Codex',
974
+ gemini: 'Gemini CLI',
975
+ windsurf: 'Windsurf',
976
+ cline: 'Cline',
977
+ lmstudio: 'LM Studio',
978
+ };
979
+
980
+ const VERIFIED_PERMISSION_PLATFORMS = {
981
+ 'claude-code': {
982
+ label: 'Claude Code',
983
+ statusTag: 'claude code',
984
+ configPath: '<code>~/.claude/settings.json</code>',
985
+ scriptPath: HOOK_BASE_SCRIPT_PATH,
986
+ settingsBlock: CLAUDE_CODE_HOOK_SETTINGS,
987
+ },
988
+ };
989
+
990
+ const PARTIAL_PERMISSION_PLATFORMS = {
991
+ gemini: {
992
+ label: 'Gemini CLI',
993
+ statusTag: 'allow / deny',
994
+ configPath: '<code>~/.gemini/settings.json</code> or <code>.gemini/settings.json</code> in your project',
995
+ scriptPath: `${HOOKS_DIR}/pretooluse-gemini.sh`,
996
+ settingsBlock: GEMINI_HOOK_SETTINGS,
997
+ limitation: 'Gemini CLI exposes native BeforeTool hooks, but it does not support an ask/confirm response path. Confirmation-style policies currently degrade to deny.',
998
+ },
999
+ cursor: {
1000
+ label: 'Cursor',
1001
+ statusTag: 'native hooks',
1002
+ configPath: '<code>.cursor/hooks.json</code> in your project, or <code>~/.cursor/hooks.json</code> for all projects',
1003
+ scriptPath: `${HOOKS_DIR}/pretooluse-cursor.sh`,
1004
+ settingsBlock: CURSOR_HOOK_SETTINGS,
1005
+ limitation: 'Cursor exposes native hooks, but its permission handling still needs broader runtime verification across allow and ask decisions.',
1006
+ },
1007
+ vscode: {
1008
+ label: 'VS Code',
1009
+ statusTag: 'native hooks',
1010
+ configPath: '<code>~/.copilot/hooks/dollhouse-permissions.json</code> and VS Code user settings',
1011
+ scriptPath: `${HOOKS_DIR}/pretooluse-vscode.sh`,
1012
+ settingsBlock: VSCODE_HOOK_SETTINGS,
1013
+ featureBlock: VSCODE_HOOK_LOCATIONS_SETTINGS,
1014
+ featureHeading: '2. Enable <code>~/.copilot/hooks</code> in VS Code user settings',
1015
+ featureCopyLabel: 'Copy VS Code hookFilesLocations settings',
1016
+ limitation: 'VS Code exposes native PreToolUse hooks, but it ignores matcher values and uses tool names that differ from Claude Code. This adapter normalizes the common built-in tools we know about.',
1017
+ },
1018
+ windsurf: {
1019
+ label: 'Windsurf',
1020
+ statusTag: 'allow / deny',
1021
+ configPath: '<code>~/.codeium/windsurf/hooks.json</code> or <code>.windsurf/hooks.json</code> in your project',
1022
+ scriptPath: `${HOOKS_DIR}/pretooluse-windsurf.sh`,
1023
+ settingsBlock: WINDSURF_HOOK_SETTINGS,
1024
+ limitation: 'Windsurf exposes native pre-run and pre-MCP hooks, but they are binary allow-or-block hooks. Confirmation-style policies currently degrade to block.',
1025
+ },
1026
+ codex: {
1027
+ label: 'Codex',
1028
+ statusTag: 'bash only',
1029
+ configPath: '<code>~/.codex/hooks.json</code> and <code>~/.codex/config.toml</code>',
1030
+ scriptPath: `${HOOKS_DIR}/pretooluse-codex.sh`,
1031
+ settingsBlock: CODEX_HOOK_SETTINGS,
1032
+ featureBlock: CODEX_HOOK_FEATURES_TOML,
1033
+ limitation: 'Codex currently only supports native PreToolUse hooks for Bash, so this turns on Bash permission guardrails only.',
1034
+ },
1035
+ };
1036
+
1037
+ const getVerifiedPermissionStatusCopy = (verified, detected) => {
1038
+ if (detected?.hookInstalled) {
1039
+ return {
1040
+ tone: 'info',
1041
+ titleText: `${verified.label} permission enforcement is enabled.`,
1042
+ messageText: 'No further changes are needed here unless you want to reinstall the hook settings.',
1043
+ };
1044
+ }
1045
+
1046
+ if (detected?.installed) {
1047
+ return {
1048
+ tone: 'warning',
1049
+ titleText: `${verified.label} is connected for this client.`,
1050
+ messageText: `DollhouseMCP is configured as an MCP server. Use Configure Now below to also install the ${verified.label} permission hook.`,
1051
+ };
1052
+ }
1053
+
1054
+ return {
1055
+ tone: 'info',
1056
+ titleText: `${verified.label} permissions are not configured yet.`,
1057
+ messageText: `First connect DollhouseMCP using Auto-updating or Pinned version, then use Configure Now below to install the ${verified.label} permission hook.`,
1058
+ };
1059
+ };
1060
+
1061
+ const getPartialPermissionStatusCopy = (partial, detected) => {
1062
+ const activationLabel = partial.label === 'Codex' ? 'Bash guardrails' : 'permission hooks';
1063
+ if (detected?.hookInstalled) {
1064
+ return {
1065
+ tone: 'info',
1066
+ titleText: `${partial.label} ${activationLabel} are enabled.`,
1067
+ messageText: partial.limitation,
1068
+ };
1069
+ }
1070
+
1071
+ if (detected?.installed) {
1072
+ return {
1073
+ tone: 'warning',
1074
+ titleText: `${partial.label} is connected for this client.`,
1075
+ messageText: `DollhouseMCP is configured as an MCP server. Use Configure Now below to turn on ${partial.label}'s native ${activationLabel}.`,
1076
+ };
1077
+ }
1078
+
1079
+ return {
1080
+ tone: 'info',
1081
+ titleText: `${partial.label} ${activationLabel} are not configured yet.`,
1082
+ messageText: `First connect DollhouseMCP using Auto-updating or Pinned version, then use Configure Now below to install ${partial.label}'s native ${activationLabel}.`,
1083
+ };
1084
+ };
1085
+
1086
+ const getManualPermissionStatusCopy = (detected) => {
1087
+ if (detected?.hookAssetsPrepared) {
1088
+ return {
1089
+ tone: 'info',
1090
+ titleText: 'Hook bridge files are already prepared for this client.',
1091
+ messageText: 'Finish the client-specific hook registration below to turn on permission enforcement.',
1092
+ };
1093
+ }
1094
+ if (detected?.installed) {
1095
+ return {
1096
+ tone: 'warning',
1097
+ titleText: 'DollhouseMCP is connected for this client.',
1098
+ messageText: 'DollhouseMCP is configured here, but permission enforcement is separate. Use the manual hook steps below to turn it on for this client.',
1099
+ };
1100
+ }
1101
+
1102
+ return {
1103
+ tone: 'info',
1104
+ titleText: 'Manual permissions setup is available for this client.',
1105
+ messageText: 'Use the steps below if you want to turn on permission enforcement for this client manually.',
1106
+ };
1107
+ };
1108
+
1109
+ const getUnsupportedPermissionStatusCopy = (platformLabel, detected) => ({
1110
+ tone: detected?.installed ? 'warning' : 'neutral',
1111
+ titleText: `Permissions & security tools are unavailable for ${platformLabel} right now.`,
1112
+ messageText: detected?.installed
1113
+ ? 'DollhouseMCP is connected for this client, but this release does not include a supported permissions setup flow here yet.'
1114
+ : 'This release does not include a supported permissions setup flow for this client yet.',
1115
+ });
1116
+
1117
+ const getPermissionStatusCopy = (platformId, detected) => {
1118
+ const verified = VERIFIED_PERMISSION_PLATFORMS[platformId];
1119
+ if (verified) {
1120
+ return getVerifiedPermissionStatusCopy(verified, detected);
1121
+ }
1122
+
1123
+ const partial = PARTIAL_PERMISSION_PLATFORMS[platformId];
1124
+ if (partial) {
1125
+ return getPartialPermissionStatusCopy(partial, detected);
1126
+ }
1127
+
1128
+ const support = PLATFORMS.find((platform) => platform.id === platformId)?.hookSupport || 'unsupported';
1129
+ if (support === 'manual') {
1130
+ return getManualPermissionStatusCopy(detected);
1131
+ }
1132
+
1133
+ const platformLabel = PERMISSION_PLATFORM_LABELS[platformId] || 'this client';
1134
+ return getUnsupportedPermissionStatusCopy(platformLabel, detected);
1135
+ };
1136
+
1137
+ const updatePermissionStatus = (panel, platformId, detected) => {
1138
+ const status = panel?.querySelector('.setup-permission-status');
1139
+ if (!status) return;
1140
+
1141
+ const title = status.querySelector('.setup-permission-status-title');
1142
+ const message = status.querySelector('.setup-permission-status-msg');
1143
+ const { tone, titleText, messageText } = getPermissionStatusCopy(platformId, detected);
1144
+
1145
+ status.dataset.state = tone;
1146
+ if (title) title.textContent = titleText;
1147
+ if (message) message.textContent = messageText;
1148
+ };
1149
+
720
1150
  /** Update notice, badge, button, AND current config display for a single platform */
721
1151
  const updatePlatformDetectionState = (platformId) => {
722
1152
  const detected = detectedConfigs[platformId];
1153
+ const panel = document.getElementById('setup-panel-' + platformId);
1154
+ const tabBtn = document.getElementById('setup-tab-' + platformId);
1155
+ updatePermissionStatus(panel, platformId, detected);
1156
+ updatePermissionInstallButton(panel?.querySelector('.setup-permission-install-btn'), detected);
1157
+
723
1158
  if (!detected?.installed) return;
724
1159
 
725
1160
  const matches = configsMatch(platformId, currentMethod);
726
- const panel = document.getElementById('setup-panel-' + platformId);
727
- const tabBtn = document.getElementById('setup-tab-' + platformId);
728
1161
 
729
1162
  updateDetectionNotice(panel?.querySelector('.setup-installed-notice'), matches);
730
1163
  updateDetectionBadge(tabBtn?.querySelector('.setup-tab-badge'), matches);
@@ -830,6 +1263,8 @@
830
1263
  .replaceAll('>', '&gt;')
831
1264
  .replaceAll('"', '&quot;');
832
1265
 
1266
+ const escapeAttr = (str) => escapeHtml(str).replaceAll("'", '&#39;');
1267
+
833
1268
  // ── Generate platform panels from registry ─────────────────────────────
834
1269
 
835
1270
  /** Build an Open config file button string, or empty if no openClient */
@@ -840,13 +1275,13 @@
840
1275
  const renderInstallSection = (p) => {
841
1276
  let html = '';
842
1277
  if (p.installClient) {
843
- html += '<div class="setup-method setup-method-primary">';
1278
+ html += '<div class="setup-method setup-method-primary" data-setup-modes="npx global">';
844
1279
  html += `<div class="setup-install-row"><button class="setup-btn setup-btn-primary setup-install-btn" type="button" data-install-client="${p.installClient}">Configure Now</button>`;
845
1280
  html += `<span class="setup-install-status" data-install-status="${p.installClient}"></span></div>`;
846
1281
  }
847
1282
  if (p.cli) {
848
1283
  const cmd = `${p.cli} mcp add dollhousemcp -- npx -y ${PKG}@latest`;
849
- if (!p.installClient) html += '<div class="setup-method setup-method-primary">';
1284
+ if (!p.installClient) html += '<div class="setup-method setup-method-primary" data-setup-modes="npx global">';
850
1285
  html += '<h3>Or run in your terminal</h3><p>Run this in your terminal:</p>';
851
1286
  html += `<div class="setup-code-block"><button class="setup-copy-btn" type="button" data-copy-text="${cmd}" aria-label="Copy command">Copy</button>`;
852
1287
  html += `<pre><code>${cmd}</code></pre></div>`;
@@ -864,9 +1299,9 @@
864
1299
  let html = '';
865
1300
 
866
1301
  if (hasPrimaryBlock) {
867
- html += `</div><div class="setup-method"><h3>Or add config manually${openBtnHtml(p.openClient)}</h3>`;
1302
+ html += `</div><div class="setup-method" data-setup-modes="npx global"><h3>Or add config manually${openBtnHtml(p.openClient)}</h3>`;
868
1303
  } else {
869
- html += `<div class="setup-method setup-method-primary"><h3>Config${openBtnHtml(p.openClient)}</h3>`;
1304
+ html += `<div class="setup-method setup-method-primary" data-setup-modes="npx global"><h3>Config${openBtnHtml(p.openClient)}</h3>`;
870
1305
  }
871
1306
  html += `<p>Add to ${p.configPath}:</p>`;
872
1307
  html += `<div class="setup-code-block"><button class="setup-copy-btn" type="button" data-copy-text='${copyText}' aria-label="Copy config">Copy</button>`;
@@ -881,13 +1316,172 @@
881
1316
  if (!p.tomlPath) return '';
882
1317
  const tomlConfig = configs[p.id]?.npxToml;
883
1318
  const tomlCode = tomlConfig?.code || '';
884
- let html = `<div class="setup-method"><h3>Or add to config${openBtnHtml(p.openClient)}</h3>`;
1319
+ let html = `<div class="setup-method" data-setup-modes="npx global"><h3>Or add to config${openBtnHtml(p.openClient)}</h3>`;
885
1320
  html += `<p>Add to ${p.tomlPath}:</p>`;
886
1321
  html += `<div class="setup-code-block"><button class="setup-copy-btn" type="button" data-copy-text='${tomlCode}' aria-label="Copy config">Copy</button>`;
887
1322
  html += `<pre><code>${tomlCode}</code></pre></div></div>`;
888
1323
  return html;
889
1324
  };
890
1325
 
1326
+ const buildPartialAutoHint = (p, partial) => {
1327
+ const base = partial.limitation;
1328
+ if (p.id === 'codex') {
1329
+ return `${base} This automatic path writes the shared hook bridge, updates <code>~/.codex/hooks.json</code>, and enables <code>features.codex_hooks</code> in <code>~/.codex/config.toml</code>.`;
1330
+ }
1331
+ if (p.id === 'vscode') {
1332
+ return `${base} This automatic path writes the shared hook bridge, creates <code>~/.copilot/hooks/dollhouse-permissions.json</code>, and enables <code>~/.copilot/hooks</code> in VS Code's <code>chat.hookFilesLocations</code> setting.`;
1333
+ }
1334
+ return `${base} This automatic path writes the shared hook bridge and updates ${partial.configPath}.`;
1335
+ };
1336
+
1337
+ const buildPartialFeatureHeading = (p, partial) => {
1338
+ if (partial.featureHeading) return partial.featureHeading;
1339
+ if (p.id === 'codex') return '2. Enable Codex hooks in <code>~/.codex/config.toml</code>';
1340
+ return '2. Add the additional client settings';
1341
+ };
1342
+
1343
+ const renderVerifiedPermissionSection = (p, verified) => {
1344
+ const permissionInstallClient = p.installClient || p.id;
1345
+ return `<div class="setup-method setup-security-mode" data-setup-modes="permissions" hidden>
1346
+ <h3>Permissions &amp; Security <span class="setup-support-badge setup-support-badge--verified">${verified.statusTag}</span></h3>
1347
+ <div class="setup-permission-status" data-state="info">
1348
+ <strong class="setup-permission-status-title"></strong>
1349
+ <p class="setup-permission-status-msg"></p>
1350
+ </div>
1351
+ <div class="setup-install-row">
1352
+ <button class="setup-btn setup-btn-primary setup-permission-install-btn" type="button" data-permission-install-client="${permissionInstallClient}">Configure Now</button>
1353
+ <span class="setup-install-status" data-permission-install-status="${permissionInstallClient}"></span>
1354
+ </div>
1355
+ <p class="setup-hint">This writes the shared hook bridge assets and updates ${verified.configPath} automatically.</p>
1356
+ </div>
1357
+ <div class="setup-method setup-security-mode" data-setup-modes="permissions" hidden>
1358
+ <details class="setup-manual-fallback">
1359
+ <summary>Manual fallback</summary>
1360
+ <div class="setup-manual-fallback-body">
1361
+ <h4>1. Save the shared hook bridge once</h4>
1362
+ <p>Save this file as <code>${HOOK_BASE_SCRIPT_PATH}</code>, then make it executable with <code>chmod +x ${HOOK_BASE_SCRIPT_PATH}</code>.</p>
1363
+ <div class="setup-code-block"><button class="setup-copy-btn" type="button" data-copy-text='${escapeAttr(HOOK_BASE_SCRIPT)}' aria-label="Copy shared hook bridge">Copy</button>
1364
+ <pre><code>${escapeHtml(HOOK_BASE_SCRIPT)}</code></pre>
1365
+ </div>
1366
+ <h4>2. Add the ${verified.label} hook settings</h4>
1367
+ <p>Add this block to ${verified.configPath} so ${verified.label} can call the hook bridge before tool use.</p>
1368
+ <div class="setup-code-block"><button class="setup-copy-btn" type="button" data-copy-text='${escapeAttr(verified.settingsBlock)}' aria-label="Copy ${verified.label} hook settings">Copy</button>
1369
+ <pre><code>${escapeHtml(verified.settingsBlock)}</code></pre>
1370
+ </div>
1371
+ <p class="setup-hint">Command hook target: <code>${verified.scriptPath}</code></p>
1372
+ </div>
1373
+ </details>
1374
+ </div>`;
1375
+ };
1376
+
1377
+ const renderPartialPermissionSection = (p, partial) => {
1378
+ const permissionInstallClient = p.installClient || p.id;
1379
+ const autoHint = buildPartialAutoHint(p, partial);
1380
+ const featureHeading = buildPartialFeatureHeading(p, partial);
1381
+ const featureSection = partial.featureBlock
1382
+ ? `<h4>${featureHeading}</h4>
1383
+ <div class="setup-code-block"><button class="setup-copy-btn" type="button" data-copy-text='${escapeAttr(partial.featureBlock)}' aria-label="${escapeAttr(partial.featureCopyLabel || `Copy ${partial.label} settings`)}">Copy</button>
1384
+ <pre><code>${escapeHtml(partial.featureBlock)}</code></pre>
1385
+ </div>`
1386
+ : '';
1387
+ const stepNumber = partial.featureBlock ? '3' : '2';
1388
+
1389
+ return `<div class="setup-method setup-security-mode" data-setup-modes="permissions" hidden>
1390
+ <h3>Permissions &amp; Security <span class="setup-support-badge setup-support-badge--manual">${partial.statusTag}</span></h3>
1391
+ <div class="setup-permission-status" data-state="info">
1392
+ <strong class="setup-permission-status-title"></strong>
1393
+ <p class="setup-permission-status-msg"></p>
1394
+ </div>
1395
+ <div class="setup-install-row">
1396
+ <button class="setup-btn setup-btn-primary setup-permission-install-btn" type="button" data-permission-install-client="${permissionInstallClient}">Configure Now</button>
1397
+ <span class="setup-install-status" data-permission-install-status="${permissionInstallClient}"></span>
1398
+ </div>
1399
+ <p class="setup-hint">${autoHint}</p>
1400
+ </div>
1401
+ <div class="setup-method setup-security-mode" data-setup-modes="permissions" hidden>
1402
+ <details class="setup-manual-fallback">
1403
+ <summary>Manual fallback</summary>
1404
+ <div class="setup-manual-fallback-body">
1405
+ <h4>1. Save the shared hook bridge once</h4>
1406
+ <p>Save this file as <code>${HOOK_BASE_SCRIPT_PATH}</code>, then make it executable with <code>chmod +x ${HOOK_BASE_SCRIPT_PATH}</code>.</p>
1407
+ <div class="setup-code-block"><button class="setup-copy-btn" type="button" data-copy-text='${escapeAttr(HOOK_BASE_SCRIPT)}' aria-label="Copy shared hook bridge">Copy</button>
1408
+ <pre><code>${escapeHtml(HOOK_BASE_SCRIPT)}</code></pre>
1409
+ </div>
1410
+ ${featureSection}
1411
+ <h4>${stepNumber}. Add the ${partial.label} hook settings in ${partial.configPath}</h4>
1412
+ <div class="setup-code-block"><button class="setup-copy-btn" type="button" data-copy-text='${escapeAttr(partial.settingsBlock)}' aria-label="Copy ${partial.label} hook settings">Copy</button>
1413
+ <pre><code>${escapeHtml(partial.settingsBlock)}</code></pre>
1414
+ </div>
1415
+ <p class="setup-hint">Command hook target: <code>${partial.scriptPath}</code></p>
1416
+ </div>
1417
+ </details>
1418
+ </div>`;
1419
+ };
1420
+
1421
+ const renderPermissionSection = (p) => {
1422
+ const hookSupport = p.hookSupport || 'unsupported';
1423
+ const configPath = p.hookConfigPath || p.configPath || p.tomlPath || 'this client’s user configuration';
1424
+
1425
+ if (hookSupport === 'verified' && VERIFIED_PERMISSION_PLATFORMS[p.id]) {
1426
+ return renderVerifiedPermissionSection(p, VERIFIED_PERMISSION_PLATFORMS[p.id]);
1427
+ }
1428
+
1429
+ if (hookSupport === 'partial' && PARTIAL_PERMISSION_PLATFORMS[p.id]) {
1430
+ return renderPartialPermissionSection(p, PARTIAL_PERMISSION_PLATFORMS[p.id]);
1431
+ }
1432
+
1433
+ if (hookSupport === 'manual') {
1434
+ const platformName = p.id === 'gemini' ? 'gemini' : p.id;
1435
+ const wrapperFilename = `pretooluse-${platformName}.sh`;
1436
+ const wrapperScript = buildHookWrapperScript(platformName);
1437
+ return `<div class="setup-method setup-security-mode" data-setup-modes="permissions" hidden>
1438
+ <h3>Permissions &amp; Security <span class="setup-support-badge setup-support-badge--manual">manual setup</span></h3>
1439
+ <div class="setup-permission-status" data-state="info">
1440
+ <strong class="setup-permission-status-title"></strong>
1441
+ <p class="setup-permission-status-msg"></p>
1442
+ </div>
1443
+ <p>To turn on permission enforcement for this client manually, add this command anywhere the client supports a pre-tool or pre-command hook:</p>
1444
+ <div class="setup-code-block"><button class="setup-copy-btn" type="button" data-copy-text="${escapeAttr(p.hookCommand)}" aria-label="Copy hook command">Copy</button>
1445
+ <pre><code>${escapeHtml(p.hookCommand)}</code></pre>
1446
+ </div>
1447
+ <p>Save this wrapper in <code>${HOOKS_DIR}</code> alongside the shared Dollhouse hook bridge:</p>
1448
+ <div class="setup-code-block"><button class="setup-copy-btn" type="button" data-copy-text='${escapeAttr(wrapperScript)}' aria-label="Copy ${wrapperFilename}">Copy</button>
1449
+ <pre><code>${escapeHtml(wrapperScript)}</code></pre>
1450
+ </div>
1451
+ <p class="setup-hint">Known config location for this client: ${configPath}</p>
1452
+ </div>`;
1453
+ }
1454
+
1455
+ return `<div class="setup-method setup-security-mode" data-setup-modes="permissions" hidden>
1456
+ <h3>Permissions &amp; Security <span class="setup-support-badge setup-support-badge--unsupported">coming soon</span></h3>
1457
+ <div class="setup-permission-status" data-state="neutral">
1458
+ <strong class="setup-permission-status-title"></strong>
1459
+ <p class="setup-permission-status-msg"></p>
1460
+ </div>
1461
+ </div>`;
1462
+ };
1463
+
1464
+ const renderPermissionsIntro = () => {
1465
+ const intro = document.getElementById('setup-permissions-intro');
1466
+ if (!intro) return;
1467
+
1468
+ intro.innerHTML = `<div class="setup-permissions-note">
1469
+ <strong>Permissions &amp; Security</strong>
1470
+ <p>Use this mode to turn on permission enforcement for supported clients. Claude Code is fully guided in this release, and Gemini CLI, Cursor, VS Code, Windsurf, plus Codex have native partial support. Where we have workable manual steps for other clients, they are shown here. Otherwise, the client will be marked as coming soon.</p>
1471
+ </div>`;
1472
+ };
1473
+
1474
+ const injectStaticPermissionsSections = () => {
1475
+ const claudeDesktopPanel = document.getElementById('setup-panel-claude-desktop');
1476
+ const claudeCodePanel = document.getElementById('setup-panel-claude-code');
1477
+ const claudeCodeConfig = PLATFORMS.find((p) => p.id === 'claude-code');
1478
+
1479
+ claudeDesktopPanel?.insertAdjacentHTML('beforeend', renderPermissionSection({ id: 'claude-desktop' }));
1480
+ if (claudeCodePanel && claudeCodeConfig) {
1481
+ claudeCodePanel.insertAdjacentHTML('beforeend', renderPermissionSection(claudeCodeConfig));
1482
+ }
1483
+ };
1484
+
891
1485
  const renderGeneratedPanels = () => {
892
1486
  const container = document.getElementById('setup-generated-panels');
893
1487
  if (!container) return;
@@ -906,7 +1500,8 @@
906
1500
  section.innerHTML =
907
1501
  renderInstallSection(p) +
908
1502
  renderJsonSection(p, hasPrimaryBlock) +
909
- renderTomlSection(p);
1503
+ renderTomlSection(p) +
1504
+ renderPermissionSection(p);
910
1505
 
911
1506
  container.appendChild(section);
912
1507
  }
@@ -1232,9 +1827,16 @@
1232
1827
  if (license.useCase) rows.push(['Use case', license.useCase]);
1233
1828
  }
1234
1829
 
1235
- licenseInfoTable.innerHTML = rows
1236
- .map(([label, value]) => `<tr><td>${label}</td><td>${value}</td></tr>`)
1237
- .join('');
1830
+ const rowNodes = rows.map(([label, value]) => {
1831
+ const tr = document.createElement('tr');
1832
+ const labelCell = document.createElement('td');
1833
+ const valueCell = document.createElement('td');
1834
+ labelCell.textContent = label;
1835
+ valueCell.textContent = value;
1836
+ tr.append(labelCell, valueCell);
1837
+ return tr;
1838
+ });
1839
+ licenseInfoTable.replaceChildren(...rowNodes);
1238
1840
  licenseDetailsPanel.hidden = false;
1239
1841
  }
1240
1842
 
@@ -1350,15 +1952,19 @@
1350
1952
  // ── Init ──────────────────────────────────────────────────────────────
1351
1953
 
1352
1954
  const os = detectOS();
1955
+ renderPermissionsIntro();
1353
1956
  renderGeneratedPanels();
1957
+ injectStaticPermissionsSections();
1354
1958
  highlightOSPaths(os);
1355
1959
  initMethodToggle();
1356
1960
  initChannelSelector();
1357
1961
  initPlatformTabs();
1358
1962
  initCopyButtons();
1359
1963
  initInstallButtons();
1964
+ initPermissionInstallButtons();
1360
1965
  initOpenButtons();
1361
1966
  fetchVersion();
1362
1967
  fetchDetection();
1363
1968
  initLicense();
1969
+ updateSetupModeSections();
1364
1970
  })();