@fleox-agent-crm/agentcrm 0.3.2 → 0.4.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 (39) hide show
  1. package/README.md +16 -16
  2. package/changelog.json +24 -0
  3. package/dist/admin/admin-files.json +7 -7
  4. package/dist/admin/assets/{chartsImpl-qlHW3VOv.js → chartsImpl-zjsXDhuy.js} +1 -1
  5. package/dist/admin/assets/firebase-C3h9mF8O.js +2 -0
  6. package/dist/admin/assets/{gitJson-wy2Tytfd.js → gitJson-nrOYgX0x.js} +1 -1
  7. package/dist/admin/assets/{index-BpaxrJx9.js → index-0ejjkW0z.js} +6 -6
  8. package/dist/admin/assets/{lucide-react.min-Djrg16BH.js → lucide-react.min-CI5dCsSm.js} +1 -1
  9. package/dist/admin/assets/{sdk-mu08RFtk.js → sdk-mepKXtVl.js} +135 -120
  10. package/dist/admin/index.html +1 -1
  11. package/dist/agent/agentcrm.mjs +29 -16
  12. package/dist/agent/chunks/api-BEl_EKwf.mjs +79 -0
  13. package/dist/agent/chunks/{firebase-BciZWdc6.mjs → firebase-Bknru2pE.mjs} +242 -63
  14. package/dist/agent/chunks/firebaseCache-sK4jA0ru.mjs +39 -0
  15. package/dist/agent/chunks/{firebaseRules-iori-s7U.mjs → firebaseRules-Jq85MBBN.mjs} +14 -3
  16. package/dist/agent/chunks/{hosting-Dnn1k3cV.mjs → hosting-C_UDyMkm.mjs} +1 -1
  17. package/dist/agent/chunks/{index-rut-KC39.mjs → index-DWecFbTi.mjs} +1 -1
  18. package/dist/agent/chunks/{install-BBTA4nMm.mjs → install-BLNflFes.mjs} +3 -3
  19. package/dist/agent/chunks/{sdk-CYgNyuoK.mjs → sdk-NYUHv7WG.mjs} +493 -214
  20. package/dist/version.json +1 -1
  21. package/package.json +1 -1
  22. package/plugin/.claude-plugin/plugin.json +1 -1
  23. package/plugin/skills/agentcrm-admin/SKILL.md +67 -67
  24. package/plugin/skills/agentcrm-admin/references/records.md +5 -5
  25. package/plugin/skills/agentcrm-admin/references/schema.md +19 -19
  26. package/plugin/skills/agentcrm-extensions/SKILL.md +8 -8
  27. package/plugin/skills/agentcrm-extensions/references/module-recipes.md +2 -2
  28. package/plugin/skills/agentcrm-extensions/references/testing.md +14 -14
  29. package/plugin/skills/agentcrm-extensions/references/theme-recipes.md +3 -3
  30. package/plugin/skills/agentcrm-install/SKILL.md +13 -13
  31. package/plugin/skills/agentcrm-install/references/firebase.md +1 -1
  32. package/plugin/skills/agentcrm-install/references/github.md +1 -1
  33. package/plugin/skills/agentcrm-install/references/mcp.md +2 -2
  34. package/plugin/skills/agentcrm-install/references/plans.md +1 -1
  35. package/plugin/skills/agentcrm-install/references/s3.md +1 -1
  36. package/plugin/skills/agentcrm-manage/SKILL.md +15 -15
  37. package/plugin/skills/agentcrm-sales/SKILL.md +18 -18
  38. package/dist/admin/assets/firebase-U38f5kc9.js +0 -2
  39. package/dist/agent/chunks/api-Dwb4xz1k.mjs +0 -27
@@ -120,7 +120,7 @@
120
120
  }
121
121
  }
122
122
  </script>
123
- <script type="module" crossorigin src="./assets/index-BpaxrJx9.js"></script>
123
+ <script type="module" crossorigin src="./assets/index-0ejjkW0z.js"></script>
124
124
  <link rel="stylesheet" crossorigin href="./assets/index-TcjySLVg.css">
125
125
  </head>
126
126
  <body>
@@ -142,7 +142,7 @@ const output = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProper
142
142
  toCliError
143
143
  }, Symbol.toStringTag, { value: "Module" }));
144
144
  const name = "@fleox-agent-crm/agentcrm";
