@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,127 @@
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 { i as instance } from "./index-BM8qeGOb.mjs";
8
+ const subscribers = /* @__PURE__ */ new Set();
9
+ function newToastId() {
10
+ if (typeof crypto !== "undefined" && crypto.randomUUID) return crypto.randomUUID();
11
+ return `${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
12
+ }
13
+ function showToast(input) {
14
+ const toast2 = { ...input, id: newToastId(), createdAt: Date.now() };
15
+ subscribers.forEach((fn) => {
16
+ try {
17
+ fn(toast2);
18
+ } catch (err) {
19
+ console.error("Toast subscriber threw:", err);
20
+ }
21
+ });
22
+ return toast2;
23
+ }
24
+ const toast = {
25
+ info: (message, durationMs) => showToast({ level: "info", message, durationMs }),
26
+ success: (message, durationMs) => showToast({ level: "success", message, durationMs }),
27
+ warn: (message, durationMs) => showToast({ level: "warn", message, durationMs }),
28
+ error: (message, durationMs) => showToast({ level: "error", message, durationMs })
29
+ };
30
+ function subscribeToToasts(fn) {
31
+ subscribers.add(fn);
32
+ return () => subscribers.delete(fn);
33
+ }
34
+ class StorageError extends Error {
35
+ constructor(message, status, body) {
36
+ super(message);
37
+ this.status = status;
38
+ this.body = body;
39
+ this.name = "StorageError";
40
+ }
41
+ }
42
+ function actionLabel(action) {
43
+ return instance.t(`storage.actions.${action}`);
44
+ }
45
+ function extractDetail(body) {
46
+ if (!body) return "";
47
+ let text = body.trim();
48
+ try {
49
+ const parsed = JSON.parse(text);
50
+ text = parsed.error ?? parsed.message ?? text;
51
+ } catch {
52
+ }
53
+ if (text.length > 240) text = text.slice(0, 240) + "…";
54
+ return text;
55
+ }
56
+ function buildErrorMessage(action, status, body) {
57
+ const label = actionLabel(action);
58
+ const detail = extractDetail(body);
59
+ switch (status) {
60
+ case 401:
61
+ case 403:
62
+ return instance.t("storage.errors.auth", { action: label });
63
+ case 404:
64
+ return instance.t("storage.errors.notFound", { action: label });
65
+ case 413:
66
+ return instance.t("storage.errors.tooLarge", { action: label });
67
+ case 429:
68
+ return instance.t("storage.errors.rateLimited", { action: label });
69
+ case 500:
70
+ case 502:
71
+ case 503:
72
+ case 504:
73
+ return instance.t("storage.errors.serverError", { action: label, status });
74
+ default:
75
+ return instance.t("storage.errors.generic", {
76
+ action: label,
77
+ status,
78
+ detail: detail ? `: ${detail}` : ""
79
+ });
80
+ }
81
+ }
82
+ async function performRequest(action, fetcher) {
83
+ let res;
84
+ try {
85
+ res = await fetcher();
86
+ } catch (err) {
87
+ if (err?.name === "AbortError") throw err;
88
+ toast.error(instance.t("storage.errors.network", { action: actionLabel(action) }));
89
+ throw err;
90
+ }
91
+ if (res.ok) return res;
92
+ const body = await res.text().catch(() => "");
93
+ const isSilenced = (action === "delete" || action === "deleteFolder") && res.status === 404;
94
+ const message = buildErrorMessage(action, res.status, body);
95
+ if (!isSilenced) toast.error(message);
96
+ throw new StorageError(message, res.status, body);
97
+ }
98
+ async function gitFetch(action, url, init, allow = []) {
99
+ let res;
100
+ try {
101
+ res = await fetch(url, init);
102
+ } catch (err) {
103
+ return performRequest(action, () => Promise.reject(err));
104
+ }
105
+ if (res.ok || allow.includes(res.status)) return res;
106
+ return performRequest(action, async () => res);
107
+ }
108
+ async function mapLimit(items, limit, worker) {
109
+ const results = new Array(items.length);
110
+ let next = 0;
111
+ async function lane() {
112
+ while (next < items.length) {
113
+ const i = next++;
114
+ results[i] = await worker(items[i]);
115
+ }
116
+ }
117
+ await Promise.all(Array.from({ length: Math.min(limit, items.length) }, lane));
118
+ return results;
119
+ }
120
+ export {
121
+ StorageError as S,
122
+ gitFetch as g,
123
+ mapLimit as m,
124
+ performRequest as p,
125
+ subscribeToToasts as s,
126
+ toast as t
127
+ };
@@ -0,0 +1,59 @@
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 ALPHABET = "0123456789abcdefghijklmnopqrstuvwxyz";
8
+ function randomChars(n) {
9
+ const bytes = new Uint8Array(n);
10
+ crypto.getRandomValues(bytes);
11
+ let out = "";
12
+ for (const b of bytes) out += ALPHABET[b % 36];
13
+ return out;
14
+ }
15
+ let lastTime = 0;
16
+ let lastRandom = "";
17
+ function newId(now = Date.now()) {
18
+ const time = Math.max(0, Math.floor(now)).toString(36).padStart(9, "0").slice(-9);
19
+ if (now === lastTime && lastRandom) {
20
+ lastRandom = increment(lastRandom);
21
+ } else {
22
+ lastTime = now;
23
+ lastRandom = randomChars(7);
24
+ }
25
+ return time + lastRandom;
26
+ }
27
+ function increment(s) {
28
+ const chars = s.split("");
29
+ for (let i = chars.length - 1; i >= 0; i--) {
30
+ const idx = ALPHABET.indexOf(chars[i]);
31
+ if (idx < 35) {
32
+ chars[i] = ALPHABET[idx + 1];
33
+ return chars.join("");
34
+ }
35
+ chars[i] = "0";
36
+ }
37
+ return chars.join("") + "0";
38
+ }
39
+ function seededId(seed) {
40
+ let h1 = 2166136261;
41
+ let h2 = 16777619;
42
+ for (let i = 0; i < seed.length; i++) {
43
+ const c = seed.charCodeAt(i);
44
+ h1 = Math.imul(h1 ^ c, 16777619);
45
+ h2 = Math.imul(h2 ^ c, 1540483477);
46
+ }
47
+ return ((h1 >>> 0).toString(36) + (h2 >>> 0).toString(36)).padStart(14, "0");
48
+ }
49
+ function slugId(label, taken = []) {
50
+ const base = label.normalize("NFKD").replace(/[̀-ͯ]/g, "").toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "") || "item";
51
+ const used = new Set(taken);
52
+ if (!used.has(base)) return base;
53
+ for (let i = 2; ; i++) if (!used.has(`${base}-${i}`)) return `${base}-${i}`;
54
+ }
55
+ export {
56
+ seededId as a,
57
+ newId as n,
58
+ slugId as s
59
+ };
@@ -0,0 +1,14 @@
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 FILE_ID_RE = /^[A-Za-z0-9_-]{1,80}$/;
8
+ function assertFileId(id) {
9
+ if (!FILE_ID_RE.test(id)) throw new Error(`Invalid file id "${id}".`);
10
+ }
11
+ export {
12
+ FILE_ID_RE as F,
13
+ assertFileId as a
14
+ };
@@ -0,0 +1,334 @@
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 { P as Papa } from "./papaparse-Df7i1dHr.mjs";
8
+ import { findOption, normalizeEmail, coerceFieldValue, recordLabel, normalizeDomain } from "./fields-B0bitgmr.mjs";
9
+ import { n as newId } from "./ids-CvnmrMJX.mjs";
10
+ import { c as camelId } from "./utils-DmI8FtXP.mjs";
11
+ import { assertCan, OpsError, actorId, emit } from "./context-CxFfqS4D.mjs";
12
+ import { prepareRecordFields, recordPatchMutation } from "./records-DcHFelq5.mjs";
13
+ import { requireObject, thaw, recordsOf, usersOf, asDoc, objectOf } from "./store-5gqRSKXg.mjs";
14
+ import "./_commonjsHelpers-ClciMIcB.mjs";
15
+ import "stream";
16
+ import "./pluginRegistry-DyHtlkcr.mjs";
17
+ import "./query-C5nJB_3C.mjs";
18
+ import "./standardObjects-CheqfDqa.mjs";
19
+ import "./relations-B9TynBmA.mjs";
20
+ function parseCsv(text, opts = {}) {
21
+ const res = Papa.parse(text.replace(/^/, ""), {
22
+ header: true,
23
+ skipEmptyLines: "greedy",
24
+ delimiter: opts.delimiter ?? "",
25
+ transformHeader: (h) => h.trim()
26
+ });
27
+ return {
28
+ headers: (res.meta.fields ?? []).filter((h) => h !== ""),
29
+ rows: res.data,
30
+ errors: res.errors.map((e) => ({ row: (e.row ?? 0) + 2, message: e.message }))
31
+ };
32
+ }
33
+ const MAPPING_OWNER = "$owner";
34
+ const MAPPING_ID = "$id";
35
+ const MAPPING_SKIP = "";
36
+ const ALIASES = {
37
+ firstName: ["first name", "firstname", "first", "given name", "prénom", "prenom"],
38
+ lastName: ["last name", "lastname", "last", "surname", "family name", "nom de famille", "nom"],
39
+ name: ["name", "full name", "fullname", "company name", "deal name", "nom complet", "raison sociale"],
40
+ email: ["email", "e-mail", "email address", "mail", "courriel", "adresse e-mail"],
41
+ phone: ["phone", "phone number", "telephone", "tel", "mobile", "téléphone"],
42
+ jobTitle: ["job title", "title", "position", "role", "poste", "fonction"],
43
+ company: ["company", "organization", "organisation", "account", "société", "entreprise"],
44
+ domain: ["domain", "website", "web site", "url", "site", "site web"],
45
+ city: ["city", "town", "ville"],
46
+ linkedin: ["linkedin", "linkedin url", "linkedin profile"],
47
+ amount: ["amount", "value", "deal value", "montant"],
48
+ stage: ["stage", "deal stage", "étape", "statut"],
49
+ closeDate: ["close date", "closing date", "expected close", "date de clôture"],
50
+ industry: ["industry", "sector", "secteur"],
51
+ employees: ["employees", "headcount", "company size", "employés", "effectif"],
52
+ annualRevenue: ["annual revenue", "revenue", "chiffre d'affaires", "ca"],
53
+ leadSource: ["lead source", "source", "origine"],
54
+ lifecycleStage: ["lifecycle stage", "lifecycle", "status", "cycle de vie"],
55
+ tags: ["tags", "labels", "étiquettes"]
56
+ };
57
+ function norm(s) {
58
+ return s.normalize("NFKD").replace(/[̀-ͯ]/g, "").toLowerCase().replace(/[^a-z0-9]+/g, " ").trim();
59
+ }
60
+ function suggestMapping(object, headers) {
61
+ const out = {};
62
+ const used = /* @__PURE__ */ new Set();
63
+ for (const h of headers) {
64
+ const n = norm(h);
65
+ let match = object.fields.find((f) => norm(f.id) === n || norm(f.label) === n || camelId(h) === f.id) ?? object.fields.find((f) => (ALIASES[f.id] ?? []).some((a) => norm(a) === n));
66
+ if (!match && (n === "owner" || n === "assigned to" || n === "proprietaire")) {
67
+ out[h] = MAPPING_OWNER;
68
+ continue;
69
+ }
70
+ if (!match && n === "id") {
71
+ out[h] = MAPPING_ID;
72
+ continue;
73
+ }
74
+ if (match && used.has(match.id)) match = void 0;
75
+ out[h] = match?.id ?? MAPPING_SKIP;
76
+ if (match) used.add(match.id);
77
+ }
78
+ return out;
79
+ }
80
+ function dedupeKey(object, fields) {
81
+ if (object.id === "people" || object.fields.some((f) => f.id === "email" && f.unique)) {
82
+ const e = normalizeEmail(fields.email);
83
+ if (e) return `email:${e}`;
84
+ }
85
+ if (object.id === "companies" || object.fields.some((f) => f.id === "domain" && f.unique)) {
86
+ const d = normalizeDomain(fields.domain);
87
+ if (d) return `domain:${d}`;
88
+ }
89
+ const label = fields[object.labelField];
90
+ if (object.id !== "people" && typeof label === "string" && label.trim()) return `label:${label.trim().toLowerCase()}`;
91
+ return null;
92
+ }
93
+ async function importCsv(ctx, opts) {
94
+ assertCan(ctx, "import");
95
+ const baseObject = requireObject(ctx.store, opts.object);
96
+ const parsed = parseCsv(opts.csv, { delimiter: opts.delimiter });
97
+ const mapping = opts.mapping ?? suggestMapping(baseObject, parsed.headers);
98
+ for (const target of Object.values(mapping)) {
99
+ if (target && target !== MAPPING_OWNER && target !== MAPPING_ID && !baseObject.fields.some((f) => f.id === target)) {
100
+ throw new OpsError("validation", `Mapping targets unknown field "${target}" on ${baseObject.labelPlural}.`);
101
+ }
102
+ }
103
+ const onDuplicate = opts.onDuplicate ?? "update";
104
+ const createOptions = opts.createOptions !== false;
105
+ const createRelated = opts.createRelated !== false;
106
+ const report = {
107
+ object: baseObject.id,
108
+ dryRun: !!opts.dryRun,
109
+ total: parsed.rows.length,
110
+ created: 0,
111
+ updated: 0,
112
+ skipped: 0,
113
+ failed: 0,
114
+ mapping,
115
+ unmappedColumns: parsed.headers.filter((h) => !mapping[h]),
116
+ newOptions: [],
117
+ createdRelated: [],
118
+ rows: []
119
+ };
120
+ const object = thaw(baseObject);
121
+ if (createOptions) {
122
+ for (const [header, fieldId] of Object.entries(mapping)) {
123
+ const field = object.fields.find((f) => f.id === fieldId);
124
+ if (!field || field.type !== "select" && field.type !== "multiSelect" || field.system) continue;
125
+ const fresh = [];
126
+ for (const row of parsed.rows) {
127
+ const raw = row[header];
128
+ if (!raw?.trim()) continue;
129
+ const parts = field.type === "multiSelect" ? raw.split(/[,;|]/) : [raw];
130
+ for (const p of parts.map((x) => x.trim()).filter(Boolean)) {
131
+ if (!findOption(field, p) && !fresh.some((x) => x.toLowerCase() === p.toLowerCase())) fresh.push(p);
132
+ }
133
+ }
134
+ if (fresh.length) {
135
+ const start = field.options?.length ?? 0;
136
+ field.options = [...field.options ?? [], ...fresh.map((label, i) => ({ id: uniqueOptionId(field, label), label, color: ["gray", "blue", "green", "yellow", "orange", "red", "purple", "pink", "teal", "indigo"][(start + i) % 10] }))];
137
+ report.newOptions.push({ field: field.id, labels: fresh });
138
+ }
139
+ }
140
+ }
141
+ const existing = recordsOf(ctx.store, object.id);
142
+ const byKey = /* @__PURE__ */ new Map();
143
+ for (const r of existing) {
144
+ const k = dedupeKey(object, r.fields);
145
+ if (k && !byKey.has(k)) byKey.set(k, r);
146
+ }
147
+ const lookups = /* @__PURE__ */ new Map();
148
+ const lookupFor = (target) => {
149
+ let m = lookups.get(target);
150
+ if (!m) {
151
+ m = /* @__PURE__ */ new Map();
152
+ const o = objectOf(ctx.store, target);
153
+ for (const r of recordsOf(ctx.store, target)) {
154
+ m.set(r.id, r.id);
155
+ m.set(recordLabel(o, r).toLowerCase(), r.id);
156
+ const d = normalizeDomain(r.fields.domain);
157
+ if (d) m.set(d, r.id);
158
+ const e = normalizeEmail(r.fields.email);
159
+ if (e) m.set(e, r.id);
160
+ }
161
+ lookups.set(target, m);
162
+ }
163
+ return m;
164
+ };
165
+ const usersByKey = /* @__PURE__ */ new Map();
166
+ for (const u of usersOf(ctx.store)) {
167
+ usersByKey.set(u.id, u.id);
168
+ usersByKey.set(normalizeEmail(u.email), u.id);
169
+ usersByKey.set(u.name.toLowerCase(), u.id);
170
+ }
171
+ const mutations = [];
172
+ const createdDocs = [];
173
+ const updatedDocs = [];
174
+ const pendingRelated = /* @__PURE__ */ new Map();
175
+ const seenInFile = /* @__PURE__ */ new Map();
176
+ const now = ctx.now();
177
+ const vctx = {
178
+ ...ctx,
179
+ store: {
180
+ ...ctx.store,
181
+ kind: ctx.store.kind,
182
+ list: (c) => ctx.store.list(c),
183
+ get: (c, id) => {
184
+ if (c === "objects" && id === object.id) return asDoc(object);
185
+ if (c === "records" && pendingRelated.has(id)) return asDoc(pendingRelated.get(id));
186
+ return ctx.store.get(c, id);
187
+ }
188
+ }
189
+ };
190
+ parsed.rows.forEach((row, index) => {
191
+ const rowNo = index + 2;
192
+ const messages = [];
193
+ const input = {};
194
+ let ownerId;
195
+ let explicitId;
196
+ for (const [header, target] of Object.entries(mapping)) {
197
+ const raw = row[header];
198
+ if (!target || raw === void 0) continue;
199
+ if (target === MAPPING_OWNER) {
200
+ if (raw.trim()) {
201
+ ownerId = usersByKey.get(raw.trim().toLowerCase()) ?? usersByKey.get(normalizeEmail(raw));
202
+ if (!ownerId) messages.push(`Unknown owner "${raw}" (ignored).`);
203
+ }
204
+ continue;
205
+ }
206
+ if (target === MAPPING_ID) {
207
+ explicitId = raw.trim() || void 0;
208
+ continue;
209
+ }
210
+ const field = object.fields.find((f) => f.id === target);
211
+ if (field.type === "relation") {
212
+ const resolved = resolveRelation(field, raw);
213
+ if (resolved !== void 0) input[target] = resolved;
214
+ continue;
215
+ }
216
+ if (field.type === "user") {
217
+ if (raw.trim()) {
218
+ const uid = usersByKey.get(raw.trim().toLowerCase()) ?? usersByKey.get(normalizeEmail(raw));
219
+ if (uid) input[target] = uid;
220
+ else messages.push(`${field.label}: unknown user "${raw}" (ignored).`);
221
+ }
222
+ continue;
223
+ }
224
+ const res = coerceFieldValue(field, raw, { dayFirst: opts.dayFirst, unknownOptions: "fail" });
225
+ if (!res.ok) messages.push(res.message);
226
+ else if (res.value !== void 0) input[target] = res.value;
227
+ }
228
+ if (messages.some((m) => !m.endsWith("(ignored)."))) {
229
+ report.failed++;
230
+ report.rows.push({ row: rowNo, action: "error", messages });
231
+ return;
232
+ }
233
+ const key = dedupeKey(object, input);
234
+ const dupInFile = key ? seenInFile.get(key) : void 0;
235
+ const existingRecord = explicitId ? existing.find((r) => r.id === explicitId) : key ? byKey.get(key) : void 0;
236
+ if (dupInFile && onDuplicate !== "create") {
237
+ report.skipped++;
238
+ report.rows.push({ row: rowNo, action: "skip", id: dupInFile, messages: [...messages, `Duplicate of row with the same ${key.split(":")[0]} earlier in the file.`] });
239
+ return;
240
+ }
241
+ try {
242
+ if (existingRecord && onDuplicate !== "create") {
243
+ if (onDuplicate === "skip") {
244
+ report.skipped++;
245
+ report.rows.push({ row: rowNo, action: "skip", id: existingRecord.id, label: recordLabel(object, existingRecord), messages: [...messages, "Already exists."] });
246
+ return;
247
+ }
248
+ const prepared2 = prepareRecordFields(vctx, object, input, existingRecord, { unknownFields: "drop" });
249
+ if (prepared2.changed.length || ownerId && ownerId !== existingRecord.ownerId) {
250
+ mutations.push(recordPatchMutation(ctx, existingRecord, prepared2.fields, prepared2.changed, ownerId ? { ownerId } : {}));
251
+ updatedDocs.push({ before: existingRecord, changed: prepared2.changed });
252
+ }
253
+ report.updated++;
254
+ if (key) seenInFile.set(key, existingRecord.id);
255
+ report.rows.push({ row: rowNo, action: "update", id: existingRecord.id, label: recordLabel(object, existingRecord), ...messages.length ? { messages } : {} });
256
+ return;
257
+ }
258
+ const prepared = prepareRecordFields(vctx, object, input, null, { unknownFields: "drop" });
259
+ const id = explicitId && !existing.some((r) => r.id === explicitId) ? explicitId : newId(now);
260
+ const record = {
261
+ id,
262
+ object: object.id,
263
+ fields: prepared.fields,
264
+ ...ownerId ?? actorId(ctx) ? { ownerId: ownerId ?? actorId(ctx) } : {},
265
+ createdAt: now,
266
+ updatedAt: now,
267
+ ...actorId(ctx) ? { createdBy: actorId(ctx), updatedBy: actorId(ctx) } : {}
268
+ };
269
+ mutations.push({ op: "set", collection: "records", doc: asDoc(record) });
270
+ createdDocs.push(record);
271
+ if (key) seenInFile.set(key, id);
272
+ report.created++;
273
+ report.rows.push({ row: rowNo, action: "create", id, label: recordLabel(object, record), ...messages.length ? { messages } : {} });
274
+ } catch (err) {
275
+ report.failed++;
276
+ const details = err.details;
277
+ report.rows.push({ row: rowNo, action: "error", messages: [...messages, ...details?.map((d) => d.message) ?? [err.message]] });
278
+ }
279
+ });
280
+ function resolveRelation(field, raw) {
281
+ const target = field.relation.object;
282
+ const parts = field.relation.cardinality === "many" ? raw.split(/[,;|]/) : [raw];
283
+ const ids = [];
284
+ for (const part of parts.map((p) => p.trim()).filter(Boolean)) {
285
+ const m = lookupFor(target);
286
+ const hit = m.get(part) ?? m.get(part.toLowerCase()) ?? m.get(normalizeDomain(part)) ?? m.get(normalizeEmail(part));
287
+ if (hit) {
288
+ ids.push(hit);
289
+ continue;
290
+ }
291
+ if (createRelated && target === "companies") {
292
+ const targetObject = objectOf(ctx.store, target);
293
+ const isDomain = /^[a-z0-9-]+(\.[a-z0-9-]+)+$/i.test(part) && !part.includes(" ");
294
+ const id = newId(now);
295
+ const fields = isDomain ? { name: part.split(".")[0].replace(/^./, (c) => c.toUpperCase()), domain: `https://${normalizeDomain(part)}` } : { name: part };
296
+ const rec = { id, object: target, fields, ...actorId(ctx) ? { ownerId: actorId(ctx), createdBy: actorId(ctx) } : {}, createdAt: now, updatedAt: now };
297
+ pendingRelated.set(id, rec);
298
+ m.set(part.toLowerCase(), id);
299
+ if (isDomain) m.set(normalizeDomain(part), id);
300
+ mutations.push({ op: "set", collection: "records", doc: asDoc(rec) });
301
+ createdDocs.push(rec);
302
+ report.createdRelated.push({ object: target, label: recordLabel(targetObject, rec) });
303
+ ids.push(id);
304
+ }
305
+ }
306
+ if (!ids.length) return void 0;
307
+ return field.relation.cardinality === "many" ? ids : ids[0];
308
+ }
309
+ if (opts.dryRun) return report;
310
+ if (report.newOptions.length) mutations.unshift({ op: "set", collection: "objects", doc: asDoc({ ...object, updatedAt: now }) });
311
+ if (mutations.length) {
312
+ await ctx.store.write(mutations, { message: `Import ${report.created} new / ${report.updated} updated ${object.labelPlural}` });
313
+ for (const r of createdDocs) await emit(ctx, "record.created", { record: r });
314
+ for (const u of updatedDocs) {
315
+ const after = ctx.store.get("records", u.before.id);
316
+ if (after) await emit(ctx, "record.updated", { record: after, before: u.before, changed: u.changed });
317
+ }
318
+ }
319
+ return report;
320
+ }
321
+ function uniqueOptionId(field, label) {
322
+ const base = label.normalize("NFKD").replace(/[̀-ͯ]/g, "").toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "") || "option";
323
+ const taken = new Set((field.options ?? []).map((o) => o.id));
324
+ if (!taken.has(base)) return base;
325
+ for (let i = 2; ; i++) if (!taken.has(`${base}-${i}`)) return `${base}-${i}`;
326
+ }
327
+ export {
328
+ MAPPING_ID,
329
+ MAPPING_OWNER,
330
+ MAPPING_SKIP,
331
+ importCsv,
332
+ parseCsv,
333
+ suggestMapping
334
+ };