total-recall-brain 3.41.0 → 3.41.2

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 (51) hide show
  1. package/package.json +1 -1
  2. package/scaffold/.agent/skills/meta-harness/references/optimized/original.md +1 -1
  3. package/scaffold/.agent/skills/total-recall/plugins/auth/README.md +10 -0
  4. package/scaffold/.agent/skills/total-recall/plugins/auth/package-lock.json +1456 -12
  5. package/scaffold/.agent/skills/total-recall/plugins/auth/package.json +4 -4
  6. package/scaffold/.agent/skills/total-recall/plugins/auth/plugin.json +1 -1
  7. package/scaffold/.agent/skills/total-recall/plugins/auth/src/cli-secret.mjs +210 -27
  8. package/scaffold/.agent/skills/total-recall/plugins/auth/src/provider-catalog.mjs +10 -1
  9. package/scaffold/.agent/skills/total-recall/plugins/auth/src/rotation-capability.mjs +15 -0
  10. package/scaffold/.agent/skills/total-recall/plugins/auth/src/secrets-env-export.mjs +78 -6
  11. package/scaffold/.agent/skills/total-recall/plugins/auth/src/secrets-merge.mjs +131 -0
  12. package/scaffold/.agent/skills/total-recall/plugins/auth/src/secrets-remote-deploy.mjs +111 -2
  13. package/scaffold/.agent/skills/total-recall/plugins/auth/src/secrets-store.mjs +26 -5
  14. package/scaffold/.agent/skills/total-recall/plugins/code-quality/package-lock.json +4 -4
  15. package/scaffold/.agent/skills/total-recall/plugins/composable-cli/package-lock.json +4 -4
  16. package/scaffold/.agent/skills/total-recall/plugins/embeddings/package-lock.json +4 -4
  17. package/scaffold/.agent/skills/total-recall/plugins/git-sentinel/package-lock.json +4 -4
  18. package/scaffold/.agent/skills/total-recall/plugins/mesh/README.md +10 -0
  19. package/scaffold/.agent/skills/total-recall/plugins/mesh/{src/core/headscale-server.mjs → capability/control-server.mjs} +5 -2
  20. package/scaffold/.agent/skills/total-recall/plugins/mesh/capability/headscale-api.mjs +192 -0
  21. package/scaffold/.agent/skills/total-recall/plugins/mesh/capability/index.mjs +46 -0
  22. package/scaffold/.agent/skills/total-recall/plugins/mesh/cli.mjs +9 -1
  23. package/scaffold/.agent/skills/total-recall/plugins/mesh/package-lock.json +23 -22
  24. package/scaffold/.agent/skills/total-recall/plugins/mesh/package.json +6 -3
  25. package/scaffold/.agent/skills/total-recall/plugins/mesh/plugin.json +242 -8
  26. package/scaffold/.agent/skills/total-recall/plugins/mesh/src/cli/mesh.mjs +14 -9
  27. package/scaffold/.agent/skills/total-recall/plugins/mesh/src/cli/server.mjs +1 -1
  28. package/scaffold/.agent/skills/total-recall/plugins/mesh/src/core/credentials.mjs +4 -2
  29. package/scaffold/.agent/skills/total-recall/plugins/mesh/src/core/headscale-client.mjs +34 -181
  30. package/scaffold/.agent/skills/total-recall/plugins/mesh/src/core/mesh-auth.mjs +2 -2
  31. package/scaffold/.agent/skills/total-recall/plugins/mesh/src/core/presence.mjs +5 -5
  32. package/scaffold/.agent/skills/total-recall/plugins/mesh/src/core/screen-view.mjs +78 -0
  33. package/scaffold/.agent/skills/total-recall/plugins/mesh/ui/DESIGN.md +211 -12
  34. package/scaffold/.agent/skills/total-recall/plugins/mesh/ui/card.js +323 -388
  35. package/scaffold/.agent/skills/total-recall/plugins/openwiki/README.md +12 -1
  36. package/scaffold/.agent/skills/total-recall/plugins/openwiki/cli.mjs +7 -4
  37. package/scaffold/.agent/skills/total-recall/plugins/openwiki/package-lock.json +6 -6
  38. package/scaffold/.agent/skills/total-recall/plugins/openwiki/package.json +3 -2
  39. package/scaffold/.agent/skills/total-recall/plugins/openwiki/plugin.json +34 -1
  40. package/scaffold/.agent/skills/total-recall/plugins/openwiki/src/bootstrap.mjs +99 -0
  41. package/scaffold/.agent/skills/total-recall/plugins/openwiki/templates/openwiki/README.md +10 -0
  42. package/scaffold/.agent/skills/total-recall/plugins/openwiki/templates/openwiki/architecture.md +5 -0
  43. package/scaffold/.agent/skills/total-recall/plugins/openwiki/templates/openwiki/ide.md +10 -0
  44. package/scaffold/.agent/skills/total-recall/plugins/openwiki/templates/openwiki/memory.md +11 -0
  45. package/scaffold/.agent/skills/total-recall/plugins/openwiki/templates/openwiki/secrets.md +7 -0
  46. package/scaffold/.agent/skills/total-recall/plugins/openwiki/templates/openwiki/skills.md +7 -0
  47. package/scaffold/.agent/skills/total-recall/plugins/security/package-lock.json +4 -4
  48. package/scaffold/.agent/skills/total-recall/references/auth.md +2 -0
  49. package/src/cli/brain.mjs +68 -0
  50. package/src/cli/connect.mjs +10 -39
  51. package/src/core/surface.mjs +2 -2
