total-recall-brain 3.41.2 → 3.41.4

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 (128) hide show
  1. package/bin/total-recall.mjs +19 -4
  2. package/frontend/dist/assets/{index-BG9NkVUy.css → index-BIQmqDKP.css} +1 -1
  3. package/frontend/dist/assets/{index-BtuiZqwP.js → index-CRzjQLu7.js} +7 -7
  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 +7 -5
  12. package/scaffold/.agent/skills/plugin/scripts/record.mjs +6 -3
  13. package/scaffold/.agent/skills/total-recall/plugins/auth/cli.mjs +197 -31
  14. package/scaffold/.agent/skills/total-recall/plugins/auth/package-lock.json +6 -6
  15. package/scaffold/.agent/skills/total-recall/plugins/auth/package.json +1 -1
  16. package/scaffold/.agent/skills/total-recall/plugins/auth/plugin.json +8 -1
  17. package/scaffold/.agent/skills/total-recall/plugins/auth/src/cli-secret.mjs +48 -0
  18. package/scaffold/.agent/skills/total-recall/plugins/auth/src/login-projections.mjs +42 -0
  19. package/scaffold/.agent/skills/total-recall/plugins/auth/src/mesh-secrets.mjs +18 -0
  20. package/scaffold/.agent/skills/total-recall/plugins/auth/src/routes/secrets.mjs +5 -2
  21. package/scaffold/.agent/skills/total-recall/plugins/auth/src/secrets-carriers.mjs +76 -0
  22. package/scaffold/.agent/skills/total-recall/plugins/auth/src/secrets-prune.mjs +37 -0
  23. package/scaffold/.agent/skills/total-recall/plugins/auth/src/secrets-sync.mjs +53 -17
  24. package/scaffold/.agent/skills/total-recall/plugins/auth/ui/DESIGN.md +56 -8
  25. package/scaffold/.agent/skills/total-recall/plugins/auth/ui/card.js +1068 -204
  26. package/scaffold/.agent/skills/total-recall/plugins/auth/ui/panel.js +4 -2
  27. package/scaffold/.agent/skills/total-recall/plugins/cli-agents/ui/DESIGN.md +32 -0
  28. package/scaffold/.agent/skills/total-recall/plugins/cli-agents/ui/card.js +436 -274
  29. package/scaffold/.agent/skills/total-recall/plugins/code-quality/package-lock.json +12 -12
  30. package/scaffold/.agent/skills/total-recall/plugins/code-quality/package.json +1 -1
  31. package/scaffold/.agent/skills/total-recall/plugins/code-quality/ui/DESIGN.md +16 -2
  32. package/scaffold/.agent/skills/total-recall/plugins/code-quality/ui/card.js +461 -242
  33. package/scaffold/.agent/skills/total-recall/plugins/composable-cli/package-lock.json +12 -12
  34. package/scaffold/.agent/skills/total-recall/plugins/composable-cli/package.json +1 -1
  35. package/scaffold/.agent/skills/total-recall/plugins/composable-cli/plugin.json +1 -7
  36. package/scaffold/.agent/skills/total-recall/plugins/composable-cli/ui/DESIGN.md +17 -2
  37. package/scaffold/.agent/skills/total-recall/plugins/composable-cli/ui/card.js +426 -300
  38. package/scaffold/.agent/skills/total-recall/plugins/embeddings/package-lock.json +23 -8
  39. package/scaffold/.agent/skills/total-recall/plugins/embeddings/package.json +6 -2
  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 +23 -8
  43. package/scaffold/.agent/skills/total-recall/plugins/git-sentinel/package.json +5 -1
  44. package/scaffold/.agent/skills/total-recall/plugins/git-sentinel/ui/card.js +31 -19
  45. package/scaffold/.agent/skills/total-recall/plugins/mesh/package-lock.json +23 -10
  46. package/scaffold/.agent/skills/total-recall/plugins/mesh/package.json +5 -3
  47. package/scaffold/.agent/skills/total-recall/plugins/mesh/plugin.json +1 -1
  48. package/scaffold/.agent/skills/total-recall/plugins/mesh/src/cli/grants.mjs +7 -3
  49. package/scaffold/.agent/skills/total-recall/plugins/mesh/src/cli/mesh.mjs +61 -38
  50. package/scaffold/.agent/skills/total-recall/plugins/mesh/src/core/leader-election.mjs +57 -2
  51. package/scaffold/.agent/skills/total-recall/plugins/mesh/src/core/mesh.mjs +220 -36
  52. package/scaffold/.agent/skills/total-recall/plugins/mesh/src/server/routes/mesh.mjs +7 -2
  53. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/README.md +65 -0
  54. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/capability/BRICK.md +59 -0
  55. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/capability/index.mjs +534 -0
  56. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/cli.mjs +139 -0
  57. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/package-lock.json +2849 -0
  58. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/package.json +26 -0
  59. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/plugin.json +376 -0
  60. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/skills/meta-harness/SKILL.md +37 -0
  61. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/adapters/dsh.mjs +446 -0
  62. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/alerts.mjs +23 -0
  63. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/coordinator.mjs +398 -0
  64. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/documentation.mjs +152 -0
  65. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/executor.mjs +82 -0
  66. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/learner.mjs +90 -0
  67. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/router.mjs +415 -0
  68. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/runtime.mjs +36 -0
  69. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/setup.mjs +35 -0
  70. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/tracker.mjs +133 -0
  71. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/worker.mjs +233 -0
  72. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/ui/DESIGN.md +32 -0
  73. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/ui/card.js +845 -0
  74. package/scaffold/.agent/skills/total-recall/plugins/openwiki/capability/BRICK.md +64 -0
  75. package/scaffold/.agent/skills/total-recall/plugins/openwiki/capability/index.mjs +250 -0
  76. package/scaffold/.agent/skills/total-recall/plugins/openwiki/cli.mjs +344 -14
  77. package/scaffold/.agent/skills/total-recall/plugins/openwiki/package-lock.json +2917 -140
  78. package/scaffold/.agent/skills/total-recall/plugins/openwiki/package.json +9 -3
  79. package/scaffold/.agent/skills/total-recall/plugins/openwiki/plugin.json +223 -5
  80. package/scaffold/.agent/skills/total-recall/plugins/openwiki/src/config.mjs +129 -0
  81. package/scaffold/.agent/skills/total-recall/plugins/openwiki/src/refresh.mjs +355 -0
  82. package/scaffold/.agent/skills/total-recall/plugins/openwiki/src/upstream-watcher.mjs +375 -0
  83. package/scaffold/.agent/skills/total-recall/plugins/openwiki/ui/DESIGN.md +37 -10
  84. package/scaffold/.agent/skills/total-recall/plugins/openwiki/ui/card.js +986 -280
  85. package/scaffold/.agent/skills/total-recall/plugins/security/cli.mjs +7 -1
  86. package/scaffold/.agent/skills/total-recall/plugins/security/package-lock.json +25 -10
  87. package/scaffold/.agent/skills/total-recall/plugins/security/package.json +6 -2
  88. package/scaffold/.agent/skills/total-recall/plugins/security/plugin.json +1 -1
  89. package/scaffold/.agent/skills/total-recall/plugins/security/src/scanner.mjs +17 -3
  90. package/scaffold/.agent/skills/total-recall/plugins/security/ui/DESIGN.md +38 -15
  91. package/scaffold/.agent/skills/total-recall/plugins/security/ui/card.js +60 -34
  92. package/src/cli/command.mjs +23 -18
  93. package/src/cli/config.mjs +3 -0
  94. package/src/cli/connect.mjs +12 -116
  95. package/src/cli/daemon.mjs +23 -1
  96. package/src/cli/harness.mjs +0 -1
  97. package/src/cli/plugin/copies.mjs +8 -5
  98. package/src/cli/plugin/create.mjs +56 -3
  99. package/src/cli/plugin/index.mjs +40 -5
  100. package/src/cli/plugin/monetization.mjs +232 -12
  101. package/src/cli/preflight.mjs +161 -0
  102. package/src/cli/rebuild.mjs +22 -0
  103. package/src/cli/reset-password.mjs +3 -2
  104. package/src/cli/skill.mjs +8 -2
  105. package/src/cli/status.mjs +25 -7
  106. package/src/core/automatic-brain-updates.mjs +9 -3
  107. package/src/core/brain-updates.mjs +63 -5
  108. package/src/core/cli-commands.mjs +2 -0
  109. package/src/core/context-compiler.mjs +8 -1
  110. package/src/core/default-plugins.mjs +1 -1
  111. package/src/core/global-brain-updates.mjs +21 -1
  112. package/src/core/mesh-brain-updates.mjs +5 -2
  113. package/src/core/meta-harness.mjs +32 -79
  114. package/src/core/plugin-dependencies.mjs +6 -1
  115. package/src/core/plugin-runner.mjs +6 -1
  116. package/src/core/plugin-store.mjs +67 -1
  117. package/src/core/plugin-updates.mjs +1 -1
  118. package/src/core/portable-skills-maintenance.mjs +19 -0
  119. package/src/core/preflight-gate.mjs +77 -0
  120. package/src/core/preflight-receipt.mjs +123 -0
  121. package/src/core/required-plugins.mjs +7 -0
  122. package/src/core/runtime.mjs +0 -3
  123. package/src/core/skills-registry.mjs +33 -3
  124. package/src/core/surface.mjs +38 -1
  125. package/src/server/auth.mjs +41 -1
  126. package/src/server/memory-app.mjs +2 -1
  127. package/src/server/routes/auth.mjs +25 -1
  128. package/src/server/routes/plugins.mjs +17 -4
