From d19a2def11888134b16808515db8133e1f7c1986 Mon Sep 17 00:00:00 2001 From: Ernestas Lel Date: Tue, 7 Jul 2026 00:18:48 +0000 Subject: [PATCH 1/4] apply code review improvements (H1-H4, M5-M10, L11-L16) High: - Replace GNU sed -i with portable awk in uninstall.sh - Add npx prefix to typecheck, @types/node, package-lock.json, fix tsc - Add vitest plugin unit tests (14 tests for readTruncated, signature) - Add GitHub Actions CI pipeline Medium: - Add MAX_CHARS byte cap alongside line cap in readTruncated - Substitute ~/.config/opencode with resolved OC_DIR at install time - Add opencode v1.17+ version check in install.sh - Log read/stat errors to stderr instead of swallowing silently - Add --dry-run flag to install.sh and uninstall.sh - Add checkOrphanedFiles warning for tools/domain/ files not in index Low: - Add CHANGELOG.md - Add /ocmem-export backup command template - Add CONTRIBUTING.md - Document experimental hook dependency and minimum opencode version in plugin - Clarify .gitignore note about committing project MEMORY.md --- .github/workflows/ci.yml | 19 + .gitignore | 3 + CHANGELOG.md | 36 + CONTRIBUTING.md | 73 ++ package-lock.json | 1731 +++++++++++++++++++++++++++++++++++++ package.json | 7 +- plugin/ocmem.ts | 72 +- scripts/install.sh | 79 +- scripts/uninstall.sh | 52 +- templates/ocmem-export.md | 53 ++ test/ocmem.test.ts | 147 ++++ tsconfig.json | 5 +- 12 files changed, 2231 insertions(+), 46 deletions(-) create mode 100644 .github/workflows/ci.yml create mode 100644 CHANGELOG.md create mode 100644 CONTRIBUTING.md create mode 100644 package-lock.json create mode 100644 templates/ocmem-export.md create mode 100644 test/ocmem.test.ts diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..7f6f7a7 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,19 @@ +name: CI + +on: + push: + branches: [main] + pull_request: + branches: [main] + +jobs: + test: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: "20" + - run: npm install + - run: npm run typecheck + - run: npm test diff --git a/.gitignore b/.gitignore index ab67142..6723062 100644 --- a/.gitignore +++ b/.gitignore @@ -8,4 +8,7 @@ node_modules/ *~ # Per-developer opencode state (plugin cache, project-local memory, etc.) +# NOTE: This repo uses .opencode/ for its own development. Users of ocmem +# SHOULD commit their .opencode/memory/MEMORY.md in their project repos +# (with an appropriate .gitignore exception). .opencode/ diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..b6940b3 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,36 @@ +# Changelog + +All notable changes to ocmem. + +## Unreleased + +### Added +- Byte-level cap (`MAX_CHARS = 8000`) in the plugin to prevent pathological line lengths from eating the context window. +- Orphaned-file detection: the plugin now scans `tools/` and `domain/` directories and warns when `.md` files exist that are not referenced in the index. +- `--dry-run` / `-n` flag on `install.sh` and `uninstall.sh` — prints every action without modifying anything. +- OpenCode version check in `install.sh` — warns if < v1.17 or if the binary is missing. +- Portable `awk`-based block removal in `uninstall.sh` (was GNU-only `sed -i`). +- Plugin unit tests (`test/ocmem.test.ts` — 14 tests covering `readTruncated` and `signature`) and vitest runner. +- GitHub Actions CI pipeline (`.github/workflows/ci.yml`) running typecheck + tests. +- `package-lock.json` committed to ensure reproducible installs. + +### Changed +- `tsconfig.json` updated: uses `npx tsc`, added `@types/node`, set `"types": ["node"]`. +- Paths in the AGENTS.md block are now substituted with the resolved config directory at install time (no more hardcoded `~/.config/opencode/`). +- Plugin error handling: unexpected read/stat errors are now logged to `stderr` instead of being silently swallowed. +- `.gitignore` now includes a note clarifying that users *should* commit their `.opencode/memory/MEMORY.md`. + +## 1.0.0 — 2026-07-06 + +### Added +- Structured memory system: global (`~/.config/opencode/memory/`) + project (`.opencode/memory/`) roots. +- `plugin/ocmem.ts` — auto-injects the project `MEMORY.md` and global index into every LLM system prompt via `experimental.chat.system.transform` and `experimental.session.compacting`. +- `scripts/install.sh` — one-command installer with `--force` support (never touches user data). +- `scripts/uninstall.sh` — uninstaller that preserves memory files by default, `--purge` to remove everything. +- `/ocmem-init` command — scaffolds project memory in any repo. +- `/ocmem-remember` command — reviews a session, classifies candidates, checks for duplicates, and writes confirmed entries. +- `/ocmem-reorganize` command — dedupe, merge, split, re-sort, and refresh the global index. +- AGENTS.md memory rules: when to write, entry format, mandatory end-of-task memory review. +- `templates/` directory with scaffolding for memory files, commands, and AGENTS.md rules. +- `docs/architecture.md` — design rationale and Claude Code comparison. +- MIT license. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..55ac013 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,73 @@ +# Contributing + +Thanks for wanting to help. + +## Getting started + +```bash +# Clone and install +git clone https://github.com/anomalyco/opencode +cd ocmem +npm install +``` + +Nothing to build — the plugin is TypeScript source consumed directly by +openCode's Bun runtime. + +## Running checks + +```bash +npm run typecheck # TypeScript compilation check +npm test # Plugin unit tests (vitest) +``` + +Both must pass before a PR can be merged. CI runs them automatically. + +## Project layout + +``` +plugin/ ocmem.ts — the opencode plugin (memory injector) +templates/ — AGENTS.md rules, memory scaffold, /ocmem-* commands +scripts/ install.sh, uninstall.sh — one-command deploy / remove +test/ ocmem.test.ts — vitest unit tests +docs/ architecture.md — design rationale +``` + +## Code style + +- **Plugin:** TypeScript only where it helps — just Node built-ins + + `import type { Plugin }` from `@opencode-ai/plugin`. No runtime + dependencies beyond what Bun ships. +- **Shell scripts:** POSIX-compatible where possible. Prefer `awk` over + `sed -i` (macOS `sed` is BSD, not GNU). Use `set -euo pipefail`. +- **Templates:** Plain markdown with YAML frontmatter (`---`) for + command templates. Match the structure of existing commands. +- Errors are logged to `stderr`, never swallowed silently. +- Known trivia goes in comments; explanations live in `docs/`. + +## Testing + +- Plugin changes → add or update tests in `test/ocmem.test.ts`. +- Shell script changes → manually test with `--dry-run` first, then + run against a temp directory. A bats-based test suite is planned + but not yet wired in. + +Run `npm test` before pushing. + +## Submitting changes + +1. Fork and branch from `main`. +2. Keep commits focused — one logical change per commit. +3. Write a clear commit message (present tense, imperative mood). +4. Run `npm run typecheck && npm test`. +5. Open a PR against `main` with a description of what changed and why. + +## What to work on + +Check the open issues, or look at `CHANGELOG.md` for the "Unreleased" +section to see what's already in flight. If you want to add something +not covered by an issue, open one first to discuss. + +## License + +MIT — see [LICENSE](LICENSE). diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..2c8bc9e --- /dev/null +++ b/package-lock.json @@ -0,0 +1,1731 @@ +{ + "name": "ocmem", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "ocmem", + "version": "1.0.0", + "devDependencies": { + "@opencode-ai/plugin": "^1.17.9", + "@types/node": "^26.1.0", + "typescript": "^5.6.0", + "vitest": "^4.1.10" + } + }, + "node_modules/@ai-sdk/provider": { + "version": "3.0.8", + "resolved": "https://registry.npmjs.org/@ai-sdk/provider/-/provider-3.0.8.tgz", + "integrity": "sha512-oGMAgGoQdBXbZqNG0Ze56CHjDZ1IDYOwGYxYjO5KLSlz5HiNQ9udIXsPZ61VWaHGZ5XW/jyjmr6t2xz2jGVwbQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "json-schema": "^0.4.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@emnapi/core": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.11.1.tgz", + "integrity": "sha512-RSvbQmHzdKzNsLYa/wHrbc3KN4sYLKAdPZxqiM2HATqv/SBk2/ENSHpvXGaLOMcsAyz0poEGqkmmKYG3OWiJEQ==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/wasi-threads": "1.2.2", + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/runtime": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.11.1.tgz", + "integrity": "sha512-vgj7R3y3Wgx24IQaGPA/R6YFXLHVMOZ0uVEyIQPaWs+rd1AzfEMXlAC22FYwO1XkKR6NPsq7mUandH8oIRdZFw==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/wasi-threads": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.2.tgz", + "integrity": "sha512-c95qOXkHdydNKhscBTebqEC1CVAZpyqOfVfBzQ1qgzyl3gfeldUjIggDbIZgDKsHLgnsM+igH7TJ/eAasaVuMA==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@msgpackr-extract/msgpackr-extract-darwin-arm64": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-darwin-arm64/-/msgpackr-extract-darwin-arm64-3.0.4.tgz", + "integrity": "sha512-LCkGo6JDfaBhgST7UpPWgNgLINpcpabaHfyz5OBx75nUYxBsaEPxjnyNjWpeb/xBup/682QnBfRBy2/LvPutZQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@msgpackr-extract/msgpackr-extract-darwin-x64": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-darwin-x64/-/msgpackr-extract-darwin-x64-3.0.4.tgz", + "integrity": "sha512-zExlW9zUJKZH/tOtVMttwjKa4Xm/3KcNjnE3dPN92uCktwavMxpgCA3MoJK/DOnTWsQgo224OaST27/mPNAf+w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@msgpackr-extract/msgpackr-extract-linux-arm": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-linux-arm/-/msgpackr-extract-linux-arm-3.0.4.tgz", + "integrity": "sha512-Tg3yX65f5GbtXLkrYEHE5oibZG9epyYWas7FogTTEJeDEF9JlXJzKgXaNhT3UXlTOeA+AfZpYZYZ0uPj7Cfquw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@msgpackr-extract/msgpackr-extract-linux-arm64": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-linux-arm64/-/msgpackr-extract-linux-arm64-3.0.4.tgz", + "integrity": "sha512-dgX0P/9wGPJeHFBG+ZmhgE6bmtMt7NP5CRBGyyktpopdk/mW4POnrpQsSLtKI1dwpc+pPLuXHDh6vvskyQE/sw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@msgpackr-extract/msgpackr-extract-linux-x64": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-linux-x64/-/msgpackr-extract-linux-x64-3.0.4.tgz", + "integrity": "sha512-8TNXMEjJc3QEy7R/x1INhgiU+XakDAFUzBhaz7+Rbrs8NH5UQeHQxxmzsSBJGyV6I1jW79undiQm8tOI+D+8FQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@msgpackr-extract/msgpackr-extract-win32-x64": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-win32-x64/-/msgpackr-extract-win32-x64-3.0.4.tgz", + "integrity": "sha512-CmCXPQrkbwExx3j946/PtHWHbYJiCRBRDl4BlkRQcJB/YOwQxJRTpoo7aTsortjgoJ1x7opzTSxn7C+ASSLVjQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@napi-rs/wasm-runtime": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.6.tgz", + "integrity": "sha512-ZLv/JdUfkvOy9eCnnBaGfiO+XimbjebAeO+MRQqD/B+FR1tnRN0tpKSJHRbE8sFfS6aqsXZ67TQjfwfsxULVbg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@tybys/wasm-util": "^0.10.3" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + }, + "peerDependencies": { + "@emnapi/core": "^1.7.1", + "@emnapi/runtime": "^1.7.1" + } + }, + "node_modules/@opencode-ai/plugin": { + "version": "1.17.14", + "resolved": "https://registry.npmjs.org/@opencode-ai/plugin/-/plugin-1.17.14.tgz", + "integrity": "sha512-upKf4QHZqjr2cqHJcJiGTUSGJFFVR26Nu8Y2QRThQ2NgXcQ44T1cRvI80nhK87wQsFcHPI842d+cPYERV+lB4w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@ai-sdk/provider": "3.0.8", + "@opencode-ai/sdk": "1.17.14", + "effect": "4.0.0-beta.83", + "zod": "4.1.8" + }, + "peerDependencies": { + "@opentui/core": ">=0.4.3", + "@opentui/keymap": ">=0.4.3", + "@opentui/solid": ">=0.4.3" + }, + "peerDependenciesMeta": { + "@opentui/core": { + "optional": true + }, + "@opentui/keymap": { + "optional": true + }, + "@opentui/solid": { + "optional": true + } + } + }, + "node_modules/@opencode-ai/sdk": { + "version": "1.17.14", + "resolved": "https://registry.npmjs.org/@opencode-ai/sdk/-/sdk-1.17.14.tgz", + "integrity": "sha512-ycSYSF0kuJmNUP38VYnsFEoUWPyjlLjAZFPtYuMt+32Tz+NqVsAPcqK3MPBRd5v+GChjAyApB/y56i7Ub08IAg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cross-spawn": "7.0.6" + } + }, + "node_modules/@oxc-project/types": { + "version": "0.138.0", + "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.138.0.tgz", + "integrity": "sha512-1a7ZKmrRTCoN1XMZ4L0PyyqrMnrNlLyPuOkdSX2MZg7IiIGRUyurNhAm73ptDOraoBcIordsIGKNPKUzy3ZmfA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/Boshen" + } + }, + "node_modules/@rolldown/binding-android-arm64": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.1.4.tgz", + "integrity": "sha512-EZLpf/8y7GXkkra90ML47kzik/GMP3EMcE9bPyHmRfxLC6z9+aW5A8poCsoxjrT5GfEcNAAvWwUHjvP1pUQkfw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-arm64": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.1.4.tgz", + "integrity": "sha512-aUi+HBvmYb7j8krl1+qJgkG8C17fO79gk3c+jPw4S8glRFc1DTija9S3EyaTSQUm5GJXYKDAsugBEhFHH2vYiQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-x64": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.1.4.tgz", + "integrity": "sha512-F7hHC3gwY11+vByKPRWqwGbeXWVgKmL+pTGCinaEhdihzBV2aQ0fvZOch9cXYUOKuKKq429HeYXOqQLc7wFCEg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-freebsd-x64": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.1.4.tgz", + "integrity": "sha512-sI5yw+7s92SK6odiEhD5lKCBlWcpjHS5qyqpVQbZAJ0fIzEUXrmbl3DH2ybR3PZogulNJF+COLtmA8hUfvkCCQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm-gnueabihf": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.1.4.tgz", + "integrity": "sha512-mCi0OKgEieFircrtVYmQAFGszRtMnZ6fpZAXrxanXAu7lqZcsK1E1RAaZNG0uKAnxox3B1f4EyQNnoyMfN1vAA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-gnu": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.1.4.tgz", + "integrity": "sha512-B9Ial3Kv5sh0SHnB1g/QWcUQCEvCF6QKGAl4zXypYj65mVI+B4AhFBwPtSN7pDrJeIx8Z7zdy4ntx+wQABom7w==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-musl": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.1.4.tgz", + "integrity": "sha512-lZVym0PuHE1KZ22gmFTC15lAkrg9iTszR617oYRB/iPY1A56ywoJzVKOJBKaot5RiikCObmur6pogpse3gRcng==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-ppc64-gnu": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.1.4.tgz", + "integrity": "sha512-t2DNiLJWNTbnEHyUzTumldML6ET4/g16467LZoDDJ3tSxGvguL5/NyC2lCsNKuyRycg9XeDQF5SSv+TNOhQEXg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-s390x-gnu": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.1.4.tgz", + "integrity": "sha512-0WIRnL1Uw4BvTZRLQt+PVgo6ZKTJadlC2btP+/EOXv2f/DWbY0rEgl+y834mIVwP1FkTlWVTrGGJXf12lru7EQ==", + "cpu": [ + "s390x" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-gnu": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.1.4.tgz", + "integrity": "sha512-JWtGshGfX+oENAKonoNkqEJX+7hC8yfhi9GUyPX1VX4mdh1y5r+ZiJLR5XzAB0aoP6s/PcILsGjKq8O0mm24bw==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-musl": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.1.4.tgz", + "integrity": "sha512-rT6yQcxUuXs4CnbofqwHRRV0iem349rLMYpTjkgQGLjrY4ado/eDzwPZPTCgTOlF6Nkp8NEv70yLMTn6qkWxsQ==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-openharmony-arm64": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.1.4.tgz", + "integrity": "sha512-KXMGoboq5cyaCQjDA4GLuRiOwBQ0EyFnJoVViLeZ45/3rFItRODEr+NdsBcVpll40hhNArlm/speWGRvj08LzA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-wasm32-wasi": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.1.4.tgz", + "integrity": "sha512-5K83rb36oJiY7BCyE9zLZtGcPV4g5wvq+xwdO0XPIwDVZI8cyB/AUjkNXGb92/rnmezEkjMOpgY61rtwjQtFwg==", + "cpu": [ + "wasm32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "1.11.1", + "@emnapi/runtime": "1.11.1", + "@napi-rs/wasm-runtime": "^1.1.6" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-win32-arm64-msvc": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.1.4.tgz", + "integrity": "sha512-PnWBtw3TV5KOg69HQQDR0mnQuyCmSGR2pAB4DC1rPF808fgKeTUMj2EOEyKATpgiuxuR5APQmiDO7PDgEjTFSA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-win32-x64-msvc": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.1.4.tgz", + "integrity": "sha512-M1lpniBePobTfsa7Ks9a199e1akxsXn+GYBUKsEzv3YFzOm1HJAMNwKI3qr0Zq+mxwx9gOZoTdP1yXRYsZUocQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.1.tgz", + "integrity": "sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@standard-schema/spec": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", + "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tybys/wasm-util": { + "version": "0.10.3", + "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.3.tgz", + "integrity": "sha512-F3fo1MYrRJYL3zER0OUOmkutjr1Vp23m7OsSgp7nq4SP6OqX6C/56XFIPAl5bt3zaBRjmW7SGz3u/6LwFpYcOg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@types/chai": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz", + "integrity": "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/deep-eql": "*", + "assertion-error": "^2.0.1" + } + }, + "node_modules/@types/deep-eql": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz", + "integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/estree": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", + "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "26.1.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-26.1.0.tgz", + "integrity": "sha512-O0A1G3xPGy4w7AgQdAQYUlQ+BKk2Oovw8eRpofyp5KdBZULnbe+WqaOVNrm705SHphCiG4XHsACrSmPu1f+Kgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~8.3.0" + } + }, + "node_modules/@vitest/expect": { + "version": "4.1.10", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.1.10.tgz", + "integrity": "sha512-YsCn+qAk1GWjQOWFEsEcL2gNQ0zmVmQu3T03qP6UyjhtmdtwtbuI+DASn/7iQB3HGTXkdBwGddzxPlmiql5vlA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@standard-schema/spec": "^1.1.0", + "@types/chai": "^5.2.2", + "@vitest/spy": "4.1.10", + "@vitest/utils": "4.1.10", + "chai": "^6.2.2", + "tinyrainbow": "^3.1.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/mocker": { + "version": "4.1.10", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.1.10.tgz", + "integrity": "sha512-v0xaezt+DKEmKfaxg133ldzADrwLGd7Ze1MfQQTYfvs8OqZIwbxyxaYURivwV7sWy5fqn3rH5uOrSp07bp44Ow==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/spy": "4.1.10", + "estree-walker": "^3.0.3", + "magic-string": "^0.30.21" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "msw": "^2.4.9", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "msw": { + "optional": true + }, + "vite": { + "optional": true + } + } + }, + "node_modules/@vitest/pretty-format": { + "version": "4.1.10", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.1.10.tgz", + "integrity": "sha512-W1HsjSH4MXQ9YfmmhLAoIYf1HRfekQCGngeIgcei6MP5QQGWUe0gkopdZQaVCFO+JDJMrAJGwa5pRpNpvy4P8Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyrainbow": "^3.1.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/runner": { + "version": "4.1.10", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.1.10.tgz", + "integrity": "sha512-IKI6kpIH+LmpROplyLwBBaCfMgOZOMsygVa6BARD6ahA04VRuJSa6OaVG7kRvSEMD870Vd91rSSw0eegtWyLGg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/utils": "4.1.10", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/snapshot": { + "version": "4.1.10", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.1.10.tgz", + "integrity": "sha512-xRkfOT1qpTAi/Ti4Y1LtfRc3kEuqxGw59eN2jN9pRWMtS/XDevekhcFSqvQqjUNGksfjMJu3Y+oJ+4Ypn2OaJw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "4.1.10", + "@vitest/utils": "4.1.10", + "magic-string": "^0.30.21", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/spy": { + "version": "4.1.10", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.1.10.tgz", + "integrity": "sha512-PLf/Ugvoq5wO/b4rwYCR1h2PSIdXz7wnkQFMiUpLdtM7l6pqVFcQIBEHyT1+l+cj7mNwAfZHzqXqDyjvOuwbDw==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/utils": { + "version": "4.1.10", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.1.10.tgz", + "integrity": "sha512-fy9am/HWxbaGt/Sawrp90vt6Y6jQwf1RX77cz3uwoJwJVMli/e1IEwRPnMNJ7vKfPTwo0diXifkpPvwH9v7nGA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "4.1.10", + "convert-source-map": "^2.0.0", + "tinyrainbow": "^3.1.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/assertion-error": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", + "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + } + }, + "node_modules/chai": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/chai/-/chai-6.2.2.tgz", + "integrity": "sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "node_modules/effect": { + "version": "4.0.0-beta.83", + "resolved": "https://registry.npmjs.org/effect/-/effect-4.0.0-beta.83.tgz", + "integrity": "sha512-0wsak8RtgGAr9UWSbVDgJHZcUqMSvicHcvaZv1MbMM7MCGgW4Rn/137J1MHQbwYPcwYGxT/IqehFd+UbYuj78w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@standard-schema/spec": "^1.1.0", + "fast-check": "^4.8.0", + "find-my-way-ts": "^0.1.6", + "ini": "^7.0.0", + "kubernetes-types": "^1.30.0", + "msgpackr": "^2.0.1", + "multipasta": "^0.2.7", + "toml": "^4.1.1", + "uuid": "^14.0.0", + "yaml": "^2.9.0" + } + }, + "node_modules/es-module-lexer": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.3.0.tgz", + "integrity": "sha512-KLdwQm2NvGLDkQDCGvmiQrhkd0JbMzXthwQAUgWjQuQdBLFa3eiBP5arXZyA+f8x+x7OXgud6bq2rxjGtHV2tw==", + "dev": true, + "license": "MIT" + }, + "node_modules/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0" + } + }, + "node_modules/expect-type": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.4.0.tgz", + "integrity": "sha512-KfYbmpRm0VbLjEvVa9yGwCi9GI34xvi7A/HXYWQO65CSD2u3MczUJSuwXKFIxlGsgBQizV9q5J9NHj4VG0n+pA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/fast-check": { + "version": "4.8.0", + "resolved": "https://registry.npmjs.org/fast-check/-/fast-check-4.8.0.tgz", + "integrity": "sha512-GOJ158CUMnN6cSahsv4+ExARvIDuzzinFjkp0E9WtiBa5zcVeLozVkWaE4IzFcc+Y48Wp1EDlUZsXRyAztQcSg==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/dubzzz" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fast-check" + } + ], + "license": "MIT", + "dependencies": { + "pure-rand": "^8.0.0" + }, + "engines": { + "node": ">=12.17.0" + } + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/find-my-way-ts": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/find-my-way-ts/-/find-my-way-ts-0.1.6.tgz", + "integrity": "sha512-a85L9ZoXtNAey3Y6Z+eBWW658kO/MwR7zIafkIUPUMf3isZG0NCs2pjW2wtjxAKuJPxMAsHUIP4ZPGv0o5gyTA==", + "dev": true, + "license": "MIT" + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/ini": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/ini/-/ini-7.0.0.tgz", + "integrity": "sha512-ifK0CgjALofS5bkrcTy4RaQ9Vx2Knf/eLeIO+NaswQEpH1UblrtTSCIvN71qQDMq0PeQ/SSPojvEJp9vvvfr+w==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^22.22.2 || ^24.15.0 || >=26.0.0" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC" + }, + "node_modules/json-schema": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.4.0.tgz", + "integrity": "sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA==", + "dev": true, + "license": "(AFL-2.1 OR BSD-3-Clause)" + }, + "node_modules/kubernetes-types": { + "version": "1.30.0", + "resolved": "https://registry.npmjs.org/kubernetes-types/-/kubernetes-types-1.30.0.tgz", + "integrity": "sha512-Dew1okvhM/SQcIa2rcgujNndZwU8VnSapDgdxlYoB84ZlpAD43U6KLAFqYo17ykSFGHNPrg0qry0bP+GJd9v7Q==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/lightningcss": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.32.0.tgz", + "integrity": "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==", + "dev": true, + "license": "MPL-2.0", + "dependencies": { + "detect-libc": "^2.0.3" + }, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "lightningcss-android-arm64": "1.32.0", + "lightningcss-darwin-arm64": "1.32.0", + "lightningcss-darwin-x64": "1.32.0", + "lightningcss-freebsd-x64": "1.32.0", + "lightningcss-linux-arm-gnueabihf": "1.32.0", + "lightningcss-linux-arm64-gnu": "1.32.0", + "lightningcss-linux-arm64-musl": "1.32.0", + "lightningcss-linux-x64-gnu": "1.32.0", + "lightningcss-linux-x64-musl": "1.32.0", + "lightningcss-win32-arm64-msvc": "1.32.0", + "lightningcss-win32-x64-msvc": "1.32.0" + } + }, + "node_modules/lightningcss-android-arm64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.32.0.tgz", + "integrity": "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-arm64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.32.0.tgz", + "integrity": "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-x64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.32.0.tgz", + "integrity": "sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-freebsd-x64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.32.0.tgz", + "integrity": "sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm-gnueabihf": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.32.0.tgz", + "integrity": "sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-gnu": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.32.0.tgz", + "integrity": "sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-musl": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.32.0.tgz", + "integrity": "sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-gnu": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.32.0.tgz", + "integrity": "sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-musl": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.32.0.tgz", + "integrity": "sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-arm64-msvc": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.32.0.tgz", + "integrity": "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-x64-msvc": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.32.0.tgz", + "integrity": "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/msgpackr": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/msgpackr/-/msgpackr-2.0.4.tgz", + "integrity": "sha512-o1C5KRmuRt+apqMr1HuGSqWStZoRBUpEsCsl15uM9VdAF1qHLtvMOU2En747EnTyEl6c4pzPewRMFF31s1CNbA==", + "dev": true, + "license": "MIT", + "optionalDependencies": { + "msgpackr-extract": "^3.0.4" + } + }, + "node_modules/msgpackr-extract": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/msgpackr-extract/-/msgpackr-extract-3.0.4.tgz", + "integrity": "sha512-4kmO/MdyUIkLIvTPr8VHLil4AtoKIoniWPIEk5+CDy0xnWC84azhSFmuJ7PxZdsYtiP5kEeQsORAVIeMgxT+Hw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "dependencies": { + "node-gyp-build-optional-packages": "5.2.2" + }, + "bin": { + "download-msgpackr-prebuilds": "bin/download-prebuilds.js" + }, + "optionalDependencies": { + "@msgpackr-extract/msgpackr-extract-darwin-arm64": "3.0.4", + "@msgpackr-extract/msgpackr-extract-darwin-x64": "3.0.4", + "@msgpackr-extract/msgpackr-extract-linux-arm": "3.0.4", + "@msgpackr-extract/msgpackr-extract-linux-arm64": "3.0.4", + "@msgpackr-extract/msgpackr-extract-linux-x64": "3.0.4", + "@msgpackr-extract/msgpackr-extract-win32-x64": "3.0.4" + } + }, + "node_modules/multipasta": { + "version": "0.2.8", + "resolved": "https://registry.npmjs.org/multipasta/-/multipasta-0.2.8.tgz", + "integrity": "sha512-ZPWuMKyv0cSO29f7hozp+k6+crZbQijV8ipMvxNxRf2SwtYGTX1ZX89Kd20VV4H9Znonx+EQn+iy1wGQsJ+b+Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "3.3.15", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.15.tgz", + "integrity": "sha512-y7Wygv/7mEOvxTuEQDB8StXdMRBWf1kR/tlhAzBRUFkB2jfcLOAxO/SHmOO2zgz1pVgK29/kyupn059/bCHdjA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/node-gyp-build-optional-packages": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/node-gyp-build-optional-packages/-/node-gyp-build-optional-packages-5.2.2.tgz", + "integrity": "sha512-s+w+rBWnpTMwSFbaE0UXsRlg7hU4FjekKU4eyAih5T8nJuNZT1nNsskXpxmeqSK9UzkBl6UgRlnKc8hz8IEqOw==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "detect-libc": "^2.0.1" + }, + "bin": { + "node-gyp-build-optional-packages": "bin.js", + "node-gyp-build-optional-packages-optional": "optional.js", + "node-gyp-build-optional-packages-test": "build-test.js" + } + }, + "node_modules/obug": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/obug/-/obug-2.1.3.tgz", + "integrity": "sha512-9miFgM2OFba7hB+pRgvtV84pYTBaoTHohvmIgiRt6dRIzbwEOIaNaP+dIlGs2fNFoB0SeISs0Jz5WFVRid6Xyg==", + "dev": true, + "funding": [ + "https://github.com/sponsors/sxzz", + "https://opencollective.com/debug" + ], + "license": "MIT", + "engines": { + "node": ">=12.20.0" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/pathe": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", + "dev": true, + "license": "MIT" + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.5.tgz", + "integrity": "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/postcss": { + "version": "8.5.16", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.16.tgz", + "integrity": "sha512-vuwillviilfKZsg0VGj5R/YwwcHx4SLsIOI/7K6mQkWx+l5cUHTjj5g0AasTBcyXsbfTgrwsUNmVUb5xVwyPwg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.12", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/pure-rand": { + "version": "8.4.1", + "resolved": "https://registry.npmjs.org/pure-rand/-/pure-rand-8.4.1.tgz", + "integrity": "sha512-c58R2+SPFcSIPXoU834QN/KPDDOSd8sXcSrqf6e83Me6Rrp1EYkxukkjXMVrKvKaADs1SOyNkWdfvLf6zY8qLQ==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/dubzzz" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fast-check" + } + ], + "license": "MIT" + }, + "node_modules/rolldown": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.1.4.tgz", + "integrity": "sha512-IjZYiLxZwpnhwhdBH2ugdTGVSdhCQUmLxLoqyjiL0JxYjyRst+5a0P3xfrTxJ5F638j4Mvvw5FAX5XE6eHpXbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@oxc-project/types": "=0.138.0", + "@rolldown/pluginutils": "^1.0.0" + }, + "bin": { + "rolldown": "bin/cli.mjs" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "optionalDependencies": { + "@rolldown/binding-android-arm64": "1.1.4", + "@rolldown/binding-darwin-arm64": "1.1.4", + "@rolldown/binding-darwin-x64": "1.1.4", + "@rolldown/binding-freebsd-x64": "1.1.4", + "@rolldown/binding-linux-arm-gnueabihf": "1.1.4", + "@rolldown/binding-linux-arm64-gnu": "1.1.4", + "@rolldown/binding-linux-arm64-musl": "1.1.4", + "@rolldown/binding-linux-ppc64-gnu": "1.1.4", + "@rolldown/binding-linux-s390x-gnu": "1.1.4", + "@rolldown/binding-linux-x64-gnu": "1.1.4", + "@rolldown/binding-linux-x64-musl": "1.1.4", + "@rolldown/binding-openharmony-arm64": "1.1.4", + "@rolldown/binding-wasm32-wasi": "1.1.4", + "@rolldown/binding-win32-arm64-msvc": "1.1.4", + "@rolldown/binding-win32-x64-msvc": "1.1.4" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/siginfo": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", + "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", + "dev": true, + "license": "ISC" + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/stackback": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", + "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", + "dev": true, + "license": "MIT" + }, + "node_modules/std-env": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-4.1.0.tgz", + "integrity": "sha512-Rq7ybcX2RuC55r9oaPVEW7/xu3tj8u4GeBYHBWCychFtzMIr86A7e3PPEBPT37sHStKX3+TiX/Fr/ACmJLVlLQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinybench": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", + "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinyexec": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.2.4.tgz", + "integrity": "sha512-SHf/r48b7vOrjve9PxJo3MN5v5yuyjHvdUcrQffT3WXMUfnGmHDVbC4k3sHJaJTgZCwpUplIaAo5ANtMyp3YHg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.17", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz", + "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.4" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tinyrainbow": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-3.1.0.tgz", + "integrity": "sha512-Bf+ILmBgretUrdJxzXM0SgXLZ3XfiaUuOj/IKQHuTXip+05Xn+uyEYdVg0kYDipTBcLrCVyUzAPz7QmArb0mmw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/toml": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/toml/-/toml-4.1.2.tgz", + "integrity": "sha512-m0vXfHODcw3gk+KONAOlVQ5yNHc3yS3B1ybM3HS1vqDoS0RWTDDVBVVTYi8hH0k+2OM1vmo9fb1WX9EVqjqfHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20" + } + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "dev": true, + "license": "0BSD", + "optional": true + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-8.3.0.tgz", + "integrity": "sha512-j375ScV60dom+YkPFIfTLcOiPxkN/buHz5GobjLhixFuANaNs3C9l4GmrWqejgXWJ7BbJcFYpTEUkS1Ge8bpZQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/uuid": { + "version": "14.0.1", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-14.0.1.tgz", + "integrity": "sha512-6ZxzVpzDXDa3bJWaHilVayA+BH/1zmxCJoVgvmqJnid/gPoKHxUrS/aC/T6LGQtNHT+XHG9fXPJB4d+IrU30Ew==", + "dev": true, + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "license": "MIT", + "bin": { + "uuid": "dist-node/bin/uuid" + } + }, + "node_modules/vite": { + "version": "8.1.3", + "resolved": "https://registry.npmjs.org/vite/-/vite-8.1.3.tgz", + "integrity": "sha512-Ds+gBRbj0lwRO2Y5hwnUBdxSwlAve9LeRyU4sNnAr0ewW0gWF0n5bgXgUzbgZ49MV9BVUAQUFYVcDUcilUExMA==", + "dev": true, + "license": "MIT", + "dependencies": { + "lightningcss": "^1.32.0", + "picomatch": "^4.0.4", + "postcss": "^8.5.16", + "rolldown": "~1.1.3", + "tinyglobby": "^0.2.17" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^20.19.0 || >=22.12.0", + "@vitejs/devtools": "^0.3.0", + "esbuild": "^0.27.0 || ^0.28.0", + "jiti": ">=1.21.0", + "less": "^4.0.0", + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "@vitejs/devtools": { + "optional": true + }, + "esbuild": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/vitest": { + "version": "4.1.10", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-4.1.10.tgz", + "integrity": "sha512-R9jUTe5S4Qb0HCd4TNqpC7oGcrMssMRGXLW80ubjWsW9VH5GF8y1Y0SFLY9AbqSk6nt0PnOx4H4WNJYZ13GUPw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/expect": "4.1.10", + "@vitest/mocker": "4.1.10", + "@vitest/pretty-format": "4.1.10", + "@vitest/runner": "4.1.10", + "@vitest/snapshot": "4.1.10", + "@vitest/spy": "4.1.10", + "@vitest/utils": "4.1.10", + "es-module-lexer": "^2.0.0", + "expect-type": "^1.3.0", + "magic-string": "^0.30.21", + "obug": "^2.1.1", + "pathe": "^2.0.3", + "picomatch": "^4.0.3", + "std-env": "^4.0.0-rc.1", + "tinybench": "^2.9.0", + "tinyexec": "^1.0.2", + "tinyglobby": "^0.2.15", + "tinyrainbow": "^3.1.0", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0", + "why-is-node-running": "^2.3.0" + }, + "bin": { + "vitest": "vitest.mjs" + }, + "engines": { + "node": "^20.0.0 || ^22.0.0 || >=24.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@edge-runtime/vm": "*", + "@opentelemetry/api": "^1.9.0", + "@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0", + "@vitest/browser-playwright": "4.1.10", + "@vitest/browser-preview": "4.1.10", + "@vitest/browser-webdriverio": "4.1.10", + "@vitest/coverage-istanbul": "4.1.10", + "@vitest/coverage-v8": "4.1.10", + "@vitest/ui": "4.1.10", + "happy-dom": "*", + "jsdom": "*", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "@edge-runtime/vm": { + "optional": true + }, + "@opentelemetry/api": { + "optional": true + }, + "@types/node": { + "optional": true + }, + "@vitest/browser-playwright": { + "optional": true + }, + "@vitest/browser-preview": { + "optional": true + }, + "@vitest/browser-webdriverio": { + "optional": true + }, + "@vitest/coverage-istanbul": { + "optional": true + }, + "@vitest/coverage-v8": { + "optional": true + }, + "@vitest/ui": { + "optional": true + }, + "happy-dom": { + "optional": true + }, + "jsdom": { + "optional": true + }, + "vite": { + "optional": false + } + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/why-is-node-running": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", + "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==", + "dev": true, + "license": "MIT", + "dependencies": { + "siginfo": "^2.0.0", + "stackback": "0.0.2" + }, + "bin": { + "why-is-node-running": "cli.js" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/yaml": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.9.0.tgz", + "integrity": "sha512-2AvhNX3mb8zd6Zy7INTtSpl1F15HW6Wnqj0srWlkKLcpYl/gMIMJiyuGq2KeI2YFxUPjdlB+3Lc10seMLtL4cA==", + "dev": true, + "license": "ISC", + "bin": { + "yaml": "bin.mjs" + }, + "engines": { + "node": ">= 14.6" + }, + "funding": { + "url": "https://github.com/sponsors/eemeli" + } + }, + "node_modules/zod": { + "version": "4.1.8", + "resolved": "https://registry.npmjs.org/zod/-/zod-4.1.8.tgz", + "integrity": "sha512-5R1P+WwQqmmMIEACyzSvo4JXHY5WiAFHRMg+zBZKgKS+Q1viRa0C1hmUKtHltoIFKtIdki3pRxkmpP74jnNYHQ==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + } + } +} diff --git a/package.json b/package.json index a7d81a1..df1ca68 100644 --- a/package.json +++ b/package.json @@ -4,10 +4,13 @@ "description": "Structured memory system for opencode, adapted from John Conneely's Claude Code memory setup.", "private": true, "scripts": { - "typecheck": "tsc --noEmit" + "typecheck": "npx tsc --noEmit", + "test": "vitest run" }, "devDependencies": { "@opencode-ai/plugin": "^1.17.9", - "typescript": "^5.6.0" + "@types/node": "^26.1.0", + "typescript": "^5.6.0", + "vitest": "^4.1.10" } } diff --git a/plugin/ocmem.ts b/plugin/ocmem.ts index ee64d41..29eff66 100644 --- a/plugin/ocmem.ts +++ b/plugin/ocmem.ts @@ -22,15 +22,18 @@ * after the first call is a couple of `statSync` calls (~sub-millisecond). */ import type { Plugin } from "@opencode-ai/plugin" -import { existsSync, readFileSync, statSync } from "node:fs" +import { existsSync, readFileSync, readdirSync, statSync } from "node:fs" import { join } from "node:path" import { homedir } from "node:os" /** Maximum number of lines injected from the project MEMORY.md. */ -const MAX_PROJECT_LINES = 200 +export const MAX_PROJECT_LINES = 200 /** Maximum number of lines injected from the global index. */ -const MAX_INDEX_LINES = 200 +export const MAX_INDEX_LINES = 200 + +/** Maximum characters injected per file (safety net for pathological line lengths). */ +const MAX_CHARS = 8000 /** Marker prepended to every injected block so the model can recognise it. */ const MARKER = "ocmem" @@ -51,33 +54,68 @@ do a final memory scan: If the answer to all three is "no", say so briefly. Do NOT wait for the user to prompt — over-recording is cheap, re-discovering is expensive.` -function readTruncated(filePath: string, maxLines: number): string | null { +export function readTruncated(filePath: string, maxLines: number): string | null { if (!existsSync(filePath)) return null try { const text = readFileSync(filePath, "utf-8").trim() if (!text) return null const lines = text.split("\n") - if (lines.length <= maxLines) return text - return lines.slice(0, maxLines).join("\n") + "\n\n…(truncated)" - } catch { + let result: string + if (lines.length <= maxLines) { + result = text + } else { + result = lines.slice(0, maxLines).join("\n") + "\n\n…(truncated)" + } + if (result.length > MAX_CHARS) { + result = result.slice(0, MAX_CHARS) + "\n\n…(truncated)" + } + return result + } catch (e) { + console.error(`ocmem: failed to read ${filePath}: ${e}`) return null } } /** Build a cheap cache key from the mtimes of the relevant files. */ -function signature(paths: string[]): string { +export function signature(paths: string[]): string { let sig = "" for (const p of paths) { if (!existsSync(p)) continue try { sig += `${p}:${statSync(p).mtimeMs};` - } catch { - /* ignore stat errors */ + } catch (e) { + console.error(`ocmem: failed to stat ${p}: ${e}`) } } return sig } +/** + * Scan the tools/ and domain/ directories for .md files that are not + * referenced in the index. Orphaned files are invisible to the agent + * until they are added to memory.md — this warning surfaces them. + */ +function checkOrphanedFiles(globalDir: string, indexContent: string): string | null { + const rels: string[] = [] + for (const sub of ["tools", "domain"]) { + const dir = join(globalDir, sub) + if (!existsSync(dir)) continue + try { + for (const f of readdirSync(dir)) { + if (!f.endsWith(".md")) continue + const rel = `${sub}/${f}` + if (!indexContent.includes(rel)) { + rels.push(rel) + } + } + } catch { + /* directory listing may fail — not critical enough to crash */ + } + } + if (rels.length === 0) return null + return `=== ${MARKER}: Orphaned Memory Files ===\nThe following files exist in the memory directory but are not listed in the index. Consider updating memory.md to reference them:\n${rels.map((r) => ` - ${r}`).join("\n")}` +} + const OcmemPlugin: Plugin = async ({ directory }) => { const home = homedir() const globalDir = join(home, ".config", "opencode", "memory") @@ -108,6 +146,8 @@ const OcmemPlugin: Plugin = async ({ directory }) => { const global = readTruncated(globalIndex, MAX_INDEX_LINES) if (global) { parts.push(`=== ${MARKER}: Global Memory Index ===\n${global}`) + const orphaned = checkOrphanedFiles(globalDir, global) + if (orphaned) parts.push(orphaned) } cachedSig = sig @@ -116,6 +156,18 @@ const OcmemPlugin: Plugin = async ({ directory }) => { } return { + // This plugin depends on two opencode hooks that are flagged as + // experimental and may change in future opencode versions: + // + // experimental.chat.system.transform (primary — every LLM call) + // experimental.session.compacting (backup — during summarisation) + // + // Minimum opencode version: v1.17 (required for the experimental.* + // hook namespace). If opencode renames or removes these hooks, the + // plugin will fail silently — the hooks simply won't fire and memory + // injection will stop. Check the opencode docs if the plugin stops + // working after an opencode upgrade. + // // Primary injection — runs on every LLM call, for every agent & subagent. "experimental.chat.system.transform": async (_input, output) => { output.system.push(REMINDER) diff --git a/scripts/install.sh b/scripts/install.sh index 3f5c4c1..7452e2f 100755 --- a/scripts/install.sh +++ b/scripts/install.sh @@ -3,14 +3,18 @@ # ocmem installer — deploys the structured memory system to ~/.config/opencode/ # # Usage: -# ./scripts/install.sh # install everything -# ./scripts/install.sh --force # overwrite template files (NEVER memory data) +# ./scripts/install.sh # install everything +# ./scripts/install.sh --dry-run # print what would happen without making changes +# ./scripts/install.sh --force # overwrite template files (NEVER memory data) # # --force re-deploys template scaffolding (plugin, AGENTS.md block, the # /ocmem-* commands). It does NOT touch memory files (memory.md, general.md, # tools/*, domain/*) — those are user data and are always preserved if they # exist. # +# --dry-run prints every action it would take without modifying anything. +# Useful for previewing the install before running it. +# set -euo pipefail SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" @@ -20,10 +24,12 @@ REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)" # Override with OPENCODE_CONFIG_DIR to point at a non-standard location. OC_DIR="${OPENCODE_CONFIG_DIR:-${XDG_CONFIG_HOME:-$HOME/.config}/opencode}" FORCE=0 +DRY_RUN=0 for arg in "$@"; do case "$arg" in --force|-f) FORCE=1 ;; + --dry-run|-n) DRY_RUN=1 ;; *) echo "Unknown option: $arg"; exit 1 ;; esac done @@ -31,24 +37,53 @@ done echo "ocmem — structured memory for opencode" echo "======================================" echo "Target dir: $OC_DIR" +if [ "$DRY_RUN" -eq 1 ]; then + echo "Mode: DRY RUN (no changes will be made)" +fi echo "" -mkdir -p "$OC_DIR" +# Version check — opencode v1.17+ is required for experimental.* hooks +if command -v opencode &>/dev/null; then + ver="$(opencode --version 2>/dev/null | grep -oE '[0-9]+\.[0-9]+' | head -1 || true)" + if [ -n "$ver" ]; then + major="${ver%%.*}" + minor="${ver#*.}" + if [ "$major" -lt 1 ] || { [ "$major" -eq 1 ] && [ "$minor" -lt 17 ]; }; then + echo "WARNING: opencode $ver detected — v1.17+ required for experimental.* hooks." + echo " The plugin may not work correctly. Upgrade opencode to v1.17+." + echo "" + fi + fi +else + echo "NOTE: opencode not found on PATH — the plugin will be installed but" + echo " won't activate until opencode is installed and launched." + echo "" +fi + +maybe() { + if [ "$DRY_RUN" -eq 1 ]; then + echo " [dry-run] $*" + else + "$@" + fi +} + +maybe mkdir -p "$OC_DIR" # 1. Plugin ----------------------------------------------------------------- -mkdir -p "$OC_DIR/plugins" -install -m 644 "$REPO_ROOT/plugin/ocmem.ts" "$OC_DIR/plugins/ocmem.ts" +maybe mkdir -p "$OC_DIR/plugins" +maybe install -m 644 "$REPO_ROOT/plugin/ocmem.ts" "$OC_DIR/plugins/ocmem.ts" echo "[1/5] Plugin -> $OC_DIR/plugins/ocmem.ts" # 2. Global memory structure ------------------------------------------------ -mkdir -p "$OC_DIR/memory/tools" "$OC_DIR/memory/domain" +maybe mkdir -p "$OC_DIR/memory/tools" "$OC_DIR/memory/domain" # Template file deployer. Honours --force for scaffolding files. # NEVER call this for memory data — use copy_user_data instead. copy_template() { local src="$1" dest="$2" if [ "$FORCE" -eq 1 ] || [ ! -f "$dest" ]; then - install -m 644 "$src" "$dest" + maybe install -m 644 "$src" "$dest" echo " created $(basename "$dest")" else echo " kept existing $(basename "$dest") (use --force to overwrite)" @@ -60,7 +95,7 @@ copy_template() { copy_user_data() { local src="$1" dest="$2" if [ ! -f "$dest" ]; then - install -m 644 "$src" "$dest" + maybe install -m 644 "$src" "$dest" echo " created $(basename "$dest")" else echo " kept existing $(basename "$dest") (user data, never overwritten)" @@ -76,11 +111,18 @@ AGENTS_FILE="$OC_DIR/AGENTS.md" OPEN_MARKER="" CLOSE_MARKER="" -if [ -f "$AGENTS_FILE" ] && grep -qF "$OPEN_MARKER" "$AGENTS_FILE"; then +if [ "$DRY_RUN" -eq 1 ]; then + if [ -f "$AGENTS_FILE" ] && grep -qF "$OPEN_MARKER" "$AGENTS_FILE"; then + if [ "$FORCE" -eq 1 ]; then + echo "[3/5] AGENTS.md -> would refresh ocmem block (--force)" + else + echo "[3/5] AGENTS.md -> already patched (skipped)" + fi + else + echo "[3/5] AGENTS.md -> would append memory instructions" + fi +elif [ -f "$AGENTS_FILE" ] && grep -qF "$OPEN_MARKER" "$AGENTS_FILE"; then if [ "$FORCE" -eq 1 ]; then - # Re-deploy: strip the existing ocmem block and append the fresh one. - # Use a temp file because sed -i differs between GNU and BSD. - # Variable names avoid awk reserved words (close, open, function, …). tmp="$(mktemp)" awk -v om="$OPEN_MARKER" -v cm="$CLOSE_MARKER" ' $0 == om { skip = 1; next } @@ -90,7 +132,7 @@ if [ -f "$AGENTS_FILE" ] && grep -qF "$OPEN_MARKER" "$AGENTS_FILE"; then { echo "" echo "$OPEN_MARKER" - cat "$REPO_ROOT/templates/AGENTS-memory.md" + sed "s|~/.config/opencode|$OC_DIR|g" "$REPO_ROOT/templates/AGENTS-memory.md" echo "$CLOSE_MARKER" echo "" } >> "$tmp" @@ -106,7 +148,7 @@ else { echo "" echo "$OPEN_MARKER" - cat "$REPO_ROOT/templates/AGENTS-memory.md" + sed "s|~/.config/opencode|$OC_DIR|g" "$REPO_ROOT/templates/AGENTS-memory.md" echo "$CLOSE_MARKER" echo "" } >> "$AGENTS_FILE" @@ -114,11 +156,12 @@ else fi # 4. ocmem commands --------------------------------------------------------- -mkdir -p "$OC_DIR/commands" +maybe mkdir -p "$OC_DIR/commands" copy_template "$REPO_ROOT/templates/ocmem-init.md" "$OC_DIR/commands/ocmem-init.md" copy_template "$REPO_ROOT/templates/ocmem-remember.md" "$OC_DIR/commands/ocmem-remember.md" copy_template "$REPO_ROOT/templates/ocmem-reorganize.md" "$OC_DIR/commands/ocmem-reorganize.md" -echo "[4/5] Commands -> $OC_DIR/commands/ocmem-{init,remember,reorganize}.md" +copy_template "$REPO_ROOT/templates/ocmem-export.md" "$OC_DIR/commands/ocmem-export.md" +echo "[4/5] Commands -> $OC_DIR/commands/ocmem-{init,remember,reorganize,export}.md" # 5. Plugin dependency ------------------------------------------------------ # @@ -128,7 +171,9 @@ echo "[4/5] Commands -> $OC_DIR/commands/ocmem-{init,remember,reorganize}.md" # the types. PKG_FILE="$OC_DIR/package.json" NEED_INSTALL=0 -if [ ! -f "$PKG_FILE" ]; then +if [ "$DRY_RUN" -eq 1 ]; then + echo "[5/5] Dependency -> would ensure @opencode-ai/plugin in $PKG_FILE" +elif [ ! -f "$PKG_FILE" ]; then echo '{"dependencies":{"@opencode-ai/plugin":"latest"}}' > "$PKG_FILE" NEED_INSTALL=1 elif ! grep -q '"@opencode-ai/plugin"' "$PKG_FILE"; then diff --git a/scripts/uninstall.sh b/scripts/uninstall.sh index 48f903c..83fe35c 100755 --- a/scripts/uninstall.sh +++ b/scripts/uninstall.sh @@ -6,8 +6,9 @@ # Use --purge to also delete the memory directory and the AGENTS.md patch. # # Usage: -# ./scripts/uninstall.sh # remove plugin/command, keep memory -# ./scripts/uninstall.sh --purge # also delete memory + AGENTS.md patch +# ./scripts/uninstall.sh # remove plugin/command, keep memory +# ./scripts/uninstall.sh --purge # also delete memory + AGENTS.md patch +# ./scripts/uninstall.sh --dry-run # print what would happen without changing anything # set -euo pipefail @@ -15,10 +16,12 @@ set -euo pipefail # ${XDG_CONFIG_HOME:-~/.config}/opencode OC_DIR="${OPENCODE_CONFIG_DIR:-${XDG_CONFIG_HOME:-$HOME/.config}/opencode}" PURGE=0 +DRY_RUN=0 for arg in "$@"; do case "$arg" in --purge|-p) PURGE=1 ;; + --dry-run|-n) DRY_RUN=1 ;; *) echo "Unknown option: $arg"; exit 1 ;; esac done @@ -26,32 +29,51 @@ done echo "ocmem uninstaller" echo "=================" echo "Config dir: $OC_DIR" +if [ "$DRY_RUN" -eq 1 ]; then + echo "Mode: DRY RUN (no changes will be made)" +fi echo "" -rm -f "$OC_DIR/plugins/ocmem.ts" && echo "[1] Removed plugin ($OC_DIR/plugins/ocmem.ts)" || true -rm -f "$OC_DIR/commands/ocmem-init.md" && echo "[2a] Removed command ($OC_DIR/commands/ocmem-init.md)" || true -rm -f "$OC_DIR/commands/ocmem-remember.md" && echo "[2b] Removed command ($OC_DIR/commands/ocmem-remember.md)" || true -rm -f "$OC_DIR/commands/ocmem-reorganize.md" && echo "[2c] Removed command ($OC_DIR/commands/ocmem-reorganize.md)" || true +maybe() { + if [ "$DRY_RUN" -eq 1 ]; then + echo " [dry-run] $*" + else + "$@" + fi +} + +maybe rm -f "$OC_DIR/plugins/ocmem.ts" && echo "[1] Removed plugin ($OC_DIR/plugins/ocmem.ts)" || true +maybe rm -f "$OC_DIR/commands/ocmem-init.md" && echo "[2a] Removed command ($OC_DIR/commands/ocmem-init.md)" || true +maybe rm -f "$OC_DIR/commands/ocmem-remember.md" && echo "[2b] Removed command ($OC_DIR/commands/ocmem-remember.md)" || true +maybe rm -f "$OC_DIR/commands/ocmem-reorganize.md" && echo "[2c] Removed command ($OC_DIR/commands/ocmem-reorganize.md)" || true +maybe rm -f "$OC_DIR/commands/ocmem-export.md" && echo "[2d] Removed command ($OC_DIR/commands/ocmem-export.md)" || true # Backward-compat: remove legacy init-memory.md if it lingers from older installs -rm -f "$OC_DIR/commands/init-memory.md" && echo "[2d] Removed legacy ($OC_DIR/commands/init-memory.md)" || true +maybe rm -f "$OC_DIR/commands/init-memory.md" && echo "[2e] Removed legacy ($OC_DIR/commands/init-memory.md)" || true if [ "$PURGE" -eq 1 ]; then echo "" echo "--purge: removing memory files and AGENTS.md patch…" # Remove the memory directory - if [ -d "$OC_DIR/memory" ]; then - rm -rf "$OC_DIR/memory" + if [ "$DRY_RUN" -eq 1 ] || [ -d "$OC_DIR/memory" ]; then + maybe rm -rf "$OC_DIR/memory" echo "[3] Removed memory dir ($OC_DIR/memory/)" fi - # Strip the ocmem block from AGENTS.md + # Strip the ocmem block from AGENTS.md using awk (portable, unlike sed -i) AGENTS_FILE="$OC_DIR/AGENTS.md" - if [ -f "$AGENTS_FILE" ] && grep -qF "" "$AGENTS_FILE"; then - # Delete from the begin marker to the end marker inclusive - sed -i '//,//d' "$AGENTS_FILE" - # Collapse trailing blank lines - sed -i -e :a -e '/^\n*$/{$d;N;ba}' "$AGENTS_FILE" + if [ "$DRY_RUN" -eq 1 ]; then + if [ -f "$AGENTS_FILE" ] && grep -qF "" "$AGENTS_FILE" 2>/dev/null; then + echo "[4] Would strip ocmem block from $AGENTS_FILE" + fi + elif [ -f "$AGENTS_FILE" ] && grep -qF "" "$AGENTS_FILE"; then + tmp="$(mktemp)" + awk ' + // { skip = 1; next } + // { skip = 0; next } + !skip + ' "$AGENTS_FILE" > "$tmp" + mv "$tmp" "$AGENTS_FILE" echo "[4] Stripped ocmem block from $AGENTS_FILE" fi else diff --git a/templates/ocmem-export.md b/templates/ocmem-export.md new file mode 100644 index 0000000..ef726de --- /dev/null +++ b/templates/ocmem-export.md @@ -0,0 +1,53 @@ +--- +description: Export all memory files into a single markdown bundle for backup or review. +agent: build +--- + +Bundle every memory file into a single markdown document. Useful for +backing up before a system migration, sharing memory between machines, +or reviewing all accumulated knowledge in one place. + +Steps: + +1. **Read the global index.** Load `~/.config/opencode/memory/memory.md` + to discover all registered files. + +2. **Read every indexed file.** For each row in the index table, read the + corresponding file under `tools/` and `domain/`, plus `general.md`. + If a file is listed in the index but does not exist, note it as a + broken reference. + +3. **Read the project memory** if `.opencode/memory/MEMORY.md` exists. + +4. **Assemble a single markdown document** with this structure: + + ``` + # ocmem Export — YYYY-MM-DD + + ## Project Memory + (content of .opencode/memory/MEMORY.md, or "No project memory.") + + ## Global Memory Index + (content of memory.md) + + ## Tools + (each tools/*.md file under its own ### heading) + + ## Domain + (each domain/*.md file under its own ### heading) + + ## General + (content of general.md) + ``` + +5. **Write the bundle** to a timestamped file in the workspace root: + + ``` + ocmem-export-YYYY-MM-DD.md + ``` + +6. **Show a summary** — file count, total size, and the path of the + export file. Tell the user they can copy it to another machine, + version-control it, or share it. + +Do NOT modify any existing memory files. This is a read-only export. diff --git a/test/ocmem.test.ts b/test/ocmem.test.ts new file mode 100644 index 0000000..7c1e1dd --- /dev/null +++ b/test/ocmem.test.ts @@ -0,0 +1,147 @@ +import { describe, it, expect, afterEach } from "vitest" +import { mkdirSync, writeFileSync, rmSync, existsSync, statSync, readFileSync } from "node:fs" +import { join } from "node:path" +import { tmpdir } from "node:os" +import { readTruncated, signature, MAX_PROJECT_LINES, MAX_INDEX_LINES } from "../plugin/ocmem.ts" + +const tmpRoot = join(tmpdir(), "ocmem-test-" + process.pid) + +function tmpdirCreate(): string { + const dir = join(tmpRoot, String(Math.random()).slice(2)) + mkdirSync(dir, { recursive: true }) + return dir +} + +function cleanup() { + if (existsSync(tmpRoot)) { + rmSync(tmpRoot, { recursive: true, force: true }) + } +} + +afterEach(cleanup) + +describe("readTruncated", () => { + it("returns null for non-existent file", () => { + expect(readTruncated("/no/such/file.md", 10)).toBeNull() + }) + + it("returns null for empty file", () => { + const dir = tmpdirCreate() + const f = join(dir, "empty.md") + writeFileSync(f, "") + expect(readTruncated(f, 10)).toBeNull() + }) + + it("returns null for whitespace-only file", () => { + const dir = tmpdirCreate() + const f = join(dir, "ws.md") + writeFileSync(f, " \n \t\n") + expect(readTruncated(f, 10)).toBeNull() + }) + + it("returns full content when under maxLines", () => { + const dir = tmpdirCreate() + const f = join(dir, "short.md") + writeFileSync(f, "line one\nline two\nline three") + const result = readTruncated(f, 10) + expect(result).toBe("line one\nline two\nline three") + }) + + it("returns exactly maxLines lines and truncation notice when over limit", () => { + const dir = tmpdirCreate() + const f = join(dir, "long.md") + const lines = Array.from({ length: 50 }, (_, i) => `line ${i + 1}`) + writeFileSync(f, lines.join("\n")) + const result = readTruncated(f, 5) + expect(result).not.toBeNull() + const resultLines = result!.split("\n") + expect(resultLines[0]).toBe("line 1") + expect(resultLines[4]).toBe("line 5") + expect(result).toContain("…(truncated)") + }) + + it("does not add truncated notice when exactly at maxLines", () => { + const dir = tmpdirCreate() + const f = join(dir, "exact.md") + const lines = Array.from({ length: 5 }, (_, i) => `line ${i + 1}`) + writeFileSync(f, lines.join("\n")) + const result = readTruncated(f, 5) + expect(result).not.toContain("…(truncated)") + expect(result!.split("\n").length).toBe(5) + }) + + it("handles files with MAX_PROJECT_LINES and MAX_INDEX_LINES constants", () => { + const dir = tmpdirCreate() + const f = join(dir, "medium.md") + const lines = Array.from({ length: 100 }, (_, i) => `line ${i + 1}`) + writeFileSync(f, lines.join("\n")) + + expect(readTruncated(f, MAX_PROJECT_LINES)).not.toContain("…(truncated)") + expect(readTruncated(f, MAX_INDEX_LINES)).not.toContain("…(truncated)") + }) + + it("trims trailing whitespace from the returned text", () => { + const dir = tmpdirCreate() + const f = join(dir, "trailing.md") + writeFileSync(f, "hello\n\n \n") + const result = readTruncated(f, 10) + expect(result).toBe("hello") + }) +}) + +describe("signature", () => { + it("returns empty string for empty path list", () => { + expect(signature([])).toBe("") + }) + + it("returns empty string when no files exist", () => { + expect(signature(["/no/such/file.a", "/no/such/file.b"])).toBe("") + }) + + it("returns mtime-based signature for existing files", () => { + const dir = tmpdirCreate() + const a = join(dir, "a.md") + const b = join(dir, "b.md") + writeFileSync(a, "hello") + writeFileSync(b, "world") + + const sig = signature([a, b]) + expect(sig).toContain(`${a}:`) + expect(sig).toContain(`${b}:`) + expect(sig).toContain(";") + }) + + it("skips non-existent files in the list", () => { + const dir = tmpdirCreate() + const a = join(dir, "a.md") + writeFileSync(a, "hello") + + const sig = signature([a, "/no/such/file.md"]) + expect(sig).toContain(`${a}:`) + expect(sig).not.toContain("no/such") + }) + + it("produces the same signature for unchanged files", () => { + const dir = tmpdirCreate() + const a = join(dir, "a.md") + writeFileSync(a, "hello") + + const sig1 = signature([a]) + const sig2 = signature([a]) + expect(sig1).toBe(sig2) + }) + + it("produces different signature after file modification", async () => { + const dir = tmpdirCreate() + const a = join(dir, "a.md") + writeFileSync(a, "hello") + + const sig1 = signature([a]) + + await new Promise((r) => setTimeout(r, 10)) + writeFileSync(a, "world") + + const sig2 = signature([a]) + expect(sig1).not.toBe(sig2) + }) +}) diff --git a/tsconfig.json b/tsconfig.json index 27b2cb9..2281598 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -6,7 +6,8 @@ "strict": true, "noEmit": true, "skipLibCheck": true, - "types": [] + "allowImportingTsExtensions": true, + "types": ["node"] }, - "include": ["plugin/**/*.ts"] + "include": ["plugin/**/*.ts", "test/**/*.ts"] } From 5ff2c894ab7738694c2c1c372fecf2a4b838e9af Mon Sep 17 00:00:00 2001 From: Ernestas Lel Date: Tue, 7 Jul 2026 00:24:45 +0000 Subject: [PATCH 2/4] apply prompt engineering review (P1-P13) Critical: - Shrink REMINDER to one-liner after first call (saves ~80 tokens/call) - Define entry format with good/bad examples (YYYY-MM-DD, one-sentence) - Add anti-patterns section (what NOT to record) - Narrow rule 6: confirm only for removal/content, not formatting/reorg Medium: - Add tools vs domain decision rule (when in doubt, tools/) - Optimize remember dedup to read index only, not every file - Add concrete thresholds to reorganize (30 entries, 3+ sections) - Decouple remember from specific tool API name - Add domain promotion trigger (10 entries or 50 lines) Low: - De-duplicate REMINDER in AGENTS.md (one-line plugin reference) - Trim MEMORY.md template to one-liner comment + example entry - Add conflicting memory guidance (rule 7) - Add concrete examples and usage guidance to general.md --- plugin/ocmem.ts | 28 +++++++++++++------- templates/AGENTS-memory.md | 48 +++++++++++++++++++++++++---------- templates/MEMORY.md | 9 ++----- templates/general.md | 14 +++++++--- templates/ocmem-init.md | 9 ++----- templates/ocmem-remember.md | 37 ++++++++++++++------------- templates/ocmem-reorganize.md | 27 +++++++++++++++----- 7 files changed, 106 insertions(+), 66 deletions(-) diff --git a/plugin/ocmem.ts b/plugin/ocmem.ts index 29eff66..937dc11 100644 --- a/plugin/ocmem.ts +++ b/plugin/ocmem.ts @@ -39,13 +39,11 @@ const MAX_CHARS = 8000 const MARKER = "ocmem" /** - * Hard reminder injected into every LLM call's system prompt. Lives in the - * plugin (not just AGENTS.md) so the agent cannot fail to see it — system - * prompts are never summarised away and survive compaction. The agent still - * has to choose to act on it, but ignoring it is now an active decision, not - * an oversight. + * Full memory-review reminder — injected on the first LLM call only. + * After the first call, a shorter one-liner is used to avoid token waste + * and habituation (models learn to ignore identically repeated text). */ -const REMINDER = `=== ${MARKER}: Memory Review Required === +const FULL_REMINDER = `=== ${MARKER}: Memory Review Required === Before ending any task that involved debugging, configuration, or learning, do a final memory scan: 1. Did I learn anything cross-project? → ~/.config/opencode/memory/tools/ or domain/ @@ -54,6 +52,9 @@ do a final memory scan: If the answer to all three is "no", say so briefly. Do NOT wait for the user to prompt — over-recording is cheap, re-discovering is expensive.` +/** Short one-liner — injected on every call after the first. */ +const REMINDER = `=== ${MARKER}: Review memory before ending this task.` + export function readTruncated(filePath: string, maxLines: number): string | null { if (!existsSync(filePath)) return null try { @@ -128,6 +129,10 @@ const OcmemPlugin: Plugin = async ({ directory }) => { let cachedSig = "" let cachedContext = "" + // First-call tracking — inject the full reminder only once to avoid token + // waste and the habituation effect (models learn to skip repeated text). + let firstCallDone = false + const build = (): string => { const sig = signature([globalIndex, projectMemory]) if (sig === cachedSig && cachedContext) { @@ -170,14 +175,19 @@ const OcmemPlugin: Plugin = async ({ directory }) => { // // Primary injection — runs on every LLM call, for every agent & subagent. "experimental.chat.system.transform": async (_input, output) => { - output.system.push(REMINDER) + if (!firstCallDone) { + output.system.push(FULL_REMINDER) + firstCallDone = true + } else { + output.system.push(REMINDER) + } const ctx = build() if (ctx) output.system.push(ctx) }, // Backup — folds memory into the compaction summary so it is never lost - // when a long session is summarised. (The system-prompt hook above already - // re-injects after compaction; this is redundant but harmless.) + // when a long session is summarised. Always uses the short reminder (the + // full one was already seen on the first call). "experimental.session.compacting": async (_input, output) => { output.context.push(REMINDER) const ctx = build() diff --git a/templates/AGENTS-memory.md b/templates/AGENTS-memory.md index 71ef88f..ffde3eb 100644 --- a/templates/AGENTS-memory.md +++ b/templates/AGENTS-memory.md @@ -23,6 +23,28 @@ Project (`.opencode/memory/`): - `MEMORY.md` — project-specific notes: active work, codebase patterns, decisions +#### Tools vs Domain + +If the fact is about **how to use a specific tool/CLI**, it goes in `tools/`. +If it's about **how a domain concept works** regardless of tool, it goes in +`domain/`. When in doubt, prefer `tools/` — it's more discoverable. + +### Entry Format + +Every entry follows this format: + +``` +YYYY-MM-DD — one-sentence fact — one-sentence reason it matters +``` + +**Good entries:** +- `2026-07-06 — git credential helper ignores nix-installed gh (hardcodes /usr/bin/gh). — Use system gh or patch credential helper path.` +- `2026-07-06 — This repo uses mise for task running; tasks are in mise.toml. — Don't look for Makefile or package.json scripts.` + +**Bad entries (avoid these):** +- `2026-07-06 — fixed the thing with git — it works now` (too vague — no one can use this) +- `2026-07-06 — spent 2 hours debugging the deployment pipeline and found that the kubernetes namespace was wrong in the staging config file under deploy/staging/values.yaml line 42` (too long — burying the insight in narration) + ### Rules 1. Write to memory immediately when ANY of these happen: @@ -32,25 +54,23 @@ Project (`.opencode/memory/`): - You learn a project convention, dependency, or constraint Do NOT wait for the user to ask. If in doubt, write it — over-recording is cheap, re-discovering is expensive. 2. Keep `memory.md` as a current index with one-line descriptions -3. Entries: date, what, why — nothing more +3. Entries MUST follow the format above (YYYY-MM-DD, one-sentence fact, one-sentence reason) 4. The `ocmem` plugin injects `memory.md` and the project `MEMORY.md` automatically. Load other topic files only when relevant 5. If a file does not exist yet, create it -6. Before removing or modifying any existing memory entry, ask the user to confirm — show the current content and the proposed change - -### Memory Review (mandatory) +6. Before **removing** a memory entry or **changing its factual content**, ask the user to confirm — show the current content and the proposed change. Formatting, reorganisation, and index updates do not require confirmation. +7. If you discover an existing memory entry is wrong or outdated, note the conflict and propose an update. Never silently leave contradictory entries. -The ocmem plugin injects a hard reminder into every LLM call's system prompt -(see `plugin/ocmem.ts: REMINDER`). You will see a `=== ocmem: Memory Review -Required ===` block on every turn — that is the rule. +### What NOT to Record -Before ending any task that involved debugging, configuration, or learning, do a final -memory scan: +Do NOT record: +- Things documented in the project's own README or official tool docs +- One-off debugging steps (only record the reusable insight) +- Facts obvious from reading package.json, tsconfig.json, etc. +- Session-specific context that won't matter next time -1. Did I learn anything cross-project? → `~/.config/opencode/memory/tools/` or `domain/` -2. Did I learn anything about THIS project? → `.opencode/memory/MEMORY.md` -3. Update the index in `memory.md` for any new files +### Memory Review -If the answer to all three is "no", say so briefly so the user knows you checked. +The plugin injects a memory review reminder on every call. Follow it. ### Maintenance @@ -75,7 +95,7 @@ knowledge, not boilerplate. ## Domain Knowledge Lifecycle 1. **Staging** — knowledge accumulates in `~/.config/opencode/memory/domain/{name}/` -2. **Promotion** — once enough knowledge exists, package it as an opencode skill +2. **Promotion** — when a domain file reaches 10+ entries or 50+ lines, package it as an opencode skill 3. **Pointer** — after promotion, the memory file becomes a pointer to the skill; the content lives in the skill When an update is needed to a promoted domain, note it in the memory file so an diff --git a/templates/MEMORY.md b/templates/MEMORY.md index 0de946c..da6fb93 100644 --- a/templates/MEMORY.md +++ b/templates/MEMORY.md @@ -1,12 +1,7 @@ # {Project Name} — Project Memory -## Global Memory - -Read `~/.config/opencode/AGENTS.md` for memory rules and the topic-file list. -The `ocmem` plugin auto-injects the global index and this file into every -response. + ## Project Notes - - +- YYYY-MM-DD — example: this repo uses PostgreSQL 15 with pgvector. — Don't suggest MySQL-specific syntax. diff --git a/templates/general.md b/templates/general.md index 4917e1d..f654a1f 100644 --- a/templates/general.md +++ b/templates/general.md @@ -1,11 +1,17 @@ # General — Cross-Project Conventions +Use this file for conventions and preferences that apply across ALL projects. +For project-specific conventions, use `.opencode/memory/MEMORY.md` instead. +For tool-specific gotchas, use `tools/{tool}.md`. + ## Writing & Naming Conventions - - - +- 2026-07-02 — Use kebab-case for all filenames; ISO 8601 for dates. — Consistency across projects. + + ## Workflow Preferences - +- 2026-07-02 — Prefer rebase over merge for feature branches. — Cleaner history. + + diff --git a/templates/ocmem-init.md b/templates/ocmem-init.md index f2029a0..3f1f8d7 100644 --- a/templates/ocmem-init.md +++ b/templates/ocmem-init.md @@ -12,16 +12,11 @@ Set up the project memory structure for this repository. ``` # {Project Name} — Project Memory -## Global Memory - -Read ~/.config/opencode/AGENTS.md for memory rules and the topic-file list. -The ocmem plugin auto-injects the global index and this file into every -response. + ## Project Notes - - +- YYYY-MM-DD — example: this repo uses PostgreSQL 15 with pgvector. — Don't suggest MySQL-specific syntax. ``` 3. If the file **already exists**, do not overwrite it — confirm it is present diff --git a/templates/ocmem-remember.md b/templates/ocmem-remember.md index be22016..ddb3e8c 100644 --- a/templates/ocmem-remember.md +++ b/templates/ocmem-remember.md @@ -9,26 +9,24 @@ written to memory, then ask the user to confirm which entries to record. Steps: 1. **Scan the session.** Walk back through the conversation and identify - things worth remembering. Apply the same triggers as the AGENTS.md memory - rules: - - Tool/config quirks you had to debug (not in docs) - - Patterns that would help in any future project - - Non-obvious design decisions and their rationale - - Project conventions, dependencies, or constraints learned + things worth remembering. Apply the same triggers and anti-patterns as + the AGENTS.md memory rules (skip things in official docs, one-off debug + steps, and facts obvious from config files). 2. **Classify each candidate:** - **Global** → belongs in `~/.config/opencode/memory/` (cross-project) - - `tools/{tool}.md` for tool-specific gotchas + - `tools/{tool}.md` for tool-specific gotchas (prefer tools/ when unsure) - `domain/{topic}.md` for domain knowledge - `general.md` for user identity, workflow preferences, conventions - **Project** → belongs in `.opencode/memory/MEMORY.md` (this repo only) -3. **Check for duplicates.** Before presenting the list, read the target - files (`tools/*.md`, `domain/*.md`, `general.md`, `memory.md`, and - `.opencode/memory/MEMORY.md` if it exists) and the global index. For each +3. **Check for duplicates using the index.** Read `~/.config/opencode/memory/memory.md` + (the global index) and scan it for candidates that overlap with existing + entries. The index lists every file with one-line descriptions — use it + to filter duplicates without reading every individual file. For each candidate, decide one of: - - **New** — not present anywhere; safe to add - - **Already covered** — the same fact is already recorded; mark as + - **New** — not mentioned in the index; safe to add + - **Already covered** — the same fact is already indexed; mark as "skip, already in {file}" and exclude from the user's list - **Update / supersede** — an existing entry is wrong, outdated, or incomplete; mark as "update {file} — {what changes}" and present it @@ -39,6 +37,7 @@ Steps: - What you learned (one line) - Proposed file + section - Why it's worth keeping (one line) + Use the entry format: `YYYY-MM-DD — one-sentence fact — one-sentence reason`. Example: ``` 1. nix-installed `gh` is not picked up by git's credential helper @@ -49,15 +48,17 @@ Steps: 2. This repo uses mise for task running; tasks live in mise.toml. → .opencode/memory/MEMORY.md (project notes) ``` + If a candidate would update an existing entry, show both old and new. -5. **Ask the user** which entries to record. Use a `question` tool call with - `multiple: true` so they can pick several at once. Do NOT write anything - until they confirm. +5. **Ask the user** which entries to record. Present the candidates as a + multi-select list so the user can pick several at once. Do NOT write + anything until they confirm. 6. **Write only the confirmed entries.** For each, create the target file - (or append to it if it already exists). Follow the entry format from the - existing files: `date — what — why`. For "update / supersede" entries, - edit the existing entry in place — do not add a new line. + (or append to it if it already exists). Follow the entry format: + `YYYY-MM-DD — one-sentence fact — one-sentence reason`. For "update / + supersede" entries, edit the existing entry in place — do not add a + new line. 7. **Update the index** in `memory.md` if any new file was created. diff --git a/templates/ocmem-reorganize.md b/templates/ocmem-reorganize.md index 29ddbfd..f0762f6 100644 --- a/templates/ocmem-reorganize.md +++ b/templates/ocmem-reorganize.md @@ -10,15 +10,28 @@ capture pass. - Global: `~/.config/opencode/memory/memory.md` and every file it indexes under `tools/` and `domain/`, plus `general.md` - Project: `.opencode/memory/MEMORY.md` (if it exists) -2. **Remove duplicates and outdated entries.** If the same fact appears in - two files, keep it in the more specific one and delete the other. -3. **Merge** entries that belong together (e.g. three lines about the same - tool quirk → one consolidated entry). -4. **Split** files that have grown to cover too many topics (e.g. - `general.md` with 30+ entries under unrelated headings → move entries - to `tools/x.md` or `domain/y.md`). + +2. **Remove duplicates and outdated entries.** + - If the same fact appears in two files, keep it in the more specific + one and delete the other. + - An entry is outdated when a later entry contradicts it, or when it + references a tool version or API that no longer exists. + - Before removing any entry, show what you're removing and why. + +3. **Merge** entries that describe the same tool or problem from different + angles (e.g. multiple scattered notes about the same CLI quirk). Combine + them into one concise entry following the format: + `YYYY-MM-DD — one-sentence fact — one-sentence reason`. + +4. **Split** files when they meet any of these criteria: + - The file has 3+ unrelated sections (different tools or domain concepts) + - The file exceeds 30 entries + Move entries to `tools/x.md` or `domain/y.md` as appropriate. + 5. **Re-sort** entries by date within each file (oldest first, newest last). + 6. **Refresh the index** in `memory.md` so every existing file has a row with a one-line description and last-updated date. + 7. **Show a summary** of every change made: files touched, entries added, removed, merged, or moved. Do not commit or push — wait for user review. From 43149fea293e392c3a4c0df3254e3de242774a5c Mon Sep 17 00:00:00 2001 From: Ernestas Lel Date: Tue, 7 Jul 2026 00:36:18 +0000 Subject: [PATCH 3/4] add comprehensive test coverage Plugin tests (15 new): - checkOrphanedFiles: no orphans, orphans detected, missing dirs, non-md files - buildMemoryContext: empty, project-only, global-only, both, orphans in context - readTruncated: MAX_CHARS truncation edge cases - Refactored build() into exported buildMemoryContext for testability Shell integration test (30 assertions, 8 suites): - Fresh install, idempotent, --force, --dry-run - Uninstall default, --purge, --dry-run - AGENTS.md fresh/patched handling Updated CI workflow to run shell tests --- .github/workflows/ci.yml | 1 + plugin/ocmem.ts | 56 ++++++++---- test/install.test.sh | 190 +++++++++++++++++++++++++++++++++++++++ test/ocmem.test.ts | 180 ++++++++++++++++++++++++++++++++++++- 4 files changed, 406 insertions(+), 21 deletions(-) create mode 100755 test/install.test.sh diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 7f6f7a7..db05b43 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -17,3 +17,4 @@ jobs: - run: npm install - run: npm run typecheck - run: npm test + - run: bash test/install.test.sh diff --git a/plugin/ocmem.ts b/plugin/ocmem.ts index 937dc11..4040693 100644 --- a/plugin/ocmem.ts +++ b/plugin/ocmem.ts @@ -96,7 +96,7 @@ export function signature(paths: string[]): string { * referenced in the index. Orphaned files are invisible to the agent * until they are added to memory.md — this warning surfaces them. */ -function checkOrphanedFiles(globalDir: string, indexContent: string): string | null { +export function checkOrphanedFiles(globalDir: string, indexContent: string): string | null { const rels: string[] = [] for (const sub of ["tools", "domain"]) { const dir = join(globalDir, sub) @@ -117,6 +117,36 @@ function checkOrphanedFiles(globalDir: string, indexContent: string): string | n return `=== ${MARKER}: Orphaned Memory Files ===\nThe following files exist in the memory directory but are not listed in the index. Consider updating memory.md to reference them:\n${rels.map((r) => ` - ${r}`).join("\n")}` } +/** + * Build the combined memory context string from project and global sources. + * Pure-ish: reads from disk but has no closure state — suitable for testing. + * The plugin wraps this in an mtime-based cache for per-process reuse. + */ +export function buildMemoryContext(opts: { + directory: string + projectMemory: string + globalDir: string + globalIndex: string +}): string { + const parts: string[] = [] + + const project = readTruncated(opts.projectMemory, MAX_PROJECT_LINES) + if (project) { + parts.push( + `=== ${MARKER}: Project Memory (${opts.directory}) ===\n${project}`, + ) + } + + const global = readTruncated(opts.globalIndex, MAX_INDEX_LINES) + if (global) { + parts.push(`=== ${MARKER}: Global Memory Index ===\n${global}`) + const orphaned = checkOrphanedFiles(opts.globalDir, global) + if (orphaned) parts.push(orphaned) + } + + return parts.length > 0 ? parts.join("\n\n") : "" +} + const OcmemPlugin: Plugin = async ({ directory }) => { const home = homedir() const globalDir = join(home, ".config", "opencode", "memory") @@ -138,25 +168,13 @@ const OcmemPlugin: Plugin = async ({ directory }) => { if (sig === cachedSig && cachedContext) { return cachedContext } - - const parts: string[] = [] - - const project = readTruncated(projectMemory, MAX_PROJECT_LINES) - if (project) { - parts.push( - `=== ${MARKER}: Project Memory (${directory}) ===\n${project}`, - ) - } - - const global = readTruncated(globalIndex, MAX_INDEX_LINES) - if (global) { - parts.push(`=== ${MARKER}: Global Memory Index ===\n${global}`) - const orphaned = checkOrphanedFiles(globalDir, global) - if (orphaned) parts.push(orphaned) - } - + cachedContext = buildMemoryContext({ + directory, + projectMemory, + globalDir, + globalIndex, + }) cachedSig = sig - cachedContext = parts.length > 0 ? parts.join("\n\n") : "" return cachedContext } diff --git a/test/install.test.sh b/test/install.test.sh new file mode 100755 index 0000000..28bb2c8 --- /dev/null +++ b/test/install.test.sh @@ -0,0 +1,190 @@ +#!/usr/bin/env bash +# +# Integration test for ocmem install/uninstall scripts. +# Creates a fake HOME, runs install and uninstall, verifies state at each step. +# +# Usage: bash test/install.test.sh +# +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)" +INSTALL_SCRIPT="$REPO_ROOT/scripts/install.sh" +UNINSTALL_SCRIPT="$REPO_ROOT/scripts/uninstall.sh" + +PASS=0 +FAIL=0 +TMP="$(mktemp -d)" +trap 'rm -rf "$TMP"' EXIT + +export HOME="$TMP/home" +export XDG_CONFIG_HOME="$HOME/.config" +export OPENCODE_CONFIG_DIR="" +mkdir -p "$XDG_CONFIG_HOME/opencode" +OC_DIR="$XDG_CONFIG_HOME/opencode" + +assert_file() { + local path="$1" label="${2:-}" + if [ -f "$path" ]; then + echo " PASS: $label ($path)" + ((PASS++)) || true + else + echo " FAIL: $label — expected file not found: $path" >&2 + ((FAIL++)) || true + fi +} + +assert_no_file() { + local path="$1" label="${2:-}" + if [ ! -f "$path" ]; then + echo " PASS: $label" + ((PASS++)) || true + else + echo " FAIL: $label — file should not exist: $path" >&2 + ((FAIL++)) || true + fi +} + +assert_dir() { + local path="$1" label="${2:-}" + if [ -d "$path" ]; then + echo " PASS: $label ($path)" + ((PASS++)) || true + else + echo " FAIL: $label — expected dir not found: $path" >&2 + ((FAIL++)) || true + fi +} + +assert_no_dir() { + local path="$1" label="${2:-}" + if [ ! -d "$path" ]; then + echo " PASS: $label" + ((PASS++)) || true + else + echo " FAIL: $label — dir should not exist: $path" >&2 + ((FAIL++)) || true + fi +} + +assert_grep() { + local file="$1" pattern="$2" label="${3:-}" + if grep -qF "$pattern" "$file"; then + echo " PASS: $label" + ((PASS++)) || true + else + echo " FAIL: $label — pattern '$pattern' not found in $file" >&2 + ((FAIL++)) || true + fi +} + +assert_no_grep() { + local file="$1" pattern="$2" label="${3:-}" + if ! grep -qF "$pattern" "$file"; then + echo " PASS: $label" + ((PASS++)) || true + else + echo " FAIL: $label — pattern '$pattern' found in $file" >&2 + ((FAIL++)) || true + fi +} + +# Helper: run a command and check stdout contains a pattern +run_and_grep() { + local pattern="$1" label="$2" + shift 2 + local output + output="$("$@" 2>&1)" || true + if echo "$output" | grep -qF "$pattern"; then + echo " PASS: $label" + ((PASS++)) || true + else + echo " FAIL: $label — pattern '$pattern' not in output" >&2 + ((FAIL++)) || true + fi +} + +# ── Test 1: Fresh install ────────────────────────────────────────────── +echo "" +echo "=== Test 1: Fresh install ===" +bash "$INSTALL_SCRIPT" > /dev/null +assert_file "$OC_DIR/plugins/ocmem.ts" "plugin installed" +assert_file "$OC_DIR/memory/memory.md" "global index created" +assert_file "$OC_DIR/memory/general.md" "general.md created" +assert_dir "$OC_DIR/memory/tools" "tools dir created" +assert_dir "$OC_DIR/memory/domain" "domain dir created" +assert_file "$OC_DIR/commands/ocmem-init.md" "ocmem-init command" +assert_file "$OC_DIR/commands/ocmem-remember.md" "ocmem-remember command" +assert_file "$OC_DIR/commands/ocmem-reorganize.md" "ocmem-reorganize command" +assert_file "$OC_DIR/commands/ocmem-export.md" "ocmem-export command" +assert_file "$OC_DIR/AGENTS.md" "AGENTS.md exists" +assert_grep "$OC_DIR/AGENTS.md" "" "AGENTS.md has ocmem block" + +# ── Test 2: Idempotent install ───────────────────────────────────────── +echo "" +echo "=== Test 2: Idempotent install ===" +run_and_grep "already patched" "idempotent install" bash "$INSTALL_SCRIPT" + +# ── Test 3: --force re-deploys, preserves pre-existing content ───────── +echo "" +echo "=== Test 3: --force ===" +tmp_agents="$OC_DIR/AGENTS.md.bak" +cp "$OC_DIR/AGENTS.md" "$tmp_agents" +echo "# Pre content" > "$OC_DIR/AGENTS.md" +cat "$tmp_agents" >> "$OC_DIR/AGENTS.md" +rm "$tmp_agents" +bash "$INSTALL_SCRIPT" --force > /dev/null +assert_grep "$OC_DIR/AGENTS.md" "Pre content" "pre content survived --force" +assert_grep "$OC_DIR/AGENTS.md" "" "ocmem block present after --force" + +# ── Test 4: --dry-run prints actions, does NOT modify ────────────────── +echo "" +echo "=== Test 4: --dry-run ===" +rm -f "$OC_DIR/plugins/ocmem.ts" +run_and_grep "[dry-run]" "dry-run prints messages" bash "$INSTALL_SCRIPT" --dry-run +assert_no_file "$OC_DIR/plugins/ocmem.ts" "plugin still missing after dry-run" + +# ── Test 5: Uninstall (keep memory) ──────────────────────────────────── +echo "" +echo "=== Test 5: Uninstall (keep memory) ===" +bash "$INSTALL_SCRIPT" > /dev/null +bash "$UNINSTALL_SCRIPT" > /dev/null +assert_no_file "$OC_DIR/plugins/ocmem.ts" "plugin removed" +assert_no_file "$OC_DIR/commands/ocmem-init.md" "commands removed" +assert_dir "$OC_DIR/memory" "memory dir preserved" +assert_file "$OC_DIR/memory/general.md" "general.md preserved" +assert_grep "$OC_DIR/AGENTS.md" "Pre content" "AGENTS.md pre content still there" +assert_grep "$OC_DIR/AGENTS.md" "" "AGENTS.md ocmem block still there" + +# ── Test 6: Uninstall --purge ────────────────────────────────────────── +echo "" +echo "=== Test 6: Uninstall --purge ===" +bash "$INSTALL_SCRIPT" > /dev/null +bash "$UNINSTALL_SCRIPT" --purge > /dev/null +assert_no_file "$OC_DIR/plugins/ocmem.ts" "plugin removed (purge)" +assert_no_dir "$OC_DIR/memory" "memory dir removed (purge)" +assert_grep "$OC_DIR/AGENTS.md" "Pre content" "pre content survived purge" +assert_no_grep "$OC_DIR/AGENTS.md" "" "ocmem block stripped" + +# ── Test 7: Uninstall --dry-run does not modify ──────────────────────── +echo "" +echo "=== Test 7: Uninstall --dry-run ===" +bash "$INSTALL_SCRIPT" > /dev/null +run_and_grep "[dry-run]" "uninstall dry-run prints messages" bash "$UNINSTALL_SCRIPT" --dry-run +assert_file "$OC_DIR/plugins/ocmem.ts" "plugin still present after dry-run uninstall" + +# ── Test 8: Fresh AGENTS.md gets ocmem block appended ────────────────── +echo "" +echo "=== Test 8: Fresh AGENTS.md ===" +echo "# Clean AGENTS.md" > "$OC_DIR/AGENTS.md" +bash "$INSTALL_SCRIPT" > /dev/null +assert_grep "$OC_DIR/AGENTS.md" "" "AGENTS.md patched" +assert_grep "$OC_DIR/AGENTS.md" "# Clean AGENTS.md" "pre content preserved" + +# ── Result ───────────────────────────────────────────────────────────── +echo "" +echo "=========================================" +echo "Results: $PASS passed, $FAIL failed" +echo "=========================================" + +[ "$FAIL" -eq 0 ] || exit 1 diff --git a/test/ocmem.test.ts b/test/ocmem.test.ts index 7c1e1dd..02a09a8 100644 --- a/test/ocmem.test.ts +++ b/test/ocmem.test.ts @@ -2,13 +2,23 @@ import { describe, it, expect, afterEach } from "vitest" import { mkdirSync, writeFileSync, rmSync, existsSync, statSync, readFileSync } from "node:fs" import { join } from "node:path" import { tmpdir } from "node:os" -import { readTruncated, signature, MAX_PROJECT_LINES, MAX_INDEX_LINES } from "../plugin/ocmem.ts" +import { + readTruncated, + signature, + checkOrphanedFiles, + buildMemoryContext, + MAX_PROJECT_LINES, + MAX_INDEX_LINES, +} from "../plugin/ocmem.ts" const tmpRoot = join(tmpdir(), "ocmem-test-" + process.pid) -function tmpdirCreate(): string { +function tmpdirCreate(...subdirs: string[]): string { const dir = join(tmpRoot, String(Math.random()).slice(2)) mkdirSync(dir, { recursive: true }) + for (const s of subdirs) { + mkdirSync(join(dir, s), { recursive: true }) + } return dir } @@ -145,3 +155,169 @@ describe("signature", () => { expect(sig1).not.toBe(sig2) }) }) + +describe("readTruncated — MAX_CHARS truncation", () => { + it("truncates single overlong line at MAX_CHARS boundary", () => { + const dir = tmpdirCreate() + const f = join(dir, "huge.md") + const long = "x".repeat(9000) + writeFileSync(f, long) + const result = readTruncated(f, 10) + expect(result).not.toBeNull() + expect(result!.length).toBeLessThanOrEqual(8000 + 14) // "…(truncated)" added + expect(result).toContain("…(truncated)") + }) + + it("truncates many-lines content exceeding MAX_CHARS", () => { + const dir = tmpdirCreate() + const f = join(dir, "many.md") + // 100 lines of 100 chars each = 10000 chars + const lines = Array.from({ length: 100 }, (_, i) => `line ${String(i).padStart(4, "0")} — ${"x".repeat(80)}`) + writeFileSync(f, lines.join("\n")) + const result = readTruncated(f, 200) + expect(result).not.toBeNull() + expect(result!.length).toBeLessThanOrEqual(8000 + 14) + }) + + it("does not apply MAX_CHARS when content is under limit", () => { + const dir = tmpdirCreate() + const f = join(dir, "small.md") + writeFileSync(f, "hello world") + const result = readTruncated(f, 10) + expect(result).toBe("hello world") + }) +}) + +describe("checkOrphanedFiles", () => { + it("returns null when tools/ and domain/ are empty", () => { + const dir = tmpdirCreate("tools", "domain") + expect(checkOrphanedFiles(dir, "tools/foo.md")) // foo.md referenced but doesn't exist + .toBeNull() + }) + + it("returns null when all .md files are referenced in the index", () => { + const dir = tmpdirCreate("tools", "domain") + writeFileSync(join(dir, "tools", "git.md"), "# Git") + writeFileSync(join(dir, "domain", "db.md"), "# DB") + const index = "| tools/git.md | desc |\n| domain/db.md | desc |" + expect(checkOrphanedFiles(dir, index)).toBeNull() + }) + + it("detects orphaned .md files not in the index", () => { + const dir = tmpdirCreate("tools", "domain") + writeFileSync(join(dir, "tools", "git.md"), "# Git") + writeFileSync(join(dir, "tools", "docker.md"), "# Docker") + const index = "| tools/git.md | desc |" + const result = checkOrphanedFiles(dir, index) + expect(result).toContain("Orphaned Memory Files") + expect(result).toContain("tools/docker.md") + expect(result).not.toContain("tools/git.md") + }) + + it("returns null when tools/ and domain/ dirs do not exist", () => { + const dir = tmpdirCreate() + // No tools/ or domain/ created + expect(checkOrphanedFiles(dir, "")).toBeNull() + }) + + it("ignores non-.md files in tools/ and domain/", () => { + const dir = tmpdirCreate("tools") + writeFileSync(join(dir, "tools", "notes.txt"), "text") + writeFileSync(join(dir, "tools", ".gitkeep"), "") + expect(checkOrphanedFiles(dir, "")).toBeNull() + }) + + it("detects orphans in domain/ directory", () => { + const dir = tmpdirCreate("domain") + writeFileSync(join(dir, "domain", "kubernetes.md"), "# K8s") + const result = checkOrphanedFiles(dir, "") + expect(result).toContain("domain/kubernetes.md") + }) +}) + +describe("buildMemoryContext", () => { + it("returns empty string when neither project nor global memory exist", () => { + const dir = tmpdirCreate() + const result = buildMemoryContext({ + directory: "/fake/project", + projectMemory: join(dir, "missing.md"), + globalDir: dir, + globalIndex: join(dir, "missing.md"), + }) + expect(result).toBe("") + }) + + it("includes project memory when it exists", () => { + const dir = tmpdirCreate() + const pm = join(dir, "MEMORY.md") + writeFileSync(pm, "# My Project\n\n- 2026-07-07 — uses Go 1.22. — Don't suggest newer features.") + const result = buildMemoryContext({ + directory: "/fake/project", + projectMemory: pm, + globalDir: dir, + globalIndex: join(dir, "missing.md"), + }) + expect(result).toContain("Project Memory (/fake/project)") + expect(result).toContain("uses Go 1.22") + }) + + it("includes global index when it exists", () => { + const dir = tmpdirCreate() + const gi = join(dir, "memory.md") + writeFileSync(gi, "| tools/git.md | Git quirks | 2026-07-06 |") + const result = buildMemoryContext({ + directory: "/fake/project", + projectMemory: join(dir, "missing.md"), + globalDir: dir, + globalIndex: gi, + }) + expect(result).toContain("Global Memory Index") + expect(result).toContain("tools/git.md") + }) + + it("includes both project and global when both exist", () => { + const dir = tmpdirCreate() + const pm = join(dir, "MEMORY.md") + const gi = join(dir, "memory.md") + writeFileSync(pm, "# Project X") + writeFileSync(gi, "| tools/foo.md | desc |") + const result = buildMemoryContext({ + directory: "/x", + projectMemory: pm, + globalDir: dir, + globalIndex: gi, + }) + expect(result).toContain("Project Memory (/x)") + expect(result).toContain("Global Memory Index") + expect(result).toContain("\n\n") // sections separated + }) + + it("includes orphaned file warning when orphans exist", () => { + const dir = tmpdirCreate("tools") + writeFileSync(join(dir, "tools", "docker.md"), "# Docker") + const gi = join(dir, "memory.md") + writeFileSync(gi, "| tools/git.md | desc |") + const result = buildMemoryContext({ + directory: "/x", + projectMemory: join(dir, "nope.md"), + globalDir: dir, + globalIndex: gi, + }) + expect(result).toContain("Orphaned Memory Files") + expect(result).toContain("tools/docker.md") + }) + + it("does not include orphaned warning when no orphans", () => { + const dir = tmpdirCreate("tools") + writeFileSync(join(dir, "tools", "git.md"), "# Git") + const gi = join(dir, "memory.md") + writeFileSync(gi, "| tools/git.md | desc |") + const result = buildMemoryContext({ + directory: "/x", + projectMemory: join(dir, "nope.md"), + globalDir: dir, + globalIndex: gi, + }) + expect(result).not.toContain("Orphaned") + }) +}) From 3836c8757b1ef7c6bd83a32024ba2e2dfbffa900 Mon Sep 17 00:00:00 2001 From: Ernestas Lel Date: Tue, 7 Jul 2026 00:48:48 +0000 Subject: [PATCH 4/4] fix: correct CI branch to master, fix CONTRIBUTING clone URL, use npm ci --- .github/workflows/ci.yml | 6 +++--- CONTRIBUTING.md | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index db05b43..d877ff9 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -2,9 +2,9 @@ name: CI on: push: - branches: [main] + branches: [master] pull_request: - branches: [main] + branches: [master] jobs: test: @@ -14,7 +14,7 @@ jobs: - uses: actions/setup-node@v4 with: node-version: "20" - - run: npm install + - run: npm ci - run: npm run typecheck - run: npm test - run: bash test/install.test.sh diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 55ac013..7a36b70 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -6,7 +6,7 @@ Thanks for wanting to help. ```bash # Clone and install -git clone https://github.com/anomalyco/opencode +git clone https://github.com/ernlel/ocmem.git cd ocmem npm install ``` @@ -56,11 +56,11 @@ Run `npm test` before pushing. ## Submitting changes -1. Fork and branch from `main`. +1. Fork and branch from `master`. 2. Keep commits focused — one logical change per commit. 3. Write a clear commit message (present tense, imperative mood). 4. Run `npm run typecheck && npm test`. -5. Open a PR against `main` with a description of what changed and why. +5. Open a PR against `master` with a description of what changed and why. ## What to work on