@dench.com/cli 0.4.0 → 0.4.2

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/dench.ts CHANGED
@@ -38,6 +38,7 @@ import {
38
38
  withCurrentSessionSelection,
39
39
  withSavedSession,
40
40
  } from "./session";
41
+ import { fetchConnectedAppsSummary, runToolCommand } from "./tools";
41
42
 
42
43
  type JsonRecord = Record<string, unknown>;
43
44
 
@@ -172,20 +173,6 @@ const api = {
172
173
  "functions/agentWorkspace:whatCanIDoHere",
173
174
  ),
174
175
  },
175
- agentToolsNode: {
176
- agentToolConnect: makeFunctionReference<"action">(
177
- "functions/agentToolsNode:agentToolConnect",
178
- ),
179
- agentToolRun: makeFunctionReference<"action">(
180
- "functions/agentToolsNode:agentToolRun",
181
- ),
182
- agentToolSearch: makeFunctionReference<"action">(
183
- "functions/agentToolsNode:agentToolSearch",
184
- ),
185
- agentToolStatus: makeFunctionReference<"action">(
186
- "functions/agentToolsNode:agentToolStatus",
187
- ),
188
- },
189
176
  files: {
190
177
  listTree: makeFunctionReference<"query">("functions/files:listTree"),
191
178
  deleteFile: makeFunctionReference<"mutation">(
@@ -362,7 +349,7 @@ function normalizeCliError(error: unknown): CliError {
362
349
  return new CliError(message);
363
350
  }
364
351
 
365
- function parseJsonObjectOption(name: string) {
352
+ function _parseJsonObjectOption(name: string) {
366
353
  const raw = option(name);
367
354
  if (!raw) return {};
368
355
  let parsed: unknown;
@@ -460,8 +447,9 @@ Usage:
460
447
  CRM (Daytona-native rewrite):
461
448
  dench crm objects <list|get|create|update|rename|delete> ...
462
449
  dench crm fields <list|create|update|delete|reorder> <object> ...
463
- dench crm entries <list|get|create|update|delete|bulk-delete> <object> ...
450
+ dench crm entries <list|get|create|create-many|update|update-many|delete|bulk-delete> <object> ...
464
451
  dench crm cells <get|set|append> <object> <entryId> <field> [value]
452
+ dench crm cells set-many <object> --updates '[...]'
465
453
  dench crm query <object> --where '...' --select ... --sort ... --limit N
466
454
  dench crm sql 'SELECT "Name" FROM lead WHERE ... LIMIT 10'
467
455
  dench crm aggregate <object> --field <name>
@@ -475,6 +463,7 @@ CRM (Daytona-native rewrite):
475
463
  dench crm docs <list|create|link>
476
464
  dench crm actions <list|run> ...
477
465
  dench crm batch --file ops.jsonl
466
+ Batch write helpers: entries create-many/update-many, cells set-many
478
467
  Help: dench crm help
479
468
 
480
469
  Live web search (Exa via the Dench Cloud Gateway, auths with DENCH_API_KEY):
@@ -594,16 +583,22 @@ function toolHelp() {
594
583
  Usage:
595
584
  dench apps [--json]
596
585
  dench tool status [toolkit] [--json]
597
- dench tool connect <toolkit> [--json]
598
- dench tool search "create github issue" [--toolkit github] [--limit 20] [--compact] [--json]
599
- dench tool run <composio_tool_slug> --args '{"key":"value"}' [--account <connected_account_id>] [--approval <approvalId>] [--json]
586
+ dench tool connect <toolkit> [--callback-url <url>] [--json]
587
+ dench tool search "create github issue" [--toolkit github] [--limit 20] [--json]
588
+ dench tool run <composio_tool_slug> --args '{"key":"value"}' [--account <connectedAccountId>] [--json]
589
+ dench tool disconnect <connectionId> [--json]
590
+
591
+ Auth:
592
+ All commands authenticate with DENCH_API_KEY (Bearer) directly to the
593
+ Dench Cloud Gateway. No \`dench login\` agent session is required —
594
+ inside a Dench sandbox the key is baked into the env automatically;
595
+ outside, export DENCH_API_KEY=<key> first.
600
596
 
601
597
  Notes:
602
- dench apps lists all connected apps. It returns the same shape as dench tool status --json.
603
- dench tool search prints compact ranked results by default. Add --json only when you need full schemas.
604
- Run clear read-only FETCH, GET, LIST, SEARCH, READ, and FIND tools through Dench without manual approval.
605
- Non-JSON tool run output redacts likely OTP codes, secrets, and tokens for display.
606
- Dench enforces the final policy and returns requiresApproval when approval is needed.
598
+ dench apps is an alias for dench tool status.
599
+ dench tool connect prints the OAuth redirect URL for the human to open.
600
+ dench tool run output is redacted for display; pass --json for raw JSON.
601
+ Override the gateway base with DENCH_GATEWAY_URL or GATEWAY_URL.
607
602
  `);
608
603
  }
609
604
 
@@ -745,8 +740,10 @@ function appsHelp() {
745
740
  Usage:
746
741
  dench apps [--json]
747
742
 
748
- Lists all connected external apps for the current Dench session.
749
- This is an alias for dench tool status [--json].
743
+ Lists all external apps connected to this organization (Slack, Gmail,
744
+ GitHub, etc.). Authenticates with DENCH_API_KEY (Bearer) directly to the
745
+ Dench Cloud Gateway — no \`dench login\` agent session required. Alias
746
+ for \`dench tool status\`.
750
747
  `);
751
748
  }
752
749
 
@@ -1899,10 +1896,7 @@ function requireSessionRuntime(
1899
1896
  * (`dench_*` / `gw_sk_*` / etc.) baked into sandbox envVars. The
1900
1897
  * server-side `requireCrmAccess` helper dispatches on the prefix.
1901
1898
  */
1902
- type AuthenticatedRuntime = Extract<
1903
- Runtime,
1904
- { mode: "session" | "apiKey" }
1905
- >;
1899
+ type AuthenticatedRuntime = Extract<Runtime, { mode: "session" | "apiKey" }>;
1906
1900
 
1907
1901
  function requireAuthenticatedRuntime(
1908
1902
  runtime: Runtime,
@@ -1950,25 +1944,6 @@ function rejectApiKeyForAgentCommand(
1950
1944
  }
1951
1945
  }
1952
1946
 
1953
- async function printToolStatus(runtime: Runtime, toolkit?: string) {
1954
- const sessionRuntime = requireSessionRuntime(runtime, "dench tool status");
1955
- try {
1956
- print(
1957
- await sessionRuntime.client.action(
1958
- api.functions.agentToolsNode.agentToolStatus,
1959
- {
1960
- sessionToken: sessionRuntime.sessionToken,
1961
- toolkit,
1962
- },
1963
- ),
1964
- );
1965
- } catch (error) {
1966
- const unavailable = toolGatewayUnavailable(error, { toolkit });
1967
- if (!unavailable) throw error;
1968
- print(json ? unavailable : formatToolUnavailable(unavailable));
1969
- }
1970
- }
1971
-
1972
1947
  function asRecord(value: unknown): JsonRecord | undefined {
1973
1948
  if (!value || typeof value !== "object" || Array.isArray(value)) {
1974
1949
  return undefined;
@@ -2004,7 +1979,7 @@ function numberField(record: JsonRecord | undefined, ...names: string[]) {
2004
1979
  return undefined;
2005
1980
  }
2006
1981
 
2007
- function booleanField(record: JsonRecord | undefined, ...names: string[]) {
1982
+ function _booleanField(record: JsonRecord | undefined, ...names: string[]) {
2008
1983
  if (!record) return undefined;
2009
1984
  for (const name of names) {
2010
1985
  const value = record[name];
@@ -2027,94 +2002,6 @@ function isMissingConvexFunction(error: unknown) {
2027
2002
  return /Could not find public function/i.test(errorMessage(error));
2028
2003
  }
2029
2004
 
2030
- function toolGatewayUnavailable(error: unknown, context: JsonRecord = {}) {
2031
- const message = errorMessage(error);
2032
- if (!/gateway API key|callGateway|Composio/i.test(message)) {
2033
- return null;
2034
- }
2035
- const invalidKey = /Invalid gateway API key/i.test(message);
2036
- const query = stringField(context, "query");
2037
- const toolkit = stringField(context, "toolkit");
2038
- const retryCommand = query
2039
- ? `Retry dench tool search ${JSON.stringify(query)}${toolkit ? ` --toolkit ${toolkit}` : ""} after the gateway key is fixed.`
2040
- : "Retry dench apps --json after the gateway key is fixed.";
2041
- return {
2042
- ok: false,
2043
- available: false,
2044
- code: invalidKey ? "invalid_gateway_api_key" : "tool_gateway_unavailable",
2045
- reason: invalidKey
2046
- ? "The workspace gateway key is invalid or out of sync."
2047
- : "The connected-app gateway is unavailable.",
2048
- ...context,
2049
- nextActions: [
2050
- "Ask a workspace admin to repair or rotate the Dench gateway key.",
2051
- retryCommand,
2052
- ],
2053
- };
2054
- }
2055
-
2056
- function formatToolUnavailable(payload: JsonRecord) {
2057
- return [
2058
- "Connected apps unavailable.",
2059
- `Reason: ${stringField(payload, "reason") ?? "tool gateway unavailable"}`,
2060
- "",
2061
- "Next actions:",
2062
- ...(
2063
- (Array.isArray(payload.nextActions)
2064
- ? payload.nextActions
2065
- : []) as unknown[]
2066
- )
2067
- .filter((item): item is string => typeof item === "string")
2068
- .map((item) => ` - ${item}`),
2069
- ].join("\n");
2070
- }
2071
-
2072
- const READ_ONLY_TOOL_ACTIONS = new Set([
2073
- "FETCH",
2074
- "GET",
2075
- "LIST",
2076
- "SEARCH",
2077
- "READ",
2078
- "FIND",
2079
- ]);
2080
-
2081
- function toolSlugTokens(toolSlug: string) {
2082
- return toolSlug
2083
- .toUpperCase()
2084
- .split(/[^A-Z0-9]+/)
2085
- .filter(Boolean);
2086
- }
2087
-
2088
- function toolApprovalHint(toolSlug: string | undefined) {
2089
- if (!toolSlug) return "approval unknown";
2090
- const readOnly = toolSlugTokens(toolSlug).find((token) =>
2091
- READ_ONLY_TOOL_ACTIONS.has(token),
2092
- );
2093
- return readOnly ? `read-only (${readOnly})` : "approval likely";
2094
- }
2095
-
2096
- function compactConnection(connection: JsonRecord) {
2097
- return {
2098
- toolkit: stringField(connection, "toolkit") ?? "unknown",
2099
- status: stringField(connection, "status") ?? "unknown",
2100
- connectedAccountId:
2101
- stringField(connection, "connectedAccountId", "connected_account_id") ??
2102
- stringField(connection, "id"),
2103
- accountLabel:
2104
- stringField(connection, "accountLabel", "account_label", "label") ?? null,
2105
- };
2106
- }
2107
-
2108
- function summarizeConnectedApps(value: unknown) {
2109
- const record = asRecord(value);
2110
- const connections = asRecordArray(record?.connections).map(compactConnection);
2111
- return {
2112
- available: true,
2113
- count: connections.length,
2114
- connections,
2115
- };
2116
- }
2117
-
2118
2005
  function workspaceSlugFromContext(context: JsonRecord) {
2119
2006
  const workspace = asRecord(context.workspace);
2120
2007
  return (
@@ -2141,41 +2028,12 @@ function nextContextCommands() {
2141
2028
  ];
2142
2029
  }
2143
2030
 
2144
- async function connectedAppsContext(runtime: Runtime) {
2145
- if (runtime.mode !== "session") {
2146
- return {
2147
- available: false,
2148
- reason: "Connected apps require a logged-in Dench agent session.",
2149
- count: 0,
2150
- connections: [],
2151
- };
2152
- }
2153
-
2154
- try {
2155
- return summarizeConnectedApps(
2156
- await runtime.client.action(
2157
- api.functions.agentToolsNode.agentToolStatus,
2158
- {
2159
- sessionToken: runtime.sessionToken,
2160
- },
2161
- ),
2162
- );
2163
- } catch (error) {
2164
- const unavailable = toolGatewayUnavailable(error);
2165
- if (unavailable) {
2166
- return {
2167
- ...unavailable,
2168
- count: 0,
2169
- connections: [],
2170
- };
2171
- }
2172
- return {
2173
- available: false,
2174
- error: errorMessage(error),
2175
- count: 0,
2176
- connections: [],
2177
- };
2178
- }
2031
+ async function connectedAppsContext(_runtime: Runtime) {
2032
+ // Reach the Dench gateway directly with DENCH_API_KEY (mirrors the
2033
+ // denchclaw pattern). This works for every Runtime mode — session,
2034
+ // apiKey, dev — because the gateway only cares about the bearer key,
2035
+ // not which Convex session is calling.
2036
+ return fetchConnectedAppsSummary();
2179
2037
  }
2180
2038
 
2181
2039
  async function buildContext(runtime: Runtime) {
@@ -2414,223 +2272,6 @@ function formatContext(context: JsonRecord) {
2414
2272
  return lines.join("\n");
2415
2273
  }
2416
2274
 
2417
- function toolkitSlugFromTool(tool: JsonRecord) {
2418
- const toolkit = tool.toolkit;
2419
- if (typeof toolkit === "string") return toolkit;
2420
- const toolkitRecord = asRecord(toolkit);
2421
- return (
2422
- stringField(toolkitRecord, "slug", "name") ??
2423
- stringField(tool, "toolkit_slug")
2424
- );
2425
- }
2426
-
2427
- function connectedToolkitSet(data: JsonRecord | undefined) {
2428
- const connected = new Set<string>();
2429
- const connectedToolkits = Array.isArray(data?.connected_toolkits)
2430
- ? data?.connected_toolkits
2431
- : [];
2432
- for (const toolkit of connectedToolkits) {
2433
- const value = stringValue(toolkit);
2434
- if (value) connected.add(value.toLowerCase());
2435
- }
2436
- for (const status of asRecordArray(data?.toolkit_connection_statuses)) {
2437
- const toolkit = stringField(status, "toolkit");
2438
- const isConnected = booleanField(status, "has_active_connection");
2439
- if (toolkit && isConnected) connected.add(toolkit.toLowerCase());
2440
- }
2441
- return connected;
2442
- }
2443
-
2444
- function searchToolItems(searchResult: unknown) {
2445
- const root = asRecord(searchResult);
2446
- const data = asRecord(root?.data) ?? root;
2447
- const directItems = asRecordArray(data?.items);
2448
- if (directItems.length > 0) {
2449
- return { data, items: directItems };
2450
- }
2451
-
2452
- const schemas = asRecord(data?.tool_schemas);
2453
- if (!schemas) return { data, items: [] };
2454
- return {
2455
- data,
2456
- items: Object.entries(schemas).flatMap<JsonRecord>(([slug, value]) => {
2457
- const tool = asRecord(value);
2458
- return tool
2459
- ? [{ ...tool, tool_slug: stringField(tool, "tool_slug") ?? slug }]
2460
- : [];
2461
- }),
2462
- };
2463
- }
2464
-
2465
- function compactToolSearchResult(searchResult: unknown) {
2466
- const root = asRecord(searchResult);
2467
- const { data, items } = searchToolItems(searchResult);
2468
- const connected = connectedToolkitSet(data);
2469
- const tools = items.map((tool) => {
2470
- const toolkit = toolkitSlugFromTool(tool);
2471
- const connectionStatus = asRecord(tool.connection_status);
2472
- const isConnected =
2473
- booleanField(connectionStatus, "is_connected") ??
2474
- (toolkit ? connected.has(toolkit.toLowerCase()) : undefined);
2475
- const slug = stringField(tool, "slug", "tool_slug") ?? "unknown_tool";
2476
- return {
2477
- slug,
2478
- name: stringField(tool, "display_name", "name") ?? slug,
2479
- toolkit: toolkit ?? "unknown",
2480
- approval: toolApprovalHint(slug),
2481
- connected:
2482
- isConnected === undefined
2483
- ? "connection unknown"
2484
- : isConnected
2485
- ? "connected"
2486
- : "not connected",
2487
- description: compactLine(tool.description) ?? null,
2488
- };
2489
- });
2490
-
2491
- return {
2492
- ok: root?.ok ?? true,
2493
- query: stringField(root, "query") ?? stringField(data, "query") ?? null,
2494
- returned: tools.length,
2495
- total:
2496
- numberField(data, "total_items", "total", "count") ??
2497
- numberField(root, "total_items", "total", "count") ??
2498
- tools.length,
2499
- tools,
2500
- };
2501
- }
2502
-
2503
- function formatToolSearchResult(searchResult: unknown) {
2504
- const summary = compactToolSearchResult(searchResult);
2505
- const query = summary.query ? ` for "${summary.query}"` : "";
2506
- if (summary.tools.length === 0) {
2507
- return `No matching tools found${query}.\nUse --json if you need the raw provider response.`;
2508
- }
2509
-
2510
- const lines = [`Top tools${query}:`];
2511
- for (const [index, tool] of summary.tools.entries()) {
2512
- lines.push(
2513
- `${index + 1}. ${tool.slug} - ${tool.name}`,
2514
- ` toolkit: ${tool.toolkit} | ${tool.approval} | ${tool.connected}`,
2515
- );
2516
- if (tool.description) {
2517
- lines.push(` ${tool.description}`);
2518
- }
2519
- }
2520
- lines.push(
2521
- "",
2522
- "Use dench tool run <slug> --args '{...}' when ready.",
2523
- "Use --json only when you need full schemas or raw arguments.",
2524
- );
2525
- return lines.join("\n");
2526
- }
2527
-
2528
- function printToolSearchResult(searchResult: unknown) {
2529
- print(json ? searchResult : formatToolSearchResult(searchResult));
2530
- }
2531
-
2532
- const SENSITIVE_KEY_RE =
2533
- /(?:otp|oneTime|verification|security|passcode|password|secret|api[_-]?key|access[_-]?token|refresh[_-]?token|authorization)/i;
2534
-
2535
- function redactSensitiveString(value: string) {
2536
- return value
2537
- .replace(
2538
- /\b((?:one[-\s]?time|verification|security|login|sign[-\s]?in|auth(?:entication)?|2fa|mfa|otp|passcode|code)[^.\n\r]{0,80}?)(\d[\d\s-]{2,10}\d)\b/gi,
2539
- "$1[redacted-code]",
2540
- )
2541
- .replace(
2542
- /\b((?:api[_\s-]?key|secret|token|password)[^.\n\r]{0,60}?)([A-Za-z0-9_=-]{12,})\b/gi,
2543
- "$1[redacted-secret]",
2544
- );
2545
- }
2546
-
2547
- function redactSensitiveDisplay(value: unknown, key?: string): unknown {
2548
- if (typeof value === "string") {
2549
- return key && SENSITIVE_KEY_RE.test(key)
2550
- ? "[redacted]"
2551
- : redactSensitiveString(value);
2552
- }
2553
- if (typeof value === "number" && key && SENSITIVE_KEY_RE.test(key)) {
2554
- return "[redacted]";
2555
- }
2556
- if (Array.isArray(value)) {
2557
- return value.map((item) => redactSensitiveDisplay(item));
2558
- }
2559
- const record = asRecord(value);
2560
- if (!record) return value;
2561
-
2562
- return Object.fromEntries(
2563
- Object.entries(record).map(([entryKey, entryValue]) => [
2564
- entryKey,
2565
- redactSensitiveDisplay(entryValue, entryKey),
2566
- ]),
2567
- );
2568
- }
2569
-
2570
- function connectionUrlFromToolConnect(result: unknown) {
2571
- const record = asRecord(result);
2572
- const data = asRecord(record?.data);
2573
- return (
2574
- stringField(record, "connectUrl", "redirect_url", "redirectUrl", "url") ??
2575
- stringField(data, "redirect_url", "redirectUrl", "url")
2576
- );
2577
- }
2578
-
2579
- function withToolConnectSummary(result: unknown) {
2580
- const record = asRecord(result);
2581
- if (!record) return result;
2582
- const toolkit = stringField(record, "toolkit") ?? "app";
2583
- const connectUrl = connectionUrlFromToolConnect(record);
2584
- if (!connectUrl) return record;
2585
- return {
2586
- ...record,
2587
- connectUrl,
2588
- linkText: `Connect ${toolkit} here`,
2589
- message: `Connect ${toolkit} here: ${connectUrl}`,
2590
- };
2591
- }
2592
-
2593
- function formatToolConnectResult(result: unknown) {
2594
- const record = asRecord(withToolConnectSummary(result));
2595
- if (!record) return String(result);
2596
- const toolkit = stringField(record, "toolkit") ?? "app";
2597
- const connectUrl = stringField(record, "connectUrl");
2598
- if (connectUrl) {
2599
- return `Connect ${toolkit} here: ${connectUrl}`;
2600
- }
2601
- return JSON.stringify(record, null, 2);
2602
- }
2603
-
2604
- function withToolRunGuidance(runtime: Runtime, result: unknown) {
2605
- const record = asRecord(result);
2606
- if (!record || record.requiresApproval !== true) return result;
2607
- const approvalId = stringField(record, "approvalId");
2608
- const statusContext =
2609
- runtime.mode === "session" || runtime.mode === "apiKey"
2610
- ? workspaceUrls(runtime.host, runtime.organization?.slug)
2611
- : {};
2612
- const approvalCommand = approvalId
2613
- ? `dench approval approve ${approvalId} --evidence "User said yes in chat" --json`
2614
- : undefined;
2615
- return {
2616
- ...record,
2617
- ...statusContext,
2618
- ...(approvalCommand ? { approvalCommand } : {}),
2619
- nextActions: [
2620
- "Ask the human for an explicit yes/no in chat.",
2621
- ...(approvalCommand ? [`If yes, run: ${approvalCommand}`] : []),
2622
- approvalId
2623
- ? `Rerun the exact same dench tool run command with --approval ${approvalId}.`
2624
- : "Rerun the exact same dench tool run command after approval.",
2625
- ],
2626
- };
2627
- }
2628
-
2629
- function printToolRunResult(runtime: Runtime, result: unknown) {
2630
- const guided = withToolRunGuidance(runtime, result);
2631
- print(json ? guided : redactSensitiveDisplay(guided));
2632
- }
2633
-
2634
2275
  async function devOverview(runtime: Awaited<ReturnType<typeof getRuntime>>) {
2635
2276
  if (runtime.mode !== "dev") {
2636
2277
  throw new Error("Not in dev mode");
@@ -2823,10 +2464,12 @@ async function runFsCommand() {
2823
2464
  function normalizeFilesPath(input: string): string {
2824
2465
  if (!input || input === "/") return "/";
2825
2466
  let cleaned = input.replace(/\\/g, "/").replace(/\/+/g, "/");
2826
- if (cleaned.startsWith("/workspace/")) cleaned = cleaned.slice("/workspace".length);
2467
+ if (cleaned.startsWith("/workspace/"))
2468
+ cleaned = cleaned.slice("/workspace".length);
2827
2469
  else if (cleaned === "/workspace") cleaned = "/";
2828
2470
  if (!cleaned.startsWith("/")) cleaned = `/${cleaned}`;
2829
- if (cleaned.length > 1 && cleaned.endsWith("/")) cleaned = cleaned.slice(0, -1);
2471
+ if (cleaned.length > 1 && cleaned.endsWith("/"))
2472
+ cleaned = cleaned.slice(0, -1);
2830
2473
  return cleaned;
2831
2474
  }
2832
2475
 
@@ -3004,9 +2647,9 @@ async function runFilesMv() {
3004
2647
  }
3005
2648
 
3006
2649
  const sourceRows = await listConvexTreeRecursive(runtime, apiKey, from);
3007
- const sourceSelf = (await listConvexTreeAt(runtime, apiKey, parentOfPath(from))).find(
3008
- (row) => row.path === from,
3009
- );
2650
+ const sourceSelf = (
2651
+ await listConvexTreeAt(runtime, apiKey, parentOfPath(from))
2652
+ ).find((row) => row.path === from);
3010
2653
  if (!sourceSelf && sourceRows.length === 0) {
3011
2654
  throw new CliError(`Source not found in Convex: ${from}`, {
3012
2655
  code: "files_mv_not_found",
@@ -3068,7 +2711,11 @@ async function runFilesRm() {
3068
2711
  const target = normalizeFilesPath(targetRaw);
3069
2712
  const recursive = hasFlag("--recursive") || hasFlag("-r");
3070
2713
 
3071
- const parentRows = await listConvexTreeAt(runtime, apiKey, parentOfPath(target));
2714
+ const parentRows = await listConvexTreeAt(
2715
+ runtime,
2716
+ apiKey,
2717
+ parentOfPath(target),
2718
+ );
3072
2719
  const self = parentRows.find((row) => row.path === target);
3073
2720
  if (!self) {
3074
2721
  if (hasFlag("--json")) {
@@ -3615,6 +3262,25 @@ async function main() {
3615
3262
  return;
3616
3263
  }
3617
3264
 
3265
+ if (command === "apps") {
3266
+ // `dench apps` is an alias for `dench tool status`. Both speak
3267
+ // straight HTTP to the Dench Cloud Gateway with DENCH_API_KEY —
3268
+ // mirrors the denchclaw pattern. No Convex session involved.
3269
+ await runToolCommand({ args: ["status"], jsonOutput: json });
3270
+ return;
3271
+ }
3272
+
3273
+ if (command === "tool") {
3274
+ // `dench tool {status,connect,search,run,disconnect}` is the model-facing
3275
+ // composio surface and only needs DENCH_API_KEY (Bearer) on the
3276
+ // gateway. Bypasses requireSessionRuntime entirely so it works in
3277
+ // every sandbox / API-key context.
3278
+ const subArgs = stripRuntimeFlags(args.slice(args.indexOf("tool") + 1));
3279
+ const filteredSubArgs = subArgs.filter((a) => a !== "--json");
3280
+ await runToolCommand({ args: filteredSubArgs, jsonOutput: json });
3281
+ return;
3282
+ }
3283
+
3618
3284
  if (command === "chat") {
3619
3285
  const subArgs = args.slice(args.indexOf("chat") + 1);
3620
3286
  const sub = subArgs[0];
@@ -3865,11 +3531,6 @@ async function main() {
3865
3531
  return;
3866
3532
  }
3867
3533
 
3868
- if (command === "apps") {
3869
- await printToolStatus(runtime);
3870
- return;
3871
- }
3872
-
3873
3534
  if (command === "context") {
3874
3535
  const context = await buildContext(runtime);
3875
3536
  print(json ? context : formatContext(context));
@@ -4022,9 +3683,7 @@ async function main() {
4022
3683
  return;
4023
3684
  }
4024
3685
  if (runtime.mode === "apiKey") {
4025
- throw loginRequiredError(
4026
- "dench tasks (agent-scoped task list)",
4027
- );
3686
+ throw loginRequiredError("dench tasks (agent-scoped task list)");
4028
3687
  }
4029
3688
  const data = await devOverview(runtime);
4030
3689
  print(data.tasks);
@@ -4314,79 +3973,6 @@ async function main() {
4314
3973
  return;
4315
3974
  }
4316
3975
 
4317
- if (command === "tool") {
4318
- const sessionRuntime = requireSessionRuntime(runtime, "dench tool");
4319
-
4320
- if (subcommand === "status") {
4321
- await printToolStatus(sessionRuntime, positional(2));
4322
- return;
4323
- }
4324
-
4325
- if (subcommand === "connect") {
4326
- const toolkit = positional(2);
4327
- if (!toolkit) throw new Error("Missing toolkit");
4328
- const result = await sessionRuntime.client.action(
4329
- api.functions.agentToolsNode.agentToolConnect,
4330
- {
4331
- sessionToken: sessionRuntime.sessionToken,
4332
- toolkit,
4333
- callbackUrl: option("--callback-url"),
4334
- },
4335
- );
4336
- print(
4337
- json ? withToolConnectSummary(result) : formatToolConnectResult(result),
4338
- );
4339
- return;
4340
- }
4341
-
4342
- if (subcommand === "search") {
4343
- const query = positional(2);
4344
- if (!query) throw new Error("Missing search query");
4345
- try {
4346
- printToolSearchResult(
4347
- await sessionRuntime.client.action(
4348
- api.functions.agentToolsNode.agentToolSearch,
4349
- {
4350
- sessionToken: sessionRuntime.sessionToken,
4351
- query,
4352
- toolkit: option("--toolkit"),
4353
- limit: parseNumberOption("--limit"),
4354
- },
4355
- ),
4356
- );
4357
- } catch (error) {
4358
- const unavailable = toolGatewayUnavailable(error, {
4359
- query,
4360
- toolkit: option("--toolkit"),
4361
- });
4362
- if (!unavailable) throw error;
4363
- print(json ? unavailable : formatToolUnavailable(unavailable));
4364
- }
4365
- return;
4366
- }
4367
-
4368
- if (subcommand === "run") {
4369
- const toolSlug = positional(2);
4370
- if (!toolSlug) throw new Error("Missing Composio tool slug");
4371
- printToolRunResult(
4372
- sessionRuntime,
4373
- await sessionRuntime.client.action(
4374
- api.functions.agentToolsNode.agentToolRun,
4375
- {
4376
- sessionToken: sessionRuntime.sessionToken,
4377
- toolSlug,
4378
- arguments: parseJsonObjectOption("--args"),
4379
- connectedAccountId: option("--account"),
4380
- approvalId: option("--approval") as never,
4381
- },
4382
- ),
4383
- );
4384
- return;
4385
- }
4386
-
4387
- throw new Error(`Unknown tool command: ${filteredArgs.join(" ")}`);
4388
- }
4389
-
4390
3976
  if (
4391
3977
  command === "approval" &&
4392
3978
  (subcommand === "approve" || subcommand === "reject")