@@ -14,8 +14,19 @@ Peers install it with `npx total-recall plugin install peer:<this-node>/openwiki
14
14
 
15
15
  ## Features
16
16
 
17
+ OpenWiki owns documentation bootstrap and templates. Total Recall core initializes portable memory without creating OpenWiki pages. After installing this plugin, create missing documentation explicitly:
18
+
19
+ ```sh
20
+ total-recall openwiki bootstrap --brain /absolute/path/to/brain \
21
+ --project-root /absolute/path/to/repository --json
22
+ ```
23
+
24
+ `--brain` is required. `--project-root` adds repository documentation; omit it to seed only the brain's `openwiki/` directory. Existing files and page symlinks are preserved. A symlinked destination directory or a non-file page fails explicitly. `--dry-run` reports planned pages without writing. Bootstrap is a local Node-only operation and does not require credentials, a Total Recall server, ingestion or a provider request.
25
+
26
+ `total-recall openwiki hubs --json` lists repository pages. `total-recall openwiki ingest <directory> --project` explicitly imports documentation through the existing memory CLI. A successful bootstrap does not claim indexing or runtime readiness.
27
+
17
28
  - **Identifier**: `openwiki`
18
- - **Version**: 0.1.0
29
+ - **Version**: 0.1.1
19
30
  - **SSSS Category**: `openwiki-hubs`
20
31
  - **CLI Command**: `npx total-recall openwiki`
21
32
  - **UI**: `ui/panel.js` custom element with DESIGN.md tokens
@@ -6,11 +6,9 @@
6
6
  // Scaffold: report honestly what exists. Add each real subcommand to
7
7
  // IMPLEMENTED and to plugin.json "cli.subcommands" as it is built.
8
8
 
9
- import { spawnSync } from "node:child_process";
10
9
  import fs from "node:fs";
11
10
  import path from "node:path";
12
-
13
- const TR = process.env.TOTAL_RECALL_BIN || "total-recall";
11
+ import { runBootstrapCommand } from './src/bootstrap.mjs';
14
12
 
15
13
  async function probeCommand(args) {
16
14
  const json = args.includes("--json");
@@ -24,6 +22,7 @@ async function probeCommand(args) {
24
22
  const result = { status, ok, latency_ms, endpoint: "/health" };
25
23
  if (json) console.log(JSON.stringify(result));
26
24
  else console.log(`Health probe: HTTP ${status} (${latency_ms}ms) from ${host}`);
25
+ if (!ok) process.exitCode = 1;
27
26
  } catch (err) {
28
27
  const latency_ms = Date.now() - start;
29
28
  const result = { status: 503, ok: false, latency_ms, error: err.message };
@@ -84,6 +83,7 @@ const IMPLEMENTED = {};
84
83
  IMPLEMENTED.probe = probeCommand;
85
84
  IMPLEMENTED.hubs = hubsCommand;
86
85
  IMPLEMENTED.ingest = ingestCommand;
86
+ IMPLEMENTED.bootstrap = runBootstrapCommand;
87
87
 
88
88
 
89
89
  export async function run(argv = []) {
@@ -112,12 +112,15 @@ export async function run(argv = []) {
112
112
  probe [--json] Check subsystem health
113
113
  hubs [--json] List indexed documentation hubs
114
114
  ingest <path> [options] Ingest Markdown files into knowledge graph
115
+ bootstrap --brain <path> [--project-root <path>] [--dry-run] [--json]
116
+ Create missing plugin-owned documentation pages
115
117
  `);
