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.
- package/frontend/dist/assets/{index-D5xRw5uq.js → index-B6AfuKv9.js} +126 -134
- package/frontend/dist/index.html +1 -1
- package/frontend/src/api/config.ts +16 -2
- package/frontend/src/api/keys.ts +111 -0
- package/frontend/src/api/notifications.ts +54 -6
- package/frontend/src/api/update.ts +41 -5
- package/frontend/src/components/BrainSelector.spec.tsx +71 -47
- package/frontend/src/components/BrainSelector.tsx +112 -35
- package/frontend/src/components/MobilePairing.spec.tsx +44 -14
- package/frontend/src/components/MobilePairing.tsx +229 -22
- package/frontend/src/pages/HealthPage.tsx +12 -16
- package/frontend/src/pages/NotificationsPage.tsx +6 -3
- package/frontend/src/pages/RulesPage.spec.tsx +46 -15
- package/frontend/src/pages/RulesPage.tsx +41 -9
- package/frontend/src/pages/SecretsPage.tsx +316 -3
- package/frontend/src/pages/SettingsPage.spec.tsx +30 -6
- package/frontend/src/pages/SettingsPage.tsx +228 -86
- package/package.json +1 -1
- package/src/cli/agent-dir.mjs +17 -2
- package/src/cli/chat.mjs +1 -1
- package/src/cli/daemon.mjs +2 -1
- package/src/cli/dream.mjs +14 -10
- package/src/cli/forget.mjs +14 -2
- package/src/cli/friction.mjs +1 -1
- package/src/cli/import-rules.mjs +8 -3
- package/src/cli/ingest.mjs +3 -2
- package/src/cli/lint.mjs +9 -6
- package/src/cli/rebuild.mjs +9 -4
- package/src/cli/remember.mjs +21 -15
- package/src/cli/secret.mjs +184 -4
- package/src/cli/share.mjs +5 -3
- package/src/cli/skill-projection.mjs +21 -2
- package/src/cli/snapshot.mjs +10 -8
- package/src/cli/update.mjs +2 -1
- package/src/core/clarity-rewriter.mjs +31 -12
- package/src/core/config.mjs +10 -4
- package/src/core/daemon-loop.mjs +33 -6
- package/src/core/embeddings.mjs +51 -3
- package/src/core/memory-title.mjs +70 -0
- package/src/core/notifications.mjs +204 -34
- package/src/core/package-auto-update.mjs +99 -3
- package/src/core/pairing.mjs +168 -0
- package/src/core/provider-account-sync.mjs +1145 -0
- package/src/core/provider-catalog.mjs +11 -4
- package/src/core/research.mjs +98 -24
- package/src/core/runtime.mjs +11 -1
- package/src/core/scheduler.mjs +31 -9
- package/src/core/schema.mjs +23 -0
- package/src/core/secret-integration-research.mjs +325 -0
- package/src/core/secrets-store.mjs +284 -14
- package/src/core/secrets-sync.mjs +1 -1
- package/src/core/skills-registry.mjs +12 -0
- package/src/core/source-adapters.mjs +247 -22
- package/src/core/surface.mjs +4 -4
- package/src/core/task-executors.mjs +161 -14
- package/src/core/throttled-fetch.mjs +5 -0
- package/src/core/vault.mjs +7 -1
- package/src/core/webhook-handlers.mjs +123 -14
- package/src/server/auth.mjs +6 -0
- package/src/server/index.mjs +56 -0
- package/src/server/rest.mjs +2 -0
- package/src/server/route-manifest.json +40 -0
- package/src/server/routes/_shared.mjs +33 -1
- package/src/server/routes/config.mjs +64 -23
- package/src/server/routes/context.mjs +14 -9
- package/src/server/routes/dashboard.mjs +39 -18
- package/src/server/routes/docs.mjs +27 -13
- package/src/server/routes/dream.mjs +10 -11
- package/src/server/routes/export.mjs +43 -12
- package/src/server/routes/field.mjs +11 -9
- package/src/server/routes/graph.mjs +31 -13
- package/src/server/routes/import.mjs +35 -16
- package/src/server/routes/instructions.mjs +41 -5
- package/src/server/routes/memory.mjs +61 -21
- package/src/server/routes/mesh.mjs +2 -1
- package/src/server/routes/notifications.mjs +103 -0
- package/src/server/routes/rules.mjs +75 -20
- package/src/server/routes/secrets.mjs +110 -4
- package/src/server/routes/share.mjs +2 -1
- package/src/server/routes/ssss.mjs +11 -3
- package/src/server/routes/system.mjs +20 -1
- package/src/server/routes/update.mjs +33 -7
- package/src/server/routes/vault.mjs +53 -28
package/src/cli/ingest.mjs
CHANGED
|
@@ -16,8 +16,9 @@
|
|
|
16
16
|
import path from 'node:path';
|
|
17
17
|
import { resolveAgentDir, resolveBrainDir } from './agent-dir.mjs';
|
|
18
18
|
|
|
19
|
-
|
|
20
|
-
const
|
|
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 {
|
|
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:
|
|
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
|
|
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(
|
|
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');
|
package/src/cli/rebuild.mjs
CHANGED
|
@@ -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
|
|
18
|
-
const
|
|
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
|
|
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
|
}
|
package/src/cli/remember.mjs
CHANGED
|
@@ -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
|
-
|
|
247
|
-
const finalTitle = title ||
|
|
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
|
-
|
|
313
|
-
const
|
|
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
|
-
|
|
330
|
-
|
|
331
|
-
|
|
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
|
|
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
|
});
|
package/src/cli/secret.mjs
CHANGED
|
@@ -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(
|
|
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
|
-
|
|
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(
|
|
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)
|
|
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 {
|
package/src/cli/snapshot.mjs
CHANGED
|
@@ -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
|
-
--
|
|
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
|
-
|
|
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 =
|
|
39
|
+
const command = remainingArgs[0];
|
|
38
40
|
|
|
39
41
|
if (command === 'create') {
|
|
40
|
-
const reason =
|
|
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 =
|
|
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'),
|
package/src/cli/update.mjs
CHANGED
|
@@ -16,7 +16,8 @@ import {
|
|
|
16
16
|
} from '../core/package-auto-update.mjs';
|
|
17
17
|
import { brainDir } from '../core/config.mjs';
|
|
18
18
|
|
|
19
|
-
|
|
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
|
-
|
|
587
|
+
const fp = nodeFilePath(node);
|
|
588
|
+
if (!fp) return;
|
|
572
589
|
try {
|
|
573
|
-
const raw = fs.readFileSync(
|
|
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(
|
|
578
|
-
invalidate(
|
|
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
|
-
|
|
602
|
+
const fp = nodeFilePath(node);
|
|
603
|
+
if (!fp) return;
|
|
586
604
|
try {
|
|
587
|
-
const raw = fs.readFileSync(
|
|
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(
|
|
596
|
-
invalidate(
|
|
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
|
-
|
|
777
|
+
const fp = nodeFilePath(node);
|
|
778
|
+
if (!fp) return;
|
|
760
779
|
try {
|
|
761
|
-
const raw = fs.readFileSync(
|
|
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(
|
|
770
|
-
invalidate(
|
|
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
|
}
|