@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/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 Error(record.error);
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) return error;
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(["--convex-url", "--host", "--backend"]);
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
- if (command === "version" || hasFlag("--version")) {
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 (!sub || sub === "help" || sub === "--help") {
4466
- const { runCrmCommand } = await import("./crm");
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
- subArgs[0] !== "draft" &&
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
+ }