total-recall-brain 3.40.1 → 3.40.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 (186) hide show
  1. package/README.md +152 -41
  2. package/bin/total-recall.mjs +16 -76
  3. package/docs/reference/cli-reference.md +10 -10
  4. package/extension/background.js +6 -2
  5. package/extension/config.js +8 -0
  6. package/extension/lib/brain-client.js +29 -4
  7. package/extension/options/options.html +1 -0
  8. package/extension/sidepanel/sidepanel.html +1 -0
  9. package/frontend/dist/assets/{index-CvQRdT2j.css → index-AGUes-VG.css} +1 -1
  10. package/frontend/dist/assets/index-Bt5iVPZj.js +5618 -0
  11. package/frontend/dist/index.html +2 -2
  12. package/frontend/src/api/keys.ts +9 -2
  13. package/frontend/src/api/plugins.ts +4 -0
  14. package/frontend/src/components/plugins/card/CardEngine.tsx +2 -1
  15. package/frontend/src/components/plugins/card/card.css +1 -1
  16. package/frontend/src/pages/IntegrationsPage.tsx +315 -8
  17. package/frontend/src/pages/PluginsPage.tsx +532 -87
  18. package/package.json +6 -5
  19. package/scaffold/.agent/skills/meta-harness/references/optimized/original.md +1 -1
  20. package/scaffold/.agent/skills/meta-harness/references/optimized/section-09.md +1 -1
  21. package/scaffold/.agent/skills/total-recall/plugins/auth/package-lock.json +10 -10
  22. package/scaffold/.agent/skills/total-recall/plugins/auth/package.json +1 -6
  23. package/scaffold/.agent/skills/total-recall/plugins/auth/plugin.json +0 -1
  24. package/scaffold/.agent/skills/total-recall/plugins/auth/src/browser-session.mjs +309 -4
  25. package/scaffold/.agent/skills/total-recall/plugins/auth/src/cli-secret.mjs +44 -34
  26. package/scaffold/.agent/skills/total-recall/plugins/auth/src/google-oauth.mjs +6 -4
  27. package/scaffold/.agent/skills/total-recall/plugins/auth/src/provider-catalog.mjs +1 -1
  28. package/scaffold/.agent/skills/total-recall/plugins/auth/src/provider-rotation-recipes.mjs +118 -4
  29. package/scaffold/.agent/skills/total-recall/plugins/auth/src/routes/auth.mjs +2 -2
  30. package/scaffold/.agent/skills/total-recall/plugins/auth/src/secrets-env-export.mjs +3 -3
  31. package/scaffold/.agent/skills/total-recall/plugins/auth/src/secrets-remote-deploy.mjs +1 -1
  32. package/scaffold/.agent/skills/total-recall/plugins/auth/src/secrets-rotate.mjs +24 -6
  33. package/scaffold/.agent/skills/total-recall/plugins/auth/ui/card.js +164 -123
  34. package/scaffold/.agent/skills/total-recall/plugins/cli-agents/README.md +65 -0
  35. package/scaffold/.agent/skills/total-recall/plugins/cli-agents/cli.mjs +76 -0
  36. package/scaffold/.agent/skills/total-recall/plugins/cli-agents/package.json +19 -0
  37. package/scaffold/.agent/skills/total-recall/plugins/cli-agents/plugin.json +290 -0
  38. package/scaffold/.agent/skills/total-recall/plugins/cli-agents/src/adapters/dsh.mjs +446 -0
  39. package/scaffold/.agent/skills/total-recall/plugins/cli-agents/src/documentation.mjs +152 -0
  40. package/scaffold/.agent/skills/total-recall/plugins/cli-agents/src/executor.mjs +71 -0
  41. package/scaffold/.agent/skills/total-recall/plugins/cli-agents/src/runtime.mjs +36 -0
  42. package/scaffold/.agent/skills/total-recall/plugins/cli-agents/src/setup.mjs +35 -0
  43. package/scaffold/.agent/skills/total-recall/plugins/cli-agents/src/worker.mjs +175 -0
  44. package/scaffold/.agent/skills/total-recall/plugins/cli-agents/ui/card.js +18 -0
  45. package/scaffold/.agent/skills/total-recall/plugins/code-quality/LICENSE +21 -0
  46. package/scaffold/.agent/skills/total-recall/plugins/code-quality/README.md +84 -0
  47. package/scaffold/.agent/skills/total-recall/plugins/code-quality/cli.mjs +79 -0
  48. package/scaffold/.agent/skills/total-recall/plugins/code-quality/commands/check.mjs +6 -0
  49. package/scaffold/.agent/skills/total-recall/plugins/code-quality/commands/report.mjs +6 -0
  50. package/scaffold/.agent/skills/total-recall/plugins/code-quality/commands/run-core.mjs +27 -0
  51. package/scaffold/.agent/skills/total-recall/plugins/code-quality/commands/snapshot.mjs +163 -0
  52. package/scaffold/.agent/skills/total-recall/plugins/code-quality/core/check.mjs +757 -0
  53. package/scaffold/.agent/skills/total-recall/plugins/code-quality/core/config.schema.json +86 -0
  54. package/scaffold/.agent/skills/total-recall/plugins/code-quality/core/detect.mjs +89 -0
  55. package/scaffold/.agent/skills/total-recall/plugins/code-quality/core/report-data.mjs +145 -0
  56. package/scaffold/.agent/skills/total-recall/plugins/code-quality/core/report.mjs +210 -0
  57. package/scaffold/.agent/skills/total-recall/plugins/code-quality/core/state.mjs +20 -0
  58. package/scaffold/.agent/skills/total-recall/plugins/code-quality/package-lock.json +2869 -0
  59. package/scaffold/.agent/skills/total-recall/plugins/code-quality/package.json +28 -0
  60. package/scaffold/.agent/skills/total-recall/plugins/code-quality/plugin.json +186 -0
  61. package/scaffold/.agent/skills/total-recall/plugins/code-quality/ui/DESIGN.md +18 -0
  62. package/scaffold/.agent/skills/total-recall/plugins/code-quality/ui/card.js +272 -0
  63. package/scaffold/.agent/skills/total-recall/plugins/code-quality/ui/code-quality-report.mjs +129 -0
  64. package/scaffold/.agent/skills/total-recall/plugins/composable-cli/LICENSE +21 -0
  65. package/scaffold/.agent/skills/total-recall/plugins/composable-cli/README.md +73 -0
  66. package/scaffold/.agent/skills/total-recall/plugins/composable-cli/cli.mjs +375 -0
  67. package/scaffold/.agent/skills/total-recall/plugins/composable-cli/examples/commands/echo-tool.mjs +23 -0
  68. package/scaffold/.agent/skills/total-recall/plugins/composable-cli/examples/commands/group.mjs +262 -0
  69. package/scaffold/.agent/skills/total-recall/plugins/composable-cli/examples/commands/project-id.mjs +74 -0
  70. package/scaffold/.agent/skills/total-recall/plugins/composable-cli/package-lock.json +2869 -0
  71. package/scaffold/.agent/skills/total-recall/plugins/composable-cli/package.json +31 -0
  72. package/scaffold/.agent/skills/total-recall/plugins/composable-cli/plugin.json +221 -0
  73. package/scaffold/.agent/skills/total-recall/plugins/composable-cli/schemas/cli_command.schema.json +107 -0
  74. package/scaffold/.agent/skills/total-recall/plugins/composable-cli/skills/composable-cli/SKILL.md +63 -0
  75. package/scaffold/.agent/skills/total-recall/plugins/composable-cli/src/command.mjs +313 -0
  76. package/scaffold/.agent/skills/total-recall/plugins/composable-cli/src/compile.mjs +19 -0
  77. package/scaffold/.agent/skills/total-recall/plugins/composable-cli/src/compose.mjs +144 -0
  78. package/scaffold/.agent/skills/total-recall/plugins/composable-cli/src/contract.mjs +110 -0
  79. package/scaffold/.agent/skills/total-recall/plugins/composable-cli/src/library.mjs +23 -0
  80. package/scaffold/.agent/skills/total-recall/plugins/composable-cli/src/registry.mjs +192 -0
  81. package/scaffold/.agent/skills/total-recall/plugins/composable-cli/src/resolve.mjs +221 -0
  82. package/scaffold/.agent/skills/total-recall/plugins/composable-cli/src/risk.mjs +126 -0
  83. package/scaffold/.agent/skills/total-recall/plugins/composable-cli/src/ssss-documents.mjs +29 -0
  84. package/scaffold/.agent/skills/total-recall/plugins/composable-cli/ui/DESIGN.md +18 -0
  85. package/scaffold/.agent/skills/total-recall/plugins/composable-cli/ui/card.js +273 -0
  86. package/scaffold/.agent/skills/total-recall/plugins/embeddings/LICENSE +21 -0
  87. package/scaffold/.agent/skills/total-recall/plugins/embeddings/README.md +20 -0
  88. package/scaffold/.agent/skills/total-recall/plugins/embeddings/cli.mjs +310 -0
  89. package/scaffold/.agent/skills/total-recall/plugins/embeddings/package-lock.json +2826 -0
  90. package/scaffold/.agent/skills/total-recall/plugins/embeddings/package.json +11 -0
  91. package/scaffold/.agent/skills/total-recall/plugins/embeddings/plugin.json +62 -0
  92. package/scaffold/.agent/skills/total-recall/plugins/embeddings/ui/card.js +297 -0
  93. package/scaffold/.agent/skills/total-recall/plugins/git-sentinel/LICENSE +21 -0
  94. package/scaffold/.agent/skills/total-recall/plugins/git-sentinel/README.md +43 -0
  95. package/scaffold/.agent/skills/total-recall/plugins/git-sentinel/cli.mjs +258 -0
  96. package/scaffold/.agent/skills/total-recall/plugins/git-sentinel/package-lock.json +4005 -0
  97. package/scaffold/.agent/skills/total-recall/plugins/git-sentinel/package.json +20 -0
  98. package/scaffold/.agent/skills/total-recall/plugins/git-sentinel/plugin.json +125 -0
  99. package/scaffold/.agent/skills/total-recall/plugins/git-sentinel/ui/DESIGN.md +18 -0
  100. package/scaffold/.agent/skills/total-recall/plugins/git-sentinel/ui/card.js +278 -0
  101. package/scaffold/.agent/skills/total-recall/plugins/mesh/README.md +9 -0
  102. package/scaffold/.agent/skills/total-recall/plugins/mesh/cli.mjs +6 -0
  103. package/scaffold/.agent/skills/total-recall/plugins/mesh/package-lock.json +4721 -0
  104. package/scaffold/.agent/skills/total-recall/plugins/mesh/package.json +3 -21
  105. package/scaffold/.agent/skills/total-recall/plugins/mesh/plugin.json +14 -1
  106. package/scaffold/.agent/skills/total-recall/plugins/mesh/src/cli/mesh.mjs +10 -0
  107. package/scaffold/.agent/skills/total-recall/plugins/mesh/src/core/headscale-client.mjs +5 -1
  108. package/scaffold/.agent/skills/total-recall/plugins/mesh/src/core/leader-election.mjs +4 -13
  109. package/scaffold/.agent/skills/total-recall/plugins/mesh/src/core/mesh-access-verify.mjs +22 -0
  110. package/scaffold/.agent/skills/total-recall/plugins/mesh/src/core/mesh-files.mjs +18 -0
  111. package/scaffold/.agent/skills/total-recall/plugins/mesh/src/core/mesh.mjs +2 -0
  112. package/scaffold/.agent/skills/total-recall/plugins/mesh/test-support/memory-root.mjs +14 -0
  113. package/scaffold/.agent/skills/total-recall/plugins/openwiki/LICENSE +21 -0
  114. package/scaffold/.agent/skills/total-recall/plugins/openwiki/README.md +35 -0
  115. package/scaffold/.agent/skills/total-recall/plugins/openwiki/cli.mjs +124 -0
  116. package/scaffold/.agent/skills/total-recall/plugins/openwiki/package-lock.json +4097 -0
  117. package/scaffold/.agent/skills/total-recall/plugins/openwiki/package.json +12 -0
  118. package/scaffold/.agent/skills/total-recall/plugins/openwiki/plugin.json +165 -0
  119. package/scaffold/.agent/skills/total-recall/plugins/openwiki/src/ingest.mjs +127 -0
  120. package/scaffold/.agent/skills/total-recall/plugins/openwiki/ui/DESIGN.md +18 -0
  121. package/scaffold/.agent/skills/total-recall/plugins/openwiki/ui/card.js +279 -0
  122. package/scaffold/.agent/skills/total-recall/plugins/openwiki/ui/panel.js +539 -0
  123. package/scaffold/.agent/skills/total-recall/plugins/security/LICENSE +21 -0
  124. package/scaffold/.agent/skills/total-recall/plugins/security/README.md +67 -0
  125. package/scaffold/.agent/skills/total-recall/plugins/security/cli.mjs +932 -0
  126. package/scaffold/.agent/skills/total-recall/plugins/security/package-lock.json +3256 -0
  127. package/scaffold/.agent/skills/total-recall/plugins/security/package.json +25 -0
  128. package/scaffold/.agent/skills/total-recall/plugins/security/plugin.json +297 -0
  129. package/scaffold/.agent/skills/total-recall/plugins/security/scripts/build-ui.mjs +4 -0
  130. package/scaffold/.agent/skills/total-recall/plugins/security/skills/security/SKILL.md +37 -0
  131. package/scaffold/.agent/skills/total-recall/plugins/security/src/android.mjs +21 -0
  132. package/scaffold/.agent/skills/total-recall/plugins/security/src/devices.mjs +94 -0
  133. package/scaffold/.agent/skills/total-recall/plugins/security/src/machines.mjs +164 -0
  134. package/scaffold/.agent/skills/total-recall/plugins/security/src/scanner.mjs +41 -0
  135. package/scaffold/.agent/skills/total-recall/plugins/security/src/windows.mjs +42 -0
  136. package/scaffold/.agent/skills/total-recall/plugins/security/ui/DESIGN.md +18 -0
  137. package/scaffold/.agent/skills/total-recall/plugins/security/ui/card.js +95 -0
  138. package/scaffold/.agent/skills/total-recall/plugins/security/ui/panel.js +98 -0
  139. package/scaffold/.agent/skills/total-recall/references/auth.md +1 -1
  140. package/src/cli/brief.mjs +33 -9
  141. package/src/cli/command.mjs +2 -7
  142. package/src/cli/config.mjs +1 -0
  143. package/src/cli/connect.mjs +5 -0
  144. package/src/cli/context.mjs +2 -1
  145. package/src/cli/daemon.mjs +36 -1
  146. package/src/cli/doctor.mjs +6 -0
  147. package/src/cli/harness.mjs +3 -0
  148. package/src/cli/help.mjs +26 -0
  149. package/src/cli/init.mjs +4 -4
  150. package/src/cli/memory-entry.mjs +1 -0
  151. package/src/cli/memory-init.mjs +2 -0
  152. package/src/cli/plugin/index.mjs +11 -0
  153. package/src/cli/plugin/install.mjs +2 -1
  154. package/src/cli/rebuild.mjs +3 -1
  155. package/src/cli/startup.mjs +2 -1
  156. package/src/cli/update.mjs +19 -0
  157. package/src/core/automatic-brain-updates.mjs +67 -0
  158. package/src/core/brain-updates.mjs +21 -2
  159. package/src/core/cli-commands.mjs +71 -0
  160. package/src/core/cli-maintenance.mjs +45 -0
  161. package/src/core/cli-quick-reference.mjs +20 -0
  162. package/src/core/cli-semantic-index.mjs +722 -0
  163. package/src/core/context-compiler.mjs +27 -1
  164. package/src/core/crons.mjs +2 -13
  165. package/src/core/daemon-control.mjs +6 -4
  166. package/src/core/daemon-loop.mjs +12 -8
  167. package/src/core/default-plugins.mjs +5 -0
  168. package/src/core/global-brain-updates.mjs +39 -0
  169. package/src/core/mesh-brain-updates.mjs +30 -0
  170. package/src/core/meta-harness.mjs +108 -0
  171. package/src/core/plugin-dependencies.mjs +2 -1
  172. package/src/core/plugin-loader.mjs +2 -1
  173. package/src/core/plugin-runner.mjs +12 -4
  174. package/src/core/plugin-setup.mjs +20 -0
  175. package/src/core/plugin-store.mjs +22 -4
  176. package/src/core/plugin-tasks.mjs +1 -1
  177. package/src/core/project-brain.mjs +2 -0
  178. package/src/core/project-status.mjs +64 -0
  179. package/src/core/required-plugins.mjs +46 -0
  180. package/src/core/scaffold-plugins.mjs +17 -3
  181. package/src/core/source-brain-updates.mjs +47 -0
  182. package/src/core/startup-health.mjs +5 -3
  183. package/src/core/surface.mjs +44 -10
  184. package/src/server/routes/extension.mjs +53 -8
  185. package/templates/onboarding-interview.md +1 -1
  186. package/frontend/dist/assets/index-SOVUlZqm.js +0 -5596
