total-recall-brain 3.13.0 โ†’ 3.13.2

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 (152) hide show
  1. package/README.md +91 -108
  2. package/bin/antigravity.mjs +81 -52
  3. package/bin/total-recall.mjs +32 -57
  4. package/extension/background.js +1 -1
  5. package/extension/options/options.js +9 -3
  6. package/extension/sidepanel/sidepanel.js +0 -2
  7. package/frontend/index.html +4 -3
  8. package/frontend/package.json +1 -0
  9. package/frontend/src/App.tsx +109 -152
  10. package/frontend/src/api.ts +605 -23
  11. package/frontend/src/components/BrainSelector.tsx +211 -199
  12. package/frontend/src/components/DocumentEditorModal.tsx +57 -0
  13. package/frontend/src/components/DocumentTable.tsx +47 -0
  14. package/frontend/src/components/Graph3D.tsx +120 -71
  15. package/frontend/src/components/UsageChart.tsx +3 -3
  16. package/frontend/src/components/brand/BrandMark.tsx +95 -0
  17. package/frontend/src/index.css +286 -86
  18. package/frontend/src/pages/ApiKeysPage.tsx +1878 -215
  19. package/frontend/src/pages/ChatPage.tsx +103 -45
  20. package/frontend/src/pages/DesignDocsPage.tsx +5 -5
  21. package/frontend/src/pages/FilesPage.tsx +10 -6
  22. package/frontend/src/pages/GraphPage.tsx +5 -3
  23. package/frontend/src/pages/HelpPage.tsx +2 -2
  24. package/frontend/src/pages/InboxPage.tsx +82 -0
  25. package/frontend/src/pages/InstructionsPage.tsx +1 -1
  26. package/frontend/src/pages/IntegrationsPage.tsx +21 -10
  27. package/frontend/src/pages/LoginPage.tsx +95 -77
  28. package/frontend/src/pages/MemoryPage.tsx +31 -8
  29. package/frontend/src/pages/OnboardingPage.tsx +690 -0
  30. package/frontend/src/pages/OpenWikiPage.tsx +412 -323
  31. package/frontend/src/pages/SettingsPage.tsx +825 -922
  32. package/frontend/src/pages/SkillsPage.tsx +272 -147
  33. package/frontend/src/pages/UsagePage.tsx +23 -31
  34. package/frontend/src/pages/VaultPage.tsx +223 -0
  35. package/frontend/src/types.ts +13 -0
  36. package/package.json +4 -4
  37. package/scaffold/.agent/skills/total-recall/SKILL.md +43 -4
  38. package/scaffold/.agent/skills/total-recall/modules/README.md +10 -0
  39. package/scaffold/.agent/skills/total-recall/modules/agents/README.md +5 -0
  40. package/scaffold/.agent/skills/total-recall/{skills/tr-cli-agents โ†’ modules/agents}/agents.yml +8 -0
  41. package/scaffold/.agent/skills/total-recall/modules/skill-deploy/README.md +9 -0
  42. package/scaffold/.agent/skills/total-recall/openwiki/README.md +10 -0
  43. package/scaffold/.agent/skills/total-recall/openwiki/architecture.md +5 -0
  44. package/scaffold/.agent/skills/total-recall/openwiki/ide.md +10 -0
  45. package/scaffold/.agent/skills/total-recall/openwiki/memory.md +11 -0
  46. package/scaffold/.agent/skills/total-recall/openwiki/secrets.md +7 -0
  47. package/scaffold/.agent/skills/total-recall/openwiki/skills.md +7 -0
  48. package/src/cli/brain.mjs +63 -5
  49. package/src/cli/connect.mjs +11 -19
  50. package/src/cli/daemon.mjs +5 -4
  51. package/src/cli/deploy-ui.mjs +8 -3
  52. package/src/cli/deploy.mjs +84 -27
  53. package/src/cli/ingest/index.mjs +1 -1
  54. package/src/cli/init.mjs +63 -34
  55. package/src/cli/remember.mjs +3 -3
  56. package/src/cli/secret.mjs +812 -0
  57. package/src/cli/setup.mjs +34 -7
  58. package/src/cli/share.mjs +2 -2
  59. package/src/cli/skill.mjs +396 -35
  60. package/src/cli/sync.mjs +4 -4
  61. package/src/cli/task.mjs +287 -0
  62. package/src/core/clarity-rewriter.mjs +5 -5
  63. package/src/core/config.mjs +44 -2
  64. package/src/core/conflict-detector.mjs +5 -5
  65. package/src/core/crons.mjs +54 -0
  66. package/src/core/daemon-loop.mjs +87 -235
  67. package/src/core/dream.mjs +170 -10
  68. package/src/core/env-import.mjs +456 -0
  69. package/src/core/fact-seeker.mjs +14 -14
  70. package/src/core/import-rules.mjs +20 -7
  71. package/src/core/inference-engine.mjs +3 -3
  72. package/src/core/migrate.mjs +14 -8
  73. package/src/core/okf-adapter.mjs +2 -2
  74. package/src/core/operation-validator.mjs +39 -331
  75. package/src/core/optimizer.mjs +7 -0
  76. package/src/core/post-mortem.mjs +2 -2
  77. package/src/core/project-brain.mjs +349 -0
  78. package/src/core/provider-catalog.mjs +296 -0
  79. package/src/core/quick-capture.mjs +1 -1
  80. package/src/core/remote-vault-sync.mjs +194 -0
  81. package/src/core/research-queue.mjs +7 -11
  82. package/src/core/runtime.mjs +33 -9
  83. package/src/core/scheduler.mjs +59 -5
  84. package/src/core/schema.mjs +14 -0
  85. package/src/core/search.mjs +1 -1
  86. package/src/core/secrets-env-export.mjs +323 -0
  87. package/src/core/secrets-rotate.mjs +245 -0
  88. package/src/core/secrets-store.mjs +645 -0
  89. package/src/core/semantic-index.mjs +3 -3
  90. package/src/core/skills-registry.mjs +1062 -0
  91. package/src/core/source-adapters.mjs +4 -1
  92. package/src/core/ssss-host-extension.mjs +94 -0
  93. package/src/core/ssss-kernel-bridge.mjs +530 -0
  94. package/src/core/surface.mjs +14 -0
  95. package/src/core/task-envelope.mjs +298 -0
  96. package/src/core/task-executors.mjs +600 -0
  97. package/src/core/validated-write.mjs +124 -56
  98. package/src/core/vault.mjs +21 -34
  99. package/src/core/webauthn-store.mjs +272 -0
  100. package/src/server/api.mjs +26 -16
  101. package/src/server/auth.mjs +51 -0
  102. package/src/server/index.mjs +3 -3
  103. package/src/server/keys.mjs +30 -4
  104. package/src/server/rest.mjs +198 -25
  105. package/src/server/route-manifest.json +156 -0
  106. package/src/server/routes/_shared.mjs +40 -1
  107. package/src/server/routes/docs.mjs +260 -0
  108. package/src/server/routes/memory.mjs +145 -32
  109. package/src/server/routes/research.mjs +30 -0
  110. package/src/server/routes/secrets.mjs +470 -0
  111. package/src/server/routes/share.mjs +2 -2
  112. package/src/server/routes/skills.mjs +452 -54
  113. package/src/server/routes/sync.mjs +48 -0
  114. package/src/server/routes/webauthn.mjs +184 -0
  115. package/src/server/update-route-manifest.mjs +39 -0
  116. package/templates/onboarding-interview.md +32 -22
  117. package/templates/openwiki/README.md +10 -0
  118. package/templates/openwiki/architecture.md +5 -0
  119. package/templates/openwiki/ide.md +10 -0
  120. package/templates/openwiki/memory.md +11 -0
  121. package/templates/openwiki/secrets.md +7 -0
  122. package/templates/openwiki/skills.md +7 -0
  123. package/scaffold/.agent/skills/total-recall/skills/tr-cli-agents/SKILL.md +0 -121
  124. package/scaffold/.agent/skills/total-recall/skills/tr-cli-agents/references/antigravity.txt +0 -20
  125. package/scaffold/.agent/skills/total-recall/skills/tr-cli-agents/references/claude.txt +0 -58
  126. package/scaffold/.agent/skills/total-recall/skills/tr-cli-agents/references/codex.txt +0 -81
  127. package/scaffold/.agent/skills/total-recall/skills/tr-cli-agents/references/gemini.txt +0 -44
  128. package/scaffold/.agent/skills/total-recall/skills/tr-cli-agents/scripts/.gitkeep +0 -0
  129. package/scaffold/.agent/skills/total-recall/skills/tr-cli-agents/subagents/.gitkeep +0 -0
  130. package/scaffold/.agent/skills/total-recall/skills/tr-research/SKILL.md +0 -338
  131. package/scaffold/.agent/skills/total-recall/skills/tr-skill/SKILL.md +0 -282
  132. package/scaffold/.agent/skills/total-recall/skills/tr-skill/assets/skill-template/SKILL.md +0 -20
  133. package/scaffold/.agent/skills/total-recall/skills/tr-skill/evals/evals.json +0 -20
  134. package/scaffold/.agent/skills/total-recall/skills/tr-skill/hooks/on-skill-add.sh +0 -14
  135. package/scaffold/.agent/skills/total-recall/skills/tr-skill/hooks/on-skill-delete.sh +0 -14
  136. package/scaffold/.agent/skills/total-recall/skills/tr-skill/learnings/.gitkeep +0 -0
  137. package/scaffold/.agent/skills/total-recall/skills/tr-skill/references/.gitkeep +0 -0
  138. package/scaffold/.agent/skills/total-recall/skills/tr-skill/scripts/.gitkeep +0 -0
  139. package/scaffold/.agent/skills/total-recall/skills/tr-skill/scripts/create-skill.sh +0 -65
  140. package/scaffold/.agent/skills/total-recall/skills/tr-skill/scripts/enforce-skill-optimization.mjs +0 -135
  141. package/scaffold/.agent/skills/total-recall/skills/tr-skill/scripts/skill-watcher.mjs +0 -64
  142. package/scaffold/.agent/skills/total-recall/skills/tr-skill/subagents/audit-all-skills.md +0 -62
  143. package/scaffold/.agent/skills/total-recall/skills/tr-ssss/SKILL.md +0 -298
  144. package/scaffold/.agent/skills/total-recall/skills/tr-ssss/evals/evals.json +0 -17
  145. package/scaffold/.agent/skills/total-recall/skills/tr-ssss/references/CHANGELOG.md +0 -124
  146. package/scaffold/.agent/skills/total-recall/skills/tr-ssss/references/ssss-spec.md +0 -1325
  147. package/scaffold/.agent/skills/total-recall/skills/tr-ssss/scripts/validate-schema.mjs +0 -232
  148. package/scaffold/.agent/skills/total-recall/skills/tr-ssss/subagents/audit-ssss-node.md +0 -9
  149. /package/scaffold/.agent/skills/total-recall/{skills/tr-cli-agents/evals โ†’ modules/skill-deploy/scripts}/.gitkeep +0 -0
  150. /package/scaffold/.agent/skills/total-recall/{skills/tr-skill โ†’ modules/skill-deploy}/scripts/find-skills.mjs +0 -0
  151. /package/scaffold/.agent/skills/total-recall/{skills/tr-skill โ†’ modules/skill-deploy}/scripts/install-skill.mjs +0 -0
  152. /package/scaffold/.agent/skills/total-recall/{skills/tr-skill โ†’ modules/skill-deploy}/scripts/scan-skill.mjs +0 -0
