From 8a9e01febc20ebb9a278143c283bd0d7b29843bf Mon Sep 17 00:00:00 2001 From: Soju06 Date: Sat, 28 Feb 2026 00:00:44 +0900 Subject: [PATCH 1/2] feat(hook): relocate detect.js to skill resource for team compatibility The hook command previously pointed to the npm package's ephemeral cache path (e.g. /tmp/bunx-501-knowpatch@latest/...), making it unusable for other team members in project-scoped installations. detect.js is now bundled as a skill resource at .agents/skills/knowpatch/bin/detect.js and copied alongside SKILL.md and corrections during install. Hook commands use scope-aware paths: - project scope: relative path (node .agents/skills/knowpatch/bin/detect.js) - user scope: absolute path (node ~/.agents/skills/knowpatch/bin/detect.js) The update system detects stale hooks and migrates them automatically. --- .gitignore | 1 + package.json | 2 +- src/commands/update.ts | 59 +++++++++++++++++++++++++++++++++--------- src/core/hooks.ts | 24 ++++++++++++++--- src/core/paths.ts | 10 ++++--- src/core/status.ts | 7 ++++- src/hooks/detect.ts | 29 +++++---------------- tests/paths.test.ts | 21 ++++++++++++--- 8 files changed, 107 insertions(+), 46 deletions(-) diff --git a/.gitignore b/.gitignore index 74299dc..68531b1 100644 --- a/.gitignore +++ b/.gitignore @@ -4,5 +4,6 @@ dist/ *.js.map .DS_Store .agents/hooks/state/ +skills/knowpatch/bin/ __pycache__ \ No newline at end of file diff --git a/package.json b/package.json index dd24023..2e6fea4 100644 --- a/package.json +++ b/package.json @@ -7,7 +7,7 @@ "knowpatch": "bin/cli.js" }, "scripts": { - "build": "bun build src/cli.ts --outdir bin --target node --format esm && bun build src/hooks/detect.ts --outdir bin --target node --format esm", + "build": "bun build src/cli.ts --outdir bin --target node --format esm && bun build src/hooks/detect.ts --outdir skills/knowpatch/bin --target node --format esm", "dev": "bun run src/cli.ts", "test": "bun test", "lint": "biome check src/ tests/", diff --git a/src/commands/update.ts b/src/commands/update.ts index fd6eef4..b2e6500 100644 --- a/src/commands/update.ts +++ b/src/commands/update.ts @@ -67,34 +67,54 @@ async function syncInstallation(scope: Scope): Promise { if (ps.implicitlyLinked) { const hookStatus = ps.platform.supportsHooks ? ps.hookInstalled - ? "hook up-to-date" + ? ps.hookUpToDate + ? "hook up-to-date" + : "hook outdated" : "hook missing" : ""; console.log( ` ${ICONS.ok} ${COLORS.success(`${ps.platform.displayName}:`)} shared via parent symlink${hookStatus ? `, ${hookStatus}` : ""}`, ); - if (ps.platform.supportsHooks && !ps.hookInstalled) { - await installPlatformHook(ps.platform, scope); - console.log( - ` ${ICONS.ok} ${COLORS.success(`${ps.platform.displayName}:`)} hook restored`, - ); + if (ps.platform.supportsHooks) { + if (!ps.hookInstalled) { + await installPlatformHook(ps.platform, scope); + console.log( + ` ${ICONS.ok} ${COLORS.success(`${ps.platform.displayName}:`)} hook restored`, + ); + } else if (!ps.hookUpToDate) { + await uninstallPlatformHook(ps.platform, scope); + await installPlatformHook(ps.platform, scope); + console.log( + ` ${ICONS.ok} ${COLORS.success(`${ps.platform.displayName}:`)} hook updated`, + ); + } } } else if (ps.symlinkValid) { const hookStatus = ps.platform.supportsHooks ? ps.hookInstalled - ? "hook up-to-date" + ? ps.hookUpToDate + ? "hook up-to-date" + : "hook outdated" : "hook missing" : ""; console.log( ` ${ICONS.ok} ${COLORS.success(`${ps.platform.displayName}:`)} symlink valid${hookStatus ? `, ${hookStatus}` : ""}`, ); - if (ps.platform.supportsHooks && !ps.hookInstalled) { - await installPlatformHook(ps.platform, scope); - console.log( - ` ${ICONS.ok} ${COLORS.success(`${ps.platform.displayName}:`)} hook restored`, - ); + if (ps.platform.supportsHooks) { + if (!ps.hookInstalled) { + await installPlatformHook(ps.platform, scope); + console.log( + ` ${ICONS.ok} ${COLORS.success(`${ps.platform.displayName}:`)} hook restored`, + ); + } else if (!ps.hookUpToDate) { + await uninstallPlatformHook(ps.platform, scope); + await installPlatformHook(ps.platform, scope); + console.log( + ` ${ICONS.ok} ${COLORS.success(`${ps.platform.displayName}:`)} hook updated`, + ); + } } } else { console.log( @@ -107,6 +127,21 @@ async function syncInstallation(scope: Scope): Promise { console.log( ` ${ICONS.ok} ${COLORS.success(`${ps.platform.displayName}:`)} symlink restored`, ); + + if (ps.platform.supportsHooks) { + if (!ps.hookInstalled) { + await installPlatformHook(ps.platform, scope); + console.log( + ` ${ICONS.ok} ${COLORS.success(`${ps.platform.displayName}:`)} hook restored`, + ); + } else if (!ps.hookUpToDate) { + await uninstallPlatformHook(ps.platform, scope); + await installPlatformHook(ps.platform, scope); + console.log( + ` ${ICONS.ok} ${COLORS.success(`${ps.platform.displayName}:`)} hook updated`, + ); + } + } } } diff --git a/src/core/hooks.ts b/src/core/hooks.ts index 64fc32e..36730ff 100644 --- a/src/core/hooks.ts +++ b/src/core/hooks.ts @@ -60,11 +60,29 @@ export async function isPlatformHookInstalled( if (!settingsPath) return false; const settings = await readSettings(settingsPath); - const hookCmd = getHookCommand(); + const hookCmd = getHookCommand(scope); const entries = settings.hooks?.UserPromptSubmit ?? []; return entries.some((m) => isOurHook(m, hookCmd)); } +/** Check if the installed hook command matches the expected command for this scope */ +export async function isPlatformHookUpToDate( + platform: PlatformConfig, + scope: Scope, +): Promise { + if (platform.hookType === "none") return true; + const settingsPath = getPlatformSettingsPath(platform, scope); + if (!settingsPath) return true; + + const hookCmd = getHookCommand(scope); + const settings = await readSettings(settingsPath); + const entries = settings.hooks?.UserPromptSubmit ?? []; + + return entries.some((m) => + m.hooks.some((h) => h.type === "command" && h.command === hookCmd), + ); +} + /** Add the knowpatch hook to a platform's settings.json */ export async function installPlatformHook( platform: PlatformConfig, @@ -74,7 +92,7 @@ export async function installPlatformHook( const settingsPath = getPlatformSettingsPath(platform, scope); if (!settingsPath) return false; - const hookCmd = getHookCommand(); + const hookCmd = getHookCommand(scope); const settings = await readSettings(settingsPath); if (!settings.hooks) { @@ -106,7 +124,7 @@ export async function uninstallPlatformHook( const settingsPath = getPlatformSettingsPath(platform, scope); if (!settingsPath) return false; - const hookCmd = getHookCommand(); + const hookCmd = getHookCommand(scope); const settings = await readSettings(settingsPath); const hooks = settings.hooks; const entries = hooks?.UserPromptSubmit; diff --git a/src/core/paths.ts b/src/core/paths.ts index 0b27d42..47673b6 100644 --- a/src/core/paths.ts +++ b/src/core/paths.ts @@ -62,9 +62,13 @@ export function getPlatformSettingsPath( return resolve(getScopeBase(scope), platform.configDir, "settings.json"); } -/** Absolute path to the built hook detect script */ -export function getHookCommand(): string { - return `node ${resolve(getPackageRoot(), "bin/detect.js")}`; +/** Hook command for settings.json — relative for project scope, absolute for user */ +export function getHookCommand(scope: Scope): string { + const detectScript = ".agents/skills/knowpatch/bin/detect.js"; + if (scope === "project") { + return `node ${detectScript}`; + } + return `node ${resolve(homedir(), detectScript)}`; } /** Safe lstat that returns null instead of throwing */ diff --git a/src/core/status.ts b/src/core/status.ts index bdda435..62f963d 100644 --- a/src/core/status.ts +++ b/src/core/status.ts @@ -1,6 +1,6 @@ import { lstat, readlink } from "node:fs/promises"; import { dirname, resolve } from "node:path"; -import { isPlatformHookInstalled } from "./hooks.js"; +import { isPlatformHookInstalled, isPlatformHookUpToDate } from "./hooks.js"; import { getAgentsSkillPath, getPlatformSkillPath, @@ -22,6 +22,7 @@ export interface PlatformStatus { symlinkValid: boolean; implicitlyLinked: boolean; hookInstalled: boolean; + hookUpToDate: boolean; } export interface InstallationStatus { @@ -85,6 +86,9 @@ export async function detectInstallation( } const hookInstalled = await isPlatformHookInstalled(platform, scope); + const hookUpToDate = hookInstalled + ? await isPlatformHookUpToDate(platform, scope) + : false; platforms.push({ platform, @@ -92,6 +96,7 @@ export async function detectInstallation( symlinkValid, implicitlyLinked, hookInstalled, + hookUpToDate, }); } diff --git a/src/hooks/detect.ts b/src/hooks/detect.ts index 328f876..b32785d 100644 --- a/src/hooks/detect.ts +++ b/src/hooks/detect.ts @@ -8,8 +8,9 @@ * is derived from correction files. */ -import { accessSync, readdirSync, readFileSync } from "node:fs"; +import { readdirSync, readFileSync } from "node:fs"; import { dirname, resolve } from "node:path"; +import { fileURLToPath } from "node:url"; export interface CorrectionEntry { header: string; @@ -73,21 +74,10 @@ export function buildMessage( ].join("\n"); } -/** Find the package root by walking up from this file */ -function findPackageRoot(): string { - let dir = dirname(new URL(import.meta.url).pathname); - while (true) { - try { - accessSync(resolve(dir, "package.json")); - return dir; - } catch { - const parent = dirname(dir); - if (parent === dir) { - throw new Error("Could not find knowpatch package root"); - } - dir = parent; - } - } +/** Resolve corrections directory relative to this script's location */ +function getCorrectionsDir(): string { + const scriptDir = dirname(fileURLToPath(import.meta.url)); + return resolve(scriptDir, "..", "corrections"); } /** Load all correction files, returning parsed tags and entries */ @@ -147,12 +137,7 @@ async function main() { return; } - let correctionsDir: string; - try { - correctionsDir = resolve(findPackageRoot(), "skills/knowpatch/corrections"); - } catch { - return; - } + const correctionsDir = getCorrectionsDir(); // Load corrections and derive keywords from tags const corrections = loadCorrections(correctionsDir); diff --git a/tests/paths.test.ts b/tests/paths.test.ts index 4a91491..dcbec2e 100644 --- a/tests/paths.test.ts +++ b/tests/paths.test.ts @@ -95,10 +95,23 @@ describe("getPlatformSettingsPath", () => { }); describe("getHookCommand", () => { - test("includes bin/detect.js", () => { - const cmd = getHookCommand(); - expect(cmd).toContain("bin/detect.js"); - expect(cmd).toStartWith("node "); + test("project scope returns relative path", () => { + const cmd = getHookCommand("project"); + expect(cmd).toBe("node .agents/skills/knowpatch/bin/detect.js"); + }); + + test("user scope returns absolute path with homedir", () => { + const { homedir } = require("node:os"); + const { resolve } = require("node:path"); + const cmd = getHookCommand("user"); + expect(cmd).toBe( + `node ${resolve(homedir(), ".agents/skills/knowpatch/bin/detect.js")}`, + ); + }); + + test("both scopes start with node prefix", () => { + expect(getHookCommand("project")).toStartWith("node "); + expect(getHookCommand("user")).toStartWith("node "); }); }); From e492a712c0bb62c9f7764ad825e9b7a8529e1eb3 Mon Sep 17 00:00:00 2001 From: Soju06 Date: Sat, 28 Feb 2026 00:02:27 +0900 Subject: [PATCH 2/2] fix(ci): update build artifact checks for relocated detect.js The detect.js build output moved from bin/ to skills/knowpatch/bin/. Update CI and release workflow verification steps to check the new path. --- .github/workflows/ci.yml | 6 +++--- .github/workflows/release.yml | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index afa743c..b6e4a56 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -56,7 +56,7 @@ jobs: set -euo pipefail TARBALL=$(ls knowpatch-*.tgz) CONTENTS=$(tar tzf "${TARBALL}") - echo "${CONTENTS}" | grep -q "package/bin/cli.js" || { echo "FAIL: bin/cli.js missing"; exit 1; } - echo "${CONTENTS}" | grep -q "package/bin/detect.js" || { echo "FAIL: bin/detect.js missing"; exit 1; } - echo "${CONTENTS}" | grep -q "package/skills/" || { echo "FAIL: skills/ missing"; exit 1; } + echo "${CONTENTS}" | grep -q "package/bin/cli.js" || { echo "FAIL: bin/cli.js missing"; exit 1; } + echo "${CONTENTS}" | grep -q "package/skills/knowpatch/bin/detect.js" || { echo "FAIL: skills/knowpatch/bin/detect.js missing"; exit 1; } + echo "${CONTENTS}" | grep -q "package/skills/" || { echo "FAIL: skills/ missing"; exit 1; } echo "Package contents verified" diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index c9a42af..b84ecd9 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -71,7 +71,7 @@ jobs: - run: bun run build - name: Verify build artifacts run: | - [ -f bin/cli.js ] && [ -f bin/detect.js ] && echo "OK" || exit 1 + [ -f bin/cli.js ] && [ -f skills/knowpatch/bin/detect.js ] && echo "OK" || exit 1 - name: Verify package contents run: npm pack --dry-run - name: Publish to npm