automagik-forge 0.1.11 → 0.1.13
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/.cargo/config.toml +13 -0
- package/.claude/commands/commit.md +376 -0
- package/.claude/commands/prompt.md +871 -0
- package/.env.example +20 -0
- package/.github/actions/setup-node/action.yml +29 -0
- package/.github/images/automagik-logo.png +0 -0
- package/.github/workflows/pre-release.yml +470 -0
- package/.github/workflows/publish.yml +145 -0
- package/.github/workflows/test.yml +63 -0
- package/.mcp.json +57 -0
- package/AGENT.md +40 -0
- package/CLAUDE.md +40 -0
- package/CODE-OF-CONDUCT.md +89 -0
- package/Cargo.toml +19 -0
- package/Dockerfile +43 -0
- package/LICENSE +201 -0
- package/Makefile +97 -0
- package/README.md +447 -143
- package/backend/.sqlx/query-01b7e2bac1261d8be3d03c03df3e5220590da6c31c77f161074fc62752d63881.json +12 -0
- package/backend/.sqlx/query-03f2b02ba6dc5ea2b3cf6b1004caea0ad6bcc10ebd63f441d321a389f026e263.json +12 -0
- package/backend/.sqlx/query-0923b77d137a29fc54d399a873ff15fc4af894490bc65a4d344a7575cb0d8643.json +12 -0
- package/backend/.sqlx/query-0f808bcdb63c5f180836e448dd64c435c51758b2fc54a52ce9e67495b1ab200e.json +68 -0
- package/backend/.sqlx/query-1268afe9ca849daa6722e3df7ca8e9e61f0d37052e782bb5452ab8e1018d9b63.json +12 -0
- package/backend/.sqlx/query-1b082630a9622f8667ee7a9aba2c2d3176019a68c6bb83d33008594821415a57.json +12 -0
- package/backend/.sqlx/query-1c7b06ba1e112abf6b945a2ff08a0b40ec23f3738c2e7399f067b558cf8d490e.json +12 -0
- package/backend/.sqlx/query-1f619f01f46859a64ded531dd0ef61abacfe62e758abe7030a6aa745140b95ca.json +104 -0
- package/backend/.sqlx/query-1fca1ce14b4b20205364cd1f1f45ebe1d2e30cd745e59e189d56487b5639dfbb.json +12 -0
- package/backend/.sqlx/query-212828320e8d871ab9d83705a040b23bcf0393dc7252177fc539a74657f578ef.json +32 -0
- package/backend/.sqlx/query-290ce5c152be8d36e58ff42570f9157beb07ab9e77a03ec6fc30b4f56f9b8f6b.json +56 -0
- package/backend/.sqlx/query-2b471d2c2e8ffbe0cd42d2a91b814c0d79f9d09200f147e3cea33ba4ce673c8a.json +68 -0
- package/backend/.sqlx/query-354a48c705bb9bb2048c1b7f10fcb714e23f9db82b7a4ea6932486197b2ede6a.json +92 -0
- package/backend/.sqlx/query-36c9e3dd10648e94b949db5c91a774ecb1e10a899ef95da74066eccedca4d8b2.json +12 -0
- package/backend/.sqlx/query-36e4ba7bbd81b402d5a20b6005755eafbb174c8dda442081823406ac32809a94.json +56 -0
- package/backend/.sqlx/query-3a5b3c98a55ca183ab20c74708e3d7e579dda37972c059e7515c4ceee4bd8dd3.json +62 -0
- package/backend/.sqlx/query-3d0a1cabf2a52e9d90cdfd29c509ca89aeb448d0c1d2446c65cd43db40735e86.json +62 -0
- package/backend/.sqlx/query-3d6bd16fbce59efe30b7f67ea342e0e4ea6d1432389c02468ad79f1f742d4031.json +56 -0
- package/backend/.sqlx/query-4049ca413b285a05aca6b25385e9c8185575f01e9069e4e8581aa45d713f612f.json +32 -0
- package/backend/.sqlx/query-412bacd3477d86369082e90f52240407abce436cb81292d42b2dbe1e5c18eea1.json +104 -0
- package/backend/.sqlx/query-417a8b1ff4e51de82aea0159a3b97932224dc325b23476cb84153d690227fd8b.json +62 -0
- package/backend/.sqlx/query-461cc1b0bb6fd909afc9dd2246e8526b3771cfbb0b22ae4b5d17b51af587b9e2.json +56 -0
- package/backend/.sqlx/query-58408c7a8cdeeda0bef359f1f9bd91299a339dc2b191462fc58c9736a56d5227.json +92 -0
- package/backend/.sqlx/query-5a886026d75d515c01f347cc203c8d99dd04c61dc468e2e4c5aa548436d13834.json +62 -0
- package/backend/.sqlx/query-5b902137b11022d2e1a5c4f6a9c83fec1a856c6a710aff831abd2382ede76b43.json +12 -0
- package/backend/.sqlx/query-5ed1238e52e59bb5f76c0f153fd99a14093f7ce2585bf9843585608f17ec575b.json +104 -0
- package/backend/.sqlx/query-6e8b860b14decfc2227dc57213f38442943d3fbef5c8418fd6b634c6e0f5e2ea.json +104 -0
- package/backend/.sqlx/query-6ec414276994c4ccb2433eaa5b1b342168557d17ddf5a52dac84cb1b59b9de8f.json +68 -0
- package/backend/.sqlx/query-6ecfa16d0cf825aacf233544b5baf151e9adfdca26c226ad71020d291fd802d5.json +62 -0
- package/backend/.sqlx/query-72509d252c39fce77520aa816cb2acbc1fb35dc2605e7be893610599b2427f2e.json +62 -0
- package/backend/.sqlx/query-75239b2da188f749707d77f3c1544332ca70db3d6d6743b2601dc0d167536437.json +62 -0
- package/backend/.sqlx/query-83d10e29f8478aff33434f9ac67068e013b888b953a2657e2bb72a6f619d04f2.json +50 -0
- package/backend/.sqlx/query-8610803360ea18b9b9d078a6981ea56abfbfe84e6354fc1d5ae4c622e01410ed.json +68 -0
- package/backend/.sqlx/query-86d03eb70eef39c59296416867f2ee66c9f7cd8b7f961fbda2f89fc0a1c442c2.json +12 -0
- package/backend/.sqlx/query-87d0feb5a6b442bad9c60068ea7569599cc6fc91a0e2692ecb42e93b03201b9d.json +68 -0
- package/backend/.sqlx/query-8a67b3b3337248f06a57bdf8a908f7ef23177431eaed82dc08c94c3e5944340e.json +12 -0
- package/backend/.sqlx/query-8f01ebd64bdcde6a090479f14810d73ba23020e76fd70854ac57f2da251702c3.json +12 -0
- package/backend/.sqlx/query-90fd607fcb2dca72239ff25e618e21e174b195991eaa33722cbf5f76da84cfab.json +62 -0
- package/backend/.sqlx/query-92e8bdbcd80c5ff3db7a35cf79492048803ef305cbdef0d0a1fe5dc881ca8c71.json +104 -0
- package/backend/.sqlx/query-93a1605f90e9672dad29b472b6ad85fa9a55ea3ffa5abcb8724b09d61be254ca.json +20 -0
- package/backend/.sqlx/query-9472c8fb477958167f5fae40b85ac44252468c5226b2cdd7770f027332eed6d7.json +104 -0
- package/backend/.sqlx/query-96036c4f9e0f48bdc5a4a4588f0c5f288ac7aaa5425cac40fc33f337e1a351f2.json +56 -0
- package/backend/.sqlx/query-9edb2c01e91fd0f0fe7b56e988c7ae0393150f50be3f419a981e035c0121dfc7.json +104 -0
- package/backend/.sqlx/query-a157cf00616f703bfba21927f1eb1c9eec2a81c02da15f66efdba0b6c375de1b.json +26 -0
- package/backend/.sqlx/query-a31fff84f3b8e532fd1160447d89d700f06ae08821fee00c9a5b60492b05259c.json +62 -0
- package/backend/.sqlx/query-a5ba908419fb3e456bdd2daca41ba06cc3212ffffb8520fc7dbbcc8b60ada314.json +12 -0
- package/backend/.sqlx/query-a6d2961718dbc3b1a925e549f49a159c561bef58c105529275f274b27e2eba5b.json +104 -0
- package/backend/.sqlx/query-a9e93d5b09b29faf66e387e4d7596a792d81e75c4d3726e83c2963e8d7c9b56f.json +104 -0
- package/backend/.sqlx/query-ac5247c8d7fb86e4650c4b0eb9420031614c831b7b085083bac20c1af314c538.json +12 -0
- package/backend/.sqlx/query-afef9467be74c411c4cb119a8b2b1aea53049877dfc30cc60b486134ba4b4c9f.json +68 -0
- package/backend/.sqlx/query-b2b2c6b4d0b1a347b5c4cb63c3a46a265d4db53be9554989a814b069d0af82f2.json +62 -0
- package/backend/.sqlx/query-c50d2ff0b12e5bcc81e371089ee2d007e233e7db93aefba4fef08e7aa68f5ab7.json +20 -0
- package/backend/.sqlx/query-c614e6056b244ca07f1b9d44e7edc9d5819225c6f8d9e077070c6e518a17f50b.json +12 -0
- package/backend/.sqlx/query-c67259be8bf4ee0cfd32167b2aa3b7fe9192809181a8171bf1c2d6df731967ae.json +12 -0
- package/backend/.sqlx/query-d2d0a1b985ebbca6a2b3e882a221a219f3199890fa640afc946ef1a792d6d8de.json +12 -0
- package/backend/.sqlx/query-d30aa5786757f32bf2b9c5fe51a45e506c71c28c5994e430d9b0546adb15ffa2.json +20 -0
- package/backend/.sqlx/query-d3b9ea1de1576af71b312924ce7f4ea8ae5dbe2ac138ea3b4470f2d5cd734846.json +12 -0
- package/backend/.sqlx/query-ed8456646fa69ddd412441955f06ff22bfb790f29466450735e0b8bb1bc4ec94.json +12 -0
- package/backend/Cargo.toml +71 -0
- package/backend/build.rs +32 -0
- package/backend/migrations/20250617183714_init.sql +44 -0
- package/backend/migrations/20250620212427_execution_processes.sql +25 -0
- package/backend/migrations/20250620214100_remove_stdout_stderr_from_task_attempts.sql +28 -0
- package/backend/migrations/20250621120000_relate_activities_to_execution_processes.sql +23 -0
- package/backend/migrations/20250623120000_executor_sessions.sql +17 -0
- package/backend/migrations/20250623130000_add_executor_type_to_execution_processes.sql +4 -0
- package/backend/migrations/20250625000000_add_dev_script_to_projects.sql +4 -0
- package/backend/migrations/20250701000000_add_branch_to_task_attempts.sql +2 -0
- package/backend/migrations/20250701000001_add_pr_tracking_to_task_attempts.sql +5 -0
- package/backend/migrations/20250701120000_add_assistant_message_to_executor_sessions.sql +2 -0
- package/backend/migrations/20250708000000_add_base_branch_to_task_attempts.sql +2 -0
- package/backend/migrations/20250709000000_add_worktree_deleted_flag.sql +2 -0
- package/backend/migrations/20250710000000_add_setup_completion.sql +3 -0
- package/backend/migrations/20250715154859_add_task_templates.sql +25 -0
- package/backend/migrations/20250716143725_add_default_templates.sql +174 -0
- package/backend/migrations/20250716161432_update_executor_names_to_kebab_case.sql +20 -0
- package/backend/migrations/20250716170000_add_parent_task_to_tasks.sql +7 -0
- package/backend/migrations/20250717000000_drop_task_attempt_activities.sql +9 -0
- package/backend/migrations/20250719000000_add_cleanup_script_to_projects.sql +2 -0
- package/backend/migrations/20250720000000_add_cleanupscript_to_process_type_constraint.sql +25 -0
- package/backend/migrations/20250723000000_add_wish_to_tasks.sql +7 -0
- package/backend/migrations/20250724000000_remove_unique_wish_constraint.sql +5 -0
- package/backend/scripts/toast-notification.ps1 +23 -0
- package/backend/sounds/abstract-sound1.wav +0 -0
- package/backend/sounds/abstract-sound2.wav +0 -0
- package/backend/sounds/abstract-sound3.wav +0 -0
- package/backend/sounds/abstract-sound4.wav +0 -0
- package/backend/sounds/cow-mooing.wav +0 -0
- package/backend/sounds/phone-vibration.wav +0 -0
- package/backend/sounds/rooster.wav +0 -0
- package/backend/src/app_state.rs +218 -0
- package/backend/src/bin/generate_types.rs +189 -0
- package/backend/src/bin/mcp_task_server.rs +191 -0
- package/backend/src/execution_monitor.rs +1193 -0
- package/backend/src/executor.rs +1053 -0
- package/backend/src/executors/amp.rs +697 -0
- package/backend/src/executors/ccr.rs +91 -0
- package/backend/src/executors/charm_opencode.rs +113 -0
- package/backend/src/executors/claude.rs +887 -0
- package/backend/src/executors/cleanup_script.rs +124 -0
- package/backend/src/executors/dev_server.rs +53 -0
- package/backend/src/executors/echo.rs +79 -0
- package/backend/src/executors/gemini/config.rs +67 -0
- package/backend/src/executors/gemini/streaming.rs +363 -0
- package/backend/src/executors/gemini.rs +765 -0
- package/backend/src/executors/mod.rs +23 -0
- package/backend/src/executors/opencode_ai.rs +113 -0
- package/backend/src/executors/setup_script.rs +130 -0
- package/backend/src/executors/sst_opencode/filter.rs +184 -0
- package/backend/src/executors/sst_opencode/tools.rs +139 -0
- package/backend/src/executors/sst_opencode.rs +756 -0
- package/backend/src/lib.rs +45 -0
- package/backend/src/main.rs +324 -0
- package/backend/src/mcp/mod.rs +1 -0
- package/backend/src/mcp/task_server.rs +850 -0
- package/backend/src/middleware/mod.rs +3 -0
- package/backend/src/middleware/model_loaders.rs +242 -0
- package/backend/src/models/api_response.rs +36 -0
- package/backend/src/models/config.rs +375 -0
- package/backend/src/models/execution_process.rs +430 -0
- package/backend/src/models/executor_session.rs +225 -0
- package/backend/src/models/mod.rs +12 -0
- package/backend/src/models/project.rs +356 -0
- package/backend/src/models/task.rs +345 -0
- package/backend/src/models/task_attempt.rs +1214 -0
- package/backend/src/models/task_template.rs +146 -0
- package/backend/src/openapi.rs +93 -0
- package/backend/src/routes/auth.rs +297 -0
- package/backend/src/routes/config.rs +385 -0
- package/backend/src/routes/filesystem.rs +228 -0
- package/backend/src/routes/health.rs +16 -0
- package/backend/src/routes/mod.rs +9 -0
- package/backend/src/routes/projects.rs +562 -0
- package/backend/src/routes/stream.rs +244 -0
- package/backend/src/routes/task_attempts.rs +1172 -0
- package/backend/src/routes/task_templates.rs +229 -0
- package/backend/src/routes/tasks.rs +353 -0
- package/backend/src/services/analytics.rs +216 -0
- package/backend/src/services/git_service.rs +1321 -0
- package/backend/src/services/github_service.rs +307 -0
- package/backend/src/services/mod.rs +13 -0
- package/backend/src/services/notification_service.rs +263 -0
- package/backend/src/services/pr_monitor.rs +214 -0
- package/backend/src/services/process_service.rs +940 -0
- package/backend/src/utils/path.rs +96 -0
- package/backend/src/utils/shell.rs +19 -0
- package/backend/src/utils/text.rs +24 -0
- package/backend/src/utils/worktree_manager.rs +578 -0
- package/backend/src/utils.rs +125 -0
- package/backend/test.db +0 -0
- package/build-npm-package.sh +61 -0
- package/dev_assets_seed/config.json +19 -0
- package/frontend/.eslintrc.json +25 -0
- package/frontend/.prettierrc.json +8 -0
- package/frontend/components.json +17 -0
- package/frontend/index.html +19 -0
- package/frontend/package-lock.json +7321 -0
- package/frontend/package.json +61 -0
- package/frontend/postcss.config.js +6 -0
- package/frontend/public/android-chrome-192x192.png +0 -0
- package/frontend/public/android-chrome-512x512.png +0 -0
- package/frontend/public/apple-touch-icon.png +0 -0
- package/frontend/public/automagik-forge-logo-dark.svg +3 -0
- package/frontend/public/automagik-forge-logo.svg +3 -0
- package/frontend/public/automagik-forge-screenshot-overview.png +0 -0
- package/frontend/public/favicon-16x16.png +0 -0
- package/frontend/public/favicon-32x32.png +0 -0
- package/frontend/public/favicon.ico +0 -0
- package/frontend/public/site.webmanifest +1 -0
- package/frontend/public/viba-kanban-favicon.png +0 -0
- package/frontend/src/App.tsx +157 -0
- package/frontend/src/components/DisclaimerDialog.tsx +106 -0
- package/frontend/src/components/GitHubLoginDialog.tsx +314 -0
- package/frontend/src/components/OnboardingDialog.tsx +185 -0
- package/frontend/src/components/PrivacyOptInDialog.tsx +130 -0
- package/frontend/src/components/ProvidePatDialog.tsx +98 -0
- package/frontend/src/components/TaskTemplateManager.tsx +336 -0
- package/frontend/src/components/config-provider.tsx +119 -0
- package/frontend/src/components/context/TaskDetailsContextProvider.tsx +470 -0
- package/frontend/src/components/context/taskDetailsContext.ts +125 -0
- package/frontend/src/components/keyboard-shortcuts-demo.tsx +35 -0
- package/frontend/src/components/layout/navbar.tsx +86 -0
- package/frontend/src/components/logo.tsx +44 -0
- package/frontend/src/components/projects/ProjectCard.tsx +155 -0
- package/frontend/src/components/projects/project-detail.tsx +251 -0
- package/frontend/src/components/projects/project-form-fields.tsx +238 -0
- package/frontend/src/components/projects/project-form.tsx +301 -0
- package/frontend/src/components/projects/project-list.tsx +200 -0
- package/frontend/src/components/projects/projects-page.tsx +20 -0
- package/frontend/src/components/tasks/BranchSelector.tsx +169 -0
- package/frontend/src/components/tasks/DeleteFileConfirmationDialog.tsx +94 -0
- package/frontend/src/components/tasks/EditorSelectionDialog.tsx +119 -0
- package/frontend/src/components/tasks/TaskCard.tsx +154 -0
- package/frontend/src/components/tasks/TaskDetails/CollapsibleToolbar.tsx +33 -0
- package/frontend/src/components/tasks/TaskDetails/DiffCard.tsx +109 -0
- package/frontend/src/components/tasks/TaskDetails/DiffChunkSection.tsx +135 -0
- package/frontend/src/components/tasks/TaskDetails/DiffFile.tsx +296 -0
- package/frontend/src/components/tasks/TaskDetails/DiffTab.tsx +32 -0
- package/frontend/src/components/tasks/TaskDetails/DisplayConversationEntry.tsx +392 -0
- package/frontend/src/components/tasks/TaskDetails/LogsTab/Conversation.tsx +256 -0
- package/frontend/src/components/tasks/TaskDetails/LogsTab/ConversationEntry.tsx +56 -0
- package/frontend/src/components/tasks/TaskDetails/LogsTab/NormalizedConversationViewer.tsx +92 -0
- package/frontend/src/components/tasks/TaskDetails/LogsTab/Prompt.tsx +22 -0
- package/frontend/src/components/tasks/TaskDetails/LogsTab/SetupScriptRunning.tsx +49 -0
- package/frontend/src/components/tasks/TaskDetails/LogsTab.tsx +186 -0
- package/frontend/src/components/tasks/TaskDetails/ProcessesTab.tsx +288 -0
- package/frontend/src/components/tasks/TaskDetails/RelatedTasksTab.tsx +216 -0
- package/frontend/src/components/tasks/TaskDetails/TabNavigation.tsx +93 -0
- package/frontend/src/components/tasks/TaskDetailsHeader.tsx +169 -0
- package/frontend/src/components/tasks/TaskDetailsPanel.tsx +126 -0
- package/frontend/src/components/tasks/TaskDetailsToolbar.tsx +302 -0
- package/frontend/src/components/tasks/TaskFollowUpSection.tsx +130 -0
- package/frontend/src/components/tasks/TaskFormDialog.tsx +400 -0
- package/frontend/src/components/tasks/TaskKanbanBoard.tsx +180 -0
- package/frontend/src/components/tasks/Toolbar/CreateAttempt.tsx +259 -0
- package/frontend/src/components/tasks/Toolbar/CreatePRDialog.tsx +243 -0
- package/frontend/src/components/tasks/Toolbar/CurrentAttempt.tsx +899 -0
- package/frontend/src/components/tasks/index.ts +2 -0
- package/frontend/src/components/theme-provider.tsx +82 -0
- package/frontend/src/components/theme-toggle.tsx +36 -0
- package/frontend/src/components/ui/alert.tsx +59 -0
- package/frontend/src/components/ui/auto-expanding-textarea.tsx +70 -0
- package/frontend/src/components/ui/badge.tsx +36 -0
- package/frontend/src/components/ui/button.tsx +56 -0
- package/frontend/src/components/ui/card.tsx +86 -0
- package/frontend/src/components/ui/checkbox.tsx +44 -0
- package/frontend/src/components/ui/chip.tsx +25 -0
- package/frontend/src/components/ui/dialog.tsx +124 -0
- package/frontend/src/components/ui/dropdown-menu.tsx +198 -0
- package/frontend/src/components/ui/file-search-textarea.tsx +292 -0
- package/frontend/src/components/ui/folder-picker.tsx +279 -0
- package/frontend/src/components/ui/input.tsx +25 -0
- package/frontend/src/components/ui/label.tsx +24 -0
- package/frontend/src/components/ui/loader.tsx +26 -0
- package/frontend/src/components/ui/markdown-renderer.tsx +75 -0
- package/frontend/src/components/ui/select.tsx +160 -0
- package/frontend/src/components/ui/separator.tsx +31 -0
- package/frontend/src/components/ui/shadcn-io/kanban/index.tsx +185 -0
- package/frontend/src/components/ui/table.tsx +117 -0
- package/frontend/src/components/ui/tabs.tsx +53 -0
- package/frontend/src/components/ui/textarea.tsx +22 -0
- package/frontend/src/components/ui/tooltip.tsx +28 -0
- package/frontend/src/hooks/useNormalizedConversation.ts +440 -0
- package/frontend/src/index.css +225 -0
- package/frontend/src/lib/api.ts +630 -0
- package/frontend/src/lib/keyboard-shortcuts.ts +266 -0
- package/frontend/src/lib/responsive-config.ts +70 -0
- package/frontend/src/lib/types.ts +39 -0
- package/frontend/src/lib/utils.ts +10 -0
- package/frontend/src/main.tsx +50 -0
- package/frontend/src/pages/McpServers.tsx +418 -0
- package/frontend/src/pages/Settings.tsx +610 -0
- package/frontend/src/pages/project-tasks.tsx +575 -0
- package/frontend/src/pages/projects.tsx +18 -0
- package/frontend/src/vite-env.d.ts +1 -0
- package/frontend/tailwind.config.js +125 -0
- package/frontend/tsconfig.json +26 -0
- package/frontend/tsconfig.node.json +10 -0
- package/frontend/vite.config.ts +33 -0
- package/npx-cli/README.md +159 -0
- package/npx-cli/automagik-forge-0.0.55.tgz +0 -0
- package/npx-cli/automagik-forge-0.1.0.tgz +0 -0
- package/{dist/linux-x64/automagik-forge.zip → npx-cli/automagik-forge-0.1.10.tgz} +0 -0
- package/npx-cli/package.json +17 -0
- package/npx-cli/vibe-kanban-0.0.55.tgz +0 -0
- package/package.json +23 -13
- package/pnpm-workspace.yaml +2 -0
- package/rust-toolchain.toml +11 -0
- package/rustfmt.toml +3 -0
- package/scripts/load-env.js +43 -0
- package/scripts/mcp_test.js +374 -0
- package/scripts/prepare-db.js +45 -0
- package/scripts/setup-dev-environment.js +274 -0
- package/scripts/start-mcp-sse.js +70 -0
- package/scripts/test-debug.js +32 -0
- package/scripts/test-mcp-sse.js +138 -0
- package/scripts/test-simple.js +44 -0
- package/scripts/test-wish-final.js +179 -0
- package/scripts/test-wish-system.js +221 -0
- package/shared/types.ts +182 -0
- package/test-npm-package.sh +42 -0
- package/dist/linux-x64/automagik-forge-mcp.zip +0 -0
- /package/{bin → npx-cli/bin}/cli.js +0 -0
@@ -0,0 +1,756 @@
|
|
1
|
+
use async_trait::async_trait;
|
2
|
+
use command_group::{AsyncCommandGroup, AsyncGroupChild};
|
3
|
+
use serde_json::{json, Value};
|
4
|
+
use tokio::{
|
5
|
+
io::{AsyncBufReadExt, BufReader},
|
6
|
+
process::Command,
|
7
|
+
};
|
8
|
+
use uuid::Uuid;
|
9
|
+
|
10
|
+
use crate::{
|
11
|
+
executor::{Executor, ExecutorError, NormalizedConversation, NormalizedEntry},
|
12
|
+
models::{execution_process::ExecutionProcess, executor_session::ExecutorSession, task::Task},
|
13
|
+
utils::shell::get_shell_command,
|
14
|
+
};
|
15
|
+
|
16
|
+
// Sub-modules for utilities
|
17
|
+
pub mod filter;
|
18
|
+
pub mod tools;
|
19
|
+
|
20
|
+
use self::{
|
21
|
+
filter::{parse_session_id_from_line, tool_usage_regex, OpenCodeFilter},
|
22
|
+
tools::{determine_action_type, generate_tool_content, normalize_tool_name},
|
23
|
+
};
|
24
|
+
|
25
|
+
struct Content {
|
26
|
+
pub stdout: Option<String>,
|
27
|
+
pub stderr: Option<String>,
|
28
|
+
}
|
29
|
+
|
30
|
+
/// Process a single line for session extraction and content formatting
|
31
|
+
async fn process_line_for_content(
|
32
|
+
line: &str,
|
33
|
+
session_extracted: &mut bool,
|
34
|
+
worktree_path: &str,
|
35
|
+
pool: &sqlx::SqlitePool,
|
36
|
+
execution_process_id: uuid::Uuid,
|
37
|
+
) -> Option<Content> {
|
38
|
+
if !*session_extracted {
|
39
|
+
if let Some(session_id) = parse_session_id_from_line(line) {
|
40
|
+
if let Err(e) =
|
41
|
+
ExecutorSession::update_session_id(pool, execution_process_id, &session_id).await
|
42
|
+
{
|
43
|
+
tracing::error!(
|
44
|
+
"Failed to update session ID for execution process {}: {}",
|
45
|
+
execution_process_id,
|
46
|
+
e
|
47
|
+
);
|
48
|
+
} else {
|
49
|
+
tracing::info!(
|
50
|
+
"Updated session ID {} for execution process {}",
|
51
|
+
session_id,
|
52
|
+
execution_process_id
|
53
|
+
);
|
54
|
+
*session_extracted = true;
|
55
|
+
}
|
56
|
+
|
57
|
+
// Don't return any content for session lines
|
58
|
+
return None;
|
59
|
+
}
|
60
|
+
}
|
61
|
+
|
62
|
+
// Check if line is noise - if so, discard it
|
63
|
+
if OpenCodeFilter::is_noise(line) {
|
64
|
+
return None;
|
65
|
+
}
|
66
|
+
|
67
|
+
if OpenCodeFilter::is_stderr(line) {
|
68
|
+
// If it's stderr, we don't need to process it further
|
69
|
+
return Some(Content {
|
70
|
+
stdout: None,
|
71
|
+
stderr: Some(line.to_string()),
|
72
|
+
});
|
73
|
+
}
|
74
|
+
|
75
|
+
// Format clean content as normalized JSON
|
76
|
+
let formatted = format_opencode_content_as_normalized_json(line, worktree_path);
|
77
|
+
Some(Content {
|
78
|
+
stdout: Some(formatted),
|
79
|
+
stderr: None,
|
80
|
+
})
|
81
|
+
}
|
82
|
+
|
83
|
+
/// Stream stderr from OpenCode process with filtering to separate clean output from noise
|
84
|
+
pub async fn stream_opencode_stderr_to_db(
|
85
|
+
output: impl tokio::io::AsyncRead + Unpin,
|
86
|
+
pool: sqlx::SqlitePool,
|
87
|
+
attempt_id: Uuid,
|
88
|
+
execution_process_id: Uuid,
|
89
|
+
worktree_path: String,
|
90
|
+
) {
|
91
|
+
let mut reader = BufReader::new(output);
|
92
|
+
let mut line = String::new();
|
93
|
+
let mut session_extracted = false;
|
94
|
+
|
95
|
+
loop {
|
96
|
+
line.clear();
|
97
|
+
|
98
|
+
match reader.read_line(&mut line).await {
|
99
|
+
Ok(0) => break, // EOF
|
100
|
+
Ok(_) => {
|
101
|
+
line = line.trim_end_matches(['\r', '\n']).to_string();
|
102
|
+
|
103
|
+
let content = process_line_for_content(
|
104
|
+
&line,
|
105
|
+
&mut session_extracted,
|
106
|
+
&worktree_path,
|
107
|
+
&pool,
|
108
|
+
execution_process_id,
|
109
|
+
)
|
110
|
+
.await;
|
111
|
+
|
112
|
+
if let Some(Content { stdout, stderr }) = content {
|
113
|
+
tracing::debug!(
|
114
|
+
"Processed OpenCode content for attempt {}: stdout={:?} stderr={:?}",
|
115
|
+
attempt_id,
|
116
|
+
stdout,
|
117
|
+
stderr,
|
118
|
+
);
|
119
|
+
if let Err(e) = ExecutionProcess::append_output(
|
120
|
+
&pool,
|
121
|
+
execution_process_id,
|
122
|
+
stdout.as_deref(),
|
123
|
+
stderr.as_deref(),
|
124
|
+
)
|
125
|
+
.await
|
126
|
+
{
|
127
|
+
tracing::error!(
|
128
|
+
"Failed to write OpenCode line for attempt {}: {}",
|
129
|
+
attempt_id,
|
130
|
+
e
|
131
|
+
);
|
132
|
+
}
|
133
|
+
}
|
134
|
+
}
|
135
|
+
Err(e) => {
|
136
|
+
tracing::error!("Error reading stderr for attempt {}: {}", attempt_id, e);
|
137
|
+
break;
|
138
|
+
}
|
139
|
+
}
|
140
|
+
}
|
141
|
+
}
|
142
|
+
|
143
|
+
/// Format OpenCode clean content as normalized JSON entries for direct database storage
|
144
|
+
fn format_opencode_content_as_normalized_json(content: &str, worktree_path: &str) -> String {
|
145
|
+
let mut results = Vec::new();
|
146
|
+
let base_timestamp = chrono::Utc::now();
|
147
|
+
let mut entry_counter = 0u32;
|
148
|
+
|
149
|
+
for line in content.lines() {
|
150
|
+
let trimmed = line.trim();
|
151
|
+
if trimmed.is_empty() {
|
152
|
+
continue;
|
153
|
+
}
|
154
|
+
|
155
|
+
// Generate unique timestamp for each entry by adding microseconds
|
156
|
+
let unique_timestamp =
|
157
|
+
base_timestamp + chrono::Duration::microseconds(entry_counter as i64);
|
158
|
+
let timestamp_str = unique_timestamp.to_rfc3339_opts(chrono::SecondsFormat::Micros, true);
|
159
|
+
entry_counter += 1;
|
160
|
+
|
161
|
+
// Try to parse as existing JSON first
|
162
|
+
if let Ok(parsed_json) = serde_json::from_str::<Value>(trimmed) {
|
163
|
+
results.push(parsed_json.to_string());
|
164
|
+
continue;
|
165
|
+
}
|
166
|
+
|
167
|
+
// Strip ANSI codes before processing
|
168
|
+
let cleaned = OpenCodeFilter::strip_ansi_codes(trimmed);
|
169
|
+
let cleaned_trim = cleaned.trim();
|
170
|
+
|
171
|
+
if cleaned_trim.is_empty() {
|
172
|
+
continue;
|
173
|
+
}
|
174
|
+
|
175
|
+
// Check for tool usage patterns after ANSI stripping: | ToolName {...}
|
176
|
+
if let Some(captures) = tool_usage_regex().captures(cleaned_trim) {
|
177
|
+
if let (Some(tool_name), Some(tool_input)) = (captures.get(1), captures.get(2)) {
|
178
|
+
// Parse tool input
|
179
|
+
let input: serde_json::Value =
|
180
|
+
serde_json::from_str(tool_input.as_str()).unwrap_or(serde_json::Value::Null);
|
181
|
+
|
182
|
+
// Normalize tool name for frontend compatibility (e.g., "Todo" → "todowrite")
|
183
|
+
let normalized_tool_name = normalize_tool_name(tool_name.as_str());
|
184
|
+
|
185
|
+
let normalized_entry = json!({
|
186
|
+
"timestamp": timestamp_str,
|
187
|
+
"entry_type": {
|
188
|
+
"type": "tool_use",
|
189
|
+
"tool_name": normalized_tool_name,
|
190
|
+
"action_type": determine_action_type(&normalized_tool_name, &input, worktree_path)
|
191
|
+
},
|
192
|
+
"content": generate_tool_content(&normalized_tool_name, &input, worktree_path),
|
193
|
+
"metadata": input
|
194
|
+
});
|
195
|
+
results.push(normalized_entry.to_string());
|
196
|
+
continue;
|
197
|
+
}
|
198
|
+
}
|
199
|
+
|
200
|
+
// Regular assistant message
|
201
|
+
let normalized_entry = json!({
|
202
|
+
"timestamp": timestamp_str,
|
203
|
+
"entry_type": {
|
204
|
+
"type": "assistant_message"
|
205
|
+
},
|
206
|
+
"content": cleaned_trim,
|
207
|
+
"metadata": null
|
208
|
+
});
|
209
|
+
results.push(normalized_entry.to_string());
|
210
|
+
}
|
211
|
+
|
212
|
+
// Ensure each JSON entry is on its own line
|
213
|
+
results.join("\n") + "\n"
|
214
|
+
}
|
215
|
+
|
216
|
+
/// An executor that uses SST Opencode CLI to process tasks
|
217
|
+
pub struct SstOpencodeExecutor {
|
218
|
+
executor_type: String,
|
219
|
+
command: String,
|
220
|
+
}
|
221
|
+
|
222
|
+
impl Default for SstOpencodeExecutor {
|
223
|
+
fn default() -> Self {
|
224
|
+
Self::new()
|
225
|
+
}
|
226
|
+
}
|
227
|
+
|
228
|
+
impl SstOpencodeExecutor {
|
229
|
+
/// Create a new SstOpencodeExecutor with default settings
|
230
|
+
pub fn new() -> Self {
|
231
|
+
Self {
|
232
|
+
executor_type: "SST Opencode".to_string(),
|
233
|
+
command: "npx -y opencode-ai@latest run --print-logs".to_string(),
|
234
|
+
}
|
235
|
+
}
|
236
|
+
}
|
237
|
+
|
238
|
+
/// An executor that resumes an SST Opencode session
|
239
|
+
|
240
|
+
#[async_trait]
|
241
|
+
impl Executor for SstOpencodeExecutor {
|
242
|
+
async fn spawn(
|
243
|
+
&self,
|
244
|
+
pool: &sqlx::SqlitePool,
|
245
|
+
task_id: Uuid,
|
246
|
+
worktree_path: &str,
|
247
|
+
) -> Result<AsyncGroupChild, ExecutorError> {
|
248
|
+
// Get the task to fetch its description
|
249
|
+
let task = Task::find_by_id(pool, task_id)
|
250
|
+
.await?
|
251
|
+
.ok_or(ExecutorError::TaskNotFound)?;
|
252
|
+
|
253
|
+
let prompt = if let Some(task_description) = task.description {
|
254
|
+
format!(
|
255
|
+
r#"project_id: {}
|
256
|
+
|
257
|
+
Task title: {}
|
258
|
+
Task description: {}"#,
|
259
|
+
task.project_id, task.title, task_description
|
260
|
+
)
|
261
|
+
} else {
|
262
|
+
format!(
|
263
|
+
r#"project_id: {}
|
264
|
+
|
265
|
+
Task title: {}"#,
|
266
|
+
task.project_id, task.title
|
267
|
+
)
|
268
|
+
};
|
269
|
+
|
270
|
+
// Use shell command for cross-platform compatibility
|
271
|
+
let (shell_cmd, shell_arg) = get_shell_command();
|
272
|
+
let opencode_command = &self.command;
|
273
|
+
|
274
|
+
let mut command = Command::new(shell_cmd);
|
275
|
+
command
|
276
|
+
.kill_on_drop(true)
|
277
|
+
.stdin(std::process::Stdio::piped())
|
278
|
+
.stdout(std::process::Stdio::null()) // Ignore stdout for OpenCode
|
279
|
+
.stderr(std::process::Stdio::piped())
|
280
|
+
.current_dir(worktree_path)
|
281
|
+
.arg(shell_arg)
|
282
|
+
.arg(opencode_command)
|
283
|
+
.env("NODE_NO_WARNINGS", "1");
|
284
|
+
|
285
|
+
let mut child = command
|
286
|
+
.group_spawn() // Create new process group so we can kill entire tree
|
287
|
+
.map_err(|e| {
|
288
|
+
crate::executor::SpawnContext::from_command(&command, &self.executor_type)
|
289
|
+
.with_task(task_id, Some(task.title.clone()))
|
290
|
+
.with_context(format!("{} CLI execution for new task", self.executor_type))
|
291
|
+
.spawn_error(e)
|
292
|
+
})?;
|
293
|
+
|
294
|
+
// Write prompt to stdin safely
|
295
|
+
if let Some(mut stdin) = child.inner().stdin.take() {
|
296
|
+
use tokio::io::AsyncWriteExt;
|
297
|
+
tracing::debug!(
|
298
|
+
"Writing prompt to OpenCode stdin for task {}: {:?}",
|
299
|
+
task_id,
|
300
|
+
prompt
|
301
|
+
);
|
302
|
+
stdin.write_all(prompt.as_bytes()).await.map_err(|e| {
|
303
|
+
let context =
|
304
|
+
crate::executor::SpawnContext::from_command(&command, &self.executor_type)
|
305
|
+
.with_task(task_id, Some(task.title.clone()))
|
306
|
+
.with_context(format!(
|
307
|
+
"Failed to write prompt to {} CLI stdin",
|
308
|
+
self.executor_type
|
309
|
+
));
|
310
|
+
ExecutorError::spawn_failed(e, context)
|
311
|
+
})?;
|
312
|
+
stdin.shutdown().await.map_err(|e| {
|
313
|
+
let context =
|
314
|
+
crate::executor::SpawnContext::from_command(&command, &self.executor_type)
|
315
|
+
.with_task(task_id, Some(task.title.clone()))
|
316
|
+
.with_context(format!("Failed to close {} CLI stdin", self.executor_type));
|
317
|
+
ExecutorError::spawn_failed(e, context)
|
318
|
+
})?;
|
319
|
+
}
|
320
|
+
|
321
|
+
Ok(child)
|
322
|
+
}
|
323
|
+
|
324
|
+
/// Execute with OpenCode filtering for stderr
|
325
|
+
async fn execute_streaming(
|
326
|
+
&self,
|
327
|
+
pool: &sqlx::SqlitePool,
|
328
|
+
task_id: Uuid,
|
329
|
+
attempt_id: Uuid,
|
330
|
+
execution_process_id: Uuid,
|
331
|
+
worktree_path: &str,
|
332
|
+
) -> Result<command_group::AsyncGroupChild, ExecutorError> {
|
333
|
+
let mut child = self.spawn(pool, task_id, worktree_path).await?;
|
334
|
+
|
335
|
+
// Take stderr pipe for OpenCode filtering
|
336
|
+
let stderr = child
|
337
|
+
.inner()
|
338
|
+
.stderr
|
339
|
+
.take()
|
340
|
+
.expect("Failed to take stderr from child process");
|
341
|
+
|
342
|
+
// Start OpenCode stderr filtering task
|
343
|
+
let pool_clone = pool.clone();
|
344
|
+
let worktree_path_clone = worktree_path.to_string();
|
345
|
+
tokio::spawn(stream_opencode_stderr_to_db(
|
346
|
+
stderr,
|
347
|
+
pool_clone,
|
348
|
+
attempt_id,
|
349
|
+
execution_process_id,
|
350
|
+
worktree_path_clone,
|
351
|
+
));
|
352
|
+
|
353
|
+
Ok(child)
|
354
|
+
}
|
355
|
+
|
356
|
+
fn normalize_logs(
|
357
|
+
&self,
|
358
|
+
logs: &str,
|
359
|
+
_worktree_path: &str,
|
360
|
+
) -> Result<NormalizedConversation, String> {
|
361
|
+
let mut entries = Vec::new();
|
362
|
+
|
363
|
+
for line in logs.lines() {
|
364
|
+
let trimmed = line.trim();
|
365
|
+
if trimmed.is_empty() {
|
366
|
+
continue;
|
367
|
+
}
|
368
|
+
|
369
|
+
// Simple passthrough: directly deserialize normalized JSON entries
|
370
|
+
if let Ok(entry) = serde_json::from_str::<NormalizedEntry>(trimmed) {
|
371
|
+
entries.push(entry);
|
372
|
+
}
|
373
|
+
}
|
374
|
+
|
375
|
+
Ok(NormalizedConversation {
|
376
|
+
entries,
|
377
|
+
session_id: None, // Session ID is stored directly in the database
|
378
|
+
executor_type: "sst-opencode".to_string(),
|
379
|
+
prompt: None,
|
380
|
+
summary: None,
|
381
|
+
})
|
382
|
+
}
|
383
|
+
|
384
|
+
/// Execute follow-up with OpenCode filtering for stderr
|
385
|
+
async fn execute_followup_streaming(
|
386
|
+
&self,
|
387
|
+
pool: &sqlx::SqlitePool,
|
388
|
+
task_id: Uuid,
|
389
|
+
attempt_id: Uuid,
|
390
|
+
execution_process_id: Uuid,
|
391
|
+
session_id: &str,
|
392
|
+
prompt: &str,
|
393
|
+
worktree_path: &str,
|
394
|
+
) -> Result<command_group::AsyncGroupChild, ExecutorError> {
|
395
|
+
let mut child = self
|
396
|
+
.spawn_followup(pool, task_id, session_id, prompt, worktree_path)
|
397
|
+
.await?;
|
398
|
+
|
399
|
+
// Take stderr pipe for OpenCode filtering
|
400
|
+
let stderr = child
|
401
|
+
.inner()
|
402
|
+
.stderr
|
403
|
+
.take()
|
404
|
+
.expect("Failed to take stderr from child process");
|
405
|
+
|
406
|
+
// Start OpenCode stderr filtering task
|
407
|
+
let pool_clone = pool.clone();
|
408
|
+
let worktree_path_clone = worktree_path.to_string();
|
409
|
+
tokio::spawn(stream_opencode_stderr_to_db(
|
410
|
+
stderr,
|
411
|
+
pool_clone,
|
412
|
+
attempt_id,
|
413
|
+
execution_process_id,
|
414
|
+
worktree_path_clone,
|
415
|
+
));
|
416
|
+
|
417
|
+
Ok(child)
|
418
|
+
}
|
419
|
+
|
420
|
+
async fn spawn_followup(
|
421
|
+
&self,
|
422
|
+
_pool: &sqlx::SqlitePool,
|
423
|
+
_task_id: Uuid,
|
424
|
+
session_id: &str,
|
425
|
+
prompt: &str,
|
426
|
+
worktree_path: &str,
|
427
|
+
) -> Result<AsyncGroupChild, ExecutorError> {
|
428
|
+
use std::process::Stdio;
|
429
|
+
|
430
|
+
use tokio::io::AsyncWriteExt;
|
431
|
+
|
432
|
+
// Use shell command for cross-platform compatibility
|
433
|
+
let (shell_cmd, shell_arg) = get_shell_command();
|
434
|
+
let opencode_command = format!("{} --session {}", self.command, session_id);
|
435
|
+
|
436
|
+
let mut command = Command::new(shell_cmd);
|
437
|
+
command
|
438
|
+
.kill_on_drop(true)
|
439
|
+
.stdin(Stdio::piped())
|
440
|
+
.stdout(Stdio::null()) // Ignore stdout for OpenCode
|
441
|
+
.stderr(Stdio::piped())
|
442
|
+
.current_dir(worktree_path)
|
443
|
+
.arg(shell_arg)
|
444
|
+
.arg(&opencode_command)
|
445
|
+
.env("NODE_NO_WARNINGS", "1");
|
446
|
+
|
447
|
+
let mut child = command.group_spawn().map_err(|e| {
|
448
|
+
crate::executor::SpawnContext::from_command(&command, &self.executor_type)
|
449
|
+
.with_context(format!(
|
450
|
+
"{} CLI followup execution for session {}",
|
451
|
+
self.executor_type, session_id
|
452
|
+
))
|
453
|
+
.spawn_error(e)
|
454
|
+
})?;
|
455
|
+
|
456
|
+
// Write prompt to stdin safely
|
457
|
+
if let Some(mut stdin) = child.inner().stdin.take() {
|
458
|
+
tracing::debug!(
|
459
|
+
"Writing prompt to {} stdin for session {}: {:?}",
|
460
|
+
self.executor_type,
|
461
|
+
session_id,
|
462
|
+
prompt
|
463
|
+
);
|
464
|
+
stdin.write_all(prompt.as_bytes()).await.map_err(|e| {
|
465
|
+
let context =
|
466
|
+
crate::executor::SpawnContext::from_command(&command, &self.executor_type)
|
467
|
+
.with_context(format!(
|
468
|
+
"Failed to write prompt to {} CLI stdin for session {}",
|
469
|
+
self.executor_type, session_id
|
470
|
+
));
|
471
|
+
ExecutorError::spawn_failed(e, context)
|
472
|
+
})?;
|
473
|
+
stdin.shutdown().await.map_err(|e| {
|
474
|
+
let context =
|
475
|
+
crate::executor::SpawnContext::from_command(&command, &self.executor_type)
|
476
|
+
.with_context(format!(
|
477
|
+
"Failed to close {} CLI stdin for session {}",
|
478
|
+
self.executor_type, session_id
|
479
|
+
));
|
480
|
+
ExecutorError::spawn_failed(e, context)
|
481
|
+
})?;
|
482
|
+
}
|
483
|
+
|
484
|
+
Ok(child)
|
485
|
+
}
|
486
|
+
}
|
487
|
+
|
488
|
+
#[cfg(test)]
|
489
|
+
mod tests {
|
490
|
+
use super::*;
|
491
|
+
use crate::{
|
492
|
+
executor::ActionType,
|
493
|
+
executors::sst_opencode::{
|
494
|
+
format_opencode_content_as_normalized_json, SstOpencodeExecutor,
|
495
|
+
},
|
496
|
+
};
|
497
|
+
|
498
|
+
// Test the actual format that comes from the database (normalized JSON entries)
|
499
|
+
#[test]
|
500
|
+
fn test_normalize_logs_with_database_format() {
|
501
|
+
let executor = SstOpencodeExecutor::new();
|
502
|
+
|
503
|
+
// This is what the database should contain after our streaming function processes it
|
504
|
+
let logs = r#"{"timestamp":"2025-07-16T18:04:00Z","entry_type":{"type":"tool_use","tool_name":"Read","action_type":{"action":"file_read","path":"hello.js"}},"content":"`hello.js`","metadata":{"filePath":"/path/to/repo/hello.js"}}
|
505
|
+
{"timestamp":"2025-07-16T18:04:01Z","entry_type":{"type":"assistant_message"},"content":"I'll read the hello.js file to see its current contents.","metadata":null}
|
506
|
+
{"timestamp":"2025-07-16T18:04:02Z","entry_type":{"type":"tool_use","tool_name":"bash","action_type":{"action":"command_run","command":"ls -la"}},"content":"`ls -la`","metadata":{"command":"ls -la"}}
|
507
|
+
{"timestamp":"2025-07-16T18:04:03Z","entry_type":{"type":"assistant_message"},"content":"The file exists and contains a hello world function.","metadata":null}"#;
|
508
|
+
|
509
|
+
let result = executor.normalize_logs(logs, "/path/to/repo").unwrap();
|
510
|
+
|
511
|
+
assert_eq!(result.entries.len(), 4);
|
512
|
+
|
513
|
+
// First entry: file read tool use
|
514
|
+
assert!(matches!(
|
515
|
+
result.entries[0].entry_type,
|
516
|
+
crate::executor::NormalizedEntryType::ToolUse { .. }
|
517
|
+
));
|
518
|
+
if let crate::executor::NormalizedEntryType::ToolUse {
|
519
|
+
tool_name,
|
520
|
+
action_type,
|
521
|
+
} = &result.entries[0].entry_type
|
522
|
+
{
|
523
|
+
assert_eq!(tool_name, "Read");
|
524
|
+
assert!(matches!(action_type, ActionType::FileRead { .. }));
|
525
|
+
}
|
526
|
+
assert_eq!(result.entries[0].content, "`hello.js`");
|
527
|
+
assert!(result.entries[0].timestamp.is_some());
|
528
|
+
|
529
|
+
// Second entry: assistant message
|
530
|
+
assert!(matches!(
|
531
|
+
result.entries[1].entry_type,
|
532
|
+
crate::executor::NormalizedEntryType::AssistantMessage
|
533
|
+
));
|
534
|
+
assert!(result.entries[1].content.contains("read the hello.js file"));
|
535
|
+
|
536
|
+
// Third entry: bash tool use
|
537
|
+
assert!(matches!(
|
538
|
+
result.entries[2].entry_type,
|
539
|
+
crate::executor::NormalizedEntryType::ToolUse { .. }
|
540
|
+
));
|
541
|
+
if let crate::executor::NormalizedEntryType::ToolUse {
|
542
|
+
tool_name,
|
543
|
+
action_type,
|
544
|
+
} = &result.entries[2].entry_type
|
545
|
+
{
|
546
|
+
assert_eq!(tool_name, "bash");
|
547
|
+
assert!(matches!(action_type, ActionType::CommandRun { .. }));
|
548
|
+
}
|
549
|
+
|
550
|
+
// Fourth entry: assistant message
|
551
|
+
assert!(matches!(
|
552
|
+
result.entries[3].entry_type,
|
553
|
+
crate::executor::NormalizedEntryType::AssistantMessage
|
554
|
+
));
|
555
|
+
assert!(result.entries[3].content.contains("The file exists"));
|
556
|
+
}
|
557
|
+
|
558
|
+
#[test]
|
559
|
+
fn test_normalize_logs_with_session_id() {
|
560
|
+
let executor = SstOpencodeExecutor::new();
|
561
|
+
|
562
|
+
// Test session ID in JSON metadata - current implementation always returns None for session_id
|
563
|
+
let logs = r#"{"timestamp":"2025-07-16T18:04:00Z","entry_type":{"type":"assistant_message"},"content":"Session started","metadata":null,"session_id":"ses_abc123"}
|
564
|
+
{"timestamp":"2025-07-16T18:04:01Z","entry_type":{"type":"assistant_message"},"content":"Hello world","metadata":null}"#;
|
565
|
+
|
566
|
+
let result = executor.normalize_logs(logs, "/tmp").unwrap();
|
567
|
+
assert_eq!(result.session_id, None); // Session ID is stored directly in the database
|
568
|
+
assert_eq!(result.entries.len(), 2);
|
569
|
+
}
|
570
|
+
|
571
|
+
#[test]
|
572
|
+
fn test_normalize_logs_legacy_fallback() {
|
573
|
+
let executor = SstOpencodeExecutor::new();
|
574
|
+
|
575
|
+
// Current implementation doesn't handle legacy format - it only parses JSON entries
|
576
|
+
let logs = r#"INFO session=ses_legacy123 starting
|
577
|
+
| Read {"filePath":"/path/to/file.js"}
|
578
|
+
This is a plain assistant message"#;
|
579
|
+
|
580
|
+
let result = executor.normalize_logs(logs, "/tmp").unwrap();
|
581
|
+
|
582
|
+
// Session ID is always None in current implementation
|
583
|
+
assert_eq!(result.session_id, None);
|
584
|
+
|
585
|
+
// Current implementation skips non-JSON lines, so no entries will be parsed
|
586
|
+
assert_eq!(result.entries.len(), 0);
|
587
|
+
}
|
588
|
+
|
589
|
+
#[test]
|
590
|
+
fn test_format_opencode_content_as_normalized_json() {
|
591
|
+
let content = r#"| Read {"filePath":"/path/to/repo/hello.js"}
|
592
|
+
I'll read this file to understand its contents.
|
593
|
+
| bash {"command":"ls -la"}
|
594
|
+
The file listing shows several items."#;
|
595
|
+
|
596
|
+
let result = format_opencode_content_as_normalized_json(content, "/path/to/repo");
|
597
|
+
let lines: Vec<&str> = result
|
598
|
+
.split('\n')
|
599
|
+
.filter(|line| !line.trim().is_empty())
|
600
|
+
.collect();
|
601
|
+
|
602
|
+
// Should have 4 entries (2 tool uses + 2 assistant messages)
|
603
|
+
assert_eq!(lines.len(), 4);
|
604
|
+
|
605
|
+
// Parse all entries and verify unique timestamps
|
606
|
+
let mut timestamps = Vec::new();
|
607
|
+
for line in &lines {
|
608
|
+
let json: serde_json::Value = serde_json::from_str(line).unwrap();
|
609
|
+
let timestamp = json["timestamp"].as_str().unwrap().to_string();
|
610
|
+
timestamps.push(timestamp);
|
611
|
+
}
|
612
|
+
|
613
|
+
// Verify all timestamps are unique (no duplicates)
|
614
|
+
let mut unique_timestamps = timestamps.clone();
|
615
|
+
unique_timestamps.sort();
|
616
|
+
unique_timestamps.dedup();
|
617
|
+
assert_eq!(
|
618
|
+
timestamps.len(),
|
619
|
+
unique_timestamps.len(),
|
620
|
+
"All timestamps should be unique"
|
621
|
+
);
|
622
|
+
|
623
|
+
// Parse the first line (should be Read tool use)
|
624
|
+
let first_json: serde_json::Value = serde_json::from_str(lines[0]).unwrap();
|
625
|
+
assert_eq!(first_json["entry_type"]["type"], "tool_use");
|
626
|
+
assert_eq!(first_json["entry_type"]["tool_name"], "Read");
|
627
|
+
assert_eq!(first_json["content"], "`hello.js`");
|
628
|
+
|
629
|
+
// Parse the second line (should be assistant message)
|
630
|
+
let second_json: serde_json::Value = serde_json::from_str(lines[1]).unwrap();
|
631
|
+
assert_eq!(second_json["entry_type"]["type"], "assistant_message");
|
632
|
+
assert!(second_json["content"]
|
633
|
+
.as_str()
|
634
|
+
.unwrap()
|
635
|
+
.contains("read this file"));
|
636
|
+
|
637
|
+
// Parse the third line (should be bash tool use)
|
638
|
+
let third_json: serde_json::Value = serde_json::from_str(lines[2]).unwrap();
|
639
|
+
assert_eq!(third_json["entry_type"]["type"], "tool_use");
|
640
|
+
assert_eq!(third_json["entry_type"]["tool_name"], "bash");
|
641
|
+
assert_eq!(third_json["content"], "`ls -la`");
|
642
|
+
|
643
|
+
// Verify timestamps include microseconds for uniqueness
|
644
|
+
for timestamp in timestamps {
|
645
|
+
assert!(
|
646
|
+
timestamp.contains('.'),
|
647
|
+
"Timestamp should include microseconds: {}",
|
648
|
+
timestamp
|
649
|
+
);
|
650
|
+
}
|
651
|
+
}
|
652
|
+
|
653
|
+
#[test]
|
654
|
+
fn test_format_opencode_content_todo_operations() {
|
655
|
+
let content = r#"| TodoWrite {"todos":[{"id":"1","content":"Fix bug","status":"completed","priority":"high"},{"id":"2","content":"Add feature","status":"in_progress","priority":"medium"}]}"#;
|
656
|
+
|
657
|
+
let result = format_opencode_content_as_normalized_json(content, "/tmp");
|
658
|
+
let json: serde_json::Value = serde_json::from_str(&result).unwrap();
|
659
|
+
|
660
|
+
assert_eq!(json["entry_type"]["type"], "tool_use");
|
661
|
+
assert_eq!(json["entry_type"]["tool_name"], "todowrite"); // Normalized from "TodoWrite"
|
662
|
+
assert_eq!(json["entry_type"]["action_type"]["action"], "other"); // Changed from task_create to other
|
663
|
+
|
664
|
+
// Should contain formatted todo list
|
665
|
+
let content_str = json["content"].as_str().unwrap();
|
666
|
+
assert!(content_str.contains("TODO List:"));
|
667
|
+
assert!(content_str.contains("✅ Fix bug (high)"));
|
668
|
+
assert!(content_str.contains("🔄 Add feature (medium)"));
|
669
|
+
}
|
670
|
+
|
671
|
+
#[test]
|
672
|
+
fn test_format_opencode_content_todo_tool() {
|
673
|
+
// Test the "Todo" tool (case-sensitive, different from todowrite/todoread)
|
674
|
+
let content = r#"| Todo {"todos":[{"id":"1","content":"Review code","status":"pending","priority":"high"},{"id":"2","content":"Write tests","status":"in_progress","priority":"low"}]}"#;
|
675
|
+
|
676
|
+
let result = format_opencode_content_as_normalized_json(content, "/tmp");
|
677
|
+
let json: serde_json::Value = serde_json::from_str(&result).unwrap();
|
678
|
+
|
679
|
+
assert_eq!(json["entry_type"]["type"], "tool_use");
|
680
|
+
assert_eq!(json["entry_type"]["tool_name"], "todowrite"); // Normalized from "Todo"
|
681
|
+
assert_eq!(json["entry_type"]["action_type"]["action"], "other"); // Changed from task_create to other
|
682
|
+
|
683
|
+
// Should contain formatted todo list with proper emojis
|
684
|
+
let content_str = json["content"].as_str().unwrap();
|
685
|
+
assert!(content_str.contains("TODO List:"));
|
686
|
+
assert!(content_str.contains("⏳ Review code (high)"));
|
687
|
+
assert!(content_str.contains("🔄 Write tests (low)"));
|
688
|
+
}
|
689
|
+
|
690
|
+
#[test]
|
691
|
+
fn test_opencode_filter_noise_detection() {
|
692
|
+
use crate::executors::sst_opencode::filter::OpenCodeFilter;
|
693
|
+
|
694
|
+
// Test noise detection
|
695
|
+
assert!(OpenCodeFilter::is_noise(""));
|
696
|
+
assert!(OpenCodeFilter::is_noise(" "));
|
697
|
+
assert!(OpenCodeFilter::is_noise("█▀▀█ █▀▀█ Banner"));
|
698
|
+
assert!(OpenCodeFilter::is_noise("@ anthropic/claude-sonnet-4"));
|
699
|
+
assert!(OpenCodeFilter::is_noise("~ https://opencode.ai/s/abc123"));
|
700
|
+
assert!(OpenCodeFilter::is_noise("DEBUG some debug info"));
|
701
|
+
assert!(OpenCodeFilter::is_noise("INFO session info"));
|
702
|
+
assert!(OpenCodeFilter::is_noise("┌─────────────────┐"));
|
703
|
+
|
704
|
+
// Test clean content detection (not noise)
|
705
|
+
assert!(!OpenCodeFilter::is_noise("| Read {\"file\":\"test.js\"}"));
|
706
|
+
assert!(!OpenCodeFilter::is_noise("Assistant response text"));
|
707
|
+
assert!(!OpenCodeFilter::is_noise("{\"type\":\"content\"}"));
|
708
|
+
assert!(!OpenCodeFilter::is_noise("session=abc123 started"));
|
709
|
+
assert!(!OpenCodeFilter::is_noise("Normal conversation text"));
|
710
|
+
}
|
711
|
+
|
712
|
+
#[test]
|
713
|
+
fn test_normalize_logs_edge_cases() {
|
714
|
+
let executor = SstOpencodeExecutor::new();
|
715
|
+
|
716
|
+
// Empty content
|
717
|
+
let result = executor.normalize_logs("", "/tmp").unwrap();
|
718
|
+
assert_eq!(result.entries.len(), 0);
|
719
|
+
|
720
|
+
// Only whitespace
|
721
|
+
let result = executor.normalize_logs(" \n\t\n ", "/tmp").unwrap();
|
722
|
+
assert_eq!(result.entries.len(), 0);
|
723
|
+
|
724
|
+
// Malformed JSON (current implementation skips invalid JSON)
|
725
|
+
let malformed = r#"{"timestamp":"2025-01-16T18:04:00Z","content":"incomplete"#;
|
726
|
+
let result = executor.normalize_logs(malformed, "/tmp").unwrap();
|
727
|
+
assert_eq!(result.entries.len(), 0); // Current implementation skips invalid JSON
|
728
|
+
|
729
|
+
// Mixed valid and invalid JSON
|
730
|
+
let mixed = r#"{"timestamp":"2025-01-16T18:04:00Z","entry_type":{"type":"assistant_message"},"content":"Valid entry","metadata":null}
|
731
|
+
Invalid line that's not JSON
|
732
|
+
{"timestamp":"2025-01-16T18:04:01Z","entry_type":{"type":"assistant_message"},"content":"Another valid entry","metadata":null}"#;
|
733
|
+
let result = executor.normalize_logs(mixed, "/tmp").unwrap();
|
734
|
+
assert_eq!(result.entries.len(), 2); // Only valid JSON entries are parsed
|
735
|
+
}
|
736
|
+
|
737
|
+
#[test]
|
738
|
+
fn test_ansi_code_stripping() {
|
739
|
+
use crate::executors::sst_opencode::filter::OpenCodeFilter;
|
740
|
+
|
741
|
+
// Test ANSI escape sequence removal
|
742
|
+
let ansi_text = "\x1b[31mRed text\x1b[0m normal text";
|
743
|
+
let cleaned = OpenCodeFilter::strip_ansi_codes(ansi_text);
|
744
|
+
assert_eq!(cleaned, "Red text normal text");
|
745
|
+
|
746
|
+
// Test unicode escape sequences
|
747
|
+
let unicode_ansi = "Text with \\u001b[32mgreen\\u001b[0m color";
|
748
|
+
let cleaned = OpenCodeFilter::strip_ansi_codes(unicode_ansi);
|
749
|
+
assert_eq!(cleaned, "Text with green color");
|
750
|
+
|
751
|
+
// Test text without ANSI codes (unchanged)
|
752
|
+
let plain_text = "Regular text without codes";
|
753
|
+
let cleaned = OpenCodeFilter::strip_ansi_codes(plain_text);
|
754
|
+
assert_eq!(cleaned, plain_text);
|
755
|
+
}
|
756
|
+
}
|