total-recall-brain 3.21.2 โ†’ 3.22.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 (81) hide show
  1. package/frontend/dist/assets/index-C7_Xpk27.css +1 -0
  2. package/frontend/dist/assets/{index-D0XeeXhG.js โ†’ index-DJjuZ3t3.js} +106 -106
  3. package/frontend/dist/index.html +2 -2
  4. package/frontend/src/App.spec.tsx +1 -1
  5. package/frontend/src/api/_base.ts +3 -3
  6. package/frontend/src/api/embeddings.ts +46 -0
  7. package/frontend/src/api/mesh.ts +47 -0
  8. package/frontend/src/api/network.spec.ts +9 -9
  9. package/frontend/src/api/network.ts +4 -4
  10. package/frontend/src/api/notifications.ts +5 -4
  11. package/frontend/src/api/webhooks.ts +1 -1
  12. package/frontend/src/components/BrainSelector.spec.tsx +4 -4
  13. package/frontend/src/components/BrainSelector.tsx +1 -1
  14. package/frontend/src/components/ContextualHelp.spec.tsx +14 -14
  15. package/frontend/src/components/ContextualHelp.tsx +3 -1
  16. package/frontend/src/components/DocumentTable.spec.tsx +1 -1
  17. package/frontend/src/components/EmbeddingProviderPanel.spec.tsx +127 -0
  18. package/frontend/src/components/EmbeddingProviderPanel.tsx +177 -0
  19. package/frontend/src/components/Graph3D.spec.tsx +4 -4
  20. package/frontend/src/components/Graph3D.tsx +3 -3
  21. package/frontend/src/components/MobilePairing.spec.tsx +1 -1
  22. package/frontend/src/components/MobilePairing.tsx +1 -1
  23. package/frontend/src/components/UsageChart.spec.tsx +4 -4
  24. package/frontend/src/components/UsageChart.tsx +1 -1
  25. package/frontend/src/components/VoiceInput.spec.tsx +6 -6
  26. package/frontend/src/components/VoiceInput.tsx +19 -4
  27. package/frontend/src/components/brand/BrandMark.spec.tsx +3 -4
  28. package/frontend/src/index.css +6 -0
  29. package/frontend/src/pages/AutomationsPage.spec.tsx +4 -4
  30. package/frontend/src/pages/ChatPage.spec.tsx +3 -3
  31. package/frontend/src/pages/CollabPage.spec.tsx +1 -2
  32. package/frontend/src/pages/DesignDocsPage.spec.tsx +1 -1
  33. package/frontend/src/pages/FilesPage.spec.tsx +2 -2
  34. package/frontend/src/pages/GraphPage.spec.tsx +1 -1
  35. package/frontend/src/pages/HealthPage.spec.tsx +2 -2
  36. package/frontend/src/pages/HelpPage.spec.tsx +5 -5
  37. package/frontend/src/pages/InboxPage.spec.tsx +4 -4
  38. package/frontend/src/pages/InstructionsPage.spec.tsx +5 -5
  39. package/frontend/src/pages/IntegrationsPage.spec.tsx +7 -7
  40. package/frontend/src/pages/MemoryPage.spec.tsx +2 -2
  41. package/frontend/src/pages/MeshPage.spec.tsx +111 -2
  42. package/frontend/src/pages/MeshPage.tsx +95 -11
  43. package/frontend/src/pages/NetworkPage.spec.tsx +7 -8
  44. package/frontend/src/pages/NetworkPage.tsx +12 -12
  45. package/frontend/src/pages/NotificationsPage.tsx +9 -9
  46. package/frontend/src/pages/OnboardingPage.spec.tsx +7 -7
  47. package/frontend/src/pages/OpenWikiPage.tsx +3 -3
  48. package/frontend/src/pages/RulesPage.tsx +3 -3
  49. package/frontend/src/pages/SecretsPage.spec.tsx +5 -5
  50. package/frontend/src/pages/SecretsPage.tsx +13 -14
  51. package/frontend/src/pages/SettingsPage.spec.tsx +6 -6
  52. package/frontend/src/pages/SettingsPage.tsx +2 -2
  53. package/frontend/src/pages/SkillsPage.spec.tsx +1 -2
  54. package/frontend/src/pages/SkillsPage.tsx +3 -3
  55. package/frontend/src/pages/TasksPage.spec.tsx +3 -3
  56. package/frontend/src/pages/UsagePage.spec.tsx +2 -2
  57. package/frontend/src/pages/UsagePage.tsx +1 -1
  58. package/frontend/src/pages/WebhooksPage.spec.tsx +1 -1
  59. package/frontend/src/pages/WebhooksPage.tsx +19 -19
  60. package/frontend/src/setupTests.ts +4 -4
  61. package/package.json +1 -1
  62. package/scaffold/.agent/skills/total-recall/SKILL.md +92 -0
  63. package/scaffold/.agent/skills/total-recall/modules/agents/agents.yml +15 -13
  64. package/scaffold/.agent/skills/total-recall/research-queue.jsonl +0 -1
  65. package/src/cli/secret.mjs +93 -12
  66. package/src/core/browser-session.mjs +185 -0
  67. package/src/core/daemon-loop.mjs +28 -1
  68. package/src/core/embeddings.mjs +52 -10
  69. package/src/core/headscale-client.mjs +227 -0
  70. package/src/core/mesh-enroll.mjs +439 -0
  71. package/src/core/ollama-embeddings.mjs +336 -0
  72. package/src/core/provider-catalog.mjs +175 -6
  73. package/src/core/provider-rotation-recipes.mjs +299 -0
  74. package/src/core/rotation-capability.mjs +189 -0
  75. package/src/core/secrets-rotate.mjs +192 -0
  76. package/src/server/rest.mjs +2 -0
  77. package/src/server/route-manifest.json +20 -0
  78. package/src/server/routes/embeddings.mjs +111 -0
  79. package/src/server/routes/headscale.mjs +21 -81
  80. package/src/server/routes/mesh.mjs +41 -0
  81. package/frontend/dist/assets/index-B4RASWbV.css +0 -1
