total-recall-brain 3.41.4 → 3.41.6

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 (147) hide show
  1. package/bin/total-recall.mjs +10 -6
  2. package/docs/reference/cli-reference.md +18 -0
  3. package/frontend/dist/assets/index-4tGQfTa2.js +5605 -0
  4. package/frontend/dist/assets/index-DtwsDyFM.css +1 -0
  5. package/frontend/dist/index.html +2 -2
  6. package/frontend/package.json +11 -0
  7. package/frontend/src/App.tsx +47 -122
  8. package/frontend/src/components/plugins/plugin-frame.ts +1 -0
  9. package/frontend/src/index.css +152 -86
  10. package/frontend/src/pages/HelpPage.tsx +13 -10
  11. package/frontend/src/pages/MemoryPage.tsx +28 -30
  12. package/frontend/src/pages/NetworkPage.tsx +3 -3
  13. package/frontend/src/pages/PluginsPage.tsx +6 -5
  14. package/frontend/src/pages/RulesPage.tsx +16 -12
  15. package/package.json +4 -3
  16. package/scaffold/.agent/skills/total-recall/plugins/code-quality/package-lock.json +3114 -36
  17. package/scaffold/.agent/skills/total-recall/plugins/code-quality/package.json +4 -1
  18. package/scaffold/.agent/skills/total-recall/plugins/composable-cli/package-lock.json +3114 -36
  19. package/scaffold/.agent/skills/total-recall/plugins/composable-cli/package.json +4 -1
  20. package/scaffold/.agent/skills/total-recall/plugins/embeddings/package-lock.json +3121 -33
  21. package/scaffold/.agent/skills/total-recall/plugins/embeddings/package.json +4 -1
  22. package/scaffold/.agent/skills/total-recall/plugins/git-sentinel/package-lock.json +3122 -34
  23. package/scaffold/.agent/skills/total-recall/plugins/git-sentinel/package.json +4 -1
  24. package/scaffold/.agent/skills/total-recall/plugins/mesh/package-lock.json +3127 -49
  25. package/scaffold/.agent/skills/total-recall/plugins/mesh/package.json +5 -2
  26. package/scaffold/.agent/skills/total-recall/plugins/mesh/src/core/mesh.mjs +1 -1
  27. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/capability/index.mjs +136 -7
  28. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/cli.mjs +135 -26
  29. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/package-lock.json +3121 -33
  30. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/package.json +5 -2
  31. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/plugin.json +228 -9
  32. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/skills/meta-harness/SKILL.md +24 -30
  33. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/alerts.mjs +29 -16
  34. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/cascade.mjs +73 -0
  35. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/coordinator.mjs +182 -365
  36. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/dispatch.mjs +33 -0
  37. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/documentation.mjs +40 -14
  38. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/executor.mjs +4 -4
  39. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/gate-adapters/agy.mjs +187 -0
  40. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/gate-adapters/claude.mjs +275 -0
  41. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/gate-adapters/cline.mjs +132 -0
  42. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/gate-adapters/codex.mjs +187 -0
  43. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/gate-adapters/cursor.mjs +158 -0
  44. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/gate-adapters/devin.mjs +157 -0
  45. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/gate-adapters/dsh.mjs +183 -0
  46. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/gate-adapters/grok.mjs +132 -0
  47. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/gate-adapters/hermes.mjs +173 -0
  48. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/gate-adapters/index.mjs +87 -0
  49. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/gate-adapters/openclaw.mjs +173 -0
  50. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/gate-adapters/pi.mjs +129 -0
  51. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/gate.mjs +12 -0
  52. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/json-reply.mjs +22 -0
  53. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/learner.mjs +72 -70
  54. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/model-catalog.mjs +195 -0
  55. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/paths.mjs +11 -0
  56. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/pool.mjs +36 -0
  57. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/review.mjs +169 -0
  58. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/router.mjs +136 -346
  59. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/runtime.mjs +1 -1
  60. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/ssss-store.mjs +20 -0
  61. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/telemetry.mjs +43 -0
  62. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/threads.mjs +156 -0
  63. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/worker.mjs +117 -35
  64. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/worktree.mjs +51 -0
  65. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/ui/card.js +398 -57
  66. package/scaffold/.agent/skills/total-recall/plugins/openwiki/cli.mjs +304 -46
  67. package/scaffold/.agent/skills/total-recall/plugins/openwiki/package-lock.json +367 -41
  68. package/scaffold/.agent/skills/total-recall/plugins/openwiki/package.json +5 -2
  69. package/scaffold/.agent/skills/total-recall/plugins/openwiki/plugin.json +33 -0
  70. package/scaffold/.agent/skills/total-recall/plugins/openwiki/src/config.mjs +2 -2
  71. package/scaffold/.agent/skills/total-recall/plugins/openwiki/src/generate.mjs +159 -0
  72. package/scaffold/.agent/skills/total-recall/plugins/openwiki/src/graph.mjs +197 -0
  73. package/scaffold/.agent/skills/total-recall/plugins/openwiki/src/lint.mjs +153 -0
  74. package/scaffold/.agent/skills/total-recall/plugins/openwiki/src/mesh-vaults.mjs +261 -0
  75. package/scaffold/.agent/skills/total-recall/plugins/openwiki/src/refresh.mjs +151 -4
  76. package/scaffold/.agent/skills/total-recall/plugins/openwiki/src/search.mjs +114 -0
  77. package/scaffold/.agent/skills/total-recall/plugins/openwiki/src/token.mjs +96 -0
  78. package/scaffold/.agent/skills/total-recall/plugins/openwiki/ui/card.js +899 -662
  79. package/scaffold/.agent/skills/total-recall/plugins/openwiki/vendor/openwiki.mjs +8987 -0
  80. package/scaffold/.agent/skills/total-recall/plugins/openwiki/vendor/skills/mermaid-diagrams/SKILL.md +44 -0
  81. package/scaffold/.agent/skills/total-recall/plugins/openwiki/vendor/skills/write-connector/SKILL.md +56 -0
  82. package/scaffold/.agent/skills/total-recall/plugins/security/package-lock.json +3097 -81
  83. package/scaffold/.agent/skills/total-recall/plugins/security/package.json +4 -1
  84. package/src/cli/agent.mjs +12 -1
  85. package/src/cli/compact.mjs +108 -0
  86. package/src/cli/connect.mjs +56 -1
  87. package/src/cli/context.mjs +55 -23
  88. package/src/cli/doctor.mjs +5 -2
  89. package/src/cli/forget.mjs +2 -0
  90. package/src/cli/harness.mjs +62 -50
  91. package/src/cli/memory-entry.mjs +2 -2
  92. package/src/cli/preflight.mjs +152 -16
  93. package/src/cli/release.mjs +386 -0
  94. package/src/cli/remember.mjs +19 -1
  95. package/src/cli/rules.mjs +54 -12
  96. package/src/cli/startup.mjs +1 -1
  97. package/src/cli/status.mjs +12 -2
  98. package/src/core/claude-hooks.mjs +41 -3
  99. package/src/core/cli-commands.mjs +2 -0
  100. package/src/core/cli-semantic-index.mjs +3 -2
  101. package/src/core/conflict-detector.mjs +23 -12
  102. package/src/core/context-compactor.mjs +145 -0
  103. package/src/core/context-policy.mjs +20 -0
  104. package/src/core/duplicate-guard.mjs +350 -0
  105. package/src/core/meta-harness.mjs +31 -11
  106. package/src/core/preflight-gate.mjs +2 -2
  107. package/src/core/preflight-receipt.mjs +29 -1
  108. package/src/core/scaffold-plugins.mjs +7 -0
  109. package/src/core/stdio-flush.mjs +23 -0
  110. package/src/core/surface.mjs +33 -10
  111. package/src/server/invoke-durability.mjs +39 -0
  112. package/src/server/invoke-write-child.mjs +25 -0
  113. package/src/server/routes/invoke.mjs +7 -0
  114. package/src/server/routes/plugins.mjs +2 -2
  115. package/frontend/dist/assets/index-BIQmqDKP.css +0 -1
  116. package/frontend/dist/assets/index-CRzjQLu7.js +0 -5631
  117. package/frontend/src/components/DaemonLogsTab.tsx +0 -54
  118. package/frontend/src/components/ResearchAgendaTab.tsx +0 -712
  119. package/frontend/src/components/TaskQueueTab.tsx +0 -193
  120. package/frontend/src/components/VoiceInput.tsx +0 -112
  121. package/frontend/src/pages/ChatPage.tsx +0 -1059
  122. package/frontend/src/pages/CollabPage.tsx +0 -470
  123. package/frontend/src/pages/DesignDocsPage.tsx +0 -621
  124. package/frontend/src/pages/FilesPage.tsx +0 -393
  125. package/frontend/src/pages/HealthPage.tsx +0 -359
  126. package/frontend/src/pages/InboxPage.tsx +0 -83
  127. package/frontend/src/pages/InstructionsPage.tsx +0 -583
  128. package/frontend/src/pages/IntegrationsPage.tsx +0 -761
  129. package/frontend/src/pages/MeshPage.css +0 -58
  130. package/frontend/src/pages/MeshPage.tsx +0 -1219
  131. package/frontend/src/pages/OkfPage.tsx +0 -535
  132. package/frontend/src/pages/SandboxPage.tsx +0 -113
  133. package/frontend/src/pages/SkillsPage.tsx +0 -1994
  134. package/frontend/src/pages/TasksPage.tsx +0 -427
  135. package/frontend/src/pages/UsagePage.tsx +0 -702
  136. package/scaffold/.agent/skills/total-recall/plugins/cli-agents/README.md +0 -65
  137. package/scaffold/.agent/skills/total-recall/plugins/cli-agents/cli.mjs +0 -76
  138. package/scaffold/.agent/skills/total-recall/plugins/cli-agents/package.json +0 -19
  139. package/scaffold/.agent/skills/total-recall/plugins/cli-agents/plugin.json +0 -266
  140. package/scaffold/.agent/skills/total-recall/plugins/cli-agents/src/adapters/dsh.mjs +0 -446
  141. package/scaffold/.agent/skills/total-recall/plugins/cli-agents/src/documentation.mjs +0 -152
  142. package/scaffold/.agent/skills/total-recall/plugins/cli-agents/src/executor.mjs +0 -71
  143. package/scaffold/.agent/skills/total-recall/plugins/cli-agents/src/runtime.mjs +0 -36
  144. package/scaffold/.agent/skills/total-recall/plugins/cli-agents/src/setup.mjs +0 -35
  145. package/scaffold/.agent/skills/total-recall/plugins/cli-agents/src/worker.mjs +0 -175
  146. package/scaffold/.agent/skills/total-recall/plugins/cli-agents/ui/DESIGN.md +0 -32
  147. package/scaffold/.agent/skills/total-recall/plugins/cli-agents/ui/card.js +0 -575
