From b878e8134b9e58f0648d7b89c0fc3f7594ffe16c Mon Sep 17 00:00:00 2001 From: ithiria894 Date: Fri, 27 Mar 2026 18:34:18 -0700 Subject: [PATCH 1/4] =?UTF-8?q?feat:=20MVP=20v0.1=20=E2=80=94=20init,=20ex?= =?UTF-8?q?port,=20status,=20watch=20commands?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Working CLI with 4 commands: - `aisona init` — detect existing AI configs, import CLAUDE.md, generate aisona.yml - `aisona export --to ` / `--all` — export aisona.yml to Claude/Cursor/Gemini/Copilot - `aisona status` — show persona summary and tool detection - `aisona watch --git` — file watcher with auto re-export and optional git sync Includes: - CLAUDE.md parser (heuristic section detection) - 4 Handlebars export templates (claude, cursor, gemini, copilot) - Tool detection for 6 AI coding tools - Debounced file watcher (chokidar) - Git auto-commit+push on change (simple-git) Co-Authored-By: Claude Opus 4.6 (1M context) --- bin/aisona.js | 43 +- package-lock.json | 881 ++++++++++++++++++++++++++++++++++++++ package.json | 12 +- src/commands/export.js | 65 +++ src/commands/init.js | 117 +++++ src/commands/status.js | 63 +++ src/commands/watch.js | 111 +++++ src/index.js | 4 +- src/lib/config.js | 63 +++ src/lib/exporter.js | 86 ++++ src/lib/parser.js | 182 ++++++++ src/templates/claude.hbs | 62 +++ src/templates/copilot.hbs | 35 ++ src/templates/cursor.hbs | 46 ++ src/templates/gemini.hbs | 58 +++ 15 files changed, 1824 insertions(+), 4 deletions(-) create mode 100644 package-lock.json create mode 100644 src/commands/export.js create mode 100644 src/commands/init.js create mode 100644 src/commands/status.js create mode 100644 src/commands/watch.js create mode 100644 src/lib/config.js create mode 100644 src/lib/exporter.js create mode 100644 src/lib/parser.js create mode 100644 src/templates/claude.hbs create mode 100644 src/templates/copilot.hbs create mode 100644 src/templates/cursor.hbs create mode 100644 src/templates/gemini.hbs diff --git a/bin/aisona.js b/bin/aisona.js index 8f0f8a4..bd05a8b 100755 --- a/bin/aisona.js +++ b/bin/aisona.js @@ -1,3 +1,42 @@ #!/usr/bin/env node -console.log('aisona — Own your AI\'s persona. Coming soon.'); -console.log('https://github.com/mcpware/aisona'); + +import { program } from 'commander'; +import { initCommand } from '../src/commands/init.js'; +import { exportCommand } from '../src/commands/export.js'; +import { statusCommand } from '../src/commands/status.js'; +import { watchCommand } from '../src/commands/watch.js'; + +program + .name('aisona') + .description('Own your AI\'s persona. Define it once, use it everywhere.') + .version('0.1.0'); + +program + .command('init') + .description('Create aisona.yml from your existing AI tool configs') + .option('--from ', 'Import from a specific tool (claude, cursor, gemini)') + .option('--dir ', 'Working directory', process.cwd()) + .action(initCommand); + +program + .command('export') + .description('Export aisona.yml to AI tool configs') + .option('--to ', 'Export to a specific tool (claude, cursor, gemini, copilot)') + .option('--all', 'Export to all enabled tools') + .option('--dir ', 'Working directory', process.cwd()) + .action(exportCommand); + +program + .command('status') + .description('Show what tools are detected and sync status') + .option('--dir ', 'Working directory', process.cwd()) + .action(statusCommand); + +program + .command('watch') + .description('Watch aisona.yml and auto re-export on change') + .option('--dir ', 'Working directory', process.cwd()) + .option('--git', 'Auto git commit + push on change') + .action(watchCommand); + +program.parse(); diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..1ea8f46 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,881 @@ +{ + "name": "@agents-io/aisona", + "version": "0.0.1", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "@agents-io/aisona", + "version": "0.0.1", + "license": "MIT", + "dependencies": { + "chalk": "^5.6.2", + "chokidar": "^5.0.0", + "commander": "^14.0.3", + "handlebars": "^4.7.9", + "inquirer": "^13.3.2", + "js-yaml": "^4.1.1", + "ora": "^9.3.0", + "simple-git": "^3.33.0" + }, + "bin": { + "aisona": "bin/aisona.js" + } + }, + "node_modules/@inquirer/ansi": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/@inquirer/ansi/-/ansi-2.0.4.tgz", + "integrity": "sha512-DpcZrQObd7S0R/U3bFdkcT5ebRwbTTC4D3tCc1vsJizmgPLxNJBo+AAFmrZwe8zk30P2QzgzGWZ3Q9uJwWuhIg==", + "license": "MIT", + "engines": { + "node": ">=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0" + } + }, + "node_modules/@inquirer/checkbox": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/@inquirer/checkbox/-/checkbox-5.1.2.tgz", + "integrity": "sha512-PubpMPO2nJgMufkoB3P2wwxNXEMUXnBIKi/ACzDUYfaoPuM7gSTmuxJeMscoLVEsR4qqrCMf5p0SiYGWnVJ8kw==", + "license": "MIT", + "dependencies": { + "@inquirer/ansi": "^2.0.4", + "@inquirer/core": "^11.1.7", + "@inquirer/figures": "^2.0.4", + "@inquirer/type": "^4.0.4" + }, + "engines": { + "node": ">=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/confirm": { + "version": "6.0.10", + "resolved": "https://registry.npmjs.org/@inquirer/confirm/-/confirm-6.0.10.tgz", + "integrity": "sha512-tiNyA73pgpQ0FQ7axqtoLUe4GDYjNCDcVsbgcA5anvwg2z6i+suEngLKKJrWKJolT//GFPZHwN30binDIHgSgQ==", + "license": "MIT", + "dependencies": { + "@inquirer/core": "^11.1.7", + "@inquirer/type": "^4.0.4" + }, + "engines": { + "node": ">=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/core": { + "version": "11.1.7", + "resolved": "https://registry.npmjs.org/@inquirer/core/-/core-11.1.7.tgz", + "integrity": "sha512-1BiBNDk9btIwYIzNZpkikIHXWeNzNncJePPqwDyVMhXhD1ebqbpn1mKGctpoqAbzywZfdG0O4tvmsGIcOevAPQ==", + "license": "MIT", + "dependencies": { + "@inquirer/ansi": "^2.0.4", + "@inquirer/figures": "^2.0.4", + "@inquirer/type": "^4.0.4", + "cli-width": "^4.1.0", + "fast-wrap-ansi": "^0.2.0", + "mute-stream": "^3.0.0", + "signal-exit": "^4.1.0" + }, + "engines": { + "node": ">=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/editor": { + "version": "5.0.10", + "resolved": "https://registry.npmjs.org/@inquirer/editor/-/editor-5.0.10.tgz", + "integrity": "sha512-VJx4XyaKea7t8hEApTw5dxeIyMtWXre2OiyJcICCRZI4hkoHsMoCnl/KbUnJJExLbH9csLLHMVR144ZhFE1CwA==", + "license": "MIT", + "dependencies": { + "@inquirer/core": "^11.1.7", + "@inquirer/external-editor": "^2.0.4", + "@inquirer/type": "^4.0.4" + }, + "engines": { + "node": ">=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/expand": { + "version": "5.0.10", + "resolved": "https://registry.npmjs.org/@inquirer/expand/-/expand-5.0.10.tgz", + "integrity": "sha512-fC0UHJPXsTRvY2fObiwuQYaAnHrp3aDqfwKUJSdfpgv18QUG054ezGbaRNStk/BKD5IPijeMKWej8VV8O5Q/eQ==", + "license": "MIT", + "dependencies": { + "@inquirer/core": "^11.1.7", + "@inquirer/type": "^4.0.4" + }, + "engines": { + "node": ">=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/external-editor": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/@inquirer/external-editor/-/external-editor-2.0.4.tgz", + "integrity": "sha512-Prenuv9C1PHj2Itx0BcAOVBTonz02Hc2Nd2DbU67PdGUaqn0nPCnV34oDyyoaZHnmfRxkpuhh/u51ThkrO+RdA==", + "license": "MIT", + "dependencies": { + "chardet": "^2.1.1", + "iconv-lite": "^0.7.2" + }, + "engines": { + "node": ">=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/figures": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/@inquirer/figures/-/figures-2.0.4.tgz", + "integrity": "sha512-eLBsjlS7rPS3WEhmOmh1znQ5IsQrxWzxWDxO51e4urv+iVrSnIHbq4zqJIOiyNdYLa+BVjwOtdetcQx1lWPpiQ==", + "license": "MIT", + "engines": { + "node": ">=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0" + } + }, + "node_modules/@inquirer/input": { + "version": "5.0.10", + "resolved": "https://registry.npmjs.org/@inquirer/input/-/input-5.0.10.tgz", + "integrity": "sha512-nvZ6qEVeX/zVtZ1dY2hTGDQpVGD3R7MYPLODPgKO8Y+RAqxkrP3i/3NwF3fZpLdaMiNuK0z2NaYIx9tPwiSegQ==", + "license": "MIT", + "dependencies": { + "@inquirer/core": "^11.1.7", + "@inquirer/type": "^4.0.4" + }, + "engines": { + "node": ">=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/number": { + "version": "4.0.10", + "resolved": "https://registry.npmjs.org/@inquirer/number/-/number-4.0.10.tgz", + "integrity": "sha512-Ht8OQstxiS3APMGjHV0aYAjRAysidWdwurWEo2i8yI5xbhOBWqizT0+MU1S2GCcuhIBg+3SgWVjEoXgfhY+XaA==", + "license": "MIT", + "dependencies": { + "@inquirer/core": "^11.1.7", + "@inquirer/type": "^4.0.4" + }, + "engines": { + "node": ">=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/password": { + "version": "5.0.10", + "resolved": "https://registry.npmjs.org/@inquirer/password/-/password-5.0.10.tgz", + "integrity": "sha512-QbNyvIE8q2GTqKLYSsA8ATG+eETo+m31DSR0+AU7x3d2FhaTWzqQek80dj3JGTo743kQc6mhBR0erMjYw5jQ0A==", + "license": "MIT", + "dependencies": { + "@inquirer/ansi": "^2.0.4", + "@inquirer/core": "^11.1.7", + "@inquirer/type": "^4.0.4" + }, + "engines": { + "node": ">=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/prompts": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/@inquirer/prompts/-/prompts-8.3.2.tgz", + "integrity": "sha512-yFroiSj2iiBFlm59amdTvAcQFvWS6ph5oKESls/uqPBect7rTU2GbjyZO2DqxMGuIwVA8z0P4K6ViPcd/cp+0w==", + "license": "MIT", + "dependencies": { + "@inquirer/checkbox": "^5.1.2", + "@inquirer/confirm": "^6.0.10", + "@inquirer/editor": "^5.0.10", + "@inquirer/expand": "^5.0.10", + "@inquirer/input": "^5.0.10", + "@inquirer/number": "^4.0.10", + "@inquirer/password": "^5.0.10", + "@inquirer/rawlist": "^5.2.6", + "@inquirer/search": "^4.1.6", + "@inquirer/select": "^5.1.2" + }, + "engines": { + "node": ">=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/rawlist": { + "version": "5.2.6", + "resolved": "https://registry.npmjs.org/@inquirer/rawlist/-/rawlist-5.2.6.tgz", + "integrity": "sha512-jfw0MLJ5TilNsa9zlJ6nmRM0ZFVZhhTICt4/6CU2Dv1ndY7l3sqqo1gIYZyMMDw0LvE1u1nzJNisfHEhJIxq5w==", + "license": "MIT", + "dependencies": { + "@inquirer/core": "^11.1.7", + "@inquirer/type": "^4.0.4" + }, + "engines": { + "node": ">=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/search": { + "version": "4.1.6", + "resolved": "https://registry.npmjs.org/@inquirer/search/-/search-4.1.6.tgz", + "integrity": "sha512-3/6kTRae98hhDevENScy7cdFEuURnSpM3JbBNg8yfXLw88HgTOl+neUuy/l9W0No5NzGsLVydhBzTIxZP7yChQ==", + "license": "MIT", + "dependencies": { + "@inquirer/core": "^11.1.7", + "@inquirer/figures": "^2.0.4", + "@inquirer/type": "^4.0.4" + }, + "engines": { + "node": ">=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/select": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/@inquirer/select/-/select-5.1.2.tgz", + "integrity": "sha512-kTK8YIkHV+f02y7bWCh7E0u2/11lul5WepVTclr3UMBtBr05PgcZNWfMa7FY57ihpQFQH/spLMHTcr0rXy50tA==", + "license": "MIT", + "dependencies": { + "@inquirer/ansi": "^2.0.4", + "@inquirer/core": "^11.1.7", + "@inquirer/figures": "^2.0.4", + "@inquirer/type": "^4.0.4" + }, + "engines": { + "node": ">=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/type": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/@inquirer/type/-/type-4.0.4.tgz", + "integrity": "sha512-PamArxO3cFJZoOzspzo6cxVlLeIftyBsZw/S9bKY5DzxqJVZgjoj1oP8d0rskKtp7sZxBycsoer1g6UeJV1BBA==", + "license": "MIT", + "engines": { + "node": ">=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@kwsites/file-exists": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@kwsites/file-exists/-/file-exists-1.1.1.tgz", + "integrity": "sha512-m9/5YGR18lIwxSFDwfE3oA7bWuq9kdau6ugN4H2rJeyhFQZcG9AgSHkQtSD15a8WvTgfz9aikZMrKPHvbpqFiw==", + "license": "MIT", + "dependencies": { + "debug": "^4.1.1" + } + }, + "node_modules/@kwsites/promise-deferred": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@kwsites/promise-deferred/-/promise-deferred-1.1.1.tgz", + "integrity": "sha512-GaHYm+c0O9MjZRu0ongGBRbinu8gVAMd2UZjji6jVmqKtZluZnptXGWhz1E8j8D2HJ3f/yMxKAUC0b+57wncIw==", + "license": "MIT" + }, + "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/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "license": "Python-2.0" + }, + "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/chardet": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/chardet/-/chardet-2.1.1.tgz", + "integrity": "sha512-PsezH1rqdV9VvyNhxxOW32/d75r01NY7TQCmOqomRo15ZSOKbpTFVsfjghxo6JloQUCGnH4k1LGu0R4yCLlWQQ==", + "license": "MIT" + }, + "node_modules/chokidar": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-5.0.0.tgz", + "integrity": "sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw==", + "license": "MIT", + "dependencies": { + "readdirp": "^5.0.0" + }, + "engines": { + "node": ">= 20.19.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/cli-cursor": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-5.0.0.tgz", + "integrity": "sha512-aCj4O5wKyszjMmDT4tZj93kxyydN/K5zPWSCe6/0AV/AA1pqe5ZBIw0a2ZfPQV7lL5/yb5HsUreJ6UFAF1tEQw==", + "license": "MIT", + "dependencies": { + "restore-cursor": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cli-spinners": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/cli-spinners/-/cli-spinners-3.4.0.tgz", + "integrity": "sha512-bXfOC4QcT1tKXGorxL3wbJm6XJPDqEnij2gQ2m7ESQuE+/z9YFIWnl/5RpTiKWbMq3EVKR4fRLJGn6DVfu0mpw==", + "license": "MIT", + "engines": { + "node": ">=18.20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cli-width": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-4.1.0.tgz", + "integrity": "sha512-ouuZd4/dm2Sw5Gmqy6bGyNNNe1qt9RpmxveLSO7KcgsTnU7RXfsw+/bukWGo1abgBiMAic068rclZsO4IWmmxQ==", + "license": "ISC", + "engines": { + "node": ">= 12" + } + }, + "node_modules/commander": { + "version": "14.0.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-14.0.3.tgz", + "integrity": "sha512-H+y0Jo/T1RZ9qPP4Eh1pkcQcLRglraJaSLoyOtHxu6AapkjWVCy2Sit1QQ4x3Dng8qDlSsZEet7g5Pq06MvTgw==", + "license": "MIT", + "engines": { + "node": ">=20" + } + }, + "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/fast-string-truncated-width": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/fast-string-truncated-width/-/fast-string-truncated-width-3.0.3.tgz", + "integrity": "sha512-0jjjIEL6+0jag3l2XWWizO64/aZVtpiGE3t0Zgqxv0DPuxiMjvB3M24fCyhZUO4KomJQPj3LTSUnDP3GpdwC0g==", + "license": "MIT" + }, + "node_modules/fast-string-width": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/fast-string-width/-/fast-string-width-3.0.2.tgz", + "integrity": "sha512-gX8LrtNEI5hq8DVUfRQMbr5lpaS4nMIWV+7XEbXk2b8kiQIizgnlr12B4dA3ZEx3308ze0O4Q1R+cHts8kyUJg==", + "license": "MIT", + "dependencies": { + "fast-string-truncated-width": "^3.0.2" + } + }, + "node_modules/fast-wrap-ansi": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/fast-wrap-ansi/-/fast-wrap-ansi-0.2.0.tgz", + "integrity": "sha512-rLV8JHxTyhVmFYhBJuMujcrHqOT2cnO5Zxj37qROj23CP39GXubJRBUFF0z8KFK77Uc0SukZUf7JZhsVEQ6n8w==", + "license": "MIT", + "dependencies": { + "fast-string-width": "^3.0.2" + } + }, + "node_modules/get-east-asian-width": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.5.0.tgz", + "integrity": "sha512-CQ+bEO+Tva/qlmw24dCejulK5pMzVnUOFOijVogd3KQs07HnRIgp8TGipvCCRT06xeYEbpbgwaCxglFyiuIcmA==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/handlebars": { + "version": "4.7.9", + "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.7.9.tgz", + "integrity": "sha512-4E71E0rpOaQuJR2A3xDZ+GM1HyWYv1clR58tC8emQNeQe3RH7MAzSbat+V0wG78LQBo6m6bzSG/L4pBuCsgnUQ==", + "license": "MIT", + "dependencies": { + "minimist": "^1.2.5", + "neo-async": "^2.6.2", + "source-map": "^0.6.1", + "wordwrap": "^1.0.0" + }, + "bin": { + "handlebars": "bin/handlebars" + }, + "engines": { + "node": ">=0.4.7" + }, + "optionalDependencies": { + "uglify-js": "^3.1.4" + } + }, + "node_modules/iconv-lite": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.2.tgz", + "integrity": "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/inquirer": { + "version": "13.3.2", + "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-13.3.2.tgz", + "integrity": "sha512-bh/OjBGxNR9qvfQj1n5bxtIF58mbOTp2InN5dKuwUK03dXcDGFsjlDinQRuXMZ4EGiJaFieUWHCAaxH2p7iUBw==", + "license": "MIT", + "dependencies": { + "@inquirer/ansi": "^2.0.4", + "@inquirer/core": "^11.1.7", + "@inquirer/prompts": "^8.3.2", + "@inquirer/type": "^4.0.4", + "mute-stream": "^3.0.0", + "run-async": "^4.0.6", + "rxjs": "^7.8.2" + }, + "engines": { + "node": ">=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/is-interactive": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-interactive/-/is-interactive-2.0.0.tgz", + "integrity": "sha512-qP1vozQRI+BMOPcjFzrjXuQvdak2pHNUMZoeG2eRbiSqyvbEf/wQtEOTOX1guk6E3t36RkaqiSt8A/6YElNxLQ==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-unicode-supported": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-2.1.0.tgz", + "integrity": "sha512-mE00Gnza5EEB3Ds0HfMyllZzbBrmLOX3vfWoj9A9PEnTfratQ/BcaJOuMhnkhjXvb2+FkY3VuHqtAGpTPmglFQ==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/js-yaml": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", + "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/log-symbols": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-7.0.1.tgz", + "integrity": "sha512-ja1E3yCr9i/0hmBVaM0bfwDjnGy8I/s6PP4DFp+yP+a+mrHO4Rm7DtmnqROTUkHIkqffC84YY7AeqX6oFk0WFg==", + "license": "MIT", + "dependencies": { + "is-unicode-supported": "^2.0.0", + "yoctocolors": "^2.1.1" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/mimic-function": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/mimic-function/-/mimic-function-5.0.1.tgz", + "integrity": "sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "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/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/mute-stream": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-3.0.0.tgz", + "integrity": "sha512-dkEJPVvun4FryqBmZ5KhDo0K9iDXAwn08tMLDinNdRBNPcYEDiWYysLcc6k3mjTMlbP9KyylvRpd4wFtwrT9rw==", + "license": "ISC", + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/neo-async": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", + "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==", + "license": "MIT" + }, + "node_modules/onetime": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-7.0.0.tgz", + "integrity": "sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ==", + "license": "MIT", + "dependencies": { + "mimic-function": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ora": { + "version": "9.3.0", + "resolved": "https://registry.npmjs.org/ora/-/ora-9.3.0.tgz", + "integrity": "sha512-lBX72MWFduWEf7v7uWf5DHp9Jn5BI8bNPGuFgtXMmr2uDz2Gz2749y3am3agSDdkhHPHYmmxEGSKH85ZLGzgXw==", + "license": "MIT", + "dependencies": { + "chalk": "^5.6.2", + "cli-cursor": "^5.0.0", + "cli-spinners": "^3.2.0", + "is-interactive": "^2.0.0", + "is-unicode-supported": "^2.1.0", + "log-symbols": "^7.0.1", + "stdin-discarder": "^0.3.1", + "string-width": "^8.1.0" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/readdirp": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-5.0.0.tgz", + "integrity": "sha512-9u/XQ1pvrQtYyMpZe7DXKv2p5CNvyVwzUB6uhLAnQwHMSgKMBR62lc7AHljaeteeHXn11XTAaLLUVZYVZyuRBQ==", + "license": "MIT", + "engines": { + "node": ">= 20.19.0" + }, + "funding": { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/restore-cursor": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-5.1.0.tgz", + "integrity": "sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA==", + "license": "MIT", + "dependencies": { + "onetime": "^7.0.0", + "signal-exit": "^4.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/run-async": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/run-async/-/run-async-4.0.6.tgz", + "integrity": "sha512-IoDlSLTs3Yq593mb3ZoKWKXMNu3UpObxhgA/Xuid5p4bbfi2jdY1Hj0m1K+0/tEuQTxIGMhQDqGjKb7RuxGpAQ==", + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/rxjs": { + "version": "7.8.2", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.2.tgz", + "integrity": "sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.1.0" + } + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "license": "MIT" + }, + "node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "license": "ISC", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/simple-git": { + "version": "3.33.0", + "resolved": "https://registry.npmjs.org/simple-git/-/simple-git-3.33.0.tgz", + "integrity": "sha512-D4V/tGC2sjsoNhoMybKyGoE+v8A60hRawKQ1iFRA1zwuDgGZCBJ4ByOzZ5J8joBbi4Oam0qiPH+GhzmSBwbJng==", + "license": "MIT", + "dependencies": { + "@kwsites/file-exists": "^1.1.1", + "@kwsites/promise-deferred": "^1.1.1", + "debug": "^4.4.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/steveukx/git-js?sponsor=1" + } + }, + "node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/stdin-discarder": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/stdin-discarder/-/stdin-discarder-0.3.1.tgz", + "integrity": "sha512-reExS1kSGoElkextOcPkel4NE99S0BWxjUHQeDFnR8S993JxpPX7KU4MNmO19NXhlJp+8dmdCbKQVNgLJh2teA==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/string-width": { + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-8.2.0.tgz", + "integrity": "sha512-6hJPQ8N0V0P3SNmP6h2J99RLuzrWz2gvT7VnK5tKvrNqJoyS9W4/Fb8mo31UiPvy00z7DQXkP2hnKBVav76thw==", + "license": "MIT", + "dependencies": { + "get-east-asian-width": "^1.5.0", + "strip-ansi": "^7.1.2" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/strip-ansi": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz", + "integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.2.2" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" + }, + "node_modules/uglify-js": { + "version": "3.19.3", + "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.19.3.tgz", + "integrity": "sha512-v3Xu+yuwBXisp6QYTcH4UbH+xYJXqnq2m/LtQVWKWzYc1iehYnLixoQDN9FH6/j9/oybfd6W9Ghwkl8+UMKTKQ==", + "license": "BSD-2-Clause", + "optional": true, + "bin": { + "uglifyjs": "bin/uglifyjs" + }, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/wordwrap": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz", + "integrity": "sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q==", + "license": "MIT" + }, + "node_modules/yoctocolors": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/yoctocolors/-/yoctocolors-2.1.2.tgz", + "integrity": "sha512-CzhO+pFNo8ajLM2d2IW/R93ipy99LWjtwblvC1RsoSUMZgyLbYFr221TnSNT7GjGdYui6P459mw9JH/g/zW2ug==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + } + } +} diff --git a/package.json b/package.json index 170da65..2877ecb 100644 --- a/package.json +++ b/package.json @@ -33,5 +33,15 @@ "homepage": "https://github.com/agents-io/aisona", "bugs": { "url": "https://github.com/agents-io/aisona/issues" + }, + "dependencies": { + "chalk": "^5.6.2", + "chokidar": "^5.0.0", + "commander": "^14.0.3", + "handlebars": "^4.7.9", + "inquirer": "^13.3.2", + "js-yaml": "^4.1.1", + "ora": "^9.3.0", + "simple-git": "^3.33.0" } -} \ No newline at end of file +} diff --git a/src/commands/export.js b/src/commands/export.js new file mode 100644 index 0000000..1c3f06c --- /dev/null +++ b/src/commands/export.js @@ -0,0 +1,65 @@ +import path from 'path'; +import chalk from 'chalk'; +import { findAisonaFile, loadAisona } from '../lib/config.js'; +import { exportToTool, exportToAll, getSupportedTools } from '../lib/exporter.js'; + +export async function exportCommand(options) { + const dir = path.resolve(options.dir || process.cwd()); + + console.log(chalk.bold('\n aisona export\n')); + + // Find aisona.yml + const aisonaPath = findAisonaFile(dir); + if (!aisonaPath) { + console.log(chalk.red(' No aisona.yml found. Run: aisona init\n')); + process.exit(1); + } + + const aisona = loadAisona(aisonaPath); + console.log(chalk.dim(` Using: ${aisonaPath}\n`)); + + if (options.to) { + // Export to specific tool + const toolId = options.to.toLowerCase(); + const supported = getSupportedTools(); + + if (!supported.includes(toolId)) { + console.log(chalk.red(` Unknown tool: ${toolId}`)); + console.log(chalk.dim(` Supported: ${supported.join(', ')}\n`)); + process.exit(1); + } + + const result = exportToTool(aisona, toolId, dir); + if (result.success) { + console.log(chalk.green(` Exported to ${result.tool} → ${result.path}\n`)); + } else { + console.log(chalk.red(` Failed: ${result.error}\n`)); + } + } else if (options.all) { + // Export to all enabled tools + const results = exportToAll(aisona, dir); + + if (results.length === 0) { + console.log(chalk.yellow(' No tools enabled in aisona.yml.\n')); + return; + } + + const succeeded = results.filter(r => r.success); + const failed = results.filter(r => !r.success); + + for (const r of succeeded) { + console.log(chalk.green(` ✓ ${r.tool} → ${r.path}`)); + } + for (const r of failed) { + console.log(chalk.red(` ✗ ${r.tool}: ${r.error}`)); + } + + console.log(chalk.dim(`\n Exported to ${succeeded.length} tool(s)\n`)); + } else { + console.log(chalk.yellow(' Specify --to or --all\n')); + console.log(chalk.dim(' Examples:')); + console.log(chalk.dim(' aisona export --to claude')); + console.log(chalk.dim(' aisona export --to cursor')); + console.log(chalk.dim(' aisona export --all\n')); + } +} diff --git a/src/commands/init.js b/src/commands/init.js new file mode 100644 index 0000000..5be6a74 --- /dev/null +++ b/src/commands/init.js @@ -0,0 +1,117 @@ +import fs from 'fs'; +import path from 'path'; +import chalk from 'chalk'; +import inquirer from 'inquirer'; +import { detectTools, parseClaudeMd, parseCursorRules } from '../lib/parser.js'; +import { findAisonaFile, getDefaultAisona, saveAisona } from '../lib/config.js'; + +export async function initCommand(options) { + const dir = path.resolve(options.dir || process.cwd()); + const home = process.env.HOME; + + console.log(chalk.bold('\n aisona init\n')); + console.log(chalk.dim(' Own your AI\'s persona. Define it once, use it everywhere.\n')); + + // Check if aisona.yml already exists + const existing = findAisonaFile(dir); + if (existing) { + const { overwrite } = await inquirer.prompt([{ + type: 'confirm', + name: 'overwrite', + message: `aisona.yml already exists at ${existing}. Overwrite?`, + default: false, + }]); + if (!overwrite) { + console.log(chalk.yellow(' Aborted.')); + return; + } + } + + // Detect existing AI tool configs + console.log(chalk.blue(' Scanning for existing AI tool configs...\n')); + const detected = detectTools(dir); + + if (detected.length === 0) { + console.log(chalk.yellow(' No existing AI tool configs found.')); + console.log(chalk.dim(' Creating a blank aisona.yml for you to fill in.\n')); + } else { + console.log(chalk.green(` Found ${detected.length} config(s):\n`)); + for (const d of detected) { + console.log(` ${chalk.cyan(d.tool)} (${d.scope}) → ${chalk.dim(d.path)}`); + } + console.log(); + } + + // Start with default structure + const aisona = getDefaultAisona(); + + // If --from specified, import from that tool + const importFrom = options.from; + const claudeConfig = detected.find(d => d.tool === 'claude'); + + if (importFrom === 'claude' || (!importFrom && claudeConfig)) { + const claudePath = importFrom === 'claude' + ? claudeConfig?.path || path.join(home, '.claude', 'CLAUDE.md') + : claudeConfig.path; + + if (fs.existsSync(claudePath)) { + console.log(chalk.blue(` Importing from ${claudePath}...\n`)); + const parsed = parseClaudeMd(claudePath); + + // Merge parsed data into aisona + if (parsed.personality.tone) aisona.personality.tone = parsed.personality.tone; + if (parsed.personality.style.length) aisona.personality.style = parsed.personality.style; + if (parsed.personality.teaching) aisona.personality.teaching = parsed.personality.teaching; + if (parsed.personality.autonomy) aisona.personality.autonomy = parsed.personality.autonomy; + if (parsed.rules.length) aisona.rules = parsed.rules; + if (parsed.preferences.length) aisona.preferences = parsed.preferences; + if (parsed.memories.length) aisona.memories = parsed.memories; + + console.log(chalk.green(` Imported: ${parsed.rules.length} rules, ${parsed.preferences.length} preferences, ${parsed.memories.length} memories\n`)); + } + } + + // Interactive: ask for basic identity info if not imported + if (!aisona.identity.name) { + const answers = await inquirer.prompt([ + { type: 'input', name: 'name', message: 'Your name (or alias):', default: process.env.USER || '' }, + { type: 'input', name: 'role', message: 'Your role:', default: 'Software developer' }, + { type: 'input', name: 'language', message: 'Preferred AI response language:', default: 'English' }, + ]); + aisona.identity.name = answers.name; + aisona.identity.role = answers.role; + aisona.identity.language = answers.language; + } + + // Ask which tools to enable + const toolChoices = ['claude', 'cursor', 'gemini', 'copilot', 'windsurf']; + const detectedToolIds = detected.map(d => d.tool); + const defaultEnabled = toolChoices.filter(t => detectedToolIds.includes(t)); + + const { enabledTools } = await inquirer.prompt([{ + type: 'checkbox', + name: 'enabledTools', + message: 'Which tools do you want to export to?', + choices: toolChoices.map(t => ({ + name: t + (detectedToolIds.includes(t) ? chalk.dim(' (detected)') : ''), + value: t, + checked: defaultEnabled.includes(t) || t === 'claude' || t === 'cursor', + })), + }]); + + for (const tool of toolChoices) { + if (aisona.tools[tool]) { + aisona.tools[tool].enabled = enabledTools.includes(tool); + } + } + + // Save + const outPath = path.join(dir, 'aisona.yml'); + saveAisona(outPath, aisona); + + console.log(chalk.green(`\n Created ${outPath}\n`)); + console.log(chalk.dim(' Next steps:')); + console.log(chalk.dim(' 1. Edit aisona.yml to refine your persona')); + console.log(chalk.dim(' 2. Run: aisona export --all')); + console.log(chalk.dim(' 3. Run: aisona watch --git (auto-sync)\n')); +} diff --git a/src/commands/status.js b/src/commands/status.js new file mode 100644 index 0000000..9a36ec5 --- /dev/null +++ b/src/commands/status.js @@ -0,0 +1,63 @@ +import path from 'path'; +import chalk from 'chalk'; +import { findAisonaFile, loadAisona } from '../lib/config.js'; +import { detectTools } from '../lib/parser.js'; +import { getSupportedTools } from '../lib/exporter.js'; + +export async function statusCommand(options) { + const dir = path.resolve(options.dir || process.cwd()); + + console.log(chalk.bold('\n aisona status\n')); + + // Check for aisona.yml + const aisonaPath = findAisonaFile(dir); + if (!aisonaPath) { + console.log(chalk.yellow(' No aisona.yml found. Run: aisona init\n')); + + // Still show detected tools + const detected = detectTools(dir); + if (detected.length > 0) { + console.log(chalk.blue(' Detected AI tool configs:\n')); + for (const d of detected) { + console.log(` ${chalk.cyan(d.tool)} (${d.scope}) → ${chalk.dim(d.path)}`); + } + console.log(); + } + return; + } + + const aisona = loadAisona(aisonaPath); + + // Persona summary + console.log(chalk.blue(' Persona:')); + if (aisona.identity?.name) console.log(` Name: ${aisona.identity.name}`); + if (aisona.identity?.role) console.log(` Role: ${aisona.identity.role}`); + if (aisona.identity?.language) console.log(` Language: ${aisona.identity.language}`); + console.log(` Rules: ${aisona.rules?.length || 0}`); + console.log(` Preferences: ${aisona.preferences?.length || 0}`); + console.log(` Memories: ${aisona.memories?.length || 0}`); + console.log(); + + // Tool status + console.log(chalk.blue(' Tools:')); + const supported = getSupportedTools(); + for (const toolId of supported) { + const config = aisona.tools?.[toolId]; + const enabled = config?.enabled !== false; + const icon = enabled ? chalk.green('✓') : chalk.dim('○'); + console.log(` ${icon} ${toolId}${!enabled ? chalk.dim(' (disabled)') : ''}`); + } + console.log(); + + // Detected configs + const detected = detectTools(dir); + if (detected.length > 0) { + console.log(chalk.blue(' Detected existing configs:')); + for (const d of detected) { + console.log(` ${chalk.cyan(d.tool)} → ${chalk.dim(d.path)}`); + } + console.log(); + } + + console.log(chalk.dim(` aisona.yml: ${aisonaPath}\n`)); +} diff --git a/src/commands/watch.js b/src/commands/watch.js new file mode 100644 index 0000000..658eb27 --- /dev/null +++ b/src/commands/watch.js @@ -0,0 +1,111 @@ +import path from 'path'; +import chalk from 'chalk'; +import chokidar from 'chokidar'; +import { simpleGit } from 'simple-git'; +import { findAisonaFile, loadAisona } from '../lib/config.js'; +import { exportToAll } from '../lib/exporter.js'; + +export async function watchCommand(options) { + const dir = path.resolve(options.dir || process.cwd()); + const useGit = options.git || false; + + console.log(chalk.bold('\n aisona watch\n')); + + const aisonaPath = findAisonaFile(dir); + if (!aisonaPath) { + console.log(chalk.red(' No aisona.yml found. Run: aisona init\n')); + process.exit(1); + } + + console.log(chalk.dim(` Watching: ${aisonaPath}`)); + if (useGit) console.log(chalk.dim(' Git auto-commit: enabled')); + console.log(chalk.dim(' Press Ctrl+C to stop.\n')); + + // Do initial export + await doExport(aisonaPath, dir, useGit); + + // Watch for changes + let debounceTimer = null; + + const watcher = chokidar.watch(aisonaPath, { + persistent: true, + ignoreInitial: true, + }); + + watcher.on('change', () => { + // Debounce: wait 2s after last change before exporting + if (debounceTimer) clearTimeout(debounceTimer); + debounceTimer = setTimeout(() => { + doExport(aisonaPath, dir, useGit); + }, 2000); + }); + + // Keep process alive + process.on('SIGINT', () => { + console.log(chalk.dim('\n Stopped watching.\n')); + watcher.close(); + process.exit(0); + }); +} + +async function doExport(aisonaPath, dir, useGit) { + try { + const aisona = loadAisona(aisonaPath); + const results = exportToAll(aisona, dir); + + const succeeded = results.filter(r => r.success); + const timestamp = new Date().toLocaleTimeString(); + + if (succeeded.length > 0) { + const tools = succeeded.map(r => r.tool).join(', '); + console.log(chalk.green(` [${timestamp}] Exported to: ${tools}`)); + + if (useGit) { + await gitCommitAndPush(dir, succeeded); + } + } + } catch (err) { + console.log(chalk.red(` Error: ${err.message}`)); + } +} + +async function gitCommitAndPush(dir, exportResults) { + try { + const git = simpleGit(dir); + + // Check if we're in a git repo + const isRepo = await git.checkIsRepo(); + if (!isRepo) { + console.log(chalk.dim(' (not a git repo — skipping git sync)')); + return; + } + + // Stage exported files + aisona.yml + const files = ['aisona.yml', ...exportResults.map(r => path.relative(dir, r.path))]; + await git.add(files); + + // Check if there are staged changes + const status = await git.status(); + if (status.staged.length === 0) { + return; // Nothing to commit + } + + // Commit + const tools = exportResults.map(r => r.tool).join(', '); + await git.commit(`aisona: auto-sync persona to ${tools}`); + console.log(chalk.dim(' git: committed')); + + // Push if remote exists + try { + const remotes = await git.getRemotes(true); + if (remotes.length > 0) { + await git.push(); + console.log(chalk.dim(' git: pushed')); + } + } catch { + // Push failed — probably no remote or auth issue. Silently skip. + } + } catch (err) { + console.log(chalk.dim(` git: ${err.message}`)); + } +} diff --git a/src/index.js b/src/index.js index 266e19f..6167f86 100644 --- a/src/index.js +++ b/src/index.js @@ -1 +1,3 @@ -export const version = '0.0.1'; +export { loadAisona, saveAisona, findAisonaFile, getDefaultAisona } from './lib/config.js'; +export { exportToTool, exportToAll, getSupportedTools } from './lib/exporter.js'; +export { detectTools, parseClaudeMd, parseCursorRules } from './lib/parser.js'; diff --git a/src/lib/config.js b/src/lib/config.js new file mode 100644 index 0000000..f61ff83 --- /dev/null +++ b/src/lib/config.js @@ -0,0 +1,63 @@ +import fs from 'fs'; +import path from 'path'; +import yaml from 'js-yaml'; + +const AISONA_FILE = 'aisona.yml'; + +export function findAisonaFile(dir) { + const filePath = path.join(dir, AISONA_FILE); + if (fs.existsSync(filePath)) return filePath; + + // Also check ~/.aisona/ + const globalPath = path.join(process.env.HOME, '.aisona', AISONA_FILE); + if (fs.existsSync(globalPath)) return globalPath; + + return null; +} + +export function loadAisona(filePath) { + const raw = fs.readFileSync(filePath, 'utf8'); + return yaml.load(raw); +} + +export function saveAisona(filePath, data) { + const out = yaml.dump(data, { + lineWidth: 120, + noRefs: true, + quotingType: '"', + forceQuotes: false, + }); + fs.mkdirSync(path.dirname(filePath), { recursive: true }); + fs.writeFileSync(filePath, out, 'utf8'); +} + +export function getDefaultAisona() { + return { + version: 1, + identity: { + name: '', + role: '', + experience: '', + context: '', + language: 'English', + }, + personality: { + tone: '', + verbosity: 'balanced', + style: [], + teaching: '', + autonomy: '', + }, + rules: [], + preferences: [], + memories: [], + tools: { + claude: { enabled: true, extra_rules: [] }, + cursor: { enabled: true, extra_rules: [] }, + gemini: { enabled: false, extra_rules: [] }, + copilot: { enabled: false, extra_rules: [] }, + windsurf: { enabled: false, extra_rules: [] }, + aider: { enabled: false, extra_rules: [] }, + }, + }; +} diff --git a/src/lib/exporter.js b/src/lib/exporter.js new file mode 100644 index 0000000..094c077 --- /dev/null +++ b/src/lib/exporter.js @@ -0,0 +1,86 @@ +import fs from 'fs'; +import path from 'path'; +import { fileURLToPath } from 'url'; +import Handlebars from 'handlebars'; + +const __dirname = path.dirname(fileURLToPath(import.meta.url)); +const TEMPLATES_DIR = path.join(__dirname, '..', 'templates'); + +// Map tool id → output file path (relative to project root or home) +const TOOL_OUTPUTS = { + claude: { file: 'CLAUDE.md', globalFile: '.claude/CLAUDE.md' }, + cursor: { file: '.cursorrules' }, + gemini: { file: 'GEMINI.md', globalFile: '.gemini/GEMINI.md' }, + copilot: { file: '.github/copilot-instructions.md' }, + windsurf: { file: '.windsurfrules' }, +}; + +function loadTemplate(toolId) { + const templatePath = path.join(TEMPLATES_DIR, `${toolId}.hbs`); + if (!fs.existsSync(templatePath)) { + return null; + } + const raw = fs.readFileSync(templatePath, 'utf8'); + return Handlebars.compile(raw, { noEscape: true }); +} + +/** + * Clean up template output — remove excessive blank lines + */ +function cleanOutput(text) { + return text + .replace(/\n{3,}/g, '\n\n') // max 2 consecutive newlines + .trim() + '\n'; +} + +/** + * Export aisona data to a specific tool's config file + */ +export function exportToTool(aisona, toolId, dir) { + const template = loadTemplate(toolId); + if (!template) { + return { success: false, error: `No template found for tool: ${toolId}` }; + } + + const toolConfig = aisona.tools?.[toolId]; + if (toolConfig && toolConfig.enabled === false) { + return { success: false, error: `Tool ${toolId} is disabled in aisona.yml` }; + } + + const output = cleanOutput(template(aisona)); + + const toolOutput = TOOL_OUTPUTS[toolId]; + if (!toolOutput) { + return { success: false, error: `Unknown output path for tool: ${toolId}` }; + } + + const outPath = path.join(dir, toolOutput.file); + fs.mkdirSync(path.dirname(outPath), { recursive: true }); + fs.writeFileSync(outPath, output, 'utf8'); + + return { success: true, path: outPath, tool: toolId }; +} + +/** + * Export to all enabled tools + */ +export function exportToAll(aisona, dir) { + const results = []; + + for (const [toolId, config] of Object.entries(aisona.tools || {})) { + if (config.enabled === false) continue; + if (!TOOL_OUTPUTS[toolId]) continue; + + const result = exportToTool(aisona, toolId, dir); + results.push(result); + } + + return results; +} + +/** + * Get list of supported tools + */ +export function getSupportedTools() { + return Object.keys(TOOL_OUTPUTS); +} diff --git a/src/lib/parser.js b/src/lib/parser.js new file mode 100644 index 0000000..7afe57e --- /dev/null +++ b/src/lib/parser.js @@ -0,0 +1,182 @@ +/** + * Parse existing AI tool configs into aisona.yml structure. + * This is the "import" logic — reads CLAUDE.md, .cursorrules, etc. + * and extracts personality, rules, and preferences. + */ + +import fs from 'fs'; +import path from 'path'; + +/** + * Parse a CLAUDE.md file into structured sections. + * CLAUDE.md is free-form markdown, so we use heuristics: + * - H2 headers (##) = section boundaries + * - Bullet points = rules/preferences + * - Prose = personality/context + */ +export function parseClaudeMd(filePath) { + const content = fs.readFileSync(filePath, 'utf8'); + const lines = content.split('\n'); + + const result = { + personality: { tone: '', style: [], teaching: '', autonomy: '' }, + rules: [], + preferences: [], + memories: [], + raw_sections: {}, + }; + + let currentSection = '_preamble'; + + for (const line of lines) { + // Detect H2 headers + const h2Match = line.match(/^##\s+(.+)/); + if (h2Match) { + currentSection = h2Match[1].trim().toLowerCase(); + result.raw_sections[currentSection] = []; + continue; + } + + if (!result.raw_sections[currentSection]) { + result.raw_sections[currentSection] = []; + } + result.raw_sections[currentSection].push(line); + } + + // Extract rules from common section names + const rulesSections = ['rules', 'autonomy', 'constraints', 'boundaries', 'hard rules']; + const prefsSections = ['preferences', 'style', 'code style', 'coding style', 'conventions']; + const personalitySections = ['language', 'tone', 'communication', 'personality', 'tone and style']; + const teachingSections = ['teaching', 'teaching style', 'learning', 'explanations', 'technical explanations']; + const memorySections = ['memory', 'memories', 'learned', 'context', 'project knowledge capture']; + + for (const [section, lines] of Object.entries(result.raw_sections)) { + const bullets = lines + .filter(l => l.match(/^[-*]\s+/)) + .map(l => l.replace(/^[-*]\s+/, '').trim()) + .filter(l => l.length > 0); + + const prose = lines + .filter(l => !l.match(/^[-*]\s+/) && !l.match(/^#/) && l.trim().length > 0) + .map(l => l.trim()) + .join(' ') + .trim(); + + if (rulesSections.some(s => section.includes(s))) { + result.rules.push(...bullets); + if (prose && section.includes('autonomy')) { + result.personality.autonomy = prose; + } + } else if (prefsSections.some(s => section.includes(s))) { + result.preferences.push(...bullets); + if (bullets.length > 0 && !result.personality.tone) { + result.personality.style.push(...bullets); + } + } else if (personalitySections.some(s => section.includes(s))) { + if (prose) result.personality.tone = prose; + result.personality.style.push(...bullets); + } else if (teachingSections.some(s => section.includes(s))) { + if (prose) result.personality.teaching = prose; + result.preferences.push(...bullets); + } else if (memorySections.some(s => section.includes(s))) { + result.memories.push(...bullets); + } else { + // Unknown sections — add bullets as preferences + if (bullets.length > 0) { + result.preferences.push(...bullets); + } + } + } + + return result; +} + +/** + * Parse .cursorrules (simple markdown, usually just rules) + */ +export function parseCursorRules(filePath) { + const content = fs.readFileSync(filePath, 'utf8'); + const lines = content.split('\n'); + + const rules = lines + .filter(l => l.match(/^[-*]\s+/)) + .map(l => l.replace(/^[-*]\s+/, '').trim()) + .filter(l => l.length > 0); + + // Non-bullet lines = prose context + const prose = lines + .filter(l => !l.match(/^[-*]\s+/) && !l.match(/^#/) && l.trim().length > 0) + .map(l => l.trim()) + .join(' ') + .trim(); + + return { rules, context: prose }; +} + +/** + * Parse GEMINI.md (same format as CLAUDE.md essentially) + */ +export function parseGeminiMd(filePath) { + return parseClaudeMd(filePath); // Same markdown format +} + +/** + * Detect which AI tool configs exist in a directory + */ +export function detectTools(dir) { + const home = process.env.HOME; + const detected = []; + + // Claude Code + const claudePaths = [ + path.join(home, '.claude', 'CLAUDE.md'), + path.join(dir, 'CLAUDE.md'), + ]; + for (const p of claudePaths) { + if (fs.existsSync(p)) { + detected.push({ tool: 'claude', path: p, scope: p.includes(home + '/.claude') ? 'global' : 'project' }); + } + } + + // Cursor + const cursorPaths = [ + path.join(dir, '.cursorrules'), + path.join(dir, '.cursor', 'rules'), + ]; + for (const p of cursorPaths) { + if (fs.existsSync(p)) { + detected.push({ tool: 'cursor', path: p, scope: 'project' }); + } + } + + // Gemini + const geminiPaths = [ + path.join(home, '.gemini', 'GEMINI.md'), + path.join(dir, 'GEMINI.md'), + ]; + for (const p of geminiPaths) { + if (fs.existsSync(p)) { + detected.push({ tool: 'gemini', path: p, scope: p.includes(home + '/.gemini') ? 'global' : 'project' }); + } + } + + // Copilot + const copilotPath = path.join(dir, '.github', 'copilot-instructions.md'); + if (fs.existsSync(copilotPath)) { + detected.push({ tool: 'copilot', path: copilotPath, scope: 'project' }); + } + + // Windsurf + const windsurfPath = path.join(dir, '.windsurfrules'); + if (fs.existsSync(windsurfPath)) { + detected.push({ tool: 'windsurf', path: windsurfPath, scope: 'project' }); + } + + // AGENTS.md + const agentsPath = path.join(dir, 'AGENTS.md'); + if (fs.existsSync(agentsPath)) { + detected.push({ tool: 'agentsmd', path: agentsPath, scope: 'project' }); + } + + return detected; +} diff --git a/src/templates/claude.hbs b/src/templates/claude.hbs new file mode 100644 index 0000000..09dace9 --- /dev/null +++ b/src/templates/claude.hbs @@ -0,0 +1,62 @@ +{{! Generated by aisona — https://github.com/agents-io/aisona }} +{{! Do not edit directly. Edit aisona.yml and run: aisona export --to claude }} + +{{#if identity.language}} +## Language + +{{identity.language}} +{{/if}} + +{{#if personality.tone}} +## Tone and Style + +{{personality.tone}} + +{{#each personality.style}} +- {{this}} +{{/each}} +{{/if}} + +{{#if personality.teaching}} +## Teaching Style + +{{personality.teaching}} +{{/if}} + +{{#if personality.autonomy}} +## Autonomy + +{{personality.autonomy}} +{{/if}} + +{{#if rules.length}} +## Rules + +{{#each rules}} +- {{this}} +{{/each}} +{{/if}} + +{{#if preferences.length}} +## Preferences + +{{#each preferences}} +- {{this}} +{{/each}} +{{/if}} + +{{#if memories.length}} +## Context + +{{#each memories}} +- {{this}} +{{/each}} +{{/if}} + +{{#if tools.claude.extra_rules.length}} +## Claude-Specific + +{{#each tools.claude.extra_rules}} +- {{this}} +{{/each}} +{{/if}} diff --git a/src/templates/copilot.hbs b/src/templates/copilot.hbs new file mode 100644 index 0000000..2e4276c --- /dev/null +++ b/src/templates/copilot.hbs @@ -0,0 +1,35 @@ +{{! Generated by aisona — https://github.com/agents-io/aisona }} +{{! Do not edit directly. Edit aisona.yml and run: aisona export --to copilot }} + +{{#if identity.language}} +Respond in {{identity.language}}. +{{/if}} + +{{#if personality.tone}} +## Style +{{personality.tone}} +{{#each personality.style}} +- {{this}} +{{/each}} +{{/if}} + +{{#if rules.length}} +## Rules +{{#each rules}} +- {{this}} +{{/each}} +{{/if}} + +{{#if preferences.length}} +## Preferences +{{#each preferences}} +- {{this}} +{{/each}} +{{/if}} + +{{#if tools.copilot.extra_rules.length}} +## Copilot-Specific +{{#each tools.copilot.extra_rules}} +- {{this}} +{{/each}} +{{/if}} diff --git a/src/templates/cursor.hbs b/src/templates/cursor.hbs new file mode 100644 index 0000000..d9a09b2 --- /dev/null +++ b/src/templates/cursor.hbs @@ -0,0 +1,46 @@ +{{! Generated by aisona — https://github.com/agents-io/aisona }} +{{! Do not edit directly. Edit aisona.yml and run: aisona export --to cursor }} + +{{#if identity.role}} +# Role: {{identity.role}} +{{/if}} + +{{#if identity.language}} +Always respond in {{identity.language}}. +{{/if}} + +{{#if personality.tone}} +## Communication +{{personality.tone}} +{{#each personality.style}} +- {{this}} +{{/each}} +{{/if}} + +{{#if rules.length}} +## Rules +{{#each rules}} +- {{this}} +{{/each}} +{{/if}} + +{{#if preferences.length}} +## Preferences +{{#each preferences}} +- {{this}} +{{/each}} +{{/if}} + +{{#if memories.length}} +## Context +{{#each memories}} +- {{this}} +{{/each}} +{{/if}} + +{{#if tools.cursor.extra_rules.length}} +## Cursor-Specific +{{#each tools.cursor.extra_rules}} +- {{this}} +{{/each}} +{{/if}} diff --git a/src/templates/gemini.hbs b/src/templates/gemini.hbs new file mode 100644 index 0000000..af0f983 --- /dev/null +++ b/src/templates/gemini.hbs @@ -0,0 +1,58 @@ +{{! Generated by aisona — https://github.com/agents-io/aisona }} +{{! Do not edit directly. Edit aisona.yml and run: aisona export --to gemini }} + +{{#if identity.language}} +## Language + +Respond in {{identity.language}}. +{{/if}} + +{{#if identity.role}} +## About the User + +Role: {{identity.role}} +{{#if identity.experience}}Experience: {{identity.experience}}{{/if}} +{{#if identity.context}}Context: {{identity.context}}{{/if}} +{{/if}} + +{{#if personality.tone}} +## Communication Style + +{{personality.tone}} + +{{#each personality.style}} +- {{this}} +{{/each}} +{{/if}} + +{{#if rules.length}} +## Rules + +{{#each rules}} +- {{this}} +{{/each}} +{{/if}} + +{{#if preferences.length}} +## Preferences + +{{#each preferences}} +- {{this}} +{{/each}} +{{/if}} + +{{#if memories.length}} +## Context + +{{#each memories}} +- {{this}} +{{/each}} +{{/if}} + +{{#if tools.gemini.extra_rules.length}} +## Gemini-Specific + +{{#each tools.gemini.extra_rules}} +- {{this}} +{{/each}} +{{/if}} From d1e41c36870ae477627aa565ed6178276939c038 Mon Sep 17 00:00:00 2001 From: ithiria894 Date: Fri, 27 Mar 2026 18:38:54 -0700 Subject: [PATCH 2/4] test: add 35 tests for config, parser, and exporter - config.test.js: roundtrip save/load, default structure, findAisonaFile - parser.test.js: CLAUDE.md parsing (rules, tone, teaching, autonomy), .cursorrules parsing, tool detection, real-world CLAUDE.md structure - exporter.test.js: all 4 tool exports, disabled tool handling, output quality Also fixes: - Parser: reorder section matching (teaching/personality before preferences) to fix false matches on "tone and style", "teaching style" sections - Exporter: check tool enabled BEFORE loading template Co-Authored-By: Claude Opus 4.6 (1M context) --- package-lock.json | 1229 ++++++++++++++++++++++++++++++++++++++++ package.json | 5 +- src/lib/exporter.js | 11 +- src/lib/parser.js | 15 +- tests/config.test.js | 78 +++ tests/exporter.test.js | 229 ++++++++ tests/parser.test.js | 203 +++++++ 7 files changed, 1758 insertions(+), 12 deletions(-) create mode 100644 tests/config.test.js create mode 100644 tests/exporter.test.js create mode 100644 tests/parser.test.js diff --git a/package-lock.json b/package-lock.json index 1ea8f46..eaa8510 100644 --- a/package-lock.json +++ b/package-lock.json @@ -20,6 +20,43 @@ }, "bin": { "aisona": "bin/aisona.js" + }, + "devDependencies": { + "vitest": "^4.1.2" + } + }, + "node_modules/@emnapi/core": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.9.1.tgz", + "integrity": "sha512-mukuNALVsoix/w1BJwFzwXBN/dHeejQtuVzcDsfOEsdpCumXb/E9j8w11h5S54tT1xhifGfbbSm/ICrObRb3KA==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/wasi-threads": "1.2.0", + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/runtime": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.9.1.tgz", + "integrity": "sha512-VYi5+ZVLhpgK4hQ0TAjiQiZ6ol0oe4mBx7mVv7IflsiEp0OWoVsp/+f9Vc1hOhE0TtkORVrI1GvzyreqpgWtkA==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/wasi-threads": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.0.tgz", + "integrity": "sha512-N10dEJNSsUx41Z6pZsXU8FjPjpBEplgH24sfkmITrBED1/U2Esum9F3lfLrMjKHHjmi557zQn7kR9R+XWXu5Rg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" } }, "node_modules/@inquirer/ansi": { @@ -350,6 +387,13 @@ } } }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, "node_modules/@kwsites/file-exists": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/@kwsites/file-exists/-/file-exists-1.1.1.tgz", @@ -365,6 +409,451 @@ "integrity": "sha512-GaHYm+c0O9MjZRu0ongGBRbinu8gVAMd2UZjji6jVmqKtZluZnptXGWhz1E8j8D2HJ3f/yMxKAUC0b+57wncIw==", "license": "MIT" }, + "node_modules/@napi-rs/wasm-runtime": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.1.tgz", + "integrity": "sha512-p64ah1M1ld8xjWv3qbvFwHiFVWrq1yFvV4f7w+mzaqiR4IlSgkqhcRdHwsGgomwzBH51sRY4NEowLxnaBjcW/A==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "^1.7.1", + "@emnapi/runtime": "^1.7.1", + "@tybys/wasm-util": "^0.10.1" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + } + }, + "node_modules/@oxc-project/types": { + "version": "0.122.0", + "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.122.0.tgz", + "integrity": "sha512-oLAl5kBpV4w69UtFZ9xqcmTi+GENWOcPF7FCrczTiBbmC0ibXxCwyvZGbO39rCVEuLGAZM84DH0pUIyyv/YJzA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/Boshen" + } + }, + "node_modules/@rolldown/binding-android-arm64": { + "version": "1.0.0-rc.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.0-rc.12.tgz", + "integrity": "sha512-pv1y2Fv0JybcykuiiD3qBOBdz6RteYojRFY1d+b95WVuzx211CRh+ytI/+9iVyWQ6koTh5dawe4S/yRfOFjgaA==", + "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-rc.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.0.0-rc.12.tgz", + "integrity": "sha512-cFYr6zTG/3PXXF3pUO+umXxt1wkRK/0AYT8lDwuqvRC+LuKYWSAQAQZjCWDQpAH172ZV6ieYrNnFzVVcnSflAg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-x64": { + "version": "1.0.0-rc.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.0.0-rc.12.tgz", + "integrity": "sha512-ZCsYknnHzeXYps0lGBz8JrF37GpE9bFVefrlmDrAQhOEi4IOIlcoU1+FwHEtyXGx2VkYAvhu7dyBf75EJQffBw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-freebsd-x64": { + "version": "1.0.0-rc.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.0.0-rc.12.tgz", + "integrity": "sha512-dMLeprcVsyJsKolRXyoTH3NL6qtsT0Y2xeuEA8WQJquWFXkEC4bcu1rLZZSnZRMtAqwtrF/Ib9Ddtpa/Gkge9Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm-gnueabihf": { + "version": "1.0.0-rc.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.0.0-rc.12.tgz", + "integrity": "sha512-YqWjAgGC/9M1lz3GR1r1rP79nMgo3mQiiA+Hfo+pvKFK1fAJ1bCi0ZQVh8noOqNacuY1qIcfyVfP6HoyBRZ85Q==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-gnu": { + "version": "1.0.0-rc.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.0.0-rc.12.tgz", + "integrity": "sha512-/I5AS4cIroLpslsmzXfwbe5OmWvSsrFuEw3mwvbQ1kDxJ822hFHIx+vsN/TAzNVyepI/j/GSzrtCIwQPeKCLIg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-musl": { + "version": "1.0.0-rc.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.0.0-rc.12.tgz", + "integrity": "sha512-V6/wZztnBqlx5hJQqNWwFdxIKN0m38p8Jas+VoSfgH54HSj9tKTt1dZvG6JRHcjh6D7TvrJPWFGaY9UBVOaWPw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-ppc64-gnu": { + "version": "1.0.0-rc.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.0.0-rc.12.tgz", + "integrity": "sha512-AP3E9BpcUYliZCxa3w5Kwj9OtEVDYK6sVoUzy4vTOJsjPOgdaJZKFmN4oOlX0Wp0RPV2ETfmIra9x1xuayFB7g==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-s390x-gnu": { + "version": "1.0.0-rc.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.0.0-rc.12.tgz", + "integrity": "sha512-nWwpvUSPkoFmZo0kQazZYOrT7J5DGOJ/+QHHzjvNlooDZED8oH82Yg67HvehPPLAg5fUff7TfWFHQS8IV1n3og==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-gnu": { + "version": "1.0.0-rc.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.0.0-rc.12.tgz", + "integrity": "sha512-RNrafz5bcwRy+O9e6P8Z/OCAJW/A+qtBczIqVYwTs14pf4iV1/+eKEjdOUta93q2TsT/FI0XYDP3TCky38LMAg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-musl": { + "version": "1.0.0-rc.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.0.0-rc.12.tgz", + "integrity": "sha512-Jpw/0iwoKWx3LJ2rc1yjFrj+T7iHZn2JDg1Yny1ma0luviFS4mhAIcd1LFNxK3EYu3DHWCps0ydXQ5i/rrJ2ig==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-openharmony-arm64": { + "version": "1.0.0-rc.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.0.0-rc.12.tgz", + "integrity": "sha512-vRugONE4yMfVn0+7lUKdKvN4D5YusEiPilaoO2sgUWpCvrncvWgPMzK00ZFFJuiPgLwgFNP5eSiUlv2tfc+lpA==", + "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-rc.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.0.0-rc.12.tgz", + "integrity": "sha512-ykGiLr/6kkiHc0XnBfmFJuCjr5ZYKKofkx+chJWDjitX+KsJuAmrzWhwyOMSHzPhzOHOy7u9HlFoa5MoAOJ/Zg==", + "cpu": [ + "wasm32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@napi-rs/wasm-runtime": "^1.1.1" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@rolldown/binding-win32-arm64-msvc": { + "version": "1.0.0-rc.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.0-rc.12.tgz", + "integrity": "sha512-5eOND4duWkwx1AzCxadcOrNeighiLwMInEADT0YM7xeEOOFcovWZCq8dadXgcRHSf3Ulh1kFo/qvzoFiCLOL1Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-win32-x64-msvc": { + "version": "1.0.0-rc.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.0.0-rc.12.tgz", + "integrity": "sha512-PyqoipaswDLAZtot351MLhrlrh6lcZPo2LSYE+VDxbVk24LVKAGOuE4hb8xZQmrPAuEtTZW8E6D2zc5EUZX4Lw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.0-rc.12", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.12.tgz", + "integrity": "sha512-HHMwmarRKvoFsJorqYlFeFRzXZqCt2ETQlEDOb9aqssrnVBB1/+xgTGtuTrIk5vzLNX1MjMtTf7W9z3tsSbrxw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@standard-schema/spec": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", + "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tybys/wasm-util": { + "version": "0.10.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, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@types/chai": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz", + "integrity": "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/deep-eql": "*", + "assertion-error": "^2.0.1" + } + }, + "node_modules/@types/deep-eql": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz", + "integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@vitest/expect": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.1.2.tgz", + "integrity": "sha512-gbu+7B0YgUJ2nkdsRJrFFW6X7NTP44WlhiclHniUhxADQJH5Szt9mZ9hWnJPJ8YwOK5zUOSSlSvyzRf0u1DSBQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@standard-schema/spec": "^1.1.0", + "@types/chai": "^5.2.2", + "@vitest/spy": "4.1.2", + "@vitest/utils": "4.1.2", + "chai": "^6.2.2", + "tinyrainbow": "^3.1.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/mocker": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.1.2.tgz", + "integrity": "sha512-Ize4iQtEALHDttPRCmN+FKqOl2vxTiNUhzobQFFt/BM1lRUTG7zRCLOykG/6Vo4E4hnUdfVLo5/eqKPukcWW7Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/spy": "4.1.2", + "estree-walker": "^3.0.3", + "magic-string": "^0.30.21" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "msw": "^2.4.9", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "msw": { + "optional": true + }, + "vite": { + "optional": true + } + } + }, + "node_modules/@vitest/pretty-format": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.1.2.tgz", + "integrity": "sha512-dwQga8aejqeuB+TvXCMzSQemvV9hNEtDDpgUKDzOmNQayl2OG241PSWeJwKRH3CiC+sESrmoFd49rfnq7T4RnA==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyrainbow": "^3.1.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/runner": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.1.2.tgz", + "integrity": "sha512-Gr+FQan34CdiYAwpGJmQG8PgkyFVmARK8/xSijia3eTFgVfpcpztWLuP6FttGNfPLJhaZVP/euvujeNYar36OQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/utils": "4.1.2", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/snapshot": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.1.2.tgz", + "integrity": "sha512-g7yfUmxYS4mNxk31qbOYsSt2F4m1E02LFqO53Xpzg3zKMhLAPZAjjfyl9e6z7HrW6LvUdTwAQR3HHfLjpko16A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "4.1.2", + "@vitest/utils": "4.1.2", + "magic-string": "^0.30.21", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/spy": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.1.2.tgz", + "integrity": "sha512-DU4fBnbVCJGNBwVA6xSToNXrkZNSiw59H8tcuUspVMsBDBST4nfvsPsEHDHGtWRRnqBERBQu7TrTKskmjqTXKA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/utils": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.1.2.tgz", + "integrity": "sha512-xw2/TiX82lQHA06cgbqRKFb5lCAy3axQ4H4SoUFhUsg+wztiet+co86IAMDtF6Vm1hc7J6j09oh/rgDn+JdKIQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "4.1.2", + "convert-source-map": "^2.0.0", + "tinyrainbow": "^3.1.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, "node_modules/ansi-regex": { "version": "6.2.2", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", @@ -383,6 +872,26 @@ "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", "license": "Python-2.0" }, + "node_modules/assertion-error": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", + "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + } + }, + "node_modules/chai": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/chai/-/chai-6.2.2.tgz", + "integrity": "sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, "node_modules/chalk": { "version": "5.6.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.2.tgz", @@ -461,6 +970,13 @@ "node": ">=20" } }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, "node_modules/debug": { "version": "4.4.3", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", @@ -478,6 +994,43 @@ } } }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "node_modules/es-module-lexer": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.0.0.tgz", + "integrity": "sha512-5POEcUuZybH7IdmGsD8wlf0AI55wMecM9rVBTI/qEAy2c1kTOm3DjFYjrBdI2K3BaJjJYfYFeRtM0t9ssnRuxw==", + "dev": true, + "license": "MIT" + }, + "node_modules/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0" + } + }, + "node_modules/expect-type": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.3.0.tgz", + "integrity": "sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.0.0" + } + }, "node_modules/fast-string-truncated-width": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/fast-string-truncated-width/-/fast-string-truncated-width-3.0.3.tgz", @@ -502,6 +1055,39 @@ "fast-string-width": "^3.0.2" } }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, "node_modules/get-east-asian-width": { "version": "1.5.0", "resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.5.0.tgz", @@ -613,6 +1199,267 @@ "js-yaml": "bin/js-yaml.js" } }, + "node_modules/lightningcss": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.32.0.tgz", + "integrity": "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==", + "dev": true, + "license": "MPL-2.0", + "dependencies": { + "detect-libc": "^2.0.3" + }, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "lightningcss-android-arm64": "1.32.0", + "lightningcss-darwin-arm64": "1.32.0", + "lightningcss-darwin-x64": "1.32.0", + "lightningcss-freebsd-x64": "1.32.0", + "lightningcss-linux-arm-gnueabihf": "1.32.0", + "lightningcss-linux-arm64-gnu": "1.32.0", + "lightningcss-linux-arm64-musl": "1.32.0", + "lightningcss-linux-x64-gnu": "1.32.0", + "lightningcss-linux-x64-musl": "1.32.0", + "lightningcss-win32-arm64-msvc": "1.32.0", + "lightningcss-win32-x64-msvc": "1.32.0" + } + }, + "node_modules/lightningcss-android-arm64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.32.0.tgz", + "integrity": "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-arm64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.32.0.tgz", + "integrity": "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-x64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.32.0.tgz", + "integrity": "sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-freebsd-x64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.32.0.tgz", + "integrity": "sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm-gnueabihf": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.32.0.tgz", + "integrity": "sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-gnu": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.32.0.tgz", + "integrity": "sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-musl": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.32.0.tgz", + "integrity": "sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-gnu": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.32.0.tgz", + "integrity": "sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-musl": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.32.0.tgz", + "integrity": "sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-arm64-msvc": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.32.0.tgz", + "integrity": "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-x64-msvc": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.32.0.tgz", + "integrity": "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, "node_modules/log-symbols": { "version": "7.0.1", "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-7.0.1.tgz", @@ -629,6 +1476,16 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, "node_modules/mimic-function": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/mimic-function/-/mimic-function-5.0.1.tgz", @@ -665,12 +1522,42 @@ "node": "^20.17.0 || >=22.9.0" } }, + "node_modules/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, "node_modules/neo-async": { "version": "2.6.2", "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==", "license": "MIT" }, + "node_modules/obug": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/obug/-/obug-2.1.1.tgz", + "integrity": "sha512-uTqF9MuPraAQ+IsnPf366RG4cP9RtUi7MLO1N3KEc+wb0a6yKpeL0lmk2IB1jY5KHPAlTc6T/JRdC/YqxHNwkQ==", + "dev": true, + "funding": [ + "https://github.com/sponsors/sxzz", + "https://opencollective.com/debug" + ], + "license": "MIT" + }, "node_modules/onetime": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/onetime/-/onetime-7.0.0.tgz", @@ -708,6 +1595,62 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/pathe": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", + "dev": true, + "license": "MIT" + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/postcss": { + "version": "8.5.8", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.8.tgz", + "integrity": "sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, "node_modules/readdirp": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-5.0.0.tgz", @@ -737,6 +1680,40 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/rolldown": { + "version": "1.0.0-rc.12", + "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.0-rc.12.tgz", + "integrity": "sha512-yP4USLIMYrwpPHEFB5JGH1uxhcslv6/hL0OyvTuY+3qlOSJvZ7ntYnoWpehBxufkgN0cvXxppuTu5hHa/zPh+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@oxc-project/types": "=0.122.0", + "@rolldown/pluginutils": "1.0.0-rc.12" + }, + "bin": { + "rolldown": "bin/cli.mjs" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "optionalDependencies": { + "@rolldown/binding-android-arm64": "1.0.0-rc.12", + "@rolldown/binding-darwin-arm64": "1.0.0-rc.12", + "@rolldown/binding-darwin-x64": "1.0.0-rc.12", + "@rolldown/binding-freebsd-x64": "1.0.0-rc.12", + "@rolldown/binding-linux-arm-gnueabihf": "1.0.0-rc.12", + "@rolldown/binding-linux-arm64-gnu": "1.0.0-rc.12", + "@rolldown/binding-linux-arm64-musl": "1.0.0-rc.12", + "@rolldown/binding-linux-ppc64-gnu": "1.0.0-rc.12", + "@rolldown/binding-linux-s390x-gnu": "1.0.0-rc.12", + "@rolldown/binding-linux-x64-gnu": "1.0.0-rc.12", + "@rolldown/binding-linux-x64-musl": "1.0.0-rc.12", + "@rolldown/binding-openharmony-arm64": "1.0.0-rc.12", + "@rolldown/binding-wasm32-wasi": "1.0.0-rc.12", + "@rolldown/binding-win32-arm64-msvc": "1.0.0-rc.12", + "@rolldown/binding-win32-x64-msvc": "1.0.0-rc.12" + } + }, "node_modules/run-async": { "version": "4.0.6", "resolved": "https://registry.npmjs.org/run-async/-/run-async-4.0.6.tgz", @@ -761,6 +1738,13 @@ "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", "license": "MIT" }, + "node_modules/siginfo": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", + "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", + "dev": true, + "license": "ISC" + }, "node_modules/signal-exit": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", @@ -797,6 +1781,30 @@ "node": ">=0.10.0" } }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/stackback": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", + "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", + "dev": true, + "license": "MIT" + }, + "node_modules/std-env": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-4.0.0.tgz", + "integrity": "sha512-zUMPtQ/HBY3/50VbpkupYHbRroTRZJPRLvreamgErJVys0ceuzMkD44J/QjqhHjOzK42GQ3QZIeFG1OYfOtKqQ==", + "dev": true, + "license": "MIT" + }, "node_modules/stdin-discarder": { "version": "0.3.1", "resolved": "https://registry.npmjs.org/stdin-discarder/-/stdin-discarder-0.3.1.tgz", @@ -840,6 +1848,50 @@ "url": "https://github.com/chalk/strip-ansi?sponsor=1" } }, + "node_modules/tinybench": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", + "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinyexec": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.0.4.tgz", + "integrity": "sha512-u9r3uZC0bdpGOXtlxUIdwf9pkmvhqJdrVCH9fapQtgy/OeTTMZ1nqH7agtvEfmGui6e1XxjcdrlxvxJvc3sMqw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.15", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", + "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.3" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tinyrainbow": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-3.1.0.tgz", + "integrity": "sha512-Bf+ILmBgretUrdJxzXM0SgXLZ3XfiaUuOj/IKQHuTXip+05Xn+uyEYdVg0kYDipTBcLrCVyUzAPz7QmArb0mmw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, "node_modules/tslib": { "version": "2.8.1", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", @@ -859,6 +1911,183 @@ "node": ">=0.8.0" } }, + "node_modules/vite": { + "version": "8.0.3", + "resolved": "https://registry.npmjs.org/vite/-/vite-8.0.3.tgz", + "integrity": "sha512-B9ifbFudT1TFhfltfaIPgjo9Z3mDynBTJSUYxTjOQruf/zHH+ezCQKcoqO+h7a9Pw9Nm/OtlXAiGT1axBgwqrQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "lightningcss": "^1.32.0", + "picomatch": "^4.0.4", + "postcss": "^8.5.8", + "rolldown": "1.0.0-rc.12", + "tinyglobby": "^0.2.15" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^20.19.0 || >=22.12.0", + "@vitejs/devtools": "^0.1.0", + "esbuild": "^0.27.0", + "jiti": ">=1.21.0", + "less": "^4.0.0", + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "@vitejs/devtools": { + "optional": true + }, + "esbuild": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/vitest": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-4.1.2.tgz", + "integrity": "sha512-xjR1dMTVHlFLh98JE3i/f/WePqJsah4A0FK9cc8Ehp9Udk0AZk6ccpIZhh1qJ/yxVWRZ+Q54ocnD8TXmkhspGg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/expect": "4.1.2", + "@vitest/mocker": "4.1.2", + "@vitest/pretty-format": "4.1.2", + "@vitest/runner": "4.1.2", + "@vitest/snapshot": "4.1.2", + "@vitest/spy": "4.1.2", + "@vitest/utils": "4.1.2", + "es-module-lexer": "^2.0.0", + "expect-type": "^1.3.0", + "magic-string": "^0.30.21", + "obug": "^2.1.1", + "pathe": "^2.0.3", + "picomatch": "^4.0.3", + "std-env": "^4.0.0-rc.1", + "tinybench": "^2.9.0", + "tinyexec": "^1.0.2", + "tinyglobby": "^0.2.15", + "tinyrainbow": "^3.1.0", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0", + "why-is-node-running": "^2.3.0" + }, + "bin": { + "vitest": "vitest.mjs" + }, + "engines": { + "node": "^20.0.0 || ^22.0.0 || >=24.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@edge-runtime/vm": "*", + "@opentelemetry/api": "^1.9.0", + "@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0", + "@vitest/browser-playwright": "4.1.2", + "@vitest/browser-preview": "4.1.2", + "@vitest/browser-webdriverio": "4.1.2", + "@vitest/ui": "4.1.2", + "happy-dom": "*", + "jsdom": "*", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "@edge-runtime/vm": { + "optional": true + }, + "@opentelemetry/api": { + "optional": true + }, + "@types/node": { + "optional": true + }, + "@vitest/browser-playwright": { + "optional": true + }, + "@vitest/browser-preview": { + "optional": true + }, + "@vitest/browser-webdriverio": { + "optional": true + }, + "@vitest/ui": { + "optional": true + }, + "happy-dom": { + "optional": true + }, + "jsdom": { + "optional": true + }, + "vite": { + "optional": false + } + } + }, + "node_modules/why-is-node-running": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", + "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==", + "dev": true, + "license": "MIT", + "dependencies": { + "siginfo": "^2.0.0", + "stackback": "0.0.2" + }, + "bin": { + "why-is-node-running": "cli.js" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/wordwrap": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz", diff --git a/package.json b/package.json index 2877ecb..9aea0c7 100644 --- a/package.json +++ b/package.json @@ -9,7 +9,7 @@ }, "scripts": { "start": "node bin/aisona.js", - "test": "echo \"No tests yet\" && exit 0" + "test": "vitest run" }, "keywords": [ "ai", @@ -43,5 +43,8 @@ "js-yaml": "^4.1.1", "ora": "^9.3.0", "simple-git": "^3.33.0" + }, + "devDependencies": { + "vitest": "^4.1.2" } } diff --git a/src/lib/exporter.js b/src/lib/exporter.js index 094c077..1c40b1b 100644 --- a/src/lib/exporter.js +++ b/src/lib/exporter.js @@ -37,16 +37,17 @@ function cleanOutput(text) { * Export aisona data to a specific tool's config file */ export function exportToTool(aisona, toolId, dir) { - const template = loadTemplate(toolId); - if (!template) { - return { success: false, error: `No template found for tool: ${toolId}` }; - } - + // Check enabled BEFORE loading template const toolConfig = aisona.tools?.[toolId]; if (toolConfig && toolConfig.enabled === false) { return { success: false, error: `Tool ${toolId} is disabled in aisona.yml` }; } + const template = loadTemplate(toolId); + if (!template) { + return { success: false, error: `No template found for tool: ${toolId}` }; + } + const output = cleanOutput(template(aisona)); const toolOutput = TOOL_OUTPUTS[toolId]; diff --git a/src/lib/parser.js b/src/lib/parser.js index 7afe57e..eb14e43 100644 --- a/src/lib/parser.js +++ b/src/lib/parser.js @@ -62,7 +62,15 @@ export function parseClaudeMd(filePath) { .join(' ') .trim(); - if (rulesSections.some(s => section.includes(s))) { + // Order matters: check more specific sections first to avoid false matches + // e.g., "teaching style" should match teaching, not style/preferences + if (teachingSections.some(s => section.includes(s))) { + if (prose) result.personality.teaching = prose; + result.preferences.push(...bullets); + } else if (personalitySections.some(s => section.includes(s))) { + if (prose) result.personality.tone = prose; + result.personality.style.push(...bullets); + } else if (rulesSections.some(s => section.includes(s))) { result.rules.push(...bullets); if (prose && section.includes('autonomy')) { result.personality.autonomy = prose; @@ -72,11 +80,6 @@ export function parseClaudeMd(filePath) { if (bullets.length > 0 && !result.personality.tone) { result.personality.style.push(...bullets); } - } else if (personalitySections.some(s => section.includes(s))) { - if (prose) result.personality.tone = prose; - result.personality.style.push(...bullets); - } else if (teachingSections.some(s => section.includes(s))) { - if (prose) result.personality.teaching = prose; result.preferences.push(...bullets); } else if (memorySections.some(s => section.includes(s))) { result.memories.push(...bullets); diff --git a/tests/config.test.js b/tests/config.test.js new file mode 100644 index 0000000..313f46d --- /dev/null +++ b/tests/config.test.js @@ -0,0 +1,78 @@ +import { describe, it, expect, beforeEach, afterEach } from 'vitest'; +import fs from 'fs'; +import path from 'path'; +import os from 'os'; +import { loadAisona, saveAisona, getDefaultAisona, findAisonaFile } from '../src/lib/config.js'; + +const TMP = path.join(os.tmpdir(), 'aisona-test-config-' + Date.now()); + +beforeEach(() => fs.mkdirSync(TMP, { recursive: true })); +afterEach(() => fs.rmSync(TMP, { recursive: true, force: true })); + +describe('getDefaultAisona', () => { + it('returns valid structure with all required fields', () => { + const def = getDefaultAisona(); + expect(def.version).toBe(1); + expect(def.identity).toBeDefined(); + expect(def.identity.name).toBe(''); + expect(def.personality).toBeDefined(); + expect(def.personality.tone).toBe(''); + expect(def.rules).toEqual([]); + expect(def.preferences).toEqual([]); + expect(def.memories).toEqual([]); + expect(def.tools.claude.enabled).toBe(true); + expect(def.tools.cursor.enabled).toBe(true); + expect(def.tools.gemini.enabled).toBe(false); + }); +}); + +describe('saveAisona + loadAisona roundtrip', () => { + it('saves and loads YAML correctly', () => { + const data = getDefaultAisona(); + data.identity.name = 'Test User'; + data.identity.language = 'Cantonese'; + data.rules = ['Never commit without asking', 'Use feature branches']; + data.personality.tone = 'Direct and concise'; + data.memories = ['User prefers short responses']; + + const filePath = path.join(TMP, 'aisona.yml'); + saveAisona(filePath, data); + + expect(fs.existsSync(filePath)).toBe(true); + + const loaded = loadAisona(filePath); + expect(loaded.identity.name).toBe('Test User'); + expect(loaded.identity.language).toBe('Cantonese'); + expect(loaded.rules).toEqual(['Never commit without asking', 'Use feature branches']); + expect(loaded.personality.tone).toBe('Direct and concise'); + expect(loaded.memories).toEqual(['User prefers short responses']); + }); + + it('preserves tool config through roundtrip', () => { + const data = getDefaultAisona(); + data.tools.claude.extra_rules = ['Explain like a debugger']; + data.tools.cursor.enabled = false; + + const filePath = path.join(TMP, 'aisona.yml'); + saveAisona(filePath, data); + const loaded = loadAisona(filePath); + + expect(loaded.tools.claude.extra_rules).toEqual(['Explain like a debugger']); + expect(loaded.tools.cursor.enabled).toBe(false); + }); +}); + +describe('findAisonaFile', () => { + it('finds aisona.yml in the given directory', () => { + const filePath = path.join(TMP, 'aisona.yml'); + fs.writeFileSync(filePath, 'version: 1\n'); + + const found = findAisonaFile(TMP); + expect(found).toBe(filePath); + }); + + it('returns null when no aisona.yml exists', () => { + const found = findAisonaFile(TMP); + expect(found).toBeNull(); + }); +}); diff --git a/tests/exporter.test.js b/tests/exporter.test.js new file mode 100644 index 0000000..d1d9594 --- /dev/null +++ b/tests/exporter.test.js @@ -0,0 +1,229 @@ +import { describe, it, expect, beforeEach, afterEach } from 'vitest'; +import fs from 'fs'; +import path from 'path'; +import os from 'os'; +import { exportToTool, exportToAll, getSupportedTools } from '../src/lib/exporter.js'; + +const TMP = path.join(os.tmpdir(), 'aisona-test-export-' + Date.now()); + +beforeEach(() => fs.mkdirSync(TMP, { recursive: true })); +afterEach(() => fs.rmSync(TMP, { recursive: true, force: true })); + +const SAMPLE_AISONA = { + version: 1, + identity: { + name: 'TestUser', + role: 'Backend engineer', + experience: '11 months', + context: 'AI security', + language: 'Cantonese (廣東話)', + }, + personality: { + tone: 'Direct and concise', + verbosity: 'concise', + style: ['No emojis', 'Short sentences'], + teaching: 'Explain like a senior engineer', + autonomy: 'Work autonomously', + }, + rules: ['Never commit without asking', 'Use feature branches'], + preferences: ['Use Playwright for testing'], + memories: ['User prefers Cantonese'], + tools: { + claude: { enabled: true, extra_rules: ['End tasks with learning section'] }, + cursor: { enabled: true, extra_rules: ['Use .mdc format'] }, + gemini: { enabled: true, extra_rules: [] }, + copilot: { enabled: true, extra_rules: [] }, + windsurf: { enabled: false, extra_rules: [] }, + }, +}; + +describe('getSupportedTools', () => { + it('returns list of supported tool ids', () => { + const tools = getSupportedTools(); + expect(tools).toContain('claude'); + expect(tools).toContain('cursor'); + expect(tools).toContain('gemini'); + expect(tools).toContain('copilot'); + expect(tools).toContain('windsurf'); + }); +}); + +describe('exportToTool — Claude', () => { + it('generates valid CLAUDE.md', () => { + const result = exportToTool(SAMPLE_AISONA, 'claude', TMP); + + expect(result.success).toBe(true); + expect(result.tool).toBe('claude'); + + const content = fs.readFileSync(result.path, 'utf8'); + expect(content).toContain('Cantonese (廣東話)'); + expect(content).toContain('Direct and concise'); + expect(content).toContain('No emojis'); + expect(content).toContain('Never commit without asking'); + expect(content).toContain('Use Playwright for testing'); + expect(content).toContain('User prefers Cantonese'); + expect(content).toContain('End tasks with learning section'); + }); + + it('includes all sections with correct headers', () => { + const result = exportToTool(SAMPLE_AISONA, 'claude', TMP); + const content = fs.readFileSync(result.path, 'utf8'); + + expect(content).toContain('## Language'); + expect(content).toContain('## Tone and Style'); + expect(content).toContain('## Teaching Style'); + expect(content).toContain('## Autonomy'); + expect(content).toContain('## Rules'); + expect(content).toContain('## Preferences'); + expect(content).toContain('## Context'); + expect(content).toContain('## Claude-Specific'); + }); + + it('writes to CLAUDE.md', () => { + exportToTool(SAMPLE_AISONA, 'claude', TMP); + expect(fs.existsSync(path.join(TMP, 'CLAUDE.md'))).toBe(true); + }); +}); + +describe('exportToTool — Cursor', () => { + it('generates valid .cursorrules', () => { + const result = exportToTool(SAMPLE_AISONA, 'cursor', TMP); + + expect(result.success).toBe(true); + const content = fs.readFileSync(result.path, 'utf8'); + + expect(content).toContain('Backend engineer'); + expect(content).toContain('Cantonese'); + expect(content).toContain('Never commit without asking'); + expect(content).toContain('Use .mdc format'); + }); + + it('writes to .cursorrules', () => { + exportToTool(SAMPLE_AISONA, 'cursor', TMP); + expect(fs.existsSync(path.join(TMP, '.cursorrules'))).toBe(true); + }); +}); + +describe('exportToTool — Gemini', () => { + it('generates valid GEMINI.md', () => { + const result = exportToTool(SAMPLE_AISONA, 'gemini', TMP); + + expect(result.success).toBe(true); + const content = fs.readFileSync(result.path, 'utf8'); + + expect(content).toContain('Cantonese'); + expect(content).toContain('Backend engineer'); + expect(content).toContain('11 months'); + expect(content).toContain('Never commit without asking'); + }); + + it('writes to GEMINI.md', () => { + exportToTool(SAMPLE_AISONA, 'gemini', TMP); + expect(fs.existsSync(path.join(TMP, 'GEMINI.md'))).toBe(true); + }); +}); + +describe('exportToTool — Copilot', () => { + it('generates valid copilot-instructions.md', () => { + const result = exportToTool(SAMPLE_AISONA, 'copilot', TMP); + + expect(result.success).toBe(true); + const content = fs.readFileSync(result.path, 'utf8'); + + expect(content).toContain('Cantonese'); + expect(content).toContain('Never commit without asking'); + }); + + it('writes to .github/copilot-instructions.md', () => { + exportToTool(SAMPLE_AISONA, 'copilot', TMP); + expect(fs.existsSync(path.join(TMP, '.github', 'copilot-instructions.md'))).toBe(true); + }); +}); + +describe('exportToTool — disabled tool', () => { + it('refuses to export to disabled tool', () => { + const result = exportToTool(SAMPLE_AISONA, 'windsurf', TMP); + expect(result.success).toBe(false); + expect(result.error).toContain('disabled'); + }); +}); + +describe('exportToTool — unknown tool', () => { + it('fails for unknown tool', () => { + const result = exportToTool(SAMPLE_AISONA, 'nonexistent', TMP); + expect(result.success).toBe(false); + }); +}); + +describe('exportToAll', () => { + it('exports to all enabled tools', () => { + const results = exportToAll(SAMPLE_AISONA, TMP); + + const succeeded = results.filter(r => r.success); + expect(succeeded.length).toBe(4); // claude, cursor, gemini, copilot (windsurf disabled) + + expect(fs.existsSync(path.join(TMP, 'CLAUDE.md'))).toBe(true); + expect(fs.existsSync(path.join(TMP, '.cursorrules'))).toBe(true); + expect(fs.existsSync(path.join(TMP, 'GEMINI.md'))).toBe(true); + expect(fs.existsSync(path.join(TMP, '.github', 'copilot-instructions.md'))).toBe(true); + // windsurf disabled — should NOT exist + expect(fs.existsSync(path.join(TMP, '.windsurfrules'))).toBe(false); + }); + + it('skips tools with no output config', () => { + const aisona = { + ...SAMPLE_AISONA, + tools: { claude: { enabled: true, extra_rules: [] } }, + }; + const results = exportToAll(aisona, TMP); + expect(results.length).toBe(1); + expect(results[0].tool).toBe('claude'); + }); +}); + +describe('export output quality', () => { + it('does not have triple+ blank lines', () => { + const result = exportToTool(SAMPLE_AISONA, 'claude', TMP); + const content = fs.readFileSync(result.path, 'utf8'); + expect(content).not.toMatch(/\n{4,}/); + }); + + it('ends with single newline', () => { + const result = exportToTool(SAMPLE_AISONA, 'claude', TMP); + const content = fs.readFileSync(result.path, 'utf8'); + expect(content.endsWith('\n')).toBe(true); + expect(content.endsWith('\n\n')).toBe(false); + }); + + it('contains aisona attribution comment', () => { + // Templates should have the aisona attribution but it's in Handlebars comments + // which don't render. Check the generated output is clean. + const result = exportToTool(SAMPLE_AISONA, 'claude', TMP); + const content = fs.readFileSync(result.path, 'utf8'); + // Should NOT contain raw handlebars + expect(content).not.toContain('{{'); + expect(content).not.toContain('}}'); + }); + + it('handles empty arrays gracefully', () => { + const minimal = { + version: 1, + identity: { name: 'Test', language: 'English' }, + personality: { tone: 'Friendly' }, + rules: [], + preferences: [], + memories: [], + tools: { claude: { enabled: true, extra_rules: [] } }, + }; + + const result = exportToTool(minimal, 'claude', TMP); + expect(result.success).toBe(true); + const content = fs.readFileSync(result.path, 'utf8'); + + // Should have language and tone but no empty sections + expect(content).toContain('English'); + expect(content).toContain('Friendly'); + // Should NOT have "## Rules" with nothing under it + expect(content).not.toContain('## Rules\n\n##'); + }); +}); diff --git a/tests/parser.test.js b/tests/parser.test.js new file mode 100644 index 0000000..ea738d5 --- /dev/null +++ b/tests/parser.test.js @@ -0,0 +1,203 @@ +import { describe, it, expect, beforeEach, afterEach } from 'vitest'; +import fs from 'fs'; +import path from 'path'; +import os from 'os'; +import { parseClaudeMd, parseCursorRules, detectTools } from '../src/lib/parser.js'; + +const TMP = path.join(os.tmpdir(), 'aisona-test-parser-' + Date.now()); + +beforeEach(() => fs.mkdirSync(TMP, { recursive: true })); +afterEach(() => fs.rmSync(TMP, { recursive: true, force: true })); + +describe('parseClaudeMd', () => { + it('parses a simple CLAUDE.md with rules and preferences', () => { + const content = `## Language + +Always reply in Cantonese. + +## Rules + +- Never commit without asking +- Never push to remote without asking + +## Preferences + +- Use Playwright for testing +- Short responses preferred +`; + const filePath = path.join(TMP, 'CLAUDE.md'); + fs.writeFileSync(filePath, content); + + const result = parseClaudeMd(filePath); + expect(result.rules).toContain('Never commit without asking'); + expect(result.rules).toContain('Never push to remote without asking'); + expect(result.preferences).toContain('Use Playwright for testing'); + }); + + it('extracts personality from tone/style sections', () => { + const content = `## Tone and Style + +Be concise and direct. + +- No emojis unless asked +- Short sentences +`; + const filePath = path.join(TMP, 'CLAUDE.md'); + fs.writeFileSync(filePath, content); + + const result = parseClaudeMd(filePath); + expect(result.personality.tone).toContain('concise and direct'); + expect(result.personality.style).toContain('No emojis unless asked'); + }); + + it('extracts autonomy from autonomy section', () => { + const content = `## Autonomy + +Work autonomously. Only ask before destructive actions. + +- Never ask before reading files +- Always ask before git push +`; + const filePath = path.join(TMP, 'CLAUDE.md'); + fs.writeFileSync(filePath, content); + + const result = parseClaudeMd(filePath); + expect(result.personality.autonomy).toContain('Work autonomously'); + expect(result.rules).toContain('Never ask before reading files'); + }); + + it('extracts teaching style', () => { + const content = `## Teaching Style + +Explain like a senior engineer. Always include a learning section after tasks. + +- Frame lessons as reusable mental models +- Never skip teaching even for simple tasks +`; + const filePath = path.join(TMP, 'CLAUDE.md'); + fs.writeFileSync(filePath, content); + + const result = parseClaudeMd(filePath); + expect(result.personality.teaching).toContain('senior engineer'); + expect(result.preferences).toContain('Frame lessons as reusable mental models'); + }); + + it('handles Nicole real-world CLAUDE.md structure', () => { + // Simulates the structure of the actual ~/.claude/CLAUDE.md + const content = `## Language + +Always reply in Cantonese (廣東話) unless the user writes in another language or explicitly asks for English. + +## Technical Explanations + +When explaining code, explain it so the user can clearly picture what the computer is doing. + +- Show where data comes from and where it goes +- Name the actual tables, functions, fields involved +- Do not skip the hidden middle steps + +## Teaching Style + +The user is a junior developer (~11 months experience). + +- End every completed task with a learning section +- Talk like a helpful senior engineer + +## Autonomy + +Work autonomously. Only ask for confirmation before: +- Destructive actions: deleting files, dropping DB tables +- Actions visible to others: pushing to remote, opening PRs + +## Project Knowledge Capture + +When working on projects, if you discover something reusable, mention it. + +- Check project docs before suggesting changes +`; + const filePath = path.join(TMP, 'CLAUDE.md'); + fs.writeFileSync(filePath, content); + + const result = parseClaudeMd(filePath); + + // Should extract rules from autonomy + expect(result.rules.length).toBeGreaterThan(0); + + // Should extract teaching + expect(result.personality.teaching).toContain('junior developer'); + + // Should extract style from technical explanations + expect(result.preferences.some(p => p.includes('data comes from'))).toBe(true); + }); + + it('handles empty CLAUDE.md', () => { + const filePath = path.join(TMP, 'CLAUDE.md'); + fs.writeFileSync(filePath, ''); + + const result = parseClaudeMd(filePath); + expect(result.rules).toEqual([]); + expect(result.preferences).toEqual([]); + }); +}); + +describe('parseCursorRules', () => { + it('extracts rules from .cursorrules', () => { + const content = `# Coding Rules + +- Always use TypeScript +- Prefer functional style +- No classes unless necessary + +Use concise variable names. +`; + const filePath = path.join(TMP, '.cursorrules'); + fs.writeFileSync(filePath, content); + + const result = parseCursorRules(filePath); + expect(result.rules).toContain('Always use TypeScript'); + expect(result.rules).toContain('Prefer functional style'); + expect(result.context).toContain('concise variable names'); + }); +}); + +describe('detectTools', () => { + it('detects CLAUDE.md in project directory', () => { + fs.writeFileSync(path.join(TMP, 'CLAUDE.md'), '# Test'); + + const detected = detectTools(TMP); + const claude = detected.find(d => d.tool === 'claude' && d.scope === 'project'); + expect(claude).toBeDefined(); + }); + + it('detects .cursorrules', () => { + fs.writeFileSync(path.join(TMP, '.cursorrules'), '# Test'); + + const detected = detectTools(TMP); + const cursor = detected.find(d => d.tool === 'cursor'); + expect(cursor).toBeDefined(); + }); + + it('detects copilot instructions', () => { + fs.mkdirSync(path.join(TMP, '.github'), { recursive: true }); + fs.writeFileSync(path.join(TMP, '.github', 'copilot-instructions.md'), '# Test'); + + const detected = detectTools(TMP); + const copilot = detected.find(d => d.tool === 'copilot'); + expect(copilot).toBeDefined(); + }); + + it('detects AGENTS.md', () => { + fs.writeFileSync(path.join(TMP, 'AGENTS.md'), '# Test'); + + const detected = detectTools(TMP); + const agents = detected.find(d => d.tool === 'agentsmd'); + expect(agents).toBeDefined(); + }); + + it('returns empty array for empty directory', () => { + const detected = detectTools(TMP); + // May detect global configs from ~/.claude etc, but no project-level + const projectLevel = detected.filter(d => d.scope === 'project'); + expect(projectLevel).toEqual([]); + }); +}); From 76b94c4e6d6d7561362c6840401ec5206f3df4a8 Mon Sep 17 00:00:00 2001 From: ithiria894 Date: Fri, 27 Mar 2026 18:56:20 -0700 Subject: [PATCH 3/4] =?UTF-8?q?fix:=20improve=20CLAUDE.md=20parser=20?= =?UTF-8?q?=E2=80=94=20better=20section=20classification=20and=20rule=20ex?= =?UTF-8?q?traction?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Rewrite section classifier with explicit category mapping (ordered by specificity) - Parse H3 headers as section boundaries (not just H2) - Extract autonomy prose rules: lines starting with Always/Never/Only → rules array - Filter standalone bold headers from bullets - Support numbered lists (1. 2. 3.) as extractable items - Fix duplicate preferences push in prefs section Dogfood test results improved: Before: 2 rules, 16 preferences, 0 memories After: 4 rules, 30 preferences, 1 memory Co-Authored-By: Claude Opus 4.6 (1M context) --- src/lib/parser.js | 169 ++++++++++++++++++++++++++++++++-------------- 1 file changed, 120 insertions(+), 49 deletions(-) diff --git a/src/lib/parser.js b/src/lib/parser.js index eb14e43..50258b9 100644 --- a/src/lib/parser.js +++ b/src/lib/parser.js @@ -10,9 +10,10 @@ import path from 'path'; /** * Parse a CLAUDE.md file into structured sections. * CLAUDE.md is free-form markdown, so we use heuristics: - * - H2 headers (##) = section boundaries - * - Bullet points = rules/preferences - * - Prose = personality/context + * - H2/H3 headers = section boundaries + * - Bullet points (- or *) = extractable items + * - Numbered lists (1. 2. 3.) = extractable items + * - Prose paragraphs = context/description */ export function parseClaudeMd(filePath) { const content = fs.readFileSync(filePath, 'utf8'); @@ -29,11 +30,13 @@ export function parseClaudeMd(filePath) { let currentSection = '_preamble'; for (const line of lines) { - // Detect H2 headers - const h2Match = line.match(/^##\s+(.+)/); - if (h2Match) { - currentSection = h2Match[1].trim().toLowerCase(); - result.raw_sections[currentSection] = []; + // Detect H2 or H3 headers as section boundaries + const headerMatch = line.match(/^#{2,3}\s+(.+)/); + if (headerMatch) { + currentSection = headerMatch[1].trim().toLowerCase(); + if (!result.raw_sections[currentSection]) { + result.raw_sections[currentSection] = []; + } continue; } @@ -43,51 +46,119 @@ export function parseClaudeMd(filePath) { result.raw_sections[currentSection].push(line); } - // Extract rules from common section names - const rulesSections = ['rules', 'autonomy', 'constraints', 'boundaries', 'hard rules']; - const prefsSections = ['preferences', 'style', 'code style', 'coding style', 'conventions']; - const personalitySections = ['language', 'tone', 'communication', 'personality', 'tone and style']; - const teachingSections = ['teaching', 'teaching style', 'learning', 'explanations', 'technical explanations']; - const memorySections = ['memory', 'memories', 'learned', 'context', 'project knowledge capture']; - - for (const [section, lines] of Object.entries(result.raw_sections)) { - const bullets = lines - .filter(l => l.match(/^[-*]\s+/)) - .map(l => l.replace(/^[-*]\s+/, '').trim()) - .filter(l => l.length > 0); - - const prose = lines - .filter(l => !l.match(/^[-*]\s+/) && !l.match(/^#/) && l.trim().length > 0) - .map(l => l.trim()) - .join(' ') - .trim(); - - // Order matters: check more specific sections first to avoid false matches - // e.g., "teaching style" should match teaching, not style/preferences - if (teachingSections.some(s => section.includes(s))) { - if (prose) result.personality.teaching = prose; - result.preferences.push(...bullets); - } else if (personalitySections.some(s => section.includes(s))) { - if (prose) result.personality.tone = prose; - result.personality.style.push(...bullets); - } else if (rulesSections.some(s => section.includes(s))) { - result.rules.push(...bullets); - if (prose && section.includes('autonomy')) { - result.personality.autonomy = prose; + // Section classification — ordered by specificity (most specific first) + const sectionMap = [ + { keywords: ['teaching style', 'teaching angle'], category: 'teaching' }, + { keywords: ['technical explanations', 'explanations'], category: 'teaching' }, + { keywords: ['language'], category: 'language' }, + { keywords: ['tone and style', 'tone', 'communication', 'personality'], category: 'personality' }, + { keywords: ['autonomy'], category: 'autonomy' }, + { keywords: ['rules', 'constraints', 'boundaries', 'hard rules'], category: 'rules' }, + { keywords: ['style', 'code style', 'coding style', 'conventions', 'preferences'], category: 'preferences' }, + { keywords: ['memory', 'memories', 'learned', 'context', 'project knowledge'], category: 'memories' }, + ]; + + function classifySection(sectionName) { + for (const entry of sectionMap) { + if (entry.keywords.some(k => sectionName.includes(k))) { + return entry.category; } - } else if (prefsSections.some(s => section.includes(s))) { - result.preferences.push(...bullets); - if (bullets.length > 0 && !result.personality.tone) { + } + return 'unknown'; + } + + for (const [section, sectionLines] of Object.entries(result.raw_sections)) { + if (section === '_preamble') continue; + + const category = classifySection(section); + + // Extract bullet points (- item, * item) and numbered lists (1. item) + const bullets = sectionLines + .filter(l => l.match(/^\s*[-*]\s+/) || l.match(/^\s*\d+\.\s+/)) + .map(l => l.replace(/^\s*[-*]\s+/, '').replace(/^\s*\d+\.\s+/, '').trim()) + .filter(l => l.length > 0 && !l.match(/^\*\*[^*]+\*\*$/)); // skip standalone bold headers + + // Extract prose — non-bullet, non-header, non-empty lines + const proseLines = sectionLines + .filter(l => + !l.match(/^\s*[-*]\s+/) && + !l.match(/^\s*\d+\.\s+/) && + !l.match(/^#{1,4}\s+/) && + !l.match(/^---\s*$/) && + l.trim().length > 0 + ) + .map(l => l.trim()); + + // First meaningful prose line (for short descriptions) + const firstProse = proseLines[0] || ''; + // Full prose (for longer sections) + const fullProse = proseLines.join(' ').trim(); + + switch (category) { + case 'language': + result.personality.tone = firstProse || fullProse; result.personality.style.push(...bullets); - } - result.preferences.push(...bullets); - } else if (memorySections.some(s => section.includes(s))) { - result.memories.push(...bullets); - } else { - // Unknown sections — add bullets as preferences - if (bullets.length > 0) { + break; + + case 'personality': + if (fullProse) result.personality.tone = fullProse; + result.personality.style.push(...bullets); + break; + + case 'teaching': + // For teaching, extract first paragraph as summary, bullets as preferences + if (firstProse) { + // If we already have teaching, append. Otherwise set. + result.personality.teaching = result.personality.teaching + ? result.personality.teaching + ' ' + firstProse + : firstProse; + } + // Bullets from teaching sections go to preferences (they're style guides) result.preferences.push(...bullets); + break; + + case 'autonomy': { + // Autonomy sections mix prose and rules. Extract both. + // Prose lines that start with "Always" or "Never" or "Only" are rules + const autonomyRules = proseLines.filter(l => + l.match(/^(Always|Never|Only|Do not|Don't)\b/i) + ); + const autonomyProse = proseLines.filter(l => + !l.match(/^(Always|Never|Only|Do not|Don't)\b/i) + ).join(' ').trim(); + + if (autonomyProse) result.personality.autonomy = autonomyProse; + result.rules.push(...autonomyRules); + result.rules.push(...bullets); + break; } + + case 'rules': + result.rules.push(...bullets); + if (fullProse && !bullets.length) { + // If rules section has prose but no bullets, treat prose as a rule + result.rules.push(fullProse); + } + break; + + case 'preferences': + result.preferences.push(...bullets); + break; + + case 'memories': + result.memories.push(...bullets); + if (fullProse && !bullets.length) { + result.memories.push(fullProse); + } + break; + + case 'unknown': + default: + // Unknown sections — bullets go to preferences, prose to memories + if (bullets.length > 0) { + result.preferences.push(...bullets); + } + break; } } From 0b8b2ae2f6f4e6253bd55d90787a9d15dc3b2741 Mon Sep 17 00:00:00 2001 From: ithiria894 Date: Fri, 27 Mar 2026 19:11:31 -0700 Subject: [PATCH 4/4] refactor: rewrite schema from "AI soul" to "AI habits portability" MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Major direction change based on user feedback: - Not building "AI's soul/personality" (too philosophical, wastes tokens) - Building "pack your AI habits and take them anywhere" (practical) Schema changes: - personality.{tone,style,teaching,autonomy} → preferences.{tone,habits,teaching,autonomy} - personality.style[] → preferences.habits[] (clearer naming) - tools.*.extra_rules[] → tools.*.extra[] (simpler) - Removed verbose personality fields (worldview, contradictions, vocabulary) - Added practical fields focus (habits, rules, memories) Updated: SPEC.md, all 4 templates, config.js, parser.js, init.js, all tests 29/29 tests passing. Co-Authored-By: Claude Opus 4.6 (1M context) --- SPEC.md | 236 ++++++++++++-------- src/commands/init.js | 11 +- src/lib/config.js | 19 +- src/lib/parser.js | 23 +- src/templates/claude.hbs | 31 ++- src/templates/copilot.hbs | 19 +- src/templates/cursor.hbs | 23 +- src/templates/gemini.hbs | 34 +-- test-output/.cursorrules | 28 +++ test-output/.github/copilot-instructions.md | 26 +++ test-output/CLAUDE.md | 44 ++++ test-output/GEMINI.md | 37 +++ test-output/aisona.yml | 80 +++++++ tests/config.test.js | 14 +- tests/exporter.test.js | 130 +++-------- tests/parser.test.js | 191 ++++------------ 16 files changed, 505 insertions(+), 441 deletions(-) create mode 100644 test-output/.cursorrules create mode 100644 test-output/.github/copilot-instructions.md create mode 100644 test-output/CLAUDE.md create mode 100644 test-output/GEMINI.md create mode 100644 test-output/aisona.yml diff --git a/SPEC.md b/SPEC.md index e97b447..58c8c2f 100644 --- a/SPEC.md +++ b/SPEC.md @@ -1,10 +1,30 @@ # aisona.yml Format Specification v1 -> The portable AI persona format. Define your AI's personality once, deploy everywhere. +> Pack your AI habits and take them anywhere. A portable format for your AI usage preferences, rules, and learned context — backed by Git, exported to any tool. ## Overview -`aisona.yml` is a structured YAML file that defines an AI assistant's persona — personality, communication style, rules, preferences, and memories — in a tool-agnostic format that can be exported to any AI coding tool. +`aisona.yml` is a structured YAML file that captures how YOU use AI tools — your language preference, your rules, your workflow habits, things your AI has learned about you. It's portable: write it once, export to Claude Code, Cursor, Gemini, Copilot, or any tool that reads config files. + +Think of it like your browser bookmarks or your shell dotfiles — except for your AI. + +## What It Captures + +``` +✅ Things worth packing: + - "Reply in Cantonese" → language preference + - "Always ask before committing" → hard rule + - "Use Playwright, not Chrome DevTools" → tool preference + - "I'm a junior dev, explain things" → context for AI calibration + - "bare npx corrupts nvm, use full path" → learned lesson + - "End tasks with a learning section" → behavior preference + +❌ Things NOT worth packing (waste of tokens): + - Your life philosophy / worldview + - Your personality contradictions + - Words you'd never use + - Your rhetorical style analysis +``` ## Schema @@ -12,157 +32,187 @@ # aisona.yml v1 version: 1 -# === IDENTITY === -# Who the user is (so the AI knows who it's talking to) +# === WHO YOU ARE === +# Enough context for AI to calibrate responses. Keep it short. identity: - name: "" # User's name or alias - role: "" # Professional role/context - experience: "" # Level of experience (helps AI calibrate explanations) - context: "" # Current work context - language: "" # Preferred response language (e.g., "Cantonese", "English") - -# === PERSONALITY === -# How the AI should behave and communicate -personality: - tone: "" # Overall communication tone + name: "" # Your name or alias + role: "" # What you do (e.g., "Backend engineer") + experience: "" # Level (e.g., "11 months", "senior", "student") + language: "" # Preferred AI response language + +# === HOW YOU LIKE IT === +# Your AI usage habits — how you want the AI to behave. +preferences: + tone: "" # e.g., "Direct and concise" or "Detailed with examples" verbosity: "" # "concise" | "balanced" | "detailed" - style: [] # List of style directives - teaching: "" # How to teach/explain (if applicable) - autonomy: "" # How autonomous the AI should be + autonomy: "" # e.g., "Work autonomously, only ask before destructive actions" + teaching: "" # e.g., "Explain what changed and why after each task" + habits: [] # List of specific behavior preferences -# === RULES === -# Hard rules the AI must follow -rules: [] # List of rule strings +# === HARD RULES === +# Things the AI must ALWAYS or NEVER do. Violations = bugs. +rules: [] -# === PREFERENCES === -# Soft preferences (nice-to-have, not hard rules) -preferences: [] # List of preference strings +# === THINGS AI LEARNED ABOUT YOU === +# Facts, lessons, context from past usage. Portable memory. +memories: [] -# === MEMORIES === -# Learned facts and preferences from past interactions -memories: [] # List of memory strings - -# === TOOLS === -# Per-tool overrides and extra configuration +# === PER-TOOL SETTINGS === +# Tool-specific overrides. Only enable tools you actually use. tools: claude: enabled: true - extra_rules: [] # Rules only for Claude - extra_context: "" # Additional context for Claude + extra: [] # Claude-specific rules/preferences cursor: enabled: true - extra_rules: [] + extra: [] gemini: - enabled: true - extra_rules: [] - copilot: - enabled: true - extra_rules: [] - windsurf: enabled: false - extra_rules: [] - aider: + extra: [] + copilot: enabled: false - extra_rules: [] - openrouter: + extra: [] + windsurf: enabled: false - system_prompt_append: "" # Appended to system prompt + extra: [] +``` + +## Real Example + +```yaml +version: 1 + +identity: + name: Nicole + role: Backend engineer + experience: 11 months + language: Cantonese (廣東話) + +preferences: + tone: Direct and concise, like a helpful senior engineer + verbosity: concise + autonomy: Work autonomously. Only ask before destructive actions or pushing to remote. + teaching: End every task with a learning section — what changed, why, and one reusable concept. + habits: + - Show execution flow step by step, like a debugger + - Don't skip hidden middle steps + - Use Playwright for testing, not Chrome DevTools + - Use full nvm path for node/npx + +rules: + - Always ask before committing or pushing + - Never include company names in public repos + - Use feature branches for big changes + - Never ask before reading files, searching, or editing + +memories: + - Side projects use GitHub ithiria894, never company account + - bare npx corrupts nvm default alias — always use full path + - Reddit posts need narrative hook, no limitations talk + - After every workflow run, update playbook + OVERVIEW.md + +tools: + claude: + enabled: true + extra: + - Explain like stepping through a debugger + - End tasks with 7-point learning section + cursor: + enabled: true + extra: [] ``` ## Field Definitions -### identity (required) +### identity -Tells the AI who it's working with. Helps calibrate tone, explanations, and assumptions. +Minimum context so AI knows who it's talking to. Keep it short — every word costs tokens. | Field | Type | Required | Description | |-------|------|----------|-------------| -| `name` | string | yes | User's name or preferred alias | -| `role` | string | yes | What the user does (e.g., "Backend engineer") | -| `experience` | string | no | Experience level (e.g., "11 months", "senior", "student") | -| `context` | string | no | Current work context | -| `language` | string | no | Preferred language for responses | +| `name` | string | yes | Your name or alias | +| `role` | string | no | What you do | +| `experience` | string | no | Helps AI calibrate explanation depth | +| `language` | string | no | Preferred response language | -### personality (required) +### preferences -Defines how the AI communicates. This is what makes your AI feel like "yours." +How you like your AI to behave. These are soft — AI should follow but they're not hard rules. | Field | Type | Required | Description | |-------|------|----------|-------------| -| `tone` | string | yes | Overall tone (e.g., "Direct and concise, like a senior engineer") | -| `verbosity` | string | no | How verbose responses should be | -| `style` | string[] | no | Specific style directives | +| `tone` | string | no | Communication style | +| `verbosity` | string | no | concise / balanced / detailed | +| `autonomy` | string | no | When to ask vs just do | | `teaching` | string | no | How to explain things | -| `autonomy` | string | no | How much to ask vs. just do | +| `habits` | string[] | no | Specific behavior preferences | -### rules (required) +### rules -Hard constraints. The AI must follow these. Violations are bugs. +Hard constraints. AI must follow these. Breaking them = bug. Type: `string[]` -Example: -```yaml -rules: - - "Never commit without asking" - - "Never include company names in public repos" - - "Use feature branches for big changes" -``` +Keep rules **actionable and specific**. "Never commit without asking" is good. "Be ethical" is useless. -### preferences (optional) +### memories -Soft preferences. The AI should try to follow these but they're not hard rules. +Things your AI learned about you from past interactions. Portable context. Type: `string[]` -### memories (optional) +These are facts and lessons, not feelings. "User's npm org is ithiria" is good. "User seems frustrated today" is not. -Facts learned from past interactions. Manually curated. +### tools -Type: `string[]` - -### tools (optional) - -Per-tool configuration. Each tool key can have: +Per-tool overrides. Each key maps to a supported export target. | Field | Type | Description | |-------|------|-------------| | `enabled` | boolean | Whether to export to this tool | -| `extra_rules` | string[] | Rules that only apply to this tool | -| `extra_context` | string | Additional context for this tool | +| `extra` | string[] | Additional rules/preferences for this tool only | ## Export Targets | Tool | Output File | Location | |------|------------|----------| | Claude Code | `CLAUDE.md` | Project root or `~/.claude/` | -| Cursor | `.cursorrules` or `.cursor/rules/*.mdc` | Project root | +| Cursor | `.cursorrules` | Project root | | Gemini CLI | `GEMINI.md` | Project root or `~/.gemini/` | | GitHub Copilot | `.github/copilot-instructions.md` | Project root | | Windsurf | `.windsurfrules` | Project root | -| Aider | `.aider.conf.yml` | Project root | -| OpenRouter | System prompt text | API config | | AGENTS.md | `AGENTS.md` | Project root | ## Design Principles -1. **Human-readable** — YAML, not JSON. Easy to edit by hand. -2. **Tool-agnostic** — No tool-specific concepts in the core schema. -3. **Personality-first** — Identity and personality are required. Rules are secondary. -4. **User-owned** — Stored in user's own Git. No platform dependency. -5. **Backwards-compatible** — New fields are always optional. v1 files work with v2+ tools. -6. **Exportable** — Every field maps to at least one export target. +1. **Practical, not philosophical** — Capture habits and preferences, not worldview and soul +2. **Token-efficient** — Every field should be worth the tokens it costs in the AI's context window +3. **Human-readable** — YAML, not JSON. Easy to edit by hand +4. **User-owned** — Stored in your own Git repo. No platform dependency +5. **Tool-agnostic** — Core schema has no tool-specific concepts +6. **Backwards-compatible** — New fields always optional. v1 files work with v2+ tools +7. **Git as protocol** — aisona.yml lives in Git. Version history, branching, sharing all come free -## Versioning +## How It Works -The `version` field indicates the schema version. The CLI will warn if the file uses a newer version than supported. +``` +1. aisona init → Scans your existing CLAUDE.md → generates aisona.yml +2. Edit aisona.yml → Tweak your preferences, add rules, curate memories +3. aisona export --all → Generates CLAUDE.md + .cursorrules + GEMINI.md + ... +4. aisona watch --git → Auto re-export on change + git commit/push +5. New machine: + git clone your-repo + aisona export --all → All your AI tools instantly know you again +``` ## Prior Art -- SOUL.md — Personality-focused but free-form markdown, no schema, no export -- AGENTS.md — Coding rules standard, no personality/memory -- rulesync — Structured rules with export to 27+ tools, no personality -- Character Card V3 — JSON schema for roleplay, not coding -- CrewAI — role/goal/backstory YAML, too minimal +| Tool | What it does | What aisona adds | +|------|-------------|-----------------| +| SOUL.md | AI personality templates (fill-in-the-blank) | Structured schema + CLI + auto-export + git sync | +| personas.sh | Download pre-made AI personas (marketplace) | Pack YOUR OWN habits, not someone else's | +| PersonaSpec | JSON format for portable identity (API-first) | YAML (human-editable) + CLI + local-first + developer-focused | +| rulesync | Sync coding rules across 27+ tools | Also captures preferences, memories, teaching style — not just rules | +| memoir | Push/restore AI tool config files | Format-first (aisona.yml as source of truth, not raw file copying) | -aisona.yml combines the personality focus of SOUL.md with the structured exportability of rulesync. +aisona = **your AI habits, packed in a YAML, exported everywhere, synced via Git.** diff --git a/src/commands/init.js b/src/commands/init.js index 5be6a74..948fce8 100644 --- a/src/commands/init.js +++ b/src/commands/init.js @@ -59,15 +59,14 @@ export async function initCommand(options) { const parsed = parseClaudeMd(claudePath); // Merge parsed data into aisona - if (parsed.personality.tone) aisona.personality.tone = parsed.personality.tone; - if (parsed.personality.style.length) aisona.personality.style = parsed.personality.style; - if (parsed.personality.teaching) aisona.personality.teaching = parsed.personality.teaching; - if (parsed.personality.autonomy) aisona.personality.autonomy = parsed.personality.autonomy; + if (parsed.preferences.tone) aisona.preferences.tone = parsed.preferences.tone; + if (parsed.preferences.habits.length) aisona.preferences.habits = parsed.preferences.habits; + if (parsed.preferences.teaching) aisona.preferences.teaching = parsed.preferences.teaching; + if (parsed.preferences.autonomy) aisona.preferences.autonomy = parsed.preferences.autonomy; if (parsed.rules.length) aisona.rules = parsed.rules; - if (parsed.preferences.length) aisona.preferences = parsed.preferences; if (parsed.memories.length) aisona.memories = parsed.memories; - console.log(chalk.green(` Imported: ${parsed.rules.length} rules, ${parsed.preferences.length} preferences, ${parsed.memories.length} memories\n`)); + console.log(chalk.green(` Imported: ${parsed.rules.length} rules, ${parsed.preferences.habits.length} habits, ${parsed.memories.length} memories\n`)); } } diff --git a/src/lib/config.js b/src/lib/config.js index f61ff83..eed85ec 100644 --- a/src/lib/config.js +++ b/src/lib/config.js @@ -38,26 +38,23 @@ export function getDefaultAisona() { name: '', role: '', experience: '', - context: '', language: 'English', }, - personality: { + preferences: { tone: '', verbosity: 'balanced', - style: [], - teaching: '', autonomy: '', + teaching: '', + habits: [], }, rules: [], - preferences: [], memories: [], tools: { - claude: { enabled: true, extra_rules: [] }, - cursor: { enabled: true, extra_rules: [] }, - gemini: { enabled: false, extra_rules: [] }, - copilot: { enabled: false, extra_rules: [] }, - windsurf: { enabled: false, extra_rules: [] }, - aider: { enabled: false, extra_rules: [] }, + claude: { enabled: true, extra: [] }, + cursor: { enabled: true, extra: [] }, + gemini: { enabled: false, extra: [] }, + copilot: { enabled: false, extra: [] }, + windsurf: { enabled: false, extra: [] }, }, }; } diff --git a/src/lib/parser.js b/src/lib/parser.js index 50258b9..7140562 100644 --- a/src/lib/parser.js +++ b/src/lib/parser.js @@ -20,9 +20,8 @@ export function parseClaudeMd(filePath) { const lines = content.split('\n'); const result = { - personality: { tone: '', style: [], teaching: '', autonomy: '' }, + preferences: { tone: '', habits: [], teaching: '', autonomy: '' }, rules: [], - preferences: [], memories: [], raw_sections: {}, }; @@ -96,25 +95,25 @@ export function parseClaudeMd(filePath) { switch (category) { case 'language': - result.personality.tone = firstProse || fullProse; - result.personality.style.push(...bullets); + result.preferences.tone = firstProse || fullProse; + result.preferences.habits.push(...bullets); break; case 'personality': - if (fullProse) result.personality.tone = fullProse; - result.personality.style.push(...bullets); + if (fullProse) result.preferences.tone = fullProse; + result.preferences.habits.push(...bullets); break; case 'teaching': // For teaching, extract first paragraph as summary, bullets as preferences if (firstProse) { // If we already have teaching, append. Otherwise set. - result.personality.teaching = result.personality.teaching - ? result.personality.teaching + ' ' + firstProse + result.preferences.teaching = result.preferences.teaching + ? result.preferences.teaching + ' ' + firstProse : firstProse; } // Bullets from teaching sections go to preferences (they're style guides) - result.preferences.push(...bullets); + result.preferences.habits.push(...bullets); break; case 'autonomy': { @@ -127,7 +126,7 @@ export function parseClaudeMd(filePath) { !l.match(/^(Always|Never|Only|Do not|Don't)\b/i) ).join(' ').trim(); - if (autonomyProse) result.personality.autonomy = autonomyProse; + if (autonomyProse) result.preferences.autonomy = autonomyProse; result.rules.push(...autonomyRules); result.rules.push(...bullets); break; @@ -142,7 +141,7 @@ export function parseClaudeMd(filePath) { break; case 'preferences': - result.preferences.push(...bullets); + result.preferences.habits.push(...bullets); break; case 'memories': @@ -156,7 +155,7 @@ export function parseClaudeMd(filePath) { default: // Unknown sections — bullets go to preferences, prose to memories if (bullets.length > 0) { - result.preferences.push(...bullets); + result.preferences.habits.push(...bullets); } break; } diff --git a/src/templates/claude.hbs b/src/templates/claude.hbs index 09dace9..950765b 100644 --- a/src/templates/claude.hbs +++ b/src/templates/claude.hbs @@ -1,5 +1,5 @@ {{! Generated by aisona — https://github.com/agents-io/aisona }} -{{! Do not edit directly. Edit aisona.yml and run: aisona export --to claude }} +{{! Source: aisona.yml | Do not edit directly. }} {{#if identity.language}} ## Language @@ -7,26 +7,29 @@ {{identity.language}} {{/if}} -{{#if personality.tone}} +{{#if preferences.tone}} ## Tone and Style -{{personality.tone}} +{{preferences.tone}} +{{#if preferences.verbosity}} +Verbosity: {{preferences.verbosity}} +{{/if}} -{{#each personality.style}} +{{#each preferences.habits}} - {{this}} {{/each}} {{/if}} -{{#if personality.teaching}} +{{#if preferences.teaching}} ## Teaching Style -{{personality.teaching}} +{{preferences.teaching}} {{/if}} -{{#if personality.autonomy}} +{{#if preferences.autonomy}} ## Autonomy -{{personality.autonomy}} +{{preferences.autonomy}} {{/if}} {{#if rules.length}} @@ -37,14 +40,6 @@ {{/each}} {{/if}} -{{#if preferences.length}} -## Preferences - -{{#each preferences}} -- {{this}} -{{/each}} -{{/if}} - {{#if memories.length}} ## Context @@ -53,10 +48,10 @@ {{/each}} {{/if}} -{{#if tools.claude.extra_rules.length}} +{{#if tools.claude.extra.length}} ## Claude-Specific -{{#each tools.claude.extra_rules}} +{{#each tools.claude.extra}} - {{this}} {{/each}} {{/if}} diff --git a/src/templates/copilot.hbs b/src/templates/copilot.hbs index 2e4276c..d2d98bd 100644 --- a/src/templates/copilot.hbs +++ b/src/templates/copilot.hbs @@ -1,14 +1,14 @@ {{! Generated by aisona — https://github.com/agents-io/aisona }} -{{! Do not edit directly. Edit aisona.yml and run: aisona export --to copilot }} +{{! Source: aisona.yml | Do not edit directly. }} {{#if identity.language}} Respond in {{identity.language}}. {{/if}} -{{#if personality.tone}} +{{#if preferences.tone}} ## Style -{{personality.tone}} -{{#each personality.style}} +{{preferences.tone}} +{{#each preferences.habits}} - {{this}} {{/each}} {{/if}} @@ -20,16 +20,9 @@ Respond in {{identity.language}}. {{/each}} {{/if}} -{{#if preferences.length}} -## Preferences -{{#each preferences}} -- {{this}} -{{/each}} -{{/if}} - -{{#if tools.copilot.extra_rules.length}} +{{#if tools.copilot.extra.length}} ## Copilot-Specific -{{#each tools.copilot.extra_rules}} +{{#each tools.copilot.extra}} - {{this}} {{/each}} {{/if}} diff --git a/src/templates/cursor.hbs b/src/templates/cursor.hbs index d9a09b2..824c92f 100644 --- a/src/templates/cursor.hbs +++ b/src/templates/cursor.hbs @@ -1,18 +1,18 @@ {{! Generated by aisona — https://github.com/agents-io/aisona }} -{{! Do not edit directly. Edit aisona.yml and run: aisona export --to cursor }} +{{! Source: aisona.yml | Do not edit directly. }} {{#if identity.role}} -# Role: {{identity.role}} +# {{identity.role}} {{/if}} {{#if identity.language}} Always respond in {{identity.language}}. {{/if}} -{{#if personality.tone}} -## Communication -{{personality.tone}} -{{#each personality.style}} +{{#if preferences.tone}} +## Style +{{preferences.tone}} +{{#each preferences.habits}} - {{this}} {{/each}} {{/if}} @@ -24,13 +24,6 @@ Always respond in {{identity.language}}. {{/each}} {{/if}} -{{#if preferences.length}} -## Preferences -{{#each preferences}} -- {{this}} -{{/each}} -{{/if}} - {{#if memories.length}} ## Context {{#each memories}} @@ -38,9 +31,9 @@ Always respond in {{identity.language}}. {{/each}} {{/if}} -{{#if tools.cursor.extra_rules.length}} +{{#if tools.cursor.extra.length}} ## Cursor-Specific -{{#each tools.cursor.extra_rules}} +{{#each tools.cursor.extra}} - {{this}} {{/each}} {{/if}} diff --git a/src/templates/gemini.hbs b/src/templates/gemini.hbs index af0f983..de53dc9 100644 --- a/src/templates/gemini.hbs +++ b/src/templates/gemini.hbs @@ -1,58 +1,40 @@ {{! Generated by aisona — https://github.com/agents-io/aisona }} -{{! Do not edit directly. Edit aisona.yml and run: aisona export --to gemini }} +{{! Source: aisona.yml | Do not edit directly. }} {{#if identity.language}} -## Language - Respond in {{identity.language}}. {{/if}} {{#if identity.role}} ## About the User - -Role: {{identity.role}} -{{#if identity.experience}}Experience: {{identity.experience}}{{/if}} -{{#if identity.context}}Context: {{identity.context}}{{/if}} +{{identity.role}}{{#if identity.experience}} ({{identity.experience}}){{/if}} {{/if}} -{{#if personality.tone}} -## Communication Style - -{{personality.tone}} - -{{#each personality.style}} +{{#if preferences.tone}} +## Communication +{{preferences.tone}} +{{#each preferences.habits}} - {{this}} {{/each}} {{/if}} {{#if rules.length}} ## Rules - {{#each rules}} - {{this}} {{/each}} {{/if}} -{{#if preferences.length}} -## Preferences - -{{#each preferences}} -- {{this}} -{{/each}} -{{/if}} - {{#if memories.length}} ## Context - {{#each memories}} - {{this}} {{/each}} {{/if}} -{{#if tools.gemini.extra_rules.length}} +{{#if tools.gemini.extra.length}} ## Gemini-Specific - -{{#each tools.gemini.extra_rules}} +{{#each tools.gemini.extra}} - {{this}} {{/each}} {{/if}} diff --git a/test-output/.cursorrules b/test-output/.cursorrules new file mode 100644 index 0000000..eed1b49 --- /dev/null +++ b/test-output/.cursorrules @@ -0,0 +1,28 @@ +# Role: Backend engineer + +Always respond in Cantonese (廣東話). + +## Communication +Always reply in Cantonese (廣東話) unless the user writes in another language or explicitly asks for English. + +## Rules +- Destructive actions: deleting files, dropping DB tables, `reset --hard`, force push to shared branches +- Actions visible to others: pushing to remote, opening/closing PRs, sending messages + +## Preferences +- Show where data comes from and where it goes +- Name the actual tables, functions, fields involved +- If there's a queue/worker, explain who creates the job and who picks it up +- If there's branching, explain each branch +- Do not skip the hidden middle steps +- Do not jump from input to output too quickly +- Do not say "this basically handles it" without explaining how +- **API change** → request/response flow, schema validation, where logic lives, how layers connect +- **DB / migration / schema** → why the column/table/enum matters, what breaks without it, how it propagates to the API layer +- **Bug fix** → what category of bug it was, why it happened, what general debugging lesson applies +- **Refactor** → what design problem existed, what principle the new structure improves +- **Cross-repo / multi-service work** → why a change in one repo requires a matching change in another, how data flows between services +- Short and clear — 3–8 sentences or a tight bullet list is usually enough +- Never skip teaching just because the task felt simple +- Do not re-explain things the user has clearly already understood +- One reusable concept or pattern per task is the minimum diff --git a/test-output/.github/copilot-instructions.md b/test-output/.github/copilot-instructions.md new file mode 100644 index 0000000..4f01798 --- /dev/null +++ b/test-output/.github/copilot-instructions.md @@ -0,0 +1,26 @@ +Respond in Cantonese (廣東話). + +## Style +Always reply in Cantonese (廣東話) unless the user writes in another language or explicitly asks for English. + +## Rules +- Destructive actions: deleting files, dropping DB tables, `reset --hard`, force push to shared branches +- Actions visible to others: pushing to remote, opening/closing PRs, sending messages + +## Preferences +- Show where data comes from and where it goes +- Name the actual tables, functions, fields involved +- If there's a queue/worker, explain who creates the job and who picks it up +- If there's branching, explain each branch +- Do not skip the hidden middle steps +- Do not jump from input to output too quickly +- Do not say "this basically handles it" without explaining how +- **API change** → request/response flow, schema validation, where logic lives, how layers connect +- **DB / migration / schema** → why the column/table/enum matters, what breaks without it, how it propagates to the API layer +- **Bug fix** → what category of bug it was, why it happened, what general debugging lesson applies +- **Refactor** → what design problem existed, what principle the new structure improves +- **Cross-repo / multi-service work** → why a change in one repo requires a matching change in another, how data flows between services +- Short and clear — 3–8 sentences or a tight bullet list is usually enough +- Never skip teaching just because the task felt simple +- Do not re-explain things the user has clearly already understood +- One reusable concept or pattern per task is the minimum diff --git a/test-output/CLAUDE.md b/test-output/CLAUDE.md new file mode 100644 index 0000000..b8ff8f7 --- /dev/null +++ b/test-output/CLAUDE.md @@ -0,0 +1,44 @@ +## Language + +Cantonese (廣東話) + +## Tone and Style + +Always reply in Cantonese (廣東話) unless the user writes in another language or explicitly asks for English. + +## Teaching Style + +The user is a junior developer (~11 months experience) working in the AI era. Because AI makes implementation fast, there is a real risk of "done but nothing learned". The goal is to make sure every task also builds real engineering knowledge. Do not assume "done" means "learned". Completing the task is not enough — help the user understand what this task is an example of, so they could reason about similar problems themselves next time without AI. **During the task** — if a step involves a concept worth noting, mention it inline. For example, if touching a Pydantic schema, briefly note what role schemas play. If writing a migration, briefly note why it exists. Keep these inline comments short (1–2 sentences), not a full lesson. **After the task** — end every completed task with a learning section. Even simple tasks have teachable patterns. Never skip this. After finishing, cover these points (not all need to be long — tailor depth to complexity): 1. **What changed** — which files, which layers, what was added or modified 2. **What problem it solves** — why this change was needed 3. **How the flow works step by step** — trace data from input to output through each layer 4. **Why this implementation** — why this approach instead of alternatives 5. **The concept or pattern** — what engineering idea this task is an example of 6. **Tradeoffs / alternatives** — what the downside of this approach is, or what another option would have been 7. **What to remember next time** — one reusable mental model or rule of thumb For different task types, emphasise: Talk like a helpful senior engineer — direct, concrete, practical. Not a textbook. Not overly formal. Use analogies when the concept is abstract. The goal is reusable mental models, not just a task report. Frame the lesson as: "If you had to do this yourself without AI, here is what you would need to understand." --- + +## Autonomy + +Work autonomously. Only ask for confirmation before: Always ask before: committing, pushing to remote, opening/closing PRs, and any destructive CLI commands (rm, rmdir, kill, pkill, truncate, dd, mkfs, chmod -R, chown -R, or anything that permanently deletes/overwrites data). Never ask before: reading files, searching, grep, find, ls, cat, editing files, running tests, installing packages. Just do it and report what you did. + +## Rules + +- Destructive actions: deleting files, dropping DB tables, `reset --hard`, force push to shared branches +- Actions visible to others: pushing to remote, opening/closing PRs, sending messages + +## Preferences + +- Show where data comes from and where it goes +- Name the actual tables, functions, fields involved +- If there's a queue/worker, explain who creates the job and who picks it up +- If there's branching, explain each branch +- Do not skip the hidden middle steps +- Do not jump from input to output too quickly +- Do not say "this basically handles it" without explaining how +- **API change** → request/response flow, schema validation, where logic lives, how layers connect +- **DB / migration / schema** → why the column/table/enum matters, what breaks without it, how it propagates to the API layer +- **Bug fix** → what category of bug it was, why it happened, what general debugging lesson applies +- **Refactor** → what design problem existed, what principle the new structure improves +- **Cross-repo / multi-service work** → why a change in one repo requires a matching change in another, how data flows between services +- Short and clear — 3–8 sentences or a tight bullet list is usually enough +- Never skip teaching just because the task felt simple +- Do not re-explain things the user has clearly already understood +- One reusable concept or pattern per task is the minimum + +## Claude-Specific + +- End every task with a learning section +- Explain like stepping through a debugger diff --git a/test-output/GEMINI.md b/test-output/GEMINI.md new file mode 100644 index 0000000..51565c5 --- /dev/null +++ b/test-output/GEMINI.md @@ -0,0 +1,37 @@ +## Language + +Respond in Cantonese (廣東話). + +## About the User + +Role: Backend engineer +Experience: 11 months +Context: AI security control plane, side projects @agents-io + +## Communication Style + +Always reply in Cantonese (廣東話) unless the user writes in another language or explicitly asks for English. + +## Rules + +- Destructive actions: deleting files, dropping DB tables, `reset --hard`, force push to shared branches +- Actions visible to others: pushing to remote, opening/closing PRs, sending messages + +## Preferences + +- Show where data comes from and where it goes +- Name the actual tables, functions, fields involved +- If there's a queue/worker, explain who creates the job and who picks it up +- If there's branching, explain each branch +- Do not skip the hidden middle steps +- Do not jump from input to output too quickly +- Do not say "this basically handles it" without explaining how +- **API change** → request/response flow, schema validation, where logic lives, how layers connect +- **DB / migration / schema** → why the column/table/enum matters, what breaks without it, how it propagates to the API layer +- **Bug fix** → what category of bug it was, why it happened, what general debugging lesson applies +- **Refactor** → what design problem existed, what principle the new structure improves +- **Cross-repo / multi-service work** → why a change in one repo requires a matching change in another, how data flows between services +- Short and clear — 3–8 sentences or a tight bullet list is usually enough +- Never skip teaching just because the task felt simple +- Do not re-explain things the user has clearly already understood +- One reusable concept or pattern per task is the minimum diff --git a/test-output/aisona.yml b/test-output/aisona.yml new file mode 100644 index 0000000..f514861 --- /dev/null +++ b/test-output/aisona.yml @@ -0,0 +1,80 @@ +version: 1 +identity: + name: Nicole + role: Backend engineer + experience: 11 months + language: Cantonese (廣東話) + context: AI security control plane, side projects @agents-io +personality: + tone: Always reply in Cantonese (廣東話) unless the user writes in another language or explicitly asks for English. + style: [] + teaching: >- + The user is a junior developer (~11 months experience) working in the AI era. Because AI makes implementation fast, + there is a real risk of "done but nothing learned". The goal is to make sure every task also builds real engineering + knowledge. Do not assume "done" means "learned". Completing the task is not enough — help the user understand what + this task is an example of, so they could reason about similar problems themselves next time without AI. **During + the task** — if a step involves a concept worth noting, mention it inline. For example, if touching a Pydantic + schema, briefly note what role schemas play. If writing a migration, briefly note why it exists. Keep these inline + comments short (1–2 sentences), not a full lesson. **After the task** — end every completed task with a learning + section. Even simple tasks have teachable patterns. Never skip this. After finishing, cover these points (not all + need to be long — tailor depth to complexity): 1. **What changed** — which files, which layers, what was added or + modified 2. **What problem it solves** — why this change was needed 3. **How the flow works step by step** — trace + data from input to output through each layer 4. **Why this implementation** — why this approach instead of + alternatives 5. **The concept or pattern** — what engineering idea this task is an example of 6. **Tradeoffs / + alternatives** — what the downside of this approach is, or what another option would have been 7. **What to remember + next time** — one reusable mental model or rule of thumb For different task types, emphasise: Talk like a helpful + senior engineer — direct, concrete, practical. Not a textbook. Not overly formal. Use analogies when the concept is + abstract. The goal is reusable mental models, not just a task report. Frame the lesson as: "If you had to do this + yourself without AI, here is what you would need to understand." --- + autonomy: >- + Work autonomously. Only ask for confirmation before: Always ask before: committing, pushing to remote, + opening/closing PRs, and any destructive CLI commands (rm, rmdir, kill, pkill, truncate, dd, mkfs, chmod -R, chown + -R, or anything that permanently deletes/overwrites data). Never ask before: reading files, searching, grep, find, + ls, cat, editing files, running tests, installing packages. Just do it and report what you did. + verbosity: concise +rules: + - "Destructive actions: deleting files, dropping DB tables, `reset --hard`, force push to shared branches" + - "Actions visible to others: pushing to remote, opening/closing PRs, sending messages" +preferences: + - Show where data comes from and where it goes + - Name the actual tables, functions, fields involved + - If there's a queue/worker, explain who creates the job and who picks it up + - If there's branching, explain each branch + - Do not skip the hidden middle steps + - Do not jump from input to output too quickly + - Do not say "this basically handles it" without explaining how + - "**API change** → request/response flow, schema validation, where logic lives, how layers connect" + - >- + **DB / migration / schema** → why the column/table/enum matters, what breaks without it, how it propagates to the + API layer + - "**Bug fix** → what category of bug it was, why it happened, what general debugging lesson applies" + - "**Refactor** → what design problem existed, what principle the new structure improves" + - >- + **Cross-repo / multi-service work** → why a change in one repo requires a matching change in another, how data flows + between services + - Short and clear — 3–8 sentences or a tight bullet list is usually enough + - Never skip teaching just because the task felt simple + - Do not re-explain things the user has clearly already understood + - One reusable concept or pattern per task is the minimum +memories: [] +tools: + claude: + enabled: true + extra_rules: + - End every task with a learning section + - Explain like stepping through a debugger + cursor: + enabled: true + extra_rules: [] + gemini: + enabled: true + extra_rules: [] + copilot: + enabled: true + extra_rules: [] + windsurf: + enabled: false + extra_rules: [] + aider: + enabled: false + extra_rules: [] diff --git a/tests/config.test.js b/tests/config.test.js index 313f46d..eaef6a9 100644 --- a/tests/config.test.js +++ b/tests/config.test.js @@ -15,10 +15,10 @@ describe('getDefaultAisona', () => { expect(def.version).toBe(1); expect(def.identity).toBeDefined(); expect(def.identity.name).toBe(''); - expect(def.personality).toBeDefined(); - expect(def.personality.tone).toBe(''); + expect(def.preferences).toBeDefined(); + expect(def.preferences.tone).toBe(''); + expect(def.preferences.habits).toEqual([]); expect(def.rules).toEqual([]); - expect(def.preferences).toEqual([]); expect(def.memories).toEqual([]); expect(def.tools.claude.enabled).toBe(true); expect(def.tools.cursor.enabled).toBe(true); @@ -32,7 +32,7 @@ describe('saveAisona + loadAisona roundtrip', () => { data.identity.name = 'Test User'; data.identity.language = 'Cantonese'; data.rules = ['Never commit without asking', 'Use feature branches']; - data.personality.tone = 'Direct and concise'; + data.preferences.tone = 'Direct and concise'; data.memories = ['User prefers short responses']; const filePath = path.join(TMP, 'aisona.yml'); @@ -44,20 +44,20 @@ describe('saveAisona + loadAisona roundtrip', () => { expect(loaded.identity.name).toBe('Test User'); expect(loaded.identity.language).toBe('Cantonese'); expect(loaded.rules).toEqual(['Never commit without asking', 'Use feature branches']); - expect(loaded.personality.tone).toBe('Direct and concise'); + expect(loaded.preferences.tone).toBe('Direct and concise'); expect(loaded.memories).toEqual(['User prefers short responses']); }); it('preserves tool config through roundtrip', () => { const data = getDefaultAisona(); - data.tools.claude.extra_rules = ['Explain like a debugger']; + data.tools.claude.extra = ['Explain like a debugger']; data.tools.cursor.enabled = false; const filePath = path.join(TMP, 'aisona.yml'); saveAisona(filePath, data); const loaded = loadAisona(filePath); - expect(loaded.tools.claude.extra_rules).toEqual(['Explain like a debugger']); + expect(loaded.tools.claude.extra).toEqual(['Explain like a debugger']); expect(loaded.tools.cursor.enabled).toBe(false); }); }); diff --git a/tests/exporter.test.js b/tests/exporter.test.js index d1d9594..103d463 100644 --- a/tests/exporter.test.js +++ b/tests/exporter.test.js @@ -9,36 +9,34 @@ const TMP = path.join(os.tmpdir(), 'aisona-test-export-' + Date.now()); beforeEach(() => fs.mkdirSync(TMP, { recursive: true })); afterEach(() => fs.rmSync(TMP, { recursive: true, force: true })); -const SAMPLE_AISONA = { +const SAMPLE = { version: 1, identity: { name: 'TestUser', role: 'Backend engineer', experience: '11 months', - context: 'AI security', language: 'Cantonese (廣東話)', }, - personality: { + preferences: { tone: 'Direct and concise', verbosity: 'concise', - style: ['No emojis', 'Short sentences'], - teaching: 'Explain like a senior engineer', autonomy: 'Work autonomously', + teaching: 'Explain like a senior engineer', + habits: ['No emojis', 'Short sentences'], }, rules: ['Never commit without asking', 'Use feature branches'], - preferences: ['Use Playwright for testing'], memories: ['User prefers Cantonese'], tools: { - claude: { enabled: true, extra_rules: ['End tasks with learning section'] }, - cursor: { enabled: true, extra_rules: ['Use .mdc format'] }, - gemini: { enabled: true, extra_rules: [] }, - copilot: { enabled: true, extra_rules: [] }, - windsurf: { enabled: false, extra_rules: [] }, + claude: { enabled: true, extra: ['End tasks with learning section'] }, + cursor: { enabled: true, extra: ['Use .mdc format'] }, + gemini: { enabled: true, extra: [] }, + copilot: { enabled: true, extra: [] }, + windsurf: { enabled: false, extra: [] }, }, }; describe('getSupportedTools', () => { - it('returns list of supported tool ids', () => { + it('returns supported tool ids', () => { const tools = getSupportedTools(); expect(tools).toContain('claude'); expect(tools).toContain('cursor'); @@ -49,181 +47,117 @@ describe('getSupportedTools', () => { }); describe('exportToTool — Claude', () => { - it('generates valid CLAUDE.md', () => { - const result = exportToTool(SAMPLE_AISONA, 'claude', TMP); - + it('generates valid CLAUDE.md with all sections', () => { + const result = exportToTool(SAMPLE, 'claude', TMP); expect(result.success).toBe(true); - expect(result.tool).toBe('claude'); const content = fs.readFileSync(result.path, 'utf8'); expect(content).toContain('Cantonese (廣東話)'); expect(content).toContain('Direct and concise'); expect(content).toContain('No emojis'); expect(content).toContain('Never commit without asking'); - expect(content).toContain('Use Playwright for testing'); expect(content).toContain('User prefers Cantonese'); expect(content).toContain('End tasks with learning section'); - }); - - it('includes all sections with correct headers', () => { - const result = exportToTool(SAMPLE_AISONA, 'claude', TMP); - const content = fs.readFileSync(result.path, 'utf8'); - expect(content).toContain('## Language'); - expect(content).toContain('## Tone and Style'); - expect(content).toContain('## Teaching Style'); - expect(content).toContain('## Autonomy'); expect(content).toContain('## Rules'); - expect(content).toContain('## Preferences'); - expect(content).toContain('## Context'); expect(content).toContain('## Claude-Specific'); }); it('writes to CLAUDE.md', () => { - exportToTool(SAMPLE_AISONA, 'claude', TMP); + exportToTool(SAMPLE, 'claude', TMP); expect(fs.existsSync(path.join(TMP, 'CLAUDE.md'))).toBe(true); }); }); describe('exportToTool — Cursor', () => { it('generates valid .cursorrules', () => { - const result = exportToTool(SAMPLE_AISONA, 'cursor', TMP); - + const result = exportToTool(SAMPLE, 'cursor', TMP); expect(result.success).toBe(true); - const content = fs.readFileSync(result.path, 'utf8'); + const content = fs.readFileSync(result.path, 'utf8'); expect(content).toContain('Backend engineer'); expect(content).toContain('Cantonese'); expect(content).toContain('Never commit without asking'); expect(content).toContain('Use .mdc format'); }); - - it('writes to .cursorrules', () => { - exportToTool(SAMPLE_AISONA, 'cursor', TMP); - expect(fs.existsSync(path.join(TMP, '.cursorrules'))).toBe(true); - }); }); describe('exportToTool — Gemini', () => { it('generates valid GEMINI.md', () => { - const result = exportToTool(SAMPLE_AISONA, 'gemini', TMP); - + const result = exportToTool(SAMPLE, 'gemini', TMP); expect(result.success).toBe(true); - const content = fs.readFileSync(result.path, 'utf8'); + const content = fs.readFileSync(result.path, 'utf8'); expect(content).toContain('Cantonese'); expect(content).toContain('Backend engineer'); expect(content).toContain('11 months'); - expect(content).toContain('Never commit without asking'); - }); - - it('writes to GEMINI.md', () => { - exportToTool(SAMPLE_AISONA, 'gemini', TMP); - expect(fs.existsSync(path.join(TMP, 'GEMINI.md'))).toBe(true); }); }); describe('exportToTool — Copilot', () => { - it('generates valid copilot-instructions.md', () => { - const result = exportToTool(SAMPLE_AISONA, 'copilot', TMP); - + it('generates copilot-instructions.md', () => { + const result = exportToTool(SAMPLE, 'copilot', TMP); expect(result.success).toBe(true); - const content = fs.readFileSync(result.path, 'utf8'); - - expect(content).toContain('Cantonese'); - expect(content).toContain('Never commit without asking'); - }); - - it('writes to .github/copilot-instructions.md', () => { - exportToTool(SAMPLE_AISONA, 'copilot', TMP); expect(fs.existsSync(path.join(TMP, '.github', 'copilot-instructions.md'))).toBe(true); }); }); -describe('exportToTool — disabled tool', () => { - it('refuses to export to disabled tool', () => { - const result = exportToTool(SAMPLE_AISONA, 'windsurf', TMP); +describe('exportToTool — disabled', () => { + it('refuses to export disabled tool', () => { + const result = exportToTool(SAMPLE, 'windsurf', TMP); expect(result.success).toBe(false); expect(result.error).toContain('disabled'); }); }); -describe('exportToTool — unknown tool', () => { - it('fails for unknown tool', () => { - const result = exportToTool(SAMPLE_AISONA, 'nonexistent', TMP); - expect(result.success).toBe(false); - }); -}); - describe('exportToAll', () => { it('exports to all enabled tools', () => { - const results = exportToAll(SAMPLE_AISONA, TMP); - + const results = exportToAll(SAMPLE, TMP); const succeeded = results.filter(r => r.success); - expect(succeeded.length).toBe(4); // claude, cursor, gemini, copilot (windsurf disabled) + expect(succeeded.length).toBe(4); expect(fs.existsSync(path.join(TMP, 'CLAUDE.md'))).toBe(true); expect(fs.existsSync(path.join(TMP, '.cursorrules'))).toBe(true); expect(fs.existsSync(path.join(TMP, 'GEMINI.md'))).toBe(true); expect(fs.existsSync(path.join(TMP, '.github', 'copilot-instructions.md'))).toBe(true); - // windsurf disabled — should NOT exist expect(fs.existsSync(path.join(TMP, '.windsurfrules'))).toBe(false); }); - - it('skips tools with no output config', () => { - const aisona = { - ...SAMPLE_AISONA, - tools: { claude: { enabled: true, extra_rules: [] } }, - }; - const results = exportToAll(aisona, TMP); - expect(results.length).toBe(1); - expect(results[0].tool).toBe('claude'); - }); }); -describe('export output quality', () => { - it('does not have triple+ blank lines', () => { - const result = exportToTool(SAMPLE_AISONA, 'claude', TMP); +describe('output quality', () => { + it('no triple blank lines', () => { + const result = exportToTool(SAMPLE, 'claude', TMP); const content = fs.readFileSync(result.path, 'utf8'); expect(content).not.toMatch(/\n{4,}/); }); it('ends with single newline', () => { - const result = exportToTool(SAMPLE_AISONA, 'claude', TMP); + const result = exportToTool(SAMPLE, 'claude', TMP); const content = fs.readFileSync(result.path, 'utf8'); expect(content.endsWith('\n')).toBe(true); expect(content.endsWith('\n\n')).toBe(false); }); - it('contains aisona attribution comment', () => { - // Templates should have the aisona attribution but it's in Handlebars comments - // which don't render. Check the generated output is clean. - const result = exportToTool(SAMPLE_AISONA, 'claude', TMP); + it('no raw handlebars in output', () => { + const result = exportToTool(SAMPLE, 'claude', TMP); const content = fs.readFileSync(result.path, 'utf8'); - // Should NOT contain raw handlebars expect(content).not.toContain('{{'); - expect(content).not.toContain('}}'); }); it('handles empty arrays gracefully', () => { const minimal = { version: 1, identity: { name: 'Test', language: 'English' }, - personality: { tone: 'Friendly' }, + preferences: { tone: 'Friendly', habits: [] }, rules: [], - preferences: [], memories: [], - tools: { claude: { enabled: true, extra_rules: [] } }, + tools: { claude: { enabled: true, extra: [] } }, }; const result = exportToTool(minimal, 'claude', TMP); expect(result.success).toBe(true); const content = fs.readFileSync(result.path, 'utf8'); - - // Should have language and tone but no empty sections expect(content).toContain('English'); expect(content).toContain('Friendly'); - // Should NOT have "## Rules" with nothing under it - expect(content).not.toContain('## Rules\n\n##'); }); }); diff --git a/tests/parser.test.js b/tests/parser.test.js index ea738d5..dade82a 100644 --- a/tests/parser.test.js +++ b/tests/parser.test.js @@ -10,194 +10,101 @@ beforeEach(() => fs.mkdirSync(TMP, { recursive: true })); afterEach(() => fs.rmSync(TMP, { recursive: true, force: true })); describe('parseClaudeMd', () => { - it('parses a simple CLAUDE.md with rules and preferences', () => { - const content = `## Language + it('parses rules and habits from simple CLAUDE.md', () => { + const content = `## Language\n\nAlways reply in Cantonese.\n\n## Rules\n\n- Never commit without asking\n- Never push to remote\n\n## Preferences\n\n- Use Playwright for testing\n- Short responses\n`; + fs.writeFileSync(path.join(TMP, 'CLAUDE.md'), content); -Always reply in Cantonese. - -## Rules - -- Never commit without asking -- Never push to remote without asking - -## Preferences - -- Use Playwright for testing -- Short responses preferred -`; - const filePath = path.join(TMP, 'CLAUDE.md'); - fs.writeFileSync(filePath, content); - - const result = parseClaudeMd(filePath); + const result = parseClaudeMd(path.join(TMP, 'CLAUDE.md')); expect(result.rules).toContain('Never commit without asking'); - expect(result.rules).toContain('Never push to remote without asking'); - expect(result.preferences).toContain('Use Playwright for testing'); + expect(result.rules).toContain('Never push to remote'); + expect(result.preferences.habits).toContain('Use Playwright for testing'); }); - it('extracts personality from tone/style sections', () => { - const content = `## Tone and Style + it('extracts tone from personality sections', () => { + const content = `## Tone and Style\n\nBe concise and direct.\n\n- No emojis unless asked\n- Short sentences\n`; + fs.writeFileSync(path.join(TMP, 'CLAUDE.md'), content); -Be concise and direct. - -- No emojis unless asked -- Short sentences -`; - const filePath = path.join(TMP, 'CLAUDE.md'); - fs.writeFileSync(filePath, content); - - const result = parseClaudeMd(filePath); - expect(result.personality.tone).toContain('concise and direct'); - expect(result.personality.style).toContain('No emojis unless asked'); + const result = parseClaudeMd(path.join(TMP, 'CLAUDE.md')); + expect(result.preferences.tone).toContain('concise and direct'); + expect(result.preferences.habits).toContain('No emojis unless asked'); }); - it('extracts autonomy from autonomy section', () => { - const content = `## Autonomy - -Work autonomously. Only ask before destructive actions. + it('extracts autonomy rules', () => { + const content = `## Autonomy\n\nWork autonomously.\n\nAlways ask before git push.\nNever ask before reading files.\n\n- Destructive actions need confirmation\n`; + fs.writeFileSync(path.join(TMP, 'CLAUDE.md'), content); -- Never ask before reading files -- Always ask before git push -`; - const filePath = path.join(TMP, 'CLAUDE.md'); - fs.writeFileSync(filePath, content); - - const result = parseClaudeMd(filePath); - expect(result.personality.autonomy).toContain('Work autonomously'); - expect(result.rules).toContain('Never ask before reading files'); + const result = parseClaudeMd(path.join(TMP, 'CLAUDE.md')); + expect(result.preferences.autonomy).toContain('Work autonomously'); + expect(result.rules).toContain('Always ask before git push.'); + expect(result.rules).toContain('Never ask before reading files.'); + expect(result.rules).toContain('Destructive actions need confirmation'); }); - it('extracts teaching style', () => { - const content = `## Teaching Style - -Explain like a senior engineer. Always include a learning section after tasks. + it('extracts teaching preferences', () => { + const content = `## Teaching Style\n\nThe user is a junior developer.\n\n- Frame lessons as reusable mental models\n- Never skip teaching\n`; + fs.writeFileSync(path.join(TMP, 'CLAUDE.md'), content); -- Frame lessons as reusable mental models -- Never skip teaching even for simple tasks -`; - const filePath = path.join(TMP, 'CLAUDE.md'); - fs.writeFileSync(filePath, content); - - const result = parseClaudeMd(filePath); - expect(result.personality.teaching).toContain('senior engineer'); - expect(result.preferences).toContain('Frame lessons as reusable mental models'); + const result = parseClaudeMd(path.join(TMP, 'CLAUDE.md')); + expect(result.preferences.teaching).toContain('junior developer'); + expect(result.preferences.habits).toContain('Frame lessons as reusable mental models'); }); - it('handles Nicole real-world CLAUDE.md structure', () => { - // Simulates the structure of the actual ~/.claude/CLAUDE.md - const content = `## Language - -Always reply in Cantonese (廣東話) unless the user writes in another language or explicitly asks for English. - -## Technical Explanations - -When explaining code, explain it so the user can clearly picture what the computer is doing. - -- Show where data comes from and where it goes -- Name the actual tables, functions, fields involved -- Do not skip the hidden middle steps - -## Teaching Style - -The user is a junior developer (~11 months experience). - -- End every completed task with a learning section -- Talk like a helpful senior engineer - -## Autonomy - -Work autonomously. Only ask for confirmation before: -- Destructive actions: deleting files, dropping DB tables -- Actions visible to others: pushing to remote, opening PRs - -## Project Knowledge Capture - -When working on projects, if you discover something reusable, mention it. - -- Check project docs before suggesting changes -`; - const filePath = path.join(TMP, 'CLAUDE.md'); - fs.writeFileSync(filePath, content); - - const result = parseClaudeMd(filePath); - - // Should extract rules from autonomy - expect(result.rules.length).toBeGreaterThan(0); - - // Should extract teaching - expect(result.personality.teaching).toContain('junior developer'); + it('handles empty CLAUDE.md', () => { + fs.writeFileSync(path.join(TMP, 'CLAUDE.md'), ''); - // Should extract style from technical explanations - expect(result.preferences.some(p => p.includes('data comes from'))).toBe(true); + const result = parseClaudeMd(path.join(TMP, 'CLAUDE.md')); + expect(result.rules).toEqual([]); + expect(result.preferences.habits).toEqual([]); }); - it('handles empty CLAUDE.md', () => { - const filePath = path.join(TMP, 'CLAUDE.md'); - fs.writeFileSync(filePath, ''); + it('handles Nicole real-world structure', () => { + const content = `## Language\n\nAlways reply in Cantonese.\n\n## Technical Explanations\n\nExplain step by step.\n\n- Show where data comes from\n- Name actual tables\n\n## Teaching Style\n\nThe user is a junior developer.\n\n- End tasks with learning section\n\n## Autonomy\n\nWork autonomously.\n\nAlways ask before committing.\nNever ask before reading files.\n\n- Destructive actions: deleting files\n- Actions visible to others: pushing\n\n## Project Knowledge Capture\n\n- Flag reusable discoveries\n`; + fs.writeFileSync(path.join(TMP, 'CLAUDE.md'), content); - const result = parseClaudeMd(filePath); - expect(result.rules).toEqual([]); - expect(result.preferences).toEqual([]); + const result = parseClaudeMd(path.join(TMP, 'CLAUDE.md')); + expect(result.rules.length).toBeGreaterThanOrEqual(4); + expect(result.preferences.teaching).toContain('junior developer'); + expect(result.preferences.habits.some(h => h.includes('data comes from'))).toBe(true); + expect(result.memories.length).toBeGreaterThanOrEqual(1); }); }); describe('parseCursorRules', () => { it('extracts rules from .cursorrules', () => { - const content = `# Coding Rules + const content = `# Rules\n\n- Always use TypeScript\n- Prefer functional style\n\nUse concise names.\n`; + fs.writeFileSync(path.join(TMP, '.cursorrules'), content); -- Always use TypeScript -- Prefer functional style -- No classes unless necessary - -Use concise variable names. -`; - const filePath = path.join(TMP, '.cursorrules'); - fs.writeFileSync(filePath, content); - - const result = parseCursorRules(filePath); + const result = parseCursorRules(path.join(TMP, '.cursorrules')); expect(result.rules).toContain('Always use TypeScript'); - expect(result.rules).toContain('Prefer functional style'); - expect(result.context).toContain('concise variable names'); + expect(result.context).toContain('concise names'); }); }); describe('detectTools', () => { it('detects CLAUDE.md in project directory', () => { fs.writeFileSync(path.join(TMP, 'CLAUDE.md'), '# Test'); - const detected = detectTools(TMP); - const claude = detected.find(d => d.tool === 'claude' && d.scope === 'project'); - expect(claude).toBeDefined(); + expect(detected.find(d => d.tool === 'claude' && d.scope === 'project')).toBeDefined(); }); it('detects .cursorrules', () => { fs.writeFileSync(path.join(TMP, '.cursorrules'), '# Test'); - - const detected = detectTools(TMP); - const cursor = detected.find(d => d.tool === 'cursor'); - expect(cursor).toBeDefined(); + expect(detectTools(TMP).find(d => d.tool === 'cursor')).toBeDefined(); }); it('detects copilot instructions', () => { fs.mkdirSync(path.join(TMP, '.github'), { recursive: true }); fs.writeFileSync(path.join(TMP, '.github', 'copilot-instructions.md'), '# Test'); - - const detected = detectTools(TMP); - const copilot = detected.find(d => d.tool === 'copilot'); - expect(copilot).toBeDefined(); + expect(detectTools(TMP).find(d => d.tool === 'copilot')).toBeDefined(); }); it('detects AGENTS.md', () => { fs.writeFileSync(path.join(TMP, 'AGENTS.md'), '# Test'); - - const detected = detectTools(TMP); - const agents = detected.find(d => d.tool === 'agentsmd'); - expect(agents).toBeDefined(); + expect(detectTools(TMP).find(d => d.tool === 'agentsmd')).toBeDefined(); }); - it('returns empty array for empty directory', () => { - const detected = detectTools(TMP); - // May detect global configs from ~/.claude etc, but no project-level - const projectLevel = detected.filter(d => d.scope === 'project'); + it('returns empty for empty directory (project-level)', () => { + const projectLevel = detectTools(TMP).filter(d => d.scope === 'project'); expect(projectLevel).toEqual([]); }); });