@dench.com/cli 0.4.3 → 0.4.4

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/chat.ts CHANGED
@@ -119,10 +119,10 @@ function chatHelp(): void {
119
119
  console.log(`Usage: dench chat <subcommand>
120
120
 
121
121
  List past chat threads (own + shared in this workspace):
122
- dench chat list [--query <substr>] [--limit N] [--include-archived] [--json]
122
+ dench chat list [--query <substr>] [--limit N] [--include-archived] [--include-automated] [--json]
123
123
 
124
124
  Full-text search across past chat messages (own + shared):
125
- dench chat search "<query>" [--limit N] [--include-current] [--json]
125
+ dench chat search "<query>" [--limit N] [--include-current] [--include-automated] [--json]
126
126
 
127
127
  Read messages from a specific past chat thread:
128
128
  dench chat read <thread-id> [--limit N] [--json]
@@ -134,12 +134,20 @@ Rename one or more past chat threads (own + shared):
134
134
  Up to 50 items per call. Titles are trimmed and clamped to 200 chars.
135
135
 
136
136
  PERMANENTLY delete one or more past chat threads (and all their messages):
137
- dench chat delete <thread-id> [<thread-id>...] [--yes] [--json]
138
- dench chat delete --ids id1,id2,id3 [--yes] [--json]
137
+ dench chat delete <thread-id> [<thread-id>...] [--yes] [--include-automated] [--json]
138
+ dench chat delete --ids id1,id2,id3 [--yes] [--include-automated] [--json]
139
139
  Up to 25 ids per call. IRREVERSIBLE — the messages are removed too.
140
140
  Without --yes, the CLI prompts for confirmation when stdin is a TTY.
141
141
  In non-interactive contexts (sandboxes, scripts), --yes is required.
142
142
 
143
+ Fire-and-forget: deletion runs in the background. The CLI returns
144
+ \`{scheduled, rejected, total}\` immediately; threads disappear from
145
+ the sidebar live as each per-thread mutation completes.
146
+
147
+ AUTOMATION THREADS PROTECTED: cron-spawned routine threads and
148
+ heartbeat ticks are filtered out of \`list\` / \`search\` and the
149
+ server REFUSES to delete them unless \`--include-automated\` is set.
150
+
143
151
  Spawn a new chat-turn workflow (same agent loop as the web UI):
144
152
  dench chat new "<prompt>" [--model <m>] [--file-context <path>] [--visibility private|shared] [--title T] [--yolo] [--follow] [--json]
145
153
  dench chat send <thread-id> "<message>" [--model <m>] [--yolo] [--follow] [--json]
@@ -209,18 +217,21 @@ async function runChatListCommand(ctx: ChatCliContext): Promise<void> {
209
217
  const titleQuery = getFlag(ctx.args, "--query");
210
218
  const limit = parseLimit(getFlag(ctx.args, "--limit"));
211
219
  const includeArchived = hasFlag(ctx.args, "--include-archived");
220
+ const includeAutomated = hasFlag(ctx.args, "--include-automated");
212
221
  out(
213
222
  ctx,
214
223
  await callQuery(ctx, api.listPastChats, {
215
224
  titleQuery,
216
225
  limit,
217
226
  includeArchived,
227
+ includeAutomated,
218
228
  }),
219
229
  );
220
230
  }
221
231
 
222
232
  async function runChatSearchCommand(ctx: ChatCliContext): Promise<void> {
223
233
  const includeCurrent = hasFlag(ctx.args, "--include-current");
234
+ const includeAutomated = hasFlag(ctx.args, "--include-automated");
224
235
  const limit = parseLimit(getFlag(ctx.args, "--limit"));
225
236
  const queryParts: string[] = [];
226
237
  while (ctx.args.length > 0) {
@@ -231,7 +242,7 @@ async function runChatSearchCommand(ctx: ChatCliContext): Promise<void> {
231
242
  const query = queryParts.join(" ").trim();
232
243
  if (!query) {
233
244
  throw new ChatCliError(
234
- 'Usage: dench chat search "<query>" [--limit N] [--include-current]',
245
+ 'Usage: dench chat search "<query>" [--limit N] [--include-current] [--include-automated]',
235
246
  );
236
247
  }
237
248
  out(
@@ -239,6 +250,7 @@ async function runChatSearchCommand(ctx: ChatCliContext): Promise<void> {
239
250
  await callQuery(ctx, api.searchPastChats, {
240
251
  query,
241
252
  limit,
253
+ includeAutomated,
242
254
  // The server defaults excludeThreadId to undefined; the agent path
243
255
  // sets it explicitly. CLI users are not "in" a thread, so we pass
244
256
  // undefined unless --include-current is intentionally set (no-op).
@@ -378,6 +390,7 @@ async function promptYesNo(question: string): Promise<boolean> {
378
390
  async function runChatDeleteCommand(ctx: ChatCliContext): Promise<void> {
379
391
  const idsFlag = getFlag(ctx.args, "--ids");
380
392
  const skipConfirm = hasFlag(ctx.args, "--yes") || hasFlag(ctx.args, "-y");
393
+ const includeAutomated = hasFlag(ctx.args, "--include-automated");
381
394
  const collected: string[] = [];
382
395
  if (idsFlag) {
383
396
  for (const part of idsFlag.split(",")) {
@@ -405,6 +418,9 @@ async function runChatDeleteCommand(ctx: ChatCliContext): Promise<void> {
405
418
  "Usage: dench chat delete <thread-id> [<thread-id>...] (or --ids id1,id2,...)",
406
419
  );
407
420
  }
421
+ // Server caps each call at 25 ids. Each accepted id is scheduled for
422
+ // background deletion (fire-and-forget), so the cap exists to bound
423
+ // the per-call validation work, not the deletion volume itself.
408
424
  if (threadIds.length > 25) {
409
425
  throw new ChatCliError(
410
426
  `Too many thread ids (${threadIds.length}); the server caps each call at 25. Run multiple invocations.`,
@@ -416,17 +432,26 @@ async function runChatDeleteCommand(ctx: ChatCliContext): Promise<void> {
416
432
  "Refusing to delete without --yes in non-interactive mode (no TTY).",
417
433
  );
418
434
  }
435
+ const automatedSuffix = includeAutomated
436
+ ? " (INCLUDING ROUTINES / HEARTBEATS — automated thread protection bypassed)"
437
+ : "";
419
438
  const ok = await promptYesNo(
420
- `PERMANENTLY delete ${threadIds.length} chat${
439
+ `PERMANENTLY queue delete of ${threadIds.length} chat${
421
440
  threadIds.length === 1 ? "" : "s"
422
- } and all their messages?`,
441
+ } and all their messages${automatedSuffix}?`,
423
442
  );
424
443
  if (!ok) {
425
444
  console.error("Aborted.");
426
445
  return;
427
446
  }
428
447
  }
429
- out(ctx, await callMutation(ctx, api.deleteThreads, { threadIds }));
448
+ out(
449
+ ctx,
450
+ await callMutation(ctx, api.deleteThreads, {
451
+ threadIds,
452
+ includeAutomated,
453
+ }),
454
+ );
430
455
  }
431
456
 
432
457
  /**
package/crm.ts CHANGED
@@ -37,6 +37,21 @@ import {
37
37
  parseJson as parseJsonRaw,
38
38
  shift as shiftRaw,
39
39
  } from "./lib/cli-args";
40
+ import {
41
+ autoDetectInputField,
42
+ detectEnrichmentCategory,
43
+ extractEnrichmentValue,
44
+ findEnrichmentColumn,
45
+ getRequiredFieldsForApolloPath,
46
+ type EnrichmentCategory,
47
+ type EnrichmentColumnDef,
48
+ type FieldCandidate,
49
+ } from "./lib/crm-enrichment";
50
+ import {
51
+ enrichCompany,
52
+ EnrichmentGatewayError,
53
+ enrichPersonByIdentifier,
54
+ } from "./lib/enrichment-gateway";
40
55
 
41
56
  type JsonRecord = Record<string, unknown>;
42
57
 
@@ -333,6 +348,9 @@ const api = {
333
348
  requestCellEnrichment: makeFunctionReference<"mutation">(
334
349
  "functions/crm/enrich:requestCellEnrichment",
335
350
  ),
351
+ applyCellEnrichmentResult: makeFunctionReference<"mutation">(
352
+ "functions/crm/enrich:applyCellEnrichmentResult",
353
+ ),
336
354
  requestObjectEnrichment: makeFunctionReference<"mutation">(
337
355
  "functions/crm/enrich:requestObjectEnrichment",
338
356
  ),
@@ -1463,6 +1481,334 @@ async function runSqlCommand(ctx: CrmCliContext): Promise<void> {
1463
1481
  );
1464
1482
  }
1465
1483
 
1484
+ // ── CRM enrichment gateway orchestration ─────────────────────────────────
1485
+
1486
+ type CrmFieldForEnrichment = FieldCandidate & {
1487
+ defaultValue?: string;
1488
+ enrichment?: {
1489
+ category: EnrichmentCategory;
1490
+ key: string;
1491
+ apolloPath: string;
1492
+ inputFieldName: string;
1493
+ };
1494
+ };
1495
+
1496
+ type CrmEntryForEnrichment = {
1497
+ id: string;
1498
+ fields: Record<string, unknown>;
1499
+ };
1500
+
1501
+ type EnrichmentTarget = {
1502
+ category: EnrichmentCategory;
1503
+ column: EnrichmentColumnDef;
1504
+ requiredFields: string[];
1505
+ inputField: CrmFieldForEnrichment;
1506
+ };
1507
+
1508
+ type CellEnrichmentArgs = {
1509
+ objectName: string;
1510
+ entryId: string;
1511
+ fieldName: string;
1512
+ provider?: string;
1513
+ category?: EnrichmentCategory;
1514
+ inputFieldName?: string;
1515
+ apolloPath?: string;
1516
+ overwrite?: boolean;
1517
+ };
1518
+
1519
+ type ObjectEnrichmentArgs = Omit<CellEnrichmentArgs, "entryId"> & {
1520
+ missingOnly: boolean;
1521
+ limit?: number;
1522
+ };
1523
+
1524
+ function parseCategoryFlag(value: string | undefined): EnrichmentCategory | undefined {
1525
+ if (value === undefined) return undefined;
1526
+ if (value === "people" || value === "company") return value;
1527
+ throw new CrmCliError("--category must be people or company");
1528
+ }
1529
+
1530
+ function parsePositiveInt(flag: string, value: string | undefined): number | undefined {
1531
+ if (value === undefined) return undefined;
1532
+ const parsed = Number(value);
1533
+ if (!Number.isInteger(parsed) || parsed < 1) {
1534
+ throw new CrmCliError(`${flag} must be a positive integer`);
1535
+ }
1536
+ return parsed;
1537
+ }
1538
+
1539
+ function hasCellValue(value: unknown): boolean {
1540
+ return value !== undefined && value !== null && value !== "";
1541
+ }
1542
+
1543
+ function asFields(value: unknown): CrmFieldForEnrichment[] {
1544
+ if (!Array.isArray(value)) return [];
1545
+ return value
1546
+ .filter((field): field is CrmFieldForEnrichment => {
1547
+ if (!field || typeof field !== "object") return false;
1548
+ const candidate = field as Record<string, unknown>;
1549
+ return typeof candidate.name === "string" && typeof candidate.type === "string";
1550
+ })
1551
+ .map((field) => ({
1552
+ id: typeof field.id === "string" ? field.id : undefined,
1553
+ name: field.name,
1554
+ type: field.type,
1555
+ defaultValue:
1556
+ typeof field.defaultValue === "string" ? field.defaultValue : undefined,
1557
+ enrichment: readFieldEnrichment(field),
1558
+ }));
1559
+ }
1560
+
1561
+ function readFieldEnrichment(
1562
+ field: Record<string, unknown>,
1563
+ ): CrmFieldForEnrichment["enrichment"] {
1564
+ const enrichment = field.enrichment;
1565
+ if (!enrichment || typeof enrichment !== "object") return undefined;
1566
+ const candidate = enrichment as Record<string, unknown>;
1567
+ const category = parseCategoryFlag(
1568
+ typeof candidate.category === "string" ? candidate.category : undefined,
1569
+ );
1570
+ const key = typeof candidate.key === "string" ? candidate.key : undefined;
1571
+ const apolloPath =
1572
+ typeof candidate.apolloPath === "string" ? candidate.apolloPath : undefined;
1573
+ const inputFieldName =
1574
+ typeof candidate.inputFieldName === "string"
1575
+ ? candidate.inputFieldName
1576
+ : undefined;
1577
+ if (!category || !key || !apolloPath || !inputFieldName) return undefined;
1578
+ return { category, key, apolloPath, inputFieldName };
1579
+ }
1580
+
1581
+ function asEntry(value: unknown): CrmEntryForEnrichment | null {
1582
+ if (!value || typeof value !== "object") return null;
1583
+ const candidate = value as Record<string, unknown>;
1584
+ if (typeof candidate.id !== "string") return null;
1585
+ const fields =
1586
+ candidate.fields && typeof candidate.fields === "object"
1587
+ ? (candidate.fields as Record<string, unknown>)
1588
+ : {};
1589
+ return { id: candidate.id, fields };
1590
+ }
1591
+
1592
+ function asEntries(value: unknown): CrmEntryForEnrichment[] {
1593
+ if (!Array.isArray(value)) return [];
1594
+ return value.map(asEntry).filter((entry): entry is CrmEntryForEnrichment => !!entry);
1595
+ }
1596
+
1597
+ function resolveEnrichmentColumn(args: {
1598
+ category: EnrichmentCategory;
1599
+ fieldName: string;
1600
+ apolloPath?: string;
1601
+ }): EnrichmentColumnDef {
1602
+ const catalogColumn = findEnrichmentColumn({
1603
+ category: args.category,
1604
+ apolloPath: args.apolloPath,
1605
+ label: args.apolloPath ? undefined : args.fieldName,
1606
+ key: args.apolloPath ? undefined : args.fieldName,
1607
+ });
1608
+ if (catalogColumn) return catalogColumn;
1609
+ if (args.apolloPath) {
1610
+ return {
1611
+ label: args.fieldName,
1612
+ key: args.apolloPath,
1613
+ fieldType: "text",
1614
+ apolloPath: args.apolloPath,
1615
+ requiredFields: getRequiredFieldsForApolloPath(
1616
+ args.category,
1617
+ args.apolloPath,
1618
+ ),
1619
+ };
1620
+ }
1621
+ throw new CrmCliError(
1622
+ `No enrichment catalog column found for "${args.fieldName}". Pass --apollo-path for a custom gateway response path.`,
1623
+ );
1624
+ }
1625
+
1626
+ async function resolveEnrichmentTarget(
1627
+ ctx: CrmCliContext,
1628
+ args: {
1629
+ objectName: string;
1630
+ fieldName: string;
1631
+ category?: EnrichmentCategory;
1632
+ inputFieldName?: string;
1633
+ apolloPath?: string;
1634
+ },
1635
+ ): Promise<EnrichmentTarget> {
1636
+ const fields = asFields(
1637
+ await callQuery(ctx, api.fields.list, { objectName: args.objectName }),
1638
+ );
1639
+ const outputField = fields.find((field) => field.name === args.fieldName);
1640
+ if (!outputField) {
1641
+ throw new CrmCliError(`Field "${args.fieldName}" not found`);
1642
+ }
1643
+
1644
+ const category =
1645
+ args.category ??
1646
+ outputField.enrichment?.category ??
1647
+ detectEnrichmentCategory(args.objectName) ??
1648
+ undefined;
1649
+ if (!category) {
1650
+ throw new CrmCliError(
1651
+ "Could not infer enrichment category. Pass --category people|company.",
1652
+ );
1653
+ }
1654
+
1655
+ const column = resolveEnrichmentColumn({
1656
+ category,
1657
+ fieldName: args.fieldName,
1658
+ apolloPath: args.apolloPath ?? outputField.enrichment?.apolloPath,
1659
+ });
1660
+
1661
+ const inputFieldName = args.inputFieldName ?? outputField.enrichment?.inputFieldName;
1662
+ const inputField = inputFieldName
1663
+ ? fields.find((field) => field.name === inputFieldName)
1664
+ : autoDetectInputField(category, fields);
1665
+ if (!inputField) {
1666
+ throw new CrmCliError(
1667
+ inputFieldName
1668
+ ? `Input field "${inputFieldName}" not found`
1669
+ : "Could not infer input field. Pass --input-field <field>.",
1670
+ );
1671
+ }
1672
+
1673
+ return {
1674
+ category,
1675
+ column,
1676
+ requiredFields: column.requiredFields,
1677
+ inputField,
1678
+ };
1679
+ }
1680
+
1681
+ async function callEnrichmentGatewayForEntry(args: {
1682
+ target: EnrichmentTarget;
1683
+ entry: CrmEntryForEnrichment;
1684
+ }): Promise<Record<string, unknown>> {
1685
+ const input = args.entry.fields[args.target.inputField.name];
1686
+ if (!hasCellValue(input)) {
1687
+ throw new CrmCliError(
1688
+ `Entry ${args.entry.id} has no value for input field "${args.target.inputField.name}"`,
1689
+ );
1690
+ }
1691
+ const inputValue = String(input);
1692
+ if (args.target.category === "people") {
1693
+ return await enrichPersonByIdentifier(inputValue, args.target.requiredFields);
1694
+ }
1695
+ return await enrichCompany({
1696
+ domain: inputValue,
1697
+ requiredFields: args.target.requiredFields,
1698
+ });
1699
+ }
1700
+
1701
+ async function enrichOneEntry(
1702
+ ctx: CrmCliContext,
1703
+ args: CellEnrichmentArgs & {
1704
+ target: EnrichmentTarget;
1705
+ entry: CrmEntryForEnrichment;
1706
+ },
1707
+ ): Promise<Record<string, unknown>> {
1708
+ try {
1709
+ const payload = await callEnrichmentGatewayForEntry({
1710
+ target: args.target,
1711
+ entry: args.entry,
1712
+ });
1713
+ const value = extractEnrichmentValue(payload, args.target.column);
1714
+ if (value === null) {
1715
+ return {
1716
+ entryId: args.entry.id,
1717
+ ok: false,
1718
+ reason: "value_not_found",
1719
+ };
1720
+ }
1721
+ const writeResult = (await callMutation(
1722
+ ctx,
1723
+ api.enrich.applyCellEnrichmentResult,
1724
+ {
1725
+ objectName: args.objectName,
1726
+ entryId: args.entry.id,
1727
+ fieldName: args.fieldName,
1728
+ value,
1729
+ provider: args.provider ?? "gateway",
1730
+ overwrite: args.overwrite,
1731
+ },
1732
+ )) as Record<string, unknown>;
1733
+ return {
1734
+ entryId: args.entry.id,
1735
+ ok: writeResult.ok === true,
1736
+ value,
1737
+ ...writeResult,
1738
+ };
1739
+ } catch (error) {
1740
+ if (error instanceof EnrichmentGatewayError || error instanceof CrmCliError) {
1741
+ return {
1742
+ entryId: args.entry.id,
1743
+ ok: false,
1744
+ reason:
1745
+ error instanceof EnrichmentGatewayError
1746
+ ? (error.code ?? "enrichment_failed")
1747
+ : "enrichment_failed",
1748
+ message: error.message,
1749
+ };
1750
+ }
1751
+ throw error;
1752
+ }
1753
+ }
1754
+
1755
+ async function enrichCellThroughGateway(
1756
+ ctx: CrmCliContext,
1757
+ args: CellEnrichmentArgs,
1758
+ ): Promise<Record<string, unknown>> {
1759
+ const target = await resolveEnrichmentTarget(ctx, args);
1760
+ const entry = asEntry(
1761
+ await callQuery(ctx, api.entries.get, {
1762
+ objectName: args.objectName,
1763
+ entryId: args.entryId,
1764
+ }),
1765
+ );
1766
+ if (!entry) throw new CrmCliError("Entry not found");
1767
+ const result = await enrichOneEntry(ctx, { ...args, target, entry });
1768
+ return {
1769
+ objectName: args.objectName,
1770
+ fieldName: args.fieldName,
1771
+ inputFieldName: target.inputField.name,
1772
+ category: target.category,
1773
+ apolloPath: target.column.apolloPath,
1774
+ result,
1775
+ };
1776
+ }
1777
+
1778
+ async function enrichObjectThroughGateway(
1779
+ ctx: CrmCliContext,
1780
+ args: ObjectEnrichmentArgs,
1781
+ ): Promise<Record<string, unknown>> {
1782
+ const target = await resolveEnrichmentTarget(ctx, args);
1783
+ const entries = asEntries(
1784
+ await callQuery(ctx, api.entries.list, {
1785
+ objectName: args.objectName,
1786
+ limit: args.limit ?? 100,
1787
+ }),
1788
+ );
1789
+ const selected = entries.filter((entry) => {
1790
+ if (!args.missingOnly) return true;
1791
+ return !hasCellValue(entry.fields[args.fieldName]);
1792
+ });
1793
+ const results: Record<string, unknown>[] = [];
1794
+ for (const entry of selected) {
1795
+ results.push(await enrichOneEntry(ctx, { ...args, target, entry }));
1796
+ }
1797
+ return {
1798
+ objectName: args.objectName,
1799
+ fieldName: args.fieldName,
1800
+ inputFieldName: target.inputField.name,
1801
+ category: target.category,
1802
+ apolloPath: target.column.apolloPath,
1803
+ total: entries.length,
1804
+ selected: selected.length,
1805
+ enriched: results.filter((result) => result.ok === true).length,
1806
+ skipped: entries.length - selected.length,
1807
+ failed: results.filter((result) => result.ok !== true).length,
1808
+ results,
1809
+ };
1810
+ }
1811
+
1466
1812
  // ── People + companies shortcuts ─────────────────────────────────────────
1467
1813
  //
1468
1814
  // These are thin wrappers around the standard objects/entries surface,
@@ -1511,20 +1857,22 @@ async function runPeopleCommand(ctx: CrmCliContext): Promise<void> {
1511
1857
  case "enrich": {
1512
1858
  const entryId = shift(ctx.args, "person entry id");
1513
1859
  const provider = getFlag(ctx.args, "--provider");
1514
- const fieldName = getFlag(ctx.args, "--field") ?? "Email Address";
1860
+ const fieldName = getFlag(ctx.args, "--field") ?? "LinkedIn URL";
1861
+ const inputFieldName = getFlag(ctx.args, "--input-field");
1862
+ const overwrite = hasFlag(ctx.args, "--overwrite");
1515
1863
  assertNoUnknownFlags(ctx.args, "crm people enrich");
1516
1864
  out(
1517
1865
  ctx,
1518
- await callMutation(ctx, api.enrich.requestObjectEnrichment, {
1866
+ await enrichCellThroughGateway(ctx, {
1519
1867
  objectName: "people",
1868
+ entryId,
1520
1869
  fieldName,
1521
1870
  provider,
1871
+ category: "people",
1872
+ inputFieldName,
1873
+ overwrite,
1522
1874
  }),
1523
1875
  );
1524
- // Don't reference entryId in the request — Phase 3 v1 enrichment is
1525
- // object-level only; cell-level lands once gateway enrichment ships.
1526
- // Print entryId in the result so the caller has context.
1527
- out(ctx, { entryId });
1528
1876
  return;
1529
1877
  }
1530
1878
  default:
@@ -1573,17 +1921,22 @@ async function runCompaniesCommand(ctx: CrmCliContext): Promise<void> {
1573
1921
  case "enrich": {
1574
1922
  const entryId = shift(ctx.args, "company entry id");
1575
1923
  const provider = getFlag(ctx.args, "--provider");
1576
- const fieldName = getFlag(ctx.args, "--field") ?? "Domain";
1924
+ const fieldName = getFlag(ctx.args, "--field") ?? "Company Name";
1925
+ const inputFieldName = getFlag(ctx.args, "--input-field") ?? "Domain";
1926
+ const overwrite = hasFlag(ctx.args, "--overwrite");
1577
1927
  assertNoUnknownFlags(ctx.args, "crm companies enrich");
1578
1928
  out(
1579
1929
  ctx,
1580
- await callMutation(ctx, api.enrich.requestObjectEnrichment, {
1930
+ await enrichCellThroughGateway(ctx, {
1581
1931
  objectName: "company",
1932
+ entryId,
1582
1933
  fieldName,
1583
1934
  provider,
1935
+ category: "company",
1936
+ inputFieldName,
1937
+ overwrite,
1584
1938
  }),
1585
1939
  );
1586
- out(ctx, { entryId });
1587
1940
  return;
1588
1941
  }
1589
1942
  default:
@@ -1601,14 +1954,22 @@ async function runEnrichCommand(ctx: CrmCliContext): Promise<void> {
1601
1954
  const entryId = shift(ctx.args, "entry id");
1602
1955
  const fieldName = shift(ctx.args, "field name");
1603
1956
  const provider = getFlag(ctx.args, "--provider");
1957
+ const inputFieldName = getFlag(ctx.args, "--input-field");
1958
+ const category = parseCategoryFlag(getFlag(ctx.args, "--category"));
1959
+ const apolloPath = getFlag(ctx.args, "--apollo-path");
1960
+ const overwrite = hasFlag(ctx.args, "--overwrite");
1604
1961
  assertNoUnknownFlags(ctx.args, "crm enrich cell");
1605
1962
  out(
1606
1963
  ctx,
1607
- await callMutation(ctx, api.enrich.requestCellEnrichment, {
1964
+ await enrichCellThroughGateway(ctx, {
1608
1965
  objectName,
1609
- entryId: entryId as any,
1966
+ entryId,
1610
1967
  fieldName,
1611
1968
  provider,
1969
+ category,
1970
+ inputFieldName,
1971
+ apolloPath,
1972
+ overwrite,
1612
1973
  }),
1613
1974
  );
1614
1975
  return;
@@ -1618,19 +1979,29 @@ async function runEnrichCommand(ctx: CrmCliContext): Promise<void> {
1618
1979
  const fieldName = getFlag(ctx.args, "--field");
1619
1980
  const missingOnly = !hasFlag(ctx.args, "--all");
1620
1981
  const provider = getFlag(ctx.args, "--provider");
1982
+ const inputFieldName = getFlag(ctx.args, "--input-field");
1983
+ const category = parseCategoryFlag(getFlag(ctx.args, "--category"));
1984
+ const apolloPath = getFlag(ctx.args, "--apollo-path");
1985
+ const overwrite = hasFlag(ctx.args, "--overwrite");
1986
+ const limit = parsePositiveInt("--limit", getFlag(ctx.args, "--limit"));
1621
1987
  assertNoUnknownFlags(ctx.args, "crm enrich object");
1622
1988
  if (!fieldName) {
1623
1989
  throw new CrmCliError(
1624
- "dench crm enrich object <name> --field <field> [--provider ...] [--all]",
1990
+ "dench crm enrich object <name> --field <field> [--input-field <field>] [--category people|company] [--apollo-path <path>] [--limit N] [--provider ...] [--all] [--overwrite]",
1625
1991
  );
1626
1992
  }
1627
1993
  out(
1628
1994
  ctx,
1629
- await callMutation(ctx, api.enrich.requestObjectEnrichment, {
1995
+ await enrichObjectThroughGateway(ctx, {
1630
1996
  objectName,
1631
1997
  fieldName,
1632
1998
  provider,
1999
+ category,
2000
+ inputFieldName,
2001
+ apolloPath,
1633
2002
  missingOnly,
2003
+ overwrite,
2004
+ limit,
1634
2005
  }),
1635
2006
  );
1636
2007
  return;
@@ -1758,14 +2129,14 @@ SQL escape hatch (subset; flat SELECTs only):
1758
2129
  People + companies shortcuts:
1759
2130
  dench crm people search '<query>'
1760
2131
  dench crm people upsert --data '{"Email Address":"jane@example.com",...}' (alias: --fields)
1761
- dench crm people enrich <entryId> [--provider apollo]
2132
+ dench crm people enrich <entryId> [--field 'LinkedIn URL'] [--input-field 'Email Address']
1762
2133
  dench crm companies search '<query>'
1763
2134
  dench crm companies upsert --domain acme.com [--name Acme]
1764
- dench crm companies enrich <entryId>
2135
+ dench crm companies enrich <entryId> [--field 'Company Name'] [--input-field Domain]
1765
2136
 
1766
2137
  Cell / row / object enrichment:
1767
- dench crm enrich cell <object> <entryId> <field> [--provider ...]
1768
- dench crm enrich object <object> --field <field> [--provider ...] [--all]
2138
+ dench crm enrich cell <object> <entryId> <field> [--input-field <field>] [--category people|company] [--apollo-path <path>] [--overwrite]
2139
+ dench crm enrich object <object> --field <field> [--input-field <field>] [--category people|company] [--apollo-path <path>] [--limit N] [--all] [--overwrite]
1769
2140
 
1770
2141
  Reports (chart-ready JSON):
1771
2142
  dench crm reports generate '<name>' --object <object> --type pie|bar|line|table --group-by <field> [--metric count|sum --metric-field <field>]
package/dench CHANGED
@@ -1,5 +1,9 @@
1
1
  #!/usr/bin/env node
2
2
 
3
+ import { loadDevEnv } from "./lib/load-dev-env.mjs";
4
+
5
+ loadDevEnv(import.meta.url);
6
+
3
7
  const entrypoint = new URL("./dench.ts", import.meta.url).href;
4
8
 
5
9
  if (process.versions.bun) {
package/dench.mjs CHANGED
@@ -1,5 +1,9 @@
1
1
  #!/usr/bin/env node
2
2
 
3
+ import { loadDevEnv } from "./lib/load-dev-env.mjs";
4
+
5
+ loadDevEnv(import.meta.url);
6
+
3
7
  const entrypoint = new URL("./dench.ts", import.meta.url).href;
4
8
 
5
9
  if (process.versions.bun) {
package/dench.ts CHANGED
@@ -1809,6 +1809,30 @@ async function getRuntime() {
1809
1809
  });
1810
1810
  }
1811
1811
 
1812
+ // Sandbox session-mode path: workflow minting provisions a real
1813
+ // dch_agent_* token at sandbox-create time. Prefer it over apiKey so
1814
+ // agentWorkspace.* commands (Composio tools) work inside sandboxes.
1815
+ const sandboxAgentToken = process.env.DENCH_AGENT_SESSION_TOKEN?.trim();
1816
+ const sandboxConvexUrl = resolveApiKeyConvexUrl();
1817
+ if (sandboxAgentToken && sandboxConvexUrl) {
1818
+ const orgId = process.env.DENCH_ORG_ID?.trim();
1819
+ const orgSlug = process.env.DENCH_ORG_SLUG?.trim();
1820
+ const apiHost =
1821
+ process.env.DENCH_API_URL?.trim() ||
1822
+ process.env.DENCH_INTERNAL_BASE?.trim() ||
1823
+ host;
1824
+ return {
1825
+ mode: "session" as const,
1826
+ host: apiHost,
1827
+ client: new ConvexHttpClient(sandboxConvexUrl),
1828
+ sessionToken: sandboxAgentToken,
1829
+ organization:
1830
+ orgId && orgSlug
1831
+ ? { id: orgId, name: orgSlug, slug: orgSlug }
1832
+ : undefined,
1833
+ };
1834
+ }
1835
+
1812
1836
  // Sandbox / automation path: the unified Dench API key + Convex URL
1813
1837
  // baked into sandbox envVars (see
1814
1838
  // src/lib/secrets/sandbox-tokens.ts) is enough to drive every
package/fs-daemon.ts CHANGED
@@ -108,6 +108,7 @@ function shouldIgnore(relPath: string): boolean {
108
108
  const parts = relPath.split("/").filter(Boolean);
109
109
  const blockedDirs = new Set([
110
110
  ".chrome-profile",
111
+ ".dench",
111
112
  ".git",
112
113
  ".next",
113
114
  ".vercel",
package/home-sync ADDED
@@ -0,0 +1,10 @@
1
+ #!/usr/bin/env node
2
+
3
+ const entrypoint = new URL("./home-sync.ts", import.meta.url).href;
4
+
5
+ if (process.versions.bun) {
6
+ await import(entrypoint);
7
+ } else {
8
+ const { tsImport } = await import("tsx/esm/api");
9
+ await tsImport(entrypoint, import.meta.url);
10
+ }