@@ -0,0 +1,313 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import { fileURLToPath, pathToFileURL } from 'node:url';
4
+ import { spawn } from 'node:child_process';
5
+ import { EXIT_CODES } from './contract.mjs';
6
+
7
+ const COMMAND_NAME = /^[a-z][a-z0-9-]{0,63}$/;
8
+ export const RISKS = new Set(['read', 'write', 'money', 'dns-cert', 'production-deploy', 'secret-revoke']);
9
+
10
+ export function safeCommandName(name) {
11
+ if (!COMMAND_NAME.test(name || '')) {
12
+ throw new Error('Command name must be lowercase kebab-case (1–64 characters)');
13
+ }
14
+ return name;
15
+ }
16
+
17
+ export function isSafeRelativePath(p) {
18
+ if (typeof p !== 'string' || !p.trim() || p.includes('\0')) return false;
19
+ if (path.isAbsolute(p)) return false;
20
+ const normalized = path.normalize(p);
21
+ if (normalized.startsWith(`..${path.sep}`) || normalized === '..') return false;
22
+ return true;
23
+ }
24
+
25
+ export function builtinCommandNames() {
26
+ return new Set([
27
+ 'help',
28
+ 'version',
29
+ 'status',
30
+ 'recall',
31
+ 'remember',
32
+ 'compile',
33
+ 'resolve',
34
+ 'secret',
35
+ 'secrets',
36
+ 'harness',
37
+ 'agent',
38
+ 'mesh',
39
+ 'brief',
40
+ 'group',
41
+ 'groups',
42
+ 'project-id',
43
+ 'skill',
44
+ 'skills',
45
+ 'plugin',
46
+ 'plugins',
47
+ 'app',
48
+ 'command',
49
+ 'start',
50
+ ]);
51
+ }
52
+
53
+ function headerTags(description, risk) {
54
+ const one = (v) => String(v).replace(/[\r\n]+/g, ' ').trim();
55
+ return (description ? `// @description ${one(description)}\n` : '') + (risk ? `// @risk ${risk}\n` : '');
56
+ }
57
+
58
+ function existingTag(file, key) {
59
+ try {
60
+ return fs.readFileSync(file, 'utf8').match(new RegExp(`^// @${key} (.+)$`, 'm'))?.[1] || undefined;
61
+ } catch {
62
+ return undefined;
63
+ }
64
+ }
65
+
66
+ export function generatePluginCommand(pluginDir, name, commandsDir) {
67
+ safeCommandName(name);
68
+ const root = fs.realpathSync(pluginDir);
69
+ const manifestPath = path.join(root, 'plugin.json');
70
+ if (!fs.existsSync(manifestPath) || !fs.statSync(manifestPath).isFile()) {
71
+ throw new Error('Plugin manifest must be a regular file');
72
+ }
73
+ const manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf8'));
74
+ const spec = (manifest.commands || []).find((entry) => entry.name === name);
75
+ if (!spec) throw new Error(`Plugin '${manifest.id}' has no command '${name}'`);
76
+ if (!isSafeRelativePath(spec.handler) || spec.handler.includes('\\') || !spec.handler.endsWith('.mjs')) {
77
+ throw new Error(`Command '${name}' needs a safe .mjs handler path`);
78
+ }
79
+ const handlerPath = path.resolve(root, spec.handler);
80
+ const relative = path.relative(root, handlerPath);
81
+ if (!relative || relative.startsWith(`..${path.sep}`) || relative === '..' || path.isAbsolute(relative)
82
+ || !fs.statSync(handlerPath).isFile() || fs.realpathSync(handlerPath) !== handlerPath) {
83
+ throw new Error(`Command '${name}' handler must be a regular file inside the plugin`);
84
+ }
85
+ fs.mkdirSync(commandsDir, { recursive: true });
86
+ const commandPath = path.join(commandsDir, `${name}.mjs`);
87
+ if (fs.existsSync(commandPath)) throw new Error(`Command '${name}' already exists`);
88
+ const handlerUrl = pathToFileURL(handlerPath).href;
89
+
90
+ const content = `// Generated from plugin ${manifest.id}; edit the plugin manifest/handler, then regenerate.
91
+ import fs from 'node:fs';
92
+ import path from 'node:path';
93
+ import crypto from 'node:crypto';
94
+ import { spawn } from 'node:child_process';
95
+ import { fileURLToPath } from 'node:url';
96
+
97
+ const name = ${JSON.stringify(name)};
98
+ const description = ${JSON.stringify(spec.description || '')};
99
+ const handlerUrl = ${JSON.stringify(handlerUrl)};
100
+ const backgroundAllowed = ${spec.background === true};
101
+
102
+ export async function run(argv = process.argv, direct = false) {
103
+ const args = argv.slice(direct ? 2 : 3);
104
+ const json = args.includes('--json');
105
+ if (args.includes('--help') || args.includes('-h')) {
106
+ console.log('Usage: total-recall ' + name + ' [args…] [--json]' + (backgroundAllowed ? ' [--background]' : '') + '\\n\\n' + description);
107
+ return;
108
+ }
109
+ try {
110
+ if (args.includes('--background')) {
111
+ if (!backgroundAllowed) throw Object.assign(new Error('Background execution is not enabled for this command'), { exitCode: 2 });
112
+ const reports = path.join(path.dirname(fileURLToPath(import.meta.url)), 'reports');
113
+ fs.mkdirSync(reports, { recursive: true, mode: 0o700 });
114
+ const report = path.join(reports, name + '-' + crypto.randomUUID() + '.log');
115
+ const fd = fs.openSync(report, 'wx', 0o600);
116
+ const child = spawn(process.execPath, [fileURLToPath(import.meta.url), ...args.filter((a) => a !== '--background')], {
117
+ detached: true, stdio: ['ignore', fd, fd]
118
+ });
119
+ fs.closeSync(fd);
120
+ child.unref();
121
+ const status = { ok: true, pid: child.pid, report };
122
+ if (json) console.log(JSON.stringify(status));
123
+ else console.log('Started ' + name + ' in background (pid ' + child.pid + '); report: ' + report);
124
+ return status;
125
+ }
126
+ const handler = await import(handlerUrl);
127
+ const entry = handler.run || handler.default;
128
+ if (typeof entry !== 'function') throw new Error('Plugin command handler must export run() or default function');
129
+ const handlerArgs = args.filter((arg) => arg !== '--json');
130
+ const result = await entry(handler.run ? [process.execPath, 'total-recall', name, ...handlerArgs] : handlerArgs);
131
+ const exitCode = Number.isInteger(result?.exitCode) ? result.exitCode : 0;
132
+ if (json) console.log(JSON.stringify({ ok: exitCode === 0, exit_code: exitCode, result: result?.data ?? result ?? null }));
133
+ process.exitCode = exitCode;
134
+ return result;
135
+ } catch (error) {
136
+ const exitCode = Number.isInteger(error?.exitCode) ? error.exitCode : 1;
137
+ if (json) console.log(JSON.stringify({ ok: false, exit_code: exitCode, error: error.message }));
138
+ else console.error(error.message);
139
+ process.exitCode = exitCode;
140
+ }
141
+ }
142
+
143
+ if (process.argv[1] && fs.realpathSync(process.argv[1]) === fileURLToPath(import.meta.url)) {
144
+ await run(process.argv, true);
145
+ }
146
+ `;
147
+ fs.writeFileSync(commandPath, content, { encoding: 'utf8', flag: 'wx', mode: 0o644 });
148
+ return commandPath;
149
+ }
150
+
151
+ export function generateSkillConfigCommand(skillId, name, commandsDir, options = {}) {
152
+ safeCommandName(name);
153
+ safeCommandName(skillId);
154
+ const repoRoot = options.repoRoot || path.resolve(commandsDir, '..', '..');
155
+ const schemaPath = path.join(repoRoot, '.agent', 'skills', skillId, 'core', 'config.schema.json');
156
+ if (!fs.existsSync(schemaPath) || !fs.statSync(schemaPath).isFile()) {
157
+ throw new Error(`Skill '${skillId}' in ${repoRoot} has no core/config.schema.json; deploy the layered skill first`);
158
+ }
159
+ fs.mkdirSync(commandsDir, { recursive: true });
160
+ const commandPath = path.join(commandsDir, `${name}.mjs`);
161
+ if (fs.existsSync(commandPath)) throw new Error(`Command '${name}' already exists`);
162
+
163
+ const content = `// Generated repo-layer config command for skill ${skillId}; the contract is .agent/skills/${skillId}/core/config.schema.json.
164
+ import fs from 'node:fs';
165
+ import path from 'node:path';
166
+ import { fileURLToPath, pathToFileURL } from 'node:url';
167
+
168
+ const skillId = ${JSON.stringify(skillId)};
169
+ const name = ${JSON.stringify(name)};
170
+
171
+ export async function run(argv = process.argv) {
172
+ const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..', '..');
173
+ const candidate = path.join(repoRoot, '.agent', 'skills', 'total-recall', 'src', 'cli', 'skill-config.mjs');
174
+ const { runSkillConfig } = await import(pathToFileURL(candidate).href);
175
+ return runSkillConfig(skillId, argv.slice(3), { repoRoot, prefix: 'total-recall ' + name });
176
+ }
177
+ `;
178
+ fs.writeFileSync(commandPath, content, { encoding: 'utf8', flag: 'wx', mode: 0o644 });
179
+ return commandPath;
180
+ }
181
+
182
+ export function commandCreate(options = {}) {
183
+ const {
184
+ name,
185
+ code,
186
+ description,
187
+ risk,
188
+ commandsDir,
189
+ fromPlugin,
190
+ configForSkill,
191
+ repoRoot,
192
+ } = options;
193
+
194
+ safeCommandName(name);
195
+
196
+ if (builtinCommandNames().has(name)) {
197
+ const err = new Error(`'${name}' is a built-in Total Recall command; a custom command with that name could never run.`);
198
+ err.exitCode = EXIT_CODES.USAGE;
199
+ throw err;
200
+ }
201
+
202
+ if (configForSkill) {
203
+ return generateSkillConfigCommand(configForSkill, name, commandsDir, { repoRoot });
204
+ }
205
+
206
+ if (fromPlugin) {
207
+ return generatePluginCommand(fromPlugin, name, commandsDir);
208
+ }
209
+
210
+ if (!code) {
211
+ const err = new Error('Missing code snippet for the command');
212
+ err.exitCode = EXIT_CODES.USAGE;
213
+ throw err;
214
+ }
215
+
216
+ fs.mkdirSync(commandsDir, { recursive: true });
217
+ const commandPath = path.join(commandsDir, `${name}.mjs`);
218
+ if (fs.existsSync(commandPath)) {
219
+ const err = new Error(`Custom command '${name}' already exists.`);
220
+ err.exitCode = EXIT_CODES.ERROR;
221
+ throw err;
222
+ }
223
+
224
+ const fileContent = `// Auto-generated custom CLI command: ${name}
225
+ ${headerTags(description, risk)}export async function run(argv = []) {
226
+ const args = Array.isArray(argv) ? argv.slice(3) : [];
227
+ ${code}
228
+ }
229
+ export default async function (args = []) {
230
+ ${code}
231
+ }
232
+ `;
233
+
234
+ fs.writeFileSync(commandPath, fileContent, 'utf8');
235
+ return commandPath;
236
+ }
237
+
238
+ export function commandRead(name, commandsDir) {
239
+ safeCommandName(name);
240
+ const commandPath = path.join(commandsDir, `${name}.mjs`);
241
+ if (!fs.existsSync(commandPath)) {
242
+ const err = new Error(`Custom command '${name}' does not exist in ${commandsDir}.`);
243
+ err.exitCode = EXIT_CODES.ERROR;
244
+ throw err;
245
+ }
246
+ return fs.readFileSync(commandPath, 'utf8');
247
+ }
248
+
249
+ export function commandUpdate(options = {}) {
250
+ const {
251
+ name,
252
+ code,
253
+ description,
254
+ risk,
255
+ commandsDir,
256
+ } = options;
257
+
258
+ safeCommandName(name);
259
+ const commandPath = path.join(commandsDir, `${name}.mjs`);
260
+ if (!fs.existsSync(commandPath)) {
261
+ const err = new Error(`Custom command '${name}' does not exist in ${commandsDir}.`);
262
+ err.exitCode = EXIT_CODES.ERROR;
263
+ throw err;
264
+ }
265
+
266
+ const keptDescription = description ?? existingTag(commandPath, 'description');
267
+ const keptRisk = risk ?? existingTag(commandPath, 'risk');
268
+ const fileContent = `// Auto-generated custom CLI command: ${name}
269
+ ${headerTags(keptDescription, keptRisk)}export async function run(argv = []) {
270
+ const args = Array.isArray(argv) ? argv.slice(3) : [];
271
+ ${code}
272
+ }
273
+ export default async function (args = []) {
274
+ ${code}
275
+ }
276
+ `;
277
+
278
+ fs.writeFileSync(commandPath, fileContent, 'utf8');
279
+ return commandPath;
280
+ }
281
+
282
+ export function commandRemove(name, commandsDir) {
283
+ safeCommandName(name);
284
+ const commandPath = path.join(commandsDir, `${name}.mjs`);
285
+ if (!fs.existsSync(commandPath)) {
286
+ const err = new Error(`Custom command '${name}' does not exist.`);
287
+ err.exitCode = EXIT_CODES.ERROR;
288
+ throw err;
289
+ }
290
+ fs.unlinkSync(commandPath);
291
+ return true;
292
+ }
293
+
294
+ export function commandList(targets = []) {
295
+ const results = [];
296
+ for (const t of targets) {
297
+ if (fs.existsSync(t.dir)) {
298
+ const files = fs.readdirSync(t.dir).filter((f) => f.endsWith('.mjs'));
299
+ for (const f of files) {
300
+ const fullPath = path.join(t.dir, f);
301
+ const name = f.replace(/\.mjs$/, '');
302
+ results.push({
303
+ name,
304
+ scope: t.scope || 'project',
305
+ path: fullPath,
306
+ description: existingTag(fullPath, 'description') || null,
307
+ risk: existingTag(fullPath, 'risk') || null,
308
+ });
309
+ }
310
+ }
311
+ }
312
+ return results;
313
+ }
@@ -0,0 +1,19 @@
1
+ /** Rebuild host projections after an owned command mutation. */
2
+ import fs from 'node:fs';
3
+ import path from 'node:path';
4
+ import { pathToFileURL } from 'node:url';
5
+
6
+ export const MUTATIONS = new Set(['create', 'update', 'remove', 'rollback', 'promote']);
7
+
8
+ export async function compileCommandMutation(args, { projectRoot = process.cwd(), packageRoot = process.env.TR_PACKAGE_ROOT, compile } = {}) {
9
+ if (!compile) {
10
+ if (!packageRoot || !path.isAbsolute(packageRoot)) throw new Error('Verified Total Recall host package context required for automatic compilation');
11
+ const root = fs.realpathSync(packageRoot);
12
+ if (JSON.parse(fs.readFileSync(path.join(root, 'package.json'), 'utf8')).name !== 'total-recall-brain') throw new Error('Selected host is not a Total Recall package');
13
+ const host = await import(pathToFileURL(path.join(root, 'src/core/plugin-store.mjs')).href);
14
+ compile = host.recompileAfterPluginMutation;
15
+ }
16
+ const result = await compile(projectRoot, args.includes('--global') || args.includes('--group'));
17
+ if (!result?.ok) throw new Error(`Command changed but automatic compilation failed: ${result?.error || 'compiler returned no success'}; run total-recall compile`);
18
+ return result;
19
+ }
@@ -0,0 +1,144 @@
1
+ /**
2
+ * tr() — Composable CLI Composition Helper
3
+ *
4
+ * Calls a verb (subcommand, custom command, or built-in) and returns
5
+ * a standard envelope { ok, exitCode, data }.
6
+ *
7
+ * v2 commands (export default function main(args, { tr, json, dryRun, log }))
8
+ * run in-process with an isolated argv and no subprocess overhead.
9
+ * Built-ins run as subprocesses with --json. Custom commands require native metadata.
10
+ *
11
+ * Secrets are passed only through stdin, never on argv.
12
+ * Nested calls inherit dryRun from the parent; a nested risky call
13
+ * without --yes fails the parent with exit 3.
14
+ */
15
+ import { spawnSync } from 'node:child_process';
16
+ import path from 'node:path';
17
+ import fs from 'node:fs';
18
+ import { isV2Command } from './contract.mjs';
19
+ import { hasRiskGate, dryRunResult } from './risk.mjs';
20
+
21
+ const CLI_PATH = process.argv[1]; // total-recall bin
22
+
23
+ /**
24
+ * Resolve a verb to find its source module.
25
+ * Follows the same resolution order as the core dispatcher:
26
+ * built-in → project custom → global custom → plugin.
27
+ * Returns { source: 'builtin'|'project'|'global'|'plugin', path: string|null }
28
+ */
29
+ function resolveVerb(verb) {
30
+ // Try built-in resolution: the bin has a COMMANDS map
31
+ // For now, we treat anything not found locally as needing subprocess
32
+
33
+ // Project custom commands
34
+ const projectCmdPath = path.join(process.cwd(), '.agent', 'commands', `${verb}.mjs`);
35
+ if (fs.existsSync(projectCmdPath)) {
36
+ return { source: 'project', path: projectCmdPath };
37
+ }
38
+
39
+ // Global custom commands
40
+ const homeDir = process.env.HOME || process.env.USERPROFILE;
41
+ if (homeDir) {
42
+ const globalCmdPath = path.join(homeDir, '.agent', 'commands', `${verb}.mjs`);
43
+ if (fs.existsSync(globalCmdPath)) {
44
+ return { source: 'global', path: globalCmdPath };
45
+ }
46
+ }
47
+
48
+ // Not found locally — use subprocess via the CLI itself
49
+ return { source: 'builtin', path: null };
50
+ }
51
+
52
+ /**
53
+ * Call a verb and return the envelope.
54
+ *
55
+ * @param {string} verb - The command to call (e.g. 'project-id')
56
+ * @param {string[]} args - Arguments to pass
57
+ * @param {object} [options]
58
+ * @param {boolean} [options.dryRun] - Inherited dry-run mode
59
+ * @param {boolean} [options.json] - Force JSON output
60
+ * @param {string} [options.stdin] - Piped stdin content (secrets only)
61
+ * @param {object} [options.log] - Optional logger
62
+ * @returns {Promise<{ok: boolean, exitCode: number, data: any}>}
63
+ */
64
+ export async function tr(verb, args = [], options = {}) {
65
+ const { dryRun = false, json = true, stdin, log } = options;
66
+
67
+ const resolved = resolveVerb(verb);
68
+
69
+ if (resolved.path && !isV2Command(resolved.path)) return { ok: false, exitCode: 1, error: `Command ${verb} requires native metadata; recreate it with composable-cli create` };
70
+
71
+ // Execute native custom commands in process
72
+ if (resolved.path && isV2Command(resolved.path)) {
73
+ try {
74
+ const mod = await import(resolved.path);
75
+ const handler = mod.default || mod.main;
76
+ if (typeof handler !== 'function') {
77
+ throw new Error(`Command '${verb}' has no default/main export`);
78
+ }
79
+
80
+ // Check risk gate for nested calls
81
+ let meta = {};
82
+ try {
83
+ const metaSrc = fs.readFileSync(resolved.path, 'utf8');
84
+ const metaMatch = metaSrc.match(/export\s+const\s+meta\s*=\s*({[\s\S]*?});/);
85
+ if (metaMatch) {
86
+ meta = Function(`"use strict"; return (${metaMatch[1]})`)();
87
+ }
88
+ } catch { /* ignore parse errors */ }
89
+
90
+ if (meta.risk && hasRiskGate(meta.risk) && !args.includes('--yes')) {
91
+ // Dry run: return the plan
92
+ const plan = await dryRunResult(handler, args, { verb, json, dryRun: true, log });
93
+ return { ok: false, exitCode: 3, data: plan, _riskGated: true };
94
+ }
95
+
96
+ const result = await handler(args, { tr: (...a) => tr(...a, { ...options, dryRun }), json, dryRun, log });
97
+ const exitCode = result?.exitCode ?? 0;
98
+ return { ok: exitCode === 0, exitCode, data: result?.data ?? result ?? null };
99
+ } catch (err) {
100
+ return { ok: false, exitCode: err.exitCode ?? 1, error: err.message };
101
+ }
102
+ }
103
+
104
+ // Fallback: subprocess with --json envelope
105
+ const allArgs = [CLI_PATH, verb, ...args];
106
+ if (json && !args.includes('--json')) {
107
+ allArgs.push('--json');
108
+ }
109
+
110
+ const spawnOpts = {
111
+ stdio: ['pipe', 'pipe', 'pipe'],
112
+ env: { ...process.env },
113
+ };
114
+
115
+ if (stdin) {
116
+ spawnOpts.input = stdin;
117
+ }
118
+
119
+ const child = spawnSync(process.execPath, allArgs, spawnOpts);
120
+ const stdout = (child.stdout || '').toString().trim();
121
+ const stderr = (child.stderr || '').toString().trim();
122
+
123
+ if (child.error) {
124
+ return { ok: false, exitCode: 1, error: child.error.message };
125
+ }
126
+
127
+ const exitCode = child.status ?? 1;
128
+
129
+ if (json) {
130
+ // Try to parse JSON envelope; last JSON line wins
131
+ const lines = stdout.split('\n').filter(Boolean);
132
+ const jsonLines = lines.filter((l) => {
133
+ try { JSON.parse(l); return true; } catch { return false; }
134
+ });
135
+ if (jsonLines.length > 0) {
136
+ const parsed = JSON.parse(jsonLines[jsonLines.length - 1]);
137
+ return { ok: parsed.ok ?? exitCode === 0, exitCode, data: parsed.result ?? parsed.data ?? parsed, ...(parsed.error ? { error: parsed.error } : {}) };
138
+ }
139
+ }
140
+
141
+ return { ok: exitCode === 0, exitCode, data: stdout || null, stderr: stderr || null };
142
+ }
143
+
144
+ export default tr;
@@ -0,0 +1,110 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import crypto from 'node:crypto';
4
+ import { spawn } from 'node:child_process';
5
+ import { fileURLToPath, pathToFileURL } from 'node:url';
6
+
7
+ /**
8
+ * Standard Composable CLI v2 Command Contract
9
+ *
10
+ * Exit codes:
11
+ * 0: Success
12
+ * 1: Failure / Error
13
+ * 2: Usage / Bad arguments
14
+ * 3: Risk gate abstained / Dry-run
15
+ */
16
+ export const EXIT_CODES = {
17
+ OK: 0,
18
+ ERROR: 1,
19
+ USAGE: 2,
20
+ ABSTAIN: 3,
21
+ };
22
+
23
+ export function wrapCommandExecution(options = {}) {
24
+ const {
25
+ name,
26
+ description = '',
27
+ handler,
28
+ backgroundAllowed = false,
29
+ } = options;
30
+
31
+ return async function run(argv = process.argv, direct = false) {
32
+ const args = argv.slice(direct ? 2 : 3);
33
+ const json = args.includes('--json');
34
+
35
+ if (args.includes('--help') || args.includes('-h')) {
36
+ const usage = `Usage: total-recall ${name} [args…] [--json]${backgroundAllowed ? ' [--background]' : ''}\n\n${description}`;
37
+ if (json) {
38
+ console.log(JSON.stringify({ ok: true, name, description, usage, background_allowed: backgroundAllowed }));
39
+ } else {
40
+ console.log(usage);
41
+ }
42
+ return;
43
+ }
44
+
45
+ try {
46
+ if (args.includes('--background')) {
47
+ if (!backgroundAllowed) {
48
+ const err = new Error('Background execution is not enabled for this command');
49
+ err.exitCode = EXIT_CODES.USAGE;
50
+ throw err;
51
+ }
52
+
53
+ const reportsDir = path.join(process.cwd(), '.agent', 'reports');
54
+ fs.mkdirSync(reportsDir, { recursive: true, mode: 0o700 });
55
+ const report = path.join(reportsDir, `${name}-${crypto.randomUUID()}.log`);
56
+ const fd = fs.openSync(report, 'wx', 0o600);
57
+
58
+ const cleanArgs = args.filter((a) => a !== '--background');
59
+ const child = spawn(process.execPath, [fileURLToPath(import.meta.url), ...cleanArgs], {
60
+ detached: true,
61
+ stdio: ['ignore', fd, fd],
62
+ });
63
+ fs.closeSync(fd);
64
+ child.unref();
65
+
66
+ const status = { ok: true, pid: child.pid, report };
67
+ if (json) console.log(JSON.stringify(status));
68
+ else console.log(`Started ${name} in background (pid ${child.pid}); report: ${report}`);
69
+ return status;
70
+ }
71
+
72
+ const handlerArgs = args.filter((arg) => arg !== '--json');
73
+ const result = await handler(handlerArgs, { json, rawArgv: argv });
74
+ const exitCode = Number.isInteger(result?.exitCode) ? result.exitCode : EXIT_CODES.OK;
75
+
76
+ if (json) {
77
+ console.log(JSON.stringify({
78
+ ok: exitCode === EXIT_CODES.OK,
79
+ exit_code: exitCode,
80
+ result: result?.data ?? result ?? null,
81
+ }));
82
+ }
83
+ process.exitCode = exitCode;
84
+ return result;
85
+ } catch (error) {
86
+ const exitCode = Number.isInteger(error?.exitCode) ? error.exitCode : EXIT_CODES.ERROR;
87
+ if (json) {
88
+ console.log(JSON.stringify({ ok: false, exit_code: exitCode, error: error.message }));
89
+ } else {
90
+ console.error(`Error: ${error.message}`);
91
+ }
92
+ process.exitCode = exitCode;
93
+ }
94
+ };
95
+ }
96
+
97
+ /**
98
+ * Detect whether a command file is v2 (export const meta present).
99
+ *
100
+ * @param {string} filePath
101
+ * @returns {boolean}
102
+ */
103
+ export function isV2Command(filePath) {
104
+ try {
105
+ const content = fs.readFileSync(filePath, 'utf8');
106
+ return /export\s+const\s+meta\s*=/.test(content);
107
+ } catch {
108
+ return false;
109
+ }
110
+ }
@@ -0,0 +1,23 @@
1
+ /** One CLI discovery view: installed plugin operations plus owned composed commands. */
2
+ import fs from 'node:fs';
3
+ import path from 'node:path';
4
+ import { pathToFileURL } from 'node:url';
5
+ import { commandList } from './command.mjs';
6
+ import { getCommandTargets } from './resolve.mjs';
7
+
8
+ export async function buildCliLibrary({ projectRoot = process.cwd(), packageRoot = process.env.TR_PACKAGE_ROOT, loadCatalog } = {}) {
9
+ if (!loadCatalog) {
10
+ if (!packageRoot || !path.isAbsolute(packageRoot)) throw new Error('Verified Total Recall host package context required for CLI library');
11
+ const root = fs.realpathSync(packageRoot);
12
+ const pkg = JSON.parse(fs.readFileSync(path.join(root, 'package.json'), 'utf8'));
13
+ if (pkg.name !== 'total-recall-brain') throw new Error('Selected host is not a Total Recall package');
14
+ const module = await import(pathToFileURL(path.join(root, 'src/core/plugin-catalog.mjs')).href);
15
+ loadCatalog = module.buildPluginCatalog;
16
+ }
17
+ const catalog = await loadCatalog(projectRoot);
18
+ const composedCommands = commandList(getCommandTargets({ cwd: projectRoot })).map(item => ({
19
+ name: item.name, scope: item.scope, description: item.description || '', risk: item.risk || null,
20
+ command: `total-recall ${item.name}`,
21
+ }));
22
+ return { ...catalog, composedCommands, evidence: 'Declared local capabilities; provider readiness requires execution verification' };
23
+ }