total-recall-brain 3.40.7 → 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.
- package/bin/total-recall-memory.mjs +0 -0
- package/bin/total-recall.mjs +9 -40
- package/docs/reference/cli-reference.md +65 -1
- package/frontend/dist/assets/{index-AGUes-VG.css → index-BG9NkVUy.css} +1 -1
- package/frontend/dist/assets/{index-Bt5iVPZj.js → index-BtuiZqwP.js} +20 -7
- package/frontend/dist/index.html +2 -2
- package/frontend/src/components/plugins/PluginPanel.tsx +31 -5
- package/frontend/src/components/plugins/card/CardEngine.tsx +15 -23
- package/frontend/src/components/plugins/card/CardScenes.tsx +52 -23
- package/frontend/src/components/plugins/card/card.css +2 -5
- package/frontend/src/components/plugins/card/contract.ts +13 -19
- package/frontend/src/components/plugins/card/format.ts +8 -1
- package/frontend/src/components/plugins/card/widgets.tsx +4 -35
- package/frontend/src/components/plugins/plugin-frame.ts +30 -3
- package/frontend/src/pages/PluginsPage.tsx +3 -2
- package/package.json +1 -1
- package/scaffold/.agent/skills/plugin/SKILL.md +110 -0
- package/scaffold/.agent/skills/plugin/evals/README.md +53 -0
- package/scaffold/.agent/skills/plugin/evals/cases/01-fabricated-seed.mjs +42 -0
- package/scaffold/.agent/skills/plugin/evals/cases/02-reassuring-default.mjs +39 -0
- package/scaffold/.agent/skills/plugin/evals/cases/03-declared-template.mjs +32 -0
- package/scaffold/.agent/skills/plugin/evals/cases/04-card-collects-credential.mjs +46 -0
- package/scaffold/.agent/skills/plugin/evals/cases/05-host-leak-in-scaffold.mjs +55 -0
- package/scaffold/.agent/skills/plugin/evals/cases/06-correct-plugin.mjs +92 -0
- package/scaffold/.agent/skills/plugin/evals/run.mjs +118 -0
- package/scaffold/.agent/skills/plugin/prompts/01-function.md +29 -0
- package/scaffold/.agent/skills/plugin/prompts/02-surfaces.md +43 -0
- package/scaffold/.agent/skills/plugin/prompts/03-scaffold.md +31 -0
- package/scaffold/.agent/skills/plugin/prompts/04-cli.md +49 -0
- package/scaffold/.agent/skills/plugin/prompts/05-credentials.md +41 -0
- package/scaffold/.agent/skills/plugin/prompts/06-card.md +46 -0
- package/scaffold/.agent/skills/plugin/prompts/07-scaffold.md +52 -0
- package/scaffold/.agent/skills/plugin/prompts/08-verify.md +43 -0
- package/scaffold/.agent/skills/plugin/references/anti-patterns.md +123 -0
- package/scaffold/.agent/skills/plugin/references/card.md +90 -0
- package/scaffold/.agent/skills/plugin/references/scaffold.md +120 -0
- package/scaffold/.agent/skills/plugin/references/surfaces.md +206 -0
- package/scaffold/.agent/skills/plugin/scripts/record.mjs +360 -0
- package/scaffold/.agent/skills/plugin/subagents/card-designer.md +60 -0
- package/scaffold/.agent/skills/plugin/subagents/plugin-reviewer.md +55 -0
- package/scaffold/.agent/skills/plugin/subagents/scaffold-extractor.md +71 -0
- package/scaffold/.agent/skills/total-recall/plugins/auth/package-lock.json +71 -1441
- package/scaffold/.agent/skills/total-recall/plugins/auth/package.json +7 -5
- package/scaffold/.agent/skills/total-recall/plugins/auth/plugin.json +3 -44
- package/scaffold/.agent/skills/total-recall/plugins/auth/scripts/agent-login-pty.py +185 -0
- package/scaffold/.agent/skills/total-recall/plugins/auth/src/cli-secret.mjs +20 -1
- package/scaffold/.agent/skills/total-recall/plugins/auth/src/host-secrets-store.mjs +15 -0
- package/scaffold/.agent/skills/total-recall/plugins/auth/src/secrets-env-export.mjs +6 -4
- package/scaffold/.agent/skills/total-recall/plugins/auth/src/secrets-remote-deploy.mjs +1 -0
- package/scaffold/.agent/skills/total-recall/plugins/cli-agents/plugin.json +0 -24
- package/scaffold/.agent/skills/total-recall/plugins/code-quality/package-lock.json +4 -4
- package/scaffold/.agent/skills/total-recall/plugins/code-quality/plugin.json +0 -21
- package/scaffold/.agent/skills/total-recall/plugins/composable-cli/package-lock.json +4 -4
- package/scaffold/.agent/skills/total-recall/plugins/composable-cli/plugin.json +0 -32
- package/scaffold/.agent/skills/total-recall/plugins/embeddings/package-lock.json +4 -4
- package/scaffold/.agent/skills/total-recall/plugins/embeddings/plugin.json +11 -0
- package/scaffold/.agent/skills/total-recall/plugins/git-sentinel/README.md +5 -2
- package/scaffold/.agent/skills/total-recall/plugins/git-sentinel/package-lock.json +4 -4
- package/scaffold/.agent/skills/total-recall/plugins/git-sentinel/plugin.json +1 -28
- package/scaffold/.agent/skills/total-recall/plugins/git-sentinel/ui/DESIGN.md +95 -13
- package/scaffold/.agent/skills/total-recall/plugins/git-sentinel/ui/card.js +553 -340
- package/scaffold/.agent/skills/total-recall/plugins/mesh/plugin.json +0 -36
- package/scaffold/.agent/skills/total-recall/plugins/mesh/ui/card.js +383 -400
- package/scaffold/.agent/skills/total-recall/plugins/openwiki/package-lock.json +4 -4
- package/scaffold/.agent/skills/total-recall/plugins/openwiki/package.json +0 -1
- package/scaffold/.agent/skills/total-recall/plugins/openwiki/plugin.json +0 -28
- package/scaffold/.agent/skills/total-recall/plugins/security/package-lock.json +4 -4
- package/scaffold/.agent/skills/total-recall/plugins/security/plugin.json +0 -32
- package/scaffold/.agent/skills/total-recall/references/auth.md +3 -1
- package/scaffold/.agent/skills/total-recall/references/memory.md +4 -0
- package/scaffold/.agent/skills/total-recall/references/openwiki.md +2 -0
- package/scaffold/.agent/skills/total-recall/references/operations.md +12 -0
- package/src/cli/brain.mjs +4 -2
- package/src/cli/context-stats.mjs +104 -0
- package/src/cli/context.mjs +8 -0
- package/src/cli/daemon.mjs +10 -1
- package/src/cli/export.mjs +3 -2
- package/src/cli/ingest-okf.mjs +12 -15
- package/src/cli/init.mjs +0 -85
- package/src/cli/lint.mjs +3 -2
- package/src/cli/plugin/copies.mjs +68 -0
- package/src/cli/plugin/create.mjs +22 -24
- package/src/cli/plugin/index.mjs +49 -3
- package/src/cli/plugin/search.mjs +19 -2
- package/src/cli/rebuild.mjs +2 -0
- package/src/cli/remember.mjs +23 -0
- package/src/cli/status.mjs +10 -3
- package/src/cli/update.mjs +3 -2
- package/src/core/action-predictor.mjs +161 -0
- package/src/core/app-deploy/runtime-cli/node.mjs.tmpl +20 -3
- package/src/core/app-deploy/source.mjs +16 -9
- package/src/core/automatic-brain-updates.mjs +6 -3
- package/src/core/brain-registry.mjs +19 -2
- package/src/core/brain-state.mjs +23 -0
- package/src/core/cli-commands.mjs +1 -0
- package/src/core/cli-maintenance.mjs +1 -1
- package/src/core/cli-semantic-index.mjs +1 -1
- package/src/core/command-surface.mjs +3 -3
- package/src/core/composed-command-resolution.mjs +21 -0
- package/src/core/config.mjs +84 -0
- package/src/core/confined-memory.mjs +93 -0
- package/src/core/context-telemetry.mjs +214 -0
- package/src/core/daemon-control.mjs +14 -43
- package/src/core/fast-recall.mjs +1 -1
- package/src/core/memory-brain.mjs +2 -0
- package/src/core/plugin-activation.mjs +38 -6
- package/src/core/plugin-bundle.mjs +54 -0
- package/src/core/plugin-catalog.mjs +1 -1
- package/src/core/plugin-context.mjs +1 -1
- package/src/core/plugin-contracts/card.mjs +69 -37
- package/src/core/plugin-loader.mjs +5 -6
- package/src/core/plugin-runner.mjs +13 -6
- package/src/core/plugin-runtime.mjs +5 -4
- package/src/core/plugin-setup.mjs +5 -3
- package/src/core/plugin-state-store.mjs +12 -1
- package/src/core/plugin-state.mjs +46 -5
- package/src/core/plugin-store.mjs +78 -3
- package/src/core/plugin-tasks.mjs +3 -1
- package/src/core/plugin-updates.mjs +117 -0
- package/src/core/plugin-vendor.mjs +10 -4
- package/src/core/project-brain.mjs +14 -41
- package/src/core/project-id.mjs +4 -1
- package/src/core/required-plugins.mjs +151 -13
- package/src/core/skills-registry.mjs +27 -3
- package/src/core/ssss-kernel-bridge.mjs +3 -1
- package/src/core/startup-health.mjs +66 -16
- package/src/core/surface.mjs +51 -44
- package/src/core/testing/plugin-fixture.mjs +69 -0
- package/src/core/vault-files.mjs +13 -0
- package/src/core/vault.mjs +4 -30
- package/src/server/index.mjs +13 -1
- package/src/server/invoke-decision-child.mjs +49 -0
- package/src/server/invoke-execution.mjs +87 -0
- package/src/server/invoke-policy.mjs +64 -0
- package/src/server/invoke-read-child.mjs +26 -0
- package/src/server/invoke-targets.mjs +49 -0
- package/src/server/invoke-write-child.mjs +19 -0
- package/src/server/routes/invoke.mjs +314 -0
- package/src/server/routes/plugins.mjs +1 -1
- package/scaffold/.agent/skills/plugins/SKILL.md +0 -57
- package/scaffold/.agent/skills/plugins/references/card-design.md +0 -55
- package/scaffold/.agent/skills/plugins/references/distribution.md +0 -23
- package/scaffold/.agent/skills/plugins/references/manifest-contract.md +0 -90
- package/scaffold/.agent/skills/plugins/references/optimized/index.md +0 -9
- package/scaffold/.agent/skills/plugins/references/optimized/manifest.json +0 -35
- package/scaffold/.agent/skills/plugins/references/optimized/original.md +0 -58
- package/scaffold/.agent/skills/plugins/references/optimized/section-01.md +0 -5
- package/scaffold/.agent/skills/plugins/references/optimized/section-02.md +0 -25
- package/scaffold/.agent/skills/plugins/references/optimized/section-03.md +0 -12
- package/scaffold/.agent/skills/plugins/references/optimized/section-04.md +0 -7
- package/scaffold/.agent/skills/plugins/references/optimized/section-05.md +0 -5
- package/scaffold/.agent/skills/total-recall/openwiki/README.md +0 -10
- package/scaffold/.agent/skills/total-recall/openwiki/architecture.md +0 -5
- package/scaffold/.agent/skills/total-recall/openwiki/ide.md +0 -10
- package/scaffold/.agent/skills/total-recall/openwiki/memory.md +0 -11
- package/scaffold/.agent/skills/total-recall/openwiki/secrets.md +0 -7
- package/scaffold/.agent/skills/total-recall/openwiki/skills.md +0 -7
- package/scaffold/openwiki/README.md +0 -10
- package/scaffold/openwiki/architecture.md +0 -5
- package/scaffold/openwiki/ide.md +0 -10
- package/scaffold/openwiki/memory.md +0 -11
- package/scaffold/openwiki/secrets.md +0 -7
- package/scaffold/openwiki/skills.md +0 -7
- package/src/core/okf-adapter.mjs +0 -559
- package/templates/openwiki/README.md +0 -10
- package/templates/openwiki/architecture.md +0 -5
- package/templates/openwiki/ide.md +0 -10
- package/templates/openwiki/memory.md +0 -11
- package/templates/openwiki/secrets.md +0 -7
- 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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 {
|
|
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
|
-
|
|
194
|
-
|
|
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.
|
|
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
|
-
|
|
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
|
|
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')),
|
package/src/core/project-id.mjs
CHANGED
|
@@ -37,7 +37,10 @@ export function readProjectId(brainDir) {
|
|
|
37
37
|
*/
|
|
38
38
|
export function ensureProjectId(brainDir) {
|
|
39
39
|
const file = brainJsonPath(brainDir);
|
|
40
|
-
|
|
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);
|