@dench.com/cli 3.6.6-staging.386 → 3.8.0-staging.389

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.
@@ -0,0 +1,86 @@
1
+ import type { ConvexHttpClient } from "convex/browser";
2
+ import { makeFunctionReference } from "convex/server";
3
+ import { CliArgError } from "./lib/cli-args";
4
+ import { parseEnvironmentOptions } from "./lib/environment-cli-args";
5
+
6
+ export const CONTEXT_SECTIONS = [
7
+ "identity",
8
+ "organisation",
9
+ "user",
10
+ "tools",
11
+ "rules",
12
+ ] as const;
13
+ export function validateContextSet(input: readonly string[]) {
14
+ const parsed = parseEnvironmentOptions(
15
+ input,
16
+ ["--content"],
17
+ ["--stdin", "--json"],
18
+ );
19
+ const [verb, section] = parsed.positional;
20
+ if (
21
+ verb !== "set" ||
22
+ parsed.positional.length !== 2 ||
23
+ !CONTEXT_SECTIONS.includes(section as never) ||
24
+ parsed.child.length
25
+ )
26
+ throw new CliArgError(
27
+ `Usage: dench context set <${CONTEXT_SECTIONS.join("|")}> --stdin | --content <text> [--json]`,
28
+ );
29
+ if (
30
+ Number(parsed.flags["--stdin"] === true) +
31
+ Number(typeof parsed.flags["--content"] === "string") !==
32
+ 1
33
+ )
34
+ throw new CliArgError(
35
+ "Choose exactly one of --stdin or --content. Store credentials with dench secrets set, never in context.",
36
+ );
37
+ return {
38
+ section: section as (typeof CONTEXT_SECTIONS)[number],
39
+ content: parsed.flags["--content"] as string | undefined,
40
+ };
41
+ }
42
+
43
+ export async function runContextSet(opts: {
44
+ convex: ConvexHttpClient;
45
+ args: string[];
46
+ sessionToken?: string;
47
+ readStdin?: () => Promise<string>;
48
+ }) {
49
+ const parsed = validateContextSet(opts.args);
50
+ const content =
51
+ parsed.content ??
52
+ (await (
53
+ opts.readStdin ??
54
+ (async () => {
55
+ const chunks: Buffer[] = [];
56
+ let size = 0;
57
+ for await (const chunk of process.stdin) {
58
+ size += Buffer.byteLength(chunk);
59
+ if (size > 32000)
60
+ throw new CliArgError(
61
+ "Context exceeds 32 KB. Keep standing instructions short; use workspace files for references.",
62
+ );
63
+ chunks.push(Buffer.from(chunk));
64
+ }
65
+ return Buffer.concat(chunks).toString("utf8");
66
+ })
67
+ )());
68
+ if (!content.trim() || Buffer.byteLength(content) > 32000)
69
+ throw new CliArgError("Context must contain 1–32000 UTF-8 bytes.");
70
+ const fn = {
71
+ identity: "updateIdentity",
72
+ organisation: "updateOrganisation",
73
+ user: "updateUserProfile",
74
+ tools: "updateToolsNotes",
75
+ rules: "writeMagicFileFromUI",
76
+ }[parsed.section];
77
+ await opts.convex.mutation(
78
+ makeFunctionReference<"mutation">(`functions/agentConfig:${fn}`),
79
+ {
80
+ ...(opts.sessionToken ? { sessionToken: opts.sessionToken } : {}),
81
+ content,
82
+ ...(parsed.section === "rules" ? { path: "/RULES.md" } : {}),
83
+ },
84
+ );
85
+ console.log(JSON.stringify({ ok: true, section: parsed.section }));
86
+ }
package/crm.ts CHANGED
@@ -239,6 +239,7 @@ function normalizeRpcError(error: unknown): Error {
239
239
  userFacingConvexErrorMessage(error) ??
240
240
  (error instanceof Error ? error.message : String(error)),
241
241
  ),
242
+ { cause: error },
242
243
  );
243
244
  }
244
245
 
