diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e67fde1..f93de51 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -24,9 +24,11 @@ jobs: - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 with: node-version: "24" + package-manager-cache: false - uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2.2.0 with: bun-version: "1.3.14" + - run: npm install --global --ignore-scripts npm@11.19.0 - run: bun install --frozen-lockfile --ignore-scripts - run: bun run check - run: | diff --git a/.github/workflows/npm-stage.yml b/.github/workflows/npm-stage.yml new file mode 100644 index 0000000..00d8999 --- /dev/null +++ b/.github/workflows/npm-stage.yml @@ -0,0 +1,163 @@ +name: Stage npm package + +on: + workflow_dispatch: + +permissions: + contents: read + +concurrency: + group: npm-stage + cancel-in-progress: false + +jobs: + verify: + name: Verify package + permissions: + contents: read + runs-on: ubuntu-latest + timeout-minutes: 35 + outputs: + archive_name: ${{ steps.artifact.outputs.archive_name }} + archive_sha512: ${{ steps.artifact.outputs.archive_sha512 }} + steps: + - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + with: + fetch-depth: 0 + persist-credentials: false + - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 + with: + node-version: "24" + package-manager-cache: false + registry-url: "https://registry.npmjs.org" + - uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2.2.0 + with: + bun-version: "1.3.14" + - name: Install staging-capable npm + run: npm install --global --ignore-scripts npm@11.19.0 + - name: Verify default-branch package identity + env: + DEFAULT_BRANCH: ${{ github.event.repository.default_branch }} + run: | + set -euo pipefail + if [[ "$GITHUB_REF" != "refs/heads/$DEFAULT_BRANCH" ]]; then + echo "::error::Dispatch this workflow from $DEFAULT_BRANCH" + exit 1 + fi + git fetch origin "$DEFAULT_BRANCH" + remote_head="$(git rev-parse "origin/$DEFAULT_BRANCH")" + if [[ "$GITHUB_SHA" != "$remote_head" ]]; then + echo "::error::Dispatch commit $GITHUB_SHA is not current $DEFAULT_BRANCH head $remote_head" + exit 1 + fi + package_name="$(node -p 'require("./package.json").name')" + package_version="$(node -p 'require("./package.json").version')" + if [[ "$package_name" != "@hraness/kb" ]]; then + echo "::error::Unexpected package name $package_name" + exit 1 + fi + if [[ ! "$package_version" =~ ^(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)$ ]]; then + echo "::error::Package version $package_version is not a stable semantic version" + exit 1 + fi + npm view "$package_name" name --json \ + --registry=https://registry.npmjs.org >/dev/null + version_error="$RUNNER_TEMP/npm-version-error.txt" + if npm view "$package_name@$package_version" version --json \ + --registry=https://registry.npmjs.org \ + >/dev/null 2>"$version_error"; then + echo "::error::$package_name@$package_version already exists" + exit 1 + fi + if ! grep -q 'E404' "$version_error"; then + cat "$version_error" + echo "::error::Could not prove that $package_name@$package_version is unpublished" + exit 1 + fi + - run: bun install --frozen-lockfile --ignore-scripts + - run: bun run check + - name: Require committed generated outputs + run: | + generated_status="$(git status --porcelain --untracked-files=all -- dist bun.lock)" + if [[ -n "$generated_status" ]]; then + printf '%s\n' "$generated_status" + exit 1 + fi + - name: Build and verify the exact npm artifact + id: artifact + run: | + set -euo pipefail + artifact_dir="$RUNNER_TEMP/npm-package" + metadata="$RUNNER_TEMP/npm-pack.json" + mkdir -p "$artifact_dir" + npm pack --json --ignore-scripts --pack-destination "$artifact_dir" > "$metadata" + cat "$metadata" + archive_name="$(node -e ' + const fs = require("node:fs"); + const value = JSON.parse(fs.readFileSync(process.argv[1], "utf8")); + if (!Array.isArray(value) || value.length !== 1 || typeof value[0]?.filename !== "string") process.exit(1); + process.stdout.write(value[0].filename); + ' "$metadata")" + archive="$artifact_dir/$archive_name" + bun run ./scripts/package-smoke.ts --archive "$archive" + archive_sha512="$(sha512sum "$archive" | cut -d ' ' -f 1)" + printf 'archive_name=%s\n' "$archive_name" >> "$GITHUB_OUTPUT" + printf 'archive_sha512=%s\n' "$archive_sha512" >> "$GITHUB_OUTPUT" + - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: npm-package-${{ github.sha }} + path: ${{ runner.temp }}/npm-package/${{ steps.artifact.outputs.archive_name }} + compression-level: 0 + if-no-files-found: error + retention-days: 7 + + stage: + name: Stage verified package + needs: verify + permissions: + contents: read + id-token: write + runs-on: ubuntu-latest + timeout-minutes: 10 + steps: + - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + with: + fetch-depth: 0 + persist-credentials: false + - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 + with: + node-version: "24" + package-manager-cache: false + registry-url: "https://registry.npmjs.org" + - name: Install staging-capable npm + run: npm install --global --ignore-scripts npm@11.19.0 + - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + name: npm-package-${{ github.sha }} + path: ${{ runner.temp }}/npm-package + - name: Stage verified package + env: + ARCHIVE_NAME: ${{ needs.verify.outputs.archive_name }} + ARCHIVE_SHA512: ${{ needs.verify.outputs.archive_sha512 }} + DEFAULT_BRANCH: ${{ github.event.repository.default_branch }} + run: | + set -euo pipefail + archive="$RUNNER_TEMP/npm-package/$ARCHIVE_NAME" + if [[ ! -f "$archive" || -L "$archive" ]]; then + echo "::error::Verified package archive is missing or linked" + exit 1 + fi + actual_sha512="$(sha512sum "$archive" | cut -d ' ' -f 1)" + if [[ "$actual_sha512" != "$ARCHIVE_SHA512" ]]; then + echo "::error::Downloaded package digest does not match the verified artifact" + exit 1 + fi + git fetch origin "$DEFAULT_BRANCH" + remote_head="$(git rev-parse "origin/$DEFAULT_BRANCH")" + if [[ "$GITHUB_SHA" != "$remote_head" ]]; then + echo "::error::Dispatch commit $GITHUB_SHA is no longer current $DEFAULT_BRANCH head $remote_head" + exit 1 + fi + npm stage publish "$archive" \ + --access public \ + --registry=https://registry.npmjs.org diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index c3afa77..c81d3b2 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -29,9 +29,12 @@ jobs: - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 with: node-version: "24" + package-manager-cache: false + registry-url: "https://registry.npmjs.org" - uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2.2.0 with: bun-version: "1.3.14" + - run: npm install --global --ignore-scripts npm@11.19.0 - name: Verify release identity id: identity env: @@ -69,6 +72,34 @@ jobs: - run: bun pm pack --dry-run --ignore-scripts - run: >- node --input-type=module -e 'const manifest = (await import("./package.json", { with: { type: "json" } })).default; await Promise.all(Object.values(manifest.exports).map(({ import: path }) => import(path)))' + - name: Verify published npm artifact + run: | + set -euo pipefail + package_name="$(node -p 'require("./package.json").name')" + package_version="$(node -p 'require("./package.json").version')" + source_dir="$RUNNER_TEMP/npm-source" + registry_dir="$RUNNER_TEMP/npm-registry" + source_metadata="$RUNNER_TEMP/npm-source.json" + registry_metadata="$RUNNER_TEMP/npm-registry.json" + mkdir -p "$source_dir" "$registry_dir" + npm pack --json --ignore-scripts --pack-destination "$source_dir" > "$source_metadata" + npm pack --json --ignore-scripts --pack-destination "$registry_dir" \ + --registry=https://registry.npmjs.org \ + "$package_name@$package_version" > "$registry_metadata" + registry_archive="$(node -e ' + const fs = require("node:fs"); + const [sourcePath, registryPath] = process.argv.slice(1); + const source = JSON.parse(fs.readFileSync(sourcePath, "utf8")); + const registry = JSON.parse(fs.readFileSync(registryPath, "utf8")); + if (!Array.isArray(source) || source.length !== 1 || !Array.isArray(registry) || registry.length !== 1) process.exit(1); + if (typeof source[0]?.integrity !== "string" || source[0].integrity !== registry[0]?.integrity) { + console.error("::error::Published npm tarball differs from the checked source artifact"); + process.exit(1); + } + if (typeof registry[0]?.filename !== "string") process.exit(1); + process.stdout.write(registry[0].filename); + ' "$source_metadata" "$registry_metadata")" + bun run ./scripts/package-smoke.ts --archive "$registry_dir/$registry_archive" publish: name: Publish diff --git a/AGENTS.md b/AGENTS.md index a15b986..ad6c317 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -9,7 +9,7 @@ - `kb/` – this source repository's authored rationale, maintained synthesis, and implementation plans; it is separate from the package's graph implementation and fixtures. - `WRITING.md` and `STYLE.md` – internal and public prose contracts. - `docs/` – design, capture, and agent-workflow documentation. -- `.github/workflows/` – read-only branch validation and checks-gated immutable GitHub Release automation. +- `.github/workflows/` – read-only branch validation, manually dispatched stage-only npm publication, and checks-gated immutable GitHub Release automation. - `portfolio-inventory.json`, `scripts/check-portfolio-inventory.ts`, and `scripts/check-installed-command-docs.ts` – canonical public package inventory and standalone public-command consistency gates. - `README.md`, `CONTRIBUTING.md`, `SECURITY.md`, and `LICENSE` – public usage, project policy, threat model, and terms. - `package.json`, `tsconfig.json`, and `bun.lock` – standalone package and frozen verification configuration. @@ -50,4 +50,5 @@ - Keep `portfolio-inventory.json` byte-canonical and consistent with the public package identity, version, repository, direct `@hraness/*` dependency edges, and Hraness-owned dependencies pinned by exact immutable GitHub specifiers. - Pair concrete behavior tests with property tests for parsing, resolution, ordering, path confinement, and round-trip laws. - Run `bun test src/benchmark.test.ts src/evaluation.test.ts src/evaluation-kb.test.ts src/search.test.ts src/sdk.test.ts` when changing rank fusion, retrieval defaults, frozen-corpus execution, or built-in evaluation adapters. The six-case synthetic rank-fusion fixture is a deterministic regression, not a retrieval-quality or performance benchmark. Keep real-corpus manifests versioned, judgments independent of rankings, raw lane evidence intact, and performance claims tied to named hardware and measured runs. Run `bun run check` before handing off a change; it must leave committed `dist/` and `bun.lock` unchanged. -- Treat a `v*` tag as a release request, not a completed release. Before tagging, confirm repository-level immutable releases are enabled; use a strictly increasing stable package version, keep the tag equal to `v` on `main`, and let the read-only verification job complete before its write-scoped publisher creates the Release. Do not create the next tag until that workflow and Release are verified because GitHub concurrency is not a durable queue. After tagging, verify the matching non-draft immutable Release is Latest. +- Follow `docs/publishing.md` for the interactive npm bootstrap and later releases. After the package exists, trust only `.github/workflows/npm-stage.yml` with `npm stage publish` permission, disallow traditional publishing tokens, inspect the staged tarball, and approve it with 2FA. Preserve `contentPolicy.class=dual-use` and the root `DISCLOSURE` in every published version. +- Treat a `v*` tag as a release request, not a completed release. Publish the exact npm version first. Before tagging, confirm repository-level immutable releases are enabled; use a strictly increasing stable package version, keep the tag equal to `v` on `main`, and let the read-only verification job compare the public npm tarball with the checked source before its write-scoped publisher creates the Release. Do not create the next tag until that workflow and Release are verified because GitHub concurrency is not a durable queue. After tagging, verify the matching non-draft immutable Release is Latest. diff --git a/DISCLOSURE b/DISCLOSURE new file mode 100644 index 0000000..38280ad --- /dev/null +++ b/DISCLOSURE @@ -0,0 +1,43 @@ +# Dual-use functionality disclosure + +`@hraness/kb` is a local-first Markdown knowledge-base and source-capture tool. +Its intended use is to preserve research, plans, decisions, and sources that the +operator is authorized to read. + +The package includes security-relevant capabilities that can be used for both +legitimate and harmful purposes: + +- It can read cookies from a local browser profile or a user-supplied cookie + file when the operator explicitly selects that source. +- It can read an attached Chrome or Chromium tab, or use an owned browser + session, to capture public or signed-in content. +- It can make bounded network requests, follow validated redirects, download + selected assets, and query fixed metadata and archive providers. +- It can invoke bounded local browser, media, PDF, OCR, Git, Rust, and search + subprocesses when the operator requests a feature that needs them. +- It writes captured Markdown, metadata, evidence, and localized assets to a + caller-selected local vault. + +Use these capabilities only for public content or content that you are entitled +and permitted to automate. Do not use this package to bypass authentication, +paywalls, CAPTCHAs, rate limits, DRM, audience controls, platform rules, or any +other access restriction. Do not use it to access another person's private +data. + +Cookie-backed capture reads a selected store and keeps matching cookies in +memory, except for a short-lived mode-0600 cookie jar used by the optional +yt-dlp path. Path-backed browser profiles are copied to a temporary directory; +the source profile is not modified. Attached browser sessions retain their own +network behavior. Current-tab capture does not navigate, click, type, upload, +or submit. URL-based rendered capture can navigate and scroll within fixed +limits. + +Captured pages, screenshots, cookies, source evidence, and terminal output can +contain credentials, private text, account names, or personal data. Review +every authenticated capture before committing, sharing, or processing it with +another service. The package applies path, network, resource, redaction, and +atomic-write controls, but those controls do not grant authorization or make +hostile content trustworthy. + +Report suspected vulnerabilities through GitHub private vulnerability +reporting at https://github.com/hraness/kb/security/advisories/new. diff --git a/README.md b/README.md index 3278043..0e7f910 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@ a knowledge base for coding agents. ## install ```sh -bun add --global github:hraness/kb#v0.17.0 +bun add --global @hraness/kb@0.17.1 ``` ## about @@ -165,6 +165,14 @@ Start with a short inherited `AGENTS.md` path for rules whose omission would mak Treat the knowledge base as repository-adjacent durable memory. Authored Markdown and Git are the record; catalogs, indexes, embeddings, and graph views are replaceable ways to find and inspect it. Checks can validate structure, captures can preserve a selected surface, and similarity can suggest candidates. None of those mechanisms proves that a source is trustworthy or an explanation is still true. People and agents must revise the knowledge as the repository changes. +## Upgrade to v0.17.1 + +Version 0.17.1 adds the public `@hraness/kb` npm installation path without +changing the runtime API introduced in 0.17.0. Bun `1.3.14` or newer is now an +explicit package requirement. Consumers should review the package's declared +dual-use capture boundary and the lifecycle scripts used by optional browser +and native search adapters before enabling those scripts. + ## Upgrade to v0.17.0 Version 0.17 adds selected portfolio federation, stable note identities, @@ -200,8 +208,8 @@ Copy this prompt into Codex, Claude Code, or another coding agent: ```text Install the `kb` Agent Skill from hraness/kb with the standard skills CLI. Use -the skill's runtime instructions to install the `kb` CLI from the immutable -v0.17.0 release only when the command is missing. Verify it with `kb doctor` +the skill's runtime instructions to install the exact `@hraness/kb@0.17.1` +registry release only when the command is missing. Verify it with `kb doctor` and `kb --help`, but do not initialize or modify a vault until I ask. ``` @@ -216,30 +224,83 @@ Both commands discover the same `kb` skill and install it into the selected agent runner. Skill installation is inert: it does not initialize a vault, refresh a catalog, or edit Markdown. When invoked, the skill uses an existing `kb` command or, when the command is missing, checks for Bun and installs the -CLI from the immutable `v0.17.0` tag. +CLI from the immutable `@hraness/kb@0.17.1` npm version. The public skills CLI reads `skills/kb/` from the repository. The immutable -`v0.17.0` package includes the same tree under +`0.17.1` npm package includes the same tree under `node_modules/@hraness/kb/skills/kb/`, and the package check verifies that the installed skill is byte-identical to the repository source. -Install the CLI from the immutable `v0.17.0` tag: +Install the two global commands with Bun: + +```sh +bun add --global @hraness/kb@0.17.1 +kb --help +kb-evaluation-builder --help +``` + +The same registry package can be installed with npm: ```sh -bun add --global github:hraness/kb#v0.17.0 +npm install --global --ignore-scripts @hraness/kb@0.17.1 kb --help ``` -For programmatic use, declare the same pinned source in a project: +Both commands are Bun executables. Bun `1.3.14` or newer must remain in `PATH` +even when npm performs the global installation. The conservative npm command +above disables dependency lifecycle scripts. Optional native search and +rendered-browser setup remain unavailable until the relevant scripts are +reviewed and enabled; run `kb doctor` to inspect the resulting capabilities. + +For programmatic use, add the exact npm version to a Bun project: + +```sh +bun add --exact @hraness/kb@0.17.1 +``` + +The resulting dependency should remain exact: ```json { "dependencies": { - "@hraness/kb": "github:hraness/kb#v0.17.0" + "@hraness/kb": "0.17.1" } } ``` +Version 0.17.1 intentionally retains two public GitHub dependencies: +`@steipete/sweet-cookie` at Hraness release `v0.4.2` for the cookie-scope safety +fork, and `@tobilu/qmd` at commit +`aa993dceb3ef8cfb71d470554ca437570f5a2b3c` for store-local model behavior. A +registry installation therefore needs Git and public GitHub access while it +resolves those dependencies. They remain part of this release's supported +installation contract until equivalent registry releases are available. + +### Review lifecycle scripts before enabling optional adapters + +[Bun blocks dependency lifecycle scripts](https://bun.sh/docs/pm/lifecycle) +unless the consumer trusts them. Run +`bun pm untrusted` in the consuming project and inspect the exact resolved +versions and scripts before allowing any of them. Do not use `bun pm trust +--all` for this package's dependency graph. + +The pinned QMD Git dependency has a `prepare` script that installs development +hooks only when its own `.git` directory exists; the packaged runtime does not +need that script. Optional rendered capture uses `agent-browser`, whose +postinstall downloads a platform-specific executable. QMD's native semantic +and language-parser paths can report lifecycle scripts for `node-llama-cpp`, +`tree-sitter-go`, `tree-sitter-javascript`, `tree-sitter-python`, and +`tree-sitter-rust`. Trust only the packages required by the capability you have +chosen, then reinstall and run `kb doctor` to verify that capability. npm runs +dependency lifecycle scripts by default, so inspect the same packages before +omitting `--ignore-scripts` from an npm installation. + +KB follows [npm's dual-use content +policy](https://docs.npmjs.com/policies/dual-use/) because it can read +explicitly selected signed-in browser state and perform bounded capture and +network operations. Read [`DISCLOSURE`](DISCLOSURE) and the [security +policy](SECURITY.md) before using authenticated capture. + Contributors can install from a checkout instead: ```sh diff --git a/bun.lock b/bun.lock index eb99b98..8e0a1e5 100644 --- a/bun.lock +++ b/bun.lock @@ -20,7 +20,7 @@ }, }, "packages": { - "@hono/node-server": ["@hono/node-server@1.19.14", "", { "peerDependencies": { "hono": "^4" } }, "sha512-GwtvgtXxnWsucXvbQXkRgqksiH2Qed37H9xHZocE5sA3N8O8O8/8FA3uclQXxXVzc9XBZuEOMK7+r02FmSpHtw=="], + "@hono/node-server": ["@hono/node-server@1.19.17", "", { "peerDependencies": { "hono": "^4" } }, "sha512-dSneS5qhiauZWGDCeK4o695Xd9nUNjviSZCMQrj10eetr8Uln1ucn6bbphOM6UynAMMtNIzZNSpL9vnASJwrPQ=="], "@huggingface/jinja": ["@huggingface/jinja@0.5.9", "", {}, "sha512-uWTG+l3VJRsl7EXxYizuL3P+cCPoc3cRqbWWRcQN0FhejRfbdq0RNhCmbY/YDtnTcz9icdLYuLDjsnz4d8JMuw=="], @@ -94,11 +94,11 @@ "@tobilu/qmd": ["@tobilu/qmd@github:hraness/qmd#aa993dc", { "dependencies": { "@modelcontextprotocol/sdk": "1.29.0", "better-sqlite3": "12.10.0", "fast-glob": "3.3.3", "node-llama-cpp": "3.18.1", "picomatch": "4.0.4", "sqlite-vec": "0.1.9", "tree-sitter-go": "0.25.0", "tree-sitter-python": "0.25.0", "tree-sitter-rust": "0.24.0", "tree-sitter-typescript": "0.23.2", "web-tree-sitter": "0.26.8", "yaml": "2.9.0", "zod": "4.2.1" }, "optionalDependencies": { "sqlite-vec-darwin-arm64": "0.1.9", "sqlite-vec-darwin-x64": "0.1.9", "sqlite-vec-linux-arm64": "0.1.9", "sqlite-vec-linux-x64": "0.1.9", "sqlite-vec-windows-x64": "0.1.9" }, "peerDependencies": { "typescript": "^5.9.3" }, "bin": { "qmd": "bin/qmd" } }, "hraness-qmd-aa993dc", "sha512-8vwNE/U5SWSscMokgqRHsdDnTT6V5wy7rsrvn3mw5vi5qdXesiIb1fb5qCsjkWSwiZhTbx0d1r+zGfzIO/T2jw=="], - "@types/bun": ["@types/bun@1.3.14", "", { "dependencies": { "bun-types": "1.3.14" } }, "sha512-h1hFqFVcvAvD9j9K7ZW7vd82aSA+rTdznZa+5bwvCwqSB1jmmfLcbIWhOLx1/+boy/xmjgCs/OMUL8hRJSmnPw=="], + "@types/bun": ["@types/bun@1.4.0", "", { "dependencies": { "bun-types": "1.4.0" } }, "sha512-K+lZULY23vRgK/CfTjFIV+tyifaNdSMlPh9j+6mQ/cLfpOznLyAuzgV/JQysyECpkBQLVMSyvjlr2fBUSA9wFQ=="], - "@types/node": ["@types/node@26.1.1", "", { "dependencies": { "undici-types": "~8.3.0" } }, "sha512-nxAkRSVkN1Y0JC1W8ky/fTfkGsMmcrRsbx+3XoZE+rMOX71kLYTV7fLXpqud1GpbpP5TuffXFqfX7fH2GgZREw=="], + "@types/node": ["@types/node@26.4.0", "", { "dependencies": { "undici-types": "~8.3.0" } }, "sha512-faiGnoIrLH/V8cibOMEAZ8pMw6oXqSukl29ra4mN8GdaB2ZewzeaLj+INpV5N+Z1eKWzY+IzaIZH2EIR6YZRNQ=="], - "@xmldom/xmldom": ["@xmldom/xmldom@0.9.10", "", {}, "sha512-A9gOqLdi6cV4ibazAjcQufGj0B1y/vDqYrcuP6d/6x8P27gRS8643Dj9o1dEKtB6O7fwxb2FgBmJS2mX7gpvdw=="], + "@xmldom/xmldom": ["@xmldom/xmldom@0.9.12", "", {}, "sha512-5AXjrcMClTryPe9LgZrygpB1lj7s0S9E0+W+AHaVKAVyHanafK86iPSvG5xHVSp/jC+VH1UXu0TAEmY279xH7A=="], "accepts": ["accepts@2.0.0", "", { "dependencies": { "mime-types": "^3.0.0", "negotiator": "^1.0.0" } }, "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng=="], @@ -110,7 +110,7 @@ "ansi-escapes": ["ansi-escapes@6.2.1", "", {}, "sha512-4nJ3yixlEthEJ9Rk4vPcdBRkZvQZlYyu8j4/Mqz5sgIkddmEnH2Yj2ZrnP9S3tQOvSNRUIgVNF/1yPpRAGNRig=="], - "ansi-regex": ["ansi-regex@6.2.2", "", {}, "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg=="], + "ansi-regex": ["ansi-regex@6.3.0", "", {}, "sha512-WpDfL7NO6j7tH88IDBNVdUJxDh9nmCteAVW9dsep846XdwF4naCBK+/tGLX3KJgcpgMRXCFlTM2hKGoK9FsdrQ=="], "ansi-styles": ["ansi-styles@6.2.3", "", {}, "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg=="], @@ -132,7 +132,7 @@ "buffer": ["buffer@5.7.1", "", { "dependencies": { "base64-js": "^1.3.1", "ieee754": "^1.1.13" } }, "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ=="], - "bun-types": ["bun-types@1.3.14", "", { "dependencies": { "@types/node": "*" } }, "sha512-4N0ig0fEomHt5R0KCFWjovxow98rIoRwKolrYdCcknNwMekCXRnWEUvgu5soYV8QXtVsrUD8B95MBOZGPvr6KQ=="], + "bun-types": ["bun-types@1.4.0", "", { "dependencies": { "@types/node": "*" } }, "sha512-iIKw23BspnQQYd3prITOBxeUsxBHnwzX6YJfGMuNOZzeNcMmVqzIIVGRm1l69ogaPQmb4wB6BN8mA5bE9YuC5Q=="], "bytes": ["bytes@3.1.2", "", {}, "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg=="], @@ -230,13 +230,13 @@ "eventsource": ["eventsource@3.0.7", "", { "dependencies": { "eventsource-parser": "^3.0.1" } }, "sha512-CRT1WTyuQoD771GW56XEZFQ/ZoSfWid1alKGDYMmkt2yl8UXrVR4pspqWNEcqKvVIzg6PAltWjxcSSPrboA4iA=="], - "eventsource-parser": ["eventsource-parser@3.1.0", "", {}, "sha512-kJezFj9YFAMLeORyi7aCLxLbD5/qWMQnoMVlVPyHIll7lgRJCc3JVln9Vgl9nwQi0YkMnhdGTMNn7CkRRAptMg=="], + "eventsource-parser": ["eventsource-parser@3.1.1", "", {}, "sha512-EKN1vKAMcZ8MlYMpaNuxN6R9yakzH6uajHcHVTqWJzvu5pWw9DyhbP35HH8MVBQ+dZjAfDxk+A8NiR9KWaXiyQ=="], "expand-template": ["expand-template@2.0.3", "", {}, "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg=="], "express": ["express@5.2.1", "", { "dependencies": { "accepts": "^2.0.0", "body-parser": "^2.2.1", "content-disposition": "^1.0.0", "content-type": "^1.0.5", "cookie": "^0.7.1", "cookie-signature": "^1.2.1", "debug": "^4.4.0", "depd": "^2.0.0", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "etag": "^1.8.1", "finalhandler": "^2.1.0", "fresh": "^2.0.0", "http-errors": "^2.0.0", "merge-descriptors": "^2.0.0", "mime-types": "^3.0.0", "on-finished": "^2.4.1", "once": "^1.4.0", "parseurl": "^1.3.3", "proxy-addr": "^2.0.7", "qs": "^6.14.0", "range-parser": "^1.2.1", "router": "^2.2.0", "send": "^1.1.0", "serve-static": "^2.2.0", "statuses": "^2.0.1", "type-is": "^2.0.1", "vary": "^1.1.2" } }, "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw=="], - "express-rate-limit": ["express-rate-limit@8.6.0", "", { "dependencies": { "debug": "^4.4.3", "ip-address": "^10.2.0" }, "peerDependencies": { "express": ">= 4.11" } }, "sha512-XKJXDsASUOo0LLtFwW5hCcQGH0N4WQc/Rn8/Pvoia+TJFOkkFPvrtW9lZOeeNcxQJspvOIERMwiRLsVFlhHEkA=="], + "express-rate-limit": ["express-rate-limit@8.6.2", "", { "dependencies": { "debug": "^4.4.3", "ip-address": "^10.2.0" }, "peerDependencies": { "express": ">= 4.11" } }, "sha512-YH4ru+eOJxQABscKFfRCy9R7x9QFGdezclVMwwgFFndzS2Xnm0uo6B0ABZsLhcpeptGv2qvuJVWlQr9gQZoC3A=="], "fast-check": ["fast-check@4.9.0", "", { "dependencies": { "pure-rand": "^8.0.0" } }, "sha512-7ms6T7SybUev/PQITciI0yLM2pOSFy5zpG8Ty7tQofcVaQUvrMXp6CBwqF6fThLCLOrfBtuHAtwq6Yu4XPCllg=="], @@ -244,7 +244,7 @@ "fast-glob": ["fast-glob@3.3.3", "", { "dependencies": { "@nodelib/fs.stat": "^2.0.2", "@nodelib/fs.walk": "^1.2.3", "glob-parent": "^5.1.2", "merge2": "^1.3.0", "micromatch": "^4.0.8" } }, "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg=="], - "fast-uri": ["fast-uri@3.1.4", "", {}, "sha512-8JnbkQ4juDyvYs4mgFGQqg4yCYtFDtUtmp2QIQq11ZZe5CFQ5wcqm1rqDgAh/QdMySuBnPzMUiJUNZG5N/AiQw=="], + "fast-uri": ["fast-uri@3.1.6", "", {}, "sha512-7Ical1vFEMr0onbVzEDIreM22I4khW+fzyQPwvAFWBp1iwdshSZRsL4jjRvPG9JP1uiqMHRto+YU6R2/CzDz5Q=="], "fastq": ["fastq@1.20.1", "", { "dependencies": { "reusify": "^1.0.4" } }, "sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw=="], @@ -264,7 +264,7 @@ "fs-constants": ["fs-constants@1.0.0", "", {}, "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow=="], - "fs-extra": ["fs-extra@11.3.6", "", { "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", "universalify": "^2.0.0" } }, "sha512-w8ZNZr2mKIc7qeNaQ9AVPT1+iFaI+Avd4xudVOvdDJ8VytREi1Ft5Ih7hd9jjehod8vAM5GMsfQ/TpPf4EyoEA=="], + "fs-extra": ["fs-extra@11.4.0", "", { "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", "universalify": "^2.0.0" } }, "sha512-EQsFzMUJkCKGr1ePqlYADkIUmHW1s3ZXr5Yqy6wbGrfUCphpl2maM/kyOIRA2HpP3AaFQTZXD4ldjek+nccddA=="], "function-bind": ["function-bind@1.1.2", "", {}, "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA=="], @@ -288,7 +288,7 @@ "hasown": ["hasown@2.0.4", "", { "dependencies": { "function-bind": "^1.1.2" } }, "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A=="], - "hono": ["hono@4.12.31", "", {}, "sha512-zJIHFrl6bq3RDd2YusFNCDlM8qUprxKswyi/OPzPyzKDdyBXDqWx8bZlZ7R+saTdSTatUmb3O7K4SspGPaEOQg=="], + "hono": ["hono@4.13.5", "", {}, "sha512-O6+/eCYRkzzzy0rPWwKLiGBR1nFuUPZynnwjxN1MBA62NNqbT0wQEzQyK2gSO5yDIDB336sXQleAhOHrzlYyKw=="], "html-escaper": ["html-escaper@3.0.3", "", {}, "sha512-RuMffC89BOWQoY0WKGpIhn5gX3iI54O6nRA0yC124NYVtzjmFWBIiFd8M0x+ZdX0P9R4lADg1mgP8C7PxGOWuQ=="], @@ -306,7 +306,7 @@ "ini": ["ini@1.3.8", "", {}, "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew=="], - "ip-address": ["ip-address@10.2.0", "", {}, "sha512-/+S6j4E9AHvW9SWMSEY9Xfy66O5PWvVEJ08O0y5JGyEKQpojb0K0GKpz/v5HJ/G0vi3D2sjGK78119oXZeE0qA=="], + "ip-address": ["ip-address@10.5.0", "", {}, "sha512-R5SnVLJmgYYvf2F2ZgwSBnelz5G4q5AxIC277GDfUaNbrZKNANcBC7RHqYYePlszf4kBolVkJauG0ZjHHFh55g=="], "ipaddr.js": ["ipaddr.js@1.9.1", "", {}, "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g=="], @@ -328,7 +328,7 @@ "isexe": ["isexe@4.0.0", "", {}, "sha512-FFUtZMpoZ8RqHS3XeXEmHWLA4thH+ZxCv2lOiPIn1Xc7CxrqhWzNSDzD+/chS/zbYezmiwWLdQC09JdQKmthOw=="], - "jose": ["jose@6.2.4", "", {}, "sha512-N8acGzVsQy6M/fjFcxtysNc4Q379TcM5dM/qKkNtsHFji88yANnXTr7BLeP75iPnFwBfQzM/jg2BZ9+HZrHCZA=="], + "jose": ["jose@6.2.10", "", {}, "sha512-iiW7J9qRFlGxvCOIBDBDxFePQSn7ZMAnrYGhrrOo6siO/MIqwfyilLR27pkfDgUk+raLuzADS8A3S/KLBisc0g=="], "json-schema-traverse": ["json-schema-traverse@1.0.0", "", {}, "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="], @@ -336,7 +336,7 @@ "jsonfile": ["jsonfile@6.2.1", "", { "dependencies": { "universalify": "^2.0.0" }, "optionalDependencies": { "graceful-fs": "^4.1.6" } }, "sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q=="], - "lifecycle-utils": ["lifecycle-utils@3.1.1", "", {}, "sha512-gNd3OvhFNjHykJE3uGntz7UuPzWlK9phrIdXxU9Adis0+ExkwnZibfxCJWiWWZ+a6VbKiZrb+9D9hCQWd4vjTg=="], + "lifecycle-utils": ["lifecycle-utils@3.2.0", "", {}, "sha512-GzzsnzJ2IwXgLpKaQmoByaE0GzTO3MU/cxZQo9pwWx6FTTZbDv73RK2B5PCbiSA2STxxRBy3lQXYmeZ4ZDNFgQ=="], "linkedom": ["linkedom@0.18.13", "", { "dependencies": { "css-select": "^7.0.0", "cssom": "^0.5.0", "html-escaper": "^3.0.3", "htmlparser2": "^10.1.0", "uhyphen": "^0.2.0" }, "peerDependencies": { "canvas": ">= 2" }, "optionalPeers": ["canvas"] }, "sha512-ES/o9qotMpzpN2MHs+Iq/JcVoOj8Fa5wiQYrTdFpvAnwXL0g66XHHUc9WUMk6nAlBtGsFQ24ne+SYnvnaQ2FSw=="], @@ -350,7 +350,7 @@ "mathml-to-latex": ["mathml-to-latex@1.8.0", "", { "dependencies": { "@xmldom/xmldom": "^0.9.10" } }, "sha512-gQ0uK3zqB8HwlfaXJkEL5rgaZNbKUiBMmBP/B/W+b+t6KcseLSuYb1b0BjLgS9ZiQa24ePkqTX8/6FaQuDL7wQ=="], - "media-typer": ["media-typer@1.1.0", "", {}, "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw=="], + "media-typer": ["media-typer@1.1.1", "", {}, "sha512-yz3xRaG20c6/BOzvYoDaGtPmGscs7YivItZEEqe6GbwNfHuxu9YNmvnEkMzKldAGY4/80pRcQRZSEnhquk9XuQ=="], "merge-descriptors": ["merge-descriptors@2.0.0", "", {}, "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g=="], @@ -380,11 +380,11 @@ "napi-build-utils": ["napi-build-utils@2.0.0", "", {}, "sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA=="], - "negotiator": ["negotiator@1.0.0", "", {}, "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg=="], + "negotiator": ["negotiator@1.1.0", "", { "dependencies": { "content-type": "^2.1.0" } }, "sha512-NMPBRMJgiQHjbd8phG3Vebdx4kZ1H121rbl5IkMqeOsahptB9BKo/d7oJ3zTXqTgagn2bWlNSXkh0QUGM31RYg=="], - "node-abi": ["node-abi@3.94.0", "", { "dependencies": { "semver": "^7.3.5" } }, "sha512-W5ZNO5KRPB5TkYmGVD9F6YqhsglXJzE6etpbmT+f6EQElhiX/UTG551cnsRGvLG3fyZEg9HwaDmNmj5nwJ4z9g=="], + "node-abi": ["node-abi@3.95.0", "", { "dependencies": { "semver": "^7.3.5" } }, "sha512-T9iGctuocf0qIWFFOTxPzjT5q0SILqaBYXt272tlBHvTKC5+3JnkMirLxNJNkXHtFyBjU2Jx+NL4Zipr0B/c6Q=="], - "node-addon-api": ["node-addon-api@8.9.0", "", {}, "sha512-ekZMeaaIzSQTSpr7X2X3iJM7lTzgnx8ahAG9pJfT/7+14mlEM8ZYQ9cgCDvSSRbReFK0oHli3WrZdCiRsgAT9Q=="], + "node-addon-api": ["node-addon-api@8.9.2", "", {}, "sha512-VijLXbi3UACN69I0JVXJsX4tjACjNoQDgv2gTF6sx2wWEi8tkSg2eX8p5gSIFi8z2+DL3oHmY6OyKce38SDolg=="], "node-api-headers": ["node-api-headers@1.9.0", "", {}, "sha512-2oNILP4jXwRB4ywnYKjVk1YyJ96n2D4EOVJO6S3oYZ5PtbJrw3Yt9TpAuX3nBLMuzn74rnfGQrv13pS9vC+YiA=="], @@ -522,13 +522,13 @@ "strip-json-comments": ["strip-json-comments@2.0.1", "", {}, "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ=="], - "tar": ["tar@7.5.21", "", { "dependencies": { "@isaacs/fs-minipass": "^4.0.0", "chownr": "^3.0.0", "minipass": "^7.1.2", "minizlib": "^3.1.0", "yallist": "^5.0.0" } }, "sha512-XdhtCvlMywwxpCW8YEq3lOXBJpUPTR2OHHcwLPO3HwsJqOHa2Ok/oJ7ruGzp+JrKoRPVCzJwAdEjqLW/vNRPHA=="], + "tar": ["tar@7.5.22", "", { "dependencies": { "@isaacs/fs-minipass": "^4.0.0", "chownr": "^3.0.0", "minipass": "^7.1.2", "minizlib": "^3.1.0", "yallist": "^5.0.0" } }, "sha512-MFO/QzvtAOmJbkhOaCTvbGcFN9L9b+JunIsDwaKljSOdcLMea3NJ1k9Usz/rjdfSXTq4dfzfeS7W4p4YOAAHeA=="], "tar-fs": ["tar-fs@2.1.5", "", { "dependencies": { "chownr": "^1.1.1", "mkdirp-classic": "^0.5.2", "pump": "^3.0.0", "tar-stream": "^2.1.4" } }, "sha512-OboTd8mmMhZDNPV+UjQcK9yKAatXu2aJ+r1w4im1Otd4M4fl2hwvdoXUxIYHFTHWK/3y3FarBP70v3vwmGlOxw=="], "tar-stream": ["tar-stream@2.2.0", "", { "dependencies": { "bl": "^4.0.3", "end-of-stream": "^1.4.1", "fs-constants": "^1.0.0", "inherits": "^2.0.3", "readable-stream": "^3.1.1" } }, "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ=="], - "temml": ["temml@0.13.3", "", {}, "sha512-GLNEdf5qBWux3adbOxFus4jlds8nCdEIkkKq99m/4GGTfqnsjlVlK/i371Ux7yYSg/WNmOyAkNT/GJlZoJ0v+w=="], + "temml": ["temml@0.13.4", "", {}, "sha512-k1yolMBswx34Jw9hZn5Xh2/GBlwqlW+wiN7QaUYUMhSa1ypfti6rlCfSmCxWyooxH1G32C/Z+qVvgAsBOELZBQ=="], "to-regex-range": ["to-regex-range@5.0.1", "", { "dependencies": { "is-number": "^7.0.0" } }, "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ=="], @@ -586,17 +586,15 @@ "yargs-parser": ["yargs-parser@21.1.1", "", {}, "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw=="], - "yoctocolors": ["yoctocolors@2.1.2", "", {}, "sha512-CzhO+pFNo8ajLM2d2IW/R93ipy99LWjtwblvC1RsoSUMZgyLbYFr221TnSNT7GjGdYui6P459mw9JH/g/zW2ug=="], + "yoctocolors": ["yoctocolors@2.2.0", "", {}, "sha512-xYqdZFUK/VYazNl/oCDYN+3WloWQwMfZxBoiNt6qNyk+xfOdi598muWE42rNZFp1kNOiqW936q5RhUdnpqElSg=="], "zod": ["zod@4.4.3", "", {}, "sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ=="], "zod-to-json-schema": ["zod-to-json-schema@3.25.2", "", { "peerDependencies": { "zod": "^3.25.28 || ^4" } }, "sha512-O/PgfnpT1xKSDeQYSCfRI5Gy3hPf91mKVDuYLUHZJMiDFptvP41MSnWofm8dnCm0256ZNfZIM7DSzuSMAFnjHA=="], - "@modelcontextprotocol/sdk/zod": ["zod@4.2.1", "", {}, "sha512-0wZ1IRqGGhMP76gLqz8EyfBXKk0J2qo2+H3fi4mcUP/KtTocoX08nmIAHl1Z2kJIZbZee8KOpBCSNPRgauucjw=="], - "@tobilu/qmd/zod": ["zod@4.2.1", "", {}, "sha512-0wZ1IRqGGhMP76gLqz8EyfBXKk0J2qo2+H3fi4mcUP/KtTocoX08nmIAHl1Z2kJIZbZee8KOpBCSNPRgauucjw=="], - "body-parser/content-type": ["content-type@2.0.0", "", {}, "sha512-j/O/d7GcZCyNl7/hwZAb606rzqkyvaDctLmckbxLzHvFBzTJHuGEdodATcP3yIRoDrLHkIATJuvzbFlp/ki2cQ=="], + "body-parser/content-type": ["content-type@2.1.0", "", {}, "sha512-mj7UPXE0jaqaOsukNZRUEfEi2AcL7C/vwmwcHV0O97eO1E1pxBZuyjlZrx5seTaNBg1U6+o35wpa35Qfcc+7ag=="], "cliui/string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="], @@ -626,6 +624,8 @@ "micromatch/picomatch": ["picomatch@2.3.2", "", {}, "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA=="], + "negotiator/content-type": ["content-type@2.1.0", "", {}, "sha512-mj7UPXE0jaqaOsukNZRUEfEi2AcL7C/vwmwcHV0O97eO1E1pxBZuyjlZrx5seTaNBg1U6+o35wpa35Qfcc+7ag=="], + "ora/cli-spinners": ["cli-spinners@3.4.0", "", {}, "sha512-bXfOC4QcT1tKXGorxL3wbJm6XJPDqEnij2gQ2m7ESQuE+/z9YFIWnl/5RpTiKWbMq3EVKR4fRLJGn6DVfu0mpw=="], "proper-lockfile/retry": ["retry@0.12.0", "", {}, "sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow=="], @@ -636,7 +636,7 @@ "tar/chownr": ["chownr@3.0.0", "", {}, "sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g=="], - "type-is/content-type": ["content-type@2.0.0", "", {}, "sha512-j/O/d7GcZCyNl7/hwZAb606rzqkyvaDctLmckbxLzHvFBzTJHuGEdodATcP3yIRoDrLHkIATJuvzbFlp/ki2cQ=="], + "type-is/content-type": ["content-type@2.1.0", "", {}, "sha512-mj7UPXE0jaqaOsukNZRUEfEi2AcL7C/vwmwcHV0O97eO1E1pxBZuyjlZrx5seTaNBg1U6+o35wpa35Qfcc+7ag=="], "wrap-ansi/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="], diff --git a/docs/publishing.md b/docs/publishing.md new file mode 100644 index 0000000..d12dc70 --- /dev/null +++ b/docs/publishing.md @@ -0,0 +1,95 @@ +# Publish KB + +KB uses an interactive first publication and stage-only trusted publishing for +later versions. npm staged publishing cannot create a package name, so the +initial registry write follows a separate path. + +## Bootstrap the npm package + +Start from the current `main` commit after its required checks pass. Use Node +24, npm 11.19.0, and Bun 1.3.14. Do not create the matching Git tag yet. + +1. Install without dependency lifecycle scripts and run the complete gate. + + ```sh + bun install --frozen-lockfile --ignore-scripts + bun run check + ``` + +2. Confirm that the check did not change the committed package outputs. + + ```sh + git status --porcelain --untracked-files=all -- dist bun.lock + ``` + + Continue only when the command produces no output. + +3. Build one npm tarball and exercise that exact file through the package + smoke. + + ```sh + kb_npm_artifact="$(mktemp -d)" + npm pack --json --ignore-scripts \ + --pack-destination "$kb_npm_artifact" \ + --registry=https://registry.npmjs.org + bun run ./scripts/package-smoke.ts \ + --archive "$kb_npm_artifact/hraness-kb-0.17.1.tgz" + ``` + + Review the complete inventory, file count, packed size, unpacked size, and + integrity before continuing. The smoke installs the exact archive with both + Bun and npm, with lifecycle scripts disabled. + +4. Publish the reviewed tarball with the signed-in maintainer session. + + ```sh + npm publish "$kb_npm_artifact/hraness-kb-0.17.1.tgz" \ + --access public \ + --ignore-scripts \ + --registry=https://registry.npmjs.org + ``` + + Complete npm's two-factor authentication prompt locally. Never put an npm + password, one-time password, recovery code, session cookie, or token in Git, + a workflow, a task file, or chat. + +5. Confirm that `@hraness/kb@0.17.1` is public, `latest` names `0.17.1`, and the + registry metadata and downloaded tarball match the reviewed artifact. Run + the same package smoke against the downloaded registry tarball. + +The tag workflow refuses to create the immutable GitHub Release until the +matching npm artifact exists and has the same integrity as a fresh source +tarball. + +## Configure trusted publishing + +After the first version exists, configure one GitHub Actions trusted publisher +in the npm package settings: + +- organization or owner: `hraness` +- repository: `kb` +- workflow filename: `npm-stage.yml` +- allowed action: `npm stage publish` only +- environment: none + +Then require publishing two-factor authentication and disallow traditional +tokens. Do not add an npm publishing token to GitHub. Preserve +`contentPolicy.class=dual-use` and the root `DISCLOSURE` in every package. + +## Stage a later version + +1. Merge a new stable version to `main` and wait for required CI. +2. Dispatch **Stage npm package** from current `main`. The workflow rejects a + tag, another branch, or a commit behind the current default-branch head. +3. Inspect the uploaded and staged artifact, including its source commit, + version, inventory, size, integrity, dual-use declaration, and disclosure. +4. Approve the stage through npm with two-factor authentication. +5. Verify the public registry package in a clean consumer. +6. Create and push the matching `v` tag on the same `main` commit. The + tag workflow verifies npm delivery before it creates the immutable GitHub + Release. + +See npm's documentation for [trusted +publishing](https://docs.npmjs.com/trusted-publishers/), [staged +publishing](https://docs.npmjs.com/staged-publishing/), and [dual-use +content](https://docs.npmjs.com/policies/dual-use/). diff --git a/package.json b/package.json index c2a426f..361220d 100644 --- a/package.json +++ b/package.json @@ -1,8 +1,11 @@ { "name": "@hraness/kb", - "version": "0.17.0", + "version": "0.17.1", "description": "A knowledge base for coding agents, built from Markdown, backlinks, semantic search, and Git context.", "license": "MIT", + "contentPolicy": { + "class": "dual-use" + }, "type": "module", "sideEffects": [ "./src/agent-context.ts", @@ -56,6 +59,9 @@ "./src/workflows/plan-radar.ts" ], "packageManager": "bun@1.3.14", + "engines": { + "bun": ">=1.3.14" + }, "repository": { "type": "git", "url": "git+https://github.com/hraness/kb.git" @@ -303,7 +309,6 @@ "src/clip/url-metadata.ts", "src/clip/url-metadata-backfill.ts", "src/clip/url-metadata-cli.ts", - "src/clip/metadata-search-tool/.gitignore", "src/clip/metadata-search-tool/Cargo.toml", "src/clip/metadata-search-tool/Cargo.lock", "src/clip/metadata-search-tool/runner.ts", @@ -363,6 +368,7 @@ "src/workflows/plan-radar.ts", "skills/kb", "README.md", + "DISCLOSURE", "LICENSE" ], "scripts": { @@ -397,6 +403,7 @@ "typescript": "^6.0.3" }, "publishConfig": { - "access": "public" + "access": "public", + "registry": "https://registry.npmjs.org" } } diff --git a/portfolio-inventory.json b/portfolio-inventory.json index 31001cb..5c58b2c 100644 --- a/portfolio-inventory.json +++ b/portfolio-inventory.json @@ -8,7 +8,7 @@ "name": "@hraness/kb", "path": ".", "visibility": "public", - "version": "0.17.0" + "version": "0.17.1" } ], "dependencies": [ diff --git a/scripts/check-workflow-yaml.test.ts b/scripts/check-workflow-yaml.test.ts index 1e8ffa1..380c216 100644 --- a/scripts/check-workflow-yaml.test.ts +++ b/scripts/check-workflow-yaml.test.ts @@ -1,6 +1,12 @@ import { describe, expect, test } from "bun:test"; -import { validateWorkflowYaml } from "./check-workflow-yaml.ts"; +import { readFile } from "node:fs/promises"; +import { resolve } from "node:path"; + +import { + validateNpmStageWorkflow, + validateWorkflowYaml, +} from "./check-workflow-yaml.ts"; describe("GitHub workflow YAML", () => { test("accepts commands with YAML-significant text inside block scalars", () => { @@ -29,4 +35,75 @@ jobs: - run: node -e 'const value = { type: "json" }' `, "workflow.yml")).toThrow("invalid YAML"); }); + + test("requires a fresh default-branch HEAD guard at the final publication boundary", async () => { + const path = resolve(import.meta.dir, "../.github/workflows/npm-stage.yml"); + const source = await readFile(path, "utf8"); + const finalGuard = 'git fetch origin "$DEFAULT_BRANCH"'; + const finalGuardIndex = source.lastIndexOf(finalGuard); + expect(finalGuardIndex).toBeGreaterThan(-1); + const missingFinalGuard = + source.slice(0, finalGuardIndex) + + "git status --short" + + source.slice(finalGuardIndex + finalGuard.length); + expect(() => validateNpmStageWorkflow(source, "npm-stage.yml")).not.toThrow(); + expect(() => validateNpmStageWorkflow( + missingFinalGuard, + "npm-stage.yml", + )).toThrow("must recheck current default-branch HEAD"); + }); + + test("keeps npm staging manual, tokenless, artifact-bound, and stage-only", async () => { + const path = resolve(import.meta.dir, "../.github/workflows/npm-stage.yml"); + const source = await readFile(path, "utf8"); + + for (const required of [ + "workflow_dispatch:", + "contents: read", + "id-token: write", + "runs-on: ubuntu-latest", + "node-version: \"24\"", + "package-manager-cache: false", + "npm@11.19.0", + "bun-version: \"1.3.14\"", + "bun install --frozen-lockfile --ignore-scripts", + "bun run check", + "git status --porcelain --untracked-files=all -- dist bun.lock", + "npm pack --json --ignore-scripts", + "scripts/package-smoke.ts --archive", + "archive_sha512", + "npm stage publish \"$archive\"", + "--registry=https://registry.npmjs.org", + ] as const) { + expect(source).toContain(required); + } + + expect(source).not.toContain("secrets.NPM_TOKEN"); + expect(source).not.toContain("NODE_AUTH_TOKEN"); + expect(source).not.toMatch(/\n\s+push:/u); + expect(source).not.toMatch(/\bnpm publish\b/u); + }); + + test("gates the immutable GitHub release on the exact public npm artifact", async () => { + const path = resolve(import.meta.dir, "../.github/workflows/release.yml"); + const source = await readFile(path, "utf8"); + + expect(source).toContain("Verify published npm artifact"); + expect(source).toContain("$package_name@$package_version"); + expect(source).toContain("source[0].integrity !== registry[0]?.integrity"); + expect(source).toContain("--registry=https://registry.npmjs.org"); + expect(source).toContain("scripts/package-smoke.ts --archive"); + }); + + test("pins publication to the canonical npm registry", async () => { + const path = resolve(import.meta.dir, "../package.json"); + const manifest = JSON.parse(await readFile(path, "utf8")) as { + readonly publishConfig?: unknown; + }; + + expect(manifest.publishConfig).toEqual({ + access: "public", + registry: "https://registry.npmjs.org", + }); + }); }); diff --git a/scripts/check-workflow-yaml.ts b/scripts/check-workflow-yaml.ts index fc9a221..c9c582c 100644 --- a/scripts/check-workflow-yaml.ts +++ b/scripts/check-workflow-yaml.ts @@ -10,7 +10,7 @@ function record(value: unknown, label: string): Record { return value as Record; } -export function validateWorkflowYaml(source: string, label: string): void { +function workflowRecord(source: string, label: string): Record { const document = parseDocument(source, { prettyErrors: true, uniqueKeys: true, @@ -27,17 +27,64 @@ export function validateWorkflowYaml(source: string, label: string): void { if (Object.keys(jobs).length === 0) { throw new Error(`${label} jobs must not be empty`); } + return workflow; +} + +export function validateWorkflowYaml(source: string, label: string): void { + workflowRecord(source, label); +} + +export function validateNpmStageWorkflow(source: string, label: string): void { + const workflow = workflowRecord(source, label); + const jobs = record(workflow.jobs, `${label} jobs`); + const stage = record(jobs.stage, `${label} stage job`); + if (!Array.isArray(stage.steps)) { + throw new Error(`${label} stage steps must be a sequence`); + } + const steps = stage.steps.map((step, index) => + record(step, `${label} stage step ${String(index + 1)}`)); + if (!steps.some((step) => + typeof step.uses === "string" && step.uses.startsWith("actions/checkout@"))) { + throw new Error(`${label} stage job must check out the dispatch commit`); + } + const publicationSteps = steps.filter((step) => + typeof step.run === "string" && step.run.includes("npm stage publish")); + if (publicationSteps.length !== 1) { + throw new Error(`${label} must contain exactly one staged-publication step`); + } + const publicationStep = publicationSteps[0]; + if (publicationStep === undefined || typeof publicationStep.run !== "string") { + throw new Error(`${label} staged-publication command is missing`); + } + const environment = record(publicationStep.env, `${label} staged-publication environment`); + if (environment.DEFAULT_BRANCH !== "${{ github.event.repository.default_branch }}") { + throw new Error(`${label} staged publication must bind the repository default branch`); + } + const guardCommands = [ + 'git fetch origin "$DEFAULT_BRANCH"', + 'remote_head="$(git rev-parse "origin/$DEFAULT_BRANCH")"', + 'if [[ "$GITHUB_SHA" != "$remote_head" ]]; then', + 'npm stage publish "$archive"', + "--registry=https://registry.npmjs.org", + ]; + let previousIndex = -1; + for (const command of guardCommands) { + const index = publicationStep.run.indexOf(command); + if (index <= previousIndex) { + throw new Error(`${label} must recheck current default-branch HEAD immediately before staged publication`); + } + previousIndex = index; + } } if (import.meta.main) { const repositoryRoot = resolve(import.meta.dir, ".."); - for (const path of [ - ".github/workflows/ci.yml", - ".github/workflows/release.yml", - ]) { - validateWorkflowYaml( - await readFile(resolve(repositoryRoot, path), "utf8"), - path, - ); + for (const path of [".github/workflows/ci.yml", ".github/workflows/release.yml"]) { + validateWorkflowYaml(await readFile(resolve(repositoryRoot, path), "utf8"), path); } + const npmStagePath = ".github/workflows/npm-stage.yml"; + validateNpmStageWorkflow( + await readFile(resolve(repositoryRoot, npmStagePath), "utf8"), + npmStagePath, + ); } diff --git a/scripts/package-smoke.ts b/scripts/package-smoke.ts index 2b526a0..5ad8e08 100644 --- a/scripts/package-smoke.ts +++ b/scripts/package-smoke.ts @@ -1,8 +1,11 @@ -import { lstat, mkdir, mkdtemp, readFile, readdir, rm, writeFile } from "node:fs/promises"; +import { lstat, mkdir, mkdtemp, readFile, readdir, rm, stat, writeFile } from "node:fs/promises"; import { tmpdir } from "node:os"; -import { delimiter, join, resolve } from "node:path"; +import { basename, delimiter, join, resolve } from "node:path"; const packageName = "@hraness/kb"; +const maximumPackageFiles = 210; +const maximumPackedBytes = 1_200_000; +const maximumUnpackedBytes = 5_250_000; const importSpecifiers = [ "@hraness/kb", "@hraness/kb/agent-context", @@ -62,12 +65,22 @@ const binNames = ["kb", "kb-evaluation-builder"]; const verificationPackages = ["@types/bun@^1.3.14","fast-check@^4.8.0","typescript@^6.0.3"]; const skillNames = ["kb"] as const; const metadataSearchToolFiles = [ - "src/clip/metadata-search-tool/.gitignore", "src/clip/metadata-search-tool/Cargo.lock", "src/clip/metadata-search-tool/Cargo.toml", "src/clip/metadata-search-tool/runner.ts", "src/clip/metadata-search-tool/src/main.rs", ] as const; +const requiredPackageFiles = [ + "DISCLOSURE", + "LICENSE", + "README.md", + "dist/cli.js", + "dist/evaluation-builder.js", + "package.json", + "skills/kb/AGENTS.md", + "skills/kb/SKILL.md", + "skills/kb/agents/openai.yaml", +] as const; async function run(command: string[], cwd: string): Promise { const process = Bun.spawn(command, { @@ -114,6 +127,30 @@ function resolveGenuineNodeExecutable(): string { throw new Error("package smoke requires a genuine Node 24 executable on PATH"); } +function resolveNpmExecutable(): string { + const executableName = process.platform === "win32" ? "npm.cmd" : "npm"; + const candidates = [...new Set( + (process.env.PATH ?? "") + .split(delimiter) + .filter((directory) => directory.length > 0) + .map((directory) => resolve(directory, executableName)), + )]; + for (const executable of candidates) { + try { + const probe = Bun.spawnSync([executable, "--version"], { + env: environment, + stdin: "ignore", + stdout: "ignore", + stderr: "ignore", + }); + if (probe.exitCode === 0) return executable; + } catch { + // Continue past absent or inaccessible PATH candidates. + } + } + throw new Error("package smoke requires npm on PATH"); +} + async function regularFiles(root: string, prefix = ""): Promise { const entries = await readdir(root, { withFileTypes: true }); const files: string[] = []; @@ -121,10 +158,12 @@ async function regularFiles(root: string, prefix = ""): Promise { left.name.localeCompare(right.name))) { const relativePath = prefix === "" ? entry.name : `${prefix}/${entry.name}`; if (entry.isSymbolicLink()) { - throw new Error(`packaged skill tree contains a symbolic link: ${relativePath}`); + throw new Error(`package tree contains a symbolic link: ${relativePath}`); } if (entry.isDirectory()) { - files.push(...await regularFiles(join(root, entry.name), relativePath)); + if (entry.name !== "node_modules") { + files.push(...await regularFiles(join(root, entry.name), relativePath)); + } } else if (entry.isFile()) { files.push(relativePath); } @@ -189,8 +228,8 @@ async function verifyInstalledSkills(consumer: string): Promise { readFile(join(installedRoot, "kb", "SKILL.md"), "utf8"), readFile(join(installedRoot, "kb", "agents", "openai.yaml"), "utf8"), ]); - if (!skill.includes(`github:hraness/kb#v${manifest.version}`)) { - throw new Error("installed KB skill pin does not match the package version"); + if (!skill.includes(`@hraness/kb@${manifest.version}`)) { + throw new Error("installed KB skill npm pin does not match the package version"); } if (!metadata.includes("$kb")) { throw new Error("installed KB skill metadata must invoke $kb explicitly"); @@ -216,6 +255,101 @@ async function verifyInstalledMetadataSearchTool(consumer: string): Promise> { + const installedPackage = join(consumer, "node_modules", "@hraness", "kb"); + type PackageIdentity = { + readonly contentPolicy?: { readonly class?: unknown }; + readonly engines?: { readonly bun?: unknown }; + readonly name?: unknown; + readonly publishConfig?: { + readonly access?: unknown; + readonly registry?: unknown; + }; + readonly version?: unknown; + }; + const [manifest, sourceManifest] = await Promise.all([ + readFile(join(installedPackage, "package.json"), "utf8").then( + (source) => JSON.parse(source) as PackageIdentity, + ), + readFile(join(repository, "package.json"), "utf8").then( + (source) => JSON.parse(source) as PackageIdentity, + ), + ]); + if ( + sourceManifest.name !== packageName + || typeof sourceManifest.version !== "string" + || manifest.name !== sourceManifest.name + || manifest.version !== sourceManifest.version + ) { + throw new Error("installed package identity does not match the source package"); + } + if (manifest.contentPolicy?.class !== "dual-use") { + throw new Error("installed package must retain contentPolicy.class=dual-use"); + } + if (manifest.engines?.bun !== ">=1.3.14") { + throw new Error("installed package must require Bun >=1.3.14"); + } + if ( + manifest.publishConfig?.access !== "public" + || manifest.publishConfig.registry !== "https://registry.npmjs.org" + ) { + throw new Error("installed package must pin public publication to the canonical npm registry"); + } + const files = await regularFiles(installedPackage); + for (const requiredPath of requiredPackageFiles) { + if (!files.includes(requiredPath)) { + throw new Error(`installed package is missing ${requiredPath}`); + } + } + for (const path of files) { + if ( + path !== "DISCLOSURE" + && path !== "LICENSE" + && path !== "README.md" + && path !== "package.json" + && !path.startsWith("dist/") + && !path.startsWith("skills/kb/") + && !path.startsWith("src/") + ) { + throw new Error(`installed package contains an unexpected path: ${path}`); + } + } + if (files.length > maximumPackageFiles) { + throw new Error( + `installed package has ${String(files.length)} files; maximum is ${String(maximumPackageFiles)}`, + ); + } + let unpackedBytes = 0; + for (const path of files) { + unpackedBytes += (await stat(join(installedPackage, path))).size; + } + if (unpackedBytes > maximumUnpackedBytes) { + throw new Error( + `installed package has ${String(unpackedBytes)} unpacked bytes; maximum is ${String(maximumUnpackedBytes)}`, + ); + } + const [sourceDisclosure, installedDisclosure] = await Promise.all([ + readFile(join(repository, "DISCLOSURE")), + readFile(join(installedPackage, "DISCLOSURE")), + ]); + if (!sourceDisclosure.equals(installedDisclosure)) { + throw new Error("installed dual-use disclosure differs from the source disclosure"); + } + return { fileCount: files.length, unpackedBytes }; +} + +function archiveArgument(): string | null { + const args = process.argv.slice(2); + if (args.length === 0) return null; + if (args.length !== 2 || args[0] !== "--archive" || args[1] === undefined) { + throw new Error("usage: bun run scripts/package-smoke.ts [--archive ]"); + } + return resolve(args[1]); +} + const repository = process.cwd(); const work = await mkdtemp(join(tmpdir(), "hraness-package-smoke-")); const temporary = join(work, "tmp"); @@ -223,35 +357,85 @@ const environment = { ...process.env, BUN_TMPDIR: temporary, TMPDIR: temporary, + npm_config_audit: "false", + npm_config_cache: join(temporary, "npm-cache"), + npm_config_fund: "false", + npm_config_ignore_scripts: "true", + npm_config_registry: "https://registry.npmjs.org", + npm_config_update_notifier: "false", }; try { - const archive = join(work, "package.tgz"); + const suppliedArchive = archiveArgument(); + const archive = suppliedArchive ?? join(work, "package.tgz"); const consumer = join(work, "consumer"); + const npmConsumer = join(work, "npm-consumer"); await mkdir(temporary, { mode: 0o700 }); await mkdir(consumer); + await mkdir(npmConsumer); const nodeExecutable = resolveGenuineNodeExecutable(); - await run([ - process.execPath, - "pm", - "pack", - "--filename", - archive, - "--ignore-scripts", - "--quiet", - ], repository); + const npmExecutable = resolveNpmExecutable(); + if (suppliedArchive === null) { + await run([ + process.execPath, + "pm", + "pack", + "--filename", + archive, + "--ignore-scripts", + "--quiet", + ], repository); + } else { + const archiveStat = await lstat(archive); + if (!archiveStat.isFile() || archiveStat.isSymbolicLink()) { + throw new Error("supplied package archive must be a regular file"); + } + } + const packedBytes = (await stat(archive)).size; + if (packedBytes > maximumPackedBytes) { + throw new Error( + `package archive has ${String(packedBytes)} bytes; maximum is ${String(maximumPackedBytes)}`, + ); + } await writeFile(join(consumer, "package.json"), JSON.stringify({ private: true, type: "module" })); + await writeFile(join(npmConsumer, "package.json"), JSON.stringify({ private: true, type: "module" })); await run([process.execPath, "add", archive, "--ignore-scripts"], consumer); + await run([ + npmExecutable, + "install", + "--ignore-scripts", + "--no-audit", + "--no-fund", + "--save-exact", + archive, + ], npmConsumer); + const bunPackage = await verifyInstalledPackagePolicy(consumer); + const npmPackage = await verifyInstalledPackagePolicy(npmConsumer); + if ( + bunPackage.fileCount !== npmPackage.fileCount + || bunPackage.unpackedBytes !== npmPackage.unpackedBytes + ) { + throw new Error("Bun and npm consumers installed different package trees"); + } await verifyInstalledSkills(consumer); + await verifyInstalledSkills(npmConsumer); await verifyInstalledMetadataSearchTool(consumer); + await verifyInstalledMetadataSearchTool(npmConsumer); await run([nodeExecutable, "--input-type=module", "-e", `await import(${JSON.stringify(packageName)})`], consumer); + await run([nodeExecutable, "--input-type=module", "-e", `await import(${JSON.stringify(packageName)})`], npmConsumer); for (const binName of binNames) { await run([join(consumer, "node_modules", ".bin", binName), "--help"], consumer); + await run([join(npmConsumer, "node_modules", ".bin", binName), "--help"], npmConsumer); } await run([ join(consumer, "node_modules", ".bin", "kb"), "url-metadata", "--help", ], consumer); + await run([ + join(npmConsumer, "node_modules", ".bin", "kb"), + "url-metadata", + "--help", + ], npmConsumer); if (verificationPackages.length > 0) { await run([process.execPath, "add", ...verificationPackages, "--ignore-scripts"], consumer); } @@ -267,6 +451,18 @@ for (const specifier of ${JSON.stringify(importSpecifiers)}) { } }`, ], consumer); + await run([ + nodeExecutable, + "--input-type=module", + "-e", + `const required = ${JSON.stringify(requiredNamedExports)}; +for (const specifier of ${JSON.stringify(importSpecifiers)}) { + const surface = await import(specifier); + for (const name of required[specifier] ?? []) { + if (typeof surface[name] !== "function") throw new Error(specifier + " is missing " + name); + } +}`, + ], npmConsumer); const consumerSource = `${importSpecifiers.map((specifier, index) => `import * as surface${String(index)} from ${JSON.stringify(specifier)};` ).join("\n")} @@ -301,6 +497,13 @@ void [${importSpecifiers.map((_specifier, index) => await writeFile(join(consumer, "tsconfig.bundler.json"), "{\n \"compilerOptions\": {\n \"target\": \"ES2023\",\n \"lib\": [\n \"ES2023\",\n \"DOM\",\n \"DOM.Iterable\"\n ],\n \"types\": [\n \"bun\",\n \"node\"\n ],\n \"strict\": true,\n \"noEmit\": true,\n \"skipLibCheck\": false,\n \"module\": \"Preserve\",\n \"moduleResolution\": \"Bundler\"\n },\n \"include\": [\n \"index.ts\"\n ]\n}"); await run([process.execPath, "x", "tsc", "-p", "./tsconfig.bundler.json"], consumer); + console.log(JSON.stringify({ + archive: basename(archive), + fileCount: bunPackage.fileCount, + packedBytes, + unpackedBytes: bunPackage.unpackedBytes, + })); + } finally { await rm(work, { recursive: true, force: true }); } diff --git a/skills/kb/SKILL.md b/skills/kb/SKILL.md index aba33c8..c118fc6 100644 --- a/skills/kb/SKILL.md +++ b/skills/kb/SKILL.md @@ -31,15 +31,16 @@ missing: ```sh command -v kb >/dev/null 2>&1 || { command -v bun >/dev/null 2>&1 || exit 1 - bun add --global github:hraness/kb#v0.17.0 + bun add --global @hraness/kb@0.17.1 } kb --help ``` -The tag is the immutable release owned by this skill. Do not replace it with a -branch, `latest`, or an unpinned package source. Run `kb doctor` when the chosen -workflow may need browser capture, media tools, PDF extraction, OCR, or local -semantic search. +The exact npm version is the immutable release owned by this skill. Do not +replace it with `latest`, a branch, or an unpinned package source. Both installed +commands require Bun `1.3.14` or newer in `PATH`. Run `kb doctor` when the +chosen workflow may need browser capture, media tools, PDF extraction, OCR, or +local semantic search. Installation ends after command verification. Never run `kb init`, create a vault, refresh a catalog, or edit Markdown as an installation side effect.