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":"apitap-service.js","names":[],"sources":["../../services/apitap-service.ts"],"sourcesContent":["/**\n * ApiTap Service — wrapper around the `apitap` CLI for browser traffic capture (Issue #614).\n *\n * Provides:\n * - isAvailable(): check if `apitap` binary is installed\n * - capture(url, options): run `apitap capture` and return discovered endpoints\n * - peek(url): run `apitap peek` for quick headless API discovery\n * - buildSkillScaffold(endpoint): generate an OpenClaw skill spec from an endpoint\n *\n * Security model:\n * - Only runs when cfg.apiTap.enabled = true (explicit opt-in)\n * - Validates URLs against allowedPatterns / blockedPatterns before running\n * - Never auto-triggers; only responds to explicit agent tool calls\n */\n\nimport { randomBytes } from \"node:crypto\";\nimport type { ApiTapConfig } from \"../config/types/features.js\";\nimport { spawn, spawnSync } from \"../utils/process-runner.js\";\n\n// ---------------------------------------------------------------------------\n// Public types\n// ---------------------------------------------------------------------------\n\ninterface RawCapturedEndpoint {\n endpoint: string;\n method: string;\n parameters?: Record<string, unknown>;\n sampleResponse?: unknown;\n contentType?: string;\n}\n\ninterface ApitapCaptureResult {\n sessionId: string;\n siteUrl: string;\n endpoints: RawCapturedEndpoint[];\n durationMs: number;\n error?: string;\n}\n\ninterface SkillScaffold {\n skillName: string;\n description: string;\n endpoint: string;\n method: string;\n parameters: Record<string, { type: string; description: string; required?: boolean }>;\n sampleResponse: string;\n curlExample: string;\n}\n\n// ---------------------------------------------------------------------------\n// URL security helpers\n// ---------------------------------------------------------------------------\n\n/**\n * Check if a URL matches any of the given glob-style patterns.\n * Supports `*` (single segment) and `**` (multi-segment) wildcards.\n */\nfunction matchesPattern(url: string, pattern: string): boolean {\n // Convert glob pattern to regex\n const escaped = pattern\n .replace(/[.+^${}()|[\\]\\\\]/g, \"\\\\$&\") // escape regex special chars except * and ?\n .replace(/\\*\\*/g, \"§DOUBLESTAR§\") // placeholder for **\n .replace(/\\*/g, \"[^/]*\") // * matches within a segment\n .replace(/§DOUBLESTAR§/g, \".*\") // ** matches across segments\n .replace(/\\?/g, \".\"); // ? matches exactly one character\n try {\n const regex = new RegExp(`^${escaped}$`, \"i\");\n if (regex.test(url)) return true;\n // Also test with trailing slash to catch patterns like **/auth/** matching /auth\n if (!url.endsWith(\"/\")) {\n return regex.test(`${url}/`);\n }\n return false;\n } catch {\n return false;\n }\n}\n\n/**\n * Check whether a fully-resolved endpoint URL matches any blocked pattern.\n * Unlike validateUrl(), this does NOT check allowedPatterns — those apply to\n * capture targets, not to individual discovered sub-endpoints.\n * Returns an error message when blocked, or null when safe to persist.\n */\nexport function isEndpointBlocked(endpointUrl: string, cfg: ApiTapConfig): string | null {\n let parsed: URL;\n try {\n parsed = new URL(endpointUrl);\n } catch {\n return \"Endpoint URL is malformed and cannot be validated.\";\n }\n if (parsed.protocol !== \"http:\" && parsed.protocol !== \"https:\") {\n return `Endpoint scheme \"${parsed.protocol}\" is not allowed.`;\n }\n const normalizedUrl = `${parsed.protocol}//${parsed.host}${parsed.pathname}`;\n for (const pattern of cfg.blockedPatterns) {\n if (matchesPattern(normalizedUrl, pattern)) {\n return `Endpoint matches blocked pattern \"${pattern}\".`;\n }\n }\n return null;\n}\n\n/**\n * Validate a URL against allowed/blocked patterns.\n * Returns null if allowed, or an error message if blocked.\n */\nexport function validateUrl(url: string, cfg: ApiTapConfig): string | null {\n // Parse and validate URL first to normalize percent-encoded paths\n let parsed: URL;\n try {\n parsed = new URL(url);\n } catch {\n return `Invalid URL: \"${url}\"`;\n }\n if (parsed.protocol !== \"http:\" && parsed.protocol !== \"https:\") {\n return `URL scheme \"${parsed.protocol}\" is not allowed. Only http: and https: are permitted.`;\n }\n\n // Build normalized URL string for pattern matching (protocol + host + pathname only)\n // Query strings and fragments are excluded to ensure path-based patterns work correctly\n const normalizedUrl = `${parsed.protocol}//${parsed.host}${parsed.pathname}`;\n\n // Check blocked patterns first (blocklist wins)\n for (const pattern of cfg.blockedPatterns) {\n if (matchesPattern(normalizedUrl, pattern)) {\n return `URL matches blocked pattern \"${pattern}\". ApiTap will not capture auth/sensitive endpoints.`;\n }\n }\n\n // Check allowed patterns (empty = allow all)\n if (cfg.allowedPatterns.length > 0) {\n const allowed = cfg.allowedPatterns.some((p) => matchesPattern(normalizedUrl, p));\n if (!allowed) {\n return \"URL does not match any allowed pattern. Configure apiTap.allowedPatterns to permit this site.\";\n }\n }\n\n return null;\n}\n\n// ---------------------------------------------------------------------------\n// ApitapService\n// ---------------------------------------------------------------------------\n\nexport class ApitapService {\n private cfg: ApiTapConfig;\n private _available: boolean | null = null;\n\n constructor(cfg: ApiTapConfig) {\n this.cfg = cfg;\n }\n\n /**\n * Check if the `apitap` CLI binary is installed and reachable.\n * Result is cached after first check.\n */\n isAvailable(): boolean {\n if (this._available !== null) return this._available;\n try {\n const result = spawnSync(\"apitap\", [\"--version\"], {\n encoding: \"utf8\",\n timeout: 5000,\n stdio: [\"ignore\", \"pipe\", \"pipe\"],\n });\n this._available = result.status === 0;\n } catch {\n this._available = false;\n }\n return this._available;\n }\n\n /**\n * Run `apitap capture` to record live browser traffic for a site.\n * Blocks until the capture session ends (timeout) or the user stops it.\n */\n async capture(siteUrl: string, timeoutSeconds?: number): Promise<ApitapCaptureResult> {\n const timeout = timeoutSeconds ?? this.cfg.captureTimeoutSeconds;\n const startMs = Date.now();\n\n return new Promise((resolve) => {\n const proc = spawn(\n \"apitap\",\n [\"capture\", \"--url\", siteUrl, \"--timeout\", String(timeout), \"--output\", \"json\", \"--no-browser-ui\"],\n {\n stdio: [\"ignore\", \"pipe\", \"pipe\"],\n },\n );\n\n let stdout = \"\";\n let stderr = \"\";\n\n proc.stdout.on(\"data\", (data: Buffer) => {\n stdout += data.toString();\n });\n\n proc.stderr.on(\"data\", (data: Buffer) => {\n stderr += data.toString();\n });\n\n const killTimer = setTimeout(\n () => {\n proc.kill(\"SIGTERM\");\n },\n (timeout + 10) * 1000,\n );\n\n proc.on(\"close\", (code) => {\n clearTimeout(killTimer);\n const durationMs = Date.now() - startMs;\n\n if (code !== 0) {\n const errMsg = stderr.trim() || `apitap capture exited with code ${code}`;\n resolve({ sessionId: \"\", siteUrl, endpoints: [], durationMs, error: errMsg });\n return;\n }\n\n resolve(this.parseCaptureOutput(stdout, siteUrl, durationMs));\n });\n\n proc.on(\"error\", (err) => {\n clearTimeout(killTimer);\n const durationMs = Date.now() - startMs;\n resolve({ sessionId: \"\", siteUrl, endpoints: [], durationMs, error: err.message });\n });\n });\n }\n\n /**\n * Run `apitap peek` for quick headless API discovery (no browser window).\n * Faster than capture but may miss dynamically loaded endpoints.\n */\n async peek(siteUrl: string): Promise<ApitapCaptureResult> {\n const timeout = this.cfg.captureTimeoutSeconds;\n const startMs = Date.now();\n\n return new Promise((resolve) => {\n const proc = spawn(\"apitap\", [\"peek\", \"--url\", siteUrl, \"--timeout\", String(timeout), \"--output\", \"json\"], {\n stdio: [\"ignore\", \"pipe\", \"pipe\"],\n });\n\n let stdout = \"\";\n let stderr = \"\";\n\n proc.stdout.on(\"data\", (data: Buffer) => {\n stdout += data.toString();\n });\n\n proc.stderr.on(\"data\", (data: Buffer) => {\n stderr += data.toString();\n });\n\n const killTimer = setTimeout(\n () => {\n proc.kill(\"SIGTERM\");\n },\n (timeout + 10) * 1000,\n );\n\n proc.on(\"close\", (code) => {\n clearTimeout(killTimer);\n const durationMs = Date.now() - startMs;\n\n if (code !== 0) {\n const errMsg = stderr.trim() || `apitap peek exited with code ${code}`;\n resolve({ sessionId: \"\", siteUrl, endpoints: [], durationMs, error: errMsg });\n return;\n }\n\n resolve(this.parseCaptureOutput(stdout, siteUrl, durationMs));\n });\n\n proc.on(\"error\", (err) => {\n clearTimeout(killTimer);\n const durationMs = Date.now() - startMs;\n resolve({ sessionId: \"\", siteUrl, endpoints: [], durationMs, error: err.message });\n });\n });\n }\n\n /**\n * Generate an OpenClaw skill scaffold from a discovered API endpoint.\n * Returns a structured spec ready for human review and skill file generation.\n */\n buildSkillScaffold(\n siteUrl: string,\n endpoint: string,\n method: string,\n parameters: Record<string, unknown>,\n sampleResponse: unknown,\n contentType?: string,\n ): SkillScaffold {\n const urlObj = (() => {\n try {\n return new URL(siteUrl);\n } catch {\n return null;\n }\n })();\n\n const host = urlObj?.hostname ?? siteUrl;\n const endpointSlug = endpoint\n .replace(/[^a-zA-Z0-9]/g, \"_\")\n .replace(/_+/g, \"_\")\n .replace(/^_|_$/g, \"\");\n const skillName = `${host.replace(/\\./g, \"_\")}_${method.toLowerCase()}_${endpointSlug}`.slice(0, 60);\n\n // Build parameter schema from observed data\n const paramSchema: Record<string, { type: string; description: string; required?: boolean }> = {};\n for (const [key, value] of Object.entries(parameters)) {\n const jsType = typeof value === \"number\" ? \"number\" : typeof value === \"boolean\" ? \"boolean\" : \"string\";\n paramSchema[key] = {\n type: jsType,\n description: `Parameter: ${key}`,\n };\n }\n\n const sampleResponseStr = (() => {\n try {\n return JSON.stringify(sampleResponse, null, 2).slice(0, 2000);\n } catch {\n return String(sampleResponse).slice(0, 2000);\n }\n })();\n\n const isAbsoluteUrl = endpoint.startsWith(\"http://\") || endpoint.startsWith(\"https://\");\n const baseUrl = isAbsoluteUrl ? encodeURI(endpoint) : encodeURI(`${siteUrl}${endpoint}`);\n const upperMethod = method.toUpperCase();\n\n // For GET/HEAD, serialize parameters as a query string so the scaffold\n // reflects the captured call semantics rather than silently dropping them.\n const queryString = Object.entries(parameters)\n .map(([k, v]) => `${encodeURIComponent(k)}=${encodeURIComponent(String(v))}`)\n .join(\"&\");\n const getUrl = queryString ? `${baseUrl}?${queryString}` : baseUrl;\n\n const isJsonContentType = contentType?.toLowerCase().includes(\"application/json\");\n\n let curlExample: string;\n if (upperMethod === \"GET\" || upperMethod === \"HEAD\") {\n curlExample = `curl \"${getUrl}\"`;\n } else {\n const curlParams = isJsonContentType\n ? (() => {\n const jsonBody = Object.entries(parameters).reduce(\n (acc, [k, v]) => {\n acc[k] = v;\n return acc;\n },\n {} as Record<string, unknown>,\n );\n const escapedJson = JSON.stringify(jsonBody).replace(/'/g, \"'\\\\''\");\n return `-H 'Content-Type: application/json' -d '${escapedJson}'`;\n })()\n : Object.entries(parameters)\n .map(([k, v]) => {\n const escapedValue = String(v).replace(/'/g, \"'\\\\''\");\n return `-d '${k}=${escapedValue}'`;\n })\n .join(\" \");\n curlExample = `curl -X ${upperMethod} \"${baseUrl}\" ${curlParams}`.trim();\n }\n\n return {\n skillName,\n description: `Call the ${method.toUpperCase()} ${endpoint} endpoint on ${host}`,\n endpoint,\n method: method.toUpperCase(),\n parameters: paramSchema,\n sampleResponse: sampleResponseStr,\n curlExample,\n };\n }\n\n // ---------------------------------------------------------------------------\n // Private helpers\n // ---------------------------------------------------------------------------\n\n private parseCaptureOutput(output: string, siteUrl: string, durationMs: number): ApitapCaptureResult {\n const trimmed = output.trim();\n if (!trimmed) {\n return {\n sessionId: \"\",\n siteUrl,\n endpoints: [],\n durationMs,\n error: \"apitap returned empty output\",\n };\n }\n\n try {\n const parsed = JSON.parse(trimmed) as Record<string, unknown>;\n const sessionId = typeof parsed.sessionId === \"string\" ? parsed.sessionId : randomSessionId();\n const rawEndpoints = Array.isArray(parsed.endpoints)\n ? (parsed.endpoints as unknown[]).slice(0, this.cfg.maxEndpointsPerSession)\n : [];\n\n const endpoints: RawCapturedEndpoint[] = rawEndpoints\n .filter((e): e is Record<string, unknown> => e !== null && typeof e === \"object\")\n .map((e) => ({\n endpoint: typeof e.endpoint === \"string\" ? e.endpoint : \"/\",\n method: typeof e.method === \"string\" ? e.method.toUpperCase() : \"GET\",\n parameters:\n e.parameters && typeof e.parameters === \"object\" && !Array.isArray(e.parameters)\n ? (e.parameters as Record<string, unknown>)\n : {},\n sampleResponse: e.sampleResponse,\n contentType: typeof e.contentType === \"string\" ? e.contentType : \"application/json\",\n }));\n\n return { sessionId, siteUrl, endpoints, durationMs };\n } catch {\n // Not valid JSON — apitap may have printed non-JSON output\n return {\n sessionId: \"\",\n siteUrl,\n endpoints: [],\n durationMs,\n error: `apitap output could not be parsed as JSON. Raw output: ${trimmed.slice(0, 200)}`,\n };\n }\n }\n}\n\nfunction randomSessionId(): string {\n return `session-${Date.now()}-${randomBytes(3).toString(\"hex\")}`;\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;AAyDA,SAAS,eAAe,KAAa,SAA0B;CAE7D,MAAM,UAAU,QACb,QAAQ,qBAAqB,OAAO,CACpC,QAAQ,SAAS,eAAe,CAChC,QAAQ,OAAO,QAAQ,CACvB,QAAQ,iBAAiB,KAAK,CAC9B,QAAQ,OAAO,IAAI;CACtB,IAAI;EACF,MAAM,QAAQ,IAAI,OAAO,IAAI,QAAQ,IAAI,IAAI;EAC7C,IAAI,MAAM,KAAK,IAAI,EAAE,OAAO;EAE5B,IAAI,CAAC,IAAI,SAAS,IAAI,EACpB,OAAO,MAAM,KAAK,GAAG,IAAI,GAAG;EAE9B,OAAO;SACD;EACN,OAAO;;;;;;;;;AAUX,SAAgB,kBAAkB,aAAqB,KAAkC;CACvF,IAAI;CACJ,IAAI;EACF,SAAS,IAAI,IAAI,YAAY;SACvB;EACN,OAAO;;CAET,IAAI,OAAO,aAAa,WAAW,OAAO,aAAa,UACrD,OAAO,oBAAoB,OAAO,SAAS;CAE7C,MAAM,gBAAgB,GAAG,OAAO,SAAS,IAAI,OAAO,OAAO,OAAO;CAClE,KAAK,MAAM,WAAW,IAAI,iBACxB,IAAI,eAAe,eAAe,QAAQ,EACxC,OAAO,qCAAqC,QAAQ;CAGxD,OAAO;;;;;;AAOT,SAAgB,YAAY,KAAa,KAAkC;CAEzE,IAAI;CACJ,IAAI;EACF,SAAS,IAAI,IAAI,IAAI;SACf;EACN,OAAO,iBAAiB,IAAI;;CAE9B,IAAI,OAAO,aAAa,WAAW,OAAO,aAAa,UACrD,OAAO,eAAe,OAAO,SAAS;CAKxC,MAAM,gBAAgB,GAAG,OAAO,SAAS,IAAI,OAAO,OAAO,OAAO;CAGlE,KAAK,MAAM,WAAW,IAAI,iBACxB,IAAI,eAAe,eAAe,QAAQ,EACxC,OAAO,gCAAgC,QAAQ;CAKnD,IAAI,IAAI,gBAAgB,SAAS;MAE3B,CADY,IAAI,gBAAgB,MAAM,MAAM,eAAe,eAAe,EAAE,CACpE,EACV,OAAO;;CAIX,OAAO;;AAOT,IAAa,gBAAb,MAA2B;CACzB;CACA,aAAqC;CAErC,YAAY,KAAmB;EAC7B,KAAK,MAAM;;;;;;CAOb,cAAuB;EACrB,IAAI,KAAK,eAAe,MAAM,OAAO,KAAK;EAC1C,IAAI;GACF,MAAM,SAAS,UAAU,UAAU,CAAC,YAAY,EAAE;IAChD,UAAU;IACV,SAAS;IACT,OAAO;KAAC;KAAU;KAAQ;KAAO;IAClC,CAAC;GACF,KAAK,aAAa,OAAO,WAAW;UAC9B;GACN,KAAK,aAAa;;EAEpB,OAAO,KAAK;;;;;;CAOd,MAAM,QAAQ,SAAiB,gBAAuD;EACpF,MAAM,UAAU,kBAAkB,KAAK,IAAI;EAC3C,MAAM,UAAU,KAAK,KAAK;EAE1B,OAAO,IAAI,SAAS,YAAY;GAC9B,MAAM,OAAO,MACX,UACA;IAAC;IAAW;IAAS;IAAS;IAAa,OAAO,QAAQ;IAAE;IAAY;IAAQ;IAAkB,EAClG,EACE,OAAO;IAAC;IAAU;IAAQ;IAAO,EAClC,CACF;GAED,IAAI,SAAS;GACb,IAAI,SAAS;GAEb,KAAK,OAAO,GAAG,SAAS,SAAiB;IACvC,UAAU,KAAK,UAAU;KACzB;GAEF,KAAK,OAAO,GAAG,SAAS,SAAiB;IACvC,UAAU,KAAK,UAAU;KACzB;GAEF,MAAM,YAAY,iBACV;IACJ,KAAK,KAAK,UAAU;OAErB,UAAU,MAAM,IAClB;GAED,KAAK,GAAG,UAAU,SAAS;IACzB,aAAa,UAAU;IACvB,MAAM,aAAa,KAAK,KAAK,GAAG;IAEhC,IAAI,SAAS,GAAG;KAEd,QAAQ;MAAE,WAAW;MAAI;MAAS,WAAW,EAAE;MAAE;MAAY,OAD9C,OAAO,MAAM,IAAI,mCAAmC;MACS,CAAC;KAC7E;;IAGF,QAAQ,KAAK,mBAAmB,QAAQ,SAAS,WAAW,CAAC;KAC7D;GAEF,KAAK,GAAG,UAAU,QAAQ;IACxB,aAAa,UAAU;IAEvB,QAAQ;KAAE,WAAW;KAAI;KAAS,WAAW,EAAE;KAAE,YAD9B,KAAK,KAAK,GAAG;KAC6B,OAAO,IAAI;KAAS,CAAC;KAClF;IACF;;;;;;CAOJ,MAAM,KAAK,SAA+C;EACxD,MAAM,UAAU,KAAK,IAAI;EACzB,MAAM,UAAU,KAAK,KAAK;EAE1B,OAAO,IAAI,SAAS,YAAY;GAC9B,MAAM,OAAO,MAAM,UAAU;IAAC;IAAQ;IAAS;IAAS;IAAa,OAAO,QAAQ;IAAE;IAAY;IAAO,EAAE,EACzG,OAAO;IAAC;IAAU;IAAQ;IAAO,EAClC,CAAC;GAEF,IAAI,SAAS;GACb,IAAI,SAAS;GAEb,KAAK,OAAO,GAAG,SAAS,SAAiB;IACvC,UAAU,KAAK,UAAU;KACzB;GAEF,KAAK,OAAO,GAAG,SAAS,SAAiB;IACvC,UAAU,KAAK,UAAU;KACzB;GAEF,MAAM,YAAY,iBACV;IACJ,KAAK,KAAK,UAAU;OAErB,UAAU,MAAM,IAClB;GAED,KAAK,GAAG,UAAU,SAAS;IACzB,aAAa,UAAU;IACvB,MAAM,aAAa,KAAK,KAAK,GAAG;IAEhC,IAAI,SAAS,GAAG;KAEd,QAAQ;MAAE,WAAW;MAAI;MAAS,WAAW,EAAE;MAAE;MAAY,OAD9C,OAAO,MAAM,IAAI,gCAAgC;MACY,CAAC;KAC7E;;IAGF,QAAQ,KAAK,mBAAmB,QAAQ,SAAS,WAAW,CAAC;KAC7D;GAEF,KAAK,GAAG,UAAU,QAAQ;IACxB,aAAa,UAAU;IAEvB,QAAQ;KAAE,WAAW;KAAI;KAAS,WAAW,EAAE;KAAE,YAD9B,KAAK,KAAK,GAAG;KAC6B,OAAO,IAAI;KAAS,CAAC;KAClF;IACF;;;;;;CAOJ,mBACE,SACA,UACA,QACA,YACA,gBACA,aACe;EASf,MAAM,cARgB;GACpB,IAAI;IACF,OAAO,IAAI,IAAI,QAAQ;WACjB;IACN,OAAO;;MAIQ,EAAE,YAAY;EACjC,MAAM,eAAe,SAClB,QAAQ,iBAAiB,IAAI,CAC7B,QAAQ,OAAO,IAAI,CACnB,QAAQ,UAAU,GAAG;EACxB,MAAM,YAAY,GAAG,KAAK,QAAQ,OAAO,IAAI,CAAC,GAAG,OAAO,aAAa,CAAC,GAAG,eAAe,MAAM,GAAG,GAAG;EAGpG,MAAM,cAAyF,EAAE;EACjG,KAAK,MAAM,CAAC,KAAK,UAAU,OAAO,QAAQ,WAAW,EAEnD,YAAY,OAAO;GACjB,MAFa,OAAO,UAAU,WAAW,WAAW,OAAO,UAAU,YAAY,YAAY;GAG7F,aAAa,cAAc;GAC5B;EAGH,MAAM,2BAA2B;GAC/B,IAAI;IACF,OAAO,KAAK,UAAU,gBAAgB,MAAM,EAAE,CAAC,MAAM,GAAG,IAAK;WACvD;IACN,OAAO,OAAO,eAAe,CAAC,MAAM,GAAG,IAAK;;MAE5C;EAGJ,MAAM,UADgB,SAAS,WAAW,UAAU,IAAI,SAAS,WAAW,WAAW,GACvD,UAAU,SAAS,GAAG,UAAU,GAAG,UAAU,WAAW;EACxF,MAAM,cAAc,OAAO,aAAa;EAIxC,MAAM,cAAc,OAAO,QAAQ,WAAW,CAC3C,KAAK,CAAC,GAAG,OAAO,GAAG,mBAAmB,EAAE,CAAC,GAAG,mBAAmB,OAAO,EAAE,CAAC,GAAG,CAC5E,KAAK,IAAI;EACZ,MAAM,SAAS,cAAc,GAAG,QAAQ,GAAG,gBAAgB;EAE3D,MAAM,oBAAoB,aAAa,aAAa,CAAC,SAAS,mBAAmB;EAEjF,IAAI;EACJ,IAAI,gBAAgB,SAAS,gBAAgB,QAC3C,cAAc,SAAS,OAAO;OAoB9B,cAAc,WAAW,YAAY,IAAI,QAAQ,IAlB9B,2BACR;GACL,MAAM,WAAW,OAAO,QAAQ,WAAW,CAAC,QACzC,KAAK,CAAC,GAAG,OAAO;IACf,IAAI,KAAK;IACT,OAAO;MAET,EAAE,CACH;GAED,OAAO,2CADa,KAAK,UAAU,SAAS,CAAC,QAAQ,MAAM,QACE,CAAC;MAC5D,GACJ,OAAO,QAAQ,WAAW,CACvB,KAAK,CAAC,GAAG,OAAO;GAEf,OAAO,OAAO,EAAE,GADK,OAAO,EAAE,CAAC,QAAQ,MAAM,QACd,CAAC;IAChC,CACD,KAAK,IAAI,GACkD,MAAM;EAG1E,OAAO;GACL;GACA,aAAa,YAAY,OAAO,aAAa,CAAC,GAAG,SAAS,eAAe;GACzE;GACA,QAAQ,OAAO,aAAa;GAC5B,YAAY;GACZ,gBAAgB;GAChB;GACD;;CAOH,mBAA2B,QAAgB,SAAiB,YAAyC;EACnG,MAAM,UAAU,OAAO,MAAM;EAC7B,IAAI,CAAC,SACH,OAAO;GACL,WAAW;GACX;GACA,WAAW,EAAE;GACb;GACA,OAAO;GACR;EAGH,IAAI;GACF,MAAM,SAAS,KAAK,MAAM,QAAQ;GAmBlC,OAAO;IAAE,WAlBS,OAAO,OAAO,cAAc,WAAW,OAAO,YAAY,iBAAiB;IAkBzE;IAAS,YAjBR,MAAM,QAAQ,OAAO,UAAU,GAC/C,OAAO,UAAwB,MAAM,GAAG,KAAK,IAAI,uBAAuB,GACzE,EAAE,EAGH,QAAQ,MAAoC,MAAM,QAAQ,OAAO,MAAM,SAAS,CAChF,KAAK,OAAO;KACX,UAAU,OAAO,EAAE,aAAa,WAAW,EAAE,WAAW;KACxD,QAAQ,OAAO,EAAE,WAAW,WAAW,EAAE,OAAO,aAAa,GAAG;KAChE,YACE,EAAE,cAAc,OAAO,EAAE,eAAe,YAAY,CAAC,MAAM,QAAQ,EAAE,WAAW,GAC3E,EAAE,aACH,EAAE;KACR,gBAAgB,EAAE;KAClB,aAAa,OAAO,EAAE,gBAAgB,WAAW,EAAE,cAAc;KAClE,EAEmC;IAAE;IAAY;UAC9C;GAEN,OAAO;IACL,WAAW;IACX;IACA,WAAW,EAAE;IACb;IACA,OAAO,0DAA0D,QAAQ,MAAM,GAAG,IAAI;IACvF;;;;AAKP,SAAS,kBAA0B;CACjC,OAAO,WAAW,KAAK,KAAK,CAAC,GAAG,YAAY,EAAE,CAAC,SAAS,MAAM"}
1
+ {"version":3,"file":"apitap-service.js","names":[],"sources":["../../services/apitap-service.ts"],"sourcesContent":["/**\n * ApiTap Service — wrapper around the `apitap` CLI for browser traffic capture (Issue #614).\n *\n * Provides:\n * - isAvailable(): check if `apitap` binary is installed\n * - capture(url, options): run `apitap capture` and return discovered endpoints\n * - peek(url): run `apitap peek` for quick headless API discovery\n * - buildSkillScaffold(endpoint): generate an OpenClaw skill spec from an endpoint\n *\n * Security model:\n * - Only runs when cfg.apiTap.enabled = true (explicit opt-in)\n * - Validates URLs against allowedPatterns / blockedPatterns before running\n * - Never auto-triggers; only responds to explicit agent tool calls\n */\n\nimport { randomBytes } from \"node:crypto\";\nimport type { ApiTapConfig } from \"../config/types/features.js\";\nimport { spawn, spawnSync } from \"../utils/process-runner.js\";\n\n// ---------------------------------------------------------------------------\n// Public types\n// ---------------------------------------------------------------------------\n\ninterface RawCapturedEndpoint {\n endpoint: string;\n method: string;\n parameters?: Record<string, unknown>;\n sampleResponse?: unknown;\n contentType?: string;\n}\n\ninterface ApitapCaptureResult {\n sessionId: string;\n siteUrl: string;\n endpoints: RawCapturedEndpoint[];\n durationMs: number;\n error?: string;\n}\n\ninterface SkillScaffold {\n skillName: string;\n description: string;\n endpoint: string;\n method: string;\n parameters: Record<string, { type: string; description: string; required?: boolean }>;\n sampleResponse: string;\n curlExample: string;\n}\n\n// ---------------------------------------------------------------------------\n// URL security helpers\n// ---------------------------------------------------------------------------\n\n/**\n * Check if a URL matches any of the given glob-style patterns.\n * Supports `*` (single segment) and `**` (multi-segment) wildcards.\n */\nfunction matchesPattern(url: string, pattern: string): boolean {\n // Convert glob pattern to regex\n const escaped = pattern\n .replace(/[.+^${}()|[\\]\\\\]/g, \"\\\\$&\") // escape regex special chars except * and ?\n .replace(/\\*\\*/g, \"§DOUBLESTAR§\") // placeholder for **\n .replace(/\\*/g, \"[^/]*\") // * matches within a segment\n .replace(/§DOUBLESTAR§/g, \".*\") // ** matches across segments\n .replace(/\\?/g, \".\"); // ? matches exactly one character\n try {\n const regex = new RegExp(`^${escaped}$`, \"i\");\n if (regex.test(url)) return true;\n // Also test with trailing slash to catch patterns like **/auth/** matching /auth\n if (!url.endsWith(\"/\")) {\n return regex.test(`${url}/`);\n }\n return false;\n } catch {\n return false;\n }\n}\n\n/**\n * Check whether a fully-resolved endpoint URL matches any blocked pattern.\n * Unlike validateUrl(), this does NOT check allowedPatterns — those apply to\n * capture targets, not to individual discovered sub-endpoints.\n * Returns an error message when blocked, or null when safe to persist.\n */\nexport function isEndpointBlocked(endpointUrl: string, cfg: ApiTapConfig): string | null {\n let parsed: URL;\n try {\n parsed = new URL(endpointUrl);\n } catch {\n return \"Endpoint URL is malformed and cannot be validated.\";\n }\n if (parsed.protocol !== \"http:\" && parsed.protocol !== \"https:\") {\n return `Endpoint scheme \"${parsed.protocol}\" is not allowed.`;\n }\n const normalizedUrl = `${parsed.protocol}//${parsed.host}${parsed.pathname}`;\n for (const pattern of cfg.blockedPatterns) {\n if (matchesPattern(normalizedUrl, pattern)) {\n return `Endpoint matches blocked pattern \"${pattern}\".`;\n }\n }\n return null;\n}\n\n/**\n * Validate a URL against allowed/blocked patterns.\n * Returns null if allowed, or an error message if blocked.\n */\nexport function validateUrl(url: string, cfg: ApiTapConfig): string | null {\n // Parse and validate URL first to normalize percent-encoded paths\n let parsed: URL;\n try {\n parsed = new URL(url);\n } catch {\n return `Invalid URL: \"${url}\"`;\n }\n if (parsed.protocol !== \"http:\" && parsed.protocol !== \"https:\") {\n return `URL scheme \"${parsed.protocol}\" is not allowed. Only http: and https: are permitted.`;\n }\n\n // Build normalized URL string for pattern matching (protocol + host + pathname only)\n // Query strings and fragments are excluded to ensure path-based patterns work correctly\n const normalizedUrl = `${parsed.protocol}//${parsed.host}${parsed.pathname}`;\n\n // Check blocked patterns first (blocklist wins)\n for (const pattern of cfg.blockedPatterns) {\n if (matchesPattern(normalizedUrl, pattern)) {\n return `URL matches blocked pattern \"${pattern}\". ApiTap will not capture auth/sensitive endpoints.`;\n }\n }\n\n // Check allowed patterns (empty = allow all)\n if (cfg.allowedPatterns.length > 0) {\n const allowed = cfg.allowedPatterns.some((p) => matchesPattern(normalizedUrl, p));\n if (!allowed) {\n return \"URL does not match any allowed pattern. Configure apiTap.allowedPatterns to permit this site.\";\n }\n }\n\n return null;\n}\n\n// ---------------------------------------------------------------------------\n// ApitapService\n// ---------------------------------------------------------------------------\n\nexport class ApitapService {\n private cfg: ApiTapConfig;\n private _available: boolean | null = null;\n\n constructor(cfg: ApiTapConfig) {\n this.cfg = cfg;\n }\n\n /**\n * Check if the `apitap` CLI binary is installed and reachable.\n * Result is cached after first check.\n */\n isAvailable(): boolean {\n if (this._available !== null) return this._available;\n try {\n const result = spawnSync(\"apitap\", [\"--version\"], {\n encoding: \"utf8\",\n timeout: 5000,\n stdio: [\"ignore\", \"pipe\", \"pipe\"],\n });\n this._available = result.status === 0;\n } catch {\n this._available = false;\n }\n return this._available;\n }\n\n /**\n * Run `apitap capture` to record live browser traffic for a site.\n * Blocks until the capture session ends (timeout) or the user stops it.\n */\n async capture(siteUrl: string, timeoutSeconds?: number): Promise<ApitapCaptureResult> {\n const timeout = timeoutSeconds ?? this.cfg.captureTimeoutSeconds;\n const startMs = Date.now();\n\n return new Promise((resolve) => {\n const proc = spawn(\n \"apitap\",\n [\"capture\", \"--url\", siteUrl, \"--timeout\", String(timeout), \"--output\", \"json\", \"--no-browser-ui\"],\n {\n stdio: [\"ignore\", \"pipe\", \"pipe\"],\n },\n );\n\n let stdout = \"\";\n let stderr = \"\";\n\n proc.stdout.on(\"data\", (data: Buffer) => {\n stdout += data.toString();\n });\n\n proc.stderr.on(\"data\", (data: Buffer) => {\n stderr += data.toString();\n });\n\n const killTimer = setTimeout(\n () => {\n proc.kill(\"SIGTERM\");\n },\n (timeout + 10) * 1000,\n );\n\n proc.on(\"close\", (code) => {\n clearTimeout(killTimer);\n const durationMs = Date.now() - startMs;\n\n if (code !== 0) {\n const errMsg = stderr.trim() || `apitap capture exited with code ${code}`;\n resolve({ sessionId: \"\", siteUrl, endpoints: [], durationMs, error: errMsg });\n return;\n }\n\n resolve(this.parseCaptureOutput(stdout, siteUrl, durationMs));\n });\n\n proc.on(\"error\", (err) => {\n clearTimeout(killTimer);\n const durationMs = Date.now() - startMs;\n resolve({ sessionId: \"\", siteUrl, endpoints: [], durationMs, error: err.message });\n });\n });\n }\n\n /**\n * Run `apitap peek` for quick headless API discovery (no browser window).\n * Faster than capture but may miss dynamically loaded endpoints.\n */\n async peek(siteUrl: string): Promise<ApitapCaptureResult> {\n const timeout = this.cfg.captureTimeoutSeconds;\n const startMs = Date.now();\n\n return new Promise((resolve) => {\n const proc = spawn(\"apitap\", [\"peek\", \"--url\", siteUrl, \"--timeout\", String(timeout), \"--output\", \"json\"], {\n stdio: [\"ignore\", \"pipe\", \"pipe\"],\n });\n\n let stdout = \"\";\n let stderr = \"\";\n\n proc.stdout.on(\"data\", (data: Buffer) => {\n stdout += data.toString();\n });\n\n proc.stderr.on(\"data\", (data: Buffer) => {\n stderr += data.toString();\n });\n\n const killTimer = setTimeout(\n () => {\n proc.kill(\"SIGTERM\");\n },\n (timeout + 10) * 1000,\n );\n\n proc.on(\"close\", (code) => {\n clearTimeout(killTimer);\n const durationMs = Date.now() - startMs;\n\n if (code !== 0) {\n const errMsg = stderr.trim() || `apitap peek exited with code ${code}`;\n resolve({ sessionId: \"\", siteUrl, endpoints: [], durationMs, error: errMsg });\n return;\n }\n\n resolve(this.parseCaptureOutput(stdout, siteUrl, durationMs));\n });\n\n proc.on(\"error\", (err) => {\n clearTimeout(killTimer);\n const durationMs = Date.now() - startMs;\n resolve({ sessionId: \"\", siteUrl, endpoints: [], durationMs, error: err.message });\n });\n });\n }\n\n /**\n * Generate an OpenClaw skill scaffold from a discovered API endpoint.\n * Returns a structured spec ready for human review and skill file generation.\n */\n buildSkillScaffold(\n siteUrl: string,\n endpoint: string,\n method: string,\n parameters: Record<string, unknown>,\n sampleResponse: unknown,\n contentType?: string,\n ): SkillScaffold {\n const urlObj = (() => {\n try {\n return new URL(siteUrl);\n } catch {\n return null;\n }\n })();\n\n const host = urlObj?.hostname ?? siteUrl;\n const endpointSlug = endpoint\n .replace(/[^a-zA-Z0-9]/g, \"_\")\n .replace(/_+/g, \"_\")\n .replace(/^_|_$/g, \"\");\n const skillName = `${host.replace(/\\./g, \"_\")}_${method.toLowerCase()}_${endpointSlug}`.slice(0, 60);\n\n // Build parameter schema from observed data\n const paramSchema: Record<string, { type: string; description: string; required?: boolean }> = {};\n for (const [key, value] of Object.entries(parameters)) {\n const jsType = typeof value === \"number\" ? \"number\" : typeof value === \"boolean\" ? \"boolean\" : \"string\";\n paramSchema[key] = {\n type: jsType,\n description: `Parameter: ${key}`,\n };\n }\n\n const sampleResponseStr = (() => {\n try {\n return JSON.stringify(sampleResponse, null, 2).slice(0, 2000);\n } catch {\n return String(sampleResponse).slice(0, 2000);\n }\n })();\n\n const isAbsoluteUrl = endpoint.startsWith(\"http://\") || endpoint.startsWith(\"https://\");\n const baseUrl = isAbsoluteUrl ? encodeURI(endpoint) : encodeURI(`${siteUrl}${endpoint}`);\n const upperMethod = method.toUpperCase();\n\n // For GET/HEAD, serialize parameters as a query string so the scaffold\n // reflects the captured call semantics rather than silently dropping them.\n const queryString = Object.entries(parameters)\n .map(([k, v]) => `${encodeURIComponent(k)}=${encodeURIComponent(String(v))}`)\n .join(\"&\");\n const getUrl = queryString ? `${baseUrl}?${queryString}` : baseUrl;\n\n const isJsonContentType = contentType?.toLowerCase().includes(\"application/json\");\n\n let curlExample: string;\n if (upperMethod === \"GET\" || upperMethod === \"HEAD\") {\n curlExample = `curl \"${getUrl}\"`;\n } else {\n const curlParams = isJsonContentType\n ? (() => {\n const jsonBody = Object.entries(parameters).reduce(\n (acc, [k, v]) => {\n acc[k] = v;\n return acc;\n },\n {} as Record<string, unknown>,\n );\n const escapedJson = JSON.stringify(jsonBody).replace(/'/g, \"'\\\\''\");\n return `-H 'Content-Type: application/json' -d '${escapedJson}'`;\n })()\n : Object.entries(parameters)\n .map(([k, v]) => {\n const escapedValue = String(v).replace(/'/g, \"'\\\\''\");\n return `-d '${k}=${escapedValue}'`;\n })\n .join(\" \");\n curlExample = `curl -X ${upperMethod} \"${baseUrl}\" ${curlParams}`.trim();\n }\n\n return {\n skillName,\n description: `Call the ${method.toUpperCase()} ${endpoint} endpoint on ${host}`,\n endpoint,\n method: method.toUpperCase(),\n parameters: paramSchema,\n sampleResponse: sampleResponseStr,\n curlExample,\n };\n }\n\n // ---------------------------------------------------------------------------\n // Private helpers\n // ---------------------------------------------------------------------------\n\n private parseCaptureOutput(output: string, siteUrl: string, durationMs: number): ApitapCaptureResult {\n const trimmed = output.trim();\n if (!trimmed) {\n return {\n sessionId: \"\",\n siteUrl,\n endpoints: [],\n durationMs,\n error: \"apitap returned empty output\",\n };\n }\n\n try {\n const parsed = JSON.parse(trimmed) as Record<string, unknown>;\n const sessionId = typeof parsed.sessionId === \"string\" ? parsed.sessionId : randomSessionId();\n const rawEndpoints = Array.isArray(parsed.endpoints)\n ? (parsed.endpoints as unknown[]).slice(0, this.cfg.maxEndpointsPerSession)\n : [];\n\n const endpoints: RawCapturedEndpoint[] = rawEndpoints\n .filter((e): e is Record<string, unknown> => e !== null && typeof e === \"object\")\n .map((e) => ({\n endpoint: typeof e.endpoint === \"string\" ? e.endpoint : \"/\",\n method: typeof e.method === \"string\" ? e.method.toUpperCase() : \"GET\",\n parameters:\n e.parameters && typeof e.parameters === \"object\" && !Array.isArray(e.parameters)\n ? (e.parameters as Record<string, unknown>)\n : {},\n sampleResponse: e.sampleResponse,\n contentType: typeof e.contentType === \"string\" ? e.contentType : \"application/json\",\n }));\n\n return { sessionId, siteUrl, endpoints, durationMs };\n } catch {\n // Not valid JSON — apitap may have printed non-JSON output\n return {\n sessionId: \"\",\n siteUrl,\n endpoints: [],\n durationMs,\n error: `apitap output could not be parsed as JSON. Raw output: ${trimmed.slice(0, 200)}`,\n };\n }\n }\n}\n\nfunction randomSessionId(): string {\n return `session-${Date.now()}-${randomBytes(3).toString(\"hex\")}`;\n}\n"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"audit-health-exit-info.js","names":[],"sources":["../../services/audit-health-exit-info.ts"],"sourcesContent":["export type AuditHealthExitReason =\n | \"ok\"\n | \"warnings\"\n | \"errors\"\n | \"strict_warnings\"\n | \"strict_errors\"\n | \"strict_failed\";\n\nexport type AuditHealthExitInfo = {\n exitCode: 0 | 2;\n exitReason: AuditHealthExitReason;\n warningCount: number;\n errorCount: number;\n strictFailureReason?: string;\n};\n\nexport function buildAuditHealthExitInfo(input: {\n strict: boolean;\n warningCount: number;\n errorCount: number;\n ok?: boolean;\n status?: string;\n}): AuditHealthExitInfo {\n const warningCount = Math.max(0, input.warningCount || 0);\n const errorCount = Math.max(0, input.errorCount || 0);\n const strict = input.strict === true;\n\n const looksDegraded = input.ok === false || (input.status != null && input.status !== \"ok\");\n const strictFailed = strict && (warningCount > 0 || errorCount > 0 || looksDegraded);\n\n if (strictFailed) {\n const strictFailureReason = (() => {\n if (errorCount > 0 && warningCount > 0) {\n return `${errorCount} error(s) and ${warningCount} warning(s) present and --strict was set`;\n }\n if (errorCount > 0) return `${errorCount} error(s) present and --strict was set`;\n if (warningCount > 0) return `${warningCount} warning(s) present and --strict was set`;\n return \"--strict was set and the report was not ok\";\n })();\n return {\n exitCode: 2,\n exitReason: errorCount > 0 ? \"strict_errors\" : warningCount > 0 ? \"strict_warnings\" : \"strict_failed\",\n warningCount,\n errorCount,\n strictFailureReason,\n };\n }\n\n return {\n exitCode: 0,\n exitReason: errorCount > 0 ? \"errors\" : warningCount > 0 ? \"warnings\" : \"ok\",\n warningCount,\n errorCount,\n };\n}\n"],"mappings":";AAgBA,SAAgB,yBAAyB,OAMjB;CACtB,MAAM,eAAe,KAAK,IAAI,GAAG,MAAM,gBAAgB,EAAE;CACzD,MAAM,aAAa,KAAK,IAAI,GAAG,MAAM,cAAc,EAAE;CACrD,MAAM,SAAS,MAAM,WAAW;CAEhC,MAAM,gBAAgB,MAAM,OAAO,SAAU,MAAM,UAAU,QAAQ,MAAM,WAAW;CAGtF,IAFqB,WAAW,eAAe,KAAK,aAAa,KAAK,gBAEpD;EAChB,MAAM,6BAA6B;GACjC,IAAI,aAAa,KAAK,eAAe,GACnC,OAAO,GAAG,WAAW,gBAAgB,aAAa;GAEpD,IAAI,aAAa,GAAG,OAAO,GAAG,WAAW;GACzC,IAAI,eAAe,GAAG,OAAO,GAAG,aAAa;GAC7C,OAAO;MACL;EACJ,OAAO;GACL,UAAU;GACV,YAAY,aAAa,IAAI,kBAAkB,eAAe,IAAI,oBAAoB;GACtF;GACA;GACA;GACD;;CAGH,OAAO;EACL,UAAU;EACV,YAAY,aAAa,IAAI,WAAW,eAAe,IAAI,aAAa;EACxE;EACA;EACD"}
1
+ {"version":3,"file":"audit-health-exit-info.js","names":[],"sources":["../../services/audit-health-exit-info.ts"],"sourcesContent":["export type AuditHealthExitReason =\n | \"ok\"\n | \"warnings\"\n | \"errors\"\n | \"strict_warnings\"\n | \"strict_errors\"\n | \"strict_failed\";\n\nexport type AuditHealthExitInfo = {\n exitCode: 0 | 2;\n exitReason: AuditHealthExitReason;\n warningCount: number;\n errorCount: number;\n strictFailureReason?: string;\n};\n\nexport function buildAuditHealthExitInfo(input: {\n strict: boolean;\n warningCount: number;\n errorCount: number;\n ok?: boolean;\n status?: string;\n}): AuditHealthExitInfo {\n const warningCount = Math.max(0, input.warningCount || 0);\n const errorCount = Math.max(0, input.errorCount || 0);\n const strict = input.strict === true;\n\n const looksDegraded = input.ok === false || (input.status != null && input.status !== \"ok\");\n const strictFailed = strict && (warningCount > 0 || errorCount > 0 || looksDegraded);\n\n if (strictFailed) {\n const strictFailureReason = (() => {\n if (errorCount > 0 && warningCount > 0) {\n return `${errorCount} error(s) and ${warningCount} warning(s) present and --strict was set`;\n }\n if (errorCount > 0) return `${errorCount} error(s) present and --strict was set`;\n if (warningCount > 0) return `${warningCount} warning(s) present and --strict was set`;\n return \"--strict was set and the report was not ok\";\n })();\n return {\n exitCode: 2,\n exitReason: errorCount > 0 ? \"strict_errors\" : warningCount > 0 ? \"strict_warnings\" : \"strict_failed\",\n warningCount,\n errorCount,\n strictFailureReason,\n };\n }\n\n return {\n exitCode: 0,\n exitReason: errorCount > 0 ? \"errors\" : warningCount > 0 ? \"warnings\" : \"ok\",\n warningCount,\n errorCount,\n };\n}\n"],"mappings":";AAgBA,SAAgB,yBAAyB,OAMjB;CACtB,MAAM,eAAe,KAAK,IAAI,GAAG,MAAM,gBAAgB,CAAC;CACxD,MAAM,aAAa,KAAK,IAAI,GAAG,MAAM,cAAc,CAAC;CACpD,MAAM,SAAS,MAAM,WAAW;CAEhC,MAAM,gBAAgB,MAAM,OAAO,SAAU,MAAM,UAAU,QAAQ,MAAM,WAAW;CAGtF,IAFqB,WAAW,eAAe,KAAK,aAAa,KAAK,gBAEpD;EAChB,MAAM,6BAA6B;GACjC,IAAI,aAAa,KAAK,eAAe,GACnC,OAAO,GAAG,WAAW,gBAAgB,aAAa;GAEpD,IAAI,aAAa,GAAG,OAAO,GAAG,WAAW;GACzC,IAAI,eAAe,GAAG,OAAO,GAAG,aAAa;GAC7C,OAAO;EACT,GAAG;EACH,OAAO;GACL,UAAU;GACV,YAAY,aAAa,IAAI,kBAAkB,eAAe,IAAI,oBAAoB;GACtF;GACA;GACA;EACF;CACF;CAEA,OAAO;EACL,UAAU;EACV,YAAY,aAAa,IAAI,WAAW,eAAe,IAAI,aAAa;EACxE;EACA;CACF;AACF"}
@@ -1 +1 @@
1
- {"version":3,"file":"audit-health-json.js","names":[],"sources":["../../services/audit-health-json.ts"],"sourcesContent":["export type ExtractedAuditHealthJson =\n | { kind: \"ok\"; value: Record<string, unknown> }\n | { kind: \"not_found\" }\n | { kind: \"parse_error\" };\n\nexport function extractAuditHealthJsonFromLog(logContent: string): ExtractedAuditHealthJson {\n // Attempt to locate the audit-health JSON payload in a mixed cron log. We avoid regex-only\n // extraction because nested JSON contains braces and newlines.\n const candidates = scanJsonObjects(logContent);\n const likelyAuditHealth = /\"schemaVersion\"\\s*:\\s*1/.test(logContent) && /\"activeFacts\"\\s*:/.test(logContent);\n if (candidates.length === 0) return likelyAuditHealth ? { kind: \"parse_error\" } : { kind: \"not_found\" };\n\n for (const raw of candidates) {\n try {\n const parsed = JSON.parse(raw) as Record<string, unknown>;\n if (isAuditHealthReport(parsed)) return { kind: \"ok\", value: parsed };\n } catch {\n // Keep scanning; a later JSON block might be the actual audit-health payload.\n }\n }\n\n return likelyAuditHealth ? { kind: \"parse_error\" } : { kind: \"not_found\" };\n}\n\nfunction isAuditHealthReport(value: unknown): value is Record<string, unknown> {\n return (\n value != null &&\n typeof value === \"object\" &&\n (value as { schemaVersion?: unknown }).schemaVersion === 1 &&\n typeof (value as { activeFacts?: unknown }).activeFacts === \"number\" &&\n Array.isArray((value as { warnings?: unknown }).warnings) &&\n Array.isArray((value as { errors?: unknown }).errors)\n );\n}\n\nfunction scanJsonObjects(text: string): string[] {\n const out: string[] = [];\n for (let i = 0; i < text.length; i++) {\n if (text[i] !== \"{\") continue;\n const end = findJsonObjectEnd(text, i);\n if (end == null) continue;\n out.push(text.slice(i, end + 1));\n i = end;\n }\n return out;\n}\n\nfunction findJsonObjectEnd(text: string, startIdx: number): number | null {\n let depth = 0;\n let inString = false;\n let escaped = false;\n for (let i = startIdx; i < text.length; i++) {\n const ch = text[i];\n if (inString) {\n if (escaped) {\n escaped = false;\n continue;\n }\n if (ch === \"\\\\\") {\n escaped = true;\n continue;\n }\n if (ch === '\"') {\n inString = false;\n }\n continue;\n }\n if (ch === '\"') {\n inString = true;\n continue;\n }\n if (ch === \"{\") depth++;\n if (ch === \"}\") {\n depth--;\n if (depth === 0) return i;\n if (depth < 0) return null;\n }\n }\n return null;\n}\n"],"mappings":";AAKA,SAAgB,8BAA8B,YAA8C;CAG1F,MAAM,aAAa,gBAAgB,WAAW;CAC9C,MAAM,oBAAoB,0BAA0B,KAAK,WAAW,IAAI,oBAAoB,KAAK,WAAW;CAC5G,IAAI,WAAW,WAAW,GAAG,OAAO,oBAAoB,EAAE,MAAM,eAAe,GAAG,EAAE,MAAM,aAAa;CAEvG,KAAK,MAAM,OAAO,YAChB,IAAI;EACF,MAAM,SAAS,KAAK,MAAM,IAAI;EAC9B,IAAI,oBAAoB,OAAO,EAAE,OAAO;GAAE,MAAM;GAAM,OAAO;GAAQ;SAC/D;CAKV,OAAO,oBAAoB,EAAE,MAAM,eAAe,GAAG,EAAE,MAAM,aAAa;;AAG5E,SAAS,oBAAoB,OAAkD;CAC7E,OACE,SAAS,QACT,OAAO,UAAU,YAChB,MAAsC,kBAAkB,KACzD,OAAQ,MAAoC,gBAAgB,YAC5D,MAAM,QAAS,MAAiC,SAAS,IACzD,MAAM,QAAS,MAA+B,OAAO;;AAIzD,SAAS,gBAAgB,MAAwB;CAC/C,MAAM,MAAgB,EAAE;CACxB,KAAK,IAAI,IAAI,GAAG,IAAI,KAAK,QAAQ,KAAK;EACpC,IAAI,KAAK,OAAO,KAAK;EACrB,MAAM,MAAM,kBAAkB,MAAM,EAAE;EACtC,IAAI,OAAO,MAAM;EACjB,IAAI,KAAK,KAAK,MAAM,GAAG,MAAM,EAAE,CAAC;EAChC,IAAI;;CAEN,OAAO;;AAGT,SAAS,kBAAkB,MAAc,UAAiC;CACxE,IAAI,QAAQ;CACZ,IAAI,WAAW;CACf,IAAI,UAAU;CACd,KAAK,IAAI,IAAI,UAAU,IAAI,KAAK,QAAQ,KAAK;EAC3C,MAAM,KAAK,KAAK;EAChB,IAAI,UAAU;GACZ,IAAI,SAAS;IACX,UAAU;IACV;;GAEF,IAAI,OAAO,MAAM;IACf,UAAU;IACV;;GAEF,IAAI,OAAO,MACT,WAAW;GAEb;;EAEF,IAAI,OAAO,MAAK;GACd,WAAW;GACX;;EAEF,IAAI,OAAO,KAAK;EAChB,IAAI,OAAO,KAAK;GACd;GACA,IAAI,UAAU,GAAG,OAAO;GACxB,IAAI,QAAQ,GAAG,OAAO;;;CAG1B,OAAO"}
1
+ {"version":3,"file":"audit-health-json.js","names":[],"sources":["../../services/audit-health-json.ts"],"sourcesContent":["export type ExtractedAuditHealthJson =\n | { kind: \"ok\"; value: Record<string, unknown> }\n | { kind: \"not_found\" }\n | { kind: \"parse_error\" };\n\nexport function extractAuditHealthJsonFromLog(logContent: string): ExtractedAuditHealthJson {\n // Attempt to locate the audit-health JSON payload in a mixed cron log. We avoid regex-only\n // extraction because nested JSON contains braces and newlines.\n const candidates = scanJsonObjects(logContent);\n const likelyAuditHealth = /\"schemaVersion\"\\s*:\\s*1/.test(logContent) && /\"activeFacts\"\\s*:/.test(logContent);\n if (candidates.length === 0) return likelyAuditHealth ? { kind: \"parse_error\" } : { kind: \"not_found\" };\n\n for (const raw of candidates) {\n try {\n const parsed = JSON.parse(raw) as Record<string, unknown>;\n if (isAuditHealthReport(parsed)) return { kind: \"ok\", value: parsed };\n } catch {\n // Keep scanning; a later JSON block might be the actual audit-health payload.\n }\n }\n\n return likelyAuditHealth ? { kind: \"parse_error\" } : { kind: \"not_found\" };\n}\n\nfunction isAuditHealthReport(value: unknown): value is Record<string, unknown> {\n return (\n value != null &&\n typeof value === \"object\" &&\n (value as { schemaVersion?: unknown }).schemaVersion === 1 &&\n typeof (value as { activeFacts?: unknown }).activeFacts === \"number\" &&\n Array.isArray((value as { warnings?: unknown }).warnings) &&\n Array.isArray((value as { errors?: unknown }).errors)\n );\n}\n\nfunction scanJsonObjects(text: string): string[] {\n const out: string[] = [];\n for (let i = 0; i < text.length; i++) {\n if (text[i] !== \"{\") continue;\n const end = findJsonObjectEnd(text, i);\n if (end == null) continue;\n out.push(text.slice(i, end + 1));\n i = end;\n }\n return out;\n}\n\nfunction findJsonObjectEnd(text: string, startIdx: number): number | null {\n let depth = 0;\n let inString = false;\n let escaped = false;\n for (let i = startIdx; i < text.length; i++) {\n const ch = text[i];\n if (inString) {\n if (escaped) {\n escaped = false;\n continue;\n }\n if (ch === \"\\\\\") {\n escaped = true;\n continue;\n }\n if (ch === '\"') {\n inString = false;\n }\n continue;\n }\n if (ch === '\"') {\n inString = true;\n continue;\n }\n if (ch === \"{\") depth++;\n if (ch === \"}\") {\n depth--;\n if (depth === 0) return i;\n if (depth < 0) return null;\n }\n }\n return null;\n}\n"],"mappings":";AAKA,SAAgB,8BAA8B,YAA8C;CAG1F,MAAM,aAAa,gBAAgB,UAAU;CAC7C,MAAM,oBAAoB,0BAA0B,KAAK,UAAU,KAAK,oBAAoB,KAAK,UAAU;CAC3G,IAAI,WAAW,WAAW,GAAG,OAAO,oBAAoB,EAAE,MAAM,cAAc,IAAI,EAAE,MAAM,YAAY;CAEtG,KAAK,MAAM,OAAO,YAChB,IAAI;EACF,MAAM,SAAS,KAAK,MAAM,GAAG;EAC7B,IAAI,oBAAoB,MAAM,GAAG,OAAO;GAAE,MAAM;GAAM,OAAO;EAAO;CACtE,QAAQ,CAER;CAGF,OAAO,oBAAoB,EAAE,MAAM,cAAc,IAAI,EAAE,MAAM,YAAY;AAC3E;AAEA,SAAS,oBAAoB,OAAkD;CAC7E,OACE,SAAS,QACT,OAAO,UAAU,YAChB,MAAsC,kBAAkB,KACzD,OAAQ,MAAoC,gBAAgB,YAC5D,MAAM,QAAS,MAAiC,QAAQ,KACxD,MAAM,QAAS,MAA+B,MAAM;AAExD;AAEA,SAAS,gBAAgB,MAAwB;CAC/C,MAAM,MAAgB,CAAC;CACvB,KAAK,IAAI,IAAI,GAAG,IAAI,KAAK,QAAQ,KAAK;EACpC,IAAI,KAAK,OAAO,KAAK;EACrB,MAAM,MAAM,kBAAkB,MAAM,CAAC;EACrC,IAAI,OAAO,MAAM;EACjB,IAAI,KAAK,KAAK,MAAM,GAAG,MAAM,CAAC,CAAC;EAC/B,IAAI;CACN;CACA,OAAO;AACT;AAEA,SAAS,kBAAkB,MAAc,UAAiC;CACxE,IAAI,QAAQ;CACZ,IAAI,WAAW;CACf,IAAI,UAAU;CACd,KAAK,IAAI,IAAI,UAAU,IAAI,KAAK,QAAQ,KAAK;EAC3C,MAAM,KAAK,KAAK;EAChB,IAAI,UAAU;GACZ,IAAI,SAAS;IACX,UAAU;IACV;GACF;GACA,IAAI,OAAO,MAAM;IACf,UAAU;IACV;GACF;GACA,IAAI,OAAO,MACT,WAAW;GAEb;EACF;EACA,IAAI,OAAO,MAAK;GACd,WAAW;GACX;EACF;EACA,IAAI,OAAO,KAAK;EAChB,IAAI,OAAO,KAAK;GACd;GACA,IAAI,UAAU,GAAG,OAAO;GACxB,IAAI,QAAQ,GAAG,OAAO;EACxB;CACF;CACA,OAAO;AACT"}
@@ -1 +1 @@
1
- {"version":3,"file":"auth-failure-detect.js","names":[],"sources":["../../services/auth-failure-detect.ts"],"sourcesContent":["/**\n * Authentication Failure Detection Service\n * Auto-Recall on Authentication Failures\n *\n * Detects authentication failures in tool results and extracts target identifiers\n * (hostname, IP, URL domain, service name) for memory recall.\n */\n\nexport type AuthFailurePattern = {\n regex: RegExp;\n type: \"ssh\" | \"http\" | \"api\" | \"generic\";\n hint: string;\n};\n\n/** Predefined auth failure patterns for common protocols */\nexport const DEFAULT_AUTH_FAILURE_PATTERNS: AuthFailurePattern[] = [\n // SSH patterns\n { regex: /Permission denied/i, type: \"ssh\", hint: \"SSH permission denied\" },\n { regex: /Authentication failed/i, type: \"ssh\", hint: \"SSH authentication failed\" },\n { regex: /publickey,password/i, type: \"ssh\", hint: \"SSH auth methods exhausted\" },\n { regex: /Host key verification failed/i, type: \"ssh\", hint: \"SSH host key verification failed\" },\n\n // HTTP patterns\n { regex: /\\b401\\b/, type: \"http\", hint: \"HTTP 401 Unauthorized\" },\n { regex: /\\b403\\b/, type: \"http\", hint: \"HTTP 403 Forbidden\" },\n { regex: /Unauthorized/i, type: \"http\", hint: \"HTTP Unauthorized\" },\n { regex: /Forbidden/i, type: \"http\", hint: \"HTTP Forbidden\" },\n\n // API patterns\n { regex: /Invalid API key/i, type: \"api\", hint: \"Invalid API key\" },\n { regex: /token expired/i, type: \"api\", hint: \"Token expired\" },\n { regex: /token invalid/i, type: \"api\", hint: \"Token invalid\" },\n { regex: /invalid_auth/i, type: \"api\", hint: \"Invalid authentication\" },\n { regex: /authentication.*(required|failed)/i, type: \"api\", hint: \"Authentication required/failed\" },\n { regex: /invalid.*(credentials|token|key)/i, type: \"api\", hint: \"Invalid credentials\" },\n];\n\ntype AuthFailureDetection = {\n detected: boolean;\n type?: \"ssh\" | \"http\" | \"api\" | \"generic\";\n hint?: string;\n target?: string; // hostname, IP, URL domain, service name\n // Note: originalText removed for security - could leak credential values from error messages\n};\n\n/**\n * Extract target identifier from tool result context.\n * Looks for:\n * - Hostnames/IPs (SSH, API endpoints)\n * - URLs and domains (HTTP APIs)\n * - Service names mentioned in context\n */\nexport function extractTarget(text: string, type: \"ssh\" | \"http\" | \"api\" | \"generic\"): string | undefined {\n // Try IP address first (highest priority) - this should match across all types\n // Note: Regex is intentionally loose (matches 999.999.999.999) for simplicity;\n // invalid IPs will fail at connection time, not here\n const ipMatch = text.match(/\\b(\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}\\.\\d{1,3})\\b/);\n if (ipMatch) return ipMatch[1];\n\n // Try URL/domain patterns (for http/api types)\n if (type === \"http\" || type === \"api\") {\n const urlMatch = text.match(/https?:\\/\\/([\\w.-]+)/i);\n if (urlMatch) return urlMatch[1];\n\n // Try domain-like patterns without protocol\n const domainMatch = text.match(/(?:to|from|at|@)\\s+([\\w.-]+\\.\\w{2,})/i);\n if (domainMatch) return domainMatch[1];\n }\n\n // Try SSH patterns: user@host\n if (type === \"ssh\") {\n const sshUserHostMatch = text.match(/(?:ssh\\s+)?[\\w.-]+@([\\w.-]+)/i);\n if (sshUserHostMatch?.[1]) return sshUserHostMatch[1];\n }\n\n // Try to find hostname-like strings (at least 3 chars, contains dot or dash)\n const hostnameMatch = text.match(/\\b([\\w-]{3,}(?:\\.[\\w-]+)+)\\b/);\n if (hostnameMatch) return hostnameMatch[1];\n\n // Try to find service names mentioned after \"to/from/at/for\" (excluding common words)\n const serviceMatch = text.match(/(?:to|from|at|for)\\s+[\"']?([\\w-]{4,})[\"']?/i);\n if (serviceMatch?.[1]) {\n const serviceName = serviceMatch[1].toLowerCase();\n // Filter out common words that aren't service names\n const commonWords = [\"the\", \"this\", \"that\", \"with\", \"your\", \"from\", \"failed\", \"authentication\", \"connection\"];\n if (!commonWords.includes(serviceName)) {\n return serviceName;\n }\n }\n\n return undefined;\n}\n\n/**\n * Detect authentication failures in tool result text.\n * Returns detection info including type, hint, and extracted target.\n */\nexport function detectAuthFailure(\n text: string,\n patterns: AuthFailurePattern[] = DEFAULT_AUTH_FAILURE_PATTERNS,\n): AuthFailureDetection {\n if (!text || text.length < 10) {\n return { detected: false };\n }\n\n for (const pattern of patterns) {\n if (pattern.regex.test(text)) {\n const target = extractTarget(text, pattern.type);\n return {\n detected: true,\n type: pattern.type,\n hint: pattern.hint,\n target,\n // originalText removed for security (could leak credential values in error messages)\n };\n }\n }\n\n return { detected: false };\n}\n\n/**\n * Build search query for memory recall based on detected auth failure.\n * Combines target identifier with credential-related terms.\n */\nexport function buildCredentialQuery(detection: AuthFailureDetection): string | null {\n if (!detection.detected || !detection.target) return null;\n\n const target = detection.target;\n const terms = [\"credential\", \"password\", \"token\", \"key\", \"auth\"];\n\n // Build query with target + credential terms\n return `${target} ${terms.join(\" \")}`;\n}\n\n/**\n * Format system hint for injection into agent context.\n * Returns a user-friendly message pointing to relevant credentials in memory.\n *\n * SECURITY: Does not include fact.text to prevent credential leaks.\n * Shows only safe metadata (entity, category, key).\n */\nexport function formatCredentialHint(\n detection: AuthFailureDetection,\n facts: Array<{ text: string; category: string; entity?: string | null; key?: string | null }>,\n): string {\n if (facts.length === 0) return \"\";\n\n const target = detection.target || \"this service\";\n const lines: string[] = [`💡 Memory has credentials for ${target}:`];\n\n for (let i = 0; i < Math.min(facts.length, 3); i++) {\n const f = facts[i];\n // Show only metadata to prevent credential leaks\n const parts: string[] = [];\n if (f.entity) parts.push(`entity: ${f.entity}`);\n if (f.key) parts.push(`key: ${f.key}`);\n if (f.category && f.category !== \"technical\") parts.push(`[${f.category}]`);\n\n const metadata = parts.length > 0 ? parts.join(\", \") : \"stored credential\";\n lines.push(` ${i + 1}. ${metadata}`);\n }\n\n return lines.join(\"\\n\");\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"auth-failure-detect.js","names":[],"sources":["../../services/auth-failure-detect.ts"],"sourcesContent":["/**\n * Authentication Failure Detection Service\n * Auto-Recall on Authentication Failures\n *\n * Detects authentication failures in tool results and extracts target identifiers\n * (hostname, IP, URL domain, service name) for memory recall.\n */\n\nexport type AuthFailurePattern = {\n regex: RegExp;\n type: \"ssh\" | \"http\" | \"api\" | \"generic\";\n hint: string;\n};\n\n/** Predefined auth failure patterns for common protocols */\nexport const DEFAULT_AUTH_FAILURE_PATTERNS: AuthFailurePattern[] = [\n // SSH patterns\n { regex: /Permission denied/i, type: \"ssh\", hint: \"SSH permission denied\" },\n { regex: /Authentication failed/i, type: \"ssh\", hint: \"SSH authentication failed\" },\n { regex: /publickey,password/i, type: \"ssh\", hint: \"SSH auth methods exhausted\" },\n { regex: /Host key verification failed/i, type: \"ssh\", hint: \"SSH host key verification failed\" },\n\n // HTTP patterns\n { regex: /\\b401\\b/, type: \"http\", hint: \"HTTP 401 Unauthorized\" },\n { regex: /\\b403\\b/, type: \"http\", hint: \"HTTP 403 Forbidden\" },\n { regex: /Unauthorized/i, type: \"http\", hint: \"HTTP Unauthorized\" },\n { regex: /Forbidden/i, type: \"http\", hint: \"HTTP Forbidden\" },\n\n // API patterns\n { regex: /Invalid API key/i, type: \"api\", hint: \"Invalid API key\" },\n { regex: /token expired/i, type: \"api\", hint: \"Token expired\" },\n { regex: /token invalid/i, type: \"api\", hint: \"Token invalid\" },\n { regex: /invalid_auth/i, type: \"api\", hint: \"Invalid authentication\" },\n { regex: /authentication.*(required|failed)/i, type: \"api\", hint: \"Authentication required/failed\" },\n { regex: /invalid.*(credentials|token|key)/i, type: \"api\", hint: \"Invalid credentials\" },\n];\n\ntype AuthFailureDetection = {\n detected: boolean;\n type?: \"ssh\" | \"http\" | \"api\" | \"generic\";\n hint?: string;\n target?: string; // hostname, IP, URL domain, service name\n // Note: originalText removed for security - could leak credential values from error messages\n};\n\n/**\n * Extract target identifier from tool result context.\n * Looks for:\n * - Hostnames/IPs (SSH, API endpoints)\n * - URLs and domains (HTTP APIs)\n * - Service names mentioned in context\n */\nexport function extractTarget(text: string, type: \"ssh\" | \"http\" | \"api\" | \"generic\"): string | undefined {\n // Try IP address first (highest priority) - this should match across all types\n // Note: Regex is intentionally loose (matches 999.999.999.999) for simplicity;\n // invalid IPs will fail at connection time, not here\n const ipMatch = text.match(/\\b(\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}\\.\\d{1,3})\\b/);\n if (ipMatch) return ipMatch[1];\n\n // Try URL/domain patterns (for http/api types)\n if (type === \"http\" || type === \"api\") {\n const urlMatch = text.match(/https?:\\/\\/([\\w.-]+)/i);\n if (urlMatch) return urlMatch[1];\n\n // Try domain-like patterns without protocol\n const domainMatch = text.match(/(?:to|from|at|@)\\s+([\\w.-]+\\.\\w{2,})/i);\n if (domainMatch) return domainMatch[1];\n }\n\n // Try SSH patterns: user@host\n if (type === \"ssh\") {\n const sshUserHostMatch = text.match(/(?:ssh\\s+)?[\\w.-]+@([\\w.-]+)/i);\n if (sshUserHostMatch?.[1]) return sshUserHostMatch[1];\n }\n\n // Try to find hostname-like strings (at least 3 chars, contains dot or dash)\n const hostnameMatch = text.match(/\\b([\\w-]{3,}(?:\\.[\\w-]+)+)\\b/);\n if (hostnameMatch) return hostnameMatch[1];\n\n // Try to find service names mentioned after \"to/from/at/for\" (excluding common words)\n const serviceMatch = text.match(/(?:to|from|at|for)\\s+[\"']?([\\w-]{4,})[\"']?/i);\n if (serviceMatch?.[1]) {\n const serviceName = serviceMatch[1].toLowerCase();\n // Filter out common words that aren't service names\n const commonWords = [\"the\", \"this\", \"that\", \"with\", \"your\", \"from\", \"failed\", \"authentication\", \"connection\"];\n if (!commonWords.includes(serviceName)) {\n return serviceName;\n }\n }\n\n return undefined;\n}\n\n/**\n * Detect authentication failures in tool result text.\n * Returns detection info including type, hint, and extracted target.\n */\nexport function detectAuthFailure(\n text: string,\n patterns: AuthFailurePattern[] = DEFAULT_AUTH_FAILURE_PATTERNS,\n): AuthFailureDetection {\n if (!text || text.length < 10) {\n return { detected: false };\n }\n\n for (const pattern of patterns) {\n if (pattern.regex.test(text)) {\n const target = extractTarget(text, pattern.type);\n return {\n detected: true,\n type: pattern.type,\n hint: pattern.hint,\n target,\n // originalText removed for security (could leak credential values in error messages)\n };\n }\n }\n\n return { detected: false };\n}\n\n/**\n * Build search query for memory recall based on detected auth failure.\n * Combines target identifier with credential-related terms.\n */\nexport function buildCredentialQuery(detection: AuthFailureDetection): string | null {\n if (!detection.detected || !detection.target) return null;\n\n const target = detection.target;\n const terms = [\"credential\", \"password\", \"token\", \"key\", \"auth\"];\n\n // Build query with target + credential terms\n return `${target} ${terms.join(\" \")}`;\n}\n\n/**\n * Format system hint for injection into agent context.\n * Returns a user-friendly message pointing to relevant credentials in memory.\n *\n * SECURITY: Does not include fact.text to prevent credential leaks.\n * Shows only safe metadata (entity, category, key).\n */\nexport function formatCredentialHint(\n detection: AuthFailureDetection,\n facts: Array<{ text: string; category: string; entity?: string | null; key?: string | null }>,\n): string {\n if (facts.length === 0) return \"\";\n\n const target = detection.target || \"this service\";\n const lines: string[] = [`💡 Memory has credentials for ${target}:`];\n\n for (let i = 0; i < Math.min(facts.length, 3); i++) {\n const f = facts[i];\n // Show only metadata to prevent credential leaks\n const parts: string[] = [];\n if (f.entity) parts.push(`entity: ${f.entity}`);\n if (f.key) parts.push(`key: ${f.key}`);\n if (f.category && f.category !== \"technical\") parts.push(`[${f.category}]`);\n\n const metadata = parts.length > 0 ? parts.join(\", \") : \"stored credential\";\n lines.push(` ${i + 1}. ${metadata}`);\n }\n\n return lines.join(\"\\n\");\n}\n"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"auto-capture.js","names":[],"sources":["../../services/auto-capture.ts"],"sourcesContent":["/**\n * Auto-capture Filters\n *\n * Extracted from index.ts - functions for determining if content should be auto-captured\n */\n\nimport { CREDENTIAL_NOTES_MAX_CHARS } from \"../utils/constants.js\";\nimport { getMemoryTriggerRegexes } from \"../utils/language-keywords.js\";\nimport { truncateText } from \"../utils/text.js\";\nimport { validateAndNormalizeServiceName, validateCredentialValue } from \"./credential-validation.js\";\n\n/** Memory triggers: English + dynamic languages from .language-keywords.json (see build-languages command). */\nexport function getMemoryTriggers(): RegExp[] {\n return getMemoryTriggerRegexes();\n}\n\nexport const SENSITIVE_PATTERNS = [\n /password/i,\n /api.?key/i,\n /secret/i,\n /token\\s+is/i, // More specific: only \"token is\" not just \"token\" (used by consolidation.ts to exclude from consolidation)\n /bearer/i, // Bearer tokens (no word boundary to catch bearer_token, etc.)\n /authorization/i, // Authorization headers (no word boundary to catch authorization_header, etc.)\n /credentials?/i, // Credentials keyword (no word boundary to catch credentials_file, etc.)\n /\\bssn\\b/i,\n /credit.?card/i,\n /AKIA[0-9A-Z]{16}/, // AWS access keys\n /-----BEGIN [^-]*PRIVATE KEY-----/, // Private key headers (RSA, EC, etc.) — non-greedy label (issue #856)\n /:\\/\\/[^\\s:@]+:[^\\s@]+@[^\\s/]+/, // Connection strings with embedded passwords (e.g., mongodb://user:pass@host) - Note: usernames with colons will fail\n];\n\n/** Patterns for capture filtering - uses broader /token/i instead of /token\\s+is/i for security */\nexport const CAPTURE_FILTER_PATTERNS = [...SENSITIVE_PATTERNS.slice(0, 3), /token/i, ...SENSITIVE_PATTERNS.slice(4)];\n\n/** Patterns that suggest a credential value - for auto-detect prompt to store */\nconst CREDENTIAL_PATTERNS: Array<{ regex: RegExp; type: string; hint: string }> = [\n { regex: /Bearer\\s+eyJ[A-Za-z0-9_-]+\\.eyJ[A-Za-z0-9_-]+\\.[A-Za-z0-9_-]+/i, type: \"bearer\", hint: \"Bearer/JWT token\" },\n { regex: /sk-[A-Za-z0-9]{20,}/, type: \"api_key\", hint: \"OpenAI-style API key (sk-...)\" },\n { regex: /ghp_[A-Za-z0-9]{36}/, type: \"api_key\", hint: \"GitHub personal access token\" },\n { regex: /gho_[A-Za-z0-9]{36}/, type: \"api_key\", hint: \"GitHub OAuth token\" },\n { regex: /xox[baprs]-[A-Za-z0-9-]{10,}/, type: \"token\", hint: \"Slack token\" },\n { regex: /ssh\\s+[\\w@.-]+\\s+[\\w@.-]+/i, type: \"ssh\", hint: \"SSH connection string\" },\n {\n regex: /[\\w.-]+@[\\w.-]+\\.\\w+.*(?:password|passwd|token|key)\\s*[:=]\\s*\\S+/i,\n type: \"password\",\n hint: \"Credentials with host/email\",\n },\n];\n\nexport function detectCredentialPatterns(text: string): Array<{ type: string; hint: string }> {\n const found: Array<{ type: string; hint: string }> = [];\n const seen = new Set<string>();\n for (const { regex, type, hint } of CREDENTIAL_PATTERNS) {\n if (regex.test(text) && !seen.has(hint)) {\n seen.add(hint);\n found.push({ type, hint });\n }\n }\n return found;\n}\n\n/** Maximum number of characters accepted for any single extracted credential value. */\nconst MAX_SECRET_VALUE_LENGTH = 8192;\n\n/** First credential-like match in text; used to extract secret for vault. */\nexport function extractCredentialMatch(text: string): { type: string; secretValue: string } | null {\n for (const { regex, type } of CREDENTIAL_PATTERNS) {\n const match = regex.exec(text);\n if (match) {\n const secretValue = match[0]\n .replace(/^Bearer\\s+/i, \"\")\n .trim()\n .slice(0, MAX_SECRET_VALUE_LENGTH);\n if (secretValue.length >= 8) return { type, secretValue };\n }\n }\n return null;\n}\n\n/** True if content should be treated as a credential (store in vault when enabled, else in memory). */\nexport function isCredentialLike(\n text: string,\n entity?: string | null,\n key?: string | null,\n value?: string | null,\n): boolean {\n if ((entity ?? \"\").toLowerCase() === \"credentials\") return true;\n const k = (key ?? \"\").toLowerCase();\n const e = (entity ?? \"\").toLowerCase();\n if ([\"api_key\", \"password\", \"token\", \"secret\", \"bearer\"].some((x) => k.includes(x) || e.includes(x))) return true;\n if (value && value.length >= 8 && /^(eyJ|sk-|ghp_|gho_|xox[baprs]-)/i.test(value)) return true;\n return CREDENTIAL_PATTERNS.some((p) => p.regex.test(text)) || CAPTURE_FILTER_PATTERNS.some((r) => r.test(text));\n}\n\nexport const VAULT_POINTER_PREFIX = \"vault:\";\n\n/** Options for tryParseCredentialForVault (e.g. from config). */\nexport type TryParseCredentialOptions = {\n /** When true, return null unless extractCredentialMatch found a pattern (reject value-only). */\n requirePatternMatch?: boolean;\n};\n\n/** Parse into vault entry when vault is enabled. Returns null if not credential-like or cannot derive service/secret. */\nexport function tryParseCredentialForVault(\n text: string,\n entity?: string | null,\n key?: string | null,\n value?: string | null,\n options?: TryParseCredentialOptions,\n): {\n service: string;\n type: \"token\" | \"password\" | \"api_key\" | \"ssh\" | \"bearer\" | \"other\";\n secretValue: string;\n url?: string;\n notes?: string;\n} | null {\n if (!isCredentialLike(text, entity, key, value)) return null;\n const match = extractCredentialMatch(text);\n if (options?.requirePatternMatch && !match) return null;\n // Prefer the secret extracted from a regex pattern match over the structured value param.\n // This prevents a narrative/metadata value (e.g. \"requires explicit login via the gateway\")\n // from being stored as a credential even when a real token was also detected in the text.\n // If a match exists, it IS the credential — use its value.\n // Only fall back to the structured value param when no pattern matched.\n const secretFromMatch = match?.secretValue ?? null;\n const secretFromParam = value && value.length >= 8 ? value : null;\n const secretValue = secretFromMatch ?? secretFromParam ?? null;\n if (!secretValue) return null;\n const typeFromPattern = (match?.type ?? \"other\") as \"token\" | \"password\" | \"api_key\" | \"ssh\" | \"bearer\" | \"other\";\n // hasPatternMatch is only true when secretValue ITSELF came from the regex match.\n // When value came from the structured param, hasPatternMatch=false so that\n // natural-language and path checks always run, preventing narrative text from being\n // stored even when the surrounding text contained a known credential pattern.\n const hasPatternMatch = secretFromMatch !== null;\n const valueValidation = validateCredentialValue(secretValue, typeFromPattern, hasPatternMatch);\n if (!valueValidation.ok) return null;\n\n const service =\n (entity?.toLowerCase() === \"credentials\" ? key : null) ||\n key ||\n (entity && entity.toLowerCase() !== \"credentials\" ? entity : null) ||\n inferServiceFromText(text) ||\n \"imported\";\n const serviceSlug = validateAndNormalizeServiceName(service);\n if (serviceSlug === null) return null;\n\n return {\n service: serviceSlug,\n type: typeFromPattern,\n secretValue,\n notes: text.length <= CREDENTIAL_NOTES_MAX_CHARS ? text : truncateText(text, CREDENTIAL_NOTES_MAX_CHARS - 3, \"...\"),\n };\n}\n\nexport function inferServiceFromText(text: string): string {\n const lower = text.toLowerCase();\n if (/home\\s*assistant|ha\\s*token|hass/i.test(lower)) return \"home-assistant\";\n if (/unifi|ubiquiti/i.test(lower)) return \"unifi\";\n if (/github|ghp_|gho_/i.test(lower)) return \"github\";\n if (/openai|sk-proj/i.test(lower)) return \"openai\";\n if (/twilio/i.test(lower)) return \"twilio\";\n if (/duckdns/i.test(lower)) return \"duckdns\";\n if (/slack|xox[baprs]/i.test(lower)) return \"slack\";\n return \"imported\";\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"auto-capture.js","names":[],"sources":["../../services/auto-capture.ts"],"sourcesContent":["/**\n * Auto-capture Filters\n *\n * Extracted from index.ts - functions for determining if content should be auto-captured\n */\n\nimport { CREDENTIAL_NOTES_MAX_CHARS } from \"../utils/constants.js\";\nimport { getMemoryTriggerRegexes } from \"../utils/language-keywords.js\";\nimport { truncateText } from \"../utils/text.js\";\nimport { validateAndNormalizeServiceName, validateCredentialValue } from \"./credential-validation.js\";\n\n/** Memory triggers: English + dynamic languages from .language-keywords.json (see build-languages command). */\nexport function getMemoryTriggers(): RegExp[] {\n return getMemoryTriggerRegexes();\n}\n\nexport const SENSITIVE_PATTERNS = [\n /password/i,\n /api.?key/i,\n /secret/i,\n /token\\s+is/i, // More specific: only \"token is\" not just \"token\" (used by consolidation.ts to exclude from consolidation)\n /bearer/i, // Bearer tokens (no word boundary to catch bearer_token, etc.)\n /authorization/i, // Authorization headers (no word boundary to catch authorization_header, etc.)\n /credentials?/i, // Credentials keyword (no word boundary to catch credentials_file, etc.)\n /\\bssn\\b/i,\n /credit.?card/i,\n /AKIA[0-9A-Z]{16}/, // AWS access keys\n /-----BEGIN [^-]*PRIVATE KEY-----/, // Private key headers (RSA, EC, etc.) — non-greedy label (issue #856)\n /:\\/\\/[^\\s:@]+:[^\\s@]+@[^\\s/]+/, // Connection strings with embedded passwords (e.g., mongodb://user:pass@host) - Note: usernames with colons will fail\n];\n\n/** Patterns for capture filtering - uses broader /token/i instead of /token\\s+is/i for security */\nexport const CAPTURE_FILTER_PATTERNS = [...SENSITIVE_PATTERNS.slice(0, 3), /token/i, ...SENSITIVE_PATTERNS.slice(4)];\n\n/** Patterns that suggest a credential value - for auto-detect prompt to store */\nconst CREDENTIAL_PATTERNS: Array<{ regex: RegExp; type: string; hint: string }> = [\n { regex: /Bearer\\s+eyJ[A-Za-z0-9_-]+\\.eyJ[A-Za-z0-9_-]+\\.[A-Za-z0-9_-]+/i, type: \"bearer\", hint: \"Bearer/JWT token\" },\n { regex: /sk-[A-Za-z0-9]{20,}/, type: \"api_key\", hint: \"OpenAI-style API key (sk-...)\" },\n { regex: /ghp_[A-Za-z0-9]{36}/, type: \"api_key\", hint: \"GitHub personal access token\" },\n { regex: /gho_[A-Za-z0-9]{36}/, type: \"api_key\", hint: \"GitHub OAuth token\" },\n { regex: /xox[baprs]-[A-Za-z0-9-]{10,}/, type: \"token\", hint: \"Slack token\" },\n { regex: /ssh\\s+[\\w@.-]+\\s+[\\w@.-]+/i, type: \"ssh\", hint: \"SSH connection string\" },\n {\n regex: /[\\w.-]+@[\\w.-]+\\.\\w+.*(?:password|passwd|token|key)\\s*[:=]\\s*\\S+/i,\n type: \"password\",\n hint: \"Credentials with host/email\",\n },\n];\n\nexport function detectCredentialPatterns(text: string): Array<{ type: string; hint: string }> {\n const found: Array<{ type: string; hint: string }> = [];\n const seen = new Set<string>();\n for (const { regex, type, hint } of CREDENTIAL_PATTERNS) {\n if (regex.test(text) && !seen.has(hint)) {\n seen.add(hint);\n found.push({ type, hint });\n }\n }\n return found;\n}\n\n/** Maximum number of characters accepted for any single extracted credential value. */\nconst MAX_SECRET_VALUE_LENGTH = 8192;\n\n/** First credential-like match in text; used to extract secret for vault. */\nexport function extractCredentialMatch(text: string): { type: string; secretValue: string } | null {\n for (const { regex, type } of CREDENTIAL_PATTERNS) {\n const match = regex.exec(text);\n if (match) {\n const secretValue = match[0]\n .replace(/^Bearer\\s+/i, \"\")\n .trim()\n .slice(0, MAX_SECRET_VALUE_LENGTH);\n if (secretValue.length >= 8) return { type, secretValue };\n }\n }\n return null;\n}\n\n/** True if content should be treated as a credential (store in vault when enabled, else in memory). */\nexport function isCredentialLike(\n text: string,\n entity?: string | null,\n key?: string | null,\n value?: string | null,\n): boolean {\n if ((entity ?? \"\").toLowerCase() === \"credentials\") return true;\n const k = (key ?? \"\").toLowerCase();\n const e = (entity ?? \"\").toLowerCase();\n if ([\"api_key\", \"password\", \"token\", \"secret\", \"bearer\"].some((x) => k.includes(x) || e.includes(x))) return true;\n if (value && value.length >= 8 && /^(eyJ|sk-|ghp_|gho_|xox[baprs]-)/i.test(value)) return true;\n return CREDENTIAL_PATTERNS.some((p) => p.regex.test(text)) || CAPTURE_FILTER_PATTERNS.some((r) => r.test(text));\n}\n\nexport const VAULT_POINTER_PREFIX = \"vault:\";\n\n/** Options for tryParseCredentialForVault (e.g. from config). */\nexport type TryParseCredentialOptions = {\n /** When true, return null unless extractCredentialMatch found a pattern (reject value-only). */\n requirePatternMatch?: boolean;\n};\n\n/** Parse into vault entry when vault is enabled. Returns null if not credential-like or cannot derive service/secret. */\nexport function tryParseCredentialForVault(\n text: string,\n entity?: string | null,\n key?: string | null,\n value?: string | null,\n options?: TryParseCredentialOptions,\n): {\n service: string;\n type: \"token\" | \"password\" | \"api_key\" | \"ssh\" | \"bearer\" | \"other\";\n secretValue: string;\n url?: string;\n notes?: string;\n} | null {\n if (!isCredentialLike(text, entity, key, value)) return null;\n const match = extractCredentialMatch(text);\n if (options?.requirePatternMatch && !match) return null;\n // Prefer the secret extracted from a regex pattern match over the structured value param.\n // This prevents a narrative/metadata value (e.g. \"requires explicit login via the gateway\")\n // from being stored as a credential even when a real token was also detected in the text.\n // If a match exists, it IS the credential — use its value.\n // Only fall back to the structured value param when no pattern matched.\n const secretFromMatch = match?.secretValue ?? null;\n const secretFromParam = value && value.length >= 8 ? value : null;\n const secretValue = secretFromMatch ?? secretFromParam ?? null;\n if (!secretValue) return null;\n const typeFromPattern = (match?.type ?? \"other\") as \"token\" | \"password\" | \"api_key\" | \"ssh\" | \"bearer\" | \"other\";\n // hasPatternMatch is only true when secretValue ITSELF came from the regex match.\n // When value came from the structured param, hasPatternMatch=false so that\n // natural-language and path checks always run, preventing narrative text from being\n // stored even when the surrounding text contained a known credential pattern.\n const hasPatternMatch = secretFromMatch !== null;\n const valueValidation = validateCredentialValue(secretValue, typeFromPattern, hasPatternMatch);\n if (!valueValidation.ok) return null;\n\n const service =\n (entity?.toLowerCase() === \"credentials\" ? key : null) ||\n key ||\n (entity && entity.toLowerCase() !== \"credentials\" ? entity : null) ||\n inferServiceFromText(text) ||\n \"imported\";\n const serviceSlug = validateAndNormalizeServiceName(service);\n if (serviceSlug === null) return null;\n\n return {\n service: serviceSlug,\n type: typeFromPattern,\n secretValue,\n notes: text.length <= CREDENTIAL_NOTES_MAX_CHARS ? text : truncateText(text, CREDENTIAL_NOTES_MAX_CHARS - 3, \"...\"),\n };\n}\n\nexport function inferServiceFromText(text: string): string {\n const lower = text.toLowerCase();\n if (/home\\s*assistant|ha\\s*token|hass/i.test(lower)) return \"home-assistant\";\n if (/unifi|ubiquiti/i.test(lower)) return \"unifi\";\n if (/github|ghp_|gho_/i.test(lower)) return \"github\";\n if (/openai|sk-proj/i.test(lower)) return \"openai\";\n if (/twilio/i.test(lower)) return \"twilio\";\n if (/duckdns/i.test(lower)) return \"duckdns\";\n if (/slack|xox[baprs]/i.test(lower)) return \"slack\";\n return \"imported\";\n}\n"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"auto-classifier.js","names":[],"sources":["../../services/auto-classifier.ts"],"sourcesContent":["/**\n * Auto-Classifier Service\n *\n * LLM-based classification of \"other\" facts into proper categories.\n * Includes optional category discovery (grouping by free-form labels).\n */\n\nimport { mkdir, readFile, writeFile } from \"node:fs/promises\";\nimport { dirname } from \"node:path\";\nimport type OpenAI from \"openai\";\nimport type { FactsDB } from \"../backends/facts-db.js\";\nimport { getMemoryCategories, isValidCategory } from \"../config.js\";\nimport { tryParseFirstJsonArray } from \"../utils/llm-json-array.js\";\nimport { fillPrompt, loadPrompt } from \"../utils/prompt-loader.js\";\nimport { is404Like, is500Like, isConnectionErrorLike, isOllamaOOM } from \"./chat.js\";\nimport { capturePluginError } from \"./error-reporter.js\";\nimport { chatCompletionTokenParams } from \"./model-capabilities.js\";\n\n/** Minimum \"other\" facts before category discovery kicks in. */\nconst MIN_OTHER_FOR_DISCOVERY = 15;\n/** Batch size for discovery prompts (leave room for JSON array of labels). */\nconst DISCOVERY_BATCH_SIZE = 25;\n/** Default cooldown between discovery runs in hours. */\nconst DEFAULT_DISCOVERY_INTERVAL_HOURS = 72;\n\n/**\n * Normalize a free-form label to a valid category slug: lowercase, alphanumeric + underscore.\n * Returns empty string if result would be \"other\" or invalid.\n */\nfunction normalizeSuggestedLabel(s: string): string {\n const t = s\n .toLowerCase()\n .trim()\n .replace(/\\s+/g, \" \")\n .replace(/[^a-z0-9_-]/g, \"_\")\n .replace(/_+/g, \"_\")\n .replace(/^_|_$/g, \"\");\n return t && t !== \"other\" && t.length <= 40 ? t : \"\";\n}\n\n/**\n * Derive the sidecar path for last-discovery timestamp from the categories file path.\n * Exported for testing.\n */\nexport function getLastDiscoveryPath(discoveredCategoriesPath: string): string {\n return `${discoveredCategoriesPath.replace(/\\.json$/i, \"\")}.last-run.json`;\n}\n\n/**\n * Read the last-discovery timestamp from the sidecar file.\n * Returns null if the file doesn't exist or is malformed.\n */\nasync function readLastDiscoveryTimestamp(lastRunPath: string): Promise<number | null> {\n try {\n const raw = await readFile(lastRunPath, \"utf-8\");\n const parsed: unknown = JSON.parse(raw);\n if (\n typeof parsed === \"object\" &&\n parsed !== null &&\n typeof (parsed as Record<string, unknown>).lastRunAt === \"number\"\n ) {\n return (parsed as { lastRunAt: number }).lastRunAt;\n }\n return null;\n } catch {\n return null;\n }\n}\n\n/**\n * Write the last-discovery timestamp to the sidecar file.\n */\nasync function writeLastDiscoveryTimestamp(lastRunPath: string, timestampMs: number): Promise<void> {\n await mkdir(dirname(lastRunPath), { recursive: true });\n await writeFile(lastRunPath, JSON.stringify({ lastRunAt: timestampMs }, null, 2), \"utf-8\");\n}\n\n/**\n * Ask the LLM to group \"other\" facts by topic (free-form labels). Labels with at least\n * minFactsForNewCategory facts become new categories; we do not tell the LLM the threshold.\n * Returns proposed category labels and persists them to discoveredCategoriesPath.\n * Discovery is intentionally non-mutating: it never writes unconfigured free-form\n * labels into facts.category, which keeps dashboard/API category validation stable.\n */\nasync function discoverCategoriesFromOther(\n factsDb: FactsDB,\n openai: OpenAI,\n config: {\n model: string;\n batchSize: number;\n suggestCategories?: boolean;\n minFactsForNewCategory?: number;\n discoveryIntervalHours?: number;\n },\n logger: { info: (msg: string) => void; warn: (msg: string) => void },\n discoveredCategoriesPath: string,\n): Promise<string[]> {\n if (config.suggestCategories !== true) return [];\n const minForNew = config.minFactsForNewCategory ?? 10;\n const others = factsDb.getByCategory(\"other\");\n if (others.length < MIN_OTHER_FOR_DISCOVERY) return [];\n\n // Cooldown check: skip LLM if last discovery ran within discoveryIntervalHours\n const intervalHours = config.discoveryIntervalHours ?? DEFAULT_DISCOVERY_INTERVAL_HOURS;\n if (intervalHours > 0) {\n const lastRunPath = getLastDiscoveryPath(discoveredCategoriesPath);\n const lastRunAt = await readLastDiscoveryTimestamp(lastRunPath);\n if (lastRunAt !== null) {\n const elapsedMs = Date.now() - lastRunAt;\n const intervalMs = intervalHours * 60 * 60 * 1000;\n if (elapsedMs < intervalMs) {\n const remainingHours = ((intervalMs - elapsedMs) / (60 * 60 * 1000)).toFixed(1);\n logger.info(\n `memory-hybrid: category discovery skipped — last run ${(elapsedMs / (60 * 60 * 1000)).toFixed(1)}h ago, cooldown ${intervalHours}h (${remainingHours}h remaining)`,\n );\n return [];\n }\n }\n }\n\n logger.info(`memory-hybrid: category discovery on ${others.length} \"other\" facts (min ${minForNew} per label)`);\n\n const existingCategories = new Set(getMemoryCategories());\n const labelToIds = new Map<string, string[]>();\n let anyBatchSucceeded = false;\n\n for (let i = 0; i < others.length; i += DISCOVERY_BATCH_SIZE) {\n const batch = others.slice(i, i + DISCOVERY_BATCH_SIZE);\n const factLines = batch.map((f, idx) => `${idx + 1}. ${f.text.slice(0, 280)}`).join(\"\\n\");\n const prompt = fillPrompt(loadPrompt(\"category-discovery\"), { facts: factLines });\n\n try {\n const { withLLMRetry } = await import(\"./chat.js\");\n const resp = await withLLMRetry(\n () =>\n openai.chat.completions.create({\n model: config.model,\n messages: [{ role: \"user\", content: prompt }],\n temperature: 0,\n ...chatCompletionTokenParams(config.model, batch.length * 24),\n }),\n { maxRetries: 2 },\n );\n const content = resp.choices?.[0]?.message?.content?.trim() || \"[]\";\n const labels = tryParseFirstJsonArray(content);\n if (!labels) continue;\n anyBatchSucceeded = true;\n for (let j = 0; j < Math.min(labels.length, batch.length); j++) {\n const raw = typeof labels[j] === \"string\" ? (labels[j] as string) : \"\";\n const label = normalizeSuggestedLabel(raw);\n if (!label) continue;\n if (!labelToIds.has(label)) labelToIds.set(label, []);\n labelToIds.get(label)?.push(batch[j].id);\n }\n } catch (err) {\n const discErr = err instanceof Error ? err : new Error(String(err));\n // Suppress GlitchTip for transient/expected LLM failures (OOM, 5xx, 404, timeout).\n const isTransient =\n isOllamaOOM(discErr) ||\n is500Like(discErr) ||\n is404Like(discErr) ||\n /timed out|llm request timeout|request was aborted/i.test(discErr.message) ||\n isConnectionErrorLike(discErr);\n if (!isTransient) {\n capturePluginError(discErr, {\n subsystem: \"auto-classifier\",\n operation: \"category-discovery-batch\",\n });\n }\n logger.warn(`memory-hybrid: category discovery batch failed: ${err}`);\n }\n if (i + DISCOVERY_BATCH_SIZE < others.length) await new Promise((r) => setTimeout(r, 400));\n }\n\n const proposedCategoryNames: string[] = [];\n for (const [label, ids] of labelToIds) {\n if (existingCategories.has(label)) continue;\n if (ids.length < minForNew) continue;\n proposedCategoryNames.push(label);\n }\n\n // Write last-run timestamp only if at least one batch succeeded (even if no new categories were created).\n // This prevents the LLM from firing again on the next cron tick when categories are settled.\n // If all batches failed, don't write the timestamp so discovery is retried on the next run.\n if (intervalHours > 0 && anyBatchSucceeded) {\n const lastRunPath = getLastDiscoveryPath(discoveredCategoriesPath);\n await writeLastDiscoveryTimestamp(lastRunPath, Date.now());\n }\n\n if (proposedCategoryNames.length === 0) return [];\n\n logger.info(\n `memory-hybrid: proposed ${proposedCategoryNames.length} new categories: ${proposedCategoryNames.join(\", \")} (${proposedCategoryNames.reduce((acc, c) => acc + (labelToIds.get(c)?.length ?? 0), 0)} matching facts left as other)`,\n );\n\n await mkdir(dirname(discoveredCategoriesPath), { recursive: true });\n let existingList: string[] = [];\n try {\n existingList = JSON.parse(await readFile(discoveredCategoriesPath, \"utf-8\")) as string[];\n } catch (err) {\n capturePluginError(err as Error, {\n operation: \"read-discovered-categories\",\n severity: \"info\",\n subsystem: \"classifier\",\n });\n // file doesn't exist yet\n }\n const merged = [...new Set([...existingList, ...proposedCategoryNames])];\n await writeFile(discoveredCategoriesPath, JSON.stringify(merged, null, 2), \"utf-8\");\n\n return proposedCategoryNames;\n}\n\n/**\n * Classify a batch of \"other\" facts into proper categories using a cheap LLM.\n *\n * Returns:\n * - `results`: factId → valid category (matched the configured allowlist).\n * - `suggestions`: factId → normalized free-form label the LLM proposed but that is not\n * in the configured set (#1188). Callers tag these facts with\n * `category-suggested:<label>` instead of silently dropping the LLM's output, so\n * `categories propose` can surface them.\n * - `success`: whether the LLM call itself succeeded.\n */\nasync function classifyBatch(\n openai: OpenAI,\n model: string,\n facts: { id: string; text: string }[],\n categories: readonly string[],\n): Promise<{ results: Map<string, string>; suggestions: Map<string, string>; success: boolean }> {\n const catList = categories.filter((c) => c !== \"other\").join(\", \");\n const factLines = facts.map((f, i) => `${i + 1}. ${f.text.slice(0, 300)}`).join(\"\\n\");\n\n const prompt = `You are a memory classifier. Categorize each fact into exactly one category.\n\nAvailable categories: ${catList}\nUse \"other\" ONLY if no category fits at all.\n\nFacts to classify:\n${factLines}\n\nRespond with ONLY a JSON array of category strings, one per fact, in order. Example: [\"fact\",\"entity\",\"preference\"]`;\n\n try {\n const { withLLMRetry } = await import(\"./chat.js\");\n const resp = await withLLMRetry(\n () =>\n openai.chat.completions.create({\n model,\n messages: [{ role: \"user\", content: prompt }],\n temperature: 0,\n ...chatCompletionTokenParams(model, facts.length * 20),\n }),\n { maxRetries: 2 },\n );\n\n const content = resp.choices?.[0]?.message?.content?.trim() || \"[]\";\n const parsed = tryParseFirstJsonArray(content);\n if (!parsed) return { results: new Map(), suggestions: new Map(), success: false };\n\n const results: string[] = parsed as string[];\n const map = new Map<string, string>();\n const suggestions = new Map<string, string>();\n\n for (let i = 0; i < Math.min(results.length, facts.length); i++) {\n const cat = results[i]?.toLowerCase()?.trim();\n if (!cat || cat === \"other\") continue;\n if (isValidCategory(cat)) {\n map.set(facts[i].id, cat);\n } else {\n const normalized = normalizeSuggestedLabel(cat);\n if (normalized) suggestions.set(facts[i].id, normalized);\n }\n }\n return { results: map, suggestions, success: true };\n } catch (err) {\n const classifyErr = err instanceof Error ? err : new Error(String(err));\n // Suppress GlitchTip for transient/expected LLM failures (OOM, 5xx, 404, timeout).\n // OOM: Ollama model too large for available RAM — not a bug, degrade gracefully.\n const isTransient =\n isOllamaOOM(classifyErr) ||\n is500Like(classifyErr) ||\n is404Like(classifyErr) ||\n /timed out|llm request timeout|request was aborted/i.test(classifyErr.message) ||\n isConnectionErrorLike(classifyErr);\n if (!isTransient) {\n capturePluginError(classifyErr, {\n operation: \"classify-batch\",\n severity: \"info\",\n subsystem: \"classifier\",\n });\n }\n return { results: new Map(), suggestions: new Map(), success: false };\n }\n}\n\n/** Progress reporter for batch CLI commands (optional). */\ntype ClassifyProgressReporter = { update: (current: number) => void; done: () => void };\n\n/** Progress bar when stdout is TTY; otherwise no-op (caller can use sink.log). */\nfunction createProgressReporter(\n sink: { log: (s: string) => void },\n total: number,\n label: string,\n): { update: (current: number, extra?: string) => void; done: () => void } {\n const isTTY = typeof process.stdout?.isTTY === \"boolean\" && process.stdout.isTTY;\n const width = 40;\n let lastLen = 0;\n let lastPct = -1;\n return {\n update(current: number, extra?: string) {\n if (total <= 0) return;\n const pct = Math.min(100, Math.floor((current / total) * 100));\n if (!isTTY) {\n // Only log at milestones to avoid spam in non-TTY (25%, 50%, 75%, 100%)\n if (pct === 100 || (pct >= 25 && pct !== lastPct && pct % 25 === 0)) {\n sink.log(`${label}: ${pct}% (${current}/${total})${extra ? ` ${extra}` : \"\"}`);\n lastPct = pct;\n }\n return;\n }\n const filled = Math.min(width, Math.round((current / total) * width));\n const arrow = filled < width ? 1 : 0;\n const dots = Math.max(0, width - filled - arrow);\n const bar = \"=\".repeat(filled) + \">\".repeat(arrow) + \".\".repeat(dots);\n const line = `${label}: ${pct}% [${bar}] ${current}/${total}${extra ? ` (${extra})` : \"\"}`;\n process.stdout.write(`\\r${line}${\" \".repeat(Math.max(0, lastLen - line.length))}`);\n lastLen = line.length;\n },\n done() {\n if (isTTY && lastLen > 0) process.stdout.write(\"\\n\");\n },\n };\n}\n\n/**\n * Run classify command: optional discovery, then batch classify with limit and dryRun.\n * Used by CLI; returns counts and optional breakdown for printing.\n */\nasync function runClassifyForCli(\n factsDb: FactsDB,\n openai: OpenAI,\n config: {\n model?: string;\n batchSize: number;\n suggestCategories?: boolean;\n minFactsForNewCategory?: number;\n discoveryIntervalHours?: number;\n },\n opts: { dryRun: boolean; limit: number; model?: string },\n discoveredPath: string,\n logger: { info: (msg: string) => void; warn: (msg: string) => void },\n progressReporter?: ClassifyProgressReporter,\n): Promise<{ reclassified: number; total: number; breakdown?: Record<string, number> }> {\n // Callers (CLI, plugin-service) must pass opts.model or config.model so one is set\n const classifyModel = opts.model ?? config.model;\n if (!classifyModel) throw new Error(\"classify model required: set autoClassify.model or pass --model\");\n const categories = getMemoryCategories();\n let others = factsDb.getByCategory(\"other\").slice(0, opts.limit);\n if (others.length === 0) {\n return { reclassified: 0, total: 0 };\n }\n\n if (!opts.dryRun && config.suggestCategories && others.length >= MIN_OTHER_FOR_DISCOVERY) {\n await discoverCategoriesFromOther(factsDb, openai, { ...config, model: classifyModel }, logger, discoveredPath);\n others = factsDb.getByCategory(\"other\").slice(0, opts.limit);\n }\n\n const numBatches = Math.ceil(others.length / config.batchSize);\n let reporter = progressReporter;\n if (!reporter && numBatches > 0) {\n const sink = { log: (m: string) => logger.info(m) };\n reporter = createProgressReporter(sink, numBatches, \"Classifying\");\n }\n let totalReclassified = 0;\n let batchIndex = 0;\n for (let i = 0; i < others.length; i += config.batchSize) {\n reporter?.update(batchIndex + 1);\n const batch = others.slice(i, i + config.batchSize).map((e) => ({ id: e.id, text: e.text }));\n const { results, suggestions } = await classifyBatch(openai, classifyModel, batch, categories);\n for (const [id, newCat] of results) {\n if (!opts.dryRun) factsDb.updateCategory(id, newCat);\n totalReclassified++;\n }\n if (!opts.dryRun) {\n for (const [id, label] of suggestions) {\n // #1188: surface invalid-but-meaningful LLM suggestions instead of silently\n // dropping them. Operators can review with `categories propose` and promote\n // via existing `categories remap`.\n factsDb.addTag(id, `category-suggested:${label}`);\n }\n }\n batchIndex++;\n if (i + config.batchSize < others.length) await new Promise((r) => setTimeout(r, 500));\n }\n reporter?.done();\n\n const breakdown = !opts.dryRun ? factsDb.statsBreakdown() : undefined;\n return { reclassified: totalReclassified, total: others.length, breakdown };\n}\n\n/**\n * Run auto-classification on all \"other\" facts. Called on schedule or manually.\n * If opts.discoveredCategoriesPath and config.suggestCategories are set, runs category discovery first\n * (LLM groups \"other\" by free-form label; labels with ≥ minFactsForNewCategory become new categories).\n * Model: use opts.model (e.g. from getDefaultCronModel) or config.model; one must be set.\n */\nasync function runAutoClassify(\n factsDb: FactsDB,\n openai: OpenAI,\n config: {\n model?: string;\n batchSize: number;\n suggestCategories?: boolean;\n minFactsForNewCategory?: number;\n discoveryIntervalHours?: number;\n },\n logger: { info: (msg: string) => void; warn: (msg: string) => void },\n opts?: { discoveredCategoriesPath?: string; model?: string },\n): Promise<{ reclassified: number; suggested: string[] }> {\n const model = opts?.model ?? config.model;\n if (!model) {\n throw new Error(\n \"auto-classify model required: set autoClassify.model or pass opts.model (e.g. from getDefaultCronModel)\",\n );\n }\n const configWithModel = { ...config, model };\n const categories = getMemoryCategories();\n\n // Optionally discover new categories from \"other\" (free-form grouping; threshold not told to LLM)\n if (opts?.discoveredCategoriesPath && config.suggestCategories) {\n await discoverCategoriesFromOther(factsDb, openai, configWithModel, logger, opts.discoveredCategoriesPath);\n }\n\n // Only classify \"other\" facts that haven't been attempted yet (or were re-ingested since last attempt).\n const others = factsDb.getUnattemptedOtherFacts();\n if (others.length === 0) {\n const totalOther = factsDb.getByCategory(\"other\").length;\n if (totalOther > 0) {\n logger.info(\n `memory-hybrid: auto-classify — ${totalOther} \"other\" facts exist but all were already attempted; skipping (new/changed facts will be retried)`,\n );\n }\n return { reclassified: 0, suggested: [] };\n }\n\n const totalOther = factsDb.getByCategory(\"other\").length;\n logger.info(\n `memory-hybrid: auto-classify starting on ${others.length} unattempted \"other\" facts (${totalOther} total \"other\")`,\n );\n\n let totalReclassified = 0;\n\n for (let i = 0; i < others.length; i += config.batchSize) {\n const batch = others.slice(i, i + config.batchSize).map((e) => ({\n id: e.id,\n text: e.text,\n }));\n\n const { results, suggestions, success } = await classifyBatch(openai, model, batch, categories);\n\n const batchIds = batch.map((b) => b.id);\n for (const [id, newCat] of results) {\n factsDb.updateCategory(id, newCat);\n totalReclassified++;\n }\n for (const [id, label] of suggestions) {\n // #1188: tag invalid LLM categories so they surface in `categories propose`\n // instead of being silently dropped. The fact stays in `category=other`.\n factsDb.addTag(id, `category-suggested:${label}`);\n }\n // Only mark classify attempt if the LLM call succeeded\n if (success) {\n factsDb.markClassifyAttempt(batchIds);\n }\n\n if (i + config.batchSize < others.length) {\n await new Promise((r) => setTimeout(r, 500));\n }\n }\n\n logger.info(`memory-hybrid: auto-classify done — reclassified ${totalReclassified}/${others.length} facts`);\n return { reclassified: totalReclassified, suggested: [] };\n}\n\n// ============================================================================\n// Exports\n// ============================================================================\n\nexport { runAutoClassify, runClassifyForCli, normalizeSuggestedLabel };\n"],"mappings":"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"}
1
+ {"version":3,"file":"auto-classifier.js","names":[],"sources":["../../services/auto-classifier.ts"],"sourcesContent":["/**\n * Auto-Classifier Service\n *\n * LLM-based classification of \"other\" facts into proper categories.\n * Includes optional category discovery (grouping by free-form labels).\n */\n\nimport { mkdir, readFile, writeFile } from \"node:fs/promises\";\nimport { dirname } from \"node:path\";\nimport type OpenAI from \"openai\";\nimport type { FactsDB } from \"../backends/facts-db.js\";\nimport { getMemoryCategories, isValidCategory } from \"../config.js\";\nimport { tryParseFirstJsonArray } from \"../utils/llm-json-array.js\";\nimport { fillPrompt, loadPrompt } from \"../utils/prompt-loader.js\";\nimport { is404Like, is500Like, isConnectionErrorLike, isOllamaOOM } from \"./chat.js\";\nimport { capturePluginError } from \"./error-reporter.js\";\nimport { chatCompletionTokenParams } from \"./model-capabilities.js\";\n\n/** Minimum \"other\" facts before category discovery kicks in. */\nconst MIN_OTHER_FOR_DISCOVERY = 15;\n/** Batch size for discovery prompts (leave room for JSON array of labels). */\nconst DISCOVERY_BATCH_SIZE = 25;\n/** Default cooldown between discovery runs in hours. */\nconst DEFAULT_DISCOVERY_INTERVAL_HOURS = 72;\n\n/**\n * Normalize a free-form label to a valid category slug: lowercase, alphanumeric + underscore.\n * Returns empty string if result would be \"other\" or invalid.\n */\nfunction normalizeSuggestedLabel(s: string): string {\n const t = s\n .toLowerCase()\n .trim()\n .replace(/\\s+/g, \" \")\n .replace(/[^a-z0-9_-]/g, \"_\")\n .replace(/_+/g, \"_\")\n .replace(/^_|_$/g, \"\");\n return t && t !== \"other\" && t.length <= 40 ? t : \"\";\n}\n\n/**\n * Derive the sidecar path for last-discovery timestamp from the categories file path.\n * Exported for testing.\n */\nexport function getLastDiscoveryPath(discoveredCategoriesPath: string): string {\n return `${discoveredCategoriesPath.replace(/\\.json$/i, \"\")}.last-run.json`;\n}\n\n/**\n * Read the last-discovery timestamp from the sidecar file.\n * Returns null if the file doesn't exist or is malformed.\n */\nasync function readLastDiscoveryTimestamp(lastRunPath: string): Promise<number | null> {\n try {\n const raw = await readFile(lastRunPath, \"utf-8\");\n const parsed: unknown = JSON.parse(raw);\n if (\n typeof parsed === \"object\" &&\n parsed !== null &&\n typeof (parsed as Record<string, unknown>).lastRunAt === \"number\"\n ) {\n return (parsed as { lastRunAt: number }).lastRunAt;\n }\n return null;\n } catch {\n return null;\n }\n}\n\n/**\n * Write the last-discovery timestamp to the sidecar file.\n */\nasync function writeLastDiscoveryTimestamp(lastRunPath: string, timestampMs: number): Promise<void> {\n await mkdir(dirname(lastRunPath), { recursive: true });\n await writeFile(lastRunPath, JSON.stringify({ lastRunAt: timestampMs }, null, 2), \"utf-8\");\n}\n\n/**\n * Ask the LLM to group \"other\" facts by topic (free-form labels). Labels with at least\n * minFactsForNewCategory facts become new categories; we do not tell the LLM the threshold.\n * Returns proposed category labels and persists them to discoveredCategoriesPath.\n * Discovery is intentionally non-mutating: it never writes unconfigured free-form\n * labels into facts.category, which keeps dashboard/API category validation stable.\n */\nasync function discoverCategoriesFromOther(\n factsDb: FactsDB,\n openai: OpenAI,\n config: {\n model: string;\n batchSize: number;\n suggestCategories?: boolean;\n minFactsForNewCategory?: number;\n discoveryIntervalHours?: number;\n },\n logger: { info: (msg: string) => void; warn: (msg: string) => void },\n discoveredCategoriesPath: string,\n): Promise<string[]> {\n if (config.suggestCategories !== true) return [];\n const minForNew = config.minFactsForNewCategory ?? 10;\n const others = factsDb.getByCategory(\"other\");\n if (others.length < MIN_OTHER_FOR_DISCOVERY) return [];\n\n // Cooldown check: skip LLM if last discovery ran within discoveryIntervalHours\n const intervalHours = config.discoveryIntervalHours ?? DEFAULT_DISCOVERY_INTERVAL_HOURS;\n if (intervalHours > 0) {\n const lastRunPath = getLastDiscoveryPath(discoveredCategoriesPath);\n const lastRunAt = await readLastDiscoveryTimestamp(lastRunPath);\n if (lastRunAt !== null) {\n const elapsedMs = Date.now() - lastRunAt;\n const intervalMs = intervalHours * 60 * 60 * 1000;\n if (elapsedMs < intervalMs) {\n const remainingHours = ((intervalMs - elapsedMs) / (60 * 60 * 1000)).toFixed(1);\n logger.info(\n `memory-hybrid: category discovery skipped — last run ${(elapsedMs / (60 * 60 * 1000)).toFixed(1)}h ago, cooldown ${intervalHours}h (${remainingHours}h remaining)`,\n );\n return [];\n }\n }\n }\n\n logger.info(`memory-hybrid: category discovery on ${others.length} \"other\" facts (min ${minForNew} per label)`);\n\n const existingCategories = new Set(getMemoryCategories());\n const labelToIds = new Map<string, string[]>();\n let anyBatchSucceeded = false;\n\n for (let i = 0; i < others.length; i += DISCOVERY_BATCH_SIZE) {\n const batch = others.slice(i, i + DISCOVERY_BATCH_SIZE);\n const factLines = batch.map((f, idx) => `${idx + 1}. ${f.text.slice(0, 280)}`).join(\"\\n\");\n const prompt = fillPrompt(loadPrompt(\"category-discovery\"), { facts: factLines });\n\n try {\n const { withLLMRetry } = await import(\"./chat.js\");\n const resp = await withLLMRetry(\n () =>\n openai.chat.completions.create({\n model: config.model,\n messages: [{ role: \"user\", content: prompt }],\n temperature: 0,\n ...chatCompletionTokenParams(config.model, batch.length * 24),\n }),\n { maxRetries: 2 },\n );\n const content = resp.choices?.[0]?.message?.content?.trim() || \"[]\";\n const labels = tryParseFirstJsonArray(content);\n if (!labels) continue;\n anyBatchSucceeded = true;\n for (let j = 0; j < Math.min(labels.length, batch.length); j++) {\n const raw = typeof labels[j] === \"string\" ? (labels[j] as string) : \"\";\n const label = normalizeSuggestedLabel(raw);\n if (!label) continue;\n if (!labelToIds.has(label)) labelToIds.set(label, []);\n labelToIds.get(label)?.push(batch[j].id);\n }\n } catch (err) {\n const discErr = err instanceof Error ? err : new Error(String(err));\n // Suppress GlitchTip for transient/expected LLM failures (OOM, 5xx, 404, timeout).\n const isTransient =\n isOllamaOOM(discErr) ||\n is500Like(discErr) ||\n is404Like(discErr) ||\n /timed out|llm request timeout|request was aborted/i.test(discErr.message) ||\n isConnectionErrorLike(discErr);\n if (!isTransient) {\n capturePluginError(discErr, {\n subsystem: \"auto-classifier\",\n operation: \"category-discovery-batch\",\n });\n }\n logger.warn(`memory-hybrid: category discovery batch failed: ${err}`);\n }\n if (i + DISCOVERY_BATCH_SIZE < others.length) await new Promise((r) => setTimeout(r, 400));\n }\n\n const proposedCategoryNames: string[] = [];\n for (const [label, ids] of labelToIds) {\n if (existingCategories.has(label)) continue;\n if (ids.length < minForNew) continue;\n proposedCategoryNames.push(label);\n }\n\n // Write last-run timestamp only if at least one batch succeeded (even if no new categories were created).\n // This prevents the LLM from firing again on the next cron tick when categories are settled.\n // If all batches failed, don't write the timestamp so discovery is retried on the next run.\n if (intervalHours > 0 && anyBatchSucceeded) {\n const lastRunPath = getLastDiscoveryPath(discoveredCategoriesPath);\n await writeLastDiscoveryTimestamp(lastRunPath, Date.now());\n }\n\n if (proposedCategoryNames.length === 0) return [];\n\n logger.info(\n `memory-hybrid: proposed ${proposedCategoryNames.length} new categories: ${proposedCategoryNames.join(\", \")} (${proposedCategoryNames.reduce((acc, c) => acc + (labelToIds.get(c)?.length ?? 0), 0)} matching facts left as other)`,\n );\n\n await mkdir(dirname(discoveredCategoriesPath), { recursive: true });\n let existingList: string[] = [];\n try {\n existingList = JSON.parse(await readFile(discoveredCategoriesPath, \"utf-8\")) as string[];\n } catch (err) {\n capturePluginError(err as Error, {\n operation: \"read-discovered-categories\",\n severity: \"info\",\n subsystem: \"classifier\",\n });\n // file doesn't exist yet\n }\n const merged = [...new Set([...existingList, ...proposedCategoryNames])];\n await writeFile(discoveredCategoriesPath, JSON.stringify(merged, null, 2), \"utf-8\");\n\n return proposedCategoryNames;\n}\n\n/**\n * Classify a batch of \"other\" facts into proper categories using a cheap LLM.\n *\n * Returns:\n * - `results`: factId → valid category (matched the configured allowlist).\n * - `suggestions`: factId → normalized free-form label the LLM proposed but that is not\n * in the configured set (#1188). Callers tag these facts with\n * `category-suggested:<label>` instead of silently dropping the LLM's output, so\n * `categories propose` can surface them.\n * - `success`: whether the LLM call itself succeeded.\n */\nasync function classifyBatch(\n openai: OpenAI,\n model: string,\n facts: { id: string; text: string }[],\n categories: readonly string[],\n): Promise<{ results: Map<string, string>; suggestions: Map<string, string>; success: boolean }> {\n const catList = categories.filter((c) => c !== \"other\").join(\", \");\n const factLines = facts.map((f, i) => `${i + 1}. ${f.text.slice(0, 300)}`).join(\"\\n\");\n\n const prompt = `You are a memory classifier. Categorize each fact into exactly one category.\n\nAvailable categories: ${catList}\nUse \"other\" ONLY if no category fits at all.\n\nFacts to classify:\n${factLines}\n\nRespond with ONLY a JSON array of category strings, one per fact, in order. Example: [\"fact\",\"entity\",\"preference\"]`;\n\n try {\n const { withLLMRetry } = await import(\"./chat.js\");\n const resp = await withLLMRetry(\n () =>\n openai.chat.completions.create({\n model,\n messages: [{ role: \"user\", content: prompt }],\n temperature: 0,\n ...chatCompletionTokenParams(model, facts.length * 20),\n }),\n { maxRetries: 2 },\n );\n\n const content = resp.choices?.[0]?.message?.content?.trim() || \"[]\";\n const parsed = tryParseFirstJsonArray(content);\n if (!parsed) return { results: new Map(), suggestions: new Map(), success: false };\n\n const results: string[] = parsed as string[];\n const map = new Map<string, string>();\n const suggestions = new Map<string, string>();\n\n for (let i = 0; i < Math.min(results.length, facts.length); i++) {\n const cat = results[i]?.toLowerCase()?.trim();\n if (!cat || cat === \"other\") continue;\n if (isValidCategory(cat)) {\n map.set(facts[i].id, cat);\n } else {\n const normalized = normalizeSuggestedLabel(cat);\n if (normalized) suggestions.set(facts[i].id, normalized);\n }\n }\n return { results: map, suggestions, success: true };\n } catch (err) {\n const classifyErr = err instanceof Error ? err : new Error(String(err));\n // Suppress GlitchTip for transient/expected LLM failures (OOM, 5xx, 404, timeout).\n // OOM: Ollama model too large for available RAM — not a bug, degrade gracefully.\n const isTransient =\n isOllamaOOM(classifyErr) ||\n is500Like(classifyErr) ||\n is404Like(classifyErr) ||\n /timed out|llm request timeout|request was aborted/i.test(classifyErr.message) ||\n isConnectionErrorLike(classifyErr);\n if (!isTransient) {\n capturePluginError(classifyErr, {\n operation: \"classify-batch\",\n severity: \"info\",\n subsystem: \"classifier\",\n });\n }\n return { results: new Map(), suggestions: new Map(), success: false };\n }\n}\n\n/** Progress reporter for batch CLI commands (optional). */\ntype ClassifyProgressReporter = { update: (current: number) => void; done: () => void };\n\n/** Progress bar when stdout is TTY; otherwise no-op (caller can use sink.log). */\nfunction createProgressReporter(\n sink: { log: (s: string) => void },\n total: number,\n label: string,\n): { update: (current: number, extra?: string) => void; done: () => void } {\n const isTTY = typeof process.stdout?.isTTY === \"boolean\" && process.stdout.isTTY;\n const width = 40;\n let lastLen = 0;\n let lastPct = -1;\n return {\n update(current: number, extra?: string) {\n if (total <= 0) return;\n const pct = Math.min(100, Math.floor((current / total) * 100));\n if (!isTTY) {\n // Only log at milestones to avoid spam in non-TTY (25%, 50%, 75%, 100%)\n if (pct === 100 || (pct >= 25 && pct !== lastPct && pct % 25 === 0)) {\n sink.log(`${label}: ${pct}% (${current}/${total})${extra ? ` ${extra}` : \"\"}`);\n lastPct = pct;\n }\n return;\n }\n const filled = Math.min(width, Math.round((current / total) * width));\n const arrow = filled < width ? 1 : 0;\n const dots = Math.max(0, width - filled - arrow);\n const bar = \"=\".repeat(filled) + \">\".repeat(arrow) + \".\".repeat(dots);\n const line = `${label}: ${pct}% [${bar}] ${current}/${total}${extra ? ` (${extra})` : \"\"}`;\n process.stdout.write(`\\r${line}${\" \".repeat(Math.max(0, lastLen - line.length))}`);\n lastLen = line.length;\n },\n done() {\n if (isTTY && lastLen > 0) process.stdout.write(\"\\n\");\n },\n };\n}\n\n/**\n * Run classify command: optional discovery, then batch classify with limit and dryRun.\n * Used by CLI; returns counts and optional breakdown for printing.\n */\nasync function runClassifyForCli(\n factsDb: FactsDB,\n openai: OpenAI,\n config: {\n model?: string;\n batchSize: number;\n suggestCategories?: boolean;\n minFactsForNewCategory?: number;\n discoveryIntervalHours?: number;\n },\n opts: { dryRun: boolean; limit: number; model?: string },\n discoveredPath: string,\n logger: { info: (msg: string) => void; warn: (msg: string) => void },\n progressReporter?: ClassifyProgressReporter,\n): Promise<{ reclassified: number; total: number; breakdown?: Record<string, number> }> {\n // Callers (CLI, plugin-service) must pass opts.model or config.model so one is set\n const classifyModel = opts.model ?? config.model;\n if (!classifyModel) throw new Error(\"classify model required: set autoClassify.model or pass --model\");\n const categories = getMemoryCategories();\n let others = factsDb.getByCategory(\"other\").slice(0, opts.limit);\n if (others.length === 0) {\n return { reclassified: 0, total: 0 };\n }\n\n if (!opts.dryRun && config.suggestCategories && others.length >= MIN_OTHER_FOR_DISCOVERY) {\n await discoverCategoriesFromOther(factsDb, openai, { ...config, model: classifyModel }, logger, discoveredPath);\n others = factsDb.getByCategory(\"other\").slice(0, opts.limit);\n }\n\n const numBatches = Math.ceil(others.length / config.batchSize);\n let reporter = progressReporter;\n if (!reporter && numBatches > 0) {\n const sink = { log: (m: string) => logger.info(m) };\n reporter = createProgressReporter(sink, numBatches, \"Classifying\");\n }\n let totalReclassified = 0;\n let batchIndex = 0;\n for (let i = 0; i < others.length; i += config.batchSize) {\n reporter?.update(batchIndex + 1);\n const batch = others.slice(i, i + config.batchSize).map((e) => ({ id: e.id, text: e.text }));\n const { results, suggestions } = await classifyBatch(openai, classifyModel, batch, categories);\n for (const [id, newCat] of results) {\n if (!opts.dryRun) factsDb.updateCategory(id, newCat);\n totalReclassified++;\n }\n if (!opts.dryRun) {\n for (const [id, label] of suggestions) {\n // #1188: surface invalid-but-meaningful LLM suggestions instead of silently\n // dropping them. Operators can review with `categories propose` and promote\n // via existing `categories remap`.\n factsDb.addTag(id, `category-suggested:${label}`);\n }\n }\n batchIndex++;\n if (i + config.batchSize < others.length) await new Promise((r) => setTimeout(r, 500));\n }\n reporter?.done();\n\n const breakdown = !opts.dryRun ? factsDb.statsBreakdown() : undefined;\n return { reclassified: totalReclassified, total: others.length, breakdown };\n}\n\n/**\n * Run auto-classification on all \"other\" facts. Called on schedule or manually.\n * If opts.discoveredCategoriesPath and config.suggestCategories are set, runs category discovery first\n * (LLM groups \"other\" by free-form label; labels with ≥ minFactsForNewCategory become new categories).\n * Model: use opts.model (e.g. from getDefaultCronModel) or config.model; one must be set.\n */\nasync function runAutoClassify(\n factsDb: FactsDB,\n openai: OpenAI,\n config: {\n model?: string;\n batchSize: number;\n suggestCategories?: boolean;\n minFactsForNewCategory?: number;\n discoveryIntervalHours?: number;\n },\n logger: { info: (msg: string) => void; warn: (msg: string) => void },\n opts?: { discoveredCategoriesPath?: string; model?: string },\n): Promise<{ reclassified: number; suggested: string[] }> {\n const model = opts?.model ?? config.model;\n if (!model) {\n throw new Error(\n \"auto-classify model required: set autoClassify.model or pass opts.model (e.g. from getDefaultCronModel)\",\n );\n }\n const configWithModel = { ...config, model };\n const categories = getMemoryCategories();\n\n // Optionally discover new categories from \"other\" (free-form grouping; threshold not told to LLM)\n if (opts?.discoveredCategoriesPath && config.suggestCategories) {\n await discoverCategoriesFromOther(factsDb, openai, configWithModel, logger, opts.discoveredCategoriesPath);\n }\n\n // Only classify \"other\" facts that haven't been attempted yet (or were re-ingested since last attempt).\n const others = factsDb.getUnattemptedOtherFacts();\n if (others.length === 0) {\n const totalOther = factsDb.getByCategory(\"other\").length;\n if (totalOther > 0) {\n logger.info(\n `memory-hybrid: auto-classify — ${totalOther} \"other\" facts exist but all were already attempted; skipping (new/changed facts will be retried)`,\n );\n }\n return { reclassified: 0, suggested: [] };\n }\n\n const totalOther = factsDb.getByCategory(\"other\").length;\n logger.info(\n `memory-hybrid: auto-classify starting on ${others.length} unattempted \"other\" facts (${totalOther} total \"other\")`,\n );\n\n let totalReclassified = 0;\n\n for (let i = 0; i < others.length; i += config.batchSize) {\n const batch = others.slice(i, i + config.batchSize).map((e) => ({\n id: e.id,\n text: e.text,\n }));\n\n const { results, suggestions, success } = await classifyBatch(openai, model, batch, categories);\n\n const batchIds = batch.map((b) => b.id);\n for (const [id, newCat] of results) {\n factsDb.updateCategory(id, newCat);\n totalReclassified++;\n }\n for (const [id, label] of suggestions) {\n // #1188: tag invalid LLM categories so they surface in `categories propose`\n // instead of being silently dropped. The fact stays in `category=other`.\n factsDb.addTag(id, `category-suggested:${label}`);\n }\n // Only mark classify attempt if the LLM call succeeded\n if (success) {\n factsDb.markClassifyAttempt(batchIds);\n }\n\n if (i + config.batchSize < others.length) {\n await new Promise((r) => setTimeout(r, 500));\n }\n }\n\n logger.info(`memory-hybrid: auto-classify done — reclassified ${totalReclassified}/${others.length} facts`);\n return { reclassified: totalReclassified, suggested: [] };\n}\n\n// ============================================================================\n// Exports\n// ============================================================================\n\nexport { runAutoClassify, runClassifyForCli, normalizeSuggestedLabel };\n"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"auto-skills-audit.js","names":[],"sources":["../../services/auto-skills-audit.ts"],"sourcesContent":["/**\n * Audit and quarantine legacy generated auto-skills (issue #1541).\n */\n\nimport { existsSync, mkdirSync, readdirSync, readFileSync, renameSync, statSync } from \"node:fs\";\nimport { join } from \"node:path\";\nimport { MAX_SKILL_FILE_BYTES } from \"../config/skill-size-limits.js\";\nimport { SkillValidator } from \"./skill-validator.js\";\n\nexport type AutoSkillAuditEntry = {\n slug: string;\n skillPath: string;\n skillBytes: number;\n skillLines: number;\n recipeBytes: number | null;\n loadable: boolean;\n transcriptLike: boolean;\n secretLike: boolean;\n injectionLike: boolean;\n validationViolations: string[];\n};\n\nexport type AutoSkillAuditReport = {\n scanned: number;\n oversized: number;\n suspicious: number;\n quarantinable: number;\n entries: AutoSkillAuditEntry[];\n};\n\nconst TRANSCRIPT_PATTERN = /\\*\\*\\w+\\*\\*\\s*\\{|\"tool\"\\s*:|tool_call_id/i;\nconst INJECTION_PATTERN = /\\bignore\\s+(?:previous|system|developer)\\s+instructions\\b/i;\n\nfunction utf8Bytes(path: string): number {\n try {\n return statSync(path).size;\n } catch {\n return 0;\n }\n}\n\nfunction scanSkillDir(skillDir: string, slug: string): AutoSkillAuditEntry | null {\n const skillPath = join(skillDir, \"SKILL.md\");\n if (!existsSync(skillPath)) return null;\n const skillContent = readFileSync(skillPath, \"utf-8\");\n const skillBytes = Buffer.byteLength(skillContent, \"utf8\");\n const skillLines = skillContent.split(/\\r?\\n/).length;\n const recipePath = join(skillDir, \"recipe.json\");\n const recipeBytes = existsSync(recipePath) ? utf8Bytes(recipePath) : null;\n const validator = new SkillValidator();\n const validation = validator.validate(skillContent);\n return {\n slug,\n skillPath,\n skillBytes,\n skillLines,\n recipeBytes,\n loadable: skillBytes <= MAX_SKILL_FILE_BYTES,\n transcriptLike: TRANSCRIPT_PATTERN.test(skillContent),\n secretLike: validation.violations.some((v) => /credential|token|private[- ]?key/i.test(v)),\n injectionLike: INJECTION_PATTERN.test(skillContent),\n validationViolations: validation.violations,\n };\n}\n\nexport function auditAutoSkills(skillsAutoPath: string): AutoSkillAuditReport {\n const entries: AutoSkillAuditEntry[] = [];\n if (!existsSync(skillsAutoPath)) {\n return { scanned: 0, oversized: 0, suspicious: 0, quarantinable: 0, entries };\n }\n for (const name of readdirSync(skillsAutoPath)) {\n if (name.startsWith(\".\")) continue;\n const skillDir = join(skillsAutoPath, name);\n try {\n if (!statSync(skillDir).isDirectory()) continue;\n } catch {\n continue;\n }\n const entry = scanSkillDir(skillDir, name);\n if (entry) entries.push(entry);\n }\n return {\n scanned: entries.length,\n oversized: entries.filter((e) => !e.loadable).length,\n suspicious: entries.filter((e) => e.transcriptLike || e.secretLike || e.injectionLike).length,\n quarantinable: entries.filter((e) => !e.loadable || e.transcriptLike || e.secretLike || e.injectionLike).length,\n entries,\n };\n}\n\nexport type QuarantineResult = {\n quarantined: string[];\n errors: string[];\n};\n\n/**\n * Move unsafe/oversized skills to skills/auto-quarantine/YYYY-MM-DD/<slug>/.\n */\nexport function quarantineAutoSkills(\n skillsAutoPath: string,\n entries: AutoSkillAuditEntry[],\n options?: { onlyOversized?: boolean; onlySuspicious?: boolean },\n): QuarantineResult {\n const date = new Date().toISOString().slice(0, 10);\n const quarantineRoot = join(skillsAutoPath, \"..\", \"auto-quarantine\", date);\n const quarantined: string[] = [];\n const errors: string[] = [];\n mkdirSync(quarantineRoot, { recursive: true });\n\n for (const entry of entries) {\n const shouldMove =\n (!options?.onlyOversized && !options?.onlySuspicious) ||\n (options.onlyOversized && !entry.loadable) ||\n (options.onlySuspicious && (entry.transcriptLike || entry.secretLike || entry.injectionLike));\n if (!shouldMove) continue;\n const srcDir = join(skillsAutoPath, entry.slug);\n const destDir = join(quarantineRoot, entry.slug);\n try {\n if (existsSync(destDir)) {\n errors.push(`${entry.slug}: destination already exists`);\n continue;\n }\n renameSync(srcDir, destDir);\n quarantined.push(entry.slug);\n } catch (err) {\n errors.push(`${entry.slug}: ${err instanceof Error ? err.message : String(err)}`);\n }\n }\n return { quarantined, errors };\n}\n"],"mappings":";;;;;;;;AA8BA,MAAM,qBAAqB;AAC3B,MAAM,oBAAoB;AAE1B,SAAS,UAAU,MAAsB;CACvC,IAAI;EACF,OAAO,SAAS,KAAK,CAAC;SAChB;EACN,OAAO;;;AAIX,SAAS,aAAa,UAAkB,MAA0C;CAChF,MAAM,YAAY,KAAK,UAAU,WAAW;CAC5C,IAAI,CAAC,WAAW,UAAU,EAAE,OAAO;CACnC,MAAM,eAAe,aAAa,WAAW,QAAQ;CACrD,MAAM,aAAa,OAAO,WAAW,cAAc,OAAO;CAC1D,MAAM,aAAa,aAAa,MAAM,QAAQ,CAAC;CAC/C,MAAM,aAAa,KAAK,UAAU,cAAc;CAChD,MAAM,cAAc,WAAW,WAAW,GAAG,UAAU,WAAW,GAAG;CAErE,MAAM,aAAa,IADG,gBACM,CAAC,SAAS,aAAa;CACnD,OAAO;EACL;EACA;EACA;EACA;EACA;EACA,UAAU,cAAc;EACxB,gBAAgB,mBAAmB,KAAK,aAAa;EACrD,YAAY,WAAW,WAAW,MAAM,MAAM,oCAAoC,KAAK,EAAE,CAAC;EAC1F,eAAe,kBAAkB,KAAK,aAAa;EACnD,sBAAsB,WAAW;EAClC;;AAGH,SAAgB,gBAAgB,gBAA8C;CAC5E,MAAM,UAAiC,EAAE;CACzC,IAAI,CAAC,WAAW,eAAe,EAC7B,OAAO;EAAE,SAAS;EAAG,WAAW;EAAG,YAAY;EAAG,eAAe;EAAG;EAAS;CAE/E,KAAK,MAAM,QAAQ,YAAY,eAAe,EAAE;EAC9C,IAAI,KAAK,WAAW,IAAI,EAAE;EAC1B,MAAM,WAAW,KAAK,gBAAgB,KAAK;EAC3C,IAAI;GACF,IAAI,CAAC,SAAS,SAAS,CAAC,aAAa,EAAE;UACjC;GACN;;EAEF,MAAM,QAAQ,aAAa,UAAU,KAAK;EAC1C,IAAI,OAAO,QAAQ,KAAK,MAAM;;CAEhC,OAAO;EACL,SAAS,QAAQ;EACjB,WAAW,QAAQ,QAAQ,MAAM,CAAC,EAAE,SAAS,CAAC;EAC9C,YAAY,QAAQ,QAAQ,MAAM,EAAE,kBAAkB,EAAE,cAAc,EAAE,cAAc,CAAC;EACvF,eAAe,QAAQ,QAAQ,MAAM,CAAC,EAAE,YAAY,EAAE,kBAAkB,EAAE,cAAc,EAAE,cAAc,CAAC;EACzG;EACD;;;;;AAWH,SAAgB,qBACd,gBACA,SACA,SACkB;CAElB,MAAM,iBAAiB,KAAK,gBAAgB,MAAM,oCADrC,IAAI,MAAM,EAAC,aAAa,CAAC,MAAM,GAAG,GAC0B,CAAC;CAC1E,MAAM,cAAwB,EAAE;CAChC,MAAM,SAAmB,EAAE;CAC3B,UAAU,gBAAgB,EAAE,WAAW,MAAM,CAAC;CAE9C,KAAK,MAAM,SAAS,SAAS;EAK3B,IAAI,EAHD,CAAC,SAAS,iBAAiB,CAAC,SAAS,kBACrC,QAAQ,iBAAiB,CAAC,MAAM,YAChC,QAAQ,mBAAmB,MAAM,kBAAkB,MAAM,cAAc,MAAM,iBAC/D;EACjB,MAAM,SAAS,KAAK,gBAAgB,MAAM,KAAK;EAC/C,MAAM,UAAU,KAAK,gBAAgB,MAAM,KAAK;EAChD,IAAI;GACF,IAAI,WAAW,QAAQ,EAAE;IACvB,OAAO,KAAK,GAAG,MAAM,KAAK,8BAA8B;IACxD;;GAEF,WAAW,QAAQ,QAAQ;GAC3B,YAAY,KAAK,MAAM,KAAK;WACrB,KAAK;GACZ,OAAO,KAAK,GAAG,MAAM,KAAK,IAAI,eAAe,QAAQ,IAAI,UAAU,OAAO,IAAI,GAAG;;;CAGrF,OAAO;EAAE;EAAa;EAAQ"}
1
+ {"version":3,"file":"auto-skills-audit.js","names":[],"sources":["../../services/auto-skills-audit.ts"],"sourcesContent":["/**\n * Audit and quarantine legacy generated auto-skills (issue #1541).\n */\n\nimport { existsSync, mkdirSync, readdirSync, readFileSync, renameSync, statSync } from \"node:fs\";\nimport { join } from \"node:path\";\nimport { MAX_SKILL_FILE_BYTES } from \"../config/skill-size-limits.js\";\nimport { SkillValidator } from \"./skill-validator.js\";\n\nexport type AutoSkillAuditEntry = {\n slug: string;\n skillPath: string;\n skillBytes: number;\n skillLines: number;\n recipeBytes: number | null;\n loadable: boolean;\n transcriptLike: boolean;\n secretLike: boolean;\n injectionLike: boolean;\n validationViolations: string[];\n};\n\nexport type AutoSkillAuditReport = {\n scanned: number;\n oversized: number;\n suspicious: number;\n quarantinable: number;\n entries: AutoSkillAuditEntry[];\n};\n\nconst TRANSCRIPT_PATTERN = /\\*\\*\\w+\\*\\*\\s*\\{|\"tool\"\\s*:|tool_call_id/i;\nconst INJECTION_PATTERN = /\\bignore\\s+(?:previous|system|developer)\\s+instructions\\b/i;\n\nfunction utf8Bytes(path: string): number {\n try {\n return statSync(path).size;\n } catch {\n return 0;\n }\n}\n\nfunction scanSkillDir(skillDir: string, slug: string): AutoSkillAuditEntry | null {\n const skillPath = join(skillDir, \"SKILL.md\");\n if (!existsSync(skillPath)) return null;\n const skillContent = readFileSync(skillPath, \"utf-8\");\n const skillBytes = Buffer.byteLength(skillContent, \"utf8\");\n const skillLines = skillContent.split(/\\r?\\n/).length;\n const recipePath = join(skillDir, \"recipe.json\");\n const recipeBytes = existsSync(recipePath) ? utf8Bytes(recipePath) : null;\n const validator = new SkillValidator();\n const validation = validator.validate(skillContent);\n return {\n slug,\n skillPath,\n skillBytes,\n skillLines,\n recipeBytes,\n loadable: skillBytes <= MAX_SKILL_FILE_BYTES,\n transcriptLike: TRANSCRIPT_PATTERN.test(skillContent),\n secretLike: validation.violations.some((v) => /credential|token|private[- ]?key/i.test(v)),\n injectionLike: INJECTION_PATTERN.test(skillContent),\n validationViolations: validation.violations,\n };\n}\n\nexport function auditAutoSkills(skillsAutoPath: string): AutoSkillAuditReport {\n const entries: AutoSkillAuditEntry[] = [];\n if (!existsSync(skillsAutoPath)) {\n return { scanned: 0, oversized: 0, suspicious: 0, quarantinable: 0, entries };\n }\n for (const name of readdirSync(skillsAutoPath)) {\n if (name.startsWith(\".\")) continue;\n const skillDir = join(skillsAutoPath, name);\n try {\n if (!statSync(skillDir).isDirectory()) continue;\n } catch {\n continue;\n }\n const entry = scanSkillDir(skillDir, name);\n if (entry) entries.push(entry);\n }\n return {\n scanned: entries.length,\n oversized: entries.filter((e) => !e.loadable).length,\n suspicious: entries.filter((e) => e.transcriptLike || e.secretLike || e.injectionLike).length,\n quarantinable: entries.filter((e) => !e.loadable || e.transcriptLike || e.secretLike || e.injectionLike).length,\n entries,\n };\n}\n\nexport type QuarantineResult = {\n quarantined: string[];\n errors: string[];\n};\n\n/**\n * Move unsafe/oversized skills to skills/auto-quarantine/YYYY-MM-DD/<slug>/.\n */\nexport function quarantineAutoSkills(\n skillsAutoPath: string,\n entries: AutoSkillAuditEntry[],\n options?: { onlyOversized?: boolean; onlySuspicious?: boolean },\n): QuarantineResult {\n const date = new Date().toISOString().slice(0, 10);\n const quarantineRoot = join(skillsAutoPath, \"..\", \"auto-quarantine\", date);\n const quarantined: string[] = [];\n const errors: string[] = [];\n mkdirSync(quarantineRoot, { recursive: true });\n\n for (const entry of entries) {\n const shouldMove =\n (!options?.onlyOversized && !options?.onlySuspicious) ||\n (options.onlyOversized && !entry.loadable) ||\n (options.onlySuspicious && (entry.transcriptLike || entry.secretLike || entry.injectionLike));\n if (!shouldMove) continue;\n const srcDir = join(skillsAutoPath, entry.slug);\n const destDir = join(quarantineRoot, entry.slug);\n try {\n if (existsSync(destDir)) {\n errors.push(`${entry.slug}: destination already exists`);\n continue;\n }\n renameSync(srcDir, destDir);\n quarantined.push(entry.slug);\n } catch (err) {\n errors.push(`${entry.slug}: ${err instanceof Error ? err.message : String(err)}`);\n }\n }\n return { quarantined, errors };\n}\n"],"mappings":";;;;;;;;AA8BA,MAAM,qBAAqB;AAC3B,MAAM,oBAAoB;AAE1B,SAAS,UAAU,MAAsB;CACvC,IAAI;EACF,OAAO,SAAS,IAAI,EAAE;CACxB,QAAQ;EACN,OAAO;CACT;AACF;AAEA,SAAS,aAAa,UAAkB,MAA0C;CAChF,MAAM,YAAY,KAAK,UAAU,UAAU;CAC3C,IAAI,CAAC,WAAW,SAAS,GAAG,OAAO;CACnC,MAAM,eAAe,aAAa,WAAW,OAAO;CACpD,MAAM,aAAa,OAAO,WAAW,cAAc,MAAM;CACzD,MAAM,aAAa,aAAa,MAAM,OAAO,EAAE;CAC/C,MAAM,aAAa,KAAK,UAAU,aAAa;CAC/C,MAAM,cAAc,WAAW,UAAU,IAAI,UAAU,UAAU,IAAI;CAErE,MAAM,aAAa,IADG,eACK,EAAE,SAAS,YAAY;CAClD,OAAO;EACL;EACA;EACA;EACA;EACA;EACA,UAAU,cAAc;EACxB,gBAAgB,mBAAmB,KAAK,YAAY;EACpD,YAAY,WAAW,WAAW,MAAM,MAAM,oCAAoC,KAAK,CAAC,CAAC;EACzF,eAAe,kBAAkB,KAAK,YAAY;EAClD,sBAAsB,WAAW;CACnC;AACF;AAEA,SAAgB,gBAAgB,gBAA8C;CAC5E,MAAM,UAAiC,CAAC;CACxC,IAAI,CAAC,WAAW,cAAc,GAC5B,OAAO;EAAE,SAAS;EAAG,WAAW;EAAG,YAAY;EAAG,eAAe;EAAG;CAAQ;CAE9E,KAAK,MAAM,QAAQ,YAAY,cAAc,GAAG;EAC9C,IAAI,KAAK,WAAW,GAAG,GAAG;EAC1B,MAAM,WAAW,KAAK,gBAAgB,IAAI;EAC1C,IAAI;GACF,IAAI,CAAC,SAAS,QAAQ,EAAE,YAAY,GAAG;EACzC,QAAQ;GACN;EACF;EACA,MAAM,QAAQ,aAAa,UAAU,IAAI;EACzC,IAAI,OAAO,QAAQ,KAAK,KAAK;CAC/B;CACA,OAAO;EACL,SAAS,QAAQ;EACjB,WAAW,QAAQ,QAAQ,MAAM,CAAC,EAAE,QAAQ,EAAE;EAC9C,YAAY,QAAQ,QAAQ,MAAM,EAAE,kBAAkB,EAAE,cAAc,EAAE,aAAa,EAAE;EACvF,eAAe,QAAQ,QAAQ,MAAM,CAAC,EAAE,YAAY,EAAE,kBAAkB,EAAE,cAAc,EAAE,aAAa,EAAE;EACzG;CACF;AACF;;;;AAUA,SAAgB,qBACd,gBACA,SACA,SACkB;CAElB,MAAM,iBAAiB,KAAK,gBAAgB,MAAM,oCADrC,IAAI,KAAK,GAAE,YAAY,EAAE,MAAM,GAAG,EACyB,CAAC;CACzE,MAAM,cAAwB,CAAC;CAC/B,MAAM,SAAmB,CAAC;CAC1B,UAAU,gBAAgB,EAAE,WAAW,KAAK,CAAC;CAE7C,KAAK,MAAM,SAAS,SAAS;EAK3B,IAAI,EAHD,CAAC,SAAS,iBAAiB,CAAC,SAAS,kBACrC,QAAQ,iBAAiB,CAAC,MAAM,YAChC,QAAQ,mBAAmB,MAAM,kBAAkB,MAAM,cAAc,MAAM,iBAC/D;EACjB,MAAM,SAAS,KAAK,gBAAgB,MAAM,IAAI;EAC9C,MAAM,UAAU,KAAK,gBAAgB,MAAM,IAAI;EAC/C,IAAI;GACF,IAAI,WAAW,OAAO,GAAG;IACvB,OAAO,KAAK,GAAG,MAAM,KAAK,6BAA6B;IACvD;GACF;GACA,WAAW,QAAQ,OAAO;GAC1B,YAAY,KAAK,MAAM,IAAI;EAC7B,SAAS,KAAK;GACZ,OAAO,KAAK,GAAG,MAAM,KAAK,IAAI,eAAe,QAAQ,IAAI,UAAU,OAAO,GAAG,GAAG;EAClF;CACF;CACA,OAAO;EAAE;EAAa;CAAO;AAC/B"}
@@ -1 +1 @@
1
- {"version":3,"file":"bootstrap-optional.js","names":[],"sources":["../../services/bootstrap-optional.ts"],"sourcesContent":["import { dirname, join } from \"node:path\";\nimport type { ClawdbotPluginApi } from \"openclaw/plugin-sdk/core\";\nimport { ApitapStore } from \"../backends/apitap-store.js\";\nimport { CredentialsDB } from \"../backends/credentials-db.js\";\nimport { CrystallizationStore } from \"../backends/crystallization-store.js\";\nimport { EventLog } from \"../backends/event-log.js\";\nimport type { FactsDB } from \"../backends/facts-db.js\";\nimport { IdentityReflectionStore } from \"../backends/identity-reflection-store.js\";\nimport { IssueStore } from \"../backends/issue-store.js\";\nimport { PersonaStateStore } from \"../backends/persona-state-store.js\";\nimport { ProposalsDB } from \"../backends/proposals-db.js\";\nimport { ToolProposalStore } from \"../backends/tool-proposal-store.js\";\nimport { WriteAheadLog } from \"../backends/wal.js\";\nimport { WorkflowStore } from \"../backends/workflow-store.js\";\nimport type { BootstrapPhaseConfig, HybridMemoryConfig } from \"../config.js\";\nimport { ProvenanceService } from \"./provenance.js\";\nimport { AliasDB } from \"./retrieval-aliases.js\";\nimport { VerificationStore } from \"./verification-store.js\";\n\nexport interface OptionalBootstrapContext {\n cfg: HybridMemoryConfig;\n api: ClawdbotPluginApi;\n factsDb: FactsDB;\n resolvedSqlitePath: string;\n}\n\nexport interface OptionalBootstrapServices {\n credentialsDb: CredentialsDB | null;\n wal: WriteAheadLog | null;\n proposalsDb: ProposalsDB | null;\n identityReflectionStore: IdentityReflectionStore | null;\n personaStateStore: PersonaStateStore | null;\n eventLog: EventLog | null;\n aliasDb: AliasDB | null;\n issueStore: IssueStore;\n workflowStore: WorkflowStore;\n crystallizationStore: CrystallizationStore;\n toolProposalStore: ToolProposalStore;\n verificationStore: VerificationStore | null;\n provenanceService: ProvenanceService | null;\n apitapStore: ApitapStore;\n}\n\ntype OptionalBootstrapInstaller = BootstrapPhaseConfig & {\n id: string;\n install(context: OptionalBootstrapContext): OptionalBootstrapServices;\n};\n\nexport const optionalBootstrapInstaller: OptionalBootstrapInstaller = {\n id: \"adjacentFeatures\",\n bootstrapPhase: \"optional\",\n install({ cfg, api, factsDb, resolvedSqlitePath }) {\n const baseDir = dirname(resolvedSqlitePath);\n\n let credentialsDb: CredentialsDB | null = null;\n if (cfg.credentials.enabled) {\n const credPath = join(baseDir, \"credentials.db\");\n credentialsDb = new CredentialsDB(credPath, cfg.credentials.encryptionKey ?? \"\");\n const st = credentialsDb.getVaultStatus();\n if (st.encryptedAtRest) {\n api.logger.info(\n `memory-hybrid: credentials vault enabled (encrypted, kdf_version=${st.kdfVersion}) (${credPath})`,\n );\n } else if (st.migrationRequired) {\n api.logger.warn(\n `memory-hybrid: credentials vault enabled (plaintext, kdf_version=${st.kdfVersion}) (${credPath}) — encryption key is configured but ignored until the vault is encrypted. Fix: run \\`openclaw hybrid-mem credentials encrypt-vault --yes\\` (see docs/CREDENTIALS.md).`,\n );\n } else {\n api.logger.warn(\n `memory-hybrid: credentials vault enabled (plaintext, kdf_version=${st.kdfVersion}) (${credPath}) — no encryption key configured. Set credentials.encryptionKey (16+ chars) or OPENCLAW_CRED_KEY when ready; until then restrict access to this file.`,\n );\n }\n }\n\n let wal: WriteAheadLog | null = null;\n if (cfg.wal.enabled) {\n const walPath = cfg.wal.walPath || join(baseDir, \"memory.wal\");\n wal = new WriteAheadLog(walPath, cfg.wal.maxAge);\n api.logger.info(`memory-hybrid: WAL enabled (${walPath})`);\n }\n\n let proposalsDb: ProposalsDB | null = null;\n if (cfg.personaProposals.enabled) {\n const proposalsPath = join(baseDir, \"proposals.db\");\n proposalsDb = new ProposalsDB(proposalsPath);\n api.logger.info(`memory-hybrid: persona proposals enabled (${proposalsPath})`);\n }\n\n let identityReflectionStore: IdentityReflectionStore | null = null;\n if (cfg.identityReflection.enabled || cfg.personaProposals.enabled || cfg.identityPromotion.enabled) {\n const identityReflectionsPath = join(baseDir, \"identity-reflections.db\");\n identityReflectionStore = new IdentityReflectionStore(identityReflectionsPath);\n api.logger.info(`memory-hybrid: identity reflections enabled (${identityReflectionsPath})`);\n }\n\n let personaStateStore: PersonaStateStore | null = null;\n if (cfg.identityPromotion.enabled || cfg.personaProposals.enabled) {\n const personaStatePath = join(baseDir, \"persona-state.db\");\n personaStateStore = new PersonaStateStore(personaStatePath);\n api.logger.info(`memory-hybrid: persona state store enabled (${personaStatePath})`);\n }\n\n let eventLog: EventLog | null = null;\n if (cfg.nightlyCycle.enabled || cfg.graph?.autoSupersede || cfg.passiveObserver.enabled) {\n const eventLogPath = join(baseDir, \"event-log.db\");\n eventLog = new EventLog(eventLogPath);\n api.logger.info(`memory-hybrid: event log initialized (${eventLogPath})`);\n }\n\n let aliasDb: AliasDB | null = null;\n if (cfg.aliases?.enabled) {\n const aliasPath = join(baseDir, \"aliases.db\");\n const aliasLancePath = join(baseDir, \"aliases.lance\");\n aliasDb = new AliasDB(aliasPath, aliasLancePath, cfg.embedding.dimensions);\n api.logger.info(`memory-hybrid: retrieval aliases enabled (${aliasPath}, ${aliasLancePath})`);\n }\n\n const issueStorePath = join(baseDir, \"issues.db\");\n const issueStore = new IssueStore(issueStorePath);\n api.logger.info(`memory-hybrid: issue store initialized (${issueStorePath})`);\n\n const workflowStorePath = join(baseDir, \"workflow-traces.db\");\n const workflowStore = new WorkflowStore(workflowStorePath);\n api.logger.info(`memory-hybrid: workflow store initialized (${workflowStorePath})`);\n\n const crystallizationStorePath = join(baseDir, \"crystallization-proposals.db\");\n const crystallizationStore = new CrystallizationStore(crystallizationStorePath);\n api.logger.info(`memory-hybrid: crystallization store initialized (${crystallizationStorePath})`);\n\n const toolProposalStorePath = join(baseDir, \"tool-proposals.db\");\n const toolProposalStore = new ToolProposalStore(toolProposalStorePath);\n api.logger.info(`memory-hybrid: tool proposal store initialized (${toolProposalStorePath})`);\n\n let verificationStore: VerificationStore | null = null;\n if (cfg.verification.enabled) {\n verificationStore = new VerificationStore(factsDb.getRawDb(), {\n backupPath: cfg.verification.backupPath,\n reverificationDays: cfg.verification.reverificationDays,\n logger: api.logger,\n });\n api.logger.info(\"memory-hybrid: verification store enabled\");\n }\n\n let provenanceService: ProvenanceService | null = null;\n if (cfg.provenance.enabled) {\n const provenancePath = join(baseDir, \"provenance.db\");\n provenanceService = new ProvenanceService(provenancePath);\n api.logger.info(`memory-hybrid: provenance tracing enabled (${provenancePath})`);\n }\n\n const apitapStorePath = join(baseDir, \"apitap-endpoints.db\");\n const apitapStore = new ApitapStore(apitapStorePath);\n api.logger.info(`memory-hybrid: apitap store initialized (${apitapStorePath})`);\n\n return {\n credentialsDb,\n wal,\n proposalsDb,\n identityReflectionStore,\n personaStateStore,\n eventLog,\n aliasDb,\n issueStore,\n workflowStore,\n crystallizationStore,\n toolProposalStore,\n verificationStore,\n provenanceService,\n apitapStore,\n };\n },\n};\n\nexport function installOptionalBootstrapServices(context: OptionalBootstrapContext): OptionalBootstrapServices {\n return optionalBootstrapInstaller.install(context);\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"bootstrap-optional.js","names":[],"sources":["../../services/bootstrap-optional.ts"],"sourcesContent":["import { dirname, join } from \"node:path\";\nimport type { ClawdbotPluginApi } from \"openclaw/plugin-sdk/core\";\nimport { ApitapStore } from \"../backends/apitap-store.js\";\nimport { CredentialsDB } from \"../backends/credentials-db.js\";\nimport { CrystallizationStore } from \"../backends/crystallization-store.js\";\nimport { EventLog } from \"../backends/event-log.js\";\nimport type { FactsDB } from \"../backends/facts-db.js\";\nimport { IdentityReflectionStore } from \"../backends/identity-reflection-store.js\";\nimport { IssueStore } from \"../backends/issue-store.js\";\nimport { PersonaStateStore } from \"../backends/persona-state-store.js\";\nimport { ProposalsDB } from \"../backends/proposals-db.js\";\nimport { ToolProposalStore } from \"../backends/tool-proposal-store.js\";\nimport { WriteAheadLog } from \"../backends/wal.js\";\nimport { WorkflowStore } from \"../backends/workflow-store.js\";\nimport type { BootstrapPhaseConfig, HybridMemoryConfig } from \"../config.js\";\nimport { ProvenanceService } from \"./provenance.js\";\nimport { AliasDB } from \"./retrieval-aliases.js\";\nimport { VerificationStore } from \"./verification-store.js\";\n\nexport interface OptionalBootstrapContext {\n cfg: HybridMemoryConfig;\n api: ClawdbotPluginApi;\n factsDb: FactsDB;\n resolvedSqlitePath: string;\n}\n\nexport interface OptionalBootstrapServices {\n credentialsDb: CredentialsDB | null;\n wal: WriteAheadLog | null;\n proposalsDb: ProposalsDB | null;\n identityReflectionStore: IdentityReflectionStore | null;\n personaStateStore: PersonaStateStore | null;\n eventLog: EventLog | null;\n aliasDb: AliasDB | null;\n issueStore: IssueStore;\n workflowStore: WorkflowStore;\n crystallizationStore: CrystallizationStore;\n toolProposalStore: ToolProposalStore;\n verificationStore: VerificationStore | null;\n provenanceService: ProvenanceService | null;\n apitapStore: ApitapStore;\n}\n\ntype OptionalBootstrapInstaller = BootstrapPhaseConfig & {\n id: string;\n install(context: OptionalBootstrapContext): OptionalBootstrapServices;\n};\n\nexport const optionalBootstrapInstaller: OptionalBootstrapInstaller = {\n id: \"adjacentFeatures\",\n bootstrapPhase: \"optional\",\n install({ cfg, api, factsDb, resolvedSqlitePath }) {\n const baseDir = dirname(resolvedSqlitePath);\n\n let credentialsDb: CredentialsDB | null = null;\n if (cfg.credentials.enabled) {\n const credPath = join(baseDir, \"credentials.db\");\n credentialsDb = new CredentialsDB(credPath, cfg.credentials.encryptionKey ?? \"\");\n const st = credentialsDb.getVaultStatus();\n if (st.encryptedAtRest) {\n api.logger.info(\n `memory-hybrid: credentials vault enabled (encrypted, kdf_version=${st.kdfVersion}) (${credPath})`,\n );\n } else if (st.migrationRequired) {\n api.logger.warn(\n `memory-hybrid: credentials vault enabled (plaintext, kdf_version=${st.kdfVersion}) (${credPath}) — encryption key is configured but ignored until the vault is encrypted. Fix: run \\`openclaw hybrid-mem credentials encrypt-vault --yes\\` (see docs/CREDENTIALS.md).`,\n );\n } else {\n api.logger.warn(\n `memory-hybrid: credentials vault enabled (plaintext, kdf_version=${st.kdfVersion}) (${credPath}) — no encryption key configured. Set credentials.encryptionKey (16+ chars) or OPENCLAW_CRED_KEY when ready; until then restrict access to this file.`,\n );\n }\n }\n\n let wal: WriteAheadLog | null = null;\n if (cfg.wal.enabled) {\n const walPath = cfg.wal.walPath || join(baseDir, \"memory.wal\");\n wal = new WriteAheadLog(walPath, cfg.wal.maxAge);\n api.logger.info(`memory-hybrid: WAL enabled (${walPath})`);\n }\n\n let proposalsDb: ProposalsDB | null = null;\n if (cfg.personaProposals.enabled) {\n const proposalsPath = join(baseDir, \"proposals.db\");\n proposalsDb = new ProposalsDB(proposalsPath);\n api.logger.info(`memory-hybrid: persona proposals enabled (${proposalsPath})`);\n }\n\n let identityReflectionStore: IdentityReflectionStore | null = null;\n if (cfg.identityReflection.enabled || cfg.personaProposals.enabled || cfg.identityPromotion.enabled) {\n const identityReflectionsPath = join(baseDir, \"identity-reflections.db\");\n identityReflectionStore = new IdentityReflectionStore(identityReflectionsPath);\n api.logger.info(`memory-hybrid: identity reflections enabled (${identityReflectionsPath})`);\n }\n\n let personaStateStore: PersonaStateStore | null = null;\n if (cfg.identityPromotion.enabled || cfg.personaProposals.enabled) {\n const personaStatePath = join(baseDir, \"persona-state.db\");\n personaStateStore = new PersonaStateStore(personaStatePath);\n api.logger.info(`memory-hybrid: persona state store enabled (${personaStatePath})`);\n }\n\n let eventLog: EventLog | null = null;\n if (cfg.nightlyCycle.enabled || cfg.graph?.autoSupersede || cfg.passiveObserver.enabled) {\n const eventLogPath = join(baseDir, \"event-log.db\");\n eventLog = new EventLog(eventLogPath);\n api.logger.info(`memory-hybrid: event log initialized (${eventLogPath})`);\n }\n\n let aliasDb: AliasDB | null = null;\n if (cfg.aliases?.enabled) {\n const aliasPath = join(baseDir, \"aliases.db\");\n const aliasLancePath = join(baseDir, \"aliases.lance\");\n aliasDb = new AliasDB(aliasPath, aliasLancePath, cfg.embedding.dimensions);\n api.logger.info(`memory-hybrid: retrieval aliases enabled (${aliasPath}, ${aliasLancePath})`);\n }\n\n const issueStorePath = join(baseDir, \"issues.db\");\n const issueStore = new IssueStore(issueStorePath);\n api.logger.info(`memory-hybrid: issue store initialized (${issueStorePath})`);\n\n const workflowStorePath = join(baseDir, \"workflow-traces.db\");\n const workflowStore = new WorkflowStore(workflowStorePath);\n api.logger.info(`memory-hybrid: workflow store initialized (${workflowStorePath})`);\n\n const crystallizationStorePath = join(baseDir, \"crystallization-proposals.db\");\n const crystallizationStore = new CrystallizationStore(crystallizationStorePath);\n api.logger.info(`memory-hybrid: crystallization store initialized (${crystallizationStorePath})`);\n\n const toolProposalStorePath = join(baseDir, \"tool-proposals.db\");\n const toolProposalStore = new ToolProposalStore(toolProposalStorePath);\n api.logger.info(`memory-hybrid: tool proposal store initialized (${toolProposalStorePath})`);\n\n let verificationStore: VerificationStore | null = null;\n if (cfg.verification.enabled) {\n verificationStore = new VerificationStore(factsDb.getRawDb(), {\n backupPath: cfg.verification.backupPath,\n reverificationDays: cfg.verification.reverificationDays,\n logger: api.logger,\n });\n api.logger.info(\"memory-hybrid: verification store enabled\");\n }\n\n let provenanceService: ProvenanceService | null = null;\n if (cfg.provenance.enabled) {\n const provenancePath = join(baseDir, \"provenance.db\");\n provenanceService = new ProvenanceService(provenancePath);\n api.logger.info(`memory-hybrid: provenance tracing enabled (${provenancePath})`);\n }\n\n const apitapStorePath = join(baseDir, \"apitap-endpoints.db\");\n const apitapStore = new ApitapStore(apitapStorePath);\n api.logger.info(`memory-hybrid: apitap store initialized (${apitapStorePath})`);\n\n return {\n credentialsDb,\n wal,\n proposalsDb,\n identityReflectionStore,\n personaStateStore,\n eventLog,\n aliasDb,\n issueStore,\n workflowStore,\n crystallizationStore,\n toolProposalStore,\n verificationStore,\n provenanceService,\n apitapStore,\n };\n },\n};\n\nexport function installOptionalBootstrapServices(context: OptionalBootstrapContext): OptionalBootstrapServices {\n return optionalBootstrapInstaller.install(context);\n}\n"],"mappings":";;;;;;;;;;;;;;;;AAgDA,MAAa,6BAAyD;CACpE,IAAI;CACJ,gBAAgB;CAChB,QAAQ,EAAE,KAAK,KAAK,SAAS,sBAAsB;EACjD,MAAM,UAAU,QAAQ,kBAAkB;EAE1C,IAAI,gBAAsC;EAC1C,IAAI,IAAI,YAAY,SAAS;GAC3B,MAAM,WAAW,KAAK,SAAS,gBAAgB;GAC/C,gBAAgB,IAAI,cAAc,UAAU,IAAI,YAAY,iBAAiB,EAAE;GAC/E,MAAM,KAAK,cAAc,eAAe;GACxC,IAAI,GAAG,iBACL,IAAI,OAAO,KACT,oEAAoE,GAAG,WAAW,KAAK,SAAS,EAClG;QACK,IAAI,GAAG,mBACZ,IAAI,OAAO,KACT,oEAAoE,GAAG,WAAW,KAAK,SAAS,uKAClG;QAEA,IAAI,OAAO,KACT,oEAAoE,GAAG,WAAW,KAAK,SAAS,sJAClG;EAEJ;EAEA,IAAI,MAA4B;EAChC,IAAI,IAAI,IAAI,SAAS;GACnB,MAAM,UAAU,IAAI,IAAI,WAAW,KAAK,SAAS,YAAY;GAC7D,MAAM,IAAI,cAAc,SAAS,IAAI,IAAI,MAAM;GAC/C,IAAI,OAAO,KAAK,+BAA+B,QAAQ,EAAE;EAC3D;EAEA,IAAI,cAAkC;EACtC,IAAI,IAAI,iBAAiB,SAAS;GAChC,MAAM,gBAAgB,KAAK,SAAS,cAAc;GAClD,cAAc,IAAI,YAAY,aAAa;GAC3C,IAAI,OAAO,KAAK,6CAA6C,cAAc,EAAE;EAC/E;EAEA,IAAI,0BAA0D;EAC9D,IAAI,IAAI,mBAAmB,WAAW,IAAI,iBAAiB,WAAW,IAAI,kBAAkB,SAAS;GACnG,MAAM,0BAA0B,KAAK,SAAS,yBAAyB;GACvE,0BAA0B,IAAI,wBAAwB,uBAAuB;GAC7E,IAAI,OAAO,KAAK,gDAAgD,wBAAwB,EAAE;EAC5F;EAEA,IAAI,oBAA8C;EAClD,IAAI,IAAI,kBAAkB,WAAW,IAAI,iBAAiB,SAAS;GACjE,MAAM,mBAAmB,KAAK,SAAS,kBAAkB;GACzD,oBAAoB,IAAI,kBAAkB,gBAAgB;GAC1D,IAAI,OAAO,KAAK,+CAA+C,iBAAiB,EAAE;EACpF;EAEA,IAAI,WAA4B;EAChC,IAAI,IAAI,aAAa,WAAW,IAAI,OAAO,iBAAiB,IAAI,gBAAgB,SAAS;GACvF,MAAM,eAAe,KAAK,SAAS,cAAc;GACjD,WAAW,IAAI,SAAS,YAAY;GACpC,IAAI,OAAO,KAAK,yCAAyC,aAAa,EAAE;EAC1E;EAEA,IAAI,UAA0B;EAC9B,IAAI,IAAI,SAAS,SAAS;GACxB,MAAM,YAAY,KAAK,SAAS,YAAY;GAC5C,MAAM,iBAAiB,KAAK,SAAS,eAAe;GACpD,UAAU,IAAI,QAAQ,WAAW,gBAAgB,IAAI,UAAU,UAAU;GACzE,IAAI,OAAO,KAAK,6CAA6C,UAAU,IAAI,eAAe,EAAE;EAC9F;EAEA,MAAM,iBAAiB,KAAK,SAAS,WAAW;EAChD,MAAM,aAAa,IAAI,WAAW,cAAc;EAChD,IAAI,OAAO,KAAK,2CAA2C,eAAe,EAAE;EAE5E,MAAM,oBAAoB,KAAK,SAAS,oBAAoB;EAC5D,MAAM,gBAAgB,IAAI,cAAc,iBAAiB;EACzD,IAAI,OAAO,KAAK,8CAA8C,kBAAkB,EAAE;EAElF,MAAM,2BAA2B,KAAK,SAAS,8BAA8B;EAC7E,MAAM,uBAAuB,IAAI,qBAAqB,wBAAwB;EAC9E,IAAI,OAAO,KAAK,qDAAqD,yBAAyB,EAAE;EAEhG,MAAM,wBAAwB,KAAK,SAAS,mBAAmB;EAC/D,MAAM,oBAAoB,IAAI,kBAAkB,qBAAqB;EACrE,IAAI,OAAO,KAAK,mDAAmD,sBAAsB,EAAE;EAE3F,IAAI,oBAA8C;EAClD,IAAI,IAAI,aAAa,SAAS;GAC5B,oBAAoB,IAAI,kBAAkB,QAAQ,SAAS,GAAG;IAC5D,YAAY,IAAI,aAAa;IAC7B,oBAAoB,IAAI,aAAa;IACrC,QAAQ,IAAI;GACd,CAAC;GACD,IAAI,OAAO,KAAK,2CAA2C;EAC7D;EAEA,IAAI,oBAA8C;EAClD,IAAI,IAAI,WAAW,SAAS;GAC1B,MAAM,iBAAiB,KAAK,SAAS,eAAe;GACpD,oBAAoB,IAAI,kBAAkB,cAAc;GACxD,IAAI,OAAO,KAAK,8CAA8C,eAAe,EAAE;EACjF;EAEA,MAAM,kBAAkB,KAAK,SAAS,qBAAqB;EAC3D,MAAM,cAAc,IAAI,YAAY,eAAe;EACnD,IAAI,OAAO,KAAK,4CAA4C,gBAAgB,EAAE;EAE9E,OAAO;GACL;GACA;GACA;GACA;GACA;GACA;GACA;GACA;GACA;GACA;GACA;GACA;GACA;GACA;EACF;CACF;AACF;AAEA,SAAgB,iCAAiC,SAA8D;CAC7G,OAAO,2BAA2B,QAAQ,OAAO;AACnD"}
@@ -1 +1 @@
1
- {"version":3,"file":"bootstrap-priority.js","names":["BOOTSTRAP_PHASE_ORDER"],"sources":["../../services/bootstrap-priority.ts"],"sourcesContent":["import type { BootstrapPhase, BootstrapPhaseConfig } from \"../config.js\";\nimport { BOOTSTRAP_PHASES } from \"../config/types/bootstrap.js\";\n\n/** Explicit startup ordering so core subsystems are always installed first. */\nconst BOOTSTRAP_PHASE_ORDER: readonly BootstrapPhase[] = BOOTSTRAP_PHASES;\n\nconst BOOTSTRAP_PHASE_PRIORITY = new Map<BootstrapPhase, number>(\n BOOTSTRAP_PHASE_ORDER.map((phase, index) => [phase, index]),\n);\n\nfunction compareBootstrapPhase(a: BootstrapPhase, b: BootstrapPhase): number {\n return (\n (BOOTSTRAP_PHASE_PRIORITY.get(a) ?? Number.MAX_SAFE_INTEGER) -\n (BOOTSTRAP_PHASE_PRIORITY.get(b) ?? Number.MAX_SAFE_INTEGER)\n );\n}\n\nexport function orderByBootstrapPhase<T extends BootstrapPhaseConfig>(entries: readonly T[]): T[] {\n return [...entries].sort((left, right) => compareBootstrapPhase(left.bootstrapPhase, right.bootstrapPhase));\n}\n"],"mappings":";;AAMA,MAAM,2BAA2B,IAAI,IACnCA,iBAAsB,KAAK,OAAO,UAAU,CAAC,OAAO,MAAM,CAAC,CAC5D;AAED,SAAS,sBAAsB,GAAmB,GAA2B;CAC3E,QACG,yBAAyB,IAAI,EAAE,IAAI,OAAO,qBAC1C,yBAAyB,IAAI,EAAE,IAAI,OAAO;;AAI/C,SAAgB,sBAAsD,SAA4B;CAChG,OAAO,CAAC,GAAG,QAAQ,CAAC,MAAM,MAAM,UAAU,sBAAsB,KAAK,gBAAgB,MAAM,eAAe,CAAC"}
1
+ {"version":3,"file":"bootstrap-priority.js","names":["BOOTSTRAP_PHASE_ORDER"],"sources":["../../services/bootstrap-priority.ts"],"sourcesContent":["import type { BootstrapPhase, BootstrapPhaseConfig } from \"../config.js\";\nimport { BOOTSTRAP_PHASES } from \"../config/types/bootstrap.js\";\n\n/** Explicit startup ordering so core subsystems are always installed first. */\nconst BOOTSTRAP_PHASE_ORDER: readonly BootstrapPhase[] = BOOTSTRAP_PHASES;\n\nconst BOOTSTRAP_PHASE_PRIORITY = new Map<BootstrapPhase, number>(\n BOOTSTRAP_PHASE_ORDER.map((phase, index) => [phase, index]),\n);\n\nfunction compareBootstrapPhase(a: BootstrapPhase, b: BootstrapPhase): number {\n return (\n (BOOTSTRAP_PHASE_PRIORITY.get(a) ?? Number.MAX_SAFE_INTEGER) -\n (BOOTSTRAP_PHASE_PRIORITY.get(b) ?? Number.MAX_SAFE_INTEGER)\n );\n}\n\nexport function orderByBootstrapPhase<T extends BootstrapPhaseConfig>(entries: readonly T[]): T[] {\n return [...entries].sort((left, right) => compareBootstrapPhase(left.bootstrapPhase, right.bootstrapPhase));\n}\n"],"mappings":";;AAMA,MAAM,2BAA2B,IAAI,IACnCA,iBAAsB,KAAK,OAAO,UAAU,CAAC,OAAO,KAAK,CAAC,CAC5D;AAEA,SAAS,sBAAsB,GAAmB,GAA2B;CAC3E,QACG,yBAAyB,IAAI,CAAC,KAAK,OAAO,qBAC1C,yBAAyB,IAAI,CAAC,KAAK,OAAO;AAE/C;AAEA,SAAgB,sBAAsD,SAA4B;CAChG,OAAO,CAAC,GAAG,OAAO,EAAE,MAAM,MAAM,UAAU,sBAAsB,KAAK,gBAAgB,MAAM,cAAc,CAAC;AAC5G"}
@@ -1 +1 @@
1
- {"version":3,"file":"bootstrap.js","names":[],"sources":["../../services/bootstrap.ts"],"sourcesContent":["import { dirname, join } from \"node:path\";\nimport type { ClawdbotPluginApi } from \"openclaw/plugin-sdk/core\";\nimport { EdictStore } from \"../backends/edict-store.js\";\nimport { FactsDB } from \"../backends/facts-db.js\";\nimport { VectorDB } from \"../backends/vector-db.js\";\nimport type { BootstrapPhaseConfig, EmbeddingModelConfig, HybridMemoryConfig } from \"../config.js\";\nimport { type EmbeddingRegistry, buildEmbeddingRegistry } from \"./embedding-registry.js\";\nimport { type EmbeddingProvider, createEmbeddingProvider } from \"./embeddings.js\";\n\nexport interface CoreBootstrapContext {\n cfg: HybridMemoryConfig;\n api: ClawdbotPluginApi;\n resolvedSqlitePath: string;\n resolvedLancePath: string;\n}\n\nexport interface CoreBootstrapServices {\n factsDb: FactsDB;\n edictStore: EdictStore;\n vectorDb: VectorDB;\n embeddings: EmbeddingProvider;\n embeddingRegistry: EmbeddingRegistry;\n}\n\ntype CoreBootstrapInstaller = BootstrapPhaseConfig & {\n id: string;\n install(context: CoreBootstrapContext): CoreBootstrapServices;\n};\n\n/** Fail fast before LanceDB when the active embedding provider reports invalid width (#944). Exported for tests. */\nexport function assertEmbeddingDimensionsForVectorDb(dim: number): void {\n if (!Number.isFinite(dim) || !Number.isInteger(dim) || dim <= 0) {\n throw new Error(\n `Invalid embedding dimensions: ${String(dim)}. Set embedding.dimensions to a positive integer matching your provider output.`,\n );\n }\n}\n\nfunction resolveEmbeddingRegistryModels(\n embedding: HybridMemoryConfig[\"embedding\"],\n): EmbeddingModelConfig[] | undefined {\n if (Array.isArray(embedding.multiModels) && embedding.multiModels.length > 0) {\n return embedding.multiModels;\n }\n const rawModels = (embedding as unknown as { models?: unknown }).models;\n if (!Array.isArray(rawModels) || rawModels.length === 0) return undefined;\n const hasObjectModels = rawModels.every((item) => item && typeof item === \"object\");\n if (!hasObjectModels) return undefined;\n return rawModels as EmbeddingModelConfig[];\n}\n\nexport const coreBootstrapInstaller: CoreBootstrapInstaller = {\n id: \"memoryCore\",\n bootstrapPhase: \"core\",\n install({ cfg, api, resolvedSqlitePath, resolvedLancePath }) {\n let factsDb: FactsDB;\n try {\n factsDb = new FactsDB(resolvedSqlitePath, {\n fuzzyDedupe: cfg.store.fuzzyDedupe,\n storeConfig: cfg.store,\n });\n } catch (err) {\n api.logger.error(`memory-hybrid: core bootstrap failed: ${err instanceof Error ? err.message : String(err)}`);\n throw err;\n }\n\n // EdictStore: separate SQLite file alongside facts DB, for verified ground-truth facts\n const edictStorePath = join(dirname(resolvedSqlitePath), \"edicts.db\");\n const edictStore = new EdictStore(edictStorePath);\n\n // Instantiate EmbeddingProvider FIRST so VectorDB always uses the actual runtime\n // dimensions. When a ChainEmbeddingProvider is active (e.g. Google in chain with\n // OpenAI model), the chain may output different dimensions (768) than the catalog\n // model dimensions (3072). Using cfg.embedding.dimensions directly caused a silent\n // mismatch where VectorDB.search() returned [] on every query. Issue #939.\n const embeddings = createEmbeddingProvider(cfg.embedding, (err) => {\n api.logger.warn(\n `memory-hybrid: ${cfg.embedding.provider} embedding unavailable (${err}), switching to OpenAI fallback`,\n );\n });\n\n const effectiveDimensions = embeddings.dimensions;\n try {\n assertEmbeddingDimensionsForVectorDb(effectiveDimensions);\n } catch (err) {\n const msg = err instanceof Error ? err.message : String(err);\n api.logger.error(`memory-hybrid: ${msg}`);\n throw err;\n }\n if (effectiveDimensions !== cfg.embedding.dimensions) {\n api.logger.warn(\n `memory-hybrid: embedding provider dimensions (${effectiveDimensions}) differ from config dimensions (${cfg.embedding.dimensions}). Using provider dimensions (${effectiveDimensions}) for LanceDB. To silence this warning, set embedding.dimensions: ${effectiveDimensions} in plugin config, or set embedding.preferredProviders explicitly to control which providers are used.`,\n );\n }\n\n const vectorDb = new VectorDB(resolvedLancePath, effectiveDimensions, cfg.vector.autoRepair);\n vectorDb.setLogger(api.logger);\n\n const embeddingRegistry = buildEmbeddingRegistry(\n embeddings,\n cfg.embedding.model,\n resolveEmbeddingRegistryModels(cfg.embedding),\n );\n\n return {\n factsDb,\n edictStore,\n vectorDb,\n embeddings,\n embeddingRegistry,\n };\n },\n};\n\nexport function installCoreBootstrapServices(context: CoreBootstrapContext): CoreBootstrapServices {\n return coreBootstrapInstaller.install(context);\n}\n"],"mappings":";;;;;;;;;;;AA8BA,SAAgB,qCAAqC,KAAmB;CACtE,IAAI,CAAC,OAAO,SAAS,IAAI,IAAI,CAAC,OAAO,UAAU,IAAI,IAAI,OAAO,GAC5D,MAAM,IAAI,MACR,iCAAiC,OAAO,IAAI,CAAC,iFAC9C;;AAIL,SAAS,+BACP,WACoC;CACpC,IAAI,MAAM,QAAQ,UAAU,YAAY,IAAI,UAAU,YAAY,SAAS,GACzE,OAAO,UAAU;CAEnB,MAAM,YAAa,UAA8C;CACjE,IAAI,CAAC,MAAM,QAAQ,UAAU,IAAI,UAAU,WAAW,GAAG,OAAO,KAAA;CAEhE,IAAI,CADoB,UAAU,OAAO,SAAS,QAAQ,OAAO,SAAS,SACtD,EAAE,OAAO,KAAA;CAC7B,OAAO;;AAGT,MAAa,yBAAiD;CAC5D,IAAI;CACJ,gBAAgB;CAChB,QAAQ,EAAE,KAAK,KAAK,oBAAoB,qBAAqB;EAC3D,IAAI;EACJ,IAAI;GACF,UAAU,IAAI,QAAQ,oBAAoB;IACxC,aAAa,IAAI,MAAM;IACvB,aAAa,IAAI;IAClB,CAAC;WACK,KAAK;GACZ,IAAI,OAAO,MAAM,yCAAyC,eAAe,QAAQ,IAAI,UAAU,OAAO,IAAI,GAAG;GAC7G,MAAM;;EAKR,MAAM,aAAa,IAAI,WADA,KAAK,QAAQ,mBAAmB,EAAE,YACT,CAAC;EAOjD,MAAM,aAAa,wBAAwB,IAAI,YAAY,QAAQ;GACjE,IAAI,OAAO,KACT,kBAAkB,IAAI,UAAU,SAAS,0BAA0B,IAAI,iCACxE;IACD;EAEF,MAAM,sBAAsB,WAAW;EACvC,IAAI;GACF,qCAAqC,oBAAoB;WAClD,KAAK;GACZ,MAAM,MAAM,eAAe,QAAQ,IAAI,UAAU,OAAO,IAAI;GAC5D,IAAI,OAAO,MAAM,kBAAkB,MAAM;GACzC,MAAM;;EAER,IAAI,wBAAwB,IAAI,UAAU,YACxC,IAAI,OAAO,KACT,iDAAiD,oBAAoB,mCAAmC,IAAI,UAAU,WAAW,gCAAgC,oBAAoB,oEAAoE,oBAAoB,wGAC9Q;EAGH,MAAM,WAAW,IAAI,SAAS,mBAAmB,qBAAqB,IAAI,OAAO,WAAW;EAC5F,SAAS,UAAU,IAAI,OAAO;EAE9B,MAAM,oBAAoB,uBACxB,YACA,IAAI,UAAU,OACd,+BAA+B,IAAI,UAAU,CAC9C;EAED,OAAO;GACL;GACA;GACA;GACA;GACA;GACD;;CAEJ;AAED,SAAgB,6BAA6B,SAAsD;CACjG,OAAO,uBAAuB,QAAQ,QAAQ"}
1
+ {"version":3,"file":"bootstrap.js","names":[],"sources":["../../services/bootstrap.ts"],"sourcesContent":["import { dirname, join } from \"node:path\";\nimport type { ClawdbotPluginApi } from \"openclaw/plugin-sdk/core\";\nimport { EdictStore } from \"../backends/edict-store.js\";\nimport { FactsDB } from \"../backends/facts-db.js\";\nimport { VectorDB } from \"../backends/vector-db.js\";\nimport type { BootstrapPhaseConfig, EmbeddingModelConfig, HybridMemoryConfig } from \"../config.js\";\nimport { type EmbeddingRegistry, buildEmbeddingRegistry } from \"./embedding-registry.js\";\nimport { type EmbeddingProvider, createEmbeddingProvider } from \"./embeddings.js\";\n\nexport interface CoreBootstrapContext {\n cfg: HybridMemoryConfig;\n api: ClawdbotPluginApi;\n resolvedSqlitePath: string;\n resolvedLancePath: string;\n}\n\nexport interface CoreBootstrapServices {\n factsDb: FactsDB;\n edictStore: EdictStore;\n vectorDb: VectorDB;\n embeddings: EmbeddingProvider;\n embeddingRegistry: EmbeddingRegistry;\n}\n\ntype CoreBootstrapInstaller = BootstrapPhaseConfig & {\n id: string;\n install(context: CoreBootstrapContext): CoreBootstrapServices;\n};\n\n/** Fail fast before LanceDB when the active embedding provider reports invalid width (#944). Exported for tests. */\nexport function assertEmbeddingDimensionsForVectorDb(dim: number): void {\n if (!Number.isFinite(dim) || !Number.isInteger(dim) || dim <= 0) {\n throw new Error(\n `Invalid embedding dimensions: ${String(dim)}. Set embedding.dimensions to a positive integer matching your provider output.`,\n );\n }\n}\n\nfunction resolveEmbeddingRegistryModels(\n embedding: HybridMemoryConfig[\"embedding\"],\n): EmbeddingModelConfig[] | undefined {\n if (Array.isArray(embedding.multiModels) && embedding.multiModels.length > 0) {\n return embedding.multiModels;\n }\n const rawModels = (embedding as unknown as { models?: unknown }).models;\n if (!Array.isArray(rawModels) || rawModels.length === 0) return undefined;\n const hasObjectModels = rawModels.every((item) => item && typeof item === \"object\");\n if (!hasObjectModels) return undefined;\n return rawModels as EmbeddingModelConfig[];\n}\n\nexport const coreBootstrapInstaller: CoreBootstrapInstaller = {\n id: \"memoryCore\",\n bootstrapPhase: \"core\",\n install({ cfg, api, resolvedSqlitePath, resolvedLancePath }) {\n let factsDb: FactsDB;\n try {\n factsDb = new FactsDB(resolvedSqlitePath, {\n fuzzyDedupe: cfg.store.fuzzyDedupe,\n storeConfig: cfg.store,\n });\n } catch (err) {\n api.logger.error(`memory-hybrid: core bootstrap failed: ${err instanceof Error ? err.message : String(err)}`);\n throw err;\n }\n\n // EdictStore: separate SQLite file alongside facts DB, for verified ground-truth facts\n const edictStorePath = join(dirname(resolvedSqlitePath), \"edicts.db\");\n const edictStore = new EdictStore(edictStorePath);\n\n // Instantiate EmbeddingProvider FIRST so VectorDB always uses the actual runtime\n // dimensions. When a ChainEmbeddingProvider is active (e.g. Google in chain with\n // OpenAI model), the chain may output different dimensions (768) than the catalog\n // model dimensions (3072). Using cfg.embedding.dimensions directly caused a silent\n // mismatch where VectorDB.search() returned [] on every query. Issue #939.\n const embeddings = createEmbeddingProvider(cfg.embedding, (err) => {\n api.logger.warn(\n `memory-hybrid: ${cfg.embedding.provider} embedding unavailable (${err}), switching to OpenAI fallback`,\n );\n });\n\n const effectiveDimensions = embeddings.dimensions;\n try {\n assertEmbeddingDimensionsForVectorDb(effectiveDimensions);\n } catch (err) {\n const msg = err instanceof Error ? err.message : String(err);\n api.logger.error(`memory-hybrid: ${msg}`);\n throw err;\n }\n if (effectiveDimensions !== cfg.embedding.dimensions) {\n api.logger.warn(\n `memory-hybrid: embedding provider dimensions (${effectiveDimensions}) differ from config dimensions (${cfg.embedding.dimensions}). Using provider dimensions (${effectiveDimensions}) for LanceDB. To silence this warning, set embedding.dimensions: ${effectiveDimensions} in plugin config, or set embedding.preferredProviders explicitly to control which providers are used.`,\n );\n }\n\n const vectorDb = new VectorDB(resolvedLancePath, effectiveDimensions, cfg.vector.autoRepair);\n vectorDb.setLogger(api.logger);\n\n const embeddingRegistry = buildEmbeddingRegistry(\n embeddings,\n cfg.embedding.model,\n resolveEmbeddingRegistryModels(cfg.embedding),\n );\n\n return {\n factsDb,\n edictStore,\n vectorDb,\n embeddings,\n embeddingRegistry,\n };\n },\n};\n\nexport function installCoreBootstrapServices(context: CoreBootstrapContext): CoreBootstrapServices {\n return coreBootstrapInstaller.install(context);\n}\n"],"mappings":";;;;;;;;;;;AA8BA,SAAgB,qCAAqC,KAAmB;CACtE,IAAI,CAAC,OAAO,SAAS,GAAG,KAAK,CAAC,OAAO,UAAU,GAAG,KAAK,OAAO,GAC5D,MAAM,IAAI,MACR,iCAAiC,OAAO,GAAG,EAAE,gFAC/C;AAEJ;AAEA,SAAS,+BACP,WACoC;CACpC,IAAI,MAAM,QAAQ,UAAU,WAAW,KAAK,UAAU,YAAY,SAAS,GACzE,OAAO,UAAU;CAEnB,MAAM,YAAa,UAA8C;CACjE,IAAI,CAAC,MAAM,QAAQ,SAAS,KAAK,UAAU,WAAW,GAAG,OAAO,KAAA;CAEhE,IAAI,CADoB,UAAU,OAAO,SAAS,QAAQ,OAAO,SAAS,QACvD,GAAG,OAAO,KAAA;CAC7B,OAAO;AACT;AAEA,MAAa,yBAAiD;CAC5D,IAAI;CACJ,gBAAgB;CAChB,QAAQ,EAAE,KAAK,KAAK,oBAAoB,qBAAqB;EAC3D,IAAI;EACJ,IAAI;GACF,UAAU,IAAI,QAAQ,oBAAoB;IACxC,aAAa,IAAI,MAAM;IACvB,aAAa,IAAI;GACnB,CAAC;EACH,SAAS,KAAK;GACZ,IAAI,OAAO,MAAM,yCAAyC,eAAe,QAAQ,IAAI,UAAU,OAAO,GAAG,GAAG;GAC5G,MAAM;EACR;EAIA,MAAM,aAAa,IAAI,WADA,KAAK,QAAQ,kBAAkB,GAAG,WACV,CAAC;EAOhD,MAAM,aAAa,wBAAwB,IAAI,YAAY,QAAQ;GACjE,IAAI,OAAO,KACT,kBAAkB,IAAI,UAAU,SAAS,0BAA0B,IAAI,gCACzE;EACF,CAAC;EAED,MAAM,sBAAsB,WAAW;EACvC,IAAI;GACF,qCAAqC,mBAAmB;EAC1D,SAAS,KAAK;GACZ,MAAM,MAAM,eAAe,QAAQ,IAAI,UAAU,OAAO,GAAG;GAC3D,IAAI,OAAO,MAAM,kBAAkB,KAAK;GACxC,MAAM;EACR;EACA,IAAI,wBAAwB,IAAI,UAAU,YACxC,IAAI,OAAO,KACT,iDAAiD,oBAAoB,mCAAmC,IAAI,UAAU,WAAW,gCAAgC,oBAAoB,oEAAoE,oBAAoB,uGAC/Q;EAGF,MAAM,WAAW,IAAI,SAAS,mBAAmB,qBAAqB,IAAI,OAAO,UAAU;EAC3F,SAAS,UAAU,IAAI,MAAM;EAE7B,MAAM,oBAAoB,uBACxB,YACA,IAAI,UAAU,OACd,+BAA+B,IAAI,SAAS,CAC9C;EAEA,OAAO;GACL;GACA;GACA;GACA;GACA;EACF;CACF;AACF;AAEA,SAAgB,6BAA6B,SAAsD;CACjG,OAAO,uBAAuB,QAAQ,OAAO;AAC/C"}
@@ -1 +1 @@
1
- {"version":3,"file":"capture-provenance.js","names":[],"sources":["../../services/capture-provenance.ts"],"sourcesContent":["import type { ClawdbotPluginApi } from \"openclaw/plugin-sdk/core\";\n\ntype CaptureOrigin = \"interactive\" | \"system\" | \"cron\";\n\ninterface CaptureProvenance {\n origin: CaptureOrigin;\n messageChannel: string | null;\n sessionId: string | null;\n shouldAutoCapture: boolean;\n reason?: string;\n}\n\ntype CaptureEvent = {\n prompt?: unknown;\n messages?: unknown[];\n};\n\nconst CRON_PROMPT_PATTERNS = [\n /GUARD CHECK \\(issue #305\\)/i,\n /Nightly memory maintenance\\./i,\n /Run self-correction analysis:/i,\n];\n\nfunction normalizeMessageChannel(channel: unknown): string | null {\n if (typeof channel !== \"string\") return null;\n const normalized = channel.trim().toLowerCase();\n return normalized ? normalized : null;\n}\n\nfunction extractEventText(event: unknown): string[] {\n const texts: string[] = [];\n const ev = (event ?? {}) as CaptureEvent;\n\n if (typeof ev.prompt === \"string\" && ev.prompt.trim()) {\n texts.push(ev.prompt);\n }\n\n for (const msg of ev.messages ?? []) {\n if (!msg || typeof msg !== \"object\") continue;\n const content = (msg as { content?: unknown }).content;\n if (typeof content === \"string\" && content.trim()) {\n texts.push(content);\n continue;\n }\n if (!Array.isArray(content)) continue;\n for (const block of content) {\n if (!block || typeof block !== \"object\") continue;\n if ((block as { type?: unknown }).type !== \"text\") continue;\n const text = (block as { text?: unknown }).text;\n if (typeof text === \"string\" && text.trim()) {\n texts.push(text);\n }\n }\n }\n\n return texts;\n}\n\nfunction matchesCronPrompt(texts: string[]): boolean {\n return texts.some((text) => CRON_PROMPT_PATTERNS.some((pattern) => pattern.test(text)));\n}\n\nexport function resolveCaptureProvenance(\n event: unknown,\n api: Pick<ClawdbotPluginApi, \"context\">,\n sessionId?: string | null,\n): CaptureProvenance {\n const messageChannel = normalizeMessageChannel(api.context?.messageChannel);\n const texts = extractEventText(event);\n const hasCronPrompt = matchesCronPrompt(texts);\n\n if (messageChannel === \"system\" && hasCronPrompt) {\n return {\n origin: \"cron\",\n messageChannel,\n sessionId: sessionId ?? api.context?.sessionId ?? null,\n shouldAutoCapture: false,\n reason: \"system cron session\",\n };\n }\n\n if (messageChannel === \"system\") {\n return {\n origin: \"system\",\n messageChannel,\n sessionId: sessionId ?? api.context?.sessionId ?? null,\n shouldAutoCapture: false,\n reason: \"non-interactive system channel\",\n };\n }\n\n if (hasCronPrompt) {\n return {\n origin: \"cron\",\n messageChannel,\n sessionId: sessionId ?? api.context?.sessionId ?? null,\n shouldAutoCapture: false,\n reason: \"detected cron maintenance prompt\",\n };\n }\n\n return {\n origin: \"interactive\",\n messageChannel,\n sessionId: sessionId ?? api.context?.sessionId ?? null,\n shouldAutoCapture: true,\n };\n}\n\nexport function getAutoCaptureExtractionMethod(\n role: \"user\" | \"assistant\",\n provenance: Pick<CaptureProvenance, \"origin\">,\n): string {\n return `auto-capture:${role}:${provenance.origin}`;\n}\n\nexport function getAutoCaptureExtractionConfidence(role: \"user\" | \"assistant\"): number {\n return role === \"user\" ? 1.0 : 0.7;\n}\n"],"mappings":";AAiBA,MAAM,uBAAuB;CAC3B;CACA;CACA;CACD;AAED,SAAS,wBAAwB,SAAiC;CAChE,IAAI,OAAO,YAAY,UAAU,OAAO;CACxC,MAAM,aAAa,QAAQ,MAAM,CAAC,aAAa;CAC/C,OAAO,aAAa,aAAa;;AAGnC,SAAS,iBAAiB,OAA0B;CAClD,MAAM,QAAkB,EAAE;CAC1B,MAAM,KAAM,SAAS,EAAE;CAEvB,IAAI,OAAO,GAAG,WAAW,YAAY,GAAG,OAAO,MAAM,EACnD,MAAM,KAAK,GAAG,OAAO;CAGvB,KAAK,MAAM,OAAO,GAAG,YAAY,EAAE,EAAE;EACnC,IAAI,CAAC,OAAO,OAAO,QAAQ,UAAU;EACrC,MAAM,UAAW,IAA8B;EAC/C,IAAI,OAAO,YAAY,YAAY,QAAQ,MAAM,EAAE;GACjD,MAAM,KAAK,QAAQ;GACnB;;EAEF,IAAI,CAAC,MAAM,QAAQ,QAAQ,EAAE;EAC7B,KAAK,MAAM,SAAS,SAAS;GAC3B,IAAI,CAAC,SAAS,OAAO,UAAU,UAAU;GACzC,IAAK,MAA6B,SAAS,QAAQ;GACnD,MAAM,OAAQ,MAA6B;GAC3C,IAAI,OAAO,SAAS,YAAY,KAAK,MAAM,EACzC,MAAM,KAAK,KAAK;;;CAKtB,OAAO;;AAGT,SAAS,kBAAkB,OAA0B;CACnD,OAAO,MAAM,MAAM,SAAS,qBAAqB,MAAM,YAAY,QAAQ,KAAK,KAAK,CAAC,CAAC;;AAGzF,SAAgB,yBACd,OACA,KACA,WACmB;CACnB,MAAM,iBAAiB,wBAAwB,IAAI,SAAS,eAAe;CAE3E,MAAM,gBAAgB,kBADR,iBAAiB,MACc,CAAC;CAE9C,IAAI,mBAAmB,YAAY,eACjC,OAAO;EACL,QAAQ;EACR;EACA,WAAW,aAAa,IAAI,SAAS,aAAa;EAClD,mBAAmB;EACnB,QAAQ;EACT;CAGH,IAAI,mBAAmB,UACrB,OAAO;EACL,QAAQ;EACR;EACA,WAAW,aAAa,IAAI,SAAS,aAAa;EAClD,mBAAmB;EACnB,QAAQ;EACT;CAGH,IAAI,eACF,OAAO;EACL,QAAQ;EACR;EACA,WAAW,aAAa,IAAI,SAAS,aAAa;EAClD,mBAAmB;EACnB,QAAQ;EACT;CAGH,OAAO;EACL,QAAQ;EACR;EACA,WAAW,aAAa,IAAI,SAAS,aAAa;EAClD,mBAAmB;EACpB;;AAGH,SAAgB,+BACd,MACA,YACQ;CACR,OAAO,gBAAgB,KAAK,GAAG,WAAW;;AAG5C,SAAgB,mCAAmC,MAAoC;CACrF,OAAO,SAAS,SAAS,IAAM"}
1
+ {"version":3,"file":"capture-provenance.js","names":[],"sources":["../../services/capture-provenance.ts"],"sourcesContent":["import type { ClawdbotPluginApi } from \"openclaw/plugin-sdk/core\";\n\ntype CaptureOrigin = \"interactive\" | \"system\" | \"cron\";\n\ninterface CaptureProvenance {\n origin: CaptureOrigin;\n messageChannel: string | null;\n sessionId: string | null;\n shouldAutoCapture: boolean;\n reason?: string;\n}\n\ntype CaptureEvent = {\n prompt?: unknown;\n messages?: unknown[];\n};\n\nconst CRON_PROMPT_PATTERNS = [\n /GUARD CHECK \\(issue #305\\)/i,\n /Nightly memory maintenance\\./i,\n /Run self-correction analysis:/i,\n];\n\nfunction normalizeMessageChannel(channel: unknown): string | null {\n if (typeof channel !== \"string\") return null;\n const normalized = channel.trim().toLowerCase();\n return normalized ? normalized : null;\n}\n\nfunction extractEventText(event: unknown): string[] {\n const texts: string[] = [];\n const ev = (event ?? {}) as CaptureEvent;\n\n if (typeof ev.prompt === \"string\" && ev.prompt.trim()) {\n texts.push(ev.prompt);\n }\n\n for (const msg of ev.messages ?? []) {\n if (!msg || typeof msg !== \"object\") continue;\n const content = (msg as { content?: unknown }).content;\n if (typeof content === \"string\" && content.trim()) {\n texts.push(content);\n continue;\n }\n if (!Array.isArray(content)) continue;\n for (const block of content) {\n if (!block || typeof block !== \"object\") continue;\n if ((block as { type?: unknown }).type !== \"text\") continue;\n const text = (block as { text?: unknown }).text;\n if (typeof text === \"string\" && text.trim()) {\n texts.push(text);\n }\n }\n }\n\n return texts;\n}\n\nfunction matchesCronPrompt(texts: string[]): boolean {\n return texts.some((text) => CRON_PROMPT_PATTERNS.some((pattern) => pattern.test(text)));\n}\n\nexport function resolveCaptureProvenance(\n event: unknown,\n api: Pick<ClawdbotPluginApi, \"context\">,\n sessionId?: string | null,\n): CaptureProvenance {\n const messageChannel = normalizeMessageChannel(api.context?.messageChannel);\n const texts = extractEventText(event);\n const hasCronPrompt = matchesCronPrompt(texts);\n\n if (messageChannel === \"system\" && hasCronPrompt) {\n return {\n origin: \"cron\",\n messageChannel,\n sessionId: sessionId ?? api.context?.sessionId ?? null,\n shouldAutoCapture: false,\n reason: \"system cron session\",\n };\n }\n\n if (messageChannel === \"system\") {\n return {\n origin: \"system\",\n messageChannel,\n sessionId: sessionId ?? api.context?.sessionId ?? null,\n shouldAutoCapture: false,\n reason: \"non-interactive system channel\",\n };\n }\n\n if (hasCronPrompt) {\n return {\n origin: \"cron\",\n messageChannel,\n sessionId: sessionId ?? api.context?.sessionId ?? null,\n shouldAutoCapture: false,\n reason: \"detected cron maintenance prompt\",\n };\n }\n\n return {\n origin: \"interactive\",\n messageChannel,\n sessionId: sessionId ?? api.context?.sessionId ?? null,\n shouldAutoCapture: true,\n };\n}\n\nexport function getAutoCaptureExtractionMethod(\n role: \"user\" | \"assistant\",\n provenance: Pick<CaptureProvenance, \"origin\">,\n): string {\n return `auto-capture:${role}:${provenance.origin}`;\n}\n\nexport function getAutoCaptureExtractionConfidence(role: \"user\" | \"assistant\"): number {\n return role === \"user\" ? 1.0 : 0.7;\n}\n"],"mappings":";AAiBA,MAAM,uBAAuB;CAC3B;CACA;CACA;AACF;AAEA,SAAS,wBAAwB,SAAiC;CAChE,IAAI,OAAO,YAAY,UAAU,OAAO;CACxC,MAAM,aAAa,QAAQ,KAAK,EAAE,YAAY;CAC9C,OAAO,aAAa,aAAa;AACnC;AAEA,SAAS,iBAAiB,OAA0B;CAClD,MAAM,QAAkB,CAAC;CACzB,MAAM,KAAM,SAAS,CAAC;CAEtB,IAAI,OAAO,GAAG,WAAW,YAAY,GAAG,OAAO,KAAK,GAClD,MAAM,KAAK,GAAG,MAAM;CAGtB,KAAK,MAAM,OAAO,GAAG,YAAY,CAAC,GAAG;EACnC,IAAI,CAAC,OAAO,OAAO,QAAQ,UAAU;EACrC,MAAM,UAAW,IAA8B;EAC/C,IAAI,OAAO,YAAY,YAAY,QAAQ,KAAK,GAAG;GACjD,MAAM,KAAK,OAAO;GAClB;EACF;EACA,IAAI,CAAC,MAAM,QAAQ,OAAO,GAAG;EAC7B,KAAK,MAAM,SAAS,SAAS;GAC3B,IAAI,CAAC,SAAS,OAAO,UAAU,UAAU;GACzC,IAAK,MAA6B,SAAS,QAAQ;GACnD,MAAM,OAAQ,MAA6B;GAC3C,IAAI,OAAO,SAAS,YAAY,KAAK,KAAK,GACxC,MAAM,KAAK,IAAI;EAEnB;CACF;CAEA,OAAO;AACT;AAEA,SAAS,kBAAkB,OAA0B;CACnD,OAAO,MAAM,MAAM,SAAS,qBAAqB,MAAM,YAAY,QAAQ,KAAK,IAAI,CAAC,CAAC;AACxF;AAEA,SAAgB,yBACd,OACA,KACA,WACmB;CACnB,MAAM,iBAAiB,wBAAwB,IAAI,SAAS,cAAc;CAE1E,MAAM,gBAAgB,kBADR,iBAAiB,KACa,CAAC;CAE7C,IAAI,mBAAmB,YAAY,eACjC,OAAO;EACL,QAAQ;EACR;EACA,WAAW,aAAa,IAAI,SAAS,aAAa;EAClD,mBAAmB;EACnB,QAAQ;CACV;CAGF,IAAI,mBAAmB,UACrB,OAAO;EACL,QAAQ;EACR;EACA,WAAW,aAAa,IAAI,SAAS,aAAa;EAClD,mBAAmB;EACnB,QAAQ;CACV;CAGF,IAAI,eACF,OAAO;EACL,QAAQ;EACR;EACA,WAAW,aAAa,IAAI,SAAS,aAAa;EAClD,mBAAmB;EACnB,QAAQ;CACV;CAGF,OAAO;EACL,QAAQ;EACR;EACA,WAAW,aAAa,IAAI,SAAS,aAAa;EAClD,mBAAmB;CACrB;AACF;AAEA,SAAgB,+BACd,MACA,YACQ;CACR,OAAO,gBAAgB,KAAK,GAAG,WAAW;AAC5C;AAEA,SAAgB,mCAAmC,MAAoC;CACrF,OAAO,SAAS,SAAS,IAAM;AACjC"}