@fleox-agent-crm/agentcrm 0.0.0-stage → 0.3.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 (129) hide show
  1. package/LICENSE +143 -0
  2. package/README.md +223 -2
  3. package/changelog.json +57 -0
  4. package/dist/admin/admin-files.json +26 -0
  5. package/dist/admin/assets/chartsImpl-CE2akJat.js +91 -0
  6. package/dist/admin/assets/firebase-BHSUggRo.js +2 -0
  7. package/dist/admin/assets/gitJson-BulGh2KV.js +3 -0
  8. package/dist/admin/assets/index-DidVVK7L.js +1181 -0
  9. package/dist/admin/assets/index-hMXwHSvN.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-DEDlHCPj.js +6 -0
  15. package/dist/admin/assets/sdk-CnrLnlRo.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 +3932 -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-B6OqNVWp.mjs +97 -0
  30. package/dist/agent/chunks/appearance-BaShelzt.mjs +174 -0
  31. package/dist/agent/chunks/automations-BWrvLqP6.mjs +698 -0
  32. package/dist/agent/chunks/backup-ClSmxmFZ.mjs +103 -0
  33. package/dist/agent/chunks/batch-CVETiJj5.mjs +142 -0
  34. package/dist/agent/chunks/compact-CsFPaAAq.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-B5hRvkNm.mjs +95 -0
  40. package/dist/agent/chunks/extCheck--7lmVvl7.mjs +193 -0
  41. package/dist/agent/chunks/extPackage-BGl5Oqs8.mjs +115 -0
  42. package/dist/agent/chunks/extScaffold-DqxI7R1e.mjs +320 -0
  43. package/dist/agent/chunks/extensionActions-D2HtpQz4.mjs +48 -0
  44. package/dist/agent/chunks/extensions-BP1k60md.mjs +151 -0
  45. package/dist/agent/chunks/featureRegistry-C9nbVShp.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-q8Xu5bft.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-BKU5F9WF.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-DmE_sBch.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-CpHoUFtL.mjs +188 -0
  67. package/dist/agent/chunks/jsx-runtime-Ca2uwGDr.mjs +76 -0
  68. package/dist/agent/chunks/loader-C9nm3iwx.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-CqdVQVJb.mjs +177 -0
  73. package/dist/agent/chunks/nodeImport-BPCt5uMu.mjs +38 -0
  74. package/dist/agent/chunks/package-DWbn3t2R.mjs +194 -0
  75. package/dist/agent/chunks/papaparse-DjBYnQM2.mjs +1285 -0
  76. package/dist/agent/chunks/pipelines-DCdRBNBV.mjs +234 -0
  77. package/dist/agent/chunks/plan-C0TBrU92.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-DoqiT9xR.mjs +2191 -0
  81. package/dist/agent/chunks/registry-Ceexawxl.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-BB59N6ke.mjs +189 -0
  85. package/dist/agent/chunks/runtime-DHo7ZEEO.mjs +14852 -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-7-4-qtSS.mjs +203 -0
  89. package/dist/agent/chunks/schema-DlHt2PxI.mjs +367 -0
  90. package/dist/agent/chunks/sdk-whvfg786.mjs +43644 -0
  91. package/dist/agent/chunks/seed-CugPGYZL.mjs +523 -0
  92. package/dist/agent/chunks/server-ISwlDRCN.mjs +242 -0
  93. package/dist/agent/chunks/settings-BtvR_jdm.mjs +76 -0
  94. package/dist/agent/chunks/standardObjects-CheqfDqa.mjs +243 -0
  95. package/dist/agent/chunks/stats-CfeJssSS.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-Bew8lxKx.mjs +844 -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-fO_BM5U_.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 +76 -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,203 @@
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
+ const NEUTRALS = ["zinc", "slate", "stone", "gray", "neutral"];
8
+ const RADII = ["none", "sm", "md", "lg", "xl"];
9
+ const DENSITIES = ["compact", "comfortable"];
10
+ const SIDEBAR_STYLES = ["default", "dark", "accent"];
11
+ const COLOR_MODES = ["system", "light", "dark"];
12
+ const CONTRASTS = ["normal", "high"];
13
+ const BUTTON_STYLES = ["neutral", "accent"];
14
+ const APPEARANCE_DOC_ID = "appearance";
15
+ const MAX_IMAGE_BYTES = 200 * 1024;
16
+ const MAX_CUSTOM_CSS = 50 * 1024;
17
+ const DEFAULT_ACCENT = "#3366ff";
18
+ const HEX_RE = /^#?([0-9a-f]{3}|[0-9a-f]{6})$/i;
19
+ function isHexColor(value) {
20
+ return typeof value === "string" && HEX_RE.test(value.trim());
21
+ }
22
+ function normalizeHex(value) {
23
+ const m = HEX_RE.exec(value.trim());
24
+ if (!m) throw new Error(`"${value}" is not a hex color (#rrggbb).`);
25
+ let h = m[1].toLowerCase();
26
+ if (h.length === 3) h = h.split("").map((c) => c + c).join("");
27
+ return `#${h}`;
28
+ }
29
+ function hexToRgb(hex) {
30
+ const h = normalizeHex(hex).slice(1);
31
+ return [parseInt(h.slice(0, 2), 16), parseInt(h.slice(2, 4), 16), parseInt(h.slice(4, 6), 16)];
32
+ }
33
+ function rgbToHex([r, g, b]) {
34
+ const c = (v) => Math.round(Math.min(255, Math.max(0, v))).toString(16).padStart(2, "0");
35
+ return `#${c(r)}${c(g)}${c(b)}`;
36
+ }
37
+ function mixHex(a, b, t) {
38
+ const x = hexToRgb(a);
39
+ const y = hexToRgb(b);
40
+ return rgbToHex([0, 1, 2].map((i) => x[i] + (y[i] - x[i]) * t));
41
+ }
42
+ const FALLBACK = 'ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto, sans-serif';
43
+ const SYSTEM_FONT_STACK = `Inter, ${FALLBACK}`;
44
+ function google(id, name) {
45
+ return { id, label: name, family: `"${name}", ${FALLBACK}`, google: name };
46
+ }
47
+ const FONTS = [
48
+ { id: "system", label: "System (default)", family: SYSTEM_FONT_STACK },
49
+ google("inter", "Inter"),
50
+ google("geist", "Geist"),
51
+ google("roboto", "Roboto"),
52
+ google("open-sans", "Open Sans"),
53
+ google("lato", "Lato"),
54
+ google("source-sans-3", "Source Sans 3"),
55
+ google("ibm-plex-sans", "IBM Plex Sans"),
56
+ google("dm-sans", "DM Sans"),
57
+ google("manrope", "Manrope"),
58
+ google("work-sans", "Work Sans"),
59
+ google("plus-jakarta-sans", "Plus Jakarta Sans"),
60
+ google("figtree", "Figtree"),
61
+ google("outfit", "Outfit"),
62
+ google("nunito-sans", "Nunito Sans"),
63
+ google("poppins", "Poppins"),
64
+ google("space-grotesk", "Space Grotesk")
65
+ ];
66
+ const DEFAULT_APPEARANCE = {
67
+ preset: "default",
68
+ accent: DEFAULT_ACCENT,
69
+ neutral: "zinc",
70
+ radius: "md",
71
+ density: "comfortable",
72
+ font: "system",
73
+ sidebar: "default",
74
+ defaultMode: "system",
75
+ contrast: "normal",
76
+ buttons: "neutral"
77
+ };
78
+ const APPEARANCE_KEYS = ["preset", "accent", "neutral", "radius", "density", "font", "sidebar", "defaultMode", "contrast", "buttons", "logo", "favicon", "customCss", "themeId"];
79
+ const ENUMS = {
80
+ neutral: NEUTRALS,
81
+ radius: RADII,
82
+ density: DENSITIES,
83
+ sidebar: SIDEBAR_STYLES,
84
+ defaultMode: COLOR_MODES,
85
+ contrast: CONTRASTS,
86
+ buttons: BUTTON_STYLES
87
+ };
88
+ const IMAGE_RE = /^data:image\/(png|jpeg|gif|webp|svg\+xml|x-icon|vnd\.microsoft\.icon);base64,[A-Za-z0-9+/]+=*$/;
89
+ const ID_RE = /^[a-z0-9][a-z0-9-]{0,63}$/;
90
+ function dataUrlBytes(url) {
91
+ const b64 = url.slice(url.indexOf(",") + 1);
92
+ const pad = b64.endsWith("==") ? 2 : b64.endsWith("=") ? 1 : 0;
93
+ return Math.floor(b64.length * 3 / 4) - pad;
94
+ }
95
+ function validateImageDataUrl(value, label = "image") {
96
+ if (typeof value !== "string" || !IMAGE_RE.test(value)) return `The ${label} must be a base64 image data URL (PNG, JPEG, GIF, WebP, SVG or ICO).`;
97
+ const bytes = dataUrlBytes(value);
98
+ if (bytes > MAX_IMAGE_BYTES) return `The ${label} is ${Math.round(bytes / 1024)} KB — the limit is ${MAX_IMAGE_BYTES / 1024} KB.`;
99
+ return null;
100
+ }
101
+ function validateAppearancePatch(patch) {
102
+ const errors = [];
103
+ const value = {};
104
+ if (!patch || typeof patch !== "object" || Array.isArray(patch)) return { value, errors: ["The appearance patch must be an object."] };
105
+ for (const [key, raw] of Object.entries(patch)) {
106
+ if (!APPEARANCE_KEYS.includes(key)) {
107
+ errors.push(`Unknown appearance key "${key}". Keys: ${APPEARANCE_KEYS.join(", ")}.`);
108
+ continue;
109
+ }
110
+ const k = key;
111
+ if (raw === null || raw === void 0) {
112
+ value[k] = void 0;
113
+ continue;
114
+ }
115
+ const allowed = ENUMS[k];
116
+ if (allowed) {
117
+ if (typeof raw !== "string" || !allowed.includes(raw)) errors.push(`${k} must be one of ${allowed.join(", ")}.`);
118
+ else value[k] = raw;
119
+ continue;
120
+ }
121
+ switch (k) {
122
+ case "accent":
123
+ if (!isHexColor(raw)) errors.push("accent must be a hex color like #3366ff.");
124
+ else value.accent = normalizeHex(raw);
125
+ break;
126
+ case "font":
127
+ if (typeof raw !== "string" || !FONTS.some((f) => f.id === raw)) errors.push(`font must be one of ${FONTS.map((f) => f.id).join(", ")}.`);
128
+ else value.font = raw;
129
+ break;
130
+ case "preset":
131
+ if (typeof raw !== "string" || !ID_RE.test(raw)) errors.push("preset must be a preset id.");
132
+ else value.preset = raw;
133
+ break;
134
+ case "themeId":
135
+ if (typeof raw !== "string" || !ID_RE.test(raw)) errors.push("themeId must be an extension id.");
136
+ else value.themeId = raw;
137
+ break;
138
+ case "logo":
139
+ case "favicon": {
140
+ if (raw === "") {
141
+ value[k] = void 0;
142
+ break;
143
+ }
144
+ const err = validateImageDataUrl(raw, k);
145
+ if (err) errors.push(err);
146
+ else value[k] = raw;
147
+ break;
148
+ }
149
+ case "customCss": {
150
+ if (typeof raw !== "string") errors.push("customCss must be a string.");
151
+ else if (new TextEncoder().encode(raw).length > MAX_CUSTOM_CSS) errors.push(`customCss is larger than ${MAX_CUSTOM_CSS / 1024} KB.`);
152
+ else if (/<\/?style|<script/i.test(raw)) errors.push("customCss must be plain CSS (no <style> / <script> tags).");
153
+ else value.customCss = raw.trim() ? raw : void 0;
154
+ break;
155
+ }
156
+ }
157
+ }
158
+ return { value, errors };
159
+ }
160
+ function resolveAppearance(stored, theme) {
161
+ const clean = (o) => {
162
+ if (!o) return {};
163
+ const { value } = validateAppearancePatch(Object.fromEntries(Object.entries(o).filter(([k, v]) => APPEARANCE_KEYS.includes(k) && v !== void 0 && v !== null)));
164
+ return Object.fromEntries(Object.entries(value).filter(([, v]) => v !== void 0));
165
+ };
166
+ const themeValues = clean(theme?.appearance);
167
+ delete themeValues.themeId;
168
+ delete themeValues.logo;
169
+ delete themeValues.favicon;
170
+ return { ...DEFAULT_APPEARANCE, ...themeValues, ...clean(stored) };
171
+ }
172
+ function stripPatchNulls(patch) {
173
+ return Object.fromEntries(Object.entries(patch).filter(([, v]) => v !== null && v !== void 0));
174
+ }
175
+ const schema = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({
176
+ __proto__: null,
177
+ APPEARANCE_KEYS,
178
+ DEFAULT_APPEARANCE,
179
+ dataUrlBytes,
180
+ resolveAppearance,
181
+ stripPatchNulls,
182
+ validateAppearancePatch,
183
+ validateImageDataUrl
184
+ }, Symbol.toStringTag, { value: "Module" }));
185
+ export {
186
+ APPEARANCE_DOC_ID as A,
187
+ BUTTON_STYLES as B,
188
+ CONTRASTS as C,
189
+ DEFAULT_ACCENT as D,
190
+ FONTS as F,
191
+ NEUTRALS as N,
192
+ RADII as R,
193
+ SIDEBAR_STYLES as S,
194
+ DEFAULT_APPEARANCE as a,
195
+ validateImageDataUrl as b,
196
+ APPEARANCE_KEYS as c,
197
+ COLOR_MODES as d,
198
+ DENSITIES as e,
199
+ mixHex as m,
200
+ resolveAppearance as r,
201
+ schema as s,
202
+ validateAppearancePatch as v
203
+ };
@@ -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 { c as camelId } from "./utils-DmI8FtXP.mjs";
8
+ import { colorAt, canConvertType, isEmptyFieldValue, convertValue } from "./fields-B0bitgmr.mjs";
9
+ import { s as slugId } from "./ids-CvnmrMJX.mjs";
10
+ import { d as defaultViewFor } from "./standardObjects-CheqfDqa.mjs";
11
+ import { e as FIELD_TYPES } from "../agentcrm.mjs";
12
+ import { assertCan, OpsError } from "./context-CxFfqS4D.mjs";
13
+ import { hardDeleteMutations } from "./records-DoqiT9xR.mjs";
14
+ import { requireObject, objectsOf, pipelinesOf, recordsOf, asDoc, viewsOf, thaw } from "./store-5gqRSKXg.mjs";
15
+ import "node:util";
16
+ import "node:fs";
17
+ import "node:path";
18
+ import "node:url";
19
+ import "node:crypto";
20
+ import "node:os";
21
+ import "node:child_process";
22
+ import "node:http";
23
+ import "./pluginRegistry-DyHtlkcr.mjs";
24
+ import "./query-C5nJB_3C.mjs";
25
+ import "./relations-B9TynBmA.mjs";
26
+ function listObjects(ctx) {
27
+ assertCan(ctx, "records.read");
28
+ return objectsOf(ctx.store);
29
+ }
30
+ function getObject(ctx, id) {
31
+ assertCan(ctx, "records.read");
32
+ return requireObject(ctx.store, id);
33
+ }
34
+ function describeSchema(ctx, objectId) {
35
+ assertCan(ctx, "records.read");
36
+ const objects = objectId ? [requireObject(ctx.store, objectId)] : objectsOf(ctx.store);
37
+ return objects.map((o) => ({
38
+ id: o.id,
39
+ label: o.labelPlural,
40
+ singular: o.labelSingular,
41
+ labelField: o.labelField,
42
+ ...o.imageField ? { imageField: o.imageField } : {},
43
+ standard: o.standard,
44
+ count: recordsOf(ctx.store, o.id).length,
45
+ fields: o.fields.map((f) => ({
46
+ id: f.id,
47
+ label: f.label,
48
+ type: f.type,
49
+ ...f.required ? { required: true } : {},
50
+ ...f.unique ? { unique: true } : {},
51
+ ...f.system ? { system: true } : {},
52
+ ...f.hidden ? { hidden: true } : {},
53
+ ...f.options ? { options: f.options.map((x) => ({ id: x.id, label: x.label })) } : {},
54
+ ...f.relation ? { relation: f.relation } : {},
55
+ ...f.currency ? { currency: f.currency } : {}
56
+ })),
57
+ pipelines: pipelinesOf(ctx.store, o.id).map((p) => ({ id: p.id, name: p.name, stages: p.stages.map((s) => ({ id: s.id, label: s.label, kind: s.kind, probability: s.probability })) }))
58
+ }));
59
+ }
60
+ function saveObject(ctx, object) {
61
+ return { op: "set", collection: "objects", doc: asDoc({ ...object, updatedAt: ctx.now() }) };
62
+ }
63
+ async function createObject(ctx, input) {
64
+ assertCan(ctx, "schema.write");
65
+ const singular = input.labelSingular?.trim();
66
+ const plural = input.labelPlural?.trim() || (singular ? `${singular}s` : "");
67
+ if (!singular) throw new OpsError("validation", "An object needs a name.");
68
+ const taken = objectsOf(ctx.store).map((o) => o.id);
69
+ const id = input.id ? input.id : slugId(plural, taken);
70
+ if (!/^[a-z][a-z0-9-]{1,40}$/.test(id)) throw new OpsError("validation", `Invalid object id "${id}" (lower-case letters, digits, dashes).`);
71
+ if (taken.includes(id)) throw new OpsError("conflict", `Object "${id}" already exists.`);
72
+ const now = ctx.now();
73
+ let object = {
74
+ id,
75
+ labelSingular: singular,
76
+ labelPlural: plural,
77
+ icon: input.icon ?? "Box",
78
+ color: input.color ?? colorAt(taken.length + 3),
79
+ standard: false,
80
+ labelField: "name",
81
+ fields: [{ id: "name", label: "Name", type: "text", required: true, system: true }],
82
+ createdAt: now,
83
+ updatedAt: now
84
+ };
85
+ for (const f of input.fields ?? []) object = { ...object, fields: [...object.fields, buildField(ctx, object, f)] };
86
+ const view = defaultViewFor(object);
87
+ await ctx.store.write(
88
+ [
89
+ { op: "set", collection: "objects", doc: asDoc(object) },
90
+ ...ctx.store.get("views", view.id) ? [] : [{ op: "set", collection: "views", doc: asDoc(view) }]
91
+ ],
92
+ { message: `Create object ${plural}` }
93
+ );
94
+ return requireObject(ctx.store, id);
95
+ }
96
+ async function updateObject(ctx, id, patch) {
97
+ assertCan(ctx, "schema.write");
98
+ const object = thaw(requireObject(ctx.store, id));
99
+ if (patch.labelSingular !== void 0) object.labelSingular = patch.labelSingular.trim() || object.labelSingular;
100
+ if (patch.labelPlural !== void 0) object.labelPlural = patch.labelPlural.trim() || object.labelPlural;
101
+ if (patch.icon !== void 0) object.icon = patch.icon;
102
+ if (patch.color !== void 0) object.color = patch.color;
103
+ if (patch.labelField !== void 0) {
104
+ const f = object.fields.find((x) => x.id === patch.labelField);
105
+ if (!f || !["text", "email", "url", "phone"].includes(f.type)) throw new OpsError("validation", `"${patch.labelField}" can't be the label field (needs a text field).`);
106
+ object.labelField = patch.labelField;
107
+ }
108
+ if (patch.imageField !== void 0) {
109
+ if (patch.imageField === null || patch.imageField === "") delete object.imageField;
110
+ else {
111
+ const f = object.fields.find((x) => x.id === patch.imageField);
112
+ if (!f || f.type !== "image") throw new OpsError("validation", `"${patch.imageField}" can't be the image field (needs an image field).`);
113
+ object.imageField = patch.imageField;
114
+ }
115
+ }
116
+ await ctx.store.write([saveObject(ctx, object)], { message: `Update object ${object.labelPlural}` });
117
+ return requireObject(ctx.store, id);
118
+ }
119
+ async function deleteObject(ctx, id) {
120
+ assertCan(ctx, "schema.write");
121
+ const object = requireObject(ctx.store, id);
122
+ if (object.standard) throw new OpsError("invalid", `${object.labelPlural} is a standard object and can't be deleted.`);
123
+ if (pipelinesOf(ctx.store, id).length) throw new OpsError("invalid", `Delete the pipelines of ${object.labelPlural} first.`);
124
+ const records = recordsOf(ctx.store, id, { includeDeleted: true });
125
+ const mutations = [{ op: "delete", collection: "objects", id }, ...hardDeleteMutations(ctx, records.map((r) => r.id))];
126
+ for (const v of viewsOf(ctx.store, id)) mutations.push({ op: "delete", collection: "views", id: v.id });
127
+ for (const other of objectsOf(ctx.store)) {
128
+ if (other.id === id) continue;
129
+ const kept = other.fields.filter((f) => !(f.type === "relation" && f.relation?.object === id));
130
+ if (kept.length !== other.fields.length) {
131
+ mutations.push(saveObject(ctx, { ...other, fields: kept }));
132
+ for (const f of other.fields.filter((x) => !kept.includes(x))) mutations.push(...clearFieldValues(ctx, other.id, f.id));
133
+ }
134
+ }
135
+ await ctx.store.write(dedupeMutations(mutations), { message: `Delete object ${object.labelPlural}` });
136
+ return { id, records: records.length };
137
+ }
138
+ function dedupeMutations(mutations) {
139
+ const deleted = new Set(mutations.filter((m) => m.op === "delete").map((m) => `${m.collection}/${m.id}`));
140
+ return mutations.filter((m) => m.op === "delete" || !deleted.has(`${m.collection}/${m.op === "set" ? m.doc.id : m.id}`));
141
+ }
142
+ function buildOptions(input, existing = []) {
143
+ const out = [];
144
+ (input ?? []).forEach((o, i) => {
145
+ const label = (typeof o === "string" ? o : o.label ?? "").trim();
146
+ if (!label) return;
147
+ const wanted = typeof o === "string" ? void 0 : o.id;
148
+ const prev = existing.find((e) => e.id === wanted) ?? existing.find((e) => e.label.toLowerCase() === label.toLowerCase());
149
+ const id = prev?.id ?? (wanted && !out.some((x) => x.id === wanted) ? wanted : slugId(label, out.map((x) => x.id)));
150
+ out.push({ id, label, color: (typeof o === "string" ? void 0 : o.color) ?? prev?.color ?? colorAt(i) });
151
+ });
152
+ return out;
153
+ }
154
+ function buildField(ctx, object, input) {
155
+ const label = input.label?.trim();
156
+ if (!label) throw new OpsError("validation", "A field needs a label.");
157
+ if (!FIELD_TYPES.includes(input.type)) throw new OpsError("validation", `Unknown field type "${input.type}". Types: ${FIELD_TYPES.join(", ")}.`);
158
+ const taken = new Set(object.fields.map((f) => f.id));
159
+ let id = input.id ?? camelId(label);
160
+ if (!id) id = "field";
161
+ if (!/^[a-zA-Z][a-zA-Z0-9_]{0,40}$/.test(id)) throw new OpsError("validation", `Invalid field id "${id}".`);
162
+ if (input.id && taken.has(id)) throw new OpsError("conflict", `Field "${id}" already exists on ${object.labelPlural}.`);
163
+ if (["id", "object", "ownerId", "createdAt", "updatedAt", "createdBy", "updatedBy", "deletedAt"].includes(id)) id = `${id}Field`;
164
+ for (let i = 2; taken.has(id); i++) id = `${camelId(label) || "field"}${i}`;
165
+ const field = { id, label, type: input.type };
166
+ if (input.description) field.description = input.description;
167
+ if (input.required) field.required = true;
168
+ if (input.unique) field.unique = true;
169
+ if (input.hidden) field.hidden = true;
170
+ if (input.type === "select" || input.type === "multiSelect") field.options = buildOptions(input.options);
171
+ if (input.type === "currency") field.currency = (input.currency ?? "USD").toUpperCase();
172
+ if (input.type === "relation") {
173
+ const rel = input.relation;
174
+ if (!rel?.object) throw new OpsError("validation", "A relation field needs a target object.");
175
+ requireObject(ctx.store, rel.object);
176
+ field.relation = { object: rel.object, cardinality: rel.cardinality === "many" ? "many" : "one", ...rel.inverseField ? { inverseField: rel.inverseField } : {} };
177
+ }
178
+ if (input.defaultValue !== void 0) field.defaultValue = input.defaultValue;
179
+ return field;
180
+ }
181
+ async function createField(ctx, objectId, input) {
182
+ assertCan(ctx, "schema.write");
183
+ const object = requireObject(ctx.store, objectId);
184
+ const field = buildField(ctx, object, input);
185
+ const imageField = object.imageField ?? (field.type === "image" ? field.id : void 0);
186
+ await ctx.store.write([saveObject(ctx, { ...object, ...imageField ? { imageField } : {}, fields: [...object.fields, field] })], { message: `Add field ${field.label}` });
187
+ return field;
188
+ }
189
+ function requireField(object, fieldId) {
190
+ const f = object.fields.find((x) => x.id === fieldId);
191
+ if (!f) throw new OpsError("notFound", `Field "${fieldId}" not found on ${object.labelPlural}.`);
192
+ return f;
193
+ }
194
+ function isPipelineManaged(ctx, objectId, fieldId) {
195
+ const pipelines = pipelinesOf(ctx.store, objectId);
196
+ return pipelines.some((p) => p.stageField === fieldId) || fieldId === "pipeline" && pipelines.length > 0;
197
+ }
198
+ async function updateField(ctx, objectId, fieldId, patch) {
199
+ assertCan(ctx, "schema.write");
200
+ const object = requireObject(ctx.store, objectId);
201
+ const before = requireField(object, fieldId);
202
+ let field = thaw(before);
203
+ if (patch.label !== void 0) {
204
+ if (!patch.label.trim()) throw new OpsError("validation", "A field needs a label.");
205
+ field.label = patch.label.trim();
206
+ }
207
+ if (patch.description !== void 0) field.description = patch.description || void 0;
208
+ if (patch.required !== void 0) field.required = patch.required || void 0;
209
+ if (patch.unique !== void 0) field.unique = patch.unique || void 0;
210
+ if (patch.hidden !== void 0) {
211
+ if (patch.hidden && fieldId === object.labelField) throw new OpsError("invalid", "The label field can't be hidden.");
212
+ field.hidden = patch.hidden || void 0;
213
+ }
214
+ if (patch.currency !== void 0) field.currency = patch.currency.toUpperCase();
215
+ if (patch.defaultValue !== void 0) field.defaultValue = patch.defaultValue;
216
+ const mutations = [];
217
+ let converted;
218
+ let cleared;
219
+ if (patch.type !== void 0 && patch.type !== before.type) {
220
+ if (before.system || isPipelineManaged(ctx, objectId, fieldId)) throw new OpsError("invalid", `${before.label} is a system field: its type can't change.`);
221
+ if (!FIELD_TYPES.includes(patch.type)) throw new OpsError("validation", `Unknown field type "${patch.type}".`);
222
+ if (!canConvertType(before.type, patch.type)) throw new OpsError("invalid", `Can't convert ${before.type} to ${patch.type}.`);
223
+ field = { ...field, type: patch.type };
224
+ if (patch.type === "select" || patch.type === "multiSelect") {
225
+ if (!patch.options && !field.options?.length) {
226
+ const values = /* @__PURE__ */ new Set();
227
+ for (const r of recordsOf(ctx.store, objectId, { includeDeleted: true })) {
228
+ const v = r.fields[fieldId];
229
+ for (const x of Array.isArray(v) ? v : [v]) if (x !== void 0 && x !== null && String(x).trim()) values.add(before.type === "select" || before.type === "multiSelect" ? before.options?.find((o) => o.id === x)?.label ?? String(x) : String(x).trim());
230
+ }
231
+ field.options = buildOptions([...values].slice(0, 200));
232
+ }
233
+ } else {
234
+ delete field.options;
235
+ }
236
+ if (patch.type === "currency") field.currency = field.currency ?? "USD";
237
+ if (patch.type !== "relation") delete field.relation;
238
+ converted = 0;
239
+ cleared = 0;
240
+ const migrateTo = patch.options ? { ...field, options: buildOptions(patch.options, field.options) } : field;
241
+ for (const r of recordsOf(ctx.store, objectId, { includeDeleted: true })) {
242
+ const v = r.fields[fieldId];
243
+ if (isEmptyFieldValue(before, v)) continue;
244
+ const next = convertValue(before, migrateTo, v);
245
+ if (next === void 0) cleared++;
246
+ else converted++;
247
+ mutations.push({ op: "patch", collection: "records", id: r.id, fields: { [`fields.${fieldId}`]: next } });
248
+ }
249
+ }
250
+ if (patch.options !== void 0) {
251
+ if (isPipelineManaged(ctx, objectId, fieldId)) throw new OpsError("invalid", `${before.label} options mirror the pipelines — edit the pipeline stages instead.`);
252
+ if (field.type !== "select" && field.type !== "multiSelect") throw new OpsError("invalid", `${field.label} has no options.`);
253
+ const nextOptions = buildOptions(patch.options, before.options);
254
+ const kept = new Set(nextOptions.map((o) => o.id));
255
+ if (before.type === field.type) {
256
+ for (const r of recordsOf(ctx.store, objectId, { includeDeleted: true })) {
257
+ const v = r.fields[fieldId];
258
+ if (field.type === "select" && typeof v === "string" && !kept.has(v)) mutations.push({ op: "patch", collection: "records", id: r.id, fields: { [`fields.${fieldId}`]: void 0 } });
259
+ if (field.type === "multiSelect" && Array.isArray(v) && v.some((x) => !kept.has(x))) {
260
+ const rest = v.filter((x) => kept.has(x));
261
+ mutations.push({ op: "patch", collection: "records", id: r.id, fields: { [`fields.${fieldId}`]: rest.length ? rest : void 0 } });
262
+ }
263
+ }
264
+ }
265
+ field.options = nextOptions;
266
+ }
267
+ const fields = object.fields.map((f) => f.id === fieldId ? field : f);
268
+ const nextObject = { ...object, fields };
269
+ if (object.imageField === fieldId && field.type !== "image") delete nextObject.imageField;
270
+ mutations.unshift(saveObject(ctx, nextObject));
271
+ await ctx.store.write(mutations, { message: `Update field ${field.label}` });
272
+ return { field: requireField(requireObject(ctx.store, objectId), fieldId), ...converted !== void 0 ? { converted, cleared } : {} };
273
+ }
274
+ function changeFieldType(ctx, objectId, fieldId, type, options) {
275
+ return updateField(ctx, objectId, fieldId, { type, ...options ? { options } : {} });
276
+ }
277
+ function clearFieldValues(ctx, objectId, fieldId) {
278
+ return recordsOf(ctx.store, objectId, { includeDeleted: true }).filter((r) => r.fields[fieldId] !== void 0).map((r) => ({ op: "patch", collection: "records", id: r.id, fields: { [`fields.${fieldId}`]: void 0 } }));
279
+ }
280
+ async function deleteField(ctx, objectId, fieldId) {
281
+ assertCan(ctx, "schema.write");
282
+ const object = requireObject(ctx.store, objectId);
283
+ const field = requireField(object, fieldId);
284
+ if (field.system) throw new OpsError("invalid", `${field.label} is a system field and can't be deleted.`);
285
+ if (field.standard) throw new OpsError("invalid", `${field.label} is a standard field: hide it instead of deleting it.`);
286
+ if (fieldId === object.labelField) throw new OpsError("invalid", "The label field can't be deleted.");
287
+ if (isPipelineManaged(ctx, objectId, fieldId)) throw new OpsError("invalid", `${field.label} holds pipeline stages.`);
288
+ const clears = clearFieldValues(ctx, objectId, fieldId);
289
+ const rest = { ...object, fields: object.fields.filter((f) => f.id !== fieldId) };
290
+ if (rest.imageField === fieldId) delete rest.imageField;
291
+ const mutations = [saveObject(ctx, rest), ...clears];
292
+ for (const v of viewsOf(ctx.store, objectId)) {
293
+ const usesField = v.columns.includes(fieldId) || v.filters.some((f) => f.field === fieldId) || v.sorts.some((s) => s.field === fieldId) || v.groupBy === fieldId;
294
+ if (!usesField) continue;
295
+ mutations.push({
296
+ op: "patch",
297
+ collection: "views",
298
+ id: v.id,
299
+ fields: {
300
+ columns: v.columns.filter((c) => c !== fieldId),
301
+ filters: v.filters.filter((f) => f.field !== fieldId),
302
+ sorts: v.sorts.filter((s) => s.field !== fieldId),
303
+ groupBy: v.groupBy === fieldId ? void 0 : v.groupBy
304
+ }
305
+ });
306
+ }
307
+ await ctx.store.write(mutations, { message: `Delete field ${field.label}` });
308
+ return { id: fieldId, cleared: clears.length };
309
+ }
310
+ async function reorderFields(ctx, objectId, order) {
311
+ assertCan(ctx, "schema.write");
312
+ const object = requireObject(ctx.store, objectId);
313
+ const pos = new Map(order.map((id, i) => [id, i]));
314
+ const fields = [...object.fields].sort((a, b) => (pos.get(a.id) ?? 1e6 + object.fields.indexOf(a)) - (pos.get(b.id) ?? 1e6 + object.fields.indexOf(b)));
315
+ await ctx.store.write([saveObject(ctx, { ...object, fields })], { message: "Reorder fields" });
316
+ return requireObject(ctx.store, objectId);
317
+ }
318
+ async function addOption(ctx, objectId, fieldId, option) {
319
+ const field = requireField(requireObject(ctx.store, objectId), fieldId);
320
+ const res = await updateField(ctx, objectId, fieldId, { options: [...field.options ?? [], option] });
321
+ return res.field.options[res.field.options.length - 1];
322
+ }
323
+ async function updateOption(ctx, objectId, fieldId, optionId, patch) {
324
+ const field = requireField(requireObject(ctx.store, objectId), fieldId);
325
+ if (!field.options?.some((o) => o.id === optionId)) throw new OpsError("notFound", `Option "${optionId}" not found on ${field.label}.`);
326
+ const res = await updateField(ctx, objectId, fieldId, { options: field.options.map((o) => o.id === optionId ? { ...o, ...patch } : o) });
327
+ return res.field.options.find((o) => o.id === optionId);
328
+ }
329
+ async function removeOption(ctx, objectId, fieldId, optionId, opts = {}) {
330
+ assertCan(ctx, "schema.write");
331
+ const object = requireObject(ctx.store, objectId);
332
+ const field = requireField(object, fieldId);
333
+ if (isPipelineManaged(ctx, objectId, fieldId)) throw new OpsError("invalid", `${field.label} options mirror the pipelines — edit the pipeline stages instead.`);
334
+ if (!field.options?.some((o) => o.id === optionId)) throw new OpsError("notFound", `Option "${optionId}" not found on ${field.label}.`);
335
+ if (opts.replaceWith && !field.options.some((o) => o.id === opts.replaceWith && o.id !== optionId)) throw new OpsError("validation", `Unknown replacement option "${opts.replaceWith}".`);
336
+ const mutations = [saveObject(ctx, { ...object, fields: object.fields.map((f) => f.id === fieldId ? { ...f, options: f.options.filter((o) => o.id !== optionId) } : f) })];
337
+ let moved = 0;
338
+ for (const r of recordsOf(ctx.store, objectId, { includeDeleted: true })) {
339
+ const v = r.fields[fieldId];
340
+ if (field.type === "select" && v === optionId) {
341
+ moved++;
342
+ mutations.push({ op: "patch", collection: "records", id: r.id, fields: { [`fields.${fieldId}`]: opts.replaceWith } });
343
+ } else if (field.type === "multiSelect" && Array.isArray(v) && v.includes(optionId)) {
344
+ moved++;
345
+ const next = [...new Set(v.map((x) => x === optionId ? opts.replaceWith : x).filter(Boolean))];
346
+ mutations.push({ op: "patch", collection: "records", id: r.id, fields: { [`fields.${fieldId}`]: next.length ? next : void 0 } });
347
+ }
348
+ }
349
+ await ctx.store.write(mutations, { message: `Remove option from ${field.label}` });
350
+ return { moved };
351
+ }
352
+ export {
353
+ addOption,
354
+ changeFieldType,
355
+ createField,
356
+ createObject,
357
+ deleteField,
358
+ deleteObject,
359
+ describeSchema,
360
+ getObject,
361
+ listObjects,
362
+ removeOption,
363
+ reorderFields,
364
+ updateField,
365
+ updateObject,
366
+ updateOption
367
+ };