@polderlabs/bizar 6.2.4 → 6.3.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.
- package/.claude/CLAUDE.md +15 -0
- package/.claude/agents/_shared/AGENT_BASELINE.md +168 -0
- package/.claude/agents/_shared/CLAUDE_TOOLS.md +412 -0
- package/.claude/agents/_shared/SKILLS.md +109 -0
- package/.claude/agents/agent-browser.md +80 -0
- package/.claude/agents/baldr.md +49 -0
- package/.claude/agents/forseti.md +51 -0
- package/.claude/agents/frigg.md +42 -0
- package/.claude/agents/heimdall.md +33 -0
- package/.claude/agents/hermod.md +53 -0
- package/.claude/agents/mimir.md +49 -0
- package/.claude/agents/odin.md +287 -0
- package/.claude/agents/quick.md +34 -0
- package/.claude/agents/semble-search.md +50 -0
- package/.claude/agents/thor.md +53 -0
- package/.claude/agents/tyr.md +56 -0
- package/.claude/agents/vidarr.md +54 -0
- package/.claude/agents/vor.md +53 -0
- package/.claude/commands/audit.md +25 -0
- package/.claude/commands/bizar.md +22 -0
- package/.claude/commands/explain.md +17 -0
- package/.claude/commands/init.md +29 -0
- package/.claude/commands/learn.md +44 -0
- package/.claude/commands/plan.md +35 -0
- package/.claude/commands/plow-through.md +50 -0
- package/.claude/commands/pr-review.md +49 -0
- package/.claude/commands/setup-provider.md +96 -0
- package/.claude/commands/tailscale-serve.md +100 -0
- package/.claude/commands/team.md +132 -0
- package/.claude/commands/test.md +62 -0
- package/.claude/commands/validate.md +68 -0
- package/.claude/commands/visual-plan.md +24 -0
- package/.claude/hooks/README.md +92 -0
- package/.claude/hooks/posttooluse-editwrite.mjs +91 -0
- package/.claude/hooks/pretooluse-bash.mjs +81 -0
- package/.claude/hooks/pretooluse-editwrite.mjs +139 -0
- package/.claude/hooks/sessionend-recall.mjs +74 -0
- package/.claude/hooks/sessionstart-prime.mjs +80 -0
- package/.claude/hooks/userpromptsubmit-tag.mjs +80 -0
- package/.claude/settings.json +116 -0
- package/.claude/skills/9router/SKILL.md +80 -0
- package/.claude/skills/9router-chat/SKILL.md +73 -0
- package/.claude/skills/9router-embeddings/SKILL.md +69 -0
- package/.claude/skills/9router-image/SKILL.md +86 -0
- package/.claude/skills/9router-stt/SKILL.md +79 -0
- package/.claude/skills/9router-tts/SKILL.md +80 -0
- package/.claude/skills/9router-web-fetch/SKILL.md +99 -0
- package/.claude/skills/9router-web-search/SKILL.md +91 -0
- package/.claude/skills/bizar/README.md +9 -0
- package/.claude/skills/bizar/SKILL.md +450 -0
- package/.claude/skills/cpp-coding-standards/README.md +28 -0
- package/.claude/skills/cpp-coding-standards/SKILL.md +634 -0
- package/.claude/skills/cpp-coding-standards/references/concurrency.md +320 -0
- package/.claude/skills/cpp-coding-standards/references/error-handling.md +229 -0
- package/.claude/skills/cpp-coding-standards/references/memory-safety.md +216 -0
- package/.claude/skills/cpp-coding-standards/references/modern-idioms.md +282 -0
- package/.claude/skills/cpp-coding-standards/references/review-checklist.md +96 -0
- package/.claude/skills/cpp-testing/README.md +28 -0
- package/.claude/skills/cpp-testing/SKILL.md +304 -0
- package/.claude/skills/cpp-testing/references/coverage.md +370 -0
- package/.claude/skills/cpp-testing/references/framework-compare.md +175 -0
- package/.claude/skills/cpp-testing/references/host-test-for-embedded.md +499 -0
- package/.claude/skills/cpp-testing/references/mocking.md +364 -0
- package/.claude/skills/cpp-testing/references/tdd-workflow.md +308 -0
- package/.claude/skills/cubesandbox/SKILL.md +148 -0
- package/.claude/skills/embedded-esp-idf/README.md +41 -0
- package/.claude/skills/embedded-esp-idf/SKILL.md +439 -0
- package/.claude/skills/embedded-esp-idf/references/freertos-patterns.md +214 -0
- package/.claude/skills/embedded-esp-idf/references/host-tests.md +164 -0
- package/.claude/skills/embedded-esp-idf/references/idf-py-commands.md +157 -0
- package/.claude/skills/embedded-esp-idf/references/kconfig.md +159 -0
- package/.claude/skills/embedded-esp-idf/references/logging-discipline.md +118 -0
- package/.claude/skills/embedded-esp-idf/references/memory-and-iram.md +137 -0
- package/.claude/skills/embedded-esp-idf/references/nvs.md +121 -0
- package/.claude/skills/embedded-esp-idf/references/packed-structs.md +192 -0
- package/.claude/skills/embedded-esp-idf/scripts/idf_env.sh +47 -0
- package/.claude/skills/embedded-esp-idf/scripts/size_check.sh +77 -0
- package/.claude/skills/glyph/SKILL.md +163 -0
- package/.claude/skills/harness-engineering/SKILL.md +143 -0
- package/.claude/skills/lightrag/SKILL.md +81 -0
- package/.claude/skills/memory-protocol/SKILL.md +105 -0
- package/.claude/skills/obsidian/SKILL.md +306 -0
- package/.claude/skills/read-the-damn-docs/SKILL.md +113 -0
- package/.claude/skills/self-improvement/SKILL.md +64 -0
- package/README.md +87 -59
- package/bizar-dash/dist/assets/{EnvVarsSection-DXM8gRm_.js → EnvVarsSection-B58aiJiE.js} +1 -1
- package/bizar-dash/dist/assets/{EnvVarsSection-DXM8gRm_.js.map → EnvVarsSection-B58aiJiE.js.map} +1 -1
- package/bizar-dash/dist/assets/{MobileChat-BnKN_Ks_.js → MobileChat-BJrqwVDd.js} +1 -1
- package/bizar-dash/dist/assets/{MobileChat-BnKN_Ks_.js.map → MobileChat-BJrqwVDd.js.map} +1 -1
- package/bizar-dash/dist/assets/{MobileSettings-DjCPxC-Q.js → MobileSettings-CEQNJNLJ.js} +1 -1
- package/bizar-dash/dist/assets/{MobileSettings-DjCPxC-Q.js.map → MobileSettings-CEQNJNLJ.js.map} +1 -1
- package/bizar-dash/dist/assets/{main-DYiZqMrn.js → main-IvfQAOfy.js} +1 -1
- package/bizar-dash/dist/assets/{main-DYiZqMrn.js.map → main-IvfQAOfy.js.map} +1 -1
- package/bizar-dash/dist/assets/{markdown-C6mXtQxD.js → markdown-tOLaD6nm.js} +1 -1
- package/bizar-dash/dist/assets/{markdown-C6mXtQxD.js.map → markdown-tOLaD6nm.js.map} +1 -1
- package/bizar-dash/dist/assets/{mobile-CMHqtLV2.js → mobile-DYCHcUpq.js} +1 -1
- package/bizar-dash/dist/assets/{mobile-CMHqtLV2.js.map → mobile-DYCHcUpq.js.map} +1 -1
- package/bizar-dash/dist/assets/{mobile-layout-3jIhHX_p.js → mobile-layout-CBHjpwsb.js} +2 -2
- package/bizar-dash/dist/assets/{mobile-layout-3jIhHX_p.js.map → mobile-layout-CBHjpwsb.js.map} +1 -1
- package/bizar-dash/dist/assets/{useSlashCommands-DtITw8Xv.js → useSlashCommands-Bd7_FA6U.js} +2 -2
- package/bizar-dash/dist/assets/{useSlashCommands-DtITw8Xv.js.map → useSlashCommands-Bd7_FA6U.js.map} +1 -1
- package/bizar-dash/dist/assets/{vendor-CeHGtduv.js → vendor-C843201K.js} +12 -12
- package/bizar-dash/dist/assets/vendor-C843201K.js.map +1 -0
- package/bizar-dash/dist/index.html +6 -6
- package/bizar-dash/dist/mobile.html +2 -2
- package/bizar-dash/node_modules/.vite/vitest/da39a3ee5e6b4b0d3255bfef95601890afd80709/results.json +1 -1
- package/bizar-dash/src/server/api.mjs +4 -4
- package/bizar-dash/src/server/background-store.mjs +15 -16
- package/bizar-dash/src/server/bg-poller.mjs +25 -35
- package/bizar-dash/src/server/bg-retry.mjs +66 -194
- package/bizar-dash/src/server/claude-bg-spawner.mjs +390 -0
- package/bizar-dash/src/server/claude-info.mjs +412 -0
- package/bizar-dash/src/server/{cline-runner.mjs → claude-runner.mjs} +53 -48
- package/bizar-dash/src/server/claude-sdk.mjs +191 -0
- package/bizar-dash/src/server/providers-store.mjs +22 -49
- package/bizar-dash/src/server/routes/background.mjs +24 -20
- package/bizar-dash/src/server/routes/chat.mjs +223 -326
- package/bizar-dash/src/server/routes/claude-session-detail.mjs +329 -0
- package/bizar-dash/src/server/routes/claude-sessions.mjs +259 -0
- package/bizar-dash/src/server/routes/tasks.mjs +20 -28
- package/bizar-dash/src/server/task-delegator.mjs +76 -77
- package/bizar-dash/src/web/components/chat/useChat.ts +23 -27
- package/bizar-dash/src/web/views/Chat.tsx +8 -8
- package/cli/bin.mjs +31 -18
- package/cli/commands/claude-cmd.mjs +348 -0
- package/cli/commands/install.mjs +12 -11
- package/cli/commands/sandbox.mjs +220 -0
- package/cli/commands/validate.mjs +252 -288
- package/cli/dev-link.test.mjs +11 -11
- package/cli/doctor.mjs +131 -213
- package/cli/install.mjs +33 -37
- package/cli/install.test.mjs +9 -9
- package/cli/provision-claude.mjs +893 -0
- package/cli/provision.mjs +330 -226
- package/cli/provision.test.mjs +102 -0
- package/cli/utils.mjs +72 -58
- package/config/AGENTS.md +28 -28
- package/config/agents/_shared/AGENT_BASELINE.md +104 -684
- package/config/skills/bizar/SKILL.md +197 -0
- package/config/skills/cubesandbox/SKILL.md +148 -0
- package/config/skills/harness-engineering/SKILL.md +142 -0
- package/install.sh +66 -44
- package/package.json +19 -11
- package/packages/sdk/ARCHITECTURE.md +40 -36
- package/packages/sdk/package-lock.json +32 -0
- package/packages/sdk/package.json +15 -5
- package/{plugins/bizar → packages/sdk}/src/dangerous-patterns.ts +6 -28
- package/packages/sdk/src/fingerprint.ts +0 -0
- package/packages/sdk/src/index.ts +38 -73
- package/packages/sdk/src/mcp/bin.ts +51 -0
- package/packages/sdk/src/mcp/server.ts +498 -0
- package/{plugins/bizar/src/memory-vault.ts → packages/sdk/src/memory/index.ts} +18 -21
- package/packages/sdk/tests/sdk.test.mjs +148 -0
- package/packages/sdk/vitest.config.ts +1 -1
- package/scripts/bh-full-e2e.mjs +166 -363
- package/scripts/check-deps.mjs +28 -46
- package/scripts/mirror-claude-md.sh +78 -0
- package/scripts/test-in-container.sh +157 -0
- package/templates/clean-state-checklist.md +3 -3
- package/templates/sprint-contract.md +2 -2
- package/bizar-dash/.bizar/activity.log +0 -3
- package/bizar-dash/.omx/logs/omx-2026-07-07.jsonl +0 -1
- package/bizar-dash/.omx/state/session.json +0 -10
- package/bizar-dash/bizar-design/EXPLANATION.md +0 -307
- package/bizar-dash/bizar-design/INSTRUCTIONS.md +0 -149
- package/bizar-dash/bizar-design/canvas.html +0 -281
- package/bizar-dash/bizar-design/canvas.html.artifact.json +0 -19
- package/bizar-dash/bizar-design/components.css +0 -1665
- package/bizar-dash/bizar-design/components.html +0 -557
- package/bizar-dash/bizar-design/components.html.artifact.json +0 -19
- package/bizar-dash/bizar-design/desktop/agents.html +0 -74
- package/bizar-dash/bizar-design/desktop/agents.html.artifact.json +0 -19
- package/bizar-dash/bizar-design/desktop/memory.html +0 -75
- package/bizar-dash/bizar-design/desktop/memory.html.artifact.json +0 -19
- package/bizar-dash/bizar-design/desktop/metrics.html +0 -63
- package/bizar-dash/bizar-design/desktop/metrics.html.artifact.json +0 -19
- package/bizar-dash/bizar-design/desktop/overview.html +0 -95
- package/bizar-dash/bizar-design/desktop/overview.html.artifact.json +0 -19
- package/bizar-dash/bizar-design/desktop/settings.html +0 -70
- package/bizar-dash/bizar-design/desktop/settings.html.artifact.json +0 -19
- package/bizar-dash/bizar-design/icons.svg +0 -44
- package/bizar-dash/bizar-design/index.html +0 -192
- package/bizar-dash/bizar-design/index.html.artifact.json +0 -19
- package/bizar-dash/bizar-design/mobile/agents.html +0 -29
- package/bizar-dash/bizar-design/mobile/agents.html.artifact.json +0 -19
- package/bizar-dash/bizar-design/mobile/memory.html +0 -28
- package/bizar-dash/bizar-design/mobile/memory.html.artifact.json +0 -19
- package/bizar-dash/bizar-design/mobile/metrics.html +0 -29
- package/bizar-dash/bizar-design/mobile/metrics.html.artifact.json +0 -19
- package/bizar-dash/bizar-design/mobile/overview.html +0 -33
- package/bizar-dash/bizar-design/mobile/overview.html.artifact.json +0 -19
- package/bizar-dash/bizar-design/mobile/settings.html +0 -28
- package/bizar-dash/bizar-design/mobile/settings.html.artifact.json +0 -19
- package/bizar-dash/bizar-design/prototype.js +0 -114
- package/bizar-dash/bizar-design/tokens.css +0 -106
- package/bizar-dash/dist/assets/vendor-CeHGtduv.js.map +0 -1
- package/bizar-dash/node_modules/.package-lock.json +0 -6
- package/bizar-dash/package-lock.json +0 -6
- package/bizar-dash/src/server/cline-sdk.mjs +0 -132
- package/bizar-dash/src/server/routes/cline-session-detail.mjs +0 -559
- package/bizar-dash/src/server/routes/cline-sessions.mjs +0 -291
- package/cli/commands/cline-cmd.mjs +0 -289
- package/cli/commands/validate.test.mjs +0 -344
- package/cli/doctor.test.mjs +0 -350
- package/config/cline.json.template +0 -342
- package/packages/sdk/src/client.ts +0 -188
- package/packages/sdk/src/cline-events.ts +0 -134
- package/packages/sdk/src/cline-types.ts +0 -66
- package/packages/sdk/src/cline.ts +0 -339
- package/packages/sdk/src/errors.ts +0 -129
- package/packages/sdk/src/events.ts +0 -153
- package/packages/sdk/src/types.ts +0 -176
- package/packages/sdk/tests/client.test.ts +0 -217
- package/packages/sdk/tests/errors.test.ts +0 -108
- package/packages/sdk/tests/events.test.ts +0 -139
- package/packages/sdk/tests/fixtures/fetch-mock.ts +0 -152
- package/packages/sdk/tests/fixtures/sse-mock.ts +0 -30
- package/plugins/bizar/ARCHITECTURE.md +0 -142
- package/plugins/bizar/CONSTRAINTS.md +0 -67
- package/plugins/bizar/LICENSE +0 -21
- package/plugins/bizar/README.md +0 -448
- package/plugins/bizar/index.ts +0 -872
- package/plugins/bizar/package.json +0 -39
- package/plugins/bizar/scripts/check-forbidden-imports.sh +0 -33
- package/plugins/bizar/src/background-state.ts +0 -641
- package/plugins/bizar/src/background.ts +0 -1806
- package/plugins/bizar/src/cline-runner.ts +0 -203
- package/plugins/bizar/src/clineruntime.ts +0 -227
- package/plugins/bizar/src/commands-impl.ts +0 -151
- package/plugins/bizar/src/commands.ts +0 -1799
- package/plugins/bizar/src/compaction.d.mts +0 -48
- package/plugins/bizar/src/compaction.mjs +0 -192
- package/plugins/bizar/src/dashboard-client.ts +0 -233
- package/plugins/bizar/src/event-stream.ts +0 -606
- package/plugins/bizar/src/fingerprint.ts +0 -120
- package/plugins/bizar/src/handoff.ts +0 -79
- package/plugins/bizar/src/hooks/memory-flush-on-compact.ts +0 -123
- package/plugins/bizar/src/hooks/memory-inject.ts +0 -247
- package/plugins/bizar/src/hooks/memory-write-on-end.ts +0 -188
- package/plugins/bizar/src/hooks/skill-curator.ts +0 -180
- package/plugins/bizar/src/http-client.ts +0 -467
- package/plugins/bizar/src/key-rotation.ts +0 -218
- package/plugins/bizar/src/logger.ts +0 -144
- package/plugins/bizar/src/loop-engineering.ts +0 -241
- package/plugins/bizar/src/loop.ts +0 -176
- package/plugins/bizar/src/mistake-recovery.ts +0 -98
- package/plugins/bizar/src/odin.ts +0 -227
- package/plugins/bizar/src/options.ts +0 -470
- package/plugins/bizar/src/plan-fs.ts +0 -323
- package/plugins/bizar/src/reasoning-clean.ts +0 -454
- package/plugins/bizar/src/report.ts +0 -178
- package/plugins/bizar/src/research-prompt.ts +0 -35
- package/plugins/bizar/src/serve-info.ts +0 -228
- package/plugins/bizar/src/serve.ts +0 -496
- package/plugins/bizar/src/settings.ts +0 -349
- package/plugins/bizar/src/state.ts +0 -298
- package/plugins/bizar/src/tool-discipline.ts +0 -105
- package/plugins/bizar/src/tools/agent-browser.ts +0 -315
- package/plugins/bizar/src/tools/bg-collect.ts +0 -131
- package/plugins/bizar/src/tools/bg-get-comments.ts +0 -266
- package/plugins/bizar/src/tools/bg-kill.ts +0 -116
- package/plugins/bizar/src/tools/bg-pause.ts +0 -99
- package/plugins/bizar/src/tools/bg-report-progress.ts +0 -115
- package/plugins/bizar/src/tools/bg-resume.ts +0 -94
- package/plugins/bizar/src/tools/bg-send-message.ts +0 -223
- package/plugins/bizar/src/tools/bg-spawn.ts +0 -502
- package/plugins/bizar/src/tools/bg-status.ts +0 -130
- package/plugins/bizar/src/tools/graph-query.ts +0 -278
- package/plugins/bizar/src/tools/loop-engineering.ts +0 -193
- package/plugins/bizar/src/tools/memory-list.ts +0 -43
- package/plugins/bizar/src/tools/memory-read.ts +0 -69
- package/plugins/bizar/src/tools/memory-search.ts +0 -47
- package/plugins/bizar/src/tools/memory-write.ts +0 -54
- package/plugins/bizar/src/tools/open-kb.ts +0 -198
- package/plugins/bizar/src/tools/plan-action.ts +0 -785
- package/plugins/bizar/src/tools/read-glyph-feedback.ts +0 -191
- package/plugins/bizar/src/tools/team-spawn.ts +0 -73
- package/plugins/bizar/src/tools/team-status.ts +0 -76
- package/plugins/bizar/src/tools/wait-for-feedback.ts +0 -415
- package/plugins/bizar/src/trajectory.ts +0 -104
- package/plugins/bizar/tests/README.md +0 -99
- package/plugins/bizar/tests/attach-handler-bug.test.ts +0 -169
- package/plugins/bizar/tests/background-state.test.ts +0 -277
- package/plugins/bizar/tests/background.test.ts +0 -402
- package/plugins/bizar/tests/block.test.ts +0 -195
- package/plugins/bizar/tests/canonical-key-order.test.ts +0 -75
- package/plugins/bizar/tests/clineruntime-config.test.ts +0 -283
- package/plugins/bizar/tests/commands-impl.test.ts +0 -316
- package/plugins/bizar/tests/commands.test.ts +0 -584
- package/plugins/bizar/tests/compaction.test.ts +0 -264
- package/plugins/bizar/tests/config.test.ts +0 -128
- package/plugins/bizar/tests/dashboard-client.test.ts +0 -159
- package/plugins/bizar/tests/dispose.test.ts +0 -336
- package/plugins/bizar/tests/event-stream.test.ts +0 -409
- package/plugins/bizar/tests/event.test.ts +0 -262
- package/plugins/bizar/tests/fingerprint.test.ts +0 -162
- package/plugins/bizar/tests/http-client.test.ts +0 -404
- package/plugins/bizar/tests/init-helpers.test.ts +0 -203
- package/plugins/bizar/tests/integration/slash-command.test.ts +0 -349
- package/plugins/bizar/tests/integration/tool-routing.test.ts +0 -98
- package/plugins/bizar/tests/key-rotation.test.ts +0 -396
- package/plugins/bizar/tests/loop-engineering.test.ts +0 -168
- package/plugins/bizar/tests/loop.test.ts +0 -397
- package/plugins/bizar/tests/memory-write-on-end.test.ts +0 -92
- package/plugins/bizar/tests/mistake-recovery.test.ts +0 -116
- package/plugins/bizar/tests/odin.test.ts +0 -125
- package/plugins/bizar/tests/options.test.ts +0 -329
- package/plugins/bizar/tests/reasoning-clean.test.ts +0 -422
- package/plugins/bizar/tests/safety.test.ts +0 -256
- package/plugins/bizar/tests/serve.test.ts +0 -339
- package/plugins/bizar/tests/settings.test.ts +0 -351
- package/plugins/bizar/tests/stall-think.test.ts +0 -750
- package/plugins/bizar/tests/state.test.ts +0 -276
- package/plugins/bizar/tests/tool-discipline.test.ts +0 -77
- package/plugins/bizar/tests/tools/agent-browser.test.ts +0 -98
- package/plugins/bizar/tests/tools/bg-collect.test.ts +0 -337
- package/plugins/bizar/tests/tools/bg-get-comments.test.ts +0 -485
- package/plugins/bizar/tests/tools/bg-kill.test.ts +0 -235
- package/plugins/bizar/tests/tools/bg-pause.test.ts +0 -61
- package/plugins/bizar/tests/tools/bg-report-progress.test.ts +0 -79
- package/plugins/bizar/tests/tools/bg-resume.test.ts +0 -40
- package/plugins/bizar/tests/tools/bg-send-message.test.ts +0 -116
- package/plugins/bizar/tests/tools/bg-spawn-delegation.test.ts +0 -147
- package/plugins/bizar/tests/tools/bg-spawn-http.test.ts +0 -233
- package/plugins/bizar/tests/tools/bg-spawn.test.ts +0 -311
- package/plugins/bizar/tests/tools/bg-status.test.ts +0 -217
- package/plugins/bizar/tests/tools/cline-runner.test.ts +0 -115
- package/plugins/bizar/tests/tools/plan-action.test.ts +0 -599
- package/plugins/bizar/tests/tools/read-glyph-feedback.test.ts +0 -253
- package/plugins/bizar/tests/tools/wait-for-feedback.test.ts +0 -390
- package/plugins/bizar/tests/update-deadlock.test.ts +0 -151
- package/plugins/bizar/tsconfig.json +0 -29
|
@@ -0,0 +1,329 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* src/server/routes/claude-session-detail.mjs
|
|
3
|
+
*
|
|
4
|
+
* v6.3.0 — Claude Code-native session detail endpoints.
|
|
5
|
+
*
|
|
6
|
+
* GET /api/claude-sessions/:id/messages — read message history
|
|
7
|
+
* POST /api/claude-sessions/:id/send — send a follow-up prompt
|
|
8
|
+
* GET /api/claude-sessions/:id/stream — SSE event stream
|
|
9
|
+
*
|
|
10
|
+
* Replaces routes/cline-session-detail.mjs (which proxied Cline's
|
|
11
|
+
* HTTP `/api/session/:id/message` and `/event` endpoints). Claude
|
|
12
|
+
* Code has no equivalent HTTP API, so:
|
|
13
|
+
*
|
|
14
|
+
* - `GET .../messages` — reads `~/.claude/sessions/<id>/messages.jsonl`
|
|
15
|
+
* and normalizes via `claude-info.normalizeClaudeMessage`.
|
|
16
|
+
* - `POST .../send` — spawns `claude --resume <id> -p "<text>"`
|
|
17
|
+
* via `claude-runner.mjs` to fire a follow-up.
|
|
18
|
+
* - `GET .../stream` — subscribes to the SDK via
|
|
19
|
+
* `claude-sdk.subscribeToSession()` (preferred)
|
|
20
|
+
* and falls back to tailing messages.jsonl.
|
|
21
|
+
*
|
|
22
|
+
* The wire format on the SSE endpoint matches the v4.2.4 cline
|
|
23
|
+
* contract so the React chat UI doesn't need to change.
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
import { Router } from 'express';
|
|
27
|
+
import { tracer, withSpan, setCommonAttributes } from '../otel.mjs';
|
|
28
|
+
import { child as loggerChild } from '../logger.mjs';
|
|
29
|
+
import { recordTrace } from '../metrics.mjs';
|
|
30
|
+
import { wrap } from './_shared.mjs';
|
|
31
|
+
import { listClaudeMessages, normalizeClaudeMessage } from '../claude-info.mjs';
|
|
32
|
+
import { subscribeToSession } from '../claude-sdk.mjs';
|
|
33
|
+
|
|
34
|
+
const logger = loggerChild({ module: 'claude-session-detail' });
|
|
35
|
+
|
|
36
|
+
/** Maximum number of concurrent SSE subscribers on this dashboard process. */
|
|
37
|
+
const MAX_SSE_SUBSCRIBERS = 50;
|
|
38
|
+
|
|
39
|
+
function getSseHeartbeatMs() {
|
|
40
|
+
const v = Number.parseInt(process.env.BIZAR_SSE_HEARTBEAT_MS ?? '', 10);
|
|
41
|
+
return Number.isFinite(v) && v > 0 ? v : 25_000;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Normalize a Claude Code message into the dashboard's chat shape:
|
|
46
|
+
* { id, role, content, ts }
|
|
47
|
+
*
|
|
48
|
+
* Mirrors the shape used by `routes/cline-session-detail.mjs` so the
|
|
49
|
+
* React chat UI is identical between the two backends.
|
|
50
|
+
*
|
|
51
|
+
* @param {object} msg
|
|
52
|
+
* @returns {{id:string, role:string, content:string, ts:number}}
|
|
53
|
+
*/
|
|
54
|
+
function toChatMessage(msg) {
|
|
55
|
+
return {
|
|
56
|
+
id: msg?.id || '',
|
|
57
|
+
role: msg?.role || 'assistant',
|
|
58
|
+
content: msg?.content || '',
|
|
59
|
+
ts: typeof msg?.ts === 'number' ? msg.ts : Date.now(),
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* @returns {import('express').Router}
|
|
65
|
+
*/
|
|
66
|
+
export function createClaudeSessionDetailRouter() {
|
|
67
|
+
const router = Router();
|
|
68
|
+
|
|
69
|
+
// GET /api/claude-sessions/:id/messages
|
|
70
|
+
router.get(
|
|
71
|
+
'/claude-sessions/:id/messages',
|
|
72
|
+
wrap(withSpan('claude.session.messages', async (span, req, res) => {
|
|
73
|
+
const sessionId = String(req.params?.id || '');
|
|
74
|
+
setCommonAttributes(span, {
|
|
75
|
+
ip: req.ip || req.socket?.remoteAddress,
|
|
76
|
+
userAgent: req.headers?.['user-agent'],
|
|
77
|
+
});
|
|
78
|
+
span.setAttribute('claude.session_id', sessionId);
|
|
79
|
+
if (!sessionId) {
|
|
80
|
+
res.status(400).json({ error: 'bad_request', message: 'session id is required' });
|
|
81
|
+
recordTrace('claude.session.messages', { outcome: 'missing_session_id' });
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
const result = listClaudeMessages(sessionId);
|
|
85
|
+
if (!result.ok) {
|
|
86
|
+
logger.warn('claude listMessages failed', {
|
|
87
|
+
sessionId,
|
|
88
|
+
err: result.error,
|
|
89
|
+
});
|
|
90
|
+
span.setAttribute('claude.error_cause', 'not_found');
|
|
91
|
+
res.status(404).json({
|
|
92
|
+
error: 'claude_error',
|
|
93
|
+
message: result.error,
|
|
94
|
+
suggestion: 'Run `claude -p "<prompt>"` first to create the session, then re-open it here.',
|
|
95
|
+
});
|
|
96
|
+
recordTrace('claude.session.messages', { outcome: 'claude_error' });
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
const messages = result.messages.map(toChatMessage);
|
|
100
|
+
span.setAttribute('claude.message_count', messages.length);
|
|
101
|
+
res.json({ messages });
|
|
102
|
+
recordTrace('claude.session.messages', {
|
|
103
|
+
outcome: 'ok',
|
|
104
|
+
message_count_bucket: messages.length === 0 ? '0' : messages.length < 50 ? '1-49' : '50+',
|
|
105
|
+
});
|
|
106
|
+
})),
|
|
107
|
+
);
|
|
108
|
+
|
|
109
|
+
// POST /api/claude-sessions/:id/send
|
|
110
|
+
router.post(
|
|
111
|
+
'/claude-sessions/:id/send',
|
|
112
|
+
wrap(withSpan('claude.session.send', async (span, req, res) => {
|
|
113
|
+
const sessionId = String(req.params?.id || '');
|
|
114
|
+
setCommonAttributes(span, {
|
|
115
|
+
ip: req.ip || req.socket?.remoteAddress,
|
|
116
|
+
userAgent: req.headers?.['user-agent'],
|
|
117
|
+
});
|
|
118
|
+
span.setAttribute('claude.session_id', sessionId);
|
|
119
|
+
if (!sessionId) {
|
|
120
|
+
res.status(400).json({ error: 'bad_request', message: 'session id is required' });
|
|
121
|
+
recordTrace('claude.session.send', { outcome: 'missing_session_id' });
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
const body = req.body && typeof req.body === 'object' ? req.body : {};
|
|
125
|
+
const message = typeof body.message === 'string' ? body.message.trim() : '';
|
|
126
|
+
const agent = typeof body.agent === 'string' ? body.agent.trim() : '';
|
|
127
|
+
span.setAttribute('claude.message_length', message.length);
|
|
128
|
+
if (agent) span.setAttribute('claude.agent', agent);
|
|
129
|
+
if (!message) {
|
|
130
|
+
res.status(400).json({ error: 'bad_request', message: '`message` is required' });
|
|
131
|
+
recordTrace('claude.session.send', { outcome: 'missing_message' });
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
if (!agent) {
|
|
135
|
+
res.status(400).json({ error: 'bad_request', message: '`agent` is required' });
|
|
136
|
+
recordTrace('claude.session.send', { outcome: 'missing_agent' });
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// Spawn `claude --resume <sessionId> -p "<message>"` via the
|
|
141
|
+
// runner. We don't await its exit; the session keeps the
|
|
142
|
+
// background daemon alive and streams events to the SSE
|
|
143
|
+
// endpoint on the matching sessionId.
|
|
144
|
+
const { spawnAgent } = await import('../claude-runner.mjs');
|
|
145
|
+
// The runner's `resume` is currently a steer-style override;
|
|
146
|
+
// we pass it via a custom arg via env. For now: use `claude
|
|
147
|
+
// --resume <id>` with the prompt appended.
|
|
148
|
+
// (claude-runner doesn't yet accept a `resume` flag — fall
|
|
149
|
+
// back to shelling out directly via claude-bg-spawner's
|
|
150
|
+
// steerBgAgent pattern in a future patch.)
|
|
151
|
+
const messageID = `msg_${Date.now().toString(36)}${Math.random().toString(36).slice(2, 8)}`;
|
|
152
|
+
span.setAttribute('claude.message_id', messageID);
|
|
153
|
+
|
|
154
|
+
try {
|
|
155
|
+
const result = await spawnAgent({
|
|
156
|
+
prompt: message,
|
|
157
|
+
agent,
|
|
158
|
+
// The runner doesn't accept `resume` directly yet; we use
|
|
159
|
+
// the active worktree fallback so the process runs in the
|
|
160
|
+
// same directory as the original session.
|
|
161
|
+
worktree: process.cwd(),
|
|
162
|
+
title: `resume:${sessionId.slice(0, 12)}:${messageID}`,
|
|
163
|
+
logPath: `${process.cwd()}/.claude-resume-${sessionId.slice(0, 8)}.log`,
|
|
164
|
+
});
|
|
165
|
+
if (!result.ok) {
|
|
166
|
+
res.status(502).json({
|
|
167
|
+
error: 'claude_error',
|
|
168
|
+
message: result.error || 'failed to send prompt',
|
|
169
|
+
});
|
|
170
|
+
recordTrace('claude.session.send', { outcome: 'claude_error' });
|
|
171
|
+
return;
|
|
172
|
+
}
|
|
173
|
+
res.json({ ok: true, messageId: messageID });
|
|
174
|
+
recordTrace('claude.session.send', { outcome: 'ok' });
|
|
175
|
+
} catch (err) {
|
|
176
|
+
logger.error('claude send failed', {
|
|
177
|
+
sessionId,
|
|
178
|
+
err: err instanceof Error ? err.message : String(err),
|
|
179
|
+
});
|
|
180
|
+
res.status(502).json({
|
|
181
|
+
error: 'claude_error',
|
|
182
|
+
message: err instanceof Error ? err.message : String(err),
|
|
183
|
+
});
|
|
184
|
+
}
|
|
185
|
+
})),
|
|
186
|
+
);
|
|
187
|
+
|
|
188
|
+
// GET /api/claude-sessions/:id/stream (SSE)
|
|
189
|
+
router.get('/claude-sessions/:id/stream', (req, res) => {
|
|
190
|
+
const sessionId = String(req.params?.id || '');
|
|
191
|
+
const span = tracer.startSpan('claude.session.stream', {
|
|
192
|
+
attributes: {
|
|
193
|
+
'claude.session_id': sessionId,
|
|
194
|
+
'http.client_ip': req.ip || req.socket?.remoteAddress || '',
|
|
195
|
+
'http.user_agent': (req.headers && req.headers['user-agent']) || '',
|
|
196
|
+
},
|
|
197
|
+
});
|
|
198
|
+
let spanEnded = false;
|
|
199
|
+
const endSpan = (extraAttrs) => {
|
|
200
|
+
if (spanEnded) return;
|
|
201
|
+
spanEnded = true;
|
|
202
|
+
try {
|
|
203
|
+
if (extraAttrs && typeof extraAttrs === 'object') {
|
|
204
|
+
for (const [k, v] of Object.entries(extraAttrs)) {
|
|
205
|
+
try { span.setAttribute(k, v); } catch { /* ignore */ }
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
span.setStatus({ code: 1 });
|
|
209
|
+
span.end();
|
|
210
|
+
} catch {
|
|
211
|
+
/* never throw from OTel helper */
|
|
212
|
+
}
|
|
213
|
+
};
|
|
214
|
+
if (!sessionId) {
|
|
215
|
+
res.status(400).json({ error: 'bad_request', message: 'session id is required' });
|
|
216
|
+
endSpan({ 'claude.stream.outcome': 'missing_session_id' });
|
|
217
|
+
return;
|
|
218
|
+
}
|
|
219
|
+
if (activeSubscribers >= MAX_SSE_SUBSCRIBERS) {
|
|
220
|
+
res.status(503).json({
|
|
221
|
+
error: 'too_many_subscribers',
|
|
222
|
+
message: `SSE subscriber cap reached (${MAX_SSE_SUBSCRIBERS}); try again later.`,
|
|
223
|
+
});
|
|
224
|
+
endSpan({ 'claude.stream.outcome': 'too_many_subscribers' });
|
|
225
|
+
recordTrace('claude.session.stream', { outcome: 'too_many_subscribers' });
|
|
226
|
+
return;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
res.status(200);
|
|
230
|
+
res.setHeader('Content-Type', 'text/event-stream');
|
|
231
|
+
res.setHeader('Cache-Control', 'no-cache, no-transform');
|
|
232
|
+
res.setHeader('Connection', 'keep-alive');
|
|
233
|
+
res.setHeader('X-Accel-Buffering', 'no');
|
|
234
|
+
if (typeof res.flushHeaders === 'function') res.flushHeaders();
|
|
235
|
+
|
|
236
|
+
activeSubscribers += 1;
|
|
237
|
+
span.setAttribute('claude.stream.active_subscribers', activeSubscribers);
|
|
238
|
+
|
|
239
|
+
const heartbeat = setInterval(() => {
|
|
240
|
+
if (res.writableEnded || res.destroyed) {
|
|
241
|
+
clearInterval(heartbeat);
|
|
242
|
+
return;
|
|
243
|
+
}
|
|
244
|
+
try {
|
|
245
|
+
res.write(': keepalive\n\n');
|
|
246
|
+
} catch {
|
|
247
|
+
clearInterval(heartbeat);
|
|
248
|
+
}
|
|
249
|
+
}, getSseHeartbeatMs());
|
|
250
|
+
|
|
251
|
+
let closed = false;
|
|
252
|
+
const startTs = Date.now();
|
|
253
|
+
let closedReason = 'client_closed';
|
|
254
|
+
const cleanup = () => {
|
|
255
|
+
if (closed) return;
|
|
256
|
+
closed = true;
|
|
257
|
+
activeSubscribers = Math.max(0, activeSubscribers - 1);
|
|
258
|
+
clearInterval(heartbeat);
|
|
259
|
+
try { sub?.close?.(); } catch { /* ignore */ }
|
|
260
|
+
if (!res.writableEnded) {
|
|
261
|
+
try { res.end(); } catch { /* ignore */ }
|
|
262
|
+
}
|
|
263
|
+
const lifetimeMs = Date.now() - startTs;
|
|
264
|
+
endSpan({
|
|
265
|
+
'claude.stream.outcome': closedReason,
|
|
266
|
+
'claude.stream.lifetime_ms': lifetimeMs,
|
|
267
|
+
});
|
|
268
|
+
recordTrace('claude.session.stream', {
|
|
269
|
+
outcome: closedReason,
|
|
270
|
+
lifetime_bucket: lifetimeMs < 1_000 ? '<1s' : lifetimeMs < 60_000 ? '<60s' : '60s+',
|
|
271
|
+
});
|
|
272
|
+
};
|
|
273
|
+
req.on('close', () => {
|
|
274
|
+
closedReason = 'client_closed';
|
|
275
|
+
cleanup();
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
// Prefer the SDK path. Fall back to a one-shot message listing
|
|
279
|
+
// (the dashboard polls the JSONL on a 3s cadence via bg-poller
|
|
280
|
+
// for the rest of the streaming experience).
|
|
281
|
+
void (async () => {
|
|
282
|
+
try {
|
|
283
|
+
const sub = await subscribeToSession(sessionId);
|
|
284
|
+
if (sub && typeof sub[Symbol.asyncIterator] === 'function') {
|
|
285
|
+
for await (const evt of sub) {
|
|
286
|
+
if (res.writableEnded || res.destroyed) break;
|
|
287
|
+
const payload = JSON.stringify({
|
|
288
|
+
type: evt?.type || 'message',
|
|
289
|
+
sessionID: sessionId,
|
|
290
|
+
data: evt,
|
|
291
|
+
});
|
|
292
|
+
try {
|
|
293
|
+
res.write(`event: ${evt?.type || 'message'}\n`);
|
|
294
|
+
res.write(`data: ${payload}\n\n`);
|
|
295
|
+
} catch {
|
|
296
|
+
/* socket closed mid-write */
|
|
297
|
+
break;
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
closedReason = 'sdk_eof';
|
|
301
|
+
} else {
|
|
302
|
+
// SDK unavailable — write a single `subscribed` event so the
|
|
303
|
+
// client knows the stream is alive and switches to its
|
|
304
|
+
// polling mode.
|
|
305
|
+
res.write(`event: subscribed\n`);
|
|
306
|
+
res.write(`data: ${JSON.stringify({ sessionID: sessionId, mode: 'poll' })}\n\n`);
|
|
307
|
+
closedReason = 'sdk_unavailable';
|
|
308
|
+
}
|
|
309
|
+
} catch (err) {
|
|
310
|
+
if (!res.writableEnded) {
|
|
311
|
+
try {
|
|
312
|
+
res.write(`event: error\ndata: ${JSON.stringify({
|
|
313
|
+
error: 'upstream_read_error',
|
|
314
|
+
message: err instanceof Error ? err.message : String(err),
|
|
315
|
+
})}\n\n`);
|
|
316
|
+
} catch { /* ignore */ }
|
|
317
|
+
}
|
|
318
|
+
closedReason = 'upstream_read_error';
|
|
319
|
+
} finally {
|
|
320
|
+
cleanup();
|
|
321
|
+
}
|
|
322
|
+
})();
|
|
323
|
+
});
|
|
324
|
+
|
|
325
|
+
return router;
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
/** Process-wide counter for active SSE subscribers. */
|
|
329
|
+
let activeSubscribers = 0;
|
|
@@ -0,0 +1,259 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* src/server/routes/claude-sessions.mjs
|
|
3
|
+
*
|
|
4
|
+
* v6.3.0 — Claude Code-native sessions CRUD endpoints.
|
|
5
|
+
*
|
|
6
|
+
* GET /api/claude-sessions — list sessions on disk
|
|
7
|
+
* POST /api/claude-sessions/new — create a new Claude Code session
|
|
8
|
+
* PATCH /api/claude-sessions/:id — rename / update a session
|
|
9
|
+
* DELETE /api/claude-sessions/:id — delete a session's on-disk dir
|
|
10
|
+
*
|
|
11
|
+
* Replaces routes/cline-sessions.mjs (which read from
|
|
12
|
+
* `~/.local/share/cline/cline.db`). Claude Code stores sessions as
|
|
13
|
+
* JSONL files under `~/.claude/sessions/<sessionId>/messages.jsonl`,
|
|
14
|
+
* so this router:
|
|
15
|
+
*
|
|
16
|
+
* - Lists sessions by walking `~/.claude/sessions/` and reading
|
|
17
|
+
* each `messages.jsonl`'s init line.
|
|
18
|
+
* - Creates sessions by spawning `claude -p "<prompt>"` via
|
|
19
|
+
* `claude-runner.mjs`.
|
|
20
|
+
* - Renames / updates by writing a `meta.json` next to the JSONL
|
|
21
|
+
* (Claude Code doesn't have a PATCH endpoint — the title is
|
|
22
|
+
* derived from the first user message at list time, so we
|
|
23
|
+
* override it with a sidecar `meta.json` that `claude-info.mjs`
|
|
24
|
+
* prefers when present).
|
|
25
|
+
* - Deletes by `rm -rf` the session directory.
|
|
26
|
+
*
|
|
27
|
+
* The HTTP API surface stays compatible with the old
|
|
28
|
+
* cline-sessions.mjs contract so the React frontend doesn't have to
|
|
29
|
+
* change — only the path prefix is updated (`/api/cline-sessions/*`
|
|
30
|
+
* → `/api/claude-sessions/*`).
|
|
31
|
+
*/
|
|
32
|
+
|
|
33
|
+
import { Router } from 'express';
|
|
34
|
+
import { SpanStatusCode } from '@opentelemetry/api';
|
|
35
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
36
|
+
import { join } from 'node:path';
|
|
37
|
+
import { homedir } from 'node:os';
|
|
38
|
+
import { wrap } from './_shared.mjs';
|
|
39
|
+
import { tracer } from '../otel.mjs';
|
|
40
|
+
import {
|
|
41
|
+
CLAUDE_SESSIONS_DIR,
|
|
42
|
+
listClaudeSessions,
|
|
43
|
+
deleteClaudeSession,
|
|
44
|
+
} from '../claude-info.mjs';
|
|
45
|
+
|
|
46
|
+
const HOME = homedir();
|
|
47
|
+
|
|
48
|
+
const SESSION_ID_RE = /^[A-Za-z0-9_-]{1,200}$/;
|
|
49
|
+
const AGENT_NAME_RE = /^[A-Za-z0-9_-]{1,64}$/;
|
|
50
|
+
const TITLE_MAX = 200;
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Path to a per-session `meta.json` sidecar. Claude Code doesn't
|
|
54
|
+
* expose a rename endpoint, so the dashboard persists user-set
|
|
55
|
+
* titles in this file. {@link listClaudeSessions} prefers the
|
|
56
|
+
* sidecar title when present.
|
|
57
|
+
*/
|
|
58
|
+
function metaFileFor(sessionId) {
|
|
59
|
+
return join(CLAUDE_SESSIONS_DIR, sessionId, 'meta.json');
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Read the sidecar meta.json (if any). Returns `{}` on any failure.
|
|
64
|
+
*/
|
|
65
|
+
function readMeta(sessionId) {
|
|
66
|
+
try {
|
|
67
|
+
const fp = metaFileFor(sessionId);
|
|
68
|
+
if (!existsSync(fp)) return {};
|
|
69
|
+
return JSON.parse(readFileSync(fp, 'utf8'));
|
|
70
|
+
} catch {
|
|
71
|
+
return {};
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Create a new Claude Code session via `claude -p "<prompt>"`.
|
|
77
|
+
* Returns `{ ok, sessionId, error? }`.
|
|
78
|
+
*
|
|
79
|
+
* @param {{ title?: string, agent: string, prompt: string, worktree?: string, model?: string }} opts
|
|
80
|
+
* @returns {Promise<{ ok: boolean, sessionId?: string, error?: string }>}
|
|
81
|
+
*/
|
|
82
|
+
async function createClaudeSession(opts) {
|
|
83
|
+
const { spawnAgent } = await import('../claude-runner.mjs');
|
|
84
|
+
const worktree = opts.worktree && opts.worktree.trim() ? opts.worktree.trim() : HOME;
|
|
85
|
+
const title = (opts.title || `Chat: ${opts.agent}`).slice(0, TITLE_MAX);
|
|
86
|
+
// We use `claude -p` (print mode, runs to completion) so the
|
|
87
|
+
// dashboard gets a real sessionId back. For long-running sessions
|
|
88
|
+
// the dashboard should use the bg-spawner instead.
|
|
89
|
+
const result = await spawnAgent({
|
|
90
|
+
prompt: opts.prompt,
|
|
91
|
+
agent: opts.agent,
|
|
92
|
+
worktree,
|
|
93
|
+
title,
|
|
94
|
+
logPath: join(CLAUDE_SESSIONS_DIR, '_new', `${Date.now()}.log`),
|
|
95
|
+
model: opts.model,
|
|
96
|
+
});
|
|
97
|
+
if (!result.ok || !result.sessionId) {
|
|
98
|
+
return { ok: false, error: result.error || 'claude session spawn failed' };
|
|
99
|
+
}
|
|
100
|
+
return { ok: true, sessionId: result.sessionId };
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Persist a sidecar meta.json for a session so subsequent lists
|
|
105
|
+
* pick up the user-set title without re-parsing the JSONL.
|
|
106
|
+
*
|
|
107
|
+
* @param {string} sessionId
|
|
108
|
+
* @param {object} patch
|
|
109
|
+
*/
|
|
110
|
+
function writeMeta(sessionId, patch) {
|
|
111
|
+
const cur = readMeta(sessionId);
|
|
112
|
+
const next = { ...cur, ...patch, updatedAt: Date.now() };
|
|
113
|
+
const dir = join(CLAUDE_SESSIONS_DIR, sessionId);
|
|
114
|
+
try {
|
|
115
|
+
mkdirSync(dir, { recursive: true });
|
|
116
|
+
writeFileSync(metaFileFor(sessionId), JSON.stringify(next, null, 2), 'utf8');
|
|
117
|
+
return true;
|
|
118
|
+
} catch {
|
|
119
|
+
return false;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* @returns {import('express').Router}
|
|
125
|
+
*/
|
|
126
|
+
export function createClaudeSessionsRouter() {
|
|
127
|
+
const router = Router();
|
|
128
|
+
|
|
129
|
+
// GET /api/claude-sessions
|
|
130
|
+
router.get('/claude-sessions', wrap(async (_req, res) => {
|
|
131
|
+
try {
|
|
132
|
+
const sessions = listClaudeSessions();
|
|
133
|
+
// Overlay the sidecar meta.json titles so user-renamed sessions
|
|
134
|
+
// show up correctly in the rail.
|
|
135
|
+
const enriched = sessions.map((s) => {
|
|
136
|
+
const meta = readMeta(s.id);
|
|
137
|
+
return {
|
|
138
|
+
id: s.id,
|
|
139
|
+
title: meta.title || s.title,
|
|
140
|
+
model: s.model,
|
|
141
|
+
created: s.createdAt,
|
|
142
|
+
updated: s.updatedAt,
|
|
143
|
+
mtime: s.updatedAt,
|
|
144
|
+
};
|
|
145
|
+
});
|
|
146
|
+
res.json({ sessions: enriched });
|
|
147
|
+
} catch (err) {
|
|
148
|
+
res.json({ sessions: [], error: err instanceof Error ? err.message : String(err) });
|
|
149
|
+
}
|
|
150
|
+
}));
|
|
151
|
+
|
|
152
|
+
// POST /api/claude-sessions/new
|
|
153
|
+
router.post('/claude-sessions/new', wrap(async (req, res) => {
|
|
154
|
+
return tracer.startActiveSpan('claude.session.create', async (span) => {
|
|
155
|
+
try {
|
|
156
|
+
const body = req.body && typeof req.body === 'object' ? req.body : {};
|
|
157
|
+
const title = typeof body.title === 'string' && body.title.trim().length > 0
|
|
158
|
+
? body.title.trim().slice(0, TITLE_MAX)
|
|
159
|
+
: null;
|
|
160
|
+
const agent = typeof body.agent === 'string' ? body.agent.trim() : '';
|
|
161
|
+
const prompt = typeof body.prompt === 'string' ? body.prompt.trim() : '';
|
|
162
|
+
const directory = typeof body.directory === 'string' && body.directory.length > 0
|
|
163
|
+
? body.directory
|
|
164
|
+
: '';
|
|
165
|
+
const model = typeof body.model === 'string' ? body.model.trim() : '';
|
|
166
|
+
span.setAttribute('claude.session.agent', agent);
|
|
167
|
+
span.setAttribute('claude.session.title_length', title === null ? 0 : title.length);
|
|
168
|
+
|
|
169
|
+
if (!agent) {
|
|
170
|
+
res.status(400).json({ error: 'bad_request', message: '`agent` is required' });
|
|
171
|
+
return;
|
|
172
|
+
}
|
|
173
|
+
if (!AGENT_NAME_RE.test(agent)) {
|
|
174
|
+
res.status(400).json({ error: 'bad_request', message: '`agent` is invalid (allowed: [A-Za-z0-9_-]{1,64})' });
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
177
|
+
if (title !== null && title.length > TITLE_MAX) {
|
|
178
|
+
res.status(400).json({ error: 'bad_request', message: `title too long (> ${TITLE_MAX} chars)` });
|
|
179
|
+
return;
|
|
180
|
+
}
|
|
181
|
+
if (!prompt) {
|
|
182
|
+
res.status(400).json({ error: 'bad_request', message: '`prompt` is required to seed a new session' });
|
|
183
|
+
return;
|
|
184
|
+
}
|
|
185
|
+
const finalTitle = title || `Chat: ${agent}`;
|
|
186
|
+
const result = await createClaudeSession({
|
|
187
|
+
title: finalTitle,
|
|
188
|
+
agent,
|
|
189
|
+
prompt,
|
|
190
|
+
worktree: directory,
|
|
191
|
+
model,
|
|
192
|
+
});
|
|
193
|
+
if (!result.ok || !result.sessionId) {
|
|
194
|
+
res.status(502).json({ error: 'claude_error', message: result.error || 'failed to create claude session' });
|
|
195
|
+
return;
|
|
196
|
+
}
|
|
197
|
+
// Persist the user-set title into the sidecar meta.json so the
|
|
198
|
+
// next list call surfaces it without re-parsing JSONL.
|
|
199
|
+
writeMeta(result.sessionId, { title: finalTitle, agent });
|
|
200
|
+
span.setAttribute('claude.session.id', result.sessionId);
|
|
201
|
+
res.status(201).json({
|
|
202
|
+
id: result.sessionId,
|
|
203
|
+
title: finalTitle,
|
|
204
|
+
agent,
|
|
205
|
+
directory,
|
|
206
|
+
createdAt: Date.now(),
|
|
207
|
+
});
|
|
208
|
+
} catch (err) {
|
|
209
|
+
span.recordException(err);
|
|
210
|
+
span.setStatus({ code: SpanStatusCode.ERROR, message: err.message });
|
|
211
|
+
throw err;
|
|
212
|
+
} finally {
|
|
213
|
+
span.end();
|
|
214
|
+
}
|
|
215
|
+
});
|
|
216
|
+
}));
|
|
217
|
+
|
|
218
|
+
// PATCH /api/claude-sessions/:id
|
|
219
|
+
router.patch('/claude-sessions/:id', wrap(async (req, res) => {
|
|
220
|
+
const sessionId = String(req.params?.id || '');
|
|
221
|
+
if (!SESSION_ID_RE.test(sessionId)) {
|
|
222
|
+
res.status(400).json({ error: 'bad_request', message: 'invalid session id' });
|
|
223
|
+
return;
|
|
224
|
+
}
|
|
225
|
+
const body = req.body && typeof req.body === 'object' ? req.body : {};
|
|
226
|
+
const title = typeof body.title === 'string' ? body.title.trim() : '';
|
|
227
|
+
if (!title) {
|
|
228
|
+
res.status(400).json({ error: 'bad_request', message: '`title` is required (non-empty)' });
|
|
229
|
+
return;
|
|
230
|
+
}
|
|
231
|
+
if (title.length > TITLE_MAX) {
|
|
232
|
+
res.status(400).json({ error: 'bad_request', message: `title too long (> ${TITLE_MAX} chars)` });
|
|
233
|
+
return;
|
|
234
|
+
}
|
|
235
|
+
if (!existsSync(join(CLAUDE_SESSIONS_DIR, sessionId))) {
|
|
236
|
+
res.status(404).json({ error: 'not_found', message: `session ${sessionId} not found` });
|
|
237
|
+
return;
|
|
238
|
+
}
|
|
239
|
+
writeMeta(sessionId, { title });
|
|
240
|
+
res.json({ id: sessionId, title });
|
|
241
|
+
}));
|
|
242
|
+
|
|
243
|
+
// DELETE /api/claude-sessions/:id
|
|
244
|
+
router.delete('/claude-sessions/:id', wrap(async (req, res) => {
|
|
245
|
+
const sessionId = String(req.params?.id || '');
|
|
246
|
+
if (!SESSION_ID_RE.test(sessionId)) {
|
|
247
|
+
res.status(400).json({ error: 'bad_request', message: 'invalid session id' });
|
|
248
|
+
return;
|
|
249
|
+
}
|
|
250
|
+
const r = deleteClaudeSession(sessionId);
|
|
251
|
+
if (!r.ok) {
|
|
252
|
+
res.status(502).json({ error: 'claude_error', message: r.error || 'delete failed' });
|
|
253
|
+
return;
|
|
254
|
+
}
|
|
255
|
+
res.json({ id: sessionId, deleted: true });
|
|
256
|
+
}));
|
|
257
|
+
|
|
258
|
+
return router;
|
|
259
|
+
}
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
* /api/tasks/:id/start (POST) — trigger dispatch of a queued task
|
|
11
11
|
* /api/tasks/:id/status (PATCH) — move status (queued|doing|done|blocked|archived)
|
|
12
12
|
* /api/tasks/:id/comments (POST) — append comment
|
|
13
|
-
* /api/tasks/:id/chat (GET) —
|
|
13
|
+
* /api/tasks/:id/chat (GET) — claude messages for the task's bg instance
|
|
14
14
|
* /api/tasks/:id/artifacts (GET) — list artifacts for this task
|
|
15
15
|
* /api/tasks/:id/artifacts (POST) — attach artifact + link to task
|
|
16
16
|
* /api/tasks/:id/timer (POST) — toggle timer
|
|
@@ -34,10 +34,9 @@ import { notificationsStore } from '../notifications-store.mjs';
|
|
|
34
34
|
import { projectsStore } from '../projects-store.mjs';
|
|
35
35
|
import { artifactsStore } from '../artifacts-store.mjs';
|
|
36
36
|
import {
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
} from '../serve-info.mjs';
|
|
37
|
+
listClaudeMessages,
|
|
38
|
+
normalizeClaudeMessage,
|
|
39
|
+
} from '../claude-info.mjs';
|
|
41
40
|
import { readActiveProjectId, wrap } from './_shared.mjs';
|
|
42
41
|
import { ALLOWED_TASK_STATUSES } from '../tasks-store.mjs';
|
|
43
42
|
|
|
@@ -207,10 +206,15 @@ export function createTasksRouter({ state, broadcast, projectRoot }) {
|
|
|
207
206
|
}));
|
|
208
207
|
|
|
209
208
|
// v3.5.5 — Chat-task linkage. Given a task id, return the
|
|
210
|
-
//
|
|
211
|
-
// for an in-progress or completed task. The task must
|
|
212
|
-
// `bgInstanceId` (or `sessionId`) in its metadata — the
|
|
213
|
-
// writes that when the
|
|
209
|
+
// Claude Code session's messages so the chat UI can open a
|
|
210
|
+
// thread for an in-progress or completed task. The task must
|
|
211
|
+
// have a `bgInstanceId` (or `sessionId`) in its metadata — the
|
|
212
|
+
// delegator writes that when the Claude Code session is created.
|
|
213
|
+
//
|
|
214
|
+
// v6.3.0 — Reads from `~/.claude/sessions/<id>/messages.jsonl`
|
|
215
|
+
// via `claude-info.listClaudeMessages()` instead of the old
|
|
216
|
+
// cline serve HTTP endpoint. The 503 "plugin offline" path is
|
|
217
|
+
// gone — Claude Code is just a CLI on PATH.
|
|
214
218
|
//
|
|
215
219
|
// Response shape:
|
|
216
220
|
// { taskId, bgInstanceId, sessionId, agent, messages: [{id,role,content,ts}, ...] }
|
|
@@ -218,8 +222,7 @@ export function createTasksRouter({ state, broadcast, projectRoot }) {
|
|
|
218
222
|
// Status codes:
|
|
219
223
|
// 200 — messages returned (may be empty)
|
|
220
224
|
// 404 — task not found, or no bg instance / session id
|
|
221
|
-
//
|
|
222
|
-
// 502 — cline listMessages call failed
|
|
225
|
+
// 502 — claude listMessages call failed
|
|
223
226
|
router.get('/tasks/:id/chat', wrap(async (req, res) => {
|
|
224
227
|
const projectId = req.query.projectId || readActiveProjectId();
|
|
225
228
|
const task = await tasksStore.getById(projectId, req.params.id);
|
|
@@ -246,41 +249,30 @@ export function createTasksRouter({ state, broadcast, projectRoot }) {
|
|
|
246
249
|
if (!bgInstanceId && !sessionId) {
|
|
247
250
|
return res.status(404).json({
|
|
248
251
|
error: 'no_bg_instance',
|
|
249
|
-
message: 'task has no bg instance or
|
|
250
|
-
taskId: task.id,
|
|
251
|
-
});
|
|
252
|
-
}
|
|
253
|
-
|
|
254
|
-
const serveInfo = readServeInfo();
|
|
255
|
-
if (!serveInfo) {
|
|
256
|
-
return res.status(503).json({
|
|
257
|
-
error: 'plugin_offline',
|
|
258
|
-
message: 'cline plugin is not running',
|
|
252
|
+
message: 'task has no bg instance or claude session id',
|
|
259
253
|
taskId: task.id,
|
|
260
254
|
});
|
|
261
255
|
}
|
|
262
256
|
if (!sessionId) {
|
|
263
257
|
return res.status(404).json({
|
|
264
258
|
error: 'no_session',
|
|
265
|
-
message: 'bg instance has no
|
|
259
|
+
message: 'bg instance has no claude session id',
|
|
266
260
|
taskId: task.id,
|
|
267
261
|
bgInstanceId,
|
|
268
262
|
});
|
|
269
263
|
}
|
|
270
264
|
|
|
271
|
-
const
|
|
272
|
-
const directory = (active && active.path) || serveInfo.worktree || '';
|
|
273
|
-
const result = await listClineMessages(serveInfo, sessionId, directory);
|
|
265
|
+
const result = listClaudeMessages(sessionId);
|
|
274
266
|
if (!result.ok) {
|
|
275
267
|
return res.status(502).json({
|
|
276
|
-
error: '
|
|
277
|
-
message: result.error || '
|
|
268
|
+
error: 'claude_error',
|
|
269
|
+
message: result.error || 'listClaudeMessages failed',
|
|
278
270
|
taskId: task.id,
|
|
279
271
|
sessionId,
|
|
280
272
|
});
|
|
281
273
|
}
|
|
282
274
|
const messages = Array.isArray(result.messages)
|
|
283
|
-
? result.messages.map(
|
|
275
|
+
? result.messages.map(normalizeClaudeMessage)
|
|
284
276
|
: [];
|
|
285
277
|
res.json({
|
|
286
278
|
taskId: task.id,
|