total-recall-brain 3.40.6 → 3.41.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 (170) hide show
  1. package/bin/total-recall-memory.mjs +0 -0
  2. package/bin/total-recall.mjs +9 -40
  3. package/docs/reference/cli-reference.md +65 -1
  4. package/frontend/dist/assets/{index-AGUes-VG.css → index-BG9NkVUy.css} +1 -1
  5. package/frontend/dist/assets/{index-Bt5iVPZj.js → index-BtuiZqwP.js} +20 -7
  6. package/frontend/dist/index.html +2 -2
  7. package/frontend/src/components/plugins/PluginPanel.tsx +31 -5
  8. package/frontend/src/components/plugins/card/CardEngine.tsx +15 -23
  9. package/frontend/src/components/plugins/card/CardScenes.tsx +52 -23
  10. package/frontend/src/components/plugins/card/card.css +2 -5
  11. package/frontend/src/components/plugins/card/contract.ts +13 -19
  12. package/frontend/src/components/plugins/card/format.ts +8 -1
  13. package/frontend/src/components/plugins/card/widgets.tsx +4 -35
  14. package/frontend/src/components/plugins/plugin-frame.ts +30 -3
  15. package/frontend/src/pages/PluginsPage.tsx +3 -2
  16. package/package.json +1 -1
  17. package/scaffold/.agent/skills/plugin/SKILL.md +110 -0
  18. package/scaffold/.agent/skills/plugin/evals/README.md +53 -0
  19. package/scaffold/.agent/skills/plugin/evals/cases/01-fabricated-seed.mjs +42 -0
  20. package/scaffold/.agent/skills/plugin/evals/cases/02-reassuring-default.mjs +39 -0
  21. package/scaffold/.agent/skills/plugin/evals/cases/03-declared-template.mjs +32 -0
  22. package/scaffold/.agent/skills/plugin/evals/cases/04-card-collects-credential.mjs +46 -0
  23. package/scaffold/.agent/skills/plugin/evals/cases/05-host-leak-in-scaffold.mjs +55 -0
  24. package/scaffold/.agent/skills/plugin/evals/cases/06-correct-plugin.mjs +92 -0
  25. package/scaffold/.agent/skills/plugin/evals/run.mjs +118 -0
  26. package/scaffold/.agent/skills/plugin/prompts/01-function.md +29 -0
  27. package/scaffold/.agent/skills/plugin/prompts/02-surfaces.md +43 -0
  28. package/scaffold/.agent/skills/plugin/prompts/03-scaffold.md +31 -0
  29. package/scaffold/.agent/skills/plugin/prompts/04-cli.md +49 -0
  30. package/scaffold/.agent/skills/plugin/prompts/05-credentials.md +41 -0
  31. package/scaffold/.agent/skills/plugin/prompts/06-card.md +46 -0
  32. package/scaffold/.agent/skills/plugin/prompts/07-scaffold.md +52 -0
  33. package/scaffold/.agent/skills/plugin/prompts/08-verify.md +43 -0
  34. package/scaffold/.agent/skills/plugin/references/anti-patterns.md +123 -0
  35. package/scaffold/.agent/skills/plugin/references/card.md +90 -0
  36. package/scaffold/.agent/skills/plugin/references/scaffold.md +120 -0
  37. package/scaffold/.agent/skills/plugin/references/surfaces.md +206 -0
  38. package/scaffold/.agent/skills/plugin/scripts/record.mjs +360 -0
  39. package/scaffold/.agent/skills/plugin/subagents/card-designer.md +60 -0
  40. package/scaffold/.agent/skills/plugin/subagents/plugin-reviewer.md +55 -0
  41. package/scaffold/.agent/skills/plugin/subagents/scaffold-extractor.md +71 -0
  42. package/scaffold/.agent/skills/total-recall/plugins/auth/package-lock.json +71 -1441
  43. package/scaffold/.agent/skills/total-recall/plugins/auth/package.json +7 -5
  44. package/scaffold/.agent/skills/total-recall/plugins/auth/plugin.json +3 -44
  45. package/scaffold/.agent/skills/total-recall/plugins/auth/scripts/agent-login-pty.py +185 -0
  46. package/scaffold/.agent/skills/total-recall/plugins/auth/src/cli-secret.mjs +20 -1
  47. package/scaffold/.agent/skills/total-recall/plugins/auth/src/host-secrets-store.mjs +15 -0
  48. package/scaffold/.agent/skills/total-recall/plugins/auth/src/secrets-env-export.mjs +6 -4
  49. package/scaffold/.agent/skills/total-recall/plugins/auth/src/secrets-remote-deploy.mjs +1 -0
  50. package/scaffold/.agent/skills/total-recall/plugins/cli-agents/plugin.json +0 -24
  51. package/scaffold/.agent/skills/total-recall/plugins/code-quality/package-lock.json +4 -4
  52. package/scaffold/.agent/skills/total-recall/plugins/code-quality/plugin.json +0 -21
  53. package/scaffold/.agent/skills/total-recall/plugins/composable-cli/package-lock.json +4 -4
  54. package/scaffold/.agent/skills/total-recall/plugins/composable-cli/plugin.json +0 -32
  55. package/scaffold/.agent/skills/total-recall/plugins/embeddings/package-lock.json +4 -4
  56. package/scaffold/.agent/skills/total-recall/plugins/embeddings/plugin.json +11 -0
  57. package/scaffold/.agent/skills/total-recall/plugins/git-sentinel/README.md +5 -2
  58. package/scaffold/.agent/skills/total-recall/plugins/git-sentinel/package-lock.json +4 -4
  59. package/scaffold/.agent/skills/total-recall/plugins/git-sentinel/plugin.json +1 -28
  60. package/scaffold/.agent/skills/total-recall/plugins/git-sentinel/ui/DESIGN.md +95 -13
  61. package/scaffold/.agent/skills/total-recall/plugins/git-sentinel/ui/card.js +553 -340
  62. package/scaffold/.agent/skills/total-recall/plugins/mesh/plugin.json +0 -36
  63. package/scaffold/.agent/skills/total-recall/plugins/mesh/ui/card.js +383 -400
  64. package/scaffold/.agent/skills/total-recall/plugins/openwiki/package-lock.json +4 -4
  65. package/scaffold/.agent/skills/total-recall/plugins/openwiki/package.json +0 -1
  66. package/scaffold/.agent/skills/total-recall/plugins/openwiki/plugin.json +0 -28
  67. package/scaffold/.agent/skills/total-recall/plugins/security/package-lock.json +4 -4
  68. package/scaffold/.agent/skills/total-recall/plugins/security/plugin.json +0 -32
  69. package/scaffold/.agent/skills/total-recall/references/auth.md +3 -1
  70. package/scaffold/.agent/skills/total-recall/references/memory.md +4 -0
  71. package/scaffold/.agent/skills/total-recall/references/openwiki.md +2 -0
  72. package/scaffold/.agent/skills/total-recall/references/operations.md +12 -0
  73. package/src/cli/brain.mjs +4 -2
  74. package/src/cli/context-stats.mjs +104 -0
  75. package/src/cli/context.mjs +8 -0
  76. package/src/cli/daemon.mjs +10 -1
  77. package/src/cli/export.mjs +3 -2
  78. package/src/cli/ingest-okf.mjs +12 -15
  79. package/src/cli/init.mjs +45 -85
  80. package/src/cli/lint.mjs +3 -2
  81. package/src/cli/plugin/copies.mjs +68 -0
  82. package/src/cli/plugin/create.mjs +22 -24
  83. package/src/cli/plugin/index.mjs +49 -3
  84. package/src/cli/plugin/search.mjs +19 -2
  85. package/src/cli/rebuild.mjs +2 -0
  86. package/src/cli/remember.mjs +23 -0
  87. package/src/cli/status.mjs +10 -3
  88. package/src/cli/update.mjs +3 -2
  89. package/src/core/action-predictor.mjs +161 -0
  90. package/src/core/app-deploy/runtime-cli/node.mjs.tmpl +20 -3
  91. package/src/core/app-deploy/source.mjs +16 -9
  92. package/src/core/automatic-brain-updates.mjs +6 -3
  93. package/src/core/brain-registry.mjs +19 -2
  94. package/src/core/brain-state.mjs +23 -0
  95. package/src/core/cli-commands.mjs +1 -0
  96. package/src/core/cli-maintenance.mjs +1 -1
  97. package/src/core/cli-semantic-index.mjs +1 -1
  98. package/src/core/command-surface.mjs +3 -3
  99. package/src/core/composed-command-resolution.mjs +21 -0
  100. package/src/core/config.mjs +84 -0
  101. package/src/core/confined-memory.mjs +93 -0
  102. package/src/core/context-telemetry.mjs +214 -0
  103. package/src/core/daemon-control.mjs +14 -43
  104. package/src/core/fast-recall.mjs +1 -1
  105. package/src/core/memory-brain.mjs +2 -0
  106. package/src/core/plugin-activation.mjs +38 -6
  107. package/src/core/plugin-bundle.mjs +54 -0
  108. package/src/core/plugin-catalog.mjs +1 -1
  109. package/src/core/plugin-context.mjs +1 -1
  110. package/src/core/plugin-contracts/card.mjs +69 -37
  111. package/src/core/plugin-loader.mjs +5 -6
  112. package/src/core/plugin-runner.mjs +13 -6
  113. package/src/core/plugin-runtime.mjs +5 -4
  114. package/src/core/plugin-setup.mjs +5 -3
  115. package/src/core/plugin-state-store.mjs +12 -1
  116. package/src/core/plugin-state.mjs +46 -5
  117. package/src/core/plugin-store.mjs +78 -3
  118. package/src/core/plugin-tasks.mjs +3 -1
  119. package/src/core/plugin-updates.mjs +117 -0
  120. package/src/core/plugin-vendor.mjs +10 -4
  121. package/src/core/project-brain.mjs +14 -41
  122. package/src/core/project-id.mjs +4 -1
  123. package/src/core/required-plugins.mjs +151 -13
  124. package/src/core/skills-registry.mjs +27 -3
  125. package/src/core/ssss-kernel-bridge.mjs +3 -1
  126. package/src/core/startup-health.mjs +66 -16
  127. package/src/core/surface.mjs +51 -44
  128. package/src/core/testing/plugin-fixture.mjs +69 -0
  129. package/src/core/vault-files.mjs +13 -0
  130. package/src/core/vault.mjs +4 -30
  131. package/src/server/index.mjs +13 -1
  132. package/src/server/invoke-decision-child.mjs +49 -0
  133. package/src/server/invoke-execution.mjs +87 -0
  134. package/src/server/invoke-policy.mjs +64 -0
  135. package/src/server/invoke-read-child.mjs +26 -0
  136. package/src/server/invoke-targets.mjs +49 -0
  137. package/src/server/invoke-write-child.mjs +19 -0
  138. package/src/server/routes/invoke.mjs +314 -0
  139. package/src/server/routes/plugins.mjs +1 -1
  140. package/scaffold/.agent/skills/plugins/SKILL.md +0 -57
  141. package/scaffold/.agent/skills/plugins/references/card-design.md +0 -55
  142. package/scaffold/.agent/skills/plugins/references/distribution.md +0 -23
  143. package/scaffold/.agent/skills/plugins/references/manifest-contract.md +0 -90
  144. package/scaffold/.agent/skills/plugins/references/optimized/index.md +0 -9
  145. package/scaffold/.agent/skills/plugins/references/optimized/manifest.json +0 -35
  146. package/scaffold/.agent/skills/plugins/references/optimized/original.md +0 -58
  147. package/scaffold/.agent/skills/plugins/references/optimized/section-01.md +0 -5
  148. package/scaffold/.agent/skills/plugins/references/optimized/section-02.md +0 -25
  149. package/scaffold/.agent/skills/plugins/references/optimized/section-03.md +0 -12
  150. package/scaffold/.agent/skills/plugins/references/optimized/section-04.md +0 -7
  151. package/scaffold/.agent/skills/plugins/references/optimized/section-05.md +0 -5
  152. package/scaffold/.agent/skills/total-recall/openwiki/README.md +0 -10
  153. package/scaffold/.agent/skills/total-recall/openwiki/architecture.md +0 -5
  154. package/scaffold/.agent/skills/total-recall/openwiki/ide.md +0 -10
  155. package/scaffold/.agent/skills/total-recall/openwiki/memory.md +0 -11
  156. package/scaffold/.agent/skills/total-recall/openwiki/secrets.md +0 -7
  157. package/scaffold/.agent/skills/total-recall/openwiki/skills.md +0 -7
  158. package/scaffold/openwiki/README.md +0 -10
  159. package/scaffold/openwiki/architecture.md +0 -5
  160. package/scaffold/openwiki/ide.md +0 -10
  161. package/scaffold/openwiki/memory.md +0 -11
  162. package/scaffold/openwiki/secrets.md +0 -7
  163. package/scaffold/openwiki/skills.md +0 -7
  164. package/src/core/okf-adapter.mjs +0 -559
  165. package/templates/openwiki/README.md +0 -10
  166. package/templates/openwiki/architecture.md +0 -5
  167. package/templates/openwiki/ide.md +0 -10
  168. package/templates/openwiki/memory.md +0 -11
  169. package/templates/openwiki/secrets.md +0 -7
  170. package/templates/openwiki/skills.md +0 -7
