@fleox-agent-crm/agentcrm 0.3.3 → 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 +13 -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 +29 -16
  12. package/dist/agent/chunks/api-BEl_EKwf.mjs +79 -0
  13. package/dist/agent/chunks/{firebase-DWR0K1OC.mjs → firebase-Bknru2pE.mjs} +230 -53
  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-DtCezG4R.mjs → index-DWecFbTi.mjs} +1 -1
  18. package/dist/agent/chunks/{install-Bp-CwDVC.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-BG1GpyBB.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-DLy5RpPl.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.3";
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-DtCezG4R.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-Bp-CwDVC.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-Bp-CwDVC.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()]) {
@@ -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
+ };