@dench.com/cli 2.0.3 → 2.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/agent-config.ts +55 -13
- package/chat-spawn.ts +12 -2
- package/crm.ts +289 -31
- package/dench.ts +77 -117
- package/package.json +1 -1
- package/tools.ts +3 -1
package/agent-config.ts
CHANGED
|
@@ -34,11 +34,51 @@ import { mkdirSync, readFileSync, unlinkSync, writeFileSync } from "node:fs";
|
|
|
34
34
|
import { tmpdir } from "node:os";
|
|
35
35
|
import { join } from "node:path";
|
|
36
36
|
import type { ConvexHttpClient } from "convex/browser";
|
|
37
|
-
import {
|
|
37
|
+
import { makeFunctionReference } from "convex/server";
|
|
38
38
|
import { CliArgError, getFlag, hasFlag } from "./lib/cli-args";
|
|
39
39
|
|
|
40
40
|
class AgentConfigCliError extends Error {}
|
|
41
41
|
|
|
42
|
+
const api = {
|
|
43
|
+
functions: {
|
|
44
|
+
agentConfig: {
|
|
45
|
+
getAgentConfig: makeFunctionReference<"query">(
|
|
46
|
+
"functions/agentConfig:getAgentConfig",
|
|
47
|
+
),
|
|
48
|
+
updateIdentity: makeFunctionReference<"mutation">(
|
|
49
|
+
"functions/agentConfig:updateIdentity",
|
|
50
|
+
),
|
|
51
|
+
updateOrganisation: makeFunctionReference<"mutation">(
|
|
52
|
+
"functions/agentConfig:updateOrganisation",
|
|
53
|
+
),
|
|
54
|
+
updateMemory: makeFunctionReference<"mutation">(
|
|
55
|
+
"functions/agentConfig:updateMemory",
|
|
56
|
+
),
|
|
57
|
+
updateToolsNotes: makeFunctionReference<"mutation">(
|
|
58
|
+
"functions/agentConfig:updateToolsNotes",
|
|
59
|
+
),
|
|
60
|
+
updateUserProfile: makeFunctionReference<"mutation">(
|
|
61
|
+
"functions/agentConfig:updateUserProfile",
|
|
62
|
+
),
|
|
63
|
+
updateHeartbeat: makeFunctionReference<"mutation">(
|
|
64
|
+
"functions/agentConfig:updateHeartbeat",
|
|
65
|
+
),
|
|
66
|
+
updateBootstrapTemplate: makeFunctionReference<"mutation">(
|
|
67
|
+
"functions/agentConfig:updateBootstrapTemplate",
|
|
68
|
+
),
|
|
69
|
+
completeBootstrap: makeFunctionReference<"mutation">(
|
|
70
|
+
"functions/agentConfig:completeBootstrap",
|
|
71
|
+
),
|
|
72
|
+
setDefaultChatModelV2: makeFunctionReference<"mutation">(
|
|
73
|
+
"functions/agentConfig:setDefaultChatModelV2",
|
|
74
|
+
),
|
|
75
|
+
setThreadModelOverride: makeFunctionReference<"mutation">(
|
|
76
|
+
"functions/agentConfig:setThreadModelOverride",
|
|
77
|
+
),
|
|
78
|
+
},
|
|
79
|
+
},
|
|
80
|
+
};
|
|
81
|
+
|
|
42
82
|
type CliCtx = {
|
|
43
83
|
convex: ConvexHttpClient;
|
|
44
84
|
args: string[];
|
|
@@ -86,7 +126,9 @@ async function readContentFromStdin(): Promise<string> {
|
|
|
86
126
|
return new Promise((resolve, reject) => {
|
|
87
127
|
const chunks: Buffer[] = [];
|
|
88
128
|
process.stdin.on("data", (chunk) => chunks.push(Buffer.from(chunk)));
|
|
89
|
-
process.stdin.on("end", () =>
|
|
129
|
+
process.stdin.on("end", () =>
|
|
130
|
+
resolve(Buffer.concat(chunks).toString("utf8")),
|
|
131
|
+
);
|
|
90
132
|
process.stdin.on("error", reject);
|
|
91
133
|
});
|
|
92
134
|
}
|
|
@@ -150,7 +192,10 @@ async function loadAgentConfig(ctx: CliCtx): Promise<{
|
|
|
150
192
|
userProfile: { content: string; updatedAt: number } | null;
|
|
151
193
|
organizationId: string;
|
|
152
194
|
}> {
|
|
153
|
-
const result = (await runQuery(
|
|
195
|
+
const result = (await runQuery(
|
|
196
|
+
ctx,
|
|
197
|
+
api.functions.agentConfig.getAgentConfig,
|
|
198
|
+
)) as {
|
|
154
199
|
agentConfig: {
|
|
155
200
|
identity: { content: string; updatedAt: number } | null;
|
|
156
201
|
organisation: { content: string; updatedAt: number } | null;
|
|
@@ -212,14 +257,14 @@ async function handleSimpleMagicFile(
|
|
|
212
257
|
} else {
|
|
213
258
|
const config = await loadAgentConfig(ctx);
|
|
214
259
|
const initial = opts.perMember
|
|
215
|
-
? config.userProfile?.content ?? ""
|
|
260
|
+
? (config.userProfile?.content ?? "")
|
|
216
261
|
: opts.field === "memoryAggregate"
|
|
217
|
-
? config.agentConfig.memoryAggregate?.content ?? ""
|
|
262
|
+
? (config.agentConfig.memoryAggregate?.content ?? "")
|
|
218
263
|
: opts.field === "toolsNotes"
|
|
219
|
-
? config.agentConfig.toolsNotes?.content ?? ""
|
|
264
|
+
? (config.agentConfig.toolsNotes?.content ?? "")
|
|
220
265
|
: opts.field === "identity"
|
|
221
|
-
? config.agentConfig.identity?.content ?? ""
|
|
222
|
-
: config.agentConfig.organisation?.content ?? "";
|
|
266
|
+
? (config.agentConfig.identity?.content ?? "")
|
|
267
|
+
: (config.agentConfig.organisation?.content ?? "");
|
|
223
268
|
content = openEditor(initial, opts.label);
|
|
224
269
|
}
|
|
225
270
|
if (!content.trim().length) {
|
|
@@ -519,8 +564,7 @@ export async function runMemoryAggregateCommand(ctx: CliCtx): Promise<void> {
|
|
|
519
564
|
}
|
|
520
565
|
if (sub === "append") {
|
|
521
566
|
const text = ctx.args.slice(1).join(" ").trim();
|
|
522
|
-
if (!text)
|
|
523
|
-
throw new AgentConfigCliError("Usage: dench mem append <text>");
|
|
567
|
+
if (!text) throw new AgentConfigCliError("Usage: dench mem append <text>");
|
|
524
568
|
await runMutation(ctx, api.functions.agentConfig.updateMemory, {
|
|
525
569
|
content: text,
|
|
526
570
|
append: true,
|
|
@@ -593,9 +637,7 @@ export async function runDailyCommand(ctx: CliCtx): Promise<void> {
|
|
|
593
637
|
if (sub === "show") {
|
|
594
638
|
const date = ctx.args[1];
|
|
595
639
|
if (!date) {
|
|
596
|
-
throw new AgentConfigCliError(
|
|
597
|
-
"Usage: dench daily show <YYYY-MM-DD>",
|
|
598
|
-
);
|
|
640
|
+
throw new AgentConfigCliError("Usage: dench daily show <YYYY-MM-DD>");
|
|
599
641
|
}
|
|
600
642
|
out(
|
|
601
643
|
ctx,
|
package/chat-spawn.ts
CHANGED
|
@@ -16,8 +16,6 @@
|
|
|
16
16
|
* (`/api/chat/[runId]/stream`) and pretty-prints text deltas + tool
|
|
17
17
|
* calls until the workflow finishes.
|
|
18
18
|
*/
|
|
19
|
-
|
|
20
|
-
import { getCurrentRuntimeTimeZone } from "@/lib/timezone";
|
|
21
19
|
import { hasFlag } from "./lib/cli-args";
|
|
22
20
|
|
|
23
21
|
class ChatSpawnError extends Error {}
|
|
@@ -33,6 +31,18 @@ type ChatSpawnContext = {
|
|
|
33
31
|
jsonOutput: boolean;
|
|
34
32
|
};
|
|
35
33
|
|
|
34
|
+
function getCurrentRuntimeTimeZone(): string | null {
|
|
35
|
+
try {
|
|
36
|
+
const timeZone = Intl.DateTimeFormat().resolvedOptions().timeZone;
|
|
37
|
+
if (!timeZone || timeZone.length > 100) return null;
|
|
38
|
+
// Validate that Intl accepts the resolved value before forwarding it.
|
|
39
|
+
Intl.DateTimeFormat("en-US", { timeZone }).format(new Date());
|
|
40
|
+
return timeZone;
|
|
41
|
+
} catch {
|
|
42
|
+
return null;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
36
46
|
function consumeFlagValue(args: string[], name: string): string | undefined {
|
|
37
47
|
const idx = args.indexOf(name);
|
|
38
48
|
if (idx === -1) return undefined;
|
package/crm.ts
CHANGED
|
@@ -48,12 +48,16 @@ import {
|
|
|
48
48
|
getRequiredFieldsForApolloPath,
|
|
49
49
|
} from "./lib/crm-enrichment";
|
|
50
50
|
import {
|
|
51
|
+
type AviatoEnrichCompanyBody,
|
|
52
|
+
type AviatoEnrichPersonBody,
|
|
51
53
|
aviatoEnrichCompany,
|
|
52
54
|
aviatoEnrichPerson,
|
|
55
|
+
type EnrichCompanyBody,
|
|
53
56
|
EnrichmentGatewayError,
|
|
54
57
|
type EnrichmentGatewayOptions,
|
|
58
|
+
type EnrichPersonBody,
|
|
55
59
|
enrichCompany,
|
|
56
|
-
|
|
60
|
+
enrichPerson,
|
|
57
61
|
} from "./lib/enrichment-gateway";
|
|
58
62
|
|
|
59
63
|
// Contact-info fields that Aviato does not return.
|
|
@@ -383,6 +387,21 @@ const api = {
|
|
|
383
387
|
update: makeFunctionReference<"mutation">("functions/crm/fields:update"),
|
|
384
388
|
remove: makeFunctionReference<"mutation">("functions/crm/fields:remove"),
|
|
385
389
|
reorder: makeFunctionReference<"mutation">("functions/crm/fields:reorder"),
|
|
390
|
+
reorderEnumValues: makeFunctionReference<"mutation">(
|
|
391
|
+
"functions/crm/fields:reorderEnumValues",
|
|
392
|
+
),
|
|
393
|
+
addEnumValue: makeFunctionReference<"mutation">(
|
|
394
|
+
"functions/crm/fields:addEnumValue",
|
|
395
|
+
),
|
|
396
|
+
removeEnumValue: makeFunctionReference<"mutation">(
|
|
397
|
+
"functions/crm/fields:removeEnumValue",
|
|
398
|
+
),
|
|
399
|
+
setEnumColor: makeFunctionReference<"mutation">(
|
|
400
|
+
"functions/crm/fields:setEnumColor",
|
|
401
|
+
),
|
|
402
|
+
renameEnumValue: makeFunctionReference<"mutation">(
|
|
403
|
+
"functions/crm/fields:renameEnumValue",
|
|
404
|
+
),
|
|
386
405
|
},
|
|
387
406
|
entries: {
|
|
388
407
|
list: makeFunctionReference<"query">("functions/crm/entries:list"),
|
|
@@ -693,6 +712,105 @@ async function runFieldsCommand(ctx: CrmCliContext): Promise<void> {
|
|
|
693
712
|
);
|
|
694
713
|
return;
|
|
695
714
|
}
|
|
715
|
+
case "enum-reorder": {
|
|
716
|
+
const objectName = shift(ctx.args, "object name");
|
|
717
|
+
const fieldName = shift(ctx.args, "field name");
|
|
718
|
+
// Same pattern as `reorder`: peel positional args after asserting
|
|
719
|
+
// there are no stray flags so a typo'd `--limit` fails loud
|
|
720
|
+
// instead of being silently consumed as an enum value.
|
|
721
|
+
assertNoUnknownFlags(ctx.args, "crm fields enum-reorder");
|
|
722
|
+
const orderedValues = [...ctx.args];
|
|
723
|
+
if (orderedValues.length === 0) {
|
|
724
|
+
throw new CrmCliError(
|
|
725
|
+
"dench crm fields enum-reorder requires at least one value. " +
|
|
726
|
+
'Pass each enum value as a positional arg: `enum-reorder task Status "Done" "In Progress" "To Do"`',
|
|
727
|
+
);
|
|
728
|
+
}
|
|
729
|
+
out(
|
|
730
|
+
ctx,
|
|
731
|
+
await callMutation(ctx, api.fields.reorderEnumValues, {
|
|
732
|
+
objectName,
|
|
733
|
+
fieldName,
|
|
734
|
+
orderedValues,
|
|
735
|
+
}),
|
|
736
|
+
);
|
|
737
|
+
return;
|
|
738
|
+
}
|
|
739
|
+
case "enum-add": {
|
|
740
|
+
const objectName = shift(ctx.args, "object name");
|
|
741
|
+
const fieldName = shift(ctx.args, "field name");
|
|
742
|
+
const value = shift(ctx.args, "enum value");
|
|
743
|
+
const color = getFlag(ctx.args, "--color");
|
|
744
|
+
const positionStr = getFlag(ctx.args, "--position");
|
|
745
|
+
const position =
|
|
746
|
+
positionStr !== undefined ? Number.parseInt(positionStr, 10) : undefined;
|
|
747
|
+
if (position !== undefined && !Number.isFinite(position)) {
|
|
748
|
+
throw new CrmCliError(
|
|
749
|
+
`--position must be an integer, got "${positionStr}"`,
|
|
750
|
+
);
|
|
751
|
+
}
|
|
752
|
+
assertNoUnknownFlags(ctx.args, "crm fields enum-add");
|
|
753
|
+
out(
|
|
754
|
+
ctx,
|
|
755
|
+
await callMutation(ctx, api.fields.addEnumValue, {
|
|
756
|
+
objectName,
|
|
757
|
+
fieldName,
|
|
758
|
+
value,
|
|
759
|
+
color,
|
|
760
|
+
position,
|
|
761
|
+
}),
|
|
762
|
+
);
|
|
763
|
+
return;
|
|
764
|
+
}
|
|
765
|
+
case "enum-remove": {
|
|
766
|
+
const objectName = shift(ctx.args, "object name");
|
|
767
|
+
const fieldName = shift(ctx.args, "field name");
|
|
768
|
+
const value = shift(ctx.args, "enum value");
|
|
769
|
+
assertNoUnknownFlags(ctx.args, "crm fields enum-remove");
|
|
770
|
+
out(
|
|
771
|
+
ctx,
|
|
772
|
+
await callMutation(ctx, api.fields.removeEnumValue, {
|
|
773
|
+
objectName,
|
|
774
|
+
fieldName,
|
|
775
|
+
value,
|
|
776
|
+
}),
|
|
777
|
+
);
|
|
778
|
+
return;
|
|
779
|
+
}
|
|
780
|
+
case "enum-rename": {
|
|
781
|
+
const objectName = shift(ctx.args, "object name");
|
|
782
|
+
const fieldName = shift(ctx.args, "field name");
|
|
783
|
+
const oldValue = shift(ctx.args, "old enum value");
|
|
784
|
+
const newValue = shift(ctx.args, "new enum value");
|
|
785
|
+
assertNoUnknownFlags(ctx.args, "crm fields enum-rename");
|
|
786
|
+
out(
|
|
787
|
+
ctx,
|
|
788
|
+
await callMutation(ctx, api.fields.renameEnumValue, {
|
|
789
|
+
objectName,
|
|
790
|
+
fieldName,
|
|
791
|
+
oldValue,
|
|
792
|
+
newValue,
|
|
793
|
+
}),
|
|
794
|
+
);
|
|
795
|
+
return;
|
|
796
|
+
}
|
|
797
|
+
case "enum-set-color": {
|
|
798
|
+
const objectName = shift(ctx.args, "object name");
|
|
799
|
+
const fieldName = shift(ctx.args, "field name");
|
|
800
|
+
const value = shift(ctx.args, "enum value");
|
|
801
|
+
const color = shift(ctx.args, "hex color");
|
|
802
|
+
assertNoUnknownFlags(ctx.args, "crm fields enum-set-color");
|
|
803
|
+
out(
|
|
804
|
+
ctx,
|
|
805
|
+
await callMutation(ctx, api.fields.setEnumColor, {
|
|
806
|
+
objectName,
|
|
807
|
+
fieldName,
|
|
808
|
+
value,
|
|
809
|
+
color,
|
|
810
|
+
}),
|
|
811
|
+
);
|
|
812
|
+
return;
|
|
813
|
+
}
|
|
696
814
|
default:
|
|
697
815
|
throw new CrmCliError(`Unknown crm fields verb: ${verb ?? "<none>"}`);
|
|
698
816
|
}
|
|
@@ -1704,6 +1822,21 @@ async function resolveEnrichmentTarget(
|
|
|
1704
1822
|
};
|
|
1705
1823
|
}
|
|
1706
1824
|
|
|
1825
|
+
/**
|
|
1826
|
+
* Build the gateway request body from a CRM entry the same way the
|
|
1827
|
+
* chat-turn `enrich_person` / `enrich_company` / `enrich_person_basic` /
|
|
1828
|
+
* `enrich_company_basic` tools do — pass every structured identifier the
|
|
1829
|
+
* row has (email, firstName, lastName, linkedinUrl, domain, …) and let the
|
|
1830
|
+
* shared gateway client (`enrichPerson`, `enrichCompany`,
|
|
1831
|
+
* `aviatoEnrichPerson`, `aviatoEnrichCompany`) validate + dispatch.
|
|
1832
|
+
*
|
|
1833
|
+
* The previous implementation collapsed the entry to a single
|
|
1834
|
+
* `--input-field` value and called `enrichPersonByIdentifier`, which
|
|
1835
|
+
* threw away the rest of the row and made FullEnrich fail with
|
|
1836
|
+
* `missing_people_identifier` whenever the input field was just an
|
|
1837
|
+
* Email (no first/last name reached the gateway, so the validator
|
|
1838
|
+
* rejected the request before any provider was called).
|
|
1839
|
+
*/
|
|
1707
1840
|
async function callEnrichmentGatewayForEntry(args: {
|
|
1708
1841
|
target: EnrichmentTarget;
|
|
1709
1842
|
entry: CrmEntryForEnrichment;
|
|
@@ -1711,64 +1844,167 @@ async function callEnrichmentGatewayForEntry(args: {
|
|
|
1711
1844
|
provider?: string;
|
|
1712
1845
|
gateway?: Pick<EnrichmentGatewayOptions, "apiKey" | "baseUrl">;
|
|
1713
1846
|
}): Promise<Record<string, unknown>> {
|
|
1714
|
-
const
|
|
1715
|
-
|
|
1847
|
+
const { entry, target } = args;
|
|
1848
|
+
// Sanity gate: the column the user picked as `--input-field` must be
|
|
1849
|
+
// populated. The body builder will pass every other available identifier
|
|
1850
|
+
// too, so the gateway has the best possible chance of resolving the row.
|
|
1851
|
+
if (!hasCellValue(entry.fields[target.inputField.name])) {
|
|
1716
1852
|
throw new CrmCliError(
|
|
1717
|
-
`Entry ${
|
|
1853
|
+
`Entry ${entry.id} has no value for input field "${target.inputField.name}"`,
|
|
1718
1854
|
);
|
|
1719
1855
|
}
|
|
1720
|
-
const
|
|
1721
|
-
const isCompany = args.target.category === "company";
|
|
1856
|
+
const isCompany = target.category === "company";
|
|
1722
1857
|
const resolvedProvider =
|
|
1723
1858
|
args.provider ?? (isCompany ? "aviato" : "fullenrich");
|
|
1724
1859
|
|
|
1725
1860
|
if (resolvedProvider === "aviato") {
|
|
1726
|
-
//
|
|
1727
|
-
|
|
1861
|
+
// Aviato does not return phone or email — surface a clear error
|
|
1862
|
+
// before we waste a gateway round-trip.
|
|
1863
|
+
const contactFields = target.requiredFields.filter(isContactField);
|
|
1728
1864
|
if (contactFields.length > 0) {
|
|
1729
1865
|
throw new CrmCliError(
|
|
1730
1866
|
`Aviato does not return phone or email; use --provider fullenrich for contact fields (${contactFields.join(", ")}).`,
|
|
1731
1867
|
);
|
|
1732
1868
|
}
|
|
1733
|
-
|
|
1734
1869
|
if (isCompany) {
|
|
1735
|
-
// inputValue is typically a domain; try to detect linkedin URLs
|
|
1736
|
-
const isLinkedin = /linkedin\.com/i.test(inputValue);
|
|
1737
1870
|
return aviatoEnrichCompany(
|
|
1738
|
-
|
|
1871
|
+
buildAviatoCompanyBodyFromEntry(entry),
|
|
1739
1872
|
args.gateway,
|
|
1740
1873
|
);
|
|
1741
|
-
} else {
|
|
1742
|
-
// For people, inputValue may be a LinkedIn URL, email, or LinkedIn ID
|
|
1743
|
-
const isLinkedin = /linkedin\.com/i.test(inputValue);
|
|
1744
|
-
const isEmail = /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(inputValue);
|
|
1745
|
-
if (isLinkedin) {
|
|
1746
|
-
return aviatoEnrichPerson({ linkedinUrl: inputValue }, args.gateway);
|
|
1747
|
-
}
|
|
1748
|
-
if (isEmail) {
|
|
1749
|
-
return aviatoEnrichPerson({ email: inputValue }, args.gateway);
|
|
1750
|
-
}
|
|
1751
|
-
return aviatoEnrichPerson({ linkedinID: inputValue }, args.gateway);
|
|
1752
1874
|
}
|
|
1875
|
+
return aviatoEnrichPerson(
|
|
1876
|
+
buildAviatoPersonBodyFromEntry(entry),
|
|
1877
|
+
args.gateway,
|
|
1878
|
+
);
|
|
1753
1879
|
}
|
|
1754
1880
|
|
|
1755
|
-
//
|
|
1881
|
+
// FullEnrich path. Mirrors `enrichPersonStep` / `enrichCompanyStep` in
|
|
1882
|
+
// src/workflows/enrichment-steps.ts: build a structured body, hand it
|
|
1883
|
+
// to the same shared gateway client.
|
|
1756
1884
|
if (isCompany) {
|
|
1757
1885
|
return enrichCompany(
|
|
1758
|
-
|
|
1759
|
-
domain: inputValue,
|
|
1760
|
-
requiredFields: args.target.requiredFields,
|
|
1761
|
-
},
|
|
1886
|
+
buildCompanyBodyFromEntry(entry, target.requiredFields),
|
|
1762
1887
|
args.gateway,
|
|
1763
1888
|
);
|
|
1764
1889
|
}
|
|
1765
|
-
return
|
|
1766
|
-
|
|
1767
|
-
args.target.requiredFields,
|
|
1890
|
+
return enrichPerson(
|
|
1891
|
+
buildPersonBodyFromEntry(entry, target.requiredFields),
|
|
1768
1892
|
args.gateway,
|
|
1769
1893
|
);
|
|
1770
1894
|
}
|
|
1771
1895
|
|
|
1896
|
+
// ── Body builders ────────────────────────────────────────────────────────
|
|
1897
|
+
//
|
|
1898
|
+
// Pick whichever canonical column name the entry happens to use. Field
|
|
1899
|
+
// naming on CRM objects has historically drifted (`Name` vs `Full Name`,
|
|
1900
|
+
// `Email` vs `Email Address`, `Website` vs `Domain`), so each builder
|
|
1901
|
+
// accepts a small list of synonyms before giving up.
|
|
1902
|
+
|
|
1903
|
+
const PEOPLE_FULL_NAME_FIELDS = ["Name", "Full Name", "Person Name"] as const;
|
|
1904
|
+
const PEOPLE_FIRST_NAME_FIELDS = ["First Name", "Given Name"] as const;
|
|
1905
|
+
const PEOPLE_LAST_NAME_FIELDS = ["Last Name", "Family Name", "Surname"] as const;
|
|
1906
|
+
const PEOPLE_EMAIL_FIELDS = [
|
|
1907
|
+
"Email",
|
|
1908
|
+
"Email Address",
|
|
1909
|
+
"Work Email",
|
|
1910
|
+
"Personal Email",
|
|
1911
|
+
] as const;
|
|
1912
|
+
const PEOPLE_LINKEDIN_FIELDS = ["LinkedIn URL", "LinkedIn", "Linkedin URL"] as const;
|
|
1913
|
+
const PEOPLE_DOMAIN_FIELDS = ["Domain", "Company Domain"] as const;
|
|
1914
|
+
|
|
1915
|
+
const COMPANY_WEBSITE_FIELDS = ["Website", "Domain", "URL", "Site"] as const;
|
|
1916
|
+
const COMPANY_LINKEDIN_FIELDS = [
|
|
1917
|
+
"LinkedIn URL",
|
|
1918
|
+
"LinkedIn",
|
|
1919
|
+
"Linkedin URL",
|
|
1920
|
+
] as const;
|
|
1921
|
+
const COMPANY_NAME_FIELDS = [
|
|
1922
|
+
"Company Name",
|
|
1923
|
+
"Name",
|
|
1924
|
+
"Organization",
|
|
1925
|
+
"Organization Name",
|
|
1926
|
+
] as const;
|
|
1927
|
+
|
|
1928
|
+
function readEntryField(
|
|
1929
|
+
entry: CrmEntryForEnrichment,
|
|
1930
|
+
candidates: readonly string[],
|
|
1931
|
+
): string | undefined {
|
|
1932
|
+
for (const name of candidates) {
|
|
1933
|
+
const value = entry.fields[name];
|
|
1934
|
+
if (typeof value === "string") {
|
|
1935
|
+
const trimmed = value.trim();
|
|
1936
|
+
if (trimmed.length > 0) return trimmed;
|
|
1937
|
+
}
|
|
1938
|
+
}
|
|
1939
|
+
return undefined;
|
|
1940
|
+
}
|
|
1941
|
+
|
|
1942
|
+
/** Split "Mendy Edri" → { firstName: "Mendy", lastName: "Edri" }. */
|
|
1943
|
+
export function splitFullName(fullName: string): {
|
|
1944
|
+
firstName?: string;
|
|
1945
|
+
lastName?: string;
|
|
1946
|
+
} {
|
|
1947
|
+
const trimmed = fullName.trim();
|
|
1948
|
+
if (!trimmed) return {};
|
|
1949
|
+
const parts = trimmed.split(/\s+/);
|
|
1950
|
+
if (parts.length === 1) return { firstName: parts[0] };
|
|
1951
|
+
return { firstName: parts[0], lastName: parts.slice(1).join(" ") };
|
|
1952
|
+
}
|
|
1953
|
+
|
|
1954
|
+
export function buildPersonBodyFromEntry(
|
|
1955
|
+
entry: CrmEntryForEnrichment,
|
|
1956
|
+
requiredFields: string[] | undefined,
|
|
1957
|
+
): EnrichPersonBody {
|
|
1958
|
+
let firstName = readEntryField(entry, PEOPLE_FIRST_NAME_FIELDS);
|
|
1959
|
+
let lastName = readEntryField(entry, PEOPLE_LAST_NAME_FIELDS);
|
|
1960
|
+
if (!firstName || !lastName) {
|
|
1961
|
+
const fullName = readEntryField(entry, PEOPLE_FULL_NAME_FIELDS);
|
|
1962
|
+
if (fullName) {
|
|
1963
|
+
const split = splitFullName(fullName);
|
|
1964
|
+
firstName ??= split.firstName;
|
|
1965
|
+
lastName ??= split.lastName;
|
|
1966
|
+
}
|
|
1967
|
+
}
|
|
1968
|
+
return {
|
|
1969
|
+
email: readEntryField(entry, PEOPLE_EMAIL_FIELDS),
|
|
1970
|
+
linkedinUrl: readEntryField(entry, PEOPLE_LINKEDIN_FIELDS),
|
|
1971
|
+
firstName,
|
|
1972
|
+
lastName,
|
|
1973
|
+
domain: readEntryField(entry, PEOPLE_DOMAIN_FIELDS),
|
|
1974
|
+
requiredFields,
|
|
1975
|
+
};
|
|
1976
|
+
}
|
|
1977
|
+
|
|
1978
|
+
export function buildCompanyBodyFromEntry(
|
|
1979
|
+
entry: CrmEntryForEnrichment,
|
|
1980
|
+
requiredFields: string[] | undefined,
|
|
1981
|
+
): EnrichCompanyBody {
|
|
1982
|
+
return {
|
|
1983
|
+
domain: readEntryField(entry, COMPANY_WEBSITE_FIELDS),
|
|
1984
|
+
companyName: readEntryField(entry, COMPANY_NAME_FIELDS),
|
|
1985
|
+
linkedinUrl: readEntryField(entry, COMPANY_LINKEDIN_FIELDS),
|
|
1986
|
+
requiredFields,
|
|
1987
|
+
};
|
|
1988
|
+
}
|
|
1989
|
+
|
|
1990
|
+
export function buildAviatoPersonBodyFromEntry(
|
|
1991
|
+
entry: CrmEntryForEnrichment,
|
|
1992
|
+
): AviatoEnrichPersonBody {
|
|
1993
|
+
return {
|
|
1994
|
+
linkedinUrl: readEntryField(entry, PEOPLE_LINKEDIN_FIELDS),
|
|
1995
|
+
email: readEntryField(entry, PEOPLE_EMAIL_FIELDS),
|
|
1996
|
+
};
|
|
1997
|
+
}
|
|
1998
|
+
|
|
1999
|
+
export function buildAviatoCompanyBodyFromEntry(
|
|
2000
|
+
entry: CrmEntryForEnrichment,
|
|
2001
|
+
): AviatoEnrichCompanyBody {
|
|
2002
|
+
return {
|
|
2003
|
+
website: readEntryField(entry, COMPANY_WEBSITE_FIELDS),
|
|
2004
|
+
linkedinUrl: readEntryField(entry, COMPANY_LINKEDIN_FIELDS),
|
|
2005
|
+
};
|
|
2006
|
+
}
|
|
2007
|
+
|
|
1772
2008
|
async function enrichOneEntry(
|
|
1773
2009
|
ctx: CrmCliContext,
|
|
1774
2010
|
args: Omit<CellEnrichmentArgs, "entryId"> & {
|
|
@@ -2148,6 +2384,28 @@ Fields (columns):
|
|
|
2148
2384
|
dench crm fields delete <object> <field>
|
|
2149
2385
|
dench crm fields reorder <object> <field1> <field2> <field3> ...
|
|
2150
2386
|
|
|
2387
|
+
Enum option management (on existing enum fields):
|
|
2388
|
+
dench crm fields enum-reorder <object> <field> <value1> <value2> ...
|
|
2389
|
+
Canonical way to set the order of kanban columns / table picker options.
|
|
2390
|
+
The trailing positional values must be a permutation of the field's
|
|
2391
|
+
current enumValues; additions/removals are rejected (use enum-add /
|
|
2392
|
+
enum-remove first). Also keeps any matching crmStatuses rows in sync
|
|
2393
|
+
so the kanban (which prefers statuses over enumValues) reflects the
|
|
2394
|
+
new order.
|
|
2395
|
+
dench crm fields enum-add <object> <field> <value> [--color HEX] [--position N]
|
|
2396
|
+
Appends <value> to the field's enumValues. --color is a hex like
|
|
2397
|
+
"#22c55e"; --position is a 0-based insertion index (defaults to append).
|
|
2398
|
+
Idempotent: re-adding an existing value only updates its color when given.
|
|
2399
|
+
dench crm fields enum-remove <object> <field> <value>
|
|
2400
|
+
Removes the value from the option list. Historical entry rows that held
|
|
2401
|
+
<value> keep the literal string (the badge falls back to the default
|
|
2402
|
+
tint), matching Notion's "remove option, keep history" semantics.
|
|
2403
|
+
dench crm fields enum-rename <object> <field> <oldValue> <newValue>
|
|
2404
|
+
Renames the option AND rewrites every entry that referenced the old
|
|
2405
|
+
value (bounded to 1000 rows per call) AND renames matching crmStatuses.
|
|
2406
|
+
dench crm fields enum-set-color <object> <field> <value> <hex>
|
|
2407
|
+
Update the color (e.g. "#3b82f6") for an existing enum option.
|
|
2408
|
+
|
|
2151
2409
|
Entries (rows):
|
|
2152
2410
|
dench crm entries list <object> [--limit N] [--with-meta] [--json]
|
|
2153
2411
|
dench crm entries get <object> <entryId>
|
package/dench.ts
CHANGED
|
@@ -376,6 +376,30 @@ function parseCommaOption(name: string) {
|
|
|
376
376
|
);
|
|
377
377
|
}
|
|
378
378
|
|
|
379
|
+
function parseMemoryVisibilityOption() {
|
|
380
|
+
const scope = option("--scope");
|
|
381
|
+
const visibility = option("--visibility");
|
|
382
|
+
if (scope && visibility && scope !== visibility) {
|
|
383
|
+
throw new CliError(
|
|
384
|
+
"--scope and --visibility must match when both are set",
|
|
385
|
+
{
|
|
386
|
+
code: "memory_scope_conflict",
|
|
387
|
+
nextActions: ["Use only --scope user or --scope org."],
|
|
388
|
+
},
|
|
389
|
+
);
|
|
390
|
+
}
|
|
391
|
+
const value = scope ?? visibility ?? "user";
|
|
392
|
+
if (value !== "user" && value !== "org") {
|
|
393
|
+
throw new CliError("--scope must be user or org", {
|
|
394
|
+
code: "invalid_memory_scope",
|
|
395
|
+
nextActions: [
|
|
396
|
+
"Use --scope user for private user memory or --scope org for shared organization memory.",
|
|
397
|
+
],
|
|
398
|
+
});
|
|
399
|
+
}
|
|
400
|
+
return value;
|
|
401
|
+
}
|
|
402
|
+
|
|
379
403
|
function logHuman(message: string) {
|
|
380
404
|
if (!json) {
|
|
381
405
|
console.error(message);
|
|
@@ -398,7 +422,7 @@ Usage:
|
|
|
398
422
|
dench context [--json]
|
|
399
423
|
dench status [--mine|--self] [--json]
|
|
400
424
|
dench memory search "query" [--limit 8] [--json]
|
|
401
|
-
dench memory save <key> "text" [--kind fact|decision|preference|goal|tool_note|other] [--tags a,b] [--sensitivity normal|broad|sensitive] [--json]
|
|
425
|
+
dench memory save <key> "text" [--kind fact|decision|preference|goal|tool_note|other] [--tags a,b] [--sensitivity normal|broad|sensitive] [--scope user|org] [--json]
|
|
402
426
|
dench artifacts [--limit 25] [--json]
|
|
403
427
|
dench suggested-work [--json]
|
|
404
428
|
dench approval request "message" [--json]
|
|
@@ -440,13 +464,13 @@ Workspace members (for CRM \`user\`-type fields):
|
|
|
440
464
|
cells assign to real members instead of being stored as unlinked
|
|
441
465
|
display-name strings. See \`dench members help\`.
|
|
442
466
|
|
|
443
|
-
Live web search (Exa via the Dench
|
|
467
|
+
Live web search (Exa via the Dench Gateway, auths with the active \`dench signin\` agent session or DENCH_API_KEY):
|
|
444
468
|
dench search "<query>" [--num-results N] [--type auto|fast|deep|deep-reasoning|neural] [--category news|company|people|...] [--include-domains a.com,b.com] [--exclude-domains c.com] [--max-chars 800] [--json]
|
|
445
469
|
dench search contents <url> [<url>...] [--max-chars 4000] [--summary "..."] [--json]
|
|
446
470
|
dench search answer "<query>" [--json]
|
|
447
471
|
Help: dench search help
|
|
448
472
|
|
|
449
|
-
Image generation / editing (
|
|
473
|
+
Image generation / editing (via the Dench Gateway):
|
|
450
474
|
dench image generate "<prompt>" [--size 1024x1024] [--quality auto|low|medium|high] [--format png|jpeg|webp] [--save-path /workspace/path/file.png] [--output <local file>] [--no-write] [--json]
|
|
451
475
|
dench image edit "<prompt>" --input <path> [--input <path> ...] [--mask <path>] [--size ...] [--quality ...] [--format ...] [--save-path ...] [--output ...] [--json]
|
|
452
476
|
Help: dench image help
|
|
@@ -472,7 +496,7 @@ Spawn a new chat-turn workflow (same as the web UI):
|
|
|
472
496
|
|
|
473
497
|
Help: dench chat help
|
|
474
498
|
|
|
475
|
-
File sync
|
|
499
|
+
File sync:
|
|
476
500
|
dench fs status [--json] [--workspace /workspace] [--no-hash] [--drift-limit N]
|
|
477
501
|
dench fs sync --initial --org-id <orgId> [--workspace /workspace]
|
|
478
502
|
dench fs stream-open <path>
|
|
@@ -488,7 +512,7 @@ returns ENOSYS on the FUSE-backed volume mount):
|
|
|
488
512
|
Persist scratch -> workspace (cp from /tmp into /workspace + Convex sync):
|
|
489
513
|
dench stage <local-path> [<workspace-dest>] [--clean] [--json]
|
|
490
514
|
|
|
491
|
-
Subagents
|
|
515
|
+
Subagents:
|
|
492
516
|
dench agent spawn '<goal>' [--parent-run-id <id>] [--sandbox own|share_parent] [--time-budget-ms N]
|
|
493
517
|
dench agent await --hook <token> | --children id1,id2,id3
|
|
494
518
|
dench agent message <toRunId> '<text>' [--from-run-id <id>]
|
|
@@ -501,7 +525,7 @@ Subagents (Daytona-native rewrite):
|
|
|
501
525
|
|
|
502
526
|
dench --version
|
|
503
527
|
|
|
504
|
-
Self-updating agent harness
|
|
528
|
+
Self-updating agent harness:
|
|
505
529
|
dench identity show | edit IDENTITY.md (org-wide)
|
|
506
530
|
dench organisation show | edit ORGANISATION.md
|
|
507
531
|
dench user show | edit USER.md (per-member)
|
|
@@ -610,10 +634,11 @@ function memoryHelp() {
|
|
|
610
634
|
|
|
611
635
|
Usage:
|
|
612
636
|
dench memory search "query" [--limit 8] [--json]
|
|
613
|
-
dench memory save <key> "text" [--kind fact|decision|preference|goal|tool_note|other] [--tags a,b] [--sensitivity normal|broad|sensitive] [--json]
|
|
637
|
+
dench memory save <key> "text" [--kind fact|decision|preference|goal|tool_note|other] [--tags a,b] [--sensitivity normal|broad|sensitive] [--scope user|org] [--json]
|
|
614
638
|
|
|
615
639
|
Use memory only for stable facts, decisions, preferences, recurring goals, and
|
|
616
|
-
tool notes. Do not store secrets or scratch notes.
|
|
640
|
+
tool notes. Do not store secrets or scratch notes. Saved memories are scoped to
|
|
641
|
+
the signed-in user by default; pass --scope org to share with the organization.
|
|
617
642
|
`);
|
|
618
643
|
}
|
|
619
644
|
|
|
@@ -2038,25 +2063,31 @@ function parseBillingTopupAmountUsd() {
|
|
|
2038
2063
|
}
|
|
2039
2064
|
|
|
2040
2065
|
async function billingStatus(runtime: Runtime) {
|
|
2041
|
-
const
|
|
2042
|
-
|
|
2066
|
+
const authedRuntime = requireAuthenticatedRuntime(
|
|
2067
|
+
runtime,
|
|
2068
|
+
"dench billing status",
|
|
2069
|
+
);
|
|
2070
|
+
const status = await authedRuntime.client.query(
|
|
2043
2071
|
api.functions.agentWorkspace.getAgentBillingStatus,
|
|
2044
|
-
{ sessionToken:
|
|
2072
|
+
{ sessionToken: authedRuntime.sessionToken },
|
|
2045
2073
|
);
|
|
2046
2074
|
print(json ? status : formatBillingStatus(status));
|
|
2047
2075
|
}
|
|
2048
2076
|
|
|
2049
2077
|
async function billingTopup(runtime: Runtime) {
|
|
2050
|
-
const
|
|
2078
|
+
const authedRuntime = requireAuthenticatedRuntime(
|
|
2079
|
+
runtime,
|
|
2080
|
+
"dench billing topup",
|
|
2081
|
+
);
|
|
2051
2082
|
const amountUsd = parseBillingTopupAmountUsd();
|
|
2052
2083
|
const response = await fetch(
|
|
2053
|
-
`${
|
|
2084
|
+
`${authedRuntime.host.replace(/\/+$/, "")}/api/stripe/create-ai-topup`,
|
|
2054
2085
|
{
|
|
2055
2086
|
method: "POST",
|
|
2056
2087
|
headers: {
|
|
2057
2088
|
accept: "application/json",
|
|
2058
2089
|
"content-type": "application/json",
|
|
2059
|
-
authorization: `Bearer ${
|
|
2090
|
+
authorization: `Bearer ${authedRuntime.sessionToken}`,
|
|
2060
2091
|
},
|
|
2061
2092
|
body: JSON.stringify({
|
|
2062
2093
|
amountUsd,
|
|
@@ -2123,7 +2154,7 @@ async function billingTopup(runtime: Runtime) {
|
|
|
2123
2154
|
* needed in that case). We surface either outcome cleanly.
|
|
2124
2155
|
*/
|
|
2125
2156
|
async function denchUpgrade(runtime: Runtime) {
|
|
2126
|
-
const
|
|
2157
|
+
const authedRuntime = requireAuthenticatedRuntime(runtime, "dench upgrade");
|
|
2127
2158
|
const tierFlag = option("--tier")?.trim().toLowerCase() || "pro";
|
|
2128
2159
|
const cycleFlag =
|
|
2129
2160
|
option("--cycle")?.trim().toLowerCase() ||
|
|
@@ -2146,13 +2177,13 @@ async function denchUpgrade(runtime: Runtime) {
|
|
|
2146
2177
|
const internalTier = tierFlag === "pro" ? "desktop" : "cloud";
|
|
2147
2178
|
|
|
2148
2179
|
const response = await fetch(
|
|
2149
|
-
`${
|
|
2180
|
+
`${authedRuntime.host.replace(/\/+$/, "")}/api/stripe/create-checkout`,
|
|
2150
2181
|
{
|
|
2151
2182
|
method: "POST",
|
|
2152
2183
|
headers: {
|
|
2153
2184
|
accept: "application/json",
|
|
2154
2185
|
"content-type": "application/json",
|
|
2155
|
-
authorization: `Bearer ${
|
|
2186
|
+
authorization: `Bearer ${authedRuntime.sessionToken}`,
|
|
2156
2187
|
},
|
|
2157
2188
|
body: JSON.stringify({
|
|
2158
2189
|
tier: internalTier,
|
|
@@ -2394,25 +2425,6 @@ function stripRuntimeFlags(args: string[]) {
|
|
|
2394
2425
|
return stripped;
|
|
2395
2426
|
}
|
|
2396
2427
|
|
|
2397
|
-
/**
|
|
2398
|
-
* `session` mode is required for `agentWorkspace.*` calls because they
|
|
2399
|
-
* gate on `validateAgentSession` server-side, which only accepts the
|
|
2400
|
-
* `dch_agent_*` token format. Sandboxes that only have a unified
|
|
2401
|
-
* `DENCH_API_KEY` (apiKey mode) cannot impersonate a specific agent
|
|
2402
|
-
* and will fail at the server with `Invalid agent session` — surface
|
|
2403
|
-
* that here as a clear error instead.
|
|
2404
|
-
*/
|
|
2405
|
-
function requireSessionRuntime(
|
|
2406
|
-
runtime: Runtime,
|
|
2407
|
-
command: string,
|
|
2408
|
-
): Extract<Runtime, { mode: "session" }> {
|
|
2409
|
-
if (runtime.mode === "session") return runtime;
|
|
2410
|
-
if (runtime.mode === "apiKey") {
|
|
2411
|
-
throw agentSessionRequiredError(command);
|
|
2412
|
-
}
|
|
2413
|
-
throw loginRequiredError(command);
|
|
2414
|
-
}
|
|
2415
|
-
|
|
2416
2428
|
/**
|
|
2417
2429
|
* Org-scoped commands (`dench crm`, `dench files`, etc.) accept either
|
|
2418
2430
|
* a `dch_agent_*` agent session token OR a unified Dench API key
|
|
@@ -2432,6 +2444,7 @@ function requireAuthenticatedRuntime(
|
|
|
2432
2444
|
type GatewayCommandAuth = {
|
|
2433
2445
|
bearerToken: string;
|
|
2434
2446
|
gatewayBaseUrl?: string;
|
|
2447
|
+
gatewayHeaders?: Record<string, string>;
|
|
2435
2448
|
};
|
|
2436
2449
|
|
|
2437
2450
|
function gatewayBaseUrlForCli(value: unknown): string | undefined {
|
|
@@ -2528,6 +2541,12 @@ async function resolveGatewayCommandAuth(
|
|
|
2528
2541
|
return {
|
|
2529
2542
|
bearerToken: gatewayKey,
|
|
2530
2543
|
gatewayBaseUrl: gatewayBaseUrlForCli(body.gatewayBaseUrl),
|
|
2544
|
+
gatewayHeaders:
|
|
2545
|
+
body.gatewayHeaders &&
|
|
2546
|
+
typeof body.gatewayHeaders === "object" &&
|
|
2547
|
+
!Array.isArray(body.gatewayHeaders)
|
|
2548
|
+
? (body.gatewayHeaders as Record<string, string>)
|
|
2549
|
+
: undefined,
|
|
2531
2550
|
};
|
|
2532
2551
|
}
|
|
2533
2552
|
|
|
@@ -2543,36 +2562,6 @@ function encodeDevCrmSessionToken(args: {
|
|
|
2543
2562
|
return `dch_dev_${encodeURIComponent(args.workspaceSlug)}:${encodeURIComponent(args.devKey)}`;
|
|
2544
2563
|
}
|
|
2545
2564
|
|
|
2546
|
-
function agentSessionRequiredError(command: string) {
|
|
2547
|
-
return new CliError(
|
|
2548
|
-
`${command} requires an agent session — DENCH_API_KEY is org-scoped and cannot impersonate a specific agent`,
|
|
2549
|
-
{
|
|
2550
|
-
code: "agent_session_required",
|
|
2551
|
-
nextActions: [
|
|
2552
|
-
'Run dench signin --kind <kind> --name "AI Agent - Project" to mint an agent session for this command.',
|
|
2553
|
-
"Or run the equivalent action via the workspace UI / dench crm if it is org-level (e.g. CRM data, files).",
|
|
2554
|
-
],
|
|
2555
|
-
},
|
|
2556
|
-
);
|
|
2557
|
-
}
|
|
2558
|
-
|
|
2559
|
-
/**
|
|
2560
|
-
* `dench approval request` has separate session vs dev branches. With
|
|
2561
|
-
* apiKey mode added, the dev branch would silently receive partial args.
|
|
2562
|
-
* Use this to short-circuit that branch with a clear error.
|
|
2563
|
-
*
|
|
2564
|
-
* Asserts away the `apiKey` mode so the caller's downstream type
|
|
2565
|
-
* narrowing (e.g. `runtime.workspaceArgs`) keeps working.
|
|
2566
|
-
*/
|
|
2567
|
-
function rejectApiKeyForAgentCommand(
|
|
2568
|
-
runtime: Runtime,
|
|
2569
|
-
command: string,
|
|
2570
|
-
): asserts runtime is Exclude<Runtime, { mode: "apiKey" }> {
|
|
2571
|
-
if (runtime.mode === "apiKey") {
|
|
2572
|
-
throw agentSessionRequiredError(command);
|
|
2573
|
-
}
|
|
2574
|
-
}
|
|
2575
|
-
|
|
2576
2565
|
function asRecord(value: unknown): JsonRecord | undefined {
|
|
2577
2566
|
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
2578
2567
|
return undefined;
|
|
@@ -2665,6 +2654,7 @@ async function connectedAppsContext(runtime: Runtime) {
|
|
|
2665
2654
|
return fetchConnectedAppsSummary({
|
|
2666
2655
|
bearerToken: gatewayAuth.bearerToken,
|
|
2667
2656
|
gatewayBaseUrl: gatewayAuth.gatewayBaseUrl,
|
|
2657
|
+
gatewayHeaders: gatewayAuth.gatewayHeaders,
|
|
2668
2658
|
});
|
|
2669
2659
|
} catch (error) {
|
|
2670
2660
|
return {
|
|
@@ -2679,7 +2669,7 @@ async function connectedAppsContext(runtime: Runtime) {
|
|
|
2679
2669
|
async function buildContext(runtime: Runtime) {
|
|
2680
2670
|
let mine: JsonRecord;
|
|
2681
2671
|
let urls: JsonRecord;
|
|
2682
|
-
if (runtime.mode === "session") {
|
|
2672
|
+
if (runtime.mode === "session" || runtime.mode === "apiKey") {
|
|
2683
2673
|
mine = (await runtime.client.query(
|
|
2684
2674
|
api.functions.agentWorkspace.whatCanIDoHere,
|
|
2685
2675
|
{ sessionToken: runtime.sessionToken },
|
|
@@ -2692,20 +2682,7 @@ async function buildContext(runtime: Runtime) {
|
|
|
2692
2682
|
);
|
|
2693
2683
|
urls = {};
|
|
2694
2684
|
} else {
|
|
2695
|
-
|
|
2696
|
-
// `whatCanIDoHere` (it gates on `validateAgentSession`). Return a
|
|
2697
|
-
// minimal context derived from sandbox envVars so dench-cli help
|
|
2698
|
-
// surfaces still work without a full login.
|
|
2699
|
-
mine = {
|
|
2700
|
-
authMode: "apiKey",
|
|
2701
|
-
organization: runtime.organization ?? null,
|
|
2702
|
-
runId: process.env.DENCH_RUN_ID?.trim() ?? null,
|
|
2703
|
-
parentRunId: process.env.DENCH_PARENT_RUN_ID?.trim() ?? null,
|
|
2704
|
-
rootRunId: process.env.DENCH_ROOT_RUN_ID?.trim() ?? null,
|
|
2705
|
-
};
|
|
2706
|
-
urls = runtime.host
|
|
2707
|
-
? workspaceUrls(runtime.host, runtime.organization?.slug)
|
|
2708
|
-
: {};
|
|
2685
|
+
throw loginRequiredError("dench context");
|
|
2709
2686
|
}
|
|
2710
2687
|
|
|
2711
2688
|
return {
|
|
@@ -2933,7 +2910,7 @@ function summarizeDevMine(workspaceSlug: string, data: WorkspaceOverview) {
|
|
|
2933
2910
|
}
|
|
2934
2911
|
|
|
2935
2912
|
async function listArtifacts(runtime: Runtime, limit?: number) {
|
|
2936
|
-
if (runtime.mode === "session") {
|
|
2913
|
+
if (runtime.mode === "session" || runtime.mode === "apiKey") {
|
|
2937
2914
|
try {
|
|
2938
2915
|
return (await runtime.client.query(
|
|
2939
2916
|
api.functions.agentWorkspace.agentListArtifacts,
|
|
@@ -2951,12 +2928,6 @@ async function listArtifacts(runtime: Runtime, limit?: number) {
|
|
|
2951
2928
|
);
|
|
2952
2929
|
}
|
|
2953
2930
|
}
|
|
2954
|
-
if (runtime.mode === "apiKey") {
|
|
2955
|
-
// No agent identity → no artifact list. Return empty so callers
|
|
2956
|
-
// (e.g. `dench artifacts`, `dench suggested-work`) degrade gracefully
|
|
2957
|
-
// instead of throwing for sandbox callers.
|
|
2958
|
-
return [] as JsonRecord[];
|
|
2959
|
-
}
|
|
2960
2931
|
const data = await devOverview(runtime);
|
|
2961
2932
|
return asRecordArray(data.recentArtifacts);
|
|
2962
2933
|
}
|
|
@@ -3884,6 +3855,7 @@ async function main() {
|
|
|
3884
3855
|
jsonOutput: json,
|
|
3885
3856
|
bearerToken: gatewayAuth.bearerToken,
|
|
3886
3857
|
gatewayBaseUrl: gatewayAuth.gatewayBaseUrl,
|
|
3858
|
+
gatewayHeaders: gatewayAuth.gatewayHeaders,
|
|
3887
3859
|
});
|
|
3888
3860
|
return;
|
|
3889
3861
|
}
|
|
@@ -3903,6 +3875,7 @@ async function main() {
|
|
|
3903
3875
|
jsonOutput: json,
|
|
3904
3876
|
bearerToken: gatewayAuth.bearerToken,
|
|
3905
3877
|
gatewayBaseUrl: gatewayAuth.gatewayBaseUrl,
|
|
3878
|
+
gatewayHeaders: gatewayAuth.gatewayHeaders,
|
|
3906
3879
|
});
|
|
3907
3880
|
return;
|
|
3908
3881
|
}
|
|
@@ -4165,15 +4138,15 @@ async function main() {
|
|
|
4165
4138
|
}
|
|
4166
4139
|
|
|
4167
4140
|
if (command === "memory") {
|
|
4168
|
-
const
|
|
4141
|
+
const authedRuntime = requireAuthenticatedRuntime(runtime, "dench memory");
|
|
4169
4142
|
if (subcommand === "search") {
|
|
4170
4143
|
const query = positionalsFrom(2).join(" ").trim();
|
|
4171
4144
|
if (!query) throw new Error('Usage: dench memory search "query"');
|
|
4172
4145
|
print(
|
|
4173
|
-
await
|
|
4146
|
+
await authedRuntime.client.query(
|
|
4174
4147
|
api.functions.agentWorkspace.agentSearchMemory,
|
|
4175
4148
|
{
|
|
4176
|
-
sessionToken:
|
|
4149
|
+
sessionToken: authedRuntime.sessionToken,
|
|
4177
4150
|
query,
|
|
4178
4151
|
limit: parseNumberOption("--limit"),
|
|
4179
4152
|
},
|
|
@@ -4188,15 +4161,16 @@ async function main() {
|
|
|
4188
4161
|
throw new Error('Usage: dench memory save <key> "text"');
|
|
4189
4162
|
}
|
|
4190
4163
|
print(
|
|
4191
|
-
await
|
|
4164
|
+
await authedRuntime.client.mutation(
|
|
4192
4165
|
api.functions.agentWorkspace.agentSaveMemory,
|
|
4193
4166
|
{
|
|
4194
|
-
sessionToken:
|
|
4167
|
+
sessionToken: authedRuntime.sessionToken,
|
|
4195
4168
|
key,
|
|
4196
4169
|
kind: (option("--kind") ?? "fact") as never,
|
|
4197
4170
|
text,
|
|
4198
4171
|
tags: parseCommaOption("--tags"),
|
|
4199
4172
|
sensitivity: (option("--sensitivity") ?? "normal") as never,
|
|
4173
|
+
visibility: parseMemoryVisibilityOption() as never,
|
|
4200
4174
|
},
|
|
4201
4175
|
),
|
|
4202
4176
|
);
|
|
@@ -4216,9 +4190,9 @@ async function main() {
|
|
|
4216
4190
|
}
|
|
4217
4191
|
|
|
4218
4192
|
if (command === "status") {
|
|
4219
|
-
if (runtime.mode === "session") {
|
|
4193
|
+
if (runtime.mode === "session" || runtime.mode === "apiKey") {
|
|
4220
4194
|
const status = (await runtime.client.query(
|
|
4221
|
-
api.functions.agentWorkspace.
|
|
4195
|
+
api.functions.agentWorkspace.whatCanIDoHere,
|
|
4222
4196
|
{
|
|
4223
4197
|
sessionToken: runtime.sessionToken,
|
|
4224
4198
|
},
|
|
@@ -4230,13 +4204,6 @@ async function main() {
|
|
|
4230
4204
|
print(status);
|
|
4231
4205
|
return;
|
|
4232
4206
|
}
|
|
4233
|
-
if (runtime.mode === "apiKey") {
|
|
4234
|
-
// No agent session → no agent-status query. Fall back to a thin
|
|
4235
|
-
// env-derived snapshot so sandbox callers still get something
|
|
4236
|
-
// useful from `dench status`.
|
|
4237
|
-
print(await buildContext(runtime));
|
|
4238
|
-
return;
|
|
4239
|
-
}
|
|
4240
4207
|
const data = await devOverview(runtime);
|
|
4241
4208
|
const output = scopedStatus
|
|
4242
4209
|
? summarizeDevMine(runtime.workspaceArgs.workspaceSlug, data)
|
|
@@ -4248,9 +4215,9 @@ async function main() {
|
|
|
4248
4215
|
}
|
|
4249
4216
|
|
|
4250
4217
|
if (command === "agents") {
|
|
4251
|
-
if (runtime.mode === "session") {
|
|
4218
|
+
if (runtime.mode === "session" || runtime.mode === "apiKey") {
|
|
4252
4219
|
const status = await runtime.client.query(
|
|
4253
|
-
api.functions.agentWorkspace.
|
|
4220
|
+
api.functions.agentWorkspace.whatCanIDoHere,
|
|
4254
4221
|
{
|
|
4255
4222
|
sessionToken: runtime.sessionToken,
|
|
4256
4223
|
},
|
|
@@ -4258,18 +4225,15 @@ async function main() {
|
|
|
4258
4225
|
print(status.agents);
|
|
4259
4226
|
return;
|
|
4260
4227
|
}
|
|
4261
|
-
if (runtime.mode === "apiKey") {
|
|
4262
|
-
throw loginRequiredError("dench agents");
|
|
4263
|
-
}
|
|
4264
4228
|
const data = await devOverview(runtime);
|
|
4265
4229
|
print(data.agents);
|
|
4266
4230
|
return;
|
|
4267
4231
|
}
|
|
4268
4232
|
|
|
4269
4233
|
if (command === "approvals") {
|
|
4270
|
-
if (runtime.mode === "session") {
|
|
4234
|
+
if (runtime.mode === "session" || runtime.mode === "apiKey") {
|
|
4271
4235
|
const status = await runtime.client.query(
|
|
4272
|
-
api.functions.agentWorkspace.
|
|
4236
|
+
api.functions.agentWorkspace.whatCanIDoHere,
|
|
4273
4237
|
{
|
|
4274
4238
|
sessionToken: runtime.sessionToken,
|
|
4275
4239
|
},
|
|
@@ -4277,9 +4241,6 @@ async function main() {
|
|
|
4277
4241
|
print(status.approvals);
|
|
4278
4242
|
return;
|
|
4279
4243
|
}
|
|
4280
|
-
if (runtime.mode === "apiKey") {
|
|
4281
|
-
throw loginRequiredError("dench approvals");
|
|
4282
|
-
}
|
|
4283
4244
|
const data = await devOverview(runtime);
|
|
4284
4245
|
print(data.approvals);
|
|
4285
4246
|
return;
|
|
@@ -4309,7 +4270,7 @@ async function main() {
|
|
|
4309
4270
|
if (command === "approval" && subcommand === "request") {
|
|
4310
4271
|
const title = positional(2);
|
|
4311
4272
|
if (!title) throw new Error("Missing approval title");
|
|
4312
|
-
if (runtime.mode === "session") {
|
|
4273
|
+
if (runtime.mode === "session" || runtime.mode === "apiKey") {
|
|
4313
4274
|
print(
|
|
4314
4275
|
await runtime.client.mutation(
|
|
4315
4276
|
api.functions.agentWorkspace.agentRequestApproval,
|
|
@@ -4323,7 +4284,6 @@ async function main() {
|
|
|
4323
4284
|
);
|
|
4324
4285
|
return;
|
|
4325
4286
|
}
|
|
4326
|
-
rejectApiKeyForAgentCommand(runtime, "dench approval request");
|
|
4327
4287
|
const approvalId = await runtime.client.mutation(
|
|
4328
4288
|
api.functions.agentWorkspace.devRequestApproval,
|
|
4329
4289
|
{
|
|
@@ -4346,15 +4306,15 @@ async function main() {
|
|
|
4346
4306
|
) {
|
|
4347
4307
|
const approvalId = positional(2);
|
|
4348
4308
|
if (!approvalId) throw new Error("Missing approval id");
|
|
4349
|
-
const
|
|
4309
|
+
const authedRuntime = requireAuthenticatedRuntime(
|
|
4350
4310
|
runtime,
|
|
4351
4311
|
"dench approval approve/reject",
|
|
4352
4312
|
);
|
|
4353
4313
|
const decision = subcommand === "approve" ? "approved" : "rejected";
|
|
4354
|
-
const result = await
|
|
4314
|
+
const result = await authedRuntime.client.mutation(
|
|
4355
4315
|
api.functions.agentWorkspace.agentDecideApproval,
|
|
4356
4316
|
{
|
|
4357
|
-
sessionToken:
|
|
4317
|
+
sessionToken: authedRuntime.sessionToken,
|
|
4358
4318
|
approvalId: approvalId as never,
|
|
4359
4319
|
decision,
|
|
4360
4320
|
evidence: option("--evidence"),
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dench.com/cli",
|
|
3
|
-
"version": "2.0
|
|
3
|
+
"version": "2.1.0",
|
|
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": {
|
package/tools.ts
CHANGED
|
@@ -50,11 +50,12 @@ export type ToolCliContext = {
|
|
|
50
50
|
jsonOutput: boolean;
|
|
51
51
|
bearerToken?: string;
|
|
52
52
|
gatewayBaseUrl?: string;
|
|
53
|
+
gatewayHeaders?: Record<string, string>;
|
|
53
54
|
};
|
|
54
55
|
|
|
55
56
|
type GatewayAuthContext = Pick<
|
|
56
57
|
ToolCliContext,
|
|
57
|
-
"bearerToken" | "gatewayBaseUrl"
|
|
58
|
+
"bearerToken" | "gatewayBaseUrl" | "gatewayHeaders"
|
|
58
59
|
>;
|
|
59
60
|
|
|
60
61
|
export async function runToolCommand(ctx: ToolCliContext): Promise<void> {
|
|
@@ -453,6 +454,7 @@ async function callGateway(
|
|
|
453
454
|
headers: {
|
|
454
455
|
"content-type": "application/json",
|
|
455
456
|
authorization: `Bearer ${bearerToken}`,
|
|
457
|
+
...(ctx.gatewayHeaders ?? {}),
|
|
456
458
|
},
|
|
457
459
|
};
|
|
458
460
|
if (body !== undefined) init.body = JSON.stringify(body);
|