@@ -2,23 +2,64 @@
2
2
  import fs from 'node:fs';
3
3
  import path from 'node:path';
4
4
  import matter from './frontmatter.mjs';
5
- import { globalPluginsDir, vaultForPluginsDir } from './plugin-loader.mjs';
5
+ import { globalPluginsDir, projectPluginsDir, vaultForPluginsDir } from './plugin-loader.mjs';
6
6
 
7
- export function pluginActivation(plugin) {
7
+ /**
8
+ * Enablement has two layers. The plugin's own brain holds the shared setting —
9
+ * the global brain, since a shared plugin lives in one folder per machine. A
10
+ * repository adds its own `plugin_record` in its project brain whose
11
+ * `runtime_enabled` overrides that setting for that repository alone. With no
12
+ * override the shared setting applies; an override carrying only a
13
+ * `runtime_disabled: false` tombstone means "follow the shared setting" and
14
+ * keeps no second copy of the plugin on disk.
15
+ */
16
+ export function projectActivationOverride(id, projectRoot) {
17
+ const pluginsDir = projectPluginsDir(projectRoot);
18
+ const file = path.join(vaultForPluginsDir(pluginsDir), 'system', 'plugins', `${id}.md`);
19
+ return { pluginsDir, file, record: readActivationRecord(file, id) };
20
+ }
21
+
22
+ export function pluginActivation(plugin, { projectRoot } = {}) {
8
23
  const pluginsDir = plugin.scope === 'scaffold' ? globalPluginsDir() : plugin.pluginsDir;
9
24
  if (!/^[a-z][a-z0-9-]{1,63}$/.test(plugin.id)) throw new Error('Invalid plugin identity');
10
25
  const file = pluginsDir ? path.join(vaultForPluginsDir(pluginsDir), 'system', 'plugins', `${plugin.id}.md`) : null;
11
26
  const record = readActivationRecord(file, plugin.id);
12
- const enabled = plugin.valid === true && record?.runtime_enabled !== false;
27
+ // Only a plugin owned by the shared (global) folder has an override to read:
28
+ // a project's own copy already is that repository's setting.
29
+ const override = plugin.scope === 'global' && projectRoot ? projectActivationOverride(plugin.id, projectRoot).record : null;
30
+ const applied = override?.runtime_enabled;
31
+ const source = typeof applied === 'boolean' ? 'project' : 'shared';
32
+ const shared = record?.runtime_enabled !== false;
33
+ const enabled = plugin.valid === true && (typeof applied === 'boolean' ? applied : shared);
13
34
  const runtime = plugin.manifest?.runtime || {};
14
35
  return {
15
- state: enabled ? 'active' : 'inactive', enabled,
16
- hasRuntime: Boolean(plugin.manifest?.tasks?.length || Object.keys(runtime).length),
36
+ state: enabled ? 'active' : 'inactive', enabled, source,
37
+ overridden: source === 'project' && applied !== shared,
17
38
  evidence: 'Host activation policy, not external process or provider health',
39
+ hasRuntime: Boolean(plugin.manifest?.tasks?.length || Object.keys(runtime).length),
18
40
  externalRuntime: Boolean(runtime.jobs?.length || runtime.daemons?.length || runtime.startup?.length),
19
41
  };
20
42
  }
21
43
 
44
+ /**
45
+ * Whether execution is permitted. `pluginActivation` reports a plugin's policy
46
+ * state, which includes whether the plugin is valid at all; a caller that
47
+ * already holds a usable plugin object asks this instead, because the only thing
48
+ * that switches execution off is an explicit negative — the shared setting or
49
+ * the repository's own override.
50
+ */
51
+ export function pluginExecutionEnabled(plugin, { projectRoot } = {}) {
52
+ if (!plugin) return false;
53
+ const pluginsDir = plugin.scope === 'scaffold' ? globalPluginsDir() : plugin.pluginsDir;
54
+ if (!pluginsDir) return true;
55
+ if (!/^[a-z][a-z0-9-]{1,63}$/.test(plugin.id)) throw new Error('Invalid plugin identity');
56
+ const file = path.join(vaultForPluginsDir(pluginsDir), 'system', 'plugins', `${plugin.id}.md`);
57
+ const record = readActivationRecord(file, plugin.id);
58
+ const override = plugin.scope === 'global' && projectRoot ? projectActivationOverride(plugin.id, projectRoot).record : null;
59
+ const applied = override?.runtime_enabled;
60
+ return typeof applied === 'boolean' ? applied : record?.runtime_enabled !== false;
61
+ }
62
+
22
63
  /** Pure bootstrap read, avoiding configuration/kernel cycles; malformed existing policy fails closed. */
23
64
  export function readActivationRecord(file, id) {
24
65
  if (!file) return null;
@@ -31,7 +31,7 @@ import {
31
31
  } from './plugin-bundle.mjs';
32
32
  import { writeVfsDocument, patchVfsDocument, deleteVfsDocument, appendVfsEvent } from './ssss-operation-service.mjs';
33
33
  import { findVfsDocumentByPath } from './vfs-documents.mjs';
34
- import { readActivationRecord } from './plugin-state.mjs';
34
+ import { readActivationRecord, projectActivationOverride } from './plugin-state.mjs';
35
35
 
36
36
  const execFileAsync = promisify(execFile);
37
37
  const ID_PATTERN = /^[a-z][a-z0-9-]{1,63}$/;
@@ -62,6 +62,18 @@ export function readPluginRecord(plugin) {
62
62
  return record;
63
63
  }
64
64
 
65
+ /**
66
+ * A repository's own enablement for a shared plugin, read from its project
67
+ * brain. This is an overlay, never an install: it is absent for a repository
68
+ * that has never switched the plugin either way.
69
+ */
70
+ export function readProjectActivationRecord(id, projectRoot) {
71
+ if (!projectRoot) return null;
72
+ const { file } = projectActivationOverride(id, projectRoot);
73
+ if (!readActivationRecord(file, id)) return null;
74
+ return findVfsDocumentByPath(recordPath(id), vaultForPluginsDir(projectPluginsDir(projectRoot)));
75
+ }
76
+
65
77
  export async function emit(vaultRoot, id, kind, payload) {
66
78
  await appendVfsEvent(
67
79
  `plugins/${id}/${kind}-${Date.now()}-${crypto.randomBytes(3).toString('hex')}`,
@@ -164,6 +176,65 @@ export async function patchPluginRecord(plugin, patches) {
164
176
  });
165
177
  }
166
178
 
179
+ /**
180
+ * Write one repository's enablement for a shared plugin as a `plugin_record`
181
+ * overlay in that repository's brain. Passing `null` clears the repository's own
182
+ * setting so it follows the shared one again. The overlay never installs the
183
+ * plugin: a repository without it uses the shared folder's copy.
184
+ */
185
+ export async function patchProjectActivationRecord(id, projectRoot, runtimeEnabled, { manifest = null } = {}) {
186
+ if (!ID_PATTERN.test(id)) throw new Error('Invalid plugin identity');
187
+ const pluginsDir = projectPluginsDir(projectRoot);
188
+ const vaultRoot = vaultForPluginsDir(pluginsDir);
189
+ const now = new Date().toISOString();
190
+ // Clear: the overlay is rewritten without `runtime_enabled`, which is what
191
+ // "follow the shared setting" means. A patch cannot remove a key, so this
192
+ // replaces the document rather than merging into it.
193
+ if (runtimeEnabled === null) {
194
+ const existing = readProjectActivationRecord(id, projectRoot);
195
+ if (!existing) return null;
196
+ const { runtime_enabled: _dropped, ...frontmatter } = { ...(existing.frontmatter || {}) };
197
+ await writeVfsDocument(recordPath(id), {
198
+ ...frontmatter,
199
+ type: 'plugin_record',
200
+ runtime_disabled: false,
201
+ timestamp: now
202
+ }, existing.body ?? buildPluginDocumentBody(manifest || {}, id), {
203
+ actorRole: 'system',
204
+ intent: `Clear repository activation override for plugin ${id}`,
205
+ vaultRoot
206
+ });
207
+ return readProjectActivationRecord(id, projectRoot);
208
+ }
209
+ if (typeof runtimeEnabled !== 'boolean') throw new Error('Plugin activation requires a boolean or null');
210
+ const previous = readProjectActivationRecord(id, projectRoot);
211
+ if (previous) {
212
+ return patchVfsDocument(recordPath(id), { runtime_enabled: runtimeEnabled, timestamp: now }, {
213
+ actorRole: 'system',
214
+ intent: `Set repository activation for plugin ${id}`,
215
+ vaultRoot
216
+ });
217
+ }
218
+ const m = manifest || {};
219
+ const frontmatter = {
220
+ type: 'plugin_record',
221
+ title: m.name || id,
222
+ description: `Repository activation for shared plugin ${id}.`,
223
+ timestamp: now,
224
+ plugin_id: id,
225
+ category: 'plugins',
226
+ tags: ['plugin', 'activation-override'],
227
+ runtime_enabled: runtimeEnabled,
228
+ source: { kind: 'shared', ref: globalPluginsDir() }
229
+ };
230
+ await writeVfsDocument(recordPath(id), frontmatter, buildPluginDocumentBody(m, id), {
231
+ actorRole: 'system',
232
+ intent: `Record repository activation for plugin ${id}`,
233
+ vaultRoot
234
+ });
235
+ return frontmatter;
236
+ }
237
+
167
238
  /** Facts about one installed plugin, merged with its record. */
168
239
  export function describePlugin(plugin) {
169
240
  const record = readPluginRecord(plugin);
@@ -394,9 +465,12 @@ export async function installPlugin(source, options = {}) {
394
465
  sourceRecord = { kind: 'bundled', ref: manifest.id };
395
466
  } else if (isGitSource(source)) {
396
467
  const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'tr-plugin-'));
468
+ let commit;
397
469
  try {
398
470
  try {
399
471
  await execFileAsync('git', ['clone', '--depth', '1', '--', source, tmp], { timeout: 120_000 });
472
+ // The installed commit is what automatic updates compare against the remote head.
473
+ commit = (await execFileAsync('git', ['-C', tmp, 'rev-parse', 'HEAD'], { timeout: 30_000 })).stdout.trim();
400
474
  } catch (err) {
401
475
  throw new Error(`Failed to clone ${source}: ${(err.stderr || err.message || '').toString().trim()}`);
402
476
  }
@@ -406,7 +480,7 @@ export async function installPlugin(source, options = {}) {
406
480
  } finally {
407
481
  fs.rmSync(tmp, { recursive: true, force: true });
408
482
  }
409
- sourceRecord = { kind: 'git', ref: source };
483
+ sourceRecord = { kind: 'git', ref: source, commit };
410
484
  } else {
411
485
  const abs = path.isAbsolute(source) ? source : path.resolve(projectRoot, source);
412
486
  if (!fs.existsSync(abs)) {
@@ -450,7 +524,8 @@ export async function installPlugin(source, options = {}) {
450
524
  throw new Error(`Plugin files were written but its record could not be: ${err.message}`);
451
525
  }
452
526
 
453
- const compilation = await recompileAfterPluginMutation(projectRoot, isGlobal);
527
+ // A batch of installs (automatic updates) compiles once after the batch instead.
528
+ const compilation = options.recompile === false ? { skipped: true } : await recompileAfterPluginMutation(projectRoot, isGlobal);
454
529
  return {
455
530
  plugin: { id: manifest.id, name: manifest.name, version: manifest.version, dir: dest, scope },
456
531
  source: sourceRecord,
@@ -14,6 +14,7 @@
14
14
  */
15
15
  import { discoverPlugins } from './plugin-loader.mjs';
16
16
  import { readPluginRecord, patchPluginRecord, ensurePluginRecord } from './plugin-store.mjs';
17
+ import { pluginExecutionEnabled } from './plugin-state.mjs';
17
18
  import { runPluginCommand } from './plugin-runner.mjs';
18
19
  import { appendVfsEvent } from './ssss-operation-service.mjs';
19
20
  import { vaultForPluginsDir } from './plugin-loader.mjs';
@@ -75,7 +76,8 @@ export async function runDuePluginTasks(options = {}) {
75
76
 
76
77
  let record = readPluginRecord(plugin);
77
78
  if (!record) record = await ensurePluginRecord(plugin);
78
- if (record?.runtime_enabled === false) continue;
79
+ // Scheduled work follows the repository's own setting for a shared plugin.
80
+ if (!pluginExecutionEnabled(plugin, { projectRoot })) continue;
79
81
  const runs = { ...(record?.task_runs || {}) };
80
82
  let changed = false;
81
83
 
@@ -0,0 +1,117 @@
1
+ /** Automatic maintenance keeps every installed plugin current with the source it was installed from. */
2
+ import fs from 'node:fs';
3
+ import path from 'node:path';
4
+ import { execFile } from 'node:child_process';
5
+ import { promisify } from 'node:util';
6
+ import { discoverPlugins, listBundledPlugins } from './plugin-loader.mjs';
7
+ import { describePlugin } from './plugin-store.mjs';
8
+ import { hashPluginDir } from './plugin-bundle.mjs';
9
+ import { DEFAULT_PLUGIN_IDS } from './default-plugins.mjs';
10
+
11
+ const execFileAsync = promisify(execFile);
12
+
13
+ /** The commit a git source's default branch points at now. */
14
+ export async function remoteGitHead(ref) {
15
+ const { stdout } = await execFileAsync('git', ['ls-remote', '--', ref, 'HEAD'], { timeout: 30_000 });
16
+ const commit = stdout.split(/\s/)[0];
17
+ if (!/^[0-9a-f]{40}$/.test(commit)) throw new Error(`No HEAD commit advertised by ${ref}`);
18
+ return commit;
19
+ }
20
+
21
+ async function peerSha({ id, ref }) {
22
+ const { listPeerPlugins } = await import('./plugin-peers.mjs');
23
+ const host = ref.slice('peer:'.length, ref.lastIndexOf('/'));
24
+ const { peers } = await listPeerPlugins();
25
+ const peer = peers.find(p => p.hostname === host || p.ip === host);
26
+ if (!peer) throw new Error(`Mesh peer '${host}' is not reachable`);
27
+ const offered = (peer.plugins || []).find(p => p.id === id);
28
+ if (!offered?.sha256) throw new Error(`Mesh peer '${host}' no longer shares '${id}'`);
29
+ return offered.sha256;
30
+ }
31
+
32
+ /**
33
+ * Decide whether one installed plugin is behind its source.
34
+ * Returns { status, install? } where install is the source to reinstall from.
35
+ */
36
+ export async function checkPlugin(plugin, { gitHead = remoteGitHead, peerHash = peerSha, bundled = listBundledPlugins } = {}) {
37
+ const { id, source } = plugin;
38
+ // The filesystem decides, not the record: a linked plugin is its owner's
39
+ // checkout (often recorded long ago as a copy) and is as current as that checkout.
40
+ if (plugin.linked || source?.kind === 'link') return { status: 'linked', ref: source?.ref || plugin.dir };
41
+ // Default plugins are refreshed in every brain by ensureRequiredPlugins with the package.
42
+ if (DEFAULT_PLUGIN_IDS.includes(id)) return { status: 'package' };
43
+ // A copy that records itself (or nothing) as its source has no upstream to follow.
44
+ if (!source?.ref || (source.kind === 'local' && path.resolve(source.ref) === path.resolve(plugin.dir))) {
45
+ return { status: 'unmanaged', error: 'No install source recorded; reinstall it from its git repository or a mesh peer with --replace to keep it updated' };
46
+ }
47
+ switch (source.kind) {
48
+ case 'public':
49
+ // A public share link pins one content hash by design.
50
+ return { status: 'pinned', ref: source.ref };
51
+ case 'bundled': {
52
+ const shipped = bundled().find(p => p.id === id);
53
+ if (!shipped) return { status: 'failed', error: `Bundled plugin '${id}' is no longer shipped in this package` };
54
+ return hashPluginDir(shipped.dir).sha256 === plugin.installed_sha256 ? { status: 'current' } : { status: 'behind', install: id };
55
+ }
56
+ case 'git': {
57
+ const head = await gitHead(source.ref);
58
+ return head === source.commit ? { status: 'current', commit: head } : { status: 'behind', install: source.ref, from: source.commit || null, to: head };
59
+ }
60
+ case 'peer': {
61
+ const sha = await peerHash({ id, ref: source.ref });
62
+ return sha === plugin.installed_sha256 ? { status: 'current' } : { status: 'behind', install: source.ref, from: plugin.installed_sha256, to: sha };
63
+ }
64
+ case 'local': {
65
+ if (!source.ref || !fs.existsSync(source.ref)) {
66
+ return { status: 'failed', error: `Installed from a directory that no longer exists (${source.ref}); reinstall it from its git repository or a mesh peer with --replace` };
67
+ }
68
+ return hashPluginDir(source.ref).sha256 === plugin.installed_sha256 ? { status: 'current' } : { status: 'behind', install: source.ref };
69
+ }
70
+ default:
71
+ return { status: 'failed', error: `Plugin '${id}' has no recorded install source` };
72
+ }
73
+ }
74
+
75
+ /**
76
+ * Check every installed plugin under the given roots and reinstall the ones that
77
+ * are behind. Surfaces are compiled once by the caller, not once per plugin.
78
+ */
79
+ export async function updateInstalledPlugins({ roots = [process.cwd()], dryRun = false, check = checkPlugin, install, discover = discoverPlugins, describe = describePlugin } = {}) {
80
+ if (!install) ({ installPlugin: install } = await import('./plugin-distribution.mjs'));
81
+ const seen = new Set();
82
+ const results = [];
83
+ for (const root of roots) {
84
+ let plugins;
85
+ try { plugins = discover(root); }
86
+ catch (error) { results.push({ root, status: 'failed', error: error.message }); continue; }
87
+ for (const found of plugins) {
88
+ if (found.scope === 'scaffold' || !found.dir || seen.has(found.dir)) continue;
89
+ seen.add(found.dir);
90
+ const row = { id: found.id, scope: found.scope, dir: found.dir };
91
+ results.push(row);
92
+ try {
93
+ const plugin = describe(found);
94
+ row.version = plugin.version;
95
+ row.source = plugin.source?.kind || null;
96
+ const verdict = await check(plugin);
97
+ Object.assign(row, verdict);
98
+ delete row.install;
99
+ if (verdict.status !== 'behind' || dryRun) continue;
100
+ const result = await install(verdict.install, { projectRoot: root, global: found.scope === 'global', replace: true, recompile: false });
101
+ row.status = 'updated';
102
+ row.previous_version = plugin.version;
103
+ row.version = result.plugin.version;
104
+ row.previous_directory = result.previousDirectory;
105
+ } catch (error) {
106
+ row.status = 'failed';
107
+ row.error = error.message;
108
+ }
109
+ }
110
+ }
111
+ return {
112
+ updated: results.filter(row => row.status === 'updated').length,
113
+ failed: results.filter(row => row.status === 'failed').length,
114
+ unmanaged: results.filter(row => row.status === 'unmanaged').length,
115
+ results
116
+ };
117
+ }
@@ -12,7 +12,7 @@ import os from 'node:os';
12
12
  import path from 'node:path';
13
13
  import { execFile } from 'node:child_process';
14
14
  import { promisify } from 'node:util';
15
- import { listPluginFiles, hashPluginDir } from './plugin-bundle.mjs';
15
+ import { pluginBundleFiles, hashPluginDir } from './plugin-bundle.mjs';
16
16
  import { listBundledPlugins } from './plugin-loader.mjs';
17
17
  import { isGitSource } from './plugin-store.mjs';
18
18
 
@@ -190,15 +190,21 @@ export async function vendorPlugin(source, dest, options = {}) {
190
190
  throw new Error(`Brick '${manifest.id}' is already in this project.`);
191
191
  }
192
192
 
193
- const files = listPluginFiles(srcDir);
194
- const sha256 = hashPluginDir(srcDir).sha256;
193
+ // A plugin that declares a capability is vendored as a brick: capability
194
+ // code only, no UI and no host manifest. Anything else is copied whole.
195
+ const audience = manifest.capability ? 'brick' : 'host';
196
+ const files = pluginBundleFiles(srcDir, { audience });
195
197
  fs.mkdirSync(target, { recursive: true });
196
198
  for (const f of files) {
197
199
  const out = path.join(target, f.path);
198
200
  fs.mkdirSync(path.dirname(out), { recursive: true });
199
- fs.copyFileSync(f.abs, out);
201
+ if (f.data) fs.writeFileSync(out, f.data);
202
+ else fs.copyFileSync(f.abs, out);
200
203
  fs.chmodSync(out, f.mode);
201
204
  }
205
+ // The recorded hash describes the vendored files, so drift detection stays
206
+ // correct when a brick is a reduced copy of its source plugin.
207
+ const sha256 = hashPluginDir(target).sha256;
202
208
  const vendoredAt = new Date().toISOString();
203
209
  const provenance = {
204
210
  vendored_from: manifest.id,
@@ -9,7 +9,6 @@
9
9
  * memory-derived/
10
10
  * sessions/
11
11
  * scheduler/queue/
12
- * openwiki/
13
12
  * skills-registry/
14
13
  * config/brain.json
15
14
  * <repo>/.agent/skills/<user-skills>/ ← sibling agent skills
@@ -46,16 +45,16 @@ function ensureDir(dir) {
46
45
  return false;
47
46
  }
48
47
 
49
- function copyDirMerge(src, dest) {
48
+ function copyDirMerge(src, dest, { skip = new Set() } = {}) {
50
49
  if (!fs.existsSync(src)) return 0;
51
50
  let n = 0;
52
51
  ensureDir(dest);
53
52
  for (const entry of fs.readdirSync(src, { withFileTypes: true })) {
54
- if (entry.name === '.git' || entry.name === 'node_modules') continue;
53
+ if (entry.name === '.git' || entry.name === 'node_modules' || skip.has(entry.name)) continue;
55
54
  const from = path.join(src, entry.name);
56
55
  const to = path.join(dest, entry.name);
57
56
  if (entry.isDirectory()) {
58
- n += copyDirMerge(from, to);
57
+ n += copyDirMerge(from, to, { skip });
59
58
  } else if (!fs.existsSync(to)) {
60
59
  fs.copyFileSync(from, to);
61
60
  n++;
@@ -64,45 +63,22 @@ function copyDirMerge(src, dest) {
64
63
  return n;
65
64
  }
66
65
 
67
- /**
68
- * Seed openwiki pages (templates if packaged).
69
- */
70
- export function ensureOpenWiki(brainDir) {
71
- const wikiDir = path.join(brainDir, 'openwiki');
72
- const created = ensureDir(wikiDir);
73
- const templateRoot = path.join(ROOT, 'templates', 'openwiki');
74
- let files = 0;
75
- if (fs.existsSync(templateRoot)) {
76
- files = copyDirMerge(templateRoot, wikiDir);
77
- } else {
78
- const pages = {
79
- 'README.md': `# OpenWiki\n\nProject knowledge docs for this Total Recall brain.\n`,
80
- 'memory.md': `# Memory\n\nUse \`npx total-recall remember\` / \`recall\` with --project.\n`,
81
- 'skills.md': `# Skills\n\nUser skills: \`.agent/skills/<name>/SKILL.md\`.\n`,
82
- 'ide.md': `# IDE\n\n\`npx total-recall connect <ide>\`\n`,
83
- 'secrets.md': `# Secrets\n\nNever put secrets in openwiki or vault markdown.\n`,
84
- };
85
- for (const [name, body] of Object.entries(pages)) {
86
- const dest = path.join(wikiDir, name);
87
- if (!fs.existsSync(dest)) {
88
- fs.writeFileSync(dest, body);
89
- files++;
90
- }
91
- }
92
- }
93
- return { wikiDir, created, files_added: files };
94
- }
95
-
96
66
  /**
97
67
  * Ensure core total-recall skill package files exist (SKILL.md minimum).
68
+ *
69
+ * The required plugins live in one shared folder per machine. `sharedBrainDir`
70
+ * names the global brain that owns them; without it the ambient global brain is
71
+ * used, which is what an interactive CLI run means.
98
72
  */
99
- export function ensureCoreSkillPackage(brainDir) {
100
- ensureRequiredPlugins(brainDir);
73
+ export function ensureCoreSkillPackage(brainDir, { sharedBrainDir } = {}) {
74
+ ensureRequiredPlugins(brainDir, sharedBrainDir ? { sharedBrainDir } : {});
101
75
  const skillMd = path.join(brainDir, 'SKILL.md');
102
76
  if (fs.existsSync(skillMd)) return { seeded: false };
103
77
  const scaffold = path.join(ROOT, 'scaffold', '.agent', 'skills', 'total-recall');
104
78
  if (fs.existsSync(scaffold)) {
105
- copyDirMerge(scaffold, brainDir);
79
+ // `plugins/` is deliberately skipped: the defaults live in one shared folder
80
+ // per machine, and copying them here is what put a copy in every repository.
81
+ copyDirMerge(scaffold, brainDir, { skip: new Set(['plugins']) });
106
82
  return { seeded: true, from: 'scaffold' };
107
83
  }
108
84
  // Scaffold directory missing — warn and fall back to a minimal SKILL.md
@@ -111,7 +87,7 @@ export function ensureCoreSkillPackage(brainDir) {
111
87
  skillMd,
112
88
  `---
113
89
  name: total-recall
114
- description: "Use this skill to operate Total Recall — portable memory, instructions, openwiki, skill deploy, and secrets."
90
+ description: "Use this skill to operate Total Recall — portable memory, instructions and plugin discovery."
115
91
  ---
116
92
 
117
93
  # Total Recall — Project Brain
@@ -169,8 +145,7 @@ export function ensureFullProjectBrain(repoRoot, opts = {}) {
169
145
  if (ensureDir(d)) dirsCreated++;
170
146
  }
171
147
 
172
- const core = ensureCoreSkillPackage(brainDir);
173
- const wiki = ensureOpenWiki(brainDir);
148
+ const core = ensureCoreSkillPackage(brainDir, { sharedBrainDir: opts.globalBrainDir });
174
149
  const identity = writeBrainIdentity(brainDir, { name, tags });
175
150
 
176
151
  // Empty skills registry if missing
@@ -207,7 +182,6 @@ export function ensureFullProjectBrain(repoRoot, opts = {}) {
207
182
  skillsDir,
208
183
  full_brain: true,
209
184
  identity,
210
- openwiki: wiki,
211
185
  core,
212
186
  dirs_created: dirsCreated,
213
187
  created_top: created,
@@ -258,7 +232,6 @@ export function inspectProjectBrain(repoRoot) {
258
232
  brainDir: fs.existsSync(brainDir),
259
233
  skillMd: fs.existsSync(path.join(brainDir, 'SKILL.md')),
260
234
  memoryVault: fs.existsSync(path.join(brainDir, 'memory-vault')),
261
- openwiki: fs.existsSync(path.join(brainDir, 'openwiki')),
262
235
  sessions: fs.existsSync(path.join(brainDir, 'sessions')),
263
236
  scheduler: fs.existsSync(path.join(brainDir, 'scheduler')),
264
237
  skillsRegistry: fs.existsSync(path.join(brainDir, 'skills-registry')),
@@ -37,7 +37,10 @@ export function readProjectId(brainDir) {
37
37
  */
38
38
  export function ensureProjectId(brainDir) {
39
39
  const file = brainJsonPath(brainDir);
40
- const cfg = readJson(file, {});
40
+ let cfg;
41
+ try { cfg = JSON.parse(fs.readFileSync(file, 'utf8')); }
42
+ catch (err) { if (err.code === 'ENOENT') cfg = {}; else throw new Error('Existing project identity is unreadable or malformed'); }
43
+ if (!cfg || typeof cfg !== 'object' || Array.isArray(cfg)) throw new Error('Existing project identity must be an object');
41
44
  if (cfg.project_id) return { project_id: cfg.project_id, created: false };
42
45
  cfg.project_id = randomUUID();
43
46
  writeJsonAtomic(file, cfg);