145
- const version = "0.3.2";
145
+ const version = "0.4.0";
146
146
  const type = "module";
147
147
  const description = "The static CRM your AI agent can run: a Twenty / HubSpot-style CRM that runs entirely in the browser or headless through the agentcrm CLI, with data on storage you own.";
148
148
  const license = "SEE LICENSE IN LICENSE";
@@ -608,7 +608,7 @@ async function openStore(runtime, opts) {
608
608
  }
609
609
  if (runtime.backend === "journal") {
610
610
  if (!opts.dataTarget) throw new CliError("internal", "journal store without a data location");
611
- const { buildConnector } = await import("./chunks/hosting-Dnn1k3cV.mjs");
611
+ const { buildConnector } = await import("./chunks/hosting-C_UDyMkm.mjs");
612
612
  const { createJournalStore } = await import("./chunks/index-B-4W0-P4.mjs");
613
613
  const { publicSettings, secrets } = rc.splitSecretSettings(opts.dataTarget.settings);
614
614
  rc.setConnectorSecrets(runtime, secrets);
@@ -629,8 +629,9 @@ async function openStore(runtime, opts) {
629
629
  const { prepareFirebaseSession: prepareFirebaseSession2 } = await Promise.resolve().then(() => firebaseSession);
630
630
  await prepareFirebaseSession2(opts.account, runtime.firebase, opts.secrets.email);
631
631
  }
632
- const { createDataStore } = await import("./chunks/index-rut-KC39.mjs");
633
- return createDataStore(runtime, { secrets: opts.secrets ?? {}, passphrase: opts.passphrase ?? null, clientId: cliClientId() });
632
+ const { createDataStore } = await import("./chunks/index-DWecFbTi.mjs");
633
+ const firebaseCache = runtime.backend === "firebase" ? (await import("./chunks/firebaseCache-sK4jA0ru.mjs")).fileFirebaseCache() : void 0;
634
+ return createDataStore(runtime, { secrets: opts.secrets ?? {}, passphrase: opts.passphrase ?? null, clientId: cliClientId(), firebaseCache });
634
635
  }
635
636
  function siteSecrets(site) {
636
637
  const passphrase = site.credentials.passphrase ? requireCredential(site.credentials.passphrase).values.passphrase : null;
@@ -707,14 +708,14 @@ async function openSession(alias, opts = {}) {
707
708
  })();
708
709
  if (target) {
709
710
  const { setStorageConfigSource, registerStorageConnector } = await import("./chunks/registry-DijAKZ24.mjs").then((n) => n.r);
710
- const { buildConnector } = await import("./chunks/hosting-Dnn1k3cV.mjs");
711
+ const { buildConnector } = await import("./chunks/hosting-C_UDyMkm.mjs");
711
712
  if (target.connectorId === "local-folder") registerStorageConnector({ id: "local-folder", create: (s) => buildConnector({ connectorId: "local-folder", settings: s }) });
712
713
  setStorageConfigSource(async () => target);
713
714
  }
714
715
  }
