@dench.com/cli 2.2.7 → 2.3.0

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 +248 -4
  2. package/package.json +1 -1
package/cron.ts CHANGED
@@ -34,10 +34,15 @@
34
34
  * disable <jobId> Soft-pause without delete.
35
35
  * run <jobId> [--wait] Manual fire; --wait blocks.
36
36
  * secret <jobId> Mint/rotate a signing secret (any type).
37
+ * events <toolkit> List a connected app's event types.
37
38
  * runs <jobId> [--limit N] [--status] Run history per trigger.
38
39
  * history [--limit N] Flat history across all crons.
39
40
  * help Print this help.
40
41
  *
42
+ * Trigger types accepted by `create --type`:
43
+ * schedule (default) | webhook | composio (app event) | crm (CRM event).
44
+ * composio/crm POST to /api/cron/jobs; the others use the Convex mutation.
45
+ *
41
46
  * Schedule flags accepted by `create` and `update`:
42
47
  * --every <duration> 30s | 5m | 2h | 1d | 12h…
43
48
  * --every-ms <number> Same, raw milliseconds.
@@ -139,6 +144,28 @@ async function postJson(
139
144
  return json;
140
145
  }
141
146
 
147
+ async function getJson(
148
+ url: string,
149
+ headers: Record<string, string>,
150
+ ): Promise<unknown> {
151
+ const response = await fetch(url, { method: "GET", headers });
152
+ const text = await response.text();
153
+ let json: unknown;
154
+ try {
155
+ json = text ? JSON.parse(text) : null;
156
+ } catch {
157
+ json = { raw: text };
158
+ }
159
+ if (!response.ok) {
160
+ const detail =
161
+ json && typeof json === "object" && "error" in json
162
+ ? String((json as Record<string, unknown>).error)
163
+ : JSON.stringify(json);
164
+ throw new CronCliError(`${url} failed (${response.status}): ${detail}`);
165
+ }
166
+ return json;
167
+ }
168
+
142
169
  async function callQuery(
143
170
  ctx: CronCliContext,
144
171
  fn: Parameters<ConvexHttpClient["query"]>[0],
@@ -367,12 +394,32 @@ async function runCreate(ctx: CronCliContext): Promise<void> {
367
394
  }
368
395
  const triggerType = (getFlag(ctx.args, "--type") ?? "schedule") as
369
396
  | "schedule"
370
- | "webhook";
371
- if (triggerType !== "schedule" && triggerType !== "webhook") {
397
+ | "webhook"
398
+ | "composio"
399
+ | "crm";
400
+ if (!["schedule", "webhook", "composio", "crm"].includes(triggerType)) {
372
401
  throw new CronCliError(
373
- `--type must be schedule|webhook (got "${triggerType}"). app-event / CRM automations are created from the dashboard.`,
402
+ `--type must be schedule|webhook|composio|crm (got "${triggerType}").`,
374
403
  );
375
404
  }
405
+
406
+ // App-event (composio) and CRM-event automations are created through the
407
+ // REST `/api/cron/jobs` POST: composio needs the upstream subscribe +
408
+ // connection-ownership flow (Node runtime, project Composio key), and CRM
409
+ // rides the same path for one create surface. schedule + webhook keep the
410
+ // direct Convex mutation below.
411
+ if (triggerType === "composio" || triggerType === "crm") {
412
+ return await runCreateAppOrCrm(ctx, {
413
+ triggerType,
414
+ name,
415
+ prompt,
416
+ description,
417
+ overlapPolicy,
418
+ disabled,
419
+ deleteAfterRun,
420
+ target,
421
+ });
422
+ }
376
423
  const isWebhook = triggerType === "webhook";
377
424
 
378
425
  let schedule: CronScheduleSpec | undefined;
@@ -447,6 +494,185 @@ async function runSecret(ctx: CronCliContext): Promise<void> {
447
494
  });
448
495
  }
449
496
 
