@dench.com/cli 2.3.2 → 2.4.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.
package/crm.ts CHANGED
@@ -88,6 +88,7 @@ type CrmCliContext = {
88
88
  * organization context.
89
89
  */
90
90
  sessionToken?: string;
91
+ runId?: string;
91
92
  enrichmentGateway?: Pick<EnrichmentGatewayOptions, "apiKey" | "baseUrl">;
92
93
  };
93
94
 
@@ -296,6 +297,7 @@ async function applyBatchOpsInChunks(
296
297
  const chunkKey = chunkIdempotencyKey(idempotencyKey, chunkIndex);
297
298
  await callMutation(ctx, api.batch.apply, {
298
299
  ops: chunk,
300
+ ...runContextArgs(ctx),
299
301
  ...(chunkKey ? { idempotencyKey: chunkKey } : {}),
300
302
  ...(allowUnknown ? { allowUnknown: true } : {}),
301
303
  });
@@ -303,6 +305,10 @@ async function applyBatchOpsInChunks(
303
305
  return chunks.length;
304
306
  }
305
307
 
308
+ function runContextArgs(ctx: CrmCliContext): Record<string, string> {
309
+ return ctx.runId ? { runId: ctx.runId } : {};
310
+ }
311
+
306
312
  /**
307
313
  * Fetch the canonical field names for an object. Best-effort: returns
308
314
  * `null` (rather than throwing) on any failure so the caller falls back
@@ -574,6 +580,11 @@ const api = {
574
580
  "functions/crm/entries:bulkDelete",
575
581
  ),
576
582
  },
583
+ history: {
584
+ listForEntry: makeFunctionReference<"query">(
585
+ "functions/crm/history:listForEntry",
586
+ ),
587
+ },
577
588
  cells: {
578
589
  get: makeFunctionReference<"query">("functions/crm/cells:get"),
579
590
  set: makeFunctionReference<"mutation">("functions/crm/cells:set"),
@@ -641,6 +652,7 @@ export async function runCrmCommand(opts: {
641
652
  args,
642
653
  jsonOutput,
643
654
  sessionToken: opts.sessionToken,
655
+ runId: process.env.DENCH_RUN_ID?.trim() || undefined,
644
656
  enrichmentGateway: opts.enrichmentGateway,
645
657
  };
646
658
  const subcommand = args.shift();
@@ -657,6 +669,8 @@ export async function runCrmCommand(opts: {
657
669
  return await runFieldsCommand(ctx);
658
670
  case "entries":
659
671
  return await runEntriesCommand(ctx);
672
+ case "history":
673
+ return await runHistoryCommand(ctx);
660
674
  case "cells":
661
675
  return await runCellsCommand(ctx);
662
676
  case "query":
@@ -1335,6 +1349,7 @@ async function runEntriesCommand(ctx: CrmCliContext): Promise<void> {
1335
1349
  await callMutation(ctx, api.entries.create, {
1336
1350
  objectName,
1337
1351
  data,
1352
+ ...runContextArgs(ctx),
1338
1353
  ...(allowUnknown ? { allowUnknown: true } : {}),
1339
1354
  }),
1340
1355
  );
@@ -1392,6 +1407,7 @@ async function runEntriesCommand(ctx: CrmCliContext): Promise<void> {
1392
1407
  objectName,
1393
1408
  entryId: entryId as any,
1394
1409
  data,
1410
+ ...runContextArgs(ctx),
1395
1411
  ...(allowUnknown ? { allowUnknown: true } : {}),
1396
1412
  }),
1397
1413
  );
@@ -1438,6 +1454,7 @@ async function runEntriesCommand(ctx: CrmCliContext): Promise<void> {
1438
1454
  ctx,
1439
1455
  await callMutation(ctx, api.entries.remove, {
1440
1456
  entryId: entryId as any,
1457
+ ...runContextArgs(ctx),
1441
1458
  }),
1442
1459
  );
1443
1460
  return;
@@ -1452,6 +1469,7 @@ async function runEntriesCommand(ctx: CrmCliContext): Promise<void> {
1452
1469
  ctx,
1453
1470
  await callMutation(ctx, api.entries.bulkDelete, {
1454
1471
  entryIds: entryIds as any,
1472
+ ...runContextArgs(ctx),
1455
1473
  }),
1456
1474
  );
1457
1475
  return;
@@ -1461,6 +1479,27 @@ async function runEntriesCommand(ctx: CrmCliContext): Promise<void> {
1461
1479
  }
1462
1480
  }
1463
1481
 
1482
+ async function runHistoryCommand(ctx: CrmCliContext): Promise<void> {
1483
+ const objectName = shift(ctx.args, "object name");
1484
+ const entryId = shift(ctx.args, "entry id");
1485
+ const includeSystem = hasFlag(ctx.args, "--include-system");
1486
+ const limit = parseInt(getFlag(ctx.args, "--limit") ?? "50", 10);
1487
+ const cursor = getFlag(ctx.args, "--cursor");
1488
+ assertNoUnknownFlags(ctx.args, "crm history");
1489
+ out(
1490
+ ctx,
1491
+ await callQuery(ctx, api.history.listForEntry, {
1492
+ objectName,
1493
+ entryId,
1494
+ includeSystem,
1495
+ paginationOpts: {
1496
+ numItems: Number.isFinite(limit) ? Math.min(Math.max(limit, 1), 100) : 50,
1497
+ cursor: cursor ?? null,
1498
+ },
1499
+ }),
1500
+ );
1501
+ }
1502
+
1464
1503
  async function runCellsCommand(ctx: CrmCliContext): Promise<void> {
1465
1504
  const verb = ctx.args.shift();
1466
1505
  switch (verb) {
@@ -1492,6 +1531,7 @@ async function runCellsCommand(ctx: CrmCliContext): Promise<void> {
1492
1531
  entryId: entryId as any,
1493
1532
  fieldName,
1494
1533
  value,
1534
+ ...runContextArgs(ctx),
1495
1535
  }),
1496
1536
  );
1497
1537
  return;
@@ -1538,6 +1578,7 @@ async function runCellsCommand(ctx: CrmCliContext): Promise<void> {
1538
1578
  entryId: entryId as any,
1539
1579
  fieldName,
1540
1580
  value,
1581
+ ...runContextArgs(ctx),
1541
1582
  }),
1542
1583
  );
1543
1584
  return;
@@ -1664,6 +1705,7 @@ async function runBatchCommand(ctx: CrmCliContext): Promise<void> {
1664
1705
  ctx,
1665
1706
  await callMutation(ctx, api.batch.apply, {
1666
1707
  ops: ops as any,
1708
+ ...runContextArgs(ctx),
1667
1709
  ...(idempotencyKey ? { idempotencyKey } : {}),
1668
1710
  } as any),
1669
1711
  );
@@ -1804,6 +1846,7 @@ async function runTransactionCommand(ctx: CrmCliContext): Promise<void> {
1804
1846
  ctx,
1805
1847
  await callMutation(ctx, api.transaction.commit, {
1806
1848
  txnId: txnId as any,
1849
+ ...runContextArgs(ctx),
1807
1850
  }),
1808
1851
  );
1809
1852
  return;
@@ -1859,6 +1902,7 @@ async function runTransactionCommand(ctx: CrmCliContext): Promise<void> {
1859
1902
  ctx,
1860
1903
  await callMutation(ctx, api.transaction.commit, {
1861
1904
  txnId: begun.txnId as any,
1905
+ ...runContextArgs(ctx),
1862
1906
  }),
1863
1907
  );
1864
1908
  } else {
@@ -2637,6 +2681,7 @@ async function runPeopleCommand(ctx: CrmCliContext): Promise<void> {
2637
2681
  await callMutation(ctx, api.entries.create, {
2638
2682
  objectName: "people",
2639
2683
  data,
2684
+ ...runContextArgs(ctx),
2640
2685
  }),
2641
2686
  );
2642
2687
  return;
@@ -2701,6 +2746,7 @@ async function runCompaniesCommand(ctx: CrmCliContext): Promise<void> {
2701
2746
  await callMutation(ctx, api.entries.create, {
2702
2747
  objectName: "company",
2703
2748
  data,
2749
+ ...runContextArgs(ctx),
2704
2750
  }),
2705
2751
  );
2706
2752
  return;
@@ -2895,6 +2941,7 @@ Fields (columns):
2895
2941
  Entries (rows):
2896
2942
  dench crm entries list <object> [--limit N] [--with-meta] [--json]
2897
2943
  dench crm entries get <object> <entryId>
2944
+ dench crm history <object> <entryId> [--include-system] [--limit N] [--cursor CURSOR] [--json]
2898
2945
  dench crm entries create <object> --data '{"Field":"Value",...}' (alias: --fields) [--allow-unknown]
2899
2946
  dench crm entries create-many <object> --data '[{"Field":"Value",...}]' [--file rows.json] [--idempotency-key <key>] [--allow-unknown]
2900
2947
  dench crm entries update <object> <entryId> --data '{...}' (alias: --fields) [--allow-unknown]
package/fs-daemon.ts CHANGED
@@ -15,8 +15,10 @@
15
15
  * `lastSyncedHash`, we download from Convex Storage to /workspace.
16
16
  * • Per-path 300ms trailing debounce on uploads (batches tight write
17
17
  * loops like editor saves).
18
- * • Optimistic conflict via `expectedPreviousHash`. On 409, we fetch
19
- * latest, write to `<path>.conflicted-<ts>`, surface in UI.
18
+ * • Optimistic conflict via `expectedPreviousHash`. On 409, we stash the
19
+ * local copy at the next free `<name> (N).<ext>` sibling (reusing an
20
+ * existing sibling that already holds the same bytes), then re-pull the
21
+ * latest canonical version. Surfaced in UI.
20
22
  * • Per-path circuit breaker: if a path syncs >5 times in 10s, pause
21
23
  * sync on that path and log loudly to stderr (the live state of
22
24
  * active breakers is exposed via `dench fs status`).
@@ -58,7 +60,7 @@ import {
58
60
  unlink,
59
61
  writeFile,
60
62
  } from "node:fs/promises";
61
- import { dirname, relative } from "node:path";
63
+ import { basename, dirname, extname, join, relative } from "node:path";
62
64
  import { ConvexClient, ConvexHttpClient } from "convex/browser";
63
65
  import { makeFunctionReference } from "convex/server";
64
66
  import { decodeInlineTextOrUndefined } from "./fs-daemon-binary";
@@ -149,6 +151,56 @@ function sha256(bytes: Uint8Array): string {
149
151
  return createHash("sha256").update(bytes).digest("hex");
150
152
  }
151
153
 
154
+ // Stash a conflicting local copy at a sibling path, using an incrementing
155
+ // suffix (`foo (1).txt`, `foo (2).txt`, …) instead of a timestamp so
156
+ // repeated conflicts produce a clean, stable series rather than an
157
+ // ever-growing pile of `.conflicted-<ts>` files. The file is written
158
+ // locally; the daemon's normal watcher path syncs it to Convex like any
159
+ // other file. Returns the local path written.
160
+ //
161
+ // Two properties keep the deterministic suffix safe:
162
+ // • Content-aware reuse — if a `(N)` slot already holds these exact bytes,
163
+ // the copy is already stashed, so we return it instead of minting yet
164
+ // another sibling. This is what keeps repeated conflicts on the same
165
+ // unsynced content from piling up (the whole point of the change), and
166
+ // it also means an unrelated remote `(N)` file mirrored to local disk is
167
+ // skipped rather than overwritten.
168
+ // • Atomic claim — otherwise we write with O_EXCL (`wx`), so two handlers
169
+ // racing for the same slot can't clobber each other; the loser gets
170
+ // EEXIST and advances to the next N.
171
+ async function stashConflictCopy(args: {
172
+ absPath: string;
173
+ bytes: Uint8Array;
174
+ hash: string;
175
+ }): Promise<string> {
176
+ const { absPath, bytes, hash } = args;
177
+ const dir = dirname(absPath);
178
+ const ext = extname(absPath);
179
+ const stem = basename(absPath, ext);
180
+ for (let i = 1; i <= 1000; i++) {
181
+ const candidate = join(dir, `${stem} (${i})${ext}`);
182
+ const existing = await readFile(candidate).catch(() => null);
183
+ if (existing) {
184
+ // Already holds our bytes → nothing to do. Different content → this
185
+ // slot belongs to something else, try the next one.
186
+ if (sha256(existing) === hash) return candidate;
187
+ continue;
188
+ }
189
+ try {
190
+ await writeFile(candidate, bytes, { flag: "wx" });
191
+ return candidate;
192
+ } catch (error) {
193
+ if ((error as NodeJS.ErrnoException).code === "EEXIST") continue;
194
+ throw error;
195
+ }
196
+ }
197
+ // Every increment up to the cap is taken — fall back to a timestamp, which
198
+ // is effectively unique, so we still stash the bytes somewhere.
199
+ const fallback = join(dir, `${stem} (${Date.now()})${ext}`);
200
+ await writeFile(fallback, bytes);
201
+ return fallback;
202
+ }
203
+
152
204
  function normalizePosixWorkspacePath(input: string): string {
153
205
  let cleaned = input.replace(/\\/g, "/").replace(/\/+/g, "/");
154
206
  if (cleaned.startsWith("/workspace/")) {
@@ -772,8 +824,7 @@ async function runDaemon(daemonArgs: Args): Promise<void> {
772
824
  if (result.versionConflict) {
773
825
  // Stash the local copy in a sibling file, then re-pull the remote
774
826
  // version into the canonical path.
775
- const conflicted = `${absPath}.conflicted-${Date.now()}`;
776
- await writeFile(conflicted, bytes);
827
+ const conflicted = await stashConflictCopy({ absPath, bytes, hash });
777
828
  console.warn(
778
829
  `[dench-fs-daemon] version conflict for ${posixPath}; local copy stashed at ${conflicted}`,
779
830
  );
@@ -716,6 +716,37 @@ const rawApiOperations = [
716
716
  responseSchema: anyResponse,
717
717
  backend: convex("query", "functions/crm/entries:get"),
718
718
  }),
719
+ op({
720
+ id: "crm.entries.history",
721
+ group: "crm",
722
+ summary: "List a CRM entry's audit history.",
723
+ cli: "dench crm history",
724
+ method: "GET",
725
+ path: "/crm/objects/{objectName}/entries/{entryId}/history",
726
+ requestSchema: noBody,
727
+ responseSchema: anyResponse,
728
+ backend: {
729
+ ...convex("query", "functions/crm/history:listForEntry"),
730
+ transformArgs: (input) => {
731
+ const limit =
732
+ typeof input.limit === "number"
733
+ ? Math.min(Math.max(Math.floor(input.limit), 1), 100)
734
+ : 50;
735
+ return {
736
+ objectName: input.objectName,
737
+ entryId: input.entryId,
738
+ includeSystem: input.includeSystem === true,
739
+ paginationOpts: {
740
+ numItems: limit,
741
+ cursor:
742
+ typeof input.cursor === "string" && input.cursor.length > 0
743
+ ? input.cursor
744
+ : null,
745
+ },
746
+ };
747
+ },
748
+ },
749
+ }),
719
750
  op({
720
751
  id: "crm.entries.create",
721
752
  group: "crm",
@@ -725,7 +756,9 @@ const rawApiOperations = [
725
756
  path: "/crm/objects/{objectName}/entries",
726
757
  requestSchema: anyObject,
727
758
  responseSchema: anyResponse,
728
- backend: convex("mutation", "functions/crm/entries:create"),
759
+ backend: convex("mutation", "functions/crm/entries:create", "bearer", {
760
+ includeRunId: true,
761
+ }),
729
762
  }),
730
763
  op({
731
764
  id: "crm.entries.createMany",
@@ -736,7 +769,9 @@ const rawApiOperations = [
736
769
  path: "/crm/objects/{objectName}/entries/batch",
737
770
  requestSchema: anyObject,
738
771
  responseSchema: anyResponse,
739
- backend: convex("mutation", "functions/crm/entries:createMany"),
772
+ backend: convex("mutation", "functions/crm/entries:createMany", "bearer", {
773
+ includeRunId: true,
774
+ }),
740
775
  }),
741
776
  op({
742
777
  id: "crm.entries.update",
@@ -747,7 +782,9 @@ const rawApiOperations = [
747
782
  path: "/crm/objects/{objectName}/entries/{entryId}",
748
783
  requestSchema: anyObject,
749
784
  responseSchema: anyResponse,
750
- backend: convex("mutation", "functions/crm/entries:update"),
785
+ backend: convex("mutation", "functions/crm/entries:update", "bearer", {
786
+ includeRunId: true,
787
+ }),
751
788
  }),
752
789
  op({
753
790
  id: "crm.entries.updateMany",
@@ -758,7 +795,9 @@ const rawApiOperations = [
758
795
  path: "/crm/objects/{objectName}/entries/batch",
759
796
  requestSchema: anyObject,
760
797
  responseSchema: anyResponse,
761
- backend: convex("mutation", "functions/crm/batch:apply"),
798
+ backend: convex("mutation", "functions/crm/batch:apply", "bearer", {
799
+ includeRunId: true,
800
+ }),
762
801
  }),
763
802
  op({
764
803
  id: "crm.entries.delete",
@@ -769,7 +808,9 @@ const rawApiOperations = [
769
808
  path: "/crm/objects/{objectName}/entries/{entryId}",
770
809
  requestSchema: anyObject,
771
810
  responseSchema: anyResponse,
772
- backend: convex("mutation", "functions/crm/entries:remove"),
811
+ backend: convex("mutation", "functions/crm/entries:remove", "bearer", {
812
+ includeRunId: true,
813
+ }),
773
814
  }),
774
815
  op({
775
816
  id: "crm.entries.bulkDelete",
@@ -780,7 +821,9 @@ const rawApiOperations = [
780
821
  path: "/crm/objects/{objectName}/entries/delete",
781
822
  requestSchema: anyObject,
782
823
  responseSchema: anyResponse,
783
- backend: convex("mutation", "functions/crm/entries:bulkDelete"),
824
+ backend: convex("mutation", "functions/crm/entries:bulkDelete", "bearer", {
825
+ includeRunId: true,
826
+ }),
784
827
  }),
785
828
 
786
829
  op({
@@ -803,7 +846,9 @@ const rawApiOperations = [
803
846
  path: "/crm/objects/{objectName}/entries/{entryId}/fields/{fieldName}",
804
847
  requestSchema: anyObject,
805
848
  responseSchema: anyResponse,
806
- backend: convex("mutation", "functions/crm/cells:set"),
849
+ backend: convex("mutation", "functions/crm/cells:set", "bearer", {
850
+ includeRunId: true,
851
+ }),
807
852
  }),
808
853
  op({
809
854
  id: "crm.cells.setMany",
@@ -814,7 +859,9 @@ const rawApiOperations = [
814
859
  path: "/crm/cells/batch",
815
860
  requestSchema: anyObject,
816
861
  responseSchema: anyResponse,
817
- backend: convex("mutation", "functions/crm/cells:setMany"),
862
+ backend: convex("mutation", "functions/crm/cells:setMany", "bearer", {
863
+ includeRunId: true,
864
+ }),
818
865
  }),
819
866
  op({
820
867
  id: "crm.cells.append",
@@ -825,7 +872,9 @@ const rawApiOperations = [
825
872
  path: "/crm/objects/{objectName}/entries/{entryId}/fields/{fieldName}/append",
826
873
  requestSchema: anyObject,
827
874
  responseSchema: anyResponse,
828
- backend: convex("mutation", "functions/crm/cells:append"),
875
+ backend: convex("mutation", "functions/crm/cells:append", "bearer", {
876
+ includeRunId: true,
877
+ }),
829
878
  }),
830
879
 
831
880
  op({
@@ -903,7 +952,9 @@ const rawApiOperations = [
903
952
  path: "/crm/batch",
904
953
  requestSchema: anyObject,
905
954
  responseSchema: anyResponse,
906
- backend: convex("mutation", "functions/crm/batch:apply"),
955
+ backend: convex("mutation", "functions/crm/batch:apply", "bearer", {
956
+ includeRunId: true,
957
+ }),
907
958
  }),
908
959
  op({
909
960
  id: "crm.transaction.begin",
@@ -936,7 +987,9 @@ const rawApiOperations = [
936
987
  path: "/crm/transactions/{txnId}/commit",
937
988
  requestSchema: anyObject,
938
989
  responseSchema: anyResponse,
939
- backend: convex("mutation", "functions/crm/transaction:commit"),
990
+ backend: convex("mutation", "functions/crm/transaction:commit", "bearer", {
991
+ includeRunId: true,
992
+ }),
940
993
  }),
941
994
  op({
942
995
  id: "crm.transaction.abort",
@@ -1325,6 +1325,17 @@ async function buildGatewayError(
1325
1325
  { status: response.status, code },
1326
1326
  );
1327
1327
  }
1328
+ if (
1329
+ response.status === 402 ||
1330
+ code === "on_demand_disabled" ||
1331
+ code === "insufficient_credits" ||
1332
+ code === "spend_limit_reached"
1333
+ ) {
1334
+ return new EnrichmentGatewayError(
1335
+ upstreamMessage ?? "AI credits exhausted.",
1336
+ { status: response.status, code },
1337
+ );
1338
+ }
1328
1339
 
1329
1340
  const detail = upstreamMessage || rawText || response.statusText;
1330
1341
  const truncated = detail.length > 500 ? `${detail.slice(0, 500)}...` : detail;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dench.com/cli",
3
- "version": "2.3.2",
3
+ "version": "2.4.1",
4
4
  "description": "Dench agent workspace CLI. v2 unifies auth behind `dench signin`; the legacy `dench login` / `dench onboard` / `dench setup` / `dench what-can-i-do` / `dench register` commands now error with a redirect.",
5
5
  "type": "module",
6
6
  "bin": {