@dench.com/cli 2.8.3 → 2.8.4-staging.154
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.
- package/apps.ts +31 -3
- package/crm.ts +609 -86
- package/dench.ts +126 -21
- package/email.ts +255 -41
- package/lib/cli-args.ts +41 -0
- package/lib/command-registry.ts +13 -3
- package/lib/levenshtein.ts +52 -0
- package/package.json +1 -1
- package/tools.ts +98 -8
|
@@ -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
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
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
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)
|
|
1176
|
-
run through \`dench tool run\` with the
|
|
1177
|
-
\`dench tool connect\`
|
|
1178
|
-
|
|
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.
|