@dench.com/cli 2.1.0 → 2.1.2
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/browser.ts +507 -0
- package/crm.ts +291 -37
- package/dench.ts +124 -41
- package/fs-daemon.ts +201 -2
- package/lib/api-schemas.ts +1100 -0
- package/lib/command-registry.ts +1664 -0
- package/lib/enrichment-gateway.ts +22 -0
- package/lib/organizationSelector.ts +58 -0
- package/linkedin.ts +210 -0
- package/package.json +6 -2
package/crm.ts
CHANGED
|
@@ -16,6 +16,7 @@
|
|
|
16
16
|
*
|
|
17
17
|
* The full surface mirrors the plan's "dench-cli CRM surface" section:
|
|
18
18
|
* crm objects list / get / create / update / delete / rename
|
|
19
|
+
* crm views list / get / entries
|
|
19
20
|
* crm fields list / create / update / delete / reorder
|
|
20
21
|
* crm entries list / get / create / create-many / update / update-many / delete / bulk-delete
|
|
21
22
|
* crm cells get / set / set-many / append
|
|
@@ -94,6 +95,32 @@ type CrmCliContext = {
|
|
|
94
95
|
// don't need to change their try/catch shape.
|
|
95
96
|
class CrmCliError extends Error {}
|
|
96
97
|
|
|
98
|
+
function userFacingConvexErrorMessage(error: unknown): string | null {
|
|
99
|
+
if (!error || typeof error !== "object") return null;
|
|
100
|
+
const data = (error as { data?: unknown }).data;
|
|
101
|
+
if (typeof data === "string" && data.trim().length > 0) {
|
|
102
|
+
return data.trim();
|
|
103
|
+
}
|
|
104
|
+
if (data && typeof data === "object" && !Array.isArray(data)) {
|
|
105
|
+
const record = data as Record<string, unknown>;
|
|
106
|
+
for (const key of ["message", "error", "reason"]) {
|
|
107
|
+
const value = record[key];
|
|
108
|
+
if (typeof value === "string" && value.trim().length > 0) {
|
|
109
|
+
return value.trim();
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
return null;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function normalizeRpcError(error: unknown): Error {
|
|
117
|
+
if (error instanceof CrmCliError) return error;
|
|
118
|
+
return new CrmCliError(
|
|
119
|
+
userFacingConvexErrorMessage(error) ??
|
|
120
|
+
(error instanceof Error ? error.message : String(error)),
|
|
121
|
+
);
|
|
122
|
+
}
|
|
123
|
+
|
|
97
124
|
function shift(args: string[], expected: string): string {
|
|
98
125
|
try {
|
|
99
126
|
return shiftRaw(args, expected);
|
|
@@ -279,10 +306,14 @@ async function callQuery(
|
|
|
279
306
|
fn: Parameters<ConvexHttpClient["query"]>[0],
|
|
280
307
|
args: Record<string, unknown> = {},
|
|
281
308
|
): Promise<unknown> {
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
309
|
+
try {
|
|
310
|
+
return await ctx.convex.query(fn, {
|
|
311
|
+
...args,
|
|
312
|
+
...(ctx.sessionToken ? { sessionToken: ctx.sessionToken } : {}),
|
|
313
|
+
} as never);
|
|
314
|
+
} catch (error) {
|
|
315
|
+
throw normalizeRpcError(error);
|
|
316
|
+
}
|
|
286
317
|
}
|
|
287
318
|
|
|
288
319
|
async function callMutation(
|
|
@@ -290,10 +321,37 @@ async function callMutation(
|
|
|
290
321
|
fn: Parameters<ConvexHttpClient["mutation"]>[0],
|
|
291
322
|
args: Record<string, unknown> = {},
|
|
292
323
|
): Promise<unknown> {
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
324
|
+
try {
|
|
325
|
+
return await ctx.convex.mutation(fn, {
|
|
326
|
+
...args,
|
|
327
|
+
...(ctx.sessionToken ? { sessionToken: ctx.sessionToken } : {}),
|
|
328
|
+
} as never);
|
|
329
|
+
} catch (error) {
|
|
330
|
+
throw normalizeRpcError(error);
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
async function suppressForwardedConvexLogs<T>(
|
|
335
|
+
fn: () => Promise<T>,
|
|
336
|
+
): Promise<T> {
|
|
337
|
+
const originalLog = console.log;
|
|
338
|
+
const originalInfo = console.info;
|
|
339
|
+
const shouldSuppress = (args: unknown[]) =>
|
|
340
|
+
typeof args[0] === "string" && args[0].startsWith("[CONVEX ");
|
|
341
|
+
console.log = (...args: Parameters<typeof console.log>) => {
|
|
342
|
+
if (shouldSuppress(args)) return;
|
|
343
|
+
originalLog(...args);
|
|
344
|
+
};
|
|
345
|
+
console.info = (...args: Parameters<typeof console.info>) => {
|
|
346
|
+
if (shouldSuppress(args)) return;
|
|
347
|
+
originalInfo(...args);
|
|
348
|
+
};
|
|
349
|
+
try {
|
|
350
|
+
return await fn();
|
|
351
|
+
} finally {
|
|
352
|
+
console.log = originalLog;
|
|
353
|
+
console.info = originalInfo;
|
|
354
|
+
}
|
|
297
355
|
}
|
|
298
356
|
|
|
299
357
|
function out(ctx: CrmCliContext, value: unknown): void {
|
|
@@ -376,6 +434,9 @@ const api = {
|
|
|
376
434
|
objects: {
|
|
377
435
|
list: makeFunctionReference<"query">("functions/crm/objects:list"),
|
|
378
436
|
get: makeFunctionReference<"query">("functions/crm/objects:get"),
|
|
437
|
+
materializeView: makeFunctionReference<"query">(
|
|
438
|
+
"functions/crm/objects:materializeView",
|
|
439
|
+
),
|
|
379
440
|
create: makeFunctionReference<"mutation">("functions/crm/objects:create"),
|
|
380
441
|
update: makeFunctionReference<"mutation">("functions/crm/objects:update"),
|
|
381
442
|
rename: makeFunctionReference<"mutation">("functions/crm/objects:rename"),
|
|
@@ -473,6 +534,8 @@ export async function runCrmCommand(opts: {
|
|
|
473
534
|
switch (subcommand) {
|
|
474
535
|
case "objects":
|
|
475
536
|
return await runObjectsCommand(ctx);
|
|
537
|
+
case "views":
|
|
538
|
+
return await runViewsCommand(ctx);
|
|
476
539
|
case "fields":
|
|
477
540
|
return await runFieldsCommand(ctx);
|
|
478
541
|
case "entries":
|
|
@@ -602,6 +665,181 @@ async function runObjectsCommand(ctx: CrmCliContext): Promise<void> {
|
|
|
602
665
|
}
|
|
603
666
|
}
|
|
604
667
|
|
|
668
|
+
type SavedViewForCli = Record<string, unknown> & {
|
|
669
|
+
name?: unknown;
|
|
670
|
+
pinnedEntryIds?: unknown;
|
|
671
|
+
filters?: unknown;
|
|
672
|
+
sort?: unknown;
|
|
673
|
+
};
|
|
674
|
+
|
|
675
|
+
type CrmObjectWithViews = {
|
|
676
|
+
name?: string;
|
|
677
|
+
savedViews?: SavedViewForCli[];
|
|
678
|
+
};
|
|
679
|
+
|
|
680
|
+
function savedViewsFromObject(
|
|
681
|
+
objectName: string,
|
|
682
|
+
object: unknown,
|
|
683
|
+
): SavedViewForCli[] {
|
|
684
|
+
if (!object || typeof object !== "object") {
|
|
685
|
+
throw new CrmCliError(`CRM object "${objectName}" not found`);
|
|
686
|
+
}
|
|
687
|
+
const savedViews = (object as CrmObjectWithViews).savedViews;
|
|
688
|
+
return Array.isArray(savedViews) ? savedViews : [];
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
function readViewName(view: SavedViewForCli): string | null {
|
|
692
|
+
return typeof view.name === "string" && view.name.trim()
|
|
693
|
+
? view.name.trim()
|
|
694
|
+
: null;
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
function resolveSavedView(
|
|
698
|
+
objectName: string,
|
|
699
|
+
views: SavedViewForCli[],
|
|
700
|
+
requestedName: string,
|
|
701
|
+
): SavedViewForCli {
|
|
702
|
+
const trimmed = requestedName.trim();
|
|
703
|
+
const exact = views.find((view) => readViewName(view) === trimmed);
|
|
704
|
+
if (exact) return exact;
|
|
705
|
+
|
|
706
|
+
const lower = trimmed.toLowerCase();
|
|
707
|
+
const insensitiveMatches = views.filter(
|
|
708
|
+
(view) => readViewName(view)?.toLowerCase() === lower,
|
|
709
|
+
);
|
|
710
|
+
if (insensitiveMatches.length === 1) return insensitiveMatches[0];
|
|
711
|
+
if (insensitiveMatches.length > 1) {
|
|
712
|
+
throw new CrmCliError(
|
|
713
|
+
`Multiple views named like "${requestedName}" exist on ${objectName}; use the exact case-sensitive name.`,
|
|
714
|
+
);
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
const available = views.map(readViewName).filter(Boolean).join(", ");
|
|
718
|
+
throw new CrmCliError(
|
|
719
|
+
`View "${requestedName}" not found on ${objectName}${
|
|
720
|
+
available ? `. Available views: ${available}` : ". No saved views exist."
|
|
721
|
+
}`,
|
|
722
|
+
);
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
function summarizeSavedView(view: SavedViewForCli): Record<string, unknown> {
|
|
726
|
+
const pinned = Array.isArray(view.pinnedEntryIds)
|
|
727
|
+
? view.pinnedEntryIds
|
|
728
|
+
: undefined;
|
|
729
|
+
return {
|
|
730
|
+
...view,
|
|
731
|
+
pinnedCount: pinned?.length ?? 0,
|
|
732
|
+
};
|
|
733
|
+
}
|
|
734
|
+
|
|
735
|
+
async function runViewsCommand(ctx: CrmCliContext): Promise<void> {
|
|
736
|
+
const verb = ctx.args.shift();
|
|
737
|
+
switch (verb) {
|
|
738
|
+
case "list": {
|
|
739
|
+
const objectName = shift(ctx.args, "object name");
|
|
740
|
+
assertNoUnknownFlags(ctx.args, "crm views list");
|
|
741
|
+
const object = await callQuery(ctx, api.objects.get, {
|
|
742
|
+
name: objectName,
|
|
743
|
+
});
|
|
744
|
+
const views = savedViewsFromObject(objectName, object).map(
|
|
745
|
+
summarizeSavedView,
|
|
746
|
+
);
|
|
747
|
+
out(ctx, views);
|
|
748
|
+
return;
|
|
749
|
+
}
|
|
750
|
+
case "get": {
|
|
751
|
+
const objectName = shift(ctx.args, "object name");
|
|
752
|
+
const viewName = shift(ctx.args, "view name");
|
|
753
|
+
assertNoUnknownFlags(ctx.args, "crm views get");
|
|
754
|
+
const object = await callQuery(ctx, api.objects.get, {
|
|
755
|
+
name: objectName,
|
|
756
|
+
});
|
|
757
|
+
const view = resolveSavedView(
|
|
758
|
+
objectName,
|
|
759
|
+
savedViewsFromObject(objectName, object),
|
|
760
|
+
viewName,
|
|
761
|
+
);
|
|
762
|
+
out(ctx, summarizeSavedView(view));
|
|
763
|
+
return;
|
|
764
|
+
}
|
|
765
|
+
case "entries": {
|
|
766
|
+
const objectName = shift(ctx.args, "object name");
|
|
767
|
+
const viewName = shift(ctx.args, "view name");
|
|
768
|
+
const page = Number.parseInt(getFlag(ctx.args, "--page") ?? "1", 10);
|
|
769
|
+
const pageSize = Number.parseInt(
|
|
770
|
+
getFlagWithAliases(ctx.args, "--page-size", ["--limit"]) ?? "100",
|
|
771
|
+
10,
|
|
772
|
+
);
|
|
773
|
+
const withMeta = hasFlag(ctx.args, "--with-meta");
|
|
774
|
+
assertNoUnknownFlags(ctx.args, "crm views entries");
|
|
775
|
+
if (!Number.isFinite(page) || page < 1) {
|
|
776
|
+
throw new CrmCliError(`--page must be a positive integer, got ${page}`);
|
|
777
|
+
}
|
|
778
|
+
if (!Number.isFinite(pageSize) || pageSize < 1) {
|
|
779
|
+
throw new CrmCliError(
|
|
780
|
+
`--page-size/--limit must be a positive integer, got ${pageSize}`,
|
|
781
|
+
);
|
|
782
|
+
}
|
|
783
|
+
|
|
784
|
+
const object = await callQuery(ctx, api.objects.get, {
|
|
785
|
+
name: objectName,
|
|
786
|
+
});
|
|
787
|
+
const view = resolveSavedView(
|
|
788
|
+
objectName,
|
|
789
|
+
savedViewsFromObject(objectName, object),
|
|
790
|
+
viewName,
|
|
791
|
+
);
|
|
792
|
+
const pinnedEntryIds = Array.isArray(view.pinnedEntryIds)
|
|
793
|
+
? view.pinnedEntryIds.filter(
|
|
794
|
+
(id): id is string => typeof id === "string" && id.length > 0,
|
|
795
|
+
)
|
|
796
|
+
: undefined;
|
|
797
|
+
const materialized = (await suppressForwardedConvexLogs(() =>
|
|
798
|
+
callQuery(ctx, api.objects.materializeView, {
|
|
799
|
+
objectName,
|
|
800
|
+
page,
|
|
801
|
+
pageSize,
|
|
802
|
+
filters: view.filters,
|
|
803
|
+
sort: Array.isArray(view.sort) ? view.sort : undefined,
|
|
804
|
+
pinnedEntryIds,
|
|
805
|
+
includeReverseRelations: false,
|
|
806
|
+
}),
|
|
807
|
+
)) as {
|
|
808
|
+
entries?: unknown[];
|
|
809
|
+
totalCount?: number;
|
|
810
|
+
page?: number;
|
|
811
|
+
pageSize?: number;
|
|
812
|
+
truncated?: boolean;
|
|
813
|
+
} | null;
|
|
814
|
+
|
|
815
|
+
if (!materialized) {
|
|
816
|
+
throw new CrmCliError(`CRM object "${objectName}" not found`);
|
|
817
|
+
}
|
|
818
|
+
const entries = Array.isArray(materialized.entries)
|
|
819
|
+
? materialized.entries
|
|
820
|
+
: [];
|
|
821
|
+
if (!withMeta) {
|
|
822
|
+
out(ctx, entries);
|
|
823
|
+
return;
|
|
824
|
+
}
|
|
825
|
+
out(ctx, {
|
|
826
|
+
objectName,
|
|
827
|
+
viewName: readViewName(view) ?? viewName,
|
|
828
|
+
view: summarizeSavedView(view),
|
|
829
|
+
totalCount: materialized.totalCount ?? entries.length,
|
|
830
|
+
returnedCount: entries.length,
|
|
831
|
+
page: materialized.page ?? page,
|
|
832
|
+
pageSize: materialized.pageSize ?? pageSize,
|
|
833
|
+
truncated: materialized.truncated ?? false,
|
|
834
|
+
entries,
|
|
835
|
+
});
|
|
836
|
+
return;
|
|
837
|
+
}
|
|
838
|
+
default:
|
|
839
|
+
throw new CrmCliError(`Unknown crm views verb: ${verb ?? "<none>"}`);
|
|
840
|
+
}
|
|
841
|
+
}
|
|
842
|
+
|
|
605
843
|
async function runFieldsCommand(ctx: CrmCliContext): Promise<void> {
|
|
606
844
|
const verb = ctx.args.shift();
|
|
607
845
|
switch (verb) {
|
|
@@ -743,7 +981,9 @@ async function runFieldsCommand(ctx: CrmCliContext): Promise<void> {
|
|
|
743
981
|
const color = getFlag(ctx.args, "--color");
|
|
744
982
|
const positionStr = getFlag(ctx.args, "--position");
|
|
745
983
|
const position =
|
|
746
|
-
positionStr !== undefined
|
|
984
|
+
positionStr !== undefined
|
|
985
|
+
? Number.parseInt(positionStr, 10)
|
|
986
|
+
: undefined;
|
|
747
987
|
if (position !== undefined && !Number.isFinite(position)) {
|
|
748
988
|
throw new CrmCliError(
|
|
749
989
|
`--position must be an integer, got "${positionStr}"`,
|
|
@@ -1902,14 +2142,22 @@ async function callEnrichmentGatewayForEntry(args: {
|
|
|
1902
2142
|
|
|
1903
2143
|
const PEOPLE_FULL_NAME_FIELDS = ["Name", "Full Name", "Person Name"] as const;
|
|
1904
2144
|
const PEOPLE_FIRST_NAME_FIELDS = ["First Name", "Given Name"] as const;
|
|
1905
|
-
const PEOPLE_LAST_NAME_FIELDS = [
|
|
2145
|
+
const PEOPLE_LAST_NAME_FIELDS = [
|
|
2146
|
+
"Last Name",
|
|
2147
|
+
"Family Name",
|
|
2148
|
+
"Surname",
|
|
2149
|
+
] as const;
|
|
1906
2150
|
const PEOPLE_EMAIL_FIELDS = [
|
|
1907
2151
|
"Email",
|
|
1908
2152
|
"Email Address",
|
|
1909
2153
|
"Work Email",
|
|
1910
2154
|
"Personal Email",
|
|
1911
2155
|
] as const;
|
|
1912
|
-
const PEOPLE_LINKEDIN_FIELDS = [
|
|
2156
|
+
const PEOPLE_LINKEDIN_FIELDS = [
|
|
2157
|
+
"LinkedIn URL",
|
|
2158
|
+
"LinkedIn",
|
|
2159
|
+
"Linkedin URL",
|
|
2160
|
+
] as const;
|
|
1913
2161
|
const PEOPLE_DOMAIN_FIELDS = ["Domain", "Company Domain"] as const;
|
|
1914
2162
|
|
|
1915
2163
|
const COMPANY_WEBSITE_FIELDS = ["Website", "Domain", "URL", "Site"] as const;
|
|
@@ -2012,6 +2260,7 @@ async function enrichOneEntry(
|
|
|
2012
2260
|
entry: CrmEntryForEnrichment;
|
|
2013
2261
|
},
|
|
2014
2262
|
): Promise<Record<string, unknown>> {
|
|
2263
|
+
let value: unknown;
|
|
2015
2264
|
try {
|
|
2016
2265
|
const payload = await callEnrichmentGatewayForEntry({
|
|
2017
2266
|
target: args.target,
|
|
@@ -2019,32 +2268,7 @@ async function enrichOneEntry(
|
|
|
2019
2268
|
provider: args.provider,
|
|
2020
2269
|
gateway: ctx.enrichmentGateway,
|
|
2021
2270
|
});
|
|
2022
|
-
|
|
2023
|
-
if (value === null) {
|
|
2024
|
-
return {
|
|
2025
|
-
entryId: args.entry.id,
|
|
2026
|
-
ok: false,
|
|
2027
|
-
reason: "value_not_found",
|
|
2028
|
-
};
|
|
2029
|
-
}
|
|
2030
|
-
const writeResult = (await callMutation(
|
|
2031
|
-
ctx,
|
|
2032
|
-
api.enrich.applyCellEnrichmentResult,
|
|
2033
|
-
{
|
|
2034
|
-
objectName: args.objectName,
|
|
2035
|
-
entryId: args.entry.id,
|
|
2036
|
-
fieldName: args.fieldName,
|
|
2037
|
-
value,
|
|
2038
|
-
provider: args.provider ?? "gateway",
|
|
2039
|
-
overwrite: args.overwrite,
|
|
2040
|
-
},
|
|
2041
|
-
)) as Record<string, unknown>;
|
|
2042
|
-
return {
|
|
2043
|
-
entryId: args.entry.id,
|
|
2044
|
-
ok: writeResult.ok === true,
|
|
2045
|
-
value,
|
|
2046
|
-
...writeResult,
|
|
2047
|
-
};
|
|
2271
|
+
value = extractEnrichmentValue(payload, args.target.column);
|
|
2048
2272
|
} catch (error) {
|
|
2049
2273
|
if (
|
|
2050
2274
|
error instanceof EnrichmentGatewayError ||
|
|
@@ -2062,6 +2286,31 @@ async function enrichOneEntry(
|
|
|
2062
2286
|
}
|
|
2063
2287
|
throw error;
|
|
2064
2288
|
}
|
|
2289
|
+
if (value === null) {
|
|
2290
|
+
return {
|
|
2291
|
+
entryId: args.entry.id,
|
|
2292
|
+
ok: false,
|
|
2293
|
+
reason: "value_not_found",
|
|
2294
|
+
};
|
|
2295
|
+
}
|
|
2296
|
+
const writeResult = (await callMutation(
|
|
2297
|
+
ctx,
|
|
2298
|
+
api.enrich.applyCellEnrichmentResult,
|
|
2299
|
+
{
|
|
2300
|
+
objectName: args.objectName,
|
|
2301
|
+
entryId: args.entry.id,
|
|
2302
|
+
fieldName: args.fieldName,
|
|
2303
|
+
value,
|
|
2304
|
+
provider: args.provider ?? "gateway",
|
|
2305
|
+
overwrite: args.overwrite,
|
|
2306
|
+
},
|
|
2307
|
+
)) as Record<string, unknown>;
|
|
2308
|
+
return {
|
|
2309
|
+
entryId: args.entry.id,
|
|
2310
|
+
ok: writeResult.ok === true,
|
|
2311
|
+
value,
|
|
2312
|
+
...writeResult,
|
|
2313
|
+
};
|
|
2065
2314
|
}
|
|
2066
2315
|
|
|
2067
2316
|
async function enrichCellThroughGateway(
|
|
@@ -2373,6 +2622,11 @@ Objects (CRM tables):
|
|
|
2373
2622
|
dench crm objects rename <from> <to>
|
|
2374
2623
|
dench crm objects delete <name>
|
|
2375
2624
|
|
|
2625
|
+
Views (saved CRM subsets):
|
|
2626
|
+
dench crm views list <object> [--json]
|
|
2627
|
+
dench crm views get <object> <view-name> [--json]
|
|
2628
|
+
dench crm views entries <object> <view-name> [--limit N] [--page N] [--with-meta] [--json]
|
|
2629
|
+
|
|
2376
2630
|
Fields (columns):
|
|
2377
2631
|
dench crm fields list <object> [--json]
|
|
2378
2632
|
dench crm fields create <object> <name> --type text|email|phone|url|number|boolean|date|enum|relation|user|tags|action|file|richtext \\
|
package/dench.ts
CHANGED
|
@@ -27,6 +27,7 @@ import {
|
|
|
27
27
|
resolveHostFromArgs,
|
|
28
28
|
STAGING_HOST,
|
|
29
29
|
} from "./host";
|
|
30
|
+
import { resolveOrganizationBySelector } from "./lib/organizationSelector";
|
|
30
31
|
import { formatApprovalOpenMessage, openUrl } from "./openUrl";
|
|
31
32
|
import {
|
|
32
33
|
explicitSessionKeyInput,
|
|
@@ -94,6 +95,10 @@ type ConfigFile = {
|
|
|
94
95
|
sessions?: Record<string, StoredSession>;
|
|
95
96
|
};
|
|
96
97
|
|
|
98
|
+
function createCliConvexClient(convexUrl: string): ConvexHttpClient {
|
|
99
|
+
return new ConvexHttpClient(convexUrl, { logger: false });
|
|
100
|
+
}
|
|
101
|
+
|
|
97
102
|
type WorkspaceOverview = {
|
|
98
103
|
agents: Array<{ _id: string } & JsonRecord>;
|
|
99
104
|
projects: JsonRecord[];
|
|
@@ -223,6 +228,7 @@ const BOOLEAN_FLAGS = new Set([
|
|
|
223
228
|
"--compact",
|
|
224
229
|
"--dev",
|
|
225
230
|
"--follow",
|
|
231
|
+
"--full-page",
|
|
226
232
|
"--help",
|
|
227
233
|
"--mine",
|
|
228
234
|
"--self",
|
|
@@ -470,6 +476,20 @@ Live web search (Exa via the Dench Gateway, auths with the active \`dench signin
|
|
|
470
476
|
dench search answer "<query>" [--json]
|
|
471
477
|
Help: dench search help
|
|
472
478
|
|
|
479
|
+
Cloud browser (Browserbase, runs entirely in Dench Cloud):
|
|
480
|
+
dench browser open [url] [--json]
|
|
481
|
+
dench browser inspect [--json]
|
|
482
|
+
dench browser navigate <url>
|
|
483
|
+
dench browser click --selector <css> | --text "Button text"
|
|
484
|
+
dench browser fill --label "Email" --value "me@example.com"
|
|
485
|
+
dench browser screenshot [--output page.jpg] [--full-page]
|
|
486
|
+
dench browser stop
|
|
487
|
+
Help: dench browser help
|
|
488
|
+
|
|
489
|
+
LinkedIn outreach (Browserbase, verified before CRM update):
|
|
490
|
+
dench linkedin connect <profileUrl> [--entry people:<entryId>] [--json]
|
|
491
|
+
Help: dench linkedin help
|
|
492
|
+
|
|
473
493
|
Image generation / editing (via the Dench Gateway):
|
|
474
494
|
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]
|
|
475
495
|
dench image edit "<prompt>" --input <path> [--input <path> ...] [--mask <path>] [--size ...] [--quality ...] [--format ...] [--save-path ...] [--output ...] [--json]
|
|
@@ -499,6 +519,7 @@ Spawn a new chat-turn workflow (same as the web UI):
|
|
|
499
519
|
File sync:
|
|
500
520
|
dench fs status [--json] [--workspace /workspace] [--no-hash] [--drift-limit N]
|
|
501
521
|
dench fs sync --initial --org-id <orgId> [--workspace /workspace]
|
|
522
|
+
dench fs pull <path> [<path>...] [--wait] [--timeout-ms N] [--json]
|
|
502
523
|
dench fs stream-open <path>
|
|
503
524
|
dench fs stream-append <token> <chunk>
|
|
504
525
|
dench fs stream-close <token>
|
|
@@ -1585,7 +1606,7 @@ async function signin() {
|
|
|
1585
1606
|
const explicitKey = explicitSessionKeyInput({ args: filteredArgs });
|
|
1586
1607
|
const host = resolveHostFromArgs(filteredArgs, undefined);
|
|
1587
1608
|
const convexUrl = await discoverConvexUrl(host);
|
|
1588
|
-
const client =
|
|
1609
|
+
const client = createCliConvexClient(convexUrl);
|
|
1589
1610
|
|
|
1590
1611
|
const sessionToken = `dch_agent_${randomUrlSafe(32)}`;
|
|
1591
1612
|
const sessionTokenHash = await sha256Hex(sessionToken);
|
|
@@ -1970,11 +1991,9 @@ async function resolveOrgChoiceForOtp(input: {
|
|
|
1970
1991
|
orgSelector: string | null;
|
|
1971
1992
|
}): Promise<{ organizationId?: string; newOrganizationName?: string }> {
|
|
1972
1993
|
if (input.orgSelector) {
|
|
1973
|
-
const match =
|
|
1974
|
-
|
|
1975
|
-
|
|
1976
|
-
org.slug === input.orgSelector ||
|
|
1977
|
-
org.name === input.orgSelector,
|
|
1994
|
+
const match = resolveOrganizationBySelector(
|
|
1995
|
+
input.organizations,
|
|
1996
|
+
input.orgSelector,
|
|
1978
1997
|
);
|
|
1979
1998
|
if (!match) {
|
|
1980
1999
|
throw new CliError(
|
|
@@ -2325,7 +2344,7 @@ async function getRuntime() {
|
|
|
2325
2344
|
return {
|
|
2326
2345
|
mode: "session" as const,
|
|
2327
2346
|
host: resolveApiHost(host),
|
|
2328
|
-
client:
|
|
2347
|
+
client: createCliConvexClient(stored.session.convexUrl),
|
|
2329
2348
|
sessionToken: stored.session.sessionToken,
|
|
2330
2349
|
organization: stored.session.organization,
|
|
2331
2350
|
};
|
|
@@ -2340,48 +2359,55 @@ async function getRuntime() {
|
|
|
2340
2359
|
});
|
|
2341
2360
|
}
|
|
2342
2361
|
|
|
2343
|
-
// Sandbox session-mode path: workflow minting provisions a real
|
|
2344
|
-
// dch_agent_* token at sandbox-create time. Prefer it over apiKey so
|
|
2345
|
-
// agentWorkspace.* commands (Composio tools) work inside sandboxes.
|
|
2346
|
-
const sandboxAgentToken = process.env.DENCH_AGENT_SESSION_TOKEN?.trim();
|
|
2347
2362
|
const sandboxConvexUrl = resolveApiKeyConvexUrl();
|
|
2348
|
-
|
|
2349
|
-
|
|
2350
|
-
|
|
2363
|
+
const orgId = process.env.DENCH_ORG_ID?.trim();
|
|
2364
|
+
const orgSlug = process.env.DENCH_ORG_SLUG?.trim();
|
|
2365
|
+
const sandboxOrganization =
|
|
2366
|
+
orgId && orgSlug
|
|
2367
|
+
? { id: orgId, name: orgSlug, slug: orgSlug }
|
|
2368
|
+
: undefined;
|
|
2369
|
+
|
|
2370
|
+
// Sandbox / automation path. PREFER the unified Dench API key over the
|
|
2371
|
+
// `dch_agent_*` agent-session token.
|
|
2372
|
+
//
|
|
2373
|
+
// Both are baked into sandbox envVars (src/lib/secrets/sandbox-tokens.ts)
|
|
2374
|
+
// and the server's `requireCrmAccess` / `requireCliWorkspaceAccess`
|
|
2375
|
+
// accept EITHER for the full CRM + files + integration surface. But the
|
|
2376
|
+
// agent-session token EXPIRES (SANDBOX_AGENT_SESSION_TTL_MS, 7d) while
|
|
2377
|
+
// the API key is org-secret-keyed with no TTL. The old order tried the
|
|
2378
|
+
// agent token first and — critically — `getRuntime` keys the session
|
|
2379
|
+
// branch purely on the token being PRESENT, never on validity, so an
|
|
2380
|
+
// expired token in a long-lived / reused sandbox would be used blindly
|
|
2381
|
+
// and the server would reject it, even though a perfectly valid API key
|
|
2382
|
+
// sat right next to it. The server itself documents the API key as the
|
|
2383
|
+
// "preferred path for dench-cli and any automation outside the browser"
|
|
2384
|
+
// and the agent token as legacy back-compat that "new sandboxes do not
|
|
2385
|
+
// need". So prefer the key; it removes the expired-token failure mode
|
|
2386
|
+
// entirely for unattended cron/heartbeat runs.
|
|
2387
|
+
const apiKey = process.env.DENCH_API_KEY?.trim();
|
|
2388
|
+
if (apiKey && sandboxConvexUrl) {
|
|
2351
2389
|
const apiHost = resolveApiHost(host);
|
|
2352
2390
|
return {
|
|
2353
|
-
mode: "
|
|
2391
|
+
mode: "apiKey" as const,
|
|
2354
2392
|
host: apiHost,
|
|
2355
|
-
client:
|
|
2356
|
-
sessionToken:
|
|
2357
|
-
organization:
|
|
2358
|
-
orgId && orgSlug
|
|
2359
|
-
? { id: orgId, name: orgSlug, slug: orgSlug }
|
|
2360
|
-
: undefined,
|
|
2393
|
+
client: createCliConvexClient(sandboxConvexUrl),
|
|
2394
|
+
sessionToken: apiKey,
|
|
2395
|
+
organization: sandboxOrganization,
|
|
2361
2396
|
};
|
|
2362
2397
|
}
|
|
2363
2398
|
|
|
2364
|
-
//
|
|
2365
|
-
//
|
|
2366
|
-
//
|
|
2367
|
-
|
|
2368
|
-
|
|
2369
|
-
// explicit `dench signin` setups keep their existing behavior.
|
|
2370
|
-
const apiKey = process.env.DENCH_API_KEY?.trim();
|
|
2371
|
-
const convexUrl = resolveApiKeyConvexUrl();
|
|
2372
|
-
if (apiKey && convexUrl) {
|
|
2373
|
-
const orgId = process.env.DENCH_ORG_ID?.trim();
|
|
2374
|
-
const orgSlug = process.env.DENCH_ORG_SLUG?.trim();
|
|
2399
|
+
// Fallback: no API key present (older sandbox images / custom setups) —
|
|
2400
|
+
// use the agent-session token. Still subject to expiry, but it's the
|
|
2401
|
+
// only credential available in this branch.
|
|
2402
|
+
const sandboxAgentToken = process.env.DENCH_AGENT_SESSION_TOKEN?.trim();
|
|
2403
|
+
if (sandboxAgentToken && sandboxConvexUrl) {
|
|
2375
2404
|
const apiHost = resolveApiHost(host);
|
|
2376
2405
|
return {
|
|
2377
|
-
mode: "
|
|
2406
|
+
mode: "session" as const,
|
|
2378
2407
|
host: apiHost,
|
|
2379
|
-
client:
|
|
2380
|
-
sessionToken:
|
|
2381
|
-
organization:
|
|
2382
|
-
orgId && orgSlug
|
|
2383
|
-
? { id: orgId, name: orgSlug, slug: orgSlug }
|
|
2384
|
-
: undefined,
|
|
2408
|
+
client: createCliConvexClient(sandboxConvexUrl),
|
|
2409
|
+
sessionToken: sandboxAgentToken,
|
|
2410
|
+
organization: sandboxOrganization,
|
|
2385
2411
|
};
|
|
2386
2412
|
}
|
|
2387
2413
|
|
|
@@ -2394,7 +2420,7 @@ async function getRuntime() {
|
|
|
2394
2420
|
|
|
2395
2421
|
return {
|
|
2396
2422
|
mode: "dev" as const,
|
|
2397
|
-
client:
|
|
2423
|
+
client: createCliConvexClient(readEnv("NEXT_PUBLIC_CONVEX_URL")),
|
|
2398
2424
|
workspaceArgs: {
|
|
2399
2425
|
devKey: readEnv("DENCH_DEV_AGENT_KEY"),
|
|
2400
2426
|
workspaceSlug: readEnv("DENCH_WORKSPACE"),
|
|
@@ -2933,7 +2959,7 @@ async function listArtifacts(runtime: Runtime, limit?: number) {
|
|
|
2933
2959
|
}
|
|
2934
2960
|
|
|
2935
2961
|
async function listSuggestedWork(runtime: Runtime) {
|
|
2936
|
-
if (runtime.mode === "session") {
|
|
2962
|
+
if (runtime.mode === "session" || runtime.mode === "apiKey") {
|
|
2937
2963
|
const context = await buildContext(runtime);
|
|
2938
2964
|
return asRecordArray(asRecord(context)?.suggestedWork);
|
|
2939
2965
|
}
|
|
@@ -3616,6 +3642,30 @@ async function main() {
|
|
|
3616
3642
|
return;
|
|
3617
3643
|
}
|
|
3618
3644
|
|
|
3645
|
+
if (
|
|
3646
|
+
command === "browser" &&
|
|
3647
|
+
(!subcommand || subcommand === "help" || hasFlag("--help"))
|
|
3648
|
+
) {
|
|
3649
|
+
const { runBrowserCommand } = await import("./browser");
|
|
3650
|
+
await runBrowserCommand({
|
|
3651
|
+
args: subcommand ? [subcommand] : ["help"],
|
|
3652
|
+
runtime: { host: "", bearerToken: "" },
|
|
3653
|
+
});
|
|
3654
|
+
return;
|
|
3655
|
+
}
|
|
3656
|
+
|
|
3657
|
+
if (
|
|
3658
|
+
command === "linkedin" &&
|
|
3659
|
+
(!subcommand || subcommand === "help" || hasFlag("--help"))
|
|
3660
|
+
) {
|
|
3661
|
+
const { runLinkedInCommand } = await import("./linkedin");
|
|
3662
|
+
await runLinkedInCommand({
|
|
3663
|
+
args: subcommand ? [subcommand] : ["help"],
|
|
3664
|
+
runtime: { host: "", bearerToken: "" },
|
|
3665
|
+
});
|
|
3666
|
+
return;
|
|
3667
|
+
}
|
|
3668
|
+
|
|
3619
3669
|
if (
|
|
3620
3670
|
command === "billing" &&
|
|
3621
3671
|
(!subcommand || subcommand === "help" || hasFlag("--help"))
|
|
@@ -3880,6 +3930,39 @@ async function main() {
|
|
|
3880
3930
|
return;
|
|
3881
3931
|
}
|
|
3882
3932
|
|
|
3933
|
+
if (command === "browser") {
|
|
3934
|
+
const subArgs = args.slice(args.indexOf("browser") + 1);
|
|
3935
|
+
const { runBrowserCommand } = await import("./browser");
|
|
3936
|
+
const runtime = await getRuntime();
|
|
3937
|
+
const authedRuntime = requireAuthenticatedRuntime(runtime, "dench browser");
|
|
3938
|
+
await runBrowserCommand({
|
|
3939
|
+
args: subArgs,
|
|
3940
|
+
runtime: {
|
|
3941
|
+
host: authedRuntime.host,
|
|
3942
|
+
bearerToken: authedRuntime.sessionToken,
|
|
3943
|
+
},
|
|
3944
|
+
});
|
|
3945
|
+
return;
|
|
3946
|
+
}
|
|
3947
|
+
|
|
3948
|
+
if (command === "linkedin") {
|
|
3949
|
+
const subArgs = args.slice(args.indexOf("linkedin") + 1);
|
|
3950
|
+
const { runLinkedInCommand } = await import("./linkedin");
|
|
3951
|
+
const runtime = await getRuntime();
|
|
3952
|
+
const authedRuntime = requireAuthenticatedRuntime(
|
|
3953
|
+
runtime,
|
|
3954
|
+
"dench linkedin",
|
|
3955
|
+
);
|
|
3956
|
+
await runLinkedInCommand({
|
|
3957
|
+
args: subArgs,
|
|
3958
|
+
runtime: {
|
|
3959
|
+
host: authedRuntime.host,
|
|
3960
|
+
bearerToken: authedRuntime.sessionToken,
|
|
3961
|
+
},
|
|
3962
|
+
});
|
|
3963
|
+
return;
|
|
3964
|
+
}
|
|
3965
|
+
|
|
3883
3966
|
if (command === "chat") {
|
|
3884
3967
|
const subArgs = args.slice(args.indexOf("chat") + 1);
|
|
3885
3968
|
const sub = subArgs[0];
|