Merge pull request #2 from nrslib/feature/remove-interactive-mode
Remove interactive mode and simplify CLI
This commit is contained in:
commit
8925609d9a
10
CLAUDE.md
10
CLAUDE.md
@ -115,4 +115,12 @@ steps:
|
|||||||
- Strict TypeScript with `noUncheckedIndexedAccess`
|
- Strict TypeScript with `noUncheckedIndexedAccess`
|
||||||
- Zod schemas for runtime validation (`src/models/schemas.ts`)
|
- Zod schemas for runtime validation (`src/models/schemas.ts`)
|
||||||
- Uses `@anthropic-ai/claude-agent-sdk` for Claude integration
|
- Uses `@anthropic-ai/claude-agent-sdk` for Claude integration
|
||||||
- React/Ink for interactive terminal UI (`src/interactive/`)
|
- Simple CLI prompts in `src/prompt/` for user interaction
|
||||||
|
|
||||||
|
## Command Design Principles
|
||||||
|
|
||||||
|
**Keep commands minimal.** Avoid proliferating commands. One command per concept.
|
||||||
|
|
||||||
|
- Use a single command with arguments/modes instead of multiple similar commands
|
||||||
|
- Example: `/config` opens permission mode selection. No need for `/sacrifice`, `/safe`, `/confirm`, etc.
|
||||||
|
- Before adding a new command, consider if existing commands can be extended
|
||||||
|
|||||||
649
package-lock.json
generated
649
package-lock.json
generated
@ -1,31 +1,27 @@
|
|||||||
{
|
{
|
||||||
"name": "wolf-orchestrator",
|
"name": "takt",
|
||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "wolf-orchestrator",
|
"name": "takt",
|
||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@anthropic-ai/claude-agent-sdk": "^0.2.19",
|
"@anthropic-ai/claude-agent-sdk": "^0.2.19",
|
||||||
"chalk": "^5.3.0",
|
"chalk": "^5.3.0",
|
||||||
"commander": "^12.1.0",
|
"commander": "^12.1.0",
|
||||||
"ink": "^5.2.1",
|
|
||||||
"ink-spinner": "^5.0.0",
|
|
||||||
"react": "^18.3.1",
|
|
||||||
"yaml": "^2.4.5",
|
"yaml": "^2.4.5",
|
||||||
"zod": "^4.3.6"
|
"zod": "^4.3.6"
|
||||||
},
|
},
|
||||||
"bin": {
|
"bin": {
|
||||||
"wolf": "bin/wolf",
|
"takt": "bin/takt",
|
||||||
"wolf-cli": "dist/cli.js"
|
"takt-cli": "dist/cli.js"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@eslint/js": "^9.39.2",
|
"@eslint/js": "^9.39.2",
|
||||||
"@types/node": "^20.14.0",
|
"@types/node": "^20.14.0",
|
||||||
"@types/react": "^18.3.27",
|
|
||||||
"@typescript-eslint/eslint-plugin": "^8.0.0",
|
"@typescript-eslint/eslint-plugin": "^8.0.0",
|
||||||
"@typescript-eslint/parser": "^8.0.0",
|
"@typescript-eslint/parser": "^8.0.0",
|
||||||
"eslint": "^9.0.0",
|
"eslint": "^9.0.0",
|
||||||
@ -37,31 +33,6 @@
|
|||||||
"node": ">=18.0.0"
|
"node": ">=18.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@alcalzone/ansi-tokenize": {
|
|
||||||
"version": "0.1.3",
|
|
||||||
"resolved": "https://registry.npmjs.org/@alcalzone/ansi-tokenize/-/ansi-tokenize-0.1.3.tgz",
|
|
||||||
"integrity": "sha512-3yWxPTq3UQ/FY9p1ErPxIyfT64elWaMvM9lIHnaqpyft63tkxodF5aUElYHrdisWve5cETkh1+KBw1yJuW0aRw==",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"ansi-styles": "^6.2.1",
|
|
||||||
"is-fullwidth-code-point": "^4.0.0"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=14.13.1"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@alcalzone/ansi-tokenize/node_modules/ansi-styles": {
|
|
||||||
"version": "6.2.3",
|
|
||||||
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz",
|
|
||||||
"integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==",
|
|
||||||
"license": "MIT",
|
|
||||||
"engines": {
|
|
||||||
"node": ">=12"
|
|
||||||
},
|
|
||||||
"funding": {
|
|
||||||
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@anthropic-ai/claude-agent-sdk": {
|
"node_modules/@anthropic-ai/claude-agent-sdk": {
|
||||||
"version": "0.2.19",
|
"version": "0.2.19",
|
||||||
"resolved": "https://registry.npmjs.org/@anthropic-ai/claude-agent-sdk/-/claude-agent-sdk-0.2.19.tgz",
|
"resolved": "https://registry.npmjs.org/@anthropic-ai/claude-agent-sdk/-/claude-agent-sdk-0.2.19.tgz",
|
||||||
@ -1382,24 +1353,6 @@
|
|||||||
"undici-types": "~6.21.0"
|
"undici-types": "~6.21.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@types/prop-types": {
|
|
||||||
"version": "15.7.15",
|
|
||||||
"resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.15.tgz",
|
|
||||||
"integrity": "sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==",
|
|
||||||
"devOptional": true,
|
|
||||||
"license": "MIT"
|
|
||||||
},
|
|
||||||
"node_modules/@types/react": {
|
|
||||||
"version": "18.3.27",
|
|
||||||
"resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.27.tgz",
|
|
||||||
"integrity": "sha512-cisd7gxkzjBKU2GgdYrTdtQx1SORymWyaAFhaxQPK9bYO9ot3Y5OikQRvY0VYQtvwjeQnizCINJAenh/V7MK2w==",
|
|
||||||
"devOptional": true,
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"@types/prop-types": "*",
|
|
||||||
"csstype": "^3.2.2"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@typescript-eslint/eslint-plugin": {
|
"node_modules/@typescript-eslint/eslint-plugin": {
|
||||||
"version": "8.53.1",
|
"version": "8.53.1",
|
||||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.53.1.tgz",
|
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.53.1.tgz",
|
||||||
@ -1786,33 +1739,6 @@
|
|||||||
"url": "https://github.com/sponsors/epoberezkin"
|
"url": "https://github.com/sponsors/epoberezkin"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/ansi-escapes": {
|
|
||||||
"version": "7.2.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-7.2.0.tgz",
|
|
||||||
"integrity": "sha512-g6LhBsl+GBPRWGWsBtutpzBYuIIdBkLEvad5C/va/74Db018+5TZiyA26cZJAr3Rft5lprVqOIPxf5Vid6tqAw==",
|
|
||||||
"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": {
|
"node_modules/ansi-styles": {
|
||||||
"version": "4.3.0",
|
"version": "4.3.0",
|
||||||
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
|
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
|
||||||
@ -1846,18 +1772,6 @@
|
|||||||
"node": ">=12"
|
"node": ">=12"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"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/balanced-match": {
|
"node_modules/balanced-match": {
|
||||||
"version": "1.0.2",
|
"version": "1.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
|
||||||
@ -1934,101 +1848,6 @@
|
|||||||
"node": ">= 16"
|
"node": ">= 16"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"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-spinners": {
|
|
||||||
"version": "2.9.2",
|
|
||||||
"resolved": "https://registry.npmjs.org/cli-spinners/-/cli-spinners-2.9.2.tgz",
|
|
||||||
"integrity": "sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg==",
|
|
||||||
"license": "MIT",
|
|
||||||
"engines": {
|
|
||||||
"node": ">=6"
|
|
||||||
},
|
|
||||||
"funding": {
|
|
||||||
"url": "https://github.com/sponsors/sindresorhus"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/cli-truncate": {
|
|
||||||
"version": "4.0.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/cli-truncate/-/cli-truncate-4.0.0.tgz",
|
|
||||||
"integrity": "sha512-nPdaFdQ0h/GEigbPClz11D0v/ZJEwxmeVZGeMo3Z5StPtUTkA9o1lD6QwoirYiSDzbcwn2XcjwmCp68W1IS4TA==",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"slice-ansi": "^5.0.0",
|
|
||||||
"string-width": "^7.0.0"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=18"
|
|
||||||
},
|
|
||||||
"funding": {
|
|
||||||
"url": "https://github.com/sponsors/sindresorhus"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/cli-truncate/node_modules/ansi-styles": {
|
|
||||||
"version": "6.2.3",
|
|
||||||
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz",
|
|
||||||
"integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==",
|
|
||||||
"license": "MIT",
|
|
||||||
"engines": {
|
|
||||||
"node": ">=12"
|
|
||||||
},
|
|
||||||
"funding": {
|
|
||||||
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/cli-truncate/node_modules/slice-ansi": {
|
|
||||||
"version": "5.0.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-5.0.0.tgz",
|
|
||||||
"integrity": "sha512-FC+lgizVPfie0kkhqUScwRu1O/lF6NOgJmlCgK+/LYxDCTk8sGelYaHDhFcDN+Sn3Cv+3VSa4Byeo+IMCzpMgQ==",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"ansi-styles": "^6.0.0",
|
|
||||||
"is-fullwidth-code-point": "^4.0.0"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=12"
|
|
||||||
},
|
|
||||||
"funding": {
|
|
||||||
"url": "https://github.com/chalk/slice-ansi?sponsor=1"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/code-excerpt": {
|
|
||||||
"version": "4.0.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/code-excerpt/-/code-excerpt-4.0.0.tgz",
|
|
||||||
"integrity": "sha512-xxodCmBen3iy2i0WtAK8FlFNrRzjUqjRsMfho58xT/wvZU1YTM3fCnRjcy1gJPMepaRlgm/0e6w8SpWHpn3/cA==",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"convert-to-spaces": "^2.0.1"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": "^12.20.0 || ^14.13.1 || >=16.0.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/color-convert": {
|
"node_modules/color-convert": {
|
||||||
"version": "2.0.1",
|
"version": "2.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
|
||||||
@ -2065,15 +1884,6 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"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/cross-spawn": {
|
"node_modules/cross-spawn": {
|
||||||
"version": "7.0.6",
|
"version": "7.0.6",
|
||||||
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
|
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
|
||||||
@ -2089,13 +1899,6 @@
|
|||||||
"node": ">= 8"
|
"node": ">= 8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/csstype": {
|
|
||||||
"version": "3.2.3",
|
|
||||||
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz",
|
|
||||||
"integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==",
|
|
||||||
"devOptional": true,
|
|
||||||
"license": "MIT"
|
|
||||||
},
|
|
||||||
"node_modules/debug": {
|
"node_modules/debug": {
|
||||||
"version": "4.4.3",
|
"version": "4.4.3",
|
||||||
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
|
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
|
||||||
@ -2131,24 +1934,6 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"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-module-lexer": {
|
"node_modules/es-module-lexer": {
|
||||||
"version": "1.7.0",
|
"version": "1.7.0",
|
||||||
"resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz",
|
"resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz",
|
||||||
@ -2156,16 +1941,6 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/es-toolkit": {
|
|
||||||
"version": "1.44.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/es-toolkit/-/es-toolkit-1.44.0.tgz",
|
|
||||||
"integrity": "sha512-6penXeZalaV88MM3cGkFZZfOoLGWshWWfdy0tWw/RlVVyhvMaWSBTOvXNeiW3e5FwdS5ePW0LGEu17zT139ktg==",
|
|
||||||
"license": "MIT",
|
|
||||||
"workspaces": [
|
|
||||||
"docs",
|
|
||||||
"benchmarks"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"node_modules/esbuild": {
|
"node_modules/esbuild": {
|
||||||
"version": "0.21.5",
|
"version": "0.21.5",
|
||||||
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz",
|
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz",
|
||||||
@ -2574,18 +2349,6 @@
|
|||||||
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
|
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/get-east-asian-width": {
|
|
||||||
"version": "1.4.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.4.0.tgz",
|
|
||||||
"integrity": "sha512-QZjmEOC+IT1uk6Rx0sX22V6uHWVwbdbxf1faPqJ1QhLdGgsRGCZoyaQBm/piRdJy/D2um6hM1UP7ZEeQ4EkP+Q==",
|
|
||||||
"license": "MIT",
|
|
||||||
"engines": {
|
|
||||||
"node": ">=18"
|
|
||||||
},
|
|
||||||
"funding": {
|
|
||||||
"url": "https://github.com/sponsors/sindresorhus"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/glob-parent": {
|
"node_modules/glob-parent": {
|
||||||
"version": "6.0.2",
|
"version": "6.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz",
|
||||||
@ -2659,94 +2422,6 @@
|
|||||||
"node": ">=0.8.19"
|
"node": ">=0.8.19"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/indent-string": {
|
|
||||||
"version": "5.0.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/indent-string/-/indent-string-5.0.0.tgz",
|
|
||||||
"integrity": "sha512-m6FAo/spmsW2Ab2fU35JTYwtOKa2yAwXSwgjSv1TJzh4Mh7mC3lzAOVLBprb72XsTrgkEIsl7YrFNAiDiRhIGg==",
|
|
||||||
"license": "MIT",
|
|
||||||
"engines": {
|
|
||||||
"node": ">=12"
|
|
||||||
},
|
|
||||||
"funding": {
|
|
||||||
"url": "https://github.com/sponsors/sindresorhus"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/ink": {
|
|
||||||
"version": "5.2.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/ink/-/ink-5.2.1.tgz",
|
|
||||||
"integrity": "sha512-BqcUyWrG9zq5HIwW6JcfFHsIYebJkWWb4fczNah1goUO0vv5vneIlfwuS85twyJ5hYR/y18FlAYUxrO9ChIWVg==",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"@alcalzone/ansi-tokenize": "^0.1.3",
|
|
||||||
"ansi-escapes": "^7.0.0",
|
|
||||||
"ansi-styles": "^6.2.1",
|
|
||||||
"auto-bind": "^5.0.1",
|
|
||||||
"chalk": "^5.3.0",
|
|
||||||
"cli-boxes": "^3.0.0",
|
|
||||||
"cli-cursor": "^4.0.0",
|
|
||||||
"cli-truncate": "^4.0.0",
|
|
||||||
"code-excerpt": "^4.0.0",
|
|
||||||
"es-toolkit": "^1.22.0",
|
|
||||||
"indent-string": "^5.0.0",
|
|
||||||
"is-in-ci": "^1.0.0",
|
|
||||||
"patch-console": "^2.0.0",
|
|
||||||
"react-reconciler": "^0.29.0",
|
|
||||||
"scheduler": "^0.23.0",
|
|
||||||
"signal-exit": "^3.0.7",
|
|
||||||
"slice-ansi": "^7.1.0",
|
|
||||||
"stack-utils": "^2.0.6",
|
|
||||||
"string-width": "^7.2.0",
|
|
||||||
"type-fest": "^4.27.0",
|
|
||||||
"widest-line": "^5.0.0",
|
|
||||||
"wrap-ansi": "^9.0.0",
|
|
||||||
"ws": "^8.18.0",
|
|
||||||
"yoga-layout": "~3.2.1"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=18"
|
|
||||||
},
|
|
||||||
"peerDependencies": {
|
|
||||||
"@types/react": ">=18.0.0",
|
|
||||||
"react": ">=18.0.0",
|
|
||||||
"react-devtools-core": "^4.19.1"
|
|
||||||
},
|
|
||||||
"peerDependenciesMeta": {
|
|
||||||
"@types/react": {
|
|
||||||
"optional": true
|
|
||||||
},
|
|
||||||
"react-devtools-core": {
|
|
||||||
"optional": true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/ink-spinner": {
|
|
||||||
"version": "5.0.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/ink-spinner/-/ink-spinner-5.0.0.tgz",
|
|
||||||
"integrity": "sha512-EYEasbEjkqLGyPOUc8hBJZNuC5GvXGMLu0w5gdTNskPc7Izc5vO3tdQEYnzvshucyGCBXc86ig0ujXPMWaQCdA==",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"cli-spinners": "^2.7.0"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=14.16"
|
|
||||||
},
|
|
||||||
"peerDependencies": {
|
|
||||||
"ink": ">=4.0.0",
|
|
||||||
"react": ">=18.0.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/ink/node_modules/ansi-styles": {
|
|
||||||
"version": "6.2.3",
|
|
||||||
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz",
|
|
||||||
"integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==",
|
|
||||||
"license": "MIT",
|
|
||||||
"engines": {
|
|
||||||
"node": ">=12"
|
|
||||||
},
|
|
||||||
"funding": {
|
|
||||||
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/is-extglob": {
|
"node_modules/is-extglob": {
|
||||||
"version": "2.1.1",
|
"version": "2.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
|
||||||
@ -2757,18 +2432,6 @@
|
|||||||
"node": ">=0.10.0"
|
"node": ">=0.10.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/is-fullwidth-code-point": {
|
|
||||||
"version": "4.0.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-4.0.0.tgz",
|
|
||||||
"integrity": "sha512-O4L094N2/dZ7xqVdrXhh9r1KODPJpFms8B5sGdJLPy664AgvXsreZUyCQQNItZRDlYug4xStLjNp/sz3HvBowQ==",
|
|
||||||
"license": "MIT",
|
|
||||||
"engines": {
|
|
||||||
"node": ">=12"
|
|
||||||
},
|
|
||||||
"funding": {
|
|
||||||
"url": "https://github.com/sponsors/sindresorhus"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/is-glob": {
|
"node_modules/is-glob": {
|
||||||
"version": "4.0.3",
|
"version": "4.0.3",
|
||||||
"resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz",
|
||||||
@ -2782,21 +2445,6 @@
|
|||||||
"node": ">=0.10.0"
|
"node": ">=0.10.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/is-in-ci": {
|
|
||||||
"version": "1.0.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/is-in-ci/-/is-in-ci-1.0.0.tgz",
|
|
||||||
"integrity": "sha512-eUuAjybVTHMYWm/U+vBO1sY/JOCgoPCXRxzdju0K+K0BiGW0SChEL1MLC0PoCIR1OlPo5YAp8HuQoUlsWEICwg==",
|
|
||||||
"license": "MIT",
|
|
||||||
"bin": {
|
|
||||||
"is-in-ci": "cli.js"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=18"
|
|
||||||
},
|
|
||||||
"funding": {
|
|
||||||
"url": "https://github.com/sponsors/sindresorhus"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/isexe": {
|
"node_modules/isexe": {
|
||||||
"version": "2.0.0",
|
"version": "2.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
|
||||||
@ -2804,12 +2452,6 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "ISC"
|
"license": "ISC"
|
||||||
},
|
},
|
||||||
"node_modules/js-tokens": {
|
|
||||||
"version": "4.0.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
|
|
||||||
"integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==",
|
|
||||||
"license": "MIT"
|
|
||||||
},
|
|
||||||
"node_modules/js-yaml": {
|
"node_modules/js-yaml": {
|
||||||
"version": "4.1.1",
|
"version": "4.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz",
|
||||||
@ -2891,18 +2533,6 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/loose-envify": {
|
|
||||||
"version": "1.4.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz",
|
|
||||||
"integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"js-tokens": "^3.0.0 || ^4.0.0"
|
|
||||||
},
|
|
||||||
"bin": {
|
|
||||||
"loose-envify": "cli.js"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/loupe": {
|
"node_modules/loupe": {
|
||||||
"version": "3.2.1",
|
"version": "3.2.1",
|
||||||
"resolved": "https://registry.npmjs.org/loupe/-/loupe-3.2.1.tgz",
|
"resolved": "https://registry.npmjs.org/loupe/-/loupe-3.2.1.tgz",
|
||||||
@ -2920,15 +2550,6 @@
|
|||||||
"@jridgewell/sourcemap-codec": "^1.5.5"
|
"@jridgewell/sourcemap-codec": "^1.5.5"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"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/minimatch": {
|
"node_modules/minimatch": {
|
||||||
"version": "9.0.5",
|
"version": "9.0.5",
|
||||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz",
|
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz",
|
||||||
@ -2978,21 +2599,6 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/onetime": {
|
|
||||||
"version": "5.1.2",
|
|
||||||
"resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz",
|
|
||||||
"integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"mimic-fn": "^2.1.0"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=6"
|
|
||||||
},
|
|
||||||
"funding": {
|
|
||||||
"url": "https://github.com/sponsors/sindresorhus"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/optionator": {
|
"node_modules/optionator": {
|
||||||
"version": "0.9.4",
|
"version": "0.9.4",
|
||||||
"resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz",
|
"resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz",
|
||||||
@ -3056,15 +2662,6 @@
|
|||||||
"node": ">=6"
|
"node": ">=6"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/patch-console": {
|
|
||||||
"version": "2.0.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/patch-console/-/patch-console-2.0.0.tgz",
|
|
||||||
"integrity": "sha512-0YNdUceMdaQwoKce1gatDScmMo5pu/tfABfnzEqeG0gtTmd7mh/WcwgUjtAeOU7N8nFFlbQBnFK2gXW5fGvmMA==",
|
|
||||||
"license": "MIT",
|
|
||||||
"engines": {
|
|
||||||
"node": "^12.20.0 || ^14.13.1 || >=16.0.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/path-exists": {
|
"node_modules/path-exists": {
|
||||||
"version": "4.0.0",
|
"version": "4.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz",
|
||||||
@ -3171,34 +2768,6 @@
|
|||||||
"node": ">=6"
|
"node": ">=6"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"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/resolve-from": {
|
"node_modules/resolve-from": {
|
||||||
"version": "4.0.0",
|
"version": "4.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz",
|
||||||
@ -3209,22 +2778,6 @@
|
|||||||
"node": ">=4"
|
"node": ">=4"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"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/rollup": {
|
"node_modules/rollup": {
|
||||||
"version": "4.56.0",
|
"version": "4.56.0",
|
||||||
"resolved": "https://registry.npmjs.org/rollup/-/rollup-4.56.0.tgz",
|
"resolved": "https://registry.npmjs.org/rollup/-/rollup-4.56.0.tgz",
|
||||||
@ -3270,15 +2823,6 @@
|
|||||||
"fsevents": "~2.3.2"
|
"fsevents": "~2.3.2"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/scheduler": {
|
|
||||||
"version": "0.23.2",
|
|
||||||
"resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz",
|
|
||||||
"integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"loose-envify": "^1.1.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/semver": {
|
"node_modules/semver": {
|
||||||
"version": "7.7.3",
|
"version": "7.7.3",
|
||||||
"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz",
|
"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz",
|
||||||
@ -3322,55 +2866,6 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "ISC"
|
"license": "ISC"
|
||||||
},
|
},
|
||||||
"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/ansi-styles": {
|
|
||||||
"version": "6.2.3",
|
|
||||||
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz",
|
|
||||||
"integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==",
|
|
||||||
"license": "MIT",
|
|
||||||
"engines": {
|
|
||||||
"node": ">=12"
|
|
||||||
},
|
|
||||||
"funding": {
|
|
||||||
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/slice-ansi/node_modules/is-fullwidth-code-point": {
|
|
||||||
"version": "5.1.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-5.1.0.tgz",
|
|
||||||
"integrity": "sha512-5XHYaSyiqADb4RnZ1Bdad6cPp8Toise4TzEjcOYDHZkTCbKgiUl7WTUCpNWHuxmDt91wnsZBc9xinNzopv3JMQ==",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"get-east-asian-width": "^1.3.1"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=18"
|
|
||||||
},
|
|
||||||
"funding": {
|
|
||||||
"url": "https://github.com/sponsors/sindresorhus"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/source-map-js": {
|
"node_modules/source-map-js": {
|
||||||
"version": "1.2.1",
|
"version": "1.2.1",
|
||||||
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
|
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
|
||||||
@ -3381,27 +2876,6 @@
|
|||||||
"node": ">=0.10.0"
|
"node": ">=0.10.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/stack-utils": {
|
|
||||||
"version": "2.0.6",
|
|
||||||
"resolved": "https://registry.npmjs.org/stack-utils/-/stack-utils-2.0.6.tgz",
|
|
||||||
"integrity": "sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ==",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"escape-string-regexp": "^2.0.0"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=10"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/stack-utils/node_modules/escape-string-regexp": {
|
|
||||||
"version": "2.0.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz",
|
|
||||||
"integrity": "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==",
|
|
||||||
"license": "MIT",
|
|
||||||
"engines": {
|
|
||||||
"node": ">=8"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/stackback": {
|
"node_modules/stackback": {
|
||||||
"version": "0.0.2",
|
"version": "0.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz",
|
||||||
@ -3416,38 +2890,6 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"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.1.2",
|
|
||||||
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.2.tgz",
|
|
||||||
"integrity": "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"ansi-regex": "^6.0.1"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=12"
|
|
||||||
},
|
|
||||||
"funding": {
|
|
||||||
"url": "https://github.com/chalk/strip-ansi?sponsor=1"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/strip-json-comments": {
|
"node_modules/strip-json-comments": {
|
||||||
"version": "3.1.1",
|
"version": "3.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz",
|
||||||
@ -3561,18 +3003,6 @@
|
|||||||
"node": ">= 0.8.0"
|
"node": ">= 0.8.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"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": {
|
"node_modules/typescript": {
|
||||||
"version": "5.9.3",
|
"version": "5.9.3",
|
||||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
|
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
|
||||||
@ -3810,21 +3240,6 @@
|
|||||||
"node": ">=8"
|
"node": ">=8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/widest-line": {
|
|
||||||
"version": "5.0.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/widest-line/-/widest-line-5.0.0.tgz",
|
|
||||||
"integrity": "sha512-c9bZp7b5YtRj2wOe6dlj32MK+Bx/M/d+9VB2SHM1OtsUHR0aV0tdP6DWh/iMt0kWi1t5g1Iudu6hQRNd1A4PVA==",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"string-width": "^7.0.0"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=18"
|
|
||||||
},
|
|
||||||
"funding": {
|
|
||||||
"url": "https://github.com/sponsors/sindresorhus"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/word-wrap": {
|
"node_modules/word-wrap": {
|
||||||
"version": "1.2.5",
|
"version": "1.2.5",
|
||||||
"resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz",
|
"resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz",
|
||||||
@ -3835,56 +3250,6 @@
|
|||||||
"node": ">=0.10.0"
|
"node": ">=0.10.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/wrap-ansi": {
|
|
||||||
"version": "9.0.2",
|
|
||||||
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-9.0.2.tgz",
|
|
||||||
"integrity": "sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww==",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"ansi-styles": "^6.2.1",
|
|
||||||
"string-width": "^7.0.0",
|
|
||||||
"strip-ansi": "^7.1.0"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=18"
|
|
||||||
},
|
|
||||||
"funding": {
|
|
||||||
"url": "https://github.com/chalk/wrap-ansi?sponsor=1"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/wrap-ansi/node_modules/ansi-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/ws": {
|
|
||||||
"version": "8.19.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/ws/-/ws-8.19.0.tgz",
|
|
||||||
"integrity": "sha512-blAT2mjOEIi0ZzruJfIhb3nps74PRWTCz1IjglWEEpQl5XS/UNama6u2/rjFkDDouqr4L67ry+1aGIALViWjDg==",
|
|
||||||
"license": "MIT",
|
|
||||||
"engines": {
|
|
||||||
"node": ">=10.0.0"
|
|
||||||
},
|
|
||||||
"peerDependencies": {
|
|
||||||
"bufferutil": "^4.0.1",
|
|
||||||
"utf-8-validate": ">=5.0.2"
|
|
||||||
},
|
|
||||||
"peerDependenciesMeta": {
|
|
||||||
"bufferutil": {
|
|
||||||
"optional": true
|
|
||||||
},
|
|
||||||
"utf-8-validate": {
|
|
||||||
"optional": true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/yaml": {
|
"node_modules/yaml": {
|
||||||
"version": "2.8.2",
|
"version": "2.8.2",
|
||||||
"resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.2.tgz",
|
"resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.2.tgz",
|
||||||
@ -3913,12 +3278,6 @@
|
|||||||
"url": "https://github.com/sponsors/sindresorhus"
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/yoga-layout": {
|
|
||||||
"version": "3.2.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/yoga-layout/-/yoga-layout-3.2.1.tgz",
|
|
||||||
"integrity": "sha512-0LPOt3AxKqMdFBZA3HBAt/t/8vIKq7VaQYbuA8WxCgung+p9TVyKRYdpvCb80HcdTN2NkbIKbhNwKUfm3tQywQ==",
|
|
||||||
"license": "MIT"
|
|
||||||
},
|
|
||||||
"node_modules/zod": {
|
"node_modules/zod": {
|
||||||
"version": "4.3.6",
|
"version": "4.3.6",
|
||||||
"resolved": "https://registry.npmjs.org/zod/-/zod-4.3.6.tgz",
|
"resolved": "https://registry.npmjs.org/zod/-/zod-4.3.6.tgz",
|
||||||
|
|||||||
@ -20,7 +20,7 @@ vi.mock('node:os', async () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Mock the prompt to avoid interactive input
|
// Mock the prompt to avoid interactive input
|
||||||
vi.mock('../interactive/prompt.js', () => ({
|
vi.mock('../prompt/index.js', () => ({
|
||||||
selectOptionWithDefault: vi.fn().mockResolvedValue('ja'),
|
selectOptionWithDefault: vi.fn().mockResolvedValue('ja'),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
|||||||
@ -1,461 +0,0 @@
|
|||||||
/**
|
|
||||||
* Tests for input handling module
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
|
|
||||||
import { mkdirSync, rmSync, existsSync } from 'node:fs';
|
|
||||||
import { join } from 'node:path';
|
|
||||||
import { tmpdir } from 'node:os';
|
|
||||||
import { randomUUID } from 'node:crypto';
|
|
||||||
import {
|
|
||||||
InputHistoryManager,
|
|
||||||
EscapeSequenceTracker,
|
|
||||||
isMultilineInputTrigger,
|
|
||||||
hasBackslashContinuation,
|
|
||||||
removeBackslashContinuation,
|
|
||||||
type KeyEvent,
|
|
||||||
} from '../interactive/input.js';
|
|
||||||
import { loadInputHistory, saveInputHistory } from '../config/paths.js';
|
|
||||||
|
|
||||||
describe('InputHistoryManager', () => {
|
|
||||||
let testDir: string;
|
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
testDir = join(tmpdir(), `takt-test-${randomUUID()}`);
|
|
||||||
mkdirSync(testDir, { recursive: true });
|
|
||||||
});
|
|
||||||
|
|
||||||
afterEach(() => {
|
|
||||||
if (existsSync(testDir)) {
|
|
||||||
rmSync(testDir, { recursive: true, force: true });
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('constructor', () => {
|
|
||||||
it('should load existing history from file', () => {
|
|
||||||
saveInputHistory(testDir, ['entry1', 'entry2']);
|
|
||||||
|
|
||||||
const manager = new InputHistoryManager(testDir);
|
|
||||||
|
|
||||||
expect(manager.getHistory()).toEqual(['entry1', 'entry2']);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should start with empty history if no file exists', () => {
|
|
||||||
const manager = new InputHistoryManager(testDir);
|
|
||||||
|
|
||||||
expect(manager.getHistory()).toEqual([]);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should initialize index at end of history', () => {
|
|
||||||
saveInputHistory(testDir, ['entry1', 'entry2']);
|
|
||||||
|
|
||||||
const manager = new InputHistoryManager(testDir);
|
|
||||||
|
|
||||||
expect(manager.getIndex()).toBe(2);
|
|
||||||
expect(manager.isAtHistoryEntry()).toBe(false);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('add', () => {
|
|
||||||
it('should add entry and persist to file', () => {
|
|
||||||
const manager = new InputHistoryManager(testDir);
|
|
||||||
|
|
||||||
manager.add('new entry');
|
|
||||||
|
|
||||||
expect(manager.getHistory()).toEqual(['new entry']);
|
|
||||||
expect(loadInputHistory(testDir)).toEqual(['new entry']);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should not add consecutive duplicates', () => {
|
|
||||||
const manager = new InputHistoryManager(testDir);
|
|
||||||
|
|
||||||
manager.add('same');
|
|
||||||
manager.add('same');
|
|
||||||
|
|
||||||
expect(manager.getHistory()).toEqual(['same']);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should allow non-consecutive duplicates', () => {
|
|
||||||
const manager = new InputHistoryManager(testDir);
|
|
||||||
|
|
||||||
manager.add('first');
|
|
||||||
manager.add('second');
|
|
||||||
manager.add('first');
|
|
||||||
|
|
||||||
expect(manager.getHistory()).toEqual(['first', 'second', 'first']);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('navigation', () => {
|
|
||||||
it('should navigate to previous entry', () => {
|
|
||||||
saveInputHistory(testDir, ['entry1', 'entry2', 'entry3']);
|
|
||||||
const manager = new InputHistoryManager(testDir);
|
|
||||||
|
|
||||||
const entry1 = manager.navigatePrevious();
|
|
||||||
const entry2 = manager.navigatePrevious();
|
|
||||||
|
|
||||||
expect(entry1).toBe('entry3');
|
|
||||||
expect(entry2).toBe('entry2');
|
|
||||||
expect(manager.getIndex()).toBe(1);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should return undefined when at start of history', () => {
|
|
||||||
saveInputHistory(testDir, ['entry1']);
|
|
||||||
const manager = new InputHistoryManager(testDir);
|
|
||||||
|
|
||||||
manager.navigatePrevious(); // Move to entry1
|
|
||||||
const result = manager.navigatePrevious(); // Try to go further back
|
|
||||||
|
|
||||||
expect(result).toBeUndefined();
|
|
||||||
expect(manager.getIndex()).toBe(0);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should navigate to next entry', () => {
|
|
||||||
saveInputHistory(testDir, ['entry1', 'entry2']);
|
|
||||||
const manager = new InputHistoryManager(testDir);
|
|
||||||
|
|
||||||
manager.navigatePrevious(); // entry2
|
|
||||||
manager.navigatePrevious(); // entry1
|
|
||||||
const result = manager.navigateNext();
|
|
||||||
|
|
||||||
expect(result).toEqual({ entry: 'entry2', isCurrentInput: false });
|
|
||||||
expect(manager.getIndex()).toBe(1);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should return current input when navigating past end', () => {
|
|
||||||
saveInputHistory(testDir, ['entry1']);
|
|
||||||
const manager = new InputHistoryManager(testDir);
|
|
||||||
|
|
||||||
manager.saveCurrentInput('my current input');
|
|
||||||
manager.navigatePrevious(); // entry1
|
|
||||||
const result = manager.navigateNext(); // back to current
|
|
||||||
|
|
||||||
expect(result).toEqual({ entry: 'my current input', isCurrentInput: true });
|
|
||||||
expect(manager.isAtHistoryEntry()).toBe(false);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should return undefined when already at end', () => {
|
|
||||||
const manager = new InputHistoryManager(testDir);
|
|
||||||
|
|
||||||
const result = manager.navigateNext();
|
|
||||||
|
|
||||||
expect(result).toBeUndefined();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('resetIndex', () => {
|
|
||||||
it('should reset index to end of history', () => {
|
|
||||||
saveInputHistory(testDir, ['entry1', 'entry2']);
|
|
||||||
const manager = new InputHistoryManager(testDir);
|
|
||||||
|
|
||||||
manager.navigatePrevious();
|
|
||||||
manager.navigatePrevious();
|
|
||||||
manager.resetIndex();
|
|
||||||
|
|
||||||
expect(manager.getIndex()).toBe(2);
|
|
||||||
expect(manager.isAtHistoryEntry()).toBe(false);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should clear saved current input', () => {
|
|
||||||
const manager = new InputHistoryManager(testDir);
|
|
||||||
|
|
||||||
manager.saveCurrentInput('some input');
|
|
||||||
manager.resetIndex();
|
|
||||||
|
|
||||||
expect(manager.getCurrentInput()).toBe('');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('getCurrentEntry', () => {
|
|
||||||
it('should return entry at current index', () => {
|
|
||||||
saveInputHistory(testDir, ['entry1', 'entry2']);
|
|
||||||
const manager = new InputHistoryManager(testDir);
|
|
||||||
|
|
||||||
manager.navigatePrevious(); // entry2
|
|
||||||
|
|
||||||
expect(manager.getCurrentEntry()).toBe('entry2');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should return undefined when at end of history', () => {
|
|
||||||
saveInputHistory(testDir, ['entry1']);
|
|
||||||
const manager = new InputHistoryManager(testDir);
|
|
||||||
|
|
||||||
expect(manager.getCurrentEntry()).toBeUndefined();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('length', () => {
|
|
||||||
it('should return history length', () => {
|
|
||||||
saveInputHistory(testDir, ['entry1', 'entry2', 'entry3']);
|
|
||||||
const manager = new InputHistoryManager(testDir);
|
|
||||||
|
|
||||||
expect(manager.length).toBe(3);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should update after adding entries', () => {
|
|
||||||
const manager = new InputHistoryManager(testDir);
|
|
||||||
|
|
||||||
expect(manager.length).toBe(0);
|
|
||||||
|
|
||||||
manager.add('entry1');
|
|
||||||
expect(manager.length).toBe(1);
|
|
||||||
|
|
||||||
manager.add('entry2');
|
|
||||||
expect(manager.length).toBe(2);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('EscapeSequenceTracker', () => {
|
|
||||||
beforeEach(() => {
|
|
||||||
vi.useFakeTimers();
|
|
||||||
});
|
|
||||||
|
|
||||||
afterEach(() => {
|
|
||||||
vi.useRealTimers();
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('constructor', () => {
|
|
||||||
it('should use default threshold of 50ms', () => {
|
|
||||||
const tracker = new EscapeSequenceTracker();
|
|
||||||
expect(tracker.getThreshold()).toBe(50);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should accept custom threshold', () => {
|
|
||||||
const tracker = new EscapeSequenceTracker(100);
|
|
||||||
expect(tracker.getThreshold()).toBe(100);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('isEscapeThenEnter', () => {
|
|
||||||
it('should return true when Enter is pressed within threshold after Escape', () => {
|
|
||||||
const tracker = new EscapeSequenceTracker(50);
|
|
||||||
|
|
||||||
tracker.trackEscape();
|
|
||||||
vi.advanceTimersByTime(30); // 30ms later
|
|
||||||
|
|
||||||
expect(tracker.isEscapeThenEnter()).toBe(true);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should return false when Enter is pressed after threshold', () => {
|
|
||||||
const tracker = new EscapeSequenceTracker(50);
|
|
||||||
|
|
||||||
tracker.trackEscape();
|
|
||||||
vi.advanceTimersByTime(60); // 60ms later (exceeds 50ms threshold)
|
|
||||||
|
|
||||||
expect(tracker.isEscapeThenEnter()).toBe(false);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should return false when Escape was never pressed', () => {
|
|
||||||
const tracker = new EscapeSequenceTracker();
|
|
||||||
|
|
||||||
expect(tracker.isEscapeThenEnter()).toBe(false);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should reset after returning true (prevent repeated triggers)', () => {
|
|
||||||
const tracker = new EscapeSequenceTracker(50);
|
|
||||||
|
|
||||||
tracker.trackEscape();
|
|
||||||
vi.advanceTimersByTime(30);
|
|
||||||
|
|
||||||
expect(tracker.isEscapeThenEnter()).toBe(true);
|
|
||||||
// Second call should return false (already reset)
|
|
||||||
expect(tracker.isEscapeThenEnter()).toBe(false);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should not reset when returning false', () => {
|
|
||||||
const tracker = new EscapeSequenceTracker(50);
|
|
||||||
|
|
||||||
tracker.trackEscape();
|
|
||||||
vi.advanceTimersByTime(60); // Over threshold
|
|
||||||
|
|
||||||
expect(tracker.isEscapeThenEnter()).toBe(false);
|
|
||||||
// Tracker should still have lastEscapeTime = 0 after false return
|
|
||||||
// New escape tracking should work
|
|
||||||
tracker.trackEscape();
|
|
||||||
vi.advanceTimersByTime(30);
|
|
||||||
expect(tracker.isEscapeThenEnter()).toBe(true);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('reset', () => {
|
|
||||||
it('should clear the tracked escape time', () => {
|
|
||||||
const tracker = new EscapeSequenceTracker(50);
|
|
||||||
|
|
||||||
tracker.trackEscape();
|
|
||||||
tracker.reset();
|
|
||||||
vi.advanceTimersByTime(10); // Within threshold
|
|
||||||
|
|
||||||
expect(tracker.isEscapeThenEnter()).toBe(false);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('isMultilineInputTrigger', () => {
|
|
||||||
let tracker: EscapeSequenceTracker;
|
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
vi.useFakeTimers();
|
|
||||||
tracker = new EscapeSequenceTracker(50);
|
|
||||||
});
|
|
||||||
|
|
||||||
afterEach(() => {
|
|
||||||
vi.useRealTimers();
|
|
||||||
});
|
|
||||||
|
|
||||||
const createKey = (overrides: Partial<KeyEvent>): KeyEvent => ({
|
|
||||||
name: undefined,
|
|
||||||
ctrl: false,
|
|
||||||
meta: false,
|
|
||||||
shift: false,
|
|
||||||
sequence: undefined,
|
|
||||||
...overrides,
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('Ctrl+Enter', () => {
|
|
||||||
it('should return true for Ctrl+Enter', () => {
|
|
||||||
const key = createKey({ name: 'return', ctrl: true });
|
|
||||||
expect(isMultilineInputTrigger(key, tracker)).toBe(true);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should return true for Ctrl+Enter with "enter" name', () => {
|
|
||||||
const key = createKey({ name: 'enter', ctrl: true });
|
|
||||||
expect(isMultilineInputTrigger(key, tracker)).toBe(true);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('Ctrl+J', () => {
|
|
||||||
it('should return true for Ctrl+J', () => {
|
|
||||||
const key = createKey({ name: 'j', ctrl: true });
|
|
||||||
expect(isMultilineInputTrigger(key, tracker)).toBe(true);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should return true for Ctrl with linefeed sequence', () => {
|
|
||||||
const key = createKey({ ctrl: true, sequence: '\n' });
|
|
||||||
expect(isMultilineInputTrigger(key, tracker)).toBe(true);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('Option+Enter (meta flag)', () => {
|
|
||||||
it('should return true for meta+Enter', () => {
|
|
||||||
const key = createKey({ name: 'return', meta: true });
|
|
||||||
expect(isMultilineInputTrigger(key, tracker)).toBe(true);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('Shift+Enter', () => {
|
|
||||||
it('should return true for Shift+Enter', () => {
|
|
||||||
const key = createKey({ name: 'return', shift: true });
|
|
||||||
expect(isMultilineInputTrigger(key, tracker)).toBe(true);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('Escape sequences', () => {
|
|
||||||
it('should return true for \\x1b\\r sequence (Terminal.app)', () => {
|
|
||||||
const key = createKey({ sequence: '\x1b\r' });
|
|
||||||
expect(isMultilineInputTrigger(key, tracker)).toBe(true);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should return true for \\u001b\\r sequence', () => {
|
|
||||||
const key = createKey({ sequence: '\u001b\r' });
|
|
||||||
expect(isMultilineInputTrigger(key, tracker)).toBe(true);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should return true for \\x1bOM sequence', () => {
|
|
||||||
const key = createKey({ sequence: '\x1bOM' });
|
|
||||||
expect(isMultilineInputTrigger(key, tracker)).toBe(true);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('iTerm2-style Escape+Enter', () => {
|
|
||||||
it('should return true for Enter pressed within threshold after Escape', () => {
|
|
||||||
tracker.trackEscape();
|
|
||||||
vi.advanceTimersByTime(30);
|
|
||||||
|
|
||||||
const key = createKey({ name: 'return' });
|
|
||||||
expect(isMultilineInputTrigger(key, tracker)).toBe(true);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should return false for Enter pressed after threshold', () => {
|
|
||||||
tracker.trackEscape();
|
|
||||||
vi.advanceTimersByTime(60);
|
|
||||||
|
|
||||||
const key = createKey({ name: 'return' });
|
|
||||||
expect(isMultilineInputTrigger(key, tracker)).toBe(false);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('Non-trigger keys', () => {
|
|
||||||
it('should return false for plain Enter', () => {
|
|
||||||
const key = createKey({ name: 'return' });
|
|
||||||
expect(isMultilineInputTrigger(key, tracker)).toBe(false);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should return false for other keys', () => {
|
|
||||||
const key = createKey({ name: 'a' });
|
|
||||||
expect(isMultilineInputTrigger(key, tracker)).toBe(false);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should return false for Ctrl without Enter/J', () => {
|
|
||||||
const key = createKey({ name: 'k', ctrl: true });
|
|
||||||
expect(isMultilineInputTrigger(key, tracker)).toBe(false);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('hasBackslashContinuation', () => {
|
|
||||||
it('should return true for line ending with single backslash', () => {
|
|
||||||
expect(hasBackslashContinuation('hello world\\')).toBe(true);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should return false for line ending with double backslash (escaped)', () => {
|
|
||||||
expect(hasBackslashContinuation('hello world\\\\')).toBe(false);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should return true for line ending with triple backslash', () => {
|
|
||||||
expect(hasBackslashContinuation('hello world\\\\\\')).toBe(true);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should return false for line without trailing backslash', () => {
|
|
||||||
expect(hasBackslashContinuation('hello world')).toBe(false);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should return false for empty line', () => {
|
|
||||||
expect(hasBackslashContinuation('')).toBe(false);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should return true for just a backslash', () => {
|
|
||||||
expect(hasBackslashContinuation('\\')).toBe(true);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should handle backslash in middle of line', () => {
|
|
||||||
expect(hasBackslashContinuation('path\\to\\file')).toBe(false);
|
|
||||||
expect(hasBackslashContinuation('path\\to\\file\\')).toBe(true);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('removeBackslashContinuation', () => {
|
|
||||||
it('should remove trailing backslash', () => {
|
|
||||||
expect(removeBackslashContinuation('hello world\\')).toBe('hello world');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should not modify line without trailing backslash', () => {
|
|
||||||
expect(removeBackslashContinuation('hello world')).toBe('hello world');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should not remove escaped backslash (double)', () => {
|
|
||||||
expect(removeBackslashContinuation('hello world\\\\')).toBe('hello world\\\\');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should remove only the continuation backslash from triple', () => {
|
|
||||||
expect(removeBackslashContinuation('hello world\\\\\\')).toBe('hello world\\\\');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should handle empty string', () => {
|
|
||||||
expect(removeBackslashContinuation('')).toBe('');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should handle just a backslash', () => {
|
|
||||||
expect(removeBackslashContinuation('\\')).toBe('');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@ -1,291 +0,0 @@
|
|||||||
/**
|
|
||||||
* Tests for multiline input state handling logic
|
|
||||||
*
|
|
||||||
* Tests the pure functions that handle state transformations for multiline text editing.
|
|
||||||
* Key detection logic has been moved to useRawKeypress.ts - see rawKeypress.test.ts for those tests.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { describe, it, expect } from 'vitest';
|
|
||||||
import {
|
|
||||||
handleCharacterInput,
|
|
||||||
handleNewLine,
|
|
||||||
handleBackspace,
|
|
||||||
handleLeftArrow,
|
|
||||||
handleRightArrow,
|
|
||||||
handleUpArrow,
|
|
||||||
handleDownArrow,
|
|
||||||
getFullInput,
|
|
||||||
createInitialState,
|
|
||||||
type MultilineInputState,
|
|
||||||
} from '../interactive/multilineInputLogic.js';
|
|
||||||
|
|
||||||
// Helper to create state
|
|
||||||
function createState(overrides: Partial<MultilineInputState> = {}): MultilineInputState {
|
|
||||||
return {
|
|
||||||
lines: [''],
|
|
||||||
currentLine: 0,
|
|
||||||
cursor: 0,
|
|
||||||
...overrides,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
describe('Character input handling', () => {
|
|
||||||
it('should insert single character at cursor position', () => {
|
|
||||||
const state = createState({ lines: ['hello'], cursor: 5 });
|
|
||||||
const result = handleCharacterInput(state, 'x');
|
|
||||||
|
|
||||||
expect(result.lines).toEqual(['hellox']);
|
|
||||||
expect(result.cursor).toBe(6);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should insert character in middle of text', () => {
|
|
||||||
const state = createState({ lines: ['helo'], cursor: 2 });
|
|
||||||
const result = handleCharacterInput(state, 'l');
|
|
||||||
|
|
||||||
expect(result.lines).toEqual(['hello']);
|
|
||||||
expect(result.cursor).toBe(3);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should handle multi-byte characters (Japanese)', () => {
|
|
||||||
const state = createState({ lines: [''], cursor: 0 });
|
|
||||||
const result = handleCharacterInput(state, 'こんにちは');
|
|
||||||
|
|
||||||
expect(result.lines).toEqual(['こんにちは']);
|
|
||||||
expect(result.cursor).toBe(5); // 5 characters
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should insert multi-byte characters at correct position', () => {
|
|
||||||
const state = createState({ lines: ['Hello'], cursor: 5 });
|
|
||||||
const result = handleCharacterInput(state, '日本語');
|
|
||||||
|
|
||||||
expect(result.lines).toEqual(['Hello日本語']);
|
|
||||||
expect(result.cursor).toBe(8); // 5 + 3 characters
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should handle empty input', () => {
|
|
||||||
const state = createState({ lines: ['test'], cursor: 4 });
|
|
||||||
const result = handleCharacterInput(state, '');
|
|
||||||
|
|
||||||
expect(result).toEqual(state);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('New line handling', () => {
|
|
||||||
it('should split line at cursor position', () => {
|
|
||||||
const state = createState({ lines: ['hello world'], cursor: 5 });
|
|
||||||
const result = handleNewLine(state);
|
|
||||||
|
|
||||||
expect(result.lines).toEqual(['hello', ' world']);
|
|
||||||
expect(result.currentLine).toBe(1);
|
|
||||||
expect(result.cursor).toBe(0);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should add empty line at end', () => {
|
|
||||||
const state = createState({ lines: ['hello'], cursor: 5 });
|
|
||||||
const result = handleNewLine(state);
|
|
||||||
|
|
||||||
expect(result.lines).toEqual(['hello', '']);
|
|
||||||
expect(result.currentLine).toBe(1);
|
|
||||||
expect(result.cursor).toBe(0);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should add empty line at start', () => {
|
|
||||||
const state = createState({ lines: ['hello'], cursor: 0 });
|
|
||||||
const result = handleNewLine(state);
|
|
||||||
|
|
||||||
expect(result.lines).toEqual(['', 'hello']);
|
|
||||||
expect(result.currentLine).toBe(1);
|
|
||||||
expect(result.cursor).toBe(0);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('Backspace handling', () => {
|
|
||||||
it('should delete character before cursor', () => {
|
|
||||||
const state = createState({ lines: ['hello'], cursor: 5 });
|
|
||||||
const result = handleBackspace(state);
|
|
||||||
|
|
||||||
expect(result.lines).toEqual(['hell']);
|
|
||||||
expect(result.cursor).toBe(4);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should delete character in middle of text', () => {
|
|
||||||
const state = createState({ lines: ['hello'], cursor: 3 });
|
|
||||||
const result = handleBackspace(state);
|
|
||||||
|
|
||||||
expect(result.lines).toEqual(['helo']);
|
|
||||||
expect(result.cursor).toBe(2);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should merge lines when at start of line', () => {
|
|
||||||
const state = createState({
|
|
||||||
lines: ['line1', 'line2'],
|
|
||||||
currentLine: 1,
|
|
||||||
cursor: 0,
|
|
||||||
});
|
|
||||||
const result = handleBackspace(state);
|
|
||||||
|
|
||||||
expect(result.lines).toEqual(['line1line2']);
|
|
||||||
expect(result.currentLine).toBe(0);
|
|
||||||
expect(result.cursor).toBe(5); // After 'line1'
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should do nothing at start of first line', () => {
|
|
||||||
const state = createState({ lines: ['hello'], cursor: 0 });
|
|
||||||
const result = handleBackspace(state);
|
|
||||||
|
|
||||||
expect(result).toEqual(state);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('Arrow key navigation', () => {
|
|
||||||
describe('Left arrow', () => {
|
|
||||||
it('should move cursor left', () => {
|
|
||||||
const state = createState({ lines: ['hello'], cursor: 3 });
|
|
||||||
const result = handleLeftArrow(state);
|
|
||||||
|
|
||||||
expect(result.cursor).toBe(2);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should move to previous line at start', () => {
|
|
||||||
const state = createState({
|
|
||||||
lines: ['line1', 'line2'],
|
|
||||||
currentLine: 1,
|
|
||||||
cursor: 0,
|
|
||||||
});
|
|
||||||
const result = handleLeftArrow(state);
|
|
||||||
|
|
||||||
expect(result.currentLine).toBe(0);
|
|
||||||
expect(result.cursor).toBe(5);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should do nothing at start of first line', () => {
|
|
||||||
const state = createState({ lines: ['hello'], cursor: 0 });
|
|
||||||
const result = handleLeftArrow(state);
|
|
||||||
|
|
||||||
expect(result).toEqual(state);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('Right arrow', () => {
|
|
||||||
it('should move cursor right', () => {
|
|
||||||
const state = createState({ lines: ['hello'], cursor: 2 });
|
|
||||||
const result = handleRightArrow(state);
|
|
||||||
|
|
||||||
expect(result.cursor).toBe(3);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should move to next line at end', () => {
|
|
||||||
const state = createState({
|
|
||||||
lines: ['line1', 'line2'],
|
|
||||||
currentLine: 0,
|
|
||||||
cursor: 5,
|
|
||||||
});
|
|
||||||
const result = handleRightArrow(state);
|
|
||||||
|
|
||||||
expect(result.currentLine).toBe(1);
|
|
||||||
expect(result.cursor).toBe(0);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should do nothing at end of last line', () => {
|
|
||||||
const state = createState({ lines: ['hello'], cursor: 5 });
|
|
||||||
const result = handleRightArrow(state);
|
|
||||||
|
|
||||||
expect(result).toEqual(state);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('Up arrow', () => {
|
|
||||||
it('should move to previous line', () => {
|
|
||||||
const state = createState({
|
|
||||||
lines: ['line1', 'line2'],
|
|
||||||
currentLine: 1,
|
|
||||||
cursor: 3,
|
|
||||||
});
|
|
||||||
const result = handleUpArrow(state);
|
|
||||||
|
|
||||||
expect(result.currentLine).toBe(0);
|
|
||||||
expect(result.cursor).toBe(3);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should adjust cursor if previous line is shorter', () => {
|
|
||||||
const state = createState({
|
|
||||||
lines: ['ab', 'longer'],
|
|
||||||
currentLine: 1,
|
|
||||||
cursor: 5,
|
|
||||||
});
|
|
||||||
const result = handleUpArrow(state);
|
|
||||||
|
|
||||||
expect(result.currentLine).toBe(0);
|
|
||||||
expect(result.cursor).toBe(2);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should do nothing on first line', () => {
|
|
||||||
const state = createState({ lines: ['hello'], cursor: 3 });
|
|
||||||
const result = handleUpArrow(state);
|
|
||||||
|
|
||||||
expect(result).toEqual(state);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('Down arrow', () => {
|
|
||||||
it('should move to next line', () => {
|
|
||||||
const state = createState({
|
|
||||||
lines: ['line1', 'line2'],
|
|
||||||
currentLine: 0,
|
|
||||||
cursor: 3,
|
|
||||||
});
|
|
||||||
const result = handleDownArrow(state);
|
|
||||||
|
|
||||||
expect(result.currentLine).toBe(1);
|
|
||||||
expect(result.cursor).toBe(3);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should adjust cursor if next line is shorter', () => {
|
|
||||||
const state = createState({
|
|
||||||
lines: ['longer', 'ab'],
|
|
||||||
currentLine: 0,
|
|
||||||
cursor: 5,
|
|
||||||
});
|
|
||||||
const result = handleDownArrow(state);
|
|
||||||
|
|
||||||
expect(result.currentLine).toBe(1);
|
|
||||||
expect(result.cursor).toBe(2);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should do nothing on last line', () => {
|
|
||||||
const state = createState({ lines: ['hello'], cursor: 3 });
|
|
||||||
const result = handleDownArrow(state);
|
|
||||||
|
|
||||||
expect(result).toEqual(state);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('Utility functions', () => {
|
|
||||||
describe('getFullInput', () => {
|
|
||||||
it('should join lines with newlines', () => {
|
|
||||||
const state = createState({ lines: ['line1', 'line2', 'line3'] });
|
|
||||||
expect(getFullInput(state)).toBe('line1\nline2\nline3');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should trim whitespace', () => {
|
|
||||||
const state = createState({ lines: [' hello ', ''] });
|
|
||||||
expect(getFullInput(state)).toBe('hello');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should return empty string for empty input', () => {
|
|
||||||
const state = createState({ lines: ['', ' ', ''] });
|
|
||||||
expect(getFullInput(state)).toBe('');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('createInitialState', () => {
|
|
||||||
it('should create empty state', () => {
|
|
||||||
const state = createInitialState();
|
|
||||||
|
|
||||||
expect(state.lines).toEqual(['']);
|
|
||||||
expect(state.currentLine).toBe(0);
|
|
||||||
expect(state.cursor).toBe(0);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@ -100,10 +100,11 @@ export async function runCustomAgent(
|
|||||||
|
|
||||||
// Custom agent with prompt
|
// Custom agent with prompt
|
||||||
const systemPrompt = loadAgentPrompt(agentConfig);
|
const systemPrompt = loadAgentPrompt(agentConfig);
|
||||||
|
const tools = agentConfig.allowedTools || ['Read', 'Glob', 'Grep', 'WebSearch', 'WebFetch'];
|
||||||
const callOptions: ClaudeCallOptions = {
|
const callOptions: ClaudeCallOptions = {
|
||||||
cwd: options.cwd,
|
cwd: options.cwd,
|
||||||
sessionId: options.sessionId,
|
sessionId: options.sessionId,
|
||||||
allowedTools: agentConfig.allowedTools || ['Read', 'Glob', 'Grep', 'WebSearch', 'WebFetch'],
|
allowedTools: tools,
|
||||||
model: options.model || agentConfig.model,
|
model: options.model || agentConfig.model,
|
||||||
statusPatterns: agentConfig.statusPatterns,
|
statusPatterns: agentConfig.statusPatterns,
|
||||||
onStream: options.onStream,
|
onStream: options.onStream,
|
||||||
|
|||||||
10
src/cli.ts
10
src/cli.ts
@ -10,6 +10,7 @@
|
|||||||
* takt /switch - Switch workflow interactively
|
* takt /switch - Switch workflow interactively
|
||||||
* takt /clear - Clear agent conversation sessions
|
* takt /clear - Clear agent conversation sessions
|
||||||
* takt /help - Show help
|
* takt /help - Show help
|
||||||
|
* takt /config - Select permission mode interactively
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { Command } from 'commander';
|
import { Command } from 'commander';
|
||||||
@ -27,9 +28,10 @@ import {
|
|||||||
runAllTasks,
|
runAllTasks,
|
||||||
showHelp,
|
showHelp,
|
||||||
switchWorkflow,
|
switchWorkflow,
|
||||||
|
switchConfig,
|
||||||
} from './commands/index.js';
|
} from './commands/index.js';
|
||||||
import { listWorkflows } from './config/workflowLoader.js';
|
import { listWorkflows } from './config/workflowLoader.js';
|
||||||
import { selectOptionWithDefault } from './interactive/prompt.js';
|
import { selectOptionWithDefault } from './prompt/index.js';
|
||||||
import { DEFAULT_WORKFLOW_NAME } from './constants.js';
|
import { DEFAULT_WORKFLOW_NAME } from './constants.js';
|
||||||
|
|
||||||
const log = createLogger('cli');
|
const log = createLogger('cli');
|
||||||
@ -97,9 +99,13 @@ program
|
|||||||
showHelp();
|
showHelp();
|
||||||
return;
|
return;
|
||||||
|
|
||||||
|
case 'config':
|
||||||
|
await switchConfig(cwd, args[0]);
|
||||||
|
return;
|
||||||
|
|
||||||
default:
|
default:
|
||||||
error(`Unknown command: /${command}`);
|
error(`Unknown command: /${command}`);
|
||||||
info('Available: /run-tasks, /switch, /clear, /help');
|
info('Available: /run-tasks, /switch, /clear, /help, /config');
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
141
src/commands/config.ts
Normal file
141
src/commands/config.ts
Normal file
@ -0,0 +1,141 @@
|
|||||||
|
/**
|
||||||
|
* Config switching command (like workflow switching)
|
||||||
|
*
|
||||||
|
* Permission mode selection that works from CLI.
|
||||||
|
* Uses selectOption for prompt selection, same pattern as switchWorkflow.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import chalk from 'chalk';
|
||||||
|
import { info, success } from '../utils/ui.js';
|
||||||
|
import { selectOption } from '../prompt/index.js';
|
||||||
|
import {
|
||||||
|
loadProjectConfig,
|
||||||
|
updateProjectConfig,
|
||||||
|
type PermissionMode,
|
||||||
|
} from '../config/projectConfig.js';
|
||||||
|
|
||||||
|
// Re-export for convenience
|
||||||
|
export type { PermissionMode } from '../config/projectConfig.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get permission mode options for selection
|
||||||
|
*/
|
||||||
|
/** Common permission mode option definitions */
|
||||||
|
export const PERMISSION_MODE_OPTIONS: {
|
||||||
|
key: PermissionMode;
|
||||||
|
label: string;
|
||||||
|
description: string;
|
||||||
|
details: string[];
|
||||||
|
icon: string;
|
||||||
|
}[] = [
|
||||||
|
{
|
||||||
|
key: 'default',
|
||||||
|
label: 'デフォルト (default)',
|
||||||
|
description: 'Agent SDK標準モード(ファイル編集自動承認、最小限の確認)',
|
||||||
|
details: [
|
||||||
|
'Claude Agent SDKの標準設定(acceptEdits)を使用',
|
||||||
|
'ファイル編集は自動承認され、確認プロンプトなしで実行',
|
||||||
|
'Bash等の危険な操作は権限確認が表示される',
|
||||||
|
'通常の開発作業に推奨',
|
||||||
|
],
|
||||||
|
icon: '📋',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'sacrifice-my-pc',
|
||||||
|
label: 'SACRIFICE-MY-PC',
|
||||||
|
description: '全ての権限リクエストが自動承認されます',
|
||||||
|
details: [
|
||||||
|
'⚠️ 警告: 全ての操作が確認なしで実行されます',
|
||||||
|
'Bash, ファイル削除, システム操作も自動承認',
|
||||||
|
'ブロック状態(判断待ち)も自動スキップ',
|
||||||
|
'完全自動化が必要な場合のみ使用してください',
|
||||||
|
],
|
||||||
|
icon: '💀',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
function getPermissionModeOptions(currentMode: PermissionMode): {
|
||||||
|
label: string;
|
||||||
|
value: PermissionMode;
|
||||||
|
description: string;
|
||||||
|
details: string[];
|
||||||
|
}[] {
|
||||||
|
return PERMISSION_MODE_OPTIONS.map((opt) => ({
|
||||||
|
label: currentMode === opt.key
|
||||||
|
? (opt.key === 'sacrifice-my-pc' ? chalk.red : chalk.blue)(`${opt.icon} ${opt.label}`) + ' (current)'
|
||||||
|
: (opt.key === 'sacrifice-my-pc' ? chalk.red : chalk.blue)(`${opt.icon} ${opt.label}`),
|
||||||
|
value: opt.key,
|
||||||
|
description: opt.description,
|
||||||
|
details: opt.details,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get current permission mode from project config
|
||||||
|
*/
|
||||||
|
export function getCurrentPermissionMode(cwd: string): PermissionMode {
|
||||||
|
const config = loadProjectConfig(cwd);
|
||||||
|
// Support both old sacrificeMode boolean and new permissionMode string
|
||||||
|
if (config.permissionMode) {
|
||||||
|
return config.permissionMode as PermissionMode;
|
||||||
|
}
|
||||||
|
// Legacy: convert sacrificeMode boolean to new format
|
||||||
|
if (config.sacrificeMode) {
|
||||||
|
return 'sacrifice-my-pc';
|
||||||
|
}
|
||||||
|
return 'default';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set permission mode in project config
|
||||||
|
*/
|
||||||
|
export function setPermissionMode(cwd: string, mode: PermissionMode): void {
|
||||||
|
updateProjectConfig(cwd, 'permissionMode', mode);
|
||||||
|
// @deprecated TODO: Remove in v1.0 - legacy sacrificeMode for backwards compatibility
|
||||||
|
updateProjectConfig(cwd, 'sacrificeMode', mode === 'sacrifice-my-pc');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Switch permission mode (like switchWorkflow)
|
||||||
|
* @returns true if switch was successful
|
||||||
|
*/
|
||||||
|
export async function switchConfig(cwd: string, modeName?: string): Promise<boolean> {
|
||||||
|
const currentMode = getCurrentPermissionMode(cwd);
|
||||||
|
|
||||||
|
// No mode specified - show selection prompt
|
||||||
|
if (!modeName) {
|
||||||
|
info(`Current mode: ${currentMode}`);
|
||||||
|
|
||||||
|
const options = getPermissionModeOptions(currentMode);
|
||||||
|
const selected = await selectOption('Select permission mode:', options);
|
||||||
|
|
||||||
|
if (!selected) {
|
||||||
|
info('Cancelled');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
modeName = selected;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate mode name
|
||||||
|
if (modeName !== 'default' && modeName !== 'sacrifice-my-pc') {
|
||||||
|
info(`Invalid mode: ${modeName}`);
|
||||||
|
info('Available modes: default, sacrifice-my-pc');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const finalMode: PermissionMode = modeName as PermissionMode;
|
||||||
|
|
||||||
|
// Save to project config
|
||||||
|
setPermissionMode(cwd, finalMode);
|
||||||
|
|
||||||
|
if (finalMode === 'sacrifice-my-pc') {
|
||||||
|
success('Switched to: sacrifice-my-pc 💀');
|
||||||
|
info('All permission requests will be auto-approved.');
|
||||||
|
} else {
|
||||||
|
success('Switched to: default 📋');
|
||||||
|
info('Using Agent SDK default mode (acceptEdits - minimal permission prompts).');
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
@ -7,3 +7,4 @@ export { executeTask, runAllTasks, type ExecuteTaskOptions } from './taskExecuti
|
|||||||
export { showHelp } from './help.js';
|
export { showHelp } from './help.js';
|
||||||
export { withAgentSession } from './session.js';
|
export { withAgentSession } from './session.js';
|
||||||
export { switchWorkflow } from './workflow.js';
|
export { switchWorkflow } from './workflow.js';
|
||||||
|
export { switchConfig, getCurrentPermissionMode, setPermissionMode, type PermissionMode } from './config.js';
|
||||||
|
|||||||
@ -5,7 +5,7 @@
|
|||||||
import { listWorkflows, loadWorkflow, getBuiltinWorkflow } from '../config/index.js';
|
import { listWorkflows, loadWorkflow, getBuiltinWorkflow } from '../config/index.js';
|
||||||
import { getCurrentWorkflow, setCurrentWorkflow } from '../config/paths.js';
|
import { getCurrentWorkflow, setCurrentWorkflow } from '../config/paths.js';
|
||||||
import { info, success, error } from '../utils/ui.js';
|
import { info, success, error } from '../utils/ui.js';
|
||||||
import { selectOption } from '../interactive/prompt.js';
|
import { selectOption } from '../prompt/index.js';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get all available workflow options
|
* Get all available workflow options
|
||||||
|
|||||||
@ -8,7 +8,7 @@
|
|||||||
import { existsSync } from 'node:fs';
|
import { existsSync } from 'node:fs';
|
||||||
import type { Language } from '../models/types.js';
|
import type { Language } from '../models/types.js';
|
||||||
import { DEFAULT_LANGUAGE } from '../constants.js';
|
import { DEFAULT_LANGUAGE } from '../constants.js';
|
||||||
import { selectOptionWithDefault } from '../interactive/prompt.js';
|
import { selectOptionWithDefault } from '../prompt/index.js';
|
||||||
import {
|
import {
|
||||||
getGlobalConfigDir,
|
getGlobalConfigDir,
|
||||||
getGlobalAgentsDir,
|
getGlobalAgentsDir,
|
||||||
|
|||||||
@ -8,11 +8,24 @@ import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'node:fs';
|
|||||||
import { join, resolve } from 'node:path';
|
import { join, resolve } from 'node:path';
|
||||||
import { parse, stringify } from 'yaml';
|
import { parse, stringify } from 'yaml';
|
||||||
|
|
||||||
|
/** Permission mode for the project
|
||||||
|
* - default: Uses Agent SDK's acceptEdits mode (auto-accepts file edits, minimal prompts)
|
||||||
|
* - sacrifice-my-pc: Auto-approves all permission requests (bypassPermissions)
|
||||||
|
*
|
||||||
|
* Note: 'confirm' mode is planned but not yet implemented
|
||||||
|
*/
|
||||||
|
export type PermissionMode = 'default' | 'sacrifice-my-pc';
|
||||||
|
|
||||||
|
/** @deprecated Use PermissionMode instead */
|
||||||
|
export type ProjectPermissionMode = PermissionMode;
|
||||||
|
|
||||||
/** Project configuration stored in .takt/config.yaml */
|
/** Project configuration stored in .takt/config.yaml */
|
||||||
export interface ProjectLocalConfig {
|
export interface ProjectLocalConfig {
|
||||||
/** Current workflow name */
|
/** Current workflow name */
|
||||||
workflow?: string;
|
workflow?: string;
|
||||||
/** Auto-approve all permissions in this project */
|
/** Permission mode setting */
|
||||||
|
permissionMode?: PermissionMode;
|
||||||
|
/** @deprecated Use permissionMode instead. Auto-approve all permissions in this project */
|
||||||
sacrificeMode?: boolean;
|
sacrificeMode?: boolean;
|
||||||
/** Verbose output mode */
|
/** Verbose output mode */
|
||||||
verbose?: boolean;
|
verbose?: boolean;
|
||||||
@ -23,6 +36,7 @@ export interface ProjectLocalConfig {
|
|||||||
/** Default project configuration */
|
/** Default project configuration */
|
||||||
const DEFAULT_PROJECT_CONFIG: ProjectLocalConfig = {
|
const DEFAULT_PROJECT_CONFIG: ProjectLocalConfig = {
|
||||||
workflow: 'default',
|
workflow: 'default',
|
||||||
|
permissionMode: 'default',
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@ -1,80 +0,0 @@
|
|||||||
/**
|
|
||||||
* Agent operation commands
|
|
||||||
*
|
|
||||||
* Commands: /agents, /agent
|
|
||||||
*/
|
|
||||||
|
|
||||||
import chalk from 'chalk';
|
|
||||||
import { listCustomAgents } from '../../config/index.js';
|
|
||||||
import { runAgent } from '../../agents/runner.js';
|
|
||||||
import { info, error, status, list, StreamDisplay } from '../../utils/ui.js';
|
|
||||||
import { commandRegistry, createCommand } from './registry.js';
|
|
||||||
import { multiLineQuestion } from '../input.js';
|
|
||||||
|
|
||||||
/** /agents - List available agents */
|
|
||||||
commandRegistry.register(
|
|
||||||
createCommand(['agents'], 'List available agents', async (_) => {
|
|
||||||
const agents = listCustomAgents();
|
|
||||||
info('Built-in: coder, architect, supervisor');
|
|
||||||
if (agents.length > 0) {
|
|
||||||
info('Custom:');
|
|
||||||
list(agents);
|
|
||||||
}
|
|
||||||
return { continue: true };
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
/** /agent <name> - Run a single agent with next input */
|
|
||||||
commandRegistry.register(
|
|
||||||
createCommand(
|
|
||||||
['agent'],
|
|
||||||
'Run a single agent with next input',
|
|
||||||
async (args, state, rl) => {
|
|
||||||
if (args.length === 0) {
|
|
||||||
error('Usage: /agent <name>');
|
|
||||||
return { continue: true };
|
|
||||||
}
|
|
||||||
|
|
||||||
const agentName = args[0];
|
|
||||||
if (!agentName) {
|
|
||||||
error('Usage: /agent <name>');
|
|
||||||
return { continue: true };
|
|
||||||
}
|
|
||||||
|
|
||||||
info(`Next input will be sent to agent: ${agentName}`);
|
|
||||||
|
|
||||||
// Read next input for the agent using multiLineQuestion for multi-line support
|
|
||||||
const agentInput = await multiLineQuestion(rl, {
|
|
||||||
promptStr: chalk.cyan('Task> '),
|
|
||||||
onCtrlC: () => {
|
|
||||||
// Return true to cancel input and resolve with empty string
|
|
||||||
info('Cancelled');
|
|
||||||
return true;
|
|
||||||
},
|
|
||||||
historyManager: state.historyManager,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (agentInput.trim()) {
|
|
||||||
const display = new StreamDisplay(agentName);
|
|
||||||
const streamHandler = display.createHandler();
|
|
||||||
|
|
||||||
try {
|
|
||||||
const response = await runAgent(agentName, agentInput, {
|
|
||||||
cwd: state.cwd,
|
|
||||||
onStream: streamHandler,
|
|
||||||
});
|
|
||||||
display.flushThinking();
|
|
||||||
display.flushText();
|
|
||||||
console.log();
|
|
||||||
status('Status', response.status);
|
|
||||||
} catch (err) {
|
|
||||||
display.flushThinking();
|
|
||||||
display.flushText();
|
|
||||||
error(err instanceof Error ? err.message : String(err));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return { continue: true };
|
|
||||||
}
|
|
||||||
)
|
|
||||||
);
|
|
||||||
@ -1,103 +0,0 @@
|
|||||||
/**
|
|
||||||
* Basic commands
|
|
||||||
*
|
|
||||||
* Commands: /help, /h, /quit, /exit, /q, /sacrifice
|
|
||||||
*/
|
|
||||||
|
|
||||||
import chalk from 'chalk';
|
|
||||||
import { info, success } from '../../utils/ui.js';
|
|
||||||
import { commandRegistry, createCommand } from './registry.js';
|
|
||||||
import { printHelp } from '../ui.js';
|
|
||||||
|
|
||||||
/** /help, /h - Show help message */
|
|
||||||
commandRegistry.register(
|
|
||||||
createCommand(['help', 'h'], 'Show help message', async () => {
|
|
||||||
printHelp();
|
|
||||||
return { continue: true };
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
/** /quit, /exit, /q - Exit takt */
|
|
||||||
commandRegistry.register(
|
|
||||||
createCommand(['quit', 'exit', 'q'], 'Exit takt', async () => {
|
|
||||||
info('Goodbye!');
|
|
||||||
return { continue: false };
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
/** /sacrifice, /yolo - Toggle sacrifice-my-pc mode */
|
|
||||||
commandRegistry.register(
|
|
||||||
createCommand(
|
|
||||||
['sacrifice', 'yolo', 'sacrificemypc', 'sacrifice-my-pc'],
|
|
||||||
'Toggle sacrifice-my-pc mode (auto-approve everything)',
|
|
||||||
async (_args, state) => {
|
|
||||||
state.sacrificeMyPcMode = !state.sacrificeMyPcMode;
|
|
||||||
|
|
||||||
if (state.sacrificeMyPcMode) {
|
|
||||||
console.log();
|
|
||||||
console.log(chalk.red('━'.repeat(60)));
|
|
||||||
console.log(chalk.red.bold('⚠️ SACRIFICE-MY-PC MODE ENABLED ⚠️'));
|
|
||||||
console.log(chalk.red('━'.repeat(60)));
|
|
||||||
console.log(chalk.yellow('All permissions will be auto-approved.'));
|
|
||||||
console.log(chalk.yellow('Blocked states will be auto-skipped.'));
|
|
||||||
console.log(chalk.red('━'.repeat(60)));
|
|
||||||
console.log();
|
|
||||||
success('Sacrifice mode: ON - May your PC rest in peace 💀');
|
|
||||||
} else {
|
|
||||||
console.log();
|
|
||||||
info('Sacrifice mode: OFF - Normal confirmation mode restored');
|
|
||||||
}
|
|
||||||
|
|
||||||
return { continue: true };
|
|
||||||
}
|
|
||||||
)
|
|
||||||
);
|
|
||||||
|
|
||||||
/** /safe, /confirm - Disable sacrifice-my-pc mode and enable confirmation mode */
|
|
||||||
commandRegistry.register(
|
|
||||||
createCommand(
|
|
||||||
['safe', 'careful', 'confirm'],
|
|
||||||
'Enable confirmation mode (prompt for permissions)',
|
|
||||||
async (_args, state) => {
|
|
||||||
if (state.sacrificeMyPcMode) {
|
|
||||||
state.sacrificeMyPcMode = false;
|
|
||||||
console.log();
|
|
||||||
console.log(chalk.green('━'.repeat(60)));
|
|
||||||
console.log(chalk.green.bold('✓ 確認モードが有効になりました'));
|
|
||||||
console.log(chalk.green('━'.repeat(60)));
|
|
||||||
console.log(chalk.gray('権限リクエスト時に以下の選択肢が表示されます:'));
|
|
||||||
console.log(chalk.gray(' [y] 許可'));
|
|
||||||
console.log(chalk.gray(' [n] 拒否'));
|
|
||||||
console.log(chalk.gray(' [a] 今後も許可(セッション中)'));
|
|
||||||
console.log(chalk.gray(' [i] このイテレーションでこのコマンドを許可'));
|
|
||||||
console.log(chalk.gray(' [p] このイテレーションでこのコマンドパターンを許可'));
|
|
||||||
console.log(chalk.gray(' [s] このイテレーションでPC全権限譲渡'));
|
|
||||||
console.log(chalk.green('━'.repeat(60)));
|
|
||||||
console.log();
|
|
||||||
success('確認モード: ON - 権限リクエストが表示されます');
|
|
||||||
} else {
|
|
||||||
info('Already in confirmation mode');
|
|
||||||
}
|
|
||||||
return { continue: true };
|
|
||||||
}
|
|
||||||
)
|
|
||||||
);
|
|
||||||
|
|
||||||
/** /mode - Show current permission mode */
|
|
||||||
commandRegistry.register(
|
|
||||||
createCommand(['mode', 'status'], 'Show current permission mode', async (_args, state) => {
|
|
||||||
console.log();
|
|
||||||
if (state.sacrificeMyPcMode) {
|
|
||||||
console.log(chalk.red.bold('現在のモード: 💀 SACRIFICE-MY-PC MODE'));
|
|
||||||
console.log(chalk.yellow(' - 全ての権限リクエストが自動承認されます'));
|
|
||||||
console.log(chalk.yellow(' - ブロック状態は自動でスキップされます'));
|
|
||||||
console.log(chalk.gray('\n/confirm または /safe で確認モードに戻れます'));
|
|
||||||
} else {
|
|
||||||
console.log(chalk.green.bold('現在のモード: ✓ 確認モード'));
|
|
||||||
console.log(chalk.gray(' - 権限リクエスト時にプロンプトが表示されます'));
|
|
||||||
console.log(chalk.gray('\n/sacrifice で全自動モードに切り替えられます'));
|
|
||||||
}
|
|
||||||
console.log();
|
|
||||||
return { continue: true };
|
|
||||||
})
|
|
||||||
);
|
|
||||||
@ -1,15 +0,0 @@
|
|||||||
/**
|
|
||||||
* Command registry and all commands
|
|
||||||
*
|
|
||||||
* Import this module to register all commands with the registry.
|
|
||||||
*/
|
|
||||||
|
|
||||||
// Export registry
|
|
||||||
export { commandRegistry, type Command, type CommandResult } from './registry.js';
|
|
||||||
|
|
||||||
// Import all command modules to trigger registration
|
|
||||||
import './basic.js';
|
|
||||||
import './session.js';
|
|
||||||
import './workflow.js';
|
|
||||||
import './agent.js';
|
|
||||||
import './task.js';
|
|
||||||
@ -1,70 +0,0 @@
|
|||||||
/**
|
|
||||||
* Command registry for REPL
|
|
||||||
*
|
|
||||||
* Provides a Command pattern implementation for handling REPL commands.
|
|
||||||
* Commands are registered here and dispatched from the main REPL loop.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import type * as readline from 'node:readline';
|
|
||||||
import type { InteractiveState } from '../types.js';
|
|
||||||
|
|
||||||
/** Command execution result */
|
|
||||||
export interface CommandResult {
|
|
||||||
/** Whether to continue the REPL loop */
|
|
||||||
continue: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Command interface */
|
|
||||||
export interface Command {
|
|
||||||
/** Command name(s) - first is primary, rest are aliases */
|
|
||||||
names: string[];
|
|
||||||
/** Brief description for help */
|
|
||||||
description: string;
|
|
||||||
/** Execute the command */
|
|
||||||
execute(
|
|
||||||
args: string[],
|
|
||||||
state: InteractiveState,
|
|
||||||
rl: readline.Interface
|
|
||||||
): Promise<CommandResult>;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Command registry */
|
|
||||||
class CommandRegistry {
|
|
||||||
private commands: Map<string, Command> = new Map();
|
|
||||||
private allCommands: Command[] = [];
|
|
||||||
|
|
||||||
/** Register a command */
|
|
||||||
register(command: Command): void {
|
|
||||||
this.allCommands.push(command);
|
|
||||||
for (const name of command.names) {
|
|
||||||
this.commands.set(name.toLowerCase(), command);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Get a command by name */
|
|
||||||
get(name: string): Command | undefined {
|
|
||||||
return this.commands.get(name.toLowerCase());
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Get all registered commands */
|
|
||||||
getAll(): Command[] {
|
|
||||||
return this.allCommands;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Check if a command exists */
|
|
||||||
has(name: string): boolean {
|
|
||||||
return this.commands.has(name.toLowerCase());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Global command registry instance */
|
|
||||||
export const commandRegistry = new CommandRegistry();
|
|
||||||
|
|
||||||
/** Helper to create a simple command */
|
|
||||||
export function createCommand(
|
|
||||||
names: string[],
|
|
||||||
description: string,
|
|
||||||
execute: Command['execute']
|
|
||||||
): Command {
|
|
||||||
return { names, description, execute };
|
|
||||||
}
|
|
||||||
@ -1,93 +0,0 @@
|
|||||||
/**
|
|
||||||
* Session management commands
|
|
||||||
*
|
|
||||||
* Commands: /clear, /cls, /reset, /status, /history
|
|
||||||
*/
|
|
||||||
|
|
||||||
import chalk from 'chalk';
|
|
||||||
import { info, success, status, divider } from '../../utils/ui.js';
|
|
||||||
import { generateSessionId } from '../../utils/session.js';
|
|
||||||
import { setCurrentWorkflow } from '../../config/paths.js';
|
|
||||||
import { commandRegistry, createCommand } from './registry.js';
|
|
||||||
import { clearScreen, printWelcome } from '../ui.js';
|
|
||||||
|
|
||||||
/** /clear - Clear session and start fresh */
|
|
||||||
commandRegistry.register(
|
|
||||||
createCommand(['clear'], 'Clear session and start fresh', async (_, state) => {
|
|
||||||
state.claudeSessionId = undefined;
|
|
||||||
state.conversationHistory = [];
|
|
||||||
state.sessionId = generateSessionId();
|
|
||||||
clearScreen();
|
|
||||||
printWelcome(state);
|
|
||||||
success('Session cleared. Starting fresh.');
|
|
||||||
return { continue: true };
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
/** /cls - Clear screen only (keep session) */
|
|
||||||
commandRegistry.register(
|
|
||||||
createCommand(['cls'], 'Clear screen only (keep session)', async (_, state) => {
|
|
||||||
clearScreen();
|
|
||||||
printWelcome(state);
|
|
||||||
return { continue: true };
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
/** /reset - Full reset (session + workflow) */
|
|
||||||
commandRegistry.register(
|
|
||||||
createCommand(['reset'], 'Full reset (session + workflow)', async (_, state) => {
|
|
||||||
state.claudeSessionId = undefined;
|
|
||||||
state.conversationHistory = [];
|
|
||||||
state.sessionId = generateSessionId();
|
|
||||||
state.workflowName = 'default';
|
|
||||||
setCurrentWorkflow(state.cwd, 'default');
|
|
||||||
clearScreen();
|
|
||||||
printWelcome(state);
|
|
||||||
success('Session and workflow reset.');
|
|
||||||
return { continue: true };
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
/** /status - Show current session info */
|
|
||||||
commandRegistry.register(
|
|
||||||
createCommand(['status'], 'Show current session info', async (_, state) => {
|
|
||||||
divider();
|
|
||||||
status('Session ID', state.sessionId);
|
|
||||||
status('Workflow', state.workflowName);
|
|
||||||
status('Project', state.cwd);
|
|
||||||
status('History', `${state.conversationHistory.length} messages`);
|
|
||||||
status(
|
|
||||||
'Claude Session',
|
|
||||||
state.claudeSessionId || '(none - will create on first message)'
|
|
||||||
);
|
|
||||||
divider();
|
|
||||||
return { continue: true };
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
/** /history - Show conversation history */
|
|
||||||
commandRegistry.register(
|
|
||||||
createCommand(['history'], 'Show conversation history', async (_, state) => {
|
|
||||||
if (state.conversationHistory.length === 0) {
|
|
||||||
info('No conversation history');
|
|
||||||
} else {
|
|
||||||
divider('═', 60);
|
|
||||||
console.log(chalk.bold.magenta(' Conversation History'));
|
|
||||||
divider('═', 60);
|
|
||||||
state.conversationHistory.forEach((msg, i) => {
|
|
||||||
const roleColor = msg.role === 'user' ? chalk.cyan : chalk.green;
|
|
||||||
const roleLabel = msg.role === 'user' ? 'You' : 'Assistant';
|
|
||||||
const preview =
|
|
||||||
msg.content.length > 100
|
|
||||||
? msg.content.slice(0, 100) + '...'
|
|
||||||
: msg.content;
|
|
||||||
console.log();
|
|
||||||
console.log(roleColor(`[${i + 1}] ${roleLabel}:`));
|
|
||||||
console.log(chalk.gray(` ${preview}`));
|
|
||||||
});
|
|
||||||
console.log();
|
|
||||||
divider('═', 60);
|
|
||||||
}
|
|
||||||
return { continue: true };
|
|
||||||
})
|
|
||||||
);
|
|
||||||
@ -1,205 +0,0 @@
|
|||||||
/**
|
|
||||||
* Task execution commands
|
|
||||||
*
|
|
||||||
* Commands: /task, /t
|
|
||||||
*/
|
|
||||||
|
|
||||||
import chalk from 'chalk';
|
|
||||||
import { header, info, error, success, divider, StreamDisplay } from '../../utils/ui.js';
|
|
||||||
import { showTaskList, type TaskInfo, type TaskResult } from '../../task/index.js';
|
|
||||||
import { commandRegistry, createCommand } from './registry.js';
|
|
||||||
import { runAgent } from '../../agents/runner.js';
|
|
||||||
import type { InteractiveState } from '../types.js';
|
|
||||||
|
|
||||||
/** Execute a task using coder agent */
|
|
||||||
async function executeTaskWithAgent(
|
|
||||||
task: TaskInfo,
|
|
||||||
state: InteractiveState
|
|
||||||
): Promise<TaskResult> {
|
|
||||||
const startedAt = new Date().toISOString();
|
|
||||||
const executionLog: string[] = [];
|
|
||||||
|
|
||||||
console.log();
|
|
||||||
divider('=', 60);
|
|
||||||
header(`Task: ${task.name}`);
|
|
||||||
divider('=', 60);
|
|
||||||
console.log(chalk.cyan(`\n${task.content}\n`));
|
|
||||||
divider('-', 60);
|
|
||||||
|
|
||||||
let response: string;
|
|
||||||
let taskSuccess: boolean;
|
|
||||||
|
|
||||||
try {
|
|
||||||
// Use stream display for real-time output
|
|
||||||
const display = new StreamDisplay('coder');
|
|
||||||
const streamHandler = display.createHandler();
|
|
||||||
|
|
||||||
const result = await runAgent('coder', task.content, {
|
|
||||||
cwd: state.cwd,
|
|
||||||
onStream: (event) => {
|
|
||||||
if (event.type !== 'result') {
|
|
||||||
streamHandler(event);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
display.flush();
|
|
||||||
|
|
||||||
taskSuccess = result.status === 'done';
|
|
||||||
response = result.content;
|
|
||||||
executionLog.push(`Response received: ${response.length} chars`);
|
|
||||||
} catch (err) {
|
|
||||||
response = `[ERROR] Task execution error: ${err instanceof Error ? err.message : String(err)}`;
|
|
||||||
taskSuccess = false;
|
|
||||||
executionLog.push(`Error: ${err}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
const completedAt = new Date().toISOString();
|
|
||||||
|
|
||||||
return {
|
|
||||||
task,
|
|
||||||
success: taskSuccess,
|
|
||||||
response,
|
|
||||||
executionLog,
|
|
||||||
startedAt,
|
|
||||||
completedAt,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Handle /task list subcommand */
|
|
||||||
async function handleTaskList(state: InteractiveState): Promise<void> {
|
|
||||||
showTaskList(state.taskRunner);
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Execute a single task and return the result */
|
|
||||||
async function executeSingleTask(
|
|
||||||
task: TaskInfo,
|
|
||||||
state: InteractiveState
|
|
||||||
): Promise<{ result: TaskResult; reportFile: string }> {
|
|
||||||
// Execute the task
|
|
||||||
const result = await executeTaskWithAgent(task, state);
|
|
||||||
|
|
||||||
// Mark task as completed
|
|
||||||
const reportFile = state.taskRunner.completeTask(result);
|
|
||||||
|
|
||||||
console.log();
|
|
||||||
divider('=', 60);
|
|
||||||
if (result.success) {
|
|
||||||
success('Task completed');
|
|
||||||
} else {
|
|
||||||
error('Task failed');
|
|
||||||
}
|
|
||||||
divider('=', 60);
|
|
||||||
info(`Report: ${reportFile}`);
|
|
||||||
|
|
||||||
return { result, reportFile };
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Run all tasks starting from the given task */
|
|
||||||
async function runTasksFromStart(
|
|
||||||
startTask: TaskInfo,
|
|
||||||
state: InteractiveState
|
|
||||||
): Promise<void> {
|
|
||||||
let task: TaskInfo | null = startTask;
|
|
||||||
let completedCount = 0;
|
|
||||||
let failedCount = 0;
|
|
||||||
|
|
||||||
while (task) {
|
|
||||||
const { result } = await executeSingleTask(task, state);
|
|
||||||
|
|
||||||
if (result.success) {
|
|
||||||
completedCount++;
|
|
||||||
} else {
|
|
||||||
failedCount++;
|
|
||||||
}
|
|
||||||
|
|
||||||
task = state.taskRunner.getNextTask();
|
|
||||||
|
|
||||||
if (task) {
|
|
||||||
console.log();
|
|
||||||
info(`Proceeding to next task: ${task.name}`);
|
|
||||||
divider('-', 60);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log();
|
|
||||||
divider('=', 60);
|
|
||||||
success(`All tasks completed! (${completedCount} succeeded, ${failedCount} failed)`);
|
|
||||||
divider('=', 60);
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Handle /task run [name] subcommand - runs all pending tasks (optionally starting from a specific task) */
|
|
||||||
async function handleTaskRun(
|
|
||||||
taskName: string | undefined,
|
|
||||||
state: InteractiveState
|
|
||||||
): Promise<void> {
|
|
||||||
let task: TaskInfo | null;
|
|
||||||
|
|
||||||
if (taskName) {
|
|
||||||
task = state.taskRunner.getTask(taskName);
|
|
||||||
if (!task) {
|
|
||||||
error(`Task '${taskName}' not found`);
|
|
||||||
showTaskList(state.taskRunner);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
task = state.taskRunner.getNextTask();
|
|
||||||
if (!task) {
|
|
||||||
info('No pending tasks.');
|
|
||||||
console.log(
|
|
||||||
chalk.gray(`Place task files (.md) in ${state.taskRunner.getTasksDir()}/`)
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
await runTasksFromStart(task, state);
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Handle /task all subcommand - alias for /task run (for backward compatibility) */
|
|
||||||
async function handleTaskAll(state: InteractiveState): Promise<void> {
|
|
||||||
const task = state.taskRunner.getNextTask();
|
|
||||||
if (!task) {
|
|
||||||
info('No pending tasks.');
|
|
||||||
console.log(
|
|
||||||
chalk.gray(`Place task files (.md) in ${state.taskRunner.getTasksDir()}/`)
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
await runTasksFromStart(task, state);
|
|
||||||
}
|
|
||||||
|
|
||||||
/** /task, /t - Task management command */
|
|
||||||
commandRegistry.register(
|
|
||||||
createCommand(
|
|
||||||
['task', 't'],
|
|
||||||
'Task management (list/run)',
|
|
||||||
async (args, state) => {
|
|
||||||
const subcommand = args[0]?.toLowerCase() ?? '';
|
|
||||||
const subargs = args.slice(1).join(' ');
|
|
||||||
|
|
||||||
// /task or /task list - show task list
|
|
||||||
if (!subcommand || subcommand === 'list') {
|
|
||||||
await handleTaskList(state);
|
|
||||||
return { continue: true };
|
|
||||||
}
|
|
||||||
|
|
||||||
// /task run [name] - run all pending tasks (optionally starting from a specific task)
|
|
||||||
if (subcommand === 'run') {
|
|
||||||
await handleTaskRun(subargs || undefined, state);
|
|
||||||
return { continue: true };
|
|
||||||
}
|
|
||||||
|
|
||||||
// /task all - alias for /task run (backward compatibility)
|
|
||||||
if (subcommand === 'all') {
|
|
||||||
await handleTaskAll(state);
|
|
||||||
return { continue: true };
|
|
||||||
}
|
|
||||||
|
|
||||||
error(`Unknown subcommand: ${subcommand}`);
|
|
||||||
info('Usage: /task [list|run [name]|all]');
|
|
||||||
return { continue: true };
|
|
||||||
}
|
|
||||||
)
|
|
||||||
);
|
|
||||||
@ -1,74 +0,0 @@
|
|||||||
/**
|
|
||||||
* Workflow management commands
|
|
||||||
*
|
|
||||||
* Commands: /switch, /sw, /workflow, /workflows
|
|
||||||
*/
|
|
||||||
|
|
||||||
import {
|
|
||||||
loadWorkflow,
|
|
||||||
listWorkflows,
|
|
||||||
getBuiltinWorkflow,
|
|
||||||
} from '../../config/index.js';
|
|
||||||
import { setCurrentWorkflow } from '../../config/paths.js';
|
|
||||||
import { header, info, success, error, list } from '../../utils/ui.js';
|
|
||||||
import { commandRegistry, createCommand } from './registry.js';
|
|
||||||
import { selectWorkflow } from '../ui.js';
|
|
||||||
|
|
||||||
/** /switch, /sw - Interactive workflow selection */
|
|
||||||
commandRegistry.register(
|
|
||||||
createCommand(
|
|
||||||
['switch', 'sw'],
|
|
||||||
'Switch workflow (interactive selection)',
|
|
||||||
async (_, state, rl) => {
|
|
||||||
const selected = await selectWorkflow(state, rl);
|
|
||||||
if (selected) {
|
|
||||||
state.workflowName = selected;
|
|
||||||
setCurrentWorkflow(state.cwd, selected);
|
|
||||||
success(`Switched to workflow: ${selected}`);
|
|
||||||
info('Enter a task to start the workflow.');
|
|
||||||
}
|
|
||||||
return { continue: true };
|
|
||||||
}
|
|
||||||
)
|
|
||||||
);
|
|
||||||
|
|
||||||
/** /workflow [name] - Show or change current workflow */
|
|
||||||
commandRegistry.register(
|
|
||||||
createCommand(
|
|
||||||
['workflow'],
|
|
||||||
'Show or change current workflow',
|
|
||||||
async (args, state) => {
|
|
||||||
if (args.length > 0) {
|
|
||||||
const newWorkflow = args.join(' ');
|
|
||||||
// Check if it exists
|
|
||||||
const builtin = getBuiltinWorkflow(newWorkflow);
|
|
||||||
const custom = loadWorkflow(newWorkflow);
|
|
||||||
if (builtin || custom) {
|
|
||||||
state.workflowName = newWorkflow;
|
|
||||||
setCurrentWorkflow(state.cwd, newWorkflow);
|
|
||||||
success(`Switched to workflow: ${newWorkflow}`);
|
|
||||||
} else {
|
|
||||||
error(`Workflow not found: ${newWorkflow}`);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
info(`Current workflow: ${state.workflowName}`);
|
|
||||||
}
|
|
||||||
return { continue: true };
|
|
||||||
}
|
|
||||||
)
|
|
||||||
);
|
|
||||||
|
|
||||||
/** /workflows - List available workflows */
|
|
||||||
commandRegistry.register(
|
|
||||||
createCommand(['workflows'], 'List available workflows', async () => {
|
|
||||||
const workflows = listWorkflows();
|
|
||||||
if (workflows.length === 0) {
|
|
||||||
info('No workflows defined.');
|
|
||||||
info('Add workflow files to ~/.takt/workflows/');
|
|
||||||
} else {
|
|
||||||
header('Available Workflows');
|
|
||||||
list(workflows);
|
|
||||||
}
|
|
||||||
return { continue: true };
|
|
||||||
})
|
|
||||||
);
|
|
||||||
@ -1,57 +0,0 @@
|
|||||||
/**
|
|
||||||
* Escape sequence tracking for iTerm2-style Option+Enter detection
|
|
||||||
*
|
|
||||||
* iTerm2 (and some other Mac terminals) send Option+Enter as two separate events:
|
|
||||||
* Escape followed by Enter. This module provides a tracker to detect this pattern
|
|
||||||
* by checking if Enter is pressed shortly after Escape.
|
|
||||||
*/
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Tracks Escape key timing for detecting iTerm2-style Option+Enter.
|
|
||||||
*
|
|
||||||
* The threshold of 50ms is based on:
|
|
||||||
* - Typical keyboard repeat delay is 200-500ms
|
|
||||||
* - Terminal escape sequence transmission is near-instantaneous (<10ms)
|
|
||||||
* - Human intentional Esc→Enter would take at least 100-150ms
|
|
||||||
* - 50ms provides a safe margin to detect machine-generated sequences
|
|
||||||
* while avoiding false positives from intentional key presses
|
|
||||||
*/
|
|
||||||
export class EscapeSequenceTracker {
|
|
||||||
private lastEscapeTime: number = 0;
|
|
||||||
private readonly thresholdMs: number;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param thresholdMs Time window to consider Esc+Enter as Option+Enter (default: 50ms)
|
|
||||||
*/
|
|
||||||
constructor(thresholdMs: number = 50) {
|
|
||||||
this.thresholdMs = thresholdMs;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Record that Escape key was pressed */
|
|
||||||
trackEscape(): void {
|
|
||||||
this.lastEscapeTime = Date.now();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Check if Enter was pressed within threshold of Escape.
|
|
||||||
* Resets the tracker if true to prevent repeated triggers.
|
|
||||||
*/
|
|
||||||
isEscapeThenEnter(): boolean {
|
|
||||||
const elapsed = Date.now() - this.lastEscapeTime;
|
|
||||||
const isRecent = elapsed < this.thresholdMs && this.lastEscapeTime > 0;
|
|
||||||
if (isRecent) {
|
|
||||||
this.lastEscapeTime = 0; // Reset to prevent accidental triggers
|
|
||||||
}
|
|
||||||
return isRecent;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Reset the tracker state */
|
|
||||||
reset(): void {
|
|
||||||
this.lastEscapeTime = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Get the threshold value (for testing) */
|
|
||||||
getThreshold(): number {
|
|
||||||
return this.thresholdMs;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,218 +0,0 @@
|
|||||||
/**
|
|
||||||
* Interactive handlers for user questions and input
|
|
||||||
*
|
|
||||||
* Handles AskUserQuestion tool responses and user input prompts
|
|
||||||
* during workflow execution.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import chalk from 'chalk';
|
|
||||||
import type { InputHistoryManager } from './input.js';
|
|
||||||
import { multiLineQuestion, createReadlineInterface } from './input.js';
|
|
||||||
import type { AskUserQuestionInput, AskUserQuestionHandler } from '../claude/process.js';
|
|
||||||
import { runAgent } from '../agents/runner.js';
|
|
||||||
import { info } from '../utils/ui.js';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Create a handler that uses another agent to answer questions.
|
|
||||||
* This allows automatic question answering by delegating to a specified agent.
|
|
||||||
*/
|
|
||||||
export function createAgentAnswerHandler(
|
|
||||||
answerAgentName: string,
|
|
||||||
cwd: string
|
|
||||||
): AskUserQuestionHandler {
|
|
||||||
return async (input: AskUserQuestionInput): Promise<Record<string, string>> => {
|
|
||||||
const answers: Record<string, string> = {};
|
|
||||||
|
|
||||||
console.log();
|
|
||||||
console.log(chalk.magenta('━'.repeat(60)));
|
|
||||||
console.log(chalk.magenta.bold(`🤖 ${answerAgentName} が質問に回答します`));
|
|
||||||
console.log(chalk.magenta('━'.repeat(60)));
|
|
||||||
|
|
||||||
for (let i = 0; i < input.questions.length; i++) {
|
|
||||||
const q = input.questions[i];
|
|
||||||
if (!q) continue;
|
|
||||||
|
|
||||||
const questionKey = `q${i}`;
|
|
||||||
|
|
||||||
// Build a prompt for the answer agent
|
|
||||||
let prompt = `以下の質問に回答してください。回答のみを出力してください。\n\n`;
|
|
||||||
prompt += `質問: ${q.question}\n`;
|
|
||||||
|
|
||||||
if (q.options && q.options.length > 0) {
|
|
||||||
prompt += `\n選択肢:\n`;
|
|
||||||
q.options.forEach((opt, idx) => {
|
|
||||||
prompt += `${idx + 1}. ${opt.label}`;
|
|
||||||
if (opt.description) {
|
|
||||||
prompt += ` - ${opt.description}`;
|
|
||||||
}
|
|
||||||
prompt += '\n';
|
|
||||||
});
|
|
||||||
prompt += `\n選択肢の番号またはラベルで回答してください。選択肢以外の回答も可能です。`;
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log(chalk.gray(`質問: ${q.question}`));
|
|
||||||
|
|
||||||
try {
|
|
||||||
const response = await runAgent(answerAgentName, prompt, {
|
|
||||||
cwd,
|
|
||||||
// Don't use session for answer agent - each question is independent
|
|
||||||
});
|
|
||||||
|
|
||||||
// Extract the answer from agent response
|
|
||||||
const answerContent = response.content.trim();
|
|
||||||
|
|
||||||
// If the agent selected a numbered option, convert to label
|
|
||||||
const options = q.options;
|
|
||||||
if (options && options.length > 0) {
|
|
||||||
const num = parseInt(answerContent, 10);
|
|
||||||
if (num >= 1 && num <= options.length) {
|
|
||||||
const selectedOption = options[num - 1];
|
|
||||||
answers[questionKey] = selectedOption?.label ?? answerContent;
|
|
||||||
} else {
|
|
||||||
// Check if agent replied with exact label
|
|
||||||
const matchedOption = options.find(
|
|
||||||
opt => opt.label.toLowerCase() === answerContent.toLowerCase()
|
|
||||||
);
|
|
||||||
if (matchedOption) {
|
|
||||||
answers[questionKey] = matchedOption.label;
|
|
||||||
} else {
|
|
||||||
answers[questionKey] = answerContent;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
answers[questionKey] = answerContent;
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log(chalk.green(`回答: ${answers[questionKey]}`));
|
|
||||||
} catch (err) {
|
|
||||||
console.log(chalk.red(`エージェントエラー: ${err instanceof Error ? err.message : String(err)}`));
|
|
||||||
// Fall back to empty answer on error
|
|
||||||
answers[questionKey] = '';
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log();
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log(chalk.magenta('━'.repeat(60)));
|
|
||||||
console.log();
|
|
||||||
|
|
||||||
return answers;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Handle AskUserQuestion tool from Claude Code.
|
|
||||||
* Displays questions to the user and collects their answers.
|
|
||||||
*/
|
|
||||||
export function createAskUserQuestionHandler(
|
|
||||||
rl: ReturnType<typeof createReadlineInterface>,
|
|
||||||
historyManager: InputHistoryManager
|
|
||||||
): AskUserQuestionHandler {
|
|
||||||
return async (input: AskUserQuestionInput): Promise<Record<string, string>> => {
|
|
||||||
const answers: Record<string, string> = {};
|
|
||||||
|
|
||||||
console.log();
|
|
||||||
console.log(chalk.blue('━'.repeat(60)));
|
|
||||||
console.log(chalk.blue.bold('❓ Claude Code からの質問'));
|
|
||||||
console.log(chalk.blue('━'.repeat(60)));
|
|
||||||
console.log();
|
|
||||||
|
|
||||||
for (let i = 0; i < input.questions.length; i++) {
|
|
||||||
const q = input.questions[i];
|
|
||||||
if (!q) continue;
|
|
||||||
|
|
||||||
const questionKey = `q${i}`;
|
|
||||||
|
|
||||||
// Show the question
|
|
||||||
if (q.header) {
|
|
||||||
console.log(chalk.cyan.bold(`[${q.header}]`));
|
|
||||||
}
|
|
||||||
console.log(chalk.white(q.question));
|
|
||||||
|
|
||||||
// Show options if available
|
|
||||||
const options = q.options;
|
|
||||||
if (options && options.length > 0) {
|
|
||||||
console.log();
|
|
||||||
options.forEach((opt, idx) => {
|
|
||||||
const label = chalk.yellow(` ${idx + 1}. ${opt.label}`);
|
|
||||||
const desc = opt.description ? chalk.gray(` - ${opt.description}`) : '';
|
|
||||||
console.log(label + desc);
|
|
||||||
});
|
|
||||||
console.log(chalk.gray(` ${options.length + 1}. その他(自由入力)`));
|
|
||||||
console.log();
|
|
||||||
|
|
||||||
// Prompt for selection
|
|
||||||
const answer = await new Promise<string>((resolve) => {
|
|
||||||
multiLineQuestion(rl, {
|
|
||||||
promptStr: chalk.magenta('選択> '),
|
|
||||||
onCtrlC: () => {
|
|
||||||
resolve('');
|
|
||||||
return true;
|
|
||||||
},
|
|
||||||
historyManager,
|
|
||||||
}).then(resolve).catch(() => resolve(''));
|
|
||||||
});
|
|
||||||
|
|
||||||
const trimmed = answer.trim();
|
|
||||||
const num = parseInt(trimmed, 10);
|
|
||||||
|
|
||||||
if (num >= 1 && num <= options.length) {
|
|
||||||
// User selected an option
|
|
||||||
const selectedOption = options[num - 1];
|
|
||||||
answers[questionKey] = selectedOption?.label ?? '';
|
|
||||||
} else if (num === options.length + 1 || isNaN(num)) {
|
|
||||||
// User selected "Other" or entered free text
|
|
||||||
if (isNaN(num) && trimmed !== '') {
|
|
||||||
answers[questionKey] = trimmed;
|
|
||||||
} else {
|
|
||||||
console.log(chalk.cyan('自由入力してください:'));
|
|
||||||
const freeAnswer = await new Promise<string>((resolve) => {
|
|
||||||
multiLineQuestion(rl, {
|
|
||||||
promptStr: chalk.magenta('回答> '),
|
|
||||||
onCtrlC: () => {
|
|
||||||
resolve('');
|
|
||||||
return true;
|
|
||||||
},
|
|
||||||
historyManager,
|
|
||||||
}).then(resolve).catch(() => resolve(''));
|
|
||||||
});
|
|
||||||
answers[questionKey] = freeAnswer.trim();
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
answers[questionKey] = trimmed;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// No options, free text input
|
|
||||||
console.log();
|
|
||||||
const answer = await new Promise<string>((resolve) => {
|
|
||||||
multiLineQuestion(rl, {
|
|
||||||
promptStr: chalk.magenta('回答> '),
|
|
||||||
onCtrlC: () => {
|
|
||||||
resolve('');
|
|
||||||
return true;
|
|
||||||
},
|
|
||||||
historyManager,
|
|
||||||
}).then(resolve).catch(() => resolve(''));
|
|
||||||
});
|
|
||||||
answers[questionKey] = answer.trim();
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log();
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log(chalk.blue('━'.repeat(60)));
|
|
||||||
console.log();
|
|
||||||
|
|
||||||
return answers;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Create a handler for sacrifice mode that auto-skips all questions.
|
|
||||||
*/
|
|
||||||
export function createSacrificeModeQuestionHandler(): AskUserQuestionHandler {
|
|
||||||
return async (_input: AskUserQuestionInput): Promise<Record<string, string>> => {
|
|
||||||
info('[SACRIFICE MODE] Auto-skipping AskUserQuestion');
|
|
||||||
return {};
|
|
||||||
};
|
|
||||||
}
|
|
||||||
@ -1,107 +0,0 @@
|
|||||||
/**
|
|
||||||
* Input history management with persistence
|
|
||||||
*
|
|
||||||
* Manages input history for the interactive REPL, providing:
|
|
||||||
* - In-memory history for session use
|
|
||||||
* - Persistent storage for cross-session recall
|
|
||||||
* - Navigation through history entries
|
|
||||||
*/
|
|
||||||
|
|
||||||
import {
|
|
||||||
loadInputHistory,
|
|
||||||
saveInputHistory,
|
|
||||||
} from '../config/paths.js';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Manages input history with persistence.
|
|
||||||
* Provides a unified interface for in-memory and file-based history.
|
|
||||||
*/
|
|
||||||
export class InputHistoryManager {
|
|
||||||
private history: string[];
|
|
||||||
private readonly projectDir: string;
|
|
||||||
private index: number;
|
|
||||||
private currentInput: string;
|
|
||||||
|
|
||||||
constructor(projectDir: string) {
|
|
||||||
this.projectDir = projectDir;
|
|
||||||
this.history = loadInputHistory(projectDir);
|
|
||||||
this.index = this.history.length;
|
|
||||||
this.currentInput = '';
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Add an entry to history (both in-memory and persistent) */
|
|
||||||
add(input: string): void {
|
|
||||||
// Don't add consecutive duplicates
|
|
||||||
if (this.history[this.history.length - 1] !== input) {
|
|
||||||
this.history.push(input);
|
|
||||||
saveInputHistory(this.projectDir, this.history);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Get the current history array (read-only) */
|
|
||||||
getHistory(): readonly string[] {
|
|
||||||
return this.history;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Get the current history index */
|
|
||||||
getIndex(): number {
|
|
||||||
return this.index;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Reset index to the end of history */
|
|
||||||
resetIndex(): void {
|
|
||||||
this.index = this.history.length;
|
|
||||||
this.currentInput = '';
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Save the current input before navigating history */
|
|
||||||
saveCurrentInput(input: string): void {
|
|
||||||
if (this.index === this.history.length) {
|
|
||||||
this.currentInput = input;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Get the saved current input */
|
|
||||||
getCurrentInput(): string {
|
|
||||||
return this.currentInput;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Navigate to the previous entry. Returns the entry or undefined if at start. */
|
|
||||||
navigatePrevious(): string | undefined {
|
|
||||||
if (this.index > 0) {
|
|
||||||
this.index--;
|
|
||||||
return this.history[this.index];
|
|
||||||
}
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Navigate to the next entry. Returns the entry, current input at end, or undefined. */
|
|
||||||
navigateNext(): { entry: string; isCurrentInput: boolean } | undefined {
|
|
||||||
if (this.index < this.history.length) {
|
|
||||||
this.index++;
|
|
||||||
if (this.index === this.history.length) {
|
|
||||||
return { entry: this.currentInput, isCurrentInput: true };
|
|
||||||
}
|
|
||||||
const entry = this.history[this.index];
|
|
||||||
if (entry !== undefined) {
|
|
||||||
return { entry, isCurrentInput: false };
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Check if currently at a history entry (not at the end) */
|
|
||||||
isAtHistoryEntry(): boolean {
|
|
||||||
return this.index < this.history.length;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Get the entry at the current index */
|
|
||||||
getCurrentEntry(): string | undefined {
|
|
||||||
return this.history[this.index];
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Get the total number of history entries */
|
|
||||||
get length(): number {
|
|
||||||
return this.history.length;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,8 +0,0 @@
|
|||||||
/**
|
|
||||||
* Interactive module - exports REPL functionality
|
|
||||||
*/
|
|
||||||
|
|
||||||
export * from './repl.js';
|
|
||||||
export * from './input.js';
|
|
||||||
export * from './types.js';
|
|
||||||
export * from './ui.js';
|
|
||||||
@ -1,241 +0,0 @@
|
|||||||
/**
|
|
||||||
* Input event handlers for multiline input
|
|
||||||
*
|
|
||||||
* Contains keypress and line handlers used by multiLineQuestion.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import * as readline from 'node:readline';
|
|
||||||
import chalk from 'chalk';
|
|
||||||
import { createLogger } from '../utils/debug.js';
|
|
||||||
import { EscapeSequenceTracker } from './escape-tracker.js';
|
|
||||||
import { InputHistoryManager } from './history-manager.js';
|
|
||||||
|
|
||||||
const log = createLogger('input');
|
|
||||||
|
|
||||||
/** Key event interface for keypress handling */
|
|
||||||
export interface KeyEvent {
|
|
||||||
name?: string;
|
|
||||||
ctrl?: boolean;
|
|
||||||
meta?: boolean;
|
|
||||||
shift?: boolean;
|
|
||||||
sequence?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** State for multiline input session */
|
|
||||||
export interface MultilineInputState {
|
|
||||||
lines: string[];
|
|
||||||
insertNewlineOnNextLine: boolean;
|
|
||||||
isFirstLine: boolean;
|
|
||||||
promptStr: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Internal readline interface properties */
|
|
||||||
interface ReadlineInternal {
|
|
||||||
line?: string;
|
|
||||||
cursor?: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Get readline's internal line state */
|
|
||||||
export function getReadlineLine(rl: readline.Interface): string {
|
|
||||||
const internal = rl as unknown as ReadlineInternal;
|
|
||||||
return internal.line ?? '';
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Set readline's internal line and cursor state */
|
|
||||||
export function setReadlineState(rl: readline.Interface, line: string, cursor: number): void {
|
|
||||||
const internal = rl as unknown as ReadlineInternal;
|
|
||||||
if ('line' in (rl as object)) {
|
|
||||||
internal.line = line;
|
|
||||||
}
|
|
||||||
if ('cursor' in (rl as object)) {
|
|
||||||
internal.cursor = cursor;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Format a history entry for display (truncate multi-line entries) */
|
|
||||||
export function formatHistoryEntry(entry: string): string {
|
|
||||||
const firstLine = entry.split('\n')[0] ?? '';
|
|
||||||
const hasMoreLines = entry.includes('\n');
|
|
||||||
return hasMoreLines ? firstLine + ' ...' : firstLine;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Determines if a key event should trigger multi-line input mode.
|
|
||||||
*/
|
|
||||||
export function isMultilineInputTrigger(
|
|
||||||
key: KeyEvent,
|
|
||||||
escapeTracker: EscapeSequenceTracker
|
|
||||||
): boolean {
|
|
||||||
const isEnterKey = key.name === 'return' || key.name === 'enter';
|
|
||||||
const modifiedEnter = isEnterKey && (key.ctrl || key.meta || key.shift);
|
|
||||||
const isCtrlJ = key.ctrl && (key.name === 'j' || key.sequence === '\n');
|
|
||||||
const escapeSequences =
|
|
||||||
key.sequence === '\x1b\r' ||
|
|
||||||
key.sequence === '\u001b\r' ||
|
|
||||||
key.sequence === '\x1bOM' ||
|
|
||||||
key.sequence === '\u001bOM';
|
|
||||||
const iterm2Style = isEnterKey && escapeTracker.isEscapeThenEnter();
|
|
||||||
|
|
||||||
const result = modifiedEnter || isCtrlJ || escapeSequences || iterm2Style;
|
|
||||||
|
|
||||||
if (isEnterKey || escapeSequences) {
|
|
||||||
log.debug('isMultilineInputTrigger', {
|
|
||||||
isEnterKey, modifiedEnter, isCtrlJ, escapeSequences, iterm2Style, result,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Check if a line ends with backslash for line continuation */
|
|
||||||
export function hasBackslashContinuation(line: string): boolean {
|
|
||||||
let backslashCount = 0;
|
|
||||||
for (let i = line.length - 1; i >= 0 && line[i] === '\\'; i--) {
|
|
||||||
backslashCount++;
|
|
||||||
}
|
|
||||||
return backslashCount % 2 === 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Remove trailing backslash used for line continuation */
|
|
||||||
export function removeBackslashContinuation(line: string): string {
|
|
||||||
if (hasBackslashContinuation(line)) {
|
|
||||||
return line.slice(0, -1);
|
|
||||||
}
|
|
||||||
return line;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Create keypress handler for multiline input.
|
|
||||||
*/
|
|
||||||
export function createKeypressHandler(
|
|
||||||
rl: readline.Interface,
|
|
||||||
state: MultilineInputState,
|
|
||||||
escapeTracker: EscapeSequenceTracker,
|
|
||||||
historyManager: InputHistoryManager
|
|
||||||
): (str: string | undefined, key: KeyEvent) => void {
|
|
||||||
const replaceCurrentLine = (newContent: string): void => {
|
|
||||||
const currentLine = getReadlineLine(rl);
|
|
||||||
process.stdout.write('\r' + state.promptStr);
|
|
||||||
process.stdout.write(' '.repeat(currentLine.length));
|
|
||||||
process.stdout.write('\r' + state.promptStr + newContent);
|
|
||||||
setReadlineState(rl, newContent, newContent.length);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (_str: string | undefined, key: KeyEvent): void => {
|
|
||||||
if (!key) return;
|
|
||||||
|
|
||||||
const seqHex = key.sequence
|
|
||||||
? [...key.sequence].map(c => '0x' + c.charCodeAt(0).toString(16).padStart(2, '0')).join(' ')
|
|
||||||
: '(none)';
|
|
||||||
log.debug('keypress', {
|
|
||||||
name: key.name, sequence: seqHex, ctrl: key.ctrl, meta: key.meta, shift: key.shift,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (key.name === 'escape' || key.sequence === '\x1b') {
|
|
||||||
escapeTracker.trackEscape();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isMultilineInputTrigger(key, escapeTracker)) {
|
|
||||||
state.insertNewlineOnNextLine = true;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!state.isFirstLine) return;
|
|
||||||
|
|
||||||
if (key.name === 'up' && historyManager.length > 0) {
|
|
||||||
historyManager.saveCurrentInput(getReadlineLine(rl));
|
|
||||||
const entry = historyManager.navigatePrevious();
|
|
||||||
if (entry !== undefined) {
|
|
||||||
replaceCurrentLine(formatHistoryEntry(entry));
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (key.name === 'down') {
|
|
||||||
const result = historyManager.navigateNext();
|
|
||||||
if (result !== undefined) {
|
|
||||||
const displayText = result.isCurrentInput ? result.entry : formatHistoryEntry(result.entry);
|
|
||||||
replaceCurrentLine(displayText);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Create line handler for multiline input.
|
|
||||||
*/
|
|
||||||
export function createLineHandler(
|
|
||||||
rl: readline.Interface,
|
|
||||||
state: MultilineInputState,
|
|
||||||
historyManager: InputHistoryManager,
|
|
||||||
cleanup: () => void,
|
|
||||||
resolve: (value: string) => void
|
|
||||||
): (line: string) => void {
|
|
||||||
const showPrompt = (): void => {
|
|
||||||
const prefix = state.isFirstLine ? state.promptStr : chalk.gray('... ');
|
|
||||||
rl.setPrompt(prefix);
|
|
||||||
rl.prompt();
|
|
||||||
};
|
|
||||||
|
|
||||||
return (line: string): void => {
|
|
||||||
const hasBackslash = hasBackslashContinuation(line);
|
|
||||||
log.debug('handleLine', { line, insertNewlineOnNextLine: state.insertNewlineOnNextLine, hasBackslash });
|
|
||||||
|
|
||||||
if (state.insertNewlineOnNextLine || hasBackslash) {
|
|
||||||
const cleanLine = hasBackslash ? removeBackslashContinuation(line) : line;
|
|
||||||
state.lines.push(cleanLine);
|
|
||||||
state.isFirstLine = false;
|
|
||||||
state.insertNewlineOnNextLine = false;
|
|
||||||
showPrompt();
|
|
||||||
} else {
|
|
||||||
cleanup();
|
|
||||||
|
|
||||||
if (historyManager.isAtHistoryEntry() && state.isFirstLine) {
|
|
||||||
const historyEntry = historyManager.getCurrentEntry();
|
|
||||||
if (historyEntry !== undefined) {
|
|
||||||
resolve(historyEntry);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
state.lines.push(line);
|
|
||||||
resolve(state.lines.join('\n'));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Create SIGINT handler for multiline input.
|
|
||||||
*/
|
|
||||||
export function createSigintHandler(
|
|
||||||
rl: readline.Interface,
|
|
||||||
state: MultilineInputState,
|
|
||||||
historyManager: InputHistoryManager,
|
|
||||||
onCtrlC: () => boolean | void,
|
|
||||||
cleanup: () => void,
|
|
||||||
resolve: (value: string) => void
|
|
||||||
): () => void {
|
|
||||||
const showPrompt = (): void => {
|
|
||||||
const prefix = state.isFirstLine ? state.promptStr : chalk.gray('... ');
|
|
||||||
rl.setPrompt(prefix);
|
|
||||||
rl.prompt();
|
|
||||||
};
|
|
||||||
|
|
||||||
return (): void => {
|
|
||||||
if (state.lines.length > 0) {
|
|
||||||
state.lines.length = 0;
|
|
||||||
state.isFirstLine = true;
|
|
||||||
state.insertNewlineOnNextLine = false;
|
|
||||||
historyManager.resetIndex();
|
|
||||||
console.log();
|
|
||||||
showPrompt();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const shouldCancel = onCtrlC();
|
|
||||||
if (shouldCancel === true) {
|
|
||||||
cleanup();
|
|
||||||
resolve('');
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
@ -1,111 +0,0 @@
|
|||||||
/**
|
|
||||||
* Input handling module for takt interactive mode
|
|
||||||
*
|
|
||||||
* Handles readline interface, multi-line input, and input history management.
|
|
||||||
*
|
|
||||||
* Multi-line input methods:
|
|
||||||
* - Ctrl+J: Works on all terminals (recommended for mac Terminal.app)
|
|
||||||
* - Ctrl+Enter: Works on terminals that support it
|
|
||||||
* - Option+Enter: Works on iTerm2 and some other Mac terminals
|
|
||||||
* - Backslash continuation: End line with \ to continue on next line
|
|
||||||
*/
|
|
||||||
|
|
||||||
import * as readline from 'node:readline';
|
|
||||||
import { emitKeypressEvents } from 'node:readline';
|
|
||||||
import { EscapeSequenceTracker } from './escape-tracker.js';
|
|
||||||
import { InputHistoryManager } from './history-manager.js';
|
|
||||||
import {
|
|
||||||
createKeypressHandler,
|
|
||||||
createLineHandler,
|
|
||||||
createSigintHandler,
|
|
||||||
type MultilineInputState,
|
|
||||||
} from './input-handlers.js';
|
|
||||||
|
|
||||||
// Re-export for backward compatibility
|
|
||||||
export { EscapeSequenceTracker } from './escape-tracker.js';
|
|
||||||
export { InputHistoryManager } from './history-manager.js';
|
|
||||||
export {
|
|
||||||
isMultilineInputTrigger,
|
|
||||||
hasBackslashContinuation,
|
|
||||||
removeBackslashContinuation,
|
|
||||||
type KeyEvent,
|
|
||||||
} from './input-handlers.js';
|
|
||||||
|
|
||||||
/** Create readline interface with keypress support */
|
|
||||||
export function createReadlineInterface(): readline.Interface {
|
|
||||||
if (process.stdin.isTTY) {
|
|
||||||
emitKeypressEvents(process.stdin);
|
|
||||||
}
|
|
||||||
|
|
||||||
return readline.createInterface({
|
|
||||||
input: process.stdin,
|
|
||||||
output: process.stdout,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Options for multiLineQuestion */
|
|
||||||
export interface MultiLineQuestionOptions {
|
|
||||||
promptStr: string;
|
|
||||||
/**
|
|
||||||
* Callback when Ctrl+C is pressed on the first line (no accumulated input).
|
|
||||||
* Return `true` to cancel and resolve with empty string.
|
|
||||||
* Return `void` or `false` to continue input (REPL behavior).
|
|
||||||
*/
|
|
||||||
onCtrlC: () => boolean | void;
|
|
||||||
historyManager: InputHistoryManager;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Multi-line input using standard readline with Option+Return support and input history.
|
|
||||||
*
|
|
||||||
* This approach preserves all readline features (arrow keys, history, etc.)
|
|
||||||
* while adding multi-line support via keypress event interception.
|
|
||||||
*
|
|
||||||
* - Enter: submit input (execute)
|
|
||||||
* - Option+Enter (Mac) / Ctrl+Enter: insert newline (multi-line input)
|
|
||||||
* - Up Arrow: navigate to previous input in history
|
|
||||||
* - Down Arrow: navigate to next input in history
|
|
||||||
* - Ctrl+C: interrupt / cancel
|
|
||||||
*/
|
|
||||||
export function multiLineQuestion(
|
|
||||||
rl: readline.Interface,
|
|
||||||
options: MultiLineQuestionOptions
|
|
||||||
): Promise<string> {
|
|
||||||
const { promptStr, onCtrlC, historyManager } = options;
|
|
||||||
|
|
||||||
return new Promise((resolve) => {
|
|
||||||
const state: MultilineInputState = {
|
|
||||||
lines: [],
|
|
||||||
insertNewlineOnNextLine: false,
|
|
||||||
isFirstLine: true,
|
|
||||||
promptStr,
|
|
||||||
};
|
|
||||||
|
|
||||||
const escapeTracker = new EscapeSequenceTracker();
|
|
||||||
historyManager.resetIndex();
|
|
||||||
|
|
||||||
const cleanup = (): void => {
|
|
||||||
process.stdin.removeListener('keypress', handleKeypress);
|
|
||||||
rl.removeListener('line', handleLine);
|
|
||||||
rl.removeListener('close', handleClose);
|
|
||||||
rl.removeListener('SIGINT', handleSigint);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleKeypress = createKeypressHandler(rl, state, escapeTracker, historyManager);
|
|
||||||
const handleLine = createLineHandler(rl, state, historyManager, cleanup, resolve);
|
|
||||||
const handleSigint = createSigintHandler(rl, state, historyManager, onCtrlC, cleanup, resolve);
|
|
||||||
|
|
||||||
const handleClose = (): void => {
|
|
||||||
cleanup();
|
|
||||||
resolve(state.lines.length > 0 ? state.lines.join('\n') : '');
|
|
||||||
};
|
|
||||||
|
|
||||||
process.stdin.on('keypress', handleKeypress);
|
|
||||||
rl.on('line', handleLine);
|
|
||||||
rl.on('close', handleClose);
|
|
||||||
rl.on('SIGINT', handleSigint);
|
|
||||||
|
|
||||||
rl.setPrompt(promptStr);
|
|
||||||
rl.prompt();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
@ -1,176 +0,0 @@
|
|||||||
/**
|
|
||||||
* Multiline input state handling logic
|
|
||||||
*
|
|
||||||
* Pure functions for handling state transformations in multiline text editing.
|
|
||||||
*/
|
|
||||||
|
|
||||||
/** State for multiline input */
|
|
||||||
export interface MultilineInputState {
|
|
||||||
lines: string[];
|
|
||||||
currentLine: number;
|
|
||||||
cursor: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Create initial state */
|
|
||||||
export function createInitialState(): MultilineInputState {
|
|
||||||
return {
|
|
||||||
lines: [''],
|
|
||||||
currentLine: 0,
|
|
||||||
cursor: 0,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Get full input as single string (trimmed) */
|
|
||||||
export function getFullInput(state: MultilineInputState): string {
|
|
||||||
return state.lines.join('\n').trim();
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Handle character input */
|
|
||||||
export function handleCharacterInput(
|
|
||||||
state: MultilineInputState,
|
|
||||||
char: string
|
|
||||||
): MultilineInputState {
|
|
||||||
const { lines, currentLine, cursor } = state;
|
|
||||||
const line = lines[currentLine] || '';
|
|
||||||
|
|
||||||
const newLine = line.slice(0, cursor) + char + line.slice(cursor);
|
|
||||||
const newLines = [...lines];
|
|
||||||
newLines[currentLine] = newLine;
|
|
||||||
|
|
||||||
return {
|
|
||||||
lines: newLines,
|
|
||||||
currentLine,
|
|
||||||
cursor: cursor + char.length,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Handle newline insertion */
|
|
||||||
export function handleNewLine(state: MultilineInputState): MultilineInputState {
|
|
||||||
const { lines, currentLine, cursor } = state;
|
|
||||||
const line = lines[currentLine] || '';
|
|
||||||
|
|
||||||
// Split current line at cursor
|
|
||||||
const before = line.slice(0, cursor);
|
|
||||||
const after = line.slice(cursor);
|
|
||||||
|
|
||||||
const newLines = [
|
|
||||||
...lines.slice(0, currentLine),
|
|
||||||
before,
|
|
||||||
after,
|
|
||||||
...lines.slice(currentLine + 1),
|
|
||||||
];
|
|
||||||
|
|
||||||
return {
|
|
||||||
lines: newLines,
|
|
||||||
currentLine: currentLine + 1,
|
|
||||||
cursor: 0,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Handle backspace */
|
|
||||||
export function handleBackspace(state: MultilineInputState): MultilineInputState {
|
|
||||||
const { lines, currentLine, cursor } = state;
|
|
||||||
const line = lines[currentLine] || '';
|
|
||||||
|
|
||||||
if (cursor > 0) {
|
|
||||||
// Delete character before cursor
|
|
||||||
const newLine = line.slice(0, cursor - 1) + line.slice(cursor);
|
|
||||||
const newLines = [...lines];
|
|
||||||
newLines[currentLine] = newLine;
|
|
||||||
|
|
||||||
return {
|
|
||||||
lines: newLines,
|
|
||||||
currentLine,
|
|
||||||
cursor: cursor - 1,
|
|
||||||
};
|
|
||||||
} else if (currentLine > 0) {
|
|
||||||
// At start of line, merge with previous line
|
|
||||||
const prevLine = lines[currentLine - 1] || '';
|
|
||||||
const mergedLine = prevLine + line;
|
|
||||||
|
|
||||||
const newLines = [
|
|
||||||
...lines.slice(0, currentLine - 1),
|
|
||||||
mergedLine,
|
|
||||||
...lines.slice(currentLine + 1),
|
|
||||||
];
|
|
||||||
|
|
||||||
return {
|
|
||||||
lines: newLines,
|
|
||||||
currentLine: currentLine - 1,
|
|
||||||
cursor: prevLine.length,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// At start of first line, nothing to do
|
|
||||||
return state;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Handle left arrow */
|
|
||||||
export function handleLeftArrow(state: MultilineInputState): MultilineInputState {
|
|
||||||
const { lines, currentLine, cursor } = state;
|
|
||||||
|
|
||||||
if (cursor > 0) {
|
|
||||||
return { ...state, cursor: cursor - 1 };
|
|
||||||
} else if (currentLine > 0) {
|
|
||||||
// Move to end of previous line
|
|
||||||
const prevLineLength = (lines[currentLine - 1] || '').length;
|
|
||||||
return {
|
|
||||||
...state,
|
|
||||||
currentLine: currentLine - 1,
|
|
||||||
cursor: prevLineLength,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
return state;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Handle right arrow */
|
|
||||||
export function handleRightArrow(state: MultilineInputState): MultilineInputState {
|
|
||||||
const { lines, currentLine, cursor } = state;
|
|
||||||
const lineLength = (lines[currentLine] || '').length;
|
|
||||||
|
|
||||||
if (cursor < lineLength) {
|
|
||||||
return { ...state, cursor: cursor + 1 };
|
|
||||||
} else if (currentLine < lines.length - 1) {
|
|
||||||
// Move to start of next line
|
|
||||||
return {
|
|
||||||
...state,
|
|
||||||
currentLine: currentLine + 1,
|
|
||||||
cursor: 0,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
return state;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Handle up arrow */
|
|
||||||
export function handleUpArrow(state: MultilineInputState): MultilineInputState {
|
|
||||||
const { lines, currentLine, cursor } = state;
|
|
||||||
|
|
||||||
if (currentLine > 0) {
|
|
||||||
const prevLineLength = (lines[currentLine - 1] || '').length;
|
|
||||||
return {
|
|
||||||
...state,
|
|
||||||
currentLine: currentLine - 1,
|
|
||||||
cursor: Math.min(cursor, prevLineLength),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
return state;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Handle down arrow */
|
|
||||||
export function handleDownArrow(state: MultilineInputState): MultilineInputState {
|
|
||||||
const { lines, currentLine, cursor } = state;
|
|
||||||
|
|
||||||
if (currentLine < lines.length - 1) {
|
|
||||||
const nextLineLength = (lines[currentLine + 1] || '').length;
|
|
||||||
return {
|
|
||||||
...state,
|
|
||||||
currentLine: currentLine + 1,
|
|
||||||
cursor: Math.min(cursor, nextLineLength),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
return state;
|
|
||||||
}
|
|
||||||
@ -1,282 +0,0 @@
|
|||||||
/**
|
|
||||||
* Interactive permission handler for takt
|
|
||||||
*
|
|
||||||
* Prompts user for permission when Claude requests access to tools
|
|
||||||
* that are not pre-approved.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import chalk from 'chalk';
|
|
||||||
import * as readline from 'node:readline';
|
|
||||||
import type { PermissionRequest, PermissionHandler } from '../claude/process.js';
|
|
||||||
import type { PermissionResult, PermissionUpdate } from '@anthropic-ai/claude-agent-sdk';
|
|
||||||
import { playInfoSound } from '../utils/notification.js';
|
|
||||||
|
|
||||||
/** Permission state for the current session */
|
|
||||||
export interface PermissionState {
|
|
||||||
/** Temporarily allowed command patterns (for this iteration) */
|
|
||||||
iterationAllowedPatterns: Set<string>;
|
|
||||||
/** Sacrifice mode for current iteration */
|
|
||||||
iterationSacrificeMode: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Create initial permission state */
|
|
||||||
export function createPermissionState(): PermissionState {
|
|
||||||
return {
|
|
||||||
iterationAllowedPatterns: new Set(),
|
|
||||||
iterationSacrificeMode: false,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Reset permission state for new iteration */
|
|
||||||
export function resetPermissionStateForIteration(state: PermissionState): void {
|
|
||||||
state.iterationAllowedPatterns.clear();
|
|
||||||
state.iterationSacrificeMode = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Format tool input for display */
|
|
||||||
function formatToolInput(toolName: string, input: Record<string, unknown>): string {
|
|
||||||
if (toolName === 'Bash') {
|
|
||||||
const command = input.command as string | undefined;
|
|
||||||
const description = input.description as string | undefined;
|
|
||||||
if (command) {
|
|
||||||
const lines = [` コマンド: ${chalk.bold(command)}`];
|
|
||||||
if (description) {
|
|
||||||
lines.push(` 説明: ${description}`);
|
|
||||||
}
|
|
||||||
return lines.join('\n');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (toolName === 'Edit' || toolName === 'Write' || toolName === 'Read') {
|
|
||||||
const filePath = input.file_path as string | undefined;
|
|
||||||
if (filePath) {
|
|
||||||
return ` ファイル: ${chalk.bold(filePath)}`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (toolName === 'WebSearch') {
|
|
||||||
const query = input.query as string | undefined;
|
|
||||||
if (query) {
|
|
||||||
return ` 検索: ${chalk.bold(query)}`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (toolName === 'WebFetch') {
|
|
||||||
const url = input.url as string | undefined;
|
|
||||||
if (url) {
|
|
||||||
return ` URL: ${chalk.bold(url)}`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Generic display for other tools
|
|
||||||
const entries = Object.entries(input).slice(0, 3);
|
|
||||||
return entries.map(([k, v]) => ` ${k}: ${JSON.stringify(v).slice(0, 50)}`).join('\n');
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Build permission rule for the tool */
|
|
||||||
function buildPermissionRule(toolName: string, input: Record<string, unknown>): string {
|
|
||||||
if (toolName === 'Bash') {
|
|
||||||
const command = (input.command as string) || '';
|
|
||||||
const firstWord = command.split(/\s+/)[0] || command;
|
|
||||||
return `Bash(${firstWord}:*)`;
|
|
||||||
}
|
|
||||||
return toolName;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Build exact command pattern for iteration-scoped permission */
|
|
||||||
function buildExactCommandPattern(toolName: string, input: Record<string, unknown>): string {
|
|
||||||
if (toolName === 'Bash') {
|
|
||||||
const command = (input.command as string) || '';
|
|
||||||
return `Bash:${command}`;
|
|
||||||
}
|
|
||||||
return `${toolName}:${JSON.stringify(input)}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Check if a pattern matches the current request */
|
|
||||||
function matchesPattern(pattern: string, toolName: string, input: Record<string, unknown>): boolean {
|
|
||||||
// Check exact command pattern
|
|
||||||
const exactPattern = buildExactCommandPattern(toolName, input);
|
|
||||||
if (pattern === exactPattern) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check tool pattern (e.g., "Bash(gh:*)")
|
|
||||||
if (toolName === 'Bash' && pattern.startsWith('Bash(')) {
|
|
||||||
const command = (input.command as string) || '';
|
|
||||||
const firstWord = command.split(/\s+/)[0] || '';
|
|
||||||
const patternPrefix = pattern.match(/^Bash\(([^:]+):\*\)$/)?.[1];
|
|
||||||
if (patternPrefix && firstWord === patternPrefix) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Create an interactive permission handler with enhanced options
|
|
||||||
*
|
|
||||||
* @param rl - Readline interface for user input
|
|
||||||
* @param permissionState - Shared permission state for iteration-scoped permissions
|
|
||||||
* @returns Permission handler function
|
|
||||||
*/
|
|
||||||
export function createInteractivePermissionHandler(
|
|
||||||
rl: readline.Interface,
|
|
||||||
permissionState?: PermissionState
|
|
||||||
): PermissionHandler {
|
|
||||||
// Use provided state or create a new one
|
|
||||||
const state = permissionState || createPermissionState();
|
|
||||||
|
|
||||||
return async (request: PermissionRequest): Promise<PermissionResult> => {
|
|
||||||
const { toolName, input, suggestions, decisionReason } = request;
|
|
||||||
|
|
||||||
// Check if sacrifice mode is active for this iteration
|
|
||||||
if (state.iterationSacrificeMode) {
|
|
||||||
return { behavior: 'allow' };
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if this command matches any iteration-allowed pattern
|
|
||||||
for (const pattern of state.iterationAllowedPatterns) {
|
|
||||||
if (matchesPattern(pattern, toolName, input)) {
|
|
||||||
return { behavior: 'allow' };
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Play notification sound
|
|
||||||
playInfoSound();
|
|
||||||
|
|
||||||
// Display permission request
|
|
||||||
console.log();
|
|
||||||
console.log(chalk.yellow('━'.repeat(60)));
|
|
||||||
console.log(chalk.yellow.bold('⚠️ 権限リクエスト'));
|
|
||||||
console.log(` ツール: ${chalk.cyan(toolName)}`);
|
|
||||||
console.log(formatToolInput(toolName, input));
|
|
||||||
if (decisionReason) {
|
|
||||||
console.log(chalk.gray(` 理由: ${decisionReason}`));
|
|
||||||
}
|
|
||||||
console.log(chalk.yellow('━'.repeat(60)));
|
|
||||||
|
|
||||||
// Show options
|
|
||||||
console.log(chalk.gray(' [y] 許可'));
|
|
||||||
console.log(chalk.gray(' [n] 拒否'));
|
|
||||||
console.log(chalk.gray(' [a] 今後も許可(セッション中)'));
|
|
||||||
console.log(chalk.gray(' [i] このイテレーションでこのコマンドを許可'));
|
|
||||||
console.log(chalk.gray(' [p] このイテレーションでこのコマンドパターンを許可'));
|
|
||||||
console.log(chalk.gray(' [s] このイテレーションでPC全権限譲渡(sacrificeモード)'));
|
|
||||||
|
|
||||||
// Prompt user
|
|
||||||
const response = await new Promise<string>((resolve) => {
|
|
||||||
rl.question(
|
|
||||||
chalk.yellow('選択してください [y/n/a/i/p/s]: '),
|
|
||||||
(answer) => {
|
|
||||||
resolve(answer.trim().toLowerCase());
|
|
||||||
}
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
if (response === 'y' || response === 'yes') {
|
|
||||||
// Allow this time only
|
|
||||||
console.log(chalk.green('✓ 許可しました'));
|
|
||||||
return { behavior: 'allow' };
|
|
||||||
}
|
|
||||||
|
|
||||||
if (response === 'a' || response === 'always') {
|
|
||||||
// Allow and remember for session
|
|
||||||
const rule = buildPermissionRule(toolName, input);
|
|
||||||
console.log(chalk.green(`✓ 許可しました (${rule} をセッション中記憶)`));
|
|
||||||
|
|
||||||
// Use suggestions if available, otherwise build our own
|
|
||||||
const updatedPermissions: PermissionUpdate[] = suggestions || [
|
|
||||||
{
|
|
||||||
type: 'addRules',
|
|
||||||
rules: [{ toolName, ruleContent: rule }],
|
|
||||||
behavior: 'allow',
|
|
||||||
destination: 'session',
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
return {
|
|
||||||
behavior: 'allow',
|
|
||||||
updatedPermissions,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
if (response === 'i') {
|
|
||||||
// Allow this exact command for this iteration
|
|
||||||
const exactPattern = buildExactCommandPattern(toolName, input);
|
|
||||||
state.iterationAllowedPatterns.add(exactPattern);
|
|
||||||
console.log(chalk.green('✓ このイテレーションでこのコマンドを許可しました'));
|
|
||||||
return { behavior: 'allow' };
|
|
||||||
}
|
|
||||||
|
|
||||||
if (response === 'p') {
|
|
||||||
// Allow this command pattern for this iteration
|
|
||||||
const pattern = buildPermissionRule(toolName, input);
|
|
||||||
state.iterationAllowedPatterns.add(pattern);
|
|
||||||
console.log(chalk.green(`✓ このイテレーションで ${pattern} パターンを許可しました`));
|
|
||||||
return { behavior: 'allow' };
|
|
||||||
}
|
|
||||||
|
|
||||||
if (response === 's' || response === 'sacrifice') {
|
|
||||||
// Sacrifice mode for this iteration
|
|
||||||
state.iterationSacrificeMode = true;
|
|
||||||
console.log(chalk.red.bold('💀 このイテレーションでPC全権限を譲渡しました'));
|
|
||||||
return { behavior: 'allow' };
|
|
||||||
}
|
|
||||||
|
|
||||||
// Deny
|
|
||||||
console.log(chalk.red('✗ 拒否しました'));
|
|
||||||
return {
|
|
||||||
behavior: 'deny',
|
|
||||||
message: 'User denied permission',
|
|
||||||
};
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Create a non-interactive permission handler that auto-allows safe tools
|
|
||||||
* and denies others without prompting.
|
|
||||||
*/
|
|
||||||
export function createAutoPermissionHandler(): PermissionHandler {
|
|
||||||
// Tools that are always safe to allow
|
|
||||||
const safeTools = new Set([
|
|
||||||
'Read',
|
|
||||||
'Glob',
|
|
||||||
'Grep',
|
|
||||||
'WebSearch',
|
|
||||||
'WebFetch',
|
|
||||||
]);
|
|
||||||
|
|
||||||
// Safe Bash command prefixes
|
|
||||||
const safeBashPrefixes = [
|
|
||||||
'ls', 'cat', 'head', 'tail', 'find', 'grep', 'which',
|
|
||||||
'pwd', 'echo', 'date', 'whoami', 'uname',
|
|
||||||
'git status', 'git log', 'git diff', 'git branch', 'git show',
|
|
||||||
'npm ', 'npx ', 'node ', 'python ', 'pip ',
|
|
||||||
];
|
|
||||||
|
|
||||||
return async (request: PermissionRequest): Promise<PermissionResult> => {
|
|
||||||
const { toolName, input } = request;
|
|
||||||
|
|
||||||
// Safe tools are always allowed
|
|
||||||
if (safeTools.has(toolName)) {
|
|
||||||
return { behavior: 'allow' };
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check Bash commands
|
|
||||||
if (toolName === 'Bash') {
|
|
||||||
const command = ((input.command as string) || '').trim();
|
|
||||||
for (const prefix of safeBashPrefixes) {
|
|
||||||
if (command.startsWith(prefix)) {
|
|
||||||
return { behavior: 'allow' };
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Deny other tools
|
|
||||||
return {
|
|
||||||
behavior: 'deny',
|
|
||||||
message: `Tool ${toolName} requires explicit permission`,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
}
|
|
||||||
@ -1,253 +0,0 @@
|
|||||||
/**
|
|
||||||
* Interactive REPL mode for takt
|
|
||||||
*
|
|
||||||
* Provides an interactive shell similar to ORCA's interactive mode.
|
|
||||||
* Features:
|
|
||||||
* - Workflow switching with /switch (/sw)
|
|
||||||
* - Multi-agent workflow execution
|
|
||||||
* - Conversation history
|
|
||||||
* - Session persistence
|
|
||||||
*/
|
|
||||||
|
|
||||||
import chalk from 'chalk';
|
|
||||||
import { loadGlobalConfig } from '../config/index.js';
|
|
||||||
import {
|
|
||||||
getCurrentWorkflow,
|
|
||||||
getProjectConfigDir,
|
|
||||||
ensureDir,
|
|
||||||
} from '../config/paths.js';
|
|
||||||
import { interruptCurrentProcess } from '../claude/process.js';
|
|
||||||
import { info } from '../utils/ui.js';
|
|
||||||
import { generateSessionId } from '../utils/session.js';
|
|
||||||
import {
|
|
||||||
createReadlineInterface,
|
|
||||||
multiLineQuestion,
|
|
||||||
InputHistoryManager,
|
|
||||||
} from './input.js';
|
|
||||||
import { TaskRunner } from '../task/index.js';
|
|
||||||
import { commandRegistry } from './commands/index.js';
|
|
||||||
import { printWelcome } from './ui.js';
|
|
||||||
import { executeMultiAgentWorkflow } from './workflow-executor.js';
|
|
||||||
import type { InteractiveState } from './types.js';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Parse user input for iteration control.
|
|
||||||
*
|
|
||||||
* Returns the requested iteration count and the actual message.
|
|
||||||
* Examples:
|
|
||||||
* "3" -> { iterations: 3, message: null } (continue with 3 more iterations)
|
|
||||||
* "fix the bug" -> { iterations: 1, message: "fix the bug" }
|
|
||||||
* "5 do something" -> { iterations: 5, message: "do something" }
|
|
||||||
*/
|
|
||||||
function parseIterationInput(input: string): { iterations: number; message: string | null } {
|
|
||||||
const trimmed = input.trim();
|
|
||||||
|
|
||||||
// Check if input is just a number (continue iterations)
|
|
||||||
if (/^\d+$/.test(trimmed)) {
|
|
||||||
const count = parseInt(trimmed, 10);
|
|
||||||
if (count > 0 && count <= 100) {
|
|
||||||
return { iterations: count, message: null };
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if input starts with a number followed by space
|
|
||||||
const match = trimmed.match(/^(\d+)\s+(.+)$/);
|
|
||||||
if (match && match[1] && match[2]) {
|
|
||||||
const count = parseInt(match[1], 10);
|
|
||||||
if (count > 0 && count <= 100) {
|
|
||||||
return { iterations: count, message: match[2] };
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Default: single iteration with the full message
|
|
||||||
return { iterations: 1, message: trimmed };
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Execute workflow with user message */
|
|
||||||
async function executeWorkflow(
|
|
||||||
message: string,
|
|
||||||
state: InteractiveState,
|
|
||||||
rl: ReturnType<typeof createReadlineInterface>
|
|
||||||
): Promise<boolean> {
|
|
||||||
// Parse iteration control from input
|
|
||||||
const { iterations, message: actualMessage } = parseIterationInput(message);
|
|
||||||
|
|
||||||
// Determine the task to use
|
|
||||||
let task: string;
|
|
||||||
if (actualMessage === null) {
|
|
||||||
// Number only - continue with previous task
|
|
||||||
if (!state.currentTask) {
|
|
||||||
info('継続するタスクがありません。タスクを入力してください。');
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
task = state.currentTask;
|
|
||||||
info(`前回のタスクを ${iterations} イテレーションで継続します`);
|
|
||||||
} else {
|
|
||||||
task = actualMessage;
|
|
||||||
state.currentTask = task;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add user message to conversation history
|
|
||||||
state.conversationHistory.push({
|
|
||||||
role: 'user',
|
|
||||||
content: message,
|
|
||||||
timestamp: new Date().toISOString(),
|
|
||||||
});
|
|
||||||
|
|
||||||
// Add to input history (for up-arrow recall)
|
|
||||||
state.historyManager.add(message);
|
|
||||||
|
|
||||||
// Add to shared user inputs (for all agents)
|
|
||||||
state.sharedUserInputs.push(task);
|
|
||||||
|
|
||||||
// Run workflow with specified iterations
|
|
||||||
const response = await executeMultiAgentWorkflow(task, state, rl, iterations);
|
|
||||||
|
|
||||||
// Add assistant response to history
|
|
||||||
state.conversationHistory.push({
|
|
||||||
role: 'assistant',
|
|
||||||
content: response,
|
|
||||||
timestamp: new Date().toISOString(),
|
|
||||||
});
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Process user input */
|
|
||||||
async function processInput(
|
|
||||||
input: string,
|
|
||||||
state: InteractiveState,
|
|
||||||
rl: ReturnType<typeof createReadlineInterface>
|
|
||||||
): Promise<boolean> {
|
|
||||||
const trimmed = input.trim();
|
|
||||||
|
|
||||||
if (!trimmed) {
|
|
||||||
return true; // Continue
|
|
||||||
}
|
|
||||||
|
|
||||||
// Handle commands
|
|
||||||
if (trimmed.startsWith('/')) {
|
|
||||||
const parts = trimmed.slice(1).split(/\s+/);
|
|
||||||
const commandName = parts[0]?.toLowerCase();
|
|
||||||
const args = parts.slice(1);
|
|
||||||
|
|
||||||
if (!commandName) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
const command = commandRegistry.get(commandName);
|
|
||||||
if (command) {
|
|
||||||
const result = await command.execute(args, state, rl);
|
|
||||||
return result.continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
info(`Unknown command: ${commandName}`);
|
|
||||||
info('Type /help for available commands');
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Execute workflow with input
|
|
||||||
return await executeWorkflow(trimmed, state, rl);
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Start interactive mode */
|
|
||||||
export async function startInteractiveMode(
|
|
||||||
cwd: string,
|
|
||||||
initialTask?: string
|
|
||||||
): Promise<void> {
|
|
||||||
// Load global config for validation
|
|
||||||
loadGlobalConfig();
|
|
||||||
const lastWorkflow = getCurrentWorkflow(cwd);
|
|
||||||
|
|
||||||
// Create history manager (handles persistence automatically)
|
|
||||||
const historyManager = new InputHistoryManager(cwd);
|
|
||||||
|
|
||||||
// Create task runner
|
|
||||||
const taskRunner = new TaskRunner(cwd);
|
|
||||||
|
|
||||||
const state: InteractiveState = {
|
|
||||||
cwd,
|
|
||||||
workflowName: lastWorkflow,
|
|
||||||
sessionId: generateSessionId(),
|
|
||||||
conversationHistory: [],
|
|
||||||
historyManager,
|
|
||||||
taskRunner,
|
|
||||||
sharedUserInputs: [],
|
|
||||||
sacrificeMyPcMode: false,
|
|
||||||
};
|
|
||||||
|
|
||||||
// Ensure project config directory exists
|
|
||||||
ensureDir(getProjectConfigDir(cwd));
|
|
||||||
|
|
||||||
printWelcome(state);
|
|
||||||
|
|
||||||
const rl = createReadlineInterface();
|
|
||||||
|
|
||||||
// Handle initial task if provided
|
|
||||||
if (initialTask) {
|
|
||||||
const shouldContinue = await processInput(initialTask, state, rl);
|
|
||||||
if (!shouldContinue) {
|
|
||||||
rl.close();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Track Ctrl+C timing for double-press exit
|
|
||||||
let lastSigintTime = 0;
|
|
||||||
|
|
||||||
// Ctrl+C handler for double-press exit
|
|
||||||
const handleCtrlC = (): void => {
|
|
||||||
console.log();
|
|
||||||
const now = Date.now();
|
|
||||||
|
|
||||||
// Try to interrupt running Claude process first
|
|
||||||
if (interruptCurrentProcess()) {
|
|
||||||
info('Interrupted. Press Ctrl+C again to exit.');
|
|
||||||
lastSigintTime = now;
|
|
||||||
} else if (now - lastSigintTime < 2000) {
|
|
||||||
// Double press within 2 seconds - exit
|
|
||||||
info('Goodbye!');
|
|
||||||
rl.close();
|
|
||||||
process.exit(0);
|
|
||||||
} else {
|
|
||||||
info('Press Ctrl+C again to exit');
|
|
||||||
lastSigintTime = now;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Main REPL loop with multi-line support
|
|
||||||
const prompt = async (): Promise<void> => {
|
|
||||||
// Show workflow indicator above prompt
|
|
||||||
const modeIndicator = state.sacrificeMyPcMode ? chalk.red(' 💀') : '';
|
|
||||||
console.log(chalk.gray(`[${state.workflowName}]`) + modeIndicator);
|
|
||||||
|
|
||||||
try {
|
|
||||||
const promptStr = state.sacrificeMyPcMode
|
|
||||||
? chalk.red('takt💀> ')
|
|
||||||
: chalk.cyan('takt> ');
|
|
||||||
|
|
||||||
const input = await multiLineQuestion(rl, {
|
|
||||||
promptStr,
|
|
||||||
onCtrlC: handleCtrlC,
|
|
||||||
historyManager: state.historyManager,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (input === '') {
|
|
||||||
// Empty input, just re-prompt
|
|
||||||
prompt();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const shouldContinue = await processInput(input, state, rl);
|
|
||||||
if (shouldContinue) {
|
|
||||||
prompt();
|
|
||||||
} else {
|
|
||||||
rl.close();
|
|
||||||
}
|
|
||||||
} catch {
|
|
||||||
rl.close();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
prompt();
|
|
||||||
}
|
|
||||||
@ -1,37 +0,0 @@
|
|||||||
/**
|
|
||||||
* Interactive mode type definitions
|
|
||||||
*/
|
|
||||||
|
|
||||||
import type { TaskRunner } from '../task/index.js';
|
|
||||||
import type { InputHistoryManager } from './input.js';
|
|
||||||
|
|
||||||
/** Conversation message */
|
|
||||||
export interface ConversationMessage {
|
|
||||||
role: 'user' | 'assistant';
|
|
||||||
content: string;
|
|
||||||
timestamp: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Interactive session state */
|
|
||||||
export interface InteractiveState {
|
|
||||||
cwd: string;
|
|
||||||
workflowName: string;
|
|
||||||
sessionId: string;
|
|
||||||
claudeSessionId?: string;
|
|
||||||
conversationHistory: ConversationMessage[];
|
|
||||||
historyManager: InputHistoryManager;
|
|
||||||
taskRunner: TaskRunner;
|
|
||||||
/** Current task for workflow continuation */
|
|
||||||
currentTask?: string;
|
|
||||||
/** Requested number of iterations (for number input) */
|
|
||||||
requestedIterations?: number;
|
|
||||||
/** All user inputs shared across agents */
|
|
||||||
sharedUserInputs: string[];
|
|
||||||
/**
|
|
||||||
* Sacrifice-my-pc mode: auto-approve all permissions and skip confirmations.
|
|
||||||
* When enabled, all permission requests are automatically approved,
|
|
||||||
* iteration limits auto-continue with 10 iterations,
|
|
||||||
* and blocked states are auto-skipped.
|
|
||||||
*/
|
|
||||||
sacrificeMyPcMode: boolean;
|
|
||||||
}
|
|
||||||
@ -1,139 +0,0 @@
|
|||||||
/**
|
|
||||||
* Interactive mode UI functions
|
|
||||||
*
|
|
||||||
* Provides display and visual functions for the interactive REPL.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import * as readline from 'node:readline';
|
|
||||||
import chalk from 'chalk';
|
|
||||||
import { header, info, error, divider } from '../utils/ui.js';
|
|
||||||
import { loadAllWorkflows } from '../config/index.js';
|
|
||||||
import type { InteractiveState } from './types.js';
|
|
||||||
|
|
||||||
/** Clear screen */
|
|
||||||
export function clearScreen(): void {
|
|
||||||
console.clear();
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Print welcome banner */
|
|
||||||
export function printWelcome(state: InteractiveState): void {
|
|
||||||
console.log(chalk.bold.cyan('═'.repeat(60)));
|
|
||||||
console.log(chalk.bold.cyan(' TAKT Interactive Mode'));
|
|
||||||
console.log(chalk.bold.cyan('═'.repeat(60)));
|
|
||||||
console.log(chalk.gray(`Project: ${state.cwd}`));
|
|
||||||
console.log(chalk.gray(`Workflow: ${state.workflowName}`));
|
|
||||||
if (state.sacrificeMyPcMode) {
|
|
||||||
console.log(chalk.red.bold('Mode: SACRIFICE-MY-PC 💀 (auto-approve all)'));
|
|
||||||
}
|
|
||||||
console.log(chalk.gray('Type /help for commands, /quit to exit'));
|
|
||||||
console.log(chalk.bold.cyan('═'.repeat(60)));
|
|
||||||
console.log();
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Print help message */
|
|
||||||
export function printHelp(): void {
|
|
||||||
header('TAKT Commands');
|
|
||||||
console.log(`
|
|
||||||
${chalk.bold.yellow('Basic Operations:')}
|
|
||||||
[message] Send message to current workflow
|
|
||||||
Up/Down Arrow Navigate input history (persisted across sessions)
|
|
||||||
Enter Submit input (execute)
|
|
||||||
|
|
||||||
${chalk.bold.cyan('Multi-line input:')}
|
|
||||||
末尾に \\ 行末にバックスラッシュで継続 (mac Terminal.app推奨)
|
|
||||||
Ctrl+J 改行を挿入 (全ターミナルで動作)
|
|
||||||
Ctrl+Enter 改行を挿入 (対応ターミナルのみ)
|
|
||||||
Option+Enter 改行を挿入 (iTerm2等)
|
|
||||||
|
|
||||||
/help, /h Show this help
|
|
||||||
/quit, /exit, /q Exit takt
|
|
||||||
|
|
||||||
${chalk.bold.yellow('Workflow Management:')}
|
|
||||||
/switch, /sw Switch workflow (interactive selection)
|
|
||||||
/workflow [name] Show or change current workflow
|
|
||||||
/workflows List available workflows
|
|
||||||
|
|
||||||
${chalk.bold.yellow('Session Management:')}
|
|
||||||
/clear Clear session and start fresh
|
|
||||||
/cls Clear screen only (keep session)
|
|
||||||
/reset Full reset (session + workflow)
|
|
||||||
/status Show current session info
|
|
||||||
/history Show conversation history
|
|
||||||
|
|
||||||
${chalk.bold.yellow('Agent Operations:')}
|
|
||||||
/agents List available agents
|
|
||||||
/agent <name> Run a single agent with next input
|
|
||||||
|
|
||||||
${chalk.bold.yellow('Task Execution:')}
|
|
||||||
/task, /t Show task list
|
|
||||||
/task run Execute next task
|
|
||||||
/task run <name> Execute specified task
|
|
||||||
|
|
||||||
${chalk.bold.yellow('Mode Control:')}
|
|
||||||
/sacrifice, /yolo Toggle sacrifice-my-pc mode (auto-approve all)
|
|
||||||
/safe Disable sacrifice mode
|
|
||||||
|
|
||||||
${chalk.bold.yellow('Workflows:')}
|
|
||||||
default Coder -> Architect loop (default)
|
|
||||||
|
|
||||||
${chalk.bold.cyan('Examples:')}
|
|
||||||
Implement a login feature
|
|
||||||
Review src/auth.ts and suggest improvements
|
|
||||||
Add tests for the previous code
|
|
||||||
`);
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Show workflow selector UI */
|
|
||||||
export async function selectWorkflow(
|
|
||||||
state: InteractiveState,
|
|
||||||
rl: readline.Interface
|
|
||||||
): Promise<string | null> {
|
|
||||||
const workflows = loadAllWorkflows();
|
|
||||||
const workflowList = Array.from(workflows.entries()).sort((a, b) =>
|
|
||||||
a[0].localeCompare(b[0])
|
|
||||||
);
|
|
||||||
|
|
||||||
if (workflowList.length === 0) {
|
|
||||||
error('No workflows available');
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log();
|
|
||||||
divider('═', 60);
|
|
||||||
console.log(chalk.bold.magenta(' Workflow Selection'));
|
|
||||||
divider('═', 60);
|
|
||||||
console.log();
|
|
||||||
|
|
||||||
workflowList.forEach(([name, workflow], index) => {
|
|
||||||
const current = name === state.workflowName ? chalk.green(' (current)') : '';
|
|
||||||
const desc = workflow.description || `${name} workflow`;
|
|
||||||
console.log(chalk.cyan(` [${index + 1}] ${name}${current}`));
|
|
||||||
console.log(chalk.gray(` ${desc}`));
|
|
||||||
});
|
|
||||||
|
|
||||||
console.log(chalk.yellow(` [0] Cancel`));
|
|
||||||
console.log();
|
|
||||||
divider('═', 60);
|
|
||||||
|
|
||||||
return new Promise((resolve) => {
|
|
||||||
rl.question(chalk.cyan('Select workflow (number)> '), (input) => {
|
|
||||||
const trimmed = input.trim();
|
|
||||||
|
|
||||||
if (!trimmed || trimmed === '0') {
|
|
||||||
info('Cancelled');
|
|
||||||
resolve(null);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const index = parseInt(trimmed, 10) - 1;
|
|
||||||
const entry = workflowList[index];
|
|
||||||
if (index >= 0 && entry) {
|
|
||||||
const [name] = entry;
|
|
||||||
resolve(name);
|
|
||||||
} else {
|
|
||||||
error('Invalid selection');
|
|
||||||
resolve(null);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
@ -1,134 +0,0 @@
|
|||||||
/**
|
|
||||||
* User input request handlers for workflow execution
|
|
||||||
*
|
|
||||||
* Handles user input prompts when an agent is blocked
|
|
||||||
* or iteration limits are reached.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import chalk from 'chalk';
|
|
||||||
import type { InputHistoryManager } from './input.js';
|
|
||||||
import { multiLineQuestion, createReadlineInterface } from './input.js';
|
|
||||||
import type { UserInputRequest, IterationLimitRequest } from '../workflow/engine.js';
|
|
||||||
import { info } from '../utils/ui.js';
|
|
||||||
import { playInfoSound } from '../utils/notification.js';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Request user input for blocked workflow step.
|
|
||||||
*
|
|
||||||
* Displays the blocked message and prompts the user for additional information.
|
|
||||||
* Returns null if the user cancels or provides empty input.
|
|
||||||
*/
|
|
||||||
export async function requestUserInput(
|
|
||||||
request: UserInputRequest,
|
|
||||||
rl: ReturnType<typeof createReadlineInterface>,
|
|
||||||
historyManager: InputHistoryManager
|
|
||||||
): Promise<string | null> {
|
|
||||||
// Play notification sound to alert user
|
|
||||||
playInfoSound();
|
|
||||||
|
|
||||||
console.log();
|
|
||||||
console.log(chalk.yellow('━'.repeat(60)));
|
|
||||||
console.log(chalk.yellow.bold('❓ エージェントからの質問'));
|
|
||||||
console.log(chalk.gray(`ステップ: ${request.step.name} (${request.step.agentDisplayName})`));
|
|
||||||
console.log();
|
|
||||||
console.log(chalk.white(request.response.content));
|
|
||||||
console.log(chalk.yellow('━'.repeat(60)));
|
|
||||||
console.log();
|
|
||||||
console.log(chalk.cyan('回答を入力してください(キャンセル: Ctrl+C)'));
|
|
||||||
console.log();
|
|
||||||
|
|
||||||
return new Promise((resolve) => {
|
|
||||||
multiLineQuestion(rl, {
|
|
||||||
promptStr: chalk.magenta('回答> '),
|
|
||||||
onCtrlC: () => {
|
|
||||||
console.log();
|
|
||||||
info('ユーザー入力がキャンセルされました');
|
|
||||||
resolve(null);
|
|
||||||
return true; // Cancel input
|
|
||||||
},
|
|
||||||
historyManager,
|
|
||||||
}).then((input) => {
|
|
||||||
if (input.trim() === '') {
|
|
||||||
info('空の入力のためキャンセルされました');
|
|
||||||
resolve(null);
|
|
||||||
} else {
|
|
||||||
resolve(input);
|
|
||||||
}
|
|
||||||
}).catch(() => {
|
|
||||||
resolve(null);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Handle iteration limit reached.
|
|
||||||
* Ask user if they want to continue and how many additional iterations.
|
|
||||||
*
|
|
||||||
* Returns:
|
|
||||||
* - number: The number of additional iterations to continue
|
|
||||||
* - null: User chose to stop the workflow
|
|
||||||
*/
|
|
||||||
export async function requestIterationContinue(
|
|
||||||
request: IterationLimitRequest,
|
|
||||||
rl: ReturnType<typeof createReadlineInterface>,
|
|
||||||
historyManager: InputHistoryManager
|
|
||||||
): Promise<number | null> {
|
|
||||||
// Play notification sound to alert user
|
|
||||||
playInfoSound();
|
|
||||||
|
|
||||||
console.log();
|
|
||||||
console.log(chalk.yellow('━'.repeat(60)));
|
|
||||||
console.log(chalk.yellow.bold('⏸ イテレーション上限に達しました'));
|
|
||||||
console.log(chalk.gray(`現在: ${request.currentIteration}/${request.maxIterations} イテレーション`));
|
|
||||||
console.log(chalk.gray(`ステップ: ${request.currentStep}`));
|
|
||||||
console.log(chalk.yellow('━'.repeat(60)));
|
|
||||||
console.log();
|
|
||||||
console.log(chalk.cyan('続けますか?'));
|
|
||||||
console.log(chalk.gray(' - 数字を入力: 追加イテレーション数(例: 5)'));
|
|
||||||
console.log(chalk.gray(' - Enter: デフォルト10イテレーション追加'));
|
|
||||||
console.log(chalk.gray(' - Ctrl+C または "n": 終了'));
|
|
||||||
console.log();
|
|
||||||
|
|
||||||
return new Promise((resolve) => {
|
|
||||||
multiLineQuestion(rl, {
|
|
||||||
promptStr: chalk.magenta('追加イテレーション> '),
|
|
||||||
onCtrlC: () => {
|
|
||||||
console.log();
|
|
||||||
info('ワークフローを終了します');
|
|
||||||
resolve(null);
|
|
||||||
return true;
|
|
||||||
},
|
|
||||||
historyManager,
|
|
||||||
}).then((input) => {
|
|
||||||
const trimmed = input.trim().toLowerCase();
|
|
||||||
|
|
||||||
// User wants to stop
|
|
||||||
if (trimmed === 'n' || trimmed === 'no' || trimmed === 'q' || trimmed === 'quit') {
|
|
||||||
info('ワークフローを終了します');
|
|
||||||
resolve(null);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Empty input = default 10 iterations
|
|
||||||
if (trimmed === '' || trimmed === 'y' || trimmed === 'yes') {
|
|
||||||
info('10 イテレーション追加します');
|
|
||||||
resolve(10);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Try to parse as number
|
|
||||||
const num = parseInt(trimmed, 10);
|
|
||||||
if (!isNaN(num) && num > 0 && num <= 100) {
|
|
||||||
info(`${num} イテレーション追加します`);
|
|
||||||
resolve(num);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Invalid input, treat as continue with default
|
|
||||||
info('10 イテレーション追加します');
|
|
||||||
resolve(10);
|
|
||||||
}).catch(() => {
|
|
||||||
resolve(null);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
@ -1,254 +0,0 @@
|
|||||||
/**
|
|
||||||
* Workflow executor for interactive mode
|
|
||||||
*
|
|
||||||
* Handles the execution of multi-agent workflows,
|
|
||||||
* including streaming output and state management.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import chalk from 'chalk';
|
|
||||||
import {
|
|
||||||
loadWorkflow,
|
|
||||||
getBuiltinWorkflow,
|
|
||||||
} from '../config/index.js';
|
|
||||||
import {
|
|
||||||
loadAgentSessions,
|
|
||||||
updateAgentSession,
|
|
||||||
} from '../config/paths.js';
|
|
||||||
import { WorkflowEngine, type UserInputRequest, type IterationLimitRequest } from '../workflow/engine.js';
|
|
||||||
import {
|
|
||||||
info,
|
|
||||||
error,
|
|
||||||
success,
|
|
||||||
StreamDisplay,
|
|
||||||
} from '../utils/ui.js';
|
|
||||||
import {
|
|
||||||
playWarningSound,
|
|
||||||
notifySuccess,
|
|
||||||
notifyError,
|
|
||||||
notifyWarning,
|
|
||||||
} from '../utils/notification.js';
|
|
||||||
import {
|
|
||||||
createSessionLog,
|
|
||||||
addToSessionLog,
|
|
||||||
finalizeSessionLog,
|
|
||||||
saveSessionLog,
|
|
||||||
} from '../utils/session.js';
|
|
||||||
import { createReadlineInterface } from './input.js';
|
|
||||||
import {
|
|
||||||
createInteractivePermissionHandler,
|
|
||||||
createPermissionState,
|
|
||||||
resetPermissionStateForIteration,
|
|
||||||
} from './permission.js';
|
|
||||||
import {
|
|
||||||
createAgentAnswerHandler,
|
|
||||||
createAskUserQuestionHandler,
|
|
||||||
createSacrificeModeQuestionHandler,
|
|
||||||
} from './handlers.js';
|
|
||||||
import { requestUserInput, requestIterationContinue } from './user-input.js';
|
|
||||||
import type { InteractiveState } from './types.js';
|
|
||||||
import type { WorkflowConfig } from '../models/types.js';
|
|
||||||
import type { AskUserQuestionHandler } from '../claude/process.js';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Execute multi-agent workflow with streaming output.
|
|
||||||
*
|
|
||||||
* This is the main workflow execution function that:
|
|
||||||
* - Loads and validates the workflow configuration
|
|
||||||
* - Sets up stream handlers for real-time output
|
|
||||||
* - Manages agent sessions for conversation continuity
|
|
||||||
* - Handles blocked states and user input requests
|
|
||||||
* - Logs session data for debugging
|
|
||||||
*/
|
|
||||||
export async function executeMultiAgentWorkflow(
|
|
||||||
message: string,
|
|
||||||
state: InteractiveState,
|
|
||||||
rl: ReturnType<typeof createReadlineInterface>,
|
|
||||||
requestedIterations: number = 10
|
|
||||||
): Promise<string> {
|
|
||||||
const builtin = getBuiltinWorkflow(state.workflowName);
|
|
||||||
let config: WorkflowConfig | null =
|
|
||||||
builtin || loadWorkflow(state.workflowName);
|
|
||||||
|
|
||||||
if (!config) {
|
|
||||||
error(`Workflow "${state.workflowName}" not found.`);
|
|
||||||
info('Available workflows: /workflow list');
|
|
||||||
return `[ERROR] Workflow "${state.workflowName}" not found`;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Apply requested iteration count
|
|
||||||
if (requestedIterations !== config.maxIterations) {
|
|
||||||
config = { ...config, maxIterations: requestedIterations };
|
|
||||||
}
|
|
||||||
|
|
||||||
const sessionLog = createSessionLog(message, state.cwd, config.name);
|
|
||||||
|
|
||||||
// Track current display for streaming
|
|
||||||
const displayRef: { current: StreamDisplay | null } = { current: null };
|
|
||||||
|
|
||||||
// Create stream handler that delegates to current display
|
|
||||||
const streamHandler = (
|
|
||||||
event: Parameters<ReturnType<StreamDisplay['createHandler']>>[0]
|
|
||||||
): void => {
|
|
||||||
if (!displayRef.current) return;
|
|
||||||
if (event.type === 'result') return;
|
|
||||||
displayRef.current.createHandler()(event);
|
|
||||||
};
|
|
||||||
|
|
||||||
// Create user input handler for blocked state
|
|
||||||
const userInputHandler = async (request: UserInputRequest): Promise<string | null> => {
|
|
||||||
// In sacrifice mode, auto-skip blocked states
|
|
||||||
if (state.sacrificeMyPcMode) {
|
|
||||||
info('[SACRIFICE MODE] Auto-skipping blocked state');
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Flush current display before prompting
|
|
||||||
if (displayRef.current) {
|
|
||||||
displayRef.current.flushThinking();
|
|
||||||
displayRef.current.flushText();
|
|
||||||
displayRef.current = null;
|
|
||||||
}
|
|
||||||
return requestUserInput(request, rl, state.historyManager);
|
|
||||||
};
|
|
||||||
|
|
||||||
// Create iteration limit handler
|
|
||||||
// Note: Even in sacrifice mode, we ask user for iteration continuation
|
|
||||||
// to prevent runaway execution
|
|
||||||
const iterationLimitHandler = async (request: IterationLimitRequest): Promise<number | null> => {
|
|
||||||
// Flush current display before prompting
|
|
||||||
if (displayRef.current) {
|
|
||||||
displayRef.current.flushThinking();
|
|
||||||
displayRef.current.flushText();
|
|
||||||
displayRef.current = null;
|
|
||||||
}
|
|
||||||
return requestIterationContinue(request, rl, state.historyManager);
|
|
||||||
};
|
|
||||||
|
|
||||||
// Load saved agent sessions for session resumption
|
|
||||||
const savedSessions = loadAgentSessions(state.cwd);
|
|
||||||
|
|
||||||
// Session update handler - persist session IDs when they change
|
|
||||||
const sessionUpdateHandler = (agentName: string, sessionId: string): void => {
|
|
||||||
updateAgentSession(state.cwd, agentName, sessionId);
|
|
||||||
};
|
|
||||||
|
|
||||||
// Create permission state for iteration-scoped permissions
|
|
||||||
const permissionState = createPermissionState();
|
|
||||||
|
|
||||||
// Create interactive permission handler (sacrifice mode uses bypassPermissions)
|
|
||||||
const permissionHandler = state.sacrificeMyPcMode
|
|
||||||
? undefined // No handler needed - we'll use bypassPermissions mode
|
|
||||||
: createInteractivePermissionHandler(rl, permissionState);
|
|
||||||
|
|
||||||
// Create AskUserQuestion handler
|
|
||||||
// Priority: sacrifice mode > answerAgent > interactive user input
|
|
||||||
let askUserQuestionHandler: AskUserQuestionHandler;
|
|
||||||
if (state.sacrificeMyPcMode) {
|
|
||||||
askUserQuestionHandler = createSacrificeModeQuestionHandler();
|
|
||||||
} else if (config.answerAgent) {
|
|
||||||
// Use another agent to answer questions
|
|
||||||
info(`質問回答エージェント: ${config.answerAgent}`);
|
|
||||||
askUserQuestionHandler = createAgentAnswerHandler(config.answerAgent, state.cwd);
|
|
||||||
} else {
|
|
||||||
// Interactive user input
|
|
||||||
askUserQuestionHandler = createAskUserQuestionHandler(rl, state.historyManager);
|
|
||||||
}
|
|
||||||
|
|
||||||
const engine = new WorkflowEngine(config, state.cwd, message, {
|
|
||||||
onStream: streamHandler,
|
|
||||||
onUserInput: userInputHandler,
|
|
||||||
initialSessions: savedSessions,
|
|
||||||
onSessionUpdate: sessionUpdateHandler,
|
|
||||||
onPermissionRequest: permissionHandler,
|
|
||||||
initialUserInputs: state.sharedUserInputs,
|
|
||||||
onAskUserQuestion: askUserQuestionHandler,
|
|
||||||
onIterationLimit: iterationLimitHandler,
|
|
||||||
bypassPermissions: state.sacrificeMyPcMode,
|
|
||||||
});
|
|
||||||
|
|
||||||
engine.on('step:start', (step, iteration) => {
|
|
||||||
// Reset iteration-scoped permission state at start of each step
|
|
||||||
resetPermissionStateForIteration(permissionState);
|
|
||||||
info(`[${iteration}/${config.maxIterations}] ${step.name} (${step.agentDisplayName})`);
|
|
||||||
displayRef.current = new StreamDisplay(step.agentDisplayName);
|
|
||||||
});
|
|
||||||
|
|
||||||
engine.on('step:complete', (step, stepResponse) => {
|
|
||||||
if (displayRef.current) {
|
|
||||||
displayRef.current.flushThinking();
|
|
||||||
displayRef.current.flushText();
|
|
||||||
displayRef.current = null;
|
|
||||||
}
|
|
||||||
console.log();
|
|
||||||
addToSessionLog(sessionLog, step.name, stepResponse);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Handle user input event (after user provides input for blocked step)
|
|
||||||
engine.on('step:user_input', (step, userInput) => {
|
|
||||||
console.log();
|
|
||||||
info(`ユーザー入力を受け取りました。${step.name} を再実行します...`);
|
|
||||||
console.log(chalk.gray(`入力内容: ${userInput.slice(0, 100)}${userInput.length > 100 ? '...' : ''}`));
|
|
||||||
console.log();
|
|
||||||
});
|
|
||||||
|
|
||||||
let wasInterrupted = false;
|
|
||||||
let loopDetected = false;
|
|
||||||
let wasBlocked = false;
|
|
||||||
engine.on('workflow:abort', (_, reason) => {
|
|
||||||
if (displayRef.current) {
|
|
||||||
displayRef.current.flushThinking();
|
|
||||||
displayRef.current.flushText();
|
|
||||||
displayRef.current = null;
|
|
||||||
}
|
|
||||||
if (reason?.includes('interrupted')) {
|
|
||||||
wasInterrupted = true;
|
|
||||||
}
|
|
||||||
if (reason?.includes('Loop detected')) {
|
|
||||||
loopDetected = true;
|
|
||||||
}
|
|
||||||
if (reason?.includes('blocked') || reason?.includes('no user input')) {
|
|
||||||
wasBlocked = true;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
try {
|
|
||||||
const finalState = await engine.run();
|
|
||||||
|
|
||||||
const statusVal = finalState.status === 'completed' ? 'completed' : 'aborted';
|
|
||||||
finalizeSessionLog(sessionLog, statusVal);
|
|
||||||
saveSessionLog(sessionLog, state.sessionId, state.cwd);
|
|
||||||
|
|
||||||
if (finalState.status === 'completed') {
|
|
||||||
success('Workflow completed!');
|
|
||||||
notifySuccess('TAKT', `ワークフロー完了 (${finalState.iteration} iterations)`);
|
|
||||||
return '[WORKFLOW COMPLETE]';
|
|
||||||
} else if (wasInterrupted) {
|
|
||||||
info('Workflow interrupted by user');
|
|
||||||
// User intentionally interrupted - sound only, no system notification needed
|
|
||||||
playWarningSound();
|
|
||||||
return '[WORKFLOW INTERRUPTED]';
|
|
||||||
} else if (loopDetected) {
|
|
||||||
error('Workflow aborted due to loop detection');
|
|
||||||
info('Tip: ループが検出されました。タスクを見直すか、/agent coder を直接使用してください。');
|
|
||||||
notifyError('TAKT', 'ループ検出により中断されました');
|
|
||||||
return '[WORKFLOW ABORTED: Loop detected]';
|
|
||||||
} else if (wasBlocked) {
|
|
||||||
info('Workflow aborted: エージェントがブロックされ、ユーザー入力が提供されませんでした');
|
|
||||||
notifyWarning('TAKT', 'ユーザー入力待ちで中断されました');
|
|
||||||
return '[WORKFLOW ABORTED: Blocked]';
|
|
||||||
} else {
|
|
||||||
error('Workflow aborted');
|
|
||||||
notifyError('TAKT', 'ワークフローが中断されました');
|
|
||||||
return '[WORKFLOW ABORTED]';
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
if (displayRef.current) {
|
|
||||||
displayRef.current.flushThinking();
|
|
||||||
displayRef.current.flushText();
|
|
||||||
}
|
|
||||||
const errMsg = `[ERROR] ${err instanceof Error ? err.message : String(err)}`;
|
|
||||||
error(errMsg);
|
|
||||||
notifyError('TAKT', `エラー: ${err instanceof Error ? err.message : String(err)}`);
|
|
||||||
return errMsg;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -13,7 +13,7 @@ import chalk from 'chalk';
|
|||||||
*/
|
*/
|
||||||
export async function selectOption<T extends string>(
|
export async function selectOption<T extends string>(
|
||||||
message: string,
|
message: string,
|
||||||
options: { label: string; value: T }[]
|
options: { label: string; value: T; description?: string; details?: string[] }[]
|
||||||
): Promise<T | null> {
|
): Promise<T | null> {
|
||||||
const rl = readline.createInterface({
|
const rl = readline.createInterface({
|
||||||
input: process.stdin,
|
input: process.stdin,
|
||||||
@ -26,6 +26,16 @@ export async function selectOption<T extends string>(
|
|||||||
|
|
||||||
options.forEach((opt, idx) => {
|
options.forEach((opt, idx) => {
|
||||||
console.log(chalk.yellow(` ${idx + 1}. `) + opt.label);
|
console.log(chalk.yellow(` ${idx + 1}. `) + opt.label);
|
||||||
|
// Display description if provided
|
||||||
|
if (opt.description) {
|
||||||
|
console.log(chalk.gray(` ${opt.description}`));
|
||||||
|
}
|
||||||
|
// Display additional details if provided
|
||||||
|
if (opt.details && opt.details.length > 0) {
|
||||||
|
opt.details.forEach((detail) => {
|
||||||
|
console.log(chalk.dim(` • ${detail}`));
|
||||||
|
});
|
||||||
|
}
|
||||||
});
|
});
|
||||||
console.log(chalk.gray(` 0. Cancel`));
|
console.log(chalk.gray(` 0. Cancel`));
|
||||||
console.log();
|
console.log();
|
||||||
Loading…
x
Reference in New Issue
Block a user