@fleox-agent-crm/agentcrm 0.3.0 → 0.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (65) hide show
  1. package/README.md +16 -16
  2. package/changelog.json +19 -0
  3. package/dist/admin/admin-files.json +8 -8
  4. package/dist/admin/assets/{chartsImpl-CE2akJat.js → chartsImpl-B45AAqnr.js} +30 -30
  5. package/dist/admin/assets/{firebase-BHSUggRo.js → firebase-C1x_l9P9.js} +2 -2
  6. package/dist/admin/assets/{gitJson-BulGh2KV.js → gitJson-CAUSxl8d.js} +1 -1
  7. package/dist/admin/assets/{index-DidVVK7L.js → index-ChFauO8y.js} +265 -265
  8. package/dist/admin/assets/{index-hMXwHSvN.css → index-TcjySLVg.css} +1 -1
  9. package/dist/admin/assets/{lucide-react.min-DEDlHCPj.js → lucide-react.min--rznIkmQ.js} +1 -1
  10. package/dist/admin/assets/{sdk-CnrLnlRo.js → sdk-DgzLACmL.js} +1 -1
  11. package/dist/admin/index.html +2 -2
  12. package/dist/agent/agentcrm.mjs +267 -142
  13. package/dist/agent/chunks/{api-B6OqNVWp.mjs → api-Cz0Ubj-B.mjs} +4 -3
  14. package/dist/agent/chunks/{appearance-BaShelzt.mjs → appearance-BloHbiLA.mjs} +1 -1
  15. package/dist/agent/chunks/{automations-BWrvLqP6.mjs → automations-DwWruxmu.mjs} +3 -3
  16. package/dist/agent/chunks/{backup-ClSmxmFZ.mjs → backup-W3CjgkAJ.mjs} +16 -3
  17. package/dist/agent/chunks/{compact-CsFPaAAq.mjs → compact-BGHI92tm.mjs} +2 -2
  18. package/dist/agent/chunks/{exportCsv-B5hRvkNm.mjs → exportCsv-C6e6XEmB.mjs} +38 -3
  19. package/dist/agent/chunks/{extCheck--7lmVvl7.mjs → extCheck-GgHG1dYB.mjs} +16 -9
  20. package/dist/agent/chunks/{extPackage-BGl5Oqs8.mjs → extPackage-DuZ6GZhp.mjs} +2 -2
  21. package/dist/agent/chunks/{extScaffold-DqxI7R1e.mjs → extScaffold-Aqyjf1nr.mjs} +3 -3
  22. package/dist/agent/chunks/{extensions-BP1k60md.mjs → extensions-Bp72uZ9a.mjs} +19 -6
  23. package/dist/agent/chunks/{featureRegistry-C9nbVShp.mjs → featureRegistry-DSsZbhrh.mjs} +3 -3
  24. package/dist/agent/chunks/{files-q8Xu5bft.mjs → files-D1daDgY0.mjs} +1 -1
  25. package/dist/agent/chunks/{forms-BKU5F9WF.mjs → forms-BfuWaQjA.mjs} +1 -1
  26. package/dist/agent/chunks/{importCsv-DmE_sBch.mjs → importCsv-LVmMTaWU.mjs} +2 -2
  27. package/dist/agent/chunks/{install-CpHoUFtL.mjs → install-CbpD_7gd.mjs} +3 -3
  28. package/dist/agent/chunks/{loader-C9nm3iwx.mjs → loader-MPD9iAvh.mjs} +20 -20
  29. package/dist/agent/chunks/{modules-CqdVQVJb.mjs → modules-clWdE-K2.mjs} +27 -2
  30. package/dist/agent/chunks/{nodeImport-BPCt5uMu.mjs → nodeImport-ylfRDvqt.mjs} +2 -2
  31. package/dist/agent/chunks/{package-DWbn3t2R.mjs → package-B7mI6HGi.mjs} +1 -1
  32. package/dist/agent/chunks/{papaparse-DjBYnQM2.mjs → papaparse-Df7i1dHr.mjs} +31 -7
  33. package/dist/agent/chunks/{pipelines-DCdRBNBV.mjs → pipelines-BAKpPk1w.mjs} +1 -1
  34. package/dist/agent/chunks/{plan-C0TBrU92.mjs → plan-BmTat6-T.mjs} +1 -1
  35. package/dist/agent/chunks/{records-DoqiT9xR.mjs → records-DcHFelq5.mjs} +5 -1
  36. package/dist/agent/chunks/{registry-Ceexawxl.mjs → registry-COfJrSmg.mjs} +18 -18
  37. package/dist/agent/chunks/{reports-BB59N6ke.mjs → reports-BACHGccj.mjs} +3 -3
  38. package/dist/agent/chunks/{runtime-DHo7ZEEO.mjs → runtime-C0Xf0Axi.mjs} +53 -32
  39. package/dist/agent/chunks/{schema-DlHt2PxI.mjs → schema-C4lSslXv.mjs} +10 -4
  40. package/dist/agent/chunks/{schema-7-4-qtSS.mjs → schema-DnLj8dUO.mjs} +10 -0
  41. package/dist/agent/chunks/{seed-CugPGYZL.mjs → seed-6MRmQQBR.mjs} +26 -8
  42. package/dist/agent/chunks/{server-ISwlDRCN.mjs → server-DC3t8r30.mjs} +5 -5
  43. package/dist/agent/chunks/{settings-BtvR_jdm.mjs → settings-Di8nzNEL.mjs} +35 -0
  44. package/dist/agent/chunks/{stats-CfeJssSS.mjs → stats-BfuLWhVV.mjs} +2 -2
  45. package/dist/agent/chunks/{tools-Bew8lxKx.mjs → tools-iSH-yMpl.mjs} +59 -19
  46. package/dist/agent/chunks/{views-fO_BM5U_.mjs → views-DH1n9Lrt.mjs} +1 -1
  47. package/dist/version.json +1 -1
  48. package/package.json +1 -1
  49. package/plugin/.claude-plugin/plugin.json +1 -1
  50. package/plugin/skills/agentcrm-admin/SKILL.md +67 -67
  51. package/plugin/skills/agentcrm-admin/references/appearance.md +1 -1
  52. package/plugin/skills/agentcrm-admin/references/records.md +5 -5
  53. package/plugin/skills/agentcrm-admin/references/schema.md +20 -20
  54. package/plugin/skills/agentcrm-extensions/SKILL.md +9 -9
  55. package/plugin/skills/agentcrm-extensions/references/module-recipes.md +2 -2
  56. package/plugin/skills/agentcrm-extensions/references/testing.md +16 -14
  57. package/plugin/skills/agentcrm-extensions/references/theme-recipes.md +3 -3
  58. package/plugin/skills/agentcrm-install/SKILL.md +13 -13
  59. package/plugin/skills/agentcrm-install/references/firebase.md +1 -1
  60. package/plugin/skills/agentcrm-install/references/github.md +1 -1
  61. package/plugin/skills/agentcrm-install/references/mcp.md +3 -3
  62. package/plugin/skills/agentcrm-install/references/plans.md +1 -1
  63. package/plugin/skills/agentcrm-install/references/s3.md +1 -1
  64. package/plugin/skills/agentcrm-manage/SKILL.md +15 -15
  65. package/plugin/skills/agentcrm-sales/SKILL.md +18 -18
@@ -5,7 +5,7 @@ import { dirname as __acrmDirname } from "node:path";
5
5
  const require2 = __acrmRequire(import.meta.url);
6
6
  const __filename = __acrmPath(import.meta.url);
7
7
  const __dirname = __acrmDirname(__filename);
8
- import { parseArgs } from "node:util";
8
+ import { format, parseArgs } from "node:util";
9
9
  import { existsSync, readFileSync, statSync, mkdirSync, writeFileSync, chmodSync, renameSync, openSync, writeSync, closeSync, unlinkSync, mkdtempSync, readdirSync, createReadStream } from "node:fs";
10
10
  import { join, resolve, dirname, basename, extname, sep, normalize, isAbsolute } from "node:path";
11
11
  import { fileURLToPath } from "node:url";
@@ -54,6 +54,13 @@ class CliError extends Error {
54
54
  this.exitCode = exitCode;
55
55
  }
56
56
  }