715
716
  {
716
717
  const forms = await import("./chunks/forms-BfuWaQjA.mjs");
717
- const { buildConnector } = await import("./chunks/hosting-Dnn1k3cV.mjs");
718
+ const { buildConnector } = await import("./chunks/hosting-C_UDyMkm.mjs");
718
719
  forms.setInboxSourceResolver((_ctx, form) => {
719
720
  if (form.inbox.kind !== "s3") return null;
720
721
  const settings = form.inbox.settings ?? {};
@@ -1713,7 +1714,7 @@ function filePersistence(account, email) {
1713
1714
  };
1714
1715
  }
1715
1716
  async function prepareFirebaseSession(account, options, email) {
1716
- const { getFirebaseApp } = await import("./chunks/sdk-CYgNyuoK.mjs");
1717
+ const { getFirebaseApp } = await import("./chunks/sdk-NYUHv7WG.mjs");
1717
1718
  const { initializeAuth } = await import("./chunks/index-Dio0aBWA.mjs").then((n) => n.x);
1718
1719
  try {
1719
1720
  initializeAuth(getFirebaseApp(options), { persistence: filePersistence(account, email.trim().toLowerCase()) });
@@ -2595,7 +2596,7 @@ const formsCommand = {
2595
2596
  const target = s.hostingTarget();
2596
2597
  if (!target) usage(`${s.site.alias} has no hosting to publish the form to.`);
2597
2598
  if (options.yes !== true) return { needsConfirmation: true, plan: { form: id, path: built.path, hosting: s.site.hosting?.type, bytes: built.html.length } };
2598
- const { buildConnector } = await import("./chunks/hosting-Dnn1k3cV.mjs");
2599
+ const { buildConnector } = await import("./chunks/hosting-C_UDyMkm.mjs");
2599
2600
  const connector = buildConnector(target);
2600
2601
  const siteUrl = typeof s.site.hosting?.settings.siteUrl === "string" ? s.site.hosting.settings.siteUrl.replace(/\/+$/, "") : "";
2601
2602
  return forms.publishForm(s.ctx, id, { put: (input) => connector.put(input), publicUrl: async (path) => siteUrl ? `${siteUrl}/${path}` : connector.publicUrl(path) });
@@ -2740,7 +2741,7 @@ const deployAdminCommand = {
2740
2741
  const sites = options.all === true ? listSites().sites.filter((s) => s.hosting) : [resolveSite(strOpt(options, "site"))];
2741
2742
  if (!sites.length) throw new CliError("no-sites", "No registered CRM has a hosting.");
2742
2743
  installBrowserEnv();
2743
- const { adminOps, applyOps, buildConnector } = await import("./chunks/hosting-Dnn1k3cV.mjs");
2744
+ const { adminOps, applyOps, buildConnector } = await import("./chunks/hosting-C_UDyMkm.mjs");
2744
2745
  const { hostingTargetOf: hostingTargetOf2 } = await Promise.resolve().then(() => session);
2745
2746
  const plans = [];
2746
2747
  for (const site of sites) {
@@ -2844,7 +2845,7 @@ async function runDoctor(site, opts = {}) {
2844
2845
  installBrowserEnv();
2845
2846
  try {
2846
2847
  if (site.runtime.backend === "journal" && site.runtime.journal.connectorId === "git-pages") {
2847
- const { probeTarget } = await import("./chunks/install-BBTA4nMm.mjs");
2848
+ const { probeTarget } = await import("./chunks/install-BLNflFes.mjs");
2848
2849
  const { dataTargetOf: dataTargetOf2 } = await Promise.resolve().then(() => session);
2849
2850
  const probe = await probeTarget(dataTargetOf2(site), { requireSiteUrl: false, requirePrivate: true });
2850
2851
  add(probe.ok ? { id: "data-repo", status: "ok", message: probe.detail } : { id: "data-repo", status: "fail", message: probe.detail, fix: "Make the data repository private and check the token's Contents permission." });
@@ -2854,6 +2855,15 @@ async function runDoctor(site, opts = {}) {
2854
2855
  try {
2855
2856
  const store = session$1.store;
2856
2857
  add({ id: "data-access", status: "ok", message: `Dataset opened (${store.kind}${session$1.user ? `, acting as ${session$1.user.email} (${session$1.user.role})` : ""}).` });
2858
+ const { isFirebaseStore } = await import("./chunks/firebase-Bknru2pE.mjs");
2859
+ const load2 = isFirebaseStore(store) ? store.lastLoad() : null;
2860
+ if (load2) {
2861
+ add({
2862
+ id: "firestore-reads",
2863
+ status: "ok",
2864
+ message: load2.mode === "incremental" ? `Loaded from the local cache + changes: ${load2.reads} Firestore reads.` : `Full read: ${load2.reads} Firestore reads (the next commands read only what changed).`
2865
+ });
2866
+ }
2857
2867
  const { usersOf, objectsOf, pipelinesOf, settingsDoc, recordsOf } = await import("./chunks/store-5gqRSKXg.mjs");
2858
2868
  const missing = [];
2859
2869
  for (const id of ["companies", "people", "deals"]) if (!objectsOf(store).some((o) => o.id === id)) missing.push(`object ${id}`);
@@ -3004,7 +3014,7 @@ const installCommand = {
3004
3014
  const { resolvePlan } = await import("./chunks/plan-BmTat6-T.mjs");
3005
3015
  const plan = resolvePlan(readPlan(planPath));
3006
3016
  if (getSite(plan.alias)) throw new CliError("site-exists", `A site named "${plan.alias}" is already registered.`, "Use another alias, or `agentcrm site forget` it first.");
3007
- const { checkPlan, runInstall } = await import("./chunks/install-BBTA4nMm.mjs");
3017
+ const { checkPlan, runInstall } = await import("./chunks/install-BLNflFes.mjs");
3008
3018
  io.err("Checking access…");
3009
3019
  const checks = await checkPlan(plan);
3010
3020
  const summary = {
@@ -3360,7 +3370,7 @@ const optionsCommand = {
3360
3370
  async run({ io, options }) {
3361
3371
  const rulesEmail = strOpt(options, "firebase-rules");
3362
3372
  if (rulesEmail) {
3363
- const { buildFirestoreRules } = await import("./chunks/firebaseRules-iori-s7U.mjs");
3373
+ const { buildFirestoreRules } = await import("./chunks/firebaseRules-Jq85MBBN.mjs");
3364
3374
  const rules = buildFirestoreRules({ adminEmail: rulesEmail });
3365
3375
  printResult(io, { email: rulesEmail, rules, where: "Firebase console → Firestore Database → Rules → paste → Publish (or `firebase deploy --only firestore:rules`)." }, () => rules);
3366
3376
  return;
@@ -4102,14 +4112,17 @@ export {
4102
4112
  findHosting as e,
4103
4113
  findBackend as f,
4104
4114
  getCredential as g,
4105
- openStore as h,
4106
- installBrowserEnv as i,
4107
- distRoot as j,
4115
+ readJsonFile as h,
4116
+ homePath as i,
4117
+ openStore as j,
4118
+ installBrowserEnv as k,
4119
+ distRoot as l,
4108
4120
  nodeImportBundle as n,
4109
4121
  ownPackageRoot as o,
4110
4122
  packageRoot as p,
4111
4123
  readVersionInfo as r,
4112
4124
  setCredential as s,
4113
4125
  toCliError as t,
4114
- validateAlias as v
4126
+ validateAlias as v,
4127
+ writeJsonFile as w
4115
4128
  };
@@ -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
+ };
@@ -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()]) {
@@ -259,10 +320,21 @@ function createFirebaseStoreWith(opts) {
259
320
  return null;
260
321
  }
261
322
  }
323
+ function getIdentity() {
324
+ if (!identityPromise) {
325
+ identityPromise = resolveIdentity().then((id) => {
326
+ currentIdentity = id;
327
+ if (!id) identityPromise = null;
328
+ return id;
329
+ });
330
+ }
331
+ return identityPromise;
332
+ }
262
333
  async function persist(mutations, before, after) {
263
334
  const ops = mutationOps(mutations, before);
335
+ if (!currentIdentity && mutations.some((m) => m.collection === "users")) await getIdentity();
264
336
  if (currentIdentity?.role === "admin") ops.push(...inviteOps(mutations, (id) => before.users.get(id) ?? rawUsers.get(id), after));
265
- for (const part of chunk(ops, FIRESTORE_BATCH_LIMIT)) await api.commit(part);
337
+ for (const part of chunkForCommit(ops)) await api.commit(part);
266
338
  for (const m of mutations) {
267
339
  if (m.collection !== "users") continue;
268
340
  const id = m.op === "set" ? m.doc.id : m.id;
@@ -271,63 +343,145 @@ function createFirebaseStoreWith(opts) {
271
343
  else rawUsers.delete(id);
272
344
  }
273
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
+ }
274
397
  const core = createStoreCore({
275
398
  kind: "firebase",
276
399
  async load() {
277
- const raw = /* @__PURE__ */ new Map();
278
- let chunks = [];
279
- await Promise.all([...COLLECTIONS.map(async (c) => raw.set(c, await readCollection(c))), readCollection(EXTENSION_CHUNKS_COLLECTION).then((d) => chunks = d)]);
280
- 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();
281
431
  scheduleReconcile();
282
432
  return snap;
283
433
  },
284
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).
285
437
  watch(onRemote) {
286
- const raw = /* @__PURE__ */ new Map();
287
- let chunks = null;
438
+ let offs = [];
439
+ let stopped = false;
288
440
  const emit = () => {
289
- if (raw.size < COLLECTIONS.length || chunks === null) return;
290
- onRemote(buildSnapshot(raw, chunks));
441
+ onRemote(snapshotFromDocsMap());
291
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());
292
482
  };
293
- const offs = COLLECTIONS.map(
294
- (c) => api.watchCollection(
295
- c,
296
- (docs) => {
297
- raw.set(c, docs);
298
- emit();
299
- },
300
- (err) => {
301
- if (isPermissionDenied(err)) raw.set(c, []);
302
- else console.warn(`[firebase] live updates of ${c} stopped:`, err);
303
- }
304
- )
305
- );
306
- offs.push(
307
- api.watchCollection(
308
- EXTENSION_CHUNKS_COLLECTION,
309
- (docs) => {
310
- chunks = docs;
311
- emit();
312
- },
313
- (err) => {
314
- if (isPermissionDenied(err)) chunks = [];
315
- else console.warn("[firebase] live updates of extension chunks stopped:", err);
316
- }
317
- )
318
- );
319
- return () => offs.forEach((off) => off());
320
- },
321
- identity() {
322
- if (!identityPromise) {
323
- identityPromise = resolveIdentity().then((id) => {
324
- currentIdentity = id;
325
- if (!id) identityPromise = null;
326
- return id;
327
- });
328
- }
329
- return identityPromise;
330
483
  },
484
+ identity: getIdentity,
331
485
  close() {
332
486
  offAuth();
333
487
  }
@@ -381,9 +535,23 @@ function createFirebaseStoreWith(opts) {
381
535
  console.warn("[firebase] could not link invited users:", err);
382
536
  }
383
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
+ };
384
547
  withFileStore(core, firebaseFileStore(api));
548
+ activeCacheClear = async () => {
549
+ if (opts.cache && cacheKey) await opts.cache.clear(cacheKey);
550
+ };
385
551
  return Object.assign(core, {
386
552
  api,
553
+ readCount: () => api.readCount(),
554
+ lastLoad: () => loadInfo,
387
555
  accessError: () => lastAccessError,
388
556
  resetSession() {
389
557
  identityPromise = null;
@@ -397,15 +565,17 @@ function firebaseApiFor(config) {
397
565
  const key = config.firebase.projectId ?? "default";
398
566
  let api = apis.get(key);
399
567
  if (!api) {
400
- api = import("./sdk-CYgNyuoK.mjs").then((m) => m.createSdkFirebaseApi(config.firebase));
568
+ api = import("./sdk-NYUHv7WG.mjs").then((m) => m.createSdkFirebaseApi(config.firebase));
401
569
  apis.set(key, api);
402
570
  api.catch(() => apis.delete(key));
403
571
  }
404
572
  return api;
405
573
  }
406
574
  let activeApi = null;
575
+ let activeCacheClear = null;
407
576
  registerAuthAdapter("firebase", {
408
577
  async signOut() {
578
+ await activeCacheClear?.();
409
579
  await activeApi?.auth.signOut();
410
580
  }
411
581
  });
@@ -449,21 +619,26 @@ async function createFirebaseStore(config, opts = {}) {
449
619
  const api = await firebaseApiFor(cfg);
450
620
  activeApi = api;
451
621
  await signInWithSecrets(api, opts.secrets);
452
- return createFirebaseStoreWith({ api });
622
+ const cache = opts.firebaseCache === void 0 ? indexedDbFirebaseCache() : opts.firebaseCache;
623
+ return createFirebaseStoreWith({ api, cache, projectId: cfg.firebase.projectId });
453
624
  }
454
625
  export {
455
626
  DELETE_FIELD,
627
+ DELETIONS_COLLECTION,
456
628
  EXTENSION_CHUNKS_COLLECTION,
457
629
  EXTENSION_CHUNK_CHARS,
458
- FIRESTORE_BATCH_LIMIT,
630
+ F as FIRESTORE_BATCH_LIMIT,
459
631
  FirebaseAccessError,
460
632
  INBOX_COLLECTION,
461
633
  INVITES_COLLECTION,
634
+ S as SERVER_TIMESTAMP,
462
635
  STORAGE_FILES_DIR,
463
636
  STORAGE_NOT_ENABLED,
637
+ T as TS_FIELD,
464
638
  assembleExtensionDocs,
465
639
  checkFirebaseAccess,
466
- chunk,
640
+ b as chunk,
641
+ chunkForCommit,
467
642
  createFirebaseStore,
468
643
  createFirebaseStoreWith,
469
644
  emailKey,
@@ -474,5 +649,9 @@ export {
474
649
  isFirebaseStore,
475
650
  isPermissionDenied,
476
651
  mutationOps,
477
- signInWithSecrets
652
+ setTrackedCollections,
653
+ signInWithSecrets,
654
+ d as splitTs,
655
+ t as tombstoneId,
656
+ w as withChangeTracking
478
657
  };