116
118
  return;
117
119
  }
118
120
 
119
- if (IMPLEMENTED[sub]) return IMPLEMENTED[sub](args.slice(1));
121
+ if (Object.hasOwn(IMPLEMENTED, sub)) return IMPLEMENTED[sub](args.slice(1));
120
122
 
123
+ if (json) console.log(JSON.stringify({ ok: false, error: `OpenWiki operation is not implemented: ${sub}` }));
121
124
  console.error(`OpenWiki: "${sub}" is not implemented.`);
122
125
  process.exitCode = 2;
123
126
  }
@@ -1,15 +1,15 @@
1
1
  {
2
2
  "name": "tr-plugin-openwiki",
3
- "version": "0.1.1",
3
+ "version": "0.1.2",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "tr-plugin-openwiki",
9
- "version": "0.1.1",
9
+ "version": "0.1.2",
10
10
  "license": "MIT",
11
11
  "devDependencies": {
12
- "total-recall-brain": "^3.40.7"
12
+ "total-recall-brain": "3.41.1"
13
13
  }
14
14
  },
15
15
  "node_modules/@hexagon/base64": {
@@ -2556,9 +2556,9 @@
2556
2556
  }
2557
2557
  },
2558
2558
  "node_modules/total-recall-brain": {
2559
- "version": "3.40.7",
2560
- "resolved": "https://registry.npmjs.org/total-recall-brain/-/total-recall-brain-3.40.7.tgz",
2561
- "integrity": "sha512-G+sEWXVucI5Zs8uV1ie8P+mV/6cncWX76ES72T5dnPB3sFAwlcG9ous2SYTUaYUOETGB2vHTSo3cV4XWkHQ0Ww==",
2559
+ "version": "3.41.1",
2560
+ "resolved": "https://registry.npmjs.org/total-recall-brain/-/total-recall-brain-3.41.1.tgz",
2561
+ "integrity": "sha512-QraXzcZpCTMssx1gaz8xjvSKf5KqirAaxzzER/ICBrWvawdNTLUDDMsVEYQUA/7thgwtL1QVIxU4dpfCGZwp+g==",
2562
2562
  "dev": true,
2563
2563
  "license": "MIT",
2564
2564
  "workspaces": [
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tr-plugin-openwiki",
3
- "version": "0.1.1",
3
+ "version": "0.1.2",
4
4
  "type": "module",
5
5
  "description": "Multi-repo knowledge graph and documentation wiki engine for Total Recall",
6
6
  "scripts": {
@@ -13,6 +13,7 @@
13
13
  "README.md",
14
14
  "LICENSE",
15
15
  "ui/",
16
- "src/"
16
+ "src/",
17
+ "templates/"
17
18
  ]
18
19
  }
@@ -2,7 +2,7 @@
2
2
  "$schema": "https://github.com/gregiteen/total-recall/blob/main/metadata.plugin.schema.json",
3
3
  "id": "openwiki",
4
4
  "name": "OpenWiki",
5
- "version": "0.1.1",
5
+ "version": "0.1.2",
6
6
  "description": "Multi-repo knowledge graph and documentation wiki engine for Total Recall.",
7
7
  "license": "MIT",
8
8
  "branding": {
@@ -83,6 +83,39 @@
83
83
  "timeout_ms": 600000,
84
84
  "effect": "write"
85
85
  },
86
+ {
87
+ "name": "bootstrap",
88
+ "description": "Create missing OpenWiki documentation pages in explicit brain and project directories",
89
+ "effect": "write",
90
+ "output": "json",
91
+ "args": [
92
+ {
93
+ "name": "brain",
94
+ "flag": "--brain",
95
+ "label": "Brain directory",
96
+ "type": "string",
97
+ "required": true
98
+ },
99
+ {
100
+ "name": "projectRoot",
101
+ "flag": "--project-root",
102
+ "label": "Project root",
103
+ "type": "string"
104
+ },
105
+ {
106
+ "name": "dryRun",
107
+ "flag": "--dry-run",
108
+ "label": "Preview without writing",
109
+ "type": "boolean"
110
+ },
111
+ {
112
+ "name": "json",
113
+ "flag": "--json",
114
+ "label": "JSON output",
115
+ "type": "boolean"
116
+ }
117
+ ]
118
+ },
86
119
  {
87
120
  "name": "probe",
88
121
  "description": "Run HTTP 200 health verification probe",
@@ -0,0 +1,99 @@
1
+ /** Plugin-owned, create-only documentation bootstrap. No host runtime required. */
2
+ import fs from 'node:fs';
3
+ import path from 'node:path';
4
+ import { fileURLToPath } from 'node:url';
5
+
6
+ const templatesDir = fileURLToPath(new URL('../templates/openwiki/', import.meta.url));
7
+
8
+ function requireAbsolute(value, label) {
9
+ if (typeof value !== 'string' || !value || !path.isAbsolute(value)) throw new Error(`${label} must be an absolute directory path`);
10
+ return path.resolve(value);
11
+ }
12
+
13
+ function inspectDirectory(directory) {
14
+ const stat = fs.lstatSync(directory, { throwIfNoEntry: false });
15
+ if (stat && (stat.isSymbolicLink() || !stat.isDirectory())) throw new Error(`Unsafe documentation directory: ${directory}`);
16
+ }
17
+
18
+ function inspectTarget(root) {
19
+ inspectDirectory(root);
20
+ const directory = path.join(root, 'openwiki');
21
+ inspectDirectory(directory);
22
+ return directory;
23
+ }
24
+
25
+ /** Preserve every existing page; filesystem projections are not canonical memory. */
26
+ export function bootstrapOpenWiki({ brain, projectRoot = null, dryRun = false } = {}) {
27
+ const brainRoot = requireAbsolute(brain, '--brain');
28
+ const project = projectRoot === null ? null : requireAbsolute(projectRoot, '--project-root');
29
+ const roots = [...new Set([brainRoot, ...(project ? [project] : [])])];
30
+ const directories = roots.map(inspectTarget);
31
+ const templates = fs.readdirSync(templatesDir, { withFileTypes: true })
32
+ .sort((a, b) => a.name.localeCompare(b.name));
33
+ if (!templates.length || templates.some(entry => !entry.isFile() || !entry.name.endsWith('.md'))) {
34
+ throw new Error('OpenWiki documentation templates are missing or invalid');
35
+ }
36
+
37
+ // Validate every destination before creating any directory or page.
38
+ for (const directory of directories) for (const entry of templates) {
39
+ const destination = path.join(directory, entry.name);
40
+ const stat = fs.lstatSync(destination, { throwIfNoEntry: false });
41
+ if (stat && !stat.isFile() && !stat.isSymbolicLink()) throw new Error(`Documentation page is not a file: ${destination}`);
42
+ }
43
+
44
+ const targets = directories.map(directory => {
45
+ const created = [], preserved = [], planned = [];
46
+ if (!dryRun) fs.mkdirSync(directory, { recursive: true });
47
+ for (const entry of templates) {
48
+ const destination = path.join(directory, entry.name);
49
+ if (fs.lstatSync(destination, { throwIfNoEntry: false })) {
50
+ preserved.push(entry.name);
51
+ continue;
52
+ }
53
+ if (dryRun) { planned.push(entry.name); continue; }
54
+ try {
55
+ fs.copyFileSync(path.join(templatesDir, entry.name), destination, fs.constants.COPYFILE_EXCL);
56
+ created.push(entry.name);
57
+ } catch (err) {
58
+ // A concurrently authored page belongs to its author, never the template.
59
+ if (err.code !== 'EEXIST') throw err;
60
+ preserved.push(entry.name);
61
+ }
62
+ }
63
+ return { directory, created, preserved, planned };
64
+ });
65
+ return { ok: true, dry_run: dryRun, targets,
66
+ files_created: targets.reduce((sum, target) => sum + target.created.length, 0),
67
+ files_preserved: targets.reduce((sum, target) => sum + target.preserved.length, 0),
68
+ files_planned: targets.reduce((sum, target) => sum + target.planned.length, 0) };
69
+ }
70
+
71
+ export async function runBootstrapCommand(args = []) {
72
+ const json = args.includes('--json');
73
+ if (args.includes('--help') || args.includes('-h')) {
74
+ console.log('Usage: total-recall openwiki bootstrap --brain <absolute-directory> [--project-root <absolute-directory>] [--dry-run] [--json]');
75
+ return { ok: true, help: true };
76
+ }
77
+ try {
78
+ const options = {};
79
+ for (let index = 0; index < args.length; index++) {
80
+ const arg = args[index];
81
+ if (arg === '--brain' || arg === '--project-root') {
82
+ const value = args[++index];
83
+ if (!value || value.startsWith('--')) throw new Error(`Missing value for ${arg}`);
84
+ options[arg === '--brain' ? 'brain' : 'projectRoot'] = value;
85
+ } else if (arg === '--dry-run') options.dryRun = true;
86
+ else if (arg !== '--json') throw new Error(`Unknown bootstrap option: ${arg}`);
87
+ }
88
+ const result = bootstrapOpenWiki(options);
89
+ if (json) console.log(JSON.stringify(result));
90
+ else console.log(`OpenWiki: ${result.files_created} pages created, ${result.files_preserved} preserved${result.dry_run ? `, ${result.files_planned} planned (dry run)` : ''}.`);
91
+ return result;
92
+ } catch (err) {
93
+ const result = { ok: false, error: err.message };
94
+ if (json) console.log(JSON.stringify(result));
95
+ console.error(`OpenWiki bootstrap failed: ${err.message}`);
96
+ process.exitCode = 1;
97
+ return result;
98
+ }
99
+ }
@@ -0,0 +1,10 @@
1
+ # OpenWiki
2
+
3
+ Portable knowledge documentation for this Total Recall brain.
4
+
5
+ | Layer | Purpose |
6
+ |-------|---------|
7
+ | **Memory vault** | SSSS nodes (rules, prefs, facts) — machine-enforceable |
8
+ | **OpenWiki** | Human/agent docs (architecture, how-tos, stack notes) |
9
+
10
+ OpenWiki documentation is provided by the OpenWiki plugin. Edit freely; do not put secrets here.
@@ -0,0 +1,5 @@
1
+ # Architecture (this brain)
2
+
3
+ Describe the systems this brain cares about: repos, stacks, deploy targets.
4
+
5
+ Project brains should document the local codebase; the global brain documents cross-repo identity and preferences.
@@ -0,0 +1,10 @@
1
+ # IDE personalization
2
+
3
+ ```bash
4
+ npx total-recall connect claude-code # CLAUDE.md
5
+ npx total-recall connect codex # AGENTS.md
6
+ npx total-recall connect cursor # .cursor/rules/
7
+ npx total-recall connect gemini # GEMINI.md
8
+ ```
9
+
10
+ Total Recall only writes inside managed inject markers. Your local rules stay intact.
@@ -0,0 +1,11 @@
1
+ # Memory
2
+
3
+ Use the Total Recall CLI for durable memory:
4
+
5
+ ```bash
6
+ npx total-recall remember invariant "Never commit secrets." --importance 5
7
+ npx total-recall recall "secrets"
8
+ npx total-recall compile
9
+ ```
10
+
11
+ Categories: invariants, preferences, facts, decisions, patterns, anti-patterns, concepts, lore.
@@ -0,0 +1,7 @@
1
+ # Secrets & API keys
2
+
3
+ Manage credentials through the auth plugin: `total-recall auth secret list|set|rotate|audit`.
4
+
5
+ **Never** put API keys in openwiki pages or compiled IDE instruction files.
6
+
7
+ Credential values belong in the encrypted auth store, never in documentation.
@@ -0,0 +1,7 @@
1
+ # Skills
2
+
3
+ - **Total Recall skill** (only nested agent skill): `.agent/skills/total-recall/SKILL.md`
4
+ - **User skills**: `.agent/skills/<name>/SKILL.md` — project or global
5
+ - **TR modules** (not skills): `.agent/skills/total-recall/modules/` — internal docs/scripts
6
+
7
+ TR will track skill installs across repos and support adaptive deploy (see TR_CORE_FOCUS).
@@ -10,7 +10,7 @@
10
10
  "license": "MIT",
11
11
  "devDependencies": {
12
12
  "jsdom": "^26.1.0",
13
- "total-recall-brain": "^3.40.7"
13
+ "total-recall-brain": "3.41.1"
14
14
  }
15
15
  },
16
16
  "node_modules/@asamuzakjp/css-color": {
@@ -2916,9 +2916,9 @@
2916
2916
  }
2917
2917
  },
