@dench.com/cli 3.6.3-staging.372 → 3.6.5-staging.384

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,44 @@
1
+ import { normalizeUnipileToolkitSlug } from "./unipile-toolkits";
2
+
3
+ /** Provider ownership is independent of connection health and search results. */
4
+ export function nativeToolkit(slug: string): string | null {
5
+ const normalized = slug
6
+ .trim()
7
+ .toLowerCase()
8
+ .replace(/[\s_-]+/g, "");
9
+ if (normalized === "slack" || normalized === "slackbot") return "slack";
10
+ return normalizeUnipileToolkitSlug(slug);
11
+ }
12
+
13
+ export function nativeToolkitForTool(slug: string): string | null {
14
+ const parts = slug.trim().split(/[_-]+/);
15
+ // Providers with separator aliases (GOOGLE_CALENDAR, MICROSOFT_OUTLOOK)
16
+ // are resolved before considering the remainder as an action name.
17
+ for (let length = Math.min(3, parts.length - 1); length > 0; length--) {
18
+ const toolkit = nativeToolkit(parts.slice(0, length).join(""));
19
+ if (toolkit) return toolkit;
20
+ }
21
+ return null;
22
+ }
23
+
24
+ export function isNativeToolRecord(value: {
25
+ slug?: unknown;
26
+ tool_slug?: unknown;
27
+ toolkit_slug?: unknown;
28
+ toolkit?: unknown;
29
+ }): boolean {
30
+ const toolkit =
31
+ value.toolkit && typeof value.toolkit === "object"
32
+ ? (value.toolkit as { slug?: unknown; name?: unknown })
33
+ : {};
34
+ const name =
35
+ value.toolkit_slug ??
36
+ (typeof value.toolkit === "string"
37
+ ? value.toolkit
38
+ : (toolkit.slug ?? toolkit.name));
39
+ const slug = value.slug ?? value.tool_slug;
40
+ return (
41
+ (typeof name === "string" && nativeToolkit(name) !== null) ||
42
+ (typeof slug === "string" && nativeToolkitForTool(slug) !== null)
43
+ );
44
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dench.com/cli",
3
- "version": "3.6.3-staging.372",
3
+ "version": "3.6.5-staging.384",
4
4
  "description": "Dench agent workspace CLI.",
5
5
  "type": "module",
6
6
  "bin": {
package/tools.ts CHANGED
@@ -57,6 +57,11 @@ import {
57
57
  type ConnectionSource,
58
58
  connectionSource,
59
59
  } from "./lib/connection-source";
60
+ import {
61
+ isNativeToolRecord,
62
+ nativeToolkit,
63
+ nativeToolkitForTool,
64
+ } from "./lib/native-providers";
60
65
  import {
61
66
  normalizeUnipileToolkitSlug,
62
67
  unipileConnectKindFromToolkitSlug,
@@ -75,6 +80,8 @@ export type ToolCliContext = {
75
80
  gatewayBaseUrl?: string;
76
81
  gatewayHeaders?: Record<string, string>;
77
82
  gatewayAuthError?: string;
83
+ /** Resolve the gateway only when a third-party operation actually needs it. */
84
+ resolveGatewayAuth?: () => Promise<GatewayAuthContext>;
78
85
  /**
79
86
  * Convex client + bearer for the Unipile half of the list. Absent on the
80
87
  * help path and whenever the caller could not be authenticated, in which
@@ -93,7 +100,11 @@ export type ToolCliContext = {
93
100
 
94
101
  type GatewayAuthContext = Pick<
95
102
  ToolCliContext,
96
- "bearerToken" | "gatewayBaseUrl" | "gatewayHeaders" | "gatewayAuthError"
103
+ | "bearerToken"
104
+ | "gatewayBaseUrl"
105
+ | "gatewayHeaders"
106
+ | "gatewayAuthError"
107
+ | "resolveGatewayAuth"
97
108
  >;
98
109
 
99
110
  type UnipileAuthContext = Pick<ToolCliContext, "convex" | "sessionToken">;
@@ -152,43 +163,9 @@ const connectCalendarRef = makeFunctionReference<"action">(
152
163
  "functions/calendarUnipileNode:createCalendarHostedAuthLink",
153
164
  );
154
165
 
155
- /**
156
- * Gmail, Outlook, Calendar and LinkedIn are brokered by Dench, and Composio
157
- * refuses to connect them — so its catalogue entries for those toolkits can
158
- * never execute, and showing them next to the ones that work just teaches an
159
- * agent to pick a tool that always answers `composio_not_connected`.
160
- *
161
- * The set comes from the server with the tools themselves rather than being
162
- * hardcoded here, so a fifth platform needs no CLI change.
163
- */
164
-
165
- /**
166
- * Composio spells some toolkits without a separator (`googlecalendar`), while
167
- * our registry uses the hyphenated form. Normalize through this map before the
168
- * membership test, or the dead Composio calendar entries survive the filter
169
- * and sit next to the working ones under two names for one product.
170
- */
171
- const COMPOSIO_TOOLKIT_ALIASES: Record<string, string> = {
172
- googlecalendar: "google-calendar",
173
- outlookcalendar: "outlook-calendar",
174
- };
175
-
176
- function withoutShadowedComposioTools(
177
- items: JsonRecord[],
178
- ownedToolkits: Set<string>,
179
- ): JsonRecord[] {
180
- if (ownedToolkits.size === 0) return items;
181
- return items.filter((item) => {
182
- const slug = (
183
- stringField(item, "toolkit_slug") ??
184
- stringField(asRecord(item.toolkit), "slug", "name") ??
185
- ""
186
- )
187
- .toLowerCase()
188
- .replaceAll("_", "-");
189
- const canonical = COMPOSIO_TOOLKIT_ALIASES[slug] ?? slug;
190
- return !canonical || !ownedToolkits.has(canonical);
191
- });
166
+ /** Native ownership survives empty searches, disconnections and registry failures. */
167
+ function withoutShadowedComposioTools(items: JsonRecord[]): JsonRecord[] {
168
+ return items.filter((item) => !isNativeToolRecord(item));
192
169
  }
193
170
 
194
171
  /**
@@ -277,14 +254,16 @@ export async function fetchConnectedAppsSummary(
277
254
  connections: unipile.connections,
278
255
  ...unipileFields,
279
256
  };
280
- return error instanceof MissingApiKeyError
281
- ? { ...failure, reason: error.message }
282
- : { ...failure, error: errorMessage(error) };
257
+ return ctx.gatewayAuthError
258
+ ? { ...failure, reason: ctx.gatewayAuthError }
259
+ : error instanceof MissingApiKeyError
260
+ ? { ...failure, reason: error.message }
261
+ : { ...failure, error: errorMessage(error) };
283
262
  }
284
263
  const connections = [
285
264
  ...extractConnections(payload)
286
265
  .map(compactConnection)
287
- .filter((connection) => !normalizeUnipileToolkitSlug(connection.toolkit)),
266
+ .filter((connection) => !nativeToolkit(connection.toolkit)),
288
267
  ...unipile.connections,
289
268
  ];
290
269
  return {
@@ -305,7 +284,7 @@ async function runStatusSubcommand(ctx: ToolCliContext): Promise<void> {
305
284
  : ctx.args[0];
306
285
  if (requestedToolkit) ctx.args.shift();
307
286
  const unipileToolkit = requestedToolkit
308
- ? normalizeUnipileToolkitSlug(requestedToolkit)
287
+ ? nativeToolkit(requestedToolkit)
309
288
  : null;
310
289
  const filterToolkit = unipileToolkit ?? requestedToolkit;
311
290
 
@@ -369,7 +348,11 @@ async function runStatusSubcommand(ctx: ToolCliContext): Promise<void> {
369
348
  }
370
349
 
371
350
  async function runSearchSubcommand(ctx: ToolCliContext): Promise<void> {
372
- const toolkit = getFlag(ctx.args, "--toolkit");
351
+ const requestedToolkit = getFlag(ctx.args, "--toolkit");
352
+ const ownedToolkit = requestedToolkit
353
+ ? nativeToolkit(requestedToolkit)
354
+ : null;
355
+ const toolkit = ownedToolkit ?? requestedToolkit;
373
356
  const limitRaw = getFlag(ctx.args, "--limit");
374
357
  const limit = limitRaw ? parsePositiveInt("--limit", limitRaw) : undefined;
375
358
  const compact = hasFlag(ctx.args, "--compact");
@@ -395,28 +378,43 @@ async function runSearchSubcommand(ctx: ToolCliContext): Promise<void> {
395
378
  if (toolkit) body.toolkit_slug = toolkit;
396
379
  if (limit) body.limit = limit;
397
380
 
398
- const [payload, unipile] = await Promise.all([
399
- callGateway(ctx, "POST", "/v1/composio/tools/search", body),
381
+ const [gateway, unipile] = await Promise.all([
382
+ ownedToolkit
383
+ ? Promise.resolve({
384
+ payload: {} as unknown,
385
+ error: undefined as string | undefined,
386
+ })
387
+ : callGateway(ctx, "POST", "/v1/composio/tools/search", body)
388
+ .then((payload) => ({
389
+ payload,
390
+ error: undefined as string | undefined,
391
+ }))
392
+ .catch((error) => ({
393
+ payload: {} as unknown,
394
+ error: errorMessage(error),
395
+ })),
400
396
  fetchUnipileTools(ctx, { query, ...(toolkit ? { toolkit } : {}) }),
401
397
  ]);
402
398
 
403
- const record = asRecord(payload) ?? {};
404
- const ownedToolkits = new Set(
405
- unipile.tools
406
- .map((tool) => stringField(asRecord(tool.toolkit), "slug") ?? "")
407
- .filter(Boolean),
408
- );
399
+ const record = asRecord(gateway.payload) ?? {};
409
400
  const merged = {
410
- ...record,
401
+ query,
411
402
  items: [
403
+ ...unipile.tools,
412
404
  ...withoutShadowedComposioTools(
413
- extractSearchItems(record),
414
- ownedToolkits,
405
+ extractSearchItems(asRecord(record.data) ?? record),
406
+ ),
407
+ ].slice(0, limit ?? 100),
408
+ // Do not retain unfiltered nested tool_schemas/data from the gateway.
409
+ connected_toolkits: [
410
+ ...[...connectedToolkitSet(asRecord(record.data) ?? record)].filter(
411
+ (slug) => !nativeToolkit(slug),
415
412
  ),
416
- ...unipile.tools,
417
413
  ],
418
414
  nativeAvailable: unipile.available,
419
415
  ...(unipile.error ? { nativeError: unipile.error } : {}),
416
+ ...(gateway.error ? { gatewayError: gateway.error } : {}),
417
+ ...(ownedToolkit ? { toolkit: ownedToolkit } : {}),
420
418
  };
421
419
 
422
420
  if (ctx.jsonOutput) {
@@ -427,20 +425,27 @@ async function runSearchSubcommand(ctx: ToolCliContext): Promise<void> {
427
425
  }
428
426
 
429
427
  /**
430
- * Whether this slug is ours. A lookup failure answers "not ours", so an old
431
- * deployment routes everything to Composio exactly as it did before.
428
+ * Resolve a native action without interpreting a broken registry as another provider.
432
429
  */
433
430
  async function resolveUnipileTool(
434
431
  ctx: UnipileAuthContext,
435
432
  slug: string,
436
433
  ): Promise<JsonRecord | null> {
437
- if (!ctx.convex) return null;
434
+ if (!ctx.convex) {
435
+ if (nativeToolkitForTool(slug))
436
+ throw new ToolCliError(
437
+ "native_unavailable: Sign in to Dench to use this integration.",
438
+ );
439
+ return null;
440
+ }
438
441
  try {
439
442
  return (
440
443
  asRecord(await ctx.convex.query(getUnipileToolRef, { slug } as never)) ??
441
444
  null
442
445
  );
443
- } catch {
446
+ } catch (error) {
447
+ if (nativeToolkitForTool(slug))
448
+ throw new ToolCliError(`native_unavailable: ${errorMessage(error)}`);
444
449
  return null;
445
450
  }
446
451
  }
@@ -528,7 +533,11 @@ async function fetchUnipileTools(
528
533
  .filter((row): row is JsonRecord => row !== undefined)
529
534
  .map((row) =>
530
535
  typeof row.source === "string"
531
- ? { ...row, source: connectionSource(row.source, "dench") }
536
+ ? {
537
+ ...row,
538
+ source: connectionSource(row.source, "dench"),
539
+ connection_status: { is_connected: true },
540
+ }
532
541
  : row,
533
542
  ),
534
543
  };
@@ -565,9 +574,18 @@ async function runRunSubcommand(ctx: ToolCliContext): Promise<void> {
565
574
  // routed to Composio, which 404s.
566
575
  const unipileTool = await resolveUnipileTool(ctx, toolSlug);
567
576
  if (unipileTool) {
577
+ if (account)
578
+ throw new ToolCliError(
579
+ "--account is for third-party connections. Pass the native mailbox/account field in --args.",
580
+ );
568
581
  await runUnipileTool(ctx, toolSlug, parsedArgs, approval);
569
582
  return;
570
583
  }
584
+ if (nativeToolkitForTool(toolSlug)) {
585
+ throw new ToolCliError(
586
+ `unsupported_native_capability: ${toolSlug} is not supported by Dench's native integration. For email drafts use dench email draft create; do not reconnect or try another provider.`,
587
+ );
588
+ }
571
589
 
572
590
  const body: Record<string, unknown> = {
573
591
  tool_slug: toolSlug,
@@ -697,6 +715,21 @@ async function runConnectSubcommand(ctx: ToolCliContext): Promise<void> {
697
715
  ctx.args.shift();
698
716
 
699
717
  const normalizedToolkit = toolkit.trim().toLowerCase();
718
+ // Hosted chat owns the human connection UI. Do not mint OAuth credentials
719
+ // in a model command or journal them in a tool result. External CLI clients
720
+ // retain their ordinary link-based flow.
721
+ if (process.env.DENCH_CHAT_ARTIFACTS === "1") {
722
+ console.log(
723
+ JSON.stringify({
724
+ ok: true,
725
+ kind: "connect",
726
+ toolkit: nativeToolkit(toolkit) ?? normalizedToolkit,
727
+ action: "connect",
728
+ }),
729
+ );
730
+ return;
731
+ }
732
+
700
733
  if (normalizedToolkit === "slack" || normalizedToolkit === "slackbot") {
701
734
  await runSlackConnectSubcommand(ctx, normalizedToolkit);
702
735
  return;
@@ -920,6 +953,11 @@ async function callGateway(
920
953
  path: string,
921
954
  body?: Record<string, unknown>,
922
955
  ): Promise<unknown> {
956
+ if (ctx.resolveGatewayAuth && !ctx.bearerToken) {
957
+ const resolve = ctx.resolveGatewayAuth;
958
+ ctx.resolveGatewayAuth = undefined;
959
+ Object.assign(ctx, await resolve());
960
+ }
923
961
  const bearerToken = requireBearerToken(ctx);
924
962
  const baseUrl = resolveGatewayBaseUrl(ctx);
925
963
  const init: RequestInit = {
@@ -1239,6 +1277,13 @@ function formatSearchResult(searchResult: unknown, compact = false): string {
1239
1277
  const items = extractSearchItems(data);
1240
1278
  const connectedSet = connectedToolkitSet(data);
1241
1279
 
1280
+ if (
1281
+ items.length === 0 &&
1282
+ typeof data?.toolkit === "string" &&
1283
+ nativeToolkit(data.toolkit)
1284
+ ) {
1285
+ return `No matching native action for ${data.toolkit}. For email drafts use dench email draft create; otherwise inspect dench tool status ${data.toolkit} --json. Native apps never fall back to Composio.`;
1286
+ }
1242
1287
  if (items.length === 0) {
1243
1288
  return [
1244
1289
  "No matching tools found.",