@dench.com/cli 3.6.6-staging.386 → 3.7.0-staging.387

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.
@@ -0,0 +1,78 @@
1
+ import { COMMAND_ARGUMENTS } from "./command-args";
2
+ import { apiOperations } from "./command-registry";
3
+
4
+ /** Literal command words only; never interpret a data value as a command. */
5
+ function words(usage: string): string[] {
6
+ const parts = usage
7
+ .replace(/^dench\s+/, "")
8
+ .split(/\s+/)
9
+ .filter(Boolean);
10
+ const end = parts.findIndex((part) => /^[<[(-]/.test(part));
11
+ return end < 0 ? parts : parts.slice(0, end);
12
+ }
13
+
14
+ function matches(
15
+ prefix: readonly string[],
16
+ candidate: readonly string[],
17
+ ): boolean {
18
+ return prefix.every((word, index) =>
19
+ candidate[index]?.split("|").includes(word),
20
+ );
21
+ }
22
+
23
+ /** Registry metadata supplies discovery, never invokes command implementations. */
24
+ export function commandHelp(argv: readonly string[], usageText = ""): string {
25
+ const args = [...argv];
26
+ if (args[0] === "help") args.shift();
27
+ const input = words(args.filter((arg) => arg !== "help").join(" "));
28
+ const usageLines = usageText
29
+ .split("\n")
30
+ .map((line) => line.trim())
31
+ .filter((line) => line.startsWith("dench "));
32
+ const candidates = [
33
+ ...usageLines.map((usage) => ({ usage, description: "" })),
34
+ ...COMMAND_ARGUMENTS.map((entry) => ({
35
+ usage:
36
+ `dench ${entry.command} ${[...entry.values.map((flag) => `[${flag} <value>]`), ...entry.switches.map((flag) => `[${flag}]`)].join(" ")}`.trim(),
37
+ description: "",
38
+ })),
39
+ ...apiOperations
40
+ .filter((op) => !op.deprecated)
41
+ .map((op) => ({ usage: op.cli, description: op.summary })),
42
+ ];
43
+ let prefix = input;
44
+ while (
45
+ prefix.length &&
46
+ !candidates.some((item) => matches(prefix, words(item.usage)))
47
+ ) {
48
+ prefix = prefix.slice(0, -1);
49
+ }
50
+ const selected = candidates.filter((item) =>
51
+ matches(prefix, words(item.usage)),
52
+ );
53
+ // Domain usage has real flag spellings; a backend schema is not a CLI parser.
54
+ const exactUsage = selected.filter((item) => !item.description);
55
+ const entries = exactUsage.length ? exactUsage : selected;
56
+ const seen = new Set<string>();
57
+ const lines = entries.filter((item) => {
58
+ if (seen.has(item.usage)) return false;
59
+ seen.add(item.usage);
60
+ return true;
61
+ });
62
+ const heading = `Usage: dench ${prefix.join(" ")}${prefix.length ? "" : "<command>"}`;
63
+ return [
64
+ heading,
65
+ ...lines
66
+ .slice(0, 12)
67
+ .map(
68
+ (item) =>
69
+ ` ${item.usage}${item.description ? `\n ${item.description}` : ""}`,
70
+ ),
71
+ ...(lines.length > 12
72
+ ? [
73
+ ` ${lines.length - 12} more commands; add a subcommand before --help.`,
74
+ ]
75
+ : []),
76
+ "Options: --help, -h (local help); --json (structured command results).",
77
+ ].join("\n");
78
+ }
@@ -974,6 +974,18 @@ const rawApiOperations = [
974
974
  responseSchema: anyResponse,
975
975
  backend: convex("query", "functions/crm/objects:list"),
976
976
  }),
977
+ op({
978
+ id: "crm.objects.listCompact",
979
+ group: "crm",
980
+ summary:
981
+ "List names, display labels, visibility and workspace counts without internal state.",
982
+ cli: "dench crm objects list --compact",
983
+ method: "GET",
984
+ path: "/crm/catalog/objects",
985
+ requestSchema: anyObject,
986
+ responseSchema: anyResponse,
987
+ backend: convex("query", "functions/crm/objects:listCompact"),
988
+ }),
977
989
  op({
978
990
  id: "crm.objects.get",
979
991
  group: "crm",
@@ -1259,6 +1271,18 @@ const rawApiOperations = [
1259
1271
  responseSchema: anyResponse,
1260
1272
  backend: convex("query", "functions/crm/fields:list"),
1261
1273
  }),
1274
+ op({
1275
+ id: "crm.fields.listCompact",
1276
+ group: "crm",
1277
+ summary:
1278
+ "List field names, types and effective constraints without indexing or enrichment internals.",
1279
+ cli: "dench crm fields list --compact",
1280
+ method: "GET",
1281
+ path: "/crm/catalog/objects/{objectName}/fields",
1282
+ requestSchema: anyObject,
1283
+ responseSchema: anyResponse,
1284
+ backend: convex("query", "functions/crm/fields:listCompact"),
1285
+ }),
1262
1286
  op({
1263
1287
  id: "crm.fields.create",
1264
1288
  group: "crm",
@@ -2306,6 +2330,18 @@ const rawApiOperations = [
2306
2330
  responseSchema: anyResponse,
2307
2331
  backend: convex("query", "functions/crm/query:search"),
2308
2332
  }),
2333
+ op({
2334
+ id: "crm.search.page",
2335
+ group: "crm",
2336
+ summary:
2337
+ "Search bounded pages of readable CRM entries, optionally projecting cells. Always follow continueCursor until isDone.",
2338
+ cli: "dench crm search --page",
2339
+ method: "GET",
2340
+ path: "/crm/search/page",
2341
+ requestSchema: anyObject,
2342
+ responseSchema: anyResponse,
2343
+ backend: convex("query", "functions/crm/query:searchPage"),
2344
+ }),
2309
2345
  op({
2310
2346
  id: "crm.statuses.list",
2311
2347
  group: "crm",
@@ -3347,6 +3383,42 @@ const rawApiOperations = [
3347
3383
  responseSchema: anyResponse,
3348
3384
  backend: convex("mutation", "functions/emailCampaigns:deleteIdentity"),
3349
3385
  }),
3386
+ op({
3387
+ id: "email.inbox.search",
3388
+ group: "email",
3389
+ summary:
3390
+ "Search authorized synced mailbox headers with bounded cursor pagination. isDone covers stored records, not provider completeness.",
3391
+ cli: "dench email inbox search",
3392
+ method: "GET",
3393
+ path: "/email/inbox/search",
3394
+ requestSchema: anyObject,
3395
+ responseSchema: anyResponse,
3396
+ backend: convex("query", "functions/emailInbox:search"),
3397
+ }),
3398
+ op({
3399
+ id: "email.inbox.thread",
3400
+ group: "email",
3401
+ summary:
3402
+ "Read a bounded page of synced headers using an exact thread handle from inbox search; bodies remain on demand.",
3403
+ cli: "dench email inbox thread",
3404
+ method: "GET",
3405
+ path: "/email/inbox/threads/{threadHandle}",
3406
+ requestSchema: anyObject,
3407
+ responseSchema: anyResponse,
3408
+ backend: convex("query", "functions/emailInbox:thread"),
3409
+ }),
3410
+ op({
3411
+ id: "email.inbox.message",
3412
+ group: "email",
3413
+ summary:
3414
+ "Read a bounded stored body using an exact message handle from inbox search; rechecks mailbox and record privacy.",
3415
+ cli: "dench email inbox message",
3416
+ method: "GET",
3417
+ path: "/email/inbox/messages/{messageHandle}",
3418
+ requestSchema: anyObject,
3419
+ responseSchema: anyResponse,
3420
+ backend: convex("query", "functions/emailInbox:message"),
3421
+ }),
3350
3422
  op({
3351
3423
  id: "email.drafts.create",
3352
3424
  group: "email",
@@ -3434,7 +3506,7 @@ const rawApiOperations = [
3434
3506
  group: "email",
3435
3507
  summary:
3436
3508
  "Send through a native Gmail or Outlook mailbox with a durable request receipt. Only use for an explicit send request.",
3437
- cli: "dench email send --mailbox",
3509
+ cli: "dench email send [--mailbox <id/email>]",
3438
3510
  method: "POST",
3439
3511
  path: "/email/native/send",
3440
3512
  requestSchema: anyObject,
@@ -3449,7 +3521,7 @@ const rawApiOperations = [
3449
3521
  group: "email",
3450
3522
  summary:
3451
3523
  "Send one raw email (no campaign): to/cc/bcc, custom from-name and reply-to, optional reply threading via replyToMessageId.",
3452
- cli: "dench email send",
3524
+ cli: "dench email send --from <verified-email>",
3453
3525
  method: "POST",
3454
3526
  path: "/email/send",
3455
3527
  requestSchema: anyObject,
@@ -5139,7 +5211,7 @@ const rawApiOperations = [
5139
5211
  id: "agentConfig.tools.edit",
5140
5212
  group: "agent-config",
5141
5213
  summary: "Update tool notes.",
5142
- cli: "dench tools notes-edit",
5214
+ cli: "dench tools edit",
5143
5215
  method: "PUT",
5144
5216
  path: "/agent-config/tools",
5145
5217
  requestSchema: anyObject,
@@ -0,0 +1,75 @@
1
+ import { backendErrorText, publicErrorDetails } from "./public-error";
2
+
3
+ export const NATIVE_EMAIL_WAIT_MS = 10_000;
4
+ const pending = new Set(["pending", "syncing", "synced", "sending"]);
5
+ type Receipt = Record<string, unknown>;
6
+
7
+ /** Observe one durable receipt. Never resubmit a send after an uncertain read. */
8
+ export async function waitForNativeEmail(
9
+ initial: unknown,
10
+ read: (draftId: string, signal: AbortSignal) => Promise<unknown>,
11
+ waitMs = NATIVE_EMAIL_WAIT_MS,
12
+ ): Promise<unknown> {
13
+ if (!Number.isFinite(waitMs) || waitMs < 0 || waitMs > 60_000)
14
+ throw new Error(
15
+ "Email receipt wait must be between 0 and 60000 milliseconds",
16
+ );
17
+ if (waitMs === 0 || !initial || typeof initial !== "object") return initial;
18
+ let receipt = initial as Receipt;
19
+ const draftId = receipt.draftId;
20
+ if (typeof draftId !== "string" || !pending.has(String(receipt.syncStatus)))
21
+ return initial;
22
+ const controller = new AbortController();
23
+ let timer: ReturnType<typeof setTimeout> | undefined;
24
+ const deadline = new Promise<never>((_, reject) => {
25
+ timer = setTimeout(() => {
26
+ controller.abort();
27
+ reject(new Error("Receipt observation timed out"));
28
+ }, waitMs);
29
+ });
30
+ try {
31
+ return await Promise.race([
32
+ (async () => {
33
+ while (!controller.signal.aborted) {
34
+ const next = await read(draftId, controller.signal);
35
+ if (
36
+ !next ||
37
+ typeof next !== "object" ||
38
+ (next as Receipt).draftId !== draftId ||
39
+ typeof (next as Receipt).syncStatus !== "string"
40
+ ) {
41
+ throw new Error("Invalid email receipt response");
42
+ }
43
+ receipt = next as Receipt;
44
+ if (!pending.has(String(receipt.syncStatus))) return receipt;
45
+ await new Promise<void>((resolve) => {
46
+ const pollTimer = setTimeout(done, 500);
47
+ function done() {
48
+ clearTimeout(pollTimer);
49
+ controller.signal.removeEventListener("abort", done);
50
+ resolve();
51
+ }
52
+ controller.signal.addEventListener("abort", done, { once: true });
53
+ });
54
+ }
55
+ return receipt;
56
+ })(),
57
+ deadline,
58
+ ]);
59
+ } catch (error) {
60
+ return {
61
+ ...receipt,
62
+ observation: {
63
+ status: controller.signal.aborted ? "timed_out" : "unavailable",
64
+ ...publicErrorDetails(error),
65
+ message: controller.signal.aborted
66
+ ? "Still awaiting the send result."
67
+ : backendErrorText(error),
68
+ nextAction: `dench email draft show ${draftId} --json`,
69
+ },
70
+ };
71
+ } finally {
72
+ if (timer !== undefined) clearTimeout(timer);
73
+ controller.abort();
74
+ }
75
+ }
@@ -1,6 +1,88 @@
1
1
  const BACKEND_CALL_PREFIX = /\[CONVEX [^\]]*\]\s*/g;
2
2
  const REQUEST_ID_ONLY = /^\[Request ID:\s*([A-Za-z0-9]+)\]\s*Server Error$/i;
3
3
 
4
+ /** Preserve actionable public fields without dumping arbitrary backend data. */
5
+ export function publicErrorDetails(error: unknown): Record<string, unknown> {
6
+ if (!error || typeof error !== "object") return {};
7
+ let outer = error as Record<string, unknown>;
8
+ // Domain wrappers can preserve the original RPC error as a cause. Keep the
9
+ // walk bounded so an accidental cyclic cause never hides the actual failure.
10
+ for (let depth = 0; depth < 3; depth++) {
11
+ if (
12
+ outer.data !== undefined ||
13
+ outer.payload !== undefined ||
14
+ typeof outer.code === "string"
15
+ )
16
+ break;
17
+ if (
18
+ !outer.cause ||
19
+ typeof outer.cause !== "object" ||
20
+ outer.cause === outer
21
+ )
22
+ break;
23
+ outer = outer.cause as Record<string, unknown>;
24
+ }
25
+ const raw = outer.data ?? outer.payload ?? outer;
26
+ let data = raw;
27
+ if (typeof raw === "string") {
28
+ try {
29
+ data = JSON.parse(raw);
30
+ } catch {
31
+ return {};
32
+ }
33
+ }
34
+ if (!data || typeof data !== "object" || Array.isArray(data)) return {};
35
+ const value = data as Record<string, unknown>;
36
+ const result: Record<string, unknown> = {};
37
+ for (const key of [
38
+ "code",
39
+ "requestId",
40
+ "operationId",
41
+ "executionStatus",
42
+ "draftId",
43
+ "receiptId",
44
+ "nextAction",
45
+ ] as const) {
46
+ if (typeof value[key] === "string") result[key] = value[key];
47
+ }
48
+ if (typeof value.retryable === "boolean") result.retryable = value.retryable;
49
+ const neverRetry = new Set(["uncertain_outcome", "unconfirmed"]);
50
+ const noAutomaticRetry = new Set([
51
+ "invalid_arguments",
52
+ "validation_failed",
53
+ "missing_connection",
54
+ "needs_connection",
55
+ "needs_sender",
56
+ "permission_denied",
57
+ "forbidden",
58
+ "login_required",
59
+ "canceled",
60
+ "cancelled",
61
+ "stale_owner",
62
+ "provider_failure",
63
+ "provider_failed",
64
+ "upstream_failed",
65
+ ]);
66
+ if (
67
+ typeof result.code === "string" &&
68
+ (neverRetry.has(result.code) ||
69
+ (result.retryable === undefined && noAutomaticRetry.has(result.code)))
70
+ )
71
+ result.retryable = false;
72
+ if (
73
+ typeof value.retryAfterMs === "number" &&
74
+ Number.isFinite(value.retryAfterMs)
75
+ ) {
76
+ result.retryAfterMs = value.retryAfterMs;
77
+ }
78
+ if (Array.isArray(value.nextActions)) {
79
+ result.nextActions = value.nextActions.filter(
80
+ (item) => typeof item === "string",
81
+ );
82
+ }
83
+ return result;
84
+ }
85
+
4
86
  export function publicErrorMessage(message: string): string {
5
87
  const cleaned = message
6
88
  .replace(BACKEND_CALL_PREFIX, "")