@dench.com/cli 0.4.9 → 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/README.md +1 -1
- package/crm.ts +60 -26
- package/dench.ts +870 -229
- package/host.ts +3 -3
- package/image.ts +49 -40
- package/lib/enrichment-gateway.ts +30 -13
- package/package.json +2 -2
- package/search.ts +56 -48
- package/tools.ts +59 -34
package/host.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export const PRODUCTION_HOST = "https://dench.
|
|
1
|
+
export const PRODUCTION_HOST = "https://dench.com";
|
|
2
2
|
export const STAGING_HOST = "https://workspace-staging.dench.com";
|
|
3
3
|
export const LOCAL_HOST = "http://localhost:3000";
|
|
4
4
|
export const DEFAULT_HOST = PRODUCTION_HOST;
|
|
@@ -63,7 +63,7 @@ export function resolveBackendAlias(input: string): string {
|
|
|
63
63
|
const value = input.trim();
|
|
64
64
|
if (!value) {
|
|
65
65
|
throw new Error(
|
|
66
|
-
"Backend value is empty. Pass local, staging, prod, or a URL like https://dench.
|
|
66
|
+
"Backend value is empty. Pass local, staging, prod, or a URL like https://dench.com.",
|
|
67
67
|
);
|
|
68
68
|
}
|
|
69
69
|
const normalized = value.toLowerCase();
|
|
@@ -80,7 +80,7 @@ export function resolveBackendAlias(input: string): string {
|
|
|
80
80
|
return normalizeHost(value);
|
|
81
81
|
} catch (_error) {
|
|
82
82
|
throw new Error(
|
|
83
|
-
`Unrecognized backend "${value}". Use local, staging, prod, or a URL like https://dench.
|
|
83
|
+
`Unrecognized backend "${value}". Use local, staging, prod, or a URL like https://dench.com.`,
|
|
84
84
|
);
|
|
85
85
|
}
|
|
86
86
|
}
|
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)) {
|
|
@@ -161,6 +161,7 @@ export type ApolloPersonEnvelope = {
|
|
|
161
161
|
linkedin_url?: string;
|
|
162
162
|
title?: string;
|
|
163
163
|
headline?: string;
|
|
164
|
+
location?: string;
|
|
164
165
|
contact: {
|
|
165
166
|
phone_numbers: Array<{
|
|
166
167
|
sanitized_number: string;
|
|
@@ -178,7 +179,12 @@ type EnrichedPersonUpsertLike = {
|
|
|
178
179
|
/** Structured title from the person's current role (distinct from headline). */
|
|
179
180
|
currentTitle?: string;
|
|
180
181
|
linkedinID?: string;
|
|
181
|
-
|
|
182
|
+
location?: string;
|
|
183
|
+
emails?: Array<{
|
|
184
|
+
address?: string;
|
|
185
|
+
normalizedAddress?: string;
|
|
186
|
+
type?: string;
|
|
187
|
+
}>;
|
|
182
188
|
phones?: Array<{ number?: string; normalizedNumber?: string }>;
|
|
183
189
|
};
|
|
184
190
|
|
|
@@ -201,6 +207,7 @@ function shapePersonToApolloEnvelope(
|
|
|
201
207
|
: undefined,
|
|
202
208
|
title: u.currentTitle,
|
|
203
209
|
headline: u.headline,
|
|
210
|
+
location: u.location,
|
|
204
211
|
contact: {
|
|
205
212
|
phone_numbers: (u.phones ?? []).map((p) => ({
|
|
206
213
|
sanitized_number: p.normalizedNumber ?? p.number ?? "",
|
|
@@ -523,8 +530,7 @@ async function submitContactBulkChunk(
|
|
|
523
530
|
const cached = resp.cachedResults ?? [];
|
|
524
531
|
const seenIndexes = new Set<number>();
|
|
525
532
|
for (const hit of cached) {
|
|
526
|
-
const idx =
|
|
527
|
-
typeof hit.inputIndex === "number" ? hit.inputIndex : undefined;
|
|
533
|
+
const idx = typeof hit.inputIndex === "number" ? hit.inputIndex : undefined;
|
|
528
534
|
if (idx === undefined) continue;
|
|
529
535
|
seenIndexes.add(idx);
|
|
530
536
|
results.set(idx, {
|
|
@@ -684,7 +690,10 @@ export async function enrichPersonBulk(
|
|
|
684
690
|
return;
|
|
685
691
|
}
|
|
686
692
|
try {
|
|
687
|
-
validContacts.push({
|
|
693
|
+
validContacts.push({
|
|
694
|
+
inputIndex: index,
|
|
695
|
+
payload: buildContactPayload(body),
|
|
696
|
+
});
|
|
688
697
|
} catch (err) {
|
|
689
698
|
results.set(index, {
|
|
690
699
|
ok: false,
|
|
@@ -751,8 +760,7 @@ async function submitReverseEmailBulkChunk(
|
|
|
751
760
|
|
|
752
761
|
const cached = resp.cachedResults ?? [];
|
|
753
762
|
for (const hit of cached) {
|
|
754
|
-
const idx =
|
|
755
|
-
typeof hit.inputIndex === "number" ? hit.inputIndex : undefined;
|
|
763
|
+
const idx = typeof hit.inputIndex === "number" ? hit.inputIndex : undefined;
|
|
756
764
|
if (idx === undefined) continue;
|
|
757
765
|
// The gateway dedupes emails, so a cache hit at index i corresponds to
|
|
758
766
|
// the deduped email at that position in the request — we then fan out to
|
|
@@ -988,7 +996,9 @@ export async function enrichCompany(
|
|
|
988
996
|
body: EnrichCompanyBody,
|
|
989
997
|
options: EnrichmentGatewayOptions = {},
|
|
990
998
|
): Promise<Record<string, unknown>> {
|
|
991
|
-
const domain = body.domain
|
|
999
|
+
const domain = body.domain
|
|
1000
|
+
? (extractDomain(body.domain) ?? body.domain)
|
|
1001
|
+
: undefined;
|
|
992
1002
|
const companyName = body.companyName?.trim();
|
|
993
1003
|
const linkedinUrl = body.linkedinUrl?.trim();
|
|
994
1004
|
|
|
@@ -1075,7 +1085,11 @@ export async function aviatoEnrichPerson(
|
|
|
1075
1085
|
|
|
1076
1086
|
const resp = raw as {
|
|
1077
1087
|
data?: EnrichedPersonUpsertLike | null;
|
|
1078
|
-
billing?: {
|
|
1088
|
+
billing?: {
|
|
1089
|
+
charged_cents?: number;
|
|
1090
|
+
cache_hit?: boolean;
|
|
1091
|
+
preview?: boolean;
|
|
1092
|
+
};
|
|
1079
1093
|
provider?: { name?: string; aviato_id?: string };
|
|
1080
1094
|
};
|
|
1081
1095
|
|
|
@@ -1125,7 +1139,11 @@ export async function aviatoEnrichCompany(
|
|
|
1125
1139
|
|
|
1126
1140
|
const resp = raw as {
|
|
1127
1141
|
data?: EnrichedCompanyUpsertLike | null;
|
|
1128
|
-
billing?: {
|
|
1142
|
+
billing?: {
|
|
1143
|
+
charged_cents?: number;
|
|
1144
|
+
cache_hit?: boolean;
|
|
1145
|
+
preview?: boolean;
|
|
1146
|
+
};
|
|
1129
1147
|
provider?: { name?: string; aviato_id?: string };
|
|
1130
1148
|
};
|
|
1131
1149
|
|
|
@@ -1219,8 +1237,7 @@ async function callGatewayJson(
|
|
|
1219
1237
|
});
|
|
1220
1238
|
|
|
1221
1239
|
const isOk =
|
|
1222
|
-
response.ok ||
|
|
1223
|
-
(args.expectedStatus?.includes(response.status) ?? false);
|
|
1240
|
+
response.ok || (args.expectedStatus?.includes(response.status) ?? false);
|
|
1224
1241
|
|
|
1225
1242
|
if (!isOk) {
|
|
1226
1243
|
throw await buildGatewayError(response, pathOrUrl.toString());
|
|
@@ -1231,7 +1248,7 @@ async function callGatewayJson(
|
|
|
1231
1248
|
|
|
1232
1249
|
function resolveBaseUrl(options: EnrichmentGatewayOptions): string {
|
|
1233
1250
|
return (
|
|
1234
|
-
options.baseUrl?.trim().replace(/\/+$/, "") ??
|
|
1251
|
+
options.baseUrl?.trim().replace(/\/+$/, "").replace(/\/v1$/, "") ??
|
|
1235
1252
|
resolveEnrichmentGatewayBaseUrl(options.env)
|
|
1236
1253
|
);
|
|
1237
1254
|
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dench.com/cli",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "Dench agent workspace CLI.",
|
|
3
|
+
"version": "2.0.1",
|
|
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": {
|
|
7
7
|
"dench": "dench",
|
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
|