Refactor context-hook to use execSync for fetching context and simplify output structure; migrate from bun:sqlite to better-sqlite3 in Database and migrations; update SearchRoutes to dynamically import context generator for improved context handling.

This commit is contained in:
Alex Newman
2025-12-07 17:23:30 -05:00
parent 85f30126aa
commit 9855ccf66d
7 changed files with 125 additions and 153 deletions
+2 -2
View File
@@ -7,8 +7,8 @@
"hooks": [
{
"type": "command",
"command": "node \"${CLAUDE_PLUGIN_ROOT}/../scripts/smart-install.js\" && node ${CLAUDE_PLUGIN_ROOT}/scripts/context-hook.js",
"timeout": 300
"command": "node ${CLAUDE_PLUGIN_ROOT}/scripts/context-hook.js",
"timeout": 5
},
{
"type": "command",
+1 -6
View File
@@ -1,7 +1,2 @@
#!/usr/bin/env node
import C from"path";import{stdin as l}from"process";import y from"path";import{homedir as D}from"os";import{existsSync as R,readFileSync as E}from"fs";import{join as o,dirname as x,basename as b}from"path";import{homedir as f}from"os";import{fileURLToPath as k}from"url";function S(){return typeof __dirname<"u"?__dirname:x(k(import.meta.url))}var U=S(),c=process.env.CLAUDE_MEM_DATA_DIR||o(f(),".claude-mem"),d=process.env.CLAUDE_CONFIG_DIR||o(f(),".claude"),W=o(c,"archives"),M=o(c,"logs"),N=o(c,"trash"),H=o(c,"backups"),F=o(c,"settings.json"),G=o(c,"claude-mem.db"),K=o(c,"vector-db"),B=o(d,"settings.json"),J=o(d,"commands"),q=o(d,"CLAUDE.md");function g(){try{let t=y.join(D(),".claude-mem","settings.json");if(R(t)){let e=JSON.parse(E(t,"utf-8")),r=parseInt(e.env?.CLAUDE_MEM_WORKER_PORT,10);if(!isNaN(r))return r}}catch{}return parseInt(process.env.CLAUDE_MEM_WORKER_PORT||"37777",10)}import{appendFileSync as P}from"fs";import{homedir as T}from"os";import{join as A}from"path";var I=A(T(),".claude-mem","silent.log");function m(t,e,r=""){let s=new Date().toISOString(),p=((new Error().stack||"").split(`
`)[2]||"").match(/at\s+(?:.*\s+)?\(?([^:]+):(\d+):(\d+)\)?/),w=p?`${p[1].split("/").pop()}:${p[2]}`:"unknown",a=`[${s}] [${w}] ${t}`;if(e!==void 0)try{a+=` ${JSON.stringify(e)}`}catch(u){a+=` [stringify error: ${u}]`}a+=`
`;try{P(I,a)}catch(u){console.error("[silent-debug] Failed to write to log:",u)}return r}async function h(t,e,r){let s=r?"&colors=true":"",n=await fetch(`http://127.0.0.1:${e}/api/context/inject?project=${encodeURIComponent(t)}${s}`,{method:"GET",signal:AbortSignal.timeout(5e3)});if(!n.ok){let i=await n.text();throw new Error(`Worker error ${n.status}: ${i}`)}return n.text()}async function _(t){let e=t?.cwd??process.cwd(),r=e?C.basename(e):"unknown-project",s=g();m("[context-hook] Requesting context from worker",{project:r,workerPort:s});try{let[n,i]=await Promise.all([h(r,s,!1),h(r,s,!0)]);return m("[context-hook] Context received",{unformattedLength:n.length,formattedLength:i.length}),{unformatted:n,formatted:i}}catch(n){m("[context-hook] Worker not reachable",{error:n.message});let i=`# [${r}] recent context
Worker not available. Start with: pm2 start claude-mem-worker`;return{unformatted:i,formatted:i}}}if(l.isTTY)_(void 0).then(({formatted:t})=>{console.log(t),process.exit(0)});else{let t="";l.on("data",e=>t+=e),l.on("end",async()=>{let e=t.trim()?JSON.parse(t):void 0,{unformatted:r,formatted:s}=await _(e);process.stderr.write(s+`
`),console.log(JSON.stringify({hookSpecificOutput:{hookEventName:"SessionStart",additionalContext:r}})),process.exit(0)})}export{_ as contextHook};
import x from"path";import{stdin as i}from"process";import{execSync as S}from"child_process";import l from"path";import{homedir as g}from"os";import{existsSync as _,readFileSync as R}from"fs";import{join as t,dirname as u,basename as y}from"path";import{homedir as c}from"os";import{fileURLToPath as d}from"url";function f(){return typeof __dirname<"u"?__dirname:u(d(import.meta.url))}var k=f(),r=process.env.CLAUDE_MEM_DATA_DIR||t(c(),".claude-mem"),s=process.env.CLAUDE_CONFIG_DIR||t(c(),".claude"),C=t(r,"archives"),I=t(r,"logs"),v=t(r,"trash"),O=t(r,"backups"),U=t(r,"settings.json"),j=t(r,"claude-mem.db"),M=t(r,"vector-db"),b=t(s,"settings.json"),H=t(s,"commands"),L=t(s,"CLAUDE.md");function a(){try{let e=l.join(g(),".claude-mem","settings.json");if(_(e)){let o=JSON.parse(R(e,"utf-8")),n=parseInt(o.env?.CLAUDE_MEM_WORKER_PORT,10);if(!isNaN(n))return n}}catch{}return parseInt(process.env.CLAUDE_MEM_WORKER_PORT||"37777",10)}async function p(e){let o=e?.cwd??process.cwd(),n=o?x.basename(o):"unknown-project",m=`http://127.0.0.1:${a()}/api/context/inject?project=${encodeURIComponent(n)}`;return S(`curl -s "${m}"`,{encoding:"utf-8",timeout:5e3})}var D=process.argv.includes("--colors");if(i.isTTY||D)p(void 0).then(e=>{console.log(e),process.exit(0)});else{let e="";i.on("data",o=>e+=o),i.on("end",async()=>{let o=e.trim()?JSON.parse(e):void 0,n=await p(o);console.log(JSON.stringify({hookSpecificOutput:{hookEventName:"SessionStart",additionalContext:n}})),process.exit(0)})}
File diff suppressed because one or more lines are too long
+25 -71
View File
@@ -6,10 +6,10 @@
* native module dependencies.
*/
import path from 'path';
import { stdin } from 'process';
import { getWorkerPort } from '../shared/worker-utils.js';
import { silentDebug } from '../utils/silent-debug.js';
import path from "path";
import { stdin } from "process";
import { execSync } from "child_process";
import { getWorkerPort } from "../shared/worker-utils.js";
export interface SessionStartInput {
session_id?: string;
@@ -20,84 +20,38 @@ export interface SessionStartInput {
[key: string]: any;
}
/**
* Fetch context from worker
*/
async function fetchContext(project: string, port: number, useFormatting: boolean): Promise<string> {
const formatParam = useFormatting ? '&colors=true' : '';
const response = await fetch(
`http://127.0.0.1:${port}/api/context/inject?project=${encodeURIComponent(project)}${formatParam}`,
{ method: 'GET', signal: AbortSignal.timeout(5000) }
);
if (!response.ok) {
const errorText = await response.text();
throw new Error(`Worker error ${response.status}: ${errorText}`);
}
return response.text();
}
/**
* Context Hook Main Logic - Fire-and-forget HTTP client
* Returns { unformatted, formatted } for dual output (stderr for user, stdout for model)
*/
async function contextHook(input?: SessionStartInput): Promise<{ unformatted: string; formatted: string }> {
async function contextHook(input?: SessionStartInput): Promise<string> {
const cwd = input?.cwd ?? process.cwd();
const project = cwd ? path.basename(cwd) : 'unknown-project';
const project = cwd ? path.basename(cwd) : "unknown-project";
const port = getWorkerPort();
silentDebug('[context-hook] Requesting context from worker', {
project,
workerPort: port
});
try {
// Fetch both versions in parallel
const [unformatted, formatted] = await Promise.all([
fetchContext(project, port, false),
fetchContext(project, port, true)
]);
silentDebug('[context-hook] Context received', { unformattedLength: unformatted.length, formattedLength: formatted.length });
return { unformatted, formatted };
} catch (error: any) {
// Worker might not be running
silentDebug('[context-hook] Worker not reachable', { error: error.message });
const fallback = `# [${project}] recent context\n\nWorker not available. Start with: pm2 start claude-mem-worker`;
return { unformatted: fallback, formatted: fallback };
}
const url = `http://127.0.0.1:${port}/api/context/inject?project=${encodeURIComponent(project)}`;
const result = execSync(`curl -s "${url}"`, { encoding: "utf-8", timeout: 5000 });
return result;
}
// Export for use by worker service (compatibility)
export { contextHook };
// Entry Point - handle stdin/stdout
const forceColors = process.argv.includes('--colors');
const forceColors = process.argv.includes("--colors");
if (stdin.isTTY || forceColors) {
// Running manually from terminal - show formatted output
contextHook(undefined).then(({ formatted }) => {
console.log(formatted);
contextHook(undefined).then((text) => {
console.log(text);
process.exit(0);
});
} else {
// Running from hook - output only JSON to stdout (no stderr)
let input = '';
stdin.on('data', (chunk) => input += chunk);
stdin.on('end', async () => {
let input = "";
stdin.on("data", (chunk) => (input += chunk));
stdin.on("end", async () => {
const parsed = input.trim() ? JSON.parse(input) : undefined;
const { unformatted } = await contextHook(parsed);
const text = await contextHook(parsed);
// Output JSON result to stdout - no stderr output
const result = {
hookSpecificOutput: {
hookEventName: "SessionStart",
additionalContext: unformatted
}
};
console.log(JSON.stringify(result));
console.log(
JSON.stringify({
hookSpecificOutput: {
hookEventName: "SessionStart",
additionalContext: text,
},
})
);
process.exit(0);
});
}
}
+1 -4
View File
@@ -1,9 +1,6 @@
import { Database as BunDatabase } from 'bun:sqlite';
import { Database } from 'better-sqlite3';
import { DATA_DIR, DB_PATH, ensureDir } from '../../shared/paths.js';
// Type alias for better-sqlite3 compatibility
type Database = BunDatabase;
export interface Migration {
version: number;
up: (db: Database) => void;
+1 -1
View File
@@ -1,4 +1,4 @@
import { Database } from 'bun:sqlite';
import { Database } from 'better-sqlite3';
import { Migration } from './Database.js';
/**
@@ -6,9 +6,7 @@
*/
import express, { Request, Response } from 'express';
import path from 'path';
import { Client } from '@modelcontextprotocol/sdk/client/index.js';
import { getPackageRoot } from '../../../../shared/paths.js';
import { logger } from '../../../../utils/logger.js';
export class SearchRoutes {
@@ -270,23 +268,21 @@ export class SearchRoutes {
*/
private async handleContextPreview(req: Request, res: Response): Promise<void> {
try {
// Dynamic import to use BUILT context-hook function
const packageRoot = getPackageRoot();
const contextHookPath = path.join(packageRoot, 'plugin', 'scripts', 'context-hook.js');
const { contextHook } = await import(contextHookPath);
// Get project from query parameter
const projectName = req.query.project as string;
if (!projectName) {
return res.status(400).json({ error: 'Project parameter is required' });
res.status(400).json({ error: 'Project parameter is required' });
return;
}
// Use project name as CWD (contextHook uses path.basename to get project)
// Import context generator (runs in worker, has access to database)
const { generateContext } = await import('../../../context-generator.js');
// Use project name as CWD (generateContext uses path.basename to get project)
const cwd = `/preview/${projectName}`;
// Generate preview context (with colors for terminal display)
const contextText = await contextHook(
// Generate context with colors for terminal display
const contextText = await generateContext(
{
session_id: 'preview-' + Date.now(),
cwd: cwd