synabun 1.3.0 → 2.0.0-beta
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/CHANGELOG.md +197 -662
- package/LICENSE-COMMERCIAL.md +33 -0
- package/NOTICE +17 -0
- package/README.md +175 -853
- package/THIRD-PARTY-LICENSES.md +131 -0
- package/benchmarks/browser-live-results.json +79 -0
- package/benchmarks/browser-v2-results.json +258 -0
- package/benchmarks/fixtures/browser/README.md +57 -0
- package/benchmarks/fixtures/browser/page-state.json +132 -0
- package/benchmarks/fixtures/browser/social.json +92 -0
- package/benchmarks/fixtures/browser/target.json +319 -0
- package/benchmarks/fixtures/desktop/README.md +58 -0
- package/benchmarks/fixtures/desktop/abstention.json +59 -0
- package/benchmarks/fixtures/desktop/adversarial.json +22 -0
- package/benchmarks/fixtures/desktop/forbidden.json +93 -0
- package/benchmarks/fixtures/desktop/generate.mjs +1023 -0
- package/benchmarks/fixtures/desktop/target.json +136 -0
- package/bin/synabun.js +30 -0
- package/docs/browser-v2.md +109 -0
- package/docs/hook-integrity.md +89 -0
- package/docs/judgments.md +182 -0
- package/docs/style-guide.md +246 -0
- package/docs/whatsapp.md +325 -0
- package/hooks/claude-code/post-plan.mjs +613 -391
- package/hooks/claude-code/post-remember.mjs +246 -122
- package/hooks/claude-code/pre-compact.mjs +171 -110
- package/hooks/claude-code/pre-task.mjs +133 -0
- package/hooks/claude-code/pre-websearch.mjs +15 -4
- package/hooks/claude-code/prompt-origin.mjs +94 -0
- package/hooks/claude-code/prompt-submit.mjs +1244 -861
- package/hooks/claude-code/session-start.mjs +114 -156
- package/hooks/claude-code/shared.mjs +1228 -368
- package/hooks/claude-code/state.mjs +167 -0
- package/hooks/claude-code/stop.mjs +515 -140
- package/hooks/claude-code/subagent-start.mjs +154 -0
- package/hooks/claude-code/subagent-stop.mjs +102 -0
- package/hooks/claude-code/task-gate.mjs +150 -0
- package/lib/claude-hooks.js +84 -0
- package/lib/claude-usage-audit.js +141 -0
- package/lib/client-config-repair.js +144 -0
- package/lib/data-home-migration.js +498 -0
- package/lib/first-launch-protection.js +240 -0
- package/lib/paths.d.ts +21 -0
- package/lib/paths.js +103 -0
- package/mcp-server/dist/config.d.ts +22 -0
- package/mcp-server/dist/config.d.ts.map +1 -0
- package/mcp-server/dist/config.js +109 -0
- package/mcp-server/dist/config.js.map +1 -0
- package/mcp-server/dist/http.d.ts +30 -0
- package/mcp-server/dist/http.d.ts.map +1 -0
- package/mcp-server/dist/http.js +276 -0
- package/mcp-server/dist/http.js.map +1 -0
- package/mcp-server/dist/index.d.ts +48 -0
- package/mcp-server/dist/index.d.ts.map +1 -0
- package/mcp-server/dist/index.js +538 -0
- package/mcp-server/dist/index.js.map +1 -0
- package/mcp-server/dist/preload-http.d.ts +3 -0
- package/mcp-server/dist/preload-http.d.ts.map +1 -0
- package/mcp-server/dist/preload-http.js +28 -0
- package/mcp-server/dist/preload-http.js.map +1 -0
- package/mcp-server/dist/preload.d.ts +3 -0
- package/mcp-server/dist/preload.d.ts.map +1 -0
- package/mcp-server/dist/preload.js +32 -0
- package/mcp-server/dist/preload.js.map +1 -0
- package/mcp-server/dist/scripts/migrate-qdrant-to-sqlite.d.ts +19 -0
- package/mcp-server/dist/scripts/migrate-qdrant-to-sqlite.d.ts.map +1 -0
- package/mcp-server/dist/scripts/migrate-qdrant-to-sqlite.js +403 -0
- package/mcp-server/dist/scripts/migrate-qdrant-to-sqlite.js.map +1 -0
- package/mcp-server/dist/scripts/setup-embeddings.d.ts +3 -0
- package/mcp-server/dist/scripts/setup-embeddings.d.ts.map +1 -0
- package/mcp-server/dist/scripts/setup-embeddings.js +15 -0
- package/mcp-server/dist/scripts/setup-embeddings.js.map +1 -0
- package/mcp-server/dist/services/assistant-judgments.d.ts +79 -0
- package/mcp-server/dist/services/assistant-judgments.d.ts.map +1 -0
- package/mcp-server/dist/services/assistant-judgments.js +128 -0
- package/mcp-server/dist/services/assistant-judgments.js.map +1 -0
- package/mcp-server/dist/services/browser-assist-gate.d.ts +314 -0
- package/mcp-server/dist/services/browser-assist-gate.d.ts.map +1 -0
- package/mcp-server/dist/services/browser-assist-gate.js +344 -0
- package/mcp-server/dist/services/browser-assist-gate.js.map +1 -0
- package/mcp-server/dist/services/browser-assist.d.ts +104 -0
- package/mcp-server/dist/services/browser-assist.d.ts.map +1 -0
- package/mcp-server/dist/services/browser-assist.js +282 -0
- package/mcp-server/dist/services/browser-assist.js.map +1 -0
- package/mcp-server/dist/services/browser-bench.d.ts +772 -0
- package/mcp-server/dist/services/browser-bench.d.ts.map +1 -0
- package/mcp-server/dist/services/browser-bench.js +424 -0
- package/mcp-server/dist/services/browser-bench.js.map +1 -0
- package/mcp-server/dist/services/browser-judgments.d.ts +196 -0
- package/mcp-server/dist/services/browser-judgments.d.ts.map +1 -0
- package/mcp-server/dist/services/browser-judgments.js +363 -0
- package/mcp-server/dist/services/browser-judgments.js.map +1 -0
- package/mcp-server/dist/services/browser-risk.d.ts +166 -0
- package/mcp-server/dist/services/browser-risk.d.ts.map +1 -0
- package/mcp-server/dist/services/browser-risk.js +458 -0
- package/mcp-server/dist/services/browser-risk.js.map +1 -0
- package/mcp-server/dist/services/categories.d.ts +61 -0
- package/mcp-server/dist/services/categories.d.ts.map +1 -0
- package/mcp-server/dist/services/categories.js +377 -0
- package/mcp-server/dist/services/categories.js.map +1 -0
- package/mcp-server/dist/services/codex-config-heal.d.ts +49 -0
- package/mcp-server/dist/services/codex-config-heal.d.ts.map +1 -0
- package/mcp-server/dist/services/codex-config-heal.js +346 -0
- package/mcp-server/dist/services/codex-config-heal.js.map +1 -0
- package/mcp-server/dist/services/desktop-assist-gate.d.ts +198 -0
- package/mcp-server/dist/services/desktop-assist-gate.d.ts.map +1 -0
- package/mcp-server/dist/services/desktop-assist-gate.js +202 -0
- package/mcp-server/dist/services/desktop-assist-gate.js.map +1 -0
- package/mcp-server/dist/services/desktop-assist.d.ts +104 -0
- package/mcp-server/dist/services/desktop-assist.d.ts.map +1 -0
- package/mcp-server/dist/services/desktop-assist.js +266 -0
- package/mcp-server/dist/services/desktop-assist.js.map +1 -0
- package/mcp-server/dist/services/desktop-bench.d.ts +553 -0
- package/mcp-server/dist/services/desktop-bench.d.ts.map +1 -0
- package/mcp-server/dist/services/desktop-bench.js +311 -0
- package/mcp-server/dist/services/desktop-bench.js.map +1 -0
- package/mcp-server/dist/services/desktop-client.d.ts +23 -0
- package/mcp-server/dist/services/desktop-client.d.ts.map +1 -0
- package/mcp-server/dist/services/desktop-client.js +79 -0
- package/mcp-server/dist/services/desktop-client.js.map +1 -0
- package/mcp-server/dist/services/desktop-judgments.d.ts +125 -0
- package/mcp-server/dist/services/desktop-judgments.d.ts.map +1 -0
- package/mcp-server/dist/services/desktop-judgments.js +177 -0
- package/mcp-server/dist/services/desktop-judgments.js.map +1 -0
- package/mcp-server/dist/services/desktop-risk.d.ts +283 -0
- package/mcp-server/dist/services/desktop-risk.d.ts.map +1 -0
- package/mcp-server/dist/services/desktop-risk.js +605 -0
- package/mcp-server/dist/services/desktop-risk.js.map +1 -0
- package/mcp-server/dist/services/discord.d.ts +112 -0
- package/mcp-server/dist/services/discord.d.ts.map +1 -0
- package/mcp-server/dist/services/discord.js +477 -0
- package/mcp-server/dist/services/discord.js.map +1 -0
- package/mcp-server/dist/services/edit-stale.d.ts +81 -0
- package/mcp-server/dist/services/edit-stale.d.ts.map +1 -0
- package/mcp-server/dist/services/edit-stale.js +358 -0
- package/mcp-server/dist/services/edit-stale.js.map +1 -0
- package/mcp-server/dist/services/embedding-service.d.ts +16 -0
- package/mcp-server/dist/services/embedding-service.d.ts.map +1 -0
- package/mcp-server/dist/services/embedding-service.js +126 -0
- package/mcp-server/dist/services/embedding-service.js.map +1 -0
- package/mcp-server/dist/services/embedding-worker.d.ts +2 -0
- package/mcp-server/dist/services/embedding-worker.d.ts.map +1 -0
- package/mcp-server/dist/services/embedding-worker.js +52 -0
- package/mcp-server/dist/services/embedding-worker.js.map +1 -0
- package/mcp-server/dist/services/fb-regions.d.ts +71 -0
- package/mcp-server/dist/services/fb-regions.d.ts.map +1 -0
- package/mcp-server/dist/services/fb-regions.js +211 -0
- package/mcp-server/dist/services/fb-regions.js.map +1 -0
- package/mcp-server/dist/services/file-checksums.d.ts +22 -0
- package/mcp-server/dist/services/file-checksums.d.ts.map +1 -0
- package/mcp-server/dist/services/file-checksums.js +55 -0
- package/mcp-server/dist/services/file-checksums.js.map +1 -0
- package/mcp-server/dist/services/file-excerpt.d.ts +24 -0
- package/mcp-server/dist/services/file-excerpt.d.ts.map +1 -0
- package/mcp-server/dist/services/file-excerpt.js +65 -0
- package/mcp-server/dist/services/file-excerpt.js.map +1 -0
- package/mcp-server/dist/services/identity.d.ts +124 -0
- package/mcp-server/dist/services/identity.d.ts.map +1 -0
- package/mcp-server/dist/services/identity.js +241 -0
- package/mcp-server/dist/services/identity.js.map +1 -0
- package/mcp-server/dist/services/local-embeddings.d.ts +2 -0
- package/mcp-server/dist/services/local-embeddings.d.ts.map +1 -0
- package/mcp-server/dist/services/local-embeddings.js +3 -0
- package/mcp-server/dist/services/local-embeddings.js.map +1 -0
- package/mcp-server/dist/services/memory-client.d.ts +4 -0
- package/mcp-server/dist/services/memory-client.d.ts.map +1 -0
- package/mcp-server/dist/services/memory-client.js +27 -0
- package/mcp-server/dist/services/memory-client.js.map +1 -0
- package/mcp-server/dist/services/memory-judge-backfill.d.ts +135 -0
- package/mcp-server/dist/services/memory-judge-backfill.d.ts.map +1 -0
- package/mcp-server/dist/services/memory-judge-backfill.js +510 -0
- package/mcp-server/dist/services/memory-judge-backfill.js.map +1 -0
- package/mcp-server/dist/services/memory-judgments.d.ts +362 -0
- package/mcp-server/dist/services/memory-judgments.d.ts.map +1 -0
- package/mcp-server/dist/services/memory-judgments.js +674 -0
- package/mcp-server/dist/services/memory-judgments.js.map +1 -0
- package/mcp-server/dist/services/memory-maintenance.d.ts +108 -0
- package/mcp-server/dist/services/memory-maintenance.d.ts.map +1 -0
- package/mcp-server/dist/services/memory-maintenance.js +403 -0
- package/mcp-server/dist/services/memory-maintenance.js.map +1 -0
- package/mcp-server/dist/services/memory-retrieval.d.ts +66 -0
- package/mcp-server/dist/services/memory-retrieval.d.ts.map +1 -0
- package/mcp-server/dist/services/memory-retrieval.js +412 -0
- package/mcp-server/dist/services/memory-retrieval.js.map +1 -0
- package/mcp-server/dist/services/memory-schema.d.ts +16 -0
- package/mcp-server/dist/services/memory-schema.d.ts.map +1 -0
- package/mcp-server/dist/services/memory-schema.js +282 -0
- package/mcp-server/dist/services/memory-schema.js.map +1 -0
- package/mcp-server/dist/services/memory-sessions.d.ts +24 -0
- package/mcp-server/dist/services/memory-sessions.d.ts.map +1 -0
- package/mcp-server/dist/services/memory-sessions.js +110 -0
- package/mcp-server/dist/services/memory-sessions.js.map +1 -0
- package/mcp-server/dist/services/memory-triage.d.ts +60 -0
- package/mcp-server/dist/services/memory-triage.d.ts.map +1 -0
- package/mcp-server/dist/services/memory-triage.js +189 -0
- package/mcp-server/dist/services/memory-triage.js.map +1 -0
- package/mcp-server/dist/services/memory-verification.d.ts +24 -0
- package/mcp-server/dist/services/memory-verification.d.ts.map +1 -0
- package/mcp-server/dist/services/memory-verification.js +92 -0
- package/mcp-server/dist/services/memory-verification.js.map +1 -0
- package/mcp-server/dist/services/memory-writer.d.ts +26 -0
- package/mcp-server/dist/services/memory-writer.d.ts.map +1 -0
- package/mcp-server/dist/services/memory-writer.js +126 -0
- package/mcp-server/dist/services/memory-writer.js.map +1 -0
- package/mcp-server/dist/services/neural-interface.d.ts +439 -0
- package/mcp-server/dist/services/neural-interface.d.ts.map +1 -0
- package/mcp-server/dist/services/neural-interface.js +1135 -0
- package/mcp-server/dist/services/neural-interface.js.map +1 -0
- package/mcp-server/dist/services/profiles.d.ts +120 -0
- package/mcp-server/dist/services/profiles.d.ts.map +1 -0
- package/mcp-server/dist/services/profiles.js +387 -0
- package/mcp-server/dist/services/profiles.js.map +1 -0
- package/mcp-server/dist/services/secret-gate.d.ts +29 -0
- package/mcp-server/dist/services/secret-gate.d.ts.map +1 -0
- package/mcp-server/dist/services/secret-gate.js +43 -0
- package/mcp-server/dist/services/secret-gate.js.map +1 -0
- package/mcp-server/dist/services/server-instructions.d.ts +10 -0
- package/mcp-server/dist/services/server-instructions.d.ts.map +1 -0
- package/mcp-server/dist/services/server-instructions.js +101 -0
- package/mcp-server/dist/services/server-instructions.js.map +1 -0
- package/mcp-server/dist/services/sqlite.d.ts +235 -0
- package/mcp-server/dist/services/sqlite.d.ts.map +1 -0
- package/mcp-server/dist/services/sqlite.js +1185 -0
- package/mcp-server/dist/services/sqlite.js.map +1 -0
- package/mcp-server/dist/services/typesafe-bench.d.ts +99 -0
- package/mcp-server/dist/services/typesafe-bench.d.ts.map +1 -0
- package/mcp-server/dist/services/typesafe-bench.js +218 -0
- package/mcp-server/dist/services/typesafe-bench.js.map +1 -0
- package/mcp-server/dist/services/typesafe-config.d.ts +342 -0
- package/mcp-server/dist/services/typesafe-config.d.ts.map +1 -0
- package/mcp-server/dist/services/typesafe-config.js +746 -0
- package/mcp-server/dist/services/typesafe-config.js.map +1 -0
- package/mcp-server/dist/services/typesafe.d.ts +213 -0
- package/mcp-server/dist/services/typesafe.d.ts.map +1 -0
- package/mcp-server/dist/services/typesafe.js +330 -0
- package/mcp-server/dist/services/typesafe.js.map +1 -0
- package/mcp-server/dist/services/vector-cache.d.ts +68 -0
- package/mcp-server/dist/services/vector-cache.d.ts.map +1 -0
- package/mcp-server/dist/services/vector-cache.js +271 -0
- package/mcp-server/dist/services/vector-cache.js.map +1 -0
- package/mcp-server/dist/services/whiteboard-geometry.d.ts +305 -0
- package/mcp-server/dist/services/whiteboard-geometry.d.ts.map +1 -0
- package/mcp-server/dist/services/whiteboard-geometry.js +666 -0
- package/mcp-server/dist/services/whiteboard-geometry.js.map +1 -0
- package/mcp-server/dist/tools/agents.d.ts +212 -0
- package/mcp-server/dist/tools/agents.d.ts.map +1 -0
- package/mcp-server/dist/tools/agents.js +407 -0
- package/mcp-server/dist/tools/agents.js.map +1 -0
- package/mcp-server/dist/tools/bluesky.d.ts +39 -0
- package/mcp-server/dist/tools/bluesky.d.ts.map +1 -0
- package/mcp-server/dist/tools/bluesky.js +1144 -0
- package/mcp-server/dist/tools/bluesky.js.map +1 -0
- package/mcp-server/dist/tools/browser-advanced.d.ts +58 -0
- package/mcp-server/dist/tools/browser-advanced.d.ts.map +1 -0
- package/mcp-server/dist/tools/browser-advanced.js +106 -0
- package/mcp-server/dist/tools/browser-advanced.js.map +1 -0
- package/mcp-server/dist/tools/browser-batch.d.ts +99 -0
- package/mcp-server/dist/tools/browser-batch.d.ts.map +1 -0
- package/mcp-server/dist/tools/browser-batch.js +187 -0
- package/mcp-server/dist/tools/browser-batch.js.map +1 -0
- package/mcp-server/dist/tools/browser-cheatsheet.d.ts +21 -0
- package/mcp-server/dist/tools/browser-cheatsheet.d.ts.map +1 -0
- package/mcp-server/dist/tools/browser-cheatsheet.js +144 -0
- package/mcp-server/dist/tools/browser-cheatsheet.js.map +1 -0
- package/mcp-server/dist/tools/browser-interact.d.ts +239 -0
- package/mcp-server/dist/tools/browser-interact.d.ts.map +1 -0
- package/mcp-server/dist/tools/browser-interact.js +300 -0
- package/mcp-server/dist/tools/browser-interact.js.map +1 -0
- package/mcp-server/dist/tools/browser-navigate.d.ts +90 -0
- package/mcp-server/dist/tools/browser-navigate.d.ts.map +1 -0
- package/mcp-server/dist/tools/browser-navigate.js +142 -0
- package/mcp-server/dist/tools/browser-navigate.js.map +1 -0
- package/mcp-server/dist/tools/browser-observe.d.ts +561 -0
- package/mcp-server/dist/tools/browser-observe.d.ts.map +1 -0
- package/mcp-server/dist/tools/browser-observe.js +2009 -0
- package/mcp-server/dist/tools/browser-observe.js.map +1 -0
- package/mcp-server/dist/tools/browser.d.ts +34 -0
- package/mcp-server/dist/tools/browser.d.ts.map +1 -0
- package/mcp-server/dist/tools/browser.js +112 -0
- package/mcp-server/dist/tools/browser.js.map +1 -0
- package/mcp-server/dist/tools/card-tools.d.ts +82 -0
- package/mcp-server/dist/tools/card-tools.d.ts.map +1 -0
- package/mcp-server/dist/tools/card-tools.js +137 -0
- package/mcp-server/dist/tools/card-tools.js.map +1 -0
- package/mcp-server/dist/tools/card.d.ts +8 -0
- package/mcp-server/dist/tools/card.d.ts.map +1 -0
- package/mcp-server/dist/tools/card.js +16 -0
- package/mcp-server/dist/tools/card.js.map +1 -0
- package/mcp-server/dist/tools/category.d.ts +32 -0
- package/mcp-server/dist/tools/category.d.ts.map +1 -0
- package/mcp-server/dist/tools/category.js +290 -0
- package/mcp-server/dist/tools/category.js.map +1 -0
- package/mcp-server/dist/tools/choice.d.ts +160 -0
- package/mcp-server/dist/tools/choice.d.ts.map +1 -0
- package/mcp-server/dist/tools/choice.js +80 -0
- package/mcp-server/dist/tools/choice.js.map +1 -0
- package/mcp-server/dist/tools/computer.d.ts +71 -0
- package/mcp-server/dist/tools/computer.d.ts.map +1 -0
- package/mcp-server/dist/tools/computer.js +174 -0
- package/mcp-server/dist/tools/computer.js.map +1 -0
- package/mcp-server/dist/tools/discord-channel.d.ts +44 -0
- package/mcp-server/dist/tools/discord-channel.d.ts.map +1 -0
- package/mcp-server/dist/tools/discord-channel.js +236 -0
- package/mcp-server/dist/tools/discord-channel.js.map +1 -0
- package/mcp-server/dist/tools/discord-guild.d.ts +20 -0
- package/mcp-server/dist/tools/discord-guild.d.ts.map +1 -0
- package/mcp-server/dist/tools/discord-guild.js +138 -0
- package/mcp-server/dist/tools/discord-guild.js.map +1 -0
- package/mcp-server/dist/tools/discord-member.d.ts +26 -0
- package/mcp-server/dist/tools/discord-member.d.ts.map +1 -0
- package/mcp-server/dist/tools/discord-member.js +122 -0
- package/mcp-server/dist/tools/discord-member.js.map +1 -0
- package/mcp-server/dist/tools/discord-message.d.ts +36 -0
- package/mcp-server/dist/tools/discord-message.d.ts.map +1 -0
- package/mcp-server/dist/tools/discord-message.js +184 -0
- package/mcp-server/dist/tools/discord-message.js.map +1 -0
- package/mcp-server/dist/tools/discord-onboarding.d.ts +32 -0
- package/mcp-server/dist/tools/discord-onboarding.d.ts.map +1 -0
- package/mcp-server/dist/tools/discord-onboarding.js +179 -0
- package/mcp-server/dist/tools/discord-onboarding.js.map +1 -0
- package/mcp-server/dist/tools/discord-role.d.ts +34 -0
- package/mcp-server/dist/tools/discord-role.d.ts.map +1 -0
- package/mcp-server/dist/tools/discord-role.js +178 -0
- package/mcp-server/dist/tools/discord-role.js.map +1 -0
- package/mcp-server/dist/tools/discord-thread.d.ts +28 -0
- package/mcp-server/dist/tools/discord-thread.d.ts.map +1 -0
- package/mcp-server/dist/tools/discord-thread.js +88 -0
- package/mcp-server/dist/tools/discord-thread.js.map +1 -0
- package/mcp-server/dist/tools/discord-webhook.d.ts +32 -0
- package/mcp-server/dist/tools/discord-webhook.d.ts.map +1 -0
- package/mcp-server/dist/tools/discord-webhook.js +154 -0
- package/mcp-server/dist/tools/discord-webhook.js.map +1 -0
- package/mcp-server/dist/tools/discord.d.ts +7 -0
- package/mcp-server/dist/tools/discord.d.ts.map +1 -0
- package/mcp-server/dist/tools/discord.js +25 -0
- package/mcp-server/dist/tools/discord.js.map +1 -0
- package/mcp-server/dist/tools/fb-groups.d.ts +98 -0
- package/mcp-server/dist/tools/fb-groups.d.ts.map +1 -0
- package/mcp-server/dist/tools/fb-groups.js +369 -0
- package/mcp-server/dist/tools/fb-groups.js.map +1 -0
- package/mcp-server/dist/tools/forget.d.ts +14 -0
- package/mcp-server/dist/tools/forget.d.ts.map +1 -0
- package/mcp-server/dist/tools/forget.js +26 -0
- package/mcp-server/dist/tools/forget.js.map +1 -0
- package/mcp-server/dist/tools/git-tools.d.ts +24 -0
- package/mcp-server/dist/tools/git-tools.d.ts.map +1 -0
- package/mcp-server/dist/tools/git-tools.js +107 -0
- package/mcp-server/dist/tools/git-tools.js.map +1 -0
- package/mcp-server/dist/tools/git.d.ts +7 -0
- package/mcp-server/dist/tools/git.d.ts.map +1 -0
- package/mcp-server/dist/tools/git.js +11 -0
- package/mcp-server/dist/tools/git.js.map +1 -0
- package/mcp-server/dist/tools/gsc/experience.d.ts +135 -0
- package/mcp-server/dist/tools/gsc/experience.d.ts.map +1 -0
- package/mcp-server/dist/tools/gsc/experience.js +351 -0
- package/mcp-server/dist/tools/gsc/experience.js.map +1 -0
- package/mcp-server/dist/tools/gsc/helpers.d.ts +78 -0
- package/mcp-server/dist/tools/gsc/helpers.d.ts.map +1 -0
- package/mcp-server/dist/tools/gsc/helpers.js +258 -0
- package/mcp-server/dist/tools/gsc/helpers.js.map +1 -0
- package/mcp-server/dist/tools/gsc/indexing.d.ts +121 -0
- package/mcp-server/dist/tools/gsc/indexing.d.ts.map +1 -0
- package/mcp-server/dist/tools/gsc/indexing.js +357 -0
- package/mcp-server/dist/tools/gsc/indexing.js.map +1 -0
- package/mcp-server/dist/tools/gsc/inspect.d.ts +78 -0
- package/mcp-server/dist/tools/gsc/inspect.d.ts.map +1 -0
- package/mcp-server/dist/tools/gsc/inspect.js +321 -0
- package/mcp-server/dist/tools/gsc/inspect.js.map +1 -0
- package/mcp-server/dist/tools/gsc/navigate.d.ts +44 -0
- package/mcp-server/dist/tools/gsc/navigate.d.ts.map +1 -0
- package/mcp-server/dist/tools/gsc/navigate.js +197 -0
- package/mcp-server/dist/tools/gsc/navigate.js.map +1 -0
- package/mcp-server/dist/tools/gsc/performance.d.ts +84 -0
- package/mcp-server/dist/tools/gsc/performance.d.ts.map +1 -0
- package/mcp-server/dist/tools/gsc/performance.js +274 -0
- package/mcp-server/dist/tools/gsc/performance.js.map +1 -0
- package/mcp-server/dist/tools/gsc/settings.d.ts +153 -0
- package/mcp-server/dist/tools/gsc/settings.d.ts.map +1 -0
- package/mcp-server/dist/tools/gsc/settings.js +425 -0
- package/mcp-server/dist/tools/gsc/settings.js.map +1 -0
- package/mcp-server/dist/tools/gsc.d.ts +7 -0
- package/mcp-server/dist/tools/gsc.d.ts.map +1 -0
- package/mcp-server/dist/tools/gsc.js +51 -0
- package/mcp-server/dist/tools/gsc.js.map +1 -0
- package/mcp-server/dist/tools/image-tools.d.ts +24 -0
- package/mcp-server/dist/tools/image-tools.d.ts.map +1 -0
- package/mcp-server/dist/tools/image-tools.js +80 -0
- package/mcp-server/dist/tools/image-tools.js.map +1 -0
- package/mcp-server/dist/tools/image.d.ts +10 -0
- package/mcp-server/dist/tools/image.d.ts.map +1 -0
- package/mcp-server/dist/tools/image.js +14 -0
- package/mcp-server/dist/tools/image.js.map +1 -0
- package/mcp-server/dist/tools/leonardo-browser-tools.d.ts +97 -0
- package/mcp-server/dist/tools/leonardo-browser-tools.d.ts.map +1 -0
- package/mcp-server/dist/tools/leonardo-browser-tools.js +228 -0
- package/mcp-server/dist/tools/leonardo-browser-tools.js.map +1 -0
- package/mcp-server/dist/tools/leonardo.d.ts +13 -0
- package/mcp-server/dist/tools/leonardo.d.ts.map +1 -0
- package/mcp-server/dist/tools/leonardo.js +21 -0
- package/mcp-server/dist/tools/leonardo.js.map +1 -0
- package/mcp-server/dist/tools/loop.d.ts +30 -0
- package/mcp-server/dist/tools/loop.d.ts.map +1 -0
- package/mcp-server/dist/tools/loop.js +401 -0
- package/mcp-server/dist/tools/loop.js.map +1 -0
- package/mcp-server/dist/tools/memories.d.ts +56 -0
- package/mcp-server/dist/tools/memories.d.ts.map +1 -0
- package/mcp-server/dist/tools/memories.js +171 -0
- package/mcp-server/dist/tools/memories.js.map +1 -0
- package/mcp-server/dist/tools/morelogin.d.ts +24 -0
- package/mcp-server/dist/tools/morelogin.d.ts.map +1 -0
- package/mcp-server/dist/tools/morelogin.js +79 -0
- package/mcp-server/dist/tools/morelogin.js.map +1 -0
- package/mcp-server/dist/tools/profile.d.ts +17 -0
- package/mcp-server/dist/tools/profile.d.ts.map +1 -0
- package/mcp-server/dist/tools/profile.js +114 -0
- package/mcp-server/dist/tools/profile.js.map +1 -0
- package/mcp-server/dist/tools/recall.d.ts +71 -0
- package/mcp-server/dist/tools/recall.d.ts.map +1 -0
- package/mcp-server/dist/tools/recall.js +147 -0
- package/mcp-server/dist/tools/recall.js.map +1 -0
- package/mcp-server/dist/tools/reflect.d.ts +56 -0
- package/mcp-server/dist/tools/reflect.d.ts.map +1 -0
- package/mcp-server/dist/tools/reflect.js +157 -0
- package/mcp-server/dist/tools/reflect.js.map +1 -0
- package/mcp-server/dist/tools/remember.d.ts +62 -0
- package/mcp-server/dist/tools/remember.d.ts.map +1 -0
- package/mcp-server/dist/tools/remember.js +201 -0
- package/mcp-server/dist/tools/remember.js.map +1 -0
- package/mcp-server/dist/tools/response.d.ts +23 -0
- package/mcp-server/dist/tools/response.d.ts.map +1 -0
- package/mcp-server/dist/tools/response.js +24 -0
- package/mcp-server/dist/tools/response.js.map +1 -0
- package/mcp-server/dist/tools/restore.d.ts +14 -0
- package/mcp-server/dist/tools/restore.d.ts.map +1 -0
- package/mcp-server/dist/tools/restore.js +23 -0
- package/mcp-server/dist/tools/restore.js.map +1 -0
- package/mcp-server/dist/tools/style-guide-tools.d.ts +51 -0
- package/mcp-server/dist/tools/style-guide-tools.d.ts.map +1 -0
- package/mcp-server/dist/tools/style-guide-tools.js +231 -0
- package/mcp-server/dist/tools/style-guide-tools.js.map +1 -0
- package/mcp-server/dist/tools/style-guide.d.ts +8 -0
- package/mcp-server/dist/tools/style-guide.d.ts.map +1 -0
- package/mcp-server/dist/tools/style-guide.js +12 -0
- package/mcp-server/dist/tools/style-guide.js.map +1 -0
- package/mcp-server/dist/tools/sync.d.ts +20 -0
- package/mcp-server/dist/tools/sync.d.ts.map +1 -0
- package/mcp-server/dist/tools/sync.js +156 -0
- package/mcp-server/dist/tools/sync.js.map +1 -0
- package/mcp-server/dist/tools/tictactoe-tools.d.ts +18 -0
- package/mcp-server/dist/tools/tictactoe-tools.d.ts.map +1 -0
- package/mcp-server/dist/tools/tictactoe-tools.js +78 -0
- package/mcp-server/dist/tools/tictactoe-tools.js.map +1 -0
- package/mcp-server/dist/tools/tictactoe.d.ts +7 -0
- package/mcp-server/dist/tools/tictactoe.d.ts.map +1 -0
- package/mcp-server/dist/tools/tictactoe.js +11 -0
- package/mcp-server/dist/tools/tictactoe.js.map +1 -0
- package/mcp-server/dist/tools/utils.d.ts +19 -0
- package/mcp-server/dist/tools/utils.d.ts.map +1 -0
- package/mcp-server/dist/tools/utils.js +54 -0
- package/mcp-server/dist/tools/utils.js.map +1 -0
- package/mcp-server/dist/tools/whiteboard-format.d.ts +88 -0
- package/mcp-server/dist/tools/whiteboard-format.d.ts.map +1 -0
- package/mcp-server/dist/tools/whiteboard-format.js +337 -0
- package/mcp-server/dist/tools/whiteboard-format.js.map +1 -0
- package/mcp-server/dist/tools/whiteboard-tools.d.ts +326 -0
- package/mcp-server/dist/tools/whiteboard-tools.d.ts.map +1 -0
- package/mcp-server/dist/tools/whiteboard-tools.js +222 -0
- package/mcp-server/dist/tools/whiteboard-tools.js.map +1 -0
- package/mcp-server/dist/tools/whiteboard.d.ts +8 -0
- package/mcp-server/dist/tools/whiteboard.d.ts.map +1 -0
- package/mcp-server/dist/tools/whiteboard.js +16 -0
- package/mcp-server/dist/tools/whiteboard.js.map +1 -0
- package/mcp-server/dist/tools/youtube/analytics.d.ts +46 -0
- package/mcp-server/dist/tools/youtube/analytics.d.ts.map +1 -0
- package/mcp-server/dist/tools/youtube/analytics.js +83 -0
- package/mcp-server/dist/tools/youtube/analytics.js.map +1 -0
- package/mcp-server/dist/tools/youtube/dedup.d.ts +35 -0
- package/mcp-server/dist/tools/youtube/dedup.d.ts.map +1 -0
- package/mcp-server/dist/tools/youtube/dedup.js +56 -0
- package/mcp-server/dist/tools/youtube/dedup.js.map +1 -0
- package/mcp-server/dist/tools/youtube/download.d.ts +31 -0
- package/mcp-server/dist/tools/youtube/download.d.ts.map +1 -0
- package/mcp-server/dist/tools/youtube/download.js +30 -0
- package/mcp-server/dist/tools/youtube/download.js.map +1 -0
- package/mcp-server/dist/tools/youtube/helpers.d.ts +66 -0
- package/mcp-server/dist/tools/youtube/helpers.d.ts.map +1 -0
- package/mcp-server/dist/tools/youtube/helpers.js +143 -0
- package/mcp-server/dist/tools/youtube/helpers.js.map +1 -0
- package/mcp-server/dist/tools/youtube/media.d.ts +43 -0
- package/mcp-server/dist/tools/youtube/media.d.ts.map +1 -0
- package/mcp-server/dist/tools/youtube/media.js +97 -0
- package/mcp-server/dist/tools/youtube/media.js.map +1 -0
- package/mcp-server/dist/tools/youtube/metadata.d.ts +33 -0
- package/mcp-server/dist/tools/youtube/metadata.d.ts.map +1 -0
- package/mcp-server/dist/tools/youtube/metadata.js +106 -0
- package/mcp-server/dist/tools/youtube/metadata.js.map +1 -0
- package/mcp-server/dist/tools/youtube/navigate.d.ts +30 -0
- package/mcp-server/dist/tools/youtube/navigate.d.ts.map +1 -0
- package/mcp-server/dist/tools/youtube/navigate.js +71 -0
- package/mcp-server/dist/tools/youtube/navigate.js.map +1 -0
- package/mcp-server/dist/tools/youtube/playlist.d.ts +29 -0
- package/mcp-server/dist/tools/youtube/playlist.d.ts.map +1 -0
- package/mcp-server/dist/tools/youtube/playlist.js +128 -0
- package/mcp-server/dist/tools/youtube/playlist.js.map +1 -0
- package/mcp-server/dist/tools/youtube/schedule.d.ts +31 -0
- package/mcp-server/dist/tools/youtube/schedule.d.ts.map +1 -0
- package/mcp-server/dist/tools/youtube/schedule.js +147 -0
- package/mcp-server/dist/tools/youtube/schedule.js.map +1 -0
- package/mcp-server/dist/tools/youtube/source.d.ts +36 -0
- package/mcp-server/dist/tools/youtube/source.d.ts.map +1 -0
- package/mcp-server/dist/tools/youtube/source.js +85 -0
- package/mcp-server/dist/tools/youtube/source.js.map +1 -0
- package/mcp-server/dist/tools/youtube/upload.d.ts +53 -0
- package/mcp-server/dist/tools/youtube/upload.d.ts.map +1 -0
- package/mcp-server/dist/tools/youtube/upload.js +255 -0
- package/mcp-server/dist/tools/youtube/upload.js.map +1 -0
- package/mcp-server/dist/tools/youtube.d.ts +12 -0
- package/mcp-server/dist/tools/youtube.d.ts.map +1 -0
- package/mcp-server/dist/tools/youtube.js +38 -0
- package/mcp-server/dist/tools/youtube.js.map +1 -0
- package/mcp-server/dist/tui.d.ts +3 -0
- package/mcp-server/dist/tui.d.ts.map +1 -0
- package/mcp-server/dist/tui.js +806 -0
- package/mcp-server/dist/tui.js.map +1 -0
- package/mcp-server/dist/types.d.ts +77 -0
- package/mcp-server/dist/types.d.ts.map +1 -0
- package/mcp-server/dist/types.js +2 -0
- package/mcp-server/dist/types.js.map +1 -0
- package/mcp-server/package-lock.json +4798 -0
- package/mcp-server/package.json +55 -38
- package/mcp-server/run.mjs +12 -2
- package/mcp-server/src/config.ts +48 -3
- package/mcp-server/src/http.ts +296 -93
- package/mcp-server/src/index.ts +546 -203
- package/mcp-server/src/preload-http.ts +19 -18
- package/mcp-server/src/preload.ts +19 -18
- package/mcp-server/src/scripts/setup-embeddings.ts +9 -0
- package/mcp-server/src/services/assistant-judgments.ts +157 -0
- package/mcp-server/src/services/browser-assist-gate.ts +383 -0
- package/mcp-server/src/services/browser-assist.ts +267 -0
- package/mcp-server/src/services/browser-bench.ts +477 -0
- package/mcp-server/src/services/browser-judgments.ts +414 -0
- package/mcp-server/src/services/browser-risk.ts +484 -0
- package/mcp-server/src/services/categories.ts +36 -19
- package/mcp-server/src/services/codex-config-heal.ts +403 -0
- package/mcp-server/src/services/desktop-assist-gate.ts +251 -0
- package/mcp-server/src/services/desktop-assist.ts +308 -0
- package/mcp-server/src/services/desktop-bench.ts +372 -0
- package/mcp-server/src/services/desktop-client.ts +76 -0
- package/mcp-server/src/services/desktop-judgments.ts +218 -0
- package/mcp-server/src/services/desktop-risk.ts +743 -0
- package/mcp-server/src/services/edit-stale.ts +299 -0
- package/mcp-server/src/services/embedding-service.ts +108 -0
- package/mcp-server/src/services/embedding-worker.ts +46 -0
- package/mcp-server/src/services/fb-regions.ts +221 -0
- package/mcp-server/src/services/file-checksums.ts +26 -9
- package/mcp-server/src/services/file-excerpt.ts +59 -0
- package/mcp-server/src/services/identity.ts +291 -0
- package/mcp-server/src/services/local-embeddings.ts +2 -77
- package/mcp-server/src/services/memory-client.ts +23 -0
- package/mcp-server/src/services/memory-judge-backfill.ts +462 -0
- package/mcp-server/src/services/memory-judgments.ts +901 -0
- package/mcp-server/src/services/memory-maintenance.ts +360 -0
- package/mcp-server/src/services/memory-retrieval.ts +349 -0
- package/mcp-server/src/services/memory-schema.ts +280 -0
- package/mcp-server/src/services/memory-sessions.ts +87 -0
- package/mcp-server/src/services/memory-triage.ts +182 -0
- package/mcp-server/src/services/memory-verification.ts +68 -0
- package/mcp-server/src/services/memory-writer.ts +125 -0
- package/mcp-server/src/services/neural-interface.ts +1217 -73
- package/mcp-server/src/services/profiles.ts +428 -0
- package/mcp-server/src/services/secret-gate.ts +52 -0
- package/mcp-server/src/services/server-instructions.ts +122 -0
- package/mcp-server/src/services/sqlite.ts +1456 -823
- package/mcp-server/src/services/typesafe-bench.ts +247 -0
- package/mcp-server/src/services/typesafe-config.ts +790 -0
- package/mcp-server/src/services/typesafe.ts +390 -0
- package/mcp-server/src/services/vector-cache.ts +255 -0
- package/mcp-server/src/services/whiteboard-geometry.ts +711 -0
- package/mcp-server/src/tools/agents.ts +539 -0
- package/mcp-server/src/tools/bluesky.ts +1186 -0
- package/mcp-server/src/tools/browser-advanced.ts +15 -6
- package/mcp-server/src/tools/browser-batch.ts +178 -0
- package/mcp-server/src/tools/browser-cheatsheet.ts +155 -0
- package/mcp-server/src/tools/browser-interact.ts +362 -255
- package/mcp-server/src/tools/browser-navigate.ts +164 -77
- package/mcp-server/src/tools/browser-observe.ts +2222 -1330
- package/mcp-server/src/tools/browser.ts +181 -107
- package/mcp-server/src/tools/card.ts +7 -5
- package/mcp-server/src/tools/choice.ts +103 -0
- package/mcp-server/src/tools/computer.ts +190 -0
- package/mcp-server/src/tools/discord.ts +10 -23
- package/mcp-server/src/tools/fb-groups.ts +387 -0
- package/mcp-server/src/tools/forget.ts +1 -1
- package/mcp-server/src/tools/git.ts +3 -1
- package/mcp-server/src/tools/gsc/experience.ts +440 -0
- package/mcp-server/src/tools/gsc/helpers.ts +288 -0
- package/mcp-server/src/tools/gsc/indexing.ts +495 -0
- package/mcp-server/src/tools/gsc/inspect.ts +391 -0
- package/mcp-server/src/tools/gsc/navigate.ts +236 -0
- package/mcp-server/src/tools/gsc/performance.ts +352 -0
- package/mcp-server/src/tools/gsc/settings.ts +564 -0
- package/mcp-server/src/tools/gsc.ts +95 -0
- package/mcp-server/src/tools/image.ts +3 -1
- package/mcp-server/src/tools/leonardo-browser-tools.ts +47 -37
- package/mcp-server/src/tools/leonardo.ts +7 -5
- package/mcp-server/src/tools/loop.ts +118 -25
- package/mcp-server/src/tools/memories.ts +65 -5
- package/mcp-server/src/tools/morelogin.ts +81 -0
- package/mcp-server/src/tools/profile.ts +122 -0
- package/mcp-server/src/tools/recall.ts +105 -261
- package/mcp-server/src/tools/reflect.ts +171 -145
- package/mcp-server/src/tools/remember.ts +223 -115
- package/mcp-server/src/tools/style-guide-tools.ts +250 -0
- package/mcp-server/src/tools/style-guide.ts +16 -0
- package/mcp-server/src/tools/sync.ts +66 -9
- package/mcp-server/src/tools/tictactoe.ts +3 -1
- package/mcp-server/src/tools/utils.ts +22 -0
- package/mcp-server/src/tools/whiteboard-format.ts +394 -0
- package/mcp-server/src/tools/whiteboard-tools.ts +191 -183
- package/mcp-server/src/tools/whiteboard.ts +7 -5
- package/mcp-server/src/tools/youtube/analytics.ts +106 -0
- package/mcp-server/src/tools/youtube/dedup.ts +57 -0
- package/mcp-server/src/tools/youtube/download.ts +41 -0
- package/mcp-server/src/tools/youtube/helpers.ts +183 -0
- package/mcp-server/src/tools/youtube/media.ts +109 -0
- package/mcp-server/src/tools/youtube/metadata.ts +112 -0
- package/mcp-server/src/tools/youtube/navigate.ts +81 -0
- package/mcp-server/src/tools/youtube/playlist.ts +134 -0
- package/mcp-server/src/tools/youtube/schedule.ts +158 -0
- package/mcp-server/src/tools/youtube/source.ts +101 -0
- package/mcp-server/src/tools/youtube/upload.ts +296 -0
- package/mcp-server/src/tools/youtube.ts +47 -0
- package/mcp-server/src/tui.ts +8 -9
- package/neural-interface/games/TicTacToe/1.svg +1 -0
- package/neural-interface/games/TicTacToe/2.svg +1 -0
- package/neural-interface/games/TicTacToe/3.svg +1 -0
- package/neural-interface/games/TicTacToe/4.svg +1 -0
- package/neural-interface/games/TicTacToe/5.svg +1 -0
- package/neural-interface/games/TicTacToe/6.svg +1 -0
- package/neural-interface/games/TicTacToe/7.svg +1 -0
- package/neural-interface/games/TicTacToe/8.svg +1 -0
- package/neural-interface/games/TicTacToe/9.svg +1 -0
- package/neural-interface/games/TicTacToe/Board.svg +1 -0
- package/neural-interface/games/TicTacToe/Circle.svg +1 -0
- package/neural-interface/games/TicTacToe/Cross.svg +1 -0
- package/neural-interface/i18n/en.json +6983 -0
- package/neural-interface/i18n/pt-BR.json +6987 -0
- package/neural-interface/lib/assistant-api.js +1005 -0
- package/neural-interface/lib/assistant-attachments.js +121 -0
- package/neural-interface/lib/assistant-brains/claude.js +207 -0
- package/neural-interface/lib/assistant-brains/codex-browser-policy-hook.mjs +43 -0
- package/neural-interface/lib/assistant-brains/codex-route-gate-hook.mjs +56 -0
- package/neural-interface/lib/assistant-brains/codex.js +213 -0
- package/neural-interface/lib/assistant-brains/opencode-browser-policy.js +47 -0
- package/neural-interface/lib/assistant-brains/opencode-route-gate.js +62 -0
- package/neural-interface/lib/assistant-brains/opencode.js +320 -0
- package/neural-interface/lib/assistant-budget.js +318 -0
- package/neural-interface/lib/assistant-catalog.js +731 -0
- package/neural-interface/lib/assistant-clarify.js +664 -0
- package/neural-interface/lib/assistant-config.js +232 -0
- package/neural-interface/lib/assistant-dispatch.js +2744 -0
- package/neural-interface/lib/assistant-envelope.js +579 -0
- package/neural-interface/lib/assistant-evidence.js +236 -0
- package/neural-interface/lib/assistant-hidden-models.js +272 -0
- package/neural-interface/lib/assistant-jev.js +117 -0
- package/neural-interface/lib/assistant-judgments.js +111 -0
- package/neural-interface/lib/assistant-media.js +335 -0
- package/neural-interface/lib/assistant-memory.js +398 -0
- package/neural-interface/lib/assistant-persona.js +444 -0
- package/neural-interface/lib/assistant-plan-permissions.js +448 -0
- package/neural-interface/lib/assistant-playbooks/design.md +87 -0
- package/neural-interface/lib/assistant-playbooks.js +72 -0
- package/neural-interface/lib/assistant-pricing.js +159 -0
- package/neural-interface/lib/assistant-route-gate-codex.js +163 -0
- package/neural-interface/lib/assistant-route-gate.js +212 -0
- package/neural-interface/lib/assistant-router.js +1324 -0
- package/neural-interface/lib/assistant-runtime.js +3337 -0
- package/neural-interface/lib/assistant-task-prompt.js +472 -0
- package/neural-interface/lib/assistant-usage.js +1688 -0
- package/neural-interface/lib/augmented-path.js +50 -0
- package/neural-interface/lib/automation-storage-migration.js +54 -0
- package/neural-interface/lib/backup-service.js +851 -0
- package/neural-interface/lib/backup-zip-reader.js +262 -0
- package/neural-interface/lib/browser-assist-api.js +41 -0
- package/neural-interface/lib/browser-console.js +176 -0
- package/neural-interface/lib/browser-execution.js +241 -0
- package/neural-interface/lib/browser-extraction.js +191 -0
- package/neural-interface/lib/browser-screenshot.js +399 -0
- package/neural-interface/lib/browser-semantic-context.js +639 -0
- package/neural-interface/lib/browser-snapshots.js +192 -0
- package/neural-interface/lib/browser-tab-recovery.js +119 -0
- package/neural-interface/lib/browser-targets.js +70 -0
- package/neural-interface/lib/browser-tool-policy.js +926 -0
- package/neural-interface/lib/claude-accounts.js +230 -0
- package/neural-interface/lib/claude-agent-bridge.js +3296 -0
- package/neural-interface/lib/claude-bypass-policy.js +99 -0
- package/neural-interface/lib/claude-history.js +429 -0
- package/neural-interface/lib/claude-model-catalog.js +483 -0
- package/neural-interface/lib/claude-panel-requests.js +165 -0
- package/neural-interface/lib/claude-panel-session.js +272 -0
- package/neural-interface/lib/claude-permission-rules.js +153 -0
- package/neural-interface/lib/claude-session-cleanup.js +71 -0
- package/neural-interface/lib/claude-session-list.js +82 -0
- package/neural-interface/lib/claude-session-ops.js +140 -0
- package/neural-interface/lib/claude-settings-view.js +51 -0
- package/neural-interface/lib/claude-skin-connect.js +79 -0
- package/neural-interface/lib/claude-stall-policy.js +7 -0
- package/neural-interface/lib/claude-temporary.js +435 -0
- package/neural-interface/lib/cli-update-planner.js +312 -0
- package/neural-interface/lib/codex-capabilities.d.ts +41 -0
- package/neural-interface/lib/codex-capabilities.js +148 -0
- package/neural-interface/lib/codex-collaboration-mode.js +17 -0
- package/neural-interface/lib/codex-compat-rpc.js +241 -0
- package/neural-interface/lib/codex-mcp-approvals.js +56 -0
- package/neural-interface/lib/codex-mcp-apps.js +47 -0
- package/neural-interface/lib/codex-model-context.js +202 -0
- package/neural-interface/lib/codex-plan-store.js +71 -0
- package/neural-interface/lib/codex-request-journal.js +242 -0
- package/neural-interface/lib/codex-runtime-path.js +213 -0
- package/neural-interface/lib/codex-session-catalog.js +231 -0
- package/neural-interface/lib/codex-session-settings.js +46 -0
- package/neural-interface/lib/codex-sidepanel-permissions.js +110 -0
- package/neural-interface/lib/codex-thread-history.js +165 -0
- package/neural-interface/lib/codex-user-verification.js +34 -0
- package/neural-interface/lib/codex-writer-lifecycle.js +152 -0
- package/neural-interface/lib/confined-fs.js +256 -0
- package/neural-interface/lib/db.js +922 -0
- package/neural-interface/lib/design-md.js +9 -0
- package/neural-interface/lib/desktop/api.js +92 -0
- package/neural-interface/lib/desktop/audit.js +62 -0
- package/neural-interface/lib/desktop/build.js +423 -0
- package/neural-interface/lib/desktop/config.js +606 -0
- package/neural-interface/lib/desktop/coords.js +76 -0
- package/neural-interface/lib/desktop/fake-helper.js +1192 -0
- package/neural-interface/lib/desktop/frames.js +77 -0
- package/neural-interface/lib/desktop/grants.js +84 -0
- package/neural-interface/lib/desktop/guards.js +170 -0
- package/neural-interface/lib/desktop/helper/SynabunDesktop.swift +3056 -0
- package/neural-interface/lib/desktop/lease.js +51 -0
- package/neural-interface/lib/desktop/manager.js +545 -0
- package/neural-interface/lib/desktop/press-contexts.js +103 -0
- package/neural-interface/lib/desktop/protocol.js +495 -0
- package/neural-interface/lib/desktop/service.js +1202 -0
- package/neural-interface/lib/effort-levels.js +148 -0
- package/neural-interface/lib/event-loop-monitor.js +67 -0
- package/neural-interface/lib/exec-loop-command.js +209 -0
- package/neural-interface/lib/folder-picker.ps1 +77 -0
- package/neural-interface/lib/http-guards.js +287 -0
- package/neural-interface/lib/jsonl-lines.js +94 -0
- package/neural-interface/lib/loop-logger.js +127 -0
- package/neural-interface/lib/mcp-client-config.js +133 -0
- package/neural-interface/lib/mcp-installer.js +487 -0
- package/neural-interface/lib/mcp-profile-refresh-coordinator.js +78 -0
- package/neural-interface/lib/mcp-profile-servers.js +120 -0
- package/neural-interface/lib/memo-cache.js +84 -0
- package/neural-interface/lib/memory-api.js +758 -0
- package/neural-interface/lib/memory-map-api.js +1150 -0
- package/neural-interface/lib/memory-map-layout.js +872 -0
- package/neural-interface/lib/memory-map-worker.js +165 -0
- package/neural-interface/lib/memory-session-capture.js +81 -0
- package/neural-interface/lib/memory-timers.js +62 -0
- package/neural-interface/lib/morelogin.js +301 -0
- package/neural-interface/lib/native-binary-runtime.js +650 -0
- package/neural-interface/lib/native-loop-goal.js +33 -0
- package/neural-interface/lib/native-loop-providers.js +1016 -0
- package/neural-interface/lib/native-loop-runtime.js +747 -0
- package/neural-interface/lib/opencode-hidden-models.js +113 -0
- package/neural-interface/lib/opencode-history.js +253 -0
- package/neural-interface/lib/opencode-runtime-config.js +58 -0
- package/neural-interface/lib/opencode-v2-client.js +486 -0
- package/neural-interface/lib/opencode-v2-ws-requests.js +955 -0
- package/neural-interface/lib/path-confine.js +213 -0
- package/neural-interface/lib/plugin-installer.js +264 -0
- package/neural-interface/lib/project-storage.js +766 -0
- package/neural-interface/lib/pty-host/core.js +603 -0
- package/neural-interface/lib/pty-host/flow.js +43 -0
- package/neural-interface/lib/pty-host/host.js +47 -0
- package/neural-interface/lib/pty-host/manager.js +360 -0
- package/neural-interface/lib/pty-host/protocol.js +81 -0
- package/neural-interface/lib/pty-host/snapshot.js +133 -0
- package/neural-interface/lib/remote-policy.js +582 -0
- package/neural-interface/lib/rulesets/api.js +175 -0
- package/neural-interface/lib/rulesets/fs-safe.js +245 -0
- package/neural-interface/lib/rulesets/installer.js +1453 -0
- package/neural-interface/lib/rulesets/json-edit.js +186 -0
- package/neural-interface/lib/rulesets/lock.js +170 -0
- package/neural-interface/lib/rulesets/managed-text.js +314 -0
- package/neural-interface/lib/rulesets/normalise.js +24 -0
- package/neural-interface/lib/rulesets/render.js +84 -0
- package/neural-interface/lib/schedule-launch-intent.js +40 -0
- package/neural-interface/lib/session-cache-builder.js +524 -0
- package/neural-interface/lib/session-chunker.js +344 -0
- package/neural-interface/lib/session-indexer.js +327 -0
- package/neural-interface/lib/session-title-generator.js +227 -0
- package/neural-interface/lib/skill-prompt-bridge.js +11 -0
- package/neural-interface/lib/style-guide/api.js +375 -0
- package/neural-interface/lib/style-guide/color.js +352 -0
- package/neural-interface/lib/style-guide/export-css.js +99 -0
- package/neural-interface/lib/style-guide/export-dtcg.js +209 -0
- package/neural-interface/lib/style-guide/export-tailwind.js +102 -0
- package/neural-interface/lib/style-guide/fonts.json +227 -0
- package/neural-interface/lib/style-guide/import.js +1268 -0
- package/neural-interface/lib/style-guide/pointers.js +103 -0
- package/neural-interface/lib/style-guide/presets.js +232 -0
- package/neural-interface/lib/style-guide/render-design-md.js +319 -0
- package/neural-interface/lib/style-guide/schema.js +848 -0
- package/neural-interface/lib/style-guide/security.js +43 -0
- package/neural-interface/lib/style-guide/store.js +491 -0
- package/neural-interface/lib/style-guide/summary.js +106 -0
- package/neural-interface/lib/style-guide/tokens.js +170 -0
- package/neural-interface/lib/synabun-update-plan.js +305 -0
- package/neural-interface/lib/synabun-version.js +179 -0
- package/neural-interface/lib/system-restore.js +99 -0
- package/neural-interface/lib/terminal-env.js +86 -0
- package/neural-interface/lib/ui-blob-store.js +106 -0
- package/neural-interface/lib/vector-cache.js +2 -0
- package/neural-interface/lib/virtual-socket.js +60 -0
- package/neural-interface/lib/whatsapp/api.js +146 -0
- package/neural-interface/lib/whatsapp/auth-store.js +389 -0
- package/neural-interface/lib/whatsapp/baileys-adapter.js +457 -0
- package/neural-interface/lib/whatsapp/brain.js +128 -0
- package/neural-interface/lib/whatsapp/bridge.js +1499 -0
- package/neural-interface/lib/whatsapp/cards.js +519 -0
- package/neural-interface/lib/whatsapp/commands.js +92 -0
- package/neural-interface/lib/whatsapp/config.js +291 -0
- package/neural-interface/lib/whatsapp/connector/entry.mjs +25 -0
- package/neural-interface/lib/whatsapp/connector/manifest.json +6 -0
- package/neural-interface/lib/whatsapp/connector/npm-shrinkwrap.json +717 -0
- package/neural-interface/lib/whatsapp/connector/package.json +8 -0
- package/neural-interface/lib/whatsapp/fake-baileys.js +444 -0
- package/neural-interface/lib/whatsapp/format.js +552 -0
- package/neural-interface/lib/whatsapp/host-core.js +1225 -0
- package/neural-interface/lib/whatsapp/host.js +119 -0
- package/neural-interface/lib/whatsapp/identity.js +288 -0
- package/neural-interface/lib/whatsapp/inbound.js +149 -0
- package/neural-interface/lib/whatsapp/installer.js +562 -0
- package/neural-interface/lib/whatsapp/manager.js +638 -0
- package/neural-interface/lib/whatsapp/paths.js +81 -0
- package/neural-interface/lib/whatsapp/protocol.js +327 -0
- package/neural-interface/lib/whatsapp/qr-svg.js +57 -0
- package/neural-interface/lib/whatsapp/redact.js +130 -0
- package/neural-interface/lib/whatsapp/service.js +1305 -0
- package/neural-interface/lib/whatsapp/vendor/qrcodegen.js +848 -0
- package/neural-interface/package-lock.json +4682 -0
- package/neural-interface/package.json +58 -27
- package/neural-interface/public/apple-touch-icon.png +0 -0
- package/neural-interface/public/category-logos/synabun.png +0 -0
- package/neural-interface/public/claude.svg +1 -0
- package/neural-interface/public/favicon-16x16.png +0 -0
- package/neural-interface/public/favicon-32x32.png +0 -0
- package/neural-interface/public/icon-192.png +0 -0
- package/neural-interface/public/icon-512.png +0 -0
- package/neural-interface/public/index.html +32 -182
- package/neural-interface/public/index2d.html +2 -2
- package/neural-interface/public/logo-synabun.png +0 -0
- package/neural-interface/public/logoHD.png +0 -0
- package/neural-interface/public/mcp.svg +4 -0
- package/neural-interface/public/offline.html +335 -299
- package/neural-interface/public/onboarding.html +692 -155
- package/neural-interface/public/opencode-logo-dark-square.svg +1 -0
- package/neural-interface/public/shared/agent-runtime-options.js +596 -0
- package/neural-interface/public/shared/api.js +339 -28
- package/neural-interface/public/shared/assistant/asst-accounts.js +318 -0
- package/neural-interface/public/shared/assistant/asst-agents-dock.js +296 -0
- package/neural-interface/public/shared/assistant/asst-api.js +270 -0
- package/neural-interface/public/shared/assistant/asst-brain-picker.js +744 -0
- package/neural-interface/public/shared/assistant/asst-budget.js +333 -0
- package/neural-interface/public/shared/assistant/asst-composer.js +656 -0
- package/neural-interface/public/shared/assistant/asst-computer.js +803 -0
- package/neural-interface/public/shared/assistant/asst-control.js +1018 -0
- package/neural-interface/public/shared/assistant/asst-hosts.js +262 -0
- package/neural-interface/public/shared/assistant/asst-mascot.js +311 -0
- package/neural-interface/public/shared/assistant/asst-menu.js +375 -0
- package/neural-interface/public/shared/assistant/asst-panel.js +1845 -0
- package/neural-interface/public/shared/assistant/asst-rack-styles.js +589 -0
- package/neural-interface/public/shared/assistant/asst-render.js +3296 -0
- package/neural-interface/public/shared/assistant/asst-route.js +1653 -0
- package/neural-interface/public/shared/assistant/asst-roving.js +75 -0
- package/neural-interface/public/shared/assistant/asst-sanitize.js +117 -0
- package/neural-interface/public/shared/assistant/asst-sidepanel-styles.js +488 -0
- package/neural-interface/public/shared/assistant/asst-sidepanel.js +675 -0
- package/neural-interface/public/shared/assistant/asst-slash.js +258 -0
- package/neural-interface/public/shared/assistant/asst-state.js +727 -0
- package/neural-interface/public/shared/assistant/asst-styles.js +1684 -0
- package/neural-interface/public/shared/assistant/asst-tool-kinds.js +1017 -0
- package/neural-interface/public/shared/assistant/asst-usage.js +444 -0
- package/neural-interface/public/shared/assistant/asst-ws.js +175 -0
- package/neural-interface/public/shared/automation-context.js +75 -0
- package/neural-interface/public/shared/blob-store.js +99 -0
- package/neural-interface/public/shared/cdx/cdx-capabilities.js +118 -0
- package/neural-interface/public/shared/cdx/cdx-cards.js +37 -0
- package/neural-interface/public/shared/cdx/cdx-commands.js +171 -0
- package/neural-interface/public/shared/cdx/cdx-context-menu.js +402 -0
- package/neural-interface/public/shared/cdx/cdx-context-model.js +220 -0
- package/neural-interface/public/shared/cdx/cdx-features.js +408 -0
- package/neural-interface/public/shared/cdx/cdx-icons.js +89 -0
- package/neural-interface/public/shared/cdx/cdx-mcp-app-host.js +414 -0
- package/neural-interface/public/shared/cdx/cdx-mcp-app-proxy.html +6 -0
- package/neural-interface/public/shared/cdx/cdx-mcp-app-proxy.js +56 -0
- package/neural-interface/public/shared/cdx/cdx-mcp-app.js +178 -0
- package/neural-interface/public/shared/cdx/cdx-model-catalog.js +120 -0
- package/neural-interface/public/shared/cdx/cdx-output-model.js +261 -0
- package/neural-interface/public/shared/cdx/cdx-output.js +382 -0
- package/neural-interface/public/shared/cdx/cdx-panel.js +1372 -0
- package/neural-interface/public/shared/cdx/cdx-plan.js +266 -0
- package/neural-interface/public/shared/cdx/cdx-protocol.js +682 -0
- package/neural-interface/public/shared/cdx/cdx-render.js +3381 -0
- package/neural-interface/public/shared/cdx/cdx-requests.js +1136 -0
- package/neural-interface/public/shared/cdx/cdx-styles.js +3251 -0
- package/neural-interface/public/shared/cdx/cdx-tabs.js +7992 -0
- package/neural-interface/public/shared/cdx/cdx-ws.js +363 -0
- package/neural-interface/public/shared/cli-status.js +169 -0
- package/neural-interface/public/shared/constants.js +22 -5
- package/neural-interface/public/shared/cp/cp-agents.js +186 -0
- package/neural-interface/public/shared/cp/cp-bash.js +252 -0
- package/neural-interface/public/shared/cp/cp-compose.js +159 -0
- package/neural-interface/public/shared/cp/cp-context-menu.js +309 -0
- package/neural-interface/public/shared/cp/cp-context-model.js +157 -0
- package/neural-interface/public/shared/cp/cp-context-window.js +80 -0
- package/neural-interface/public/shared/cp/cp-ctx.js +11 -0
- package/neural-interface/public/shared/cp/cp-diff.js +255 -0
- package/neural-interface/public/shared/cp/cp-event-rows.js +637 -0
- package/neural-interface/public/shared/cp/cp-events.js +846 -0
- package/neural-interface/public/shared/cp/cp-markdown.js +104 -0
- package/neural-interface/public/shared/cp/cp-permission-model.js +905 -0
- package/neural-interface/public/shared/cp/cp-permissions.js +626 -0
- package/neural-interface/public/shared/cp/cp-rehydrate.js +113 -0
- package/neural-interface/public/shared/cp/cp-restore.js +224 -0
- package/neural-interface/public/shared/cp/cp-session-model.js +338 -0
- package/neural-interface/public/shared/cp/cp-session-settings.js +254 -0
- package/neural-interface/public/shared/cp/cp-sessions.js +227 -0
- package/neural-interface/public/shared/cp/cp-statusline.js +153 -0
- package/neural-interface/public/shared/cp/cp-styles.js +349 -0
- package/neural-interface/public/shared/cp/cp-tasks-model.js +282 -0
- package/neural-interface/public/shared/cp/cp-tasks.js +107 -0
- package/neural-interface/public/shared/cp/cp-temporary-view.js +56 -0
- package/neural-interface/public/shared/cp/cp-temporary.js +231 -0
- package/neural-interface/public/shared/cp/cp-tool-cards.js +326 -0
- package/neural-interface/public/shared/cp/cp-tool-results.js +618 -0
- package/neural-interface/public/shared/cp/cp-usage-model.js +124 -0
- package/neural-interface/public/shared/graph-activity.js +68 -0
- package/neural-interface/public/shared/html-shell.js +107 -12
- package/neural-interface/public/shared/i18n.js +132 -34
- package/neural-interface/public/shared/node-positions-store.js +44 -0
- package/neural-interface/public/shared/ocp-hidden-models.js +116 -0
- package/neural-interface/public/shared/ocp-v2/ocp-v2-approvals.js +355 -0
- package/neural-interface/public/shared/ocp-v2/ocp-v2-autoaccept-button.js +35 -0
- package/neural-interface/public/shared/ocp-v2/ocp-v2-automation-ownership.js +72 -0
- package/neural-interface/public/shared/ocp-v2/ocp-v2-binding.js +280 -0
- package/neural-interface/public/shared/ocp-v2/ocp-v2-caps.js +67 -0
- package/neural-interface/public/shared/ocp-v2/ocp-v2-changes-logic.js +179 -0
- package/neural-interface/public/shared/ocp-v2/ocp-v2-changes.js +164 -0
- package/neural-interface/public/shared/ocp-v2/ocp-v2-childpanel.js +725 -0
- package/neural-interface/public/shared/ocp-v2/ocp-v2-compact-button.js +75 -0
- package/neural-interface/public/shared/ocp-v2/ocp-v2-composer-logic.js +1204 -0
- package/neural-interface/public/shared/ocp-v2/ocp-v2-confirm-logic.js +117 -0
- package/neural-interface/public/shared/ocp-v2/ocp-v2-confirm.js +59 -0
- package/neural-interface/public/shared/ocp-v2/ocp-v2-context-gauge.js +252 -0
- package/neural-interface/public/shared/ocp-v2/ocp-v2-context-menu.js +478 -0
- package/neural-interface/public/shared/ocp-v2/ocp-v2-context-model.js +179 -0
- package/neural-interface/public/shared/ocp-v2/ocp-v2-events.js +233 -0
- package/neural-interface/public/shared/ocp-v2/ocp-v2-icons.js +30 -0
- package/neural-interface/public/shared/ocp-v2/ocp-v2-manager.js +1065 -0
- package/neural-interface/public/shared/ocp-v2/ocp-v2-mention-hints.js +182 -0
- package/neural-interface/public/shared/ocp-v2/ocp-v2-modelpicker.js +655 -0
- package/neural-interface/public/shared/ocp-v2/ocp-v2-panel.js +2636 -0
- package/neural-interface/public/shared/ocp-v2/ocp-v2-plan.js +601 -0
- package/neural-interface/public/shared/ocp-v2/ocp-v2-profile-inheritance.js +78 -0
- package/neural-interface/public/shared/ocp-v2/ocp-v2-projectbar.js +649 -0
- package/neural-interface/public/shared/ocp-v2/ocp-v2-providers.js +35 -0
- package/neural-interface/public/shared/ocp-v2/ocp-v2-rehydrate.js +357 -0
- package/neural-interface/public/shared/ocp-v2/ocp-v2-render-logic.js +189 -0
- package/neural-interface/public/shared/ocp-v2/ocp-v2-render.js +1869 -0
- package/neural-interface/public/shared/ocp-v2/ocp-v2-select.js +42 -0
- package/neural-interface/public/shared/ocp-v2/ocp-v2-send-logic.js +117 -0
- package/neural-interface/public/shared/ocp-v2/ocp-v2-send.js +1714 -0
- package/neural-interface/public/shared/ocp-v2/ocp-v2-session-actions.js +468 -0
- package/neural-interface/public/shared/ocp-v2/ocp-v2-session-widgets.js +63 -0
- package/neural-interface/public/shared/ocp-v2/ocp-v2-sessions-logic.js +366 -0
- package/neural-interface/public/shared/ocp-v2/ocp-v2-settings-link.js +19 -0
- package/neural-interface/public/shared/ocp-v2/ocp-v2-slash-hints.js +343 -0
- package/neural-interface/public/shared/ocp-v2/ocp-v2-state.js +746 -0
- package/neural-interface/public/shared/ocp-v2/ocp-v2-status-logic.js +347 -0
- package/neural-interface/public/shared/ocp-v2/ocp-v2-status.js +338 -0
- package/neural-interface/public/shared/ocp-v2/ocp-v2-styles.js +3543 -0
- package/neural-interface/public/shared/ocp-v2/ocp-v2-subagents-logic.js +482 -0
- package/neural-interface/public/shared/ocp-v2/ocp-v2-tools-logic.js +351 -0
- package/neural-interface/public/shared/ocp-v2/ocp-v2-tools.js +168 -0
- package/neural-interface/public/shared/ocp-v2/ocp-v2-variantpicker.js +315 -0
- package/neural-interface/public/shared/ocp-v2/ocp-v2-ws.js +545 -0
- package/neural-interface/public/shared/provider-icons.js +30 -0
- package/neural-interface/public/shared/session-title.js +100 -0
- package/neural-interface/public/shared/settings/settings-ia.js +389 -0
- package/neural-interface/public/shared/settings/settings-kit.js +234 -0
- package/neural-interface/public/shared/settings/settings-search.js +463 -0
- package/neural-interface/public/shared/settings/settings-shell.js +570 -0
- package/neural-interface/public/shared/state.js +5 -0
- package/neural-interface/public/shared/storage.js +50 -7
- package/neural-interface/public/shared/styleguide/sg-agents.js +42 -0
- package/neural-interface/public/shared/styleguide/sg-brand.js +4 -0
- package/neural-interface/public/shared/styleguide/sg-color-client.js +388 -0
- package/neural-interface/public/shared/styleguide/sg-colors.js +148 -0
- package/neural-interface/public/shared/styleguide/sg-components.js +35 -0
- package/neural-interface/public/shared/styleguide/sg-history.js +41 -0
- package/neural-interface/public/shared/styleguide/sg-icons-logo.js +38 -0
- package/neural-interface/public/shared/styleguide/sg-imagery.js +4 -0
- package/neural-interface/public/shared/styleguide/sg-import-export.js +47 -0
- package/neural-interface/public/shared/styleguide/sg-kit.js +281 -0
- package/neural-interface/public/shared/styleguide/sg-labels.js +318 -0
- package/neural-interface/public/shared/styleguide/sg-layout.js +4 -0
- package/neural-interface/public/shared/styleguide/sg-motion.js +5 -0
- package/neural-interface/public/shared/styleguide/sg-panel.js +229 -0
- package/neural-interface/public/shared/styleguide/sg-preview.js +138 -0
- package/neural-interface/public/shared/styleguide/sg-rules.js +9 -0
- package/neural-interface/public/shared/styleguide/sg-shape.js +24 -0
- package/neural-interface/public/shared/styleguide/sg-state.js +132 -0
- package/neural-interface/public/shared/styleguide/sg-typography.js +124 -0
- package/neural-interface/public/shared/styles.css +7883 -1155
- package/neural-interface/public/shared/synabun-mascot.js +1625 -0
- package/neural-interface/public/shared/synabun-ticker.js +376 -0
- package/neural-interface/public/shared/term-renderer.js +343 -0
- package/neural-interface/public/shared/ui-assistant-panel.js +10 -0
- package/neural-interface/public/shared/ui-assistant.js +77 -0
- package/neural-interface/public/shared/ui-automation-studio.js +5105 -3573
- package/neural-interface/public/shared/ui-claude-panel.js +11685 -6333
- package/neural-interface/public/shared/ui-codex-panel.js +12 -0
- package/neural-interface/public/shared/ui-cost-widget.js +55 -34
- package/neural-interface/public/shared/ui-detail.js +77 -14
- package/neural-interface/public/shared/ui-explorer.js +360 -181
- package/neural-interface/public/shared/ui-file-explorer.js +485 -107
- package/neural-interface/public/shared/ui-help.js +2 -2
- package/neural-interface/public/shared/ui-image-gallery.js +45 -21
- package/neural-interface/public/shared/ui-judgments.js +1346 -0
- package/neural-interface/public/shared/ui-keybinds.js +3 -2
- package/neural-interface/public/shared/ui-link.js +105 -51
- package/neural-interface/public/shared/ui-loading.js +4 -4
- package/neural-interface/public/shared/ui-memory-maintenance.js +111 -0
- package/neural-interface/public/shared/ui-menubar.js +89 -4
- package/neural-interface/public/shared/ui-native-loop-router-state.js +94 -0
- package/neural-interface/public/shared/ui-native-loop-router.js +493 -0
- package/neural-interface/public/shared/ui-native-window-id.js +7 -0
- package/neural-interface/public/shared/ui-navbar.js +357 -222
- package/neural-interface/public/shared/ui-notifications.js +58 -12
- package/neural-interface/public/shared/ui-opencode-panel-v2.js +17 -0
- package/neural-interface/public/shared/ui-panels.js +15 -7
- package/neural-interface/public/shared/ui-resume.js +823 -653
- package/neural-interface/public/shared/ui-schedule-queue.js +172 -0
- package/neural-interface/public/shared/ui-schedules-studio.js +1882 -0
- package/neural-interface/public/shared/ui-scroll-follow.js +194 -0
- package/neural-interface/public/shared/ui-sessions.js +789 -34
- package/neural-interface/public/shared/ui-settings.js +5793 -2190
- package/neural-interface/public/shared/ui-sidepanel-layout.js +109 -0
- package/neural-interface/public/shared/ui-sidepanel-runtime.js +19 -0
- package/neural-interface/public/shared/ui-sidepanel-session-entry.js +40 -0
- package/neural-interface/public/shared/ui-sidepanel-sessions.js +221 -0
- package/neural-interface/public/shared/ui-sidepanel-state.js +131 -0
- package/neural-interface/public/shared/ui-sidepanel-tray.js +547 -0
- package/neural-interface/public/shared/ui-sidepanel-windows.js +387 -0
- package/neural-interface/public/shared/ui-skills.js +18 -32
- package/neural-interface/public/shared/ui-styleguide.js +35 -0
- package/neural-interface/public/shared/ui-sync.js +87 -0
- package/neural-interface/public/shared/ui-terminal.js +7877 -6385
- package/neural-interface/public/shared/ui-tool-updates.js +292 -0
- package/neural-interface/public/shared/ui-tooltip.js +156 -15
- package/neural-interface/public/shared/ui-tutorial-draw.js +119 -683
- package/neural-interface/public/shared/ui-tutorial-layout.js +135 -0
- package/neural-interface/public/shared/ui-tutorial-steps.js +59 -2209
- package/neural-interface/public/shared/ui-tutorial.css +102 -0
- package/neural-interface/public/shared/ui-tutorial.js +557 -1076
- package/neural-interface/public/shared/ui-update.js +420 -68
- package/neural-interface/public/shared/ui-whatsapp.js +941 -0
- package/neural-interface/public/shared/ui-whiteboard.js +1076 -233
- package/neural-interface/public/shared/ui-workspaces.js +2 -1
- package/neural-interface/public/shared/utils.js +58 -0
- package/neural-interface/public/shared/vterm-buffer.js +15 -5
- package/neural-interface/public/shared/whatsapp/wa-phone.js +65 -0
- package/neural-interface/public/shared/whatsapp/wa-view.js +411 -0
- package/neural-interface/public/sidepanel-session.html +18 -0
- package/neural-interface/public/sw.js +8 -2
- package/neural-interface/public/synabun.png +0 -0
- package/neural-interface/public/variant/2d/graph.js +1707 -1635
- package/neural-interface/public/variant/2d/lasso.js +2 -1
- package/neural-interface/public/variant/2d/layout.js +595 -594
- package/neural-interface/public/variant/2d/main.js +556 -521
- package/neural-interface/public/variant/2d/settings-gfx.js +18 -24
- package/neural-interface/public/variant/3d/main.js +516 -500
- package/neural-interface/public/variant/3d/map-controls.js +144 -0
- package/neural-interface/public/variant/3d/map-data.js +251 -0
- package/neural-interface/public/variant/3d/map-labels.js +241 -0
- package/neural-interface/public/variant/3d/map-pick.js +90 -0
- package/neural-interface/public/variant/3d/map-renderer.js +521 -0
- package/neural-interface/public/variant/3d/map-shaders.js +158 -0
- package/neural-interface/public/variant/3d/map-view.js +618 -0
- package/neural-interface/public/vendor/three/manifest.json +3 -0
- package/neural-interface/public/vendor/three/three-0.169.0.min.js +3844 -0
- package/neural-interface/public/vendor/xterm/manifest.json +3 -0
- package/neural-interface/public/vendor/xterm/xterm-6.0.0.css +285 -0
- package/neural-interface/public/vendor/xterm/xterm-6.0.0.min.js +192 -0
- package/neural-interface/scripts/build-vendor.mjs +64 -0
- package/neural-interface/scripts/codex-protocol-fixture.mjs +83 -0
- package/neural-interface/scripts/rebuild-pty.js +37 -23
- package/neural-interface/scripts/vendor-three-entry.js +24 -0
- package/neural-interface/scripts/vendor-xterm-entry.js +8 -0
- package/neural-interface/server.js +33948 -15000
- package/neural-interface/skins/default/skin.css +90 -0
- package/neural-interface/skins/default/skin.json +8 -0
- package/neural-interface/templates/rulesets/README.md +9 -0
- package/neural-interface/templates/rulesets/claude.md +3 -0
- package/neural-interface/templates/rulesets/codex.md +2 -0
- package/neural-interface/templates/rulesets/coexistence.md +6 -0
- package/neural-interface/templates/rulesets/core.md +26 -0
- package/neural-interface/templates/rulesets/cursor.md +2 -0
- package/neural-interface/templates/rulesets/gemini.md +2 -0
- package/neural-interface/templates/rulesets/legacy-hashes.json +227 -0
- package/neural-interface/templates/rulesets/manifest.json +12 -0
- package/neural-interface/templates/rulesets/opencode.md +2 -0
- package/package.json +119 -53
- package/postinstall.js +125 -26
- package/preuninstall.js +187 -0
- package/scripts/audit-claude-hook-usage.mjs +12 -0
- package/scripts/audit-memory-duplicates.mjs +44 -0
- package/scripts/measure-claude-hook-overhead.mjs +77 -0
- package/scripts/recover-user-config.mjs +254 -0
- package/scripts/restore-data-home-from-backup.mjs +239 -0
- package/setup.js +804 -260
- package/skills/leonardo/SKILL.md +19 -10
- package/skills/leonardo/modules/image-prompter.md +18 -12
- package/skills/leonardo/modules/video-prompter.md +19 -13
- package/skills/synabun/SKILL.md +209 -185
- package/skills/synabun/icon.png +0 -0
- package/skills/synabun/modules/audit.md +380 -374
- package/skills/synabun/modules/changelog.md +15 -6
- package/skills/synabun/modules/idea.md +148 -144
- package/skills/synabun/modules/memorize.md +13 -4
- package/skills/synabun/modules/schedule.md +198 -0
- package/skills/synabun/synabunicon.png +0 -0
- package/updater.cmd +31 -0
- package/updater.mjs +382 -0
- package/updater.sh +29 -0
- package/hooks/claude-code/test-user-learning.mjs +0 -531
- package/neural-interface/public/claude-chat.css +0 -878
- package/neural-interface/public/claude-chat.html +0 -92
- package/neural-interface/public/claude-chat.js +0 -1168
- package/neural-interface/public/index-legacy.html +0 -14677
- package/neural-interface/public/index2d-legacy.html +0 -8193
- package/neural-interface/public/shared/html-term-renderer.js +0 -1022
- package/neural-interface/public/variant/3d/background.js +0 -884
- package/neural-interface/public/variant/3d/camera.js +0 -661
- package/neural-interface/public/variant/3d/gfx.js +0 -116
- package/neural-interface/public/variant/3d/graph.js +0 -2399
- package/neural-interface/public/variant/3d/settings-gfx.js +0 -228
- package/neural-interface/templates/CLAUDE-template.md +0 -211
|
@@ -0,0 +1,1688 @@
|
|
|
1
|
+
// ═══════════════════════════════════════════
|
|
2
|
+
// SynaBun — Assistant usage (exact token accounting)
|
|
3
|
+
// ═══════════════════════════════════════════
|
|
4
|
+
//
|
|
5
|
+
// A "task" is one human prompt in an Assistant session plus everything it
|
|
6
|
+
// causes: brain turns, dispatched worker runs (claude-code / codex / opencode),
|
|
7
|
+
// their sub-agents and helper calls. Every token of a task is counted once,
|
|
8
|
+
// normalized to five additive classes, and shown live.
|
|
9
|
+
//
|
|
10
|
+
// Pure module: Node built-ins only, no HTTP, no SDK. Four parts.
|
|
11
|
+
//
|
|
12
|
+
// 1. TOKENS { input, cacheWrite, cacheRead, output, reasoning }: non-negative
|
|
13
|
+
// integers that never overlap, so their sum is the total. The same meaning
|
|
14
|
+
// for every provider: `input` is uncached input only, `output` is visible
|
|
15
|
+
// output only. Views add the two sides people read: inputTotal (input +
|
|
16
|
+
// cacheWrite + cacheRead: everything sent to the model) and outputTotal
|
|
17
|
+
// (output + reasoning: everything it generated); total = inputTotal +
|
|
18
|
+
// outputTotal. Normalizers turn each provider's usage into that shape:
|
|
19
|
+
// claudeUsageTokens(usage) Anthropic usage (result.usage / message.usage)
|
|
20
|
+
// claudeModelUsageTokens(entry) one SDK ModelUsage entry
|
|
21
|
+
// codexUsageTokens(usage) → { tokens, mismatch } (Codex input includes cached, output includes reasoning)
|
|
22
|
+
// openCodeTokens(tokens) already five classes
|
|
23
|
+
//
|
|
24
|
+
// 2. METERS one per provider session. Each keeps a small JSON-safe state()
|
|
25
|
+
// the caller persists and hands back as { state }.
|
|
26
|
+
// createClaudeMeter onEvent(sdkMessage) → { settled, pendingChanged }. A `result` settles the
|
|
27
|
+
// per-model DELTA of the cumulative result.modelUsage (main loop, Task
|
|
28
|
+
// sub-agents, sidechains, compaction, helper models). settled.rows is ready
|
|
29
|
+
// for ledger.settle(). pending() is the live estimate between results.
|
|
30
|
+
// createCodexMeter poll() → the NEW usage since the last poll, read from the thread's rollout
|
|
31
|
+
// files (root + sub-agent threads), one row per thread and turn.
|
|
32
|
+
// poll({ discover: true }) discovers new files immediately at turn end.
|
|
33
|
+
// createOpenCodeMeter onEvent(type, event) → true when totals changed; total(), markTurn(),
|
|
34
|
+
// reconcile(). Counts every session of the run's own `opencode serve`.
|
|
35
|
+
//
|
|
36
|
+
// 3. LEDGER createUsageLedger({ dataDir }): one append-only file per assistant
|
|
37
|
+
// session, <dataDir>/assistant-usage/<sessionId>.jsonl, plus in-memory
|
|
38
|
+
// provisional ("pending") tokens for turns still running. Failed appends stay
|
|
39
|
+
// queued until the next append or flush(). Dropped sessions ignore late writes
|
|
40
|
+
// until beginTask() reopens them. taskView() is what the panel gauge renders:
|
|
41
|
+
// its `session` block (every task of the session added up, read back from the
|
|
42
|
+
// file after a restart) is the headline, its `task` the current task's detail.
|
|
43
|
+
//
|
|
44
|
+
// 4. HOW A CALLER WIRES IT (the contract the dispatcher, runtime, API and UI code against)
|
|
45
|
+
// human prompt ledger.beginTask(sessionId, { title }) → { id, n }
|
|
46
|
+
// run dispatched remember the task id on the run; pass it as taskId on every settle
|
|
47
|
+
// Claude SDK message const { settled, pendingChanged } = meter.onEvent(message)
|
|
48
|
+
// settled → for (const row of settled.rows) ledger.settle({ ...ids, ...row,
|
|
49
|
+
// fidelity: settled.fidelity, reason: settled.reason, source: settled.source })
|
|
50
|
+
// pendingChanged → ledger.setPending(sessionId, agentKey, { ...ids, tokens: meter.pending().tokens })
|
|
51
|
+
// Codex turn end / tick for (const row of meter.poll()) ledger.settle({ ...ids, part: row.part, tokens: row.tokens,
|
|
52
|
+
// model: row.model || runModel, fidelity: row.fidelity, reason: row.reason, source: row.source })
|
|
53
|
+
// (price row.usage, the raw provider keys, with assistant-budget's codexUsageCostUsd)
|
|
54
|
+
// OpenCode event if (meter.onEvent(type, event)) ledger.setPending(…, { tokens: live delta since the last mark })
|
|
55
|
+
// OpenCode turn end await meter.reconcile(fetchers); const d = meter.markTurn(); settle d.main as part 'main'
|
|
56
|
+
// and d.subagents as part 'subagents'
|
|
57
|
+
// agentKey is 'brain' for the brain and the runId for a run. settle() clears
|
|
58
|
+
// that agent's pending (its provisional tokens are booked now), except for
|
|
59
|
+
// part 'aux'. Persist meter.state() with the run / session and pass it back
|
|
60
|
+
// after a restart, otherwise a Claude meter cannot difference its first result.
|
|
61
|
+
//
|
|
62
|
+
// Why modelUsage and not result.usage (SDK 0.3.288): result.usage is the main
|
|
63
|
+
// agent loop only, per turn; result.modelUsage is cumulative for the query()
|
|
64
|
+
// call and covers everything. Measured on a 17-result brain session: 151.3M
|
|
65
|
+
// tokens by modelUsage deltas against 58.3M by result.usage.
|
|
66
|
+
// Why Codex rollout records and not turn.completed.usage: the latter is
|
|
67
|
+
// cumulative for a resumed thread (second turns were booked twice), and the
|
|
68
|
+
// per-response `token_usage_record` lines also include the compaction call.
|
|
69
|
+
|
|
70
|
+
import nodeFs from 'node:fs';
|
|
71
|
+
import { homedir } from 'node:os';
|
|
72
|
+
import { join } from 'node:path';
|
|
73
|
+
|
|
74
|
+
// ── Part 1: tokens ───────────────────────────────────────────────────────────
|
|
75
|
+
|
|
76
|
+
/** The five additive token classes, in the order ledger rows store them. */
|
|
77
|
+
export const TOKEN_KEYS = Object.freeze(['input', 'cacheWrite', 'cacheRead', 'output', 'reasoning']);
|
|
78
|
+
|
|
79
|
+
/** A non-negative integer, 0 for anything else. */
|
|
80
|
+
function count(value) {
|
|
81
|
+
const n = Number(value);
|
|
82
|
+
return Number.isFinite(n) && n > 0 ? Math.trunc(n) : 0;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/** A non-negative finite USD amount, 0 for anything else. */
|
|
86
|
+
function usd(value) {
|
|
87
|
+
const n = Number(value);
|
|
88
|
+
return Number.isFinite(n) && n > 0 ? n : 0;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const roundUsd = (value) => Number(usd(value).toFixed(8));
|
|
92
|
+
|
|
93
|
+
export function zeroTokens() {
|
|
94
|
+
return { input: 0, cacheWrite: 0, cacheRead: 0, output: 0, reasoning: 0 };
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/** Any token-ish value (object, ledger row array, null) as a clean five-class object. */
|
|
98
|
+
export function cleanTokens(value) {
|
|
99
|
+
if (Array.isArray(value)) return rowToTokens(value);
|
|
100
|
+
const t = value && typeof value === 'object' ? value : {};
|
|
101
|
+
return { input: count(t.input), cacheWrite: count(t.cacheWrite), cacheRead: count(t.cacheRead), output: count(t.output), reasoning: count(t.reasoning) };
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export function addTokens(a, b) {
|
|
105
|
+
const x = cleanTokens(a);
|
|
106
|
+
const y = cleanTokens(b);
|
|
107
|
+
for (const key of TOKEN_KEYS) x[key] += y[key];
|
|
108
|
+
return x;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/** a - b, each class clamped at 0. */
|
|
112
|
+
export function subTokens(a, b) {
|
|
113
|
+
const x = cleanTokens(a);
|
|
114
|
+
const y = cleanTokens(b);
|
|
115
|
+
for (const key of TOKEN_KEYS) x[key] = Math.max(0, x[key] - y[key]);
|
|
116
|
+
return x;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/** The per-class maximum of a and b (snapshots of one growing message). */
|
|
120
|
+
export function maxTokens(a, b) {
|
|
121
|
+
const x = cleanTokens(a);
|
|
122
|
+
const y = cleanTokens(b);
|
|
123
|
+
for (const key of TOKEN_KEYS) x[key] = Math.max(x[key], y[key]);
|
|
124
|
+
return x;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
export function totalTokens(tokens) {
|
|
128
|
+
const t = cleanTokens(tokens);
|
|
129
|
+
return t.input + t.cacheWrite + t.cacheRead + t.output + t.reasoning;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/** The five classes plus `total` and its two sides: inputTotal (input + cacheWrite + cacheRead) and outputTotal (output + reasoning). */
|
|
133
|
+
export function withTotal(tokens) {
|
|
134
|
+
const t = cleanTokens(tokens);
|
|
135
|
+
const inputTotal = t.input + t.cacheWrite + t.cacheRead;
|
|
136
|
+
const outputTotal = t.output + t.reasoning;
|
|
137
|
+
return { ...t, total: inputTotal + outputTotal, inputTotal, outputTotal };
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/** Tokens as the ledger's array: [input, cacheWrite, cacheRead, output, reasoning]. */
|
|
141
|
+
export function tokensToRow(tokens) {
|
|
142
|
+
const t = cleanTokens(tokens);
|
|
143
|
+
return TOKEN_KEYS.map((key) => t[key]);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
export function rowToTokens(row) {
|
|
147
|
+
const list = Array.isArray(row) ? row : [];
|
|
148
|
+
return { input: count(list[0]), cacheWrite: count(list[1]), cacheRead: count(list[2]), output: count(list[3]), reasoning: count(list[4]) };
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
const sameTokens = (a, b) => TOKEN_KEYS.every((key) => a[key] === b[key]);
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Anthropic usage (result.usage or message.usage). Thinking tokens are inside
|
|
155
|
+
* output_tokens, so they are split out. Every other key (iterations,
|
|
156
|
+
* server_tool_use, the cache_creation split, fallback credits) is ignored.
|
|
157
|
+
*/
|
|
158
|
+
export function claudeUsageTokens(usage) {
|
|
159
|
+
const u = usage && typeof usage === 'object' ? usage : {};
|
|
160
|
+
const out = count(u.output_tokens);
|
|
161
|
+
const reasoning = Math.min(out, count(u.output_tokens_details?.thinking_tokens));
|
|
162
|
+
return { input: count(u.input_tokens), cacheWrite: count(u.cache_creation_input_tokens), cacheRead: count(u.cache_read_input_tokens), output: out - reasoning, reasoning };
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/** One SDK ModelUsage entry ({ inputTokens, outputTokens, thinkingTokens?, cacheReadInputTokens, cacheCreationInputTokens }); thinkingTokens is already inside outputTokens. */
|
|
166
|
+
export function claudeModelUsageTokens(entry) {
|
|
167
|
+
const e = entry && typeof entry === 'object' ? entry : {};
|
|
168
|
+
const out = count(e.outputTokens);
|
|
169
|
+
const reasoning = Math.min(out, count(e.thinkingTokens));
|
|
170
|
+
return { input: count(e.inputTokens), cacheWrite: count(e.cacheCreationInputTokens), cacheRead: count(e.cacheReadInputTokens), output: out - reasoning, reasoning };
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/** The provider keys a Codex usage object carries, in snake_case. */
|
|
174
|
+
export const CODEX_USAGE_KEYS = Object.freeze(['input_tokens', 'cached_input_tokens', 'cache_write_input_tokens', 'output_tokens', 'reasoning_output_tokens', 'total_tokens']);
|
|
175
|
+
const CODEX_CAMEL = Object.freeze({ input_tokens: 'inputTokens', cached_input_tokens: 'cachedInputTokens', cache_write_input_tokens: 'cacheWriteInputTokens', output_tokens: 'outputTokens', reasoning_output_tokens: 'reasoningOutputTokens', total_tokens: 'totalTokens' });
|
|
176
|
+
|
|
177
|
+
/** Codex usage in snake_case whatever spelling it came in; total_tokens is null when the provider sent none. */
|
|
178
|
+
function codexRawUsage(usage) {
|
|
179
|
+
const u = usage && typeof usage === 'object' ? usage : {};
|
|
180
|
+
const raw = {};
|
|
181
|
+
for (const key of CODEX_USAGE_KEYS) raw[key] = count(u[key] ?? u[CODEX_CAMEL[key]]);
|
|
182
|
+
const total = u.total_tokens ?? u.totalTokens;
|
|
183
|
+
raw.total_tokens = Number.isFinite(Number(total)) && total !== null && total !== '' ? count(total) : null;
|
|
184
|
+
return raw;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* Codex usage, snake_case or camelCase. Codex input INCLUDES the cached and
|
|
189
|
+
* cache-write tokens and output INCLUDES reasoning (total = input + output), so
|
|
190
|
+
* both are taken apart. `mismatch` is true when the provider's own total is
|
|
191
|
+
* present and differs from the five classes' sum.
|
|
192
|
+
* @returns {{ tokens: object, mismatch: boolean }}
|
|
193
|
+
*/
|
|
194
|
+
export function codexUsageTokens(usage) {
|
|
195
|
+
const raw = codexRawUsage(usage);
|
|
196
|
+
const tokens = {
|
|
197
|
+
input: Math.max(0, raw.input_tokens - raw.cached_input_tokens - raw.cache_write_input_tokens),
|
|
198
|
+
cacheWrite: raw.cache_write_input_tokens,
|
|
199
|
+
cacheRead: raw.cached_input_tokens,
|
|
200
|
+
output: Math.max(0, raw.output_tokens - raw.reasoning_output_tokens),
|
|
201
|
+
reasoning: raw.reasoning_output_tokens,
|
|
202
|
+
};
|
|
203
|
+
return { tokens, mismatch: raw.total_tokens !== null && raw.total_tokens !== totalTokens(tokens) };
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/** OpenCode message tokens ({ input, output, reasoning, cache:{ read, write } }): already five additive classes. */
|
|
207
|
+
export function openCodeTokens(tokens) {
|
|
208
|
+
const t = tokens && typeof tokens === 'object' ? tokens : {};
|
|
209
|
+
return { input: count(t.input), cacheWrite: count(t.cache?.write), cacheRead: count(t.cache?.read), output: count(t.output), reasoning: count(t.reasoning) };
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// ── Part 2A: Claude meter ────────────────────────────────────────────────────
|
|
213
|
+
|
|
214
|
+
/** Counters that only grow inside one query(); a lower value means the counter restarted. thinkingTokens is left out: a resumed session may record it only partly. */
|
|
215
|
+
const CLAUDE_COUNTERS = Object.freeze(['inputTokens', 'outputTokens', 'cacheReadInputTokens', 'cacheCreationInputTokens']);
|
|
216
|
+
const CLAUDE_RESULT_MEMORY = 20;
|
|
217
|
+
/** `source` of a row counted from the stream because no result will count it (see createClaudeMeter). */
|
|
218
|
+
export const CLAUDE_ESTIMATE_SOURCE = 'claude-stream-estimate';
|
|
219
|
+
|
|
220
|
+
/** "claude-opus-5-5[1m]" → "claude-opus-5-5": modelUsage keys carry a variant suffix that message.model does not. */
|
|
221
|
+
export function baseModel(model) {
|
|
222
|
+
return String(model || '').replace(/\[[^\]]*\]$/, '').trim();
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/** result.modelUsage as { [model]: counters }, or null when it is missing or empty. */
|
|
226
|
+
function readModelUsage(modelUsage) {
|
|
227
|
+
if (!modelUsage || typeof modelUsage !== 'object' || Array.isArray(modelUsage)) return null;
|
|
228
|
+
const out = {};
|
|
229
|
+
for (const [model, entry] of Object.entries(modelUsage)) {
|
|
230
|
+
if (!entry || typeof entry !== 'object') continue;
|
|
231
|
+
out[model] = {
|
|
232
|
+
inputTokens: count(entry.inputTokens),
|
|
233
|
+
outputTokens: count(entry.outputTokens),
|
|
234
|
+
thinkingTokens: count(entry.thinkingTokens),
|
|
235
|
+
cacheReadInputTokens: count(entry.cacheReadInputTokens),
|
|
236
|
+
cacheCreationInputTokens: count(entry.cacheCreationInputTokens),
|
|
237
|
+
costUSD: usd(entry.costUSD),
|
|
238
|
+
};
|
|
239
|
+
}
|
|
240
|
+
return Object.keys(out).length ? out : null;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
const modelUsageIsZero = (current) => Object.values(current).every((c) => CLAUDE_COUNTERS.every((key) => c[key] === 0) && c.costUSD === 0);
|
|
244
|
+
|
|
245
|
+
/** The model that spent the most tokens in a { [model]: counters } map, or null. */
|
|
246
|
+
function topModel(current) {
|
|
247
|
+
let best = null;
|
|
248
|
+
let bestTotal = -1;
|
|
249
|
+
for (const [model, counters] of Object.entries(current || {})) {
|
|
250
|
+
const total = totalTokens(claudeModelUsageTokens(counters));
|
|
251
|
+
if (total > bestTotal) { best = model; bestTotal = total; }
|
|
252
|
+
}
|
|
253
|
+
return best;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
/**
|
|
257
|
+
* What ran outside the main loop: tokens minus main. Output and reasoning are
|
|
258
|
+
* differenced together first, so a turn whose two sources split thinking
|
|
259
|
+
* differently still gives total(tokens) - total(main).
|
|
260
|
+
*/
|
|
261
|
+
function outsideMain(tokens, main) {
|
|
262
|
+
const out = Math.max(0, tokens.output + tokens.reasoning - main.output - main.reasoning);
|
|
263
|
+
const reasoning = Math.min(out, Math.max(0, tokens.reasoning - main.reasoning));
|
|
264
|
+
return {
|
|
265
|
+
input: Math.max(0, tokens.input - main.input),
|
|
266
|
+
cacheWrite: Math.max(0, tokens.cacheWrite - main.cacheWrite),
|
|
267
|
+
cacheRead: Math.max(0, tokens.cacheRead - main.cacheRead),
|
|
268
|
+
output: out - reasoning,
|
|
269
|
+
reasoning,
|
|
270
|
+
};
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
/**
|
|
274
|
+
* Meter for ONE Claude provider session. Feed every Claude Agent SDK message
|
|
275
|
+
* in order with onEvent(message).
|
|
276
|
+
*
|
|
277
|
+
* onEvent → { settled, pendingChanged }
|
|
278
|
+
* settled is null except on a `result` that settles a turn:
|
|
279
|
+
* { byModel:[{ model, tokens, costUsd }], tokens, main, subagents, costUsd,
|
|
280
|
+
* fidelity:'exact'|'partial', reason, source, resultUuid, sessionId, rows }
|
|
281
|
+
* tokens everything the turn spent (the per-model delta of result.modelUsage)
|
|
282
|
+
* main claudeUsageTokens(result.usage): the main agent loop
|
|
283
|
+
* subagents tokens - main: Task sub-agents, sidechains, compaction, helper models
|
|
284
|
+
* costUsd sum of the models' costUSD deltas (null when it cannot be differenced)
|
|
285
|
+
* rows [{ model, part:'main'|'subagents'|'aux', tokens, costUsd }] summing exactly to
|
|
286
|
+
* `tokens`, ready for ledger.settle(). A model's cost rides on its first row (null
|
|
287
|
+
* on the other). 'aux' is a model that produced no assistant message in this
|
|
288
|
+
* turn (a helper call such as a title or summary).
|
|
289
|
+
* fidelity 'partial' reasons:
|
|
290
|
+
* 'no-baseline' the session had earlier turns and no snapshot: only `main` is known
|
|
291
|
+
* 'no-model-usage' a result without modelUsage (old CLI): only `main` is known
|
|
292
|
+
* 'counter-mismatch' the main loop reported more than the delta, so the counter restarted
|
|
293
|
+
* unseen; the turn is raised to at least `main`
|
|
294
|
+
* A duplicate result (same uuid as one of the last 20) and an error result
|
|
295
|
+
* whose modelUsage is missing or all zero settle nothing and leave the snapshot alone.
|
|
296
|
+
* pendingChanged is true when pending() changed (a result clears it).
|
|
297
|
+
* A failed result (the turn was interrupted) can leave a main-loop call out of its
|
|
298
|
+
* totals: the call was cut off mid-answer, the CLI never counts it, the provider billed
|
|
299
|
+
* it. Such calls are added to `rows` as estimates, each with its own { fidelity:'partial',
|
|
300
|
+
* reason:'interrupted', source:'claude-stream-estimate', costUsd:null }, and summed in
|
|
301
|
+
* `estimated`. Exact rows carry no such fields: the settled turn's own apply to them.
|
|
302
|
+
*
|
|
303
|
+
* settlePending({ reason }) → rows of the same estimate shape for everything pending
|
|
304
|
+
* (reason 'no-result' by default), for when no result will come at all: the turn was
|
|
305
|
+
* aborted, the process is going. What they count is taken off the next result's delta
|
|
306
|
+
* should one arrive after all, so a call the CLI did count is never booked twice.
|
|
307
|
+
*
|
|
308
|
+
* pending() → { tokens, byModel:[{ model, tokens }] }: the live estimate since
|
|
309
|
+
* the last result, from `assistant` messages (sub-agent calls included) and
|
|
310
|
+
* `stream_event` message_start / message_delta. Never settle it. A background
|
|
311
|
+
* sub-agent can still report after a result; that is settled by the NEXT
|
|
312
|
+
* result, so when no further turn will come call clearPending().
|
|
313
|
+
* expectHistory() the session has earlier turns this meter never saw.
|
|
314
|
+
* reset() the caller started a fresh, non-resumed process: forget the snapshot.
|
|
315
|
+
* state() { snapshot, unbaselined, results, history, sessionId } (JSON-safe).
|
|
316
|
+
*/
|
|
317
|
+
export function createClaudeMeter({ state } = {}) {
|
|
318
|
+
let snapshot = readModelUsage(state?.snapshot);
|
|
319
|
+
let results = Array.isArray(state?.results) ? state.results.filter((id) => typeof id === 'string').slice(-CLAUDE_RESULT_MEMORY) : [];
|
|
320
|
+
let history = state?.history === true;
|
|
321
|
+
let sessionId = typeof state?.sessionId === 'string' ? state.sessionId : null;
|
|
322
|
+
let unbaselined = cleanTokens(state?.unbaselined);
|
|
323
|
+
|
|
324
|
+
// Live estimate: message id → the highest counters seen for that API call.
|
|
325
|
+
const calls = new Map();
|
|
326
|
+
const openCall = new Map(); // parent_tool_use_id ('' for main) → message ids awaiting message_stop
|
|
327
|
+
let pendingTokens = zeroTokens();
|
|
328
|
+
let mainModel = null; // model of the main loop's last assistant message
|
|
329
|
+
let subModels = new Set(); // models seen under a parent_tool_use_id since the last result
|
|
330
|
+
|
|
331
|
+
const callTokens = (call) => {
|
|
332
|
+
const reasoning = Math.min(call.out, call.thinking);
|
|
333
|
+
return { input: call.input, cacheWrite: call.cacheWrite, cacheRead: call.cacheRead, output: call.out - reasoning, reasoning };
|
|
334
|
+
};
|
|
335
|
+
|
|
336
|
+
/** Raise one API call's counters to what this usage object says; true when the estimate grew. */
|
|
337
|
+
function noteCall(id, model, usage, main = true) {
|
|
338
|
+
if (!id || !usage || typeof usage !== 'object') return false;
|
|
339
|
+
const call = calls.get(id) || { model: null, main, input: 0, cacheWrite: 0, cacheRead: 0, out: 0, thinking: 0 };
|
|
340
|
+
const before = callTokens(call);
|
|
341
|
+
if (!call.model && model) call.model = String(model);
|
|
342
|
+
call.input = Math.max(call.input, count(usage.input_tokens));
|
|
343
|
+
call.cacheWrite = Math.max(call.cacheWrite, count(usage.cache_creation_input_tokens));
|
|
344
|
+
call.cacheRead = Math.max(call.cacheRead, count(usage.cache_read_input_tokens));
|
|
345
|
+
call.out = Math.max(call.out, count(usage.output_tokens));
|
|
346
|
+
call.thinking = Math.max(call.thinking, count(usage.output_tokens_details?.thinking_tokens));
|
|
347
|
+
calls.set(id, call);
|
|
348
|
+
const after = callTokens(call);
|
|
349
|
+
if (sameTokens(before, after)) return false;
|
|
350
|
+
pendingTokens = addTokens(subTokens(pendingTokens, before), after);
|
|
351
|
+
return true;
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
function clearPending() {
|
|
355
|
+
const had = calls.size > 0 && totalTokens(pendingTokens) > 0;
|
|
356
|
+
calls.clear();
|
|
357
|
+
openCall.clear();
|
|
358
|
+
pendingTokens = zeroTokens();
|
|
359
|
+
return had;
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
function pending() {
|
|
363
|
+
const byModel = new Map();
|
|
364
|
+
for (const call of calls.values()) {
|
|
365
|
+
const model = call.model || 'unknown';
|
|
366
|
+
byModel.set(model, addTokens(byModel.get(model), callTokens(call)));
|
|
367
|
+
}
|
|
368
|
+
return {
|
|
369
|
+
tokens: { ...pendingTokens },
|
|
370
|
+
byModel: [...byModel].filter(([, tokens]) => totalTokens(tokens) > 0).map(([model, tokens]) => ({ model, tokens })),
|
|
371
|
+
};
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
function onAssistant(message) {
|
|
375
|
+
const inner = message.message;
|
|
376
|
+
if (!inner || typeof inner !== 'object') return false;
|
|
377
|
+
const model = inner.model && inner.model !== '<synthetic>' ? String(inner.model) : null;
|
|
378
|
+
if (model) {
|
|
379
|
+
if (message.parent_tool_use_id) subModels.add(baseModel(model));
|
|
380
|
+
else mainModel = baseModel(model);
|
|
381
|
+
}
|
|
382
|
+
return noteCall(inner.id, model, inner.usage, !message.parent_tool_use_id);
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
function onStreamEvent(message) {
|
|
386
|
+
const event = message.event;
|
|
387
|
+
if (!event || typeof event !== 'object') return false;
|
|
388
|
+
const parent = message.parent_tool_use_id || '';
|
|
389
|
+
if (event.type === 'message_start' && event.message?.id) {
|
|
390
|
+
// A new start can arrive before the old message stops. Its deltas wait their turn.
|
|
391
|
+
const queue = openCall.get(parent) || [];
|
|
392
|
+
queue.push(event.message.id);
|
|
393
|
+
openCall.set(parent, queue);
|
|
394
|
+
return noteCall(event.message.id, event.message.model, event.message.usage, !parent);
|
|
395
|
+
}
|
|
396
|
+
// output_tokens here is cumulative for the message open under this parent.
|
|
397
|
+
if (event.type === 'message_delta') return noteCall(openCall.get(parent)?.[0], null, event.usage, !parent);
|
|
398
|
+
if (event.type === 'message_stop') {
|
|
399
|
+
const queue = openCall.get(parent);
|
|
400
|
+
queue?.shift();
|
|
401
|
+
if (!queue?.length) openCall.delete(parent);
|
|
402
|
+
}
|
|
403
|
+
return false;
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
/** Split a settled turn into ledger rows that sum exactly to its tokens. */
|
|
407
|
+
function splitRows(byModel, subagents) {
|
|
408
|
+
if (!byModel.length) return [];
|
|
409
|
+
const wanted = byModel.find((entry) => mainModel && baseModel(entry.model) === mainModel);
|
|
410
|
+
const mainEntry = wanted || byModel.reduce((best, entry) => (totalTokens(entry.tokens) > totalTokens(best.tokens) ? entry : best), byModel[0]);
|
|
411
|
+
const others = [];
|
|
412
|
+
let rest = cleanTokens(subagents);
|
|
413
|
+
for (const entry of byModel) {
|
|
414
|
+
if (entry === mainEntry) continue;
|
|
415
|
+
others.push({ model: entry.model, part: subModels.has(baseModel(entry.model)) ? 'subagents' : 'aux', tokens: entry.tokens, costUsd: entry.costUsd });
|
|
416
|
+
rest = subTokens(rest, entry.tokens);
|
|
417
|
+
}
|
|
418
|
+
// What is left of the outside-the-main-loop tokens ran on the main model (sub-agents on the same model).
|
|
419
|
+
const share = zeroTokens();
|
|
420
|
+
for (const key of TOKEN_KEYS) share[key] = Math.min(rest[key], mainEntry.tokens[key]);
|
|
421
|
+
const rows = [{ model: mainEntry.model, part: 'main', tokens: subTokens(mainEntry.tokens, share), costUsd: mainEntry.costUsd }];
|
|
422
|
+
if (totalTokens(share) > 0) rows.push({ model: mainEntry.model, part: 'subagents', tokens: share, costUsd: null });
|
|
423
|
+
return [...rows, ...others].filter((row) => totalTokens(row.tokens) > 0 || row.costUsd > 0);
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
const remember = (uuid) => { if (uuid) results = [...results, uuid].slice(-CLAUDE_RESULT_MEMORY); };
|
|
427
|
+
|
|
428
|
+
/** A stream model name as result.modelUsage spells it ("claude-opus-5-5" → "claude-opus-5-5[1m]"), so its rows join that model's. */
|
|
429
|
+
const usageModelName = (model) => Object.keys(snapshot || {}).find((key) => baseModel(key) === baseModel(model)) || String(model || 'unknown');
|
|
430
|
+
|
|
431
|
+
/** The pending calls `only` keeps, as estimate rows: one per model and part. */
|
|
432
|
+
function estimateRows(reason, only = () => true) {
|
|
433
|
+
const groups = new Map();
|
|
434
|
+
for (const call of calls.values()) {
|
|
435
|
+
if (!only(call)) continue;
|
|
436
|
+
const model = usageModelName(call.model);
|
|
437
|
+
const part = call.main ? 'main' : 'subagents';
|
|
438
|
+
const key = part + '\n' + model;
|
|
439
|
+
const group = groups.get(key) || { model, part, tokens: zeroTokens(), costUsd: null, fidelity: 'partial', reason, source: CLAUDE_ESTIMATE_SOURCE };
|
|
440
|
+
group.tokens = addTokens(group.tokens, callTokens(call));
|
|
441
|
+
groups.set(key, group);
|
|
442
|
+
}
|
|
443
|
+
return [...groups.values()].filter((row) => totalTokens(row.tokens) > 0);
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
/**
|
|
447
|
+
* Main-loop calls the stream showed and a failed result does not count: the turn was cut off
|
|
448
|
+
* while they ran. The CLI leaves them out of its totals for good; the provider billed them.
|
|
449
|
+
* Sub-agent calls are not judged here: a background one can still finish and be counted.
|
|
450
|
+
*/
|
|
451
|
+
function lostCalls(byModel) {
|
|
452
|
+
const rows = [];
|
|
453
|
+
for (const row of estimateRows('interrupted', (call) => call.main)) {
|
|
454
|
+
const counted = byModel.filter((entry) => baseModel(entry.model) === baseModel(row.model)).reduce((sum, entry) => addTokens(sum, entry.tokens), zeroTokens());
|
|
455
|
+
const missing = outsideMain(row.tokens, counted);
|
|
456
|
+
if (totalTokens(missing) > 0) rows.push({ ...row, tokens: missing });
|
|
457
|
+
}
|
|
458
|
+
return rows;
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
function mainOnly({ current, main, uuid, reason }) {
|
|
462
|
+
const model = topModel(current) || (mainModel || 'unknown');
|
|
463
|
+
return {
|
|
464
|
+
byModel: [{ model, tokens: main, costUsd: null }],
|
|
465
|
+
tokens: main, main, subagents: zeroTokens(), costUsd: null,
|
|
466
|
+
fidelity: 'partial', reason, source: 'claude-result-usage', resultUuid: uuid, sessionId,
|
|
467
|
+
rows: totalTokens(main) > 0 ? [{ model, part: 'main', tokens: main, costUsd: null }] : [],
|
|
468
|
+
estimated: zeroTokens(),
|
|
469
|
+
};
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
function onResult(message) {
|
|
473
|
+
const uuid = typeof message.uuid === 'string' && message.uuid ? message.uuid : null;
|
|
474
|
+
if (uuid && results.includes(uuid)) return null;
|
|
475
|
+
const current = readModelUsage(message.modelUsage);
|
|
476
|
+
const failed = message.is_error === true || String(message.subtype || '').startsWith('error');
|
|
477
|
+
// A crash or startup-error result carries zeroed counters: it says nothing about the session's totals.
|
|
478
|
+
if (failed && (!current || modelUsageIsZero(current))) return null;
|
|
479
|
+
const main = claudeUsageTokens(message.usage);
|
|
480
|
+
remember(uuid);
|
|
481
|
+
if (!current) {
|
|
482
|
+
unbaselined = addTokens(unbaselined, main);
|
|
483
|
+
return mainOnly({ current: null, main, uuid, reason: 'no-model-usage' });
|
|
484
|
+
}
|
|
485
|
+
if (!snapshot && history) {
|
|
486
|
+
snapshot = current;
|
|
487
|
+
unbaselined = zeroTokens();
|
|
488
|
+
return mainOnly({ current, main, uuid, reason: 'no-baseline' });
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
const previous = snapshot || {};
|
|
492
|
+
// CLI process counters reset together, including models absent from this result.
|
|
493
|
+
const restarted = Object.entries(current).some(([model, now]) => previous[model] && CLAUDE_COUNTERS.some((key) => now[key] < previous[model][key]));
|
|
494
|
+
let byModel = [];
|
|
495
|
+
for (const [model, now] of Object.entries(current)) {
|
|
496
|
+
const before = previous[model];
|
|
497
|
+
let delta = now;
|
|
498
|
+
if (!restarted && before) {
|
|
499
|
+
delta = { costUSD: Math.max(0, now.costUSD - before.costUSD), thinkingTokens: Math.max(0, now.thinkingTokens - before.thinkingTokens) };
|
|
500
|
+
for (const key of CLAUDE_COUNTERS) delta[key] = now[key] - before[key];
|
|
501
|
+
if (now.thinkingTokens < before.thinkingTokens) {
|
|
502
|
+
// Thinking is inside output; use this turn's count when that counter reset alone.
|
|
503
|
+
delta.thinkingTokens = Math.min(delta.outputTokens, count(message.usage?.output_tokens_details?.thinking_tokens));
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
const tokens = claudeModelUsageTokens(delta);
|
|
507
|
+
if (totalTokens(tokens) > 0 || delta.costUSD > 0) byModel.push({ model, tokens, costUsd: roundUsd(delta.costUSD) });
|
|
508
|
+
}
|
|
509
|
+
// After a restart the old counters of models that did not come back are gone too.
|
|
510
|
+
snapshot = restarted ? current : { ...previous, ...current };
|
|
511
|
+
let tokens = byModel.reduce((sum, entry) => addTokens(sum, entry.tokens), zeroTokens());
|
|
512
|
+
let fidelity = 'exact';
|
|
513
|
+
let reason = null;
|
|
514
|
+
// Checked on the delta as the CLI reported it, before anything already booked is taken off it.
|
|
515
|
+
const short = outsideMain(main, tokens);
|
|
516
|
+
if (totalTokens(short) > 0) {
|
|
517
|
+
// The main loop alone reported more than every model together: the counter restarted without
|
|
518
|
+
// dropping below the snapshot. Count at least the main loop and flag the turn.
|
|
519
|
+
const model = topModel(current) || 'unknown';
|
|
520
|
+
const entry = byModel.find((item) => item.model === model);
|
|
521
|
+
if (entry) entry.tokens = addTokens(entry.tokens, short);
|
|
522
|
+
else byModel = [...byModel, { model, tokens: short, costUsd: 0 }];
|
|
523
|
+
tokens = addTokens(tokens, short);
|
|
524
|
+
fidelity = 'partial';
|
|
525
|
+
reason = 'counter-mismatch';
|
|
526
|
+
}
|
|
527
|
+
// What was already booked and is contained in this cumulative delta is removed once, class by
|
|
528
|
+
// class: a prior result without modelUsage (booked from result.usage), or calls booked as an
|
|
529
|
+
// estimate when no result was expected (settlePending) that this late result counts after all.
|
|
530
|
+
if (!restarted && totalTokens(unbaselined)) {
|
|
531
|
+
let remaining = unbaselined;
|
|
532
|
+
const preferred = mainModel || baseModel(topModel(current));
|
|
533
|
+
for (const entry of [...byModel].sort((a, b) => Number(baseModel(b.model) === preferred) - Number(baseModel(a.model) === preferred))) {
|
|
534
|
+
const deducted = zeroTokens();
|
|
535
|
+
for (const key of TOKEN_KEYS) deducted[key] = Math.min(entry.tokens[key], remaining[key]);
|
|
536
|
+
entry.tokens = subTokens(entry.tokens, deducted);
|
|
537
|
+
remaining = subTokens(remaining, deducted);
|
|
538
|
+
}
|
|
539
|
+
tokens = byModel.reduce((sum, entry) => addTokens(sum, entry.tokens), zeroTokens());
|
|
540
|
+
}
|
|
541
|
+
unbaselined = zeroTokens();
|
|
542
|
+
|
|
543
|
+
const subagents = outsideMain(tokens, main);
|
|
544
|
+
const lost = failed ? lostCalls(byModel) : [];
|
|
545
|
+
return {
|
|
546
|
+
byModel, tokens, main, subagents,
|
|
547
|
+
costUsd: roundUsd(byModel.reduce((sum, entry) => sum + entry.costUsd, 0)),
|
|
548
|
+
fidelity, reason, source: 'claude-model-usage', resultUuid: uuid, sessionId,
|
|
549
|
+
rows: [...splitRows(byModel, subagents), ...lost],
|
|
550
|
+
estimated: lost.reduce((sum, row) => addTokens(sum, row.tokens), zeroTokens()),
|
|
551
|
+
};
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
function onEvent(message) {
|
|
555
|
+
if (!message || typeof message !== 'object') return { settled: null, pendingChanged: false };
|
|
556
|
+
if (typeof message.session_id === 'string' && message.session_id) sessionId = message.session_id;
|
|
557
|
+
if (message.type === 'assistant') return { settled: null, pendingChanged: onAssistant(message) };
|
|
558
|
+
if (message.type === 'stream_event') return { settled: null, pendingChanged: onStreamEvent(message) };
|
|
559
|
+
if (message.type !== 'result') return { settled: null, pendingChanged: false };
|
|
560
|
+
const settled = onResult(message);
|
|
561
|
+
if (!settled) return { settled: null, pendingChanged: false };
|
|
562
|
+
const pendingChanged = clearPending();
|
|
563
|
+
subModels = new Set();
|
|
564
|
+
return { settled, pendingChanged };
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
return {
|
|
568
|
+
onEvent,
|
|
569
|
+
pending,
|
|
570
|
+
/** Drop the live estimate (a later result settles those calls); true when it was not empty. */
|
|
571
|
+
clearPending,
|
|
572
|
+
settlePending({ reason = 'no-result' } = {}) {
|
|
573
|
+
const rows = estimateRows(reason);
|
|
574
|
+
if (rows.length) unbaselined = addTokens(unbaselined, pendingTokens);
|
|
575
|
+
clearPending();
|
|
576
|
+
return rows;
|
|
577
|
+
},
|
|
578
|
+
expectHistory() { history = true; },
|
|
579
|
+
reset() { snapshot = null; unbaselined = zeroTokens(); history = false; clearPending(); },
|
|
580
|
+
state: () => ({ snapshot: snapshot ? JSON.parse(JSON.stringify(snapshot)) : null, unbaselined: { ...unbaselined }, results: [...results], history, sessionId }),
|
|
581
|
+
};
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
// ── Part 2B: Codex meter ─────────────────────────────────────────────────────
|
|
585
|
+
|
|
586
|
+
const DAY_MS = 86_400_000;
|
|
587
|
+
const CODEX_SEEN_MAX = 64;
|
|
588
|
+
const DISCOVER_EVERY_MS = 15_000;
|
|
589
|
+
const READ_CHUNK = 1024 * 1024;
|
|
590
|
+
const FIRST_LINE_MAX = 1024 * 1024;
|
|
591
|
+
const NEWLINE = 10;
|
|
592
|
+
/** A line longer than this is only parsed when its head names a type the meter reads. */
|
|
593
|
+
const SHORT_LINE = 16 * 1024;
|
|
594
|
+
const HEAD_TYPE = /"type"\s*:\s*"(token_usage_record|event_msg|turn_context)"/;
|
|
595
|
+
|
|
596
|
+
/** Where the Codex CLI keeps its sessions: $CODEX_HOME, else ~/.codex. */
|
|
597
|
+
export function defaultCodexHome(env = process.env) {
|
|
598
|
+
return env.CODEX_HOME || join(homedir(), '.codex');
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
/** Rollout folders are named by LOCAL date: YYYY/MM/DD. */
|
|
602
|
+
function localDay(ms) {
|
|
603
|
+
const d = new Date(ms);
|
|
604
|
+
return `${d.getFullYear()}/${String(d.getMonth() + 1).padStart(2, '0')}/${String(d.getDate()).padStart(2, '0')}`;
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
function zeroCodexUsage() {
|
|
608
|
+
const usage = {};
|
|
609
|
+
for (const key of CODEX_USAGE_KEYS) usage[key] = 0;
|
|
610
|
+
return usage;
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
/** Cheap test before JSON.parse: rollouts hold huge tool-output lines the meter never needs. */
|
|
614
|
+
function meterReadsLine(line) {
|
|
615
|
+
if (line.length <= SHORT_LINE) return line.includes('"token_usage_record"') || line.includes('"token_count"') || line.includes('"turn_context"');
|
|
616
|
+
const head = HEAD_TYPE.exec(line.slice(0, 512));
|
|
617
|
+
return !!head && (head[1] !== 'event_msg' || line.includes('"token_count"'));
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
/**
|
|
621
|
+
* Meter for ONE Codex thread and its sub-agent threads, read from the rollout
|
|
622
|
+
* files <codexHome>/sessions/YYYY/MM/DD/rollout-<timestamp>-<threadId>.jsonl.
|
|
623
|
+
*
|
|
624
|
+
* poll({ discover }?) → the NEW usage since the previous poll (synchronous; [] when nothing is new):
|
|
625
|
+
* [{ threadId, part:'main'|'subagents', turnId, model, tokens, usage, responses, mismatches,
|
|
626
|
+
* fidelity, reason, source }], one row per thread, turn and source.
|
|
627
|
+
* tokens five classes (codexUsageTokens of every counted response, summed)
|
|
628
|
+
* usage the raw provider keys summed (input_tokens includes cached), for list pricing
|
|
629
|
+
* model the thread's model from its last `turn_context` line, or null
|
|
630
|
+
* responses model responses counted (the compaction call is one of them)
|
|
631
|
+
* mismatches responses whose own total_tokens differed from their classes' sum
|
|
632
|
+
* Normal case (codex-cli ≥ 0.153): one `token_usage_record` line per model response; fidelity
|
|
633
|
+
* 'exact', source 'codex-records'. A record counts iff its payload.thread_id is the file's own
|
|
634
|
+
* thread (an old CLI replays the parent's history inside a forked child), its timestamp is
|
|
635
|
+
* >= sinceMs, and lies above that file's timestamp/response-id high-water mark.
|
|
636
|
+
* Older CLI (a rollout with no such line): each `token_count` event's info.last_token_usage,
|
|
637
|
+
* fidelity 'partial', reason 'codex-no-records', source 'codex-token-count'. Codex repeats that
|
|
638
|
+
* event, so one whose running total did not move is skipped.
|
|
639
|
+
* Sub-agent threads are separate rollout files whose first line is a session_meta with
|
|
640
|
+
* payload.session_id === threadId and another id; they are looked for in the day folders from
|
|
641
|
+
* sinceMs to now (local dates, one day of slack). The root is also looked for by name in older
|
|
642
|
+
* folders, since a thread that began before sinceMs keeps appending to its first file.
|
|
643
|
+
* With `longPromptTokens` (a size, or model → size) a response whose prompt (input_tokens) is
|
|
644
|
+
* over that size goes to a row of its own with long:true, the others to long:false: OpenAI
|
|
645
|
+
* prices such a request at its long-context rates, whole. Without it (or for a model with no
|
|
646
|
+
* such size) rows carry no `long`.
|
|
647
|
+
* A missing rollout gives [] and is looked for again on the next poll.
|
|
648
|
+
* Tracked files are tailed on every poll. New-file discovery runs every 15 s after the root
|
|
649
|
+
* is found, or immediately with poll({ discover: true }) (useful when a turn closes).
|
|
650
|
+
*
|
|
651
|
+
* state() → { files:{ [path]:{ offset, inode, threadId, records, tc, model, turn, mark } }, seen:[responseId…] }
|
|
652
|
+
* (records = the file has token_usage_record lines, tc = the last token_count running total).
|
|
653
|
+
* rollouts() → [{ path, threadId, part }] found so far.
|
|
654
|
+
*
|
|
655
|
+
* @param {object} options
|
|
656
|
+
* @param {string} [options.codexHome] defaults to $CODEX_HOME, else ~/.codex
|
|
657
|
+
* @param {string} options.threadId the root thread (the run's providerThreadId)
|
|
658
|
+
* @param {number} [options.sinceMs] ignore usage older than this (0 = count the whole thread)
|
|
659
|
+
* @param {number|((model: string|null) => number)} [options.longPromptTokens] split rows at this prompt size (0 = no split)
|
|
660
|
+
* @param {object} [options.state] a previous state()
|
|
661
|
+
* @param {object} [options.fs] node:fs-like (readdirSync, statSync, openSync, readSync, closeSync)
|
|
662
|
+
* @param {() => number} [options.now]
|
|
663
|
+
*/
|
|
664
|
+
export function createCodexMeter({ codexHome = defaultCodexHome(), threadId, sinceMs = 0, longPromptTokens = 0, state, fs = nodeFs, now = Date.now } = {}) {
|
|
665
|
+
const root = String(threadId || '');
|
|
666
|
+
const since = Number(sinceMs) > 0 ? Number(sinceMs) : 0;
|
|
667
|
+
const longSizeOf = (model) => {
|
|
668
|
+
let size = 0;
|
|
669
|
+
try { size = Number(typeof longPromptTokens === 'function' ? longPromptTokens(model) : longPromptTokens); } catch { size = 0; }
|
|
670
|
+
return size > 0 ? size : 0;
|
|
671
|
+
};
|
|
672
|
+
const files = new Map(); // path → offset, identity, thread, fallback total and response high-water mark
|
|
673
|
+
const notOurs = new Set(); // rollouts of other threads (memory only)
|
|
674
|
+
let seenList = Array.isArray(state?.seen) ? state.seen.filter((id) => typeof id === 'string').slice(-CODEX_SEEN_MAX) : [];
|
|
675
|
+
const seen = new Set(seenList);
|
|
676
|
+
for (const [path, file] of Object.entries(state?.files && typeof state.files === 'object' ? state.files : {})) {
|
|
677
|
+
if (!file || typeof file.threadId !== 'string') continue;
|
|
678
|
+
files.set(path, {
|
|
679
|
+
offset: count(file.offset), threadId: file.threadId, records: file.records === true,
|
|
680
|
+
tc: Number.isFinite(file.tc) ? file.tc : null, model: file.model || null, turn: file.turn || null,
|
|
681
|
+
inode: file.inode ?? null, mark: { at: String(file.mark?.at || ''), ids: Array.isArray(file.mark?.ids) ? file.mark.ids.filter((id) => typeof id === 'string') : [] },
|
|
682
|
+
});
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
const list = (dir) => { try { return fs.readdirSync(dir); } catch { return []; } };
|
|
686
|
+
|
|
687
|
+
/** The day folders from `fromMs` to now (one day of slack on both ends); every folder when fromMs is 0. */
|
|
688
|
+
function dayDirs(fromMs) {
|
|
689
|
+
const lo = fromMs ? localDay(fromMs - DAY_MS) : '0000/00/00';
|
|
690
|
+
const hi = localDay(now() + DAY_MS);
|
|
691
|
+
const base = join(codexHome, 'sessions');
|
|
692
|
+
const out = [];
|
|
693
|
+
for (const year of list(base)) {
|
|
694
|
+
if (!/^\d{4}$/.test(year) || year < lo.slice(0, 4) || year > hi.slice(0, 4)) continue;
|
|
695
|
+
for (const month of list(join(base, year))) {
|
|
696
|
+
const ym = `${year}/${month}`;
|
|
697
|
+
if (!/^\d{2}$/.test(month) || ym < lo.slice(0, 7) || ym > hi.slice(0, 7)) continue;
|
|
698
|
+
for (const day of list(join(base, year, month))) {
|
|
699
|
+
const ymd = `${ym}/${day}`;
|
|
700
|
+
if (/^\d{2}$/.test(day) && ymd >= lo && ymd <= hi) out.push(join(base, year, month, day));
|
|
701
|
+
}
|
|
702
|
+
}
|
|
703
|
+
}
|
|
704
|
+
return out.sort();
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
/** The file's first complete line, null while it has none yet, '' when it is too long to be a session_meta. */
|
|
708
|
+
function firstLine(path) {
|
|
709
|
+
let fd;
|
|
710
|
+
try { fd = fs.openSync(path, 'r'); } catch { return null; }
|
|
711
|
+
try {
|
|
712
|
+
const parts = [];
|
|
713
|
+
let position = 0;
|
|
714
|
+
while (position < FIRST_LINE_MAX) {
|
|
715
|
+
const chunk = Buffer.alloc(64 * 1024);
|
|
716
|
+
const read = fs.readSync(fd, chunk, 0, chunk.length, position);
|
|
717
|
+
if (read <= 0) return null;
|
|
718
|
+
const end = chunk.subarray(0, read).indexOf(NEWLINE);
|
|
719
|
+
if (end !== -1) { parts.push(chunk.subarray(0, end)); return Buffer.concat(parts).toString('utf8'); }
|
|
720
|
+
parts.push(chunk.subarray(0, read));
|
|
721
|
+
position += read;
|
|
722
|
+
}
|
|
723
|
+
return '';
|
|
724
|
+
} catch { return null; } finally { try { fs.closeSync(fd); } catch { /* already closed */ } }
|
|
725
|
+
}
|
|
726
|
+
|
|
727
|
+
/** The sub-agent thread id when this rollout belongs to the root thread's family, false when not, null when not known yet. */
|
|
728
|
+
function childThread(path) {
|
|
729
|
+
const line = firstLine(path);
|
|
730
|
+
if (line === null) return null;
|
|
731
|
+
let entry = null;
|
|
732
|
+
try { entry = JSON.parse(line); } catch { return false; }
|
|
733
|
+
const payload = entry?.type === 'session_meta' ? entry.payload : null;
|
|
734
|
+
if (!payload || payload.session_id !== root || typeof payload.id !== 'string' || payload.id === root) return false;
|
|
735
|
+
return payload.id;
|
|
736
|
+
}
|
|
737
|
+
|
|
738
|
+
const track = (path, thread) => files.set(path, { offset: 0, inode: null, threadId: thread, records: false, tc: null, model: null, turn: null, mark: { at: '', ids: [] } });
|
|
739
|
+
const hasRoot = () => [...files.values()].some((file) => file.threadId === root);
|
|
740
|
+
let discoveredAt = -Infinity;
|
|
741
|
+
|
|
742
|
+
/** Find the root rollout and sub-agent rollouts not tracked yet. */
|
|
743
|
+
function discover() {
|
|
744
|
+
if (!root) return;
|
|
745
|
+
const suffix = `-${root}.jsonl`;
|
|
746
|
+
for (const dir of dayDirs(since)) {
|
|
747
|
+
for (const name of list(dir)) {
|
|
748
|
+
if (!name.startsWith('rollout-') || !name.endsWith('.jsonl')) continue;
|
|
749
|
+
const path = join(dir, name);
|
|
750
|
+
if (files.has(path) || notOurs.has(path)) continue;
|
|
751
|
+
const thread = name.endsWith(suffix) ? root : childThread(path);
|
|
752
|
+
if (thread === null) continue; // still being created: ask again next poll
|
|
753
|
+
if (thread === false) { notOurs.add(path); continue; }
|
|
754
|
+
track(path, thread);
|
|
755
|
+
}
|
|
756
|
+
}
|
|
757
|
+
// A thread older than sinceMs keeps appending to the rollout in the folder of the day it began:
|
|
758
|
+
// look for the root by name in every folder while it is missing.
|
|
759
|
+
if (!since || hasRoot()) return;
|
|
760
|
+
for (const dir of dayDirs(0)) {
|
|
761
|
+
for (const name of list(dir)) {
|
|
762
|
+
if (name.startsWith('rollout-') && name.endsWith(suffix) && !files.has(join(dir, name))) track(join(dir, name), root);
|
|
763
|
+
}
|
|
764
|
+
}
|
|
765
|
+
}
|
|
766
|
+
|
|
767
|
+
/** Call onLine for every complete line from `offset`; returns the offset after the last complete line. */
|
|
768
|
+
function readLines(path, offset, size, onLine) {
|
|
769
|
+
let fd;
|
|
770
|
+
try { fd = fs.openSync(path, 'r'); } catch { return offset; }
|
|
771
|
+
let consumed = offset;
|
|
772
|
+
try {
|
|
773
|
+
const chunk = Buffer.alloc(Math.min(READ_CHUNK, Math.max(1, size - offset)));
|
|
774
|
+
let carry = null;
|
|
775
|
+
let position = offset;
|
|
776
|
+
while (position < size) {
|
|
777
|
+
const read = fs.readSync(fd, chunk, 0, Math.min(chunk.length, size - position), position);
|
|
778
|
+
if (read <= 0) break;
|
|
779
|
+
position += read;
|
|
780
|
+
const data = carry ? Buffer.concat([carry, chunk.subarray(0, read)]) : chunk.subarray(0, read);
|
|
781
|
+
let start = 0;
|
|
782
|
+
for (let end = data.indexOf(NEWLINE, start); end !== -1; end = data.indexOf(NEWLINE, start)) {
|
|
783
|
+
if (end > start) onLine(data.toString('utf8', start, end));
|
|
784
|
+
start = end + 1;
|
|
785
|
+
}
|
|
786
|
+
consumed = position - (data.length - start);
|
|
787
|
+
carry = start < data.length ? Buffer.from(data.subarray(start)) : null;
|
|
788
|
+
}
|
|
789
|
+
} catch { /* unreadable right now: keep what was consumed */ } finally { try { fs.closeSync(fd); } catch { /* already closed */ } }
|
|
790
|
+
return consumed;
|
|
791
|
+
}
|
|
792
|
+
|
|
793
|
+
const inWindow = (timestamp) => {
|
|
794
|
+
if (!since) return true;
|
|
795
|
+
const at = Date.parse(timestamp);
|
|
796
|
+
return Number.isFinite(at) && at >= since;
|
|
797
|
+
};
|
|
798
|
+
|
|
799
|
+
function rememberResponse(id) {
|
|
800
|
+
seen.add(id);
|
|
801
|
+
seenList.push(id);
|
|
802
|
+
if (seenList.length > CODEX_SEEN_MAX) seen.delete(seenList.shift());
|
|
803
|
+
}
|
|
804
|
+
|
|
805
|
+
/** Add one response's usage to its thread + turn group. */
|
|
806
|
+
function book(groups, file, turnId, usage, source) {
|
|
807
|
+
const raw = codexRawUsage(usage);
|
|
808
|
+
const longSize = longSizeOf(file.model || null);
|
|
809
|
+
const long = longSize ? raw.input_tokens > longSize : null;
|
|
810
|
+
const key = `${file.threadId}|${turnId || ''}|${source}|${long === true ? 'long' : ''}`;
|
|
811
|
+
let group = groups.get(key);
|
|
812
|
+
if (!group) {
|
|
813
|
+
const exact = source === 'codex-records';
|
|
814
|
+
group = {
|
|
815
|
+
threadId: file.threadId, part: file.threadId === root ? 'main' : 'subagents', turnId: turnId || null, model: file.model || null,
|
|
816
|
+
tokens: zeroTokens(), usage: zeroCodexUsage(), responses: 0, mismatches: 0,
|
|
817
|
+
fidelity: exact ? 'exact' : 'partial', reason: exact ? null : 'codex-no-records', source,
|
|
818
|
+
...(long === null ? {} : { long }),
|
|
819
|
+
};
|
|
820
|
+
groups.set(key, group);
|
|
821
|
+
}
|
|
822
|
+
const { tokens, mismatch } = codexUsageTokens(raw);
|
|
823
|
+
group.tokens = addTokens(group.tokens, tokens);
|
|
824
|
+
for (const name of CODEX_USAGE_KEYS) group.usage[name] += name === 'total_tokens' ? (raw.total_tokens ?? raw.input_tokens + raw.output_tokens) : raw[name];
|
|
825
|
+
group.responses += 1;
|
|
826
|
+
if (mismatch) group.mismatches += 1;
|
|
827
|
+
if (file.model) group.model = file.model;
|
|
828
|
+
}
|
|
829
|
+
|
|
830
|
+
function onLine(file, line, groups) {
|
|
831
|
+
if (!meterReadsLine(line)) return;
|
|
832
|
+
let entry = null;
|
|
833
|
+
try { entry = JSON.parse(line); } catch { return; }
|
|
834
|
+
const payload = entry?.payload;
|
|
835
|
+
if (!payload || typeof payload !== 'object') return;
|
|
836
|
+
if (entry.type === 'turn_context') {
|
|
837
|
+
if (payload.model) file.model = String(payload.model);
|
|
838
|
+
if (payload.turn_id) file.turn = String(payload.turn_id);
|
|
839
|
+
return;
|
|
840
|
+
}
|
|
841
|
+
if (entry.type === 'token_usage_record') {
|
|
842
|
+
if (payload.thread_id !== file.threadId) return; // the parent's history replayed inside a forked child
|
|
843
|
+
file.records = true;
|
|
844
|
+
if (!inWindow(entry.timestamp)) return;
|
|
845
|
+
const responseId = payload.response_id ? String(payload.response_id) : null;
|
|
846
|
+
const at = String(entry.timestamp || '');
|
|
847
|
+
if (at < file.mark.at || (at === file.mark.at && (!responseId || file.mark.ids.includes(responseId)))) return;
|
|
848
|
+
if (responseId) {
|
|
849
|
+
if (seen.has(responseId)) return;
|
|
850
|
+
rememberResponse(responseId);
|
|
851
|
+
}
|
|
852
|
+
if (at > file.mark.at) file.mark = { at, ids: [] };
|
|
853
|
+
if (responseId) file.mark.ids.push(responseId);
|
|
854
|
+
book(groups, file, payload.turn_id || file.turn, payload.usage, 'codex-records');
|
|
855
|
+
return;
|
|
856
|
+
}
|
|
857
|
+
if (entry.type !== 'event_msg' || payload.type !== 'token_count' || !payload.info || file.records) return;
|
|
858
|
+
const running = count(payload.info.total_token_usage?.total_tokens);
|
|
859
|
+
if (running === file.tc) return; // the same event again (rate-limit refresh): nothing new was spent
|
|
860
|
+
file.tc = running;
|
|
861
|
+
if (!payload.info.last_token_usage || !inWindow(entry.timestamp)) return;
|
|
862
|
+
book(groups, file, file.turn, payload.info.last_token_usage, 'codex-token-count');
|
|
863
|
+
}
|
|
864
|
+
|
|
865
|
+
function poll(options = {}) {
|
|
866
|
+
if (options?.discover === true || !hasRoot() || now() - discoveredAt >= DISCOVER_EVERY_MS) { discover(); discoveredAt = now(); }
|
|
867
|
+
const groups = new Map();
|
|
868
|
+
const ordered = [...files].sort(([, a], [, b]) => Number(b.threadId === root) - Number(a.threadId === root));
|
|
869
|
+
for (const [path, file] of ordered) {
|
|
870
|
+
let size = 0;
|
|
871
|
+
let inode = null;
|
|
872
|
+
try { const stat = fs.statSync(path); size = stat.size; inode = stat.ino ?? null; } catch { continue; }
|
|
873
|
+
if ((file.inode !== null && inode !== file.inode) || size < file.offset) {
|
|
874
|
+
file.offset = 0; file.tc = null; file.records = false;
|
|
875
|
+
}
|
|
876
|
+
file.inode = inode;
|
|
877
|
+
if (size === file.offset) continue;
|
|
878
|
+
file.offset = readLines(path, file.offset, size, (line) => onLine(file, line, groups));
|
|
879
|
+
}
|
|
880
|
+
return [...groups.values()];
|
|
881
|
+
}
|
|
882
|
+
|
|
883
|
+
return {
|
|
884
|
+
poll,
|
|
885
|
+
rollouts: () => [...files].map(([path, file]) => ({ path, threadId: file.threadId, part: file.threadId === root ? 'main' : 'subagents' })),
|
|
886
|
+
state: () => ({ files: Object.fromEntries([...files].map(([path, file]) => [path, { ...file }])), seen: [...seenList] }),
|
|
887
|
+
};
|
|
888
|
+
}
|
|
889
|
+
|
|
890
|
+
// ── Part 2C: OpenCode meter ──────────────────────────────────────────────────
|
|
891
|
+
|
|
892
|
+
const OPENCODE_SESSION_MAX = 500;
|
|
893
|
+
const modelKey = (provider, model) => String(provider || '') + '\n' + String(model || '');
|
|
894
|
+
const splitModelKey = (key) => { const at = key.indexOf('\n'); return { provider: key.slice(0, at) || null, model: key.slice(at + 1) || 'unknown' }; };
|
|
895
|
+
|
|
896
|
+
/**
|
|
897
|
+
* Meter for one isolated OpenCode serve. Message maxima stay in memory for the
|
|
898
|
+
* meter lifetime. State stores only per-session counted totals and the highest
|
|
899
|
+
* message id, so a restart can ignore replayed events without serializing messages.
|
|
900
|
+
* markTurn and live difference each session before classifying it under the root.
|
|
901
|
+
*/
|
|
902
|
+
export function createOpenCodeMeter({ state } = {}) {
|
|
903
|
+
let root = typeof state?.root === 'string' ? state.root : null;
|
|
904
|
+
const entries = new Map(); // session:id → { session, id, key, tokens, cost }
|
|
905
|
+
const base = new Map(); // restored or reconciled totals → { upTo, cost, models }
|
|
906
|
+
const sessions = new Set();
|
|
907
|
+
const mark = new Map(); // session → totals at the last markTurn
|
|
908
|
+
|
|
909
|
+
const blank = () => ({ upTo: '', cost: 0, models: new Map() });
|
|
910
|
+
const getBase = (session) => {
|
|
911
|
+
if (!base.has(session)) base.set(session, blank());
|
|
912
|
+
return base.get(session);
|
|
913
|
+
};
|
|
914
|
+
const readSummary = (raw) => {
|
|
915
|
+
const summary = blank();
|
|
916
|
+
summary.upTo = String(raw?.upTo || '');
|
|
917
|
+
summary.cost = usd(raw?.cost);
|
|
918
|
+
for (const [key, row] of Object.entries(raw?.models || {})) summary.models.set(key, rowToTokens(row));
|
|
919
|
+
return summary;
|
|
920
|
+
};
|
|
921
|
+
const encodeSummary = (summary) => ({
|
|
922
|
+
upTo: summary.upTo, cost: summary.cost,
|
|
923
|
+
models: Object.fromEntries([...summary.models].map(([key, tokens]) => [key, tokensToRow(tokens)])),
|
|
924
|
+
});
|
|
925
|
+
const addEntry = (summary, entry) => {
|
|
926
|
+
summary.models.set(entry.key, addTokens(summary.models.get(entry.key), entry.tokens));
|
|
927
|
+
summary.cost += entry.cost;
|
|
928
|
+
if (entry.id > summary.upTo) summary.upTo = entry.id;
|
|
929
|
+
};
|
|
930
|
+
const summaryTokens = (summary) => [...summary.models.values()].reduce(addTokens, zeroTokens());
|
|
931
|
+
|
|
932
|
+
for (const [session, raw] of Object.entries(state?.sessions || {})) {
|
|
933
|
+
base.set(session, readSummary(raw));
|
|
934
|
+
sessions.add(session);
|
|
935
|
+
}
|
|
936
|
+
// Accept previously persisted folded states while moving to the compact format.
|
|
937
|
+
if (!state?.sessions) {
|
|
938
|
+
for (const [session, raw] of Object.entries(state?.base || {})) {
|
|
939
|
+
base.set(session, readSummary(raw));
|
|
940
|
+
sessions.add(session);
|
|
941
|
+
}
|
|
942
|
+
for (const row of Array.isArray(state?.entries) ? state.entries : []) {
|
|
943
|
+
if (!Array.isArray(row) || typeof row[0] !== 'string' || typeof row[1] !== 'string') continue;
|
|
944
|
+
entries.set(row[0] + ':' + row[1], { session: row[0], id: row[1], key: String(row[2] ?? modelKey('', '')), tokens: rowToTokens(row[3]), cost: usd(row[4]) });
|
|
945
|
+
sessions.add(row[0]);
|
|
946
|
+
}
|
|
947
|
+
}
|
|
948
|
+
for (const [session, raw] of Object.entries(state?.marks || {})) mark.set(session, readSummary(raw));
|
|
949
|
+
|
|
950
|
+
function sessionTotals() {
|
|
951
|
+
const all = new Map([...base].map(([session, summary]) => [session, {
|
|
952
|
+
upTo: summary.upTo, cost: summary.cost, models: new Map([...summary.models].map(([key, tokens]) => [key, { ...tokens }])),
|
|
953
|
+
}]));
|
|
954
|
+
for (const entry of entries.values()) {
|
|
955
|
+
if (!all.has(entry.session)) all.set(entry.session, blank());
|
|
956
|
+
addEntry(all.get(entry.session), entry);
|
|
957
|
+
}
|
|
958
|
+
return all;
|
|
959
|
+
}
|
|
960
|
+
|
|
961
|
+
function view(all, earlier = null) {
|
|
962
|
+
const main = zeroTokens();
|
|
963
|
+
const subagents = zeroTokens();
|
|
964
|
+
const models = new Map();
|
|
965
|
+
let cost = 0;
|
|
966
|
+
for (const [session, summary] of all) {
|
|
967
|
+
const prior = earlier?.get(session);
|
|
968
|
+
const part = !root || session === root ? main : subagents;
|
|
969
|
+
for (const [key, tokens] of summary.models) {
|
|
970
|
+
const delta = prior ? subTokens(tokens, prior.models.get(key)) : tokens;
|
|
971
|
+
for (const name of TOKEN_KEYS) part[name] += delta[name];
|
|
972
|
+
models.set(key, addTokens(models.get(key), delta));
|
|
973
|
+
}
|
|
974
|
+
cost += prior ? Math.max(0, summary.cost - prior.cost) : summary.cost;
|
|
975
|
+
}
|
|
976
|
+
return {
|
|
977
|
+
tokens: addTokens(main, subagents), costUsd: roundUsd(cost), main, subagents,
|
|
978
|
+
byModel: [...models].filter(([, tokens]) => totalTokens(tokens) > 0)
|
|
979
|
+
.map(([key, tokens]) => ({ ...splitModelKey(key), tokens }))
|
|
980
|
+
.sort((a, b) => totalTokens(b.tokens) - totalTokens(a.tokens)),
|
|
981
|
+
};
|
|
982
|
+
}
|
|
983
|
+
|
|
984
|
+
// Older state held a global mark. Assign its counted model totals to the
|
|
985
|
+
// sessions that supplied them, so an unmarked turn still survives upgrade.
|
|
986
|
+
if (!state?.marks && state?.mark) {
|
|
987
|
+
const all = sessionTotals();
|
|
988
|
+
const ordered = [...all].sort(([a], [b]) => Number(b === root) - Number(a === root));
|
|
989
|
+
for (const [key, row] of Object.entries(state.mark.models || {})) {
|
|
990
|
+
let remaining = rowToTokens(row);
|
|
991
|
+
for (const [session, summary] of ordered) {
|
|
992
|
+
const available = summary.models.get(key);
|
|
993
|
+
if (!available) continue;
|
|
994
|
+
const counted = zeroTokens();
|
|
995
|
+
for (const name of TOKEN_KEYS) counted[name] = Math.min(remaining[name], available[name]);
|
|
996
|
+
const prior = mark.get(session) || blank();
|
|
997
|
+
prior.models.set(key, addTokens(prior.models.get(key), counted));
|
|
998
|
+
mark.set(session, prior);
|
|
999
|
+
remaining = subTokens(remaining, counted);
|
|
1000
|
+
}
|
|
1001
|
+
}
|
|
1002
|
+
let cost = usd(state.mark.cost);
|
|
1003
|
+
for (const [session, summary] of ordered) {
|
|
1004
|
+
const prior = mark.get(session) || blank();
|
|
1005
|
+
prior.cost = Math.min(cost, summary.cost);
|
|
1006
|
+
cost -= prior.cost;
|
|
1007
|
+
mark.set(session, prior);
|
|
1008
|
+
}
|
|
1009
|
+
}
|
|
1010
|
+
|
|
1011
|
+
function merge(info, fallbackSession = null) {
|
|
1012
|
+
if (!info || typeof info !== 'object' || info.role !== 'assistant') return false;
|
|
1013
|
+
const session = String(info.sessionID || fallbackSession || '');
|
|
1014
|
+
const id = String(info.id || '');
|
|
1015
|
+
if (!session || !id) return false;
|
|
1016
|
+
sessions.add(session);
|
|
1017
|
+
const mapKey = session + ':' + id;
|
|
1018
|
+
const known = entries.get(mapKey);
|
|
1019
|
+
if (!known && id <= (base.get(session)?.upTo || '')) return false;
|
|
1020
|
+
const tokens = openCodeTokens(info.tokens);
|
|
1021
|
+
const cost = usd(info.cost);
|
|
1022
|
+
if (!known) {
|
|
1023
|
+
if (totalTokens(tokens) === 0 && cost === 0) return false;
|
|
1024
|
+
entries.set(mapKey, { session, id, key: modelKey(info.providerID, info.modelID), tokens, cost });
|
|
1025
|
+
return true;
|
|
1026
|
+
}
|
|
1027
|
+
const raised = maxTokens(known.tokens, tokens);
|
|
1028
|
+
const changed = !sameTokens(raised, known.tokens) || cost > known.cost;
|
|
1029
|
+
known.tokens = raised;
|
|
1030
|
+
known.cost = Math.max(known.cost, cost);
|
|
1031
|
+
if (known.key === modelKey('', '') && (info.providerID || info.modelID)) known.key = modelKey(info.providerID, info.modelID);
|
|
1032
|
+
return changed;
|
|
1033
|
+
}
|
|
1034
|
+
|
|
1035
|
+
function onEvent(eventType, event) {
|
|
1036
|
+
const type = String(eventType || event?.type || '').replace(':', '.');
|
|
1037
|
+
const info = event?.info || event?.message?.info || event?.properties?.info || null;
|
|
1038
|
+
if (type === 'session.created' || type === 'session.updated') {
|
|
1039
|
+
if (info?.id && sessions.size < OPENCODE_SESSION_MAX) sessions.add(String(info.id));
|
|
1040
|
+
return false;
|
|
1041
|
+
}
|
|
1042
|
+
if (type !== 'message.updated') return false;
|
|
1043
|
+
return merge(info);
|
|
1044
|
+
}
|
|
1045
|
+
|
|
1046
|
+
async function reconcile({ children = async () => [], messages = async () => [] } = {}) {
|
|
1047
|
+
const queue = [...(root ? [root] : []), ...sessions];
|
|
1048
|
+
const visited = new Set();
|
|
1049
|
+
let changed = false;
|
|
1050
|
+
while (queue.length && visited.size < OPENCODE_SESSION_MAX) {
|
|
1051
|
+
const session = queue.shift();
|
|
1052
|
+
if (!session || visited.has(session)) continue;
|
|
1053
|
+
visited.add(session);
|
|
1054
|
+
let stored = [];
|
|
1055
|
+
try { stored = await messages(session); } catch { stored = []; }
|
|
1056
|
+
const storedMessages = new Map();
|
|
1057
|
+
for (const item of Array.isArray(stored) ? stored : []) {
|
|
1058
|
+
const info = item?.info || item;
|
|
1059
|
+
if (info?.role !== 'assistant' || !info.id) continue;
|
|
1060
|
+
const id = String(info.id);
|
|
1061
|
+
const key = modelKey(info.providerID, info.modelID);
|
|
1062
|
+
const prior = storedMessages.get(id);
|
|
1063
|
+
const tokens = openCodeTokens(info.tokens);
|
|
1064
|
+
const cost = usd(info.cost);
|
|
1065
|
+
storedMessages.set(id, prior ? { id, key: prior.key, tokens: maxTokens(prior.tokens, tokens), cost: Math.max(prior.cost, cost) } : { id, key, tokens, cost });
|
|
1066
|
+
if (merge(info, session)) changed = true;
|
|
1067
|
+
}
|
|
1068
|
+
// A restored meter does not retain old messages. A full storage scan can
|
|
1069
|
+
// still reveal an old snapshot that was missed before the restart.
|
|
1070
|
+
const expected = blank();
|
|
1071
|
+
for (const entry of storedMessages.values()) addEntry(expected, entry);
|
|
1072
|
+
const actual = sessionTotals().get(session) || blank();
|
|
1073
|
+
const target = getBase(session);
|
|
1074
|
+
const missing = subTokens(summaryTokens(expected), summaryTokens(actual));
|
|
1075
|
+
if (totalTokens(missing)) {
|
|
1076
|
+
const key = [...expected.models].sort((a, b) => totalTokens(b[1]) - totalTokens(a[1]))[0]?.[0] || modelKey('', '');
|
|
1077
|
+
target.models.set(key, addTokens(target.models.get(key), missing));
|
|
1078
|
+
changed = true;
|
|
1079
|
+
}
|
|
1080
|
+
if (expected.cost > actual.cost) { target.cost += expected.cost - actual.cost; changed = true; }
|
|
1081
|
+
if (expected.upTo > target.upTo) target.upTo = expected.upTo;
|
|
1082
|
+
let kids = [];
|
|
1083
|
+
try { kids = await children(session); } catch { kids = []; }
|
|
1084
|
+
for (const kid of Array.isArray(kids) ? kids : []) queue.push(typeof kid === 'string' ? kid : String(kid?.id || ''));
|
|
1085
|
+
}
|
|
1086
|
+
return changed;
|
|
1087
|
+
}
|
|
1088
|
+
|
|
1089
|
+
return {
|
|
1090
|
+
onEvent,
|
|
1091
|
+
setRoot(sessionId) { root = sessionId ? String(sessionId) : null; if (root) sessions.add(root); },
|
|
1092
|
+
total: () => view(sessionTotals()),
|
|
1093
|
+
live: () => view(sessionTotals(), mark),
|
|
1094
|
+
markTurn() {
|
|
1095
|
+
const totals = sessionTotals();
|
|
1096
|
+
const delta = view(totals, mark);
|
|
1097
|
+
mark.clear();
|
|
1098
|
+
for (const [session, summary] of totals) mark.set(session, summary);
|
|
1099
|
+
return delta;
|
|
1100
|
+
},
|
|
1101
|
+
reconcile,
|
|
1102
|
+
state: () => ({
|
|
1103
|
+
root,
|
|
1104
|
+
sessions: Object.fromEntries([...sessionTotals()].map(([session, summary]) => [session, encodeSummary(summary)])),
|
|
1105
|
+
marks: Object.fromEntries([...mark].map(([session, summary]) => [session, encodeSummary(summary)])),
|
|
1106
|
+
}),
|
|
1107
|
+
};
|
|
1108
|
+
}
|
|
1109
|
+
|
|
1110
|
+
// ── Part 3: ledger ───────────────────────────────────────────────────────────
|
|
1111
|
+
|
|
1112
|
+
const LEDGER_DIR = 'assistant-usage';
|
|
1113
|
+
const JUDGMENT_TTL_MS = 5000;
|
|
1114
|
+
const TITLE_MAX = 200;
|
|
1115
|
+
const PARTS = Object.freeze(['main', 'subagents', 'aux']);
|
|
1116
|
+
/** How a dollar figure is known, weakest last: a sum is only as good as its weakest part. */
|
|
1117
|
+
const COST_BASES = Object.freeze(['free', 'reported', 'estimated', 'unpriced']);
|
|
1118
|
+
/** The weaker of two cost bases (null when neither is one). */
|
|
1119
|
+
export function weakerCostBasis(a, b) {
|
|
1120
|
+
const x = COST_BASES.indexOf(a);
|
|
1121
|
+
const y = COST_BASES.indexOf(b);
|
|
1122
|
+
return x < 0 && y < 0 ? null : COST_BASES[Math.max(x, y)];
|
|
1123
|
+
}
|
|
1124
|
+
|
|
1125
|
+
/** A session id as a file name; null when it cannot be one. */
|
|
1126
|
+
function safeSessionId(sessionId) {
|
|
1127
|
+
const id = String(sessionId || '').replace(/[^A-Za-z0-9._-]/g, '_');
|
|
1128
|
+
return id && id !== '.' && id !== '..' ? id : null;
|
|
1129
|
+
}
|
|
1130
|
+
|
|
1131
|
+
/** <dataDir>/assistant-usage/<sessionId>.jsonl, or null for an unusable id. */
|
|
1132
|
+
export function usageLedgerPath(dataDir, sessionId) {
|
|
1133
|
+
const id = safeSessionId(sessionId);
|
|
1134
|
+
return id ? join(dataDir, LEDGER_DIR, `${id}.jsonl`) : null;
|
|
1135
|
+
}
|
|
1136
|
+
|
|
1137
|
+
/**
|
|
1138
|
+
* Parse a ledger file: { tasks:[{ id, n, at, title }], rows:[use lines with tokens as an object],
|
|
1139
|
+
* judged:[{ task, at, input, output, calls, costUsd }], corrupt, endsClean }.
|
|
1140
|
+
* Corrupt lines are counted and skipped. A missing file is an empty ledger.
|
|
1141
|
+
*/
|
|
1142
|
+
export function readUsageLedgerFile(path, fs = nodeFs) {
|
|
1143
|
+
const out = { tasks: [], rows: [], judged: [], corrupt: 0, endsClean: true };
|
|
1144
|
+
let text = '';
|
|
1145
|
+
try { text = fs.readFileSync(path, 'utf8'); } catch { return out; }
|
|
1146
|
+
out.endsClean = text === '' || text.endsWith('\n');
|
|
1147
|
+
for (const line of text.split('\n')) {
|
|
1148
|
+
if (!line.trim()) continue;
|
|
1149
|
+
let entry = null;
|
|
1150
|
+
try { entry = JSON.parse(line); } catch { out.corrupt += 1; continue; }
|
|
1151
|
+
if (entry?.t === 'task' && typeof entry.id === 'string') {
|
|
1152
|
+
out.tasks.push({ id: entry.id, n: count(entry.n), at: count(entry.at), title: typeof entry.title === 'string' ? entry.title : null });
|
|
1153
|
+
} else if (entry?.t === 'use' && typeof entry.task === 'string') {
|
|
1154
|
+
out.rows.push({ ...entry, tokens: rowToTokens(entry.tokens) });
|
|
1155
|
+
} else if (entry?.t === 'jev' && typeof entry.task === 'string') {
|
|
1156
|
+
out.judged.push({ task: entry.task, at: count(entry.at), input: count(entry.input), output: count(entry.output), calls: count(entry.calls), costUsd: usd(entry.cost) });
|
|
1157
|
+
} else out.corrupt += 1;
|
|
1158
|
+
}
|
|
1159
|
+
return out;
|
|
1160
|
+
}
|
|
1161
|
+
|
|
1162
|
+
/** Who a row belongs to: 'brain', or the run id. */
|
|
1163
|
+
const agentKeyOf = (scope, runId) => (scope === 'brain' ? 'brain' : String(runId || 'run'));
|
|
1164
|
+
|
|
1165
|
+
/** The larger of two judgment counts, field by field; the other when one is missing. */
|
|
1166
|
+
function moreJudged(a, b) {
|
|
1167
|
+
if (!a || !b) return a || b || null;
|
|
1168
|
+
return { input: Math.max(a.input, b.input), output: Math.max(a.output, b.output), calls: Math.max(a.calls, b.calls), costUsd: Math.max(usd(a.costUsd), usd(b.costUsd)) };
|
|
1169
|
+
}
|
|
1170
|
+
|
|
1171
|
+
/**
|
|
1172
|
+
* The usage ledger: one append-only file per assistant session
|
|
1173
|
+
* (<dataDir>/assistant-usage/<sessionId>.jsonl), loaded on first use and cached.
|
|
1174
|
+
* {"t":"task","id","n","at","title"}
|
|
1175
|
+
* {"t":"use","at","task","scope":"brain"|"run","run","turn","provider","model","part":"main"|"subagents"|"aux",
|
|
1176
|
+
* "tokens":[input,cacheWrite,cacheRead,output,reasoning],"cost","basis","fid":"exact"|"partial","why","src"}
|
|
1177
|
+
* {"t":"jev","at","task","input","output","calls","cost"} a task's Jev judgments, as last counted (the largest wins)
|
|
1178
|
+
*
|
|
1179
|
+
* beginTask(sessionId, { title, at }) → { id, n } a human prompt: id `task-<n>`, n from 1; becomes the current task
|
|
1180
|
+
* currentTask(sessionId) → { id, n, title, startedAt } | null
|
|
1181
|
+
* ensureTask(sessionId) → the current task, creating { id:'task-0', n:0, title:null } when the session has none
|
|
1182
|
+
* settle({ sessionId, taskId, scope, runId, turn, provider, model, part, tokens, costUsd, costBasis, fidelity, reason, source })
|
|
1183
|
+
* appends one row and returns it; null when it has zero tokens and no cost. taskId defaults to the
|
|
1184
|
+
* current task (ensureTask); an unknown one falls back to it. tokens is a five-class object or a
|
|
1185
|
+
* ledger array. part defaults to 'main', fidelity to 'exact'; `at` (ms) overrides the row time.
|
|
1186
|
+
* Clears the pending of that agent ('brain', or the runId) unless part is 'aux'.
|
|
1187
|
+
* setPending(sessionId, agentKey, { taskId, scope, runId, provider, model, tokens }) replaces that agent's
|
|
1188
|
+
* provisional tokens (zero tokens clear it); clearPending(sessionId, agentKey). Never written to disk.
|
|
1189
|
+
* taskView(sessionId, taskId = 'current') → the gauge's view (see below)
|
|
1190
|
+
* sessionView(sessionId) → { sessionId, tokens, pending, costUsd, costBasis, live, fidelity, unsynced, models,
|
|
1191
|
+
* tasks:[{ id, n, title, startedAt, total, inputTotal, outputTotal, costUsd, fidelity, live }] } (oldest first)
|
|
1192
|
+
* flush(sessionId?) → remaining unsynced lines after retrying them in order
|
|
1193
|
+
* runView(sessionId, runId) → { tokens, pending, subagents:{ total }, fidelity, taskIds } | null, over every task the run worked in
|
|
1194
|
+
* subscribe(listener) → unsubscribe; listener(sessionId) after beginTask, settle and every pending change
|
|
1195
|
+
* dropSession(sessionId) deletes the file and cache; late writes are ignored until beginTask(id)
|
|
1196
|
+
*
|
|
1197
|
+
* taskView:
|
|
1198
|
+
* { sessionId,
|
|
1199
|
+
* task: null | { id, n, title, startedAt, live, fidelity, tokens, pending, costUsd,
|
|
1200
|
+
* agents:[{ key, scope, runId, provider, model, tokens, pending, subagents:{ total }, fidelity, partialReason }],
|
|
1201
|
+
* costBasis, models:[{ provider, model, tokens, costUsd, costBasis }] },
|
|
1202
|
+
* session:{ tokens, pending, costUsd, costBasis, live, fidelity, tasks, unsynced,
|
|
1203
|
+
* models:[{ provider, model, tokens, costUsd, costBasis }] },
|
|
1204
|
+
* recent:[{ id, n, title, total, inputTotal, outputTotal, costUsd, fidelity, live }] }
|
|
1205
|
+
* `session` is the headline: every task of the session added up, so it only grows as tasks are
|
|
1206
|
+
* added. It is built from the file, so a restart shows the same totals.
|
|
1207
|
+
* Every `tokens` is settled + pending (five classes + total + inputTotal + outputTotal); `pending`
|
|
1208
|
+
* is the provisional share inside it. live = an agent of the task has pending tokens. fidelity =
|
|
1209
|
+
* 'live' when live, else 'partial' when any of its rows is partial, else 'exact'. agents: brain
|
|
1210
|
+
* first, then runs in first-seen order; an agent that used several models reports the one with
|
|
1211
|
+
* most tokens; subagents.total counts its part 'subagents' rows. costUsd sums the rows that
|
|
1212
|
+
* carried a cost; costBasis is the weakest basis among them ('free' < 'reported' < 'estimated' <
|
|
1213
|
+
* 'unpriced', null when no row named one). models: most tokens first.
|
|
1214
|
+
* recent = up to 5 other tasks, newest first.
|
|
1215
|
+
* With `judgments` (a SYNC function ({ sessionId, taskId, runIds, sinceMs, untilMs }) →
|
|
1216
|
+
* { input, output, calls } | null) one more agent { key:'judgments', scope:'judgments',
|
|
1217
|
+
* provider:'jev', calls } is added last and counted in the task and session totals; its answer is
|
|
1218
|
+
* cached 5 s for the current task or one with pending tokens; a closed task with no pending
|
|
1219
|
+
* keeps its final answer until dropSession() clears the session cache. The largest count seen
|
|
1220
|
+
* for a task is written to the file ("jev" line) and never undercut: the judgment log is pruned,
|
|
1221
|
+
* so a later read (or a restart) can find less than was shown.
|
|
1222
|
+
*
|
|
1223
|
+
* @param {object} options
|
|
1224
|
+
* @param {string} options.dataDir
|
|
1225
|
+
* @param {() => number} [options.now]
|
|
1226
|
+
* @param {(event: string, detail: string) => void} [options.log]
|
|
1227
|
+
* @param {Function|null} [options.judgments]
|
|
1228
|
+
* @param {object} [options.fs] node:fs-like (tests)
|
|
1229
|
+
*/
|
|
1230
|
+
export function createUsageLedger({ dataDir, now = Date.now, log = () => {}, judgments = null, fs = nodeFs } = {}) {
|
|
1231
|
+
if (!dataDir) throw new Error('createUsageLedger requires dataDir');
|
|
1232
|
+
const sessions = new Map();
|
|
1233
|
+
const dropped = new Set();
|
|
1234
|
+
const listeners = new Set();
|
|
1235
|
+
|
|
1236
|
+
const newTask = ({ id, n, title = null, startedAt = 0 }) => ({ id, n, title, startedAt, order: [], agents: new Map(), partial: false, costUsd: 0 });
|
|
1237
|
+
const newAgent = (key, scope, runId) => ({
|
|
1238
|
+
key, scope, runId, provider: null, tokens: zeroTokens(), subagents: zeroTokens(), costUsd: 0,
|
|
1239
|
+
partial: false, partialReason: null, models: new Map(), money: new Map(), // money: model → { costUsd, basis }
|
|
1240
|
+
});
|
|
1241
|
+
|
|
1242
|
+
function noteAgent(task, key) {
|
|
1243
|
+
if (!task.order.includes(key)) task.order.push(key);
|
|
1244
|
+
}
|
|
1245
|
+
|
|
1246
|
+
/** Add one settled row to its task's aggregates. */
|
|
1247
|
+
function apply(session, row) {
|
|
1248
|
+
let task = session.tasks.get(row.task);
|
|
1249
|
+
if (!task) {
|
|
1250
|
+
// A row whose task line was lost (corrupt line): keep its tokens under a placeholder.
|
|
1251
|
+
const n = count(/^task-(\d+)$/.exec(row.task)?.[1]);
|
|
1252
|
+
task = newTask({ id: row.task, n, startedAt: count(row.at) });
|
|
1253
|
+
session.tasks.set(task.id, task);
|
|
1254
|
+
session.maxN = Math.max(session.maxN, n);
|
|
1255
|
+
if (!session.current) session.current = task.id;
|
|
1256
|
+
}
|
|
1257
|
+
const key = agentKeyOf(row.scope, row.run);
|
|
1258
|
+
noteAgent(task, key);
|
|
1259
|
+
let agent = task.agents.get(key);
|
|
1260
|
+
if (!agent) { agent = newAgent(key, row.scope === 'brain' ? 'brain' : 'run', row.scope === 'brain' ? null : row.run || null); task.agents.set(key, agent); }
|
|
1261
|
+
const tokens = cleanTokens(row.tokens);
|
|
1262
|
+
agent.tokens = addTokens(agent.tokens, tokens);
|
|
1263
|
+
if (row.part === 'subagents') agent.subagents = addTokens(agent.subagents, tokens);
|
|
1264
|
+
if (!agent.provider && row.provider) agent.provider = row.provider;
|
|
1265
|
+
const mKey = modelKey(row.provider, row.model);
|
|
1266
|
+
agent.models.set(mKey, addTokens(agent.models.get(mKey), tokens));
|
|
1267
|
+
const cost = usd(row.cost);
|
|
1268
|
+
agent.costUsd += cost;
|
|
1269
|
+
task.costUsd += cost;
|
|
1270
|
+
const money = agent.money.get(mKey) || { costUsd: 0, basis: null };
|
|
1271
|
+
money.costUsd += cost;
|
|
1272
|
+
money.basis = weakerCostBasis(money.basis, row.basis);
|
|
1273
|
+
agent.money.set(mKey, money);
|
|
1274
|
+
if (row.fid === 'partial') {
|
|
1275
|
+
task.partial = true;
|
|
1276
|
+
agent.partial = true;
|
|
1277
|
+
if (!agent.partialReason) agent.partialReason = row.why || 'partial';
|
|
1278
|
+
}
|
|
1279
|
+
}
|
|
1280
|
+
|
|
1281
|
+
/** The session's cached aggregates, loaded from its file on first use; null for an unusable id. */
|
|
1282
|
+
function open(sessionId) {
|
|
1283
|
+
const path = usageLedgerPath(dataDir, sessionId);
|
|
1284
|
+
if (!path) return null;
|
|
1285
|
+
const id = String(sessionId);
|
|
1286
|
+
if (dropped.has(id)) return null;
|
|
1287
|
+
if (sessions.has(id)) return sessions.get(id);
|
|
1288
|
+
const session = { id, path, tasks: new Map(), current: null, maxN: 0, pending: new Map(), judged: new Map(), jev: new Map(), endsClean: true, retry: [] };
|
|
1289
|
+
const file = readUsageLedgerFile(path, fs);
|
|
1290
|
+
session.endsClean = file.endsClean;
|
|
1291
|
+
if (file.corrupt) log('assistant-usage:corrupt-lines', `${id}: ${file.corrupt} skipped`);
|
|
1292
|
+
for (const line of file.tasks) {
|
|
1293
|
+
const known = session.tasks.get(line.id);
|
|
1294
|
+
if (known) { known.n = line.n; known.title = line.title; known.startedAt = line.at; } else session.tasks.set(line.id, newTask({ id: line.id, n: line.n, title: line.title, startedAt: line.at }));
|
|
1295
|
+
session.maxN = Math.max(session.maxN, line.n);
|
|
1296
|
+
session.current = line.id;
|
|
1297
|
+
}
|
|
1298
|
+
for (const row of file.rows) apply(session, row);
|
|
1299
|
+
for (const line of file.judged) session.jev.set(line.task, moreJudged(session.jev.get(line.task), line));
|
|
1300
|
+
sessions.set(id, session);
|
|
1301
|
+
return session;
|
|
1302
|
+
}
|
|
1303
|
+
|
|
1304
|
+
function drain(session) {
|
|
1305
|
+
while (session.retry.length) {
|
|
1306
|
+
try {
|
|
1307
|
+
fs.mkdirSync(join(dataDir, LEDGER_DIR), { recursive: true });
|
|
1308
|
+
// A torn last line (crash mid-append) must not swallow this one.
|
|
1309
|
+
fs.appendFileSync(session.path, `${session.endsClean ? '' : '\n'}${JSON.stringify(session.retry[0])}\n`);
|
|
1310
|
+
session.endsClean = true;
|
|
1311
|
+
session.retry.shift();
|
|
1312
|
+
} catch (error) {
|
|
1313
|
+
session.endsClean = readUsageLedgerFile(session.path, fs).endsClean;
|
|
1314
|
+
try { log('assistant-usage:append-error', error?.message || String(error)); } catch { /* an append failure stays queued */ }
|
|
1315
|
+
break;
|
|
1316
|
+
}
|
|
1317
|
+
}
|
|
1318
|
+
return session.retry.length;
|
|
1319
|
+
}
|
|
1320
|
+
|
|
1321
|
+
function append(session, line) {
|
|
1322
|
+
session.retry.push(line);
|
|
1323
|
+
drain(session);
|
|
1324
|
+
}
|
|
1325
|
+
|
|
1326
|
+
function flush(sessionId = null) {
|
|
1327
|
+
if (sessionId !== null) {
|
|
1328
|
+
const session = open(sessionId);
|
|
1329
|
+
if (!session) return 0;
|
|
1330
|
+
const before = session.retry.length;
|
|
1331
|
+
const remaining = drain(session);
|
|
1332
|
+
if (remaining !== before) notify(session.id);
|
|
1333
|
+
return remaining;
|
|
1334
|
+
}
|
|
1335
|
+
let remaining = 0;
|
|
1336
|
+
for (const session of sessions.values()) {
|
|
1337
|
+
const before = session.retry.length;
|
|
1338
|
+
const left = drain(session);
|
|
1339
|
+
if (left !== before) notify(session.id);
|
|
1340
|
+
remaining += left;
|
|
1341
|
+
}
|
|
1342
|
+
return remaining;
|
|
1343
|
+
}
|
|
1344
|
+
|
|
1345
|
+
function notify(sessionId) {
|
|
1346
|
+
for (const listener of [...listeners]) {
|
|
1347
|
+
try { listener(sessionId); } catch (error) {
|
|
1348
|
+
try { log('assistant-usage:listener-error', error?.message || String(error)); } catch { /* a listener cannot break accounting */ }
|
|
1349
|
+
}
|
|
1350
|
+
}
|
|
1351
|
+
}
|
|
1352
|
+
|
|
1353
|
+
const publicTask = (task) => (task ? { id: task.id, n: task.n, title: task.title, startedAt: task.startedAt } : null);
|
|
1354
|
+
|
|
1355
|
+
function startTask(session, { id, n, title, at }) {
|
|
1356
|
+
const task = newTask({ id, n, title, startedAt: at });
|
|
1357
|
+
session.tasks.set(id, task);
|
|
1358
|
+
session.maxN = Math.max(session.maxN, n);
|
|
1359
|
+
session.current = id;
|
|
1360
|
+
append(session, { t: 'task', id, n, at, title });
|
|
1361
|
+
return task;
|
|
1362
|
+
}
|
|
1363
|
+
|
|
1364
|
+
function beginTask(sessionId, { title = null, at = null } = {}) {
|
|
1365
|
+
if (!usageLedgerPath(dataDir, sessionId)) return null;
|
|
1366
|
+
dropped.delete(String(sessionId));
|
|
1367
|
+
const session = open(sessionId);
|
|
1368
|
+
if (!session) return null;
|
|
1369
|
+
const n = session.maxN + 1;
|
|
1370
|
+
const clean = typeof title === 'string' && title.trim() ? title.trim().slice(0, TITLE_MAX) : null;
|
|
1371
|
+
const task = startTask(session, { id: `task-${n}`, n, title: clean, at: count(at) || now() });
|
|
1372
|
+
notify(session.id);
|
|
1373
|
+
return { id: task.id, n: task.n };
|
|
1374
|
+
}
|
|
1375
|
+
|
|
1376
|
+
function currentTask(sessionId) {
|
|
1377
|
+
const session = open(sessionId);
|
|
1378
|
+
return session ? publicTask(session.tasks.get(session.current)) : null;
|
|
1379
|
+
}
|
|
1380
|
+
|
|
1381
|
+
function ensure(session) {
|
|
1382
|
+
return session.tasks.get(session.current) || startTask(session, { id: 'task-0', n: 0, title: null, at: now() });
|
|
1383
|
+
}
|
|
1384
|
+
|
|
1385
|
+
function ensureTask(sessionId) {
|
|
1386
|
+
const session = open(sessionId);
|
|
1387
|
+
return session ? publicTask(ensure(session)) : null;
|
|
1388
|
+
}
|
|
1389
|
+
|
|
1390
|
+
/** The task a caller named: an id, 'current' or nothing (both mean the current task). */
|
|
1391
|
+
function taskFor(session, taskId) {
|
|
1392
|
+
if (taskId && taskId !== 'current') {
|
|
1393
|
+
const task = session.tasks.get(String(taskId));
|
|
1394
|
+
if (task) return task;
|
|
1395
|
+
log('assistant-usage:unknown-task', `${session.id}: ${taskId}`);
|
|
1396
|
+
}
|
|
1397
|
+
return ensure(session);
|
|
1398
|
+
}
|
|
1399
|
+
|
|
1400
|
+
function settle(input = {}) {
|
|
1401
|
+
const session = open(input.sessionId);
|
|
1402
|
+
if (!session) return null;
|
|
1403
|
+
const tokens = cleanTokens(input.tokens);
|
|
1404
|
+
const cost = Number.isFinite(input.costUsd) && input.costUsd >= 0 ? Number(input.costUsd) : null;
|
|
1405
|
+
if (totalTokens(tokens) === 0 && !cost) return null;
|
|
1406
|
+
const task = taskFor(session, input.taskId);
|
|
1407
|
+
const scope = input.scope === 'brain' ? 'brain' : 'run';
|
|
1408
|
+
const row = {
|
|
1409
|
+
t: 'use', at: count(input.at) || now(), task: task.id, scope,
|
|
1410
|
+
run: scope === 'brain' ? null : (input.runId ? String(input.runId) : null),
|
|
1411
|
+
turn: Number.isInteger(input.turn) ? input.turn : null,
|
|
1412
|
+
provider: input.provider ? String(input.provider) : null,
|
|
1413
|
+
model: input.model ? String(input.model) : null,
|
|
1414
|
+
part: PARTS.includes(input.part) ? input.part : 'main',
|
|
1415
|
+
tokens: tokensToRow(tokens),
|
|
1416
|
+
cost,
|
|
1417
|
+
basis: input.costBasis ? String(input.costBasis) : null,
|
|
1418
|
+
fid: input.fidelity === 'partial' ? 'partial' : 'exact',
|
|
1419
|
+
why: input.reason ? String(input.reason) : null,
|
|
1420
|
+
src: input.source ? String(input.source) : null,
|
|
1421
|
+
};
|
|
1422
|
+
append(session, row);
|
|
1423
|
+
apply(session, row);
|
|
1424
|
+
if (row.part !== 'aux') session.pending.delete(agentKeyOf(scope, row.run)); // provisional tokens are booked now
|
|
1425
|
+
notify(session.id);
|
|
1426
|
+
return row;
|
|
1427
|
+
}
|
|
1428
|
+
|
|
1429
|
+
function setPending(sessionId, agentKey, input = {}) {
|
|
1430
|
+
const session = open(sessionId);
|
|
1431
|
+
const key = String(agentKey || '');
|
|
1432
|
+
if (!session || !key) return false;
|
|
1433
|
+
const tokens = cleanTokens(input.tokens);
|
|
1434
|
+
const known = session.pending.get(key);
|
|
1435
|
+
if (totalTokens(tokens) === 0) return clearPending(sessionId, key);
|
|
1436
|
+
const task = taskFor(session, input.taskId);
|
|
1437
|
+
const scope = key === 'brain' || input.scope === 'brain' ? 'brain' : 'run';
|
|
1438
|
+
const next = {
|
|
1439
|
+
taskId: task.id, scope, runId: scope === 'brain' ? null : String(input.runId || key),
|
|
1440
|
+
provider: input.provider ? String(input.provider) : null, model: input.model ? String(input.model) : null, tokens,
|
|
1441
|
+
};
|
|
1442
|
+
if (known && known.taskId === next.taskId && known.model === next.model && sameTokens(known.tokens, tokens)) return false;
|
|
1443
|
+
session.pending.set(key, next);
|
|
1444
|
+
if (task.id !== session.current && session.judged.get(task.id)?.closed) session.judged.delete(task.id); // a closed task is live again
|
|
1445
|
+
noteAgent(task, key);
|
|
1446
|
+
notify(session.id);
|
|
1447
|
+
return true;
|
|
1448
|
+
}
|
|
1449
|
+
|
|
1450
|
+
function clearPending(sessionId, agentKey) {
|
|
1451
|
+
const session = open(sessionId);
|
|
1452
|
+
if (!session || !session.pending.delete(String(agentKey || ''))) return false;
|
|
1453
|
+
notify(session.id);
|
|
1454
|
+
return true;
|
|
1455
|
+
}
|
|
1456
|
+
|
|
1457
|
+
/** The next task by number, whose start closes this task's time window. */
|
|
1458
|
+
function nextTask(session, task) {
|
|
1459
|
+
let next = null;
|
|
1460
|
+
for (const other of session.tasks.values()) if (other.n > task.n && (!next || other.n < next.n)) next = other;
|
|
1461
|
+
return next;
|
|
1462
|
+
}
|
|
1463
|
+
|
|
1464
|
+
/**
|
|
1465
|
+
* Jev judgments of a task ({ input, output, calls, costUsd }); closed tasks without pending freeze.
|
|
1466
|
+
* Never less than what the file holds for the task, and a larger count is written to it: the
|
|
1467
|
+
* judgment log is pruned, so a count that was shown must not shrink later. Without a reader the
|
|
1468
|
+
* file's count stands.
|
|
1469
|
+
*/
|
|
1470
|
+
function judged(session, task, liveTaskIds) {
|
|
1471
|
+
const saved = session.jev.get(task.id) || null;
|
|
1472
|
+
if (typeof judgments !== 'function') return saved;
|
|
1473
|
+
const closed = task.id !== session.current && !liveTaskIds.has(task.id);
|
|
1474
|
+
const cached = session.judged.get(task.id);
|
|
1475
|
+
if (cached && (closed ? cached.closed : now() - cached.at < JUDGMENT_TTL_MS)) return cached.value;
|
|
1476
|
+
let value = null;
|
|
1477
|
+
try {
|
|
1478
|
+
const answer = judgments({
|
|
1479
|
+
sessionId: session.id, taskId: task.id, sinceMs: task.startedAt, untilMs: nextTask(session, task)?.startedAt ?? null,
|
|
1480
|
+
runIds: [...task.agents.values()].filter((agent) => agent.scope === 'run' && agent.runId).map((agent) => agent.runId),
|
|
1481
|
+
});
|
|
1482
|
+
if (answer && typeof answer === 'object') value = { input: count(answer.input), output: count(answer.output), calls: count(answer.calls), costUsd: usd(answer.costUsd) };
|
|
1483
|
+
} catch (error) {
|
|
1484
|
+
log('assistant-usage:judgments-error', error?.message || String(error));
|
|
1485
|
+
}
|
|
1486
|
+
value = moreJudged(saved, value);
|
|
1487
|
+
session.judged.set(task.id, { at: now(), value, closed });
|
|
1488
|
+
const grew = value && (value.input || value.output || value.calls)
|
|
1489
|
+
&& (!saved || value.input > saved.input || value.output > saved.output || value.calls > saved.calls || value.costUsd > saved.costUsd);
|
|
1490
|
+
if (grew) {
|
|
1491
|
+
session.jev.set(task.id, value);
|
|
1492
|
+
append(session, { t: 'jev', at: now(), task: task.id, input: value.input, output: value.output, calls: value.calls, cost: roundUsd(value.costUsd) });
|
|
1493
|
+
}
|
|
1494
|
+
return value;
|
|
1495
|
+
}
|
|
1496
|
+
|
|
1497
|
+
/** Everything a view needs about one task: per-agent rows plus totals (settled + pending + judgments). */
|
|
1498
|
+
function summarize(session, task, liveTaskIds) {
|
|
1499
|
+
const agents = [];
|
|
1500
|
+
const models = new Map();
|
|
1501
|
+
const money = new Map(); // model → { costUsd, basis }
|
|
1502
|
+
let tokens = zeroTokens();
|
|
1503
|
+
let pending = zeroTokens();
|
|
1504
|
+
let costUsd = task.costUsd;
|
|
1505
|
+
const keys = [...task.order].sort((a, b) => Number(b === 'brain') - Number(a === 'brain'));
|
|
1506
|
+
for (const key of keys) {
|
|
1507
|
+
const agent = task.agents.get(key) || null;
|
|
1508
|
+
const held = session.pending.get(key);
|
|
1509
|
+
const mine = held && held.taskId === task.id ? held : null;
|
|
1510
|
+
if (!agent && !mine) continue;
|
|
1511
|
+
const agentPending = mine ? mine.tokens : zeroTokens();
|
|
1512
|
+
const agentModels = new Map(agent ? agent.models : []);
|
|
1513
|
+
if (mine) {
|
|
1514
|
+
const mKey = modelKey(mine.provider, mine.model);
|
|
1515
|
+
agentModels.set(mKey, addTokens(agentModels.get(mKey), agentPending));
|
|
1516
|
+
}
|
|
1517
|
+
let top = null;
|
|
1518
|
+
for (const [mKey, used] of agentModels) {
|
|
1519
|
+
models.set(mKey, addTokens(models.get(mKey), used));
|
|
1520
|
+
if (!top || totalTokens(used) > totalTokens(agentModels.get(top))) top = mKey;
|
|
1521
|
+
}
|
|
1522
|
+
for (const [mKey, held] of agent ? agent.money : []) {
|
|
1523
|
+
const known = money.get(mKey) || { costUsd: 0, basis: null };
|
|
1524
|
+
money.set(mKey, { costUsd: known.costUsd + held.costUsd, basis: weakerCostBasis(known.basis, held.basis) });
|
|
1525
|
+
}
|
|
1526
|
+
const all = addTokens(agent?.tokens, agentPending);
|
|
1527
|
+
const live = totalTokens(agentPending) > 0;
|
|
1528
|
+
tokens = addTokens(tokens, all);
|
|
1529
|
+
pending = addTokens(pending, agentPending);
|
|
1530
|
+
agents.push({
|
|
1531
|
+
key,
|
|
1532
|
+
scope: agent?.scope || mine.scope,
|
|
1533
|
+
runId: agent ? agent.runId : mine.runId,
|
|
1534
|
+
provider: agent?.provider || mine?.provider || null,
|
|
1535
|
+
model: top ? splitModelKey(top).model : null,
|
|
1536
|
+
tokens: withTotal(all),
|
|
1537
|
+
pending: withTotal(agentPending),
|
|
1538
|
+
subagents: { total: totalTokens(agent?.subagents) },
|
|
1539
|
+
fidelity: live ? 'live' : agent?.partial ? 'partial' : 'exact',
|
|
1540
|
+
partialReason: agent?.partial ? agent.partialReason : null,
|
|
1541
|
+
});
|
|
1542
|
+
}
|
|
1543
|
+
const jev = judged(session, task, liveTaskIds);
|
|
1544
|
+
if (jev && (jev.input || jev.output || jev.calls)) {
|
|
1545
|
+
const used = { ...zeroTokens(), input: jev.input, output: jev.output };
|
|
1546
|
+
tokens = addTokens(tokens, used);
|
|
1547
|
+
costUsd += jev.costUsd;
|
|
1548
|
+
models.set(modelKey('jev', 'jev'), used);
|
|
1549
|
+
// Jev reports no charge: its dollars are its tokens at the configured rate.
|
|
1550
|
+
money.set(modelKey('jev', 'jev'), { costUsd: jev.costUsd, basis: jev.costUsd > 0 ? 'estimated' : null });
|
|
1551
|
+
agents.push({
|
|
1552
|
+
key: 'judgments', scope: 'judgments', runId: null, provider: 'jev', model: 'jev',
|
|
1553
|
+
tokens: withTotal(used), pending: withTotal(zeroTokens()), subagents: { total: 0 }, fidelity: 'exact', partialReason: null, calls: jev.calls,
|
|
1554
|
+
});
|
|
1555
|
+
}
|
|
1556
|
+
const live = totalTokens(pending) > 0;
|
|
1557
|
+
const modelRows = [...models]
|
|
1558
|
+
.filter(([, used]) => totalTokens(used) > 0)
|
|
1559
|
+
.map(([mKey, used]) => ({ ...splitModelKey(mKey), tokens: withTotal(used), costUsd: roundUsd(money.get(mKey)?.costUsd), costBasis: money.get(mKey)?.basis || null }))
|
|
1560
|
+
.sort((a, b) => b.tokens.total - a.tokens.total);
|
|
1561
|
+
return {
|
|
1562
|
+
id: task.id, n: task.n, title: task.title, startedAt: task.startedAt,
|
|
1563
|
+
live, fidelity: live ? 'live' : task.partial ? 'partial' : 'exact',
|
|
1564
|
+
tokens: withTotal(tokens), pending: withTotal(pending), costUsd: roundUsd(costUsd),
|
|
1565
|
+
costBasis: modelRows.reduce((basis, row) => weakerCostBasis(basis, row.costBasis), null),
|
|
1566
|
+
agents,
|
|
1567
|
+
models: modelRows,
|
|
1568
|
+
};
|
|
1569
|
+
}
|
|
1570
|
+
|
|
1571
|
+
const brief = (summary) => ({
|
|
1572
|
+
id: summary.id, n: summary.n, title: summary.title, total: summary.tokens.total, inputTotal: summary.tokens.inputTotal, outputTotal: summary.tokens.outputTotal,
|
|
1573
|
+
costUsd: summary.costUsd, fidelity: summary.fidelity, live: summary.live,
|
|
1574
|
+
});
|
|
1575
|
+
|
|
1576
|
+
/**
|
|
1577
|
+
* Every task of the session summarized, oldest first, with the session totals: the tasks added
|
|
1578
|
+
* up, per class, per model and in dollars. Rows are only ever appended, so the settled part of
|
|
1579
|
+
* the total never goes down.
|
|
1580
|
+
*/
|
|
1581
|
+
function summarizeSession(session) {
|
|
1582
|
+
const liveTaskIds = new Set([...session.pending.values()].filter((held) => totalTokens(held.tokens) > 0).map((held) => held.taskId));
|
|
1583
|
+
const tasks = [...session.tasks.values()].sort((a, b) => a.n - b.n).map((task) => summarize(session, task, liveTaskIds));
|
|
1584
|
+
let tokens = zeroTokens();
|
|
1585
|
+
let pending = zeroTokens();
|
|
1586
|
+
let costUsd = 0;
|
|
1587
|
+
const models = new Map();
|
|
1588
|
+
for (const summary of tasks) {
|
|
1589
|
+
tokens = addTokens(tokens, summary.tokens);
|
|
1590
|
+
pending = addTokens(pending, summary.pending);
|
|
1591
|
+
costUsd += summary.costUsd;
|
|
1592
|
+
for (const row of summary.models) {
|
|
1593
|
+
const mKey = modelKey(row.provider, row.model);
|
|
1594
|
+
const known = models.get(mKey) || { tokens: zeroTokens(), costUsd: 0, basis: null };
|
|
1595
|
+
models.set(mKey, { tokens: addTokens(known.tokens, row.tokens), costUsd: known.costUsd + row.costUsd, basis: weakerCostBasis(known.basis, row.costBasis) });
|
|
1596
|
+
}
|
|
1597
|
+
}
|
|
1598
|
+
const modelRows = [...models]
|
|
1599
|
+
.map(([mKey, held]) => ({ ...splitModelKey(mKey), tokens: withTotal(held.tokens), costUsd: roundUsd(held.costUsd), costBasis: held.basis }))
|
|
1600
|
+
.sort((a, b) => b.tokens.total - a.tokens.total);
|
|
1601
|
+
const live = totalTokens(pending) > 0;
|
|
1602
|
+
const partial = [...session.tasks.values()].some((task) => task.partial);
|
|
1603
|
+
return {
|
|
1604
|
+
tasks, tokens: withTotal(tokens), pending: withTotal(pending), costUsd: roundUsd(costUsd),
|
|
1605
|
+
costBasis: modelRows.reduce((basis, row) => weakerCostBasis(basis, row.costBasis), null),
|
|
1606
|
+
live, fidelity: live ? 'live' : partial ? 'partial' : 'exact', models: modelRows,
|
|
1607
|
+
};
|
|
1608
|
+
}
|
|
1609
|
+
|
|
1610
|
+
/** The session block of a view: the headline. */
|
|
1611
|
+
const sessionBlock = (session, all) => ({
|
|
1612
|
+
tokens: all.tokens, pending: all.pending, costUsd: all.costUsd, costBasis: all.costBasis, live: all.live, fidelity: all.fidelity,
|
|
1613
|
+
tasks: all.tasks.length, unsynced: session.retry.length, models: all.models,
|
|
1614
|
+
});
|
|
1615
|
+
|
|
1616
|
+
function taskView(sessionId, taskId = 'current') {
|
|
1617
|
+
const session = open(sessionId);
|
|
1618
|
+
const none = withTotal(zeroTokens());
|
|
1619
|
+
const empty = { sessionId: String(sessionId || ''), task: null, session: { tokens: none, pending: none, costUsd: 0, costBasis: null, live: false, fidelity: 'exact', tasks: 0, unsynced: 0, models: [] }, recent: [] };
|
|
1620
|
+
if (!session) return empty;
|
|
1621
|
+
const all = summarizeSession(session);
|
|
1622
|
+
const wanted = taskId === 'current' || !taskId ? session.current : String(taskId);
|
|
1623
|
+
const task = all.tasks.find((summary) => summary.id === wanted) || null;
|
|
1624
|
+
return {
|
|
1625
|
+
sessionId: session.id,
|
|
1626
|
+
task,
|
|
1627
|
+
session: sessionBlock(session, all),
|
|
1628
|
+
recent: all.tasks.filter((summary) => summary.id !== wanted).reverse().slice(0, 5).map(brief),
|
|
1629
|
+
};
|
|
1630
|
+
}
|
|
1631
|
+
|
|
1632
|
+
function sessionView(sessionId) {
|
|
1633
|
+
const session = open(sessionId);
|
|
1634
|
+
const none = withTotal(zeroTokens());
|
|
1635
|
+
if (!session) return { sessionId: String(sessionId || ''), tokens: none, pending: none, costUsd: 0, costBasis: null, live: false, fidelity: 'exact', unsynced: 0, models: [], tasks: [] };
|
|
1636
|
+
const all = summarizeSession(session);
|
|
1637
|
+
const { tasks: _count, ...block } = sessionBlock(session, all);
|
|
1638
|
+
return { sessionId: session.id, ...block, tasks: all.tasks.map((summary) => ({ ...brief(summary), startedAt: summary.startedAt })) };
|
|
1639
|
+
}
|
|
1640
|
+
|
|
1641
|
+
function runView(sessionId, runId) {
|
|
1642
|
+
const session = open(sessionId);
|
|
1643
|
+
const key = String(runId || '');
|
|
1644
|
+
if (!session || !key) return null;
|
|
1645
|
+
let tokens = zeroTokens();
|
|
1646
|
+
let subagents = zeroTokens();
|
|
1647
|
+
let partial = false;
|
|
1648
|
+
const taskIds = [];
|
|
1649
|
+
for (const task of [...session.tasks.values()].sort((a, b) => a.n - b.n)) {
|
|
1650
|
+
const agent = task.agents.get(key);
|
|
1651
|
+
if (!agent || agent.scope !== 'run') continue;
|
|
1652
|
+
tokens = addTokens(tokens, agent.tokens);
|
|
1653
|
+
subagents = addTokens(subagents, agent.subagents);
|
|
1654
|
+
partial = partial || agent.partial;
|
|
1655
|
+
taskIds.push(task.id);
|
|
1656
|
+
}
|
|
1657
|
+
const held = session.pending.get(key);
|
|
1658
|
+
const pending = held && held.scope === 'run' ? held.tokens : zeroTokens();
|
|
1659
|
+
if (held && held.scope === 'run' && !taskIds.includes(held.taskId)) taskIds.push(held.taskId);
|
|
1660
|
+
if (!taskIds.length) return null;
|
|
1661
|
+
const live = totalTokens(pending) > 0;
|
|
1662
|
+
return {
|
|
1663
|
+
tokens: withTotal(addTokens(tokens, pending)), pending: withTotal(pending), subagents: { total: totalTokens(subagents) },
|
|
1664
|
+
fidelity: live ? 'live' : partial ? 'partial' : 'exact', taskIds,
|
|
1665
|
+
};
|
|
1666
|
+
}
|
|
1667
|
+
|
|
1668
|
+
function dropSession(sessionId) {
|
|
1669
|
+
const path = usageLedgerPath(dataDir, sessionId);
|
|
1670
|
+
if (!path) return false;
|
|
1671
|
+
const id = String(sessionId);
|
|
1672
|
+
sessions.delete(id);
|
|
1673
|
+
dropped.add(id);
|
|
1674
|
+
try { fs.rmSync(path, { force: true }); } catch (error) { log('assistant-usage:drop-error', error?.message || String(error)); return false; }
|
|
1675
|
+
notify(id);
|
|
1676
|
+
return true;
|
|
1677
|
+
}
|
|
1678
|
+
|
|
1679
|
+
return {
|
|
1680
|
+
beginTask, currentTask, ensureTask, settle, setPending, clearPending,
|
|
1681
|
+
taskView, sessionView, runView, flush, dropSession,
|
|
1682
|
+
subscribe(listener) {
|
|
1683
|
+
if (typeof listener !== 'function') return () => {};
|
|
1684
|
+
listeners.add(listener);
|
|
1685
|
+
return () => { listeners.delete(listener); };
|
|
1686
|
+
},
|
|
1687
|
+
};
|
|
1688
|
+
}
|