@dench.com/cli 2.0.0 → 2.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/crm.ts +60 -26
- package/dench.ts +224 -74
- package/image.ts +49 -40
- package/lib/enrichment-gateway.ts +2 -2
- package/package.json +1 -1
- package/search.ts +56 -48
- package/tools.ts +59 -34
package/crm.ts
CHANGED
|
@@ -26,7 +26,7 @@
|
|
|
26
26
|
* crm docs list / create / link
|
|
27
27
|
* crm actions list / run / runs
|
|
28
28
|
*/
|
|
29
|
-
import { ConvexHttpClient } from "convex/browser";
|
|
29
|
+
import type { ConvexHttpClient } from "convex/browser";
|
|
30
30
|
import { makeFunctionReference } from "convex/server";
|
|
31
31
|
import {
|
|
32
32
|
assertNoUnknownFlags as assertNoUnknownFlagsRaw,
|
|
@@ -40,18 +40,19 @@ import {
|
|
|
40
40
|
import {
|
|
41
41
|
autoDetectInputField,
|
|
42
42
|
detectEnrichmentCategory,
|
|
43
|
-
extractEnrichmentValue,
|
|
44
|
-
findEnrichmentColumn,
|
|
45
|
-
getRequiredFieldsForApolloPath,
|
|
46
43
|
type EnrichmentCategory,
|
|
47
44
|
type EnrichmentColumnDef,
|
|
45
|
+
extractEnrichmentValue,
|
|
48
46
|
type FieldCandidate,
|
|
47
|
+
findEnrichmentColumn,
|
|
48
|
+
getRequiredFieldsForApolloPath,
|
|
49
49
|
} from "./lib/crm-enrichment";
|
|
50
50
|
import {
|
|
51
51
|
aviatoEnrichCompany,
|
|
52
52
|
aviatoEnrichPerson,
|
|
53
|
-
enrichCompany,
|
|
54
53
|
EnrichmentGatewayError,
|
|
54
|
+
type EnrichmentGatewayOptions,
|
|
55
|
+
enrichCompany,
|
|
55
56
|
enrichPersonByIdentifier,
|
|
56
57
|
} from "./lib/enrichment-gateway";
|
|
57
58
|
|
|
@@ -82,6 +83,7 @@ type CrmCliContext = {
|
|
|
82
83
|
* organization context.
|
|
83
84
|
*/
|
|
84
85
|
sessionToken?: string;
|
|
86
|
+
enrichmentGateway?: Pick<EnrichmentGatewayOptions, "apiKey" | "baseUrl">;
|
|
85
87
|
};
|
|
86
88
|
|
|
87
89
|
// Adapt the shared helpers to throw CrmCliError so existing call sites
|
|
@@ -310,12 +312,14 @@ function out(ctx: CrmCliContext, value: unknown): void {
|
|
|
310
312
|
console.log(JSON.stringify(value, null, 2));
|
|
311
313
|
}
|
|
312
314
|
|
|
313
|
-
function getFieldEnrichmentFlag(args: string[]):
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
315
|
+
function getFieldEnrichmentFlag(args: string[]):
|
|
316
|
+
| {
|
|
317
|
+
category: EnrichmentCategory;
|
|
318
|
+
key: string;
|
|
319
|
+
apolloPath: string;
|
|
320
|
+
inputFieldName: string;
|
|
321
|
+
}
|
|
322
|
+
| undefined {
|
|
319
323
|
const category = parseCategoryFlag(getFlag(args, "--enrichment-category"));
|
|
320
324
|
const key = getFlag(args, "--enrichment-key");
|
|
321
325
|
const apolloPath = getFlag(args, "--apollo-path");
|
|
@@ -431,6 +435,7 @@ export async function runCrmCommand(opts: {
|
|
|
431
435
|
convex: ConvexHttpClient;
|
|
432
436
|
args: string[];
|
|
433
437
|
sessionToken?: string;
|
|
438
|
+
enrichmentGateway?: Pick<EnrichmentGatewayOptions, "apiKey" | "baseUrl">;
|
|
434
439
|
}): Promise<void> {
|
|
435
440
|
const args = [...opts.args];
|
|
436
441
|
const jsonOutput = hasFlag(args, "--json");
|
|
@@ -439,6 +444,7 @@ export async function runCrmCommand(opts: {
|
|
|
439
444
|
args,
|
|
440
445
|
jsonOutput,
|
|
441
446
|
sessionToken: opts.sessionToken,
|
|
447
|
+
enrichmentGateway: opts.enrichmentGateway,
|
|
442
448
|
};
|
|
443
449
|
const subcommand = args.shift();
|
|
444
450
|
if (!subcommand || subcommand === "help" || subcommand === "--help") {
|
|
@@ -1531,13 +1537,18 @@ type ObjectEnrichmentArgs = Omit<CellEnrichmentArgs, "entryId"> & {
|
|
|
1531
1537
|
limit?: number;
|
|
1532
1538
|
};
|
|
1533
1539
|
|
|
1534
|
-
function parseCategoryFlag(
|
|
1540
|
+
function parseCategoryFlag(
|
|
1541
|
+
value: string | undefined,
|
|
1542
|
+
): EnrichmentCategory | undefined {
|
|
1535
1543
|
if (value === undefined) return undefined;
|
|
1536
1544
|
if (value === "people" || value === "company") return value;
|
|
1537
1545
|
throw new CrmCliError("--category must be people or company");
|
|
1538
1546
|
}
|
|
1539
1547
|
|
|
1540
|
-
function parsePositiveInt(
|
|
1548
|
+
function parsePositiveInt(
|
|
1549
|
+
flag: string,
|
|
1550
|
+
value: string | undefined,
|
|
1551
|
+
): number | undefined {
|
|
1541
1552
|
if (value === undefined) return undefined;
|
|
1542
1553
|
const parsed = Number(value);
|
|
1543
1554
|
if (!Number.isInteger(parsed) || parsed < 1) {
|
|
@@ -1556,7 +1567,9 @@ function asFields(value: unknown): CrmFieldForEnrichment[] {
|
|
|
1556
1567
|
.filter((field): field is CrmFieldForEnrichment => {
|
|
1557
1568
|
if (!field || typeof field !== "object") return false;
|
|
1558
1569
|
const candidate = field as Record<string, unknown>;
|
|
1559
|
-
return
|
|
1570
|
+
return (
|
|
1571
|
+
typeof candidate.name === "string" && typeof candidate.type === "string"
|
|
1572
|
+
);
|
|
1560
1573
|
})
|
|
1561
1574
|
.map((field) => ({
|
|
1562
1575
|
id: typeof field.id === "string" ? field.id : undefined,
|
|
@@ -1601,7 +1614,9 @@ function asEntry(value: unknown): CrmEntryForEnrichment | null {
|
|
|
1601
1614
|
|
|
1602
1615
|
function asEntries(value: unknown): CrmEntryForEnrichment[] {
|
|
1603
1616
|
if (!Array.isArray(value)) return [];
|
|
1604
|
-
return value
|
|
1617
|
+
return value
|
|
1618
|
+
.map(asEntry)
|
|
1619
|
+
.filter((entry): entry is CrmEntryForEnrichment => !!entry);
|
|
1605
1620
|
}
|
|
1606
1621
|
|
|
1607
1622
|
function resolveEnrichmentColumn(args: {
|
|
@@ -1668,7 +1683,8 @@ async function resolveEnrichmentTarget(
|
|
|
1668
1683
|
apolloPath: args.apolloPath ?? outputField.enrichment?.apolloPath,
|
|
1669
1684
|
});
|
|
1670
1685
|
|
|
1671
|
-
const inputFieldName =
|
|
1686
|
+
const inputFieldName =
|
|
1687
|
+
args.inputFieldName ?? outputField.enrichment?.inputFieldName;
|
|
1672
1688
|
const inputField = inputFieldName
|
|
1673
1689
|
? fields.find((field) => field.name === inputFieldName)
|
|
1674
1690
|
: autoDetectInputField(category, fields);
|
|
@@ -1693,6 +1709,7 @@ async function callEnrichmentGatewayForEntry(args: {
|
|
|
1693
1709
|
entry: CrmEntryForEnrichment;
|
|
1694
1710
|
/** Explicit provider override. Defaults: people=fullenrich, company=aviato. */
|
|
1695
1711
|
provider?: string;
|
|
1712
|
+
gateway?: Pick<EnrichmentGatewayOptions, "apiKey" | "baseUrl">;
|
|
1696
1713
|
}): Promise<Record<string, unknown>> {
|
|
1697
1714
|
const input = args.entry.fields[args.target.inputField.name];
|
|
1698
1715
|
if (!hasCellValue(input)) {
|
|
@@ -1702,7 +1719,8 @@ async function callEnrichmentGatewayForEntry(args: {
|
|
|
1702
1719
|
}
|
|
1703
1720
|
const inputValue = String(input);
|
|
1704
1721
|
const isCompany = args.target.category === "company";
|
|
1705
|
-
const resolvedProvider =
|
|
1722
|
+
const resolvedProvider =
|
|
1723
|
+
args.provider ?? (isCompany ? "aviato" : "fullenrich");
|
|
1706
1724
|
|
|
1707
1725
|
if (resolvedProvider === "aviato") {
|
|
1708
1726
|
// Guard: Aviato does not return phone or email.
|
|
@@ -1718,25 +1736,37 @@ async function callEnrichmentGatewayForEntry(args: {
|
|
|
1718
1736
|
const isLinkedin = /linkedin\.com/i.test(inputValue);
|
|
1719
1737
|
return aviatoEnrichCompany(
|
|
1720
1738
|
isLinkedin ? { linkedinUrl: inputValue } : { website: inputValue },
|
|
1739
|
+
args.gateway,
|
|
1721
1740
|
);
|
|
1722
1741
|
} else {
|
|
1723
1742
|
// For people, inputValue may be a LinkedIn URL, email, or LinkedIn ID
|
|
1724
1743
|
const isLinkedin = /linkedin\.com/i.test(inputValue);
|
|
1725
1744
|
const isEmail = /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(inputValue);
|
|
1726
|
-
if (isLinkedin)
|
|
1727
|
-
|
|
1728
|
-
|
|
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);
|
|
1729
1752
|
}
|
|
1730
1753
|
}
|
|
1731
1754
|
|
|
1732
1755
|
// fullenrich path (default for people, explicit override for companies)
|
|
1733
1756
|
if (isCompany) {
|
|
1734
|
-
return enrichCompany(
|
|
1735
|
-
|
|
1736
|
-
|
|
1737
|
-
|
|
1757
|
+
return enrichCompany(
|
|
1758
|
+
{
|
|
1759
|
+
domain: inputValue,
|
|
1760
|
+
requiredFields: args.target.requiredFields,
|
|
1761
|
+
},
|
|
1762
|
+
args.gateway,
|
|
1763
|
+
);
|
|
1738
1764
|
}
|
|
1739
|
-
return enrichPersonByIdentifier(
|
|
1765
|
+
return enrichPersonByIdentifier(
|
|
1766
|
+
inputValue,
|
|
1767
|
+
args.target.requiredFields,
|
|
1768
|
+
args.gateway,
|
|
1769
|
+
);
|
|
1740
1770
|
}
|
|
1741
1771
|
|
|
1742
1772
|
async function enrichOneEntry(
|
|
@@ -1751,6 +1781,7 @@ async function enrichOneEntry(
|
|
|
1751
1781
|
target: args.target,
|
|
1752
1782
|
entry: args.entry,
|
|
1753
1783
|
provider: args.provider,
|
|
1784
|
+
gateway: ctx.enrichmentGateway,
|
|
1754
1785
|
});
|
|
1755
1786
|
const value = extractEnrichmentValue(payload, args.target.column);
|
|
1756
1787
|
if (value === null) {
|
|
@@ -1779,7 +1810,10 @@ async function enrichOneEntry(
|
|
|
1779
1810
|
...writeResult,
|
|
1780
1811
|
};
|
|
1781
1812
|
} catch (error) {
|
|
1782
|
-
if (
|
|
1813
|
+
if (
|
|
1814
|
+
error instanceof EnrichmentGatewayError ||
|
|
1815
|
+
error instanceof CrmCliError
|
|
1816
|
+
) {
|
|
1783
1817
|
return {
|
|
1784
1818
|
entryId: args.entry.id,
|
|
1785
1819
|
ok: false,
|
package/dench.ts
CHANGED
|
@@ -966,11 +966,12 @@ function backendSnapshot(config: ConfigFile) {
|
|
|
966
966
|
: null;
|
|
967
967
|
const envHost = process.env.DENCH_HOST?.trim() || null;
|
|
968
968
|
const argBackend =
|
|
969
|
-
option("--backend") ??
|
|
969
|
+
option("--backend") ??
|
|
970
|
+
(hasFlag("--prod")
|
|
970
971
|
? "production"
|
|
971
972
|
: hasFlag("--staging")
|
|
972
973
|
? "staging"
|
|
973
|
-
: option("--host") ?? null);
|
|
974
|
+
: (option("--host") ?? null));
|
|
974
975
|
|
|
975
976
|
let source: "flag" | "env" | "config" | "default";
|
|
976
977
|
if (argBackend) source = "flag";
|
|
@@ -1078,7 +1079,9 @@ async function backendSet() {
|
|
|
1078
1079
|
"Local backend selected. dench will now also load .env / .env.local from the workspace so GATEWAY_URL etc. are picked up.",
|
|
1079
1080
|
);
|
|
1080
1081
|
}
|
|
1081
|
-
print(
|
|
1082
|
+
print(
|
|
1083
|
+
"Future `dench` commands without --prod/--staging/--host will target this backend.",
|
|
1084
|
+
);
|
|
1082
1085
|
}
|
|
1083
1086
|
|
|
1084
1087
|
async function backendClear() {
|
|
@@ -1706,8 +1709,7 @@ async function signinViaBrowser(input: {
|
|
|
1706
1709
|
agentName: input.agentName,
|
|
1707
1710
|
agentKind: input.agentKind,
|
|
1708
1711
|
intent: input.intent,
|
|
1709
|
-
proposedOrganizationName:
|
|
1710
|
-
input.proposedOrganizationName ?? undefined,
|
|
1712
|
+
proposedOrganizationName: input.proposedOrganizationName ?? undefined,
|
|
1711
1713
|
requestedOrganizationSelector: input.orgSelector ?? undefined,
|
|
1712
1714
|
userAgent: "@dench.com/cli",
|
|
1713
1715
|
},
|
|
@@ -1841,7 +1843,8 @@ async function signinViaOtp(input: {
|
|
|
1841
1843
|
// resolvable (single admin org or create_workspace intent) and the
|
|
1842
1844
|
// server hands us a session, OR we get an auth token + list and
|
|
1843
1845
|
// need a third call.
|
|
1844
|
-
const code =
|
|
1846
|
+
const code =
|
|
1847
|
+
option("--otp-code")?.trim() || (await promptLine("Paste OTP: "));
|
|
1845
1848
|
if (!/^\d{6}$/.test(code)) {
|
|
1846
1849
|
throw new CliError("OTP code must be exactly 6 digits.", {
|
|
1847
1850
|
code: "invalid_otp_code",
|
|
@@ -1857,8 +1860,7 @@ async function signinViaOtp(input: {
|
|
|
1857
1860
|
agentKind: input.agentKind,
|
|
1858
1861
|
sessionTokenHash: input.sessionTokenHash,
|
|
1859
1862
|
intent: input.intent,
|
|
1860
|
-
proposedOrganizationName:
|
|
1861
|
-
input.proposedOrganizationName ?? undefined,
|
|
1863
|
+
proposedOrganizationName: input.proposedOrganizationName ?? undefined,
|
|
1862
1864
|
organizationSelector: input.orgSelector ?? undefined,
|
|
1863
1865
|
userAgent: "@dench.com/cli",
|
|
1864
1866
|
}),
|
|
@@ -1878,8 +1880,15 @@ async function signinViaOtp(input: {
|
|
|
1878
1880
|
|
|
1879
1881
|
// Branch A: server minted the session in one shot.
|
|
1880
1882
|
if (result.kind === "completed") {
|
|
1881
|
-
assertOtpCompletedOrgMatchesSelector(
|
|
1882
|
-
|
|
1883
|
+
assertOtpCompletedOrgMatchesSelector(
|
|
1884
|
+
result.organization,
|
|
1885
|
+
input.orgSelector,
|
|
1886
|
+
);
|
|
1887
|
+
await finalizeOtpSession(
|
|
1888
|
+
input,
|
|
1889
|
+
result.organization,
|
|
1890
|
+
result.sessionExpiresAt,
|
|
1891
|
+
);
|
|
1883
1892
|
return;
|
|
1884
1893
|
}
|
|
1885
1894
|
|
|
@@ -2020,7 +2029,11 @@ async function resolveOrgChoiceForOtp(input: {
|
|
|
2020
2029
|
|
|
2021
2030
|
const answer = await promptLine("Pick a number: ");
|
|
2022
2031
|
const choice = Number(answer);
|
|
2023
|
-
if (
|
|
2032
|
+
if (
|
|
2033
|
+
!Number.isFinite(choice) ||
|
|
2034
|
+
choice < 1 ||
|
|
2035
|
+
choice > input.organizations.length + 1
|
|
2036
|
+
) {
|
|
2024
2037
|
throw new CliError(`Invalid choice: ${answer}`, {
|
|
2025
2038
|
code: "invalid_org_choice",
|
|
2026
2039
|
});
|
|
@@ -2234,9 +2247,7 @@ async function denchUpgrade(runtime: Runtime) {
|
|
|
2234
2247
|
if (updatedInPlace) {
|
|
2235
2248
|
const message = `Upgraded to Dench ${tierFlag === "pro" ? "Pro" : "Max"} in place — no Stripe Checkout needed.`;
|
|
2236
2249
|
print(
|
|
2237
|
-
json
|
|
2238
|
-
? { ok: true, updated: true, tier: tierFlag, message }
|
|
2239
|
-
: message,
|
|
2250
|
+
json ? { ok: true, updated: true, tier: tierFlag, message } : message,
|
|
2240
2251
|
);
|
|
2241
2252
|
return;
|
|
2242
2253
|
}
|
|
@@ -2487,6 +2498,103 @@ function requireAuthenticatedRuntime(
|
|
|
2487
2498
|
throw loginRequiredError(command);
|
|
2488
2499
|
}
|
|
2489
2500
|
|
|
2501
|
+
type GatewayCommandAuth = {
|
|
2502
|
+
bearerToken: string;
|
|
2503
|
+
gatewayBaseUrl?: string;
|
|
2504
|
+
};
|
|
2505
|
+
|
|
2506
|
+
function gatewayBaseUrlForCli(value: unknown): string | undefined {
|
|
2507
|
+
if (typeof value !== "string") return undefined;
|
|
2508
|
+
const trimmed = value.trim().replace(/\/+$/, "");
|
|
2509
|
+
if (!trimmed) return undefined;
|
|
2510
|
+
// The desktop endpoint returns a `/v1` base; CLI gateway modules append
|
|
2511
|
+
// `/v1/...` paths themselves, so normalize to the root origin here.
|
|
2512
|
+
return trimmed.replace(/\/v1$/, "");
|
|
2513
|
+
}
|
|
2514
|
+
|
|
2515
|
+
async function parseJsonResponseBody(response: Response): Promise<JsonRecord> {
|
|
2516
|
+
const text = await response.text().catch(() => "");
|
|
2517
|
+
if (!text.trim()) return {};
|
|
2518
|
+
try {
|
|
2519
|
+
const parsed = JSON.parse(text);
|
|
2520
|
+
return parsed && typeof parsed === "object" && !Array.isArray(parsed)
|
|
2521
|
+
? (parsed as JsonRecord)
|
|
2522
|
+
: {};
|
|
2523
|
+
} catch {
|
|
2524
|
+
return { error: text };
|
|
2525
|
+
}
|
|
2526
|
+
}
|
|
2527
|
+
|
|
2528
|
+
function gatewayAuthError(
|
|
2529
|
+
command: string,
|
|
2530
|
+
response: Response,
|
|
2531
|
+
body: JsonRecord,
|
|
2532
|
+
) {
|
|
2533
|
+
const code =
|
|
2534
|
+
typeof body.error === "string" && body.error.trim()
|
|
2535
|
+
? body.error.trim()
|
|
2536
|
+
: `gateway_key_${response.status}`;
|
|
2537
|
+
if (response.status === 403 && code === "no_active_subscription") {
|
|
2538
|
+
return new CliError(`${command} requires a paid Dench workspace`, {
|
|
2539
|
+
code: "paid_workspace_required",
|
|
2540
|
+
nextActions: [
|
|
2541
|
+
"Run dench upgrade --tier pro or dench upgrade --tier max, then retry.",
|
|
2542
|
+
],
|
|
2543
|
+
});
|
|
2544
|
+
}
|
|
2545
|
+
if (response.status === 401) {
|
|
2546
|
+
return loginRequiredError(command);
|
|
2547
|
+
}
|
|
2548
|
+
return new CliError(`${command} could not get a gateway key (${code})`, {
|
|
2549
|
+
code,
|
|
2550
|
+
status: response.status,
|
|
2551
|
+
nextActions: [
|
|
2552
|
+
"Confirm this agent is signed in to the intended workspace.",
|
|
2553
|
+
"If the workspace is paid, retry after a minute in case gateway key provisioning is still finishing.",
|
|
2554
|
+
],
|
|
2555
|
+
});
|
|
2556
|
+
}
|
|
2557
|
+
|
|
2558
|
+
async function resolveGatewayCommandAuth(
|
|
2559
|
+
runtime: Runtime,
|
|
2560
|
+
command: string,
|
|
2561
|
+
): Promise<GatewayCommandAuth> {
|
|
2562
|
+
if (runtime.mode === "apiKey") {
|
|
2563
|
+
return { bearerToken: runtime.sessionToken };
|
|
2564
|
+
}
|
|
2565
|
+
|
|
2566
|
+
if (runtime.mode === "session") {
|
|
2567
|
+
const response = await fetch(
|
|
2568
|
+
`${runtime.host.replace(/\/+$/, "")}/api/desktop/organizations/gateway-key`,
|
|
2569
|
+
{
|
|
2570
|
+
headers: {
|
|
2571
|
+
accept: "application/json",
|
|
2572
|
+
authorization: `Bearer ${runtime.sessionToken}`,
|
|
2573
|
+
},
|
|
2574
|
+
},
|
|
2575
|
+
);
|
|
2576
|
+
const body = await parseJsonResponseBody(response);
|
|
2577
|
+
if (!response.ok) {
|
|
2578
|
+
throw gatewayAuthError(command, response, body);
|
|
2579
|
+
}
|
|
2580
|
+
const gatewayKey =
|
|
2581
|
+
typeof body.gatewayKey === "string" ? body.gatewayKey.trim() : "";
|
|
2582
|
+
if (!gatewayKey) {
|
|
2583
|
+
throw new CliError(`${command} could not get a gateway key`, {
|
|
2584
|
+
code: "missing_gateway_key",
|
|
2585
|
+
});
|
|
2586
|
+
}
|
|
2587
|
+
return {
|
|
2588
|
+
bearerToken: gatewayKey,
|
|
2589
|
+
gatewayBaseUrl: gatewayBaseUrlForCli(body.gatewayBaseUrl),
|
|
2590
|
+
};
|
|
2591
|
+
}
|
|
2592
|
+
|
|
2593
|
+
const envApiKey = process.env.DENCH_API_KEY?.trim();
|
|
2594
|
+
if (envApiKey) return { bearerToken: envApiKey };
|
|
2595
|
+
throw loginRequiredError(command);
|
|
2596
|
+
}
|
|
2597
|
+
|
|
2490
2598
|
function encodeDevCrmSessionToken(args: {
|
|
2491
2599
|
workspaceSlug: string;
|
|
2492
2600
|
devKey: string;
|
|
@@ -2609,12 +2717,24 @@ function nextContextCommands() {
|
|
|
2609
2717
|
];
|
|
2610
2718
|
}
|
|
2611
2719
|
|
|
2612
|
-
async function connectedAppsContext(
|
|
2613
|
-
|
|
2614
|
-
|
|
2615
|
-
|
|
2616
|
-
|
|
2617
|
-
|
|
2720
|
+
async function connectedAppsContext(runtime: Runtime) {
|
|
2721
|
+
try {
|
|
2722
|
+
const gatewayAuth = await resolveGatewayCommandAuth(
|
|
2723
|
+
runtime,
|
|
2724
|
+
"dench context",
|
|
2725
|
+
);
|
|
2726
|
+
return fetchConnectedAppsSummary({
|
|
2727
|
+
bearerToken: gatewayAuth.bearerToken,
|
|
2728
|
+
gatewayBaseUrl: gatewayAuth.gatewayBaseUrl,
|
|
2729
|
+
});
|
|
2730
|
+
} catch (error) {
|
|
2731
|
+
return {
|
|
2732
|
+
available: false,
|
|
2733
|
+
reason: errorMessage(error),
|
|
2734
|
+
count: 0,
|
|
2735
|
+
connections: [],
|
|
2736
|
+
};
|
|
2737
|
+
}
|
|
2618
2738
|
}
|
|
2619
2739
|
|
|
2620
2740
|
async function buildContext(runtime: Runtime) {
|
|
@@ -3024,26 +3144,12 @@ function normalizeFilesPath(input: string): string {
|
|
|
3024
3144
|
return cleaned;
|
|
3025
3145
|
}
|
|
3026
3146
|
|
|
3027
|
-
function
|
|
3028
|
-
|
|
3029
|
-
|
|
3030
|
-
|
|
3031
|
-
|
|
3032
|
-
|
|
3033
|
-
const apiKey = filesApiKey(runtime);
|
|
3034
|
-
if (!apiKey) {
|
|
3035
|
-
throw new CliError(
|
|
3036
|
-
`${command} needs DENCH_API_KEY (or a sandbox-baked api-key session). It bypasses the user-session auth path.`,
|
|
3037
|
-
{
|
|
3038
|
-
code: "files_api_key_required",
|
|
3039
|
-
nextActions: [
|
|
3040
|
-
"Run inside a Dench sandbox where DENCH_API_KEY is baked in.",
|
|
3041
|
-
"Or export DENCH_API_KEY=<key> first (see dench signin output).",
|
|
3042
|
-
],
|
|
3043
|
-
},
|
|
3044
|
-
);
|
|
3045
|
-
}
|
|
3046
|
-
return apiKey;
|
|
3147
|
+
async function requireFilesApiKey(
|
|
3148
|
+
runtime: Runtime,
|
|
3149
|
+
command: string,
|
|
3150
|
+
): Promise<string> {
|
|
3151
|
+
const auth = await resolveGatewayCommandAuth(runtime, command);
|
|
3152
|
+
return auth.bearerToken;
|
|
3047
3153
|
}
|
|
3048
3154
|
|
|
3049
3155
|
type FileTreeRow = {
|
|
@@ -3143,7 +3249,7 @@ async function uploadConvexFileBytes(
|
|
|
3143
3249
|
|
|
3144
3250
|
async function runFilesLs() {
|
|
3145
3251
|
const runtime = await getRuntime();
|
|
3146
|
-
const apiKey = requireFilesApiKey(runtime, "dench files ls");
|
|
3252
|
+
const apiKey = await requireFilesApiKey(runtime, "dench files ls");
|
|
3147
3253
|
const target = normalizeFilesPath(positional(2) ?? "/");
|
|
3148
3254
|
const recursive = hasFlag("--recursive") || hasFlag("-r");
|
|
3149
3255
|
const rows = recursive
|
|
@@ -3180,7 +3286,7 @@ async function runFilesLs() {
|
|
|
3180
3286
|
|
|
3181
3287
|
async function runFilesMv() {
|
|
3182
3288
|
const runtime = await getRuntime();
|
|
3183
|
-
const apiKey = requireFilesApiKey(runtime, "dench files mv");
|
|
3289
|
+
const apiKey = await requireFilesApiKey(runtime, "dench files mv");
|
|
3184
3290
|
const fromRaw = positional(2);
|
|
3185
3291
|
const toRaw = positional(3);
|
|
3186
3292
|
if (!fromRaw || !toRaw) {
|
|
@@ -3252,7 +3358,7 @@ async function runFilesMv() {
|
|
|
3252
3358
|
|
|
3253
3359
|
async function runFilesRm() {
|
|
3254
3360
|
const runtime = await getRuntime();
|
|
3255
|
-
const apiKey = requireFilesApiKey(runtime, "dench files rm");
|
|
3361
|
+
const apiKey = await requireFilesApiKey(runtime, "dench files rm");
|
|
3256
3362
|
const targetRaw = positional(2);
|
|
3257
3363
|
if (!targetRaw) {
|
|
3258
3364
|
throw new CliError("Usage: dench files rm <path> [--recursive]", {
|
|
@@ -3439,7 +3545,7 @@ async function* walkLocalFiles(
|
|
|
3439
3545
|
|
|
3440
3546
|
async function runStage() {
|
|
3441
3547
|
const runtime = await getRuntime();
|
|
3442
|
-
const apiKey = requireFilesApiKey(runtime, "dench stage");
|
|
3548
|
+
const apiKey = await requireFilesApiKey(runtime, "dench stage");
|
|
3443
3549
|
const localRaw = positional(1);
|
|
3444
3550
|
const destRaw = positional(2);
|
|
3445
3551
|
if (!localRaw) {
|
|
@@ -3694,15 +3800,13 @@ async function main() {
|
|
|
3694
3800
|
// clean redirect to the new unified surface so old `npx` caches and
|
|
3695
3801
|
// pre-existing agent prompts get a one-line nudge instead of an
|
|
3696
3802
|
// opaque "Unknown command" error.
|
|
3697
|
-
if (
|
|
3698
|
-
isDeprecatedAuthCommand
|
|
3699
|
-
) {
|
|
3803
|
+
if (isDeprecatedAuthCommand) {
|
|
3700
3804
|
throw new CliError(
|
|
3701
3805
|
`\`dench ${command}\` was removed in @dench.com/cli v2. Run \`dench signin\` instead.`,
|
|
3702
3806
|
{
|
|
3703
3807
|
code: "deprecated_command",
|
|
3704
3808
|
nextActions: [
|
|
3705
|
-
|
|
3809
|
+
"Run `dench signin` to connect this agent to an existing workspace.",
|
|
3706
3810
|
'Run `dench signin --new-workspace --org-name "<name>"` to create a free CRM-only workspace.',
|
|
3707
3811
|
"Upgrade the CLI globally with `npm install -g @dench.com/cli@latest`.",
|
|
3708
3812
|
],
|
|
@@ -3746,10 +3850,7 @@ async function main() {
|
|
|
3746
3850
|
return;
|
|
3747
3851
|
}
|
|
3748
3852
|
|
|
3749
|
-
if (
|
|
3750
|
-
command === "backend" &&
|
|
3751
|
-
(subcommand === "help" || hasFlag("--help"))
|
|
3752
|
-
) {
|
|
3853
|
+
if (command === "backend" && (subcommand === "help" || hasFlag("--help"))) {
|
|
3753
3854
|
backendHelp();
|
|
3754
3855
|
return;
|
|
3755
3856
|
}
|
|
@@ -3776,18 +3877,38 @@ async function main() {
|
|
|
3776
3877
|
// `requireCrmAccess` (convex/lib/crm/access.ts) dispatches on the
|
|
3777
3878
|
// bearer prefix and resolves the org for both.
|
|
3778
3879
|
if (runtime.mode === "dev") {
|
|
3880
|
+
const gatewayAuth =
|
|
3881
|
+
subArgs[0] === "enrich"
|
|
3882
|
+
? await resolveGatewayCommandAuth(runtime, "dench crm enrich")
|
|
3883
|
+
: undefined;
|
|
3779
3884
|
await runCrmCommand({
|
|
3780
3885
|
convex: runtime.client,
|
|
3781
3886
|
args: subArgs,
|
|
3782
3887
|
sessionToken: encodeDevCrmSessionToken(runtime.workspaceArgs),
|
|
3888
|
+
enrichmentGateway: gatewayAuth
|
|
3889
|
+
? {
|
|
3890
|
+
apiKey: gatewayAuth.bearerToken,
|
|
3891
|
+
baseUrl: gatewayAuth.gatewayBaseUrl,
|
|
3892
|
+
}
|
|
3893
|
+
: undefined,
|
|
3783
3894
|
});
|
|
3784
3895
|
return;
|
|
3785
3896
|
}
|
|
3786
3897
|
const authedRuntime = requireAuthenticatedRuntime(runtime, "dench crm");
|
|
3898
|
+
const gatewayAuth =
|
|
3899
|
+
subArgs[0] === "enrich"
|
|
3900
|
+
? await resolveGatewayCommandAuth(authedRuntime, "dench crm enrich")
|
|
3901
|
+
: undefined;
|
|
3787
3902
|
await runCrmCommand({
|
|
3788
3903
|
convex: authedRuntime.client,
|
|
3789
3904
|
args: subArgs,
|
|
3790
3905
|
sessionToken: authedRuntime.sessionToken,
|
|
3906
|
+
enrichmentGateway: gatewayAuth
|
|
3907
|
+
? {
|
|
3908
|
+
apiKey: gatewayAuth.bearerToken,
|
|
3909
|
+
baseUrl: gatewayAuth.gatewayBaseUrl,
|
|
3910
|
+
}
|
|
3911
|
+
: undefined,
|
|
3791
3912
|
});
|
|
3792
3913
|
return;
|
|
3793
3914
|
}
|
|
@@ -3820,10 +3941,7 @@ async function main() {
|
|
|
3820
3941
|
});
|
|
3821
3942
|
return;
|
|
3822
3943
|
}
|
|
3823
|
-
const authedRuntime = requireAuthenticatedRuntime(
|
|
3824
|
-
runtime,
|
|
3825
|
-
"dench members",
|
|
3826
|
-
);
|
|
3944
|
+
const authedRuntime = requireAuthenticatedRuntime(runtime, "dench members");
|
|
3827
3945
|
await runMembersCommand({
|
|
3828
3946
|
convex: authedRuntime.client,
|
|
3829
3947
|
args: subArgs,
|
|
@@ -3848,44 +3966,76 @@ async function main() {
|
|
|
3848
3966
|
}
|
|
3849
3967
|
|
|
3850
3968
|
if (command === "search") {
|
|
3851
|
-
// `dench search` talks straight to the gateway over HTTP using
|
|
3852
|
-
// DENCH_API_KEY; it deliberately bypasses requireSessionRuntime so
|
|
3853
|
-
// it works in any sandbox / CI environment that has the key.
|
|
3854
3969
|
const subArgs = args.slice(args.indexOf("search") + 1);
|
|
3855
3970
|
const filteredSubArgs = subArgs.filter((a) => a !== "--json");
|
|
3856
3971
|
const { runSearchCommand } = await import("./search");
|
|
3857
|
-
|
|
3972
|
+
const sub = filteredSubArgs[0];
|
|
3973
|
+
if (!sub || sub === "help" || sub === "--help" || sub === "-h") {
|
|
3974
|
+
await runSearchCommand({ args: filteredSubArgs, jsonOutput: json });
|
|
3975
|
+
return;
|
|
3976
|
+
}
|
|
3977
|
+
const runtime = await getRuntime();
|
|
3978
|
+
const gatewayAuth = await resolveGatewayCommandAuth(
|
|
3979
|
+
runtime,
|
|
3980
|
+
"dench search",
|
|
3981
|
+
);
|
|
3982
|
+
await runSearchCommand({
|
|
3983
|
+
args: filteredSubArgs,
|
|
3984
|
+
jsonOutput: json,
|
|
3985
|
+
bearerToken: gatewayAuth.bearerToken,
|
|
3986
|
+
gatewayBaseUrl: gatewayAuth.gatewayBaseUrl,
|
|
3987
|
+
});
|
|
3858
3988
|
return;
|
|
3859
3989
|
}
|
|
3860
3990
|
|
|
3861
3991
|
if (command === "image") {
|
|
3862
|
-
// `dench image generate|edit` mirrors `dench search`: straight HTTP
|
|
3863
|
-
// to the gateway with DENCH_API_KEY, no Convex session required.
|
|
3864
|
-
// The gateway writes the bytes to Convex Storage on success; we
|
|
3865
|
-
// also drop them on local disk so the next bash command sees them.
|
|
3866
3992
|
const subArgs = args.slice(args.indexOf("image") + 1);
|
|
3867
3993
|
const filteredSubArgs = subArgs.filter((a) => a !== "--json");
|
|
3868
3994
|
const { runImageCommand } = await import("./image");
|
|
3869
|
-
|
|
3995
|
+
const sub = filteredSubArgs[0];
|
|
3996
|
+
if (!sub || sub === "help" || sub === "--help" || sub === "-h") {
|
|
3997
|
+
await runImageCommand({ args: filteredSubArgs, jsonOutput: json });
|
|
3998
|
+
return;
|
|
3999
|
+
}
|
|
4000
|
+
const runtime = await getRuntime();
|
|
4001
|
+
const gatewayAuth = await resolveGatewayCommandAuth(runtime, "dench image");
|
|
4002
|
+
await runImageCommand({
|
|
4003
|
+
args: filteredSubArgs,
|
|
4004
|
+
jsonOutput: json,
|
|
4005
|
+
bearerToken: gatewayAuth.bearerToken,
|
|
4006
|
+
gatewayBaseUrl: gatewayAuth.gatewayBaseUrl,
|
|
4007
|
+
});
|
|
3870
4008
|
return;
|
|
3871
4009
|
}
|
|
3872
4010
|
|
|
3873
4011
|
if (command === "apps") {
|
|
3874
|
-
|
|
3875
|
-
|
|
3876
|
-
|
|
3877
|
-
|
|
4012
|
+
const runtime = await getRuntime();
|
|
4013
|
+
const gatewayAuth = await resolveGatewayCommandAuth(runtime, "dench apps");
|
|
4014
|
+
await runToolCommand({
|
|
4015
|
+
args: ["status"],
|
|
4016
|
+
jsonOutput: json,
|
|
4017
|
+
bearerToken: gatewayAuth.bearerToken,
|
|
4018
|
+
gatewayBaseUrl: gatewayAuth.gatewayBaseUrl,
|
|
4019
|
+
});
|
|
3878
4020
|
return;
|
|
3879
4021
|
}
|
|
3880
4022
|
|
|
3881
4023
|
if (command === "tool") {
|
|
3882
|
-
// `dench tool {status,connect,search,run,disconnect}` is the model-facing
|
|
3883
|
-
// composio surface and only needs DENCH_API_KEY (Bearer) on the
|
|
3884
|
-
// gateway. Bypasses requireSessionRuntime entirely so it works in
|
|
3885
|
-
// every sandbox / API-key context.
|
|
3886
4024
|
const subArgs = stripRuntimeFlags(args.slice(args.indexOf("tool") + 1));
|
|
3887
4025
|
const filteredSubArgs = subArgs.filter((a) => a !== "--json");
|
|
3888
|
-
|
|
4026
|
+
const sub = filteredSubArgs[0];
|
|
4027
|
+
if (!sub || sub === "help" || sub === "--help" || sub === "-h") {
|
|
4028
|
+
await runToolCommand({ args: filteredSubArgs, jsonOutput: json });
|
|
4029
|
+
return;
|
|
4030
|
+
}
|
|
4031
|
+
const runtime = await getRuntime();
|
|
4032
|
+
const gatewayAuth = await resolveGatewayCommandAuth(runtime, "dench tool");
|
|
4033
|
+
await runToolCommand({
|
|
4034
|
+
args: filteredSubArgs,
|
|
4035
|
+
jsonOutput: json,
|
|
4036
|
+
bearerToken: gatewayAuth.bearerToken,
|
|
4037
|
+
gatewayBaseUrl: gatewayAuth.gatewayBaseUrl,
|
|
4038
|
+
});
|
|
3889
4039
|
return;
|
|
3890
4040
|
}
|
|
3891
4041
|
|
package/image.ts
CHANGED
|
@@ -3,9 +3,9 @@
|
|
|
3
3
|
* the Dench Cloud Gateway.
|
|
4
4
|
*
|
|
5
5
|
* Mirrors `dench search`: bypasses Convex entirely and talks straight HTTP
|
|
6
|
-
* to the gateway, authenticating with
|
|
7
|
-
*
|
|
8
|
-
*
|
|
6
|
+
* to the gateway, authenticating with either `DENCH_API_KEY` or a
|
|
7
|
+
* paid-workspace gateway key resolved from the active agent session by the
|
|
8
|
+
* top-level dispatcher.
|
|
9
9
|
*
|
|
10
10
|
* Persistence model: the gateway already writes the bytes to Convex Storage
|
|
11
11
|
* + the org's fileTree on success (so the workspace UI sees the file
|
|
@@ -27,38 +27,33 @@
|
|
|
27
27
|
|
|
28
28
|
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
|
29
29
|
import { dirname, isAbsolute, resolve } from "node:path";
|
|
30
|
-
import {
|
|
31
|
-
CliArgError,
|
|
32
|
-
getFlag,
|
|
33
|
-
hasFlag,
|
|
34
|
-
shift as shiftRaw,
|
|
35
|
-
} from "./lib/cli-args";
|
|
30
|
+
import { getFlag, hasFlag } from "./lib/cli-args";
|
|
36
31
|
|
|
37
32
|
type ImageCliContext = {
|
|
38
33
|
args: string[];
|
|
39
34
|
jsonOutput: boolean;
|
|
35
|
+
bearerToken?: string;
|
|
36
|
+
gatewayBaseUrl?: string;
|
|
40
37
|
};
|
|
41
38
|
|
|
42
39
|
class ImageCliError extends Error {}
|
|
43
40
|
|
|
44
|
-
function shift(args: string[], expected: string): string {
|
|
45
|
-
try {
|
|
46
|
-
return shiftRaw(args, expected);
|
|
47
|
-
} catch (error) {
|
|
48
|
-
if (error instanceof CliArgError) {
|
|
49
|
-
throw new ImageCliError(error.message);
|
|
50
|
-
}
|
|
51
|
-
throw error;
|
|
52
|
-
}
|
|
53
|
-
}
|
|
54
|
-
|
|
55
41
|
const ALLOWED_QUALITIES = new Set(["auto", "low", "medium", "high"]);
|
|
56
42
|
const ALLOWED_FORMATS = new Set(["png", "jpeg", "webp"]);
|
|
57
43
|
|
|
58
|
-
function
|
|
44
|
+
function normalizeGatewayBaseUrl(value: string): string {
|
|
45
|
+
return value.replace(/\/+$/, "").replace(/\/v1$/, "");
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function resolveGatewayBaseUrl(
|
|
49
|
+
ctx?: Pick<ImageCliContext, "gatewayBaseUrl">,
|
|
50
|
+
): string {
|
|
51
|
+
if (ctx?.gatewayBaseUrl?.trim()) {
|
|
52
|
+
return normalizeGatewayBaseUrl(ctx.gatewayBaseUrl.trim());
|
|
53
|
+
}
|
|
59
54
|
const explicit =
|
|
60
55
|
process.env.DENCH_GATEWAY_URL?.trim() || process.env.GATEWAY_URL?.trim();
|
|
61
|
-
if (explicit) return explicit
|
|
56
|
+
if (explicit) return normalizeGatewayBaseUrl(explicit);
|
|
62
57
|
const host = process.env.DENCH_HOST?.trim();
|
|
63
58
|
if (host && /localhost|127\.0\.0\.1/.test(host)) {
|
|
64
59
|
return "http://localhost:8787";
|
|
@@ -66,16 +61,17 @@ function resolveGatewayBaseUrl(): string {
|
|
|
66
61
|
return "https://gateway.merseoriginals.com";
|
|
67
62
|
}
|
|
68
63
|
|
|
69
|
-
function
|
|
70
|
-
|
|
71
|
-
|
|
64
|
+
function requireBearerToken(
|
|
65
|
+
ctx?: Pick<ImageCliContext, "bearerToken">,
|
|
66
|
+
): string {
|
|
67
|
+
const bearerToken =
|
|
68
|
+
ctx?.bearerToken?.trim() || process.env.DENCH_API_KEY?.trim();
|
|
69
|
+
if (!bearerToken) {
|
|
72
70
|
throw new ImageCliError(
|
|
73
|
-
"
|
|
74
|
-
"the env automatically; outside, export it manually or run " +
|
|
75
|
-
"`dench login` first.",
|
|
71
|
+
"No Dench gateway credential found. Set DENCH_API_KEY or run `dench signin` for a paid workspace.",
|
|
76
72
|
);
|
|
77
73
|
}
|
|
78
|
-
return
|
|
74
|
+
return bearerToken;
|
|
79
75
|
}
|
|
80
76
|
|
|
81
77
|
function resolveDefaultVolumeRoot(): string {
|
|
@@ -125,16 +121,17 @@ type GatewayImageResponse = {
|
|
|
125
121
|
};
|
|
126
122
|
|
|
127
123
|
async function callGateway(
|
|
124
|
+
ctx: ImageCliContext,
|
|
128
125
|
path: "/v1/images/generations" | "/v1/images/edits",
|
|
129
126
|
body: Record<string, unknown>,
|
|
130
127
|
): Promise<GatewayImageResponse> {
|
|
131
|
-
const
|
|
132
|
-
const baseUrl = resolveGatewayBaseUrl();
|
|
128
|
+
const bearerToken = requireBearerToken(ctx);
|
|
129
|
+
const baseUrl = resolveGatewayBaseUrl(ctx);
|
|
133
130
|
const response = await fetch(`${baseUrl}${path}`, {
|
|
134
131
|
method: "POST",
|
|
135
132
|
headers: {
|
|
136
133
|
"content-type": "application/json",
|
|
137
|
-
Authorization: `Bearer ${
|
|
134
|
+
Authorization: `Bearer ${bearerToken}`,
|
|
138
135
|
},
|
|
139
136
|
body: JSON.stringify(body),
|
|
140
137
|
});
|
|
@@ -246,7 +243,16 @@ function parseCommonOpts(args: string[]): CommonImageOpts {
|
|
|
246
243
|
`Invalid --moderation: ${moderation}. Allowed: auto, low`,
|
|
247
244
|
);
|
|
248
245
|
}
|
|
249
|
-
return {
|
|
246
|
+
return {
|
|
247
|
+
size,
|
|
248
|
+
quality,
|
|
249
|
+
format,
|
|
250
|
+
savePath,
|
|
251
|
+
output,
|
|
252
|
+
noWrite,
|
|
253
|
+
background,
|
|
254
|
+
moderation,
|
|
255
|
+
};
|
|
250
256
|
}
|
|
251
257
|
|
|
252
258
|
function buildGenerationBody(
|
|
@@ -283,7 +289,7 @@ async function runGenerateSubcommand(ctx: ImageCliContext): Promise<void> {
|
|
|
283
289
|
);
|
|
284
290
|
}
|
|
285
291
|
const body = buildGenerationBody(prompt, opts);
|
|
286
|
-
const result = await callGateway("/v1/images/generations", body);
|
|
292
|
+
const result = await callGateway(ctx, "/v1/images/generations", body);
|
|
287
293
|
if (!result.b64_json) {
|
|
288
294
|
throw new ImageCliError("Gateway response did not include image bytes.");
|
|
289
295
|
}
|
|
@@ -341,14 +347,16 @@ async function runEditSubcommand(ctx: ImageCliContext): Promise<void> {
|
|
|
341
347
|
`Too many --input paths (${inputs.length}); the gateway accepts at most 8.`,
|
|
342
348
|
);
|
|
343
349
|
}
|
|
344
|
-
const inputBase64 = await Promise.all(
|
|
350
|
+
const inputBase64 = await Promise.all(
|
|
351
|
+
inputs.map((p) => readBase64FromPath(p)),
|
|
352
|
+
);
|
|
345
353
|
const maskBase64 = mask ? await readBase64FromPath(mask) : undefined;
|
|
346
354
|
const body = {
|
|
347
355
|
...buildGenerationBody(prompt, opts),
|
|
348
356
|
input_images_base64: inputBase64,
|
|
349
357
|
...(maskBase64 ? { mask_base64: maskBase64 } : {}),
|
|
350
358
|
};
|
|
351
|
-
const result = await callGateway("/v1/images/edits", body);
|
|
359
|
+
const result = await callGateway(ctx, "/v1/images/edits", body);
|
|
352
360
|
if (!result.b64_json) {
|
|
353
361
|
throw new ImageCliError("Gateway response did not include image bytes.");
|
|
354
362
|
}
|
|
@@ -379,9 +387,10 @@ function imageHelp(): void {
|
|
|
379
387
|
console.log(`Usage: dench image <subcommand>
|
|
380
388
|
|
|
381
389
|
Generate or edit images via the Dench Cloud Gateway (gpt-image-2). Auths
|
|
382
|
-
with DENCH_API_KEY
|
|
383
|
-
writes the bytes to Convex
|
|
384
|
-
and, when running inside a sandbox,
|
|
390
|
+
with DENCH_API_KEY or the active \`dench signin\` agent session. Agent
|
|
391
|
+
sessions require a paid workspace. Each call writes the bytes to Convex
|
|
392
|
+
Storage (workspace tree updates immediately) and, when running inside a sandbox,
|
|
393
|
+
also writes the decoded bytes to
|
|
385
394
|
local disk so subsequent bash commands see the file without waiting on
|
|
386
395
|
the daemon's downward sync.
|
|
387
396
|
|
|
@@ -397,7 +406,7 @@ Edit / remix one or more existing images (image-to-image):
|
|
|
397
406
|
[--save-path ...] [--output ...] [--no-write] [--json]
|
|
398
407
|
|
|
399
408
|
Environment variables:
|
|
400
|
-
DENCH_API_KEY
|
|
409
|
+
DENCH_API_KEY Optional when a dench signin agent session is active.
|
|
401
410
|
DENCH_GATEWAY_URL Override the gateway base URL.
|
|
402
411
|
GATEWAY_URL Same, used when DENCH_GATEWAY_URL is unset.
|
|
403
412
|
DENCH_HOST If it points at localhost, defaults to
|
|
@@ -81,7 +81,7 @@ export function resolveEnrichmentGatewayBaseUrl(
|
|
|
81
81
|
env: EnrichmentGatewayEnv = process.env,
|
|
82
82
|
): string {
|
|
83
83
|
const explicit = env.DENCH_GATEWAY_URL?.trim() || env.GATEWAY_URL?.trim();
|
|
84
|
-
if (explicit) return explicit.replace(/\/+$/, "");
|
|
84
|
+
if (explicit) return explicit.replace(/\/+$/, "").replace(/\/v1$/, "");
|
|
85
85
|
|
|
86
86
|
const host = env.DENCH_HOST?.trim();
|
|
87
87
|
if (host && /localhost|127\.0\.0\.1/.test(host)) {
|
|
@@ -1248,7 +1248,7 @@ async function callGatewayJson(
|
|
|
1248
1248
|
|
|
1249
1249
|
function resolveBaseUrl(options: EnrichmentGatewayOptions): string {
|
|
1250
1250
|
return (
|
|
1251
|
-
options.baseUrl?.trim().replace(/\/+$/, "") ??
|
|
1251
|
+
options.baseUrl?.trim().replace(/\/+$/, "").replace(/\/v1$/, "") ??
|
|
1252
1252
|
resolveEnrichmentGatewayBaseUrl(options.env)
|
|
1253
1253
|
);
|
|
1254
1254
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dench.com/cli",
|
|
3
|
-
"version": "2.0.
|
|
3
|
+
"version": "2.0.1",
|
|
4
4
|
"description": "Dench agent workspace CLI. v2 unifies auth behind `dench signin`; the legacy `dench login` / `dench onboard` / `dench setup` / `dench what-can-i-do` / `dench register` commands now error with a redirect.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
package/search.ts
CHANGED
|
@@ -2,11 +2,9 @@
|
|
|
2
2
|
* `dench search` — live web access through the Dench Cloud Gateway.
|
|
3
3
|
*
|
|
4
4
|
* The gateway exposes Exa Search at three routes; we mirror them as
|
|
5
|
-
* three subcommands and authenticate with
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
* gateway, so it works in any environment that has the env var
|
|
9
|
-
* (sandboxes, CI, local dev with `DENCH_API_KEY` exported manually).
|
|
5
|
+
* three subcommands and authenticate with either `DENCH_API_KEY` or a
|
|
6
|
+
* paid-workspace gateway key resolved from the active agent session by
|
|
7
|
+
* the top-level dispatcher.
|
|
10
8
|
*
|
|
11
9
|
* Subcommands:
|
|
12
10
|
* search "<query>" [--num-results N] [--type auto|fast|deep|deep-reasoning|neural]
|
|
@@ -17,32 +15,21 @@
|
|
|
17
15
|
* search answer "<query>" [--json]
|
|
18
16
|
*/
|
|
19
17
|
|
|
20
|
-
import {
|
|
21
|
-
CliArgError,
|
|
22
|
-
getFlag,
|
|
23
|
-
hasFlag,
|
|
24
|
-
shift as shiftRaw,
|
|
25
|
-
} from "./lib/cli-args";
|
|
18
|
+
import { getFlag } from "./lib/cli-args";
|
|
26
19
|
|
|
27
20
|
type SearchCliContext = {
|
|
28
21
|
args: string[];
|
|
29
22
|
jsonOutput: boolean;
|
|
23
|
+
bearerToken?: string;
|
|
24
|
+
gatewayBaseUrl?: string;
|
|
30
25
|
};
|
|
31
26
|
|
|
32
27
|
class SearchCliError extends Error {}
|
|
33
28
|
|
|
34
|
-
function
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
if (error instanceof CliArgError) {
|
|
39
|
-
throw new SearchCliError(error.message);
|
|
40
|
-
}
|
|
41
|
-
throw error;
|
|
42
|
-
}
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
function parsePositiveInt(name: string, raw: string | undefined): number | undefined {
|
|
29
|
+
function parsePositiveInt(
|
|
30
|
+
name: string,
|
|
31
|
+
raw: string | undefined,
|
|
32
|
+
): number | undefined {
|
|
46
33
|
if (raw === undefined) return undefined;
|
|
47
34
|
const parsed = Number(raw);
|
|
48
35
|
if (!Number.isFinite(parsed) || parsed <= 0) {
|
|
@@ -78,10 +65,19 @@ const ALLOWED_CATEGORIES = new Set([
|
|
|
78
65
|
"people",
|
|
79
66
|
]);
|
|
80
67
|
|
|
81
|
-
function
|
|
68
|
+
function normalizeGatewayBaseUrl(value: string): string {
|
|
69
|
+
return value.replace(/\/+$/, "").replace(/\/v1$/, "");
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function resolveGatewayBaseUrl(
|
|
73
|
+
ctx?: Pick<SearchCliContext, "gatewayBaseUrl">,
|
|
74
|
+
): string {
|
|
75
|
+
if (ctx?.gatewayBaseUrl?.trim()) {
|
|
76
|
+
return normalizeGatewayBaseUrl(ctx.gatewayBaseUrl.trim());
|
|
77
|
+
}
|
|
82
78
|
const explicit =
|
|
83
79
|
process.env.DENCH_GATEWAY_URL?.trim() || process.env.GATEWAY_URL?.trim();
|
|
84
|
-
if (explicit) return explicit
|
|
80
|
+
if (explicit) return normalizeGatewayBaseUrl(explicit);
|
|
85
81
|
// Local dev: the gateway listens on :8787 (matches gateway/src/index.ts
|
|
86
82
|
// and the dev-server skill). Anything that smells like localhost in
|
|
87
83
|
// DENCH_HOST routes to the local gateway too so `dench --host
|
|
@@ -93,29 +89,31 @@ function resolveGatewayBaseUrl(): string {
|
|
|
93
89
|
return "https://gateway.merseoriginals.com";
|
|
94
90
|
}
|
|
95
91
|
|
|
96
|
-
function
|
|
97
|
-
|
|
98
|
-
|
|
92
|
+
function requireBearerToken(
|
|
93
|
+
ctx?: Pick<SearchCliContext, "bearerToken">,
|
|
94
|
+
): string {
|
|
95
|
+
const bearerToken =
|
|
96
|
+
ctx?.bearerToken?.trim() || process.env.DENCH_API_KEY?.trim();
|
|
97
|
+
if (!bearerToken) {
|
|
99
98
|
throw new SearchCliError(
|
|
100
|
-
"
|
|
101
|
-
"the env automatically; outside, export it manually or run " +
|
|
102
|
-
"`dench login` first.",
|
|
99
|
+
"No Dench gateway credential found. Set DENCH_API_KEY or run `dench signin` for a paid workspace.",
|
|
103
100
|
);
|
|
104
101
|
}
|
|
105
|
-
return
|
|
102
|
+
return bearerToken;
|
|
106
103
|
}
|
|
107
104
|
|
|
108
105
|
async function callGateway(
|
|
106
|
+
ctx: SearchCliContext,
|
|
109
107
|
path: "/v1/search" | "/v1/search/contents" | "/v1/search/answer",
|
|
110
108
|
body: Record<string, unknown>,
|
|
111
109
|
): Promise<unknown> {
|
|
112
|
-
const
|
|
113
|
-
const baseUrl = resolveGatewayBaseUrl();
|
|
110
|
+
const bearerToken = requireBearerToken(ctx);
|
|
111
|
+
const baseUrl = resolveGatewayBaseUrl(ctx);
|
|
114
112
|
const response = await fetch(`${baseUrl}${path}`, {
|
|
115
113
|
method: "POST",
|
|
116
114
|
headers: {
|
|
117
115
|
"content-type": "application/json",
|
|
118
|
-
Authorization: `Bearer ${
|
|
116
|
+
Authorization: `Bearer ${bearerToken}`,
|
|
119
117
|
},
|
|
120
118
|
body: JSON.stringify(body),
|
|
121
119
|
});
|
|
@@ -143,7 +141,9 @@ function asString(value: unknown): string | undefined {
|
|
|
143
141
|
}
|
|
144
142
|
|
|
145
143
|
function asNumber(value: unknown): number | undefined {
|
|
146
|
-
return typeof value === "number" && Number.isFinite(value)
|
|
144
|
+
return typeof value === "number" && Number.isFinite(value)
|
|
145
|
+
? value
|
|
146
|
+
: undefined;
|
|
147
147
|
}
|
|
148
148
|
|
|
149
149
|
function asObject(value: unknown): Record<string, unknown> | undefined {
|
|
@@ -184,7 +184,8 @@ function formatSearchResults(payload: unknown, limit?: number): string {
|
|
|
184
184
|
lines.push("(no results)");
|
|
185
185
|
return lines.join("\n");
|
|
186
186
|
}
|
|
187
|
-
const cap =
|
|
187
|
+
const cap =
|
|
188
|
+
limit !== undefined ? Math.min(results.length, limit) : results.length;
|
|
188
189
|
for (let i = 0; i < cap; i++) {
|
|
189
190
|
const result = asObject(results[i]);
|
|
190
191
|
if (!result) continue;
|
|
@@ -197,7 +198,9 @@ function formatSearchResults(payload: unknown, limit?: number): string {
|
|
|
197
198
|
const snippet =
|
|
198
199
|
asString(result.text) ??
|
|
199
200
|
(Array.isArray(result.highlights)
|
|
200
|
-
? result.highlights
|
|
201
|
+
? result.highlights
|
|
202
|
+
.filter((h): h is string => typeof h === "string")
|
|
203
|
+
.join(" ")
|
|
201
204
|
: undefined) ??
|
|
202
205
|
asString(result.summary);
|
|
203
206
|
lines.push(`${i + 1}. ${title}`);
|
|
@@ -284,7 +287,10 @@ function out(ctx: SearchCliContext, payload: unknown, formatted: string): void {
|
|
|
284
287
|
// ────────────────────────────────────────────────────────────────────────────
|
|
285
288
|
|
|
286
289
|
async function runSearchSubcommand(ctx: SearchCliContext): Promise<void> {
|
|
287
|
-
const numResults = parsePositiveInt(
|
|
290
|
+
const numResults = parsePositiveInt(
|
|
291
|
+
"--num-results",
|
|
292
|
+
getFlag(ctx.args, "--num-results"),
|
|
293
|
+
);
|
|
288
294
|
const type = getFlag(ctx.args, "--type");
|
|
289
295
|
if (type !== undefined && !ALLOWED_TYPES.has(type)) {
|
|
290
296
|
throw new SearchCliError(
|
|
@@ -321,12 +327,15 @@ async function runSearchSubcommand(ctx: SearchCliContext): Promise<void> {
|
|
|
321
327
|
if (category) body.category = category;
|
|
322
328
|
if (includeDomains) body.includeDomains = includeDomains;
|
|
323
329
|
if (excludeDomains) body.excludeDomains = excludeDomains;
|
|
324
|
-
const payload = await callGateway("/v1/search", body);
|
|
330
|
+
const payload = await callGateway(ctx, "/v1/search", body);
|
|
325
331
|
out(ctx, payload, formatSearchResults(payload, numResults));
|
|
326
332
|
}
|
|
327
333
|
|
|
328
334
|
async function runContentsSubcommand(ctx: SearchCliContext): Promise<void> {
|
|
329
|
-
const maxChars = parsePositiveInt(
|
|
335
|
+
const maxChars = parsePositiveInt(
|
|
336
|
+
"--max-chars",
|
|
337
|
+
getFlag(ctx.args, "--max-chars"),
|
|
338
|
+
);
|
|
330
339
|
const summary = getFlag(ctx.args, "--summary");
|
|
331
340
|
const urls = ctx.args.slice();
|
|
332
341
|
ctx.args.length = 0;
|
|
@@ -346,7 +355,7 @@ async function runContentsSubcommand(ctx: SearchCliContext): Promise<void> {
|
|
|
346
355
|
highlights: { maxCharacters: 200 },
|
|
347
356
|
};
|
|
348
357
|
if (summary) body.summary = { query: summary };
|
|
349
|
-
const payload = await callGateway("/v1/search/contents", body);
|
|
358
|
+
const payload = await callGateway(ctx, "/v1/search/contents", body);
|
|
350
359
|
out(ctx, payload, formatContentsResults(payload));
|
|
351
360
|
}
|
|
352
361
|
|
|
@@ -355,11 +364,9 @@ async function runAnswerSubcommand(ctx: SearchCliContext): Promise<void> {
|
|
|
355
364
|
ctx.args.length = 0;
|
|
356
365
|
const query = queryParts.join(" ").trim();
|
|
357
366
|
if (!query) {
|
|
358
|
-
throw new SearchCliError(
|
|
359
|
-
'Usage: dench search answer "<query>" [--json]',
|
|
360
|
-
);
|
|
367
|
+
throw new SearchCliError('Usage: dench search answer "<query>" [--json]');
|
|
361
368
|
}
|
|
362
|
-
const payload = await callGateway("/v1/search/answer", {
|
|
369
|
+
const payload = await callGateway(ctx, "/v1/search/answer", {
|
|
363
370
|
query,
|
|
364
371
|
text: true,
|
|
365
372
|
stream: false,
|
|
@@ -371,7 +378,8 @@ function searchHelp(): void {
|
|
|
371
378
|
console.log(`Usage: dench search <subcommand>
|
|
372
379
|
|
|
373
380
|
Live web access through the Dench Cloud Gateway (Exa Search). Auths
|
|
374
|
-
with DENCH_API_KEY
|
|
381
|
+
with DENCH_API_KEY or the active \`dench signin\` agent session. Agent
|
|
382
|
+
sessions require a paid workspace.
|
|
375
383
|
|
|
376
384
|
Search the web (default subcommand):
|
|
377
385
|
dench search "<query>" [--num-results N] [--type auto|fast|deep|deep-reasoning|neural]
|
|
@@ -386,7 +394,7 @@ One-shot synthesised answer with citations (Exa /search/answer):
|
|
|
386
394
|
dench search answer "<query>" [--json]
|
|
387
395
|
|
|
388
396
|
Environment variables:
|
|
389
|
-
DENCH_API_KEY
|
|
397
|
+
DENCH_API_KEY Optional when a dench signin agent session is active.
|
|
390
398
|
DENCH_GATEWAY_URL Override the gateway base URL.
|
|
391
399
|
GATEWAY_URL Same, used when DENCH_GATEWAY_URL is unset.
|
|
392
400
|
DENCH_HOST If it points at localhost, defaults to
|
package/tools.ts
CHANGED
|
@@ -3,13 +3,9 @@
|
|
|
3
3
|
* Composio's get-apps / get-tools / execute-tool surface.
|
|
4
4
|
*
|
|
5
5
|
* Mirrors `denchclaw`'s pattern: this CLI is a thin wrapper over five
|
|
6
|
-
* gateway endpoints, authenticated with
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
* environment that has the API key (sandboxes, CI, local dev with the
|
|
10
|
-
* key exported manually). The previous implementation forced
|
|
11
|
-
* `requireSessionRuntime` and routed through a Convex action, which
|
|
12
|
-
* broke `apiKey` runtime mode entirely.
|
|
6
|
+
* gateway endpoints, authenticated with either `DENCH_API_KEY` or a
|
|
7
|
+
* paid-workspace gateway key resolved from the active agent session by
|
|
8
|
+
* the top-level dispatcher.
|
|
13
9
|
*
|
|
14
10
|
* Subcommands:
|
|
15
11
|
*
|
|
@@ -20,7 +16,7 @@
|
|
|
20
16
|
* dench tool connect <toolkit> [--callback-url <url>] [--json] — print OAuth redirect URL
|
|
21
17
|
* dench tool disconnect <connectionId> [--json]
|
|
22
18
|
*
|
|
23
|
-
* Endpoints used (all `Authorization: Bearer
|
|
19
|
+
* Endpoints used (all `Authorization: Bearer <gateway credential>`):
|
|
24
20
|
*
|
|
25
21
|
* GET /v1/composio/connections
|
|
26
22
|
* GET /v1/composio/toolkits?search=<slug>
|
|
@@ -36,10 +32,7 @@
|
|
|
36
32
|
* working.
|
|
37
33
|
*/
|
|
38
34
|
|
|
39
|
-
import {
|
|
40
|
-
PRODUCTION_API_URL,
|
|
41
|
-
PRODUCTION_GATEWAY_URL,
|
|
42
|
-
} from "./host";
|
|
35
|
+
import { PRODUCTION_API_URL, PRODUCTION_GATEWAY_URL } from "./host";
|
|
43
36
|
import {
|
|
44
37
|
CliArgError,
|
|
45
38
|
getFlag,
|
|
@@ -55,8 +48,15 @@ import {
|
|
|
55
48
|
export type ToolCliContext = {
|
|
56
49
|
args: string[];
|
|
57
50
|
jsonOutput: boolean;
|
|
51
|
+
bearerToken?: string;
|
|
52
|
+
gatewayBaseUrl?: string;
|
|
58
53
|
};
|
|
59
54
|
|
|
55
|
+
type GatewayAuthContext = Pick<
|
|
56
|
+
ToolCliContext,
|
|
57
|
+
"bearerToken" | "gatewayBaseUrl"
|
|
58
|
+
>;
|
|
59
|
+
|
|
60
60
|
export async function runToolCommand(ctx: ToolCliContext): Promise<void> {
|
|
61
61
|
const sub = ctx.args[0];
|
|
62
62
|
if (!sub || sub === "help" || sub === "--help" || sub === "-h") {
|
|
@@ -91,11 +91,14 @@ export async function runToolCommand(ctx: ToolCliContext): Promise<void> {
|
|
|
91
91
|
|
|
92
92
|
/**
|
|
93
93
|
* Summarise the user's connected apps for `dench context` and friends.
|
|
94
|
-
*
|
|
94
|
+
* Speaks straight to the gateway with either DENCH_API_KEY or a gateway
|
|
95
|
+
* key resolved from the active agent session.
|
|
95
96
|
* Returns the same legacy shape the previous Convex-routed path did so
|
|
96
97
|
* existing parsers don't need to change.
|
|
97
98
|
*/
|
|
98
|
-
export async function fetchConnectedAppsSummary(
|
|
99
|
+
export async function fetchConnectedAppsSummary(
|
|
100
|
+
ctx: GatewayAuthContext = {},
|
|
101
|
+
): Promise<{
|
|
99
102
|
available: boolean;
|
|
100
103
|
count: number;
|
|
101
104
|
connections: Array<{
|
|
@@ -109,7 +112,7 @@ export async function fetchConnectedAppsSummary(): Promise<{
|
|
|
109
112
|
}> {
|
|
110
113
|
let payload: unknown;
|
|
111
114
|
try {
|
|
112
|
-
payload = await callGateway("GET", "/v1/composio/connections");
|
|
115
|
+
payload = await callGateway(ctx, "GET", "/v1/composio/connections");
|
|
113
116
|
} catch (error) {
|
|
114
117
|
if (error instanceof MissingApiKeyError) {
|
|
115
118
|
return {
|
|
@@ -139,6 +142,7 @@ async function runStatusSubcommand(ctx: ToolCliContext): Promise<void> {
|
|
|
139
142
|
if (filterToolkit) ctx.args.shift();
|
|
140
143
|
|
|
141
144
|
const connectionsPayload = await callGateway(
|
|
145
|
+
ctx,
|
|
142
146
|
"GET",
|
|
143
147
|
"/v1/composio/connections",
|
|
144
148
|
);
|
|
@@ -146,6 +150,7 @@ async function runStatusSubcommand(ctx: ToolCliContext): Promise<void> {
|
|
|
146
150
|
if (filterToolkit) {
|
|
147
151
|
const search = encodeURIComponent(filterToolkit);
|
|
148
152
|
toolkitsPayload = await callGateway(
|
|
153
|
+
ctx,
|
|
149
154
|
"GET",
|
|
150
155
|
`/v1/composio/toolkits?search=${search}`,
|
|
151
156
|
);
|
|
@@ -194,7 +199,12 @@ async function runSearchSubcommand(ctx: ToolCliContext): Promise<void> {
|
|
|
194
199
|
if (toolkit) body.toolkit_slug = toolkit;
|
|
195
200
|
if (limit) body.limit = limit;
|
|
196
201
|
|
|
197
|
-
const payload = await callGateway(
|
|
202
|
+
const payload = await callGateway(
|
|
203
|
+
ctx,
|
|
204
|
+
"POST",
|
|
205
|
+
"/v1/composio/tools/search",
|
|
206
|
+
body,
|
|
207
|
+
);
|
|
198
208
|
if (ctx.jsonOutput) {
|
|
199
209
|
console.log(JSON.stringify(payload, null, 2));
|
|
200
210
|
return;
|
|
@@ -231,7 +241,12 @@ async function runRunSubcommand(ctx: ToolCliContext): Promise<void> {
|
|
|
231
241
|
|
|
232
242
|
let payload: unknown;
|
|
233
243
|
try {
|
|
234
|
-
payload = await callGateway(
|
|
244
|
+
payload = await callGateway(
|
|
245
|
+
ctx,
|
|
246
|
+
"POST",
|
|
247
|
+
"/v1/composio/tools/execute",
|
|
248
|
+
body,
|
|
249
|
+
);
|
|
235
250
|
} catch (error) {
|
|
236
251
|
if (error instanceof GatewayHttpError && isLikelyNoConnection(error)) {
|
|
237
252
|
const noConnPayload = noConnectionPayload(toolSlug, error);
|
|
@@ -266,7 +281,7 @@ async function runConnectSubcommand(ctx: ToolCliContext): Promise<void> {
|
|
|
266
281
|
process.env.DENCH_OAUTH_CALLBACK_URL?.trim() ??
|
|
267
282
|
`${resolveAppPublicOrigin()}/api/composio/callback`;
|
|
268
283
|
|
|
269
|
-
const payload = await callGateway("POST", "/v1/composio/connect", {
|
|
284
|
+
const payload = await callGateway(ctx, "POST", "/v1/composio/connect", {
|
|
270
285
|
toolkit,
|
|
271
286
|
callback_url: callbackUrl,
|
|
272
287
|
});
|
|
@@ -313,7 +328,7 @@ async function runDisconnectSubcommand(ctx: ToolCliContext): Promise<void> {
|
|
|
313
328
|
const path = `/v1/composio/connections/${encodeURIComponent(connectionId)}`;
|
|
314
329
|
let payload: unknown = { ok: true, deleted: true };
|
|
315
330
|
try {
|
|
316
|
-
payload = await callGateway("DELETE", path);
|
|
331
|
+
payload = await callGateway(ctx, "DELETE", path);
|
|
317
332
|
} catch (error) {
|
|
318
333
|
// Composio returns 404 for connections that are already gone — surface
|
|
319
334
|
// that as a soft success so callers can keep their local state in sync.
|
|
@@ -350,9 +365,8 @@ class ToolCliError extends Error {
|
|
|
350
365
|
class MissingApiKeyError extends ToolCliError {
|
|
351
366
|
constructor() {
|
|
352
367
|
super(
|
|
353
|
-
"
|
|
354
|
-
"
|
|
355
|
-
"`dench login` and copy the api-key from the output.",
|
|
368
|
+
"No Dench gateway credential found. Set DENCH_API_KEY or run " +
|
|
369
|
+
"`dench signin` for a paid workspace.",
|
|
356
370
|
);
|
|
357
371
|
this.name = "MissingApiKeyError";
|
|
358
372
|
}
|
|
@@ -376,10 +390,19 @@ class GatewayHttpError extends ToolCliError {
|
|
|
376
390
|
}
|
|
377
391
|
}
|
|
378
392
|
|
|
379
|
-
function
|
|
393
|
+
function normalizeGatewayBaseUrl(value: string): string {
|
|
394
|
+
return value.replace(/\/+$/, "").replace(/\/v1$/, "");
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
function resolveGatewayBaseUrl(
|
|
398
|
+
ctx?: Pick<ToolCliContext, "gatewayBaseUrl">,
|
|
399
|
+
): string {
|
|
400
|
+
if (ctx?.gatewayBaseUrl?.trim()) {
|
|
401
|
+
return normalizeGatewayBaseUrl(ctx.gatewayBaseUrl.trim());
|
|
402
|
+
}
|
|
380
403
|
const explicit =
|
|
381
404
|
process.env.DENCH_GATEWAY_URL?.trim() || process.env.GATEWAY_URL?.trim();
|
|
382
|
-
if (explicit) return explicit
|
|
405
|
+
if (explicit) return normalizeGatewayBaseUrl(explicit);
|
|
383
406
|
const host = process.env.DENCH_HOST?.trim();
|
|
384
407
|
if (host && /localhost|127\.0\.0\.1/.test(host)) {
|
|
385
408
|
return "http://localhost:8787";
|
|
@@ -410,24 +433,26 @@ function resolveAppPublicOrigin(): string {
|
|
|
410
433
|
return PRODUCTION_API_URL;
|
|
411
434
|
}
|
|
412
435
|
|
|
413
|
-
function
|
|
414
|
-
const
|
|
415
|
-
|
|
416
|
-
|
|
436
|
+
function requireBearerToken(ctx?: Pick<ToolCliContext, "bearerToken">): string {
|
|
437
|
+
const bearerToken =
|
|
438
|
+
ctx?.bearerToken?.trim() || process.env.DENCH_API_KEY?.trim();
|
|
439
|
+
if (!bearerToken) throw new MissingApiKeyError();
|
|
440
|
+
return bearerToken;
|
|
417
441
|
}
|
|
418
442
|
|
|
419
443
|
async function callGateway(
|
|
444
|
+
ctx: GatewayAuthContext,
|
|
420
445
|
method: "GET" | "POST" | "DELETE",
|
|
421
446
|
path: string,
|
|
422
447
|
body?: Record<string, unknown>,
|
|
423
448
|
): Promise<unknown> {
|
|
424
|
-
const
|
|
425
|
-
const baseUrl = resolveGatewayBaseUrl();
|
|
449
|
+
const bearerToken = requireBearerToken(ctx);
|
|
450
|
+
const baseUrl = resolveGatewayBaseUrl(ctx);
|
|
426
451
|
const init: RequestInit = {
|
|
427
452
|
method,
|
|
428
453
|
headers: {
|
|
429
454
|
"content-type": "application/json",
|
|
430
|
-
authorization: `Bearer ${
|
|
455
|
+
authorization: `Bearer ${bearerToken}`,
|
|
431
456
|
},
|
|
432
457
|
};
|
|
433
458
|
if (body !== undefined) init.body = JSON.stringify(body);
|
|
@@ -730,8 +755,8 @@ Usage:
|
|
|
730
755
|
dench tool disconnect <connectionId> [--json]
|
|
731
756
|
|
|
732
757
|
Notes:
|
|
733
|
-
All commands authenticate with DENCH_API_KEY
|
|
734
|
-
|
|
758
|
+
All commands authenticate with DENCH_API_KEY or the active \`dench signin\`
|
|
759
|
+
agent session. Agent sessions require a paid workspace.
|
|
735
760
|
dench tool connect prints the OAuth redirect URL for the human to open.
|
|
736
761
|
dench tool run output is redacted for display; pass --json for raw output.
|
|
737
762
|
Override the gateway base with DENCH_GATEWAY_URL or GATEWAY_URL.
|
|
@@ -804,7 +829,7 @@ function parsePositiveInt(name: string, raw: string): number {
|
|
|
804
829
|
// them as unused when callers tree-shake.
|
|
805
830
|
export { ToolCliError, GatewayHttpError, MissingApiKeyError };
|
|
806
831
|
// Kept reachable so a future caller can build their own dispatcher.
|
|
807
|
-
export { resolveGatewayBaseUrl,
|
|
832
|
+
export { resolveGatewayBaseUrl, requireBearerToken, callGateway };
|
|
808
833
|
// shiftRaw / hasFlag from cli-args are not used in this module yet but
|
|
809
834
|
// keep the import group consistent with cli/search.ts conventions.
|
|
810
835
|
void shiftRaw;
|