diff --git a/PLAN.md b/PLAN.md index 64b3122..04ef956 100644 --- a/PLAN.md +++ b/PLAN.md @@ -125,6 +125,7 @@ Daily quota window alignment with early dummy commands. ### v0.3.0 - Queue Watcher & Project Automation **Target: Q1 2026** **Priority: High** +**Status: md-queue library complete ✅ | Integration in progress** ### Queue-Based Session Orchestration @@ -132,10 +133,49 @@ Transform ccremote into a generic queue watcher that automatically spawns Claude **Use Case:** Any project that needs automated async Claude processing based on a simple queue system. -#### Core Features +#### md-queue Library ✅ IMPLEMENTED +**Status: Complete and ready for integration** + +A markdown-based queue system with YAML frontmatter for state persistence. Works with both Bun and Node. + +**Location:** `src/md-queue/` + +**Key Features:** +- **State machine:** `pending → processing → done/error` with reprocessing support +- **Soft locks:** `host:pid:timestamp` format with stale detection (5min timeout) +- **Atomic writes:** `.tmp` → rename pattern for sync safety +- **Reconciliation:** Directory sweeps to find pending/stale items +- **Concurrency control:** Configurable parallel processing +- **Reprocessing:** Track `previous_attempts[]` with reasons + +**Core Classes:** +- `AssetManager` - File operations with atomic writes +- `LockManager` - Soft lock coordination across workers +- `StateManager` - State machine transitions +- `Reconciler` - Directory sweeps and reconciliation +- `Processor` - Claim → Execute → Update workflow + +**API:** +```typescript +import { createQueue } from './md-queue'; + +const queue = createQueue({ + basePath: '/path/to/project', + lockTimeout: 5 * 60 * 1000, + maxRetries: 3 +}); + +// Find and process pending items +const pending = await queue.reconciler.findPending('_q/high'); +await queue.processor.processItem(item, async (item) => { + // Spawn Claude session, etc. +}); +``` + +#### Queue Watcher Features (To Be Integrated) **1. Queue Folder Monitoring** -- Watch `_q/high/`, `_q/medium/`, `_q/low/` folders in project directory (CWD) +- Watch `_q/high/`, `_q/medium/`, `_q/low/` folders using md-queue reconciler - Support both files (.md) and folders (for multi-file items like transaction exports) - Priority-based processing schedules: - High: Every 10 seconds @@ -143,19 +183,21 @@ Transform ccremote into a generic queue watcher that automatically spawns Claude - Low: Every 15 minutes **2. Automatic Session Spawning** +- Use md-queue processor to claim and process items - Spawn Claude Code sessions with custom prompts describing queue contents - Use `--permission-mode acceptEdits` flag - Session naming: `q-{priority}-{timestamp}` - Discord integration for session tracking **3. HTTP API (Fastify)** -- `POST /queue` - Create queue items (for Custom GPTs, webhooks) -- `GET /queue/status` - Get item counts per priority -- `GET /queue/items/:priority` - List items in priority folder +- `POST /queue` - Create queue items using md-queue AssetManager +- `GET /queue/status` - Get item counts via md-queue reconciler +- `GET /queue/items/:priority` - List items using md-queue filters - Bearer token authentication via env var - Customizable port (default: 3000) **4. Queue Monitoring** +- Use md-queue reconciler for stale lock detection - Session timeout detection (default: 10 minutes) - High-priority backlog alerts (>10 items) - Discord notifications for queue status @@ -179,21 +221,27 @@ ccremote monitor #### Implementation Details **New Files:** -- `src/managers/QueueManager.ts` - Queue watcher logic +- `src/managers/QueueManager.ts` - Queue watcher using md-queue reconciler/processor - `src/managers/QueueMonitor.ts` - Backlog & timeout monitoring -- `src/api/server.ts` - Fastify HTTP API +- `src/api/server.ts` - Fastify HTTP API using md-queue AssetManager - `src/commands/watch.ts` - Watch command - `src/commands/queue.ts` - Queue status/process commands - `src/commands/monitor.ts` - Monitoring command +**Existing Infrastructure (md-queue):** +- `src/md-queue/` - Complete queue library (AssetManager, LockManager, StateManager, Reconciler, Processor) +- Ready for integration with queue watcher + **Key Principles:** - Works with any project that has `_q/` folder structure - Claude discovers CLAUDE.md naturally (no --prompt-file flag) - Custom prompts describe queue contents dynamically - CWD-based operation (no configuration needed for project path) +- md-queue handles all state persistence and coordination **Integration Points:** - Extends existing SessionManager for spawning +- Uses md-queue for all queue operations (find, claim, process) - Uses existing Discord bot for notifications - Builds on current tmux integration - Compatible with quota scheduling system @@ -228,11 +276,20 @@ Claude processes, moves to `_q/archive/{priority}/`, updates project files. --- **Dependencies:** -- Fastify (HTTP server) -- yaml (for frontmatter parsing) +- ✅ yaml v2.8.1 (installed - for frontmatter parsing) +- ✅ md-queue library (implemented - core queue infrastructure) +- Fastify (HTTP server) - to be added - Existing ccremote infrastructure -**Estimated Effort:** 2-3 weeks +**Estimated Effort:** +- ✅ md-queue library: Complete (~1 week) +- Remaining: 1-2 weeks for queue watcher integration + +**Progress:** +- ✅ md-queue library implemented and typechecking +- ⏳ QueueManager integration pending +- ⏳ HTTP API pending +- ⏳ Discord integration pending **Success Criteria:** - Queue watcher runs reliably in background @@ -240,6 +297,7 @@ Claude processes, moves to `_q/archive/{priority}/`, updates project files. - HTTP API accepts external queue items - Discord shows queue processing status - Works with multiple concurrent projects +- md-queue handles all state management correctly --- diff --git a/package-lock.json b/package-lock.json index d8c980e..6692418 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13,6 +13,8 @@ "discord.js": "^14.14.1", "dotenv": "^17.2.2", "pm2": "^6.0.10", + "update-notifier": "^7.3.1", + "yaml": "^2.8.1", "zod": "^3.25.67" }, "bin": { @@ -23,18 +25,19 @@ "@ryoppippi/eslint-config": "^0.3.7", "@types/bun": "^1.2.20", "@types/node": "^20.11.16", + "@types/update-notifier": "^6.0.8", "bumpp": "^10.2.3", "eslint": "^9.33.0", "eslint-plugin-format": "^1.0.1", "gunshi": "^0.26.3", "publint": "^0.3.12", - "tsdown": "^0.14.1", + "tsdown": "^0.15.7", "tsx": "^4.7.0", "typescript": "^5.3.3", "vitest": "^3.2.4" }, "engines": { - "node": ">=20.19.4" + "node": ">=18.0.0" } }, "node_modules/@antfu/eslint-config": { @@ -168,14 +171,14 @@ } }, "node_modules/@babel/generator": { - "version": "7.28.3", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.28.3.tgz", - "integrity": "sha512-3lSpxGgvnmZznmBkCRnVREPUFJv2wrv9iAoFDvADJc0ypmdOxdUtcLeBgBJ6zE0PMeTKnxeQzyk0xTBq4Ep7zw==", + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.28.5.tgz", + "integrity": "sha512-3EwLFhZ38J4VyIP6WNtt2kUdW9dokXA9Cr4IVIFHuCpZ3H8/YFOl5JjZHisrn1fATPBmKKqXzDFvh9fUwHz6CQ==", "dev": true, "license": "MIT", "dependencies": { - "@babel/parser": "^7.28.3", - "@babel/types": "^7.28.2", + "@babel/parser": "^7.28.5", + "@babel/types": "^7.28.5", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" @@ -195,9 +198,9 @@ } }, "node_modules/@babel/helper-validator-identifier": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.27.1.tgz", - "integrity": "sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow==", + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", + "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", "dev": true, "license": "MIT", "engines": { @@ -205,13 +208,13 @@ } }, "node_modules/@babel/parser": { - "version": "7.28.4", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.4.tgz", - "integrity": "sha512-yZbBqeM6TkpP9du/I2pUZnJsRMGGvOuIrhjzC1AwHwW+6he4mni6Bp/m8ijn0iOuZuPI2BfkCoSRunpyjnrQKg==", + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.5.tgz", + "integrity": "sha512-KKBU1VGYR7ORr3At5HAtUQ+TV3SzRCXmA/8OdDZiLDBIZxVyzXuztPjfLd3BV1PRAQGCMWWSHYhL0F8d5uHBDQ==", "dev": true, "license": "MIT", "dependencies": { - "@babel/types": "^7.28.4" + "@babel/types": "^7.28.5" }, "bin": { "parser": "bin/babel-parser.js" @@ -221,14 +224,14 @@ } }, "node_modules/@babel/types": { - "version": "7.28.4", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.4.tgz", - "integrity": "sha512-bkFqkLhh3pMBUQQkpVgWDWq/lqzc2678eUyDlTBhRqhCHFguYYGM0Efga7tYk4TogG/3x0EEl66/OQ+WGbWB/Q==", + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.5.tgz", + "integrity": "sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA==", "dev": true, "license": "MIT", "dependencies": { "@babel/helper-string-parser": "^7.27.1", - "@babel/helper-validator-identifier": "^7.27.1" + "@babel/helper-validator-identifier": "^7.28.5" }, "engines": { "node": ">=6.9.0" @@ -406,9 +409,9 @@ "license": "MIT" }, "node_modules/@emnapi/core": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.5.0.tgz", - "integrity": "sha512-sbP8GzB1WDzacS8fgNPpHlp6C9VZe+SJP3F90W9rLemaQj2PzIuTEl1qDOYQf58YIpyjViI24y9aPWCjEzY2cg==", + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.6.0.tgz", + "integrity": "sha512-zq/ay+9fNIJJtJiZxdTnXS20PllcYMX3OE23ESc4HK/bdYu3cOWYVhsOhVnXALfU/uqJIxn5NBPd9z4v+SfoSg==", "dev": true, "license": "MIT", "optional": true, @@ -418,9 +421,9 @@ } }, "node_modules/@emnapi/runtime": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.5.0.tgz", - "integrity": "sha512-97/BJ3iXHww3djw6hYIfErCZFee7qCtrneuLa20UXFCOTCfBM2cvQHjWJ2EG0s0MtdNwInarqCTz35i4wWXHsQ==", + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.6.0.tgz", + "integrity": "sha512-obtUmAHTMjll499P+D9A3axeJFlhdjOWdKUNs/U6QIGT7V5RjcUW1xToAzjvmgTSQhDbYn/NwfTRoJcQ2rNBxA==", "dev": true, "license": "MIT", "optional": true, @@ -1230,9 +1233,9 @@ "license": "MIT" }, "node_modules/@jridgewell/trace-mapping": { - "version": "0.3.30", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.30.tgz", - "integrity": "sha512-GQ7Nw5G2lTu/BtHTKfXhKHok2WGetd4XYcVKGx00SjAk8GMwgJM3zr6zORiPGuOE+/vkc90KtTosSSvaCjKb2Q==", + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", "dev": true, "license": "MIT", "dependencies": { @@ -1241,16 +1244,16 @@ } }, "node_modules/@napi-rs/wasm-runtime": { - "version": "0.2.12", - "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-0.2.12.tgz", - "integrity": "sha512-ZVWUcfwY4E/yPitQJl481FjFo3K22D6qF0DuFH6Y/nbnE11GY5uguDxZMGXPQ8WQ0128MXQD7TnfHyK4oWoIJQ==", + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.0.7.tgz", + "integrity": "sha512-SeDnOO0Tk7Okiq6DbXmmBODgOAb9dp9gjlphokTUxmt8U3liIP1ZsozBahH69j/RJv+Rfs6IwUKHTgQYJ/HBAw==", "dev": true, "license": "MIT", "optional": true, "dependencies": { - "@emnapi/core": "^1.4.3", - "@emnapi/runtime": "^1.4.3", - "@tybys/wasm-util": "^0.10.0" + "@emnapi/core": "^1.5.0", + "@emnapi/runtime": "^1.5.0", + "@tybys/wasm-util": "^0.10.1" } }, "node_modules/@nodelib/fs.scandir": { @@ -1291,20 +1294,10 @@ "node": ">= 8" } }, - "node_modules/@oxc-project/runtime": { - "version": "0.72.3", - "resolved": "https://registry.npmjs.org/@oxc-project/runtime/-/runtime-0.72.3.tgz", - "integrity": "sha512-FtOS+0v7rZcnjXzYTTqv1vu/KDptD1UztFgoZkYBGe/6TcNFm+SP/jQoLvzau1SPir95WgDOBOUm2Gmsm+bQag==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, "node_modules/@oxc-project/types": { - "version": "0.72.3", - "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.72.3.tgz", - "integrity": "sha512-CfAC4wrmMkUoISpQkFAIfMVvlPfQV3xg7ZlcqPXPOIMQhdKIId44G8W0mCPgtpWdFFAyJ+SFtiM+9vbyCkoVng==", + "version": "0.95.0", + "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.95.0.tgz", + "integrity": "sha512-vACy7vhpMPhjEJhULNxrdR0D943TkA/MigMpJCHmBHvMXxRStRi/dPtTlfQ3uDwWSzRpT8z+7ImjZVf8JWBocQ==", "dev": true, "license": "MIT", "funding": { @@ -1602,6 +1595,47 @@ "debug": "^4.3.1" } }, + "node_modules/@pnpm/config.env-replace": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@pnpm/config.env-replace/-/config.env-replace-1.1.0.tgz", + "integrity": "sha512-htyl8TWnKL7K/ESFa1oW2UB5lVDxuF5DpM7tBi6Hu2LNL3mWkIzNLG6N4zoCUP1lCKNxWy/3iu8mS8MvToGd6w==", + "license": "MIT", + "engines": { + "node": ">=12.22.0" + } + }, + "node_modules/@pnpm/network.ca-file": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@pnpm/network.ca-file/-/network.ca-file-1.0.2.tgz", + "integrity": "sha512-YcPQ8a0jwYU9bTdJDpXjMi7Brhkr1mXsXrUJvjqM2mQDgkRiz8jFaQGOdaLxgjtUfQgZhKy/O3cG/YwmgKaxLA==", + "license": "MIT", + "dependencies": { + "graceful-fs": "4.2.10" + }, + "engines": { + "node": ">=12.22.0" + } + }, + "node_modules/@pnpm/network.ca-file/node_modules/graceful-fs": { + "version": "4.2.10", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.10.tgz", + "integrity": "sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA==", + "license": "ISC" + }, + "node_modules/@pnpm/npm-conf": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/@pnpm/npm-conf/-/npm-conf-2.3.1.tgz", + "integrity": "sha512-c83qWb22rNRuB0UaVCI0uRPNRr8Z0FWnEIvT47jiHAmOIUHbBOg5XvV7pM5x+rKn9HRpjxquDbXYSXr3fAKFcw==", + "license": "MIT", + "dependencies": { + "@pnpm/config.env-replace": "^1.1.0", + "@pnpm/network.ca-file": "^1.0.1", + "config-chain": "^1.1.11" + }, + "engines": { + "node": ">=12" + } + }, "node_modules/@publint/pack": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/@publint/pack/-/pack-0.1.2.tgz", @@ -1628,10 +1662,27 @@ "url": "https://github.com/sponsors/sxzz" } }, + "node_modules/@rolldown/binding-android-arm64": { + "version": "1.0.0-beta.44", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.0-beta.44.tgz", + "integrity": "sha512-g9ejDOehJFhxC1DIXQuZQ9bKv4lRDioOTL42cJjFjqKPl1L7DVb9QQQE1FxokGEIMr6FezLipxwnzOXWe7DNPg==", + "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.0-beta.13-commit.024b632", - "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.0.0-beta.13-commit.024b632.tgz", - "integrity": "sha512-dkMfisSkfS3Rbyj+qL6HFQmGNlwCKhkwH7pKg2oVhzpEQYnuP0YIUGV4WXsTd3hxoHNgs+LQU5LJe78IhE2q6g==", + "version": "1.0.0-beta.44", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.0.0-beta.44.tgz", + "integrity": "sha512-PxAW1PXLPmCzfhfKIS53kwpjLGTUdIfX4Ht+l9mj05C3lYCGaGowcNsYi2rdxWH24vSTmeK+ajDNRmmmrK0M7g==", "cpu": [ "arm64" ], @@ -1640,12 +1691,15 @@ "optional": true, "os": [ "darwin" - ] + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } }, "node_modules/@rolldown/binding-darwin-x64": { - "version": "1.0.0-beta.13-commit.024b632", - "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.0.0-beta.13-commit.024b632.tgz", - "integrity": "sha512-qbtggWQ+iiwls7A+M9RymMcMwga/LscZ+XamWNhDVzHPVEnv0bYePN7Kh+kPQDNdYxM+6xhZyZWBkMdLj1MNqg==", + "version": "1.0.0-beta.44", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.0.0-beta.44.tgz", + "integrity": "sha512-/CtQqs1oO9uSb5Ju60rZvsdjE7Pzn8EK2ISAdl2jedjMzeD/4neNyCbwyJOAPzU+GIQTZVyrFZJX+t7HXR1R/g==", "cpu": [ "x64" ], @@ -1654,12 +1708,15 @@ "optional": true, "os": [ "darwin" - ] + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } }, "node_modules/@rolldown/binding-freebsd-x64": { - "version": "1.0.0-beta.13-commit.024b632", - "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.0.0-beta.13-commit.024b632.tgz", - "integrity": "sha512-GrSy4boSJd7dR1fP0chqcxTdbDYa+KaRuffqZXZjh4aTaSuCEyuH0lmciDeJKOXBJaBoPFuisx7+Q/WDWdW0ng==", + "version": "1.0.0-beta.44", + "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.0.0-beta.44.tgz", + "integrity": "sha512-V5Q5W9c4+2GJ4QabmjmVV6alY97zhC/MZBaLkDtHwGy3qwzbM4DYgXUbun/0a8AH5hGhuU27tUIlYz6ZBlvgOA==", "cpu": [ "x64" ], @@ -1668,12 +1725,15 @@ "optional": true, "os": [ "freebsd" - ] + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } }, "node_modules/@rolldown/binding-linux-arm-gnueabihf": { - "version": "1.0.0-beta.13-commit.024b632", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.0.0-beta.13-commit.024b632.tgz", - "integrity": "sha512-AcTYqfzSbTsR5pxOdZeUR+7JzWojQSFcLQ8SrdmrQBOmubvMNhnObDJ+OqEFql8TrLhqRPJ+nzfdENGjVmMxEw==", + "version": "1.0.0-beta.44", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.0.0-beta.44.tgz", + "integrity": "sha512-X6adjkHeFqKsTU0FXdNN9HY4LDozPqIfHcnXovE5RkYLWIjMWuc489mIZ6iyhrMbCqMUla9IOsh5dvXSGT9o9A==", "cpu": [ "arm" ], @@ -1682,12 +1742,15 @@ "optional": true, "os": [ "linux" - ] + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } }, "node_modules/@rolldown/binding-linux-arm64-gnu": { - "version": "1.0.0-beta.13-commit.024b632", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.0.0-beta.13-commit.024b632.tgz", - "integrity": "sha512-Z2kfzCFGZcksDqXHiOddcPuMkEJNLG8wgBW3FmK8ucmiwIrYz4goqQcHvUkQ+n3FKKyq2h67EuBHHCXi4CnDWg==", + "version": "1.0.0-beta.44", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.0.0-beta.44.tgz", + "integrity": "sha512-kRRKGZI4DXWa6ANFr3dLA85aSVkwPdgXaRjfanwY84tfc3LncDiIjyWCb042e3ckPzYhHSZ3LmisO+cdOIYL6Q==", "cpu": [ "arm64" ], @@ -1696,12 +1759,15 @@ "optional": true, "os": [ "linux" - ] + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } }, "node_modules/@rolldown/binding-linux-arm64-musl": { - "version": "1.0.0-beta.13-commit.024b632", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.0.0-beta.13-commit.024b632.tgz", - "integrity": "sha512-2YOaZ6vsE6NDpj6PTo2nBRu/bjMSkhRG80oQahX0bt+pvigaWT3x0Nw522fT9FOuhvKhzsqaFhtVl8SFYcXYTQ==", + "version": "1.0.0-beta.44", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.0.0-beta.44.tgz", + "integrity": "sha512-hMtiN9xX1NhxXBa2U3Up4XkVcsVp2h73yYtMDY59z9CDLEZLrik9RVLhBL5QtoX4zZKJ8HZKJtWuGYvtmkCbIQ==", "cpu": [ "arm64" ], @@ -1710,12 +1776,15 @@ "optional": true, "os": [ "linux" - ] + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } }, "node_modules/@rolldown/binding-linux-x64-gnu": { - "version": "1.0.0-beta.13-commit.024b632", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.0.0-beta.13-commit.024b632.tgz", - "integrity": "sha512-bqb+MXYXcRTW9z26VmqttxDGYmhudne1jt1jvjbkIqDomjIJPCY6Gu6dQ9nPk561Zs2c5MB737KTc+HJe/EapA==", + "version": "1.0.0-beta.44", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.0.0-beta.44.tgz", + "integrity": "sha512-rd1LzbpXQuR8MTG43JB9VyXDjG7ogSJbIkBpZEHJ8oMKzL6j47kQT5BpIXrg3b5UVygW9QCI2fpFdMocT5Kudg==", "cpu": [ "x64" ], @@ -1724,12 +1793,15 @@ "optional": true, "os": [ "linux" - ] + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } }, "node_modules/@rolldown/binding-linux-x64-musl": { - "version": "1.0.0-beta.13-commit.024b632", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.0.0-beta.13-commit.024b632.tgz", - "integrity": "sha512-oynj2ltmiV1gMYiuJ/HHqmRgfk7+a0tk9RoLt0xRSwQXPHWPMftcZYJh8r2pi0/bR/AGypDfpY9fsYcULa2Hpw==", + "version": "1.0.0-beta.44", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.0.0-beta.44.tgz", + "integrity": "sha512-qI2IiPqmPRW25exXkuQr3TlweCDc05YvvbSDRPCuPsWkwb70dTiSoXn8iFxT4PWqTi71wWHg1Wyta9PlVhX5VA==", "cpu": [ "x64" ], @@ -1738,12 +1810,32 @@ "optional": true, "os": [ "linux" - ] + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-openharmony-arm64": { + "version": "1.0.0-beta.44", + "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.0.0-beta.44.tgz", + "integrity": "sha512-+vHvEc1pL5iJRFlldLC8mjm6P4Qciyfh2bh5ZI6yxDQKbYhCHRKNURaKz1mFcwxhVL5YMYsLyaqM3qizVif9MQ==", + "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.0-beta.13-commit.024b632", - "resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.0.0-beta.13-commit.024b632.tgz", - "integrity": "sha512-7bOTebAR3zVY/TZTaaMnD6kGedlfPLlgcpD5Kuo02EHFgJnf02HpOvqRdzW39+mI/mDOf5K0JOULiXjgdKw5Zg==", + "version": "1.0.0-beta.44", + "resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.0.0-beta.44.tgz", + "integrity": "sha512-XSgLxRrtFj6RpTeMYmmQDAwHjKseYGKUn5LPiIdW4Cq+f5SBSStL2ToBDxkbdxKPEbCZptnLPQ/nfKcAxrC8Xg==", "cpu": [ "wasm32" ], @@ -1751,16 +1843,16 @@ "license": "MIT", "optional": true, "dependencies": { - "@napi-rs/wasm-runtime": "^0.2.10" + "@napi-rs/wasm-runtime": "^1.0.7" }, "engines": { - "node": ">=14.21.3" + "node": ">=14.0.0" } }, "node_modules/@rolldown/binding-win32-arm64-msvc": { - "version": "1.0.0-beta.13-commit.024b632", - "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.0-beta.13-commit.024b632.tgz", - "integrity": "sha512-bwUSHGdMFf2UmEfEqKBRdVW2Qt2Nhmk+4H8lSDsG4lMx8aJ2nAVK0Vem1skmuOZJYocJEe4lJZBxl8q8SAAgAg==", + "version": "1.0.0-beta.44", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.0-beta.44.tgz", + "integrity": "sha512-cF1LJdDIX02cJrFrX3wwQ6IzFM7I74BYeKFkzdcIA4QZ0+2WA7/NsKIgjvrunupepWb1Y6PFWdRlHSaz5AW1Wg==", "cpu": [ "arm64" ], @@ -1769,12 +1861,15 @@ "optional": true, "os": [ "win32" - ] + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } }, "node_modules/@rolldown/binding-win32-ia32-msvc": { - "version": "1.0.0-beta.13-commit.024b632", - "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-ia32-msvc/-/binding-win32-ia32-msvc-1.0.0-beta.13-commit.024b632.tgz", - "integrity": "sha512-QG+EWXIa7IcQgpVF6zpxjAikc82NP5Zmu2GjoOiRRWFHQNLaEZx9/WNt/k6ncRA2yI0+f9vNdq9G34Z0pW+Fwg==", + "version": "1.0.0-beta.44", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-ia32-msvc/-/binding-win32-ia32-msvc-1.0.0-beta.44.tgz", + "integrity": "sha512-5uaJonDafhHiMn+iEh7qUp3QQ4Gihv3lEOxKfN8Vwadpy0e+5o28DWI42DpJ9YBYMrVy4JOWJ/3etB/sptpUwA==", "cpu": [ "ia32" ], @@ -1783,12 +1878,15 @@ "optional": true, "os": [ "win32" - ] + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } }, "node_modules/@rolldown/binding-win32-x64-msvc": { - "version": "1.0.0-beta.13-commit.024b632", - "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.0.0-beta.13-commit.024b632.tgz", - "integrity": "sha512-40gOnsAJOP/jqnAgkYsj7kQD1+U5ZJcRA4hHeL6ouCsqMFIqS4bmOhUYDOM3O9dDawmrG7zadY+gu1FKtMix9g==", + "version": "1.0.0-beta.44", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.0.0-beta.44.tgz", + "integrity": "sha512-vsqhWAFJkkmgfBN/lkLCWTXF1PuPhMjfnAyru48KvF7mVh2+K7WkKYHezF3Fjz4X/mPScOcIv+g6cf6wnI6eWg==", "cpu": [ "x64" ], @@ -1797,12 +1895,15 @@ "optional": true, "os": [ "win32" - ] + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } }, "node_modules/@rolldown/pluginutils": { - "version": "1.0.0-beta.13-commit.024b632", - "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.13-commit.024b632.tgz", - "integrity": "sha512-9/h9ID36/orsoJx8kd2E/wxQ+bif87Blg/7LAu3t9wqfXPPezu02MYR96NOH9G/Aiwr8YgdaKfDE97IZcg/MTw==", + "version": "1.0.0-beta.44", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.44.tgz", + "integrity": "sha512-g6eW7Zwnr2c5RADIoqziHoVs6b3W5QTQ4+qbpfjbkMJ9x+8Og211VW/oot2dj9dVwaK/UyC6Yo+02gV+wWQVNg==", "dev": true, "license": "MIT" }, @@ -2211,9 +2312,9 @@ "license": "MIT" }, "node_modules/@tybys/wasm-util": { - "version": "0.10.0", - "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.0.tgz", - "integrity": "sha512-VyyPYFlOMNylG45GoAe0xDoLwWuowvf92F9kySqzYh8vmYm7D2u4iUJKa1tOUpS70Ku13ASrOkS4ScXFsTaCNQ==", + "version": "0.10.1", + "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.1.tgz", + "integrity": "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==", "dev": true, "license": "MIT", "optional": true, @@ -2241,6 +2342,13 @@ "@types/deep-eql": "*" } }, + "node_modules/@types/configstore": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/@types/configstore/-/configstore-6.0.2.tgz", + "integrity": "sha512-OS//b51j9uyR3zvwD04Kfs5kHpve2qalQ18JhY/ho3voGYUTPLEG90/ocfKPI48hyHH8T04f7KEEbK6Ue60oZQ==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/debug": { "version": "4.1.12", "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.12.tgz", @@ -2316,6 +2424,17 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/update-notifier": { + "version": "6.0.8", + "resolved": "https://registry.npmjs.org/@types/update-notifier/-/update-notifier-6.0.8.tgz", + "integrity": "sha512-IlDFnfSVfYQD+cKIg63DEXn3RFmd7W1iYtKQsJodcHK9R1yr8aKbKaPKfBxzPpcHCq2DU8zUq4PIPmy19Thjfg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/configstore": "*", + "boxen": "^7.1.1" + } + }, "node_modules/@types/ws": { "version": "8.18.1", "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.1.tgz", @@ -2859,6 +2978,56 @@ "amp": "0.3.1" } }, + "node_modules/ansi-align": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/ansi-align/-/ansi-align-3.0.1.tgz", + "integrity": "sha512-IOfwwBF5iczOjp/WeY4YxyjqAFMQoZufdQWDd19SEExbVLNXqvpzSJ/M7Za4/sCPmQ0+GRquoA7bGcINcxew6w==", + "license": "ISC", + "dependencies": { + "string-width": "^4.1.0" + } + }, + "node_modules/ansi-align/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-align/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "license": "MIT" + }, + "node_modules/ansi-align/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-align/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/ansi-colors": { "version": "4.1.3", "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.3.tgz", @@ -2868,6 +3037,18 @@ "node": ">=6" } }, + "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/ansi-styles": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", @@ -2884,9 +3065,9 @@ } }, "node_modules/ansis": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/ansis/-/ansis-4.1.0.tgz", - "integrity": "sha512-BGcItUBWSMRgOCe+SVZJ+S7yTRG0eGt9cXAHev72yuGcY23hnLA7Bky5L/xLyPINoSN95geovfBkqoTlNZYa7w==", + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/ansis/-/ansis-4.2.0.tgz", + "integrity": "sha512-HqZ5rWlFjGiV0tDm3UxxgNRqsOTniqoKZu0pIAfh7TZQMGuZK+hH0drySty0si0QXj1ieop4+SkSfPZBPPkHig==", "dev": true, "license": "ISC", "engines": { @@ -2965,17 +3146,17 @@ } }, "node_modules/ast-kit": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ast-kit/-/ast-kit-2.1.2.tgz", - "integrity": "sha512-cl76xfBQM6pztbrFWRnxbrDm9EOqDr1BF6+qQnnDZG2Co2LjyUktkN9GTJfBAfdae+DbT2nJf2nCGAdDDN7W2g==", + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ast-kit/-/ast-kit-2.1.3.tgz", + "integrity": "sha512-TH+b3Lv6pUjy/Nu0m6A2JULtdzLpmqF9x1Dhj00ZoEiML8qvVA9j1flkzTKNYgdEhWrjDwtWNpyyCUbfQe514g==", "dev": true, "license": "MIT", "dependencies": { - "@babel/parser": "^7.28.0", + "@babel/parser": "^7.28.4", "pathe": "^2.0.3" }, "engines": { - "node": ">=20.18.0" + "node": ">=20.19.0" }, "funding": { "url": "https://github.com/sponsors/sxzz" @@ -2999,6 +3180,15 @@ "integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==", "license": "MIT" }, + "node_modules/atomically": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/atomically/-/atomically-2.0.3.tgz", + "integrity": "sha512-kU6FmrwZ3Lx7/7y3hPS5QnbJfaohcIul5fGqf7ok+4KklIEk9tJ0C2IQPdacSbVUWv6zVHXEBWoWd6NrVMT7Cw==", + "dependencies": { + "stubborn-fs": "^1.2.5", + "when-exit": "^2.1.1" + } + }, "node_modules/balanced-match": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", @@ -3028,9 +3218,9 @@ } }, "node_modules/birpc": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/birpc/-/birpc-2.5.0.tgz", - "integrity": "sha512-VSWO/W6nNQdyP520F1mhf+Lc2f8pjGQOtoHHm7Ze8Go1kX7akpVIrtTa0fn+HB0QJEDVacl6aO08YE0PgXfdnQ==", + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/birpc/-/birpc-2.6.1.tgz", + "integrity": "sha512-LPnFhlDpdSH6FJhJyn4M0kFO7vtQ5iPw24FnG0y21q09xC7e8+1LeR31S1MAIrDAHp4m7aas4bEkTDTvMAtebQ==", "dev": true, "license": "MIT", "funding": { @@ -3050,6 +3240,42 @@ "dev": true, "license": "ISC" }, + "node_modules/boxen": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/boxen/-/boxen-7.1.1.tgz", + "integrity": "sha512-2hCgjEmP8YLWQ130n2FerGv7rYpfBmnmp9Uy2Le1vge6X3gZIfSmEzP5QTDElFxcvVcXlEn8Aq6MU/PZygIOog==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-align": "^3.0.1", + "camelcase": "^7.0.1", + "chalk": "^5.2.0", + "cli-boxes": "^3.0.0", + "string-width": "^5.1.2", + "type-fest": "^2.13.0", + "widest-line": "^4.0.1", + "wrap-ansi": "^8.1.0" + }, + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/boxen/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==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.17.0 || ^14.13 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, "node_modules/brace-expansion": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", @@ -3212,6 +3438,19 @@ "node": ">=6" } }, + "node_modules/camelcase": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-7.0.1.tgz", + "integrity": "sha512-xlx1yCK2Oc1APsPXDL2LdlNP6+uu8OCDdhOBSVT279M/S+y75O30C2VuD8T2ogdePBBl7PfPF4504tnLgX3zfw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/caniuse-lite": { "version": "1.0.30001741", "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001741.tgz", @@ -3377,6 +3616,18 @@ "node": ">=0.8.0" } }, + "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-tableau": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/cli-tableau/-/cli-tableau-2.0.1.tgz", @@ -3449,6 +3700,34 @@ "dev": true, "license": "MIT" }, + "node_modules/config-chain": { + "version": "1.1.13", + "resolved": "https://registry.npmjs.org/config-chain/-/config-chain-1.1.13.tgz", + "integrity": "sha512-qj+f8APARXHrM0hraqXYb2/bOVSV4PvJQlNZ/DVj0QrmNM2q2euizkeuVckQ57J+W0mRH6Hvi+k50M4Jul2VRQ==", + "license": "MIT", + "dependencies": { + "ini": "^1.3.4", + "proto-list": "~1.2.1" + } + }, + "node_modules/configstore": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/configstore/-/configstore-7.1.0.tgz", + "integrity": "sha512-N4oog6YJWbR9kGyXvS7jEykLDXIE2C0ILYqNBZBp9iwiJpoCBWYsuAdW6PPFn6w06jjnC+3JstVvWHO4cZqvRg==", + "license": "BSD-2-Clause", + "dependencies": { + "atomically": "^2.0.3", + "dot-prop": "^9.0.0", + "graceful-fs": "^4.2.11", + "xdg-basedir": "^5.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/consola": { "version": "3.4.2", "resolved": "https://registry.npmjs.org/consola/-/consola-3.4.2.tgz", @@ -3536,9 +3815,9 @@ "license": "MIT" }, "node_modules/debug": { - "version": "4.4.1", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz", - "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==", + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", "license": "MIT", "dependencies": { "ms": "^2.1.3" @@ -3576,6 +3855,15 @@ "node": ">=6" } }, + "node_modules/deep-extend": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", + "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==", + "license": "MIT", + "engines": { + "node": ">=4.0.0" + } + }, "node_modules/deep-is": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", @@ -3681,6 +3969,33 @@ "url": "https://github.com/discordjs/discord.js?sponsor" } }, + "node_modules/dot-prop": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/dot-prop/-/dot-prop-9.0.0.tgz", + "integrity": "sha512-1gxPBJpI/pcjQhKgIU91II6Wkay+dLcN3M6rf2uwP8hRur3HtQXjVrdAK3sjC0piaEuxzMwjXChcETiJl47lAQ==", + "license": "MIT", + "dependencies": { + "type-fest": "^4.18.2" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/dot-prop/node_modules/type-fest": { + "version": "4.41.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.41.0.tgz", + "integrity": "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==", + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/dotenv": { "version": "17.2.2", "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.2.2.tgz", @@ -3714,6 +4029,13 @@ } } }, + "node_modules/eastasianwidth": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", + "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", + "dev": true, + "license": "MIT" + }, "node_modules/electron-to-chromium": { "version": "1.5.214", "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.214.tgz", @@ -3721,7 +4043,14 @@ "dev": true, "license": "ISC" }, - "node_modules/empathic": { + "node_modules/emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "dev": true, + "license": "MIT" + }, + "node_modules/empathic": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/empathic/-/empathic-2.0.0.tgz", "integrity": "sha512-i6UzDscO/XfAcNYD75CfICkmfLedpyPDdozrLMmQc5ORaQcdMoc21OnlEylMIqI7U8eniKrPMxxtj8k0vhmJhA==", @@ -3830,6 +4159,18 @@ "node": ">=6" } }, + "node_modules/escape-goat": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-goat/-/escape-goat-4.0.0.tgz", + "integrity": "sha512-2Sd4ShcWxbx6OY1IHyla/CVNwvg7XwZVoXZHcSu9w9SReNP1EzzD5T8NWKIR38fIqEns9kDWKUQTXXAmlDrdPg==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/escape-string-regexp": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", @@ -4980,10 +5321,22 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/get-east-asian-width": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.4.0.tgz", + "integrity": "sha512-QZjmEOC+IT1uk6Rx0sX22V6uHWVwbdbxf1faPqJ1QhLdGgsRGCZoyaQBm/piRdJy/D2um6hM1UP7ZEeQ4EkP+Q==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/get-tsconfig": { - "version": "4.10.1", - "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.10.1.tgz", - "integrity": "sha512-auHyJ4AgMz7vgS8Hp3N6HXSmlMdUyhSUrfBF16w153rxtLIEOE+HGqaBppczZvnHLqQJfiHotCYpNhl0lUROFQ==", + "version": "4.13.0", + "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.13.0.tgz", + "integrity": "sha512-1VKTZJCwBrvbd+Wn3AOgQP/2Av+TfTCOlE4AcRJE72W1ksZXbAx8PPBR9RzgTeSPzlPMHrbANMH3LbltH73wxQ==", "dev": true, "license": "MIT", "dependencies": { @@ -5057,6 +5410,30 @@ "node": ">=10.13.0" } }, + "node_modules/global-directory": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/global-directory/-/global-directory-4.0.1.tgz", + "integrity": "sha512-wHTUcDUoZ1H5/0iVqEudYW4/kAlN5cZ3j/bXn0Dpbizl9iaUVeWSHqiOjsgk6OW2bkLclbBjzewBz6weQ1zA2Q==", + "license": "MIT", + "dependencies": { + "ini": "4.1.1" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/global-directory/node_modules/ini": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/ini/-/ini-4.1.1.tgz", + "integrity": "sha512-QQnnxNyfvmHFIsj7gkPcYymR8Jdw/o7mp5ZFihxn6h8Ci6fh3Dx4E1gPjpQEpIuPo9XVNY/ZUwh4BPMjGyL01g==", + "license": "ISC", + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, "node_modules/globals": { "version": "16.3.0", "resolved": "https://registry.npmjs.org/globals/-/globals-16.3.0.tgz", @@ -5081,7 +5458,6 @@ "version": "4.2.11", "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", - "dev": true, "license": "ISC" }, "node_modules/graphemer": { @@ -5290,6 +5666,15 @@ "node": ">=0.10.0" } }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/is-glob": { "version": "4.0.3", "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", @@ -5302,6 +5687,49 @@ "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-installed-globally": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-installed-globally/-/is-installed-globally-1.0.0.tgz", + "integrity": "sha512-K55T22lfpQ63N4KEN57jZUAaAYqYHEe8veb/TycJRk9DdSCLLcovXz/mL6mOnhQaZsQGwPhuFopdQIlqGSEjiQ==", + "license": "MIT", + "dependencies": { + "global-directory": "^4.0.1", + "is-path-inside": "^4.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-npm": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/is-npm/-/is-npm-6.1.0.tgz", + "integrity": "sha512-O2z4/kNgyjhQwVR1Wpkbfc19JIhggF97NZNCpWTnjH7kVcZMUrnut9XSN7txI7VdyIYk5ZatOq3zvSuWpU8hoA==", + "license": "MIT", + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "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", @@ -5311,6 +5739,18 @@ "node": ">=0.12.0" } }, + "node_modules/is-path-inside": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-4.0.0.tgz", + "integrity": "sha512-lJJV/5dYS+RcL8uQdBDW9c9uWFLLBNRyFhnAKXw5tVqLlKZ4RMGZKv+YQ/IA3OhD+RpbJa1LLFM1FQPGyIXvOA==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/isexe": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", @@ -5473,6 +5913,33 @@ "optional": true, "peer": true }, + "node_modules/ky": { + "version": "1.13.0", + "resolved": "https://registry.npmjs.org/ky/-/ky-1.13.0.tgz", + "integrity": "sha512-JeNNGs44hVUp2XxO3FY9WV28ymG7LgO4wju4HL/dCq1A8eKDcFgVrdCn1ssn+3Q/5OQilv5aYsL0DMt5mmAV9w==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sindresorhus/ky?sponsor=1" + } + }, + "node_modules/latest-version": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/latest-version/-/latest-version-9.0.0.tgz", + "integrity": "sha512-7W0vV3rqv5tokqkBAFV1LbR7HPOWzXQDpDgEuib/aJ1jsZZx6x3c2mBI+TJhJzOhkGeaLbCKEHXEXLfirtG2JA==", + "license": "MIT", + "dependencies": { + "package-json": "^10.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/levn": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", @@ -5586,9 +6053,9 @@ "license": "MIT" }, "node_modules/magic-string": { - "version": "0.30.18", - "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.18.tgz", - "integrity": "sha512-yi8swmWbO17qHhwIBNeeZxTceJMeBvWJaId6dyvTSOwTipqeHhMhOrz6513r1sOKnpvQ7zkhlG8tPrpilwTxHQ==", + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", "dev": true, "license": "MIT", "dependencies": { @@ -6522,6 +6989,15 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/mkdirp": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", @@ -6810,6 +7286,24 @@ "node": ">= 14" } }, + "node_modules/package-json": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/package-json/-/package-json-10.0.1.tgz", + "integrity": "sha512-ua1L4OgXSBdsu1FPb7F3tYH0F48a6kxvod4pLUlGY9COeJAJQNX/sNH2IiEmsxw7lqYiAwrdHMjz1FctOsyDQg==", + "license": "MIT", + "dependencies": { + "ky": "^1.2.0", + "registry-auth-token": "^5.0.2", + "registry-url": "^6.0.1", + "semver": "^7.6.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/package-manager-detector": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/package-manager-detector/-/package-manager-detector-1.3.0.tgz", @@ -7125,23 +7619,6 @@ "fsevents": "~2.3.2" } }, - "node_modules/pm2/node_modules/debug": { - "version": "4.4.3", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", - "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", - "license": "MIT", - "dependencies": { - "ms": "^2.1.3" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, "node_modules/pm2/node_modules/glob-parent": { "version": "5.1.2", "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", @@ -7391,6 +7868,12 @@ "read": "^1.0.4" } }, + "node_modules/proto-list": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/proto-list/-/proto-list-1.2.4.tgz", + "integrity": "sha512-vtK/94akxsTMhe0/cbfpR+syPuszcuwhqVjJq26CuNDgFGj682oRBXOP5MJpv2r7JtE8MsiepGIqvvOTBwn2vA==", + "license": "ISC" + }, "node_modules/proxy-agent": { "version": "6.4.0", "resolved": "https://registry.npmjs.org/proxy-agent/-/proxy-agent-6.4.0.tgz", @@ -7448,6 +7931,21 @@ "node": ">=6" } }, + "node_modules/pupa": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/pupa/-/pupa-3.3.0.tgz", + "integrity": "sha512-LjgDO2zPtoXP2wJpDjZrGdojii1uqO0cnwKoIoUzkfS98HDmbeiGmYiXo3lXeFlq2xvne1QFQhwYXSUCLKtEuA==", + "license": "MIT", + "dependencies": { + "escape-goat": "^4.0.0" + }, + "engines": { + "node": ">=12.20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/quansync": { "version": "0.2.11", "resolved": "https://registry.npmjs.org/quansync/-/quansync-0.2.11.tgz", @@ -7486,6 +7984,30 @@ ], "license": "MIT" }, + "node_modules/rc": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", + "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", + "license": "(BSD-2-Clause OR MIT OR Apache-2.0)", + "dependencies": { + "deep-extend": "^0.6.0", + "ini": "~1.3.0", + "minimist": "^1.2.0", + "strip-json-comments": "~2.0.1" + }, + "bin": { + "rc": "cli.js" + } + }, + "node_modules/rc/node_modules/strip-json-comments": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", + "integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/rc9": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/rc9/-/rc9-2.1.2.tgz", @@ -7560,6 +8082,33 @@ "regexp-tree": "bin/regexp-tree" } }, + "node_modules/registry-auth-token": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/registry-auth-token/-/registry-auth-token-5.1.0.tgz", + "integrity": "sha512-GdekYuwLXLxMuFTwAPg5UKGLW/UXzQrZvH/Zj791BQif5T05T0RsaLfHc9q3ZOKi7n+BoprPD9mJ0O0k4xzUlw==", + "license": "MIT", + "dependencies": { + "@pnpm/npm-conf": "^2.1.0" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/registry-url": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/registry-url/-/registry-url-6.0.1.tgz", + "integrity": "sha512-+crtS5QjFRqFCoQmvGduwYWEBng99ZvmFvF+cUJkGYF1L1BfU8C6Zp9T7f5vPAwyLkUExpvK+ANVZmGU49qi4Q==", + "license": "MIT", + "dependencies": { + "rc": "1.2.8" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/regjsparser": { "version": "0.12.0", "resolved": "https://registry.npmjs.org/regjsparser/-/regjsparser-0.12.0.tgz", @@ -7652,50 +8201,54 @@ } }, "node_modules/rolldown": { - "version": "1.0.0-beta.13-commit.024b632", - "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.0-beta.13-commit.024b632.tgz", - "integrity": "sha512-sntAHxNJ22WdcXVHQDoRst4eOJZjuT3S1aqsNWsvK2aaFVPgpVPY3WGwvJ91SvH/oTdRCyJw5PwpzbaMdKdYqQ==", + "version": "1.0.0-beta.44", + "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.0-beta.44.tgz", + "integrity": "sha512-gcqgyCi3g93Fhr49PKvymE8PoaGS0sf6ajQrsYaQ8o5de6aUEbD6rJZiJbhOfpcqOnycgsAsUNPYri1h25NgsQ==", "dev": true, "license": "MIT", "dependencies": { - "@oxc-project/runtime": "=0.72.3", - "@oxc-project/types": "=0.72.3", - "@rolldown/pluginutils": "1.0.0-beta.13-commit.024b632", - "ansis": "^4.0.0" + "@oxc-project/types": "=0.95.0", + "@rolldown/pluginutils": "1.0.0-beta.44" }, "bin": { "rolldown": "bin/cli.mjs" }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, "optionalDependencies": { - "@rolldown/binding-darwin-arm64": "1.0.0-beta.13-commit.024b632", - "@rolldown/binding-darwin-x64": "1.0.0-beta.13-commit.024b632", - "@rolldown/binding-freebsd-x64": "1.0.0-beta.13-commit.024b632", - "@rolldown/binding-linux-arm-gnueabihf": "1.0.0-beta.13-commit.024b632", - "@rolldown/binding-linux-arm64-gnu": "1.0.0-beta.13-commit.024b632", - "@rolldown/binding-linux-arm64-musl": "1.0.0-beta.13-commit.024b632", - "@rolldown/binding-linux-x64-gnu": "1.0.0-beta.13-commit.024b632", - "@rolldown/binding-linux-x64-musl": "1.0.0-beta.13-commit.024b632", - "@rolldown/binding-wasm32-wasi": "1.0.0-beta.13-commit.024b632", - "@rolldown/binding-win32-arm64-msvc": "1.0.0-beta.13-commit.024b632", - "@rolldown/binding-win32-ia32-msvc": "1.0.0-beta.13-commit.024b632", - "@rolldown/binding-win32-x64-msvc": "1.0.0-beta.13-commit.024b632" + "@rolldown/binding-android-arm64": "1.0.0-beta.44", + "@rolldown/binding-darwin-arm64": "1.0.0-beta.44", + "@rolldown/binding-darwin-x64": "1.0.0-beta.44", + "@rolldown/binding-freebsd-x64": "1.0.0-beta.44", + "@rolldown/binding-linux-arm-gnueabihf": "1.0.0-beta.44", + "@rolldown/binding-linux-arm64-gnu": "1.0.0-beta.44", + "@rolldown/binding-linux-arm64-musl": "1.0.0-beta.44", + "@rolldown/binding-linux-x64-gnu": "1.0.0-beta.44", + "@rolldown/binding-linux-x64-musl": "1.0.0-beta.44", + "@rolldown/binding-openharmony-arm64": "1.0.0-beta.44", + "@rolldown/binding-wasm32-wasi": "1.0.0-beta.44", + "@rolldown/binding-win32-arm64-msvc": "1.0.0-beta.44", + "@rolldown/binding-win32-ia32-msvc": "1.0.0-beta.44", + "@rolldown/binding-win32-x64-msvc": "1.0.0-beta.44" } }, "node_modules/rolldown-plugin-dts": { - "version": "0.15.10", - "resolved": "https://registry.npmjs.org/rolldown-plugin-dts/-/rolldown-plugin-dts-0.15.10.tgz", - "integrity": "sha512-8cPVAVQUo9tYAoEpc3jFV9RxSil13hrRRg8cHC9gLXxRMNtWPc1LNMSDXzjyD+5Vny49sDZH77JlXp/vlc4I3g==", + "version": "0.16.12", + "resolved": "https://registry.npmjs.org/rolldown-plugin-dts/-/rolldown-plugin-dts-0.16.12.tgz", + "integrity": "sha512-9dGjm5oqtKcbZNhpzyBgb8KrYiU616A7IqcFWG7Msp1RKAXQ/hapjivRg+g5IYWSiFhnk3OKYV5T4Ft1t8Cczg==", "dev": true, "license": "MIT", "dependencies": { "@babel/generator": "^7.28.3", - "@babel/parser": "^7.28.3", - "@babel/types": "^7.28.2", - "ast-kit": "^2.1.2", - "birpc": "^2.5.0", - "debug": "^4.4.1", + "@babel/parser": "^7.28.4", + "@babel/types": "^7.28.4", + "ast-kit": "^2.1.3", + "birpc": "^2.6.1", + "debug": "^4.4.3", "dts-resolver": "^2.1.2", - "get-tsconfig": "^4.10.1" + "get-tsconfig": "^4.12.0", + "magic-string": "^0.30.19" }, "engines": { "node": ">=20.18.0" @@ -7704,12 +8257,16 @@ "url": "https://github.com/sponsors/sxzz" }, "peerDependencies": { + "@ts-macro/tsc": "^0.3.6", "@typescript/native-preview": ">=7.0.0-dev.20250601.1", "rolldown": "^1.0.0-beta.9", "typescript": "^5.0.0", - "vue-tsc": "~3.0.3" + "vue-tsc": "~3.1.0" }, "peerDependenciesMeta": { + "@ts-macro/tsc": { + "optional": true + }, "@typescript/native-preview": { "optional": true }, @@ -8039,6 +8596,39 @@ "dev": true, "license": "MIT" }, + "node_modules/string-width": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/strip-ansi": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.2.tgz", + "integrity": "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, "node_modules/strip-indent": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-4.0.0.tgz", @@ -8081,6 +8671,11 @@ "url": "https://github.com/sponsors/antfu" } }, + "node_modules/stubborn-fs": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/stubborn-fs/-/stubborn-fs-1.2.5.tgz", + "integrity": "sha512-H2N9c26eXjzL/S/K+i/RHHcFanE74dptvvjM8iwzwbVcWY/zjBbgRqF3K0DY4+OD+uTTASTBvDoxPDaPN02D7g==" + }, "node_modules/supports-color": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", @@ -8366,24 +8961,24 @@ "license": "MIT" }, "node_modules/tsdown": { - "version": "0.14.2", - "resolved": "https://registry.npmjs.org/tsdown/-/tsdown-0.14.2.tgz", - "integrity": "sha512-6ThtxVZoTlR5YJov5rYvH8N1+/S/rD/pGfehdCLGznGgbxz+73EASV1tsIIZkLw2n+SXcERqHhcB/OkyxdKv3A==", + "version": "0.15.9", + "resolved": "https://registry.npmjs.org/tsdown/-/tsdown-0.15.9.tgz", + "integrity": "sha512-C0EJYpXIYdlJokTumIL4lmv/wEiB20oa6iiYsXFE7Q0VKF3Ju6TQ7XAn4JQdm+2iQGEfl8cnEKcX5DB7iVR5Dw==", "dev": true, "license": "MIT", "dependencies": { - "ansis": "^4.1.0", + "ansis": "^4.2.0", "cac": "^6.7.14", "chokidar": "^4.0.3", - "debug": "^4.4.1", + "debug": "^4.4.3", "diff": "^8.0.2", "empathic": "^2.0.0", "hookable": "^5.5.3", - "rolldown": "latest", - "rolldown-plugin-dts": "^0.15.8", - "semver": "^7.7.2", + "rolldown": "1.0.0-beta.44", + "rolldown-plugin-dts": "^0.16.12", + "semver": "^7.7.3", "tinyexec": "^1.0.1", - "tinyglobby": "^0.2.14", + "tinyglobby": "^0.2.15", "tree-kill": "^1.2.2", "unconfig": "^7.3.3" }, @@ -8421,6 +9016,19 @@ } } }, + "node_modules/tsdown/node_modules/semver": { + "version": "7.7.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", + "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/tslib": { "version": "2.8.1", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", @@ -8488,6 +9096,19 @@ "node": ">= 0.8.0" } }, + "node_modules/type-fest": { + "version": "2.19.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-2.19.0.tgz", + "integrity": "sha512-RAH822pAdBgcNMAfWnCBU3CFZcfZ/i1eZjwFU/dsLKumyuuP3niueg2UAukXYF0E2AAoc82ZSSf9J0WQBinzHA==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=12.20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/typescript": { "version": "5.9.2", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.2.tgz", @@ -8630,6 +9251,155 @@ "browserslist": ">= 4.21.0" } }, + "node_modules/update-notifier": { + "version": "7.3.1", + "resolved": "https://registry.npmjs.org/update-notifier/-/update-notifier-7.3.1.tgz", + "integrity": "sha512-+dwUY4L35XFYEzE+OAL3sarJdUioVovq+8f7lcIJ7wnmnYQV5UD1Y/lcwaMSyaQ6Bj3JMj1XSTjZbNLHn/19yA==", + "license": "BSD-2-Clause", + "dependencies": { + "boxen": "^8.0.1", + "chalk": "^5.3.0", + "configstore": "^7.0.0", + "is-in-ci": "^1.0.0", + "is-installed-globally": "^1.0.0", + "is-npm": "^6.0.0", + "latest-version": "^9.0.0", + "pupa": "^3.1.0", + "semver": "^7.6.3", + "xdg-basedir": "^5.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/yeoman/update-notifier?sponsor=1" + } + }, + "node_modules/update-notifier/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/update-notifier/node_modules/boxen": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/boxen/-/boxen-8.0.1.tgz", + "integrity": "sha512-F3PH5k5juxom4xktynS7MoFY+NUWH5LC4CnH11YB8NPew+HLpmBLCybSAEyb2F+4pRXhuhWqFesoQd6DAyc2hw==", + "license": "MIT", + "dependencies": { + "ansi-align": "^3.0.1", + "camelcase": "^8.0.0", + "chalk": "^5.3.0", + "cli-boxes": "^3.0.0", + "string-width": "^7.2.0", + "type-fest": "^4.21.0", + "widest-line": "^5.0.0", + "wrap-ansi": "^9.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/update-notifier/node_modules/camelcase": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-8.0.0.tgz", + "integrity": "sha512-8WB3Jcas3swSvjIeA2yvCJ+Miyz5l1ZmB6HFb9R1317dt9LCQoswg/BGrmAmkWVEszSrrg4RwmO46qIm2OEnSA==", + "license": "MIT", + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/update-notifier/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/update-notifier/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/update-notifier/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/update-notifier/node_modules/type-fest": { + "version": "4.41.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.41.0.tgz", + "integrity": "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==", + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/update-notifier/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/update-notifier/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/uri-js": { "version": "4.4.1", "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", @@ -8886,6 +9656,12 @@ "url": "https://opencollective.com/eslint" } }, + "node_modules/when-exit": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/when-exit/-/when-exit-2.1.4.tgz", + "integrity": "sha512-4rnvd3A1t16PWzrBUcSDZqcAmsUIy4minDXT/CZ8F2mVDgd65i4Aalimgz1aQkRGU0iH5eT5+6Rx2TK8o443Pg==", + "license": "MIT" + }, "node_modules/which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", @@ -8919,6 +9695,22 @@ "node": ">=8" } }, + "node_modules/widest-line": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/widest-line/-/widest-line-4.0.1.tgz", + "integrity": "sha512-o0cyEG0e8GPzT4iGHphIOh0cJOV8fivsXxddQasHPHfoZf1ZexrfeA21w2NaEN1RHE+fXlfISmOE8R9N3u3Qig==", + "dev": true, + "license": "MIT", + "dependencies": { + "string-width": "^5.0.1" + }, + "engines": { + "node": ">=12" + }, + "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", @@ -8929,6 +9721,37 @@ "node": ">=0.10.0" } }, + "node_modules/wrap-ansi": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", + "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.1.0", + "string-width": "^5.0.1", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?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==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, "node_modules/ws": { "version": "8.18.3", "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.3.tgz", @@ -8950,6 +9773,18 @@ } } }, + "node_modules/xdg-basedir": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/xdg-basedir/-/xdg-basedir-5.1.0.tgz", + "integrity": "sha512-GCPAHLvrIH13+c0SuacwvRYj2SxJXQ4kaVTT5xgL3kPrz56XxkF21IGhjSE1+W0aw7gpBWRGXLCPnPby6lSpmQ==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/xml-name-validator": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-4.0.0.tgz", @@ -8970,7 +9805,6 @@ "version": "2.8.1", "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.1.tgz", "integrity": "sha512-lcYcMxX2PO9XMGvAJkJ3OsNMw+/7FKes7/hgerGUYWIoWu5j/+YQqcZr5JnPZWzOsEBgMbSbiSTn/dv/69Mkpw==", - "dev": true, "license": "ISC", "bin": { "yaml": "bin.mjs" diff --git a/package.json b/package.json index 6976f5f..4fc8d7e 100644 --- a/package.json +++ b/package.json @@ -21,7 +21,19 @@ "remote" ], "exports": { - ".": "./dist/index.js" + ".": { + "types": "./dist/index.d.ts", + "default": "./dist/index.js" + }, + "./md-queue": { + "types": "./src/md-queue/index.ts", + "default": "./src/md-queue/index.ts" + } + }, + "typesVersions": { + "*": { + "md-queue": ["./src/md-queue/index.ts"] + } }, "main": "./dist/index.js", "module": "./dist/index.js", @@ -33,7 +45,7 @@ "dist" ], "engines": { - "node": ">=20.19.4" + "node": ">=18.0.0" }, "scripts": { "build": "tsdown", @@ -43,8 +55,8 @@ "prepack": "bun run build", "prepublishOnly": "npx publint --pack npm", "check": "bun lint && bun typecheck && vitest run && bun run build", - "release": "echo '🚀 Starting release process...' && git branch --show-current | grep -q '^main$' && bun run check && npm pack && echo '📦 Package created. Testing locally...' && npm install -g ./ccremote-*.tgz && ccremote --version && echo '✅ Local test passed! Creating tag for current version...' && npx bumpp $(node -p \"require('./package.json').version\") --yes && npm publish && echo '🎉 Release complete!'", - "release:test": "bun run check && npm pack && npm install -g ./ccremote-*.tgz && ccremote --version && echo '✅ Package tests passed!'", + "release": "echo '🚀 Starting release process...' && git branch --show-current | grep -q '^main$' && bun run check && npm pack && echo '📦 Package created. Testing locally...' && npm install -g ./ccremote-*.tgz --registry=$(npm config get registry) --no-audit && ccremote --version && echo '✅ Local test passed! Creating tag for current version...' && npx bumpp $(node -p \"require('./package.json').version\") --yes && npm publish && echo '🎉 Release complete!'", + "release:test": "bun run check && npm pack && (npm uninstall -g ccremote --registry=$(npm config get registry) 2>/dev/null || true) && npm install -g ./ccremote-*.tgz --registry=$(npm config get registry) --no-audit && ccremote --version && echo '✅ Package tests passed!'", "start": "bun run ./src/index.ts", "test": "vitest", "typecheck": "tsc --noEmit", @@ -76,6 +88,7 @@ "dotenv": "^17.2.2", "pm2": "^6.0.10", "update-notifier": "^7.3.1", + "yaml": "^2.8.1", "zod": "^3.25.67" } } diff --git a/src/commands/clean.ts b/src/commands/clean.ts index cbf19a5..9280b30 100644 --- a/src/commands/clean.ts +++ b/src/commands/clean.ts @@ -81,6 +81,15 @@ export const cleanCommand = define({ let reason = ''; const tmuxExists = await tmuxManager.sessionExists(session.tmuxSession); + const daemonInfo = daemonManager.getDaemon(session.id); + const daemonRunning = daemonInfo !== null; + + // Rule 0: NEVER clean if daemon is running - it's actively monitoring + // This prevents race conditions where status might be temporarily 'ended' + if (daemonRunning) { + consola.info(`Session ${session.id} daemon is running - skipping cleanup`); + continue; + } // Rule 1: If explicitly marked as ended, double-check tmux is actually dead // This prevents cleaning sessions that were incorrectly marked as ended @@ -93,7 +102,7 @@ export const cleanCommand = define({ consola.warn(`Session ${session.id} marked as ended but tmux is still active - skipping cleanup`); } } - // Rule 2: If tmux session is dead, clean it (session is definitely not in use) + // Rule 2: If tmux session is dead AND no daemon, clean it (session is definitely not in use) else if (!tmuxExists) { shouldClean = true; reason = 'tmux session dead'; @@ -225,6 +234,7 @@ export const cleanCommand = define({ // Delete Discord channels for ended sessions let deletedChannels = 0; + let skippedChannels = 0; if (discordBot) { // Delete channels for sessions being cleaned up for (const session of cleanupSessions) { @@ -247,6 +257,10 @@ export const cleanCommand = define({ deletedChannels++; consola.info(`📺 Deleted orphaned Discord channel ${channelId}`); } + else { + skippedChannels++; + consola.info(`⏭️ Skipped orphaned channel ${channelId} (insufficient permissions)`); + } } catch (error: unknown) { consola.warn(`Failed to delete orphaned channel ${channelId}: ${error instanceof Error ? error.message : String(error)}`); @@ -263,6 +277,10 @@ export const cleanCommand = define({ deletedChannels++; consola.info(`📺 Deleted archived Discord channel ${channelId}`); } + else { + skippedChannels++; + consola.info(`⏭️ Skipped archived channel ${channelId} (insufficient permissions)`); + } } catch (error: unknown) { consola.warn(`Failed to delete archived channel ${channelId}: ${error instanceof Error ? error.message : String(error)}`); @@ -305,8 +323,35 @@ export const cleanCommand = define({ consola.info(` • Archived ${archivedCount} log files`); if (discordBot) { consola.info(` • Deleted ${deletedChannels} Discord channels`); + if (skippedChannels > 0) { + consola.info(` • Skipped ${skippedChannels} channels (insufficient permissions)`); + } await discordBot.shutdown(); } + + // Show permission fix instructions if channels were skipped + if (skippedChannels > 0) { + consola.box( + '⚠️ Permission Error - Some Discord channels could not be deleted\n\n' + + 'Your Discord bot lacks the required permissions to delete these channels.\n\n' + + 'To fix this, you have two options:\n\n' + + '1. Administrator permission (recommended):\n' + + ' • Go to Discord Developer Portal → OAuth2 → URL Generator\n' + + ' • Select scope: bot\n' + + ' • Select permission: Administrator\n' + + ' • Use the generated URL to re-invite your bot\n\n' + + '2. Minimal permissions:\n' + + ' • Manage Channels (create/delete session channels)\n' + + ' • Manage Roles (edit channel overwrites)\n' + + ' • Send Messages (send notifications)\n' + + ' • Read Message History (read approval responses)\n\n' + + 'Note: Administrator permission is recommended as it avoids role\n' + + 'hierarchy issues and ensures reliable channel management.\n\n' + + 'After updating permissions, run "ccremote clean" again to remove\n' + + 'the remaining channels.\n\n' + + 'See: https://github.com/generativereality/ccremote#discord-setup', + ); + } } catch (error: unknown) { consola.error('Failed to clean sessions:', error instanceof Error ? error.message : String(error)); diff --git a/src/commands/start.ts b/src/commands/start.ts index d8da068..800416c 100644 --- a/src/commands/start.ts +++ b/src/commands/start.ts @@ -162,6 +162,15 @@ export const startCommand = define({ process.exit(1); } + // Check if tmux server is responsive + const healthCheck = await tmuxManager.isTmuxHealthy(); + if (!healthCheck.healthy) { + consola.error('tmux server is frozen or unresponsive'); + consola.error(''); + consola.error(healthCheck.error || 'Unknown error'); + process.exit(1); + } + await sessionManager.initialize(); // Create session diff --git a/src/core/daemon.ts b/src/core/daemon.ts index 1fe2f21..2e52a31 100644 --- a/src/core/daemon.ts +++ b/src/core/daemon.ts @@ -62,10 +62,46 @@ export class Daemon { return !!(this.discordBot && this.config.discordBotToken); } + /** + * Set up global error handlers to prevent uncaught errors from crashing the daemon + */ + private setupGlobalErrorHandlers(): void { + // Catch unhandled promise rejections + process.on('unhandledRejection', (reason: any, _promise: Promise) => { + this.log('ERROR', `Unhandled Promise Rejection: ${reason instanceof Error ? reason.message : String(reason)}`); + if (reason instanceof Error && reason.stack) { + this.log('ERROR', `Stack trace: ${reason.stack}`); + } + }); + + // Catch uncaught exceptions + process.on('uncaughtException', (error: Error) => { + this.log('ERROR', `Uncaught Exception: ${error.message}`); + if (error.stack) { + this.log('ERROR', `Stack trace: ${error.stack}`); + } + + // For critical errors, attempt graceful shutdown + // But don't exit immediately - give other cleanup a chance + setTimeout(() => { + this.log('ERROR', 'Daemon exiting due to uncaught exception'); + process.exit(1); + }, 1000); + }); + + // Catch warnings (like unhandled error events) + process.on('warning', (warning: Error) => { + this.log('WARN', `Process warning: ${warning.name} - ${warning.message}`); + }); + } + /** * Start the daemon process */ async start(): Promise { + // Set up global error handlers FIRST to catch any errors during startup + this.setupGlobalErrorHandlers(); + try { this.log('INFO', `Daemon starting for session ${this.config.sessionId} (PID: ${process.pid})`); this.log('INFO', `Working directory: ${process.cwd()}`); diff --git a/src/core/discord.ts b/src/core/discord.ts index 5071ea4..62ba678 100644 --- a/src/core/discord.ts +++ b/src/core/discord.ts @@ -85,16 +85,58 @@ export class DiscordBot { private async performLogin(token: string): Promise { return new Promise((resolve, reject) => { + let settled = false; // Track if promise has been resolved/rejected + let timeoutId: NodeJS.Timeout; + + const cleanup = (): void => { + settled = true; + clearTimeout(timeoutId); + }; + // Add timeout to prevent hanging forever - const timeout = setTimeout(() => { + timeoutId = setTimeout(() => { + if (settled) { + return; + } const error = new Error('Opening handshake has timed out'); console.error('[DISCORD] Discord bot login timed out after 30 seconds'); + cleanup(); reject(error); }, 30000); + // Set up error handler FIRST to catch any errors during connection + // This includes WebSocket errors that might occur during handshake + const errorHandler = (error: Error): void => { + if (settled) { + return; + } + console.error('[DISCORD] Discord client error during startup:', error); + cleanup(); + reject(error); + }; + + // Attach error handler to catch all errors during login + this.client.once(Events.Error, errorHandler); + + // Also catch any WebSocket errors directly (in case they bypass Events.Error) + const wsErrorHandler = (error: Error): void => { + console.warn('[DISCORD] WebSocket error during connection:', error.message); + // Don't reject here - let the main error handler deal with it + // This is just for logging + }; + + // Access the WebSocket and add error handler if it exists + if (this.client.ws) { + // @ts-expect-error - 'error' is not in GatewayDispatchEvents but is a valid WebSocket event + this.client.ws.once('error', wsErrorHandler); + } + this.client.once(Events.ClientReady, () => { + if (settled) { + return; + } console.info('[DISCORD] ClientReady event fired!'); - clearTimeout(timeout); + cleanup(); this.isReady = true; this.readyTimestamp = Date.now(); @@ -112,13 +154,6 @@ export class DiscordBot { resolve(); }); - // Add error handler - this.client.once(Events.Error, (error) => { - console.error('[DISCORD] Discord client error during startup:', error); - clearTimeout(timeout); - reject(error); - }); - // Now login - the event will fire after this console.info('[DISCORD] Calling client.login()...'); this.client.login(token) @@ -126,8 +161,11 @@ export class DiscordBot { console.info('[DISCORD] client.login() resolved successfully'); }) .catch((error) => { + if (settled) { + return; + } console.error('[DISCORD] Login failed:', error); - clearTimeout(timeout); + cleanup(); reject(error); }); }); @@ -142,8 +180,18 @@ export class DiscordBot { }); this.client.on(Events.Error, (error) => { - console.error('Discord client error:', error); + console.error('[DISCORD] Client error (runtime):', error.message || error); + // Don't crash - just log the error }); + + // Handle WebSocket errors during runtime (not just during connection) + if (this.client.ws) { + // eslint-disable-next-line ts/no-unsafe-argument + this.client.ws.on('error' as any, (error: Error) => { + console.warn('[DISCORD] WebSocket error (runtime):', error.message || error); + // Don't crash - errors during runtime should trigger reconnection via health check + }); + } } private async handleMessage(message: Message): Promise { @@ -462,47 +510,54 @@ export class DiscordBot { /** * Delete a Discord channel and send a final message before deletion + * Returns true if deletion succeeded, false if bot lacks permissions */ - private async deleteChannel(channelId: string, finalMessage: string, deleteReason: string): Promise { - const channel = await this.client.channels.fetch(channelId) as TextChannel; - if (!channel || channel.type !== ChannelType.GuildText) { - return; - } + private async deleteChannel(channelId: string, finalMessage: string, deleteReason: string): Promise { + try { + const channel = await this.client.channels.fetch(channelId) as TextChannel; + if (!channel || channel.type !== ChannelType.GuildText) { + return false; + } - const guild = channel.guild; - const botMember = guild.members.me; + const guild = channel.guild; + const botMember = guild.members.me; - // Ensure bot has access to the channel before trying to send a message - if (botMember) { - try { - await channel.permissionOverwrites.edit(botMember, { - ViewChannel: true, - SendMessages: true, - ReadMessageHistory: true, - }); - } - catch (permError) { - console.warn(`Failed to ensure bot permissions for channel ${channelId}:`, permError); - // Continue anyway - we might still be able to delete without sending a message + // Check if bot has permissions to manage this channel before attempting anything + if (botMember) { + const botPermissions = channel.permissionsFor(botMember); + if (!botPermissions?.has(PermissionFlagsBits.ManageChannels)) { + console.warn(`[DISCORD] Bot lacks ManageChannels permission for channel ${channelId} (${channel.name}) - skipping`); + return false; + } } - } - // Try to send a final message before deletion (but don't fail if we can't) - await safeDiscordOperation( - async () => { - await channel.send(finalMessage); - }, - 'send deletion notification', - { warn: console.warn, debug: console.info }, - { - maxRetries: 1, - baseDelayMs: 1000, - }, - ); + // Try to send a final message before deletion (but don't fail if we can't) + await safeDiscordOperation( + async () => { + await channel.send(finalMessage); + }, + 'send deletion notification', + { warn: console.warn, debug: console.info }, + { + maxRetries: 1, + baseDelayMs: 1000, + }, + ); - // Wait briefly for the message to be sent, then delete the channel - await new Promise(resolve => setTimeout(resolve, 2000)); - await channel.delete(deleteReason); + // Wait briefly for the message to be sent, then delete the channel + await new Promise(resolve => setTimeout(resolve, 2000)); + await channel.delete(deleteReason); + return true; + } + catch (error: any) { + // Check if this is a permissions error + if (error?.code === 50001 || error?.message?.includes('Missing Access')) { + console.warn(`[DISCORD] Missing permissions to delete channel ${channelId} - skipping`); + return false; + } + // Re-throw other errors + throw error; + } } async cleanupSessionChannel(sessionId: string): Promise { @@ -744,24 +799,27 @@ export class DiscordBot { */ async deleteOrphanedChannel(channelId: string): Promise { try { - await this.deleteChannel( + const deleted = await this.deleteChannel( channelId, '🏁 Orphaned channel detected during cleanup. This channel will be deleted.', 'Orphaned channel cleanup', ); - // Clean up our internal mappings - this.channelSessionMap.delete(channelId); - // Find and remove any sessionId mappings that point to this channel - for (const [sessionId, mappedChannelId] of this.sessionChannelMap.entries()) { - if (mappedChannelId === channelId) { - this.sessionChannelMap.delete(sessionId); - break; + if (deleted) { + // Clean up our internal mappings + this.channelSessionMap.delete(channelId); + // Find and remove any sessionId mappings that point to this channel + for (const [sessionId, mappedChannelId] of this.sessionChannelMap.entries()) { + if (mappedChannelId === channelId) { + this.sessionChannelMap.delete(sessionId); + break; + } } + + console.info(`[DISCORD] Deleted orphaned channel ${channelId}`); } - console.info(`[DISCORD] Deleted orphaned channel`); - return true; + return deleted; } catch (error) { console.warn(`[DISCORD] Failed to delete orphaned channel ${channelId}:`, error); diff --git a/src/core/tmux.ts b/src/core/tmux.ts index 68382ec..8f5b4fe 100644 --- a/src/core/tmux.ts +++ b/src/core/tmux.ts @@ -3,11 +3,43 @@ import { promisify } from 'node:util'; const execAsync = promisify(exec); +const TMUX_TIMEOUT = 5000; // 5 second timeout for tmux commands +const TMUX_HEALTH_CHECK_TIMEOUT = 2000; // 2 second timeout for health checks + +export class TmuxTimeoutError extends Error { + constructor(command: string, timeout: number) { + super(`Tmux command timed out after ${timeout}ms: ${command}`); + this.name = 'TmuxTimeoutError'; + } +} + export class TmuxManager { + /** + * Execute a tmux command with timeout protection + */ + private async execWithTimeout(command: string, timeout: number = TMUX_TIMEOUT): Promise<{ stdout: string; stderr: string }> { + try { + return await Promise.race([ + execAsync(command), + new Promise((_, reject) => + setTimeout(() => reject(new TmuxTimeoutError(command, timeout)), timeout), + ), + ]); + } + catch (error) { + if (error instanceof TmuxTimeoutError) { + throw error; + } + throw error; + } + } + + /** + * Check if tmux is installed + */ async isTmuxAvailable(): Promise { try { - const command = 'tmux -V'; - await execAsync(command); + await this.execWithTimeout('tmux -V', TMUX_HEALTH_CHECK_TIMEOUT); return true; } catch { @@ -15,6 +47,31 @@ export class TmuxManager { } } + /** + * Check if tmux server is responsive (health check) + */ + async isTmuxHealthy(): Promise<{ healthy: boolean; error?: string }> { + try { + // Try to list sessions - this will fail fast if tmux server is hung + await this.execWithTimeout('tmux list-sessions 2>&1', TMUX_HEALTH_CHECK_TIMEOUT); + return { healthy: true }; + } + catch (error) { + if (error instanceof TmuxTimeoutError) { + return { + healthy: false, + error: 'Tmux server is unresponsive. The server may be frozen or hung.\n\n' + + 'To recover:\n' + + ' 1. Kill tmux: pkill -9 tmux\n' + + ' 2. Remove socket: rm -f /tmp/tmux-*/default\n' + + ' 3. Try again', + }; + } + // If there's an error but it's not a timeout, tmux is responsive (just no sessions) + return { healthy: true }; + } + } + async createSession(sessionName: string): Promise { try { // Use ccremote-specific tmux config if it exists, otherwise use default with mouse mode @@ -26,19 +83,24 @@ export class TmuxManager { ? `tmux new-session -d -s "${sessionName}" -c "${process.cwd()}"` : `tmux new-session -d -s "${sessionName}" -c "${process.cwd()}" \\; set -g mouse on`; - await execAsync(createCommand); + await this.execWithTimeout(createCommand); // Load ccremote config into the session if it exists if (hasConfig) { const sourceCommand = `tmux source-file "${ccremoteConfig}"`; - await execAsync(sourceCommand); + await this.execWithTimeout(sourceCommand); } // Start Claude in the session const startClaudeCommand = `tmux send-keys -t "${sessionName}" "claude" Enter`; - await execAsync(startClaudeCommand); + await this.execWithTimeout(startClaudeCommand); } catch (error) { + if (error instanceof TmuxTimeoutError) { + // eslint-disable-next-line unicorn/prefer-type-error + throw new Error(`Tmux server is unresponsive. Cannot create session.\n\n${error.message}`); + } + throw new Error(`Failed to create tmux session: ${error instanceof Error ? error.message : error}`); } } @@ -46,10 +108,15 @@ export class TmuxManager { async capturePane(sessionName: string): Promise { try { const command = `tmux capture-pane -t "${sessionName}" -p`; - const { stdout } = await execAsync(command); + const { stdout } = await this.execWithTimeout(command); return stdout; } catch (error) { + if (error instanceof TmuxTimeoutError) { + // eslint-disable-next-line unicorn/prefer-type-error + throw new Error(`Tmux server is unresponsive. Cannot capture pane.\n\n${error.message}`); + } + throw new Error(`Failed to capture tmux pane: ${error instanceof Error ? error.message : error}`); } } @@ -58,10 +125,15 @@ export class TmuxManager { try { // Use -e flag to include escape sequences for text/background attributes const command = `tmux capture-pane -t "${sessionName}" -p -e`; - const { stdout } = await execAsync(command); + const { stdout } = await this.execWithTimeout(command); return stdout; } catch (error) { + if (error instanceof TmuxTimeoutError) { + // eslint-disable-next-line unicorn/prefer-type-error + throw new Error(`Tmux server is unresponsive. Cannot capture pane.\n\n${error.message}`); + } + throw new Error(`Failed to capture tmux pane with colors: ${error instanceof Error ? error.message : error}`); } } @@ -70,9 +142,14 @@ export class TmuxManager { try { // Send keys to tmux session const command = `tmux send-keys -t "${sessionName}" "${keys}" Enter`; - await execAsync(command); + await this.execWithTimeout(command); } catch (error) { + if (error instanceof TmuxTimeoutError) { + // eslint-disable-next-line unicorn/prefer-type-error + throw new Error(`Tmux server is unresponsive. Cannot send keys.\n\n${error.message}`); + } + throw new Error(`Failed to send keys to tmux: ${error instanceof Error ? error.message : error}`); } } @@ -81,9 +158,14 @@ export class TmuxManager { try { // Send raw keys without Enter (for approvals like '1' or '2') const command = `tmux send-keys -t "${sessionName}" "${keys}"`; - await execAsync(command); + await this.execWithTimeout(command); } catch (error) { + if (error instanceof TmuxTimeoutError) { + // eslint-disable-next-line unicorn/prefer-type-error + throw new Error(`Tmux server is unresponsive. Cannot send keys.\n\n${error.message}`); + } + throw new Error(`Failed to send raw keys to tmux: ${error instanceof Error ? error.message : error}`); } } @@ -92,9 +174,14 @@ export class TmuxManager { try { // Clear current input line const command = `tmux send-keys -t "${sessionName}" C-u`; - await execAsync(command); + await this.execWithTimeout(command); } catch (error) { + if (error instanceof TmuxTimeoutError) { + // eslint-disable-next-line unicorn/prefer-type-error + throw new Error(`Tmux server is unresponsive. Cannot clear input.\n\n${error.message}`); + } + throw new Error(`Failed to clear tmux input: ${error instanceof Error ? error.message : error}`); } } @@ -102,13 +189,7 @@ export class TmuxManager { async sessionExists(sessionName: string): Promise { try { const command = `tmux has-session -t "${sessionName}"`; - // Add timeout to prevent hanging - await Promise.race([ - execAsync(command), - new Promise((_, reject) => - setTimeout(() => reject(new Error('Timeout')), 5000), - ), - ]); + await this.execWithTimeout(command); return true; } catch { @@ -119,11 +200,16 @@ export class TmuxManager { async killSession(sessionName: string): Promise { try { const command = `tmux kill-session -t "${sessionName}"`; - await execAsync(command); + await this.execWithTimeout(command); } catch (error) { // Don't throw if session doesn't exist if (!error || !String(error).includes('session not found')) { + if (error instanceof TmuxTimeoutError) { + // eslint-disable-next-line unicorn/prefer-type-error + throw new Error(`Tmux server is unresponsive. Cannot kill session.\n\n${error.message}`); + } + throw new Error(`Failed to kill tmux session: ${error instanceof Error ? error.message : error}`); } } @@ -132,7 +218,7 @@ export class TmuxManager { async listSessions(): Promise> { try { const command = 'tmux list-sessions -F "#{session_name},#{session_created},#{session_windows}"'; - const { stdout } = await execAsync(command); + const { stdout } = await this.execWithTimeout(command); return stdout.trim().split('\n').filter(line => line.length > 0).map((line) => { const [name, created, windows] = line.split(','); return { @@ -142,7 +228,10 @@ export class TmuxManager { }; }); } - catch { + catch (error) { + if (error instanceof TmuxTimeoutError) { + console.warn('Tmux server is unresponsive. Cannot list sessions.'); + } return []; } } diff --git a/src/md-queue/AssetManager.ts b/src/md-queue/AssetManager.ts new file mode 100644 index 0000000..4b24b47 --- /dev/null +++ b/src/md-queue/AssetManager.ts @@ -0,0 +1,260 @@ +/** + * AssetManager: File operations for md-queue + * + * Handles reading, writing, and atomic updates to markdown files + * with YAML frontmatter. Works with both Node.js and Bun. + */ + +import type { Frontmatter, QueueItem } from './types'; +import { promises as fs } from 'node:fs'; +import * as path from 'node:path'; +import { parse as parseYAML, stringify as stringifyYAML } from 'yaml'; + +/** + * Manages markdown file operations with atomic writes for sync safety + */ +export class AssetManager { + /** + * Read a queue item from disk + * + * @param filePath - Absolute path to markdown file + * @returns Parsed queue item or null if file doesn't exist + */ + async read(filePath: string): Promise { + try { + // Check if file exists and read content + const content = await fs.readFile(filePath, 'utf-8'); + + // Parse frontmatter and content + const { frontmatter, content: markdownContent } = this.parseFrontmatter(content); + + return { + path: filePath, + frontmatter, + content: markdownContent, + }; + } + catch (error: any) { + // File doesn't exist + if (error.code === 'ENOENT') { + return null; + } + // Re-throw other errors + throw error; + } + } + + /** + * Create a new queue item + * + * @param filePath - Absolute path where file should be created + * @param frontmatter - YAML frontmatter object + * @param content - Markdown content (optional, defaults to empty string) + */ + async create( + filePath: string, + frontmatter: Frontmatter, + content: string = '', + ): Promise { + // Ensure parent directory exists + const dir = path.dirname(filePath); + await fs.mkdir(dir, { recursive: true }); + + // Write file atomically + await this.atomicWrite(filePath, frontmatter, content); + } + + /** + * Update frontmatter fields (partial update) + * + * @param filePath - Absolute path to markdown file + * @param updates - Partial frontmatter updates (deep merge) + */ + async updateFrontmatter( + filePath: string, + updates: Partial, + ): Promise { + // Read existing item + const item = await this.read(filePath); + if (!item) { + throw new Error(`File not found: ${filePath}`); + } + + // Deep merge updates into frontmatter + const updatedFrontmatter = this.deepMerge(item.frontmatter, updates); + + // Write updated item atomically + await this.atomicWrite(filePath, updatedFrontmatter, item.content); + } + + /** + * Atomic write using .tmp file and rename + * + * Ensures sync safety: the file is either old or new, never corrupt. + * + * @param filePath - Target file path + * @param frontmatter - YAML frontmatter object + * @param content - Markdown content + */ + async atomicWrite( + filePath: string, + frontmatter: Frontmatter, + content: string, + ): Promise { + // Serialize frontmatter to YAML + const yamlContent = this.serializeFrontmatter(frontmatter); + + // Construct full file content + const fullContent = `---\n${yamlContent}---\n\n${content}`; + + // Write to temporary file + const tmpPath = `${filePath}.tmp`; + await fs.writeFile(tmpPath, fullContent, 'utf-8'); + + // Atomic rename (replaces target file atomically) + await fs.rename(tmpPath, filePath); + } + + /** + * Delete a queue item + * + * @param filePath - Absolute path to markdown file + */ + async delete(filePath: string): Promise { + try { + await fs.unlink(filePath); + } + catch (error: any) { + // Ignore if file doesn't exist + if (error.code !== 'ENOENT') { + throw error; + } + } + } + + /** + * Move/rename a queue item + * + * @param sourcePath - Current file path + * @param targetPath - New file path + */ + async move(sourcePath: string, targetPath: string): Promise { + // Ensure target parent directory exists + const targetDir = path.dirname(targetPath); + await fs.mkdir(targetDir, { recursive: true }); + + // Rename/move file (atomic operation) + await fs.rename(sourcePath, targetPath); + } + + /** + * Check if a file exists + * + * @param filePath - File path to check + * @returns True if file exists + */ + async exists(filePath: string): Promise { + try { + await fs.access(filePath); + return true; + } + catch { + return false; + } + } + + /** + * Parse frontmatter from raw markdown content + * + * @param content - Raw markdown file content + * @returns Parsed frontmatter and content + */ + protected parseFrontmatter(content: string): { + frontmatter: Frontmatter; + content: string; + } { + // Check for frontmatter delimiter at start + if (!content.startsWith('---\n')) { + throw new Error('Invalid markdown: missing frontmatter delimiter'); + } + + // Find the end of frontmatter (second ---) + const endDelimiterIndex = content.indexOf('\n---\n', 4); + if (endDelimiterIndex === -1) { + throw new Error('Invalid markdown: missing frontmatter end delimiter'); + } + + // Extract YAML content + const yamlContent = content.substring(4, endDelimiterIndex); + + // Parse YAML + const frontmatter = parseYAML(yamlContent) as Frontmatter; + + // Extract remaining content (skip the --- and following newlines) + const markdownContent = content.substring(endDelimiterIndex + 5).trimStart(); + + return { + frontmatter, + content: markdownContent, + }; + } + + /** + * Serialize frontmatter to YAML + * + * @param frontmatter - Frontmatter object + * @returns YAML string + */ + protected serializeFrontmatter(frontmatter: Frontmatter): string { + // Serialize to YAML + const yamlString = stringifyYAML(frontmatter, { + lineWidth: 0, // Disable line wrapping + defaultStringType: 'QUOTE_DOUBLE', // Use double quotes for strings + defaultKeyType: 'PLAIN', // Plain keys (no quotes) + }); + + return yamlString; + } + + /** + * Deep merge objects (for partial frontmatter updates) + * + * @param target - Target object + * @param source - Source object with updates + * @returns Merged object + */ + protected deepMerge(target: T, source: Partial): T { + // Create a copy of target + const result = (Array.isArray(target) ? [...target] : { ...target }) as T; + + // Merge source into result + for (const key in source) { + if (Object.prototype.hasOwnProperty.call(source, key)) { + const sourceValue = source[key]; + const targetValue = (target as Record)[key]; + + if (sourceValue === undefined) { + continue; + } + + // If both are objects (and not arrays), merge recursively + if ( + typeof sourceValue === 'object' + && sourceValue !== null + && !Array.isArray(sourceValue) + && typeof targetValue === 'object' + && targetValue !== null + && !Array.isArray(targetValue) + ) { + (result as Record)[key] = this.deepMerge(targetValue, sourceValue); + } + else { + // Otherwise, replace value (arrays are replaced, not merged) + (result as Record)[key] = sourceValue; + } + } + } + + return result; + } +} diff --git a/src/md-queue/LockManager.ts b/src/md-queue/LockManager.ts new file mode 100644 index 0000000..fd7b3e2 --- /dev/null +++ b/src/md-queue/LockManager.ts @@ -0,0 +1,239 @@ +/** + * LockManager: Soft lock management for md-queue + * + * Implements soft locks using host:pid:timestamp format stored in frontmatter. + * Supports stale lock detection and automatic cleanup. + */ + +import type { AssetManager } from './AssetManager'; +import type { Lock, QueueConfig, QueueItem } from './types'; +import { hostname } from 'node:os'; + +/** + * Manages soft locks for coordinating processing across workers + */ +export class LockManager { + private hostname: string; + private defaultLockTimeout: number; + + constructor(config?: Partial) { + this.hostname = config?.hostname || this.detectHostname(); + this.defaultLockTimeout = config?.lockTimeout || 5 * 60 * 1000; // 5 minutes + } + + /** + * Create a lock string for the current process + * + * @returns Lock string in format: hostname:pid:timestamp + */ + createLock(): string { + return `${this.hostname}:${process.pid}:${Date.now()}`; + } + + /** + * Parse a lock string into components + * + * @param lockString - Lock string to parse + * @returns Parsed lock object or null if invalid + */ + parseLock(lockString: string): Lock | null { + const parts = lockString.split(':'); + if (parts.length !== 3) { + return null; + } + + const [host, pidStr, timestampStr] = parts; + const pid = Number.parseInt(pidStr, 10); + const timestamp = Number.parseInt(timestampStr, 10); + + if (Number.isNaN(pid) || Number.isNaN(timestamp)) { + return null; + } + + return { host, pid, timestamp }; + } + + /** + * Check if a lock is stale (exceeded timeout) + * + * @param lockString - Lock string to check + * @param timeoutMs - Optional timeout override (defaults to config) + * @returns True if lock is stale + */ + isStale(lockString: string, timeoutMs?: number): boolean { + const lock = this.parseLock(lockString); + if (!lock) { + return true; // Invalid lock is considered stale + } + + const timeout = timeoutMs ?? this.defaultLockTimeout; + const now = Date.now(); + return now - lock.timestamp > timeout; + } + + /** + * Check if a lock belongs to the current process + * + * @param lockString - Lock string to check + * @returns True if lock is owned by current process + */ + isOwnLock(lockString: string): boolean { + const lock = this.parseLock(lockString); + if (!lock) { + return false; + } + + return lock.host === this.hostname && lock.pid === process.pid; + } + + /** + * Attempt to acquire a lock on an item + * + * @param item - Queue item to lock + * @param assetManager - AssetManager for updating frontmatter + * @returns True if lock was acquired, false if already locked + */ + async acquireLock( + item: QueueItem, + assetManager: AssetManager, + ): Promise { + const currentLock = item.frontmatter.status.lock; + + // Check if already locked by another process + if (currentLock) { + // Check if it's our own lock + if (this.isOwnLock(currentLock)) { + // Already have the lock, nothing to do + return true; + } + + // Check if the lock is stale + if (!this.isStale(currentLock)) { + // Lock is held by another process and not stale + return false; + } + } + + // Create new lock + const newLock = this.createLock(); + + // Update item frontmatter with lock and processing status + await assetManager.updateFrontmatter(item.path, { + status: { + ...item.frontmatter.status, + lock: newLock, + phase: 'processing', + last_update: new Date().toISOString(), + attempts: item.frontmatter.status.attempts + 1, + }, + }); + + return true; + } + + /** + * Release a lock on an item + * + * @param item - Queue item to unlock + * @param assetManager - AssetManager for updating frontmatter + */ + async releaseLock( + item: QueueItem, + assetManager: AssetManager, + ): Promise { + const currentLock = item.frontmatter.status.lock; + + if (!currentLock) { + // No lock to release + return; + } + + // Verify we own the lock + if (!this.isOwnLock(currentLock)) { + throw new Error( + `Cannot release lock owned by another process: ${currentLock}`, + ); + } + + // Release the lock + await assetManager.updateFrontmatter(item.path, { + status: { + ...item.frontmatter.status, + lock: null, + }, + }); + } + + /** + * Reset a stale lock (for reconciliation) + * + * @param item - Queue item with stale lock + * @param assetManager - AssetManager for updating frontmatter + */ + async resetStaleLock( + item: QueueItem, + assetManager: AssetManager, + ): Promise { + const currentLock = item.frontmatter.status.lock; + + if (!currentLock) { + return; + } + + // Verify lock is actually stale + if (!this.isStale(currentLock)) { + throw new Error(`Cannot reset non-stale lock: ${currentLock}`); + } + + // Reset to pending state + await assetManager.updateFrontmatter(item.path, { + status: { + ...item.frontmatter.status, + lock: null, + phase: 'pending', + last_update: new Date().toISOString(), + }, + }); + } + + /** + * Check if an item is currently locked by another process + * + * @param item - Queue item to check + * @returns True if locked by another process (not stale) + */ + isLockedByOther(item: QueueItem): boolean { + const currentLock = item.frontmatter.status.lock; + + if (!currentLock) { + return false; + } + + // Check if it's our own lock + if (this.isOwnLock(currentLock)) { + return false; + } + + // Check if the lock is stale + if (this.isStale(currentLock)) { + return false; + } + + // Locked by another process and not stale + return true; + } + + /** + * Detect hostname for locks + * + * @returns Hostname string + */ + private detectHostname(): string { + try { + return hostname(); + } + catch { + return 'unknown'; + } + } +} diff --git a/src/md-queue/Processor.ts b/src/md-queue/Processor.ts new file mode 100644 index 0000000..008d789 --- /dev/null +++ b/src/md-queue/Processor.ts @@ -0,0 +1,263 @@ +/** + * Processor: Processing orchestration for md-queue + * + * Handles the claim → execute → update workflow for queue items. + * Supports batch processing with concurrency control. + */ + +import type { AssetManager } from './AssetManager'; +import type { LockManager } from './LockManager'; +import type { Reconciler } from './Reconciler'; +import type { StateManager } from './StateManager'; +import type { ProcessOptions, ProcessReport, QueueItem } from './types'; + +/** + * Orchestrates queue item processing + */ +export class Processor { + private assetManager: AssetManager; + private lockManager: LockManager; + private stateManager: StateManager; + private reconciler: Reconciler; + + constructor( + assetManager: AssetManager, + lockManager: LockManager, + stateManager: StateManager, + reconciler: Reconciler, + ) { + this.assetManager = assetManager; + this.lockManager = lockManager; + this.stateManager = stateManager; + this.reconciler = reconciler; + } + + /** + * Process a single queue item + * + * Workflow: + * 1. Claim: Acquire lock and transition to 'processing' + * 2. Execute: Run the handler function + * 3. Update: Mark as done or error based on result + * + * @param item - Queue item to process + * @param handler - Async function that processes the item + * @returns Result from handler + */ + async processItem( + item: QueueItem, + handler: (item: QueueItem) => Promise, + ): Promise { + // Claim the item + const claimed = await this.claimItem(item); + if (!claimed) { + throw new Error(`Failed to acquire lock on item: ${item.path}`); + } + + try { + // Execute handler + const result = await handler(item); + + // Mark as done + await this.completeSuccess(item, result as Record); + + return result; + } + catch (error) { + // Mark as error + await this.completeError(item, error as Error); + + // Re-throw the error + throw error; + } + } + + /** + * Process all pending items in a directory + * + * @param directory - Directory to process + * @param handler - Handler function for each item + * @param options - Processing options + * @returns Processing report + */ + async processDirectory( + directory: string, + handler: (item: QueueItem) => Promise, + options?: ProcessOptions, + ): Promise { + const started_at = new Date().toISOString(); + const startTime = Date.now(); + + // Find processable items + const items = await this.reconciler.findProcessable(directory); + + // Process items with concurrency control + const maxConcurrent = options?.maxConcurrent || 1; + const results = await this.processWithConcurrency( + items, + handler, + maxConcurrent, + ); + + const finished_at = new Date().toISOString(); + const duration = Date.now() - startTime; + + return { + ...results, + duration, + started_at, + finished_at, + }; + } + + /** + * Process items with concurrency control + * + * @param items - Items to process + * @param handler - Handler function + * @param maxConcurrent - Maximum concurrent processing + * @returns Results object + */ + protected async processWithConcurrency( + items: QueueItem[], + handler: (item: QueueItem) => Promise, + maxConcurrent: number = 1, + ): Promise<{ + processed: number; + failed: number; + skipped: number; + }> { + const results = { + processed: 0, + failed: 0, + skipped: 0, + }; + + // Process items in chunks to control concurrency + for (let i = 0; i < items.length; i += maxConcurrent) { + const chunk = items.slice(i, i + maxConcurrent); + + // Process chunk concurrently + await Promise.all( + chunk.map(async (item) => { + try { + await this.processItem(item, handler); + results.processed++; + } + catch (error) { + // Check if this was a lock acquisition failure + if ( + error instanceof Error + && error.message.includes('Failed to acquire lock') + ) { + results.skipped++; + } + else { + results.failed++; + } + } + }), + ); + } + + return results; + } + + /** + * Claim an item for processing + * + * Acquires lock and transitions to 'processing' phase. + * + * @param item - Queue item to claim + * @returns True if claimed successfully + */ + protected async claimItem(item: QueueItem): Promise { + // Try to acquire lock + const acquired = await this.lockManager.acquireLock( + item, + this.assetManager, + ); + + return acquired; + } + + /** + * Execute handler with retry logic + * + * @param item - Queue item being processed + * @param handler - Handler function + * @param maxRetries - Maximum retry attempts + * @returns Handler result + */ + protected async executeWithRetry( + item: QueueItem, + handler: (item: QueueItem) => Promise, + maxRetries: number = 3, + ): Promise { + let lastError: Error | null = null; + + for (let attempt = 0; attempt <= maxRetries; attempt++) { + try { + return await handler(item); + } + catch (error) { + lastError = error as Error; + + if (attempt < maxRetries) { + // Wait before retry (exponential backoff) + const waitTime = 2 ** attempt * 1000; // 1s, 2s, 4s, 8s... + await new Promise(resolve => setTimeout(resolve, waitTime)); + } + } + } + + // All retries exhausted + throw lastError; + } + + /** + * Complete processing successfully + * + * @param item - Queue item that was processed + * @param result - Processing result (e.g., transcript, caption) + */ + protected async completeSuccess( + item: QueueItem, + result: Record, + ): Promise { + await this.stateManager.markDone(item, result, this.assetManager); + } + + /** + * Complete processing with error + * + * @param item - Queue item that failed + * @param error - Error that occurred + */ + protected async completeError( + item: QueueItem, + error: Error, + ): Promise { + await this.stateManager.markError(item, error, this.assetManager); + } + + /** + * Process items in batches with concurrency limit + * + * @param items - Items to process + * @param batchSize - Number of items per batch + * @param handler - Handler function + * @returns Processing counts + */ + protected async processBatches( + items: QueueItem[], + batchSize: number, + handler: (item: QueueItem) => Promise, + ): Promise<{ + processed: number; + failed: number; + skipped: number; + }> { + return this.processWithConcurrency(items, handler, batchSize); + } +} diff --git a/src/md-queue/README.md b/src/md-queue/README.md new file mode 100644 index 0000000..9d0b196 --- /dev/null +++ b/src/md-queue/README.md @@ -0,0 +1,385 @@ +# md-queue API Design + +**Version:** 1.0.0 +**Status:** Design Complete - Ready for Implementation +**Date:** 2025-10-20 + +This directory contains the TypeScript API design for **md-queue**, the unified markdown-based queue system used by both ccremote and the RememberThis Mac app. + +## Overview + +md-queue is a queue system that uses markdown files with YAML frontmatter for state persistence. It provides: + +- **Per-asset tracking** - Each item is a markdown file with state machine +- **Soft locks** - Coordinate processing across multiple workers +- **Atomic writes** - Safe for sync services (Obsidian Sync, Dropbox) +- **Reconciliation** - Find and reset stale items +- **Reprocessing** - Support for retrying with different parameters + +## Files + +### Core Types + +**`types.ts`** - Core type definitions +- `QueueItem` - Complete queue item (frontmatter + content) +- `Frontmatter` - YAML frontmatter structure +- `Status` - Processing status with state machine +- `Phase` - State machine phases (pending/processing/done/error) +- `Lock` - Lock information +- `ModelResult` - AI model processing results +- `ReconciliationReport` - Reconciliation statistics +- `ProcessReport` - Processing statistics +- `FilterOptions` - Item filtering criteria +- `ProcessOptions` - Processing configuration +- `QueueConfig` - Queue configuration + +### Managers + +**`AssetManager.ts`** - File operations +- `read()` - Read queue item from disk +- `create()` - Create new queue item +- `updateFrontmatter()` - Update frontmatter fields +- `atomicWrite()` - Atomic write (safe for sync) +- `delete()` - Delete queue item +- `move()` - Move/rename queue item + +**`LockManager.ts`** - Lock operations +- `createLock()` - Create lock for current process +- `parseLock()` - Parse lock string +- `isStale()` - Check if lock is stale +- `acquireLock()` - Acquire lock on item +- `releaseLock()` - Release lock +- `resetStaleLock()` - Reset stale lock to pending + +**`StateManager.ts`** - State transitions +- `transition()` - Transition to new phase +- `markDone()` - Mark item as done +- `markError()` - Mark item as error +- `resetToPending()` - Reset for reprocessing +- `hasExceededRetries()` - Check retry limit + +**`Reconciler.ts`** - Directory sweeps +- `findItems()` - Find items with filters +- `findPending()` - Find pending items +- `findStale()` - Find stale locks +- `findErrors()` - Find error items +- `reconcile()` - Reconcile directory +- `findProcessable()` - Find items ready to process +- `getStats()` - Get queue statistics + +**`Processor.ts`** - Processing orchestration +- `processItem()` - Process single item (claim → execute → update) +- `processDirectory()` - Process all pending in directory +- Concurrency control +- Retry logic +- Error handling + +**`index.ts`** - Public API +- `createQueue()` - Initialize queue with all managers +- `createFrontmatter()` - Create basic frontmatter +- Helper functions +- Exports all types and classes + +## Usage Example + +### Basic Setup + +```typescript +import { createQueue } from 'md-queue'; + +const queue = createQueue({ + basePath: '/Users/me/vault', + lockTimeout: 5 * 60 * 1000, // 5 minutes + maxRetries: 3 +}); +``` + +### Voice Memo Processing (Mac App) + +```typescript +// Create per-asset file when voice memo detected +const frontmatter = createFrontmatter('voice_memo', voiceMemoPath); +frontmatter.source = { + path: voiceMemoPath, + created_at: new Date().toISOString(), + duration: audioDuration, + size: fileSize +}; + +await queue.assetManager.create( + 'life-assets/voice/2025/memo-001.md', + frontmatter, + '' // No content yet +); + +// Reconcile and process pending +const pending = await queue.reconciler.findPending('life-assets/voice'); + +for (const item of pending) { + await queue.processor.processItem(item, async (item) => { + // Transcribe + const transcript = await transcribeAudio(item.frontmatter.source.path); + + // Update with result + await queue.assetManager.updateFrontmatter(item.path, { + status: { phase: 'done' }, + models: { + whisper: { + model: 'base', + at: new Date().toISOString(), + text: transcript, + detected_language: 'en', + confidence: 0.95 + } + } + }); + + // Emit rollup + await emitRollup(item, transcript); + }); +} +``` + +### Queue Processing (ccremote) + +```typescript +// Watch _q/high/ folder +const pending = await queue.reconciler.findPending('_q/high'); + +await queue.processor.processDirectory( + '_q/high', + async (item) => { + // Build Claude prompt + const prompt = buildQueuePrompt(item); + + // Spawn Claude session + await spawnClaudeSession({ + name: `q-high-${Date.now()}`, + prompt, + discord: true + }); + + // Archive + await queue.assetManager.move( + item.path, + item.path.replace('_q/high', '_q/archive/high') + ); + }, + { + maxConcurrent: 3, + stopOnError: false + } +); +``` + +### Reprocessing (Wrong Language Detected) + +```typescript +// Find item to reprocess +const item = await queue.assetManager.read( + 'life-assets/voice/2025/memo-001.md' +); + +// Check if reprocessing needed +if (item.frontmatter.models?.whisper?.confidence < 0.5) { + // Reset to pending with reason + await queue.stateManager.resetToPending( + item, + 'low_confidence_language_detection', + queue.assetManager + ); + + // Set language override + await queue.assetManager.updateFrontmatter(item.path, { + options: { + force_language: 'en' + } + }); +} +``` + +### Reconciliation + +```typescript +// Run periodic reconciliation +setInterval(async () => { + const report = await queue.reconciler.reconcile('life-assets/voice'); + + console.log('Reconciliation Report:', { + pending: report.pending, + processing: report.processing, + done: report.done, + error: report.error, + staleReset: report.staleReset, + timestamp: report.timestamp + }); + + // Alert if too many errors + if (report.error > 10) { + sendDiscordNotification('Too many errors in voice queue'); + } +}, 5 * 60 * 1000); // Every 5 minutes +``` + +## State Machine + +``` +pending ──────────► processing ──────────► done + ▲ │ + │ │ + └─────────────────────┴──────────► error +``` + +**Valid Transitions:** +- `pending → processing` - Lock acquired, processing starts +- `processing → done` - Processing succeeded +- `processing → error` - Processing failed +- `done → pending` - Reprocessing requested +- `error → pending` - Retry after error + +## Frontmatter Structure + +### Voice Memo Example + +```yaml +--- +type: voice_memo +status: + phase: done + last_update: 2025-10-20T20:30:00Z + attempts: 1 + lock: null +source: + path: ~/Library/Group Containers/group.com.apple.VoiceMemos.shared/Recordings/memo.m4a + created_at: 2025-10-20T20:25:00Z + duration: 12.7 + size: 45632 +models: + whisper: + model: base + at: 2025-10-20T20:30:15Z + text: "Avåsosenthal är 11 av november klockan 12 30..." + detected_language: sv + confidence: 0.865 +timestamp: 2025-10-20T20:25:00Z +--- + +# Transcription + +... +``` + +### Rollup Queue Item Example + +```yaml +--- +type: rollup +priority: high +status: + phase: pending + last_update: 2025-10-20T20:31:00Z + attempts: 0 + lock: null +source: + type: voice_memo + asset_path: life-assets/voice/2025/memo-001.md +timestamp: 2025-10-20T20:31:00Z +--- + +New voice memo transcribed: [link to asset] + +Transcript: +> Avåsosenthal är 11 av november... + +Please process this into the diary. +``` + +## Implementation Notes + +### Bun vs Node Compatibility + +Write in vanilla TypeScript. Avoid: +- Bun-specific APIs +- Node-specific APIs (where possible) + +Use conditional imports or runtime detection where needed. + +### Atomic Writes + +Always use `.tmp` → rename pattern: + +```typescript +// Write to temporary file +await fs.writeFile(`${path}.tmp`, content); + +// Atomic rename +await fs.rename(`${path}.tmp`, path); +``` + +This ensures sync services never see partial files. + +### Lock Timeout + +Default: **5 minutes** + +Adjust based on task duration: +- Voice transcription: 1-2 minutes (short timeout OK) +- Photo captioning: 5-10 seconds (very short) +- Claude processing: 5-10 minutes (longer timeout) + +### Error Handling + +- **Transient errors**: Retry with exponential backoff +- **Permanent errors**: Mark as error, require manual intervention +- **Max retries**: Default 3, configurable + +### Concurrency + +Default: **1** (sequential processing) + +Can be increased for: +- Bulk imports +- Batch processing +- High-volume queues + +Use `maxConcurrent` option carefully - locks prevent conflicts, but file system can still be overwhelmed. + +## Next Steps + +1. **Implement in ccremote** (Step 3) + - Create `ccremote/src/md-queue/` directory + - Copy these files + - Implement all methods + - Write tests + +2. **Use in ccremote** (Step 4) + - Update QueueManager to use md-queue + - Test with `_q/` folders + +3. **Use in Electron** (Step 5) + - Import md-queue from ccremote + - Update voice processor + - Test with voice memos + +4. **End-to-end testing** (Step 7) + - Voice memo → diary entry + - Photo → diary entry + - Manual queue item → Claude processing + +## Files in This Design + +- `README.md` - This file (design overview) +- `types.ts` - Core type definitions +- `AssetManager.ts` - File operations +- `LockManager.ts` - Lock management +- `StateManager.ts` - State transitions +- `Reconciler.ts` - Directory sweeps +- `Processor.ts` - Processing orchestration +- `index.ts` - Public API + +All files are TypeScript **interfaces only** - no implementation yet. These will be moved to `ccremote/src/md-queue/` and implemented there. + +--- + +**Status:** ✅ Design complete, ready for implementation +**Next:** Move to ccremote and implement diff --git a/src/md-queue/Reconciler.ts b/src/md-queue/Reconciler.ts new file mode 100644 index 0000000..345843c --- /dev/null +++ b/src/md-queue/Reconciler.ts @@ -0,0 +1,317 @@ +/** + * Reconciler: Directory sweep and reconciliation for md-queue + * + * Finds queue items in directories, identifies stale locks, + * and provides reconciliation reports. + */ + +import type { AssetManager } from './AssetManager'; +import type { LockManager } from './LockManager'; +import type { StateManager } from './StateManager'; +import type { + FilterOptions, + QueueConfig, + QueueItem, + ReconciliationReport, +} from './types'; +import { promises as fs } from 'node:fs'; +import * as path from 'node:path'; + +/** + * Handles directory sweeps and reconciliation + */ +export class Reconciler { + private assetManager: AssetManager; + private lockManager: LockManager; + private stateManager: StateManager; + + constructor( + assetManager: AssetManager, + lockManager: LockManager, + stateManager: StateManager, + _config?: Partial, + ) { + this.assetManager = assetManager; + this.lockManager = lockManager; + this.stateManager = stateManager; + } + + /** + * Find all markdown files in a directory (recursive) + * + * @param directory - Directory to search + * @param filter - Optional filter criteria + * @returns Array of queue items + */ + async findItems( + directory: string, + filter?: FilterOptions, + ): Promise { + // Find all markdown files + const markdownFiles = await this.findMarkdownFiles(directory); + + // Read and parse each file + const items: QueueItem[] = []; + for (const filePath of markdownFiles) { + try { + const item = await this.assetManager.read(filePath); + if (item && this.matchesFilter(item, filter)) { + items.push(item); + } + } + catch (error) { + // Skip files that can't be parsed + console.error(`Failed to read ${filePath}:`, error); + } + } + + // Apply limit if specified + if (filter?.limit) { + return items.slice(0, filter.limit); + } + + return items; + } + + /** + * Find items in pending phase + * + * @param directory - Directory to search + * @returns Array of pending items + */ + async findPending(directory: string): Promise { + return this.findItems(directory, { phase: 'pending' }); + } + + /** + * Find items with stale locks + * + * @param directory - Directory to search + * @param timeoutMs - Lock timeout (optional, uses default) + * @returns Array of items with stale locks + */ + async findStale( + directory: string, + timeoutMs?: number, + ): Promise { + // Find all items in processing phase + const processingItems = await this.findItems(directory, { phase: 'processing' }); + + // Filter by stale locks + return processingItems.filter((item) => { + const lock = item.frontmatter.status.lock; + return lock && this.lockManager.isStale(lock, timeoutMs); + }); + } + + /** + * Find items in error phase + * + * @param directory - Directory to search + * @returns Array of error items + */ + async findErrors(directory: string): Promise { + return this.findItems(directory, { phase: 'error' }); + } + + /** + * Reconcile a directory (find and reset stale locks) + * + * @param directory - Directory to reconcile + * @returns Reconciliation report + */ + async reconcile(directory: string): Promise { + // Find all items + const allItems = await this.findItems(directory); + + // Count by phase + const stats = { + pending: 0, + processing: 0, + done: 0, + error: 0, + staleReset: 0, + }; + + for (const item of allItems) { + const phase = item.frontmatter.status.phase; + stats[phase]++; + } + + // Find and reset stale locks + const staleItems = await this.findStale(directory); + for (const item of staleItems) { + await this.lockManager.resetStaleLock(item, this.assetManager); + stats.staleReset++; + } + + return { + ...stats, + timestamp: new Date().toISOString(), + }; + } + + /** + * Find items that can be processed now + * + * Returns items in pending phase that are not locked. + * + * @param directory - Directory to search + * @param limit - Maximum items to return + * @returns Array of processable items + */ + async findProcessable( + directory: string, + limit?: number, + ): Promise { + // Find pending items + const pendingItems = await this.findPending(directory); + + // Filter out items locked by other processes + const processable = pendingItems.filter( + item => !this.lockManager.isLockedByOther(item), + ); + + // Apply limit if specified + if (limit) { + return processable.slice(0, limit); + } + + return processable; + } + + /** + * Get queue statistics for a directory + * + * @param directory - Directory to analyze + * @returns Statistics object + */ + async getStats(directory: string): Promise<{ + total: number; + pending: number; + processing: number; + done: number; + error: number; + stale: number; + }> { + // Find all items + const allItems = await this.findItems(directory); + + // Initialize counts + const stats = { + total: allItems.length, + pending: 0, + processing: 0, + done: 0, + error: 0, + stale: 0, + }; + + // Count by phase and stale locks + for (const item of allItems) { + const phase = item.frontmatter.status.phase; + stats[phase]++; + + // Check for stale lock + if (phase === 'processing') { + const lock = item.frontmatter.status.lock; + if (lock && this.lockManager.isStale(lock)) { + stats.stale++; + } + } + } + + return stats; + } + + /** + * Recursively find all .md files in a directory + * + * @param directory - Directory to search + * @returns Array of absolute file paths + */ + protected async findMarkdownFiles(directory: string): Promise { + const results: string[] = []; + + try { + // Check if directory exists + await fs.access(directory); + + // Read directory contents + const entries = await fs.readdir(directory, { withFileTypes: true }); + + for (const entry of entries) { + const fullPath = path.join(directory, entry.name); + + if (entry.isDirectory()) { + // Recurse into subdirectories + const subResults = await this.findMarkdownFiles(fullPath); + results.push(...subResults); + } + else if (entry.isFile() && entry.name.endsWith('.md')) { + // Exclude .tmp files + if (!entry.name.endsWith('.tmp')) { + results.push(fullPath); + } + } + } + } + catch (error: any) { + // If directory doesn't exist, return empty array + if (error.code !== 'ENOENT') { + throw error; + } + } + + return results; + } + + /** + * Apply filter criteria to an item + * + * @param item - Queue item to check + * @param filter - Filter criteria + * @returns True if item matches filter + */ + protected matchesFilter( + item: QueueItem, + filter?: FilterOptions, + ): boolean { + if (!filter) { + return true; + } + + // Check phase filter + if (filter.phase) { + const phases = Array.isArray(filter.phase) ? filter.phase : [filter.phase]; + if (!phases.includes(item.frontmatter.status.phase)) { + return false; + } + } + + // Check type filter + if (filter.type) { + const types = Array.isArray(filter.type) ? filter.type : [filter.type]; + if (!types.includes(item.frontmatter.type)) { + return false; + } + } + + // Check priority filter + if (filter.priority && item.frontmatter.priority !== filter.priority) { + return false; + } + + // Check includeDone filter + if (filter.includeDone === false && item.frontmatter.status.phase === 'done') { + return false; + } + + // Check includeError filter + if (filter.includeError === false && item.frontmatter.status.phase === 'error') { + return false; + } + + return true; + } +} diff --git a/src/md-queue/StateManager.ts b/src/md-queue/StateManager.ts new file mode 100644 index 0000000..13f3856 --- /dev/null +++ b/src/md-queue/StateManager.ts @@ -0,0 +1,204 @@ +/** + * StateManager: State machine transitions for md-queue + * + * Handles transitions between phases: pending → processing → done/error + * Supports reprocessing by resetting items back to pending. + */ + +import type { AssetManager } from './AssetManager'; +import type { Phase, PreviousAttempt, QueueItem } from './types'; + +/** + * Manages state transitions for queue items + */ +export class StateManager { + /** + * Transition an item to a new phase + * + * @param item - Queue item to transition + * @param newPhase - Target phase + * @param metadata - Additional metadata to merge into frontmatter + * @param assetManager - AssetManager for updating frontmatter + */ + async transition( + item: QueueItem, + newPhase: Phase, + metadata: Record = {}, + assetManager: AssetManager, + ): Promise { + // Validate transition is allowed + this.validateTransition(item.frontmatter.status.phase, newPhase); + + // Update status + await assetManager.updateFrontmatter(item.path, { + status: { + ...item.frontmatter.status, + phase: newPhase, + last_update: new Date().toISOString(), + }, + ...metadata, + }); + } + + /** + * Mark item as done (successful processing) + * + * @param item - Queue item to mark done + * @param result - Optional result data to store in frontmatter + * @param assetManager - AssetManager for updating frontmatter + */ + async markDone( + item: QueueItem, + result: Record = {}, + assetManager: AssetManager, + ): Promise { + await assetManager.updateFrontmatter(item.path, { + status: { + ...item.frontmatter.status, + phase: 'done', + lock: null, + last_update: new Date().toISOString(), + }, + ...result, + }); + } + + /** + * Mark item as error (processing failed) + * + * @param item - Queue item to mark error + * @param error - Error that occurred + * @param assetManager - AssetManager for updating frontmatter + */ + async markError( + item: QueueItem, + error: Error, + assetManager: AssetManager, + ): Promise { + await assetManager.updateFrontmatter(item.path, { + status: { + ...item.frontmatter.status, + phase: 'error', + lock: null, + last_update: new Date().toISOString(), + last_error: error.message, + }, + }); + } + + /** + * Reset item to pending (for reprocessing) + * + * @param item - Queue item to reset + * @param reason - Reason for reprocessing + * @param assetManager - AssetManager for updating frontmatter + */ + async resetToPending( + item: QueueItem, + reason: string, + assetManager: AssetManager, + ): Promise { + // Archive current state to previous_attempts + const previousAttempt = this.archiveCurrentAttempt(item); + const previousAttempts = item.frontmatter.previous_attempts || []; + + await assetManager.updateFrontmatter(item.path, { + status: { + ...item.frontmatter.status, + phase: 'pending', + lock: null, + last_update: new Date().toISOString(), + reprocess_reason: reason, + attempts: 0, // Reset attempts for reprocessing + last_error: undefined, // Clear error + }, + previous_attempts: [...previousAttempts, previousAttempt], + }); + } + + /** + * Archive current processing attempt to history + * + * @param item - Queue item to archive + * @returns Previous attempt record + */ + protected archiveCurrentAttempt(item: QueueItem): PreviousAttempt { + const attempt: PreviousAttempt = { + phase: item.frontmatter.status.phase, + at: new Date().toISOString(), + }; + + // Include error message if phase is error + if (item.frontmatter.status.phase === 'error' && item.frontmatter.status.last_error) { + attempt.error = item.frontmatter.status.last_error; + } + + // Include model information if available + if (item.frontmatter.models) { + // Check for whisper model + if (item.frontmatter.models.whisper) { + attempt.model = item.frontmatter.models.whisper.model; + attempt.detected_language = item.frontmatter.models.whisper.detected_language; + attempt.confidence = item.frontmatter.models.whisper.confidence; + } + // Check for vision_caption model + else if (item.frontmatter.models.vision_caption) { + attempt.model = item.frontmatter.models.vision_caption.model; + } + } + + return attempt; + } + + /** + * Validate state transition is allowed + * + * @param currentPhase - Current phase + * @param newPhase - Target phase + * @throws Error if transition is not allowed + */ + protected validateTransition(currentPhase: Phase, newPhase: Phase): void { + const validTransitions: Record = { + pending: ['processing'], + processing: ['done', 'error'], + done: ['pending'], // Allow reprocessing + error: ['pending'], // Allow retry + }; + + const allowedTargets = validTransitions[currentPhase]; + if (!allowedTargets.includes(newPhase)) { + throw new Error( + `Invalid state transition: ${currentPhase} → ${newPhase}`, + ); + } + } + + /** + * Check if item has exceeded max retry attempts + * + * @param item - Queue item to check + * @param maxRetries - Maximum retry attempts (default: 3) + * @returns True if max retries exceeded + */ + hasExceededRetries(item: QueueItem, maxRetries: number = 3): boolean { + return item.frontmatter.status.attempts > maxRetries; + } + + /** + * Increment attempt counter + * + * @param item - Queue item to update + * @param assetManager - AssetManager for updating frontmatter + */ + async incrementAttempts( + item: QueueItem, + assetManager: AssetManager, + ): Promise { + await assetManager.updateFrontmatter(item.path, { + status: { + ...item.frontmatter.status, + attempts: item.frontmatter.status.attempts + 1, + }, + }); + } +} diff --git a/src/md-queue/index.ts b/src/md-queue/index.ts new file mode 100644 index 0000000..ab793de --- /dev/null +++ b/src/md-queue/index.ts @@ -0,0 +1,174 @@ +/** + * md-queue: Markdown-Based Queue System + * + * A unified queue implementation using markdown files with YAML frontmatter + * for state persistence. Works with both Bun (ccremote) and Node (Electron). + * + * @example + * ```typescript + * import { createQueue } from 'md-queue'; + * + * // Initialize queue + * const queue = createQueue({ basePath: '/path/to/vault' }); + * + * // Find pending items + * const pending = await queue.reconciler.findPending('_q/high'); + * + * // Process items + * for (const item of pending) { + * await queue.processor.processItem(item, async (item) => { + * // Your processing logic here + * console.log('Processing:', item.path); + * }); + * } + * ``` + */ + +import type { Frontmatter, Lock, QueueConfig } from './types'; +// Export all types +// Import managers for createQueue +import { AssetManager } from './AssetManager'; +import { LockManager } from './LockManager'; +import { Processor } from './Processor'; +import { Reconciler } from './Reconciler'; +import { StateManager } from './StateManager'; + +// Export managers +export { AssetManager } from './AssetManager'; + +export { LockManager } from './LockManager'; +export { Processor } from './Processor'; +export { Reconciler } from './Reconciler'; +export { StateManager } from './StateManager'; +export * from './types'; + +// Re-export commonly used types for convenience +export type { + FilterOptions, + Frontmatter, + Lock, + ModelResult, + Phase, + ProcessOptions, + ProcessReport, + QueueConfig, + QueueItem, + ReconciliationReport, + Status, +} from './types'; + +/** + * Queue instance with all managers + */ +export type Queue = { + assetManager: AssetManager; + lockManager: LockManager; + stateManager: StateManager; + reconciler: Reconciler; + processor: Processor; + config: QueueConfig; +}; + +/** + * Create a fully initialized queue instance + * + * @param config - Queue configuration + * @returns Queue instance with all managers + * + * @example + * ```typescript + * const queue = createQueue({ + * basePath: '/Users/me/vault', + * lockTimeout: 5 * 60 * 1000, // 5 minutes + * maxRetries: 3 + * }); + * ``` + */ +export function createQueue(config: QueueConfig): Queue { + // Create managers + const assetManager = new AssetManager(); + const lockManager = new LockManager(config); + const stateManager = new StateManager(); + const reconciler = new Reconciler(assetManager, lockManager, stateManager, config); + const processor = new Processor(assetManager, lockManager, stateManager, reconciler); + + return { + assetManager, + lockManager, + stateManager, + reconciler, + processor, + config, + }; +} + +/** + * Helper: Create a basic queue item frontmatter + * + * @param type - Item type (e.g., 'voice_memo', 'photo', 'rollup') + * @param sourcePath - Path to source file (optional) + * @returns Initialized frontmatter object + */ +export function createFrontmatter( + type: string, + sourcePath?: string, +): Frontmatter { + return { + type, + status: { + phase: 'pending', + last_update: new Date().toISOString(), + attempts: 0, + lock: null, + }, + source: sourcePath + ? { + path: sourcePath, + } + : undefined, + timestamp: new Date().toISOString(), + }; +} + +/** + * Helper: Parse lock string to Lock object + * + * @param lockString - Lock string (host:pid:timestamp) + * @returns Parsed lock or null + */ +export function parseLock(lockString: string): Lock | null { + const parts = lockString.split(':'); + if (parts.length !== 3) { + return null; + } + + const [host, pidStr, timestampStr] = parts; + const pid = Number.parseInt(pidStr, 10); + const timestamp = Number.parseInt(timestampStr, 10); + + if (Number.isNaN(pid) || Number.isNaN(timestamp)) { + return null; + } + + return { host, pid, timestamp }; +} + +/** + * Helper: Check if a lock is stale + * + * @param lockString - Lock string to check + * @param timeoutMs - Timeout in milliseconds (default: 5 minutes) + * @returns True if stale + */ +export function isLockStale( + lockString: string, + timeoutMs: number = 5 * 60 * 1000, +): boolean { + const lock = parseLock(lockString); + if (!lock) { + return true; // Invalid lock is considered stale + } + + const now = Date.now(); + return now - lock.timestamp > timeoutMs; +} diff --git a/src/md-queue/types.ts b/src/md-queue/types.ts new file mode 100644 index 0000000..31e01f5 --- /dev/null +++ b/src/md-queue/types.ts @@ -0,0 +1,270 @@ +/** + * md-queue: Markdown-Based Queue System + * + * Core type definitions for queue items, locks, and processing. + * Designed to work with both Bun (ccremote) and Node (Electron). + */ + +/** + * Queue item phase in the state machine + */ +export type Phase = 'pending' | 'processing' | 'done' | 'error'; + +/** + * Processing status with state machine tracking + */ +export type Status = { + /** Current phase in the state machine */ + phase: Phase; + + /** ISO 8601 timestamp of last status update */ + last_update: string; + + /** Number of processing attempts */ + attempts: number; + + /** Lock string (host:pid:timestamp) or null if not locked */ + lock: string | null; + + /** Error message if phase is 'error' */ + last_error?: string; + + /** Reason for reprocessing (if item was reset from done/error to pending) */ + reprocess_reason?: string; +}; + +/** + * Lock information (parsed from lock string) + */ +export type Lock = { + /** Hostname of the machine holding the lock */ + host: string; + + /** Process ID holding the lock */ + pid: number; + + /** Unix timestamp (ms) when lock was acquired */ + timestamp: number; +}; + +/** + * Model processing result (e.g., Whisper transcription, VLM caption) + */ +export type ModelResult = { + /** Model identifier (e.g., 'base', 'minicpm-v:4.5') */ + model: string; + + /** ISO 8601 timestamp when processed */ + at: string; + + /** Model output (transcript, caption, etc.) */ + text: string; + + /** Detected language code (for Whisper) */ + detected_language?: string; + + /** Language detection confidence (0-1) */ + confidence?: number; + + /** Any additional metadata */ + [key: string]: any; +}; + +/** + * Previous processing attempt (for reprocessing tracking) + */ +export type PreviousAttempt = { + /** Phase reached in this attempt */ + phase: Phase; + + /** Model used (if applicable) */ + model?: string; + + /** Language detected (if applicable) */ + detected_language?: string; + + /** Confidence score (if applicable) */ + confidence?: number; + + /** ISO 8601 timestamp */ + at: string; + + /** Error message (if failed) */ + error?: string; +}; + +/** + * Queue item frontmatter structure + */ +export type Frontmatter = { + /** Item type (e.g., 'voice_memo', 'photo', 'rollup') */ + type: string; + + /** Processing status */ + status: Status; + + /** Model processing results (e.g., whisper, vision_caption) */ + models?: { + [modelType: string]: ModelResult; + }; + + /** Source information (e.g., original file path, photo UUID) */ + source?: { + /** Path to source file */ + path?: string; + + /** Photo UUID (for photos) */ + uuid?: string; + + /** Creation timestamp */ + created_at?: string; + + /** File size in bytes */ + size?: number; + + /** Duration in seconds (for audio/video) */ + duration?: number; + + [key: string]: any; + }; + + /** Previous processing attempts (for reprocessing) */ + previous_attempts?: PreviousAttempt[]; + + /** Processing options/overrides */ + options?: { + /** Force specific language (override auto-detection) */ + force_language?: string; + + /** Force specific model */ + force_model?: string; + + [key: string]: any; + }; + + /** Priority for rollup items */ + priority?: 'high' | 'medium' | 'low'; + + /** Timestamp when item was created */ + timestamp?: string; + + /** Allow additional fields */ + [key: string]: any; +}; + +/** + * Complete queue item (frontmatter + content) + */ +export type QueueItem = { + /** Absolute path to the markdown file */ + path: string; + + /** Parsed frontmatter */ + frontmatter: Frontmatter; + + /** Markdown content (after frontmatter) */ + content: string; +}; + +/** + * Reconciliation report from directory sweep + */ +export type ReconciliationReport = { + /** Number of items in pending phase */ + pending: number; + + /** Number of items in processing phase */ + processing: number; + + /** Number of items in done phase */ + done: number; + + /** Number of items in error phase */ + error: number; + + /** Number of stale locks reset to pending */ + staleReset: number; + + /** ISO 8601 timestamp of reconciliation */ + timestamp: string; +}; + +/** + * Processing report from batch processing + */ +export type ProcessReport = { + /** Number of items successfully processed */ + processed: number; + + /** Number of items that failed */ + failed: number; + + /** Number of items skipped (already processing) */ + skipped: number; + + /** Total processing time in milliseconds */ + duration: number; + + /** ISO 8601 timestamp when processing started */ + started_at: string; + + /** ISO 8601 timestamp when processing finished */ + finished_at: string; +}; + +/** + * Filter options for finding items + */ +export type FilterOptions = { + /** Filter by phase */ + phase?: Phase | Phase[]; + + /** Filter by type */ + type?: string | string[]; + + /** Filter by priority */ + priority?: 'high' | 'medium' | 'low'; + + /** Include done items */ + includeDone?: boolean; + + /** Include error items */ + includeError?: boolean; + + /** Maximum items to return */ + limit?: number; +}; + +/** + * Processing options + */ +export type ProcessOptions = { + /** Maximum concurrent processing */ + maxConcurrent?: number; + + /** Lock timeout in milliseconds (default: 5 minutes) */ + lockTimeout?: number; + + /** Maximum retries on error (default: 3) */ + maxRetries?: number; + + /** Stop on first error */ + stopOnError?: boolean; +}; + +/** + * Configuration for md-queue + */ +export type QueueConfig = { + /** Base path to vault/project */ + basePath: string; + + /** Default lock timeout in milliseconds */ + lockTimeout?: number; + + /** Default max retries */ + maxRetries?: number; + + /** Hostname for locks (auto-detected if not provided) */ + hostname?: string; +};