@vellumai/assistant 0.5.16 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (407) hide show
  1. package/ARCHITECTURE.md +1 -1
  2. package/Dockerfile +0 -3
  3. package/knip.json +2 -1
  4. package/openapi.yaml +660 -80
  5. package/package.json +1 -1
  6. package/src/__tests__/actor-token-service.test.ts +68 -0
  7. package/src/__tests__/agent-loop.test.ts +0 -32
  8. package/src/__tests__/always-loaded-tools-guard.test.ts +2 -2
  9. package/src/__tests__/anthropic-provider.test.ts +57 -3
  10. package/src/__tests__/app-compiler.test.ts +120 -0
  11. package/src/__tests__/assistant-feature-flags-integration.test.ts +2 -2
  12. package/src/__tests__/call-conversation-messages.test.ts +2 -6
  13. package/src/__tests__/call-domain.test.ts +2 -6
  14. package/src/__tests__/call-pointer-messages.test.ts +2 -14
  15. package/src/__tests__/call-recovery.test.ts +2 -6
  16. package/src/__tests__/call-routes-http.test.ts +2 -6
  17. package/src/__tests__/call-store.test.ts +2 -6
  18. package/src/__tests__/cancel-resolves-conversation-key.test.ts +2 -6
  19. package/src/__tests__/canonical-guardian-store.test.ts +2 -6
  20. package/src/__tests__/channel-delivery-store.test.ts +2 -6
  21. package/src/__tests__/channel-retry-sweep.test.ts +2 -6
  22. package/src/__tests__/checker.test.ts +25 -3
  23. package/src/__tests__/clawhub.test.ts +54 -24
  24. package/src/__tests__/cli-command-risk-guard.test.ts +14 -0
  25. package/src/__tests__/cli-memory.test.ts +74 -69
  26. package/src/__tests__/config-schema.test.ts +1 -1
  27. package/src/__tests__/config-set-platform-guard.test.ts +302 -0
  28. package/src/__tests__/confirmation-request-guardian-bridge.test.ts +2 -6
  29. package/src/__tests__/contacts-tools.test.ts +31 -0
  30. package/src/__tests__/context-overflow-reducer.test.ts +86 -0
  31. package/src/__tests__/context-token-estimator.test.ts +175 -10
  32. package/src/__tests__/conversation-agent-loop-overflow.test.ts +9 -0
  33. package/src/__tests__/conversation-agent-loop.test.ts +9 -0
  34. package/src/__tests__/conversation-attachments.test.ts +2 -6
  35. package/src/__tests__/conversation-attention-store.test.ts +2 -6
  36. package/src/__tests__/conversation-clear-safety.test.ts +2 -6
  37. package/src/__tests__/conversation-delete-schedule-cleanup.test.ts +4 -10
  38. package/src/__tests__/conversation-disk-view-integration.test.ts +2 -6
  39. package/src/__tests__/conversation-disk-view.test.ts +2 -6
  40. package/src/__tests__/conversation-error.test.ts +33 -2
  41. package/src/__tests__/conversation-fork-crud.test.ts +2 -6
  42. package/src/__tests__/conversation-history-web-search.test.ts +5 -0
  43. package/src/__tests__/conversation-load-history-repair.test.ts +5 -1
  44. package/src/__tests__/conversation-media-retry.test.ts +91 -0
  45. package/src/__tests__/conversation-starter-routes.test.ts +20 -11
  46. package/src/__tests__/conversation-store.test.ts +2 -6
  47. package/src/__tests__/conversation-usage.test.ts +2 -6
  48. package/src/__tests__/conversation-wipe.test.ts +11 -408
  49. package/src/__tests__/credential-execution-feature-gates.test.ts +3 -3
  50. package/src/__tests__/credential-execution-shell-lockdown.test.ts +2 -2
  51. package/src/__tests__/credential-security-e2e.test.ts +2 -0
  52. package/src/__tests__/followup-tools.test.ts +2 -6
  53. package/src/__tests__/graph-extraction-event-date.test.ts +186 -0
  54. package/src/__tests__/guardian-action-conversation-turn.test.ts +2 -6
  55. package/src/__tests__/guardian-action-followup-executor.test.ts +2 -6
  56. package/src/__tests__/guardian-action-followup-store.test.ts +2 -6
  57. package/src/__tests__/guardian-action-grant-mint-consume.test.ts +2 -6
  58. package/src/__tests__/guardian-action-late-reply.test.ts +2 -6
  59. package/src/__tests__/guardian-action-store.test.ts +2 -6
  60. package/src/__tests__/guardian-binding-drift-heal.test.ts +2 -6
  61. package/src/__tests__/guardian-decision-primitive-canonical.test.ts +8 -8
  62. package/src/__tests__/guardian-dispatch.test.ts +2 -6
  63. package/src/__tests__/guardian-grant-minting.test.ts +2 -14
  64. package/src/__tests__/guardian-principal-id-roundtrip.test.ts +2 -6
  65. package/src/__tests__/guardian-routing-invariants.test.ts +192 -6
  66. package/src/__tests__/guardian-routing-state.test.ts +2 -6
  67. package/src/__tests__/guardian-verification-voice-binding.test.ts +2 -6
  68. package/src/__tests__/inbound-invite-redemption.test.ts +2 -6
  69. package/src/__tests__/injection-block.test.ts +154 -0
  70. package/src/__tests__/install-meta.test.ts +506 -0
  71. package/src/__tests__/install-skill-routing.test.ts +292 -0
  72. package/src/__tests__/invite-redemption-service.test.ts +2 -6
  73. package/src/__tests__/invite-routes-http.test.ts +2 -6
  74. package/src/__tests__/jobs-store-qdrant-breaker.test.ts +2 -14
  75. package/src/__tests__/list-messages-attachments.test.ts +2 -6
  76. package/src/__tests__/llm-context-route-provider.test.ts +2 -6
  77. package/src/__tests__/llm-request-log-turn-query.test.ts +2 -6
  78. package/src/__tests__/llm-usage-store.test.ts +2 -6
  79. package/src/__tests__/log-export-workspace.test.ts +2 -6
  80. package/src/__tests__/managed-store.test.ts +38 -11
  81. package/src/__tests__/memory-jobs-worker-backoff.test.ts +2 -8
  82. package/src/__tests__/memory-recall-log-store.test.ts +2 -6
  83. package/src/__tests__/memory-upsert-concurrency.test.ts +4 -112
  84. package/src/__tests__/non-member-access-request.test.ts +2 -6
  85. package/src/__tests__/notification-guardian-path.test.ts +2 -6
  86. package/src/__tests__/oauth-cli.test.ts +364 -2
  87. package/src/__tests__/oauth2-gateway-transport.test.ts +18 -3
  88. package/src/__tests__/outlook-attachments.test.ts +301 -0
  89. package/src/__tests__/outlook-automation-tools.test.ts +425 -0
  90. package/src/__tests__/outlook-categories.test.ts +212 -0
  91. package/src/__tests__/outlook-client-automation.test.ts +246 -0
  92. package/src/__tests__/outlook-compose-tools.test.ts +325 -0
  93. package/src/__tests__/outlook-declutter-tools.test.ts +585 -0
  94. package/src/__tests__/outlook-email-watcher.test.ts +322 -0
  95. package/src/__tests__/outlook-follow-up.test.ts +196 -0
  96. package/src/__tests__/outlook-messaging-provider.test.ts +498 -3
  97. package/src/__tests__/outlook-trash.test.ts +77 -0
  98. package/src/__tests__/outlook-unsubscribe.test.ts +250 -0
  99. package/src/__tests__/platform-callback-registration.test.ts +4 -4
  100. package/src/__tests__/playbook-execution.test.ts +76 -80
  101. package/src/__tests__/playbook-tools.test.ts +5 -7
  102. package/src/__tests__/provider-error-scenarios.test.ts +21 -0
  103. package/src/__tests__/rebuild-index-graph-nodes.test.ts +273 -0
  104. package/src/__tests__/registry.test.ts +2 -2
  105. package/src/__tests__/require-fresh-approval.test.ts +64 -2
  106. package/src/__tests__/runtime-events-sse-parity.test.ts +2 -6
  107. package/src/__tests__/runtime-events-sse.test.ts +2 -6
  108. package/src/__tests__/schedule-store.test.ts +2 -6
  109. package/src/__tests__/schedule-tools.test.ts +2 -6
  110. package/src/__tests__/scheduler-recurrence.test.ts +1 -5
  111. package/src/__tests__/scoped-approval-grants.test.ts +2 -6
  112. package/src/__tests__/scoped-grant-security-matrix.test.ts +2 -6
  113. package/src/__tests__/search-skills-unified.test.ts +421 -0
  114. package/src/__tests__/secret-onetime-send.test.ts +2 -0
  115. package/src/__tests__/send-endpoint-busy.test.ts +2 -6
  116. package/src/__tests__/sequence-store.test.ts +2 -6
  117. package/src/__tests__/server-history-render.test.ts +2 -6
  118. package/src/__tests__/skill-feature-flags-integration.test.ts +38 -31
  119. package/src/__tests__/skill-feature-flags.test.ts +6 -6
  120. package/src/__tests__/skill-load-feature-flag.test.ts +11 -11
  121. package/src/__tests__/skill-memory.test.ts +140 -98
  122. package/src/__tests__/skills-uninstall.test.ts +2 -2
  123. package/src/__tests__/skills.test.ts +1 -1
  124. package/src/__tests__/slack-inbound-verification.test.ts +2 -6
  125. package/src/__tests__/task-compiler.test.ts +2 -6
  126. package/src/__tests__/task-management-tools.test.ts +2 -6
  127. package/src/__tests__/task-memory-cleanup.test.ts +173 -229
  128. package/src/__tests__/task-runner.test.ts +2 -6
  129. package/src/__tests__/task-scheduler.test.ts +2 -6
  130. package/src/__tests__/test-preload.ts +3 -0
  131. package/src/__tests__/tool-approval-handler.test.ts +2 -6
  132. package/src/__tests__/tool-grant-request-escalation.test.ts +2 -6
  133. package/src/__tests__/tool-side-effects-slack-dm.test.ts +276 -0
  134. package/src/__tests__/trust-store.test.ts +1 -1
  135. package/src/__tests__/trusted-contact-inline-approval-integration.test.ts +2 -6
  136. package/src/__tests__/trusted-contact-lifecycle-notifications.test.ts +2 -6
  137. package/src/__tests__/trusted-contact-multichannel.test.ts +2 -6
  138. package/src/__tests__/trusted-contact-verification.test.ts +2 -6
  139. package/src/__tests__/turn-boundary-resolution.test.ts +2 -6
  140. package/src/__tests__/usage-cache-backfill-migration.test.ts +1 -6
  141. package/src/__tests__/usage-routes.test.ts +2 -6
  142. package/src/__tests__/verification-control-plane-policy.test.ts +0 -2
  143. package/src/__tests__/voice-invite-redemption.test.ts +2 -6
  144. package/src/__tests__/voice-scoped-grant-consumer.test.ts +2 -6
  145. package/src/__tests__/voice-session-bridge.test.ts +2 -6
  146. package/src/__tests__/volume-security-guard.test.ts +2 -0
  147. package/src/__tests__/workspace-lifecycle.test.ts +29 -1
  148. package/src/__tests__/workspace-migration-009-backfill-conversation-disk-view.test.ts +2 -6
  149. package/src/__tests__/workspace-migration-013-repair-conversation-disk-view.test.ts +2 -6
  150. package/src/__tests__/workspace-migration-026-backfill-install-meta.test.ts +558 -0
  151. package/src/__tests__/workspace-policy.test.ts +1 -1
  152. package/src/agent/attachments.ts +7 -2
  153. package/src/agent/image-optimize.ts +165 -0
  154. package/src/agent/loop.ts +1 -15
  155. package/src/bundler/app-compiler.ts +179 -2
  156. package/src/bundler/package-resolver.ts +3 -5
  157. package/src/cli/__tests__/notifications.test.ts +1 -2
  158. package/src/cli/cli-memory.ts +67 -64
  159. package/src/cli/commands/avatar.ts +3 -3
  160. package/src/cli/commands/config.ts +26 -13
  161. package/src/cli/commands/doctor.ts +2 -2
  162. package/src/cli/commands/memory.ts +41 -55
  163. package/src/cli/commands/oauth/__tests__/connect.test.ts +2 -2
  164. package/src/cli/commands/oauth/__tests__/disconnect.test.ts +2 -2
  165. package/src/cli/commands/oauth/__tests__/mode.test.ts +8 -1
  166. package/src/cli/commands/oauth/__tests__/status.test.ts +2 -2
  167. package/src/cli/commands/oauth/connect.ts +11 -6
  168. package/src/cli/commands/oauth/mode.ts +7 -0
  169. package/src/cli/commands/oauth/shared.ts +39 -3
  170. package/src/cli/commands/platform/__tests__/connect.test.ts +1 -1
  171. package/src/cli/commands/platform/__tests__/disconnect.test.ts +1 -1
  172. package/src/cli/commands/platform/__tests__/status.test.ts +5 -5
  173. package/src/cli/commands/platform/index.ts +16 -16
  174. package/src/cli/commands/skills.ts +88 -16
  175. package/src/cli/commands/trust.ts +2 -2
  176. package/src/cli/lib/daemon-credential-client.ts +2 -3
  177. package/src/config/bundled-skills/acp/TOOLS.json +1 -1
  178. package/src/config/bundled-skills/contacts/SKILL.md +0 -1
  179. package/src/config/bundled-skills/contacts/TOOLS.json +0 -8
  180. package/src/config/bundled-skills/contacts/tools/contact-upsert.ts +0 -4
  181. package/src/config/bundled-skills/gmail/SKILL.md +2 -10
  182. package/src/config/bundled-skills/google-calendar/SKILL.md +1 -9
  183. package/src/config/bundled-skills/messaging/SKILL.md +10 -18
  184. package/src/config/bundled-skills/messaging/tools/messaging-analyze-style.ts +40 -33
  185. package/src/config/bundled-skills/outlook/SKILL.md +189 -0
  186. package/src/config/bundled-skills/outlook/TOOLS.json +530 -0
  187. package/src/config/bundled-skills/outlook/tools/outlook-attachments.ts +85 -0
  188. package/src/config/bundled-skills/outlook/tools/outlook-categories.ts +77 -0
  189. package/src/config/bundled-skills/outlook/tools/outlook-draft.ts +84 -0
  190. package/src/config/bundled-skills/outlook/tools/outlook-follow-up.ts +94 -0
  191. package/src/config/bundled-skills/outlook/tools/outlook-forward.ts +49 -0
  192. package/src/config/bundled-skills/outlook/tools/outlook-outreach-scan.ts +237 -0
  193. package/src/config/bundled-skills/outlook/tools/outlook-rules.ts +161 -0
  194. package/src/config/bundled-skills/outlook/tools/outlook-send-draft.ts +32 -0
  195. package/src/config/bundled-skills/outlook/tools/outlook-sender-digest.ts +272 -0
  196. package/src/config/bundled-skills/outlook/tools/outlook-trash.ts +29 -0
  197. package/src/config/bundled-skills/outlook/tools/outlook-unsubscribe.ts +129 -0
  198. package/src/config/bundled-skills/outlook/tools/outlook-vacation.ts +87 -0
  199. package/src/config/bundled-skills/outlook/tools/shared.ts +20 -0
  200. package/src/config/bundled-skills/outlook-calendar/SKILL.md +51 -0
  201. package/src/config/bundled-skills/outlook-calendar/TOOLS.json +221 -0
  202. package/src/config/bundled-skills/outlook-calendar/calendar-client.ts +252 -0
  203. package/src/config/bundled-skills/outlook-calendar/tools/outlook-calendar-check-availability.ts +53 -0
  204. package/src/config/bundled-skills/outlook-calendar/tools/outlook-calendar-create-event.ts +74 -0
  205. package/src/config/bundled-skills/outlook-calendar/tools/outlook-calendar-get-event.ts +18 -0
  206. package/src/config/bundled-skills/outlook-calendar/tools/outlook-calendar-list-events.ts +46 -0
  207. package/src/config/bundled-skills/outlook-calendar/tools/outlook-calendar-rsvp.ts +36 -0
  208. package/src/config/bundled-skills/outlook-calendar/tools/shared.ts +17 -0
  209. package/src/config/bundled-skills/outlook-calendar/types.ts +120 -0
  210. package/src/config/bundled-skills/playbooks/tools/playbook-create.ts +47 -40
  211. package/src/config/bundled-skills/playbooks/tools/playbook-delete.ts +16 -29
  212. package/src/config/bundled-skills/playbooks/tools/playbook-list.ts +16 -18
  213. package/src/config/bundled-skills/playbooks/tools/playbook-update.ts +39 -47
  214. package/src/config/bundled-skills/slack/SKILL.md +1 -7
  215. package/src/config/bundled-tool-registry.ts +56 -4
  216. package/src/config/env-registry.ts +15 -8
  217. package/src/config/feature-flag-registry.json +21 -124
  218. package/src/config/schemas/platform.ts +8 -0
  219. package/src/config/schemas/timeouts.ts +1 -1
  220. package/src/config/skills.ts +18 -7
  221. package/src/context/token-estimator.ts +25 -18
  222. package/src/context/window-manager.ts +6 -2
  223. package/src/credential-execution/process-manager.ts +3 -1
  224. package/src/daemon/context-overflow-reducer.ts +46 -2
  225. package/src/daemon/conversation-agent-loop-handlers.ts +123 -82
  226. package/src/daemon/conversation-agent-loop.ts +96 -61
  227. package/src/daemon/conversation-error.ts +31 -8
  228. package/src/daemon/conversation-lifecycle.ts +33 -0
  229. package/src/daemon/conversation-media-retry.ts +85 -7
  230. package/src/daemon/conversation-notifiers.ts +4 -1
  231. package/src/daemon/conversation-runtime-assembly.ts +5 -0
  232. package/src/daemon/conversation.ts +41 -2
  233. package/src/daemon/daemon-control.ts +8 -2
  234. package/src/daemon/handlers/shared.ts +22 -12
  235. package/src/daemon/handlers/skills.ts +416 -202
  236. package/src/daemon/lifecycle.ts +40 -1
  237. package/src/daemon/main.ts +5 -1
  238. package/src/daemon/message-types/conversations.ts +4 -1
  239. package/src/daemon/message-types/messages.ts +3 -1
  240. package/src/daemon/message-types/skills.ts +97 -36
  241. package/src/daemon/providers-setup.ts +5 -0
  242. package/src/daemon/server.ts +11 -2
  243. package/src/daemon/tool-side-effects.ts +27 -5
  244. package/src/heartbeat/heartbeat-service.ts +1 -0
  245. package/src/hooks/cli.ts +2 -2
  246. package/src/hooks/runner.ts +15 -38
  247. package/src/inbound/platform-callback-registration.ts +14 -14
  248. package/src/memory/admin.ts +11 -45
  249. package/src/memory/conversation-bootstrap.ts +2 -0
  250. package/src/memory/conversation-crud.ts +242 -348
  251. package/src/memory/conversation-group-migration.ts +157 -0
  252. package/src/memory/conversation-queries.ts +4 -2
  253. package/src/memory/db-init.ts +30 -3
  254. package/src/memory/embed.ts +73 -0
  255. package/src/memory/embedding-backend.ts +8 -14
  256. package/src/memory/embedding-runtime-manager.ts +12 -114
  257. package/src/memory/fingerprint.ts +2 -2
  258. package/src/memory/graph/bootstrap.ts +512 -0
  259. package/src/memory/graph/capability-seed.ts +297 -0
  260. package/src/memory/graph/consolidation.ts +691 -0
  261. package/src/memory/graph/conversation-graph-memory.ts +630 -0
  262. package/src/memory/graph/decay.test.ts +208 -0
  263. package/src/memory/graph/decay.ts +195 -0
  264. package/src/memory/graph/extraction-job.ts +69 -0
  265. package/src/memory/graph/extraction.test.ts +936 -0
  266. package/src/memory/graph/extraction.ts +1254 -0
  267. package/src/memory/graph/graph-search.ts +266 -0
  268. package/src/memory/graph/image-ref-utils.ts +29 -0
  269. package/src/memory/graph/injection.test.ts +513 -0
  270. package/src/memory/graph/injection.ts +439 -0
  271. package/src/memory/graph/inspect.ts +534 -0
  272. package/src/memory/graph/narrative.ts +267 -0
  273. package/src/memory/graph/pattern-scan.ts +269 -0
  274. package/src/memory/graph/retriever.ts +1008 -0
  275. package/src/memory/graph/scoring.test.ts +548 -0
  276. package/src/memory/graph/scoring.ts +232 -0
  277. package/src/memory/graph/serendipity.ts +65 -0
  278. package/src/memory/graph/store.test.ts +1050 -0
  279. package/src/memory/graph/store.ts +699 -0
  280. package/src/memory/graph/tool-handlers.ts +426 -0
  281. package/src/memory/graph/tools.ts +141 -0
  282. package/src/memory/graph/triggers.test.ts +487 -0
  283. package/src/memory/graph/triggers.ts +223 -0
  284. package/src/memory/graph/types.ts +271 -0
  285. package/src/memory/group-crud.ts +191 -0
  286. package/src/memory/indexer.ts +37 -19
  287. package/src/memory/job-handlers/cleanup.ts +0 -53
  288. package/src/memory/job-handlers/conversation-starters.ts +91 -53
  289. package/src/memory/job-handlers/embedding.ts +5 -31
  290. package/src/memory/job-handlers/index-maintenance.ts +23 -11
  291. package/src/memory/job-handlers/summarization.ts +32 -17
  292. package/src/memory/job-utils.ts +1 -1
  293. package/src/memory/jobs-store.ts +50 -70
  294. package/src/memory/jobs-worker.ts +147 -112
  295. package/src/memory/message-content.ts +1 -0
  296. package/src/memory/migrations/202-memory-graph-tables.ts +130 -0
  297. package/src/memory/migrations/203-drop-memory-items-tables.ts +23 -0
  298. package/src/memory/migrations/204-rename-memory-graph-type-values.ts +46 -0
  299. package/src/memory/migrations/205-memory-graph-image-refs.ts +11 -0
  300. package/src/memory/migrations/index.ts +4 -0
  301. package/src/memory/migrations/registry.ts +8 -0
  302. package/src/memory/qdrant-client.ts +44 -17
  303. package/src/memory/schema/index.ts +1 -0
  304. package/src/memory/schema/memory-graph.ts +139 -0
  305. package/src/memory/search/semantic.ts +47 -91
  306. package/src/memory/task-memory-cleanup.ts +28 -50
  307. package/src/messaging/providers/outlook/adapter.ts +8 -1
  308. package/src/messaging/providers/outlook/client.ts +299 -0
  309. package/src/messaging/providers/outlook/types.ts +118 -0
  310. package/src/notifications/adapters/macos.ts +1 -0
  311. package/src/notifications/copy-composer.ts +9 -0
  312. package/src/notifications/signal.ts +16 -0
  313. package/src/oauth/seed-providers.ts +2 -1
  314. package/src/permissions/checker.ts +24 -3
  315. package/src/permissions/defaults.ts +4 -4
  316. package/src/permissions/workspace-policy.ts +1 -1
  317. package/src/playbooks/playbook-compiler.ts +19 -18
  318. package/src/playbooks/types.ts +4 -3
  319. package/src/prompts/system-prompt.ts +3 -29
  320. package/src/providers/anthropic/client.ts +47 -19
  321. package/src/providers/gemini/client.ts +1 -1
  322. package/src/providers/openai/client.ts +1 -1
  323. package/src/providers/registry.ts +1 -1
  324. package/src/providers/retry.ts +19 -3
  325. package/src/runtime/actor-trust-resolver.ts +5 -1
  326. package/src/runtime/auth/route-policy.ts +7 -0
  327. package/src/runtime/guardian-reply-router.ts +5 -1
  328. package/src/runtime/http-server.ts +23 -3
  329. package/src/runtime/middleware/auth.ts +20 -0
  330. package/src/runtime/routes/attachment-routes.test.ts +106 -0
  331. package/src/runtime/routes/attachment-routes.ts +106 -16
  332. package/src/runtime/routes/brain-graph-routes.ts +21 -22
  333. package/src/runtime/routes/btw-routes.ts +8 -0
  334. package/src/runtime/routes/conversation-management-routes.ts +2 -0
  335. package/src/runtime/routes/conversation-starter-routes.ts +2 -2
  336. package/src/runtime/routes/debug-routes.ts +1 -1
  337. package/src/runtime/routes/global-search-routes.ts +21 -19
  338. package/src/runtime/routes/group-routes.ts +207 -0
  339. package/src/runtime/routes/guardian-action-routes.ts +21 -10
  340. package/src/runtime/routes/guardian-bootstrap-routes.ts +23 -19
  341. package/src/runtime/routes/inbound-message-handler.ts +19 -0
  342. package/src/runtime/routes/inbound-stages/guardian-activation-intercept.test.ts +292 -0
  343. package/src/runtime/routes/inbound-stages/guardian-activation-intercept.ts +207 -0
  344. package/src/runtime/routes/memory-item-routes.test.ts +2 -14
  345. package/src/runtime/routes/memory-item-routes.ts +341 -388
  346. package/src/runtime/routes/schedule-routes.ts +2 -0
  347. package/src/runtime/routes/skills-routes.ts +103 -37
  348. package/src/runtime/routes/work-items-routes.test.ts +2 -6
  349. package/src/schedule/scheduler.ts +8 -1
  350. package/src/security/oauth2.ts +1 -1
  351. package/src/security/secure-keys.ts +4 -8
  352. package/src/shared/provider-env-vars.ts +19 -0
  353. package/src/skills/catalog-cache.ts +5 -0
  354. package/src/skills/catalog-install.ts +15 -14
  355. package/src/skills/clawhub.ts +134 -154
  356. package/src/skills/install-meta.ts +208 -0
  357. package/src/skills/managed-store.ts +27 -16
  358. package/src/skills/skill-memory.ts +152 -77
  359. package/src/skills/skillssh-registry.ts +19 -17
  360. package/src/tasks/task-runner.ts +3 -1
  361. package/src/telemetry/usage-telemetry-reporter.test.ts +3 -5
  362. package/src/tools/browser/runtime-check.ts +3 -1
  363. package/src/tools/memory/register.ts +63 -46
  364. package/src/tools/permission-checker.ts +7 -1
  365. package/src/tools/shared/filesystem/image-read.ts +22 -85
  366. package/src/tools/terminal/safe-env.ts +1 -0
  367. package/src/tools/tool-manifest.ts +3 -3
  368. package/src/util/browser.ts +25 -10
  369. package/src/util/bun-runtime.ts +172 -0
  370. package/src/watcher/providers/outlook-calendar.ts +343 -0
  371. package/src/watcher/providers/outlook.ts +198 -0
  372. package/src/workspace/migrations/025-remove-oauth-app-setup-skills.ts +76 -0
  373. package/src/workspace/migrations/026-backfill-install-meta.ts +325 -0
  374. package/src/workspace/migrations/027-remove-orphaned-optimized-images-cache.ts +42 -0
  375. package/src/workspace/migrations/registry.ts +6 -0
  376. package/src/__tests__/context-memory-e2e.test.ts +0 -415
  377. package/src/__tests__/journal-context.test.ts +0 -268
  378. package/src/__tests__/memory-context-benchmark.benchmark.test.ts +0 -297
  379. package/src/__tests__/memory-lifecycle-e2e.test.ts +0 -459
  380. package/src/__tests__/memory-query-builder.test.ts +0 -59
  381. package/src/__tests__/memory-recall-quality.test.ts +0 -1046
  382. package/src/__tests__/memory-regressions.experimental.test.ts +0 -629
  383. package/src/__tests__/memory-regressions.test.ts +0 -3696
  384. package/src/__tests__/memory-retrieval.benchmark.test.ts +0 -295
  385. package/src/daemon/conversation-memory.ts +0 -207
  386. package/src/memory/conversation-starters-cadence.ts +0 -74
  387. package/src/memory/items-extractor.ts +0 -860
  388. package/src/memory/job-handlers/batch-extraction.ts +0 -753
  389. package/src/memory/job-handlers/extraction.ts +0 -40
  390. package/src/memory/job-handlers/journal-carry-forward.test.ts +0 -355
  391. package/src/memory/job-handlers/journal-carry-forward.ts +0 -255
  392. package/src/memory/journal-memory.ts +0 -224
  393. package/src/memory/query-builder.ts +0 -47
  394. package/src/memory/query-expansion.ts +0 -83
  395. package/src/memory/retriever.test.ts +0 -1592
  396. package/src/memory/retriever.ts +0 -1331
  397. package/src/memory/search/formatting.test.ts +0 -140
  398. package/src/memory/search/formatting.ts +0 -262
  399. package/src/memory/search/mmr.ts +0 -139
  400. package/src/memory/search/ranking.ts +0 -15
  401. package/src/memory/search/staleness.ts +0 -40
  402. package/src/memory/search/tier-classifier.ts +0 -18
  403. package/src/memory/search/types.ts +0 -121
  404. package/src/prompts/journal-context.ts +0 -154
  405. package/src/tools/memory/definitions.ts +0 -69
  406. package/src/tools/memory/handlers.test.ts +0 -562
  407. package/src/tools/memory/handlers.ts +0 -434
