@dench.com/cli 2.7.4 → 2.7.5

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 (3) hide show
  1. package/dench.ts +1 -1
  2. package/package.json +1 -1
  3. package/tools.ts +108 -8
package/dench.ts CHANGED
@@ -677,7 +677,7 @@ Usage:
677
677
  dench integrations [--json]
678
678
  dench tool status [toolkit] [--json]
679
679
  dench tool connect <toolkit> [--callback-url <url>] [--json]
680
- dench tool search "create github issue" [--toolkit github] [--limit 20] [--json]
680
+ dench tool search "create github issue" [--toolkit github] [--limit 20] [--compact] [--json]
681
681
  dench tool run <composio_tool_slug> --args '{"key":"value"}' [--account <connectedAccountId>] [--json]
682
682
  dench tool disconnect <connectionId> [--json]
683
683
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dench.com/cli",
3
- "version": "2.7.4",
3
+ "version": "2.7.5",
4
4
  "description": "Dench agent workspace CLI.",
5
5
  "type": "module",
6
6
  "bin": {
package/tools.ts CHANGED
@@ -11,7 +11,7 @@
11
11
  *
12
12
  * dench apps [--json] — list connected apps
13
13
  * dench tool status [toolkit?] [--json] — list connections (optionally narrowed)
14
- * dench tool search "<query>" [--toolkit <slug>] [--limit N] [--json]
14
+ * dench tool search "<query>" [--toolkit <slug>] [--limit N] [--compact] [--json]
15
15
  * dench tool run <slug> [--args <json>] [--account <id>] [--json]
16
16
  * dench tool connect <toolkit> [--callback-url <url>] [--json] — print OAuth redirect URL
17
17
  * dench tool disconnect <connectionId> [--json]
@@ -187,12 +187,22 @@ async function runSearchSubcommand(ctx: ToolCliContext): Promise<void> {
187
187
  const toolkit = getFlag(ctx.args, "--toolkit");
188
188
  const limitRaw = getFlag(ctx.args, "--limit");
189
189
  const limit = limitRaw ? parsePositiveInt("--limit", limitRaw) : undefined;
190
+ const compact = hasFlag(ctx.args, "--compact");
190
191
  const queryParts = ctx.args.slice();
191
192
  ctx.args.length = 0;
193
+ // Everything left is the query, so an unconsumed flag would be searched for
194
+ // as a word and quietly match nothing. Refuse it instead: a typo that reads
195
+ // as "no results" is indistinguishable from an empty catalog.
196
+ const unknown = queryParts.find((part) => part.startsWith("--"));
197
+ if (unknown) {
198
+ throw new ToolCliError(
199
+ `Unknown flag for dench tool search: ${unknown}. Supported: --toolkit <slug>, --limit N, --compact, --json.`,
200
+ );
201
+ }
192
202
  const query = queryParts.join(" ").trim();
193
203
  if (!query) {
194
204
  throw new ToolCliError(
195
- 'Usage: dench tool search "<query>" [--toolkit <slug>] [--limit N] [--json]',
205
+ 'Usage: dench tool search "<query>" [--toolkit <slug>] [--limit N] [--compact] [--json]',
196
206
  );
197
207
  }
198
208
 
@@ -210,7 +220,7 @@ async function runSearchSubcommand(ctx: ToolCliContext): Promise<void> {
210
220
  console.log(JSON.stringify(payload, null, 2));
211
221
  return;
212
222
  }
213
- console.log(formatSearchResult(payload));
223
+ console.log(formatSearchResult(payload, compact));
214
224
  }
215
225
 
216
226
  async function runRunSubcommand(ctx: ToolCliContext): Promise<void> {
@@ -249,7 +259,22 @@ async function runRunSubcommand(ctx: ToolCliContext): Promise<void> {
249
259
  body,
250
260
  );
251
261
  } catch (error) {
252
- if (error instanceof GatewayHttpError && isLikelyNoConnection(error)) {
262
+ if (!(error instanceof GatewayHttpError)) throw error;
263
+ // Ambiguity is the opposite of a missing connection, and it has to be
264
+ // checked first: the gateway reports it with a 400 whose body also trips
265
+ // the no-connection hints, which would send the caller off to add a third
266
+ // account when the fix is to name one of the two they have.
267
+ if (isAccountSelectionRequired(error)) {
268
+ const selection = accountSelectionPayload(toolSlug, error);
269
+ console.log(
270
+ ctx.jsonOutput
271
+ ? JSON.stringify(selection, null, 2)
272
+ : formatAccountSelection(selection),
273
+ );
274
+ process.exitCode = 1;
275
+ return;
276
+ }
277
+ if (isLikelyNoConnection(error)) {
253
278
  const noConnPayload = noConnectionPayload(toolSlug, error);
254
279
  if (ctx.jsonOutput) {
255
280
  console.log(JSON.stringify(noConnPayload, null, 2));
@@ -475,8 +500,9 @@ async function callGateway(
475
500
  // Result extraction & formatting
476
501
  // ---------------------------------------------------------------------------
477
502
 
503
+ const ACCOUNT_SELECTION_CODE = "composio_account_selection_required";
504
+
478
505
  const NO_CONNECTION_HINTS = [
479
- "composio_account_selection_required",
480
506
  "no active connection",
481
507
  "no connection found",
482
508
  "connected_account_id is required",
@@ -493,6 +519,78 @@ function isLikelyNoConnection(error: GatewayHttpError): boolean {
493
519
  return NO_CONNECTION_HINTS.some((hint) => lower.includes(hint));
494
520
  }
495
521
 
522
+ function isAccountSelectionRequired(error: GatewayHttpError): boolean {
523
+ return error.body.toLowerCase().includes(ACCOUNT_SELECTION_CODE);
524
+ }
525
+
526
+ function accountSelectionPayload(toolSlug: string, error: GatewayHttpError) {
527
+ const accounts = accountChoices(error.body);
528
+ return {
529
+ ok: false,
530
+ account_selection_required: true,
531
+ tool_slug: toolSlug,
532
+ toolkit: toolkitFromSlug(toolSlug),
533
+ accounts,
534
+ error: error.message,
535
+ next_actions: accounts.length
536
+ ? accounts.map(
537
+ (account) =>
538
+ `Re-run with --account ${account.connected_account_id}${account.label ? ` (${account.label})` : ""}`,
539
+ )
540
+ : [
541
+ "Re-run with --account <connected_account_id>. Run `dench tool status <toolkit> --json` to list them.",
542
+ ],
543
+ };
544
+ }
545
+
546
+ /**
547
+ * The candidate accounts named in an account-selection error. The gateway
548
+ * already lists them, so parse them back out rather than spending a round trip
549
+ * re-listing connections the caller was just handed.
550
+ *
551
+ * They arrive as a JSON array inside the JSON-encoded error message, so decode
552
+ * the body first: that hands the array back unescaped instead of leaving us to
553
+ * undo the escaping by hand.
554
+ */
555
+ function accountChoices(body: string) {
556
+ const error = asRecord(asRecord(parseJsonOrUndefined(body))?.error);
557
+ const message = stringField(error, "message") ?? "";
558
+ const start = message.indexOf("[");
559
+ const end = message.lastIndexOf("]");
560
+ if (start === -1 || end <= start) return [];
561
+ const parsed = parseJsonOrUndefined(message.slice(start, end + 1));
562
+ return asRecordArray(parsed).flatMap((entry) => {
563
+ const id = stringField(entry, "connected_account_id");
564
+ if (!id) return [];
565
+ return [
566
+ { connected_account_id: id, label: stringField(entry, "label", "email") },
567
+ ];
568
+ });
569
+ }
570
+
571
+ function parseJsonOrUndefined(value: string): unknown {
572
+ try {
573
+ return JSON.parse(value);
574
+ } catch {
575
+ return undefined;
576
+ }
577
+ }
578
+
579
+ function formatAccountSelection(
580
+ payload: ReturnType<typeof accountSelectionPayload>,
581
+ ) {
582
+ return [
583
+ "Multiple active accounts for this tool — name the one to use.",
584
+ `Tool: ${payload.tool_slug}`,
585
+ payload.toolkit ? `Toolkit: ${payload.toolkit}` : null,
586
+ "",
587
+ "Next actions:",
588
+ ...payload.next_actions.map((line) => ` - ${line}`),
589
+ ]
590
+ .filter((line) => line !== null)
591
+ .join("\n");
592
+ }
593
+
496
594
  function noConnectionPayload(toolSlug: string, error: GatewayHttpError) {
497
595
  const toolkit = toolkitFromSlug(toolSlug);
498
596
  return {
@@ -623,7 +721,7 @@ function toolApprovalHint(toolSlug: string | undefined): string {
623
721
  return readOnly ? `read-only (${readOnly})` : "approval likely";
624
722
  }
625
723
 
626
- function formatSearchResult(searchResult: unknown): string {
724
+ function formatSearchResult(searchResult: unknown, compact = false): string {
627
725
  const root = asRecord(searchResult);
628
726
  const data = asRecord(root?.data) ?? root;
629
727
  const items = extractSearchItems(data);
@@ -658,7 +756,9 @@ function formatSearchResult(searchResult: unknown): string {
658
756
  const isConnected =
659
757
  booleanField(asRecord(tool.connection_status), "is_connected") ??
660
758
  connectedSet.has(toolkit.toLowerCase());
661
- const description = compactLine(tool.description) ?? null;
759
+ const description = compact
760
+ ? null
761
+ : (compactLine(tool.description) ?? null);
662
762
  lines.push(
663
763
  `${index + 1}. ${slug} - ${name}`,
664
764
  ` toolkit: ${toolkit} | ${toolApprovalHint(slug)} | ${
@@ -752,7 +852,7 @@ Usage:
752
852
  dench apps [--json]
753
853
  dench tool status [toolkit] [--json]
754
854
  dench tool connect <toolkit> [--callback-url <url>] [--json]
755
- dench tool search "create github issue" [--toolkit github] [--limit 20] [--json]
855
+ dench tool search "create github issue" [--toolkit github] [--limit 20] [--compact] [--json]
756
856
  dench tool run <composio_tool_slug> --args '{"key":"value"}' [--account <connectedAccountId>] [--json]
757
857
  dench tool disconnect <connectionId> [--json]
758
858