@selvakumaresra/specship 0.1.3 → 0.4.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/README.md +2 -2
- package/commands/ss-design-implement.md +84 -0
- package/commands/ss-design-loop.md +125 -0
- package/commands/{cg-drifted.md → ss-drifted.md} +2 -2
- package/commands/{cg-fix.md → ss-fix.md} +1 -1
- package/commands/{cg-implement.md → ss-implement.md} +1 -1
- package/commands/ss-spec-author.md +43 -0
- package/commands/ss-spec-review.md +48 -0
- package/dist/bin/node-version-check.d.ts +37 -0
- package/dist/bin/node-version-check.d.ts.map +1 -0
- package/dist/bin/node-version-check.js +79 -0
- package/dist/bin/node-version-check.js.map +1 -0
- package/dist/bin/specship.d.ts +25 -0
- package/dist/bin/specship.d.ts.map +1 -0
- package/dist/bin/specship.js +2085 -0
- package/dist/bin/specship.js.map +1 -0
- package/dist/bin/uninstall.d.ts +13 -0
- package/dist/bin/uninstall.d.ts.map +1 -0
- package/dist/bin/uninstall.js +35 -0
- package/dist/bin/uninstall.js.map +1 -0
- package/dist/context/formatter.d.ts +30 -0
- package/dist/context/formatter.d.ts.map +1 -0
- package/dist/context/formatter.js +263 -0
- package/dist/context/formatter.js.map +1 -0
- package/dist/context/index.d.ts +119 -0
- package/dist/context/index.d.ts.map +1 -0
- package/dist/context/index.js +1289 -0
- package/dist/context/index.js.map +1 -0
- package/dist/context/markers.d.ts +19 -0
- package/dist/context/markers.d.ts.map +1 -0
- package/dist/context/markers.js +22 -0
- package/dist/context/markers.js.map +1 -0
- package/dist/db/index.d.ts +103 -0
- package/dist/db/index.d.ts.map +1 -0
- package/dist/db/index.js +279 -0
- package/dist/db/index.js.map +1 -0
- package/dist/db/migrations.d.ts +44 -0
- package/dist/db/migrations.d.ts.map +1 -0
- package/dist/db/migrations.js +462 -0
- package/dist/db/migrations.js.map +1 -0
- package/dist/db/queries.d.ts +357 -0
- package/dist/db/queries.d.ts.map +1 -0
- package/dist/db/queries.js +1504 -0
- package/dist/db/queries.js.map +1 -0
- package/dist/db/schema.sql +419 -0
- package/dist/db/spec-queries.d.ts +101 -0
- package/dist/db/spec-queries.d.ts.map +1 -0
- package/dist/db/spec-queries.js +675 -0
- package/dist/db/spec-queries.js.map +1 -0
- package/dist/db/sqlite-adapter.d.ts +65 -0
- package/dist/db/sqlite-adapter.d.ts.map +1 -0
- package/dist/db/sqlite-adapter.js +214 -0
- package/dist/db/sqlite-adapter.js.map +1 -0
- package/dist/designer/artifact-store.js +54 -0
- package/dist/designer/browser.js +141 -0
- package/dist/designer/cdp-ensure.js +60 -0
- package/dist/designer/cdp-env.js +18 -0
- package/dist/designer/cdp-trace.js +599 -0
- package/dist/designer/cross-platform.js +74 -0
- package/dist/designer/designer-controller.js +1413 -0
- package/dist/designer/file-panel.js +39 -0
- package/dist/designer/interstitials.js +97 -0
- package/dist/designer/oopif-reader.js +176 -0
- package/dist/designer/package-meta.js +18 -0
- package/dist/designer/preview-host.js +50 -0
- package/dist/designer/repo-root.js +31 -0
- package/dist/designer/run-state.js +353 -0
- package/dist/designer/session-store.js +59 -0
- package/dist/designer/ui-anchors.js +651 -0
- package/dist/directory.d.ts +67 -0
- package/dist/directory.d.ts.map +1 -0
- package/dist/directory.js +267 -0
- package/dist/directory.js.map +1 -0
- package/dist/errors.d.ts +136 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +219 -0
- package/dist/errors.js.map +1 -0
- package/dist/extraction/dfm-extractor.d.ts +31 -0
- package/dist/extraction/dfm-extractor.d.ts.map +1 -0
- package/dist/extraction/dfm-extractor.js +151 -0
- package/dist/extraction/dfm-extractor.js.map +1 -0
- package/dist/extraction/generated-detection.d.ts +30 -0
- package/dist/extraction/generated-detection.d.ts.map +1 -0
- package/dist/extraction/generated-detection.js +80 -0
- package/dist/extraction/generated-detection.js.map +1 -0
- package/dist/extraction/grammars.d.ts +100 -0
- package/dist/extraction/grammars.d.ts.map +1 -0
- package/dist/extraction/grammars.js +426 -0
- package/dist/extraction/grammars.js.map +1 -0
- package/dist/extraction/index.d.ts +138 -0
- package/dist/extraction/index.d.ts.map +1 -0
- package/dist/extraction/index.js +1394 -0
- package/dist/extraction/index.js.map +1 -0
- package/dist/extraction/languages/c-cpp.d.ts +4 -0
- package/dist/extraction/languages/c-cpp.d.ts.map +1 -0
- package/dist/extraction/languages/c-cpp.js +171 -0
- package/dist/extraction/languages/c-cpp.js.map +1 -0
- package/dist/extraction/languages/csharp.d.ts +3 -0
- package/dist/extraction/languages/csharp.d.ts.map +1 -0
- package/dist/extraction/languages/csharp.js +73 -0
- package/dist/extraction/languages/csharp.js.map +1 -0
- package/dist/extraction/languages/dart.d.ts +3 -0
- package/dist/extraction/languages/dart.d.ts.map +1 -0
- package/dist/extraction/languages/dart.js +192 -0
- package/dist/extraction/languages/dart.js.map +1 -0
- package/dist/extraction/languages/go.d.ts +3 -0
- package/dist/extraction/languages/go.d.ts.map +1 -0
- package/dist/extraction/languages/go.js +74 -0
- package/dist/extraction/languages/go.js.map +1 -0
- package/dist/extraction/languages/index.d.ts +10 -0
- package/dist/extraction/languages/index.d.ts.map +1 -0
- package/dist/extraction/languages/index.js +51 -0
- package/dist/extraction/languages/index.js.map +1 -0
- package/dist/extraction/languages/java.d.ts +3 -0
- package/dist/extraction/languages/java.d.ts.map +1 -0
- package/dist/extraction/languages/java.js +70 -0
- package/dist/extraction/languages/java.js.map +1 -0
- package/dist/extraction/languages/javascript.d.ts +3 -0
- package/dist/extraction/languages/javascript.d.ts.map +1 -0
- package/dist/extraction/languages/javascript.js +90 -0
- package/dist/extraction/languages/javascript.js.map +1 -0
- package/dist/extraction/languages/kotlin.d.ts +3 -0
- package/dist/extraction/languages/kotlin.d.ts.map +1 -0
- package/dist/extraction/languages/kotlin.js +259 -0
- package/dist/extraction/languages/kotlin.js.map +1 -0
- package/dist/extraction/languages/lua.d.ts +3 -0
- package/dist/extraction/languages/lua.d.ts.map +1 -0
- package/dist/extraction/languages/lua.js +150 -0
- package/dist/extraction/languages/lua.js.map +1 -0
- package/dist/extraction/languages/luau.d.ts +3 -0
- package/dist/extraction/languages/luau.d.ts.map +1 -0
- package/dist/extraction/languages/luau.js +37 -0
- package/dist/extraction/languages/luau.js.map +1 -0
- package/dist/extraction/languages/objc.d.ts +3 -0
- package/dist/extraction/languages/objc.d.ts.map +1 -0
- package/dist/extraction/languages/objc.js +133 -0
- package/dist/extraction/languages/objc.js.map +1 -0
- package/dist/extraction/languages/pascal.d.ts +3 -0
- package/dist/extraction/languages/pascal.d.ts.map +1 -0
- package/dist/extraction/languages/pascal.js +66 -0
- package/dist/extraction/languages/pascal.js.map +1 -0
- package/dist/extraction/languages/php.d.ts +3 -0
- package/dist/extraction/languages/php.d.ts.map +1 -0
- package/dist/extraction/languages/php.js +107 -0
- package/dist/extraction/languages/php.js.map +1 -0
- package/dist/extraction/languages/python.d.ts +3 -0
- package/dist/extraction/languages/python.d.ts.map +1 -0
- package/dist/extraction/languages/python.js +56 -0
- package/dist/extraction/languages/python.js.map +1 -0
- package/dist/extraction/languages/ruby.d.ts +3 -0
- package/dist/extraction/languages/ruby.d.ts.map +1 -0
- package/dist/extraction/languages/ruby.js +114 -0
- package/dist/extraction/languages/ruby.js.map +1 -0
- package/dist/extraction/languages/rust.d.ts +3 -0
- package/dist/extraction/languages/rust.d.ts.map +1 -0
- package/dist/extraction/languages/rust.js +109 -0
- package/dist/extraction/languages/rust.js.map +1 -0
- package/dist/extraction/languages/scala.d.ts +3 -0
- package/dist/extraction/languages/scala.d.ts.map +1 -0
- package/dist/extraction/languages/scala.js +139 -0
- package/dist/extraction/languages/scala.js.map +1 -0
- package/dist/extraction/languages/swift.d.ts +3 -0
- package/dist/extraction/languages/swift.d.ts.map +1 -0
- package/dist/extraction/languages/swift.js +91 -0
- package/dist/extraction/languages/swift.js.map +1 -0
- package/dist/extraction/languages/typescript.d.ts +3 -0
- package/dist/extraction/languages/typescript.d.ts.map +1 -0
- package/dist/extraction/languages/typescript.js +129 -0
- package/dist/extraction/languages/typescript.js.map +1 -0
- package/dist/extraction/liquid-extractor.d.ts +52 -0
- package/dist/extraction/liquid-extractor.d.ts.map +1 -0
- package/dist/extraction/liquid-extractor.js +313 -0
- package/dist/extraction/liquid-extractor.js.map +1 -0
- package/dist/extraction/mybatis-extractor.d.ts +48 -0
- package/dist/extraction/mybatis-extractor.d.ts.map +1 -0
- package/dist/extraction/mybatis-extractor.js +198 -0
- package/dist/extraction/mybatis-extractor.js.map +1 -0
- package/dist/extraction/parse-worker.d.ts +8 -0
- package/dist/extraction/parse-worker.d.ts.map +1 -0
- package/dist/extraction/parse-worker.js +94 -0
- package/dist/extraction/parse-worker.js.map +1 -0
- package/dist/extraction/specs/markdown-spec-extractor.d.ts +59 -0
- package/dist/extraction/specs/markdown-spec-extractor.d.ts.map +1 -0
- package/dist/extraction/specs/markdown-spec-extractor.js +327 -0
- package/dist/extraction/specs/markdown-spec-extractor.js.map +1 -0
- package/dist/extraction/specs/types.d.ts +39 -0
- package/dist/extraction/specs/types.d.ts.map +1 -0
- package/dist/extraction/specs/types.js +8 -0
- package/dist/extraction/specs/types.js.map +1 -0
- package/dist/extraction/svelte-extractor.d.ts +56 -0
- package/dist/extraction/svelte-extractor.d.ts.map +1 -0
- package/dist/extraction/svelte-extractor.js +272 -0
- package/dist/extraction/svelte-extractor.js.map +1 -0
- package/dist/extraction/tree-sitter-helpers.d.ts +28 -0
- package/dist/extraction/tree-sitter-helpers.d.ts.map +1 -0
- package/dist/extraction/tree-sitter-helpers.js +103 -0
- package/dist/extraction/tree-sitter-helpers.js.map +1 -0
- package/dist/extraction/tree-sitter-types.d.ts +193 -0
- package/dist/extraction/tree-sitter-types.d.ts.map +1 -0
- package/dist/extraction/tree-sitter-types.js +10 -0
- package/dist/extraction/tree-sitter-types.js.map +1 -0
- package/dist/extraction/tree-sitter.d.ts +317 -0
- package/dist/extraction/tree-sitter.d.ts.map +1 -0
- package/dist/extraction/tree-sitter.js +3092 -0
- package/dist/extraction/tree-sitter.js.map +1 -0
- package/dist/extraction/vue-extractor.d.ts +51 -0
- package/dist/extraction/vue-extractor.d.ts.map +1 -0
- package/dist/extraction/vue-extractor.js +251 -0
- package/dist/extraction/vue-extractor.js.map +1 -0
- package/dist/extraction/wasm/tree-sitter-lua.wasm +0 -0
- package/dist/extraction/wasm/tree-sitter-luau.wasm +0 -0
- package/dist/extraction/wasm/tree-sitter-pascal.wasm +0 -0
- package/dist/extraction/wasm/tree-sitter-scala.wasm +0 -0
- package/dist/extraction/wasm-runtime-flags.d.ts +38 -0
- package/dist/extraction/wasm-runtime-flags.d.ts.map +1 -0
- package/dist/extraction/wasm-runtime-flags.js +106 -0
- package/dist/extraction/wasm-runtime-flags.js.map +1 -0
- package/dist/graph/index.d.ts +8 -0
- package/dist/graph/index.d.ts.map +1 -0
- package/dist/graph/index.js +13 -0
- package/dist/graph/index.js.map +1 -0
- package/dist/graph/queries.d.ts +106 -0
- package/dist/graph/queries.d.ts.map +1 -0
- package/dist/graph/queries.js +366 -0
- package/dist/graph/queries.js.map +1 -0
- package/dist/graph/traversal.d.ts +127 -0
- package/dist/graph/traversal.d.ts.map +1 -0
- package/dist/graph/traversal.js +531 -0
- package/dist/graph/traversal.js.map +1 -0
- package/dist/index.d.ts +551 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +1165 -0
- package/dist/index.js.map +1 -0
- package/dist/installer/config-writer.d.ts +28 -0
- package/dist/installer/config-writer.d.ts.map +1 -0
- package/dist/installer/config-writer.js +91 -0
- package/dist/installer/config-writer.js.map +1 -0
- package/dist/installer/index.d.ts +92 -0
- package/dist/installer/index.d.ts.map +1 -0
- package/dist/installer/index.js +416 -0
- package/dist/installer/index.js.map +1 -0
- package/dist/installer/instructions-template.d.ts +35 -0
- package/dist/installer/instructions-template.d.ts.map +1 -0
- package/dist/installer/instructions-template.js +51 -0
- package/dist/installer/instructions-template.js.map +1 -0
- package/dist/installer/targets/claude.d.ts +117 -0
- package/dist/installer/targets/claude.d.ts.map +1 -0
- package/dist/installer/targets/claude.js +736 -0
- package/dist/installer/targets/claude.js.map +1 -0
- package/dist/installer/targets/registry.d.ts +19 -0
- package/dist/installer/targets/registry.d.ts.map +1 -0
- package/dist/installer/targets/registry.js +31 -0
- package/dist/installer/targets/registry.js.map +1 -0
- package/dist/installer/targets/shared.d.ts +76 -0
- package/dist/installer/targets/shared.d.ts.map +1 -0
- package/dist/installer/targets/shared.js +256 -0
- package/dist/installer/targets/shared.js.map +1 -0
- package/dist/installer/targets/types.d.ts +84 -0
- package/dist/installer/targets/types.d.ts.map +1 -0
- package/dist/installer/targets/types.js +12 -0
- package/dist/installer/targets/types.js.map +1 -0
- package/dist/isolation/worktree.d.ts +65 -0
- package/dist/isolation/worktree.d.ts.map +1 -0
- package/dist/isolation/worktree.js +231 -0
- package/dist/isolation/worktree.js.map +1 -0
- package/dist/mcp/daemon-paths.d.ts +46 -0
- package/dist/mcp/daemon-paths.d.ts.map +1 -0
- package/dist/mcp/daemon-paths.js +125 -0
- package/dist/mcp/daemon-paths.js.map +1 -0
- package/dist/mcp/daemon.d.ts +161 -0
- package/dist/mcp/daemon.d.ts.map +1 -0
- package/dist/mcp/daemon.js +403 -0
- package/dist/mcp/daemon.js.map +1 -0
- package/dist/mcp/designer-tools.d.ts +33 -0
- package/dist/mcp/designer-tools.d.ts.map +1 -0
- package/dist/mcp/designer-tools.js +313 -0
- package/dist/mcp/designer-tools.js.map +1 -0
- package/dist/mcp/engine.d.ts +105 -0
- package/dist/mcp/engine.d.ts.map +1 -0
- package/dist/mcp/engine.js +270 -0
- package/dist/mcp/engine.js.map +1 -0
- package/dist/mcp/index.d.ts +112 -0
- package/dist/mcp/index.d.ts.map +1 -0
- package/dist/mcp/index.js +477 -0
- package/dist/mcp/index.js.map +1 -0
- package/dist/mcp/proxy.d.ts +81 -0
- package/dist/mcp/proxy.d.ts.map +1 -0
- package/dist/mcp/proxy.js +510 -0
- package/dist/mcp/proxy.js.map +1 -0
- package/dist/mcp/server-instructions.d.ts +18 -0
- package/dist/mcp/server-instructions.d.ts.map +1 -0
- package/dist/mcp/server-instructions.js +77 -0
- package/dist/mcp/server-instructions.js.map +1 -0
- package/dist/mcp/session.d.ts +77 -0
- package/dist/mcp/session.d.ts.map +1 -0
- package/dist/mcp/session.js +294 -0
- package/dist/mcp/session.js.map +1 -0
- package/dist/mcp/spec-tools.d.ts +39 -0
- package/dist/mcp/spec-tools.d.ts.map +1 -0
- package/dist/mcp/spec-tools.js +326 -0
- package/dist/mcp/spec-tools.js.map +1 -0
- package/dist/mcp/tools.d.ts +404 -0
- package/dist/mcp/tools.d.ts.map +1 -0
- package/dist/mcp/tools.js +3087 -0
- package/dist/mcp/tools.js.map +1 -0
- package/dist/mcp/transport.d.ts +188 -0
- package/dist/mcp/transport.d.ts.map +1 -0
- package/dist/mcp/transport.js +343 -0
- package/dist/mcp/transport.js.map +1 -0
- package/dist/mcp/version.d.ts +19 -0
- package/dist/mcp/version.d.ts.map +1 -0
- package/dist/mcp/version.js +71 -0
- package/dist/mcp/version.js.map +1 -0
- package/dist/resolution/callback-synthesizer.d.ts +10 -0
- package/dist/resolution/callback-synthesizer.d.ts.map +1 -0
- package/dist/resolution/callback-synthesizer.js +1300 -0
- package/dist/resolution/callback-synthesizer.js.map +1 -0
- package/dist/resolution/frameworks/cargo-workspace.d.ts +18 -0
- package/dist/resolution/frameworks/cargo-workspace.d.ts.map +1 -0
- package/dist/resolution/frameworks/cargo-workspace.js +225 -0
- package/dist/resolution/frameworks/cargo-workspace.js.map +1 -0
- package/dist/resolution/frameworks/csharp.d.ts +8 -0
- package/dist/resolution/frameworks/csharp.d.ts.map +1 -0
- package/dist/resolution/frameworks/csharp.js +241 -0
- package/dist/resolution/frameworks/csharp.js.map +1 -0
- package/dist/resolution/frameworks/drupal.d.ts +51 -0
- package/dist/resolution/frameworks/drupal.d.ts.map +1 -0
- package/dist/resolution/frameworks/drupal.js +367 -0
- package/dist/resolution/frameworks/drupal.js.map +1 -0
- package/dist/resolution/frameworks/expo-modules.d.ts +3 -0
- package/dist/resolution/frameworks/expo-modules.d.ts.map +1 -0
- package/dist/resolution/frameworks/expo-modules.js +143 -0
- package/dist/resolution/frameworks/expo-modules.js.map +1 -0
- package/dist/resolution/frameworks/express.d.ts +8 -0
- package/dist/resolution/frameworks/express.d.ts.map +1 -0
- package/dist/resolution/frameworks/express.js +308 -0
- package/dist/resolution/frameworks/express.js.map +1 -0
- package/dist/resolution/frameworks/fabric.d.ts +3 -0
- package/dist/resolution/frameworks/fabric.d.ts.map +1 -0
- package/dist/resolution/frameworks/fabric.js +354 -0
- package/dist/resolution/frameworks/fabric.js.map +1 -0
- package/dist/resolution/frameworks/go.d.ts +8 -0
- package/dist/resolution/frameworks/go.d.ts.map +1 -0
- package/dist/resolution/frameworks/go.js +161 -0
- package/dist/resolution/frameworks/go.js.map +1 -0
- package/dist/resolution/frameworks/index.d.ts +48 -0
- package/dist/resolution/frameworks/index.d.ts.map +1 -0
- package/dist/resolution/frameworks/index.js +161 -0
- package/dist/resolution/frameworks/index.js.map +1 -0
- package/dist/resolution/frameworks/java.d.ts +8 -0
- package/dist/resolution/frameworks/java.d.ts.map +1 -0
- package/dist/resolution/frameworks/java.js +504 -0
- package/dist/resolution/frameworks/java.js.map +1 -0
- package/dist/resolution/frameworks/laravel.d.ts +13 -0
- package/dist/resolution/frameworks/laravel.d.ts.map +1 -0
- package/dist/resolution/frameworks/laravel.js +257 -0
- package/dist/resolution/frameworks/laravel.js.map +1 -0
- package/dist/resolution/frameworks/nestjs.d.ts +26 -0
- package/dist/resolution/frameworks/nestjs.d.ts.map +1 -0
- package/dist/resolution/frameworks/nestjs.js +698 -0
- package/dist/resolution/frameworks/nestjs.js.map +1 -0
- package/dist/resolution/frameworks/play.d.ts +19 -0
- package/dist/resolution/frameworks/play.d.ts.map +1 -0
- package/dist/resolution/frameworks/play.js +111 -0
- package/dist/resolution/frameworks/play.js.map +1 -0
- package/dist/resolution/frameworks/python.d.ts +10 -0
- package/dist/resolution/frameworks/python.d.ts.map +1 -0
- package/dist/resolution/frameworks/python.js +396 -0
- package/dist/resolution/frameworks/python.js.map +1 -0
- package/dist/resolution/frameworks/react-native.d.ts +3 -0
- package/dist/resolution/frameworks/react-native.d.ts.map +1 -0
- package/dist/resolution/frameworks/react-native.js +360 -0
- package/dist/resolution/frameworks/react-native.js.map +1 -0
- package/dist/resolution/frameworks/react.d.ts +8 -0
- package/dist/resolution/frameworks/react.d.ts.map +1 -0
- package/dist/resolution/frameworks/react.js +365 -0
- package/dist/resolution/frameworks/react.js.map +1 -0
- package/dist/resolution/frameworks/ruby.d.ts +8 -0
- package/dist/resolution/frameworks/ruby.d.ts.map +1 -0
- package/dist/resolution/frameworks/ruby.js +302 -0
- package/dist/resolution/frameworks/ruby.js.map +1 -0
- package/dist/resolution/frameworks/rust.d.ts +8 -0
- package/dist/resolution/frameworks/rust.d.ts.map +1 -0
- package/dist/resolution/frameworks/rust.js +304 -0
- package/dist/resolution/frameworks/rust.js.map +1 -0
- package/dist/resolution/frameworks/svelte.d.ts +9 -0
- package/dist/resolution/frameworks/svelte.d.ts.map +1 -0
- package/dist/resolution/frameworks/svelte.js +249 -0
- package/dist/resolution/frameworks/svelte.js.map +1 -0
- package/dist/resolution/frameworks/swift-objc.d.ts +37 -0
- package/dist/resolution/frameworks/swift-objc.d.ts.map +1 -0
- package/dist/resolution/frameworks/swift-objc.js +252 -0
- package/dist/resolution/frameworks/swift-objc.js.map +1 -0
- package/dist/resolution/frameworks/swift.d.ts +10 -0
- package/dist/resolution/frameworks/swift.d.ts.map +1 -0
- package/dist/resolution/frameworks/swift.js +400 -0
- package/dist/resolution/frameworks/swift.js.map +1 -0
- package/dist/resolution/frameworks/vue.d.ts +9 -0
- package/dist/resolution/frameworks/vue.d.ts.map +1 -0
- package/dist/resolution/frameworks/vue.js +306 -0
- package/dist/resolution/frameworks/vue.js.map +1 -0
- package/dist/resolution/go-module.d.ts +26 -0
- package/dist/resolution/go-module.d.ts.map +1 -0
- package/dist/resolution/go-module.js +78 -0
- package/dist/resolution/go-module.js.map +1 -0
- package/dist/resolution/import-resolver.d.ts +68 -0
- package/dist/resolution/import-resolver.d.ts.map +1 -0
- package/dist/resolution/import-resolver.js +1275 -0
- package/dist/resolution/import-resolver.js.map +1 -0
- package/dist/resolution/index.d.ts +117 -0
- package/dist/resolution/index.d.ts.map +1 -0
- package/dist/resolution/index.js +895 -0
- package/dist/resolution/index.js.map +1 -0
- package/dist/resolution/lru-cache.d.ts +24 -0
- package/dist/resolution/lru-cache.d.ts.map +1 -0
- package/dist/resolution/lru-cache.js +62 -0
- package/dist/resolution/lru-cache.js.map +1 -0
- package/dist/resolution/name-matcher.d.ts +32 -0
- package/dist/resolution/name-matcher.d.ts.map +1 -0
- package/dist/resolution/name-matcher.js +596 -0
- package/dist/resolution/name-matcher.js.map +1 -0
- package/dist/resolution/path-aliases.d.ts +68 -0
- package/dist/resolution/path-aliases.d.ts.map +1 -0
- package/dist/resolution/path-aliases.js +238 -0
- package/dist/resolution/path-aliases.js.map +1 -0
- package/dist/resolution/spec-link-resolver.d.ts +103 -0
- package/dist/resolution/spec-link-resolver.d.ts.map +1 -0
- package/dist/resolution/spec-link-resolver.js +259 -0
- package/dist/resolution/spec-link-resolver.js.map +1 -0
- package/dist/resolution/strip-comments.d.ts +27 -0
- package/dist/resolution/strip-comments.d.ts.map +1 -0
- package/dist/resolution/strip-comments.js +441 -0
- package/dist/resolution/strip-comments.js.map +1 -0
- package/dist/resolution/swift-objc-bridge.d.ts +134 -0
- package/dist/resolution/swift-objc-bridge.d.ts.map +1 -0
- package/dist/resolution/swift-objc-bridge.js +256 -0
- package/dist/resolution/swift-objc-bridge.js.map +1 -0
- package/dist/resolution/types.d.ts +216 -0
- package/dist/resolution/types.d.ts.map +1 -0
- package/dist/resolution/types.js +8 -0
- package/dist/resolution/types.js.map +1 -0
- package/dist/resolution/workspace-packages.d.ts +48 -0
- package/dist/resolution/workspace-packages.d.ts.map +1 -0
- package/dist/resolution/workspace-packages.js +208 -0
- package/dist/resolution/workspace-packages.js.map +1 -0
- package/dist/search/query-parser.d.ts +57 -0
- package/dist/search/query-parser.d.ts.map +1 -0
- package/dist/search/query-parser.js +177 -0
- package/dist/search/query-parser.js.map +1 -0
- package/dist/search/query-utils.d.ts +71 -0
- package/dist/search/query-utils.d.ts.map +1 -0
- package/dist/search/query-utils.js +380 -0
- package/dist/search/query-utils.js.map +1 -0
- package/dist/server/cli.js +152 -0
- package/dist/server/index.js +12 -0
- package/dist/server/ingest/index.js +18 -0
- package/dist/server/ingest/ingestor.js +506 -0
- package/dist/server/ingest/parser.js +104 -0
- package/dist/server/ingest/pricing.js +78 -0
- package/dist/server/ingest/types.js +9 -0
- package/dist/server/ingest/watcher.js +77 -0
- package/dist/server/package.json +3 -0
- package/dist/server/project-registry.js +101 -0
- package/dist/server/routes/claude.js +868 -0
- package/dist/server/routes/graph.js +211 -0
- package/dist/server/routes/memory.js +272 -0
- package/dist/server/routes/projects.js +197 -0
- package/dist/server/routes/spec.js +265 -0
- package/dist/server/routes/status.js +112 -0
- package/dist/server/routes/workflow.js +212 -0
- package/dist/server/server.js +206 -0
- package/dist/server/static-handler.js +87 -0
- package/dist/sync/git-hooks.d.ts +45 -0
- package/dist/sync/git-hooks.d.ts.map +1 -0
- package/dist/sync/git-hooks.js +225 -0
- package/dist/sync/git-hooks.js.map +1 -0
- package/dist/sync/index.d.ts +19 -0
- package/dist/sync/index.d.ts.map +1 -0
- package/dist/sync/index.js +35 -0
- package/dist/sync/index.js.map +1 -0
- package/dist/sync/watch-policy.d.ts +48 -0
- package/dist/sync/watch-policy.d.ts.map +1 -0
- package/dist/sync/watch-policy.js +124 -0
- package/dist/sync/watch-policy.js.map +1 -0
- package/dist/sync/watcher.d.ts +283 -0
- package/dist/sync/watcher.d.ts.map +1 -0
- package/dist/sync/watcher.js +606 -0
- package/dist/sync/watcher.js.map +1 -0
- package/dist/sync/worktree.d.ts +54 -0
- package/dist/sync/worktree.d.ts.map +1 -0
- package/dist/sync/worktree.js +137 -0
- package/dist/sync/worktree.js.map +1 -0
- package/dist/types.d.ts +623 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +108 -0
- package/dist/types.js.map +1 -0
- package/dist/ui/glyphs.d.ts +42 -0
- package/dist/ui/glyphs.d.ts.map +1 -0
- package/dist/ui/glyphs.js +78 -0
- package/dist/ui/glyphs.js.map +1 -0
- package/dist/ui/shimmer-progress.d.ts +11 -0
- package/dist/ui/shimmer-progress.d.ts.map +1 -0
- package/dist/ui/shimmer-progress.js +90 -0
- package/dist/ui/shimmer-progress.js.map +1 -0
- package/dist/ui/shimmer-worker.d.ts +2 -0
- package/dist/ui/shimmer-worker.d.ts.map +1 -0
- package/dist/ui/shimmer-worker.js +118 -0
- package/dist/ui/shimmer-worker.js.map +1 -0
- package/dist/ui/types.d.ts +17 -0
- package/dist/ui/types.d.ts.map +1 -0
- package/dist/ui/types.js +3 -0
- package/dist/ui/types.js.map +1 -0
- package/dist/utils.d.ts +205 -0
- package/dist/utils.d.ts.map +1 -0
- package/dist/utils.js +549 -0
- package/dist/utils.js.map +1 -0
- package/dist/web/chunk-2AJCHB7P.js +1 -0
- package/dist/web/chunk-2CPLUFCH.js +2 -0
- package/dist/web/chunk-2GBEK2GM.js +1 -0
- package/dist/web/chunk-2I7L37NS.js +1 -0
- package/dist/web/chunk-2NAWAJB5.js +1 -0
- package/dist/web/chunk-2OJBIPE4.js +1 -0
- package/dist/web/chunk-2YUJNZ2Y.js +6 -0
- package/dist/web/chunk-3E2WB6D5.js +1 -0
- package/dist/web/chunk-3EBFYSCH.js +2 -0
- package/dist/web/chunk-3QCQ4BXS.js +1 -0
- package/dist/web/chunk-42XVAQ6I.js +1 -0
- package/dist/web/chunk-45QHGCB4.js +17 -0
- package/dist/web/chunk-4IMMPEYM.js +1 -0
- package/dist/web/chunk-4TJQJPCZ.js +1 -0
- package/dist/web/chunk-4WZIHTPC.js +1 -0
- package/dist/web/chunk-4YVSYOSD.js +1 -0
- package/dist/web/chunk-5BQIOYKW.js +1 -0
- package/dist/web/chunk-5HGWHUJA.js +1 -0
- package/dist/web/chunk-5Y244R4G.js +1 -0
- package/dist/web/chunk-6RRDPT5Z.js +1 -0
- package/dist/web/chunk-6VKB2ZWM.js +1 -0
- package/dist/web/chunk-7DMFVTU4.js +1 -0
- package/dist/web/chunk-7RNS77UP.js +1 -0
- package/dist/web/chunk-7SMPKVEP.js +1 -0
- package/dist/web/chunk-A5R3MJMO.js +1 -0
- package/dist/web/chunk-ASZ77FMZ.js +1 -0
- package/dist/web/chunk-AZJVTPLU.js +1 -0
- package/dist/web/chunk-B3YPFY6A.js +1 -0
- package/dist/web/chunk-BLBRMCN2.js +1 -0
- package/dist/web/chunk-BMIAXD2V.js +2 -0
- package/dist/web/chunk-BUXWEHIY.js +1 -0
- package/dist/web/chunk-BYZFQSM6.js +1 -0
- package/dist/web/chunk-D5OCNEJA.js +2 -0
- package/dist/web/chunk-DLQPZWSI.css +1 -0
- package/dist/web/chunk-DTRN7FZR.js +1 -0
- package/dist/web/chunk-DYRFLPJA.js +1 -0
- package/dist/web/chunk-E3J3CXR5.js +1 -0
- package/dist/web/chunk-E44X4RH2.js +1 -0
- package/dist/web/chunk-E73OX2P7.js +1 -0
- package/dist/web/chunk-EAXRKDLV.js +1 -0
- package/dist/web/chunk-EBKKDHYI.js +1 -0
- package/dist/web/chunk-EE7V7Q5P.js +1 -0
- package/dist/web/chunk-EKY2FUHU.js +1 -0
- package/dist/web/chunk-EMGMOEVR.js +1 -0
- package/dist/web/chunk-EP6XOPXH.js +1 -0
- package/dist/web/chunk-ESGDLJOJ.js +1 -0
- package/dist/web/chunk-ETJG7NCY.js +1 -0
- package/dist/web/chunk-EUUEFEDI.js +1 -0
- package/dist/web/chunk-FGNZDHTL.js +11 -0
- package/dist/web/chunk-FHZHD2ZG.js +1 -0
- package/dist/web/chunk-FIJW2UNJ.js +1 -0
- package/dist/web/chunk-FMV5PXRC.js +5 -0
- package/dist/web/chunk-G7VZT5KB.js +3 -0
- package/dist/web/chunk-GR72OOCN.js +1 -0
- package/dist/web/chunk-GRZYXPSO.js +7 -0
- package/dist/web/chunk-GWPVKJIY.js +1 -0
- package/dist/web/chunk-GYGPS3AN.js +1 -0
- package/dist/web/chunk-H7AF7YS4.js +1 -0
- package/dist/web/chunk-HDZDQILN.js +1 -0
- package/dist/web/chunk-HMK6UO6N.js +1 -0
- package/dist/web/chunk-HZA6NEAB.js +1 -0
- package/dist/web/chunk-IHEE5NYJ.js +1 -0
- package/dist/web/chunk-ISNEBICW.js +1 -0
- package/dist/web/chunk-J2GZVLHH.js +1 -0
- package/dist/web/chunk-JTFXTIPE.js +903 -0
- package/dist/web/chunk-L37MTFSG.js +3 -0
- package/dist/web/chunk-LB6JPLX2.js +1 -0
- package/dist/web/chunk-LNSVDHCI.js +1 -0
- package/dist/web/chunk-LVGIY3SO.js +1 -0
- package/dist/web/chunk-LXLHIHEN.js +1 -0
- package/dist/web/chunk-MC4DFIHG.js +1 -0
- package/dist/web/chunk-MVOMVPYB.js +1 -0
- package/dist/web/chunk-N6SS4G6S.js +1 -0
- package/dist/web/chunk-NTBJG6SJ.js +1 -0
- package/dist/web/chunk-NUDB3Q2Y.js +3 -0
- package/dist/web/chunk-NZEZCT65.js +1 -0
- package/dist/web/chunk-OXEF5E3E.js +1 -0
- package/dist/web/chunk-PDN6QYGJ.js +4 -0
- package/dist/web/chunk-PGGJPDJG.js +1 -0
- package/dist/web/chunk-PUYSJNJR.js +1 -0
- package/dist/web/chunk-Q2RVFS45.js +1 -0
- package/dist/web/chunk-Q7L6LLAK.js +1 -0
- package/dist/web/chunk-QCMKJIWY.js +1 -0
- package/dist/web/chunk-QH6CF3M3.js +1 -0
- package/dist/web/chunk-QQ5LD7PI.js +1 -0
- package/dist/web/chunk-QR6L3KAC.js +1 -0
- package/dist/web/chunk-R2DLK4HO.js +1 -0
- package/dist/web/chunk-R5W2MDZN.js +1 -0
- package/dist/web/chunk-RD6TVPOT.js +1 -0
- package/dist/web/chunk-RKY4EJYJ.js +1 -0
- package/dist/web/chunk-RONYWVY7.js +1 -0
- package/dist/web/chunk-RXKXYF2C.js +1 -0
- package/dist/web/chunk-SBWU7JFC.js +1 -0
- package/dist/web/chunk-SEXBRGYK.js +1 -0
- package/dist/web/chunk-SHPTC4RL.js +1 -0
- package/dist/web/chunk-SUZYBYDW.js +1 -0
- package/dist/web/chunk-SWKJRNYY.js +1 -0
- package/dist/web/chunk-T66XVKGB.js +1 -0
- package/dist/web/chunk-T7AZ65JP.js +1 -0
- package/dist/web/chunk-TCZDVOHD.js +1 -0
- package/dist/web/chunk-TPTITA3V.js +1 -0
- package/dist/web/chunk-TR335633.js +1 -0
- package/dist/web/chunk-UBOZGQNK.js +1 -0
- package/dist/web/chunk-UR5KDXPX.js +1 -0
- package/dist/web/chunk-UR6O2GEH.js +1 -0
- package/dist/web/chunk-UTNMGWTP.js +1 -0
- package/dist/web/chunk-UYC52MBC.js +1 -0
- package/dist/web/chunk-VECWMHJP.js +1 -0
- package/dist/web/chunk-VUACT35R.js +3 -0
- package/dist/web/chunk-VZI7H4SZ.js +1 -0
- package/dist/web/chunk-WAI2JMZP.js +1 -0
- package/dist/web/chunk-WB6YHOD4.js +1 -0
- package/dist/web/chunk-WBT64AWV.js +1 -0
- package/dist/web/chunk-WCKHQIYN.js +1 -0
- package/dist/web/chunk-WDU3WICG.js +1 -0
- package/dist/web/chunk-WFXJIXZE.js +4 -0
- package/dist/web/chunk-WLIMNDS3.js +1 -0
- package/dist/web/chunk-WTGYRH3Z.js +298 -0
- package/dist/web/chunk-WXTCVDTP.js +1 -0
- package/dist/web/chunk-X2HTISHL.js +1 -0
- package/dist/web/chunk-XCDHWLVH.js +1 -0
- package/dist/web/chunk-Y3H6FFUZ.js +1 -0
- package/dist/web/chunk-Y4F5ULGJ.js +1 -0
- package/dist/web/chunk-YAMRN47K.js +2 -0
- package/dist/web/chunk-YEGKAAEE.js +1 -0
- package/dist/web/chunk-YM2KU57F.js +1 -0
- package/dist/web/chunk-YRERBP6T.js +1 -0
- package/dist/web/chunk-ZLV4VCDG.js +3 -0
- package/dist/web/chunk-ZTVI5KFF.js +1 -0
- package/dist/web/favicon-16.png +0 -0
- package/dist/web/favicon-180.png +0 -0
- package/dist/web/favicon-32.png +0 -0
- package/dist/web/favicon-512.png +0 -0
- package/dist/web/favicon-small.svg +15 -0
- package/dist/web/favicon.ico +0 -0
- package/dist/web/favicon.svg +20 -0
- package/dist/web/index.html +145 -0
- package/dist/web/main-ESADRXN2.css +1 -0
- package/dist/web/main-R53HA54V.js +1 -0
- package/dist/web/media/codicon-LN6W7LCM.ttf +0 -0
- package/dist/web/styles-KSOPUVDA.css +1 -0
- package/dist/web/sw.js +69 -0
- package/dist/workflows/condition-evaluator.d.ts +75 -0
- package/dist/workflows/condition-evaluator.d.ts.map +1 -0
- package/dist/workflows/condition-evaluator.js +282 -0
- package/dist/workflows/condition-evaluator.js.map +1 -0
- package/dist/workflows/defaults/claude-design-implement.yaml +336 -0
- package/dist/workflows/defaults/index.d.ts +26 -0
- package/dist/workflows/defaults/index.d.ts.map +1 -0
- package/dist/workflows/defaults/index.js +94 -0
- package/dist/workflows/defaults/index.js.map +1 -0
- package/dist/workflows/defaults/spec-author.yaml +214 -0
- package/dist/workflows/defaults/spec-fix.yaml +110 -0
- package/dist/workflows/defaults/spec-implement.yaml +150 -0
- package/dist/workflows/defaults/spec-relink.yaml +81 -0
- package/dist/workflows/defaults/spec-verify.yaml +51 -0
- package/dist/workflows/discovery.d.ts +46 -0
- package/dist/workflows/discovery.d.ts.map +1 -0
- package/dist/workflows/discovery.js +193 -0
- package/dist/workflows/discovery.js.map +1 -0
- package/dist/workflows/executor.d.ts +83 -0
- package/dist/workflows/executor.d.ts.map +1 -0
- package/dist/workflows/executor.js +624 -0
- package/dist/workflows/executor.js.map +1 -0
- package/dist/workflows/runners/approval.d.ts +18 -0
- package/dist/workflows/runners/approval.d.ts.map +1 -0
- package/dist/workflows/runners/approval.js +34 -0
- package/dist/workflows/runners/approval.js.map +1 -0
- package/dist/workflows/runners/bash.d.ts +13 -0
- package/dist/workflows/runners/bash.d.ts.map +1 -0
- package/dist/workflows/runners/bash.js +143 -0
- package/dist/workflows/runners/bash.js.map +1 -0
- package/dist/workflows/runners/cancel.d.ts +10 -0
- package/dist/workflows/runners/cancel.d.ts.map +1 -0
- package/dist/workflows/runners/cancel.js +19 -0
- package/dist/workflows/runners/cancel.js.map +1 -0
- package/dist/workflows/runners/prompt.d.ts +28 -0
- package/dist/workflows/runners/prompt.d.ts.map +1 -0
- package/dist/workflows/runners/prompt.js +212 -0
- package/dist/workflows/runners/prompt.js.map +1 -0
- package/dist/workflows/runners/script.d.ts +17 -0
- package/dist/workflows/runners/script.d.ts.map +1 -0
- package/dist/workflows/runners/script.js +155 -0
- package/dist/workflows/runners/script.js.map +1 -0
- package/dist/workflows/runners/types.d.ts +51 -0
- package/dist/workflows/runners/types.d.ts.map +1 -0
- package/dist/workflows/runners/types.js +13 -0
- package/dist/workflows/runners/types.js.map +1 -0
- package/dist/workflows/schemas/workflow.d.ts +166 -0
- package/dist/workflows/schemas/workflow.d.ts.map +1 -0
- package/dist/workflows/schemas/workflow.js +437 -0
- package/dist/workflows/schemas/workflow.js.map +1 -0
- package/hooks/hooks.json +11 -0
- package/package.json +7 -3
- package/scripts/offline-install.sh +19 -6
- package/selectors.json +41 -0
- /package/commands/{cg-explore.md → ss-explore.md} +0 -0
- /package/commands/{cg-impact.md → ss-impact.md} +0 -0
- /package/commands/{cg-relink.md → ss-relink.md} +0 -0
- /package/commands/{cg-spec.md → ss-spec.md} +0 -0
- /package/commands/{cg-sync.md → ss-sync.md} +0 -0
- /package/commands/{cg-trace.md → ss-trace.md} +0 -0
|
@@ -0,0 +1,868 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Claude Code analytics routes.
|
|
3
|
+
*
|
|
4
|
+
* All queries hit specship's SQLite directly. The ingest worker
|
|
5
|
+
* (`@selvakumaresra/specship-ingest`) writes to claude_* tables; this layer
|
|
6
|
+
* just rolls up.
|
|
7
|
+
*
|
|
8
|
+
* Endpoints:
|
|
9
|
+
* GET /api/claude/projects — every indexed Claude project
|
|
10
|
+
* GET /api/claude/sessions?project=&limit= — sessions list
|
|
11
|
+
* GET /api/claude/session/:id — session detail (prompts + tools)
|
|
12
|
+
* GET /api/claude/heatmap?range= — file/tool/subagent heatmaps
|
|
13
|
+
* GET /api/claude/costs?range= — cost rollup, timeseries, per-model
|
|
14
|
+
* GET /api/claude/compare — per-project cost comparison
|
|
15
|
+
* GET /api/claude/tips — rule-based tips engine output
|
|
16
|
+
* POST /api/claude/ingest — force a one-shot ingest pass
|
|
17
|
+
*/
|
|
18
|
+
import { decodeProjectSlug } from '../ingest/index.js';
|
|
19
|
+
/**
|
|
20
|
+
* Normalize a `?project=` filter value to the form stored in
|
|
21
|
+
* claude_sessions.project_path. The Sessions page (and any other UI
|
|
22
|
+
* surface that uses ProjectsService.projectQuery()) sends the directory
|
|
23
|
+
* SLUG that names the Claude Code transcript dir (e.g.
|
|
24
|
+
* `-Users-foo-projects-bar`), but the ingestor writes the DECODED path
|
|
25
|
+
* (e.g. `/Users/foo/projects/bar`) into project_path because that's the
|
|
26
|
+
* value the agent actually identifies the project by. Comparing slug
|
|
27
|
+
* against path always fails and the list comes back empty even though
|
|
28
|
+
* the rows are there — fix by decoding the slug form here.
|
|
29
|
+
*
|
|
30
|
+
* A path passed in directly (doesn't start with `-`) round-trips
|
|
31
|
+
* unchanged, so curl-by-path and the old behavior both keep working.
|
|
32
|
+
*/
|
|
33
|
+
function normalizeProjectFilter(input) {
|
|
34
|
+
return decodeProjectSlug(input);
|
|
35
|
+
}
|
|
36
|
+
const RANGE_WINDOW_MS = {
|
|
37
|
+
today: 24 * 60 * 60 * 1000,
|
|
38
|
+
week: 7 * 24 * 60 * 60 * 1000,
|
|
39
|
+
month: 30 * 24 * 60 * 60 * 1000,
|
|
40
|
+
all: Number.MAX_SAFE_INTEGER,
|
|
41
|
+
};
|
|
42
|
+
function rangeKey(input) {
|
|
43
|
+
if (input === 'today' || input === 'week' || input === 'month' || input === 'all')
|
|
44
|
+
return input;
|
|
45
|
+
return 'week';
|
|
46
|
+
}
|
|
47
|
+
function rangeStart(key) {
|
|
48
|
+
if (key === 'all')
|
|
49
|
+
return 0;
|
|
50
|
+
return Date.now() - RANGE_WINDOW_MS[key];
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Bounds of the period immediately BEFORE the current range window — used for
|
|
54
|
+
* week-over-week (wowDelta) comparisons. For 'all' there's no prior period, so
|
|
55
|
+
* both bounds collapse to 0 and callers should treat the delta as 0.
|
|
56
|
+
*/
|
|
57
|
+
function priorWindow(key) {
|
|
58
|
+
if (key === 'all')
|
|
59
|
+
return { start: 0, end: 0 };
|
|
60
|
+
const w = RANGE_WINDOW_MS[key];
|
|
61
|
+
const end = Date.now() - w;
|
|
62
|
+
return { start: end - w, end };
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Get the internal SQLite handle off the DatabaseConnection so we can run
|
|
66
|
+
* Claude-specific aggregate queries directly. SpecShip exposes this via
|
|
67
|
+
* `getDb()`-style accessors. Falls back to digging via the queries property.
|
|
68
|
+
*/
|
|
69
|
+
function getDb(cg) {
|
|
70
|
+
// SpecShip exposes the underlying DB via its DatabaseConnection. Look it
|
|
71
|
+
// up via the private field as a fallback — works because it's the same
|
|
72
|
+
// shape regardless of which adapter (better-sqlite3 / node:sqlite) is active.
|
|
73
|
+
const anyCg = cg;
|
|
74
|
+
if (anyCg.db?.getDb)
|
|
75
|
+
return anyCg.db.getDb();
|
|
76
|
+
if (anyCg.queries?.db)
|
|
77
|
+
return anyCg.queries.db;
|
|
78
|
+
throw new Error('specship DB handle not accessible from server context');
|
|
79
|
+
}
|
|
80
|
+
export async function registerClaudeRoutes(app) {
|
|
81
|
+
/**
|
|
82
|
+
* Analytics routes share one SQLite — the boot-time "primary" project's
|
|
83
|
+
* specship.db hosts the cross-project claude_* tables. Without a primary
|
|
84
|
+
* the JSONL ingest has nowhere to write and there's nothing to query, so
|
|
85
|
+
* every analytics handler asks here first.
|
|
86
|
+
*/
|
|
87
|
+
function requirePrimary(reply) {
|
|
88
|
+
if (!app.primaryCg) {
|
|
89
|
+
reply.code(409).send({ error: 'analytics unavailable: no primary project configured', code: 'no_primary' });
|
|
90
|
+
return null;
|
|
91
|
+
}
|
|
92
|
+
return app.primaryCg;
|
|
93
|
+
}
|
|
94
|
+
app.get('/api/claude/projects', async (_req, reply) => {
|
|
95
|
+
const cg = requirePrimary(reply);
|
|
96
|
+
if (!cg)
|
|
97
|
+
return;
|
|
98
|
+
const db = getDb(cg);
|
|
99
|
+
const rows = db.prepare(`
|
|
100
|
+
SELECT p.path, p.name, p.first_seen, p.last_seen,
|
|
101
|
+
COUNT(s.id) as sessions,
|
|
102
|
+
COALESCE(SUM(s.total_cost_usd), 0) as cost,
|
|
103
|
+
COALESCE(SUM(s.total_cache_read_tokens), 0) as cacheRead,
|
|
104
|
+
COALESCE(SUM(s.total_cache_creation_tokens + s.total_input_tokens), 0) as totalInput,
|
|
105
|
+
COALESCE(SUM(s.prompt_count), 0) as prompts
|
|
106
|
+
FROM claude_projects p
|
|
107
|
+
LEFT JOIN claude_sessions s ON s.project_path = p.path
|
|
108
|
+
GROUP BY p.path
|
|
109
|
+
ORDER BY cost DESC
|
|
110
|
+
`).all();
|
|
111
|
+
return { projects: rows };
|
|
112
|
+
});
|
|
113
|
+
app.get('/api/claude/sessions', async (req, reply) => {
|
|
114
|
+
const cg = requirePrimary(reply);
|
|
115
|
+
if (!cg)
|
|
116
|
+
return;
|
|
117
|
+
const db = getDb(cg);
|
|
118
|
+
const limit = Math.min(parseInt(req.query.limit ?? '100', 10) || 100, 500);
|
|
119
|
+
const since = rangeStart(rangeKey(req.query.range));
|
|
120
|
+
const params = [since];
|
|
121
|
+
let whereProject = '';
|
|
122
|
+
if (req.query.project) {
|
|
123
|
+
whereProject = ' AND project_path = ?';
|
|
124
|
+
params.push(normalizeProjectFilter(req.query.project));
|
|
125
|
+
}
|
|
126
|
+
params.push(limit);
|
|
127
|
+
const sessions = db.prepare(`
|
|
128
|
+
SELECT * FROM claude_sessions
|
|
129
|
+
WHERE started_at >= ?${whereProject}
|
|
130
|
+
ORDER BY started_at DESC
|
|
131
|
+
LIMIT ?
|
|
132
|
+
`).all(...params);
|
|
133
|
+
return { sessions };
|
|
134
|
+
});
|
|
135
|
+
app.get('/api/claude/session/:id', async (req, reply) => {
|
|
136
|
+
const cg = requirePrimary(reply);
|
|
137
|
+
if (!cg)
|
|
138
|
+
return;
|
|
139
|
+
const db = getDb(cg);
|
|
140
|
+
const session = db.prepare('SELECT * FROM claude_sessions WHERE id = ?').get(req.params.id);
|
|
141
|
+
if (!session)
|
|
142
|
+
return reply.code(404).send({ error: 'session not found' });
|
|
143
|
+
const prompts = db.prepare(`
|
|
144
|
+
SELECT * FROM claude_prompts WHERE session_id = ? ORDER BY ts ASC
|
|
145
|
+
`).all(req.params.id);
|
|
146
|
+
const toolCalls = db.prepare(`
|
|
147
|
+
SELECT * FROM claude_tool_calls WHERE session_id = ? ORDER BY ts ASC
|
|
148
|
+
`).all(req.params.id);
|
|
149
|
+
// Per-prompt wall-clock duration: the gap from this prompt to the next one
|
|
150
|
+
// (last prompt runs to the session's end). Lets the UI show "how long did
|
|
151
|
+
// this turn take" without a separate per-event timeline.
|
|
152
|
+
const endedAt = session.ended_at ?? null;
|
|
153
|
+
prompts.forEach((p, i) => {
|
|
154
|
+
const next = prompts[i + 1];
|
|
155
|
+
const end = next ? next.ts : (endedAt ?? p.ts);
|
|
156
|
+
p.durationMs = Math.max(0, end - p.ts);
|
|
157
|
+
});
|
|
158
|
+
return { session, prompts, toolCalls };
|
|
159
|
+
});
|
|
160
|
+
/**
|
|
161
|
+
* SSE event stream for a single session — pushes new prompts and tool
|
|
162
|
+
* calls as the JSONL ingest watcher lands them, so the dashboard's
|
|
163
|
+
* Session Detail page can update without polling. Mirrors the shape used
|
|
164
|
+
* by /api/workflows/runs/:id/events.
|
|
165
|
+
*
|
|
166
|
+
* Polling cadence inside the loop is 500 ms — fast enough that the
|
|
167
|
+
* end-to-end "user hit Enter in Claude Code → prompt visible in
|
|
168
|
+
* dashboard" latency stays well under one second (300 ms watcher
|
|
169
|
+
* debounce + ≤50 ms ingest + ≤500 ms poll). Heartbeat every 15 s
|
|
170
|
+
* keeps idle connections alive past any proxy or browser tab-throttle
|
|
171
|
+
* cutoff.
|
|
172
|
+
*
|
|
173
|
+
* The client (LiveSessionTail in session-detail.ts) doesn't merge events
|
|
174
|
+
* incrementally — it just calls resource.refetch() on every event since
|
|
175
|
+
* the detail endpoint is local + cheap. Server-side, that means we only
|
|
176
|
+
* need to push enough info for the client to know "something changed"
|
|
177
|
+
* (id + ts), not the full row payloads. We send the full row anyway so
|
|
178
|
+
* a future client could merge incrementally without an API change.
|
|
179
|
+
*/
|
|
180
|
+
app.get('/api/claude/session/:id/events', async (req, reply) => {
|
|
181
|
+
const cg = requirePrimary(reply);
|
|
182
|
+
if (!cg)
|
|
183
|
+
return;
|
|
184
|
+
const db = getDb(cg);
|
|
185
|
+
const sessionId = req.params.id;
|
|
186
|
+
// Confirm the session exists before opening the stream — saves a
|
|
187
|
+
// doomed connection from polling forever against a typo.
|
|
188
|
+
const sessionRow = db.prepare('SELECT id FROM claude_sessions WHERE id = ?').get(sessionId);
|
|
189
|
+
if (!sessionRow) {
|
|
190
|
+
return reply.code(404).send({ error: 'session not found' });
|
|
191
|
+
}
|
|
192
|
+
// Resume points — clients can pick up after a disconnect without
|
|
193
|
+
// re-receiving every prompt. Defaults to "now" so an opening client
|
|
194
|
+
// only sees future events.
|
|
195
|
+
let lastPromptTs = parseInt(req.query.sincePromptTs ?? '0', 10);
|
|
196
|
+
let lastToolTs = parseInt(req.query.sinceToolTs ?? '0', 10);
|
|
197
|
+
if (!lastPromptTs || Number.isNaN(lastPromptTs)) {
|
|
198
|
+
const row = db.prepare('SELECT MAX(ts) as m FROM claude_prompts WHERE session_id = ?').get(sessionId);
|
|
199
|
+
lastPromptTs = row?.m ?? 0;
|
|
200
|
+
}
|
|
201
|
+
if (!lastToolTs || Number.isNaN(lastToolTs)) {
|
|
202
|
+
const row = db.prepare('SELECT MAX(ts) as m FROM claude_tool_calls WHERE session_id = ?').get(sessionId);
|
|
203
|
+
lastToolTs = row?.m ?? 0;
|
|
204
|
+
}
|
|
205
|
+
reply.raw.setHeader('Content-Type', 'text/event-stream');
|
|
206
|
+
reply.raw.setHeader('Cache-Control', 'no-cache');
|
|
207
|
+
reply.raw.setHeader('Connection', 'keep-alive');
|
|
208
|
+
reply.raw.setHeader('X-Accel-Buffering', 'no'); // nginx-friendly
|
|
209
|
+
reply.raw.flushHeaders();
|
|
210
|
+
// Initial snapshot — gives the client a clean baseline (so it knows
|
|
211
|
+
// SSE is wired up even before any new event fires) and the cursor
|
|
212
|
+
// positions it should resume from on reconnect.
|
|
213
|
+
reply.raw.write(`event: snapshot\ndata: ${JSON.stringify({ sessionId, lastPromptTs, lastToolTs })}\n\n`);
|
|
214
|
+
let closed = false;
|
|
215
|
+
req.raw.on('close', () => { closed = true; });
|
|
216
|
+
const newPromptsStmt = db.prepare('SELECT * FROM claude_prompts WHERE session_id = ? AND ts > ? ORDER BY ts ASC');
|
|
217
|
+
const newToolsStmt = db.prepare('SELECT * FROM claude_tool_calls WHERE session_id = ? AND ts > ? ORDER BY ts ASC');
|
|
218
|
+
let lastHeartbeat = Date.now();
|
|
219
|
+
const tick = () => {
|
|
220
|
+
if (closed)
|
|
221
|
+
return;
|
|
222
|
+
try {
|
|
223
|
+
const freshPrompts = newPromptsStmt.all(sessionId, lastPromptTs);
|
|
224
|
+
for (const p of freshPrompts) {
|
|
225
|
+
reply.raw.write(`event: prompt_added\ndata: ${JSON.stringify(p)}\n\n`);
|
|
226
|
+
if (p.ts > lastPromptTs)
|
|
227
|
+
lastPromptTs = p.ts;
|
|
228
|
+
}
|
|
229
|
+
const freshTools = newToolsStmt.all(sessionId, lastToolTs);
|
|
230
|
+
for (const t of freshTools) {
|
|
231
|
+
reply.raw.write(`event: tool_call_added\ndata: ${JSON.stringify(t)}\n\n`);
|
|
232
|
+
if (t.ts > lastToolTs)
|
|
233
|
+
lastToolTs = t.ts;
|
|
234
|
+
}
|
|
235
|
+
// Heartbeat every 15 s — keeps proxies / browser tab throttles
|
|
236
|
+
// from killing an otherwise-idle connection.
|
|
237
|
+
const now = Date.now();
|
|
238
|
+
if (now - lastHeartbeat >= 15_000) {
|
|
239
|
+
reply.raw.write(`: keepalive ${now}\n\n`);
|
|
240
|
+
lastHeartbeat = now;
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
catch (err) {
|
|
244
|
+
// Surface DB errors to the client as a named event and end the
|
|
245
|
+
// stream — the client's onError handler flips to polling.
|
|
246
|
+
reply.raw.write(`event: stream_error\ndata: ${JSON.stringify({ message: err instanceof Error ? err.message : String(err) })}\n\n`);
|
|
247
|
+
reply.raw.end();
|
|
248
|
+
closed = true;
|
|
249
|
+
return;
|
|
250
|
+
}
|
|
251
|
+
setTimeout(tick, 500);
|
|
252
|
+
};
|
|
253
|
+
void tick();
|
|
254
|
+
});
|
|
255
|
+
/**
|
|
256
|
+
* Session-level aggregation for the Session Detail "Session summary" panel.
|
|
257
|
+
* Three things callers can't cheaply derive client-side from the per-prompt
|
|
258
|
+
* list: tool usage rolled up by name, files touched across all turns, and
|
|
259
|
+
* slash-command / skill invocation counts (which require either a regex
|
|
260
|
+
* over every prompt's text or json_extract over every Skill tool input).
|
|
261
|
+
* Computed once per session-detail page load (and after refresh) — cheap
|
|
262
|
+
* enough to recompute on every call, no caching.
|
|
263
|
+
*/
|
|
264
|
+
app.get('/api/claude/session/:id/summary', async (req, reply) => {
|
|
265
|
+
const cg = requirePrimary(reply);
|
|
266
|
+
if (!cg)
|
|
267
|
+
return;
|
|
268
|
+
const db = getDb(cg);
|
|
269
|
+
const sessionId = req.params.id;
|
|
270
|
+
const session = db.prepare('SELECT id, started_at, ended_at FROM claude_sessions WHERE id = ?').get(sessionId);
|
|
271
|
+
if (!session)
|
|
272
|
+
return reply.code(404).send({ error: 'session not found' });
|
|
273
|
+
// Tools used in this session, rolled up by name. Mirrors the heatmap
|
|
274
|
+
// tool query but scoped to one session, so the panel can answer "what
|
|
275
|
+
// did Claude DO in this session" at a glance.
|
|
276
|
+
const byTool = db.prepare(`
|
|
277
|
+
SELECT tool_name as name, COUNT(*) as calls, COALESCE(SUM(result_length), 0) as totalBytes
|
|
278
|
+
FROM claude_tool_calls
|
|
279
|
+
WHERE session_id = ?
|
|
280
|
+
GROUP BY tool_name
|
|
281
|
+
ORDER BY calls DESC
|
|
282
|
+
`).all(sessionId);
|
|
283
|
+
// Models used. Most sessions stay on one model but sidechains can fan
|
|
284
|
+
// out to Haiku, so this surfaces multi-model spend that the session-level
|
|
285
|
+
// last_model column hides.
|
|
286
|
+
const byModel = db.prepare(`
|
|
287
|
+
SELECT model, COUNT(*) as prompts, COALESCE(SUM(cost_usd), 0) as cost
|
|
288
|
+
FROM claude_prompts
|
|
289
|
+
WHERE session_id = ? AND model IS NOT NULL
|
|
290
|
+
GROUP BY model
|
|
291
|
+
ORDER BY cost DESC
|
|
292
|
+
`).all(sessionId);
|
|
293
|
+
// Files touched via Read/Edit/Write/NotebookEdit. input_summary IS the
|
|
294
|
+
// file path for those tools (set in the ingestor's parser). Group by
|
|
295
|
+
// path, count ops, also record which tool last touched it so the UI
|
|
296
|
+
// can show a "last op" hint (Read vs Edit).
|
|
297
|
+
const filesTouched = db.prepare(`
|
|
298
|
+
SELECT input_summary as path, COUNT(*) as ops,
|
|
299
|
+
(SELECT tool_name FROM claude_tool_calls AS inner_tc
|
|
300
|
+
WHERE inner_tc.session_id = tc.session_id
|
|
301
|
+
AND inner_tc.input_summary = tc.input_summary
|
|
302
|
+
ORDER BY inner_tc.ts DESC LIMIT 1) as lastOp
|
|
303
|
+
FROM claude_tool_calls AS tc
|
|
304
|
+
WHERE tc.session_id = ?
|
|
305
|
+
AND tc.tool_name IN ('Read','Edit','Write','NotebookEdit','MultiEdit')
|
|
306
|
+
AND tc.input_summary != ''
|
|
307
|
+
GROUP BY tc.input_summary
|
|
308
|
+
ORDER BY ops DESC
|
|
309
|
+
LIMIT 30
|
|
310
|
+
`).all(sessionId);
|
|
311
|
+
// Skills invoked via the Skill tool. The ingestor stashes the
|
|
312
|
+
// JSON-serialized input under input_summary, so json_extract pulls the
|
|
313
|
+
// skill name straight out without needing the v7 input_json column to
|
|
314
|
+
// be backfilled.
|
|
315
|
+
const skills = db.prepare(`
|
|
316
|
+
SELECT
|
|
317
|
+
COALESCE(json_extract(input_summary, '$.skill'), json_extract(input_summary, '$.skill_name'), 'unknown') as name,
|
|
318
|
+
COUNT(*) as count
|
|
319
|
+
FROM claude_tool_calls
|
|
320
|
+
WHERE session_id = ? AND tool_name = 'Skill'
|
|
321
|
+
GROUP BY name
|
|
322
|
+
ORDER BY count DESC
|
|
323
|
+
`).all(sessionId);
|
|
324
|
+
// Slash commands invoked. Claude Code wraps each one in a
|
|
325
|
+
// <command-name>/foo</command-name> tag in the user-prompt text.
|
|
326
|
+
// Pull every prompt that has that tag and regex it client-side here
|
|
327
|
+
// (SQLite has no built-in regex). Cheap — most sessions have <50.
|
|
328
|
+
const taggedPrompts = db.prepare(`
|
|
329
|
+
SELECT text FROM claude_prompts WHERE session_id = ? AND text LIKE '%<command-name>%'
|
|
330
|
+
`).all(sessionId);
|
|
331
|
+
const cmdCounts = new Map();
|
|
332
|
+
const cmdRe = /<command-name>([^<]+)<\/command-name>/g;
|
|
333
|
+
for (const { text } of taggedPrompts) {
|
|
334
|
+
if (!text)
|
|
335
|
+
continue;
|
|
336
|
+
for (const m of text.matchAll(cmdRe)) {
|
|
337
|
+
const raw = m[1]?.trim();
|
|
338
|
+
if (!raw)
|
|
339
|
+
continue;
|
|
340
|
+
cmdCounts.set(raw, (cmdCounts.get(raw) ?? 0) + 1);
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
const slashCommands = Array.from(cmdCounts.entries())
|
|
344
|
+
.map(([name, count]) => ({ name, count }))
|
|
345
|
+
.sort((a, b) => b.count - a.count);
|
|
346
|
+
const durationMs = session.started_at && session.ended_at
|
|
347
|
+
? Math.max(0, session.ended_at - session.started_at)
|
|
348
|
+
: 0;
|
|
349
|
+
return {
|
|
350
|
+
sessionId,
|
|
351
|
+
byTool,
|
|
352
|
+
byModel,
|
|
353
|
+
slashCommands,
|
|
354
|
+
skills,
|
|
355
|
+
filesTouched,
|
|
356
|
+
durationMs,
|
|
357
|
+
};
|
|
358
|
+
});
|
|
359
|
+
app.get('/api/claude/heatmap', async (req, reply) => {
|
|
360
|
+
const cg = requirePrimary(reply);
|
|
361
|
+
if (!cg)
|
|
362
|
+
return;
|
|
363
|
+
const db = getDb(cg);
|
|
364
|
+
const since = rangeStart(rangeKey(req.query.range));
|
|
365
|
+
// Files heatmap — input_summary doubles as the file path for Read/Edit/Write.
|
|
366
|
+
const files = db.prepare(`
|
|
367
|
+
SELECT input_summary as path, COUNT(*) as calls, SUM(result_length) as resultBytes
|
|
368
|
+
FROM claude_tool_calls
|
|
369
|
+
WHERE ts >= ? AND tool_name IN ('Read','Edit','Write','NotebookEdit') AND input_summary != ''
|
|
370
|
+
GROUP BY input_summary
|
|
371
|
+
ORDER BY calls DESC
|
|
372
|
+
LIMIT 100
|
|
373
|
+
`).all(since);
|
|
374
|
+
// Per-file 7-day call trend — one sparkline (oldest→newest) per file cell.
|
|
375
|
+
// Always a fixed 7-day window, independent of `range`, so the trend reads
|
|
376
|
+
// consistently regardless of the heatmap's selected window.
|
|
377
|
+
const dayMs = 24 * 60 * 60 * 1000;
|
|
378
|
+
const trend7Start = Math.floor(Date.now() / dayMs) * dayMs - 6 * dayMs;
|
|
379
|
+
const trendRows = db.prepare(`
|
|
380
|
+
SELECT input_summary as path, CAST((ts - ?) / ? AS INTEGER) as bucket, COUNT(*) as calls
|
|
381
|
+
FROM claude_tool_calls
|
|
382
|
+
WHERE ts >= ? AND tool_name IN ('Read','Edit','Write','NotebookEdit') AND input_summary != ''
|
|
383
|
+
GROUP BY input_summary, bucket
|
|
384
|
+
`).all(trend7Start, dayMs, trend7Start);
|
|
385
|
+
const trendByPath = new Map();
|
|
386
|
+
for (const r of trendRows) {
|
|
387
|
+
if (r.bucket < 0 || r.bucket > 6)
|
|
388
|
+
continue;
|
|
389
|
+
const arr = trendByPath.get(r.path) ?? [0, 0, 0, 0, 0, 0, 0];
|
|
390
|
+
arr[r.bucket] = r.calls;
|
|
391
|
+
trendByPath.set(r.path, arr);
|
|
392
|
+
}
|
|
393
|
+
for (const f of files) {
|
|
394
|
+
f.trend = trendByPath.get(f.path) ?? [0, 0, 0, 0, 0, 0, 0];
|
|
395
|
+
}
|
|
396
|
+
// Tools heatmap.
|
|
397
|
+
const tools = db.prepare(`
|
|
398
|
+
SELECT tool_name as name, COUNT(*) as calls, SUM(result_length) as resultBytes
|
|
399
|
+
FROM claude_tool_calls
|
|
400
|
+
WHERE ts >= ?
|
|
401
|
+
GROUP BY tool_name
|
|
402
|
+
ORDER BY calls DESC
|
|
403
|
+
`).all(since);
|
|
404
|
+
// Subagent attribution (is_sidechain at the prompt level — main vs. sidechain rollup).
|
|
405
|
+
const subagents = db.prepare(`
|
|
406
|
+
SELECT
|
|
407
|
+
CASE WHEN p.is_sidechain = 1 THEN 'subagent' ELSE 'main' END as type,
|
|
408
|
+
COUNT(*) as prompts,
|
|
409
|
+
SUM(p.input_tokens + p.output_tokens + p.cache_creation_tokens + p.cache_read_tokens) as tokens,
|
|
410
|
+
SUM(p.cost_usd) as cost
|
|
411
|
+
FROM claude_prompts p
|
|
412
|
+
WHERE p.ts >= ?
|
|
413
|
+
GROUP BY type
|
|
414
|
+
`).all(since);
|
|
415
|
+
// Subagent breakdown by name — Task tool calls grouped by subagent_type.
|
|
416
|
+
// input_summary is the JSON-serialized tool input; json_extract pulls
|
|
417
|
+
// out subagent_type (defaults to 'general-purpose' when unset).
|
|
418
|
+
const subagentByName = db.prepare(`
|
|
419
|
+
SELECT
|
|
420
|
+
COALESCE(NULLIF(json_extract(input_summary, '$.subagent_type'), ''), 'general-purpose') as name,
|
|
421
|
+
COUNT(*) as calls,
|
|
422
|
+
MIN(ts) as firstSeen,
|
|
423
|
+
MAX(ts) as lastSeen
|
|
424
|
+
FROM claude_tool_calls
|
|
425
|
+
WHERE ts >= ? AND tool_name = 'Task'
|
|
426
|
+
GROUP BY name
|
|
427
|
+
ORDER BY calls DESC
|
|
428
|
+
`).all(since);
|
|
429
|
+
return { files, tools, subagents, subagentByName };
|
|
430
|
+
});
|
|
431
|
+
/**
|
|
432
|
+
* Drill-down: which sessions touched a given file (via Read/Edit/Write).
|
|
433
|
+
* Used by the heatmap page when the user clicks a file cell.
|
|
434
|
+
*/
|
|
435
|
+
app.get('/api/claude/heatmap/file', async (req, reply) => {
|
|
436
|
+
const path = req.query.path;
|
|
437
|
+
if (!path)
|
|
438
|
+
return reply.code(400).send({ error: 'path required' });
|
|
439
|
+
const cg = requirePrimary(reply);
|
|
440
|
+
if (!cg)
|
|
441
|
+
return;
|
|
442
|
+
const db = getDb(cg);
|
|
443
|
+
const since = rangeStart(rangeKey(req.query.range));
|
|
444
|
+
const sessions = db.prepare(`
|
|
445
|
+
SELECT t.session_id, s.last_model, s.project_path,
|
|
446
|
+
COUNT(*) as calls, COALESCE(SUM(t.result_length), 0) as bytes,
|
|
447
|
+
MAX(t.ts) as lastTs, MIN(t.ts) as firstTs
|
|
448
|
+
FROM claude_tool_calls t
|
|
449
|
+
LEFT JOIN claude_sessions s ON s.id = t.session_id
|
|
450
|
+
WHERE t.input_summary = ? AND t.ts >= ? AND t.tool_name IN ('Read','Edit','Write','NotebookEdit')
|
|
451
|
+
GROUP BY t.session_id
|
|
452
|
+
ORDER BY calls DESC
|
|
453
|
+
LIMIT 50
|
|
454
|
+
`).all(path, since);
|
|
455
|
+
const byTool = db.prepare(`
|
|
456
|
+
SELECT tool_name as name, COUNT(*) as calls, COALESCE(SUM(result_length), 0) as bytes
|
|
457
|
+
FROM claude_tool_calls
|
|
458
|
+
WHERE input_summary = ? AND ts >= ? AND tool_name IN ('Read','Edit','Write','NotebookEdit')
|
|
459
|
+
GROUP BY tool_name
|
|
460
|
+
ORDER BY calls DESC
|
|
461
|
+
`).all(path, since);
|
|
462
|
+
return { path, sessions, byTool };
|
|
463
|
+
});
|
|
464
|
+
/**
|
|
465
|
+
* Drill-down: the top distinct inputs for a given tool (file paths for
|
|
466
|
+
* Read/Edit/Write, patterns for Grep/Glob, commands for Bash).
|
|
467
|
+
*/
|
|
468
|
+
app.get('/api/claude/heatmap/tool', async (req, reply) => {
|
|
469
|
+
const name = req.query.name;
|
|
470
|
+
if (!name)
|
|
471
|
+
return reply.code(400).send({ error: 'name required' });
|
|
472
|
+
const cg = requirePrimary(reply);
|
|
473
|
+
if (!cg)
|
|
474
|
+
return;
|
|
475
|
+
const db = getDb(cg);
|
|
476
|
+
const since = rangeStart(rangeKey(req.query.range));
|
|
477
|
+
const totals = db.prepare(`
|
|
478
|
+
SELECT COUNT(*) as calls, COALESCE(SUM(result_length), 0) as bytes,
|
|
479
|
+
COUNT(DISTINCT session_id) as sessions
|
|
480
|
+
FROM claude_tool_calls
|
|
481
|
+
WHERE tool_name = ? AND ts >= ?
|
|
482
|
+
`).get(name, since);
|
|
483
|
+
const inputs = db.prepare(`
|
|
484
|
+
SELECT
|
|
485
|
+
CASE WHEN length(input_summary) > 120 THEN substr(input_summary, 1, 120) || '…'
|
|
486
|
+
ELSE input_summary END as input,
|
|
487
|
+
COUNT(*) as calls,
|
|
488
|
+
COALESCE(SUM(result_length), 0) as bytes,
|
|
489
|
+
MAX(ts) as lastTs
|
|
490
|
+
FROM claude_tool_calls
|
|
491
|
+
WHERE tool_name = ? AND ts >= ? AND input_summary != ''
|
|
492
|
+
GROUP BY input_summary
|
|
493
|
+
ORDER BY calls DESC
|
|
494
|
+
LIMIT 30
|
|
495
|
+
`).all(name, since);
|
|
496
|
+
const recentSessions = db.prepare(`
|
|
497
|
+
SELECT t.session_id, s.last_model, s.project_path, COUNT(*) as calls, MAX(t.ts) as lastTs
|
|
498
|
+
FROM claude_tool_calls t
|
|
499
|
+
LEFT JOIN claude_sessions s ON s.id = t.session_id
|
|
500
|
+
WHERE t.tool_name = ? AND t.ts >= ?
|
|
501
|
+
GROUP BY t.session_id
|
|
502
|
+
ORDER BY lastTs DESC
|
|
503
|
+
LIMIT 20
|
|
504
|
+
`).all(name, since);
|
|
505
|
+
return { tool: name, totals, inputs, recentSessions };
|
|
506
|
+
});
|
|
507
|
+
/**
|
|
508
|
+
* Drill-down: invocations of a specific subagent (by subagent_type name).
|
|
509
|
+
*/
|
|
510
|
+
app.get('/api/claude/heatmap/subagent', async (req, reply) => {
|
|
511
|
+
const type = req.query.type;
|
|
512
|
+
if (!type)
|
|
513
|
+
return reply.code(400).send({ error: 'type required' });
|
|
514
|
+
const cg = requirePrimary(reply);
|
|
515
|
+
if (!cg)
|
|
516
|
+
return;
|
|
517
|
+
const db = getDb(cg);
|
|
518
|
+
const since = rangeStart(rangeKey(req.query.range));
|
|
519
|
+
const totals = db.prepare(`
|
|
520
|
+
SELECT COUNT(*) as calls, COUNT(DISTINCT session_id) as sessions
|
|
521
|
+
FROM claude_tool_calls
|
|
522
|
+
WHERE tool_name = 'Task' AND ts >= ?
|
|
523
|
+
AND COALESCE(NULLIF(json_extract(input_summary, '$.subagent_type'), ''), 'general-purpose') = ?
|
|
524
|
+
`).get(since, type);
|
|
525
|
+
const invocations = db.prepare(`
|
|
526
|
+
SELECT
|
|
527
|
+
t.session_id,
|
|
528
|
+
t.ts,
|
|
529
|
+
COALESCE(json_extract(t.input_summary, '$.description'), '') as description,
|
|
530
|
+
COALESCE(json_extract(t.input_summary, '$.prompt'), '') as prompt,
|
|
531
|
+
s.last_model
|
|
532
|
+
FROM claude_tool_calls t
|
|
533
|
+
LEFT JOIN claude_sessions s ON s.id = t.session_id
|
|
534
|
+
WHERE t.tool_name = 'Task' AND t.ts >= ?
|
|
535
|
+
AND COALESCE(NULLIF(json_extract(t.input_summary, '$.subagent_type'), ''), 'general-purpose') = ?
|
|
536
|
+
ORDER BY t.ts DESC
|
|
537
|
+
LIMIT 50
|
|
538
|
+
`).all(since, type);
|
|
539
|
+
return { subagent: type, totals, invocations };
|
|
540
|
+
});
|
|
541
|
+
app.get('/api/claude/cache', async (req, reply) => {
|
|
542
|
+
const cg = requirePrimary(reply);
|
|
543
|
+
if (!cg)
|
|
544
|
+
return;
|
|
545
|
+
const db = getDb(cg);
|
|
546
|
+
const since = rangeStart(rangeKey(req.query.range));
|
|
547
|
+
// Aggregate cache totals from claude_sessions for sessions in window.
|
|
548
|
+
const agg = db.prepare(`
|
|
549
|
+
SELECT
|
|
550
|
+
COALESCE(SUM(total_input_tokens), 0) as inp,
|
|
551
|
+
COALESCE(SUM(total_output_tokens), 0) as out,
|
|
552
|
+
COALESCE(SUM(total_cache_creation_tokens), 0) as cw,
|
|
553
|
+
COALESCE(SUM(total_cache_read_tokens), 0) as cr,
|
|
554
|
+
COALESCE(SUM(total_cost_usd), 0) as cost
|
|
555
|
+
FROM claude_sessions
|
|
556
|
+
WHERE started_at >= ?
|
|
557
|
+
`).get(since);
|
|
558
|
+
const total = (agg.inp ?? 0) + (agg.cw ?? 0) + (agg.cr ?? 0);
|
|
559
|
+
const readRate = total > 0 ? agg.cr / total : 0;
|
|
560
|
+
// Dollars saved estimate: cache_read tokens billed at ~10% of input;
|
|
561
|
+
// savings vs charging them at input rate ≈ 0.9 × (cr / 1M) × inputRate.
|
|
562
|
+
// Use Opus 4-7 input ($15/M) as a generous upper bound — the UI shows
|
|
563
|
+
// this as an approximation.
|
|
564
|
+
const dollarsSaved = ((agg.cr ?? 0) / 1_000_000) * 15 * 0.9;
|
|
565
|
+
// Week-over-week reuse delta: current read-rate minus the prior equal-length
|
|
566
|
+
// window's read-rate. Fractional (e.g. 0.06 → "+6%" in the UI). Zero for
|
|
567
|
+
// 'all' (no prior period) or when there's no prior-window data to compare.
|
|
568
|
+
const prior = priorWindow(rangeKey(req.query.range));
|
|
569
|
+
let wowDelta = 0;
|
|
570
|
+
if (prior.end > prior.start) {
|
|
571
|
+
const pAgg = db.prepare(`
|
|
572
|
+
SELECT
|
|
573
|
+
COALESCE(SUM(total_input_tokens), 0) as inp,
|
|
574
|
+
COALESCE(SUM(total_cache_creation_tokens), 0) as cw,
|
|
575
|
+
COALESCE(SUM(total_cache_read_tokens), 0) as cr
|
|
576
|
+
FROM claude_sessions
|
|
577
|
+
WHERE started_at >= ? AND started_at < ?
|
|
578
|
+
`).get(prior.start, prior.end);
|
|
579
|
+
const pTotal = (pAgg.inp ?? 0) + (pAgg.cw ?? 0) + (pAgg.cr ?? 0);
|
|
580
|
+
if (pTotal > 0)
|
|
581
|
+
wowDelta = readRate - pAgg.cr / pTotal;
|
|
582
|
+
}
|
|
583
|
+
return {
|
|
584
|
+
readRate,
|
|
585
|
+
creationTokens: agg.cw ?? 0,
|
|
586
|
+
readTokens: agg.cr ?? 0,
|
|
587
|
+
inputTokens: agg.inp ?? 0,
|
|
588
|
+
outputTokens: agg.out ?? 0,
|
|
589
|
+
totalCost: agg.cost ?? 0,
|
|
590
|
+
dollarsSaved,
|
|
591
|
+
wowDelta,
|
|
592
|
+
};
|
|
593
|
+
});
|
|
594
|
+
app.get('/api/claude/costs', async (req, reply) => {
|
|
595
|
+
const cg = requirePrimary(reply);
|
|
596
|
+
if (!cg)
|
|
597
|
+
return;
|
|
598
|
+
const db = getDb(cg);
|
|
599
|
+
const since = rangeStart(rangeKey(req.query.range));
|
|
600
|
+
const total = db.prepare(`SELECT SUM(total_cost_usd) as t FROM claude_sessions WHERE started_at >= ?`).get(since);
|
|
601
|
+
// Week-over-week spend delta: fractional change in total cost vs the prior
|
|
602
|
+
// equal-length window (e.g. -0.08 → "-8%"). Zero for 'all' or no prior data.
|
|
603
|
+
const prior = priorWindow(rangeKey(req.query.range));
|
|
604
|
+
let wowDelta = 0;
|
|
605
|
+
if (prior.end > prior.start) {
|
|
606
|
+
const pTotal = db.prepare(`SELECT SUM(total_cost_usd) as t FROM claude_sessions WHERE started_at >= ? AND started_at < ?`).get(prior.start, prior.end);
|
|
607
|
+
const pt = pTotal.t ?? 0;
|
|
608
|
+
if (pt > 0)
|
|
609
|
+
wowDelta = ((total.t ?? 0) - pt) / pt;
|
|
610
|
+
}
|
|
611
|
+
// Top prompts by cost.
|
|
612
|
+
const topPrompts = db.prepare(`
|
|
613
|
+
SELECT id, session_id, substr(text, 1, 200) as text, model, cost_usd,
|
|
614
|
+
input_tokens, output_tokens, cache_creation_tokens, cache_read_tokens, ts
|
|
615
|
+
FROM claude_prompts
|
|
616
|
+
WHERE ts >= ? AND cost_usd > 0
|
|
617
|
+
ORDER BY cost_usd DESC
|
|
618
|
+
LIMIT 50
|
|
619
|
+
`).all(since);
|
|
620
|
+
// Daily timeseries: bucket by 24h windows.
|
|
621
|
+
const days = 30;
|
|
622
|
+
const dayMs = 24 * 60 * 60 * 1000;
|
|
623
|
+
const dayBoundary = Math.floor(Date.now() / dayMs) * dayMs - (days - 1) * dayMs;
|
|
624
|
+
const series = db.prepare(`
|
|
625
|
+
SELECT
|
|
626
|
+
CAST((ts - ?) / ? AS INTEGER) as bucket,
|
|
627
|
+
SUM(cost_usd) as cost,
|
|
628
|
+
COUNT(*) as prompts
|
|
629
|
+
FROM claude_prompts
|
|
630
|
+
WHERE ts >= ?
|
|
631
|
+
GROUP BY bucket
|
|
632
|
+
ORDER BY bucket ASC
|
|
633
|
+
`).all(dayBoundary, dayMs, dayBoundary);
|
|
634
|
+
// Densify with zeros for missing days.
|
|
635
|
+
const dense = [];
|
|
636
|
+
for (let i = 0; i < days; i++) {
|
|
637
|
+
const found = series.find((s) => s.bucket === i);
|
|
638
|
+
dense.push({ day: days - 1 - i, cost: found?.cost ?? 0, prompts: found?.prompts ?? 0 });
|
|
639
|
+
}
|
|
640
|
+
// By model.
|
|
641
|
+
const byModel = db.prepare(`
|
|
642
|
+
SELECT model, COUNT(*) as prompts, SUM(cost_usd) as cost
|
|
643
|
+
FROM claude_prompts
|
|
644
|
+
WHERE ts >= ? AND model IS NOT NULL
|
|
645
|
+
GROUP BY model
|
|
646
|
+
ORDER BY cost DESC
|
|
647
|
+
`).all(since);
|
|
648
|
+
return { total: total.t ?? 0, topPrompts, series: dense, byModel, wowDelta };
|
|
649
|
+
});
|
|
650
|
+
/**
|
|
651
|
+
* GET /api/claude/stats?range= — the four dashboard stat tiles, each with a
|
|
652
|
+
* value, a fractional week-over-week delta (vs the prior equal-length window)
|
|
653
|
+
* and a 7-day sparkline series (oldest→newest). Drift is a live graph metric
|
|
654
|
+
* with no history, so it returns value-only.
|
|
655
|
+
*/
|
|
656
|
+
app.get('/api/claude/stats', async (req, reply) => {
|
|
657
|
+
const cg = requirePrimary(reply);
|
|
658
|
+
if (!cg)
|
|
659
|
+
return;
|
|
660
|
+
const db = getDb(cg);
|
|
661
|
+
const rkey = rangeKey(req.query.range);
|
|
662
|
+
const since = rangeStart(rkey);
|
|
663
|
+
const prior = priorWindow(rkey);
|
|
664
|
+
const dayMs = 24 * 60 * 60 * 1000;
|
|
665
|
+
const d7Start = Math.floor(Date.now() / dayMs) * dayMs - 6 * dayMs;
|
|
666
|
+
const dense7 = (rows) => {
|
|
667
|
+
const a = [0, 0, 0, 0, 0, 0, 0];
|
|
668
|
+
for (const r of rows)
|
|
669
|
+
if (r.bucket >= 0 && r.bucket <= 6)
|
|
670
|
+
a[r.bucket] = r.v;
|
|
671
|
+
return a;
|
|
672
|
+
};
|
|
673
|
+
const countSince = (sql, ...p) => db.prepare(sql).get(...p).c ?? 0;
|
|
674
|
+
// --- Tool calls ---
|
|
675
|
+
const tcTotal = countSince(`SELECT COUNT(*) c FROM claude_tool_calls WHERE ts >= ?`, since);
|
|
676
|
+
const tcPrior = prior.end > prior.start
|
|
677
|
+
? countSince(`SELECT COUNT(*) c FROM claude_tool_calls WHERE ts >= ? AND ts < ?`, prior.start, prior.end)
|
|
678
|
+
: 0;
|
|
679
|
+
const tcSeries = db.prepare(`SELECT CAST((ts - ?) / ? AS INTEGER) as bucket, COUNT(*) as v FROM claude_tool_calls WHERE ts >= ? GROUP BY bucket`).all(d7Start, dayMs, d7Start);
|
|
680
|
+
const toolCalls = { value: tcTotal, delta: tcPrior > 0 ? (tcTotal - tcPrior) / tcPrior : 0, series: dense7(tcSeries) };
|
|
681
|
+
// --- Subagent spend share (by cost) ---
|
|
682
|
+
const subPctOf = (start, end) => {
|
|
683
|
+
const where = end == null ? `ts >= ?` : `ts >= ? AND ts < ?`;
|
|
684
|
+
const args = end == null ? [start] : [start, end];
|
|
685
|
+
const rows = db.prepare(`SELECT is_sidechain as side, COALESCE(SUM(cost_usd), 0) as cost FROM claude_prompts WHERE ${where} GROUP BY is_sidechain`).all(...args);
|
|
686
|
+
const sub = rows.find((r) => r.side === 1)?.cost ?? 0;
|
|
687
|
+
const tot = rows.reduce((a, r) => a + (r.cost ?? 0), 0);
|
|
688
|
+
return tot > 0 ? sub / tot : 0;
|
|
689
|
+
};
|
|
690
|
+
const subPct = subPctOf(since, null);
|
|
691
|
+
const subPctPrior = prior.end > prior.start ? subPctOf(prior.start, prior.end) : 0;
|
|
692
|
+
const saDaily = db.prepare(`
|
|
693
|
+
SELECT CAST((ts - ?) / ? AS INTEGER) as bucket,
|
|
694
|
+
SUM(CASE WHEN is_sidechain = 1 THEN cost_usd ELSE 0 END) as sub,
|
|
695
|
+
SUM(cost_usd) as tot
|
|
696
|
+
FROM claude_prompts WHERE ts >= ? GROUP BY bucket
|
|
697
|
+
`).all(d7Start, dayMs, d7Start);
|
|
698
|
+
const saSeries = [0, 0, 0, 0, 0, 0, 0];
|
|
699
|
+
for (const r of saDaily)
|
|
700
|
+
if (r.bucket >= 0 && r.bucket <= 6)
|
|
701
|
+
saSeries[r.bucket] = r.tot > 0 ? Math.round((r.sub / r.tot) * 100) : 0;
|
|
702
|
+
const subagentPct = { value: Math.round(subPct * 100), delta: subPct - subPctPrior, series: saSeries };
|
|
703
|
+
// --- Last session cost (delta vs previous session, spark of recent sessions) ---
|
|
704
|
+
const recent = db.prepare(`SELECT total_cost_usd as cost FROM claude_sessions ORDER BY started_at DESC LIMIT 10`).all();
|
|
705
|
+
const lastCost = recent[0]?.cost ?? 0;
|
|
706
|
+
const prevCost = recent[1]?.cost ?? 0;
|
|
707
|
+
const lastSessionCost = {
|
|
708
|
+
value: lastCost,
|
|
709
|
+
delta: prevCost > 0 ? (lastCost - prevCost) / prevCost : 0,
|
|
710
|
+
series: recent.map((r) => r.cost ?? 0).reverse(),
|
|
711
|
+
};
|
|
712
|
+
// --- Drift (live graph count, no time series) ---
|
|
713
|
+
const driftCount = cg.getSpecQueries().getLinksByState(['drifted', 'broken', 'orphaned']).length;
|
|
714
|
+
const drift = { value: driftCount, delta: 0, series: [] };
|
|
715
|
+
return { lastSessionCost, toolCalls, subagentPct, drift };
|
|
716
|
+
});
|
|
717
|
+
app.get('/api/claude/compare', async (_req, reply) => {
|
|
718
|
+
const cg = requirePrimary(reply);
|
|
719
|
+
if (!cg)
|
|
720
|
+
return;
|
|
721
|
+
const db = getDb(cg);
|
|
722
|
+
const rows = db.prepare(`
|
|
723
|
+
SELECT
|
|
724
|
+
p.path, p.name,
|
|
725
|
+
COUNT(s.id) as sessions,
|
|
726
|
+
COALESCE(SUM(s.total_cost_usd), 0) as cost,
|
|
727
|
+
COALESCE(AVG(s.total_cost_usd), 0) as avgCost,
|
|
728
|
+
COALESCE(SUM(s.prompt_count), 0) as prompts,
|
|
729
|
+
CASE WHEN SUM(s.total_input_tokens + s.total_cache_creation_tokens + s.total_cache_read_tokens) > 0
|
|
730
|
+
THEN CAST(SUM(s.total_cache_read_tokens) AS REAL) / SUM(s.total_input_tokens + s.total_cache_creation_tokens + s.total_cache_read_tokens)
|
|
731
|
+
ELSE 0 END as cacheHit
|
|
732
|
+
FROM claude_projects p
|
|
733
|
+
LEFT JOIN claude_sessions s ON s.project_path = p.path
|
|
734
|
+
GROUP BY p.path
|
|
735
|
+
ORDER BY cost DESC
|
|
736
|
+
`).all();
|
|
737
|
+
// Per-model cost split per project (drives the stacked bars).
|
|
738
|
+
const modelRows = db.prepare(`
|
|
739
|
+
SELECT s.project_path as path, p.model as model, COALESCE(SUM(p.cost_usd), 0) as cost
|
|
740
|
+
FROM claude_prompts p
|
|
741
|
+
JOIN claude_sessions s ON s.id = p.session_id
|
|
742
|
+
WHERE p.model IS NOT NULL
|
|
743
|
+
GROUP BY s.project_path, p.model
|
|
744
|
+
`).all();
|
|
745
|
+
const byModelByPath = new Map();
|
|
746
|
+
for (const r of modelRows) {
|
|
747
|
+
const arr = byModelByPath.get(r.path) ?? [];
|
|
748
|
+
arr.push({ model: r.model, cost: r.cost });
|
|
749
|
+
byModelByPath.set(r.path, arr);
|
|
750
|
+
}
|
|
751
|
+
// Top tools per project (top 4 by call count).
|
|
752
|
+
const toolRows = db.prepare(`
|
|
753
|
+
SELECT s.project_path as path, t.tool_name as name, COUNT(*) as calls
|
|
754
|
+
FROM claude_tool_calls t
|
|
755
|
+
JOIN claude_sessions s ON s.id = t.session_id
|
|
756
|
+
GROUP BY s.project_path, t.tool_name
|
|
757
|
+
`).all();
|
|
758
|
+
const toolsByPath = new Map();
|
|
759
|
+
for (const r of toolRows) {
|
|
760
|
+
const arr = toolsByPath.get(r.path) ?? [];
|
|
761
|
+
arr.push({ name: r.name, calls: r.calls });
|
|
762
|
+
toolsByPath.set(r.path, arr);
|
|
763
|
+
}
|
|
764
|
+
const projects = rows.map((p) => ({
|
|
765
|
+
...p,
|
|
766
|
+
byModel: (byModelByPath.get(p.path) ?? []).sort((a, b) => b.cost - a.cost),
|
|
767
|
+
topTools: (toolsByPath.get(p.path) ?? [])
|
|
768
|
+
.sort((a, b) => b.calls - a.calls)
|
|
769
|
+
.slice(0, 4)
|
|
770
|
+
.map((t) => t.name),
|
|
771
|
+
}));
|
|
772
|
+
return { projects };
|
|
773
|
+
});
|
|
774
|
+
/**
|
|
775
|
+
* Rule-based tips engine. Each rule is a SQL query that finds a wasteful
|
|
776
|
+
* pattern in the user's recent transcripts; the result is shaped into a
|
|
777
|
+
* tip card matching the design system's voice.
|
|
778
|
+
*/
|
|
779
|
+
app.get('/api/claude/tips', async (_req, reply) => {
|
|
780
|
+
const cg = requirePrimary(reply);
|
|
781
|
+
if (!cg)
|
|
782
|
+
return;
|
|
783
|
+
const db = getDb(cg);
|
|
784
|
+
const tips = [];
|
|
785
|
+
// Rule 1: "you read X N times" — same file path Read more than 10 times in
|
|
786
|
+
// a single session.
|
|
787
|
+
const wastefulReads = db.prepare(`
|
|
788
|
+
SELECT session_id, input_summary as file, COUNT(*) as n
|
|
789
|
+
FROM claude_tool_calls
|
|
790
|
+
WHERE tool_name = 'Read' AND input_summary != ''
|
|
791
|
+
GROUP BY session_id, input_summary
|
|
792
|
+
HAVING n >= 10
|
|
793
|
+
ORDER BY n DESC
|
|
794
|
+
LIMIT 5
|
|
795
|
+
`).all();
|
|
796
|
+
for (const r of wastefulReads) {
|
|
797
|
+
tips.push({
|
|
798
|
+
id: 'wasteful_reads:' + r.session_id + ':' + r.file,
|
|
799
|
+
severity: 'error',
|
|
800
|
+
icon: 'wrench',
|
|
801
|
+
title: `You read ${r.file.split('/').pop()} ${r.n}× in one session — specship_explore covers it`,
|
|
802
|
+
why: 'Re-reading the same file burns input tokens every turn. A single structural query returns callers, callees, and linked specs at once.',
|
|
803
|
+
evidence: { session: r.session_id, detail: `Read(${r.file}) × ${r.n}` },
|
|
804
|
+
fix: `specship_explore --symbol ${r.file.replace(/\.\w+$/, '').split('/').pop()}`,
|
|
805
|
+
saving: '≈$0.10/read avoided',
|
|
806
|
+
});
|
|
807
|
+
}
|
|
808
|
+
// Rule 2: "tool returned X tokens" — any single tool call with result_length > 50000.
|
|
809
|
+
const heavyResults = db.prepare(`
|
|
810
|
+
SELECT id, session_id, tool_name, input_summary, result_length
|
|
811
|
+
FROM claude_tool_calls
|
|
812
|
+
WHERE result_length > 50000
|
|
813
|
+
ORDER BY result_length DESC
|
|
814
|
+
LIMIT 5
|
|
815
|
+
`).all();
|
|
816
|
+
for (const r of heavyResults) {
|
|
817
|
+
tips.push({
|
|
818
|
+
id: 'heavy_result:' + r.id,
|
|
819
|
+
severity: 'error',
|
|
820
|
+
icon: 'flame',
|
|
821
|
+
title: `${r.tool_name} returned ${Math.round(r.result_length / 1000)}k tokens — try a structural query`,
|
|
822
|
+
why: 'Tools that dump raw content into context are the dominant cost driver. A structural query returns just what the agent needs.',
|
|
823
|
+
evidence: { session: r.session_id, detail: `${r.tool_name}(${r.input_summary.slice(0, 100)}) → ${r.result_length} tokens` },
|
|
824
|
+
fix: r.tool_name === 'Bash' ? 'specship_search instead of Bash(grep)' : 'specship_explore on the symbol',
|
|
825
|
+
saving: `~$${((r.result_length / 1_000_000) * 15).toFixed(2)} on this call`,
|
|
826
|
+
});
|
|
827
|
+
}
|
|
828
|
+
// Rule 3: "cache miss rate" — sessions with > 10 prompts and cache_read_rate < 0.3.
|
|
829
|
+
const lowCache = db.prepare(`
|
|
830
|
+
SELECT s.id, s.total_cache_read_tokens as cr, s.total_input_tokens as ti, s.total_cache_creation_tokens as cw, s.prompt_count, s.last_model
|
|
831
|
+
FROM claude_sessions s
|
|
832
|
+
WHERE s.prompt_count >= 10
|
|
833
|
+
AND (s.total_input_tokens + s.total_cache_creation_tokens + s.total_cache_read_tokens) > 0
|
|
834
|
+
ORDER BY (CAST(s.total_cache_read_tokens AS REAL) / (s.total_input_tokens + s.total_cache_creation_tokens + s.total_cache_read_tokens)) ASC
|
|
835
|
+
LIMIT 3
|
|
836
|
+
`).all();
|
|
837
|
+
for (const r of lowCache) {
|
|
838
|
+
const total = r.ti + r.cw + r.cr;
|
|
839
|
+
const rate = total > 0 ? r.cr / total : 0;
|
|
840
|
+
if (rate >= 0.3)
|
|
841
|
+
continue;
|
|
842
|
+
tips.push({
|
|
843
|
+
id: 'low_cache:' + r.id,
|
|
844
|
+
severity: 'warn',
|
|
845
|
+
icon: 'database',
|
|
846
|
+
title: `Cache read rate is ${Math.round(rate * 100)}% on a ${r.prompt_count}-prompt session`,
|
|
847
|
+
why: 'When the prompt prefix changes every turn, the 1h cache gets invalidated. Pinning a stable system-prompt prefix lets the cache absorb most of your input.',
|
|
848
|
+
evidence: { session: r.id, detail: `cache_read=${(r.cr / 1_000_000).toFixed(2)}M / total=${(total / 1_000_000).toFixed(2)}M` },
|
|
849
|
+
fix: 'Pin a stable system-prompt prefix in .claude/settings.json',
|
|
850
|
+
saving: '~$X.XX / session (model-dependent)',
|
|
851
|
+
});
|
|
852
|
+
}
|
|
853
|
+
// Sort: errors before warns before info, then within bucket by saving heuristic.
|
|
854
|
+
const order = { error: 0, warn: 1, info: 2 };
|
|
855
|
+
tips.sort((a, b) => (order[a.severity] ?? 9) - (order[b.severity] ?? 9));
|
|
856
|
+
return { tips };
|
|
857
|
+
});
|
|
858
|
+
/**
|
|
859
|
+
* Force a one-shot ingest pass. Useful for "Refresh" button in the UI.
|
|
860
|
+
*/
|
|
861
|
+
app.post('/api/claude/ingest', async () => {
|
|
862
|
+
const watcher = app.watcher;
|
|
863
|
+
if (!watcher)
|
|
864
|
+
return { ok: false, error: 'watcher not running' };
|
|
865
|
+
const stats = watcher.ingestNow();
|
|
866
|
+
return { ok: true, stats };
|
|
867
|
+
});
|
|
868
|
+
}
|