@@ -0,0 +1,812 @@
1
+ /**
2
+ * total-recall secret โ€” manage API keys and credentials (not in the memory vault).
3
+ *
4
+ * Usage:
5
+ * npx total-recall secret set <key> <value> [--provider name] [--scope global|project]
6
+ * npx total-recall secret get <key> # prints value (use carefully)
7
+ * npx total-recall secret list
8
+ * npx total-recall secret rotate <key> <new-value>
9
+ * npx total-recall secret delete <key>
10
+ * npx total-recall secret audit [--limit N]
11
+ * npx total-recall secret usage [--days N]
12
+ * npx total-recall secret usage-record --provider x --cost 0.01
13
+ */
14
+
15
+ import {
16
+ setSecret,
17
+ getSecret,
18
+ listSecretsMeta,
19
+ rotateSecret,
20
+ deleteSecret,
21
+ readSecretAudit,
22
+ recordUsage,
23
+ summarizeUsage,
24
+ summarizeUsageByKey,
25
+ loadBudgetConfig,
26
+ resolveSecretsPath,
27
+ textContainsSecrets,
28
+ getSecretsCatalog,
29
+ updateSecretMeta,
30
+ listRotationDue,
31
+ } from '../core/secrets-store.mjs';
32
+ import {
33
+ scanEnvSources,
34
+ publicScanResult,
35
+ importEnvSecrets,
36
+ parseEnvText,
37
+ inferProvider,
38
+ } from '../core/env-import.mjs';
39
+ import {
40
+ exportEnvToProject,
41
+ exportEnvToRegistry,
42
+ buildEnvProjection,
43
+ buildDeploySecretsPayload,
44
+ } from '../core/secrets-env-export.mjs';
45
+ import {
46
+ rotateSecretAndExport,
47
+ enqueueRotationDueTasks,
48
+ getBrowserRotateAssist,
49
+ } from '../core/secrets-rotate.mjs';
50
+ import { listProviders, getProvider } from '../core/provider-catalog.mjs';
51
+ import { resolveBrainDir, parseLayerFlag } from './agent-dir.mjs';
52
+ import fs from 'node:fs';
53
+ import path from 'node:path';
54
+
55
+ function printHelp() {
56
+ console.log(`
57
+ total-recall secret โ€” Secrets store (separate from memory vault)
58
+
59
+ Usage: total-recall secret <command> [options]
60
+
61
+ Commands:
62
+ set <key> <value> Store a secret (0600 file; optional AES via TR_SECRETS_PASSWORD)
63
+ get <key> Print secret value (audited)
64
+ list List keys + metadata only (no values)
65
+ rotate <key> <value> Replace value and mark rotated
66
+ delete <key> Remove a secret
67
+ audit [--limit N] Recent set/get/rotate events (no values)
68
+ usage [--days N] Sum usage.jsonl for last N days (default 1)
69
+ usage-record Append a usage event (see flags)
70
+ check-surfaces Fail if any secret value appears in INSTRUCTIONS.md / openwiki
71
+ export-env Write .env FROM secrets store (SSOT โ†’ repo/deploy projection)
72
+ --path <dir> Target repo root (default: cwd)
73
+ --all-projects Export to every project-registry path
74
+ --filename .env Output name (default .env)
75
+ --no-example Skip .env.example
76
+ --no-global Only keys bound to this repo (skip unbound)
77
+ --replace-all Overwrite entire .env (default: merge TR block only)
78
+ --dry-run Show keys that would be written
79
+ import-env One-time migrate: scan local .env โ†’ store (not the steady-state path)
80
+ --all / --file / --overwrite / --dry-run
81
+ catalog Full catalog: keys, providers, usage, rotation, cost
82
+ providers List provider registry (docs/schema/tiers)
83
+ meta <key> Update metadata (no value change)
84
+ --repo <name> Bind to exactly ONE product repo (not multi)
85
+ --repos <name> Same as --repo (single name only; multi rejected)
86
+ --tier <id> Subscription tier
87
+ --monthly-cost <n> Planned monthly $
88
+ --monthly-cap <n> Cap $
89
+ --docs <url> API docs URL
90
+ --rotate-days <n> Auto-rotation interval
91
+ --auto-rotate Enable auto-rotate flag
92
+ --notes <text>
93
+ --label <text>
94
+ --project <path>
95
+ rotation-due List keys overdue for rotation
96
+ --enqueue Create daemon tasks + browser-use prompts for each due key
97
+ rotate-browser <key> Print supervised browser-use rotation instructions
98
+ usage Cost/events (add --key-ref for per-key)
99
+
100
+ rotate options:
101
+ --export-env After rotate, write .env to bound repos (and cwd if unbound)
102
+ --export-all After rotate, export-env to all project-registry paths
103
+
104
+ set / rotate options:
105
+ --provider <name> Optional provider tag (e.g. openai, anthropic)
106
+ --scope global|project
107
+ --repo / --repos Bind secret to repo(s)
108
+ --tier / --monthly-cost / --rotate-days
109
+
110
+ usage-record options:
111
+ --provider <name>
112
+ --model <id>
113
+ --input-tokens <n>
114
+ --output-tokens <n>
115
+ --cost <usd>
116
+ --key-ref <secret-name> name only, never the value
117
+
118
+ Env:
119
+ TR_SECRETS_PASSWORD If set, encrypt secrets.enc with AES-256-GCM
120
+
121
+ Never put secrets in remember/vault/openwiki/instruction shims.
122
+ `);
123
+ }
124
+
125
+ function parseArgs(args) {
126
+ const out = {
127
+ command: null,
128
+ key: null,
129
+ value: null,
130
+ provider: null,
131
+ scope: 'global',
132
+ limit: 50,
133
+ days: 1,
134
+ model: null,
135
+ input_tokens: 0,
136
+ output_tokens: 0,
137
+ cost: null,
138
+ key_ref: null,
139
+ help: false,
140
+ all: false,
141
+ dryRun: false,
142
+ overwrite: false,
143
+ file: null,
144
+ keys: [],
145
+ repos: [],
146
+ tier: null,
147
+ monthly_cost: null,
148
+ monthly_cap: null,
149
+ docs: null,
150
+ rotate_days: null,
151
+ auto_rotate: false,
152
+ notes: null,
153
+ label: null,
154
+ project: null,
155
+ path: null,
156
+ allProjects: false,
157
+ filename: '.env',
158
+ noExample: false,
159
+ noGlobal: false,
160
+ replaceAll: false,
161
+ exportEnv: false,
162
+ exportAll: false,
163
+ enqueue: false,
164
+ };
165
+ if (!args.length || args[0] === '--help' || args[0] === '-h') {
166
+ out.help = true;
167
+ return out;
168
+ }
169
+ out.command = args[0];
170
+ let i = 1;
171
+ if (
172
+ ['set', 'get', 'rotate', 'delete', 'meta', 'rotate-browser', 'rm'].includes(out.command) &&
173
+ args[i] &&
174
+ !args[i].startsWith('--')
175
+ ) {
176
+ out.key = args[i++];
177
+ }
178
+ if (['set', 'rotate'].includes(out.command) && args[i] && !args[i].startsWith('--')) {
179
+ out.value = args[i++];
180
+ }
181
+ while (i < args.length) {
182
+ const a = args[i];
183
+ switch (a) {
184
+ case '--provider':
185
+ out.provider = args[++i];
186
+ break;
187
+ case '--scope':
188
+ out.scope = args[++i];
189
+ break;
190
+ case '--limit':
191
+ out.limit = parseInt(args[++i], 10) || 50;
192
+ break;
193
+ case '--days':
194
+ out.days = parseInt(args[++i], 10) || 1;
195
+ break;
196
+ case '--model':
197
+ out.model = args[++i];
198
+ break;
199
+ case '--input-tokens':
200
+ out.input_tokens = parseInt(args[++i], 10) || 0;
201
+ break;
202
+ case '--output-tokens':
203
+ out.output_tokens = parseInt(args[++i], 10) || 0;
204
+ break;
205
+ case '--cost':
206
+ out.cost = parseFloat(args[++i]);
207
+ break;
208
+ case '--key-ref':
209
+ out.key_ref = args[++i];
210
+ break;
211
+ case '--all':
212
+ out.all = true;
213
+ break;
214
+ case '--dry-run':
215
+ out.dryRun = true;
216
+ break;
217
+ case '--overwrite':
218
+ out.overwrite = true;
219
+ break;
220
+ case '--file':
221
+ out.file = args[++i];
222
+ break;
223
+ case '--repo':
224
+ out.repos.push(args[++i]);
225
+ break;
226
+ case '--repos':
227
+ out.repos.push(
228
+ ...String(args[++i] || '')
229
+ .split(',')
230
+ .map((s) => s.trim())
231
+ .filter(Boolean),
232
+ );
233
+ break;
234
+ case '--tier':
235
+ out.tier = args[++i];
236
+ break;
237
+ case '--monthly-cost':
238
+ out.monthly_cost = parseFloat(args[++i]);
239
+ break;
240
+ case '--monthly-cap':
241
+ out.monthly_cap = parseFloat(args[++i]);
242
+ break;
243
+ case '--docs':
244
+ out.docs = args[++i];
245
+ break;
246
+ case '--rotate-days':
247
+ out.rotate_days = parseInt(args[++i], 10);
248
+ break;
249
+ case '--auto-rotate':
250
+ out.auto_rotate = true;
251
+ break;
252
+ case '--notes':
253
+ out.notes = args[++i];
254
+ break;
255
+ case '--label':
256
+ out.label = args[++i];
257
+ break;
258
+ case '--project':
259
+ out.project = args[++i];
260
+ break;
261
+ case '--path':
262
+ out.path = args[++i];
263
+ break;
264
+ case '--all-projects':
265
+ out.allProjects = true;
266
+ break;
267
+ case '--filename':
268
+ out.filename = args[++i] || '.env';
269
+ break;
270
+ case '--no-example':
271
+ out.noExample = true;
272
+ break;
273
+ case '--no-global':
274
+ out.noGlobal = true;
275
+ break;
276
+ case '--replace-all':
277
+ out.replaceAll = true;
278
+ break;
279
+ case '--export-env':
280
+ out.exportEnv = true;
281
+ break;
282
+ case '--export-all':
283
+ out.exportAll = true;
284
+ out.exportEnv = true;
285
+ break;
286
+ case '--enqueue':
287
+ out.enqueue = true;
288
+ break;
289
+ case '--help':
290
+ case '-h':
291
+ out.help = true;
292
+ break;
293
+ default:
294
+ if (!a.startsWith('--') && out.command === 'import-env') {
295
+ out.keys.push(a);
296
+ } else if (!a.startsWith('--') && !out.key) out.key = a;
297
+ else if (!a.startsWith('--') && !out.value) out.value = a;
298
+ }
299
+ i++;
300
+ }
301
+ return out;
302
+ }
303
+
304
+ export default async function secretCli(argv) {
305
+ const { layer, remainingArgs } = parseLayerFlag(argv);
306
+ const opts = parseArgs(remainingArgs);
307
+ if (opts.help || !opts.command) {
308
+ printHelp();
309
+ return;
310
+ }
311
+
312
+ let brainDir;
313
+ try {
314
+ brainDir = resolveBrainDir(layer);
315
+ } catch (err) {
316
+ console.error(`โŒ ${err.message}`);
317
+ process.exit(1);
318
+ }
319
+
320
+ try {
321
+ if (opts.command === 'set') {
322
+ if (!opts.key || opts.value == null) {
323
+ console.error('Usage: total-recall secret set <key> <value>');
324
+ process.exit(1);
325
+ }
326
+ await setSecret(brainDir, opts.key, opts.value, {
327
+ provider: opts.provider || inferProvider(opts.key),
328
+ scope: opts.scope,
329
+ repos: opts.repos.length ? opts.repos : undefined,
330
+ subscription_tier: opts.tier,
331
+ monthly_cost_usd: opts.monthly_cost,
332
+ monthly_cap_usd: opts.monthly_cap,
333
+ api_docs_url: opts.docs,
334
+ rotate_every_days: opts.rotate_days,
335
+ auto_rotate: opts.auto_rotate,
336
+ notes: opts.notes,
337
+ label: opts.label,
338
+ project_path: opts.project,
339
+ });
340
+ console.log(`\n โœ… Secret set: ${opts.key}`);
341
+ console.log(` store: ${resolveSecretsPath(brainDir)}`);
342
+ console.log(` (value not printed)\n`);
343
+ return;
344
+ }
345
+
346
+ if (opts.command === 'catalog') {
347
+ const cat = await getSecretsCatalog(brainDir);
348
+ console.log(`\n ๐Ÿ“ฆ Secrets catalog โ€” ${cat.store}`);
349
+ console.log(
350
+ ` keys=${cat.summary.total_keys} providers=${cat.summary.providers_active} planned/mo=$${cat.summary.monthly_subscription_usd.toFixed(2)} usage30d=$${cat.summary.usage_30d.cost_usd.toFixed(4)} rotate_due=${cat.summary.rotation_overdue}\n`,
351
+ );
352
+
353
+ // Group: multi-repo errors โ†’ one section per product repo โ†’ Developer secrets
354
+ const byRepo = new Map();
355
+ const developer = [];
356
+ const multiError = [];
357
+ for (const k of cat.keys) {
358
+ const repos = Array.isArray(k.repos) ? k.repos.filter(Boolean) : [];
359
+ if (repos.length > 1 || k.multi_repo_error) {
360
+ multiError.push(k);
361
+ continue;
362
+ }
363
+ if (!repos.length) {
364
+ developer.push(k);
365
+ continue;
366
+ }
367
+ const r = repos[0];
368
+ if (!byRepo.has(r)) byRepo.set(r, []);
369
+ byRepo.get(r).push(k);
370
+ }
371
+ const sections = [];
372
+ if (multiError.length) sections.push(['โš ๏ธ NEEDS FIX โ€” multi-repo (one key โ†’ one repo only)', multiError]);
373
+ for (const [repo, list] of [...byRepo.entries()].sort(([a], [b]) => a.localeCompare(b))) {
374
+ sections.push([repo, list]);
375
+ }
376
+ if (developer.length) sections.push(['Developer secrets (tooling / unbound)', developer]);
377
+
378
+ if (multiError.length) {
379
+ console.log(
380
+ ` โŒ ${multiError.length} key(s) bound to multiple repos โ€” fix with:\n` +
381
+ ` secret meta <KEY> --repo <one-repo>\n` +
382
+ ` or clear repos for Developer secrets\n`,
383
+ );
384
+ }
385
+
386
+ for (const [repo, list] of sections) {
387
+ console.log(` โ”€โ”€ ${repo} (${list.length})`);
388
+ for (const k of list.sort((a, b) => a.key.localeCompare(b.key))) {
389
+ const due = k.rotation_overdue
390
+ ? 'โš ๏ธ ROTATE'
391
+ : k.next_rotate_due
392
+ ? `due ${k.next_rotate_due.slice(0, 10)}`
393
+ : '';
394
+ const multi =
395
+ k.multi_repo_error || (k.repos?.length > 1)
396
+ ? ` โŒ multi=[${(k.repos || []).join(',')}]`
397
+ : '';
398
+ console.log(
399
+ ` โ€ข ${k.key.padEnd(28)} ${(k.provider || '-').padEnd(12)} tier=${(k.subscription_tier || '-').padEnd(8)} $${String(k.monthly_cost_usd ?? '-').padStart(6)} u30=$${k.usage_30d.cost_usd.toFixed(3)} ${due}${multi}`,
400
+ );
401
+ if (k.api_docs_url) console.log(` docs: ${k.api_docs_url}`);
402
+ }
403
+ console.log('');
404
+ }
405
+ return;
406
+ }
407
+
408
+ if (opts.command === 'providers') {
409
+ console.log('\n Provider registry\n');
410
+ for (const p of listProviders()) {
411
+ console.log(` โ€ข ${p.id.padEnd(14)} ${p.name}`);
412
+ console.log(` docs: ${p.docs_url}`);
413
+ if (p.pricing_url) console.log(` pricing: ${p.pricing_url}`);
414
+ console.log(` keys: ${p.key_patterns.join(', ')}`);
415
+ console.log(` auth: ${p.schema.auth} ${p.schema.header || ''}`);
416
+ if (p.tiers?.length) {
417
+ console.log(` tiers: ${p.tiers.map((t) => t.id).join(', ')}`);
418
+ }
419
+ console.log('');
420
+ }
421
+ return;
422
+ }
423
+
424
+ if (opts.command === 'meta') {
425
+ if (!opts.key) {
426
+ console.error('Usage: total-recall secret meta <key> [--repo ...] [--tier ...]');
427
+ process.exit(1);
428
+ }
429
+ const patch = {};
430
+ if (opts.repos.length) patch.repos = opts.repos;
431
+ if (opts.tier != null) patch.subscription_tier = opts.tier;
432
+ if (opts.monthly_cost != null) patch.monthly_cost_usd = opts.monthly_cost;
433
+ if (opts.monthly_cap != null) patch.monthly_cap_usd = opts.monthly_cap;
434
+ if (opts.docs) patch.api_docs_url = opts.docs;
435
+ if (opts.rotate_days != null) patch.rotate_every_days = opts.rotate_days;
436
+ if (opts.auto_rotate) patch.auto_rotate = true;
437
+ if (opts.notes) patch.notes = opts.notes;
438
+ if (opts.label) patch.label = opts.label;
439
+ if (opts.project) patch.project_path = opts.project;
440
+ if (opts.provider) patch.provider = opts.provider;
441
+ if (opts.scope) patch.scope = opts.scope;
442
+ // Fill docs from catalog if provider set and no docs
443
+ if (patch.provider && !patch.api_docs_url) {
444
+ const p = getProvider(patch.provider);
445
+ if (p) {
446
+ patch.api_docs_url = p.docs_url;
447
+ if (patch.monthly_cap_usd == null) patch.monthly_cap_usd = p.default_monthly_cap_usd;
448
+ }
449
+ }
450
+ const row = await updateSecretMeta(brainDir, opts.key, patch, { actor: 'cli' });
451
+ console.log(`\n โœ… Meta updated: ${opts.key}`);
452
+ console.log(` provider=${row.provider} tier=${row.subscription_tier} repos=${(row.repos || []).join(',') || '-'}`);
453
+ console.log(` monthly_cost=$${row.monthly_cost_usd ?? '-'} cap=$${row.monthly_cap_usd ?? '-'}`);
454
+ console.log(` docs=${row.api_docs_url || '-'}`);
455
+ console.log(` rotate_every=${row.rotate_every_days || '-'}d next=${row.next_rotate_due || '-'}\n`);
456
+ return;
457
+ }
458
+
459
+ if (opts.command === 'rotation-due') {
460
+ const due = await listRotationDue(brainDir, { autoOnly: false });
461
+ if (!due.length) {
462
+ console.log('\n No secrets overdue for rotation.\n');
463
+ return;
464
+ }
465
+ console.log(`\n โš ๏ธ ${due.length} secret(s) overdue for rotation\n`);
466
+ for (const k of due) {
467
+ console.log(
468
+ ` โ€ข ${k.key} provider=${k.provider || '-'} due=${k.next_rotate_due} auto=${k.auto_rotate ? 'yes' : 'no'}`,
469
+ );
470
+ }
471
+ if (opts.enqueue) {
472
+ const r = await enqueueRotationDueTasks(brainDir, { autoOnly: false });
473
+ console.log(`\n ๐Ÿ“‹ Enqueued ${r.tasks.filter((t) => t.status === 'created').length} rotation assist task(s)`);
474
+ }
475
+ console.log('\n Rotate: npx total-recall secret rotate <key> <new> --export-env');
476
+ console.log(' Browser assist: npx total-recall secret rotate-browser <key>');
477
+ console.log(' Enqueue tasks: npx total-recall secret rotation-due --enqueue\n');
478
+ return;
479
+ }
480
+
481
+ if (opts.command === 'get') {
482
+ if (!opts.key) {
483
+ console.error('Usage: total-recall secret get <key>');
484
+ process.exit(1);
485
+ }
486
+ const r = await getSecret(brainDir, opts.key);
487
+ if (!r.found) {
488
+ console.error(` Secret not found: ${opts.key}`);
489
+ process.exit(1);
490
+ }
491
+ // Print value only โ€” user explicitly asked
492
+ console.log(r.value);
493
+ return;
494
+ }
495
+
496
+ if (opts.command === 'list') {
497
+ const rows = await listSecretsMeta(brainDir);
498
+ if (!rows.length) {
499
+ console.log(`\n No secrets in ${resolveSecretsPath(brainDir)}\n`);
500
+ return;
501
+ }
502
+ console.log(`\n Secrets metadata โ€” ${resolveSecretsPath(brainDir)}\n`);
503
+ for (const r of rows) {
504
+ const repos = r.repos?.length ? r.repos.join(',') : '-';
505
+ console.log(
506
+ ` โ€ข ${r.key.padEnd(28)} len=${String(r.length).padStart(4)} ${(r.provider || '-').padEnd(12)} tier=${r.subscription_tier || '-'} $${r.monthly_cost_usd ?? '-'} repos=${repos}`,
507
+ );
508
+ if (r.api_docs_url) console.log(` docs: ${r.api_docs_url}`);
509
+ }
510
+ console.log('');
511
+ return;
512
+ }
513
+
514
+ if (opts.command === 'rotate') {
515
+ if (!opts.key || !opts.value) {
516
+ console.error('Usage: total-recall secret rotate <key> <new-value> [--export-env|--export-all]');
517
+ process.exit(1);
518
+ }
519
+ if (opts.exportEnv || opts.exportAll) {
520
+ const r = await rotateSecretAndExport(brainDir, opts.key, opts.value, {
521
+ provider: opts.provider,
522
+ actor: 'cli',
523
+ exportEnv: true,
524
+ exportAllProjects: opts.exportAll,
525
+ exportCwd: true,
526
+ includeGlobal: true,
527
+ });
528
+ console.log(` โœ… Rotated: ${opts.key}`);
529
+ if (r.next_rotate_due) console.log(` next due: ${r.next_rotate_due}`);
530
+ const ok = (r.exports || []).filter((e) => e.ok !== false);
531
+ console.log(` export-env: ${ok.length} target(s)`);
532
+ for (const e of ok) {
533
+ if (e.envPath) console.log(` โ†’ ${e.envPath} (${e.count} keys)`);
534
+ }
535
+ return;
536
+ }
537
+ await rotateSecret(brainDir, opts.key, opts.value, {
538
+ provider: opts.provider,
539
+ scope: opts.scope,
540
+ });
541
+ console.log(` โœ… Rotated: ${opts.key}`);
542
+ console.log(` tip: add --export-env to push .env projections after rotate`);
543
+ return;
544
+ }
545
+
546
+ if (opts.command === 'rotate-browser') {
547
+ if (!opts.key) {
548
+ console.error('Usage: total-recall secret rotate-browser <key>');
549
+ process.exit(1);
550
+ }
551
+ const assist = await getBrowserRotateAssist(brainDir, opts.key);
552
+ console.log(`\n ๐ŸŒ Supervised browser rotation โ€” ${assist.key}\n`);
553
+ if (assist.console_url) console.log(` Console: ${assist.console_url}`);
554
+ if (assist.docs_url) console.log(` Docs: ${assist.docs_url}`);
555
+ console.log(` Overdue: ${assist.overdue ? 'YES' : 'no'}`);
556
+ console.log('\n' + assist.prompt + '\n');
557
+ return;
558
+ }
559
+
560
+ if (opts.command === 'delete' || opts.command === 'rm') {
561
+ if (!opts.key) {
562
+ console.error('Usage: total-recall secret delete <key>');
563
+ process.exit(1);
564
+ }
565
+ const r = await deleteSecret(brainDir, opts.key);
566
+ if (!r.found) {
567
+ console.error(` Secret not found: ${opts.key}`);
568
+ process.exit(1);
569
+ }
570
+ console.log(` โœ… Deleted: ${opts.key}`);
571
+ return;
572
+ }
573
+
574
+ if (opts.command === 'audit') {
575
+ const events = readSecretAudit(brainDir, { limit: opts.limit });
576
+ if (!events.length) {
577
+ console.log('\n No secret audit events yet.\n');
578
+ return;
579
+ }
580
+ console.log(`\n Secret audit (last ${events.length})\n`);
581
+ for (const e of events) {
582
+ console.log(` ${e.ts} ${e.action.padEnd(8)} ${e.key} ${e.actor || ''}`);
583
+ }
584
+ console.log('');
585
+ return;
586
+ }
587
+
588
+ if (opts.command === 'usage') {
589
+ const day = summarizeUsage(brainDir, { days: opts.days });
590
+ const week = summarizeUsage(brainDir, { days: 7 });
591
+ const month = summarizeUsage(brainDir, { days: 30 });
592
+ const by = summarizeUsageByKey(brainDir, { days: 30 });
593
+ const budget = loadBudgetConfig(brainDir);
594
+ console.log(`\n Usage summary`);
595
+ console.log(` last ${opts.days}d: events=${day.events} cost=$${day.cost_usd.toFixed(4)} tokens_in=${day.input_tokens} tokens_out=${day.output_tokens}`);
596
+ console.log(` last 7d: events=${week.events} cost=$${week.cost_usd.toFixed(4)}`);
597
+ console.log(` last 30d: events=${month.events} cost=$${month.cost_usd.toFixed(4)}`);
598
+ const dailyCap = budget.config.daily_cap_usd ?? budget.config.dailyCapUsd;
599
+ const weeklyCap = budget.config.weekly_cap_usd ?? budget.config.weeklyCapUsd;
600
+ if (dailyCap != null) {
601
+ console.log(` budget daily_cap_usd=${dailyCap} ${day.cost_usd > dailyCap ? 'โš ๏ธ OVER' : 'ok'}`);
602
+ }
603
+ if (weeklyCap != null) {
604
+ console.log(` budget weekly_cap_usd=${weeklyCap} ${week.cost_usd > weeklyCap ? 'โš ๏ธ OVER' : 'ok'}`);
605
+ }
606
+ if (budget.path) console.log(` budget file: ${budget.path}`);
607
+ const keyEntries = Object.entries(by.by_key).filter(([k]) => k !== '_unattributed');
608
+ if (keyEntries.length) {
609
+ console.log(`\n By key_ref (30d)`);
610
+ for (const [k, v] of keyEntries.sort((a, b) => b[1].cost_usd - a[1].cost_usd)) {
611
+ console.log(` ${k.padEnd(28)} events=${v.events} cost=$${v.cost_usd.toFixed(4)}`);
612
+ }
613
+ }
614
+ const provEntries = Object.entries(by.by_provider);
615
+ if (provEntries.length) {
616
+ console.log(`\n By provider (30d)`);
617
+ for (const [p, v] of provEntries.sort((a, b) => b[1].cost_usd - a[1].cost_usd)) {
618
+ console.log(` ${p.padEnd(16)} events=${v.events} cost=$${v.cost_usd.toFixed(4)}`);
619
+ }
620
+ }
621
+ console.log('');
622
+ return;
623
+ }
624
+
625
+ if (opts.command === 'usage-record') {
626
+ const row = recordUsage(brainDir, {
627
+ provider: opts.provider || 'unknown',
628
+ model: opts.model,
629
+ input_tokens: opts.input_tokens,
630
+ output_tokens: opts.output_tokens,
631
+ cost_usd: opts.cost,
632
+ key_ref: opts.key_ref,
633
+ source: 'cli',
634
+ });
635
+ console.log(` โœ… Usage recorded: ${JSON.stringify(row)}`);
636
+ return;
637
+ }
638
+
639
+ if (opts.command === 'export-env' || opts.command === 'sync-env' || opts.command === 'apply-env') {
640
+ const exportOpts = {
641
+ filename: opts.filename || '.env',
642
+ example: !opts.noExample,
643
+ dryRun: opts.dryRun,
644
+ includeGlobal: !opts.noGlobal,
645
+ replaceAll: !!opts.replaceAll,
646
+ keys: opts.keys.length ? opts.keys : undefined,
647
+ };
648
+
649
+ if (opts.allProjects) {
650
+ const results = await exportEnvToRegistry(brainDir, exportOpts);
651
+ console.log(`\n ๐Ÿ“ค export-env โ†’ ${results.length} project(s) from ${resolveSecretsPath(brainDir)}\n`);
652
+ for (const r of results) {
653
+ if (!r.ok) {
654
+ console.log(` โœ— ${r.name || r.path}: ${r.error}`);
655
+ } else if (r.dryRun) {
656
+ console.log(` ยท ${r.name || r.projectSlug}: would write ${r.count} keys โ†’ ${r.envPath}`);
657
+ } else {
658
+ console.log(` โœ“ ${r.name || r.projectSlug}: ${r.count} keys โ†’ ${r.envPath}`);
659
+ }
660
+ }
661
+ console.log('');
662
+ return;
663
+ }
664
+
665
+ const targetPath = opts.path || opts.project || process.cwd();
666
+ const result = await exportEnvToProject(brainDir, targetPath, exportOpts);
667
+ if (result.dryRun) {
668
+ console.log(`\n ๐Ÿ“ค dry-run export-env`);
669
+ console.log(` store: ${result.store}`);
670
+ console.log(` would write ${result.count} keys โ†’ ${result.envPath}`);
671
+ if (result.keys.length) {
672
+ for (const k of result.keys) console.log(` โ€ข ${k}`);
673
+ }
674
+ console.log('');
675
+ return;
676
+ }
677
+ console.log(`\n โœ… Exported ${result.count} secret(s) โ†’ ${result.envPath}`);
678
+ if (result.examplePath) console.log(` example: ${result.examplePath}`);
679
+ console.log(` store: ${result.store}`);
680
+ console.log(` (values written with mode 0600; not printed)\n`);
681
+ return;
682
+ }
683
+
684
+ if (opts.command === 'import-env') {
685
+ const existing = await listSecretsMeta(brainDir);
686
+ const existingSet = new Set(existing.map((e) => e.key));
687
+ let scan = scanEnvSources({ brainDir, includeProcessEnv: true, cwd: process.cwd() });
688
+
689
+ if (opts.file) {
690
+ if (!fs.existsSync(opts.file)) {
691
+ console.error(` File not found: ${opts.file}`);
692
+ process.exit(1);
693
+ }
694
+ const text = fs.readFileSync(opts.file, 'utf8');
695
+ const map = parseEnvText(text);
696
+ // merge file into scan candidates via import pairs path
697
+ const pub = publicScanResult(scan, existingSet);
698
+ console.log(`\n ๐Ÿ” Env scan (${pub.count} candidates)\n`);
699
+ for (const c of pub.candidates) {
700
+ console.log(
701
+ ` ${c.already_set ? 'ยท' : '+'} ${c.key.padEnd(32)} ${c.masked.padEnd(28)} ${c.source_label}`,
702
+ );
703
+ }
704
+ if (Object.keys(map).length) {
705
+ console.log(`\n Also loading --file ${opts.file} (${Object.keys(map).length} vars)`);
706
+ }
707
+ if (opts.dryRun) {
708
+ console.log('\n Dry-run โ€” nothing written.\n');
709
+ return;
710
+ }
711
+ const keys = opts.keys.length
712
+ ? opts.keys
713
+ : opts.all
714
+ ? undefined
715
+ : pub.candidates.filter((c) => !c.already_set).map((c) => c.key);
716
+ const result = await importEnvSecrets(brainDir, {
717
+ pairs: map,
718
+ keys: opts.keys.length ? opts.keys : undefined,
719
+ all: opts.all || (!opts.keys.length && !Object.keys(map).length),
720
+ overwrite: opts.overwrite,
721
+ actor: 'cli-import-env',
722
+ });
723
+ // If --file provided, also import matching keys from file pairs
724
+ if (Object.keys(map).length) {
725
+ const fileResult = await importEnvSecrets(brainDir, {
726
+ pairs: map,
727
+ keys: opts.keys.length ? opts.keys : Object.keys(map),
728
+ overwrite: opts.overwrite,
729
+ actor: 'cli-import-env',
730
+ });
731
+ console.log(
732
+ `\n โœ… Imported ${fileResult.imported_count} from file, skipped ${fileResult.skipped_count}\n`,
733
+ );
734
+ return;
735
+ }
736
+ console.log(
737
+ `\n โœ… Imported ${result.imported_count}, skipped ${result.skipped_count}, errors ${result.errors.length}\n`,
738
+ );
739
+ return;
740
+ }
741
+
742
+ const pub = publicScanResult(scan, existingSet);
743
+ console.log(`\n ๐Ÿ” Env scan โ€” ${pub.count} candidate secrets\n`);
744
+ for (const c of pub.candidates) {
745
+ const mark = c.already_set ? 'ยท' : '+';
746
+ console.log(
747
+ ` ${mark} ${c.key.padEnd(32)} ${c.masked.padEnd(28)} ${c.provider || ''} ${c.source_label}`,
748
+ );
749
+ }
750
+ if (!pub.count) {
751
+ console.log(' No known API keys found in process.env or common .env paths.\n');
752
+ return;
753
+ }
754
+ if (opts.dryRun || (!opts.all && !opts.keys.length)) {
755
+ console.log(
756
+ '\n Dry-run / preview. Import with:\n npx total-recall secret import-env --all\n npx total-recall secret import-env OPENAI_API_KEY ANTHROPIC_API_KEY\n',
757
+ );
758
+ return;
759
+ }
760
+ const result = await importEnvSecrets(brainDir, {
761
+ keys: opts.keys.length ? opts.keys : undefined,
762
+ all: opts.all || !opts.keys.length,
763
+ overwrite: opts.overwrite,
764
+ actor: 'cli-import-env',
765
+ });
766
+ console.log(`\n โœ… Imported ${result.imported_count}`);
767
+ if (result.skipped_count) console.log(` skipped (already set): ${result.skipped_count}`);
768
+ for (const e of result.errors) console.error(` error ${e.key}: ${e.error}`);
769
+ console.log(` store: ${resolveSecretsPath(brainDir)}\n`);
770
+ return;
771
+ }
772
+
773
+ if (opts.command === 'check-surfaces') {
774
+ const texts = [];
775
+ const candidates = [
776
+ path.join(brainDir, 'INSTRUCTIONS.md'),
777
+ path.join(process.cwd(), 'INSTRUCTIONS.md'),
778
+ path.join(brainDir, 'openwiki'),
779
+ ];
780
+ for (const c of candidates) {
781
+ if (!fs.existsSync(c)) continue;
782
+ if (fs.statSync(c).isDirectory()) {
783
+ for (const f of fs.readdirSync(c)) {
784
+ if (f.endsWith('.md')) {
785
+ texts.push({ file: path.join(c, f), body: fs.readFileSync(path.join(c, f), 'utf8') });
786
+ }
787
+ }
788
+ } else {
789
+ texts.push({ file: c, body: fs.readFileSync(c, 'utf8') });
790
+ }
791
+ }
792
+ let bad = false;
793
+ for (const t of texts) {
794
+ const r = await textContainsSecrets(brainDir, t.body);
795
+ if (r.leak) {
796
+ bad = true;
797
+ console.error(` โŒ LEAK in ${t.file}: keys ${r.keys.join(', ')}`);
798
+ }
799
+ }
800
+ if (bad) process.exit(1);
801
+ console.log(` โœ… No secret values found in ${texts.length} surface/openwiki file(s)`);
802
+ return;
803
+ }
804
+
805
+ console.error(`Unknown secret command: ${opts.command}`);
806
+ printHelp();
807
+ process.exit(1);
808
+ } catch (err) {
809
+ console.error(`โŒ ${err.message}`);
810
+ process.exit(1);
811
+ }
812
+ }