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.
- package/frontend/dist/assets/index-C7_Xpk27.css +1 -0
- package/frontend/dist/assets/{index-D0XeeXhG.js โ index-DJjuZ3t3.js} +106 -106
- package/frontend/dist/index.html +2 -2
- package/frontend/src/App.spec.tsx +1 -1
- package/frontend/src/api/_base.ts +3 -3
- package/frontend/src/api/embeddings.ts +46 -0
- package/frontend/src/api/mesh.ts +47 -0
- package/frontend/src/api/network.spec.ts +9 -9
- package/frontend/src/api/network.ts +4 -4
- package/frontend/src/api/notifications.ts +5 -4
- package/frontend/src/api/webhooks.ts +1 -1
- package/frontend/src/components/BrainSelector.spec.tsx +4 -4
- package/frontend/src/components/BrainSelector.tsx +1 -1
- package/frontend/src/components/ContextualHelp.spec.tsx +14 -14
- package/frontend/src/components/ContextualHelp.tsx +3 -1
- package/frontend/src/components/DocumentTable.spec.tsx +1 -1
- package/frontend/src/components/EmbeddingProviderPanel.spec.tsx +127 -0
- package/frontend/src/components/EmbeddingProviderPanel.tsx +177 -0
- package/frontend/src/components/Graph3D.spec.tsx +4 -4
- package/frontend/src/components/Graph3D.tsx +3 -3
- package/frontend/src/components/MobilePairing.spec.tsx +1 -1
- package/frontend/src/components/MobilePairing.tsx +1 -1
- package/frontend/src/components/UsageChart.spec.tsx +4 -4
- package/frontend/src/components/UsageChart.tsx +1 -1
- package/frontend/src/components/VoiceInput.spec.tsx +6 -6
- package/frontend/src/components/VoiceInput.tsx +19 -4
- package/frontend/src/components/brand/BrandMark.spec.tsx +3 -4
- package/frontend/src/index.css +6 -0
- package/frontend/src/pages/AutomationsPage.spec.tsx +4 -4
- package/frontend/src/pages/ChatPage.spec.tsx +3 -3
- package/frontend/src/pages/CollabPage.spec.tsx +1 -2
- package/frontend/src/pages/DesignDocsPage.spec.tsx +1 -1
- package/frontend/src/pages/FilesPage.spec.tsx +2 -2
- package/frontend/src/pages/GraphPage.spec.tsx +1 -1
- package/frontend/src/pages/HealthPage.spec.tsx +2 -2
- package/frontend/src/pages/HelpPage.spec.tsx +5 -5
- package/frontend/src/pages/InboxPage.spec.tsx +4 -4
- package/frontend/src/pages/InstructionsPage.spec.tsx +5 -5
- package/frontend/src/pages/IntegrationsPage.spec.tsx +7 -7
- package/frontend/src/pages/MemoryPage.spec.tsx +2 -2
- package/frontend/src/pages/MeshPage.spec.tsx +111 -2
- package/frontend/src/pages/MeshPage.tsx +95 -11
- package/frontend/src/pages/NetworkPage.spec.tsx +7 -8
- package/frontend/src/pages/NetworkPage.tsx +12 -12
- package/frontend/src/pages/NotificationsPage.tsx +9 -9
- package/frontend/src/pages/OnboardingPage.spec.tsx +7 -7
- package/frontend/src/pages/OpenWikiPage.tsx +3 -3
- package/frontend/src/pages/RulesPage.tsx +3 -3
- package/frontend/src/pages/SecretsPage.spec.tsx +5 -5
- package/frontend/src/pages/SecretsPage.tsx +13 -14
- package/frontend/src/pages/SettingsPage.spec.tsx +6 -6
- package/frontend/src/pages/SettingsPage.tsx +2 -2
- package/frontend/src/pages/SkillsPage.spec.tsx +1 -2
- package/frontend/src/pages/SkillsPage.tsx +3 -3
- package/frontend/src/pages/TasksPage.spec.tsx +3 -3
- package/frontend/src/pages/UsagePage.spec.tsx +2 -2
- package/frontend/src/pages/UsagePage.tsx +1 -1
- package/frontend/src/pages/WebhooksPage.spec.tsx +1 -1
- package/frontend/src/pages/WebhooksPage.tsx +19 -19
- package/frontend/src/setupTests.ts +4 -4
- package/package.json +1 -1
- package/scaffold/.agent/skills/total-recall/SKILL.md +92 -0
- package/scaffold/.agent/skills/total-recall/modules/agents/agents.yml +15 -13
- package/scaffold/.agent/skills/total-recall/research-queue.jsonl +0 -1
- package/src/cli/secret.mjs +93 -12
- package/src/core/browser-session.mjs +185 -0
- package/src/core/daemon-loop.mjs +28 -1
- package/src/core/embeddings.mjs +52 -10
- package/src/core/headscale-client.mjs +227 -0
- package/src/core/mesh-enroll.mjs +439 -0
- package/src/core/ollama-embeddings.mjs +336 -0
- package/src/core/provider-catalog.mjs +175 -6
- package/src/core/provider-rotation-recipes.mjs +299 -0
- package/src/core/rotation-capability.mjs +189 -0
- package/src/core/secrets-rotate.mjs +192 -0
- package/src/server/rest.mjs +2 -0
- package/src/server/route-manifest.json +20 -0
- package/src/server/routes/embeddings.mjs +111 -0
- package/src/server/routes/headscale.mjs +21 -81
- package/src/server/routes/mesh.mjs +41 -0
- 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
|
|
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
|
|
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
|
|
32
|
-
vi.mocked(api.triggerRecompile).mockResolvedValue({ success: true, message: 'ok' } as
|
|
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
|
|
47
|
-
vi.mocked(api.checkUpdate).mockResolvedValue({ updateAvailable: false } as
|
|
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,
|
|
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
|
|
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
|
|
637
|
-
setError(err.message
|
|
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
|
|
15
|
-
vi.mocked(api.listResearch).mockResolvedValue({ items: [] } as
|
|
16
|
-
vi.mocked(api.fetchLogs).mockResolvedValue({ content: '' } as
|
|
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
|
|
20
|
+
} as never);
|
|
21
21
|
|
|
22
|
-
vi.mocked(api.fetchConfigJson).mockResolvedValue({} as
|
|
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:
|
|
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
|
|
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:
|
|
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
|
-
|
|
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
|
|
53
|
-
setError(err.message
|
|
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
|
-
}, [
|
|
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
|
|
70
|
-
setError(err.message
|
|
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
|
|
79
|
-
setError(err.message
|
|
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
|
|
92
|
-
setError(err.message
|
|
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
|
|
102
|
-
setError(err.message
|
|
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
|
|
115
|
-
setError(err.message
|
|
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:
|
|
13
|
-
onerror: ((event:
|
|
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
|
|
31
|
+
(globalThis as typeof globalThis & { ResizeObserver: unknown }).ResizeObserver = MockResizeObserver;
|
|
32
32
|
|
|
33
33
|
// Mock localStorage
|
|
34
34
|
const localStorageMock = (function () {
|
package/package.json
CHANGED
|
@@ -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
|
|
15
|
-
# it โ including the appended prompt โ is swallowed as a tool name.
|
|
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").
|
|
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
|
|
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
|
|
37
|
-
#
|
|
38
|
-
#
|
|
39
|
-
#
|
|
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
|
|
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}}
|
package/src/cli/secret.mjs
CHANGED
|
@@ -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>
|
|
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
|
-
[
|
|
243
|
-
|
|
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(
|
|
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
|
-
|
|
954
|
-
console.log(
|
|
955
|
-
if (
|
|
956
|
-
if (
|
|
957
|
-
console.log(
|
|
958
|
-
|
|
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
|
|