total-recall-brain 3.43.2 → 3.44.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 (103) hide show
  1. package/bin/total-recall-server-release.mjs +4 -0
  2. package/docs/reference/cli-reference.md +14 -0
  3. package/package.json +5 -4
  4. package/scaffold/.agent/skills/plugin/SKILL.md +7 -2
  5. package/scaffold/.agent/skills/plugin/evals/README.md +5 -0
  6. package/scaffold/.agent/skills/plugin/evals/cases/01-fabricated-seed.mjs +1 -0
  7. package/scaffold/.agent/skills/plugin/evals/cases/02-reassuring-default.mjs +1 -0
  8. package/scaffold/.agent/skills/plugin/evals/cases/03-declared-template.mjs +1 -0
  9. package/scaffold/.agent/skills/plugin/evals/cases/04-card-collects-credential.mjs +1 -0
  10. package/scaffold/.agent/skills/plugin/evals/cases/05-host-leak-in-scaffold.mjs +1 -0
  11. package/scaffold/.agent/skills/plugin/evals/cases/06-correct-plugin.mjs +1 -0
  12. package/scaffold/.agent/skills/plugin/evals/cases/07-missing-reuse.mjs +11 -0
  13. package/scaffold/.agent/skills/plugin/evals/cases/08-unresolved-owner-command.mjs +12 -0
  14. package/scaffold/.agent/skills/plugin/evals/cases/09-missing-brick.mjs +12 -0
  15. package/scaffold/.agent/skills/plugin/prompts/01-function.md +4 -3
  16. package/scaffold/.agent/skills/plugin/prompts/08-verify.md +1 -0
  17. package/scaffold/.agent/skills/plugin/references/surfaces.md +12 -2
  18. package/scaffold/.agent/skills/plugin/scripts/record.mjs +59 -2
  19. package/scaffold/.agent/skills/total-recall/plugins/auth/README.md +8 -0
  20. package/scaffold/.agent/skills/total-recall/plugins/auth/package-lock.json +14 -13
  21. package/scaffold/.agent/skills/total-recall/plugins/auth/package.json +3 -3
  22. package/scaffold/.agent/skills/total-recall/plugins/auth/plugin.json +1 -1
  23. package/scaffold/.agent/skills/total-recall/plugins/auth/src/cli-gh-login.mjs +102 -0
  24. package/scaffold/.agent/skills/total-recall/plugins/auth/src/cli-secret.mjs +18 -1
  25. package/scaffold/.agent/skills/total-recall/plugins/auth/src/login-projections.mjs +157 -13
  26. package/scaffold/.agent/skills/total-recall/plugins/auth/src/secrets-sync.mjs +161 -17
  27. package/scaffold/.agent/skills/total-recall/plugins/code-quality/README.md +2 -2
  28. package/scaffold/.agent/skills/total-recall/plugins/code-quality/core/check.mjs +39 -6
  29. package/scaffold/.agent/skills/total-recall/plugins/code-quality/core/config.schema.json +2 -1
  30. package/scaffold/.agent/skills/total-recall/plugins/code-quality/package-lock.json +14 -13
  31. package/scaffold/.agent/skills/total-recall/plugins/code-quality/package.json +3 -3
  32. package/scaffold/.agent/skills/total-recall/plugins/code-quality/plugin.json +1 -1
  33. package/scaffold/.agent/skills/total-recall/plugins/composable-cli/package-lock.json +14 -13
  34. package/scaffold/.agent/skills/total-recall/plugins/composable-cli/package.json +3 -3
  35. package/scaffold/.agent/skills/total-recall/plugins/composable-cli/plugin.json +1 -1
  36. package/scaffold/.agent/skills/total-recall/plugins/embeddings/package-lock.json +14 -13
  37. package/scaffold/.agent/skills/total-recall/plugins/embeddings/package.json +3 -3
  38. package/scaffold/.agent/skills/total-recall/plugins/embeddings/plugin.json +1 -1
  39. package/scaffold/.agent/skills/total-recall/plugins/git-sentinel/package-lock.json +14 -13
  40. package/scaffold/.agent/skills/total-recall/plugins/git-sentinel/package.json +3 -3
  41. package/scaffold/.agent/skills/total-recall/plugins/git-sentinel/plugin.json +1 -1
  42. package/scaffold/.agent/skills/total-recall/plugins/mesh/package-lock.json +14 -13
  43. package/scaffold/.agent/skills/total-recall/plugins/mesh/package.json +3 -3
  44. package/scaffold/.agent/skills/total-recall/plugins/mesh/plugin.json +1 -1
  45. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/README.md +38 -5
  46. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/cli.mjs +38 -7
  47. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/package-lock.json +14 -13
  48. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/package.json +3 -3
  49. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/plugin.json +85 -3
  50. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/skills/meta-harness/SKILL.md +2 -1
  51. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/adoption.mjs +104 -0
  52. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/cascade.mjs +12 -1
  53. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/coordinator.mjs +15 -3
  54. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/dispatch-request.mjs +38 -0
  55. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/dispatch.mjs +42 -11
  56. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/executor.mjs +40 -13
  57. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/included-usage.mjs +249 -0
  58. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/learner.mjs +3 -0
  59. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/pool.mjs +16 -8
  60. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/process-manager.mjs +1 -0
  61. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/reasoning.mjs +5 -0
  62. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/remote-operation.mjs +1 -1
  63. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/router.mjs +27 -11
  64. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/runtime.mjs +28 -6
  65. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/usage-queue.mjs +110 -0
  66. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/worker.mjs +322 -40
  67. package/scaffold/.agent/skills/total-recall/plugins/openwiki/package-lock.json +14 -13
  68. package/scaffold/.agent/skills/total-recall/plugins/openwiki/package.json +3 -3
  69. package/scaffold/.agent/skills/total-recall/plugins/openwiki/plugin.json +1 -1
  70. package/scaffold/.agent/skills/total-recall/plugins/security/package-lock.json +14 -13
  71. package/scaffold/.agent/skills/total-recall/plugins/security/package.json +3 -3
  72. package/scaffold/.agent/skills/total-recall/plugins/security/plugin.json +1 -1
  73. package/scaffold/.agent/skills/total-recall/plugins/ssss/assets/scaffold.json +5 -5
  74. package/scaffold/.agent/skills/total-recall/plugins/ssss/package-lock.json +6 -6
  75. package/scaffold/.agent/skills/total-recall/plugins/ssss/package.json +2 -2
  76. package/scaffold/.agent/skills/total-recall/plugins/ssss/plugin.json +1 -1
  77. package/src/cli/connect.mjs +2 -29
  78. package/src/cli/context.mjs +1 -0
  79. package/src/cli/ingest-okf.mjs +4 -4
  80. package/src/cli/release.mjs +27 -2
  81. package/src/cli/startup.mjs +1 -0
  82. package/src/cli/update.mjs +14 -3
  83. package/src/core/automatic-brain-updates.mjs +13 -3
  84. package/src/core/autopull-install.mjs +14 -0
  85. package/src/core/brain-updates.mjs +12 -2
  86. package/src/core/context-compactor.mjs +7 -2
  87. package/src/core/context-compiler.mjs +6 -1
  88. package/src/core/context-policy.mjs +7 -3
  89. package/src/core/daemon-control.mjs +22 -0
  90. package/src/core/global-brain-updates.mjs +3 -3
  91. package/src/core/mesh-brain-updates.mjs +20 -5
  92. package/src/core/running-server.mjs +76 -0
  93. package/src/core/server-release.mjs +487 -0
  94. package/src/core/server-restart.mjs +128 -7
  95. package/src/core/source-brain-updates.mjs +35 -6
  96. package/src/core/ssss-kernel-bridge.mjs +1 -2
  97. package/src/core/startup-health.mjs +13 -2
  98. package/src/core/surface.mjs +3 -3
  99. package/src/core/task-context.mjs +9 -8
  100. package/src/core/validated-write.mjs +4 -0
  101. package/src/core/vault-files.mjs +8 -2
  102. package/src/core/vault.mjs +1 -1
  103. package/src/server/index.mjs +17 -1
