From 61171004f7f337003a5d3cc362e1cecd79b828a5 Mon Sep 17 00:00:00 2001 From: 00PrabalK00 Date: Wed, 3 Jun 2026 22:31:23 +0700 Subject: [PATCH] add ink shell frontend --- .gitignore | 1 + README.md | 6 + bin/continuum.js | 19 ++ package-lock.json | 635 +++++++++++++++++++++++++++++++++++++++ package.json | 17 ++ src/cli.tsx | 186 ++++++++++++ src/continuum-runtime.ts | 214 +++++++++++++ tsconfig.json | 15 + 8 files changed, 1093 insertions(+) create mode 100644 package-lock.json create mode 100644 src/cli.tsx create mode 100644 src/continuum-runtime.ts create mode 100644 tsconfig.json diff --git a/.gitignore b/.gitignore index c601fa5..a92e580 100644 --- a/.gitignore +++ b/.gitignore @@ -6,3 +6,4 @@ dist/ build/ *.egg-info/ .continuum/ +node_modules/ diff --git a/README.md b/README.md index 6cfcf2e..8bcc0b1 100644 --- a/README.md +++ b/README.md @@ -14,6 +14,7 @@ Local shared memory and controlled workflows for AI coding agents. - Optional: Ollama for local embeddings and summaries. - Optional: an OpenRouter API key for hosted model calls. - Optional: an Obsidian vault folder for readable mirrored handoffs. +- Optional: Bun for the TypeScript/React/Ink interactive shell front end. ## Install @@ -85,6 +86,11 @@ continuum shell continuum shell --agent gemini --color always --animation on ``` +When launched from the npm entry point in an interactive terminal, `continuum +shell` prefers the Bun-powered TypeScript/React/Ink shell. If Bun is not +installed, Continuum falls back to the Python shell with the same slash-command +semantics. + The shell uses slash commands and automatically scopes actions to the current project: diff --git a/bin/continuum.js b/bin/continuum.js index ab32103..530f31c 100644 --- a/bin/continuum.js +++ b/bin/continuum.js @@ -9,6 +9,25 @@ const env = { ...process.env }; env.PYTHONPATH = env.PYTHONPATH ? `${root}${path.delimiter}${env.PYTHONPATH}` : root; env.PYTHONDONTWRITEBYTECODE = "1"; const forwarded = process.argv.slice(2); + +if ( + forwarded[0] === "shell" && + process.stdin.isTTY && + process.stdout.isTTY && + env.CONTINUUM_PYTHON_SHELL !== "1" +) { + const shellArgs = forwarded.slice(1); + const bun = spawnSync("bun", ["run", path.join(root, "src", "cli.tsx"), ...shellArgs], { + stdio: "inherit", + env, + cwd: process.cwd(), + }); + if (!bun.error || bun.error.code !== "ENOENT") { + process.exit(bun.status === null ? 1 : bun.status); + } + console.error("Continuum Ink shell requires Bun. Falling back to the Python shell."); +} + const candidates = process.platform === "win32" ? [["py", ["-3", "-m", "continuum"]], ["python", ["-m", "continuum"]]] : [["python3", ["-m", "continuum"]], ["python", ["-m", "continuum"]]]; diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..d99c79f --- /dev/null +++ b/package-lock.json @@ -0,0 +1,635 @@ +{ + "name": "continuum-agent-memory", + "version": "0.9.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "continuum-agent-memory", + "version": "0.9.0", + "license": "MIT", + "dependencies": { + "ink": "^5.2.1", + "react": "^18.3.1", + "yoga-layout": "^3.2.1" + }, + "bin": { + "continuum": "bin/continuum.js", + "continuum-agent-memory": "bin/continuum.js" + }, + "devDependencies": { + "@types/node": "^24.0.0", + "@types/react": "^18.3.23", + "typescript": "^5.8.3" + }, + "engines": { + "bun": ">=1.1.0", + "node": ">=18" + } + }, + "node_modules/@alcalzone/ansi-tokenize": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/@alcalzone/ansi-tokenize/-/ansi-tokenize-0.1.3.tgz", + "integrity": "sha512-3yWxPTq3UQ/FY9p1ErPxIyfT64elWaMvM9lIHnaqpyft63tkxodF5aUElYHrdisWve5cETkh1+KBw1yJuW0aRw==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.2.1", + "is-fullwidth-code-point": "^4.0.0" + }, + "engines": { + "node": ">=14.13.1" + } + }, + "node_modules/@types/node": { + "version": "24.12.4", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.12.4.tgz", + "integrity": "sha512-GUUEShf+PBCGW2KaXwcIt3Yk+e3pkKwWKb9GSyM9WQVE+ep2jzmHdGsHzu4wgcZy5fN9FBdVzjpBQsYlpfpgLA==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~7.16.0" + } + }, + "node_modules/@types/prop-types": { + "version": "15.7.15", + "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.15.tgz", + "integrity": "sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/@types/react": { + "version": "18.3.30", + "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.30.tgz", + "integrity": "sha512-3ek6mwJL5/VBewBcY4S66cqlCtK3qi4WIq37Z0m/NHw1hjhI7274Mx1qz/+ggSzyBCOEf7eHjBN6INjPAWYfYw==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "@types/prop-types": "*", + "csstype": "^3.2.2" + } + }, + "node_modules/ansi-escapes": { + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-7.3.0.tgz", + "integrity": "sha512-BvU8nYgGQBxcmMuEeUEmNTvrMVjJNSH7RgW24vXexN4Ven6qCvy4TntnvlnwnMLTVlcRQQdbRY8NKnaIoeWDNg==", + "license": "MIT", + "dependencies": { + "environment": "^1.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ansi-regex": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", + "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/ansi-styles": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/auto-bind": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/auto-bind/-/auto-bind-5.0.1.tgz", + "integrity": "sha512-ooviqdwwgfIfNmDwo94wlshcdzfO64XV0Cg6oDsDYBJfITDz1EngD2z7DkbvCWn+XIMsIqW27sEVF6qcpJrRcg==", + "license": "MIT", + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/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/cli-boxes": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/cli-boxes/-/cli-boxes-3.0.0.tgz", + "integrity": "sha512-/lzGpEWL/8PfI0BmBOPRwp0c/wFNX1RdUML3jK/RcSBA9T8mZDdQpqYBKtCFTOfQbwPqWEOpjqW+Fnayc0969g==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cli-cursor": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-4.0.0.tgz", + "integrity": "sha512-VGtlMu3x/4DOtIUwEkRezxUZ2lBacNJCHash0N0WeZDBS+7Ux1dm3XWAgWYxLJFMMdOeXMHXorshEFhbMSGelg==", + "license": "MIT", + "dependencies": { + "restore-cursor": "^4.0.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cli-truncate": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/cli-truncate/-/cli-truncate-4.0.0.tgz", + "integrity": "sha512-nPdaFdQ0h/GEigbPClz11D0v/ZJEwxmeVZGeMo3Z5StPtUTkA9o1lD6QwoirYiSDzbcwn2XcjwmCp68W1IS4TA==", + "license": "MIT", + "dependencies": { + "slice-ansi": "^5.0.0", + "string-width": "^7.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cli-truncate/node_modules/slice-ansi": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-5.0.0.tgz", + "integrity": "sha512-FC+lgizVPfie0kkhqUScwRu1O/lF6NOgJmlCgK+/LYxDCTk8sGelYaHDhFcDN+Sn3Cv+3VSa4Byeo+IMCzpMgQ==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.0.0", + "is-fullwidth-code-point": "^4.0.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/slice-ansi?sponsor=1" + } + }, + "node_modules/code-excerpt": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/code-excerpt/-/code-excerpt-4.0.0.tgz", + "integrity": "sha512-xxodCmBen3iy2i0WtAK8FlFNrRzjUqjRsMfho58xT/wvZU1YTM3fCnRjcy1gJPMepaRlgm/0e6w8SpWHpn3/cA==", + "license": "MIT", + "dependencies": { + "convert-to-spaces": "^2.0.1" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + } + }, + "node_modules/convert-to-spaces": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/convert-to-spaces/-/convert-to-spaces-2.0.1.tgz", + "integrity": "sha512-rcQ1bsQO9799wq24uE5AM2tAILy4gXGIK/njFWcVQkGNZ96edlpY+A7bjwvzjYvLDyzmG1MmMLZhpcsb+klNMQ==", + "license": "MIT", + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + } + }, + "node_modules/csstype": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/emoji-regex": { + "version": "10.6.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.6.0.tgz", + "integrity": "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==", + "license": "MIT" + }, + "node_modules/environment": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/environment/-/environment-1.1.0.tgz", + "integrity": "sha512-xUtoPkMggbz0MPyPiIWr1Kp4aeWJjDZ6SMvURhimjdZgsRuDplF5/s9hcgGhyXMhs+6vpnuoiZ2kFiu3FMnS8Q==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/es-toolkit": { + "version": "1.47.0", + "resolved": "https://registry.npmjs.org/es-toolkit/-/es-toolkit-1.47.0.tgz", + "integrity": "sha512-n1GuoD0WEQZMBk5tttoZSqwgyLx01oqa5XsBmCHwPyNe1S9jPBEmtR2pSgp2kJuWE3ciFZ6yRHmY4pM4C3OOkw==", + "license": "MIT", + "workspaces": [ + "docs", + "benchmarks" + ] + }, + "node_modules/escape-string-regexp": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz", + "integrity": "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/get-east-asian-width": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.6.0.tgz", + "integrity": "sha512-QRbvDIbx6YklUe6RxeTeleMR0yv3cYH6PsPZHcnVn7xv7zO1BHN8r0XETu8n6Ye3Q+ahtSarc3WgtNWmehIBfA==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/indent-string": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-5.0.0.tgz", + "integrity": "sha512-m6FAo/spmsW2Ab2fU35JTYwtOKa2yAwXSwgjSv1TJzh4Mh7mC3lzAOVLBprb72XsTrgkEIsl7YrFNAiDiRhIGg==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ink": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/ink/-/ink-5.2.1.tgz", + "integrity": "sha512-BqcUyWrG9zq5HIwW6JcfFHsIYebJkWWb4fczNah1goUO0vv5vneIlfwuS85twyJ5hYR/y18FlAYUxrO9ChIWVg==", + "license": "MIT", + "dependencies": { + "@alcalzone/ansi-tokenize": "^0.1.3", + "ansi-escapes": "^7.0.0", + "ansi-styles": "^6.2.1", + "auto-bind": "^5.0.1", + "chalk": "^5.3.0", + "cli-boxes": "^3.0.0", + "cli-cursor": "^4.0.0", + "cli-truncate": "^4.0.0", + "code-excerpt": "^4.0.0", + "es-toolkit": "^1.22.0", + "indent-string": "^5.0.0", + "is-in-ci": "^1.0.0", + "patch-console": "^2.0.0", + "react-reconciler": "^0.29.0", + "scheduler": "^0.23.0", + "signal-exit": "^3.0.7", + "slice-ansi": "^7.1.0", + "stack-utils": "^2.0.6", + "string-width": "^7.2.0", + "type-fest": "^4.27.0", + "widest-line": "^5.0.0", + "wrap-ansi": "^9.0.0", + "ws": "^8.18.0", + "yoga-layout": "~3.2.1" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/react": ">=18.0.0", + "react": ">=18.0.0", + "react-devtools-core": "^4.19.1" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "react-devtools-core": { + "optional": true + } + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-4.0.0.tgz", + "integrity": "sha512-O4L094N2/dZ7xqVdrXhh9r1KODPJpFms8B5sGdJLPy664AgvXsreZUyCQQNItZRDlYug4xStLjNp/sz3HvBowQ==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-in-ci": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-in-ci/-/is-in-ci-1.0.0.tgz", + "integrity": "sha512-eUuAjybVTHMYWm/U+vBO1sY/JOCgoPCXRxzdju0K+K0BiGW0SChEL1MLC0PoCIR1OlPo5YAp8HuQoUlsWEICwg==", + "license": "MIT", + "bin": { + "is-in-ci": "cli.js" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "license": "MIT" + }, + "node_modules/loose-envify": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "license": "MIT", + "dependencies": { + "js-tokens": "^3.0.0 || ^4.0.0" + }, + "bin": { + "loose-envify": "cli.js" + } + }, + "node_modules/mimic-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", + "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/onetime": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", + "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", + "license": "MIT", + "dependencies": { + "mimic-fn": "^2.1.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/patch-console": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/patch-console/-/patch-console-2.0.0.tgz", + "integrity": "sha512-0YNdUceMdaQwoKce1gatDScmMo5pu/tfABfnzEqeG0gtTmd7mh/WcwgUjtAeOU7N8nFFlbQBnFK2gXW5fGvmMA==", + "license": "MIT", + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + } + }, + "node_modules/react": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", + "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-reconciler": { + "version": "0.29.2", + "resolved": "https://registry.npmjs.org/react-reconciler/-/react-reconciler-0.29.2.tgz", + "integrity": "sha512-zZQqIiYgDCTP/f1N/mAR10nJGrPD2ZR+jDSEsKWJHYC7Cm2wodlwbR3upZRdC3cjIjSlTLNVyO7Iu0Yy7t2AYg==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0", + "scheduler": "^0.23.2" + }, + "engines": { + "node": ">=0.10.0" + }, + "peerDependencies": { + "react": "^18.3.1" + } + }, + "node_modules/restore-cursor": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-4.0.0.tgz", + "integrity": "sha512-I9fPXU9geO9bHOt9pHHOhOkYerIMsmVaWB0rA2AI9ERh/+x/i7MV5HKBNrg+ljO5eoPVgCcnFuRjJ9uH6I/3eg==", + "license": "MIT", + "dependencies": { + "onetime": "^5.1.0", + "signal-exit": "^3.0.2" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/scheduler": { + "version": "0.23.2", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz", + "integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0" + } + }, + "node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "license": "ISC" + }, + "node_modules/slice-ansi": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-7.1.2.tgz", + "integrity": "sha512-iOBWFgUX7caIZiuutICxVgX1SdxwAVFFKwt1EvMYYec/NWO5meOJ6K5uQxhrYBdQJne4KxiqZc+KptFOWFSI9w==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.2.1", + "is-fullwidth-code-point": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/chalk/slice-ansi?sponsor=1" + } + }, + "node_modules/slice-ansi/node_modules/is-fullwidth-code-point": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-5.1.0.tgz", + "integrity": "sha512-5XHYaSyiqADb4RnZ1Bdad6cPp8Toise4TzEjcOYDHZkTCbKgiUl7WTUCpNWHuxmDt91wnsZBc9xinNzopv3JMQ==", + "license": "MIT", + "dependencies": { + "get-east-asian-width": "^1.3.1" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/stack-utils": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/stack-utils/-/stack-utils-2.0.6.tgz", + "integrity": "sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ==", + "license": "MIT", + "dependencies": { + "escape-string-regexp": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/string-width": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", + "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", + "license": "MIT", + "dependencies": { + "emoji-regex": "^10.3.0", + "get-east-asian-width": "^1.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/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/type-fest": { + "version": "4.41.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.41.0.tgz", + "integrity": "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==", + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "7.16.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz", + "integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==", + "dev": true, + "license": "MIT" + }, + "node_modules/widest-line": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/widest-line/-/widest-line-5.0.0.tgz", + "integrity": "sha512-c9bZp7b5YtRj2wOe6dlj32MK+Bx/M/d+9VB2SHM1OtsUHR0aV0tdP6DWh/iMt0kWi1t5g1Iudu6hQRNd1A4PVA==", + "license": "MIT", + "dependencies": { + "string-width": "^7.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/wrap-ansi": { + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-9.0.2.tgz", + "integrity": "sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.2.1", + "string-width": "^7.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/ws": { + "version": "8.21.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.21.0.tgz", + "integrity": "sha512-Vsp28b7DRcimFQvrqu2Wek3z1iYxDCWqHYB8Qsnk/S4RfaCQzPGPyBNuVjJV3cd6UiKtUtp6sNM77gWvzcCH+g==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/yoga-layout": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/yoga-layout/-/yoga-layout-3.2.1.tgz", + "integrity": "sha512-0LPOt3AxKqMdFBZA3HBAt/t/8vIKq7VaQYbuA8WxCgung+p9TVyKRYdpvCb80HcdTN2NkbIKbhNwKUfm3tQywQ==", + "license": "MIT" + } + } +} diff --git a/package.json b/package.json index 897f7ab..8edc053 100644 --- a/package.json +++ b/package.json @@ -7,8 +7,24 @@ "continuum": "bin/continuum.js", "continuum-agent-memory": "bin/continuum.js" }, + "scripts": { + "shell": "bun run src/cli.tsx", + "typecheck": "tsc --noEmit" + }, + "dependencies": { + "ink": "^5.2.1", + "react": "^18.3.1", + "yoga-layout": "^3.2.1" + }, + "devDependencies": { + "@types/node": "^24.0.0", + "@types/react": "^18.3.23", + "typescript": "^5.8.3" + }, "files": [ "bin/", + "src/", + "tsconfig.json", ".dockerignore", "CONTRIBUTORS.md", "continuum/*.py", @@ -34,6 +50,7 @@ "obsidian" ], "engines": { + "bun": ">=1.1.0", "node": ">=18" } } diff --git a/src/cli.tsx b/src/cli.tsx new file mode 100644 index 0000000..347041a --- /dev/null +++ b/src/cli.tsx @@ -0,0 +1,186 @@ +#!/usr/bin/env bun +import React, {useMemo, useState} from "react"; +import {Box, render, Text, useApp, useInput} from "ink"; +import path from "node:path"; +import {fileURLToPath} from "node:url"; +import {type Agent, runContinuum, translateInput} from "./continuum-runtime.ts"; + +type Entry = { + kind: "system" | "input" | "output" | "error"; + text: string; +}; + +const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), ".."); +const shellArgs = parseShellArgs(process.argv.slice(2)); + +function App() { + const {exit} = useApp(); + const [agent, setAgent] = useState(shellArgs.agent); + const [input, setInput] = useState(""); + const [busy, setBusy] = useState(false); + const [entries, setEntries] = useState([ + {kind: "system", text: "Continuum shell. Type /help for commands, /quit to exit."}, + ]); + + const columns = process.stdout.columns || 100; + const promptWidth = useMemo(() => { + // Ink renders through Yoga; keeping the prompt width stable prevents input jitter. + return Math.max(12, Math.min(24, Math.floor(columns * 0.22))); + }, [columns]); + + useInput((value, key) => { + if (busy) { + return; + } + if (key.return) { + void submit(); + return; + } + if (key.backspace || key.delete) { + setInput((current) => current.slice(0, -1)); + return; + } + if (key.ctrl && value === "c") { + exit(); + return; + } + if (value) { + setInput((current) => current + value); + } + }); + + async function submit() { + const command = input.trim(); + setInput(""); + if (!command) { + return; + } + setEntries((current) => [...current, {kind: "input", text: command}]); + const translated = translateInput(command, agent, {project: shellArgs.project, vault: shellArgs.vault, root}); + if (!Array.isArray(translated)) { + handleLocal(translated.local); + return; + } + setBusy(true); + const result = await runContinuum(translated, {project: shellArgs.project, vault: shellArgs.vault, root}); + setEntries((current) => [ + ...current, + {kind: result.code === 0 ? "output" : "error", text: result.output.trim() || `(exit ${result.code})`}, + ]); + setBusy(false); + } + + function handleLocal(action: string) { + if (action === "quit") { + exit(); + return; + } + if (action.startsWith("agent:")) { + const next = action.split(":")[1] as Agent; + setAgent(next); + setEntries((current) => [...current, {kind: "system", text: `Selected agent: ${next}`}]); + return; + } + if (action.startsWith("switch:")) { + const [, next, mode] = action.split(":") as ["switch", Agent, string]; + setAgent(next); + setEntries((current) => [...current, {kind: "system", text: `Selected agent: ${next}; run /resume ${next} ${mode} to inject context.`}]); + return; + } + setEntries((current) => [...current, {kind: "system", text: helpText(action)}]); + } + + return ( + + + Continuum + Project: {shellArgs.project} + Runtime: TypeScript + React Ink, Yoga layout, Bun-first + + + {entries.slice(-12).map((entry, index) => ( + + {prefixFor(entry.kind)} {entry.text} + + ))} + + + + continuum[{agent}]> + + {input} + {busy ? running : null} + + + ); +} + +function colorFor(kind: Entry["kind"]) { + if (kind === "error") return "red"; + if (kind === "input") return "cyan"; + if (kind === "system") return "gray"; + return undefined; +} + +function prefixFor(kind: Entry["kind"]) { + if (kind === "input") return ">"; + if (kind === "error") return "!"; + return "-"; +} + +function agentColor(agent: Agent) { + if (agent === "claude") return "magenta"; + if (agent === "gemini") return "blue"; + return "cyan"; +} + +function helpText(action: string) { + if (action.endsWith("-help")) { + return `Usage: /${action.replace("-help", "")} [arguments]`; + } + if (action === "agent-help") { + return "Usage: /agent claude|codex|gemini"; + } + if (action === "switch-help") { + return "Usage: /switch claude|codex|gemini [compact|normal|deep]"; + } + if (action === "unknown") { + return "Unknown command. Type /help."; + } + return [ + "Commands: /status, /doctor, /up, /down, /handoff task | next step, /agent, /chat, /terminal, /resume-terminal, /memory, /plan, /task, /team, /worktree, /ui, /quit.", + "Plain text is sent to the selected agent with compact Continuum context.", + ].join("\n"); +} + +function parseShellArgs(argv: string[]): {project: string; vault?: string; agent: Agent} { + let project = process.cwd(); + let vault: string | undefined; + let agent: Agent = "codex"; + for (let index = 0; index < argv.length; index += 1) { + const value = argv[index]; + if (value === "--project" && argv[index + 1]) { + project = path.resolve(argv[index + 1]); + index += 1; + } else if (value.startsWith("--project=")) { + project = path.resolve(value.slice("--project=".length)); + } else if (value === "--vault" && argv[index + 1]) { + vault = path.resolve(argv[index + 1]); + index += 1; + } else if (value.startsWith("--vault=")) { + vault = path.resolve(value.slice("--vault=".length)); + } else if (value === "--agent" && isAgent(argv[index + 1])) { + agent = argv[index + 1] as Agent; + index += 1; + } else if (value.startsWith("--agent=") && isAgent(value.slice("--agent=".length))) { + agent = value.slice("--agent=".length) as Agent; + } + } + return {project, vault, agent}; +} + +function isAgent(value: string | undefined): value is Agent { + return value === "claude" || value === "codex" || value === "gemini"; +} + +render(); diff --git a/src/continuum-runtime.ts b/src/continuum-runtime.ts new file mode 100644 index 0000000..a172ca8 --- /dev/null +++ b/src/continuum-runtime.ts @@ -0,0 +1,214 @@ +import path from "node:path"; +import {spawn, spawnSync} from "node:child_process"; + +export type Agent = "claude" | "codex" | "gemini"; + +export type RuntimeOptions = { + project: string; + vault?: string; + root: string; +}; + +export type CommandResult = { + code: number; + output: string; +}; + +const topLevelCommon = new Set([ + "init", + "daemon", + "up", + "down", + "logs", + "handoff", + "run", + "resume", + "status", + "doctor", + "search", + "service", + "autostart", + "mcp", + "ui", + "shell", + "instruct", + "chat", +]); + +const nestedCommon = new Set([ + "session", + "adapters", + "task", + "providers", + "model", + "memory", + "context", + "message", + "team", + "worktree", + "route", +]); + +const agents = new Set(["claude", "codex", "gemini"]); + +export function commonArgs(options: RuntimeOptions): string[] { + const args = ["--project", path.resolve(options.project)]; + if (options.vault) { + args.push("--vault", path.resolve(options.vault)); + } + return args; +} + +export function pythonPrefix(): [string, string[]] { + const candidates: [string, string[]][] = process.platform === "win32" + ? [["py", ["-3", "-m", "continuum"]], ["python", ["-m", "continuum"]]] + : [["python3", ["-m", "continuum"]], ["python", ["-m", "continuum"]]]; + for (const candidate of candidates) { + const result = spawnSync(candidate[0], [...candidate[1], "--version"], { + env: pythonEnv(process.cwd()), + stdio: "ignore", + }); + if (!result.error) { + return candidate; + } + } + return candidates[candidates.length - 1]; +} + +export function pythonEnv(root: string): NodeJS.ProcessEnv { + return { + ...process.env, + PYTHONPATH: process.env.PYTHONPATH ? `${root}${path.delimiter}${process.env.PYTHONPATH}` : root, + PYTHONDONTWRITEBYTECODE: "1", + }; +} + +export function runContinuum(argv: string[], options: RuntimeOptions): Promise { + const [program, prefix] = pythonPrefix(); + return new Promise((resolve) => { + const child = spawn(program, [...prefix, ...argv], { + cwd: options.project, + env: pythonEnv(options.root), + shell: false, + }); + let output = ""; + child.stdout.on("data", (chunk: Buffer) => { + output += chunk.toString("utf8"); + }); + child.stderr.on("data", (chunk: Buffer) => { + output += chunk.toString("utf8"); + }); + child.on("error", (error) => { + resolve({code: 1, output: String(error.message)}); + }); + child.on("close", (code) => { + resolve({code: code ?? 1, output}); + }); + }); +} + +export function translateInput(input: string, agent: Agent, options: RuntimeOptions): string[] | {local: string} { + const line = input.trim(); + const common = commonArgs(options); + if (!line || line === "/" || line === "/help" || line === "help" || line === "?") { + return {local: "help"}; + } + if (line === "/quit" || line === "/exit" || line === "quit" || line === "exit") { + return {local: "quit"}; + } + if (!line.startsWith("/")) { + return ["chat", ...common, agent, "compact", line]; + } + const parts = splitCommand(line.slice(1)); + const command = parts[0]?.toLowerCase(); + const rest = parts.slice(1); + if (!command) { + return {local: "help"}; + } + if (command === "agent") { + const next = rest[0]; + return next && agents.has(next as Agent) ? {local: `agent:${next}`} : {local: "agent-help"}; + } + if (command === "status" || command === "doctor" || command === "up" || command === "down" || command === "logs") { + return [command, ...common, ...rest]; + } + if (command === "search") { + return ["search", ...common, rest.join(" ")]; + } + if (command === "handoff" && rest.includes("|")) { + const divider = rest.indexOf("|"); + return ["handoff", ...common, "--task", rest.slice(0, divider).join(" "), "--next-step", rest.slice(divider + 1).join(" ")]; + } + if (command === "chat") { + const [target, mode, body] = agentModeBody(rest, agent); + return ["chat", ...common, target, mode, body]; + } + if (command === "switch") { + const next = rest[0]; + if (!next || !agents.has(next as Agent)) { + return {local: "switch-help"}; + } + const mode = rest[1] && ["compact", "normal", "deep"].includes(rest[1]) ? rest[1] : "compact"; + return {local: `switch:${next}:${mode}`}; + } + if (command === "terminal" || command === "pty") { + const [target, passthrough] = agentAndRest(rest, agent); + return ["run", ...common, "--interactive", target, ...approvalArgs(target), ...passthrough]; + } + if (command === "resume-terminal" || command === "resume-pty") { + const [target, passthrough] = agentAndRest(rest, agent); + const mode = passthrough[0] && ["compact", "normal", "deep"].includes(passthrough[0]) ? passthrough.shift()! : "compact"; + return ["resume", ...common, "--interactive", target, mode, ...approvalArgs(target), ...passthrough]; + } + if (command === "memory") { + const semantic = rest.includes("--semantic") ? ["--semantic"] : []; + const query = rest.filter((item) => item !== "--semantic").join(" "); + return ["memory", "retrieve", ...common, query, ...semantic]; + } + if (command === "plan") { + return ["team", "run", ...common, "default_dev_team", rest.join(" ")]; + } + if (topLevelCommon.has(command)) { + return injectCommon([command, ...rest], 1, common); + } + if (nestedCommon.has(command)) { + return rest.length ? injectCommon([command, ...rest], 2, common) : {local: `${command}-help`}; + } + return {local: "unknown"}; +} + +function splitCommand(value: string): string[] { + const matches = value.match(/"([^"]*)"|'([^']*)'|\S+/g) ?? []; + return matches.map((item) => item.replace(/^["']|["']$/g, "")); +} + +function injectCommon(argv: string[], index: number, common: string[]): string[] { + if (argv.some((item) => item === "--project" || item.startsWith("--project="))) { + return argv; + } + return [...argv.slice(0, index), ...common, ...argv.slice(index)]; +} + +function agentAndRest(values: string[], fallback: Agent): [Agent, string[]] { + const first = values[0]; + if (first && agents.has(first as Agent)) { + return [first as Agent, values.slice(1)]; + } + return [fallback, [...values]]; +} + +function agentModeBody(values: string[], fallback: Agent): [Agent, string, string] { + const [target, rest] = agentAndRest(values, fallback); + const mode = rest[0] && ["compact", "normal", "deep"].includes(rest[0]) ? rest.shift()! : "compact"; + return [target, mode, rest.join(" ")]; +} + +function approvalArgs(agent: Agent): string[] { + if (agent === "codex") { + return ["--ask-for-approval", "on-request"]; + } + if (agent === "gemini") { + return ["--approval-mode", "default"]; + } + return ["--permission-mode", "default"]; +} diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..a4773d3 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,15 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "ESNext", + "moduleResolution": "Bundler", + "jsx": "react-jsx", + "strict": true, + "allowImportingTsExtensions": true, + "esModuleInterop": true, + "skipLibCheck": true, + "types": ["node"], + "noEmit": true + }, + "include": ["src/**/*.ts", "src/**/*.tsx"] +}