@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.
- package/README.md +16 -16
- package/changelog.json +13 -0
- package/dist/admin/admin-files.json +7 -7
- package/dist/admin/assets/{chartsImpl-aBuZ1X4W.js → chartsImpl-zjsXDhuy.js} +1 -1
- package/dist/admin/assets/firebase-C3h9mF8O.js +2 -0
- package/dist/admin/assets/{gitJson-DzX3eZ2P.js → gitJson-nrOYgX0x.js} +1 -1
- package/dist/admin/assets/{index-DLy5RpPl.js → index-0ejjkW0z.js} +6 -6
- package/dist/admin/assets/{lucide-react.min-BgcmukkH.js → lucide-react.min-CI5dCsSm.js} +1 -1
- package/dist/admin/assets/{sdk-DtP9zqdi.js → sdk-mepKXtVl.js} +135 -120
- package/dist/admin/index.html +1 -1
- package/dist/agent/agentcrm.mjs +29 -16
- package/dist/agent/chunks/api-BEl_EKwf.mjs +79 -0
- package/dist/agent/chunks/{firebase-DWR0K1OC.mjs → firebase-Bknru2pE.mjs} +230 -53
- package/dist/agent/chunks/firebaseCache-sK4jA0ru.mjs +39 -0
- package/dist/agent/chunks/{firebaseRules-iori-s7U.mjs → firebaseRules-Jq85MBBN.mjs} +14 -3
- package/dist/agent/chunks/{hosting-Dnn1k3cV.mjs → hosting-C_UDyMkm.mjs} +1 -1
- package/dist/agent/chunks/{index-DtCezG4R.mjs → index-DWecFbTi.mjs} +1 -1
- package/dist/agent/chunks/{install-Bp-CwDVC.mjs → install-BLNflFes.mjs} +3 -3
- package/dist/agent/chunks/{sdk-CYgNyuoK.mjs → sdk-NYUHv7WG.mjs} +493 -214
- package/dist/version.json +1 -1
- package/package.json +1 -1
- package/plugin/.claude-plugin/plugin.json +1 -1
- package/plugin/skills/agentcrm-admin/SKILL.md +67 -67
- package/plugin/skills/agentcrm-admin/references/records.md +5 -5
- package/plugin/skills/agentcrm-admin/references/schema.md +19 -19
- package/plugin/skills/agentcrm-extensions/SKILL.md +8 -8
- package/plugin/skills/agentcrm-extensions/references/module-recipes.md +2 -2
- package/plugin/skills/agentcrm-extensions/references/testing.md +14 -14
- package/plugin/skills/agentcrm-extensions/references/theme-recipes.md +3 -3
- package/plugin/skills/agentcrm-install/SKILL.md +13 -13
- package/plugin/skills/agentcrm-install/references/firebase.md +1 -1
- package/plugin/skills/agentcrm-install/references/github.md +1 -1
- package/plugin/skills/agentcrm-install/references/mcp.md +2 -2
- package/plugin/skills/agentcrm-install/references/plans.md +1 -1
- package/plugin/skills/agentcrm-install/references/s3.md +1 -1
- package/plugin/skills/agentcrm-manage/SKILL.md +15 -15
- package/plugin/skills/agentcrm-sales/SKILL.md +18 -18
- package/dist/admin/assets/firebase-BG1GpyBB.js +0 -2
- package/dist/agent/chunks/api-Dwb4xz1k.mjs +0 -27
package/dist/admin/index.html
CHANGED
|
@@ -120,7 +120,7 @@
|
|
|
120
120
|
}
|
|
121
121
|
}
|
|
122
122
|
</script>
|
|
123
|
-
<script type="module" crossorigin src="./assets/index-
|
|
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>
|
package/dist/agent/agentcrm.mjs
CHANGED
|
@@ -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.
|
|
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-
|
|
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-
|
|
633
|
-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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
|
-
|
|
4106
|
-
|
|
4107
|
-
|
|
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
|
|
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(
|
|
41
|
-
return normalizeDoc({ ...
|
|
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((
|
|
107
|
-
const counts =
|
|
108
|
-
if (!counts || typeof counts !== "object") return
|
|
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(
|
|
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,
|
|
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 } =
|
|
117
|
-
return { id:
|
|
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
|
|
191
|
-
for (const
|
|
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
|
|
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
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
const
|
|
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
|
-
|
|
298
|
-
let
|
|
438
|
+
let offs = [];
|
|
439
|
+
let stopped = false;
|
|
299
440
|
const emit = () => {
|
|
300
|
-
|
|
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-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
+
};
|