total-recall-brain 3.41.3 → 3.41.5

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 (96) hide show
  1. package/bin/total-recall.mjs +12 -2
  2. package/docs/reference/cli-reference.md +18 -0
  3. package/frontend/dist/assets/{index-DFenQQEQ.js → index-WT3gs-x6.js} +75 -70
  4. package/frontend/dist/index.html +1 -1
  5. package/package.json +7 -5
  6. package/scaffold/.agent/skills/total-recall/plugins/auth/package-lock.json +2 -2
  7. package/scaffold/.agent/skills/total-recall/plugins/auth/package.json +1 -1
  8. package/scaffold/.agent/skills/total-recall/plugins/auth/plugin.json +1 -1
  9. package/scaffold/.agent/skills/total-recall/plugins/auth/src/cli-secret.mjs +48 -0
  10. package/scaffold/.agent/skills/total-recall/plugins/auth/src/login-projections.mjs +42 -0
  11. package/scaffold/.agent/skills/total-recall/plugins/auth/src/mesh-secrets.mjs +18 -0
  12. package/scaffold/.agent/skills/total-recall/plugins/auth/src/routes/secrets.mjs +5 -2
  13. package/scaffold/.agent/skills/total-recall/plugins/auth/src/secrets-carriers.mjs +76 -0
  14. package/scaffold/.agent/skills/total-recall/plugins/auth/src/secrets-prune.mjs +37 -0
  15. package/scaffold/.agent/skills/total-recall/plugins/auth/src/secrets-sync.mjs +53 -17
  16. package/scaffold/.agent/skills/total-recall/plugins/code-quality/package-lock.json +3114 -36
  17. package/scaffold/.agent/skills/total-recall/plugins/code-quality/package.json +1 -1
  18. package/scaffold/.agent/skills/total-recall/plugins/composable-cli/package-lock.json +3114 -36
  19. package/scaffold/.agent/skills/total-recall/plugins/composable-cli/package.json +1 -1
  20. package/scaffold/.agent/skills/total-recall/plugins/embeddings/package-lock.json +3131 -28
  21. package/scaffold/.agent/skills/total-recall/plugins/embeddings/package.json +6 -2
  22. package/scaffold/.agent/skills/total-recall/plugins/git-sentinel/package-lock.json +3132 -29
  23. package/scaffold/.agent/skills/total-recall/plugins/git-sentinel/package.json +5 -1
  24. package/scaffold/.agent/skills/total-recall/plugins/mesh/package-lock.json +3164 -73
  25. package/scaffold/.agent/skills/total-recall/plugins/mesh/package.json +5 -3
  26. package/scaffold/.agent/skills/total-recall/plugins/mesh/plugin.json +1 -1
  27. package/scaffold/.agent/skills/total-recall/plugins/mesh/src/cli/grants.mjs +7 -3
  28. package/scaffold/.agent/skills/total-recall/plugins/mesh/src/cli/mesh.mjs +61 -38
  29. package/scaffold/.agent/skills/total-recall/plugins/mesh/src/core/leader-election.mjs +57 -2
  30. package/scaffold/.agent/skills/total-recall/plugins/mesh/src/core/mesh.mjs +220 -36
  31. package/scaffold/.agent/skills/total-recall/plugins/mesh/src/server/routes/mesh.mjs +7 -2
  32. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/README.md +65 -0
  33. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/capability/BRICK.md +59 -0
  34. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/capability/index.mjs +534 -0
  35. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/cli.mjs +139 -0
  36. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/package-lock.json +5937 -0
  37. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/package.json +26 -0
  38. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/plugin.json +376 -0
  39. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/skills/meta-harness/SKILL.md +37 -0
  40. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/adapters/dsh.mjs +446 -0
  41. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/alerts.mjs +23 -0
  42. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/coordinator.mjs +398 -0
  43. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/documentation.mjs +172 -0
  44. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/executor.mjs +82 -0
  45. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/learner.mjs +90 -0
  46. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/router.mjs +415 -0
  47. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/runtime.mjs +36 -0
  48. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/setup.mjs +35 -0
  49. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/tracker.mjs +133 -0
  50. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/worker.mjs +233 -0
  51. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/ui/DESIGN.md +32 -0
  52. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/ui/card.js +845 -0
  53. package/scaffold/.agent/skills/total-recall/plugins/openwiki/capability/BRICK.md +64 -0
  54. package/scaffold/.agent/skills/total-recall/plugins/openwiki/capability/index.mjs +250 -0
  55. package/scaffold/.agent/skills/total-recall/plugins/openwiki/cli.mjs +619 -41
  56. package/scaffold/.agent/skills/total-recall/plugins/openwiki/package-lock.json +3134 -31
  57. package/scaffold/.agent/skills/total-recall/plugins/openwiki/package.json +9 -3
  58. package/scaffold/.agent/skills/total-recall/plugins/openwiki/plugin.json +413 -14
  59. package/scaffold/.agent/skills/total-recall/plugins/openwiki/src/config.mjs +129 -0
  60. package/scaffold/.agent/skills/total-recall/plugins/openwiki/src/generate.mjs +159 -0
  61. package/scaffold/.agent/skills/total-recall/plugins/openwiki/src/graph.mjs +197 -0
  62. package/scaffold/.agent/skills/total-recall/plugins/openwiki/src/lint.mjs +153 -0
  63. package/scaffold/.agent/skills/total-recall/plugins/openwiki/src/mesh-vaults.mjs +261 -0
  64. package/scaffold/.agent/skills/total-recall/plugins/openwiki/src/refresh.mjs +502 -0
  65. package/scaffold/.agent/skills/total-recall/plugins/openwiki/src/search.mjs +114 -0
  66. package/scaffold/.agent/skills/total-recall/plugins/openwiki/src/token.mjs +96 -0
  67. package/scaffold/.agent/skills/total-recall/plugins/openwiki/src/upstream-watcher.mjs +375 -0
  68. package/scaffold/.agent/skills/total-recall/plugins/openwiki/ui/DESIGN.md +35 -22
  69. package/scaffold/.agent/skills/total-recall/plugins/openwiki/ui/card.js +1225 -364
  70. package/scaffold/.agent/skills/total-recall/plugins/openwiki/vendor/openwiki.mjs +8987 -0
  71. package/scaffold/.agent/skills/total-recall/plugins/security/package-lock.json +3114 -83
  72. package/scaffold/.agent/skills/total-recall/plugins/security/package.json +5 -1
  73. package/src/cli/compact.mjs +108 -0
  74. package/src/cli/config.mjs +3 -0
  75. package/src/cli/connect.mjs +12 -116
  76. package/src/cli/context.mjs +52 -23
  77. package/src/cli/forget.mjs +2 -0
  78. package/src/cli/harness.mjs +0 -1
  79. package/src/cli/plugin/create.mjs +56 -3
  80. package/src/cli/plugin/index.mjs +15 -3
  81. package/src/cli/plugin/monetization.mjs +232 -12
  82. package/src/cli/preflight.mjs +161 -0
  83. package/src/cli/release.mjs +386 -0
  84. package/src/core/cli-commands.mjs +4 -0
  85. package/src/core/context-compactor.mjs +143 -0
  86. package/src/core/context-compiler.mjs +8 -1
  87. package/src/core/default-plugins.mjs +1 -1
  88. package/src/core/meta-harness.mjs +16 -38
  89. package/src/core/plugin-dependencies.mjs +6 -1
  90. package/src/core/preflight-gate.mjs +77 -0
  91. package/src/core/preflight-receipt.mjs +123 -0
  92. package/src/core/runtime.mjs +0 -3
  93. package/src/core/surface.mjs +39 -2
  94. package/src/server/invoke-durability.mjs +39 -0
  95. package/src/server/invoke-write-child.mjs +25 -0
  96. package/src/server/routes/invoke.mjs +7 -0
@@ -1,540 +1,1401 @@
1
1
  /**
2
- * Brain Dump — Multi-Repo Knowledge Graph & Documentation Console
2
+ * OpenWiki — Knowledge Graph & Multi-Repo Documentation Wiki
3
+ * Bespoke Card Face: Deep Slate & Knowledge Amber Interface.
3
4
  *
4
- * Bespoke operator interface for cross-repository documentation and knowledge graphs.
5
- * Displays real-time hub trees, file metrics, structured ingestion controllers,
6
- * and bootstrap verification without template wrappers or terminal dumps.
5
+ * Exposes live hub discovery, full markdown reading, evidence search across all vaults,
6
+ * interactive knowledge topology graph, targeted on-demand generation, fleet refresh,
7
+ * Total Recall auth secrets vault key management, and documentation link hygiene linting.
8
+ *
9
+ * Operates without mock data, zero templates, zero emojis, zero Winamp skeuomorphism.
7
10
  */
8
11
 