@@ -0,0 +1,132 @@
1
+ /**
2
+ * Cline Agent SDK & plugin hook adapter.
3
+ * Official doc: https://docs.cline.bot/sdk/plugins.md
4
+ */
5
+ import fs from 'node:fs';
6
+ import path from 'node:path';
7
+ import os from 'node:os';
8
+
9
+ const ACK_COMMAND = 'total-recall tr-preflight --ack --task "<task>" --action read,edit,test';
10
+
11
+ export const clineAdapter = {
12
+ id: 'cline',
13
+ level: 'enforced (unverified live)',
14
+ source: {
15
+ url: 'https://docs.cline.bot/sdk/plugins.md',
16
+ retrieved: '2026-11-04',
17
+ note: 'Cline SDK plugin beforeTool hooks; hookErrorMode default is ignore (fail-open)'
18
+ },
19
+ limits: [
20
+ 'Cline defaults hookErrorMode to ignore (fail-open); requires explicit strict config',
21
+ 'Returns { skip: true, reason } or { cancel: true } on denied calls'
22
+ ],
23
+
24
+ parse(input = {}) {
25
+ const event = input.event || 'beforeTool';
26
+ const toolName = input.toolName || input.tool || '';
27
+ const toolInput = input.input || input.params || input.toolInput || {};
28
+ const cwd = input.cwd || process.cwd();
29
+
30
+ let tool = 'other';
31
+ let command;
32
+ let filePath;
33
+
34
+ if (toolName === 'execute_command' || toolName === 'Bash' || toolName === 'shell') {
35
+ tool = 'shell';
36
+ command = toolInput.command || toolInput.cmd || '';
37
+ } else if (toolName === 'write_to_file' || toolName === 'replace_in_file' || toolName === 'Edit' || toolName === 'Write') {
38
+ tool = 'write';
39
+ filePath = toolInput.path || toolInput.file_path || '';
40
+ } else if (toolName === 'read_file' || toolName === 'list_files') {
41
+ tool = 'read';
42
+ filePath = toolInput.path || '';
43
+ }
44
+
45
+ return {
46
+ event,
47
+ tool,
48
+ command,
49
+ path: filePath,
50
+ cwd,
51
+ raw: input
52
+ };
53
+ },
54
+
55
+ emit(decision, event, cwd = process.cwd()) {
56
+ if (decision?.decision === 'allow') {
57
+ const payload = { skip: false, cancel: false };
58
+ return { stdout: `${JSON.stringify(payload)}\n`, exitCode: 0 };
59
+ }
60
+
61
+ const reason = decision?.reason || `TR-PREFLIGHT gate: action denied. Run: ${decision?.fix || ACK_COMMAND}`;
62
+ const payload = {
63
+ skip: true,
64
+ cancel: true,
65
+ reason,
66
+ errorMessage: reason,
67
+ fix: decision?.fix || ACK_COMMAND
68
+ };
69
+ return { stdout: `${JSON.stringify(payload)}\n`, exitCode: 2 };
70
+ },
71
+
72
+ async run({ stdin, decideWith }) {
73
+ const parsed = this.parse(stdin);
74
+ const decision = decideWith({
75
+ tool: parsed.tool,
76
+ command: parsed.command,
77
+ path: parsed.path,
78
+ cwd: parsed.cwd
79
+ });
80
+ return this.emit(decision, parsed.event, parsed.cwd);
81
+ },
82
+
83
+ install({ home = os.homedir(), dryRun = false } = {}) {
84
+ const clineDir = path.join(home, 'Documents', 'Cline', 'Hooks');
85
+ const hooksPath = path.join(clineDir, 'preflight-hook.json');
86
+ const added = [];
87
+
88
+ const hookDef = {
89
+ name: 'total-recall-preflight',
90
+ hookErrorMode: 'block',
91
+ command: 'total-recall preflight gate --harness cline',
92
+ timeout: 5
93
+ };
94
+
95
+ if (!dryRun) {
96
+ fs.mkdirSync(clineDir, { recursive: true });
97
+ fs.writeFileSync(hooksPath, `${JSON.stringify(hookDef, null, 2)}\n`);
98
+ added.push('managed Cline beforeTool hook');
99
+ }
100
+
101
+ return { settings: hooksPath, added };
102
+ },
103
+
104
+ verify({ home = os.homedir() } = {}) {
105
+ const hooksPath = path.join(home, 'Documents', 'Cline', 'Hooks', 'preflight-hook.json');
106
+ const installed = fs.existsSync(hooksPath);
107
+
108
+ const syntheticDeniedCall = {
109
+ event: 'beforeTool',
110
+ toolName: 'execute_command',
111
+ input: { command: 'rm -rf /tmp/foo' },
112
+ cwd: '/nonexistent-directory-without-receipt'
113
+ };
114
+
115
+ const out = this.emit(
116
+ { decision: 'deny', reason: 'receipt missing', fix: ACK_COMMAND },
117
+ 'beforeTool',
118
+ syntheticDeniedCall.cwd
119
+ );
120
+
121
+ const exercised = out.exitCode === 2 && (out.stdout.includes('"skip":true') || out.stdout.includes('"cancel":true'));
122
+
123
+ return {
124
+ installed,
125
+ exercised,
126
+ level: 'enforced (unverified live)',
127
+ native: out.stdout.trim()
128
+ };
129
+ }
130
+ };
131
+
132
+ export default clineAdapter;
@@ -0,0 +1,187 @@
1
+ /**
2
+ * OpenAI Codex PreToolUse hook adapter.
3
+ * Official doc: https://learn.chatgpt.com/docs/hooks
4
+ */
5
+ import fs from 'node:fs';
6
+ import path from 'node:path';
7
+ import os from 'node:os';
8
+
9
+ const ACK_COMMAND = 'total-recall tr-preflight --ack --task "<task>" --action read,edit,test';
10
+
11
+ export const codexAdapter = {
12
+ id: 'codex',
13
+ level: 'enforced',
14
+ source: {
15
+ url: 'https://learn.chatgpt.com/docs/hooks',
16
+ retrieved: '2026-11-04',
17
+ note: 'Codex CLI hooks in ~/.codex/hooks.json or inline in config.toml'
18
+ },
19
+ limits: [
20
+ 'Non-managed hooks require trust in config.toml [hooks.state]',
21
+ 'Blocks with exit 2 and decision: "block" / permissionDecision: "deny"'
22
+ ],
23
+
24
+ parse(input = {}) {
25
+ const event = input.hook_event_name || input.event || 'PreToolUse';
26
+ const toolName = input.tool_name || input.toolName || '';
27
+ const toolInput = input.tool_input || input.toolInput || {};
28
+ const cwd = input.cwd || process.cwd();
29
+
30
+ let tool = 'other';
31
+ let command;
32
+ let filePath;
33
+
34
+ if (toolName === 'exec_command' || toolName === 'Bash' || toolName === 'shell') {
35
+ tool = 'shell';
36
+ command = toolInput.cmd || toolInput.command || '';
37
+ } else if (toolName === 'apply_patch' || toolName === 'Edit' || toolName === 'Write') {
38
+ tool = 'write';
39
+ filePath = toolInput.path || toolInput.file_path || '';
40
+ } else if (toolName === 'read_file' || toolName === 'Read' || /^(Glob|Grep)$/.test(toolName)) {
41
+ tool = 'read';
42
+ filePath = toolInput.path || toolInput.file_path || '';
43
+ }
44
+
45
+ return {
46
+ event,
47
+ tool,
48
+ command,
49
+ path: filePath,
50
+ cwd,
51
+ raw: input
52
+ };
53
+ },
54
+
55
+ emit(decision, event, cwd = process.cwd()) {
56
+ if (decision?.decision === 'allow') {
57
+ return { stdout: '', exitCode: 0 };
58
+ }
59
+
60
+ const reason = decision?.reason || `TR-PREFLIGHT gate: action denied. Run: ${decision?.fix || ACK_COMMAND}`;
61
+ const payload = {
62
+ decision: 'block',
63
+ hookSpecificOutput: {
64
+ permissionDecision: 'deny',
65
+ permissionDecisionReason: reason
66
+ },
67
+ reason,
68
+ fix: decision?.fix || ACK_COMMAND
69
+ };
70
+ return { stdout: `${JSON.stringify(payload)}\n`, exitCode: 2 };
71
+ },
72
+
73
+ async run({ stdin, decideWith }) {
74
+ const parsed = this.parse(stdin);
75
+ const decision = decideWith({
76
+ tool: parsed.tool,
77
+ command: parsed.command,
78
+ path: parsed.path,
79
+ cwd: parsed.cwd
80
+ });
81
+ return this.emit(decision, parsed.event, parsed.cwd);
82
+ },
83
+
84
+ install({ home = os.homedir(), dryRun = false, remove = false, off = false } = {}) {
85
+ const isRemove = remove || off;
86
+ const codexDir = path.join(home, '.codex');
87
+ const hooksPath = path.join(codexDir, 'hooks.json');
88
+ const legacyGate = path.join(codexDir, 'hooks', 'total-recall-gate.mjs');
89
+ const added = [];
90
+ const removed = [];
91
+
92
+ let hooksConfig = { hooks: {} };
93
+ if (fs.existsSync(hooksPath)) {
94
+ try {
95
+ hooksConfig = JSON.parse(fs.readFileSync(hooksPath, 'utf8') || '{}');
96
+ } catch (err) {
97
+ throw new Error(`${hooksPath} is not valid JSON; fix it before installing hooks: ${err.message}`);
98
+ }
99
+ }
100
+ hooksConfig.hooks = hooksConfig.hooks || {};
101
+ hooksConfig.hooks.PreToolUse = hooksConfig.hooks.PreToolUse || [];
102
+
103
+ if (isRemove) {
104
+ const beforeLen = hooksConfig.hooks.PreToolUse.length;
105
+ hooksConfig.hooks.PreToolUse = hooksConfig.hooks.PreToolUse.filter(e =>
106
+ !String(e.command || '').includes('preflight') && !String(e.command || '').includes('total-recall-gate')
107
+ );
108
+ if (hooksConfig.hooks.PreToolUse.length < beforeLen) {
109
+ removed.push('managed PreToolUse gate');
110
+ }
111
+ if (!dryRun && fs.existsSync(hooksPath)) {
112
+ fs.writeFileSync(hooksPath, `${JSON.stringify(hooksConfig, null, 2)}\n`);
113
+ }
114
+ return { settings: hooksPath, added: [], removed };
115
+ }
116
+
117
+ // Remove legacy unmanaged gate entries (F-011)
118
+ const beforeLen = hooksConfig.hooks.PreToolUse.length;
119
+ hooksConfig.hooks.PreToolUse = hooksConfig.hooks.PreToolUse.filter(e => {
120
+ const cmd = String(e.command || '');
121
+ return !cmd.includes('total-recall-gate.mjs');
122
+ });
123
+ if (hooksConfig.hooks.PreToolUse.length < beforeLen) {
124
+ removed.push('legacy hand-made gate hook');
125
+ }
126
+
127
+ const gateCommand = 'total-recall preflight gate --harness codex';
128
+ const hasPreTool = hooksConfig.hooks.PreToolUse.some(e =>
129
+ String(e.command || '').includes('preflight gate --harness codex')
130
+ );
131
+
132
+ if (!hasPreTool) {
133
+ hooksConfig.hooks.PreToolUse.push({
134
+ matcher: '*',
135
+ command: gateCommand,
136
+ timeout: 10
137
+ });
138
+ added.push('managed PreToolUse gate');
139
+ }
140
+
141
+ if (!dryRun && (added.length || removed.length)) {
142
+ fs.mkdirSync(codexDir, { recursive: true });
143
+ if (fs.existsSync(hooksPath)) {
144
+ fs.copyFileSync(hooksPath, `${hooksPath}.bak-total-recall`);
145
+ }
146
+ fs.writeFileSync(hooksPath, `${JSON.stringify(hooksConfig, null, 2)}\n`);
147
+ }
148
+
149
+ return { settings: hooksPath, added, removed };
150
+ },
151
+
152
+ verify({ home = os.homedir() } = {}) {
153
+ const hooksPath = path.join(home, '.codex', 'hooks.json');
154
+ let installed = false;
155
+ if (fs.existsSync(hooksPath)) {
156
+ try {
157
+ const config = JSON.parse(fs.readFileSync(hooksPath, 'utf8') || '{}');
158
+ const preHooks = config.hooks?.PreToolUse || [];
159
+ installed = preHooks.some(e => String(e.command || '').includes('preflight gate --harness codex'));
160
+ } catch { /* unparseable */ }
161
+ }
162
+
163
+ const syntheticDeniedCall = {
164
+ hook_event_name: 'PreToolUse',
165
+ tool_name: 'exec_command',
166
+ tool_input: { cmd: 'rm -rf /tmp/foo' },
167
+ cwd: '/nonexistent-directory-without-receipt'
168
+ };
169
+
170
+ const out = this.emit(
171
+ { decision: 'deny', reason: 'receipt missing', fix: ACK_COMMAND },
172
+ 'PreToolUse',
173
+ syntheticDeniedCall.cwd
174
+ );
175
+
176
+ const exercised = out.exitCode === 2 && (out.stdout.includes('"decision":"block"') || out.stdout.includes('"permissionDecision":"deny"'));
177
+
178
+ return {
179
+ installed,
180
+ exercised,
181
+ level: 'enforced',
182
+ native: out.stdout.trim()
183
+ };
184
+ }
185
+ };
186
+
187
+ export default codexAdapter;
@@ -0,0 +1,158 @@
1
+ /**
2
+ * Cursor IDE & agent hook adapter.
3
+ * Official doc: https://cursor.com/docs/hooks
4
+ */
5
+ import fs from 'node:fs';
6
+ import path from 'node:path';
7
+ import os from 'node:os';
8
+
9
+ const ACK_COMMAND = 'total-recall tr-preflight --ack --task "<task>" --action read,edit,test';
10
+
11
+ export const cursorAdapter = {
12
+ id: 'cursor',
13
+ level: 'enforced (unverified live)',
14
+ source: {
15
+ url: 'https://cursor.com/docs/hooks',
16
+ retrieved: '2026-11-04',
17
+ note: 'Cursor hooks in ~/.cursor/hooks.json and .cursor/hooks.json'
18
+ },
19
+ limits: [
20
+ 'Cursor defaults to fail-open unless failClosed: true is set',
21
+ 'User-level hooks apply across workspaces; requires failClosed flag'
22
+ ],
23
+
24
+ parse(input = {}) {
25
+ const event = input.event || input.hookEventName || 'preToolUse';
26
+ const toolName = input.toolName || input.tool || '';
27
+ const toolInput = input.toolInput || input.input || {};
28
+ const cwd = input.cwd || input.workspaceRoot || process.cwd();
29
+
30
+ let tool = 'other';
31
+ let command;
32
+ let filePath;
33
+
34
+ if (event === 'beforeShellExecution' || toolName === 'execute' || toolName === 'terminal' || toolName === 'Bash') {
35
+ tool = 'shell';
36
+ command = input.command || toolInput.command || toolInput.cmd || '';
37
+ } else if (event === 'beforeReadFile' || toolName === 'readFile' || toolName === 'Read') {
38
+ tool = 'read';
39
+ filePath = input.file || input.path || toolInput.file || toolInput.path || '';
40
+ } else if (toolName === 'edit' || toolName === 'Write' || toolName === 'Edit') {
41
+ tool = 'write';
42
+ filePath = input.file || input.path || toolInput.file || toolInput.path || '';
43
+ }
44
+
45
+ return {
46
+ event,
47
+ tool,
48
+ command,
49
+ path: filePath,
50
+ cwd,
51
+ raw: input
52
+ };
53
+ },
54
+
55
+ emit(decision, event, cwd = process.cwd()) {
56
+ if (decision?.decision === 'allow') {
57
+ const payload = { permission: 'allow' };
58
+ return { stdout: `${JSON.stringify(payload)}\n`, exitCode: 0 };
59
+ }
60
+
61
+ const reason = decision?.reason || `TR-PREFLIGHT gate: action denied. Run: ${decision?.fix || ACK_COMMAND}`;
62
+ const payload = {
63
+ permission: 'deny',
64
+ reason,
65
+ fix: decision?.fix || ACK_COMMAND
66
+ };
67
+ return { stdout: `${JSON.stringify(payload)}\n`, exitCode: 2 };
68
+ },
69
+
70
+ async run({ stdin, decideWith }) {
71
+ const parsed = this.parse(stdin);
72
+ const decision = decideWith({
73
+ tool: parsed.tool,
74
+ command: parsed.command,
75
+ path: parsed.path,
76
+ cwd: parsed.cwd
77
+ });
78
+ return this.emit(decision, parsed.event, parsed.cwd);
79
+ },
80
+
81
+ install({ home = os.homedir(), dryRun = false } = {}) {
82
+ const cursorDir = path.join(home, '.cursor');
83
+ const hooksPath = path.join(cursorDir, 'hooks.json');
84
+ const added = [];
85
+
86
+ let config = { hooks: {}, failClosed: true };
87
+ if (fs.existsSync(hooksPath)) {
88
+ try {
89
+ config = JSON.parse(fs.readFileSync(hooksPath, 'utf8') || '{}');
90
+ } catch (err) {
91
+ throw new Error(`${hooksPath} is not valid JSON; fix it before installing hooks: ${err.message}`);
92
+ }
93
+ }
94
+ config.failClosed = true;
95
+ config.hooks = config.hooks || {};
96
+ config.hooks.preToolUse = config.hooks.preToolUse || [];
97
+
98
+ const gateCommand = 'total-recall preflight gate --harness cursor';
99
+ const hasHook = config.hooks.preToolUse.some(e =>
100
+ String(e.command || '').includes('preflight gate --harness cursor')
101
+ );
102
+
103
+ if (!hasHook) {
104
+ config.hooks.preToolUse.push({
105
+ matcher: '*',
106
+ command: gateCommand,
107
+ timeout: 10
108
+ });
109
+ added.push('managed preToolUse gate (failClosed: true)');
110
+ }
111
+
112
+ if (!dryRun && added.length) {
113
+ fs.mkdirSync(cursorDir, { recursive: true });
114
+ if (fs.existsSync(hooksPath)) {
115
+ fs.copyFileSync(hooksPath, `${hooksPath}.bak-total-recall`);
116
+ }
117
+ fs.writeFileSync(hooksPath, `${JSON.stringify(config, null, 2)}\n`);
118
+ }
119
+
120
+ return { settings: hooksPath, added };
121
+ },
122
+
123
+ verify({ home = os.homedir() } = {}) {
124
+ const hooksPath = path.join(home, '.cursor', 'hooks.json');
125
+ let installed = false;
126
+ if (fs.existsSync(hooksPath)) {
127
+ try {
128
+ const config = JSON.parse(fs.readFileSync(hooksPath, 'utf8') || '{}');
129
+ const preHooks = config.hooks?.preToolUse || [];
130
+ installed = preHooks.some(e => String(e.command || '').includes('preflight gate --harness cursor'));
131
+ } catch { /* unparseable */ }
132
+ }
133
+
134
+ const syntheticDeniedCall = {
135
+ event: 'preToolUse',
136
+ toolName: 'execute',
137
+ toolInput: { command: 'rm -rf /tmp/foo' },
138
+ cwd: '/nonexistent-directory-without-receipt'
139
+ };
140
+
141
+ const out = this.emit(
142
+ { decision: 'deny', reason: 'receipt missing', fix: ACK_COMMAND },
143
+ 'preToolUse',
144
+ syntheticDeniedCall.cwd
145
+ );
146
+
147
+ const exercised = out.exitCode === 2 && out.stdout.includes('"permission":"deny"');
148
+
149
+ return {
150
+ installed,
151
+ exercised,
152
+ level: installed ? 'enforced (unverified live)' : 'enforced (unverified live)',
153
+ native: out.stdout.trim()
154
+ };
155
+ }
156
+ };
157
+
158
+ export default cursorAdapter;
@@ -0,0 +1,157 @@
1
+ /**
2
+ * Devin CLI & Windsurf Cascade hook adapter.
3
+ * Official doc: https://docs.devin.ai/cli/extensibility/hooks/lifecycle-hooks
4
+ */
5
+ import fs from 'node:fs';
6
+ import path from 'node:path';
7
+ import os from 'node:os';
8
+
9
+ const ACK_COMMAND = 'total-recall tr-preflight --ack --task "<task>" --action read,edit,test';
10
+
11
+ export const devinAdapter = {
12
+ id: 'devin',
13
+ aliases: ['windsurf'],
14
+ level: 'enforced (unverified live)',
15
+ source: {
16
+ url: 'https://docs.devin.ai/cli/extensibility/hooks/lifecycle-hooks',
17
+ retrieved: '2026-11-04',
18
+ note: 'Devin/Windsurf hooks in ~/.config/devin/config.json or .devin/hooks.v1.json'
19
+ },
20
+ limits: [
21
+ 'Hooks do not run when Devin operates in Restricted Mode',
22
+ 'Exit 2 blocks tool execution in PreToolUse'
23
+ ],
24
+
25
+ parse(input = {}) {
26
+ const event = input.event || input.hookEventName || 'PreToolUse';
27
+ const toolName = input.tool || input.toolName || input.tool_name || '';
28
+ const toolInput = input.toolInput || input.input || {};
29
+ const cwd = input.cwd || process.cwd();
30
+
31
+ let tool = 'other';
32
+ let command;
33
+ let filePath;
34
+
35
+ if (toolName === 'exec' || toolName === 'Bash' || toolName === 'shell') {
36
+ tool = 'shell';
37
+ command = toolInput.cmd || toolInput.command || '';
38
+ } else if (toolName === 'edit' || toolName === 'write') {
39
+ tool = 'write';
40
+ filePath = toolInput.path || toolInput.file || '';
41
+ } else if (toolName === 'read') {
42
+ tool = 'read';
43
+ filePath = toolInput.path || toolInput.file || '';
44
+ }
45
+
46
+ return {
47
+ event,
48
+ tool,
49
+ command,
50
+ path: filePath,
51
+ cwd,
52
+ raw: input
53
+ };
54
+ },
55
+
56
+ emit(decision, event, cwd = process.cwd()) {
57
+ if (decision?.decision === 'allow') {
58
+ return { stdout: '', exitCode: 0 };
59
+ }
60
+
61
+ const reason = decision?.reason || `TR-PREFLIGHT gate: action denied. Run: ${decision?.fix || ACK_COMMAND}`;
62
+ const payload = {
63
+ decision: 'deny',
64
+ reason,
65
+ fix: decision?.fix || ACK_COMMAND
66
+ };
67
+ return { stdout: `${JSON.stringify(payload)}\n`, exitCode: 2 };
68
+ },
69
+
70
+ async run({ stdin, decideWith }) {
71
+ const parsed = this.parse(stdin);
72
+ const decision = decideWith({
73
+ tool: parsed.tool,
74
+ command: parsed.command,
75
+ path: parsed.path,
76
+ cwd: parsed.cwd
77
+ });
78
+ return this.emit(decision, parsed.event, parsed.cwd);
79
+ },
80
+
81
+ install({ home = os.homedir(), dryRun = false } = {}) {
82
+ const devinDir = path.join(home, '.config', 'devin');
83
+ const hooksPath = path.join(devinDir, 'hooks.json');
84
+ const added = [];
85
+
86
+ let config = { hooks: {} };
87
+ if (fs.existsSync(hooksPath)) {
88
+ try {
89
+ config = JSON.parse(fs.readFileSync(hooksPath, 'utf8') || '{}');
90
+ } catch (err) {
91
+ throw new Error(`${hooksPath} is not valid JSON; fix it before installing hooks: ${err.message}`);
92
+ }
93
+ }
94
+ config.hooks = config.hooks || {};
95
+ config.hooks.PreToolUse = config.hooks.PreToolUse || [];
96
+
97
+ const gateCommand = 'total-recall preflight gate --harness devin';
98
+ const hasHook = config.hooks.PreToolUse.some(e =>
99
+ String(e.command || '').includes('preflight gate --harness devin')
100
+ );
101
+
102
+ if (!hasHook) {
103
+ config.hooks.PreToolUse.push({
104
+ matcher: '*',
105
+ command: gateCommand,
106
+ timeout: 10
107
+ });
108
+ added.push('managed PreToolUse gate');
109
+ }
110
+
111
+ if (!dryRun && added.length) {
112
+ fs.mkdirSync(devinDir, { recursive: true });
113
+ if (fs.existsSync(hooksPath)) {
114
+ fs.copyFileSync(hooksPath, `${hooksPath}.bak-total-recall`);
115
+ }
116
+ fs.writeFileSync(hooksPath, `${JSON.stringify(config, null, 2)}\n`);
117
+ }
118
+
119
+ return { settings: hooksPath, added };
120
+ },
121
+
122
+ verify({ home = os.homedir() } = {}) {
123
+ const hooksPath = path.join(home, '.config', 'devin', 'hooks.json');
124
+ let installed = false;
125
+ if (fs.existsSync(hooksPath)) {
126
+ try {
127
+ const config = JSON.parse(fs.readFileSync(hooksPath, 'utf8') || '{}');
128
+ const preHooks = config.hooks?.PreToolUse || [];
129
+ installed = preHooks.some(e => String(e.command || '').includes('preflight gate --harness devin'));
130
+ } catch { /* unparseable */ }
131
+ }
132
+
133
+ const syntheticDeniedCall = {
134
+ event: 'PreToolUse',
135
+ tool: 'exec',
136
+ toolInput: { cmd: 'rm -rf /tmp/foo' },
137
+ cwd: '/nonexistent-directory-without-receipt'
138
+ };
139
+
140
+ const out = this.emit(
141
+ { decision: 'deny', reason: 'receipt missing', fix: ACK_COMMAND },
142
+ 'PreToolUse',
143
+ syntheticDeniedCall.cwd
144
+ );
145
+
146
+ const exercised = out.exitCode === 2 && out.stdout.includes('"decision":"deny"');
147
+
148
+ return {
149
+ installed,
150
+ exercised,
151
+ level: 'enforced (unverified live)',
152
+ native: out.stdout.trim()
153
+ };
154
+ }
155
+ };
156
+
157
+ export default devinAdapter;