total-recall-brain 3.18.2 → 3.19.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.
Files changed (83) hide show
  1. package/frontend/dist/assets/{index-D5xRw5uq.js → index-B6AfuKv9.js} +126 -134
  2. package/frontend/dist/index.html +1 -1
  3. package/frontend/src/api/config.ts +16 -2
  4. package/frontend/src/api/keys.ts +111 -0
  5. package/frontend/src/api/notifications.ts +54 -6
  6. package/frontend/src/api/update.ts +41 -5
  7. package/frontend/src/components/BrainSelector.spec.tsx +71 -47
  8. package/frontend/src/components/BrainSelector.tsx +112 -35
  9. package/frontend/src/components/MobilePairing.spec.tsx +44 -14
  10. package/frontend/src/components/MobilePairing.tsx +229 -22
  11. package/frontend/src/pages/HealthPage.tsx +12 -16
  12. package/frontend/src/pages/NotificationsPage.tsx +6 -3
  13. package/frontend/src/pages/RulesPage.spec.tsx +46 -15
  14. package/frontend/src/pages/RulesPage.tsx +41 -9
  15. package/frontend/src/pages/SecretsPage.tsx +316 -3
  16. package/frontend/src/pages/SettingsPage.spec.tsx +30 -6
  17. package/frontend/src/pages/SettingsPage.tsx +228 -86
  18. package/package.json +1 -1
  19. package/src/cli/agent-dir.mjs +17 -2
  20. package/src/cli/chat.mjs +1 -1
  21. package/src/cli/daemon.mjs +2 -1
  22. package/src/cli/dream.mjs +14 -10
  23. package/src/cli/forget.mjs +14 -2
  24. package/src/cli/friction.mjs +1 -1
  25. package/src/cli/import-rules.mjs +8 -3
  26. package/src/cli/ingest.mjs +3 -2
  27. package/src/cli/lint.mjs +9 -6
  28. package/src/cli/rebuild.mjs +9 -4
  29. package/src/cli/remember.mjs +21 -15
  30. package/src/cli/secret.mjs +184 -4
  31. package/src/cli/share.mjs +5 -3
  32. package/src/cli/skill-projection.mjs +21 -2
  33. package/src/cli/snapshot.mjs +10 -8
  34. package/src/cli/update.mjs +2 -1
  35. package/src/core/clarity-rewriter.mjs +31 -12
  36. package/src/core/config.mjs +10 -4
  37. package/src/core/daemon-loop.mjs +33 -6
  38. package/src/core/embeddings.mjs +51 -3
  39. package/src/core/memory-title.mjs +70 -0
  40. package/src/core/notifications.mjs +204 -34
  41. package/src/core/package-auto-update.mjs +99 -3
  42. package/src/core/pairing.mjs +168 -0
  43. package/src/core/provider-account-sync.mjs +1145 -0
  44. package/src/core/provider-catalog.mjs +11 -4
  45. package/src/core/research.mjs +98 -24
  46. package/src/core/runtime.mjs +11 -1
  47. package/src/core/scheduler.mjs +31 -9
  48. package/src/core/schema.mjs +23 -0
  49. package/src/core/secret-integration-research.mjs +325 -0
  50. package/src/core/secrets-store.mjs +284 -14
  51. package/src/core/secrets-sync.mjs +1 -1
  52. package/src/core/skills-registry.mjs +12 -0
  53. package/src/core/source-adapters.mjs +247 -22
  54. package/src/core/surface.mjs +4 -4
  55. package/src/core/task-executors.mjs +161 -14
  56. package/src/core/throttled-fetch.mjs +5 -0
  57. package/src/core/vault.mjs +7 -1
  58. package/src/core/webhook-handlers.mjs +123 -14
  59. package/src/server/auth.mjs +6 -0
  60. package/src/server/index.mjs +56 -0
  61. package/src/server/rest.mjs +2 -0
  62. package/src/server/route-manifest.json +40 -0
  63. package/src/server/routes/_shared.mjs +33 -1
  64. package/src/server/routes/config.mjs +64 -23
  65. package/src/server/routes/context.mjs +14 -9
  66. package/src/server/routes/dashboard.mjs +39 -18
  67. package/src/server/routes/docs.mjs +27 -13
  68. package/src/server/routes/dream.mjs +10 -11
  69. package/src/server/routes/export.mjs +43 -12
  70. package/src/server/routes/field.mjs +11 -9
  71. package/src/server/routes/graph.mjs +31 -13
  72. package/src/server/routes/import.mjs +35 -16
  73. package/src/server/routes/instructions.mjs +41 -5
  74. package/src/server/routes/memory.mjs +61 -21
  75. package/src/server/routes/mesh.mjs +2 -1
  76. package/src/server/routes/notifications.mjs +103 -0
  77. package/src/server/routes/rules.mjs +75 -20
  78. package/src/server/routes/secrets.mjs +110 -4
  79. package/src/server/routes/share.mjs +2 -1
  80. package/src/server/routes/ssss.mjs +11 -3
  81. package/src/server/routes/system.mjs +20 -1
  82. package/src/server/routes/update.mjs +33 -7
  83. package/src/server/routes/vault.mjs +53 -28
