total-recall-brain 3.12.2 → 3.13.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,472 @@
1
+ import { useState, useEffect, useRef } from 'react';
2
+ import { searchMemory, runSandbox } from '../api';
3
+ import type { MemoryNode } from '../types';
4
+
5
+ // ─── Styles ────────────────────────────────────────────────────────────────────
6
+
7
+ const inputStyle: React.CSSProperties = {
8
+ background: 'var(--bg-secondary)',
9
+ color: 'var(--text-primary)',
10
+ border: '1px solid var(--border)',
11
+ borderRadius: 6,
12
+ padding: '10px 14px',
13
+ fontSize: 14,
14
+ outline: 'none',
15
+ width: '100%',
16
+ boxSizing: 'border-box',
17
+ };
18
+
19
+ const cardStyle: React.CSSProperties = {
20
+ background: 'rgba(255,255,255,0.03)',
21
+ border: '1px solid var(--border)',
22
+ backdropFilter: 'blur(12px)',
23
+ borderRadius: 10,
24
+ padding: 24,
25
+ };
26
+
27
+ const outputStyle: React.CSSProperties = {
28
+ background: 'var(--bg-secondary)',
29
+ border: '1px solid var(--border)',
30
+ borderRadius: 8,
31
+ padding: 16,
32
+ fontFamily: 'var(--font-mono, monospace)',
33
+ fontSize: 13,
34
+ lineHeight: 1.6,
35
+ color: 'var(--text-secondary)',
36
+ whiteSpace: 'pre-wrap',
37
+ wordBreak: 'break-word',
38
+ maxHeight: 280,
39
+ overflowY: 'auto',
40
+ marginTop: 16,
41
+ };
42
+
43
+ const labelStyle: React.CSSProperties = {
44
+ fontSize: 13,
45
+ fontWeight: 500,
46
+ color: 'var(--text-secondary)',
47
+ };
48
+
49
+ const badgeStyle = (bg: string, fg: string): React.CSSProperties => ({
50
+ display: 'inline-block',
51
+ padding: '3px 10px',
52
+ borderRadius: 4,
53
+ fontSize: 11,
54
+ fontWeight: 600,
55
+ textTransform: 'uppercase',
56
+ background: bg,
57
+ color: fg,
58
+ letterSpacing: 0.4,
59
+ });
60
+
61
+ const nodeCardStyle: React.CSSProperties = {
62
+ ...cardStyle,
63
+ padding: 18,
64
+ cursor: 'pointer',
65
+ transition: 'border-color 0.2s, transform 0.15s',
66
+ };
67
+
68
+ const pillStyle: React.CSSProperties = {
69
+ display: 'inline-block',
70
+ padding: '2px 8px',
71
+ borderRadius: 3,
72
+ fontSize: 11,
73
+ fontWeight: 500,
74
+ background: 'rgba(108,92,231,0.15)',
75
+ color: 'var(--accent)',
76
+ marginRight: 4,
77
+ marginBottom: 4,
78
+ };
79
+
80
+ const drawerOverlay: React.CSSProperties = {
81
+ position: 'fixed',
82
+ inset: 0,
83
+ background: 'rgba(0,0,0,0.5)',
84
+ zIndex: 8000,
85
+ display: 'flex',
86
+ justifyContent: 'flex-end',
87
+ };
88
+
89
+ const drawerPanel: React.CSSProperties = {
90
+ width: '100%',
91
+ maxWidth: 560,
92
+ height: '100%',
93
+ background: 'var(--bg-primary)',
94
+ borderLeft: '1px solid var(--border)',
95
+ overflowY: 'auto',
96
+ padding: 32,
97
+ animation: 'slideInRight 0.25s ease-out',
98
+ };
99
+
100
+ // ─── Helpers ───────────────────────────────────────────────────────────────────
101
+
102
+ function excerpt(node: MemoryNode, maxLen = 200): string {
103
+ const text = node.body || node.content || node.excerpt || '';
104
+ if (text.length <= maxLen) return text;
105
+ return text.slice(0, maxLen).trimEnd() + '…';
106
+ }
107
+
108
+ function categoryColor(cat: string): { bg: string; fg: string } {
109
+ const map: Record<string, { bg: string; fg: string }> = {
110
+ invariant: { bg: 'rgba(255,107,107,0.15)', fg: 'var(--error)' },
111
+ preference: { bg: 'rgba(108,92,231,0.15)', fg: 'var(--accent)' },
112
+ fact: { bg: 'rgba(0,206,201,0.15)', fg: 'var(--success)' },
113
+ concept: { bg: 'rgba(253,203,110,0.15)', fg: 'var(--warning)' },
114
+ pattern: { bg: 'rgba(108,92,231,0.15)', fg: 'var(--accent)' },
115
+ decision: { bg: 'rgba(253,203,110,0.15)', fg: 'var(--warning)' },
116
+ lore: { bg: 'rgba(158,158,158,0.15)', fg: 'var(--text-secondary)' },
117
+ };
118
+ return map[cat] || { bg: 'rgba(158,158,158,0.12)', fg: 'var(--text-secondary)' };
119
+ }
120
+
121
+ // ─── Component ─────────────────────────────────────────────────────────────────
122
+
123
+ export default function OpenWikiPage() {
124
+ // Status
125
+ const [initialized, setInitialized] = useState<boolean | null>(null);
126
+ const [nodeCount, setNodeCount] = useState(0);
127
+ const [statusLoading, setStatusLoading] = useState(true);
128
+
129
+ // Ingestion
130
+ const [wikiPath, setWikiPath] = useState('.');
131
+ const [cmdOutput, setCmdOutput] = useState('');
132
+ const [cmdRunning, setCmdRunning] = useState(false);
133
+ const [cmdError, setCmdError] = useState(false);
134
+
135
+ // Node Browser
136
+ const [nodes, setNodes] = useState<MemoryNode[]>([]);
137
+ const [nodesLoading, setNodesLoading] = useState(false);
138
+ const [selectedNode, setSelectedNode] = useState<MemoryNode | null>(null);
139
+ const drawerRef = useRef<HTMLDivElement>(null);
140
+
141
+ // ── Data Fetching ──
142
+
143
+ const fetchNodes = async () => {
144
+ setNodesLoading(true);
145
+ try {
146
+ const results = await searchMemory('openwiki');
147
+ setNodes(results);
148
+ setNodeCount(results.length);
149
+ setInitialized(results.length > 0);
150
+ } catch {
151
+ setNodes([]);
152
+ setInitialized(false);
153
+ } finally {
154
+ setNodesLoading(false);
155
+ setStatusLoading(false);
156
+ }
157
+ };
158
+
159
+ useEffect(() => {
160
+ fetchNodes();
161
+ // eslint-disable-next-line react-hooks/exhaustive-deps
162
+ }, []);
163
+
164
+ // ── Ingestion Handlers ──
165
+
166
+ const runCliCommand = async (command: string) => {
167
+ setCmdRunning(true);
168
+ setCmdOutput('');
169
+ setCmdError(false);
170
+ try {
171
+ const code = `
172
+ const { execSync } = require('child_process');
173
+ try {
174
+ const out = execSync(${JSON.stringify(command)}, { encoding: 'utf8', timeout: 60000 });
175
+ console.log(out);
176
+ } catch (e) {
177
+ console.error(e.stderr || e.message);
178
+ process.exit(1);
179
+ }`;
180
+ const result = await runSandbox(code, 60000);
181
+ setCmdOutput(result.output);
182
+ setCmdError(!result.success);
183
+ if (result.success) {
184
+ // Refresh nodes after successful ingestion
185
+ await fetchNodes();
186
+ }
187
+ } catch (e) {
188
+ setCmdOutput((e as Error).message);
189
+ setCmdError(true);
190
+ } finally {
191
+ setCmdRunning(false);
192
+ }
193
+ };
194
+
195
+ const handleInit = () => {
196
+ runCliCommand('npx -y openwiki --init');
197
+ };
198
+
199
+ const handleIngest = () => {
200
+ const safePath = wikiPath.trim() || '.';
201
+ runCliCommand(`npx total-recall ingest openwiki ${JSON.stringify(safePath)}`);
202
+ };
203
+
204
+ // ── Drawer close on outside click ──
205
+ const handleOverlayClick = (e: React.MouseEvent) => {
206
+ if (drawerRef.current && !drawerRef.current.contains(e.target as Node)) {
207
+ setSelectedNode(null);
208
+ }
209
+ };
210
+
211
+ // ── Render ──
212
+
213
+ return (
214
+ <div className="page">
215
+ {/* Inline keyframes for drawer animation */}
216
+ <style>{`
217
+ @keyframes slideInRight {
218
+ from { transform: translateX(100%); opacity: 0; }
219
+ to { transform: translateX(0); opacity: 1; }
220
+ }
221
+ .openwiki-node-card:hover {
222
+ border-color: var(--accent) !important;
223
+ transform: translateY(-2px);
224
+ }
225
+ `}</style>
226
+
227
+ <div className="page-header">
228
+ <h1>📚 OpenWiki Browser</h1>
229
+ <p style={{ color: 'var(--text-secondary)', margin: '4px 0 0' }}>
230
+ Auto-document your codebase with OpenWiki and ingest nodes into memory.
231
+ </p>
232
+ </div>
233
+
234
+ {/* ── Status Banner ── */}
235
+ <div
236
+ id="openwiki-status-banner"
237
+ style={{
238
+ ...cardStyle,
239
+ display: 'flex',
240
+ alignItems: 'center',
241
+ justifyContent: 'space-between',
242
+ flexWrap: 'wrap',
243
+ gap: 12,
244
+ marginBottom: 24,
245
+ padding: '16px 24px',
246
+ }}
247
+ >
248
+ <div style={{ display: 'flex', alignItems: 'center', gap: 12 }}>
249
+ {statusLoading ? (
250
+ <span style={{ color: 'var(--text-secondary)', fontSize: 14 }}>Checking status…</span>
251
+ ) : initialized ? (
252
+ <>
253
+ <span style={badgeStyle('rgba(0,206,201,0.15)', 'var(--success)')}>Initialized</span>
254
+ <span style={{ color: 'var(--text-secondary)', fontSize: 13 }}>
255
+ {nodeCount} node{nodeCount !== 1 ? 's' : ''} ingested
256
+ </span>
257
+ </>
258
+ ) : (
259
+ <span style={badgeStyle('rgba(255,107,107,0.15)', 'var(--error)')}>Not Initialized</span>
260
+ )}
261
+ </div>
262
+ <span style={{ color: 'var(--text-secondary)', fontSize: 12, fontFamily: 'var(--font-mono, monospace)' }}>
263
+ openwiki · total-recall integration
264
+ </span>
265
+ </div>
266
+
267
+ {/* ── Ingestion Section ── */}
268
+ <div style={{ ...cardStyle, marginBottom: 24 }}>
269
+ <h2 style={{ fontSize: 16, fontWeight: 600, color: 'var(--text-primary)', marginBottom: 16 }}>
270
+ Ingestion
271
+ </h2>
272
+ <div style={{ display: 'flex', flexDirection: 'column', gap: 14 }}>
273
+ <div style={{ display: 'flex', flexDirection: 'column', gap: 6 }}>
274
+ <label htmlFor="openwiki-path" style={labelStyle}>OpenWiki Directory</label>
275
+ <input
276
+ id="openwiki-path"
277
+ type="text"
278
+ placeholder="Path to OpenWiki directory (default: current project root)"
279
+ value={wikiPath}
280
+ onChange={e => setWikiPath(e.target.value)}
281
+ style={inputStyle}
282
+ />
283
+ </div>
284
+
285
+ <div style={{ display: 'flex', gap: 10, flexWrap: 'wrap' }}>
286
+ <button
287
+ id="openwiki-init"
288
+ className="btn btn-ghost"
289
+ onClick={handleInit}
290
+ disabled={cmdRunning}
291
+ >
292
+ {cmdRunning ? '⏳' : '⚙'} Initialize OpenWiki
293
+ </button>
294
+ <button
295
+ id="openwiki-ingest"
296
+ className="btn btn-primary"
297
+ onClick={handleIngest}
298
+ disabled={cmdRunning}
299
+ >
300
+ {cmdRunning ? '⏳ Ingesting…' : '📥 Ingest into Memory'}
301
+ </button>
302
+ </div>
303
+ </div>
304
+
305
+ {cmdOutput && (
306
+ <div
307
+ id="openwiki-cmd-output"
308
+ style={{ ...outputStyle, borderColor: cmdError ? 'var(--error)' : 'var(--border)' }}
309
+ >
310
+ {cmdOutput}
311
+ </div>
312
+ )}
313
+ </div>
314
+
315
+ {/* ── Node Browser ── */}
316
+ <div style={{ marginBottom: 24 }}>
317
+ <h2 style={{ fontSize: 16, fontWeight: 600, color: 'var(--text-primary)', marginBottom: 16 }}>
318
+ Node Browser
319
+ </h2>
320
+
321
+ {nodesLoading && (
322
+ <div style={{ textAlign: 'center', padding: '40px 20px', color: 'var(--text-secondary)' }}>
323
+ Loading nodes…
324
+ </div>
325
+ )}
326
+
327
+ {!nodesLoading && nodes.length === 0 && (
328
+ <div
329
+ id="openwiki-empty-state"
330
+ style={{
331
+ ...cardStyle,
332
+ textAlign: 'center',
333
+ padding: '48px 24px',
334
+ }}
335
+ >
336
+ <div style={{ fontSize: 36, marginBottom: 12 }}>📭</div>
337
+ <p style={{ color: 'var(--text-secondary)', fontSize: 15, maxWidth: 400, margin: '0 auto' }}>
338
+ No OpenWiki nodes found. Initialize OpenWiki to auto-document your codebase.
339
+ </p>
340
+ </div>
341
+ )}
342
+
343
+ {!nodesLoading && nodes.length > 0 && (
344
+ <div
345
+ style={{
346
+ display: 'grid',
347
+ gridTemplateColumns: 'repeat(auto-fill, minmax(300px, 1fr))',
348
+ gap: 14,
349
+ }}
350
+ >
351
+ {nodes.map(node => {
352
+ const cc = categoryColor(node.category);
353
+ return (
354
+ <div
355
+ key={node.slug}
356
+ id={`openwiki-node-${node.slug}`}
357
+ className="openwiki-node-card"
358
+ style={nodeCardStyle}
359
+ onClick={() => setSelectedNode(node)}
360
+ role="button"
361
+ tabIndex={0}
362
+ onKeyDown={e => { if (e.key === 'Enter') setSelectedNode(node); }}
363
+ >
364
+ <div style={{ display: 'flex', alignItems: 'center', gap: 8, marginBottom: 8 }}>
365
+ <span style={{ fontSize: 15, fontWeight: 600, color: 'var(--text-primary)', flex: 1, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>
366
+ {node.title}
367
+ </span>
368
+ <span style={badgeStyle(cc.bg, cc.fg)}>{node.category}</span>
369
+ </div>
370
+
371
+ {node.tags && node.tags.length > 0 && (
372
+ <div style={{ marginBottom: 8 }}>
373
+ {node.tags.slice(0, 5).map(tag => (
374
+ <span key={tag} style={pillStyle}>{tag}</span>
375
+ ))}
376
+ {node.tags.length > 5 && (
377
+ <span style={{ ...pillStyle, background: 'rgba(158,158,158,0.12)', color: 'var(--text-secondary)' }}>
378
+ +{node.tags.length - 5}
379
+ </span>
380
+ )}
381
+ </div>
382
+ )}
383
+
384
+ <p style={{ color: 'var(--text-secondary)', fontSize: 13, lineHeight: 1.5, margin: 0 }}>
385
+ {excerpt(node)}
386
+ </p>
387
+ </div>
388
+ );
389
+ })}
390
+ </div>
391
+ )}
392
+ </div>
393
+
394
+ {/* ── Slide-out Drawer ── */}
395
+ {selectedNode && (
396
+ <div style={drawerOverlay} onClick={handleOverlayClick}>
397
+ <div ref={drawerRef} style={drawerPanel} id="openwiki-node-drawer">
398
+ <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'flex-start', marginBottom: 24 }}>
399
+ <div style={{ flex: 1, minWidth: 0 }}>
400
+ <h2 style={{ fontSize: 20, fontWeight: 600, color: 'var(--text-primary)', margin: '0 0 8px', wordBreak: 'break-word' }}>
401
+ {selectedNode.title}
402
+ </h2>
403
+ <div style={{ display: 'flex', alignItems: 'center', gap: 8, flexWrap: 'wrap' }}>
404
+ {(() => { const cc = categoryColor(selectedNode.category); return <span style={badgeStyle(cc.bg, cc.fg)}>{selectedNode.category}</span>; })()}
405
+ <span style={{ fontSize: 12, color: 'var(--text-secondary)', fontFamily: 'var(--font-mono, monospace)' }}>
406
+ {selectedNode.slug}
407
+ </span>
408
+ </div>
409
+ </div>
410
+ <button
411
+ id="openwiki-drawer-close"
412
+ className="btn btn-ghost"
413
+ onClick={() => setSelectedNode(null)}
414
+ style={{ fontSize: 18, padding: '4px 10px', flexShrink: 0 }}
415
+ aria-label="Close drawer"
416
+ >
417
+ ✕
418
+ </button>
419
+ </div>
420
+
421
+ {selectedNode.tags && selectedNode.tags.length > 0 && (
422
+ <div style={{ marginBottom: 16 }}>
423
+ {selectedNode.tags.map(tag => (
424
+ <span key={tag} style={pillStyle}>{tag}</span>
425
+ ))}
426
+ </div>
427
+ )}
428
+
429
+ {selectedNode.importance && (
430
+ <div style={{ marginBottom: 12, fontSize: 13, color: 'var(--text-secondary)' }}>
431
+ <strong style={{ color: 'var(--text-primary)' }}>Importance:</strong> {selectedNode.importance}/5
432
+ {selectedNode.modality && (
433
+ <span> · <strong style={{ color: 'var(--text-primary)' }}>Modality:</strong> {selectedNode.modality}</span>
434
+ )}
435
+ {selectedNode.priority && (
436
+ <span> · <strong style={{ color: 'var(--text-primary)' }}>Priority:</strong> {selectedNode.priority}</span>
437
+ )}
438
+ </div>
439
+ )}
440
+
441
+ <div
442
+ style={{
443
+ background: 'var(--bg-secondary)',
444
+ border: '1px solid var(--border)',
445
+ borderRadius: 8,
446
+ padding: 20,
447
+ fontSize: 14,
448
+ lineHeight: 1.7,
449
+ color: 'var(--text-primary)',
450
+ whiteSpace: 'pre-wrap',
451
+ wordBreak: 'break-word',
452
+ }}
453
+ >
454
+ {selectedNode.body || selectedNode.content || selectedNode.excerpt || 'No content available.'}
455
+ </div>
456
+
457
+ {selectedNode.related && selectedNode.related.length > 0 && (
458
+ <div style={{ marginTop: 16 }}>
459
+ <span style={{ fontSize: 13, fontWeight: 500, color: 'var(--text-secondary)' }}>Related: </span>
460
+ {selectedNode.related.map(slug => (
461
+ <span key={slug} style={{ ...pillStyle, background: 'rgba(158,158,158,0.12)', color: 'var(--text-secondary)' }}>
462
+ {slug}
463
+ </span>
464
+ ))}
465
+ </div>
466
+ )}
467
+ </div>
468
+ </div>
469
+ )}
470
+ </div>
471
+ );
472
+ }
@@ -835,6 +835,189 @@ export default function SettingsPage() {
835
835
  </div>
836
836
  </div>
837
837
 
838
+ {/* UCW WORKSPACE EXPORT (SSSS §16 .ucw Bundle Format) */}
839
+ <div style={{
840
+ background: 'rgba(255,255,255,0.03)',
841
+ border: '1px solid var(--border)',
842
+ borderRadius: 10,
843
+ padding: 20,
844
+ marginTop: 20,
845
+ backdropFilter: 'blur(12px)',
846
+ }}>
847
+ <div style={{ display: 'flex', alignItems: 'center', gap: 10, marginBottom: 16 }}>
848
+ <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="var(--accent)" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round">
849
+ <path d="M21 15v4a2 2 0 01-2 2H5a2 2 0 01-2-2v-4" />
850
+ <polyline points="7 10 12 15 17 10" />
851
+ <line x1="12" y1="15" x2="12" y2="3" />
852
+ </svg>
853
+ <h3 style={{ margin: 0, fontSize: 16, fontWeight: 600, color: 'var(--text-primary)' }}>UCW Bundle (SSSS §16)</h3>
854
+ <span className="badge badge-accent" style={{ fontSize: 10, padding: '2px 8px' }}>UltraChat Workspace</span>
855
+ </div>
856
+ <p style={{ fontSize: 13, color: 'var(--text-secondary)', marginBottom: 16 }}>
857
+ Export your vault as a <code style={{ background: 'rgba(255,255,255,0.06)', padding: '2px 6px', borderRadius: 4, fontFamily: 'var(--font-mono, monospace)' }}>package.ucw.json</code> bundle
858
+ per the SSSS §16 Universal Containerized Workspace format. Bundles can be backed up, shared, sold, and re-provisioned across any SSSS-compatible host.
859
+ </p>
860
+ <div style={{ display: 'flex', gap: 16, flexWrap: 'wrap', alignItems: 'center', marginBottom: 16 }}>
861
+ <div style={{ display: 'flex', flexDirection: 'column', gap: 4 }}>
862
+ <label style={{ fontSize: 11, fontWeight: 500, color: 'var(--text-secondary)' }}>Export Profile</label>
863
+ <select
864
+ id="ucw-export-profile"
865
+ style={{ background: 'var(--bg-secondary)', color: 'var(--text-primary)', border: '1px solid var(--border)', borderRadius: 6, padding: '6px 12px', fontSize: 12, outline: 'none' }}
866
+ >
867
+ <option value="backup">backup — full vault snapshot</option>
868
+ <option value="template">template — strips tenant_private</option>
869
+ <option value="sale">sale — portable marketplace bundle</option>
870
+ </select>
871
+ </div>
872
+ <div style={{ display: 'flex', flexDirection: 'column', gap: 4, flex: '1 1 200px' }}>
873
+ <label style={{ fontSize: 11, fontWeight: 500, color: 'var(--text-secondary)' }}>Output Filename</label>
874
+ <input
875
+ id="ucw-export-filename"
876
+ type="text"
877
+ defaultValue="package.ucw.json"
878
+ style={{ background: 'var(--bg-secondary)', color: 'var(--text-primary)', border: '1px solid var(--border)', borderRadius: 6, padding: '6px 12px', fontSize: 12, outline: 'none', fontFamily: 'var(--font-mono, monospace)' }}
879
+ />
880
+ </div>
881
+ </div>
882
+ <div style={{ display: 'flex', gap: 10, flexWrap: 'wrap' }}>
883
+ <button
884
+ id="ucw-export-btn"
885
+ className="btn btn-primary"
886
+ style={{ background: 'linear-gradient(135deg, var(--accent), #5a4bd1)', color: '#fff', borderRadius: 6, fontSize: 13, padding: '8px 20px' }}
887
+ onClick={async () => {
888
+ try {
889
+ const profile = (document.getElementById('ucw-export-profile') as HTMLSelectElement)?.value || 'backup';
890
+ const filename = (document.getElementById('ucw-export-filename') as HTMLInputElement)?.value || 'package.ucw.json';
891
+ const { runSandbox } = await import('../api');
892
+ const result = await runSandbox(`const { execSync } = require("child_process"); try { const out = execSync("npx ssss export . --profile ${profile} --out ${filename} 2>&1", { encoding: "utf-8", cwd: process.env.HOME + "/.agent/skills/total-recall" }); console.log(JSON.stringify({ success: true, output: out })); } catch(e) { console.log(JSON.stringify({ success: false, output: e.stdout || e.message })); }`, 60000);
893
+ if (result.success) {
894
+ setSuccess(true);
895
+ setTimeout(() => setSuccess(false), 5000);
896
+ } else {
897
+ setError(result.output || 'UCW export failed');
898
+ }
899
+ } catch (err: unknown) {
900
+ setError((err as Error).message || 'Export failed');
901
+ }
902
+ }}
903
+ >
904
+ ⬇ Export .ucw Bundle
905
+ </button>
906
+ <button
907
+ id="ucw-validate-btn"
908
+ className="btn btn-ghost"
909
+ style={{ borderRadius: 6, fontSize: 13, padding: '8px 20px' }}
910
+ onClick={async () => {
911
+ try {
912
+ const { runSandbox } = await import('../api');
913
+ const result = await runSandbox('const { execSync } = require("child_process"); try { const out = execSync("npx ssss validate package.ucw.json 2>&1", { encoding: "utf-8", cwd: process.env.HOME + "/.agent/skills/total-recall" }); console.log(JSON.stringify({ success: true, output: out })); } catch(e) { console.log(JSON.stringify({ success: false, output: e.stdout || e.message })); }', 30000);
914
+ if (result.success) {
915
+ setSuccess(true);
916
+ setTimeout(() => setSuccess(false), 5000);
917
+ } else {
918
+ setError(result.output || 'Validation failed');
919
+ }
920
+ } catch (err: unknown) {
921
+ setError((err as Error).message || 'Validation failed');
922
+ }
923
+ }}
924
+ >
925
+ ✓ Validate Bundle
926
+ </button>
927
+ <button
928
+ id="ucw-import-btn"
929
+ className="btn btn-ghost"
930
+ style={{ borderRadius: 6, fontSize: 13, padding: '8px 20px' }}
931
+ onClick={async () => {
932
+ try {
933
+ const { runSandbox } = await import('../api');
934
+ const result = await runSandbox('const { execSync } = require("child_process"); try { const out = execSync("npx ssss import package.ucw.json --vault . 2>&1", { encoding: "utf-8", cwd: process.env.HOME + "/.agent/skills/total-recall" }); console.log(JSON.stringify({ success: true, output: out })); } catch(e) { console.log(JSON.stringify({ success: false, output: e.stdout || e.message })); }', 60000);
935
+ if (result.success) {
936
+ setSuccess(true);
937
+ setTimeout(() => setSuccess(false), 5000);
938
+ } else {
939
+ setError(result.output || 'Import failed');
940
+ }
941
+ } catch (err: unknown) {
942
+ setError((err as Error).message || 'Import failed');
943
+ }
944
+ }}
945
+ >
946
+ ⬆ Import .ucw Bundle
947
+ </button>
948
+ <button
949
+ id="ucw-inspect-btn"
950
+ className="btn btn-ghost"
951
+ style={{ borderRadius: 6, fontSize: 13, padding: '8px 20px' }}
952
+ onClick={async () => {
953
+ try {
954
+ const { runSandbox } = await import('../api');
955
+ const result = await runSandbox('const { execSync } = require("child_process"); try { const out = execSync("npx ssss inspect package.ucw.json --files 2>&1", { encoding: "utf-8", cwd: process.env.HOME + "/.agent/skills/total-recall" }); console.log(JSON.stringify({ success: true, output: out })); } catch(e) { console.log(JSON.stringify({ success: false, output: e.stdout || e.message })); }', 30000);
956
+ if (result.success) {
957
+ setSuccess(true);
958
+ setTimeout(() => setSuccess(false), 5000);
959
+ } else {
960
+ setError(result.output || 'Inspect failed');
961
+ }
962
+ } catch (err: unknown) {
963
+ setError((err as Error).message || 'Inspect failed');
964
+ }
965
+ }}
966
+ >
967
+ 🔍 Inspect Bundle
968
+ </button>
969
+ </div>
970
+ <p style={{ fontSize: 11, color: 'var(--text-tertiary)', marginTop: 12 }}>
971
+ Powered by <code style={{ fontFamily: 'var(--font-mono, monospace)' }}>@ssss/cli</code> v0.7.0 — <code style={{ fontFamily: 'var(--font-mono, monospace)' }}>npx ssss export|import|validate|inspect</code>
972
+ </p>
973
+ </div>
974
+
975
+ {/* SECRETS.ENC KEY VIEWER */}
976
+ <div style={{
977
+ background: 'rgba(255,255,255,0.03)',
978
+ border: '1px solid var(--border)',
979
+ borderRadius: 10,
980
+ padding: 20,
981
+ marginTop: 16,
982
+ backdropFilter: 'blur(12px)',
983
+ }}>
984
+ <div style={{ display: 'flex', alignItems: 'center', gap: 10, marginBottom: 16 }}>
985
+ <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="var(--warning)" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round">
986
+ <rect x="3" y="11" width="18" height="11" rx="2" ry="2" />
987
+ <path d="M7 11V7a5 5 0 0110 0v4" />
988
+ </svg>
989
+ <h3 style={{ margin: 0, fontSize: 16, fontWeight: 600, color: 'var(--text-primary)' }}>Stored Secrets (secrets.enc)</h3>
990
+ </div>
991
+ <p style={{ fontSize: 13, color: 'var(--text-secondary)', marginBottom: 16 }}>
992
+ These credentials are stored in your encrypted secrets file. Values are masked for security.
993
+ </p>
994
+ <div style={{ display: 'grid', gap: 8 }}>
995
+ {configData?.secrets && Object.entries(configData.secrets).map(([key, value]) => (
996
+ <div key={key} style={{
997
+ display: 'flex',
998
+ alignItems: 'center',
999
+ justifyContent: 'space-between',
1000
+ padding: '8px 12px',
1001
+ background: 'rgba(255,255,255,0.02)',
1002
+ borderRadius: 6,
1003
+ border: '1px solid rgba(255,255,255,0.04)',
1004
+ }}>
1005
+ <span style={{ fontSize: 13, fontFamily: 'var(--font-mono, monospace)', color: 'var(--text-primary)' }}>
1006
+ {key}
1007
+ </span>
1008
+ <span style={{ fontSize: 12, fontFamily: 'var(--font-mono, monospace)', color: 'var(--text-tertiary)' }}>
1009
+ {value ? `${String(value).slice(0, 4)}${'•'.repeat(Math.min(20, String(value).length - 4))}` : '(not set)'}
1010
+ </span>
1011
+ </div>
1012
+ ))}
1013
+ {(!configData?.secrets || Object.keys(configData.secrets).length === 0) && (
1014
+ <div style={{ fontSize: 13, color: 'var(--text-tertiary)', padding: '12px', textAlign: 'center' }}>
1015
+ No secrets configured. Add API keys through the visual settings editor above.
1016
+ </div>
1017
+ )}
1018
+ </div>
1019
+ </div>
1020
+
838
1021
  {/* FOOTER ACTIONS */}
839
1022
  <div style={{ display: 'flex', justifyContent: 'flex-end', marginTop: 16, borderTop: '1px solid var(--border)', paddingTop: 16, flexShrink: 0 }}>
840
1023
  <button