@collage-dam/mcp-server 0.1.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 (306) hide show
  1. package/.env.example +56 -0
  2. package/CHANGELOG.md +90 -0
  3. package/LICENSE +21 -0
  4. package/README.md +512 -0
  5. package/dist/client.d.ts +497 -0
  6. package/dist/client.d.ts.map +1 -0
  7. package/dist/client.js +1162 -0
  8. package/dist/client.js.map +1 -0
  9. package/dist/conventions/confirmation.d.ts +89 -0
  10. package/dist/conventions/confirmation.d.ts.map +1 -0
  11. package/dist/conventions/confirmation.js +132 -0
  12. package/dist/conventions/confirmation.js.map +1 -0
  13. package/dist/conventions/dry-run/batch-executor.d.ts +36 -0
  14. package/dist/conventions/dry-run/batch-executor.d.ts.map +1 -0
  15. package/dist/conventions/dry-run/batch-executor.js +89 -0
  16. package/dist/conventions/dry-run/batch-executor.js.map +1 -0
  17. package/dist/conventions/dry-run/diff-renderer.d.ts +34 -0
  18. package/dist/conventions/dry-run/diff-renderer.d.ts.map +1 -0
  19. package/dist/conventions/dry-run/diff-renderer.js +158 -0
  20. package/dist/conventions/dry-run/diff-renderer.js.map +1 -0
  21. package/dist/conventions/dry-run/index.d.ts +13 -0
  22. package/dist/conventions/dry-run/index.d.ts.map +1 -0
  23. package/dist/conventions/dry-run/index.js +10 -0
  24. package/dist/conventions/dry-run/index.js.map +1 -0
  25. package/dist/conventions/dry-run/mutating-tool.d.ts +64 -0
  26. package/dist/conventions/dry-run/mutating-tool.d.ts.map +1 -0
  27. package/dist/conventions/dry-run/mutating-tool.js +88 -0
  28. package/dist/conventions/dry-run/mutating-tool.js.map +1 -0
  29. package/dist/conventions/dry-run/summary.d.ts +66 -0
  30. package/dist/conventions/dry-run/summary.d.ts.map +1 -0
  31. package/dist/conventions/dry-run/summary.js +185 -0
  32. package/dist/conventions/dry-run/summary.js.map +1 -0
  33. package/dist/conventions/dry-run/types.d.ts +597 -0
  34. package/dist/conventions/dry-run/types.d.ts.map +1 -0
  35. package/dist/conventions/dry-run/types.js +108 -0
  36. package/dist/conventions/dry-run/types.js.map +1 -0
  37. package/dist/conventions/dry-run/with-dry-run.d.ts +66 -0
  38. package/dist/conventions/dry-run/with-dry-run.d.ts.map +1 -0
  39. package/dist/conventions/dry-run/with-dry-run.js +219 -0
  40. package/dist/conventions/dry-run/with-dry-run.js.map +1 -0
  41. package/dist/conventions/env.d.ts +49 -0
  42. package/dist/conventions/env.d.ts.map +1 -0
  43. package/dist/conventions/env.js +84 -0
  44. package/dist/conventions/env.js.map +1 -0
  45. package/dist/conventions/errors.d.ts +68 -0
  46. package/dist/conventions/errors.d.ts.map +1 -0
  47. package/dist/conventions/errors.js +81 -0
  48. package/dist/conventions/errors.js.map +1 -0
  49. package/dist/conventions/logger.d.ts +28 -0
  50. package/dist/conventions/logger.d.ts.map +1 -0
  51. package/dist/conventions/logger.js +105 -0
  52. package/dist/conventions/logger.js.map +1 -0
  53. package/dist/conventions/pagination.d.ts +37 -0
  54. package/dist/conventions/pagination.d.ts.map +1 -0
  55. package/dist/conventions/pagination.js +53 -0
  56. package/dist/conventions/pagination.js.map +1 -0
  57. package/dist/conventions/rate-limiter.d.ts +54 -0
  58. package/dist/conventions/rate-limiter.d.ts.map +1 -0
  59. package/dist/conventions/rate-limiter.js +143 -0
  60. package/dist/conventions/rate-limiter.js.map +1 -0
  61. package/dist/conventions/response-budget.d.ts +66 -0
  62. package/dist/conventions/response-budget.d.ts.map +1 -0
  63. package/dist/conventions/response-budget.js +89 -0
  64. package/dist/conventions/response-budget.js.map +1 -0
  65. package/dist/conventions/schema-version.d.ts +27 -0
  66. package/dist/conventions/schema-version.d.ts.map +1 -0
  67. package/dist/conventions/schema-version.js +29 -0
  68. package/dist/conventions/schema-version.js.map +1 -0
  69. package/dist/conventions/state-store-redis.d.ts +32 -0
  70. package/dist/conventions/state-store-redis.d.ts.map +1 -0
  71. package/dist/conventions/state-store-redis.js +77 -0
  72. package/dist/conventions/state-store-redis.js.map +1 -0
  73. package/dist/conventions/state-store.d.ts +46 -0
  74. package/dist/conventions/state-store.d.ts.map +1 -0
  75. package/dist/conventions/state-store.js +105 -0
  76. package/dist/conventions/state-store.js.map +1 -0
  77. package/dist/index.d.ts +5 -0
  78. package/dist/index.d.ts.map +1 -0
  79. package/dist/index.js +421 -0
  80. package/dist/index.js.map +1 -0
  81. package/dist/prompts/collection-audit.d.ts +13 -0
  82. package/dist/prompts/collection-audit.d.ts.map +1 -0
  83. package/dist/prompts/collection-audit.js +168 -0
  84. package/dist/prompts/collection-audit.js.map +1 -0
  85. package/dist/prompts/create-distribution.d.ts +15 -0
  86. package/dist/prompts/create-distribution.d.ts.map +1 -0
  87. package/dist/prompts/create-distribution.js +111 -0
  88. package/dist/prompts/create-distribution.js.map +1 -0
  89. package/dist/prompts/helpers.d.ts +20 -0
  90. package/dist/prompts/helpers.d.ts.map +1 -0
  91. package/dist/prompts/helpers.js +53 -0
  92. package/dist/prompts/helpers.js.map +1 -0
  93. package/dist/prompts/library-health-audit.d.ts +13 -0
  94. package/dist/prompts/library-health-audit.d.ts.map +1 -0
  95. package/dist/prompts/library-health-audit.js +131 -0
  96. package/dist/prompts/library-health-audit.js.map +1 -0
  97. package/dist/prompts/usage-insights.d.ts +13 -0
  98. package/dist/prompts/usage-insights.d.ts.map +1 -0
  99. package/dist/prompts/usage-insights.js +98 -0
  100. package/dist/prompts/usage-insights.js.map +1 -0
  101. package/dist/prompts/wrap-prompt-as-tool.d.ts +48 -0
  102. package/dist/prompts/wrap-prompt-as-tool.d.ts.map +1 -0
  103. package/dist/prompts/wrap-prompt-as-tool.js +61 -0
  104. package/dist/prompts/wrap-prompt-as-tool.js.map +1 -0
  105. package/dist/resources/asset-by-id.d.ts +4 -0
  106. package/dist/resources/asset-by-id.d.ts.map +1 -0
  107. package/dist/resources/asset-by-id.js +27 -0
  108. package/dist/resources/asset-by-id.js.map +1 -0
  109. package/dist/resources/collections.d.ts +5 -0
  110. package/dist/resources/collections.d.ts.map +1 -0
  111. package/dist/resources/collections.js +48 -0
  112. package/dist/resources/collections.js.map +1 -0
  113. package/dist/resources/custom-fields.d.ts +4 -0
  114. package/dist/resources/custom-fields.d.ts.map +1 -0
  115. package/dist/resources/custom-fields.js +30 -0
  116. package/dist/resources/custom-fields.js.map +1 -0
  117. package/dist/resources/folders.d.ts +5 -0
  118. package/dist/resources/folders.d.ts.map +1 -0
  119. package/dist/resources/folders.js +73 -0
  120. package/dist/resources/folders.js.map +1 -0
  121. package/dist/resources/helpers.d.ts +17 -0
  122. package/dist/resources/helpers.d.ts.map +1 -0
  123. package/dist/resources/helpers.js +59 -0
  124. package/dist/resources/helpers.js.map +1 -0
  125. package/dist/resources/portals.d.ts +5 -0
  126. package/dist/resources/portals.d.ts.map +1 -0
  127. package/dist/resources/portals.js +81 -0
  128. package/dist/resources/portals.js.map +1 -0
  129. package/dist/resources/recent-and-dashboard.d.ts +5 -0
  130. package/dist/resources/recent-and-dashboard.d.ts.map +1 -0
  131. package/dist/resources/recent-and-dashboard.js +42 -0
  132. package/dist/resources/recent-and-dashboard.js.map +1 -0
  133. package/dist/tools/asset-selection.d.ts +102 -0
  134. package/dist/tools/asset-selection.d.ts.map +1 -0
  135. package/dist/tools/asset-selection.js +133 -0
  136. package/dist/tools/asset-selection.js.map +1 -0
  137. package/dist/tools/audit/audit-folder-structure.d.ts +108 -0
  138. package/dist/tools/audit/audit-folder-structure.d.ts.map +1 -0
  139. package/dist/tools/audit/audit-folder-structure.js +260 -0
  140. package/dist/tools/audit/audit-folder-structure.js.map +1 -0
  141. package/dist/tools/audit/audit-naming-conventions.d.ts +83 -0
  142. package/dist/tools/audit/audit-naming-conventions.d.ts.map +1 -0
  143. package/dist/tools/audit/audit-naming-conventions.js +238 -0
  144. package/dist/tools/audit/audit-naming-conventions.js.map +1 -0
  145. package/dist/tools/audit/audit-tagging-hygiene.d.ts +77 -0
  146. package/dist/tools/audit/audit-tagging-hygiene.d.ts.map +1 -0
  147. package/dist/tools/audit/audit-tagging-hygiene.js +402 -0
  148. package/dist/tools/audit/audit-tagging-hygiene.js.map +1 -0
  149. package/dist/tools/audit/detect-duplicates.d.ts +62 -0
  150. package/dist/tools/audit/detect-duplicates.d.ts.map +1 -0
  151. package/dist/tools/audit/detect-duplicates.js +0 -0
  152. package/dist/tools/audit/detect-duplicates.js.map +1 -0
  153. package/dist/tools/audit/types.d.ts +526 -0
  154. package/dist/tools/audit/types.d.ts.map +1 -0
  155. package/dist/tools/audit/types.js +188 -0
  156. package/dist/tools/audit/types.js.map +1 -0
  157. package/dist/tools/bulk-move-assets.d.ts +78 -0
  158. package/dist/tools/bulk-move-assets.d.ts.map +1 -0
  159. package/dist/tools/bulk-move-assets.js +122 -0
  160. package/dist/tools/bulk-move-assets.js.map +1 -0
  161. package/dist/tools/bulk-normalize-filenames.d.ts +62 -0
  162. package/dist/tools/bulk-normalize-filenames.d.ts.map +1 -0
  163. package/dist/tools/bulk-normalize-filenames.js +237 -0
  164. package/dist/tools/bulk-normalize-filenames.js.map +1 -0
  165. package/dist/tools/bulk-rename-assets.d.ts +79 -0
  166. package/dist/tools/bulk-rename-assets.d.ts.map +1 -0
  167. package/dist/tools/bulk-rename-assets.js +139 -0
  168. package/dist/tools/bulk-rename-assets.js.map +1 -0
  169. package/dist/tools/bulk-tags.d.ts +107 -0
  170. package/dist/tools/bulk-tags.d.ts.map +1 -0
  171. package/dist/tools/bulk-tags.js +220 -0
  172. package/dist/tools/bulk-tags.js.map +1 -0
  173. package/dist/tools/client-adapters.d.ts +76 -0
  174. package/dist/tools/client-adapters.d.ts.map +1 -0
  175. package/dist/tools/client-adapters.js +648 -0
  176. package/dist/tools/client-adapters.js.map +1 -0
  177. package/dist/tools/collection-membership.d.ts +90 -0
  178. package/dist/tools/collection-membership.d.ts.map +1 -0
  179. package/dist/tools/collection-membership.js +195 -0
  180. package/dist/tools/collection-membership.js.map +1 -0
  181. package/dist/tools/create-collection.d.ts +63 -0
  182. package/dist/tools/create-collection.d.ts.map +1 -0
  183. package/dist/tools/create-collection.js +151 -0
  184. package/dist/tools/create-collection.js.map +1 -0
  185. package/dist/tools/create-folder.d.ts +46 -0
  186. package/dist/tools/create-folder.d.ts.map +1 -0
  187. package/dist/tools/create-folder.js +83 -0
  188. package/dist/tools/create-folder.js.map +1 -0
  189. package/dist/tools/create-share-link.d.ts +107 -0
  190. package/dist/tools/create-share-link.d.ts.map +1 -0
  191. package/dist/tools/create-share-link.js +239 -0
  192. package/dist/tools/create-share-link.js.map +1 -0
  193. package/dist/tools/get-asset-details.d.ts +401 -0
  194. package/dist/tools/get-asset-details.d.ts.map +1 -0
  195. package/dist/tools/get-asset-details.js +56 -0
  196. package/dist/tools/get-asset-details.js.map +1 -0
  197. package/dist/tools/get-collection.d.ts +126 -0
  198. package/dist/tools/get-collection.d.ts.map +1 -0
  199. package/dist/tools/get-collection.js +52 -0
  200. package/dist/tools/get-collection.js.map +1 -0
  201. package/dist/tools/get-embed-code.d.ts +195 -0
  202. package/dist/tools/get-embed-code.d.ts.map +1 -0
  203. package/dist/tools/get-embed-code.js +214 -0
  204. package/dist/tools/get-embed-code.js.map +1 -0
  205. package/dist/tools/insights/analyze-share-links.d.ts +159 -0
  206. package/dist/tools/insights/analyze-share-links.d.ts.map +1 -0
  207. package/dist/tools/insights/analyze-share-links.js +314 -0
  208. package/dist/tools/insights/analyze-share-links.js.map +1 -0
  209. package/dist/tools/insights/insight-cache.d.ts +36 -0
  210. package/dist/tools/insights/insight-cache.d.ts.map +1 -0
  211. package/dist/tools/insights/insight-cache.js +98 -0
  212. package/dist/tools/insights/insight-cache.js.map +1 -0
  213. package/dist/tools/insights/report-asset-activation.d.ts +149 -0
  214. package/dist/tools/insights/report-asset-activation.d.ts.map +1 -0
  215. package/dist/tools/insights/report-asset-activation.js +380 -0
  216. package/dist/tools/insights/report-asset-activation.js.map +1 -0
  217. package/dist/tools/insights/report-stale-assets.d.ts +120 -0
  218. package/dist/tools/insights/report-stale-assets.d.ts.map +1 -0
  219. package/dist/tools/insights/report-stale-assets.js +281 -0
  220. package/dist/tools/insights/report-stale-assets.js.map +1 -0
  221. package/dist/tools/insights/report-top-assets.d.ts +139 -0
  222. package/dist/tools/insights/report-top-assets.d.ts.map +1 -0
  223. package/dist/tools/insights/report-top-assets.js +407 -0
  224. package/dist/tools/insights/report-top-assets.js.map +1 -0
  225. package/dist/tools/list-categories.d.ts +127 -0
  226. package/dist/tools/list-categories.d.ts.map +1 -0
  227. package/dist/tools/list-categories.js +68 -0
  228. package/dist/tools/list-categories.js.map +1 -0
  229. package/dist/tools/list-collections.d.ts +127 -0
  230. package/dist/tools/list-collections.d.ts.map +1 -0
  231. package/dist/tools/list-collections.js +53 -0
  232. package/dist/tools/list-collections.js.map +1 -0
  233. package/dist/tools/list-custom-fields.d.ts +125 -0
  234. package/dist/tools/list-custom-fields.d.ts.map +1 -0
  235. package/dist/tools/list-custom-fields.js +51 -0
  236. package/dist/tools/list-custom-fields.js.map +1 -0
  237. package/dist/tools/list-share-links.d.ts +192 -0
  238. package/dist/tools/list-share-links.d.ts.map +1 -0
  239. package/dist/tools/list-share-links.js +92 -0
  240. package/dist/tools/list-share-links.js.map +1 -0
  241. package/dist/tools/list-workspaces.d.ts +88 -0
  242. package/dist/tools/list-workspaces.d.ts.map +1 -0
  243. package/dist/tools/list-workspaces.js +71 -0
  244. package/dist/tools/list-workspaces.js.map +1 -0
  245. package/dist/tools/move-asset.d.ts +48 -0
  246. package/dist/tools/move-asset.d.ts.map +1 -0
  247. package/dist/tools/move-asset.js +85 -0
  248. package/dist/tools/move-asset.js.map +1 -0
  249. package/dist/tools/rename-asset.d.ts +88 -0
  250. package/dist/tools/rename-asset.d.ts.map +1 -0
  251. package/dist/tools/rename-asset.js +100 -0
  252. package/dist/tools/rename-asset.js.map +1 -0
  253. package/dist/tools/rename-folder.d.ts +55 -0
  254. package/dist/tools/rename-folder.d.ts.map +1 -0
  255. package/dist/tools/rename-folder.js +101 -0
  256. package/dist/tools/rename-folder.js.map +1 -0
  257. package/dist/tools/revoke-share-link.d.ts +55 -0
  258. package/dist/tools/revoke-share-link.d.ts.map +1 -0
  259. package/dist/tools/revoke-share-link.js +77 -0
  260. package/dist/tools/revoke-share-link.js.map +1 -0
  261. package/dist/tools/search/facets.d.ts +34 -0
  262. package/dist/tools/search/facets.d.ts.map +1 -0
  263. package/dist/tools/search/facets.js +147 -0
  264. package/dist/tools/search/facets.js.map +1 -0
  265. package/dist/tools/search/filter-builder.d.ts +33 -0
  266. package/dist/tools/search/filter-builder.d.ts.map +1 -0
  267. package/dist/tools/search/filter-builder.js +111 -0
  268. package/dist/tools/search/filter-builder.js.map +1 -0
  269. package/dist/tools/search/search-assets.d.ts +41 -0
  270. package/dist/tools/search/search-assets.d.ts.map +1 -0
  271. package/dist/tools/search/search-assets.js +162 -0
  272. package/dist/tools/search/search-assets.js.map +1 -0
  273. package/dist/tools/search/search-collections.d.ts +35 -0
  274. package/dist/tools/search/search-collections.d.ts.map +1 -0
  275. package/dist/tools/search/search-collections.js +103 -0
  276. package/dist/tools/search/search-collections.js.map +1 -0
  277. package/dist/tools/search/types.d.ts +1047 -0
  278. package/dist/tools/search/types.d.ts.map +1 -0
  279. package/dist/tools/search/types.js +216 -0
  280. package/dist/tools/search/types.js.map +1 -0
  281. package/dist/tools/update-asset-metadata.d.ts +78 -0
  282. package/dist/tools/update-asset-metadata.d.ts.map +1 -0
  283. package/dist/tools/update-asset-metadata.js +203 -0
  284. package/dist/tools/update-asset-metadata.js.map +1 -0
  285. package/dist/tools/update-collection.d.ts +69 -0
  286. package/dist/tools/update-collection.d.ts.map +1 -0
  287. package/dist/tools/update-collection.js +142 -0
  288. package/dist/tools/update-collection.js.map +1 -0
  289. package/dist/tools/view-category-contents.d.ts +231 -0
  290. package/dist/tools/view-category-contents.d.ts.map +1 -0
  291. package/dist/tools/view-category-contents.js +97 -0
  292. package/dist/tools/view-category-contents.js.map +1 -0
  293. package/dist/types.d.ts +1326 -0
  294. package/dist/types.d.ts.map +1 -0
  295. package/dist/types.js +288 -0
  296. package/dist/types.js.map +1 -0
  297. package/dist/typesense.d.ts +84 -0
  298. package/dist/typesense.d.ts.map +1 -0
  299. package/dist/typesense.js +243 -0
  300. package/dist/typesense.js.map +1 -0
  301. package/docs/api-field-verification.md +244 -0
  302. package/docs/deployment-runbook.md +446 -0
  303. package/docs/security-review.md +195 -0
  304. package/docs/typesense-filter-schema.md +262 -0
  305. package/docs/verified-endpoints.md +38 -0
  306. package/package.json +72 -0