@@ -18,18 +18,18 @@ describe('SettingsPage', () => {
18
18
  budget: { budget: {} },
19
19
  brain: {},
20
20
  secrets: {},
21
- } as any);
21
+ } as never);
22
22
  vi.mocked(api.fetchHealth).mockResolvedValue({
23
23
  status: 'healthy',
24
24
  version: '1.0.0',
25
25
  cli_agents: ['claude'],
26
- } as any);
26
+ } as never);
27
27
  vi.mocked(api.checkUpdate).mockResolvedValue({
28
28
  updateAvailable: false,
29
29
  currentVersion: '1.0.0',
30
30
  latestVersion: '1.0.0',
31
- } as any);
32
- vi.mocked(api.triggerRecompile).mockResolvedValue({ success: true, message: 'ok' } as any);
31
+ } as never);
32
+ vi.mocked(api.triggerRecompile).mockResolvedValue({ success: true, message: 'ok' } as never);
33
33
 
34
34
  await act(async () => {
35
35
  render(<SettingsPage />);
@@ -43,8 +43,8 @@ describe('SettingsPage', () => {
43
43
  it('shows error + retry when config fails', async () => {
44
44
  vi.spyOn(console, 'error').mockImplementation(() => {});
45
45
  vi.mocked(api.fetchConfigJson).mockRejectedValue(new Error('Config JSON API error: 500'));
46
- vi.mocked(api.fetchHealth).mockResolvedValue({ status: 'healthy' } as any);
47
- vi.mocked(api.checkUpdate).mockResolvedValue({ updateAvailable: false } as any);
46
+ vi.mocked(api.fetchHealth).mockResolvedValue({ status: 'healthy' } as never);
47
+ vi.mocked(api.checkUpdate).mockResolvedValue({ updateAvailable: false } as never);
48
48
  vi.mocked(api.fetchBrains).mockResolvedValue([]);
49
49
 
50
50
  await act(async () => {
@@ -15,7 +15,7 @@ import type { ConfigJson } from '../types';
15
15
  import { MobilePairing } from '../components/MobilePairing';
16
16
 
17
17
  function normalizeConfigJson(data: ConfigJson): ConfigJson {
18
- const next = { ...data } as ConfigJson & Record<string, any>;
18
+ const next = { ...data } as ConfigJson & Record<string, unknown>;
19
19
  if (!next.security) next.security = {};
20
20
  if (!next.security.dashboard) next.security.dashboard = {};
21
21
  if (!next.security.api) next.security.api = {};
@@ -79,7 +79,7 @@ export default function SettingsPage({ activeBrainId }: { activeBrainId?: string
79
79
  }),
80
80
  )
81
81
  .catch(console.error);
82
- loadConfig();
82
+ setTimeout(() => { void loadConfig(); }, 0);
83
83
  }, [activeBrainId]);
84
84
 
85
85
 
@@ -1,4 +1,3 @@
1
- // @ts-nocheck
2
1
  import { describe, it, expect, vi, beforeEach } from 'vitest';
3
2
  import { render, screen } from '@testing-library/react';
4
3
  import SkillsPage from './SkillsPage';
@@ -10,7 +9,7 @@ describe('SkillsPage', () => {
10
9
  beforeEach(() => {
11
10
  vi.resetAllMocks();
12
11
  vi.mocked(api.listSkills).mockResolvedValue([
13
- { name: 'test-skill', description: 'A test skill', repo: 'test' }
12
+ { name: 'test-skill', description: 'A test skill', repo: 'test' } as any
14
13
  ]);
15
14
  });
16
15
 
@@ -327,7 +327,7 @@ export default function SkillsPage({ activeBrainId }: { activeBrainId?: string }
327
327
  try {
328
328
  const skillDetails = await fetchSkill(target.name, target.repo);
329
329
  setSkillContent(skillDetails.content || '');
330
- } catch (err) {
330
+ } catch {
331
331
  console.warn(`Could not load SKILL.md for ${target.name}`);
332
332
  setSkillContent('');
333
333
  }
@@ -633,8 +633,8 @@ Configure triggers, options, and prompts inside this rules sheet to hot-recompil
633
633
  setError(res.message || 'Failed to generate repo-expert');
634
634
  }
635
635
  setTimeout(() => setSuccess(null), 5000);
636
- } catch (err: any) {
637
- setError(err.message || 'Error generating repo-expert');
636
+ } catch (err) {
637
+ setError(err instanceof Error ? err.message : 'Error generating repo-expert');
638
638
  } finally {
639
639
  void fetchSkillsList(selectedSkill?.name, selectedSkill?.repo);
640
640
  }
@@ -11,9 +11,9 @@ describe('TasksPage', () => {
11
11
  });
12
12
 
13
13
  it('renders without crashing', async () => {
14
- vi.mocked(api.listTasks).mockResolvedValue({ items: [] } as any);
15
- vi.mocked(api.listResearch).mockResolvedValue({ items: [] } as any);
16
- vi.mocked(api.fetchLogs).mockResolvedValue({ content: '' } as any);
14
+ vi.mocked(api.listTasks).mockResolvedValue({ items: [] } as never);
15
+ vi.mocked(api.listResearch).mockResolvedValue({ items: [] } as never);
16
+ vi.mocked(api.fetchLogs).mockResolvedValue({ content: '' } as never);
17
17
  render(<TasksPage />);
18
18
  expect(screen.getByText(/Cognitive Dashboard/i)).toBeInTheDocument();
19
19
  });
@@ -17,9 +17,9 @@ describe('UsagePage', () => {
17
17
  breakdown: {
18
18
  gemini: { dailyUsd: 1.0, weeklyUsd: 5.0, dailyTokens: 100, weeklyTokens: 500 }
19
19
  }
20
- } as any);
20
+ } as never);
21
21
 
22
- vi.mocked(api.fetchConfigJson).mockResolvedValue({} as any);
22
+ vi.mocked(api.fetchConfigJson).mockResolvedValue({} as never);
23
23
 
24
24
  await act(async () => {
25
25
  render(<UsagePage />);
@@ -495,7 +495,7 @@ export default function UsagePage() {
495
495
  <Tooltip
496
496
  contentStyle={{ background: '#1a1a2e', border: '1px solid rgba(255,255,255,0.1)', borderRadius: 8, fontSize: 12, color: '#e8e8e8' }}
497
497
  labelStyle={{ color: '#fff', fontWeight: 600, marginBottom: 4 }}
498
- formatter={(value: any, name: any) => [`$${Number(value).toFixed(3)}`, String(name).charAt(0).toUpperCase() + String(name).slice(1)]}
498
+ formatter={(value: unknown, name: unknown) => [`$${Number(value).toFixed(3)}`, String(name).charAt(0).toUpperCase() + String(name).slice(1)]}
499
499
  />
500
500
  <Legend wrapperStyle={{ fontSize: 12, color: 'var(--text-secondary)' }} />
501
501
  <Area type="monotone" dataKey="gemini" stackId="1" stroke="#3b82f6" fill="url(#gradGemini)" strokeWidth={2} />
@@ -71,7 +71,7 @@ describe('WebhooksPage', () => {
71
71
  it('runs through the Add Webhook wizard', async () => {
72
72
  vi.mocked(fetchWebhookConfigs).mockResolvedValue([]);
73
73
  vi.mocked(fetchWebhookEvents).mockResolvedValue([]);
74
- vi.mocked(addWebhookConfig).mockResolvedValue({} as any);
74
+ vi.mocked(addWebhookConfig).mockResolvedValue({} as never);
75
75
 
76
76
  render(<WebhooksPage />);
77
77
 
@@ -1,4 +1,4 @@
1
- import { useEffect, useState } from 'react';
1
+ import { useEffect, useState, useCallback } from 'react';
2
2
  import {
3
3
  fetchWebhookConfigs,
4
4
  fetchWebhookEvents,
@@ -11,7 +11,7 @@ import type { WebhookConfig, WebhookEvent } from '../api/webhooks';
11
11
  import './WebhooksPage.css';
12
12
 
13
13
  // Expandable JSON Viewer Component
14
- function JsonViewer({ data }: { data: any }) {
14
+ function JsonViewer({ data }: { data: unknown }) {
15
15
  const [expanded, setExpanded] = useState(false);
16
16
 
17
17
  if (!data) return null;
@@ -42,32 +42,32 @@ export function WebhooksPage() {
42
42
  const [wizardStep, setWizardStep] = useState(1);
43
43
  const [newConfig, setNewConfig] = useState<Partial<WebhookConfig>>({ status: 'active', events: [] });
44
44
 
45
- async function loadData() {
45
+ const loadData = useCallback(async () => {
46
46
  setLoading(true);
47
47
  setError(null);
48
48
  try {
49
49
  const [c, e] = await Promise.all([fetchWebhookConfigs(), fetchWebhookEvents(filterProvider || undefined)]);
50
50
  setConfigs(c);
51
51
  setEvents(e);
52
- } catch (err: any) {
53
- setError(err.message || 'Failed to load webhooks data');
52
+ } catch (err) {
53
+ setError(err instanceof Error ? err.message : 'Failed to load webhooks data');
54
54
  } finally {
55
55
  setLoading(false);
56
56
  }
57
- }
57
+ }, [filterProvider]);
58
58
 
59
59
  useEffect(() => {
60
- loadData();
61
- const interval = setInterval(loadData, 10000);
60
+ setTimeout(() => { void loadData() }, 0);
61
+ const interval = setInterval(() => { void loadData() }, 10000);
62
62
  return () => clearInterval(interval);
63
- }, [filterProvider]);
63
+ }, [loadData]);
64
64
 
65
65
  async function handleTest(provider: string) {
66
66
  try {
67
67
  await triggerTestWebhook(provider);
68
68
  await loadData();
69
- } catch (err: any) {
70
- setError(err.message || 'Failed to trigger test webhook');
69
+ } catch (err) {
70
+ setError(err instanceof Error ? err.message : 'Failed to trigger test webhook');
71
71
  }
72
72
  }
73
73
 
@@ -75,8 +75,8 @@ export function WebhooksPage() {
75
75
  try {
76
76
  await redeliverWebhookEvent(eventId);
77
77
  await loadData();
78
- } catch (err: any) {
79
- setError(err.message || 'Failed to re-deliver webhook event');
78
+ } catch (err) {
79
+ setError(err instanceof Error ? err.message : 'Failed to re-deliver webhook event');
80
80
  }
81
81
  }
82
82
 
@@ -88,8 +88,8 @@ export function WebhooksPage() {
88
88
  setWizardStep(1);
89
89
  setNewConfig({ status: 'active', events: [] });
90
90
  await loadData();
91
- } catch (err: any) {
92
- setError(err.message || 'Failed to add webhook');
91
+ } catch (err) {
92
+ setError(err instanceof Error ? err.message : 'Failed to add webhook');
93
93
  }
94
94
  }
95
95
 
@@ -98,8 +98,8 @@ export function WebhooksPage() {
98
98
  try {
99
99
  await deleteWebhookConfig(provider);
100
100
  await loadData();
101
- } catch (err: any) {
102
- setError(err.message || 'Failed to delete webhook');
101
+ } catch (err) {
102
+ setError(err instanceof Error ? err.message : 'Failed to delete webhook');
103
103
  }
104
104
  }
105
105
 
@@ -111,8 +111,8 @@ export function WebhooksPage() {
111
111
  .join('');
112
112
  await addWebhookConfig({ provider, status: 'active', secret: newSecret });
113
113
  await loadData();
114
- } catch (err: any) {
115
- setError(err.message || 'Failed to rotate secret');
114
+ } catch (err) {
115
+ setError(err instanceof Error ? err.message : 'Failed to rotate secret');
116
116
  }
117
117
  }
118
118
 
@@ -7,10 +7,10 @@ afterEach(() => {
7
7
  });
8
8
 
9
9
  class MockEventSource {
10
- constructor(_url: string) {}
10
+ constructor(_url: string) { void _url; }
11
11
  close() {}
12
- onmessage: ((event: any) => void) | null = null;
13
- onerror: ((event: any) => void) | null = null;
12
+ onmessage: ((event: unknown) => void) | null = null;
13
+ onerror: ((event: unknown) => void) | null = null;
14
14
  }
15
15
  if (typeof window !== 'undefined') {
16
16
  Object.defineProperty(window, 'EventSource', {
@@ -28,7 +28,7 @@ class MockResizeObserver {
28
28
  unobserve() {}
29
29
  disconnect() {}
30
30
  }
31
- (globalThis as any).ResizeObserver = MockResizeObserver;
31
+ (globalThis as typeof globalThis & { ResizeObserver: unknown }).ResizeObserver = MockResizeObserver;
32
32
 
33
33
  // Mock localStorage
34
34
  const localStorageMock = (function () {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "total-recall-brain",
3
- "version": "3.21.2",
3
+ "version": "3.22.0",
4
4
  "description": "Personal AI system โ€” database-free, SSSS-powered intelligence engine deployable to any POSIX host.",
5
5
  "type": "module",
6
6
  "workspaces": [
@@ -315,6 +315,98 @@ curl -H "Authorization: Bearer <YOUR_PAT_TOKEN>" \
315
315
 
316
316
  ---
317
317
 
318
+ ## ๐Ÿ” SECRETS STORE (separate from the memory vault โ€” `secret` subcommand)
319
+
320
+ API keys and credentials are **not** memory nodes. They live encrypted at
321
+ `<brain>/config/secrets.enc`, managed exclusively via `npx total-recall secret
322
+ <cmd>` โ€” never write them into vault markdown, openwiki, or a compiled
323
+ INSTRUCTIONS.md/skill surface (`secret check-surfaces` fails the build if one
324
+ ever leaks in). Full reference: `npx total-recall secret --help`.
325
+
326
+ ```bash
327
+ npx total-recall secret set <key> <value> # store (0600, optional AES)
328
+ npx total-recall secret rotate <key> <new-value> # replace + mark rotated
329
+ npx total-recall secret get <key> # print value (audited)
330
+ npx total-recall secret list # metadata only, no values
331
+ npx total-recall secret export-env --path <repo> # SSOT โ†’ local .env.local
332
+ ```
333
+
334
+ ### Executed rotation โ€” `rotate-auto` / `rotate-browser` (added 3.22.0)
335
+
336
+ Rotation used to only ever **print instructions**. It now executes. Every key
337
+ resolves to one of four rotation classes, so nothing is silently unrotatable:
338
+
339
+ | Class | How it rotates | Human needed? |
340
+ |---|---|---|
341
+ | `self_generated` | TR mints the value itself (`JWT_SECRET`, `DB_PASSWORD`, `*_WEBHOOK_SECRET`) | **No** |
342
+ | `provider_browser` | TR drives the provider console in its own browser profile | Login/2FA once |
343
+ | `provider_api` | Provider key-management API | No |
344
+ | `manual` | Human-only artifact (SSH keys, recovery codes, OAuth app secrets) | Yes, with reason |
345
+
346
+ ```bash
347
+ npx total-recall secret rotation-status # coverage for every key
348
+ npx total-recall secret rotation-status --json
349
+ npx total-recall secret rotate-auto <key> # pick the right method automatically
350
+ npx total-recall secret rotate-browser <key> # force the console path
351
+ npx total-recall secret rotate-browser <key> --print-only # legacy: just print steps
352
+ npx total-recall secret browser-logout # wipe TR's browser profile
353
+ ```
354
+
355
+ **Browser profile.** Console rotation uses a persistent Chromium profile at
356
+ `<brain>/browser-profile` (created `0700`). You log in once per provider; the
357
+ daemon can then rotate unattended. That directory holds live Stripe/GitHub
358
+ sessions โ€” treat it as **equally sensitive to `secrets.enc`**: never sync it,
359
+ never back it up off-machine, and `browser-logout` when you're done.
360
+
361
+ **Safety invariants** (enforced by `rotation-capability.spec.mjs`):
362
+
363
+ - The new value is read from the browser's own clipboard **inside TR's process** โ€”
364
+ it never transits an argv string, an env var, or a chat transcript.
365
+ - Only recipes marked `verified: true` auto-click. Unverified providers run
366
+ **supervised** โ€” TR opens the console, tells you what to click, and detects the
367
+ result. It never blind-clicks guessed selectors on a billing dashboard.
368
+ - A captured value is shape-checked against the provider pattern and, where an
369
+ API probe exists, **verified to authenticate before it replaces the old one**.
370
+ A failed check stores nothing.
371
+ - The old credential is never revoked until the new one is stored and exported.
372
+
373
+ ### Remote (production) deploy โ€” `secret remote` (added 3.21.1)
374
+
375
+ `export-env` only ever wrote a **local** file โ€” there was no path from the
376
+ SSOT to an actual production host. `secret remote` closes that gap over SSH,
377
+ entirely generically: every host/path/restart-command is declared per-repo in
378
+ that repo's own `<brain>/config/remote-targets.json`, never hardcoded in
379
+ Total Recall core.
380
+
381
+ ```bash
382
+ # One-time: register a target (per repo, per environment)
383
+ npx total-recall secret remote add production \
384
+ --host <ip-or-hostname> --path </remote/dir> \
385
+ --filename .env.local --restart-cmd "pm2 restart <app> --update-env"
386
+
387
+ npx total-recall secret remote list # show configured targets
388
+ npx total-recall secret remote deploy production # push current SSOT now
389
+ npx total-recall secret remote deploy production --dry-run
390
+
391
+ # Steady state: rotate AND ship to prod AND restart, in one call
392
+ npx total-recall secret rotate SMTP2GO_API_KEY "<new-value>" --remote production
393
+ ```
394
+
395
+ Secret values travel over SSH **stdin only** โ€” never as an argv string or in
396
+ a shell-interpolated command โ€” written via a remote temp-file + atomic
397
+ rename so a dropped connection can't leave a truncated secrets file live,
398
+ then `chmod 600`. This is also why a value should never be pasted into chat
399
+ to get it into production: paste it straight into `secret set`/`secret
400
+ rotate` in your own terminal, or โ€” if a provider's API supports minting a
401
+ new key from an existing one (e.g. SMTP2GO `POST /api_keys/add`) โ€” mint the
402
+ replacement programmatically and it never has to be typed anywhere at all.
403
+ When self-minting a replacement key, scope it to include the provider's own
404
+ key-management endpoints (not just its functional one) so it can service
405
+ its *own* next rotation too โ€” otherwise you've built a one-shot capability
406
+ that dead-ends back at a manual dashboard step.
407
+
408
+ ---
409
+
318
410
  <!-- BEGIN INJECTED MEMORY: do not edit by hand; rebuilt by total-recall surface -->
319
411
  <!-- @route: tfidf, generated_at: 2026-05-21T03:34:14.837Z -->
320
412
 
@@ -10,10 +10,10 @@ agents:
10
10
  enabled: true
11
11
  exec: flag
12
12
 
13
- # `-p` MUST be present (non-interactive requires --print) and MUST be LAST:
14
- # `--tools` is variadic (`<tools...>`) in the shipped build, so anything after
15
- # it โ€” including the appended prompt โ€” is swallowed as a tool name. Proven
16
- # both ways on claude v2.1.201.
13
+ # `-p` MUST be present (non-interactive requires --print) and MUST be the LAST
14
+ # flag: `--tools` is variadic (`<tools...>`) in the shipped build, so anything
15
+ # after it โ€” including the appended prompt โ€” is swallowed as a tool name.
16
+ # Proven both ways on claude v2.1.201.
17
17
  - name: claude
18
18
  binary: claude
19
19
  flags: --output-format json --permission-mode bypassPermissions --setting-sources local --tools "" -p
@@ -23,9 +23,10 @@ agents:
23
23
 
24
24
  # `-m` is REQUIRED: ~/.codex/config.toml may pin a model the account cannot
25
25
  # use (seen: gpt-5.6-sol -> HTTP 400 "not supported when using Codex with a
26
- # ChatGPT account"). Account-supported slugs: ~/.codex/models_cache.json.
26
+ # ChatGPT account"). Overriding per-dispatch avoids editing global config.
27
+ # Account-supported slugs are listed in ~/.codex/models_cache.json.
27
28
  # `--full-auto` is hidden from --help but still works as a deprecated alias;
28
- # the explicit `--sandbox` form is OpenAI's current recommendation.
29
+ # the explicit `--sandbox` form is what OpenAI now recommends for scripts.
29
30
  - name: codex
30
31
  binary: codex
31
32
  flags: -m gpt-5.5 --sandbox workspace-write --json --skip-git-repo-check
@@ -33,10 +34,11 @@ agents:
33
34
  enabled: true
34
35
  exec: subcommand
35
36
 
36
- # DISABLED โ€” `antigravity` on PATH is total-recall/bin/antigravity.mjs, a thin
37
- # wrapper that POSTs to generativelanguage.googleapis.com with GOOGLE_API_KEY,
38
- # i.e. the METERED Gemini API, not the Ultra plan. Verified 2026-08-01: every
39
- # call returned 429 "You exceeded your current quota". Use `agy` above.
37
+ # DISABLED โ€” `antigravity` on PATH is bin/antigravity.mjs, a thin wrapper that
38
+ # POSTs to generativelanguage.googleapis.com with GOOGLE_API_KEY, i.e. the
39
+ # METERED Gemini API, not the Ultra plan. Verified 2026-08-01: every call
40
+ # returned 429 "You exceeded your current quota". Use `agy` above instead.
41
+ # Do not confuse the two names.
40
42
  - name: antigravity
41
43
  binary: antigravity
42
44
  flags: --sandbox=false --yolo -o json
@@ -44,8 +46,8 @@ agents:
44
46
  enabled: false
45
47
  exec: flag
46
48
 
47
- # DISABLED โ€” this user has no Gemini CLI. Do not enable because a `gemini`
48
- # binary resolves on PATH; their Google surface is `agy`.
49
+ # DISABLED โ€” this user has no Gemini CLI. Do not enable on the basis that a
50
+ # `gemini` binary resolves on PATH; their Google surface is `agy`.
49
51
  - name: gemini
50
52
  binary: gemini
51
53
  flags: --sandbox=false --yolo -o json
@@ -53,7 +55,7 @@ agents:
53
55
  enabled: false
54
56
  exec: flag
55
57
 
56
- # DISABLED โ€” not installed. A stale binary may still resolve at
58
+ # DISABLED โ€” not installed for this user. A stale binary may still resolve at
57
59
  # ~/.grok/bin/grok; PATH presence is not proof an agent works.
58
60
  - name: grok
59
61
  binary: grok
@@ -1 +0,0 @@
1
- {"id":"0efa65c2-0231-4267-a304-e0285e50011e","topic":"Total Recall repository improvement research: SSSS validated write pipeline, filesystem-native memory engines, local-first semantic indexing, Express 5 API hardening, 2026 agent memory architecture patterns","status":"pending","priority":"high","notes":"Queued from repo-wide analysis request on 2026-06-01. Focus on current best practices and meta-patterns for filesystem-native memory OS, validated operation envelopes, append-only logs, cache invalidation, research queue architecture, and secure local-first AI tools.","node_slug":null,"research_phase":"acquisition","created_at":"2026-06-01T06:07:40.102Z","updated_at":"2026-06-01T06:07:40.126Z","completed_at":null,"summary":{"id":"0efa65c2-0231-4267-a304-e0285e50011e","topic":"Total Recall repository improvement research: SSSS validated write pipeline, filesystem-native memory engines, local-first semantic indexing, Express 5 API hardening, 2026 agent memory architecture patterns","status":"pending","phase":"acquisition","last_updated":"2026-06-01T06:07:40.126Z","conclusions":[],"ongoing_directions":["Research project is enqueued / in progress in phase: acquisition"],"sources_count":0}}
@@ -29,6 +29,7 @@ import {
29
29
  updateSecretMeta,
30
30
  listRotationDue,
31
31
  getSharedValueHealth,
32
+ loadSecrets,
32
33
  } from '../core/secrets-store.mjs';
33
34
  import {
34
35
  scanEnvSources,
@@ -140,7 +141,13 @@ function printHelp() {
140
141
  --headscale-url <https-url>
141
142
  rotation-due List keys overdue for rotation
142
143
  --enqueue Create daemon tasks + browser-use prompts for each due key
143
- rotate-browser <key> Print supervised browser-use rotation instructions
144
+ rotate-browser <key> Drive the provider console in TR's browser and rotate
145
+ --print-only Only print instructions (legacy behaviour)
146
+ --headless Allow headless for verified recipes
147
+ rotate-auto <key> Rotate by whatever method the key's class supports
148
+ rotation-status Rotation coverage for every key in the vault
149
+ --json Machine-readable output
150
+ browser-logout Delete TR's persistent browser profile (all sessions)
144
151
  usage Cost/events (add --key-ref for per-key)
145
152
 
146
153
  rotate options:
@@ -239,9 +246,18 @@ function parseArgs(args) {
239
246
  }
240
247
  }
241
248
  if (
242
- ['set', 'get', 'rotate', 'delete', 'meta', 'rotate-browser', 'rm', 'generate', 'account-sync'].includes(
243
- out.command,
244
- ) &&
249
+ [
250
+ 'set',
251
+ 'get',
252
+ 'rotate',
253
+ 'delete',
254
+ 'meta',
255
+ 'rotate-browser',
256
+ 'rotate-auto',
257
+ 'rm',
258
+ 'generate',
259
+ 'account-sync',
260
+ ].includes(out.command) &&
245
261
  args[i] &&
246
262
  !args[i].startsWith('--')
247
263
  ) {
@@ -376,6 +392,15 @@ function parseArgs(args) {
376
392
  case '--enqueue':
377
393
  out.enqueue = true;
378
394
  break;
395
+ case '--print-only':
396
+ out.printOnly = true;
397
+ break;
398
+ case '--headless':
399
+ out.headless = true;
400
+ break;
401
+ case '--json':
402
+ out.json = true;
403
+ break;
379
404
  case '--bytes':
380
405
  out.bytes = parseInt(args[++i], 10) || 32;
381
406
  break;
@@ -945,17 +970,73 @@ export default async function secretCli(argv) {
945
970
  return;
946
971
  }
947
972
 
948
- if (opts.command === 'rotate-browser') {
973
+ if (opts.command === 'rotate-browser' || opts.command === 'rotate-auto') {
949
974
  if (!opts.key) {
950
- console.error('Usage: total-recall secret rotate-browser <key>');
975
+ console.error(`Usage: total-recall secret ${opts.command} <key>`);
976
+ process.exit(1);
977
+ }
978
+
979
+ if (opts.printOnly) {
980
+ const assist = await getBrowserRotateAssist(brainDir, opts.key);
981
+ console.log(`\n ๐ŸŒ Supervised browser rotation โ€” ${assist.key}\n`);
982
+ if (assist.console_url) console.log(` Console: ${assist.console_url}`);
983
+ if (assist.docs_url) console.log(` Docs: ${assist.docs_url}`);
984
+ console.log(` Overdue: ${assist.overdue ? 'YES' : 'no'}`);
985
+ console.log('\n' + assist.prompt + '\n');
986
+ return;
987
+ }
988
+
989
+ const { rotateViaBrowser, rotateAuto } = await import('../core/secrets-rotate.mjs');
990
+ const run = opts.command === 'rotate-auto' ? rotateAuto : rotateViaBrowser;
991
+ console.log(`\n ๐Ÿ” Rotating ${opts.key}โ€ฆ\n`);
992
+ const r = await run(brainDir, opts.key, {
993
+ headless: !!opts.headless,
994
+ exportEnv: opts.exportEnv !== false,
995
+ onStatus: (m) => console.log(` ยท ${m}`),
996
+ });
997
+
998
+ if (!r.ok) {
999
+ console.error(`\n โŒ ${r.error}`);
1000
+ if (r.console_url) console.error(` Console: ${r.console_url}`);
951
1001
  process.exit(1);
952
1002
  }
953
- const assist = await getBrowserRotateAssist(brainDir, opts.key);
954
- console.log(`\n ๐ŸŒ Supervised browser rotation โ€” ${assist.key}\n`);
955
- if (assist.console_url) console.log(` Console: ${assist.console_url}`);
956
- if (assist.docs_url) console.log(` Docs: ${assist.docs_url}`);
957
- console.log(` Overdue: ${assist.overdue ? 'YES' : 'no'}`);
958
- console.log('\n' + assist.prompt + '\n');
1003
+ console.log(`\n โœ… Rotated ${r.key}` + (r.method ? ` via ${r.method}` : ''));
1004
+ if (r.verified === true) console.log(' verified against provider API');
1005
+ if (r.supervised) console.log(' (supervised โ€” recipe not yet auto-verified)');
1006
+ if (r.exports?.length) console.log(` exported to ${r.exports.length} project(s)`);
1007
+ if (r.revoke_hint) console.log(`\n โš ๏ธ ${r.revoke_hint}`);
1008
+ return;
1009
+ }
1010
+
1011
+ if (opts.command === 'rotation-status') {
1012
+ const { planAll, summarizePlans } = await import('../core/rotation-capability.mjs');
1013
+ const all = await loadSecrets(brainDir);
1014
+ const keys = Object.keys(all || {}).filter((k) => k !== 'meta');
1015
+ const plans = planAll(keys, all.meta || {});
1016
+ const sum = summarizePlans(plans);
1017
+
1018
+ if (opts.json) {
1019
+ console.log(JSON.stringify({ summary: sum, plans }, null, 2));
1020
+ return;
1021
+ }
1022
+
1023
+ console.log(`\n ๐Ÿ”„ Rotation coverage โ€” ${sum.automatable}/${sum.total} automatable\n`);
1024
+ for (const [cls, n] of Object.entries(sum.byClass)) {
1025
+ console.log(` ${cls.padEnd(18)} ${n}`);
1026
+ }
1027
+ const manual = plans.filter((p) => !p.automatable && p.class !== 'non_secret');
1028
+ if (manual.length) {
1029
+ console.log(`\n Needs a human (${manual.length}):`);
1030
+ for (const p of manual) console.log(` ${p.key.padEnd(34)} ${p.reason}`);
1031
+ }
1032
+ console.log('');
1033
+ return;
1034
+ }
1035
+
1036
+ if (opts.command === 'browser-logout') {
1037
+ const { clearProfile } = await import('../core/browser-session.mjs');
1038
+ const r = clearProfile(brainDir);
1039
+ console.log(r.removed ? ` โœ… Cleared browser profile: ${r.path}` : ` (no profile at ${r.path})`);
959
1040
  return;
960
1041
  }
961
1042