@fleox-agent-crm/agentcrm 0.0.0-stage → 0.3.1

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 (129) hide show
  1. package/LICENSE +143 -0
  2. package/README.md +223 -2
  3. package/changelog.json +76 -0
  4. package/dist/admin/admin-files.json +26 -0
  5. package/dist/admin/assets/chartsImpl-B45AAqnr.js +91 -0
  6. package/dist/admin/assets/firebase-C1x_l9P9.js +2 -0
  7. package/dist/admin/assets/gitJson-CAUSxl8d.js +3 -0
  8. package/dist/admin/assets/index-ChFauO8y.js +1181 -0
  9. package/dist/admin/assets/index-TcjySLVg.css +1 -0
  10. package/dist/admin/assets/index.esm-BFc7vwnl.js +400 -0
  11. package/dist/admin/assets/index.esm-CtEQZRb1.js +1651 -0
  12. package/dist/admin/assets/index.esm-Dum0J07z.js +16 -0
  13. package/dist/admin/assets/index.esm2017-EOzKbpUS.js +390 -0
  14. package/dist/admin/assets/lucide-react.min--rznIkmQ.js +6 -0
  15. package/dist/admin/assets/sdk-DgzLACmL.js +1717 -0
  16. package/dist/admin/config.js +3 -0
  17. package/dist/admin/favicon.svg +1 -0
  18. package/dist/admin/index.html +139 -0
  19. package/dist/admin/runtime/agentcrm-runtime.js +104 -0
  20. package/dist/admin/runtime/lucide-react.js +791 -0
  21. package/dist/admin/runtime/react-dom-client.js +17 -0
  22. package/dist/admin/runtime/react-dom.js +20 -0
  23. package/dist/admin/runtime/react-i18next.js +19 -0
  24. package/dist/admin/runtime/react-jsx-runtime.js +17 -0
  25. package/dist/admin/runtime/react.js +22 -0
  26. package/dist/agent/agentcrm.mjs +4057 -0
  27. package/dist/agent/chunks/_commonjsHelpers-ClciMIcB.mjs +14 -0
  28. package/dist/agent/chunks/activities-Cj_SIOay.mjs +227 -0
  29. package/dist/agent/chunks/api-Cz0Ubj-B.mjs +98 -0
  30. package/dist/agent/chunks/appearance-BloHbiLA.mjs +174 -0
  31. package/dist/agent/chunks/automations-DwWruxmu.mjs +698 -0
  32. package/dist/agent/chunks/backup-W3CjgkAJ.mjs +116 -0
  33. package/dist/agent/chunks/batch-CVETiJj5.mjs +142 -0
  34. package/dist/agent/chunks/compact-BGHI92tm.mjs +69 -0
  35. package/dist/agent/chunks/context-CxFfqS4D.mjs +65 -0
  36. package/dist/agent/chunks/core-Dy7vY9H6.mjs +255 -0
  37. package/dist/agent/chunks/dedupe-BepmsaHv.mjs +123 -0
  38. package/dist/agent/chunks/encoding-C9MzdP95.mjs +36 -0
  39. package/dist/agent/chunks/exportCsv-C6e6XEmB.mjs +130 -0
  40. package/dist/agent/chunks/extCheck-GgHG1dYB.mjs +200 -0
  41. package/dist/agent/chunks/extPackage-DuZ6GZhp.mjs +115 -0
  42. package/dist/agent/chunks/extScaffold-Aqyjf1nr.mjs +320 -0
  43. package/dist/agent/chunks/extensionActions-D2HtpQz4.mjs +48 -0
  44. package/dist/agent/chunks/extensions-Bp72uZ9a.mjs +164 -0
  45. package/dist/agent/chunks/featureRegistry-DSsZbhrh.mjs +149 -0
  46. package/dist/agent/chunks/fields-B0bitgmr.mjs +476 -0
  47. package/dist/agent/chunks/files-CjsPJSR6.mjs +158 -0
  48. package/dist/agent/chunks/files-D1daDgY0.mjs +367 -0
  49. package/dist/agent/chunks/firebase-DxGVQV9E.mjs +454 -0
  50. package/dist/agent/chunks/firebaseRules-iori-s7U.mjs +221 -0
  51. package/dist/agent/chunks/forms-BfuWaQjA.mjs +847 -0
  52. package/dist/agent/chunks/gitJson-fjr8mcHe.mjs +528 -0
  53. package/dist/agent/chunks/hosting-Bme7LxPc.mjs +144 -0
  54. package/dist/agent/chunks/http-Bk9ICb7I.mjs +127 -0
  55. package/dist/agent/chunks/ids-CvnmrMJX.mjs +59 -0
  56. package/dist/agent/chunks/ids-u-noYcy5.mjs +14 -0
  57. package/dist/agent/chunks/importCsv-LVmMTaWU.mjs +334 -0
  58. package/dist/agent/chunks/index-B-4W0-P4.mjs +535 -0
  59. package/dist/agent/chunks/index-BM8qeGOb.mjs +2489 -0
  60. package/dist/agent/chunks/index-Bnmav-Oh.mjs +311 -0
  61. package/dist/agent/chunks/index-CRkw3_iF.mjs +147 -0
  62. package/dist/agent/chunks/index-CWrrF1wl.mjs +111 -0
  63. package/dist/agent/chunks/index-DUSdTBkz.mjs +2988 -0
  64. package/dist/agent/chunks/index-RCmo-uld.mjs +26 -0
  65. package/dist/agent/chunks/index-w03ReRs4.mjs +9780 -0
  66. package/dist/agent/chunks/install-CbpD_7gd.mjs +188 -0
  67. package/dist/agent/chunks/jsx-runtime-Ca2uwGDr.mjs +76 -0
  68. package/dist/agent/chunks/loader-MPD9iAvh.mjs +184 -0
  69. package/dist/agent/chunks/lucide-react.min-Bt3gtepY.mjs +64 -0
  70. package/dist/agent/chunks/memory-WNR3VHUJ.mjs +56 -0
  71. package/dist/agent/chunks/migrate-CsClnnN8.mjs +182 -0
  72. package/dist/agent/chunks/modules-clWdE-K2.mjs +202 -0
  73. package/dist/agent/chunks/nodeImport-ylfRDvqt.mjs +38 -0
  74. package/dist/agent/chunks/package-B7mI6HGi.mjs +194 -0
  75. package/dist/agent/chunks/papaparse-Df7i1dHr.mjs +1309 -0
  76. package/dist/agent/chunks/pipelines-BAKpPk1w.mjs +234 -0
  77. package/dist/agent/chunks/plan-BmTat6-T.mjs +254 -0
  78. package/dist/agent/chunks/pluginRegistry-DyHtlkcr.mjs +83 -0
  79. package/dist/agent/chunks/query-C5nJB_3C.mjs +192 -0
  80. package/dist/agent/chunks/records-DcHFelq5.mjs +2195 -0
  81. package/dist/agent/chunks/registry-COfJrSmg.mjs +182 -0
  82. package/dist/agent/chunks/registry-DijAKZ24.mjs +431 -0
  83. package/dist/agent/chunks/relations-B9TynBmA.mjs +191 -0
  84. package/dist/agent/chunks/reports-BACHGccj.mjs +189 -0
  85. package/dist/agent/chunks/runtime-C0Xf0Axi.mjs +14873 -0
  86. package/dist/agent/chunks/runtimeConfig-CCOFg1RM.mjs +250 -0
  87. package/dist/agent/chunks/s3-Dz50-NuA.mjs +903 -0
  88. package/dist/agent/chunks/schema-C4lSslXv.mjs +373 -0
  89. package/dist/agent/chunks/schema-DnLj8dUO.mjs +213 -0
  90. package/dist/agent/chunks/sdk-whvfg786.mjs +43644 -0
  91. package/dist/agent/chunks/seed-6MRmQQBR.mjs +541 -0
  92. package/dist/agent/chunks/server-DC3t8r30.mjs +242 -0
  93. package/dist/agent/chunks/settings-Di8nzNEL.mjs +111 -0
  94. package/dist/agent/chunks/standardObjects-CheqfDqa.mjs +243 -0
  95. package/dist/agent/chunks/stats-BfuLWhVV.mjs +51 -0
  96. package/dist/agent/chunks/store-5gqRSKXg.mjs +122 -0
  97. package/dist/agent/chunks/themes-DPZFv3yc.mjs +43 -0
  98. package/dist/agent/chunks/tools-iSH-yMpl.mjs +884 -0
  99. package/dist/agent/chunks/types-BOy9PUl7.mjs +44 -0
  100. package/dist/agent/chunks/users-DI1BvS0f.mjs +107 -0
  101. package/dist/agent/chunks/utils-DmI8FtXP.mjs +83 -0
  102. package/dist/agent/chunks/views-DH1n9Lrt.mjs +109 -0
  103. package/dist/version.json +7 -0
  104. package/package.json +97 -4
  105. package/plugin/.claude-plugin/plugin.json +17 -0
  106. package/plugin/skills/agentcrm-admin/SKILL.md +191 -0
  107. package/plugin/skills/agentcrm-admin/references/appearance.md +26 -0
  108. package/plugin/skills/agentcrm-admin/references/import.md +16 -0
  109. package/plugin/skills/agentcrm-admin/references/records.md +30 -0
  110. package/plugin/skills/agentcrm-admin/references/schema.md +59 -0
  111. package/plugin/skills/agentcrm-extensions/SKILL.md +93 -0
  112. package/plugin/skills/agentcrm-extensions/references/module-recipes.md +213 -0
  113. package/plugin/skills/agentcrm-extensions/references/runtime-api.md +41 -0
  114. package/plugin/skills/agentcrm-extensions/references/testing.md +78 -0
  115. package/plugin/skills/agentcrm-extensions/references/theme-recipes.md +65 -0
  116. package/plugin/skills/agentcrm-install/SKILL.md +116 -0
  117. package/plugin/skills/agentcrm-install/references/firebase.md +18 -0
  118. package/plugin/skills/agentcrm-install/references/github.md +20 -0
  119. package/plugin/skills/agentcrm-install/references/mcp.md +17 -0
  120. package/plugin/skills/agentcrm-install/references/plans.md +45 -0
  121. package/plugin/skills/agentcrm-install/references/s3.md +24 -0
  122. package/plugin/skills/agentcrm-manage/SKILL.md +108 -0
  123. package/plugin/skills/agentcrm-manage/references/cron.md +11 -0
  124. package/plugin/skills/agentcrm-manage/references/doctor.md +18 -0
  125. package/plugin/skills/agentcrm-sales/SKILL.md +92 -0
  126. package/plugin/skills/agentcrm-sales/references/enrichment.md +13 -0
  127. package/plugin/skills/agentcrm-sales/references/playbooks.md +29 -0
  128. package/plugin/skills/agentcrm-sales/references/weekly-report.md +27 -0
  129. package/types/agentcrm-runtime.d.ts +894 -0
