From 49e0d7f13577d6e68d24f2ed332bd62d8b841a16 Mon Sep 17 00:00:00 2001 From: Mert Koseoglu Date: Thu, 5 Mar 2026 01:34:46 +0300 Subject: [PATCH] refactor: flatten monorepo to src/ and add adapter pattern for multi-platform hooks MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Migrate from 3-package monorepo (shared, session, core) to flat src/ structure. Add HookAdapter interface with Claude Code adapter implementation for future multi-platform support (Gemini CLI, OpenCode, Copilot CLI, Cursor per #46). - Move packages/*/src/ → src/ and src/session/ - Replace @context-mode/* imports with relative paths - Add src/adapters/types.ts (HookAdapter interface, normalized event types) - Add src/adapters/claude-code/ (adapter, hooks, config modules) - Refactor CLI to use adapter pattern instead of hardcoded logic - Remove packages/, tsconfig.base.json, scripts/fix-imports.mjs - Simplify to single tsconfig.json (no project references) - All 492 tests pass Co-Authored-By: Claude Opus 4.6 --- .github/workflows/ci.yml | 2 +- hooks/sessionstart.mjs | 72 ++-- package.json | 21 +- packages/core/.gitignore | 1 - packages/core/package.json | 8 - packages/core/tsconfig.json | 6 - packages/session/.gitignore | 1 - packages/session/package.json | 13 - packages/session/src/.gitkeep | 0 packages/session/tsconfig.json | 10 - packages/shared/.gitignore | 1 - packages/shared/package.json | 10 - packages/shared/src/.gitkeep | 0 packages/shared/tsconfig.json | 9 - scripts/fix-imports.mjs | 75 ---- src/adapters/claude-code/config.ts | 400 ++++++++++++++++++ src/adapters/claude-code/hooks.ts | 108 +++++ src/adapters/claude-code/index.ts | 301 +++++++++++++ src/adapters/types.ts | 221 ++++++++++ {packages/core/src => src}/cli.ts | 307 +++----------- {packages/shared/src => src}/db-base.ts | 0 {packages/core/src => src}/executor.ts | 6 +- {packages/core/src => src}/runtime.ts | 0 {packages/core/src => src}/security.ts | 0 {packages/core/src => src}/server.ts | 0 {packages/session/src => src/session}/db.ts | 6 +- .../session/src => src/session}/extract.ts | 0 .../session/src => src/session}/snapshot.ts | 2 +- {packages/core/src => src}/store.ts | 8 +- {packages/shared/src => src}/truncate.ts | 0 {packages/shared/src => src}/types.ts | 2 +- start.mjs | 2 +- tests/benchmark.ts | 4 +- tests/cli-hook-path.test.ts | 2 +- tests/context-comparison.ts | 4 +- tests/ecosystem-benchmark.ts | 4 +- tests/executor.test.ts | 6 +- tests/extract-snippet.test.ts | 4 +- tests/fuzzy-search.test.ts | 2 +- tests/intent-search.test.ts | 2 +- tests/live-benchmark-v04.ts | 6 +- tests/live-benchmark.ts | 2 +- tests/project-dir.test.ts | 4 +- tests/search-fallback-integration.test.ts | 2 +- tests/search-wiring.test.ts | 2 +- tests/session/session-db.test.ts | 2 +- tests/session/session-extract.test.ts | 2 +- tests/session/session-pipeline.test.ts | 6 +- tests/session/session-snapshot.test.ts | 2 +- tests/store.test.ts | 2 +- tests/stream-cap.test.ts | 4 +- tests/subagent-budget.test.ts | 2 +- tests/turndown.test.ts | 2 +- tests/use-cases.ts | 4 +- tsconfig.base.json | 14 - tsconfig.json | 21 +- 56 files changed, 1194 insertions(+), 503 deletions(-) delete mode 100644 packages/core/.gitignore delete mode 100644 packages/core/package.json delete mode 100644 packages/core/tsconfig.json delete mode 100644 packages/session/.gitignore delete mode 100644 packages/session/package.json delete mode 100644 packages/session/src/.gitkeep delete mode 100644 packages/session/tsconfig.json delete mode 100644 packages/shared/.gitignore delete mode 100644 packages/shared/package.json delete mode 100644 packages/shared/src/.gitkeep delete mode 100644 packages/shared/tsconfig.json delete mode 100644 scripts/fix-imports.mjs create mode 100644 src/adapters/claude-code/config.ts create mode 100644 src/adapters/claude-code/hooks.ts create mode 100644 src/adapters/claude-code/index.ts create mode 100644 src/adapters/types.ts rename {packages/core/src => src}/cli.ts (66%) rename {packages/shared/src => src}/db-base.ts (100%) rename {packages/core/src => src}/executor.ts (98%) rename {packages/core/src => src}/runtime.ts (100%) rename {packages/core/src => src}/security.ts (100%) rename {packages/core/src => src}/server.ts (100%) rename {packages/session/src => src/session}/db.ts (98%) rename {packages/session/src => src/session}/extract.ts (100%) rename {packages/session/src => src/session}/snapshot.ts (99%) rename {packages/core/src => src}/store.ts (98%) rename {packages/shared/src => src}/truncate.ts (100%) rename {packages/shared/src => src}/types.ts (99%) delete mode 100644 tsconfig.base.json diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 44080ba2..5ebc4c38 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -55,4 +55,4 @@ jobs: run: npm test - name: Doctor (verify server starts) - run: npx tsx packages/core/src/cli.ts doctor + run: npx tsx src/cli.ts doctor diff --git a/hooks/sessionstart.mjs b/hooks/sessionstart.mjs index f56e86f6..af9ebe40 100644 --- a/hooks/sessionstart.mjs +++ b/hooks/sessionstart.mjs @@ -226,36 +226,6 @@ try { tableRows.push(`| Last request | 1 | "${promptPreview}" |`); } - if (tableRows.length > 0) { - block += `\n `; - block += `\n MANDATORY OUTPUT — Display this table as your FIRST output:`; - block += `\n ---`; - block += `\n **context-mode** — session ${isCompact ? "restored" : "loaded"}`; - block += `\n `; - block += `\n | Preserved | Count | Details |`; - block += `\n |-----------|------:|---------|`; - for (const row of tableRows) { - block += `\n ${row}`; - } - if (isCompact) { - block += `\n `; - block += `\n *Context compacted — session knowledge restored.*`; - } else { - block += `\n `; - block += `\n *Previous session loaded via --continue.*`; - } - block += `\n ---`; - block += `\n `; - } - - // ── Last user prompt — always in context (critical for compact) ── - if (lastPrompt) { - block += `\n ${lastPrompt}`; - if (isCompact) { - block += `\n Continue working on the request above. Do NOT ask the user to repeat themselves.`; - } - } - // ── Mandatory search — LLM must restore working memory from FTS5 ── const queries = []; if (fileNames.size > 0) queries.push("active files tracked"); @@ -266,13 +236,41 @@ try { if (grouped.error?.length > 0) queries.push("errors encountered"); if (!queries.length) queries.push("session resume overview"); - block += `\n `; - block += `\n BEFORE doing anything else:`; - block += `\n 1. Display the summary table to the user.`; - block += `\n 2. Restore working memory — search session events:`; - block += `\n mcp__plugin_context-mode_context-mode__search(queries: ${JSON.stringify(queries)}, source: "session-events")`; - block += `\n 3. Then continue the user's last request.`; - block += `\n `; + // ── STEP 1: Search call — must fire BEFORE any text output ── + block += `\n `; + block += `\n You MUST call this tool FIRST, before generating ANY text output:`; + block += `\n mcp__plugin_context-mode_context-mode__search(queries: ${JSON.stringify(queries)}, source: "session-events")`; + block += `\n This is a blocking requirement. Do not skip this step.`; + block += `\n `; + + // ── STEP 2: Display table — must be FIRST text the user sees ── + if (tableRows.length > 0) { + block += `\n `; + block += `\n After the search completes, your FIRST text output to the user MUST be exactly this table:`; + block += `\n \`\`\``; + block += `\n ---`; + block += `\n **context-mode** — session ${isCompact ? "restored" : "loaded"}`; + block += `\n `; + block += `\n | Preserved | Count | Details |`; + block += `\n |-----------|------:|---------|`; + for (const row of tableRows) { + block += `\n ${row}`; + } + block += `\n `; + block += `\n *${isCompact ? "Context compacted — session knowledge restored." : "Previous session loaded via --continue."}*`; + block += `\n ---`; + block += `\n \`\`\``; + block += `\n Do NOT skip this table. Do NOT paraphrase it. Copy it exactly as shown above.`; + block += `\n `; + } + + // ── STEP 3: Last user prompt + continue ── + if (lastPrompt) { + block += `\n ${lastPrompt}`; + if (isCompact) { + block += `\n After displaying the table, continue working on the request above. Do NOT ask the user to repeat themselves.`; + } + } block += `\n`; return block; diff --git a/package.json b/package.json index 6e3f8f83..de2cea31 100644 --- a/package.json +++ b/package.json @@ -1,8 +1,5 @@ { "name": "context-mode", - "workspaces": [ - "packages/*" - ], "version": "0.9.23-dev", "type": "module", "description": "Claude Code MCP plugin that saves 98% of your context window. Sandboxed code execution, FTS5 knowledge base, and intent-driven search.", @@ -31,10 +28,6 @@ "files": [ "build", "hooks", - "packages/shared/dist", - "packages/shared/package.json", - "packages/session/dist", - "packages/session/package.json", "server.bundle.mjs", "skills", ".claude-plugin", @@ -44,13 +37,13 @@ "LICENSE" ], "scripts": { - "build": "tsc -b", - "bundle": "esbuild packages/core/src/server.ts --bundle --platform=node --target=node18 --format=esm --outfile=server.bundle.mjs --external:better-sqlite3 --external:turndown --external:turndown-plugin-gfm --external:@mixmark-io/domino --minify", - "prepublishOnly": "npm run build && node scripts/fix-imports.mjs", - "dev": "npx tsx packages/core/src/server.ts", - "setup": "npx tsx packages/core/src/cli.ts setup", - "doctor": "npx tsx packages/core/src/cli.ts doctor", - "typecheck": "tsc -b --noEmit", + "build": "tsc", + "bundle": "esbuild src/server.ts --bundle --platform=node --target=node18 --format=esm --outfile=server.bundle.mjs --external:better-sqlite3 --external:turndown --external:turndown-plugin-gfm --external:@mixmark-io/domino --minify", + "prepublishOnly": "npm run build", + "dev": "npx tsx src/server.ts", + "setup": "npx tsx src/cli.ts setup", + "doctor": "npx tsx src/cli.ts doctor", + "typecheck": "tsc --noEmit", "test": "vitest run", "test:watch": "vitest", "benchmark": "npx tsx tests/benchmark.ts", diff --git a/packages/core/.gitignore b/packages/core/.gitignore deleted file mode 100644 index 849ddff3..00000000 --- a/packages/core/.gitignore +++ /dev/null @@ -1 +0,0 @@ -dist/ diff --git a/packages/core/package.json b/packages/core/package.json deleted file mode 100644 index 71a42958..00000000 --- a/packages/core/package.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "name": "@context-mode/core", - "private": true, - "type": "module", - "dependencies": { - "@context-mode/shared": "*" - } -} diff --git a/packages/core/tsconfig.json b/packages/core/tsconfig.json deleted file mode 100644 index ba36542a..00000000 --- a/packages/core/tsconfig.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "extends": "../../tsconfig.base.json", - "compilerOptions": { "outDir": "../../build", "rootDir": "src", "composite": true }, - "references": [{ "path": "../shared" }], - "include": ["src"] -} diff --git a/packages/session/.gitignore b/packages/session/.gitignore deleted file mode 100644 index 849ddff3..00000000 --- a/packages/session/.gitignore +++ /dev/null @@ -1 +0,0 @@ -dist/ diff --git a/packages/session/package.json b/packages/session/package.json deleted file mode 100644 index e4568edd..00000000 --- a/packages/session/package.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "name": "@context-mode/session", - "private": true, - "type": "module", - "exports": { - "./extract": { "types": "./src/extract.ts", "default": "./dist/extract.js" }, - "./db": { "types": "./src/db.ts", "default": "./dist/db.js" }, - "./snapshot": { "types": "./src/snapshot.ts", "default": "./dist/snapshot.js" } - }, - "dependencies": { - "@context-mode/shared": "*" - } -} diff --git a/packages/session/src/.gitkeep b/packages/session/src/.gitkeep deleted file mode 100644 index e69de29b..00000000 diff --git a/packages/session/tsconfig.json b/packages/session/tsconfig.json deleted file mode 100644 index df44431e..00000000 --- a/packages/session/tsconfig.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "extends": "../../tsconfig.base.json", - "compilerOptions": { - "outDir": "./dist", - "rootDir": "src", - "composite": true - }, - "references": [{ "path": "../shared" }], - "include": ["src"] -} diff --git a/packages/shared/.gitignore b/packages/shared/.gitignore deleted file mode 100644 index 849ddff3..00000000 --- a/packages/shared/.gitignore +++ /dev/null @@ -1 +0,0 @@ -dist/ diff --git a/packages/shared/package.json b/packages/shared/package.json deleted file mode 100644 index af12fc2c..00000000 --- a/packages/shared/package.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "name": "@context-mode/shared", - "private": true, - "type": "module", - "exports": { - "./db-base": { "types": "./src/db-base.ts", "default": "./dist/db-base.js" }, - "./truncate": { "types": "./src/truncate.ts", "default": "./dist/truncate.js" }, - "./types": { "types": "./src/types.ts", "default": "./dist/types.js" } - } -} diff --git a/packages/shared/src/.gitkeep b/packages/shared/src/.gitkeep deleted file mode 100644 index e69de29b..00000000 diff --git a/packages/shared/tsconfig.json b/packages/shared/tsconfig.json deleted file mode 100644 index a64cd291..00000000 --- a/packages/shared/tsconfig.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "extends": "../../tsconfig.base.json", - "compilerOptions": { - "outDir": "./dist", - "rootDir": "src", - "composite": true - }, - "include": ["src"] -} diff --git a/scripts/fix-imports.mjs b/scripts/fix-imports.mjs deleted file mode 100644 index 440244c0..00000000 --- a/scripts/fix-imports.mjs +++ /dev/null @@ -1,75 +0,0 @@ -#!/usr/bin/env node -/** - * Post-build import rewriter. - * - * Rewrites workspace package imports (@context-mode/shared/*) - * to relative paths in compiled output directories. - * This ensures the published npm package works without workspace symlinks. - * - * Targets: - * - build/ (core package output — imports become ../packages/shared/dist/*) - * - packages/session/dist/ (session package output — imports become ../../shared/dist/*) - */ - -import { readdirSync, readFileSync, writeFileSync } from "node:fs"; -import { join } from "node:path"; - -// ── Core build output (build/*.js, build/*.d.ts) ── -const CORE_BUILD = join(import.meta.dirname, "..", "build"); - -const CORE_REWRITES = [ - [/@context-mode\/shared\/db-base/g, "../packages/shared/dist/db-base.js"], - [/@context-mode\/shared\/truncate/g, "../packages/shared/dist/truncate.js"], - [/@context-mode\/shared\/types/g, "../packages/shared/dist/types.js"], -]; - -// ── Session dist output (packages/session/dist/*.js) ── -const SESSION_DIST = join(import.meta.dirname, "..", "packages", "session", "dist"); - -const SESSION_REWRITES = [ - [/@context-mode\/shared\/db-base/g, "../../shared/dist/db-base.js"], - [/@context-mode\/shared\/truncate/g, "../../shared/dist/truncate.js"], - [/@context-mode\/shared\/types/g, "../../shared/dist/types.js"], -]; - -function rewriteDir(dir, rewrites, label) { - let rewritten = 0; - - let files; - try { - files = readdirSync(dir); - } catch { - console.log(` ${label}: directory not found, skipping`); - return 0; - } - - for (const file of files) { - if (!file.endsWith(".js") && !file.endsWith(".d.ts")) continue; - - const filePath = join(dir, file); - let content = readFileSync(filePath, "utf-8"); - let changed = false; - - for (const [pattern, replacement] of rewrites) { - if (pattern.test(content)) { - // Reset lastIndex since we're reusing the regex - pattern.lastIndex = 0; - content = content.replace(pattern, replacement); - changed = true; - } - } - - if (changed) { - writeFileSync(filePath, content); - rewritten++; - console.log(` Rewritten: ${label}/${file}`); - } - } - - return rewritten; -} - -const coreCount = rewriteDir(CORE_BUILD, CORE_REWRITES, "build"); -const sessionCount = rewriteDir(SESSION_DIST, SESSION_REWRITES, "session"); - -console.log(`fix-imports: ${coreCount + sessionCount} files rewritten (core: ${coreCount}, session: ${sessionCount})`); diff --git a/src/adapters/claude-code/config.ts b/src/adapters/claude-code/config.ts new file mode 100644 index 00000000..98600828 --- /dev/null +++ b/src/adapters/claude-code/config.ts @@ -0,0 +1,400 @@ +/** + * adapters/claude-code/config — Claude Code configuration management. + * + * Handles reading/writing ~/.claude/settings.json, hook installation, + * plugin registration verification, and upgrade operations specific + * to the Claude Code platform. + */ + +import { + readFileSync, + writeFileSync, + copyFileSync, + accessSync, + readdirSync, + chmodSync, + constants, +} from "node:fs"; +import { resolve } from "node:path"; +import { homedir } from "node:os"; +import { ClaudeCodeAdapter } from "./index.js"; +import { + HOOK_TYPES, + HOOK_SCRIPTS, + PRE_TOOL_USE_MATCHER_PATTERN, + isContextModeHook, + buildHookCommand, + type HookType, +} from "./hooks.js"; + +// ───────────────────────────────────────────────────────── +// Settings management +// ───────────────────────────────────────────────────────── + +const adapter = new ClaudeCodeAdapter(); + +/** Read Claude Code settings from ~/.claude/settings.json. */ +export function readSettings(): Record | null { + return adapter.readSettings(); +} + +/** Write Claude Code settings to ~/.claude/settings.json. */ +export function writeSettings(settings: Record): void { + adapter.writeSettings(settings); +} + +/** Get the settings file path. */ +export function getSettingsPath(): string { + return adapter.getSettingsPath(); +} + +/** Backup settings.json before modifications. Returns backup path or null. */ +export function backupSettings(): string | null { + const settingsPath = adapter.getSettingsPath(); + try { + accessSync(settingsPath, constants.R_OK); + const backupPath = settingsPath + ".bak"; + copyFileSync(settingsPath, backupPath); + return backupPath; + } catch { + return null; + } +} + +// ───────────────────────────────────────────────────────── +// Hook configuration +// ───────────────────────────────────────────────────────── + +/** + * Configure a specific hook type in settings.json. + * Returns a description of what was done. + */ +export function configureHook( + settings: Record, + hookType: HookType, + pluginRoot: string, +): string { + const hooks = (settings.hooks ?? {}) as Record; + const command = buildHookCommand(hookType, pluginRoot); + + if (hookType === HOOK_TYPES.PRE_TOOL_USE) { + // PreToolUse uses a consolidated matcher pattern + const entry = { + matcher: PRE_TOOL_USE_MATCHER_PATTERN, + hooks: [{ type: "command", command }], + }; + + const existing = hooks.PreToolUse as + | Array> + | undefined; + + if (existing && Array.isArray(existing)) { + const idx = existing.findIndex((e) => + isContextModeHook( + e as { hooks?: Array<{ command?: string }> }, + hookType, + ), + ); + if (idx >= 0) { + existing[idx] = entry; + hooks.PreToolUse = existing; + settings.hooks = hooks; + return `Updated existing ${hookType} hook entry`; + } + existing.push(entry); + hooks.PreToolUse = existing; + settings.hooks = hooks; + return `Added ${hookType} hook entry to existing hooks`; + } + + hooks.PreToolUse = [entry]; + settings.hooks = hooks; + return `Created ${hookType} hooks section`; + } + + // All other hook types use empty matcher (match all) + const entry = { + matcher: "", + hooks: [{ type: "command", command }], + }; + + const existing = hooks[hookType] as + | Array> + | undefined; + + if (existing && Array.isArray(existing)) { + const idx = existing.findIndex((e) => + isContextModeHook( + e as { hooks?: Array<{ command?: string }> }, + hookType, + ), + ); + if (idx >= 0) { + existing[idx] = entry; + hooks[hookType] = existing; + settings.hooks = hooks; + return `Updated existing ${hookType} hook entry`; + } + existing.push(entry); + hooks[hookType] = existing; + settings.hooks = hooks; + return `Added ${hookType} hook entry to existing hooks`; + } + + hooks[hookType] = [entry]; + settings.hooks = hooks; + return `Created ${hookType} hooks section`; +} + +/** + * Configure all context-mode hooks in settings.json. + * Returns list of change descriptions. + */ +export function configureAllHooks( + settings: Record, + pluginRoot: string, +): string[] { + const hookTypes: HookType[] = [ + HOOK_TYPES.PRE_TOOL_USE, + HOOK_TYPES.POST_TOOL_USE, + HOOK_TYPES.PRE_COMPACT, + HOOK_TYPES.SESSION_START, + HOOK_TYPES.USER_PROMPT_SUBMIT, + ]; + + return hookTypes.map((type) => configureHook(settings, type, pluginRoot)); +} + +// ───────────────────────────────────────────────────────── +// Hook validation (for doctor command) +// ───────────────────────────────────────────────────────── + +export interface HookCheckResult { + hookType: HookType; + status: "pass" | "fail" | "warn"; + message: string; +} + +/** + * Validate that all required hooks are properly configured. + */ +export function validateHooks(pluginRoot: string): HookCheckResult[] { + const results: HookCheckResult[] = []; + const settings = readSettings(); + + if (!settings) { + results.push({ + hookType: HOOK_TYPES.PRE_TOOL_USE, + status: "fail", + message: "Could not read ~/.claude/settings.json", + }); + return results; + } + + const hooks = settings.hooks as Record | undefined; + + // Check PreToolUse + const preToolUse = hooks?.PreToolUse as + | Array<{ matcher?: string; hooks?: Array<{ command?: string }> }> + | undefined; + if (preToolUse && preToolUse.length > 0) { + const hasHook = preToolUse.some((entry) => + isContextModeHook(entry, HOOK_TYPES.PRE_TOOL_USE), + ); + results.push({ + hookType: HOOK_TYPES.PRE_TOOL_USE, + status: hasHook ? "pass" : "fail", + message: hasHook + ? "PreToolUse hook configured" + : "PreToolUse exists but does not point to pretooluse.mjs", + }); + } else { + results.push({ + hookType: HOOK_TYPES.PRE_TOOL_USE, + status: "fail", + message: "No PreToolUse hooks found", + }); + } + + // Check SessionStart + const sessionStart = hooks?.SessionStart as + | Array<{ matcher?: string; hooks?: Array<{ command?: string }> }> + | undefined; + if (sessionStart && sessionStart.length > 0) { + const hasHook = sessionStart.some((entry) => + isContextModeHook(entry, HOOK_TYPES.SESSION_START), + ); + results.push({ + hookType: HOOK_TYPES.SESSION_START, + status: hasHook ? "pass" : "fail", + message: hasHook + ? "SessionStart hook configured" + : "SessionStart exists but does not point to sessionstart.mjs", + }); + } else { + results.push({ + hookType: HOOK_TYPES.SESSION_START, + status: "fail", + message: "No SessionStart hooks found", + }); + } + + return results; +} + +// ───────────────────────────────────────────────────────── +// Plugin registration +// ───────────────────────────────────────────────────────── + +export interface PluginCheckResult { + status: "pass" | "warn"; + pluginKey?: string; + message: string; +} + +/** Check if context-mode is registered in enabledPlugins. */ +export function checkPluginRegistration(): PluginCheckResult { + const settings = readSettings(); + if (!settings) { + return { + status: "warn", + message: "Could not read settings.json", + }; + } + + const enabledPlugins = settings.enabledPlugins as + | Record + | undefined; + if (!enabledPlugins) { + return { + status: "warn", + message: "No enabledPlugins section found (might be using standalone MCP mode)", + }; + } + + const pluginKey = Object.keys(enabledPlugins).find((k) => + k.startsWith("context-mode"), + ); + + if (pluginKey && enabledPlugins[pluginKey]) { + return { + status: "pass", + pluginKey, + message: `Plugin enabled: ${pluginKey}`, + }; + } + + return { + status: "warn", + message: + "context-mode not in enabledPlugins (might be using standalone MCP mode)", + }; +} + +// ───────────────────────────────────────────────────────── +// Version management +// ───────────────────────────────────────────────────────── + +/** Get the installed marketplace/plugin version. */ +export function getMarketplaceVersion(): string { + // Primary: read from installed_plugins.json + try { + const ipPath = resolve( + homedir(), + ".claude", + "plugins", + "installed_plugins.json", + ); + const ipRaw = JSON.parse(readFileSync(ipPath, "utf-8")); + const plugins = ipRaw.plugins ?? {}; + for (const [key, entries] of Object.entries(plugins)) { + if (!key.toLowerCase().includes("context-mode")) continue; + const arr = entries as Array>; + if (arr.length > 0 && typeof arr[0].version === "string") { + return arr[0].version; + } + } + } catch { + /* fallback below */ + } + + // Fallback: scan common plugin cache locations + const bases = [ + resolve(homedir(), ".claude"), + resolve(homedir(), ".config", "claude"), + ]; + for (const base of bases) { + const cacheDir = resolve( + base, + "plugins", + "cache", + "claude-context-mode", + "context-mode", + ); + try { + const entries = readdirSync(cacheDir); + const versions = entries + .filter((e) => /^\d+\.\d+\.\d+/.test(e)) + .sort((a, b) => { + const pa = a.split(".").map(Number); + const pb = b.split(".").map(Number); + for (let i = 0; i < 3; i++) { + if ((pa[i] ?? 0) !== (pb[i] ?? 0)) + return (pa[i] ?? 0) - (pb[i] ?? 0); + } + return 0; + }); + if (versions.length > 0) return versions[versions.length - 1]; + } catch { + /* continue */ + } + } + return "not installed"; +} + +/** Update installed_plugins.json registry to point to given path and version. */ +export function updatePluginRegistry( + pluginRoot: string, + newVersion: string, +): void { + try { + const ipPath = resolve( + homedir(), + ".claude", + "plugins", + "installed_plugins.json", + ); + const ipRaw = JSON.parse(readFileSync(ipPath, "utf-8")); + for (const [key, entries] of Object.entries(ipRaw.plugins || {})) { + if (!key.toLowerCase().includes("context-mode")) continue; + for (const entry of entries as Array>) { + entry.installPath = pluginRoot; + entry.version = newVersion; + entry.lastUpdated = new Date().toISOString(); + } + } + writeFileSync(ipPath, JSON.stringify(ipRaw, null, 2) + "\n", "utf-8"); + } catch { + /* best effort */ + } +} + +// ───────────────────────────────────────────────────────── +// Hook script permissions +// ───────────────────────────────────────────────────────── + +/** Set executable permissions on all hook scripts. Returns paths that were set. */ +export function setHookPermissions(pluginRoot: string): string[] { + const set: string[] = []; + for (const [, scriptName] of Object.entries(HOOK_SCRIPTS)) { + const scriptPath = resolve(pluginRoot, "hooks", scriptName); + try { + accessSync(scriptPath, constants.R_OK); + chmodSync(scriptPath, 0o755); + set.push(scriptPath); + } catch { + /* skip missing scripts */ + } + } + return set; +} diff --git a/src/adapters/claude-code/hooks.ts b/src/adapters/claude-code/hooks.ts new file mode 100644 index 00000000..b6a4a8e7 --- /dev/null +++ b/src/adapters/claude-code/hooks.ts @@ -0,0 +1,108 @@ +/** + * adapters/claude-code/hooks — Claude Code hook definitions and matchers. + * + * Defines the hook types, matchers, and registration format specific to + * Claude Code's hook system. This module is used by: + * - CLI setup/upgrade commands (to configure hooks in settings.json) + * - Doctor command (to validate hook configuration) + * - hooks.json generation + * + * Claude Code hook system reference: + * - Hooks are registered in ~/.claude/settings.json under "hooks" key + * - Each hook type maps to an array of { matcher, hooks } entries + * - matcher: tool name pattern (empty = match all tools) + * - hooks: array of { type: "command", command: "..." } + * - Input: JSON on stdin + * - Output: JSON on stdout (or empty for passthrough) + */ + +// ───────────────────────────────────────────────────────── +// Hook type constants +// ───────────────────────────────────────────────────────── + +/** Claude Code hook types. */ +export const HOOK_TYPES = { + PRE_TOOL_USE: "PreToolUse", + POST_TOOL_USE: "PostToolUse", + PRE_COMPACT: "PreCompact", + SESSION_START: "SessionStart", + USER_PROMPT_SUBMIT: "UserPromptSubmit", +} as const; + +export type HookType = (typeof HOOK_TYPES)[keyof typeof HOOK_TYPES]; + +// ───────────────────────────────────────────────────────── +// PreToolUse matchers +// ───────────────────────────────────────────────────────── + +/** Tools that context-mode's PreToolUse hook intercepts. */ +export const PRE_TOOL_USE_MATCHERS = [ + "Bash", + "WebFetch", + "Read", + "Grep", + "Task", + "mcp__plugin_context-mode_context-mode__execute", + "mcp__plugin_context-mode_context-mode__execute_file", + "mcp__plugin_context-mode_context-mode__batch_execute", +] as const; + +/** + * Combined matcher pattern for settings.json (pipe-separated). + * Used by the upgrade command when writing a single consolidated entry. + */ +export const PRE_TOOL_USE_MATCHER_PATTERN = PRE_TOOL_USE_MATCHERS.join("|"); + +// ───────────────────────────────────────────────────────── +// Hook script file names +// ───────────────────────────────────────────────────────── + +/** Map of hook types to their script file names. */ +export const HOOK_SCRIPTS: Record = { + PreToolUse: "pretooluse.mjs", + PostToolUse: "posttooluse.mjs", + PreCompact: "precompact.mjs", + SessionStart: "sessionstart.mjs", + UserPromptSubmit: "userpromptsubmit.mjs", +}; + +// ───────────────────────────────────────────────────────── +// Hook validation +// ───────────────────────────────────────────────────────── + +/** Required hooks that must be configured for context-mode to function. */ +export const REQUIRED_HOOKS: HookType[] = [ + HOOK_TYPES.PRE_TOOL_USE, + HOOK_TYPES.SESSION_START, +]; + +/** Optional hooks that enhance functionality but aren't critical. */ +export const OPTIONAL_HOOKS: HookType[] = [ + HOOK_TYPES.POST_TOOL_USE, + HOOK_TYPES.PRE_COMPACT, + HOOK_TYPES.USER_PROMPT_SUBMIT, +]; + +/** + * Check if a hook entry points to a context-mode hook script. + */ +export function isContextModeHook( + entry: { hooks?: Array<{ command?: string }> }, + hookType: HookType, +): boolean { + const scriptName = HOOK_SCRIPTS[hookType]; + return ( + entry.hooks?.some((h) => h.command?.includes(scriptName)) ?? false + ); +} + +/** + * Build the hook command string for a given hook type and plugin root. + */ +export function buildHookCommand( + hookType: HookType, + pluginRoot: string, +): string { + const scriptName = HOOK_SCRIPTS[hookType]; + return `node ${pluginRoot}/hooks/${scriptName}`; +} diff --git a/src/adapters/claude-code/index.ts b/src/adapters/claude-code/index.ts new file mode 100644 index 00000000..d8a5a7fb --- /dev/null +++ b/src/adapters/claude-code/index.ts @@ -0,0 +1,301 @@ +/** + * adapters/claude-code — Claude Code platform adapter. + * + * Implements HookAdapter for Claude Code's JSON stdin/stdout hook paradigm. + * + * Claude Code hook specifics: + * - I/O: JSON on stdin, JSON on stdout + * - Arg modification: `updatedInput` field in response + * - Blocking: `permissionDecision: "deny"` in response + * - PostToolUse output: `updatedMCPToolOutput` field + * - PreCompact: stdout on exit 0 + * - Session ID: transcript_path UUID > session_id > CLAUDE_SESSION_ID > ppid + * - Config: ~/.claude/settings.json + * - Session dir: ~/.claude/context-mode/sessions/ + */ + +import { createHash } from "node:crypto"; +import { readFileSync, writeFileSync, mkdirSync } from "node:fs"; +import { resolve, join } from "node:path"; +import { homedir } from "node:os"; + +import type { + HookAdapter, + HookParadigm, + PlatformCapabilities, + PreToolUseEvent, + PostToolUseEvent, + PreCompactEvent, + SessionStartEvent, + PreToolUseResponse, + PostToolUseResponse, + PreCompactResponse, + SessionStartResponse, + HookRegistration, +} from "../types.js"; + +// ───────────────────────────────────────────────────────── +// Claude Code raw input types +// ───────────────────────────────────────────────────────── + +interface ClaudeCodeHookInput { + tool_name?: string; + tool_input?: Record; + tool_output?: string; + is_error?: boolean; + session_id?: string; + transcript_path?: string; + source?: string; +} + +// ───────────────────────────────────────────────────────── +// Adapter implementation +// ───────────────────────────────────────────────────────── + +export class ClaudeCodeAdapter implements HookAdapter { + readonly name = "Claude Code"; + readonly paradigm: HookParadigm = "json-stdio"; + + readonly capabilities: PlatformCapabilities = { + preToolUse: true, + postToolUse: true, + preCompact: true, + sessionStart: true, + canModifyArgs: true, + canModifyOutput: true, + canInjectSessionContext: true, + }; + + // ── Input parsing ────────────────────────────────────── + + parsePreToolUseInput(raw: unknown): PreToolUseEvent { + const input = raw as ClaudeCodeHookInput; + return { + toolName: input.tool_name ?? "", + toolInput: input.tool_input ?? {}, + sessionId: this.extractSessionId(input), + projectDir: process.env.CLAUDE_PROJECT_DIR, + raw, + }; + } + + parsePostToolUseInput(raw: unknown): PostToolUseEvent { + const input = raw as ClaudeCodeHookInput; + return { + toolName: input.tool_name ?? "", + toolInput: input.tool_input ?? {}, + toolOutput: input.tool_output, + isError: input.is_error, + sessionId: this.extractSessionId(input), + projectDir: process.env.CLAUDE_PROJECT_DIR, + raw, + }; + } + + parsePreCompactInput(raw: unknown): PreCompactEvent { + const input = raw as ClaudeCodeHookInput; + return { + sessionId: this.extractSessionId(input), + projectDir: process.env.CLAUDE_PROJECT_DIR, + raw, + }; + } + + parseSessionStartInput(raw: unknown): SessionStartEvent { + const input = raw as ClaudeCodeHookInput; + const rawSource = input.source ?? "startup"; + + let source: SessionStartEvent["source"]; + switch (rawSource) { + case "compact": + source = "compact"; + break; + case "resume": + source = "resume"; + break; + case "clear": + source = "clear"; + break; + default: + source = "startup"; + } + + return { + sessionId: this.extractSessionId(input), + source, + projectDir: process.env.CLAUDE_PROJECT_DIR, + raw, + }; + } + + // ── Response formatting ──────────────────────────────── + + formatPreToolUseResponse(response: PreToolUseResponse): unknown { + if (response.decision === "deny") { + return { + permissionDecision: "deny", + reason: response.reason ?? "Blocked by context-mode hook", + }; + } + if (response.decision === "modify" && response.updatedInput) { + return { updatedInput: response.updatedInput }; + } + // "allow" — return null/undefined for passthrough + return undefined; + } + + formatPostToolUseResponse(response: PostToolUseResponse): unknown { + const result: Record = {}; + if (response.additionalContext) { + result.additionalContext = response.additionalContext; + } + if (response.updatedOutput) { + result.updatedMCPToolOutput = response.updatedOutput; + } + return Object.keys(result).length > 0 ? result : undefined; + } + + formatPreCompactResponse(response: PreCompactResponse): unknown { + // Claude Code: stdout content on exit 0 is injected as context + return response.context ?? ""; + } + + formatSessionStartResponse(response: SessionStartResponse): unknown { + // Claude Code: stdout content is injected as additional context + return response.context ?? ""; + } + + // ── Configuration ────────────────────────────────────── + + getSettingsPath(): string { + return resolve(homedir(), ".claude", "settings.json"); + } + + getSessionDir(): string { + const dir = join(homedir(), ".claude", "context-mode", "sessions"); + mkdirSync(dir, { recursive: true }); + return dir; + } + + getSessionDBPath(projectDir: string): string { + const hash = createHash("sha256") + .update(projectDir) + .digest("hex") + .slice(0, 16); + return join(this.getSessionDir(), `${hash}.db`); + } + + getSessionEventsPath(projectDir: string): string { + const hash = createHash("sha256") + .update(projectDir) + .digest("hex") + .slice(0, 16); + return join(this.getSessionDir(), `${hash}-events.md`); + } + + generateHookConfig(pluginRoot: string): HookRegistration { + const preToolUseCommand = `node ${pluginRoot}/hooks/pretooluse.mjs`; + const preToolUseMatchers = [ + "Bash", + "WebFetch", + "Read", + "Grep", + "Task", + "mcp__plugin_context-mode_context-mode__execute", + "mcp__plugin_context-mode_context-mode__execute_file", + "mcp__plugin_context-mode_context-mode__batch_execute", + ]; + + return { + PreToolUse: preToolUseMatchers.map((matcher) => ({ + matcher, + hooks: [{ type: "command", command: preToolUseCommand }], + })), + PostToolUse: [ + { + matcher: "", + hooks: [ + { + type: "command", + command: `node ${pluginRoot}/hooks/posttooluse.mjs`, + }, + ], + }, + ], + PreCompact: [ + { + matcher: "", + hooks: [ + { + type: "command", + command: `node ${pluginRoot}/hooks/precompact.mjs`, + }, + ], + }, + ], + UserPromptSubmit: [ + { + matcher: "", + hooks: [ + { + type: "command", + command: `node ${pluginRoot}/hooks/userpromptsubmit.mjs`, + }, + ], + }, + ], + SessionStart: [ + { + matcher: "", + hooks: [ + { + type: "command", + command: `node ${pluginRoot}/hooks/sessionstart.mjs`, + }, + ], + }, + ], + }; + } + + readSettings(): Record | null { + try { + const raw = readFileSync(this.getSettingsPath(), "utf-8"); + return JSON.parse(raw) as Record; + } catch { + return null; + } + } + + writeSettings(settings: Record): void { + writeFileSync( + this.getSettingsPath(), + JSON.stringify(settings, null, 2) + "\n", + "utf-8", + ); + } + + // ── Internal helpers ─────────────────────────────────── + + /** + * Extract session ID from Claude Code hook input. + * Priority: transcript_path UUID > session_id field > CLAUDE_SESSION_ID env > ppid fallback. + */ + private extractSessionId(input: ClaudeCodeHookInput): string { + if (input.transcript_path) { + const match = input.transcript_path.match( + /([a-f0-9-]{36})\.jsonl$/, + ); + if (match) return match[1]; + } + if (input.session_id) return input.session_id; + if (process.env.CLAUDE_SESSION_ID) return process.env.CLAUDE_SESSION_ID; + return `pid-${process.ppid}`; + } +} + +/** + * Singleton adapter instance. + * Use this for all Claude Code platform operations. + */ +export const claudeCodeAdapter = new ClaudeCodeAdapter(); diff --git a/src/adapters/types.ts b/src/adapters/types.ts new file mode 100644 index 00000000..df0cce4e --- /dev/null +++ b/src/adapters/types.ts @@ -0,0 +1,221 @@ +/** + * adapters/types — Platform adapter interface for multi-platform hook support. + * + * Defines the contract that each platform adapter must implement. + * Three paradigms exist across supported platforms: + * A) JSON stdin/stdout — Claude Code, Gemini CLI, VS Code Copilot, Copilot CLI, Cursor + * B) TS Plugin Functions — OpenCode + * C) MCP-only (no hooks) — Codex CLI + * + * The MCP server layer is 100% portable and needs no adapter. + * Only the hook layer requires platform-specific adapters. + */ + +// ───────────────────────────────────────────────────────── +// Hook paradigm +// ───────────────────────────────────────────────────────── + +export type HookParadigm = "json-stdio" | "ts-plugin" | "mcp-only"; + +// ───────────────────────────────────────────────────────── +// Platform capabilities +// ───────────────────────────────────────────────────────── + +export interface PlatformCapabilities { + /** Platform supports PreToolUse / BeforeTool / tool.execute.before hooks. */ + preToolUse: boolean; + /** Platform supports PostToolUse / AfterTool / tool.execute.after hooks. */ + postToolUse: boolean; + /** Platform supports PreCompact / PreCompress / session.compacting hooks. */ + preCompact: boolean; + /** Platform supports SessionStart / session.created hooks. */ + sessionStart: boolean; + /** Platform allows modifying tool input arguments via hooks. */ + canModifyArgs: boolean; + /** Platform allows modifying tool output via PostToolUse hooks. */ + canModifyOutput: boolean; + /** Platform allows injecting context during session start or compaction. */ + canInjectSessionContext: boolean; +} + +// ───────────────────────────────────────────────────────── +// Normalized hook event types +// ───────────────────────────────────────────────────────── + +/** Normalized PreToolUse event — platform-agnostic representation. */ +export interface PreToolUseEvent { + /** Tool name being invoked (e.g., "Bash", "Read", "WebFetch"). */ + toolName: string; + /** Tool input arguments as key-value pairs. */ + toolInput: Record; + /** Session ID extracted by the adapter. */ + sessionId: string; + /** Project directory (if available). */ + projectDir?: string; + /** Raw platform-specific input (for passthrough if needed). */ + raw: unknown; +} + +/** Normalized PostToolUse event — platform-agnostic representation. */ +export interface PostToolUseEvent { + /** Tool name that was invoked. */ + toolName: string; + /** Tool input arguments. */ + toolInput: Record; + /** Tool output/response (if available). */ + toolOutput?: string; + /** Whether the tool call resulted in an error. */ + isError?: boolean; + /** Session ID extracted by the adapter. */ + sessionId: string; + /** Project directory (if available). */ + projectDir?: string; + /** Raw platform-specific input. */ + raw: unknown; +} + +/** Normalized PreCompact event. */ +export interface PreCompactEvent { + /** Session ID. */ + sessionId: string; + /** Project directory (if available). */ + projectDir?: string; + /** Raw platform-specific input. */ + raw: unknown; +} + +/** Normalized SessionStart event. */ +export interface SessionStartEvent { + /** Session ID. */ + sessionId: string; + /** Lifecycle source: fresh start, compaction, resume, or clear. */ + source: "startup" | "compact" | "resume" | "clear"; + /** Project directory (if available). */ + projectDir?: string; + /** Raw platform-specific input. */ + raw: unknown; +} + +// ───────────────────────────────────────────────────────── +// Hook response types +// ───────────────────────────────────────────────────────── + +/** Response from PreToolUse hook — can block, modify, or pass through. */ +export interface PreToolUseResponse { + /** "allow" = pass through, "deny" = block tool execution, "modify" = change input. */ + decision: "allow" | "deny" | "modify"; + /** Reason for denial (shown to the model). */ + reason?: string; + /** Modified tool input (only when decision = "modify"). */ + updatedInput?: Record; +} + +/** Response from PostToolUse hook — can inject context or modify output. */ +export interface PostToolUseResponse { + /** Additional context to inject after tool output. */ + additionalContext?: string; + /** Modified tool output (if platform supports it). */ + updatedOutput?: string; +} + +/** Response from PreCompact hook — injects context before compaction. */ +export interface PreCompactResponse { + /** Context to preserve across compaction. */ + context?: string; +} + +/** Response from SessionStart hook — injects context at session start. */ +export interface SessionStartResponse { + /** Context to inject at session start. */ + context?: string; +} + +// ───────────────────────────────────────────────────────── +// Hook config types +// ───────────────────────────────────────────────────────── + +/** A single hook entry in platform configuration. */ +export interface HookEntry { + /** Tool matcher pattern (empty = match all). */ + matcher: string; + /** Hook commands/handlers to execute. */ + hooks: Array<{ + type: string; + command: string; + }>; +} + +/** Hook registration map — maps hook types to their entries. */ +export type HookRegistration = Record; + +// ───────────────────────────────────────────────────────── +// Adapter interface +// ───────────────────────────────────────────────────────── + +/** + * HookAdapter — contract for platform-specific hook implementations. + * + * Each supported platform (Claude Code, Gemini CLI, OpenCode, etc.) + * provides an adapter that normalizes its hook I/O into a common format. + */ +export interface HookAdapter { + /** Human-readable platform name (e.g., "Claude Code", "Gemini CLI"). */ + readonly name: string; + + /** Hook I/O paradigm used by this platform. */ + readonly paradigm: HookParadigm; + + /** What this platform supports. */ + readonly capabilities: PlatformCapabilities; + + // ── Input parsing ────────────────────────────────────── + + /** Parse raw PreToolUse input into normalized form. */ + parsePreToolUseInput(raw: unknown): PreToolUseEvent; + + /** Parse raw PostToolUse input into normalized form. */ + parsePostToolUseInput(raw: unknown): PostToolUseEvent; + + /** Parse raw PreCompact input (optional — not all platforms support it). */ + parsePreCompactInput?(raw: unknown): PreCompactEvent; + + /** Parse raw SessionStart input (optional — not all platforms support it). */ + parseSessionStartInput?(raw: unknown): SessionStartEvent; + + // ── Response formatting ──────────────────────────────── + + /** Format a PreToolUse response into platform-specific output. */ + formatPreToolUseResponse(response: PreToolUseResponse): unknown; + + /** Format a PostToolUse response into platform-specific output. */ + formatPostToolUseResponse(response: PostToolUseResponse): unknown; + + /** Format a PreCompact response into platform-specific output. */ + formatPreCompactResponse?(response: PreCompactResponse): unknown; + + /** Format a SessionStart response into platform-specific output. */ + formatSessionStartResponse?(response: SessionStartResponse): unknown; + + // ── Configuration ────────────────────────────────────── + + /** Path to the platform's settings file (e.g., ~/.claude/settings.json). */ + getSettingsPath(): string; + + /** Directory where session data is stored. */ + getSessionDir(): string; + + /** Compute per-project session DB path. */ + getSessionDBPath(projectDir: string): string; + + /** Compute per-project session events file path. */ + getSessionEventsPath(projectDir: string): string; + + /** Generate hook registration config for this platform. */ + generateHookConfig(pluginRoot: string): HookRegistration; + + /** Read current platform settings. */ + readSettings(): Record | null; + + /** Write platform settings. */ + writeSettings(settings: Record): void; +} diff --git a/packages/core/src/cli.ts b/src/cli.ts similarity index 66% rename from packages/core/src/cli.ts rename to src/cli.ts index b4eaff26..ca79e46d 100644 --- a/packages/core/src/cli.ts +++ b/src/cli.ts @@ -13,10 +13,9 @@ import * as p from "@clack/prompts"; import color from "picocolors"; import { execSync } from "node:child_process"; -import { readFileSync, writeFileSync, copyFileSync, cpSync, chmodSync, accessSync, readdirSync, rmSync, constants } from "node:fs"; +import { readFileSync, cpSync, accessSync, readdirSync, rmSync, constants } from "node:fs"; import { resolve, dirname } from "node:path"; import { fileURLToPath } from "node:url"; -import { homedir } from "node:os"; import { detectRuntimes, getRuntimeSummary, @@ -24,6 +23,21 @@ import { getAvailableLanguages, } from "./runtime.js"; +// ── Adapter imports ────────────────────────────────────── +import { + readSettings, + getSettingsPath, + backupSettings, + configureHook, + validateHooks, + checkPluginRegistration, + getMarketplaceVersion, + updatePluginRegistry, + setHookPermissions, + writeSettings, +} from "./adapters/claude-code/config.js"; +import { HOOK_TYPES } from "./adapters/claude-code/hooks.js"; + const args = process.argv.slice(2); if (args[0] === "setup") { @@ -52,23 +66,6 @@ function getPluginRoot(): string { return resolve(__dirname, ".."); } -function getSettingsPath(): string { - return resolve(homedir(), ".claude", "settings.json"); -} - -function readSettings(): Record | null { - try { - const raw = readFileSync(getSettingsPath(), "utf-8"); - return JSON.parse(raw) as Record; - } catch { - return null; - } -} - -function getHookScriptPath(): string { - return resolve(getPluginRoot(), "hooks", "pretooluse.mjs"); -} - function getLocalVersion(): string { try { const pkg = JSON.parse(readFileSync(resolve(getPluginRoot(), "package.json"), "utf-8")); @@ -89,50 +86,6 @@ async function fetchLatestVersion(): Promise { } } -function getMarketplaceVersion(): string { - // Primary: read from installed_plugins.json (source of truth for Claude Code) - try { - const ipPath = resolve(homedir(), ".claude", "plugins", "installed_plugins.json"); - const ipRaw = JSON.parse(readFileSync(ipPath, "utf-8")); - const plugins = ipRaw.plugins ?? {}; - for (const [key, entries] of Object.entries(plugins)) { - if (!key.toLowerCase().includes("context-mode")) continue; - const arr = entries as Array>; - if (arr.length > 0 && typeof arr[0].version === "string") { - return arr[0].version; - } - } - } catch { /* fallback below */ } - - // Fallback: read from own package.json - const localVer = getLocalVersion(); - if (localVer !== "unknown") return localVer; - - // Last resort: scan common plugin cache locations - const bases = [ - resolve(homedir(), ".claude"), - resolve(homedir(), ".config", "claude"), - ]; - for (const base of bases) { - const cacheDir = resolve(base, "plugins", "cache", "claude-context-mode", "context-mode"); - try { - const entries = readdirSync(cacheDir); - const versions = entries - .filter((e) => /^\d+\.\d+\.\d+/.test(e)) - .sort((a, b) => { - const pa = a.split(".").map(Number); - const pb = b.split(".").map(Number); - for (let i = 0; i < 3; i++) { - if ((pa[i] ?? 0) !== (pb[i] ?? 0)) return (pa[i] ?? 0) - (pb[i] ?? 0); - } - return 0; - }); - if (versions.length > 0) return versions[versions.length - 1]; - } catch { /* continue */ } - } - return "not installed"; -} - function semverGt(a: string, b: string): boolean { const pa = a.split(".").map(Number); const pb = b.split(".").map(Number); @@ -232,68 +185,26 @@ async function doctor(): Promise { } } - // Hooks installed + // Hooks installed — using adapter validation p.log.step("Checking hooks configuration..."); - const settings = readSettings(); - const hookScriptPath = getHookScriptPath(); + const pluginRoot = getPluginRoot(); + const hookResults = validateHooks(pluginRoot); - if (settings) { - const hooks = settings.hooks as Record | undefined; - const preToolUse = hooks?.PreToolUse as Array<{ matcher?: string; hooks?: Array<{ command?: string }> }> | undefined; - - if (preToolUse && preToolUse.length > 0) { - const hasCorrectHook = preToolUse.some((entry) => - entry.hooks?.some((h) => h.command?.includes("pretooluse.mjs")), - ); - if (hasCorrectHook) { - p.log.success(color.green("Hooks installed: PASS") + " — PreToolUse hook configured"); - } else { - p.log.error( - color.red("Hooks installed: FAIL") + - " — PreToolUse exists but does not point to pretooluse.mjs" + - color.dim("\n Run: npx context-mode upgrade"), - ); - } + for (const result of hookResults) { + if (result.status === "pass") { + p.log.success(color.green(`${result.hookType} hook: PASS`) + ` — ${result.message}`); } else { p.log.error( - color.red("Hooks installed: FAIL") + - " — No PreToolUse hooks found" + + color.red(`${result.hookType} hook: FAIL`) + + ` — ${result.message}` + color.dim("\n Run: npx context-mode upgrade"), ); } - - // Check SessionStart hook - const sessionStart = hooks?.SessionStart as Array<{ matcher?: string; hooks?: Array<{ command?: string }> }> | undefined; - if (sessionStart && sessionStart.length > 0) { - const hasSessionHook = sessionStart.some((entry) => - entry.hooks?.some((h) => h.command?.includes("sessionstart.mjs")), - ); - if (hasSessionHook) { - p.log.success(color.green("SessionStart hook: PASS") + " — SessionStart hook configured"); - } else { - p.log.error( - color.red("SessionStart hook: FAIL") + - " — SessionStart exists but does not point to sessionstart.mjs" + - color.dim("\n Run: npx context-mode upgrade"), - ); - } - } else { - p.log.error( - color.red("SessionStart hook: FAIL") + - " — No SessionStart hooks found" + - color.dim("\n Run: npx context-mode upgrade"), - ); - } - } else { - p.log.error( - color.red("Hooks installed: FAIL") + - " — Could not read ~/.claude/settings.json" + - color.dim("\n Run: npx context-mode upgrade"), - ); } // Hook script exists p.log.step("Checking hook script..."); + const hookScriptPath = resolve(pluginRoot, "hooks", "pretooluse.mjs"); try { accessSync(hookScriptPath, constants.R_OK); p.log.success(color.green("Hook script exists: PASS") + color.dim(` — ${hookScriptPath}`)); @@ -304,34 +215,15 @@ async function doctor(): Promise { ); } - // Plugin enabled + // Plugin enabled — using adapter check p.log.step("Checking plugin registration..."); - if (settings) { - const enabledPlugins = settings.enabledPlugins as Record | undefined; - if (enabledPlugins) { - const pluginKey = Object.keys(enabledPlugins).find((k) => - k.startsWith("context-mode"), - ); - if (pluginKey && enabledPlugins[pluginKey]) { - p.log.success(color.green("Plugin enabled: PASS") + color.dim(` — ${pluginKey}`)); - } else { - p.log.warn( - color.yellow("Plugin enabled: WARN") + - " — context-mode not in enabledPlugins" + - color.dim(" (might be using standalone MCP mode)"), - ); - } - } else { - p.log.warn( - color.yellow("Plugin enabled: WARN") + - " — no enabledPlugins section found" + - color.dim(" (might be using standalone MCP mode)"), - ); - } + const pluginCheck = checkPluginRegistration(); + if (pluginCheck.status === "pass") { + p.log.success(color.green("Plugin enabled: PASS") + color.dim(` — ${pluginCheck.pluginKey}`)); } else { p.log.warn( color.yellow("Plugin enabled: WARN") + - " — could not read settings.json", + ` — ${pluginCheck.message}`, ); } @@ -439,7 +331,6 @@ async function upgrade() { p.intro(color.bgCyan(color.black(" context-mode upgrade "))); let pluginRoot = getPluginRoot(); - const settingsPath = getSettingsPath(); const changes: string[] = []; const s = p.spinner(); @@ -520,21 +411,9 @@ async function upgrade() { } s.stop(color.green(`Updated in-place to v${newVersion}`)); - // Fix registry to point back to this pluginRoot (self-heal may have changed it) - try { - const ipPath = resolve(homedir(), ".claude", "plugins", "installed_plugins.json"); - const ipRaw = JSON.parse(readFileSync(ipPath, "utf-8")); - for (const [key, entries] of Object.entries(ipRaw.plugins || {})) { - if (!key.toLowerCase().includes("context-mode")) continue; - for (const entry of (entries as Array>)) { - entry.installPath = pluginRoot; - entry.version = newVersion; - entry.lastUpdated = new Date().toISOString(); - } - } - writeFileSync(ipPath, JSON.stringify(ipRaw, null, 2) + "\n", "utf-8"); - p.log.info(color.dim(" Registry synced to " + pluginRoot)); - } catch { /* best effort */ } + // Fix registry — using adapter + updatePluginRegistry(pluginRoot, newVersion); + p.log.info(color.dim(" Registry synced to " + pluginRoot)); // Install production deps (rebuild native modules if needed) s.start("Installing production dependencies"); @@ -580,105 +459,38 @@ async function upgrade() { try { rmSync(tmpDir, { recursive: true, force: true }); } catch { /* ignore */ } } - // Step 3: Backup settings.json + // Step 3: Backup settings.json — using adapter p.log.step("Backing up settings.json..."); - try { - accessSync(settingsPath, constants.R_OK); - const backupPath = settingsPath + ".bak"; - copyFileSync(settingsPath, backupPath); + const backupPath = backupSettings(); + if (backupPath) { p.log.success(color.green("Backup created") + color.dim(" -> " + backupPath)); changes.push("Backed up settings.json"); - } catch { + } else { p.log.warn( color.yellow("No existing settings.json to backup") + " — a new one will be created", ); } - // Step 4: Fix hooks - p.log.step("Configuring PreToolUse hooks..."); - const hookScriptPath = toUnixPath(resolve(pluginRoot, "hooks", "pretooluse.mjs")); + // Step 4: Fix hooks — using adapter + p.log.step("Configuring hooks..."); const settings = readSettings() ?? {}; - const desiredHookEntry = { - matcher: "Bash|Read|Grep|WebFetch|Task|mcp__plugin_context-mode_context-mode__execute|mcp__plugin_context-mode_context-mode__execute_file|mcp__plugin_context-mode_context-mode__batch_execute", - hooks: [ - { - type: "command", - command: "node " + hookScriptPath, - }, - ], - }; + const hookTypes = [ + HOOK_TYPES.PRE_TOOL_USE, + HOOK_TYPES.SESSION_START, + ] as const; - const hooks = (settings.hooks ?? {}) as Record; - const existingPreToolUse = hooks.PreToolUse as Array> | undefined; - - if (existingPreToolUse && Array.isArray(existingPreToolUse)) { - const existingIdx = existingPreToolUse.findIndex((entry) => { - const entryHooks = entry.hooks as Array<{ command?: string }> | undefined; - return entryHooks?.some((h) => h.command?.includes("pretooluse.mjs")); - }); - - if (existingIdx >= 0) { - existingPreToolUse[existingIdx] = desiredHookEntry; - p.log.info(color.dim("Updated existing PreToolUse hook entry")); - changes.push("Updated existing PreToolUse hook entry"); - } else { - existingPreToolUse.push(desiredHookEntry); - p.log.info(color.dim("Added PreToolUse hook entry")); - changes.push("Added PreToolUse hook entry to existing hooks"); - } - hooks.PreToolUse = existingPreToolUse; - } else { - hooks.PreToolUse = [desiredHookEntry]; - p.log.info(color.dim("Created PreToolUse hooks section")); - changes.push("Created PreToolUse hooks section"); + for (const hookType of hookTypes) { + const result = configureHook(settings, hookType, pluginRoot); + p.log.info(color.dim(` ${result}`)); + changes.push(result); } - // --- SessionStart hook --- - p.log.step("Configuring SessionStart hook..."); - const sessionHookScriptPath = resolve(pluginRoot, "hooks", "sessionstart.mjs"); - - const desiredSessionHookEntry = { - matcher: "", - hooks: [ - { - type: "command", - command: "node " + sessionHookScriptPath, - }, - ], - }; - - const existingSessionStart = hooks.SessionStart as Array> | undefined; - - if (existingSessionStart && Array.isArray(existingSessionStart)) { - const existingSessionIdx = existingSessionStart.findIndex((entry) => { - const entryHooks = entry.hooks as Array<{ command?: string }> | undefined; - return entryHooks?.some((h) => h.command?.includes("sessionstart.mjs")); - }); - - if (existingSessionIdx >= 0) { - existingSessionStart[existingSessionIdx] = desiredSessionHookEntry; - p.log.info(color.dim("Updated existing SessionStart hook entry")); - changes.push("Updated existing SessionStart hook entry"); - } else { - existingSessionStart.push(desiredSessionHookEntry); - p.log.info(color.dim("Added SessionStart hook entry")); - changes.push("Added SessionStart hook entry to existing hooks"); - } - hooks.SessionStart = existingSessionStart; - } else { - hooks.SessionStart = [desiredSessionHookEntry]; - p.log.info(color.dim("Created SessionStart hooks section")); - changes.push("Created SessionStart hooks section"); - } - - settings.hooks = hooks; - - // Write updated settings + // Write updated settings — using adapter try { - writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + "\n", "utf-8"); - p.log.success(color.green("Hooks configured") + color.dim(" -> " + settingsPath)); + writeSettings(settings); + p.log.success(color.green("Hooks configured") + color.dim(" -> " + getSettingsPath())); } catch (err: unknown) { const message = err instanceof Error ? err.message : String(err); p.log.error(color.red("Failed to write settings.json") + " — " + message); @@ -686,17 +498,16 @@ async function upgrade() { process.exit(1); } - // Step 5: Set hook script permissions + // Step 5: Set hook script permissions — using adapter p.log.step("Setting hook script permissions..."); - try { - accessSync(hookScriptPath, constants.R_OK); - chmodSync(hookScriptPath, 0o755); - p.log.success(color.green("Permissions set") + color.dim(" — chmod +x " + hookScriptPath)); - changes.push("Set pretooluse.mjs as executable"); - } catch { + const permSet = setHookPermissions(pluginRoot); + if (permSet.length > 0) { + p.log.success(color.green("Permissions set") + color.dim(` — ${permSet.length} hook script(s)`)); + changes.push(`Set ${permSet.length} hook scripts as executable`); + } else { p.log.error( - color.red("Hook script not found") + - color.dim(" — expected at " + hookScriptPath), + color.red("No hook scripts found") + + color.dim(" — expected in " + resolve(pluginRoot, "hooks")), ); } @@ -893,5 +704,3 @@ async function setup() { color.dim(available.length + " languages ready."), ); } - - diff --git a/packages/shared/src/db-base.ts b/src/db-base.ts similarity index 100% rename from packages/shared/src/db-base.ts rename to src/db-base.ts diff --git a/packages/core/src/executor.ts b/src/executor.ts similarity index 98% rename from packages/core/src/executor.ts rename to src/executor.ts index 6fd6f869..839a8632 100644 --- a/packages/core/src/executor.ts +++ b/src/executor.ts @@ -8,9 +8,9 @@ import { type RuntimeMap, type Language, } from "./runtime.js"; -import { smartTruncate } from "@context-mode/shared/truncate"; -export type { ExecResult } from "@context-mode/shared/types"; -import type { ExecResult } from "@context-mode/shared/types"; +import { smartTruncate } from "./truncate.js"; +export type { ExecResult } from "./types.js"; +import type { ExecResult } from "./types.js"; const isWin = process.platform === "win32"; diff --git a/packages/core/src/runtime.ts b/src/runtime.ts similarity index 100% rename from packages/core/src/runtime.ts rename to src/runtime.ts diff --git a/packages/core/src/security.ts b/src/security.ts similarity index 100% rename from packages/core/src/security.ts rename to src/security.ts diff --git a/packages/core/src/server.ts b/src/server.ts similarity index 100% rename from packages/core/src/server.ts rename to src/server.ts diff --git a/packages/session/src/db.ts b/src/session/db.ts similarity index 98% rename from packages/session/src/db.ts rename to src/session/db.ts index edb13d3c..83b57042 100644 --- a/packages/session/src/db.ts +++ b/src/session/db.ts @@ -6,9 +6,9 @@ * the shared package. */ -import { SQLiteBase, defaultDBPath } from "@context-mode/shared/db-base"; -import type { PreparedStatement } from "@context-mode/shared/db-base"; -import type { SessionEvent } from "@context-mode/shared/types"; +import { SQLiteBase, defaultDBPath } from "../db-base.js"; +import type { PreparedStatement } from "../db-base.js"; +import type { SessionEvent } from "../types.js"; import { createHash } from "node:crypto"; // ───────────────────────────────────────────────────────── diff --git a/packages/session/src/extract.ts b/src/session/extract.ts similarity index 100% rename from packages/session/src/extract.ts rename to src/session/extract.ts diff --git a/packages/session/src/snapshot.ts b/src/session/snapshot.ts similarity index 99% rename from packages/session/src/snapshot.ts rename to src/session/snapshot.ts index 2869bccf..d1a8d022 100644 --- a/packages/session/src/snapshot.ts +++ b/src/session/snapshot.ts @@ -11,7 +11,7 @@ * P3-P4 (subagent, skill, role, data, intent): 15% = ~308 bytes */ -import { escapeXML, truncateString } from "@context-mode/shared/truncate"; +import { escapeXML, truncateString } from "../truncate.js"; // ── Types ──────────────────────────────────────────────────────────────────── diff --git a/packages/core/src/store.ts b/src/store.ts similarity index 98% rename from packages/core/src/store.ts rename to src/store.ts index 475d0439..5b446c77 100644 --- a/packages/core/src/store.ts +++ b/src/store.ts @@ -9,8 +9,8 @@ */ import type { Database as DatabaseInstance } from "better-sqlite3"; -import { loadDatabase, applyWALPragmas } from "@context-mode/shared/db-base"; -import type { PreparedStatement } from "@context-mode/shared/db-base"; +import { loadDatabase, applyWALPragmas } from "./db-base.js"; +import type { PreparedStatement } from "./db-base.js"; import { readFileSync, readdirSync, unlinkSync, existsSync } from "node:fs"; import { tmpdir } from "node:os"; import { join } from "node:path"; @@ -25,8 +25,8 @@ interface Chunk { hasCode: boolean; } -import type { IndexResult, SearchResult, StoreStats } from "@context-mode/shared/types"; -export type { IndexResult, SearchResult, StoreStats } from "@context-mode/shared/types"; +import type { IndexResult, SearchResult, StoreStats } from "./types.js"; +export type { IndexResult, SearchResult, StoreStats } from "./types.js"; // ───────────────────────────────────────────────────────── // Constants diff --git a/packages/shared/src/truncate.ts b/src/truncate.ts similarity index 100% rename from packages/shared/src/truncate.ts rename to src/truncate.ts diff --git a/packages/shared/src/types.ts b/src/types.ts similarity index 99% rename from packages/shared/src/types.ts rename to src/types.ts index b06ab129..f3e3296d 100644 --- a/packages/shared/src/types.ts +++ b/src/types.ts @@ -3,7 +3,7 @@ * * Contains interfaces that are genuinely shared between the core (ContentStore, * PolyglotExecutor) and the session domain (SessionDB, event extraction). - * Import from "@context-mode/shared/types". + * Import from "./types.js". */ // ───────────────────────────────────────────────────────── diff --git a/start.mjs b/start.mjs index 7a5e5035..89a6eede 100644 --- a/start.mjs +++ b/start.mjs @@ -87,7 +87,7 @@ if (existsSync(resolve(__dirname, "server.bundle.mjs"))) { } if (!existsSync(resolve(__dirname, "build", "server.js"))) { try { - execSync("npx tsc -b --silent", { cwd: __dirname, stdio: "pipe", timeout: 30000 }); + execSync("npx tsc --silent", { cwd: __dirname, stdio: "pipe", timeout: 30000 }); } catch { /* best effort */ } } await import("./build/server.js"); diff --git a/tests/benchmark.ts b/tests/benchmark.ts index e9b9226a..b622b074 100644 --- a/tests/benchmark.ts +++ b/tests/benchmark.ts @@ -1,10 +1,10 @@ -import { PolyglotExecutor } from "../packages/core/src/executor.js"; +import { PolyglotExecutor } from "../src/executor.js"; import { detectRuntimes, getRuntimeSummary, hasBunRuntime, type Language, -} from "../packages/core/src/runtime.js"; +} from "../src/runtime.js"; const runtimes = detectRuntimes(); const executor = new PolyglotExecutor({ runtimes }); diff --git a/tests/cli-hook-path.test.ts b/tests/cli-hook-path.test.ts index fa3fb993..d36e4634 100644 --- a/tests/cli-hook-path.test.ts +++ b/tests/cli-hook-path.test.ts @@ -10,7 +10,7 @@ import { strict as assert } from "node:assert"; import { describe, test } from "vitest"; -import { toUnixPath } from "../packages/core/src/cli.js"; +import { toUnixPath } from "../src/cli.js"; describe("CLI Hook Path Tests", () => { test("toUnixPath: converts backslashes to forward slashes", () => { diff --git a/tests/context-comparison.ts b/tests/context-comparison.ts index fc9df840..951d19b2 100644 --- a/tests/context-comparison.ts +++ b/tests/context-comparison.ts @@ -8,8 +8,8 @@ * Fixtures in tests/fixtures/ represent real-world data Claude Code encounters daily. */ -import { PolyglotExecutor } from "../packages/core/src/executor.js"; -import { detectRuntimes, getRuntimeSummary } from "../packages/core/src/runtime.js"; +import { PolyglotExecutor } from "../src/executor.js"; +import { detectRuntimes, getRuntimeSummary } from "../src/runtime.js"; import { readFileSync } from "node:fs"; import { join, dirname } from "node:path"; import { fileURLToPath } from "node:url"; diff --git a/tests/ecosystem-benchmark.ts b/tests/ecosystem-benchmark.ts index f6e199f2..052501f1 100644 --- a/tests/ecosystem-benchmark.ts +++ b/tests/ecosystem-benchmark.ts @@ -13,8 +13,8 @@ * This provides honest, reproducible benchmarks for the open-source community. */ -import { PolyglotExecutor } from "../packages/core/src/executor.js"; -import { detectRuntimes, getRuntimeSummary } from "../packages/core/src/runtime.js"; +import { PolyglotExecutor } from "../src/executor.js"; +import { detectRuntimes, getRuntimeSummary } from "../src/runtime.js"; import { readFileSync, existsSync } from "node:fs"; import { join, dirname } from "node:path"; import { fileURLToPath } from "node:url"; diff --git a/tests/executor.test.ts b/tests/executor.test.ts index eb1f3b7e..e6f2b19f 100644 --- a/tests/executor.test.ts +++ b/tests/executor.test.ts @@ -3,13 +3,13 @@ import { strict as assert } from "node:assert"; import { writeFileSync, mkdirSync, rmSync } from "node:fs"; import { join } from "node:path"; import { tmpdir } from "node:os"; -import { PolyglotExecutor } from "../packages/core/src/executor.js"; +import { PolyglotExecutor } from "../src/executor.js"; import { detectRuntimes, buildCommand, getRuntimeSummary, type RuntimeMap, -} from "../packages/core/src/runtime.js"; +} from "../src/runtime.js"; const runtimes = detectRuntimes(); const executor = new PolyglotExecutor({ runtimes }); @@ -1127,7 +1127,7 @@ describe("Windows Shell Support", () => { }); test("getAvailableLanguages always includes shell", async () => { - const { getAvailableLanguages } = await import("../packages/core/src/runtime.js"); + const { getAvailableLanguages } = await import("../src/runtime.js"); const langs = getAvailableLanguages(runtimes); assert.ok(langs.includes("shell"), `shell should always be in available languages, got: ${langs}`); }); diff --git a/tests/extract-snippet.test.ts b/tests/extract-snippet.test.ts index 8fb5b64c..1a838328 100644 --- a/tests/extract-snippet.test.ts +++ b/tests/extract-snippet.test.ts @@ -9,8 +9,8 @@ import { describe, test } from "vitest"; import { strict as assert } from "node:assert"; -import { extractSnippet, positionsFromHighlight } from "../packages/core/src/server.js"; -import { ContentStore } from "../packages/core/src/store.js"; +import { extractSnippet, positionsFromHighlight } from "../src/server.js"; +import { ContentStore } from "../src/store.js"; const STX = "\x02"; const ETX = "\x03"; diff --git a/tests/fuzzy-search.test.ts b/tests/fuzzy-search.test.ts index 3bed479c..0fc3a079 100644 --- a/tests/fuzzy-search.test.ts +++ b/tests/fuzzy-search.test.ts @@ -14,7 +14,7 @@ import { describe, test, expect } from "vitest"; import { strict as assert } from "node:assert"; import { tmpdir } from "node:os"; import { join } from "node:path"; -import { ContentStore } from "../packages/core/src/store.js"; +import { ContentStore } from "../src/store.js"; function createStore(): ContentStore { const path = join( diff --git a/tests/intent-search.test.ts b/tests/intent-search.test.ts index e5946366..130c45ac 100644 --- a/tests/intent-search.test.ts +++ b/tests/intent-search.test.ts @@ -11,7 +11,7 @@ import { strict as assert } from "node:assert"; import { describe, test } from "vitest"; -import { ContentStore } from "../packages/core/src/store.js"; +import { ContentStore } from "../src/store.js"; // ───────────────────────────────────────────────────────── // Smart Truncation simulation (60% head + 40% tail) diff --git a/tests/live-benchmark-v04.ts b/tests/live-benchmark-v04.ts index 2b5bcc11..cefd83ac 100644 --- a/tests/live-benchmark-v04.ts +++ b/tests/live-benchmark-v04.ts @@ -3,9 +3,9 @@ * Tests ALL 5 tools with real-world use cases */ -import { PolyglotExecutor } from "../packages/core/src/executor.js"; -import { ContentStore } from "../packages/core/src/store.js"; -import { detectRuntimes } from "../packages/core/src/runtime.js"; +import { PolyglotExecutor } from "../src/executor.js"; +import { ContentStore } from "../src/store.js"; +import { detectRuntimes } from "../src/runtime.js"; import { readFileSync, writeFileSync } from "node:fs"; import { join, dirname } from "node:path"; import { fileURLToPath } from "node:url"; diff --git a/tests/live-benchmark.ts b/tests/live-benchmark.ts index fca5f281..ced47145 100644 --- a/tests/live-benchmark.ts +++ b/tests/live-benchmark.ts @@ -8,7 +8,7 @@ import { readFileSync, existsSync } from "node:fs"; import { join, dirname } from "node:path"; import { fileURLToPath } from "node:url"; -import { ContentStore } from "../packages/core/src/store.js"; +import { ContentStore } from "../src/store.js"; const __dirname = dirname(fileURLToPath(import.meta.url)); const fixtureDir = join(__dirname, "fixtures"); diff --git a/tests/project-dir.test.ts b/tests/project-dir.test.ts index 2d5c8aaf..84991958 100644 --- a/tests/project-dir.test.ts +++ b/tests/project-dir.test.ts @@ -3,8 +3,8 @@ import { writeFileSync, mkdirSync, rmSync } from "node:fs"; import { join, resolve } from "node:path"; import { tmpdir } from "node:os"; import { describe, test, afterAll } from "vitest"; -import { PolyglotExecutor } from "../packages/core/src/executor.js"; -import { detectRuntimes } from "../packages/core/src/runtime.js"; +import { PolyglotExecutor } from "../src/executor.js"; +import { detectRuntimes } from "../src/runtime.js"; const runtimes = detectRuntimes(); diff --git a/tests/search-fallback-integration.test.ts b/tests/search-fallback-integration.test.ts index 990e824d..7dd0b3fb 100644 --- a/tests/search-fallback-integration.test.ts +++ b/tests/search-fallback-integration.test.ts @@ -16,7 +16,7 @@ import { describe, test } from "vitest"; import { strict as assert } from "node:assert"; import { tmpdir } from "node:os"; import { join } from "node:path"; -import { ContentStore } from "../packages/core/src/store.js"; +import { ContentStore } from "../src/store.js"; function createStore(): ContentStore { const path = join( diff --git a/tests/search-wiring.test.ts b/tests/search-wiring.test.ts index a7784610..63044c29 100644 --- a/tests/search-wiring.test.ts +++ b/tests/search-wiring.test.ts @@ -15,7 +15,7 @@ import { describe, test, expect } from "vitest"; import { strict as assert } from "node:assert"; import { tmpdir } from "node:os"; import { join } from "node:path"; -import { ContentStore } from "../packages/core/src/store.js"; +import { ContentStore } from "../src/store.js"; function createStore(): ContentStore { const path = join( diff --git a/tests/session/session-db.test.ts b/tests/session/session-db.test.ts index 33e3ea4e..5f835784 100644 --- a/tests/session/session-db.test.ts +++ b/tests/session/session-db.test.ts @@ -3,7 +3,7 @@ import { join } from "node:path"; import { tmpdir } from "node:os"; import { randomUUID } from "node:crypto"; import { afterAll, describe, test } from "vitest"; -import { SessionDB } from "../../packages/session/src/db.js"; +import { SessionDB } from "../../src/session/db.js"; const cleanups: Array<() => void> = []; diff --git a/tests/session/session-extract.test.ts b/tests/session/session-extract.test.ts index e52aad55..fd5e3479 100644 --- a/tests/session/session-extract.test.ts +++ b/tests/session/session-extract.test.ts @@ -1,6 +1,6 @@ import { strict as assert } from "node:assert"; import { describe, test } from "vitest"; -import { extractEvents, extractUserEvents } from "../../packages/session/src/extract.js"; +import { extractEvents, extractUserEvents } from "../../src/session/extract.js"; // ════════════════════════════════════════════ // SLICE 1: FILE EVENT EXTRACTION diff --git a/tests/session/session-pipeline.test.ts b/tests/session/session-pipeline.test.ts index 999b6431..2ee83312 100644 --- a/tests/session/session-pipeline.test.ts +++ b/tests/session/session-pipeline.test.ts @@ -3,9 +3,9 @@ import { tmpdir } from "node:os"; import { join } from "node:path"; import { randomUUID } from "node:crypto"; import { afterAll, describe, test } from "vitest"; -import { extractEvents, extractUserEvents } from "../../packages/session/src/extract.js"; -import { SessionDB } from "../../packages/session/src/db.js"; -import { buildResumeSnapshot } from "../../packages/session/src/snapshot.js"; +import { extractEvents, extractUserEvents } from "../../src/session/extract.js"; +import { SessionDB } from "../../src/session/db.js"; +import { buildResumeSnapshot } from "../../src/session/snapshot.js"; const cleanups: Array<() => void> = []; diff --git a/tests/session/session-snapshot.test.ts b/tests/session/session-snapshot.test.ts index 94c2594e..233962aa 100644 --- a/tests/session/session-snapshot.test.ts +++ b/tests/session/session-snapshot.test.ts @@ -10,7 +10,7 @@ import { renderErrors, renderIntent, type StoredEvent, -} from "../../packages/session/src/snapshot.js"; +} from "../../src/session/snapshot.js"; // ── Helpers ── function makeEvent(overrides: Partial & Pick): StoredEvent { diff --git a/tests/store.test.ts b/tests/store.test.ts index 001b4947..866c43c4 100644 --- a/tests/store.test.ts +++ b/tests/store.test.ts @@ -11,7 +11,7 @@ import { readFileSync, writeFileSync, existsSync } from "node:fs"; import { join, dirname } from "node:path"; import { tmpdir } from "node:os"; import { fileURLToPath } from "node:url"; -import { ContentStore, cleanupStaleDBs } from "../packages/core/src/store.js"; +import { ContentStore, cleanupStaleDBs } from "../src/store.js"; const __dirname = dirname(fileURLToPath(import.meta.url)); const fixtureDir = join(__dirname, "fixtures"); diff --git a/tests/stream-cap.test.ts b/tests/stream-cap.test.ts index fa658d03..979bdf25 100644 --- a/tests/stream-cap.test.ts +++ b/tests/stream-cap.test.ts @@ -3,8 +3,8 @@ import { strict as assert } from "node:assert"; import { writeFileSync, mkdtempSync, rmSync } from "node:fs"; import { join } from "node:path"; import { tmpdir } from "node:os"; -import { PolyglotExecutor } from "../packages/core/src/executor.js"; -import { detectRuntimes } from "../packages/core/src/runtime.js"; +import { PolyglotExecutor } from "../src/executor.js"; +import { detectRuntimes } from "../src/runtime.js"; const runtimes = detectRuntimes(); diff --git a/tests/subagent-budget.test.ts b/tests/subagent-budget.test.ts index 40cdcb03..a5ac3726 100644 --- a/tests/subagent-budget.test.ts +++ b/tests/subagent-budget.test.ts @@ -15,7 +15,7 @@ import { spawnSync, execSync } from "node:child_process"; import { join, dirname } from "node:path"; import { fileURLToPath } from "node:url"; import { describe, test } from "vitest"; -import { ContentStore } from "../packages/core/src/store.js"; +import { ContentStore } from "../src/store.js"; const __dirname = dirname(fileURLToPath(import.meta.url)); const HOOK_PATH = join(__dirname, "..", "hooks", "pretooluse.mjs"); diff --git a/tests/turndown.test.ts b/tests/turndown.test.ts index cab3bfaa..191e0c3f 100644 --- a/tests/turndown.test.ts +++ b/tests/turndown.test.ts @@ -1,6 +1,6 @@ import { strict as assert } from "node:assert"; import { describe, test } from "vitest"; -import { PolyglotExecutor } from "../packages/core/src/executor.js"; +import { PolyglotExecutor } from "../src/executor.js"; import { createRequire } from "node:module"; // Resolve turndown path the same way server.ts will diff --git a/tests/use-cases.ts b/tests/use-cases.ts index a373f7c8..c2df222d 100644 --- a/tests/use-cases.ts +++ b/tests/use-cases.ts @@ -6,8 +6,8 @@ */ import { strict as assert } from "node:assert"; -import { PolyglotExecutor } from "../packages/core/src/executor.js"; -import { detectRuntimes, getRuntimeSummary } from "../packages/core/src/runtime.js"; +import { PolyglotExecutor } from "../src/executor.js"; +import { detectRuntimes, getRuntimeSummary } from "../src/runtime.js"; import { readFileSync } from "node:fs"; import { join, dirname } from "node:path"; import { fileURLToPath } from "node:url"; diff --git a/tsconfig.base.json b/tsconfig.base.json deleted file mode 100644 index f18d10f0..00000000 --- a/tsconfig.base.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "compilerOptions": { - "target": "ES2022", - "module": "NodeNext", - "moduleResolution": "NodeNext", - "declaration": true, - "strict": true, - "esModuleInterop": true, - "skipLibCheck": true, - "forceConsistentCasingInFileNames": true, - "resolveJsonModule": true, - "isolatedModules": true - } -} diff --git a/tsconfig.json b/tsconfig.json index b1b9c385..fd973e3d 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,8 +1,17 @@ { - "files": [], - "references": [ - { "path": "packages/shared" }, - { "path": "packages/core" }, - { "path": "packages/session" } - ] + "compilerOptions": { + "target": "ES2022", + "module": "NodeNext", + "moduleResolution": "NodeNext", + "outDir": "./build", + "rootDir": "./src", + "declaration": true, + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "resolveJsonModule": true, + "isolatedModules": true + }, + "include": ["src"] }