lonny-agent 0.2.4 → 0.2.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.github/workflows/ci.yml +21 -0
- package/AGENTS.md +106 -0
- package/CHANGELOG.md +24 -0
- package/README.md +326 -326
- package/dist/agent/__tests__/session-restore.test.d.ts +2 -0
- package/dist/agent/__tests__/session-restore.test.d.ts.map +1 -0
- package/dist/agent/__tests__/session-restore.test.js +260 -0
- package/dist/agent/__tests__/session-restore.test.js.map +1 -0
- package/dist/agent/commands.d.ts +26 -0
- package/dist/agent/commands.d.ts.map +1 -0
- package/dist/agent/commands.js +253 -0
- package/dist/agent/commands.js.map +1 -0
- package/dist/agent/compaction.js +8 -1
- package/dist/agent/compaction.js.map +1 -1
- package/dist/agent/index.d.ts +2 -1
- package/dist/agent/index.d.ts.map +1 -1
- package/dist/agent/index.js +2 -2
- package/dist/agent/index.js.map +1 -1
- package/dist/agent/llm.d.ts +4 -0
- package/dist/agent/llm.d.ts.map +1 -1
- package/dist/agent/memory.d.ts +12 -0
- package/dist/agent/memory.d.ts.map +1 -0
- package/dist/agent/memory.js +51 -0
- package/dist/agent/memory.js.map +1 -0
- package/dist/agent/prompt-builder.d.ts +6 -1
- package/dist/agent/prompt-builder.d.ts.map +1 -1
- package/dist/agent/prompt-builder.js +100 -33
- package/dist/agent/prompt-builder.js.map +1 -1
- package/dist/agent/providers/google.d.ts.map +1 -1
- package/dist/agent/providers/google.js.map +1 -1
- package/dist/agent/providers/openai.d.ts +1 -0
- package/dist/agent/providers/openai.d.ts.map +1 -1
- package/dist/agent/providers/openai.js +59 -6
- package/dist/agent/providers/openai.js.map +1 -1
- package/dist/agent/session-chat.d.ts +3 -0
- package/dist/agent/session-chat.d.ts.map +1 -0
- package/dist/agent/session-chat.js +384 -0
- package/dist/agent/session-chat.js.map +1 -0
- package/dist/agent/session-display.d.ts +29 -0
- package/dist/agent/session-display.d.ts.map +1 -0
- package/dist/agent/session-display.js +179 -0
- package/dist/agent/session-display.js.map +1 -0
- package/dist/agent/session-persistence.d.ts +55 -0
- package/dist/agent/session-persistence.d.ts.map +1 -0
- package/dist/agent/session-persistence.js +101 -0
- package/dist/agent/session-persistence.js.map +1 -0
- package/dist/agent/session-utils.d.ts +5 -0
- package/dist/agent/session-utils.d.ts.map +1 -0
- package/dist/agent/session-utils.js +51 -0
- package/dist/agent/session-utils.js.map +1 -0
- package/dist/agent/session.d.ts +19 -17
- package/dist/agent/session.d.ts.map +1 -1
- package/dist/agent/session.js +234 -578
- package/dist/agent/session.js.map +1 -1
- package/dist/cli/index.d.ts +4 -0
- package/dist/cli/index.d.ts.map +1 -1
- package/dist/cli/index.js +67 -2
- package/dist/cli/index.js.map +1 -1
- package/dist/config/index.d.ts +2 -2
- package/dist/config/index.d.ts.map +1 -1
- package/dist/config/index.js +7 -1
- package/dist/config/index.js.map +1 -1
- package/dist/index.js +24 -3
- package/dist/index.js.map +1 -1
- package/dist/tools/__tests__/bash.test.js +169 -2
- package/dist/tools/__tests__/bash.test.js.map +1 -1
- package/dist/tools/__tests__/edit.test.js +1091 -340
- package/dist/tools/__tests__/edit.test.js.map +1 -1
- package/dist/tools/__tests__/fetch.test.js +1 -1
- package/dist/tools/__tests__/fetch.test.js.map +1 -1
- package/dist/tools/__tests__/glob.test.js +1 -1
- package/dist/tools/__tests__/glob.test.js.map +1 -1
- package/dist/tools/__tests__/grep.test.js +33 -1
- package/dist/tools/__tests__/grep.test.js.map +1 -1
- package/dist/tools/__tests__/ls.test.js +1 -1
- package/dist/tools/__tests__/ls.test.js.map +1 -1
- package/dist/tools/__tests__/read.test.js +1 -1
- package/dist/tools/__tests__/read.test.js.map +1 -1
- package/dist/tools/__tests__/registry.test.js +313 -0
- package/dist/tools/__tests__/registry.test.js.map +1 -1
- package/dist/tools/__tests__/sed.test.d.ts +2 -0
- package/dist/tools/__tests__/sed.test.d.ts.map +1 -0
- package/dist/tools/__tests__/sed.test.js +228 -0
- package/dist/tools/__tests__/sed.test.js.map +1 -0
- package/dist/tools/__tests__/write_plan.test.js +1 -1
- package/dist/tools/__tests__/write_plan.test.js.map +1 -1
- package/dist/tools/bash.d.ts.map +1 -1
- package/dist/tools/bash.js +711 -18
- package/dist/tools/bash.js.map +1 -1
- package/dist/tools/codebase/ast/__tests__/ast-edit-demo.d.ts +2 -0
- package/dist/tools/codebase/ast/__tests__/ast-edit-demo.d.ts.map +1 -0
- package/dist/tools/codebase/ast/__tests__/ast-edit-demo.js +7 -0
- package/dist/tools/codebase/ast/__tests__/ast-edit-demo.js.map +1 -0
- package/dist/tools/codebase/ast/__tests__/ast-edit-final.d.ts +6 -0
- package/dist/tools/codebase/ast/__tests__/ast-edit-final.d.ts.map +1 -0
- package/dist/tools/codebase/ast/__tests__/ast-edit-final.js +11 -0
- package/dist/tools/codebase/ast/__tests__/ast-edit-final.js.map +1 -0
- package/dist/tools/codebase/ast/__tests__/ast-edit-test.d.ts +2 -0
- package/dist/tools/codebase/ast/__tests__/ast-edit-test.d.ts.map +1 -0
- package/dist/tools/codebase/ast/__tests__/ast-edit-test.js +9 -0
- package/dist/tools/codebase/ast/__tests__/ast-edit-test.js.map +1 -0
- package/dist/tools/codebase/ast/__tests__/integration.test.d.ts +2 -0
- package/dist/tools/codebase/ast/__tests__/integration.test.d.ts.map +1 -0
- package/dist/tools/codebase/ast/__tests__/integration.test.js +415 -0
- package/dist/tools/codebase/ast/__tests__/integration.test.js.map +1 -0
- package/dist/tools/codebase/ast/__tests__/tree-sitter.test.d.ts +2 -0
- package/dist/tools/codebase/ast/__tests__/tree-sitter.test.d.ts.map +1 -0
- package/dist/tools/codebase/ast/__tests__/tree-sitter.test.js +414 -0
- package/dist/tools/codebase/ast/__tests__/tree-sitter.test.js.map +1 -0
- package/dist/tools/codebase/ast/adapter.d.ts +23 -0
- package/dist/tools/codebase/ast/adapter.d.ts.map +1 -0
- package/dist/tools/codebase/ast/adapter.js +11 -0
- package/dist/tools/codebase/ast/adapter.js.map +1 -0
- package/dist/tools/codebase/ast/tools.d.ts +3 -0
- package/dist/tools/codebase/ast/tools.d.ts.map +1 -0
- package/dist/tools/codebase/ast/tools.js +286 -0
- package/dist/tools/codebase/ast/tools.js.map +1 -0
- package/dist/tools/codebase/ast/tree-sitter-adapter.d.ts +3 -0
- package/dist/tools/codebase/ast/tree-sitter-adapter.d.ts.map +1 -0
- package/dist/tools/codebase/ast/tree-sitter-adapter.js +412 -0
- package/dist/tools/codebase/ast/tree-sitter-adapter.js.map +1 -0
- package/dist/tools/codebase/ast/tree-sitter-converters.d.ts +4 -0
- package/dist/tools/codebase/ast/tree-sitter-converters.d.ts.map +1 -0
- package/dist/tools/codebase/ast/tree-sitter-converters.js +447 -0
- package/dist/tools/codebase/ast/tree-sitter-converters.js.map +1 -0
- package/dist/tools/codebase/ast/types.d.ts +103 -0
- package/dist/tools/codebase/ast/types.d.ts.map +1 -0
- package/dist/tools/codebase/ast/types.js +2 -0
- package/dist/tools/codebase/ast/types.js.map +1 -0
- package/dist/tools/codebase/ast/wasm/tree-sitter-python.wasm +0 -0
- package/dist/tools/codebase/ast/wasm/tree-sitter-tsx.wasm +0 -0
- package/dist/tools/codebase/ast/wasm/tree-sitter-typescript.wasm +0 -0
- package/dist/tools/codebase/ast/wasm/tree-sitter.wasm +0 -0
- package/dist/tools/codebase/ast/wasm/web-tree-sitter.wasm +0 -0
- package/dist/tools/codebase/find.d.ts +3 -0
- package/dist/tools/codebase/find.d.ts.map +1 -0
- package/dist/tools/codebase/find.js +61 -0
- package/dist/tools/codebase/find.js.map +1 -0
- package/dist/tools/codebase/glob.d.ts +3 -0
- package/dist/tools/codebase/glob.d.ts.map +1 -0
- package/dist/tools/codebase/glob.js +37 -0
- package/dist/tools/codebase/glob.js.map +1 -0
- package/dist/tools/codebase/grep.d.ts +3 -0
- package/dist/tools/codebase/grep.d.ts.map +1 -0
- package/dist/tools/codebase/grep.js +169 -0
- package/dist/tools/codebase/grep.js.map +1 -0
- package/dist/tools/codebase/ls.d.ts +3 -0
- package/dist/tools/codebase/ls.d.ts.map +1 -0
- package/dist/tools/codebase/ls.js +39 -0
- package/dist/tools/codebase/ls.js.map +1 -0
- package/dist/tools/codebase/read.d.ts +10 -0
- package/dist/tools/codebase/read.d.ts.map +1 -0
- package/dist/tools/codebase/read.js +81 -0
- package/dist/tools/codebase/read.js.map +1 -0
- package/dist/tools/delete_memory.d.ts +3 -0
- package/dist/tools/delete_memory.d.ts.map +1 -0
- package/dist/tools/delete_memory.js +46 -0
- package/dist/tools/delete_memory.js.map +1 -0
- package/dist/tools/edit/diff-compute.d.ts +4 -0
- package/dist/tools/edit/diff-compute.d.ts.map +1 -0
- package/dist/tools/edit/diff-compute.js +18 -0
- package/dist/tools/edit/diff-compute.js.map +1 -0
- package/dist/tools/edit/diff-render.d.ts +18 -0
- package/dist/tools/edit/diff-render.d.ts.map +1 -0
- package/dist/tools/edit/diff-render.js +128 -0
- package/dist/tools/edit/diff-render.js.map +1 -0
- package/dist/tools/edit/edit.d.ts +4 -0
- package/dist/tools/edit/edit.d.ts.map +1 -0
- package/dist/tools/edit/edit.js +440 -0
- package/dist/tools/edit/edit.js.map +1 -0
- package/dist/tools/edit/matcher.d.ts +24 -0
- package/dist/tools/edit/matcher.d.ts.map +1 -0
- package/dist/tools/edit/matcher.js +66 -0
- package/dist/tools/edit/matcher.js.map +1 -0
- package/dist/tools/edit/parser.d.ts +10 -0
- package/dist/tools/edit/parser.d.ts.map +1 -0
- package/dist/tools/edit/parser.js +120 -0
- package/dist/tools/edit/parser.js.map +1 -0
- package/dist/tools/edit/types.d.ts +16 -0
- package/dist/tools/edit/types.d.ts.map +1 -0
- package/dist/tools/edit/types.js +2 -0
- package/dist/tools/edit/types.js.map +1 -0
- package/dist/tools/edit/write_plan.d.ts +5 -0
- package/dist/tools/edit/write_plan.d.ts.map +1 -0
- package/dist/tools/edit/write_plan.js +98 -0
- package/dist/tools/edit/write_plan.js.map +1 -0
- package/dist/tools/edit.d.ts +50 -9
- package/dist/tools/edit.d.ts.map +1 -1
- package/dist/tools/edit.js +394 -159
- package/dist/tools/edit.js.map +1 -1
- package/dist/tools/execute/bash/constants.d.ts +24 -0
- package/dist/tools/execute/bash/constants.d.ts.map +1 -0
- package/dist/tools/execute/bash/constants.js +88 -0
- package/dist/tools/execute/bash/constants.js.map +1 -0
- package/dist/tools/execute/bash/errors.d.ts +39 -0
- package/dist/tools/execute/bash/errors.d.ts.map +1 -0
- package/dist/tools/execute/bash/errors.js +249 -0
- package/dist/tools/execute/bash/errors.js.map +1 -0
- package/dist/tools/execute/bash/execution.d.ts +18 -0
- package/dist/tools/execute/bash/execution.d.ts.map +1 -0
- package/dist/tools/execute/bash/execution.js +207 -0
- package/dist/tools/execute/bash/execution.js.map +1 -0
- package/dist/tools/execute/bash/index.d.ts +3 -0
- package/dist/tools/execute/bash/index.d.ts.map +1 -0
- package/dist/tools/execute/bash/index.js +117 -0
- package/dist/tools/execute/bash/index.js.map +1 -0
- package/dist/tools/execute/bash/platform.d.ts +6 -0
- package/dist/tools/execute/bash/platform.d.ts.map +1 -0
- package/dist/tools/execute/bash/platform.js +64 -0
- package/dist/tools/execute/bash/platform.js.map +1 -0
- package/dist/tools/execute/bash/security.d.ts +12 -0
- package/dist/tools/execute/bash/security.d.ts.map +1 -0
- package/dist/tools/execute/bash/security.js +26 -0
- package/dist/tools/execute/bash/security.js.map +1 -0
- package/dist/tools/execute/bash/validation.d.ts +16 -0
- package/dist/tools/execute/bash/validation.d.ts.map +1 -0
- package/dist/tools/execute/bash/validation.js +57 -0
- package/dist/tools/execute/bash/validation.js.map +1 -0
- package/dist/tools/execute/bash.d.ts +3 -0
- package/dist/tools/execute/bash.d.ts.map +1 -0
- package/dist/tools/execute/bash.js +728 -0
- package/dist/tools/execute/bash.js.map +1 -0
- package/dist/tools/execute/git.d.ts +3 -0
- package/dist/tools/execute/git.d.ts.map +1 -0
- package/dist/tools/execute/git.js +183 -0
- package/dist/tools/execute/git.js.map +1 -0
- package/dist/tools/execute/task_complete.d.ts +7 -0
- package/dist/tools/execute/task_complete.d.ts.map +1 -0
- package/dist/tools/execute/task_complete.js +38 -0
- package/dist/tools/execute/task_complete.js.map +1 -0
- package/dist/tools/fetch.d.ts.map +1 -1
- package/dist/tools/fetch.js +2 -0
- package/dist/tools/fetch.js.map +1 -1
- package/dist/tools/find.d.ts.map +1 -1
- package/dist/tools/find.js +2 -0
- package/dist/tools/find.js.map +1 -1
- package/dist/tools/git.d.ts.map +1 -1
- package/dist/tools/git.js +2 -0
- package/dist/tools/git.js.map +1 -1
- package/dist/tools/glob.d.ts.map +1 -1
- package/dist/tools/glob.js +2 -0
- package/dist/tools/glob.js.map +1 -1
- package/dist/tools/grep.d.ts.map +1 -1
- package/dist/tools/grep.js +56 -30
- package/dist/tools/grep.js.map +1 -1
- package/dist/tools/install/clawhub.d.ts +26 -0
- package/dist/tools/install/clawhub.d.ts.map +1 -0
- package/dist/tools/install/clawhub.js +182 -0
- package/dist/tools/install/clawhub.js.map +1 -0
- package/dist/tools/install/constants.d.ts +5 -0
- package/dist/tools/install/constants.d.ts.map +1 -0
- package/dist/tools/install/constants.js +5 -0
- package/dist/tools/install/constants.js.map +1 -0
- package/dist/tools/install/install_skill.d.ts +3 -0
- package/dist/tools/install/install_skill.d.ts.map +1 -0
- package/dist/tools/install/install_skill.js +132 -0
- package/dist/tools/install/install_skill.js.map +1 -0
- package/dist/tools/install/npm.d.ts +8 -0
- package/dist/tools/install/npm.d.ts.map +1 -0
- package/dist/tools/install/npm.js +52 -0
- package/dist/tools/install/npm.js.map +1 -0
- package/dist/tools/install/skill-content.d.ts +9 -0
- package/dist/tools/install/skill-content.d.ts.map +1 -0
- package/dist/tools/install/skill-content.js +89 -0
- package/dist/tools/install/skill-content.js.map +1 -0
- package/dist/tools/install_skill.d.ts.map +1 -1
- package/dist/tools/install_skill.js +2 -0
- package/dist/tools/install_skill.js.map +1 -1
- package/dist/tools/list_memory.d.ts +3 -0
- package/dist/tools/list_memory.d.ts.map +1 -0
- package/dist/tools/list_memory.js +55 -0
- package/dist/tools/list_memory.js.map +1 -0
- package/dist/tools/ls.d.ts.map +1 -1
- package/dist/tools/ls.js +2 -0
- package/dist/tools/ls.js.map +1 -1
- package/dist/tools/memory/delete_memory.d.ts +3 -0
- package/dist/tools/memory/delete_memory.d.ts.map +1 -0
- package/dist/tools/memory/delete_memory.js +46 -0
- package/dist/tools/memory/delete_memory.js.map +1 -0
- package/dist/tools/memory/list_memory.d.ts +3 -0
- package/dist/tools/memory/list_memory.d.ts.map +1 -0
- package/dist/tools/memory/list_memory.js +55 -0
- package/dist/tools/memory/list_memory.js.map +1 -0
- package/dist/tools/memory/save_memory.d.ts +3 -0
- package/dist/tools/memory/save_memory.d.ts.map +1 -0
- package/dist/tools/memory/save_memory.js +45 -0
- package/dist/tools/memory/save_memory.js.map +1 -0
- package/dist/tools/read.d.ts.map +1 -1
- package/dist/tools/read.js +2 -0
- package/dist/tools/read.js.map +1 -1
- package/dist/tools/registry.d.ts +7 -8
- package/dist/tools/registry.d.ts.map +1 -1
- package/dist/tools/registry.js +114 -46
- package/dist/tools/registry.js.map +1 -1
- package/dist/tools/save_memory.d.ts +3 -0
- package/dist/tools/save_memory.d.ts.map +1 -0
- package/dist/tools/save_memory.js +45 -0
- package/dist/tools/save_memory.js.map +1 -0
- package/dist/tools/search.d.ts.map +1 -1
- package/dist/tools/search.js +2 -0
- package/dist/tools/search.js.map +1 -1
- package/dist/tools/sed.d.ts +4 -0
- package/dist/tools/sed.d.ts.map +1 -0
- package/dist/tools/sed.js +121 -0
- package/dist/tools/sed.js.map +1 -0
- package/dist/tools/tree.d.ts +12 -0
- package/dist/tools/tree.d.ts.map +1 -0
- package/dist/tools/tree.js +87 -0
- package/dist/tools/tree.js.map +1 -0
- package/dist/tools/types.d.ts +12 -0
- package/dist/tools/types.d.ts.map +1 -1
- package/dist/tools/web/fetch.d.ts +3 -0
- package/dist/tools/web/fetch.d.ts.map +1 -0
- package/dist/tools/web/fetch.js +49 -0
- package/dist/tools/web/fetch.js.map +1 -0
- package/dist/tools/web/search.d.ts +3 -0
- package/dist/tools/web/search.d.ts.map +1 -0
- package/dist/tools/web/search.js +134 -0
- package/dist/tools/web/search.js.map +1 -0
- package/dist/tools/write_plan.d.ts.map +1 -1
- package/dist/tools/write_plan.js +2 -0
- package/dist/tools/write_plan.js.map +1 -1
- package/dist/tui/commands.d.ts +4 -0
- package/dist/tui/commands.d.ts.map +1 -0
- package/dist/tui/commands.js +118 -0
- package/dist/tui/commands.js.map +1 -0
- package/dist/tui/components/colors.d.ts +36 -0
- package/dist/tui/components/colors.d.ts.map +1 -0
- package/dist/tui/components/colors.js +48 -0
- package/dist/tui/components/colors.js.map +1 -0
- package/dist/tui/components/header-bar.d.ts +26 -0
- package/dist/tui/components/header-bar.d.ts.map +1 -0
- package/dist/tui/components/header-bar.js +78 -0
- package/dist/tui/components/header-bar.js.map +1 -0
- package/dist/tui/components/index.d.ts +9 -0
- package/dist/tui/components/index.d.ts.map +1 -0
- package/dist/tui/components/index.js +9 -0
- package/dist/tui/components/index.js.map +1 -0
- package/dist/tui/components/landing-screen.d.ts +12 -0
- package/dist/tui/components/landing-screen.d.ts.map +1 -0
- package/dist/tui/components/landing-screen.js +53 -0
- package/dist/tui/components/landing-screen.js.map +1 -0
- package/dist/tui/components/pixel-logo.d.ts +5 -0
- package/dist/tui/components/pixel-logo.d.ts.map +1 -0
- package/dist/tui/components/pixel-logo.js +27 -0
- package/dist/tui/components/pixel-logo.js.map +1 -0
- package/dist/tui/components/plan-utils.d.ts +11 -0
- package/dist/tui/components/plan-utils.d.ts.map +1 -0
- package/dist/tui/components/plan-utils.js +62 -0
- package/dist/tui/components/plan-utils.js.map +1 -0
- package/dist/tui/components/plans-list.d.ts +18 -0
- package/dist/tui/components/plans-list.d.ts.map +1 -0
- package/dist/tui/components/plans-list.js +46 -0
- package/dist/tui/components/plans-list.js.map +1 -0
- package/dist/tui/components/rich-footer.d.ts +29 -0
- package/dist/tui/components/rich-footer.d.ts.map +1 -0
- package/dist/tui/components/rich-footer.js +124 -0
- package/dist/tui/components/rich-footer.js.map +1 -0
- package/dist/tui/components/todo-panel.d.ts +14 -0
- package/dist/tui/components/todo-panel.d.ts.map +1 -0
- package/dist/tui/components/todo-panel.js +83 -0
- package/dist/tui/components/todo-panel.js.map +1 -0
- package/dist/tui/components.d.ts.map +1 -1
- package/dist/tui/components.js +14 -7
- package/dist/tui/components.js.map +1 -1
- package/dist/tui/index.d.ts +2 -1
- package/dist/tui/index.d.ts.map +1 -1
- package/dist/tui/index.js +99 -505
- package/dist/tui/index.js.map +1 -1
- package/dist/tui/overlays.d.ts +28 -0
- package/dist/tui/overlays.d.ts.map +1 -0
- package/dist/tui/overlays.js +154 -0
- package/dist/tui/overlays.js.map +1 -0
- package/dist/tui/themes.d.ts +5 -0
- package/dist/tui/themes.d.ts.map +1 -0
- package/dist/tui/themes.js +38 -0
- package/dist/tui/themes.js.map +1 -0
- package/dist/web/index.d.ts.map +1 -1
- package/dist/web/index.js +62 -101
- package/dist/web/index.js.map +1 -1
- package/dist/web/public/app.js +5 -855
- package/dist/web/public/confirm.js +70 -0
- package/dist/web/public/index.html +121 -120
- package/dist/web/public/input.js +107 -0
- package/dist/web/public/messages.js +411 -0
- package/dist/web/public/sidebar.js +82 -0
- package/dist/web/public/state.js +72 -0
- package/dist/web/public/style.css +247 -70
- package/dist/web/public/utils.js +37 -0
- package/dist/web/public/websocket.js +267 -0
- package/dist/web/public/ws.js +19 -0
- package/dist/web/session-bridge.d.ts.map +1 -1
- package/dist/web/session-bridge.js +85 -10
- package/dist/web/session-bridge.js.map +1 -1
- package/docs/ast-tools.md +117 -0
- package/package.json +11 -5
- package/scripts/check-line-count.mjs +53 -0
- package/scripts/copy-ast-wasm.mjs +32 -0
- package/scripts/copy-native.mjs +24 -0
- package/scripts/copy-web.mjs +15 -0
- package/src/agent/__tests__/session-restore.test.ts +284 -0
- package/src/agent/commands.ts +274 -0
- package/src/agent/compaction.ts +9 -2
- package/src/agent/index.ts +7 -2
- package/src/agent/llm.ts +4 -0
- package/src/agent/memory.ts +60 -0
- package/src/agent/prompt-builder.ts +111 -33
- package/src/agent/providers/google.ts +0 -1
- package/src/agent/providers/openai.ts +79 -8
- package/src/agent/session-chat.ts +480 -0
- package/src/agent/session-display.ts +195 -0
- package/src/agent/session-persistence.ts +146 -0
- package/src/agent/session-utils.ts +54 -0
- package/src/agent/session.ts +269 -670
- package/src/cli/index.ts +73 -4
- package/src/config/index.ts +14 -7
- package/src/index.ts +30 -3
- package/src/tools/__tests__/bash.test.ts +188 -2
- package/src/tools/__tests__/edit.test.ts +1216 -356
- package/src/tools/__tests__/fetch.test.ts +1 -1
- package/src/tools/__tests__/glob.test.ts +1 -1
- package/src/tools/__tests__/grep.test.ts +36 -1
- package/src/tools/__tests__/ls.test.ts +1 -1
- package/src/tools/__tests__/read.test.ts +1 -1
- package/src/tools/__tests__/registry.test.ts +332 -0
- package/src/tools/__tests__/write_plan.test.ts +1 -1
- package/src/tools/codebase/ast/__tests__/integration.test.ts +452 -0
- package/src/tools/codebase/ast/__tests__/tree-sitter.test.ts +466 -0
- package/src/tools/codebase/ast/adapter.ts +44 -0
- package/src/tools/codebase/ast/tools.ts +322 -0
- package/src/tools/codebase/ast/tree-sitter-adapter.ts +468 -0
- package/src/tools/codebase/ast/tree-sitter-converters.ts +480 -0
- package/src/tools/codebase/ast/types.ts +102 -0
- package/src/tools/codebase/ast/wasm/tree-sitter-python.wasm +0 -0
- package/src/tools/codebase/ast/wasm/tree-sitter-tsx.wasm +0 -0
- package/src/tools/codebase/ast/wasm/tree-sitter-typescript.wasm +0 -0
- package/src/tools/codebase/ast/wasm/tree-sitter.wasm +0 -0
- package/src/tools/codebase/ast/wasm/web-tree-sitter.wasm +0 -0
- package/src/tools/{find.ts → codebase/find.ts} +4 -2
- package/src/tools/{glob.ts → codebase/glob.ts} +4 -2
- package/src/tools/{grep.ts → codebase/grep.ts} +61 -32
- package/src/tools/{ls.ts → codebase/ls.ts} +4 -2
- package/src/tools/{read.ts → codebase/read.ts} +5 -3
- package/src/tools/edit/diff-compute.ts +21 -0
- package/src/tools/edit/diff-render.ts +145 -0
- package/src/tools/edit/edit.ts +468 -0
- package/src/tools/edit/matcher.ts +70 -0
- package/src/tools/edit/parser.ts +131 -0
- package/src/tools/edit/types.ts +19 -0
- package/src/tools/{write_plan.ts → edit/write_plan.ts} +4 -2
- package/src/tools/execute/bash/constants.ts +93 -0
- package/src/tools/execute/bash/errors.ts +304 -0
- package/src/tools/execute/bash/execution.ts +211 -0
- package/src/tools/execute/bash/index.ts +131 -0
- package/src/tools/execute/bash/platform.ts +65 -0
- package/src/tools/execute/bash/security.ts +28 -0
- package/src/tools/execute/bash/validation.ts +78 -0
- package/src/tools/execute/git.ts +209 -0
- package/src/tools/execute/task_complete.ts +40 -0
- package/src/tools/install/clawhub.ts +222 -0
- package/src/tools/install/constants.ts +4 -0
- package/src/tools/install/install_skill.ts +150 -0
- package/src/tools/install/npm.ts +60 -0
- package/src/tools/install/skill-content.ts +108 -0
- package/src/tools/memory/delete_memory.ts +46 -0
- package/src/tools/memory/list_memory.ts +53 -0
- package/src/tools/memory/save_memory.ts +45 -0
- package/src/tools/registry.ts +121 -50
- package/src/tools/tree.ts +103 -0
- package/src/tools/types.ts +13 -0
- package/src/tools/{fetch.ts → web/fetch.ts} +4 -2
- package/src/tools/{search.ts → web/search.ts} +4 -2
- package/src/tui/commands.ts +135 -0
- package/src/tui/components/colors.ts +52 -0
- package/src/tui/components/header-bar.ts +88 -0
- package/src/tui/components/index.ts +8 -0
- package/src/tui/components/landing-screen.ts +67 -0
- package/src/tui/components/pixel-logo.ts +33 -0
- package/src/tui/components/plan-utils.ts +69 -0
- package/src/tui/components/plans-list.ts +55 -0
- package/src/tui/components/rich-footer.ts +146 -0
- package/src/tui/components/todo-panel.ts +95 -0
- package/src/tui/index.ts +110 -561
- package/src/tui/overlays.ts +203 -0
- package/src/tui/themes.ts +41 -0
- package/src/web/index.ts +66 -107
- package/src/web/public/app.js +5 -855
- package/src/web/public/confirm.js +70 -0
- package/src/web/public/index.html +121 -120
- package/src/web/public/input.js +107 -0
- package/src/web/public/messages.js +411 -0
- package/src/web/public/sidebar.js +82 -0
- package/src/web/public/state.js +72 -0
- package/src/web/public/style.css +247 -70
- package/src/web/public/utils.js +37 -0
- package/src/web/public/websocket.js +267 -0
- package/src/web/public/ws.js +19 -0
- package/src/web/session-bridge.ts +90 -12
- package/.claude/settings.local.json +0 -14
- package/.kilo/plans/1780064105789-mighty-pixel.md +0 -171
- package/.kilo/plans/1780293725888-quick-wizard.md +0 -62
- package/.lonny/plan-web-cwd-status.md +0 -38
- package/AGENT.md +0 -99
- package/src/tools/bash.ts +0 -39
- package/src/tools/edit.ts +0 -554
- package/src/tools/exec.ts +0 -348
- package/src/tools/git.ts +0 -76
- package/src/tools/install_skill.ts +0 -539
- package/src/tui/components.ts +0 -635
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { readdirSync, readFileSync } from 'node:fs'
|
|
2
|
+
import { dirname, join, relative } from 'node:path'
|
|
3
|
+
import { fileURLToPath } from 'node:url'
|
|
4
|
+
|
|
5
|
+
const MAX_LINES = 500
|
|
6
|
+
const __dirname = dirname(fileURLToPath(import.meta.url))
|
|
7
|
+
const ROOT = join(__dirname, '..')
|
|
8
|
+
const SRC = join(ROOT, 'src')
|
|
9
|
+
|
|
10
|
+
/** Recursively collect .ts files in a directory (skip node_modules, dist, __tests__). */
|
|
11
|
+
function walk(dir) {
|
|
12
|
+
const entries = readdirSync(dir, { withFileTypes: true })
|
|
13
|
+
const files = []
|
|
14
|
+
for (const entry of entries) {
|
|
15
|
+
const full = join(dir, entry.name)
|
|
16
|
+
if (entry.isDirectory()) {
|
|
17
|
+
if (
|
|
18
|
+
entry.name === 'node_modules' ||
|
|
19
|
+
entry.name === 'dist' ||
|
|
20
|
+
entry.name === '__tests__' ||
|
|
21
|
+
entry.name === 'pi-tui' ||
|
|
22
|
+
entry.name.startsWith('.')
|
|
23
|
+
)
|
|
24
|
+
continue
|
|
25
|
+
files.push(...walk(full))
|
|
26
|
+
} else if (entry.name.endsWith('.ts') && !entry.name.endsWith('.d.ts')) {
|
|
27
|
+
files.push(full)
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
return files
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
let exitCode = 0
|
|
34
|
+
const files = walk(SRC)
|
|
35
|
+
|
|
36
|
+
for (const file of files) {
|
|
37
|
+
try {
|
|
38
|
+
const content = readFileSync(file, 'utf-8')
|
|
39
|
+
const lines = content.split('\n').length
|
|
40
|
+
if (lines > MAX_LINES) {
|
|
41
|
+
const rel = relative(ROOT, file).replace(/\\/g, '/')
|
|
42
|
+
console.log(`⚠ ${rel} (${lines} lines) exceeds ${MAX_LINES}-line limit`)
|
|
43
|
+
exitCode = 1
|
|
44
|
+
}
|
|
45
|
+
} catch {
|
|
46
|
+
// skip unreadable files
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
if (exitCode === 0) {
|
|
51
|
+
console.log(`✓ All ${files.length} source files are under ${MAX_LINES} lines`)
|
|
52
|
+
}
|
|
53
|
+
process.exit(exitCode)
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { copyFileSync, mkdirSync } from 'node:fs'
|
|
2
|
+
import { createRequire } from 'node:module'
|
|
3
|
+
import { dirname, join } from 'node:path'
|
|
4
|
+
import { fileURLToPath } from 'node:url'
|
|
5
|
+
|
|
6
|
+
const __dirname = dirname(fileURLToPath(import.meta.url))
|
|
7
|
+
const require = createRequire(import.meta.url)
|
|
8
|
+
|
|
9
|
+
const targets = [
|
|
10
|
+
join(__dirname, '..', 'src', 'tools', 'codebase', 'ast', 'wasm'),
|
|
11
|
+
join(__dirname, '..', 'dist', 'tools', 'codebase', 'ast', 'wasm'),
|
|
12
|
+
]
|
|
13
|
+
|
|
14
|
+
const wasmMap = [
|
|
15
|
+
{ id: 'web-tree-sitter/web-tree-sitter.wasm', out: 'web-tree-sitter.wasm' },
|
|
16
|
+
{ id: 'tree-sitter-typescript/tree-sitter-typescript.wasm', out: 'tree-sitter-typescript.wasm' },
|
|
17
|
+
{ id: 'tree-sitter-typescript/tree-sitter-tsx.wasm', out: 'tree-sitter-tsx.wasm' },
|
|
18
|
+
{ id: 'tree-sitter-python/tree-sitter-python.wasm', out: 'tree-sitter-python.wasm' },
|
|
19
|
+
]
|
|
20
|
+
|
|
21
|
+
for (const dest of targets) {
|
|
22
|
+
mkdirSync(dest, { recursive: true })
|
|
23
|
+
for (const { id, out } of wasmMap) {
|
|
24
|
+
try {
|
|
25
|
+
const src = require.resolve(id)
|
|
26
|
+
copyFileSync(src, join(dest, out))
|
|
27
|
+
console.log(` ✓ ${dest.endsWith('wasm') ? out : out} → ${dest.slice(dest.indexOf('tools'))}`)
|
|
28
|
+
} catch (err) {
|
|
29
|
+
console.warn(` ⚠ Failed to copy ${out}: ${err.message}`)
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
// copy:native — recursively copies src/pi-tui/native/ to dist/pi-tui/native/
|
|
2
|
+
import { copyFileSync, existsSync, mkdirSync, readdirSync, statSync } from 'fs'
|
|
3
|
+
import { join } from 'path'
|
|
4
|
+
import { fileURLToPath } from 'url'
|
|
5
|
+
|
|
6
|
+
const __dirname = fileURLToPath(new URL('..', import.meta.url))
|
|
7
|
+
|
|
8
|
+
function cpDir(s, d) {
|
|
9
|
+
mkdirSync(d, { recursive: true })
|
|
10
|
+
for (const e of readdirSync(s)) {
|
|
11
|
+
const sp = join(s, e)
|
|
12
|
+
const dp = join(d, e)
|
|
13
|
+
if (statSync(sp).isDirectory()) {
|
|
14
|
+
cpDir(sp, dp)
|
|
15
|
+
} else {
|
|
16
|
+
copyFileSync(sp, dp)
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const src = join(__dirname, 'src', 'pi-tui', 'native')
|
|
22
|
+
const dst = join(__dirname, 'dist', 'pi-tui', 'native')
|
|
23
|
+
|
|
24
|
+
if (existsSync(src)) cpDir(src, dst)
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
// copy:web — copies src/web/public/ to dist/web/public/
|
|
2
|
+
import { copyFileSync, existsSync, mkdirSync, readdirSync } from 'fs'
|
|
3
|
+
import { join } from 'path'
|
|
4
|
+
import { fileURLToPath } from 'url'
|
|
5
|
+
|
|
6
|
+
const __dirname = fileURLToPath(new URL('..', import.meta.url))
|
|
7
|
+
const src = join(__dirname, 'src', 'web', 'public')
|
|
8
|
+
const dst = join(__dirname, 'dist', 'web', 'public')
|
|
9
|
+
|
|
10
|
+
if (existsSync(src)) {
|
|
11
|
+
if (!existsSync(dst)) mkdirSync(dst, { recursive: true })
|
|
12
|
+
for (const f of readdirSync(src)) {
|
|
13
|
+
copyFileSync(join(src, f), join(dst, f))
|
|
14
|
+
}
|
|
15
|
+
}
|
|
@@ -0,0 +1,284 @@
|
|
|
1
|
+
import * as crypto from 'node:crypto'
|
|
2
|
+
import * as fs from 'node:fs'
|
|
3
|
+
import * as os from 'node:os'
|
|
4
|
+
import * as path from 'node:path'
|
|
5
|
+
import { afterEach, beforeEach, describe, expect, test } from 'vitest'
|
|
6
|
+
import type { Config } from '../../config/index.js'
|
|
7
|
+
import type { LLMMessage } from '../llm.js'
|
|
8
|
+
import { Session } from '../session.js'
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Helper: build a minimal Config with a project-subdir cwd for test session files.
|
|
12
|
+
* Using the project directory ensures detectPackageManager etc. find a valid cwd.
|
|
13
|
+
* Each test gets a fresh subdirectory that gets cleaned up after.
|
|
14
|
+
*/
|
|
15
|
+
function testConfig(): Config {
|
|
16
|
+
const testDir = path.join(process.cwd(), '.test-sessions', crypto.randomUUID().slice(0, 8))
|
|
17
|
+
fs.mkdirSync(testDir, { recursive: true })
|
|
18
|
+
// Create a package.json so detectPackageManager doesn't fail async
|
|
19
|
+
fs.writeFileSync(path.join(testDir, 'package.json'), JSON.stringify({ name: 'test' }), 'utf-8')
|
|
20
|
+
return {
|
|
21
|
+
mode: 'code',
|
|
22
|
+
model: 'test-model',
|
|
23
|
+
provider: 'openai',
|
|
24
|
+
apiKey: 'test-key',
|
|
25
|
+
baseUrl: 'http://test',
|
|
26
|
+
cwd: testDir,
|
|
27
|
+
autoApprove: true,
|
|
28
|
+
thinking: false,
|
|
29
|
+
reasoningEffort: 'medium',
|
|
30
|
+
enableCache: false,
|
|
31
|
+
strictTools: false,
|
|
32
|
+
contextWindow: 128_000,
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Build the session file path matching what save() would write.
|
|
38
|
+
*/
|
|
39
|
+
function sessionFilePath(cwd: string, sessionId: string): string {
|
|
40
|
+
const { createHash } = require('node:crypto')
|
|
41
|
+
const absPath = path.resolve(cwd)
|
|
42
|
+
const hash = createHash('sha256').update(absPath, 'utf-8').digest('hex').slice(0, 12)
|
|
43
|
+
const dirName = path.basename(absPath)
|
|
44
|
+
const safeName = dirName.replace(/[<>:"/\\|?*]/g, '_')
|
|
45
|
+
const base = `${safeName}-${hash}`
|
|
46
|
+
const sessionDir = path.join(os.homedir(), '.lonny', 'sessions')
|
|
47
|
+
return path.join(sessionDir, `${base}-${sessionId}.json`)
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
describe('Session save/load cycle', () => {
|
|
51
|
+
let config: Config
|
|
52
|
+
let sessionId: string
|
|
53
|
+
let filePath: string
|
|
54
|
+
|
|
55
|
+
beforeEach(() => {
|
|
56
|
+
config = testConfig()
|
|
57
|
+
const { randomUUID } = require('node:crypto')
|
|
58
|
+
sessionId = randomUUID().slice(0, 8)
|
|
59
|
+
filePath = sessionFilePath(config.cwd, sessionId)
|
|
60
|
+
// Ensure session directory exists
|
|
61
|
+
const dir = path.dirname(filePath)
|
|
62
|
+
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true })
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
afterEach(() => {
|
|
66
|
+
try {
|
|
67
|
+
if (fs.existsSync(filePath)) fs.unlinkSync(filePath)
|
|
68
|
+
} catch {
|
|
69
|
+
/* ignore */
|
|
70
|
+
}
|
|
71
|
+
// Don't clean up .test-sessions dir — buildSystemPrompt fires async and may
|
|
72
|
+
// still be reading the cwd. The dir is gitignored and cleaned on restart.
|
|
73
|
+
})
|
|
74
|
+
|
|
75
|
+
test('restores assistant message with text + tool_calls + tool results', async () => {
|
|
76
|
+
const messages: LLMMessage[] = [
|
|
77
|
+
{ role: 'system', content: 'You are a helpful assistant.' },
|
|
78
|
+
{ role: 'user', content: 'Fix the bug in the code.' },
|
|
79
|
+
{
|
|
80
|
+
role: 'assistant',
|
|
81
|
+
content: 'I see the root cause. Three fixes needed:\n\n1. Remove X\n2. Add Y',
|
|
82
|
+
tool_calls: [
|
|
83
|
+
{ id: 'call_abc', name: 'edit', input: { file: 'test.ts', old: 'a', new: 'b' } },
|
|
84
|
+
{ id: 'call_def', name: 'read', input: { file: 'test.ts' } },
|
|
85
|
+
],
|
|
86
|
+
},
|
|
87
|
+
{ role: 'tool', content: 'Edited test.ts', tool_call_id: 'call_abc', name: 'edit' },
|
|
88
|
+
{ role: 'tool', content: 'File contents...', tool_call_id: 'call_def', name: 'read' },
|
|
89
|
+
{ role: 'assistant', content: 'Done with fixes.' },
|
|
90
|
+
{ role: 'user', content: 'Great, thanks!' },
|
|
91
|
+
]
|
|
92
|
+
|
|
93
|
+
// Write session file (simulating what save() would produce)
|
|
94
|
+
const data = {
|
|
95
|
+
id: sessionId,
|
|
96
|
+
cwd: path.resolve(config.cwd),
|
|
97
|
+
messages,
|
|
98
|
+
totalInputTokens: 100,
|
|
99
|
+
totalOutputTokens: 50,
|
|
100
|
+
totalApiCalls: 2,
|
|
101
|
+
mode: 'code',
|
|
102
|
+
model: 'test-model',
|
|
103
|
+
provider: 'test-provider',
|
|
104
|
+
createdAt: new Date().toISOString(),
|
|
105
|
+
updatedAt: new Date().toISOString(),
|
|
106
|
+
}
|
|
107
|
+
fs.writeFileSync(filePath, JSON.stringify(data, null, 2), 'utf-8')
|
|
108
|
+
|
|
109
|
+
// Load the session from disk
|
|
110
|
+
const session = await Session.load(config)
|
|
111
|
+
expect(session).not.toBeNull()
|
|
112
|
+
|
|
113
|
+
// Verify ALL messages are preserved
|
|
114
|
+
expect(session!.messages.length).toBe(messages.length)
|
|
115
|
+
for (let i = 0; i < messages.length; i++) {
|
|
116
|
+
expect(session!.messages[i].role).toBe(messages[i].role)
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// Verify the critical assistant message (text + tool_calls)
|
|
120
|
+
const assistantWithTools = session!.messages.find(
|
|
121
|
+
m => m.role === 'assistant' && m.tool_calls && m.tool_calls.length > 0,
|
|
122
|
+
)
|
|
123
|
+
expect(assistantWithTools).toBeDefined()
|
|
124
|
+
expect(assistantWithTools!.content).toContain('I see the root cause')
|
|
125
|
+
expect(assistantWithTools!.tool_calls).toHaveLength(2)
|
|
126
|
+
|
|
127
|
+
// Verify tool results still reference the correct tool_call_ids
|
|
128
|
+
const toolMsgs = session!.messages.filter(m => m.role === 'tool')
|
|
129
|
+
expect(toolMsgs).toHaveLength(2)
|
|
130
|
+
expect(toolMsgs[0].tool_call_id).toBe('call_abc')
|
|
131
|
+
expect(toolMsgs[1].tool_call_id).toBe('call_def')
|
|
132
|
+
})
|
|
133
|
+
|
|
134
|
+
test('preserves assistant message with text-only (no tool_calls)', async () => {
|
|
135
|
+
const messages: LLMMessage[] = [
|
|
136
|
+
{ role: 'system', content: 'system prompt' },
|
|
137
|
+
{ role: 'user', content: 'Hello' },
|
|
138
|
+
{ role: 'assistant', content: 'Hi there!' },
|
|
139
|
+
{ role: 'user', content: 'How are you?' },
|
|
140
|
+
{ role: 'assistant', content: "I'm great, thanks!" },
|
|
141
|
+
]
|
|
142
|
+
|
|
143
|
+
const data = {
|
|
144
|
+
id: sessionId,
|
|
145
|
+
cwd: path.resolve(config.cwd),
|
|
146
|
+
messages,
|
|
147
|
+
totalInputTokens: 0,
|
|
148
|
+
totalOutputTokens: 0,
|
|
149
|
+
totalApiCalls: 0,
|
|
150
|
+
mode: 'code',
|
|
151
|
+
model: 'test-model',
|
|
152
|
+
provider: 'test-provider',
|
|
153
|
+
createdAt: new Date().toISOString(),
|
|
154
|
+
updatedAt: new Date().toISOString(),
|
|
155
|
+
}
|
|
156
|
+
fs.writeFileSync(filePath, JSON.stringify(data, null, 2), 'utf-8')
|
|
157
|
+
|
|
158
|
+
const session = await Session.load(config)
|
|
159
|
+
expect(session).not.toBeNull()
|
|
160
|
+
expect(session!.messages.length).toBe(messages.length)
|
|
161
|
+
expect(session!.messages[2].content).toBe('Hi there!')
|
|
162
|
+
expect(session!.messages[4].content).toBe("I'm great, thanks!")
|
|
163
|
+
})
|
|
164
|
+
|
|
165
|
+
test('loads session with most messages when multiple session files exist', async () => {
|
|
166
|
+
// Create a "newer but emptier" session file
|
|
167
|
+
const newSessionId = 'new_empty_' + require('node:crypto').randomUUID().slice(0, 4)
|
|
168
|
+
const newFilePath = sessionFilePath(config.cwd, newSessionId)
|
|
169
|
+
const emptyMessages: LLMMessage[] = [
|
|
170
|
+
{ role: 'system', content: 'system prompt' },
|
|
171
|
+
{ role: 'user', content: 'Hello' },
|
|
172
|
+
]
|
|
173
|
+
const newData = {
|
|
174
|
+
id: newSessionId,
|
|
175
|
+
cwd: path.resolve(config.cwd),
|
|
176
|
+
messages: emptyMessages,
|
|
177
|
+
totalInputTokens: 0,
|
|
178
|
+
totalOutputTokens: 0,
|
|
179
|
+
totalApiCalls: 0,
|
|
180
|
+
mode: 'code',
|
|
181
|
+
model: 'test-model',
|
|
182
|
+
provider: 'test-provider',
|
|
183
|
+
createdAt: new Date(Date.now() + 60_000).toISOString(), // 1 minute in the future
|
|
184
|
+
updatedAt: new Date(Date.now() + 60_000).toISOString(), // 1 minute in the future
|
|
185
|
+
}
|
|
186
|
+
fs.writeFileSync(newFilePath, JSON.stringify(newData, null, 2), 'utf-8')
|
|
187
|
+
|
|
188
|
+
// Create an "older but fuller" session file
|
|
189
|
+
const fullMessages: LLMMessage[] = [
|
|
190
|
+
{ role: 'system', content: 'system prompt' },
|
|
191
|
+
{ role: 'user', content: 'Do the first thing.' },
|
|
192
|
+
{
|
|
193
|
+
role: 'assistant',
|
|
194
|
+
content: 'Working on it.',
|
|
195
|
+
tool_calls: [{ id: 't1', name: 'bash', input: { command: 'echo 1' } }],
|
|
196
|
+
},
|
|
197
|
+
{ role: 'tool', content: '1', tool_call_id: 't1', name: 'bash' },
|
|
198
|
+
{ role: 'user', content: 'Now the second thing.' },
|
|
199
|
+
{ role: 'assistant', content: 'Doing it.' },
|
|
200
|
+
{ role: 'user', content: 'Great!' },
|
|
201
|
+
]
|
|
202
|
+
|
|
203
|
+
const data = {
|
|
204
|
+
id: sessionId,
|
|
205
|
+
cwd: path.resolve(config.cwd),
|
|
206
|
+
messages: fullMessages,
|
|
207
|
+
totalInputTokens: 0,
|
|
208
|
+
totalOutputTokens: 0,
|
|
209
|
+
totalApiCalls: 0,
|
|
210
|
+
mode: 'code',
|
|
211
|
+
model: 'test-model',
|
|
212
|
+
provider: 'test-provider',
|
|
213
|
+
createdAt: new Date().toISOString(),
|
|
214
|
+
updatedAt: new Date().toISOString(), // OLDER timestamp
|
|
215
|
+
}
|
|
216
|
+
fs.writeFileSync(filePath, JSON.stringify(data, null, 2), 'utf-8')
|
|
217
|
+
|
|
218
|
+
// Load — should pick the FULL session (7 messages) over the NEWER but EMPTIER one (2 messages)
|
|
219
|
+
const session = await Session.load(config)
|
|
220
|
+
expect(session).not.toBeNull()
|
|
221
|
+
expect(session!.messages.length).toBe(7)
|
|
222
|
+
expect(session!.messages[1].content).toBe('Do the first thing.')
|
|
223
|
+
|
|
224
|
+
// Clean up the extra file
|
|
225
|
+
try {
|
|
226
|
+
fs.unlinkSync(newFilePath)
|
|
227
|
+
} catch {
|
|
228
|
+
/* ignore */
|
|
229
|
+
}
|
|
230
|
+
})
|
|
231
|
+
|
|
232
|
+
test('sanitizeMessages keeps complete assistant+tool sequences', async () => {
|
|
233
|
+
// Two complete turns, each with tool_calls+results — should all survive
|
|
234
|
+
const messages: LLMMessage[] = [
|
|
235
|
+
{ role: 'system', content: 'system prompt' },
|
|
236
|
+
{ role: 'user', content: 'Do the first thing.' },
|
|
237
|
+
{
|
|
238
|
+
role: 'assistant',
|
|
239
|
+
content: 'Working on it.',
|
|
240
|
+
tool_calls: [{ id: 't1', name: 'bash', input: { command: 'echo 1' } }],
|
|
241
|
+
},
|
|
242
|
+
{ role: 'tool', content: '1', tool_call_id: 't1', name: 'bash' },
|
|
243
|
+
{ role: 'user', content: 'Now the second thing.' },
|
|
244
|
+
{
|
|
245
|
+
role: 'assistant',
|
|
246
|
+
content: 'Doing it.',
|
|
247
|
+
tool_calls: [
|
|
248
|
+
{ id: 't2', name: 'bash', input: { command: 'echo 2' } },
|
|
249
|
+
{ id: 't3', name: 'edit', input: { file: 'x.ts', old: 'a', new: 'b' } },
|
|
250
|
+
],
|
|
251
|
+
},
|
|
252
|
+
{ role: 'tool', content: '2', tool_call_id: 't2', name: 'bash' },
|
|
253
|
+
{ role: 'tool', content: 'Edited x.ts', tool_call_id: 't3', name: 'edit' },
|
|
254
|
+
{ role: 'assistant', content: 'Done!' },
|
|
255
|
+
]
|
|
256
|
+
|
|
257
|
+
const data = {
|
|
258
|
+
id: sessionId,
|
|
259
|
+
cwd: path.resolve(config.cwd),
|
|
260
|
+
messages,
|
|
261
|
+
totalInputTokens: 0,
|
|
262
|
+
totalOutputTokens: 0,
|
|
263
|
+
totalApiCalls: 0,
|
|
264
|
+
mode: 'code',
|
|
265
|
+
model: 'test-model',
|
|
266
|
+
provider: 'test-provider',
|
|
267
|
+
createdAt: new Date().toISOString(),
|
|
268
|
+
updatedAt: new Date().toISOString(),
|
|
269
|
+
}
|
|
270
|
+
fs.writeFileSync(filePath, JSON.stringify(data, null, 2), 'utf-8')
|
|
271
|
+
|
|
272
|
+
const session = await Session.load(config)
|
|
273
|
+
expect(session).not.toBeNull()
|
|
274
|
+
expect(session!.messages.length).toBe(messages.length)
|
|
275
|
+
|
|
276
|
+
// Both assistant messages with tool_calls should be present
|
|
277
|
+
const assistantWithTools = session!.messages.filter(
|
|
278
|
+
m => m.role === 'assistant' && m.tool_calls && m.tool_calls.length > 0,
|
|
279
|
+
)
|
|
280
|
+
expect(assistantWithTools).toHaveLength(2)
|
|
281
|
+
expect(assistantWithTools[0].content).toBe('Working on it.')
|
|
282
|
+
expect(assistantWithTools[1].content).toBe('Doing it.')
|
|
283
|
+
})
|
|
284
|
+
})
|
|
@@ -0,0 +1,274 @@
|
|
|
1
|
+
import type { Config } from '../config/index.js'
|
|
2
|
+
import { resetTokenUsage } from '../config/tokens.js'
|
|
3
|
+
import { fmtErr } from '../tools/errors.js'
|
|
4
|
+
import { resetGlobalEventBus } from './event-bus.js'
|
|
5
|
+
import type { LLMMessage } from './llm.js'
|
|
6
|
+
import { ensurePromptsDir, loadPromptTemplates } from './prompt-templates.js'
|
|
7
|
+
import { Session } from './session.js'
|
|
8
|
+
import { ensureSkillsDir, loadSkills } from './skills.js'
|
|
9
|
+
|
|
10
|
+
export interface CommandUI {
|
|
11
|
+
write(text: string): void
|
|
12
|
+
replaceContent(text: string): void
|
|
13
|
+
onStateChange(): void
|
|
14
|
+
onNewSession(session: Session): void
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface CommandEnv {
|
|
18
|
+
session: Session
|
|
19
|
+
config: Config
|
|
20
|
+
ui: CommandUI
|
|
21
|
+
isRunning: boolean
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function handleNew(env: CommandEnv): Session {
|
|
25
|
+
env.session.stop()
|
|
26
|
+
Session.clearSavedSession(env.config.cwd)
|
|
27
|
+
resetTokenUsage(env.config.cwd)
|
|
28
|
+
resetGlobalEventBus()
|
|
29
|
+
const newSession = new Session(env.config, env.session.output)
|
|
30
|
+
newSession.onPlanWritten = env.session.onPlanWritten
|
|
31
|
+
env.ui.replaceContent('')
|
|
32
|
+
env.ui.onNewSession(newSession)
|
|
33
|
+
env.ui.onStateChange()
|
|
34
|
+
return newSession
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export async function handleMode(env: CommandEnv, mode: string): Promise<boolean> {
|
|
38
|
+
if (mode !== 'code' && mode !== 'plan' && mode !== 'ask' && mode !== 'loop') {
|
|
39
|
+
env.ui.write(`Usage: /mode code|plan|ask|loop (current: ${env.session.config.mode})`)
|
|
40
|
+
return true
|
|
41
|
+
}
|
|
42
|
+
await env.session.setMode(mode)
|
|
43
|
+
env.ui.write(`Switched to ${mode} mode`)
|
|
44
|
+
env.ui.onStateChange()
|
|
45
|
+
return true
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function handleModel(env: CommandEnv, model: string): boolean {
|
|
49
|
+
if (!model) {
|
|
50
|
+
env.ui.write(`Current model: ${env.session.config.model}`)
|
|
51
|
+
return true
|
|
52
|
+
}
|
|
53
|
+
env.session.config.model = model
|
|
54
|
+
// Trigger system prompt rebuild by re-setting mode
|
|
55
|
+
env.session.setMode(env.session.config.mode)
|
|
56
|
+
env.ui.write(`Model switched to ${model}`)
|
|
57
|
+
env.ui.onStateChange()
|
|
58
|
+
return true
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export function handlePrompts(env: CommandEnv): boolean {
|
|
62
|
+
const templates = loadPromptTemplates(env.config.cwd)
|
|
63
|
+
if (templates.length === 0) {
|
|
64
|
+
env.ui.write('No prompt templates found. Create .md files in .lonny/prompts/')
|
|
65
|
+
} else {
|
|
66
|
+
let msg = `Prompt Templates (${templates.length}):\n`
|
|
67
|
+
for (const t of templates) {
|
|
68
|
+
msg += ` \u2022 ${t.name}`
|
|
69
|
+
if (t.description) msg += ` \u2014 ${t.description}`
|
|
70
|
+
msg += '\n'
|
|
71
|
+
}
|
|
72
|
+
env.ui.write(msg.trimEnd())
|
|
73
|
+
}
|
|
74
|
+
return true
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export function handleSkills(env: CommandEnv): boolean {
|
|
78
|
+
const skills = loadSkills(env.config.cwd)
|
|
79
|
+
if (skills.length === 0) {
|
|
80
|
+
env.ui.write('No skills loaded. Create .md files in .lonny/skills/')
|
|
81
|
+
} else {
|
|
82
|
+
let msg = `Active Skills (${skills.length}):\n`
|
|
83
|
+
for (const s of skills) {
|
|
84
|
+
msg += ` \u2022 ${s.name}`
|
|
85
|
+
if (s.description) msg += ` \u2014 ${s.description}`
|
|
86
|
+
msg += '\n'
|
|
87
|
+
}
|
|
88
|
+
env.ui.write(msg.trimEnd())
|
|
89
|
+
}
|
|
90
|
+
return true
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export function handleSessions(env: CommandEnv): boolean {
|
|
94
|
+
const allSessions = Session.listSessions()
|
|
95
|
+
if (allSessions.length === 0) {
|
|
96
|
+
env.ui.write('No saved sessions found.')
|
|
97
|
+
return true
|
|
98
|
+
}
|
|
99
|
+
let msg = `Saved Sessions (${allSessions.length}):\n`
|
|
100
|
+
msg +=
|
|
101
|
+
' ID Title Mode Messages Tokens Updated\n'
|
|
102
|
+
for (const s of allSessions) {
|
|
103
|
+
const id = s.id.padEnd(9)
|
|
104
|
+
const title = (s.title || '(untitled)').slice(0, 42).padEnd(43)
|
|
105
|
+
const mode = s.mode.padEnd(7)
|
|
106
|
+
const msgs = String(s.messageCount).padEnd(9)
|
|
107
|
+
const tokens = String(s.totalInputTokens + s.totalOutputTokens).padEnd(9)
|
|
108
|
+
const date = s.updatedAt.slice(0, 10)
|
|
109
|
+
msg += ` ${id} ${title} ${mode} ${msgs} ${tokens} ${date}\n`
|
|
110
|
+
}
|
|
111
|
+
msg += '\n Use /session delete <id> to delete a session'
|
|
112
|
+
env.ui.write(msg)
|
|
113
|
+
return true
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export async function handleSessionCommand(env: CommandEnv, arg: string): Promise<boolean> {
|
|
117
|
+
if (arg === 'delete' || arg.startsWith('delete ')) {
|
|
118
|
+
const id = arg.slice(arg.startsWith('delete ') ? 7 : 6).trim()
|
|
119
|
+
if (id) {
|
|
120
|
+
const deleted = Session.deleteSession(id)
|
|
121
|
+
if (deleted) {
|
|
122
|
+
env.ui.write(`Deleted session ${id}`)
|
|
123
|
+
} else {
|
|
124
|
+
env.ui.write(`Session not found: ${id}`)
|
|
125
|
+
}
|
|
126
|
+
} else {
|
|
127
|
+
env.ui.write('Usage: /session delete <id>')
|
|
128
|
+
}
|
|
129
|
+
return true
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
if (arg.startsWith('title ')) {
|
|
133
|
+
const title = arg.slice(6).trim()
|
|
134
|
+
if (title) {
|
|
135
|
+
env.session.sessionTitle = title
|
|
136
|
+
env.session.save()
|
|
137
|
+
env.ui.write(`Session titled: ${title}`)
|
|
138
|
+
} else {
|
|
139
|
+
env.ui.write('Usage: /session title <name>')
|
|
140
|
+
}
|
|
141
|
+
return true
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
if (arg === 'export') {
|
|
145
|
+
try {
|
|
146
|
+
const filePath = env.session.exportSession()
|
|
147
|
+
env.ui.write(`Session exported to ${filePath}`)
|
|
148
|
+
} catch (err) {
|
|
149
|
+
env.ui.write(`Export failed: ${fmtErr(err)}`)
|
|
150
|
+
}
|
|
151
|
+
return true
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
if (arg === 'switch' || arg.startsWith('switch ')) {
|
|
155
|
+
const id = arg.startsWith('switch ') ? arg.slice(7).trim() : ''
|
|
156
|
+
if (!id) {
|
|
157
|
+
env.ui.write('Usage: /session switch <id>. Use /sessions to list session IDs.')
|
|
158
|
+
return true
|
|
159
|
+
}
|
|
160
|
+
if (env.isRunning) {
|
|
161
|
+
env.ui.write('Cannot switch session while agent is running. Wait or use /stop first.')
|
|
162
|
+
return true
|
|
163
|
+
}
|
|
164
|
+
const switched = await Session.loadById(id, env.config, env.session.output)
|
|
165
|
+
if (switched) {
|
|
166
|
+
switched.onPlanWritten = env.session.onPlanWritten
|
|
167
|
+
const preview = lastUserQuestion(switched.messages)
|
|
168
|
+
let msg = `Switched to session ${switched.sessionTitle || switched.sessionId}`
|
|
169
|
+
if (preview) msg += ` Last question: ${preview}`
|
|
170
|
+
env.ui.replaceContent(msg + '\n')
|
|
171
|
+
env.ui.onNewSession(switched)
|
|
172
|
+
env.ui.onStateChange()
|
|
173
|
+
} else {
|
|
174
|
+
env.ui.write(`Session not found: ${id}`)
|
|
175
|
+
}
|
|
176
|
+
return true
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// Default: show current session info
|
|
180
|
+
const id = env.session.sessionId
|
|
181
|
+
const title = env.session.sessionTitle || '(untitled)'
|
|
182
|
+
const mode = env.session.config.mode
|
|
183
|
+
const model = env.session.config.model
|
|
184
|
+
const provider = env.session.config.provider
|
|
185
|
+
const msgs = env.session.messages.length
|
|
186
|
+
const totalIn = env.session.totalInputTokens
|
|
187
|
+
const totalOut = env.session.totalOutputTokens
|
|
188
|
+
const totalApi = env.session.totalApiCalls
|
|
189
|
+
const created = env.session.sessionCreatedAt.slice(0, 10)
|
|
190
|
+
const info =
|
|
191
|
+
'' +
|
|
192
|
+
`ID: ${id}\n` +
|
|
193
|
+
`Title: ${title}\n` +
|
|
194
|
+
`Mode: ${mode}\n` +
|
|
195
|
+
`Model: ${model} (${provider})\n` +
|
|
196
|
+
`Messages: ${msgs}\n` +
|
|
197
|
+
`Tokens: ${totalIn + totalOut} (in: ${totalIn}, out: ${totalOut})\n` +
|
|
198
|
+
`API Calls: ${totalApi}\n` +
|
|
199
|
+
`Created: ${created}\n`
|
|
200
|
+
env.ui.write(info.trimEnd())
|
|
201
|
+
return true
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
export async function handleFork(env: CommandEnv): Promise<boolean> {
|
|
205
|
+
if (env.isRunning) {
|
|
206
|
+
env.ui.write('Cannot fork while agent is running. Wait or use /stop first.')
|
|
207
|
+
return true
|
|
208
|
+
}
|
|
209
|
+
if (env.session.messages.length <= 1) {
|
|
210
|
+
env.ui.write('No conversation to fork. Start a conversation first.')
|
|
211
|
+
return true
|
|
212
|
+
}
|
|
213
|
+
const forked = env.session.fork()
|
|
214
|
+
forked.onPlanWritten = env.session.onPlanWritten
|
|
215
|
+
const baseTitle = forked.sessionTitle || 'forked session'
|
|
216
|
+
env.ui.write(`Forked new session: ${baseTitle}`)
|
|
217
|
+
env.ui.write(` ID: ${forked.sessionId}`)
|
|
218
|
+
env.ui.onNewSession(forked)
|
|
219
|
+
env.ui.onStateChange()
|
|
220
|
+
return true
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
export function handleInit(env: CommandEnv): boolean {
|
|
224
|
+
ensureSkillsDir(env.config.cwd)
|
|
225
|
+
ensurePromptsDir(env.config.cwd)
|
|
226
|
+
env.ui.write('Initialized .lonny/skills/ and .lonny/prompts/')
|
|
227
|
+
return true
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
export function handleStop(env: CommandEnv): boolean {
|
|
231
|
+
if (!env.isRunning) {
|
|
232
|
+
env.ui.write('Agent is not running.')
|
|
233
|
+
return true
|
|
234
|
+
}
|
|
235
|
+
env.session.stop()
|
|
236
|
+
env.ui.write('Stopping agent...')
|
|
237
|
+
return true
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
function lastUserQuestion(messages: LLMMessage[]): string | null {
|
|
241
|
+
const lastUserMsg = [...messages].reverse().find(m => m.role === 'user')
|
|
242
|
+
if (!lastUserMsg || typeof lastUserMsg.content !== 'string') return null
|
|
243
|
+
return lastUserMsg.content.length > 80
|
|
244
|
+
? `${lastUserMsg.content.slice(0, 80)}\u2026`
|
|
245
|
+
: lastUserMsg.content
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
export async function dispatchCommand(env: CommandEnv, cmd: string, arg: string): Promise<boolean> {
|
|
249
|
+
switch (cmd) {
|
|
250
|
+
case 'new':
|
|
251
|
+
handleNew(env)
|
|
252
|
+
return true
|
|
253
|
+
case 'mode':
|
|
254
|
+
return handleMode(env, arg)
|
|
255
|
+
case 'model':
|
|
256
|
+
return handleModel(env, arg)
|
|
257
|
+
case 'prompts':
|
|
258
|
+
return handlePrompts(env)
|
|
259
|
+
case 'skills':
|
|
260
|
+
return handleSkills(env)
|
|
261
|
+
case 'sessions':
|
|
262
|
+
return handleSessions(env)
|
|
263
|
+
case 'session':
|
|
264
|
+
return handleSessionCommand(env, arg)
|
|
265
|
+
case 'fork':
|
|
266
|
+
return handleFork(env)
|
|
267
|
+
case 'init':
|
|
268
|
+
return handleInit(env)
|
|
269
|
+
case 'stop':
|
|
270
|
+
return handleStop(env)
|
|
271
|
+
default:
|
|
272
|
+
return false
|
|
273
|
+
}
|
|
274
|
+
}
|