helixmind 0.1.0
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.
Potentially problematic release.
This version of helixmind might be problematic. Click here for more details.
- package/LICENSE +20 -0
- package/README.md +207 -0
- package/dist/cli/agent/autonomous.d.ts +10 -0
- package/dist/cli/agent/autonomous.d.ts.map +1 -0
- package/dist/cli/agent/autonomous.js +172 -0
- package/dist/cli/agent/autonomous.js.map +1 -0
- package/dist/cli/agent/loop.d.ts +76 -0
- package/dist/cli/agent/loop.d.ts.map +1 -0
- package/dist/cli/agent/loop.js +333 -0
- package/dist/cli/agent/loop.js.map +1 -0
- package/dist/cli/agent/permissions.d.ts +28 -0
- package/dist/cli/agent/permissions.d.ts.map +1 -0
- package/dist/cli/agent/permissions.js +180 -0
- package/dist/cli/agent/permissions.js.map +1 -0
- package/dist/cli/agent/sandbox.d.ts +17 -0
- package/dist/cli/agent/sandbox.d.ts.map +1 -0
- package/dist/cli/agent/sandbox.js +124 -0
- package/dist/cli/agent/sandbox.js.map +1 -0
- package/dist/cli/agent/tools/edit-file.d.ts +2 -0
- package/dist/cli/agent/tools/edit-file.d.ts.map +1 -0
- package/dist/cli/agent/tools/edit-file.js +49 -0
- package/dist/cli/agent/tools/edit-file.js.map +1 -0
- package/dist/cli/agent/tools/find.d.ts +2 -0
- package/dist/cli/agent/tools/find.d.ts.map +1 -0
- package/dist/cli/agent/tools/find.js +35 -0
- package/dist/cli/agent/tools/find.js.map +1 -0
- package/dist/cli/agent/tools/git-commit.d.ts +2 -0
- package/dist/cli/agent/tools/git-commit.d.ts.map +1 -0
- package/dist/cli/agent/tools/git-commit.js +51 -0
- package/dist/cli/agent/tools/git-commit.js.map +1 -0
- package/dist/cli/agent/tools/git-diff.d.ts +2 -0
- package/dist/cli/agent/tools/git-diff.d.ts.map +1 -0
- package/dist/cli/agent/tools/git-diff.js +36 -0
- package/dist/cli/agent/tools/git-diff.js.map +1 -0
- package/dist/cli/agent/tools/git-log.d.ts +2 -0
- package/dist/cli/agent/tools/git-log.d.ts.map +1 -0
- package/dist/cli/agent/tools/git-log.js +32 -0
- package/dist/cli/agent/tools/git-log.js.map +1 -0
- package/dist/cli/agent/tools/git-status.d.ts +2 -0
- package/dist/cli/agent/tools/git-status.d.ts.map +1 -0
- package/dist/cli/agent/tools/git-status.js +38 -0
- package/dist/cli/agent/tools/git-status.js.map +1 -0
- package/dist/cli/agent/tools/list-dir.d.ts +2 -0
- package/dist/cli/agent/tools/list-dir.d.ts.map +1 -0
- package/dist/cli/agent/tools/list-dir.js +73 -0
- package/dist/cli/agent/tools/list-dir.js.map +1 -0
- package/dist/cli/agent/tools/read-file.d.ts +2 -0
- package/dist/cli/agent/tools/read-file.d.ts.map +1 -0
- package/dist/cli/agent/tools/read-file.js +45 -0
- package/dist/cli/agent/tools/read-file.js.map +1 -0
- package/dist/cli/agent/tools/registry.d.ts +18 -0
- package/dist/cli/agent/tools/registry.d.ts.map +1 -0
- package/dist/cli/agent/tools/registry.js +31 -0
- package/dist/cli/agent/tools/registry.js.map +1 -0
- package/dist/cli/agent/tools/run-command.d.ts +2 -0
- package/dist/cli/agent/tools/run-command.d.ts.map +1 -0
- package/dist/cli/agent/tools/run-command.js +79 -0
- package/dist/cli/agent/tools/run-command.js.map +1 -0
- package/dist/cli/agent/tools/search.d.ts +2 -0
- package/dist/cli/agent/tools/search.d.ts.map +1 -0
- package/dist/cli/agent/tools/search.js +104 -0
- package/dist/cli/agent/tools/search.js.map +1 -0
- package/dist/cli/agent/tools/spiral-query.d.ts +2 -0
- package/dist/cli/agent/tools/spiral-query.d.ts.map +1 -0
- package/dist/cli/agent/tools/spiral-query.js +52 -0
- package/dist/cli/agent/tools/spiral-query.js.map +1 -0
- package/dist/cli/agent/tools/spiral-store.d.ts +2 -0
- package/dist/cli/agent/tools/spiral-store.d.ts.map +1 -0
- package/dist/cli/agent/tools/spiral-store.js +37 -0
- package/dist/cli/agent/tools/spiral-store.js.map +1 -0
- package/dist/cli/agent/tools/web-research.d.ts +2 -0
- package/dist/cli/agent/tools/web-research.d.ts.map +1 -0
- package/dist/cli/agent/tools/web-research.js +96 -0
- package/dist/cli/agent/tools/web-research.js.map +1 -0
- package/dist/cli/agent/tools/write-file.d.ts +2 -0
- package/dist/cli/agent/tools/write-file.d.ts.map +1 -0
- package/dist/cli/agent/tools/write-file.js +91 -0
- package/dist/cli/agent/tools/write-file.js.map +1 -0
- package/dist/cli/agent/undo.d.ts +30 -0
- package/dist/cli/agent/undo.d.ts.map +1 -0
- package/dist/cli/agent/undo.js +48 -0
- package/dist/cli/agent/undo.js.map +1 -0
- package/dist/cli/auth/callback-server.d.ts +15 -0
- package/dist/cli/auth/callback-server.d.ts.map +1 -0
- package/dist/cli/auth/callback-server.js +168 -0
- package/dist/cli/auth/callback-server.js.map +1 -0
- package/dist/cli/auth/feature-gate.d.ts +19 -0
- package/dist/cli/auth/feature-gate.d.ts.map +1 -0
- package/dist/cli/auth/feature-gate.js +74 -0
- package/dist/cli/auth/feature-gate.js.map +1 -0
- package/dist/cli/auth/guard.d.ts +10 -0
- package/dist/cli/auth/guard.d.ts.map +1 -0
- package/dist/cli/auth/guard.js +46 -0
- package/dist/cli/auth/guard.js.map +1 -0
- package/dist/cli/auth/login.d.ts +22 -0
- package/dist/cli/auth/login.d.ts.map +1 -0
- package/dist/cli/auth/login.js +194 -0
- package/dist/cli/auth/login.js.map +1 -0
- package/dist/cli/auth/logout.d.ts +6 -0
- package/dist/cli/auth/logout.d.ts.map +1 -0
- package/dist/cli/auth/logout.js +36 -0
- package/dist/cli/auth/logout.js.map +1 -0
- package/dist/cli/bench/dataset.d.ts +13 -0
- package/dist/cli/bench/dataset.d.ts.map +1 -0
- package/dist/cli/bench/dataset.js +97 -0
- package/dist/cli/bench/dataset.js.map +1 -0
- package/dist/cli/bench/harness.d.ts +7 -0
- package/dist/cli/bench/harness.d.ts.map +1 -0
- package/dist/cli/bench/harness.js +135 -0
- package/dist/cli/bench/harness.js.map +1 -0
- package/dist/cli/bench/metrics.d.ts +15 -0
- package/dist/cli/bench/metrics.d.ts.map +1 -0
- package/dist/cli/bench/metrics.js +98 -0
- package/dist/cli/bench/metrics.js.map +1 -0
- package/dist/cli/bench/output.d.ts +42 -0
- package/dist/cli/bench/output.d.ts.map +1 -0
- package/dist/cli/bench/output.js +140 -0
- package/dist/cli/bench/output.js.map +1 -0
- package/dist/cli/bench/prompt.d.ts +13 -0
- package/dist/cli/bench/prompt.d.ts.map +1 -0
- package/dist/cli/bench/prompt.js +106 -0
- package/dist/cli/bench/prompt.js.map +1 -0
- package/dist/cli/bench/runner.d.ts +14 -0
- package/dist/cli/bench/runner.d.ts.map +1 -0
- package/dist/cli/bench/runner.js +334 -0
- package/dist/cli/bench/runner.js.map +1 -0
- package/dist/cli/bench/types.d.ts +109 -0
- package/dist/cli/bench/types.d.ts.map +1 -0
- package/dist/cli/bench/types.js +2 -0
- package/dist/cli/bench/types.js.map +1 -0
- package/dist/cli/bench/ui.d.ts +12 -0
- package/dist/cli/bench/ui.d.ts.map +1 -0
- package/dist/cli/bench/ui.js +126 -0
- package/dist/cli/bench/ui.js.map +1 -0
- package/dist/cli/brain/archive.d.ts +33 -0
- package/dist/cli/brain/archive.d.ts.map +1 -0
- package/dist/cli/brain/archive.js +159 -0
- package/dist/cli/brain/archive.js.map +1 -0
- package/dist/cli/brain/control-protocol.d.ts +159 -0
- package/dist/cli/brain/control-protocol.d.ts.map +1 -0
- package/dist/cli/brain/control-protocol.js +41 -0
- package/dist/cli/brain/control-protocol.js.map +1 -0
- package/dist/cli/brain/exporter.d.ts +34 -0
- package/dist/cli/brain/exporter.d.ts.map +1 -0
- package/dist/cli/brain/exporter.js +37 -0
- package/dist/cli/brain/exporter.js.map +1 -0
- package/dist/cli/brain/generator.d.ts +67 -0
- package/dist/cli/brain/generator.d.ts.map +1 -0
- package/dist/cli/brain/generator.js +239 -0
- package/dist/cli/brain/generator.js.map +1 -0
- package/dist/cli/brain/relay-client.d.ts +8 -0
- package/dist/cli/brain/relay-client.d.ts.map +1 -0
- package/dist/cli/brain/relay-client.js +173 -0
- package/dist/cli/brain/relay-client.js.map +1 -0
- package/dist/cli/brain/server.d.ts +34 -0
- package/dist/cli/brain/server.d.ts.map +1 -0
- package/dist/cli/brain/server.js +425 -0
- package/dist/cli/brain/server.js.map +1 -0
- package/dist/cli/brain/template.d.ts +3 -0
- package/dist/cli/brain/template.d.ts.map +1 -0
- package/dist/cli/brain/template.js +2072 -0
- package/dist/cli/brain/template.js.map +1 -0
- package/dist/cli/checkpoints/browser.d.ts +27 -0
- package/dist/cli/checkpoints/browser.d.ts.map +1 -0
- package/dist/cli/checkpoints/browser.js +238 -0
- package/dist/cli/checkpoints/browser.js.map +1 -0
- package/dist/cli/checkpoints/keybinding.d.ts +22 -0
- package/dist/cli/checkpoints/keybinding.d.ts.map +1 -0
- package/dist/cli/checkpoints/keybinding.js +43 -0
- package/dist/cli/checkpoints/keybinding.js.map +1 -0
- package/dist/cli/checkpoints/revert.d.ts +37 -0
- package/dist/cli/checkpoints/revert.d.ts.map +1 -0
- package/dist/cli/checkpoints/revert.js +144 -0
- package/dist/cli/checkpoints/revert.js.map +1 -0
- package/dist/cli/checkpoints/store.d.ts +48 -0
- package/dist/cli/checkpoints/store.d.ts.map +1 -0
- package/dist/cli/checkpoints/store.js +188 -0
- package/dist/cli/checkpoints/store.js.map +1 -0
- package/dist/cli/commands/archive.d.ts +7 -0
- package/dist/cli/commands/archive.d.ts.map +1 -0
- package/dist/cli/commands/archive.js +66 -0
- package/dist/cli/commands/archive.js.map +1 -0
- package/dist/cli/commands/auth.d.ts +10 -0
- package/dist/cli/commands/auth.d.ts.map +1 -0
- package/dist/cli/commands/auth.js +44 -0
- package/dist/cli/commands/auth.js.map +1 -0
- package/dist/cli/commands/bench.d.ts +25 -0
- package/dist/cli/commands/bench.d.ts.map +1 -0
- package/dist/cli/commands/bench.js +114 -0
- package/dist/cli/commands/bench.js.map +1 -0
- package/dist/cli/commands/chat.d.ts +11 -0
- package/dist/cli/commands/chat.d.ts.map +1 -0
- package/dist/cli/commands/chat.js +2321 -0
- package/dist/cli/commands/chat.js.map +1 -0
- package/dist/cli/commands/config.d.ts +4 -0
- package/dist/cli/commands/config.d.ts.map +1 -0
- package/dist/cli/commands/config.js +41 -0
- package/dist/cli/commands/config.js.map +1 -0
- package/dist/cli/commands/feed.d.ts +6 -0
- package/dist/cli/commands/feed.d.ts.map +1 -0
- package/dist/cli/commands/feed.js +95 -0
- package/dist/cli/commands/feed.js.map +1 -0
- package/dist/cli/commands/helix-menu.d.ts +4 -0
- package/dist/cli/commands/helix-menu.d.ts.map +1 -0
- package/dist/cli/commands/helix-menu.js +400 -0
- package/dist/cli/commands/helix-menu.js.map +1 -0
- package/dist/cli/commands/init.d.ts +2 -0
- package/dist/cli/commands/init.d.ts.map +1 -0
- package/dist/cli/commands/init.js +26 -0
- package/dist/cli/commands/init.js.map +1 -0
- package/dist/cli/commands/setup.d.ts +20 -0
- package/dist/cli/commands/setup.d.ts.map +1 -0
- package/dist/cli/commands/setup.js +314 -0
- package/dist/cli/commands/setup.js.map +1 -0
- package/dist/cli/commands/spiral.d.ts +4 -0
- package/dist/cli/commands/spiral.d.ts.map +1 -0
- package/dist/cli/commands/spiral.js +81 -0
- package/dist/cli/commands/spiral.js.map +1 -0
- package/dist/cli/config/store.d.ts +72 -0
- package/dist/cli/config/store.d.ts.map +1 -0
- package/dist/cli/config/store.js +241 -0
- package/dist/cli/config/store.js.map +1 -0
- package/dist/cli/context/assembler.d.ts +8 -0
- package/dist/cli/context/assembler.d.ts.map +1 -0
- package/dist/cli/context/assembler.js +124 -0
- package/dist/cli/context/assembler.js.map +1 -0
- package/dist/cli/context/project.d.ts +13 -0
- package/dist/cli/context/project.d.ts.map +1 -0
- package/dist/cli/context/project.js +126 -0
- package/dist/cli/context/project.js.map +1 -0
- package/dist/cli/context/session-buffer.d.ts +57 -0
- package/dist/cli/context/session-buffer.d.ts.map +1 -0
- package/dist/cli/context/session-buffer.js +268 -0
- package/dist/cli/context/session-buffer.js.map +1 -0
- package/dist/cli/context/trimmer.d.ts +26 -0
- package/dist/cli/context/trimmer.d.ts.map +1 -0
- package/dist/cli/context/trimmer.js +105 -0
- package/dist/cli/context/trimmer.js.map +1 -0
- package/dist/cli/feed/analyzer.d.ts +17 -0
- package/dist/cli/feed/analyzer.d.ts.map +1 -0
- package/dist/cli/feed/analyzer.js +220 -0
- package/dist/cli/feed/analyzer.js.map +1 -0
- package/dist/cli/feed/intent.d.ts +8 -0
- package/dist/cli/feed/intent.d.ts.map +1 -0
- package/dist/cli/feed/intent.js +70 -0
- package/dist/cli/feed/intent.js.map +1 -0
- package/dist/cli/feed/parser.d.ts +23 -0
- package/dist/cli/feed/parser.d.ts.map +1 -0
- package/dist/cli/feed/parser.js +166 -0
- package/dist/cli/feed/parser.js.map +1 -0
- package/dist/cli/feed/pipeline.d.ts +32 -0
- package/dist/cli/feed/pipeline.d.ts.map +1 -0
- package/dist/cli/feed/pipeline.js +242 -0
- package/dist/cli/feed/pipeline.js.map +1 -0
- package/dist/cli/feed/reader.d.ts +10 -0
- package/dist/cli/feed/reader.d.ts.map +1 -0
- package/dist/cli/feed/reader.js +61 -0
- package/dist/cli/feed/reader.js.map +1 -0
- package/dist/cli/feed/scanner.d.ts +10 -0
- package/dist/cli/feed/scanner.d.ts.map +1 -0
- package/dist/cli/feed/scanner.js +124 -0
- package/dist/cli/feed/scanner.js.map +1 -0
- package/dist/cli/feed/watcher.d.ts +14 -0
- package/dist/cli/feed/watcher.d.ts.map +1 -0
- package/dist/cli/feed/watcher.js +76 -0
- package/dist/cli/feed/watcher.js.map +1 -0
- package/dist/cli/index.d.ts +3 -0
- package/dist/cli/index.d.ts.map +1 -0
- package/dist/cli/index.js +204 -0
- package/dist/cli/index.js.map +1 -0
- package/dist/cli/providers/anthropic.d.ts +10 -0
- package/dist/cli/providers/anthropic.d.ts.map +1 -0
- package/dist/cli/providers/anthropic.js +117 -0
- package/dist/cli/providers/anthropic.js.map +1 -0
- package/dist/cli/providers/ollama.d.ts +37 -0
- package/dist/cli/providers/ollama.d.ts.map +1 -0
- package/dist/cli/providers/ollama.js +151 -0
- package/dist/cli/providers/ollama.js.map +1 -0
- package/dist/cli/providers/openai.d.ts +13 -0
- package/dist/cli/providers/openai.d.ts.map +1 -0
- package/dist/cli/providers/openai.js +283 -0
- package/dist/cli/providers/openai.js.map +1 -0
- package/dist/cli/providers/rate-limiter.d.ts +51 -0
- package/dist/cli/providers/rate-limiter.d.ts.map +1 -0
- package/dist/cli/providers/rate-limiter.js +164 -0
- package/dist/cli/providers/rate-limiter.js.map +1 -0
- package/dist/cli/providers/registry.d.ts +16 -0
- package/dist/cli/providers/registry.d.ts.map +1 -0
- package/dist/cli/providers/registry.js +99 -0
- package/dist/cli/providers/registry.js.map +1 -0
- package/dist/cli/providers/types.d.ts +61 -0
- package/dist/cli/providers/types.d.ts.map +1 -0
- package/dist/cli/providers/types.js +2 -0
- package/dist/cli/providers/types.js.map +1 -0
- package/dist/cli/sessions/manager.d.ts +69 -0
- package/dist/cli/sessions/manager.d.ts.map +1 -0
- package/dist/cli/sessions/manager.js +200 -0
- package/dist/cli/sessions/manager.js.map +1 -0
- package/dist/cli/sessions/session.d.ts +54 -0
- package/dist/cli/sessions/session.d.ts.map +1 -0
- package/dist/cli/sessions/session.js +70 -0
- package/dist/cli/sessions/session.js.map +1 -0
- package/dist/cli/sessions/tab-view.d.ts +18 -0
- package/dist/cli/sessions/tab-view.d.ts.map +1 -0
- package/dist/cli/sessions/tab-view.js +134 -0
- package/dist/cli/sessions/tab-view.js.map +1 -0
- package/dist/cli/ui/activity.d.ts +82 -0
- package/dist/cli/ui/activity.d.ts.map +1 -0
- package/dist/cli/ui/activity.js +309 -0
- package/dist/cli/ui/activity.js.map +1 -0
- package/dist/cli/ui/chat-view.d.ts +10 -0
- package/dist/cli/ui/chat-view.d.ts.map +1 -0
- package/dist/cli/ui/chat-view.js +165 -0
- package/dist/cli/ui/chat-view.js.map +1 -0
- package/dist/cli/ui/command-suggest.d.ts +22 -0
- package/dist/cli/ui/command-suggest.d.ts.map +1 -0
- package/dist/cli/ui/command-suggest.js +115 -0
- package/dist/cli/ui/command-suggest.js.map +1 -0
- package/dist/cli/ui/logo.d.ts +3 -0
- package/dist/cli/ui/logo.d.ts.map +1 -0
- package/dist/cli/ui/logo.js +25 -0
- package/dist/cli/ui/logo.js.map +1 -0
- package/dist/cli/ui/progress.d.ts +21 -0
- package/dist/cli/ui/progress.d.ts.map +1 -0
- package/dist/cli/ui/progress.js +125 -0
- package/dist/cli/ui/progress.js.map +1 -0
- package/dist/cli/ui/select-menu.d.ts +22 -0
- package/dist/cli/ui/select-menu.d.ts.map +1 -0
- package/dist/cli/ui/select-menu.js +152 -0
- package/dist/cli/ui/select-menu.js.map +1 -0
- package/dist/cli/ui/spinner.d.ts +3 -0
- package/dist/cli/ui/spinner.d.ts.map +1 -0
- package/dist/cli/ui/spinner.js +14 -0
- package/dist/cli/ui/spinner.js.map +1 -0
- package/dist/cli/ui/statusbar.d.ts +65 -0
- package/dist/cli/ui/statusbar.d.ts.map +1 -0
- package/dist/cli/ui/statusbar.js +272 -0
- package/dist/cli/ui/statusbar.js.map +1 -0
- package/dist/cli/ui/theme.d.ts +20 -0
- package/dist/cli/ui/theme.d.ts.map +1 -0
- package/dist/cli/ui/theme.js +25 -0
- package/dist/cli/ui/theme.js.map +1 -0
- package/dist/cli/ui/tool-output.d.ts +25 -0
- package/dist/cli/ui/tool-output.d.ts.map +1 -0
- package/dist/cli/ui/tool-output.js +171 -0
- package/dist/cli/ui/tool-output.js.map +1 -0
- package/dist/cli/validation/autofix.d.ts +32 -0
- package/dist/cli/validation/autofix.d.ts.map +1 -0
- package/dist/cli/validation/autofix.js +148 -0
- package/dist/cli/validation/autofix.js.map +1 -0
- package/dist/cli/validation/classifier.d.ts +17 -0
- package/dist/cli/validation/classifier.d.ts.map +1 -0
- package/dist/cli/validation/classifier.js +174 -0
- package/dist/cli/validation/classifier.js.map +1 -0
- package/dist/cli/validation/criteria.d.ts +22 -0
- package/dist/cli/validation/criteria.d.ts.map +1 -0
- package/dist/cli/validation/criteria.js +188 -0
- package/dist/cli/validation/criteria.js.map +1 -0
- package/dist/cli/validation/dynamic-checks.d.ts +16 -0
- package/dist/cli/validation/dynamic-checks.d.ts.map +1 -0
- package/dist/cli/validation/dynamic-checks.js +109 -0
- package/dist/cli/validation/dynamic-checks.js.map +1 -0
- package/dist/cli/validation/model.d.ts +16 -0
- package/dist/cli/validation/model.d.ts.map +1 -0
- package/dist/cli/validation/model.js +59 -0
- package/dist/cli/validation/model.js.map +1 -0
- package/dist/cli/validation/reporter.d.ts +18 -0
- package/dist/cli/validation/reporter.d.ts.map +1 -0
- package/dist/cli/validation/reporter.js +167 -0
- package/dist/cli/validation/reporter.js.map +1 -0
- package/dist/cli/validation/spiral-checks.d.ts +12 -0
- package/dist/cli/validation/spiral-checks.d.ts.map +1 -0
- package/dist/cli/validation/spiral-checks.js +167 -0
- package/dist/cli/validation/spiral-checks.js.map +1 -0
- package/dist/cli/validation/static-checks.d.ts +26 -0
- package/dist/cli/validation/static-checks.d.ts.map +1 -0
- package/dist/cli/validation/static-checks.js +492 -0
- package/dist/cli/validation/static-checks.js.map +1 -0
- package/dist/cli/validation/stats.d.ts +29 -0
- package/dist/cli/validation/stats.d.ts.map +1 -0
- package/dist/cli/validation/stats.js +137 -0
- package/dist/cli/validation/stats.js.map +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +35 -0
- package/dist/index.js.map +1 -0
- package/dist/server.d.ts +8 -0
- package/dist/server.d.ts.map +1 -0
- package/dist/server.js +43 -0
- package/dist/server.js.map +1 -0
- package/dist/spiral/cloud/content-extractor.d.ts +27 -0
- package/dist/spiral/cloud/content-extractor.d.ts.map +1 -0
- package/dist/spiral/cloud/content-extractor.js +175 -0
- package/dist/spiral/cloud/content-extractor.js.map +1 -0
- package/dist/spiral/cloud/search-provider.d.ts +22 -0
- package/dist/spiral/cloud/search-provider.d.ts.map +1 -0
- package/dist/spiral/cloud/search-provider.js +212 -0
- package/dist/spiral/cloud/search-provider.js.map +1 -0
- package/dist/spiral/cloud/topic-detector.d.ts +25 -0
- package/dist/spiral/cloud/topic-detector.d.ts.map +1 -0
- package/dist/spiral/cloud/topic-detector.js +196 -0
- package/dist/spiral/cloud/topic-detector.js.map +1 -0
- package/dist/spiral/cloud/web-enricher.d.ts +58 -0
- package/dist/spiral/cloud/web-enricher.d.ts.map +1 -0
- package/dist/spiral/cloud/web-enricher.js +170 -0
- package/dist/spiral/cloud/web-enricher.js.map +1 -0
- package/dist/spiral/compression.d.ts +54 -0
- package/dist/spiral/compression.d.ts.map +1 -0
- package/dist/spiral/compression.js +175 -0
- package/dist/spiral/compression.js.map +1 -0
- package/dist/spiral/embeddings.d.ts +24 -0
- package/dist/spiral/embeddings.d.ts.map +1 -0
- package/dist/spiral/embeddings.js +69 -0
- package/dist/spiral/embeddings.js.map +1 -0
- package/dist/spiral/engine.d.ts +95 -0
- package/dist/spiral/engine.d.ts.map +1 -0
- package/dist/spiral/engine.js +271 -0
- package/dist/spiral/engine.js.map +1 -0
- package/dist/spiral/injection.d.ts +29 -0
- package/dist/spiral/injection.d.ts.map +1 -0
- package/dist/spiral/injection.js +164 -0
- package/dist/spiral/injection.js.map +1 -0
- package/dist/spiral/relevance.d.ts +37 -0
- package/dist/spiral/relevance.d.ts.map +1 -0
- package/dist/spiral/relevance.js +75 -0
- package/dist/spiral/relevance.js.map +1 -0
- package/dist/storage/database.d.ts +11 -0
- package/dist/storage/database.d.ts.map +1 -0
- package/dist/storage/database.js +141 -0
- package/dist/storage/database.js.map +1 -0
- package/dist/storage/edges.d.ts +25 -0
- package/dist/storage/edges.d.ts.map +1 -0
- package/dist/storage/edges.js +69 -0
- package/dist/storage/edges.js.map +1 -0
- package/dist/storage/nodes.d.ts +45 -0
- package/dist/storage/nodes.d.ts.map +1 -0
- package/dist/storage/nodes.js +124 -0
- package/dist/storage/nodes.js.map +1 -0
- package/dist/storage/vectors.d.ts +25 -0
- package/dist/storage/vectors.d.ts.map +1 -0
- package/dist/storage/vectors.js +110 -0
- package/dist/storage/vectors.js.map +1 -0
- package/dist/tools/spiral-compact.d.ts +14 -0
- package/dist/tools/spiral-compact.d.ts.map +1 -0
- package/dist/tools/spiral-compact.js +14 -0
- package/dist/tools/spiral-compact.js.map +1 -0
- package/dist/tools/spiral-query.d.ts +18 -0
- package/dist/tools/spiral-query.d.ts.map +1 -0
- package/dist/tools/spiral-query.js +16 -0
- package/dist/tools/spiral-query.js.map +1 -0
- package/dist/tools/spiral-relate.d.ts +21 -0
- package/dist/tools/spiral-relate.d.ts.map +1 -0
- package/dist/tools/spiral-relate.js +21 -0
- package/dist/tools/spiral-relate.js.map +1 -0
- package/dist/tools/spiral-status.d.ts +8 -0
- package/dist/tools/spiral-status.d.ts.map +1 -0
- package/dist/tools/spiral-status.js +10 -0
- package/dist/tools/spiral-status.js.map +1 -0
- package/dist/tools/spiral-store.d.ts +41 -0
- package/dist/tools/spiral-store.d.ts.map +1 -0
- package/dist/tools/spiral-store.js +22 -0
- package/dist/tools/spiral-store.js.map +1 -0
- package/dist/types.d.ts +92 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +9 -0
- package/dist/types.js.map +1 -0
- package/dist/utils/config.d.ts +14 -0
- package/dist/utils/config.d.ts.map +1 -0
- package/dist/utils/config.js +44 -0
- package/dist/utils/config.js.map +1 -0
- package/dist/utils/logger.d.ts +10 -0
- package/dist/utils/logger.d.ts.map +1 -0
- package/dist/utils/logger.js +40 -0
- package/dist/utils/logger.js.map +1 -0
- package/dist/utils/tokens.d.ts +21 -0
- package/dist/utils/tokens.d.ts.map +1 -0
- package/dist/utils/tokens.js +33 -0
- package/dist/utils/tokens.js.map +1 -0
- package/package.json +90 -0
|
@@ -0,0 +1,2321 @@
|
|
|
1
|
+
import * as readline from 'node:readline';
|
|
2
|
+
import { homedir } from 'node:os';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
import { ConfigStore } from '../config/store.js';
|
|
5
|
+
import { createProvider } from '../providers/registry.js';
|
|
6
|
+
import { analyzeProject } from '../context/project.js';
|
|
7
|
+
import { assembleSystemPrompt } from '../context/assembler.js';
|
|
8
|
+
import { renderLogo } from '../ui/logo.js';
|
|
9
|
+
import { renderError, renderInfo, renderSpiralStatus, renderUserMessage, } from '../ui/chat-view.js';
|
|
10
|
+
import { isInsideToolBlock } from '../ui/tool-output.js';
|
|
11
|
+
import { renderFeedProgress, renderFeedSummary } from '../ui/progress.js';
|
|
12
|
+
import { ActivityIndicator } from '../ui/activity.js';
|
|
13
|
+
import { theme } from '../ui/theme.js';
|
|
14
|
+
import { detectFeedIntent } from '../feed/intent.js';
|
|
15
|
+
import { runFeedPipeline } from '../feed/pipeline.js';
|
|
16
|
+
import { showHelixMenu } from './helix-menu.js';
|
|
17
|
+
import { initializeTools } from '../agent/tools/registry.js';
|
|
18
|
+
import { runAgentLoop, AgentController, AgentAbortError } from '../agent/loop.js';
|
|
19
|
+
import { PermissionManager } from '../agent/permissions.js';
|
|
20
|
+
import { UndoStack } from '../agent/undo.js';
|
|
21
|
+
import { writeStatusBar, renderStatusBar, getGitInfo, truncateBar } from '../ui/statusbar.js';
|
|
22
|
+
import { CheckpointStore } from '../checkpoints/store.js';
|
|
23
|
+
import { createKeybindingState, processKeypress } from '../checkpoints/keybinding.js';
|
|
24
|
+
import { runCheckpointBrowser } from '../checkpoints/browser.js';
|
|
25
|
+
import { runFirstTimeSetup, showModelSwitcher, showKeyManagement } from './setup.js';
|
|
26
|
+
import { SessionBuffer } from '../context/session-buffer.js';
|
|
27
|
+
import { trimConversation } from '../context/trimmer.js';
|
|
28
|
+
import { runAutonomousLoop, SECURITY_PROMPT } from '../agent/autonomous.js';
|
|
29
|
+
import { SessionManager } from '../sessions/manager.js';
|
|
30
|
+
import { renderSessionNotification, renderSessionList } from '../sessions/tab-view.js';
|
|
31
|
+
import { getSuggestions, writeSuggestions, clearSuggestions } from '../ui/command-suggest.js';
|
|
32
|
+
import { selectMenu } from '../ui/select-menu.js';
|
|
33
|
+
import { classifyTask } from '../validation/classifier.js';
|
|
34
|
+
import { generateCriteria } from '../validation/criteria.js';
|
|
35
|
+
import { validationLoop } from '../validation/autofix.js';
|
|
36
|
+
import { createValidationProvider } from '../validation/model.js';
|
|
37
|
+
import { renderValidationSummary, renderValidationStart, renderClassification } from '../validation/reporter.js';
|
|
38
|
+
import { storeValidationResult, getValidationStats, renderValidationStats } from '../validation/stats.js';
|
|
39
|
+
import chalk from 'chalk';
|
|
40
|
+
const HELP_CATEGORIES = [
|
|
41
|
+
{
|
|
42
|
+
category: 'Chat & Interaction', color: '#00d4ff',
|
|
43
|
+
items: [
|
|
44
|
+
{ cmd: '/clear', label: '/clear', description: 'Clear conversation history' },
|
|
45
|
+
{ cmd: '/model', label: '/model', description: 'Switch LLM model' },
|
|
46
|
+
{ cmd: '/keys', label: '/keys', description: 'Manage API keys' },
|
|
47
|
+
{ cmd: '/yolo', label: '/yolo', description: 'Toggle YOLO mode' },
|
|
48
|
+
{ cmd: '/skip-permissions', label: '/skip-permissions', description: 'Toggle skip-permissions' },
|
|
49
|
+
],
|
|
50
|
+
},
|
|
51
|
+
{
|
|
52
|
+
category: 'Spiral Memory', color: '#00ff88',
|
|
53
|
+
items: [
|
|
54
|
+
{ cmd: '/spiral', label: '/spiral', description: 'Show spiral status (nodes per level)' },
|
|
55
|
+
{ cmd: '/feed', label: '/feed', description: 'Feed files into spiral' },
|
|
56
|
+
{ cmd: '/context', label: '/context', description: 'Show context size & embeddings' },
|
|
57
|
+
{ cmd: '/compact', label: '/compact', description: 'Trigger spiral evolution' },
|
|
58
|
+
{ cmd: '/tokens', label: '/tokens', description: 'Show token usage & memory' },
|
|
59
|
+
],
|
|
60
|
+
},
|
|
61
|
+
{
|
|
62
|
+
category: 'Visualization & Brain', color: '#4169e1',
|
|
63
|
+
items: [
|
|
64
|
+
{ cmd: '/brain', label: '/brain', description: 'Brain scope + 3D visualization' },
|
|
65
|
+
{ cmd: '/brain local', label: '/brain local', description: 'Switch to project-local brain' },
|
|
66
|
+
{ cmd: '/brain global', label: '/brain global', description: 'Switch to global brain' },
|
|
67
|
+
{ cmd: '/helix', label: '/helix', description: 'Command Center + Brain (auto-start)' },
|
|
68
|
+
{ cmd: '/helixlocal', label: '/helixlocal', description: 'Command Center + local brain' },
|
|
69
|
+
{ cmd: '/helixglobal', label: '/helixglobal', description: 'Command Center + global brain' },
|
|
70
|
+
],
|
|
71
|
+
},
|
|
72
|
+
{
|
|
73
|
+
category: 'Autonomous & Security', color: '#ff6600',
|
|
74
|
+
items: [
|
|
75
|
+
{ cmd: '/auto', label: '/auto', description: 'Autonomous mode' },
|
|
76
|
+
{ cmd: '/stop', label: '/stop', description: 'Stop autonomous mode' },
|
|
77
|
+
{ cmd: '/security', label: '/security', description: 'Run security audit (background)' },
|
|
78
|
+
{ cmd: '/sessions', label: '/sessions', description: 'List all sessions & tabs' },
|
|
79
|
+
{ cmd: '/local', label: '/local', description: 'Local LLM setup (Ollama)' },
|
|
80
|
+
],
|
|
81
|
+
},
|
|
82
|
+
{
|
|
83
|
+
category: 'Validation Matrix', color: '#00cc66',
|
|
84
|
+
items: [
|
|
85
|
+
{ cmd: '/validation', label: '/validation', description: 'Show validation status' },
|
|
86
|
+
{ cmd: '/validation on', label: '/validation on', description: 'Enable output validation' },
|
|
87
|
+
{ cmd: '/validation off', label: '/validation off', description: 'Disable output validation' },
|
|
88
|
+
{ cmd: '/validation verbose', label: '/validation verbose', description: 'Toggle verbose mode' },
|
|
89
|
+
{ cmd: '/validation strict', label: '/validation strict', description: 'Toggle strict mode' },
|
|
90
|
+
{ cmd: '/validation stats', label: '/validation stats', description: 'Show validation statistics' },
|
|
91
|
+
],
|
|
92
|
+
},
|
|
93
|
+
{
|
|
94
|
+
category: 'Code & Git', color: '#8a2be2',
|
|
95
|
+
items: [
|
|
96
|
+
{ cmd: '/undo', label: '/undo', description: 'Undo file changes' },
|
|
97
|
+
{ cmd: '/diff', label: '/diff', description: 'Show uncommitted git changes' },
|
|
98
|
+
{ cmd: '/git', label: '/git', description: 'Show git branch & status' },
|
|
99
|
+
{ cmd: '/project', label: '/project', description: 'Show project info' },
|
|
100
|
+
{ cmd: '/export', label: '/export', description: 'Export spiral as ZIP' },
|
|
101
|
+
],
|
|
102
|
+
},
|
|
103
|
+
{
|
|
104
|
+
category: 'Account & Auth', color: '#00d4ff',
|
|
105
|
+
items: [
|
|
106
|
+
{ cmd: '/login', label: '/login', description: 'Log in to HelixMind web platform' },
|
|
107
|
+
{ cmd: '/logout', label: '/logout', description: 'Log out and revoke API key' },
|
|
108
|
+
{ cmd: '/whoami', label: '/whoami', description: 'Show account & plan info' },
|
|
109
|
+
],
|
|
110
|
+
},
|
|
111
|
+
{
|
|
112
|
+
category: 'Navigation', color: '#6c757d',
|
|
113
|
+
items: [
|
|
114
|
+
{ cmd: '/exit', label: '/exit', description: 'Exit HelixMind' },
|
|
115
|
+
],
|
|
116
|
+
},
|
|
117
|
+
];
|
|
118
|
+
/** Build flat MenuItem[] with category headers as disabled separators */
|
|
119
|
+
function buildHelpMenuItems() {
|
|
120
|
+
const items = [];
|
|
121
|
+
const commands = [];
|
|
122
|
+
for (const cat of HELP_CATEGORIES) {
|
|
123
|
+
items.push({ label: chalk.hex(cat.color).bold(cat.category), disabled: true });
|
|
124
|
+
commands.push('');
|
|
125
|
+
for (const item of cat.items) {
|
|
126
|
+
items.push({ label: theme.primary(item.label), description: item.description });
|
|
127
|
+
commands.push(item.cmd);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
items.push({ label: '', disabled: true });
|
|
131
|
+
commands.push('');
|
|
132
|
+
items.push({ label: chalk.dim('ESC Stop Agent | Ctrl+C Clear | Tab Autocomplete'), disabled: true });
|
|
133
|
+
commands.push('');
|
|
134
|
+
return { items, commands };
|
|
135
|
+
}
|
|
136
|
+
// Keep static HELP_TEXT as fallback for non-TTY
|
|
137
|
+
const HELP_TEXT = `
|
|
138
|
+
${chalk.hex('#00d4ff').bold(' Chat & Interaction')}
|
|
139
|
+
${theme.primary('/help'.padEnd(22))} ${theme.dim('Show this help')}
|
|
140
|
+
${theme.primary('/clear'.padEnd(22))} ${theme.dim('Clear conversation history')}
|
|
141
|
+
${theme.primary('/model [name]'.padEnd(22))} ${theme.dim('Switch model (interactive or direct: /model gpt-4o)')}
|
|
142
|
+
${theme.primary('/keys'.padEnd(22))} ${theme.dim('Add/remove/update API keys')}
|
|
143
|
+
${theme.primary('/yolo [on|off]'.padEnd(22))} ${theme.dim('Toggle YOLO mode — auto-approve ALL operations')}
|
|
144
|
+
${theme.primary('/skip-permissions'.padEnd(22))} ${theme.dim('Toggle skip-permissions (auto-approve safe ops)')}
|
|
145
|
+
|
|
146
|
+
${chalk.hex('#00ff88').bold(' Spiral Memory')}
|
|
147
|
+
${theme.primary('/spiral'.padEnd(22))} ${theme.dim('Show spiral status (nodes per level)')}
|
|
148
|
+
${theme.primary('/feed [path]'.padEnd(22))} ${theme.dim('Feed files into spiral (default: current dir)')}
|
|
149
|
+
${theme.primary('/context'.padEnd(22))} ${theme.dim('Show current context size & embeddings')}
|
|
150
|
+
${theme.primary('/compact'.padEnd(22))} ${theme.dim('Trigger spiral evolution (promote/demote nodes)')}
|
|
151
|
+
${theme.primary('/tokens'.padEnd(22))} ${theme.dim('Show token usage, checkpoints, memory')}
|
|
152
|
+
|
|
153
|
+
${chalk.hex('#4169e1').bold(' Visualization & Brain')}
|
|
154
|
+
${theme.primary('/brain'.padEnd(22))} ${theme.dim('Show brain scope + open 3D visualization')}
|
|
155
|
+
${theme.primary('/brain local'.padEnd(22))} ${theme.dim('Switch to project-local brain (.helixmind/)')}
|
|
156
|
+
${theme.primary('/brain global'.padEnd(22))} ${theme.dim('Switch to global brain (~/.spiral-context/)')}
|
|
157
|
+
${theme.primary('/helix'.padEnd(22))} ${theme.dim('Command Center + Brain (auto-start local)')}
|
|
158
|
+
${theme.primary('/helixlocal'.padEnd(22))} ${theme.dim('Command Center + local brain')}
|
|
159
|
+
${theme.primary('/helixglobal'.padEnd(22))} ${theme.dim('Command Center + global brain')}
|
|
160
|
+
|
|
161
|
+
${chalk.hex('#ff6600').bold(' Autonomous & Security')}
|
|
162
|
+
${theme.primary('/auto'.padEnd(22))} ${theme.dim('Autonomous mode \u2014 find & fix issues continuously')}
|
|
163
|
+
${theme.primary('/stop'.padEnd(22))} ${theme.dim('Stop autonomous mode')}
|
|
164
|
+
${theme.primary('/security'.padEnd(22))} ${theme.dim('Run comprehensive security audit (background)')}
|
|
165
|
+
${theme.primary('/sessions'.padEnd(22))} ${theme.dim('List all sessions & tabs')}
|
|
166
|
+
${theme.primary('/local'.padEnd(22))} ${theme.dim('Local LLM setup \u2014 Ollama models')}
|
|
167
|
+
|
|
168
|
+
${chalk.hex('#00cc66').bold(' Validation Matrix')}
|
|
169
|
+
${theme.primary('/validation'.padEnd(22))} ${theme.dim('Show validation mode')}
|
|
170
|
+
${theme.primary('/validation on'.padEnd(22))} ${theme.dim('Enable output validation')}
|
|
171
|
+
${theme.primary('/validation off'.padEnd(22))} ${theme.dim('Disable output validation')}
|
|
172
|
+
${theme.primary('/validation verbose'.padEnd(22))} ${theme.dim('Show every check detail')}
|
|
173
|
+
${theme.primary('/validation strict'.padEnd(22))} ${theme.dim('Treat warnings as errors')}
|
|
174
|
+
${theme.primary('/validation stats'.padEnd(22))} ${theme.dim('Show validation statistics')}
|
|
175
|
+
|
|
176
|
+
${chalk.hex('#8a2be2').bold(' Code & Git')}
|
|
177
|
+
${theme.primary('/undo [n|list]'.padEnd(22))} ${theme.dim('Undo last n file changes (or list history)')}
|
|
178
|
+
${theme.primary('/diff'.padEnd(22))} ${theme.dim('Show all uncommitted git changes')}
|
|
179
|
+
${theme.primary('/git'.padEnd(22))} ${theme.dim('Show git branch & status')}
|
|
180
|
+
${theme.primary('/project'.padEnd(22))} ${theme.dim('Show detected project info')}
|
|
181
|
+
${theme.primary('/export [dir]'.padEnd(22))} ${theme.dim('Export spiral as ZIP archive')}
|
|
182
|
+
|
|
183
|
+
${chalk.hex('#6c757d').bold(' Navigation')}
|
|
184
|
+
${theme.primary('/exit /quit'.padEnd(22))} ${theme.dim('Exit HelixMind')}
|
|
185
|
+
${theme.dim(' ESC'.padEnd(22))} ${theme.dim('Stop agent (immediately interrupts)')}
|
|
186
|
+
${theme.dim(' Ctrl+C'.padEnd(22))} ${theme.dim('Clear input (or double to force exit)')}
|
|
187
|
+
${theme.dim(' Tab'.padEnd(22))} ${theme.dim('Autocomplete command')}
|
|
188
|
+
`;
|
|
189
|
+
export async function chatCommand(options) {
|
|
190
|
+
const configDir = join(homedir(), '.helixmind');
|
|
191
|
+
const store = new ConfigStore(configDir);
|
|
192
|
+
let config = store.getAll();
|
|
193
|
+
// Show logo early
|
|
194
|
+
process.stdout.write(renderLogo());
|
|
195
|
+
// ─── Auth Gate: require login on first use ───────────────────
|
|
196
|
+
// Once logged in, credentials are cached locally.
|
|
197
|
+
// Offline use works with cached auth — no server needed.
|
|
198
|
+
if (!store.isLoggedIn()) {
|
|
199
|
+
const { requireAuth } = await import('../auth/guard.js');
|
|
200
|
+
await requireAuth();
|
|
201
|
+
config = store.getAll();
|
|
202
|
+
}
|
|
203
|
+
else {
|
|
204
|
+
// Background auth check: verify token is still valid when online.
|
|
205
|
+
// If offline or server unreachable, cached auth stays valid silently.
|
|
206
|
+
import('../auth/feature-gate.js').then(({ refreshPlanInfo }) => refreshPlanInfo(store)).catch(() => { });
|
|
207
|
+
}
|
|
208
|
+
// First-time setup: prompt for LLM API key if none configured
|
|
209
|
+
if (!store.hasApiKey()) {
|
|
210
|
+
const success = await runFirstTimeSetup(store);
|
|
211
|
+
if (!success) {
|
|
212
|
+
process.exit(1);
|
|
213
|
+
}
|
|
214
|
+
config = store.getAll();
|
|
215
|
+
}
|
|
216
|
+
// Create provider
|
|
217
|
+
let provider;
|
|
218
|
+
try {
|
|
219
|
+
provider = createProvider(config.provider, config.apiKey, config.model, config.providers[config.provider]?.baseURL);
|
|
220
|
+
}
|
|
221
|
+
catch (err) {
|
|
222
|
+
renderError(`Failed to initialize provider: ${err}`);
|
|
223
|
+
process.exit(1);
|
|
224
|
+
}
|
|
225
|
+
// Register rate limit handler for user-visible feedback
|
|
226
|
+
const { onRateLimitWait } = await import('../providers/rate-limiter.js');
|
|
227
|
+
onRateLimitWait((waitMs, reason) => {
|
|
228
|
+
if (waitMs > 1000) {
|
|
229
|
+
process.stdout.write(`\r\x1b[K ${chalk.yellow('\u23F3')} ${chalk.dim(`Rate limit: waiting ${Math.ceil(waitMs / 1000)}s (${reason})`)}`);
|
|
230
|
+
}
|
|
231
|
+
});
|
|
232
|
+
// Initialize agent tools
|
|
233
|
+
await initializeTools();
|
|
234
|
+
// Analyze project context
|
|
235
|
+
const project = await analyzeProject(process.cwd());
|
|
236
|
+
// Conversation history (for agent loop, we use ToolMessage format)
|
|
237
|
+
const messages = [];
|
|
238
|
+
const agentHistory = [];
|
|
239
|
+
// Permission manager
|
|
240
|
+
const permissions = new PermissionManager();
|
|
241
|
+
if (options.yolo)
|
|
242
|
+
permissions.setYolo(true);
|
|
243
|
+
if (options.skipPermissions)
|
|
244
|
+
permissions.setSkipPermissions(true);
|
|
245
|
+
// Undo stack
|
|
246
|
+
const undoStack = new UndoStack();
|
|
247
|
+
// Checkpoint store
|
|
248
|
+
const checkpointStore = new CheckpointStore();
|
|
249
|
+
// Session buffer (working memory)
|
|
250
|
+
const sessionBuffer = new SessionBuffer();
|
|
251
|
+
// Activity indicator (replaces spinner)
|
|
252
|
+
const activity = new ActivityIndicator();
|
|
253
|
+
// Agent controller for pause/resume
|
|
254
|
+
const agentController = new AgentController();
|
|
255
|
+
let agentRunning = false;
|
|
256
|
+
let autonomousMode = false;
|
|
257
|
+
// Forward-declared findings handler (reassigned by control protocol if active)
|
|
258
|
+
let pushFindingsToBrainFn = null;
|
|
259
|
+
// Session Manager — manages background sessions (security, auto, etc.)
|
|
260
|
+
const sessionMgr = new SessionManager({
|
|
261
|
+
flags: {
|
|
262
|
+
yolo: options.yolo ?? false,
|
|
263
|
+
skipPermissions: options.skipPermissions ?? false,
|
|
264
|
+
},
|
|
265
|
+
onSessionComplete: (session) => {
|
|
266
|
+
// Show notification in the terminal when a background session finishes
|
|
267
|
+
if (session.id !== 'main') {
|
|
268
|
+
process.stdout.write(renderSessionNotification(session));
|
|
269
|
+
// Push findings to brain visualization (if browser is open)
|
|
270
|
+
if (session.result?.text && pushFindingsToBrainFn) {
|
|
271
|
+
pushFindingsToBrainFn(session);
|
|
272
|
+
}
|
|
273
|
+
updateStatusBar();
|
|
274
|
+
// Re-prompt if user is idle — use showPrompt() for full separator+hint+statusbar
|
|
275
|
+
if (!agentRunning) {
|
|
276
|
+
showPrompt();
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
},
|
|
280
|
+
onSessionAutoClose: () => {
|
|
281
|
+
// Tab was auto-removed after timeout — refresh the tab bar
|
|
282
|
+
updateStatusBar();
|
|
283
|
+
},
|
|
284
|
+
});
|
|
285
|
+
// Validation Matrix state
|
|
286
|
+
let validationEnabled = options.validation !== false; // Default ON
|
|
287
|
+
let validationVerbose = options.validationVerbose ?? false;
|
|
288
|
+
let validationStrict = options.validationStrict ?? false;
|
|
289
|
+
// Session metrics
|
|
290
|
+
let sessionTokensInput = 0;
|
|
291
|
+
let sessionTokensOutput = 0;
|
|
292
|
+
let sessionToolCalls = 0;
|
|
293
|
+
let roundToolCalls = 0;
|
|
294
|
+
// Brain scope: project-local if .helixmind/ exists, else global
|
|
295
|
+
// Auto-create .helixmind/ for new projects (opt-in for local brain)
|
|
296
|
+
const { detectBrainScope, resolveDataDir: resolveSpiralDir, loadConfig: loadSpiralConfig } = await import('../../utils/config.js');
|
|
297
|
+
const { mkdirSync, existsSync } = await import('node:fs');
|
|
298
|
+
let brainScope = detectBrainScope(process.cwd());
|
|
299
|
+
// Auto-create .helixmind/ if it doesn't exist (local brain by default for projects)
|
|
300
|
+
const helixDir = join(process.cwd(), '.helixmind');
|
|
301
|
+
if (!existsSync(helixDir)) {
|
|
302
|
+
mkdirSync(helixDir, { recursive: true });
|
|
303
|
+
renderInfo(chalk.dim(' Created .helixmind/ directory for local brain'));
|
|
304
|
+
brainScope = 'project';
|
|
305
|
+
}
|
|
306
|
+
let spiralEngine = null;
|
|
307
|
+
async function initSpiralEngine(scope) {
|
|
308
|
+
try {
|
|
309
|
+
const { SpiralEngine } = await import('../../spiral/engine.js');
|
|
310
|
+
const dataDir = resolveSpiralDir(scope, process.cwd());
|
|
311
|
+
const spiralConfig = loadSpiralConfig(dataDir);
|
|
312
|
+
const engine = new SpiralEngine(spiralConfig);
|
|
313
|
+
await engine.initialize();
|
|
314
|
+
return engine;
|
|
315
|
+
}
|
|
316
|
+
catch {
|
|
317
|
+
return null;
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
if (config.spiral.enabled) {
|
|
321
|
+
spiralEngine = await initSpiralEngine(brainScope);
|
|
322
|
+
}
|
|
323
|
+
// Create session start checkpoint
|
|
324
|
+
checkpointStore.create({
|
|
325
|
+
type: 'session_start',
|
|
326
|
+
label: 'Session started',
|
|
327
|
+
messageIndex: 0,
|
|
328
|
+
});
|
|
329
|
+
// Single message mode
|
|
330
|
+
if (options.message) {
|
|
331
|
+
await sendAgentMessage(options.message, agentHistory, provider, project, spiralEngine, config, permissions, undoStack, checkpointStore, agentController, activity, sessionBuffer, (inp, out) => { sessionTokensInput += inp; sessionTokensOutput += out; }, () => { sessionToolCalls++; }, undefined, { enabled: validationEnabled, verbose: validationVerbose, strict: validationStrict });
|
|
332
|
+
spiralEngine?.close();
|
|
333
|
+
return;
|
|
334
|
+
}
|
|
335
|
+
// Interactive mode
|
|
336
|
+
renderInfo(` Provider: ${config.provider} | Model: ${config.model}`);
|
|
337
|
+
if (project.name !== 'unknown') {
|
|
338
|
+
renderInfo(` Project: ${project.name} (${project.type})`);
|
|
339
|
+
}
|
|
340
|
+
const brainLabel = brainScope === 'project'
|
|
341
|
+
? chalk.cyan('project-local') + chalk.dim(` (.helixmind/)`)
|
|
342
|
+
: chalk.dim('global') + chalk.dim(` (~/.spiral-context/)`);
|
|
343
|
+
renderInfo(` Brain: ${brainLabel}`);
|
|
344
|
+
// Show mode-specific startup info
|
|
345
|
+
const modeLabel = permissions.getModeLabel();
|
|
346
|
+
renderInfo(` Agent mode: ${modeLabel} permissions`);
|
|
347
|
+
// Show warnings for skip-permissions / yolo
|
|
348
|
+
if (options.skipPermissions && options.yolo) {
|
|
349
|
+
showFullAutonomousWarning();
|
|
350
|
+
}
|
|
351
|
+
else if (options.skipPermissions) {
|
|
352
|
+
showSkipPermissionsWarning();
|
|
353
|
+
}
|
|
354
|
+
// === Start Brain Server BEFORE prompt (no async output during typing) ===
|
|
355
|
+
let brainUrl = null;
|
|
356
|
+
if (spiralEngine && config.spiral.enabled) {
|
|
357
|
+
try {
|
|
358
|
+
const { exportBrainData } = await import('../brain/exporter.js');
|
|
359
|
+
const { startLiveBrain } = await import('../brain/generator.js');
|
|
360
|
+
const data = exportBrainData(spiralEngine, project.name || 'HelixMind', brainScope);
|
|
361
|
+
if (data.meta.totalNodes > 0) {
|
|
362
|
+
brainUrl = await startLiveBrain(spiralEngine, project.name || 'HelixMind', brainScope);
|
|
363
|
+
renderInfo(` \u{1F9E0} Brain: ${chalk.dim(brainUrl)}`);
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
catch { /* brain server optional */ }
|
|
367
|
+
}
|
|
368
|
+
// === Register CLI ↔ Web control protocol ===
|
|
369
|
+
if (brainUrl) {
|
|
370
|
+
try {
|
|
371
|
+
const { registerControlHandlers, setInstanceMeta, getBrainToken, pushSessionCreated, pushSessionUpdate, pushSessionRemoved, pushOutputLine, startRelayClient, } = await import('../brain/generator.js');
|
|
372
|
+
const { serializeSession, buildInstanceMeta, resetInstanceStartTime } = await import('../brain/control-protocol.js');
|
|
373
|
+
resetInstanceStartTime();
|
|
374
|
+
// Collected findings for getFindings() handler
|
|
375
|
+
const collectedFindings = [];
|
|
376
|
+
// Set instance metadata for discovery
|
|
377
|
+
const instanceId = (await import('node:crypto')).randomUUID().slice(0, 8);
|
|
378
|
+
const updateMeta = () => {
|
|
379
|
+
const meta = buildInstanceMeta(project.name || 'HelixMind', process.cwd(), config.model, config.provider, '0.1.0', instanceId);
|
|
380
|
+
setInstanceMeta(meta);
|
|
381
|
+
return meta;
|
|
382
|
+
};
|
|
383
|
+
updateMeta();
|
|
384
|
+
// Wire output streaming: when any session captures output, push to control clients
|
|
385
|
+
const wireSessionOutput = (session) => {
|
|
386
|
+
session.onCapture = (line, index) => {
|
|
387
|
+
pushOutputLine(session.id, line, index);
|
|
388
|
+
};
|
|
389
|
+
};
|
|
390
|
+
// Register control handlers
|
|
391
|
+
registerControlHandlers({
|
|
392
|
+
listSessions: () => sessionMgr.all.map(serializeSession),
|
|
393
|
+
startAuto: (goal) => {
|
|
394
|
+
const sessionName = goal ? `\u{1F504} Auto: ${goal.slice(0, 30)}` : '\u{1F504} Auto';
|
|
395
|
+
const bgSession = sessionMgr.create(sessionName, '\u{1F504}', agentHistory);
|
|
396
|
+
bgSession.start();
|
|
397
|
+
wireSessionOutput(bgSession);
|
|
398
|
+
pushSessionCreated(serializeSession(bgSession));
|
|
399
|
+
// Trigger autonomous mode (same as /auto start)
|
|
400
|
+
autonomousMode = true;
|
|
401
|
+
(async () => {
|
|
402
|
+
const completed = [];
|
|
403
|
+
try {
|
|
404
|
+
await runAutonomousLoop({
|
|
405
|
+
sendMessage: async (prompt) => {
|
|
406
|
+
bgSession.controller.reset();
|
|
407
|
+
const resultTextHolder = { text: '' };
|
|
408
|
+
const origAddSummary = bgSession.buffer.addAssistantSummary.bind(bgSession.buffer);
|
|
409
|
+
bgSession.buffer.addAssistantSummary = (t) => {
|
|
410
|
+
resultTextHolder.text = t;
|
|
411
|
+
origAddSummary(t);
|
|
412
|
+
};
|
|
413
|
+
await sendAgentMessage(prompt, bgSession.history, provider, project, spiralEngine, config, permissions, bgSession.undoStack, checkpointStore, bgSession.controller, new ActivityIndicator(), bgSession.buffer, (inp, out) => { sessionTokensInput += inp; sessionTokensOutput += out; }, () => { sessionToolCalls++; }, undefined, { enabled: false, verbose: false, strict: false });
|
|
414
|
+
bgSession.buffer.addAssistantSummary = origAddSummary;
|
|
415
|
+
return resultTextHolder.text;
|
|
416
|
+
},
|
|
417
|
+
isAborted: () => !autonomousMode || bgSession.controller.isAborted,
|
|
418
|
+
onRoundStart: (round) => {
|
|
419
|
+
bgSession.controller.reset();
|
|
420
|
+
bgSession.capture(`Round ${round}...`);
|
|
421
|
+
},
|
|
422
|
+
onRoundEnd: (_round, summary) => {
|
|
423
|
+
completed.push(summary);
|
|
424
|
+
bgSession.capture(`\u2713 ${summary}`);
|
|
425
|
+
pushSessionUpdate(serializeSession(bgSession));
|
|
426
|
+
},
|
|
427
|
+
updateStatus: () => updateStatusBar(),
|
|
428
|
+
}, goal);
|
|
429
|
+
}
|
|
430
|
+
catch (err) {
|
|
431
|
+
if (!(err instanceof AgentAbortError)) {
|
|
432
|
+
bgSession.capture(`Error: ${err}`);
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
autonomousMode = false;
|
|
436
|
+
sessionMgr.complete(bgSession.id, {
|
|
437
|
+
text: completed.join('\n'),
|
|
438
|
+
steps: [],
|
|
439
|
+
errors: bgSession.controller.isAborted ? ['Aborted by user'] : [],
|
|
440
|
+
durationMs: bgSession.elapsed,
|
|
441
|
+
});
|
|
442
|
+
pushSessionUpdate(serializeSession(bgSession));
|
|
443
|
+
})();
|
|
444
|
+
return bgSession.id;
|
|
445
|
+
},
|
|
446
|
+
startSecurity: () => {
|
|
447
|
+
const bgSession = sessionMgr.create('\u{1F512} Security', '\u{1F512}', agentHistory);
|
|
448
|
+
bgSession.start();
|
|
449
|
+
wireSessionOutput(bgSession);
|
|
450
|
+
pushSessionCreated(serializeSession(bgSession));
|
|
451
|
+
runBackgroundSession(bgSession, SECURITY_PROMPT, provider, project, spiralEngine, config, permissions, checkpointStore, (inp, out) => { sessionTokensInput += inp; sessionTokensOutput += out; }, () => { sessionToolCalls++; }, { enabled: validationEnabled, verbose: validationVerbose, strict: validationStrict }).then(result => {
|
|
452
|
+
sessionMgr.complete(bgSession.id, result);
|
|
453
|
+
pushSessionUpdate(serializeSession(bgSession));
|
|
454
|
+
}).catch(err => {
|
|
455
|
+
if (!(err instanceof AgentAbortError)) {
|
|
456
|
+
sessionMgr.complete(bgSession.id, {
|
|
457
|
+
text: '',
|
|
458
|
+
steps: [],
|
|
459
|
+
errors: [err instanceof Error ? err.message : String(err)],
|
|
460
|
+
durationMs: bgSession.elapsed,
|
|
461
|
+
});
|
|
462
|
+
pushSessionUpdate(serializeSession(bgSession));
|
|
463
|
+
}
|
|
464
|
+
});
|
|
465
|
+
return bgSession.id;
|
|
466
|
+
},
|
|
467
|
+
abortSession: (sessionId) => {
|
|
468
|
+
const session = sessionMgr.get(sessionId);
|
|
469
|
+
if (!session)
|
|
470
|
+
return false;
|
|
471
|
+
sessionMgr.abort(sessionId);
|
|
472
|
+
pushSessionUpdate(serializeSession(session));
|
|
473
|
+
return true;
|
|
474
|
+
},
|
|
475
|
+
sendChat: (text) => {
|
|
476
|
+
// Queue chat text to be processed as if the user typed it
|
|
477
|
+
typeAheadBuffer.push(text);
|
|
478
|
+
},
|
|
479
|
+
getFindings: () => [...collectedFindings],
|
|
480
|
+
});
|
|
481
|
+
// Override forward-reference to also collect findings for control protocol
|
|
482
|
+
pushFindingsToBrainFn = (session) => {
|
|
483
|
+
pushFindingsToBrain(session);
|
|
484
|
+
// Also collect findings for the control protocol
|
|
485
|
+
const text = session.result?.text || '';
|
|
486
|
+
const severityPatterns = [
|
|
487
|
+
{ regex: /\*\*CRITICAL\*\*[:\s]*(.+?)(?:\n|$)/gi, severity: 'critical' },
|
|
488
|
+
{ regex: /\*\*HIGH\*\*[:\s]*(.+?)(?:\n|$)/gi, severity: 'high' },
|
|
489
|
+
{ regex: /\*\*MEDIUM\*\*[:\s]*(.+?)(?:\n|$)/gi, severity: 'medium' },
|
|
490
|
+
{ regex: /\*\*LOW\*\*[:\s]*(.+?)(?:\n|$)/gi, severity: 'low' },
|
|
491
|
+
{ regex: /DONE:\s*(.+?)(?:\n|$)/gi, severity: 'info' },
|
|
492
|
+
];
|
|
493
|
+
for (const { regex, severity } of severityPatterns) {
|
|
494
|
+
let match;
|
|
495
|
+
while ((match = regex.exec(text)) !== null) {
|
|
496
|
+
const finding = match[1].trim();
|
|
497
|
+
if (finding.length > 5) {
|
|
498
|
+
const fileMatch = finding.match(/(?:in |file[:\s]+|path[:\s]+)([^\s,]+\.\w+)/i);
|
|
499
|
+
collectedFindings.push({
|
|
500
|
+
sessionName: session.name,
|
|
501
|
+
finding,
|
|
502
|
+
severity,
|
|
503
|
+
file: fileMatch?.[1] || '',
|
|
504
|
+
timestamp: Date.now(),
|
|
505
|
+
});
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
};
|
|
510
|
+
// Log connection token
|
|
511
|
+
const token = getBrainToken();
|
|
512
|
+
if (token) {
|
|
513
|
+
renderInfo(` \u{1F511} Brain token: ${chalk.dim(token.slice(-4))} ${chalk.dim('(full token in /brain)')}`);
|
|
514
|
+
}
|
|
515
|
+
// Start relay client if configured
|
|
516
|
+
const relayUrl = config.relay?.url;
|
|
517
|
+
const relayApiKey = config.relay?.apiKey;
|
|
518
|
+
if (relayUrl && relayApiKey) {
|
|
519
|
+
startRelayClient(relayUrl, relayApiKey, {
|
|
520
|
+
listSessions: () => sessionMgr.all.map(serializeSession),
|
|
521
|
+
startAuto: (goal) => { /* relay delegates to local handlers — already registered */ return ''; },
|
|
522
|
+
startSecurity: () => '',
|
|
523
|
+
abortSession: (id) => { sessionMgr.abort(id); return true; },
|
|
524
|
+
sendChat: (text) => { typeAheadBuffer.push(text); },
|
|
525
|
+
getFindings: () => [...collectedFindings],
|
|
526
|
+
}, updateMeta).catch(() => { });
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
catch { /* control protocol optional */ }
|
|
530
|
+
}
|
|
531
|
+
renderInfo(` Type /help for commands, ESC = stop agent, Ctrl+C twice to exit\n`);
|
|
532
|
+
process.stdout.write(theme.separator + '\n');
|
|
533
|
+
// Flag: true while user is at the prompt (typing). Footer timer skips updates
|
|
534
|
+
// when this is true to prevent cursor-jumping from interfering with readline.
|
|
535
|
+
let isAtPrompt = false;
|
|
536
|
+
// Flag: true while inline progress (\r\x1b[K) is actively writing.
|
|
537
|
+
// Suppresses the footer timer to prevent cursor-jumping flicker.
|
|
538
|
+
let inlineProgressActive = false;
|
|
539
|
+
// Wrap renderFeedProgress to automatically suppress footer timer during feed
|
|
540
|
+
const wrappedFeedProgress = (progress) => {
|
|
541
|
+
inlineProgressActive = progress.stage !== 'done';
|
|
542
|
+
renderFeedProgress(progress);
|
|
543
|
+
};
|
|
544
|
+
// Guard: ignore line events shortly after a sub-menu that used its own readline
|
|
545
|
+
// (e.g. /model → Add provider → askText). The sub-readline can leave phantom
|
|
546
|
+
// line events on stdin that would be misinterpreted as user messages.
|
|
547
|
+
let drainUntil = 0;
|
|
548
|
+
/**
|
|
549
|
+
* Build the readline prompt string. Must be a SINGLE line with ANSI codes
|
|
550
|
+
* wrapped in \x01..\x02 so readline can correctly compute visible width
|
|
551
|
+
* and position the cursor where the user types.
|
|
552
|
+
*/
|
|
553
|
+
function makePrompt() {
|
|
554
|
+
const ansiStart = '\x01'; // RL_PROMPT_START_IGNORE
|
|
555
|
+
const ansiEnd = '\x02'; // RL_PROMPT_END_IGNORE
|
|
556
|
+
// Wrap each ANSI escape sequence so readline ignores it for width calculation
|
|
557
|
+
const gt = chalk.hex('#00d4ff').bold('>');
|
|
558
|
+
const escaped = gt.replace(/(\x1b\[[0-9;]*m)/g, `${ansiStart}$1${ansiEnd}`);
|
|
559
|
+
return `${escaped} `;
|
|
560
|
+
}
|
|
561
|
+
/**
|
|
562
|
+
* Show the full prompt area:
|
|
563
|
+
* ──────────────────
|
|
564
|
+
* ▸▸ safe permissions · esc = stop · /help
|
|
565
|
+
* 🌀 L1:... | tokens | model | git
|
|
566
|
+
* > _ ← cursor here (last line)
|
|
567
|
+
*
|
|
568
|
+
* Info is written ABOVE the prompt as normal scrolling text.
|
|
569
|
+
* The prompt is always the last line — no ANSI cursor tricks needed.
|
|
570
|
+
*/
|
|
571
|
+
function showPrompt() {
|
|
572
|
+
const w = Math.max(20, (process.stdout.columns || 80) - 2);
|
|
573
|
+
const sep = chalk.hex('#00d4ff').dim('\u2500'.repeat(w));
|
|
574
|
+
const data = getStatusBarData();
|
|
575
|
+
const bar = renderStatusBar(data, w);
|
|
576
|
+
// Build hint line
|
|
577
|
+
const hints = [];
|
|
578
|
+
if (data.permissionMode === 'yolo')
|
|
579
|
+
hints.push(chalk.red('\u25B8\u25B8 yolo mode'));
|
|
580
|
+
else if (data.permissionMode === 'skip')
|
|
581
|
+
hints.push(chalk.yellow('\u25B8\u25B8 skip permissions'));
|
|
582
|
+
else
|
|
583
|
+
hints.push(chalk.green('\u25B8\u25B8 safe permissions'));
|
|
584
|
+
hints.push(chalk.dim('esc = stop'));
|
|
585
|
+
hints.push(chalk.dim('/help'));
|
|
586
|
+
const hintLine = hints.join(chalk.dim(' \u00B7 '));
|
|
587
|
+
// Write info above the prompt, then the prompt as the last line
|
|
588
|
+
isAtPrompt = true;
|
|
589
|
+
process.stdout.write(`\n${sep}\n ${hintLine}\n ${bar}\n`);
|
|
590
|
+
rl.prompt();
|
|
591
|
+
}
|
|
592
|
+
/** Build current status bar data object */
|
|
593
|
+
function getStatusBarData() {
|
|
594
|
+
const spiralStatus = spiralEngine ? spiralEngine.status() : null;
|
|
595
|
+
const git = getGitInfo(process.cwd());
|
|
596
|
+
const l6Count = spiralEngine ? spiralEngine.webKnowledgeCount() : 0;
|
|
597
|
+
return {
|
|
598
|
+
spiral: spiralStatus ? {
|
|
599
|
+
l1: spiralStatus.per_level[1] ?? 0,
|
|
600
|
+
l2: spiralStatus.per_level[2] ?? 0,
|
|
601
|
+
l3: spiralStatus.per_level[3] ?? 0,
|
|
602
|
+
l4: spiralStatus.per_level[4] ?? 0,
|
|
603
|
+
l5: spiralStatus.per_level[5] ?? 0,
|
|
604
|
+
l6: l6Count,
|
|
605
|
+
} : { l1: 0, l2: 0, l3: 0, l4: 0, l5: 0, l6: 0 },
|
|
606
|
+
sessionTokens: sessionTokensInput + sessionTokensOutput,
|
|
607
|
+
tokens: {
|
|
608
|
+
thisMessage: sessionTokensOutput,
|
|
609
|
+
thisSession: sessionTokensInput + sessionTokensOutput,
|
|
610
|
+
},
|
|
611
|
+
tools: { callsThisRound: roundToolCalls },
|
|
612
|
+
model: config.model,
|
|
613
|
+
git,
|
|
614
|
+
checkpoints: checkpointStore.count,
|
|
615
|
+
permissionMode: permissions.getModeLabel(),
|
|
616
|
+
autonomous: autonomousMode,
|
|
617
|
+
paused: agentController.isPaused,
|
|
618
|
+
plan: store.get('relay.plan') ?? undefined,
|
|
619
|
+
};
|
|
620
|
+
}
|
|
621
|
+
const rl = readline.createInterface({
|
|
622
|
+
input: process.stdin,
|
|
623
|
+
output: process.stdout,
|
|
624
|
+
prompt: makePrompt(),
|
|
625
|
+
completer: (line) => {
|
|
626
|
+
if (line.startsWith('/')) {
|
|
627
|
+
const matches = getSuggestions(line).map(s => s.cmd);
|
|
628
|
+
return [matches.length > 0 ? matches : [], line];
|
|
629
|
+
}
|
|
630
|
+
return [[], line];
|
|
631
|
+
},
|
|
632
|
+
});
|
|
633
|
+
// Update prompt and activity scroll region on terminal resize
|
|
634
|
+
process.stdout.on('resize', () => {
|
|
635
|
+
rl.setPrompt(makePrompt());
|
|
636
|
+
activity.handleResize();
|
|
637
|
+
});
|
|
638
|
+
// Track suggestion overlay state
|
|
639
|
+
let lastSuggestionCount = 0;
|
|
640
|
+
permissions.setReadline(rl);
|
|
641
|
+
permissions.setPromptCallback((active) => { isAtPrompt = active; });
|
|
642
|
+
// Activity indicator renders on the bottom terminal row (absolute positioned,
|
|
643
|
+
// same row as statusbar). The footer timer already skips statusbar draws when
|
|
644
|
+
// activity.isAnimating is true, so there's no conflict.
|
|
645
|
+
// Ctrl+C behavior:
|
|
646
|
+
// - If there's text on the line → clear the line (like a normal terminal)
|
|
647
|
+
// - If line is empty → count towards exit (double Ctrl+C = exit)
|
|
648
|
+
let ctrlCCount = 0;
|
|
649
|
+
let ctrlCTimer = null;
|
|
650
|
+
process.on('SIGINT', () => {
|
|
651
|
+
// If agent is running, treat Ctrl+C as interrupt
|
|
652
|
+
if (agentRunning) {
|
|
653
|
+
activity.stop('Stopped');
|
|
654
|
+
agentController.abort();
|
|
655
|
+
autonomousMode = false;
|
|
656
|
+
process.stdout.write('\n');
|
|
657
|
+
renderInfo('\u23F9 Agent interrupted.');
|
|
658
|
+
return;
|
|
659
|
+
}
|
|
660
|
+
// Check if readline has text on the current line
|
|
661
|
+
const currentLine = rl.line || '';
|
|
662
|
+
if (currentLine.length > 0) {
|
|
663
|
+
// Clear current input — write a new line and re-prompt
|
|
664
|
+
process.stdout.write('\n');
|
|
665
|
+
rl.line = '';
|
|
666
|
+
rl.cursor = 0;
|
|
667
|
+
isAtPrompt = true;
|
|
668
|
+
rl.prompt();
|
|
669
|
+
ctrlCCount = 0;
|
|
670
|
+
return;
|
|
671
|
+
}
|
|
672
|
+
// Empty line — count towards exit
|
|
673
|
+
ctrlCCount++;
|
|
674
|
+
if (ctrlCCount >= 2) {
|
|
675
|
+
process.stdout.write('\n');
|
|
676
|
+
renderInfo('Force exit \u2014 saving state...');
|
|
677
|
+
if (spiralEngine) {
|
|
678
|
+
try {
|
|
679
|
+
spiralEngine.saveState(messages).catch(() => { });
|
|
680
|
+
}
|
|
681
|
+
catch { /* best effort */ }
|
|
682
|
+
spiralEngine.close();
|
|
683
|
+
}
|
|
684
|
+
// Stop brain server
|
|
685
|
+
import('../brain/generator.js').then(m => m.stopLiveBrain()).catch(() => { });
|
|
686
|
+
process.exit(0);
|
|
687
|
+
}
|
|
688
|
+
process.stdout.write('\n');
|
|
689
|
+
renderInfo('Press Ctrl+C again to exit, or type a message to continue.');
|
|
690
|
+
isAtPrompt = true;
|
|
691
|
+
rl.prompt();
|
|
692
|
+
if (ctrlCTimer)
|
|
693
|
+
clearTimeout(ctrlCTimer);
|
|
694
|
+
ctrlCTimer = setTimeout(() => { ctrlCCount = 0; }, 2000);
|
|
695
|
+
});
|
|
696
|
+
/** Register brain event handlers (voice, scope switch) — reusable for auto-start and /brain */
|
|
697
|
+
async function registerBrainHandlers() {
|
|
698
|
+
const { onBrainVoiceInput, onBrainScopeSwitch, pushScopeChange, onBrainModelActivate, pushModelActivated } = await import('../brain/generator.js');
|
|
699
|
+
onBrainVoiceInput((text) => {
|
|
700
|
+
process.stdout.write('\r\x1b[K');
|
|
701
|
+
process.stdout.write(` ${chalk.dim('\u{1F3A4} Voice:')} ${chalk.cyan(text)}\n`);
|
|
702
|
+
rl.write(null, { ctrl: true, name: 'u' });
|
|
703
|
+
rl.write(text);
|
|
704
|
+
});
|
|
705
|
+
onBrainModelActivate(async (modelName) => {
|
|
706
|
+
try {
|
|
707
|
+
// Ensure Ollama is registered as a provider
|
|
708
|
+
store.addProvider('ollama', 'ollama', 'http://localhost:11434/v1');
|
|
709
|
+
store.switchProvider('ollama', modelName);
|
|
710
|
+
config = store.getAll();
|
|
711
|
+
provider = createProvider('ollama', 'ollama', modelName, 'http://localhost:11434/v1');
|
|
712
|
+
pushModelActivated(modelName);
|
|
713
|
+
process.stdout.write(`\n ${chalk.green('\u26A1')} Model activated: ${chalk.cyan(modelName)} ${chalk.dim('(ollama)')}\n`);
|
|
714
|
+
isAtPrompt = true;
|
|
715
|
+
rl.prompt();
|
|
716
|
+
}
|
|
717
|
+
catch (err) {
|
|
718
|
+
process.stdout.write(`\n ${chalk.red('Model activation failed:')} ${err}\n`);
|
|
719
|
+
isAtPrompt = true;
|
|
720
|
+
rl.prompt();
|
|
721
|
+
}
|
|
722
|
+
});
|
|
723
|
+
onBrainScopeSwitch(async (newScope) => {
|
|
724
|
+
if (newScope === brainScope)
|
|
725
|
+
return;
|
|
726
|
+
try {
|
|
727
|
+
if (spiralEngine) {
|
|
728
|
+
try {
|
|
729
|
+
spiralEngine.close();
|
|
730
|
+
}
|
|
731
|
+
catch { /* best effort */ }
|
|
732
|
+
}
|
|
733
|
+
brainScope = newScope;
|
|
734
|
+
if (newScope === 'project') {
|
|
735
|
+
const { mkdirSync, existsSync } = await import('node:fs');
|
|
736
|
+
const { join } = await import('node:path');
|
|
737
|
+
const helixDir = join(process.cwd(), '.helixmind');
|
|
738
|
+
if (!existsSync(helixDir))
|
|
739
|
+
mkdirSync(helixDir, { recursive: true });
|
|
740
|
+
}
|
|
741
|
+
spiralEngine = await initSpiralEngine(newScope);
|
|
742
|
+
const { exportBrainData } = await import('../brain/exporter.js');
|
|
743
|
+
const { startLiveBrain } = await import('../brain/generator.js');
|
|
744
|
+
await startLiveBrain(spiralEngine, project.name || 'HelixMind', newScope);
|
|
745
|
+
pushScopeChange(newScope);
|
|
746
|
+
const scopeLabel = newScope === 'project'
|
|
747
|
+
? chalk.cyan('\u{1F4C1} project-local (.helixmind/)')
|
|
748
|
+
: chalk.dim('\u{1F310} global (~/.spiral-context/)');
|
|
749
|
+
process.stdout.write(`\n \u{1F9E0} Brain switched to ${scopeLabel}\n`);
|
|
750
|
+
isAtPrompt = true;
|
|
751
|
+
rl.prompt();
|
|
752
|
+
}
|
|
753
|
+
catch (err) {
|
|
754
|
+
process.stdout.write(`\n ${chalk.red('Brain switch failed:')} ${err}\n`);
|
|
755
|
+
isAtPrompt = true;
|
|
756
|
+
rl.prompt();
|
|
757
|
+
}
|
|
758
|
+
});
|
|
759
|
+
}
|
|
760
|
+
// Register brain handlers if server was started during startup
|
|
761
|
+
if (brainUrl) {
|
|
762
|
+
registerBrainHandlers().catch(() => { });
|
|
763
|
+
}
|
|
764
|
+
// Keybinding state for double-ESC detection
|
|
765
|
+
const keyState = createKeybindingState();
|
|
766
|
+
// Handle raw keypresses for double-ESC + command suggestions
|
|
767
|
+
if (process.stdin.isTTY) {
|
|
768
|
+
process.stdin.on('keypress', async (_str, key) => {
|
|
769
|
+
if (!key)
|
|
770
|
+
return;
|
|
771
|
+
// === Tab switching: Ctrl+PageUp / Ctrl+PageDown ===
|
|
772
|
+
if (key.ctrl && key.name === 'pageup') {
|
|
773
|
+
sessionMgr.switchPrev();
|
|
774
|
+
writeTabBar();
|
|
775
|
+
return;
|
|
776
|
+
}
|
|
777
|
+
if (key.ctrl && key.name === 'pagedown') {
|
|
778
|
+
sessionMgr.switchNext();
|
|
779
|
+
writeTabBar();
|
|
780
|
+
return;
|
|
781
|
+
}
|
|
782
|
+
// === Command Suggestions ===
|
|
783
|
+
if (!agentRunning) {
|
|
784
|
+
const currentLine = (rl.line || '');
|
|
785
|
+
if (currentLine.startsWith('/') && currentLine.length >= 2) {
|
|
786
|
+
const suggestions = getSuggestions(currentLine);
|
|
787
|
+
// Clear old suggestions
|
|
788
|
+
if (lastSuggestionCount > 0)
|
|
789
|
+
clearSuggestions(lastSuggestionCount);
|
|
790
|
+
// Show new ones
|
|
791
|
+
if (suggestions.length > 0)
|
|
792
|
+
writeSuggestions(suggestions);
|
|
793
|
+
lastSuggestionCount = suggestions.length;
|
|
794
|
+
}
|
|
795
|
+
else {
|
|
796
|
+
// Clear suggestions when not typing a command
|
|
797
|
+
if (lastSuggestionCount > 0) {
|
|
798
|
+
clearSuggestions(lastSuggestionCount);
|
|
799
|
+
lastSuggestionCount = 0;
|
|
800
|
+
}
|
|
801
|
+
}
|
|
802
|
+
}
|
|
803
|
+
// === ESC detection ===
|
|
804
|
+
// Single ESC stops immediately when agent is running
|
|
805
|
+
// Double ESC works as fallback anytime
|
|
806
|
+
if (key.name === 'escape') {
|
|
807
|
+
if (agentRunning || sessionMgr.hasBackgroundTasks || autonomousMode) {
|
|
808
|
+
// Clear any suggestions
|
|
809
|
+
if (lastSuggestionCount > 0) {
|
|
810
|
+
clearSuggestions(lastSuggestionCount);
|
|
811
|
+
lastSuggestionCount = 0;
|
|
812
|
+
}
|
|
813
|
+
// IMMEDIATE STOP — single ESC press
|
|
814
|
+
activity.stop('Stopped');
|
|
815
|
+
agentController.abort();
|
|
816
|
+
sessionMgr.abortAll();
|
|
817
|
+
autonomousMode = false;
|
|
818
|
+
// Clear type-ahead buffer to prevent agent restarting after abort
|
|
819
|
+
typeAheadBuffer.length = 0;
|
|
820
|
+
// Reset agent state immediately (don't wait for async propagation)
|
|
821
|
+
agentRunning = false;
|
|
822
|
+
process.stdout.write('\n');
|
|
823
|
+
renderInfo(chalk.red('\u23F9 STOPPED') + chalk.dim(' \u2014 All agents interrupted.'));
|
|
824
|
+
// Restore prompt so user can type again
|
|
825
|
+
showPrompt();
|
|
826
|
+
return;
|
|
827
|
+
}
|
|
828
|
+
}
|
|
829
|
+
// Double-ESC detection (for checkpoint browser when nothing is running)
|
|
830
|
+
const result = processKeypress(key, keyState);
|
|
831
|
+
if (result.action === 'open_browser' && !agentRunning) {
|
|
832
|
+
// Open checkpoint browser
|
|
833
|
+
rl.pause();
|
|
834
|
+
try {
|
|
835
|
+
const browserResult = await runCheckpointBrowser({
|
|
836
|
+
store: checkpointStore,
|
|
837
|
+
agentHistory,
|
|
838
|
+
simpleMessages: messages.map(m => ({ role: m.role, content: typeof m.content === 'string' ? m.content : '' })),
|
|
839
|
+
isPaused: false,
|
|
840
|
+
});
|
|
841
|
+
if (browserResult.action === 'revert') {
|
|
842
|
+
const r = browserResult.result;
|
|
843
|
+
process.stdout.write('\n');
|
|
844
|
+
if (r.messagesRemoved > 0)
|
|
845
|
+
renderInfo(chalk.yellow(`${r.messagesRemoved} message(s) reverted`));
|
|
846
|
+
if (r.filesReverted > 0)
|
|
847
|
+
renderInfo(chalk.yellow(`${r.filesReverted} file(s) reverted`));
|
|
848
|
+
}
|
|
849
|
+
}
|
|
850
|
+
catch {
|
|
851
|
+
// Browser closed unexpectedly
|
|
852
|
+
}
|
|
853
|
+
rl.resume();
|
|
854
|
+
showPrompt();
|
|
855
|
+
}
|
|
856
|
+
});
|
|
857
|
+
}
|
|
858
|
+
// Update statusbar — uses save/restore cursor (DECSC/DECRC).
|
|
859
|
+
// Only called during agent work to update token counts etc.
|
|
860
|
+
function updateStatusBar() {
|
|
861
|
+
if (!process.stdout.isTTY)
|
|
862
|
+
return;
|
|
863
|
+
const data = getStatusBarData();
|
|
864
|
+
writeStatusBar(data);
|
|
865
|
+
// Draw tab bar if there are background sessions, otherwise clear stale tab bar
|
|
866
|
+
if (sessionMgr.all.length > 1) {
|
|
867
|
+
writeTabBar();
|
|
868
|
+
}
|
|
869
|
+
else {
|
|
870
|
+
// Clear the tab bar row when no background sessions remain
|
|
871
|
+
clearTabBarRow();
|
|
872
|
+
}
|
|
873
|
+
}
|
|
874
|
+
/** Clear the tab bar row (row N-1) to remove stale tab bar text */
|
|
875
|
+
function clearTabBarRow() {
|
|
876
|
+
if (!process.stdout.isTTY)
|
|
877
|
+
return;
|
|
878
|
+
const termHeight = process.stdout.rows || 24;
|
|
879
|
+
process.stdout.write(`\x1b7` + // Save cursor
|
|
880
|
+
`\x1b[${termHeight - 1};0H` + // Move to tab bar row
|
|
881
|
+
`\x1b[2K` + // Clear line
|
|
882
|
+
`\x1b8`);
|
|
883
|
+
}
|
|
884
|
+
/** Draw the session tab bar above the statusbar */
|
|
885
|
+
function writeTabBar() {
|
|
886
|
+
if (!process.stdout.isTTY)
|
|
887
|
+
return;
|
|
888
|
+
if (sessionMgr.all.length <= 1)
|
|
889
|
+
return;
|
|
890
|
+
const tabBar = sessionMgr.renderTabs();
|
|
891
|
+
const termHeight = process.stdout.rows || 24;
|
|
892
|
+
const termWidth = (process.stdout.columns || 80) - 2;
|
|
893
|
+
// Truncate tab bar to terminal width to prevent overflow into other rows
|
|
894
|
+
const safeTabBar = truncateBar(tabBar, termWidth);
|
|
895
|
+
// Write tab bar above the statusbar (termHeight - 1)
|
|
896
|
+
// Layout: ..., tabbar(N-1), statusbar(N)
|
|
897
|
+
process.stdout.write(`\x1b7` + // Save cursor
|
|
898
|
+
`\x1b[${termHeight - 1};0H` + // Move to row above statusbar
|
|
899
|
+
`\x1b[2K` + // Clear line
|
|
900
|
+
` ${safeTabBar}` + // Tab bar (truncated to fit)
|
|
901
|
+
`\x1b8`);
|
|
902
|
+
}
|
|
903
|
+
/** Push session findings to brain visualization */
|
|
904
|
+
function pushFindingsToBrain(session) {
|
|
905
|
+
import('../brain/generator.js').then(mod => {
|
|
906
|
+
if (!mod.isBrainServerRunning())
|
|
907
|
+
return;
|
|
908
|
+
const text = session.result?.text || '';
|
|
909
|
+
const sessionName = session.name;
|
|
910
|
+
// Parse findings from security/auto output — look for severity markers
|
|
911
|
+
const severityPatterns = [
|
|
912
|
+
{ regex: /\*\*CRITICAL\*\*[:\s]*(.+?)(?:\n|$)/gi, severity: 'critical' },
|
|
913
|
+
{ regex: /\*\*HIGH\*\*[:\s]*(.+?)(?:\n|$)/gi, severity: 'high' },
|
|
914
|
+
{ regex: /\*\*MEDIUM\*\*[:\s]*(.+?)(?:\n|$)/gi, severity: 'medium' },
|
|
915
|
+
{ regex: /\*\*LOW\*\*[:\s]*(.+?)(?:\n|$)/gi, severity: 'low' },
|
|
916
|
+
{ regex: /DONE:\s*(.+?)(?:\n|$)/gi, severity: 'info' },
|
|
917
|
+
];
|
|
918
|
+
for (const { regex, severity } of severityPatterns) {
|
|
919
|
+
let match;
|
|
920
|
+
while ((match = regex.exec(text)) !== null) {
|
|
921
|
+
const finding = match[1].trim();
|
|
922
|
+
if (finding.length > 5) {
|
|
923
|
+
// Try to extract file path from the finding text
|
|
924
|
+
const fileMatch = finding.match(/(?:in |file[:\s]+|path[:\s]+)([^\s,]+\.\w+)/i);
|
|
925
|
+
mod.pushAgentFinding(sessionName, finding, severity, fileMatch?.[1]);
|
|
926
|
+
}
|
|
927
|
+
}
|
|
928
|
+
}
|
|
929
|
+
// If no structured findings found but text exists, push a summary
|
|
930
|
+
if (text.length > 20) {
|
|
931
|
+
const lines = text.split('\n').filter(l => l.trim());
|
|
932
|
+
if (lines.length > 0) {
|
|
933
|
+
const summary = lines[0].slice(0, 120);
|
|
934
|
+
mod.pushAgentFinding(sessionName, summary, 'info');
|
|
935
|
+
}
|
|
936
|
+
}
|
|
937
|
+
}).catch(() => { });
|
|
938
|
+
}
|
|
939
|
+
// Set forward-reference for session completion callback
|
|
940
|
+
pushFindingsToBrainFn = pushFindingsToBrain;
|
|
941
|
+
// Type-ahead buffer: stores user input submitted while agent is running
|
|
942
|
+
const typeAheadBuffer = [];
|
|
943
|
+
// Paste detection — collects rapid-fire line events into a buffer
|
|
944
|
+
let pasteBuffer = [];
|
|
945
|
+
let pasteTimer = null;
|
|
946
|
+
const PASTE_THRESHOLD_MS = 50; // Lines arriving faster than this = paste
|
|
947
|
+
// Show full prompt area on startup (separator + status + > prompt)
|
|
948
|
+
showPrompt();
|
|
949
|
+
// Footer timer — redraws status bar during agent work (absolute positioning).
|
|
950
|
+
// Skipped when:
|
|
951
|
+
// - user is at readline prompt (isAtPrompt) — prevents cursor-jumping
|
|
952
|
+
// - activity indicator is animating — prevents flicker collision
|
|
953
|
+
// - inline progress active (inlineProgressActive) — prevents flicker over feed progress
|
|
954
|
+
const footerTimer = setInterval(() => {
|
|
955
|
+
if (process.stdout.isTTY && !isAtPrompt && !activity.isAnimating && !inlineProgressActive)
|
|
956
|
+
updateStatusBar();
|
|
957
|
+
}, 500);
|
|
958
|
+
footerTimer.unref();
|
|
959
|
+
/** Process a complete input (single line or assembled paste block) */
|
|
960
|
+
async function processInput(input) {
|
|
961
|
+
// Handle /feed directly here (needs access to inlineProgressActive flag)
|
|
962
|
+
if (input.startsWith('/feed')) {
|
|
963
|
+
if (spiralEngine) {
|
|
964
|
+
const feedPath = input.split(/\s+/)[1];
|
|
965
|
+
const rootDir = process.cwd();
|
|
966
|
+
renderInfo('\u{1F300} Feeding project...\n');
|
|
967
|
+
try {
|
|
968
|
+
const result = await runFeedPipeline(rootDir, spiralEngine, {
|
|
969
|
+
targetPath: feedPath,
|
|
970
|
+
onProgress: wrappedFeedProgress,
|
|
971
|
+
});
|
|
972
|
+
renderFeedSummary(result);
|
|
973
|
+
checkpointStore.create({
|
|
974
|
+
type: 'feed',
|
|
975
|
+
label: `Feed ${feedPath || './'}`,
|
|
976
|
+
messageIndex: agentHistory.length,
|
|
977
|
+
});
|
|
978
|
+
}
|
|
979
|
+
catch (err) {
|
|
980
|
+
inlineProgressActive = false;
|
|
981
|
+
renderError(`Feed failed: ${err}`);
|
|
982
|
+
}
|
|
983
|
+
}
|
|
984
|
+
else {
|
|
985
|
+
renderInfo('Spiral engine not available.');
|
|
986
|
+
}
|
|
987
|
+
showPrompt();
|
|
988
|
+
return;
|
|
989
|
+
}
|
|
990
|
+
// Handle slash commands
|
|
991
|
+
if (input.startsWith('/')) {
|
|
992
|
+
const handled = await handleSlashCommand(input, messages, agentHistory, config, spiralEngine, store, rl, permissions, undoStack, checkpointStore, sessionBuffer, { input: sessionTokensInput, output: sessionTokensOutput }, sessionToolCalls, (newProvider) => { provider = newProvider; config = store.getAll(); }, async (newScope) => {
|
|
993
|
+
// Switch brain scope
|
|
994
|
+
if (spiralEngine) {
|
|
995
|
+
try {
|
|
996
|
+
spiralEngine.close();
|
|
997
|
+
}
|
|
998
|
+
catch { /* best effort */ }
|
|
999
|
+
}
|
|
1000
|
+
brainScope = newScope;
|
|
1001
|
+
// Create .helixmind/ dir if switching to project and it doesn't exist
|
|
1002
|
+
if (newScope === 'project') {
|
|
1003
|
+
const { mkdirSync, existsSync } = await import('node:fs');
|
|
1004
|
+
const projDir = join(process.cwd(), '.helixmind');
|
|
1005
|
+
if (!existsSync(projDir)) {
|
|
1006
|
+
mkdirSync(projDir, { recursive: true });
|
|
1007
|
+
renderInfo(chalk.dim(' Created .helixmind/ directory'));
|
|
1008
|
+
}
|
|
1009
|
+
}
|
|
1010
|
+
spiralEngine = await initSpiralEngine(newScope);
|
|
1011
|
+
}, brainScope, async (action, goal) => {
|
|
1012
|
+
if (action === 'stop') {
|
|
1013
|
+
// Stop all background sessions + autonomous mode
|
|
1014
|
+
const running = sessionMgr.running;
|
|
1015
|
+
if (running.length > 0) {
|
|
1016
|
+
for (const s of running) {
|
|
1017
|
+
s.abort();
|
|
1018
|
+
renderInfo(`\u23F9 Stopped: ${s.icon} ${s.name}`);
|
|
1019
|
+
}
|
|
1020
|
+
autonomousMode = false;
|
|
1021
|
+
updateStatusBar();
|
|
1022
|
+
}
|
|
1023
|
+
else if (autonomousMode) {
|
|
1024
|
+
autonomousMode = false;
|
|
1025
|
+
agentController.abort();
|
|
1026
|
+
renderInfo('\u23F9 Stopping autonomous mode...');
|
|
1027
|
+
}
|
|
1028
|
+
else {
|
|
1029
|
+
renderInfo('No background sessions running.');
|
|
1030
|
+
}
|
|
1031
|
+
return;
|
|
1032
|
+
}
|
|
1033
|
+
if (action === 'security') {
|
|
1034
|
+
// Security audit — runs as BACKGROUND SESSION
|
|
1035
|
+
const bgSession = sessionMgr.create('\u{1F512} Security', '\u{1F512}', agentHistory);
|
|
1036
|
+
bgSession.start();
|
|
1037
|
+
renderInfo(`${chalk.hex('#00d4ff')('\u{1F512}')} Security audit started ${chalk.dim(`[session ${bgSession.id}]`)}`);
|
|
1038
|
+
updateStatusBar();
|
|
1039
|
+
// Run in background — don't await, user gets prompt back immediately
|
|
1040
|
+
runBackgroundSession(bgSession, SECURITY_PROMPT, provider, project, spiralEngine, config, permissions, checkpointStore, (inp, out) => { sessionTokensInput += inp; sessionTokensOutput += out; }, () => { sessionToolCalls++; }, { enabled: validationEnabled, verbose: validationVerbose, strict: validationStrict }).then(result => {
|
|
1041
|
+
sessionMgr.complete(bgSession.id, result);
|
|
1042
|
+
}).catch(err => {
|
|
1043
|
+
if (!(err instanceof AgentAbortError)) {
|
|
1044
|
+
sessionMgr.complete(bgSession.id, {
|
|
1045
|
+
text: '',
|
|
1046
|
+
steps: [],
|
|
1047
|
+
errors: [err instanceof Error ? err.message : String(err)],
|
|
1048
|
+
durationMs: bgSession.elapsed,
|
|
1049
|
+
});
|
|
1050
|
+
}
|
|
1051
|
+
});
|
|
1052
|
+
return;
|
|
1053
|
+
}
|
|
1054
|
+
if (action === 'start') {
|
|
1055
|
+
// Check if autonomous is already running
|
|
1056
|
+
const existingAuto = sessionMgr.background.find(s => s.name.includes('Auto') && s.status === 'running');
|
|
1057
|
+
if (existingAuto) {
|
|
1058
|
+
renderInfo('Autonomous mode already running.');
|
|
1059
|
+
return;
|
|
1060
|
+
}
|
|
1061
|
+
// Enter autonomous mode as BACKGROUND SESSION
|
|
1062
|
+
autonomousMode = true;
|
|
1063
|
+
const sessionName = goal ? `\u{1F504} Auto: ${goal.slice(0, 30)}` : '\u{1F504} Auto';
|
|
1064
|
+
const bgSession = sessionMgr.create(sessionName, '\u{1F504}', agentHistory);
|
|
1065
|
+
bgSession.start();
|
|
1066
|
+
const goalHint = goal ? ` \u2014 ${chalk.white(goal.length > 50 ? goal.slice(0, 47) + '...' : goal)}` : '';
|
|
1067
|
+
renderInfo(`${chalk.hex('#ff6600')('\u{1F504}')} Autonomous mode started${goalHint} ${chalk.dim(`[session ${bgSession.id}]`)}`);
|
|
1068
|
+
updateStatusBar();
|
|
1069
|
+
// Run in background — user keeps their prompt
|
|
1070
|
+
(async () => {
|
|
1071
|
+
const completed = [];
|
|
1072
|
+
try {
|
|
1073
|
+
await runAutonomousLoop({
|
|
1074
|
+
sendMessage: async (prompt) => {
|
|
1075
|
+
bgSession.controller.reset();
|
|
1076
|
+
const resultTextHolder = { text: '' };
|
|
1077
|
+
const origAddSummary = bgSession.buffer.addAssistantSummary.bind(bgSession.buffer);
|
|
1078
|
+
bgSession.buffer.addAssistantSummary = (t) => {
|
|
1079
|
+
resultTextHolder.text = t;
|
|
1080
|
+
origAddSummary(t);
|
|
1081
|
+
};
|
|
1082
|
+
await sendAgentMessage(prompt, bgSession.history, provider, project, spiralEngine, config, permissions, bgSession.undoStack, checkpointStore, bgSession.controller, new ActivityIndicator(), bgSession.buffer, (inp, out) => { sessionTokensInput += inp; sessionTokensOutput += out; }, () => { sessionToolCalls++; }, undefined, { enabled: false, verbose: false, strict: false });
|
|
1083
|
+
bgSession.buffer.addAssistantSummary = origAddSummary;
|
|
1084
|
+
return resultTextHolder.text;
|
|
1085
|
+
},
|
|
1086
|
+
isAborted: () => !autonomousMode || bgSession.controller.isAborted,
|
|
1087
|
+
onRoundStart: (round) => {
|
|
1088
|
+
bgSession.controller.reset();
|
|
1089
|
+
bgSession.capture(`Round ${round}...`);
|
|
1090
|
+
},
|
|
1091
|
+
onRoundEnd: (_round, summary) => {
|
|
1092
|
+
completed.push(summary);
|
|
1093
|
+
bgSession.capture(`\u2713 ${summary}`);
|
|
1094
|
+
updateStatusBar();
|
|
1095
|
+
},
|
|
1096
|
+
updateStatus: () => updateStatusBar(),
|
|
1097
|
+
}, goal);
|
|
1098
|
+
}
|
|
1099
|
+
catch (err) {
|
|
1100
|
+
if (!(err instanceof AgentAbortError)) {
|
|
1101
|
+
bgSession.capture(`Error: ${err}`);
|
|
1102
|
+
}
|
|
1103
|
+
}
|
|
1104
|
+
autonomousMode = false;
|
|
1105
|
+
sessionMgr.complete(bgSession.id, {
|
|
1106
|
+
text: completed.join('\n'),
|
|
1107
|
+
steps: [],
|
|
1108
|
+
errors: bgSession.controller.isAborted ? ['Aborted by user'] : [],
|
|
1109
|
+
durationMs: bgSession.elapsed,
|
|
1110
|
+
});
|
|
1111
|
+
updateStatusBar();
|
|
1112
|
+
})();
|
|
1113
|
+
}
|
|
1114
|
+
}, (action) => {
|
|
1115
|
+
// /validation handler
|
|
1116
|
+
switch (action) {
|
|
1117
|
+
case 'on':
|
|
1118
|
+
validationEnabled = true;
|
|
1119
|
+
renderInfo('Validation Matrix: ON');
|
|
1120
|
+
break;
|
|
1121
|
+
case 'off':
|
|
1122
|
+
validationEnabled = false;
|
|
1123
|
+
renderInfo('Validation Matrix: OFF');
|
|
1124
|
+
break;
|
|
1125
|
+
case 'verbose':
|
|
1126
|
+
validationVerbose = !validationVerbose;
|
|
1127
|
+
renderInfo(`Validation verbose: ${validationVerbose ? 'ON' : 'OFF'}`);
|
|
1128
|
+
break;
|
|
1129
|
+
case 'strict':
|
|
1130
|
+
validationStrict = !validationStrict;
|
|
1131
|
+
renderInfo(`Validation strict: ${validationStrict ? 'ON' : 'OFF'}`);
|
|
1132
|
+
break;
|
|
1133
|
+
case 'stats':
|
|
1134
|
+
getValidationStats(spiralEngine).then(stats => {
|
|
1135
|
+
if (stats) {
|
|
1136
|
+
process.stdout.write(renderValidationStats(stats));
|
|
1137
|
+
}
|
|
1138
|
+
else {
|
|
1139
|
+
renderInfo('No validation statistics yet.');
|
|
1140
|
+
}
|
|
1141
|
+
}).catch(() => renderInfo('Could not load stats.'));
|
|
1142
|
+
break;
|
|
1143
|
+
default:
|
|
1144
|
+
renderInfo(`Validation Matrix: ${validationEnabled ? 'ON' : 'OFF'} | Verbose: ${validationVerbose ? 'ON' : 'OFF'} | Strict: ${validationStrict ? 'ON' : 'OFF'}`);
|
|
1145
|
+
}
|
|
1146
|
+
}, sessionMgr, registerBrainHandlers, (active) => { isAtPrompt = active; });
|
|
1147
|
+
if (handled === 'exit') {
|
|
1148
|
+
spiralEngine?.close();
|
|
1149
|
+
rl.close();
|
|
1150
|
+
process.exit(0);
|
|
1151
|
+
}
|
|
1152
|
+
if (handled === 'drain') {
|
|
1153
|
+
// Sub-menu used its own readline — ignore line events for 500ms
|
|
1154
|
+
drainUntil = Date.now() + 500;
|
|
1155
|
+
}
|
|
1156
|
+
showPrompt();
|
|
1157
|
+
return;
|
|
1158
|
+
}
|
|
1159
|
+
// Render user message explicitly so it persists in the chat scroll history.
|
|
1160
|
+
// Readline's prompt echo can be overwritten by activity indicator / agent output.
|
|
1161
|
+
renderUserMessage(input);
|
|
1162
|
+
// Track user message in session buffer
|
|
1163
|
+
sessionBuffer.addUserMessage(input);
|
|
1164
|
+
// Create checkpoint for user message
|
|
1165
|
+
checkpointStore.createForChat(input, agentHistory.length);
|
|
1166
|
+
// === TYPE-AHEAD SUPPORT ===
|
|
1167
|
+
// Don't pause readline — let user type next prompt while agent works.
|
|
1168
|
+
// Buffer submitted lines during agent execution for processing after.
|
|
1169
|
+
// Show hint so user knows they can still type.
|
|
1170
|
+
process.stdout.write(chalk.dim(' \u{1F4AC} Type-ahead active \u2014 input queued for after agent finishes\n'));
|
|
1171
|
+
// Send message through agent loop
|
|
1172
|
+
roundToolCalls = 0;
|
|
1173
|
+
agentRunning = true;
|
|
1174
|
+
agentController.reset();
|
|
1175
|
+
updateStatusBar();
|
|
1176
|
+
await sendAgentMessage(input, agentHistory, provider, project, spiralEngine, config, permissions, undoStack, checkpointStore, agentController, activity, sessionBuffer, (inp, out) => {
|
|
1177
|
+
sessionTokensInput += inp;
|
|
1178
|
+
sessionTokensOutput += out;
|
|
1179
|
+
}, () => {
|
|
1180
|
+
sessionToolCalls++;
|
|
1181
|
+
roundToolCalls++;
|
|
1182
|
+
}, () => {
|
|
1183
|
+
// Activity started — readline stays active for type-ahead buffering
|
|
1184
|
+
// but we do NOT show a visible prompt (it would collide with tool output)
|
|
1185
|
+
isAtPrompt = false;
|
|
1186
|
+
}, { enabled: validationEnabled, verbose: validationVerbose, strict: validationStrict });
|
|
1187
|
+
agentRunning = false;
|
|
1188
|
+
// Keep simple message history for state persistence
|
|
1189
|
+
messages.push({ role: 'user', content: input });
|
|
1190
|
+
// Process any type-ahead input that was buffered during agent work
|
|
1191
|
+
// Skip if agent was aborted (ESC already cleared the buffer, but guard against race)
|
|
1192
|
+
while (typeAheadBuffer.length > 0 && !agentController.isAborted) {
|
|
1193
|
+
const buffered = typeAheadBuffer.shift();
|
|
1194
|
+
if (buffered.trim()) {
|
|
1195
|
+
// Process the buffered input as if user just typed it
|
|
1196
|
+
sessionBuffer.addUserMessage(buffered.trim());
|
|
1197
|
+
checkpointStore.createForChat(buffered.trim(), agentHistory.length);
|
|
1198
|
+
roundToolCalls = 0;
|
|
1199
|
+
agentRunning = true;
|
|
1200
|
+
agentController.reset();
|
|
1201
|
+
updateStatusBar();
|
|
1202
|
+
await sendAgentMessage(buffered.trim(), agentHistory, provider, project, spiralEngine, config, permissions, undoStack, checkpointStore, agentController, activity, sessionBuffer, (inp, out) => { sessionTokensInput += inp; sessionTokensOutput += out; }, () => { sessionToolCalls++; roundToolCalls++; }, () => { isAtPrompt = true; rl.prompt(); }, { enabled: validationEnabled, verbose: validationVerbose, strict: validationStrict });
|
|
1203
|
+
agentRunning = false;
|
|
1204
|
+
messages.push({ role: 'user', content: buffered.trim() });
|
|
1205
|
+
}
|
|
1206
|
+
}
|
|
1207
|
+
showPrompt();
|
|
1208
|
+
}
|
|
1209
|
+
// === Paste-aware line handler ===
|
|
1210
|
+
// Rapid-fire line events (< 50ms apart) = multi-line paste.
|
|
1211
|
+
// We collect them and show a preview instead of sending immediately.
|
|
1212
|
+
rl.on('line', (line) => {
|
|
1213
|
+
isAtPrompt = false;
|
|
1214
|
+
ctrlCCount = 0;
|
|
1215
|
+
// Guard: skip phantom line events from sub-readline
|
|
1216
|
+
if (Date.now() < drainUntil) {
|
|
1217
|
+
showPrompt();
|
|
1218
|
+
return;
|
|
1219
|
+
}
|
|
1220
|
+
// Clear command suggestions on submit
|
|
1221
|
+
if (lastSuggestionCount > 0) {
|
|
1222
|
+
clearSuggestions(lastSuggestionCount);
|
|
1223
|
+
lastSuggestionCount = 0;
|
|
1224
|
+
}
|
|
1225
|
+
const trimmed = line.trim();
|
|
1226
|
+
// If paste buffer has content and user pressed Enter on empty line → send it
|
|
1227
|
+
if (!trimmed && pasteBuffer.length > 0) {
|
|
1228
|
+
const assembled = pasteBuffer.join('\n');
|
|
1229
|
+
pasteBuffer = [];
|
|
1230
|
+
if (pasteTimer) {
|
|
1231
|
+
clearTimeout(pasteTimer);
|
|
1232
|
+
pasteTimer = null;
|
|
1233
|
+
}
|
|
1234
|
+
// Show full pasted text as user message
|
|
1235
|
+
process.stdout.write(`\x1b[2K\r`);
|
|
1236
|
+
processInput(assembled);
|
|
1237
|
+
return;
|
|
1238
|
+
}
|
|
1239
|
+
if (!trimmed) {
|
|
1240
|
+
isAtPrompt = true;
|
|
1241
|
+
rl.prompt();
|
|
1242
|
+
return;
|
|
1243
|
+
}
|
|
1244
|
+
// If agent is running, buffer for type-ahead (no paste detection needed)
|
|
1245
|
+
if (agentRunning) {
|
|
1246
|
+
typeAheadBuffer.push(trimmed);
|
|
1247
|
+
process.stdout.write(` ${theme.dim('\u23F3 Queued:')} ${theme.dim(trimmed)}\n`);
|
|
1248
|
+
// Re-show prompt for further type-ahead input
|
|
1249
|
+
rl.prompt();
|
|
1250
|
+
return;
|
|
1251
|
+
}
|
|
1252
|
+
// Paste detection: if a timer is already running, this is a continuation
|
|
1253
|
+
if (pasteTimer) {
|
|
1254
|
+
pasteBuffer.push(line);
|
|
1255
|
+
clearTimeout(pasteTimer);
|
|
1256
|
+
// Show updated preview
|
|
1257
|
+
const count = pasteBuffer.length;
|
|
1258
|
+
process.stdout.write(`\x1b[2K\r ${chalk.dim(`(${count} Zeilen eingefuegt — Enter zum Senden, Esc zum Verwerfen)`)}`);
|
|
1259
|
+
pasteTimer = setTimeout(() => {
|
|
1260
|
+
// Paste ended — show final preview and wait for Enter
|
|
1261
|
+
pasteTimer = null;
|
|
1262
|
+
const count = pasteBuffer.length;
|
|
1263
|
+
const preview = pasteBuffer[0].slice(0, 60);
|
|
1264
|
+
process.stdout.write(`\x1b[2K\r ${chalk.cyan(`[${count} Zeilen]`)} ${chalk.dim(preview + (pasteBuffer[0].length > 60 ? '...' : ''))}\n`);
|
|
1265
|
+
process.stdout.write(` ${chalk.dim('Enter = senden | Esc = verwerfen')}\n`);
|
|
1266
|
+
rl.prompt();
|
|
1267
|
+
}, PASTE_THRESHOLD_MS);
|
|
1268
|
+
return;
|
|
1269
|
+
}
|
|
1270
|
+
// First line — start the paste timer
|
|
1271
|
+
pasteBuffer = [line];
|
|
1272
|
+
pasteTimer = setTimeout(() => {
|
|
1273
|
+
// Timer expired without more lines → this was a normal single-line input
|
|
1274
|
+
pasteTimer = null;
|
|
1275
|
+
const singleInput = pasteBuffer.join('\n').trim();
|
|
1276
|
+
pasteBuffer = [];
|
|
1277
|
+
if (singleInput) {
|
|
1278
|
+
processInput(singleInput);
|
|
1279
|
+
}
|
|
1280
|
+
else {
|
|
1281
|
+
isAtPrompt = true;
|
|
1282
|
+
rl.prompt();
|
|
1283
|
+
}
|
|
1284
|
+
}, PASTE_THRESHOLD_MS);
|
|
1285
|
+
});
|
|
1286
|
+
// Handle Esc to discard paste buffer
|
|
1287
|
+
if (process.stdin.isTTY) {
|
|
1288
|
+
const origKeypress = process.stdin.listeners('keypress');
|
|
1289
|
+
// Insert paste-cancel before the existing ESC handler
|
|
1290
|
+
process.stdin.prependListener('keypress', (_str, key) => {
|
|
1291
|
+
if (key?.name === 'escape' && pasteBuffer.length > 0 && !agentRunning) {
|
|
1292
|
+
pasteBuffer = [];
|
|
1293
|
+
if (pasteTimer) {
|
|
1294
|
+
clearTimeout(pasteTimer);
|
|
1295
|
+
pasteTimer = null;
|
|
1296
|
+
}
|
|
1297
|
+
process.stdout.write(`\x1b[2K\r ${chalk.dim('Paste verworfen.')}\n`);
|
|
1298
|
+
showPrompt();
|
|
1299
|
+
}
|
|
1300
|
+
});
|
|
1301
|
+
}
|
|
1302
|
+
rl.on('close', async () => {
|
|
1303
|
+
clearInterval(footerTimer);
|
|
1304
|
+
if (spiralEngine) {
|
|
1305
|
+
// Persist session buffer (goals, entities, decisions) into spiral brain
|
|
1306
|
+
// so next session with the same brain can recall them
|
|
1307
|
+
try {
|
|
1308
|
+
const goals = sessionBuffer.getGoals();
|
|
1309
|
+
const entities = sessionBuffer.getEntities();
|
|
1310
|
+
if (goals.length > 0) {
|
|
1311
|
+
await spiralEngine.store(`[Session Goals] ${goals.join(' | ')}`, 'decision', { tags: ['session', 'goals'] });
|
|
1312
|
+
}
|
|
1313
|
+
if (entities.size > 0) {
|
|
1314
|
+
const entryList = [...entities.entries()].map(([k, v]) => `${k}=${v}`).join(', ');
|
|
1315
|
+
await spiralEngine.store(`[Session Refs] ${entryList}`, 'summary', { tags: ['session', 'entities'] });
|
|
1316
|
+
}
|
|
1317
|
+
}
|
|
1318
|
+
catch { /* best effort */ }
|
|
1319
|
+
try {
|
|
1320
|
+
await spiralEngine.saveState(messages);
|
|
1321
|
+
}
|
|
1322
|
+
catch { /* best effort */ }
|
|
1323
|
+
spiralEngine.close();
|
|
1324
|
+
}
|
|
1325
|
+
process.stdout.write('\n');
|
|
1326
|
+
process.exit(0);
|
|
1327
|
+
});
|
|
1328
|
+
}
|
|
1329
|
+
/**
|
|
1330
|
+
* Run an agent task in a background session.
|
|
1331
|
+
* Uses the session's own history, buffer, controller, and undo stack.
|
|
1332
|
+
* Output goes to the session's capture buffer (not stdout).
|
|
1333
|
+
*/
|
|
1334
|
+
async function runBackgroundSession(session, prompt, provider, project, spiralEngine, config, permissions, checkpointStore, onTokens, onToolCall, validationOpts) {
|
|
1335
|
+
const bgActivity = new ActivityIndicator();
|
|
1336
|
+
await sendAgentMessage(prompt, session.history, provider, project, spiralEngine, config, permissions, session.undoStack, checkpointStore, session.controller, bgActivity, session.buffer, onTokens, onToolCall, undefined, validationOpts);
|
|
1337
|
+
// Build result from the session buffer
|
|
1338
|
+
const steps = session.buffer.getRecentErrors().map((e, i) => ({
|
|
1339
|
+
num: i + 1,
|
|
1340
|
+
tool: 'background',
|
|
1341
|
+
label: e.summary,
|
|
1342
|
+
status: 'error',
|
|
1343
|
+
error: e.summary,
|
|
1344
|
+
}));
|
|
1345
|
+
return {
|
|
1346
|
+
text: session.buffer.buildContext(),
|
|
1347
|
+
steps,
|
|
1348
|
+
errors: [],
|
|
1349
|
+
durationMs: session.elapsed,
|
|
1350
|
+
};
|
|
1351
|
+
}
|
|
1352
|
+
async function sendAgentMessage(input, agentHistory, provider, project, spiralEngine, config, permissions, undoStack, checkpointStore, controller, activity, sessionBuffer, onTokens, onToolCall, onAgentStart, validationOpts) {
|
|
1353
|
+
// User message was rendered by renderUserMessage() in the caller before entering here.
|
|
1354
|
+
// Intent Detection: Check if user wants to feed the codebase
|
|
1355
|
+
const feedIntent = detectFeedIntent(input);
|
|
1356
|
+
if (feedIntent.detected && feedIntent.confidence > 0.7 && spiralEngine) {
|
|
1357
|
+
renderInfo('\u{1F300} Analyzing project in the background...\n');
|
|
1358
|
+
const rootDir = process.cwd();
|
|
1359
|
+
// Background feed runs silently (no progress output) to avoid colliding
|
|
1360
|
+
// with the activity indicator which also writes \r\x1b[K on the same line.
|
|
1361
|
+
runFeedPipeline(rootDir, spiralEngine, {
|
|
1362
|
+
targetPath: feedIntent.path,
|
|
1363
|
+
}).then(result => {
|
|
1364
|
+
if (result.nodesCreated > 0) {
|
|
1365
|
+
process.stdout.write(chalk.dim(` \u{1F300} Feed: +${result.nodesCreated} nodes from ${result.filesRead} files\n`));
|
|
1366
|
+
}
|
|
1367
|
+
}).catch(() => { });
|
|
1368
|
+
}
|
|
1369
|
+
// === WEB ENRICHMENT (background) ===
|
|
1370
|
+
// Automatically fetch web knowledge about the topic while the agent works.
|
|
1371
|
+
// Runs in background — results are stored in spiral brain for this + future queries.
|
|
1372
|
+
// Available for ALL tiers — this is the core intelligence that makes HelixMind useful.
|
|
1373
|
+
let enrichmentPromise = null;
|
|
1374
|
+
if (spiralEngine) {
|
|
1375
|
+
try {
|
|
1376
|
+
const { enrichFromWeb } = await import('../../spiral/cloud/web-enricher.js');
|
|
1377
|
+
const { pushWebKnowledge, isBrainServerRunning } = await import('../brain/generator.js');
|
|
1378
|
+
enrichmentPromise = enrichFromWeb(input, spiralEngine, {
|
|
1379
|
+
maxTopics: 2,
|
|
1380
|
+
maxPagesPerTopic: 2,
|
|
1381
|
+
minQuality: 0.4,
|
|
1382
|
+
onKnowledgeFound: (topic, summary, source) => {
|
|
1383
|
+
// Push live update to brain visualization (if open in browser)
|
|
1384
|
+
if (isBrainServerRunning()) {
|
|
1385
|
+
pushWebKnowledge(topic, summary, source);
|
|
1386
|
+
}
|
|
1387
|
+
},
|
|
1388
|
+
}).catch(() => null);
|
|
1389
|
+
}
|
|
1390
|
+
catch {
|
|
1391
|
+
// Web enrichment module not available, continue without
|
|
1392
|
+
}
|
|
1393
|
+
}
|
|
1394
|
+
// Query spiral context for system prompt enrichment
|
|
1395
|
+
let spiralContext = {
|
|
1396
|
+
level_1: [], level_2: [], level_3: [], level_4: [], level_5: [],
|
|
1397
|
+
total_tokens: 0, node_count: 0,
|
|
1398
|
+
};
|
|
1399
|
+
if (spiralEngine) {
|
|
1400
|
+
try {
|
|
1401
|
+
spiralContext = await spiralEngine.query(input, config.spiral.maxTokensBudget);
|
|
1402
|
+
}
|
|
1403
|
+
catch {
|
|
1404
|
+
// Spiral query failed, continue without
|
|
1405
|
+
}
|
|
1406
|
+
}
|
|
1407
|
+
// Assemble system prompt with spiral context + project info + session memory
|
|
1408
|
+
const sessionContext = sessionBuffer.buildContext();
|
|
1409
|
+
const systemPrompt = assembleSystemPrompt(project.name !== 'unknown' ? project : null, spiralContext, sessionContext || undefined, { provider: provider.name, model: provider.model });
|
|
1410
|
+
// Auto-trim context when approaching budget limit
|
|
1411
|
+
const maxBudget = config.spiral.maxTokensBudget || 200000;
|
|
1412
|
+
trimConversation(agentHistory, maxBudget, sessionBuffer);
|
|
1413
|
+
// Start the glowing activity indicator (reserves bottom row via scroll region)
|
|
1414
|
+
activity.start();
|
|
1415
|
+
// Notify caller so it can show the readline prompt for type-ahead
|
|
1416
|
+
onAgentStart?.();
|
|
1417
|
+
try {
|
|
1418
|
+
const result = await runAgentLoop(input, agentHistory, {
|
|
1419
|
+
provider,
|
|
1420
|
+
systemPrompt,
|
|
1421
|
+
permissions,
|
|
1422
|
+
toolContext: {
|
|
1423
|
+
projectRoot: process.cwd(),
|
|
1424
|
+
undoStack,
|
|
1425
|
+
spiralEngine,
|
|
1426
|
+
},
|
|
1427
|
+
checkpointStore,
|
|
1428
|
+
sessionBuffer,
|
|
1429
|
+
onThinking: () => {
|
|
1430
|
+
// Resume animation before each LLM call (timer keeps running)
|
|
1431
|
+
activity.setBlockMode(isInsideToolBlock());
|
|
1432
|
+
if (!activity.isAnimating) {
|
|
1433
|
+
activity.resumeAnimation();
|
|
1434
|
+
}
|
|
1435
|
+
},
|
|
1436
|
+
onTokensUsed: (inp, out) => {
|
|
1437
|
+
onTokens(inp, out);
|
|
1438
|
+
},
|
|
1439
|
+
onToolCall: () => {
|
|
1440
|
+
activity.pauseAnimation(); // Pause animation during tool execution (timer keeps running)
|
|
1441
|
+
onToolCall();
|
|
1442
|
+
},
|
|
1443
|
+
onStepStart: (num, _tool, label) => {
|
|
1444
|
+
activity.setStep(num, label);
|
|
1445
|
+
},
|
|
1446
|
+
onStepEnd: (_num, _tool, status) => {
|
|
1447
|
+
if (status === 'error')
|
|
1448
|
+
activity.setError();
|
|
1449
|
+
},
|
|
1450
|
+
onBeforeAnswer: () => {
|
|
1451
|
+
activity.stop(); // Writes colorful "HelixMind Done" replacing animation
|
|
1452
|
+
},
|
|
1453
|
+
}, controller);
|
|
1454
|
+
// activity.stop() was already called via onBeforeAnswer (shows colorful "Done" line)
|
|
1455
|
+
// Ensure stopped if onBeforeAnswer wasn't reached (e.g. no tools, direct answer)
|
|
1456
|
+
if (activity.isRunning)
|
|
1457
|
+
activity.stop();
|
|
1458
|
+
// CRITICAL: Adopt updated conversation history from agent loop.
|
|
1459
|
+
// runAgentLoop works on a copy — we must sync it back so the next turn
|
|
1460
|
+
// sees the full conversation (user message + assistant + tool results).
|
|
1461
|
+
agentHistory.length = 0;
|
|
1462
|
+
agentHistory.push(...result.updatedHistory);
|
|
1463
|
+
// ═══ PHASE 3: VALIDATION MATRIX ═══
|
|
1464
|
+
if (validationOpts?.enabled && result.text) {
|
|
1465
|
+
try {
|
|
1466
|
+
// Phase 1: Classify
|
|
1467
|
+
const classification = classifyTask(input);
|
|
1468
|
+
if (classification.category !== 'chat_only') {
|
|
1469
|
+
// Generate criteria
|
|
1470
|
+
let spiralContextStr = '';
|
|
1471
|
+
if (spiralEngine) {
|
|
1472
|
+
try {
|
|
1473
|
+
const sq = await spiralEngine.query(input, undefined, [3, 4, 5]);
|
|
1474
|
+
spiralContextStr = [...sq.level_3, ...sq.level_4, ...sq.level_5]
|
|
1475
|
+
.map((n) => n.content).join('\n');
|
|
1476
|
+
}
|
|
1477
|
+
catch { /* ignore */ }
|
|
1478
|
+
}
|
|
1479
|
+
const criteria = generateCriteria(classification, input, spiralContextStr || undefined);
|
|
1480
|
+
if (criteria.length > 0) {
|
|
1481
|
+
if (validationOpts.verbose) {
|
|
1482
|
+
process.stdout.write(renderClassification(classification.category, classification.complexity, criteria.length) + '\n');
|
|
1483
|
+
}
|
|
1484
|
+
process.stdout.write(renderValidationStart());
|
|
1485
|
+
// Create validation provider (smaller/faster model)
|
|
1486
|
+
let valProvider;
|
|
1487
|
+
try {
|
|
1488
|
+
valProvider = createValidationProvider(config.model, config.provider, config.apiKey);
|
|
1489
|
+
}
|
|
1490
|
+
catch { /* use without dynamic checks */ }
|
|
1491
|
+
// Run validation loop
|
|
1492
|
+
const valResult = await validationLoop(result.text, {
|
|
1493
|
+
criteria,
|
|
1494
|
+
userRequest: input,
|
|
1495
|
+
spiralContext: spiralContextStr,
|
|
1496
|
+
spiralEngine: spiralEngine || undefined,
|
|
1497
|
+
validationProvider: valProvider,
|
|
1498
|
+
maxLoops: 3,
|
|
1499
|
+
});
|
|
1500
|
+
// If strict mode, promote warnings to effective errors
|
|
1501
|
+
if (validationOpts.strict) {
|
|
1502
|
+
for (const r of valResult.results) {
|
|
1503
|
+
if (!r.passed && r.severity === 'warning') {
|
|
1504
|
+
r.severity = 'error';
|
|
1505
|
+
}
|
|
1506
|
+
}
|
|
1507
|
+
}
|
|
1508
|
+
// Show summary
|
|
1509
|
+
process.stdout.write(renderValidationSummary(valResult, validationOpts.verbose) + '\n');
|
|
1510
|
+
// Store stats in spiral
|
|
1511
|
+
await storeValidationResult(valResult, classification.category, spiralEngine || undefined);
|
|
1512
|
+
}
|
|
1513
|
+
}
|
|
1514
|
+
}
|
|
1515
|
+
catch {
|
|
1516
|
+
// Validation should never block the user
|
|
1517
|
+
}
|
|
1518
|
+
}
|
|
1519
|
+
// Track assistant response in session buffer
|
|
1520
|
+
if (result.text) {
|
|
1521
|
+
sessionBuffer.addAssistantSummary(result.text);
|
|
1522
|
+
}
|
|
1523
|
+
// Create checkpoint for agent response
|
|
1524
|
+
if (result.text) {
|
|
1525
|
+
checkpointStore.createForChat(result.text.length > 60 ? result.text.slice(0, 60) + '...' : result.text, agentHistory.length);
|
|
1526
|
+
}
|
|
1527
|
+
// Store turn summary in spiral (user request + agent response)
|
|
1528
|
+
if (spiralEngine && config.spiral.autoStore && result.text) {
|
|
1529
|
+
const turnSummary = `User: ${input.slice(0, 100)} → Agent: ${result.text.slice(0, 400)}`;
|
|
1530
|
+
spiralEngine.store(turnSummary, 'summary', { tags: ['session', 'turn'] }).catch(() => { });
|
|
1531
|
+
}
|
|
1532
|
+
// Show web enrichment results (if any arrived while agent worked)
|
|
1533
|
+
if (enrichmentPromise) {
|
|
1534
|
+
try {
|
|
1535
|
+
const enrichResult = await enrichmentPromise;
|
|
1536
|
+
if (enrichResult && enrichResult.nodesStored > 0) {
|
|
1537
|
+
const topicList = enrichResult.topics.join(', ');
|
|
1538
|
+
process.stdout.write(chalk.dim(` \u{1F310} Web: +${enrichResult.nodesStored} knowledge nodes stored `) +
|
|
1539
|
+
chalk.dim(`(${topicList})`) +
|
|
1540
|
+
chalk.dim(` [${enrichResult.duration_ms}ms]\n`));
|
|
1541
|
+
}
|
|
1542
|
+
}
|
|
1543
|
+
catch {
|
|
1544
|
+
// Enrichment error — silent, never block the user
|
|
1545
|
+
}
|
|
1546
|
+
}
|
|
1547
|
+
}
|
|
1548
|
+
catch (err) {
|
|
1549
|
+
if (activity.isRunning)
|
|
1550
|
+
activity.stop('Stopped');
|
|
1551
|
+
if (err instanceof AgentAbortError) {
|
|
1552
|
+
renderInfo('\n\u23F9 Agent aborted.');
|
|
1553
|
+
}
|
|
1554
|
+
else {
|
|
1555
|
+
const errMsg = err instanceof Error ? err.message : String(err);
|
|
1556
|
+
// Categorize and show user-friendly error
|
|
1557
|
+
const { isRateLimitError: isRL } = await import('../providers/rate-limiter.js');
|
|
1558
|
+
if (isRL(err)) {
|
|
1559
|
+
process.stdout.write('\n');
|
|
1560
|
+
renderError('Rate limit reached. Waiting and retrying automatically next time.');
|
|
1561
|
+
renderInfo(chalk.dim(' Tip: Use /compact to reduce spiral nodes, or wait a moment before retrying.'));
|
|
1562
|
+
}
|
|
1563
|
+
else if (errMsg.includes('authentication') || errMsg.includes('401') || errMsg.includes('invalid.*key')) {
|
|
1564
|
+
renderError('Authentication failed. Your API key may be invalid or expired.');
|
|
1565
|
+
renderInfo(chalk.dim(' Fix: /keys to update your API key.'));
|
|
1566
|
+
}
|
|
1567
|
+
else if (errMsg.includes('ENOTFOUND') || errMsg.includes('ECONNREFUSED') || errMsg.includes('network')) {
|
|
1568
|
+
renderError('Network error — cannot reach the API server.');
|
|
1569
|
+
renderInfo(chalk.dim(' Check your internet connection and try again.'));
|
|
1570
|
+
}
|
|
1571
|
+
else if (errMsg.includes('context_length') || errMsg.includes('too many tokens') || errMsg.includes('maximum context')) {
|
|
1572
|
+
renderError('Context too large for the model.');
|
|
1573
|
+
renderInfo(chalk.dim(' Fix: /clear to reset conversation, or /compact to reduce spiral size.'));
|
|
1574
|
+
}
|
|
1575
|
+
else if (errMsg.includes('Max retries exceeded')) {
|
|
1576
|
+
renderError('API temporarily unavailable after multiple retries.');
|
|
1577
|
+
renderInfo(chalk.dim(' Wait a moment and try again. The rate limiter will auto-recover.'));
|
|
1578
|
+
}
|
|
1579
|
+
else {
|
|
1580
|
+
renderError(errMsg.length > 200 ? errMsg.slice(0, 200) + '...' : errMsg);
|
|
1581
|
+
}
|
|
1582
|
+
// Track error in session buffer
|
|
1583
|
+
sessionBuffer.addToolError('agent_loop', errMsg);
|
|
1584
|
+
}
|
|
1585
|
+
}
|
|
1586
|
+
}
|
|
1587
|
+
function showSkipPermissionsWarning() {
|
|
1588
|
+
const w = chalk.yellow;
|
|
1589
|
+
const g = chalk.green;
|
|
1590
|
+
const d = chalk.dim;
|
|
1591
|
+
process.stdout.write('\n');
|
|
1592
|
+
process.stdout.write(d('\u256D\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256E') + '\n');
|
|
1593
|
+
process.stdout.write(d('\u2502 ') + w('\u26A0\uFE0F SKIP-PERMISSIONS MODE') + d(' \u2502') + '\n');
|
|
1594
|
+
process.stdout.write(d('\u2502') + d(' \u2502') + '\n');
|
|
1595
|
+
process.stdout.write(d('\u2502 ') + 'HelixMind will automatically:' + d(' \u2502') + '\n');
|
|
1596
|
+
process.stdout.write(d('\u2502 ') + g('\u2713') + ' Read and write files' + d(' \u2502') + '\n');
|
|
1597
|
+
process.stdout.write(d('\u2502 ') + g('\u2713') + ' Edit existing code' + d(' \u2502') + '\n');
|
|
1598
|
+
process.stdout.write(d('\u2502 ') + g('\u2713') + ' Run shell commands (safe ones)' + d(' \u2502') + '\n');
|
|
1599
|
+
process.stdout.write(d('\u2502 ') + g('\u2713') + ' Create git commits' + d(' \u2502') + '\n');
|
|
1600
|
+
process.stdout.write(d('\u2502') + d(' \u2502') + '\n');
|
|
1601
|
+
process.stdout.write(d('\u2502 ') + 'Still requires confirmation for:' + d(' \u2502') + '\n');
|
|
1602
|
+
process.stdout.write(d('\u2502 ') + w('\u26A0') + ' Dangerous commands (rm -rf, sudo)' + d(' \u2502') + '\n');
|
|
1603
|
+
process.stdout.write(d('\u2502') + d(' \u2502') + '\n');
|
|
1604
|
+
process.stdout.write(d('\u2502 ') + d('ESC = stop agent --yolo = skip all') + d(' \u2502') + '\n');
|
|
1605
|
+
process.stdout.write(d('\u2570\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256F') + '\n\n');
|
|
1606
|
+
}
|
|
1607
|
+
function showFullAutonomousWarning() {
|
|
1608
|
+
const r = chalk.red;
|
|
1609
|
+
const d = chalk.dim;
|
|
1610
|
+
process.stdout.write('\n');
|
|
1611
|
+
process.stdout.write(d('\u256D\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256E') + '\n');
|
|
1612
|
+
process.stdout.write(d('\u2502 ') + r('\u{1F525} FULL AUTONOMOUS MODE') + d(' \u2502') + '\n');
|
|
1613
|
+
process.stdout.write(d('\u2502') + d(' \u2502') + '\n');
|
|
1614
|
+
process.stdout.write(d('\u2502 ') + 'HelixMind will execute ALL actions' + d(' \u2502') + '\n');
|
|
1615
|
+
process.stdout.write(d('\u2502 ') + 'without asking. No confirmations.' + d(' \u2502') + '\n');
|
|
1616
|
+
process.stdout.write(d('\u2502') + d(' \u2502') + '\n');
|
|
1617
|
+
process.stdout.write(d('\u2502 ') + d('ESC = stop agent if needed.') + d(' \u2502') + '\n');
|
|
1618
|
+
process.stdout.write(d('\u2570\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256F') + '\n\n');
|
|
1619
|
+
}
|
|
1620
|
+
async function handleSlashCommand(input, messages, agentHistory, config, spiralEngine, store, rl, permissions, undoStack, checkpointStore, sessionBuffer, sessionTokens, sessionToolCalls, onProviderSwitch, onBrainSwitch, currentBrainScope, onAutonomous, onValidation, sessionManager, onRegisterBrainHandlers, onSubPrompt) {
|
|
1621
|
+
const parts = input.split(/\s+/);
|
|
1622
|
+
const cmd = parts[0].toLowerCase();
|
|
1623
|
+
switch (cmd) {
|
|
1624
|
+
case '/help': {
|
|
1625
|
+
if (!process.stdin.isTTY) {
|
|
1626
|
+
// Non-interactive: show static text
|
|
1627
|
+
process.stdout.write(HELP_TEXT);
|
|
1628
|
+
break;
|
|
1629
|
+
}
|
|
1630
|
+
// Pause readline so selectMenu can take raw input
|
|
1631
|
+
onSubPrompt?.(true);
|
|
1632
|
+
rl.pause();
|
|
1633
|
+
process.stdout.write('\n');
|
|
1634
|
+
const { items: helpItems, commands: helpCmds } = buildHelpMenuItems();
|
|
1635
|
+
const helpIdx = await selectMenu(helpItems, {
|
|
1636
|
+
title: chalk.hex('#00d4ff').bold('HelixMind Commands'),
|
|
1637
|
+
cancelLabel: 'Close',
|
|
1638
|
+
pageSize: 15,
|
|
1639
|
+
});
|
|
1640
|
+
rl.resume();
|
|
1641
|
+
if (helpIdx >= 0 && helpCmds[helpIdx]) {
|
|
1642
|
+
// Execute the selected command
|
|
1643
|
+
return handleSlashCommand(helpCmds[helpIdx], messages, agentHistory, config, spiralEngine, store, rl, permissions, undoStack, checkpointStore, sessionBuffer, sessionTokens, sessionToolCalls, onProviderSwitch, onBrainSwitch, currentBrainScope, onAutonomous, onValidation, sessionManager, onRegisterBrainHandlers, onSubPrompt);
|
|
1644
|
+
}
|
|
1645
|
+
break;
|
|
1646
|
+
}
|
|
1647
|
+
case '/clear':
|
|
1648
|
+
messages.length = 0;
|
|
1649
|
+
agentHistory.length = 0;
|
|
1650
|
+
renderInfo('Conversation cleared.');
|
|
1651
|
+
break;
|
|
1652
|
+
case '/model': {
|
|
1653
|
+
const directModel = parts[1];
|
|
1654
|
+
if (directModel) {
|
|
1655
|
+
// Check if it looks like an Ollama model (contains ':' or is a known local model pattern)
|
|
1656
|
+
const isOllamaModel = directModel.includes(':') || directModel.match(/^(qwen|llama|deepseek|codellama|mistral|phi|gemma|starcoder)/i);
|
|
1657
|
+
if (isOllamaModel) {
|
|
1658
|
+
// Switch to Ollama provider
|
|
1659
|
+
store.addProvider('ollama', 'ollama', 'http://localhost:11434/v1');
|
|
1660
|
+
store.switchProvider('ollama', directModel);
|
|
1661
|
+
try {
|
|
1662
|
+
const newProvider = createProvider('ollama', 'ollama', directModel, 'http://localhost:11434/v1');
|
|
1663
|
+
onProviderSwitch?.(newProvider);
|
|
1664
|
+
renderInfo(`Switched to: ollama / ${directModel}`);
|
|
1665
|
+
}
|
|
1666
|
+
catch (err) {
|
|
1667
|
+
renderError(`Failed to switch: ${err}`);
|
|
1668
|
+
}
|
|
1669
|
+
}
|
|
1670
|
+
else {
|
|
1671
|
+
// Regular provider model switch
|
|
1672
|
+
store.switchModel(directModel);
|
|
1673
|
+
const newConfig = store.getAll();
|
|
1674
|
+
try {
|
|
1675
|
+
const newProvider = createProvider(newConfig.provider, newConfig.apiKey, newConfig.model, newConfig.providers[newConfig.provider]?.baseURL);
|
|
1676
|
+
onProviderSwitch?.(newProvider);
|
|
1677
|
+
renderInfo(`Switched to: ${newConfig.provider} / ${newConfig.model}`);
|
|
1678
|
+
}
|
|
1679
|
+
catch (err) {
|
|
1680
|
+
renderError(`Failed to switch: ${err}`);
|
|
1681
|
+
}
|
|
1682
|
+
}
|
|
1683
|
+
}
|
|
1684
|
+
else {
|
|
1685
|
+
// Interactive picker — suppress statusbar to prevent cursor interference
|
|
1686
|
+
onSubPrompt?.(true);
|
|
1687
|
+
rl.pause();
|
|
1688
|
+
const configBefore = store.getAll();
|
|
1689
|
+
const result = await showModelSwitcher(store, rl);
|
|
1690
|
+
rl.resume();
|
|
1691
|
+
// Always refresh provider if config changed (covers "Add new provider" path too)
|
|
1692
|
+
const newConfig = store.getAll();
|
|
1693
|
+
const configChanged = newConfig.provider !== configBefore.provider
|
|
1694
|
+
|| newConfig.model !== configBefore.model
|
|
1695
|
+
|| newConfig.apiKey !== configBefore.apiKey;
|
|
1696
|
+
if ((result || configChanged) && onProviderSwitch && newConfig.apiKey) {
|
|
1697
|
+
try {
|
|
1698
|
+
const newProvider = createProvider(newConfig.provider, newConfig.apiKey, newConfig.model, newConfig.providers[newConfig.provider]?.baseURL);
|
|
1699
|
+
onProviderSwitch(newProvider);
|
|
1700
|
+
}
|
|
1701
|
+
catch (err) {
|
|
1702
|
+
renderError(`Failed to switch: ${err}`);
|
|
1703
|
+
}
|
|
1704
|
+
}
|
|
1705
|
+
}
|
|
1706
|
+
return 'drain'; // Sub-readline may leave phantom line events
|
|
1707
|
+
}
|
|
1708
|
+
case '/keys': {
|
|
1709
|
+
// Suppress statusbar to prevent cursor interference during text input
|
|
1710
|
+
onSubPrompt?.(true);
|
|
1711
|
+
rl.pause();
|
|
1712
|
+
await showKeyManagement(store, rl);
|
|
1713
|
+
rl.resume();
|
|
1714
|
+
// Refresh provider after key changes
|
|
1715
|
+
const newConfig = store.getAll();
|
|
1716
|
+
if (newConfig.apiKey && onProviderSwitch) {
|
|
1717
|
+
try {
|
|
1718
|
+
const newProvider = createProvider(newConfig.provider, newConfig.apiKey, newConfig.model, newConfig.providers[newConfig.provider]?.baseURL);
|
|
1719
|
+
onProviderSwitch(newProvider);
|
|
1720
|
+
}
|
|
1721
|
+
catch { /* ignore */ }
|
|
1722
|
+
}
|
|
1723
|
+
return 'drain'; // Sub-readline may leave phantom line events
|
|
1724
|
+
}
|
|
1725
|
+
case '/spiral':
|
|
1726
|
+
if (spiralEngine) {
|
|
1727
|
+
try {
|
|
1728
|
+
const status = spiralEngine.status();
|
|
1729
|
+
renderSpiralStatus(status.total_nodes, status.per_level[1] ?? 0, status.per_level[2] ?? 0, status.per_level[3] ?? 0, status.per_level[4] ?? 0, status.per_level[5] ?? 0);
|
|
1730
|
+
}
|
|
1731
|
+
catch {
|
|
1732
|
+
renderInfo('Spiral engine not available.');
|
|
1733
|
+
}
|
|
1734
|
+
}
|
|
1735
|
+
else {
|
|
1736
|
+
renderInfo('Spiral engine disabled.');
|
|
1737
|
+
}
|
|
1738
|
+
break;
|
|
1739
|
+
case '/helix':
|
|
1740
|
+
case '/helixlocal':
|
|
1741
|
+
// Always use local brain for /helix and /helixlocal
|
|
1742
|
+
if (onBrainSwitch && currentBrainScope !== 'project') {
|
|
1743
|
+
await onBrainSwitch('project');
|
|
1744
|
+
renderInfo(chalk.cyan('\u{1F4C1} Switched to project-local brain (.helixmind/)'));
|
|
1745
|
+
try {
|
|
1746
|
+
const { pushScopeChange, isBrainServerRunning } = await import('../brain/generator.js');
|
|
1747
|
+
if (isBrainServerRunning())
|
|
1748
|
+
pushScopeChange('project');
|
|
1749
|
+
}
|
|
1750
|
+
catch { /* optional */ }
|
|
1751
|
+
}
|
|
1752
|
+
// Auto-start brain visualization
|
|
1753
|
+
if (spiralEngine) {
|
|
1754
|
+
try {
|
|
1755
|
+
const { exportBrainData } = await import('../brain/exporter.js');
|
|
1756
|
+
const { startLiveBrain, isBrainServerRunning } = await import('../brain/generator.js');
|
|
1757
|
+
const { exec } = await import('node:child_process');
|
|
1758
|
+
const { platform } = await import('node:os');
|
|
1759
|
+
const data = exportBrainData(spiralEngine, 'HelixMind Project', 'project');
|
|
1760
|
+
if (data.meta.totalNodes > 0 && !isBrainServerRunning()) {
|
|
1761
|
+
const url = await startLiveBrain(spiralEngine, 'HelixMind Project', 'project');
|
|
1762
|
+
if (onRegisterBrainHandlers)
|
|
1763
|
+
await onRegisterBrainHandlers();
|
|
1764
|
+
const openCmd = platform() === 'win32' ? `start "" "${url}"`
|
|
1765
|
+
: platform() === 'darwin' ? `open "${url}"`
|
|
1766
|
+
: `xdg-open "${url}"`;
|
|
1767
|
+
exec(openCmd, () => { });
|
|
1768
|
+
process.stdout.write(` ${theme.success('\u{1F9E0} Brain View started:')} ${url}\n`);
|
|
1769
|
+
}
|
|
1770
|
+
}
|
|
1771
|
+
catch { /* brain optional */ }
|
|
1772
|
+
}
|
|
1773
|
+
onSubPrompt?.(true);
|
|
1774
|
+
rl.pause();
|
|
1775
|
+
await showHelixMenu(spiralEngine, store, 'project');
|
|
1776
|
+
rl.resume();
|
|
1777
|
+
return 'drain';
|
|
1778
|
+
case '/helixglobal':
|
|
1779
|
+
// Use global brain for /helixglobal
|
|
1780
|
+
if (onBrainSwitch && currentBrainScope !== 'global') {
|
|
1781
|
+
await onBrainSwitch('global');
|
|
1782
|
+
renderInfo(chalk.dim('\u{1F310} Switched to global brain (~/.spiral-context/)'));
|
|
1783
|
+
try {
|
|
1784
|
+
const { pushScopeChange, isBrainServerRunning } = await import('../brain/generator.js');
|
|
1785
|
+
if (isBrainServerRunning())
|
|
1786
|
+
pushScopeChange('global');
|
|
1787
|
+
}
|
|
1788
|
+
catch { /* optional */ }
|
|
1789
|
+
}
|
|
1790
|
+
// Auto-start brain visualization
|
|
1791
|
+
if (spiralEngine) {
|
|
1792
|
+
try {
|
|
1793
|
+
const { exportBrainData } = await import('../brain/exporter.js');
|
|
1794
|
+
const { startLiveBrain, isBrainServerRunning } = await import('../brain/generator.js');
|
|
1795
|
+
const { exec } = await import('node:child_process');
|
|
1796
|
+
const { platform } = await import('node:os');
|
|
1797
|
+
const data = exportBrainData(spiralEngine, 'HelixMind Project', 'global');
|
|
1798
|
+
if (data.meta.totalNodes > 0 && !isBrainServerRunning()) {
|
|
1799
|
+
const url = await startLiveBrain(spiralEngine, 'HelixMind Project', 'global');
|
|
1800
|
+
if (onRegisterBrainHandlers)
|
|
1801
|
+
await onRegisterBrainHandlers();
|
|
1802
|
+
const openCmd = platform() === 'win32' ? `start "" "${url}"`
|
|
1803
|
+
: platform() === 'darwin' ? `open "${url}"`
|
|
1804
|
+
: `xdg-open "${url}"`;
|
|
1805
|
+
exec(openCmd, () => { });
|
|
1806
|
+
process.stdout.write(` ${theme.success('\u{1F9E0} Brain View started:')} ${url}\n`);
|
|
1807
|
+
}
|
|
1808
|
+
}
|
|
1809
|
+
catch { /* brain optional */ }
|
|
1810
|
+
}
|
|
1811
|
+
onSubPrompt?.(true);
|
|
1812
|
+
rl.pause();
|
|
1813
|
+
await showHelixMenu(spiralEngine, store, 'global');
|
|
1814
|
+
rl.resume();
|
|
1815
|
+
return 'drain';
|
|
1816
|
+
case '/brain': {
|
|
1817
|
+
const brainArg = parts[1]?.toLowerCase();
|
|
1818
|
+
// /brain local — switch to project-local brain
|
|
1819
|
+
if (brainArg === 'local' || brainArg === 'project') {
|
|
1820
|
+
if (currentBrainScope === 'project') {
|
|
1821
|
+
renderInfo('Already using project-local brain.');
|
|
1822
|
+
}
|
|
1823
|
+
else if (onBrainSwitch) {
|
|
1824
|
+
await onBrainSwitch('project');
|
|
1825
|
+
renderInfo(chalk.cyan('\u{1F4C1} Switched to project-local brain (.helixmind/)'));
|
|
1826
|
+
// Update browser if open
|
|
1827
|
+
try {
|
|
1828
|
+
const { pushScopeChange, isBrainServerRunning } = await import('../brain/generator.js');
|
|
1829
|
+
if (isBrainServerRunning())
|
|
1830
|
+
pushScopeChange('project');
|
|
1831
|
+
}
|
|
1832
|
+
catch { /* optional */ }
|
|
1833
|
+
}
|
|
1834
|
+
break;
|
|
1835
|
+
}
|
|
1836
|
+
// /brain global — switch to global brain
|
|
1837
|
+
if (brainArg === 'global') {
|
|
1838
|
+
if (currentBrainScope === 'global') {
|
|
1839
|
+
renderInfo('Already using global brain.');
|
|
1840
|
+
}
|
|
1841
|
+
else if (onBrainSwitch) {
|
|
1842
|
+
await onBrainSwitch('global');
|
|
1843
|
+
renderInfo(chalk.dim('\u{1F310} Switched to global brain (~/.spiral-context/)'));
|
|
1844
|
+
// Update browser if open
|
|
1845
|
+
try {
|
|
1846
|
+
const { pushScopeChange, isBrainServerRunning } = await import('../brain/generator.js');
|
|
1847
|
+
if (isBrainServerRunning())
|
|
1848
|
+
pushScopeChange('global');
|
|
1849
|
+
}
|
|
1850
|
+
catch { /* optional */ }
|
|
1851
|
+
}
|
|
1852
|
+
break;
|
|
1853
|
+
}
|
|
1854
|
+
// /brain (no arg) — show status + open 3D view
|
|
1855
|
+
if (!brainArg) {
|
|
1856
|
+
const scopeLabel = currentBrainScope === 'project'
|
|
1857
|
+
? chalk.cyan('project-local') + chalk.dim(' (.helixmind/)')
|
|
1858
|
+
: chalk.dim('global') + chalk.dim(' (~/.spiral-context/)');
|
|
1859
|
+
renderInfo(`Brain scope: ${scopeLabel}`);
|
|
1860
|
+
renderInfo(chalk.dim(' /brain local — switch to project brain'));
|
|
1861
|
+
renderInfo(chalk.dim(' /brain global — switch to global brain'));
|
|
1862
|
+
process.stdout.write('\n');
|
|
1863
|
+
}
|
|
1864
|
+
// Open 3D visualization (for /brain or /brain view)
|
|
1865
|
+
if (!brainArg || brainArg === 'view') {
|
|
1866
|
+
if (spiralEngine) {
|
|
1867
|
+
try {
|
|
1868
|
+
const { exportBrainData } = await import('../brain/exporter.js');
|
|
1869
|
+
const { startLiveBrain } = await import('../brain/generator.js');
|
|
1870
|
+
const { exec } = await import('node:child_process');
|
|
1871
|
+
const { platform } = await import('node:os');
|
|
1872
|
+
const data = exportBrainData(spiralEngine, 'HelixMind Project', currentBrainScope);
|
|
1873
|
+
if (data.meta.totalNodes === 0) {
|
|
1874
|
+
renderInfo('Spiral is empty. Feed some files first: /feed');
|
|
1875
|
+
break;
|
|
1876
|
+
}
|
|
1877
|
+
const url = await startLiveBrain(spiralEngine, 'HelixMind Project', currentBrainScope);
|
|
1878
|
+
// Register voice + scope switch handlers
|
|
1879
|
+
if (onRegisterBrainHandlers)
|
|
1880
|
+
await onRegisterBrainHandlers();
|
|
1881
|
+
const openCmd = platform() === 'win32' ? `start "" "${url}"`
|
|
1882
|
+
: platform() === 'darwin' ? `open "${url}"`
|
|
1883
|
+
: `xdg-open "${url}"`;
|
|
1884
|
+
exec(openCmd, () => { });
|
|
1885
|
+
process.stdout.write(` ${theme.success('\u{1F9E0} Brain View live at:')} ${url}\n`);
|
|
1886
|
+
renderInfo('Auto-updates when spiral changes. Voice input enabled via browser mic.');
|
|
1887
|
+
}
|
|
1888
|
+
catch (err) {
|
|
1889
|
+
renderError(`Brain view failed: ${err}`);
|
|
1890
|
+
}
|
|
1891
|
+
}
|
|
1892
|
+
else {
|
|
1893
|
+
renderInfo('Spiral engine not available.');
|
|
1894
|
+
}
|
|
1895
|
+
}
|
|
1896
|
+
break;
|
|
1897
|
+
}
|
|
1898
|
+
case '/feed':
|
|
1899
|
+
// Handled directly in chatCommand() for access to inlineProgressActive flag
|
|
1900
|
+
break;
|
|
1901
|
+
case '/context':
|
|
1902
|
+
if (spiralEngine) {
|
|
1903
|
+
const status = spiralEngine.status();
|
|
1904
|
+
renderInfo(`Context: ${status.total_nodes} spiral nodes, ${status.total_edges} edges`);
|
|
1905
|
+
renderInfo(` Storage: ${(status.storage_size_bytes / 1024).toFixed(1)} KB`);
|
|
1906
|
+
renderInfo(` Embeddings: ${status.embedding_status}`);
|
|
1907
|
+
renderInfo(` Session buffer: ${sessionBuffer.eventCount} events, ${sessionBuffer.totalErrors} errors`);
|
|
1908
|
+
renderInfo(` Files modified: ${sessionBuffer.getModifiedFiles().length}`);
|
|
1909
|
+
}
|
|
1910
|
+
else {
|
|
1911
|
+
renderInfo('Spiral engine not available.');
|
|
1912
|
+
}
|
|
1913
|
+
break;
|
|
1914
|
+
case '/project': {
|
|
1915
|
+
const { analyzeProject } = await import('../context/project.js');
|
|
1916
|
+
const proj = await analyzeProject(process.cwd());
|
|
1917
|
+
renderInfo(`Project: ${proj.name} (${proj.type})`);
|
|
1918
|
+
if (proj.frameworks?.length)
|
|
1919
|
+
renderInfo(` Frameworks: ${proj.frameworks.join(', ')}`);
|
|
1920
|
+
renderInfo(` Files: ${proj.files?.length ?? 'unknown'}`);
|
|
1921
|
+
break;
|
|
1922
|
+
}
|
|
1923
|
+
case '/compact':
|
|
1924
|
+
if (spiralEngine) {
|
|
1925
|
+
const result = spiralEngine.evolve();
|
|
1926
|
+
renderInfo(`Evolution: ${result.promoted} promoted, ${result.demoted} demoted, ${result.summarized} summarized`);
|
|
1927
|
+
}
|
|
1928
|
+
else {
|
|
1929
|
+
renderInfo('Spiral engine not available.');
|
|
1930
|
+
}
|
|
1931
|
+
break;
|
|
1932
|
+
case '/tokens':
|
|
1933
|
+
renderInfo(`Session tokens: ${sessionTokens.input} in, ${sessionTokens.output} out (${sessionTokens.input + sessionTokens.output} total)`);
|
|
1934
|
+
renderInfo(`Tool calls: ${sessionToolCalls}`);
|
|
1935
|
+
renderInfo(`Checkpoints: ${checkpointStore.count}`);
|
|
1936
|
+
renderInfo(`Memory (snapshots): ${(checkpointStore.memoryUsage / 1024).toFixed(1)} KB`);
|
|
1937
|
+
renderInfo(`Session buffer: ${sessionBuffer.eventCount} events`);
|
|
1938
|
+
break;
|
|
1939
|
+
case '/yolo': {
|
|
1940
|
+
const arg = parts[1]?.toLowerCase();
|
|
1941
|
+
if (arg === 'on') {
|
|
1942
|
+
permissions.setYolo(true);
|
|
1943
|
+
renderInfo('YOLO mode ON \u2014 ALL operations auto-approved');
|
|
1944
|
+
}
|
|
1945
|
+
else if (arg === 'off') {
|
|
1946
|
+
permissions.setYolo(false);
|
|
1947
|
+
renderInfo('YOLO mode OFF');
|
|
1948
|
+
}
|
|
1949
|
+
else {
|
|
1950
|
+
renderInfo(`YOLO mode: ${permissions.isYolo() ? 'ON' : 'OFF'}`);
|
|
1951
|
+
}
|
|
1952
|
+
break;
|
|
1953
|
+
}
|
|
1954
|
+
case '/skip-permissions': {
|
|
1955
|
+
const arg = parts[1]?.toLowerCase();
|
|
1956
|
+
if (arg === 'on') {
|
|
1957
|
+
permissions.setSkipPermissions(true);
|
|
1958
|
+
renderInfo('Skip-permissions ON \u2014 write operations auto-approved (dangerous still asks)');
|
|
1959
|
+
}
|
|
1960
|
+
else if (arg === 'off') {
|
|
1961
|
+
permissions.setSkipPermissions(false);
|
|
1962
|
+
renderInfo('Skip-permissions OFF \u2014 write operations require confirmation');
|
|
1963
|
+
}
|
|
1964
|
+
else {
|
|
1965
|
+
renderInfo(`Skip-permissions: ${permissions.isSkipPermissions() ? 'ON' : 'OFF'}`);
|
|
1966
|
+
}
|
|
1967
|
+
break;
|
|
1968
|
+
}
|
|
1969
|
+
case '/undo': {
|
|
1970
|
+
const countArg = parts[1];
|
|
1971
|
+
if (countArg === 'list') {
|
|
1972
|
+
const entries = undoStack.list();
|
|
1973
|
+
if (entries.length === 0) {
|
|
1974
|
+
renderInfo('No undo history.');
|
|
1975
|
+
}
|
|
1976
|
+
else {
|
|
1977
|
+
renderInfo(`Undo history (${entries.length} entries):`);
|
|
1978
|
+
for (const entry of entries.slice(0, 10)) {
|
|
1979
|
+
const age = Math.round((Date.now() - entry.timestamp) / 1000);
|
|
1980
|
+
renderInfo(` ${entry.tool}: ${entry.path} (${age}s ago)`);
|
|
1981
|
+
}
|
|
1982
|
+
}
|
|
1983
|
+
}
|
|
1984
|
+
else {
|
|
1985
|
+
const count = parseInt(countArg) || 1;
|
|
1986
|
+
const result = undoStack.undo(count);
|
|
1987
|
+
if (result.undone === 0) {
|
|
1988
|
+
renderInfo('Nothing to undo.');
|
|
1989
|
+
}
|
|
1990
|
+
else {
|
|
1991
|
+
renderInfo(`Undone ${result.undone} change(s):`);
|
|
1992
|
+
for (const entry of result.entries) {
|
|
1993
|
+
renderInfo(` Reverted: ${entry.path}`);
|
|
1994
|
+
}
|
|
1995
|
+
}
|
|
1996
|
+
}
|
|
1997
|
+
break;
|
|
1998
|
+
}
|
|
1999
|
+
case '/validation': {
|
|
2000
|
+
const vArg = parts[1]?.toLowerCase();
|
|
2001
|
+
if (onValidation) {
|
|
2002
|
+
onValidation(vArg || 'status');
|
|
2003
|
+
}
|
|
2004
|
+
break;
|
|
2005
|
+
}
|
|
2006
|
+
case '/diff':
|
|
2007
|
+
try {
|
|
2008
|
+
const { execSync } = await import('node:child_process');
|
|
2009
|
+
const diff = execSync('git diff', { cwd: process.cwd(), encoding: 'utf-8' }).trim();
|
|
2010
|
+
if (diff) {
|
|
2011
|
+
process.stdout.write(`\n${diff}\n\n`);
|
|
2012
|
+
}
|
|
2013
|
+
else {
|
|
2014
|
+
renderInfo('No uncommitted changes.');
|
|
2015
|
+
}
|
|
2016
|
+
}
|
|
2017
|
+
catch {
|
|
2018
|
+
renderInfo('Not a git repository.');
|
|
2019
|
+
}
|
|
2020
|
+
break;
|
|
2021
|
+
case '/git':
|
|
2022
|
+
try {
|
|
2023
|
+
const { execSync } = await import('node:child_process');
|
|
2024
|
+
const status = execSync('git status --short', { cwd: process.cwd(), encoding: 'utf-8' }).trim();
|
|
2025
|
+
const branch = execSync('git branch --show-current', { cwd: process.cwd(), encoding: 'utf-8' }).trim();
|
|
2026
|
+
renderInfo(`Branch: ${branch}`);
|
|
2027
|
+
if (status) {
|
|
2028
|
+
process.stdout.write(`\n${status}\n\n`);
|
|
2029
|
+
}
|
|
2030
|
+
else {
|
|
2031
|
+
renderInfo('Working tree clean.');
|
|
2032
|
+
}
|
|
2033
|
+
}
|
|
2034
|
+
catch {
|
|
2035
|
+
renderInfo('Not a git repository.');
|
|
2036
|
+
}
|
|
2037
|
+
break;
|
|
2038
|
+
case '/export': {
|
|
2039
|
+
const outputDir = parts[1] || process.cwd();
|
|
2040
|
+
if (spiralEngine) {
|
|
2041
|
+
try {
|
|
2042
|
+
const { exportToZip } = await import('../brain/archive.js');
|
|
2043
|
+
const zipPath = exportToZip(spiralEngine, outputDir);
|
|
2044
|
+
renderInfo(`Exported to: ${zipPath}`);
|
|
2045
|
+
}
|
|
2046
|
+
catch (err) {
|
|
2047
|
+
renderError(`Export failed: ${err}`);
|
|
2048
|
+
}
|
|
2049
|
+
}
|
|
2050
|
+
else {
|
|
2051
|
+
renderInfo('Spiral engine not available.');
|
|
2052
|
+
}
|
|
2053
|
+
break;
|
|
2054
|
+
}
|
|
2055
|
+
case '/login': {
|
|
2056
|
+
const { loginCommand } = await import('./auth.js');
|
|
2057
|
+
await loginCommand({});
|
|
2058
|
+
return 'drain';
|
|
2059
|
+
}
|
|
2060
|
+
case '/logout': {
|
|
2061
|
+
const { logoutCommand } = await import('./auth.js');
|
|
2062
|
+
await logoutCommand({});
|
|
2063
|
+
return 'drain';
|
|
2064
|
+
}
|
|
2065
|
+
case '/whoami': {
|
|
2066
|
+
const { whoamiCommand } = await import('./auth.js');
|
|
2067
|
+
await whoamiCommand();
|
|
2068
|
+
return;
|
|
2069
|
+
}
|
|
2070
|
+
case '/exit':
|
|
2071
|
+
case '/quit': {
|
|
2072
|
+
// Stop live brain server
|
|
2073
|
+
try {
|
|
2074
|
+
const { stopLiveBrain } = await import('../brain/generator.js');
|
|
2075
|
+
stopLiveBrain();
|
|
2076
|
+
}
|
|
2077
|
+
catch { /* ignore */ }
|
|
2078
|
+
if (spiralEngine) {
|
|
2079
|
+
renderInfo('Saving state...');
|
|
2080
|
+
try {
|
|
2081
|
+
await spiralEngine.saveState(messages);
|
|
2082
|
+
}
|
|
2083
|
+
catch { /* best effort */ }
|
|
2084
|
+
}
|
|
2085
|
+
renderInfo('Goodbye!');
|
|
2086
|
+
return 'exit';
|
|
2087
|
+
}
|
|
2088
|
+
case '/auto':
|
|
2089
|
+
case '/dontstop': {
|
|
2090
|
+
if (!onAutonomous)
|
|
2091
|
+
break;
|
|
2092
|
+
// Extract goal text after "/auto " (e.g. "/auto fix all TypeScript errors")
|
|
2093
|
+
const autoGoal = input.replace(/^\/(auto|dontstop)\s*/i, '').trim() || undefined;
|
|
2094
|
+
if (autoGoal) {
|
|
2095
|
+
// Goal provided — start directly without confirmation menu
|
|
2096
|
+
await onAutonomous('start', autoGoal);
|
|
2097
|
+
}
|
|
2098
|
+
else {
|
|
2099
|
+
// No goal — show confirmation menu
|
|
2100
|
+
rl.pause();
|
|
2101
|
+
process.stdout.write('\n');
|
|
2102
|
+
const autoConfirm = await selectMenu([
|
|
2103
|
+
{ label: chalk.hex('#ff6600').bold('Start autonomous mode'), description: 'HelixMind will continuously scan and fix issues' },
|
|
2104
|
+
{ label: 'Cancel', description: 'Go back' },
|
|
2105
|
+
], { title: chalk.hex('#ff6600').bold('Autonomous Mode'), cancelLabel: 'Cancel' });
|
|
2106
|
+
rl.resume();
|
|
2107
|
+
if (autoConfirm === 0) {
|
|
2108
|
+
await onAutonomous('start');
|
|
2109
|
+
}
|
|
2110
|
+
else {
|
|
2111
|
+
renderInfo('Autonomous mode cancelled.');
|
|
2112
|
+
}
|
|
2113
|
+
}
|
|
2114
|
+
break;
|
|
2115
|
+
}
|
|
2116
|
+
case '/stop':
|
|
2117
|
+
if (onAutonomous) {
|
|
2118
|
+
await onAutonomous('stop');
|
|
2119
|
+
}
|
|
2120
|
+
else {
|
|
2121
|
+
renderInfo('No autonomous mode running.');
|
|
2122
|
+
}
|
|
2123
|
+
break;
|
|
2124
|
+
case '/security':
|
|
2125
|
+
if (onAutonomous) {
|
|
2126
|
+
await onAutonomous('security');
|
|
2127
|
+
}
|
|
2128
|
+
break;
|
|
2129
|
+
case '/sessions':
|
|
2130
|
+
case '/session': {
|
|
2131
|
+
if (!sessionManager)
|
|
2132
|
+
break;
|
|
2133
|
+
const subCmd = parts[1]?.toLowerCase();
|
|
2134
|
+
if (subCmd === 'close' || subCmd === 'remove') {
|
|
2135
|
+
const targetId = parts[2];
|
|
2136
|
+
if (!targetId) {
|
|
2137
|
+
renderInfo('Usage: /session close <id>');
|
|
2138
|
+
break;
|
|
2139
|
+
}
|
|
2140
|
+
const removed = sessionManager.remove(targetId);
|
|
2141
|
+
if (removed) {
|
|
2142
|
+
renderInfo(`Session ${targetId} closed.`);
|
|
2143
|
+
}
|
|
2144
|
+
else {
|
|
2145
|
+
renderInfo(`Session "${targetId}" not found or is the main session.`);
|
|
2146
|
+
}
|
|
2147
|
+
}
|
|
2148
|
+
else if (subCmd === 'stop') {
|
|
2149
|
+
const targetId = parts[2];
|
|
2150
|
+
if (targetId) {
|
|
2151
|
+
const session = sessionManager.get(targetId);
|
|
2152
|
+
if (session && session.status === 'running') {
|
|
2153
|
+
session.abort();
|
|
2154
|
+
renderInfo(`Stopped: ${session.icon} ${session.name}`);
|
|
2155
|
+
}
|
|
2156
|
+
else {
|
|
2157
|
+
renderInfo(`Session "${targetId}" not found or not running.`);
|
|
2158
|
+
}
|
|
2159
|
+
}
|
|
2160
|
+
else {
|
|
2161
|
+
// Stop all background
|
|
2162
|
+
sessionManager.abortAll();
|
|
2163
|
+
renderInfo('All background sessions stopped.');
|
|
2164
|
+
}
|
|
2165
|
+
}
|
|
2166
|
+
else if (subCmd === 'switch') {
|
|
2167
|
+
const targetId = parts[2];
|
|
2168
|
+
if (targetId && sessionManager.switchTo(targetId)) {
|
|
2169
|
+
const s = sessionManager.active;
|
|
2170
|
+
renderInfo(`Switched to: ${s.icon} ${s.name}`);
|
|
2171
|
+
// Replay captured output
|
|
2172
|
+
if (s.output.length > 0) {
|
|
2173
|
+
process.stdout.write('\n' + chalk.dim('--- Session output ---') + '\n');
|
|
2174
|
+
for (const line of s.output.slice(-20)) {
|
|
2175
|
+
process.stdout.write(' ' + chalk.dim(line) + '\n');
|
|
2176
|
+
}
|
|
2177
|
+
process.stdout.write(chalk.dim('--- End ---') + '\n');
|
|
2178
|
+
}
|
|
2179
|
+
}
|
|
2180
|
+
else {
|
|
2181
|
+
renderInfo(`Session "${targetId || '?'}" not found.`);
|
|
2182
|
+
}
|
|
2183
|
+
}
|
|
2184
|
+
else {
|
|
2185
|
+
// Show session list
|
|
2186
|
+
process.stdout.write(renderSessionList(sessionManager.all, sessionManager.activeId));
|
|
2187
|
+
}
|
|
2188
|
+
break;
|
|
2189
|
+
}
|
|
2190
|
+
case '/local': {
|
|
2191
|
+
// Local LLM setup via Ollama
|
|
2192
|
+
rl.pause();
|
|
2193
|
+
const { isOllamaRunning, listOllamaModels, pullOllamaModel, formatModelSize, RECOMMENDED_MODELS } = await import('../providers/ollama.js');
|
|
2194
|
+
process.stdout.write('\n');
|
|
2195
|
+
const d = chalk.dim;
|
|
2196
|
+
const c = chalk.hex('#00d4ff');
|
|
2197
|
+
// Step 1: Check if Ollama is running
|
|
2198
|
+
const running = await isOllamaRunning();
|
|
2199
|
+
if (!running) {
|
|
2200
|
+
process.stdout.write(d('\u256D\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256E') + '\n' +
|
|
2201
|
+
d('\u2502 ') + chalk.yellow('\u26A0 Ollama not detected') + d(' \u2502') + '\n' +
|
|
2202
|
+
d('\u2502') + d(' \u2502') + '\n' +
|
|
2203
|
+
d('\u2502 ') + '1. Install: ' + c('https://ollama.com') + d(' \u2502') + '\n' +
|
|
2204
|
+
d('\u2502 ') + '2. Start: ' + c('ollama serve') + d(' \u2502') + '\n' +
|
|
2205
|
+
d('\u2502 ') + '3. Run: ' + c('/local') + ' again' + d(' \u2502') + '\n' +
|
|
2206
|
+
d('\u2502') + d(' \u2502') + '\n' +
|
|
2207
|
+
d('\u2570\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256F') + '\n\n');
|
|
2208
|
+
rl.resume();
|
|
2209
|
+
break;
|
|
2210
|
+
}
|
|
2211
|
+
// Step 2: Get installed models
|
|
2212
|
+
const installed = await listOllamaModels();
|
|
2213
|
+
const installedNames = new Set(installed.map(m => m.name));
|
|
2214
|
+
// Build menu: installed models first, then recommended to download
|
|
2215
|
+
const menuItems = [];
|
|
2216
|
+
const menuActions = [];
|
|
2217
|
+
if (installed.length > 0) {
|
|
2218
|
+
menuItems.push({ label: c.bold('Installed Models'), disabled: true });
|
|
2219
|
+
menuActions.push({ action: 'use', model: '' });
|
|
2220
|
+
for (const m of installed) {
|
|
2221
|
+
const size = formatModelSize(m.size);
|
|
2222
|
+
const quant = m.details?.quantization_level || '';
|
|
2223
|
+
const active = config.provider === 'ollama' && config.model === m.name;
|
|
2224
|
+
menuItems.push({
|
|
2225
|
+
label: theme.primary(m.name),
|
|
2226
|
+
description: `${size} ${quant}`,
|
|
2227
|
+
marker: active ? chalk.green('\u25C0 active') : undefined,
|
|
2228
|
+
});
|
|
2229
|
+
menuActions.push({ action: 'use', model: m.name });
|
|
2230
|
+
}
|
|
2231
|
+
}
|
|
2232
|
+
// Recommended models not yet installed
|
|
2233
|
+
const notInstalled = RECOMMENDED_MODELS.filter(r => !installedNames.has(r.name));
|
|
2234
|
+
if (notInstalled.length > 0) {
|
|
2235
|
+
menuItems.push({ label: '', disabled: true });
|
|
2236
|
+
menuActions.push({ action: 'pull', model: '' });
|
|
2237
|
+
menuItems.push({ label: chalk.hex('#00ff88').bold('Download New Model'), disabled: true });
|
|
2238
|
+
menuActions.push({ action: 'pull', model: '' });
|
|
2239
|
+
for (const r of notInstalled) {
|
|
2240
|
+
menuItems.push({
|
|
2241
|
+
label: chalk.hex('#00ff88')(r.name),
|
|
2242
|
+
description: `${r.size} \u2014 ${r.description}`,
|
|
2243
|
+
});
|
|
2244
|
+
menuActions.push({ action: 'pull', model: r.name });
|
|
2245
|
+
}
|
|
2246
|
+
}
|
|
2247
|
+
menuItems.push({ label: '', disabled: true });
|
|
2248
|
+
menuActions.push({ action: 'use', model: '' });
|
|
2249
|
+
menuItems.push({
|
|
2250
|
+
label: d(`Ollama running \u2713 | ${installed.length} model(s) installed`),
|
|
2251
|
+
disabled: true,
|
|
2252
|
+
});
|
|
2253
|
+
menuActions.push({ action: 'use', model: '' });
|
|
2254
|
+
const idx = await selectMenu(menuItems, {
|
|
2255
|
+
title: c.bold('\u{1F916} Local LLM Setup (Ollama)'),
|
|
2256
|
+
cancelLabel: 'Back',
|
|
2257
|
+
pageSize: 14,
|
|
2258
|
+
});
|
|
2259
|
+
rl.resume();
|
|
2260
|
+
if (idx < 0 || !menuActions[idx]?.model)
|
|
2261
|
+
break;
|
|
2262
|
+
const selected = menuActions[idx];
|
|
2263
|
+
if (selected.action === 'use') {
|
|
2264
|
+
// Switch to Ollama with selected model
|
|
2265
|
+
store.addProvider('ollama', 'ollama', 'http://localhost:11434/v1');
|
|
2266
|
+
store.switchProvider('ollama', selected.model);
|
|
2267
|
+
config = store.getAll();
|
|
2268
|
+
try {
|
|
2269
|
+
const newProvider = createProvider('ollama', 'ollama', selected.model, 'http://localhost:11434/v1');
|
|
2270
|
+
onProviderSwitch?.(newProvider);
|
|
2271
|
+
renderInfo(`\u2705 Switched to local: ${chalk.bold(selected.model)}`);
|
|
2272
|
+
}
|
|
2273
|
+
catch (err) {
|
|
2274
|
+
renderError(`Failed to switch: ${err}`);
|
|
2275
|
+
}
|
|
2276
|
+
}
|
|
2277
|
+
else if (selected.action === 'pull') {
|
|
2278
|
+
// Download model
|
|
2279
|
+
renderInfo(`\u{2B07}\uFE0F Downloading ${chalk.bold(selected.model)}...`);
|
|
2280
|
+
process.stdout.write(d(' This may take a few minutes depending on model size.\n\n'));
|
|
2281
|
+
let lastPct = -1;
|
|
2282
|
+
const success = await pullOllamaModel(selected.model, (status, completed, total) => {
|
|
2283
|
+
if (completed && total && total > 0) {
|
|
2284
|
+
const pct = Math.round((completed / total) * 100);
|
|
2285
|
+
if (pct !== lastPct) {
|
|
2286
|
+
lastPct = pct;
|
|
2287
|
+
const bar = '\u2588'.repeat(Math.floor(pct / 5)) + '\u2591'.repeat(20 - Math.floor(pct / 5));
|
|
2288
|
+
process.stdout.write(`\r ${c(bar)} ${pct}% ${d(status || '')}`);
|
|
2289
|
+
}
|
|
2290
|
+
}
|
|
2291
|
+
else if (status) {
|
|
2292
|
+
process.stdout.write(`\r\x1b[K ${d(status)}`);
|
|
2293
|
+
}
|
|
2294
|
+
});
|
|
2295
|
+
process.stdout.write('\r\x1b[K');
|
|
2296
|
+
if (success) {
|
|
2297
|
+
renderInfo(`\u2705 Downloaded ${chalk.bold(selected.model)}`);
|
|
2298
|
+
// Auto-switch to the new model
|
|
2299
|
+
store.addProvider('ollama', 'ollama', 'http://localhost:11434/v1');
|
|
2300
|
+
store.switchProvider('ollama', selected.model);
|
|
2301
|
+
config = store.getAll();
|
|
2302
|
+
try {
|
|
2303
|
+
const newProvider = createProvider('ollama', 'ollama', selected.model, 'http://localhost:11434/v1');
|
|
2304
|
+
onProviderSwitch?.(newProvider);
|
|
2305
|
+
renderInfo(`\u2705 Active model: ${chalk.bold(selected.model)}`);
|
|
2306
|
+
}
|
|
2307
|
+
catch (err) {
|
|
2308
|
+
renderError(`Model downloaded but failed to switch: ${err}`);
|
|
2309
|
+
}
|
|
2310
|
+
}
|
|
2311
|
+
else {
|
|
2312
|
+
renderError(`Failed to download ${selected.model}. Check Ollama logs.`);
|
|
2313
|
+
}
|
|
2314
|
+
}
|
|
2315
|
+
break;
|
|
2316
|
+
}
|
|
2317
|
+
default:
|
|
2318
|
+
renderError(`Unknown command: ${cmd}. Type /help for available commands.`);
|
|
2319
|
+
}
|
|
2320
|
+
}
|
|
2321
|
+
//# sourceMappingURL=chat.js.map
|