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.
- package/bin/total-recall.mjs +10 -6
- package/docs/reference/cli-reference.md +18 -0
- package/frontend/dist/assets/index-4tGQfTa2.js +5605 -0
- package/frontend/dist/assets/index-DtwsDyFM.css +1 -0
- package/frontend/dist/index.html +2 -2
- package/frontend/package.json +11 -0
- package/frontend/src/App.tsx +47 -122
- package/frontend/src/components/plugins/plugin-frame.ts +1 -0
- package/frontend/src/index.css +152 -86
- package/frontend/src/pages/HelpPage.tsx +13 -10
- package/frontend/src/pages/MemoryPage.tsx +28 -30
- package/frontend/src/pages/NetworkPage.tsx +3 -3
- package/frontend/src/pages/PluginsPage.tsx +6 -5
- package/frontend/src/pages/RulesPage.tsx +16 -12
- package/package.json +4 -3
- package/scaffold/.agent/skills/total-recall/plugins/code-quality/package-lock.json +3114 -36
- package/scaffold/.agent/skills/total-recall/plugins/code-quality/package.json +4 -1
- package/scaffold/.agent/skills/total-recall/plugins/composable-cli/package-lock.json +3114 -36
- package/scaffold/.agent/skills/total-recall/plugins/composable-cli/package.json +4 -1
- package/scaffold/.agent/skills/total-recall/plugins/embeddings/package-lock.json +3121 -33
- package/scaffold/.agent/skills/total-recall/plugins/embeddings/package.json +4 -1
- package/scaffold/.agent/skills/total-recall/plugins/git-sentinel/package-lock.json +3122 -34
- package/scaffold/.agent/skills/total-recall/plugins/git-sentinel/package.json +4 -1
- package/scaffold/.agent/skills/total-recall/plugins/mesh/package-lock.json +3127 -49
- package/scaffold/.agent/skills/total-recall/plugins/mesh/package.json +5 -2
- package/scaffold/.agent/skills/total-recall/plugins/mesh/src/core/mesh.mjs +1 -1
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/capability/index.mjs +136 -7
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/cli.mjs +135 -26
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/package-lock.json +3121 -33
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/package.json +5 -2
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/plugin.json +228 -9
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/skills/meta-harness/SKILL.md +24 -30
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/alerts.mjs +29 -16
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/cascade.mjs +73 -0
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/coordinator.mjs +182 -365
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/dispatch.mjs +33 -0
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/documentation.mjs +40 -14
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/executor.mjs +4 -4
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/gate-adapters/agy.mjs +187 -0
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/gate-adapters/claude.mjs +275 -0
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/gate-adapters/cline.mjs +132 -0
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/gate-adapters/codex.mjs +187 -0
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/gate-adapters/cursor.mjs +158 -0
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/gate-adapters/devin.mjs +157 -0
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/gate-adapters/dsh.mjs +183 -0
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/gate-adapters/grok.mjs +132 -0
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/gate-adapters/hermes.mjs +173 -0
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/gate-adapters/index.mjs +87 -0
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/gate-adapters/openclaw.mjs +173 -0
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/gate-adapters/pi.mjs +129 -0
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/gate.mjs +12 -0
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/json-reply.mjs +22 -0
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/learner.mjs +72 -70
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/model-catalog.mjs +195 -0
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/paths.mjs +11 -0
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/pool.mjs +36 -0
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/review.mjs +169 -0
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/router.mjs +136 -346
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/runtime.mjs +1 -1
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/ssss-store.mjs +20 -0
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/telemetry.mjs +43 -0
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/threads.mjs +156 -0
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/worker.mjs +117 -35
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/worktree.mjs +51 -0
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/ui/card.js +398 -57
- package/scaffold/.agent/skills/total-recall/plugins/openwiki/cli.mjs +304 -46
- package/scaffold/.agent/skills/total-recall/plugins/openwiki/package-lock.json +367 -41
- package/scaffold/.agent/skills/total-recall/plugins/openwiki/package.json +5 -2
- package/scaffold/.agent/skills/total-recall/plugins/openwiki/plugin.json +33 -0
- package/scaffold/.agent/skills/total-recall/plugins/openwiki/src/config.mjs +2 -2
- package/scaffold/.agent/skills/total-recall/plugins/openwiki/src/generate.mjs +159 -0
- package/scaffold/.agent/skills/total-recall/plugins/openwiki/src/graph.mjs +197 -0
- package/scaffold/.agent/skills/total-recall/plugins/openwiki/src/lint.mjs +153 -0
- package/scaffold/.agent/skills/total-recall/plugins/openwiki/src/mesh-vaults.mjs +261 -0
- package/scaffold/.agent/skills/total-recall/plugins/openwiki/src/refresh.mjs +151 -4
- package/scaffold/.agent/skills/total-recall/plugins/openwiki/src/search.mjs +114 -0
- package/scaffold/.agent/skills/total-recall/plugins/openwiki/src/token.mjs +96 -0
- package/scaffold/.agent/skills/total-recall/plugins/openwiki/ui/card.js +899 -662
- package/scaffold/.agent/skills/total-recall/plugins/openwiki/vendor/openwiki.mjs +8987 -0
- package/scaffold/.agent/skills/total-recall/plugins/openwiki/vendor/skills/mermaid-diagrams/SKILL.md +44 -0
- package/scaffold/.agent/skills/total-recall/plugins/openwiki/vendor/skills/write-connector/SKILL.md +56 -0
- package/scaffold/.agent/skills/total-recall/plugins/security/package-lock.json +3097 -81
- package/scaffold/.agent/skills/total-recall/plugins/security/package.json +4 -1
- package/src/cli/agent.mjs +12 -1
- package/src/cli/compact.mjs +108 -0
- package/src/cli/connect.mjs +56 -1
- package/src/cli/context.mjs +55 -23
- package/src/cli/doctor.mjs +5 -2
- package/src/cli/forget.mjs +2 -0
- package/src/cli/harness.mjs +62 -50
- package/src/cli/memory-entry.mjs +2 -2
- package/src/cli/preflight.mjs +152 -16
- package/src/cli/release.mjs +386 -0
- package/src/cli/remember.mjs +19 -1
- package/src/cli/rules.mjs +54 -12
- package/src/cli/startup.mjs +1 -1
- package/src/cli/status.mjs +12 -2
- package/src/core/claude-hooks.mjs +41 -3
- package/src/core/cli-commands.mjs +2 -0
- package/src/core/cli-semantic-index.mjs +3 -2
- package/src/core/conflict-detector.mjs +23 -12
- package/src/core/context-compactor.mjs +145 -0
- package/src/core/context-policy.mjs +20 -0
- package/src/core/duplicate-guard.mjs +350 -0
- package/src/core/meta-harness.mjs +31 -11
- package/src/core/preflight-gate.mjs +2 -2
- package/src/core/preflight-receipt.mjs +29 -1
- package/src/core/scaffold-plugins.mjs +7 -0
- package/src/core/stdio-flush.mjs +23 -0
- package/src/core/surface.mjs +33 -10
- package/src/server/invoke-durability.mjs +39 -0
- package/src/server/invoke-write-child.mjs +25 -0
- package/src/server/routes/invoke.mjs +7 -0
- package/src/server/routes/plugins.mjs +2 -2
- package/frontend/dist/assets/index-BIQmqDKP.css +0 -1
- package/frontend/dist/assets/index-CRzjQLu7.js +0 -5631
- package/frontend/src/components/DaemonLogsTab.tsx +0 -54
- package/frontend/src/components/ResearchAgendaTab.tsx +0 -712
- package/frontend/src/components/TaskQueueTab.tsx +0 -193
- package/frontend/src/components/VoiceInput.tsx +0 -112
- package/frontend/src/pages/ChatPage.tsx +0 -1059
- package/frontend/src/pages/CollabPage.tsx +0 -470
- package/frontend/src/pages/DesignDocsPage.tsx +0 -621
- package/frontend/src/pages/FilesPage.tsx +0 -393
- package/frontend/src/pages/HealthPage.tsx +0 -359
- package/frontend/src/pages/InboxPage.tsx +0 -83
- package/frontend/src/pages/InstructionsPage.tsx +0 -583
- package/frontend/src/pages/IntegrationsPage.tsx +0 -761
- package/frontend/src/pages/MeshPage.css +0 -58
- package/frontend/src/pages/MeshPage.tsx +0 -1219
- package/frontend/src/pages/OkfPage.tsx +0 -535
- package/frontend/src/pages/SandboxPage.tsx +0 -113
- package/frontend/src/pages/SkillsPage.tsx +0 -1994
- package/frontend/src/pages/TasksPage.tsx +0 -427
- package/frontend/src/pages/UsagePage.tsx +0 -702
- package/scaffold/.agent/skills/total-recall/plugins/cli-agents/README.md +0 -65
- package/scaffold/.agent/skills/total-recall/plugins/cli-agents/cli.mjs +0 -76
- package/scaffold/.agent/skills/total-recall/plugins/cli-agents/package.json +0 -19
- package/scaffold/.agent/skills/total-recall/plugins/cli-agents/plugin.json +0 -266
- package/scaffold/.agent/skills/total-recall/plugins/cli-agents/src/adapters/dsh.mjs +0 -446
- package/scaffold/.agent/skills/total-recall/plugins/cli-agents/src/documentation.mjs +0 -152
- package/scaffold/.agent/skills/total-recall/plugins/cli-agents/src/executor.mjs +0 -71
- package/scaffold/.agent/skills/total-recall/plugins/cli-agents/src/runtime.mjs +0 -36
- package/scaffold/.agent/skills/total-recall/plugins/cli-agents/src/setup.mjs +0 -35
- package/scaffold/.agent/skills/total-recall/plugins/cli-agents/src/worker.mjs +0 -175
- package/scaffold/.agent/skills/total-recall/plugins/cli-agents/ui/DESIGN.md +0 -32
- 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;
|