@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,123 @@
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 { isEmptyFieldValue, normalizeEmail, normalizeDomain, recordLabel } from "./fields-B0bitgmr.mjs";
8
+ import { assertCan, OpsError, actorId, emit } from "./context-CxFfqS4D.mjs";
9
+ import { c as rewriteReferences, d as fileMergeMutations } from "./relations-B9TynBmA.mjs";
10
+ import { requireRecord, requireObject, activitiesOf, asDoc, recordsOf } from "./store-5gqRSKXg.mjs";
11
+ import "./pluginRegistry-DyHtlkcr.mjs";
12
+ function normLabel(s) {
13
+ return typeof s === "string" ? s.normalize("NFKD").replace(/[̀-ͯ]/g, "").toLowerCase().replace(/\b(inc|llc|ltd|sas|sarl|gmbh|corp|corporation|co)\b\.?/g, "").replace(/[^a-z0-9]+/g, " ").trim() : "";
14
+ }
15
+ function findDuplicates(ctx, objectId, opts = {}) {
16
+ assertCan(ctx, "records.read");
17
+ const object = requireObject(ctx.store, objectId);
18
+ const has = (id) => object.fields.some((f) => f.id === id);
19
+ const by = opts.by ?? [...has("email") ? ["email"] : [], ...has("domain") ? ["domain"] : [], "name"];
20
+ const records = recordsOf(ctx.store, objectId);
21
+ const groups = [];
22
+ const grouped = /* @__PURE__ */ new Set();
23
+ for (const reason of by) {
24
+ const buckets = /* @__PURE__ */ new Map();
25
+ for (const r of records) {
26
+ const key = reason === "email" ? normalizeEmail(r.fields.email) : reason === "domain" ? normalizeDomain(r.fields.domain) : reason === "phone" ? String(r.fields.phone ?? "").replace(/[^\d]/g, "").slice(-9) : normLabel(recordLabel(object, r));
27
+ if (!key || reason === "phone" && key.length < 6) continue;
28
+ const list = buckets.get(key) ?? [];
29
+ list.push(r);
30
+ buckets.set(key, list);
31
+ }
32
+ for (const [key, list] of buckets) {
33
+ if (list.length < 2) continue;
34
+ const sig = list.map((r) => r.id).sort().join(",");
35
+ if (grouped.has(sig)) continue;
36
+ grouped.add(sig);
37
+ groups.push({
38
+ reason,
39
+ key,
40
+ records: list.sort((a, b) => b.updatedAt - a.updatedAt).map((r) => ({ id: r.id, label: recordLabel(object, r), updatedAt: r.updatedAt }))
41
+ });
42
+ }
43
+ }
44
+ return groups;
45
+ }
46
+ function mergedFields(object, primary, others, pick) {
47
+ const all = [primary, ...[...others].sort((a, b) => b.updatedAt - a.updatedAt)];
48
+ const out = { ...primary.fields };
49
+ for (const field of object.fields) {
50
+ const chosen = pick[field.id];
51
+ if (chosen) {
52
+ const src = all.find((r) => r.id === chosen);
53
+ if (src) {
54
+ if (isEmptyFieldValue(field, src.fields[field.id])) delete out[field.id];
55
+ else out[field.id] = src.fields[field.id];
56
+ }
57
+ continue;
58
+ }
59
+ const isArray = field.type === "tags" || field.type === "multiSelect" || field.type === "files" || field.type === "relation" && field.relation?.cardinality === "many";
60
+ if (isArray) {
61
+ const union = [...new Set(all.flatMap((r) => Array.isArray(r.fields[field.id]) ? r.fields[field.id] : []))];
62
+ if (union.length) out[field.id] = union;
63
+ continue;
64
+ }
65
+ if (isEmptyFieldValue(field, out[field.id])) {
66
+ const donor = all.find((r) => !isEmptyFieldValue(field, r.fields[field.id]));
67
+ if (donor) out[field.id] = donor.fields[field.id];
68
+ }
69
+ }
70
+ return out;
71
+ }
72
+ async function mergeRecords(ctx, primaryId, duplicateIds, opts = {}) {
73
+ assertCan(ctx, "records.write");
74
+ assertCan(ctx, "records.delete");
75
+ const primary = requireRecord(ctx.store, primaryId);
76
+ const ids = [...new Set(duplicateIds)].filter((id) => id !== primaryId);
77
+ if (!ids.length) throw new OpsError("validation", "Nothing to merge: give at least one other record.");
78
+ const others = ids.map((id) => requireRecord(ctx.store, id));
79
+ for (const o of others) if (o.object !== primary.object) throw new OpsError("validation", `Can't merge a ${o.object} record into a ${primary.object} record.`);
80
+ const object = requireObject(ctx.store, primary.object);
81
+ const fields = mergedFields(object, primary, others, opts.pick ?? {});
82
+ for (const f of object.fields) {
83
+ if (f.type !== "relation" || f.relation?.object !== object.id) continue;
84
+ const v = fields[f.id];
85
+ if (Array.isArray(v)) {
86
+ const rest = v.filter((x) => x !== primaryId && !ids.includes(x));
87
+ if (rest.length) fields[f.id] = rest;
88
+ else delete fields[f.id];
89
+ } else if (v === primaryId || ids.includes(v)) delete fields[f.id];
90
+ }
91
+ const now = ctx.now();
92
+ const record = {
93
+ ...primary,
94
+ fields,
95
+ ownerId: primary.ownerId ?? others.find((o) => o.ownerId)?.ownerId,
96
+ createdAt: Math.min(primary.createdAt, ...others.map((o) => o.createdAt)),
97
+ updatedAt: now,
98
+ ...actorId(ctx) ? { updatedBy: actorId(ctx) } : {}
99
+ };
100
+ if (!record.ownerId) delete record.ownerId;
101
+ const refs = rewriteReferences(ctx, ids, primaryId);
102
+ const mutations = [{ op: "set", collection: "records", doc: asDoc(record) }, ...refs];
103
+ let movedActivities = 0;
104
+ const doomed = new Set(ids);
105
+ for (const a of activitiesOf(ctx.store, { includeDeleted: true })) {
106
+ if (!a.targets.some((t) => doomed.has(t.id))) continue;
107
+ movedActivities++;
108
+ const targets = a.targets.map((t) => doomed.has(t.id) ? { object: primary.object, id: primaryId } : t);
109
+ const unique = targets.filter((t, i) => targets.findIndex((x) => x.id === t.id) === i);
110
+ mutations.push({ op: "patch", collection: "activities", id: a.id, fields: { targets: unique, updatedAt: now } });
111
+ }
112
+ for (const id of ids) mutations.push({ op: "delete", collection: "records", id });
113
+ mutations.push(...fileMergeMutations(ctx, ids, { object: primary.object, id: primaryId }));
114
+ await ctx.store.write(mutations, { message: `Merge ${ids.length + 1} ${object.labelPlural}` });
115
+ const stored = requireRecord(ctx.store, primaryId);
116
+ await emit(ctx, "record.updated", { record: stored, before: primary, changed: Object.keys(fields).filter((k) => JSON.stringify(fields[k]) !== JSON.stringify(primary.fields[k])) });
117
+ for (const o of others) await emit(ctx, "record.deleted", { record: o, hard: true });
118
+ return { record: stored, merged: ids, movedActivities, rewiredReferences: refs.length };
119
+ }
120
+ export {
121
+ findDuplicates,
122
+ mergeRecords
123
+ };
@@ -0,0 +1,36 @@
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
+ function bytesToBase64(bytes) {
8
+ let binary = "";
9
+ const chunk = 32768;
10
+ for (let i = 0; i < bytes.length; i += chunk) {
11
+ binary += String.fromCharCode(...bytes.subarray(i, i + chunk));
12
+ }
13
+ return btoa(binary);
14
+ }
15
+ function base64ToBytes(base64) {
16
+ const binary = atob(base64.replace(/\s+/g, ""));
17
+ const bytes = new Uint8Array(binary.length);
18
+ for (let i = 0; i < binary.length; i++) bytes[i] = binary.charCodeAt(i);
19
+ return bytes;
20
+ }
21
+ function utf8ToBase64(text) {
22
+ return bytesToBase64(new TextEncoder().encode(text));
23
+ }
24
+ function base64ToUtf8(base64) {
25
+ return new TextDecoder().decode(base64ToBytes(base64));
26
+ }
27
+ function changeToBytes(change) {
28
+ return change.encoding === "base64" ? base64ToBytes(change.content) : new TextEncoder().encode(change.content);
29
+ }
30
+ export {
31
+ base64ToUtf8 as a,
32
+ bytesToBase64 as b,
33
+ changeToBytes as c,
34
+ base64ToBytes as d,
35
+ utf8ToBase64 as u
36
+ };
@@ -0,0 +1,130 @@
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 { formatAddress, formatFieldValue } from "./fields-B0bitgmr.mjs";
9
+ import { a as applySorts, b as applyFilters, r as resolveField, g as getRecordValue } from "./query-C5nJB_3C.mjs";
10
+ import { COLLECTIONS } from "./types-BOy9PUl7.mjs";
11
+ import { assertCan, OpsError } from "./context-CxFfqS4D.mjs";
12
+ import { queryContext } from "./records-DcHFelq5.mjs";
13
+ import { requireObject, recordsOf, userOf } from "./store-5gqRSKXg.mjs";
14
+ import "./_commonjsHelpers-ClciMIcB.mjs";
15
+ import "stream";
16
+ import "./pluginRegistry-DyHtlkcr.mjs";
17
+ import "./ids-CvnmrMJX.mjs";
18
+ import "./standardObjects-CheqfDqa.mjs";
19
+ import "./relations-B9TynBmA.mjs";
20
+ function exportCsv(ctx, objectId, opts = {}) {
21
+ assertCan(ctx, "export");
22
+ const object = requireObject(ctx.store, objectId);
23
+ let filters = opts.filters ?? [];
24
+ let sorts = opts.sorts ?? [];
25
+ if (opts.viewId) {
26
+ const view = ctx.store.get("views", opts.viewId);
27
+ if (!view) throw new OpsError("notFound", `View "${opts.viewId}" not found.`);
28
+ filters = [...view.filters, ...filters];
29
+ if (!sorts.length) sorts = view.sorts;
30
+ }
31
+ const qctx = queryContext(ctx, object);
32
+ const records = applySorts(applyFilters(recordsOf(ctx.store, objectId, { includeDeleted: opts.includeDeleted }), filters, qctx), sorts, qctx);
33
+ const fieldIds = opts.fields ?? object.fields.map((f) => f.id);
34
+ const columns = [...opts.includeId !== false ? ["id"] : [], ...fieldIds.filter((f) => f !== "id")];
35
+ const raw = opts.values === "raw";
36
+ const header = columns.map((c) => c === "id" ? "ID" : resolveField(object, c)?.label ?? c);
37
+ const rows = records.map(
38
+ (r) => columns.map((c) => {
39
+ const field = resolveField(object, c);
40
+ const value = getRecordValue(r, c);
41
+ if (value === void 0 || value === null) return "";
42
+ if (raw) {
43
+ if (field?.type === "address") return formatAddress(value);
44
+ return Array.isArray(value) ? value.join(", ") : typeof value === "object" ? JSON.stringify(value) : String(value);
45
+ }
46
+ if (!field) return String(value);
47
+ if (field.type === "datetime" && typeof value === "number") return new Date(value).toISOString();
48
+ if (field.type === "date" || field.type === "number" || field.type === "currency" || field.type === "percent") return String(value);
49
+ if (c === "ownerId" || c === "createdBy" || c === "updatedBy") return userOf(ctx.store, String(value))?.email ?? String(value);
50
+ return formatFieldValue(field, value, qctx);
51
+ })
52
+ );
53
+ return Papa.unparse({ fields: header, data: rows }, { newline: "\n" });
54
+ }
55
+ function exportJson(ctx, opts = {}) {
56
+ assertCan(ctx, "export");
57
+ const wanted = opts.collections ?? COLLECTIONS.filter((c) => c !== "automationRuns");
58
+ return {
59
+ format: "agentcrm-export",
60
+ version: 1,
61
+ exportedAt: new Date(ctx.now()).toISOString(),
62
+ collections: Object.fromEntries(wanted.map((c) => [c, ctx.store.list(c)]))
63
+ };
64
+ }
65
+ function planImportJson(ctx, data, opts = {}) {
66
+ assertCan(ctx, "settings.write");
67
+ if (data?.format !== "agentcrm-export") throw new OpsError("validation", "Not an AgentCRM JSON export.");
68
+ const mode = opts.mode ?? "merge";
69
+ const plan = { mode, collections: {}, added: 0, overwritten: 0, deleted: 0, warnings: [] };
70
+ for (const c of COLLECTIONS) {
71
+ const docs = data.collections[c];
72
+ if (!docs) continue;
73
+ const ids = new Set(docs.filter((d) => d && typeof d.id === "string").map((d) => d.id));
74
+ let added = 0;
75
+ let overwritten = 0;
76
+ for (const id of ids) ctx.store.get(c, id) ? overwritten++ : added++;
77
+ const deleted = mode === "replace" ? ctx.store.list(c).filter((d) => !ids.has(d.id)).length : 0;
78
+ plan.collections[c] = { added, overwritten, deleted };
79
+ plan.added += added;
80
+ plan.overwritten += overwritten;
81
+ plan.deleted += deleted;
82
+ }
83
+ const here = ctx.store.get("settings", "general");
84
+ const there = (data.collections.settings ?? []).find((d) => d.id === "general");
85
+ if (here?.name && there?.name && here.name !== there.name) {
86
+ plan.sourceName = there.name;
87
+ plan.warnings.push(`This backup comes from another CRM ("${there.name}"): its settings, users and schema replace the ones with the same ids here (this CRM becomes "${there.name}").`);
88
+ }
89
+ const users = data.collections.users ?? [];
90
+ const replacedUsers = users.filter((u) => {
91
+ const cur = ctx.store.get("users", u.id);
92
+ return cur?.email && u.email && cur.email.toLowerCase() !== u.email.toLowerCase();
93
+ });
94
+ if (replacedUsers.length) {
95
+ plan.warnings.push(`${replacedUsers.length} user(s) of this CRM are replaced by different people with the same id: ${replacedUsers.map((u) => `${String(ctx.store.get("users", u.id).email)} → ${String(u.email)}`).join(", ")}.`);
96
+ }
97
+ return plan;
98
+ }
99
+ async function importJson(ctx, data, opts = {}) {
100
+ assertCan(ctx, "settings.write");
101
+ if (data?.format !== "agentcrm-export") throw new OpsError("validation", "Not an AgentCRM JSON export.");
102
+ const mutations = [];
103
+ let deleted = 0;
104
+ for (const c of COLLECTIONS) {
105
+ const docs = data.collections[c];
106
+ if (!docs) continue;
107
+ const ids = /* @__PURE__ */ new Set();
108
+ for (const doc of docs) {
109
+ if (!doc || typeof doc.id !== "string") continue;
110
+ ids.add(doc.id);
111
+ mutations.push({ op: "set", collection: c, doc });
112
+ }
113
+ if (opts.mode === "replace") {
114
+ for (const existing of ctx.store.list(c)) {
115
+ if (!ids.has(existing.id)) {
116
+ mutations.push({ op: "delete", collection: c, id: existing.id });
117
+ deleted++;
118
+ }
119
+ }
120
+ }
121
+ }
122
+ await ctx.store.write(mutations, { message: "Restore JSON export" });
123
+ return { written: mutations.length - deleted, deleted };
124
+ }
125
+ export {
126
+ exportCsv,
127
+ exportJson,
128
+ importJson,
129
+ planImportJson
130
+ };
@@ -0,0 +1,200 @@
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 { b as EXTENSION_MIN_API_VERSION, E as EXTENSION_API_VERSION, a as EXTENSION_LIMITS, R as RUNTIME_SPECIFIERS } from "./api-Cz0Ubj-B.mjs";
8
+ import { bundleImports, validateManifest, ExtensionPackageError } from "./package-B7mI6HGi.mjs";
9
+ import { n as nodeImportBundle } from "../agentcrm.mjs";
10
+ import "./index-w03ReRs4.mjs";
11
+ import "./_commonjsHelpers-ClciMIcB.mjs";
12
+ import "stream";
13
+ import "events";
14
+ import "buffer";
15
+ import "util";
16
+ import "node:util";
17
+ import "node:fs";
18
+ import "node:path";
19
+ import "node:url";
20
+ import "node:crypto";
21
+ import "node:os";
22
+ import "node:child_process";
23
+ import "node:http";
24
+ const bytes = (s) => s ? new TextEncoder().encode(s).length : 0;
25
+ function countKeys(o) {
26
+ if (!o || typeof o !== "object") return 0;
27
+ return Object.values(o).reduce((n, v) => n + (v && typeof v === "object" ? countKeys(v) : 1), 0);
28
+ }
29
+ function withTimeout(p, ms) {
30
+ return Promise.race([p, new Promise((_, reject) => setTimeout(() => reject(new Error(`timed out after ${ms} ms`)), ms).unref?.())]);
31
+ }
32
+ async function checkExtension(raw, opts = {}) {
33
+ const errors = [...raw.problems];
34
+ const warnings = [];
35
+ const bundle = raw.files["bundle.js"];
36
+ const style = raw.files["style.css"];
37
+ const m = raw.manifest && typeof raw.manifest === "object" ? raw.manifest : {};
38
+ const report = {
39
+ valid: false,
40
+ source: raw.source,
41
+ sourceType: raw.sourceType,
42
+ id: typeof m.id === "string" ? m.id : null,
43
+ kind: m.kind === "module" || m.kind === "theme" ? m.kind : null,
44
+ name: typeof m.name === "string" ? m.name : null,
45
+ version: typeof m.version === "string" ? m.version : null,
46
+ apiVersion: typeof m.apiVersion === "string" ? m.apiVersion : null,
47
+ hostApi: { version: EXTENSION_API_VERSION, min: EXTENSION_MIN_API_VERSION },
48
+ size: { bundle: bytes(bundle), style: bytes(style), ...raw.zipBytes !== void 0 ? { zip: raw.zipBytes } : {} },
49
+ imports: bundle ? bundleImports(bundle) : [],
50
+ errors,
51
+ warnings,
52
+ loaded: false
53
+ };
54
+ let manifest = null;
55
+ if (raw.manifest) {
56
+ try {
57
+ manifest = validateManifest(raw.manifest);
58
+ } catch (err) {
59
+ if (err instanceof ExtensionPackageError) {
60
+ if (err.code === "incompatibleApi") errors.push({ code: "incompatibleApi", message: err.message, hint: `Set "apiVersion" between ${EXTENSION_MIN_API_VERSION} and ${EXTENSION_API_VERSION} (manifest.json and the default export).` });
61
+ else for (const d of err.details?.length ? err.details : [err.message]) errors.push({ code: "manifest", message: d });
62
+ } else throw err;
63
+ }
64
+ }
65
+ if (manifest && opts.kind && manifest.kind !== opts.kind) errors.push({ code: "kind", message: `Expected a ${opts.kind}, the manifest says ${manifest.kind}.` });
66
+ if (manifest && !manifest.description) warnings.push({ code: "description", message: "manifest.json has no description (shown in Settings → Modules and the install plan)." });
67
+ if (manifest && !manifest.author) warnings.push({ code: "author", message: "manifest.json has no author (shown in the install plan, helps admins trust it)." });
68
+ if (bundle !== void 0) {
69
+ if (!bundle.trim()) errors.push({ code: "missingEntry", message: "bundle.js is empty." });
70
+ if (bytes(bundle) > EXTENSION_LIMITS.bundle) errors.push({ code: "tooLarge", message: `bundle.js is ${(bytes(bundle) / 1024 / 1024).toFixed(2)} MB — the limit is ${EXTENSION_LIMITS.bundle / 1024 / 1024} MB.`, hint: "Keep react, react-i18next, lucide-react and @agent-crm/runtime external (vite.config.ts)." });
71
+ const foreign = report.imports.filter((s) => !RUNTIME_SPECIFIERS.includes(s));
72
+ if (foreign.length) errors.push({ code: "imports", message: `bundle.js imports modules the admin doesn't provide: ${foreign.join(", ")}.`, hint: `Bundle them, or import only from ${RUNTIME_SPECIFIERS.join(", ")}.` });
73
+ if (/\bjsxDEV\b|react\/jsx-dev-runtime/.test(bundle)) errors.push({ code: "devJsx", message: "bundle.js uses the development JSX runtime.", hint: 'Build in production mode (esbuild: { jsx: "automatic", jsxDev: false }).' });
74
+ if (!/\bexport\s*(?:default\b|\{[^}]*\bas\s+default\b)/.test(bundle)) errors.push({ code: "noDefaultExport", message: "bundle.js has no default export.", hint: "export default defineModule({...}) / defineTheme({...})." });
75
+ } else if (!errors.some((e) => e.code === "notBuilt")) {
76
+ errors.push({ code: "missingEntry", message: "The package has no bundle.js." });
77
+ }
78
+ if (manifest?.style && style === void 0) errors.push({ code: "missingStyle", message: 'manifest.json declares "style": "style.css" but there is no style.css.', hint: "Add src/style.css (projects) or style.css next to bundle.js." });
79
+ if (style !== void 0 && bytes(style) > EXTENSION_LIMITS.style) errors.push({ code: "tooLarge", message: `style.css is larger than ${EXTENSION_LIMITS.style / 1024} KB.` });
80
+ if (style !== void 0) cssWarnings(style, "style.css", warnings);
81
+ for (const f of raw.ignored) warnings.push({ code: "ignored", message: `${f} is not part of the package (only manifest.json, bundle.js and style.css are).${/style\.css$/.test(f) ? ' Declare "style": "style.css" in manifest.json to ship it.' : ""}` });
82
+ if (!errors.length && manifest && bundle) await loadInSandbox(report, manifest, { "bundle.js": bundle, ...style ? { "style.css": style } : {} }, opts);
83
+ report.valid = errors.length === 0;
84
+ return report;
85
+ }
86
+ function cssWarnings(css, label, warnings) {
87
+ if (/@import\s+(?:url\(\s*)?["']?\s*(?:https?:)?\/\//i.test(css)) warnings.push({ code: "css", message: `${label} @imports a remote stylesheet: every user's browser would fetch it.`, hint: `Inline the CSS; for fonts set the theme's "font" (the CRM loads Google Fonts itself).` });
88
+ if (/<\/?(?:style|script)/i.test(css)) warnings.push({ code: "css", message: `${label} contains <style> / <script> tags: it must be plain CSS.` });
89
+ }
90
+ async function loadInSandbox(report, manifest, files, opts) {
91
+ const importBundle = await nodeImportBundle();
92
+ const [{ createMemoryStore }, { createOpsContext }, { createPluginRegistry, plugins }, { seedCrm }, { usersOf }, loader, registry, aiTools, actions, features, themes, schema] = await Promise.all([
93
+ import("./memory-WNR3VHUJ.mjs"),
94
+ import("./context-CxFfqS4D.mjs"),
95
+ import("./pluginRegistry-DyHtlkcr.mjs"),
96
+ import("./seed-6MRmQQBR.mjs"),
97
+ import("./store-5gqRSKXg.mjs"),
98
+ import("./loader-MPD9iAvh.mjs"),
99
+ import("./registry-COfJrSmg.mjs"),
100
+ import("./tools-iSH-yMpl.mjs"),
101
+ import("./extensionActions-D2HtpQz4.mjs"),
102
+ import("./featureRegistry-DSsZbhrh.mjs"),
103
+ import("./themes-DPZFv3yc.mjs"),
104
+ import("./schema-DnLj8dUO.mjs").then((n) => n.s)
105
+ ]);
106
+ const store = createMemoryStore();
107
+ await store.ready();
108
+ const sandboxPlugins = createPluginRegistry(() => void 0);
109
+ const system = createOpsContext(store, null, { plugins: sandboxPlugins });
110
+ await seedCrm(system, { admin: { name: "Check Admin", email: "check@example.com" }, demo: true, name: "Extension check" });
111
+ const admin = createOpsContext(store, usersOf(store).find((u) => u.role === "admin") ?? null, { plugins: sandboxPlugins });
112
+ const now = Date.now();
113
+ const doc = { id: manifest.id, kind: manifest.kind, manifest, enabled: true, files, installedAt: now, updatedAt: now };
114
+ await store.write([{ op: "set", collection: "extensions", doc }]);
115
+ const { BUILTIN_MODULES } = await import("./api-Cz0Ubj-B.mjs").then((n) => n.m);
116
+ const missingDeps = (manifest.dependsOn ?? []).filter((d) => !BUILTIN_MODULES.some((b) => b.id === d));
117
+ for (const d of missingDeps) report.warnings.push({ code: "dependency", message: `Depends on "${d}", which is not a built-in module: install that extension first on the real CRM.` });
118
+ const toolsBefore = new Set(aiTools.listAiTools().map((t) => t.name));
119
+ const actionsBefore = new Set(actions.listAutomationActions().map((a) => a.id));
120
+ const hooksBefore = plugins.listHooks();
121
+ const sync = await loader.syncExtensions(store, { importBundle, onError: () => void 0 });
122
+ try {
123
+ const failure = sync.failed.find((f) => f.id === manifest.id);
124
+ if (failure || registry.getLoadedExtension(manifest.id)?.status !== "loaded") {
125
+ const reason = failure?.error ?? registry.getLoadedExtension(manifest.id)?.error ?? "not loaded";
126
+ report.errors.push({ code: "load", message: `The bundle failed to load: ${reason}`, hint: "Check the default export (id and apiVersion equal to manifest.json), AI tool names (snake_case, unique) and automation action ids (<moduleId>.<action>)." });
127
+ return;
128
+ }
129
+ report.loaded = true;
130
+ if (manifest.kind === "theme") {
131
+ const theme = themes.getExternalTheme(manifest.id);
132
+ if (!theme) {
133
+ report.errors.push({ code: "theme", message: "The theme did not register." });
134
+ return;
135
+ }
136
+ const { errors } = schema.validateAppearancePatch(theme.appearance);
137
+ for (const e of errors) report.errors.push({ code: "appearance", message: `Theme appearance: ${e}` });
138
+ for (const k of ["logo", "favicon", "themeId", "preset"]) if (k in (theme.appearance ?? {})) report.warnings.push({ code: "appearance", message: `Theme appearance sets "${k}" — themes should leave it to the workspace.` });
139
+ const ownCss = (theme.css ?? "").replace(files["style.css"] ?? "", "");
140
+ if (ownCss.trim()) cssWarnings(ownCss, "The theme's css", report.warnings);
141
+ report.theme = { name: theme.name, appearance: { ...theme.appearance }, css: bytes(theme.css) };
142
+ return;
143
+ }
144
+ const feature = features.getAllFeatures().find((f) => f.id === manifest.id);
145
+ const slots = {};
146
+ for (const [name, items] of Object.entries(feature?.slots ?? {})) {
147
+ if (!items) continue;
148
+ slots[name] = Array.isArray(items) ? items.map((x, i) => x && typeof x === "object" && "id" in x ? String(x.id) : `#${i}`) : ["(component)"];
149
+ }
150
+ const en = feature?.i18n?.en ?? {};
151
+ const fr = feature?.i18n?.fr;
152
+ const tools = aiTools.listAiTools().filter((t) => !toolsBefore.has(t.name));
153
+ const hooksAfter = plugins.listHooks();
154
+ report.registered = {
155
+ routes: (feature?.routes ?? []).map((r2) => r2.path),
156
+ navItems: (feature?.navItems ?? []).map((n) => ({ id: n.id, to: n.to, label: n.label })),
157
+ slots,
158
+ settingsPage: typeof feature?.settingsPage === "function" || typeof feature?.settingsPage === "object" && feature?.settingsPage !== null,
159
+ i18n: { en: countKeys(en), fr: countKeys(fr) },
160
+ aiTools: tools.map((t) => ({ name: t.name, mutating: t.mutating, description: t.description })),
161
+ automationActions: actions.listAutomationActions().filter((a) => !actionsBefore.has(a.id)).map((a) => ({ id: a.id, label: a.label, ...a.objects ? { objects: a.objects } : {} })),
162
+ hooks: { actions: hooksAfter.actions.filter((h) => !hooksBefore.actions.includes(h)), filters: hooksAfter.filters.filter((h) => !hooksBefore.filters.includes(h)) }
163
+ };
164
+ const r = report.registered;
165
+ if (!r.routes.length && !r.navItems.length && !Object.keys(r.slots).length && !r.settingsPage && !r.aiTools.length && !r.automationActions.length && !r.hooks.actions.length && !r.hooks.filters.length) {
166
+ report.warnings.push({ code: "empty", message: "The module registers nothing (no route, slot, settings page, AI tool, automation action or hook)." });
167
+ }
168
+ if (r.i18n.en && !fr) report.warnings.push({ code: "i18n", message: "No French bundle (feature.i18n.fr): French users see the English strings." });
169
+ for (const n of r.navItems) {
170
+ if (!r.routes.includes(n.to)) report.warnings.push({ code: "nav", message: `Navigation entry "${n.id}" points to ${n.to}, which no route of this module serves.` });
171
+ const key = n.label.startsWith(`${manifest.id}:`) ? n.label.slice(manifest.id.length + 1) : null;
172
+ if (key && !key.split(".").reduce((o, k) => o && typeof o === "object" ? o[k] : void 0, en)) report.warnings.push({ code: "i18n", message: `Navigation label "${n.label}" has no English string.` });
173
+ }
174
+ for (const t of tools) {
175
+ const snake = manifest.id.replace(/-/g, "_");
176
+ if (!t.name.includes(snake) && !t.name.startsWith(snake.split("_")[0])) report.warnings.push({ code: "toolName", message: `AI tool "${t.name}" doesn't start with the module id: prefix it to avoid clashes with other extensions.` });
177
+ if ((t.description ?? "").length < 20) report.warnings.push({ code: "toolDescription", message: `AI tool "${t.name}" needs a clearer description (the model reads it to decide when to call it).` });
178
+ }
179
+ for (const a of r.automationActions) if (!a.id.startsWith(`${manifest.id}.`)) report.warnings.push({ code: "actionId", message: `Automation action "${a.id}" should be namespaced "${manifest.id}.<action>".` });
180
+ if (opts.callTools !== false) {
181
+ report.toolCalls = [];
182
+ for (const t of tools) {
183
+ if (t.inputSchema.required?.length) continue;
184
+ try {
185
+ await withTimeout(Promise.resolve(aiTools.callTool(admin, t.name, {})), 1e4);
186
+ report.toolCalls.push({ name: t.name, ok: true });
187
+ } catch (err) {
188
+ const message = err?.message ?? String(err);
189
+ report.toolCalls.push({ name: t.name, ok: false, error: message });
190
+ report.warnings.push({ code: "toolCall", message: `AI tool "${t.name}" failed when called with {} on demo data: ${message}` });
191
+ }
192
+ }
193
+ }
194
+ } finally {
195
+ registry.deactivateExtension(manifest.id);
196
+ }
197
+ }
198
+ export {
199
+ checkExtension
200
+ };
@@ -0,0 +1,115 @@
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 { existsSync, statSync, readFileSync } from "node:fs";
8
+ import { resolve, join } from "node:path";
9
+ import { a as EXTENSION_LIMITS } from "./api-Cz0Ubj-B.mjs";
10
+ import { validatePackage, ExtensionPackageError } from "./package-B7mI6HGi.mjs";
11
+ import { C as CliError } from "../agentcrm.mjs";
12
+ import "./index-w03ReRs4.mjs";
13
+ import "./_commonjsHelpers-ClciMIcB.mjs";
14
+ import "stream";
15
+ import "events";
16
+ import "buffer";
17
+ import "util";
18
+ import "node:util";
19
+ import "node:url";
20
+ import "node:crypto";
21
+ import "node:os";
22
+ import "node:child_process";
23
+ import "node:http";
24
+ function parseManifest(text, problems) {
25
+ if (text === void 0) {
26
+ problems.push({ code: "missingManifest", message: "No manifest.json found.", hint: "Point at the project folder (manifest.json at its root), an unpacked package or the .zip." });
27
+ return null;
28
+ }
29
+ try {
30
+ return JSON.parse(text.replace(/^/, ""));
31
+ } catch (err) {
32
+ problems.push({ code: "invalidManifest", message: `manifest.json is not valid JSON (${err.message}).` });
33
+ return null;
34
+ }
35
+ }
36
+ async function readZip(source) {
37
+ const problems = [];
38
+ const data = readFileSync(source);
39
+ const { default: JSZip } = await import("./index-w03ReRs4.mjs").then((n) => n.i);
40
+ let zip;
41
+ try {
42
+ zip = await JSZip.loadAsync(data);
43
+ } catch (err) {
44
+ throw new CliError("invalid-zip", `${source} is not a readable ZIP: ${err.message}`, void 0, 2);
45
+ }
46
+ const names = Object.keys(zip.files).filter((n) => !zip.files[n].dir);
47
+ const kept = names.filter((n) => !n.startsWith("__MACOSX/") && !n.split("/").pop().startsWith("."));
48
+ let prefix = "";
49
+ if (!kept.includes("manifest.json")) {
50
+ const candidates = kept.filter((n) => /^[^/]+\/manifest\.json$/.test(n));
51
+ if (candidates.length === 1) prefix = candidates[0].slice(0, -"manifest.json".length);
52
+ }
53
+ const read = async (name) => {
54
+ const f = zip.file(prefix + name);
55
+ return f ? f.async("string") : void 0;
56
+ };
57
+ const known = new Set(["manifest.json", "bundle.js", "style.css"].map((n) => prefix + n));
58
+ const manifest = parseManifest(await read("manifest.json"), problems);
59
+ const bundle = await read("bundle.js");
60
+ const style = await read("style.css");
61
+ if (data.byteLength > EXTENSION_LIMITS.zip) problems.push({ code: "tooLarge", message: `The ZIP is ${(data.byteLength / 1024 / 1024).toFixed(2)} MB — the limit is ${EXTENSION_LIMITS.zip / 1024 / 1024} MB.` });
62
+ return {
63
+ source,
64
+ sourceType: "zip",
65
+ manifest,
66
+ files: { ...bundle !== void 0 ? { "bundle.js": bundle } : {}, ...style !== void 0 ? { "style.css": style } : {} },
67
+ zipBytes: data.byteLength,
68
+ problems,
69
+ ignored: names.filter((n) => !known.has(n))
70
+ };
71
+ }
72
+ function readText(path) {
73
+ return existsSync(path) && statSync(path).isFile() ? readFileSync(path, "utf8") : void 0;
74
+ }
75
+ function readFolder(source) {
76
+ const problems = [];
77
+ const manifest = parseManifest(readText(join(source, "manifest.json")), problems);
78
+ const declaresStyle = !!manifest && typeof manifest === "object" && manifest.style === "style.css";
79
+ if (existsSync(join(source, "bundle.js")) && !existsSync(join(source, "package.json"))) {
80
+ const bundle2 = readText(join(source, "bundle.js"));
81
+ const style2 = readText(join(source, "style.css"));
82
+ return { source, sourceType: "folder", manifest, files: { ...bundle2 !== void 0 ? { "bundle.js": bundle2 } : {}, ...style2 !== void 0 ? { "style.css": style2 } : {} }, problems, ignored: [] };
83
+ }
84
+ const bundle = readText(join(source, "dist", "bundle.js"));
85
+ if (bundle === void 0) problems.push({ code: "notBuilt", message: "dist/bundle.js not found.", hint: "Build the project first: npm run build (vite build)." });
86
+ const stylePath = [join(source, "dist", "style.css"), join(source, "src", "style.css"), join(source, "style.css")].find((p) => existsSync(p));
87
+ const style = declaresStyle && stylePath ? readText(stylePath) : void 0;
88
+ return { source, sourceType: "project", manifest, files: { ...bundle !== void 0 ? { "bundle.js": bundle } : {}, ...style !== void 0 ? { "style.css": style } : {} }, problems, ignored: stylePath && !declaresStyle ? [stylePath] : [] };
89
+ }
90
+ async function readRawPackage(input) {
91
+ const source = resolve(input);
92
+ if (!existsSync(source)) throw new CliError("not-found", `${input} does not exist.`, "Pass the extension's project folder, an unpacked package folder or its .zip.", 2);
93
+ if (statSync(source).isFile()) {
94
+ if (!/\.zip$/i.test(source)) throw new CliError("usage", `${input} is not a .zip.`, "Pass the extension's .zip, its project folder or an unpacked folder.", 2);
95
+ return readZip(source);
96
+ }
97
+ return readFolder(source);
98
+ }
99
+ async function readPackage(input) {
100
+ const raw = await readRawPackage(input);
101
+ if (raw.problems.length) {
102
+ const p = raw.problems[0];
103
+ throw new CliError(p.code === "notBuilt" ? "not-built" : "invalid-package", raw.problems.map((x) => x.message).join(" "), p.hint ?? "Run `agentcrm ext check` on it for the full report.", 2);
104
+ }
105
+ try {
106
+ return { raw, pkg: validatePackage({ manifest: raw.manifest, files: raw.files }) };
107
+ } catch (err) {
108
+ if (err instanceof ExtensionPackageError) throw new CliError("invalid-package", err.message, "Run `agentcrm ext check` on it for the full report.", 2);
109
+ throw err;
110
+ }
111
+ }
112
+ export {
113
+ readPackage,
114
+ readRawPackage
115
+ };