diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index fce8022..14d53a9 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -260,3 +260,149 @@ jobs: echo "kimetsu update --check" >> $GITHUB_STEP_SUMMARY echo "kimetsu uninstall --dry-run" >> $GITHUB_STEP_SUMMARY echo '```' >> $GITHUB_STEP_SUMMARY + + publish-npm: + # Publish the `kimetsu` npm package after the binary matrix + GH Release + # succeed. esbuild/turbo style: one per-platform package per target (lean + # binary inside, os/cpu set) plus the main `kimetsu` package that lists + # them as optionalDependencies. npm installs only the matching platform + # package; the bin/cli.js launcher execs its binary. No postinstall. + # + # Embeddings is NOT shipped as a package — it's fetched on demand by the + # launcher when KIMETSU_NPM_FLAVOR=embeddings is set (see npm/kimetsu). + # + # Gated, like publish-crates, on: + # (a) tag push (handled by `if`) + # (b) the repo variable PUBLISH_NPM == 'true' + # (c) the NPM_TOKEN secret being set + # To enable: `gh variable set PUBLISH_NPM --body true` and + # `gh secret set NPM_TOKEN` (an npm automation token). + name: publish to npm + needs: release + runs-on: ubuntu-latest + if: ${{ startsWith(github.ref, 'refs/tags/v') && vars.PUBLISH_NPM == 'true' }} + steps: + - uses: actions/checkout@v4 + + - name: setup node + uses: actions/setup-node@v4 + with: + node-version: 20 + registry-url: https://registry.npmjs.org + + - name: download all artifacts + uses: actions/download-artifact@v4 + with: + path: dist + + - name: verify NPM_TOKEN secret is set + env: + NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} + run: | + if [ -z "$NODE_AUTH_TOKEN" ]; then + echo "::error::NPM_TOKEN secret is not set. Skipping npm publish." + echo "::error::Add the secret via: gh secret set NPM_TOKEN" + exit 1 + fi + echo "NPM_TOKEN is set (length: ${#NODE_AUTH_TOKEN})" + + - name: assemble + publish platform packages + env: + NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} + run: | + set -euo pipefail + VERSION="${GITHUB_REF_NAME#v}" + + # npm-key target-triple os cpu binname ext + PLATFORMS=( + "linux-x64 x86_64-unknown-linux-gnu linux x64 kimetsu tar.gz" + "darwin-x64 x86_64-apple-darwin darwin x64 kimetsu tar.gz" + "darwin-arm64 aarch64-apple-darwin darwin arm64 kimetsu tar.gz" + "win32-x64 x86_64-pc-windows-msvc win32 x64 kimetsu.exe zip" + ) + + mkdir -p stage + for row in "${PLATFORMS[@]}"; do + read -r key target osv cpuv binname ext <<<"$row" + stem="kimetsu-${VERSION}-${target}-lean" + archive="dist/kimetsu-${target}-lean/${stem}.${ext}" + if [ ! -f "$archive" ]; then + echo "::error::missing lean artifact for ${target}: ${archive}" + exit 1 + fi + + tmp="$(mktemp -d)" + if [ "$ext" = "zip" ]; then + unzip -q "$archive" -d "$tmp" + else + tar -xf "$archive" -C "$tmp" + fi + + pkgdir="stage/${key}" + mkdir -p "$pkgdir/bin" + if [ -f "$tmp/${stem}/${binname}" ]; then + cp "$tmp/${stem}/${binname}" "$pkgdir/bin/${binname}" + elif [ -f "$tmp/${binname}" ]; then + cp "$tmp/${binname}" "$pkgdir/bin/${binname}" + else + echo "::error::binary ${binname} not found inside ${archive}" + exit 1 + fi + [ "$ext" = "zip" ] || chmod +x "$pkgdir/bin/${binname}" + + node -e ' + const fs = require("fs"); + const [dir, name, version, osv, cpuv] = process.argv.slice(1); + fs.writeFileSync(dir + "/package.json", JSON.stringify({ + name, + version, + description: name + " — prebuilt kimetsu binary", + repository: { type: "git", url: "git+https://github.com/RodCor/kimetsu.git" }, + license: "MIT OR Apache-2.0", + os: [osv], + cpu: [cpuv], + files: ["bin"], + }, null, 2) + "\n"); + ' "$pkgdir" "@kimetsu/${key}" "$VERSION" "$osv" "$cpuv" + + echo "publishing @kimetsu/${key}@${VERSION}" + ( cd "$pkgdir" && npm publish --access public ) + done + + - name: stamp + publish main package + env: + NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} + run: | + set -euo pipefail + VERSION="${GITHUB_REF_NAME#v}" + node -e ' + const fs = require("fs"); + const version = process.argv[1]; + const p = "npm/kimetsu/package.json"; + const j = JSON.parse(fs.readFileSync(p, "utf8")); + j.version = version; + for (const k of Object.keys(j.optionalDependencies || {})) { + j.optionalDependencies[k] = version; + } + fs.writeFileSync(p, JSON.stringify(j, null, 2) + "\n"); + ' "$VERSION" + echo "publishing kimetsu@${VERSION}" + ( cd npm/kimetsu && npm publish --access public ) + + - name: summary + run: | + VERSION="${GITHUB_REF_NAME#v}" + echo "## npm publish — v${VERSION} ✅" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "Published the main package + 4 platform packages:" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "- [\`kimetsu\`](https://www.npmjs.com/package/kimetsu/v/${VERSION})" >> $GITHUB_STEP_SUMMARY + for k in linux-x64 darwin-x64 darwin-arm64 win32-x64; do + echo "- [\`@kimetsu/${k}\`](https://www.npmjs.com/package/@kimetsu/${k}/v/${VERSION})" >> $GITHUB_STEP_SUMMARY + done + echo "" >> $GITHUB_STEP_SUMMARY + echo "Users can now install with:" >> $GITHUB_STEP_SUMMARY + echo '```' >> $GITHUB_STEP_SUMMARY + echo "npm install -g kimetsu # lean build" >> $GITHUB_STEP_SUMMARY + echo "KIMETSU_NPM_FLAVOR=embeddings npm install -g kimetsu # semantic build" >> $GITHUB_STEP_SUMMARY + echo '```' >> $GITHUB_STEP_SUMMARY diff --git a/CHANGELOG.md b/CHANGELOG.md index 5b3c649..43c4f77 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,21 @@ follow [SemVer](https://semver.org/spec/v2.0.0.html) with the caveat that pre-1.0 minor bumps may include breaking changes (documented in the release notes). +## Unreleased + +ADDED + * **npm distribution.** Kimetsu now publishes to npm — `npm install -g kimetsu` + installs the prebuilt native binary for your platform, no Rust toolchain + required. Uses the esbuild/turbo model: per-platform packages + (`@kimetsu/linux-x64`, `@kimetsu/darwin-x64`, `@kimetsu/darwin-arm64`, + `@kimetsu/win32-x64`) selected via `optionalDependencies`, with a thin + `bin/cli.js` launcher that execs the matching binary. No postinstall, so it + works under `npm install --ignore-scripts`. The semantic build is fetched on + demand when `KIMETSU_NPM_FLAVOR=embeddings` is set. Published from the + existing release pipeline (new `publish-npm` job, gated on the `PUBLISH_NPM` + repo variable + `NPM_TOKEN` secret, mirroring the crates.io gate). Sources + live in [`npm/`](npm/). + ## v0.8.0 — proactive recall, selectable embedding model, full MCP control The release that makes the brain **proactive** and gives the agent (and user) diff --git a/README.md b/README.md index ef05ff7..f00eb14 100644 --- a/README.md +++ b/README.md @@ -109,8 +109,22 @@ cargo install kimetsu-cli --features embeddings cargo install --path crates/kimetsu-cli # add --features embeddings for semantic search ``` -Prefer not to touch the Rust toolchain? Pre-built binaries for -**Linux / macOS / Windows** ship on every +Prefer not to touch the Rust toolchain? Two options. + +**npm** — installs the prebuilt binary for your platform, no Rust required: + +```bash +npm install -g kimetsu # lean build +KIMETSU_NPM_FLAVOR=embeddings npm install -g kimetsu # opt into the semantic build +``` + +npm pulls only the matching per-platform package (`@kimetsu/*`) via +optionalDependencies — there's no postinstall download, so it works under +`npm install --ignore-scripts`. The embeddings build is fetched on first run and +is available where ONNX Runtime prebuilts exist (Linux x64, macOS Apple Silicon, +Windows x64); elsewhere it falls back to lean. See [`npm/`](npm/) for details. + +**Pre-built archives** — for **Linux / macOS / Windows** on every [GitHub Release](https://github.com/RodCor/kimetsu/releases). Extract the archive and put `kimetsu` / `kimetsu.exe` somewhere on `PATH` (`~/.local/bin`, `/usr/local/bin`, or `%USERPROFILE%\.cargo\bin`). Lean archives are published for Linux, diff --git a/npm/README.md b/npm/README.md new file mode 100644 index 0000000..dd67e09 --- /dev/null +++ b/npm/README.md @@ -0,0 +1,47 @@ +# npm distribution + +Kimetsu is also published to npm as the `kimetsu` package, so JS/TS users can +`npm install -g kimetsu` without a Rust toolchain. npm ships the **same prebuilt +native binary** as the GitHub Release — it is not a reimplementation. + +## Layout + +``` +npm/ + kimetsu/ main package — committed source (launcher, no binaries) + bin/cli.js resolves the platform package and execs its binary + lib/embeddings.js on-demand embeddings download (KIMETSU_NPM_FLAVOR=embeddings) + package.json optionalDependencies -> the 4 @kimetsu/* platform packages + README.md + README.md this file +``` + +## How publishing works (esbuild / turbo style) + +Platform packages are **not committed** — binaries never live in git. They are +assembled and published entirely in CI (`.github/workflows/release.yml`, the +`publish-npm` job) from the **lean** release archives the `build` matrix already +produces: + +``` +@kimetsu/linux-x64 os:[linux] cpu:[x64] x86_64-unknown-linux-gnu +@kimetsu/darwin-x64 os:[darwin] cpu:[x64] x86_64-apple-darwin +@kimetsu/darwin-arm64 os:[darwin] cpu:[arm64] aarch64-apple-darwin +@kimetsu/win32-x64 os:[win32] cpu:[x64] x86_64-pc-windows-msvc +``` + +npm installs only the platform package whose `os`/`cpu` match the host; the +launcher `require.resolve`s its binary and execs it. No postinstall script — it +works under `npm install --ignore-scripts`. + +The embeddings build is larger and only supported on three targets, so it is +fetched on demand by the launcher when `KIMETSU_NPM_FLAVOR=embeddings` is set, +rather than shipped as a package. + +## Versioning + +The committed `kimetsu/package.json` carries a `0.0.0` sentinel. CI stamps the +real version (`${GITHUB_REF_NAME#v}`) into the main package and every +`@kimetsu/*` package + `optionalDependencies` entry at publish time, so npm +always matches the crates.io / GitHub Release for the same tag. The single +source of truth remains `Cargo.toml` `[workspace.package] version`. diff --git a/npm/kimetsu/README.md b/npm/kimetsu/README.md new file mode 100644 index 0000000..ab4a63b --- /dev/null +++ b/npm/kimetsu/README.md @@ -0,0 +1,54 @@ +# kimetsu + +A persistent memory **brain** sidecar for Claude Code and Codex. It accumulates +generalizable knowledge across sessions and retrieves it on demand. + +This npm package installs the prebuilt native `kimetsu` binary for your platform — +**no Rust toolchain required**. It's the same binary published on +[GitHub Releases](https://github.com/RodCor/kimetsu/releases) and via +`cargo install kimetsu-cli`. + +## Install + +```bash +npm install -g kimetsu +kimetsu --version +kimetsu doctor # checks paths, brain.db, embedder, MCP, bridge +``` + +npm downloads only the platform package that matches your OS/CPU +(`@kimetsu/linux-x64`, `@kimetsu/darwin-x64`, `@kimetsu/darwin-arm64`, +`@kimetsu/win32-x64`) via `optionalDependencies`. There is **no postinstall +download** — it works under `npm install --ignore-scripts`. + +### Semantic (embeddings) build + +The default install is the **lean** build: fast lexical (FTS) retrieval, no model +download. To opt into the semantic build (fastembed + ONNX; first run downloads +BGE-small), set `KIMETSU_NPM_FLAVOR=embeddings`: + +```bash +KIMETSU_NPM_FLAVOR=embeddings npm install -g kimetsu +``` + +With that env var set, the launcher fetches and caches the embeddings binary from +the matching GitHub Release on first run. Embeddings prebuilts exist for +**Linux x64, macOS Apple Silicon, and Windows x64** (the targets ONNX Runtime +ships prebuilts for); elsewhere the launcher falls back to the lean build. + +## Supported platforms + +| OS | Arch | Lean | Embeddings | +|---------------|-------|------|------------| +| Linux | x64 | ✅ | ✅ | +| macOS (Intel) | x64 | ✅ | ❌ | +| macOS (Apple) | arm64 | ✅ | ✅ | +| Windows | x64 | ✅ | ✅ | + +On unsupported platforms, install with `cargo install kimetsu-cli` or grab an +archive from the [releases page](https://github.com/RodCor/kimetsu/releases). + +## Links + +- Source & full docs: +- License: MIT OR Apache-2.0 diff --git a/npm/kimetsu/bin/cli.js b/npm/kimetsu/bin/cli.js new file mode 100644 index 0000000..e48ac96 --- /dev/null +++ b/npm/kimetsu/bin/cli.js @@ -0,0 +1,141 @@ +#!/usr/bin/env node +"use strict"; + +// Launcher for the `kimetsu` npm package. +// +// Kimetsu is a native Rust binary. npm ships it through per-platform +// optionalDependencies (@kimetsu/-) — npm installs only the +// one whose os/cpu match the host, and this launcher execs its binary, +// forwarding all args, stdio, and the exit code. +// +// The platform/arch -> target-triple mapping mirrors `release_target()`, +// `binary_name()`, and `embeddings_assets_supported()` in +// crates/kimetsu-cli/src/update.rs, so npm behaves identically to the +// built-in `kimetsu update` self-updater. + +const { spawnSync } = require("child_process"); + +// key = `${process.platform}-${process.arch}` +const PLATFORMS = { + "linux-x64": { + pkg: "@kimetsu/linux-x64", + target: "x86_64-unknown-linux-gnu", + bin: "kimetsu", + embeddings: true, + }, + "darwin-x64": { + pkg: "@kimetsu/darwin-x64", + target: "x86_64-apple-darwin", + bin: "kimetsu", + embeddings: false, // ort ships no prebuilt ONNX Runtime for x86_64 macOS + }, + "darwin-arm64": { + pkg: "@kimetsu/darwin-arm64", + target: "aarch64-apple-darwin", + bin: "kimetsu", + embeddings: true, + }, + "win32-x64": { + pkg: "@kimetsu/win32-x64", + target: "x86_64-pc-windows-msvc", + bin: "kimetsu.exe", + embeddings: true, + }, +}; + +const VERSION = require("../package.json").version; +const REPO_URL = "https://github.com/RodCor/kimetsu"; + +function fail(message) { + process.stderr.write(`kimetsu: ${message}\n`); + process.exit(1); +} + +function unsupportedPlatformMessage(key) { + return ( + `no prebuilt Kimetsu binary for ${key} (${process.platform}/${process.arch}).\n` + + `Prebuilt npm binaries cover: ${Object.keys(PLATFORMS).join(", ")}.\n` + + `Install another way:\n` + + ` - cargo install kimetsu-cli\n` + + ` - grab an archive from ${REPO_URL}/releases` + ); +} + +function missingOptionalDepMessage(entry) { + return ( + `the platform package ${entry.pkg} is not installed.\n` + + `This usually means npm skipped optional dependencies (e.g. --no-optional or\n` + + `--ignore-scripts on an older npm). Reinstall with optional deps enabled:\n` + + ` npm install -g kimetsu\n` + + `Or install another way: cargo install kimetsu-cli, or an archive from\n` + + `${REPO_URL}/releases` + ); +} + +function leanBinaryPath(entry) { + try { + return require.resolve(`${entry.pkg}/bin/${entry.bin}`); + } catch (_err) { + return null; + } +} + +async function resolveBinary() { + const key = `${process.platform}-${process.arch}`; + const entry = PLATFORMS[key]; + if (!entry) { + fail(unsupportedPlatformMessage(key)); + } + + const wantEmbeddings = + (process.env.KIMETSU_NPM_FLAVOR || "").toLowerCase() === "embeddings"; + + if (wantEmbeddings) { + if (!entry.embeddings) { + process.stderr.write( + `kimetsu: embeddings build is not available for ${entry.target}; ` + + `using the lean build.\n` + ); + } else { + try { + const { ensureEmbeddingsBinary } = require("../lib/embeddings"); + const embeddingsBin = await ensureEmbeddingsBinary({ + version: VERSION, + target: entry.target, + binName: entry.bin, + }); + if (embeddingsBin) return embeddingsBin; + } catch (err) { + process.stderr.write( + `kimetsu: could not obtain embeddings build (${err.message}); ` + + `falling back to the lean build.\n` + ); + } + } + } + + const lean = leanBinaryPath(entry); + if (!lean) { + fail(missingOptionalDepMessage(entry)); + } + return lean; +} + +async function main() { + const binary = await resolveBinary(); + const result = spawnSync(binary, process.argv.slice(2), { + stdio: "inherit", + windowsHide: true, + }); + + if (result.error) { + fail(`failed to launch ${binary}: ${result.error.message}`); + } + // Propagate a fatal signal as the conventional 128+signal exit code. + if (result.signal) { + process.exit(1); + } + process.exit(result.status === null ? 1 : result.status); +} + +main(); diff --git a/npm/kimetsu/lib/embeddings.js b/npm/kimetsu/lib/embeddings.js new file mode 100644 index 0000000..e4a08df --- /dev/null +++ b/npm/kimetsu/lib/embeddings.js @@ -0,0 +1,132 @@ +"use strict"; + +// On-demand embeddings build fetcher. +// +// optionalDependencies can only be selected by npm from os/cpu — it cannot be +// driven by an env var. So the lean build ships as the platform package, and +// the (larger) embeddings build is fetched on first use when the user opts in +// with KIMETSU_NPM_FLAVOR=embeddings. +// +// This mirrors the asset naming + download/extract flow in +// crates/kimetsu-cli/src/update.rs (download_asset / extract_binary) and the +// archive layout produced by .github/workflows/release.yml (an archive whose +// single top-level dir is `kimetsu---embeddings/`). + +const fs = require("fs"); +const os = require("os"); +const path = require("path"); +const https = require("https"); +const { execFileSync } = require("child_process"); + +const REPO = "RodCor/kimetsu"; + +function cacheRoot() { + if (process.platform === "win32") { + return process.env.LOCALAPPDATA || path.join(os.homedir(), "AppData", "Local"); + } + if (process.platform === "darwin") { + return path.join(os.homedir(), "Library", "Caches"); + } + return process.env.XDG_CACHE_HOME || path.join(os.homedir(), ".cache"); +} + +function downloadUrl(version, assetName) { + return `https://github.com/${REPO}/releases/download/v${version}/${assetName}`; +} + +// GitHub release downloads redirect (to objects.githubusercontent.com / S3). +function download(url, dest, redirectsLeft = 5) { + return new Promise((resolve, reject) => { + const req = https.get( + url, + { + headers: { + Accept: "application/octet-stream", + "User-Agent": `kimetsu-npm/${require("../package.json").version}`, + }, + }, + (res) => { + if ( + res.statusCode && + res.statusCode >= 300 && + res.statusCode < 400 && + res.headers.location + ) { + res.resume(); + if (redirectsLeft <= 0) { + reject(new Error("too many redirects")); + return; + } + resolve(download(res.headers.location, dest, redirectsLeft - 1)); + return; + } + if (res.statusCode !== 200) { + res.resume(); + reject(new Error(`download failed: HTTP ${res.statusCode} for ${url}`)); + return; + } + const file = fs.createWriteStream(dest); + res.pipe(file); + file.on("finish", () => file.close((err) => (err ? reject(err) : resolve()))); + file.on("error", reject); + } + ); + req.on("error", reject); + }); +} + +// bsdtar (`tar`) is present on Linux, macOS, and Windows 10 1803+, and handles +// both .tar.gz and .zip — so we shell out instead of bundling a tar/unzip dep. +function extract(archive, dest) { + fs.mkdirSync(dest, { recursive: true }); + execFileSync("tar", ["-xf", archive, "-C", dest], { stdio: "ignore" }); +} + +async function ensureEmbeddingsBinary({ version, target, binName }) { + const cacheDir = path.join( + cacheRoot(), + "kimetsu", + "npm", + `v${version}`, + `${target}-embeddings` + ); + const cachedBin = path.join(cacheDir, binName); + if (fs.existsSync(cachedBin)) return cachedBin; + + const ext = process.platform === "win32" ? "zip" : "tar.gz"; + const stem = `kimetsu-${version}-${target}-embeddings`; + const assetName = `${stem}.${ext}`; + + const workdir = fs.mkdtempSync(path.join(os.tmpdir(), "kimetsu-npm-")); + try { + const archivePath = path.join(workdir, assetName); + process.stderr.write(`kimetsu: fetching embeddings build (${assetName})…\n`); + await download(downloadUrl(version, assetName), archivePath); + + const extractDir = path.join(workdir, "extract"); + extract(archivePath, extractDir); + + // Archives wrap the binary in a top-level `/` directory. + const candidates = [ + path.join(extractDir, stem, binName), + path.join(extractDir, binName), + ]; + const extracted = candidates.find((p) => fs.existsSync(p)); + if (!extracted) { + throw new Error(`binary ${binName} not found inside ${assetName}`); + } + + fs.mkdirSync(cacheDir, { recursive: true }); + fs.copyFileSync(extracted, cachedBin); + if (process.platform !== "win32") fs.chmodSync(cachedBin, 0o755); + return cachedBin; + } finally { + try { + fs.rmSync(workdir, { recursive: true, force: true }); + } catch (_err) { + /* best effort */ + } + } +} + +module.exports = { ensureEmbeddingsBinary }; diff --git a/npm/kimetsu/package.json b/npm/kimetsu/package.json new file mode 100644 index 0000000..ee8d15c --- /dev/null +++ b/npm/kimetsu/package.json @@ -0,0 +1,42 @@ +{ + "name": "kimetsu", + "version": "0.0.0", + "description": "Kimetsu — a persistent memory brain sidecar for Claude Code and Codex. Installs the prebuilt native binary for your platform.", + "keywords": [ + "kimetsu", + "mcp", + "claude", + "codex", + "memory", + "rag", + "cli" + ], + "homepage": "https://github.com/RodCor/kimetsu#readme", + "bugs": { + "url": "https://github.com/RodCor/kimetsu/issues" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/RodCor/kimetsu.git", + "directory": "npm/kimetsu" + }, + "license": "MIT OR Apache-2.0", + "type": "commonjs", + "bin": { + "kimetsu": "bin/cli.js" + }, + "files": [ + "bin/cli.js", + "lib/embeddings.js", + "README.md" + ], + "engines": { + "node": ">=16" + }, + "optionalDependencies": { + "@kimetsu/linux-x64": "0.0.0", + "@kimetsu/darwin-x64": "0.0.0", + "@kimetsu/darwin-arm64": "0.0.0", + "@kimetsu/win32-x64": "0.0.0" + } +}