@@ -0,0 +1,367 @@
1
+ import { createRequire as __acrmRequire } from "node:module";
2
+ import { fileURLToPath as __acrmPath } from "node:url";
3
+ import { dirname as __acrmDirname } from "node:path";
4
+ const require2 = __acrmRequire(import.meta.url);
5
+ const __filename = __acrmPath(import.meta.url);
6
+ const __dirname = __acrmDirname(__filename);
7
+ import { sanitizeFileName, guessMime, sha256Hex, thumbIdOf, fileKind, DEFAULT_MAX_UPLOAD_MB } from "./files-CjsPJSR6.mjs";
8
+ import { n as newId } from "./ids-CvnmrMJX.mjs";
9
+ import { f as fileStoreOf, F as FileStoreUnavailableError } from "./index-RCmo-uld.mjs";
10
+ import { m as mapLimit } from "./http-Bk9ICb7I.mjs";
11
+ import { assertCan, OpsError, actorId } from "./context-CxFfqS4D.mjs";
12
+ import { s as sameTarget, e as fileFieldIds, g as fileIdsIn, f as fileTargetPatch, h as referencedFileIds } from "./relations-B9TynBmA.mjs";
13
+ import { timelineScope } from "./activities-Cj_SIOay.mjs";
14
+ import { updateRecord } from "./records-DcHFelq5.mjs";
15
+ import { fileOf, requireFile, filesOf, recordOf, activitiesOf, settingsDoc, asDoc, requireRecord, objectOf, requireObject, recordsOf } from "./store-5gqRSKXg.mjs";
16
+ import "./index-BM8qeGOb.mjs";
17
+ import "./pluginRegistry-DyHtlkcr.mjs";
18
+ import "./fields-B0bitgmr.mjs";
19
+ import "./query-C5nJB_3C.mjs";
20
+ import "../agentcrm.mjs";
21
+ import "node:util";
22
+ import "node:fs";
23
+ import "node:path";
24
+ import "node:url";
25
+ import "node:crypto";
26
+ import "node:os";
27
+ import "node:child_process";
28
+ import "node:http";
29
+ import "./standardObjects-CheqfDqa.mjs";
30
+ const ORPHAN_GRACE_MS = 864e5;
31
+ function maxUploadBytes(ctx) {
32
+ const mb = settingsDoc(ctx.store)?.maxUploadMb;
33
+ return Math.round((typeof mb === "number" && mb > 0 ? mb : DEFAULT_MAX_UPLOAD_MB) * 1024 * 1024);
34
+ }
35
+ function resolveTarget(ctx, ref) {
36
+ const id = typeof ref === "string" ? ref : ref.id;
37
+ const kind = typeof ref === "string" ? void 0 : ref.kind;
38
+ if (kind !== "activity") {
39
+ const r = recordOf(ctx.store, id);
40
+ if (r && !r.deletedAt) return { kind: "record", object: r.object, id: r.id };
41
+ if (kind === "record") throw new OpsError("validation", `Record "${id}" not found.`);
42
+ }
43
+ const a = ctx.store.get("activities", id);
44
+ if (a && !a.deletedAt) return { kind: "activity", id: a.id };
45
+ throw new OpsError("validation", `Target "${id}" is neither a record nor an activity.`);
46
+ }
47
+ function uniqueTargets(targets) {
48
+ return targets.filter((t, i) => targets.findIndex((x) => sameTarget(x, t)) === i);
49
+ }
50
+ function activityAttachmentMutations(ctx, fileId, add, remove = []) {
51
+ const out = [];
52
+ for (const id of /* @__PURE__ */ new Set([...add, ...remove])) {
53
+ const a = ctx.store.get("activities", id);
54
+ if (!a) continue;
55
+ const current = a.attachments ?? [];
56
+ const next = add.includes(id) ? current.includes(fileId) ? current : [...current, fileId] : current.filter((x) => x !== fileId);
57
+ if (next.length === current.length && next.every((x, i) => x === current[i])) continue;
58
+ out.push({ op: "patch", collection: "activities", id, fields: { attachments: next.length ? next : void 0, updatedAt: ctx.now() } });
59
+ }
60
+ return out;
61
+ }
62
+ async function uploadFile(ctx, input) {
63
+ assertCan(ctx, "records.write");
64
+ if (!(input.bytes instanceof Uint8Array)) throw new OpsError("validation", "uploadFile: bytes must be a Uint8Array.");
65
+ const name = sanitizeFileName(input.name);
66
+ const limit = maxUploadBytes(ctx);
67
+ if (input.bytes.length > limit) {
68
+ throw new OpsError("validation", `${name} is too large (${Math.ceil(input.bytes.length / 1048576)} MB > ${Math.round(limit / 1048576)} MB, Settings → General → Max upload size).`);
69
+ }
70
+ if (input.bytes.length === 0) throw new OpsError("validation", `${name} is empty.`);
71
+ const targets = uniqueTargets((input.targets ?? []).map((t) => resolveTarget(ctx, t)));
72
+ const files = fileStoreOf(ctx.store);
73
+ const now = ctx.now();
74
+ const id = input.id ?? newId(now);
75
+ if (fileOf(ctx.store, id)) throw new OpsError("conflict", `File "${id}" already exists.`);
76
+ const mime = guessMime(name, input.mime);
77
+ const meta = {
78
+ id,
79
+ name,
80
+ mime,
81
+ size: input.bytes.length,
82
+ ...input.width ? { width: Math.round(input.width) } : {},
83
+ ...input.height ? { height: Math.round(input.height) } : {},
84
+ ...input.thumbnail ? { thumbId: thumbIdOf(id) } : {},
85
+ sha256: await sha256Hex(input.bytes),
86
+ createdAt: now,
87
+ updatedAt: now,
88
+ ...actorId(ctx) ? { createdBy: actorId(ctx) } : {},
89
+ targets
90
+ };
91
+ const blobs = [{ id, bytes: input.bytes, meta: { name, mime } }];
92
+ if (input.thumbnail) blobs.push({ id: thumbIdOf(id), bytes: input.thumbnail.bytes, meta: { name: `thumb-${name}`, mime: input.thumbnail.mime ?? "image/webp" } });
93
+ try {
94
+ if (files.putMany) await files.putMany(blobs);
95
+ else for (const b of blobs) await files.put(b.id, b.bytes, b.meta);
96
+ } catch (err) {
97
+ if (err instanceof FileStoreUnavailableError) throw new OpsError("invalid", err.message);
98
+ throw err;
99
+ }
100
+ const mutations = [{ op: "set", collection: "files", doc: asDoc(meta) }];
101
+ mutations.push(...activityAttachmentMutations(ctx, id, targets.filter((t) => t.kind === "activity").map((t) => t.id)));
102
+ await ctx.store.write(mutations, { message: `Upload ${name}` });
103
+ return requireFile(ctx.store, id);
104
+ }
105
+ async function attachFile(ctx, fileId, target) {
106
+ assertCan(ctx, "records.write");
107
+ const file = requireFile(ctx.store, fileId);
108
+ const t = resolveTarget(ctx, target);
109
+ const mutations = [];
110
+ const patch = fileTargetPatch(ctx, file, [t]);
111
+ if (patch) mutations.push(patch);
112
+ if (t.kind === "activity") mutations.push(...activityAttachmentMutations(ctx, fileId, [t.id]));
113
+ if (mutations.length) await ctx.store.write(mutations, { message: `Attach ${file.name}` });
114
+ return requireFile(ctx.store, fileId);
115
+ }
116
+ async function detachFile(ctx, fileId, target) {
117
+ assertCan(ctx, "records.write");
118
+ const file = requireFile(ctx.store, fileId);
119
+ const id = typeof target === "string" ? target : target.id;
120
+ const t = file.targets.find((x) => x.id === id);
121
+ if (!t) return file;
122
+ const mutations = [];
123
+ const patch = fileTargetPatch(ctx, file, [], [t]);
124
+ if (patch) mutations.push(patch);
125
+ if (t.kind === "activity") mutations.push(...activityAttachmentMutations(ctx, fileId, [], [t.id]));
126
+ if (t.kind === "record") mutations.push(...recordFieldCleanup(ctx, fileId, [t.id]));
127
+ await ctx.store.write(mutations, { message: `Detach ${file.name}` });
128
+ return requireFile(ctx.store, fileId);
129
+ }
130
+ function recordFieldCleanup(ctx, fileId, onlyRecords) {
131
+ const out = [];
132
+ const now = ctx.now();
133
+ for (const r of recordsOf(ctx.store, void 0, { includeDeleted: true })) {
134
+ if (onlyRecords && !onlyRecords.includes(r.id)) continue;
135
+ const fields = {};
136
+ for (const f of fileFieldIds(ctx, r.object)) {
137
+ const ids = fileIdsIn(r.fields[f]);
138
+ if (!ids.includes(fileId)) continue;
139
+ const rest = ids.filter((x) => x !== fileId);
140
+ fields[`fields.${f}`] = Array.isArray(r.fields[f]) ? rest.length ? rest : void 0 : void 0;
141
+ }
142
+ if (Object.keys(fields).length) out.push({ op: "patch", collection: "records", id: r.id, fields: { ...fields, updatedAt: now } });
143
+ }
144
+ return out;
145
+ }
146
+ function getFile(ctx, id, opts = {}) {
147
+ assertCan(ctx, "records.read");
148
+ return requireFile(ctx.store, id, opts);
149
+ }
150
+ async function getFileBytes(ctx, id, opts = {}) {
151
+ assertCan(ctx, "records.read");
152
+ const file = requireFile(ctx.store, id, { includeDeleted: true });
153
+ const files = fileStoreOf(ctx.store);
154
+ if (opts.thumb && file.thumbId) {
155
+ const thumb = await files.get(file.thumbId).catch(() => null);
156
+ if (thumb) return thumb;
157
+ }
158
+ const bytes = await files.get(file.id);
159
+ if (!bytes) throw new OpsError("notFound", `The bytes of ${file.name} are missing from the storage.`);
160
+ return bytes;
161
+ }
162
+ function listFiles(ctx, target, opts = {}) {
163
+ assertCan(ctx, "records.read");
164
+ const all = filesOf(ctx.store, { includeDeleted: opts.includeDeleted });
165
+ let items;
166
+ if (!target) {
167
+ items = all.map((f) => ({ ...f, via: f.targets[0] ? { kind: f.targets[0].kind, id: f.targets[0].id } : { kind: "record", id: "" } }));
168
+ } else {
169
+ const id = typeof target === "string" ? target : target.id;
170
+ const activity = ctx.store.get("activities", id);
171
+ const record = recordOf(ctx.store, id);
172
+ if (!record && !activity) throw new OpsError("notFound", `"${id}" is neither a record nor an activity.`);
173
+ const byId = new Map(all.map((f) => [f.id, f]));
174
+ const out = /* @__PURE__ */ new Map();
175
+ const add = (f, via) => {
176
+ if (f && !out.has(f.id)) out.set(f.id, { ...f, via });
177
+ };
178
+ if (activity && !record) {
179
+ for (const f of all) if (f.targets.some((t) => t.kind === "activity" && t.id === id)) add(f, { kind: "activity", id });
180
+ for (const fid of activity.attachments ?? []) add(byId.get(fid), { kind: "activity", id });
181
+ } else if (record) {
182
+ const scope = opts.includeRelated ? timelineScope(ctx, record.id, true) : /* @__PURE__ */ new Set([record.id]);
183
+ const fieldIdsOf = (r) => fileFieldIds(ctx, r.object).flatMap((f) => fileIdsIn(r.fields[f]));
184
+ for (const f of all) if (f.targets.some((t) => t.kind === "record" && t.id === record.id)) add(f, { kind: "record", id: record.id });
185
+ for (const fid of fieldIdsOf(record)) add(byId.get(fid), { kind: "record", id: record.id });
186
+ if (opts.includeActivities !== false || opts.includeRelated) {
187
+ for (const a of activitiesOf(ctx.store)) {
188
+ if (!a.targets.some((t) => scope.has(t.id))) continue;
189
+ for (const f of all) if (f.targets.some((t) => t.kind === "activity" && t.id === a.id)) add(f, { kind: "activity", id: a.id });
190
+ for (const fid of a.attachments ?? []) add(byId.get(fid), { kind: "activity", id: a.id });
191
+ }
192
+ }
193
+ if (opts.includeRelated) {
194
+ for (const rid of scope) {
195
+ if (rid === record.id) continue;
196
+ const r = recordOf(ctx.store, rid);
197
+ if (!r || r.deletedAt) continue;
198
+ for (const f of all) if (f.targets.some((t) => t.kind === "record" && t.id === rid)) add(f, { kind: "related", id: rid });
199
+ for (const fid of fieldIdsOf(r)) add(byId.get(fid), { kind: "related", id: rid });
200
+ }
201
+ }
202
+ }
203
+ items = [...out.values()];
204
+ }
205
+ if (opts.kind) items = items.filter((f) => fileKind(f.mime, f.name) === opts.kind);
206
+ const q = opts.search?.trim().toLowerCase();
207
+ if (q) items = items.filter((f) => f.name.toLowerCase().includes(q));
208
+ items.sort((a, b) => b.createdAt - a.createdAt || (a.id < b.id ? 1 : -1));
209
+ return opts.limit ? items.slice(0, opts.limit) : items;
210
+ }
211
+ function fileUsage(ctx) {
212
+ assertCan(ctx, "records.read");
213
+ const usage = { count: 0, totalBytes: 0, images: 0, deleted: 0, deletedBytes: 0 };
214
+ for (const f of filesOf(ctx.store, { includeDeleted: true })) {
215
+ if (f.deletedAt) {
216
+ usage.deleted++;
217
+ usage.deletedBytes += f.size;
218
+ continue;
219
+ }
220
+ usage.count++;
221
+ usage.totalBytes += f.size;
222
+ if (fileKind(f.mime, f.name) === "image") usage.images++;
223
+ }
224
+ return usage;
225
+ }
226
+ async function renameFile(ctx, id, name) {
227
+ assertCan(ctx, "records.write");
228
+ requireFile(ctx.store, id);
229
+ const clean = sanitizeFileName(name);
230
+ await ctx.store.write([{ op: "patch", collection: "files", id, fields: { name: clean, updatedAt: ctx.now() } }], { message: "Rename file" });
231
+ return requireFile(ctx.store, id);
232
+ }
233
+ async function deleteFile(ctx, id) {
234
+ assertCan(ctx, "records.write");
235
+ requireFile(ctx.store, id);
236
+ const now = ctx.now();
237
+ const mutations = [{ op: "patch", collection: "files", id, fields: { deletedAt: now, updatedAt: now } }];
238
+ const holders = activitiesOf(ctx.store, { includeDeleted: true }).filter((a) => a.attachments?.includes(id)).map((a) => a.id);
239
+ mutations.push(...activityAttachmentMutations(ctx, id, [], holders));
240
+ mutations.push(...recordFieldCleanup(ctx, id));
241
+ await ctx.store.write(mutations, { message: "Delete file" });
242
+ return { id };
243
+ }
244
+ async function restoreFile(ctx, id) {
245
+ assertCan(ctx, "records.write");
246
+ const f = requireFile(ctx.store, id, { includeDeleted: true });
247
+ if (!f.deletedAt) return f;
248
+ const mutations = [{ op: "patch", collection: "files", id, fields: { deletedAt: void 0, updatedAt: ctx.now() } }];
249
+ mutations.push(...activityAttachmentMutations(ctx, id, f.targets.filter((t) => t.kind === "activity").map((t) => t.id)));
250
+ await ctx.store.write(mutations, { message: "Restore file" });
251
+ return requireFile(ctx.store, id);
252
+ }
253
+ async function purgeFiles(ctx, opts = {}) {
254
+ assertCan(ctx, "compact");
255
+ const report = { purged: [], blobsDeleted: 0, orphanBlobs: 0, errors: [], dryRun: !!opts.dryRun };
256
+ const store = ctx.store.files;
257
+ const now = ctx.now();
258
+ const cutoff = now - (opts.olderThanDays ?? 0) * 864e5;
259
+ const referenced = referencedFileIds(ctx);
260
+ const exists = (t) => t.kind === "record" ? !!recordOf(ctx.store, t.id) : !!ctx.store.get("activities", t.id);
261
+ const all = filesOf(ctx.store, { includeDeleted: true });
262
+ const doomed = all.filter((f) => {
263
+ if (f.deletedAt) return f.deletedAt <= cutoff;
264
+ if (referenced.has(f.id)) return false;
265
+ const live = f.targets.filter(exists);
266
+ if (live.length) return false;
267
+ return f.targets.length > 0 || f.createdAt <= now - ORPHAN_GRACE_MS;
268
+ });
269
+ if (opts.dryRun || !store) {
270
+ report.purged = doomed.map((f) => f.id);
271
+ return report;
272
+ }
273
+ const done = [];
274
+ await mapLimit(doomed, 4, async (f) => {
275
+ try {
276
+ await store.delete(f.id);
277
+ if (f.thumbId) await store.delete(f.thumbId);
278
+ report.blobsDeleted += f.thumbId ? 2 : 1;
279
+ done.push(f.id);
280
+ } catch (err) {
281
+ report.errors.push({ id: f.id, message: err.message });
282
+ }
283
+ });
284
+ if (done.length) {
285
+ const mutations = done.map((id) => ({ op: "delete", collection: "files", id }));
286
+ for (const id of done) {
287
+ const holders = activitiesOf(ctx.store, { includeDeleted: true }).filter((a) => a.attachments?.includes(id)).map((a) => a.id);
288
+ mutations.push(...activityAttachmentMutations(ctx, id, [], holders));
289
+ }
290
+ await ctx.store.write(mutations, { message: `Purge ${done.length} files` });
291
+ }
292
+ report.purged = done;
293
+ if (store.list) {
294
+ try {
295
+ const known = /* @__PURE__ */ new Set();
296
+ for (const f of filesOf(ctx.store, { includeDeleted: true })) {
297
+ known.add(f.id);
298
+ if (f.thumbId) known.add(f.thumbId);
299
+ }
300
+ const orphans = (await store.list()).filter((id) => !known.has(id));
301
+ await mapLimit(orphans, 4, (id) => store.delete(id));
302
+ report.orphanBlobs = orphans.length;
303
+ } catch (err) {
304
+ report.errors.push({ id: "*", message: `Listing stored files failed: ${err.message}` });
305
+ }
306
+ }
307
+ return report;
308
+ }
309
+ function imageFieldOf(ctx, record, fieldId) {
310
+ const object = requireObject(ctx.store, record.object);
311
+ const id = fieldId ?? object.imageField;
312
+ if (!id) throw new OpsError("validation", `${object.labelPlural} have no image field (Settings → Data model → ${object.labelPlural} → image field).`);
313
+ const field = object.fields.find((f) => f.id === id);
314
+ if (!field) throw new OpsError("validation", `Unknown field "${id}" on ${object.labelPlural}.`);
315
+ if (field.type !== "image") throw new OpsError("validation", `${field.label} is not an image field.`);
316
+ return id;
317
+ }
318
+ async function setRecordImage(ctx, recordId, fieldId, file) {
319
+ assertCan(ctx, "records.write");
320
+ const record = requireRecord(ctx.store, recordId);
321
+ const field = imageFieldOf(ctx, record, fieldId);
322
+ if (!/^image\//.test(guessMime(file.name, file.mime))) throw new OpsError("validation", `${sanitizeFileName(file.name)} is not an image.`);
323
+ const previous = typeof record.fields[field] === "string" ? record.fields[field] : void 0;
324
+ const meta = await uploadFile(ctx, { ...file, targets: [{ kind: "record", object: record.object, id: record.id }] });
325
+ const updated = await updateRecord(ctx, recordId, { [field]: meta.id });
326
+ if (previous && previous !== meta.id) await retireImage(ctx, previous, record.id);
327
+ return { record: updated, file: meta };
328
+ }
329
+ async function clearRecordImage(ctx, recordId, fieldId) {
330
+ assertCan(ctx, "records.write");
331
+ const record = requireRecord(ctx.store, recordId);
332
+ const field = imageFieldOf(ctx, record, fieldId);
333
+ const previous = typeof record.fields[field] === "string" ? record.fields[field] : void 0;
334
+ const updated = await updateRecord(ctx, recordId, { [field]: null });
335
+ if (previous) await retireImage(ctx, previous, record.id);
336
+ return updated;
337
+ }
338
+ async function retireImage(ctx, fileId, recordId) {
339
+ const f = fileOf(ctx.store, fileId);
340
+ if (!f || f.deletedAt) return;
341
+ if (referencedFileIds(ctx).has(fileId)) return;
342
+ if (f.targets.some((t) => !(t.kind === "record" && t.id === recordId))) return;
343
+ await deleteFile(ctx, fileId);
344
+ }
345
+ function recordImageId(ctx, record) {
346
+ const object = objectOf(ctx.store, record.object);
347
+ const id = object?.imageField ? record.fields[object.imageField] : void 0;
348
+ return typeof id === "string" && id ? id : void 0;
349
+ }
350
+ export {
351
+ ORPHAN_GRACE_MS,
352
+ attachFile,
353
+ clearRecordImage,
354
+ deleteFile,
355
+ detachFile,
356
+ fileUsage,
357
+ getFile,
358
+ getFileBytes,
359
+ listFiles,
360
+ maxUploadBytes,
361
+ purgeFiles,
362
+ recordImageId,
363
+ renameFile,
364
+ restoreFile,
365
+ setRecordImage,
366
+ uploadFile
367
+ };