@dench.com/cli 0.4.3 → 0.4.5
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 +33 -8
- package/crm.ts +388 -17
- package/dench +4 -0
- package/dench.mjs +4 -0
- package/dench.ts +24 -0
- package/fs-daemon.ts +1 -0
- package/home-sync +10 -0
- package/home-sync-lib.ts +185 -0
- package/home-sync.ts +243 -0
- package/lib/crm-enrichment.ts +431 -0
- package/lib/enrichment-gateway.ts +279 -0
- package/lib/load-dev-env.mjs +102 -0
- package/package.json +6 -2
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(
|
|
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") ?? "
|
|
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
|
|
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") ?? "
|
|
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
|
|
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
|
|
1964
|
+
await enrichCellThroughGateway(ctx, {
|
|
1608
1965
|
objectName,
|
|
1609
|
-
entryId
|
|
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
|
|
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> [--
|
|
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> [--
|
|
1768
|
-
dench crm enrich object <object> --field <field> [--
|
|
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
package/dench.mjs
CHANGED
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
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
|
+
}
|