Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 40 additions & 0 deletions .github/release-please-config.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,46 @@
"type": "json",
"path": ".claude-plugin/plugin.json",
"jsonpath": "$.version"
},
{
"type": "generic",
"path": "skills/knowpatch/SKILL.md"
},
{
"type": "generic",
"path": "skills/knowpatch/corrections/cli-tools.md"
},
{
"type": "generic",
"path": "skills/knowpatch/corrections/frameworks.md"
},
{
"type": "generic",
"path": "skills/knowpatch/corrections/frontier-models.md"
},
{
"type": "generic",
"path": "skills/knowpatch/corrections/javascript.md"
},
{
"type": "generic",
"path": "skills/knowpatch/corrections/macos.md"
},
{
"type": "generic",
"path": "skills/knowpatch/corrections/open-source-models.md"
},
{
"type": "generic",
"path": "skills/knowpatch/corrections/platforms.md"
},
{
"type": "generic",
"path": "skills/knowpatch/corrections/python.md"
},
{
"type": "generic",
"path": "skills/knowpatch/corrections/runtimes.md"
}
]
}
Expand Down
1 change: 1 addition & 0 deletions skills/knowpatch/SKILL.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
---
name: knowpatch
version: "0.4.0" # x-release-please-version
description: >
LLM knowledge cutoff compensator — knowledge corrections for breaking changes and API drift.
Covers: renamed packages (shadcn-ui→shadcn), changed APIs (z.string().email()→z.email()),
Expand Down
1 change: 1 addition & 0 deletions skills/knowpatch/corrections/cli-tools.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
ecosystem: cli-tools
description: CLI tool renames, command changes, major versions
tags: [shadcn, tailwind, eslint, create-react-app, vite, webpack, prettier, cli]
version: "0.4.0" # x-release-please-version
last_updated: "2026-02-24"
---

Expand Down
1 change: 1 addition & 0 deletions skills/knowpatch/corrections/frameworks.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
ecosystem: frameworks
description: Framework major version breaking changes
tags: [next, nextjs, svelte, vue, nuxt, astro, remix, angular, framework]
version: "0.4.0" # x-release-please-version
last_updated: "2026-02-24"
---

Expand Down
1 change: 1 addition & 0 deletions skills/knowpatch/corrections/frontier-models.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
ecosystem: frontier-models
description: Proprietary frontier AI model names, IDs, SDK versions, multimodal support
tags: [claude, gpt, gemini, openai, anthropic, model, llm, sdk, ai]
version: "0.4.0" # x-release-please-version
last_updated: "2026-02-27"
---

Expand Down
1 change: 1 addition & 0 deletions skills/knowpatch/corrections/javascript.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
ecosystem: javascript
description: JS/TS library API changes
tags: [zod, react, typescript, npm, bun, deno, pnpm, esm, types, javascript, js, ts]
version: "0.4.0" # x-release-please-version
last_updated: "2026-02-24"
---

Expand Down
1 change: 1 addition & 0 deletions skills/knowpatch/corrections/macos.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
ecosystem: macos
description: macOS 26 version naming, Liquid Glass, Swift 6.2, system toolchain, Apple framework changes
tags: [macos, tahoe, xcode, swift, swiftui, liquid-glass, metal, rosetta, intel, apple-silicon, foundation-models]
version: "0.4.0" # x-release-please-version
last_updated: "2026-02-27"
---

Expand Down
1 change: 1 addition & 0 deletions skills/knowpatch/corrections/open-source-models.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
ecosystem: open-source-models
description: Open-source frontier LLMs for coding, agentic tasks, and self-hosting (2026)
tags: [open-source, self-hosted, coding-model, llama, deepseek, mistral, kimi, minimax, glm, qwen, vllm, sglang, swe-bench, moe, model, llm, ai]
version: "0.4.0" # x-release-please-version
last_updated: "2026-02-27"
---

Expand Down
1 change: 1 addition & 0 deletions skills/knowpatch/corrections/platforms.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
ecosystem: platforms
description: BaaS/platform API key changes, auth patterns
tags: [supabase, anon, service_role, publishable, secret, jwks, baas, firebase, platform]
version: "0.4.0" # x-release-please-version
last_updated: "2026-02-25"
---

Expand Down
1 change: 1 addition & 0 deletions skills/knowpatch/corrections/python.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
ecosystem: python
description: Python ecosystem tool/library changes
tags: [pip, uv, poetry, pydantic, fastapi, django, ruff, flask, sqlalchemy, python]
version: "0.4.0" # x-release-please-version
last_updated: "2026-02-24"
---

Expand Down
1 change: 1 addition & 0 deletions skills/knowpatch/corrections/runtimes.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
ecosystem: runtimes
description: Runtime version tracks, LTS status
tags: [node, python, bun, deno, go, java, runtime]
version: "0.4.0" # x-release-please-version
last_updated: "2026-02-24"
---

