@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/host.ts CHANGED
@@ -1,4 +1,4 @@
1
- export const PRODUCTION_HOST = "https://dench.dev";
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.dev.",
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.dev.`,
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 the `DENCH_API_KEY` baked into every
7
- * sandbox at create time. Works anywhere that env var is exported (sandbox,
8
- * CI, local dev).
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 resolveGatewayBaseUrl(): string {
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.replace(/\/+$/, "");
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 requireApiKey(): string {
70
- const apiKey = process.env.DENCH_API_KEY?.trim();
71
- if (!apiKey) {
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
- "DENCH_API_KEY is not set. Inside a Dench sandbox it is baked into " +
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 apiKey;
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 apiKey = requireApiKey();
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 ${apiKey}`,
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 { size, quality, format, savePath, output, noWrite, background, moderation };
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(inputs.map((p) => readBase64FromPath(p)));
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 (baked into every sandbox automatically). Each call
383
- writes the bytes to Convex Storage (workspace tree updates immediately)
384
- and, when running inside a sandbox, also writes the decoded bytes to
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 Required. Bearer token sent to the gateway.
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
- emails?: Array<{ address?: string; normalizedAddress?: string; type?: string }>;
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({ inputIndex: index, payload: buildContactPayload(body) });
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 ? extractDomain(body.domain) ?? body.domain : undefined;
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?: { charged_cents?: number; cache_hit?: boolean; preview?: boolean };
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?: { charged_cents?: number; cache_hit?: boolean; preview?: boolean };
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.9",
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 the `DENCH_API_KEY` baked
6
- * into every sandbox at create time. Unlike most other CLI surfaces
7
- * this one bypasses Convex entirely — it speaks straight HTTP to the
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 shift(args: string[], expected: string): string {
35
- try {
36
- return shiftRaw(args, expected);
37
- } catch (error) {
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 resolveGatewayBaseUrl(): string {
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.replace(/\/+$/, "");
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 requireApiKey(): string {
97
- const apiKey = process.env.DENCH_API_KEY?.trim();
98
- if (!apiKey) {
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
- "DENCH_API_KEY is not set. Inside a Dench sandbox it is baked into " +
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 apiKey;
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 apiKey = requireApiKey();
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 ${apiKey}`,
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) ? value : undefined;
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 = limit !== undefined ? Math.min(results.length, limit) : results.length;
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.filter((h): h is string => typeof h === "string").join(" ")
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("--num-results", getFlag(ctx.args, "--num-results"));
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("--max-chars", getFlag(ctx.args, "--max-chars"));
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 (baked into every sandbox automatically).
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 Required. Bearer token sent to the gateway.
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