total-recall-brain 3.40.0 → 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 (187) 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/backup.mjs +71 -3
  141. package/src/cli/brief.mjs +33 -9
  142. package/src/cli/command.mjs +2 -7
  143. package/src/cli/config.mjs +1 -0
  144. package/src/cli/connect.mjs +5 -0
  145. package/src/cli/context.mjs +2 -1
  146. package/src/cli/daemon.mjs +36 -1
  147. package/src/cli/doctor.mjs +6 -0
  148. package/src/cli/harness.mjs +3 -0
  149. package/src/cli/help.mjs +26 -0
  150. package/src/cli/init.mjs +4 -4
  151. package/src/cli/memory-entry.mjs +1 -0
  152. package/src/cli/memory-init.mjs +2 -0
  153. package/src/cli/plugin/index.mjs +11 -0
  154. package/src/cli/plugin/install.mjs +2 -1
  155. package/src/cli/rebuild.mjs +3 -1
  156. package/src/cli/startup.mjs +2 -1
  157. package/src/cli/update.mjs +19 -0
  158. package/src/core/automatic-brain-updates.mjs +67 -0
  159. package/src/core/brain-updates.mjs +21 -2
  160. package/src/core/cli-commands.mjs +71 -0
  161. package/src/core/cli-maintenance.mjs +45 -0
  162. package/src/core/cli-quick-reference.mjs +20 -0
  163. package/src/core/cli-semantic-index.mjs +722 -0
  164. package/src/core/context-compiler.mjs +27 -1
  165. package/src/core/crons.mjs +2 -13
  166. package/src/core/daemon-control.mjs +6 -4
  167. package/src/core/daemon-loop.mjs +12 -8
  168. package/src/core/default-plugins.mjs +5 -0
  169. package/src/core/global-brain-updates.mjs +39 -0
  170. package/src/core/mesh-brain-updates.mjs +30 -0
  171. package/src/core/meta-harness.mjs +108 -0
  172. package/src/core/plugin-dependencies.mjs +2 -1
  173. package/src/core/plugin-loader.mjs +2 -1
  174. package/src/core/plugin-runner.mjs +12 -4
  175. package/src/core/plugin-setup.mjs +20 -0
  176. package/src/core/plugin-store.mjs +22 -4
  177. package/src/core/plugin-tasks.mjs +1 -1
  178. package/src/core/project-brain.mjs +2 -0
  179. package/src/core/project-status.mjs +64 -0
  180. package/src/core/required-plugins.mjs +46 -0
  181. package/src/core/scaffold-plugins.mjs +17 -3
  182. package/src/core/source-brain-updates.mjs +47 -0
  183. package/src/core/startup-health.mjs +5 -3
  184. package/src/core/surface.mjs +44 -10
  185. package/src/server/routes/extension.mjs +53 -8
  186. package/templates/onboarding-interview.md +1 -1
  187. package/frontend/dist/assets/index-SOVUlZqm.js +0 -5596
