From 45745e851d908410c92a90275014d49f27c6dbb8 Mon Sep 17 00:00:00 2001 From: Eduardo Borjas Date: Tue, 2 Jun 2026 12:16:14 -0600 Subject: [PATCH 1/6] docs: apply GitHub repo growth standard polish MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Fix CI badge URL: eduardoborjas → aedneth (was pointing at wrong username) - Add npm version badge to README header - Add "Why magnus?" section: positions vs raw yt-dlp, browser extensions, and manual Apify runs with outcome-first reasoning - Add Roadmap section (v0.1.0 checked, v0.2.0–v1.0.0 milestones) - Promote CHANGELOG [Unreleased] → [0.1.0] 2026-06-02 with Fixed entries for the two CI bugs resolved on the branch - Fix wrong GitHub username in CONTRIBUTING.md clone URL and .github/ISSUE_TEMPLATE/config.yml security policy link Co-Authored-By: Claude Sonnet 4.6 --- .github/ISSUE_TEMPLATE/config.yml | 2 +- CHANGELOG.md | 12 ++++++++++-- CONTRIBUTING.md | 2 +- README.md | 20 +++++++++++++++++++- 4 files changed, 31 insertions(+), 5 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml index 2c9cd68..e3707d2 100644 --- a/.github/ISSUE_TEMPLATE/config.yml +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -1,7 +1,7 @@ blank_issues_enabled: false contact_links: - name: Security vulnerability - url: https://github.com/eduardoborjas/magnus-cli/security/policy + url: https://github.com/aedneth/magnus-cli/security/policy about: Please report security issues privately per SECURITY.md, not as public issues. - name: Commercial license inquiry url: mailto:eduardoa.borjas@gmail.com diff --git a/CHANGELOG.md b/CHANGELOG.md index 1e3dcd2..9fb0bd9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,9 +6,11 @@ All notable changes to this project are documented here. The format is based on ## [Unreleased] +## [0.1.0] — 2026-06-02 + ### Added -- Initial project scaffold: TypeScript + ESM, `tsup` bundler, Vitest, ESLint/Prettier, CI. +- Initial project scaffold: TypeScript + ESM, `tsup` bundler, Vitest, ESLint/Prettier, CI matrix (ubuntu/macos/windows × node 20/22). - Download core: ordered platform detection (`music.youtube.com` before `youtube.com`), audio/video flag builders, configurable output-folder map, `yt-dlp` subprocess wrapper. - Commands: `download` (default), `audio`, `batch`, `capture`, `stats`, `config`, @@ -20,4 +22,10 @@ All notable changes to this project are documented here. The format is based on never-store-expiring-MP4 rule. - Dual licensing: AGPL-3.0 + commercial. -[Unreleased]: https://github.com/eduardoborjas/magnus-cli/commits/main +### Fixed + +- CI workflow: moved `npm run build` before `npm test` so `dist/cli.js` exists when agent-mode integration tests run. +- CLI stdout flushing: replaced `process.exit()` with `process.exitCode` assignment to prevent pipe-buffer truncation on macOS + Node 20 for large JSON outputs (e.g. `magnus manifest --json`). + +[Unreleased]: https://github.com/aedneth/magnus-cli/compare/v0.1.0...HEAD +[0.1.0]: https://github.com/aedneth/magnus-cli/releases/tag/v0.1.0 diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 39a4776..164872b 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -5,7 +5,7 @@ Thanks for your interest in improving Magnus! This guide covers the basics. ## Development setup ```bash -git clone https://github.com/eduardoborjas/magnus-cli.git +git clone https://github.com/aedneth/magnus-cli.git cd magnus-cli npm install npm run build diff --git a/README.md b/README.md index de63d12..7cc85d4 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,8 @@ **Universal `yt-dlp` media downloader + Apify-driven social-capture pipeline — built for humans *and* terminal AI agents.** -[![CI](https://github.com/eduardoborjas/magnus-cli/actions/workflows/ci.yml/badge.svg)](https://github.com/eduardoborjas/magnus-cli/actions/workflows/ci.yml) +[![CI](https://github.com/aedneth/magnus-cli/actions/workflows/ci.yml/badge.svg)](https://github.com/aedneth/magnus-cli/actions/workflows/ci.yml) +[![npm](https://img.shields.io/npm/v/magnus.svg)](https://www.npmjs.com/package/magnus) [![License: AGPL-3.0](https://img.shields.io/badge/license-AGPL--3.0%20%2B%20Commercial-blue.svg)](#license) [![Node](https://img.shields.io/badge/node-%3E%3D20-brightgreen.svg)](https://nodejs.org) @@ -67,6 +68,14 @@ magnus config list # show resolved config > **Tip:** always quote URLs containing `&`, or your shell will background the command. +## Why magnus? + +**vs raw `yt-dlp`:** Every platform needs different flags, different output paths, and different post-processing. Magnus encodes those rules once — `music.youtube.com` always goes to `~/Music/YT Music/`, YouTube video always goes to `~/Videos/Social Media/YouTube/`, TikTok always strips the watermark — so you type one command and get the right file, in the right place, every time. It also wraps every operation in a clean JSON envelope, making `yt-dlp`-backed workflows scriptable and CI-safe. + +**vs browser extensions:** Browser extensions are one-at-a-time, TTY-dependent, and invisible to scripts and agents. Magnus runs headless, accepts URLs from stdin, and pipelines into `jq`, shell scripts, Claude Code, and CI jobs. `cat urls.txt | magnus batch - --json` is a valid production command. + +**vs manual Apify runs:** Apify's web UI has no spend controls, no resume capability, and returns raw JSON with no rendering. Magnus adds a `$40-abort / $50-hard-cap` canary gate, per-platform SQLite resume queues (restart any run from where it stopped), and renders results directly into CKIS/Obsidian Markdown — one command from saved posts to vault notes. + ## Configuration Config lives under XDG paths (`~/.config/magnus/` on Linux): @@ -80,6 +89,15 @@ magnus config path # print the resolved config directory magnus config set capture.budgetHardCapUsd 50 ``` +## Roadmap + +- [x] **v0.1.0** — download core (ordered platform detection, audio/video flag sets, output-folder map), Apify capture pipeline (SQLite queues, canary, spend caps, CKIS Markdown), full agent-native layer (`--json`, `--yes`, manifest, MCP server), dual AGPL-3.0 + commercial license +- [ ] **v0.2.0** — interactive Ink v5 TUI: live progress bars, download spinners, capture cost dashboard +- [ ] **v0.3.0** — `magnus doctor` full auto-fix mode, `npm publish` pipeline, first public npm release +- [ ] **v0.4.0** — playlist support, concurrent download pool, per-item retry with back-off +- [ ] **v0.5.0** — capture queue management TUI, per-actor Apify cost breakdown, queue drain/pause commands +- [ ] **v1.0.0** — stable public API, full docs site, public launch + ## License Magnus is **dual-licensed**: From db3f65a96b7fd1f023b91afba69ef5af53228415 Mon Sep 17 00:00:00 2001 From: Eduardo Borjas Date: Tue, 2 Jun 2026 12:50:35 -0600 Subject: [PATCH 2/6] fix: apply Opus architecture audit findings (security, observability, cost) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit C1 — queue.ts: narrow table params to QueueTable union type; use bound LIMIT parameter instead of string-interpolated Number(limit) to close the SQL concatenation pattern in an agent-callable tool. M1 — capture runners: coerce --budget-cap/--limit from string (Commander returns option values as strings; runners were casting string as number, making assertCanaryAcceptable compare number > string via JS coercion). M2 — capture runners: wire --limit across all three runners (instagram, tiktok, youtube); the flag existed in the manifest but was never read. M3 — capture runners: pass projected next-batch cost to assertWithinBudget before each runActor call so the hard cap fires pre-charge, not post- charge. Track per-URL canary rate and multiply by batch size. M4 — youtube.ts: rethrow non-ENOENT yt-dlp startup errors as MagnusError(Network) instead of silently continuing into a render loop that finds no info.json and exits 0 with 0 notes processed. M5 — db.ts: key openDb cache by resolved file path (was module-global singleton, so --config dir isolation did not affect the DB handle). m1 — redact.ts: add Bearer token and generic key=VALUE patterns alongside the existing Apify token regex. m2 — apify.ts: warn to stderr when Apify cost fields are absent (schema drift) instead of silently recording $0; budget guard would not fire. m3 — instagram.ts: add shortCode-based fallback lookup for dataset items (URL normalization mismatch on query-string variants like ?igshid=). m4 — config.ts: remove duplicate ctx.log.info in config list that wrote the config JSON to both stderr and stdout. m5 — cli.ts: guard injectDefaultCommand against argv.length < 2; replace non-null assertions with typed casts. s1 — remove dead ink, react, @types/react dependencies (zero imports). s5 — cli.ts: use process.exitCode in main().catch for consistency with the execute() drain pattern; avoids premature stream teardown. Co-Authored-By: Claude Sonnet 4.6 --- package-lock.json | 645 ----------------------------------- package.json | 5 +- src/capture/apify.ts | 11 +- src/capture/db.ts | 9 +- src/capture/queue.ts | 22 +- src/capture/run/instagram.ts | 31 +- src/capture/run/tiktok.ts | 19 +- src/capture/run/youtube.ts | 8 +- src/cli.ts | 5 +- src/commands/config.ts | 1 - src/util/redact.ts | 7 +- 11 files changed, 86 insertions(+), 677 deletions(-) diff --git a/package-lock.json b/package-lock.json index adff97a..220a7b9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -15,9 +15,7 @@ "commander": "^13.0.0", "env-paths": "^3.0.0", "execa": "^9.4.0", - "ink": "^5.0.1", "picocolors": "^1.1.0", - "react": "^18.3.1", "yaml": "^2.5.1", "zod": "^3.23.8" }, @@ -29,7 +27,6 @@ "@eslint/js": "^9.10.0", "@types/better-sqlite3": "^7.6.11", "@types/node": "^20.16.0", - "@types/react": "^18.3.5", "eslint": "^9.10.0", "prettier": "^3.3.3", "tsup": "^8.2.4", @@ -41,31 +38,6 @@ "node": ">=20" } }, - "node_modules/@alcalzone/ansi-tokenize": { - "version": "0.1.3", - "resolved": "https://registry.npmjs.org/@alcalzone/ansi-tokenize/-/ansi-tokenize-0.1.3.tgz", - "integrity": "sha512-3yWxPTq3UQ/FY9p1ErPxIyfT64elWaMvM9lIHnaqpyft63tkxodF5aUElYHrdisWve5cETkh1+KBw1yJuW0aRw==", - "license": "MIT", - "dependencies": { - "ansi-styles": "^6.2.1", - "is-fullwidth-code-point": "^4.0.0" - }, - "engines": { - "node": ">=14.13.1" - } - }, - "node_modules/@alcalzone/ansi-tokenize/node_modules/ansi-styles": { - "version": "6.2.3", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", - "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, "node_modules/@apify/consts": { "version": "2.53.2", "resolved": "https://registry.npmjs.org/@apify/consts/-/consts-2.53.2.tgz", @@ -1707,24 +1679,6 @@ "undici-types": "~6.21.0" } }, - "node_modules/@types/prop-types": { - "version": "15.7.15", - "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.15.tgz", - "integrity": "sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==", - "devOptional": true, - "license": "MIT" - }, - "node_modules/@types/react": { - "version": "18.3.29", - "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.29.tgz", - "integrity": "sha512-ch0qJdr2JY0r04NXSprbK6TXOgnaJ1Tz23fm5W+z0/CBah6BSBc3n96h7K9GOtwh0HrilNWHIBzE1Ko4Dcw/Wg==", - "devOptional": true, - "license": "MIT", - "dependencies": { - "@types/prop-types": "*", - "csstype": "^3.2.2" - } - }, "node_modules/@typescript-eslint/eslint-plugin": { "version": "8.60.0", "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.60.0.tgz", @@ -2224,21 +2178,6 @@ "node": ">=6" } }, - "node_modules/ansi-escapes": { - "version": "7.3.0", - "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-7.3.0.tgz", - "integrity": "sha512-BvU8nYgGQBxcmMuEeUEmNTvrMVjJNSH7RgW24vXexN4Ven6qCvy4TntnvlnwnMLTVlcRQQdbRY8NKnaIoeWDNg==", - "license": "MIT", - "dependencies": { - "environment": "^1.0.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/ansi-regex": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", @@ -2346,18 +2285,6 @@ "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", "license": "MIT" }, - "node_modules/auto-bind": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/auto-bind/-/auto-bind-5.0.1.tgz", - "integrity": "sha512-ooviqdwwgfIfNmDwo94wlshcdzfO64XV0Cg6oDsDYBJfITDz1EngD2z7DkbvCWn+XIMsIqW27sEVF6qcpJrRcg==", - "license": "MIT", - "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/axios": { "version": "1.16.1", "resolved": "https://registry.npmjs.org/axios/-/axios-1.16.1.tgz", @@ -2668,89 +2595,6 @@ "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==", "license": "ISC" }, - "node_modules/cli-boxes": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/cli-boxes/-/cli-boxes-3.0.0.tgz", - "integrity": "sha512-/lzGpEWL/8PfI0BmBOPRwp0c/wFNX1RdUML3jK/RcSBA9T8mZDdQpqYBKtCFTOfQbwPqWEOpjqW+Fnayc0969g==", - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/cli-cursor": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-4.0.0.tgz", - "integrity": "sha512-VGtlMu3x/4DOtIUwEkRezxUZ2lBacNJCHash0N0WeZDBS+7Ux1dm3XWAgWYxLJFMMdOeXMHXorshEFhbMSGelg==", - "license": "MIT", - "dependencies": { - "restore-cursor": "^4.0.0" - }, - "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/cli-truncate": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/cli-truncate/-/cli-truncate-4.0.0.tgz", - "integrity": "sha512-nPdaFdQ0h/GEigbPClz11D0v/ZJEwxmeVZGeMo3Z5StPtUTkA9o1lD6QwoirYiSDzbcwn2XcjwmCp68W1IS4TA==", - "license": "MIT", - "dependencies": { - "slice-ansi": "^5.0.0", - "string-width": "^7.0.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/cli-truncate/node_modules/ansi-styles": { - "version": "6.2.3", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", - "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/cli-truncate/node_modules/slice-ansi": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-5.0.0.tgz", - "integrity": "sha512-FC+lgizVPfie0kkhqUScwRu1O/lF6NOgJmlCgK+/LYxDCTk8sGelYaHDhFcDN+Sn3Cv+3VSa4Byeo+IMCzpMgQ==", - "license": "MIT", - "dependencies": { - "ansi-styles": "^6.0.0", - "is-fullwidth-code-point": "^4.0.0" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/slice-ansi?sponsor=1" - } - }, - "node_modules/code-excerpt": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/code-excerpt/-/code-excerpt-4.0.0.tgz", - "integrity": "sha512-xxodCmBen3iy2i0WtAK8FlFNrRzjUqjRsMfho58xT/wvZU1YTM3fCnRjcy1gJPMepaRlgm/0e6w8SpWHpn3/cA==", - "license": "MIT", - "dependencies": { - "convert-to-spaces": "^2.0.1" - }, - "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" - } - }, "node_modules/color-convert": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", @@ -2838,15 +2682,6 @@ "node": ">= 0.6" } }, - "node_modules/convert-to-spaces": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/convert-to-spaces/-/convert-to-spaces-2.0.1.tgz", - "integrity": "sha512-rcQ1bsQO9799wq24uE5AM2tAILy4gXGIK/njFWcVQkGNZ96edlpY+A7bjwvzjYvLDyzmG1MmMLZhpcsb+klNMQ==", - "license": "MIT", - "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" - } - }, "node_modules/cookie": { "version": "0.7.2", "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", @@ -2896,13 +2731,6 @@ "node": ">= 8" } }, - "node_modules/csstype": { - "version": "3.2.3", - "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", - "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", - "devOptional": true, - "license": "MIT" - }, "node_modules/data-uri-to-buffer": { "version": "6.0.2", "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-6.0.2.tgz", @@ -3069,12 +2897,6 @@ "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", "license": "MIT" }, - "node_modules/emoji-regex": { - "version": "10.6.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.6.0.tgz", - "integrity": "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==", - "license": "MIT" - }, "node_modules/encodeurl": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", @@ -3119,18 +2941,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/environment": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/environment/-/environment-1.1.0.tgz", - "integrity": "sha512-xUtoPkMggbz0MPyPiIWr1Kp4aeWJjDZ6SMvURhimjdZgsRuDplF5/s9hcgGhyXMhs+6vpnuoiZ2kFiu3FMnS8Q==", - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/es-define-property": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", @@ -3183,16 +2993,6 @@ "node": ">= 0.4" } }, - "node_modules/es-toolkit": { - "version": "1.47.0", - "resolved": "https://registry.npmjs.org/es-toolkit/-/es-toolkit-1.47.0.tgz", - "integrity": "sha512-n1GuoD0WEQZMBk5tttoZSqwgyLx01oqa5XsBmCHwPyNe1S9jPBEmtR2pSgp2kJuWE3ciFZ6yRHmY4pM4C3OOkw==", - "license": "MIT", - "workspaces": [ - "docs", - "benchmarks" - ] - }, "node_modules/esbuild": { "version": "0.27.7", "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.7.tgz", @@ -3993,18 +3793,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/get-east-asian-width": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.6.0.tgz", - "integrity": "sha512-QRbvDIbx6YklUe6RxeTeleMR0yv3cYH6PsPZHcnVn7xv7zO1BHN8r0XETu8n6Ye3Q+ahtSarc3WgtNWmehIBfA==", - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/get-intrinsic": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", @@ -4359,18 +4147,6 @@ "node": ">=0.8.19" } }, - "node_modules/indent-string": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-5.0.0.tgz", - "integrity": "sha512-m6FAo/spmsW2Ab2fU35JTYwtOKa2yAwXSwgjSv1TJzh4Mh7mC3lzAOVLBprb72XsTrgkEIsl7YrFNAiDiRhIGg==", - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/inherits": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", @@ -4383,84 +4159,6 @@ "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", "license": "ISC" }, - "node_modules/ink": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/ink/-/ink-5.2.1.tgz", - "integrity": "sha512-BqcUyWrG9zq5HIwW6JcfFHsIYebJkWWb4fczNah1goUO0vv5vneIlfwuS85twyJ5hYR/y18FlAYUxrO9ChIWVg==", - "license": "MIT", - "dependencies": { - "@alcalzone/ansi-tokenize": "^0.1.3", - "ansi-escapes": "^7.0.0", - "ansi-styles": "^6.2.1", - "auto-bind": "^5.0.1", - "chalk": "^5.3.0", - "cli-boxes": "^3.0.0", - "cli-cursor": "^4.0.0", - "cli-truncate": "^4.0.0", - "code-excerpt": "^4.0.0", - "es-toolkit": "^1.22.0", - "indent-string": "^5.0.0", - "is-in-ci": "^1.0.0", - "patch-console": "^2.0.0", - "react-reconciler": "^0.29.0", - "scheduler": "^0.23.0", - "signal-exit": "^3.0.7", - "slice-ansi": "^7.1.0", - "stack-utils": "^2.0.6", - "string-width": "^7.2.0", - "type-fest": "^4.27.0", - "widest-line": "^5.0.0", - "wrap-ansi": "^9.0.0", - "ws": "^8.18.0", - "yoga-layout": "~3.2.1" - }, - "engines": { - "node": ">=18" - }, - "peerDependencies": { - "@types/react": ">=18.0.0", - "react": ">=18.0.0", - "react-devtools-core": "^4.19.1" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "react-devtools-core": { - "optional": true - } - } - }, - "node_modules/ink/node_modules/ansi-styles": { - "version": "6.2.3", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", - "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/ink/node_modules/chalk": { - "version": "5.6.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.2.tgz", - "integrity": "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==", - "license": "MIT", - "engines": { - "node": "^12.17.0 || ^14.13 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/ink/node_modules/signal-exit": { - "version": "3.0.7", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", - "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", - "license": "ISC" - }, "node_modules/ip-address": { "version": "10.2.0", "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.2.0.tgz", @@ -4489,18 +4187,6 @@ "node": ">=0.10.0" } }, - "node_modules/is-fullwidth-code-point": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-4.0.0.tgz", - "integrity": "sha512-O4L094N2/dZ7xqVdrXhh9r1KODPJpFms8B5sGdJLPy664AgvXsreZUyCQQNItZRDlYug4xStLjNp/sz3HvBowQ==", - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/is-glob": { "version": "4.0.3", "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", @@ -4514,21 +4200,6 @@ "node": ">=0.10.0" } }, - "node_modules/is-in-ci": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-in-ci/-/is-in-ci-1.0.0.tgz", - "integrity": "sha512-eUuAjybVTHMYWm/U+vBO1sY/JOCgoPCXRxzdju0K+K0BiGW0SChEL1MLC0PoCIR1OlPo5YAp8HuQoUlsWEICwg==", - "license": "MIT", - "bin": { - "is-in-ci": "cli.js" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/is-number": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", @@ -4638,12 +4309,6 @@ "node": ">=10" } }, - "node_modules/js-tokens": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", - "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", - "license": "MIT" - }, "node_modules/js-yaml": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", @@ -4781,18 +4446,6 @@ "dev": true, "license": "MIT" }, - "node_modules/loose-envify": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", - "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", - "license": "MIT", - "dependencies": { - "js-tokens": "^3.0.0 || ^4.0.0" - }, - "bin": { - "loose-envify": "cli.js" - } - }, "node_modules/loupe": { "version": "3.2.1", "resolved": "https://registry.npmjs.org/loupe/-/loupe-3.2.1.tgz", @@ -4898,15 +4551,6 @@ "url": "https://opencollective.com/express" } }, - "node_modules/mimic-fn": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", - "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", - "license": "MIT", - "engines": { - "node": ">=6" - } - }, "node_modules/mimic-response": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", @@ -5120,21 +4764,6 @@ "wrappy": "1" } }, - "node_modules/onetime": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", - "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", - "license": "MIT", - "dependencies": { - "mimic-fn": "^2.1.0" - }, - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/optionator": { "version": "0.9.4", "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", @@ -5339,15 +4968,6 @@ "node": ">= 0.8" } }, - "node_modules/patch-console": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/patch-console/-/patch-console-2.0.0.tgz", - "integrity": "sha512-0YNdUceMdaQwoKce1gatDScmMo5pu/tfABfnzEqeG0gtTmd7mh/WcwgUjtAeOU7N8nFFlbQBnFK2gXW5fGvmMA==", - "license": "MIT", - "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" - } - }, "node_modules/path-exists": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", @@ -5794,34 +5414,6 @@ "node": ">=0.10.0" } }, - "node_modules/react": { - "version": "18.3.1", - "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", - "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", - "license": "MIT", - "dependencies": { - "loose-envify": "^1.1.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/react-reconciler": { - "version": "0.29.2", - "resolved": "https://registry.npmjs.org/react-reconciler/-/react-reconciler-0.29.2.tgz", - "integrity": "sha512-zZQqIiYgDCTP/f1N/mAR10nJGrPD2ZR+jDSEsKWJHYC7Cm2wodlwbR3upZRdC3cjIjSlTLNVyO7Iu0Yy7t2AYg==", - "license": "MIT", - "dependencies": { - "loose-envify": "^1.1.0", - "scheduler": "^0.23.2" - }, - "engines": { - "node": ">=0.10.0" - }, - "peerDependencies": { - "react": "^18.3.1" - } - }, "node_modules/read-yaml-file": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/read-yaml-file/-/read-yaml-file-1.1.0.tgz", @@ -5909,28 +5501,6 @@ "node": ">=8" } }, - "node_modules/restore-cursor": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-4.0.0.tgz", - "integrity": "sha512-I9fPXU9geO9bHOt9pHHOhOkYerIMsmVaWB0rA2AI9ERh/+x/i7MV5HKBNrg+ljO5eoPVgCcnFuRjJ9uH6I/3eg==", - "license": "MIT", - "dependencies": { - "onetime": "^5.1.0", - "signal-exit": "^3.0.2" - }, - "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/restore-cursor/node_modules/signal-exit": { - "version": "3.0.7", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", - "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", - "license": "ISC" - }, "node_modules/retry": { "version": "0.13.1", "resolved": "https://registry.npmjs.org/retry/-/retry-0.13.1.tgz", @@ -6069,15 +5639,6 @@ "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", "license": "MIT" }, - "node_modules/scheduler": { - "version": "0.23.2", - "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz", - "integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==", - "license": "MIT", - "dependencies": { - "loose-envify": "^1.1.0" - } - }, "node_modules/semver": { "version": "7.8.1", "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.1.tgz", @@ -6308,49 +5869,6 @@ "node": ">=8" } }, - "node_modules/slice-ansi": { - "version": "7.1.2", - "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-7.1.2.tgz", - "integrity": "sha512-iOBWFgUX7caIZiuutICxVgX1SdxwAVFFKwt1EvMYYec/NWO5meOJ6K5uQxhrYBdQJne4KxiqZc+KptFOWFSI9w==", - "license": "MIT", - "dependencies": { - "ansi-styles": "^6.2.1", - "is-fullwidth-code-point": "^5.0.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/chalk/slice-ansi?sponsor=1" - } - }, - "node_modules/slice-ansi/node_modules/ansi-styles": { - "version": "6.2.3", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", - "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/slice-ansi/node_modules/is-fullwidth-code-point": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-5.1.0.tgz", - "integrity": "sha512-5XHYaSyiqADb4RnZ1Bdad6cPp8Toise4TzEjcOYDHZkTCbKgiUl7WTUCpNWHuxmDt91wnsZBc9xinNzopv3JMQ==", - "license": "MIT", - "dependencies": { - "get-east-asian-width": "^1.3.1" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/smart-buffer": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz", @@ -6436,27 +5954,6 @@ "dev": true, "license": "BSD-3-Clause" }, - "node_modules/stack-utils": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/stack-utils/-/stack-utils-2.0.6.tgz", - "integrity": "sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ==", - "license": "MIT", - "dependencies": { - "escape-string-regexp": "^2.0.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/stack-utils/node_modules/escape-string-regexp": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz", - "integrity": "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==", - "license": "MIT", - "engines": { - "node": ">=8" - } - }, "node_modules/stackback": { "version": "0.0.2", "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", @@ -6489,50 +5986,6 @@ "safe-buffer": "~5.2.0" } }, - "node_modules/string-width": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", - "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", - "license": "MIT", - "dependencies": { - "emoji-regex": "^10.3.0", - "get-east-asian-width": "^1.0.0", - "strip-ansi": "^7.1.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/string-width/node_modules/ansi-regex": { - "version": "6.2.2", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", - "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-regex?sponsor=1" - } - }, - "node_modules/string-width/node_modules/strip-ansi": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz", - "integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==", - "license": "MIT", - "dependencies": { - "ansi-regex": "^6.2.2" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/strip-ansi?sponsor=1" - } - }, "node_modules/strip-ansi": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", @@ -7714,21 +7167,6 @@ "node": ">=8" } }, - "node_modules/widest-line": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/widest-line/-/widest-line-5.0.0.tgz", - "integrity": "sha512-c9bZp7b5YtRj2wOe6dlj32MK+Bx/M/d+9VB2SHM1OtsUHR0aV0tdP6DWh/iMt0kWi1t5g1Iudu6hQRNd1A4PVA==", - "license": "MIT", - "dependencies": { - "string-width": "^7.0.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/word-wrap": { "version": "1.2.5", "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", @@ -7739,89 +7177,12 @@ "node": ">=0.10.0" } }, - "node_modules/wrap-ansi": { - "version": "9.0.2", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-9.0.2.tgz", - "integrity": "sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww==", - "license": "MIT", - "dependencies": { - "ansi-styles": "^6.2.1", - "string-width": "^7.0.0", - "strip-ansi": "^7.1.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" - } - }, - "node_modules/wrap-ansi/node_modules/ansi-regex": { - "version": "6.2.2", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", - "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-regex?sponsor=1" - } - }, - "node_modules/wrap-ansi/node_modules/ansi-styles": { - "version": "6.2.3", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", - "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/wrap-ansi/node_modules/strip-ansi": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz", - "integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==", - "license": "MIT", - "dependencies": { - "ansi-regex": "^6.2.2" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/strip-ansi?sponsor=1" - } - }, "node_modules/wrappy": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", "license": "ISC" }, - "node_modules/ws": { - "version": "8.21.0", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.21.0.tgz", - "integrity": "sha512-Vsp28b7DRcimFQvrqu2Wek3z1iYxDCWqHYB8Qsnk/S4RfaCQzPGPyBNuVjJV3cd6UiKtUtp6sNM77gWvzcCH+g==", - "license": "MIT", - "engines": { - "node": ">=10.0.0" - }, - "peerDependencies": { - "bufferutil": "^4.0.1", - "utf-8-validate": ">=5.0.2" - }, - "peerDependenciesMeta": { - "bufferutil": { - "optional": true - }, - "utf-8-validate": { - "optional": true - } - } - }, "node_modules/yaml": { "version": "2.9.0", "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.9.0.tgz", @@ -7862,12 +7223,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/yoga-layout": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/yoga-layout/-/yoga-layout-3.2.1.tgz", - "integrity": "sha512-0LPOt3AxKqMdFBZA3HBAt/t/8vIKq7VaQYbuA8WxCgung+p9TVyKRYdpvCb80HcdTN2NkbIKbhNwKUfm3tQywQ==", - "license": "MIT" - }, "node_modules/zod": { "version": "3.25.76", "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", diff --git a/package.json b/package.json index 37e0496..39e51f8 100644 --- a/package.json +++ b/package.json @@ -63,9 +63,7 @@ "commander": "^13.0.0", "env-paths": "^3.0.0", "execa": "^9.4.0", - "ink": "^5.0.1", "picocolors": "^1.1.0", - "react": "^18.3.1", "yaml": "^2.5.1", "zod": "^3.23.8" }, @@ -74,12 +72,11 @@ "@eslint/js": "^9.10.0", "@types/better-sqlite3": "^7.6.11", "@types/node": "^20.16.0", - "@types/react": "^18.3.5", "eslint": "^9.10.0", - "typescript-eslint": "^8.5.0", "prettier": "^3.3.3", "tsup": "^8.2.4", "typescript": "^5.5.4", + "typescript-eslint": "^8.5.0", "vitest": "^2.1.1" } } diff --git a/src/capture/apify.ts b/src/capture/apify.ts index 2c3f7f3..0ab9400 100644 --- a/src/capture/apify.ts +++ b/src/capture/apify.ts @@ -54,12 +54,19 @@ export async function runActor( } const { items } = await client.dataset(run.defaultDatasetId).listItems(); const stats = (run as unknown as { usageTotalUsd?: number; stats?: { costInUsd?: number } }); - const costUsd = stats.usageTotalUsd ?? stats.stats?.costInUsd ?? 0; + const costUsd = stats.usageTotalUsd ?? stats.stats?.costInUsd; + if (costUsd === undefined) { + // Apify renamed or removed the cost fields. Recording $0 means the budget + // guard will not fire — investigate and update the field names above. + process.stderr.write( + `[magnus] warn: Apify cost fields absent on run ${run.id}; recording $0. Budget guard may not trip.\n`, + ); + } return { status: run.status, datasetId: run.defaultDatasetId, runId: run.id, - costUsd, + costUsd: costUsd ?? 0, items: items as Array>, }; } diff --git a/src/capture/db.ts b/src/capture/db.ts index 96d6102..9f52070 100644 --- a/src/capture/db.ts +++ b/src/capture/db.ts @@ -5,16 +5,17 @@ import { dbFile } from '../config/paths.js'; export type DB = Database.Database; -let cached: DB | null = null; +const dbCache = new Map(); -/** Open (and memoize) the magnus SQLite database, creating all tables. */ +/** Open (and memoize by path) the magnus SQLite database, creating all tables. */ export function openDb(file = dbFile()): DB { - if (cached) return cached; + const existing = dbCache.get(file); + if (existing) return existing; if (file !== ':memory:') mkdirSync(dirname(file), { recursive: true }); const db = new Database(file); db.pragma('journal_mode = WAL'); migrate(db); - cached = db; + dbCache.set(file, db); return db; } diff --git a/src/capture/queue.ts b/src/capture/queue.ts index d677167..0dd2e82 100644 --- a/src/capture/queue.ts +++ b/src/capture/queue.ts @@ -2,6 +2,7 @@ import type { DB } from './db.js'; import type { IgUrl } from './sources.js'; export type QueueStatus = 'pending' | 'running' | 'done' | 'failed'; +export type QueueTable = 'ig_queue' | 'tt_queue' | 'yt_queue'; /** Per-platform resume queue. Survives mid-run aborts; pending rows resume. */ @@ -27,22 +28,29 @@ export function enqueueSimple(db: DB, table: 'tt_queue' | 'yt_queue', urls: stri return tx(urls); } -export function pendingCount(db: DB, table: string): number { +export function pendingCount(db: DB, table: QueueTable): number { const row = db.prepare(`SELECT COUNT(*) AS c FROM ${table} WHERE status='pending'`).get() as { c: number; }; return row.c; } -export function pendingUrls(db: DB, table: string, limit?: number): string[] { - const sql = - `SELECT url FROM ${table} WHERE status='pending'` + (limit ? ` LIMIT ${Number(limit)}` : ''); - return (db.prepare(sql).all() as Array<{ url: string }>).map((r) => r.url); +export function pendingUrls(db: DB, table: QueueTable, limit?: number): string[] { + if (limit !== undefined) { + return ( + db + .prepare(`SELECT url FROM ${table} WHERE status='pending' LIMIT ?`) + .all(limit) as Array<{ url: string }> + ).map((r) => r.url); + } + return ( + db.prepare(`SELECT url FROM ${table} WHERE status='pending'`).all() as Array<{ url: string }> + ).map((r) => r.url); } export function markStatus( db: DB, - table: string, + table: QueueTable, url: string, status: QueueStatus, error?: string, @@ -57,6 +65,6 @@ export function markStatus( * Recovery: any row left in 'running' from a killed process is reset to * 'pending' so the next run picks it back up. Call at queue startup. */ -export function recoverRunning(db: DB, table: string): number { +export function recoverRunning(db: DB, table: QueueTable): number { return db.prepare(`UPDATE ${table} SET status='pending' WHERE status='running'`).run().changes; } diff --git a/src/capture/run/instagram.ts b/src/capture/run/instagram.ts index 4f946d6..f589f4d 100644 --- a/src/capture/run/instagram.ts +++ b/src/capture/run/instagram.ts @@ -68,10 +68,18 @@ async function processBatch( spentUsd += run.costUsd; const byUrl = new Map>(); - for (const it of run.items) byUrl.set(String(it.url ?? '').replace(/\/?$/, '/'), it); + const byShortCode = new Map>(); + for (const it of run.items) { + byUrl.set(String(it.url ?? '').replace(/\/?$/, '/'), it); + const sc = String(it.shortCode ?? it.id ?? ''); + if (sc) byShortCode.set(sc, it); + } + + const scFromUrl = (u: string) => u.match(/\/(p|reel|tv)\/([A-Za-z0-9_-]+)/)?.[2] ?? null; for (const u of urls) { - const item = byUrl.get(u.replace(/\/?$/, '/')); + const sc = scFromUrl(u); + const item = byUrl.get(u.replace(/\/?$/, '/')) ?? (sc ? byShortCode.get(sc) : undefined); if (!item) { markStatus(db, 'ig_queue', u, 'failed', 'no_dataset_item'); failed++; @@ -132,21 +140,28 @@ export async function runInstagram(ctx: CommandContext): Promise } const { budgetAbortUsd: abortUsd, budgetHardCapUsd: hardCapUsd, canarySize, igBatch } = ctx.config.capture; - const ceiling = (ctx.opts.budgetCap as number | undefined) ?? hardCapUsd; + const rawCap = ctx.opts.budgetCap; + const ceiling = rawCap !== undefined ? parseFloat(String(rawCap)) : hardCapUsd; + const limitOpt = ctx.opts.limit !== undefined ? parseInt(String(ctx.opts.limit), 10) : undefined; const client = makeClient(); let processed = 0; let failed = 0; let collisions = 0; + let remaining = limitOpt; // Canary ALWAYS runs first; --yes only bypasses the abort on projection overage. - const sample = sampleRows(db, canarySize); + const canarycap = Math.min(canarySize, remaining ?? canarySize); + const sample = sampleRows(db, canarycap); + let ratePerUrl = 0; if (sample.length > 0) { const r = await processBatch(vault, db, client, sample); processed += r.processed; failed += r.failed; collisions += r.collisions; + if (remaining !== undefined) remaining -= sample.length; const projection = projectCost(sample.length, r.spentUsd, pending); + ratePerUrl = sample.length > 0 ? r.spentUsd / sample.length : 0; ctx.log.info( `Canary: ${sample.length} URLs cost $${r.spentUsd.toFixed(4)} → projected $${projection.projectedCostUsd.toFixed(2)} for ${pending}.`, ); @@ -154,13 +169,17 @@ export async function runInstagram(ctx: CommandContext): Promise } for (;;) { - assertWithinBudget(budgetState(db, abortUsd, hardCapUsd)); - const rows = sampleRows(db, igBatch); + if (remaining !== undefined && remaining <= 0) break; + const batchSize = Math.min(igBatch, remaining ?? igBatch); + const projectedBatch = batchSize * ratePerUrl; + assertWithinBudget(budgetState(db, abortUsd, hardCapUsd), projectedBatch); + const rows = sampleRows(db, batchSize); if (rows.length === 0) break; const r = await processBatch(vault, db, client, rows); processed += r.processed; failed += r.failed; collisions += r.collisions; + if (remaining !== undefined) remaining -= rows.length; ctx.log.info(`Instagram: ${processed} processed, $${igSpend(db).toFixed(2)} spent.`); } diff --git a/src/capture/run/tiktok.ts b/src/capture/run/tiktok.ts index cc1a44b..053c5b3 100644 --- a/src/capture/run/tiktok.ts +++ b/src/capture/run/tiktok.ts @@ -95,24 +95,31 @@ export async function runTikTok(ctx: CommandContext): Promise { } const { budgetAbortUsd: abortUsd, budgetHardCapUsd: hardCapUsd, canarySize, ttBatch } = ctx.config.capture; - const ceiling = (ctx.opts.budgetCap as number | undefined) ?? hardCapUsd; + const rawCap = ctx.opts.budgetCap; + const ceiling = rawCap !== undefined ? parseFloat(String(rawCap)) : hardCapUsd; + const limitOpt = ctx.opts.limit !== undefined ? parseInt(String(ctx.opts.limit), 10) : undefined; const client = makeClient(); let processed = 0; let failed = 0; let collisions = 0; let spentUsd = 0; + let remaining = limitOpt; // Mandatory canary for TikTok (pricing not stably published). Always runs; // --yes only bypasses the abort on projection overage. - const sample = pending(db, canarySize); + const canarycap = Math.min(canarySize, remaining ?? canarySize); + const sample = pending(db, canarycap); + let ratePerUrl = 0; if (sample.length > 0) { const r = await processBatch(vault, db, client, sample); processed += r.processed; failed += r.failed; collisions += r.collisions; spentUsd += r.spentUsd; + if (remaining !== undefined) remaining -= sample.length; const projection = projectCost(sample.length, r.spentUsd, total); + ratePerUrl = sample.length > 0 ? r.spentUsd / sample.length : 0; ctx.log.info( `Canary: ${sample.length} URLs cost $${r.spentUsd.toFixed(4)} → projected $${projection.projectedCostUsd.toFixed(2)} for ${total}.`, ); @@ -120,14 +127,18 @@ export async function runTikTok(ctx: CommandContext): Promise { } for (;;) { - assertWithinBudget(budgetState(db, abortUsd, hardCapUsd)); - const batch = pending(db, ttBatch); + if (remaining !== undefined && remaining <= 0) break; + const batchSize = Math.min(ttBatch, remaining ?? ttBatch); + const projectedBatch = batchSize * ratePerUrl; + assertWithinBudget(budgetState(db, abortUsd, hardCapUsd), projectedBatch); + const batch = pending(db, batchSize); if (batch.length === 0) break; const r = await processBatch(vault, db, client, batch); processed += r.processed; failed += r.failed; collisions += r.collisions; spentUsd += r.spentUsd; + if (remaining !== undefined) remaining -= batch.length; ctx.log.info(`TikTok: ${processed} processed, $${spentUsd.toFixed(2)} spent.`); } diff --git a/src/capture/run/youtube.ts b/src/capture/run/youtube.ts index 874377b..0363038 100644 --- a/src/capture/run/youtube.ts +++ b/src/capture/run/youtube.ts @@ -111,8 +111,10 @@ export async function runYouTube(ctx: CommandContext): Promise { return { platform: 'youtube', enqueued, processed: 0, failed: 0, spentUsd: 0, collisions: 0 }; } + const limitOpt = ctx.opts.limit !== undefined ? parseInt(String(ctx.opts.limit), 10) : undefined; + // Write the pending batch file and run yt-dlp (subs + info.json only, $0). - const batchUrls = pendingUrls(db, 'yt_queue'); + const batchUrls = pendingUrls(db, 'yt_queue', limitOpt); const batchFile = join(work, 'batch.txt'); mkdirSync(work, { recursive: true }); writeFileSync(batchFile, batchUrls.join('\n'), 'utf8'); @@ -132,6 +134,10 @@ export async function runYouTube(ctx: CommandContext): Promise { hint: 'Install yt-dlp; capture YouTube needs it on PATH.', }); } + throw new MagnusError(`yt-dlp failed to start: ${(err as Error).message}`, { + code: ExitCode.Network, + errorCode: 'E_YTDLP_ERROR', + }); } // Render each video from its info.json + best transcript. diff --git a/src/cli.ts b/src/cli.ts index 21a8e95..5d8c7e1 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -106,11 +106,12 @@ async function execute( * `magnus ` form and route to the default `download` command. */ function injectDefaultCommand(argv: string[]): string[] { + if (argv.length < 2) return argv; const rest = argv.slice(2); const firstNonFlag = rest.find((a) => !a.startsWith('-')); if (!firstNonFlag) return argv; if (findCommand(firstNonFlag)) return argv; - return [argv[0]!, argv[1]!, 'download', ...rest]; + return [argv[0] as string, argv[1] as string, 'download', ...rest]; } export async function main(argv = process.argv): Promise { @@ -142,5 +143,5 @@ export async function main(argv = process.argv): Promise { main().catch((err) => { process.stderr.write(`fatal: ${(err as Error).message}\n`); - process.exit(ExitCode.Error); + process.exitCode = ExitCode.Error; }); diff --git a/src/commands/config.ts b/src/commands/config.ts index b6aa7fc..54ab070 100644 --- a/src/commands/config.ts +++ b/src/commands/config.ts @@ -38,7 +38,6 @@ async function runConfig(ctx: CommandContext): Promise { case undefined: case 'list': { const cfg = loadConfig(override); - ctx.log.info(JSON.stringify(cfg, null, 2)); return { data: cfg, human: JSON.stringify(cfg, null, 2) }; } case 'path': { diff --git a/src/util/redact.ts b/src/util/redact.ts index de57f86..2d36777 100644 --- a/src/util/redact.ts +++ b/src/util/redact.ts @@ -5,10 +5,15 @@ const SECRET_KEYS = /^(apify_token|token|authorization|cookie|cookies|password|secret)$/i; const APIFY_TOKEN_RE = /apify_api_[A-Za-z0-9]+/g; +const BEARER_RE = /(Bearer\s+)\S+/gi; +const GENERIC_SECRET_RE = /\b(token|secret|password|api[-_]?key)\s*=\s*\S+/gi; /** Replace known secret patterns inside a free-text string. */ export function redactString(input: string): string { - return input.replace(APIFY_TOKEN_RE, 'apify_api_***'); + return input + .replace(APIFY_TOKEN_RE, 'apify_api_***') + .replace(BEARER_RE, '$1***') + .replace(GENERIC_SECRET_RE, (_, k: string) => `${k}=***`); } /** Deep-redact an object/array for safe serialization. */ From 2714f8c572af1fb0034b998edfa4700560dd05d4 Mon Sep 17 00:00:00 2001 From: Eduardo Borjas Date: Wed, 3 Jun 2026 00:43:32 -0600 Subject: [PATCH 3/6] chore(brain): add per-project second brain layer (.brain/ + hooks) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Portable three-layer knowledge graph integration: SessionStart/PostToolUse/ UserPromptSubmit/Stop/PreToolUse hooks, graphify graph (auto-updates on commit), decisions + bugs ADRs committed, runtime artifacts gitignored. No-ops gracefully without local CKIS vault — external contributors unaffected. Registered in Dev Brain (~/Documents/Dev Brain/); graph-report auto-synced to CKIS 02-projects//graph-report.md on each commit. Co-Authored-By: Claude Sonnet 4.6 (1M context) --- .brain/BRAIN.md | 91 ++++++++ .brain/README.md | 52 +++++ .brain/bugs/README.md | 30 +++ .brain/config.sh | 29 +++ .brain/decisions/README.md | 32 +++ .brain/graph/.gitkeep | 0 .brain/scripts/assemble-context.sh | 165 +++++++++++++ .brain/scripts/lib/compact-routing.sh | 53 +++++ .brain/scripts/log-compact.sh | 89 +++++++ .brain/scripts/log-session.sh | 297 ++++++++++++++++++++++++ .brain/scripts/log-tool-event.sh | 112 +++++++++ .brain/scripts/register-to-dev-brain.sh | 10 + .brain/scripts/sync-graph-to-vault.sh | 50 ++++ .brain/scripts/sync-obsidian-graph.sh | 54 +++++ .brain/sessions/.gitkeep | 0 .claude/settings.json | 67 ++++++ .gitignore | 12 + 17 files changed, 1143 insertions(+) create mode 100644 .brain/BRAIN.md create mode 100644 .brain/README.md create mode 100644 .brain/bugs/README.md create mode 100644 .brain/config.sh create mode 100644 .brain/decisions/README.md create mode 100644 .brain/graph/.gitkeep create mode 100755 .brain/scripts/assemble-context.sh create mode 100755 .brain/scripts/lib/compact-routing.sh create mode 100755 .brain/scripts/log-compact.sh create mode 100755 .brain/scripts/log-session.sh create mode 100755 .brain/scripts/log-tool-event.sh create mode 100755 .brain/scripts/register-to-dev-brain.sh create mode 100755 .brain/scripts/sync-graph-to-vault.sh create mode 100755 .brain/scripts/sync-obsidian-graph.sh create mode 100644 .brain/sessions/.gitkeep create mode 100644 .claude/settings.json diff --git a/.brain/BRAIN.md b/.brain/BRAIN.md new file mode 100644 index 0000000..d726d2b --- /dev/null +++ b/.brain/BRAIN.md @@ -0,0 +1,91 @@ +# `.brain/` — agent workflow rules + +This repo has a per-project second brain at `.brain/`. Hooks in +`.claude/settings.json` drive it automatically. Your job during a session: +keep the brain useful for the *next* session. + +## At session start (already automatic) + +The `SessionStart` hook runs `.brain/scripts/assemble-context.sh`, which +generates `.brain/_CONTEXT.md` and injects it into context. It contains: +- Pointers to CKIS (`_MEMORY.md`, `_overview.md`, architecture spec) +- Last 3 session summaries +- Open decisions and bugs +- Top of `GRAPH_REPORT.md` (if Graphify has run) + +You don't need to read `_CONTEXT.md` again — it's already in context. + +## During the session — what's auto-captured + +The `PostToolUse` hook runs `.brain/scripts/log-tool-event.sh` after every +Bash call and silently appends one line to `.brain/sessions/_active.md` +**only** for these objectively important events: + +- `npm run build` / `npm test` / `npm run lint` → success or failure (with last + ~8 lines of output on failure, so the error is preserved next session) +- `git commit` → SHA + message + diffstat + +The `UserPromptSubmit` hook runs `.brain/scripts/log-compact.sh` on every user +prompt but only acts on `/compact` commands — everything else is ignored. When +detected, it writes a timestamp breadcrumb to `.brain/.compact-triggers`. + +Everything else (file edits, reads, other shell commands, regular prompts) is +**not logged** — that would be noise. The Stop hook merges `_active.md` into +the final session log under `## Iterations`. This means: every build, every +test, every commit you make in a session is permanently in the brain — +searchable, visible to the next session, and impossible to lose to a `/clear`. + +When `/compact` runs, the Stop hook extracts the full summary from the session +JSONL transcript and writes it to `.brain/sessions/compacts/-compact.md`. +The session file gets a `## Compactions` section with a pointer and a 200-char +excerpt — the full summary is never auto-inlined to keep `_CONTEXT.md` lean. + +## During the session + +When the work warrants it, write to: + +- **`.brain/decisions/YYYY-MM-DD-.md`** — for any decision Eduardo + makes about Korvex Web (architecture, dependency, deploy, scope). Use the + CKIS decision-log format. See `.brain/decisions/README.md`. +- **`.brain/bugs/YYYY-MM-DD-.md`** — when a bug is found *and* fixed, + capture the lesson (root cause, why it happened, how to prevent it). The + patch lives in the commit; this file is for the *why*. + +Important decisions also get a one-line cross-post to +`~/Documents/Second Brain/00-inbox/_MEMORY.md` Open Decisions. + +## Before ending the session + +The Stop hook fills in the objective metadata automatically: +- Iterations (every build/test/lint/commit, with timestamps) +- Commits made, files changed, duration, branch state + +Your only job is the **narrative `## Summary`** — and only when it adds +value. If the session was a routine commit-and-iterate cycle, the iteration +log already tells the story. Fill in the Summary when: + +- A non-obvious decision was made mid-session +- A bug had a *why* worth remembering (root cause, not patch) +- Something is unfinished and the next session needs to know + +Format: 2–4 bullets max. What was done · What was decided · What's next. +Skip it if the iteration log + commits speak for themselves. + +## CKIS bridge — when to escalate + +| Situation | Goes to | +| --- | --- | +| Routine code change, bug fix, refactor | Just commit. No brain entry needed. | +| Decision about *this* project | `.brain/decisions/` | +| Bug worth a postmortem | `.brain/bugs/` | +| Strategic / cross-project / personal | CKIS `_MEMORY.md` + `02-projects/korvex/_overview.md` | +| Pattern reusable across projects | CKIS `03-knowledge/permanent-notes/` | + +When in doubt: project decision → `.brain/`. Strategic → CKIS. + +## Hard rules + +- Never modify `.brain/_CONTEXT.md` by hand — it's regenerated each session. +- Never delete files in `.brain/decisions/` or `.brain/bugs/` — supersede instead. +- `.brain/sessions/` is gitignored (personal); `decisions/` and `bugs/` are committed. +- Graphify rebuilds `.brain/graph/` on every commit (post-commit hook). Don't edit it. diff --git a/.brain/README.md b/.brain/README.md new file mode 100644 index 0000000..28a887b --- /dev/null +++ b/.brain/README.md @@ -0,0 +1,52 @@ +--- +type: brain-readme +project: korvex-web +created: 2026-05-03 +modified: 2026-05-03 +tags: [brain, ckis-bridge] +--- + +# `.brain/` — Per-project second brain + +Project-level memory layer for Claude Code sessions. Bridges this repo to the +global CKIS vault at `~/Documents/Second Brain/`. + +## What lives here + +| Path | Purpose | Committed? | +| ---------------- | ------------------------------------------------ | --------------- | +| `_CONTEXT.md` | Auto-assembled session start context | No (regenerable) | +| `decisions/` | Decision logs (CKIS format — see skill card) | **Yes** | +| `bugs/` | Bug → fix narratives | **Yes** | +| `sessions/` | Per-session summaries written by Stop hook | No (personal) | +| `graph/` | Graphify output (`graph.json`, `GRAPH_REPORT.md`, vault/) | No (regenerable) | +| `scripts/` | Hooks + assembler scripts | **Yes** | +| `config.sh` | Per-project config (CKIS paths, project slug) | **Yes** | + +## How it works + +1. **SessionStart hook** runs `scripts/assemble-context.sh`: + - Concatenates latest 3 session summaries + open decisions + active bugs + - Pulls god-nodes section from `graph/GRAPH_REPORT.md` if Graphify has run + - Adds pointers to CKIS `_MEMORY.md` and project `_overview.md` + - Writes the result to `_CONTEXT.md` and emits it as session context. + +2. **Stop hook** runs `scripts/log-session.sh`: + - Records git diff, commits, branch, duration vs. session start. + - Creates `sessions/YYYY-MM-DD-HHMM-session.md` with a "Summary" section to fill in. + +3. **Graphify** rebuilds `graph/` automatically on every git commit + (via `graphify hook install`) and is symlinked from the CKIS vault under + `02-projects/korvex/graph/` so the graph view shows up alongside the + curated overview. + +## Bridge to CKIS + +- Strategic / cross-project state → `~/Documents/Second Brain/00-inbox/_MEMORY.md` +- Project overview (curated) → `~/Documents/Second Brain/02-projects/korvex/_overview.md` +- Architecture spec → `~/Documents/Second Brain/03-knowledge/permanent-notes/per-project-second-brain.md` + +## See also + +- `00-system/ckis/06-decision-execution-and-review-protocol.md` — decision-log format +- `.claude/skills/ckis-decision-log/` — skill that writes here diff --git a/.brain/bugs/README.md b/.brain/bugs/README.md new file mode 100644 index 0000000..d723e9b --- /dev/null +++ b/.brain/bugs/README.md @@ -0,0 +1,30 @@ +--- +type: index +created: 2026-05-03 +tags: [bugs, brain] +--- + +# Bugs + +Bug → fix narratives for `korvex-web`. Capture the *why*, not just the patch +(the patch is in the commit; this folder is for the lesson). + +## File naming + +`YYYY-MM-DD-.md`. + +## Required frontmatter + +```yaml +--- +type: bug +project: korvex +status: open | fixed | wontfix +date: YYYY-MM-DD +severity: low | medium | high +related-commits: [, ...] +tags: [bug, korvex] +--- +``` + +Bugs with `status: open` are surfaced in `_CONTEXT.md` at session start. diff --git a/.brain/config.sh b/.brain/config.sh new file mode 100644 index 0000000..c9f0ebc --- /dev/null +++ b/.brain/config.sh @@ -0,0 +1,29 @@ +# Per-project .brain/ configuration +# Sourced by every script in .brain/scripts/. + +# Project identity +PROJECT_SLUG="magnus-cli" +PROJECT_NAME="Magnus CLI" + +# CKIS vault paths (absolute — adjust per machine if vault relocates) +CKIS_VAULT="$HOME/Documents/Second Brain" +CKIS_MEMORY="$CKIS_VAULT/00-inbox/_MEMORY.md" +CKIS_PROJECT_OVERVIEW="$CKIS_VAULT/02-projects/$PROJECT_SLUG/_overview.md" +CKIS_ARCHITECTURE_NOTE="$CKIS_VAULT/03-knowledge/permanent-notes/per-project-second-brain.md" + +# Brain paths (relative to repo root) +BRAIN_DIR=".brain" +SESSIONS_DIR="$BRAIN_DIR/sessions" +DECISIONS_DIR="$BRAIN_DIR/decisions" +BUGS_DIR="$BRAIN_DIR/bugs" +GRAPH_DIR="$BRAIN_DIR/graph" +CONTEXT_FILE="$BRAIN_DIR/_CONTEXT.md" +SESSION_STATE="$BRAIN_DIR/.session-state" + +# How many recent session summaries to inline in _CONTEXT.md +RECENT_SESSIONS_LIMIT=3 + +# Dev Brain vault (Obsidian — code graph + wiki layer, separate from CKIS) +DEV_BRAIN_VAULT="$HOME/Documents/Dev Brain" +# Rebuild --obsidian vault every N commits (expensive: one .md per node) +OBSIDIAN_GRAPH_CADENCE=10 diff --git a/.brain/decisions/README.md b/.brain/decisions/README.md new file mode 100644 index 0000000..bc9d5c1 --- /dev/null +++ b/.brain/decisions/README.md @@ -0,0 +1,32 @@ +--- +type: index +created: 2026-05-03 +tags: [decisions, brain] +--- + +# Decisions + +Decision logs for `korvex-web`. CKIS format — see +`~/Documents/Second Brain/00-system/ckis/06-decision-execution-and-review-protocol.md`. + +## File naming + +`YYYY-MM-DD-.md` — one decision per file. + +## Required frontmatter + +```yaml +--- +type: decision +project: korvex +status: proposed | adopted | superseded +date: YYYY-MM-DD +reversal-cost: low | medium | high +review-by: YYYY-MM-DD or empty +tags: [decision, korvex] +--- +``` + +Decisions with `status: proposed` are surfaced in `_CONTEXT.md` at session start +and in CKIS `_MEMORY.md` Open Decisions. Important decisions are cross-posted +to `_MEMORY.md` as one-line entries pointing back here. diff --git a/.brain/graph/.gitkeep b/.brain/graph/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/.brain/scripts/assemble-context.sh b/.brain/scripts/assemble-context.sh new file mode 100755 index 0000000..d524902 --- /dev/null +++ b/.brain/scripts/assemble-context.sh @@ -0,0 +1,165 @@ +#!/usr/bin/env bash +# assemble-context.sh — SessionStart hook +# +# Builds .brain/_CONTEXT.md from: +# - latest N session summaries +# - open decisions (status: proposed) +# - open bugs (status: open) +# - god-nodes section from .brain/graph/GRAPH_REPORT.md (if Graphify ran) +# - pointers to CKIS vault +# +# Records session start state to .brain/.session-state for the Stop hook. +# Emits the assembled context to stdout (Claude Code injects it into the session). + +set -euo pipefail + +# Resolve repo root from this script's location. +REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" +cd "$REPO_ROOT" + +# shellcheck disable=SC1091 +source "$REPO_ROOT/.brain/config.sh" + +NOW="$(date -u +"%Y-%m-%dT%H:%M:%SZ")" +NOW_LOCAL="$(date +"%Y-%m-%d %H:%M %Z")" +HEAD_SHA="$(git rev-parse --short HEAD 2>/dev/null || echo "no-git")" +BRANCH="$(git rev-parse --abbrev-ref HEAD 2>/dev/null || echo "no-git")" + +# Record session-start state for the Stop hook. +mkdir -p "$BRAIN_DIR" +cat > "$SESSION_STATE" </dev/null || true +fi + +# Rotate orphaned compact-triggers (from a session that never reached Stop). +COMPACT_TRIGGERS="$BRAIN_DIR/.compact-triggers" +if [ -f "$COMPACT_TRIGGERS" ]; then + mkdir -p "$SESSIONS_DIR" + mv "$COMPACT_TRIGGERS" "$SESSIONS_DIR/_orphaned-compacts-$(date +%Y-%m-%d-%H%M).log" 2>/dev/null || true +fi + +# Sync GRAPH_REPORT.md into the CKIS vault (catch-up in case post-commit hook missed it). +bash "$REPO_ROOT/.brain/scripts/sync-graph-to-vault.sh" >/dev/null 2>&1 || true + +# Build _CONTEXT.md. +{ + echo "---" + echo "type: project-context" + echo "project: $PROJECT_SLUG" + echo "generated: $NOW" + echo "branch: $BRANCH" + echo "head: $HEAD_SHA" + echo "tags: [context, brain, $PROJECT_SLUG]" + echo "---" + echo + echo "# $PROJECT_NAME — Session Context" + echo + echo "> Auto-generated by \`.brain/scripts/assemble-context.sh\` at session start." + echo "> Do not hand-edit. Update sources in \`.brain/decisions/\`, \`.brain/bugs/\`, or \`.brain/sessions/\`." + echo + echo "**Branch:** \`$BRANCH\` · **HEAD:** \`$HEAD_SHA\` · **Started:** $NOW_LOCAL" + echo + echo "━━━" + echo + echo "## CKIS pointers" + echo + echo "- Live business state → \`$CKIS_MEMORY\`" + echo "- Project overview (curated) → \`$CKIS_PROJECT_OVERVIEW\`" + echo "- Architecture spec → \`$CKIS_ARCHITECTURE_NOTE\`" + echo + echo "━━━" + echo + echo "## Recent sessions (last $RECENT_SESSIONS_LIMIT)" + echo + if compgen -G "$SESSIONS_DIR/*.md" > /dev/null; then + # Newest first, take N. + mapfile -t recent < <(ls -1t "$SESSIONS_DIR"/*.md 2>/dev/null | head -n "$RECENT_SESSIONS_LIMIT") + for f in "${recent[@]}"; do + echo "### $(basename "$f" .md)" + echo + cat "$f" + echo + echo "---" + echo + done + else + echo "_No prior sessions logged yet._" + echo + fi + + echo "━━━" + echo + echo "## Open decisions" + echo + if compgen -G "$DECISIONS_DIR/*.md" > /dev/null; then + found=0 + for f in "$DECISIONS_DIR"/*.md; do + [ "$(basename "$f")" = "README.md" ] && continue + # Match `status: proposed` in frontmatter. + if awk '/^---$/{c++} c==1 && /^status:[[:space:]]*proposed/{print; exit}' "$f" | grep -q proposed; then + title="$(awk '/^# /{sub(/^# /,""); print; exit}' "$f")" + [ -z "$title" ] && title="$(basename "$f" .md)" + echo "- [$title]($f)" + found=1 + fi + done + [ "$found" = "0" ] && echo "_No open decisions._" + else + echo "_No decisions logged yet._" + fi + echo + + echo "━━━" + echo + echo "## Open bugs" + echo + if compgen -G "$BUGS_DIR/*.md" > /dev/null; then + found=0 + for f in "$BUGS_DIR"/*.md; do + [ "$(basename "$f")" = "README.md" ] && continue + if awk '/^---$/{c++} c==1 && /^status:[[:space:]]*open/{print; exit}' "$f" | grep -q open; then + title="$(awk '/^# /{sub(/^# /,""); print; exit}' "$f")" + [ -z "$title" ] && title="$(basename "$f" .md)" + echo "- [$title]($f)" + found=1 + fi + done + [ "$found" = "0" ] && echo "_No open bugs._" + else + echo "_No bugs logged yet._" + fi + echo + + echo "━━━" + echo + echo "## Code graph (Graphify)" + echo + REPORT="$GRAPH_DIR/GRAPH_REPORT.md" + if [ -f "$REPORT" ]; then + # Inline only the "God nodes" or "Surprising connections" section, capped. + awk ' + /^## (God nodes|Surprising connections|Suggested questions)/ { keep=1; print; next } + keep && /^## / { keep=0 } + keep { print } + ' "$REPORT" | head -n 80 + echo + echo "_Full graph report: \`$REPORT\`_" + else + echo "_Graphify has not run yet. Run: \`graphify .\` then \`graphify hook install\`._" + fi + echo +} > "$CONTEXT_FILE" + +# Emit to stdout so the SessionStart hook injects it as context. +cat "$CONTEXT_FILE" diff --git a/.brain/scripts/lib/compact-routing.sh b/.brain/scripts/lib/compact-routing.sh new file mode 100755 index 0000000..e07b671 --- /dev/null +++ b/.brain/scripts/lib/compact-routing.sh @@ -0,0 +1,53 @@ +#!/usr/bin/env bash +# compact-routing.sh — shared helpers for routing compact summaries to Dev Brain. +# Source-only. No side effects on load. + +# route_compact_to_dev_brain +# Copies a compact .md to $DEV_BRAIN_VAULT/sessions/compacts//. +# Idempotent: skips if destination already exists with same content. +# Fail-safe: any error is swallowed; never affects caller exit status. +route_compact_to_dev_brain() { + local src="$1" project="$2" ts="$3" sid="${4:-unknown}" + local vault="${DEV_BRAIN_VAULT:-$HOME/Documents/Dev Brain}" + + [ -f "$src" ] || return 0 + [ -d "$vault" ] || return 0 + + local dest_dir="$vault/sessions/compacts/$project" + mkdir -p "$dest_dir" 2>/dev/null || return 0 + + local base + base="$(basename "$src")" + local dest="$dest_dir/$base" + + # Idempotency: skip if destination already exists with identical content. + if [ -f "$dest" ] && cmp -s "$src" "$dest" 2>/dev/null; then + return 0 + fi + + # Build dest content: source file + wikilinks footer (for Obsidian graph connectivity). + local tmp="$dest.tmp.$$" + { + cat "$src" + # Inject footer only if not already present. + if ! grep -q "\[\[wiki/$project\]\]" "$src" 2>/dev/null; then + echo "" + echo "---" + printf '[[wiki/%s]] · [[sessions/index]]\n' "$project" + fi + } > "$tmp" 2>/dev/null || return 0 + mv -f "$tmp" "$dest" 2>/dev/null || { rm -f "$tmp"; return 0; } + + echo "[brain] Compact routed → $dest" >&2 + return 0 +} + +# route_all_session_compacts +# Reads the COMPACTS_TMP ledger (ts|path|excerpt) and routes each entry. +route_all_session_compacts() { + local tmp="$1" project="$2" sid="${3:-unknown}" + [ -f "$tmp" ] || return 0 + while IFS='|' read -r ts path excerpt; do + [ -n "$path" ] && route_compact_to_dev_brain "$path" "$project" "$ts" "$sid" + done < "$tmp" +} diff --git a/.brain/scripts/log-compact.sh b/.brain/scripts/log-compact.sh new file mode 100755 index 0000000..b0cdd7b --- /dev/null +++ b/.brain/scripts/log-compact.sh @@ -0,0 +1,89 @@ +#!/usr/bin/env bash +# log-compact.sh — UserPromptSubmit hook +# +# Fires when the user submits "/compact" or "/compact ". +# At this moment, the NEW compact hasn't been generated yet — but any PRIOR +# compact from this session IS already in the transcript. We eagerly extract +# the most recent prior compact and route it to Dev Brain immediately, so +# long-running sessions don't accumulate un-mirrored compacts until session end. +# +# The Stop hook (log-session.sh) is the final catch-all — this is best-effort +# acceleration. Both use the same idempotent route_compact_to_dev_brain helper. +# +# Fail-safe: any error → silent no-op. UserPromptSubmit must not emit stdout. + +set -uo pipefail + +REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" +cd "$REPO_ROOT" + +source "$REPO_ROOT/.brain/config.sh" 2>/dev/null || exit 0 + +PAYLOAD="" +[ ! -t 0 ] && PAYLOAD="$(cat)" +[ -z "$PAYLOAD" ] && exit 0 + +command -v jq >/dev/null 2>&1 || exit 0 + +PROMPT="$(echo "$PAYLOAD" | jq -r '.prompt // empty' 2>/dev/null || echo "")" +case "$PROMPT" in + "/compact"|"/compact "*) ;; + *) exit 0 ;; +esac + +TRANSCRIPT_PATH="$(echo "$PAYLOAD" | jq -r '.transcript_path // empty' 2>/dev/null || echo "")" +SESSION_ID="$(echo "$PAYLOAD" | jq -r '.session_id // empty' 2>/dev/null || echo "")" +[ -n "$TRANSCRIPT_PATH" ] && [ -f "$TRANSCRIPT_PATH" ] || exit 0 + +mkdir -p "$SESSIONS_DIR/compacts" 2>/dev/null || exit 0 + +# Extract the most recent prior compact summary (if any). +LAST_COMPACT="$(jq -cs ' + def textify: + if type == "array" then + [ .[] | select(.type == "text") | .text ] | join("\n\n") + elif type == "string" then . + else "" end; + + [ .[] + | select(.isCompactSummary == true) + | {ts: (.timestamp // "unknown"), + content: (.message.content | textify)} + | select(.content != "") + ] | last // empty +' "$TRANSCRIPT_PATH" 2>/dev/null)" + +[ -z "$LAST_COMPACT" ] || [ "$LAST_COMPACT" = "null" ] && exit 0 + +TS="$(echo "$LAST_COMPACT" | jq -r '.ts')" +CONTENT="$(echo "$LAST_COMPACT" | jq -r '.content')" +SLUG="$(date -u -d "$TS" +"%Y-%m-%d-%H%M" 2>/dev/null || date -u +"%Y-%m-%d-%H%M")" +OUT_FILE="$SESSIONS_DIR/compacts/${SLUG}-compact.md" + +# Write compact file only if it doesn't already exist (idempotent). +if [ ! -f "$OUT_FILE" ]; then + { + echo "---" + echo "type: compact-summary" + echo "project: $PROJECT_SLUG" + echo "session-id: ${SESSION_ID:-unknown}" + echo "compacted-at: $TS" + echo "source: log-compact.sh (eager)" + echo "tags: [compact, $PROJECT_SLUG]" + echo "---" + echo + echo "# Compact Summary — $TS" + echo + echo "$CONTENT" + } > "$OUT_FILE" 2>/dev/null +fi + +# Route to Dev Brain. +if [ -f "$REPO_ROOT/.brain/scripts/lib/compact-routing.sh" ]; then + # shellcheck disable=SC1091 + source "$REPO_ROOT/.brain/scripts/lib/compact-routing.sh" + route_compact_to_dev_brain "$OUT_FILE" "$PROJECT_SLUG" "$TS" "${SESSION_ID:-unknown}" +fi 2>/dev/null || true + +# No stdout — UserPromptSubmit must not inject context. +exit 0 diff --git a/.brain/scripts/log-session.sh b/.brain/scripts/log-session.sh new file mode 100755 index 0000000..f08e701 --- /dev/null +++ b/.brain/scripts/log-session.sh @@ -0,0 +1,297 @@ +#!/usr/bin/env bash +# log-session.sh — Stop hook +# +# Captures objective session metadata at end of session: +# - timestamp + duration +# - branch + git diff vs. session start +# - commits made during the session +# - /compact summaries extracted from JSONL transcript +# +# Writes .brain/sessions/YYYY-MM-DD-HHMM-session.md. +# Compact summaries go to .brain/sessions/compacts/ (separate files, pointer in session). + +set -euo pipefail + +REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" +cd "$REPO_ROOT" + +# shellcheck disable=SC1091 +source "$REPO_ROOT/.brain/config.sh" + +# Read stdin payload if present (Claude Code Stop hook sends JSON). +PAYLOAD="" +if [ ! -t 0 ]; then + PAYLOAD="$(cat)" +fi + +SESSION_ID="" +TRANSCRIPT_PATH="" +if [ -n "$PAYLOAD" ] && command -v jq >/dev/null 2>&1; then + SESSION_ID="$(echo "$PAYLOAD" | jq -r '.session_id // empty' 2>/dev/null || true)" + TRANSCRIPT_PATH="$(echo "$PAYLOAD" | jq -r '.transcript_path // empty' 2>/dev/null || true)" +fi + +NOW_UTC="$(date -u +"%Y-%m-%dT%H:%M:%SZ")" +NOW_LOCAL="$(date +"%Y-%m-%d %H:%M %Z")" +DATE_TAG="$(date +"%Y-%m-%d-%H%M")" +BRANCH="$(git rev-parse --abbrev-ref HEAD 2>/dev/null || echo "no-git")" +HEAD_SHA="$(git rev-parse --short HEAD 2>/dev/null || echo "no-git")" + +# Load session-start state if recorded. +START_SHA="" +START_LOCAL="" +SESSION_START_UTC="" +DURATION="" +if [ -f "$SESSION_STATE" ]; then + # shellcheck disable=SC1090 + source "$SESSION_STATE" + START_SHA="${SESSION_START_SHA:-}" + START_LOCAL="${SESSION_START_LOCAL:-}" + SESSION_START_UTC="${SESSION_START_UTC:-}" + if [ -n "${SESSION_START_UTC:-}" ]; then + start_epoch="$(date -u -d "$SESSION_START_UTC" +%s 2>/dev/null || echo 0)" + end_epoch="$(date -u +%s)" + if [ "$start_epoch" -gt 0 ]; then + delta=$((end_epoch - start_epoch)) + DURATION="$((delta / 60)) min" + fi + fi +fi + +# Compute git activity during the session. +FILES_CHANGED="" +COMMITS_MADE="" +DIFFSTAT="" +if [ -n "$START_SHA" ] && [ "$START_SHA" != "$HEAD_SHA" ]; then + FILES_CHANGED="$(git diff --name-only "$START_SHA" HEAD 2>/dev/null || true)" + COMMITS_MADE="$(git log --oneline "$START_SHA..HEAD" 2>/dev/null || true)" + DIFFSTAT="$(git diff --stat "$START_SHA" HEAD 2>/dev/null | tail -n 1 || true)" +fi +WORKING_TREE="$(git status --short 2>/dev/null || true)" + +# Pull in iterations captured during the session by log-tool-event.sh. +ACTIVE_LOG="$SESSIONS_DIR/_active.md" +ITERATIONS="" +if [ -f "$ACTIVE_LOG" ]; then + ITERATIONS="$(grep -v '^" + echo + echo "## Iterations" + echo + echo "_Auto-captured by \`log-tool-event.sh\` during the session: builds, tests, lint, commits._" + echo + if [ -n "$ITERATIONS" ]; then + echo "$ITERATIONS" + else + echo "_No build/test/lint/commit events recorded._" + fi + echo + echo "## Compactions" + echo + if [ "${#COMPACT_LINES[@]}" -gt 0 ]; then + for line in "${COMPACT_LINES[@]}"; do + IFS='|' read -r ts file excerpt <<< "$line" + echo "- **$ts** → \`$file\`" + echo " > ${excerpt}…" + done + else + echo "_No /compact during this session._" + fi + echo + echo "## Commits made" + echo + if [ -n "$COMMITS_MADE" ]; then + echo '```' + echo "$COMMITS_MADE" + echo '```' + else + echo "_No commits made during this session._" + fi + echo + echo "## Files changed" + echo + if [ -n "$FILES_CHANGED" ]; then + echo '```' + echo "$FILES_CHANGED" + echo '```' + [ -n "$DIFFSTAT" ] && echo "**Diffstat:** $DIFFSTAT" + else + echo "_No tracked files changed via commits._" + fi + echo + echo "## Working tree at end" + echo + if [ -n "$WORKING_TREE" ]; then + echo '```' + echo "$WORKING_TREE" + echo '```' + else + echo "_Clean._" + fi + echo +} > "$OUT" + +# Cleanup transient session state. +rm -f "$SESSION_STATE" +rm -f "$ACTIVE_LOG" +rm -f "$COMPACTS_TMP" + +echo "[brain] Session logged → $OUT" >&2 + +# ── Dev Brain session index ─────────────────────────────────────────────────── +# Append a pointer to Dev Brain so any agent can query session history. +# Fails silently — Dev Brain indexing must never break the primary stop hook. +{ + DEV_BRAIN_VAULT="${DEV_BRAIN_VAULT:-$HOME/Documents/Dev Brain}" + if [ -d "$DEV_BRAIN_VAULT" ]; then + IDX="$DEV_BRAIN_VAULT/sessions/index.md" + # Hybrid summary: compact → commit → last assistant turn → diffstat → no-summary. + # All tiers routed through _sb_sanitize to keep the pipe-delimited index format intact. + _sb_sanitize() { + tr '\n\r\t|' ' ' | tr -s ' ' | sed 's/^ *//;s/ *$//' | head -c 120 + } + + SUMMARY_LINE="" + + # Tier 1: compact excerpt (LLM-distilled, highest signal when present) + if [ "${#COMPACT_LINES[@]}" -gt 0 ]; then + SUMMARY_LINE="$(printf '%s' "${COMPACT_LINES[0]}" | awk -F'|' '{for(i=3;i<=NF;i++)printf "%s%s",$i,(i/dev/null | _sb_sanitize)" + fi + + # Tier 4: diffstat as structural fallback + if [ -z "$SUMMARY_LINE" ] && [ -n "$DIFFSTAT" ]; then + SUMMARY_LINE="$(printf '%s' "$DIFFSTAT" | _sb_sanitize)" + fi + + SUMMARY_LINE="${SUMMARY_LINE:-no-summary}" + # Append one-liner to global index (pipe-separated for grep/awk) + echo "${NOW_UTC} | ${PROJECT_SLUG} | ${DURATION:-unknown} | ${HEAD_SHA} | ${SUMMARY_LINE} | ${OUT}" >> "$IDX" + echo "[brain] Session indexed → $IDX" >&2 + fi +} 2>/dev/null || true diff --git a/.brain/scripts/log-tool-event.sh b/.brain/scripts/log-tool-event.sh new file mode 100755 index 0000000..5d4aa29 --- /dev/null +++ b/.brain/scripts/log-tool-event.sh @@ -0,0 +1,112 @@ +#!/usr/bin/env bash +# log-tool-event.sh — PostToolUse hook +# +# Captures objectively important Bash command outcomes (builds, tests, lint, +# commits) and appends them to the active session log: +# .brain/sessions/_active.md +# +# All other tool calls (Edit, Read, Write, etc.) and unrelated Bash commands +# are ignored. The Stop hook merges _active.md into the final session log. +# +# Fail-safe: any parse error or missing field results in a silent no-op so +# the user's session is never disrupted. + +set -uo pipefail # no -e: never break the user's session + +REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" +cd "$REPO_ROOT" + +# shellcheck disable=SC1091 +source "$REPO_ROOT/.brain/config.sh" 2>/dev/null || exit 0 + +ACTIVE_LOG="$SESSIONS_DIR/_active.md" + +# Read JSON payload from stdin (Claude Code provides tool_name, tool_input, tool_response). +PAYLOAD="" +if [ ! -t 0 ]; then + PAYLOAD="$(cat)" +fi +[ -z "$PAYLOAD" ] && exit 0 + +# jq is required to parse the payload — degrade gracefully if missing. +command -v jq >/dev/null 2>&1 || exit 0 + +TOOL_NAME=$(echo "$PAYLOAD" | jq -r '.tool_name // empty' 2>/dev/null || echo "") +[ "$TOOL_NAME" != "Bash" ] && exit 0 + +CMD=$(echo "$PAYLOAD" | jq -r '.tool_input.command // empty' 2>/dev/null || echo "") +[ -z "$CMD" ] && exit 0 + +# Filter: only log build / test / lint / commit. Everything else is noise. +EVENT_KIND="" +case "$CMD" in + *"npm run build"*|*"yarn build"*|*"pnpm build"*) EVENT_KIND="build" ;; + *"npm run test"*|*"npm test"*|*"yarn test"*|*"pnpm test"*) EVENT_KIND="test" ;; + *"npm run lint"*|*"yarn lint"*|*"pnpm lint"*) EVENT_KIND="lint" ;; + *"git commit"*) EVENT_KIND="commit" ;; + *) exit 0 ;; +esac + +# Extract result fields (defensive — schema may vary). +EXIT_CODE=$(echo "$PAYLOAD" | jq -r ' + .tool_response.exit_code // + .tool_response.exitCode // + .tool_response.returncode // + 0 +' 2>/dev/null || echo "0") + +OUTPUT=$(echo "$PAYLOAD" | jq -r ' + .tool_response.output // + .tool_response.stdout // + .tool_response.content // + empty +' 2>/dev/null || echo "") + +# Init active log if missing. +mkdir -p "$SESSIONS_DIR" +if [ ! -f "$ACTIVE_LOG" ]; then + { + echo "" + echo + } > "$ACTIVE_LOG" +fi + +NOW="$(date +"%H:%M")" +STATUS="✅" +[ "$EXIT_CODE" != "0" ] && STATUS="❌" + +case "$EVENT_KIND" in + commit) + if [ "$EXIT_CODE" = "0" ]; then + SHA="$(git rev-parse --short HEAD 2>/dev/null || echo "?")" + MSG="$(git log -1 --pretty=%s 2>/dev/null | head -c 100 || echo "")" + FILES="$(git show --stat --format= HEAD 2>/dev/null | tail -n 1 | tr -s ' ' || echo "")" + { + echo "- [${NOW}] **commit** \`${SHA}\` · ${MSG}" + [ -n "$FILES" ] && echo " - ${FILES}" + } >> "$ACTIVE_LOG" + else + echo "- [${NOW}] ❌ **commit failed** (exit ${EXIT_CODE})" >> "$ACTIVE_LOG" + fi + ;; + build|test|lint) + # Truncate command for readability. + CMD_SHORT="$(echo "$CMD" | head -c 80 | tr '\n' ' ')" + if [ "$EXIT_CODE" = "0" ]; then + echo "- [${NOW}] ${STATUS} **${EVENT_KIND}** \`${CMD_SHORT}\`" >> "$ACTIVE_LOG" + else + # Capture last 8 lines of output on failure — that's where the error usually is. + TAIL="$(echo "$OUTPUT" | tail -n 8)" + { + echo "- [${NOW}] ${STATUS} **${EVENT_KIND} FAILED** \`${CMD_SHORT}\` (exit ${EXIT_CODE})" + if [ -n "$TAIL" ]; then + echo ' ```' + echo "$TAIL" | sed 's/^/ /' + echo ' ```' + fi + } >> "$ACTIVE_LOG" + fi + ;; +esac + +exit 0 diff --git a/.brain/scripts/register-to-dev-brain.sh b/.brain/scripts/register-to-dev-brain.sh new file mode 100755 index 0000000..144a3af --- /dev/null +++ b/.brain/scripts/register-to-dev-brain.sh @@ -0,0 +1,10 @@ +#!/usr/bin/env bash +# register-to-dev-brain.sh — Register this project in Dev Brain's registry. +# Run once per project (idempotent). Updates projects.json + AGENT_README.md. +set -uo pipefail +REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" +source "$REPO_ROOT/.brain/config.sh" 2>/dev/null +bash "$HOME/Documents/Dev Brain/.scripts/register-project.sh" \ + "$PROJECT_SLUG" \ + "$PROJECT_NAME" \ + "$REPO_ROOT" diff --git a/.brain/scripts/sync-graph-to-vault.sh b/.brain/scripts/sync-graph-to-vault.sh new file mode 100755 index 0000000..cb3e821 --- /dev/null +++ b/.brain/scripts/sync-graph-to-vault.sh @@ -0,0 +1,50 @@ +#!/usr/bin/env bash +# sync-graph-to-vault.sh — Copies GRAPH_REPORT.md into the CKIS vault. +# +# Called from: +# - assemble-context.sh (SessionStart) — catch-up on every session open +# - post-commit.brain git hook — low-latency sync after each commit +# +# Wraps the report in CKIS-standard frontmatter so Obsidian indexes it +# correctly. Skips if the file is already identical to avoid mtime churn. + +set -uo pipefail + +REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" +cd "$REPO_ROOT" + +source "$REPO_ROOT/.brain/config.sh" 2>/dev/null || exit 0 + +SRC="$REPO_ROOT/$GRAPH_DIR/GRAPH_REPORT.md" +DEST_DIR="$CKIS_VAULT/02-projects/$PROJECT_SLUG" +DEST="$DEST_DIR/graph-report.md" + +[ -f "$SRC" ] || exit 0 +[ -d "$DEST_DIR" ] || exit 0 # vault not mounted on this machine — no-op + +NOW="$(date -u +"%Y-%m-%dT%H:%M:%SZ")" +TMP="$(mktemp)" + +{ + echo "---" + echo "type: graph-report" + echo "project: $PROJECT_SLUG" + echo "source: \"$SRC\"" + echo "generated: $NOW" + echo "auto: true" + echo "tags: [graph, $PROJECT_SLUG, auto-generated]" + echo "---" + echo + echo "> Auto-synced from \`.brain/graph/GRAPH_REPORT.md\` — do not hand-edit." + echo + cat "$SRC" +} > "$TMP" + +# Skip write if content is identical (avoids triggering Obsidian re-index). +if [ -f "$DEST" ] && cmp -s "$TMP" "$DEST"; then + rm -f "$TMP" + exit 0 +fi + +mv "$TMP" "$DEST" +echo "[brain] graph-report synced → $DEST" >&2 diff --git a/.brain/scripts/sync-obsidian-graph.sh b/.brain/scripts/sync-obsidian-graph.sh new file mode 100755 index 0000000..157e188 --- /dev/null +++ b/.brain/scripts/sync-obsidian-graph.sh @@ -0,0 +1,54 @@ +#!/usr/bin/env bash +# sync-obsidian-graph.sh — regenerates Graphify Obsidian notes for this project. +# +# Reads .brain/graph/graph.json and writes one .md file per code node into +# ~/Documents/Dev Brain/code-graph// using the graphify.export Python API +# (the graphify CLI's `update` command does not expose --obsidian; the flag only +# exists in the Claude skill's full pipeline, so we call the Python API directly). +# +# Called from: +# - post-commit.brain (cadence-gated: every OBSIDIAN_GRAPH_CADENCE commits) +# - Manually: bash .brain/scripts/sync-obsidian-graph.sh + +set -uo pipefail + +REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" +cd "$REPO_ROOT" + +source "$REPO_ROOT/.brain/config.sh" 2>/dev/null || exit 0 + +DEV_BRAIN_VAULT="${DEV_BRAIN_VAULT:-$HOME/Documents/Dev Brain}" +OBS_DIR="$DEV_BRAIN_VAULT/code-graph/$PROJECT_SLUG" +GRAPH_JSON="$REPO_ROOT/$GRAPH_DIR/graph.json" + +[ -d "$DEV_BRAIN_VAULT" ] || { echo "[brain] Dev Brain vault not found at $DEV_BRAIN_VAULT — skipping" >&2; exit 0; } +[ -f "$GRAPH_JSON" ] || { echo "[brain] graph.json not found — run graphify update . first" >&2; exit 0; } + +mkdir -p "$OBS_DIR" + +python3 - "$GRAPH_JSON" "$OBS_DIR" <<'PYEOF' +import sys, json, warnings +import networkx as nx +from pathlib import Path + +graph_path, obs_dir = Path(sys.argv[1]), sys.argv[2] + +with warnings.catch_warnings(): + warnings.simplefilter("ignore") + G = nx.node_link_graph(json.loads(graph_path.read_text()), edges="links") + +communities = {} +for node, data in G.nodes(data=True): + c = data.get("community", 0) + communities.setdefault(c, []).append(node) + +from graphify.export import to_obsidian +n = to_obsidian(G, communities, obs_dir) +print(f"[brain] {n} Obsidian notes written to {obs_dir}", file=sys.stderr) +PYEOF + +# ── Build Dev Brain wiki page for this project ──────────────────────────────── +BUILD_WIKI="$DEV_BRAIN_VAULT/.scripts/build-wiki-page.sh" +if [ -x "$BUILD_WIKI" ]; then + bash "$BUILD_WIKI" "$PROJECT_SLUG" 2>/dev/null || true +fi diff --git a/.brain/sessions/.gitkeep b/.brain/sessions/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/.claude/settings.json b/.claude/settings.json new file mode 100644 index 0000000..1f38971 --- /dev/null +++ b/.claude/settings.json @@ -0,0 +1,67 @@ +{ + "permissions": { + "allow": [ + "Bash(npm run build)", + "Bash(npm run dev)", + "Bash(npm run lint)", + "Bash(git *)", + "Bash(bash .brain/scripts/*)", + "Bash(graphify *)", + "Bash(bash .brain/scripts/sync-obsidian-graph.sh)" + ] + }, + "hooks": { + "SessionStart": [ + { + "hooks": [ + { + "type": "command", + "command": "bash -c 'cd \"$(git rev-parse --show-toplevel 2>/dev/null)\" && bash .brain/scripts/assemble-context.sh'" + } + ] + } + ], + "PostToolUse": [ + { + "matcher": "Bash", + "hooks": [ + { + "type": "command", + "command": "bash -c 'cd \"$(git rev-parse --show-toplevel 2>/dev/null)\" && bash .brain/scripts/log-tool-event.sh'" + } + ] + } + ], + "UserPromptSubmit": [ + { + "hooks": [ + { + "type": "command", + "command": "bash -c 'cd \"$(git rev-parse --show-toplevel 2>/dev/null)\" && bash .brain/scripts/log-compact.sh'" + } + ] + } + ], + "Stop": [ + { + "hooks": [ + { + "type": "command", + "command": "bash -c 'cd \"$(git rev-parse --show-toplevel 2>/dev/null)\" && bash .brain/scripts/log-session.sh'" + } + ] + } + ], + "PreToolUse": [ + { + "matcher": "Bash", + "hooks": [ + { + "type": "command", + "command": "CMD=$(python3 -c \"import json,sys; d=json.load(sys.stdin); print(d.get('tool_input',d).get('command',''))\" 2>/dev/null || true); case \"$CMD\" in *grep*|*rg\\ *|*ripgrep*|*find\\ *|*fd\\ *|*ack\\ *|*ag\\ *) [ -f graphify-out/graph.json ] && echo '{\"hookSpecificOutput\":{\"hookEventName\":\"PreToolUse\",\"additionalContext\":\"graphify: Knowledge graph exists. Read graphify-out/GRAPH_REPORT.md for god nodes and community structure before searching raw files.\"}}' || true ;; esac" + } + ] + } + ] + } +} diff --git a/.gitignore b/.gitignore index 61b11ee..d997f35 100644 --- a/.gitignore +++ b/.gitignore @@ -7,3 +7,15 @@ coverage/ # Private architecture inputs — stripped before public release (see README) docs/planning/ + +# .brain/ — per-project second brain (runtime artifacts only; scripts/config committed) +# decisions/, bugs/, scripts/, BRAIN.md, README.md, config.sh are versioned. +.brain/_CONTEXT.md +.brain/.session-state +.brain/.compact-triggers +.brain/sessions/* +!.brain/sessions/.gitkeep +.brain/graph/* +!.brain/graph/.gitkeep +# graphify-out is a symlink → .brain/graph (regenerable, not committed) +graphify-out From ad14415445fa6134590576009d37a89b77fdee38 Mon Sep 17 00:00:00 2001 From: Eduardo Borjas Date: Wed, 3 Jun 2026 04:16:04 -0600 Subject: [PATCH 4/6] chore(security): zero-trust npm hardening + CVE patches MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add .npmrc: ignore-scripts=true, registry pin, save-exact=true - Patch GHSA-5xrq-8626-4rwp: vitest → ^4.1.8 (critical — arbitrary file read/exec) - Pin all GitHub Actions to commit SHAs (not mutable tags) - Add --ignore-scripts to all npm ci / pnpm install steps in CI - Add explicit native module whitelist with npm_config_ignore_scripts=false rebuild - Add npm audit --audit-level=high gate to all CI and release workflows - Add permissions: {} (deny-all default) with per-job minimum grants - Add persist-credentials: false to all checkout steps - Add weekly security-audit.yml workflow (runs every Monday 09:00 UTC) - Add SECURITY.md with supply chain security policy and vulnerability disclosure Co-Authored-By: Claude Sonnet 4.6 (1M context) --- .github/workflows/ci.yml | 42 +- .github/workflows/release.yml | 42 +- .github/workflows/security-audit.yml | 30 + .npmrc | 19 + SECURITY.md | 43 +- package-lock.json | 1481 +++++++++++++++----------- package.json | 2 +- 7 files changed, 990 insertions(+), 669 deletions(-) create mode 100644 .github/workflows/security-audit.yml create mode 100644 .npmrc diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 05e0dcb..59a02a7 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -4,24 +4,52 @@ on: push: branches: [main] pull_request: + branches: [main] + +permissions: {} jobs: build: name: ${{ matrix.os }} / node ${{ matrix.node }} runs-on: ${{ matrix.os }} + permissions: + contents: read strategy: fail-fast: false matrix: os: [ubuntu-latest, macos-latest, windows-latest] node: [20, 22] steps: - - uses: actions/checkout@v4 - - uses: actions/setup-node@v4 + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + with: + persist-credentials: false + + - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 with: node-version: ${{ matrix.node }} cache: npm - - run: npm ci - - run: npm run lint - - run: npm run typecheck - - run: npm run build - - run: npm test + + - name: Install dependencies (zero-trust) + run: npm ci --ignore-scripts + + # better-sqlite3 is a direct dep that compiles native bindings. + # Whitelisted: source-available, widely audited SQLite3 wrapper. + - name: Rebuild trusted native modules + run: npm rebuild better-sqlite3 + env: + npm_config_ignore_scripts: 'false' + + - name: Dependency security audit + run: npm audit --audit-level=high + + - name: Lint + run: npm run lint + + - name: Type check + run: npm run typecheck + + - name: Build + run: npm run build + + - name: Test + run: npm test diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index e552467..78fd774 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -5,23 +5,45 @@ on: tags: - 'v*' -permissions: - contents: read - id-token: write +permissions: {} jobs: publish: runs-on: ubuntu-latest + permissions: + contents: write + id-token: write steps: - - uses: actions/checkout@v4 - - uses: actions/setup-node@v4 + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: - node-version: 20 + persist-credentials: false + + - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 + with: + node-version: '22' registry-url: https://registry.npmjs.org cache: npm - - run: npm ci - - run: npm test - - run: npm run build - - run: npm publish --provenance --access public + + - name: Install dependencies (zero-trust) + run: npm ci --ignore-scripts + + - name: Rebuild trusted native modules + run: npm rebuild better-sqlite3 + env: + npm_config_ignore_scripts: 'false' + + - name: Dependency security audit + run: npm audit --audit-level=high + + - name: Type check + Test + Build + run: npm run typecheck && npm test && npm run build + + - name: Create GitHub Release + uses: softprops/action-gh-release@3bb12739c298aeb8a4eeaf626c5b8d85266b0e65 # v2 + with: + generate_release_notes: true + + - name: Publish to npm + run: npm publish --provenance --access public --ignore-scripts env: NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} diff --git a/.github/workflows/security-audit.yml b/.github/workflows/security-audit.yml new file mode 100644 index 0000000..0bdf5d8 --- /dev/null +++ b/.github/workflows/security-audit.yml @@ -0,0 +1,30 @@ +name: Security Audit + +on: + schedule: + - cron: '0 9 * * 1' # Every Monday 09:00 UTC + workflow_dispatch: # Manual trigger + +permissions: {} + +jobs: + audit: + runs-on: ubuntu-latest + permissions: + contents: read + steps: + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + with: + persist-credentials: false + + - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 + with: + node-version: '22' + + - name: Install dependencies (zero-trust) + run: npm ci --ignore-scripts + + # Weekly audit catches moderate/low that are non-blocking in CI. + # Fail on moderate or above — gives 7 days to assess before next push. + - name: Full dependency audit + run: npm audit --audit-level=moderate diff --git a/.npmrc b/.npmrc new file mode 100644 index 0000000..7947ff5 --- /dev/null +++ b/.npmrc @@ -0,0 +1,19 @@ +# ── Security lockdown ───────────────────────────────────────────────────── +# Block lifecycle scripts (postinstall/preinstall) during npm install/ci. +# Prevents supply chain attacks from executing arbitrary code on install. +# NOTE: npm run build/test/etc. still work — this only blocks INSTALL hooks. +ignore-scripts=true + +# Pin to the official registry — prevents registry confusion / substitution attacks +registry=https://registry.npmjs.org/ + +# Save exact versions (no ^ or ~ ranges) when adding new packages +save-exact=true + +# Enforce lockfile — fail if package-lock.json is missing or out of sync +# (enforced via npm ci in CI; locally this is a reminder) +package-lock=true + +# Disable noisy output +fund=false + diff --git a/SECURITY.md b/SECURITY.md index 190eeee..8ef98d0 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -1,25 +1,38 @@ # Security Policy -## Supported versions +## Supported Versions -Magnus is pre-1.0. Security fixes target the latest released `0.x` minor version. +| Version | Supported | +|---------|-----------| +| latest | ✅ | +| < latest | ❌ — update to latest | -## Reporting a vulnerability +## Reporting a Vulnerability -Please **do not** open a public issue for security vulnerabilities. +**Do not open a public GitHub issue for security vulnerabilities.** -Email **eduardoa.borjas@gmail.com** with: +Email: eduardoa.borjas@gmail.com -- a description of the issue and its impact, -- steps to reproduce (proof of concept if possible), -- any suggested remediation. +Include: +- Description of the vulnerability +- Steps to reproduce +- Potential impact +- Any suggested fix (optional) -You can expect an acknowledgement within **5 business days**. Coordinated disclosure is -appreciated; please give a reasonable window for a fix before any public disclosure. +You will receive a response within 48 hours. If confirmed, a patch will be released within 7 days. -## Handling of secrets +## Supply Chain Security -Magnus stores secrets (e.g. `APIFY_TOKEN`) in `~/.config/magnus/.env` with mode `600` -and **never** logs them or includes them in `--json` output. If you find a path that -leaks a secret into logs, stdout, error messages, or the spend ledger, treat it as a -security issue and report it privately. +This project implements zero-trust npm security: + +- **`ignore-scripts=true`** in `.npmrc` — blocks all postinstall/preinstall lifecycle scripts during `npm install`/`npm ci`. Prevents supply chain attacks via compromised transitive dependencies. +- **Explicit native module whitelist** — only named, reviewed native modules (listed in CI) are allowed to compile. All others are blocked. +- **Pinned GitHub Actions** — all Actions are pinned to a specific commit SHA, not a mutable tag. This prevents compromised Action tags from injecting malicious steps. +- **`npm publish --provenance`** — every published release includes a signed SLSA attestation linking the package to the exact GitHub Actions run that built it. Verify with: `npm audit signatures @` +- **`npm ci` in all CI jobs** — never `npm install`. Enforces exact cryptographic hash matching against `package-lock.json`. +- **Minimum permissions** — each CI job declares only the permissions it needs. Default is `permissions: {}` (deny all). +- **Weekly automated audit** — the Security Audit workflow runs every Monday at 09:00 UTC and fails on any moderate or higher vulnerability. + +## Known Mitigations + +Any known vulnerability mitigations (e.g., transitive dependency overrides) are documented in the relevant CI workflow files with inline comments. diff --git a/package-lock.json b/package-lock.json index 220a7b9..9cc8603 100644 --- a/package-lock.json +++ b/package-lock.json @@ -32,7 +32,7 @@ "tsup": "^8.2.4", "typescript": "^5.5.4", "typescript-eslint": "^8.5.0", - "vitest": "^2.1.1" + "vitest": "^4.1.8" }, "engines": { "node": ">=20" @@ -360,6 +360,40 @@ "node": ">=16.0.0" } }, + "node_modules/@emnapi/core": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.10.0.tgz", + "integrity": "sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/wasi-threads": "1.2.1", + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/runtime": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.10.0.tgz", + "integrity": "sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/wasi-threads": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.1.tgz", + "integrity": "sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, "node_modules/@esbuild/aix-ppc64": { "version": "0.27.7", "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.7.tgz", @@ -1221,6 +1255,25 @@ } } }, + "node_modules/@napi-rs/wasm-runtime": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.4.tgz", + "integrity": "sha512-3NQNNgA1YSlJb/kMH1ildASP9HW7/7kYnRI2szWJaofaS1hWmbGI4H+d3+22aGzXXN9IJ+n+GiFVcGipJP18ow==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@tybys/wasm-util": "^0.10.1" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + }, + "peerDependencies": { + "@emnapi/core": "^1.7.1", + "@emnapi/runtime": "^1.7.1" + } + }, "node_modules/@nodelib/fs.scandir": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", @@ -1259,6 +1312,280 @@ "node": ">= 8" } }, + "node_modules/@oxc-project/types": { + "version": "0.133.0", + "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.133.0.tgz", + "integrity": "sha512-KzkdCd6Uxqnf6l3HOw1xfatAlUURA0g14cvBYFyJ5SaNOQbOUvBr9PKArcPcrNIeRsBdgcUzOGrhKveVpvOIGA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/Boshen" + } + }, + "node_modules/@rolldown/binding-android-arm64": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.3.tgz", + "integrity": "sha512-454rs7jHngixp/NMxd5srYD57OnzSlZ/eFTETjORQHLwJG1lRtmNOJcBerZlfu4GjKqeq8aCCIQrMdHyhI51Hw==", + "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.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.0.3.tgz", + "integrity": "sha512-PcAhP+ynjURNyy8SKGl5DQP94aGuB/7JrXJb/t7P+hanXvQVMWzUvRRhBAcg/lNRadBhoUPqSoP4xw5tR/KBEA==", + "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.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.0.3.tgz", + "integrity": "sha512-9YpfeUvSE2RS7wysJ81uOZkXJz7f7Q55H2Gvp3VEw/EsahqDtrphrZ0EwDLK5vvKOzaCrBsjF8JmnMLcUt78Gg==", + "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.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.0.3.tgz", + "integrity": "sha512-yB1IlAsSNHncV6SCTL27/MVGR5htvQsoGxIv5KMGXALp+Ll1wYsn+x98M9MW7qa+NdSbvrrY7ANI4wLJ0n1e6g==", + "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.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.0.3.tgz", + "integrity": "sha512-Yi30IVAAfLUCy2MseFjbB1jAMDl1VMCAas5StnYp8da9+CKvMd2H2cbEjWcw5NPaPqzvYkVIaF1nNUG+b7u/sw==", + "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.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.0.3.tgz", + "integrity": "sha512-jsO7R8To+AdlYgUmN5sHSCZbfhtMBkO0WUx8iORQnPcMMdgr7qM2DQmMwgabs3GhNztdmoKkMKQFHD6DTMCIQw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-musl": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.0.3.tgz", + "integrity": "sha512-VWkUHwWriDciit80wleYwKILoR/KMvxh/IdwS/paX+ZgpuRpCrKLUdadJbc0NpBEiyhpYawsJ73j9aCvOH+f7Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-ppc64-gnu": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.0.3.tgz", + "integrity": "sha512-5f1laC0SlIR0yDbFCd8acUhvJIag6N3zC5P7oUPN6wX0aOma+uKJ0wBDH5aq7I1PVI2ttTlhJwzwRIBnLiSGEg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-s390x-gnu": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.0.3.tgz", + "integrity": "sha512-Iq4ko0r4XsgbrF/LunNgHtAGLRRVE2kXonAXQ/MV0mC6jQpMOhW1SvtZja2EhC/kd05++bP78dsqBeIQyYJ6Yg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-gnu": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.0.3.tgz", + "integrity": "sha512-B8m6tD5+/N5FeNQFbKlLA/2yVq9ycQP1SeedyEYYKWBNR3ZQbkvIUcNnDNM03lO1l5F2roiiFJGgvoLLyZXtSg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-musl": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.0.3.tgz", + "integrity": "sha512-pSdpdUJHkuCxun9LE7jvgUB9qsRgaiyNNCX7m/AvHTcq67AiT/Yhoxvw5zPfhrM8k/BfP8ce/hMOpthKDpEUow==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-openharmony-arm64": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.0.3.tgz", + "integrity": "sha512-OXXS3RKJgX2uLwM+gYyuH5omcH8fL1LJs96pZGgtetVCahON57+d4SJHzTgZiOjxgGkSnpXpOsWuPDGAKAigEg==", + "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.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.0.3.tgz", + "integrity": "sha512-JTtb8BWFynicNSoPrehsCzBtOKjZ6jhMiPFEmOiuXg1Fl8dn2KHQob+GuPSGR0dryQa1PQJbzjF3dqO/whhjLg==", + "cpu": [ + "wasm32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "1.10.0", + "@emnapi/runtime": "1.10.0", + "@napi-rs/wasm-runtime": "^1.1.4" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-win32-arm64-msvc": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.3.tgz", + "integrity": "sha512-gEdFFEN70A/jxb2svrWsN3aDL7OUtmvlOy+6fa2jxG8K0wQ1ZbdeLGnidov6Yu5/733dI5ySfzFlQ/cb0bSz1g==", + "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.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.0.3.tgz", + "integrity": "sha512-eXB7CHuaQdqmJcc3koCNtNPmT/bj2gc999kUFgBxG8Ac0NdgXc4rkCHhqrgrhN3zddvvvrgzj1e90SuSfmyIXA==", + "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/@rollup/rollup-android-arm-eabi": { "version": "4.60.4", "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.60.4.tgz", @@ -1639,12 +1966,30 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "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/@tootallnate/quickjs-emscripten": { "version": "0.23.0", "resolved": "https://registry.npmjs.org/@tootallnate/quickjs-emscripten/-/quickjs-emscripten-0.23.0.tgz", "integrity": "sha512-C5Mc6rdnsaJDjO3UpGW/CQTHtCKaYlScZTly4JIu97Jxo/odCiH0ITnDXSJPTOrEKk/ycSZ0AOgTmkDtkOsvIA==", "license": "MIT" }, + "node_modules/@tybys/wasm-util": { + "version": "0.10.2", + "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.2.tgz", + "integrity": "sha512-RoBvJ2X0wuKlWFIjrwffGw1IqZHKQqzIchKaadZZfnNpsAYp2mM0h36JtPCjNDAHGgYez/15uMBpfGwchhiMgg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, "node_modules/@types/better-sqlite3": { "version": "7.6.13", "resolved": "https://registry.npmjs.org/@types/better-sqlite3/-/better-sqlite3-7.6.13.tgz", @@ -1655,6 +2000,24 @@ "@types/node": "*" } }, + "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", @@ -1962,38 +2325,40 @@ } }, "node_modules/@vitest/expect": { - "version": "2.1.9", - "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-2.1.9.tgz", - "integrity": "sha512-UJCIkTBenHeKT1TTlKMJWy1laZewsRIzYighyYiJKZreqtdxSos/S1t+ktRMQWu2CKqaarrkeszJx1cgC5tGZw==", + "version": "4.1.8", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.1.8.tgz", + "integrity": "sha512-h3nDO677RDLEGlBxyQ5CW8RlMThSKSRLUePLOx09gNIWRL40edgA1GCZSZgf1W55MFAG6/Sw14KeaAnqv0NKdQ==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/spy": "2.1.9", - "@vitest/utils": "2.1.9", - "chai": "^5.1.2", - "tinyrainbow": "^1.2.0" + "@standard-schema/spec": "^1.1.0", + "@types/chai": "^5.2.2", + "@vitest/spy": "4.1.8", + "@vitest/utils": "4.1.8", + "chai": "^6.2.2", + "tinyrainbow": "^3.1.0" }, "funding": { "url": "https://opencollective.com/vitest" } }, "node_modules/@vitest/mocker": { - "version": "2.1.9", - "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-2.1.9.tgz", - "integrity": "sha512-tVL6uJgoUdi6icpxmdrn5YNo3g3Dxv+IHJBr0GXHaEdTcw3F+cPKnsXFhli6nO+f/6SDKPHEK1UN+k+TQv0Ehg==", + "version": "4.1.8", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.1.8.tgz", + "integrity": "sha512-LEiN/xe4OSIbKe9HQIp5OC24agGD9J5CnmMgsLohVVoOPWL9a2sBoR6VBx43jQZb7Kr1l4RCuyCJzcAa0+dojw==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/spy": "2.1.9", + "@vitest/spy": "4.1.8", "estree-walker": "^3.0.3", - "magic-string": "^0.30.12" + "magic-string": "^0.30.21" }, "funding": { "url": "https://opencollective.com/vitest" }, "peerDependencies": { "msw": "^2.4.9", - "vite": "^5.0.0" + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" }, "peerDependenciesMeta": { "msw": { @@ -2005,84 +2370,68 @@ } }, "node_modules/@vitest/pretty-format": { - "version": "2.1.9", - "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-2.1.9.tgz", - "integrity": "sha512-KhRIdGV2U9HOUzxfiHmY8IFHTdqtOhIzCpd8WRdJiE7D/HUcZVD0EgQCVjm+Q9gkUXWgBvMmTtZgIG48wq7sOQ==", + "version": "4.1.8", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.1.8.tgz", + "integrity": "sha512-9GasEBxpZ1VYIpqHf/0+YGg121uSNwCKOJqIrTwWP/TB7DmFCiaBpNl3aPZzoLWfWkuqhbH8vJIVobZkvdo2cA==", "dev": true, "license": "MIT", "dependencies": { - "tinyrainbow": "^1.2.0" + "tinyrainbow": "^3.1.0" }, "funding": { "url": "https://opencollective.com/vitest" } }, "node_modules/@vitest/runner": { - "version": "2.1.9", - "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-2.1.9.tgz", - "integrity": "sha512-ZXSSqTFIrzduD63btIfEyOmNcBmQvgOVsPNPe0jYtESiXkhd8u2erDLnMxmGrDCwHCCHE7hxwRDCT3pt0esT4g==", + "version": "4.1.8", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.1.8.tgz", + "integrity": "sha512-EmVxeBAfMJvycdjd6Hm+RbFBbA9fKvo0Kx37hNpBYoYeavH3RNsBXWDooR1mgD52dCrxIIuP7UotpfiwOikvcg==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/utils": "2.1.9", - "pathe": "^1.1.2" + "@vitest/utils": "4.1.8", + "pathe": "^2.0.3" }, "funding": { "url": "https://opencollective.com/vitest" } }, - "node_modules/@vitest/runner/node_modules/pathe": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/pathe/-/pathe-1.1.2.tgz", - "integrity": "sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==", - "dev": true, - "license": "MIT" - }, "node_modules/@vitest/snapshot": { - "version": "2.1.9", - "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-2.1.9.tgz", - "integrity": "sha512-oBO82rEjsxLNJincVhLhaxxZdEtV0EFHMK5Kmx5sJ6H9L183dHECjiefOAdnqpIgT5eZwT04PoggUnW88vOBNQ==", + "version": "4.1.8", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.1.8.tgz", + "integrity": "sha512-acfZboRmAIf05DEKcBQy33VXojFJjtUdLyo7oOmV9kebb2xdU01UknNiPuPZoJZQyO7DF0gZdTGTpeAzET9QPQ==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/pretty-format": "2.1.9", - "magic-string": "^0.30.12", - "pathe": "^1.1.2" + "@vitest/pretty-format": "4.1.8", + "@vitest/utils": "4.1.8", + "magic-string": "^0.30.21", + "pathe": "^2.0.3" }, "funding": { "url": "https://opencollective.com/vitest" } }, - "node_modules/@vitest/snapshot/node_modules/pathe": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/pathe/-/pathe-1.1.2.tgz", - "integrity": "sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==", - "dev": true, - "license": "MIT" - }, "node_modules/@vitest/spy": { - "version": "2.1.9", - "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-2.1.9.tgz", - "integrity": "sha512-E1B35FwzXXTs9FHNK6bDszs7mtydNi5MIfUWpceJ8Xbfb1gBMscAnwLbEu+B44ed6W3XjL9/ehLPHR1fkf1KLQ==", + "version": "4.1.8", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.1.8.tgz", + "integrity": "sha512-6EevtBp6OZOPF7bmz36HrGMeP3txgVSrgebWxHOafDXGkhIzfXK14f8KF6MuFfgXXUeHxmpD3BQxkV00/3s5mA==", "dev": true, "license": "MIT", - "dependencies": { - "tinyspy": "^3.0.2" - }, "funding": { "url": "https://opencollective.com/vitest" } }, "node_modules/@vitest/utils": { - "version": "2.1.9", - "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-2.1.9.tgz", - "integrity": "sha512-v0psaMSkNJ3A2NMrUEHFRzJtDPFn+/VWZ5WxImB21T9fjucJRmS7xCS3ppEnARb9y11OAzaD+P2Ps+b+BGX5iQ==", + "version": "4.1.8", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.1.8.tgz", + "integrity": "sha512-uOJamYALNhfJ6iolExyQM40yIQwDqYnkKtQ5VCiSe17E33H0aQ/u+1GlRuz4LZBk6Mm3sg90G9hEbmEt37C1Zg==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/pretty-format": "2.1.9", - "loupe": "^3.1.2", - "tinyrainbow": "^1.2.0" + "@vitest/pretty-format": "4.1.8", + "convert-source-map": "^2.0.0", + "tinyrainbow": "^3.1.0" }, "funding": { "url": "https://opencollective.com/vitest" @@ -2523,18 +2872,11 @@ } }, "node_modules/chai": { - "version": "5.3.3", - "resolved": "https://registry.npmjs.org/chai/-/chai-5.3.3.tgz", - "integrity": "sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw==", + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/chai/-/chai-6.2.2.tgz", + "integrity": "sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==", "dev": true, "license": "MIT", - "dependencies": { - "assertion-error": "^2.0.1", - "check-error": "^2.1.1", - "deep-eql": "^5.0.1", - "loupe": "^3.1.0", - "pathval": "^2.0.0" - }, "engines": { "node": ">=18" } @@ -2563,16 +2905,6 @@ "dev": true, "license": "MIT" }, - "node_modules/check-error": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/check-error/-/check-error-2.1.3.tgz", - "integrity": "sha512-PAJdDJusoxnwm1VwW07VWwUN1sl7smmC3OKggvndJFadxxDRyFJBX/ggnu/KE4kQAB7a3Dp8f/YXC1FlUprWmA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 16" - } - }, "node_modules/chokidar": { "version": "4.0.3", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz", @@ -2682,6 +3014,13 @@ "node": ">= 0.6" } }, + "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/cookie": { "version": "0.7.2", "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", @@ -2772,16 +3111,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/deep-eql": { - "version": "5.0.2", - "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-5.0.2.tgz", - "integrity": "sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, "node_modules/deep-extend": { "version": "0.6.0", "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", @@ -2960,9 +3289,9 @@ } }, "node_modules/es-module-lexer": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz", - "integrity": "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==", + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.1.0.tgz", + "integrity": "sha512-n27zTYMjYu1aj4MjCWzSP7G9r75utsaoc8m61weK+W8JMBGGQybd43GstCXZ3WNmSFtGT9wi59qQTW6mhTR5LQ==", "dev": true, "license": "MIT" }, @@ -4382,6 +4711,267 @@ "node": ">= 0.8.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, + "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, + "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, + "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, + "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/lilconfig": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz", @@ -4446,13 +5036,6 @@ "dev": true, "license": "MIT" }, - "node_modules/loupe": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/loupe/-/loupe-3.2.1.tgz", - "integrity": "sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ==", - "dev": true, - "license": "MIT" - }, "node_modules/lru-cache": { "version": "7.18.3", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.18.3.tgz", @@ -4743,6 +5326,17 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/obug": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/obug/-/obug-2.1.1.tgz", + "integrity": "sha512-uTqF9MuPraAQ+IsnPf366RG4cP9RtUi7MLO1N3KEc+wb0a6yKpeL0lmk2IB1jY5KHPAlTc6T/JRdC/YqxHNwkQ==", + "dev": true, + "funding": [ + "https://github.com/sponsors/sxzz", + "https://opencollective.com/debug" + ], + "license": "MIT" + }, "node_modules/on-finished": { "version": "2.4.1", "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", @@ -5014,16 +5608,6 @@ "dev": true, "license": "MIT" }, - "node_modules/pathval": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/pathval/-/pathval-2.0.1.tgz", - "integrity": "sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 14.16" - } - }, "node_modules/picocolors": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", @@ -5521,6 +6105,40 @@ "node": ">=0.10.0" } }, + "node_modules/rolldown": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.3.tgz", + "integrity": "sha512-i00lAJ2ks1BYr7rjNjKC7BcqAS7nVfiT3QX1SI5aY+AFHblCmaUf9OE9dbdzDvW6dJxbi2ZCZiy9v3CcwOiX3g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@oxc-project/types": "=0.133.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.0.3", + "@rolldown/binding-darwin-arm64": "1.0.3", + "@rolldown/binding-darwin-x64": "1.0.3", + "@rolldown/binding-freebsd-x64": "1.0.3", + "@rolldown/binding-linux-arm-gnueabihf": "1.0.3", + "@rolldown/binding-linux-arm64-gnu": "1.0.3", + "@rolldown/binding-linux-arm64-musl": "1.0.3", + "@rolldown/binding-linux-ppc64-gnu": "1.0.3", + "@rolldown/binding-linux-s390x-gnu": "1.0.3", + "@rolldown/binding-linux-x64-gnu": "1.0.3", + "@rolldown/binding-linux-x64-musl": "1.0.3", + "@rolldown/binding-openharmony-arm64": "1.0.3", + "@rolldown/binding-wasm32-wasi": "1.0.3", + "@rolldown/binding-win32-arm64-msvc": "1.0.3", + "@rolldown/binding-win32-x64-msvc": "1.0.3" + } + }, "node_modules/rollup": { "version": "4.60.4", "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.60.4.tgz", @@ -5971,9 +6589,9 @@ } }, "node_modules/std-env": { - "version": "3.10.0", - "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.10.0.tgz", - "integrity": "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==", + "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" }, @@ -6206,30 +6824,10 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, - "node_modules/tinypool": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-1.1.1.tgz", - "integrity": "sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^18.0.0 || >=20.0.0" - } - }, "node_modules/tinyrainbow": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-1.2.0.tgz", - "integrity": "sha512-weEDEq7Z5eTHPDh4xjX789+fHfF+P8boiFB+0vbWzpbnbsEr/GRaohi/uMKxg8RZMXnl1ItAi/IUHWMsjDV7kQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/tinyspy": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-3.0.2.tgz", - "integrity": "sha512-n1cw8k1k0x4pgA2+9XrOkFydTerNcJ1zWCO5Nn9scWHTD+5tp8dghT2x1uduQePZTZgd3Tupf+x9BxJjeJi77Q==", + "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": { @@ -6543,21 +7141,23 @@ } }, "node_modules/vite": { - "version": "5.4.21", - "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.21.tgz", - "integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==", + "version": "8.0.16", + "resolved": "https://registry.npmjs.org/vite/-/vite-8.0.16.tgz", + "integrity": "sha512-h9bXPmJichP5fLmVQo3PyaGSDE2n3aPuomeAlVRm0JLmt4rY6zmPKd59HYI4LNW8oTK7tlTsuC7l/m7awx9Jcw==", "dev": true, "license": "MIT", "dependencies": { - "esbuild": "^0.21.3", - "postcss": "^8.4.43", - "rollup": "^4.20.0" + "lightningcss": "^1.32.0", + "picomatch": "^4.0.4", + "postcss": "^8.5.15", + "rolldown": "1.0.3", + "tinyglobby": "^0.2.17" }, "bin": { "vite": "bin/vite.js" }, "engines": { - "node": "^18.0.0 || >=20.0.0" + "node": "^20.19.0 || >=22.12.0" }, "funding": { "url": "https://github.com/vitejs/vite?sponsor=1" @@ -6566,23 +7166,33 @@ "fsevents": "~2.3.3" }, "peerDependencies": { - "@types/node": "^18.0.0 || >=20.0.0", - "less": "*", - "lightningcss": "^1.21.0", - "sass": "*", - "sass-embedded": "*", - "stylus": "*", - "sugarss": "*", - "terser": "^5.4.0" + "@types/node": "^20.19.0 || >=22.12.0", + "@vitejs/devtools": "^0.1.18", + "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 }, - "less": { + "@vitejs/devtools": { "optional": true }, - "lightningcss": { + "esbuild": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { "optional": true }, "sass": { @@ -6599,522 +7209,102 @@ }, "terser": { "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true } } }, - "node_modules/vite-node": { - "version": "2.1.9", - "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-2.1.9.tgz", - "integrity": "sha512-AM9aQ/IPrW/6ENLQg3AGY4K1N2TGZdR5e4gu/MmmR2xR3Ll1+dib+nook92g4TV3PXVyeyxdWwtaCAiUL0hMxA==", + "node_modules/vite/node_modules/picomatch": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", "dev": true, "license": "MIT", - "dependencies": { - "cac": "^6.7.14", - "debug": "^4.3.7", - "es-module-lexer": "^1.5.4", - "pathe": "^1.1.2", - "vite": "^5.0.0" - }, - "bin": { - "vite-node": "vite-node.mjs" - }, - "engines": { - "node": "^18.0.0 || >=20.0.0" - }, - "funding": { - "url": "https://opencollective.com/vitest" - } - }, - "node_modules/vite-node/node_modules/pathe": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/pathe/-/pathe-1.1.2.tgz", - "integrity": "sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/vite/node_modules/@esbuild/aix-ppc64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", - "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==", - "cpu": [ - "ppc64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "aix" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/vite/node_modules/@esbuild/android-arm": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz", - "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/vite/node_modules/@esbuild/android-arm64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz", - "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/vite/node_modules/@esbuild/android-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz", - "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/vite/node_modules/@esbuild/darwin-arm64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz", - "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/vite/node_modules/@esbuild/darwin-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz", - "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/vite/node_modules/@esbuild/freebsd-arm64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz", - "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/vite/node_modules/@esbuild/freebsd-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz", - "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/vite/node_modules/@esbuild/linux-arm": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz", - "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/vite/node_modules/@esbuild/linux-arm64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz", - "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/vite/node_modules/@esbuild/linux-ia32": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz", - "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==", - "cpu": [ - "ia32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/vite/node_modules/@esbuild/linux-loong64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz", - "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==", - "cpu": [ - "loong64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/vite/node_modules/@esbuild/linux-mips64el": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz", - "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==", - "cpu": [ - "mips64el" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/vite/node_modules/@esbuild/linux-ppc64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz", - "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==", - "cpu": [ - "ppc64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/vite/node_modules/@esbuild/linux-riscv64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz", - "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==", - "cpu": [ - "riscv64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/vite/node_modules/@esbuild/linux-s390x": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz", - "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==", - "cpu": [ - "s390x" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/vite/node_modules/@esbuild/linux-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz", - "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/vite/node_modules/@esbuild/netbsd-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz", - "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "netbsd" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/vite/node_modules/@esbuild/openbsd-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz", - "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openbsd" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/vite/node_modules/@esbuild/sunos-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz", - "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "sunos" - ], "engines": { "node": ">=12" - } - }, - "node_modules/vite/node_modules/@esbuild/win32-arm64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz", - "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/vite/node_modules/@esbuild/win32-ia32": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz", - "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==", - "cpu": [ - "ia32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/vite/node_modules/@esbuild/win32-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz", - "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/vite/node_modules/esbuild": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz", - "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==", - "dev": true, - "hasInstallScript": true, - "license": "MIT", - "bin": { - "esbuild": "bin/esbuild" }, - "engines": { - "node": ">=12" - }, - "optionalDependencies": { - "@esbuild/aix-ppc64": "0.21.5", - "@esbuild/android-arm": "0.21.5", - "@esbuild/android-arm64": "0.21.5", - "@esbuild/android-x64": "0.21.5", - "@esbuild/darwin-arm64": "0.21.5", - "@esbuild/darwin-x64": "0.21.5", - "@esbuild/freebsd-arm64": "0.21.5", - "@esbuild/freebsd-x64": "0.21.5", - "@esbuild/linux-arm": "0.21.5", - "@esbuild/linux-arm64": "0.21.5", - "@esbuild/linux-ia32": "0.21.5", - "@esbuild/linux-loong64": "0.21.5", - "@esbuild/linux-mips64el": "0.21.5", - "@esbuild/linux-ppc64": "0.21.5", - "@esbuild/linux-riscv64": "0.21.5", - "@esbuild/linux-s390x": "0.21.5", - "@esbuild/linux-x64": "0.21.5", - "@esbuild/netbsd-x64": "0.21.5", - "@esbuild/openbsd-x64": "0.21.5", - "@esbuild/sunos-x64": "0.21.5", - "@esbuild/win32-arm64": "0.21.5", - "@esbuild/win32-ia32": "0.21.5", - "@esbuild/win32-x64": "0.21.5" + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" } }, "node_modules/vitest": { - "version": "2.1.9", - "resolved": "https://registry.npmjs.org/vitest/-/vitest-2.1.9.tgz", - "integrity": "sha512-MSmPM9REYqDGBI8439mA4mWhV5sKmDlBKWIYbA3lRb2PTHACE0mgKwA8yQ2xq9vxDTuk4iPrECBAEW2aoFXY0Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "@vitest/expect": "2.1.9", - "@vitest/mocker": "2.1.9", - "@vitest/pretty-format": "^2.1.9", - "@vitest/runner": "2.1.9", - "@vitest/snapshot": "2.1.9", - "@vitest/spy": "2.1.9", - "@vitest/utils": "2.1.9", - "chai": "^5.1.2", - "debug": "^4.3.7", - "expect-type": "^1.1.0", - "magic-string": "^0.30.12", - "pathe": "^1.1.2", - "std-env": "^3.8.0", + "version": "4.1.8", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-4.1.8.tgz", + "integrity": "sha512-flY6ScbCIt9HThs+C5HS7jvGOB560DJtk/Z15IQROTA6zEy49Nh8T/dofWTQL+n3vswqn87sbJNiuqw1SDp5Ig==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/expect": "4.1.8", + "@vitest/mocker": "4.1.8", + "@vitest/pretty-format": "4.1.8", + "@vitest/runner": "4.1.8", + "@vitest/snapshot": "4.1.8", + "@vitest/spy": "4.1.8", + "@vitest/utils": "4.1.8", + "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": "^0.3.1", - "tinypool": "^1.0.1", - "tinyrainbow": "^1.2.0", - "vite": "^5.0.0", - "vite-node": "2.1.9", + "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": "^18.0.0 || >=20.0.0" + "node": "^20.0.0 || ^22.0.0 || >=24.0.0" }, "funding": { "url": "https://opencollective.com/vitest" }, "peerDependencies": { "@edge-runtime/vm": "*", - "@types/node": "^18.0.0 || >=20.0.0", - "@vitest/browser": "2.1.9", - "@vitest/ui": "2.1.9", + "@opentelemetry/api": "^1.9.0", + "@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0", + "@vitest/browser-playwright": "4.1.8", + "@vitest/browser-preview": "4.1.8", + "@vitest/browser-webdriverio": "4.1.8", + "@vitest/coverage-istanbul": "4.1.8", + "@vitest/coverage-v8": "4.1.8", + "@vitest/ui": "4.1.8", "happy-dom": "*", - "jsdom": "*" + "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": { + "@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": { @@ -7125,15 +7315,34 @@ }, "jsdom": { "optional": true + }, + "vite": { + "optional": false } } }, - "node_modules/vitest/node_modules/pathe": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/pathe/-/pathe-1.1.2.tgz", - "integrity": "sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==", + "node_modules/vitest/node_modules/picomatch": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", "dev": true, - "license": "MIT" + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/vitest/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/which": { "version": "2.0.2", diff --git a/package.json b/package.json index 39e51f8..b401642 100644 --- a/package.json +++ b/package.json @@ -77,6 +77,6 @@ "tsup": "^8.2.4", "typescript": "^5.5.4", "typescript-eslint": "^8.5.0", - "vitest": "^2.1.1" + "vitest": "^4.1.8" } } From 9ebe0ecf47176c002f2bde89e719af998e418a19 Mon Sep 17 00:00:00 2001 From: Eduardo Borjas Date: Wed, 3 Jun 2026 09:46:17 -0600 Subject: [PATCH 5/6] =?UTF-8?q?feat:=20v1.0.0=20=E2=80=94=20capture=20--wa?= =?UTF-8?q?tch=20(recmp3),=20scoped=20npm=20publish,=20ADRs?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Track C — `magnus capture --watch`: real-time system-audio capture via recmp3. Records the PulseAudio .monitor source, transcribes with Groq Whisper ($0 Apify), and files one CKIS Type-B note with transcript_source: recmp3-whisper into /00-inbox/. Interactive ENTER-to-stop or headless --duration; monitor source auto-detect with --source / RECMP3_SOURCE / config override. - Rename package to @aedneth/magnus-cli (both unscoped names taken on npm); binary stays `magnus`. First public npm release. - Fix package.json homepage/repository/bugs URLs (eduardoborjas -> aedneth). - Add capture.recmp3Source + capture.inboxSubdir config; add recmp3-whisper to the TranscriptSource union. - Record 6 architecture decisions as ADRs in .brain/decisions/. - Bump 0.1.0 -> 1.0.0; update CHANGELOG, README (install/quickstart/roadmap). Tests: 52 passing (12 new for watch). Build/lint/typecheck/audit clean. Co-Authored-By: Claude Opus 4.8 --- .brain/OPUS_BOOTSTRAP.md | 288 ++++++++++++++++++ .brain/decisions/2026-06-03-config-surface.md | 37 +++ ...026-06-03-language-single-stack-node-ts.md | 37 +++ .../decisions/2026-06-03-milestone-scope.md | 40 +++ .../2026-06-03-no-ink-tui-streaming-stdout.md | 42 +++ .../2026-06-03-package-name-scoped.md | 36 +++ .brain/decisions/2026-06-03-watch-spec.md | 49 +++ CHANGELOG.md | 30 +- README.md | 36 ++- package-lock.json | 4 +- package.json | 13 +- src/capture/render/frontmatter.ts | 2 +- src/capture/run/watch.ts | 283 +++++++++++++++++ src/commands/capture.ts | 33 +- src/config/schema.ts | 4 + src/version.ts | 2 +- test/watch.test.ts | 168 ++++++++++ 17 files changed, 1085 insertions(+), 19 deletions(-) create mode 100644 .brain/OPUS_BOOTSTRAP.md create mode 100644 .brain/decisions/2026-06-03-config-surface.md create mode 100644 .brain/decisions/2026-06-03-language-single-stack-node-ts.md create mode 100644 .brain/decisions/2026-06-03-milestone-scope.md create mode 100644 .brain/decisions/2026-06-03-no-ink-tui-streaming-stdout.md create mode 100644 .brain/decisions/2026-06-03-package-name-scoped.md create mode 100644 .brain/decisions/2026-06-03-watch-spec.md create mode 100644 src/capture/run/watch.ts create mode 100644 test/watch.test.ts diff --git a/.brain/OPUS_BOOTSTRAP.md b/.brain/OPUS_BOOTSTRAP.md new file mode 100644 index 0000000..4f3c777 --- /dev/null +++ b/.brain/OPUS_BOOTSTRAP.md @@ -0,0 +1,288 @@ +--- +type: opus-bootstrap +project: magnus-cli +created: 2026-06-03 +status: active +--- + +# OPUS ORCHESTRATOR BOOTSTRAP — magnus-cli + +> Paste this as your first message after `claude --resume ` in the magnus-cli tab. +> Model: `claude-opus-4-8` (set via `/model claude-opus-4-8` after resume). + +--- + +## YOUR ROLE + +You are the **Opus 4.8 orchestrator** for magnus-cli. Your mission: take this project from its scaffolded **v0.1.0** to a working, published **v1.0.0** — the universal media downloader + CKIS social capture engine. + +This is a two-track project: +- **Track A (Download MVP):** Single-URL yt-dlp wrapper that works today, no external service dependency +- **Track B (Capture Pipeline):** Bulk social ingestion via Apify — requires external setup (has blockers), implement after Track A ships + +You will orchestrate to v1.0.0 = Track A complete + Track B architecturally complete + npm published. + +--- + +## STEP 1 — LOAD CONTEXT FIRST + +```bash +cat .brain/_CONTEXT.md +cat graphify-out/GRAPH_REPORT.md +cat ~/Documents/Second\ Brain/02-projects/magnus-cli/_overview.md +cat package.json +find src/ -name "*.ts" | sort +``` + +Also read these source files before planning (they contain the critical implementation specs): +```bash +cat ~/Documents/Second\ Brain/04-resources/tools/yt-dlp-universal-downloader.md +cat ~/Documents/Second\ Brain/00-systems/workflows/ckis-social-captures-pipeline-master-plan.md +``` + +Key facts (ground truth): +- **Current version:** v0.1.0 +- **Stack:** Node.js 20 ESM + TypeScript 5.5 + Ink v5 + Commander v13 + SQLite (better-sqlite3) +- **Core god nodes:** `runYouTube()` (19), `runInstagram()` (18), `runTikTok()` (18), `MagnusError` (18), `openDb()` (12), `Logger` (11), `resolvePath()` (10), `planDownload()` (9), `renderNote()` (9) +- **What exists:** Full architecture scaffold — platform handlers, SQLite queue, capture pipeline wiring, config/secrets store, Apify client integration points +- **License:** AGPL-3.0 + Dual Commercial (FINAL) +- **Open decisions to resolve in ultraplan:** See CKIS overview Step 2 below + +--- + +## STEP 2 — ULTRAPLAN TARGET (v1.0.0) + +### OPEN DECISIONS — RESOLVE THESE FIRST (before any code) + +Read the open decisions from the CKIS overview and resolve them: + +1. **Language for capture scripts:** Resolve to **Node.js/TypeScript** (single-stack, avoids Python dep, consistent with CLI). `runInstagram()` and `runTikTok()` are already TypeScript god nodes — stay in-stack. + +2. **Config surface:** Resolve to **`~/.config/magnus/config.json` + `~/.config/magnus/.env`** — separate concerns (paths/prefs vs secrets). Mirror `loadConfig()` + `loadSecrets()` from `store.ts` (already wired per graph). + +3. **TUI framework:** Resolve to **Ink v5 with `ora` spinners for battery operations** — Ink is already in the scaffold. For capture mode (long-running), use `ora` + `cli-progress`. For single URL download, simple Ink progress line. + +4. **Package name:** Resolve to **`magnus-cli`** (global) — consistent with the suite naming pattern. Binary: `magnus`. + +5. **First milestone scope:** Resolve to **Track A (single URL download) first** — this ships as v0.2.0. Track B (capture) ships as v0.3.0–v1.0.0. + +6. **`magnus capture --watch` spec:** Implement as documented in [[recmp3-social-capture-pipeline]] — detect monitor source, record on play, stop at reel end. This is v1.0.0 scope (after basic capture works). + +Write resolved decisions as ADR files in `.brain/decisions/` before generating the sprint. + +### Track A — Single URL Download (v0.2.0) + +The `planDownload()` god node is the core — implement it fully: +- `magnus ` — auto-detect platform from URL (CRITICAL: `music.youtube.com` must match before `youtube.com`) +- Platform detection: YouTube, YouTube Music, Instagram, TikTok, Twitter/X, Twitch, Bandcamp, SoundCloud, generic +- Audio mode (music platforms): `yt-dlp -x --audio-format mp3 --embed-metadata --embed-thumbnail` +- Video mode (all else): `yt-dlp -f "bv*+ba/b"` +- Smart output folders: `~/Videos/Social Media/{YouTube,TikTok,...}`, `~/Music/YT Music/{General,Soundtracks}/` +- Ink progress bar during download (stream yt-dlp stdout) +- `magnus audio ` — force audio mode on any URL +- `magnus doctor` — verify yt-dlp, ffmpeg, output dir write access +- Full test suite for platform detection logic (this is pure logic, no subprocess needed) +- `magnus stats` — download history by platform/date (reads SQLite history) + +### Track B — Capture Pipeline (v0.3.0 → v0.4.0) + +Build on the `runInstagram()`, `runTikTok()`, `runCapture()` scaffold already present: +- `magnus capture instagram` — Apify `apify/instagram-reel-scraper` + `apify/instagram-post-scraper` +- `magnus capture tiktok` — Apify `scrape-creators/best-tiktok-transcripts-scraper` +- `magnus capture youtube` — yt-dlp `--write-auto-subs` + webvtt-py, no Apify +- **20-URL canary mandatory** before bulk (hardcode this check — never bypass) +- **Spend cap enforcement:** `$40` soft warn / `$50` hard abort (Apify cost tracking) +- SQLite resume queue (`ig_queue`, `tt_queue`, `yt_queue`) — survives mid-run abort +- `renderNote()` — CKIS Type-B frontmatter: `type: resource`, `subtype: social-capture`, `transcript_source: native|auto-subs|unavailable` +- Atomic vault writes (write complete note or nothing — no partial files) + +### Track C — `magnus capture --watch` (v1.0.0) + +Integrate with recmp3-cli's system audio capture pipeline: +- Detect PulseAudio monitor source (same logic as recmp3's source detection) +- Start recording when user triggers, stop at content boundary +- Prompt: URL + author (paste-in) +- Write CKIS note via `renderNote()` with transcript from the recorded audio +- This is the automation of `[[recmp3-social-capture-pipeline]]` + +### D. Agent-Native Mode +- `--json` flag on all commands (already scaffolded via `Logger` god node) +- `MagnusError` must serialize to JSON cleanly +- Document `magnus --json` output schema in README for Claude Code integration + +--- + +## STEP 3 — SPRINT ATOMIZATION RULES + +1. **Resolve all open decisions FIRST** (Task 0) — write ADRs, no code yet +2. **Track A before Track B** — nothing in Track B until `magnus ` works E2E +3. **Platform detection = its own task** — pure logic, testable without subprocess +4. **SQLite schema is a foundational task** — must be stable before any capture tasks +5. **Apify integration tasks are marked `[BLOCKED-EXTERNAL]`** — implement the code, but flag that they need Apify account + credentials to test E2E +6. **Every task leaves `npm run build && npm test` passing** + +Example: +``` +TASK-01 [INFRA]: Resolve all open decisions → write 6 ADR files in .brain/decisions/ + Done: 6 ADR files exist, each with decision + rationale + consequences + Depends: nothing + +TASK-02 [CORE]: Implement platform URL detection (planDownload() layer) + Files: src/core/detect.ts (new), src/core/detect.test.ts + Done: all platform URLs in test suite route to correct platform enum; + music.youtube.com detected before youtube.com + Depends: TASK-01 +``` + +--- + +## STEP 4 — WORKER ORCHESTRATION PROTOCOL + +**Worker briefing template:** +``` +You are a Sonnet execution agent for magnus-cli — a universal media downloader and CKIS social capture tool. + +ARCHITECTURE FACTS: +- yt-dlp is called as a subprocess (spawn, not exec) — always stream stdout for progress +- music.youtube.com MUST be checked before youtube.com in platform detection (order is critical) +- SQLite via better-sqlite3 (synchronous API) — openDb() is the entry point, never open directly +- MagnusError is the error base class — all thrown errors must extend it +- renderNote() produces CKIS Type-B frontmatter — never write vault notes manually +- Never download MP4s for capture mode — CDN URLs expire in 3 days +- Spend cap: $40 warn / $50 abort — this is hardcoded, never bypassed, never made configurable + +TASK: [exact task spec] +CONTEXT: [relevant god nodes] +CONSTRAINTS: +- Read before edit +- Immediately runnable +- npm run build + npm test must pass +- [BLOCKED-EXTERNAL] tasks: write the code, mock the external calls in tests +- Do NOT commit + +Report back: files changed + verification commands. +``` + +--- + +## STEP 5 — AUDIT PROTOCOL + +```bash +npm run build && npm test +npx tsc --noEmit +npm audit --audit-level=moderate +``` + +**Audit checklist:** +- [ ] `magnus doctor` passes: yt-dlp found, ffmpeg found, output dirs writable +- [ ] Platform detection: `music.youtube.com` routes to `youtube-music` (not `youtube`) +- [ ] `MagnusError` serializes to `{ code, message, platform? }` — no raw Error objects leaked +- [ ] `openDb()` creates tables on first run (idempotent) +- [ ] Spend cap check is NOT configurable — hardcoded at $40/$50 +- [ ] No Apify API key in any test fixture or default config +- [ ] `renderNote()` output validates against CKIS Type-B frontmatter schema +- [ ] SQLite resume queue: aborting mid-run and restarting resumes from checkpoint +- [ ] `--json` flag produces valid JSON on stdout on all commands + +--- + +## STEP 6 — COMPLETION CRITERIA + COMMIT + +- [ ] `npm run build` → exit 0 +- [ ] `npm test` → all tests pass (platform detection fully covered) +- [ ] `npx tsc --noEmit` → 0 errors +- [ ] `npm audit` → 0 moderate/high +- [ ] `magnus doctor` → passes (yt-dlp + ffmpeg verified) +- [ ] `magnus https://www.youtube.com/watch?v=dQw4w9WgXcQ` → downloads to correct folder +- [ ] `npm publish --dry-run` → exits cleanly + +```bash +git add [specific files] +git commit -m "feat: magnus-cli v1.0.0 — universal download, social capture pipeline, CKIS integration" +git tag v1.0.0 +git push origin main --tags +``` + +--- + +## STEP 7 — PERSONAL GUIDE (after v1.0.0 is pushed) + +This step runs **only after Step 6 is fully complete** — v1.0.0 tagged, pushed, and npm published. + +Write Eduardo's personal reference guide to: +`~/Documents/Second Brain/02-projects/magnus-cli/personal-guide.md` + +**What this IS:** Eduardo's personal manual — how he uses Magnus to download media and ingest social content into his CKIS vault, written in plain language with his real folder structure and real commands. +**What this is NOT:** developer docs or a README for strangers. + +### Guide structure: + +``` +--- +type: guide +subtype: personal-tool-reference +project: magnus-cli +created: +modified: +tags: [guide, cli, personal-reference, magnus, descarga, media, social-capture, ckis] +related: + - "[[02-projects/magnus-cli/_overview]]" + - "[[00-systems/workflows/ckis-social-captures-pipeline-master-plan]]" + - "[[00-systems/workflows/recmp3-social-capture-pipeline]]" +--- + +# magnus — Mi Guía Personal +``` + +**Section 1 — ¿Qué es esto y para qué lo uso?** +Plain-language: Magnus reemplaza la función `yt()` del `.bashrc` y automatiza todo el pipeline de captura social. Qué hacía Eduardo antes (bash aliases, descarga manual, copiar-pegar). Qué resuelve Magnus ahora. Los dos tracks: descargas simples vs captura masiva de posts guardados. + +**Section 2 — Cómo lo tengo instalado y configurado** +`npm i -g magnus-cli` → `magnus` global. Dónde están los archivos de config (`~/.config/magnus/`). Las carpetas de output: `~/Videos/Social Media/`, `~/Music/YT Music/`. El archivo `.env` con las API keys (Apify, etc). `magnus doctor` — output limpio en la máquina de Eduardo. + +**Section 3 — Todos los comandos con ejemplos reales** +Every command with Eduardo's exact usage: +- `magnus ` — detecta plataforma automáticamente, descarga al folder correcto. Ejemplo real: YouTube video, TikTok, Instagram Reel, YouTube Music +- `magnus audio ` — fuerza modo MP3. Cuándo usar esto (música, podcasts) +- `magnus batch ` — descarga desde lista de URLs. Cómo Eduardo arma la lista +- `magnus capture instagram` — ingesta masiva de posts guardados via Apify. Qué genera (notas en CKIS) +- `magnus capture tiktok` — igual para TikTok +- `magnus capture youtube` — playlists/historial via yt-dlp (gratis, sin Apify) +- `magnus capture --watch` — el pipeline de system audio con recmp3 (el use case más poderoso) +- `magnus stats` — historial por plataforma y fecha +- `magnus config` — qué tiene Eduardo configurado +- `magnus doctor` — verificación completa + +**Section 4 — Mis casos de uso personales** + +1. **Descarga rápida:** `magnus https://...` → el URL que acabo de ver, descargado al folder correcto automáticamente. Antes: abrir yt-dlp, recordar los flags, especificar el folder. Ahora: un comando. +2. **Captura de contenido de Instagram:** tengo 5,200 posts guardados — `magnus capture instagram` los convierte en notas CKIS con transcript nativo. Eduardo puede buscar cualquier cosa que haya guardado en Instagram en Obsidian. +3. **Pipeline recmp3 + Magnus:** ver un Reel → `recwatch` graba el audio → `magnus capture --watch` lo convierte en nota CKIS con transcript y metadatos del post. El workflow completo sin abrir ninguna app. +4. **Música:** `magnus audio https://music.youtube.com/...` → MP3 con metadata y thumbnail en `~/Music/YT Music/` + +**Section 5 — El pipeline de captura social explicado simple** +Cómo funciona el proceso completo: Apify obtiene el post → Magnus extrae el transcript → `renderNote()` crea la nota en Obsidian → Eduardo puede buscar en su vault todo lo que consumió. Por qué no descarga los MP4s (los CDN URLs expiran en 3 días). Por qué el canary de 20 URLs antes del bulk (para no gastar $50 en Apify si algo falla). + +**Section 6 — Lo que aprendí** +- La trampa del orden en detección de plataformas: `music.youtube.com` DEBE detectarse antes que `youtube.com` +- Por qué el resume queue en SQLite salva la vida en capturas de miles de posts +- El spend cap de $40/$50 de Apify: por qué está hardcodeado y por qué eso es bueno +- bgutil-ytdlp-pot-provider: qué es y por qué YouTube lo necesita ahora + +**Section 7 — Referencia rápida** +Cheat sheet de todos los comandos con una línea de descripción. + +### After writing the guide: +1. Write the file to the exact CKIS path above +2. Add `[[personal-guide]]` wikilink to `02-projects/magnus-cli/_overview.md` +3. Update `_overview.md`'s `modified:` to today + +--- + +## CKIS INTEGRATION REMINDER + +- `.brain/decisions/` — ADR for every resolved open decision (write these in Task 0) +- `renderNote()` output must stay in sync with CKIS vault conventions +- Every commit triggers graph-report sync to CKIS automatically + +Begin by executing Step 1 (Load Context) now. diff --git a/.brain/decisions/2026-06-03-config-surface.md b/.brain/decisions/2026-06-03-config-surface.md new file mode 100644 index 0000000..08df926 --- /dev/null +++ b/.brain/decisions/2026-06-03-config-surface.md @@ -0,0 +1,37 @@ +--- +type: decision +project: magnus-cli +status: adopted +date: 2026-06-03 +reversal-cost: medium +review-by: +tags: [decision, magnus-cli, config, security] +--- + +# ADR-0002 — Split config surface: config.json + .env (XDG) + +## Decision + +Two files under the XDG config dir (`~/.config/magnus/` via `env-paths`): + +- **`config.json`** — non-secret prefs: output-folder templates, budget caps, + canary size, batch sizes, vault path, `recmp3Source`, `inboxSubdir`. +- **`.env`** (mode `0600`) — secrets only: `APIFY_TOKEN`. Loaded by + `loadSecrets()`, never serialized, never logged (passes through `redact()`). + +## Rationale + +- Separates concerns: prefs are safe to print (`magnus config list`); secrets + never are. `config list` was explicitly fixed to not echo secrets. +- Mirrors `~/.config/ckis/.env` conventions already on the machine. +- `--config ` overrides the whole directory for isolation (tests, multiple + vaults); the SQLite handle is keyed per-path so isolation is real. + +## Consequences + +- Secret leakage guarded at three layers: `.env` 0600, `SECRET_KEYS` redaction, + and Bearer/`key=VALUE` pattern redaction in `redactString()`. +- `APIFY_TOKEN` is read from env/.env only — never a CLI flag (wouldn't want it + in shell history / `ps`). + +See [[2026-06-03-language-single-stack-node-ts]]. diff --git a/.brain/decisions/2026-06-03-language-single-stack-node-ts.md b/.brain/decisions/2026-06-03-language-single-stack-node-ts.md new file mode 100644 index 0000000..cdd1ff1 --- /dev/null +++ b/.brain/decisions/2026-06-03-language-single-stack-node-ts.md @@ -0,0 +1,37 @@ +--- +type: decision +project: magnus-cli +status: adopted +date: 2026-06-03 +reversal-cost: high +review-by: +tags: [decision, magnus-cli, architecture] +--- + +# ADR-0001 — Single-stack Node.js + TypeScript (no Python for capture) + +## Decision + +All of magnus — CLI shell, download core, **and** the Apify capture pipeline — +is Node.js 20 (ESM) + TypeScript 5.5. The original overview left open whether +capture orchestration should stay in Python (proven in the CKIS Master Plan). +Resolved: **Node/TypeScript everywhere.** + +## Rationale + +- `runInstagram()`, `runTikTok()`, `runYouTube()` are already TypeScript god + nodes in the shipped v0.1.0 scaffold — they exist and pass tests. +- Single stack = one toolchain (tsup/vitest/eslint), one dependency tree, one + install (`npm i -g`), no Python venv to provision on the target laptop. +- `apify-client` has a first-class JS SDK; yt-dlp is shelled out the same way + from any language. + +## Consequences + +- No Python interpreter dependency on the user's machine. `doctor` checks + yt-dlp + ffmpeg only. +- webvtt parsing is done in-stack (`src/capture/vtt.ts`) rather than via the + Python `webvtt-py` referenced in the Master Plan. + +Supersedes the "leaning Python for capture scripts" note in `_overview.md`. +See [[2026-06-03-config-surface]]. diff --git a/.brain/decisions/2026-06-03-milestone-scope.md b/.brain/decisions/2026-06-03-milestone-scope.md new file mode 100644 index 0000000..7949fac --- /dev/null +++ b/.brain/decisions/2026-06-03-milestone-scope.md @@ -0,0 +1,40 @@ +--- +type: decision +project: magnus-cli +status: adopted +date: 2026-06-03 +reversal-cost: medium +review-by: +tags: [decision, magnus-cli, roadmap, release] +--- + +# ADR-0005 — Milestone scope: what v1.0.0 actually is + +## Decision + +v1.0.0 = **Track A (download) shipped + Track B (capture) code-complete + +Track C (`capture --watch`) implemented + npm published.** + +The OPUS_BOOTSTRAP planned Track A as a *future* v0.2.0 — but Track A was +already fully implemented and shipped in v0.1.0. This ADR re-bases the roadmap +on the real state rather than the stale "not yet scaffolded" assumption. + +## Real state at decision time (commit ad14415) + +- **Track A — done.** `detectPlatform()` (music.youtube.com ordered first), + audio/video flag builders, smart output folders, `audio`, `batch`, `stats`, + `doctor`. yt-dlp 2026.03.17 + ffmpeg present → `doctor` passes. 40 tests. +- **Track B — code-complete, `[BLOCKED-EXTERNAL]`.** Canary, $40/$50 cap, + per-platform SQLite resume queues, atomic vault writes, renderNote — all + implemented. Cannot E2E-verify without an Apify account/token (deferred). +- **Track C — built in this session.** `capture --watch` via recmp3. +- **Agent-native — done.** `--json`, `manifest`, MCP, exit codes. + +## Consequences + +- Going straight from v0.1.0 → **v1.0.0** (skipping the planned 0.2/0.3/0.4 + intermediates) because their scope already landed. +- Track B stays behind an external dependency; its code is shipped and unit- + tested with mocks, flagged in `doctor` (`apify-token: not set`). + +See [[2026-06-03-package-name-scoped]] and [[2026-06-03-watch-spec]]. diff --git a/.brain/decisions/2026-06-03-no-ink-tui-streaming-stdout.md b/.brain/decisions/2026-06-03-no-ink-tui-streaming-stdout.md new file mode 100644 index 0000000..b796a96 --- /dev/null +++ b/.brain/decisions/2026-06-03-no-ink-tui-streaming-stdout.md @@ -0,0 +1,42 @@ +--- +type: decision +project: magnus-cli +status: adopted +date: 2026-06-03 +reversal-cost: low +review-by: +tags: [decision, magnus-cli, ux, agent-native] +--- + +# ADR-0003 — Drop Ink TUI; stream child stdout + agent-native output + +## Decision + +magnus ships **no Ink/React TUI.** Progress is the downloader's own output: +`runDownload()` inherits yt-dlp's stdout/stderr so the native progress bar shows +through. Long capture runs log structured lines to stderr. Machine callers use +`--json`. + +This **reverses** the OPUS_BOOTSTRAP's "Ink v5 + ora" resolution (decision #3), +which was written against a stale "not yet scaffolded" assumption. + +## Rationale + +- The v0.1.0 audit removed `ink`, `react`, `@types/react` — they had **zero + imports**. Carrying a TUI runtime for a tool that mostly shells out to yt-dlp + (which already renders progress) is dead weight, especially on the target + Celeron N4120 laptop. +- Agent-native is the priority: `--json` envelope on stdout, logs on stderr, + deterministic exit codes, `magnus manifest`, MCP server. A React reconciler + fights that model. +- `capture --watch` is the one interactive command; it uses Node's built-in + `readline/promises` (prompt on stderr, ENTER to stop), no framework needed. + +## Consequences + +- No rich multi-bar UI for batch downloads — acceptable; yt-dlp prints per-item. +- Smaller bundle, fewer transitive deps, faster cold start. +- If a TUI is ever wanted, it's additive and can sit behind a TTY check without + touching the agent path. + +See [[2026-06-03-watch-spec]]. diff --git a/.brain/decisions/2026-06-03-package-name-scoped.md b/.brain/decisions/2026-06-03-package-name-scoped.md new file mode 100644 index 0000000..0e3e591 --- /dev/null +++ b/.brain/decisions/2026-06-03-package-name-scoped.md @@ -0,0 +1,36 @@ +--- +type: decision +project: magnus-cli +status: adopted +date: 2026-06-03 +reversal-cost: high +review-by: +tags: [decision, magnus-cli, npm, release] +--- + +# ADR-0004 — Publish as @aedneth/magnus-cli (scoped), bin `magnus` + +## Decision + +npm package name = **`@aedneth/magnus-cli`** (public scoped). Installed binary +stays **`magnus`**. `publishConfig.access = public`. + +## Rationale + +- Both unscoped names are **already owned by other people** on npm: + `magnus-cli@1.0.7` and `magnus@0.30.0`. Neither is available. +- Scope `@aedneth` matches the GitHub org (`github.com/aedneth/magnus-cli`) and + the npm account the publish token authenticates as (`npm whoami` → `aedneth`). +- Scoped name is free (registry 404) and namespaces the suite cleanly. +- The OPUS_BOOTSTRAP's decision #4 ("magnus-cli unscoped") was made before name + availability was checked — this ADR records the corrected reality. + +## Consequences + +- Users install with `npm i -g @aedneth/magnus-cli`; the command is still + `magnus`. +- package.json `homepage` / `repository` / `bugs` URLs corrected from the + cloud-agent's wrong `eduardoborjas` username to `aedneth` (the v0.1.0 doc + sweep missed package.json specifically). + +See [[2026-06-03-milestone-scope]]. diff --git a/.brain/decisions/2026-06-03-watch-spec.md b/.brain/decisions/2026-06-03-watch-spec.md new file mode 100644 index 0000000..c513019 --- /dev/null +++ b/.brain/decisions/2026-06-03-watch-spec.md @@ -0,0 +1,49 @@ +--- +type: decision +project: magnus-cli +status: adopted +date: 2026-06-03 +reversal-cost: medium +review-by: +tags: [decision, magnus-cli, capture, recmp3, ckis] +--- + +# ADR-0006 — `capture --watch` spec (recmp3 system-audio integration) + +## Decision + +`magnus capture --watch` records the PulseAudio **monitor** +source via recmp3, lets recmp3 transcribe (Groq Whisper), and files one CKIS +Type-B note with `transcript_source: recmp3-whisper` into `/00-inbox/`. +This automates the manual `recmp3-social-capture-pipeline` workflow. + +## Mechanics + +- **recmp3 is shelled out** (`RECMP3_BIN` override for tests) with + `record --transcribe --json -y`. stdout is **captured** (its JSON envelope — + transcript at `data.transcription.text`); stderr is **inherited** (live UI). +- **Stop signal:** recmp3 headless records until SIGINT. Interactively, magnus + reads ENTER then sends `SIGINT` to the child. Headless/agent mode requires + `--duration ` (no TTY) — this keeps `--watch` testable and scriptable. +- **Source selection:** `--source` → `RECMP3_SOURCE` → `config.capture.recmp3Source` + → auto-detect (`recmp3 sources --json`, pick the id ending in `.monitor`). +- **Metadata:** `--url` + `--author` (+ optional `--collection`); prompted on + stderr when interactive, required as flags when headless. + +## Rationale + +- $0 Apify — only recmp3's Groq credits (~$0.001/min). Right tool when you're + already watching the reel and curating as you go. +- Monitor source (not mic) captures what plays through the speakers. +- Prompts/readline write to **stderr** so `--json` stdout stays a clean + envelope. + +## Consequences + +- Adds `recmp3-whisper` to the `TranscriptSource` union and `recmp3Source` / + `inboxSubdir` to the capture config schema. +- `--watch` is the only interactive command; everything else is agent-clean. +- recmp3 must be installed and have a transcription provider configured; failure + surfaces as `E_RECMP3_FAILED` (exit 4) with a `recmp3 doctor` hint. + +See [[2026-06-03-no-ink-tui-streaming-stdout]] and [[2026-06-03-milestone-scope]]. diff --git a/CHANGELOG.md b/CHANGELOG.md index 9fb0bd9..7e92394 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,33 @@ All notable changes to this project are documented here. The format is based on ## [Unreleased] +## [1.0.0] — 2026-06-03 + +### Added + +- `capture --watch` (Track C): real-time system-audio capture via + [recmp3](https://github.com/aedneth/recmp3-cli). Records the PulseAudio + `.monitor` source while you play a saved reel/TikTok, transcribes it (Groq + Whisper, $0 Apify), and files one CKIS Type-B note with + `transcript_source: recmp3-whisper` into `/00-inbox/`. Interactive + ENTER-to-stop, or headless `--duration ` with `--url`/`--author`. Source + auto-detects, overridable via `--source` / `RECMP3_SOURCE` / + `capture.recmp3Source`. +- Config: `capture.inboxSubdir` (default `00-inbox`) and `capture.recmp3Source`. +- Architecture decision records under `.brain/decisions/` (ADR-0001…0006). + +### Changed + +- **Package renamed to `@aedneth/magnus-cli`** (scoped) — both unscoped names + (`magnus`, `magnus-cli`) are owned by other publishers on npm. Binary stays + `magnus`. First public npm release. + +### Fixed + +- `package.json` `homepage` / `repository` / `bugs` URLs corrected from the + wrong `eduardoborjas` GitHub username to `aedneth` (the v0.1.0 doc sweep + missed package.json). + ## [0.1.0] — 2026-06-02 ### Added @@ -27,5 +54,6 @@ All notable changes to this project are documented here. The format is based on - CI workflow: moved `npm run build` before `npm test` so `dist/cli.js` exists when agent-mode integration tests run. - CLI stdout flushing: replaced `process.exit()` with `process.exitCode` assignment to prevent pipe-buffer truncation on macOS + Node 20 for large JSON outputs (e.g. `magnus manifest --json`). -[Unreleased]: https://github.com/aedneth/magnus-cli/compare/v0.1.0...HEAD +[Unreleased]: https://github.com/aedneth/magnus-cli/compare/v1.0.0...HEAD +[1.0.0]: https://github.com/aedneth/magnus-cli/compare/v0.1.0...v1.0.0 [0.1.0]: https://github.com/aedneth/magnus-cli/releases/tag/v0.1.0 diff --git a/README.md b/README.md index 7cc85d4..9d2d4a7 100644 --- a/README.md +++ b/README.md @@ -43,10 +43,12 @@ See [`AGENTS.md`](./AGENTS.md) for the agent integration guide. ## Install ```bash -npm install -g magnus -# or: pnpm add -g magnus +npm install -g @aedneth/magnus-cli +# or: pnpm add -g @aedneth/magnus-cli ``` +The installed binary is `magnus`. + Magnus shells out to **`yt-dlp`** and **`ffmpeg`** — install those too, then verify: ```bash @@ -62,10 +64,27 @@ magnus audio "https://…" --soundtrack # → ~/Music/YT Music/Soundt magnus batch urls.txt # one URL per line (use - for stdin) magnus capture instagram --input ./ig_saved/ # bulk-ingest saved posts (canary-gated) +magnus capture instagram --watch # record what you're watching → CKIS note magnus stats # download history by platform magnus config list # show resolved config ``` +### `capture --watch` — record what you're watching + +When you're already playing a saved reel/TikTok, `--watch` captures the +**system-audio monitor** via [recmp3](https://github.com/aedneth/recmp3-cli), +transcribes it (Groq Whisper — $0 Apify), and files one CKIS note with +`transcript_source: recmp3-whisper`: + +```bash +magnus capture instagram --watch # press ENTER when the reel ends +magnus capture tiktok --watch \ # headless / scriptable + --url https://www.tiktok.com/@user/video/123 --author user --duration 45 +``` + +Source auto-detects the `.monitor` device; override with `--source` or +`RECMP3_SOURCE`. Notes land in `/00-inbox/`. + > **Tip:** always quote URLs containing `&`, or your shell will background the command. ## Why magnus? @@ -92,11 +111,14 @@ magnus config set capture.budgetHardCapUsd 50 ## Roadmap - [x] **v0.1.0** — download core (ordered platform detection, audio/video flag sets, output-folder map), Apify capture pipeline (SQLite queues, canary, spend caps, CKIS Markdown), full agent-native layer (`--json`, `--yes`, manifest, MCP server), dual AGPL-3.0 + commercial license -- [ ] **v0.2.0** — interactive Ink v5 TUI: live progress bars, download spinners, capture cost dashboard -- [ ] **v0.3.0** — `magnus doctor` full auto-fix mode, `npm publish` pipeline, first public npm release -- [ ] **v0.4.0** — playlist support, concurrent download pool, per-item retry with back-off -- [ ] **v0.5.0** — capture queue management TUI, per-actor Apify cost breakdown, queue drain/pause commands -- [ ] **v1.0.0** — stable public API, full docs site, public launch +- [x] **v1.0.0** — `capture --watch` (real-time recmp3 system-audio capture → CKIS notes), published to npm as `@aedneth/magnus-cli` +- [ ] **v1.1.0** — playlist support, concurrent download pool, per-item retry with back-off +- [ ] **v1.2.0** — capture queue management commands (drain/pause), per-actor Apify cost breakdown +- [ ] **future** — full docs site + +> The interactive Ink TUI from earlier plans was dropped in favor of streaming +> the downloader's native progress and a clean `--json` contract — see +> `.brain/decisions/2026-06-03-no-ink-tui-streaming-stdout.md`. ## License diff --git a/package-lock.json b/package-lock.json index 9cc8603..3c08f46 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "magnus", - "version": "0.1.0", + "version": "1.0.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "magnus", - "version": "0.1.0", + "version": "1.0.0", "license": "AGPL-3.0-or-later", "dependencies": { "@modelcontextprotocol/sdk": "^1.0.0", diff --git a/package.json b/package.json index b401642..706e85d 100644 --- a/package.json +++ b/package.json @@ -1,17 +1,20 @@ { - "name": "magnus", - "version": "0.1.0", + "name": "@aedneth/magnus-cli", + "version": "1.0.0", "description": "Universal yt-dlp media downloader + Apify-driven social-capture pipeline into CKIS markdown. Agent-native, cross-platform CLI.", "type": "module", "license": "AGPL-3.0-or-later", "author": "Eduardo Borjas ", - "homepage": "https://github.com/eduardoborjas/magnus-cli#readme", + "homepage": "https://github.com/aedneth/magnus-cli#readme", + "publishConfig": { + "access": "public" + }, "repository": { "type": "git", - "url": "git+https://github.com/eduardoborjas/magnus-cli.git" + "url": "git+https://github.com/aedneth/magnus-cli.git" }, "bugs": { - "url": "https://github.com/eduardoborjas/magnus-cli/issues" + "url": "https://github.com/aedneth/magnus-cli/issues" }, "keywords": [ "yt-dlp", diff --git a/src/capture/render/frontmatter.ts b/src/capture/render/frontmatter.ts index 245fab8..9c5fe47 100644 --- a/src/capture/render/frontmatter.ts +++ b/src/capture/render/frontmatter.ts @@ -1,6 +1,6 @@ import { stringify } from 'yaml'; -export type TranscriptSource = 'native' | 'auto-subs' | 'unavailable'; +export type TranscriptSource = 'native' | 'auto-subs' | 'recmp3-whisper' | 'unavailable'; export type CaptureSource = 'instagram' | 'tiktok' | 'youtube'; /** diff --git a/src/capture/run/watch.ts b/src/capture/run/watch.ts new file mode 100644 index 0000000..70c2aea --- /dev/null +++ b/src/capture/run/watch.ts @@ -0,0 +1,283 @@ +/** + * `magnus capture --watch` — real-time system-audio capture. + * + * You're already watching a saved Instagram reel or TikTok. Instead of scraping + * it via Apify ($), this records the system-audio monitor with recmp3, lets + * recmp3 transcribe it (Groq Whisper), and writes a CKIS Type-B note with + * `transcript_source: recmp3-whisper`. $0 Apify — only recmp3's Groq credits. + * + * recmp3 is shelled out as an agent-native child: stdout is captured (its JSON + * envelope), stderr is inherited (its live recording UI). The recording stops + * on SIGINT — interactively we send that when the user presses ENTER; headless + * we pass `--duration ` so no TTY is required. + * + * See: 00-systems/workflows/recmp3-social-capture-pipeline.md + */ + +import { execa } from 'execa'; +import { createInterface } from 'node:readline/promises'; +import type { CommandContext } from '../../agent/context.js'; +import { ExitCode, MagnusError } from '../../agent/exit.js'; +import { renderNote, type CaptureNote, type CaptureSource } from '../render/frontmatter.js'; +import { deploy } from '../vault.js'; +import { resolveVault } from './shared.js'; +import type { CaptureSummary } from './shared.js'; +import { join } from 'node:path'; + +/** recmp3 audio source as returned by `recmp3 sources --json`. */ +export interface RecmpSource { + id: string; + label?: string; + isDefault?: boolean; +} + +/** + * Pick the system-audio *monitor* source (what plays through the speakers), + * never the microphone. Monitor source ids end in `.monitor` on PulseAudio. + */ +export function pickMonitorSource(sources: RecmpSource[]): string | null { + const monitor = sources.find((s) => s.id.endsWith('.monitor')); + if (monitor) return monitor.id; + const byLabel = sources.find((s) => /monitor/i.test(s.label ?? '')); + return byLabel?.id ?? null; +} + +/** Only Instagram and TikTok make sense for watch capture (YouTube uses subs). */ +const WATCH_PLATFORMS = new Set(['instagram', 'tiktok']); + +/** Derive a stable note id from the post URL, per platform. */ +export function deriveId(platform: CaptureSource, url: string): string { + if (platform === 'instagram') { + const m = url.match(/\/(?:p|reel|reels|tv)\/([A-Za-z0-9_-]+)/); + if (m) return m[1] as string; + } else if (platform === 'tiktok') { + const m = url.match(/\/video\/(\d+)/) ?? url.match(/\/photo\/(\d+)/); + if (m) return m[1] as string; + } + // Fallback: a sortable timestamp id so the note still has a unique filename. + return `watch-${new Date().toISOString().replace(/[:.]/g, '-')}`; +} + +export interface WatchNoteInput { + platform: CaptureSource; + url: string; + author: string; + collection?: string | null; + transcript: string; + language?: string; + durationSeconds?: number | null; + createdDate: string; +} + +/** Build the CKIS note for a watch capture. Pure — unit-tested without subprocess. */ +export function buildWatchNote(input: WatchNoteInput): { filename: string; markdown: string } { + const author = (input.author || 'unknown').replace(/^@/, ''); + const authorUrl = + input.platform === 'instagram' + ? `https://www.instagram.com/${author}/` + : `https://www.tiktok.com/@${author}`; + const transcript = input.transcript.trim(); + const note: CaptureNote = { + source: input.platform, + url: input.url, + id: deriveId(input.platform, input.url), + author, + authorUrl, + collection: input.collection ?? null, + language: input.language ?? 'unknown', + durationSeconds: input.durationSeconds ?? null, + // recmp3 captures audio only — no scraped engagement metadata. + likes: null, + comments: null, + views: null, + transcriptSource: 'recmp3-whisper', + transcript, + caption: '', + }; + return renderNote(note, input.createdDate); +} + +/** Extract the transcript text from a recmp3 `record --transcribe --json` envelope. */ +export function transcriptFromEnvelope(stdout: string): { + text: string; + language?: string; + durationSec?: number; +} { + let parsed: unknown; + try { + parsed = JSON.parse(stdout); + } catch { + throw new MagnusError('recmp3 did not emit a JSON envelope.', { + code: ExitCode.Error, + errorCode: 'E_RECMP3_PARSE', + hint: 'Update recmp3, or run `recmp3 record --transcribe --json` directly to inspect output.', + }); + } + const data = (parsed as { data?: Record }).data ?? {}; + const t = (data.transcription as Record | undefined) ?? undefined; + return { + text: ((t?.text as string) ?? '').trim(), + language: t?.language as string | undefined, + durationSec: (t?.durationSec as number | undefined) ?? (data.durationSec as number | undefined), + }; +} + +/** recmp3 binary — overridable via env so tests can point at a stub. */ +function recmpBin(): string { + return process.env.RECMP3_BIN || 'recmp3'; +} + +async function resolveMonitorSource(ctx: CommandContext): Promise { + const explicit = + (ctx.opts.source as string | undefined) ?? + process.env.RECMP3_SOURCE ?? + ctx.config.capture.recmp3Source ?? + undefined; + if (explicit) return explicit; + + // Auto-detect via `recmp3 sources --json`. + const res = await execa(recmpBin(), ['sources', '--json'], { reject: false }); + if (res.exitCode !== 0) { + throw new MagnusError('Could not list recmp3 audio sources.', { + code: ExitCode.MissingDependency, + errorCode: 'E_NO_RECMP3', + hint: 'Install recmp3 and run `recmp3 sources`, or pass --source .', + }); + } + let sources: RecmpSource[] = []; + try { + sources = ((JSON.parse(res.stdout) as { data?: { sources?: RecmpSource[] } }).data?.sources) ?? []; + } catch { + /* fall through to the empty-source error below */ + } + const monitor = pickMonitorSource(sources); + if (!monitor) { + throw new MagnusError('No system-audio monitor source found.', { + code: ExitCode.Config, + errorCode: 'E_NO_MONITOR', + hint: 'Pass --source , set capture.recmp3Source, or export RECMP3_SOURCE. List with `recmp3 sources`.', + }); + } + return monitor; +} + +/** Prompt on stderr (keeps stdout clean for the JSON envelope). Interactive only. */ +async function prompt(question: string): Promise { + const rl = createInterface({ input: process.stdin, output: process.stderr }); + try { + return (await rl.question(question)).trim(); + } finally { + rl.close(); + } +} + +export async function runWatch(ctx: CommandContext): Promise { + const platformArg = (ctx.args[0] ?? (ctx.opts.platform as string | undefined) ?? '').toLowerCase(); + const platform = (platformArg === 'ig' ? 'instagram' : platformArg === 'tt' ? 'tiktok' : platformArg) as CaptureSource; + if (!WATCH_PLATFORMS.has(platform)) { + throw new MagnusError(`--watch supports instagram or tiktok, not "${platformArg || '(none)'}".`, { + code: ExitCode.Usage, + errorCode: 'E_WATCH_PLATFORM', + hint: 'Use: magnus capture instagram --watch (or tiktok)', + }); + } + + const vault = resolveVault(ctx); + const inboxDir = join(vault, ctx.config.capture.inboxSubdir); + + const durationOpt = + ctx.opts.duration !== undefined ? parseInt(String(ctx.opts.duration), 10) : undefined; + const interactive = !ctx.mode.agent && !ctx.mode.yes; + + // Headless requires --duration (no ENTER to stop) and url/author up front. + let url = ctx.opts.url as string | undefined; + let author = ctx.opts.author as string | undefined; + const collection = (ctx.opts.collection as string | undefined) ?? null; + + if (!interactive && durationOpt === undefined) { + throw new MagnusError('Non-interactive watch needs --duration .', { + code: ExitCode.Usage, + errorCode: 'E_WATCH_NO_TTY', + hint: 'Run in a terminal (press ENTER to stop), or pass --duration with --url and --author.', + }); + } + if (!interactive && (!url || !author)) { + throw new MagnusError('Non-interactive watch needs --url and --author.', { + code: ExitCode.Usage, + errorCode: 'E_WATCH_NO_META', + hint: 'Pass --url --author .', + }); + } + + const source = await resolveMonitorSource(ctx); + + // Build recmp3 invocation. stdout captured (its JSON), stderr inherited (UI). + const args = ['record', '--transcribe', '--json', '-y']; + if (durationOpt !== undefined) args.push('--duration', String(durationOpt)); + + ctx.log.info(`Recording system audio via recmp3 (source: ${source})…`); + if (interactive && durationOpt === undefined) { + ctx.log.info('▶ Play the reel now. Press ENTER when it ends to stop & transcribe.'); + } + + const child = execa(recmpBin(), args, { + env: { ...process.env, RECMP3_SOURCE: source }, + stdin: 'ignore', + stderr: 'inherit', + reject: false, + }); + + // Interactive stop: ENTER → SIGINT to recmp3. + if (interactive && durationOpt === undefined) { + const rl = createInterface({ input: process.stdin, output: process.stderr }); + await rl.question(''); + rl.close(); + child.kill('SIGINT'); + } + + const result = await child; + if (result.exitCode !== 0) { + throw new MagnusError(`recmp3 exited with code ${result.exitCode}.`, { + code: ExitCode.Network, + errorCode: 'E_RECMP3_FAILED', + hint: 'Check recmp3 transcription provider config: `recmp3 doctor`.', + }); + } + + const { text, language, durationSec } = transcriptFromEnvelope(result.stdout); + + // Collect URL + author interactively if not provided via flags. + if (interactive) { + if (!url) url = await prompt('Post URL: '); + if (!author) author = await prompt('Author handle: '); + } + if (!url || !author) { + throw new MagnusError('A post URL and author are required to file the note.', { + code: ExitCode.Usage, + errorCode: 'E_WATCH_NO_META', + }); + } + + const createdDate = new Date().toISOString().slice(0, 10); + const { filename, markdown } = buildWatchNote({ + platform, + url, + author, + collection, + transcript: text, + language, + durationSeconds: durationSec ?? null, + createdDate, + }); + + const { deployed, collisions } = deploy([{ filename, markdown }], inboxDir, join(inboxDir, 'failed')); + + return { + platform, + enqueued: 0, + processed: deployed, + failed: 0, + spentUsd: 0, + collisions: collisions.length, + }; +} diff --git a/src/commands/capture.ts b/src/commands/capture.ts index 1c07613..c86ca87 100644 --- a/src/commands/capture.ts +++ b/src/commands/capture.ts @@ -4,6 +4,7 @@ import { MagnusError, ExitCode } from '../agent/exit.js'; import { runInstagram } from '../capture/run/instagram.js'; import { runTikTok } from '../capture/run/tiktok.js'; import { runYouTube } from '../capture/run/youtube.js'; +import { runWatch } from '../capture/run/watch.js'; import type { CaptureSummary } from '../capture/run/shared.js'; const ALIASES: Record = { @@ -16,6 +17,20 @@ const ALIASES: Record = { }; async function runCapture(ctx: CommandContext): Promise { + // Watch mode: real-time system-audio capture (recmp3), not bulk scraping. + if (ctx.opts.watch) { + const summary = await runWatch(ctx); + const exitCode = summary.processed === 0 ? ExitCode.Empty : ExitCode.Success; + return { + data: summary, + human: + summary.processed > 0 + ? `${summary.platform}: filed 1 watch note${summary.collisions ? ' (collision parked)' : ''}.` + : `${summary.platform}: nothing filed.`, + exitCode, + }; + } + const platformArg = ctx.args[0]; const platform = platformArg ? ALIASES[platformArg.toLowerCase()] : undefined; if (!platform) { @@ -51,19 +66,27 @@ export const captureCommand: Command = { name: 'capture', summary: 'Bulk-ingest saved social posts into CKIS markdown (canary + budget gated)', description: - 'Ingests saved Instagram (Apify), TikTok (Apify), or YouTube (local yt-dlp) posts into the CKIS vault as Type-B notes. Runs a 20-URL canary before bulk, enforces the $40 abort / $50 hard cap, resumes via SQLite queues, and never downloads the expiring MP4. Pass --yes to skip the canary prompt.', + 'Ingests saved Instagram (Apify), TikTok (Apify), or YouTube (local yt-dlp) posts into the CKIS vault as Type-B notes. Runs a 20-URL canary before bulk, enforces the $40 abort / $50 hard cap, resumes via SQLite queues, and never downloads the expiring MP4. Pass --yes to skip the canary prompt.\n\n--watch records the system-audio monitor with recmp3 while you play a reel, transcribes it (Groq Whisper, $0 Apify), and files one note with transcript_source: recmp3-whisper. Interactive: press ENTER to stop. Headless: pass --duration with --url and --author.', args: [{ name: 'platform', description: 'instagram | tiktok | youtube', required: true }], flags: [ { flags: '--input ', description: 'Input file/dir (collection .txt dir, export JSON/TXT, or Takeout CSV)' }, { flags: '--vault ', description: 'CKIS vault root (overrides config/CKIS_VAULT)', env: 'CKIS_VAULT' }, { flags: '--budget-cap ', description: 'Override the per-run canary ceiling' }, { flags: '--limit ', description: 'Cap how many URLs to process' }, + { flags: '--watch', description: 'Real-time system-audio capture via recmp3 (instagram|tiktok)' }, + { flags: '--url ', description: 'Post URL for the watch note (else prompted)' }, + { flags: '--author ', description: 'Author handle for the watch note (else prompted)' }, + { flags: '--collection ', description: 'CKIS collection tag for the watch note' }, + { flags: '--source ', description: 'recmp3 monitor source (else auto-detect)', env: 'RECMP3_SOURCE' }, + { flags: '--duration ', description: 'Fixed recording length for headless watch (no ENTER)' }, ], - exitCodes: ['Success', 'Usage', 'Config', 'Budget', 'Network', 'Partial', 'Empty', 'Error'], + exitCodes: ['Success', 'Usage', 'Config', 'Budget', 'Network', 'Partial', 'Empty', 'MissingDependency', 'Error'], examples: [ 'magnus capture instagram --input ./ig_saved/', 'magnus capture tiktok --input user_data.json --json', 'magnus capture youtube --input takeout.csv --yes', + 'magnus capture instagram --watch', + 'magnus capture tiktok --watch --url https://www.tiktok.com/@user/video/123 --author user --duration 45', ], inputSchema: z.object({ platform: z.enum(['instagram', 'tiktok', 'youtube', 'ig', 'tt', 'yt']), @@ -71,6 +94,12 @@ export const captureCommand: Command = { vault: z.string().optional(), budgetCap: z.number().optional(), limit: z.number().optional(), + watch: z.boolean().optional(), + url: z.string().optional(), + author: z.string().optional(), + collection: z.string().optional(), + source: z.string().optional(), + duration: z.number().optional(), }), run: runCapture, }; diff --git a/src/config/schema.ts b/src/config/schema.ts index 24b4712..57ee8bb 100644 --- a/src/config/schema.ts +++ b/src/config/schema.ts @@ -22,6 +22,10 @@ export const captureSchema = z.object({ canarySize: z.number().int().positive().default(20), igBatch: z.number().int().positive().default(250), ttBatch: z.number().int().positive().default(200), + /** Inbox subdirectory within the vault where `capture --watch` notes land. */ + inboxSubdir: z.string().default('00-inbox'), + /** PulseAudio monitor source for `capture --watch`. Null → auto-detect via `recmp3 sources`. */ + recmp3Source: z.string().nullable().default(null), }); export const ytdlpSchema = z.object({ diff --git a/src/version.ts b/src/version.ts index 0a19987..6e565e0 100644 --- a/src/version.ts +++ b/src/version.ts @@ -1,2 +1,2 @@ /** Kept in sync with package.json by the release process. */ -export const VERSION = '0.1.0'; +export const VERSION = '1.0.0'; diff --git a/test/watch.test.ts b/test/watch.test.ts new file mode 100644 index 0000000..e7512c4 --- /dev/null +++ b/test/watch.test.ts @@ -0,0 +1,168 @@ +import { describe, it, expect } from 'vitest'; +import { execFileSync } from 'node:child_process'; +import { fileURLToPath } from 'node:url'; +import { dirname, join } from 'node:path'; +import { mkdtempSync, writeFileSync, chmodSync, existsSync, readFileSync, readdirSync } from 'node:fs'; +import { tmpdir } from 'node:os'; +import { parse } from 'yaml'; +import { + pickMonitorSource, + deriveId, + buildWatchNote, + transcriptFromEnvelope, +} from '../src/capture/run/watch.js'; + +describe('pickMonitorSource', () => { + it('prefers the .monitor source over mic/default', () => { + const id = pickMonitorSource([ + { id: 'default', isDefault: true }, + { id: 'alsa_output.pci.stereo-fallback.monitor', label: 'system audio monitor' }, + { id: 'alsa_input.pci.stereo-fallback' }, + ]); + expect(id).toBe('alsa_output.pci.stereo-fallback.monitor'); + }); + + it('falls back to a label match when no id ends in .monitor', () => { + expect(pickMonitorSource([{ id: 'x', label: 'Stereo Monitor of Built-in Audio' }])).toBe('x'); + }); + + it('returns null when nothing looks like a monitor', () => { + expect(pickMonitorSource([{ id: 'default' }, { id: 'mic' }])).toBeNull(); + }); +}); + +describe('deriveId', () => { + it('extracts an Instagram shortcode from reel/p/tv URLs', () => { + expect(deriveId('instagram', 'https://www.instagram.com/reel/Cabc_123/')).toBe('Cabc_123'); + expect(deriveId('instagram', 'https://www.instagram.com/p/XYZ-9/')).toBe('XYZ-9'); + }); + + it('extracts a TikTok video id', () => { + expect(deriveId('tiktok', 'https://www.tiktok.com/@user/video/7412345678901234567')).toBe( + '7412345678901234567', + ); + }); + + it('falls back to a timestamp id when the URL has no recognizable id', () => { + expect(deriveId('instagram', 'https://example.com/whatever')).toMatch(/^watch-/); + }); +}); + +describe('transcriptFromEnvelope', () => { + it('reads transcript text + language + duration from a recmp3 record envelope', () => { + const env = JSON.stringify({ + ok: true, + command: 'record', + data: { durationSec: 30, transcription: { text: ' hi there ', language: 'en', durationSec: 28 } }, + }); + expect(transcriptFromEnvelope(env)).toEqual({ text: 'hi there', language: 'en', durationSec: 28 }); + }); + + it('throws E_RECMP3_PARSE on non-JSON output', () => { + expect(() => transcriptFromEnvelope('not json')).toThrow(/JSON envelope/); + }); + + it('tolerates a missing transcription block', () => { + expect(transcriptFromEnvelope('{"data":{}}')).toEqual({ + text: '', + language: undefined, + durationSec: undefined, + }); + }); +}); + +describe('buildWatchNote', () => { + it('produces a recmp3-whisper Type-B note for instagram', () => { + const { filename, markdown } = buildWatchNote({ + platform: 'instagram', + url: 'https://www.instagram.com/reel/ABC123/', + author: '@cooluser', + collection: 'vibecoding', + transcript: 'This is the spoken transcript.', + language: 'en', + durationSeconds: 41, + createdDate: '2026-06-03', + }); + const fm = parse(markdown.split('---')[1] as string) as Record; + expect(fm.type).toBe('resource'); + expect(fm.subtype).toBe('social-capture'); + expect(fm.source).toBe('instagram'); + expect(fm.transcript_source).toBe('recmp3-whisper'); + expect(fm.shortcode).toBe('ABC123'); + expect(fm.author).toBe('cooluser'); // @ stripped + expect(fm.author_url).toBe('https://www.instagram.com/cooluser/'); + expect(markdown).toContain('This is the spoken transcript.'); + expect(filename).toBe('cooluser-ABC123.md'); + }); + + it('builds a tiktok author_url and video_id note', () => { + const { markdown } = buildWatchNote({ + platform: 'tiktok', + url: 'https://www.tiktok.com/@dude/video/7412345678901234567', + author: 'dude', + transcript: 'tok transcript', + createdDate: '2026-06-03', + }); + const fm = parse(markdown.split('---')[1] as string) as Record; + expect(fm.source).toBe('tiktok'); + expect(fm.video_id).toBe('7412345678901234567'); + expect(fm.author_url).toBe('https://www.tiktok.com/@dude'); + }); +}); + +// ── Headless integration: stub recmp3, run the built CLI end-to-end ────────── +// Uses a shebang stub script; skipped on Windows where that isn't executable. +const CLI = join(dirname(fileURLToPath(import.meta.url)), '..', 'dist', 'cli.js'); + +describe.skipIf(process.platform === 'win32')('capture --watch (built CLI, stubbed recmp3)', () => { + it('records via recmp3 stub and files one note into the vault inbox', () => { + const work = mkdtempSync(join(tmpdir(), 'magnus-watch-')); + const vault = join(work, 'vault'); + const stub = join(work, 'recmp3-stub.mjs'); + + // Minimal recmp3 that emits a record envelope on stdout regardless of args. + writeFileSync( + stub, + `#!/usr/bin/env node +process.stdout.write(JSON.stringify({ + ok: true, command: 'record', schemaVersion: 1, + data: { audioPath: '/tmp/x.wav', durationSec: 12, + transcription: { text: 'Stubbed whisper transcript.', provider: 'groq', model: 'whisper', language: 'en', durationSec: 12, latencyMs: 5, segments: [] } } +}) + '\\n'); +`, + 'utf8', + ); + chmodSync(stub, 0o755); + + const cfg = mkdtempSync(join(tmpdir(), 'magnus-cfg-')); + const out = execFileSync( + 'node', + [ + CLI, 'capture', 'tiktok', '--watch', + '--url', 'https://www.tiktok.com/@stubuser/video/7400000000000000000', + '--author', 'stubuser', + '--collection', 'vibecoding', + '--source', 'fake.monitor', + '--duration', '1', + '--vault', vault, + '--json', + '--config', cfg, + ], + { input: '', encoding: 'utf8', env: { ...process.env, RECMP3_BIN: stub, NO_COLOR: '1' } }, + ); + + const envelope = JSON.parse(out) as { ok: boolean; data: { processed: number } }; + expect(envelope.ok).toBe(true); + expect(envelope.data.processed).toBe(1); + + const inbox = join(vault, '00-inbox'); + expect(existsSync(inbox)).toBe(true); + const notes = readdirSync(inbox).filter((f) => f.endsWith('.md')); + expect(notes).toHaveLength(1); + const md = readFileSync(join(inbox, notes[0] as string), 'utf8'); + expect(md).toContain('transcript_source: recmp3-whisper'); + expect(md).toContain('Stubbed whisper transcript.'); + // yaml quotes the 19-digit id to keep it a string (not a lossy float). + expect(md).toMatch(/video_id: "?7400000000000000000"?/); + }); +}); From 547b99eb0c53f9f674818cbb5986aeb0b6a14234 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 3 Jun 2026 15:48:30 +0000 Subject: [PATCH 6/6] chore(deps): bump actions/setup-node from 4.4.0 to 6.4.0 Bumps [actions/setup-node](https://github.com/actions/setup-node) from 4.4.0 to 6.4.0. - [Release notes](https://github.com/actions/setup-node/releases) - [Commits](https://github.com/actions/setup-node/compare/49933ea5288caeca8642d1e84afbd3f7d6820020...48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e) --- updated-dependencies: - dependency-name: actions/setup-node dependency-version: '6' dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- .github/workflows/ci.yml | 2 +- .github/workflows/release.yml | 2 +- .github/workflows/security-audit.yml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 59a02a7..84696c7 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -24,7 +24,7 @@ jobs: with: persist-credentials: false - - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 + - uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 with: node-version: ${{ matrix.node }} cache: npm diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 78fd774..c313a59 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -18,7 +18,7 @@ jobs: with: persist-credentials: false - - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 + - uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 with: node-version: '22' registry-url: https://registry.npmjs.org diff --git a/.github/workflows/security-audit.yml b/.github/workflows/security-audit.yml index 0bdf5d8..7636c73 100644 --- a/.github/workflows/security-audit.yml +++ b/.github/workflows/security-audit.yml @@ -17,7 +17,7 @@ jobs: with: persist-credentials: false - - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 + - uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 with: node-version: '22'