@@ -311,6 +311,13 @@ function printHelp() {
311
311
  the provider rejects the key; a value another key shares is refused.
312
312
  --yes Revoke it
313
313
  --json Machine-readable output
314
+ gh-login status Compare this machine's gh login with github_token (no values)
315
+ gh-login adopt Save gh's token as github_token for every machine that holds a grant.
316
+ Shows the scope change unless --yes is given.
317
+ --yes Do it
318
+ --revoke-old Also end the previous token at GitHub, once the new one is in place
319
+ gh-login use-store Give up this machine's own gh login and sign in from github_token.
320
+ Shows the plan unless --yes is given.
314
321
  rotation-status Rotation coverage for every key in the vault
315
322
  --json Machine-readable output
316
323
  browser-logout Delete TR's persistent browser profile (all sessions)
@@ -429,6 +436,7 @@ function parseArgs(args) {
429
436
  'rotate-auto',
430
437
  'provider-keys',
431
438
  'revoke',
439
+ 'gh-login',
432
440
  'rm',
433
441
  'generate',
434
442
  'account-sync',
@@ -629,6 +637,9 @@ function parseArgs(args) {
629
637
  case '--revoke':
630
638
  out.revokeHash = args[++i];
631
639
  break;
640
+ case '--revoke-old':
641
+ out.revokeOld = true;
642
+ break;
632
643
  case '--yes':
633
644
  out.yes = true;
634
645
  break;
@@ -707,7 +718,7 @@ function parseArgs(args) {
707
718
  out.keys.push(a);
708
719
  } else if (!a.startsWith('--') && !out.key) out.key = a;
709
720
  else if (!a.startsWith('--') && !out.value) out.value = a;
710
- else if (a.startsWith('--') && out.command === 'revoke') out.unknownOption = a;
721
+ else if (a.startsWith('--') && ['revoke', 'gh-login'].includes(out.command)) out.unknownOption = a;
711
722
  }
712
723
  i++;
713
724
  }
@@ -1637,6 +1648,12 @@ export default async function secretCli(argv) {
1637
1648
  return;
1638
1649
  }
1639
1650
 
1651
+ if (opts.command === 'gh-login') {
1652
+ const { runGhLogin } = await import('./cli-gh-login.mjs');
1653
+ if ((await runGhLogin(brainDir, opts)) !== 0) await exitAfterFlush(1);
1654
+ return;
1655
+ }
1656
+
1640
1657
  if (opts.command === 'delete' || opts.command === 'rm') {
1641
1658
  if (!opts.key) {
1642
1659
  console.error('Usage: total-recall auth secret delete <key>');
@@ -4,16 +4,32 @@
4
4
  * A secret that reaches a node (see secrets-sync.mjs) is only useful to people and
5
5
  * agents there once the tool that uses it is signed in. This signs `gh` in from
6
6
  * `github_token`, and wires git to it, so a token rotated once lands on every machine
7
- * without anyone logging in by hand. Nothing here prints or logs the token.
7
+ * without anyone logging in by hand.
8
+ *
9
+ * A login changed on the machine itself (`gh auth refresh -s workflow`) belongs to the operator,
10
+ * and comparing values cannot tell that from a rotation. So each node keeps the pair of tokens it
11
+ * last looked at, as fingerprints. Store token moved: rotation, sign in. `gh` token moved: someone
12
+ * else wrote it, so check both with GitHub first, and overwrite only when nothing would be lost.
13
+ *
14
+ * Nothing here prints or logs a token.
8
15
  */
9
16
  import { execFile } from 'node:child_process';
17
+ import crypto from 'node:crypto';
10
18
 
11
19
  const TIMEOUT_MS = 20_000;
20
+ const HOST = 'github.com';
21
+ const REVOKE_URL = 'https://api.github.com/credentials/revoke';
22
+ const NO_LOGIN = /no oauth token/i;
12
23
 
13
24
  /** Run a command, optionally feeding stdin; never rejects. */
14
- export function defaultRun(cmd, args, input) {
25
+ export function defaultRun(cmd, args, input, env = {}) {
15
26
  return new Promise((resolve) => {
16
- const child = execFile(cmd, args, { timeout: TIMEOUT_MS, env: { ...process.env, GH_PROMPT_DISABLED: '1', GH_NO_UPDATE_NOTIFIER: '1' } }, (error, stdout, stderr) => {
27
+ const childEnv = { ...process.env, GH_PROMPT_DISABLED: '1', GH_NO_UPDATE_NOTIFIER: '1' };
28
+ // Either of these outranks gh's stored login, so a daemon that inherited one would
29
+ // report it as the login and could not sign in at all.
30
+ delete childEnv.GH_TOKEN;
31
+ delete childEnv.GITHUB_TOKEN;
32
+ const child = execFile(cmd, args, { timeout: TIMEOUT_MS, env: { ...childEnv, ...env } }, (error, stdout, stderr) => {
17
33
  resolve({ ok: !error, code: error?.code ?? 0, stdout: String(stdout || ''), stderr: String(stderr || '') });
18
34
  });
19
35
  child.stdin?.end(input === undefined ? undefined : String(input));
@@ -22,21 +38,149 @@ export function defaultRun(cmd, args, input) {
22
38
 
23
39
  const firstLine = (text) => String(text || '').split('\n').map((l) => l.trim()).find(Boolean) || 'unknown error';
24
40
 
41
+ /** The same value `auth secret list` shortens to its fingerprint. */
42
+ export const tokenFingerprint = (value) => crypto.createHash('sha256').update(String(value)).digest('hex');
43
+
44
+ /** What `gh` holds on this machine. The token stays in memory, for the caller to compare. */
45
+ export async function readGhToken(run = defaultRun) {
46
+ const res = await run('gh', ['auth', 'token', '--hostname', HOST]);
47
+ if (res.code === 'ENOENT') return { state: 'gh-missing' };
48
+ if (res.ok && res.stdout.trim()) return { state: 'token', token: res.stdout.trim() };
49
+ if (res.ok || NO_LOGIN.test(res.stderr)) return { state: 'none' };
50
+ // A locked keychain or a timeout is not an empty login: signing in over it could lose one.
51
+ return { state: 'unreadable', error: firstLine(res.stderr) };
52
+ }
53
+
25
54
  /**
26
- * Sign `gh` in with `token` when it is not already, then point git at it.
27
- * @returns {{ status: 'no-token'|'gh-missing'|'current'|'logged-in'|'failed', error?: string }}
55
+ * Ask GitHub about one token, which travels in this child's environment only.
56
+ * `gh api --include` prints the status line even for a 401 and nothing at all when offline,
57
+ * which separates a dead token from a dead network without matching error text.
58
+ * @returns {Promise<{ state: 'ok'|'rejected'|'unknown', scopes: string[], login?: string }>}
28
59
  */
29
- export async function projectGithubLogin({ token, run = defaultRun } = {}) {
60
+ export async function probeGithubToken(token, run = defaultRun) {
61
+ const res = await run('gh', ['api', '--hostname', HOST, '--include', 'user'], undefined, { GH_TOKEN: token });
62
+ const [head] = res.stdout.split(/\r?\n\r?\n/, 1);
63
+ const status = Number(head.match(/^HTTP\/\S+ (\d{3})/)?.[1]);
64
+ if (status === 401) return { state: 'rejected', scopes: [] };
65
+ if (status !== 200) return { state: 'unknown', scopes: [] };
66
+ let login = null;
67
+ try { login = JSON.parse(res.stdout.slice(head.length)).login; } catch { /* reported as unknown below */ }
68
+ if (!login) return { state: 'unknown', scopes: [] };
69
+ const scopes = (head.match(/^x-oauth-scopes:(.*)$/im)?.[1] || '').split(',').map((s) => s.trim()).filter(Boolean).sort();
70
+ return { state: 'ok', login, scopes };
71
+ }
72
+
73
+ /** What a `gh` token can do that the store's cannot, and the reverse. */
74
+ export function compareScopes(gh, store) {
75
+ return {
76
+ added: gh.scopes.filter((scope) => !store.scopes.includes(scope)),
77
+ removed: store.scopes.filter((scope) => !gh.scopes.includes(scope)),
78
+ };
79
+ }
80
+
81
+ /** Sign `gh` in with `token` on stdin, then point git at it. */
82
+ export async function signInGh(token, run = defaultRun) {
83
+ const login = await run('gh', ['auth', 'login', '--hostname', HOST, '--git-protocol', 'https', '--with-token'], token);
84
+ if (!login.ok) return { status: 'failed', error: firstLine(login.stderr).replaceAll(token, '***') };
85
+ await run('gh', ['auth', 'setup-git', '--hostname', HOST]);
86
+ return { status: 'logged-in' };
87
+ }
88
+
89
+ /**
90
+ * Bring `gh` in step with the store, unless that would lose something the operator holds.
91
+ *
92
+ * @param {{ token?: string, record?: { store: string, gh: string }, run?: Function }} options
93
+ * `record` is the pair this node last looked at; pass back the `record` of the result.
94
+ * @returns {Promise<{
95
+ * status: 'no-token'|'gh-missing'|'current'|'logged-in'|'diverged'|'failed',
96
+ * reason?: string, first?: boolean, login?: string, store_login?: string|null,
97
+ * added?: string[], removed?: string[], replaced?: object, record?: object, error?: string,
98
+ * }>} `diverged` left `gh` alone; `first` is true only when this is news, and `record` is
99
+ * missing or unchanged when GitHub could not be asked.
100
+ */
101
+ export async function projectGithubLogin({ token, record, run = defaultRun } = {}) {
30
102
  const wanted = String(token || '').trim();
31
103
  if (!wanted) return { status: 'no-token' };
32
104
 
33
- const current = await run('gh', ['auth', 'token']);
34
- if (current.code === 'ENOENT') return { status: 'gh-missing' };
35
- if (current.ok && current.stdout.trim() === wanted) return { status: 'current' };
105
+ const held = await readGhToken(run);
106
+ if (held.state === 'gh-missing') return { status: 'gh-missing' };
107
+ if (held.state === 'unreadable') return { status: 'failed', error: held.error.replaceAll(wanted, '***') };
36
108
 
37
- const login = await run('gh', ['auth', 'login', '--hostname', 'github.com', '--git-protocol', 'https', '--with-token'], wanted);
38
- if (!login.ok) return { status: 'failed', error: firstLine(login.stderr).replaceAll(wanted, '***') };
109
+ const storeFp = tokenFingerprint(wanted);
110
+ const inStep = { store: storeFp, gh: storeFp };
111
+ const signIn = async (reason, extra = {}) => {
112
+ const result = await signInGh(wanted, run);
113
+ return result.status === 'logged-in' ? { ...result, reason, ...extra, record: inStep } : result;
114
+ };
39
115
 
40
- await run('gh', ['auth', 'setup-git', '--hostname', 'github.com']);
41
- return { status: 'logged-in' };
116
+ if (held.state === 'none') return signIn('no-login');
117
+ if (held.token === wanted) return { status: 'current', record: inStep };
118
+
119
+ const ghFp = tokenFingerprint(held.token);
120
+ if (record?.gh === ghFp) {
121
+ // Nothing moved: this difference was already reported, so stay quiet and off the network.
122
+ if (record.store === storeFp) return { status: 'diverged', first: false, reason: record.reason, login: record.login, store_login: record.store_login, added: record.added, removed: record.removed, record };
123
+ // Only the store moved. A rotation is deliberate, so it lands, over a local login too.
124
+ return signIn('rotation', record.gh === record.store ? {} : { replaced: { reason: record.reason, login: record.login, added: record.added, removed: record.removed } });
125
+ }
126
+
127
+ const [gh, store] = await Promise.all([probeGithubToken(held.token, run), probeGithubToken(wanted, run)]);
128
+ if (gh.state === 'rejected') return signIn('gh-token-rejected');
129
+ // Offline, or GitHub answered oddly: not knowing is no reason to overwrite. Look again next cycle.
130
+ if (gh.state !== 'ok' || store.state === 'unknown') return { status: 'diverged', first: false, reason: 'unverified', record };
131
+
132
+ const keep = (reason, scopes = { added: [], removed: [] }) => {
133
+ const next = { store: storeFp, gh: ghFp, reason, login: gh.login, store_login: store.login ?? null, ...scopes };
134
+ return { status: 'diverged', first: true, reason, login: next.login, store_login: next.store_login, ...scopes, record: next };
135
+ };
136
+ if (store.state === 'rejected') return keep('store-token-rejected');
137
+ if (gh.login !== store.login) return keep('different-account');
138
+ const scopes = compareScopes(gh, store);
139
+ const sameScopes = !scopes.added.length && !scopes.removed.length;
140
+ // A local grant was removed again: same account, same powers, so nothing to lose.
141
+ if (sameScopes && record && record.gh !== record.store) return signIn('same-scopes');
142
+ // Otherwise someone asked for this new token on purpose, usually to rotate. Keep it.
143
+ return keep(sameScopes ? 'new-token' : 'different-scopes', scopes);
144
+ }
145
+
146
+ /** Read-only comparison for operators. Holds fingerprints only, never a token. */
147
+ export async function inspectGithubLogin({ token, run = defaultRun } = {}) {
148
+ const wanted = String(token || '').trim();
149
+ if (!wanted) return { state: 'no-token' };
150
+ const held = await readGhToken(run);
151
+ if (held.state === 'gh-missing') return { state: 'gh-missing' };
152
+ if (held.state === 'unreadable') return { state: 'unreadable', error: held.error.replaceAll(wanted, '***') };
153
+ const store = { fingerprint: tokenFingerprint(wanted).slice(0, 12), ...(await probeGithubToken(wanted, run)) };
154
+ if (held.state === 'none') return { state: 'no-login', store };
155
+ if (held.token === wanted) return { state: 'same', store };
156
+ const gh = { fingerprint: tokenFingerprint(held.token).slice(0, 12), ...(await probeGithubToken(held.token, run)) };
157
+ // A rejected or unreachable token reports no scopes, which is not the same as holding none.
158
+ return { state: 'diverged', gh, store, ...(gh.state === 'ok' && store.state === 'ok' ? compareScopes(gh, store) : { added: [], removed: [] }) };
159
+ }
160
+
161
+ /**
162
+ * End a token at GitHub. This endpoint refuses authenticated requests, so it goes out without `gh`,
163
+ * and it answers 202 before anything happens: only GitHub rejecting the token afterwards is proof.
164
+ */
165
+ export async function revokeGithubToken(token, {
166
+ run = defaultRun,
167
+ fetchImpl = fetch,
168
+ sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms)),
169
+ attempts = 5,
170
+ waitMs = 2000,
171
+ } = {}) {
172
+ let requestStatus = null;
173
+ try {
174
+ const res = await fetchImpl(REVOKE_URL, {
175
+ method: 'POST',
176
+ headers: { accept: 'application/vnd.github+json', 'content-type': 'application/json', 'user-agent': 'total-recall' },
177
+ body: JSON.stringify({ credentials: [token] }),
178
+ });
179
+ requestStatus = res.status;
180
+ } catch { /* the check below decides */ }
181
+ for (let attempt = 0; attempt < attempts; attempt += 1) {
182
+ if (attempt) await sleep(waitMs);
183
+ if ((await probeGithubToken(token, run)).state === 'rejected') return { revoked: true, request_status: requestStatus };
184
+ }
185
+ return { revoked: false, request_status: requestStatus };
42
186
  }
@@ -3,8 +3,18 @@ import crypto from 'node:crypto';
3
3
  import { getLeaderInfo, isLeader } from './leader-election.mjs';
4
4
  import { logger } from './logger.mjs';
5
5
  import { brainDir } from './config.mjs';
6
- import { resolveSecretsPath, loadSecrets, saveSecrets, ensureMeta, appendAuditAsync, META_KEY } from './secrets-store.mjs';
7
- import { projectGithubLogin } from './login-projections.mjs';
6
+ import { resolveSecretsPath, loadSecrets, saveSecrets, ensureMeta, appendAuditAsync, rotateSecret, META_KEY } from './secrets-store.mjs';
7
+ import {
8
+ compareScopes,
9
+ defaultRun,
10
+ inspectGithubLogin,
11
+ probeGithubToken,
12
+ projectGithubLogin,
13
+ readGhToken,
14
+ revokeGithubToken,
15
+ signInGh,
16
+ tokenFingerprint,
17
+ } from './login-projections.mjs';
8
18
 
9
19
  const REQUEST_TIMEOUT_MS = 10_000;
10
20
  const FETCH_CONCURRENCY = 8;
@@ -33,6 +43,21 @@ export function secretsSyncMode(env = process.env) {
33
43
  return 'mesh-identity';
34
44
  }
35
45
 
46
+ /** The keys the leader says this machine may read. */
47
+ async function listGrantedKeys(base, fetchImpl) {
48
+ let listResponse;
49
+ try {
50
+ listResponse = await fetchImpl(base, { signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS) });
51
+ } catch (err) {
52
+ // "fetch failed" alone named neither the address nor the reason.
53
+ throw new Error(`No Total Recall server answered at ${base}: ${err.cause?.code || err.message}`);
54
+ }
55
+ if (!listResponse.ok) throw new Error(`Leader returned ${listResponse.status}`);
56
+ const { keys } = await listResponse.json();
57
+ if (!Array.isArray(keys)) throw new Error('Invalid grant list');
58
+ return keys;
59
+ }
60
+
36
61
  /**
37
62
  * Grant-based sync: ask the leader which keys this machine may read, fetch
38
63
  * only those, and merge them into this node's own encrypted store. Keys that
@@ -45,16 +70,7 @@ export async function syncGrantedSecrets(leaderIp, { dir = brainDir, fetchImpl =
45
70
  // The leader's port, never this node's: the droplet serves on 3900 while devices
46
71
  // default to 3000, and assuming our own port made every sync fail.
47
72
  const base = meshUrl(leaderIp, '/api/secrets/mesh', port || serverPort());
48
- let listResponse;
49
- try {
50
- listResponse = await fetchImpl(base, { signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS) });
51
- } catch (err) {
52
- // "fetch failed" alone named neither the address nor the reason.
53
- throw new Error(`No Total Recall server answered at ${base}: ${err.cause?.code || err.message}`);
54
- }
55
- if (!listResponse.ok) throw new Error(`Leader returned ${listResponse.status}`);
56
- const { keys } = await listResponse.json();
57
- if (!Array.isArray(keys)) throw new Error('Invalid grant list');
73
+ const keys = await listGrantedKeys(base, fetchImpl);
58
74
 
59
75
  // Each key costs the leader a store decryption plus a mesh round trip (2-3 s from a laptop),
60
76
  // so a few at a time keeps a full sync under a minute instead of several.
@@ -93,15 +109,143 @@ export async function syncGrantedSecrets(leaderIp, { dir = brainDir, fetchImpl =
93
109
  return { granted: keys.length, updated, removed: removed.length };
94
110
  }
95
111
 
96
- /** Sign in the tools that use synced secrets (today: gh from github_token). */
97
- export async function projectLogins({ dir = brainDir, run } = {}) {
98
- const secrets = await loadSecrets(dir);
99
- const github = await projectGithubLogin({ token: secrets.github_token || secrets.GITHUB_TOKEN, run });
100
- if (github.status === 'logged-in') await appendAuditAsync(dir, { action: 'login_projection', key: 'github_token -> gh', actor: 'mesh-sync' });
112
+ const githubKeyName = (secrets) => (secrets.GITHUB_TOKEN && !secrets.github_token ? 'GITHUB_TOKEN' : 'github_token');
113
+
114
+ /** Reloads first: the checks take seconds, and a key saved meanwhile must survive. */
115
+ async function saveGithubRecord(dir, record) {
116
+ const secrets = ensureMeta(await loadSecrets(dir));
117
+ secrets[META_KEY].login_projections = { ...secrets[META_KEY].login_projections, gh: { ...record, at: new Date().toISOString() } };
118
+ await saveSecrets(dir, secrets);
119
+ }
120
+
121
+ /** One line on what a kept `gh` login holds that the store does not. */
122
+ function describeDifference({ reason, login, store_login: storeLogin, added = [], removed = [] }) {
123
+ if (reason === 'store-token-rejected') return 'GitHub rejects the stored token';
124
+ if (reason === 'different-account') return `gh is signed in as ${login}, the stored token belongs to ${storeLogin}`;
125
+ if (reason === 'new-token') return 'a new token with the same scopes';
126
+ return [added.length ? `adds ${added.join(', ')}` : '', removed.length ? `drops ${removed.join(', ')}` : ''].filter(Boolean).join('; ');
127
+ }
128
+
129
+ /** A desktop notice, once per token. Best effort: the audit log is the record. */
130
+ async function alertOperator(run, eventId, title, message) {
131
+ await run('total-recall', ['alerts', 'send', '--title', title, '--message', message, '--channels', 'os', '--severity', 'info', '--source', 'auth', '--event-id', eventId]);
132
+ }
133
+
134
+ async function reportGithubLogin(dir, github, run) {
101
135
  if (github.status === 'failed') logger.error('secrets-sync', 'gh login from github_token failed', { error: github.error });
136
+ if (github.status === 'logged-in') {
137
+ await appendAuditAsync(dir, { action: 'login_projection', key: `github_token -> gh (${github.reason})`, actor: 'mesh-sync' });
138
+ if (github.replaced) {
139
+ const message = `A rotated github_token replaced this machine's own gh login (${describeDifference(github.replaced)}).`;
140
+ logger.warn('secrets-sync', message);
141
+ await alertOperator(run, `gh-login-replaced-${github.record.store.slice(0, 12)}`, 'gh login replaced by rotation', message);
142
+ }
143
+ }
144
+ if (github.status === 'diverged' && github.first) {
145
+ const difference = describeDifference(github);
146
+ await appendAuditAsync(dir, { action: 'login_projection_kept', key: `gh differs from github_token (${difference})`, actor: 'mesh-sync' });
147
+ const message = `gh on this machine differs from github_token (${difference}), so the sync left it alone. See: total-recall auth secret gh-login status`;
148
+ logger.warn('secrets-sync', message);
149
+ await alertOperator(run, `gh-login-kept-${github.record.gh.slice(0, 12)}`, 'gh login kept', message);
150
+ }
151
+ }
152
+
153
+ /** Sign in the tools that use synced secrets (today: gh from github_token). */
154
+ export async function projectLogins({ dir = brainDir, run = defaultRun } = {}) {
155
+ const secrets = ensureMeta(await loadSecrets(dir));
156
+ const record = secrets[META_KEY].login_projections?.gh;
157
+ const github = await projectGithubLogin({ token: secrets.github_token || secrets.GITHUB_TOKEN, record, run });
158
+ if (github.record && (github.record.store !== record?.store || github.record.gh !== record?.gh)) await saveGithubRecord(dir, github.record);
159
+ await reportGithubLogin(dir, github, run);
102
160
  return { github };
103
161
  }
104
162
 
163
+ /** How `gh` on this machine compares with the store. Changes nothing. */
164
+ export async function githubLoginStatus({ dir = brainDir, run = defaultRun } = {}) {
165
+ const secrets = await loadSecrets(dir);
166
+ return inspectGithubLogin({ token: secrets.github_token || secrets.GITHUB_TOKEN, run });
167
+ }
168
+
169
+ /** Give up this machine's own `gh` login and sign in from the store. Plans unless `yes`. */
170
+ export async function useStoreGithubLogin({ dir = brainDir, yes = false, run = defaultRun } = {}) {
171
+ const secrets = await loadSecrets(dir);
172
+ const token = secrets.github_token || secrets.GITHUB_TOKEN;
173
+ if (!token) throw new Error('The store holds no github_token');
174
+ const status = await inspectGithubLogin({ token, run });
175
+ if (status.state === 'same') return { ok: true, changed: false, ...status };
176
+ if (!yes) return { ok: true, planned: true, ...status };
177
+ const result = await signInGh(token, run);
178
+ if (result.status !== 'logged-in') return { ok: false, changed: false, error: result.error };
179
+ const fingerprint = tokenFingerprint(token);
180
+ await saveGithubRecord(dir, { store: fingerprint, gh: fingerprint });
181
+ await appendAuditAsync(dir, { action: 'login_projection', key: 'github_token -> gh (operator)', actor: 'cli' });
182
+ return { ok: true, changed: true, ...status };
183
+ }
184
+
185
+ /**
186
+ * Save this machine's `gh` token as the mesh's `github_token`. This is what widens a credential
187
+ * across the mesh, so it plans unless `yes`. Only the leader's store reaches other nodes, and the
188
+ * next sync would overwrite a follower's own copy, so a follower writes to the leader over the mesh.
189
+ */
190
+ export async function adoptGithubLogin({ dir = brainDir, yes = false, revokeOld = false, run = defaultRun, exec, fetchImpl = fetch, sleep } = {}) {
191
+ const secrets = await loadSecrets(dir);
192
+ const key = githubKeyName(secrets);
193
+ const previous = secrets[key];
194
+ const held = await readGhToken(run);
195
+ if (held.state !== 'token') throw new Error(held.error || 'gh holds no token to adopt. Sign in with gh first; nothing changed.');
196
+ if (!previous) throw new Error(`The store holds no ${key}; nothing changed. Save it with: total-recall auth secret set ${key} --stdin`);
197
+ const fingerprint = tokenFingerprint(held.token);
198
+ if (held.token === previous) return { ok: true, adopted: false, state: 'same', key, fingerprint: fingerprint.slice(0, 12) };
199
+
200
+ const [gh, store] = await Promise.all([probeGithubToken(held.token, run), probeGithubToken(previous, run)]);
201
+ if (gh.state === 'rejected') throw new Error("GitHub rejects gh's token; nothing changed. Sign in with gh again first.");
202
+ if (gh.state !== 'ok' || store.state === 'unknown') throw new Error('Could not check both tokens with GitHub; nothing changed.');
203
+ if (store.state === 'ok' && store.login !== gh.login) throw new Error(`gh is signed in as ${gh.login}, but ${key} belongs to ${store.login}; nothing changed.`);
204
+
205
+ const leader = await isLeader();
206
+ const leaderInfo = leader ? null : await getLeaderInfo();
207
+ if (!leader && !leaderInfo?.ip) throw new Error('No leader found to write to; nothing changed.');
208
+ const plan = {
209
+ key,
210
+ login: gh.login,
211
+ target: leader ? 'this machine' : leaderInfo.ip,
212
+ gh: { fingerprint: fingerprint.slice(0, 12), scopes: gh.scopes },
213
+ store: { fingerprint: tokenFingerprint(previous).slice(0, 12), state: store.state, scopes: store.scopes },
214
+ // A rejected token reports no scopes, which is not the same as holding none.
215
+ ...(store.state === 'ok' ? compareScopes(gh, store) : { added: [], removed: [] }),
216
+ revoke_old: revokeOld,
217
+ };
218
+ if (!yes) return { ok: true, planned: true, ...plan };
219
+
220
+ if (leader) {
221
+ await rotateSecret(dir, key, held.token, { actor: 'gh-login-adopt' });
222
+ } else {
223
+ // Checked first: without a grant the leader would change and this machine would never catch up.
224
+ const granted = await listGrantedKeys(meshUrl(leaderInfo.ip, '/api/secrets/mesh', leaderInfo.port || serverPort()), fetch);
225
+ if (!granted.includes(key)) throw new Error(`This machine holds no grant for ${key}, so it would not receive the token back; nothing changed.`);
226
+ const runOnLeader = exec || (await import('./mesh.mjs')).execMeshCommand;
227
+ const rotated = await runOnLeader(leaderInfo.ip, `total-recall auth secret rotate ${key} --stdin`, { input: held.token, timeoutMs: 60_000 });
228
+ if (!rotated.success) throw new Error(`The leader did not take the token, so nothing changed: ${String(rotated.stderr || 'no detail').split('\n')[0].replaceAll(held.token, '***')}`);
229
+ // Prove it came back through the sync every other node uses.
230
+ await syncGrantedSecrets(leaderInfo.ip, { dir, port: leaderInfo.port });
231
+ if ((await loadSecrets(dir))[key] !== held.token) throw new Error(`The leader took the token, but this machine did not get it back. Check that its grant includes ${key}. The old token was left working.`);
232
+ // The leader runs no sync of its own, so its gh would keep the old token.
233
+ const leaderLogin = await runOnLeader(leaderInfo.ip, 'gh auth login --hostname github.com --git-protocol https --with-token && gh auth setup-git --hostname github.com', { input: held.token, timeoutMs: 60_000 });
234
+ plan.leader_gh = leaderLogin.success ? 'signed-in' : 'failed';
235
+ }
236
+ await saveGithubRecord(dir, { store: fingerprint, gh: fingerprint });
237
+ await appendAuditAsync(dir, { action: 'login_adopted', key: `gh -> ${key}${plan.added.length ? ` (adds ${plan.added.join(', ')})` : ''}`, actor: 'cli' });
238
+
239
+ if (!revokeOld || store.state !== 'ok') return { ok: plan.leader_gh !== 'failed', adopted: true, ...plan };
240
+ // Never end the old token while a machine still signs in with it.
241
+ if (plan.leader_gh === 'failed') return { ok: false, adopted: true, ...plan, revocation: { revoked: false, skipped: "the leader's gh still uses the old token" } };
242
+ const revocation = await revokeGithubToken(previous, { run, fetchImpl, ...(sleep ? { sleep } : {}) });
243
+ await appendAuditAsync(dir, { action: revocation.revoked ? 'revoke' : 'revoke_failed', key: `${key} (previous value)`, actor: 'cli' });
244
+ // GitHub does not say whether this ends only the one token, so check the new one still works.
245
+ revocation.new_token_works = (await probeGithubToken(held.token, run)).state === 'ok';
246
+ return { ok: revocation.revoked && revocation.new_token_works, adopted: true, ...plan, revocation };
247
+ }
248
+
105
249
  export async function syncLoop() {
106
250
  secretsSyncMode();
107
251
  if (!(await isLeader())) {
@@ -5,7 +5,7 @@ One-shot, fail-closed code-quality gates for any repository. The plugin ships a
5
5
  ## What it does
6
6
 
7
7
  - Runs each configured gate once, as a background job, and writes `reports/latest.json`. There is no daemon.
8
- - Gate kinds: a command (typecheck, lint, tests, conformance suites) with an output parser (`tsc`, `eslint-stylish`, `eslint-compact`, `biome`, `flake8`, `mypy`, `black`, `generic`), or `grep-forbid`, which turns a written repository invariant into a failing gate.
8
+ - Gate kinds: a command (typecheck, lint, tests, conformance suites) with an output parser (`tsc`, `eslint-stylish`, `eslint-compact`, `biome`, `flake8`, `mypy`, `black`, `generic`), or `grep-forbid`, which turns a written repository invariant into a failing gate. A `grep-forbid` pattern can read its terms from a file with `patternFile`, for words a published config must not contain.
9
9
  - Tiers (`fast`, `full`, `remote`) keep heavy work off small machines. A bounded run lists the gates it skipped.
10
10
  - Locking is fail-closed and machine-wide, so only one check runs per host. `--steal-lock` recovers a dead or unverifiable lock and refuses to take one from a verified running check.
11
11
  - Reports record coverage, the git head and source freshness. A gate that scans zero files, or a tool that fails without parseable output, is never reported as clean.
@@ -44,7 +44,7 @@ The canonical gate list is a `skill_config` record in the repository's Total Rec
44
44
 
45
45
  ## Data and network access
46
46
 
47
- The core runs only the commands in the repository's own config, with the repository as working directory. It reads tracked source files for `grep-forbid` gates and writes only `reports/` and lock files. It makes no network requests. Commands you configure may.
47
+ The core runs only the commands in the repository's own config, with the repository as working directory. It reads tracked source files and any `patternFile` for `grep-forbid` gates and writes only `reports/` and lock files. It makes no network requests. Commands you configure may.
48
48
 
49
49
  ## Development
50
50
 
@@ -104,8 +104,11 @@ function loadConfig() {
104
104
  if (c.kind === 'grep-forbid') {
105
105
  if (!Array.isArray(c.patterns) || c.patterns.length === 0) problems.push(`${where}: grep-forbid needs a non-empty "patterns" array`);
106
106
  else c.patterns.forEach((p, j) => {
107
- if (!p.pattern) problems.push(`${where}.patterns[${j}]: missing "pattern"`);
108
- else if (/^\(\?[a-z]+\)/.test(p.pattern)) {
107
+ if (!p.pattern === !p.patternFile) problems.push(`${where}.patterns[${j}]: needs exactly one of "pattern" and "patternFile"`);
108
+ else if (p.patternFile) {
109
+ try { new RegExp('x', p.flags || ''); }
110
+ catch (e) { problems.push(`${where}.patterns[${j}]: invalid flags — ${e.message}`); }
111
+ } else if (/^\(\?[a-z]+\)/.test(p.pattern)) {
109
112
  problems.push(`${where}.patterns[${j}]: JS RegExp has no inline (?i) flags — use "flags": "i" instead`);
110
113
  } else {
111
114
  try { new RegExp(p.pattern, p.flags || ''); }
@@ -503,11 +506,41 @@ function applyFallbackViolationBaseline(check, findings) {
503
506
  return retained;
504
507
  }
505
508
 
509
+ /**
510
+ * Reads a pattern's terms from a file, for words the config must not contain because the config
511
+ * is published and the words are private. Each line is a literal term; a # line is a comment.
512
+ */
513
+ function readPatternFile(file) {
514
+ let text;
515
+ try { text = readFileSync(path.resolve(REPO_ROOT, file), 'utf8'); }
516
+ catch (error) { if (error.code === 'ENOENT') return { absent: true }; throw error; }
517
+ const terms = text.split('\n').map((line) => line.trim()).filter((line) => line && !line.startsWith('#'));
518
+ return { terms };
519
+ }
520
+
506
521
  function runGrepForbid(check) {
507
522
  let findings = [];
508
- const patterns = (check.patterns || []).map((p) => ({
509
- re: new RegExp(p.pattern, p.flags || ''), message: p.message, code: p.code || 'FORBIDDEN'
510
- }));
523
+ const notes = [];
524
+ const patterns = [];
525
+ for (const p of check.patterns || []) {
526
+ const rule = { message: p.message, code: p.code || 'FORBIDDEN' };
527
+ if (!p.patternFile) { patterns.push({ ...rule, re: new RegExp(p.pattern, p.flags || '') }); continue; }
528
+ const { absent, terms } = readPatternFile(p.patternFile);
529
+ if (absent) {
530
+ // A public clone has no private list. Say the rule did not run; never call that a pass.
531
+ const note = `${check.id}: ${rule.code} did not run, ${p.patternFile} is not present.`;
532
+ notes.push(note);
533
+ console.error(` [code-quality] ${note}`);
534
+ } else if (!terms.length) {
535
+ findings.push({
536
+ file: '.agent/skills/code-quality/config.json', line: 1, col: 1,
537
+ code: 'CQ-EMPTY-LIST', check: check.id, severity: 'error',
538
+ message: `grep-forbid check "${check.id}" reads no terms from ${p.patternFile}.`, snippet: p.patternFile
539
+ });
540
+ } else {
541
+ patterns.push({ ...rule, re: new RegExp(terms.map((term) => term.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')).join('|'), p.flags || '') });
542
+ }
543
+ }
511
544
  const exts = new Set(check.extensions || config.sourceExtensions || ['.ts', '.tsx', '.js', '.jsx', '.mjs', '.cjs', '.py']);
512
545
  const ignore = (check.ignorePaths || []).map((p) => new RegExp(p));
513
546
  // Optional allowlist: when present, ONLY files matching one of these path
@@ -556,7 +589,7 @@ function runGrepForbid(check) {
556
589
  return Promise.resolve({
557
590
  id: check.id, ok: findings.length === 0, exitCode: findings.length ? 1 : 0,
558
591
  timedOut: false, durationMs: 0, findings, scanned,
559
- raw: findings.map((f) => `${f.file}:${f.line}: ${f.code} ${f.message}`).join('\n')
592
+ raw: [...notes, ...findings.map((f) => `${f.file}:${f.line}: ${f.code} ${f.message}`)].join('\n')
560
593
  });
561
594
  }
562
595
 
@@ -67,9 +67,10 @@
67
67
  "items": {
68
68
  "type": "object",
69
69
  "additionalProperties": false,
70
- "required": ["pattern"],
70
+ "oneOf": [{ "required": ["pattern"] }, { "required": ["patternFile"] }],
71
71
  "properties": {
72
72
  "pattern": { "type": "string", "minLength": 1 },
73
+ "patternFile": { "type": "string", "minLength": 1, "description": "Repo-relative file of literal terms, one a line, for words the published config must not contain. The rule reports that it did not run when the file is absent." },
73
74
  "flags": { "type": "string", "pattern": "^[dgimsuvy]*$" },
74
75
  "code": { "type": "string" },
75
76
  "message": { "type": "string" }
@@ -1,18 +1,18 @@
1
1
  {
2
2
  "name": "tr-plugin-code-quality",
3
- "version": "0.2.4",
3
+ "version": "0.2.6",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "tr-plugin-code-quality",
9
- "version": "0.2.4",
9
+ "version": "0.2.6",
10
10
  "license": "MIT",
11
11
  "dependencies": {
12
- "@gregiteen/ssss-cli": "0.14.4"
12
+ "@gregiteen/ssss-cli": "0.14.5"
13
13
  },
14
14
  "devDependencies": {
15
- "total-recall-brain": "3.42.1"
15
+ "total-recall-brain": "3.43.3"
16
16
  }
17
17
  },
18
18
  "node_modules/@alcalzone/ansi-tokenize": {
@@ -715,9 +715,9 @@
715
715
  "license": "MIT"
716
716
  },
717
717
  "node_modules/@gregiteen/ssss-cli": {
718
- "version": "0.14.4",
719
- "resolved": "https://registry.npmjs.org/@gregiteen/ssss-cli/-/ssss-cli-0.14.4.tgz",
720
- "integrity": "sha512-R6JIgPRvS9xXp0xb6BYO3rauv+72r35GIiDrJcvbjPdTSHFZXr8/jRxFOgoF8yTed/5xyGFF3ey3DI9bOjmHVA==",
718
+ "version": "0.14.5",
719
+ "resolved": "https://registry.npmjs.org/@gregiteen/ssss-cli/-/ssss-cli-0.14.5.tgz",
720
+ "integrity": "sha512-qVEYOocnwn97uJ0eDzmuwN06CYsYWr2t3+iKylpisBHX4wlLWk7P8EmFFdBlx7NyKVIZDDeYT164Gd7/T8UGgw==",
721
721
  "license": "ISC",
722
722
  "bin": {
723
723
  "ssss": "scripts/ssss.mjs"
@@ -1580,9 +1580,9 @@
1580
1580
  },
1581
1581
  "node_modules/@ssss/cli": {
1582
1582
  "name": "@gregiteen/ssss-cli",
1583
- "version": "0.14.4",
1584
- "resolved": "https://registry.npmjs.org/@gregiteen/ssss-cli/-/ssss-cli-0.14.4.tgz",
1585
- "integrity": "sha512-R6JIgPRvS9xXp0xb6BYO3rauv+72r35GIiDrJcvbjPdTSHFZXr8/jRxFOgoF8yTed/5xyGFF3ey3DI9bOjmHVA==",
1583
+ "version": "0.14.5",
1584
+ "resolved": "https://registry.npmjs.org/@gregiteen/ssss-cli/-/ssss-cli-0.14.5.tgz",
1585
+ "integrity": "sha512-qVEYOocnwn97uJ0eDzmuwN06CYsYWr2t3+iKylpisBHX4wlLWk7P8EmFFdBlx7NyKVIZDDeYT164Gd7/T8UGgw==",
1586
1586
  "dev": true,
1587
1587
  "license": "ISC",
1588
1588
  "bin": {
@@ -5543,9 +5543,9 @@
5543
5543
  }
5544
5544
  },
5545
5545
  "node_modules/total-recall-brain": {
5546
- "version": "3.42.1",
5547
- "resolved": "https://registry.npmjs.org/total-recall-brain/-/total-recall-brain-3.42.1.tgz",
5548
- "integrity": "sha512-woH7mdkGwPFYlKPZurhE8L3BPe+cSeIruNGaPOjGZJm/O8C92Bbr1gsdOY0tTLg5QHlgPHcXG1GMtsm9lPDz8w==",
5546
+ "version": "3.43.3",
5547
+ "resolved": "https://registry.npmjs.org/total-recall-brain/-/total-recall-brain-3.43.3.tgz",
5548
+ "integrity": "sha512-yjfPN/BQxEEa+iodyFXQov/6fRt1pZg/7RcmgsbpALwBt6Ki3JJjT3tzArBC7NxHQmXgGYyKnCYId6xe/W2rPg==",
5549
5549
  "dev": true,
5550
5550
  "license": "MIT",
5551
5551
  "workspaces": [
@@ -5576,6 +5576,7 @@
5576
5576
  "antigravity": "bin/antigravity.mjs",
5577
5577
  "total-recall": "bin/total-recall.mjs",
5578
5578
  "total-recall-memory": "bin/total-recall-memory.mjs",
5579
+ "total-recall-server-release": "bin/total-recall-server-release.mjs",
5579
5580
  "vibebrain": "bin/total-recall.mjs"
5580
5581
  },
5581
5582
  "optionalDependencies": {