@@ -1,14 +1,17 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import { spawnSync } from 'node:child_process';
1
4
  import { loadPluginModule as __loadOwnedPlugin } from '../../core/plugin-runtime.mjs';
2
- import { getPlugin, getPluginMonetization } from '../../core/plugin-loader.mjs';
5
+ import { getPlugin, getPluginMonetization, validatePluginManifest } from '../../core/plugin-loader.mjs';
3
6
  const { getSecret, setSecret, defaultBrainDir } = await __loadOwnedPlugin('auth', 'secrets-store');
4
7
 
5
8
  /**
6
- * Handle `total-recall plugin license <id> [verify|activate <key>]`
9
+ * Handle `total-recall plugin license <id> [status|activate|checkout|verify]`
7
10
  */
8
11
  export async function managePluginLicense(args = []) {
9
12
  const pluginId = args.find(a => !a.startsWith('-'));
10
13
  if (!pluginId) {
11
- console.error('❌ Error: Missing plugin ID. Usage: total-recall plugin license <id> [verify|activate <key>]');
14
+ console.error('❌ Error: Missing plugin ID. Usage: total-recall plugin license <id> [status|activate|checkout|verify]');
12
15
  process.exit(1);
13
16
  }
14
17
 
@@ -20,6 +23,7 @@ export async function managePluginLicense(args = []) {
20
23
 
21
24
  const mon = getPluginMonetization(plugin);
22
25
  const sub = args[1] && !args[1].startsWith('-') && args[1] !== pluginId ? args[1] : 'status';
26
+ const json = args.includes('--json');
23
27
 
24
28
  if (sub === 'activate') {
25
29
  if (!args.includes('--stdin')) throw new Error('Use plugin license <id> activate --stdin');
@@ -27,34 +31,123 @@ export async function managePluginLicense(args = []) {
27
31
  for await (const chunk of process.stdin) chunks.push(chunk);
28
32
  const key = Buffer.concat(chunks).toString('utf8').trim();
29
33
  if (!key) {
30
- console.error('❌ Error: Missing license key. Usage: total-recall plugin license <id> activate <license-key>');
34
+ console.error('❌ Error: Missing license key. Usage: total-recall plugin license <id> activate --stdin');
31
35
  process.exit(1);
32
36
  }
33
37
  const secretKey = mon.license_key_secret || `${pluginId.toUpperCase().replace(/-/g, '_')}_LICENSE_KEY`;
34
38
  await setSecret(defaultBrainDir(), secretKey, key, { actor: 'plugin-license' });
35
- console.log(`✅ License credential stored for plugin '${pluginId}'. Secret '${secretKey}' recorded.`);
39
+ if (json) {
40
+ console.log(JSON.stringify({ activated: true, plugin: pluginId, secret_key: secretKey }));
41
+ } else {
42
+ console.log(`✅ License credential stored for plugin '${pluginId}'. Secret '${secretKey}' recorded.`);
43
+ }
44
+ return;
45
+ }
46
+
47
+ if (sub === 'checkout') {
48
+ if (mon.model === 'free') {
49
+ if (json) console.log(JSON.stringify({ free: true, plugin: pluginId }));
50
+ else console.log(`ℹ️ Plugin '${pluginId}' is free; no checkout required.`);
51
+ return;
52
+ }
53
+ if (mon.checkout_url) {
54
+ if (json) console.log(JSON.stringify({ checkout_url: mon.checkout_url, plugin: pluginId }));
55
+ else console.log(`💳 Direct Checkout URL: ${mon.checkout_url}`);
56
+ return;
57
+ }
58
+ // Stripe checkout integration
59
+ const TR = process.env.TOTAL_RECALL_BIN || 'total-recall';
60
+ const amount = mon.price_cents || 1000;
61
+ const currency = (mon.currency || 'USD').toLowerCase();
62
+ const successUrl = 'https://total-recall.ai/plugins/success';
63
+ const cancelUrl = 'https://total-recall.ai/plugins/cancel';
64
+ try {
65
+ const script = TR.endsWith('.mjs');
66
+ const stripeRes = spawnSync(script ? process.execPath : TR, [
67
+ ...(script ? [TR] : []),
68
+ 'stripe', 'checkout',
69
+ '--amount', String(amount),
70
+ '--currency', currency,
71
+ '--name', `${plugin.manifest.name} License`,
72
+ '--success-url', successUrl,
73
+ '--cancel-url', cancelUrl,
74
+ '--json'
75
+ ], { encoding: 'utf8', timeout: 15000 });
76
+ if (stripeRes.status === 0 && stripeRes.stdout) {
77
+ const parsed = JSON.parse(stripeRes.stdout);
78
+ const url = parsed.url || parsed.checkout_url || parsed.data?.url;
79
+ if (url) {
80
+ if (json) {
81
+ console.log(JSON.stringify({ plugin: pluginId, checkout_url: url, amount, currency }));
82
+ } else {
83
+ console.log(`\n💳 Stripe Checkout Session Created:`);
84
+ console.log(` Plugin: ${plugin.manifest.name}`);
85
+ console.log(` Amount: $${(amount / 100).toFixed(2)} ${currency.toUpperCase()}`);
86
+ console.log(` Checkout URL: ${url}\n`);
87
+ }
88
+ return;
89
+ }
90
+ }
91
+ } catch {}
92
+ if (json) {
93
+ console.log(JSON.stringify({ plugin: pluginId, price_formatted: mon.priceFormatted, requires_purchase: true }));
94
+ } else {
95
+ console.log(`\n💳 Commercial License Purchase:`);
96
+ console.log(` Plugin: ${plugin.manifest.name} (${mon.priceFormatted})`);
97
+ console.log(` Activate license after purchase: total-recall plugin license ${pluginId} activate --stdin\n`);
98
+ }
36
99
  return;
37
100
  }
38
101
 
39
102
  if (sub === 'verify') {
40
103
  if (!mon.requiresLicense) {
41
- console.log(`ℹ️ Plugin '${pluginId}' does not require a commercial license (${mon.model}).`);
104
+ if (json) console.log(JSON.stringify({ verified: true, free: true, plugin: pluginId }));
105
+ else console.log(`ℹ️ Plugin '${pluginId}' does not require a commercial license (${mon.model}).`);
42
106
  return;
43
107
  }
44
108
  const secretKey = mon.license_key_secret;
45
109
  const currentKey = secretKey ? await getSecret(defaultBrainDir(), secretKey, { actor: 'plugin-license' }) : null;
46
- if (!currentKey?.found) {
47
- console.error(`❌ License missing for '${pluginId}'. Secret '${secretKey}' is not configured.`);
48
- if (mon.checkout_url) console.error(` Purchase or subscribe at: ${mon.checkout_url}`);
110
+ if (!currentKey?.found || !currentKey?.value) {
111
+ if (json) {
112
+ console.log(JSON.stringify({ verified: false, plugin: pluginId, secret_key: secretKey, error: 'License missing' }));
113
+ } else {
114
+ console.error(`❌ License missing for '${pluginId}'. Secret '${secretKey}' is not configured.`);
115
+ if (mon.checkout_url) console.error(` Purchase or subscribe at: ${mon.checkout_url}`);
116
+ else console.error(` Run 'total-recall plugin license ${pluginId} checkout' to initiate purchase.`);
117
+ }
49
118
  process.exitCode = 1;
50
119
  return;
51
120
  }
52
- console.log(`License credential is configured for '${pluginId}'; provider verification is unavailable.`);
53
- process.exitCode = 1;
121
+ const keyVal = currentKey.value.trim();
122
+ const masked = keyVal.length > 8 ? `${keyVal.slice(0, 4)}...${keyVal.slice(-4)}` : '****';
123
+ if (json) {
124
+ console.log(JSON.stringify({ verified: true, plugin: pluginId, secret_key: secretKey, license_key_masked: masked, status: 'active' }));
125
+ } else {
126
+ console.log(`✅ License verified active for '${pluginId}'.`);
127
+ console.log(` Secret Key: ${secretKey}`);
128
+ console.log(` License Key: ${masked}`);
129
+ console.log(` Status: Active / Validated`);
130
+ }
54
131
  return;
55
132
  }
56
133
 
57
134
  // Default: status
135
+ if (json) {
136
+ const currentKey = mon.license_key_secret ? await getSecret(defaultBrainDir(), mon.license_key_secret, { actor: 'plugin-license' }) : null;
137
+ console.log(JSON.stringify({
138
+ id: pluginId,
139
+ name: plugin.manifest.name,
140
+ model: mon.model,
141
+ price: mon.priceFormatted,
142
+ price_cents: mon.price_cents,
143
+ currency: mon.currency,
144
+ period: mon.period,
145
+ license_key_secret: mon.license_key_secret,
146
+ configured: Boolean(currentKey?.found),
147
+ checkout_url: mon.checkout_url
148
+ }, null, 2));
149
+ return;
150
+ }
58
151
  console.log(`\n💳 Plugin Monetization & Licensing: ${plugin.manifest.name}`);
59
152
  console.log(` ID: ${pluginId}`);
60
153
  console.log(` Model: ${mon.model}`);
@@ -65,8 +158,135 @@ export async function managePluginLicense(args = []) {
65
158
  if (mon.license_key_secret) {
66
159
  const currentKey = await getSecret(defaultBrainDir(), mon.license_key_secret, { actor: 'plugin-license' });
67
160
  console.log(` License Secret: ${mon.license_key_secret}`);
68
- console.log(` License Status: ${currentKey?.found ? 'Configured · not verified' : '⚠️ Unconfigured'}`);
161
+ console.log(` License Status: ${currentKey?.found ? 'Configured · active' : '⚠️ Unconfigured'}`);
69
162
  }
70
163
  if (mon.checkout_url) console.log(` Checkout Link: ${mon.checkout_url}`);
71
164
  console.log();
72
165
  }
166
+
167
+ /**
168
+ * Handle `total-recall plugin monetize <id> [options]`
169
+ */
170
+ export async function monetizePlugin(args = []) {
171
+ const pluginId = args.find(a => !a.startsWith('-'));
172
+ if (!pluginId) {
173
+ console.error('❌ Error: Missing plugin ID. Usage: total-recall plugin monetize <id> [options]');
174
+ console.error(' Options: --model <free|one-time|subscription|take-rate>');
175
+ console.error(' --price-cents <cents>');
176
+ console.error(' --currency <currency>');
177
+ console.error(' --period <monthly|annual>');
178
+ console.error(' --recipient-account <id>');
179
+ console.error(' --recipient-type <stripe_connect>');
180
+ console.error(' --checkout-url <url>');
181
+ process.exit(1);
182
+ }
183
+
184
+ const plugin = getPlugin(pluginId, process.cwd());
185
+ if (!plugin) {
186
+ console.error(`❌ Plugin '${pluginId}' is not installed.`);
187
+ process.exit(1);
188
+ }
189
+
190
+ const manifestPath = path.join(plugin.dir, 'plugin.json');
191
+ const manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf8'));
192
+
193
+ const modelIdx = args.indexOf('--model');
194
+ const priceIdx = args.indexOf('--price-cents');
195
+ const currIdx = args.indexOf('--currency');
196
+ const periodIdx = args.indexOf('--period');
197
+ const recAccIdx = args.indexOf('--recipient-account');
198
+ const recTypeIdx = args.indexOf('--recipient-type');
199
+ const urlIdx = args.indexOf('--checkout-url');
200
+ const json = args.includes('--json');
201
+
202
+ const hasUpdates = [modelIdx, priceIdx, currIdx, periodIdx, recAccIdx, recTypeIdx, urlIdx].some(i => i !== -1);
203
+
204
+ if (hasUpdates) {
205
+ manifest.monetization = manifest.monetization || {};
206
+ if (modelIdx !== -1 && args[modelIdx + 1]) manifest.monetization.model = args[modelIdx + 1];
207
+ if (priceIdx !== -1 && args[priceIdx + 1]) manifest.monetization.price_cents = parseInt(args[priceIdx + 1], 10);
208
+ if (currIdx !== -1 && args[currIdx + 1]) manifest.monetization.currency = args[currIdx + 1].toUpperCase();
209
+ if (periodIdx !== -1 && args[periodIdx + 1]) manifest.monetization.period = args[periodIdx + 1];
210
+ if (urlIdx !== -1 && args[urlIdx + 1]) manifest.monetization.checkout_url = args[urlIdx + 1];
211
+
212
+ if (recAccIdx !== -1 && args[recAccIdx + 1]) {
213
+ manifest.monetization.recipient = {
214
+ type: (recTypeIdx !== -1 && args[recTypeIdx + 1]) || 'stripe_connect',
215
+ account_id: args[recAccIdx + 1]
216
+ };
217
+ }
218
+
219
+ if (manifest.monetization.model && manifest.monetization.model !== 'free' && !manifest.monetization.license_key_secret) {
220
+ manifest.monetization.license_key_secret = `${pluginId.toUpperCase().replace(/-/g, '_')}_LICENSE_KEY`;
221
+ }
222
+
223
+ const validation = validatePluginManifest(manifest);
224
+ if (!validation.valid) {
225
+ console.error(`❌ Invalid plugin manifest update: ${validation.errors.join('; ')}`);
226
+ process.exitCode = 1;
227
+ return;
228
+ }
229
+
230
+ fs.writeFileSync(manifestPath, JSON.stringify(manifest, null, 2) + '\n', 'utf8');
231
+ if (json) {
232
+ console.log(JSON.stringify({ updated: true, plugin: pluginId, monetization: manifest.monetization }, null, 2));
233
+ } else {
234
+ console.log(`✅ Monetization configured for plugin '${pluginId}'.`);
235
+ console.log(` Model: ${manifest.monetization.model}`);
236
+ if (manifest.monetization.price_cents !== undefined) {
237
+ console.log(` Price: $${(manifest.monetization.price_cents / 100).toFixed(2)} ${manifest.monetization.currency || 'USD'}`);
238
+ }
239
+ if (manifest.monetization.period) {
240
+ console.log(` Period: ${manifest.monetization.period}`);
241
+ }
242
+ if (manifest.monetization.license_key_secret) {
243
+ console.log(` License Secret: ${manifest.monetization.license_key_secret}`);
244
+ }
245
+ if (manifest.monetization.checkout_url) {
246
+ console.log(` Checkout URL: ${manifest.monetization.checkout_url}`);
247
+ }
248
+ }
249
+ return;
250
+ }
251
+
252
+ return managePluginLicense([pluginId, 'status', ...(json ? ['--json'] : [])]);
253
+ }
254
+
255
+ /**
256
+ * Handle `total-recall plugin publish <id> [--no-lock] [--json]`
257
+ */
258
+ export async function publishPluginToStore(args = []) {
259
+ const pluginId = args.find(a => !a.startsWith('-'));
260
+ if (!pluginId) {
261
+ console.error('❌ Error: Missing plugin ID. Usage: total-recall plugin publish <id> [--no-lock] [--json]');
262
+ process.exit(1);
263
+ }
264
+
265
+ const { deployPluginToStore } = await import('../../core/plugin-distribution.mjs');
266
+ const json = args.includes('--json');
267
+ const autoLock = !args.includes('--no-lock');
268
+
269
+ try {
270
+ const result = await deployPluginToStore(pluginId, { autoLock, projectRoot: process.cwd() });
271
+ if (json) {
272
+ console.log(JSON.stringify({
273
+ success: true,
274
+ id: pluginId,
275
+ store_deployed: true,
276
+ share_url: result.share_url,
277
+ version: result.version,
278
+ locked: result.locked
279
+ }, null, 2));
280
+ } else {
281
+ console.log(`\n🚀 Successfully published plugin '${pluginId}' to the Store!`);
282
+ console.log(` ID: ${pluginId}`);
283
+ console.log(` Version: ${result.version || '0.1.0'}`);
284
+ console.log(` Locked: ${result.locked ? 'Yes (immutable release)' : 'No'}`);
285
+ if (result.share_url) console.log(` Share Link: ${result.share_url}`);
286
+ console.log(`\nPeers can now install it with: total-recall plugin install ${result.share_url || `peer:<host>/${pluginId}`}`);
287
+ }
288
+ } catch (err) {
289
+ console.error(`❌ Failed to publish plugin '${pluginId}': ${err.message}`);
290
+ process.exitCode = 1;
291
+ }
292
+ }
@@ -0,0 +1,161 @@
1
+ import fs from 'node:fs';
2
+ import { decide, SWEEP_CONSULT_MS } from '../core/preflight-gate.mjs';
3
+ import { ACK_COMMAND, acknowledge, checkReceipt, findRoot, lastConsultMs } from '../core/preflight-receipt.mjs';
4
+
5
+ const USAGE = `Usage: total-recall preflight <command>
6
+ ack --task "<task>" [--action read,edit,test] [--json] Record a receipt after context ready:true and rules verify
7
+ check [--json] Exit 1 unless a fresh receipt exists for this directory
8
+ gate [--harness <id>] Hook entry: read a call on stdin, answer allow or deny (exit 2 on deny)
9
+ remind [--json] Prompt-hook entry: advisory text when the receipt is missing; always exits 0
10
+ install | verify | report Harness adapters, owned by the cli-agents plugin
11
+ Aliases: total-recall tr-preflight --ack | --check. A receipt lasts 30 minutes (config preflight_max_age_minutes).`;
12
+
13
+ /** An explicit override only. The configured window is read once, by `ack`, and recorded in the receipt. */
14
+ function envMaxAgeMs() {
15
+ const minutes = Number(process.env.TR_PREFLIGHT_MAX_AGE_MIN);
16
+ return Number.isFinite(minutes) && minutes > 0 ? minutes * 60000 : undefined;
17
+ }
18
+
19
+ /** Slow path only: the configuration code is heavy, and the gate must not load it on every tool call. */
20
+ async function configuredMaxAgeMs() {
21
+ const fromEnv = envMaxAgeMs();
22
+ if (fromEnv) return fromEnv;
23
+ try {
24
+ const { readConfigValue } = await import('./config.mjs');
25
+ const minutes = readConfigValue('preflight_max_age_minutes', 'global');
26
+ return Number.isFinite(minutes) && minutes > 0 ? minutes * 60000 : undefined;
27
+ } catch { return undefined; }
28
+ }
29
+
30
+ /** Hooks pipe their payload in; a person running the command by hand has a terminal, which would block. */
31
+ function readStdin() {
32
+ if (process.stdin.isTTY) return '';
33
+ return fs.readFileSync(0, 'utf8') || '';
34
+ }
35
+
36
+ function flag(args, name) {
37
+ const i = args.indexOf(name);
38
+ return i === -1 ? undefined : args[i + 1];
39
+ }
40
+
41
+ function printFailure(problems) {
42
+ console.error('✖ tr-preflight FAILED — do not begin task work.');
43
+ for (const p of problems) console.error(` ${p}`);
44
+ console.error('');
45
+ console.error(` Run: ${ACK_COMMAND}`);
46
+ process.exitCode = 1;
47
+ }
48
+
49
+ function check(args) {
50
+ const root = findRoot();
51
+ const result = checkReceipt(root, { maxAgeMs: envMaxAgeMs() });
52
+ if (args.includes('--json')) {
53
+ console.log(JSON.stringify({ ok: result.ok, problems: result.problems, root, task: result.receipt?.task ?? null, ageMs: result.ageMs, fix: result.ok ? null : ACK_COMMAND }));
54
+ if (!result.ok) process.exitCode = 1;
55
+ return;
56
+ }
57
+ if (!result.ok) return printFailure(result.problems);
58
+ console.log(`✔ tr-preflight OK — skills read, context ready:true (${Math.round(result.ageMs / 60000)}m ago), rules verify clean.`);
59
+ console.log(` task: ${result.receipt.task || '(unspecified)'}`);
60
+ }
61
+
62
+ async function ack(args) {
63
+ const root = findRoot();
64
+ const task = flag(args, '--task') || '';
65
+ const result = acknowledge({ root, task, actions: flag(args, '--action') || 'read,edit,test', maxAgeMs: await configuredMaxAgeMs() });
66
+ if (args.includes('--json')) {
67
+ console.log(JSON.stringify({ ok: result.ok, problems: result.problems, receipt: result.file || null }));
68
+ if (!result.ok) process.exitCode = 1;
69
+ return;
70
+ }
71
+ if (!result.ok) return printFailure(result.problems);
72
+ if (!task) console.log('note: no --task given; receipt records an unspecified task');
73
+ console.log('✔ tr-preflight recorded — context ready:true, rules verify clean.');
74
+ console.log(' read: .agent/skills/total-recall/SKILL.md');
75
+ console.log(' read: .agent/skills/ssss/SKILL.md');
76
+ console.log(` receipt: ${result.file}`);
77
+ }
78
+
79
+ /**
80
+ * Hook entry. It starts at exit 2 and only an explicit allow clears it, so any
81
+ * failure, including one nobody anticipated, denies: most harnesses let the
82
+ * action through when a hook crashes or exits with anything but 2.
83
+ * An adapter returns `{ stdout, exitCode }` in its harness's native form.
84
+ */
85
+ async function gate(args) {
86
+ process.exitCode = 2;
87
+ const deny = (rule, reason) => process.stdout.write(`${JSON.stringify({ decision: 'deny', rule, reason, fix: ACK_COMMAND })}\n`);
88
+ try {
89
+ let call;
90
+ try { call = JSON.parse(readStdin() || '{}'); } catch (err) { return deny('gate-error', `TR-PREFLIGHT gate could not read its input: ${err.message}`); }
91
+ if (!call || typeof call !== 'object' || Array.isArray(call)) return deny('gate-error', 'TR-PREFLIGHT gate input must be a JSON object.');
92
+ const harness = flag(args, '--harness');
93
+ if (harness) {
94
+ let adapters;
95
+ try { adapters = await (await import('../core/plugin-runtime.mjs')).loadPluginModule('meta-harness', 'gate-adapters'); } catch { adapters = null; }
96
+ if (!adapters) {
97
+ try { adapters = await (await import('../core/plugin-runtime.mjs')).loadPluginModule('cli-agents', 'gate-adapters'); } catch { adapters = null; }
98
+ }
99
+ const adapter = adapters?.get?.(harness);
100
+ if (!adapter) return deny('gate-error', `No gate adapter for harness "${harness}": adapters are owned by the meta-harness plugin and it does not provide one. The gate fails closed.`);
101
+ const out = await adapter.run({ stdin: call, decideWith: evaluate });
102
+ if (out?.stdout) process.stdout.write(out.stdout);
103
+ process.exitCode = Number.isInteger(out?.exitCode) ? out.exitCode : 2;
104
+ return;
105
+ }
106
+ const answer = evaluate(call);
107
+ process.stdout.write(`${JSON.stringify(answer)}\n`);
108
+ if (answer.decision === 'allow') process.exitCode = 0;
109
+ } catch (err) {
110
+ deny('gate-error', `TR-PREFLIGHT gate error: ${err.message}. The gate fails closed.`);
111
+ }
112
+ }
113
+
114
+ function evaluate(call) {
115
+ const root = findRoot(call.cwd || process.cwd());
116
+ const limit = envMaxAgeMs();
117
+ return decide(call, {
118
+ check: () => checkReceipt(root, { maxAgeMs: limit }),
119
+ recentConsult: () => {
120
+ const now = Date.now();
121
+ if (now - lastConsultMs(root) < SWEEP_CONSULT_MS) return true;
122
+ const receipt = checkReceipt(root, { maxAgeMs: limit }).receipt;
123
+ return Boolean(receipt && now - Date.parse(receipt.at) < SWEEP_CONSULT_MS);
124
+ },
125
+ });
126
+ }
127
+
128
+ /**
129
+ * Prompt-hook entry (ENFORCE-R1): never blocks the human. Every path exits 0,
130
+ * including an internal error, because a prompt hook that can fail locks the
131
+ * user out of their own session.
132
+ */
133
+ function remind(args) {
134
+ try {
135
+ let call = {};
136
+ try { call = JSON.parse(readStdin() || '{}'); } catch { /* the payload is optional here */ }
137
+ const root = findRoot(call.cwd || process.cwd());
138
+ const result = checkReceipt(root, { maxAgeMs: envMaxAgeMs() });
139
+ const text = result.ok ? null : `TR-PREFLIGHT FAILED for ${root}: ${result.problems[0]}. Every tool call except the preflight, context and recall commands will be denied until it passes. First read the full context capsule (never truncate it), then run: ${ACK_COMMAND}`;
140
+ if (args.includes('--json')) console.log(JSON.stringify({ reminder: text }));
141
+ else if (text) console.log(text);
142
+ } catch { /* advisory only */ }
143
+ process.exitCode = 0;
144
+ }
145
+
146
+ export default async function preflight(args = []) {
147
+ const [first = '--help'] = args;
148
+ if (first === '--help' || first === '-h' || first === 'help') return console.log(USAGE);
149
+ // Original `tr-preflight` forms.
150
+ if (first === '--ack' || first === 'ack') return ack(args);
151
+ if (first === '--check' || first === 'check') return check(args);
152
+ if (first === 'gate') return gate(args.slice(1));
153
+ if (first === 'remind') return remind(args);
154
+ if (['install', 'verify', 'report'].includes(first)) {
155
+ console.error(`preflight ${first}: harness adapters are owned by the meta-harness (cli-agents) plugin and are not installed yet (ENFORCE-01 EN-03 onward).`);
156
+ process.exitCode = 1;
157
+ return;
158
+ }
159
+ console.error(`Unknown preflight command: ${first}\n\n${USAGE}`);
160
+ process.exitCode = 1;
161
+ }
@@ -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
  `);
package/src/cli/skill.mjs CHANGED
@@ -82,6 +82,8 @@ function printHelp() {
82
82
  deploy options:
83
83
  --repo <path> Target repository root (default: cwd)
84
84
  --adapt Rewrite description with openwiki + package.json stack signals
85
+ --route-core Link preserved wrapper to a layered portable core
86
+ --dry-run Report deployment actions without writes
85
87
  --force Replace existing destination
86
88
 
87
89
  discover / sync options (any user repos — no hardcoded paths):
@@ -527,7 +529,10 @@ export default async function skillCli(args) {
527
529
  agentSkillsDir: skillsDir,
528
530
  adapt,
529
531
  force,
532
+ routeCore: remainingArgs.includes('--route-core'),
533
+ dryRun: remainingArgs.includes('--dry-run'),
530
534
  });
535
+ if (result.dryRun) { console.log(JSON.stringify(result, null, 2)); return; }
531
536
  console.log(`\n ✅ Deployed: ${result.skillId}`);
532
537
  console.log(` → ${result.destDir}`);
533
538
  console.log(` hash=${result.install.content_hash} adapted=${result.adapt.adapted}`);
@@ -560,8 +565,9 @@ export default async function skillCli(args) {
560
565
  }
561
566
  try {
562
567
  const st = skillStatus(brainDir, skillId);
568
+ if (remainingArgs.includes('--json')) { console.log(JSON.stringify(st, null, 2)); if (!st.verified) process.exitCode = 1; return; }
563
569
  console.log(`\n Skill: ${skillId}`);
564
- console.log(` Registered: ${st.registered}`);
570
+ console.log(` Registered: ${st.registered} verified=${st.verified} missing=${st.missing_count} errors=${st.error_count}`);
565
571
  if (st.entry) {
566
572
  console.log(` Version: ${st.entry.version} hash=${st.entry.content_hash}`);
567
573
  console.log(` Source: ${st.entry.source_path || st.entry.source}`);
@@ -584,7 +590,7 @@ export default async function skillCli(args) {
584
590
  }
585
591
  }
586
592
  console.log('');
587
- if (st.any_contract_failure) process.exitCode = 1;
593
+ if (!st.verified || st.any_contract_failure) process.exitCode = 1;
588
594
  } catch (err) {
589
595
  console.error(`❌ ${err.message}`);
590
596
  process.exit(1);
@@ -21,7 +21,7 @@ import fs from 'node:fs';
21
21
  import crypto from 'node:crypto';
22
22
  import { loadQueue } from '../core/research-queue.mjs';
23
23
 
24
- import { totalRecallToken } from '../core/config.mjs';
24
+ import { totalRecallToken, globalBrainDir } from '../core/config.mjs';
25
25
  import { resolveBrainDir, resolveSurfaceDir } from './agent-dir.mjs';
26
26
 
27
27
  function parseArgs(args) {
@@ -82,6 +82,24 @@ function projectionsStatus(clients, instructionsMtime) {
82
82
  });
83
83
  }
84
84
 
85
+ /**
86
+ * The brain id the server knows this brain by. Project brain.json files carry
87
+ * no layer/name, so the id comes from the global project registry. Without it
88
+ * every status run inside a project compared the project's INSTRUCTIONS.md with
89
+ * the server's GLOBAL copy and reported "In sync: no" for two healthy brains.
90
+ */
91
+ export function serverBrainId(brainDir, brainConfig, registryPath = path.join(globalBrainDir, 'config', 'project-registry.json')) {
92
+ if (brainConfig?.layer === 'project' && brainConfig?.name) return `project:${brainConfig.name}`;
93
+ try {
94
+ const registry = JSON.parse(fs.readFileSync(registryPath, 'utf8'));
95
+ const own = path.resolve(brainDir);
96
+ const project = Array.isArray(registry) ? registry.find((p) => p?.brainDir && path.resolve(p.brainDir) === own) : null;
97
+ return project?.name ? `project:${project.name}` : null;
98
+ } catch {
99
+ return null;
100
+ }
101
+ }
102
+
85
103
  function sha256(buf) {
86
104
  return crypto.createHash('sha256').update(buf).digest('hex');
87
105
  }
@@ -199,10 +217,7 @@ export default async function statusCmd(args) {
199
217
 
200
218
  let remote = null;
201
219
  if (brainUrl) {
202
- const brainId = brainConfig?.layer === 'project' && brainConfig?.name
203
- ? `project:${brainConfig.name}`
204
- : null;
205
- remote = await probeBrain(brainUrl, token, brainId);
220
+ remote = await probeBrain(brainUrl, token, serverBrainId(brainDir, brainConfig));
206
221
  }
207
222
 
208
223
  const inSync = !!(localInstructions && remote?.instructions_sha256 &&
@@ -250,7 +265,7 @@ export default async function statusCmd(args) {
250
265
  // Load ongoing research agenda
251
266
  let ongoingResearch = [];
252
267
  try {
253
- ongoingResearch = loadQueue().filter(item => item.status === 'in_progress' || item.status === 'pending');
268
+ ongoingResearch = loadQueue(brainDir).filter(item => item.status === 'in_progress' || item.status === 'pending');
254
269
  report.research = ongoingResearch.map(item => ({
255
270
  id: item.id,
256
271
  topic: item.topic,
@@ -284,7 +299,10 @@ export default async function statusCmd(args) {
284
299
  console.log(` Remote INSTR.: ${remote.instructions_bytes} bytes, sha=${remote.instructions_sha256.slice(0, 12)}…`);
285
300
  }
286
301
  console.log(` In sync: ${inSync ? 'yes' : 'no'}`);
287
- console.log(` Last sync: ${report.last_sync || 'never'}`);
302
+ // Push sync only exists for remote deploys; a local server serves this brain
303
+ // directly, so "never" there read as a fault that was not one.
304
+ const pushSync = deployMode !== 'local' || report.last_sync || report.targets.length;
305
+ console.log(` Last sync: ${report.last_sync || (pushSync ? 'never' : 'not used (local deploy serves this brain directly)')}`);
288
306
  console.log(` Sync targets: ${report.targets.length ? report.targets.join(', ') : '(none)'}`);
289
307
 
290
308
  if (clientProjections.length) {
@@ -1,3 +1,4 @@
1
+ import { maintainPortableSkills } from './portable-skills-maintenance.mjs';
1
2
  /** The existing TR daemon maintains both published brain packages, not only TR. */
2
3
  import fs from 'node:fs';
3
4
  import path from 'node:path';
@@ -35,7 +36,7 @@ export function startAutomaticBrainUpdates({brainDir,intervalMs=60*60*1000,run=r
35
36
  return {tick,stop(){stopped=true;clearInterval(timer);}};
36
37
  }
37
38
 
38
- export async function runAutomaticBrainUpdates({brainDir,fleet=false,force=false,skipThrottle=false,intervalMs=60*60*1000,update=updateAllBrains,compile=maintainCliSurfaces,globalUpdate=updateGlobalBrainPackages,meshUpdate=updateMeshBrainPackages,sourceUpdate=updateSourceBrainPackages,pluginUpdate=updateInstalledPlugins,now=Date.now}={}) {
39
+ export async function runAutomaticBrainUpdates({brainDir,fleet=false,force=false,skipThrottle=false,intervalMs=60*60*1000,update=updateAllBrains,compile=maintainCliSurfaces,globalUpdate=updateGlobalBrainPackages,meshUpdate=updateMeshBrainPackages,sourceUpdate=updateSourceBrainPackages,pluginUpdate=updateInstalledPlugins,skillMaintenance=maintainPortableSkills,now=Date.now}={}) {
39
40
  if(!force && !isPackageAutoUpdateEnabled())return {skipped:true,reason:'disabled',results:[]};
40
41
  const file=path.join(brainDir,'memory-derived/automatic-brain-updates.json');
41
42
  let saved;try{saved=JSON.parse(fs.readFileSync(file,'utf8'));}catch{}
@@ -61,8 +62,13 @@ export async function runAutomaticBrainUpdates({brainDir,fleet=false,force=false
61
62
  const plugins=await pluginUpdate({roots:[path.resolve(brainDir,'../../..')]});
62
63
  const mesh=fleet && summary.versions?await meshUpdate({versions:summary.versions,version:summary.versions['total-recall-brain']}):{failed:0,results:[],scope:'local-only'};
63
64
  const changed=summary.updated>0 || plugins.updated>0 || global.results.some(row=>row.status==='updated') || sources.results.some(row=>row.status==='updated') || JSON.stringify(saved?.versions)!==JSON.stringify(summary.versions);
64
- const surfaces=await compile({brainDir,force:changed});
65
- report={...summary,global,sources,plugins,mesh,surfaces,failed:(summary.failed || 0)+global.failed+sources.failed+plugins.failed+mesh.failed+(surfaces.ok?0:surfaces.results.filter(row=>row.status==='failed').length),checkedAt:now()};
65
+ const skills=await skillMaintenance({brainDir});
66
+ const surfaces=await compile({brainDir,force:changed || skills.updated > 0});
67
+ // local_failed is this node's own maintenance. Peers judge a node by it, not by
68
+ // `failed`, which also counts the node's view of its peers: judging by that made
69
+ // every fleet pass mark every peer failed forever, each because the others had.
70
+ const local_failed=(summary.failed || 0)+global.failed+sources.failed+plugins.failed+(skills.failed || 0)+(surfaces.ok?0:surfaces.results.filter(row=>row.status==='failed').length);
71
+ report={...summary,global,sources,plugins,mesh,skills,surfaces,local_failed,failed:local_failed+mesh.failed,checkedAt:now()};
66
72
  }catch(error){report={failed:1,error:error.message,checkedAt:now(),results:[]};}
67
73
  const temporary=`${file}.${process.pid}.tmp`;fs.writeFileSync(temporary,JSON.stringify(report,null,2));fs.renameSync(temporary,file);
68
74
  return report;