@@ -16,8 +16,9 @@
16
16
  import path from 'node:path';
17
17
  import { resolveAgentDir, resolveBrainDir } from './agent-dir.mjs';
18
18
 
19
- const AGENT_DIR = resolveAgentDir();
20
- const BRAIN_DIR = resolveBrainDir();
19
+ // Session ingest lands in the global brain by default.
20
+ const AGENT_DIR = resolveAgentDir('global');
21
+ const BRAIN_DIR = resolveBrainDir('global');
21
22
 
22
23
  function parseArgs(args) {
23
24
  const opts = { sources: null, watch: false, help: false };
package/src/cli/lint.mjs CHANGED
@@ -17,11 +17,9 @@
17
17
  import fs from 'node:fs';
18
18
  import path from 'node:path';
19
19
  import { fileURLToPath } from 'node:url';
20
- import { resolveAgentDir, resolveBrainDir } from './agent-dir.mjs';
20
+ import { resolveBrainDir, parseLayerFlag } from './agent-dir.mjs';
21
21
 
22
22
  const __dirname = path.dirname(fileURLToPath(import.meta.url));
23
- const AGENT_DIR = resolveAgentDir();
24
- const BRAIN_DIR = resolveBrainDir();
25
23
 
26
24
  function parseArgs(args) {
27
25
  const opts = { vault: null, strict: false, json: false, help: false, okf: false };
@@ -47,7 +45,9 @@ function printHelp() {
47
45
  Usage: total-recall lint [options]
48
46
 
49
47
  Options:
50
- --vault <path> Override vault directory (default: ~/.agent/memory-vault)
48
+ --vault <path> Override vault directory (default: global brain vault)
49
+ --global Lint the global vault (default)
50
+ --project Lint the project vault for the current repo
51
51
  --strict Treat warnings as errors (exit 1 on any issue)
52
52
  --okf Verify OKF v0.1 draft metadata compliance
53
53
  --json Output results as JSONL
@@ -60,10 +60,13 @@ function printHelp() {
60
60
  }
61
61
 
62
62
  export default async function lint(args) {
63
- const opts = parseArgs(args);
63
+ const { layer, remainingArgs } = parseLayerFlag(args);
64
+ // Default layer is global when no --project/--global (matches help text)
65
+ const effectiveLayer = layer === 'auto' ? 'global' : layer;
66
+ const opts = parseArgs(remainingArgs);
64
67
  if (opts.help) { printHelp(); return; }
65
68
 
66
- const vaultDir = opts.vault || path.join(BRAIN_DIR, 'memory-vault');
69
+ const vaultDir = opts.vault || path.join(resolveBrainDir(effectiveLayer), 'memory-vault');
67
70
 
68
71
  if (opts.okf) {
69
72
  const { lintOkfCompliance } = await import('../core/okf-adapter.mjs');
@@ -2,7 +2,7 @@ import fs from 'fs';
2
2
  import path from 'path';
3
3
  import { compileSurface } from '../core/surface.mjs';
4
4
  import { detectIndexDrift } from '../core/drift-detector.mjs';
5
- import { resolveAgentDir, resolveBrainDir } from './agent-dir.mjs';
5
+ import { resolveAgentDir, resolveBrainDir, parseLayerFlag } from './agent-dir.mjs';
6
6
  import { logger } from '../core/logger.mjs';
7
7
 
8
8
  /**
@@ -14,8 +14,9 @@ import { logger } from '../core/logger.mjs';
14
14
  */
15
15
 
16
16
  export async function runRebuild(options = {}) {
17
- const agentDir = resolveAgentDir();
18
- const brainDir = resolveBrainDir();
17
+ const layer = options.layer || 'auto';
18
+ const agentDir = resolveAgentDir(layer);
19
+ const brainDir = resolveBrainDir(layer);
19
20
  const vaultDir = path.join(brainDir, 'memory-vault');
20
21
  const skillsDir = path.join(agentDir, 'skills');
21
22
  const derivedDir = path.join(brainDir, 'memory-derived');
@@ -82,7 +83,11 @@ export async function runRebuild(options = {}) {
82
83
  }
83
84
 
84
85
  export default async function cli(args) {
85
- const options = { check: args.includes('--check') };
86
+ const { layer, remainingArgs } = parseLayerFlag(args);
87
+ const options = {
88
+ check: remainingArgs.includes('--check'),
89
+ layer,
90
+ };
86
91
  const code = await runRebuild(options);
87
92
  process.exit(code);
88
93
  }
@@ -5,6 +5,7 @@ import matter from 'gray-matter';
5
5
  import { resolveAgentDir, resolveBrainDir, parseLayerFlag, getBothBrains, defaultLayerForCategory } from './agent-dir.mjs';
6
6
  import { compileSurface } from '../core/surface.mjs';
7
7
  import { writeNodeValidatedAsync } from '../core/validated-write.mjs';
8
+ import { defaultTitleFromBody } from '../core/memory-title.mjs';
8
9
 
9
10
  function printHelp() {
10
11
  console.log(`
@@ -243,8 +244,12 @@ export default async function remember(args) {
243
244
  // Create individual SSSS v2 node in the vault
244
245
  const contentHash = crypto.createHash('md5').update(bodyContent).digest('hex').slice(0, 8);
245
246
  const finalSlug = slug || `${category}-${contentHash}`;
246
- const truncatedContent = bodyContent.slice(0, 50) + (bodyContent.length > 50 ? '...' : '');
247
- const finalTitle = title || `Self-captured memory: ${truncatedContent.replace(/\n/g, ' ')}`;
247
+ // Provenance lives in source.type + tags — not a title prefix
248
+ const finalTitle = title || defaultTitleFromBody(bodyContent);
249
+ const finalTags = Array.isArray(tags) ? [...tags] : [];
250
+ if (!finalTags.includes('self-captured') && !title) {
251
+ finalTags.push('self-captured');
252
+ }
248
253
 
249
254
  const now = new Date().toISOString();
250
255
 
@@ -264,12 +269,13 @@ export default async function remember(args) {
264
269
  source: {
265
270
  type: 'remember-cli',
266
271
  session_id: process.env.TR_SESSION_ID || 'remember-session',
267
- evidence_count: 1
272
+ evidence_count: 1,
273
+ capture: 'self',
268
274
  },
269
275
  supersedes: [],
270
276
  superseded_by: null,
271
277
  contradicts: [],
272
- tags,
278
+ tags: finalTags,
273
279
  related,
274
280
  routes_to_skills: [],
275
281
  sentiment_polarity: type.toLowerCase() === 'preference' ? 'preference' : 'descriptive',
@@ -309,8 +315,9 @@ export default async function remember(args) {
309
315
  const oldTitle = (data.title || '').trim().toLowerCase();
310
316
  const newTitle = finalTitle.trim().toLowerCase();
311
317
 
312
- const words1 = new Set(bodyContent.toLowerCase().match(/\\b\\w+\\b/g) || []);
313
- const words2 = new Set((oldContent || '').toLowerCase().match(/\\b\\w+\\b/g) || []);
318
+ // Word tokens for Jaccard similarity (must be real \b/\w, not double-escaped literals)
319
+ const words1 = new Set(bodyContent.toLowerCase().match(/\b\w+\b/g) || []);
320
+ const words2 = new Set((oldContent || '').toLowerCase().match(/\b\w+\b/g) || []);
314
321
  const intersection = new Set([...words1].filter(x => words2.has(x)));
315
322
  const union = new Set([...words1, ...words2]);
316
323
  const similarity = union.size === 0 ? 0 : intersection.size / union.size;
@@ -325,14 +332,10 @@ export default async function remember(args) {
325
332
  data.superseded_by = finalSlug;
326
333
  data.updated = now;
327
334
  node.supersedes = [...(node.supersedes || []), data.slug];
328
-
329
- const lines = ['---'];
330
- for (const key of Object.keys(data)) {
331
- lines.push(`${key}: ${JSON.stringify(data[key])}`);
332
- }
333
- lines.push('---');
334
- lines.push(oldContent);
335
- fs.writeFileSync(p, lines.join('\n'));
335
+
336
+ // Use gray-matter so nested objects/arrays stay valid YAML
337
+ const bodyOut = (oldContent || '').trim();
338
+ fs.writeFileSync(p, matter.stringify(bodyOut ? `${bodyOut}\n` : '', data), 'utf8');
336
339
  }
337
340
  }
338
341
  }
@@ -360,7 +363,10 @@ export default async function remember(args) {
360
363
  console.log(' ⏳ Recompiling active memory surfaces and indexes in the background...');
361
364
  try {
362
365
  const { spawn } = await import('node:child_process');
363
- const child = spawn(process.argv[0], [process.argv[1], 'compile'], {
366
+ const compileArgs = [process.argv[1], 'compile'];
367
+ if (layer === 'project') compileArgs.push('--project');
368
+ else if (layer === 'global') compileArgs.push('--global');
369
+ const child = spawn(process.argv[0], compileArgs, {
364
370
  detached: true,
365
371
  stdio: 'ignore'
366
372
  });
@@ -28,6 +28,7 @@ import {
28
28
  getSecretsCatalog,
29
29
  updateSecretMeta,
30
30
  listRotationDue,
31
+ getSharedValueHealth,
31
32
  } from '../core/secrets-store.mjs';
32
33
  import {
33
34
  scanEnvSources,
@@ -79,9 +80,22 @@ function printHelp() {
79
80
  --dry-run Show keys that would be written
80
81
  import-env One-time migrate: scan local .env → store (not the steady-state path)
81
82
  --all / --file / --overwrite / --dry-run
82
- catalog Full catalog: keys, providers, usage, rotation, cost
83
+ catalog Full catalog: keys, providers, usage, rotation, cost, tracking
83
84
  providers List provider registry (docs/schema/tiers)
85
+ account-sync [key] Live-probe vendor account/usage/subscription APIs
86
+ --all Sync every set secret (default if no key)
87
+ --strict partial (no $ API) → ERROR (default)
88
+ --no-strict Allow partial status
89
+ --use-ai AI-classify unknown keys (slow)
90
+ --exempt Mark key tracking_exempt (with optional --monthly-cost)
91
+ tracking-health Fail (exit 1) if any set secret is untracked
92
+ shared List credential values reused across keys/apps (exit 1 if any)
93
+ --shared-ok Mark key as intentional share (waive) — use with meta
84
94
  meta <key> Update metadata (no value change)
95
+ --tracking-exempt Waive live account API requirement
96
+ --no-tracking-exempt
97
+ --shared-ok Allow this key to share its value with other secret names
98
+ --no-shared-ok
85
99
  --repo <name> Bind to exactly ONE product repo (not multi)
86
100
  --repos <name> Same as --repo (single name only; multi rejected)
87
101
  --tier <id> Subscription tier
@@ -167,6 +181,11 @@ function parseArgs(args) {
167
181
  enqueue: false,
168
182
  bytes: 32,
169
183
  format: 'base64url',
184
+ strict: true,
185
+ use_ai: false,
186
+ exempt: false,
187
+ tracking_exempt: null,
188
+ shared_ok: null,
170
189
  };
171
190
  if (!args.length || args[0] === '--help' || args[0] === '-h') {
172
191
  out.help = true;
@@ -175,7 +194,9 @@ function parseArgs(args) {
175
194
  out.command = args[0];
176
195
  let i = 1;
177
196
  if (
178
- ['set', 'get', 'rotate', 'delete', 'meta', 'rotate-browser', 'rm', 'generate'].includes(out.command) &&
197
+ ['set', 'get', 'rotate', 'delete', 'meta', 'rotate-browser', 'rm', 'generate', 'account-sync'].includes(
198
+ out.command,
199
+ ) &&
179
200
  args[i] &&
180
201
  !args[i].startsWith('--')
181
202
  ) {
@@ -301,6 +322,30 @@ function parseArgs(args) {
301
322
  case '--format':
302
323
  out.format = args[++i] || 'base64url';
303
324
  break;
325
+ case '--strict':
326
+ out.strict = true;
327
+ break;
328
+ case '--no-strict':
329
+ out.strict = false;
330
+ break;
331
+ case '--use-ai':
332
+ out.use_ai = true;
333
+ break;
334
+ case '--exempt':
335
+ out.exempt = true;
336
+ break;
337
+ case '--tracking-exempt':
338
+ out.tracking_exempt = true;
339
+ break;
340
+ case '--no-tracking-exempt':
341
+ out.tracking_exempt = false;
342
+ break;
343
+ case '--shared-ok':
344
+ out.shared_ok = true;
345
+ break;
346
+ case '--no-shared-ok':
347
+ out.shared_ok = false;
348
+ break;
304
349
  case '--help':
305
350
  case '-h':
306
351
  out.help = true;
@@ -457,6 +502,121 @@ export default async function secretCli(argv) {
457
502
  return;
458
503
  }
459
504
 
505
+ if (opts.command === 'account-sync' || opts.command === 'sync-accounts') {
506
+ const {
507
+ syncSecretAccount,
508
+ syncAllSecretAccounts,
509
+ getTrackingHealth,
510
+ } = await import('../core/provider-account-sync.mjs');
511
+ const syncOpts = {
512
+ strict: opts.strict !== false,
513
+ use_ai: !!opts.use_ai,
514
+ force_exempt: !!opts.exempt,
515
+ };
516
+ if (opts.key) {
517
+ if (opts.exempt) {
518
+ await updateSecretMeta(
519
+ brainDir,
520
+ opts.key,
521
+ {
522
+ tracking_exempt: true,
523
+ monthly_cost_usd: opts.monthly_cost ?? undefined,
524
+ },
525
+ { actor: 'cli' },
526
+ );
527
+ }
528
+ const r = await syncSecretAccount(brainDir, opts.key, syncOpts);
529
+ const icon =
530
+ r.tracking_status === 'ok' ? '✅' : r.tracking_status === 'exempt' ? '⏭' : '❌';
531
+ console.log(`\n ${icon} ${r.key} status=${r.tracking_status} provider=${r.provider || '-'}`);
532
+ if (r.probe) console.log(` probe: ${r.probe}`);
533
+ if (r.error) console.log(` error: ${r.error}`);
534
+ if (r.account) console.log(` account: ${JSON.stringify(r.account)}`);
535
+ if (r.usage) console.log(` usage: ${JSON.stringify(r.usage)}`);
536
+ if (r.subscription) console.log(` subscription: ${JSON.stringify(r.subscription)}`);
537
+ console.log('');
538
+ if (r.tracking_status === 'error') process.exit(1);
539
+ return;
540
+ }
541
+ console.log('\n 🔄 Live provider account/usage sync (all set secrets)…\n');
542
+ const report = await syncAllSecretAccounts(brainDir, syncOpts);
543
+ console.log(` total=${report.total} ok/exempt=${report.ok} errors=${report.errors}`);
544
+ console.log(` ${report.healthy ? '✅' : '❌'} ${report.message}`);
545
+ if (report.error_keys?.length) {
546
+ console.log('\n Untracked / failed:');
547
+ for (const e of report.error_keys.slice(0, 50)) {
548
+ console.log(` • ${e.key}: ${e.error}`);
549
+ }
550
+ if (report.error_keys.length > 50) {
551
+ console.log(` … and ${report.error_keys.length - 50} more`);
552
+ }
553
+ }
554
+ const health = await getTrackingHealth(brainDir);
555
+ console.log(
556
+ `\n tracking-health: healthy=${health.healthy} ok=${health.ok} exempt=${health.exempt} errors=${health.errors}\n`,
557
+ );
558
+ if (!report.healthy) process.exit(1);
559
+ return;
560
+ }
561
+
562
+ if (opts.command === 'tracking-health' || opts.command === 'tracking') {
563
+ const { getTrackingHealth } = await import('../core/provider-account-sync.mjs');
564
+ const health = await getTrackingHealth(brainDir);
565
+ console.log(`\n ${health.healthy ? '✅' : '❌'} ${health.message}`);
566
+ console.log(` ok=${health.ok} exempt=${health.exempt} errors=${health.errors}`);
567
+ if (health.error_keys?.length) {
568
+ console.log('');
569
+ for (const e of health.error_keys) {
570
+ console.log(` • ${e.key.padEnd(28)} ${(e.provider || '-').padEnd(14)} ${e.error}`);
571
+ }
572
+ }
573
+ const shared = await getSharedValueHealth(brainDir);
574
+ console.log(`\n ${shared.healthy ? '✅' : '❌'} ${shared.message}`);
575
+ if (shared.errors?.length) {
576
+ for (const e of shared.errors) {
577
+ console.log(` • fp=${e.fingerprint} apps=[${e.apps.join(', ')}]`);
578
+ console.log(` keys: ${e.keys.join(', ')}`);
579
+ }
580
+ }
581
+ console.log('');
582
+ if (!health.healthy || !shared.healthy) process.exit(1);
583
+ return;
584
+ }
585
+
586
+ if (opts.command === 'shared' || opts.command === 'shared-values' || opts.command === 'duplicates') {
587
+ const shared = await getSharedValueHealth(brainDir);
588
+ console.log(`\n ${shared.healthy ? '✅' : '❌'} ${shared.message}`);
589
+ console.log(
590
+ ` groups=${shared.groups.length} error_groups=${shared.error_groups} multi_app=${shared.multi_app_groups} keys_in_error=${shared.shared_keys}`,
591
+ );
592
+ if (shared.groups.length) {
593
+ console.log('');
594
+ for (const g of shared.groups) {
595
+ const icon = g.severity === 'error' ? '❌' : '⏭';
596
+ console.log(
597
+ ` ${icon} fingerprint ${g.fingerprint} ×${g.count} apps=[${g.apps.join(', ')}]`,
598
+ );
599
+ for (const m of g.members) {
600
+ console.log(
601
+ ` • ${m.key.padEnd(36)} app=${String(m.app).padEnd(16)} provider=${m.provider || '-'} ${m.masked || ''}`,
602
+ );
603
+ }
604
+ if (g.error) console.log(` → ${g.error}`);
605
+ console.log(
606
+ ` fix: rotate each key to a unique value, then: secret rotate <key> <new> --export-env`,
607
+ );
608
+ console.log(
609
+ ` waive (rare): secret meta <key> --shared-ok # only if intentional mirror`,
610
+ );
611
+ console.log('');
612
+ }
613
+ } else {
614
+ console.log('\n No shared credential values detected.\n');
615
+ }
616
+ if (!shared.healthy) process.exit(1);
617
+ return;
618
+ }
619
+
460
620
  if (opts.command === 'meta') {
461
621
  if (!opts.key) {
462
622
  console.error('Usage: total-recall secret meta <key> [--repo ...] [--tier ...]');
@@ -475,6 +635,11 @@ export default async function secretCli(argv) {
475
635
  if (opts.project) patch.project_path = opts.project;
476
636
  if (opts.provider) patch.provider = opts.provider;
477
637
  if (opts.scope) patch.scope = opts.scope;
638
+ if (opts.tracking_exempt != null) {
639
+ patch.tracking_exempt = opts.tracking_exempt;
640
+ if (opts.tracking_exempt) patch.tracking_status = 'exempt';
641
+ }
642
+ if (opts.shared_ok != null) patch.shared_value_ok = opts.shared_ok;
478
643
  // Fill docs from catalog if provider set and no docs
479
644
  if (patch.provider && !patch.api_docs_url) {
480
645
  const p = getProvider(patch.provider);
@@ -538,10 +703,22 @@ export default async function secretCli(argv) {
538
703
  console.log(`\n Secrets metadata — ${resolveSecretsPath(brainDir)}\n`);
539
704
  for (const r of rows) {
540
705
  const repos = r.repos?.length ? r.repos.join(',') : '-';
706
+ const tr = r.tracking_status || 'never';
707
+ const trIcon = tr === 'ok' ? '✅' : tr === 'exempt' ? '⏭' : '❌';
708
+ const share = r.shared_value ? ' 🔗SHARED' : '';
541
709
  console.log(
542
- ` • ${r.key.padEnd(28)} len=${String(r.length).padStart(4)} ${(r.provider || '-').padEnd(12)} tier=${r.subscription_tier || '-'} $${r.monthly_cost_usd ?? '-'} repos=${repos}`,
710
+ ` • ${r.key.padEnd(28)} len=${String(r.length).padStart(4)} ${(r.provider || '-').padEnd(12)} ${trIcon}${tr.padEnd(7)} tier=${r.subscription_tier || '-'} $${r.monthly_cost_usd ?? '-'} repos=${repos}${share}`,
543
711
  );
544
712
  if (r.api_docs_url) console.log(` docs: ${r.api_docs_url}`);
713
+ if (r.tracking_error && tr === 'error') console.log(` tracking: ${r.tracking_error}`);
714
+ if (r.shared_value_error) {
715
+ console.log(` shared: ${r.shared_value_error}`);
716
+ if (r.shared_with?.length) {
717
+ console.log(
718
+ ` same value as: ${r.shared_with.map((s) => `${s.key}(${s.app})`).join(', ')}`,
719
+ );
720
+ }
721
+ }
545
722
  }
546
723
  console.log('');
547
724
  return;
@@ -808,9 +985,12 @@ export default async function secretCli(argv) {
808
985
 
809
986
  if (opts.command === 'check-surfaces') {
810
987
  const texts = [];
988
+ const agentRoot = path.dirname(path.dirname(brainDir)); // …/skills/total-recall → …/.agent (or similar)
811
989
  const candidates = [
812
- path.join(brainDir, 'INSTRUCTIONS.md'),
813
990
  path.join(process.cwd(), 'INSTRUCTIONS.md'),
991
+ path.join(process.cwd(), 'AGENTS.md'),
992
+ path.join(agentRoot, 'INSTRUCTIONS.md'),
993
+ path.join(brainDir, 'INSTRUCTIONS.md'), // legacy
814
994
  path.join(brainDir, 'openwiki'),
815
995
  ];
816
996
  for (const c of candidates) {
package/src/cli/share.mjs CHANGED
@@ -1,5 +1,5 @@
1
1
  import crypto from 'node:crypto';
2
- import { parseLayerFlag, resolveBrainDir, getBothBrains, defaultLayerForCategory } from './agent-dir.mjs';
2
+ import { parseLayerFlag, resolveAgentDir, resolveBrainDir, getBothBrains, defaultLayerForCategory } from './agent-dir.mjs';
3
3
  import { createMemoryNode } from '../core/vault.mjs';
4
4
  import { writeNodeValidatedAsync } from '../core/validated-write.mjs';
5
5
  import { addToQueue } from '../core/research-queue.mjs';
@@ -128,10 +128,12 @@ export default async function share(args) {
128
128
  }
129
129
 
130
130
  const resolvedBrainDir = resolveBrainDir(layer);
131
+ const resolvedAgentDir = resolveAgentDir(layer);
131
132
  const vaultDir = path.join(resolvedBrainDir, 'memory-vault');
132
- const skillsDir = path.join(resolvedBrainDir, '..', '..');
133
+ // skillsDir must be <agent>/.agent/skills (or ~/.agent/skills), not the agent root
134
+ const skillsDir = path.join(resolvedAgentDir, 'skills');
133
135
  const derivedDir = path.join(resolvedBrainDir, 'memory-derived');
134
- const instructionsFile = path.join(skillsDir, '..', 'INSTRUCTIONS.md');
136
+ const instructionsFile = path.join(resolvedAgentDir, 'INSTRUCTIONS.md');
135
137
 
136
138
  const slugBase = category || 'facts';
137
139
  const slug = `${slugBase}-${crypto.randomBytes(4).toString('hex')}`;
@@ -70,6 +70,22 @@ function pathExists(filePath) {
70
70
  }
71
71
  }
72
72
 
73
+ // Marker written by tools like repo-expert-generate.mjs into any SKILL.md
74
+ // they produce. A real (non-symlink) directory carrying this marker is
75
+ // leftover tool output, not a user's hand-authored skill — it is always safe
76
+ // to heal back onto the canonical source, even without --force.
77
+ const AUTO_GENERATED_MARKER = '**Auto-generated** by `npx total-recall';
78
+
79
+ function isStaleGeneratedDir(entryPath) {
80
+ try {
81
+ if (!fs.statSync(entryPath).isDirectory()) return false;
82
+ const content = fs.readFileSync(path.join(entryPath, 'SKILL.md'), 'utf8');
83
+ return content.includes(AUTO_GENERATED_MARKER);
84
+ } catch {
85
+ return false;
86
+ }
87
+ }
88
+
73
89
  /**
74
90
  * Skill source roots, in priority order. The canonical Total Recall location
75
91
  * (.agent/skills, singular) wins, but we also scan the Antigravity-native
@@ -115,7 +131,10 @@ export function discoverRepoSkills(agentDir) {
115
131
  * Idempotent, self-healing, and strictly additive: a symlink that is broken or
116
132
  * stale is refreshed even without --force; a *real* (non-symlink) entry is
117
133
  * never clobbered unless --force (so a user's own skill always wins a name
118
- * collision); a skill whose source already *is* the destination is left alone.
134
+ * collision) — UNLESS that real entry is itself stale tool output (carries
135
+ * the auto-generated marker), in which case it is healed back onto the
136
+ * canonical source; a skill whose source already *is* the destination is
137
+ * left alone.
119
138
  */
120
139
  export function projectSkillsAsCommands(destDir, skills, opts = {}) {
121
140
  fs.mkdirSync(destDir, { recursive: true });
@@ -138,7 +157,7 @@ export function projectSkillsAsCommands(destDir, skills, opts = {}) {
138
157
  continue;
139
158
  }
140
159
  fs.unlinkSync(linkPath); // stale, broken, or --force → refresh
141
- } else if (!opts.force) {
160
+ } else if (!opts.force && !isStaleGeneratedDir(linkPath)) {
142
161
  written.push({ name: skill.name, action: 'skipped' }); // user's real file/dir wins
143
162
  continue;
144
163
  } else {
@@ -1,5 +1,5 @@
1
1
  import { createSnapshot, listSnapshots, rollbackVault } from '../core/snapshot.mjs';
2
- import { resolveAgentDir, resolveBrainDir } from './agent-dir.mjs';
2
+ import { resolveAgentDir, resolveBrainDir, parseLayerFlag } from './agent-dir.mjs';
3
3
  import { compileSurface } from '../core/surface.mjs';
4
4
  import path from 'path';
5
5
 
@@ -24,20 +24,22 @@ function printHelp() {
24
24
  rollback <id> Restore the vault to a specific snapshot ID
25
25
 
26
26
  Options:
27
- --help, -h Show this help
27
+ --global / --project Target brain layer (default: auto — project if present)
28
+ --help, -h Show this help
28
29
  `);
29
30
  }
30
31
 
31
32
  export default async function snapshotCli(args) {
32
- if (args.length === 0 || args.includes('--help') || args.includes('-h')) {
33
+ const { layer, remainingArgs } = parseLayerFlag(args);
34
+ if (remainingArgs.length === 0 || remainingArgs.includes('--help') || remainingArgs.includes('-h')) {
33
35
  printHelp();
34
36
  return;
35
37
  }
36
38
 
37
- const command = args[0];
39
+ const command = remainingArgs[0];
38
40
 
39
41
  if (command === 'create') {
40
- const reason = args.slice(1).join(' ') || 'manual';
42
+ const reason = remainingArgs.slice(1).join(' ') || 'manual';
41
43
  console.log('📸 Creating snapshot...');
42
44
  const result = createSnapshot(reason);
43
45
  if (result.success) {
@@ -69,7 +71,7 @@ export default async function snapshotCli(args) {
69
71
  }
70
72
 
71
73
  else if (command === 'rollback') {
72
- const id = args[1];
74
+ const id = remainingArgs[1];
73
75
  if (!id) {
74
76
  console.error('❌ You must provide a snapshot ID to rollback to.');
75
77
  console.error('Usage: total-recall snapshot rollback <id>');
@@ -84,8 +86,8 @@ export default async function snapshotCli(args) {
84
86
  console.log(`✅ Rollback successful.`);
85
87
  console.log(`🔄 Recompiling derived indexes from restored vault...`);
86
88
 
87
- const agentDir = resolveAgentDir();
88
- const brainDir = resolveBrainDir();
89
+ const agentDir = resolveAgentDir(layer);
90
+ const brainDir = resolveBrainDir(layer);
89
91
  await compileSurface({
90
92
  vaultDir: path.join(brainDir, 'memory-vault'),
91
93
  skillsDir: path.join(agentDir, 'skills'),
@@ -16,7 +16,8 @@ import {
16
16
  } from '../core/package-auto-update.mjs';
17
17
  import { brainDir } from '../core/config.mjs';
18
18
 
19
- function parseArgs(args) {
19
+ /** @param {string[]} args */
20
+ export function parseArgs(args) {
20
21
  const opts = {
21
22
  apply: false,
22
23
  force: false,
@@ -567,24 +567,42 @@ export async function runCutoffAudit({ vaultDir, queueDir, runtimeConfig }) {
567
567
  return { audited: batch.length, flagged, critical };
568
568
  }
569
569
 
570
+ /** Resolve vault dir from node file path (…/memory-vault/category/slug.md). */
571
+ function vaultDirFromNodeFile(filePath) {
572
+ if (!filePath) return null;
573
+ let cur = path.dirname(filePath);
574
+ while (cur && cur !== path.dirname(cur)) {
575
+ if (path.basename(cur) === 'memory-vault') return cur;
576
+ cur = path.dirname(cur);
577
+ }
578
+ // Fallback: parent of category dir
579
+ return path.dirname(path.dirname(filePath));
580
+ }
581
+
582
+ function nodeFilePath(node) {
583
+ return node?._filepath || node?._filePath || null;
584
+ }
585
+
570
586
  async function stampVerified(node) {
571
- if (!node._filepath) return;
587
+ const fp = nodeFilePath(node);
588
+ if (!fp) return;
572
589
  try {
573
- const raw = fs.readFileSync(node._filepath, 'utf8');
590
+ const raw = fs.readFileSync(fp, 'utf8');
574
591
  const { data, content } = matter(raw);
575
592
  data.x_cutoff_verified_at = new Date().toISOString();
576
593
  data.x_cutoff_risk = 'none';
577
- atomicWrite(node._filepath, safeStringify(content, data));
578
- invalidate(path.dirname(node._filepath));
594
+ atomicWrite(fp, safeStringify(content, data));
595
+ invalidate(vaultDirFromNodeFile(fp));
579
596
  } catch (err) {
580
597
  logger.debug('clarity-rewriter: stampCutoffRiskNone failed', { err: err.message });
581
598
  }
582
599
  }
583
600
 
584
601
  async function stampCutoffRisk(node, riskLevel, claimCount) {
585
- if (!node._filepath) return;
602
+ const fp = nodeFilePath(node);
603
+ if (!fp) return;
586
604
  try {
587
- const raw = fs.readFileSync(node._filepath, 'utf8');
605
+ const raw = fs.readFileSync(fp, 'utf8');
588
606
  const { data, content } = matter(raw);
589
607
  data.x_cutoff_risk = riskLevel;
590
608
  data.x_cutoff_claim_count = claimCount;
@@ -592,8 +610,8 @@ async function stampCutoffRisk(node, riskLevel, claimCount) {
592
610
  // Lower confidence to reflect that claims are unverified
593
611
  const confidencePenalty = riskLevel === 'critical' ? 0.3 : riskLevel === 'high' ? 0.2 : 0.1;
594
612
  data.confidence = Math.max(0.1, (data.confidence || 0.5) - confidencePenalty);
595
- atomicWrite(node._filepath, safeStringify(content, data));
596
- invalidate(path.dirname(node._filepath));
613
+ atomicWrite(fp, safeStringify(content, data));
614
+ invalidate(vaultDirFromNodeFile(fp));
597
615
  } catch (err) {
598
616
  logger.debug('clarity-rewriter: stampCutoffRisk failed', { err: err.message });
599
617
  }
@@ -756,9 +774,10 @@ export async function writeCorrection(originalSlug, researchEvidence, { vaultDir
756
774
  }
757
775
 
758
776
  async function stampNodeForCorrection(node, correctionSlug, disposition) {
759
- if (!node._filepath) return;
777
+ const fp = nodeFilePath(node);
778
+ if (!fp) return;
760
779
  try {
761
- const raw = fs.readFileSync(node._filepath, 'utf8');
780
+ const raw = fs.readFileSync(fp, 'utf8');
762
781
  const { data, content } = matter(raw);
763
782
  data.status = disposition === 'deleted' ? 'archived' : 'superseded';
764
783
  data.superseded_by = correctionSlug;
@@ -766,8 +785,8 @@ async function stampNodeForCorrection(node, correctionSlug, disposition) {
766
785
  // Lower confidence significantly — this fact has been proven wrong
767
786
  data.confidence = Math.max(0.05, (data.confidence || 0.5) * 0.3);
768
787
  data.updated = new Date().toISOString();
769
- atomicWrite(node._filepath, safeStringify(content, data));
770
- invalidate(path.dirname(node._filepath));
788
+ atomicWrite(fp, safeStringify(content, data));
789
+ invalidate(vaultDirFromNodeFile(fp));
771
790
  } catch (err) {
772
791
  logger.debug('clarity-rewriter: stampNodeForCorrection failed', { err: err.message });
773
792
  }