@@ -1,14 +1,13 @@
1
- import {
2
- existsSync,
3
- readdirSync,
4
- readFileSync,
5
- statSync,
6
- writeFileSync,
7
- } from "node:fs";
1
+ import { existsSync, readFileSync } from "node:fs";
8
2
  import { dirname, join } from "node:path";
9
3
 
10
4
  import { getLogger } from "../util/logger.js";
11
5
  import { getWorkspaceSkillsDir } from "../util/platform.js";
6
+ import {
7
+ computeSkillHash,
8
+ readInstallMeta,
9
+ writeInstallMeta,
10
+ } from "./install-meta.js";
12
11
 
13
12
  const log = getLogger("clawhub");
14
13
 
@@ -54,61 +53,15 @@ export function loadIntegrityManifest(): IntegrityManifest {
54
53
  }
55
54
  }
56
55
 
57
- function saveIntegrityManifest(manifest: IntegrityManifest): void {
58
- writeFileSync(
59
- getIntegrityPath(),
60
- JSON.stringify(manifest, null, 2) + "\n",
61
- "utf-8",
62
- );
63
- }
64
-
65
- /** Collect all file contents in a directory tree, sorted by relative path for determinism. */
66
- function collectFileContents(
67
- dir: string,
68
- prefix = "",
69
- ): Array<{ relPath: string; content: Buffer }> {
70
- const results: Array<{ relPath: string; content: Buffer }> = [];
71
- if (!existsSync(dir)) return results;
72
-
73
- const entries = readdirSync(dir, { withFileTypes: true });
74
- for (const entry of entries) {
75
- const relPath = prefix ? `${prefix}/${entry.name}` : entry.name;
76
- const fullPath = join(dir, entry.name);
77
- if (entry.isDirectory()) {
78
- results.push(...collectFileContents(fullPath, relPath));
79
- } else if (entry.isFile()) {
80
- results.push({ relPath, content: readFileSync(fullPath) });
81
- }
82
- }
83
- return results.sort((a, b) => a.relPath.localeCompare(b.relPath));
84
- }
85
-
86
- /**
87
- * Compute a SHA-256 hash over all files in a skill directory.
88
- * Returns format: "v2:sha256hex" (version prefix added to support hash format evolution).
89
- */
90
- function computeSkillHash(skillDir: string): string | null {
91
- if (!existsSync(skillDir) || !statSync(skillDir).isDirectory()) return null;
92
-
93
- const files = collectFileContents(skillDir);
94
- if (files.length === 0) return null;
95
-
96
- const hasher = new Bun.CryptoHasher("sha256");
97
- for (const file of files) {
98
- // Length-prefix each segment to prevent boundary ambiguity collisions
99
- const pathBuf = Buffer.from(file.relPath, "utf-8");
100
- hasher.update(`${pathBuf.length}:`);
101
- hasher.update(pathBuf);
102
- hasher.update(`${file.content.length}:`);
103
- hasher.update(file.content);
104
- }
105
- return `v2:${hasher.digest("hex")}`;
106
- }
107
-
108
56
  /**
109
57
  * Record or verify the content hash of an installed skill.
110
58
  * On first install: stores the hash (trust-on-first-use).
111
59
  * On subsequent installs: compares with stored hash and warns on mismatch.
60
+ *
61
+ * Always writes to `install-meta.json`. For skills that don't have one yet,
62
+ * creates a new `install-meta.json` with minimal metadata. Reads from the
63
+ * legacy `.integrity.json` manifest as a read-only fallback for stored hashes
64
+ * from skills that haven't been migrated yet.
112
65
  */