@@ -0,0 +1,648 @@
1
+ // ── Tool Client Adapters ─────────────────────────────────────────────
2
+ // Each Phase 1 mutating tool declares a narrow client interface it needs
3
+ // (rename, metadata read/write, tag read/write, collection metadata
4
+ // read/write, single-collection membership read). This module wires those
5
+ // interfaces to live `CollageClient` methods.
6
+ //
7
+ // Read paths reuse `getAssetDetails` (POST view-detail) where possible so
8
+ // we avoid duplicating round-trips for the same data. Custom-field reads
9
+ // fan out to `getAssetCustomFields` because `view-detail` does not include
10
+ // the custom-field projection in the same shape the metadata tool needs.
11
+ //
12
+ // The lone exception is `searchAssets` (Typesense-backed) — that adapter
13
+ // stays UPSTREAM-stubbed until the public Typesense host is provisioned.
14
+ import { createToolError } from '../conventions/errors.js';
15
+ import { generateRequestId } from '../conventions/logger.js';
16
+ import { runSearchAssets } from './search/search-assets.js';
17
+ import { ENUMERATOR_HARD_CAP, } from './insights/analyze-share-links.js';
18
+ // ── Asset Enumerator (audit_tagging_hygiene) ─────────────────────────
19
+ /**
20
+ * Backed by `CollageClient.enumerateAssets()` which iterates folders via
21
+ * `view-files-with-category`. Workspace-wide flat enumeration is otherwise
22
+ * Typesense-only.
23
+ *
24
+ * Returns a typed `UPSTREAM` error if folder listing 402s (current
25
+ * BREZ-workspace state) — the audit tool surfaces that to the caller as
26
+ * a structured failure instead of an empty result.
27
+ */
28
+ export function buildAssetEnumerator(client) {
29
+ return {
30
+ enumerateAssets: async () => {
31
+ const res = await client.enumerateAssets();
32
+ if (!res.ok)
33
+ return { ok: false, error: res.error };
34
+ return { ok: true, assets: res.data.map((a) => ({ id: a.id, tags: a.tags })) };
35
+ },
36
+ };
37
+ }
38
+ /** Helper for tests / future swap-in: enumerator backed by an in-memory list. */
39
+ export function fixedAssetEnumerator(assets) {
40
+ return {
41
+ enumerateAssets: async () => ({ ok: true, assets: [...assets] }),
42
+ };
43
+ }
44
+ // ── Search adapter (selection 'query' path) ──────────────────────────
45
+ /**
46
+ * Default unavailable adapter — kept for tests and for the
47
+ * pre-Typesense-provisioned mode.
48
+ */
49
+ export function buildSearchAdapter(_client) {
50
+ return {
51
+ searchAssets: async () => ({
52
+ ok: false,
53
+ error: createToolError('UPSTREAM', 'Asset search is not available: the public Typesense host has not ' +
54
+ 'been provisioned yet. Use `ids` or `collection_id` selection.'),
55
+ }),
56
+ };
57
+ }
58
+ /**
59
+ * Search adapter backed by the Collage Nuxt search proxy. Bridges the
60
+ * bulk-tag tools' `AssetSearchAdapter` interface to the workspace-scoped
61
+ * `search_assets` runner. Returns up to the first page (per_page=250) of
62
+ * hits, projected down to the `ResolvedAsset` shape expected by the
63
+ * resolver. The `tags` array is only present when the index actually
64
+ * has tags indexed for the row — empty when the indexer has not yet
65
+ * caught up.
66
+ */
67
+ export function buildTypesenseSearchAdapter(searchClient, workspaceId) {
68
+ return {
69
+ searchAssets: async (query) => {
70
+ const requestId = generateRequestId();
71
+ const result = await runSearchAssets(searchClient, workspaceId, {
72
+ q: query,
73
+ page: 1,
74
+ per_page: 250,
75
+ sort_by: '_text_match:desc,modified_at:desc',
76
+ projection: 'verbose',
77
+ }, requestId);
78
+ if (result.isError === true) {
79
+ const sc = result.structuredContent;
80
+ const err = sc.error ?? { code: 'UPSTREAM', message: 'unknown search failure', retriable: true };
81
+ return { ok: false, error: createToolError(err.code, err.message) };
82
+ }
83
+ const sc = result.structuredContent;
84
+ const assets = [];
85
+ for (const hit of sc.hits) {
86
+ const asset = { id: String(hit.id) };
87
+ if (hit.display_file_name !== null && hit.display_file_name !== undefined) {
88
+ asset.display_file_name = hit.display_file_name;
89
+ }
90
+ if (Array.isArray(hit.tags)) {
91
+ asset.tags = hit.tags;
92
+ }
93
+ assets.push(asset);
94
+ }
95
+ return { ok: true, assets };
96
+ },
97
+ };
98
+ }
99
+ // ── rename_asset client ──────────────────────────────────────────────
100
+ export function buildRenameAssetClient(client) {
101
+ return {
102
+ getCurrentName: async (assetId) => {
103
+ const res = await client.getAssetDetails(assetId);
104
+ if (!res.ok)
105
+ return null;
106
+ const name = res.data.display_file_name;
107
+ return name === '' ? null : name;
108
+ },
109
+ renameAsset: async (assetId, newName) => {
110
+ const res = await client.renameAsset(assetId, newName);
111
+ if (!res.ok)
112
+ return { ok: false, error: res.error };
113
+ return { ok: true };
114
+ },
115
+ };
116
+ }
117
+ // ── update_asset_metadata client ─────────────────────────────────────
118
+ /**
119
+ * `view-detail` carries `display_file_name` and `description` directly. We
120
+ * map `display_file_name` onto the tool's `title` slot (the
121
+ * Admin-Frontend rename UI does the same — there is no separate "title"
122
+ * column in `digital_assets`).
123
+ *
124
+ * Custom fields come from a separate `get-custom-fields` call, projected
125
+ * by `name` (with `field_name` as fallback) into `Record<string, CustomFieldValue>`.
126
+ * Update side: we re-fetch the same projection, overwrite each `value` for
127
+ * the requested keys, and POST the full array back via `update-custom-fields`.
128
+ */
129
+ export function buildUpdateAssetMetadataClient(client) {
130
+ return {
131
+ getMetadata: async (assetId) => {
132
+ const detail = await client.getAssetDetails(assetId);
133
+ if (!detail.ok)
134
+ return { ok: false, error: detail.error };
135
+ const fields = await client.getAssetCustomFields(assetId);
136
+ const customFields = fields.ok
137
+ ? projectCustomFieldsToValues(fields.data)
138
+ : {};
139
+ const snapshot = {
140
+ title: snapshotTitle(detail.data),
141
+ description: detail.data.description ?? null,
142
+ custom_fields: customFields,
143
+ };
144
+ return { ok: true, snapshot };
145
+ },
146
+ updateMetadata: async (assetId, patch) => {
147
+ const errors = [];
148
+ if (patch.title !== undefined) {
149
+ const r = await client.updateAssetField(assetId, 'display_file_name', patch.title);
150
+ if (!r.ok)
151
+ errors.push(r.error);
152
+ }
153
+ if (patch.description !== undefined) {
154
+ const r = await client.updateAssetField(assetId, 'description', patch.description);
155
+ if (!r.ok)
156
+ errors.push(r.error);
157
+ }
158
+ if (patch.custom_fields !== undefined) {
159
+ // Re-fetch the projection so we can overwrite values by name and
160
+ // round-trip the full row shape the upstream expects.
161
+ const current = await client.getAssetCustomFields(assetId);
162
+ if (!current.ok) {
163
+ errors.push(current.error);
164
+ }
165
+ else {
166
+ const updated = mergeCustomFieldUpdates(current.data, patch.custom_fields);
167
+ const r = await client.updateAssetCustomFields(assetId, updated);
168
+ if (!r.ok)
169
+ errors.push(r.error);
170
+ }
171
+ }
172
+ if (errors.length > 0) {
173
+ const first = errors[0];
174
+ if (first === undefined) {
175
+ return {
176
+ ok: false,
177
+ error: createToolError('INTERNAL', 'metadata update failed without a specific error'),
178
+ };
179
+ }
180
+ return { ok: false, error: first };
181
+ }
182
+ return { ok: true };
183
+ },
184
+ };
185
+ }
186
+ function snapshotTitle(detail) {
187
+ const name = detail.display_file_name;
188
+ return name === '' ? null : name;
189
+ }
190
+ function projectCustomFieldsToValues(fields) {
191
+ const out = {};
192
+ for (const f of fields) {
193
+ const key = f.name ?? f.field_name;
194
+ if (key === undefined)
195
+ continue;
196
+ out[key] = coerceToCustomFieldValue(f.value);
197
+ }
198
+ return out;
199
+ }
200
+ function coerceToCustomFieldValue(value) {
201
+ if (value === null || value === undefined)
202
+ return null;
203
+ if (typeof value === 'string' ||
204
+ typeof value === 'number' ||
205
+ typeof value === 'boolean') {
206
+ return value;
207
+ }
208
+ if (Array.isArray(value)) {
209
+ const arr = [];
210
+ for (const v of value) {
211
+ if (typeof v === 'string' || typeof v === 'number')
212
+ arr.push(v);
213
+ }
214
+ return arr;
215
+ }
216
+ // Fall back to JSON string so the diff renderer has *something* to show.
217
+ return JSON.stringify(value);
218
+ }
219
+ function mergeCustomFieldUpdates(current, patch) {
220
+ return current.map((f) => {
221
+ const key = f.name ?? f.field_name;
222
+ const next = key !== undefined && key in patch ? patch[key] : undefined;
223
+ if (next === undefined) {
224
+ // Untouched — pass through the row as the upstream expects.
225
+ return { ...f };
226
+ }
227
+ return { ...f, value: next };
228
+ });
229
+ }
230
+ // ── bulk_*_tags client ───────────────────────────────────────────────
231
+ /**
232
+ * Tag read uses `view-detail.tags` (array of `{id, tag_name}`). Write side:
233
+ * `setTags` is a full-replace — diff against current, add new via
234
+ * `add-tags-to-multiple-file`, remove others via
235
+ * `delete-tag-from-multiple-file` (called once per removed tag because the
236
+ * upstream takes a single `tag_name` per request).
237
+ */
238
+ export function buildBulkTagsClient(client) {
239
+ return {
240
+ getCurrentTags: async (assetId) => {
241
+ const res = await client.getAssetDetails(assetId);
242
+ if (!res.ok)
243
+ return { ok: false, error: res.error };
244
+ const names = (res.data.tags ?? []).map((t) => t.tag_name);
245
+ return { ok: true, tags: names };
246
+ },
247
+ setTags: async (assetId, tags) => {
248
+ const current = await client.getAssetDetails(assetId);
249
+ if (!current.ok)
250
+ return { ok: false, error: current.error };
251
+ const currentNames = (current.data.tags ?? []).map((t) => t.tag_name);
252
+ const wanted = new Set(tags);
253
+ const have = new Set(currentNames);
254
+ const toAdd = tags.filter((t) => !have.has(t));
255
+ const toRemove = currentNames.filter((t) => !wanted.has(t));
256
+ if (toAdd.length > 0) {
257
+ const r = await client.addTagsToAssets([assetId], toAdd);
258
+ if (!r.ok)
259
+ return { ok: false, error: r.error };
260
+ }
261
+ for (const name of toRemove) {
262
+ const r = await client.removeTagFromAssets([assetId], name);
263
+ if (!r.ok)
264
+ return { ok: false, error: r.error };
265
+ }
266
+ return { ok: true };
267
+ },
268
+ };
269
+ }
270
+ // ── create_collection client ─────────────────────────────────────────
271
+ export function buildCreateCollectionClient(client) {
272
+ return {
273
+ createCollection: async (input) => {
274
+ const res = await client.createCollection({
275
+ name: input.name,
276
+ description: input.description,
277
+ });
278
+ if (!res.ok)
279
+ return { ok: false, error: res.error };
280
+ return { ok: true, collection_id: String(res.data.id) };
281
+ },
282
+ addAssetsToCollection: async (collectionId, assetIds) => {
283
+ const res = await client.addAssetsToCollection(collectionId, assetIds);
284
+ if (!res.ok)
285
+ return { ok: false, error: res.error };
286
+ return { ok: true };
287
+ },
288
+ };
289
+ }
290
+ // ── update_collection client ─────────────────────────────────────────
291
+ export function buildUpdateCollectionClient(client) {
292
+ return {
293
+ getCollectionMetadata: async (collectionId) => {
294
+ // CollageClient exposes listCollections; locate the target by id.
295
+ const res = await client.listCollections();
296
+ if (!res.ok)
297
+ return { ok: false, error: res.error };
298
+ const match = res.data.find((c) => String(c.id) === collectionId);
299
+ if (match === undefined) {
300
+ return {
301
+ ok: false,
302
+ error: createToolError('NOT_FOUND', `Collection ${collectionId} not found in workspace.`),
303
+ };
304
+ }
305
+ return {
306
+ ok: true,
307
+ snapshot: {
308
+ name: match.name ?? null,
309
+ description: match.description ?? null,
310
+ },
311
+ };
312
+ },
313
+ updateCollectionMetadata: async (collectionId, patch) => {
314
+ const res = await client.updateCollection(collectionId, patch);
315
+ if (!res.ok)
316
+ return { ok: false, error: res.error };
317
+ return { ok: true };
318
+ },
319
+ };
320
+ }
321
+ // ── add/remove_to_collection client ──────────────────────────────────
322
+ export function buildCollectionMembershipClient(client) {
323
+ return {
324
+ getCollectionAssetIds: async (collectionId) => {
325
+ const res = await client.listCollections();
326
+ if (!res.ok)
327
+ return { ok: false, error: res.error };
328
+ const match = res.data.find((c) => String(c.id) === collectionId);
329
+ if (match === undefined) {
330
+ return {
331
+ ok: false,
332
+ error: createToolError('NOT_FOUND', `Collection ${collectionId} not found in workspace.`),
333
+ };
334
+ }
335
+ const ids = (match.assets_id ?? []).map((id) => String(id));
336
+ return { ok: true, asset_ids: ids };
337
+ },
338
+ addAssetsToCollection: async (collectionId, assetIds) => {
339
+ const res = await client.addAssetsToCollection(collectionId, assetIds);
340
+ if (!res.ok)
341
+ return { ok: false, error: res.error };
342
+ return { ok: true };
343
+ },
344
+ removeAssetsFromCollection: async (collectionId, assetIds) => {
345
+ const res = await client.removeAssetsFromCollection(collectionId, assetIds);
346
+ if (!res.ok)
347
+ return { ok: false, error: res.error };
348
+ return { ok: true };
349
+ },
350
+ };
351
+ }
352
+ export function buildCreateShareLinkClient(client) {
353
+ return {
354
+ createShareLink: async (request) => {
355
+ // Map the dry-run-resolved request to the right upstream endpoint:
356
+ // collection target → POST collection/generate-share-url,
357
+ // asset target → POST dashboard/generate-share-assets-url.
358
+ const sharedFields = {
359
+ title: request.title,
360
+ description: request.description,
361
+ password: request.password,
362
+ hide_download: request.hide_download,
363
+ expiration: request.expiration,
364
+ };
365
+ if (request.target_kind === 'collection' && request.collection_id !== undefined) {
366
+ const res = await client.generateCollectionShareUrl({
367
+ collection_id: request.collection_id,
368
+ assets: request.asset_ids,
369
+ ...sharedFields,
370
+ });
371
+ if (!res.ok)
372
+ return { ok: false, error: res.error };
373
+ return {
374
+ ok: true,
375
+ share_id: String(res.data.id),
376
+ share_url: res.data.share_url,
377
+ };
378
+ }
379
+ const res = await client.generateShareAssetsUrl({
380
+ assets: request.asset_ids,
381
+ category: [],
382
+ ...sharedFields,
383
+ });
384
+ if (!res.ok)
385
+ return { ok: false, error: res.error };
386
+ return {
387
+ ok: true,
388
+ share_id: String(res.data.id),
389
+ share_url: res.data.share_url,
390
+ };
391
+ },
392
+ };
393
+ }
394
+ // ── revoke_share_link client ─────────────────────────────────────────
395
+ export function buildRevokeShareLinkClient(client) {
396
+ return {
397
+ getShareLink: async (shareId) => {
398
+ const res = await client.getShareLink(shareId);
399
+ if (!res.ok)
400
+ return { ok: false, error: res.error };
401
+ const row = res.data;
402
+ const password = row.password;
403
+ const passwordProtected = typeof password === 'string' && password.length > 0;
404
+ const snapshot = {
405
+ id: String(row.id),
406
+ title: row.title ?? null,
407
+ share_url: row.share_url ?? row.share_new_url ?? null,
408
+ expiration: row.expiration ?? null,
409
+ password_protected: passwordProtected,
410
+ };
411
+ return { ok: true, snapshot };
412
+ },
413
+ revokeShareLink: async (shareId) => {
414
+ const res = await client.revokeShareLink(shareId);
415
+ if (!res.ok)
416
+ return { ok: false, error: res.error };
417
+ return { ok: true };
418
+ },
419
+ };
420
+ }
421
+ // ── create_folder / rename_folder / move_asset clients ───────────────
422
+ export function buildCreateFolderClient(client) {
423
+ return {
424
+ createFolder: async (input) => {
425
+ const res = await client.createCategory({
426
+ folder_name: input.folder_name,
427
+ parent_id: input.parent_id,
428
+ });
429
+ if (!res.ok)
430
+ return { ok: false, error: res.error };
431
+ return { ok: true, folder_id: String(res.data.id) };
432
+ },
433
+ };
434
+ }
435
+ export function buildRenameFolderClient(client) {
436
+ return {
437
+ getCurrentFolderName: async (categoryId) => {
438
+ const list = await client.listAllCategoriesRecursive();
439
+ if (!list.ok)
440
+ return null;
441
+ const found = list.data.find((c) => String(c.id) === categoryId);
442
+ return found?.folder_name ?? null;
443
+ },
444
+ renameFolder: async (input) => {
445
+ const renameInput = {
446
+ folder_name: input.folder_name,
447
+ };
448
+ if (input.description !== undefined) {
449
+ renameInput.description = input.description;
450
+ }
451
+ const res = await client.renameCategory(input.category_id, renameInput);
452
+ if (!res.ok)
453
+ return { ok: false, error: res.error };
454
+ return { ok: true };
455
+ },
456
+ };
457
+ }
458
+ export function buildMoveAssetClient(client) {
459
+ return {
460
+ moveAsset: async (input) => {
461
+ const res = await client.moveAssetsAndFolders({
462
+ asset_ids: [input.asset_id],
463
+ folder_ids: [],
464
+ destination_id: input.destination_category_id,
465
+ });
466
+ if (!res.ok)
467
+ return { ok: false, error: res.error };
468
+ return { ok: true };
469
+ },
470
+ };
471
+ }
472
+ // ── analyze_share_links enumerator ───────────────────────────────────
473
+ //
474
+ // Walks `client.listShareLinks` once per status bucket
475
+ // (active / expired / revoked) so each row can be tagged with its
476
+ // upstream-derived status. Pages until the upstream paginator is
477
+ // exhausted or the internal cap (`ENUMERATOR_HARD_CAP`) is hit. When
478
+ // the cap is reached, the partial dataset is returned with
479
+ // `truncated: true`; the analyzer surfaces that to the caller as
480
+ // `report.truncated` so downstream renderers can flag the partial
481
+ // result.
482
+ //
483
+ // Why per-status fetches: the live `ShareLink` schema does not expose a
484
+ // `status` column; the only way to discriminate active vs expired vs
485
+ // revoked is to query each upstream `filter_by` value separately. This
486
+ // fans out to three list calls per analysis — acceptable for the read-
487
+ // only use case and easily memoized by the T5 cache layer.
488
+ const SHARE_LINK_PAGE_SIZE = 200;
489
+ export function buildShareLinkAnalyticsEnumerator(client) {
490
+ return {
491
+ enumerate: async () => {
492
+ const statuses = ['active', 'expired', 'revoked'];
493
+ const collected = [];
494
+ let truncated = false;
495
+ let lastError = null;
496
+ let anyOk = false;
497
+ for (const status of statuses) {
498
+ if (collected.length >= ENUMERATOR_HARD_CAP) {
499
+ truncated = true;
500
+ break;
501
+ }
502
+ let page = 1;
503
+ // Per-status pagination loop.
504
+ // eslint-disable-next-line no-constant-condition
505
+ while (true) {
506
+ const res = await client.listShareLinks({
507
+ page,
508
+ pageSize: SHARE_LINK_PAGE_SIZE,
509
+ filterBy: status,
510
+ });
511
+ if (!res.ok) {
512
+ // Track per-status failures so a single 4xx (e.g. 'revoked'
513
+ // unsupported) does not mask successful status fetches. If
514
+ // every status fails, surface the last error.
515
+ lastError = res.error;
516
+ break;
517
+ }
518
+ anyOk = true;
519
+ const rows = res.data.data;
520
+ for (const row of rows) {
521
+ if (collected.length >= ENUMERATOR_HARD_CAP) {
522
+ truncated = true;
523
+ break;
524
+ }
525
+ collected.push(projectShareLinkToRow(row, status));
526
+ }
527
+ if (truncated)
528
+ break;
529
+ // Stop when we've reached the last page or the upstream
530
+ // returned fewer rows than the requested page size.
531
+ const lastPage = res.data.last_page;
532
+ if (lastPage !== undefined && page >= lastPage)
533
+ break;
534
+ if (rows.length < SHARE_LINK_PAGE_SIZE)
535
+ break;
536
+ page += 1;
537
+ }
538
+ if (truncated)
539
+ break;
540
+ }
541
+ if (!anyOk && lastError !== null) {
542
+ return { ok: false, error: lastError };
543
+ }
544
+ return { ok: true, rows: collected, truncated };
545
+ },
546
+ };
547
+ }
548
+ function projectShareLinkToRow(row, status) {
549
+ // `total_visited` may be number | string | null per upstream schema.
550
+ const viewCountRaw = row.total_visited;
551
+ let viewCount = 0;
552
+ if (typeof viewCountRaw === 'number' && Number.isFinite(viewCountRaw)) {
553
+ viewCount = viewCountRaw;
554
+ }
555
+ else if (typeof viewCountRaw === 'string') {
556
+ const parsed = Number(viewCountRaw);
557
+ if (Number.isFinite(parsed))
558
+ viewCount = parsed;
559
+ }
560
+ const assetIds = (row.asset_ids ?? []).map((id) => String(id));
561
+ // `ShareLink` does not yet model `collection_id` — it's a passthrough
562
+ // field. Read it defensively without a runtime cast to `any`.
563
+ const collectionIdRaw = row['collection_id'];
564
+ const collectionId = typeof collectionIdRaw === 'string' || typeof collectionIdRaw === 'number'
565
+ ? String(collectionIdRaw)
566
+ : null;
567
+ // `expiration` is the upstream's expires_at field (string | null).
568
+ const expiresAt = typeof row.expiration === 'string' && row.expiration.length > 0
569
+ ? row.expiration
570
+ : null;
571
+ const createdAt = typeof row.created_at === 'string' && row.created_at.length > 0
572
+ ? row.created_at
573
+ : null;
574
+ const userId = row.user_id;
575
+ const createdByUserId = typeof userId === 'string' || typeof userId === 'number'
576
+ ? String(userId)
577
+ : null;
578
+ return {
579
+ id: String(row.id),
580
+ title: typeof row.title === 'string' ? row.title : null,
581
+ share_url: typeof row.share_url === 'string' && row.share_url.length > 0
582
+ ? row.share_url
583
+ : typeof row.share_new_url === 'string' && row.share_new_url.length > 0
584
+ ? row.share_new_url
585
+ : null,
586
+ view_count: viewCount,
587
+ created_at: createdAt,
588
+ expires_at: expiresAt,
589
+ status,
590
+ created_by_user_id: createdByUserId,
591
+ asset_ids: assetIds,
592
+ collection_id: collectionId,
593
+ };
594
+ }
595
+ // ── report_asset_activation dashboard reader ─────────────────────────
596
+ //
597
+ // `getDashboardCommonData()` returns `unknown` at the client layer. The
598
+ // reader narrows it through a small zod schema covering only the fields
599
+ // the activation report consumes. When the parse fails (i.e. the
600
+ // upstream shape changes or the workspace is in a degraded state that
601
+ // returns a different envelope), the reader returns `ok: true,
602
+ // snapshot: null` so the report can degrade gracefully to
603
+ // `total_assets: null` rather than failing.
604
+ import { z } from 'zod';
605
+ /**
606
+ * Narrow projection of `dashboard/common-data`. The upstream envelope
607
+ * carries dozens of opaque keys; we only require `total_assets` to be a
608
+ * non-negative integer. Several plausible field names are accepted to
609
+ * tolerate small upstream renames (the live Admin-Frontend reads either
610
+ * `total_assets` or `total_record` depending on the dashboard widget).
611
+ */
612
+ const DashboardCommonDataSchema = z
613
+ .object({
614
+ total_assets: z.number().int().nonnegative().optional(),
615
+ total_record: z.number().int().nonnegative().optional(),
616
+ data: z
617
+ .object({
618
+ total_assets: z.number().int().nonnegative().optional(),
619
+ total_record: z.number().int().nonnegative().optional(),
620
+ })
621
+ .passthrough()
622
+ .optional(),
623
+ })
624
+ .passthrough();
625
+ export function buildDashboardCommonDataReader(client) {
626
+ return {
627
+ read: async () => {
628
+ const res = await client.getDashboardCommonData();
629
+ if (!res.ok)
630
+ return { ok: false, error: res.error };
631
+ const parsed = DashboardCommonDataSchema.safeParse(res.data);
632
+ if (!parsed.success) {
633
+ // Shape regressed — surface as null so the report can degrade.
634
+ return { ok: true, snapshot: null };
635
+ }
636
+ const total = parsed.data.total_assets ??
637
+ parsed.data.total_record ??
638
+ parsed.data.data?.total_assets ??
639
+ parsed.data.data?.total_record ??
640
+ null;
641
+ if (total === null)
642
+ return { ok: true, snapshot: null };
643
+ const snapshot = { total_assets: total };
644
+ return { ok: true, snapshot };
645
+ },
646
+ };
647
+ }
648
+ //# sourceMappingURL=client-adapters.js.map