openclaw-hybrid-memory 2026.5.310 → 2026.6.10

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.
Files changed (535) hide show
  1. package/api/plugin-runtime.ts +2 -0
  2. package/backends/facts-db/contradictions.ts +1 -1
  3. package/cli/cmd-extract-directives.ts +225 -11
  4. package/cli/cmd-extract-proposals.ts +5 -6
  5. package/cli/cmd-extract-reinforcement.ts +71 -0
  6. package/cli/cmd-feedback.ts +15 -9
  7. package/cli/commands/manage/register-reflection-pipeline.ts +247 -13
  8. package/cli/commands/manage/register-storage-maintenance.ts +224 -15
  9. package/cli/commands/manage/storage-stats-helpers.ts +13 -2
  10. package/cli/context.ts +9 -19
  11. package/cli/distill.ts +31 -1
  12. package/cli/register.ts +28 -38
  13. package/dist/api/plugin-runtime.js.map +1 -1
  14. package/dist/backends/agent-health-store.js.map +1 -1
  15. package/dist/backends/apitap-store.js.map +1 -1
  16. package/dist/backends/audit-store.js.map +1 -1
  17. package/dist/backends/base-sqlite-store.js.map +1 -1
  18. package/dist/backends/cost-tracker.js.map +1 -1
  19. package/dist/backends/credentials-db.js +2 -3
  20. package/dist/backends/credentials-db.js.map +1 -1
  21. package/dist/backends/crystallization-store.js.map +1 -1
  22. package/dist/backends/edict-store.js.map +1 -1
  23. package/dist/backends/event-bus.js.map +1 -1
  24. package/dist/backends/event-log.js.map +1 -1
  25. package/dist/backends/facts-db/cache-manager.js.map +1 -1
  26. package/dist/backends/facts-db/clusters.js.map +1 -1
  27. package/dist/backends/facts-db/contradictions.js +1 -1
  28. package/dist/backends/facts-db/contradictions.js.map +1 -1
  29. package/dist/backends/facts-db/crud.js.map +1 -1
  30. package/dist/backends/facts-db/db-connection.js.map +1 -1
  31. package/dist/backends/facts-db/entity-autolink.js.map +1 -1
  32. package/dist/backends/facts-db/entity-layer.js.map +1 -1
  33. package/dist/backends/facts-db/episodes.js.map +1 -1
  34. package/dist/backends/facts-db/fact-queries.js.map +1 -1
  35. package/dist/backends/facts-db/fact-read-queries.js.map +1 -1
  36. package/dist/backends/facts-db/facts-db-layer1.js.map +1 -1
  37. package/dist/backends/facts-db/facts-db-layer2.js.map +1 -1
  38. package/dist/backends/facts-db/facts-db-layer3.js.map +1 -1
  39. package/dist/backends/facts-db/fts-text.js.map +1 -1
  40. package/dist/backends/facts-db/generated-skills/policy.js.map +1 -1
  41. package/dist/backends/facts-db/generated-skills.js.map +1 -1
  42. package/dist/backends/facts-db/housekeeping.js.map +1 -1
  43. package/dist/backends/facts-db/links.js.map +1 -1
  44. package/dist/backends/facts-db/maintenance.js.map +1 -1
  45. package/dist/backends/facts-db/procedures/crud.js.map +1 -1
  46. package/dist/backends/facts-db/procedures/internal.js.map +1 -1
  47. package/dist/backends/facts-db/procedures/promotion.js.map +1 -1
  48. package/dist/backends/facts-db/procedures/search.js.map +1 -1
  49. package/dist/backends/facts-db/procedures/stats.js.map +1 -1
  50. package/dist/backends/facts-db/reinforcement.js.map +1 -1
  51. package/dist/backends/facts-db/row-mapper.js.map +1 -1
  52. package/dist/backends/facts-db/scan-cursors.js.map +1 -1
  53. package/dist/backends/facts-db/schema-bootstrap.js.map +1 -1
  54. package/dist/backends/facts-db/scope-sql.js.map +1 -1
  55. package/dist/backends/facts-db/search.js.map +1 -1
  56. package/dist/backends/facts-db/stats.js.map +1 -1
  57. package/dist/backends/facts-db/types.js.map +1 -1
  58. package/dist/backends/facts-db/variants.js.map +1 -1
  59. package/dist/backends/identity-reflection-store.js.map +1 -1
  60. package/dist/backends/issue-store.js.map +1 -1
  61. package/dist/backends/learnings-db.js.map +1 -1
  62. package/dist/backends/migrations/facts-migrations.js.map +1 -1
  63. package/dist/backends/migrations/procedures.js.map +1 -1
  64. package/dist/backends/narratives-db.js.map +1 -1
  65. package/dist/backends/persona-state-store.js.map +1 -1
  66. package/dist/backends/proposals-db.js.map +1 -1
  67. package/dist/backends/scope-filter-sql.js.map +1 -1
  68. package/dist/backends/sqlite-schema-meta.js.map +1 -1
  69. package/dist/backends/tool-proposal-store.js.map +1 -1
  70. package/dist/backends/vector-db/constants.js.map +1 -1
  71. package/dist/backends/vector-db/path-utils.js.map +1 -1
  72. package/dist/backends/vector-db/runtime-locks.js.map +1 -1
  73. package/dist/backends/vector-db/vector-db-class.js.map +1 -1
  74. package/dist/backends/wal.js.map +1 -1
  75. package/dist/backends/workflow-store.js.map +1 -1
  76. package/dist/benchmark/shadow-eval.js.map +1 -1
  77. package/dist/cli/active-tasks.js.map +1 -1
  78. package/dist/cli/backup.js.map +1 -1
  79. package/dist/cli/benchmark.js.map +1 -1
  80. package/dist/cli/cmd-backfill.js.map +1 -1
  81. package/dist/cli/cmd-config.js.map +1 -1
  82. package/dist/cli/cmd-credentials.js.map +1 -1
  83. package/dist/cli/cmd-demo.js.map +1 -1
  84. package/dist/cli/cmd-distill.js.map +1 -1
  85. package/dist/cli/cmd-doctor.js.map +1 -1
  86. package/dist/cli/cmd-examples.js.map +1 -1
  87. package/dist/cli/cmd-extract-daily.js.map +1 -1
  88. package/dist/cli/cmd-extract-directives.js +141 -10
  89. package/dist/cli/cmd-extract-directives.js.map +1 -1
  90. package/dist/cli/cmd-extract-procedures.js.map +1 -1
  91. package/dist/cli/cmd-extract-proposals.js +3 -2
  92. package/dist/cli/cmd-extract-proposals.js.map +1 -1
  93. package/dist/cli/cmd-extract-reinforcement.js +39 -0
  94. package/dist/cli/cmd-extract-reinforcement.js.map +1 -1
  95. package/dist/cli/cmd-extract-sessions.js.map +1 -1
  96. package/dist/cli/cmd-feedback.js +9 -4
  97. package/dist/cli/cmd-feedback.js.map +1 -1
  98. package/dist/cli/cmd-health.js.map +1 -1
  99. package/dist/cli/cmd-providers.js.map +1 -1
  100. package/dist/cli/cmd-selfcorrection.js.map +1 -1
  101. package/dist/cli/cmd-setup.js.map +1 -1
  102. package/dist/cli/cmd-status.js.map +1 -1
  103. package/dist/cli/cmd-store.js.map +1 -1
  104. package/dist/cli/cmd-user-friendly.js.map +1 -1
  105. package/dist/cli/cmd-verify.js.map +1 -1
  106. package/dist/cli/commands/manage/bindings.js.map +1 -1
  107. package/dist/cli/commands/manage/dream-cycle-followup.js.map +1 -1
  108. package/dist/cli/commands/manage/maintenance-heartbeat.js.map +1 -1
  109. package/dist/cli/commands/manage/register-agents-audit-runall.js.map +1 -1
  110. package/dist/cli/commands/manage/register-analyze-maintenance-logs.js.map +1 -1
  111. package/dist/cli/commands/manage/register-budget-proposals.js.map +1 -1
  112. package/dist/cli/commands/manage/register-config-cli.js.map +1 -1
  113. package/dist/cli/commands/manage/register-corrections-and-pipeline.js.map +1 -1
  114. package/dist/cli/commands/manage/register-corrections.js.map +1 -1
  115. package/dist/cli/commands/manage/register-council.js.map +1 -1
  116. package/dist/cli/commands/manage/register-credentials-scope.js.map +1 -1
  117. package/dist/cli/commands/manage/register-digest.js.map +1 -1
  118. package/dist/cli/commands/manage/register-lifecycle.js.map +1 -1
  119. package/dist/cli/commands/manage/register-procedure-lifecycle.js.map +1 -1
  120. package/dist/cli/commands/manage/register-reconcile-cron-ledgers.js.map +1 -1
  121. package/dist/cli/commands/manage/register-reflection-pipeline.js +144 -7
  122. package/dist/cli/commands/manage/register-reflection-pipeline.js.map +1 -1
  123. package/dist/cli/commands/manage/register-self-correction-feedback.js.map +1 -1
  124. package/dist/cli/commands/manage/register-storage-and-stats.js.map +1 -1
  125. package/dist/cli/commands/manage/register-storage-entities-decay.js.map +1 -1
  126. package/dist/cli/commands/manage/register-storage-graph-audit.js.map +1 -1
  127. package/dist/cli/commands/manage/register-storage-maintenance.js +152 -9
  128. package/dist/cli/commands/manage/register-storage-maintenance.js.map +1 -1
  129. package/dist/cli/commands/manage/register-validate-cron-exit.js.map +1 -1
  130. package/dist/cli/commands/manage/storage-stats-helpers.js +10 -3
  131. package/dist/cli/commands/manage/storage-stats-helpers.js.map +1 -1
  132. package/dist/cli/commands/register-manage-commands.js.map +1 -1
  133. package/dist/cli/config-feature-summaries.js.map +1 -1
  134. package/dist/cli/config-output-sink.js.map +1 -1
  135. package/dist/cli/distill-session-jsonl.js.map +1 -1
  136. package/dist/cli/distill.js +10 -1
  137. package/dist/cli/distill.js.map +1 -1
  138. package/dist/cli/global-verbose.js.map +1 -1
  139. package/dist/cli/goals.js.map +1 -1
  140. package/dist/cli/hybrid-mem-commander-utils.js.map +1 -1
  141. package/dist/cli/install/config-merge.js.map +1 -1
  142. package/dist/cli/install/cron-jobs.js.map +1 -1
  143. package/dist/cli/install/embedding-detect.js.map +1 -1
  144. package/dist/cli/install/run-install.js.map +1 -1
  145. package/dist/cli/install/workspace.js.map +1 -1
  146. package/dist/cli/proposals.js.map +1 -1
  147. package/dist/cli/register.js.map +1 -1
  148. package/dist/cli/shared.js.map +1 -1
  149. package/dist/cli/skills.js.map +1 -1
  150. package/dist/cli/task-queue-status.js.map +1 -1
  151. package/dist/cli/verified.js.map +1 -1
  152. package/dist/cli/verify/fact-count.js.map +1 -1
  153. package/dist/cli/verify/openclaw-config.js.map +1 -1
  154. package/dist/cli/verify/plugin-config-credentials.js.map +1 -1
  155. package/dist/cli/verify/sections/config-cron.js.map +1 -1
  156. package/dist/cli/verify/sections/embeddings.js.map +1 -1
  157. package/dist/cli/verify/sections/infrastructure.js.map +1 -1
  158. package/dist/cli/verify/sections/llm-models.js.map +1 -1
  159. package/dist/cli/verify/sections/reconcile.js.map +1 -1
  160. package/dist/cli/verify/verify-run-state.js.map +1 -1
  161. package/dist/cli/verify-llm-azure-auth.js.map +1 -1
  162. package/dist/cli/verify.js.map +1 -1
  163. package/dist/config/hybrid-schema.js.map +1 -1
  164. package/dist/config/index.js.map +1 -1
  165. package/dist/config/maintenance-fallback-policy.js.map +1 -1
  166. package/dist/config/parsers/capture.js.map +1 -1
  167. package/dist/config/parsers/core.js.map +1 -1
  168. package/dist/config/parsers/features.js.map +1 -1
  169. package/dist/config/parsers/index.js.map +1 -1
  170. package/dist/config/parsers/maintenance.js.map +1 -1
  171. package/dist/config/parsers/retrieval.js.map +1 -1
  172. package/dist/config/parsers/sensors.js.map +1 -1
  173. package/dist/config/skill-sections.js.map +1 -1
  174. package/dist/config/skill-size-limits.js.map +1 -1
  175. package/dist/config/types/agents.js.map +1 -1
  176. package/dist/config/types/bootstrap.js.map +1 -1
  177. package/dist/config/types/core.js.map +1 -1
  178. package/dist/config/types/index.js.map +1 -1
  179. package/dist/config/utils.js.map +1 -1
  180. package/dist/index-help.js.map +1 -1
  181. package/dist/index-testing-exports.js.map +1 -1
  182. package/dist/index.d.ts +1 -1
  183. package/dist/index.js +2 -2
  184. package/dist/index.js.map +1 -1
  185. package/dist/lifecycle/hook-resolution-api.js.map +1 -1
  186. package/dist/lifecycle/hooks.js +0 -1
  187. package/dist/lifecycle/hooks.js.map +1 -1
  188. package/dist/lifecycle/resolve-agent-id.js.map +1 -1
  189. package/dist/lifecycle/session-state.js.map +1 -1
  190. package/dist/lifecycle/stage-active-task.js.map +1 -1
  191. package/dist/lifecycle/stage-auth-failure.js.map +1 -1
  192. package/dist/lifecycle/stage-capture/run-capture.js.map +1 -1
  193. package/dist/lifecycle/stage-capture.js.map +1 -1
  194. package/dist/lifecycle/stage-cleanup.js.map +1 -1
  195. package/dist/lifecycle/stage-credential-hint.js.map +1 -1
  196. package/dist/lifecycle/stage-frustration.js.map +1 -1
  197. package/dist/lifecycle/stage-goal-stewardship.js.map +1 -1
  198. package/dist/lifecycle/stage-goal-subagent.js.map +1 -1
  199. package/dist/lifecycle/stage-injection.js +1 -1
  200. package/dist/lifecycle/stage-injection.js.map +1 -1
  201. package/dist/lifecycle/stage-recall/run-recall.js.map +1 -1
  202. package/dist/lifecycle/stage-recall.js.map +1 -1
  203. package/dist/lifecycle/stage-setup.js.map +1 -1
  204. package/dist/routes/dashboard/collectors.js.map +1 -1
  205. package/dist/routes/dashboard/html.js.map +1 -1
  206. package/dist/routes/dashboard/server.js.map +1 -1
  207. package/dist/routes/dashboard-graph.js.map +1 -1
  208. package/dist/routes/graphql-resolvers.js.map +1 -1
  209. package/dist/routes/graphql-server.js.map +1 -1
  210. package/dist/services/active-task-checkpoint.js.map +1 -1
  211. package/dist/services/active-task-injection.js.map +1 -1
  212. package/dist/services/active-task.js.map +1 -1
  213. package/dist/services/adaptive-catch-up-pacing.js +25 -0
  214. package/dist/services/adaptive-catch-up-pacing.js.map +1 -0
  215. package/dist/services/adaptive-maintenance-llm.js.map +1 -1
  216. package/dist/services/adaptive-model-limits.js.map +1 -1
  217. package/dist/services/ambient-retrieval.js.map +1 -1
  218. package/dist/services/apitap-service.js.map +1 -1
  219. package/dist/services/audit-health-exit-info.js.map +1 -1
  220. package/dist/services/audit-health-json.js.map +1 -1
  221. package/dist/services/auth-failure-detect.js.map +1 -1
  222. package/dist/services/auto-capture.js.map +1 -1
  223. package/dist/services/auto-classifier.js.map +1 -1
  224. package/dist/services/auto-skills-audit.js.map +1 -1
  225. package/dist/services/bootstrap-optional.js.map +1 -1
  226. package/dist/services/bootstrap-priority.js.map +1 -1
  227. package/dist/services/bootstrap.js.map +1 -1
  228. package/dist/services/capture-provenance.js.map +1 -1
  229. package/dist/services/capture-utils.js.map +1 -1
  230. package/dist/services/chat.js +22 -3
  231. package/dist/services/chat.js.map +1 -1
  232. package/dist/services/classification-scope.js.map +1 -1
  233. package/dist/services/classification.js.map +1 -1
  234. package/dist/services/cli-sql-dump.js.map +1 -1
  235. package/dist/services/consolidation.js.map +1 -1
  236. package/dist/services/context-audit.js +1 -1
  237. package/dist/services/context-audit.js.map +1 -1
  238. package/dist/services/context-budget.js.map +1 -1
  239. package/dist/services/context-engine.js.map +1 -1
  240. package/dist/services/contextual-variants.js.map +1 -1
  241. package/dist/services/continuous-verifier.js.map +1 -1
  242. package/dist/services/contradiction-adjudicator.js.map +1 -1
  243. package/dist/services/cost-context.js.map +1 -1
  244. package/dist/services/cost-feature-labels.js.map +1 -1
  245. package/dist/services/credential-migration.js.map +1 -1
  246. package/dist/services/credential-scanner.js.map +1 -1
  247. package/dist/services/credential-validation.js.map +1 -1
  248. package/dist/services/cron-exit-validator.js.map +1 -1
  249. package/dist/services/cron-guard.js.map +1 -1
  250. package/dist/services/cron-job-bash-harness.js +52 -5
  251. package/dist/services/cron-job-bash-harness.js.map +1 -1
  252. package/dist/services/cron-maintenance-reconciler.js +1 -3
  253. package/dist/services/cron-maintenance-reconciler.js.map +1 -1
  254. package/dist/services/cross-agent-learning.js.map +1 -1
  255. package/dist/services/crystallization-proposer.js.map +1 -1
  256. package/dist/services/dedupe-policy.js.map +1 -1
  257. package/dist/services/deprecated-cron-commands.js.map +1 -1
  258. package/dist/services/directive-extract.js.map +1 -1
  259. package/dist/services/document-chunker.js.map +1 -1
  260. package/dist/services/document-grader.js.map +1 -1
  261. package/dist/services/dream-cycle.js.map +1 -1
  262. package/dist/services/embedding-migration.js.map +1 -1
  263. package/dist/services/embedding-registry.js.map +1 -1
  264. package/dist/services/embeddings/chain-provider.js.map +1 -1
  265. package/dist/services/embeddings/factory.js.map +1 -1
  266. package/dist/services/embeddings/fallback-provider.js.map +1 -1
  267. package/dist/services/embeddings/ollama-provider.js.map +1 -1
  268. package/dist/services/embeddings/onnx-provider.js.map +1 -1
  269. package/dist/services/embeddings/openai-provider.js.map +1 -1
  270. package/dist/services/embeddings/shared.js +3 -3
  271. package/dist/services/embeddings/shared.js.map +1 -1
  272. package/dist/services/embeddings/types.js.map +1 -1
  273. package/dist/services/entity-enrichment-adaptive.js +128 -0
  274. package/dist/services/entity-enrichment-adaptive.js.map +1 -0
  275. package/dist/services/entity-enrichment-cli.js +389 -42
  276. package/dist/services/entity-enrichment-cli.js.map +1 -1
  277. package/dist/services/entity-enrichment.js +31 -5
  278. package/dist/services/entity-enrichment.js.map +1 -1
  279. package/dist/services/error-reporter/noisy-errors.js.map +1 -1
  280. package/dist/services/error-reporter/sanitize.js.map +1 -1
  281. package/dist/services/error-reporter.js.map +1 -1
  282. package/dist/services/event-hub-repair.js.map +1 -1
  283. package/dist/services/export-memory.js.map +1 -1
  284. package/dist/services/fact-extraction.js.map +1 -1
  285. package/dist/services/feedback-effectiveness.js.map +1 -1
  286. package/dist/services/find-duplicates.js.map +1 -1
  287. package/dist/services/frustration-detector.js.map +1 -1
  288. package/dist/services/fts-search.js.map +1 -1
  289. package/dist/services/gap-detector.js.map +1 -1
  290. package/dist/services/generated-skill-lifecycle.js.map +1 -1
  291. package/dist/services/generated-skill-validation.js.map +1 -1
  292. package/dist/services/goal-active-task-mirror.js.map +1 -1
  293. package/dist/services/goal-circuit-breaker.js.map +1 -1
  294. package/dist/services/goal-health.js.map +1 -1
  295. package/dist/services/goal-registry.js.map +1 -1
  296. package/dist/services/goal-stewardship-heartbeat.js.map +1 -1
  297. package/dist/services/goal-stewardship-llm-triage.js.map +1 -1
  298. package/dist/services/goal-stewardship-verify-cron.js.map +1 -1
  299. package/dist/services/goal-stewardship.js.map +1 -1
  300. package/dist/services/goal-subagent.js.map +1 -1
  301. package/dist/services/graph-retrieval.js.map +1 -1
  302. package/dist/services/humanizer-score.js.map +1 -1
  303. package/dist/services/hybrid-mem-cron-default-job-steps.js.map +1 -1
  304. package/dist/services/hyde-helper.js.map +1 -1
  305. package/dist/services/identity-reflection.js.map +1 -1
  306. package/dist/services/implicit-feedback-extract.js.map +1 -1
  307. package/dist/services/index.js.map +1 -1
  308. package/dist/services/ingest-utils.js.map +1 -1
  309. package/dist/services/intent-template.js.map +1 -1
  310. package/dist/services/json-array-parser.js.map +1 -1
  311. package/dist/services/knowledge-gaps.js.map +1 -1
  312. package/dist/services/language-keywords-build.js.map +1 -1
  313. package/dist/services/lifecycle/github-adapter.js.map +1 -1
  314. package/dist/services/llm-rate-limit-headers.js +1 -2
  315. package/dist/services/llm-rate-limit-headers.js.map +1 -1
  316. package/dist/services/maintenance-auto-fix.js.map +1 -1
  317. package/dist/services/maintenance-log-analyzer.js +7 -1
  318. package/dist/services/maintenance-log-analyzer.js.map +1 -1
  319. package/dist/services/maintenance-timestamp.js.map +1 -1
  320. package/dist/services/memory-diagnostics.js.map +1 -1
  321. package/dist/services/memory-index.js.map +1 -1
  322. package/dist/services/merge-results.js.map +1 -1
  323. package/dist/services/model-capabilities.js.map +1 -1
  324. package/dist/services/model-pricing.js.map +1 -1
  325. package/dist/services/narrative-recall.js.map +1 -1
  326. package/dist/services/openclaw-session-artifact.js.map +1 -1
  327. package/dist/services/passive-observer.js.map +1 -1
  328. package/dist/services/pattern-detector-hash.js.map +1 -1
  329. package/dist/services/pattern-detector.js.map +1 -1
  330. package/dist/services/pending-autopilot/foundation.js.map +1 -1
  331. package/dist/services/pending-autopilot/redaction.js.map +1 -1
  332. package/dist/services/pending-autopilot/store.js.map +1 -1
  333. package/dist/services/pending-autopilot/types.js.map +1 -1
  334. package/dist/services/pending-digest-autopilot-cron.js.map +1 -1
  335. package/dist/services/pending-digest-autopilot.js.map +1 -1
  336. package/dist/services/pending-review-digest.js.map +1 -1
  337. package/dist/services/persona-proposal-triage.js.map +1 -1
  338. package/dist/services/persona-state-promotion.js.map +1 -1
  339. package/dist/services/post-compaction-recall.js.map +1 -1
  340. package/dist/services/pre-consolidation-flush.js.map +1 -1
  341. package/dist/services/pre-finalization-guard.js.map +1 -1
  342. package/dist/services/procedure-cluster.js.map +1 -1
  343. package/dist/services/procedure-extractor.js.map +1 -1
  344. package/dist/services/procedure-promotion/duplicate-skill-cache.js.map +1 -1
  345. package/dist/services/procedure-promotion-policy.js.map +1 -1
  346. package/dist/services/procedure-selection-metrics.js.map +1 -1
  347. package/dist/services/procedure-skill-eval.js.map +1 -1
  348. package/dist/services/procedure-skill-generator.js.map +1 -1
  349. package/dist/services/procedure-skill-recipe.js.map +1 -1
  350. package/dist/services/procedure-skill-shrink.js.map +1 -1
  351. package/dist/services/procedure-skill-workflow.js.map +1 -1
  352. package/dist/services/provenance.js.map +1 -1
  353. package/dist/services/public-export-bundle.js.map +1 -1
  354. package/dist/services/python-bridge.js.map +1 -1
  355. package/dist/services/query-expander.js.map +1 -1
  356. package/dist/services/query-validator.js.map +1 -1
  357. package/dist/services/recall-pipeline.js.map +1 -1
  358. package/dist/services/recall-timing.js.map +1 -1
  359. package/dist/services/recent-http-attempts.js.map +1 -1
  360. package/dist/services/reflection/shared.js.map +1 -1
  361. package/dist/services/reflection.js.map +1 -1
  362. package/dist/services/reinforcement-extract.js.map +1 -1
  363. package/dist/services/reranker.js.map +1 -1
  364. package/dist/services/responses-adapter.js.map +1 -1
  365. package/dist/services/retrieval-aliases.js.map +1 -1
  366. package/dist/services/retrieval-mode-policy.js.map +1 -1
  367. package/dist/services/retrieval-orchestrator/packing.js.map +1 -1
  368. package/dist/services/retrieval-orchestrator.d.ts +2 -3
  369. package/dist/services/retrieval-orchestrator.js.map +1 -1
  370. package/dist/services/rrf-fusion.js.map +1 -1
  371. package/dist/services/self-correction-extract.js.map +1 -1
  372. package/dist/services/session-observability.js.map +1 -1
  373. package/dist/services/session-pre-filter.js.map +1 -1
  374. package/dist/services/shortest-path.js.map +1 -1
  375. package/dist/services/skill-allowed-tools.js.map +1 -1
  376. package/dist/services/skill-creator-validator.js.map +1 -1
  377. package/dist/services/skill-crystallizer-helpers.js.map +1 -1
  378. package/dist/services/skill-crystallizer.js.map +1 -1
  379. package/dist/services/skill-description-builder.js.map +1 -1
  380. package/dist/services/skill-eval-synthesizer.js.map +1 -1
  381. package/dist/services/skill-examples-builder.js.map +1 -1
  382. package/dist/services/skill-frontmatter.js.map +1 -1
  383. package/dist/services/skill-name-validator.js.map +1 -1
  384. package/dist/services/skill-prompt-injection.js.map +1 -1
  385. package/dist/services/skill-reference-sidecar.js.map +1 -1
  386. package/dist/services/skill-script-bundler.js.map +1 -1
  387. package/dist/services/skill-validator.js.map +1 -1
  388. package/dist/services/startup-memory-attribution.js.map +1 -1
  389. package/dist/services/task-hygiene.js.map +1 -1
  390. package/dist/services/task-ledger/canonical.js.map +1 -1
  391. package/dist/services/task-ledger-facts.js.map +1 -1
  392. package/dist/services/task-queue-leases.js.map +1 -1
  393. package/dist/services/task-queue-watchdog.js.map +1 -1
  394. package/dist/services/tool-effectiveness.js.map +1 -1
  395. package/dist/services/tool-proposer.js.map +1 -1
  396. package/dist/services/tools-md-section.js.map +1 -1
  397. package/dist/services/topic-clusters.js.map +1 -1
  398. package/dist/services/trajectory-tracker.js.map +1 -1
  399. package/dist/services/vector-backend-observability.js.map +1 -1
  400. package/dist/services/vector-lifecycle-audit.js.map +1 -1
  401. package/dist/services/vector-maintenance.js.map +1 -1
  402. package/dist/services/vector-search.js.map +1 -1
  403. package/dist/services/verification-store.js.map +1 -1
  404. package/dist/services/verified-fact-triage.js.map +1 -1
  405. package/dist/services/wal-helpers.js.map +1 -1
  406. package/dist/services/workflow-tracker.js.map +1 -1
  407. package/dist/setup/bootstrap-databases.js.map +1 -1
  408. package/dist/setup/cli-context/cli-services.js.map +1 -1
  409. package/dist/setup/cli-context/help-text.js.map +1 -1
  410. package/dist/setup/cli-context/metadata.js.map +1 -1
  411. package/dist/setup/cli-context/register-cli-with-help.js.map +1 -1
  412. package/dist/setup/cli-context/register-full.js.map +1 -1
  413. package/dist/setup/cli-context/register-help.js.map +1 -1
  414. package/dist/setup/cost-instrumentation.js.map +1 -1
  415. package/dist/setup/hybrid-memory-generation-state.js.map +1 -1
  416. package/dist/setup/hybrid-memory-reload-coordinator.js +13 -13
  417. package/dist/setup/hybrid-memory-reload-coordinator.js.map +1 -1
  418. package/dist/setup/plugin-service.js.map +1 -1
  419. package/dist/setup/provider-router.js.map +1 -1
  420. package/dist/setup/register-context-engine.js.map +1 -1
  421. package/dist/setup/register-hooks.js.map +1 -1
  422. package/dist/setup/register-plugin.js +25 -21
  423. package/dist/setup/register-plugin.js.map +1 -1
  424. package/dist/setup/register-tools.js.map +1 -1
  425. package/dist/setup/reregister-policy.js +2 -2
  426. package/dist/setup/reregister-policy.js.map +1 -1
  427. package/dist/setup/tool-installers.js.map +1 -1
  428. package/dist/setup/workspace-bootstrap.js.map +1 -1
  429. package/dist/src/worker/narratives.js.map +1 -1
  430. package/dist/tools/apitap-tools.js.map +1 -1
  431. package/dist/tools/credential-tools.js.map +1 -1
  432. package/dist/tools/crystallization-tools.js.map +1 -1
  433. package/dist/tools/dashboard-routes.js.map +1 -1
  434. package/dist/tools/document-tools.js.map +1 -1
  435. package/dist/tools/goal-tools.js.map +1 -1
  436. package/dist/tools/graph-tools.js.map +1 -1
  437. package/dist/tools/issue-tools.js.map +1 -1
  438. package/dist/tools/memory/build-runtime.js.map +1 -1
  439. package/dist/tools/memory/helpers.js.map +1 -1
  440. package/dist/tools/memory/register-checkpoint-tools.js.map +1 -1
  441. package/dist/tools/memory/register-directory-tools.js.map +1 -1
  442. package/dist/tools/memory/register-edict-tools.js.map +1 -1
  443. package/dist/tools/memory/register-episode-tools.js.map +1 -1
  444. package/dist/tools/memory/register-recall-tools.js.map +1 -1
  445. package/dist/tools/memory/register-store-tools.js.map +1 -1
  446. package/dist/tools/memory-tools.js.map +1 -1
  447. package/dist/tools/persona-tools.js.map +1 -1
  448. package/dist/tools/provenance-tools.js.map +1 -1
  449. package/dist/tools/public-api-routes.js.map +1 -1
  450. package/dist/tools/safe-register-http-route.js.map +1 -1
  451. package/dist/tools/self-extension-tools.js.map +1 -1
  452. package/dist/tools/task-hygiene-tools.js.map +1 -1
  453. package/dist/tools/utility-tools.js.map +1 -1
  454. package/dist/tools/verification-tools.js.map +1 -1
  455. package/dist/tools/workflow-tools.js.map +1 -1
  456. package/dist/types/issue-types.js.map +1 -1
  457. package/dist/types/learnings-types.js.map +1 -1
  458. package/dist/types/memory.js.map +1 -1
  459. package/dist/utils/apim-gateway-fetch.js.map +1 -1
  460. package/dist/utils/atomic-write.js.map +1 -1
  461. package/dist/utils/auth-failover.js.map +1 -1
  462. package/dist/utils/auth.js.map +1 -1
  463. package/dist/utils/compaction-model-watchdog.js.map +1 -1
  464. package/dist/utils/consolidation-controls.js.map +1 -1
  465. package/dist/utils/constants.js.map +1 -1
  466. package/dist/utils/date-detector.js.map +1 -1
  467. package/dist/utils/dates.js.map +1 -1
  468. package/dist/utils/decay.js.map +1 -1
  469. package/dist/utils/duration.js.map +1 -1
  470. package/dist/utils/embed-call.js.map +1 -1
  471. package/dist/utils/entity-lookup-resolve.js.map +1 -1
  472. package/dist/utils/entity-mention-quality.js.map +1 -1
  473. package/dist/utils/entity-stopwords.js.map +1 -1
  474. package/dist/utils/env-manager.js.map +1 -1
  475. package/dist/utils/error-tracking.js.map +1 -1
  476. package/dist/utils/event-loop-yield.js.map +1 -1
  477. package/dist/utils/extract-last-user-message.js.map +1 -1
  478. package/dist/utils/extraction-from-template.js.map +1 -1
  479. package/dist/utils/fact-embeddings.js.map +1 -1
  480. package/dist/utils/file-snapshot.js.map +1 -1
  481. package/dist/utils/format.js.map +1 -1
  482. package/dist/utils/fs.js.map +1 -1
  483. package/dist/utils/gh-repo-arg.js.map +1 -1
  484. package/dist/utils/hybrid-mem-json-cli.js.map +1 -1
  485. package/dist/utils/language-keywords.js.map +1 -1
  486. package/dist/utils/lifecycle-generation.js.map +1 -1
  487. package/dist/utils/llm-json-array.js.map +1 -1
  488. package/dist/utils/llm-selection.js.map +1 -1
  489. package/dist/utils/logger.js.map +1 -1
  490. package/dist/utils/model-provider-family.js.map +1 -1
  491. package/dist/utils/model-tier.js.map +1 -1
  492. package/dist/utils/openclaw-agent-defaults.js.map +1 -1
  493. package/dist/utils/path.js.map +1 -1
  494. package/dist/utils/plugin-root.js.map +1 -1
  495. package/dist/utils/plugin-update-check.js.map +1 -1
  496. package/dist/utils/procedure-risk.js.map +1 -1
  497. package/dist/utils/progress-indicators.js.map +1 -1
  498. package/dist/utils/prompt-loader.js.map +1 -1
  499. package/dist/utils/provenance.js.map +1 -1
  500. package/dist/utils/provider-detection.js.map +1 -1
  501. package/dist/utils/registration-superseded.js.map +1 -1
  502. package/dist/utils/salience.js.map +1 -1
  503. package/dist/utils/sanitize-messages.js.map +1 -1
  504. package/dist/utils/scope-filter.js.map +1 -1
  505. package/dist/utils/skill-discovery.js.map +1 -1
  506. package/dist/utils/sqlite-file-perms.js.map +1 -1
  507. package/dist/utils/sqlite-outcome-compat.js.map +1 -1
  508. package/dist/utils/sqlite-transaction.js.map +1 -1
  509. package/dist/utils/stable-stringify.js.map +1 -1
  510. package/dist/utils/subagent-ended-utils.js.map +1 -1
  511. package/dist/utils/tags.js.map +1 -1
  512. package/dist/utils/text.js.map +1 -1
  513. package/dist/utils/timeout.js.map +1 -1
  514. package/dist/utils/typebox.js.map +1 -1
  515. package/dist/utils/version-check.js.map +1 -1
  516. package/dist/utils/wal-replay.js.map +1 -1
  517. package/dist/versionInfo.js.map +1 -1
  518. package/index.ts +2 -2
  519. package/lifecycle/hooks.ts +0 -1
  520. package/npm-shrinkwrap.json +487 -186
  521. package/openclaw.plugin.json +1 -1
  522. package/package.json +2 -2
  523. package/services/adaptive-catch-up-pacing.ts +28 -0
  524. package/services/chat.ts +34 -1
  525. package/services/cron-job-bash-harness.ts +52 -5
  526. package/services/embeddings/shared.ts +5 -2
  527. package/services/entity-enrichment-adaptive.ts +245 -0
  528. package/services/entity-enrichment-cli.ts +553 -47
  529. package/services/entity-enrichment.ts +43 -2
  530. package/services/llm-rate-limit-headers.ts +1 -4
  531. package/services/maintenance-log-analyzer.ts +13 -9
  532. package/services/reinforcement-extract.ts +19 -0
  533. package/setup/hybrid-memory-reload-coordinator.ts +26 -0
  534. package/setup/register-plugin.ts +62 -32
  535. package/setup/reregister-policy.ts +10 -5