@@ -246,7 +247,8 @@ export function shift(args: string[], expected: string): string {
246
247
  try {
247
248
  return shiftRaw(args, expected);
248
249
  } catch (error) {
249
- if (error instanceof CliArgError) throw new CrmCliError(error.message);
250
+ if (error instanceof CliArgError)
251
+ throw new CrmCliError(error.message, { cause: error });
250
252
  throw error;
251
253
  }
252
254
  }
@@ -290,7 +292,8 @@ function resolveLimitFlag(
290
292
  try {
291
293
  parsed = parseLimitRaw(raw, opts);
292
294
  } catch (error) {
293
- if (error instanceof CliArgError) throw new CrmCliError(error.message);
295
+ if (error instanceof CliArgError)
296
+ throw new CrmCliError(error.message, { cause: error });
294
297
  throw error;
295
298
  }
296
299
  if (parsed.clamped) {
@@ -365,7 +368,8 @@ function parseJson(value: string | undefined): unknown {
365
368
  try {
366
369
  return parseJsonRaw(value);
367
370
  } catch (error) {
368
- if (error instanceof CliArgError) throw new CrmCliError(error.message);
371
+ if (error instanceof CliArgError)
372
+ throw new CrmCliError(error.message, { cause: error });
369
373
  throw error;
370
374
  }
371
375
  }
@@ -377,7 +381,8 @@ export function assertNoUnknownFlags(
377
381
  try {
378
382
  assertNoUnknownFlagsRaw(args, command);
379
383
  } catch (error) {
380
- if (error instanceof CliArgError) throw new CrmCliError(error.message);
384
+ if (error instanceof CliArgError)
385
+ throw new CrmCliError(error.message, { cause: error });
381
386
  throw error;
382
387
  }
383
388
  }
@@ -780,11 +785,23 @@ export async function callQuery(
780
785
  }
781
786
  }
782
787
 
788
+ /** Every parsed token must be accounted for before an external effect. */
789
+ function assertWriteArguments(ctx: CrmCliContext): void {
790
+ assertNoUnknownFlags(ctx.args, "crm");
791
+ if (ctx.args.length) {
792
+ const error = new CliArgError(
793
+ `Unexpected argument: ${ctx.args[0]}. Run the command with --help for usage.`,
794
+ );
795
+ throw new CrmCliError(error.message, { cause: error });
796
+ }
797
+ }
798
+
783
799
  export async function callMutation(
784
800
  ctx: CrmCliContext,
785
801
  fn: Parameters<ConvexHttpClient["mutation"]>[0],
786
802
  args: Record<string, unknown> = {},
787
803
  ): Promise<unknown> {
804
+ assertWriteArguments(ctx);
788
805
  try {
789
806
  return await ctx.convex.mutation(fn, {
790
807
  ...args,
@@ -800,6 +817,7 @@ export async function callAction(
800
817
  fn: Parameters<ConvexHttpClient["action"]>[0],
801
818
  args: Record<string, unknown> = {},
802
819
  ): Promise<unknown> {
820
+ assertWriteArguments(ctx);
803
821
  try {
804
822
  return await ctx.convex.action(fn, {
805
823
  ...args,
@@ -914,6 +932,9 @@ const api = {
914
932
  },
915
933
  objects: {
916
934
  list: makeFunctionReference<"query">("functions/crm/objects:list"),
935
+ listCompact: makeFunctionReference<"query">(
936
+ "functions/crm/objects:listCompact",
937
+ ),
917
938
  get: makeFunctionReference<"query">("functions/crm/objects:get"),
918
939
  materializeView: makeFunctionReference<"action">(
919
940
  "functions/crm/objects:completeView",
@@ -963,6 +984,9 @@ const api = {
963
984
  },
964
985
  fields: {
965
986
  list: makeFunctionReference<"query">("functions/crm/fields:list"),
987
+ listCompact: makeFunctionReference<"query">(
988
+ "functions/crm/fields:listCompact",
989
+ ),
966
990
  create: makeFunctionReference<"mutation">("functions/crm/fields:create"),
967
991
  update: makeFunctionReference<"mutation">("functions/crm/fields:update"),
968
992
  remove: makeFunctionReference<"mutation">("functions/crm/fields:remove"),
@@ -1013,6 +1037,9 @@ const api = {
1013
1037
  ),
1014
1038
  aggregate: makeFunctionReference<"query">("functions/crm/query:aggregate"),
1015
1039
  search: makeFunctionReference<"query">("functions/crm/query:search"),
1040
+ searchPage: makeFunctionReference<"query">(
1041
+ "functions/crm/query:searchPage",
1042
+ ),
1016
1043
  },
1017
1044
  statuses: {
1018
1045
  list: makeFunctionReference<"query">("functions/crm/statuses:list"),
@@ -1459,8 +1486,16 @@ async function runObjectsCommand(ctx: CrmCliContext): Promise<void> {
1459
1486
  const verb = ctx.args.shift();
1460
1487
  switch (verb) {
1461
1488
  case "list": {
1489
+ const compact = hasFlag(ctx.args, "--compact");
1462
1490
  assertNoUnknownFlags(ctx.args, "crm objects list");
1463
- out(ctx, await callQuery(ctx, api.objects.list, {}));
1491
+ out(
1492
+ ctx,
1493
+ await callQuery(
1494
+ ctx,
1495
+ compact ? api.objects.listCompact : api.objects.list,
1496
+ {},
1497
+ ),
1498
+ );
1464
1499
  return;
1465
1500
  }
1466
1501
  case "get": {
@@ -2159,8 +2194,16 @@ async function runFieldsCommand(ctx: CrmCliContext): Promise<void> {
2159
2194
  switch (verb) {
2160
2195
  case "list": {
2161
2196
  const objectName = shift(ctx.args, "object name");
2197
+ const compact = hasFlag(ctx.args, "--compact");
2162
2198
  assertNoUnknownFlags(ctx.args, "crm fields list");
2163
- out(ctx, await callQuery(ctx, api.fields.list, { objectName }));
2199
+ out(
2200
+ ctx,
2201
+ await callQuery(
2202
+ ctx,
2203
+ compact ? api.fields.listCompact : api.fields.list,
2204
+ { objectName },
2205
+ ),
2206
+ );
2164
2207
  return;
2165
2208
  }
2166
2209
  case "create": {
@@ -2294,7 +2337,7 @@ async function runFieldsCommand(ctx: CrmCliContext): Promise<void> {
2294
2337
  // Assert before consuming the rest as positional field names so a
2295
2338
  // typo'd flag fails loudly rather than getting silently filtered.
2296
2339
  assertNoUnknownFlags(ctx.args, "crm fields reorder");
2297
- const orderedFieldNames = [...ctx.args];
2340
+ const orderedFieldNames = ctx.args.splice(0);
2298
2341
  out(
2299
2342
  ctx,
2300
2343
  await callMutation(ctx, api.fields.reorder, {
@@ -2311,7 +2354,7 @@ async function runFieldsCommand(ctx: CrmCliContext): Promise<void> {
2311
2354
  // there are no stray flags so a typo'd `--limit` fails loud
2312
2355
  // instead of being silently consumed as an enum value.
2313
2356
  assertNoUnknownFlags(ctx.args, "crm fields enum-reorder");
2314
- const orderedValues = [...ctx.args];
2357
+ const orderedValues = ctx.args.splice(0);
2315
2358
  if (orderedValues.length === 0) {
2316
2359
  throw new CrmCliError(
2317
2360
  "dench crm fields enum-reorder requires at least one value. " +
@@ -2417,6 +2460,15 @@ async function runEntriesCommand(ctx: CrmCliContext): Promise<void> {
2417
2460
  const objectName = shift(ctx.args, "object name");
2418
2461
  const withMeta = hasFlag(ctx.args, "--with-meta");
2419
2462
  const cursorSpecified = ctx.args.includes("--cursor");
2463
+ const cursorValue = ctx.args[ctx.args.indexOf("--cursor") + 1];
2464
+ if (
2465
+ cursorSpecified &&
2466
+ (!cursorValue?.trim() || cursorValue.startsWith("--"))
2467
+ )
2468
+ throw new CrmCliError(
2469
+ "--cursor requires a non-empty continuation cursor.",
2470
+ { cause: new CliArgError("Invalid --cursor") },
2471
+ );
2420
2472
  const cursor = getFlag(ctx.args, "--cursor");
2421
2473
  if (cursorSpecified && (!cursor?.trim() || cursor.startsWith("--")))
2422
2474
  throw new CrmCliError(
@@ -2483,8 +2535,7 @@ async function runEntriesCommand(ctx: CrmCliContext): Promise<void> {
2483
2535
  { entryCount?: number; visibility?: string } | null,
2484
2536
  ];
2485
2537
  const entries = Array.isArray(result?.page) ? result.page : [];
2486
- const isDone =
2487
- typeof result?.isDone === "boolean" ? result.isDone : null;
2538
+ const isDone = typeof result?.isDone === "boolean" ? result.isDone : null;
2488
2539
  const truncated = result?.truncated === true;
2489
2540
  const continueCursor =
2490
2541
  isDone === false ? result?.continueCursor || null : null;
@@ -3234,25 +3285,31 @@ async function runAggregateCommand(ctx: CrmCliContext): Promise<void> {
3234
3285
 
3235
3286
  async function runSearchCommand(ctx: CrmCliContext): Promise<void> {
3236
3287
  const objectName = getFlag(ctx.args, "--object");
3237
- // Search caps at 100 server-side. Warn on stderr when clamped (the
3238
- // output is a bare JSON array, so stdout must stay untouched).
3288
+ const selectRaw = getFlag(ctx.args, "--select");
3289
+ const select = selectRaw?.split(",").map((name) => name.trim());
3290
+ if (select && (select.length > 30 || select.some((name) => !name))) {
3291
+ throw new CrmCliError(
3292
+ "--select requires between 1 and 30 nonempty field names.",
3293
+ );
3294
+ }
3295
+ const page = hasFlag(ctx.args, "--page");
3296
+ const cursor = getFlag(ctx.args, "--cursor");
3297
+ const projected = select !== undefined || page || cursor !== undefined;
3239
3298
  const limit = resolveLimitFlag(ctx.args, {
3240
- defaultValue: 50,
3299
+ defaultValue: projected ? 20 : 50,
3241
3300
  cap: 100,
3242
3301
  command: "crm search",
3243
3302
  });
3244
- // Drain known flags before joining the rest into the search text so a
3245
- // typo like `--objct` surfaces instead of being concatenated into the
3246
- // query string.
3247
3303
  assertNoUnknownFlags(ctx.args, "crm search");
3248
3304
  const text = ctx.args.join(" ").trim();
3249
3305
  if (!text) throw new CrmCliError("Search text required");
3250
3306
  out(
3251
3307
  ctx,
3252
- await callQuery(ctx, api.query.search, {
3308
+ await callQuery(ctx, projected ? api.query.searchPage : api.query.search, {
3253
3309
  text,
3254
3310
  objectName,
3255
3311
  limit: limit.effective,
3312
+ ...(projected ? { select, cursor } : {}),
3256
3313
  }),
3257
3314
  );
3258
3315
  }
@@ -3382,7 +3439,8 @@ async function runActionsCommand(ctx: CrmCliContext): Promise<void> {
3382
3439
  const objectName = shift(ctx.args, "object name");
3383
3440
  const fieldId = shift(ctx.args, "field id");
3384
3441
  const actionId = shift(ctx.args, "action id");
3385
- const entryIds = ctx.args.filter((a) => !a.startsWith("--"));
3442
+ assertNoUnknownFlags(ctx.args, "crm actions run");
3443
+ const entryIds = ctx.args.splice(0);
3386
3444
  if (entryIds.length === 0) {
3387
3445
  throw new CrmCliError("Provide at least one entry id");
3388
3446
  }
@@ -3922,7 +3980,7 @@ export function remapKeys(
3922
3980
  // ── SQL escape hatch ─────────────────────────────────────────────────────
3923
3981
 
3924
3982
  async function runSqlCommand(ctx: CrmCliContext): Promise<void> {
3925
- const sql = ctx.args.join(" ").trim();
3983
+ const sql = ctx.args.splice(0).join(" ").trim();
3926
3984
  if (!sql) {
3927
3985
  throw new CrmCliError("dench crm sql '<SELECT ... FROM ...>'");
3928
3986
  }
@@ -4411,6 +4469,7 @@ async function enrichCellThroughGateway(
4411
4469
  ctx: CrmCliContext,
4412
4470
  args: CellEnrichmentArgs,
4413
4471
  ): Promise<Record<string, unknown>> {
4472
+ assertWriteArguments(ctx);
4414
4473
  const target = await resolveEnrichmentTarget(ctx, args);
4415
4474
  const entry = asEntry(
4416
4475
  await callQuery(ctx, api.entries.get, {
@@ -4434,6 +4493,7 @@ async function enrichObjectThroughGateway(
4434
4493
  ctx: CrmCliContext,
4435
4494
  args: ObjectEnrichmentArgs,
4436
4495
  ): Promise<Record<string, unknown>> {
4496
+ assertWriteArguments(ctx);
4437
4497
  const target = await resolveEnrichmentTarget(ctx, args);
4438
4498
  const entries = asEntries(
4439
4499
  await callQuery(ctx, api.entries.list, {
@@ -4615,6 +4675,7 @@ async function runEnrichCommand(ctx: CrmCliContext): Promise<void> {
4615
4675
  const lookups = input.map((value, index) =>
4616
4676
  requireNonEmptyJsonRecord(value, `lookup ${index + 1}`),
4617
4677
  );
4678
+ assertWriteArguments(ctx);
4618
4679
  out(ctx, await enrichProfiles({ kind, lookups }, ctx.enrichmentGateway));
4619
4680
  return;
4620
4681
  }
@@ -4720,10 +4781,14 @@ async function runReportsCommand(ctx: CrmCliContext): Promise<void> {
4720
4781
  }
4721
4782
 
4722
4783
  function crmHelp(): void {
4723
- console.log(`Usage: dench crm <subcommand>
4784
+ console.log(crmHelpText());
4785
+ }
4786
+
4787
+ export function crmHelpText(): string {
4788
+ return `Usage: dench crm <subcommand>
4724
4789
 
4725
4790
  Objects (CRM tables):
4726
- dench crm objects list [--json]
4791
+ dench crm objects list [--json] [--compact]
4727
4792
  dench crm objects get <name>
4728
4793
  dench crm objects create <name> [--description ...] [--default-view kanban] [--icon ...]
4729
4794
  dench crm objects update <name> [--default-view ...] [--icon ...] [--display-name "Clients"] [--display-name-singular "Client"] [--visibility workspace|owner_only|owner_plus_members]
@@ -4810,7 +4875,7 @@ Sidebar (progressive disclosure — Library + home Pinned strip; layout is WORKS
4810
4875
  shared shortcut, not a Library move; open to any member.
4811
4876
 
4812
4877
  Fields (columns):
4813
- dench crm fields list <object> [--json]
4878
+ dench crm fields list <object> [--compact] [--json]
4814
4879
  dench crm fields create <object> <name> --type text|email|phone|url|number|boolean|date|enum|relation|user|tags|action|file|richtext \\
4815
4880
  [--required] [--description ...] [--enum-values JSON] [--enum-colors JSON] \\
4816
4881
  [--enum-multiple] [--related-object <name>] [--relationship many_to_one|many_to_many] \\
@@ -5031,7 +5096,10 @@ Aggregates:
5031
5096
  dench crm aggregate <object> --field Status [--sum Amount]
5032
5097
 
5033
5098
  Full-text search:
5034
- dench crm search [--object <name>] [--limit N] <text>
5099
+ dench crm search [--object <name>] [--select "Name,Email"] [--limit N] [--page] [--cursor <cursor>] <text>
5100
+ --select returns a compact page with stable IDs, selected cells, isDone
5101
+ and continueCursor. Follow cursors until isDone for complete results.
5102
+ Without --select/--page/--cursor the legacy full-row array is unchanged.
5035
5103
 
5036
5104
  Statuses (kanban columns):
5037
5105
  dench crm statuses list <object>
@@ -5215,5 +5283,5 @@ unambiguous, but prefer canonical names from 'crm objects list'.
5215
5283
 
5216
5284
  Global flags:
5217
5285
  --json Output raw JSON instead of pretty-printed text.
5218
- `);
5286
+ `;
5219
5287
  }
package/cron.ts CHANGED
@@ -392,6 +392,9 @@ async function runCreate(ctx: CronCliContext): Promise<void> {
392
392
  "--prompt is required (becomes the user message of each cron-fired chat thread)",
393
393
  );
394
394
  }
395
+ const environment = getFlag(ctx.args, "--environment");
396
+ if (environment && !/^[a-z][a-z0-9-]{0,63}$/.test(environment))
397
+ throw new CronCliError("Invalid environment name");
395
398
  const description = getFlag(ctx.args, "--description");
396
399
  const overlapPolicy = getFlag(ctx.args, "--overlap") as
397
400
  | "skip"
@@ -431,6 +434,7 @@ async function runCreate(ctx: CronCliContext): Promise<void> {
431
434
  triggerType,
432
435
  name,
433
436
  prompt,
437
+ environment,
434
438
  description,
435
439
  overlapPolicy,
436
440
  disabled,
@@ -462,6 +466,7 @@ async function runCreate(ctx: CronCliContext): Promise<void> {
462
466
  const created = (await callMutation(ctx, createCronTriggerRef, {
463
467
  name,
464
468
  prompt,
469
+ environment,
465
470
  triggerType,
466
471
  schedule,
467
472
  description,
@@ -551,6 +556,7 @@ async function runSecret(ctx: CronCliContext): Promise<void> {
551
556
  }
552
557
 
553
558
  export type AppOrCrmCreateBase = {
559
+ environment?: string;
554
560
  triggerType: "composio" | "crm";
555
561
  name: string;
556
562
  prompt: string;
@@ -584,6 +590,7 @@ export function buildAppOrCrmCreateBody(
584
590
  const body: Record<string, unknown> = {
585
591
  name: base.name,
586
592
  prompt: base.prompt,
593
+ environment: base.environment,
587
594
  triggerType: base.triggerType,
588
595
  description: base.description,
589
596
  overlapPolicy: base.overlapPolicy,
@@ -731,6 +738,15 @@ async function runEvents(ctx: CronCliContext): Promise<void> {
731
738
 
732
739
  async function runUpdate(ctx: CronCliContext): Promise<void> {
733
740
  const jobId = shift(ctx.args, "job id");
741
+ const environment = getFlag(ctx.args, "--environment");
742
+ const clearEnvironment = hasFlag(ctx.args, "--clear-environment");
743
+ if (
744
+ environment &&
745
+ (clearEnvironment || !/^[a-z][a-z0-9-]{0,63}$/.test(environment))
746
+ )
747
+ throw new CronCliError(
748
+ "Choose a valid --environment name or --clear-environment",
749
+ );
734
750
  const name = getFlag(ctx.args, "--name");
735
751
  const prompt = getFlag(ctx.args, "--prompt");
736
752
  const description = getFlag(ctx.args, "--description");
@@ -753,6 +769,7 @@ async function runUpdate(ctx: CronCliContext): Promise<void> {
753
769
  const schedule = consumeScheduleFlags(ctx.args);
754
770
  const result = await callMutation(ctx, updateCronTriggerRef, {
755
771
  jobId,
772
+ environment: clearEnvironment ? null : environment,
756
773
  name,
757
774
  prompt,
758
775
  description,
@@ -866,6 +883,8 @@ Browse:
866
883
  dench cron events <toolkit> [--json] # app-event types for a connected app
867
884
 
868
885
  Create / update:
886
+ Add --environment <name> to bind a ready reusable setup.
887
+ New runs use its latest ready revision; --clear-environment removes the binding on update.
869
888
  dench cron create "Daily standup digest" \\
870
889
  --prompt "Pull yesterday's commits + Slack #standup msgs and post a summary…" \\
871
890
  --type schedule # default. also: webhook | app | crm (see below)