@fleox-agent-crm/agentcrm 0.3.3 → 0.5.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 (62) hide show
  1. package/README.md +16 -16
  2. package/changelog.json +22 -0
  3. package/dist/admin/admin-files.json +7 -7
  4. package/dist/admin/assets/{chartsImpl-aBuZ1X4W.js → chartsImpl-zjsXDhuy.js} +1 -1
  5. package/dist/admin/assets/firebase-C3h9mF8O.js +2 -0
  6. package/dist/admin/assets/{gitJson-DzX3eZ2P.js → gitJson-nrOYgX0x.js} +1 -1
  7. package/dist/admin/assets/{index-DLy5RpPl.js → index-0ejjkW0z.js} +6 -6
  8. package/dist/admin/assets/{lucide-react.min-BgcmukkH.js → lucide-react.min-CI5dCsSm.js} +1 -1
  9. package/dist/admin/assets/{sdk-DtP9zqdi.js → sdk-mepKXtVl.js} +135 -120
  10. package/dist/admin/index.html +1 -1
  11. package/dist/agent/agentcrm.mjs +159 -74
  12. package/dist/agent/chunks/api-BEl_EKwf.mjs +79 -0
  13. package/dist/agent/chunks/{automations-DwWruxmu.mjs → automations-BqY8EwfJ.mjs} +3 -3
  14. package/dist/agent/chunks/{backup-W3CjgkAJ.mjs → backup-CO0EzdF_.mjs} +3 -3
  15. package/dist/agent/chunks/{compact-BGHI92tm.mjs → compact-qF5t42ko.mjs} +3 -3
  16. package/dist/agent/chunks/{exportCsv-C6e6XEmB.mjs → exportCsv-C-fIHLm1.mjs} +2 -2
  17. package/dist/agent/chunks/{extCheck-GgHG1dYB.mjs → extCheck-BcbUc7eB.mjs} +4 -4
  18. package/dist/agent/chunks/{files-D1daDgY0.mjs → files-C4YONVqQ.mjs} +2 -2
  19. package/dist/agent/chunks/{firebase-DWR0K1OC.mjs → firebase-Bknru2pE.mjs} +230 -53
  20. package/dist/agent/chunks/firebaseCache-sK4jA0ru.mjs +39 -0
  21. package/dist/agent/chunks/{firebaseRules-iori-s7U.mjs → firebaseRules-Jq85MBBN.mjs} +14 -3
  22. package/dist/agent/chunks/{forms-BfuWaQjA.mjs → forms-DJDop3XY.mjs} +2 -2
  23. package/dist/agent/chunks/{hosting-Dnn1k3cV.mjs → hosting-C_UDyMkm.mjs} +1 -1
  24. package/dist/agent/chunks/{importCsv-LVmMTaWU.mjs → importCsv-XjQJ9QIR.mjs} +2 -2
  25. package/dist/agent/chunks/{index-DtCezG4R.mjs → index-DWecFbTi.mjs} +1 -1
  26. package/dist/agent/chunks/{install-Bp-CwDVC.mjs → install-D1lgtRIH.mjs} +4 -4
  27. package/dist/agent/chunks/{loader-MPD9iAvh.mjs → loader-Cv0UJf6b.mjs} +11 -11
  28. package/dist/agent/chunks/{migrate-CsClnnN8.mjs → migrate-Cc_8MHZv.mjs} +1 -1
  29. package/dist/agent/chunks/{pipelines-BAKpPk1w.mjs → pipelines-DiJ2dLqQ.mjs} +3 -3
  30. package/dist/agent/chunks/{records-DcHFelq5.mjs → records-CCTxxMWF.mjs} +1 -1
  31. package/dist/agent/chunks/{registry-COfJrSmg.mjs → registry-ClzNZv9Z.mjs} +10 -10
  32. package/dist/agent/chunks/{reports-BACHGccj.mjs → reports-WlI_A5Nr.mjs} +3 -3
  33. package/dist/agent/chunks/{runtime-C0Xf0Axi.mjs → runtime-BAF5vU5D.mjs} +17 -17
  34. package/dist/agent/chunks/{schema-C4lSslXv.mjs → schema-DOnnH2zL.mjs} +3 -3
  35. package/dist/agent/chunks/schemaApply-DKHipylx.mjs +164 -0
  36. package/dist/agent/chunks/{sdk-CYgNyuoK.mjs → sdk-NYUHv7WG.mjs} +493 -214
  37. package/dist/agent/chunks/{seed-6MRmQQBR.mjs → seed-Bb26Zsni.mjs} +2 -2
  38. package/dist/agent/chunks/{server-DC3t8r30.mjs → server-Bch4TQon.mjs} +3 -3
  39. package/dist/agent/chunks/{standardObjects-CheqfDqa.mjs → standardObjects-vrmqIVLH.mjs} +1 -1
  40. package/dist/agent/chunks/{stats-BfuLWhVV.mjs → stats-DfmDP3tE.mjs} +3 -3
  41. package/dist/agent/chunks/{tools-iSH-yMpl.mjs → tools-CCk24tdh.mjs} +9 -9
  42. package/dist/agent/chunks/{views-DH1n9Lrt.mjs → views-BbhmQO3n.mjs} +4 -4
  43. package/dist/version.json +1 -1
  44. package/package.json +1 -1
  45. package/plugin/.claude-plugin/plugin.json +1 -1
  46. package/plugin/skills/agentcrm-admin/SKILL.md +67 -67
  47. package/plugin/skills/agentcrm-admin/references/records.md +5 -5
  48. package/plugin/skills/agentcrm-admin/references/schema.md +19 -19
  49. package/plugin/skills/agentcrm-extensions/SKILL.md +8 -8
  50. package/plugin/skills/agentcrm-extensions/references/module-recipes.md +2 -2
  51. package/plugin/skills/agentcrm-extensions/references/testing.md +14 -14
  52. package/plugin/skills/agentcrm-extensions/references/theme-recipes.md +3 -3
  53. package/plugin/skills/agentcrm-install/SKILL.md +13 -13
  54. package/plugin/skills/agentcrm-install/references/firebase.md +1 -1
  55. package/plugin/skills/agentcrm-install/references/github.md +1 -1
  56. package/plugin/skills/agentcrm-install/references/mcp.md +2 -2
  57. package/plugin/skills/agentcrm-install/references/plans.md +1 -1
  58. package/plugin/skills/agentcrm-install/references/s3.md +1 -1
  59. package/plugin/skills/agentcrm-manage/SKILL.md +15 -15
  60. package/plugin/skills/agentcrm-sales/SKILL.md +18 -18
  61. package/dist/admin/assets/firebase-BG1GpyBB.js +0 -2
  62. package/dist/agent/chunks/api-Dwb4xz1k.mjs +0 -27
