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"]
}