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
|
@@ -14,6 +14,16 @@ export const DOC_SOURCES = [
|
|
|
14
14
|
{ id: 'muse', index: 'https://dev.meta.ai/docs/muse-code', prefix: 'https://dev.meta.ai/docs/muse-code' },
|
|
15
15
|
{ id: 'pi', repo: 'badlogic/pi-mono', branch: 'main', directories: ['packages/coding-agent/docs/'], index: 'https://raw.githubusercontent.com/badlogic/pi-mono/main/packages/coding-agent/README.md' },
|
|
16
16
|
{ id: 'hermes', repo: 'NousResearch/hermes-agent', branch: 'main', directories: ['website/docs/'], index: 'https://raw.githubusercontent.com/NousResearch/hermes-agent/main/website/docs/reference/cli-commands.md' },
|
|
17
|
+
{ id: 'devin', index: 'https://docs.devin.ai/llms.txt', prefix: 'https://docs.devin.ai/', catalog:true },
|
|
18
|
+
{ id: 'windsurf', index: 'https://docs.windsurf.com/llms.txt', prefix: 'https://docs.windsurf.com/', catalog:true },
|
|
19
|
+
{ id: 'cline', index: 'https://docs.cline.bot/llms.txt', prefix: 'https://docs.cline.bot/', catalog:true },
|
|
20
|
+
{ id: 'openclaw', index: 'https://docs.openclaw.ai/llms.txt', prefix: 'https://docs.openclaw.ai/', catalog:true },
|
|
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 },
|
|
24
|
+
{ id: 'replit', index: 'https://docs.replit.com/llms.txt', prefix: 'https://docs.replit.com/', catalog:true },
|
|
25
|
+
{ id: 'lovable', index: 'https://docs.lovable.dev/llms.txt', prefix: 'https://docs.lovable.dev/', catalog:true },
|
|
26
|
+
{ id: 'ollama', index: 'https://docs.ollama.com/llms.txt', prefix: 'https://docs.ollama.com/', catalog:true },
|
|
17
27
|
];
|
|
18
28
|
const hash = value => crypto.createHash('sha256').update(value).digest('hex');
|
|
19
29
|
const TTL = 24 * 60 * 60 * 1000;
|
|
@@ -21,7 +31,11 @@ function pageFile(directory,id,file){if(!/^[a-f0-9]{64}-[a-f0-9]{64}\.txt$/.test
|
|
|
21
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); }
|
|
22
32
|
export function documentationDirectory() {
|
|
23
33
|
const brain = process.env.TR_SECRETS_BRAIN || path.join(process.env.AGENT_DIR || path.join(os.homedir(), '.agent'), 'skills/total-recall');
|
|
24
|
-
|
|
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;
|
|
25
39
|
}
|
|
26
40
|
function metadata(directory, id) { try { return JSON.parse(fs.readFileSync(path.join(directory,id,'index.json'),'utf8')); } catch { return null; } }
|
|
27
41
|
export function documentationStatus({ directory = documentationDirectory(), agent, now = Date.now() } = {}) {
|
|
@@ -39,11 +53,11 @@ export function documentationStatus({ directory = documentationDirectory(), agen
|
|
|
39
53
|
return { ok:agents.every(row=>row.fresh), directory, agents };
|
|
40
54
|
}
|
|
41
55
|
async function download(url, fetchImpl) {
|
|
42
|
-
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','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']);
|
|
43
57
|
let target = new URL(url);
|
|
44
58
|
for(let redirects=0;redirects<5;redirects++) {
|
|
45
59
|
if(target.protocol!=='https:' || target.username || target.password || !allowed.has(target.hostname)) throw new Error(`Unapproved official documentation origin: ${target.hostname}`);
|
|
46
|
-
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':'*/*'}});
|
|
47
61
|
if(response.status>=300 && response.status<400){target=new URL(response.headers.get('location'),target);continue;}
|
|
48
62
|
// Some entries advertise Markdown exports which no longer exist while the
|
|
49
63
|
// same official HTML document remains available. Fetch that representation.
|
|
@@ -63,15 +77,16 @@ function links(text,base,prefix) {
|
|
|
63
77
|
for(const match of text.matchAll(/\]\(([^\s)]+)(?:\s+[^)]*)?\)/g)){try{values.push(new URL(match[1],base).href);}catch{}}
|
|
64
78
|
for(const match of text.matchAll(/href=["']([^"']+)["']/g)) {try{values.push(new URL(match[1],base).href);}catch{}}
|
|
65
79
|
return [...new Set(values.map(value=>{try{const url=new URL(value);url.hash='';url.search='';url.pathname=url.pathname.replace(/\/{2,}/g,'/').replace(/\/$/,'') || '/';return url.href;}catch{return '';}}))]
|
|
66
|
-
.filter(url=>url.startsWith(prefix) && !/\.(png|jpg|jpeg|svg|gif|css|js|woff2?|ico|pdf)$/i.test(url));
|
|
80
|
+
.filter(url=>url.startsWith(prefix) && !/%3[CE]|[<>"'`\s]/i.test(url) && !/\.(png|jpg|jpeg|svg|gif|css|js|woff2?|ico|pdf|json|ya?ml|xml|zip|gz|tgz|lock)$/i.test(url));
|
|
67
81
|
}
|
|
68
82
|
export async function refreshDocumentation({ directory = documentationDirectory(), agent, fetchImpl = fetch, now = () => new Date(), maxPages = 1500 } = {}) {
|
|
69
83
|
const sources=agent?DOC_SOURCES.filter(source=>source.id===agent):DOC_SOURCES;
|
|
70
84
|
if(!sources.length)throw new Error('Unknown documentation adapter');
|
|
71
85
|
const reports=[];
|
|
72
86
|
for(const source of sources) {
|
|
73
|
-
const previous=metadata(directory,source.id), pages=[], queue=[source.index], seen=new Set(), queued=new Set(queue), errors=[];
|
|
74
|
-
|
|
87
|
+
const previous=metadata(directory,source.id), pages=[], queue=[source.index], seen=new Set(), queued=new Set(queue), errors=[], discovered=new Set(), skipped=[];
|
|
88
|
+
// `found` marks a link taken from HTML navigation. The vendor's catalog, the index and the repository tree are authoritative; a link found by scraping is not.
|
|
89
|
+
const enqueue=(url,found=false)=>{if(!queued.has(url)){queued.add(url);queue.push(url);if(found)discovered.add(url);}};
|
|
75
90
|
try {
|
|
76
91
|
if(source.repo) {
|
|
77
92
|
const tree=JSON.parse(await download(`https://api.github.com/repos/${source.repo}/git/trees/${source.branch}?recursive=1`,fetchImpl));
|
|
@@ -85,25 +100,33 @@ export async function refreshDocumentation({ directory = documentationDirectory(
|
|
|
85
100
|
if(seen.size>maxPages)throw new Error('Official documentation exceeds configured page budget');
|
|
86
101
|
const downloads=await Promise.allSettled(batch.map(async url=>({url,text:await download(url,fetchImpl)})));
|
|
87
102
|
for(let index=0;index<downloads.length;index++) {
|
|
88
|
-
const result=downloads[index];
|
|
103
|
+
const result=downloads[index];
|
|
104
|
+
if(result.status==='rejected'){
|
|
105
|
+
const message=result.reason.message;
|
|
106
|
+
// A scraped link that is gone (404 or 410) is a stale or illustrative link, such as a placeholder in a code sample. It is reported as skipped and does not fail the refresh. Anything else, and anything the vendor itself lists, stays a failure.
|
|
107
|
+
if(discovered.has(batch[index]) && /HTTP (404|410)\b/.test(message)){skipped.push({url:batch[index],error:message});continue;}
|
|
108
|
+
// A page that redirects off the approved origins (for example a trust-center portal) is an external link, not documentation. It is never followed, and it is reported. The index itself redirecting away still fails the refresh.
|
|
109
|
+
if(batch[index]!==source.index && /^Unapproved official documentation origin/.test(message)){skipped.push({url:batch[index],error:message});continue;}
|
|
110
|
+
errors.push({url:batch[index],error:message});continue;
|
|
111
|
+
}
|
|
89
112
|
const {url,text}=result.value;
|
|
90
113
|
const sha256=hash(text),file=`${hash(url)}-${sha256}.txt`;
|
|
91
114
|
atomic(path.join(directory,source.id,file),text);
|
|
92
115
|
pages.push({url,file,sha256,bytes:Buffer.byteLength(text)});
|
|
93
116
|
// A published llms index is the authoritative documentation catalog.
|
|
94
117
|
// HTML navigation also contains unrelated and obsolete marketing links.
|
|
95
|
-
if(source.prefix && (!source.catalog || url.endsWith('llms.txt')))for(const link of links(text,url,source.prefix))enqueue(link);
|
|
118
|
+
if(source.prefix && (!source.catalog || url.endsWith('llms.txt')))for(const link of links(text,url,source.prefix))enqueue(link,!url.endsWith('llms.txt'));
|
|
96
119
|
}
|
|
97
120
|
}
|
|
98
121
|
const checkedAt=now().toISOString();
|
|
99
122
|
if(errors.length){
|
|
100
123
|
const error=`${errors.length} official document(s) failed: ${errors[0].error}`;
|
|
101
124
|
const preserved=previous?.complete?previous:{id:source.id,complete:false,pages};
|
|
102
|
-
atomic(path.join(directory,source.id,'index.json'),JSON.stringify({...preserved,lastAttempt:checkedAt,error,failedCount:errors.length,failures:errors},null,2));
|
|
103
|
-
reports.push({id:source.id,ok:false,error,pages:preserved.pages?.length || 0,downloadedPages:pages.length,failedCount:errors.length,failures:errors});continue;
|
|
125
|
+
atomic(path.join(directory,source.id,'index.json'),JSON.stringify({...preserved,lastAttempt:checkedAt,error,failedCount:errors.length,failures:errors,...(skipped.length?{skipped}:{})},null,2));
|
|
126
|
+
reports.push({id:source.id,ok:false,error,pages:preserved.pages?.length || 0,downloadedPages:pages.length,failedCount:errors.length,failures:errors,...(skipped.length?{skippedCount:skipped.length,skipped}:{})});continue;
|
|
104
127
|
}
|
|
105
|
-
atomic(path.join(directory,source.id,'index.json'),JSON.stringify({id:source.id,complete:true,checkedAt,lastAttempt:checkedAt,pages},null,2));
|
|
106
|
-
reports.push({id:source.id,ok:true,pages:pages.length});
|
|
128
|
+
atomic(path.join(directory,source.id,'index.json'),JSON.stringify({id:source.id,complete:true,checkedAt,lastAttempt:checkedAt,pages,...(skipped.length?{skipped}:{})},null,2));
|
|
129
|
+
reports.push({id:source.id,ok:true,pages:pages.length,...(skipped.length?{skippedCount:skipped.length,skipped}:{})});
|
|
107
130
|
} catch(error) {
|
|
108
131
|
atomic(path.join(directory,source.id,'index.json'),JSON.stringify({...previous,id:source.id,lastAttempt:now().toISOString(),error:error.message},null,2));
|
|
109
132
|
reports.push({id:source.id,ok:false,error:error.message,pages:previous?.pages?.length || 0});
|
|
@@ -142,9 +165,12 @@ export function readDocumentation({agent,url,directory=documentationDirectory()}
|
|
|
142
165
|
export function searchDocumentation({agent,query,directory=documentationDirectory()}) {
|
|
143
166
|
if(typeof query!=='string' || !query.trim())throw new Error('Documentation query required');
|
|
144
167
|
const sources=agent?DOC_SOURCES.filter(source=>source.id===agent):DOC_SOURCES;
|
|
145
|
-
const matches=[];
|
|
168
|
+
const matches=[],phrase=query.trim().toLowerCase(),terms=phrase.split(/\s+/).filter(Boolean);
|
|
146
169
|
for(const source of sources)for(const page of metadata(directory,source.id)?.pages || []) {
|
|
147
|
-
const doc=readDocumentation({agent:source.id,url:page.url,directory});const
|
|
170
|
+
const doc=readDocumentation({agent:source.id,url:page.url,directory});const text=doc.content.toLowerCase();
|
|
171
|
+
// The exact phrase wins. Otherwise a page matches when it contains every term, in any order, so a query of two words is not limited to pages where they happen to be adjacent.
|
|
172
|
+
let index=text.indexOf(phrase);
|
|
173
|
+
if(index<0 && terms.length>1 && terms.every(term=>text.includes(term)))index=Math.min(...terms.map(term=>text.indexOf(term)));
|
|
148
174
|
if(index>=0)matches.push({agent:source.id,url:page.url,excerpt:doc.content.slice(Math.max(0,index-120),index+600)});
|
|
149
175
|
if(matches.length>=30)return {matches,truncated:true};
|
|
150
176
|
}
|
|
@@ -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;
|