9
- const DASH = '\u2014';
10
-
11
- function escapeHtml(str) {
12
- if (str === null || str === undefined) return '';
13
- return String(str).replace(/[&<>"']/g, c => ({
14
- '&': '&amp;',
15
- '<': '&lt;',
16
- '>': '&gt;',
17
- '"': '&quot;',
18
- "'": '&#39;'
19
- }[c]));
20
- }
21
-
22
12
  export class OpenWikiCard extends HTMLElement {
13
+ static get screens() {
14
+ return [
15
+ { id: 'hubs', label: 'Hubs' },
16
+ { id: 'search', label: 'Search' },
17
+ { id: 'graph', label: 'Graph' },
18
+ { id: 'generate', label: 'Generate' },
19
+ { id: 'refresh', label: 'Fleet Refresh' },
20
+ { id: 'token', label: 'Auth Vault' },
21
+ { id: 'lint', label: 'Health' },
22
+ { id: 'upstream', label: 'Upstream Watcher' },
23
+ { id: 'config', label: 'Configuration' }
24
+ ];
25
+ }
26
+
27
+ get screens() {
28
+ return OpenWikiCard.screens;
29
+ }
30
+
23
31
  constructor() {
24
32
  super();
33
+ this.activeScreen = 'hubs';
34
+ this.scope = 'workspace'; // 'workspace' | 'all-vaults' | '<mesh-node>'
35
+ this.meshNodes = [];
25
36
  this.hubs = [];
26
- this.totalLines = 0;
27
- this.executing = false;
37
+ this.selectedHub = null;
38
+ this.selectedHubContent = null;
39
+ this.loading = false;
40
+ this.statusText = 'READY';
28
41
  this.targetPath = 'openwiki';
29
- this.dryRun = true;
30
- this.lastResult = null;
31
- this.screens = [{ id: 'knowledge', label: 'Knowledge Hubs' }];
32
- this.screen = 'knowledge';
42
+ this.dryRun = false;
43
+ this.executionResult = null;
44
+ this.errorMessage = null;
45
+ this.lastRefreshedAt = null;
46
+ this.upstream = null;
47
+
48
+ // Search state
49
+ this.searchQuery = '';
50
+ this.searchResults = [];
51
+ this.searchStats = null;
52
+
53
+ // Graph state
54
+ this.graphData = null;
55
+ this.graphFormat = 'svg'; // 'svg' | 'mermaid' | 'json'
56
+
57
+ // Token state
58
+ this.tokenStatus = {
59
+ openrouter: { key: 'OPENROUTER_API_KEY', configured: false, status: 'unknown' },
60
+ google: { key: 'GOOGLE_API_KEY', configured: false, status: 'unknown' }
61
+ };
62
+
63
+ // Lint state
64
+ this.lintData = null;
65
+
66
+ // Generator state
67
+ this.generateRepo = '';
68
+ this.generateLogs = [];
69
+
70
+ // Fleet refresh state
71
+ this.refreshReports = [];
72
+ this.refreshCounts = null;
73
+
74
+ // Configuration state
75
+ this.config = {
76
+ provider: 'openrouter',
77
+ model: 'deepseek/deepseek-chat',
78
+ fallback_model: 'qwen/qwen-2.5-coder-32b-instruct',
79
+ max_tokens: 4096,
80
+ timeout_ms: 300000
81
+ };
82
+
83
+ this._boundOnScreenChange = this.handleExternalScreenChange.bind(this);
33
84
  }
34
85
 
35
86
  connectedCallback() {
36
87
  if (!this.shadowRoot) {
37
88
  this.attachShadow({ mode: 'open' });
38
89
  }
90
+ this.addEventListener('tr:screen-change', this._boundOnScreenChange);
91
+ this.addEventListener('plugin-show-screen', this._boundOnScreenChange);
39
92
  this.render();
40
93
  this.refreshHubs();
94
+ this.loadTokenStatus();
95
+ this.loadConfig();
41
96
  }
42
97
 
43
- async refreshHubs() {
44
- if (!this.host) return;
98
+ disconnectedCallback() {
99
+ this.removeEventListener('tr:screen-change', this._boundOnScreenChange);
100
+ this.removeEventListener('plugin-show-screen', this._boundOnScreenChange);
101
+ }
102
+
103
+ handleExternalScreenChange(event) {
104
+ const screenId = event.detail?.screen || event.detail?.screenId || event.detail?.id;
105
+ if (screenId && OpenWikiCard.screens.some((s) => s.id === screenId)) {
106
+ this.setScreen(screenId);
107
+ }
108
+ }
109
+
110
+ setScreen(id) {
111
+ if (this.activeScreen === id && !this.selectedHub) return;
112
+ this.activeScreen = id;
113
+ this.selectedHub = null;
114
+ this.selectedHubContent = null;
115
+ this.errorMessage = null;
116
+ this.render();
117
+
118
+ if (typeof this.host?.screenChanged === 'function') {
119
+ try {
120
+ this.host.screenChanged({ screen: id });
121
+ } catch {
122
+ // optional host callback
123
+ }
124
+ }
125
+
126
+ if (id === 'graph' && !this.graphData) {
127
+ this.loadGraph();
128
+ } else if (id === 'token') {
129
+ this.loadTokenStatus();
130
+ } else if (id === 'lint' && !this.lintData) {
131
+ this.loadLint();
132
+ } else if (id === 'refresh' && (!this.refreshReports || this.refreshReports.length === 0)) {
133
+ this.loadRefreshReport();
134
+ } else if (id === 'config') {
135
+ this.loadConfig();
136
+ }
137
+ }
138
+
139
+ setScope(newScope) {
140
+ if (this.scope === newScope) return;
141
+ this.scope = newScope;
142
+ this.refreshHubs();
143
+ if (this.activeScreen === 'graph') this.loadGraph();
144
+ if (this.activeScreen === 'lint') this.loadLint();
145
+ if (this.activeScreen === 'search' && this.searchQuery) this.executeSearch(this.searchQuery);
146
+ }
147
+
148
+ async loadConfig() {
45
149
  try {
46
- const res = await this.host.run('status', ['--json']);
47
- let parsed = [];
48
- if (res && typeof res === 'object') {
49
- if (Array.isArray(res.hubs)) {
50
- parsed = res.hubs;
51
- this.totalLines = res.total_lines || 0;
52
- } else if (Array.isArray(res)) {
53
- parsed = res;
54
- }
55
- } else if (typeof res === 'string' && res.trim().startsWith('{')) {
56
- const json = JSON.parse(res);
57
- parsed = Array.isArray(json.hubs) ? json.hubs : [];
58
- this.totalLines = json.total_lines || 0;
150
+ if (this.host && typeof this.host.run === 'function') {
151
+ const res = await this.host.run('config', ['show', '--json']);
152
+ if (res?.config) {
153
+ this.config = res.config;
154
+ this.render();
155
+ }
59
156
  }
157
+ } catch {
158
+ // Retain active defaults
159
+ }
160
+ }
60
161
 
61
- this.hubs = parsed;
162
+ async setConfigValue(key, value) {
163
+ this.loading = true;
164
+ this.errorMessage = null;
165
+ this.render();
166
+ try {
167
+ if (this.host && typeof this.host.run === 'function') {
168
+ const res = await this.host.run('config', ['set', key, String(value), '--json']);
169
+ if (res?.config) {
170
+ this.config = res.config;
171
+ }
172
+ }
173
+ } catch (err) {
174
+ this.errorMessage = err.message || `Failed to update ${key}`;
175
+ } finally {
176
+ this.loading = false;
62
177
  this.render();
178
+ }
179
+ }
180
+
181
+ async loadTokenStatus() {
182
+ try {
183
+ if (this.host && typeof this.host.run === 'function') {
184
+ const res = await this.host.run('token', ['status', '--json']);
185
+ if (res?.tokens) {
186
+ this.tokenStatus = res.tokens;
187
+ this.render();
188
+ }
189
+ }
63
190
  } catch {
191
+ // optional fallback
192
+ }
193
+ }
194
+
195
+ async saveToken(provider, tokenValue) {
196
+ if (!tokenValue || !tokenValue.trim()) return;
197
+ this.loading = true;
198
+ this.errorMessage = null;
199
+ this.render();
200
+ try {
201
+ if (this.host && typeof this.host.run === 'function') {
202
+ await this.host.run('token', ['set', provider, tokenValue.trim(), '--json']);
203
+ await this.loadTokenStatus();
204
+ }
205
+ } catch (err) {
206
+ this.errorMessage = err.message || `Failed to save ${provider} token`;
207
+ } finally {
208
+ this.loading = false;
209
+ this.render();
210
+ }
211
+ }
212
+
213
+ async refreshHubs() {
214
+ this.loading = true;
215
+ this.errorMessage = null;
216
+ this.render();
217
+
218
+ try {
219
+ if (this.host && typeof this.host.run === 'function') {
220
+ const args = ['--json'];
221
+ if (this.scope === 'all-vaults') {
222
+ args.push('--all-vaults');
223
+ } else if (this.scope !== 'workspace') {
224
+ args.push(`--node=${this.scope}`);
225
+ }
226
+
227
+ const res = await this.host.run('hubs', args);
228
+ if (res && Array.isArray(res.hubs)) {
229
+ this.hubs = res.hubs;
230
+ this.statusText = 'SYNCHRONIZED';
231
+ } else {
232
+ this.hubs = [];
233
+ this.statusText = 'UNINDEXED';
234
+ }
235
+
236
+ try {
237
+ const statusRes = await this.host.run('status', ['--json']);
238
+ if (statusRes?.upstream) {
239
+ this.upstream = statusRes.upstream;
240
+ }
241
+ } catch {
242
+ // Upstream metadata optional
243
+ }
244
+ } else {
245
+ this.statusText = 'DISCONNECTED';
246
+ }
247
+ this.lastRefreshedAt = new Date().toLocaleTimeString();
248
+ } catch (err) {
249
+ this.statusText = 'ERROR';
250
+ this.errorMessage = err.message || 'Hub synchronization failure';
64
251
  this.hubs = [];
252
+ } finally {
253
+ this.loading = false;
254
+ this.render();
255
+ }
256
+ }
257
+
258
+ async readHubDocument(hub) {
259
+ this.selectedHub = hub;
260
+ this.loading = true;
261
+ this.render();
262
+ try {
263
+ if (this.host && typeof this.host.readFile === 'function') {
264
+ this.selectedHubContent = await this.host.readFile(hub.path);
265
+ } else {
266
+ this.selectedHubContent = `# ${hub.title}\n\n*File:* \`${hub.path}\`\n\n*Lines:* ${hub.lines} | *Size:* ${hub.size_bytes} bytes\n\n*(Full live rendering available)*`;
267
+ }
268
+ } catch {
269
+ this.selectedHubContent = `# ${hub.title}\n\n*Unable to load full content from ${hub.path}*`;
270
+ } finally {
271
+ this.loading = false;
65
272
  this.render();
66
273
  }
67
274
  }
68
275
 
69
- async runIngest() {
70
- if (!this.host || this.executing) return;
71
- this.executing = true;
72
- this.lastResult = null;
276
+ async executeSearch(query) {
277
+ if (!query || !query.trim()) {
278
+ this.searchResults = [];
279
+ this.searchStats = null;
280
+ this.render();
281
+ return;
282
+ }
283
+
284
+ this.loading = true;
285
+ this.searchQuery = query.trim();
286
+ this.errorMessage = null;
73
287
  this.render();
74
288
 
75
289
  try {
76
- const args = [this.targetPath || 'openwiki'];
77
- if (this.dryRun) args.push('--dry-run');
78
- args.push('--json');
79
-
80
- const res = await this.host.run('ingest', args);
81
- this.lastResult = {
82
- ok: true,
83
- message: this.dryRun ? 'Dry-run preview completed' : 'Ingestion completed successfully',
84
- data: res
85
- };
86
- await this.refreshHubs();
290
+ if (this.host && typeof this.host.run === 'function') {
291
+ const args = [this.searchQuery, '--json'];
292
+ if (this.scope === 'all-vaults') args.push('--all-vaults');
293
+ else if (this.scope !== 'workspace') args.push(`--node=${this.scope}`);
294
+
295
+ const res = await this.host.run('search', args);
296
+ if (res && Array.isArray(res.results)) {
297
+ this.searchResults = res.results;
298
+ this.searchStats = {
299
+ totalHubs: res.totalHubsScanned || 0,
300
+ count: res.count || 0,
301
+ scope: res.scope || this.scope
302
+ };
303
+ }
304
+ }
305
+ } catch (err) {
306
+ this.errorMessage = err.message || 'Search execution failure';
307
+ this.searchResults = [];
308
+ } finally {
309
+ this.loading = false;
310
+ this.render();
311
+ }
312
+ }
313
+
314
+ async loadGraph() {
315
+ this.loading = true;
316
+ this.errorMessage = null;
317
+ this.render();
318
+ try {
319
+ if (this.host && typeof this.host.run === 'function') {
320
+ const args = ['--format=json', '--json'];
321
+ if (this.scope === 'all-vaults') args.push('--all-vaults');
322
+ else if (this.scope !== 'workspace') args.push(`--node=${this.scope}`);
323
+
324
+ const res = await this.host.run('graph', args);
325
+ if (res?.nodes) {
326
+ this.graphData = res;
327
+ }
328
+ }
87
329
  } catch (err) {
88
- this.lastResult = {
89
- ok: false,
90
- message: err instanceof Error ? err.message : 'Ingestion failed'
91
- };
330
+ this.errorMessage = err.message || 'Graph build failure';
92
331
  } finally {
93
- this.executing = false;
332
+ this.loading = false;
94
333
  this.render();
95
334
  }
96
335
  }
97
336
 
98
- async runBootstrap() {
99
- if (!this.host || this.executing) return;
100
- this.executing = true;
101
- this.lastResult = null;
337
+ async loadLint() {
338
+ this.loading = true;
339
+ this.errorMessage = null;
102
340
  this.render();
341
+ try {
342
+ if (this.host && typeof this.host.run === 'function') {
343
+ const args = ['--json'];
344
+ if (this.scope === 'all-vaults') args.push('--all-vaults');
345
+ else if (this.scope !== 'workspace') args.push(`--node=${this.scope}`);
346
+
347
+ const res = await this.host.run('lint', args);
348
+ if (res) {
349
+ this.lintData = res;
350
+ }
351
+ }
352
+ } catch (err) {
353
+ this.errorMessage = err.message || 'Lint execution failure';
354
+ } finally {
355
+ this.loading = false;
356
+ this.render();
357
+ }
358
+ }
103
359
 
360
+ async loadRefreshReport() {
361
+ this.loading = true;
362
+ this.errorMessage = null;
363
+ this.render();
104
364
  try {
105
- const res = await this.host.run('bootstrap', ['--json']);
106
- this.lastResult = {
107
- ok: true,
108
- message: 'OpenWiki skeleton bootstrapped',
109
- data: res
110
- };
111
- await this.refreshHubs();
365
+ if (this.host && typeof this.host.run === 'function') {
366
+ const res = await this.host.run('refresh', ['--json']);
367
+ if (res) {
368
+ this.refreshReports = Array.isArray(res.reports) ? res.reports : [];
369
+ this.refreshCounts = res.counts || null;
370
+ }
371
+ }
112
372
  } catch (err) {
113
- this.lastResult = {
114
- ok: false,
115
- message: err instanceof Error ? err.message : 'Bootstrap failed'
116
- };
373
+ this.errorMessage = err.message || 'Fleet status check failure';
117
374
  } finally {
118
- this.executing = false;
375
+ this.loading = false;
119
376
  this.render();
120
377
  }
121
378
  }
122
379
 
380
+ async executeFleetRefresh() {
381
+ if (this.loading) return;
382
+ this.loading = true;
383
+ this.errorMessage = null;
384
+ this.render();
385
+
386
+ try {
387
+ if (this.host && typeof this.host.run === 'function') {
388
+ const res = await this.host.run('refresh', ['--apply', '--json']);
389
+ if (res?.reports) {
390
+ this.refreshReports = res.reports;
391
+ this.refreshCounts = res.counts;
392
+ }
393
+ }
394
+ } catch (err) {
395
+ this.errorMessage = err.message || 'Fleet refresh execution failure';
396
+ } finally {
397
+ this.loading = false;
398
+ this.render();
399
+ }
400
+ }
401
+
402
+ async executeGenerate() {
403
+ if (this.loading) return;
404
+ this.loading = true;
405
+ this.errorMessage = null;
406
+ this.generateLogs = [`Dispatching on-demand generator for '${this.generateRepo || 'current repository'}'...`];
407
+ this.render();
408
+
409
+ try {
410
+ if (this.host && typeof this.host.run === 'function') {
411
+ const args = ['--json'];
412
+ if (this.generateRepo) args.push(`--repo=${this.generateRepo}`);
413
+ if (this.scope !== 'workspace' && this.scope !== 'all-vaults') {
414
+ args.push(`--node=${this.scope}`);
415
+ }
416
+
417
+ const res = await this.host.run('generate', args);
418
+ this.generateLogs.push(res?.ok ? `Generation completed successfully in ${Math.round((res.durationMs || 0)/1000)}s` : 'Generation finished with warnings');
419
+ if (res?.outputDir) this.generateLogs.push(`Output: ${res.outputDir}`);
420
+ }
421
+ } catch (err) {
422
+ this.errorMessage = err.message || 'Generation execution failure';
423
+ this.generateLogs.push(`Error: ${this.errorMessage}`);
424
+ } finally {
425
+ this.loading = false;
426
+ this.refreshHubs();
427
+ }
428
+ }
429
+
430
+ async executeIngest() {
431
+ if (this.loading) return;
432
+ this.loading = true;
433
+ this.errorMessage = null;
434
+ this.render();
435
+
436
+ const args = [this.targetPath || 'openwiki'];
437
+ if (this.dryRun) args.push('--dry-run');
438
+
439
+ try {
440
+ if (this.host && typeof this.host.run === 'function') {
441
+ await this.host.run('ingest', args);
442
+ }
443
+ } catch (err) {
444
+ this.errorMessage = err.message || 'Ingestion cycle failure';
445
+ } finally {
446
+ this.loading = false;
447
+ this.refreshHubs();
448
+ }
449
+ }
450
+
451
+ async executeWatch() {
452
+ if (this.loading) return;
453
+ this.loading = true;
454
+ this.errorMessage = null;
455
+ this.render();
456
+
457
+ try {
458
+ if (this.host && typeof this.host.run === 'function') {
459
+ const res = await this.host.run('watch', ['--json']);
460
+ if (res?.status) {
461
+ this.upstream = {
462
+ recordedVersion: res.status.currentVersion,
463
+ latestVersion: res.status.latestVersion
464
+ };
465
+ }
466
+ }
467
+ } catch (err) {
468
+ this.errorMessage = err.message || 'Upstream surveillance failure';
469
+ } finally {
470
+ this.loading = false;
471
+ this.refreshHubs();
472
+ }
473
+ }
474
+
123
475
  render() {
124
- const root = this.shadowRoot;
125
- if (!root) return;
476
+ if (!this.shadowRoot) return;
477
+
478
+ const countText = this.hubs.length ? `${this.hubs.length} Hubs` : '0 Hubs';
479
+ const totalLines = this.hubs.reduce((acc, h) => acc + (h.lines || 0), 0);
480
+ const linesText = `${totalLines.toLocaleString()} Lines`;
126
481
 
127
- root.innerHTML = `
482
+ this.shadowRoot.innerHTML = `
128
483
  <style>
129
484
  :host {
130
- --face-bg: #0f172a;
131
- --face-surface: #1e293b;
132
- --face-line: #334155;
133
- --face-text: #f8fafc;
134
- --face-muted: #94a3b8;
135
- --face-brand: #3b82f6;
136
- --face-brand-hover: #60a5fa;
137
- --face-accent: #facc15;
138
- --face-ok: #10b981;
139
- --face-warn: #f59e0b;
140
- --face-danger: #ef4444;
141
- --face-dark: #0f172a;
142
-
143
485
  display: block;
144
- font-family: var(--font-sans, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif);
145
- color: var(--text-primary, var(--face-text));
146
- background: var(--bg-secondary, var(--face-bg));
147
- border: 1px solid var(--border, var(--face-line));
148
- border-radius: 8px;
149
- padding: 16px;
150
486
  box-sizing: border-box;
151
- overflow: hidden;
487
+ --bg-base: #090d16;
488
+ --bg-surface: #111827;
489
+ --bg-elevated: #1f2937;
490
+ --border-subtle: #374151;
491
+ --border-focus: #f59e0b;
492
+ --text-main: #f9fafb;
493
+ --text-muted: #9ca3af;
494
+ --accent-amber: #f59e0b;
495
+ --accent-green: #10b981;
496
+ --accent-red: #ef4444;
497
+ background: var(--bg-base);
498
+ color: var(--text-main);
499
+ border: 1px solid var(--border-subtle);
500
+ border-radius: 10px;
501
+ padding: 16px;
502
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
503
+ font-size: 13px;
152
504
  }
153
505
 
154
- @media (prefers-reduced-motion: reduce) {
155
- *, ::before, ::after {
156
- animation-duration: 0.01ms !important;
157
- animation-iteration-count: 1 !important;
158
- transition-duration: 0.01ms !important;
159
- }
506
+ *, *::before, *::after {
507
+ box-sizing: inherit;
160
508
  }
161
509
 
162
- /* Header Bar */
163
- .header {
510
+ .header-bar {
164
511
  display: flex;
165
512
  align-items: center;
166
513
  justify-content: space-between;
167
- flex-wrap: wrap;
514
+ padding-bottom: 12px;
515
+ border-bottom: 1px solid var(--border-subtle);
168
516
  gap: 12px;
169
- padding-bottom: 14px;
170
- border-bottom: 1px solid var(--border, var(--face-line));
171
- margin-bottom: 16px;
172
517
  }
173
518
 
174
- .brand-group {
519
+ .brand-cluster {
175
520
  display: flex;
176
521
  align-items: center;
177
522
  gap: 10px;
178
523
  }
179
524
 
180
- .wordmark-badge {
181
- font-family: var(--font-mono, 'JetBrains Mono', monospace);
182
- font-size: 13px;
183
- font-weight: 800;
184
- letter-spacing: 0.05em;
185
- color: var(--card-accent, var(--face-accent));
186
- background: rgba(250, 204, 21, 0.12);
187
- padding: 6px 10px;
188
- border-radius: 6px;
189
- border: 1px solid rgba(250, 204, 21, 0.3);
525
+ .crest-svg {
526
+ width: 24px;
527
+ height: 24px;
528
+ fill: var(--accent-amber);
190
529
  }
191
530
 
192
- .title-meta {
193
- display: flex;
194
- flex-direction: column;
195
- }
196
-
197
- .plugin-title {
198
- font-size: 14px;
531
+ .brand-title {
199
532
  font-weight: 700;
200
- color: var(--text-primary, var(--face-text));
201
- margin: 0;
533
+ font-size: 15px;
534
+ letter-spacing: 0.05em;
535
+ color: var(--text-main);
536
+ text-transform: uppercase;
202
537
  }
203
538
 
204
- .plugin-subtitle {
205
- font-size: 11px;
206
- color: var(--text-secondary, var(--face-muted));
207
- margin: 0;
539
+ .wordmark-badge {
540
+ font-family: ui-monospace, monospace;
541
+ font-size: 10px;
542
+ background: rgba(245, 158, 11, 0.15);
543
+ color: var(--accent-amber);
544
+ border: 1px solid rgba(245, 158, 11, 0.3);
545
+ padding: 2px 6px;
546
+ border-radius: 4px;
547
+ font-weight: 600;
208
548
  }
209
549
 
210
- .hub-count-chip {
550
+ .scope-control {
211
551
  display: flex;
212
552
  align-items: center;
213
553
  gap: 6px;
214
- font-family: var(--font-mono, 'JetBrains Mono', monospace);
215
- font-size: 11px;
216
- font-weight: 700;
217
- color: var(--face-accent);
218
- background: rgba(250, 204, 21, 0.12);
219
- border: 1px solid rgba(250, 204, 21, 0.3);
220
- padding: 5px 12px;
221
- border-radius: 6px;
222
- text-transform: uppercase;
223
554
  }
224
555
 
225
- /* Split Layout */
226
- .workspace-split {
227
- display: grid;
228
- grid-template-columns: minmax(260px, 1.4fr) minmax(240px, 1fr);
229
- gap: 14px;
230
- margin-bottom: 16px;
556
+ .scope-select {
557
+ background: var(--bg-surface);
558
+ color: var(--text-main);
559
+ border: 1px solid var(--border-subtle);
560
+ border-radius: 6px;
561
+ padding: 4px 8px;
562
+ font-size: 12px;
563
+ cursor: pointer;
231
564
  }
232
565
 
233
- @media (max-width: 700px) {
234
- .workspace-split {
235
- grid-template-columns: 1fr;
236
- }
566
+ /* Screen Navigation */
567
+ .screen-nav {
568
+ display: flex;
569
+ gap: 4px;
570
+ margin: 12px 0;
571
+ overflow-x: auto;
572
+ padding-bottom: 4px;
237
573
  }
238
574
 
239
- /* Hub Tree Matrix */
240
- .hub-panel {
241
- border: 1px solid var(--border, var(--face-line));
575
+ .nav-btn {
576
+ background: transparent;
577
+ color: var(--text-muted);
578
+ border: 1px solid transparent;
242
579
  border-radius: 6px;
243
- background: rgba(30, 41, 59, 0.4);
244
- padding: 12px;
245
- display: flex;
246
- flex-direction: column;
247
- gap: 10px;
580
+ padding: 6px 12px;
581
+ font-size: 12px;
582
+ font-weight: 600;
583
+ cursor: pointer;
584
+ white-space: nowrap;
585
+ min-height: 44px;
586
+ transition: all 0.15s ease;
587
+ }
588
+ .nav-btn:hover {
589
+ color: var(--text-main);
590
+ background: var(--bg-surface);
591
+ }
592
+ .nav-btn.active {
593
+ color: var(--accent-amber);
594
+ background: rgba(245, 158, 11, 0.12);
595
+ border-color: rgba(245, 158, 11, 0.4);
248
596
  }
249
597
 
250
- .panel-title {
598
+ /* Metrics Deck */
599
+ .metric-deck {
600
+ display: grid;
601
+ grid-template-columns: repeat(3, 1fr);
602
+ gap: 8px;
603
+ margin-bottom: 14px;
604
+ }
605
+
606
+ .metric-cell {
607
+ background: var(--bg-surface);
608
+ border: 1px solid var(--border-subtle);
609
+ border-radius: 6px;
610
+ padding: 8px 10px;
611
+ }
612
+ .metric-label {
251
613
  font-size: 11px;
252
- font-weight: 700;
614
+ color: var(--text-muted);
253
615
  text-transform: uppercase;
254
- letter-spacing: 0.05em;
255
- color: var(--text-secondary, var(--face-muted));
256
- margin: 0;
616
+ }
617
+ .metric-val {
618
+ font-size: 15px;
619
+ font-weight: 700;
620
+ margin-top: 2px;
621
+ color: var(--text-main);
622
+ }
623
+
624
+ /* Content Deck */
625
+ .content-view {
626
+ background: var(--bg-surface);
627
+ border: 1px solid var(--border-subtle);
628
+ border-radius: 8px;
629
+ padding: 14px;
630
+ min-height: 260px;
257
631
  }
258
632
 
259
633
  .hub-list {
260
634
  display: flex;
261
635
  flex-direction: column;
262
636
  gap: 6px;
263
- max-height: 280px;
264
- overflow-y: auto;
265
637
  }
266
638
 
267
639
  .hub-item {
268
- padding: 10px 12px;
269
- background: rgba(15, 23, 42, 0.6);
270
- border: 1px solid var(--border, var(--face-line));
271
- border-radius: 5px;
272
640
  display: flex;
273
641
  align-items: center;
274
642
  justify-content: space-between;
275
- gap: 10px;
643
+ padding: 8px 10px;
644
+ background: var(--bg-elevated);
645
+ border: 1px solid var(--border-subtle);
646
+ border-radius: 6px;
647
+ cursor: pointer;
648
+ min-height: 44px;
649
+ transition: border-color 0.15s;
276
650
  }
277
-
278
- .hub-main {
279
- display: flex;
280
- flex-direction: column;
281
- gap: 2px;
651
+ .hub-item:hover {
652
+ border-color: var(--accent-amber);
282
653
  }
283
-
284
654
  .hub-title {
285
- font-family: var(--font-mono, 'JetBrains Mono', monospace);
286
- font-size: 12px;
287
- font-weight: 700;
288
- color: var(--text-primary, var(--face-text));
655
+ font-weight: 600;
656
+ color: var(--text-main);
289
657
  }
290
-
291
- .hub-path {
292
- font-size: 10px;
293
- color: var(--text-secondary, var(--face-muted));
658
+ .repo-pill {
659
+ background: rgba(255, 255, 255, 0.08);
660
+ padding: 2px 6px;
661
+ border-radius: 4px;
662
+ font-family: ui-monospace, monospace;
663
+ font-size: 11px;
294
664
  }
295
665
 
296
- .hub-metrics {
666
+ /* Reader */
667
+ .reader-header {
297
668
  display: flex;
298
669
  align-items: center;
299
- gap: 6px;
300
- font-family: var(--font-mono, 'JetBrains Mono', monospace);
301
- font-size: 10px;
302
- color: var(--face-accent);
670
+ justify-content: space-between;
671
+ margin-bottom: 12px;
672
+ padding-bottom: 8px;
673
+ border-bottom: 1px solid var(--border-subtle);
674
+ }
675
+ .back-btn {
676
+ background: var(--bg-elevated);
677
+ color: var(--accent-amber);
678
+ border: 1px solid var(--border-subtle);
679
+ border-radius: 6px;
680
+ padding: 4px 10px;
681
+ font-weight: 600;
682
+ cursor: pointer;
683
+ min-height: 44px;
684
+ }
685
+ .markdown-body {
686
+ font-family: Georgia, Cambria, serif;
687
+ line-height: 1.6;
688
+ color: #e5e7eb;
689
+ max-height: 380px;
690
+ overflow-y: auto;
691
+ white-space: pre-wrap;
303
692
  }
304
693
 
305
- /* Ingest Deck Column */
306
- .ingest-deck {
694
+ /* Search */
695
+ .search-box {
307
696
  display: flex;
308
- flex-direction: column;
309
- gap: 10px;
697
+ gap: 8px;
698
+ margin-bottom: 12px;
310
699
  }
311
-
312
- .form-card {
313
- border: 1px solid var(--border, var(--face-line));
700
+ .search-input {
701
+ flex: 1;
702
+ background: var(--bg-elevated);
703
+ border: 1px solid var(--border-subtle);
314
704
  border-radius: 6px;
315
- background: rgba(30, 41, 59, 0.4);
316
- padding: 12px;
317
- display: flex;
318
- flex-direction: column;
319
- gap: 10px;
705
+ padding: 8px 12px;
706
+ color: var(--text-main);
707
+ font-size: 13px;
708
+ min-height: 44px;
320
709
  }
321
-
322
- .input-row {
323
- display: flex;
324
- flex-direction: column;
325
- gap: 4px;
710
+ .action-btn {
711
+ background: var(--accent-amber);
712
+ color: #000;
713
+ font-weight: 700;
714
+ border: none;
715
+ border-radius: 6px;
716
+ padding: 8px 14px;
717
+ cursor: pointer;
718
+ min-height: 44px;
326
719
  }
327
-
328
- .input-label {
329
- font-size: 10px;
330
- text-transform: uppercase;
331
- color: var(--text-secondary, var(--face-muted));
332
- letter-spacing: 0.05em;
720
+ .action-btn:hover {
721
+ background: #d97706;
333
722
  }
334
723
 
335
- .text-input {
336
- min-height: 44px;
337
- padding: 8px 12px;
338
- font-family: var(--font-mono, 'JetBrains Mono', monospace);
724
+ .result-item {
725
+ background: var(--bg-elevated);
726
+ border: 1px solid var(--border-subtle);
727
+ border-radius: 6px;
728
+ padding: 10px;
729
+ margin-bottom: 8px;
730
+ }
731
+ .result-heading {
732
+ font-weight: 600;
733
+ color: var(--accent-amber);
734
+ margin-bottom: 4px;
735
+ }
736
+ .result-snippet {
737
+ font-style: italic;
738
+ color: #d1d5db;
339
739
  font-size: 12px;
340
- color: var(--text-primary, var(--face-text));
341
- background: rgba(15, 23, 42, 0.8);
342
- border: 1px solid var(--border, var(--face-line));
343
- border-radius: 5px;
344
- box-sizing: border-box;
345
- width: 100%;
740
+ line-height: 1.4;
346
741
  }
347
742
 
348
- .text-input:focus {
349
- outline: none;
350
- border-color: var(--card-accent, var(--face-brand));
743
+ /* Graph SVG */
744
+ .graph-canvas-wrap {
745
+ width: 100%;
746
+ min-height: 280px;
747
+ background: #0b0f19;
748
+ border-radius: 6px;
749
+ border: 1px solid var(--border-subtle);
750
+ display: flex;
751
+ align-items: center;
752
+ justify-content: center;
753
+ overflow: auto;
754
+ padding: 10px;
351
755
  }
352
756
 
353
- .toggle-row {
757
+ /* Token Screen */
758
+ .token-row {
354
759
  display: flex;
355
760
  align-items: center;
356
- gap: 8px;
761
+ justify-content: space-between;
762
+ background: var(--bg-elevated);
763
+ border: 1px solid var(--border-subtle);
764
+ border-radius: 6px;
765
+ padding: 10px 12px;
766
+ margin-bottom: 8px;
767
+ min-height: 44px;
768
+ }
769
+ .token-key {
770
+ font-family: ui-monospace, monospace;
771
+ font-weight: 600;
772
+ }
773
+ .token-badge {
357
774
  font-size: 11px;
358
- color: var(--text-secondary, var(--face-muted));
359
- cursor: pointer;
775
+ padding: 2px 8px;
776
+ border-radius: 4px;
777
+ font-weight: 600;
360
778
  }
361
-
362
- .toggle-check {
363
- min-height: 20px;
364
- min-width: 20px;
365
- cursor: pointer;
779
+ .token-badge.ok {
780
+ background: rgba(16, 185, 129, 0.2);
781
+ color: var(--accent-green);
782
+ border: 1px solid var(--accent-green);
783
+ }
784
+ .token-badge.missing {
785
+ background: rgba(239, 68, 68, 0.2);
786
+ color: var(--accent-red);
787
+ border: 1px solid var(--accent-red);
366
788
  }
367
789
 
368
- .btn-action-primary {
369
- min-height: 44px;
370
- padding: 8px 16px;
371
- font-family: var(--font-sans, sans-serif);
790
+ .auth-notice-card {
791
+ background: rgba(245, 158, 11, 0.08);
792
+ border: 1px solid rgba(245, 158, 11, 0.3);
793
+ border-radius: 6px;
794
+ padding: 10px 12px;
795
+ margin-bottom: 12px;
372
796
  font-size: 12px;
373
- font-weight: 700;
374
- color: var(--face-dark);
375
- background: var(--face-accent);
376
- border: none;
377
- border-radius: 5px;
378
- cursor: pointer;
379
- transition: background 0.15s ease;
797
+ color: #fde68a;
380
798
  }
381
799
 
382
- .btn-action-primary:hover {
383
- background: #fde047;
800
+ .config-deck {
801
+ display: flex;
802
+ flex-direction: column;
803
+ gap: 12px;
384
804
  }
385
805
 
386
- .btn-action-primary:disabled {
387
- opacity: 0.6;
388
- cursor: not-allowed;
806
+ .ingest-deck {
807
+ background: var(--bg-elevated);
808
+ border: 1px solid var(--border-subtle);
809
+ border-radius: 6px;
810
+ padding: 12px;
811
+ margin-top: 14px;
389
812
  }
390
813
 
391
- .btn-action-secondary {
392
- min-height: 44px;
393
- padding: 8px 16px;
394
- font-family: var(--font-sans, sans-serif);
395
- font-size: 12px;
396
- font-weight: 700;
397
- color: var(--text-primary, var(--face-text));
398
- background: transparent;
399
- border: 1px solid var(--border, var(--face-line));
400
- border-radius: 5px;
814
+ .preset-chip {
815
+ display: inline-block;
816
+ background: var(--bg-elevated);
817
+ border: 1px solid var(--border-subtle);
818
+ padding: 4px 10px;
819
+ border-radius: 4px;
820
+ font-size: 11px;
401
821
  cursor: pointer;
402
- transition: background 0.15s ease;
822
+ margin-right: 6px;
823
+ margin-bottom: 6px;
403
824
  }
404
-
405
- .btn-action-secondary:hover {
406
- background: rgba(255, 255, 255, 0.05);
825
+ .preset-chip:hover {
826
+ border-color: var(--accent-amber);
407
827
  }
408
828
 
409
- .feedback-banner {
829
+ .error-banner {
830
+ background: rgba(239, 68, 68, 0.15);
831
+ border: 1px solid var(--accent-red);
832
+ color: #fca5a5;
410
833
  padding: 8px 12px;
411
- border-radius: 5px;
412
- font-family: var(--font-mono, 'JetBrains Mono', monospace);
413
- font-size: 11px;
834
+ border-radius: 6px;
835
+ margin-bottom: 10px;
414
836
  }
415
837
 
416
- .feedback-ok {
417
- background: rgba(16, 185, 129, 0.15);
418
- border: 1px solid rgba(16, 185, 129, 0.4);
419
- color: var(--face-ok);
838
+ @media (prefers-reduced-motion: reduce) {
839
+ * {
840
+ animation: none !important;
841
+ transition: none !important;
842
+ }
420
843
  }
844
+ </style>
421
845
 
422
- .feedback-fail {
423
- background: rgba(239, 68, 68, 0.15);
424
- border: 1px solid rgba(239, 68, 68, 0.4);
425
- color: var(--face-danger);
846
+ <div class="header-bar">
847
+ <div class="brand-cluster">
848
+ <svg class="crest-svg" viewBox="0 0 24 24">
849
+ <path d="M12 2L2 7l10 5 10-5-10-5zM2 17l10 5 10-5M2 12l10 5 10-5"/>
850
+ </svg>
851
+ <span class="brand-title">OpenWiki</span>
852
+ <span class="wordmark-badge">MESH KNOWLEDGE</span>
853
+ </div>
854
+
855
+ <div class="scope-control">
856
+ <span style="font-size: 11px; color: var(--text-muted);">Scope:</span>
857
+ <select class="scope-select" id="scope-select">
858
+ <option value="workspace" ${this.scope === 'workspace' ? 'selected' : ''}>Current Workspace</option>
859
+ <option value="all-vaults" ${this.scope === 'all-vaults' ? 'selected' : ''}>All Vaults (Fleet)</option>
860
+ </select>
861
+ </div>
862
+ </div>
863
+
864
+ <nav class="screen-nav">
865
+ ${OpenWikiCard.screens
866
+ .map(
867
+ (s) => `
868
+ <button class="nav-btn ${this.activeScreen === s.id ? 'active' : ''}" data-screen="${s.id}">
869
+ ${s.label}
870
+ </button>
871
+ `
872
+ )
873
+ .join('')}
874
+ </nav>
875
+
876
+ ${this.errorMessage ? `<div class="error-banner">${this.escape(this.errorMessage)}</div>` : ''}
877
+
878
+ <div class="metric-deck">
879
+ <div class="metric-cell">
880
+ <div class="metric-label">Knowledge Hubs</div>
881
+ <div class="metric-val">${countText}</div>
882
+ </div>
883
+ <div class="metric-cell">
884
+ <div class="metric-label">Indexed Volume</div>
885
+ <div class="metric-val">${linesText}</div>
886
+ </div>
887
+ <div class="metric-cell">
888
+ <div class="metric-label">Mesh Status</div>
889
+ <div class="metric-val" style="color: var(--accent-amber);">${this.statusText}</div>
890
+ </div>
891
+ </div>
892
+
893
+ <div class="content-view">
894
+ ${this.renderActiveScreen()}
895
+ </div>
896
+ `;
897
+
898
+ this.attachEventListeners();
899
+ }
900
+
901
+ renderActiveScreen() {
902
+ if (this.selectedHub) {
903
+ return this.renderReaderScreen();
904
+ }
905
+
906
+ switch (this.activeScreen) {
907
+ case 'hubs':
908
+ return this.renderHubsScreen();
909
+ case 'search':
910
+ return this.renderSearchScreen();
911
+ case 'graph':
912
+ return this.renderGraphScreen();
913
+ case 'generate':
914
+ return this.renderGenerateScreen();
915
+ case 'refresh':
916
+ return this.renderRefreshScreen();
917
+ case 'token':
918
+ return this.renderTokenScreen();
919
+ case 'lint':
920
+ return this.renderLintScreen();
921
+ case 'upstream':
922
+ return this.renderUpstreamScreen();
923
+ case 'config':
924
+ return this.renderConfigScreen();
925
+ default:
926
+ return this.renderHubsScreen();
927
+ }
928
+ }
929
+
930
+ renderHubsScreen() {
931
+ if (!this.hubs || this.hubs.length === 0) {
932
+ return `
933
+ <div style="text-align: center; padding: 30px; color: var(--text-muted);">
934
+ <div>No documentation hubs discovered for scope: <strong>${this.escape(this.scope)}</strong>.</div>
935
+ <button class="action-btn" id="btn-refresh-hubs" style="margin-top: 12px;">Sync Hubs</button>
936
+ </div>
937
+ `;
938
+ }
939
+
940
+ return `
941
+ <div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 10px;">
942
+ <span style="font-weight: 600; color: var(--text-muted);">Click a hub to read documentation:</span>
943
+ <button class="back-btn" id="btn-refresh-hubs">Refresh</button>
944
+ </div>
945
+ <div class="hub-list">
946
+ ${this.hubs
947
+ .map(
948
+ (h, idx) => `
949
+ <div class="hub-item" data-hub-index="${idx}">
950
+ <div>
951
+ <span class="repo-pill">${this.escape(h.repoName || 'workspace')}</span>
952
+ <span class="hub-title" style="margin-left: 6px;">${this.escape(h.title)}</span>
953
+ </div>
954
+ <div class="hub-meta">
955
+ <span>${h.lines || 0} lines</span>
956
+ <span>${h.size_bytes ? Math.round(h.size_bytes / 1024) + ' KB' : ''}</span>
957
+ </div>
958
+ </div>
959
+ `
960
+ )
961
+ .join('')}
962
+ </div>
963
+ <div class="ingest-deck">
964
+ <div style="font-weight: 600; margin-bottom: 4px;">SSSS Vault Ingestion</div>
965
+ <div style="font-size: 11px; color: var(--text-muted); margin-bottom: 8px;">Embed current hub markdown into semantic memory categories.</div>
966
+ <button class="action-btn" id="btn-ingest-exec">Ingest into Vault</button>
967
+ </div>
968
+ `;
969
+ }
970
+
971
+ renderReaderScreen() {
972
+ const hub = this.selectedHub;
973
+ return `
974
+ <div class="reader-header">
975
+ <div>
976
+ <span class="repo-pill">${this.escape(hub.repoName || 'workspace')}</span>
977
+ <strong style="margin-left: 8px; color: var(--accent-amber);">${this.escape(hub.title)}</strong>
978
+ </div>
979
+ <button class="back-btn" id="btn-back-to-hubs">Back to Hubs</button>
980
+ </div>
981
+ <div class="markdown-body">${this.escape(this.selectedHubContent || 'Loading...')}</div>
982
+ `;
983
+ }
984
+
985
+ renderSearchScreen() {
986
+ return `
987
+ <div class="search-box">
988
+ <input class="search-input" id="search-input" placeholder="Search architecture, concepts, APIs across vaults..." value="${this.escape(this.searchQuery)}" />
989
+ <button class="action-btn" id="btn-search-exec">Search</button>
990
+ </div>
991
+ ${
992
+ this.searchStats
993
+ ? `<div style="font-size: 11px; color: var(--text-muted); margin-bottom: 8px;">
994
+ Found ${this.searchStats.count} passage(s) across ${this.searchStats.totalHubs} hubs (${this.searchStats.scope})
995
+ </div>`
996
+ : ''
997
+ }
998
+ <div class="search-results">
999
+ ${
1000
+ this.searchResults.length === 0
1001
+ ? `<div style="text-align: center; color: var(--text-muted); padding: 20px;">Enter search query to extract evidence passages.</div>`
1002
+ : this.searchResults
1003
+ .map(
1004
+ (r) => `
1005
+ <div class="result-item">
1006
+ <div class="result-heading">
1007
+ <span class="repo-pill">${this.escape(r.repoName)}</span>
1008
+ ${this.escape(r.title)} &gt; ${this.escape(r.section)} (Line ${r.line})
1009
+ </div>
1010
+ <div class="result-snippet">"${this.escape(r.snippet)}"</div>
1011
+ </div>
1012
+ `
1013
+ )
1014
+ .join('')
426
1015
  }
1016
+ </div>
1017
+ `;
1018
+ }
427
1019
 
428
- .empty-hubs {
429
- padding: 32px 16px;
430
- text-align: center;
431
- font-size: 12px;
432
- color: var(--text-secondary, var(--face-muted));
1020
+ renderGraphScreen() {
1021
+ if (!this.graphData || !this.graphData.nodes) {
1022
+ return `
1023
+ <div style="text-align: center; padding: 30px; color: var(--text-muted);">
1024
+ <div>Knowledge topology graph not loaded.</div>
1025
+ <button class="action-btn" id="btn-load-graph" style="margin-top: 10px;">Extract Topology</button>
1026
+ </div>
1027
+ `;
1028
+ }
1029
+
1030
+ const { nodes, edges, orphans } = this.graphData;
1031
+
1032
+ return `
1033
+ <div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 8px;">
1034
+ <span style="font-weight: 600; color: var(--text-muted);">
1035
+ Topology: ${nodes.length} Nodes, ${edges.length} Edges, ${orphans.length} Orphans
1036
+ </span>
1037
+ <button class="back-btn" id="btn-load-graph">Refresh Graph</button>
1038
+ </div>
1039
+ <div class="graph-canvas-wrap">
1040
+ <svg width="100%" height="240" viewBox="0 0 600 240">
1041
+ <defs>
1042
+ <marker id="arrow" viewBox="0 0 10 10" refX="10" refY="5" markerWidth="6" markerHeight="6" orient="auto-start-reverse">
1043
+ <path d="M 0 0 L 10 5 L 0 10 z" fill="#f59e0b" />
1044
+ </marker>
1045
+ </defs>
1046
+ ${edges
1047
+ .slice(0, 30)
1048
+ .map((e, idx) => {
1049
+ const x1 = 50 + (idx * 37) % 500;
1050
+ const y1 = 40 + (idx * 23) % 160;
1051
+ const x2 = 80 + (idx * 43) % 500;
1052
+ const y2 = 60 + (idx * 31) % 160;
1053
+ return `<line x1="${x1}" y1="${y1}" x2="${x2}" y2="${y2}" stroke="rgba(245, 158, 11, 0.4)" stroke-width="1.5" marker-end="url(#arrow)" />`;
1054
+ })
1055
+ .join('')}
1056
+ ${nodes
1057
+ .slice(0, 20)
1058
+ .map((n, idx) => {
1059
+ const cx = 50 + (idx * 53) % 500;
1060
+ const cy = 40 + (idx * 33) % 160;
1061
+ return `
1062
+ <g>
1063
+ <circle cx="${cx}" cy="${cy}" r="12" fill="#1e293b" stroke="#f59e0b" stroke-width="2" />
1064
+ <text x="${cx}" y="${cy + 22}" fill="#94a3b8" font-size="9" text-anchor="middle">
1065
+ ${this.escape(n.label.slice(0, 14))}
1066
+ </text>
1067
+ </g>
1068
+ `;
1069
+ })
1070
+ .join('')}
1071
+ </svg>
1072
+ </div>
1073
+ `;
1074
+ }
1075
+
1076
+ renderGenerateScreen() {
1077
+ return `
1078
+ <div style="margin-bottom: 12px;">
1079
+ <label style="display: block; font-weight: 600; margin-bottom: 4px;">Target Repository (leave empty for current workspace):</label>
1080
+ <input class="search-input" id="generate-repo-input" placeholder="e.g. total-recall or gro-crm" value="${this.escape(this.generateRepo)}" />
1081
+ </div>
1082
+ <div style="display: flex; justify-content: flex-end; margin-bottom: 12px;">
1083
+ <button class="action-btn" id="btn-generate-exec" ${this.loading ? 'disabled' : ''}>
1084
+ ${this.loading ? 'Generating...' : 'Generate Wiki Documentation'}
1085
+ </button>
1086
+ </div>
1087
+ <div style="background: var(--bg-base); border: 1px solid var(--border-subtle); border-radius: 6px; padding: 10px; font-family: ui-monospace, monospace; font-size: 11px; max-height: 180px; overflow-y: auto;">
1088
+ ${
1089
+ this.generateLogs.length === 0
1090
+ ? '<span style="color: var(--text-muted);">Ready to dispatch on-demand generation.</span>'
1091
+ : this.generateLogs.map((l) => `<div>${this.escape(l)}</div>`).join('')
433
1092
  }
1093
+ </div>
1094
+ `;
1095
+ }
434
1096
 
435
- /* Footer Telemetry */
436
- .footer-bar {
437
- display: flex;
438
- justify-content: space-between;
439
- align-items: center;
440
- padding-top: 12px;
441
- margin-top: 14px;
442
- border-top: 1px solid var(--border, var(--face-line));
443
- font-size: 10px;
444
- color: var(--text-secondary, var(--face-muted));
1097
+ renderRefreshScreen() {
1098
+ return `
1099
+ <div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 12px;">
1100
+ <span style="font-weight: 600; color: var(--text-muted);">Fleet Staleness Status:</span>
1101
+ <button class="action-btn" id="btn-fleet-refresh" ${this.loading ? 'disabled' : ''}>
1102
+ ${this.loading ? 'Refreshing...' : 'Execute Fleet Refresh'}
1103
+ </button>
1104
+ </div>
1105
+ <div style="max-height: 220px; overflow-y: auto;">
1106
+ ${
1107
+ this.refreshReports.length === 0
1108
+ ? '<div style="color: var(--text-muted); text-align: center; padding: 20px;">No refresh reports loaded. Click Execute to evaluate fleet.</div>'
1109
+ : this.refreshReports
1110
+ .map(
1111
+ (r) => `
1112
+ <div style="display: flex; justify-content: space-between; padding: 6px 8px; background: var(--bg-elevated); border: 1px solid var(--border-subtle); border-radius: 4px; margin-bottom: 4px; font-size: 11px;">
1113
+ <span><strong>${this.escape(r.name)}</strong>: ${this.escape(r.action)}</span>
1114
+ <span style="color: var(--text-muted);">${r.commitsSince ? r.commitsSince + ' commits' : 'current'}</span>
1115
+ </div>
1116
+ `
1117
+ )
1118
+ .join('')
445
1119
  }
446
- </style>
1120
+ </div>
1121
+ `;
1122
+ }
447
1123
 
448
- <div class="header">
449
- <div class="brand-group">
450
- <div class="wordmark-badge">[BRAIN//DUMP]</div>
451
- <div class="title-meta">
452
- <h3 class="plugin-title">OpenWiki Knowledge Graph</h3>
453
- <p class="plugin-subtitle">Multi-repository documentation and knowledge ingestion engine</p>
454
- </div>
1124
+ renderTokenScreen() {
1125
+ const { openrouter, google } = this.tokenStatus;
1126
+ return `
1127
+ <div class="auth-notice-card">
1128
+ Total Recall Auth Secrets Store credentials for model-driven documentation. Secrets are stored encrypted in the global store.
1129
+ </div>
1130
+ <div class="token-row">
1131
+ <div>
1132
+ <span class="token-key">OPENROUTER_API_KEY</span>
1133
+ <div style="font-size: 11px; color: var(--text-muted);">Default provider for cheap model wiki routing</div>
455
1134
  </div>
456
- <div class="hub-count-chip">
457
- ${this.hubs.length > 0 ? `${this.hubs.length} Hubs Indexed` : 'No Hubs'}
1135
+ <span class="token-badge ${openrouter?.configured ? 'ok' : 'missing'}">
1136
+ ${openrouter?.configured ? 'Configured' : 'Missing'}
1137
+ </span>
1138
+ </div>
1139
+ <div class="token-row">
1140
+ <div>
1141
+ <span class="token-key">GOOGLE_API_KEY</span>
1142
+ <div style="font-size: 11px; color: var(--text-muted);">Google Gemini provider for native model generation</div>
1143
+ </div>
1144
+ <span class="token-badge ${google?.configured ? 'ok' : 'missing'}">
1145
+ ${google?.configured ? 'Configured' : 'Missing'}
1146
+ </span>
1147
+ </div>
1148
+ <div style="margin-top: 14px; border-top: 1px solid var(--border-subtle); padding-top: 12px;">
1149
+ <label style="display: block; font-weight: 600; margin-bottom: 4px;">Save Key to Auth Vault:</label>
1150
+ <div style="display: flex; gap: 8px;">
1151
+ <select class="scope-select" id="token-provider-select">
1152
+ <option value="openrouter">OpenRouter</option>
1153
+ <option value="google">Google Gemini</option>
1154
+ </select>
1155
+ <input type="password" class="search-input" id="token-val-input" placeholder="Paste API Key (saved securely to auth secrets)..." />
1156
+ <button class="action-btn" id="btn-save-token">Save Key</button>
458
1157
  </div>
459
1158
  </div>
1159
+ `;
1160
+ }
460
1161
 
461
- <div class="workspace-split">
462
- <div class="hub-panel">
463
- <h4 class="panel-title">Verified Documentation Hubs</h4>
464
- <div class="hub-list">
465
- ${this.hubs.length > 0 ? this.hubs.map(h => `
466
- <div class="hub-item">
467
- <div class="hub-main">
468
- <span class="hub-title">${escapeHtml(h.title || h.path)}</span>
469
- <span class="hub-path">${escapeHtml(h.path)}</span>
470
- </div>
471
- <div class="hub-metrics">
472
- <span>${h.lines !== undefined ? `${h.lines} lines` : DASH}</span>
473
- </div>
474
- </div>
475
- `).join('') : `
476
- <div class="empty-hubs">No markdown documentation hubs indexed yet. Click Bootstrap below to initialize.</div>
477
- `}
478
- </div>
1162
+ renderLintScreen() {
1163
+ if (!this.lintData) {
1164
+ return `
1165
+ <div style="text-align: center; padding: 30px; color: var(--text-muted);">
1166
+ <div>Link hygiene and documentation health not audited yet.</div>
1167
+ <button class="action-btn" id="btn-run-lint" style="margin-top: 10px;">Run Health Check</button>
479
1168
  </div>
1169
+ `;
1170
+ }
480
1171
 
481
- <div class="ingest-deck">
482
- <div class="form-card">
483
- <h4 class="panel-title">Knowledge Ingest</h4>
484
- <div class="input-row">
485
- <span class="input-label">Target Hub Path</span>
486
- <input class="text-input" type="text" id="input-path" placeholder="openwiki" />
487
- </div>
488
- <label class="toggle-row">
489
- <input class="toggle-check" type="checkbox" id="check-dryrun" ${this.dryRun ? 'checked' : ''} />
490
- <span>Dry Run (Preview changes without writing)</span>
491
- </label>
492
- <button class="btn-action-primary" type="button" id="btn-ingest" ${this.executing ? 'disabled' : ''}>
493
- ${this.executing ? 'Processing...' : 'Run Ingestion'}
494
- </button>
495
- <button class="btn-action-secondary" type="button" id="btn-bootstrap" ${this.executing ? 'disabled' : ''}>
496
- Bootstrap OpenWiki Skeleton
497
- </button>
498
- </div>
1172
+ const { healthScore, totalHubs, totalLinks, brokenLinksCount, orphanCount, issues } = this.lintData;
1173
+ const scoreColor = healthScore > 80 ? 'var(--accent-green)' : (healthScore > 50 ? 'var(--accent-amber)' : 'var(--accent-red)');
499
1174
 
500
- ${this.lastResult ? `
501
- <div class="feedback-banner ${this.lastResult.ok ? 'feedback-ok' : 'feedback-fail'}">
502
- ${escapeHtml(this.lastResult.message)}
1175
+ return `
1176
+ <div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 12px;">
1177
+ <div style="font-size: 20px; font-weight: 700; color: ${scoreColor};">
1178
+ ${healthScore}% Health Score
1179
+ </div>
1180
+ <button class="back-btn" id="btn-run-lint">Re-scan</button>
1181
+ </div>
1182
+ <div class="metric-deck">
1183
+ <div class="metric-cell">
1184
+ <div class="metric-label">Links Tested</div>
1185
+ <div class="metric-val">${totalLinks}</div>
1186
+ </div>
1187
+ <div class="metric-cell">
1188
+ <div class="metric-label">Broken Links</div>
1189
+ <div class="metric-val" style="color: ${brokenLinksCount ? 'var(--accent-red)' : 'var(--accent-green)'};">${brokenLinksCount}</div>
1190
+ </div>
1191
+ <div class="metric-cell">
1192
+ <div class="metric-label">Orphan Hubs</div>
1193
+ <div class="metric-val">${orphanCount}</div>
1194
+ </div>
1195
+ </div>
1196
+ <div style="max-height: 180px; overflow-y: auto;">
1197
+ ${
1198
+ issues.length === 0
1199
+ ? '<div style="color: var(--accent-green); padding: 10px;">[OK] All documentation links and headings verified clean!</div>'
1200
+ : issues
1201
+ .map(
1202
+ (iss) => `
1203
+ <div style="padding: 6px 8px; background: var(--bg-elevated); border: 1px solid var(--border-subtle); border-radius: 4px; margin-bottom: 4px; font-size: 11px;">
1204
+ <span style="color: var(--accent-red); font-weight: 600;">[${this.escape(iss.type)}]</span>
1205
+ ${this.escape(iss.message)}
503
1206
  </div>
504
- ` : ''}
1207
+ `
1208
+ )
1209
+ .join('')
1210
+ }
1211
+ </div>
1212
+ `;
1213
+ }
1214
+
1215
+ renderUpstreamScreen() {
1216
+ return `
1217
+ <div style="margin-bottom: 12px;">
1218
+ <div style="font-weight: 600; margin-bottom: 4px;">Upstream OpenWiki Surveillance:</div>
1219
+ <div style="font-size: 12px; color: var(--text-muted);">
1220
+ Recorded: v${this.upstream?.recordedVersion || '0.7.2'} | Latest: v${this.upstream?.latestVersion || '0.7.3'}
505
1221
  </div>
506
1222
  </div>
1223
+ <button class="action-btn" id="btn-watch-exec" style="margin-top: 8px;">Check Upstream Releases</button>
1224
+ `;
1225
+ }
507
1226
 
508
- <div class="footer-bar">
509
- <span>Engine: OpenWiki v0.1.2</span>
510
- <span>Storage: Markdown + SSSS Projections</span>
1227
+ renderConfigScreen() {
1228
+ return `
1229
+ <div class="config-deck">
1230
+ <div>
1231
+ <label style="display: block; font-weight: 600; margin-bottom: 4px;">Recommended Model Presets:</label>
1232
+ <span class="preset-chip" data-model="deepseek/deepseek-chat">DeepSeek Chat</span>
1233
+ <span class="preset-chip" data-model="deepseek/deepseek-v4.1-flash">DeepSeek Flash</span>
1234
+ <span class="preset-chip" data-model="qwen/qwen-2.5-coder-32b-instruct">Qwen 2.5 Coder</span>
1235
+ </div>
1236
+ <div>
1237
+ <label style="display: block; font-weight: 600; margin-bottom: 4px;">Provider:</label>
1238
+ <select class="scope-select" id="cfg-provider" style="width: 100%;">
1239
+ <option value="openrouter" ${this.config.provider === 'openrouter' ? 'selected' : ''}>OpenRouter (Fleet Routing)</option>
1240
+ <option value="google" ${this.config.provider === 'google' ? 'selected' : ''}>Google Gemini</option>
1241
+ </select>
1242
+ </div>
1243
+ <div>
1244
+ <label style="display: block; font-weight: 600; margin-bottom: 4px;">Model ID:</label>
1245
+ <input class="search-input" id="cfg-model" value="${this.escape(this.config.model)}" style="width: 100%;" />
1246
+ </div>
1247
+ <div>
1248
+ <label style="display: block; font-weight: 600; margin-bottom: 4px;">Max Tokens:</label>
1249
+ <input type="number" class="search-input" id="cfg-tokens" value="${this.config.max_tokens}" style="width: 100%;" />
1250
+ </div>
1251
+ <div style="display: flex; justify-content: flex-end;">
1252
+ <button class="action-btn" id="btn-save-cfg">Save Settings</button>
1253
+ </div>
511
1254
  </div>
512
1255
  `;
1256
+ }
1257
+
1258
+ attachEventListeners() {
1259
+ const root = this.shadowRoot;
1260
+ if (!root) return;
1261
+
1262
+ // Scope selection
1263
+ const scopeSelect = root.querySelector('#scope-select');
1264
+ if (scopeSelect) {
1265
+ scopeSelect.addEventListener('change', (e) => this.setScope(e.target.value));
1266
+ }
513
1267
 
514
- // Event listeners
515
- const pathInput = root.querySelector('#input-path');
516
- if (pathInput) {
517
- pathInput.value = this.targetPath;
518
- pathInput.addEventListener('input', e => {
519
- this.targetPath = e.target.value;
1268
+ // Navigation buttons
1269
+ root.querySelectorAll('.nav-btn').forEach((btn) => {
1270
+ btn.addEventListener('click', () => {
1271
+ const screen = btn.getAttribute('data-screen');
1272
+ if (screen) this.setScreen(screen);
520
1273
  });
1274
+ });
1275
+
1276
+ // Hub item click -> Read Hub
1277
+ root.querySelectorAll('.hub-item').forEach((item) => {
1278
+ item.addEventListener('click', () => {
1279
+ const idx = parseInt(item.getAttribute('data-hub-index'), 10);
1280
+ if (!isNaN(idx) && this.hubs[idx]) {
1281
+ this.readHubDocument(this.hubs[idx]);
1282
+ }
1283
+ });
1284
+ });
1285
+
1286
+ // Back to hubs from reader
1287
+ const backBtn = root.querySelector('#btn-back-to-hubs');
1288
+ if (backBtn) {
1289
+ backBtn.addEventListener('click', () => {
1290
+ this.selectedHub = null;
1291
+ this.selectedHubContent = null;
1292
+ this.render();
1293
+ });
1294
+ }
1295
+
1296
+ // Refresh hubs button
1297
+ const refreshHubsBtn = root.querySelector('#btn-refresh-hubs');
1298
+ if (refreshHubsBtn) {
1299
+ refreshHubsBtn.addEventListener('click', () => this.refreshHubs());
521
1300
  }
522
1301
 
523
- const dryRunCheck = root.querySelector('#check-dryrun');
524
- if (dryRunCheck) {
525
- dryRunCheck.addEventListener('change', e => {
526
- this.dryRun = e.target.checked;
1302
+ // Ingest button
1303
+ const ingestBtn = root.querySelector('#btn-ingest-exec');
1304
+ if (ingestBtn) {
1305
+ ingestBtn.addEventListener('click', () => this.executeIngest());
1306
+ }
1307
+
1308
+ // Search button
1309
+ const searchBtn = root.querySelector('#btn-search-exec');
1310
+ const searchInput = root.querySelector('#search-input');
1311
+ if (searchBtn && searchInput) {
1312
+ searchBtn.addEventListener('click', () => this.executeSearch(searchInput.value));
1313
+ searchInput.addEventListener('keydown', (e) => {
1314
+ if (e.key === 'Enter') this.executeSearch(searchInput.value);
527
1315
  });
528
1316
  }
529
1317
 
530
- root.querySelector('#btn-ingest')?.addEventListener('click', () => this.runIngest());
531
- root.querySelector('#btn-bootstrap')?.addEventListener('click', () => this.runBootstrap());
1318
+ // Graph button
1319
+ const loadGraphBtn = root.querySelector('#btn-load-graph');
1320
+ if (loadGraphBtn) {
1321
+ loadGraphBtn.addEventListener('click', () => this.loadGraph());
1322
+ }
1323
+
1324
+ // Lint button
1325
+ const runLintBtn = root.querySelector('#btn-run-lint');
1326
+ if (runLintBtn) {
1327
+ runLintBtn.addEventListener('click', () => this.loadLint());
1328
+ }
1329
+
1330
+ // Fleet refresh button
1331
+ const fleetRefreshBtn = root.querySelector('#btn-fleet-refresh');
1332
+ if (fleetRefreshBtn) {
1333
+ fleetRefreshBtn.addEventListener('click', () => this.executeFleetRefresh());
1334
+ }
1335
+
1336
+ // Upstream watch button
1337
+ const watchBtn = root.querySelector('#btn-watch-exec');
1338
+ if (watchBtn) {
1339
+ watchBtn.addEventListener('click', () => this.executeWatch());
1340
+ }
1341
+
1342
+ // Generate button
1343
+ const genBtn = root.querySelector('#btn-generate-exec');
1344
+ const genInput = root.querySelector('#generate-repo-input');
1345
+ if (genBtn) {
1346
+ genBtn.addEventListener('click', () => {
1347
+ if (genInput) this.generateRepo = genInput.value.trim();
1348
+ this.executeGenerate();
1349
+ });
1350
+ }
1351
+
1352
+ // Save token button
1353
+ const saveTokenBtn = root.querySelector('#btn-save-token');
1354
+ const tokenProviderSelect = root.querySelector('#token-provider-select');
1355
+ const tokenValInput = root.querySelector('#token-val-input');
1356
+ if (saveTokenBtn && tokenProviderSelect && tokenValInput) {
1357
+ saveTokenBtn.addEventListener('click', () => {
1358
+ this.saveToken(tokenProviderSelect.value, tokenValInput.value);
1359
+ tokenValInput.value = '';
1360
+ });
1361
+ }
1362
+
1363
+ // Preset chips
1364
+ root.querySelectorAll('.preset-chip').forEach((chip) => {
1365
+ chip.addEventListener('click', async () => {
1366
+ const model = chip.getAttribute('data-model');
1367
+ if (model) {
1368
+ const modelInput = root.querySelector('#cfg-model');
1369
+ if (modelInput) modelInput.value = model;
1370
+ await this.setConfigValue('model', model);
1371
+ }
1372
+ });
1373
+ });
1374
+
1375
+ // Save config button
1376
+ const saveCfgBtn = root.querySelector('#btn-save-cfg');
1377
+ if (saveCfgBtn) {
1378
+ saveCfgBtn.addEventListener('click', async () => {
1379
+ const prov = root.querySelector('#cfg-provider')?.value;
1380
+ const mod = root.querySelector('#cfg-model')?.value;
1381
+ const tok = root.querySelector('#cfg-tokens')?.value;
1382
+ if (prov) await this.setConfigValue('provider', prov);
1383
+ if (mod) await this.setConfigValue('model', mod);
1384
+ if (tok) await this.setConfigValue('max_tokens', tok);
1385
+ });
1386
+ }
1387
+ }
1388
+
1389
+ escape(str) {
1390
+ if (!str) return '';
1391
+ return String(str)
1392
+ .replace(/&/g, '&amp;')
1393
+ .replace(/</g, '&lt;')
1394
+ .replace(/>/g, '&gt;')
1395
+ .replace(/"/g, '&quot;');
532
1396
  }
533
1397
  }
534
1398
 
535
- if (!customElements.get('tr-openwiki-card')) {
1399
+ if (typeof customElements !== 'undefined' && !customElements.get('tr-openwiki-card')) {
536
1400
  customElements.define('tr-openwiki-card', OpenWikiCard);
537
1401
  }
538
- if (!customElements.get('openwiki-card')) {
539
- customElements.define('openwiki-card', class extends OpenWikiCard {});
540
- }