57
+ function routeConsoleToStderr(target = console, write = (t) => void process.stderr.write(t)) {
58
+ const toStderr = (...args) => write(`${format(...args)}
59
+ `);
60
+ target.log = toStderr;
61
+ target.info = toStderr;
62
+ target.debug = toStderr;
63
+ }
57
64
  function createIo(json) {
58
65
  return {
59
66
  json,
@@ -68,10 +75,20 @@ function printResult(io, data, human) {
68
75
  const OPS_HINTS = {
69
76
  forbidden: "The CRM user the CLI acts as lacks this permission (see `site show` → user, `users list`). Ask an admin, or re-register the site with an admin user (`site add … --user <email>`).",
70
77
  notFound: "Check the id (list commands print them: `records list --object …`, `objects list`, `views list`…).",
71
- validation: "Fix the input: `objects get <object>` lists field ids, types and select options.",
78
+ validation: "Fix the input and retry (`agentcrm help <command>` lists the options; for record fields, `objects get <object>` lists field ids, types and select options).",
72
79
  conflict: "Something with the same key already exists — list it first, then update instead of creating.",
73
80
  invalid: "Check the arguments with `agentcrm help <command>`."
74
81
  };
82
+ function dependencyHint(details) {
83
+ const ids = (key) => Array.isArray(details?.[key]) ? details[key].map(String) : [];
84
+ const blockers = ids("blockers");
85
+ if (blockers.length) return `Blocked by: ${blockers.join(", ")}. Re-run with --cascade (AI tools: "cascade": true) to change ${blockers.length > 1 ? "them" : "it"} too — show the user the plan's \`changed\` list first.`;
86
+ const dependents = ids("dependents");
87
+ if (dependents.length) return `Disable or uninstall ${dependents.join(", ")} first (\`ext disable <id>\` / \`ext uninstall <id>\`), or pass --force to \`ext uninstall\`.`;
88
+ const missing = ids("missing");
89
+ if (missing.length) return `Install the missing module(s) first: ${missing.join(", ")} (\`ext install <zip>\`).`;
90
+ return null;
91
+ }
75
92
  function knownError(error) {
76
93
  if (!(error instanceof Error)) return null;
77
94
  if (error.name === "PassphraseError") {
@@ -84,7 +101,8 @@ function knownError(error) {
84
101
  }
85
102
  if (error.name === "OpsError") {
86
103
  const code = error.code ?? "invalid";
87
- return new CliError(code, error.message, OPS_HINTS[code], code === "validation" || code === "invalid" ? 2 : 1);
104
+ const details = error.details;
105
+ return new CliError(code, error.message, code === "conflict" && dependencyHint(details) || OPS_HINTS[code], code === "validation" || code === "invalid" ? 2 : 1);
88
106
  }
89
107
  if (error.name === "FieldValidationError") {
90
108
  return new CliError("validation", error.message, OPS_HINTS.validation, 2);
@@ -120,10 +138,11 @@ const output = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProper
120
138
  knownError,
121
139
  printError,
122
140
  printResult,
141
+ routeConsoleToStderr,
123
142
  toCliError
124
143
  }, Symbol.toStringTag, { value: "Module" }));
125
144
  const name = "@fleox-agent-crm/agentcrm";
126
- const version = "0.3.0";
145
+ const version = "0.3.1";
127
146
  const type = "module";
128
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.";
129
148
  const license = "SEE LICENSE IN LICENSE";
@@ -505,8 +524,8 @@ function tmpFolder() {
505
524
  return tmpBase;
506
525
  }
507
526
  async function nodeImportBundle() {
508
- await import("./chunks/runtime-DHo7ZEEO.mjs");
509
- const { createNodeImportBundle } = await import("./chunks/nodeImport-BPCt5uMu.mjs");
527
+ await import("./chunks/runtime-C0Xf0Axi.mjs");
528
+ const { createNodeImportBundle } = await import("./chunks/nodeImport-ylfRDvqt.mjs");
510
529
  return createNodeImportBundle({ runtimeDir: runtimeDir(), tmpDir: tmpFolder() });
511
530
  }
512
531
  function hasEnabledExtensions(store) {
@@ -520,15 +539,9 @@ async function loadSessionExtensions(store, opts = {}) {
520
539
  if (!loadedAny && !opts.force && !hasEnabledExtensions(store)) return null;
521
540
  try {
522
541
  const importBundle = await nodeImportBundle();
523
- const { syncExtensions } = await import("./chunks/loader-C9nm3iwx.mjs");
542
+ const { syncExtensions } = await import("./chunks/loader-MPD9iAvh.mjs");
524
543
  loadedAny = true;
525
- const original = console.error;
526
- console.error = () => void 0;
527
- try {
528
- return await syncExtensions(store, { importBundle });
529
- } finally {
530
- console.error = original;
531
- }
544
+ return await syncExtensions(store, { importBundle, onError: () => void 0 });
532
545
  } catch (err) {
533
546
  return { loaded: [], unloaded: [], failed: [{ id: "*", error: err.message }], skipped: [] };
534
547
  }
@@ -540,7 +553,7 @@ async function loadSessionExtensions(store, opts = {}) {
540
553
  sync,
541
554
  async close() {
542
555
  if (!loadedAny) return;
543
- (await import("./chunks/registry-Ceexawxl.mjs")).deactivateAllExtensions();
556
+ (await import("./chunks/registry-COfJrSmg.mjs")).deactivateAllExtensions();
544
557
  }
545
558
  };
546
559
  }
@@ -663,7 +676,7 @@ async function openSession(alias, opts = {}) {
663
676
  const [{ createOpsContext }, { createPluginRegistry, plugins: defaultPlugins }, { installAutomations }] = await Promise.all([
664
677
  import("./chunks/context-CxFfqS4D.mjs"),
665
678
  import("./chunks/pluginRegistry-DyHtlkcr.mjs"),
666
- import("./chunks/automations-BWrvLqP6.mjs")
679
+ import("./chunks/automations-DwWruxmu.mjs")
667
680
  ]);
668
681
  const own = createPluginRegistry((hook, err) => process.stderr.write(`[agentcrm] ${hook} handler failed: ${String(err?.message ?? err)}
669
682
  `));
@@ -696,7 +709,7 @@ async function openSession(alias, opts = {}) {
696
709
  }
697
710
  }
698
711
  {
699
- const forms = await import("./chunks/forms-BKU5F9WF.mjs");
712
+ const forms = await import("./chunks/forms-BfuWaQjA.mjs");
700
713
  const { buildConnector } = await import("./chunks/hosting-Bme7LxPc.mjs");
701
714
  forms.setInboxSourceResolver((_ctx, form) => {
702
715
  if (form.inbox.kind !== "s3") return null;
@@ -768,6 +781,12 @@ const session = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.definePrope
768
781
  openStore,
769
782
  withSession
770
783
  }, Symbol.toStringTag, { value: "Module" }));
784
+ function writeOutFile(path, data) {
785
+ const abs = resolve(path);
786
+ mkdirSync(dirname(abs), { recursive: true });
787
+ writeFileSync(abs, data);
788
+ return abs;
789
+ }
771
790
  function usage(message, hint) {
772
791
  throw new CliError("usage", message, hint, 2);
773
792
  }
@@ -912,7 +931,7 @@ function readLocalFile(path) {
912
931
  return { name: basename(full), bytes: new Uint8Array(readFileSync(full)) };
913
932
  }
914
933
  async function uploadLocalFiles(ctx, paths, targets2 = []) {
915
- const { uploadFile } = await import("./chunks/files-q8Xu5bft.mjs");
934
+ const { uploadFile } = await import("./chunks/files-D1daDgY0.mjs");
916
935
  const { guessMime, imageDimensions } = await import("./chunks/files-CjsPJSR6.mjs");
917
936
  const out = [];
918
937
  for (const p of paths) {
@@ -960,7 +979,7 @@ const filesCommand = {
960
979
  switch (action) {
961
980
  case "list":
962
981
  return withSession(io, options, {}, async ({ ctx }) => {
963
- const { listFiles } = await import("./chunks/files-q8Xu5bft.mjs");
982
+ const { listFiles } = await import("./chunks/files-D1daDgY0.mjs");
964
983
  const target = strOpt(options, "record") ?? strOpt(options, "activity") ?? null;
965
984
  const items = listFiles(ctx, target, { includeRelated: boolOpt(options, "related"), kind: strOpt(options, "kind"), search: strOpt(options, "search") });
966
985
  const limit = Math.min(1e3, numOpt(options, "limit") ?? 100);
@@ -975,7 +994,7 @@ ${table(r.files, ["id", "name", "mime", "size", "createdAt"])}`);
975
994
  if (record && activity) usage("Give --record or --activity, not both.");
976
995
  for (const p of args) readLocalFile(p);
977
996
  return withSession(io, options, { lock: true }, async ({ ctx }) => {
978
- const { attachFile } = await import("./chunks/files-q8Xu5bft.mjs");
997
+ const { attachFile } = await import("./chunks/files-D1daDgY0.mjs");
979
998
  const files2 = await uploadLocalFiles(ctx, args, record ? [record] : []);
980
999
  if (activity) for (const f of files2) await attachFile(ctx, f.id, { kind: "activity", id: activity });
981
1000
  const { requireFile } = await import("./chunks/store-5gqRSKXg.mjs");
@@ -988,17 +1007,17 @@ ${table(r.uploaded, ["id", "name", "size"])}`);
988
1007
  const out = strOpt(options, "out");
989
1008
  if (!out) usage("files get needs --out <path>.");
990
1009
  return withSession(io, options, {}, async ({ ctx }) => {
991
- const { getFile, getFileBytes } = await import("./chunks/files-q8Xu5bft.mjs");
1010
+ const { getFile, getFileBytes } = await import("./chunks/files-D1daDgY0.mjs");
992
1011
  const meta = getFile(ctx, id);
993
1012
  const bytes = await getFileBytes(ctx, id, { thumb: boolOpt(options, "thumb") });
994
- writeFileSync(resolve(out), bytes);
1013
+ writeOutFile(out, bytes);
995
1014
  return { id, name: meta.name, mime: meta.mime, bytes: bytes.length, path: resolve(out) };
996
1015
  }, (r) => `Wrote ${r.bytes} bytes (${r.name}) to ${r.path}`);
997
1016
  }
998
1017
  case "delete": {
999
1018
  const id = positional(args, 0, "file id", U$1.delete);
1000
1019
  return withSession(io, options, { lock: options.yes === true }, async ({ ctx }) => {
1001
- const { getFile, deleteFile } = await import("./chunks/files-q8Xu5bft.mjs");
1020
+ const { getFile, deleteFile } = await import("./chunks/files-D1daDgY0.mjs");
1002
1021
  const meta = fileOut(getFile(ctx, id));
1003
1022
  if (options.yes !== true) return { needsConfirmation: true, plan: { delete: meta } };
1004
1023
  await deleteFile(ctx, id);
@@ -1006,7 +1025,7 @@ ${table(r.uploaded, ["id", "name", "size"])}`);
1006
1025
  }, (r) => "plan" in r && r.plan ? `Would delete ${r.plan.delete.name} (${r.plan.delete.id}). Re-run with --yes once the user agrees.` : `Deleted ${"deleted" in r ? r.deleted : id} (bytes purged by \`agentcrm compact\`).`);
1007
1026
  }
1008
1027
  case "usage":
1009
- return withSession(io, options, {}, async ({ ctx }) => (await import("./chunks/files-q8Xu5bft.mjs")).fileUsage(ctx));
1028
+ return withSession(io, options, {}, async ({ ctx }) => (await import("./chunks/files-D1daDgY0.mjs")).fileUsage(ctx));
1010
1029
  default:
1011
1030
  usage(`Unknown files action "${action ?? ""}".`, "Use list, upload, get, delete or usage.");
1012
1031
  }
@@ -1019,7 +1038,7 @@ async function recordImageAction(io, options, args) {
1019
1038
  if (!remove && !path) usage("records image needs an image path (or --remove).");
1020
1039
  const local = remove ? null : readLocalFile(path);
1021
1040
  return withSession(io, options, { lock: true }, async ({ ctx }) => {
1022
- const { setRecordImage, clearRecordImage } = await import("./chunks/files-q8Xu5bft.mjs");
1041
+ const { setRecordImage, clearRecordImage } = await import("./chunks/files-D1daDgY0.mjs");
1023
1042
  const field = strOpt(options, "field");
1024
1043
  if (!local) {
1025
1044
  const r = await clearRecordImage(ctx, recordId, field);
@@ -1307,7 +1326,7 @@ const recordsCommand = {
1307
1326
  const filters = parseFilters(multiOpt(options, "filter"), FILTER_OPS);
1308
1327
  const sorts = parseSorts(multiOpt(options, "sort"));
1309
1328
  return withSession(io, options, {}, async ({ ctx }) => {
1310
- const { listRecords } = await import("./chunks/records-DoqiT9xR.mjs");
1329
+ const { listRecords } = await import("./chunks/records-DcHFelq5.mjs");
1311
1330
  const page = listRecords(ctx, objectId, {
1312
1331
  filters,
1313
1332
  sorts,
@@ -1325,7 +1344,7 @@ ${table(r.records, ["id", "label", "updatedAt"])}`);
1325
1344
  case "get": {
1326
1345
  const id = positional(args, 0, "record id", U.get);
1327
1346
  return withSession(io, options, {}, async ({ ctx }) => {
1328
- const { getRecordDetails } = await import("./chunks/records-DoqiT9xR.mjs");
1347
+ const { getRecordDetails } = await import("./chunks/records-DcHFelq5.mjs");
1329
1348
  const d = getRecordDetails(ctx, id);
1330
1349
  return { ...await recordOut(ctx, d.record), relations: d.relations, related: d.inverse.filter((i) => i.records.length), activityCount: d.activityCount };
1331
1350
  });
@@ -1336,7 +1355,7 @@ ${table(r.records, ["id", "label", "updatedAt"])}`);
1336
1355
  if (!Object.keys(fields).length) usage("records create needs --fields or --set.", `Usage: agentcrm ${U.create}`);
1337
1356
  const owner = strOpt(options, "owner");
1338
1357
  return withSession(io, options, { lock: true }, async ({ ctx }) => {
1339
- const { createRecord } = await import("./chunks/records-DoqiT9xR.mjs");
1358
+ const { createRecord } = await import("./chunks/records-DcHFelq5.mjs");
1340
1359
  return recordOut(ctx, await createRecord(ctx, objectId, fields, owner ? { ownerId: owner } : {}));
1341
1360
  }, (r) => `Created ${r.object} ${r.id} (${String(r.label)})`);
1342
1361
  }
@@ -1346,7 +1365,7 @@ ${table(r.records, ["id", "label", "updatedAt"])}`);
1346
1365
  const owner = strOpt(options, "owner");
1347
1366
  if (!Object.keys(fields).length && owner === void 0) usage("records update needs --fields, --set or --owner.");
1348
1367
  return withSession(io, options, { lock: true }, async ({ ctx }) => {
1349
- const { updateRecord } = await import("./chunks/records-DoqiT9xR.mjs");
1368
+ const { updateRecord } = await import("./chunks/records-DcHFelq5.mjs");
1350
1369
  return recordOut(ctx, await updateRecord(ctx, id, fields, owner !== void 0 ? { ownerId: owner || null } : {}));
1351
1370
  }, (r) => `Updated ${r.id} (${String(r.label)})`);
1352
1371
  }
@@ -1354,7 +1373,7 @@ ${table(r.records, ["id", "label", "updatedAt"])}`);
1354
1373
  if (!args.length) usage("records delete needs at least one id.", `Usage: agentcrm ${U.delete}`);
1355
1374
  const hard = boolOpt(options, "hard");
1356
1375
  return withSession(io, options, { lock: options.yes === true }, async ({ ctx }) => {
1357
- const { getRecord, deleteRecord, bulkDelete } = await import("./chunks/records-DoqiT9xR.mjs");
1376
+ const { getRecord, deleteRecord, bulkDelete } = await import("./chunks/records-DcHFelq5.mjs");
1358
1377
  const { recordLabel } = await import("./chunks/fields-B0bitgmr.mjs");
1359
1378
  const { objectOf } = await import("./chunks/store-5gqRSKXg.mjs");
1360
1379
  const targets2 = args.map((id) => getRecord(ctx, id, { includeDeleted: true }));
@@ -1367,7 +1386,7 @@ ${table(r.records, ["id", "label", "updatedAt"])}`);
1367
1386
  case "restore": {
1368
1387
  const id = positional(args, 0, "record id", U.restore);
1369
1388
  return withSession(io, options, { lock: true }, async ({ ctx }) => {
1370
- const { restoreRecord } = await import("./chunks/records-DoqiT9xR.mjs");
1389
+ const { restoreRecord } = await import("./chunks/records-DcHFelq5.mjs");
1371
1390
  return recordOut(ctx, await restoreRecord(ctx, id));
1372
1391
  });
1373
1392
  }
@@ -1375,7 +1394,7 @@ ${table(r.records, ["id", "label", "updatedAt"])}`);
1375
1394
  const query = args.join(" ").trim();
1376
1395
  if (!query) usage("records search needs a query.", `Usage: agentcrm ${U.search}`);
1377
1396
  return withSession(io, options, {}, async ({ ctx }) => {
1378
- const { searchRecords } = await import("./chunks/records-DoqiT9xR.mjs");
1397
+ const { searchRecords } = await import("./chunks/records-DcHFelq5.mjs");
1379
1398
  return searchRecords(ctx, query, { objects: listOpt(options, "object"), limit: Math.min(100, numOpt(options, "limit") ?? 20) });
1380
1399
  });
1381
1400
  }
@@ -1392,7 +1411,7 @@ ${table(r.records, ["id", "label", "updatedAt"])}`);
1392
1411
  const [primary, ...dups] = args;
1393
1412
  if (!primary || !dups.length) usage("records merge needs <primaryId> <duplicateId>….", `Usage: agentcrm ${U.merge}`);
1394
1413
  return withSession(io, options, { lock: options.yes === true }, async ({ ctx }) => {
1395
- const { getRecord } = await import("./chunks/records-DoqiT9xR.mjs");
1414
+ const { getRecord } = await import("./chunks/records-DcHFelq5.mjs");
1396
1415
  const all = [primary, ...dups].map((id) => getRecord(ctx, id));
1397
1416
  const plan = { primary: await recordOut(ctx, all[0]), duplicates: await Promise.all(all.slice(1).map((r) => recordOut(ctx, r))) };
1398
1417
  if (options.yes !== true) return { needsConfirmation: true, plan };
@@ -1424,13 +1443,13 @@ const pipelinesCommand = {
1424
1443
  io,
1425
1444
  options,
1426
1445
  {},
1427
- async ({ ctx }) => (await import("./chunks/pipelines-DCdRBNBV.mjs")).listPipelines(ctx, strOpt(options, "object")),
1446
+ async ({ ctx }) => (await import("./chunks/pipelines-BAKpPk1w.mjs")).listPipelines(ctx, strOpt(options, "object")),
1428
1447
  (ps) => ps.map((p) => `${p.id} (${p.name}, ${p.object}): ${p.stages.map((s) => `${s.label} ${Math.round(s.probability * (s.probability <= 1 ? 100 : 1))}%`).join(" → ")}`).join("\n")
1429
1448
  );
1430
1449
  }
1431
1450
  if (action === "move") {
1432
1451
  if (!id || !stage.length) usage("pipelines move needs <recordId> <stage>.");
1433
- return withSession(io, options, { lock: true }, async ({ ctx }) => recordOut(ctx, await (await import("./chunks/pipelines-DCdRBNBV.mjs")).moveStage(ctx, id, stage.join(" "), { lostReason: strOpt(options, "lost-reason") })));
1452
+ return withSession(io, options, { lock: true }, async ({ ctx }) => recordOut(ctx, await (await import("./chunks/pipelines-BAKpPk1w.mjs")).moveStage(ctx, id, stage.join(" "), { lostReason: strOpt(options, "lost-reason") })));
1434
1453
  }
1435
1454
  usage(`Unknown pipelines action "${action}".`, "Use list or move.");
1436
1455
  }
@@ -1462,7 +1481,7 @@ const viewsCommand = {
1462
1481
  const [action = "list", id] = positionals;
1463
1482
  switch (action) {
1464
1483
  case "list":
1465
- return withSession(io, options, {}, async ({ ctx }) => (await import("./chunks/views-fO_BM5U_.mjs")).listViews(ctx, strOpt(options, "object")), (vs) => table(vs, ["id", "object", "name", "type", "shared"]));
1484
+ return withSession(io, options, {}, async ({ ctx }) => (await import("./chunks/views-DH1n9Lrt.mjs")).listViews(ctx, strOpt(options, "object")), (vs) => table(vs, ["id", "object", "name", "type", "shared"]));
1466
1485
  case "create": {
1467
1486
  const type2 = strOpt(options, "type") ?? "table";
1468
1487
  if (type2 !== "table" && type2 !== "kanban") usage("--type must be table or kanban.");
@@ -1477,12 +1496,12 @@ const viewsCommand = {
1477
1496
  ...strOpt(options, "pipeline") ? { pipelineId: strOpt(options, "pipeline") } : {},
1478
1497
  shared: boolOpt(options, "shared")
1479
1498
  };
1480
- return withSession(io, options, { lock: true }, async ({ ctx }) => (await import("./chunks/views-fO_BM5U_.mjs")).createView(ctx, input));
1499
+ return withSession(io, options, { lock: true }, async ({ ctx }) => (await import("./chunks/views-DH1n9Lrt.mjs")).createView(ctx, input));
1481
1500
  }
1482
1501
  case "delete":
1483
1502
  if (!id) usage("views delete needs a view id.");
1484
1503
  return withSession(io, options, { lock: options.yes === true }, async ({ ctx }) => {
1485
- const { getView, deleteView } = await import("./chunks/views-fO_BM5U_.mjs");
1504
+ const { getView, deleteView } = await import("./chunks/views-DH1n9Lrt.mjs");
1486
1505
  const v = getView(ctx, id);
1487
1506
  if (options.yes !== true) return { needsConfirmation: true, plan: { delete: { id: v.id, name: v.name, object: v.object, shared: v.shared } } };
1488
1507
  return deleteView(ctx, id);
@@ -1545,11 +1564,11 @@ const settingsCommand = {
1545
1564
  options: [SITE_OPTION, AS_OPTION, { name: "set", type: "string", description: "key=value (repeatable), e.g. --set staleDealDays=21 --set defaultCurrency=EUR.", multiple: true }, { name: "data", type: "string", description: "JSON object of settings, or @file." }],
1546
1565
  async run({ io, options, positionals }) {
1547
1566
  const [action = "get"] = positionals;
1548
- if (action === "get") return withSession(io, options, {}, async ({ ctx }) => (await import("./chunks/settings-BtvR_jdm.mjs")).getSettings(ctx));
1567
+ if (action === "get") return withSession(io, options, {}, async ({ ctx }) => (await import("./chunks/settings-Di8nzNEL.mjs")).getSettings(ctx));
1549
1568
  if (action === "set") {
1550
1569
  const patch = fieldsInput(options, "data");
1551
1570
  if (!Object.keys(patch).length) usage("settings set needs --set key=value or --data.");
1552
- return withSession(io, options, { lock: true }, async ({ ctx }) => (await import("./chunks/settings-BtvR_jdm.mjs")).updateSettings(ctx, patch));
1571
+ return withSession(io, options, { lock: true }, async ({ ctx }) => (await import("./chunks/settings-Di8nzNEL.mjs")).updateSettings(ctx, patch));
1553
1572
  }
1554
1573
  usage(`Unknown settings action "${action}".`, "Use get or set.");
1555
1574
  }
@@ -1705,7 +1724,7 @@ const credentialsCommand = {
1705
1724
  return;
1706
1725
  }
1707
1726
  if (!process.stdin.isTTY) {
1708
- throw new CliError("needs-terminal", "verify reads the value the USER pastes in their own terminal.", `Ask the user to run it themselves (in Claude Code: \`! npx -y @fleox-agent-crm/agentcrm credentials verify ${account}\`).`);
1727
+ throw new CliError("needs-terminal", "verify reads the value the USER pastes in their own terminal.", `Ask the user to run it themselves (in Claude Code: \`! npx -y ${PACKAGE_NAME}@${readVersionInfo().version} credentials verify ${account}\`).`);
1709
1728
  }
1710
1729
  try {
1711
1730
  copyToClipboard("");
@@ -1765,6 +1784,18 @@ function parseJsonValue(raw, name2) {
1765
1784
  usage(`--${name2} is not valid JSON: ${err.message}`, `Example: --${name2} '{"targets":{"owner":50000}}' or --${name2} @config.json`);
1766
1785
  }
1767
1786
  }
1787
+ const MODULE_CONFIG_HINT = "Module configuration is JSON (≤ 256 KB) readable by every CRM user: no secrets (apiKey, token, secret, password…). Module ids: `agentcrm modules list`.";
1788
+ async function withHint(hint, p) {
1789
+ try {
1790
+ return await p;
1791
+ } catch (err) {
1792
+ const e = err;
1793
+ if (e?.name === "OpsError" && (e.code === "validation" || e.code === "notFound" || e.code === "invalid")) {
1794
+ throw new CliError(e.code, e.message, hint, e.code === "notFound" ? 1 : 2);
1795
+ }
1796
+ throw err;
1797
+ }
1798
+ }
1768
1799
  const modulesCommand = {
1769
1800
  name: "modules",
1770
1801
  usage: "modules <list|get|enable|disable|config> …",
@@ -1782,13 +1813,13 @@ const modulesCommand = {
1782
1813
  const apply = options.yes === true;
1783
1814
  switch (action) {
1784
1815
  case "list":
1785
- return withSession(io, options, {}, async ({ ctx }) => (await import("./chunks/modules-CqdVQVJb.mjs")).listModules(ctx), (rows) => table(
1816
+ return withSession(io, options, {}, async ({ ctx }) => (await import("./chunks/modules-clWdE-K2.mjs")).listModules(ctx), (rows) => table(
1786
1817
  rows.map((m) => ({ ...m, enabled: m.enabled ? "yes" : "no", required: m.required ? "yes" : "", dependsOn: m.dependsOn.join(",") })),
1787
1818
  ["id", "name", "category", "source", "enabled", "required", "dependsOn"]
1788
1819
  ));
1789
1820
  case "get": {
1790
1821
  const id = positional(positionals, 1, "module id", "modules get <id>");
1791
- return withSession(io, options, {}, async ({ ctx }) => (await import("./chunks/modules-CqdVQVJb.mjs")).getModule(ctx, id));
1822
+ return withSession(io, options, {}, async ({ ctx }) => (await import("./chunks/modules-clWdE-K2.mjs")).getModule(ctx, id));
1792
1823
  }
1793
1824
  case "enable":
1794
1825
  case "disable": {
@@ -1796,7 +1827,7 @@ const modulesCommand = {
1796
1827
  const enabled = action === "enable";
1797
1828
  const cascade = boolOpt(options, "cascade");
1798
1829
  return withSession(io, options, { lock: apply }, async ({ ctx }) => {
1799
- const { setModuleEnabled } = await import("./chunks/modules-CqdVQVJb.mjs");
1830
+ const { setModuleEnabled } = await import("./chunks/modules-clWdE-K2.mjs");
1800
1831
  if (apply) return setModuleEnabled(ctx, id, enabled, { cascade });
1801
1832
  const { result } = await dryRun(ctx, (copy) => setModuleEnabled(copy, id, enabled, { cascade }));
1802
1833
  return { needsConfirmation: true, plan: result };
@@ -1806,20 +1837,20 @@ Nothing changed — re-run with --yes once the user agrees.` : r.changed.length
1806
1837
  case "config": {
1807
1838
  const sub = positionals[1];
1808
1839
  const id = positional(positionals, 2, "module id", "modules config <get|set> <id>");
1809
- if (sub === "get") return withSession(io, options, {}, async ({ ctx }) => ({ id, config: (await import("./chunks/modules-CqdVQVJb.mjs")).getModuleConfig(ctx, id) ?? null }));
1840
+ if (sub === "get") return withSession(io, options, {}, async ({ ctx }) => ({ id, config: (await import("./chunks/modules-clWdE-K2.mjs")).getModuleConfig(ctx, id) ?? null }));
1810
1841
  if (sub !== "set") usage(`Unknown modules config action "${sub ?? ""}".`, "Use modules config get <id> or modules config set <id> --value <json>.");
1811
1842
  const clear = boolOpt(options, "clear");
1812
1843
  const raw = strOpt(options, "value");
1813
1844
  if (!clear && raw === void 0) usage("modules config set needs --value <json|@file|-> (or --clear).");
1814
1845
  const value = clear ? null : parseJsonValue(raw, "value");
1815
- return withSession(io, options, { lock: apply }, async ({ ctx }) => {
1816
- const mod = await import("./chunks/modules-CqdVQVJb.mjs");
1846
+ return withHint(MODULE_CONFIG_HINT, withSession(io, options, { lock: apply }, async ({ ctx }) => {
1847
+ const mod = await import("./chunks/modules-clWdE-K2.mjs");
1817
1848
  mod.getModule(ctx, id);
1818
1849
  const before = mod.getModuleConfig(ctx, id) ?? null;
1819
1850
  if (apply) return { id, config: await mod.setModuleConfig(ctx, id, value) ?? null, before };
1820
1851
  const { result } = await dryRun(ctx, (copy) => mod.setModuleConfig(copy, id, value));
1821
1852
  return { needsConfirmation: true, plan: { id, before, after: result ?? null } };
1822
- });
1853
+ }));
1823
1854
  }
1824
1855
  default:
1825
1856
  usage(`Unknown modules action "${action}".`, "Use list, get, enable, disable or config.");
@@ -1845,14 +1876,27 @@ function imageDataUrl(file2) {
1845
1876
  function view(a, full) {
1846
1877
  if (full) return { ...a };
1847
1878
  const { logo, favicon, customCss, ...rest } = a;
1848
- const kb2 = (s) => Math.round((s.length - s.indexOf(",") - 1) * 3 / 4 / 1024);
1879
+ const image = (s) => `${s.slice(5, s.indexOf(";"))}, ${formatSize(Math.floor((s.length - s.indexOf(",") - 1) * 3 / 4))}, #${shortHash(s)}`;
1849
1880
  return {
1850
1881
  ...rest,
1851
- logo: logo ? `${logo.slice(5, logo.indexOf(";"))}, ${kb2(logo)} KB` : null,
1852
- favicon: favicon ? `${favicon.slice(5, favicon.indexOf(";"))}, ${kb2(favicon)} KB` : null,
1853
- customCss: customCss ? `${new TextEncoder().encode(customCss).length} bytes` : null
1882
+ logo: logo ? image(logo) : null,
1883
+ favicon: favicon ? image(favicon) : null,
1884
+ customCss: customCss ? `${new TextEncoder().encode(customCss).length} bytes, #${shortHash(customCss)}` : null
1854
1885
  };
1855
1886
  }
1887
+ function formatSize(n) {
1888
+ return n < 1024 ? `${n} B` : `${(n / 1024).toFixed(1)} KB`;
1889
+ }
1890
+ function shortHash(s) {
1891
+ let h = 2166136261;
1892
+ for (let i = 0; i < s.length; i++) {
1893
+ h ^= s.charCodeAt(i);
1894
+ h = Math.imul(h, 16777619);
1895
+ }
1896
+ return (h >>> 0).toString(16).padStart(8, "0");
1897
+ }
1898
+ const APPEARANCE_HINT = "Allowed values: `agentcrm help appearance`; presets and installed themes: `agentcrm appearance presets`.";
1899
+ const withAppearanceHint = (p) => withHint(APPEARANCE_HINT, p);
1856
1900
  function diff(before, after) {
1857
1901
  const out = {};
1858
1902
  for (const k of /* @__PURE__ */ new Set([...Object.keys(before), ...Object.keys(after)])) {
@@ -1918,8 +1962,8 @@ const appearanceCommand = {
1918
1962
  const [action = "get"] = positionals;
1919
1963
  const apply = options.yes === true;
1920
1964
  const full = boolOpt(options, "full");
1921
- const appearance = () => import("./chunks/appearance-BaShelzt.mjs");
1922
- const change = (describe, op) => withSession(io, options, { lock: apply }, async ({ ctx }) => {
1965
+ const appearance = () => import("./chunks/appearance-BloHbiLA.mjs");
1966
+ const change = (describe, op) => withAppearanceHint(withSession(io, options, { lock: apply }, async ({ ctx }) => {
1923
1967
  const before = view((await appearance()).getAppearance(ctx), full);
1924
1968
  if (apply) {
1925
1969
  const after = view(await op(ctx), full);
@@ -1931,7 +1975,7 @@ const appearanceCommand = {
1931
1975
  const changes = r.plan ? r.plan.changes : r.changes ?? {};
1932
1976
  const lines = Object.entries(changes).map(([k, c]) => ` ${k}: ${JSON.stringify(c.from)} → ${JSON.stringify(c.to)}`);
1933
1977
  return [`${r.plan ? "Would apply" : "Applied"}: ${describe}`, ...lines.length ? lines : [" (no visible change)"], ...r.plan ? ["Nothing changed — re-run with --yes once the user agrees."] : []].join("\n");
1934
- });
1978
+ }));
1935
1979
  switch (action) {
1936
1980
  case "get":
1937
1981
  return withSession(io, options, {}, async ({ ctx }) => {
@@ -1986,7 +2030,7 @@ async function scaffold({ io, options, positionals }) {
1986
2030
  if (existsSync(dir) && readdirSync(dir).length > 0 && !boolOpt(options, "force")) {
1987
2031
  throw new CliError("dir-not-empty", `${dir} already exists and is not empty.`, "Pick another --dir (or --force to write into it).", 2);
1988
2032
  }
1989
- const { scaffoldExtension } = await import("./chunks/extScaffold-DqxI7R1e.mjs");
2033
+ const { scaffoldExtension } = await import("./chunks/extScaffold-Aqyjf1nr.mjs");
1990
2034
  const res = scaffoldExtension({ kind, id, dir, name: strOpt(options, "name"), description: strOpt(options, "description"), author: strOpt(options, "author") });
1991
2035
  printResult(io, res, () => [`Created ${kind} "${id}" in ${dir}:`, ...res.files.map((f) => ` ${f}`), "", "Next:", ...res.next.map((n) => ` ${n}`)].join("\n"));
1992
2036
  }
@@ -1994,8 +2038,8 @@ async function check({ io, options, positionals }) {
1994
2038
  const target = positionals[1] ?? ".";
1995
2039
  const kind = strOpt(options, "kind");
1996
2040
  if (kind && kind !== "module" && kind !== "theme") usage("--kind is module or theme.");
1997
- const { readRawPackage } = await import("./chunks/extPackage-BGl5Oqs8.mjs");
1998
- const { checkExtension } = await import("./chunks/extCheck--7lmVvl7.mjs");
2041
+ const { readRawPackage } = await import("./chunks/extPackage-DuZ6GZhp.mjs");
2042
+ const { checkExtension } = await import("./chunks/extCheck-GgHG1dYB.mjs");
1999
2043
  const raw = await readRawPackage(target);
2000
2044
  const report = await checkExtension(raw, { kind, callTools: !boolOpt(options, "no-call") });
2001
2045
  printResult(io, report, () => {
@@ -2024,10 +2068,10 @@ async function check({ io, options, positionals }) {
2024
2068
  }
2025
2069
  async function pack({ io, options, positionals }) {
2026
2070
  const target = positionals[1] ?? ".";
2027
- const { readPackage } = await import("./chunks/extPackage-BGl5Oqs8.mjs");
2071
+ const { readPackage } = await import("./chunks/extPackage-DuZ6GZhp.mjs");
2028
2072
  const { raw, pkg: pkg2 } = await readPackage(target);
2029
2073
  if (raw.sourceType === "zip") usage("ext pack takes a project or an unpacked folder, not a .zip.");
2030
- const { packExtension } = await import("./chunks/package-DWbn3t2R.mjs");
2074
+ const { packExtension } = await import("./chunks/package-B7mI6HGi.mjs");
2031
2075
  const bytes = await packExtension(pkg2);
2032
2076
  const out = resolve(strOpt(options, "out") ?? join(raw.source, `${pkg2.manifest.id}-${pkg2.manifest.version}.zip`));
2033
2077
  mkdirSync(dirname(out), { recursive: true });
@@ -2037,12 +2081,23 @@ async function pack({ io, options, positionals }) {
2037
2081
  }
2038
2082
  async function install({ io, options, positionals }) {
2039
2083
  const target = positional(positionals, 1, "ZIP or folder", "ext install <zip|dir> [--site <alias>] [--yes]");
2040
- const { readPackage } = await import("./chunks/extPackage-BGl5Oqs8.mjs");
2084
+ const { readPackage } = await import("./chunks/extPackage-DuZ6GZhp.mjs");
2041
2085
  const { pkg: pkg2, raw } = await readPackage(target);
2042
2086
  const apply = options.yes === true;
2043
2087
  const disabled = boolOpt(options, "disabled");
2088
+ if (!boolOpt(options, "force")) {
2089
+ const { checkExtension } = await import("./chunks/extCheck-GgHG1dYB.mjs");
2090
+ const report = await checkExtension(raw, { callTools: false });
2091
+ if (!report.valid) {
2092
+ throw new CliError(
2093
+ "invalid-package",
2094
+ `The extension doesn't pass the check: ${report.errors.map((e) => e.message).join(" ")}`,
2095
+ `Nothing was installed. Run \`agentcrm ext check ${target}\` for the full report (--force installs it anyway).`
2096
+ );
2097
+ }
2098
+ }
2044
2099
  return withSession(io, options, { lock: apply }, async (s) => {
2045
- const ext = await import("./chunks/extensions-BP1k60md.mjs");
2100
+ const ext = await import("./chunks/extensions-Bp72uZ9a.mjs");
2046
2101
  const existing = s.store.get("extensions", pkg2.manifest.id);
2047
2102
  const m = pkg2.manifest;
2048
2103
  const plan = {
@@ -2067,7 +2122,7 @@ async function install({ io, options, positionals }) {
2067
2122
  }
2068
2123
  const result = await ext.installExtension(s.ctx, pkg2, disabled ? { enabled: false } : {});
2069
2124
  const report = await s.extensions.sync();
2070
- const { getLoadedExtension } = await import("./chunks/registry-Ceexawxl.mjs");
2125
+ const { getLoadedExtension } = await import("./chunks/registry-COfJrSmg.mjs");
2071
2126
  const loaded = getLoadedExtension(m.id);
2072
2127
  const failure = report?.failed.find((f) => f.id === m.id || f.id === "*");
2073
2128
  if (failure && !loaded) return { ...result, author: m.author ?? null, load: { status: "error", error: failure.error } };
@@ -2078,7 +2133,7 @@ async function install({ io, options, positionals }) {
2078
2133
  ...r.plan.warnings.map((w) => ` note: ${w}`),
2079
2134
  ` ${r.plan.warning}`,
2080
2135
  "Nothing changed — re-run with --yes once the user agrees."
2081
- ].join("\n") : `${r.mode === "upgrade" ? `Upgraded ${r.previousVersion} → ` : "Installed "}${r.id} ${r.version} (${r.enabled ? "enabled" : "disabled"}; load: ${r.load.status}${"error" in r.load ? ` — ${r.load.error}` : ""})${r.warnings.length ? `
2136
+ ].join("\n") : `${r.mode === "upgrade" ? `Upgraded ${r.id} ${r.previousVersion} → ${r.version}` : `Installed ${r.id} ${r.version}`} (${r.enabled ? "enabled" : "disabled"}; load: ${r.load.status}${"error" in r.load ? ` — ${r.load.error}` : ""})${r.warnings.length ? `
2082
2137
  ${r.warnings.join("\n ")}` : ""}`);
2083
2138
  }
2084
2139
  const extCommand = {
@@ -2089,7 +2144,7 @@ const extCommand = {
2089
2144
  { name: "new", usage: "ext new <module|theme> <id> [--dir <path>] [--name <name>] [--description <text>] [--author <name>] [--force]", summary: "Scaffold a standalone extension project (package.json, vite.config.ts, tsconfig + runtime typings, src/index.tsx, README).", writes: true },
2090
2145
  { name: "check", usage: "ext check [<dir|zip>] [--kind module|theme] [--no-call]", summary: "Validate manifest, ids, apiVersion, sizes, imports; load the bundle in a throw-away CRM and report routes, nav, slots, AI tools, automation actions, theme values." },
2091
2146
  { name: "pack", usage: "ext pack [<dir>] [--out <file.zip>]", summary: "ZIP manifest.json + bundle.js (+ style.css) for Settings → Modules → Install or `ext install`.", writes: true },
2092
- { name: "install", usage: "ext install <zip|dir> [--site <alias>] [--disabled] [--yes]", summary: "Install or upgrade an extension on a CRM. Prints the plan (id, version, author, upgrade, size) unless --yes.", writes: true, destructive: true },
2147
+ { name: "install", usage: "ext install <zip|dir> [--site <alias>] [--disabled] [--force] [--yes]", summary: "Install or upgrade an extension on a CRM. The bundle is first loaded in a throw-away CRM (like ext check; --force skips it). Prints the plan (id, version, author, upgrade, size) unless --yes.", writes: true, destructive: true },
2093
2148
  { name: "list", usage: "ext list [--kind module|theme]", summary: "Installed extensions with version, enabled state and load status in this process." },
2094
2149
  { name: "enable", usage: "ext enable <id> [--cascade] [--yes]", summary: "Enable an installed extension (plan unless --yes).", writes: true },
2095
2150
  { name: "disable", usage: "ext disable <id> [--cascade] [--yes]", summary: "Disable it without uninstalling (plan unless --yes).", writes: true },
@@ -2104,7 +2159,7 @@ const extCommand = {
2104
2159
  { name: "name", type: "string", description: "new: display name." },
2105
2160
  { name: "description", type: "string", description: "new: one-sentence description." },
2106
2161
  { name: "author", type: "string", description: "new: author shown in the install plan and Settings → Modules." },
2107
- { name: "force", type: "boolean", description: "new: write into a non-empty folder. uninstall: even if enabled modules depend on it." },
2162
+ { name: "force", type: "boolean", description: "new: write into a non-empty folder. install: skip the trial load (ext check). uninstall: even if enabled modules depend on it." },
2108
2163
  { name: "kind", type: "string", description: "check: expected kind. list: only modules or themes." },
2109
2164
  { name: "no-call", type: "boolean", description: "check: don't smoke-call the AI tools on the demo data." },
2110
2165
  { name: "out", type: "string", description: "pack / export: ZIP path (default: <dir>/<id>-<version>.zip, ./<id>-<version>.zip)." },
@@ -2128,8 +2183,8 @@ const extCommand = {
2128
2183
  const kind = strOpt(options, "kind");
2129
2184
  if (kind && kind !== "module" && kind !== "theme") usage("--kind is module or theme.");
2130
2185
  return withSession(io, options, {}, async (s) => {
2131
- const { listExtensions } = await import("./chunks/extensions-BP1k60md.mjs");
2132
- const { getLoadedExtension } = await import("./chunks/registry-Ceexawxl.mjs");
2186
+ const { listExtensions } = await import("./chunks/extensions-Bp72uZ9a.mjs");
2187
+ const { getLoadedExtension } = await import("./chunks/registry-COfJrSmg.mjs");
2133
2188
  return listExtensions(s.ctx, kind ? { kind } : {}).map((e) => ({ ...e, load: s.extensions.safeMode ? "skipped" : getLoadedExtension(e.id)?.status ?? (e.enabled ? "not loaded" : "disabled"), ...getLoadedExtension(e.id)?.error ? { loadError: getLoadedExtension(e.id)?.error } : {} }));
2134
2189
  }, (rows) => table(rows.map((r) => ({ ...r, enabled: r.enabled ? "yes" : "no" })), ["id", "kind", "name", "version", "enabled", "load", "author"]));
2135
2190
  }
@@ -2139,15 +2194,15 @@ const extCommand = {
2139
2194
  const enabled = action === "enable";
2140
2195
  const cascade = boolOpt(options, "cascade");
2141
2196
  return withSession(io, options, { lock: apply }, async (s) => {
2142
- const ext = await import("./chunks/extensions-BP1k60md.mjs");
2197
+ const ext = await import("./chunks/extensions-Bp72uZ9a.mjs");
2143
2198
  const before = ext.getExtension(s.ctx, id);
2144
2199
  if (!apply) {
2145
2200
  const { result } = await dryRun(s.ctx, async (copy) => {
2146
2201
  await ext.setExtensionEnabled(copy, id, enabled, { cascade });
2147
- return (await import("./chunks/modules-CqdVQVJb.mjs")).moduleStates(copy.store);
2202
+ return (await import("./chunks/modules-clWdE-K2.mjs")).moduleStates(copy.store);
2148
2203
  });
2149
2204
  const now = new Map(result.map((m) => [m.id, m.enabled]));
2150
- const changed = (await import("./chunks/modules-CqdVQVJb.mjs")).moduleStates(s.store).filter((m) => now.has(m.id) && now.get(m.id) !== m.enabled).map((m) => ({ id: m.id, enabled: now.get(m.id) }));
2205
+ const changed = (await import("./chunks/modules-clWdE-K2.mjs")).moduleStates(s.store).filter((m) => now.has(m.id) && now.get(m.id) !== m.enabled).map((m) => ({ id: m.id, enabled: now.get(m.id) }));
2151
2206
  if (before.kind === "theme" && before.enabled !== enabled) changed.push({ id, enabled });
2152
2207
  return { needsConfirmation: true, plan: { id, kind: before.kind, from: before.enabled, to: enabled, changed } };
2153
2208
  }
@@ -2161,7 +2216,7 @@ Nothing changed — re-run with --yes once the user agrees.` : `${r.id} is now $
2161
2216
  const id = positional(positionals, 1, "extension id", "ext uninstall <id> [--yes]");
2162
2217
  const force = boolOpt(options, "force");
2163
2218
  return withSession(io, options, { lock: apply }, async (s) => {
2164
- const ext = await import("./chunks/extensions-BP1k60md.mjs");
2219
+ const ext = await import("./chunks/extensions-Bp72uZ9a.mjs");
2165
2220
  const summary = ext.getExtension(s.ctx, id);
2166
2221
  if (!apply) {
2167
2222
  await dryRun(s.ctx, (copy) => ext.uninstallExtension(copy, id, { force }));
@@ -2176,8 +2231,8 @@ Nothing changed — re-run with --yes once the user agrees.` : `Uninstalled ${r.
2176
2231
  case "export": {
2177
2232
  const id = positional(positionals, 1, "extension id", "ext export <id> [--out <file.zip>]");
2178
2233
  return withSession(io, options, {}, async (s) => {
2179
- const { getExtensionPackage } = await import("./chunks/extensions-BP1k60md.mjs");
2180
- const { packExtension } = await import("./chunks/package-DWbn3t2R.mjs");
2234
+ const { getExtensionPackage } = await import("./chunks/extensions-Bp72uZ9a.mjs");
2235
+ const { packExtension } = await import("./chunks/package-B7mI6HGi.mjs");
2181
2236
  const pkg2 = getExtensionPackage(s.ctx, id);
2182
2237
  const bytes = await packExtension(pkg2);
2183
2238
  const out = resolve(strOpt(options, "out") ?? `${id}-${pkg2.manifest.version}.zip`);
@@ -2191,9 +2246,6 @@ Nothing changed — re-run with --yes once the user agrees.` : `Uninstalled ${r.
2191
2246
  }
2192
2247
  }
2193
2248
  };
2194
- function printPlan(io, plan) {
2195
- printResult(io, plan, () => `Would restore ${String(plan.plan.restore)} (${String(plan.plan.mode)}). Re-run with --yes once the user agrees.`);
2196
- }
2197
2249
  function mappingOpt(options) {
2198
2250
  const raw = typeof options.map === "string" ? readTextValue(options.map, "map").trim() : void 0;
2199
2251
  if (!raw) return void 0;
@@ -2242,10 +2294,22 @@ const importCommand = {
2242
2294
  if (!existsSync2(path)) usage(`${path} does not exist.`);
2243
2295
  const bytes = new Uint8Array(readFileSync2(path));
2244
2296
  if (options.yes !== true) {
2245
- printPlan(io, { needsConfirmation: true, plan: { restore: resolve(path), bytes: bytes.length, mode: options.replace === true ? "replace" : "merge" } });
2246
- return;
2297
+ return withSession(
2298
+ io,
2299
+ options,
2300
+ {},
2301
+ async ({ ctx }) => {
2302
+ const plan = await (await import("./chunks/backup-W3CjgkAJ.mjs")).planBackupZip(ctx, bytes, { mode: options.replace === true ? "replace" : "merge" });
2303
+ return { needsConfirmation: true, plan: { restore: resolve(path), bytes: bytes.length, ...plan } };
2304
+ },
2305
+ (r) => [
2306
+ `Would restore ${r.plan.restore} (${r.plan.mode}): ${r.plan.added} new, ${r.plan.overwritten} overwritten, ${r.plan.deleted} deleted document(s), ${r.plan.files} file(s).`,
2307
+ ...r.plan.warnings.map((w) => `Warning: ${w}`),
2308
+ "Re-run with --yes once the user agrees."
2309
+ ].join("\n")
2310
+ );
2247
2311
  }
2248
- return withSession(io, options, { lock: true }, async ({ ctx }) => (await import("./chunks/backup-ClSmxmFZ.mjs")).importBackupZip(ctx, bytes, { mode: options.replace === true ? "replace" : "merge" }), (r) => `Restored ${r.written} document(s) and ${r.files} file(s)`);
2312
+ return withSession(io, options, { lock: true }, async ({ ctx }) => (await import("./chunks/backup-W3CjgkAJ.mjs")).importBackupZip(ctx, bytes, { mode: options.replace === true ? "replace" : "merge" }), (r) => `Restored ${r.written} document(s) and ${r.files} file(s)`);
2249
2313
  }
2250
2314
  if (kind !== "csv") usage('Only "import csv" and "import zip" are supported.', "Usage: agentcrm import csv <file> --object <id>");
2251
2315
  const source = positional(positionals, 1, "CSV file (or -)", "import csv <file|-> --object <id>");
@@ -2256,7 +2320,7 @@ const importCommand = {
2256
2320
  const apply = options.yes === true && options["dry-run"] !== true;
2257
2321
  const shown = numOpt(options, "rows") ?? 100;
2258
2322
  return withSession(io, options, { lock: apply }, async ({ ctx }) => {
2259
- const { importCsv } = await import("./chunks/importCsv-DmE_sBch.mjs");
2323
+ const { importCsv } = await import("./chunks/importCsv-LVmMTaWU.mjs");
2260
2324
  const report = await importCsv(ctx, {
2261
2325
  object: objectId,
2262
2326
  csv,
@@ -2305,16 +2369,16 @@ const exportCommand = {
2305
2369
  const file2 = strOpt(options, "out");
2306
2370
  if (!file2) usage("export zip needs --out <file.zip>.");
2307
2371
  return withSession(io, options, {}, async ({ ctx }) => {
2308
- const { exportBackupZip } = await import("./chunks/backup-ClSmxmFZ.mjs");
2372
+ const { exportBackupZip } = await import("./chunks/backup-W3CjgkAJ.mjs");
2309
2373
  const { zip, report } = await exportBackupZip(ctx, { includeFiles: options["no-files"] !== true });
2310
- writeFileSync(resolve(file2), zip);
2374
+ writeOutFile(file2, zip);
2311
2375
  return { file: resolve(file2), zipBytes: zip.length, files: report.files, missing: report.missing, fileBytes: report.bytes };
2312
2376
  }, (r) => `Wrote ${r.zipBytes} bytes (${r.files} file(s)${r.missing.length ? `, ${r.missing.length} missing` : ""}) to ${r.file}`);
2313
2377
  }
2314
2378
  const out = strOpt(options, "out");
2315
2379
  const deliver = (text, extra) => {
2316
2380
  if (out) {
2317
- writeFileSync(resolve(out), text);
2381
+ writeOutFile(out, text);
2318
2382
  return { file: resolve(out), bytes: Buffer.byteLength(text), ...extra };
2319
2383
  }
2320
2384
  return { ...extra, content: text };
@@ -2324,7 +2388,7 @@ const exportCommand = {
2324
2388
  const filters = parseFilters(multiOpt(options, "filter"), FILTER_OPS);
2325
2389
  const sorts = parseSorts(multiOpt(options, "sort"));
2326
2390
  return withSession(io, options, {}, async ({ ctx }) => {
2327
- const { exportCsv } = await import("./chunks/exportCsv-B5hRvkNm.mjs");
2391
+ const { exportCsv } = await import("./chunks/exportCsv-C6e6XEmB.mjs");
2328
2392
  const csv = exportCsv(ctx, objectId, {
2329
2393
  viewId: strOpt(options, "view"),
2330
2394
  filters,
@@ -2333,12 +2397,14 @@ const exportCommand = {
2333
2397
  values: boolOpt(options, "raw") ? "raw" : "labels",
2334
2398
  includeDeleted: boolOpt(options, "deleted")
2335
2399
  });
2336
- return deliver(csv, { object: objectId, rows: Math.max(0, csv.split(/\r?\n/).filter(Boolean).length - 1) });
2400
+ const { default: Papa } = await import("./chunks/papaparse-Df7i1dHr.mjs").then((n) => n.p);
2401
+ const rows = Math.max(0, Papa.parse(csv, { skipEmptyLines: true }).data.length - 1);
2402
+ return deliver(csv, { object: objectId, rows });
2337
2403
  }, (r) => "content" in r ? String(r.content) : `Wrote ${String(r.rows)} row(s) to ${String(r.file)}`);
2338
2404
  }
2339
2405
  if (kind === "json") {
2340
2406
  return withSession(io, options, {}, async ({ ctx }) => {
2341
- const { exportJson } = await import("./chunks/exportCsv-B5hRvkNm.mjs");
2407
+ const { exportJson } = await import("./chunks/exportCsv-C6e6XEmB.mjs");
2342
2408
  const data = exportJson(ctx, { collections: listOpt(options, "collections") });
2343
2409
  const text = JSON.stringify(data, null, 2);
2344
2410
  if (out) return deliver(text, {});
@@ -2374,6 +2440,7 @@ const reportCommand = {
2374
2440
  async run({ io, options, positionals }) {
2375
2441
  const name2 = positional(positionals, 0, "report name", "report <name>");
2376
2442
  const internal = REPORT_ALIASES[name2] ?? name2;
2443
+ if (!Object.values(REPORT_ALIASES).includes(internal)) usage(`Unknown report "${name2}".`, `Reports: ${Object.keys(REPORT_ALIASES).join(", ")}.`);
2377
2444
  const params = {
2378
2445
  ...jsonObjectOpt(options, "params") ?? {},
2379
2446
  ...numOpt(options, "months") !== void 0 ? { months: numOpt(options, "months") } : {},
@@ -2381,7 +2448,7 @@ const reportCommand = {
2381
2448
  ...strOpt(options, "pipeline") ? { pipelineId: strOpt(options, "pipeline") } : {},
2382
2449
  ...strOpt(options, "owner") ? { ownerId: strOpt(options, "owner") } : {}
2383
2450
  };
2384
- return withSession(io, options, {}, async ({ ctx }) => ({ report: name2, data: (await import("./chunks/reports-BB59N6ke.mjs")).runReport(ctx, internal, params) }));
2451
+ return withSession(io, options, {}, async ({ ctx }) => ({ report: name2, data: (await import("./chunks/reports-BACHGccj.mjs")).runReport(ctx, internal, params) }));
2385
2452
  }
2386
2453
  };
2387
2454
  const statsCommand = {
@@ -2391,7 +2458,7 @@ const statsCommand = {
2391
2458
  options: [SITE_OPTION, AS_OPTION],
2392
2459
  async run({ io, options }) {
2393
2460
  return withSession(io, options, {}, async ({ ctx, store }) => {
2394
- const stats = (await import("./chunks/stats-CfeJssSS.mjs")).getStats(ctx);
2461
+ const stats = (await import("./chunks/stats-BfuLWhVV.mjs")).getStats(ctx);
2395
2462
  const journal = store.stats;
2396
2463
  return { ...stats, ...journal ? { journal: await journal.call(store) } : {} };
2397
2464
  }, (s) => [`backend: ${s.backend}`, ...s.objects.map((o) => ` ${o.label}: ${o.count}${o.deleted ? ` (+${o.deleted} deleted)` : ""}`), `open tasks: ${s.openTasks} (${s.overdueTasks} overdue)`, `users: ${s.users}`].join("\n"));
@@ -2405,7 +2472,7 @@ const compactCommand = {
2405
2472
  async run({ io, options }) {
2406
2473
  const apply = options.yes === true;
2407
2474
  return withSession(io, options, { lock: apply }, async ({ ctx }) => {
2408
- const { compactData } = await import("./chunks/compact-CsFPaAAq.mjs");
2475
+ const { compactData } = await import("./chunks/compact-BGHI92tm.mjs");
2409
2476
  const report = await compactData(ctx, { olderThanDays: numOpt(options, "older-than"), keepRuns: numOpt(options, "keep-runs"), dryRun: !apply });
2410
2477
  return apply ? report : { needsConfirmation: true, plan: report };
2411
2478
  }, (r) => "plan" in r ? `Would purge ${r.plan.purgedRecords.length} record(s), ${r.plan.purgedActivities} activit(ies), ${r.plan.purgedRuns} run(s), ${r.plan.purgedFiles} file(s). Re-run with --yes once the user agrees.` : `Purged ${r.purgedRecords.length} record(s), ${r.purgedFiles} file(s); storage compacted: ${r.storageCompacted}`);
@@ -2426,19 +2493,19 @@ const automationsCommand = {
2426
2493
  const [action = "list", id] = positionals;
2427
2494
  switch (action) {
2428
2495
  case "list":
2429
- return withSession(io, options, {}, async ({ ctx }) => (await import("./chunks/automations-BWrvLqP6.mjs")).listAutomations(ctx).map((a) => ({ id: a.id, name: a.name, enabled: a.enabled, object: a.object, trigger: a.trigger, actions: a.actions.map((x) => x.type), lastRunAt: a.lastRunAt ? new Date(a.lastRunAt).toISOString() : null })), (rows) => table(rows, ["id", "name", "enabled", "object", "lastRunAt"]));
2496
+ return withSession(io, options, {}, async ({ ctx }) => (await import("./chunks/automations-DwWruxmu.mjs")).listAutomations(ctx).map((a) => ({ id: a.id, name: a.name, enabled: a.enabled, object: a.object, trigger: a.trigger, actions: a.actions.map((x) => x.type), lastRunAt: a.lastRunAt ? new Date(a.lastRunAt).toISOString() : null })), (rows) => table(rows, ["id", "name", "enabled", "object", "lastRunAt"]));
2430
2497
  case "enable":
2431
2498
  case "disable": {
2432
2499
  if (!id) usage(`automations ${action} needs an id.`);
2433
2500
  return withSession(io, options, { lock: true }, async ({ ctx }) => {
2434
- const mod = await import("./chunks/automations-BWrvLqP6.mjs");
2501
+ const mod = await import("./chunks/automations-DwWruxmu.mjs");
2435
2502
  return mod.setAutomationEnabled(ctx, id, action === "enable");
2436
2503
  });
2437
2504
  }
2438
2505
  case "run": {
2439
2506
  const apply = options.yes === true;
2440
2507
  return withSession(io, options, { lock: apply }, async ({ ctx }) => {
2441
- const { runScheduled } = await import("./chunks/automations-BWrvLqP6.mjs");
2508
+ const { runScheduled } = await import("./chunks/automations-DwWruxmu.mjs");
2442
2509
  const report = await runScheduled(ctx, { dryRun: !apply, automationId: strOpt(options, "id"), force: boolOpt(options, "force") });
2443
2510
  return apply ? report : { needsConfirmation: true, ...report };
2444
2511
  }, (r) => `${"needsConfirmation" in r ? "DRY RUN — " : ""}ran ${r.ran}, skipped ${r.skipped}, errors ${r.errors}`);
@@ -2462,11 +2529,11 @@ const formsCommand = {
2462
2529
  const [action = "list", id] = positionals;
2463
2530
  switch (action) {
2464
2531
  case "list":
2465
- return withSession(io, options, {}, async ({ ctx }) => (await import("./chunks/forms-BKU5F9WF.mjs")).listForms(ctx), (fs) => table(fs, ["id", "name", "object", "publishedPath"]));
2532
+ return withSession(io, options, {}, async ({ ctx }) => (await import("./chunks/forms-BfuWaQjA.mjs")).listForms(ctx), (fs) => table(fs, ["id", "name", "object", "publishedPath"]));
2466
2533
  case "publish": {
2467
2534
  if (!id) usage("forms publish needs a form id.");
2468
2535
  return withSession(io, options, { lock: options.yes === true }, async (s) => {
2469
- const forms = await import("./chunks/forms-BKU5F9WF.mjs");
2536
+ const forms = await import("./chunks/forms-BfuWaQjA.mjs");
2470
2537
  const built = await forms.buildFormHtml(s.ctx, id);
2471
2538
  const target = s.hostingTarget();
2472
2539
  if (!target) usage(`${s.site.alias} has no hosting to publish the form to.`);
@@ -2480,7 +2547,7 @@ const formsCommand = {
2480
2547
  case "ingest": {
2481
2548
  const apply = options.yes === true;
2482
2549
  return withSession(io, options, { lock: apply }, async ({ ctx }) => {
2483
- const { ingestInbox } = await import("./chunks/forms-BKU5F9WF.mjs");
2550
+ const { ingestInbox } = await import("./chunks/forms-BfuWaQjA.mjs");
2484
2551
  const report = await ingestInbox(ctx, strOpt(options, "form"), { dryRun: !apply, limit: numOpt(options, "limit") });
2485
2552
  return apply ? report : { needsConfirmation: true, ...report };
2486
2553
  }, (r) => `${"needsConfirmation" in r ? "DRY RUN — " : ""}processed ${r.processed}: ${r.created} created, ${r.updated} updated, ${r.failed} failed`);
@@ -2549,6 +2616,31 @@ async function fetchHostedAdmin(adminUrl, fetchImpl = fetch) {
2549
2616
  return { version: null, dataSchema: null, error: "version.json is not valid JSON" };
2550
2617
  }
2551
2618
  }
2619
+ function localAdminFolder(hosting) {
2620
+ if (!hosting || hosting.connectorId !== "local-folder") return null;
2621
+ const root = typeof hosting.settings.path === "string" ? hosting.settings.path : "";
2622
+ if (!root) return null;
2623
+ return join(root, hosting.folder ?? "admin");
2624
+ }
2625
+ function readLocalAdmin(folder) {
2626
+ for (const name2 of ["version.json", "admin-files.json"]) {
2627
+ const file2 = join(folder, name2);
2628
+ if (!existsSync(file2)) continue;
2629
+ try {
2630
+ const m = JSON.parse(readFileSync(file2, "utf8"));
2631
+ return { version: m.version ?? null, dataSchema: m.dataSchema ?? null };
2632
+ } catch {
2633
+ return { version: null, dataSchema: null, error: `${name2} is not valid JSON` };
2634
+ }
2635
+ }
2636
+ return { version: null, dataSchema: null, error: `no version.json in ${folder}` };
2637
+ }
2638
+ async function siteAdminVersion(site) {
2639
+ if (site.adminUrl) return { ...await fetchHostedAdmin(site.adminUrl), source: "url" };
2640
+ const folder = localAdminFolder(site.hosting);
2641
+ if (folder) return { ...readLocalAdmin(folder), source: "folder" };
2642
+ return { version: null, dataSchema: null, source: "none" };
2643
+ }
2552
2644
  function readChangelog() {
2553
2645
  try {
2554
2646
  const data = JSON.parse(readFileSync(join(packageRoot(), "changelog.json"), "utf8"));
@@ -2596,7 +2688,7 @@ const deployAdminCommand = {
2596
2688
  const plans = [];
2597
2689
  for (const site of sites) {
2598
2690
  if (!site.hosting) throw new CliError("not-hosted", `${site.alias} has no hosting recorded.`, "Register it with `site add --admin-url …` from a CRM whose admin is hosted, or reinstall with a hosting.");
2599
- const hosted = site.adminUrl ? await fetchHostedAdmin(site.adminUrl) : { version: null, dataSchema: null };
2691
+ const hosted = await siteAdminVersion(site);
2600
2692
  const plan = {
2601
2693
  alias: site.alias,
2602
2694
  adminUrl: site.adminUrl,
@@ -2695,7 +2787,7 @@ async function runDoctor(site, opts = {}) {
2695
2787
  installBrowserEnv();
2696
2788
  try {
2697
2789
  if (site.runtime.backend === "journal" && site.runtime.journal.connectorId === "git-pages") {
2698
- const { probeTarget } = await import("./chunks/install-CpHoUFtL.mjs");
2790
+ const { probeTarget } = await import("./chunks/install-CbpD_7gd.mjs");
2699
2791
  const { dataTargetOf: dataTargetOf2 } = await Promise.resolve().then(() => session);
2700
2792
  const probe = await probeTarget(dataTargetOf2(site), { requireSiteUrl: false, requirePrivate: true });
2701
2793
  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." });
@@ -2712,7 +2804,7 @@ async function runDoctor(site, opts = {}) {
2712
2804
  if (!settingsDoc(store)) missing.push("settings");
2713
2805
  if (!usersOf(store).some((u) => u.role === "admin" && !u.disabled)) missing.push("an active admin user");
2714
2806
  if (missing.length && opts.repair) {
2715
- const { seedCrm } = await import("./chunks/seed-CugPGYZL.mjs");
2807
+ const { seedCrm } = await import("./chunks/seed-6MRmQQBR.mjs");
2716
2808
  repaired = await seedCrm(session$1.system, { admin: site.user ? { name: site.user.split("@")[0], email: site.user } : void 0 });
2717
2809
  add({ id: "dataset", status: "ok", message: `Repaired: seeded ${missing.join(", ")}.` });
2718
2810
  } else {
@@ -2740,26 +2832,40 @@ async function runDoctor(site, opts = {}) {
2740
2832
  add({ id: "data-access", status: "fail", message: e.message, fix: e.hint });
2741
2833
  }
2742
2834
  const adminUrl = site.adminUrl;
2835
+ const adminFolder = adminUrl ? null : localAdminFolder(site.hosting);
2743
2836
  let origin = null;
2744
- if (!adminUrl) {
2837
+ if (!adminUrl && !adminFolder) {
2745
2838
  add({ id: "admin", status: "skip", message: "Admin not hosted (use `agentcrm serve`)." });
2746
2839
  } else {
2747
- origin = new URL(adminUrl).origin;
2748
- add(
2749
- adminUrl.startsWith("https://") || /^http:\/\/(localhost|127\.0\.0\.1)/.test(adminUrl) ? { id: "https", status: "ok", message: "Admin served over HTTPS (or localhost)." } : { id: "https", status: "fail", message: `${adminUrl} is plain HTTP: browsers refuse WebCrypto (signing, encryption) there.`, fix: "Serve the admin over HTTPS (CDN / custom domain with TLS, the bucket's https endpoint, GitHub Pages)." }
2750
- );
2751
- const index = await fetchText(`${adminUrl}index.html`);
2840
+ if (adminUrl) {
2841
+ origin = new URL(adminUrl).origin;
2842
+ add(
2843
+ adminUrl.startsWith("https://") || /^http:\/\/(localhost|127\.0\.0\.1)/.test(adminUrl) ? { id: "https", status: "ok", message: "Admin served over HTTPS (or localhost)." } : { id: "https", status: "fail", message: `${adminUrl} is plain HTTP: browsers refuse WebCrypto (signing, encryption) there.`, fix: "Serve the admin over HTTPS (CDN / custom domain with TLS, the bucket's https endpoint, GitHub Pages)." }
2844
+ );
2845
+ }
2846
+ const read = async (name2) => {
2847
+ if (adminUrl) {
2848
+ const res = await fetchText(`${adminUrl}${name2}`);
2849
+ return { ok: res.ok, text: res.text, error: res.error ?? (res.ok ? void 0 : `HTTP ${res.status}`) };
2850
+ }
2851
+ const file2 = join(adminFolder, name2);
2852
+ if (!existsSync(file2)) return { ok: false, text: "", error: `${file2} not found` };
2853
+ return { ok: true, text: readFileSync(file2, "utf8") };
2854
+ };
2855
+ const index = await read("index.html");
2752
2856
  if (!index.ok) {
2753
- add({ id: "admin", status: "fail", message: `Admin page not served (${index.error ?? `HTTP ${index.status}`}).`, fix: `\`agentcrm deploy-admin --site ${site.alias} --yes\` (Pages hosts take a minute after a commit).` });
2857
+ add({ id: "admin", status: "fail", message: `Admin page not ${adminUrl ? "served" : "found"} (${index.error}).`, fix: `\`agentcrm deploy-admin --site ${site.alias} --yes\`${adminUrl ? " (Pages hosts take a minute after a commit)" : ""}.` });
2754
2858
  } else {
2755
- add({ id: "admin", status: "ok", message: `Admin served at ${adminUrl}.` });
2756
- const hosted = await fetchHostedAdmin(adminUrl);
2859
+ add(
2860
+ adminUrl ? { id: "admin", status: "ok", message: `Admin served at ${adminUrl}.` } : { id: "admin", status: "ok", message: `Admin in the folder ${adminFolder} (no public URL): open it with \`agentcrm serve --dir ${adminFolder}\`.` }
2861
+ );
2862
+ const hosted = adminUrl ? await fetchHostedAdmin(adminUrl) : readLocalAdmin(adminFolder);
2757
2863
  add(
2758
2864
  !hosted.version ? { id: "admin-version", status: "warn", message: "version.json missing next to the admin.", fix: `\`agentcrm deploy-admin --site ${site.alias} --yes\`.` } : compareVersions(hosted.version, info.version) >= 0 ? { id: "admin-version", status: "ok", message: `Admin ${hosted.version} is up to date.` } : { id: "admin-version", status: "warn", message: `Admin ${hosted.version} is older than ${info.version}.`, fix: `Offer \`agentcrm deploy-admin --site ${site.alias}\`.` }
2759
2865
  );
2760
- const cfg = await fetchText(`${adminUrl}config.js`);
2866
+ const cfg = await read("config.js");
2761
2867
  try {
2762
- if (!cfg.ok) throw new Error(cfg.error ?? `HTTP ${cfg.status}`);
2868
+ if (!cfg.ok) throw new Error(cfg.error);
2763
2869
  const runtime = parseConfigJs(cfg.text);
2764
2870
  const secrets = findSecretKeys(runtime);
2765
2871
  if (secrets.length) add({ id: "config-js", status: "fail", message: `config.js publishes secrets: ${secrets.join(", ")}.`, fix: "Revoke those keys NOW, then redeploy the admin (config.js is regenerated without secrets)." });
@@ -2823,7 +2929,7 @@ function readPlan(source) {
2823
2929
  try {
2824
2930
  return JSON.parse(text);
2825
2931
  } catch (err) {
2826
- throw new CliError("invalid-plan", `The plan is not valid JSON: ${err.message}`, void 0, 2);
2932
+ throw new CliError("invalid-plan", `The plan is not valid JSON: ${err.message}`, "Write the plan as one JSON object (`agentcrm options --json` documents every field).", 2);
2827
2933
  }
2828
2934
  }
2829
2935
  const installCommand = {
@@ -2838,10 +2944,10 @@ const installCommand = {
2838
2944
  const planPath = strOpt(options, "plan");
2839
2945
  if (!planPath) usage("install needs --plan <file|->.", "Run `agentcrm options` for the format.");
2840
2946
  installBrowserEnv();
2841
- const { resolvePlan } = await import("./chunks/plan-C0TBrU92.mjs");
2947
+ const { resolvePlan } = await import("./chunks/plan-BmTat6-T.mjs");
2842
2948
  const plan = resolvePlan(readPlan(planPath));
2843
2949
  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.");
2844
- const { checkPlan, runInstall } = await import("./chunks/install-CpHoUFtL.mjs");
2950
+ const { checkPlan, runInstall } = await import("./chunks/install-CbpD_7gd.mjs");
2845
2951
  io.err("Checking access…");
2846
2952
  const checks = await checkPlan(plan);
2847
2953
  const summary = {
@@ -2861,7 +2967,7 @@ const installCommand = {
2861
2967
  throw new CliError(
2862
2968
  "check-failed",
2863
2969
  [!checks.data.ok && `Data: ${checks.data.detail}`, checks.hosting && !checks.hosting.ok && `Hosting: ${checks.hosting.detail}`].filter(Boolean).join(" — "),
2864
- "Fix the access (key / token permissions, bucket / repo names, URL, CORS) and retry the dry run."
2970
+ checks.data.existing && !checks.data.ok ? `Reuse it: the user stores its passphrase (\`agentcrm credentials set ${plan.passphraseAccount ?? `passphrase:${plan.alias}`}\` in their own terminal, or --from-env), then add "encryption": { "credential": "${plan.passphraseAccount ?? `passphrase:${plan.alias}`}" } to the plan's data and retry the dry run. Or pick another folder / root for a new CRM.` : "Fix the access (key / token permissions, bucket / repo names, URL, CORS) and retry the dry run."
2865
2971
  );
2866
2972
  }
2867
2973
  if (options["dry-run"] === true) {
@@ -2887,6 +2993,8 @@ const installCommand = {
2887
2993
  });
2888
2994
  const nextSteps = [];
2889
2995
  if (plan.adminUrl) nextSteps.push(`Open the admin: ${plan.adminUrl}`);
2996
+ else if (localAdminFolder(plan.hosting)) nextSteps.push(`The admin is in ${localAdminFolder(plan.hosting)} (no public URL): open it with \`agentcrm serve --dir ${localAdminFolder(plan.hosting)}\`, or serve that folder over HTTPS.`);
2997
+ else if (plan.hosting) nextSteps.push(`The admin was uploaded to the ${plan.hosting.type} hosting; its public URL is unknown (add siteUrl to the plan's hosting settings next time).`);
2890
2998
  else nextSteps.push("The admin was not uploaded: run `agentcrm serve` to use it on this computer.");
2891
2999
  if (plan.backend === "journal") {
2892
3000
  nextSteps.push(`Each new browser asks once for the data location keys${plan.passphraseAccount ? " and the passphrase" : ""} (they stay in that browser).`);
@@ -3136,9 +3244,10 @@ const versionCommand = {
3136
3244
  }
3137
3245
  };
3138
3246
  async function siteStatus(site, version2, dataSchema) {
3139
- const base = { alias: site.alias, adminUrl: site.adminUrl };
3140
- if (!site.adminUrl) return { ...base, adminVersion: null, state: "not-hosted" };
3141
- const hosted = await fetchHostedAdmin(site.adminUrl);
3247
+ const folder = site.adminUrl ? null : localAdminFolder(site.hosting);
3248
+ const base = { alias: site.alias, adminUrl: site.adminUrl, ...folder ? { adminFolder: folder } : {} };
3249
+ const hosted = await siteAdminVersion(site);
3250
+ if (hosted.source === "none") return { ...base, adminVersion: null, state: "not-hosted" };
3142
3251
  if (!hosted.version) return { ...base, adminVersion: null, state: "unreachable", error: hosted.error };
3143
3252
  if (hosted.version !== site.adminVersion) putSite({ ...site, adminVersion: hosted.version });
3144
3253
  const cmp = compareVersions(hosted.version, version2);
@@ -3199,7 +3308,7 @@ const optionsCommand = {
3199
3308
  printResult(io, { email: rulesEmail, rules, where: "Firebase console → Firestore Database → Rules → paste → Publish (or `firebase deploy --only firestore:rules`)." }, () => rules);
3200
3309
  return;
3201
3310
  }
3202
- const { PLAN_EXAMPLE } = await import("./chunks/plan-C0TBrU92.mjs");
3311
+ const { PLAN_EXAMPLE } = await import("./chunks/plan-BmTat6-T.mjs");
3203
3312
  const data = {
3204
3313
  order: ["data backend", "data location", "admin hosting"],
3205
3314
  backends: DATA_BACKENDS,
@@ -3272,13 +3381,13 @@ const objectsCommand = {
3272
3381
  switch (action) {
3273
3382
  case "list":
3274
3383
  return withSession(io, options, {}, async ({ ctx, store }) => {
3275
- const { listObjects } = await import("./chunks/schema-DlHt2PxI.mjs");
3384
+ const { listObjects } = await import("./chunks/schema-C4lSslXv.mjs");
3276
3385
  const { recordsOf } = await import("./chunks/store-5gqRSKXg.mjs");
3277
3386
  return listObjects(ctx).map((o) => ({ id: o.id, labelSingular: o.labelSingular, labelPlural: o.labelPlural, standard: o.standard, labelField: o.labelField, fields: o.fields.length, records: recordsOf(store, o.id).length }));
3278
3387
  }, (rows) => table(rows, ["id", "labelPlural", "fields", "records", "standard"]));
3279
3388
  case "get": {
3280
3389
  const objectId = positional(positionals, 1, "object id", "objects get <id>");
3281
- return withSession(io, options, {}, async ({ ctx }) => (await import("./chunks/schema-DlHt2PxI.mjs")).describeSchema(ctx, objectId));
3390
+ return withSession(io, options, {}, async ({ ctx }) => (await import("./chunks/schema-C4lSslXv.mjs")).describeSchema(ctx, objectId)[0]);
3282
3391
  }
3283
3392
  case "create": {
3284
3393
  const data = jsonObjectOpt(options, "data") ?? {};
@@ -3291,7 +3400,7 @@ const objectsCommand = {
3291
3400
  ...strOpt(options, "color") ? { color: strOpt(options, "color") } : {}
3292
3401
  };
3293
3402
  if (!input.labelSingular || !input.labelPlural) usage("objects create needs --singular and --plural (or --data).");
3294
- return withSession(io, options, { lock: true }, async ({ ctx }) => (await import("./chunks/schema-DlHt2PxI.mjs")).createObject(ctx, input));
3403
+ return withSession(io, options, { lock: true }, async ({ ctx }) => (await import("./chunks/schema-C4lSslXv.mjs")).createObject(ctx, input));
3295
3404
  }
3296
3405
  case "update": {
3297
3406
  const objectId = positional(positionals, 1, "object id", "objects update <id> …");
@@ -3303,12 +3412,12 @@ const objectsCommand = {
3303
3412
  ...strOpt(options, "label-field") ? { labelField: strOpt(options, "label-field") } : {}
3304
3413
  };
3305
3414
  if (!Object.keys(patch).length) usage("objects update needs --singular, --plural, --icon, --color or --label-field.");
3306
- return withSession(io, options, { lock: true }, async ({ ctx }) => (await import("./chunks/schema-DlHt2PxI.mjs")).updateObject(ctx, objectId, patch));
3415
+ return withSession(io, options, { lock: true }, async ({ ctx }) => (await import("./chunks/schema-C4lSslXv.mjs")).updateObject(ctx, objectId, patch));
3307
3416
  }
3308
3417
  case "delete": {
3309
3418
  if (!id) usage("objects delete needs an object id.");
3310
3419
  return withSession(io, options, { lock: options.yes === true }, async ({ ctx, store }) => {
3311
- const { getObject, deleteObject } = await import("./chunks/schema-DlHt2PxI.mjs");
3420
+ const { getObject, deleteObject } = await import("./chunks/schema-C4lSslXv.mjs");
3312
3421
  const { recordsOf } = await import("./chunks/store-5gqRSKXg.mjs");
3313
3422
  const o = getObject(ctx, id);
3314
3423
  const plan = { object: o.id, label: o.labelPlural, standard: o.standard, recordsDeleted: recordsOf(store, o.id, { includeDeleted: true }).length };
@@ -3366,7 +3475,7 @@ const fieldsCommand = {
3366
3475
  ...boolOpt(options, "unique") ? { unique: true } : {},
3367
3476
  ...strOpt(options, "description") ? { description: strOpt(options, "description") } : {}
3368
3477
  };
3369
- return withSession(io, options, { lock: true }, async ({ ctx }) => (await import("./chunks/schema-DlHt2PxI.mjs")).createField(ctx, objectId, input));
3478
+ return withSession(io, options, { lock: true }, async ({ ctx }) => (await import("./chunks/schema-C4lSslXv.mjs")).createField(ctx, objectId, input));
3370
3479
  }
3371
3480
  case "update": {
3372
3481
  if (!fieldId) usage("fields update needs a field id.");
@@ -3383,12 +3492,12 @@ const fieldsCommand = {
3383
3492
  ...strOpt(options, "description") !== void 0 ? { description: strOpt(options, "description") } : {}
3384
3493
  };
3385
3494
  if (!Object.keys(patch).length) usage("fields update needs at least one change.");
3386
- return withSession(io, options, { lock: true }, async ({ ctx }) => (await import("./chunks/schema-DlHt2PxI.mjs")).updateField(ctx, objectId, fieldId, patch));
3495
+ return withSession(io, options, { lock: true }, async ({ ctx }) => (await import("./chunks/schema-C4lSslXv.mjs")).updateField(ctx, objectId, fieldId, patch));
3387
3496
  }
3388
3497
  case "delete": {
3389
3498
  if (!fieldId) usage("fields delete needs a field id.");
3390
3499
  return withSession(io, options, { lock: options.yes === true }, async ({ ctx, store }) => {
3391
- const { getObject, deleteField } = await import("./chunks/schema-DlHt2PxI.mjs");
3500
+ const { getObject, deleteField } = await import("./chunks/schema-C4lSslXv.mjs");
3392
3501
  const { recordsOf } = await import("./chunks/store-5gqRSKXg.mjs");
3393
3502
  const field = getObject(ctx, objectId).fields.find((f) => f.id === fieldId);
3394
3503
  if (!field) usage(`No field "${fieldId}" on ${objectId}.`, `\`agentcrm objects get ${objectId}\` lists them.`);
@@ -3695,7 +3804,7 @@ const toolsCommand = {
3695
3804
  async run({ io, options, positionals }) {
3696
3805
  const [action = "list"] = positionals;
3697
3806
  if (action === "list") {
3698
- const { listAiTools, AI_TOOLS } = await import("./chunks/tools-Bew8lxKx.mjs");
3807
+ const { listAiTools, AI_TOOLS } = await import("./chunks/tools-iSH-yMpl.mjs");
3699
3808
  const builtin = new Set(AI_TOOLS.map((t) => t.name));
3700
3809
  const shape = () => listAiTools().map((t) => ({ name: t.name, description: t.description, mutating: t.mutating, source: builtin.has(t.name) ? "builtin" : "extension", inputSchema: t.inputSchema }));
3701
3810
  const human = (r) => table(r.tools, ["name", "mutating", "source", "description"]);
@@ -3715,7 +3824,7 @@ const toolsCommand = {
3715
3824
  }
3716
3825
  if (action === "call") {
3717
3826
  const name2 = positional(positionals, 1, "tool name", "tools call <name> --input <json>");
3718
- const { findTool, callTool, listAiTools } = await import("./chunks/tools-Bew8lxKx.mjs");
3827
+ const { findTool, callTool, listAiTools } = await import("./chunks/tools-iSH-yMpl.mjs");
3719
3828
  const input = jsonObjectOpt(options, "input") ?? {};
3720
3829
  const builtin = findTool(name2);
3721
3830
  return withSession(io, options, { lock: builtin ? builtin.mutating : true }, async ({ ctx }) => {
@@ -3737,8 +3846,8 @@ const mcpCommand = {
3737
3846
  const log = (msg) => process.stderr.write(`${msg}
3738
3847
  `);
3739
3848
  for (const w of session2.warnings) log(`[agentcrm mcp] note: ${w}`);
3740
- const { listAiTools } = await import("./chunks/tools-Bew8lxKx.mjs");
3741
- const { createMcpServer, serveStdio } = await import("./chunks/server-ISwlDRCN.mjs");
3849
+ const { listAiTools } = await import("./chunks/tools-iSH-yMpl.mjs");
3850
+ const { createMcpServer, serveStdio } = await import("./chunks/server-DC3t8r30.mjs");
3742
3851
  let lastRefresh = Date.now();
3743
3852
  const server = createMcpServer({
3744
3853
  readOnly,
@@ -3761,7 +3870,10 @@ const mcpCommand = {
3761
3870
  async withWrite(fn) {
3762
3871
  const unlock = lockSite(session2.site.alias);
3763
3872
  try {
3764
- return await fn();
3873
+ const result = await fn();
3874
+ const report = await session2.extensions.sync();
3875
+ for (const f of report?.failed ?? []) log(`[agentcrm mcp] extension ${f.id} failed to load: ${f.error}`);
3876
+ return result;
3765
3877
  } finally {
3766
3878
  unlock();
3767
3879
  }
@@ -3866,7 +3978,11 @@ async function main(argv, io) {
3866
3978
  const json = argv.includes("--json");
3867
3979
  const out = createIo(json);
3868
3980
  try {
3869
- const [name2 = "help", ...rest] = argv.filter((a) => a !== "--json");
3981
+ const args = argv.filter((a) => a !== "--json");
3982
+ const leading = [];
3983
+ while (args[0] && LEADING_GLOBAL_FLAGS.has(args[0])) leading.push(args.shift());
3984
+ const [name2 = "help", ...rest] = args;
3985
+ rest.push(...leading);
3870
3986
  if (name2 === "--help" || name2 === "-h") return await run("help", rest, out) ?? 0;
3871
3987
  if (name2 === "--version" || name2 === "-v") return await run("version", rest, out) ?? 0;
3872
3988
  return await run(name2, rest, out) ?? 0;
@@ -3874,6 +3990,7 @@ async function main(argv, io) {
3874
3990
  return printError(out, error);
3875
3991
  }
3876
3992
  }
3993
+ const LEADING_GLOBAL_FLAGS = new Set(GLOBAL_OPTIONS.filter((o) => o.type === "boolean" && o.name !== "help").map((o) => `--${o.name}`));
3877
3994
  async function run(name2, args, io) {
3878
3995
  const command = findCommand(name2);
3879
3996
  if (!command) {
@@ -3891,13 +4008,21 @@ async function run(name2, args, io) {
3891
4008
  throw new CliError("usage", e.message, `Usage: agentcrm ${command.usage}`, 2);
3892
4009
  }
3893
4010
  if (parsed.values.help && command.name !== "help") return run("help", [command.name], io);
3894
- return command.run({
3895
- io,
3896
- options: parsed.values,
3897
- positionals: parsed.positionals
3898
- });
4011
+ try {
4012
+ return await command.run({
4013
+ io,
4014
+ options: parsed.values,
4015
+ positionals: parsed.positionals
4016
+ });
4017
+ } catch (e) {
4018
+ if (e instanceof CliError && !e.hint && e.exitCode === 2) {
4019
+ throw new CliError(e.code, e.message, `Usage: agentcrm ${command.usage} (details: \`agentcrm help ${command.name}\`).`, e.exitCode);
4020
+ }
4021
+ throw e;
4022
+ }
3899
4023
  }
3900
4024
  installBrowserEnv();
4025
+ routeConsoleToStderr();
3901
4026
  main(process.argv.slice(2)).then(
3902
4027
  (code) => process.exit(code),
3903
4028
  (err) => {
@@ -3913,11 +4038,11 @@ export {
3913
4038
  LOCATIONS as L,
3914
4039
  PACKAGE_NAME as P,
3915
4040
  USER_ROLES as U,
3916
- parseAccount as a,
3917
- requireCredential as b,
3918
- findLocation as c,
3919
- findHosting as d,
3920
- FIELD_TYPES as e,
4041
+ FIELD_TYPES as a,
4042
+ parseAccount as b,
4043
+ requireCredential as c,
4044
+ findLocation as d,
4045
+ findHosting as e,
3921
4046
  findBackend as f,
3922
4047
  getCredential as g,
3923
4048
  openStore as h,