total-recall-brain 3.41.1 → 3.41.3

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 (105) hide show
  1. package/bin/total-recall.mjs +9 -3
  2. package/frontend/dist/assets/{index-BG9NkVUy.css → index-BIQmqDKP.css} +1 -1
  3. package/frontend/dist/assets/{index-BtuiZqwP.js → index-DFenQQEQ.js} +74 -79
  4. package/frontend/dist/index.html +2 -2
  5. package/frontend/dist/sw.js +8 -5
  6. package/frontend/src/api/auth.ts +0 -3
  7. package/frontend/src/components/plugins/PluginPanel.tsx +7 -3
  8. package/frontend/src/components/plugins/card/CardEngine.tsx +1 -1
  9. package/frontend/src/pages/LoginPage.tsx +1 -41
  10. package/frontend/src/pages/PluginsPage.tsx +14 -7
  11. package/package.json +2 -2
  12. package/scaffold/.agent/skills/plugin/scripts/record.mjs +6 -3
  13. package/scaffold/.agent/skills/total-recall/plugins/auth/README.md +10 -0
  14. package/scaffold/.agent/skills/total-recall/plugins/auth/cli.mjs +197 -31
  15. package/scaffold/.agent/skills/total-recall/plugins/auth/package-lock.json +1456 -12
  16. package/scaffold/.agent/skills/total-recall/plugins/auth/package.json +4 -4
  17. package/scaffold/.agent/skills/total-recall/plugins/auth/plugin.json +8 -1
  18. package/scaffold/.agent/skills/total-recall/plugins/auth/src/cli-secret.mjs +210 -27
  19. package/scaffold/.agent/skills/total-recall/plugins/auth/src/provider-catalog.mjs +10 -1
  20. package/scaffold/.agent/skills/total-recall/plugins/auth/src/rotation-capability.mjs +15 -0
  21. package/scaffold/.agent/skills/total-recall/plugins/auth/src/secrets-env-export.mjs +78 -6
  22. package/scaffold/.agent/skills/total-recall/plugins/auth/src/secrets-merge.mjs +131 -0
  23. package/scaffold/.agent/skills/total-recall/plugins/auth/src/secrets-remote-deploy.mjs +111 -2
  24. package/scaffold/.agent/skills/total-recall/plugins/auth/src/secrets-store.mjs +26 -5
  25. package/scaffold/.agent/skills/total-recall/plugins/auth/ui/DESIGN.md +56 -8
  26. package/scaffold/.agent/skills/total-recall/plugins/auth/ui/card.js +1068 -204
  27. package/scaffold/.agent/skills/total-recall/plugins/auth/ui/panel.js +4 -2
  28. package/scaffold/.agent/skills/total-recall/plugins/cli-agents/ui/DESIGN.md +32 -0
  29. package/scaffold/.agent/skills/total-recall/plugins/cli-agents/ui/card.js +436 -274
  30. package/scaffold/.agent/skills/total-recall/plugins/code-quality/package-lock.json +4 -4
  31. package/scaffold/.agent/skills/total-recall/plugins/code-quality/package.json +1 -1
  32. package/scaffold/.agent/skills/total-recall/plugins/code-quality/ui/DESIGN.md +16 -2
  33. package/scaffold/.agent/skills/total-recall/plugins/code-quality/ui/card.js +461 -242
  34. package/scaffold/.agent/skills/total-recall/plugins/composable-cli/package-lock.json +4 -4
  35. package/scaffold/.agent/skills/total-recall/plugins/composable-cli/package.json +1 -1
  36. package/scaffold/.agent/skills/total-recall/plugins/composable-cli/plugin.json +1 -7
  37. package/scaffold/.agent/skills/total-recall/plugins/composable-cli/ui/DESIGN.md +17 -2
  38. package/scaffold/.agent/skills/total-recall/plugins/composable-cli/ui/card.js +426 -300
  39. package/scaffold/.agent/skills/total-recall/plugins/embeddings/package-lock.json +4 -4
  40. package/scaffold/.agent/skills/total-recall/plugins/embeddings/ui/DESIGN.md +32 -0
  41. package/scaffold/.agent/skills/total-recall/plugins/embeddings/ui/card.js +350 -283
  42. package/scaffold/.agent/skills/total-recall/plugins/git-sentinel/package-lock.json +4 -4
  43. package/scaffold/.agent/skills/total-recall/plugins/git-sentinel/ui/card.js +31 -19
  44. package/scaffold/.agent/skills/total-recall/plugins/mesh/README.md +10 -0
  45. package/scaffold/.agent/skills/total-recall/plugins/mesh/{src/core/headscale-server.mjs → capability/control-server.mjs} +5 -2
  46. package/scaffold/.agent/skills/total-recall/plugins/mesh/capability/headscale-api.mjs +192 -0
  47. package/scaffold/.agent/skills/total-recall/plugins/mesh/capability/index.mjs +46 -0
  48. package/scaffold/.agent/skills/total-recall/plugins/mesh/cli.mjs +9 -1
  49. package/scaffold/.agent/skills/total-recall/plugins/mesh/package-lock.json +23 -22
  50. package/scaffold/.agent/skills/total-recall/plugins/mesh/package.json +6 -3
  51. package/scaffold/.agent/skills/total-recall/plugins/mesh/plugin.json +242 -8
  52. package/scaffold/.agent/skills/total-recall/plugins/mesh/src/cli/mesh.mjs +14 -9
  53. package/scaffold/.agent/skills/total-recall/plugins/mesh/src/cli/server.mjs +1 -1
  54. package/scaffold/.agent/skills/total-recall/plugins/mesh/src/core/credentials.mjs +4 -2
  55. package/scaffold/.agent/skills/total-recall/plugins/mesh/src/core/headscale-client.mjs +34 -181
  56. package/scaffold/.agent/skills/total-recall/plugins/mesh/src/core/mesh-auth.mjs +2 -2
  57. package/scaffold/.agent/skills/total-recall/plugins/mesh/src/core/presence.mjs +5 -5
  58. package/scaffold/.agent/skills/total-recall/plugins/mesh/src/core/screen-view.mjs +78 -0
  59. package/scaffold/.agent/skills/total-recall/plugins/mesh/ui/DESIGN.md +211 -12
  60. package/scaffold/.agent/skills/total-recall/plugins/mesh/ui/card.js +323 -388
  61. package/scaffold/.agent/skills/total-recall/plugins/openwiki/README.md +12 -1
  62. package/scaffold/.agent/skills/total-recall/plugins/openwiki/cli.mjs +7 -4
  63. package/scaffold/.agent/skills/total-recall/plugins/openwiki/package-lock.json +6 -6
  64. package/scaffold/.agent/skills/total-recall/plugins/openwiki/package.json +3 -2
  65. package/scaffold/.agent/skills/total-recall/plugins/openwiki/plugin.json +34 -1
  66. package/scaffold/.agent/skills/total-recall/plugins/openwiki/src/bootstrap.mjs +99 -0
  67. package/scaffold/.agent/skills/total-recall/plugins/openwiki/templates/openwiki/README.md +10 -0
  68. package/scaffold/.agent/skills/total-recall/plugins/openwiki/templates/openwiki/architecture.md +5 -0
  69. package/scaffold/.agent/skills/total-recall/plugins/openwiki/templates/openwiki/ide.md +10 -0
  70. package/scaffold/.agent/skills/total-recall/plugins/openwiki/templates/openwiki/memory.md +11 -0
  71. package/scaffold/.agent/skills/total-recall/plugins/openwiki/templates/openwiki/secrets.md +7 -0
  72. package/scaffold/.agent/skills/total-recall/plugins/openwiki/templates/openwiki/skills.md +7 -0
  73. package/scaffold/.agent/skills/total-recall/plugins/openwiki/ui/DESIGN.md +16 -2
  74. package/scaffold/.agent/skills/total-recall/plugins/openwiki/ui/card.js +380 -298
  75. package/scaffold/.agent/skills/total-recall/plugins/security/cli.mjs +7 -1
  76. package/scaffold/.agent/skills/total-recall/plugins/security/package-lock.json +6 -6
  77. package/scaffold/.agent/skills/total-recall/plugins/security/package.json +1 -1
  78. package/scaffold/.agent/skills/total-recall/plugins/security/plugin.json +1 -1
  79. package/scaffold/.agent/skills/total-recall/plugins/security/src/scanner.mjs +17 -3
  80. package/scaffold/.agent/skills/total-recall/plugins/security/ui/DESIGN.md +38 -15
  81. package/scaffold/.agent/skills/total-recall/plugins/security/ui/card.js +60 -34
  82. package/scaffold/.agent/skills/total-recall/references/auth.md +2 -0
  83. package/src/cli/command.mjs +23 -18
  84. package/src/cli/daemon.mjs +23 -1
  85. package/src/cli/plugin/copies.mjs +8 -5
  86. package/src/cli/plugin/index.mjs +25 -2
  87. package/src/cli/rebuild.mjs +22 -0
  88. package/src/cli/reset-password.mjs +3 -2
  89. package/src/cli/skill.mjs +8 -2
  90. package/src/cli/status.mjs +25 -7
  91. package/src/core/automatic-brain-updates.mjs +9 -3
  92. package/src/core/brain-updates.mjs +63 -5
  93. package/src/core/global-brain-updates.mjs +21 -1
  94. package/src/core/mesh-brain-updates.mjs +5 -2
  95. package/src/core/meta-harness.mjs +16 -41
  96. package/src/core/plugin-runner.mjs +6 -1
  97. package/src/core/plugin-store.mjs +67 -1
  98. package/src/core/plugin-updates.mjs +1 -1
  99. package/src/core/portable-skills-maintenance.mjs +19 -0
  100. package/src/core/required-plugins.mjs +7 -0
  101. package/src/core/skills-registry.mjs +33 -3
  102. package/src/server/auth.mjs +41 -1
  103. package/src/server/memory-app.mjs +2 -1
  104. package/src/server/routes/auth.mjs +25 -1
  105. package/src/server/routes/plugins.mjs +17 -4