@@ -0,0 +1,192 @@
1
+ /**
2
+ * Command Registry — cli_command SSSS documents, history, rollback
3
+ *
4
+ * Each registered command has a sibling <name>.md SSSS document beside its
5
+ * <name>.mjs file. History is kept in <scope dir>/commands/.history/<name>/<version>.mjs.
6
+ */
7
+ import fs from 'node:fs';
8
+ import path from 'node:path';
9
+ import crypto from 'node:crypto';
10
+ import { randomUUID } from 'node:crypto';
11
+ import { EXIT_CODES } from './contract.mjs';
12
+ import { validateRisk } from './risk.mjs';
13
+ import { writeCommandDocument, parseDocument } from './ssss-documents.mjs';
14
+
15
+
16
+ /**
17
+ * Write the cli_command SSSS document for a command.
18
+ *
19
+ * @param {object} options
20
+ * @param {string} options.name - Command name
21
+ * @param {string} options.commandsDir - Directory containing the command
22
+ * @param {'project'|'group'|'global'} [options.scope]
23
+ * @param {string} [options.ownerProject]
24
+ * @param {string} [options.group]
25
+ * @param {string} [options.risk]
26
+ * @param {number} [options.version]
27
+ * @param {string} [options.author]
28
+ * @param {string} [options.description]
29
+ * @param {boolean} [options.background]
30
+ * @param {string} [options.source]
31
+ * @returns {object} The written registry document
32
+ */
33
+ export function writeRegistry(options = {}) {
34
+ const {
35
+ name,
36
+ commandsDir,
37
+ scope = 'project',
38
+ ownerProject,
39
+ group,
40
+ risk = 'write',
41
+ version = 1,
42
+ author = process.env.AGENT_ID || 'unknown',
43
+ description = '',
44
+ background = false,
45
+ source = 'inline',
46
+ } = options;
47
+
48
+ const cmdFile = path.join(commandsDir, `${name}.mjs`);
49
+ if (!fs.existsSync(cmdFile)) {
50
+ const err = new Error(`Command '${name}' does not exist in ${commandsDir}.`);
51
+ err.exitCode = EXIT_CODES.ERROR;
52
+ throw err;
53
+ }
54
+
55
+ validateRisk(risk);
56
+
57
+ const now = new Date().toISOString();
58
+ const previous = readRegistry(name, commandsDir);
59
+ const code = fs.readFileSync(cmdFile, 'utf8');
60
+ const sha = crypto.createHash('sha256').update(code).digest('hex');
61
+
62
+ const doc = {
63
+ ...previous,
64
+ type: 'cli_command',
65
+ id: previous?.id || randomUUID(),
66
+ name,
67
+ scope,
68
+ risk,
69
+ version,
70
+ author,
71
+ created: previous?.created || now,
72
+ updated: now,
73
+ description,
74
+ background_allowed: background,
75
+ code_sha256: sha,
76
+ source,
77
+ };
78
+
79
+ // Optional fields
80
+ if (ownerProject) doc.owner_project = ownerProject;
81
+ if (group) doc.group = group;
82
+
83
+ const docPath = path.join(commandsDir, `${name}.md`);
84
+ fs.mkdirSync(commandsDir, { recursive: true });
85
+ const prose = fs.existsSync(docPath) ? parseDocument(fs.readFileSync(docPath, 'utf8')).body : `# Command: ${name}\n\n${description}\n`;
86
+ writeCommandDocument(docPath, doc, prose);
87
+ return doc;
88
+ }
89
+
90
+ /**
91
+ * Read the cli_command document for a command, if present.
92
+ *
93
+ * @param {string} name
94
+ * @param {string} commandsDir
95
+ * @returns {object | null}
96
+ */
97
+ export function readRegistry(name, commandsDir) {
98
+ const docPath = path.join(commandsDir, `${name}.md`);
99
+ if (!fs.existsSync(docPath)) return null;
100
+ const content = fs.readFileSync(docPath, 'utf8');
101
+ const doc = parseDocument(content).data;
102
+ // Only return if it's a cli_command
103
+ if (doc.type !== 'cli_command') return null;
104
+ delete doc.description_if_present;
105
+ return doc;
106
+ }
107
+
108
+ /**
109
+ * Snapshot the currently registered version of a command before an update.
110
+ *
111
+ * @param {string} name
112
+ * @param {string} commandsDir
113
+ * @returns {number|null} The version number that was snapshotted, or null if none
114
+ */
115
+ export function snapshotForHistory(name, commandsDir) {
116
+ const cmdFile = path.join(commandsDir, `${name}.mjs`);
117
+ if (!fs.existsSync(cmdFile)) return null;
118
+
119
+ const doc = readRegistry(name, commandsDir);
120
+ const version = doc?.version || 1;
121
+
122
+ const historyDir = path.join(commandsDir, '.history', name);
123
+ fs.mkdirSync(historyDir, { recursive: true });
124
+
125
+ const snapFile = path.join(historyDir, `v${version}.mjs`);
126
+ if (!fs.existsSync(snapFile)) {
127
+ fs.copyFileSync(cmdFile, snapFile);
128
+ }
129
+ return version;
130
+ }
131
+
132
+ /**
133
+ * List history versions for a command.
134
+ *
135
+ * @param {string} name
136
+ * @param {string} commandsDir
137
+ * @returns {number[]} Sorted list of version numbers
138
+ */
139
+ export function listHistory(name, commandsDir) {
140
+ const histDir = path.join(commandsDir, '.history', name);
141
+ if (!fs.existsSync(histDir)) return [];
142
+ return fs.readdirSync(histDir)
143
+ .filter((f) => /^v\d+\.mjs$/.test(f))
144
+ .map((f) => parseInt(f.replace(/\.mjs$/, '').replace('v', ''), 10))
145
+ .sort((a, b) => a - b);
146
+ }
147
+
148
+ /**
149
+ * Roll back a command to a previous version.
150
+ *
151
+ * @param {string} name
152
+ * @param {number} version - Target version to restore
153
+ * @param {string} commandsDir
154
+ * @returns {number} The restored version
155
+ */
156
+ export function rollback(name, version, commandsDir) {
157
+ const snapFile = path.join(commandsDir, '.history', name, `v${version}.mjs`);
158
+ if (!fs.existsSync(snapFile)) {
159
+ const err = new Error(`Version ${version} of '${name}' does not exist in history.`);
160
+ err.exitCode = EXIT_CODES.ERROR;
161
+ throw err;
162
+ }
163
+
164
+ const cmdFile = path.join(commandsDir, `${name}.mjs`);
165
+ if (!fs.existsSync(cmdFile)) {
166
+ const err = new Error(`Command '${name}' does not exist.`);
167
+ err.exitCode = EXIT_CODES.ERROR;
168
+ throw err;
169
+ }
170
+
171
+ // Snapshot current before rollback
172
+ snapshotForHistory(name, commandsDir);
173
+
174
+ // Restore the target version
175
+ fs.copyFileSync(snapFile, cmdFile);
176
+
177
+ // Bump the registry version to reflect the rollback
178
+ const doc = readRegistry(name, commandsDir);
179
+ if (doc) {
180
+ const newVersion = (doc.version || 0) + 1;
181
+ fm(path.join(commandsDir, `${name}.md`), { ...doc, version: newVersion, updated: new Date().toISOString() });
182
+ }
183
+
184
+ return version;
185
+ }
186
+
187
+ /** Internal: rewrite a registry doc file with updated fields. */
188
+ function fm(docPath, doc) {
189
+ const body = parseDocument(fs.readFileSync(docPath, 'utf8')).body;
190
+ const code = fs.readFileSync(docPath.replace(/\.md$/, '.mjs'), 'utf8');
191
+ writeCommandDocument(docPath, { ...doc, code_sha256: crypto.createHash('sha256').update(code).digest('hex') }, body);
192
+ }
@@ -0,0 +1,221 @@
1
+ /**
2
+ * Command Resolution — scope search, shadow detection, group scope
3
+ *
4
+ * Resolution order for `total-recall <verb>`:
5
+ * 1. Built-in (provided by the total-recall core)
6
+ * 2. Project custom commands (<repo>/.agent/commands/)
7
+ * 3. Group commands (resolved through project_group membership, nearest first)
8
+ * 4. Global custom commands (~/.agent/commands/)
9
+ * 5. VFS integration (<agentDir>/skills/total-recall/integrations/)
10
+ * 6. Plugin manifests whose cli.command matches
11
+ *
12
+ * This module provides:
13
+ * - Shadow detection: refuse built-in names, warn on scope shadowing
14
+ * - Group scope search through project_group membership
15
+ * - Full resolution with provenance
16
+ */
17
+ import fs from 'node:fs';
18
+ import path from 'node:path';
19
+ import { builtinCommandNames } from './command.mjs';
20
+ import { EXIT_CODES } from './contract.mjs';
21
+
22
+ /**
23
+ * Find all commands directories for scoped resolution.
24
+ * Returns an ordered array of { scope, dir } objects.
25
+ *
26
+ * @param {object} [options]
27
+ * @param {string} [options.cwd] - Current working directory (default: process.cwd())
28
+ * @param {string[]} [options.groupDirs] - Optional group command directories
29
+ * @returns {{ scope: string, dir: string }[]}
30
+ */
31
+ export function getCommandTargets(options = {}) {
32
+ const cwd = options.cwd || process.cwd();
33
+ const targets = [];
34
+
35
+ // Project scope
36
+ const projectDir = path.join(cwd, '.agent', 'commands');
37
+ if (fs.existsSync(projectDir)) {
38
+ targets.push({ scope: 'project', dir: projectDir });
39
+ }
40
+
41
+ // Group scope (resolved from project_group documents)
42
+ const groupDirs = options.groupDirs || resolveGroupDirs(cwd);
43
+ for (const gd of groupDirs) {
44
+ if (fs.existsSync(gd)) {
45
+ targets.push({ scope: 'group', dir: gd });
46
+ }
47
+ }
48
+
49
+ // Global scope
50
+ const homeDir = process.env.HOME || process.env.USERPROFILE;
51
+ if (homeDir) {
52
+ const globalDir = path.join(homeDir, '.agent', 'commands');
53
+ if (fs.existsSync(globalDir)) {
54
+ targets.push({ scope: 'global', dir: globalDir });
55
+ }
56
+ }
57
+
58
+ return targets;
59
+ }
60
+
61
+ /**
62
+ * Resolve group command directories from project_group documents.
63
+ * Reads groups from ~/.agent/skills/total-recall/groups/
64
+ * and finds groups that contain this project.
65
+ *
66
+ * @param {string} cwd - Current working directory
67
+ * @returns {string[]} Array of group command directory paths
68
+ */
69
+ export function resolveGroupDirs(cwd) {
70
+ const dirs = [];
71
+ const homeDir = process.env.HOME || process.env.USERPROFILE;
72
+ if (!homeDir) return dirs;
73
+
74
+ const groupsDir = path.join(homeDir, '.agent', 'skills', 'total-recall', 'groups');
75
+ if (!fs.existsSync(groupsDir)) return dirs;
76
+
77
+ try {
78
+ const projectBrainPath = path.join(cwd, '.agent', 'skills', 'total-recall', 'config', 'brain.json');
79
+ let projectId = null;
80
+ if (fs.existsSync(projectBrainPath)) {
81
+ try {
82
+ const brain = JSON.parse(fs.readFileSync(projectBrainPath, 'utf8'));
83
+ projectId = brain.project_id;
84
+ } catch { /* ignore */ }
85
+ }
86
+
87
+ const groupFiles = fs.readdirSync(groupsDir).filter((f) => f.endsWith('.md'));
88
+ for (const gf of groupFiles) {
89
+ try {
90
+ const content = fs.readFileSync(path.join(groupsDir, gf), 'utf8');
91
+ const frontmatter = content.match(/^---\n([\s\S]*?)\n---/);
92
+ if (!frontmatter) continue;
93
+ const yaml = frontmatter[1];
94
+ // Simple YAML-like parsing for members
95
+ const membersMatch = yaml.match(/members:\s*\[([^\]]*)\]/);
96
+ if (!membersMatch) continue;
97
+
98
+ const membersStr = membersMatch[1];
99
+ // Check if this project is a member
100
+ const isMember = projectId && membersStr.includes(projectId);
101
+
102
+ if (isMember) {
103
+ const groupCommandsDir = path.join(homeDir, '.agent', 'commands',
104
+ 'groups', gf.replace(/\.md$/, ''));
105
+ dirs.push(groupCommandsDir);
106
+ }
107
+ } catch { /* ignore */ }
108
+ }
109
+ } catch { /* ignore */ }
110
+
111
+ return dirs;
112
+ }
113
+
114
+ /**
115
+ * Detect whether a name shadows another command in the resolution chain.
116
+ *
117
+ * @param {string} name - Command name to check
118
+ * @param {object} [options]
119
+ * @param {boolean} [options.throwOnBuiltin=true] - Throw if name is a built-in
120
+ * @returns {{ shadows: string|null, level: string|null }}
121
+ * shadows: the scope whose command of the same name would be shadowed (or null)
122
+ * level: 'builtin' | 'project' | 'group' | 'global' | null
123
+ */
124
+ export function detectShadow(name, options = {}) {
125
+ const { throwOnBuiltin = true } = options;
126
+ const builtins = builtinCommandNames();
127
+
128
+ // Check built-in (absolute refusal)
129
+ if (builtins.has(name)) {
130
+ if (throwOnBuiltin) {
131
+ const err = new Error(`'${name}' is a built-in Total Recall command; a custom command with that name could never run.`);
132
+ err.exitCode = EXIT_CODES.USAGE;
133
+ throw err;
134
+ }
135
+ return { shadows: name, level: 'builtin' };
136
+ }
137
+
138
+ // Check project over group/global
139
+ const targets = getCommandTargets();
140
+ for (let i = 0; i < targets.length; i++) {
141
+ const cmdPath = path.join(targets[i].dir, `${name}.mjs`);
142
+ if (fs.existsSync(cmdPath)) {
143
+ // This scope already has the command; if we're creating at a higher
144
+ // priority scope, it shadows the lower one
145
+ if (i > 0) {
146
+ return { shadows: name, level: targets[i].scope };
147
+ }
148
+ // Same scope — already exists error, not shadowing
149
+ return { shadows: null, level: null };
150
+ }
151
+ }
152
+
153
+ return { shadows: null, level: null };
154
+ }
155
+
156
+ /**
157
+ * Check whether a name would shadow and return a warning if appropriate.
158
+ * Does not throw for built-ins (use detectShadow for that).
159
+ *
160
+ * @param {string} name - Command name to check
161
+ * @returns {string|null} Warning message or null
162
+ */
163
+ export function shadowWarning(name) {
164
+ // Refuse built-in names with a clear error
165
+ const builtins = builtinCommandNames();
166
+ if (builtins.has(name)) {
167
+ return `'${name}' is a built-in Total Recall command; a custom command with that name could never run.`;
168
+ }
169
+
170
+ const targets = getCommandTargets();
171
+ let found = false;
172
+ for (const t of targets) {
173
+ const cmdPath = path.join(t.dir, `${name}.mjs`);
174
+ if (fs.existsSync(cmdPath)) {
175
+ if (!found) {
176
+ found = true; // First one wins — no warning
177
+ } else {
178
+ return `Warning: '${name}' exists in scope '${t.scope}' but will be shadowed by a higher-priority scope.`;
179
+ }
180
+ }
181
+ }
182
+ return null;
183
+ }
184
+
185
+ /**
186
+ * Resolve a verb through the full scope chain and return its path and source.
187
+ *
188
+ * @param {string} verb - Command name
189
+ * @param {object} [options]
190
+ * @param {string} [options.cwd]
191
+ * @returns {{ path: string|null, scope: string|null }}
192
+ */
193
+ export function resolveCommand(verb, options = {}) {
194
+ const cwd = options.cwd || process.cwd();
195
+
196
+ // 1. Project
197
+ const projectPath = path.join(cwd, '.agent', 'commands', `${verb}.mjs`);
198
+ if (fs.existsSync(projectPath)) {
199
+ return { path: projectPath, scope: 'project' };
200
+ }
201
+
202
+ // 2. Group (through membership)
203
+ const groupDirs = resolveGroupDirs(cwd);
204
+ for (const gd of groupDirs) {
205
+ const groupPath = path.join(gd, `${verb}.mjs`);
206
+ if (fs.existsSync(groupPath)) {
207
+ return { path: groupPath, scope: 'group' };
208
+ }
209
+ }
210
+
211
+ // 3. Global
212
+ const homeDir = process.env.HOME || process.env.USERPROFILE;
213
+ if (homeDir) {
214
+ const globalPath = path.join(homeDir, '.agent', 'commands', `${verb}.mjs`);
215
+ if (fs.existsSync(globalPath)) {
216
+ return { path: globalPath, scope: 'global' };
217
+ }
218
+ }
219
+
220
+ return { path: null, scope: null };
221
+ }
@@ -0,0 +1,126 @@
1
+ /**
2
+ * Risk Gate Module
3
+ *
4
+ * Risk classes and the dry-run / --yes gate for composable CLI commands.
5
+ *
6
+ * Risk classes:
7
+ * - read: No side effects. Runs by default.
8
+ * - write: Mutates local files only. Runs by default.
9
+ * - money: Affects financial state. Requires --yes to run.
10
+ * - dns-cert: Changes DNS records or TLS certificates. Requires --yes.
11
+ * - production-deploy: Deploys to production. Requires --yes.
12
+ * - secret-revoke: Rotates/revokes credentials. Requires --yes.
13
+ *
14
+ * Flow:
15
+ * 1. A command declares its risk class in export const meta = { risk: '...' }
16
+ * 2. tr() checks hasRiskGate(risk) before calling the handler
17
+ * 3. If gated and no --yes, calls dryRunResult() to get the plan, returns exit 3
18
+ * 4. With --yes, the handler runs as normal
19
+ * 5. Nested tr() calls inherit dryRun; a gated nested call without --yes
20
+ * fails the parent with exit 3
21
+ */
22
+ import { EXIT_CODES } from './contract.mjs';
23
+
24
+ /** Risk classes that require --yes to execute */
25
+ export const GATED_RISKS = new Set([
26
+ 'money',
27
+ 'dns-cert',
28
+ 'production-deploy',
29
+ 'secret-revoke',
30
+ ]);
31
+
32
+ /** All valid risk classes */
33
+ export const ALL_RISKS = new Set([
34
+ 'read',
35
+ 'write',
36
+ ...GATED_RISKS,
37
+ ]);
38
+
39
+ /**
40
+ * Check whether a risk class requires the --yes gate.
41
+ *
42
+ * @param {string} risk
43
+ * @returns {boolean}
44
+ */
45
+ export function hasRiskGate(risk) {
46
+ return GATED_RISKS.has(risk);
47
+ }
48
+
49
+ /**
50
+ * Validate that a risk class is recognized.
51
+ *
52
+ * @param {string} risk
53
+ * @throws {Error} If the risk class is not recognized
54
+ */
55
+ export function validateRisk(risk) {
56
+ if (!ALL_RISKS.has(risk)) {
57
+ const err = new Error(`Unknown risk class "${risk}". Valid: ${[...ALL_RISKS].join(', ')}`);
58
+ err.exitCode = EXIT_CODES.USAGE;
59
+ throw err;
60
+ }
61
+ return risk;
62
+ }
63
+
64
+ /**
65
+ * Run a handler in dry-run mode to obtain a plan.
66
+ * The handler receives { dryRun: true } in its context and should
67
+ * return a description of what it would change.
68
+ *
69
+ * @param {Function} handler - The v2 command handler
70
+ * @param {string[]} args - Arguments
71
+ * @param {object} context - Execution context { verb, json, dryRun, log }
72
+ * @returns {Promise<object>} Plan data
73
+ */
74
+ export async function dryRunResult(handler, args, context = {}) {
75
+ const { verb, json, dryRun: _, log } = context;
76
+
77
+ // Call the handler with dryRun: true
78
+ const plan = await handler(args, { ...context, dryRun: true });
79
+
80
+ const planOutput = {
81
+ risk: 'gated',
82
+ command: verb,
83
+ args: args.filter((a) => a !== '--yes'),
84
+ plan: plan?.data ?? plan ?? 'No plan returned by command',
85
+ note: 'Run with --yes to execute',
86
+ };
87
+
88
+ if (json) {
89
+ console.log(JSON.stringify({ ok: false, exit_code: EXIT_CODES.ABSTAIN, result: planOutput }));
90
+ } else {
91
+ console.log(` ${verb} would make the following changes:`);
92
+ console.log(JSON.stringify(planOutput.plan, null, 2));
93
+ console.log(`\nPass --yes to execute.`);
94
+ }
95
+
96
+ process.exitCode = EXIT_CODES.ABSTAIN;
97
+ return planOutput;
98
+ }
99
+
100
+ /**
101
+ * Create an argv parser that looks for --yes and strips it.
102
+ * Also removes --dry-run when dryRun is handled by the context.
103
+ *
104
+ * @param {string[]} args
105
+ * @param {object} [options]
106
+ * @param {boolean} [options.stripYes=true]
107
+ * @param {boolean} [options.stripDryRun=true]
108
+ * @returns {{ cleanArgs: string[], hasYes: boolean }}
109
+ */
110
+ export function parseRiskArgs(args, { stripYes = true, stripDryRun = true } = {}) {
111
+ let hasYes = false;
112
+ let cleanArgs = args;
113
+ if (stripYes) {
114
+ cleanArgs = cleanArgs.filter((a) => {
115
+ if (a === '--yes' || a === '-y') {
116
+ hasYes = true;
117
+ return false;
118
+ }
119
+ return true;
120
+ });
121
+ }
122
+ if (stripDryRun) {
123
+ cleanArgs = cleanArgs.filter((a) => a !== '--dry-run');
124
+ }
125
+ return { cleanArgs, hasYes };
126
+ }
@@ -0,0 +1,29 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import { createRequire } from 'node:module';
4
+ import { pathToFileURL } from 'node:url';
5
+ const require = createRequire(import.meta.url);
6
+ let source;
7
+ if (process.env.TR_PACKAGE_ROOT) {
8
+ const root = path.resolve(process.env.TR_PACKAGE_ROOT);
9
+ if (JSON.parse(fs.readFileSync(path.join(root, 'package.json'), 'utf8')).name !== 'total-recall-brain') throw new Error('Invalid native Total Recall host');
10
+ source = path.join(root, 'src/core/plugin-document-store.mjs');
11
+ } else source = require.resolve('total-recall-brain/src/core/plugin-document-store.mjs');
12
+ export const { pluginDocumentOperation, parseDocument, serializeDocument } = await import(pathToFileURL(source).href);
13
+ export const extension = { registry: 'composable-cli', extends: 'ssss', version: '1.0.0', document_primitives: {
14
+ cli_command: { family: 'capability', append_only: false, portability: 'tenant_private',
15
+ required_fields: ['type', 'id', 'name', 'scope', 'risk', 'version', 'code_sha256'], immutable_fields: ['id'],
16
+ patterns: { name: '^[a-z][a-z0-9-]{0,63}$', code_sha256: '^[a-f0-9]{64}$' },
17
+ enums: { scope: ['project', 'group', 'global'], risk: ['read', 'write', 'money', 'dns-cert', 'production-deploy', 'secret-revoke'] } },
18
+ } };
19
+ export function writeCommandDocument(file, metadata, body) {
20
+ const now = new Date().toISOString();
21
+ return pluginDocumentOperation({ type: 'operation', path: path.basename(file),
22
+ content: serializeDocument({ ...metadata, title: metadata.title || `Command ${metadata.name}`,
23
+ description: metadata.description || `CLI command ${metadata.name}`, timestamp: now }, body) },
24
+ { plugin: 'composable-cli', vaultRoot: path.dirname(path.resolve(file)), extension });
25
+ }
26
+ export function deleteCommandDocument(file) {
27
+ return pluginDocumentOperation({ type: 'delete', path: path.basename(file) },
28
+ { plugin: 'composable-cli', vaultRoot: path.dirname(path.resolve(file)), extension });
29
+ }
@@ -0,0 +1,18 @@
1
+ # DESIGN.md — Pipe Dream
2
+
3
+ ```yaml
4
+ tokens:
5
+ brand-name: "Pipe Dream"
6
+ wordmark: "[PIPE//DREAM]"
7
+ color-primary: "#a855f7"
8
+ color-background: "#09090b"
9
+ color-accent: "#22c55e"
10
+ badge: "CLI Pipelines"
11
+ font-mono: "'JetBrains Mono', 'Fira Code', monospace"
12
+ border-radius: "8px"
13
+ ```
14
+
15
+ ## Brand & Typographic Wordmark
16
+ - **Name**: Pipe Dream
17
+ - **Wordmark Logo**: `[PIPE//DREAM]` (custom typographic badge, no generic icons)
18
+ - **Palette**: Primary `#a855f7`, Surface `#09090b`, Accent `#22c55e`.