113
66
  export function verifyAndRecordSkillHash(slug: string): void {
114
67
  const skillDir = join(getManagedSkillsDir(), slug);
@@ -118,17 +71,28 @@ export function verifyAndRecordSkillHash(slug: string): void {
118
71
  return;
119
72
  }
120
73
 
121
- const manifest = loadIntegrityManifest();
122
- const existing = manifest[slug];
74
+ // Try install-meta.json first for stored hash
75
+ const installMeta = readInstallMeta(skillDir);
76
+ let storedHash: string | undefined;
123
77
 
124
- if (existing) {
125
- const storedHash = existing.sha256;
78
+ if (installMeta?.contentHash) {
79
+ storedHash = installMeta.contentHash;
80
+ } else {
81
+ // Fall back to legacy .integrity.json manifest
82
+ const manifest = loadIntegrityManifest();
83
+ const existing = manifest[slug];
84
+ if (existing) {
85
+ storedHash =
86
+ typeof existing.sha256 === "string" ? existing.sha256 : undefined;
87
+ }
88
+ }
126
89
 
127
- // Guard against corrupted manifest entries where sha256 is not a string
90
+ if (storedHash) {
91
+ // Guard against corrupted entries where hash is not a string
128
92
  if (typeof storedHash !== "string") {
129
93
  log.warn(
130
94
  { slug },
131
- "Integrity manifest entry has non-string sha256 — re-recording hash",
95
+ "Stored hash has non-string value — re-recording hash",
132
96
  );
133
97
  } else if (!storedHash.startsWith("v2:")) {
134
98
  // Unknown format (not v2: prefix) — warn about integrity mismatch
@@ -155,9 +119,18 @@ export function verifyAndRecordSkillHash(slug: string): void {
155
119
  );
156
120
  }
157
121
 
158
- // Always store the latest hash
159
- manifest[slug] = { sha256: hash, installedAt: new Date().toISOString() };
160
- saveIntegrityManifest(manifest);
122
+ // Write to install-meta.json (preferred). If no install-meta exists yet,
123
+ // create one with minimal metadata.
124
+ if (installMeta) {
125
+ writeInstallMeta(skillDir, { ...installMeta, contentHash: hash });
126
+ } else {
127
+ writeInstallMeta(skillDir, {
128
+ origin: "clawhub",
129
+ installedAt: new Date().toISOString(),
130
+ slug,
131
+ contentHash: hash,
132
+ });
133
+ }
161
134
  }
162
135
 
163
136
  interface ClawhubInstallResult {
@@ -248,7 +221,7 @@ async function runClawhub(
248
221
 
249
222
  export async function clawhubInstall(
250
223
  slug: string,
251
- opts?: { version?: string },
224
+ opts?: { version?: string; contactId?: string },
252
225
  ): Promise<ClawhubInstallResult> {
253
226
  if (!validateSlug(slug)) {
254
227
  return { success: false, error: `Invalid skill slug: ${slug}` };
@@ -264,7 +237,19 @@ export async function clawhubInstall(
264
237
  result.stderr.trim() || result.stdout.trim() || "Unknown error";
265
238
  return { success: false, error };
266
239
  }
267
- verifyAndRecordSkillHash(slug);
240
+
241
+ // Write install-meta.json for the installed skill.
242
+ // contentHash is included here, so there's no need to call
243
+ // verifyAndRecordSkillHash() — it would just rewrite the same data.
244
+ const skillDir = join(getManagedSkillsDir(), slug);
245
+ writeInstallMeta(skillDir, {
246
+ origin: "clawhub",
247
+ slug,
248
+ installedAt: new Date().toISOString(),
249
+ ...(opts?.contactId ? { installedBy: opts.contactId } : {}),
250
+ contentHash: computeSkillHash(skillDir) ?? undefined,
251
+ });
252
+
268
253
  return { success: true, skillName: slug };
269
254
  } catch (err) {
270
255
  const message = err instanceof Error ? err.message : String(err);
@@ -292,63 +277,63 @@ export async function clawhubUpdate(
292
277
 
293
278
  export async function clawhubSearch(
294
279
  query: string,
280
+ opts?: { limit?: number },
295
281
  ): Promise<ClawhubSearchResult> {
282
+ const limit = opts?.limit ?? 25;
283
+
296
284
  // Empty query: use explore (browse trending) instead of search
297
285
  if (!query.trim()) {
298
- return clawhubExplore();
286
+ return clawhubExplore({ limit });
299
287
  }
300
288
 
289
+ const result = await runClawhub(["search", query, "--limit", String(limit)]);
290
+ if (result.exitCode !== 0) {
291
+ const error =
292
+ result.stderr.trim() || result.stdout.trim() || "Unknown error";
293
+ throw new Error(`clawhub search failed: ${error}`);
294
+ }
295
+ // Try JSON first
301
296
  try {
302
- const result = await runClawhub(["search", query, "--limit", "25"]);
303
- if (result.exitCode !== 0) {
304
- return { skills: [] };
297
+ const parsed = JSON.parse(result.stdout);
298
+ if (Array.isArray(parsed)) {
299
+ return {
300
+ skills: parsed.map((s: ClawhubSearchResultItem) => ({
301
+ ...s,
302
+ source: s.source ?? ("clawhub" as const),
303
+ })),
304
+ };
305
305
  }
306
- // Try JSON first
307
- try {
308
- const parsed = JSON.parse(result.stdout);
309
- if (Array.isArray(parsed)) {
310
- return {
311
- skills: parsed.map((s: ClawhubSearchResultItem) => ({
312
- ...s,
313
- source: s.source ?? ("clawhub" as const),
314
- })),
315
- };
316
- }
317
- if (parsed.skills && Array.isArray(parsed.skills)) {
318
- return {
319
- skills: parsed.skills.map((s: ClawhubSearchResultItem) => ({
320
- ...s,
321
- source: s.source ?? ("clawhub" as const),
322
- })),
323
- };
324
- }
325
- } catch {
326
- // CLI outputs text: "slug vVersion DisplayName (score)"
306
+ if (parsed.skills && Array.isArray(parsed.skills)) {
307
+ return {
308
+ skills: parsed.skills.map((s: ClawhubSearchResultItem) => ({
309
+ ...s,
310
+ source: s.source ?? ("clawhub" as const),
311
+ })),
312
+ };
327
313
  }
314
+ } catch {
315
+ // CLI outputs text: "slug vVersion DisplayName (score)"
316
+ }
328
317
 
329
- // Parse text output lines: "slug vVersion Display Name (score)"
330
- const skills: ClawhubSearchResultItem[] = [];
331
- for (const line of result.stdout.split("\n")) {
332
- const match = line.match(/^(\S+)\s+v(\S+)\s+(.+?)\s+\([\d.]+\)\s*$/);
333
- if (match) {
334
- skills.push({
335
- slug: match[1],
336
- version: match[2],
337
- name: match[3].trim(),
338
- description: "",
339
- author: "",
340
- stars: 0,
341
- installs: 0,
342
- createdAt: 0,
343
- source: "clawhub",
344
- });
345
- }
318
+ // Parse text output lines: "slug vVersion Display Name (score)"
319
+ const skills: ClawhubSearchResultItem[] = [];
320
+ for (const line of result.stdout.split("\n")) {
321
+ const match = line.match(/^(\S+)\s+v(\S+)\s+(.+?)\s+\([\d.]+\)\s*$/);
322
+ if (match) {
323
+ skills.push({
324
+ slug: match[1],
325
+ version: match[2],
326
+ name: match[3].trim(),
327
+ description: "",
328
+ author: "",
329
+ stars: 0,
330
+ installs: 0,
331
+ createdAt: 0,
332
+ source: "clawhub",
333
+ });
346
334
  }
347
- return { skills };
348
- } catch (err) {
349
- log.warn({ err }, "clawhub search failed");
350
- return { skills: [] };
351
335
  }
336
+ return { skills };
352
337
  }
353
338
 
354
339
  export async function clawhubExplore(opts?: {
@@ -358,46 +343,41 @@ export async function clawhubExplore(opts?: {
358
343
  const limit = String(opts?.limit ?? 25);
359
344
  const sort = opts?.sort ?? "installsAllTime";
360
345
 
346
+ const result = await runClawhub([
347
+ "explore",
348
+ "--json",
349
+ "--limit",
350
+ limit,
351
+ "--sort",
352
+ sort,
353
+ ]);
354
+ if (result.exitCode !== 0) {
355
+ const error =
356
+ result.stderr.trim() || result.stdout.trim() || "Unknown error";
357
+ throw new Error(`clawhub explore failed: ${error}`);
358
+ }
361
359
  try {
362
- const result = await runClawhub([
363
- "explore",
364
- "--json",
365
- "--limit",
366
- limit,
367
- "--sort",
368
- sort,
369
- ]);
370
- if (result.exitCode !== 0) {
371
- return { skills: [] };
372
- }
373
- try {
374
- const parsed = JSON.parse(result.stdout);
375
- const items = parsed.items ?? parsed;
376
- if (!Array.isArray(items)) return { skills: [] };
377
-
378
- // Normalize explore response to ClawhubSearchResultItem shape
379
- const skills: ClawhubSearchResultItem[] = items.map(
380
- (item: Record<string, unknown>) => ({
381
- name: (item.displayName as string) ?? (item.slug as string) ?? "",
382
- slug: (item.slug as string) ?? "",
383
- description: (item.summary as string) ?? "",
384
- author: (item.author as string) ?? "",
385
- stars: (item.stats as Record<string, number>)?.stars ?? 0,
386
- installs:
387
- (item.stats as Record<string, number>)?.installsAllTime ?? 0,
388
- version: (item.tags as Record<string, string>)?.latest ?? "",
389
- createdAt: (item.createdAt as number) ?? 0,
390
- source: "clawhub",
391
- }),
392
- );
393
- return { skills };
394
- } catch {
395
- // parse failure
396
- }
397
- return { skills: [] };
398
- } catch (err) {
399
- log.warn({ err }, "clawhub explore failed");
400
- return { skills: [] };
360
+ const parsed = JSON.parse(result.stdout);
361
+ const items = parsed.items ?? parsed;
362
+ if (!Array.isArray(items)) return { skills: [] };
363
+
364
+ // Normalize explore response to ClawhubSearchResultItem shape
365
+ const skills: ClawhubSearchResultItem[] = items.map(
366
+ (item: Record<string, unknown>) => ({
367
+ name: (item.displayName as string) ?? (item.slug as string) ?? "",
368
+ slug: (item.slug as string) ?? "",
369
+ description: (item.summary as string) ?? "",
370
+ author: (item.author as string) ?? "",
371
+ stars: (item.stats as Record<string, number>)?.stars ?? 0,
372
+ installs: (item.stats as Record<string, number>)?.installsAllTime ?? 0,
373
+ version: (item.tags as Record<string, string>)?.latest ?? "",
374
+ createdAt: (item.createdAt as number) ?? 0,
375
+ source: "clawhub",
376
+ }),
377
+ );
378
+ return { skills };
379
+ } catch {
380
+ throw new Error("Failed to parse clawhub explore output");
401
381
  }
402
382
  }
403
383
 
@@ -0,0 +1,208 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import {
3
+ existsSync,
4
+ mkdirSync,
5
+ readdirSync,
6
+ readFileSync,
7
+ renameSync,
8
+ statSync,
9
+ writeFileSync,
10
+ } from "node:fs";
11
+ import { dirname, join } from "node:path";
12
+
13
+ // ─── SkillInstallMeta type ──────────────────────────────────────────────────
14
+
15
+ export interface SkillInstallMeta {
16
+ origin: "vellum" | "clawhub" | "skillssh" | "custom";
17
+ installedAt: string; // ISO 8601
18
+ installedBy?: string; // actorPrincipalId from auth context (identifies who initiated the install)
19
+ backfilledBy?: string; // set by migration that backfilled this file (e.g. "migration-026")
20
+ version?: string; // semver if known
21
+ slug?: string; // registry slug
22
+ sourceRepo?: string; // GitHub repo (e.g. "vercel-labs/agent-skills")
23
+ contentHash?: string; // SHA-256 content hash (v2:hex format)
24
+ }
25
+
26
+ // ─── Atomic write helper ────────────────────────────────────────────────────
27
+
28
+ function atomicWriteFile(filePath: string, content: string): void {
29
+ const dir = dirname(filePath);
30
+ mkdirSync(dir, { recursive: true });
31
+ const tmpPath = join(dir, `.tmp-${randomUUID()}`);
32
+ writeFileSync(tmpPath, content, "utf-8");
33
+ renameSync(tmpPath, filePath);
34
+ }
35
+
36
+ // ─── Write install-meta.json ────────────────────────────────────────────────
37
+
38
+ const INSTALL_META_FILENAME = "install-meta.json";
39
+ const LEGACY_VERSION_FILENAME = "version.json";
40
+
41
+ /**
42
+ * Atomically write `install-meta.json` inside the skill directory.
43
+ */
44
+ export function writeInstallMeta(
45
+ skillDir: string,
46
+ meta: SkillInstallMeta,
47
+ ): void {
48
+ const filePath = join(skillDir, INSTALL_META_FILENAME);
49
+ atomicWriteFile(filePath, JSON.stringify(meta, null, 2) + "\n");
50
+ }
51
+
52
+ // ─── Read install-meta.json (with legacy fallback) ──────────────────────────
53
+
54
+ /**
55
+ * Reads `install-meta.json` from the skill directory. If not found, falls
56
+ * back to reading legacy `version.json` and inferring the origin:
57
+ *
58
+ * - Has `origin: "skills.sh"` -> `origin: "skillssh"`, copies `source` as
59
+ * `sourceRepo` and `skillSlug` as `slug`.
60
+ * - Has `version` but no `origin` field -> `origin: "vellum"`.
61
+ * - Otherwise -> `origin: "custom"`.
62
+ *
63
+ * Legacy files never have `installedBy`, so it will be `undefined` for
64
+ * backfilled skills.
65
+ *
66
+ * If neither file exists, returns `null`.
67
+ */
68
+ export function readInstallMeta(skillDir: string): SkillInstallMeta | null {
69
+ // Try install-meta.json first
70
+ const metaPath = join(skillDir, INSTALL_META_FILENAME);
71
+ if (existsSync(metaPath)) {
72
+ try {
73
+ return JSON.parse(readFileSync(metaPath, "utf-8")) as SkillInstallMeta;
74
+ } catch {
75
+ // Malformed install-meta.json (partial write, manual edit, etc.) —
76
+ // fall through to the legacy version.json path so we don't lose
77
+ // provenance info when a valid legacy file exists.
78
+ }
79
+ }
80
+
81
+ // Fall back to legacy version.json
82
+ const legacyPath = join(skillDir, LEGACY_VERSION_FILENAME);
83
+ if (!existsSync(legacyPath)) {
84
+ return null;
85
+ }
86
+
87
+ try {
88
+ const raw = JSON.parse(readFileSync(legacyPath, "utf-8")) as Record<
89
+ string,
90
+ unknown
91
+ >;
92
+ return inferFromLegacyVersionJson(raw);
93
+ } catch {
94
+ return null;
95
+ }
96
+ }
97
+
98
+ /**
99
+ * Infer a SkillInstallMeta from a legacy version.json object.
100
+ */
101
+ function inferFromLegacyVersionJson(
102
+ raw: Record<string, unknown>,
103
+ ): SkillInstallMeta {
104
+ // skills.sh origin: has `origin: "skills.sh"`
105
+ if (raw.origin === "skills.sh") {
106
+ return {
107
+ origin: "skillssh",
108
+ installedAt:
109
+ typeof raw.installedAt === "string"
110
+ ? raw.installedAt
111
+ : new Date().toISOString(),
112
+ sourceRepo: typeof raw.source === "string" ? raw.source : undefined,
113
+ slug: typeof raw.skillSlug === "string" ? raw.skillSlug : undefined,
114
+ };
115
+ }
116
+
117
+ // Vellum (first-party catalog) origin: has `version` but no `origin` field
118
+ if (typeof raw.version === "string" && !("origin" in raw)) {
119
+ return {
120
+ origin: "vellum",
121
+ installedAt:
122
+ typeof raw.installedAt === "string"
123
+ ? raw.installedAt
124
+ : new Date().toISOString(),
125
+ version: raw.version,
126
+ };
127
+ }
128
+
129
+ // Unknown format -> custom
130
+ return {
131
+ origin: "custom",
132
+ installedAt:
133
+ typeof raw.installedAt === "string"
134
+ ? raw.installedAt
135
+ : new Date().toISOString(),
136
+ };
137
+ }
138
+
139
+ // ─── Content hash computation ───────────────────────────────────────────────
140
+
141
+ /**
142
+ * Metadata files excluded from content hashing. These are written by the
143
+ * installer and must not contribute to the content hash — otherwise the hash
144
+ * stored inside `install-meta.json` would change after writing the file.
145
+ */
146
+ const METADATA_FILENAMES = new Set([
147
+ INSTALL_META_FILENAME, // install-meta.json
148
+ LEGACY_VERSION_FILENAME, // version.json
149
+ ]);
150
+
151
+ /**
152
+ * Collect all file contents in a directory tree, sorted by relative path
153
+ * for determinism. Metadata files (`install-meta.json`, `version.json`) at
154
+ * the root level are excluded so the content hash covers only actual skill
155
+ * content.
156
+ */
157
+ export function collectFileContents(
158
+ dir: string,
159
+ prefix = "",
160
+ ): Array<{ relPath: string; content: Buffer }> {
161
+ const results: Array<{ relPath: string; content: Buffer }> = [];
162
+ if (!existsSync(dir)) return results;
163
+
164
+ const entries = readdirSync(dir, { withFileTypes: true });
165
+ for (const entry of entries) {
166
+ // Exclude metadata files at the root level (prefix === "").
167
+ // Only exclude actual files — a directory with a metadata name should
168
+ // still be traversed so nested content contributes to the hash.
169
+ if (!prefix && entry.isFile() && METADATA_FILENAMES.has(entry.name))
170
+ continue;
171
+
172
+ const relPath = prefix ? `${prefix}/${entry.name}` : entry.name;
173
+ const fullPath = join(dir, entry.name);
174
+ if (entry.isDirectory()) {
175
+ results.push(...collectFileContents(fullPath, relPath));
176
+ } else if (entry.isFile()) {
177
+ results.push({ relPath, content: readFileSync(fullPath) });
178
+ }
179
+ }
180
+ return results.sort((a, b) => a.relPath.localeCompare(b.relPath));
181
+ }
182
+
183
+ /**
184
+ * Compute a SHA-256 hash over all files in a skill directory.
185
+ * Returns format: "v2:sha256hex" (version prefix added to support hash format
186
+ * evolution).
187
+ *
188
+ * This is the content hash used by the integrity manifest (trust-on-first-use).
189
+ * It differs from `computeSkillVersionHash` in `version-hash.ts`, which uses a
190
+ * different hashing strategy (v1: prefix) for version identity.
191
+ */
192
+ export function computeSkillHash(skillDir: string): string | null {
193
+ if (!existsSync(skillDir) || !statSync(skillDir).isDirectory()) return null;
194
+
195
+ const files = collectFileContents(skillDir);
196
+ if (files.length === 0) return null;
197
+
198
+ const hasher = new Bun.CryptoHasher("sha256");
199
+ for (const file of files) {
200
+ // Length-prefix each segment to prevent boundary ambiguity collisions
201
+ const pathBuf = Buffer.from(file.relPath, "utf-8");
202
+ hasher.update(`${pathBuf.length}:`);
203
+ hasher.update(pathBuf);
204
+ hasher.update(`${file.content.length}:`);
205
+ hasher.update(file.content);
206
+ }
207
+ return `v2:${hasher.digest("hex")}`;
208
+ }
@@ -13,6 +13,7 @@ import { stringify as stringifyYaml } from "yaml";
13
13
 
14
14
  import { getLogger } from "../util/logger.js";
15
15
  import { getWorkspaceSkillsDir } from "../util/platform.js";
16
+ import { writeInstallMeta } from "./install-meta.js";
16
17
  import { deleteSkillCapabilityMemory } from "./skill-memory.js";
17
18
 
18
19
  const log = getLogger("managed-store");
@@ -165,15 +166,20 @@ function getVersionMetaPath(id: string): string {
165
166
  return join(getManagedSkillDir(id), "version.json");
166
167
  }
167
168
 
168
- function writeVersionMeta(id: string, version: string): void {
169
- const meta: SkillVersionMeta = {
170
- version,
171
- installedAt: new Date().toISOString(),
172
- };
173
- atomicWriteFile(getVersionMetaPath(id), JSON.stringify(meta, null, 2) + "\n");
174
- }
175
-
176
169
  export function readSkillVersion(id: string): string | null {
170
+ // Try install-meta.json first (new format)
171
+ const installMetaPath = join(getManagedSkillDir(id), "install-meta.json");
172
+ if (existsSync(installMetaPath)) {
173
+ try {
174
+ const raw = readFileSync(installMetaPath, "utf-8");
175
+ const meta = JSON.parse(raw) as { version?: string };
176
+ if (meta.version) return meta.version;
177
+ } catch {
178
+ // Fall through to legacy path
179
+ }
180
+ }
181
+
182
+ // Fall back to legacy version.json
177
183
  const metaPath = getVersionMetaPath(id);
178
184
  if (!existsSync(metaPath)) return null;
179
185
  try {
@@ -197,6 +203,7 @@ interface CreateManagedSkillParams {
197
203
  addToIndex?: boolean;
198
204
  includes?: string[];
199
205
  version?: string;
206
+ contactId?: string;
200
207
  }
201
208
 
202
209
  interface CreateManagedSkillResult {
@@ -259,14 +266,18 @@ export function createManagedSkill(
259
266
  mkdirSync(skillDir, { recursive: true });
260
267
  atomicWriteFile(skillFilePath, content);
261
268
 
262
- if (params.version) {
263
- writeVersionMeta(params.id, params.version);
264
- } else {
265
- // Remove stale version metadata when overwriting without a version
266
- const metaPath = getVersionMetaPath(params.id);
267
- if (existsSync(metaPath)) {
268
- rmSync(metaPath);
269
- }
269
+ // Write install metadata
270
+ writeInstallMeta(skillDir, {
271
+ origin: "custom",
272
+ installedAt: new Date().toISOString(),
273
+ ...(params.version ? { version: params.version } : {}),
274
+ ...(params.contactId ? { installedBy: params.contactId } : {}),
275
+ });
276
+
277
+ // Clean up legacy version.json if present (superseded by install-meta.json)
278
+ const metaPath = getVersionMetaPath(params.id);
279
+ if (existsSync(metaPath)) {
280
+ rmSync(metaPath);
270
281
  }
271
282
 
272
283
  log.info(