2918
2918
  "node_modules/total-recall-brain": {
2919
- "version": "3.40.7",
2920
- "resolved": "https://registry.npmjs.org/total-recall-brain/-/total-recall-brain-3.40.7.tgz",
2921
- "integrity": "sha512-G+sEWXVucI5Zs8uV1ie8P+mV/6cncWX76ES72T5dnPB3sFAwlcG9ous2SYTUaYUOETGB2vHTSo3cV4XWkHQ0Ww==",
2919
+ "version": "3.41.1",
2920
+ "resolved": "https://registry.npmjs.org/total-recall-brain/-/total-recall-brain-3.41.1.tgz",
2921
+ "integrity": "sha512-QraXzcZpCTMssx1gaz8xjvSKf5KqirAaxzzER/ICBrWvawdNTLUDDMsVEYQUA/7thgwtL1QVIxU4dpfCGZwp+g==",
2922
2922
  "dev": true,
2923
2923
  "license": "MIT",
2924
2924
  "workspaces": [
@@ -8,6 +8,8 @@ There is one secrets store per host: the global auth store. The CLI never select
8
8
 
9
9
  For provider key setup, verify against an authenticated provider operation before saving; format-only checks and timeouts are not verification. Require HTTPS except explicit loopback development, reject redirects containing credentials, and return safe errors. Rotation follows verify → store/project → deploy/verify → revoke, respecting the provider's supported recipe and user authorization. Manual provider steps remain manual. Exported environment files are deployment projections, not an alternate credential authority.
10
10
 
11
+ Every secret operation goes through `total-recall auth secret`; never read, compare or copy credential values with shell tools. Check a copied credential with `fingerprint --from-clipboard --expect-prefix <p> --not <KEY>` before `rotate <KEY> --from-clipboard`. After `remote deploy <target> --keys … [--path <repo>]`, confirm with `remote verify <target> --keys …` (fingerprint match, no values). A process manager can keep a captured environment that overrides the deployed file (pm2 does): move the keys the app uses into the store with `remote import-pm2 <target> --app <name> --keys … --apply`, then restart the app with a clean environment so it reads only the projection. Stripe webhook signing secrets are issued by Stripe; roll them in the dashboard, never mint them.
12
+
11
13
  Current migration remains in progress until package/bootstrap tests, all-plugin bypass checks, owner suites and end-to-end UI/API checks pass. Manifest `auth.plugin` declarations alone do not certify any of these properties.
12
14
 
13
15
  ## Sessions, passwords and legacy data
package/src/cli/brain.mjs CHANGED
@@ -16,6 +16,10 @@ function printHelp() { console.log(`
16
16
  register <project-path> Track any project path (full brain + registry)
17
17
  unregister <project-path> Remove a project directory path from the registry
18
18
  ensure <project-path> Ensure a full project brain layout + register
19
+ import <vault-dir> --to <project-path> [--dry-run]
20
+ Import every memory node from another vault directory
21
+ (an archive or a retired brain) into that project's
22
+ brain through validated writes; existing slugs are kept
19
23
 
20
24
  Any repo works — pass absolute or relative paths you choose. No product defaults.
21
25
 
@@ -27,6 +31,7 @@ function printHelp() { console.log(`
27
31
  npx total-recall brain off hermes-agent
28
32
  npx total-recall brain on global
29
33
  npx total-recall brain unregister /path/to/project
34
+ npx total-recall brain import ~/Archive/old-brain/memory-vault --to ~/code/my-lib
30
35
 
31
36
  Skill multi-repo sync: registered projects + TR_SYNC_REPOS + skill track/--repo
32
37
  `);
@@ -58,6 +63,9 @@ export default async function brain(args) {
58
63
  case 'ensure':
59
64
  await handleEnsure(args.slice(1), globalBrainDir);
60
65
  break;
66
+ case 'import':
67
+ await handleImport(args.slice(1));
68
+ break;
61
69
  case 'on':
62
70
  case 'off':
63
71
  await handleToggle(subcommand.toLowerCase() === 'on', args.slice(1), globalBrainDir);
@@ -104,11 +112,14 @@ async function handleEnsure(targetArgs, globalBrainDir) {
104
112
  tags: ['project-brain', 'full-brain'],
105
113
  });
106
114
  const health = inspectProjectBrain(abs);
115
+ const { ensureProjectGitignore } = await import('./init.mjs');
116
+ const ignored = ensureProjectGitignore(abs);
107
117
  console.log(`\n ✅ Full project brain ready`);
108
118
  console.log(` Repo: ${result.repoRoot}`);
109
119
  console.log(` Brain: ${result.brainDir}`);
110
120
  console.log(` complete=${health.complete} full_brain=${health.full_brain}`);
111
121
  console.log(` directories created +${result.dirs_created}`);
122
+ if (ignored.length) console.log(` .gitignore +${ignored.length} brain-state exclusions`);
112
123
  console.log('');
113
124
  }
114
125
 
@@ -224,6 +235,11 @@ async function handleRegister(registryPath, targetArgs) {
224
235
  register: true,
225
236
  });
226
237
  console.log(` ✅ Full project brain ready at ${path.join(absoluteRoot, '.agent', 'skills', 'total-recall')}`);
238
+ // A registered repo carries a brain whose config holds a live PAT; keep it
239
+ // out of git the same way `init --project` does.
240
+ const { ensureProjectGitignore } = await import('./init.mjs');
241
+ const ignored = ensureProjectGitignore(absoluteRoot);
242
+ if (ignored.length) console.log(` ✅ .gitignore +${ignored.length} brain-state exclusions`);
227
243
  } catch (err) {
228
244
  console.error(` ⚠️ Could not fully provision brain layout: ${err.message}`);
229
245
  process.exitCode = 1;
@@ -300,6 +316,58 @@ async function handleUnregister(registryPath, targetArgs) {
300
316
  console.log(` ✅ Successfully unregistered project: "${absoluteRoot}"`);
301
317
  }
302
318
 
319
+ /**
320
+ * Import memory nodes from a vault directory into a project's brain.
321
+ *
322
+ * The source is read, never written: an archived brain, or the vault of a brain
323
+ * being retired. Every node goes through the validated SSSS write path at the
324
+ * same vault-relative path, so the target's event log records each one. A slug
325
+ * the target already holds is left alone, which makes a rerun safe.
326
+ */
327
+ async function handleImport(targetArgs) {
328
+ const toIdx = targetArgs.indexOf('--to');
329
+ const source = targetArgs.find((a, i) => !a.startsWith('-') && i !== toIdx + 1);
330
+ const target = toIdx !== -1 ? targetArgs[toIdx + 1] : null;
331
+ const dryRun = targetArgs.includes('--dry-run');
332
+ if (!source || !target) {
333
+ console.error(' ⚠️ Usage: npx total-recall brain import <vault-dir> --to <project-path> [--dry-run]');
334
+ process.exit(1);
335
+ }
336
+ const sourceDir = path.resolve(source);
337
+ const projectRoot = path.resolve(target);
338
+ if (!fs.existsSync(sourceDir) || !fs.statSync(sourceDir).isDirectory()) {
339
+ console.error(` ⚠️ Source vault not found: ${sourceDir}`);
340
+ process.exit(1);
341
+ }
342
+ const vaultDir = path.join(projectRoot, '.agent', 'skills', 'total-recall', 'memory-vault');
343
+ if (!fs.existsSync(vaultDir)) {
344
+ console.error(` ⚠️ No project brain at ${projectRoot}. Run: npx total-recall brain register ${projectRoot}`);
345
+ process.exit(1);
346
+ }
347
+ if (path.resolve(sourceDir) === path.resolve(vaultDir)) {
348
+ console.error(' ⚠️ Source and target are the same vault.');
349
+ process.exit(1);
350
+ }
351
+ const { loadNodes } = await import('../core/vault.mjs');
352
+ const { writeNodeValidatedAsync } = await import('../core/validated-write.mjs');
353
+ const existing = new Set(loadNodes(vaultDir).map(n => n.slug));
354
+ let imported = 0, skipped = 0;
355
+ const failed = [];
356
+ for (const node of loadNodes(sourceDir)) {
357
+ if (existing.has(node.slug)) { skipped++; continue; }
358
+ const rel = path.relative(sourceDir, node._filePath).split(path.sep).join('/');
359
+ if (dryRun) { imported++; continue; }
360
+ const { _filePath, _filepath, ...clean } = node;
361
+ const result = await writeNodeValidatedAsync(clean, vaultDir, { path: rel });
362
+ if (result.success) { imported++; existing.add(node.slug); }
363
+ else failed.push(`${rel}: ${result.validation?.errors?.join('; ') || result.error}`);
364
+ }
365
+ const verb = dryRun ? 'Would import' : 'Imported';
366
+ console.log(` ${failed.length ? '⚠️ ' : '✅'} ${verb} ${imported} node(s) into ${vaultDir}; ${skipped} already present`);
367
+ for (const line of failed) console.error(` ✗ ${line}`);
368
+ if (failed.length) process.exitCode = 1;
369
+ }
370
+
303
371
  function countMdFiles(dir) {
304
372
  if (!fs.existsSync(dir)) return 0;
305
373
  let count = 0;
@@ -25,16 +25,20 @@ export const CLIENTS = {
25
25
  },
26
26
  hermes: {
27
27
  label: 'Hermes Agent',
28
- mode: 'hermes',
29
- memoryPath: path.join(os.homedir(), '.hermes', 'memories', 'MEMORY.md'),
30
- userPath: path.join(os.homedir(), '.hermes', 'memories', 'USER.md'),
28
+ mode: 'symlink',
29
+ // Hermes loads the AGENTS.md chain (git root to cwd) as context files.
30
+ // ~/.hermes/memories/MEMORY.md is the agent's own notes store, read only
31
+ // from HERMES_HOME; Total Recall never writes it.
32
+ target: 'AGENTS.md',
31
33
  skillsProjection: { scope: 'project', dir: path.join('.hermes', 'skills') }
32
34
  },
33
35
  'hermes-agent': {
34
36
  label: 'Hermes Agent',
35
- mode: 'hermes',
36
- memoryPath: path.join(os.homedir(), '.hermes', 'memories', 'MEMORY.md'),
37
- userPath: path.join(os.homedir(), '.hermes', 'memories', 'USER.md'),
37
+ mode: 'symlink',
38
+ // Hermes loads the AGENTS.md chain (git root to cwd) as context files.
39
+ // ~/.hermes/memories/MEMORY.md is the agent's own notes store, read only
40
+ // from HERMES_HOME; Total Recall never writes it.
41
+ target: 'AGENTS.md',
38
42
  skillsProjection: { scope: 'project', dir: path.join('.hermes', 'skills') }
39
43
  },
40
44
  dsh: {
@@ -884,39 +888,6 @@ export default async function connect(args) {
884
888
  result.projection = { targetPath, action: 'symlinked' };
885
889
  }
886
890
  result.notes.push(` ~/.pi/agent/AGENTS.md → ${instructionsPath}`);
887
- } else if (preset.mode === 'hermes') {
888
- // Hermes: write compiled MEMORY.md to ~/.hermes/memories/MEMORY.md (max 2200 chars)
889
- // and project-local .hermes/memories/MEMORY.md if inside a project workspace
890
- const instructions = readInstructions(cwd, agentDir);
891
- if (!instructions) throw new Error('No INSTRUCTIONS.md found. Run npx total-recall init or compile first.');
892
- const homeDir = process.env.HOME || os.homedir();
893
- const globalMemPath = path.join(homeDir, '.hermes', 'memories', 'MEMORY.md');
894
- const capped = instructions.slice(0, 2200);
895
-
896
- let wroteGlobal = false;
897
- try {
898
- fs.mkdirSync(path.dirname(globalMemPath), { recursive: true });
899
- fs.writeFileSync(globalMemPath, capped, 'utf8');
900
- wroteGlobal = true;
901
- } catch {
902
- // Gracefully continue if global directory is unwritable in sandbox
903
- }
904
-
905
- let localMemPath = null;
906
- if (!isHome) {
907
- localMemPath = path.join(cwd, '.hermes', 'memories', 'MEMORY.md');
908
- fs.mkdirSync(path.dirname(localMemPath), { recursive: true });
909
- fs.writeFileSync(localMemPath, capped, 'utf8');
910
- }
911
-
912
- result.projection = { targetPath: localMemPath || globalMemPath, action: 'written' };
913
- if (wroteGlobal) {
914
- result.notes.push(` Hermes MEMORY.md (${capped.length} chars) → ${globalMemPath}`);
915
- }
916
- if (localMemPath) {
917
- result.notes.push(` Hermes local MEMORY.md → ${localMemPath}`);
918
- }
919
- result.notes.push(' Note: Hermes has a 2,200-char limit on MEMORY.md. Total Recall auto-caps this.');
920
891
  } else if (preset.mode === 'dsh') {
921
892
  // DeepSeek Harness: write global ~/.dsh/memory/MEMORY.md and symlink project AGENTS.md
922
893
  const instructions = readInstructions(cwd, agentDir);
@@ -567,8 +567,8 @@ const CLIENT_SHIMS = {
567
567
  vscode: ['.github/copilot-instructions.md', '.vscode/copilot-instructions.md'],
568
568
  githubCopilot: ['.github/copilot-instructions.md'],
569
569
  pi: ['AGENTS.md'],
570
- hermes: ['.hermes/memories/MEMORY.md'],
571
- 'hermes-agent': ['.hermes/memories/MEMORY.md'],
570
+ hermes: ['AGENTS.md'],
571
+ 'hermes-agent': ['AGENTS.md'],
572
572
  dsh: ['AGENTS.md'],
573
573
  'deepseek-harness': ['AGENTS.md'],
574
574
  openclaw: ['MEMORY.md', 'AGENTS.md']