@@ -0,0 +1,79 @@
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 DELETE_FIELD = Symbol("deleteField");
8
+ const TS_FIELD = "_ts";
9
+ const DELETIONS_COLLECTION = "deletions";
10
+ const SERVER_TIMESTAMP = Symbol("serverTimestamp");
11
+ let trackedCollections = /* @__PURE__ */ new Set();
12
+ function setTrackedCollections(collections) {
13
+ trackedCollections = new Set(collections);
14
+ }
15
+ function tombstoneId(collection, id) {
16
+ return `${collection}__${id}`;
17
+ }
18
+ function withChangeTracking(ops) {
19
+ const out = [];
20
+ for (const op of ops) {
21
+ if (!trackedCollections.has(op.collection)) {
22
+ out.push(op);
23
+ } else if (op.type === "delete") {
24
+ out.push(op, { type: "set", collection: DELETIONS_COLLECTION, id: tombstoneId(op.collection, op.id), data: { collection: op.collection, id: op.id, [TS_FIELD]: SERVER_TIMESTAMP } });
25
+ } else {
26
+ out.push({ ...op, data: { ...op.data, [TS_FIELD]: SERVER_TIMESTAMP } });
27
+ }
28
+ }
29
+ return out;
30
+ }
31
+ function splitTs(id, raw) {
32
+ const { [TS_FIELD]: ts, ...data } = raw;
33
+ return typeof ts === "number" ? { id, data, ts } : { id, data };
34
+ }
35
+ function errorCode(err) {
36
+ return err?.code ?? "";
37
+ }
38
+ function isPermissionDenied(err) {
39
+ const code = errorCode(err);
40
+ return code === "permission-denied" || code === "firestore/permission-denied";
41
+ }
42
+ const FIRESTORE_BATCH_LIMIT = 500;
43
+ function chunkForCommit(ops) {
44
+ const out = [];
45
+ let part = [];
46
+ let weight = 0;
47
+ for (const op of ops) {
48
+ const w = op.type === "delete" && trackedCollections.has(op.collection) ? 2 : 1;
49
+ if (weight + w > FIRESTORE_BATCH_LIMIT) {
50
+ out.push(part);
51
+ part = [];
52
+ weight = 0;
53
+ }
54
+ part.push(op);
55
+ weight += w;
56
+ }
57
+ if (part.length) out.push(part);
58
+ return out;
59
+ }
60
+ function chunk(items, size) {
61
+ const out = [];
62
+ for (let i = 0; i < items.length; i += size) out.push(items.slice(i, i + size));
63
+ return out;
64
+ }
65
+ export {
66
+ DELETE_FIELD as D,
67
+ FIRESTORE_BATCH_LIMIT as F,
68
+ SERVER_TIMESTAMP as S,
69
+ TS_FIELD as T,
70
+ DELETIONS_COLLECTION as a,
71
+ chunk as b,
72
+ chunkForCommit as c,
73
+ splitTs as d,
74
+ errorCode as e,
75
+ isPermissionDenied as i,
76
+ setTrackedCollections as s,
77
+ tombstoneId as t,
78
+ withChangeTracking as w
79
+ };
@@ -8,16 +8,16 @@ import { toIsoDate, recordLabel, formatFieldValue, normalizeEmail } from "./fiel
8
8
  import { n as newId } from "./ids-CvnmrMJX.mjs";
9
9
  import { plugins } from "./pluginRegistry-DyHtlkcr.mjs";
10
10
  import { r as resolveField, m as matchesFilter } from "./query-C5nJB_3C.mjs";
