@dench.com/cli 2.8.3 → 2.8.5-staging.175

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,52 @@
1
+ /**
2
+ * Shared Levenshtein edit-distance helper for CLI "did you mean …?"
3
+ * suggestions. Lives in cli/lib so `dench crm`, `dench apps`, and any
4
+ * other hand-rolled dispatcher can suggest the closest known verb
5
+ * instead of dead-ending on `Unknown subcommand: entires`.
6
+ */
7
+
8
+ export function levenshteinDistance(a: string, b: string): number {
9
+ if (a === b) return 0;
10
+ if (a.length === 0) return b.length;
11
+ if (b.length === 0) return a.length;
12
+ let prev = Array.from({ length: b.length + 1 }, (_, i) => i);
13
+ let curr = new Array<number>(b.length + 1);
14
+ for (let i = 1; i <= a.length; i += 1) {
15
+ curr[0] = i;
16
+ for (let j = 1; j <= b.length; j += 1) {
17
+ const cost = a[i - 1] === b[j - 1] ? 0 : 1;
18
+ curr[j] = Math.min(prev[j] + 1, curr[j - 1] + 1, prev[j - 1] + cost);
19
+ }
20
+ [prev, curr] = [curr, prev];
21
+ }
22
+ return prev[b.length];
23
+ }
24
+
25
+ /**
26
+ * Return the candidate closest to `input` (case-insensitive), or null
27
+ * when nothing is plausibly a typo. Exact case-insensitive matches win
28
+ * outright; otherwise the best candidate must be within
29
+ * max(2, floor(len/2)) edits — same threshold the CRM field
30
+ * suggester has used in production.
31
+ */
32
+ export function closestMatch(
33
+ input: string,
34
+ candidates: readonly string[],
35
+ ): string | null {
36
+ const lower = input.toLowerCase();
37
+ const caseMatch = candidates.find(
38
+ (candidate) => candidate.toLowerCase() === lower,
39
+ );
40
+ if (caseMatch) return caseMatch;
41
+ let best: string | null = null;
42
+ let bestDistance = Number.POSITIVE_INFINITY;
43
+ for (const candidate of candidates) {
44
+ const distance = levenshteinDistance(lower, candidate.toLowerCase());
45
+ if (distance < bestDistance) {
46
+ bestDistance = distance;
47
+ best = candidate;
48
+ }
49
+ }
50
+ const threshold = Math.max(2, Math.floor(input.length / 2));
51
+ return best !== null && bestDistance <= threshold ? best : null;
52
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dench.com/cli",
3
- "version": "2.8.3",
3
+ "version": "2.8.5-staging.175",
4
4
  "description": "Dench agent workspace CLI.",
5
5
  "type": "module",
6
6
  "bin": {
package/tools.ts CHANGED
@@ -7,10 +7,14 @@
7
7
  * paid-workspace gateway key resolved from the active agent session by
8
8
  * the top-level dispatcher.
9
9
  *
10
- * The one exception is the connection LISTING, which is not a pure gateway
11
- * passthrough: Gmail, Outlook, Calendar and LinkedIn are brokered by Dench
12
- * through Unipile, not Composio, so `status` also reads them from Convex and
13
- * merges the two into one list. See `fetchUnipileConnections`.
10
+ * Two exceptions are not pure gateway passthroughs:
11
+ * - the connection LISTING: Gmail, Outlook, Calendar and LinkedIn (Unipile)
12
+ * and Slack (Dench's in-house Slack app) are brokered by Dench, not
13
+ * Composio, so `status` also reads them from Convex and merges the two
14
+ * into one list. See `fetchUnipileConnections`.
15
+ * - `connect slack`: prints the in-house Slack install URL
16
+ * (`/api/slack/install?slug=…`) instead of a Composio OAuth redirect —
17
+ * the in-house connection always takes preference over Composio Slack.
14
18
  *
15
19
  * Subcommands:
16
20
  *
@@ -65,6 +69,13 @@ export type ToolCliContext = {
65
69
  */
66
70
  convex?: ConvexHttpClient;
67
71
  sessionToken?: string;
72
+ /**
73
+ * The session's app host + workspace slug, when the dispatcher knows them.
74
+ * Used to build first-party links (the in-house Slack install URL) against
75
+ * the RIGHT tier — a staging session must not print a production URL.
76
+ */
77
+ appOrigin?: string;
78
+ workspaceSlug?: string;
68
79
  };
69
80
 
70
81
  type GatewayAuthContext = Pick<
@@ -549,6 +560,74 @@ async function runRunSubcommand(ctx: ToolCliContext): Promise<void> {
549
560
  console.log(redactForDisplay(payload));
550
561
  }
551
562
 
563
+ const getAgentBillingStatusRef = makeFunctionReference<"query">(
564
+ "functions/agentWorkspace:getAgentBillingStatus",
565
+ );
566
+
567
+ /**
568
+ * The active workspace's slug, for building the in-house Slack install URL.
569
+ * Best-effort — a failure yields null and the caller prints a placeholder the
570
+ * human can fill in, rather than the whole command failing.
571
+ */
572
+ async function resolveWorkspaceSlug(
573
+ ctx: UnipileAuthContext,
574
+ ): Promise<string | null> {
575
+ if (!ctx.convex) return null;
576
+ try {
577
+ const result = await ctx.convex.query(getAgentBillingStatusRef, {
578
+ ...(ctx.sessionToken ? { sessionToken: ctx.sessionToken } : {}),
579
+ } as never);
580
+ return stringField(asRecord(asRecord(result)?.workspace), "slug") ?? null;
581
+ } catch {
582
+ return null;
583
+ }
584
+ }
585
+
586
+ /**
587
+ * Slack connects through Dench's OWN Slack app — the in-house install that
588
+ * powers both the messaging channel (DM / @mention the bot) and the agent
589
+ * SLACK_* tools. It is never connected through Composio, so `dench tool
590
+ * connect slack` prints the first-party OAuth install URL instead of asking
591
+ * the gateway for a Composio redirect. Admin-only: the URL enforces org-admin
592
+ * membership itself.
593
+ */
594
+ async function runSlackConnectSubcommand(
595
+ ctx: ToolCliContext,
596
+ toolkit: string,
597
+ ): Promise<void> {
598
+ const slug = ctx.workspaceSlug?.trim() || (await resolveWorkspaceSlug(ctx));
599
+ const origin = (ctx.appOrigin?.trim() || resolveAppPublicOrigin()).replace(
600
+ /\/+$/,
601
+ "",
602
+ );
603
+ const redirectUrl = `${origin}/api/slack/install?slug=${
604
+ slug ? encodeURIComponent(slug) : "<workspace-slug>"
605
+ }`;
606
+ const message = `Connect Slack here (org admin only): ${redirectUrl}`;
607
+ const summary = {
608
+ ok: true,
609
+ toolkit,
610
+ inHouse: true,
611
+ redirectUrl,
612
+ message,
613
+ note:
614
+ "Slack is Dench's in-house integration (not Composio). One install powers " +
615
+ "both the Slack messaging channel and the agent SLACK_* tools " +
616
+ "(SLACK_SEND_MESSAGE, SLACK_FETCH_HISTORY, …).",
617
+ };
618
+ if (ctx.jsonOutput) {
619
+ console.log(JSON.stringify(summary, null, 2));
620
+ return;
621
+ }
622
+ console.log(message);
623
+ console.log(
624
+ "Slack is Dench's in-house integration (not Composio). Once an admin installs it,",
625
+ );
626
+ console.log(
627
+ "run tools with `dench tool run SLACK_SEND_MESSAGE --args '{…}'` — see `dench tool search --toolkit slack`.",
628
+ );
629
+ }
630
+
552
631
  async function runConnectSubcommand(ctx: ToolCliContext): Promise<void> {
553
632
  const toolkit = ctx.args[0];
554
633
  if (!toolkit || toolkit.startsWith("--")) {
@@ -557,6 +636,12 @@ async function runConnectSubcommand(ctx: ToolCliContext): Promise<void> {
557
636
  );
558
637
  }
559
638
  ctx.args.shift();
639
+
640
+ const normalizedToolkit = toolkit.trim().toLowerCase();
641
+ if (normalizedToolkit === "slack" || normalizedToolkit === "slackbot") {
642
+ await runSlackConnectSubcommand(ctx, normalizedToolkit);
643
+ return;
644
+ }
560
645
  const callbackUrl =
561
646
  getFlag(ctx.args, "--callback-url") ??
562
647
  process.env.DENCH_OAUTH_CALLBACK_URL?.trim() ??
@@ -1172,10 +1257,15 @@ Notes:
1172
1257
  All commands authenticate with DENCH_API_KEY or the active \`dench signin\`
1173
1258
  agent session. Agent sessions require a paid workspace.
1174
1259
  Results carry a "source". "composio" rows are brokered by Composio; "unipile"
1175
- rows (gmail, outlook, calendar, linkedin) are connected by Dench itself. Both
1176
- run through \`dench tool run\` with the same slug + --args contract; only
1177
- \`dench tool connect\` is Composio-only — the human connects the other four
1178
- from the workspace Marketplace.
1260
+ rows (gmail, outlook, calendar, linkedin) and "dench" rows (slack) are
1261
+ connected by Dench itself. ALL of them run through \`dench tool run\` with the
1262
+ same slug + --args contract. \`dench tool connect slack\` prints the in-house
1263
+ Slack install URL (org admin, one click); gmail/outlook/calendar/linkedin are
1264
+ connected by a human from the workspace Marketplace; every other toolkit
1265
+ connects through Composio's OAuth redirect.
1266
+ Slack is ALWAYS in-house: SLACK_* slugs (SLACK_SEND_MESSAGE with Block Kit
1267
+ blocks/buttons, SLACK_FETCH_HISTORY, SLACK_LIST_CHANNELS, …) run through the
1268
+ org's own Slack app install and can post to / read any channel.
1179
1269
  A write tool returns approval_required with an approvalId. Once a human
1180
1270
  approves it, re-run with --approval <approvalId>; an approval is bound to that
1181
1271
  exact tool and argument set.