Expand Down
168 changes: 160 additions & 8 deletions src/commands/install.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,15 @@
import { cp, mkdir, rm, symlink } from "node:fs/promises";
import { cp, mkdir, readdir, readFile, rm, symlink } from "node:fs/promises";
import { homedir } from "node:os";
import { dirname, resolve } from "node:path";
import { checkbox } from "@inquirer/prompts";
import { installPlatformHook, isPlatformHookInstalled } from "../core/hooks.js";
import {
installPlatformHook,
isPlatformHookInstalled,
isPlatformHookUpToDate,
uninstallPlatformHook,
} from "../core/hooks.js";
import { getUpdateCommand } from "../core/package-manager.js";
import { parseFrontmatter } from "../core/parser.js";
import {
getAgentsSkillPath,
getPlatformSkillPath,
Expand All @@ -16,6 +23,7 @@ import {
safeLstat,
} from "../core/paths.js";
import { PLATFORMS, type PlatformConfig } from "../core/platforms.js";
import { checkForUpdate } from "../core/version.js";
import { isInteractive } from "../ui/interactive.js";
import { COLORS, ICONS } from "../ui/palette.js";

Expand Down Expand Up @@ -79,9 +87,8 @@ export async function installCommand(options: InstallOptions): Promise<void> {
// 1. Canonical: .agents/skills/knowpatch ← cp -r from source
const canonicalPath = getAgentsSkillPath(scope);
if (await isCanonicalInstalled(canonicalPath)) {
console.log(
` ${ICONS.ok} ${COLORS.success("Canonical:")} ${canonicalPath}`,
);
const syncResult = await syncCanonical(source, canonicalPath);
console.log(` ${ICONS.ok} ${COLORS.success("Canonical:")} ${syncResult}`);
} else {
const existing = await safeLstat(canonicalPath);
if (existing !== null) {
Expand Down Expand Up @@ -137,9 +144,17 @@ export async function installCommand(options: InstallOptions): Promise<void> {
// 3. Hook registration
if (platform.supportsHooks) {
if (await isPlatformHookInstalled(platform, scope)) {
console.log(
` ${ICONS.ok} ${COLORS.success(`${platform.displayName}:`)} hook already registered`,
);
if (!(await isPlatformHookUpToDate(platform, scope))) {
await uninstallPlatformHook(platform, scope);
await installPlatformHook(platform, scope);
console.log(
` ${ICONS.ok} ${COLORS.success(`${platform.displayName}:`)} hook updated`,
);
} else {
console.log(
` ${ICONS.ok} ${COLORS.success(`${platform.displayName}:`)} hook already registered`,
);
}
} else {
await installPlatformHook(platform, scope);
console.log(
Expand All @@ -155,4 +170,141 @@ export async function installCommand(options: InstallOptions): Promise<void> {
console.log(
` Done! Installed for ${count} platform${count !== 1 ? "s" : ""}.`,
);

// Non-blocking update notification
showUpdateNotification().catch(() => {});
}

/** Read the version field from a SKILL.md or correction file's YAML frontmatter */
async function readFileVersion(filePath: string): Promise<string | undefined> {
try {
const content = await readFile(filePath, "utf-8");
const { frontmatter } = parseFrontmatter(content);
return (frontmatter as Record<string, unknown> | null)?.version as
| string
| undefined;
} catch {
return undefined;
}
}

/**
* Sync canonical installation with source, using file-level version patching.
* Returns a human-readable status string.
*/
async function syncCanonical(
source: string,
canonicalPath: string,
): Promise<string> {
const installedVersion = await readFileVersion(
resolve(canonicalPath, "SKILL.md"),
);
const sourceVersion = await readFileVersion(resolve(source, "SKILL.md"));

// No version tag → v0.3 legacy → full re-sync
if (!installedVersion) {
await rm(canonicalPath, { recursive: true, force: true });
await mkdir(dirname(canonicalPath), { recursive: true });
await cp(source, canonicalPath, { recursive: true });
return `migrated from legacy → ${sourceVersion ?? "latest"}`;
}

// Same version → up to date
if (installedVersion === sourceVersion) {
return `up to date (${installedVersion})`;
}

// Different version → file-level patch
const patched = await patchFiles(source, canonicalPath);
return `updated ${installedVersion} → ${sourceVersion ?? "latest"} (${patched} file${patched !== 1 ? "s" : ""})`;
}

/**
* Patch individual files by comparing source and installed versions.
* Returns the number of files patched.
*/
async function patchFiles(source: string, installed: string): Promise<number> {
let patched = 0;

// Patch SKILL.md
const srcSkillVer = await readFileVersion(resolve(source, "SKILL.md"));
const instSkillVer = await readFileVersion(resolve(installed, "SKILL.md"));
if (srcSkillVer !== instSkillVer) {
await cp(resolve(source, "SKILL.md"), resolve(installed, "SKILL.md"));
patched++;
}

// Patch corrections/
const srcCorr = resolve(source, "corrections");
const instCorr = resolve(installed, "corrections");
await mkdir(instCorr, { recursive: true });

const srcFiles = (await pathExists(srcCorr))
? (await readdir(srcCorr)).filter((f) => f.endsWith(".md"))
: [];
const instFiles = (await pathExists(instCorr))
? (await readdir(instCorr)).filter((f) => f.endsWith(".md"))
: [];

const srcSet = new Set(srcFiles);
const instSet = new Set(instFiles);

// Copy new or updated files
for (const file of srcFiles) {
const srcVer = await readFileVersion(resolve(srcCorr, file));
const instVer = instSet.has(file)
? await readFileVersion(resolve(instCorr, file))
: undefined;

if (srcVer !== instVer || !instSet.has(file)) {
await cp(resolve(srcCorr, file), resolve(instCorr, file));
patched++;
}
}

// Remove files that no longer exist in source
for (const file of instFiles) {
if (!srcSet.has(file)) {
await rm(resolve(instCorr, file), { force: true });
patched++;
}
}

// Patch bin/detect.js if source has it
const srcDetect = resolve(source, "bin/detect.js");
const instDetect = resolve(installed, "bin/detect.js");
if (await pathExists(srcDetect)) {
await mkdir(resolve(installed, "bin"), { recursive: true });
await cp(srcDetect, instDetect);
patched++;
}

return patched;
}

/** Show a non-blocking update notification after install */
async function showUpdateNotification(): Promise<void> {
const pkgPath = resolve(
dirname(new URL(import.meta.url).pathname),
"../../package.json",
);
let currentVersion: string;
try {
const pkg = JSON.parse(await readFile(pkgPath, "utf-8")) as {
version: string;
};
currentVersion = pkg.version;
} catch {
return;
}

const newer = await checkForUpdate(currentVersion);
if (!newer) return;

const updateCmd = getUpdateCommand();
console.log();
console.log(
` ${ICONS.drift} ${COLORS.warn(`Update available: ${currentVersion} → ${newer}`)}`,
);
console.log(` Run ${COLORS.info(updateCmd)} to update`);
}
25 changes: 15 additions & 10 deletions src/commands/update.ts
Original file line number Diff line number Diff line change
Expand Up @@ -259,16 +259,19 @@ export async function updateCommand(
const scope: Scope = (options.scope as Scope | undefined) ?? "user";

// CLI version check
if (isInteractive()) {
const spinner = startSpinner("Checking for updates...");
const newer = await checkForUpdate(currentVersion);
spinner.stop();
const spinner = isInteractive()
? startSpinner("Checking for updates...")
: null;
const newer = await checkForUpdate(currentVersion);
spinner?.stop();

if (newer) {
console.log(
` ${ICONS.drift} ${COLORS.warn(`New version available: ${currentVersion} → ${newer}`)}`,
);
const updateCmd = getUpdateCommand();
if (newer) {
const updateCmd = getUpdateCommand();
console.log(
` ${ICONS.drift} ${COLORS.warn(`New version available: ${currentVersion} → ${newer}`)}`,
);

if (isInteractive()) {
const doUpdate = await confirm({
message: `Update knowpatch CLI? (${updateCmd})`,
default: true,
Expand All @@ -284,8 +287,10 @@ export async function updateCommand(
);
}
}
console.log();
} else {
console.log(` Run ${COLORS.info(updateCmd)} to update`);
}
console.log();
}

console.log(" Checking installation...");
Expand Down
15 changes: 15 additions & 0 deletions src/core/package-manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,23 @@ export function detectPackageManager(): PackageManager {
return "npm";
}

/** Detect if running from an ephemeral bunx/npx cache directory */
export function isEphemeral(): boolean {
const root = getPackageRoot();
// bun ephemeral: /tmp/…/bunx-… or macOS /var/folders/…/T/bunx-…
if (/\/tmp\//.test(root) || /\/T\/bunx-/.test(root)) return true;
// npm ephemeral: /_npx/ or /npx/
if (/\/_npx\//.test(root) || /\/npx\//.test(root)) return true;
return false;
}

export function getUpdateCommand(): string {
const pm = detectPackageManager();

if (isEphemeral()) {
return pm === "bun" ? "bunx knowpatch@latest" : "npx knowpatch@latest";
}

const cmds: Record<PackageManager, string> = {
bun: "bun update -g knowpatch",
npm: "npm update -g knowpatch",
Expand Down
Loading