11
- import { queryContext, setRecordOwner, updateRecord } from "./records-DcHFelq5.mjs";
11
+ import { queryContext, setRecordOwner, updateRecord } from "./records-CCTxxMWF.mjs";
12
12
  import { getSettings } from "./settings-Di8nzNEL.mjs";
13
13
  import { userOf, automationRunsOf, asDoc, requireRecord, requireObject, automationsOf, objectOf, recordsOf, thaw, recordOf, usersOf } from "./store-5gqRSKXg.mjs";
14
14
  import { F as FILTER_OPS } from "../agentcrm.mjs";
15
15
  import { lastActivityAt, addNote, createTask } from "./activities-Cj_SIOay.mjs";
16
16
  import { OpsError, assertCan } from "./context-CxFfqS4D.mjs";
17
- import { pipelineOfRecord } from "./pipelines-BAKpPk1w.mjs";
17
+ import { pipelineOfRecord } from "./pipelines-DiJ2dLqQ.mjs";
18
18
  import { getAutomationAction, listAutomationActions } from "./extensionActions-D2HtpQz4.mjs";
19
19
  import { registerAutomationAction, subscribeAutomationActions } from "./extensionActions-D2HtpQz4.mjs";
20
- import "./standardObjects-CheqfDqa.mjs";
20
+ import "./standardObjects-vrmqIVLH.mjs";
21
21
  import "./relations-B9TynBmA.mjs";
22
22
  import "./runtimeConfig-CCOFg1RM.mjs";
23
23
  import "node:util";
@@ -7,7 +7,7 @@ const __dirname = __acrmDirname(__filename);
7
7
  import { J as JSZip } from "./index-w03ReRs4.mjs";
8
8
  import { F as FILE_ID_RE } from "./ids-u-noYcy5.mjs";
9
9
  import { assertCan, OpsError } from "./context-CxFfqS4D.mjs";
10
- import { exportJson, importJson, planImportJson } from "./exportCsv-C6e6XEmB.mjs";
10
+ import { exportJson, importJson, planImportJson } from "./exportCsv-C-fIHLm1.mjs";
11
11
  import { filesOf } from "./store-5gqRSKXg.mjs";
12
12
  import "./_commonjsHelpers-ClciMIcB.mjs";
13
13
  import "stream";
@@ -19,9 +19,9 @@ import "./papaparse-Df7i1dHr.mjs";
19
19
  import "./fields-B0bitgmr.mjs";
20
20
  import "./query-C5nJB_3C.mjs";
21
21
  import "./types-BOy9PUl7.mjs";
22
- import "./records-DcHFelq5.mjs";
22
+ import "./records-CCTxxMWF.mjs";
23
23
  import "./ids-CvnmrMJX.mjs";
24
- import "./standardObjects-CheqfDqa.mjs";
24
+ import "./standardObjects-vrmqIVLH.mjs";
25
25
  import "./relations-B9TynBmA.mjs";
26
26
  const BACKUP_DATA_FILE = "data.json";
27
27
  const BACKUP_FILES_DIR = "files/";
@@ -6,8 +6,8 @@ const __filename = __acrmPath(import.meta.url);
6
6
  const __dirname = __acrmDirname(__filename);
7
7
  import { assertCan } from "./context-CxFfqS4D.mjs";
8
8
  import { b as fileCleanupMutations } from "./relations-B9TynBmA.mjs";
9
- import { purgeFiles } from "./files-D1daDgY0.mjs";
10
- import { hardDeleteMutations } from "./records-DcHFelq5.mjs";
9
+ import { purgeFiles } from "./files-C4YONVqQ.mjs";
10
+ import { hardDeleteMutations } from "./records-CCTxxMWF.mjs";
11
11
  import { recordsOf, activitiesOf, automationRunsOf } from "./store-5gqRSKXg.mjs";
12
12
  import "./pluginRegistry-DyHtlkcr.mjs";
13
13
  import "./fields-B0bitgmr.mjs";
@@ -27,7 +27,7 @@ import "node:crypto";
27
27
  import "node:os";
28
28
  import "node:child_process";
29
29
  import "node:http";
