@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.
- package/agent-config.ts +17 -3
- package/app-sdk.d.ts +248 -10
- package/crm.ts +92 -24
- package/dench.ts +128 -19
- package/email-inbox.ts +169 -0
- package/email.ts +103 -21
- package/lib/api-schemas.ts +156 -14
- package/lib/app-sdk-catalog.generated.ts +9 -3
- package/lib/bundled-references.ts +17 -0
- package/lib/cli-args.ts +30 -3
- package/lib/command-args.ts +237 -0
- package/lib/command-help.ts +78 -0
- package/lib/command-registry.ts +75 -3
- package/lib/native-email-wait.ts +75 -0
- package/lib/public-error.ts +82 -0
- package/lib/references.generated.json +66 -0
- package/package.json +3 -1
- package/reference.ts +47 -0
|
@@ -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
|
+
}
|
package/lib/command-registry.ts
CHANGED
|
@@ -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
|
|
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
|
+
}
|
package/lib/public-error.ts
CHANGED
|
@@ -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, "")
|