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,535 @@
1
+ import { useState } from 'react';
2
+ import { runSandbox } from '../api';
3
+
4
+ type OkfTab = 'import' | 'export' | 'lint';
5
+ type ConflictMode = 'skip' | 'warn' | 'overwrite';
6
+ type CategoryOverride = 'auto' | 'invariants' | 'preferences' | 'facts' | 'concepts' | 'patterns' | 'decisions' | 'lore';
7
+ type ExportFormat = 'directory' | 'tar.gz';
8
+ type ExportScope = 'all' | 'global' | 'project';
9
+
10
+ interface LintRow {
11
+ slug: string;
12
+ field: string;
13
+ severity: 'error' | 'warning' | 'info';
14
+ message: string;
15
+ }
16
+
17
+ // ─── Styles ────────────────────────────────────────────────────────────────────
18
+
19
+ const tabBarStyle: React.CSSProperties = {
20
+ display: 'inline-flex',
21
+ gap: 0,
22
+ borderBottom: '1px solid var(--border)',
23
+ marginBottom: 24,
24
+ };
25
+
26
+ const tabStyle = (active: boolean): React.CSSProperties => ({
27
+ padding: '10px 20px',
28
+ background: 'transparent',
29
+ border: 'none',
30
+ borderBottom: active ? '2px solid var(--accent)' : '2px solid transparent',
31
+ color: active ? 'var(--text-primary)' : 'var(--text-secondary)',
32
+ fontWeight: active ? 600 : 400,
33
+ fontSize: 14,
34
+ cursor: 'pointer',
35
+ transition: 'color 0.2s, border-color 0.2s',
36
+ });
37
+
38
+ const inputStyle: React.CSSProperties = {
39
+ background: 'var(--bg-secondary)',
40
+ color: 'var(--text-primary)',
41
+ border: '1px solid var(--border)',
42
+ borderRadius: 6,
43
+ padding: '10px 14px',
44
+ fontSize: 14,
45
+ outline: 'none',
46
+ width: '100%',
47
+ boxSizing: 'border-box',
48
+ };
49
+
50
+ const selectStyle: React.CSSProperties = {
51
+ ...inputStyle,
52
+ appearance: 'none',
53
+ backgroundImage: `url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' fill='%239e9e9e' viewBox='0 0 16 16'%3E%3Cpath d='M8 11L3 6h10z'/%3E%3C/svg%3E")`,
54
+ backgroundRepeat: 'no-repeat',
55
+ backgroundPosition: 'right 12px center',
56
+ paddingRight: 32,
57
+ };
58
+
59
+ const fieldGroup: React.CSSProperties = {
60
+ display: 'flex',
61
+ flexDirection: 'column',
62
+ gap: 6,
63
+ };
64
+
65
+ const labelStyle: React.CSSProperties = {
66
+ fontSize: 13,
67
+ fontWeight: 500,
68
+ color: 'var(--text-secondary)',
69
+ };
70
+
71
+ const cardStyle: React.CSSProperties = {
72
+ background: 'rgba(255,255,255,0.03)',
73
+ border: '1px solid var(--border)',
74
+ backdropFilter: 'blur(12px)',
75
+ borderRadius: 10,
76
+ padding: 24,
77
+ };
78
+
79
+ const outputStyle: React.CSSProperties = {
80
+ background: 'var(--bg-secondary)',
81
+ border: '1px solid var(--border)',
82
+ borderRadius: 8,
83
+ padding: 16,
84
+ fontFamily: 'var(--font-mono, monospace)',
85
+ fontSize: 13,
86
+ lineHeight: 1.6,
87
+ color: 'var(--text-secondary)',
88
+ whiteSpace: 'pre-wrap',
89
+ wordBreak: 'break-word',
90
+ maxHeight: 320,
91
+ overflowY: 'auto',
92
+ marginTop: 16,
93
+ };
94
+
95
+ const formGrid: React.CSSProperties = {
96
+ display: 'grid',
97
+ gridTemplateColumns: '1fr 1fr',
98
+ gap: 16,
99
+ };
100
+
101
+ const severityColor: Record<string, string> = {
102
+ error: 'var(--error)',
103
+ warning: 'var(--warning)',
104
+ info: 'var(--accent)',
105
+ };
106
+
107
+ // ─── Component ─────────────────────────────────────────────────────────────────
108
+
109
+ export default function OkfPage() {
110
+ const [activeTab, setActiveTab] = useState<OkfTab>('import');
111
+
112
+ // ── Import state ──
113
+ const [importPath, setImportPath] = useState('');
114
+ const [conflictMode, setConflictMode] = useState<ConflictMode>('warn');
115
+ const [categoryOverride, setCategoryOverride] = useState<CategoryOverride>('auto');
116
+ const [importanceOverride, setImportanceOverride] = useState('');
117
+ const [importOutput, setImportOutput] = useState('');
118
+ const [importRunning, setImportRunning] = useState(false);
119
+ const [importError, setImportError] = useState(false);
120
+
121
+ // ── Export state ──
122
+ const [exportPath, setExportPath] = useState('');
123
+ const [stripMeta, setStripMeta] = useState(false);
124
+ const [exportFormat, setExportFormat] = useState<ExportFormat>('directory');
125
+ const [exportScope, setExportScope] = useState<ExportScope>('all');
126
+ const [exportOutput, setExportOutput] = useState('');
127
+ const [exportRunning, setExportRunning] = useState(false);
128
+ const [exportError, setExportError] = useState(false);
129
+
130
+ // ── Lint state ──
131
+ const [lintRows, setLintRows] = useState<LintRow[]>([]);
132
+ const [lintRan, setLintRan] = useState(false);
133
+ const [lintRunning, setLintRunning] = useState(false);
134
+ const [lintRawOutput, setLintRawOutput] = useState('');
135
+
136
+ // ── Handlers ──
137
+
138
+ const handleImport = async () => {
139
+ if (!importPath.trim()) return;
140
+ setImportRunning(true);
141
+ setImportOutput('');
142
+ setImportError(false);
143
+ try {
144
+ let cmd = `npx total-recall ingest okf ${JSON.stringify(importPath.trim())} --on-conflict ${conflictMode}`;
145
+ if (categoryOverride !== 'auto') cmd += ` --category ${categoryOverride}`;
146
+ const imp = parseInt(importanceOverride, 10);
147
+ if (imp >= 1 && imp <= 5) cmd += ` --importance ${imp}`;
148
+
149
+ const code = `
150
+ const { execSync } = require('child_process');
151
+ try {
152
+ const out = execSync(${JSON.stringify(cmd)}, { encoding: 'utf8', timeout: 30000 });
153
+ console.log(out);
154
+ } catch (e) {
155
+ console.error(e.stderr || e.message);
156
+ process.exit(1);
157
+ }`;
158
+ const result = await runSandbox(code, 30000);
159
+ setImportOutput(result.output);
160
+ setImportError(!result.success);
161
+ } catch (e) {
162
+ setImportOutput((e as Error).message);
163
+ setImportError(true);
164
+ } finally {
165
+ setImportRunning(false);
166
+ }
167
+ };
168
+
169
+ const handleExport = async () => {
170
+ if (!exportPath.trim()) return;
171
+ setExportRunning(true);
172
+ setExportOutput('');
173
+ setExportError(false);
174
+ try {
175
+ let cmd = `npx total-recall export okf ${JSON.stringify(exportPath.trim())} --format ${exportFormat} --scope ${exportScope}`;
176
+ if (stripMeta) cmd += ' --strip-ssss';
177
+
178
+ const code = `
179
+ const { execSync } = require('child_process');
180
+ try {
181
+ const out = execSync(${JSON.stringify(cmd)}, { encoding: 'utf8', timeout: 30000 });
182
+ console.log(out);
183
+ } catch (e) {
184
+ console.error(e.stderr || e.message);
185
+ process.exit(1);
186
+ }`;
187
+ const result = await runSandbox(code, 30000);
188
+ setExportOutput(result.output);
189
+ setExportError(!result.success);
190
+ } catch (e) {
191
+ setExportOutput((e as Error).message);
192
+ setExportError(true);
193
+ } finally {
194
+ setExportRunning(false);
195
+ }
196
+ };
197
+
198
+ const handleLint = async () => {
199
+ setLintRunning(true);
200
+ setLintRows([]);
201
+ setLintRawOutput('');
202
+ setLintRan(false);
203
+ try {
204
+ const code = `
205
+ const { execSync } = require('child_process');
206
+ try {
207
+ const out = execSync('npx total-recall lint okf --json', { encoding: 'utf8', timeout: 30000 });
208
+ console.log(out);
209
+ } catch (e) {
210
+ // lint exits non-zero if there are errors — output is still valid
211
+ if (e.stdout) console.log(e.stdout);
212
+ else console.error(e.stderr || e.message);
213
+ }`;
214
+ const result = await runSandbox(code, 30000);
215
+ setLintRawOutput(result.output);
216
+
217
+ // Try to parse JSON lint output
218
+ try {
219
+ const parsed = JSON.parse(result.output);
220
+ const rows: LintRow[] = (Array.isArray(parsed) ? parsed : parsed.issues || []).map(
221
+ (item: { slug?: string; field?: string; severity?: string; message?: string }) => ({
222
+ slug: item.slug || '—',
223
+ field: item.field || '—',
224
+ severity: (['error', 'warning', 'info'].includes(item.severity || '') ? item.severity : 'info') as LintRow['severity'],
225
+ message: item.message || '',
226
+ }),
227
+ );
228
+ setLintRows(rows);
229
+ } catch {
230
+ // Non-JSON output — show raw
231
+ }
232
+ setLintRan(true);
233
+ } catch (e) {
234
+ setLintRawOutput((e as Error).message);
235
+ setLintRan(true);
236
+ } finally {
237
+ setLintRunning(false);
238
+ }
239
+ };
240
+
241
+ const lintErrors = lintRows.filter(r => r.severity === 'error').length;
242
+ const lintWarnings = lintRows.filter(r => r.severity === 'warning').length;
243
+
244
+ // ── Render ──
245
+
246
+ return (
247
+ <div className="page">
248
+ <div className="page-header">
249
+ <h1>📦 Open Knowledge Format</h1>
250
+ <p style={{ color: 'var(--text-secondary)', margin: '4px 0 0' }}>
251
+ Import, export, and lint OKF bundles for interoperable memory exchange.
252
+ </p>
253
+ </div>
254
+
255
+ {/* Tab Navigation */}
256
+ <div style={tabBarStyle}>
257
+ {(['import', 'export', 'lint'] as OkfTab[]).map(tab => (
258
+ <button
259
+ key={tab}
260
+ id={`okf-tab-${tab}`}
261
+ style={tabStyle(activeTab === tab)}
262
+ onClick={() => setActiveTab(tab)}
263
+ >
264
+ {tab === 'import' && '⬇ Import'}
265
+ {tab === 'export' && '⬆ Export'}
266
+ {tab === 'lint' && '🔍 Lint'}
267
+ </button>
268
+ ))}
269
+ </div>
270
+
271
+ {/* ── Import Tab ── */}
272
+ {activeTab === 'import' && (
273
+ <div style={cardStyle}>
274
+ <p style={{ color: 'var(--text-secondary)', fontSize: 14, marginBottom: 20 }}>
275
+ Import Open Knowledge Format bundles into your memory vault.
276
+ </p>
277
+
278
+ <div style={{ display: 'flex', flexDirection: 'column', gap: 16 }}>
279
+ <div style={fieldGroup}>
280
+ <label htmlFor="okf-import-path" style={labelStyle}>OKF Bundle Path</label>
281
+ <input
282
+ id="okf-import-path"
283
+ type="text"
284
+ placeholder="/path/to/okf-bundle"
285
+ value={importPath}
286
+ onChange={e => setImportPath(e.target.value)}
287
+ style={inputStyle}
288
+ />
289
+ </div>
290
+
291
+ <div style={formGrid}>
292
+ <div style={fieldGroup}>
293
+ <label htmlFor="okf-conflict-mode" style={labelStyle}>Conflict Resolution</label>
294
+ <select
295
+ id="okf-conflict-mode"
296
+ value={conflictMode}
297
+ onChange={e => setConflictMode(e.target.value as ConflictMode)}
298
+ style={selectStyle}
299
+ >
300
+ <option value="skip">Skip</option>
301
+ <option value="warn">Warn (default)</option>
302
+ <option value="overwrite">Overwrite</option>
303
+ </select>
304
+ </div>
305
+
306
+ <div style={fieldGroup}>
307
+ <label htmlFor="okf-category-override" style={labelStyle}>Category Override</label>
308
+ <select
309
+ id="okf-category-override"
310
+ value={categoryOverride}
311
+ onChange={e => setCategoryOverride(e.target.value as CategoryOverride)}
312
+ style={selectStyle}
313
+ >
314
+ <option value="auto">Auto (default)</option>
315
+ <option value="invariants">Invariants</option>
316
+ <option value="preferences">Preferences</option>
317
+ <option value="facts">Facts</option>
318
+ <option value="concepts">Concepts</option>
319
+ <option value="patterns">Patterns</option>
320
+ <option value="decisions">Decisions</option>
321
+ <option value="lore">Lore</option>
322
+ </select>
323
+ </div>
324
+ </div>
325
+
326
+ <div style={{ ...fieldGroup, maxWidth: 200 }}>
327
+ <label htmlFor="okf-importance-override" style={labelStyle}>Importance Override (1–5)</label>
328
+ <input
329
+ id="okf-importance-override"
330
+ type="number"
331
+ min={1}
332
+ max={5}
333
+ placeholder="Optional"
334
+ value={importanceOverride}
335
+ onChange={e => setImportanceOverride(e.target.value)}
336
+ style={inputStyle}
337
+ />
338
+ </div>
339
+
340
+ <button
341
+ id="okf-import-run"
342
+ className="btn btn-primary"
343
+ onClick={handleImport}
344
+ disabled={importRunning || !importPath.trim()}
345
+ style={{ alignSelf: 'flex-start', marginTop: 4 }}
346
+ >
347
+ {importRunning ? '⏳ Importing…' : '⬇ Import Bundle'}
348
+ </button>
349
+ </div>
350
+
351
+ {importOutput && (
352
+ <div id="okf-import-output" style={{ ...outputStyle, borderColor: importError ? 'var(--error)' : 'var(--border)' }}>
353
+ {importOutput}
354
+ </div>
355
+ )}
356
+ </div>
357
+ )}
358
+
359
+ {/* ── Export Tab ── */}
360
+ {activeTab === 'export' && (
361
+ <div style={cardStyle}>
362
+ <p style={{ color: 'var(--text-secondary)', fontSize: 14, marginBottom: 20 }}>
363
+ Export your memory vault as an Open Knowledge Format bundle.
364
+ </p>
365
+
366
+ <div style={{ display: 'flex', flexDirection: 'column', gap: 16 }}>
367
+ <div style={fieldGroup}>
368
+ <label htmlFor="okf-export-path" style={labelStyle}>Output Path</label>
369
+ <input
370
+ id="okf-export-path"
371
+ type="text"
372
+ placeholder="/path/to/export"
373
+ value={exportPath}
374
+ onChange={e => setExportPath(e.target.value)}
375
+ style={inputStyle}
376
+ />
377
+ </div>
378
+
379
+ <div style={formGrid}>
380
+ <div style={fieldGroup}>
381
+ <label htmlFor="okf-export-format" style={labelStyle}>Format</label>
382
+ <select
383
+ id="okf-export-format"
384
+ value={exportFormat}
385
+ onChange={e => setExportFormat(e.target.value as ExportFormat)}
386
+ style={selectStyle}
387
+ >
388
+ <option value="directory">Directory</option>
389
+ <option value="tar.gz">tar.gz</option>
390
+ </select>
391
+ </div>
392
+
393
+ <div style={fieldGroup}>
394
+ <label htmlFor="okf-export-scope" style={labelStyle}>Scope</label>
395
+ <select
396
+ id="okf-export-scope"
397
+ value={exportScope}
398
+ onChange={e => setExportScope(e.target.value as ExportScope)}
399
+ style={selectStyle}
400
+ >
401
+ <option value="all">All</option>
402
+ <option value="global">Global</option>
403
+ <option value="project">Project</option>
404
+ </select>
405
+ </div>
406
+ </div>
407
+
408
+ <label
409
+ htmlFor="okf-strip-meta"
410
+ style={{ display: 'flex', alignItems: 'center', gap: 8, cursor: 'pointer', fontSize: 14, color: 'var(--text-secondary)' }}
411
+ >
412
+ <input
413
+ id="okf-strip-meta"
414
+ type="checkbox"
415
+ checked={stripMeta}
416
+ onChange={e => setStripMeta(e.target.checked)}
417
+ style={{ accentColor: 'var(--accent)' }}
418
+ />
419
+ Strip SSSS metadata
420
+ </label>
421
+
422
+ <button
423
+ id="okf-export-run"
424
+ className="btn btn-primary"
425
+ onClick={handleExport}
426
+ disabled={exportRunning || !exportPath.trim()}
427
+ style={{ alignSelf: 'flex-start', marginTop: 4 }}
428
+ >
429
+ {exportRunning ? '⏳ Exporting…' : '⬆ Export Vault'}
430
+ </button>
431
+ </div>
432
+
433
+ {exportOutput && (
434
+ <div id="okf-export-output" style={{ ...outputStyle, borderColor: exportError ? 'var(--error)' : 'var(--border)' }}>
435
+ {exportOutput}
436
+ </div>
437
+ )}
438
+ </div>
439
+ )}
440
+
441
+ {/* ── Lint Tab ── */}
442
+ {activeTab === 'lint' && (
443
+ <div style={cardStyle}>
444
+ <p style={{ color: 'var(--text-secondary)', fontSize: 14, marginBottom: 20 }}>
445
+ Run OKF compliance checks across all memory nodes.
446
+ </p>
447
+
448
+ <button
449
+ id="okf-lint-run"
450
+ className="btn btn-primary"
451
+ onClick={handleLint}
452
+ disabled={lintRunning}
453
+ style={{ marginBottom: 20 }}
454
+ >
455
+ {lintRunning ? '⏳ Checking…' : '🔍 Run Compliance Check'}
456
+ </button>
457
+
458
+ {lintRan && lintRows.length > 0 && (
459
+ <>
460
+ <div style={{ overflowX: 'auto' }}>
461
+ <table style={{ width: '100%', borderCollapse: 'collapse', fontSize: 13 }}>
462
+ <thead>
463
+ <tr style={{ borderBottom: '1px solid var(--border)' }}>
464
+ <th style={{ textAlign: 'left', padding: '10px 12px', color: 'var(--text-secondary)', fontWeight: 500 }}>Slug</th>
465
+ <th style={{ textAlign: 'left', padding: '10px 12px', color: 'var(--text-secondary)', fontWeight: 500 }}>Field</th>
466
+ <th style={{ textAlign: 'left', padding: '10px 12px', color: 'var(--text-secondary)', fontWeight: 500 }}>Severity</th>
467
+ <th style={{ textAlign: 'left', padding: '10px 12px', color: 'var(--text-secondary)', fontWeight: 500 }}>Message</th>
468
+ </tr>
469
+ </thead>
470
+ <tbody>
471
+ {lintRows.map((row, i) => (
472
+ <tr key={i} style={{ borderBottom: '1px solid var(--border)' }}>
473
+ <td style={{ padding: '10px 12px', color: 'var(--text-primary)', fontFamily: 'var(--font-mono, monospace)' }}>{row.slug}</td>
474
+ <td style={{ padding: '10px 12px', color: 'var(--text-secondary)' }}>{row.field}</td>
475
+ <td style={{ padding: '10px 12px' }}>
476
+ <span
477
+ className="badge"
478
+ style={{
479
+ background: `${severityColor[row.severity]}22`,
480
+ color: severityColor[row.severity],
481
+ padding: '3px 10px',
482
+ borderRadius: 4,
483
+ fontSize: 12,
484
+ fontWeight: 600,
485
+ textTransform: 'uppercase',
486
+ }}
487
+ >
488
+ {row.severity}
489
+ </span>
490
+ </td>
491
+ <td style={{ padding: '10px 12px', color: 'var(--text-secondary)' }}>{row.message}</td>
492
+ </tr>
493
+ ))}
494
+ </tbody>
495
+ <tfoot>
496
+ <tr style={{ borderTop: '2px solid var(--border)' }}>
497
+ <td colSpan={4} style={{ padding: '12px', color: 'var(--text-secondary)', fontSize: 13 }}>
498
+ <strong style={{ color: 'var(--text-primary)' }}>Total: {lintRows.length}</strong>
499
+ {' · '}
500
+ <span style={{ color: 'var(--error)' }}>Errors: {lintErrors}</span>
501
+ {' · '}
502
+ <span style={{ color: 'var(--warning)' }}>Warnings: {lintWarnings}</span>
503
+ </td>
504
+ </tr>
505
+ </tfoot>
506
+ </table>
507
+ </div>
508
+ </>
509
+ )}
510
+
511
+ {lintRan && lintRows.length === 0 && !lintRawOutput && (
512
+ <div
513
+ id="okf-lint-pass"
514
+ style={{
515
+ textAlign: 'center',
516
+ padding: '40px 20px',
517
+ color: 'var(--success)',
518
+ fontSize: 15,
519
+ fontWeight: 500,
520
+ }}
521
+ >
522
+ ✅ All nodes pass OKF compliance
523
+ </div>
524
+ )}
525
+
526
+ {lintRan && lintRows.length === 0 && lintRawOutput && (
527
+ <div id="okf-lint-output" style={outputStyle}>
528
+ {lintRawOutput}
529
+ </div>
530
+ )}
531
+ </div>
532
+ )}
533
+ </div>
534
+ );
535
+ }