30
- import "./standardObjects-CheqfDqa.mjs";
30
+ import "./standardObjects-vrmqIVLH.mjs";
31
31
  async function compactData(ctx, opts = {}) {
32
32
  assertCan(ctx, "compact");
33
33
  const cutoff = ctx.now() - (opts.olderThanDays ?? 0) * 864e5;
@@ -9,13 +9,13 @@ import { formatAddress, formatFieldValue } from "./fields-B0bitgmr.mjs";
9
9
  import { a as applySorts, b as applyFilters, r as resolveField, g as getRecordValue } from "./query-C5nJB_3C.mjs";
10
10
  import { COLLECTIONS } from "./types-BOy9PUl7.mjs";
11
11
  import { assertCan, OpsError } from "./context-CxFfqS4D.mjs";
12
- import { queryContext } from "./records-DcHFelq5.mjs";
12
+ import { queryContext } from "./records-CCTxxMWF.mjs";
13
13
  import { requireObject, recordsOf, userOf } from "./store-5gqRSKXg.mjs";
14
14
  import "./_commonjsHelpers-ClciMIcB.mjs";
15
15
  import "stream";
16
16
  import "./pluginRegistry-DyHtlkcr.mjs";
17
17
  import "./ids-CvnmrMJX.mjs";
18
- import "./standardObjects-CheqfDqa.mjs";
18
+ import "./standardObjects-vrmqIVLH.mjs";
19
19
  import "./relations-B9TynBmA.mjs";
20
20
  function exportCsv(ctx, objectId, opts = {}) {
21
21
  assertCan(ctx, "export");
@@ -93,11 +93,11 @@ async function loadInSandbox(report, manifest, files, opts) {
93
93
  import("./memory-WNR3VHUJ.mjs"),
94
94
  import("./context-CxFfqS4D.mjs"),
95
95
  import("./pluginRegistry-DyHtlkcr.mjs"),
96
- import("./seed-6MRmQQBR.mjs"),
96
+ import("./seed-Bb26Zsni.mjs"),
97
97
  import("./store-5gqRSKXg.mjs"),
98
- import("./loader-MPD9iAvh.mjs"),
99
- import("./registry-COfJrSmg.mjs"),
100
- import("./tools-iSH-yMpl.mjs"),
98
+ import("./loader-Cv0UJf6b.mjs"),
99
+ import("./registry-ClzNZv9Z.mjs"),
100
+ import("./tools-CCk24tdh.mjs"),
101
101
  import("./extensionActions-D2HtpQz4.mjs"),
102
102
  import("./featureRegistry-DSsZbhrh.mjs"),
103
103
  import("./themes-DPZFv3yc.mjs"),
@@ -11,7 +11,7 @@ import { m as mapLimit } from "./http-Bk9ICb7I.mjs";
11
11
  import { assertCan, OpsError, actorId } from "./context-CxFfqS4D.mjs";
12
12
  import { s as sameTarget, e as fileFieldIds, g as fileIdsIn, f as fileTargetPatch, h as referencedFileIds } from "./relations-B9TynBmA.mjs";
13
13
  import { timelineScope } from "./activities-Cj_SIOay.mjs";
14
- import { updateRecord } from "./records-DcHFelq5.mjs";
14
+ import { updateRecord } from "./records-CCTxxMWF.mjs";
15
15
  import { fileOf, requireFile, filesOf, recordOf, activitiesOf, settingsDoc, asDoc, requireRecord, objectOf, requireObject, recordsOf } from "./store-5gqRSKXg.mjs";
16
16
  import "./index-BM8qeGOb.mjs";
17
17
  import "./pluginRegistry-DyHtlkcr.mjs";
@@ -26,7 +26,7 @@ import "node:crypto";
26
26
  import "node:os";
27
27
  import "node:child_process";
28
28
  import "node:http";
29
- import "./standardObjects-CheqfDqa.mjs";
29
+ import "./standardObjects-vrmqIVLH.mjs";
30
30
  const ORPHAN_GRACE_MS = 864e5;
31
31
  function maxUploadBytes(ctx) {
32
32
  const mb = settingsDoc(ctx.store)?.maxUploadMb;
@@ -7,7 +7,8 @@ const __dirname = __acrmDirname(__filename);
7
7
  import "./jsx-runtime-Ca2uwGDr.mjs";
8
8
  import { r as reactExports } from "./index-Bnmav-Oh.mjs";
9
9
  import { c as createStoreCore, p as plainCopy, e as emptySnapshot, n as normalizeDoc } from "./core-Dy7vY9H6.mjs";
10
- import { i as isPermissionDenied, D as DELETE_FIELD, e as errorCode, c as chunk, F as FIRESTORE_BATCH_LIMIT } from "./api-Dwb4xz1k.mjs";
10
+ import { i as isPermissionDenied, D as DELETE_FIELD, a as DELETIONS_COLLECTION, e as errorCode, c as chunkForCommit, s as setTrackedCollections } from "./api-BEl_EKwf.mjs";
11
+ import { F, S, T, b, d, t, w } from "./api-BEl_EKwf.mjs";
11
12
  import { w as withFileStore, F as FileStoreUnavailableError } from "./index-RCmo-uld.mjs";
12
13
  import { COLLECTIONS } from "./types-BOy9PUl7.mjs";
13
14
  import { a as assertFileId } from "./ids-u-noYcy5.mjs";
@@ -17,10 +18,70 @@ function registerAuthAdapter(backend, adapter) {
17
18
  adapters.set(backend, adapter);
18
19
  }
19
20
  reactExports.createContext(null);
21
+ const FIREBASE_CACHE_VERSION = 1;
22
+ const FULL_RELOAD_MS = 7 * 24 * 3600 * 1e3;
23
+ function firebaseCacheKey(projectId, uid) {
24
+ return `${projectId}:${uid}`;
25
+ }
26
+ function isUsableCache(state, now) {
27
+ return !!state && state.v === FIREBASE_CACHE_VERSION && typeof state.cursor === "number" && now - state.fullAt < FULL_RELOAD_MS;
28
+ }
29
+ const DB_NAME = "agentcrm-firebase-cache";
30
+ const STORE = "datasets";
31
+ function openDb() {
32
+ return new Promise((resolve, reject) => {
33
+ const req = indexedDB.open(DB_NAME, 1);
34
+ req.onupgradeneeded = () => req.result.createObjectStore(STORE);
35
+ req.onsuccess = () => resolve(req.result);
36
+ req.onerror = () => reject(req.error);
37
+ });
38
+ }
39
+ function run(mode, fn) {
40
+ return openDb().then(
41
+ (db) => new Promise((resolve, reject) => {
42
+ const tx = db.transaction(STORE, mode);
43
+ const req = fn(tx.objectStore(STORE));
44
+ tx.oncomplete = () => {
45
+ db.close();
46
+ resolve(req.result);
47
+ };
48
+ tx.onerror = () => {
49
+ db.close();
50
+ reject(tx.error);
51
+ };
52
+ })
53
+ );
54
+ }
55
+ function indexedDbFirebaseCache() {
56
+ if (typeof indexedDB === "undefined") return null;
57
+ return {
58
+ async read(key) {
59
+ try {
60
+ return await run("readonly", (s) => s.get(key)) ?? null;
61
+ } catch {
62
+ return null;
63
+ }
64
+ },
65
+ async write(key, state) {
66
+ try {
67
+ await run("readwrite", (s) => s.put(state, key));
68
+ } catch {
69
+ }
70
+ },
71
+ async clear(key) {
72
+ try {
73
+ await run("readwrite", (s) => s.delete(key));
74
+ } catch {
75
+ }
76
+ }
77
+ };
78
+ }
20
79
  const INVITES_COLLECTION = "invites";
21
80
  const EXTENSION_CHUNKS_COLLECTION = "extensionChunks";
22
81
  const EXTENSION_CHUNK_CHARS = 3e5;
23
82
  const INBOX_COLLECTION = "inbox";
83
+ const TRACKED = [...COLLECTIONS, EXTENSION_CHUNKS_COLLECTION];
84
+ setTrackedCollections(TRACKED);
24
85
  const ROLES = ["admin", "member", "viewer"];
25
86
  class FirebaseAccessError extends Error {
26
87
  constructor(reason, message, email) {
@@ -37,8 +98,8 @@ function toData(doc) {
37
98
  const { id: _id, ...rest } = plainCopy(doc);
38
99
  return rest;
39
100
  }
40
- function fromData(d) {
41
- return normalizeDoc({ ...d.data, id: d.id });
101
+ function fromData(d2) {
102
+ return normalizeDoc({ ...d2.data, id: d2.id });
42
103
  }
43
104
  function isRole(v) {
44
105
  return typeof v === "string" && ROLES.includes(v);
@@ -103,18 +164,18 @@ function assembleExtensionDocs(docs, chunks) {
103
164
  const ext = String(c.data.ext ?? "");
104
165
  byExt.set(ext, [...byExt.get(ext) ?? [], c]);
105
166
  }
106
- return docs.map((d) => {
107
- const counts = d.data.chunks;
108
- if (!counts || typeof counts !== "object") return d;
167
+ return docs.map((d2) => {
168
+ const counts = d2.data.chunks;
169
+ if (!counts || typeof counts !== "object") return d2;
109
170
  const files = {};
110
- const parts = byExt.get(d.id) ?? [];
171
+ const parts = byExt.get(d2.id) ?? [];
111
172
  for (const [file, n] of Object.entries(counts)) {
112
- const pieces = parts.filter((p) => p.data.file === file).sort((a, b) => Number(a.data.index) - Number(b.data.index));
173
+ const pieces = parts.filter((p) => p.data.file === file).sort((a, b2) => Number(a.data.index) - Number(b2.data.index));
113
174
  if (pieces.length !== n) continue;
114
175
  files[file] = pieces.map((p) => String(p.data.data ?? "")).join("");
115
176
  }
116
- const { chunks: _chunks, ...rest } = d.data;
117
- return { id: d.id, data: { ...rest, files } };
177
+ const { chunks: _chunks, ...rest } = d2.data;
178
+ return { id: d2.id, data: { ...rest, files } };
118
179
  });
119
180
  }
120
181
  function mutationOps(mutations, before) {
@@ -187,8 +248,8 @@ function createFirebaseStoreWith(opts) {
187
248
  function buildSnapshot(raw, chunks) {
188
249
  const s = emptySnapshot();
189
250
  for (const c of COLLECTIONS) {
190
- const docs = c === "extensions" ? assembleExtensionDocs(raw.get(c) ?? [], chunks) : raw.get(c) ?? [];
191
- for (const d of docs) s[c].set(d.id, fromData(d));
251
+ const docs2 = c === "extensions" ? assembleExtensionDocs(raw.get(c) ?? [], chunks) : raw.get(c) ?? [];
252
+ for (const d2 of docs2) s[c].set(d2.id, fromData(d2));
192
253
  }
193
254
  rawUsers = new Map(s.users);
194
255
  for (const u of [...s.users.values()]) {
@@ -273,7 +334,7 @@ function createFirebaseStoreWith(opts) {
273
334
  const ops = mutationOps(mutations, before);
274
335
  if (!currentIdentity && mutations.some((m) => m.collection === "users")) await getIdentity();
275
336
  if (currentIdentity?.role === "admin") ops.push(...inviteOps(mutations, (id) => before.users.get(id) ?? rawUsers.get(id), after));
276
- for (const part of chunk(ops, FIRESTORE_BATCH_LIMIT)) await api.commit(part);
337
+ for (const part of chunkForCommit(ops)) await api.commit(part);
277
338
  for (const m of mutations) {
278
339
  if (m.collection !== "users") continue;
279
340
  const id = m.op === "set" ? m.doc.id : m.id;
@@ -282,52 +343,143 @@ function createFirebaseStoreWith(opts) {
282
343
  else rawUsers.delete(id);
283
344
  }
284
345
  }
346
+ const docs = new Map(TRACKED.map((c) => [c, /* @__PURE__ */ new Map()]));
347
+ let cursor = 0;
348
+ let fullAt = 0;
349
+ let cacheKey = null;
350
+ let loadInfo = null;
351
+ let markLoaded = () => void 0;
352
+ const loadedOnce = new Promise((resolve) => markLoaded = resolve);
353
+ let saveTimer = null;
354
+ const advance = (d2) => {
355
+ if (typeof d2.ts === "number" && d2.ts > cursor) cursor = d2.ts;
356
+ };
357
+ function upsert(c, list) {
358
+ const m = docs.get(c);
359
+ for (const d2 of list) {
360
+ m.set(d2.id, d2);
361
+ advance(d2);
362
+ }
363
+ }
364
+ function applyTombstones(list) {
365
+ for (const t2 of list) {
366
+ advance(t2);
367
+ const c = t2.data.collection;
368
+ const id = t2.data.id;
369
+ if (typeof c === "string" && typeof id === "string") docs.get(c)?.delete(id);
370
+ }
371
+ }
372
+ function snapshotFromDocsMap() {
373
+ const raw = new Map(COLLECTIONS.map((c) => [c, [...docs.get(c).values()]]));
374
+ return buildSnapshot(raw, [...docs.get(EXTENSION_CHUNKS_COLLECTION).values()]);
375
+ }
376
+ async function saveCache() {
377
+ if (!opts.cache || !cacheKey) return;
378
+ const collections = Object.fromEntries(TRACKED.map((c) => [c, [...docs.get(c).values()]]));
379
+ await opts.cache.write(cacheKey, { v: FIREBASE_CACHE_VERSION, cursor, fullAt, collections });
380
+ }
381
+ function scheduleSave() {
382
+ if (saveTimer) return;
383
+ saveTimer = setTimeout(() => {
384
+ saveTimer = null;
385
+ void saveCache();
386
+ }, 1e3);
387
+ saveTimer.unref?.();
388
+ }
389
+ async function readSince(c, since) {
390
+ try {
391
+ return await api.listSince(c, since);
392
+ } catch (err) {
393
+ if (isPermissionDenied(err)) return null;
394
+ throw err;
395
+ }
396
+ }
285
397
  const core = createStoreCore({
286
398
  kind: "firebase",
287
399
  async load() {
288
- const raw = /* @__PURE__ */ new Map();
289
- let chunks = [];
290
- await Promise.all([...COLLECTIONS.map(async (c) => raw.set(c, await readCollection(c))), readCollection(EXTENSION_CHUNKS_COLLECTION).then((d) => chunks = d)]);
291
- const snap = buildSnapshot(raw, chunks);
400
+ const readsBefore = api.readCount();
401
+ const user = await api.auth.currentUser();
402
+ cacheKey = opts.cache && opts.projectId && user ? firebaseCacheKey(opts.projectId, user.uid) : null;
403
+ const cached = cacheKey ? await opts.cache.read(cacheKey) : null;
404
+ let mode = "full";
405
+ if (isUsableCache(cached, now())) {
406
+ mode = "incremental";
407
+ cursor = cached.cursor;
408
+ fullAt = cached.fullAt;
409
+ for (const c of TRACKED) docs.set(c, new Map((cached.collections[c] ?? []).map((d2) => [d2.id, d2])));
410
+ const since = cursor;
411
+ const changed = await Promise.all(TRACKED.map(async (c) => [c, await readSince(c, since)]));
412
+ for (const [c, list] of changed) {
413
+ if (list === null) docs.set(c, /* @__PURE__ */ new Map());
414
+ else upsert(c, list);
415
+ }
416
+ applyTombstones(await readSince(DELETIONS_COLLECTION, since) ?? []);
417
+ } else {
418
+ cursor = 0;
419
+ const all = await Promise.all(TRACKED.map(async (c) => [c, await readCollection(c)]));
420
+ for (const [c, list] of all) {
421
+ docs.set(c, /* @__PURE__ */ new Map());
422
+ upsert(c, list);
423
+ }
424
+ fullAt = now();
425
+ }
426
+ loadInfo = { mode, reads: api.readCount() - readsBefore };
427
+ globalThis.__AGENTCRM_FIRESTORE__ = { ...loadInfo, cursor, totalReads: api.readCount };
428
+ await saveCache();
429
+ markLoaded();
430
+ const snap = snapshotFromDocsMap();
292
431
  scheduleReconcile();
293
432
  return snap;
294
433
  },
295
434
  persist: (mutations, before, after) => persist(mutations, before, after),
435
+ // Live updates: one listener per collection on documents written after
436
+ // the load's cursor — only changes are read (and billed).
296
437
  watch(onRemote) {
297
- const raw = /* @__PURE__ */ new Map();
298
- let chunks = null;
438
+ let offs = [];
439
+ let stopped = false;
299
440
  const emit = () => {
300
- if (raw.size < COLLECTIONS.length || chunks === null) return;
301
- onRemote(buildSnapshot(raw, chunks));
441
+ onRemote(snapshotFromDocsMap());
302
442
  scheduleReconcile();
443
+ scheduleSave();
444
+ };
445
+ void loadedOnce.then(() => {
446
+ if (stopped) return;
447
+ const since = cursor;
448
+ offs = TRACKED.map(
449
+ (c) => api.watchSince(
450
+ c,
451
+ since,
452
+ (ch) => {
453
+ upsert(c, ch.upserts);
454
+ for (const id of ch.removed) docs.get(c).delete(id);
455
+ emit();
456
+ },
457
+ (err) => {
458
+ if (isPermissionDenied(err)) {
459
+ docs.set(c, /* @__PURE__ */ new Map());
460
+ emit();
461
+ } else console.warn(`[firebase] live updates of ${c} stopped:`, err);
462
+ }
463
+ )
464
+ );
465
+ offs.push(
466
+ api.watchSince(
467
+ DELETIONS_COLLECTION,
468
+ since,
469
+ (ch) => {
470
+ applyTombstones(ch.upserts);
471
+ emit();
472
+ },
473
+ (err) => {
474
+ if (!isPermissionDenied(err)) console.warn("[firebase] live deletions stopped:", err);
475
+ }
476
+ )
477
+ );
478
+ });
479
+ return () => {
480
+ stopped = true;
481
+ offs.forEach((off) => off());
303
482
  };
304
- const offs = COLLECTIONS.map(
305
- (c) => api.watchCollection(
306
- c,
307
- (docs) => {
308
- raw.set(c, docs);
309
- emit();
310
- },
311
- (err) => {
312
- if (isPermissionDenied(err)) raw.set(c, []);
313
- else console.warn(`[firebase] live updates of ${c} stopped:`, err);
314
- }
315
- )
316
- );
317
- offs.push(
318
- api.watchCollection(
319
- EXTENSION_CHUNKS_COLLECTION,
320
- (docs) => {
321
- chunks = docs;
322
- emit();
323
- },
324
- (err) => {
325
- if (isPermissionDenied(err)) chunks = [];
326
- else console.warn("[firebase] live updates of extension chunks stopped:", err);
327
- }
328
- )
329
- );
330
- return () => offs.forEach((off) => off());
331
483
  },
332
484
  identity: getIdentity,
333
485
  close() {
@@ -383,9 +535,23 @@ function createFirebaseStoreWith(opts) {
383
535
  console.warn("[firebase] could not link invited users:", err);
384
536
  }
385
537
  }
538
+ const closeCore = core.close?.bind(core);
539
+ core.close = () => {
540
+ if (saveTimer) {
541
+ clearTimeout(saveTimer);
542
+ saveTimer = null;
543
+ void saveCache();
544
+ }
545
+ closeCore?.();
546
+ };
386
547
  withFileStore(core, firebaseFileStore(api));
548
+ activeCacheClear = async () => {
549
+ if (opts.cache && cacheKey) await opts.cache.clear(cacheKey);
550
+ };
387
551
  return Object.assign(core, {
388
552
  api,
553
+ readCount: () => api.readCount(),
554
+ lastLoad: () => loadInfo,
389
555
  accessError: () => lastAccessError,
390
556
  resetSession() {
391
557
  identityPromise = null;
@@ -399,15 +565,17 @@ function firebaseApiFor(config) {
399
565
  const key = config.firebase.projectId ?? "default";
400
566
  let api = apis.get(key);
401
567
  if (!api) {
402
- api = import("./sdk-CYgNyuoK.mjs").then((m) => m.createSdkFirebaseApi(config.firebase));
568
+ api = import("./sdk-NYUHv7WG.mjs").then((m) => m.createSdkFirebaseApi(config.firebase));
403
569
  apis.set(key, api);
404
570
  api.catch(() => apis.delete(key));
405
571
  }
406
572
  return api;
407
573
  }
408
574
  let activeApi = null;
575
+ let activeCacheClear = null;
409
576
  registerAuthAdapter("firebase", {
410
577
  async signOut() {
578
+ await activeCacheClear?.();
411
579
  await activeApi?.auth.signOut();
412
580
  }
413
581
  });
@@ -451,21 +619,26 @@ async function createFirebaseStore(config, opts = {}) {
451
619
  const api = await firebaseApiFor(cfg);
452
620
  activeApi = api;
453
621
  await signInWithSecrets(api, opts.secrets);
454
- return createFirebaseStoreWith({ api });
622
+ const cache = opts.firebaseCache === void 0 ? indexedDbFirebaseCache() : opts.firebaseCache;
623
+ return createFirebaseStoreWith({ api, cache, projectId: cfg.firebase.projectId });
455
624
  }
456
625
  export {
457
626
  DELETE_FIELD,
627
+ DELETIONS_COLLECTION,
458
628
  EXTENSION_CHUNKS_COLLECTION,
459
629
  EXTENSION_CHUNK_CHARS,
460
- FIRESTORE_BATCH_LIMIT,
630
+ F as FIRESTORE_BATCH_LIMIT,
461
631
  FirebaseAccessError,
462
632
  INBOX_COLLECTION,
463
633
  INVITES_COLLECTION,
634
+ S as SERVER_TIMESTAMP,
464
635
  STORAGE_FILES_DIR,
465
636
  STORAGE_NOT_ENABLED,
637
+ T as TS_FIELD,
466
638
  assembleExtensionDocs,
467
639
  checkFirebaseAccess,
468
- chunk,
640
+ b as chunk,
641
+ chunkForCommit,
469
642
  createFirebaseStore,
470
643
  createFirebaseStoreWith,
471
644
  emailKey,
@@ -476,5 +649,9 @@ export {
476
649
  isFirebaseStore,
477
650
  isPermissionDenied,
478
651
  mutationOps,
479
- signInWithSecrets
652
+ setTrackedCollections,
653
+ signInWithSecrets,
654
+ d as splitTs,
655
+ t as tombstoneId,
656
+ w as withChangeTracking
480
657
  };
@@ -0,0 +1,39 @@
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, rmSync } from "node:fs";
8
+ import { w as writeJsonFile, h as readJsonFile, i as homePath } from "../agentcrm.mjs";
9
+ import "node:util";
10
+ import "node:path";
11
+ import "node:url";
12
+ import "node:crypto";
13
+ import "node:os";
14
+ import "node:child_process";
15
+ import "node:http";
16
+ function cacheFile(key) {
17
+ return homePath("cache", `firebase-${key.replace(/[^a-z0-9._-]+/gi, "_")}.json`);
18
+ }
19
+ function fileFirebaseCache() {
20
+ return {
21
+ async read(key) {
22
+ try {
23
+ return readJsonFile(cacheFile(key), null);
24
+ } catch {
25
+ return null;
26
+ }
27
+ },
28
+ async write(key, state) {
29
+ writeJsonFile(cacheFile(key), state, 384);
30
+ },
31
+ async clear(key) {
32
+ const f = cacheFile(key);
33
+ if (existsSync(f)) rmSync(f, { force: true });
34
+ }
35
+ };
36
+ }
37
+ export {
38
+ fileFirebaseCache
39
+ };
@@ -124,7 +124,7 @@ function buildFirestoreRules(input) {
124
124
  " // Nobody changes their own role / email / disabled flag.",
125
125
  " allow update: if (isAdmin() && validRole(request.resource.data.role))",
126
126
  " || (isActive() && request.auth.uid == uid",
127
- " && request.resource.data.diff(resource.data).affectedKeys().hasOnly(['name', 'avatarUrl', 'updatedAt']));",
127
+ " && request.resource.data.diff(resource.data).affectedKeys().hasOnly(['name', 'avatarUrl', 'updatedAt', '_ts']));",
128
128
  " allow delete: if isAdmin();",
129
129
  " }",
130
130
  "",
@@ -152,7 +152,7 @@ function buildFirestoreRules(input) {
152
152
  " allow read: if isActive();",
153
153
  " allow create, delete: if isAdmin();",
154
154
  " allow update: if isAdmin()",
155
- " || (isMember() && request.resource.data.diff(resource.data).affectedKeys().hasOnly(['lastRunAt', 'updatedAt']));",
155
+ " || (isMember() && request.resource.data.diff(resource.data).affectedKeys().hasOnly(['lastRunAt', 'updatedAt', '_ts']));",
156
156
  " }",
157
157
  "",
158
158
  " match /automationRuns/{id} {",
@@ -180,6 +180,17 @@ function buildFirestoreRules(input) {
180
180
  " allow write: if isAdmin();",
181
181
  " }",
182
182
  "",
183
+ " // Tombstones of deleted documents, so cached copies drop them on the",
184
+ " // next incremental read (src/data/firebase/api.ts, withChangeTracking).",
185
+ " // Written in the same batch as the delete, which the rules above judge.",
186
+ " match /deletions/{id} {",
187
+ " allow read: if isActive();",
188
+ " allow create, update: if isMember()",
189
+ " && request.resource.data.keys().hasOnly(['collection', 'id', '_ts'])",
190
+ " && request.resource.data._ts == request.time;",
191
+ " allow delete: if isAdmin();",
192
+ " }",
193
+ "",
183
194
  " // ─── inbox/{id}: public lead-form submissions ──────────────────────",
184
195
  " function validSubmission(d) {",
185
196
  " return d.keys().hasAll(['formId', 'data', 'createdAt'])",
@@ -213,7 +224,7 @@ function buildFirestoreRules(input) {
213
224
  );
214
225
  return lines.join("\n");
215
226
  }
216
- [...MEMBER_WRITE, "views", "automations", "automationRuns", "files", ...ADMIN_WRITE, "users"];
227
+ [...MEMBER_WRITE, "views", "automations", "automationRuns", "files", ...ADMIN_WRITE, "users", "deletions"];
217
228
  export {
218
229
  INBOX_LIMITS,
219
230
  buildFirestoreRules,