@dench.com/cli 2.7.3 → 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.
- package/apps.ts +226 -0
- package/dench.ts +118 -6
- package/lib/command-registry.ts +26 -0
- package/package.json +2 -1
- package/tools.ts +108 -8
package/apps.ts
ADDED
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `dench apps` — the App Store surface (create / list / gallery / install /
|
|
3
|
+
* publish / uninstall / enable / pin). Distinct from `dench integrations`
|
|
4
|
+
* (Composio connected apps). Backed by convex/functions/appStore.ts — the same
|
|
5
|
+
* functions the workspace App Store panel and the API v1 `/apps/*` routes call.
|
|
6
|
+
*
|
|
7
|
+
* Thin dispatcher; forwards the caller's bearer through `sessionToken`, which
|
|
8
|
+
* the server's requireCrmAccess accepts (and appStore forwards to the file
|
|
9
|
+
* functions as an apiKey for publish/install/create).
|
|
10
|
+
*/
|
|
11
|
+
import type { ConvexHttpClient } from "convex/browser";
|
|
12
|
+
import { makeFunctionReference } from "convex/server";
|
|
13
|
+
|
|
14
|
+
const api = {
|
|
15
|
+
listInstalled: makeFunctionReference<"query">(
|
|
16
|
+
"functions/appStore:listInstalled",
|
|
17
|
+
),
|
|
18
|
+
listListings: makeFunctionReference<"query">(
|
|
19
|
+
"functions/appStore:listListings",
|
|
20
|
+
),
|
|
21
|
+
listGallery: makeFunctionReference<"query">("functions/appStore:listGallery"),
|
|
22
|
+
install: makeFunctionReference<"action">("functions/appStore:install"),
|
|
23
|
+
publish: makeFunctionReference<"action">("functions/appStore:publish"),
|
|
24
|
+
createApp: makeFunctionReference<"action">("functions/appStore:createApp"),
|
|
25
|
+
uninstall: makeFunctionReference<"mutation">("functions/appStore:uninstall"),
|
|
26
|
+
setEnabled: makeFunctionReference<"mutation">(
|
|
27
|
+
"functions/appStore:setEnabled",
|
|
28
|
+
),
|
|
29
|
+
setPinned: makeFunctionReference<"mutation">("functions/appStore:setPinned"),
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
class AppsCliError extends Error {}
|
|
33
|
+
|
|
34
|
+
type Ctx = {
|
|
35
|
+
convex: ConvexHttpClient;
|
|
36
|
+
args: string[];
|
|
37
|
+
sessionToken?: string;
|
|
38
|
+
jsonOutput: boolean;
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
function shift(args: string[], label: string): string {
|
|
42
|
+
const value = args.shift();
|
|
43
|
+
if (!value) throw new AppsCliError(`Missing ${label}`);
|
|
44
|
+
return value;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function takeFlag(args: string[], name: string): string | undefined {
|
|
48
|
+
const i = args.indexOf(name);
|
|
49
|
+
if (i === -1) return undefined;
|
|
50
|
+
const next = args[i + 1];
|
|
51
|
+
if (next !== undefined && !next.startsWith("--")) {
|
|
52
|
+
args.splice(i, 2);
|
|
53
|
+
return next;
|
|
54
|
+
}
|
|
55
|
+
args.splice(i, 1);
|
|
56
|
+
return undefined;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function firstPositional(args: string[]): string | undefined {
|
|
60
|
+
return args.find((a) => !a.startsWith("--"));
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function out(ctx: Ctx, value: unknown) {
|
|
64
|
+
console.log(JSON.stringify(value, null, ctx.jsonOutput ? 0 : 2));
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function withAuth(ctx: Ctx, args: Record<string, unknown>) {
|
|
68
|
+
return {
|
|
69
|
+
...args,
|
|
70
|
+
...(ctx.sessionToken ? { sessionToken: ctx.sessionToken } : {}),
|
|
71
|
+
} as never;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function appsHelp() {
|
|
75
|
+
console.log(`Usage: dench apps <subcommand> (App Store — not Composio; see 'dench integrations')
|
|
76
|
+
|
|
77
|
+
dench apps list [--json] Installed apps in this workspace
|
|
78
|
+
dench apps gallery [--json] Discover listings — Official (Dench) + Community
|
|
79
|
+
dench apps info <slug> [--json] One installed app's record
|
|
80
|
+
dench apps create <slug> --name "My App" [--icon <name>] [--description "…"]
|
|
81
|
+
Scaffold .dench.yaml + index.html and register it
|
|
82
|
+
dench apps install <listingId> [--json] Install a gallery listing into this workspace
|
|
83
|
+
dench apps publish <appPath> --slug <slug> --name "My App" \\
|
|
84
|
+
[--description "…"] [--icon <name>] [--visibility org|public|official|unlisted] [--version 1.0.0]
|
|
85
|
+
Bundle an app's files into a version + listing
|
|
86
|
+
dench apps uninstall <slug> Remove an installed app
|
|
87
|
+
dench apps enable | disable <slug> Toggle whether the app loads
|
|
88
|
+
dench apps pin | unpin <slug> Toggle the rail pin
|
|
89
|
+
`);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export async function runAppsCommand(opts: {
|
|
93
|
+
convex: ConvexHttpClient;
|
|
94
|
+
args: string[];
|
|
95
|
+
sessionToken?: string;
|
|
96
|
+
}): Promise<void> {
|
|
97
|
+
const jsonOutput = opts.args.includes("--json");
|
|
98
|
+
const args = opts.args.filter((a) => a !== "--json");
|
|
99
|
+
const ctx: Ctx = {
|
|
100
|
+
convex: opts.convex,
|
|
101
|
+
args,
|
|
102
|
+
sessionToken: opts.sessionToken,
|
|
103
|
+
jsonOutput,
|
|
104
|
+
};
|
|
105
|
+
const sub = ctx.args.shift();
|
|
106
|
+
switch (sub) {
|
|
107
|
+
case undefined:
|
|
108
|
+
if (jsonOutput) {
|
|
109
|
+
out(ctx, await ctx.convex.query(api.listInstalled, withAuth(ctx, {})));
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
appsHelp();
|
|
113
|
+
return;
|
|
114
|
+
case "help":
|
|
115
|
+
case "--help":
|
|
116
|
+
appsHelp();
|
|
117
|
+
return;
|
|
118
|
+
case "list":
|
|
119
|
+
out(ctx, await ctx.convex.query(api.listInstalled, withAuth(ctx, {})));
|
|
120
|
+
return;
|
|
121
|
+
case "gallery":
|
|
122
|
+
out(ctx, await ctx.convex.query(api.listGallery, withAuth(ctx, {})));
|
|
123
|
+
return;
|
|
124
|
+
case "info": {
|
|
125
|
+
const slug = shift(ctx.args, "app slug");
|
|
126
|
+
const installed = (await ctx.convex.query(
|
|
127
|
+
api.listInstalled,
|
|
128
|
+
withAuth(ctx, {}),
|
|
129
|
+
)) as Array<{ slug: string }>;
|
|
130
|
+
out(ctx, installed.find((a) => a.slug === slug) ?? null);
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
case "create": {
|
|
134
|
+
const name = takeFlag(ctx.args, "--name");
|
|
135
|
+
const icon = takeFlag(ctx.args, "--icon");
|
|
136
|
+
const description = takeFlag(ctx.args, "--description");
|
|
137
|
+
const slug = firstPositional(ctx.args) ?? name;
|
|
138
|
+
if (!slug) {
|
|
139
|
+
throw new AppsCliError(
|
|
140
|
+
'Usage: dench apps create <slug> --name "My App" [--icon <name>]',
|
|
141
|
+
);
|
|
142
|
+
}
|
|
143
|
+
out(
|
|
144
|
+
ctx,
|
|
145
|
+
await ctx.convex.action(
|
|
146
|
+
api.createApp,
|
|
147
|
+
withAuth(ctx, {
|
|
148
|
+
slug,
|
|
149
|
+
name: name ?? slug,
|
|
150
|
+
...(icon ? { icon } : {}),
|
|
151
|
+
...(description ? { description } : {}),
|
|
152
|
+
}),
|
|
153
|
+
),
|
|
154
|
+
);
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
case "install": {
|
|
158
|
+
const listingId = shift(ctx.args, "listing id");
|
|
159
|
+
out(
|
|
160
|
+
ctx,
|
|
161
|
+
await ctx.convex.action(api.install, withAuth(ctx, { listingId })),
|
|
162
|
+
);
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
165
|
+
case "publish": {
|
|
166
|
+
const slug = takeFlag(ctx.args, "--slug");
|
|
167
|
+
const name = takeFlag(ctx.args, "--name");
|
|
168
|
+
const description = takeFlag(ctx.args, "--description");
|
|
169
|
+
const icon = takeFlag(ctx.args, "--icon");
|
|
170
|
+
const visibility = takeFlag(ctx.args, "--visibility");
|
|
171
|
+
const version = takeFlag(ctx.args, "--version");
|
|
172
|
+
const appPath = firstPositional(ctx.args);
|
|
173
|
+
if (!appPath || !slug || !name) {
|
|
174
|
+
throw new AppsCliError(
|
|
175
|
+
'Usage: dench apps publish <appPath> --slug <slug> --name "My App"',
|
|
176
|
+
);
|
|
177
|
+
}
|
|
178
|
+
out(
|
|
179
|
+
ctx,
|
|
180
|
+
await ctx.convex.action(
|
|
181
|
+
api.publish,
|
|
182
|
+
withAuth(ctx, {
|
|
183
|
+
appPath,
|
|
184
|
+
slug,
|
|
185
|
+
name,
|
|
186
|
+
...(description ? { description } : {}),
|
|
187
|
+
...(icon ? { icon } : {}),
|
|
188
|
+
...(visibility ? { visibility } : {}),
|
|
189
|
+
...(version ? { version } : {}),
|
|
190
|
+
}),
|
|
191
|
+
),
|
|
192
|
+
);
|
|
193
|
+
return;
|
|
194
|
+
}
|
|
195
|
+
case "uninstall": {
|
|
196
|
+
const slug = shift(ctx.args, "app slug");
|
|
197
|
+
out(
|
|
198
|
+
ctx,
|
|
199
|
+
await ctx.convex.mutation(api.uninstall, withAuth(ctx, { slug })),
|
|
200
|
+
);
|
|
201
|
+
return;
|
|
202
|
+
}
|
|
203
|
+
case "enable":
|
|
204
|
+
case "disable": {
|
|
205
|
+
const slug = shift(ctx.args, "app slug");
|
|
206
|
+
await ctx.convex.mutation(
|
|
207
|
+
api.setEnabled,
|
|
208
|
+
withAuth(ctx, { slug, enabled: sub === "enable" }),
|
|
209
|
+
);
|
|
210
|
+
out(ctx, { ok: true, slug, enabled: sub === "enable" });
|
|
211
|
+
return;
|
|
212
|
+
}
|
|
213
|
+
case "pin":
|
|
214
|
+
case "unpin": {
|
|
215
|
+
const slug = shift(ctx.args, "app slug");
|
|
216
|
+
await ctx.convex.mutation(
|
|
217
|
+
api.setPinned,
|
|
218
|
+
withAuth(ctx, { slug, pinned: sub === "pin" }),
|
|
219
|
+
);
|
|
220
|
+
out(ctx, { ok: true, slug, pinned: sub === "pin" });
|
|
221
|
+
return;
|
|
222
|
+
}
|
|
223
|
+
default:
|
|
224
|
+
throw new AppsCliError(`Unknown apps subcommand: ${sub}`);
|
|
225
|
+
}
|
|
226
|
+
}
|
package/dench.ts
CHANGED
|
@@ -174,6 +174,12 @@ const api = {
|
|
|
174
174
|
},
|
|
175
175
|
files: {
|
|
176
176
|
listTree: makeFunctionReference<"query">("functions/files:listTree"),
|
|
177
|
+
getFileContent: makeFunctionReference<"query">(
|
|
178
|
+
"functions/files:getFileContent",
|
|
179
|
+
),
|
|
180
|
+
editFileFromUI: makeFunctionReference<"action">(
|
|
181
|
+
"functions/files:editFileFromUI",
|
|
182
|
+
),
|
|
177
183
|
deleteFile: makeFunctionReference<"mutation">(
|
|
178
184
|
"functions/files:deleteFile",
|
|
179
185
|
),
|
|
@@ -471,7 +477,7 @@ Usage:
|
|
|
471
477
|
dench approval reject <approvalId> [--evidence "User said no in chat"] [--json]
|
|
472
478
|
dench billing status [--json]
|
|
473
479
|
dench billing topup --amount 5 [--no-open] [--json]
|
|
474
|
-
dench
|
|
480
|
+
dench integrations [--json]
|
|
475
481
|
dench tool status [toolkit] [--json]
|
|
476
482
|
dench tool connect <toolkit> [--json]
|
|
477
483
|
dench tool search "create github issue" [--toolkit github] [--limit 20] [--compact] [--json]
|
|
@@ -611,7 +617,8 @@ Self-updating agent harness:
|
|
|
611
617
|
Help: dench identity help
|
|
612
618
|
|
|
613
619
|
External tools:
|
|
614
|
-
dench
|
|
620
|
+
dench integrations is an alias for dench tool status -- it lists connected apps.
|
|
621
|
+
dench apps is the App Store (create / install / publish / list), not Composio.
|
|
615
622
|
dench tool with no subcommand prints tool help.
|
|
616
623
|
Read-only FETCH, GET, LIST, SEARCH, READ, and FIND tools do not need manual approval.
|
|
617
624
|
|
|
@@ -667,10 +674,10 @@ function toolHelp() {
|
|
|
667
674
|
console.log(`Dench tool commands
|
|
668
675
|
|
|
669
676
|
Usage:
|
|
670
|
-
dench
|
|
677
|
+
dench integrations [--json]
|
|
671
678
|
dench tool status [toolkit] [--json]
|
|
672
679
|
dench tool connect <toolkit> [--callback-url <url>] [--json]
|
|
673
|
-
dench tool search "create github issue" [--toolkit github] [--limit 20] [--json]
|
|
680
|
+
dench tool search "create github issue" [--toolkit github] [--limit 20] [--compact] [--json]
|
|
674
681
|
dench tool run <composio_tool_slug> --args '{"key":"value"}' [--account <connectedAccountId>] [--json]
|
|
675
682
|
dench tool disconnect <connectionId> [--json]
|
|
676
683
|
|
|
@@ -682,7 +689,7 @@ Auth:
|
|
|
682
689
|
same way (the CLI exchanges the session for a gateway key on demand).
|
|
683
690
|
|
|
684
691
|
Notes:
|
|
685
|
-
dench
|
|
692
|
+
dench integrations is an alias for dench tool status.
|
|
686
693
|
dench tool connect prints the OAuth redirect URL for the human to open.
|
|
687
694
|
dench tool run output is redacted for display; pass --json for raw JSON.
|
|
688
695
|
Override the gateway base with DENCH_GATEWAY_URL or GATEWAY_URL.
|
|
@@ -3420,6 +3427,105 @@ async function runFilesRm() {
|
|
|
3420
3427
|
});
|
|
3421
3428
|
}
|
|
3422
3429
|
|
|
3430
|
+
/**
|
|
3431
|
+
* Print a workspace file's text.
|
|
3432
|
+
*
|
|
3433
|
+
* `download` cannot reach the magic files (IDENTITY.md, ORGANISATION.md, …)
|
|
3434
|
+
* because they render from agentConfig and have no storage blob, so this is
|
|
3435
|
+
* the only way to read them from outside a browser session.
|
|
3436
|
+
*/
|
|
3437
|
+
async function runFilesCat() {
|
|
3438
|
+
const runtime = await getRuntime();
|
|
3439
|
+
const apiKey = await requireFilesApiKey(runtime, "dench files cat");
|
|
3440
|
+
const targetRaw = positional(2);
|
|
3441
|
+
if (!targetRaw) {
|
|
3442
|
+
throw new CliError("Usage: dench files cat <path> [--json]", {
|
|
3443
|
+
code: "files_cat_usage",
|
|
3444
|
+
});
|
|
3445
|
+
}
|
|
3446
|
+
const target = normalizeFilesPath(targetRaw);
|
|
3447
|
+
const file = (await runtime.client.query(api.functions.files.getFileContent, {
|
|
3448
|
+
path: target,
|
|
3449
|
+
apiKey,
|
|
3450
|
+
} as never)) as {
|
|
3451
|
+
path: string;
|
|
3452
|
+
isDir: boolean;
|
|
3453
|
+
virtual: boolean;
|
|
3454
|
+
size: number | null;
|
|
3455
|
+
content: string | null;
|
|
3456
|
+
downloadable: boolean;
|
|
3457
|
+
} | null;
|
|
3458
|
+
|
|
3459
|
+
if (!file) {
|
|
3460
|
+
throw new CliError(`Not found in Convex: ${target}`, {
|
|
3461
|
+
code: "files_cat_not_found",
|
|
3462
|
+
nextActions: [
|
|
3463
|
+
"Run `dench files ls` on the parent directory to confirm.",
|
|
3464
|
+
"If the file only exists on the sandbox FS, run `dench fs sync --initial` first.",
|
|
3465
|
+
],
|
|
3466
|
+
});
|
|
3467
|
+
}
|
|
3468
|
+
if (file.isDir) {
|
|
3469
|
+
throw new CliError(`${target} is a directory`, {
|
|
3470
|
+
code: "files_cat_directory",
|
|
3471
|
+
nextActions: ["Run `dench files ls` to list its entries."],
|
|
3472
|
+
});
|
|
3473
|
+
}
|
|
3474
|
+
if (file.content === null) {
|
|
3475
|
+
throw new CliError(`No inline text for ${target}`, {
|
|
3476
|
+
code: "files_cat_not_text",
|
|
3477
|
+
nextActions: file.downloadable
|
|
3478
|
+
? ["Run `dench files download` to fetch the bytes."]
|
|
3479
|
+
: ["The file has no stored bytes yet."],
|
|
3480
|
+
});
|
|
3481
|
+
}
|
|
3482
|
+
|
|
3483
|
+
if (hasFlag("--json")) {
|
|
3484
|
+
print({ ok: true, ...file });
|
|
3485
|
+
return;
|
|
3486
|
+
}
|
|
3487
|
+
process.stdout.write(
|
|
3488
|
+
file.content.endsWith("\n") ? file.content : `${file.content}\n`,
|
|
3489
|
+
);
|
|
3490
|
+
}
|
|
3491
|
+
|
|
3492
|
+
/**
|
|
3493
|
+
* Replace a workspace file's text, reading the new content from stdin.
|
|
3494
|
+
*
|
|
3495
|
+
* `stage` copies from a sandbox path and `commitFileUpload` needs an upload
|
|
3496
|
+
* round-trip, so neither works for "change these few lines from a terminal".
|
|
3497
|
+
*/
|
|
3498
|
+
async function runFilesWrite() {
|
|
3499
|
+
const runtime = await getRuntime();
|
|
3500
|
+
const apiKey = await requireFilesApiKey(runtime, "dench files write");
|
|
3501
|
+
const targetRaw = positional(2);
|
|
3502
|
+
if (!targetRaw) {
|
|
3503
|
+
throw new CliError("Usage: dench files write <path> < file.md", {
|
|
3504
|
+
code: "files_write_usage",
|
|
3505
|
+
nextActions: ["Pipe the new contents in on stdin."],
|
|
3506
|
+
});
|
|
3507
|
+
}
|
|
3508
|
+
const target = normalizeFilesPath(targetRaw);
|
|
3509
|
+
const content = await new Response(Bun.stdin.stream()).text();
|
|
3510
|
+
|
|
3511
|
+
const result = (await runtime.client.action(
|
|
3512
|
+
api.functions.files.editFileFromUI,
|
|
3513
|
+
{
|
|
3514
|
+
path: target,
|
|
3515
|
+
content,
|
|
3516
|
+
apiKey,
|
|
3517
|
+
} as never,
|
|
3518
|
+
)) as { path: string; contentHash: string };
|
|
3519
|
+
|
|
3520
|
+
if (hasFlag("--json")) {
|
|
3521
|
+
print({ ok: true, ...result, bytes: content.length });
|
|
3522
|
+
return;
|
|
3523
|
+
}
|
|
3524
|
+
process.stdout.write(
|
|
3525
|
+
`dench files write: ${result.path} (${content.length}B)\n`,
|
|
3526
|
+
);
|
|
3527
|
+
}
|
|
3528
|
+
|
|
3423
3529
|
async function writeLocalFileBytes(localPath: string, bytes: Uint8Array) {
|
|
3424
3530
|
const parent = dirname(localPath);
|
|
3425
3531
|
if (parent && parent !== ".") {
|
|
@@ -3554,15 +3660,19 @@ async function runFilesCommand() {
|
|
|
3554
3660
|
if (!sub || sub === "help" || hasFlag("--help")) {
|
|
3555
3661
|
process.stdout.write(
|
|
3556
3662
|
[
|
|
3557
|
-
"dench files <ls|mv|rm|download> — workspace file ops that update the UI tree",
|
|
3663
|
+
"dench files <ls|cat|write|mv|rm|download> — workspace file ops that update the UI tree",
|
|
3558
3664
|
"",
|
|
3559
3665
|
" dench files ls [<path>] [--recursive] [--json]",
|
|
3666
|
+
" dench files cat <path> [--json]",
|
|
3667
|
+
" dench files write <path> < file.md (content on stdin)",
|
|
3560
3668
|
" dench files mv <src> <dst> [--json]",
|
|
3561
3669
|
" dench files rm <path> [--recursive] [--json]",
|
|
3562
3670
|
" dench files download <path> [<local-dest>] [--url] [--json]",
|
|
3563
3671
|
"",
|
|
3564
3672
|
"Paths are POSIX paths under /workspace; you can pass either form.",
|
|
3565
3673
|
"These mutate Convex directly (and the daemon syncs to the volume).",
|
|
3674
|
+
"cat prints inline text and is the only way to read the magic files",
|
|
3675
|
+
"(IDENTITY.md, ORGANISATION.md, …), which have no stored bytes.",
|
|
3566
3676
|
"download fetches canonical bytes from Convex (dirs recurse);",
|
|
3567
3677
|
"--url prints a short-lived signed URL instead of saving.",
|
|
3568
3678
|
].join("\n"),
|
|
@@ -3570,6 +3680,8 @@ async function runFilesCommand() {
|
|
|
3570
3680
|
return;
|
|
3571
3681
|
}
|
|
3572
3682
|
if (sub === "ls") return await runFilesLs();
|
|
3683
|
+
if (sub === "cat") return await runFilesCat();
|
|
3684
|
+
if (sub === "write") return await runFilesWrite();
|
|
3573
3685
|
if (sub === "mv") return await runFilesMv();
|
|
3574
3686
|
if (sub === "rm") return await runFilesRm();
|
|
3575
3687
|
if (sub === "download") return await runFilesDownload();
|
package/lib/command-registry.ts
CHANGED
|
@@ -1957,6 +1957,32 @@ const rawApiOperations = [
|
|
|
1957
1957
|
tokenArg: "apiKey",
|
|
1958
1958
|
}),
|
|
1959
1959
|
}),
|
|
1960
|
+
op({
|
|
1961
|
+
id: "files.content",
|
|
1962
|
+
group: "files",
|
|
1963
|
+
summary: "Read a workspace file's text.",
|
|
1964
|
+
cli: "dench files cat",
|
|
1965
|
+
method: "GET",
|
|
1966
|
+
path: "/files/content",
|
|
1967
|
+
requestSchema: noBody,
|
|
1968
|
+
responseSchema: anyResponse,
|
|
1969
|
+
backend: convex("query", "functions/files:getFileContent", "bearer", {
|
|
1970
|
+
tokenArg: "apiKey",
|
|
1971
|
+
}),
|
|
1972
|
+
}),
|
|
1973
|
+
op({
|
|
1974
|
+
id: "files.write",
|
|
1975
|
+
group: "files",
|
|
1976
|
+
summary: "Write a workspace file's text.",
|
|
1977
|
+
cli: "dench files write",
|
|
1978
|
+
method: "PUT",
|
|
1979
|
+
path: "/files/content",
|
|
1980
|
+
requestSchema: anyObject,
|
|
1981
|
+
responseSchema: anyResponse,
|
|
1982
|
+
backend: convex("action", "functions/files:editFileFromUI", "bearer", {
|
|
1983
|
+
tokenArg: "apiKey",
|
|
1984
|
+
}),
|
|
1985
|
+
}),
|
|
1960
1986
|
op({
|
|
1961
1987
|
id: "files.downloadUrl",
|
|
1962
1988
|
group: "files",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dench.com/cli",
|
|
3
|
-
"version": "2.7.
|
|
3
|
+
"version": "2.7.5",
|
|
4
4
|
"description": "Dench agent workspace CLI.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -32,6 +32,7 @@
|
|
|
32
32
|
"search.ts",
|
|
33
33
|
"image.ts",
|
|
34
34
|
"tools.ts",
|
|
35
|
+
"apps.ts",
|
|
35
36
|
"agentKind.ts",
|
|
36
37
|
"agent-config.ts",
|
|
37
38
|
"host.ts",
|
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
|
|
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 =
|
|
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
|
|