497
+ export type AppOrCrmCreateBase = {
498
+ triggerType: "composio" | "crm";
499
+ name: string;
500
+ prompt: string;
501
+ description?: string;
502
+ overlapPolicy?: "skip" | "queue" | "kill_old";
503
+ disabled: boolean;
504
+ deleteAfterRun: boolean;
505
+ target?: "chat_turn";
506
+ };
507
+
508
+ /**
509
+ * Build the `/api/cron/jobs` POST body for an app-event (composio) or
510
+ * CRM-event automation from the create flags. Pure (no network) so the flag
511
+ * parsing + validation + payload shape can be unit-tested directly. Consumes
512
+ * the type-specific flags off `args` (composio: --connection/--event/--toolkit/
513
+ * --trigger-config; crm: --object/--event/--where/--where-logic) and throws a
514
+ * `CronCliError` with discovery hints when a required input is missing.
515
+ */
516
+ export function buildAppOrCrmCreateBody(
517
+ args: string[],
518
+ base: AppOrCrmCreateBase,
519
+ ): Record<string, unknown> {
520
+ // Neither type takes a schedule — reject one early instead of silently
521
+ // dropping it server-side.
522
+ if (consumeScheduleFlags(args)) {
523
+ throw new CronCliError(
524
+ `${base.triggerType} automations don't take a schedule. Omit --every/--cron/--at.`,
525
+ );
526
+ }
527
+
528
+ const body: Record<string, unknown> = {
529
+ name: base.name,
530
+ prompt: base.prompt,
531
+ triggerType: base.triggerType,
532
+ description: base.description,
533
+ overlapPolicy: base.overlapPolicy,
534
+ enabled: base.disabled ? false : undefined,
535
+ deleteAfterRun: base.deleteAfterRun ? true : undefined,
536
+ target: base.target,
537
+ };
538
+
539
+ if (base.triggerType === "composio") {
540
+ const connection = getFlag(args, "--connection");
541
+ const event = getFlag(args, "--event");
542
+ if (!connection?.trim() || !event?.trim()) {
543
+ throw new CronCliError(
544
+ "app-event automations need --connection <connectedAccountId> and --event <triggerSlug>.\n" +
545
+ " Discover apps: dench apps --json (use connectedAccountId + toolkit)\n" +
546
+ " Discover events: dench cron events <toolkit> (use the event slug)\n" +
547
+ "Example:\n" +
548
+ ' dench cron create "New GitHub star" --prompt "Thank the new stargazer" \\\n' +
549
+ " --type composio --connection ca_123 --event GITHUB_STAR_ADDED_EVENT --toolkit github",
550
+ );
551
+ }
552
+ const toolkit = getFlag(args, "--toolkit");
553
+ const triggerConfigRaw = getFlag(args, "--trigger-config");
554
+ let triggerConfig: unknown;
555
+ if (triggerConfigRaw) {
556
+ try {
557
+ triggerConfig = JSON.parse(triggerConfigRaw);
558
+ } catch {
559
+ throw new CronCliError(
560
+ `--trigger-config must be valid JSON (got ${triggerConfigRaw})`,
561
+ );
562
+ }
563
+ }
564
+ body.composioConnectionId = connection.trim();
565
+ body.composioTriggerSlug = event.trim();
566
+ if (toolkit?.trim()) body.composioToolkitSlug = toolkit.trim();
567
+ if (triggerConfig !== undefined) body.triggerConfig = triggerConfig;
568
+ } else {
569
+ const object = getFlag(args, "--object");
570
+ const event = getFlag(args, "--event");
571
+ if (!object?.trim() || !event?.trim()) {
572
+ throw new CronCliError(
573
+ "CRM-event automations need --object <objectId> and --event create|update|delete.\n" +
574
+ " Discover objects: dench crm objects list\n" +
575
+ " Discover fields: dench crm fields list <object> (filter on field _id via --where)\n" +
576
+ "Example:\n" +
577
+ ' dench cron create "New company" --prompt "Research the new company" \\\n' +
578
+ " --type crm --object <objectId> --event create",
579
+ );
580
+ }
581
+ if (!["create", "update", "delete"].includes(event)) {
582
+ throw new CronCliError(
583
+ `--event must be create|update|delete for CRM automations (got "${event}")`,
584
+ );
585
+ }
586
+ const whereRaw = getFlag(args, "--where");
587
+ const whereLogic = (getFlag(args, "--where-logic") ?? "AND") as
588
+ | "AND"
589
+ | "OR";
590
+ if (!["AND", "OR"].includes(whereLogic)) {
591
+ throw new CronCliError(
592
+ `--where-logic must be AND|OR (got "${whereLogic}")`,
593
+ );
594
+ }
595
+ const crmTriggerConfig: Record<string, unknown> = {
596
+ eventType: event,
597
+ objectId: object.trim(),
598
+ };
599
+ if (whereRaw) {
600
+ let whereGroups: unknown;
601
+ try {
602
+ whereGroups = JSON.parse(whereRaw);
603
+ } catch {
604
+ throw new CronCliError(
605
+ "--where must be valid JSON: an array of groups like " +
606
+ '\'[{"logic":"AND","conditions":[{"fieldId":"<id>","operator":"equals","value":"x"}]}]\'',
607
+ );
608
+ }
609
+ if (!Array.isArray(whereGroups)) {
610
+ throw new CronCliError(
611
+ "--where must be a JSON array of where-groups (each { logic, conditions }).",
612
+ );
613
+ }
614
+ crmTriggerConfig.whereGroups = whereGroups;
615
+ crmTriggerConfig.whereLogic = whereLogic;
616
+ }
617
+ body.crmTriggerConfig = crmTriggerConfig;
618
+ }
619
+
620
+ return body;
621
+ }
622
+
623
+ /**
624
+ * Create an app-event (composio) or CRM-event automation via the REST
625
+ * `/api/cron/jobs` POST. composio creation also subscribes the trigger
626
+ * instance upstream + verifies the org owns the connection (handled
627
+ * server-side in the Node route); CRM creation validates the object +
628
+ * filter field ids in the mutation.
629
+ */
630
+ async function runCreateAppOrCrm(
631
+ ctx: CronCliContext,
632
+ base: AppOrCrmCreateBase,
633
+ ): Promise<void> {
634
+ const body = buildAppOrCrmCreateBody(ctx.args, base);
635
+ const url = `${getApiBase()}/api/cron/jobs`;
636
+ const created = await postJson(
637
+ url,
638
+ apiKeyHeaders(ctx.sessionToken ?? ""),
639
+ body,
640
+ );
641
+ out(ctx, created);
642
+ }
643
+
644
+ /**
645
+ * List the app-event types a connected app publishes, so the caller can pick
646
+ * a `--event <triggerSlug>` for `dench cron create --type composio`. Backed by
647
+ * `GET /api/cron/trigger-types` (project-level Composio catalogue).
648
+ */
649
+ async function runEvents(ctx: CronCliContext): Promise<void> {
650
+ const toolkit = shift(
651
+ ctx.args,
652
+ "toolkit slug (e.g. github — run `dench apps --json` to see each app's slug)",
653
+ );
654
+ const url = `${getApiBase()}/api/cron/trigger-types?toolkit=${encodeURIComponent(toolkit)}`;
655
+ const result = (await getJson(
656
+ url,
657
+ apiKeyHeaders(ctx.sessionToken ?? ""),
658
+ )) as {
659
+ items?: Array<{ slug?: string; name?: string; description?: string }>;
660
+ };
661
+ if (ctx.jsonOutput) {
662
+ out(ctx, result);
663
+ return;
664
+ }
665
+ const items = result.items ?? [];
666
+ if (items.length === 0) {
667
+ console.log(`(no app events found for "${toolkit}")`);
668
+ return;
669
+ }
670
+ for (const item of items) {
671
+ console.log(`${item.slug ?? "?"} — ${item.name ?? ""}`);
672
+ if (item.description) console.log(` ${item.description}`);
673
+ }
674
+ }
675
+
450
676
  async function runUpdate(ctx: CronCliContext): Promise<void> {
451
677
  const jobId = shift(ctx.args, "job id");
452
678
  const name = getFlag(ctx.args, "--name");
@@ -578,11 +804,12 @@ Browse:
578
804
  dench cron get <jobId> [--json]
579
805
  dench cron history [--limit 50] [--status ok|error|skipped] [--json]
580
806
  dench cron runs <jobId> [--limit 50] [--status ok|error|skipped] [--json]
807
+ dench cron events <toolkit> [--json] # app-event types for a connected app
581
808
 
582
809
  Create / update:
583
810
  dench cron create "Daily standup digest" \\
584
811
  --prompt "Pull yesterday's commits + Slack #standup msgs and post a summary…" \\
585
- --type schedule # default; or --type webhook (no schedule)
812
+ --type schedule # default. also: webhook | composio | crm (see below)
586
813
  --every 1d # or --cron "0 9 * * 1-5" or --at 2026-05-04T09:00:00Z
587
814
  [--description "..."] # human-readable note shown in dashboard / list
588
815
  [--overlap skip|queue|kill_old] # default: skip
@@ -595,6 +822,21 @@ Create / update:
595
822
  # signing secret unless --no-secret. The secret is shown ONCE.
596
823
  dench cron create "Inbound lead triage" --prompt "Triage the lead…" --type webhook
597
824
 
825
+ # App-event automation (fires when something happens in a connected app).
826
+ # 1. dench apps --json → connectedAccountId + toolkit
827
+ # 2. dench cron events <toolkit> → event slug
828
+ dench cron create "New GitHub star" --prompt "Thank the new stargazer" \\
829
+ --type composio --connection <connectedAccountId> --event <triggerSlug> \\
830
+ [--toolkit <slug>] [--trigger-config '<json>']
831
+
832
+ # CRM-event automation (fires on a CRM object create/update/delete).
833
+ # 1. dench crm objects list → objectId
834
+ # 2. dench crm fields list <object> → field _id (for --where filters)
835
+ dench cron create "New company added" --prompt "Research the new company" \\
836
+ --type crm --object <objectId> --event create|update|delete \\
837
+ [--where '[{"logic":"AND","conditions":[{"fieldId":"<id>","operator":"equals","value":"x"}]}]'] \\
838
+ [--where-logic AND|OR]
839
+
598
840
  dench cron update <jobId> [--name N] [--prompt P] [--description D] \\
599
841
  [--every X | --cron EXPR | --at ISO] [--enabled true|false] [--overlap …]
600
842
 
@@ -676,6 +918,8 @@ export async function runCronCommand(opts: {
676
918
  return await runRunNow(ctx);
677
919
  case "secret":
678
920
  return await runSecret(ctx);
921
+ case "events":
922
+ return await runEvents(ctx);
679
923
  case "runs":
680
924
  return await runRuns(ctx);
681
925
  case "history":
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dench.com/cli",
3
- "version": "2.2.7",
3
+ "version": "2.3.0",
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": {