@dench.com/cli 3.6.5-staging.384 → 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 +197 -43
- package/dench.ts +135 -22
- 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
package/dench.ts
CHANGED
|
@@ -28,10 +28,12 @@ import {
|
|
|
28
28
|
resolveHostFromArgs,
|
|
29
29
|
STAGING_HOST,
|
|
30
30
|
} from "./host";
|
|
31
|
+
import { CliArgError, isHelpRequest } from "./lib/cli-args";
|
|
31
32
|
import { cliVersion } from "./lib/cli-version";
|
|
33
|
+
import { assertDeclaredCommandFlags } from "./lib/command-args";
|
|
32
34
|
import { denchConfigDirectory } from "./lib/config-path.mjs";
|
|
33
35
|
import { resolveOrganizationBySelector } from "./lib/organizationSelector";
|
|
34
|
-
import { backendErrorText } from "./lib/public-error";
|
|
36
|
+
import { backendErrorText, publicErrorDetails } from "./lib/public-error";
|
|
35
37
|
import { formatApprovalOpenMessage, openUrl } from "./openUrl";
|
|
36
38
|
import {
|
|
37
39
|
explicitSessionKeyInput,
|
|
@@ -215,7 +217,10 @@ const api = {
|
|
|
215
217
|
};
|
|
216
218
|
const CONFIG_DIR = denchConfigDirectory();
|
|
217
219
|
const CONFIG_PATH = join(CONFIG_DIR, "config.json");
|
|
218
|
-
const args = process.argv.slice(2)
|
|
220
|
+
const args = process.argv.slice(2).flatMap((arg) => {
|
|
221
|
+
const split = arg.startsWith("--") ? arg.indexOf("=") : -1;
|
|
222
|
+
return split > 2 ? [arg.slice(0, split), arg.slice(split + 1)] : [arg];
|
|
223
|
+
});
|
|
219
224
|
const json = args.includes("--json");
|
|
220
225
|
const filteredArgs = args.filter((arg) => arg !== "--json");
|
|
221
226
|
const scopedStatus = args.includes("--mine") || args.includes("--self");
|
|
@@ -321,13 +326,18 @@ function throwIfCliError(value: unknown) {
|
|
|
321
326
|
}
|
|
322
327
|
const record = value as JsonRecord;
|
|
323
328
|
if (record.ok === false && typeof record.error === "string") {
|
|
324
|
-
throw new
|
|
329
|
+
throw new CliError(record.error, publicErrorDetails(record));
|
|
325
330
|
}
|
|
326
331
|
}
|
|
327
332
|
|
|
328
333
|
function normalizeCliError(error: unknown): CliError {
|
|
329
|
-
if (error instanceof CliError)
|
|
334
|
+
if (error instanceof CliError) {
|
|
335
|
+
error.payload = { ...error.payload, ...publicErrorDetails(error.payload) };
|
|
336
|
+
return error;
|
|
337
|
+
}
|
|
330
338
|
const message = backendErrorText(error);
|
|
339
|
+
if (error instanceof CliArgError)
|
|
340
|
+
return new CliError(message, { code: error.code, retryable: false });
|
|
331
341
|
if (message.includes("Invalid dev agent key")) {
|
|
332
342
|
return new CliError("Invalid dev agent key", {
|
|
333
343
|
code: "invalid_dev_agent_key",
|
|
@@ -369,7 +379,10 @@ function normalizeCliError(error: unknown): CliError {
|
|
|
369
379
|
],
|
|
370
380
|
});
|
|
371
381
|
}
|
|
372
|
-
return new CliError(message
|
|
382
|
+
return new CliError(message, {
|
|
383
|
+
code: "cli_error",
|
|
384
|
+
...publicErrorDetails(error),
|
|
385
|
+
});
|
|
373
386
|
}
|
|
374
387
|
|
|
375
388
|
function _parseJsonObjectOption(name: string) {
|
|
@@ -673,6 +686,8 @@ Subagents:
|
|
|
673
686
|
Help: dench agent help
|
|
674
687
|
|
|
675
688
|
dench --version
|
|
689
|
+
dench reference <name> [--json] Offline CLI-version-matched skill/reference
|
|
690
|
+
dench reference --list [--json] Bundled reference index and revision
|
|
676
691
|
|
|
677
692
|
Self-updating agent harness:
|
|
678
693
|
dench identity show | edit IDENTITY.md (org-wide)
|
|
@@ -2701,12 +2716,21 @@ function stripRuntimeFlags(args: string[]) {
|
|
|
2701
2716
|
"--staging",
|
|
2702
2717
|
"--use-dev-env",
|
|
2703
2718
|
]);
|
|
2704
|
-
const valueFlags = new Set([
|
|
2719
|
+
const valueFlags = new Set([
|
|
2720
|
+
"--convex-url",
|
|
2721
|
+
"--host",
|
|
2722
|
+
"--backend",
|
|
2723
|
+
"--session-key",
|
|
2724
|
+
"--session",
|
|
2725
|
+
]);
|
|
2705
2726
|
const stripped: string[] = [];
|
|
2706
2727
|
for (let i = 0; i < args.length; i++) {
|
|
2707
2728
|
const arg = args[i];
|
|
2708
2729
|
if (booleanFlags.has(arg)) continue;
|
|
2709
2730
|
if (valueFlags.has(arg)) {
|
|
2731
|
+
if (!args[i + 1] || args[i + 1].startsWith("--")) {
|
|
2732
|
+
throw new CliArgError(`Missing value for ${arg}`);
|
|
2733
|
+
}
|
|
2710
2734
|
i++;
|
|
2711
2735
|
continue;
|
|
2712
2736
|
}
|
|
@@ -3392,7 +3416,7 @@ async function resolveFsDaemonEnv(): Promise<NodeJS.ProcessEnv> {
|
|
|
3392
3416
|
}
|
|
3393
3417
|
|
|
3394
3418
|
async function runFsCommand() {
|
|
3395
|
-
const subArgs = args.slice(args.indexOf("fs") + 1);
|
|
3419
|
+
const subArgs = stripRuntimeFlags(args.slice(args.indexOf("fs") + 1));
|
|
3396
3420
|
const configuredBinary = process.env.DENCH_FS_DAEMON_BIN?.trim();
|
|
3397
3421
|
const binary = configuredBinary || "dench-fs-daemon";
|
|
3398
3422
|
const daemonEnv = await resolveFsDaemonEnv();
|
|
@@ -4303,10 +4327,95 @@ function isCommandNotFound(error: unknown) {
|
|
|
4303
4327
|
}
|
|
4304
4328
|
|
|
4305
4329
|
async function main() {
|
|
4330
|
+
if (isHelpRequest(args)) {
|
|
4331
|
+
const helpArgs = stripRuntimeFlags(args).filter((arg) => arg !== "--json");
|
|
4332
|
+
if (helpArgs[0] === "help") helpArgs.shift();
|
|
4333
|
+
if (!helpArgs.length || ["--help", "-h"].includes(helpArgs[0])) {
|
|
4334
|
+
help();
|
|
4335
|
+
return;
|
|
4336
|
+
}
|
|
4337
|
+
const path = helpArgs.filter(
|
|
4338
|
+
(arg) => !["help", "--help", "-h"].includes(arg),
|
|
4339
|
+
);
|
|
4340
|
+
// Root help retains domain-specific setup, account and pagination guidance.
|
|
4341
|
+
// These are explicit help-only paths; never pass mutation arguments to a handler.
|
|
4342
|
+
if (path.length === 1) {
|
|
4343
|
+
const roots: Record<string, () => void | Promise<void>> = {
|
|
4344
|
+
reference: async () =>
|
|
4345
|
+
console.log((await import("./reference")).REFERENCE_HELP),
|
|
4346
|
+
tool: toolHelp,
|
|
4347
|
+
integrations: integrationsHelp,
|
|
4348
|
+
backend: backendHelp,
|
|
4349
|
+
signin: signinHelp,
|
|
4350
|
+
login: signinHelp,
|
|
4351
|
+
onboard: signinHelp,
|
|
4352
|
+
setup: signinHelp,
|
|
4353
|
+
"what-can-i-do": signinHelp,
|
|
4354
|
+
register: signinHelp,
|
|
4355
|
+
memory: memoryHelp,
|
|
4356
|
+
billing: billingHelp,
|
|
4357
|
+
logout: logoutHelp,
|
|
4358
|
+
context: contextHelp,
|
|
4359
|
+
artifacts: artifactsHelp,
|
|
4360
|
+
"suggested-work": artifactsHelp,
|
|
4361
|
+
lists: listsCliHelp,
|
|
4362
|
+
leads: leadsHelp,
|
|
4363
|
+
companies: companiesHelp,
|
|
4364
|
+
jobs: jobsHelp,
|
|
4365
|
+
status: statusHelp,
|
|
4366
|
+
web: async () => (await import("./web")).webHelp(),
|
|
4367
|
+
crm: async () =>
|
|
4368
|
+
(await import("./crm")).runCrmCommand({
|
|
4369
|
+
convex: {} as ConvexHttpClient,
|
|
4370
|
+
args: ["help"],
|
|
4371
|
+
}),
|
|
4372
|
+
apps: async () =>
|
|
4373
|
+
(await import("./apps")).runAppsCommand({
|
|
4374
|
+
convex: {} as ConvexHttpClient,
|
|
4375
|
+
args: ["help"],
|
|
4376
|
+
}),
|
|
4377
|
+
};
|
|
4378
|
+
if (roots[path[0]]) {
|
|
4379
|
+
await roots[path[0]]();
|
|
4380
|
+
return;
|
|
4381
|
+
}
|
|
4382
|
+
}
|
|
4383
|
+
if (helpArgs[0] === "reference") {
|
|
4384
|
+
console.log((await import("./reference")).REFERENCE_HELP);
|
|
4385
|
+
return;
|
|
4386
|
+
}
|
|
4387
|
+
// Preserve the small, established CRM read help including its cursor contract.
|
|
4388
|
+
if (helpArgs[0] === "crm" && ["query", "aggregate"].includes(helpArgs[1])) {
|
|
4389
|
+
const { runCrmCommand } = await import("./crm");
|
|
4390
|
+
await runCrmCommand({
|
|
4391
|
+
convex: {} as ConvexHttpClient,
|
|
4392
|
+
args: [...helpArgs.slice(1), "--help"],
|
|
4393
|
+
});
|
|
4394
|
+
return;
|
|
4395
|
+
}
|
|
4396
|
+
const { commandHelp } = await import("./lib/command-help");
|
|
4397
|
+
const usage =
|
|
4398
|
+
helpArgs[0] === "email"
|
|
4399
|
+
? (await import("./email")).emailHelpText()
|
|
4400
|
+
: helpArgs[0] === "crm"
|
|
4401
|
+
? (await import("./crm")).crmHelpText()
|
|
4402
|
+
: "";
|
|
4403
|
+
console.log(commandHelp(helpArgs, usage));
|
|
4404
|
+
return;
|
|
4405
|
+
}
|
|
4306
4406
|
const command = positional(0);
|
|
4307
4407
|
const subcommand = positional(1);
|
|
4308
4408
|
|
|
4309
|
-
|
|
4409
|
+
assertDeclaredCommandFlags(stripRuntimeFlags(args));
|
|
4410
|
+
|
|
4411
|
+
if (command === "reference") {
|
|
4412
|
+
await (await import("./reference")).runReferenceCommand(
|
|
4413
|
+
stripRuntimeFlags(args.slice(args.indexOf("reference") + 1)),
|
|
4414
|
+
);
|
|
4415
|
+
return;
|
|
4416
|
+
}
|
|
4417
|
+
|
|
4418
|
+
if (command === "version" || stripRuntimeFlags(args)[0] === "--version") {
|
|
4310
4419
|
console.log(await cliVersion());
|
|
4311
4420
|
return;
|
|
4312
4421
|
}
|
|
@@ -4459,18 +4568,22 @@ async function main() {
|
|
|
4459
4568
|
if (command === "crm") {
|
|
4460
4569
|
const subArgs = stripRuntimeFlags(args.slice(args.indexOf("crm") + 1));
|
|
4461
4570
|
const sub = subArgs[0];
|
|
4571
|
+
const { runCrmCommand, isCrmReadHelpRequest } = await import("./crm");
|
|
4462
4572
|
// Short-circuit help so users don't need a session to read the
|
|
4463
4573
|
// surface. The crm dispatcher's own help resolves --json + the
|
|
4464
4574
|
// subcommand list inline.
|
|
4465
|
-
if (
|
|
4466
|
-
|
|
4575
|
+
if (
|
|
4576
|
+
!sub ||
|
|
4577
|
+
sub === "help" ||
|
|
4578
|
+
sub === "--help" ||
|
|
4579
|
+
isCrmReadHelpRequest(subArgs)
|
|
4580
|
+
) {
|
|
4467
4581
|
// runCrmCommand only needs `convex` for actual queries; help
|
|
4468
4582
|
// doesn't touch it. Pass a minimal stub.
|
|
4469
4583
|
// biome-ignore lint/suspicious/noExplicitAny: harmless help-only stub
|
|
4470
4584
|
await runCrmCommand({ convex: {} as any, args: subArgs });
|
|
4471
4585
|
return;
|
|
4472
4586
|
}
|
|
4473
|
-
const { runCrmCommand } = await import("./crm");
|
|
4474
4587
|
const runtime = await getRuntime();
|
|
4475
4588
|
// CRM is org-scoped — accept either an agent session token
|
|
4476
4589
|
// (`dch_agent_*` from `dench signin`) or a unified Dench API key
|
|
@@ -4737,7 +4850,7 @@ async function main() {
|
|
|
4737
4850
|
}
|
|
4738
4851
|
|
|
4739
4852
|
if (command === "web") {
|
|
4740
|
-
const subArgs = args.slice(args.indexOf("web") + 1);
|
|
4853
|
+
const subArgs = stripRuntimeFlags(args.slice(args.indexOf("web") + 1));
|
|
4741
4854
|
const filteredSubArgs = subArgs.filter((a) => a !== "--json");
|
|
4742
4855
|
const { runWebCommand, webHelp } = await import("./web");
|
|
4743
4856
|
const sub = filteredSubArgs[0];
|
|
@@ -4757,7 +4870,7 @@ async function main() {
|
|
|
4757
4870
|
}
|
|
4758
4871
|
|
|
4759
4872
|
if (command === "search") {
|
|
4760
|
-
const subArgs = args.slice(args.indexOf("search") + 1);
|
|
4873
|
+
const subArgs = stripRuntimeFlags(args.slice(args.indexOf("search") + 1));
|
|
4761
4874
|
const filteredSubArgs = subArgs.filter((a) => a !== "--json");
|
|
4762
4875
|
const { runSearchCommand } = await import("./search");
|
|
4763
4876
|
const sub = filteredSubArgs[0];
|
|
@@ -4780,7 +4893,7 @@ async function main() {
|
|
|
4780
4893
|
}
|
|
4781
4894
|
|
|
4782
4895
|
if (command === "image") {
|
|
4783
|
-
const subArgs = args.slice(args.indexOf("image") + 1);
|
|
4896
|
+
const subArgs = stripRuntimeFlags(args.slice(args.indexOf("image") + 1));
|
|
4784
4897
|
const filteredSubArgs = subArgs.filter((a) => a !== "--json");
|
|
4785
4898
|
const { runImageCommand } = await import("./image");
|
|
4786
4899
|
const sub = filteredSubArgs[0];
|
|
@@ -4800,7 +4913,7 @@ async function main() {
|
|
|
4800
4913
|
}
|
|
4801
4914
|
|
|
4802
4915
|
if (command === "email") {
|
|
4803
|
-
const subArgs = args.slice(args.indexOf("email") + 1);
|
|
4916
|
+
const subArgs = stripRuntimeFlags(args.slice(args.indexOf("email") + 1));
|
|
4804
4917
|
const { runEmailCommand } = await import("./email");
|
|
4805
4918
|
const runtime = await getRuntime();
|
|
4806
4919
|
const authedRuntime = requireAuthenticatedRuntime(runtime, "dench email");
|
|
@@ -4809,7 +4922,7 @@ async function main() {
|
|
|
4809
4922
|
| undefined;
|
|
4810
4923
|
try {
|
|
4811
4924
|
if (
|
|
4812
|
-
|
|
4925
|
+
!["draft", "inbox", "send"].includes(subArgs[0]) &&
|
|
4813
4926
|
!subArgs.some(
|
|
4814
4927
|
(arg) => arg === "--mailbox" || arg.startsWith("--mailbox="),
|
|
4815
4928
|
)
|
|
@@ -4918,7 +5031,7 @@ async function main() {
|
|
|
4918
5031
|
}
|
|
4919
5032
|
|
|
4920
5033
|
if (command === "browser") {
|
|
4921
|
-
const subArgs = args.slice(args.indexOf("browser") + 1);
|
|
5034
|
+
const subArgs = stripRuntimeFlags(args.slice(args.indexOf("browser") + 1));
|
|
4922
5035
|
const { runBrowserCommand } = await import("./browser");
|
|
4923
5036
|
const runtime = await getRuntime();
|
|
4924
5037
|
const authedRuntime = requireAuthenticatedRuntime(runtime, "dench browser");
|
|
@@ -4943,7 +5056,7 @@ async function main() {
|
|
|
4943
5056
|
// `dench ads` is canonical; `dench ad` and `dench adline` are aliases for
|
|
4944
5057
|
// the same command (one code path in ./ads).
|
|
4945
5058
|
if (command === "ads" || command === "ad" || command === "adline") {
|
|
4946
|
-
const subArgs = args.slice(args.indexOf(command) + 1);
|
|
5059
|
+
const subArgs = stripRuntimeFlags(args.slice(args.indexOf(command) + 1));
|
|
4947
5060
|
const { runAdsCommand } = await import("./ads");
|
|
4948
5061
|
// Earning is opt-in: resolve the agent session token best-effort so a
|
|
4949
5062
|
// signed-in developer gets credited on flush, but never require login —
|
|
@@ -4979,7 +5092,7 @@ async function main() {
|
|
|
4979
5092
|
}
|
|
4980
5093
|
|
|
4981
5094
|
if (command === "chat") {
|
|
4982
|
-
const subArgs = args.slice(args.indexOf("chat") + 1);
|
|
5095
|
+
const subArgs = stripRuntimeFlags(args.slice(args.indexOf("chat") + 1));
|
|
4983
5096
|
const sub = subArgs[0];
|
|
4984
5097
|
if (!sub || sub === "help" || sub === "--help") {
|
|
4985
5098
|
const { runChatCommand } = await import("./chat");
|
|
@@ -5029,7 +5142,7 @@ async function main() {
|
|
|
5029
5142
|
}
|
|
5030
5143
|
|
|
5031
5144
|
if (command === "agent") {
|
|
5032
|
-
const subArgs = args.slice(args.indexOf("agent") + 1);
|
|
5145
|
+
const subArgs = stripRuntimeFlags(args.slice(args.indexOf("agent") + 1));
|
|
5033
5146
|
const sub = subArgs[0];
|
|
5034
5147
|
if (!sub || sub === "help" || sub === "--help") {
|
|
5035
5148
|
const { runAgentCommand } = await import("./agent");
|
|
@@ -5106,7 +5219,7 @@ async function main() {
|
|
|
5106
5219
|
// `dench cron`. Same dispatch, same auth, same handler — only the
|
|
5107
5220
|
// top-level command name differs to match the workspace UI rename.
|
|
5108
5221
|
if (command === "cron" || command === "routine" || command === "routines") {
|
|
5109
|
-
const subArgs = args.slice(args.indexOf(command) + 1);
|
|
5222
|
+
const subArgs = stripRuntimeFlags(args.slice(args.indexOf(command) + 1));
|
|
5110
5223
|
const sub = subArgs[0];
|
|
5111
5224
|
// Help is a no-auth path so users can read the surface without a
|
|
5112
5225
|
// session — mirrors `dench crm help`.
|
|
@@ -5155,7 +5268,7 @@ async function main() {
|
|
|
5155
5268
|
"daily",
|
|
5156
5269
|
]);
|
|
5157
5270
|
if (AGENT_CONFIG_COMMANDS.has(command)) {
|
|
5158
|
-
const subArgs = args.slice(args.indexOf(command) + 1);
|
|
5271
|
+
const subArgs = stripRuntimeFlags(args.slice(args.indexOf(command) + 1));
|
|
5159
5272
|
if (
|
|
5160
5273
|
subArgs[0] === "help" ||
|
|
5161
5274
|
subArgs.includes("--help") ||
|
package/email-inbox.ts
ADDED
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
import type { ConvexHttpClient } from "convex/browser";
|
|
2
|
+
import { makeFunctionReference } from "convex/server";
|
|
3
|
+
import { assertNoUnknownFlags, getFlag, hasFlag } from "./lib/cli-args";
|
|
4
|
+
|
|
5
|
+
export const EMAIL_INBOX_HELP = `
|
|
6
|
+
dench email inbox search [--mailbox <id|email>] [--from <email>] [--to <email>] [--subject <text>] [--after <ISO-date|milliseconds>] [--before <ISO-date|milliseconds>] [--direction inbound|outbound] [--limit 1..50] [--cursor <cursor>] [--json]
|
|
7
|
+
dench email inbox thread <thread-handle> [--limit 1..50] [--cursor <cursor>] [--json]
|
|
8
|
+
dench email inbox message <message-handle> [--format text|html] [--max-bytes 1..100000] [--json]
|
|
9
|
+
|
|
10
|
+
Search reads synced headers only; it does not contact the provider or search message bodies.
|
|
11
|
+
--from/--to match exact addresses (to includes cc/bcc); --subject is a case-insensitive substring.
|
|
12
|
+
--after is inclusive, --before exclusive; date-only values mean midnight UTC. Older undated rows use createdAt.
|
|
13
|
+
isDone means the persisted scan ended, not that the provider's entire mailbox has been synced.
|
|
14
|
+
coverage.privacyLimited reports older rows omitted when complete record privacy cannot be established.
|
|
15
|
+
Keep the returned cursor through empty filtered pages. Thread/message handles come from these results.
|
|
16
|
+
`;
|
|
17
|
+
|
|
18
|
+
export type EmailInboxCliContext = {
|
|
19
|
+
convex: ConvexHttpClient;
|
|
20
|
+
args: string[];
|
|
21
|
+
jsonOutput: boolean;
|
|
22
|
+
sessionToken?: string;
|
|
23
|
+
};
|
|
24
|
+
function valueFlag(args: string[], name: string) {
|
|
25
|
+
const present = args.includes(name);
|
|
26
|
+
const value = getFlag(args, name);
|
|
27
|
+
if (present && (!value?.trim() || value.startsWith("--")))
|
|
28
|
+
throw new Error(`${name} requires a value`);
|
|
29
|
+
return value;
|
|
30
|
+
}
|
|
31
|
+
function integerFlag(args: string[], name: string, maximum: number) {
|
|
32
|
+
const raw = valueFlag(args, name);
|
|
33
|
+
if (raw === undefined) return undefined;
|
|
34
|
+
const value = Number(raw);
|
|
35
|
+
if (
|
|
36
|
+
!/^\d+$/.test(raw) ||
|
|
37
|
+
!Number.isInteger(value) ||
|
|
38
|
+
value < 1 ||
|
|
39
|
+
value > maximum
|
|
40
|
+
)
|
|
41
|
+
throw new Error(`${name} must be an integer from 1 to ${maximum}`);
|
|
42
|
+
return value;
|
|
43
|
+
}
|
|
44
|
+
function dateFlag(args: string[], name: string) {
|
|
45
|
+
const raw = valueFlag(args, name);
|
|
46
|
+
if (raw === undefined) return undefined;
|
|
47
|
+
if (/^-?\d+$/.test(raw)) {
|
|
48
|
+
const value = Number(raw);
|
|
49
|
+
if (Number.isSafeInteger(value)) return value;
|
|
50
|
+
}
|
|
51
|
+
const dateOnly = /^\d{4}-\d{2}-\d{2}$/.test(raw);
|
|
52
|
+
if (dateOnly || /^\d{4}-\d{2}-\d{2}T.*(?:Z|[+-]\d{2}:\d{2})$/.test(raw)) {
|
|
53
|
+
const value = Date.parse(dateOnly ? `${raw}T00:00:00.000Z` : raw);
|
|
54
|
+
const day = raw.slice(0, 10);
|
|
55
|
+
const calendar = Date.parse(`${day}T00:00:00.000Z`);
|
|
56
|
+
if (
|
|
57
|
+
Number.isFinite(value) &&
|
|
58
|
+
Number.isFinite(calendar) &&
|
|
59
|
+
new Date(calendar).toISOString().slice(0, 10) === day
|
|
60
|
+
)
|
|
61
|
+
return value;
|
|
62
|
+
}
|
|
63
|
+
throw new Error(
|
|
64
|
+
`${name} must be YYYY-MM-DD, an ISO timestamp with timezone, or Unix milliseconds`,
|
|
65
|
+
);
|
|
66
|
+
}
|
|
67
|
+
export async function runEmailInboxCommand(
|
|
68
|
+
ctx: EmailInboxCliContext,
|
|
69
|
+
): Promise<void> {
|
|
70
|
+
const args = [...ctx.args];
|
|
71
|
+
if (
|
|
72
|
+
!args.length ||
|
|
73
|
+
args.includes("--help") ||
|
|
74
|
+
args.includes("-h") ||
|
|
75
|
+
args[0] === "help"
|
|
76
|
+
) {
|
|
77
|
+
console.log(EMAIL_INBOX_HELP.trim());
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
const command = args.shift();
|
|
81
|
+
const jsonOutput = hasFlag(args, "--json") || ctx.jsonOutput;
|
|
82
|
+
const input: Record<string, unknown> = {};
|
|
83
|
+
const add = (key: string, value: unknown) => {
|
|
84
|
+
if (value !== undefined) input[key] = value;
|
|
85
|
+
};
|
|
86
|
+
if (command === "search") {
|
|
87
|
+
for (const flag of ["mailbox", "from", "to", "subject"])
|
|
88
|
+
add(flag, valueFlag(args, `--${flag}`));
|
|
89
|
+
add("after", dateFlag(args, "--after"));
|
|
90
|
+
add("before", dateFlag(args, "--before"));
|
|
91
|
+
const direction = valueFlag(args, "--direction");
|
|
92
|
+
if (
|
|
93
|
+
direction !== undefined &&
|
|
94
|
+
direction !== "inbound" &&
|
|
95
|
+
direction !== "outbound"
|
|
96
|
+
)
|
|
97
|
+
throw new Error("--direction must be inbound or outbound");
|
|
98
|
+
add("direction", direction);
|
|
99
|
+
if (
|
|
100
|
+
typeof input.after === "number" &&
|
|
101
|
+
typeof input.before === "number" &&
|
|
102
|
+
input.after >= input.before
|
|
103
|
+
)
|
|
104
|
+
throw new Error("--after must be earlier than --before");
|
|
105
|
+
} else if (command === "thread" || command === "message") {
|
|
106
|
+
const handle = args.shift();
|
|
107
|
+
if (
|
|
108
|
+
!handle?.startsWith(`email-${command}:`) ||
|
|
109
|
+
handle.length <= `email-${command}:`.length
|
|
110
|
+
)
|
|
111
|
+
throw new Error(
|
|
112
|
+
`Use an exact ${command} handle returned by email inbox search`,
|
|
113
|
+
);
|
|
114
|
+
input[`${command}Handle`] = handle;
|
|
115
|
+
} else
|
|
116
|
+
throw new Error("Unknown inbox command. Run `dench email inbox --help`.");
|
|
117
|
+
if (command === "message") {
|
|
118
|
+
const format = valueFlag(args, "--format");
|
|
119
|
+
if (format !== undefined && format !== "text" && format !== "html")
|
|
120
|
+
throw new Error("--format must be text or html");
|
|
121
|
+
add("format", format);
|
|
122
|
+
add("maxBytes", integerFlag(args, "--max-bytes", 100_000));
|
|
123
|
+
} else {
|
|
124
|
+
add("limit", integerFlag(args, "--limit", 50));
|
|
125
|
+
add("cursor", valueFlag(args, "--cursor"));
|
|
126
|
+
}
|
|
127
|
+
assertNoUnknownFlags(args, "email inbox");
|
|
128
|
+
if (args.length)
|
|
129
|
+
throw new Error(
|
|
130
|
+
`Unexpected inbox argument: ${args[0]}. Run \`dench email inbox --help\`.`,
|
|
131
|
+
);
|
|
132
|
+
const result = await ctx.convex.query(
|
|
133
|
+
makeFunctionReference<"query">(`functions/emailInbox:${command}`),
|
|
134
|
+
{
|
|
135
|
+
...input,
|
|
136
|
+
...(ctx.sessionToken ? { sessionToken: ctx.sessionToken } : {}),
|
|
137
|
+
},
|
|
138
|
+
);
|
|
139
|
+
if (jsonOutput) {
|
|
140
|
+
console.log(JSON.stringify(result, null, 2));
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
143
|
+
if (command === "message") {
|
|
144
|
+
console.log(
|
|
145
|
+
`${result.header.subject}\nFrom: ${result.header.from.email}\n\n${result.body.content}`,
|
|
146
|
+
);
|
|
147
|
+
if (result.body.truncated)
|
|
148
|
+
console.log(
|
|
149
|
+
`\nBody truncated to the requested byte limit (${result.body.totalBytes} bytes stored).`,
|
|
150
|
+
);
|
|
151
|
+
} else {
|
|
152
|
+
for (const row of result.page)
|
|
153
|
+
console.log(
|
|
154
|
+
`${new Date(row.at).toISOString()} ${row.from.email} ${row.subject}\n ${row.messageHandle} ${row.threadHandle}`,
|
|
155
|
+
);
|
|
156
|
+
if (!result.page.length) console.log("No matching headers on this page.");
|
|
157
|
+
if (!result.isDone)
|
|
158
|
+
console.log(
|
|
159
|
+
`Continue with --cursor ${JSON.stringify(result.continueCursor)}`,
|
|
160
|
+
);
|
|
161
|
+
}
|
|
162
|
+
if (result.coverage.privacyLimited)
|
|
163
|
+
console.log(
|
|
164
|
+
"Some older messages were omitted because their full record privacy scope is unavailable.",
|
|
165
|
+
);
|
|
166
|
+
console.log(
|
|
167
|
+
`Synced mailbox: ${result.coverage.mailboxEmail} (${result.coverage.phase}); provider completeness is not established.`,
|
|
168
|
+
);
|
|
169
|
+
}
|