@@ -1 +1 @@
1
- {"version":3,"file":"agent-health-store.js","names":[],"sources":["../../backends/agent-health-store.ts"],"sourcesContent":["/**\n * Per-agent health snapshots (Issue #789) — local SQLite WAL.\n */\n\nimport { mkdirSync } from \"node:fs\";\nimport { dirname, join } from \"node:path\";\nimport { DatabaseSync } from \"node:sqlite\";\nimport type { ForgeTaskItem } from \"../types/dashboard-types.js\";\nimport { BaseSqliteStore } from \"./base-sqlite-store.js\";\n\ntype AgentHealthOutcome = \"success\" | \"partial\" | \"failed\" | \"idle\";\n\ninterface AgentHealthRecord {\n agentId: string;\n sessionId: string | null;\n lastSeen: number;\n lastTask: string;\n outcome: AgentHealthOutcome;\n latencyMs?: number;\n tokensUsed?: number;\n errorCount: number;\n anomalyScore: number;\n nextAgent: string[];\n}\n\nexport type AgentHealthStatus = \"healthy\" | \"unknown\" | \"stale\" | \"degraded\" | \"idle\";\n\nexport interface AgentHealthView extends AgentHealthRecord {\n status: AgentHealthStatus;\n score: number;\n}\n\nconst STALE_MS = 4 * 3600 * 1000;\n\nexport class AgentHealthStore extends BaseSqliteStore {\n constructor(dbPath: string) {\n mkdirSync(dirname(dbPath), { recursive: true });\n const db = new DatabaseSync(dbPath);\n super(db);\n this.liveDb.exec(`\n CREATE TABLE IF NOT EXISTS agent_health (\n agent_id TEXT PRIMARY KEY,\n session_id TEXT,\n last_seen_ms INTEGER NOT NULL,\n last_task TEXT NOT NULL DEFAULT '',\n outcome TEXT NOT NULL CHECK(outcome IN ('success','partial','failed','idle')),\n latency_ms INTEGER,\n tokens_used INTEGER,\n error_count INTEGER NOT NULL DEFAULT 0,\n anomaly_score REAL NOT NULL DEFAULT 0,\n next_agent_json TEXT,\n updated_at INTEGER NOT NULL\n );\n `);\n }\n\n protected getSubsystemName(): string {\n return \"agent-health-store\";\n }\n\n upsert(input: AgentHealthRecord): void {\n const now = Date.now();\n const nextJson = JSON.stringify(input.nextAgent ?? []);\n this.liveDb\n .prepare(\n `INSERT INTO agent_health (agent_id, session_id, last_seen_ms, last_task, outcome, latency_ms, tokens_used, error_count, anomaly_score, next_agent_json, updated_at)\n VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)\n ON CONFLICT(agent_id) DO UPDATE SET\n session_id = excluded.session_id,\n last_seen_ms = excluded.last_seen_ms,\n last_task = excluded.last_task,\n outcome = excluded.outcome,\n latency_ms = excluded.latency_ms,\n tokens_used = excluded.tokens_used,\n error_count = excluded.error_count,\n anomaly_score = excluded.anomaly_score,\n next_agent_json = excluded.next_agent_json,\n updated_at = excluded.updated_at`,\n )\n .run(\n input.agentId,\n input.sessionId ?? null,\n input.lastSeen,\n input.lastTask,\n input.outcome,\n input.latencyMs ?? null,\n input.tokensUsed ?? null,\n input.errorCount,\n input.anomalyScore,\n nextJson,\n now,\n );\n }\n\n listAll(): AgentHealthRecord[] {\n const rows = this.liveDb\n .prepare(\n \"SELECT agent_id, session_id, last_seen_ms, last_task, outcome, latency_ms, tokens_used, error_count, anomaly_score, next_agent_json FROM agent_health\",\n )\n .all() as Array<{\n agent_id: string;\n session_id: string | null;\n last_seen_ms: number;\n last_task: string;\n outcome: string;\n latency_ms: number | null;\n tokens_used: number | null;\n error_count: number;\n anomaly_score: number;\n next_agent_json: string | null;\n }>;\n return rows.map((r) => ({\n agentId: r.agent_id,\n sessionId: r.session_id,\n lastSeen: r.last_seen_ms,\n lastTask: r.last_task,\n outcome: r.outcome as AgentHealthOutcome,\n latencyMs: r.latency_ms ?? undefined,\n tokensUsed: r.tokens_used ?? undefined,\n errorCount: r.error_count,\n anomalyScore: r.anomaly_score,\n nextAgent: r.next_agent_json ? (JSON.parse(r.next_agent_json) as string[]) : [],\n }));\n }\n\n get(agentId: string): AgentHealthRecord | null {\n const row = this.liveDb\n .prepare(\n \"SELECT agent_id, session_id, last_seen_ms, last_task, outcome, latency_ms, tokens_used, error_count, anomaly_score, next_agent_json FROM agent_health WHERE agent_id = ?\",\n )\n .get(agentId) as\n | {\n agent_id: string;\n session_id: string | null;\n last_seen_ms: number;\n last_task: string;\n outcome: string;\n latency_ms: number | null;\n tokens_used: number | null;\n error_count: number;\n anomaly_score: number;\n next_agent_json: string | null;\n }\n | undefined;\n if (!row) return null;\n return {\n agentId: row.agent_id,\n sessionId: row.session_id,\n lastSeen: row.last_seen_ms,\n lastTask: row.last_task,\n outcome: row.outcome as AgentHealthOutcome,\n latencyMs: row.latency_ms ?? undefined,\n tokensUsed: row.tokens_used ?? undefined,\n errorCount: row.error_count,\n anomalyScore: row.anomaly_score,\n nextAgent: row.next_agent_json ? (JSON.parse(row.next_agent_json) as string[]) : [],\n };\n }\n\n prune(retentionDays = 30): number {\n const cutoff = Date.now() - retentionDays * 24 * 3600 * 1000;\n const r = this.liveDb.prepare(\"DELETE FROM agent_health WHERE updated_at < ?\").run(cutoff);\n return Number(r.changes ?? 0);\n }\n}\n\nexport function agentHealthDbPathForMemorySqlite(memorySqlitePath: string): string | null {\n if (!memorySqlitePath || memorySqlitePath === \":memory:\") return null;\n return join(dirname(memorySqlitePath), \"agent-health.db\");\n}\n\nconst DEFAULT_AGENT_IDS = [\"forge\", \"scholar\", \"hearth\", \"warden\", \"ralph\", \"builder\", \"reaver\"] as const;\n\nfunction mapForgeStatus(s?: string): AgentHealthOutcome {\n const x = (s ?? \"\").toLowerCase();\n if (x.includes(\"fail\") || x.includes(\"error\")) return \"failed\";\n if (x.includes(\"partial\")) return \"partial\";\n if (x.includes(\"idle\")) return \"idle\";\n return \"success\";\n}\n\n/**\n * Merge persisted health rows with live Forge agent JSON files for Mission Control / CLI.\n */\nexport function mergeAgentHealthDashboard(forge: ForgeTaskItem[], dbRows: AgentHealthRecord[]): AgentHealthView[] {\n const byId = new Map<string, AgentHealthRecord>();\n for (const r of dbRows) {\n byId.set(r.agentId.toLowerCase(), { ...r, agentId: r.agentId.toLowerCase() });\n }\n for (const f of forge) {\n const id = (f.agent ?? \"unknown\").toLowerCase();\n const started = f.started_at ? Date.parse(f.started_at) : Date.now();\n const syn: AgentHealthRecord = {\n agentId: id,\n sessionId: null,\n lastSeen: Number.isFinite(started) ? started : Date.now(),\n lastTask: f.task ?? \"\",\n outcome: mapForgeStatus(f.status),\n errorCount: byId.get(id)?.errorCount ?? 0,\n anomalyScore: byId.get(id)?.anomalyScore ?? 0,\n nextAgent: byId.get(id)?.nextAgent ?? [],\n };\n const prev = byId.get(id);\n if (!prev || syn.lastSeen >= prev.lastSeen) {\n byId.set(id, {\n ...prev,\n ...syn,\n errorCount: prev?.errorCount ?? syn.errorCount,\n anomalyScore: prev?.anomalyScore ?? syn.anomalyScore,\n nextAgent: prev?.nextAgent?.length ? prev.nextAgent : syn.nextAgent,\n });\n }\n }\n const ids = new Set<string>([...DEFAULT_AGENT_IDS.map((a) => a.toLowerCase()), ...byId.keys()]);\n const out: AgentHealthView[] = [];\n for (const id of ids) {\n const raw =\n byId.get(id) ??\n ({\n agentId: id,\n sessionId: null,\n lastSeen: 0,\n lastTask: \"\",\n outcome: \"idle\",\n errorCount: 0,\n anomalyScore: 0,\n nextAgent: [],\n } satisfies AgentHealthRecord);\n out.push(computeHealthView(raw));\n }\n out.sort((a, b) => a.agentId.localeCompare(b.agentId));\n return out;\n}\n\nfunction computeHealthView(r: AgentHealthRecord, nowMs: number = Date.now()): AgentHealthView {\n const age = nowMs - r.lastSeen;\n let status: AgentHealthStatus = \"healthy\";\n if (r.lastSeen <= 0 || !r.agentId) {\n status = \"unknown\";\n } else if (r.outcome === \"idle\") {\n status = \"idle\";\n } else if (r.outcome === \"failed\" || r.outcome === \"partial\") {\n status = \"degraded\";\n } else if (age > STALE_MS) {\n status = \"stale\";\n }\n const wasDegradedFromStale = status === \"stale\" && (r.errorCount >= 2 || r.anomalyScore >= 0.6);\n if (status !== \"unknown\" && (r.errorCount >= 2 || r.anomalyScore >= 0.6)) {\n status = \"degraded\";\n }\n\n let score = 75;\n if (status === \"healthy\") score = 88 + Math.min(12, Math.floor((1 - r.anomalyScore) * 12));\n if (status === \"idle\") score = 70;\n if (status === \"stale\") score = 45;\n if (status === \"degraded\") {\n const degradedScore = Math.max(20, 55 - Math.floor(r.anomalyScore * 40));\n score = wasDegradedFromStale ? Math.min(45, degradedScore) : degradedScore;\n }\n if (status === \"unknown\") score = 0;\n\n return { ...r, status, score };\n}\n"],"mappings":";;;;;;;;AAgCA,MAAM,WAAW,IAAI,OAAO;AAE5B,IAAa,mBAAb,cAAsC,gBAAgB;CACpD,YAAY,QAAgB;EAC1B,UAAU,QAAQ,OAAO,EAAE,EAAE,WAAW,MAAM,CAAC;EAC/C,MAAM,KAAK,IAAI,aAAa,OAAO;EACnC,MAAM,GAAG;EACT,KAAK,OAAO,KAAK;;;;;;;;;;;;;;MAcf;;CAGJ,mBAAqC;EACnC,OAAO;;CAGT,OAAO,OAAgC;EACrC,MAAM,MAAM,KAAK,KAAK;EACtB,MAAM,WAAW,KAAK,UAAU,MAAM,aAAa,EAAE,CAAC;EACtD,KAAK,OACF,QACC;;;;;;;;;;;;6CAaD,CACA,IACC,MAAM,SACN,MAAM,aAAa,MACnB,MAAM,UACN,MAAM,UACN,MAAM,SACN,MAAM,aAAa,MACnB,MAAM,cAAc,MACpB,MAAM,YACN,MAAM,cACN,UACA,IACD;;CAGL,UAA+B;EAiB7B,OAhBa,KAAK,OACf,QACC,wJACD,CACA,KAYQ,CAAC,KAAK,OAAO;GACtB,SAAS,EAAE;GACX,WAAW,EAAE;GACb,UAAU,EAAE;GACZ,UAAU,EAAE;GACZ,SAAS,EAAE;GACX,WAAW,EAAE,cAAc,KAAA;GAC3B,YAAY,EAAE,eAAe,KAAA;GAC7B,YAAY,EAAE;GACd,cAAc,EAAE;GAChB,WAAW,EAAE,kBAAmB,KAAK,MAAM,EAAE,gBAAgB,GAAgB,EAAE;GAChF,EAAE;;CAGL,IAAI,SAA2C;EAC7C,MAAM,MAAM,KAAK,OACd,QACC,2KACD,CACA,IAAI,QAAQ;EAcf,IAAI,CAAC,KAAK,OAAO;EACjB,OAAO;GACL,SAAS,IAAI;GACb,WAAW,IAAI;GACf,UAAU,IAAI;GACd,UAAU,IAAI;GACd,SAAS,IAAI;GACb,WAAW,IAAI,cAAc,KAAA;GAC7B,YAAY,IAAI,eAAe,KAAA;GAC/B,YAAY,IAAI;GAChB,cAAc,IAAI;GAClB,WAAW,IAAI,kBAAmB,KAAK,MAAM,IAAI,gBAAgB,GAAgB,EAAE;GACpF;;CAGH,MAAM,gBAAgB,IAAY;EAChC,MAAM,SAAS,KAAK,KAAK,GAAG,gBAAgB,KAAK,OAAO;EACxD,MAAM,IAAI,KAAK,OAAO,QAAQ,gDAAgD,CAAC,IAAI,OAAO;EAC1F,OAAO,OAAO,EAAE,WAAW,EAAE;;;AAIjC,SAAgB,iCAAiC,kBAAyC;CACxF,IAAI,CAAC,oBAAoB,qBAAqB,YAAY,OAAO;CACjE,OAAO,KAAK,QAAQ,iBAAiB,EAAE,kBAAkB;;AAG3D,MAAM,oBAAoB;CAAC;CAAS;CAAW;CAAU;CAAU;CAAS;CAAW;CAAS;AAEhG,SAAS,eAAe,GAAgC;CACtD,MAAM,KAAK,KAAK,IAAI,aAAa;CACjC,IAAI,EAAE,SAAS,OAAO,IAAI,EAAE,SAAS,QAAQ,EAAE,OAAO;CACtD,IAAI,EAAE,SAAS,UAAU,EAAE,OAAO;CAClC,IAAI,EAAE,SAAS,OAAO,EAAE,OAAO;CAC/B,OAAO;;;;;AAMT,SAAgB,0BAA0B,OAAwB,QAAgD;CAChH,MAAM,uBAAO,IAAI,KAAgC;CACjD,KAAK,MAAM,KAAK,QACd,KAAK,IAAI,EAAE,QAAQ,aAAa,EAAE;EAAE,GAAG;EAAG,SAAS,EAAE,QAAQ,aAAa;EAAE,CAAC;CAE/E,KAAK,MAAM,KAAK,OAAO;EACrB,MAAM,MAAM,EAAE,SAAS,WAAW,aAAa;EAC/C,MAAM,UAAU,EAAE,aAAa,KAAK,MAAM,EAAE,WAAW,GAAG,KAAK,KAAK;EACpE,MAAM,MAAyB;GAC7B,SAAS;GACT,WAAW;GACX,UAAU,OAAO,SAAS,QAAQ,GAAG,UAAU,KAAK,KAAK;GACzD,UAAU,EAAE,QAAQ;GACpB,SAAS,eAAe,EAAE,OAAO;GACjC,YAAY,KAAK,IAAI,GAAG,EAAE,cAAc;GACxC,cAAc,KAAK,IAAI,GAAG,EAAE,gBAAgB;GAC5C,WAAW,KAAK,IAAI,GAAG,EAAE,aAAa,EAAE;GACzC;EACD,MAAM,OAAO,KAAK,IAAI,GAAG;EACzB,IAAI,CAAC,QAAQ,IAAI,YAAY,KAAK,UAChC,KAAK,IAAI,IAAI;GACX,GAAG;GACH,GAAG;GACH,YAAY,MAAM,cAAc,IAAI;GACpC,cAAc,MAAM,gBAAgB,IAAI;GACxC,WAAW,MAAM,WAAW,SAAS,KAAK,YAAY,IAAI;GAC3D,CAAC;;CAGN,MAAM,MAAM,IAAI,IAAY,CAAC,GAAG,kBAAkB,KAAK,MAAM,EAAE,aAAa,CAAC,EAAE,GAAG,KAAK,MAAM,CAAC,CAAC;CAC/F,MAAM,MAAyB,EAAE;CACjC,KAAK,MAAM,MAAM,KAAK;EACpB,MAAM,MACJ,KAAK,IAAI,GAAG,IACX;GACC,SAAS;GACT,WAAW;GACX,UAAU;GACV,UAAU;GACV,SAAS;GACT,YAAY;GACZ,cAAc;GACd,WAAW,EAAE;GACd;EACH,IAAI,KAAK,kBAAkB,IAAI,CAAC;;CAElC,IAAI,MAAM,GAAG,MAAM,EAAE,QAAQ,cAAc,EAAE,QAAQ,CAAC;CACtD,OAAO;;AAGT,SAAS,kBAAkB,GAAsB,QAAgB,KAAK,KAAK,EAAmB;CAC5F,MAAM,MAAM,QAAQ,EAAE;CACtB,IAAI,SAA4B;CAChC,IAAI,EAAE,YAAY,KAAK,CAAC,EAAE,SACxB,SAAS;MACJ,IAAI,EAAE,YAAY,QACvB,SAAS;MACJ,IAAI,EAAE,YAAY,YAAY,EAAE,YAAY,WACjD,SAAS;MACJ,IAAI,MAAM,UACf,SAAS;CAEX,MAAM,uBAAuB,WAAW,YAAY,EAAE,cAAc,KAAK,EAAE,gBAAgB;CAC3F,IAAI,WAAW,cAAc,EAAE,cAAc,KAAK,EAAE,gBAAgB,KAClE,SAAS;CAGX,IAAI,QAAQ;CACZ,IAAI,WAAW,WAAW,QAAQ,KAAK,KAAK,IAAI,IAAI,KAAK,OAAO,IAAI,EAAE,gBAAgB,GAAG,CAAC;CAC1F,IAAI,WAAW,QAAQ,QAAQ;CAC/B,IAAI,WAAW,SAAS,QAAQ;CAChC,IAAI,WAAW,YAAY;EACzB,MAAM,gBAAgB,KAAK,IAAI,IAAI,KAAK,KAAK,MAAM,EAAE,eAAe,GAAG,CAAC;EACxE,QAAQ,uBAAuB,KAAK,IAAI,IAAI,cAAc,GAAG;;CAE/D,IAAI,WAAW,WAAW,QAAQ;CAElC,OAAO;EAAE,GAAG;EAAG;EAAQ;EAAO"}
1
+ {"version":3,"file":"agent-health-store.js","names":[],"sources":["../../backends/agent-health-store.ts"],"sourcesContent":["/**\n * Per-agent health snapshots (Issue #789) — local SQLite WAL.\n */\n\nimport { mkdirSync } from \"node:fs\";\nimport { dirname, join } from \"node:path\";\nimport { DatabaseSync } from \"node:sqlite\";\nimport type { ForgeTaskItem } from \"../types/dashboard-types.js\";\nimport { BaseSqliteStore } from \"./base-sqlite-store.js\";\n\ntype AgentHealthOutcome = \"success\" | \"partial\" | \"failed\" | \"idle\";\n\ninterface AgentHealthRecord {\n agentId: string;\n sessionId: string | null;\n lastSeen: number;\n lastTask: string;\n outcome: AgentHealthOutcome;\n latencyMs?: number;\n tokensUsed?: number;\n errorCount: number;\n anomalyScore: number;\n nextAgent: string[];\n}\n\nexport type AgentHealthStatus = \"healthy\" | \"unknown\" | \"stale\" | \"degraded\" | \"idle\";\n\nexport interface AgentHealthView extends AgentHealthRecord {\n status: AgentHealthStatus;\n score: number;\n}\n\nconst STALE_MS = 4 * 3600 * 1000;\n\nexport class AgentHealthStore extends BaseSqliteStore {\n constructor(dbPath: string) {\n mkdirSync(dirname(dbPath), { recursive: true });\n const db = new DatabaseSync(dbPath);\n super(db);\n this.liveDb.exec(`\n CREATE TABLE IF NOT EXISTS agent_health (\n agent_id TEXT PRIMARY KEY,\n session_id TEXT,\n last_seen_ms INTEGER NOT NULL,\n last_task TEXT NOT NULL DEFAULT '',\n outcome TEXT NOT NULL CHECK(outcome IN ('success','partial','failed','idle')),\n latency_ms INTEGER,\n tokens_used INTEGER,\n error_count INTEGER NOT NULL DEFAULT 0,\n anomaly_score REAL NOT NULL DEFAULT 0,\n next_agent_json TEXT,\n updated_at INTEGER NOT NULL\n );\n `);\n }\n\n protected getSubsystemName(): string {\n return \"agent-health-store\";\n }\n\n upsert(input: AgentHealthRecord): void {\n const now = Date.now();\n const nextJson = JSON.stringify(input.nextAgent ?? []);\n this.liveDb\n .prepare(\n `INSERT INTO agent_health (agent_id, session_id, last_seen_ms, last_task, outcome, latency_ms, tokens_used, error_count, anomaly_score, next_agent_json, updated_at)\n VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)\n ON CONFLICT(agent_id) DO UPDATE SET\n session_id = excluded.session_id,\n last_seen_ms = excluded.last_seen_ms,\n last_task = excluded.last_task,\n outcome = excluded.outcome,\n latency_ms = excluded.latency_ms,\n tokens_used = excluded.tokens_used,\n error_count = excluded.error_count,\n anomaly_score = excluded.anomaly_score,\n next_agent_json = excluded.next_agent_json,\n updated_at = excluded.updated_at`,\n )\n .run(\n input.agentId,\n input.sessionId ?? null,\n input.lastSeen,\n input.lastTask,\n input.outcome,\n input.latencyMs ?? null,\n input.tokensUsed ?? null,\n input.errorCount,\n input.anomalyScore,\n nextJson,\n now,\n );\n }\n\n listAll(): AgentHealthRecord[] {\n const rows = this.liveDb\n .prepare(\n \"SELECT agent_id, session_id, last_seen_ms, last_task, outcome, latency_ms, tokens_used, error_count, anomaly_score, next_agent_json FROM agent_health\",\n )\n .all() as Array<{\n agent_id: string;\n session_id: string | null;\n last_seen_ms: number;\n last_task: string;\n outcome: string;\n latency_ms: number | null;\n tokens_used: number | null;\n error_count: number;\n anomaly_score: number;\n next_agent_json: string | null;\n }>;\n return rows.map((r) => ({\n agentId: r.agent_id,\n sessionId: r.session_id,\n lastSeen: r.last_seen_ms,\n lastTask: r.last_task,\n outcome: r.outcome as AgentHealthOutcome,\n latencyMs: r.latency_ms ?? undefined,\n tokensUsed: r.tokens_used ?? undefined,\n errorCount: r.error_count,\n anomalyScore: r.anomaly_score,\n nextAgent: r.next_agent_json ? (JSON.parse(r.next_agent_json) as string[]) : [],\n }));\n }\n\n get(agentId: string): AgentHealthRecord | null {\n const row = this.liveDb\n .prepare(\n \"SELECT agent_id, session_id, last_seen_ms, last_task, outcome, latency_ms, tokens_used, error_count, anomaly_score, next_agent_json FROM agent_health WHERE agent_id = ?\",\n )\n .get(agentId) as\n | {\n agent_id: string;\n session_id: string | null;\n last_seen_ms: number;\n last_task: string;\n outcome: string;\n latency_ms: number | null;\n tokens_used: number | null;\n error_count: number;\n anomaly_score: number;\n next_agent_json: string | null;\n }\n | undefined;\n if (!row) return null;\n return {\n agentId: row.agent_id,\n sessionId: row.session_id,\n lastSeen: row.last_seen_ms,\n lastTask: row.last_task,\n outcome: row.outcome as AgentHealthOutcome,\n latencyMs: row.latency_ms ?? undefined,\n tokensUsed: row.tokens_used ?? undefined,\n errorCount: row.error_count,\n anomalyScore: row.anomaly_score,\n nextAgent: row.next_agent_json ? (JSON.parse(row.next_agent_json) as string[]) : [],\n };\n }\n\n prune(retentionDays = 30): number {\n const cutoff = Date.now() - retentionDays * 24 * 3600 * 1000;\n const r = this.liveDb.prepare(\"DELETE FROM agent_health WHERE updated_at < ?\").run(cutoff);\n return Number(r.changes ?? 0);\n }\n}\n\nexport function agentHealthDbPathForMemorySqlite(memorySqlitePath: string): string | null {\n if (!memorySqlitePath || memorySqlitePath === \":memory:\") return null;\n return join(dirname(memorySqlitePath), \"agent-health.db\");\n}\n\nconst DEFAULT_AGENT_IDS = [\"forge\", \"scholar\", \"hearth\", \"warden\", \"ralph\", \"builder\", \"reaver\"] as const;\n\nfunction mapForgeStatus(s?: string): AgentHealthOutcome {\n const x = (s ?? \"\").toLowerCase();\n if (x.includes(\"fail\") || x.includes(\"error\")) return \"failed\";\n if (x.includes(\"partial\")) return \"partial\";\n if (x.includes(\"idle\")) return \"idle\";\n return \"success\";\n}\n\n/**\n * Merge persisted health rows with live Forge agent JSON files for Mission Control / CLI.\n */\nexport function mergeAgentHealthDashboard(forge: ForgeTaskItem[], dbRows: AgentHealthRecord[]): AgentHealthView[] {\n const byId = new Map<string, AgentHealthRecord>();\n for (const r of dbRows) {\n byId.set(r.agentId.toLowerCase(), { ...r, agentId: r.agentId.toLowerCase() });\n }\n for (const f of forge) {\n const id = (f.agent ?? \"unknown\").toLowerCase();\n const started = f.started_at ? Date.parse(f.started_at) : Date.now();\n const syn: AgentHealthRecord = {\n agentId: id,\n sessionId: null,\n lastSeen: Number.isFinite(started) ? started : Date.now(),\n lastTask: f.task ?? \"\",\n outcome: mapForgeStatus(f.status),\n errorCount: byId.get(id)?.errorCount ?? 0,\n anomalyScore: byId.get(id)?.anomalyScore ?? 0,\n nextAgent: byId.get(id)?.nextAgent ?? [],\n };\n const prev = byId.get(id);\n if (!prev || syn.lastSeen >= prev.lastSeen) {\n byId.set(id, {\n ...prev,\n ...syn,\n errorCount: prev?.errorCount ?? syn.errorCount,\n anomalyScore: prev?.anomalyScore ?? syn.anomalyScore,\n nextAgent: prev?.nextAgent?.length ? prev.nextAgent : syn.nextAgent,\n });\n }\n }\n const ids = new Set<string>([...DEFAULT_AGENT_IDS.map((a) => a.toLowerCase()), ...byId.keys()]);\n const out: AgentHealthView[] = [];\n for (const id of ids) {\n const raw =\n byId.get(id) ??\n ({\n agentId: id,\n sessionId: null,\n lastSeen: 0,\n lastTask: \"\",\n outcome: \"idle\",\n errorCount: 0,\n anomalyScore: 0,\n nextAgent: [],\n } satisfies AgentHealthRecord);\n out.push(computeHealthView(raw));\n }\n out.sort((a, b) => a.agentId.localeCompare(b.agentId));\n return out;\n}\n\nfunction computeHealthView(r: AgentHealthRecord, nowMs: number = Date.now()): AgentHealthView {\n const age = nowMs - r.lastSeen;\n let status: AgentHealthStatus = \"healthy\";\n if (r.lastSeen <= 0 || !r.agentId) {\n status = \"unknown\";\n } else if (r.outcome === \"idle\") {\n status = \"idle\";\n } else if (r.outcome === \"failed\" || r.outcome === \"partial\") {\n status = \"degraded\";\n } else if (age > STALE_MS) {\n status = \"stale\";\n }\n const wasDegradedFromStale = status === \"stale\" && (r.errorCount >= 2 || r.anomalyScore >= 0.6);\n if (status !== \"unknown\" && (r.errorCount >= 2 || r.anomalyScore >= 0.6)) {\n status = \"degraded\";\n }\n\n let score = 75;\n if (status === \"healthy\") score = 88 + Math.min(12, Math.floor((1 - r.anomalyScore) * 12));\n if (status === \"idle\") score = 70;\n if (status === \"stale\") score = 45;\n if (status === \"degraded\") {\n const degradedScore = Math.max(20, 55 - Math.floor(r.anomalyScore * 40));\n score = wasDegradedFromStale ? Math.min(45, degradedScore) : degradedScore;\n }\n if (status === \"unknown\") score = 0;\n\n return { ...r, status, score };\n}\n"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"apitap-store.js","names":[],"sources":["../../backends/apitap-store.ts"],"sourcesContent":["/**\n * ApiTap Store — SQLite backend for discovered API endpoints (Issue #614).\n *\n * Persists endpoints discovered by `apitap capture` / `apitap peek` with TTL expiry.\n * Each entry represents one parameterized API endpoint with sample request/response.\n *\n * Schema follows IssueStore / ToolProposalStore conventions.\n */\n\nimport { randomUUID } from \"node:crypto\";\nimport { mkdirSync } from \"node:fs\";\nimport { dirname } from \"node:path\";\nimport { DatabaseSync } from \"node:sqlite\";\nimport type { SQLInputValue } from \"node:sqlite\";\n\nimport { BaseSqliteStore } from \"./base-sqlite-store.js\";\n\n// ---------------------------------------------------------------------------\n// Public types\n// ---------------------------------------------------------------------------\n\ntype ApitapEndpointStatus = \"pending\" | \"reviewed\" | \"accepted\" | \"rejected\";\n\ninterface ApitapEndpoint {\n id: string;\n /** Original site URL that was captured. */\n siteUrl: string;\n /** Discovered API endpoint path (e.g. /api/v1/users). */\n endpoint: string;\n /** HTTP method (GET, POST, PUT, DELETE, PATCH). */\n method: string;\n /** Parsed parameters object. */\n parameters: Record<string, unknown>;\n /** Parsed sample response body. */\n sampleResponse: unknown;\n /** Content-Type of response. */\n contentType: string;\n /** Session ID that captured this endpoint. */\n sessionId: string;\n /** ISO timestamp when captured. */\n capturedAt: string;\n /** ISO timestamp when TTL expires (null = never). */\n expiresAt: string | null;\n status: ApitapEndpointStatus;\n createdAt: string;\n updatedAt: string;\n}\n\ninterface CreateApitapEndpointInput {\n siteUrl: string;\n endpoint: string;\n method: string;\n /** Parameters as an object (serialized to JSON) or JSON string. */\n parameters?: Record<string, unknown> | string;\n /** Sample response (serialized to JSON) or JSON string. */\n sampleResponse?: unknown | string;\n contentType?: string;\n sessionId?: string;\n capturedAt?: string;\n /** ISO timestamp for expiry (null = never). Takes priority over endpointTtlDays. */\n expiresAt?: string | null;\n /** Convenience: compute expiresAt from TTL days from now. */\n endpointTtlDays?: number;\n}\n\ninterface ApitapEndpointFilter {\n siteUrl?: string;\n sessionId?: string;\n status?: ApitapEndpointStatus;\n includeExpired?: boolean;\n limit?: number;\n}\n\n// ---------------------------------------------------------------------------\n// ApitapStore\n// ---------------------------------------------------------------------------\n\nexport class ApitapStore extends BaseSqliteStore {\n constructor(dbPath: string) {\n mkdirSync(dirname(dbPath), { recursive: true });\n const db = new DatabaseSync(dbPath);\n super(db);\n\n this.liveDb.exec(`\n CREATE TABLE IF NOT EXISTS apitap_endpoints (\n id TEXT PRIMARY KEY,\n site_url TEXT NOT NULL,\n endpoint TEXT NOT NULL,\n method TEXT NOT NULL DEFAULT 'GET',\n parameters TEXT NOT NULL DEFAULT '{}',\n sample_response TEXT NOT NULL DEFAULT '',\n content_type TEXT NOT NULL DEFAULT '',\n session_id TEXT NOT NULL DEFAULT '',\n captured_at TEXT NOT NULL DEFAULT (datetime('now')),\n expires_at TEXT,\n status TEXT NOT NULL DEFAULT 'pending',\n created_at TEXT NOT NULL DEFAULT (datetime('now')),\n updated_at TEXT NOT NULL DEFAULT (datetime('now'))\n );\n\n CREATE INDEX IF NOT EXISTS idx_apitap_site_url ON apitap_endpoints(site_url);\n CREATE INDEX IF NOT EXISTS idx_apitap_session ON apitap_endpoints(session_id);\n CREATE INDEX IF NOT EXISTS idx_apitap_status ON apitap_endpoints(status);\n CREATE INDEX IF NOT EXISTS idx_apitap_expires ON apitap_endpoints(expires_at);\n `);\n }\n\n protected getSubsystemName(): string {\n return \"apitap-store\";\n }\n\n // -------------------------------------------------------------------------\n // create\n // -------------------------------------------------------------------------\n\n create(input: CreateApitapEndpointInput): ApitapEndpoint {\n const id = randomUUID();\n const now = new Date().toISOString();\n\n // Serialize parameters/sampleResponse objects to JSON strings for SQLite\n const parametersStr =\n typeof input.parameters === \"string\" ? input.parameters : JSON.stringify(input.parameters ?? {});\n const sampleResponseStr =\n typeof input.sampleResponse === \"string\" ? input.sampleResponse : JSON.stringify(input.sampleResponse ?? null);\n\n // Compute expiresAt: explicit value wins; endpointTtlDays computes if no explicit expiresAt\n let expiresAt: string | null = input.expiresAt !== undefined ? (input.expiresAt ?? null) : null;\n if (expiresAt === null && !(\"expiresAt\" in input) && input.endpointTtlDays && input.endpointTtlDays > 0) {\n expiresAt = new Date(Date.now() + input.endpointTtlDays * 24 * 60 * 60_000).toISOString();\n }\n\n this.liveDb\n .prepare(\n `INSERT INTO apitap_endpoints\n (id, site_url, endpoint, method, parameters, sample_response, content_type,\n session_id, captured_at, expires_at, status, created_at, updated_at)\n VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 'pending', ?, ?)`,\n )\n .run(\n id,\n input.siteUrl,\n input.endpoint,\n input.method.toUpperCase(),\n parametersStr,\n sampleResponseStr,\n input.contentType ?? \"\",\n input.sessionId ?? \"\",\n input.capturedAt ?? now,\n expiresAt,\n now,\n now,\n );\n\n // biome-ignore lint/style/noNonNullAssertion: Known to exist\n return this.getById(id)!;\n }\n\n // -------------------------------------------------------------------------\n // getById\n // -------------------------------------------------------------------------\n\n getById(id: string): ApitapEndpoint | null {\n const row = this.liveDb.prepare(\"SELECT * FROM apitap_endpoints WHERE id = ?\").get(id) as\n | Record<string, unknown>\n | undefined;\n if (!row) return null;\n return this.rowToEndpoint(row);\n }\n\n // -------------------------------------------------------------------------\n // list — filtered listing\n // -------------------------------------------------------------------------\n\n list(filter?: ApitapEndpointFilter): ApitapEndpoint[] {\n let query = \"SELECT * FROM apitap_endpoints WHERE 1=1\";\n const params: SQLInputValue[] = [];\n\n if (filter?.siteUrl) {\n const escapedSiteUrl = filter.siteUrl.replace(/\\\\/g, \"\\\\\\\\\").replace(/[%_]/g, \"\\\\$&\");\n query += \" AND site_url LIKE ? ESCAPE '\\\\'\";\n params.push(`%${escapedSiteUrl}%`);\n }\n if (filter?.sessionId) {\n query += \" AND session_id = ?\";\n params.push(filter.sessionId);\n }\n if (filter?.status) {\n query += \" AND status = ?\";\n params.push(filter.status);\n }\n if (!filter?.includeExpired) {\n query += \" AND (expires_at IS NULL OR expires_at > strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))\";\n }\n\n query += \" ORDER BY created_at DESC\";\n\n if (filter?.limit && filter.limit > 0) {\n query += \" LIMIT ?\";\n params.push(filter.limit);\n }\n\n const rows = this.liveDb.prepare(query).all(...params) as Record<string, unknown>[];\n return rows.map((r) => this.rowToEndpoint(r));\n }\n\n // -------------------------------------------------------------------------\n // updateStatus\n // -------------------------------------------------------------------------\n\n updateStatus(id: string, status: ApitapEndpointStatus): ApitapEndpoint | null {\n const now = new Date().toISOString();\n const result = this.liveDb\n .prepare(\"UPDATE apitap_endpoints SET status = ?, updated_at = ? WHERE id = ?\")\n .run(status, now, id);\n\n if (result.changes === 0) return null;\n return this.getById(id);\n }\n\n // -------------------------------------------------------------------------\n // deleteExpired — prune endpoints past their TTL\n // -------------------------------------------------------------------------\n\n deleteExpired(): number {\n const result = this.liveDb\n .prepare(\n \"DELETE FROM apitap_endpoints WHERE expires_at IS NOT NULL AND expires_at <= strftime('%Y-%m-%dT%H:%M:%fZ', 'now')\",\n )\n .run();\n return Number(result.changes);\n }\n\n // -------------------------------------------------------------------------\n // count\n // -------------------------------------------------------------------------\n\n count(status?: ApitapEndpointStatus): number {\n const row = status\n ? (this.liveDb.prepare(\"SELECT COUNT(*) as n FROM apitap_endpoints WHERE status = ?\").get(status) as {\n n: number;\n })\n : (this.liveDb.prepare(\"SELECT COUNT(*) as n FROM apitap_endpoints\").get() as { n: number });\n return row.n;\n }\n\n // -------------------------------------------------------------------------\n // existsForSession — check if a session has already been captured\n // -------------------------------------------------------------------------\n\n countForSession(sessionId: string): number {\n const row = this.liveDb\n .prepare(\"SELECT COUNT(*) as n FROM apitap_endpoints WHERE session_id = ?\")\n .get(sessionId) as {\n n: number;\n };\n return row.n;\n }\n\n // -------------------------------------------------------------------------\n // Private helpers\n // -------------------------------------------------------------------------\n\n private rowToEndpoint(row: Record<string, unknown>): ApitapEndpoint {\n let parameters: Record<string, unknown> = {};\n try {\n parameters = JSON.parse((row.parameters as string) ?? \"{}\");\n } catch {\n /* keep empty */\n }\n let sampleResponse: unknown = null;\n try {\n sampleResponse = JSON.parse((row.sample_response as string) ?? \"null\");\n } catch {\n /* keep null */\n }\n return {\n id: row.id as string,\n siteUrl: row.site_url as string,\n endpoint: row.endpoint as string,\n method: row.method as string,\n parameters,\n sampleResponse,\n contentType: row.content_type as string,\n sessionId: row.session_id as string,\n capturedAt: row.captured_at as string,\n expiresAt: (row.expires_at as string | null) ?? null,\n status: row.status as ApitapEndpointStatus,\n createdAt: row.created_at as string,\n updatedAt: row.updated_at as string,\n };\n }\n}\n"],"mappings":";;;;;;;;;;;;;;AA6EA,IAAa,cAAb,cAAiC,gBAAgB;CAC/C,YAAY,QAAgB;EAC1B,UAAU,QAAQ,OAAO,EAAE,EAAE,WAAW,MAAM,CAAC;EAC/C,MAAM,KAAK,IAAI,aAAa,OAAO;EACnC,MAAM,GAAG;EAET,KAAK,OAAO,KAAK;;;;;;;;;;;;;;;;;;;;;MAqBf;;CAGJ,mBAAqC;EACnC,OAAO;;CAOT,OAAO,OAAkD;EACvD,MAAM,KAAK,YAAY;EACvB,MAAM,uBAAM,IAAI,MAAM,EAAC,aAAa;EAGpC,MAAM,gBACJ,OAAO,MAAM,eAAe,WAAW,MAAM,aAAa,KAAK,UAAU,MAAM,cAAc,EAAE,CAAC;EAClG,MAAM,oBACJ,OAAO,MAAM,mBAAmB,WAAW,MAAM,iBAAiB,KAAK,UAAU,MAAM,kBAAkB,KAAK;EAGhH,IAAI,YAA2B,MAAM,cAAc,KAAA,IAAa,MAAM,aAAa,OAAQ;EAC3F,IAAI,cAAc,QAAQ,EAAE,eAAe,UAAU,MAAM,mBAAmB,MAAM,kBAAkB,GACpG,YAAY,IAAI,KAAK,KAAK,KAAK,GAAG,MAAM,kBAAkB,KAAK,KAAK,IAAO,CAAC,aAAa;EAG3F,KAAK,OACF,QACC;;;iEAID,CACA,IACC,IACA,MAAM,SACN,MAAM,UACN,MAAM,OAAO,aAAa,EAC1B,eACA,mBACA,MAAM,eAAe,IACrB,MAAM,aAAa,IACnB,MAAM,cAAc,KACpB,WACA,KACA,IACD;EAGH,OAAO,KAAK,QAAQ,GAAG;;CAOzB,QAAQ,IAAmC;EACzC,MAAM,MAAM,KAAK,OAAO,QAAQ,8CAA8C,CAAC,IAAI,GAAG;EAGtF,IAAI,CAAC,KAAK,OAAO;EACjB,OAAO,KAAK,cAAc,IAAI;;CAOhC,KAAK,QAAiD;EACpD,IAAI,QAAQ;EACZ,MAAM,SAA0B,EAAE;EAElC,IAAI,QAAQ,SAAS;GACnB,MAAM,iBAAiB,OAAO,QAAQ,QAAQ,OAAO,OAAO,CAAC,QAAQ,SAAS,OAAO;GACrF,SAAS;GACT,OAAO,KAAK,IAAI,eAAe,GAAG;;EAEpC,IAAI,QAAQ,WAAW;GACrB,SAAS;GACT,OAAO,KAAK,OAAO,UAAU;;EAE/B,IAAI,QAAQ,QAAQ;GAClB,SAAS;GACT,OAAO,KAAK,OAAO,OAAO;;EAE5B,IAAI,CAAC,QAAQ,gBACX,SAAS;EAGX,SAAS;EAET,IAAI,QAAQ,SAAS,OAAO,QAAQ,GAAG;GACrC,SAAS;GACT,OAAO,KAAK,OAAO,MAAM;;EAI3B,OADa,KAAK,OAAO,QAAQ,MAAM,CAAC,IAAI,GAAG,OACpC,CAAC,KAAK,MAAM,KAAK,cAAc,EAAE,CAAC;;CAO/C,aAAa,IAAY,QAAqD;EAC5E,MAAM,uBAAM,IAAI,MAAM,EAAC,aAAa;EAKpC,IAJe,KAAK,OACjB,QAAQ,sEAAsE,CAC9E,IAAI,QAAQ,KAAK,GAEV,CAAC,YAAY,GAAG,OAAO;EACjC,OAAO,KAAK,QAAQ,GAAG;;CAOzB,gBAAwB;EACtB,MAAM,SAAS,KAAK,OACjB,QACC,oHACD,CACA,KAAK;EACR,OAAO,OAAO,OAAO,QAAQ;;CAO/B,MAAM,QAAuC;EAM3C,QALY,SACP,KAAK,OAAO,QAAQ,8DAA8D,CAAC,IAAI,OAAO,GAG9F,KAAK,OAAO,QAAQ,6CAA6C,CAAC,KAAK,EACjE;;CAOb,gBAAgB,WAA2B;EAMzC,OALY,KAAK,OACd,QAAQ,kEAAkE,CAC1E,IAAI,UAGG,CAAC;;CAOb,cAAsB,KAA8C;EAClE,IAAI,aAAsC,EAAE;EAC5C,IAAI;GACF,aAAa,KAAK,MAAO,IAAI,cAAyB,KAAK;UACrD;EAGR,IAAI,iBAA0B;EAC9B,IAAI;GACF,iBAAiB,KAAK,MAAO,IAAI,mBAA8B,OAAO;UAChE;EAGR,OAAO;GACL,IAAI,IAAI;GACR,SAAS,IAAI;GACb,UAAU,IAAI;GACd,QAAQ,IAAI;GACZ;GACA;GACA,aAAa,IAAI;GACjB,WAAW,IAAI;GACf,YAAY,IAAI;GAChB,WAAY,IAAI,cAAgC;GAChD,QAAQ,IAAI;GACZ,WAAW,IAAI;GACf,WAAW,IAAI;GAChB"}
1
+ {"version":3,"file":"apitap-store.js","names":[],"sources":["../../backends/apitap-store.ts"],"sourcesContent":["/**\n * ApiTap Store — SQLite backend for discovered API endpoints (Issue #614).\n *\n * Persists endpoints discovered by `apitap capture` / `apitap peek` with TTL expiry.\n * Each entry represents one parameterized API endpoint with sample request/response.\n *\n * Schema follows IssueStore / ToolProposalStore conventions.\n */\n\nimport { randomUUID } from \"node:crypto\";\nimport { mkdirSync } from \"node:fs\";\nimport { dirname } from \"node:path\";\nimport { DatabaseSync } from \"node:sqlite\";\nimport type { SQLInputValue } from \"node:sqlite\";\n\nimport { BaseSqliteStore } from \"./base-sqlite-store.js\";\n\n// ---------------------------------------------------------------------------\n// Public types\n// ---------------------------------------------------------------------------\n\ntype ApitapEndpointStatus = \"pending\" | \"reviewed\" | \"accepted\" | \"rejected\";\n\ninterface ApitapEndpoint {\n id: string;\n /** Original site URL that was captured. */\n siteUrl: string;\n /** Discovered API endpoint path (e.g. /api/v1/users). */\n endpoint: string;\n /** HTTP method (GET, POST, PUT, DELETE, PATCH). */\n method: string;\n /** Parsed parameters object. */\n parameters: Record<string, unknown>;\n /** Parsed sample response body. */\n sampleResponse: unknown;\n /** Content-Type of response. */\n contentType: string;\n /** Session ID that captured this endpoint. */\n sessionId: string;\n /** ISO timestamp when captured. */\n capturedAt: string;\n /** ISO timestamp when TTL expires (null = never). */\n expiresAt: string | null;\n status: ApitapEndpointStatus;\n createdAt: string;\n updatedAt: string;\n}\n\ninterface CreateApitapEndpointInput {\n siteUrl: string;\n endpoint: string;\n method: string;\n /** Parameters as an object (serialized to JSON) or JSON string. */\n parameters?: Record<string, unknown> | string;\n /** Sample response (serialized to JSON) or JSON string. */\n sampleResponse?: unknown | string;\n contentType?: string;\n sessionId?: string;\n capturedAt?: string;\n /** ISO timestamp for expiry (null = never). Takes priority over endpointTtlDays. */\n expiresAt?: string | null;\n /** Convenience: compute expiresAt from TTL days from now. */\n endpointTtlDays?: number;\n}\n\ninterface ApitapEndpointFilter {\n siteUrl?: string;\n sessionId?: string;\n status?: ApitapEndpointStatus;\n includeExpired?: boolean;\n limit?: number;\n}\n\n// ---------------------------------------------------------------------------\n// ApitapStore\n// ---------------------------------------------------------------------------\n\nexport class ApitapStore extends BaseSqliteStore {\n constructor(dbPath: string) {\n mkdirSync(dirname(dbPath), { recursive: true });\n const db = new DatabaseSync(dbPath);\n super(db);\n\n this.liveDb.exec(`\n CREATE TABLE IF NOT EXISTS apitap_endpoints (\n id TEXT PRIMARY KEY,\n site_url TEXT NOT NULL,\n endpoint TEXT NOT NULL,\n method TEXT NOT NULL DEFAULT 'GET',\n parameters TEXT NOT NULL DEFAULT '{}',\n sample_response TEXT NOT NULL DEFAULT '',\n content_type TEXT NOT NULL DEFAULT '',\n session_id TEXT NOT NULL DEFAULT '',\n captured_at TEXT NOT NULL DEFAULT (datetime('now')),\n expires_at TEXT,\n status TEXT NOT NULL DEFAULT 'pending',\n created_at TEXT NOT NULL DEFAULT (datetime('now')),\n updated_at TEXT NOT NULL DEFAULT (datetime('now'))\n );\n\n CREATE INDEX IF NOT EXISTS idx_apitap_site_url ON apitap_endpoints(site_url);\n CREATE INDEX IF NOT EXISTS idx_apitap_session ON apitap_endpoints(session_id);\n CREATE INDEX IF NOT EXISTS idx_apitap_status ON apitap_endpoints(status);\n CREATE INDEX IF NOT EXISTS idx_apitap_expires ON apitap_endpoints(expires_at);\n `);\n }\n\n protected getSubsystemName(): string {\n return \"apitap-store\";\n }\n\n // -------------------------------------------------------------------------\n // create\n // -------------------------------------------------------------------------\n\n create(input: CreateApitapEndpointInput): ApitapEndpoint {\n const id = randomUUID();\n const now = new Date().toISOString();\n\n // Serialize parameters/sampleResponse objects to JSON strings for SQLite\n const parametersStr =\n typeof input.parameters === \"string\" ? input.parameters : JSON.stringify(input.parameters ?? {});\n const sampleResponseStr =\n typeof input.sampleResponse === \"string\" ? input.sampleResponse : JSON.stringify(input.sampleResponse ?? null);\n\n // Compute expiresAt: explicit value wins; endpointTtlDays computes if no explicit expiresAt\n let expiresAt: string | null = input.expiresAt !== undefined ? (input.expiresAt ?? null) : null;\n if (expiresAt === null && !(\"expiresAt\" in input) && input.endpointTtlDays && input.endpointTtlDays > 0) {\n expiresAt = new Date(Date.now() + input.endpointTtlDays * 24 * 60 * 60_000).toISOString();\n }\n\n this.liveDb\n .prepare(\n `INSERT INTO apitap_endpoints\n (id, site_url, endpoint, method, parameters, sample_response, content_type,\n session_id, captured_at, expires_at, status, created_at, updated_at)\n VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 'pending', ?, ?)`,\n )\n .run(\n id,\n input.siteUrl,\n input.endpoint,\n input.method.toUpperCase(),\n parametersStr,\n sampleResponseStr,\n input.contentType ?? \"\",\n input.sessionId ?? \"\",\n input.capturedAt ?? now,\n expiresAt,\n now,\n now,\n );\n\n // biome-ignore lint/style/noNonNullAssertion: Known to exist\n return this.getById(id)!;\n }\n\n // -------------------------------------------------------------------------\n // getById\n // -------------------------------------------------------------------------\n\n getById(id: string): ApitapEndpoint | null {\n const row = this.liveDb.prepare(\"SELECT * FROM apitap_endpoints WHERE id = ?\").get(id) as\n | Record<string, unknown>\n | undefined;\n if (!row) return null;\n return this.rowToEndpoint(row);\n }\n\n // -------------------------------------------------------------------------\n // list — filtered listing\n // -------------------------------------------------------------------------\n\n list(filter?: ApitapEndpointFilter): ApitapEndpoint[] {\n let query = \"SELECT * FROM apitap_endpoints WHERE 1=1\";\n const params: SQLInputValue[] = [];\n\n if (filter?.siteUrl) {\n const escapedSiteUrl = filter.siteUrl.replace(/\\\\/g, \"\\\\\\\\\").replace(/[%_]/g, \"\\\\$&\");\n query += \" AND site_url LIKE ? ESCAPE '\\\\'\";\n params.push(`%${escapedSiteUrl}%`);\n }\n if (filter?.sessionId) {\n query += \" AND session_id = ?\";\n params.push(filter.sessionId);\n }\n if (filter?.status) {\n query += \" AND status = ?\";\n params.push(filter.status);\n }\n if (!filter?.includeExpired) {\n query += \" AND (expires_at IS NULL OR expires_at > strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))\";\n }\n\n query += \" ORDER BY created_at DESC\";\n\n if (filter?.limit && filter.limit > 0) {\n query += \" LIMIT ?\";\n params.push(filter.limit);\n }\n\n const rows = this.liveDb.prepare(query).all(...params) as Record<string, unknown>[];\n return rows.map((r) => this.rowToEndpoint(r));\n }\n\n // -------------------------------------------------------------------------\n // updateStatus\n // -------------------------------------------------------------------------\n\n updateStatus(id: string, status: ApitapEndpointStatus): ApitapEndpoint | null {\n const now = new Date().toISOString();\n const result = this.liveDb\n .prepare(\"UPDATE apitap_endpoints SET status = ?, updated_at = ? WHERE id = ?\")\n .run(status, now, id);\n\n if (result.changes === 0) return null;\n return this.getById(id);\n }\n\n // -------------------------------------------------------------------------\n // deleteExpired — prune endpoints past their TTL\n // -------------------------------------------------------------------------\n\n deleteExpired(): number {\n const result = this.liveDb\n .prepare(\n \"DELETE FROM apitap_endpoints WHERE expires_at IS NOT NULL AND expires_at <= strftime('%Y-%m-%dT%H:%M:%fZ', 'now')\",\n )\n .run();\n return Number(result.changes);\n }\n\n // -------------------------------------------------------------------------\n // count\n // -------------------------------------------------------------------------\n\n count(status?: ApitapEndpointStatus): number {\n const row = status\n ? (this.liveDb.prepare(\"SELECT COUNT(*) as n FROM apitap_endpoints WHERE status = ?\").get(status) as {\n n: number;\n })\n : (this.liveDb.prepare(\"SELECT COUNT(*) as n FROM apitap_endpoints\").get() as { n: number });\n return row.n;\n }\n\n // -------------------------------------------------------------------------\n // existsForSession — check if a session has already been captured\n // -------------------------------------------------------------------------\n\n countForSession(sessionId: string): number {\n const row = this.liveDb\n .prepare(\"SELECT COUNT(*) as n FROM apitap_endpoints WHERE session_id = ?\")\n .get(sessionId) as {\n n: number;\n };\n return row.n;\n }\n\n // -------------------------------------------------------------------------\n // Private helpers\n // -------------------------------------------------------------------------\n\n private rowToEndpoint(row: Record<string, unknown>): ApitapEndpoint {\n let parameters: Record<string, unknown> = {};\n try {\n parameters = JSON.parse((row.parameters as string) ?? \"{}\");\n } catch {\n /* keep empty */\n }\n let sampleResponse: unknown = null;\n try {\n sampleResponse = JSON.parse((row.sample_response as string) ?? \"null\");\n } catch {\n /* keep null */\n }\n return {\n id: row.id as string,\n siteUrl: row.site_url as string,\n endpoint: row.endpoint as string,\n method: row.method as string,\n parameters,\n sampleResponse,\n contentType: row.content_type as string,\n sessionId: row.session_id as string,\n capturedAt: row.captured_at as string,\n expiresAt: (row.expires_at as string | null) ?? null,\n status: row.status as ApitapEndpointStatus,\n createdAt: row.created_at as string,\n updatedAt: row.updated_at as string,\n };\n }\n}\n"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"audit-store.js","names":[],"sources":["../../backends/audit-store.ts"],"sourcesContent":["/**\n * Cross-agent audit log (Issue #790) — SQLite WAL, local-only.\n */\n\nimport { randomUUID } from \"node:crypto\";\nimport { mkdirSync } from \"node:fs\";\nimport { dirname, join } from \"node:path\";\nimport { DatabaseSync } from \"node:sqlite\";\nimport { BaseSqliteStore } from \"./base-sqlite-store.js\";\n\ntype AuditOutcome = \"success\" | \"partial\" | \"failed\" | \"skipped\";\n\nexport interface AuditEventInput {\n timestamp?: number;\n agentId: string;\n action: string;\n target?: string | null;\n outcome: AuditOutcome;\n durationMs?: number;\n error?: string;\n context?: Record<string, unknown>;\n sessionId?: string;\n model?: string;\n tokens?: number;\n}\n\ninterface AuditEventRow {\n id: string;\n timestamp: number;\n agentId: string;\n action: string;\n target: string | null;\n outcome: AuditOutcome;\n durationMs: number | null;\n error: string | null;\n context: Record<string, unknown> | null;\n sessionId: string | null;\n model: string | null;\n tokens: number | null;\n}\n\nconst SENSITIVE_KEYS = /(api[_-]?key|password|secret|authorization|bearer|cookie|\\btoken\\b)/i;\n\nfunction scrubValue(value: unknown, seen: WeakSet<object>): unknown {\n // Primitives (except bigint) are safe and JSON-serializable as-is.\n if (\n value === null ||\n typeof value === \"string\" ||\n typeof value === \"number\" ||\n typeof value === \"boolean\" ||\n typeof value === \"undefined\"\n ) {\n return value;\n }\n\n if (typeof value === \"bigint\") {\n // Normalize bigint to string to avoid JSON serialization errors.\n return value.toString();\n }\n\n if (typeof value === \"function\" || typeof value === \"symbol\") {\n // Functions and symbols are not JSON-serializable; avoid leaking details.\n return \"[non-serializable]\";\n }\n\n if (typeof value === \"object\") {\n const obj = value as object;\n\n if (seen.has(obj)) {\n // Prevent infinite recursion on cyclic structures.\n return \"[circular]\";\n }\n seen.add(obj);\n\n if (Array.isArray(value)) {\n // Recursively scrub each array element (handles arrays of objects with sensitive keys).\n return (value as unknown[]).map((item) => scrubValue(item, seen));\n }\n\n const record = value as Record<string, unknown>;\n const out: Record<string, unknown> = {};\n for (const [k, v] of Object.entries(record)) {\n if (SENSITIVE_KEYS.test(k)) {\n out[k] = \"[redacted]\";\n } else {\n out[k] = scrubValue(v, seen);\n }\n }\n return out;\n }\n\n // Fallback for any remaining unexpected types.\n return \"[non-serializable]\";\n}\n\nfunction scrubContext(obj: Record<string, unknown>): Record<string, unknown> {\n const seen = new WeakSet<object>();\n const scrubbed = scrubValue(obj, seen);\n\n // Ensure we always return a plain object as declared.\n if (scrubbed && typeof scrubbed === \"object\" && !Array.isArray(scrubbed)) {\n return scrubbed as Record<string, unknown>;\n }\n return {};\n}\n\nexport class AuditStore extends BaseSqliteStore {\n /** Cached from `sqlite_master`: whether live `audit_log` CHECK includes `skipped`. */\n private auditLogAllowsSkipped: boolean | undefined;\n\n constructor(dbPath: string) {\n mkdirSync(dirname(dbPath), { recursive: true });\n const db = new DatabaseSync(dbPath);\n super(db);\n this.liveDb.exec(`\n CREATE TABLE IF NOT EXISTS audit_log (\n id TEXT PRIMARY KEY,\n timestamp INTEGER NOT NULL,\n agent_id TEXT NOT NULL,\n action TEXT NOT NULL,\n target TEXT,\n outcome TEXT NOT NULL CHECK(outcome IN ('success','partial','failed','skipped')),\n duration_ms INTEGER,\n error TEXT,\n context TEXT,\n session_id TEXT,\n model TEXT,\n tokens INTEGER\n );\n CREATE INDEX IF NOT EXISTS idx_audit_agent_ts ON audit_log(agent_id, timestamp);\n CREATE INDEX IF NOT EXISTS idx_audit_action_ts ON audit_log(action, timestamp);\n CREATE INDEX IF NOT EXISTS idx_audit_target ON audit_log(target);\n CREATE INDEX IF NOT EXISTS idx_audit_session ON audit_log(session_id, timestamp);\n CREATE INDEX IF NOT EXISTS idx_audit_ts ON audit_log(timestamp);\n `);\n }\n\n protected getSubsystemName(): string {\n return \"audit-store\";\n }\n\n /**\n * Legacy `audit_log` CHECK may be `('success','partial','failed')` without\n * `skipped`. Map `skipped` → `failed` for inserts only (semantic: suppressed /\n * no-op path, not a hard error).\n */\n private outcomeForAuditInsert(outcome: AuditOutcome): AuditOutcome {\n if (outcome !== \"skipped\") {\n return outcome;\n }\n if (this.auditLogAllowsSkipped === undefined) {\n const row = this.liveDb\n .prepare(\"SELECT sql FROM sqlite_master WHERE type = 'table' AND name = 'audit_log'\")\n .get() as { sql?: string } | undefined;\n const sql = typeof row?.sql === \"string\" ? row.sql : \"\";\n this.auditLogAllowsSkipped = sql.includes(\"'skipped'\");\n }\n return this.auditLogAllowsSkipped ? outcome : \"failed\";\n }\n\n append(input: AuditEventInput): string {\n const id = randomUUID();\n const ts = input.timestamp ?? Date.now();\n const ctxJson = input.context != null ? JSON.stringify(scrubContext(input.context)) : null;\n const outcome = this.outcomeForAuditInsert(input.outcome);\n this.liveDb\n .prepare(\n `INSERT INTO audit_log (id, timestamp, agent_id, action, target, outcome, duration_ms, error, context, session_id, model, tokens)\n VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,\n )\n .run(\n id,\n ts,\n input.agentId,\n input.action,\n input.target ?? null,\n outcome,\n input.durationMs ?? null,\n input.error ?? null,\n ctxJson,\n input.sessionId ?? null,\n input.model ?? null,\n input.tokens ?? null,\n );\n return id;\n }\n\n query(opts: {\n sinceMs?: number;\n untilMs?: number;\n agentId?: string;\n action?: string;\n outcome?: AuditOutcome;\n targetContains?: string;\n sessionId?: string;\n limit?: number;\n }): AuditEventRow[] {\n const limit = Math.min(Math.max(1, opts.limit ?? 200), 5000);\n const clauses: string[] = [];\n const params: (string | number)[] = [];\n if (opts.sinceMs != null) {\n clauses.push(\"timestamp >= ?\");\n params.push(opts.sinceMs);\n }\n if (opts.untilMs != null) {\n clauses.push(\"timestamp <= ?\");\n params.push(opts.untilMs);\n }\n if (opts.agentId) {\n clauses.push(\"agent_id = ?\");\n params.push(opts.agentId);\n }\n if (opts.action) {\n clauses.push(\"action = ?\");\n params.push(opts.action);\n }\n if (opts.outcome) {\n clauses.push(\"outcome = ?\");\n params.push(opts.outcome);\n }\n if (opts.targetContains) {\n const escapedTarget = opts.targetContains.replace(/\\\\/g, \"\\\\\\\\\").replace(/[%_]/g, \"\\\\$&\");\n clauses.push(\"target LIKE ? ESCAPE '\\\\'\");\n params.push(`%${escapedTarget}%`);\n }\n if (opts.sessionId) {\n clauses.push(\"session_id = ?\");\n params.push(opts.sessionId);\n }\n const where = clauses.length > 0 ? `WHERE ${clauses.join(\" AND \")}` : \"\";\n const rows = this.liveDb\n .prepare(\n `SELECT id, timestamp, agent_id, action, target, outcome, duration_ms, error, context, session_id, model, tokens\n FROM audit_log ${where} ORDER BY timestamp DESC LIMIT ?`,\n )\n .all(...params, limit) as Array<{\n id: string;\n timestamp: number;\n agent_id: string;\n action: string;\n target: string | null;\n outcome: string;\n duration_ms: number | null;\n error: string | null;\n context: string | null;\n session_id: string | null;\n model: string | null;\n tokens: number | null;\n }>;\n return rows.map((r) => ({\n id: r.id,\n timestamp: r.timestamp,\n agentId: r.agent_id,\n action: r.action,\n target: r.target,\n outcome: r.outcome as AuditOutcome,\n durationMs: r.duration_ms,\n error: r.error,\n context: r.context ? (JSON.parse(r.context) as Record<string, unknown>) : null,\n sessionId: r.session_id,\n model: r.model,\n tokens: r.tokens,\n }));\n }\n\n summary24h(): {\n total: number;\n byOutcome: Record<AuditOutcome, number>;\n byAgent: Record<string, number>;\n } {\n const since = Date.now() - 24 * 3600 * 1000;\n const rows = this.liveDb\n .prepare(\"SELECT outcome, agent_id, COUNT(*) as c FROM audit_log WHERE timestamp >= ? GROUP BY outcome, agent_id\")\n .all(since) as Array<{ outcome: string; agent_id: string; c: number }>;\n let total = 0;\n const byOutcome: Record<AuditOutcome, number> = { success: 0, partial: 0, failed: 0, skipped: 0 };\n const byAgent: Record<string, number> = {};\n for (const r of rows) {\n const c = Number(r.c);\n total += c;\n const o = r.outcome as AuditOutcome;\n if (o in byOutcome) byOutcome[o] += c;\n byAgent[r.agent_id] = (byAgent[r.agent_id] ?? 0) + c;\n }\n return { total, byOutcome, byAgent };\n }\n\n /** Remove entries older than retention days (default 90). */\n prune(retentionDays = 90): number {\n const cutoff = Date.now() - retentionDays * 24 * 3600 * 1000;\n const r = this.liveDb.prepare(\"DELETE FROM audit_log WHERE timestamp < ?\").run(cutoff);\n return Number(r.changes ?? 0);\n }\n}\n\nexport function auditDbPathForMemorySqlite(memorySqlitePath: string): string | null {\n if (!memorySqlitePath || memorySqlitePath === \":memory:\") return null;\n return join(dirname(memorySqlitePath), \"audit.db\");\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"audit-store.js","names":[],"sources":["../../backends/audit-store.ts"],"sourcesContent":["/**\n * Cross-agent audit log (Issue #790) — SQLite WAL, local-only.\n */\n\nimport { randomUUID } from \"node:crypto\";\nimport { mkdirSync } from \"node:fs\";\nimport { dirname, join } from \"node:path\";\nimport { DatabaseSync } from \"node:sqlite\";\nimport { BaseSqliteStore } from \"./base-sqlite-store.js\";\n\ntype AuditOutcome = \"success\" | \"partial\" | \"failed\" | \"skipped\";\n\nexport interface AuditEventInput {\n timestamp?: number;\n agentId: string;\n action: string;\n target?: string | null;\n outcome: AuditOutcome;\n durationMs?: number;\n error?: string;\n context?: Record<string, unknown>;\n sessionId?: string;\n model?: string;\n tokens?: number;\n}\n\ninterface AuditEventRow {\n id: string;\n timestamp: number;\n agentId: string;\n action: string;\n target: string | null;\n outcome: AuditOutcome;\n durationMs: number | null;\n error: string | null;\n context: Record<string, unknown> | null;\n sessionId: string | null;\n model: string | null;\n tokens: number | null;\n}\n\nconst SENSITIVE_KEYS = /(api[_-]?key|password|secret|authorization|bearer|cookie|\\btoken\\b)/i;\n\nfunction scrubValue(value: unknown, seen: WeakSet<object>): unknown {\n // Primitives (except bigint) are safe and JSON-serializable as-is.\n if (\n value === null ||\n typeof value === \"string\" ||\n typeof value === \"number\" ||\n typeof value === \"boolean\" ||\n typeof value === \"undefined\"\n ) {\n return value;\n }\n\n if (typeof value === \"bigint\") {\n // Normalize bigint to string to avoid JSON serialization errors.\n return value.toString();\n }\n\n if (typeof value === \"function\" || typeof value === \"symbol\") {\n // Functions and symbols are not JSON-serializable; avoid leaking details.\n return \"[non-serializable]\";\n }\n\n if (typeof value === \"object\") {\n const obj = value as object;\n\n if (seen.has(obj)) {\n // Prevent infinite recursion on cyclic structures.\n return \"[circular]\";\n }\n seen.add(obj);\n\n if (Array.isArray(value)) {\n // Recursively scrub each array element (handles arrays of objects with sensitive keys).\n return (value as unknown[]).map((item) => scrubValue(item, seen));\n }\n\n const record = value as Record<string, unknown>;\n const out: Record<string, unknown> = {};\n for (const [k, v] of Object.entries(record)) {\n if (SENSITIVE_KEYS.test(k)) {\n out[k] = \"[redacted]\";\n } else {\n out[k] = scrubValue(v, seen);\n }\n }\n return out;\n }\n\n // Fallback for any remaining unexpected types.\n return \"[non-serializable]\";\n}\n\nfunction scrubContext(obj: Record<string, unknown>): Record<string, unknown> {\n const seen = new WeakSet<object>();\n const scrubbed = scrubValue(obj, seen);\n\n // Ensure we always return a plain object as declared.\n if (scrubbed && typeof scrubbed === \"object\" && !Array.isArray(scrubbed)) {\n return scrubbed as Record<string, unknown>;\n }\n return {};\n}\n\nexport class AuditStore extends BaseSqliteStore {\n /** Cached from `sqlite_master`: whether live `audit_log` CHECK includes `skipped`. */\n private auditLogAllowsSkipped: boolean | undefined;\n\n constructor(dbPath: string) {\n mkdirSync(dirname(dbPath), { recursive: true });\n const db = new DatabaseSync(dbPath);\n super(db);\n this.liveDb.exec(`\n CREATE TABLE IF NOT EXISTS audit_log (\n id TEXT PRIMARY KEY,\n timestamp INTEGER NOT NULL,\n agent_id TEXT NOT NULL,\n action TEXT NOT NULL,\n target TEXT,\n outcome TEXT NOT NULL CHECK(outcome IN ('success','partial','failed','skipped')),\n duration_ms INTEGER,\n error TEXT,\n context TEXT,\n session_id TEXT,\n model TEXT,\n tokens INTEGER\n );\n CREATE INDEX IF NOT EXISTS idx_audit_agent_ts ON audit_log(agent_id, timestamp);\n CREATE INDEX IF NOT EXISTS idx_audit_action_ts ON audit_log(action, timestamp);\n CREATE INDEX IF NOT EXISTS idx_audit_target ON audit_log(target);\n CREATE INDEX IF NOT EXISTS idx_audit_session ON audit_log(session_id, timestamp);\n CREATE INDEX IF NOT EXISTS idx_audit_ts ON audit_log(timestamp);\n `);\n }\n\n protected getSubsystemName(): string {\n return \"audit-store\";\n }\n\n /**\n * Legacy `audit_log` CHECK may be `('success','partial','failed')` without\n * `skipped`. Map `skipped` → `failed` for inserts only (semantic: suppressed /\n * no-op path, not a hard error).\n */\n private outcomeForAuditInsert(outcome: AuditOutcome): AuditOutcome {\n if (outcome !== \"skipped\") {\n return outcome;\n }\n if (this.auditLogAllowsSkipped === undefined) {\n const row = this.liveDb\n .prepare(\"SELECT sql FROM sqlite_master WHERE type = 'table' AND name = 'audit_log'\")\n .get() as { sql?: string } | undefined;\n const sql = typeof row?.sql === \"string\" ? row.sql : \"\";\n this.auditLogAllowsSkipped = sql.includes(\"'skipped'\");\n }\n return this.auditLogAllowsSkipped ? outcome : \"failed\";\n }\n\n append(input: AuditEventInput): string {\n const id = randomUUID();\n const ts = input.timestamp ?? Date.now();\n const ctxJson = input.context != null ? JSON.stringify(scrubContext(input.context)) : null;\n const outcome = this.outcomeForAuditInsert(input.outcome);\n this.liveDb\n .prepare(\n `INSERT INTO audit_log (id, timestamp, agent_id, action, target, outcome, duration_ms, error, context, session_id, model, tokens)\n VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,\n )\n .run(\n id,\n ts,\n input.agentId,\n input.action,\n input.target ?? null,\n outcome,\n input.durationMs ?? null,\n input.error ?? null,\n ctxJson,\n input.sessionId ?? null,\n input.model ?? null,\n input.tokens ?? null,\n );\n return id;\n }\n\n query(opts: {\n sinceMs?: number;\n untilMs?: number;\n agentId?: string;\n action?: string;\n outcome?: AuditOutcome;\n targetContains?: string;\n sessionId?: string;\n limit?: number;\n }): AuditEventRow[] {\n const limit = Math.min(Math.max(1, opts.limit ?? 200), 5000);\n const clauses: string[] = [];\n const params: (string | number)[] = [];\n if (opts.sinceMs != null) {\n clauses.push(\"timestamp >= ?\");\n params.push(opts.sinceMs);\n }\n if (opts.untilMs != null) {\n clauses.push(\"timestamp <= ?\");\n params.push(opts.untilMs);\n }\n if (opts.agentId) {\n clauses.push(\"agent_id = ?\");\n params.push(opts.agentId);\n }\n if (opts.action) {\n clauses.push(\"action = ?\");\n params.push(opts.action);\n }\n if (opts.outcome) {\n clauses.push(\"outcome = ?\");\n params.push(opts.outcome);\n }\n if (opts.targetContains) {\n const escapedTarget = opts.targetContains.replace(/\\\\/g, \"\\\\\\\\\").replace(/[%_]/g, \"\\\\$&\");\n clauses.push(\"target LIKE ? ESCAPE '\\\\'\");\n params.push(`%${escapedTarget}%`);\n }\n if (opts.sessionId) {\n clauses.push(\"session_id = ?\");\n params.push(opts.sessionId);\n }\n const where = clauses.length > 0 ? `WHERE ${clauses.join(\" AND \")}` : \"\";\n const rows = this.liveDb\n .prepare(\n `SELECT id, timestamp, agent_id, action, target, outcome, duration_ms, error, context, session_id, model, tokens\n FROM audit_log ${where} ORDER BY timestamp DESC LIMIT ?`,\n )\n .all(...params, limit) as Array<{\n id: string;\n timestamp: number;\n agent_id: string;\n action: string;\n target: string | null;\n outcome: string;\n duration_ms: number | null;\n error: string | null;\n context: string | null;\n session_id: string | null;\n model: string | null;\n tokens: number | null;\n }>;\n return rows.map((r) => ({\n id: r.id,\n timestamp: r.timestamp,\n agentId: r.agent_id,\n action: r.action,\n target: r.target,\n outcome: r.outcome as AuditOutcome,\n durationMs: r.duration_ms,\n error: r.error,\n context: r.context ? (JSON.parse(r.context) as Record<string, unknown>) : null,\n sessionId: r.session_id,\n model: r.model,\n tokens: r.tokens,\n }));\n }\n\n summary24h(): {\n total: number;\n byOutcome: Record<AuditOutcome, number>;\n byAgent: Record<string, number>;\n } {\n const since = Date.now() - 24 * 3600 * 1000;\n const rows = this.liveDb\n .prepare(\"SELECT outcome, agent_id, COUNT(*) as c FROM audit_log WHERE timestamp >= ? GROUP BY outcome, agent_id\")\n .all(since) as Array<{ outcome: string; agent_id: string; c: number }>;\n let total = 0;\n const byOutcome: Record<AuditOutcome, number> = { success: 0, partial: 0, failed: 0, skipped: 0 };\n const byAgent: Record<string, number> = {};\n for (const r of rows) {\n const c = Number(r.c);\n total += c;\n const o = r.outcome as AuditOutcome;\n if (o in byOutcome) byOutcome[o] += c;\n byAgent[r.agent_id] = (byAgent[r.agent_id] ?? 0) + c;\n }\n return { total, byOutcome, byAgent };\n }\n\n /** Remove entries older than retention days (default 90). */\n prune(retentionDays = 90): number {\n const cutoff = Date.now() - retentionDays * 24 * 3600 * 1000;\n const r = this.liveDb.prepare(\"DELETE FROM audit_log WHERE timestamp < ?\").run(cutoff);\n return Number(r.changes ?? 0);\n }\n}\n\nexport function auditDbPathForMemorySqlite(memorySqlitePath: string): string | null {\n if (!memorySqlitePath || memorySqlitePath === \":memory:\") return null;\n return join(dirname(memorySqlitePath), \"audit.db\");\n}\n"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"base-sqlite-store.js","names":[],"sources":["../../backends/base-sqlite-store.ts"],"sourcesContent":["/**\n * Base class for SQLite stores with defensive reconnection logic.\n *\n * Provides shared functionality for:\n * - Database connection management with automatic reopening after SIGUSR1\n * - Pragma application (WAL mode, busy timeout, optional foreign keys)\n * - Lifecycle tracking (open/closed state)\n * - Optional deferred close: when `deferClose` is true, `close()` waits until no\n * `runWithDb` operation is in flight (#1015, plugin reload vs in-flight list).\n *\n * Subclasses must call `this.liveDb` to access the database handle instead of\n * accessing `this.db` directly. The getter ensures the connection is open and\n * pragmas are reapplied after a restart.\n *\n * Stores with `deferClose: true` should wrap each public DB operation in\n * `this.runWithDb(\"operationName\", () => { ... })` and use `*Internal` helpers\n * to avoid nested `runWithDb` calls.\n */\n\nimport type { DatabaseSync } from \"node:sqlite\";\nimport { capturePluginError } from \"../services/error-reporter.js\";\nimport { SQLITE_BUSY_TIMEOUT_MS } from \"../utils/constants.js\";\n\ninterface BaseSqliteStoreOptions {\n /** Enable foreign key constraints (default: false). */\n foreignKeys?: boolean;\n /** Additional custom pragmas to apply on open/reopen. */\n customPragmas?: string[];\n /**\n * When true, `close()` only runs after in-flight `runWithDb` work completes (#1015).\n * Subclasses must use `runWithDb` for operations that touch `liveDb`.\n */\n deferClose?: boolean;\n}\n\ntype ClosePhase = \"open\" | \"closing\" | \"shutdown\";\n\nexport abstract class BaseSqliteStore {\n protected db: DatabaseSync;\n protected _dbOpen = true;\n private _closed = false;\n private readonly options: BaseSqliteStoreOptions;\n private readonly deferClose: boolean;\n private activeOps = 0;\n private closePhase: ClosePhase = \"open\";\n\n constructor(db: DatabaseSync, options: BaseSqliteStoreOptions = {}) {\n this.db = db;\n this.options = options;\n this.deferClose = options.deferClose === true;\n this.applyPragmas();\n }\n\n protected applyPragmas(): void {\n this.db.exec(\"PRAGMA journal_mode = WAL\");\n this.db.exec(`PRAGMA busy_timeout = ${SQLITE_BUSY_TIMEOUT_MS}`);\n\n if (this.options.foreignKeys) {\n this.db.exec(\"PRAGMA foreign_keys = ON\");\n }\n\n if (this.options.customPragmas) {\n for (const pragma of this.options.customPragmas) {\n this.db.exec(pragma);\n }\n }\n }\n\n protected get closed(): boolean {\n return this._closed;\n }\n\n protected get liveDb(): DatabaseSync {\n if (this.closePhase === \"shutdown\") {\n throw new Error(\"The database connection is not open\");\n }\n if (this.deferClose && this.closePhase === \"closing\" && this.activeOps === 0) {\n throw new Error(\"The database connection is not open\");\n }\n if (!this._dbOpen) {\n this.db.open();\n this._dbOpen = true;\n this._closed = false;\n this.applyPragmas();\n }\n return this.db;\n }\n\n /**\n * Wrap a synchronous DB operation. With `deferClose`, participates in reference-counted\n * shutdown so `close()` does not run until the callback returns.\n */\n protected runWithDb<T>(operation: string, fn: () => T): T {\n if (!this.deferClose) {\n return this.runSqliteOp(operation, fn);\n }\n if (this.closePhase === \"shutdown\") {\n throw new Error(\"The database connection is not open\");\n }\n if (this.closePhase === \"closing\") {\n throw new Error(\"The database connection is not open\");\n }\n this.activeOps += 1;\n try {\n return this.runSqliteOp(operation, fn);\n } finally {\n this.activeOps -= 1;\n // `close()` may set phase to \"closing\" while this op runs — TS cannot see that mutation.\n const phaseAfter = this.closePhase as ClosePhase;\n if (this.deferClose && phaseAfter === \"closing\" && this.activeOps === 0) {\n this.finalizeShutdown();\n }\n }\n }\n\n /**\n * Run a synchronous DB operation; if the native handle was closed while `_dbOpen` stayed true\n * (lifecycle race, external close), reopen once and retry (#968).\n */\n protected runSqliteOp<T>(operation: string, fn: () => T): T {\n try {\n return fn();\n } catch (err) {\n const msg = err instanceof Error ? err.message : String(err);\n if (!/not open|connection is not open|The database connection is not open|database is not open/i.test(msg)) {\n throw err;\n }\n const phase = this.closePhase as ClosePhase;\n if (phase === \"shutdown\" || (this.deferClose && phase === \"closing\")) {\n throw err;\n }\n capturePluginError(err instanceof Error ? err : new Error(String(err)), {\n subsystem: this.getSubsystemName(),\n operation,\n phase: \"sqlite-reconnect\",\n severity: \"info\",\n });\n this._dbOpen = false;\n this._closed = false;\n try {\n this.db.open();\n } catch (openErr) {\n capturePluginError(openErr instanceof Error ? openErr : new Error(String(openErr)), {\n subsystem: this.getSubsystemName(),\n operation: `${operation}:reopen-failed`,\n severity: \"warning\",\n });\n throw openErr instanceof Error ? openErr : new Error(String(openErr));\n }\n this._dbOpen = true;\n this.applyPragmas();\n return fn();\n }\n }\n\n isOpen(): boolean {\n if (this.deferClose) {\n return this.closePhase === \"open\" && this._dbOpen;\n }\n return !this._closed && this._dbOpen;\n }\n\n /**\n * Permanently close this store, preventing any future reopening via `liveDb`.\n *\n * Unlike `close()`, which is designed for background maintenance and allows\n * the store to auto-reopen on the next use, `permanentClose()` transitions the\n * internal phase to \"shutdown\" so that any subsequent access via `liveDb` throws\n * \"The database connection is not open\".\n *\n * This must be called during runtime teardown (e.g. plugin re-registration) to\n * prevent stale tool/hook closures from silently resurrecting old SQLite handles\n * and accumulating duplicate DB connections (issue #1550).\n *\n * For deferClose stores: if operations are currently in flight the native handle\n * is closed immediately; any in-flight ops that later try to access `liveDb`\n * will throw rather than using a half-closed connection.\n */\n permanentClose(): void {\n if (this.closePhase === \"shutdown\") return;\n this.closePhase = \"shutdown\";\n this._closed = true;\n const wasOpen = this._dbOpen;\n this._dbOpen = false;\n if (wasOpen) {\n try {\n this.db.close();\n } catch (err) {\n capturePluginError(err instanceof Error ? err : new Error(String(err)), {\n operation: \"db-permanent-close\",\n subsystem: this.getSubsystemName(),\n severity: \"warning\",\n });\n }\n }\n }\n\n private finalizeShutdown(): void {\n if (this.closePhase === \"shutdown\") return;\n this.closePhase = \"shutdown\";\n this._closed = true;\n this._dbOpen = false;\n try {\n this.db.close();\n } catch (err) {\n capturePluginError(err instanceof Error ? err : new Error(String(err)), {\n operation: \"db-close\",\n subsystem: this.getSubsystemName(),\n severity: \"info\",\n });\n }\n }\n\n close(): void {\n if (this.deferClose) {\n // Make close idempotent and non-terminal for deferred stores.\n // We intentionally close the native handle immediately when idle, but keep\n // lifecycle in \"open\" so a subsequent operation can lazily reopen via `liveDb`.\n // This avoids runtime \"The database connection is not open\" for tools that\n // run after background maintenance closed the store (#1079).\n if (this.closePhase === \"closing\" && this.activeOps > 0) {\n return;\n }\n if (this.closePhase === \"shutdown\") {\n return;\n }\n if (this.activeOps > 0) {\n this.closePhase = \"closing\";\n return;\n }\n if (!this._dbOpen) return;\n\n this._dbOpen = false;\n this._closed = false;\n this.closePhase = \"open\";\n try {\n this.db.close();\n } catch (err) {\n capturePluginError(err instanceof Error ? err : new Error(String(err)), {\n operation: \"db-close\",\n subsystem: this.getSubsystemName(),\n severity: \"info\",\n });\n }\n return;\n }\n if (this._closed) return;\n this._closed = true;\n this._dbOpen = false;\n try {\n this.db.close();\n } catch (err) {\n capturePluginError(err instanceof Error ? err : new Error(String(err)), {\n operation: \"db-close\",\n subsystem: this.getSubsystemName(),\n severity: \"info\",\n });\n }\n }\n\n /**\n * Subclasses should override this to provide a descriptive name for error reporting.\n * Default implementation returns the class name.\n */\n protected getSubsystemName(): string {\n return this.constructor.name.toLowerCase();\n }\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"base-sqlite-store.js","names":[],"sources":["../../backends/base-sqlite-store.ts"],"sourcesContent":["/**\n * Base class for SQLite stores with defensive reconnection logic.\n *\n * Provides shared functionality for:\n * - Database connection management with automatic reopening after SIGUSR1\n * - Pragma application (WAL mode, busy timeout, optional foreign keys)\n * - Lifecycle tracking (open/closed state)\n * - Optional deferred close: when `deferClose` is true, `close()` waits until no\n * `runWithDb` operation is in flight (#1015, plugin reload vs in-flight list).\n *\n * Subclasses must call `this.liveDb` to access the database handle instead of\n * accessing `this.db` directly. The getter ensures the connection is open and\n * pragmas are reapplied after a restart.\n *\n * Stores with `deferClose: true` should wrap each public DB operation in\n * `this.runWithDb(\"operationName\", () => { ... })` and use `*Internal` helpers\n * to avoid nested `runWithDb` calls.\n */\n\nimport type { DatabaseSync } from \"node:sqlite\";\nimport { capturePluginError } from \"../services/error-reporter.js\";\nimport { SQLITE_BUSY_TIMEOUT_MS } from \"../utils/constants.js\";\n\ninterface BaseSqliteStoreOptions {\n /** Enable foreign key constraints (default: false). */\n foreignKeys?: boolean;\n /** Additional custom pragmas to apply on open/reopen. */\n customPragmas?: string[];\n /**\n * When true, `close()` only runs after in-flight `runWithDb` work completes (#1015).\n * Subclasses must use `runWithDb` for operations that touch `liveDb`.\n */\n deferClose?: boolean;\n}\n\ntype ClosePhase = \"open\" | \"closing\" | \"shutdown\";\n\nexport abstract class BaseSqliteStore {\n protected db: DatabaseSync;\n protected _dbOpen = true;\n private _closed = false;\n private readonly options: BaseSqliteStoreOptions;\n private readonly deferClose: boolean;\n private activeOps = 0;\n private closePhase: ClosePhase = \"open\";\n\n constructor(db: DatabaseSync, options: BaseSqliteStoreOptions = {}) {\n this.db = db;\n this.options = options;\n this.deferClose = options.deferClose === true;\n this.applyPragmas();\n }\n\n protected applyPragmas(): void {\n this.db.exec(\"PRAGMA journal_mode = WAL\");\n this.db.exec(`PRAGMA busy_timeout = ${SQLITE_BUSY_TIMEOUT_MS}`);\n\n if (this.options.foreignKeys) {\n this.db.exec(\"PRAGMA foreign_keys = ON\");\n }\n\n if (this.options.customPragmas) {\n for (const pragma of this.options.customPragmas) {\n this.db.exec(pragma);\n }\n }\n }\n\n protected get closed(): boolean {\n return this._closed;\n }\n\n protected get liveDb(): DatabaseSync {\n if (this.closePhase === \"shutdown\") {\n throw new Error(\"The database connection is not open\");\n }\n if (this.deferClose && this.closePhase === \"closing\" && this.activeOps === 0) {\n throw new Error(\"The database connection is not open\");\n }\n if (!this._dbOpen) {\n this.db.open();\n this._dbOpen = true;\n this._closed = false;\n this.applyPragmas();\n }\n return this.db;\n }\n\n /**\n * Wrap a synchronous DB operation. With `deferClose`, participates in reference-counted\n * shutdown so `close()` does not run until the callback returns.\n */\n protected runWithDb<T>(operation: string, fn: () => T): T {\n if (!this.deferClose) {\n return this.runSqliteOp(operation, fn);\n }\n if (this.closePhase === \"shutdown\") {\n throw new Error(\"The database connection is not open\");\n }\n if (this.closePhase === \"closing\") {\n throw new Error(\"The database connection is not open\");\n }\n this.activeOps += 1;\n try {\n return this.runSqliteOp(operation, fn);\n } finally {\n this.activeOps -= 1;\n // `close()` may set phase to \"closing\" while this op runs — TS cannot see that mutation.\n const phaseAfter = this.closePhase as ClosePhase;\n if (this.deferClose && phaseAfter === \"closing\" && this.activeOps === 0) {\n this.finalizeShutdown();\n }\n }\n }\n\n /**\n * Run a synchronous DB operation; if the native handle was closed while `_dbOpen` stayed true\n * (lifecycle race, external close), reopen once and retry (#968).\n */\n protected runSqliteOp<T>(operation: string, fn: () => T): T {\n try {\n return fn();\n } catch (err) {\n const msg = err instanceof Error ? err.message : String(err);\n if (!/not open|connection is not open|The database connection is not open|database is not open/i.test(msg)) {\n throw err;\n }\n const phase = this.closePhase as ClosePhase;\n if (phase === \"shutdown\" || (this.deferClose && phase === \"closing\")) {\n throw err;\n }\n capturePluginError(err instanceof Error ? err : new Error(String(err)), {\n subsystem: this.getSubsystemName(),\n operation,\n phase: \"sqlite-reconnect\",\n severity: \"info\",\n });\n this._dbOpen = false;\n this._closed = false;\n try {\n this.db.open();\n } catch (openErr) {\n capturePluginError(openErr instanceof Error ? openErr : new Error(String(openErr)), {\n subsystem: this.getSubsystemName(),\n operation: `${operation}:reopen-failed`,\n severity: \"warning\",\n });\n throw openErr instanceof Error ? openErr : new Error(String(openErr));\n }\n this._dbOpen = true;\n this.applyPragmas();\n return fn();\n }\n }\n\n isOpen(): boolean {\n if (this.deferClose) {\n return this.closePhase === \"open\" && this._dbOpen;\n }\n return !this._closed && this._dbOpen;\n }\n\n /**\n * Permanently close this store, preventing any future reopening via `liveDb`.\n *\n * Unlike `close()`, which is designed for background maintenance and allows\n * the store to auto-reopen on the next use, `permanentClose()` transitions the\n * internal phase to \"shutdown\" so that any subsequent access via `liveDb` throws\n * \"The database connection is not open\".\n *\n * This must be called during runtime teardown (e.g. plugin re-registration) to\n * prevent stale tool/hook closures from silently resurrecting old SQLite handles\n * and accumulating duplicate DB connections (issue #1550).\n *\n * For deferClose stores: if operations are currently in flight the native handle\n * is closed immediately; any in-flight ops that later try to access `liveDb`\n * will throw rather than using a half-closed connection.\n */\n permanentClose(): void {\n if (this.closePhase === \"shutdown\") return;\n this.closePhase = \"shutdown\";\n this._closed = true;\n const wasOpen = this._dbOpen;\n this._dbOpen = false;\n if (wasOpen) {\n try {\n this.db.close();\n } catch (err) {\n capturePluginError(err instanceof Error ? err : new Error(String(err)), {\n operation: \"db-permanent-close\",\n subsystem: this.getSubsystemName(),\n severity: \"warning\",\n });\n }\n }\n }\n\n private finalizeShutdown(): void {\n if (this.closePhase === \"shutdown\") return;\n this.closePhase = \"shutdown\";\n this._closed = true;\n this._dbOpen = false;\n try {\n this.db.close();\n } catch (err) {\n capturePluginError(err instanceof Error ? err : new Error(String(err)), {\n operation: \"db-close\",\n subsystem: this.getSubsystemName(),\n severity: \"info\",\n });\n }\n }\n\n close(): void {\n if (this.deferClose) {\n // Make close idempotent and non-terminal for deferred stores.\n // We intentionally close the native handle immediately when idle, but keep\n // lifecycle in \"open\" so a subsequent operation can lazily reopen via `liveDb`.\n // This avoids runtime \"The database connection is not open\" for tools that\n // run after background maintenance closed the store (#1079).\n if (this.closePhase === \"closing\" && this.activeOps > 0) {\n return;\n }\n if (this.closePhase === \"shutdown\") {\n return;\n }\n if (this.activeOps > 0) {\n this.closePhase = \"closing\";\n return;\n }\n if (!this._dbOpen) return;\n\n this._dbOpen = false;\n this._closed = false;\n this.closePhase = \"open\";\n try {\n this.db.close();\n } catch (err) {\n capturePluginError(err instanceof Error ? err : new Error(String(err)), {\n operation: \"db-close\",\n subsystem: this.getSubsystemName(),\n severity: \"info\",\n });\n }\n return;\n }\n if (this._closed) return;\n this._closed = true;\n this._dbOpen = false;\n try {\n this.db.close();\n } catch (err) {\n capturePluginError(err instanceof Error ? err : new Error(String(err)), {\n operation: \"db-close\",\n subsystem: this.getSubsystemName(),\n severity: \"info\",\n });\n }\n }\n\n /**\n * Subclasses should override this to provide a descriptive name for error reporting.\n * Default implementation returns the class name.\n */\n protected getSubsystemName(): string {\n return this.constructor.name.toLowerCase();\n }\n}\n"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"cost-tracker.js","names":[],"sources":["../../backends/cost-tracker.ts"],"sourcesContent":["/**\n * LLM Cost Tracker — persists per-call token usage into llm_cost_log table\n * and intelligent-automation savings into llm_savings_log, both in the\n * existing memory.db SQLite database (shared with FactsDB).\n *\n * ⚠️ Costs are estimates based on published model pricing, not billing-accurate.\n */\n\nimport type { DatabaseSync } from \"node:sqlite\";\nimport { estimateCost } from \"../services/model-pricing.js\";\nimport { pluginLogger } from \"../utils/logger.js\";\nimport type { FactsDB } from \"./facts-db.js\";\n\n/**\n * A savings entry records work performed automatically that would have\n * otherwise required manual LLM calls or human effort.\n * E.g. self-correction auto-fixing an incident, or auto-classify batching N facts.\n */\ninterface SavingsEntry {\n /** Feature that generated the savings (e.g. 'self-correction', 'auto-classify'). */\n feature: string;\n /** Human-readable description of the action (e.g. 'auto-fixed incident'). */\n action: string;\n /** Number of individual operations that were avoided or batched. */\n countAvoided: number;\n /** Estimated USD value of the savings (may be 0 if unknown). */\n estimatedSavingUsd: number;\n /** Optional free-text note for debugging. */\n note?: string;\n}\n\ninterface SavingsFeatureRow {\n feature: string;\n /** Number of recordSavings() calls contributing to this feature. */\n entries: number;\n countAvoided: number;\n estimatedSavingUsd: number;\n}\n\ninterface SavingsReport {\n features: SavingsFeatureRow[];\n total: {\n entries: number;\n countAvoided: number;\n estimatedSavingUsd: number;\n };\n days: number;\n}\n\ninterface CostEntry {\n feature: string; // e.g. 'auto-classify', 'query-expansion'\n model: string; // e.g. 'openai/gpt-4.1-nano'\n inputTokens: number;\n outputTokens: number;\n durationMs?: number;\n success?: boolean; // default true\n}\n\ninterface FeatureCostRow {\n feature: string;\n calls: number;\n inputTokens: number;\n outputTokens: number;\n estimatedCostUsd: number;\n}\n\ninterface ModelBreakdown {\n model: string;\n calls: number;\n inputTokens: number;\n outputTokens: number;\n estimatedCostUsd: number;\n}\n\ninterface CostReport {\n features: FeatureCostRow[];\n total: {\n calls: number;\n inputTokens: number;\n outputTokens: number;\n estimatedCostUsd: number;\n };\n days: number;\n /** Number of calls whose model was not in the pricing table (estimated_cost_usd IS NULL). */\n unknownModelCalls: number;\n /** Distinct unrecognized model names (for the warning message). */\n unknownModels: string[];\n}\n\nexport class CostTracker {\n /**\n * Use FactsDB + getRawDb() per operation so writes go through the same live handle / reopen\n * path as the rest of the store. A cached DatabaseSync goes stale after close() (reload) or\n * native \"database is not open\" races (#968-style).\n */\n private readonly factsDb: FactsDB;\n /** Rate-limit: log at most one DB error per session to avoid spamming the console. */\n private _errorLogged = false;\n\n constructor(factsDb: FactsDB) {\n this.factsDb = factsDb;\n this.initSchema();\n }\n\n /** Active SQLite handle; skip only when the store is actually shut down (e.g. plugin teardown). */\n private db(): DatabaseSync | null {\n try {\n return this.factsDb.getRawDb();\n } catch {\n return null;\n }\n }\n\n private initSchema(): void {\n const db = this.db();\n if (!db) return;\n db.exec(`\n CREATE TABLE IF NOT EXISTS llm_cost_log (\n id INTEGER PRIMARY KEY AUTOINCREMENT,\n timestamp INTEGER NOT NULL DEFAULT (unixepoch()),\n feature TEXT NOT NULL,\n model TEXT NOT NULL,\n input_tokens INTEGER NOT NULL,\n output_tokens INTEGER NOT NULL,\n estimated_cost_usd REAL,\n duration_ms INTEGER,\n success INTEGER NOT NULL DEFAULT 1\n );\n CREATE INDEX IF NOT EXISTS idx_cost_log_feature ON llm_cost_log(feature);\n CREATE INDEX IF NOT EXISTS idx_cost_log_timestamp ON llm_cost_log(timestamp);\n\n CREATE TABLE IF NOT EXISTS llm_savings_log (\n id INTEGER PRIMARY KEY AUTOINCREMENT,\n timestamp INTEGER NOT NULL DEFAULT (unixepoch()),\n feature TEXT NOT NULL,\n action TEXT NOT NULL,\n count_avoided INTEGER NOT NULL DEFAULT 0,\n estimated_saving_usd REAL NOT NULL DEFAULT 0,\n note TEXT\n );\n CREATE INDEX IF NOT EXISTS idx_savings_log_feature ON llm_savings_log(feature);\n CREATE INDEX IF NOT EXISTS idx_savings_log_timestamp ON llm_savings_log(timestamp);\n `);\n // Correct mis-prefixed model names (e.g. gateway sent openai/gemini-* → store as google/gemini-*)\n try {\n db.exec(`\n UPDATE llm_cost_log SET model = 'google/' || substr(model, 8) WHERE model LIKE 'openai/gemini-%';\n UPDATE llm_cost_log SET model = 'anthropic/' || substr(model, 8) WHERE model LIKE 'openai/claude-%';\n `);\n } catch {\n // ignore\n }\n }\n\n record(entry: CostEntry): void {\n try {\n const db = this.db();\n if (!db) return;\n const cost = estimateCost(entry.model, entry.inputTokens, entry.outputTokens);\n db.prepare(\n `INSERT INTO llm_cost_log (feature, model, input_tokens, output_tokens, estimated_cost_usd, duration_ms, success)\n VALUES (?, ?, ?, ?, ?, ?, ?)`,\n ).run(\n entry.feature,\n entry.model,\n entry.inputTokens,\n entry.outputTokens,\n cost,\n entry.durationMs ?? null,\n (entry.success ?? true) ? 1 : 0,\n );\n } catch (err) {\n // Never let cost tracking break LLM calls — but log the first failure per session for debuggability\n if (!this._errorLogged) {\n this._errorLogged = true;\n pluginLogger.warn(\n `[cost-tracker] Failed to record cost entry: ${err instanceof Error ? err.message : String(err)}`,\n );\n }\n }\n }\n\n getReport(options: { days?: number; feature?: string } = {}): CostReport {\n const days = options.days ?? 7;\n const cutoff = Math.floor(Date.now() / 1000) - days * 86400;\n const db = this.db();\n if (!db) {\n return {\n features: [],\n total: { calls: 0, inputTokens: 0, outputTokens: 0, estimatedCostUsd: 0 },\n days,\n unknownModelCalls: 0,\n unknownModels: [],\n };\n }\n\n let query = `SELECT feature,\n COUNT(*) AS calls,\n SUM(input_tokens) AS inputTokens,\n SUM(output_tokens) AS outputTokens,\n COALESCE(SUM(estimated_cost_usd), 0) AS estimatedCostUsd\n FROM llm_cost_log\n WHERE timestamp >= ?`;\n const params: (number | string)[] = [cutoff];\n\n if (options.feature) {\n query += \" AND feature = ?\";\n params.push(options.feature);\n }\n query += \" GROUP BY feature ORDER BY estimatedCostUsd DESC\";\n\n const rows = db.prepare(query).all(...params) as Array<{\n feature: string;\n calls: number | bigint;\n inputTokens: number | bigint;\n outputTokens: number | bigint;\n estimatedCostUsd: number;\n }>;\n\n const features: FeatureCostRow[] = rows.map((r) => ({\n feature: r.feature,\n calls: Number(r.calls),\n inputTokens: Number(r.inputTokens),\n outputTokens: Number(r.outputTokens),\n estimatedCostUsd: r.estimatedCostUsd ?? 0,\n }));\n\n const total = features.reduce(\n (acc, r) => ({\n calls: acc.calls + r.calls,\n inputTokens: acc.inputTokens + r.inputTokens,\n outputTokens: acc.outputTokens + r.outputTokens,\n estimatedCostUsd: acc.estimatedCostUsd + r.estimatedCostUsd,\n }),\n { calls: 0, inputTokens: 0, outputTokens: 0, estimatedCostUsd: 0 },\n );\n\n // Unknown-model query: calls where estimated_cost_usd IS NULL\n let unknownModelCalls = 0;\n let unknownModels: string[] = [];\n try {\n let unknownQuery = `SELECT COUNT(*) AS cnt, GROUP_CONCAT(DISTINCT model) AS models\n FROM llm_cost_log WHERE timestamp >= ? AND estimated_cost_usd IS NULL`;\n const unknownParams: (number | string)[] = [cutoff];\n if (options.feature) {\n unknownQuery += \" AND feature = ?\";\n unknownParams.push(options.feature);\n }\n const unknownRow = db.prepare(unknownQuery).get(...unknownParams) as\n | {\n cnt: number | bigint;\n models: string | null;\n }\n | undefined;\n unknownModelCalls = Number(unknownRow?.cnt ?? 0);\n unknownModels = unknownRow?.models ? unknownRow.models.split(\",\").filter(Boolean) : [];\n } catch {\n /* best-effort */\n }\n\n return { features, total, days, unknownModelCalls, unknownModels };\n }\n\n getModelBreakdown(days = 7): ModelBreakdown[] {\n const db = this.db();\n if (!db) return [];\n const cutoff = Math.floor(Date.now() / 1000) - days * 86400;\n const rows = db\n .prepare(\n `SELECT model,\n COUNT(*) AS calls,\n SUM(input_tokens) AS inputTokens,\n SUM(output_tokens) AS outputTokens,\n COALESCE(SUM(estimated_cost_usd), 0) AS estimatedCostUsd\n FROM llm_cost_log\n WHERE timestamp >= ?\n GROUP BY model\n ORDER BY estimatedCostUsd DESC`,\n )\n .all(cutoff) as Array<{\n model: string;\n calls: number | bigint;\n inputTokens: number | bigint;\n outputTokens: number | bigint;\n estimatedCostUsd: number;\n }>;\n\n return rows.map((r) => ({\n model: r.model,\n calls: Number(r.calls),\n inputTokens: Number(r.inputTokens),\n outputTokens: Number(r.outputTokens),\n estimatedCostUsd: r.estimatedCostUsd ?? 0,\n }));\n }\n\n getTotalCost(days = 7): {\n calls: number;\n inputTokens: number;\n outputTokens: number;\n estimatedCostUsd: number;\n } {\n const db = this.db();\n if (!db) {\n return { calls: 0, inputTokens: 0, outputTokens: 0, estimatedCostUsd: 0 };\n }\n const cutoff = Math.floor(Date.now() / 1000) - days * 86400;\n const row = db\n .prepare(\n `SELECT COUNT(*) AS calls,\n COALESCE(SUM(input_tokens), 0) AS inputTokens,\n COALESCE(SUM(output_tokens), 0) AS outputTokens,\n COALESCE(SUM(estimated_cost_usd), 0) AS estimatedCostUsd\n FROM llm_cost_log\n WHERE timestamp >= ?`,\n )\n .get(cutoff) as {\n calls: number | bigint;\n inputTokens: number | bigint;\n outputTokens: number | bigint;\n estimatedCostUsd: number;\n };\n\n return {\n calls: Number(row.calls),\n inputTokens: Number(row.inputTokens),\n outputTokens: Number(row.outputTokens),\n estimatedCostUsd: row.estimatedCostUsd ?? 0,\n };\n }\n\n /**\n * Record an intelligent-automation saving event.\n * Call this whenever a lifecycle feature does work that avoids manual LLM calls.\n */\n recordSavings(entry: SavingsEntry): void {\n try {\n const db = this.db();\n if (!db) return;\n db.prepare(\n `INSERT INTO llm_savings_log (feature, action, count_avoided, estimated_saving_usd, note)\n VALUES (?, ?, ?, ?, ?)`,\n ).run(entry.feature, entry.action, entry.countAvoided, entry.estimatedSavingUsd, entry.note ?? null);\n } catch (err) {\n if (!this._errorLogged) {\n this._errorLogged = true;\n pluginLogger.warn(\n `[cost-tracker] Failed to record savings entry: ${err instanceof Error ? err.message : String(err)}`,\n );\n }\n }\n }\n\n /**\n * Return a savings report grouped by feature for the last `days` days.\n */\n getSavingsReport(days = 7): SavingsReport {\n const db = this.db();\n if (!db) {\n return {\n features: [],\n total: { entries: 0, countAvoided: 0, estimatedSavingUsd: 0 },\n days,\n };\n }\n const cutoff = Math.floor(Date.now() / 1000) - days * 86400;\n const rows = db\n .prepare(\n `SELECT feature,\n COUNT(*) AS entries,\n SUM(count_avoided) AS countAvoided,\n SUM(estimated_saving_usd) AS estimatedSavingUsd\n FROM llm_savings_log\n WHERE timestamp >= ?\n GROUP BY feature\n ORDER BY estimatedSavingUsd DESC`,\n )\n .all(cutoff) as Array<{\n feature: string;\n entries: number | bigint;\n countAvoided: number | bigint;\n estimatedSavingUsd: number;\n }>;\n\n const features: SavingsFeatureRow[] = rows.map((r) => ({\n feature: r.feature,\n entries: Number(r.entries),\n countAvoided: Number(r.countAvoided),\n estimatedSavingUsd: r.estimatedSavingUsd ?? 0,\n }));\n\n const total = features.reduce(\n (acc, r) => ({\n entries: acc.entries + r.entries,\n countAvoided: acc.countAvoided + r.countAvoided,\n estimatedSavingUsd: acc.estimatedSavingUsd + r.estimatedSavingUsd,\n }),\n { entries: 0, countAvoided: 0, estimatedSavingUsd: 0 },\n );\n\n return { features, total, days };\n }\n\n /**\n * Delete entries older than retainDays (default 90). Returns number deleted.\n * Prunes both llm_cost_log and llm_savings_log.\n */\n pruneOldEntries(retainDays = 90): number {\n const db = this.db();\n if (!db) return 0;\n const cutoff = Math.floor(Date.now() / 1000) - retainDays * 86400;\n const costResult = db.prepare(\"DELETE FROM llm_cost_log WHERE timestamp < ?\").run(cutoff);\n const savingsResult = db.prepare(\"DELETE FROM llm_savings_log WHERE timestamp < ?\").run(cutoff);\n return Number(costResult.changes) + Number(savingsResult.changes);\n }\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"cost-tracker.js","names":[],"sources":["../../backends/cost-tracker.ts"],"sourcesContent":["/**\n * LLM Cost Tracker — persists per-call token usage into llm_cost_log table\n * and intelligent-automation savings into llm_savings_log, both in the\n * existing memory.db SQLite database (shared with FactsDB).\n *\n * ⚠️ Costs are estimates based on published model pricing, not billing-accurate.\n */\n\nimport type { DatabaseSync } from \"node:sqlite\";\nimport { estimateCost } from \"../services/model-pricing.js\";\nimport { pluginLogger } from \"../utils/logger.js\";\nimport type { FactsDB } from \"./facts-db.js\";\n\n/**\n * A savings entry records work performed automatically that would have\n * otherwise required manual LLM calls or human effort.\n * E.g. self-correction auto-fixing an incident, or auto-classify batching N facts.\n */\ninterface SavingsEntry {\n /** Feature that generated the savings (e.g. 'self-correction', 'auto-classify'). */\n feature: string;\n /** Human-readable description of the action (e.g. 'auto-fixed incident'). */\n action: string;\n /** Number of individual operations that were avoided or batched. */\n countAvoided: number;\n /** Estimated USD value of the savings (may be 0 if unknown). */\n estimatedSavingUsd: number;\n /** Optional free-text note for debugging. */\n note?: string;\n}\n\ninterface SavingsFeatureRow {\n feature: string;\n /** Number of recordSavings() calls contributing to this feature. */\n entries: number;\n countAvoided: number;\n estimatedSavingUsd: number;\n}\n\ninterface SavingsReport {\n features: SavingsFeatureRow[];\n total: {\n entries: number;\n countAvoided: number;\n estimatedSavingUsd: number;\n };\n days: number;\n}\n\ninterface CostEntry {\n feature: string; // e.g. 'auto-classify', 'query-expansion'\n model: string; // e.g. 'openai/gpt-4.1-nano'\n inputTokens: number;\n outputTokens: number;\n durationMs?: number;\n success?: boolean; // default true\n}\n\ninterface FeatureCostRow {\n feature: string;\n calls: number;\n inputTokens: number;\n outputTokens: number;\n estimatedCostUsd: number;\n}\n\ninterface ModelBreakdown {\n model: string;\n calls: number;\n inputTokens: number;\n outputTokens: number;\n estimatedCostUsd: number;\n}\n\ninterface CostReport {\n features: FeatureCostRow[];\n total: {\n calls: number;\n inputTokens: number;\n outputTokens: number;\n estimatedCostUsd: number;\n };\n days: number;\n /** Number of calls whose model was not in the pricing table (estimated_cost_usd IS NULL). */\n unknownModelCalls: number;\n /** Distinct unrecognized model names (for the warning message). */\n unknownModels: string[];\n}\n\nexport class CostTracker {\n /**\n * Use FactsDB + getRawDb() per operation so writes go through the same live handle / reopen\n * path as the rest of the store. A cached DatabaseSync goes stale after close() (reload) or\n * native \"database is not open\" races (#968-style).\n */\n private readonly factsDb: FactsDB;\n /** Rate-limit: log at most one DB error per session to avoid spamming the console. */\n private _errorLogged = false;\n\n constructor(factsDb: FactsDB) {\n this.factsDb = factsDb;\n this.initSchema();\n }\n\n /** Active SQLite handle; skip only when the store is actually shut down (e.g. plugin teardown). */\n private db(): DatabaseSync | null {\n try {\n return this.factsDb.getRawDb();\n } catch {\n return null;\n }\n }\n\n private initSchema(): void {\n const db = this.db();\n if (!db) return;\n db.exec(`\n CREATE TABLE IF NOT EXISTS llm_cost_log (\n id INTEGER PRIMARY KEY AUTOINCREMENT,\n timestamp INTEGER NOT NULL DEFAULT (unixepoch()),\n feature TEXT NOT NULL,\n model TEXT NOT NULL,\n input_tokens INTEGER NOT NULL,\n output_tokens INTEGER NOT NULL,\n estimated_cost_usd REAL,\n duration_ms INTEGER,\n success INTEGER NOT NULL DEFAULT 1\n );\n CREATE INDEX IF NOT EXISTS idx_cost_log_feature ON llm_cost_log(feature);\n CREATE INDEX IF NOT EXISTS idx_cost_log_timestamp ON llm_cost_log(timestamp);\n\n CREATE TABLE IF NOT EXISTS llm_savings_log (\n id INTEGER PRIMARY KEY AUTOINCREMENT,\n timestamp INTEGER NOT NULL DEFAULT (unixepoch()),\n feature TEXT NOT NULL,\n action TEXT NOT NULL,\n count_avoided INTEGER NOT NULL DEFAULT 0,\n estimated_saving_usd REAL NOT NULL DEFAULT 0,\n note TEXT\n );\n CREATE INDEX IF NOT EXISTS idx_savings_log_feature ON llm_savings_log(feature);\n CREATE INDEX IF NOT EXISTS idx_savings_log_timestamp ON llm_savings_log(timestamp);\n `);\n // Correct mis-prefixed model names (e.g. gateway sent openai/gemini-* → store as google/gemini-*)\n try {\n db.exec(`\n UPDATE llm_cost_log SET model = 'google/' || substr(model, 8) WHERE model LIKE 'openai/gemini-%';\n UPDATE llm_cost_log SET model = 'anthropic/' || substr(model, 8) WHERE model LIKE 'openai/claude-%';\n `);\n } catch {\n // ignore\n }\n }\n\n record(entry: CostEntry): void {\n try {\n const db = this.db();\n if (!db) return;\n const cost = estimateCost(entry.model, entry.inputTokens, entry.outputTokens);\n db.prepare(\n `INSERT INTO llm_cost_log (feature, model, input_tokens, output_tokens, estimated_cost_usd, duration_ms, success)\n VALUES (?, ?, ?, ?, ?, ?, ?)`,\n ).run(\n entry.feature,\n entry.model,\n entry.inputTokens,\n entry.outputTokens,\n cost,\n entry.durationMs ?? null,\n (entry.success ?? true) ? 1 : 0,\n );\n } catch (err) {\n // Never let cost tracking break LLM calls — but log the first failure per session for debuggability\n if (!this._errorLogged) {\n this._errorLogged = true;\n pluginLogger.warn(\n `[cost-tracker] Failed to record cost entry: ${err instanceof Error ? err.message : String(err)}`,\n );\n }\n }\n }\n\n getReport(options: { days?: number; feature?: string } = {}): CostReport {\n const days = options.days ?? 7;\n const cutoff = Math.floor(Date.now() / 1000) - days * 86400;\n const db = this.db();\n if (!db) {\n return {\n features: [],\n total: { calls: 0, inputTokens: 0, outputTokens: 0, estimatedCostUsd: 0 },\n days,\n unknownModelCalls: 0,\n unknownModels: [],\n };\n }\n\n let query = `SELECT feature,\n COUNT(*) AS calls,\n SUM(input_tokens) AS inputTokens,\n SUM(output_tokens) AS outputTokens,\n COALESCE(SUM(estimated_cost_usd), 0) AS estimatedCostUsd\n FROM llm_cost_log\n WHERE timestamp >= ?`;\n const params: (number | string)[] = [cutoff];\n\n if (options.feature) {\n query += \" AND feature = ?\";\n params.push(options.feature);\n }\n query += \" GROUP BY feature ORDER BY estimatedCostUsd DESC\";\n\n const rows = db.prepare(query).all(...params) as Array<{\n feature: string;\n calls: number | bigint;\n inputTokens: number | bigint;\n outputTokens: number | bigint;\n estimatedCostUsd: number;\n }>;\n\n const features: FeatureCostRow[] = rows.map((r) => ({\n feature: r.feature,\n calls: Number(r.calls),\n inputTokens: Number(r.inputTokens),\n outputTokens: Number(r.outputTokens),\n estimatedCostUsd: r.estimatedCostUsd ?? 0,\n }));\n\n const total = features.reduce(\n (acc, r) => ({\n calls: acc.calls + r.calls,\n inputTokens: acc.inputTokens + r.inputTokens,\n outputTokens: acc.outputTokens + r.outputTokens,\n estimatedCostUsd: acc.estimatedCostUsd + r.estimatedCostUsd,\n }),\n { calls: 0, inputTokens: 0, outputTokens: 0, estimatedCostUsd: 0 },\n );\n\n // Unknown-model query: calls where estimated_cost_usd IS NULL\n let unknownModelCalls = 0;\n let unknownModels: string[] = [];\n try {\n let unknownQuery = `SELECT COUNT(*) AS cnt, GROUP_CONCAT(DISTINCT model) AS models\n FROM llm_cost_log WHERE timestamp >= ? AND estimated_cost_usd IS NULL`;\n const unknownParams: (number | string)[] = [cutoff];\n if (options.feature) {\n unknownQuery += \" AND feature = ?\";\n unknownParams.push(options.feature);\n }\n const unknownRow = db.prepare(unknownQuery).get(...unknownParams) as\n | {\n cnt: number | bigint;\n models: string | null;\n }\n | undefined;\n unknownModelCalls = Number(unknownRow?.cnt ?? 0);\n unknownModels = unknownRow?.models ? unknownRow.models.split(\",\").filter(Boolean) : [];\n } catch {\n /* best-effort */\n }\n\n return { features, total, days, unknownModelCalls, unknownModels };\n }\n\n getModelBreakdown(days = 7): ModelBreakdown[] {\n const db = this.db();\n if (!db) return [];\n const cutoff = Math.floor(Date.now() / 1000) - days * 86400;\n const rows = db\n .prepare(\n `SELECT model,\n COUNT(*) AS calls,\n SUM(input_tokens) AS inputTokens,\n SUM(output_tokens) AS outputTokens,\n COALESCE(SUM(estimated_cost_usd), 0) AS estimatedCostUsd\n FROM llm_cost_log\n WHERE timestamp >= ?\n GROUP BY model\n ORDER BY estimatedCostUsd DESC`,\n )\n .all(cutoff) as Array<{\n model: string;\n calls: number | bigint;\n inputTokens: number | bigint;\n outputTokens: number | bigint;\n estimatedCostUsd: number;\n }>;\n\n return rows.map((r) => ({\n model: r.model,\n calls: Number(r.calls),\n inputTokens: Number(r.inputTokens),\n outputTokens: Number(r.outputTokens),\n estimatedCostUsd: r.estimatedCostUsd ?? 0,\n }));\n }\n\n getTotalCost(days = 7): {\n calls: number;\n inputTokens: number;\n outputTokens: number;\n estimatedCostUsd: number;\n } {\n const db = this.db();\n if (!db) {\n return { calls: 0, inputTokens: 0, outputTokens: 0, estimatedCostUsd: 0 };\n }\n const cutoff = Math.floor(Date.now() / 1000) - days * 86400;\n const row = db\n .prepare(\n `SELECT COUNT(*) AS calls,\n COALESCE(SUM(input_tokens), 0) AS inputTokens,\n COALESCE(SUM(output_tokens), 0) AS outputTokens,\n COALESCE(SUM(estimated_cost_usd), 0) AS estimatedCostUsd\n FROM llm_cost_log\n WHERE timestamp >= ?`,\n )\n .get(cutoff) as {\n calls: number | bigint;\n inputTokens: number | bigint;\n outputTokens: number | bigint;\n estimatedCostUsd: number;\n };\n\n return {\n calls: Number(row.calls),\n inputTokens: Number(row.inputTokens),\n outputTokens: Number(row.outputTokens),\n estimatedCostUsd: row.estimatedCostUsd ?? 0,\n };\n }\n\n /**\n * Record an intelligent-automation saving event.\n * Call this whenever a lifecycle feature does work that avoids manual LLM calls.\n */\n recordSavings(entry: SavingsEntry): void {\n try {\n const db = this.db();\n if (!db) return;\n db.prepare(\n `INSERT INTO llm_savings_log (feature, action, count_avoided, estimated_saving_usd, note)\n VALUES (?, ?, ?, ?, ?)`,\n ).run(entry.feature, entry.action, entry.countAvoided, entry.estimatedSavingUsd, entry.note ?? null);\n } catch (err) {\n if (!this._errorLogged) {\n this._errorLogged = true;\n pluginLogger.warn(\n `[cost-tracker] Failed to record savings entry: ${err instanceof Error ? err.message : String(err)}`,\n );\n }\n }\n }\n\n /**\n * Return a savings report grouped by feature for the last `days` days.\n */\n getSavingsReport(days = 7): SavingsReport {\n const db = this.db();\n if (!db) {\n return {\n features: [],\n total: { entries: 0, countAvoided: 0, estimatedSavingUsd: 0 },\n days,\n };\n }\n const cutoff = Math.floor(Date.now() / 1000) - days * 86400;\n const rows = db\n .prepare(\n `SELECT feature,\n COUNT(*) AS entries,\n SUM(count_avoided) AS countAvoided,\n SUM(estimated_saving_usd) AS estimatedSavingUsd\n FROM llm_savings_log\n WHERE timestamp >= ?\n GROUP BY feature\n ORDER BY estimatedSavingUsd DESC`,\n )\n .all(cutoff) as Array<{\n feature: string;\n entries: number | bigint;\n countAvoided: number | bigint;\n estimatedSavingUsd: number;\n }>;\n\n const features: SavingsFeatureRow[] = rows.map((r) => ({\n feature: r.feature,\n entries: Number(r.entries),\n countAvoided: Number(r.countAvoided),\n estimatedSavingUsd: r.estimatedSavingUsd ?? 0,\n }));\n\n const total = features.reduce(\n (acc, r) => ({\n entries: acc.entries + r.entries,\n countAvoided: acc.countAvoided + r.countAvoided,\n estimatedSavingUsd: acc.estimatedSavingUsd + r.estimatedSavingUsd,\n }),\n { entries: 0, countAvoided: 0, estimatedSavingUsd: 0 },\n );\n\n return { features, total, days };\n }\n\n /**\n * Delete entries older than retainDays (default 90). Returns number deleted.\n * Prunes both llm_cost_log and llm_savings_log.\n */\n pruneOldEntries(retainDays = 90): number {\n const db = this.db();\n if (!db) return 0;\n const cutoff = Math.floor(Date.now() / 1000) - retainDays * 86400;\n const costResult = db.prepare(\"DELETE FROM llm_cost_log WHERE timestamp < ?\").run(cutoff);\n const savingsResult = db.prepare(\"DELETE FROM llm_savings_log WHERE timestamp < ?\").run(cutoff);\n return Number(costResult.changes) + Number(savingsResult.changes);\n }\n}\n"],"mappings":"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"}
@@ -19,7 +19,6 @@ function toBuffer(val) {
19
19
  return Buffer.isBuffer(val) ? val : Buffer.from(val);
20
20
  }
21
21
  const CRED_IV_LEN = 12;
22
- const CRED_AUTH_TAG_LEN = 16;
23
22
  const CRED_ALGO = "aes-256-gcm";
24
23
  const CRED_KDF_VERSION = 2;
25
24
  const CRED_KDF_PLAINTEXT = 0;
@@ -55,8 +54,8 @@ function encryptValue(plaintext, key) {
55
54
  }
56
55
  function decryptValue(buffer, key) {
57
56
  const iv = buffer.subarray(0, CRED_IV_LEN);
58
- const authTag = buffer.subarray(CRED_IV_LEN, CRED_IV_LEN + CRED_AUTH_TAG_LEN);
59
- const encrypted = buffer.subarray(CRED_IV_LEN + CRED_AUTH_TAG_LEN);
57
+ const authTag = buffer.subarray(CRED_IV_LEN, 28);
58
+ const encrypted = buffer.subarray(28);
60
59
  const decipher = createDecipheriv(CRED_ALGO, key, iv);
61
60
  decipher.setAuthTag(authTag);
62
61
  return Buffer.concat([decipher.update(encrypted), decipher.final()]).toString("utf8");
@@ -1 +1 @@
1
- {"version":3,"file":"credentials-db.js","names":[],"sources":["../../backends/credentials-db.ts"],"sourcesContent":["/**\n * Credentials Store (opt-in)\n * Optional AES-256-GCM encryption with scrypt KDF. When no encryption key is set,\n * values are stored in plaintext; the user may secure data by other means (e.g. filesystem permissions).\n */\n\nimport { createCipheriv, createDecipheriv, createHash, randomBytes, scryptSync } from \"node:crypto\";\nimport { mkdirSync } from \"node:fs\";\nimport { dirname } from \"node:path\";\nimport { DatabaseSync } from \"node:sqlite\";\nimport type { CredentialType } from \"../config.js\";\nimport { assertValidCredentialRow } from \"../services/credential-validation.js\";\nimport { capturePluginError } from \"../services/error-reporter.js\";\nimport { pluginLogger } from \"../utils/logger.js\";\nimport { tryRestrictSqliteDbFileMode } from \"../utils/sqlite-file-perms.js\";\nimport { createTransaction } from \"../utils/sqlite-transaction.js\";\nimport { BaseSqliteStore } from \"./base-sqlite-store.js\";\n\n/** node:sqlite returns BLOBs as Uint8Array; convert to Buffer for crypto ops. */\nfunction toBuffer(val: Uint8Array | Buffer): Buffer {\n return Buffer.isBuffer(val) ? val : Buffer.from(val);\n}\n\nconst CRED_IV_LEN = 12;\nconst CRED_AUTH_TAG_LEN = 16;\nconst CRED_ALGO = \"aes-256-gcm\";\nconst CRED_KDF_VERSION = 2; // v1 = SHA-256 (legacy), v2 = scrypt\nconst CRED_KDF_PLAINTEXT = 0; // no encryption (user secures by other means)\n\n/** Log once per vault path: legacy v1 KDF is weak; opening triggers migration to scrypt when possible. */\nconst _v1KdfWarnedPaths = new Set<string>();\n\n/** v1 only: legacy SHA-256 KDF (weak). Existing vaults cannot be decrypted with another KDF. */\nfunction deriveKeyV1Legacy(password: string): Buffer {\n // codeql[js/insufficient-password-hash]\n // Legacy v1 on-disk format only; new vaults use deriveKeyV2 (scrypt). Changing this breaks existing vaults.\n return createHash(\"sha256\").update(password, \"utf8\").digest();\n}\n\n/** v2: scrypt (N=16384, r=8, p=1). */\nfunction deriveKeyV2(password: string, salt: Buffer): Buffer {\n return scryptSync(password, salt, 32, { N: 16384, r: 8, p: 1 });\n}\n\n/** Dispatch v1 (legacy) vs v2 (scrypt). Prefer deriveKeyV2 for new material when version is known. */\nfunction deriveKey(password: string, salt: Buffer, version: number = CRED_KDF_VERSION): Buffer {\n if (version === 1) {\n // codeql[js/insufficient-password-hash]\n // Password only flows to the intentional legacy KDF above; v2 uses scrypt.\n return deriveKeyV1Legacy(password);\n }\n return deriveKeyV2(password, salt);\n}\n\nfunction encryptValue(plaintext: string, key: Buffer): Buffer {\n const iv = randomBytes(CRED_IV_LEN);\n const cipher = createCipheriv(CRED_ALGO, key, iv);\n const encrypted = Buffer.concat([cipher.update(plaintext, \"utf8\"), cipher.final()]);\n const authTag = cipher.getAuthTag();\n return Buffer.concat([iv, authTag, encrypted]);\n}\n\nfunction decryptValue(buffer: Buffer, key: Buffer): string {\n const iv = buffer.subarray(0, CRED_IV_LEN);\n const authTag = buffer.subarray(CRED_IV_LEN, CRED_IV_LEN + CRED_AUTH_TAG_LEN);\n const encrypted = buffer.subarray(CRED_IV_LEN + CRED_AUTH_TAG_LEN);\n const decipher = createDecipheriv(CRED_ALGO, key, iv);\n decipher.setAuthTag(authTag);\n return Buffer.concat([decipher.update(encrypted), decipher.final()]).toString(\"utf8\");\n}\n\nexport type CredentialEntry = {\n service: string;\n type: CredentialType;\n value: string;\n url: string | null;\n notes: string | null;\n created: number;\n updated: number;\n expires: number | null;\n};\n\nexport class CredentialsDB extends BaseSqliteStore {\n private readonly dbPath: string;\n private key!: Buffer;\n private kdfVersion!: number;\n private salt!: Buffer;\n /** When false, values are stored and read as plaintext (no encryption). Mutable when DB metadata overrides key length heuristics. */\n private storesEncryptedValues: boolean;\n /** True when a 16+ character encryption key was provided to the constructor. */\n private readonly configuredKeyPresent: boolean;\n // SECURITY NOTE: Raw password is stored only for lazy migration from legacy SHA-256 to scrypt.\n // Migration is triggered on first successful get() to verify the password is correct before re-encrypting.\n // After migration completes, this field is cleared to minimize exposure in memory.\n private password: string | null;\n\n constructor(dbPath: string, encryptionKey: string) {\n const keyLooksEncrypted = encryptionKey.length >= 16;\n mkdirSync(dirname(dbPath), { recursive: true });\n const db = new DatabaseSync(dbPath);\n super(db);\n this.dbPath = dbPath;\n this.configuredKeyPresent = keyLooksEncrypted;\n tryRestrictSqliteDbFileMode(dbPath);\n this.storesEncryptedValues = keyLooksEncrypted;\n\n this.liveDb.exec(`\n CREATE TABLE IF NOT EXISTS vault_meta (\n key TEXT PRIMARY KEY,\n value BLOB NOT NULL\n )\n `);\n\n this.liveDb.exec(`\n CREATE TABLE IF NOT EXISTS credentials (\n service TEXT NOT NULL,\n type TEXT NOT NULL DEFAULT 'other',\n value BLOB NOT NULL,\n url TEXT,\n notes TEXT,\n created INTEGER NOT NULL,\n updated INTEGER NOT NULL,\n expires INTEGER,\n PRIMARY KEY (service, type)\n )\n `);\n this.liveDb.exec(`\n CREATE INDEX IF NOT EXISTS idx_credentials_service ON credentials(service)\n `);\n\n const versionRow = this.liveDb.prepare(\"SELECT value FROM vault_meta WHERE key = 'kdf_version'\").get() as\n | { value: Uint8Array | Buffer }\n | undefined;\n const saltRow = this.liveDb.prepare(\"SELECT value FROM vault_meta WHERE key = 'salt'\").get() as\n | { value: Uint8Array | Buffer }\n | undefined;\n\n if (!keyLooksEncrypted) {\n // Plaintext vault: no key derived\n this.kdfVersion = CRED_KDF_PLAINTEXT;\n this.salt = Buffer.alloc(0);\n this.key = Buffer.alloc(0);\n this.password = null;\n if (versionRow && versionRow.value != null && toBuffer(versionRow.value)[0] !== CRED_KDF_PLAINTEXT) {\n throw new Error(\n \"Credentials vault was created with encryption. Set credentials.encryptionKey (or OPENCLAW_CRED_KEY) to open it, or use a new vault path for an unencrypted vault.\",\n );\n }\n if (!versionRow) {\n // C1 FIX: Check if vault has encrypted data before marking as plaintext\n const hasCredentials =\n (this.liveDb.prepare(\"SELECT COUNT(*) as count FROM credentials\").get() as { count: number }).count > 0;\n if (hasCredentials) {\n throw new Error(\n \"Credentials vault contains data but no encryption metadata. This vault may have encrypted credentials. Provide credentials.encryptionKey to open it.\",\n );\n }\n this.liveDb\n .prepare(\"INSERT OR REPLACE INTO vault_meta (key, value) VALUES ('kdf_version', ?)\")\n .run(Buffer.from([CRED_KDF_PLAINTEXT]));\n }\n return;\n }\n\n // Check if vault is plaintext first (before assuming legacy)\n if (versionRow && versionRow.value != null && toBuffer(versionRow.value)[0] === CRED_KDF_PLAINTEXT) {\n // DB metadata says plaintext — never treat as encrypted regardless of key length (Issue #835).\n this.storesEncryptedValues = false;\n this.kdfVersion = CRED_KDF_PLAINTEXT;\n this.salt = Buffer.alloc(0);\n this.key = Buffer.alloc(0);\n this.password = null;\n // Optionally warn that key is being ignored\n if (encryptionKey.length >= 16) {\n pluginLogger.warn(\n \"Credentials vault is in plaintext mode (kdf_version=0). The configured encryption key is being ignored. \" +\n \"To encrypt the existing vault at rest, run: openclaw hybrid-mem credentials encrypt-vault --yes\",\n );\n }\n return;\n }\n\n if (!versionRow || !saltRow) {\n const hasCredentials =\n (this.liveDb.prepare(\"SELECT COUNT(*) as count FROM credentials\").get() as { count: number }).count > 0;\n\n if (hasCredentials) {\n this.kdfVersion = 1;\n this.salt = Buffer.alloc(0);\n this.key = deriveKey(encryptionKey, this.salt, 1);\n this.password = encryptionKey;\n } else {\n this.kdfVersion = CRED_KDF_VERSION;\n this.salt = randomBytes(32);\n this.key = deriveKey(encryptionKey, this.salt, this.kdfVersion);\n this.password = null;\n this.liveDb\n .prepare(\"INSERT OR REPLACE INTO vault_meta (key, value) VALUES ('kdf_version', ?)\")\n .run(Buffer.from([this.kdfVersion]));\n this.liveDb.prepare(\"INSERT OR REPLACE INTO vault_meta (key, value) VALUES ('salt', ?)\").run(this.salt);\n }\n } else {\n this.kdfVersion = versionRow.value != null ? toBuffer(versionRow.value)[0] : CRED_KDF_VERSION;\n this.salt = toBuffer(saltRow.value);\n this.key = deriveKey(encryptionKey, this.salt, this.kdfVersion);\n this.password = this.kdfVersion === 1 ? encryptionKey : null;\n }\n\n if (this.storesEncryptedValues && this.kdfVersion === 1 && !_v1KdfWarnedPaths.has(dbPath)) {\n _v1KdfWarnedPaths.add(dbPath);\n pluginLogger.warn(\n \"memory-hybrid: credentials vault uses legacy key derivation (v1 / SHA-256). This is weak against offline attacks. \" +\n \"The vault will migrate to scrypt (v2) automatically after the next successful unlock. \" +\n \"Set a strong OPENCLAW_CRED_KEY and restart, or rotate secrets after migration.\",\n );\n }\n }\n\n protected getSubsystemName(): string {\n return \"credentials\";\n }\n\n getVaultStatus(): {\n dbPath: string;\n kdfVersion: number;\n encryptedAtRest: boolean;\n configuredKeyPresent: boolean;\n keyIgnored: boolean;\n migrationRequired: boolean;\n } {\n const encryptedAtRest = this.kdfVersion !== CRED_KDF_PLAINTEXT;\n const keyIgnored = this.kdfVersion === CRED_KDF_PLAINTEXT && this.configuredKeyPresent;\n const migrationRequired = keyIgnored;\n return {\n dbPath: this.dbPath,\n kdfVersion: this.kdfVersion,\n encryptedAtRest,\n configuredKeyPresent: this.configuredKeyPresent,\n keyIgnored,\n migrationRequired,\n };\n }\n\n /**\n * Encrypt an existing plaintext vault in place.\n * Requires a 16+ character encryption key. Safe and idempotent:\n * - Throws if vault is already encrypted\n * - Performs an in-DB transaction so it cannot partially convert rows\n */\n enableEncryptionAtRest(encryptionKey: string): { migrated: number; kdfVersion: number } {\n const keyLooksEncrypted = encryptionKey.length >= 16;\n if (!keyLooksEncrypted) {\n throw new Error(\n \"Encryption key is missing or too short. Set credentials.encryptionKey (16+ chars) or OPENCLAW_CRED_KEY before encrypting the vault.\",\n );\n }\n if (this.kdfVersion !== CRED_KDF_PLAINTEXT) {\n throw new Error(`Credentials vault is already encrypted (kdf_version=${this.kdfVersion}).`);\n }\n\n const newSalt = randomBytes(32);\n const newKdfVersion = CRED_KDF_VERSION;\n const newKey = deriveKey(encryptionKey, newSalt, newKdfVersion);\n\n let migratedCount = 0;\n const migrate = createTransaction(\n this.liveDb,\n () => {\n const rows = this.liveDb.prepare(\"SELECT service, type, value FROM credentials\").all() as Array<{\n service: string;\n type: string;\n value: Uint8Array | Buffer;\n }>;\n migratedCount = rows.length;\n const updateStmt = this.liveDb.prepare(\"UPDATE credentials SET value = ? WHERE service = ? AND type = ?\");\n for (const r of rows) {\n const plaintext = toBuffer(r.value).toString(\"utf8\");\n const encrypted = encryptValue(plaintext, newKey);\n updateStmt.run(encrypted as unknown as Uint8Array, r.service, r.type);\n }\n this.liveDb\n .prepare(\"INSERT OR REPLACE INTO vault_meta (key, value) VALUES ('kdf_version', ?)\")\n .run(Buffer.from([newKdfVersion]));\n this.liveDb.prepare(\"INSERT OR REPLACE INTO vault_meta (key, value) VALUES ('salt', ?)\").run(newSalt);\n },\n \"IMMEDIATE\",\n );\n\n migrate();\n\n this.kdfVersion = newKdfVersion;\n this.salt = newSalt;\n this.key = newKey;\n this.password = null;\n this.storesEncryptedValues = true;\n\n return { migrated: migratedCount, kdfVersion: this.kdfVersion };\n }\n\n /**\n * Insert or update a credential. On conflict (service, type), `updated` and value fields refresh;\n * `created` is preserved from the original row — intentional for \"same key, rotated secret\" flows (#894).\n */\n store(entry: {\n service: string;\n type: CredentialType;\n value: string;\n url?: string;\n notes?: string;\n expires?: number | null;\n }): CredentialEntry {\n const now = Math.floor(Date.now() / 1000);\n const stored = this.storesEncryptedValues ? encryptValue(entry.value, this.key) : Buffer.from(entry.value, \"utf8\");\n this.liveDb\n .prepare(\n `INSERT INTO credentials (service, type, value, url, notes, created, updated, expires)\n VALUES (?, ?, ?, ?, ?, ?, ?, ?)\n ON CONFLICT(service, type) DO UPDATE SET\n value = excluded.value,\n url = excluded.url,\n notes = excluded.notes,\n updated = excluded.updated,\n expires = excluded.expires`,\n )\n .run(entry.service, entry.type, stored, entry.url ?? null, entry.notes ?? null, now, now, entry.expires ?? null);\n return {\n service: entry.service,\n type: entry.type,\n value: \"[redacted]\",\n url: entry.url ?? null,\n notes: entry.notes ?? null,\n created: now,\n updated: now,\n expires: entry.expires ?? null,\n };\n }\n\n get(service: string, type?: CredentialType): CredentialEntry | null {\n const row = type\n ? (this.liveDb.prepare(\"SELECT * FROM credentials WHERE service = ? AND type = ?\").get(service, type) as\n | Record<string, unknown>\n | undefined)\n : (this.liveDb\n .prepare(\"SELECT * FROM credentials WHERE service = ? ORDER BY updated DESC LIMIT 1\")\n .get(service) as Record<string, unknown> | undefined);\n if (!row) return null;\n const buf = toBuffer(row.value as Uint8Array | Buffer);\n const value = this.storesEncryptedValues ? decryptValue(buf, this.key) : buf.toString(\"utf8\");\n\n if (this.kdfVersion === 1) {\n try {\n this.migrateLegacyVault();\n } catch (err) {\n capturePluginError(err as Error, {\n operation: \"migrate-vault\",\n severity: \"info\",\n subsystem: \"credentials\",\n });\n // Migration is best-effort; failure should not block credential retrieval\n }\n }\n\n const out = {\n service: row.service as string,\n type: row.type as string as CredentialType,\n value,\n url: (row.url as string) ?? null,\n notes: (row.notes as string) ?? null,\n created: row.created as number,\n updated: row.updated as number,\n expires: (row.expires as number) ?? null,\n };\n assertValidCredentialRow(out);\n return out;\n }\n\n /** Migrate legacy SHA-256 vault to scrypt. Called after first successful decryption. */\n private migrateLegacyVault(): void {\n if (!this.password) {\n throw new Error(\"Migration requires password\");\n }\n\n // Generate new salt and derive new key with scrypt (avoid deriveKey dispatcher so static analysis sees only scrypt here)\n const migrationSalt = randomBytes(32);\n const newKey = deriveKeyV2(this.password, migrationSalt);\n\n // Wrap read + mutations in one transaction (IMMEDIATE) so new rows cannot appear after the snapshot.\n const migrate = createTransaction(\n this.liveDb,\n () => {\n const rows = this.liveDb.prepare(\"SELECT * FROM credentials\").all() as Array<Record<string, unknown>>;\n const updateStmt = this.liveDb.prepare(\"UPDATE credentials SET value = ? WHERE service = ? AND type = ?\");\n for (const row of rows) {\n const oldBuf = toBuffer(row.value as Uint8Array | Buffer);\n const plaintext = decryptValue(oldBuf, this.key); // Decrypt with old key\n const newEncrypted = encryptValue(plaintext, newKey); // Encrypt with new key\n updateStmt.run(newEncrypted as unknown as Uint8Array, row.service as string, row.type as string);\n }\n\n this.liveDb\n .prepare(\"INSERT OR REPLACE INTO vault_meta (key, value) VALUES ('kdf_version', ?)\")\n .run(Buffer.from([CRED_KDF_VERSION]));\n this.liveDb.prepare(\"INSERT OR REPLACE INTO vault_meta (key, value) VALUES ('salt', ?)\").run(migrationSalt);\n },\n \"IMMEDIATE\",\n );\n\n migrate();\n\n this.salt = migrationSalt;\n\n // Update instance state\n this.kdfVersion = CRED_KDF_VERSION;\n this.key = newKey;\n this.password = null;\n }\n\n /**\n * Store only if no entry exists for this service+type.\n * Returns the stored entry on success, or null if an entry already existed (skipped).\n * Use this for auto-capture to avoid overwriting user-managed credentials.\n *\n * Uses a single `INSERT … ON CONFLICT(service, type) DO NOTHING` statement so the\n * check-and-insert is atomic — no TOCTOU race between concurrent writers.\n *\n * Also treats underscore ↔ hyphen variants as equivalent (e.g. `openai_api` and\n * `openai-api`) so that migration from the pre-normalisation naming convention does\n * not create duplicate vault entries on subsequent auto-capture runs.\n */\n storeIfNew(entry: {\n service: string;\n type: CredentialType;\n value: string;\n url?: string;\n notes?: string;\n expires?: number | null;\n }): CredentialEntry | null {\n // Check for a legacy cross-variant (underscore ↔ hyphen) before inserting so we\n // don't create a parallel entry alongside an existing differently-named one.\n const legacyVariant = entry.service.includes(\"_\")\n ? entry.service.replace(/_/g, \"-\")\n : entry.service.replace(/-/g, \"_\");\n if (legacyVariant !== entry.service && this.exists(legacyVariant, entry.type)) {\n return null;\n }\n\n const now = Math.floor(Date.now() / 1000);\n const stored = this.storesEncryptedValues ? encryptValue(entry.value, this.key) : Buffer.from(entry.value, \"utf8\");\n const result = this.liveDb\n .prepare(\n `INSERT INTO credentials (service, type, value, url, notes, created, updated, expires)\n VALUES (?, ?, ?, ?, ?, ?, ?, ?)\n ON CONFLICT(service, type) DO NOTHING`,\n )\n .run(entry.service, entry.type, stored, entry.url ?? null, entry.notes ?? null, now, now, entry.expires ?? null);\n if (result.changes === 0) {\n // A credential already exists for this service+type; do not overwrite it.\n return null;\n }\n return {\n service: entry.service,\n type: entry.type,\n value: \"[redacted]\",\n url: entry.url ?? null,\n notes: entry.notes ?? null,\n created: now,\n updated: now,\n expires: entry.expires ?? null,\n };\n }\n\n /** Returns true if an entry already exists for the given service (and optional type). */\n exists(service: string, type?: CredentialType): boolean {\n if (type) {\n const row = this.liveDb\n .prepare(\"SELECT 1 FROM credentials WHERE service = ? AND type = ? LIMIT 1\")\n .get(service, type);\n return !!row;\n }\n const row = this.liveDb.prepare(\"SELECT 1 FROM credentials WHERE service = ? LIMIT 1\").get(service);\n return !!row;\n }\n\n /**\n * List all credentials with decrypted values.\n * Use sparingly (decrypts every value). Primarily for audit operations.\n */\n listAll(): CredentialEntry[] {\n const rows = this.liveDb.prepare(\"SELECT * FROM credentials ORDER BY service, type\").all() as Array<\n Record<string, unknown>\n >;\n const out: CredentialEntry[] = [];\n for (const row of rows) {\n try {\n const buf = toBuffer(row.value as Uint8Array | Buffer);\n const value = this.storesEncryptedValues ? decryptValue(buf, this.key) : buf.toString(\"utf8\");\n const entry: CredentialEntry = {\n service: row.service as string,\n type: row.type as string as CredentialType,\n value,\n url: (row.url as string) ?? null,\n notes: (row.notes as string) ?? null,\n created: row.created as number,\n updated: row.updated as number,\n expires: (row.expires as number) ?? null,\n };\n assertValidCredentialRow(entry);\n out.push(entry);\n } catch (err) {\n capturePluginError(err instanceof Error ? err : new Error(String(err)), {\n subsystem: \"credentials\",\n operation: \"list-all-skip-row\",\n severity: \"warning\",\n });\n }\n }\n return out;\n }\n\n list(): Array<{ service: string; type: string; url: string | null; expires: number | null }> {\n const rows = this.liveDb\n .prepare(\"SELECT service, type, url, expires FROM credentials ORDER BY service, type\")\n .all() as Array<{\n service: string;\n type: string;\n url: string | null;\n expires: number | null;\n }>;\n return rows;\n }\n\n delete(service: string, type?: CredentialType): boolean {\n if (type) {\n const r = this.liveDb.prepare(\"DELETE FROM credentials WHERE service = ? AND type = ?\").run(service, type);\n return r.changes > 0;\n }\n const r = this.liveDb.prepare(\"DELETE FROM credentials WHERE service = ?\").run(service);\n return r.changes > 0;\n }\n}\n\n// Export encryption primitives for testing\nexport { deriveKey, encryptValue, decryptValue };\n"],"mappings":"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"}
1
+ {"version":3,"file":"credentials-db.js","names":[],"sources":["../../backends/credentials-db.ts"],"sourcesContent":["/**\n * Credentials Store (opt-in)\n * Optional AES-256-GCM encryption with scrypt KDF. When no encryption key is set,\n * values are stored in plaintext; the user may secure data by other means (e.g. filesystem permissions).\n */\n\nimport { createCipheriv, createDecipheriv, createHash, randomBytes, scryptSync } from \"node:crypto\";\nimport { mkdirSync } from \"node:fs\";\nimport { dirname } from \"node:path\";\nimport { DatabaseSync } from \"node:sqlite\";\nimport type { CredentialType } from \"../config.js\";\nimport { assertValidCredentialRow } from \"../services/credential-validation.js\";\nimport { capturePluginError } from \"../services/error-reporter.js\";\nimport { pluginLogger } from \"../utils/logger.js\";\nimport { tryRestrictSqliteDbFileMode } from \"../utils/sqlite-file-perms.js\";\nimport { createTransaction } from \"../utils/sqlite-transaction.js\";\nimport { BaseSqliteStore } from \"./base-sqlite-store.js\";\n\n/** node:sqlite returns BLOBs as Uint8Array; convert to Buffer for crypto ops. */\nfunction toBuffer(val: Uint8Array | Buffer): Buffer {\n return Buffer.isBuffer(val) ? val : Buffer.from(val);\n}\n\nconst CRED_IV_LEN = 12;\nconst CRED_AUTH_TAG_LEN = 16;\nconst CRED_ALGO = \"aes-256-gcm\";\nconst CRED_KDF_VERSION = 2; // v1 = SHA-256 (legacy), v2 = scrypt\nconst CRED_KDF_PLAINTEXT = 0; // no encryption (user secures by other means)\n\n/** Log once per vault path: legacy v1 KDF is weak; opening triggers migration to scrypt when possible. */\nconst _v1KdfWarnedPaths = new Set<string>();\n\n/** v1 only: legacy SHA-256 KDF (weak). Existing vaults cannot be decrypted with another KDF. */\nfunction deriveKeyV1Legacy(password: string): Buffer {\n // codeql[js/insufficient-password-hash]\n // Legacy v1 on-disk format only; new vaults use deriveKeyV2 (scrypt). Changing this breaks existing vaults.\n return createHash(\"sha256\").update(password, \"utf8\").digest();\n}\n\n/** v2: scrypt (N=16384, r=8, p=1). */\nfunction deriveKeyV2(password: string, salt: Buffer): Buffer {\n return scryptSync(password, salt, 32, { N: 16384, r: 8, p: 1 });\n}\n\n/** Dispatch v1 (legacy) vs v2 (scrypt). Prefer deriveKeyV2 for new material when version is known. */\nfunction deriveKey(password: string, salt: Buffer, version: number = CRED_KDF_VERSION): Buffer {\n if (version === 1) {\n // codeql[js/insufficient-password-hash]\n // Password only flows to the intentional legacy KDF above; v2 uses scrypt.\n return deriveKeyV1Legacy(password);\n }\n return deriveKeyV2(password, salt);\n}\n\nfunction encryptValue(plaintext: string, key: Buffer): Buffer {\n const iv = randomBytes(CRED_IV_LEN);\n const cipher = createCipheriv(CRED_ALGO, key, iv);\n const encrypted = Buffer.concat([cipher.update(plaintext, \"utf8\"), cipher.final()]);\n const authTag = cipher.getAuthTag();\n return Buffer.concat([iv, authTag, encrypted]);\n}\n\nfunction decryptValue(buffer: Buffer, key: Buffer): string {\n const iv = buffer.subarray(0, CRED_IV_LEN);\n const authTag = buffer.subarray(CRED_IV_LEN, CRED_IV_LEN + CRED_AUTH_TAG_LEN);\n const encrypted = buffer.subarray(CRED_IV_LEN + CRED_AUTH_TAG_LEN);\n const decipher = createDecipheriv(CRED_ALGO, key, iv);\n decipher.setAuthTag(authTag);\n return Buffer.concat([decipher.update(encrypted), decipher.final()]).toString(\"utf8\");\n}\n\nexport type CredentialEntry = {\n service: string;\n type: CredentialType;\n value: string;\n url: string | null;\n notes: string | null;\n created: number;\n updated: number;\n expires: number | null;\n};\n\nexport class CredentialsDB extends BaseSqliteStore {\n private readonly dbPath: string;\n private key!: Buffer;\n private kdfVersion!: number;\n private salt!: Buffer;\n /** When false, values are stored and read as plaintext (no encryption). Mutable when DB metadata overrides key length heuristics. */\n private storesEncryptedValues: boolean;\n /** True when a 16+ character encryption key was provided to the constructor. */\n private readonly configuredKeyPresent: boolean;\n // SECURITY NOTE: Raw password is stored only for lazy migration from legacy SHA-256 to scrypt.\n // Migration is triggered on first successful get() to verify the password is correct before re-encrypting.\n // After migration completes, this field is cleared to minimize exposure in memory.\n private password: string | null;\n\n constructor(dbPath: string, encryptionKey: string) {\n const keyLooksEncrypted = encryptionKey.length >= 16;\n mkdirSync(dirname(dbPath), { recursive: true });\n const db = new DatabaseSync(dbPath);\n super(db);\n this.dbPath = dbPath;\n this.configuredKeyPresent = keyLooksEncrypted;\n tryRestrictSqliteDbFileMode(dbPath);\n this.storesEncryptedValues = keyLooksEncrypted;\n\n this.liveDb.exec(`\n CREATE TABLE IF NOT EXISTS vault_meta (\n key TEXT PRIMARY KEY,\n value BLOB NOT NULL\n )\n `);\n\n this.liveDb.exec(`\n CREATE TABLE IF NOT EXISTS credentials (\n service TEXT NOT NULL,\n type TEXT NOT NULL DEFAULT 'other',\n value BLOB NOT NULL,\n url TEXT,\n notes TEXT,\n created INTEGER NOT NULL,\n updated INTEGER NOT NULL,\n expires INTEGER,\n PRIMARY KEY (service, type)\n )\n `);\n this.liveDb.exec(`\n CREATE INDEX IF NOT EXISTS idx_credentials_service ON credentials(service)\n `);\n\n const versionRow = this.liveDb.prepare(\"SELECT value FROM vault_meta WHERE key = 'kdf_version'\").get() as\n | { value: Uint8Array | Buffer }\n | undefined;\n const saltRow = this.liveDb.prepare(\"SELECT value FROM vault_meta WHERE key = 'salt'\").get() as\n | { value: Uint8Array | Buffer }\n | undefined;\n\n if (!keyLooksEncrypted) {\n // Plaintext vault: no key derived\n this.kdfVersion = CRED_KDF_PLAINTEXT;\n this.salt = Buffer.alloc(0);\n this.key = Buffer.alloc(0);\n this.password = null;\n if (versionRow && versionRow.value != null && toBuffer(versionRow.value)[0] !== CRED_KDF_PLAINTEXT) {\n throw new Error(\n \"Credentials vault was created with encryption. Set credentials.encryptionKey (or OPENCLAW_CRED_KEY) to open it, or use a new vault path for an unencrypted vault.\",\n );\n }\n if (!versionRow) {\n // C1 FIX: Check if vault has encrypted data before marking as plaintext\n const hasCredentials =\n (this.liveDb.prepare(\"SELECT COUNT(*) as count FROM credentials\").get() as { count: number }).count > 0;\n if (hasCredentials) {\n throw new Error(\n \"Credentials vault contains data but no encryption metadata. This vault may have encrypted credentials. Provide credentials.encryptionKey to open it.\",\n );\n }\n this.liveDb\n .prepare(\"INSERT OR REPLACE INTO vault_meta (key, value) VALUES ('kdf_version', ?)\")\n .run(Buffer.from([CRED_KDF_PLAINTEXT]));\n }\n return;\n }\n\n // Check if vault is plaintext first (before assuming legacy)\n if (versionRow && versionRow.value != null && toBuffer(versionRow.value)[0] === CRED_KDF_PLAINTEXT) {\n // DB metadata says plaintext — never treat as encrypted regardless of key length (Issue #835).\n this.storesEncryptedValues = false;\n this.kdfVersion = CRED_KDF_PLAINTEXT;\n this.salt = Buffer.alloc(0);\n this.key = Buffer.alloc(0);\n this.password = null;\n // Optionally warn that key is being ignored\n if (encryptionKey.length >= 16) {\n pluginLogger.warn(\n \"Credentials vault is in plaintext mode (kdf_version=0). The configured encryption key is being ignored. \" +\n \"To encrypt the existing vault at rest, run: openclaw hybrid-mem credentials encrypt-vault --yes\",\n );\n }\n return;\n }\n\n if (!versionRow || !saltRow) {\n const hasCredentials =\n (this.liveDb.prepare(\"SELECT COUNT(*) as count FROM credentials\").get() as { count: number }).count > 0;\n\n if (hasCredentials) {\n this.kdfVersion = 1;\n this.salt = Buffer.alloc(0);\n this.key = deriveKey(encryptionKey, this.salt, 1);\n this.password = encryptionKey;\n } else {\n this.kdfVersion = CRED_KDF_VERSION;\n this.salt = randomBytes(32);\n this.key = deriveKey(encryptionKey, this.salt, this.kdfVersion);\n this.password = null;\n this.liveDb\n .prepare(\"INSERT OR REPLACE INTO vault_meta (key, value) VALUES ('kdf_version', ?)\")\n .run(Buffer.from([this.kdfVersion]));\n this.liveDb.prepare(\"INSERT OR REPLACE INTO vault_meta (key, value) VALUES ('salt', ?)\").run(this.salt);\n }\n } else {\n this.kdfVersion = versionRow.value != null ? toBuffer(versionRow.value)[0] : CRED_KDF_VERSION;\n this.salt = toBuffer(saltRow.value);\n this.key = deriveKey(encryptionKey, this.salt, this.kdfVersion);\n this.password = this.kdfVersion === 1 ? encryptionKey : null;\n }\n\n if (this.storesEncryptedValues && this.kdfVersion === 1 && !_v1KdfWarnedPaths.has(dbPath)) {\n _v1KdfWarnedPaths.add(dbPath);\n pluginLogger.warn(\n \"memory-hybrid: credentials vault uses legacy key derivation (v1 / SHA-256). This is weak against offline attacks. \" +\n \"The vault will migrate to scrypt (v2) automatically after the next successful unlock. \" +\n \"Set a strong OPENCLAW_CRED_KEY and restart, or rotate secrets after migration.\",\n );\n }\n }\n\n protected getSubsystemName(): string {\n return \"credentials\";\n }\n\n getVaultStatus(): {\n dbPath: string;\n kdfVersion: number;\n encryptedAtRest: boolean;\n configuredKeyPresent: boolean;\n keyIgnored: boolean;\n migrationRequired: boolean;\n } {\n const encryptedAtRest = this.kdfVersion !== CRED_KDF_PLAINTEXT;\n const keyIgnored = this.kdfVersion === CRED_KDF_PLAINTEXT && this.configuredKeyPresent;\n const migrationRequired = keyIgnored;\n return {\n dbPath: this.dbPath,\n kdfVersion: this.kdfVersion,\n encryptedAtRest,\n configuredKeyPresent: this.configuredKeyPresent,\n keyIgnored,\n migrationRequired,\n };\n }\n\n /**\n * Encrypt an existing plaintext vault in place.\n * Requires a 16+ character encryption key. Safe and idempotent:\n * - Throws if vault is already encrypted\n * - Performs an in-DB transaction so it cannot partially convert rows\n */\n enableEncryptionAtRest(encryptionKey: string): { migrated: number; kdfVersion: number } {\n const keyLooksEncrypted = encryptionKey.length >= 16;\n if (!keyLooksEncrypted) {\n throw new Error(\n \"Encryption key is missing or too short. Set credentials.encryptionKey (16+ chars) or OPENCLAW_CRED_KEY before encrypting the vault.\",\n );\n }\n if (this.kdfVersion !== CRED_KDF_PLAINTEXT) {\n throw new Error(`Credentials vault is already encrypted (kdf_version=${this.kdfVersion}).`);\n }\n\n const newSalt = randomBytes(32);\n const newKdfVersion = CRED_KDF_VERSION;\n const newKey = deriveKey(encryptionKey, newSalt, newKdfVersion);\n\n let migratedCount = 0;\n const migrate = createTransaction(\n this.liveDb,\n () => {\n const rows = this.liveDb.prepare(\"SELECT service, type, value FROM credentials\").all() as Array<{\n service: string;\n type: string;\n value: Uint8Array | Buffer;\n }>;\n migratedCount = rows.length;\n const updateStmt = this.liveDb.prepare(\"UPDATE credentials SET value = ? WHERE service = ? AND type = ?\");\n for (const r of rows) {\n const plaintext = toBuffer(r.value).toString(\"utf8\");\n const encrypted = encryptValue(plaintext, newKey);\n updateStmt.run(encrypted as unknown as Uint8Array, r.service, r.type);\n }\n this.liveDb\n .prepare(\"INSERT OR REPLACE INTO vault_meta (key, value) VALUES ('kdf_version', ?)\")\n .run(Buffer.from([newKdfVersion]));\n this.liveDb.prepare(\"INSERT OR REPLACE INTO vault_meta (key, value) VALUES ('salt', ?)\").run(newSalt);\n },\n \"IMMEDIATE\",\n );\n\n migrate();\n\n this.kdfVersion = newKdfVersion;\n this.salt = newSalt;\n this.key = newKey;\n this.password = null;\n this.storesEncryptedValues = true;\n\n return { migrated: migratedCount, kdfVersion: this.kdfVersion };\n }\n\n /**\n * Insert or update a credential. On conflict (service, type), `updated` and value fields refresh;\n * `created` is preserved from the original row — intentional for \"same key, rotated secret\" flows (#894).\n */\n store(entry: {\n service: string;\n type: CredentialType;\n value: string;\n url?: string;\n notes?: string;\n expires?: number | null;\n }): CredentialEntry {\n const now = Math.floor(Date.now() / 1000);\n const stored = this.storesEncryptedValues ? encryptValue(entry.value, this.key) : Buffer.from(entry.value, \"utf8\");\n this.liveDb\n .prepare(\n `INSERT INTO credentials (service, type, value, url, notes, created, updated, expires)\n VALUES (?, ?, ?, ?, ?, ?, ?, ?)\n ON CONFLICT(service, type) DO UPDATE SET\n value = excluded.value,\n url = excluded.url,\n notes = excluded.notes,\n updated = excluded.updated,\n expires = excluded.expires`,\n )\n .run(entry.service, entry.type, stored, entry.url ?? null, entry.notes ?? null, now, now, entry.expires ?? null);\n return {\n service: entry.service,\n type: entry.type,\n value: \"[redacted]\",\n url: entry.url ?? null,\n notes: entry.notes ?? null,\n created: now,\n updated: now,\n expires: entry.expires ?? null,\n };\n }\n\n get(service: string, type?: CredentialType): CredentialEntry | null {\n const row = type\n ? (this.liveDb.prepare(\"SELECT * FROM credentials WHERE service = ? AND type = ?\").get(service, type) as\n | Record<string, unknown>\n | undefined)\n : (this.liveDb\n .prepare(\"SELECT * FROM credentials WHERE service = ? ORDER BY updated DESC LIMIT 1\")\n .get(service) as Record<string, unknown> | undefined);\n if (!row) return null;\n const buf = toBuffer(row.value as Uint8Array | Buffer);\n const value = this.storesEncryptedValues ? decryptValue(buf, this.key) : buf.toString(\"utf8\");\n\n if (this.kdfVersion === 1) {\n try {\n this.migrateLegacyVault();\n } catch (err) {\n capturePluginError(err as Error, {\n operation: \"migrate-vault\",\n severity: \"info\",\n subsystem: \"credentials\",\n });\n // Migration is best-effort; failure should not block credential retrieval\n }\n }\n\n const out = {\n service: row.service as string,\n type: row.type as string as CredentialType,\n value,\n url: (row.url as string) ?? null,\n notes: (row.notes as string) ?? null,\n created: row.created as number,\n updated: row.updated as number,\n expires: (row.expires as number) ?? null,\n };\n assertValidCredentialRow(out);\n return out;\n }\n\n /** Migrate legacy SHA-256 vault to scrypt. Called after first successful decryption. */\n private migrateLegacyVault(): void {\n if (!this.password) {\n throw new Error(\"Migration requires password\");\n }\n\n // Generate new salt and derive new key with scrypt (avoid deriveKey dispatcher so static analysis sees only scrypt here)\n const migrationSalt = randomBytes(32);\n const newKey = deriveKeyV2(this.password, migrationSalt);\n\n // Wrap read + mutations in one transaction (IMMEDIATE) so new rows cannot appear after the snapshot.\n const migrate = createTransaction(\n this.liveDb,\n () => {\n const rows = this.liveDb.prepare(\"SELECT * FROM credentials\").all() as Array<Record<string, unknown>>;\n const updateStmt = this.liveDb.prepare(\"UPDATE credentials SET value = ? WHERE service = ? AND type = ?\");\n for (const row of rows) {\n const oldBuf = toBuffer(row.value as Uint8Array | Buffer);\n const plaintext = decryptValue(oldBuf, this.key); // Decrypt with old key\n const newEncrypted = encryptValue(plaintext, newKey); // Encrypt with new key\n updateStmt.run(newEncrypted as unknown as Uint8Array, row.service as string, row.type as string);\n }\n\n this.liveDb\n .prepare(\"INSERT OR REPLACE INTO vault_meta (key, value) VALUES ('kdf_version', ?)\")\n .run(Buffer.from([CRED_KDF_VERSION]));\n this.liveDb.prepare(\"INSERT OR REPLACE INTO vault_meta (key, value) VALUES ('salt', ?)\").run(migrationSalt);\n },\n \"IMMEDIATE\",\n );\n\n migrate();\n\n this.salt = migrationSalt;\n\n // Update instance state\n this.kdfVersion = CRED_KDF_VERSION;\n this.key = newKey;\n this.password = null;\n }\n\n /**\n * Store only if no entry exists for this service+type.\n * Returns the stored entry on success, or null if an entry already existed (skipped).\n * Use this for auto-capture to avoid overwriting user-managed credentials.\n *\n * Uses a single `INSERT … ON CONFLICT(service, type) DO NOTHING` statement so the\n * check-and-insert is atomic — no TOCTOU race between concurrent writers.\n *\n * Also treats underscore ↔ hyphen variants as equivalent (e.g. `openai_api` and\n * `openai-api`) so that migration from the pre-normalisation naming convention does\n * not create duplicate vault entries on subsequent auto-capture runs.\n */\n storeIfNew(entry: {\n service: string;\n type: CredentialType;\n value: string;\n url?: string;\n notes?: string;\n expires?: number | null;\n }): CredentialEntry | null {\n // Check for a legacy cross-variant (underscore ↔ hyphen) before inserting so we\n // don't create a parallel entry alongside an existing differently-named one.\n const legacyVariant = entry.service.includes(\"_\")\n ? entry.service.replace(/_/g, \"-\")\n : entry.service.replace(/-/g, \"_\");\n if (legacyVariant !== entry.service && this.exists(legacyVariant, entry.type)) {\n return null;\n }\n\n const now = Math.floor(Date.now() / 1000);\n const stored = this.storesEncryptedValues ? encryptValue(entry.value, this.key) : Buffer.from(entry.value, \"utf8\");\n const result = this.liveDb\n .prepare(\n `INSERT INTO credentials (service, type, value, url, notes, created, updated, expires)\n VALUES (?, ?, ?, ?, ?, ?, ?, ?)\n ON CONFLICT(service, type) DO NOTHING`,\n )\n .run(entry.service, entry.type, stored, entry.url ?? null, entry.notes ?? null, now, now, entry.expires ?? null);\n if (result.changes === 0) {\n // A credential already exists for this service+type; do not overwrite it.\n return null;\n }\n return {\n service: entry.service,\n type: entry.type,\n value: \"[redacted]\",\n url: entry.url ?? null,\n notes: entry.notes ?? null,\n created: now,\n updated: now,\n expires: entry.expires ?? null,\n };\n }\n\n /** Returns true if an entry already exists for the given service (and optional type). */\n exists(service: string, type?: CredentialType): boolean {\n if (type) {\n const row = this.liveDb\n .prepare(\"SELECT 1 FROM credentials WHERE service = ? AND type = ? LIMIT 1\")\n .get(service, type);\n return !!row;\n }\n const row = this.liveDb.prepare(\"SELECT 1 FROM credentials WHERE service = ? LIMIT 1\").get(service);\n return !!row;\n }\n\n /**\n * List all credentials with decrypted values.\n * Use sparingly (decrypts every value). Primarily for audit operations.\n */\n listAll(): CredentialEntry[] {\n const rows = this.liveDb.prepare(\"SELECT * FROM credentials ORDER BY service, type\").all() as Array<\n Record<string, unknown>\n >;\n const out: CredentialEntry[] = [];\n for (const row of rows) {\n try {\n const buf = toBuffer(row.value as Uint8Array | Buffer);\n const value = this.storesEncryptedValues ? decryptValue(buf, this.key) : buf.toString(\"utf8\");\n const entry: CredentialEntry = {\n service: row.service as string,\n type: row.type as string as CredentialType,\n value,\n url: (row.url as string) ?? null,\n notes: (row.notes as string) ?? null,\n created: row.created as number,\n updated: row.updated as number,\n expires: (row.expires as number) ?? null,\n };\n assertValidCredentialRow(entry);\n out.push(entry);\n } catch (err) {\n capturePluginError(err instanceof Error ? err : new Error(String(err)), {\n subsystem: \"credentials\",\n operation: \"list-all-skip-row\",\n severity: \"warning\",\n });\n }\n }\n return out;\n }\n\n list(): Array<{ service: string; type: string; url: string | null; expires: number | null }> {\n const rows = this.liveDb\n .prepare(\"SELECT service, type, url, expires FROM credentials ORDER BY service, type\")\n .all() as Array<{\n service: string;\n type: string;\n url: string | null;\n expires: number | null;\n }>;\n return rows;\n }\n\n delete(service: string, type?: CredentialType): boolean {\n if (type) {\n const r = this.liveDb.prepare(\"DELETE FROM credentials WHERE service = ? AND type = ?\").run(service, type);\n return r.changes > 0;\n }\n const r = this.liveDb.prepare(\"DELETE FROM credentials WHERE service = ?\").run(service);\n return r.changes > 0;\n }\n}\n\n// Export encryption primitives for testing\nexport { deriveKey, encryptValue, decryptValue };\n"],"mappings":"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"}