@@ -907,7 +907,13 @@ export async function run(argv = []) {
907
907
  }
908
908
  if (IMPLEMENTED[sub]) {
909
909
  try { return await IMPLEMENTED[sub](args.slice(1)); }
910
- catch { console.log(JSON.stringify({ status: 'failed', operation: sub, error: 'Security operation failed; verify arguments, owning plugin availability and authorization. No success was established.' })); process.exitCode = 1; return; }
910
+ catch (error) {
911
+ // Say what failed. The generic text hid every cause, including a scan that
912
+ // simply hit its coverage limit. Messages are paths and limits, never matched content.
913
+ console.log(JSON.stringify({ status: 'failed', operation: sub, error: `Security operation failed: ${String(error?.message || error).slice(0, 300)}. No success was established.` }));
914
+ process.exitCode = 1;
915
+ return;
916
+ }
911
917
  }
912
918
  console.error(`Security: "${sub}" is not implemented.`);
913
919
  process.exitCode = 2;
@@ -1,16 +1,16 @@
1
1
  {
2
2
  "name": "tr-plugin-security",
3
- "version": "0.3.2",
3
+ "version": "0.3.3",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "tr-plugin-security",
9
- "version": "0.3.2",
9
+ "version": "0.3.3",
10
10
  "license": "MIT",
11
11
  "devDependencies": {
12
12
  "jsdom": "^26.1.0",
13
- "total-recall-brain": "^3.40.7"
13
+ "total-recall-brain": "3.41.2"
14
14
  }
15
15
  },
