@dench.com/cli 2.2.6 → 2.2.7

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.
Files changed (2) hide show
  1. package/cron.ts +94 -17
  2. package/package.json +1 -1
package/cron.ts CHANGED
@@ -33,6 +33,7 @@
33
33
  * enable <jobId> Re-arm a paused cron.
34
34
  * disable <jobId> Soft-pause without delete.
35
35
  * run <jobId> [--wait] Manual fire; --wait blocks.
36
+ * secret <jobId> Mint/rotate a signing secret (any type).
36
37
  * runs <jobId> [--limit N] [--status] Run history per trigger.
37
38
  * history [--limit N] Flat history across all crons.
38
39
  * help Print this help.
@@ -196,7 +197,9 @@ export type CronScheduleSpec =
196
197
  *
197
198
  * `--at` implicitly drops `--every` / `--cron` if both are passed.
198
199
  */
199
- export function consumeScheduleFlags(args: string[]): CronScheduleSpec | undefined {
200
+ export function consumeScheduleFlags(
201
+ args: string[],
202
+ ): CronScheduleSpec | undefined {
200
203
  const at = getFlag(args, "--at");
201
204
  if (at) {
202
205
  const ts = new Date(at).getTime();
@@ -335,10 +338,15 @@ async function runGet(ctx: CronCliContext): Promise<void> {
335
338
  }
336
339
 
337
340
  async function runCreate(ctx: CronCliContext): Promise<void> {
338
- const name = shift(ctx.args, "name (positional, e.g. dench cron create \"Daily standup digest\" --prompt … --every 1d)");
341
+ const name = shift(
342
+ ctx.args,
343
+ 'name (positional, e.g. dench cron create "Daily standup digest" --prompt … --every 1d)',
344
+ );
339
345
  const prompt = getFlag(ctx.args, "--prompt");
340
346
  if (!prompt || !prompt.trim()) {
341
- throw new CronCliError("--prompt is required (becomes the user message of each cron-fired chat thread)");
347
+ throw new CronCliError(
348
+ "--prompt is required (becomes the user message of each cron-fired chat thread)",
349
+ );
342
350
  }
343
351
  const description = getFlag(ctx.args, "--description");
344
352
  const overlapPolicy = getFlag(ctx.args, "--overlap") as
@@ -346,10 +354,7 @@ async function runCreate(ctx: CronCliContext): Promise<void> {
346
354
  | "queue"
347
355
  | "kill_old"
348
356
  | undefined;
349
- if (
350
- overlapPolicy &&
351
- !["skip", "queue", "kill_old"].includes(overlapPolicy)
352
- ) {
357
+ if (overlapPolicy && !["skip", "queue", "kill_old"].includes(overlapPolicy)) {
353
358
  throw new CronCliError(
354
359
  `--overlap must be one of skip|queue|kill_old (got "${overlapPolicy}")`,
355
360
  );
@@ -360,26 +365,86 @@ async function runCreate(ctx: CronCliContext): Promise<void> {
360
365
  if (target && target !== "chat_turn") {
361
366
  throw new CronCliError(`--target must be chat_turn (got "${target}")`);
362
367
  }
363
- const schedule = consumeScheduleFlags(ctx.args);
364
- if (!schedule) {
368
+ const triggerType = (getFlag(ctx.args, "--type") ?? "schedule") as
369
+ | "schedule"
370
+ | "webhook";
371
+ if (triggerType !== "schedule" && triggerType !== "webhook") {
365
372
  throw new CronCliError(
366
- "One of --every / --cron / --at is required. Examples:\n" +
367
- " dench cron create \"Hourly health check\" --prompt 'Check uptime…' --every 1h\n" +
368
- ' dench cron create "Weekday digest" --prompt \'Pull yesterday…\' --cron "0 9 * * 1-5"\n' +
369
- ' dench cron create "May 4 launch" --prompt \'Send launch tweet\' --at 2026-05-04T09:00:00Z',
373
+ `--type must be schedule|webhook (got "${triggerType}"). app-event / CRM automations are created from the dashboard.`,
370
374
  );
371
375
  }
372
- const result = await callMutation(ctx, createCronTriggerRef, {
376
+ const isWebhook = triggerType === "webhook";
377
+
378
+ let schedule: CronScheduleSpec | undefined;
379
+ if (isWebhook) {
380
+ // Webhook automations fire on demand / via their signed URL — no schedule.
381
+ if (consumeScheduleFlags(ctx.args)) {
382
+ throw new CronCliError(
383
+ "Webhook automations don't take a schedule. Omit --every/--cron/--at.",
384
+ );
385
+ }
386
+ } else {
387
+ schedule = consumeScheduleFlags(ctx.args);
388
+ if (!schedule) {
389
+ throw new CronCliError(
390
+ "One of --every / --cron / --at is required for schedule automations. Examples:\n" +
391
+ " dench cron create \"Hourly health check\" --prompt 'Check uptime…' --every 1h\n" +
392
+ ' dench cron create "Weekday digest" --prompt \'Pull yesterday…\' --cron "0 9 * * 1-5"\n' +
393
+ " dench cron create \"Inbound lead\" --prompt 'Triage the lead…' --type webhook",
394
+ );
395
+ }
396
+ }
397
+ const created = (await callMutation(ctx, createCronTriggerRef, {
373
398
  name,
374
399
  prompt,
400
+ triggerType,
375
401
  schedule,
376
402
  description,
377
403
  overlapPolicy,
378
404
  enabled: disabled ? false : undefined,
379
405
  deleteAfterRun: deleteAfterRun ? true : undefined,
380
406
  target,
407
+ })) as { jobId?: string };
408
+
409
+ // Webhook automations need a signing secret to be externally callable. Mint
410
+ // one now (unless --no-secret) so `create` hands back a working URL+secret.
411
+ if (isWebhook && !hasFlag(ctx.args, "--no-secret") && created.jobId) {
412
+ const secret = await mintAutomationSecret(ctx, created.jobId);
413
+ out(ctx, { ...created, ...secret });
414
+ return;
415
+ }
416
+ out(ctx, created);
417
+ }
418
+
419
+ /**
420
+ * Mint (or rotate) a signing secret for an automation via the Next REST route
421
+ * (secret encryption runs in the Node route, not Convex's V8 runtime). Returns
422
+ * the plaintext secret + callable URL — shown once.
423
+ */
424
+ async function mintAutomationSecret(
425
+ ctx: CronCliContext,
426
+ jobId: string,
427
+ ): Promise<{ webhookUrl: string | null; webhookSecret: string | null }> {
428
+ const url = `${getApiBase()}/api/cron/jobs/${encodeURIComponent(jobId)}/regenerate-secret`;
429
+ const res = (await postJson(
430
+ url,
431
+ apiKeyHeaders(ctx.sessionToken ?? ""),
432
+ {},
433
+ )) as { webhookUrl?: string | null; webhookSecret?: string | null };
434
+ return {
435
+ webhookUrl: res.webhookUrl ?? null,
436
+ webhookSecret: res.webhookSecret ?? null,
437
+ };
438
+ }
439
+
440
+ async function runSecret(ctx: CronCliContext): Promise<void> {
441
+ const jobId = shift(ctx.args, "job id");
442
+ const secret = await mintAutomationSecret(ctx, jobId);
443
+ out(ctx, {
444
+ jobId,
445
+ ...secret,
446
+ note: "Secret shown once — store it now. Re-running rotates it (invalidates the old one).",
381
447
  });
382
- out(ctx, result);
383
448
  }
384
449
 
385
450
  async function runUpdate(ctx: CronCliContext): Promise<void> {
@@ -458,7 +523,11 @@ async function runRunNow(ctx: CronCliContext): Promise<void> {
458
523
  })) as { entries: Array<{ ts: number; status?: string }> };
459
524
  const matching = runs.entries.find(() => true);
460
525
  if (matching && matching.status) {
461
- out(ctx, { ...fired, finalStatus: matching.status, durationMs: Date.now() - start });
526
+ out(ctx, {
527
+ ...fired,
528
+ finalStatus: matching.status,
529
+ durationMs: Date.now() - start,
530
+ });
462
531
  return;
463
532
  }
464
533
  await new Promise((resolve) => setTimeout(resolve, 3000));
@@ -510,9 +579,10 @@ Browse:
510
579
  dench cron history [--limit 50] [--status ok|error|skipped] [--json]
511
580
  dench cron runs <jobId> [--limit 50] [--status ok|error|skipped] [--json]
512
581
 
513
- Create / update (one of --every / --cron / --at is required for create):
582
+ Create / update:
514
583
  dench cron create "Daily standup digest" \\
515
584
  --prompt "Pull yesterday's commits + Slack #standup msgs and post a summary…" \\
585
+ --type schedule # default; or --type webhook (no schedule)
516
586
  --every 1d # or --cron "0 9 * * 1-5" or --at 2026-05-04T09:00:00Z
517
587
  [--description "..."] # human-readable note shown in dashboard / list
518
588
  [--overlap skip|queue|kill_old] # default: skip
@@ -521,6 +591,10 @@ Create / update (one of --every / --cron / --at is required for create):
521
591
  [--target chat_turn] # default: chat_turn (fires a fresh chat thread)
522
592
  [--json]
523
593
 
594
+ # Webhook automation (fires on demand or via a signed URL). Mints + prints a
595
+ # signing secret unless --no-secret. The secret is shown ONCE.
596
+ dench cron create "Inbound lead triage" --prompt "Triage the lead…" --type webhook
597
+
524
598
  dench cron update <jobId> [--name N] [--prompt P] [--description D] \\
525
599
  [--every X | --cron EXPR | --at ISO] [--enabled true|false] [--overlap …]
526
600
 
@@ -529,6 +603,7 @@ Lifecycle:
529
603
  dench cron disable <jobId>
530
604
  dench cron delete <jobId>
531
605
  dench cron run <jobId> [--wait] [--json] # manual fire; --wait blocks until run finishes
606
+ dench cron secret <jobId> [--json] # mint/rotate a signing secret → callable signed URL (any automation type)
532
607
 
533
608
  Schedule formats:
534
609
  --every <duration> Repeat every N. Accepts 30s, 5m, 2h, 1d, 12h, etc.
@@ -599,6 +674,8 @@ export async function runCronCommand(opts: {
599
674
  return await runEnable(ctx, false);
600
675
  case "run":
601
676
  return await runRunNow(ctx);
677
+ case "secret":
678
+ return await runSecret(ctx);
602
679
  case "runs":
603
680
  return await runRuns(ctx);
604
681
  case "history":
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dench.com/cli",
3
- "version": "2.2.6",
3
+ "version": "2.2.7",
4
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": {