total-recall-brain 3.41.5 → 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 +7 -4
- 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 +3 -2
- package/scaffold/.agent/skills/total-recall/plugins/code-quality/package-lock.json +8 -8
- package/scaffold/.agent/skills/total-recall/plugins/code-quality/package.json +3 -0
- package/scaffold/.agent/skills/total-recall/plugins/composable-cli/package-lock.json +8 -8
- package/scaffold/.agent/skills/total-recall/plugins/composable-cli/package.json +3 -0
- package/scaffold/.agent/skills/total-recall/plugins/embeddings/package-lock.json +8 -8
- package/scaffold/.agent/skills/total-recall/plugins/embeddings/package.json +3 -0
- package/scaffold/.agent/skills/total-recall/plugins/git-sentinel/package-lock.json +8 -8
- package/scaffold/.agent/skills/total-recall/plugins/git-sentinel/package.json +3 -0
- package/scaffold/.agent/skills/total-recall/plugins/mesh/package-lock.json +8 -8
- package/scaffold/.agent/skills/total-recall/plugins/mesh/package.json +4 -1
- 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 +8 -8
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/package.json +4 -1
- 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 +9 -3
- 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 +10 -0
- package/scaffold/.agent/skills/total-recall/plugins/openwiki/package-lock.json +3 -3
- package/scaffold/.agent/skills/total-recall/plugins/openwiki/package.json +3 -0
- package/scaffold/.agent/skills/total-recall/plugins/openwiki/plugin.json +42 -190
- package/scaffold/.agent/skills/total-recall/plugins/openwiki/vendor/openwiki.mjs +1 -1
- 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 +8 -8
- package/scaffold/.agent/skills/total-recall/plugins/security/package.json +3 -0
- package/src/cli/agent.mjs +12 -1
- package/src/cli/compact.mjs +2 -2
- package/src/cli/connect.mjs +56 -1
- package/src/cli/context.mjs +10 -7
- package/src/cli/doctor.mjs +5 -2
- 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/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-semantic-index.mjs +3 -2
- package/src/core/conflict-detector.mjs +23 -12
- package/src/core/context-compactor.mjs +2 -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 +28 -0
- 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/routes/plugins.mjs +2 -2
- package/frontend/dist/assets/index-BIQmqDKP.css +0 -1
- package/frontend/dist/assets/index-WT3gs-x6.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
|
@@ -19,6 +19,8 @@ export const DOC_SOURCES = [
|
|
|
19
19
|
{ id: 'cline', index: 'https://docs.cline.bot/llms.txt', prefix: 'https://docs.cline.bot/', catalog:true },
|
|
20
20
|
{ id: 'openclaw', index: 'https://docs.openclaw.ai/llms.txt', prefix: 'https://docs.openclaw.ai/', catalog:true },
|
|
21
21
|
{ id: 'aider', index: 'https://aider.chat/docs/', prefix: 'https://aider.chat/docs/' },
|
|
22
|
+
{ id: 'goose', index: 'https://block.github.io/goose/docs/quickstart', prefix: 'https://block.github.io/goose/docs/' },
|
|
23
|
+
{ id: 'openhands', index: 'https://docs.openhands.dev/llms.txt', prefix: 'https://docs.openhands.dev/', catalog:true },
|
|
22
24
|
{ id: 'replit', index: 'https://docs.replit.com/llms.txt', prefix: 'https://docs.replit.com/', catalog:true },
|
|
23
25
|
{ id: 'lovable', index: 'https://docs.lovable.dev/llms.txt', prefix: 'https://docs.lovable.dev/', catalog:true },
|
|
24
26
|
{ id: 'ollama', index: 'https://docs.ollama.com/llms.txt', prefix: 'https://docs.ollama.com/', catalog:true },
|
|
@@ -29,7 +31,11 @@ function pageFile(directory,id,file){if(!/^[a-f0-9]{64}-[a-f0-9]{64}\.txt$/.test
|
|
|
29
31
|
function atomic(file, value) { fs.mkdirSync(path.dirname(file), { recursive: true }); const temp = `${file}.${process.pid}.tmp`; fs.writeFileSync(temp, value, { mode: 0o600 }); fs.renameSync(temp, file); }
|
|
30
32
|
export function documentationDirectory() {
|
|
31
33
|
const brain = process.env.TR_SECRETS_BRAIN || path.join(process.env.AGENT_DIR || path.join(os.homedir(), '.agent'), 'skills/total-recall');
|
|
32
|
-
|
|
34
|
+
const directory = path.join(brain, 'memory-derived/meta-harness/docs');
|
|
35
|
+
// Keep the downloaded pages when the old directory name is still on disk.
|
|
36
|
+
const legacy = path.join(brain, 'memory-derived/cli-agents/docs');
|
|
37
|
+
if (!fs.existsSync(directory) && fs.existsSync(legacy)) { fs.mkdirSync(path.dirname(directory), { recursive: true }); fs.renameSync(legacy, directory); }
|
|
38
|
+
return directory;
|
|
33
39
|
}
|
|
34
40
|
function metadata(directory, id) { try { return JSON.parse(fs.readFileSync(path.join(directory,id,'index.json'),'utf8')); } catch { return null; } }
|
|
35
41
|
export function documentationStatus({ directory = documentationDirectory(), agent, now = Date.now() } = {}) {
|
|
@@ -47,11 +53,11 @@ export function documentationStatus({ directory = documentationDirectory(), agen
|
|
|
47
53
|
return { ok:agents.every(row=>row.fresh), directory, agents };
|
|
48
54
|
}
|
|
49
55
|
async function download(url, fetchImpl) {
|
|
50
|
-
const allowed = new Set(['code.claude.com','claude.com','platform.claude.com','docs.anthropic.com','anthropic.com','developers.openai.com','openai.com','help.openai.com','learn.chatgpt.com','chatgpt.com','antigravity.google','cursor.com','docs.x.ai','dev.meta.ai','docs.devin.ai','docs.windsurf.com','windsurf.com','docs.cline.bot','docs.openclaw.ai','aider.chat','docs.replit.com','docs.lovable.dev','docs.ollama.com','raw.githubusercontent.com','api.github.com']);
|
|
56
|
+
const allowed = new Set(['code.claude.com','claude.com','platform.claude.com','docs.anthropic.com','anthropic.com','developers.openai.com','openai.com','help.openai.com','learn.chatgpt.com','chatgpt.com','antigravity.google','cursor.com','docs.x.ai','dev.meta.ai','docs.devin.ai','docs.windsurf.com','windsurf.com','docs.cline.bot','docs.openclaw.ai','aider.chat','docs.replit.com','docs.lovable.dev','docs.ollama.com','block.github.io','docs.openhands.dev','raw.githubusercontent.com','api.github.com']);
|
|
51
57
|
let target = new URL(url);
|
|
52
58
|
for(let redirects=0;redirects<5;redirects++) {
|
|
53
59
|
if(target.protocol!=='https:' || target.username || target.password || !allowed.has(target.hostname)) throw new Error(`Unapproved official documentation origin: ${target.hostname}`);
|
|
54
|
-
const response=await fetchImpl(target,{redirect:'manual',signal:AbortSignal.timeout(20000),headers:{'User-Agent':'Total-Recall-
|
|
60
|
+
const response=await fetchImpl(target,{redirect:'manual',signal:AbortSignal.timeout(20000),headers:{'User-Agent':'Total-Recall-Meta-Harness-Docs','Accept':'*/*'}});
|
|
55
61
|
if(response.status>=300 && response.status<400){target=new URL(response.headers.get('location'),target);continue;}
|
|
56
62
|
// Some entries advertise Markdown exports which no longer exist while the
|
|
57
63
|
// same official HTML document remains available. Fetch that representation.
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import fs from 'node:fs';
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
import { pathToFileURL } from 'node:url';
|
|
4
|
-
import { runWorker, runProcess, getActiveAdapters } from './worker.mjs';
|
|
4
|
+
import { runWorker, runProcess, getActiveAdapters, notReady } from './worker.mjs';
|
|
5
5
|
|
|
6
6
|
async function hostModule(name) {
|
|
7
7
|
const selected = process.env.TR_PACKAGE_ROOT;
|
|
@@ -36,7 +36,7 @@ export async function listTargets() {
|
|
|
36
36
|
|
|
37
37
|
export function workerProgram(request) {
|
|
38
38
|
const source = fs.readFileSync(new URL('./worker.mjs', import.meta.url), 'utf8');
|
|
39
|
-
return source + '\nrunWorker(' + JSON.stringify(request) + ').then(result=>console.log(JSON.stringify(result))).catch(
|
|
39
|
+
return source + '\nrunWorker(' + JSON.stringify(request) + ').then(result=>console.log(JSON.stringify(result))).catch(error=>{console.error(String(error?.message||"Selected agent worker failed"));process.exitCode=1;});\n';
|
|
40
40
|
}
|
|
41
41
|
|
|
42
42
|
export async function executeOnNode(node, request, { worker = runWorker, processRunner = runProcess, mesh, access } = {}) {
|
|
@@ -49,8 +49,8 @@ export async function executeOnNode(node, request, { worker = runWorker, process
|
|
|
49
49
|
const resolved = access.resolveNodeAccess(target);
|
|
50
50
|
const args = access.buildSshArgs(resolved, { command: 'export PATH="$HOME/.local/bin:/opt/homebrew/bin:/usr/local/bin:$PATH"; exec node --input-type=module', extraOptions: ['BatchMode=yes', 'ConnectTimeout=10'] });
|
|
51
51
|
if (!args) throw new Error('Selected mesh node has no verified SSH access');
|
|
52
|
-
const result = await processRunner('ssh', args, { input: workerProgram(request), timeoutMs: request.operation === 'inspect' ? 120000 :
|
|
53
|
-
if (result.reason || result.exitCode !== 0) throw
|
|
52
|
+
const result = await processRunner('ssh', args, { input: workerProgram(request), timeoutMs: request.operation === 'inspect' ? 120000 : (request.timeoutMs || 900000) + 30000 });
|
|
53
|
+
if (result.reason || result.exitCode !== 0) throw notReady(`Worker on ${node} failed: ${String(result.stderr || result.reason || 'verify node access, workspace and agent installation').trim().slice(0, 400)}`);
|
|
54
54
|
try { return JSON.parse(result.stdout); } catch { throw new Error('Selected mesh worker returned invalid data'); }
|
|
55
55
|
}
|
|
56
56
|
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Google Antigravity (agy) PreToolUse & lifecycle hook adapter.
|
|
3
|
+
* Official doc: https://antigravity.google/docs/hooks?tab=cli
|
|
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 agyAdapter = {
|
|
12
|
+
id: 'agy',
|
|
13
|
+
aliases: ['antigravity'],
|
|
14
|
+
level: 'enforced',
|
|
15
|
+
source: {
|
|
16
|
+
url: 'https://antigravity.google/docs/hooks?tab=cli',
|
|
17
|
+
retrieved: '2026-11-04',
|
|
18
|
+
note: 'Antigravity CLI hooks in ~/.gemini/config/hooks.json or .agents/hooks.json'
|
|
19
|
+
},
|
|
20
|
+
readablePaths: [
|
|
21
|
+
/\/\.gemini\/antigravity\/brain\/[^/]+\/\.system_generated\/tasks\/[^/]+\.log$/,
|
|
22
|
+
/\/\.gemini\/antigravity\/brain\/[^/]+\/\.system_generated\/steps\/[^/]+\/output\.txt$/
|
|
23
|
+
],
|
|
24
|
+
limits: [
|
|
25
|
+
'Hooks receive camelCase toolCall fields: toolCall.name and toolCall.args',
|
|
26
|
+
'PreToolUse blocks execution with decision: "deny" and exit code 2'
|
|
27
|
+
],
|
|
28
|
+
|
|
29
|
+
parse(input = {}) {
|
|
30
|
+
const event = input.event || input.hookEventName || 'PreToolUse';
|
|
31
|
+
const toolCall = input.toolCall || {};
|
|
32
|
+
const name = toolCall.name || input.toolName || input.tool_name || '';
|
|
33
|
+
const args = toolCall.args || input.toolArgs || input.tool_input || {};
|
|
34
|
+
const cwd = input.cwd || input.workspacePaths?.[0] || process.cwd();
|
|
35
|
+
|
|
36
|
+
let tool = 'other';
|
|
37
|
+
let command;
|
|
38
|
+
let filePath;
|
|
39
|
+
|
|
40
|
+
if (name === 'run_command' || name === 'Bash') {
|
|
41
|
+
tool = 'shell';
|
|
42
|
+
command = args.CommandLine || args.command || '';
|
|
43
|
+
} else if (name === 'read_file' || /^(Read|Glob|Grep)$/.test(name)) {
|
|
44
|
+
tool = 'read';
|
|
45
|
+
filePath = args.Path || args.file_path || args.path || '';
|
|
46
|
+
} else if (name === 'write_file' || name === 'edit_file' || /^(Edit|Write)$/.test(name)) {
|
|
47
|
+
tool = 'write';
|
|
48
|
+
filePath = args.Path || args.file_path || args.path || '';
|
|
49
|
+
} else if (name === 'search_web' || /^(WebFetch|WebSearch)$/.test(name)) {
|
|
50
|
+
tool = 'web';
|
|
51
|
+
} else if (/^(ask_question|show_message|manage_task)$/.test(name)) {
|
|
52
|
+
tool = 'meta';
|
|
53
|
+
} else if (/^(invoke_subagent|send_message|manage_subagents)$/.test(name)) {
|
|
54
|
+
tool = 'agent';
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
return {
|
|
58
|
+
event,
|
|
59
|
+
tool,
|
|
60
|
+
command,
|
|
61
|
+
path: filePath,
|
|
62
|
+
cwd,
|
|
63
|
+
raw: input
|
|
64
|
+
};
|
|
65
|
+
},
|
|
66
|
+
|
|
67
|
+
emit(decision, event, cwd = process.cwd()) {
|
|
68
|
+
if (decision?.decision === 'allow') {
|
|
69
|
+
const payload = { decision: 'allow' };
|
|
70
|
+
return { stdout: `${JSON.stringify(payload)}\n`, exitCode: 0 };
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const reason = decision?.reason || `TR-PREFLIGHT gate: action denied. Run: ${decision?.fix || ACK_COMMAND}`;
|
|
74
|
+
const payload = {
|
|
75
|
+
decision: 'deny',
|
|
76
|
+
reason,
|
|
77
|
+
fix: decision?.fix || ACK_COMMAND
|
|
78
|
+
};
|
|
79
|
+
return { stdout: `${JSON.stringify(payload)}\n`, exitCode: 2 };
|
|
80
|
+
},
|
|
81
|
+
|
|
82
|
+
async run({ stdin, decideWith }) {
|
|
83
|
+
const parsed = this.parse(stdin);
|
|
84
|
+
const decision = decideWith({
|
|
85
|
+
tool: parsed.tool,
|
|
86
|
+
command: parsed.command,
|
|
87
|
+
path: parsed.path,
|
|
88
|
+
cwd: parsed.cwd
|
|
89
|
+
});
|
|
90
|
+
return this.emit(decision, parsed.event, parsed.cwd);
|
|
91
|
+
},
|
|
92
|
+
|
|
93
|
+
install({ home = os.homedir(), dryRun = false, remove = false, off = false } = {}) {
|
|
94
|
+
const isRemove = remove || off;
|
|
95
|
+
const geminiDir = path.join(home, '.gemini', 'config');
|
|
96
|
+
const hooksPath = path.join(geminiDir, 'hooks.json');
|
|
97
|
+
const added = [];
|
|
98
|
+
const removed = [];
|
|
99
|
+
|
|
100
|
+
let config = { hooks: {} };
|
|
101
|
+
if (fs.existsSync(hooksPath)) {
|
|
102
|
+
try {
|
|
103
|
+
config = JSON.parse(fs.readFileSync(hooksPath, 'utf8') || '{}');
|
|
104
|
+
} catch (err) {
|
|
105
|
+
throw new Error(`${hooksPath} is not valid JSON; fix it before installing hooks: ${err.message}`);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
config.hooks = config.hooks || {};
|
|
109
|
+
config.hooks.PreToolUse = config.hooks.PreToolUse || [];
|
|
110
|
+
|
|
111
|
+
if (isRemove) {
|
|
112
|
+
const beforeLen = config.hooks.PreToolUse.length;
|
|
113
|
+
config.hooks.PreToolUse = config.hooks.PreToolUse.filter(e =>
|
|
114
|
+
!String(e.command || '').includes('preflight')
|
|
115
|
+
);
|
|
116
|
+
if (config.hooks.PreToolUse.length < beforeLen) {
|
|
117
|
+
removed.push('managed PreToolUse gate');
|
|
118
|
+
}
|
|
119
|
+
if (!dryRun && fs.existsSync(hooksPath)) {
|
|
120
|
+
fs.writeFileSync(hooksPath, `${JSON.stringify(config, null, 2)}\n`);
|
|
121
|
+
}
|
|
122
|
+
return { settings: hooksPath, added: [], removed };
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const gateCommand = 'total-recall preflight gate --harness agy';
|
|
126
|
+
const hasPreTool = config.hooks.PreToolUse.some(e =>
|
|
127
|
+
String(e.command || '').includes('preflight gate --harness agy')
|
|
128
|
+
);
|
|
129
|
+
|
|
130
|
+
if (!hasPreTool) {
|
|
131
|
+
config.hooks.PreToolUse.push({
|
|
132
|
+
matcher: '*',
|
|
133
|
+
command: gateCommand,
|
|
134
|
+
timeout: 10
|
|
135
|
+
});
|
|
136
|
+
added.push('managed PreToolUse gate');
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
if (!dryRun && added.length) {
|
|
140
|
+
fs.mkdirSync(geminiDir, { recursive: true });
|
|
141
|
+
if (fs.existsSync(hooksPath)) {
|
|
142
|
+
fs.copyFileSync(hooksPath, `${hooksPath}.bak-total-recall`);
|
|
143
|
+
}
|
|
144
|
+
fs.writeFileSync(hooksPath, `${JSON.stringify(config, null, 2)}\n`);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
return { settings: hooksPath, added };
|
|
148
|
+
},
|
|
149
|
+
|
|
150
|
+
verify({ home = os.homedir() } = {}) {
|
|
151
|
+
const hooksPath = path.join(home, '.gemini', 'config', 'hooks.json');
|
|
152
|
+
let installed = false;
|
|
153
|
+
if (fs.existsSync(hooksPath)) {
|
|
154
|
+
try {
|
|
155
|
+
const config = JSON.parse(fs.readFileSync(hooksPath, 'utf8') || '{}');
|
|
156
|
+
const preHooks = config.hooks?.PreToolUse || [];
|
|
157
|
+
installed = preHooks.some(e => String(e.command || '').includes('preflight gate --harness agy'));
|
|
158
|
+
} catch { /* unparseable */ }
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
const syntheticDeniedCall = {
|
|
162
|
+
event: 'PreToolUse',
|
|
163
|
+
toolCall: {
|
|
164
|
+
name: 'run_command',
|
|
165
|
+
args: { CommandLine: 'rm -rf /tmp/foo' }
|
|
166
|
+
},
|
|
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":"deny"');
|
|
177
|
+
|
|
178
|
+
return {
|
|
179
|
+
installed,
|
|
180
|
+
exercised,
|
|
181
|
+
level: 'enforced',
|
|
182
|
+
native: out.stdout.trim()
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
};
|
|
186
|
+
|
|
187
|
+
export default agyAdapter;
|
|
@@ -0,0 +1,275 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Claude Code PreToolUse & UserPromptSubmit adapter.
|
|
3
|
+
* Official doc: https://docs.anthropic.com/en/docs/agents-and-tools/claude-code/overview
|
|
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 claudeAdapter = {
|
|
12
|
+
id: 'claude',
|
|
13
|
+
level: 'enforced',
|
|
14
|
+
source: {
|
|
15
|
+
url: 'https://docs.anthropic.com/en/docs/agents-and-tools/claude-code/overview',
|
|
16
|
+
retrieved: '2026-11-04',
|
|
17
|
+
note: 'Claude Code hook configuration in settings.json with PreToolUse and UserPromptSubmit'
|
|
18
|
+
},
|
|
19
|
+
readablePaths: [
|
|
20
|
+
/\/\.claude\/projects\/[^/]+\/[^/]+\/tool-results\/[^/]+$/
|
|
21
|
+
],
|
|
22
|
+
limits: [
|
|
23
|
+
'UserPromptSubmit hook is advisory-only per ENFORCE-R1 and never blocks turn',
|
|
24
|
+
'PreToolUse blocks tool executions with exit 2 or permissionDecision: "deny"'
|
|
25
|
+
],
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Parse Claude Code native hook JSON from stdin into normalized shape:
|
|
29
|
+
* { event, tool, command, path, cwd }
|
|
30
|
+
*/
|
|
31
|
+
parse(input = {}) {
|
|
32
|
+
const event = input.hook_event_name || (input.tool_name ? 'PreToolUse' : 'UserPromptSubmit');
|
|
33
|
+
const toolName = input.tool_name || '';
|
|
34
|
+
const cwd = input.cwd || process.cwd();
|
|
35
|
+
const toolInput = input.tool_input || {};
|
|
36
|
+
|
|
37
|
+
let tool = 'other';
|
|
38
|
+
let command;
|
|
39
|
+
let filePath;
|
|
40
|
+
|
|
41
|
+
if (toolName === 'Bash') {
|
|
42
|
+
tool = 'shell';
|
|
43
|
+
command = toolInput.command || '';
|
|
44
|
+
} else if (/^(Read|Glob|Grep)$/.test(toolName)) {
|
|
45
|
+
tool = 'read';
|
|
46
|
+
filePath = toolInput.file_path || toolInput.path || toolInput.pattern || '';
|
|
47
|
+
} else if (/^(Edit|Write|MultiEdit|NotebookEdit)$/.test(toolName)) {
|
|
48
|
+
tool = 'write';
|
|
49
|
+
filePath = toolInput.file_path || toolInput.notebook_path || '';
|
|
50
|
+
} else if (/^(WebFetch|WebSearch)$/.test(toolName)) {
|
|
51
|
+
tool = 'web';
|
|
52
|
+
} else if (/^mcp__ccd_turn__/.test(toolName)) {
|
|
53
|
+
tool = 'ui';
|
|
54
|
+
} else if (/^mcp__/.test(toolName)) {
|
|
55
|
+
tool = 'mcp';
|
|
56
|
+
} else if (toolName === 'Agent') {
|
|
57
|
+
tool = 'agent';
|
|
58
|
+
} else if (/^(ToolSearch|Skill|AskUserQuestion)$/.test(toolName)) {
|
|
59
|
+
tool = 'meta';
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
return {
|
|
63
|
+
event,
|
|
64
|
+
tool,
|
|
65
|
+
command,
|
|
66
|
+
path: filePath,
|
|
67
|
+
cwd,
|
|
68
|
+
raw: input
|
|
69
|
+
};
|
|
70
|
+
},
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Emit Claude Code native response: { stdout, exitCode }
|
|
74
|
+
*/
|
|
75
|
+
emit(decision, event, cwd = process.cwd()) {
|
|
76
|
+
if (event === 'UserPromptSubmit') {
|
|
77
|
+
// ENFORCE-R1: Prompt hooks never block
|
|
78
|
+
if (decision?.decision === 'deny') {
|
|
79
|
+
const payload = {
|
|
80
|
+
hookSpecificOutput: {
|
|
81
|
+
hookEventName: 'UserPromptSubmit',
|
|
82
|
+
additionalContext: `TR-PREFLIGHT FAILED for ${cwd}: ${decision.reason || 'check failed'}. First read the full context capsule (never truncate it), then run: ${decision.fix || ACK_COMMAND}`
|
|
83
|
+
}
|
|
84
|
+
};
|
|
85
|
+
return { stdout: `${JSON.stringify(payload)}\n`, exitCode: 0 };
|
|
86
|
+
}
|
|
87
|
+
return { stdout: '', exitCode: 0 };
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// PreToolUse
|
|
91
|
+
if (decision?.decision === 'allow') {
|
|
92
|
+
return { stdout: '', exitCode: 0 };
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// Denied
|
|
96
|
+
const payload = {
|
|
97
|
+
hookSpecificOutput: {
|
|
98
|
+
hookEventName: 'PreToolUse',
|
|
99
|
+
permissionDecision: 'deny',
|
|
100
|
+
permissionDecisionReason: decision?.reason || `TR-PREFLIGHT gate: action denied. Run: ${decision?.fix || ACK_COMMAND}`
|
|
101
|
+
}
|
|
102
|
+
};
|
|
103
|
+
return { stdout: `${JSON.stringify(payload)}\n`, exitCode: 2 };
|
|
104
|
+
},
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Run the gate pipeline for Claude Code
|
|
108
|
+
*/
|
|
109
|
+
async run({ stdin, decideWith }) {
|
|
110
|
+
const parsed = this.parse(stdin);
|
|
111
|
+
const decision = decideWith({
|
|
112
|
+
tool: parsed.tool,
|
|
113
|
+
command: parsed.command,
|
|
114
|
+
path: parsed.path,
|
|
115
|
+
cwd: parsed.cwd
|
|
116
|
+
});
|
|
117
|
+
return this.emit(decision, parsed.event, parsed.cwd);
|
|
118
|
+
},
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Install Claude Code managed hooks in settings.json, removing the stopgap
|
|
122
|
+
*/
|
|
123
|
+
install({ home = os.homedir(), dryRun = false, remove = false, off = false } = {}) {
|
|
124
|
+
const isRemove = remove || off;
|
|
125
|
+
const claudeDir = path.join(home, '.claude');
|
|
126
|
+
const settingsPath = path.join(claudeDir, 'settings.json');
|
|
127
|
+
const stopgapPath = path.join(claudeDir, 'hooks', 'total-recall-preflight.mjs');
|
|
128
|
+
const added = [];
|
|
129
|
+
const removed = [];
|
|
130
|
+
|
|
131
|
+
let settings = {};
|
|
132
|
+
if (fs.existsSync(settingsPath)) {
|
|
133
|
+
try {
|
|
134
|
+
settings = JSON.parse(fs.readFileSync(settingsPath, 'utf8') || '{}');
|
|
135
|
+
} catch (err) {
|
|
136
|
+
throw new Error(`${settingsPath} is not valid JSON; fix it before installing hooks: ${err.message}`);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const hooks = settings.hooks || (settings.hooks = {});
|
|
141
|
+
|
|
142
|
+
if (isRemove) {
|
|
143
|
+
for (const event of ['PreToolUse', 'UserPromptSubmit', 'SessionStart', 'PostToolUse']) {
|
|
144
|
+
if (Array.isArray(hooks[event])) {
|
|
145
|
+
const beforeLen = hooks[event].length;
|
|
146
|
+
hooks[event] = hooks[event].filter(entry => {
|
|
147
|
+
const subHooks = entry.hooks || [];
|
|
148
|
+
return !subHooks.some(h => String(h.command || '').includes('preflight') || String(h.command || '').includes('total-recall-preflight'));
|
|
149
|
+
});
|
|
150
|
+
if (hooks[event].length < beforeLen) {
|
|
151
|
+
removed.push(`managed ${event} hook entry`);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
if (!dryRun && (removed.length || fs.existsSync(settingsPath))) {
|
|
156
|
+
fs.writeFileSync(settingsPath, `${JSON.stringify(settings, null, 2)}\n`);
|
|
157
|
+
}
|
|
158
|
+
return { settings: settingsPath, added: [], removed };
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// 1. Remove stopgap references if present
|
|
162
|
+
for (const event of ['PreToolUse', 'UserPromptSubmit', 'SessionStart', 'PostToolUse']) {
|
|
163
|
+
if (Array.isArray(hooks[event])) {
|
|
164
|
+
const beforeLen = hooks[event].length;
|
|
165
|
+
hooks[event] = hooks[event].filter(entry => {
|
|
166
|
+
const subHooks = entry.hooks || [];
|
|
167
|
+
return !subHooks.some(h => String(h.command || '').includes('total-recall-preflight.mjs'));
|
|
168
|
+
});
|
|
169
|
+
if (hooks[event].length < beforeLen) {
|
|
170
|
+
removed.push(`stopgap ${event} hook entry`);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// Remove stopgap file on disk if not dryRun
|
|
176
|
+
if (!dryRun && fs.existsSync(stopgapPath)) {
|
|
177
|
+
try {
|
|
178
|
+
fs.unlinkSync(stopgapPath);
|
|
179
|
+
removed.push('total-recall-preflight.mjs file');
|
|
180
|
+
} catch { /* ignore */ }
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// 2. Add managed preflight gate entries
|
|
184
|
+
const gateCommand = 'total-recall preflight gate --harness claude';
|
|
185
|
+
const remindCommand = 'total-recall preflight remind';
|
|
186
|
+
|
|
187
|
+
// PreToolUse: match all tools
|
|
188
|
+
hooks.PreToolUse = hooks.PreToolUse || [];
|
|
189
|
+
const hasPreTool = hooks.PreToolUse.some(e =>
|
|
190
|
+
(e.hooks || []).some(h => String(h.command || '').includes('preflight gate --harness claude'))
|
|
191
|
+
);
|
|
192
|
+
if (!hasPreTool) {
|
|
193
|
+
hooks.PreToolUse.push({
|
|
194
|
+
matcher: '*',
|
|
195
|
+
hooks: [{
|
|
196
|
+
type: 'command',
|
|
197
|
+
command: gateCommand,
|
|
198
|
+
timeout: 5
|
|
199
|
+
}]
|
|
200
|
+
});
|
|
201
|
+
added.push('managed PreToolUse gate');
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// UserPromptSubmit: advisory reminder
|
|
205
|
+
hooks.UserPromptSubmit = hooks.UserPromptSubmit || [];
|
|
206
|
+
const hasPrompt = hooks.UserPromptSubmit.some(e =>
|
|
207
|
+
(e.hooks || []).some(h => String(h.command || '').includes('preflight remind'))
|
|
208
|
+
);
|
|
209
|
+
if (!hasPrompt) {
|
|
210
|
+
hooks.UserPromptSubmit.push({
|
|
211
|
+
hooks: [{
|
|
212
|
+
type: 'command',
|
|
213
|
+
command: remindCommand,
|
|
214
|
+
timeout: 5
|
|
215
|
+
}]
|
|
216
|
+
});
|
|
217
|
+
added.push('managed UserPromptSubmit reminder');
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
if (!dryRun && (added.length || removed.length)) {
|
|
221
|
+
fs.mkdirSync(claudeDir, { recursive: true });
|
|
222
|
+
if (fs.existsSync(settingsPath)) {
|
|
223
|
+
fs.copyFileSync(settingsPath, `${settingsPath}.bak-total-recall`);
|
|
224
|
+
}
|
|
225
|
+
fs.writeFileSync(settingsPath, `${JSON.stringify(settings, null, 2)}\n`);
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
return { settings: settingsPath, added, removed };
|
|
229
|
+
},
|
|
230
|
+
|
|
231
|
+
/**
|
|
232
|
+
* Verify installed hook with a synthetic denied tool call
|
|
233
|
+
*/
|
|
234
|
+
verify({ home = os.homedir() } = {}) {
|
|
235
|
+
const settingsPath = path.join(home, '.claude', 'settings.json');
|
|
236
|
+
if (!fs.existsSync(settingsPath)) {
|
|
237
|
+
return { installed: false, exercised: false, reason: 'settings.json not found' };
|
|
238
|
+
}
|
|
239
|
+
let settings;
|
|
240
|
+
try {
|
|
241
|
+
settings = JSON.parse(fs.readFileSync(settingsPath, 'utf8') || '{}');
|
|
242
|
+
} catch {
|
|
243
|
+
return { installed: false, exercised: false, reason: 'settings.json is unparseable' };
|
|
244
|
+
}
|
|
245
|
+
const preHooks = settings.hooks?.PreToolUse || [];
|
|
246
|
+
const installed = preHooks.some(e =>
|
|
247
|
+
(e.hooks || []).some(h => String(h.command || '').includes('preflight gate --harness claude'))
|
|
248
|
+
);
|
|
249
|
+
|
|
250
|
+
// Exercise with synthetic denied payload
|
|
251
|
+
const syntheticDeniedCall = {
|
|
252
|
+
hook_event_name: 'PreToolUse',
|
|
253
|
+
tool_name: 'Bash',
|
|
254
|
+
tool_input: { command: 'git status' },
|
|
255
|
+
cwd: '/nonexistent-directory-without-receipt'
|
|
256
|
+
};
|
|
257
|
+
|
|
258
|
+
const out = this.emit(
|
|
259
|
+
{ decision: 'deny', reason: 'receipt missing', fix: ACK_COMMAND },
|
|
260
|
+
'PreToolUse',
|
|
261
|
+
syntheticDeniedCall.cwd
|
|
262
|
+
);
|
|
263
|
+
|
|
264
|
+
const exercised = out.exitCode === 2 && out.stdout.includes('"permissionDecision":"deny"');
|
|
265
|
+
|
|
266
|
+
return {
|
|
267
|
+
installed,
|
|
268
|
+
exercised,
|
|
269
|
+
level: 'enforced',
|
|
270
|
+
native: out.stdout.trim()
|
|
271
|
+
};
|
|
272
|
+
}
|
|
273
|
+
};
|
|
274
|
+
|
|
275
|
+
export default claudeAdapter;
|
|
@@ -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;
|