16
16
  "node_modules/@asamuzakjp/css-color": {
@@ -2916,9 +2916,9 @@
2916
2916
  }
2917
2917
  },
2918
2918
  "node_modules/total-recall-brain": {
2919
- "version": "3.40.7",
2920
- "resolved": "https://registry.npmjs.org/total-recall-brain/-/total-recall-brain-3.40.7.tgz",
2921
- "integrity": "sha512-G+sEWXVucI5Zs8uV1ie8P+mV/6cncWX76ES72T5dnPB3sFAwlcG9ous2SYTUaYUOETGB2vHTSo3cV4XWkHQ0Ww==",
2919
+ "version": "3.41.2",
2920
+ "resolved": "https://registry.npmjs.org/total-recall-brain/-/total-recall-brain-3.41.2.tgz",
2921
+ "integrity": "sha512-bwJFAFAyvk9ysf+5zhBzMxrKus1Hj1AygUWdBNt+vQbM2sfSEf6701sa30KwfGyU8+cMuN6YNT5Mn63mFZIi/w==",
2922
2922
  "dev": true,
2923
2923
  "license": "MIT",
2924
2924
  "workspaces": [
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tr-plugin-security",
3
- "version": "0.3.2",
3
+ "version": "0.3.3",
4
4
  "type": "module",
5
5
  "description": "Standalone security suite for Total Recall: static analysis, file and permission hardening schemes, AES-256-GCM encryption tools, and egress audit controls",
6
6
  "scripts": {
@@ -2,7 +2,7 @@
2
2
  "$schema": "https://github.com/gregiteen/total-recall/blob/main/metadata.plugin.schema.json",
3
3
  "id": "security",
4
4
  "name": "Security Assessments",
5
- "version": "0.3.2",
5
+ "version": "0.3.3",
6
6
  "description": "Machine and device security assessments for macOS, Linux, Windows, Android, Chromebook, iPhone and iPad, with explicit measured and manual coverage.",
7
7
  "license": "MIT",
8
8
  "branding": {
@@ -5,8 +5,12 @@ import path from 'node:path';
5
5
  export function scanSource(targetPath) {
6
6
  const root = fs.realpathSync(path.resolve(targetPath));
7
7
  const findings = [];
8
- const skipped = new Set(['node_modules', '.git', 'dist', 'coverage', 'memory-vault', '.cache', '.venv', '__pycache__']);
8
+ // Generated brain data (indexes, sessions, logs) is not source; scanning it
9
+ // only hit the size limit and aborted the whole scan.
10
+ const skipped = new Set(['node_modules', '.git', 'dist', 'coverage', 'memory-vault', 'memory-derived', 'sessions', '.cache', '.venv', '__pycache__']);
11
+ const MAX_FILES = 20000, MAX_BYTES = 2097152;
9
12
  let count = 0;
13
+ let limited = false;
10
14
  function visit(file) {
11
15
  const stat = fs.lstatSync(file);
12
16
  if (stat.isSymbolicLink()) {
@@ -14,11 +18,21 @@ export function scanSource(targetPath) {
14
18
  return;
15
19
  }
16
20
  if (stat.isDirectory()) {
17
- for (const name of fs.readdirSync(file)) if (!skipped.has(name)) visit(path.join(file, name));
21
+ for (const name of fs.readdirSync(file)) { if (limited) return; if (!skipped.has(name)) visit(path.join(file, name)); }
18
22
  return;
19
23
  }
20
24
  if (!stat.isFile() || !/\.(?:[cm]?js|tsx?|py|sh|bash|ps1|json|env|ya?ml)$|(?:^|\/)\.env(?:\.|$)/.test(file)) return;
21
- if (++count > 20000 || stat.size > 2097152) throw new Error('Scan coverage limit exceeded; narrow the target');
25
+ // Report incomplete coverage as a finding instead of aborting: an aborted
26
+ // scan returned nothing, which callers could not tell apart from a failure.
27
+ if (stat.size > MAX_BYTES) {
28
+ findings.push({ file: path.relative(root, file), line: 0, rule: 'oversized-file-unscanned', severity: 'HIGH', detail: `Larger than ${MAX_BYTES} bytes; not inspected, coverage incomplete` });
29
+ return;
30
+ }
31
+ if (++count > MAX_FILES) {
32
+ limited = true;
33
+ findings.push({ file: '.', line: 0, rule: 'coverage-limit-reached', severity: 'HIGH', detail: `Stopped after ${MAX_FILES} files; narrow the target, coverage incomplete` });
34
+ return;
35
+ }
22
36
  const testFile = /(?:^|\/)(?:test|tests|fixtures)(?:\/|$)|\.(?:test|spec)\./.test(file);
23
37
  fs.readFileSync(file, 'utf8').split('\n').forEach((line, index) => {
24
38
  const rules = [];
@@ -1,18 +1,41 @@
1
- # DESIGN.md — Tin Foil
1
+ # DESIGN.md — Security Plugin (Tin Foil)
2
2
 
3
- ```yaml
4
- tokens:
5
- brand-name: "Tin Foil"
6
- wordmark: "[TIN//FOIL]"
7
- color-primary: "#f59e0b"
8
- color-background: "#0a0a0c"
9
- color-accent: "#e2e8f0"
10
- badge: "Hardening"
11
- font-mono: "'JetBrains Mono', 'Fira Code', monospace"
12
- border-radius: "8px"
3
+ ## Brand Identity & Aesthetic
4
+ - **Brand Name**: Tin Foil
5
+ - **Wordmark**: `[TIN//FOIL]`
6
+ - **Theme**: Crimson and Obsidian Host Hardening Console
7
+ - **Tone**: Deterministic, vigilant, fail-closed, precise
8
+
9
+ ## Design Tokens
10
+
11
+ The card face defines and consumes these custom properties:
12
+
13
+ ```css
14
+ :host {
15
+ --face-bg: #180d11;
16
+ --face-bg-subtle: #251017;
17
+ --face-bg-surface: #11070a;
18
+ --face-bg-table: #0d0508;
19
+ --face-bg-code: #0a0406;
20
+ --face-accent: #f43f5e;
21
+ --face-accent-hover: rgba(244, 63, 94, 0.22);
22
+ --face-accent-glow: rgba(244, 63, 94, 0.35);
23
+ --face-border-subtle: rgba(244, 63, 94, 0.28);
24
+ --face-border-faint: rgba(244, 63, 94, 0.2);
25
+ --face-border-cell: rgba(244, 63, 94, 0.15);
26
+ --face-border-alert: rgba(244, 63, 94, 0.25);
27
+ --face-text: #f1f5f9;
28
+ --face-text-heading: #fecdd3;
29
+ --face-text-muted: #fda4af;
30
+ --face-text-cell: #f8fafc;
31
+ --face-text-desc: #cbd5e1;
32
+ --face-danger: #fb7185;
33
+ --face-warning: #fca5a5;
34
+ }
13
35
  ```
14
36
 
15
- ## Brand & Typographic Wordmark
16
- - **Name**: Tin Foil
17
- - **Wordmark Logo**: `[TIN//FOIL]` (custom typographic badge, no generic icons)
18
- - **Palette**: Primary `#f59e0b`, Surface `#0a0a0c`, Accent `#e2e8f0`.
37
+ ## System Integration
38
+ - The face binds `--text-primary` with fallback to `var(--face-text)`.
39
+ - The face binds `--border` with fallback to `var(--face-accent-glow)`.
40
+ - All clickable elements enforce a minimum height of 44px for touch ergonomics.
41
+ - Reduced motion styles suppress animation when requested by system settings.
@@ -5,11 +5,31 @@ export class SecuritySecurityCard extends HTMLElement {
5
5
  root.innerHTML = `
6
6
  <style>
7
7
  :host {
8
+ --face-bg: #180d11;
9
+ --face-bg-subtle: #251017;
10
+ --face-bg-surface: #11070a;
11
+ --face-bg-table: #0d0508;
12
+ --face-bg-code: #0a0406;
13
+ --face-accent: #f43f5e;
14
+ --face-accent-hover: rgba(244, 63, 94, 0.22);
15
+ --face-accent-glow: rgba(244, 63, 94, 0.35);
16
+ --face-border-subtle: rgba(244, 63, 94, 0.28);
17
+ --face-border-faint: rgba(244, 63, 94, 0.2);
18
+ --face-border-cell: rgba(244, 63, 94, 0.15);
19
+ --face-border-alert: rgba(244, 63, 94, 0.25);
20
+ --face-text: #f1f5f9;
21
+ --face-text-heading: #fecdd3;
22
+ --face-text-muted: #fda4af;
23
+ --face-text-cell: #f8fafc;
24
+ --face-text-desc: #cbd5e1;
25
+ --face-danger: #fb7185;
26
+ --face-warning: #fca5a5;
27
+
8
28
  display: block;
9
29
  font-family: ui-sans-serif, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
10
- color: #f1f5f9;
11
- background: #180d11;
12
- border: 1px solid rgba(244, 63, 94, 0.35);
30
+ color: var(--text-primary, var(--face-text));
31
+ background: var(--face-bg);
32
+ border: 1px solid var(--border, var(--face-accent-glow));
13
33
  border-radius: 10px;
14
34
  padding: 18px;
15
35
  box-shadow: 0 10px 28px rgba(0, 0, 0, 0.45);
@@ -27,7 +47,7 @@ export class SecuritySecurityCard extends HTMLElement {
27
47
  .brand-icon {
28
48
  width: 32px;
29
49
  height: 38px;
30
- color: #f43f5e;
50
+ color: var(--face-accent);
31
51
  flex-shrink: 0;
32
52
  }
33
53
 
@@ -43,12 +63,12 @@ export class SecuritySecurityCard extends HTMLElement {
43
63
  font-weight: 700;
44
64
  letter-spacing: 0.03em;
45
65
  text-transform: uppercase;
46
- color: #fecdd3;
66
+ color: var(--face-text-heading);
47
67
  }
48
68
 
49
69
  p.desc {
50
70
  margin: 0 0 14px 0;
51
- color: #fda4af;
71
+ color: var(--face-text-muted);
52
72
  font-size: 12px;
53
73
  line-height: 1.5;
54
74
  }
@@ -66,11 +86,11 @@ export class SecuritySecurityCard extends HTMLElement {
66
86
 
67
87
  button, input, select {
68
88
  min-height: 44px;
69
- border: 1px solid rgba(244, 63, 94, 0.28);
89
+ border: 1px solid var(--face-border-subtle);
70
90
  border-radius: 6px;
71
91
  padding: 8px 12px;
72
- background: #251017;
73
- color: #f1f5f9;
92
+ background: var(--face-bg-subtle);
93
+ color: var(--face-text);
74
94
  font-size: 11px;
75
95
  font-weight: 600;
76
96
  letter-spacing: 0.02em;
@@ -84,8 +104,8 @@ export class SecuritySecurityCard extends HTMLElement {
84
104
  }
85
105
 
86
106
  button:hover {
87
- background: rgba(244, 63, 94, 0.22);
88
- border-color: #f43f5e;
107
+ background: var(--face-accent-hover);
108
+ border-color: var(--face-accent);
89
109
  }
90
110
 
91
111
  button:active {
@@ -101,17 +121,17 @@ export class SecuritySecurityCard extends HTMLElement {
101
121
  min-width: 0;
102
122
  flex: 1;
103
123
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
104
- background: #11070a;
124
+ background: var(--face-bg-surface);
105
125
  }
106
126
 
107
127
  input:focus, select:focus {
108
- border-color: #f43f5e;
128
+ border-color: var(--face-accent);
109
129
  outline: none;
110
- box-shadow: 0 0 0 1px rgba(244, 63, 94, 0.3);
130
+ box-shadow: 0 0 0 1px var(--face-accent-glow);
111
131
  }
112
132
 
113
133
  select {
114
- background: #11070a;
134
+ background: var(--face-bg-surface);
115
135
  cursor: pointer;
116
136
  }
117
137
 
@@ -120,66 +140,66 @@ export class SecuritySecurityCard extends HTMLElement {
120
140
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
121
141
  font-size: 11px;
122
142
  font-weight: 700;
123
- color: #fb7185;
124
- background: #11070a;
143
+ color: var(--face-danger);
144
+ background: var(--face-bg-surface);
125
145
  padding: 6px 10px;
126
146
  border-radius: 4px;
127
- border: 1px solid rgba(244, 63, 94, 0.2);
147
+ border: 1px solid var(--face-border-faint);
128
148
  }
129
149
 
130
150
  .scroll {
131
151
  overflow-x: auto;
132
152
  margin-bottom: 10px;
133
153
  border-radius: 6px;
134
- border: 1px solid rgba(244, 63, 94, 0.2);
154
+ border: 1px solid var(--face-border-faint);
135
155
  }
136
156
 
137
157
  table {
138
158
  width: 100%;
139
159
  border-collapse: collapse;
140
160
  font-size: 11px;
141
- background: #0d0508;
161
+ background: var(--face-bg-table);
142
162
  }
143
163
 
144
164
  th {
145
- background: #251017;
146
- color: #fecdd3;
165
+ background: var(--face-bg-subtle);
166
+ color: var(--face-text-heading);
147
167
  font-weight: 700;
148
168
  text-transform: uppercase;
149
169
  letter-spacing: 0.04em;
150
170
  padding: 8px 10px;
151
- border-bottom: 1px solid rgba(244, 63, 94, 0.3);
171
+ border-bottom: 1px solid var(--face-border-subtle);
152
172
  text-align: left;
153
173
  }
154
174
 
155
175
  td {
156
176
  padding: 8px 10px;
157
177
  text-align: left;
158
- border-bottom: 1px solid rgba(244, 63, 94, 0.15);
178
+ border-bottom: 1px solid var(--face-border-cell);
159
179
  vertical-align: top;
160
- color: #f8fafc;
180
+ color: var(--face-text-cell);
161
181
  }
162
182
 
163
183
  td:first-child {
164
184
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
165
185
  font-weight: 600;
166
- color: #fda4af;
186
+ color: var(--face-text-muted);
167
187
  }
168
188
 
169
189
  td:last-child {
170
190
  line-height: 1.45;
171
- color: #cbd5e1;
191
+ color: var(--face-text-desc);
172
192
  }
173
193
 
174
194
  pre {
175
- background: #0a0406;
176
- border: 1px solid rgba(244, 63, 94, 0.2);
195
+ background: var(--face-bg-code);
196
+ border: 1px solid var(--face-border-faint);
177
197
  border-radius: 6px;
178
198
  padding: 8px 10px;
179
199
  font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
180
200
  font-size: 10.5px;
181
201
  line-height: 1.45;
182
- color: #fb7185;
202
+ color: var(--face-danger);
183
203
  white-space: pre-wrap;
184
204
  overflow-wrap: anywhere;
185
205
  max-height: 160px;
@@ -188,8 +208,8 @@ export class SecuritySecurityCard extends HTMLElement {
188
208
  }
189
209
 
190
210
  #device-review {
191
- background: #11070a;
192
- border: 1px solid rgba(244, 63, 94, 0.25);
211
+ background: var(--face-bg-surface);
212
+ border: 1px solid var(--face-border-alert);
193
213
  border-radius: 6px;
194
214
  padding: 10px;
195
215
  margin-top: 10px;
@@ -198,7 +218,7 @@ export class SecuritySecurityCard extends HTMLElement {
198
218
  #device-review p {
199
219
  margin: 0 0 8px 0;
200
220
  font-size: 11px;
201
- color: #fca5a5;
221
+ color: var(--face-warning);
202
222
  }
203
223
 
204
224
  #device-checks label {
@@ -209,9 +229,15 @@ export class SecuritySecurityCard extends HTMLElement {
209
229
  #device-checks label p {
210
230
  margin: 0 0 4px 0;
211
231
  font-size: 11px;
212
- color: #fecdd3;
232
+ color: var(--face-text-heading);
213
233
  font-weight: 600;
214
234
  }
235
+
236
+ @media (prefers-reduced-motion: reduce) {
237
+ button, input, select {
238
+ transition: none !important;
239
+ }
240
+ }
215
241
  </style>
216
242
 
217
243
  <header>
@@ -8,6 +8,8 @@ There is one secrets store per host: the global auth store. The CLI never select
8
8
 
9
9
  For provider key setup, verify against an authenticated provider operation before saving; format-only checks and timeouts are not verification. Require HTTPS except explicit loopback development, reject redirects containing credentials, and return safe errors. Rotation follows verify → store/project → deploy/verify → revoke, respecting the provider's supported recipe and user authorization. Manual provider steps remain manual. Exported environment files are deployment projections, not an alternate credential authority.
10
10
 
11
+ Every secret operation goes through `total-recall auth secret`; never read, compare or copy credential values with shell tools. Check a copied credential with `fingerprint --from-clipboard --expect-prefix <p> --not <KEY>` before `rotate <KEY> --from-clipboard`. After `remote deploy <target> --keys … [--path <repo>]`, confirm with `remote verify <target> --keys …` (fingerprint match, no values). A process manager can keep a captured environment that overrides the deployed file (pm2 does): move the keys the app uses into the store with `remote import-pm2 <target> --app <name> --keys … --apply`, then restart the app with a clean environment so it reads only the projection. Stripe webhook signing secrets are issued by Stripe; roll them in the dashboard, never mint them.
12
+
11
13
  Current migration remains in progress until package/bootstrap tests, all-plugin bypass checks, owner suites and end-to-end UI/API checks pass. Manifest `auth.plugin` declarations alone do not certify any of these properties.
12
14
 
13
15
  ## Sessions, passwords and legacy data
@@ -222,6 +222,27 @@ function headerTags(description, risk) {
222
222
  return (description ? `// @description ${one(description)}\n` : '') + (risk ? `// @risk ${risk}\n` : '');
223
223
  }
224
224
 
225
+ /**
226
+ * Build a custom command's file from author-supplied source.
227
+ *
228
+ * The source may be a BODY for run(argv) or a complete module. Blindly wrapping it failed two ways:
229
+ * a module produced "Unexpected identifier 'fs'", and a body that declared its own `args` produced
230
+ * "Identifier 'args' has already been declared". Detect the shape instead of assuming it, and declare
231
+ * `args` only when the body does not. The default export delegates so the body is written once.
232
+ */
233
+ function buildCommandFile(name, description, risk, code) {
234
+ const source = String(code ?? '');
235
+ const header = `// Auto-generated custom CLI command: ${name}\n${headerTags(description, risk)}`;
236
+ if (/^\s*(?:import|export)\s/m.test(source)) {
237
+ return `${header}// Supplied as a complete module; used as written.\n${source}\n`;
238
+ }
239
+ const declaresArgs = /(?:^|\n)\s*(?:const|let|var)\s+args\b/.test(source);
240
+ const argsLine = declaresArgs ? '' : 'const args = Array.isArray(argv) ? argv.slice(3) : [];\n';
241
+ return `${header}// Your source is the BODY of run(argv). \`argv\` is in scope${declaresArgs ? '' : ', and so is `args` (argv minus the command name)'}.\n`
242
+ + "// Use `await import('node:module')`; write no import/export statements here.\n"
243
+ + `export async function run(argv = []) {\n${argsLine}${source}\n}\nexport default function (argv = []) {\n return run(argv);\n}\n`;
244
+ }
245
+
225
246
  function existingTag(file, key) {
226
247
  try { return fs.readFileSync(file, 'utf8').match(new RegExp(`^// @${key} (.+)$`, 'm'))?.[1] || undefined; } catch { return undefined; }
227
248
  }
@@ -373,15 +394,7 @@ export default async function commandCmd(rawArgs = []) {
373
394
  }
374
395
 
375
396
  // Wrap code in an exported run/default function for composable CLI execution
376
- const fileContent = `// Auto-generated custom CLI command: ${name}
377
- ${headerTags(description, risk)}export async function run(argv = []) {
378
- const args = Array.isArray(argv) ? argv.slice(3) : [];
379
- ${code}
380
- }
381
- export default async function (args = []) {
382
- ${code}
383
- }
384
- `;
397
+ const fileContent = buildCommandFile(name, description, risk, code);
385
398
 
386
399
  fs.writeFileSync(commandPath, fileContent, 'utf8');
387
400
  console.log(`\x1b[32m✔ Successfully created custom command: \x1b[1mnpx total-recall ${name}\x1b[0m`);
@@ -413,15 +426,7 @@ export default async function (args = []) {
413
426
  // Keep the declared description/risk unless the update replaces them.
414
427
  const keptDescription = description ?? existingTag(commandPath, 'description');
415
428
  const keptRisk = risk ?? existingTag(commandPath, 'risk');
416
- const fileContent = `// Auto-generated custom CLI command: ${name}
417
- ${headerTags(keptDescription, keptRisk)}export async function run(argv = []) {
418
- const args = Array.isArray(argv) ? argv.slice(3) : [];
419
- ${code}
420
- }
421
- export default async function (args = []) {
422
- ${code}
423
- }
424
- `;
429
+ const fileContent = buildCommandFile(name, keptDescription, keptRisk, code);
425
430
 
426
431
  fs.writeFileSync(commandPath, fileContent, 'utf8');
427
432
  console.log(`\x1b[32m✔ Successfully updated custom command: \x1b[1m${name}\x1b[0m`);
@@ -218,7 +218,29 @@ export default async function daemon(args) {
218
218
 
219
219
  switch (subcommand) {
220
220
  case 'ensure': {
221
- if(!useSystemd)throw new Error('Managed daemon ensure currently requires systemd on the daemon host');
221
+ if(useLaunchd){
222
+ // macOS: the launchd agent owns the daemon. Load it if needed, restart it
223
+ // on --restart (or start it when nothing runs), then verify the process
224
+ // launchd reports is the one holding the brain lock.
225
+ const label='com.totalrecall.daemon',target=`gui/${process.getuid()}/${label}`;
226
+ const launchdPid=()=>{const r=spawnSync('launchctl',['list',label],{encoding:'utf8'});const m=r.status===0&&/"PID"\s*=\s*(\d+)/.exec(r.stdout||'');return m?Number(m[1]):0;};
227
+ if(spawnSync('launchctl',['list',label],{stdio:'ignore'}).status!==0){
228
+ const boot=spawnSync('launchctl',['bootstrap',`gui/${process.getuid()}`,launchAgentPlist(label)],{encoding:'utf8'});
229
+ if(boot.status!==0)throw new Error(`Cannot load ${label}: ${(boot.stderr||'').trim()}`);
230
+ }
231
+ const owner=readPid(),managedPid=launchdPid();
232
+ if(owner && owner!==managedPid && !await stopDaemon())throw new Error('Cannot stop verified detached daemon');
233
+ if(args.includes('--restart') || !managedPid || (owner && owner!==managedPid)){
234
+ const kick=spawnSync('launchctl',['kickstart','-k',target],{encoding:'utf8'});
235
+ if(kick.status!==0)throw new Error(`launchctl kickstart failed: ${(kick.stderr||'').trim()}`);
236
+ }
237
+ await new Promise(resolve=>setTimeout(resolve,3000));
238
+ const pid=readPid(),current=launchdPid();
239
+ if(!pid || pid!==current)throw new Error('Managed daemon did not retain ownership of the brain lock');
240
+ console.log(JSON.stringify({ok:true,manager:'launchd',pid,enabled:true}));
241
+ break;
242
+ }
243
+ if(!useSystemd)throw new Error('Managed daemon ensure needs systemd or a launchd agent on the daemon host');
222
244
  // A detached worker can hold the same brain lock while systemd reports a
223
245
  // successful short-lived start. Stop that verified owner before takeover.
224
246
  const managed=spawnSync('systemctl',['show','total-recall-daemon','--property=MainPID','--value'],{encoding:'utf8'});
@@ -17,8 +17,9 @@ export function pluginCopyRows(projectRoot = process.cwd(), { sharedDir = global
17
17
  root: projectRoot,
18
18
  sharedDir,
19
19
  count: audit.count,
20
- removable: audit.copies.filter(copy => copy.edited === false).map(copy => copy.id),
21
- edited: audit.copies.filter(copy => copy.edited === true).map(copy => copy.id),
20
+ removable: audit.copies.filter(copy => copy.edited === false || copy.stale === true).map(copy => copy.id),
21
+ edited: audit.copies.filter(copy => copy.edited === true && copy.stale !== true).map(copy => copy.id),
22
+ stale: audit.copies.filter(copy => copy.stale === true).map(copy => copy.id),
22
23
  unverified: audit.copies.filter(copy => copy.edited === null).map(copy => copy.id),
23
24
  copies: audit.copies
24
25
  };
@@ -58,11 +59,13 @@ export async function manageProjectPluginCopies(args = []) {
58
59
  return;
59
60
  }
60
61
  for (const copy of report.copies) {
61
- const verdict = copy.edited === false ? 'identical to the shared install' : copy.edited === true ? 'DIFFERS from the shared install' : 'no shared install to compare';
62
+ const verdict = copy.stale === true ? 'STALE — older than the shared install; it shadows the shared plugin and must be removed'
63
+ : copy.edited === false ? 'identical to the shared install'
64
+ : copy.edited === true ? 'DIFFERS from the shared install' : 'no shared install to compare';
62
65
  console.log(` ${copy.id} v${copy.version || '?'} (shared: v${copy.sharedVersion || '—'}) — ${verdict}`);
63
66
  }
64
- console.log(`\n ${report.removable.length} removable, ${report.edited.length} edited, ${report.unverified.length} unverified`);
67
+ console.log(`\n ${report.removable.length} removable (${report.stale.length} stale), ${report.edited.length} edited, ${report.unverified.length} unverified`);
65
68
  if (report.removal) console.log(` Removed: ${report.removal.removed.join(', ') || 'none'}${report.removal.failed.length ? `; failed: ${report.removal.failed.map(row => row.id).join(', ')}` : ''}`);
66
- else if (report.removable.length) console.log(' Re-run with --apply to remove the identical copies.');
69
+ else if (report.removable.length) console.log(' Re-run with --apply to remove the removable copies.');
67
70
  console.log();
68
71
  }
@@ -6,8 +6,8 @@ import { createPlugin } from './create.mjs';
6
6
  import { vendorPlugin, listProjectBricks } from './vendor.mjs';
7
7
  import { manageProjectPluginCopies } from './copies.mjs';
8
8
  import { managePluginLicense } from './monetization.mjs';
9
- import { getPlugin, getPluginMonetization } from '../../core/plugin-loader.mjs';
10
- import { describePlugin } from '../../core/plugin-store.mjs';
9
+ import { getPlugin, getPluginMonetization, globalPluginsDir, projectPluginsDir } from '../../core/plugin-loader.mjs';
10
+ import { describePlugin, pruneReplacementArchives, replacementArchiveDir } from '../../core/plugin-store.mjs';
11
11
  import { buildPluginCatalog } from '../../core/plugin-catalog.mjs';
12
12
  import { setPluginActive } from '../../core/plugin-activation.mjs';
13
13
 
@@ -29,6 +29,10 @@ Commands:
29
29
  refresh Rebuild installed command inventories and instruction surfaces
30
30
  copies [dir] [--apply] Audit this repository's leftover copies of the shared default
31
31
  plugins; --apply removes only the ones identical to the shared install
32
+ replacements [--prune] [--keep N] [--project] [--json]
33
+ Copies kept by install --replace (default: shared folder). Reports
34
+ the space; --prune keeps the newest N per plugin (default 1) and
35
+ drops node_modules from the kept ones
32
36
  on <id> / off <id> Enable/disable host execution for this repository alone
33
37
  --all-repos Change the shared setting for every repository
34
38
  --clear Drop this repository's override; follow the shared setting
@@ -136,6 +140,25 @@ export async function run(argv = []) {
136
140
  return;
137
141
  }
138
142
 
143
+ if (command === 'replacements') {
144
+ const keepAt = rest.indexOf('--keep');
145
+ const keep = keepAt === -1 ? 1 : Number(rest[keepAt + 1]);
146
+ if (!Number.isInteger(keep) || keep < 0) {
147
+ console.error('Usage: total-recall plugin replacements [--prune] [--keep N] [--project] [--json]');
148
+ process.exitCode = 1;
149
+ return;
150
+ }
151
+ const dir = replacementArchiveDir(rest.includes('--project') ? projectPluginsDir(process.cwd()) : globalPluginsDir());
152
+ const apply = rest.includes('--prune');
153
+ const r = pruneReplacementArchives(dir, { keep, apply });
154
+ if (rest.includes('--json')) { console.log(JSON.stringify({ ...r, applied: apply }, null, 2)); return; }
155
+ const gb = (r.bytesFreed / 1073741824).toFixed(2);
156
+ console.log(`\n ${r.archiveDir}`);
157
+ console.log(` ${r.kept.length + r.removed.length} retained cop${r.kept.length + r.removed.length === 1 ? 'y' : 'ies'}; keeping ${r.kept.length}, ${apply ? 'removed' : 'would remove'} ${r.removed.length}, ${apply ? 'stripped' : 'would strip'} node_modules from ${r.stripped.length}`);
158
+ console.log(` ${apply ? 'Freed' : 'Would free'} ${gb} GB${apply ? '' : ' — run with --prune to apply'}\n`);
159
+ return;
160
+ }
161
+
139
162
  if (command === 'copies' || command === 'duplicates') {
140
163
  await manageProjectPluginCopies(rest);
141
164
  return;
@@ -163,6 +163,28 @@ export async function compileRegisteredProjects({ brains = listLocalBrains(), co
163
163
  }
164
164
 
165
165
  export default async function cli(args) {
166
+ // `--help` must never execute the command. This parsed flags and went straight to a rebuild, so
167
+ // `total-recall compile --help` (and rebuild) threw away the derived indexes and recompiled every
168
+ // brain instead of printing usage.
169
+ if (args.includes('--help') || args.includes('-h')) {
170
+ console.log(`
171
+ total-recall compile | rebuild — SSSS projection rebuild
172
+
173
+ Discards the derived indexes and rebuilds them deterministically from the canonical vault,
174
+ then recompiles instruction surfaces.
175
+
176
+ total-recall compile [--check] [--force] [--all] [--global|--project]
177
+
178
+ --check Report drift between the vault and derived indexes; change nothing.
179
+ --force Rebuild even when nothing is stale.
180
+ --all Recompile every registered project brain as well.
181
+ --global Target the global brain (~/.agent).
182
+ --project Target this repository's brain (<cwd>/.agent).
183
+
184
+ A global compile is what \`remember --global\` runs, because global rules land in every project.
185
+ `);
186
+ process.exit(0);
187
+ }
166
188
  const { layer, remainingArgs } = parseLayerFlag(args);
167
189
  const options = {
168
190
  check: remainingArgs.includes('--check'),
@@ -54,9 +54,10 @@ export default async function resetPassword(args) {
54
54
  console.log(`
55
55
  total-recall reset-password
56
56
 
57
- Resets the dashboard admin password in ~/.agent/skills/total-recall/config/security.yml.
57
+ Resets the dashboard admin password in the encrypted auth store (the only place the server
58
+ reads it; any password_hash left in config/security.yml is ignored).
58
59
  Prompts for the new password; it is never taken as an argument.
59
- Sets force_password_reset to true so you will be prompted to change it upon first login.
60
+ Existing sessions are invalidated. Setting the well-known default forces a change on first login.
60
61
 
61
62
  For automation, pipe it instead: echo "\$NEW_PW" | total-recall reset-password
62
63
  `);