@dench.com/cli 0.3.8 → 0.4.1

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.
Files changed (5) hide show
  1. package/chat.ts +215 -8
  2. package/cron.ts +8 -4
  3. package/dench.ts +166 -503
  4. package/package.json +2 -1
  5. package/tools.ts +808 -0
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;
@@ -492,6 +479,10 @@ Past chat threads (own + shared in this workspace):
492
479
  dench chat list [--query <substr>] [--limit N] [--include-archived] [--json]
493
480
  dench chat search "<query>" [--limit N] [--include-current] [--json]
494
481
  dench chat read <thread-id> [--limit N] [--json]
482
+ dench chat rename <thread-id> <new-title> [--json]
483
+ dench chat rename --from-stdin [--json] (batch via JSON {items:[...]})
484
+ dench chat delete <thread-id> [<thread-id>...] [--yes] [--json]
485
+ dench chat delete --ids id1,id2 [--yes] [--json] (PERMANENTLY deletes threads)
495
486
 
496
487
  Spawn a new chat-turn workflow (same as the web UI):
497
488
  dench chat new "<prompt>" [--model <m>] [--file-context <path>] [--visibility private|shared] [--title T] [--yolo] [--follow] [--json]
@@ -534,6 +525,20 @@ Subagents (Daytona-native rewrite):
534
525
 
535
526
  dench --version
536
527
 
528
+ Self-updating agent harness (OpenClaw parity):
529
+ dench identity show | edit IDENTITY.md (org-wide)
530
+ dench organisation show | edit ORGANISATION.md
531
+ dench user show | edit USER.md (per-member)
532
+ dench tools show | edit TOOLS.md notes section
533
+ dench mem show | append "<text>" | set MEMORY.md aggregate
534
+ dench heartbeat status | enable | disable
535
+ | interval <30m|1h|…> | instructions
536
+ dench bootstrap status | complete | reopen | template
537
+ dench model default <id|clear>
538
+ dench model thread <threadId> <id|clear>
539
+ dench daily today | show <YYYY-MM-DD>
540
+ Help: dench identity help
541
+
537
542
  External tools:
538
543
  dench apps is an alias for dench tool status -- it lists connected apps.
539
544
  dench tool with no subcommand prints tool help.
@@ -576,16 +581,22 @@ function toolHelp() {
576
581
  Usage:
577
582
  dench apps [--json]
578
583
  dench tool status [toolkit] [--json]
579
- dench tool connect <toolkit> [--json]
580
- dench tool search "create github issue" [--toolkit github] [--limit 20] [--compact] [--json]
581
- dench tool run <composio_tool_slug> --args '{"key":"value"}' [--account <connected_account_id>] [--approval <approvalId>] [--json]
584
+ dench tool connect <toolkit> [--callback-url <url>] [--json]
585
+ dench tool search "create github issue" [--toolkit github] [--limit 20] [--json]
586
+ dench tool run <composio_tool_slug> --args '{"key":"value"}' [--account <connectedAccountId>] [--json]
587
+ dench tool disconnect <connectionId> [--json]
588
+
589
+ Auth:
590
+ All commands authenticate with DENCH_API_KEY (Bearer) directly to the
591
+ Dench Cloud Gateway. No \`dench login\` agent session is required —
592
+ inside a Dench sandbox the key is baked into the env automatically;
593
+ outside, export DENCH_API_KEY=<key> first.
582
594
 
583
595
  Notes:
584
- dench apps lists all connected apps. It returns the same shape as dench tool status --json.
585
- dench tool search prints compact ranked results by default. Add --json only when you need full schemas.
586
- Run clear read-only FETCH, GET, LIST, SEARCH, READ, and FIND tools through Dench without manual approval.
587
- Non-JSON tool run output redacts likely OTP codes, secrets, and tokens for display.
588
- Dench enforces the final policy and returns requiresApproval when approval is needed.
596
+ dench apps is an alias for dench tool status.
597
+ dench tool connect prints the OAuth redirect URL for the human to open.
598
+ dench tool run output is redacted for display; pass --json for raw JSON.
599
+ Override the gateway base with DENCH_GATEWAY_URL or GATEWAY_URL.
589
600
  `);
590
601
  }
591
602
 
@@ -727,8 +738,10 @@ function appsHelp() {
727
738
  Usage:
728
739
  dench apps [--json]
729
740
 
730
- Lists all connected external apps for the current Dench session.
731
- This is an alias for dench tool status [--json].
741
+ Lists all external apps connected to this organization (Slack, Gmail,
742
+ GitHub, etc.). Authenticates with DENCH_API_KEY (Bearer) directly to the
743
+ Dench Cloud Gateway — no \`dench login\` agent session required. Alias
744
+ for \`dench tool status\`.
732
745
  `);
733
746
  }
734
747
 
@@ -1794,30 +1807,6 @@ async function getRuntime() {
1794
1807
  });
1795
1808
  }
1796
1809
 
1797
- // Sandbox session-mode path: workflow minting provisions a real
1798
- // dch_agent_* token at sandbox-create time. Prefer it over apiKey so
1799
- // agentWorkspace.* commands (Composio tools) work inside sandboxes.
1800
- const sandboxAgentToken = process.env.DENCH_AGENT_SESSION_TOKEN?.trim();
1801
- const sandboxConvexUrl = resolveApiKeyConvexUrl();
1802
- if (sandboxAgentToken && sandboxConvexUrl) {
1803
- const orgId = process.env.DENCH_ORG_ID?.trim();
1804
- const orgSlug = process.env.DENCH_ORG_SLUG?.trim();
1805
- const apiHost =
1806
- process.env.DENCH_API_URL?.trim() ||
1807
- process.env.DENCH_INTERNAL_BASE?.trim() ||
1808
- host;
1809
- return {
1810
- mode: "session" as const,
1811
- host: apiHost,
1812
- client: new ConvexHttpClient(sandboxConvexUrl),
1813
- sessionToken: sandboxAgentToken,
1814
- organization:
1815
- orgId && orgSlug
1816
- ? { id: orgId, name: orgSlug, slug: orgSlug }
1817
- : undefined,
1818
- };
1819
- }
1820
-
1821
1810
  // Sandbox / automation path: the unified Dench API key + Convex URL
1822
1811
  // baked into sandbox envVars (see
1823
1812
  // src/lib/secrets/sandbox-tokens.ts) is enough to drive every
@@ -1905,10 +1894,7 @@ function requireSessionRuntime(
1905
1894
  * (`dench_*` / `gw_sk_*` / etc.) baked into sandbox envVars. The
1906
1895
  * server-side `requireCrmAccess` helper dispatches on the prefix.
1907
1896
  */
1908
- type AuthenticatedRuntime = Extract<
1909
- Runtime,
1910
- { mode: "session" | "apiKey" }
1911
- >;
1897
+ type AuthenticatedRuntime = Extract<Runtime, { mode: "session" | "apiKey" }>;
1912
1898
 
1913
1899
  function requireAuthenticatedRuntime(
1914
1900
  runtime: Runtime,
@@ -1956,25 +1942,6 @@ function rejectApiKeyForAgentCommand(
1956
1942
  }
1957
1943
  }
1958
1944
 
1959
- async function printToolStatus(runtime: Runtime, toolkit?: string) {
1960
- const sessionRuntime = requireSessionRuntime(runtime, "dench tool status");
1961
- try {
1962
- print(
1963
- await sessionRuntime.client.action(
1964
- api.functions.agentToolsNode.agentToolStatus,
1965
- {
1966
- sessionToken: sessionRuntime.sessionToken,
1967
- toolkit,
1968
- },
1969
- ),
1970
- );
1971
- } catch (error) {
1972
- const unavailable = toolGatewayUnavailable(error, { toolkit });
1973
- if (!unavailable) throw error;
1974
- print(json ? unavailable : formatToolUnavailable(unavailable));
1975
- }
1976
- }
1977
-
1978
1945
  function asRecord(value: unknown): JsonRecord | undefined {
1979
1946
  if (!value || typeof value !== "object" || Array.isArray(value)) {
1980
1947
  return undefined;
@@ -2010,7 +1977,7 @@ function numberField(record: JsonRecord | undefined, ...names: string[]) {
2010
1977
  return undefined;
2011
1978
  }
2012
1979
 
2013
- function booleanField(record: JsonRecord | undefined, ...names: string[]) {
1980
+ function _booleanField(record: JsonRecord | undefined, ...names: string[]) {
2014
1981
  if (!record) return undefined;
2015
1982
  for (const name of names) {
2016
1983
  const value = record[name];
@@ -2033,94 +2000,6 @@ function isMissingConvexFunction(error: unknown) {
2033
2000
  return /Could not find public function/i.test(errorMessage(error));
2034
2001
  }
2035
2002
 
2036
- function toolGatewayUnavailable(error: unknown, context: JsonRecord = {}) {
2037
- const message = errorMessage(error);
2038
- if (!/gateway API key|callGateway|Composio/i.test(message)) {
2039
- return null;
2040
- }
2041
- const invalidKey = /Invalid gateway API key/i.test(message);
2042
- const query = stringField(context, "query");
2043
- const toolkit = stringField(context, "toolkit");
2044
- const retryCommand = query
2045
- ? `Retry dench tool search ${JSON.stringify(query)}${toolkit ? ` --toolkit ${toolkit}` : ""} after the gateway key is fixed.`
2046
- : "Retry dench apps --json after the gateway key is fixed.";
2047
- return {
2048
- ok: false,
2049
- available: false,
2050
- code: invalidKey ? "invalid_gateway_api_key" : "tool_gateway_unavailable",
2051
- reason: invalidKey
2052
- ? "The workspace gateway key is invalid or out of sync."
2053
- : "The connected-app gateway is unavailable.",
2054
- ...context,
2055
- nextActions: [
2056
- "Ask a workspace admin to repair or rotate the Dench gateway key.",
2057
- retryCommand,
2058
- ],
2059
- };
2060
- }
2061
-
2062
- function formatToolUnavailable(payload: JsonRecord) {
2063
- return [
2064
- "Connected apps unavailable.",
2065
- `Reason: ${stringField(payload, "reason") ?? "tool gateway unavailable"}`,
2066
- "",
2067
- "Next actions:",
2068
- ...(
2069
- (Array.isArray(payload.nextActions)
2070
- ? payload.nextActions
2071
- : []) as unknown[]
2072
- )
2073
- .filter((item): item is string => typeof item === "string")
2074
- .map((item) => ` - ${item}`),
2075
- ].join("\n");
2076
- }
2077
-
2078
- const READ_ONLY_TOOL_ACTIONS = new Set([
2079
- "FETCH",
2080
- "GET",
2081
- "LIST",
2082
- "SEARCH",
2083
- "READ",
2084
- "FIND",
2085
- ]);
2086
-
2087
- function toolSlugTokens(toolSlug: string) {
2088
- return toolSlug
2089
- .toUpperCase()
2090
- .split(/[^A-Z0-9]+/)
2091
- .filter(Boolean);
2092
- }
2093
-
2094
- function toolApprovalHint(toolSlug: string | undefined) {
2095
- if (!toolSlug) return "approval unknown";
2096
- const readOnly = toolSlugTokens(toolSlug).find((token) =>
2097
- READ_ONLY_TOOL_ACTIONS.has(token),
2098
- );
2099
- return readOnly ? `read-only (${readOnly})` : "approval likely";
2100
- }
2101
-
2102
- function compactConnection(connection: JsonRecord) {
2103
- return {
2104
- toolkit: stringField(connection, "toolkit") ?? "unknown",
2105
- status: stringField(connection, "status") ?? "unknown",
2106
- connectedAccountId:
2107
- stringField(connection, "connectedAccountId", "connected_account_id") ??
2108
- stringField(connection, "id"),
2109
- accountLabel:
2110
- stringField(connection, "accountLabel", "account_label", "label") ?? null,
2111
- };
2112
- }
2113
-
2114
- function summarizeConnectedApps(value: unknown) {
2115
- const record = asRecord(value);
2116
- const connections = asRecordArray(record?.connections).map(compactConnection);
2117
- return {
2118
- available: true,
2119
- count: connections.length,
2120
- connections,
2121
- };
2122
- }
2123
-
2124
2003
  function workspaceSlugFromContext(context: JsonRecord) {
2125
2004
  const workspace = asRecord(context.workspace);
2126
2005
  return (
@@ -2147,41 +2026,12 @@ function nextContextCommands() {
2147
2026
  ];
2148
2027
  }
2149
2028
 
2150
- async function connectedAppsContext(runtime: Runtime) {
2151
- if (runtime.mode !== "session") {
2152
- return {
2153
- available: false,
2154
- reason: "Connected apps require a logged-in Dench agent session.",
2155
- count: 0,
2156
- connections: [],
2157
- };
2158
- }
2159
-
2160
- try {
2161
- return summarizeConnectedApps(
2162
- await runtime.client.action(
2163
- api.functions.agentToolsNode.agentToolStatus,
2164
- {
2165
- sessionToken: runtime.sessionToken,
2166
- },
2167
- ),
2168
- );
2169
- } catch (error) {
2170
- const unavailable = toolGatewayUnavailable(error);
2171
- if (unavailable) {
2172
- return {
2173
- ...unavailable,
2174
- count: 0,
2175
- connections: [],
2176
- };
2177
- }
2178
- return {
2179
- available: false,
2180
- error: errorMessage(error),
2181
- count: 0,
2182
- connections: [],
2183
- };
2184
- }
2029
+ async function connectedAppsContext(_runtime: Runtime) {
2030
+ // Reach the Dench gateway directly with DENCH_API_KEY (mirrors the
2031
+ // denchclaw pattern). This works for every Runtime mode — session,
2032
+ // apiKey, dev — because the gateway only cares about the bearer key,
2033
+ // not which Convex session is calling.
2034
+ return fetchConnectedAppsSummary();
2185
2035
  }
2186
2036
 
2187
2037
  async function buildContext(runtime: Runtime) {
@@ -2420,223 +2270,6 @@ function formatContext(context: JsonRecord) {
2420
2270
  return lines.join("\n");
2421
2271
  }
2422
2272
 
2423
- function toolkitSlugFromTool(tool: JsonRecord) {
2424
- const toolkit = tool.toolkit;
2425
- if (typeof toolkit === "string") return toolkit;
2426
- const toolkitRecord = asRecord(toolkit);
2427
- return (
2428
- stringField(toolkitRecord, "slug", "name") ??
2429
- stringField(tool, "toolkit_slug")
2430
- );
2431
- }
2432
-
2433
- function connectedToolkitSet(data: JsonRecord | undefined) {
2434
- const connected = new Set<string>();
2435
- const connectedToolkits = Array.isArray(data?.connected_toolkits)
2436
- ? data?.connected_toolkits
2437
- : [];
2438
- for (const toolkit of connectedToolkits) {
2439
- const value = stringValue(toolkit);
2440
- if (value) connected.add(value.toLowerCase());
2441
- }
2442
- for (const status of asRecordArray(data?.toolkit_connection_statuses)) {
2443
- const toolkit = stringField(status, "toolkit");
2444
- const isConnected = booleanField(status, "has_active_connection");
2445
- if (toolkit && isConnected) connected.add(toolkit.toLowerCase());
2446
- }
2447
- return connected;
2448
- }
2449
-
2450
- function searchToolItems(searchResult: unknown) {
2451
- const root = asRecord(searchResult);
2452
- const data = asRecord(root?.data) ?? root;
2453
- const directItems = asRecordArray(data?.items);
2454
- if (directItems.length > 0) {
2455
- return { data, items: directItems };
2456
- }
2457
-
2458
- const schemas = asRecord(data?.tool_schemas);
2459
- if (!schemas) return { data, items: [] };
2460
- return {
2461
- data,
2462
- items: Object.entries(schemas).flatMap<JsonRecord>(([slug, value]) => {
2463
- const tool = asRecord(value);
2464
- return tool
2465
- ? [{ ...tool, tool_slug: stringField(tool, "tool_slug") ?? slug }]
2466
- : [];
2467
- }),
2468
- };
2469
- }
2470
-
2471
- function compactToolSearchResult(searchResult: unknown) {
2472
- const root = asRecord(searchResult);
2473
- const { data, items } = searchToolItems(searchResult);
2474
- const connected = connectedToolkitSet(data);
2475
- const tools = items.map((tool) => {
2476
- const toolkit = toolkitSlugFromTool(tool);
2477
- const connectionStatus = asRecord(tool.connection_status);
2478
- const isConnected =
2479
- booleanField(connectionStatus, "is_connected") ??
2480
- (toolkit ? connected.has(toolkit.toLowerCase()) : undefined);
2481
- const slug = stringField(tool, "slug", "tool_slug") ?? "unknown_tool";
2482
- return {
2483
- slug,
2484
- name: stringField(tool, "display_name", "name") ?? slug,
2485
- toolkit: toolkit ?? "unknown",
2486
- approval: toolApprovalHint(slug),
2487
- connected:
2488
- isConnected === undefined
2489
- ? "connection unknown"
2490
- : isConnected
2491
- ? "connected"
2492
- : "not connected",
2493
- description: compactLine(tool.description) ?? null,
2494
- };
2495
- });
2496
-
2497
- return {
2498
- ok: root?.ok ?? true,
2499
- query: stringField(root, "query") ?? stringField(data, "query") ?? null,
2500
- returned: tools.length,
2501
- total:
2502
- numberField(data, "total_items", "total", "count") ??
2503
- numberField(root, "total_items", "total", "count") ??
2504
- tools.length,
2505
- tools,
2506
- };
2507
- }
2508
-
2509
- function formatToolSearchResult(searchResult: unknown) {
2510
- const summary = compactToolSearchResult(searchResult);
2511
- const query = summary.query ? ` for "${summary.query}"` : "";
2512
- if (summary.tools.length === 0) {
2513
- return `No matching tools found${query}.\nUse --json if you need the raw provider response.`;
2514
- }
2515
-
2516
- const lines = [`Top tools${query}:`];
2517
- for (const [index, tool] of summary.tools.entries()) {
2518
- lines.push(
2519
- `${index + 1}. ${tool.slug} - ${tool.name}`,
2520
- ` toolkit: ${tool.toolkit} | ${tool.approval} | ${tool.connected}`,
2521
- );
2522
- if (tool.description) {
2523
- lines.push(` ${tool.description}`);
2524
- }
2525
- }
2526
- lines.push(
2527
- "",
2528
- "Use dench tool run <slug> --args '{...}' when ready.",
2529
- "Use --json only when you need full schemas or raw arguments.",
2530
- );
2531
- return lines.join("\n");
2532
- }
2533
-
2534
- function printToolSearchResult(searchResult: unknown) {
2535
- print(json ? searchResult : formatToolSearchResult(searchResult));
2536
- }
2537
-
2538
- const SENSITIVE_KEY_RE =
2539
- /(?:otp|oneTime|verification|security|passcode|password|secret|api[_-]?key|access[_-]?token|refresh[_-]?token|authorization)/i;
2540
-
2541
- function redactSensitiveString(value: string) {
2542
- return value
2543
- .replace(
2544
- /\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,
2545
- "$1[redacted-code]",
2546
- )
2547
- .replace(
2548
- /\b((?:api[_\s-]?key|secret|token|password)[^.\n\r]{0,60}?)([A-Za-z0-9_=-]{12,})\b/gi,
2549
- "$1[redacted-secret]",
2550
- );
2551
- }
2552
-
2553
- function redactSensitiveDisplay(value: unknown, key?: string): unknown {
2554
- if (typeof value === "string") {
2555
- return key && SENSITIVE_KEY_RE.test(key)
2556
- ? "[redacted]"
2557
- : redactSensitiveString(value);
2558
- }
2559
- if (typeof value === "number" && key && SENSITIVE_KEY_RE.test(key)) {
2560
- return "[redacted]";
2561
- }
2562
- if (Array.isArray(value)) {
2563
- return value.map((item) => redactSensitiveDisplay(item));
2564
- }
2565
- const record = asRecord(value);
2566
- if (!record) return value;
2567
-
2568
- return Object.fromEntries(
2569
- Object.entries(record).map(([entryKey, entryValue]) => [
2570
- entryKey,
2571
- redactSensitiveDisplay(entryValue, entryKey),
2572
- ]),
2573
- );
2574
- }
2575
-
2576
- function connectionUrlFromToolConnect(result: unknown) {
2577
- const record = asRecord(result);
2578
- const data = asRecord(record?.data);
2579
- return (
2580
- stringField(record, "connectUrl", "redirect_url", "redirectUrl", "url") ??
2581
- stringField(data, "redirect_url", "redirectUrl", "url")
2582
- );
2583
- }
2584
-
2585
- function withToolConnectSummary(result: unknown) {
2586
- const record = asRecord(result);
2587
- if (!record) return result;
2588
- const toolkit = stringField(record, "toolkit") ?? "app";
2589
- const connectUrl = connectionUrlFromToolConnect(record);
2590
- if (!connectUrl) return record;
2591
- return {
2592
- ...record,
2593
- connectUrl,
2594
- linkText: `Connect ${toolkit} here`,
2595
- message: `Connect ${toolkit} here: ${connectUrl}`,
2596
- };
2597
- }
2598
-
2599
- function formatToolConnectResult(result: unknown) {
2600
- const record = asRecord(withToolConnectSummary(result));
2601
- if (!record) return String(result);
2602
- const toolkit = stringField(record, "toolkit") ?? "app";
2603
- const connectUrl = stringField(record, "connectUrl");
2604
- if (connectUrl) {
2605
- return `Connect ${toolkit} here: ${connectUrl}`;
2606
- }
2607
- return JSON.stringify(record, null, 2);
2608
- }
2609
-
2610
- function withToolRunGuidance(runtime: Runtime, result: unknown) {
2611
- const record = asRecord(result);
2612
- if (!record || record.requiresApproval !== true) return result;
2613
- const approvalId = stringField(record, "approvalId");
2614
- const statusContext =
2615
- runtime.mode === "session" || runtime.mode === "apiKey"
2616
- ? workspaceUrls(runtime.host, runtime.organization?.slug)
2617
- : {};
2618
- const approvalCommand = approvalId
2619
- ? `dench approval approve ${approvalId} --evidence "User said yes in chat" --json`
2620
- : undefined;
2621
- return {
2622
- ...record,
2623
- ...statusContext,
2624
- ...(approvalCommand ? { approvalCommand } : {}),
2625
- nextActions: [
2626
- "Ask the human for an explicit yes/no in chat.",
2627
- ...(approvalCommand ? [`If yes, run: ${approvalCommand}`] : []),
2628
- approvalId
2629
- ? `Rerun the exact same dench tool run command with --approval ${approvalId}.`
2630
- : "Rerun the exact same dench tool run command after approval.",
2631
- ],
2632
- };
2633
- }
2634
-
2635
- function printToolRunResult(runtime: Runtime, result: unknown) {
2636
- const guided = withToolRunGuidance(runtime, result);
2637
- print(json ? guided : redactSensitiveDisplay(guided));
2638
- }
2639
-
2640
2273
  async function devOverview(runtime: Awaited<ReturnType<typeof getRuntime>>) {
2641
2274
  if (runtime.mode !== "dev") {
2642
2275
  throw new Error("Not in dev mode");
@@ -2829,10 +2462,12 @@ async function runFsCommand() {
2829
2462
  function normalizeFilesPath(input: string): string {
2830
2463
  if (!input || input === "/") return "/";
2831
2464
  let cleaned = input.replace(/\\/g, "/").replace(/\/+/g, "/");
2832
- if (cleaned.startsWith("/workspace/")) cleaned = cleaned.slice("/workspace".length);
2465
+ if (cleaned.startsWith("/workspace/"))
2466
+ cleaned = cleaned.slice("/workspace".length);
2833
2467
  else if (cleaned === "/workspace") cleaned = "/";
2834
2468
  if (!cleaned.startsWith("/")) cleaned = `/${cleaned}`;
2835
- if (cleaned.length > 1 && cleaned.endsWith("/")) cleaned = cleaned.slice(0, -1);
2469
+ if (cleaned.length > 1 && cleaned.endsWith("/"))
2470
+ cleaned = cleaned.slice(0, -1);
2836
2471
  return cleaned;
2837
2472
  }
2838
2473
 
@@ -3010,9 +2645,9 @@ async function runFilesMv() {
3010
2645
  }
3011
2646
 
3012
2647
  const sourceRows = await listConvexTreeRecursive(runtime, apiKey, from);
3013
- const sourceSelf = (await listConvexTreeAt(runtime, apiKey, parentOfPath(from))).find(
3014
- (row) => row.path === from,
3015
- );
2648
+ const sourceSelf = (
2649
+ await listConvexTreeAt(runtime, apiKey, parentOfPath(from))
2650
+ ).find((row) => row.path === from);
3016
2651
  if (!sourceSelf && sourceRows.length === 0) {
3017
2652
  throw new CliError(`Source not found in Convex: ${from}`, {
3018
2653
  code: "files_mv_not_found",
@@ -3074,7 +2709,11 @@ async function runFilesRm() {
3074
2709
  const target = normalizeFilesPath(targetRaw);
3075
2710
  const recursive = hasFlag("--recursive") || hasFlag("-r");
3076
2711
 
3077
- const parentRows = await listConvexTreeAt(runtime, apiKey, parentOfPath(target));
2712
+ const parentRows = await listConvexTreeAt(
2713
+ runtime,
2714
+ apiKey,
2715
+ parentOfPath(target),
2716
+ );
3078
2717
  const self = parentRows.find((row) => row.path === target);
3079
2718
  if (!self) {
3080
2719
  if (hasFlag("--json")) {
@@ -3621,6 +3260,25 @@ async function main() {
3621
3260
  return;
3622
3261
  }
3623
3262
 
3263
+ if (command === "apps") {
3264
+ // `dench apps` is an alias for `dench tool status`. Both speak
3265
+ // straight HTTP to the Dench Cloud Gateway with DENCH_API_KEY —
3266
+ // mirrors the denchclaw pattern. No Convex session involved.
3267
+ await runToolCommand({ args: ["status"], jsonOutput: json });
3268
+ return;
3269
+ }
3270
+
3271
+ if (command === "tool") {
3272
+ // `dench tool {status,connect,search,run,disconnect}` is the model-facing
3273
+ // composio surface and only needs DENCH_API_KEY (Bearer) on the
3274
+ // gateway. Bypasses requireSessionRuntime entirely so it works in
3275
+ // every sandbox / API-key context.
3276
+ const subArgs = stripRuntimeFlags(args.slice(args.indexOf("tool") + 1));
3277
+ const filteredSubArgs = subArgs.filter((a) => a !== "--json");
3278
+ await runToolCommand({ args: filteredSubArgs, jsonOutput: json });
3279
+ return;
3280
+ }
3281
+
3624
3282
  if (command === "chat") {
3625
3283
  const subArgs = args.slice(args.indexOf("chat") + 1);
3626
3284
  const sub = subArgs[0];
@@ -3713,8 +3371,11 @@ async function main() {
3713
3371
  return;
3714
3372
  }
3715
3373
 
3716
- if (command === "cron") {
3717
- const subArgs = args.slice(args.indexOf("cron") + 1);
3374
+ // `dench routine` / `dench routines` are UI-friendly aliases for
3375
+ // `dench cron`. Same dispatch, same auth, same handler — only the
3376
+ // top-level command name differs to match the workspace UI rename.
3377
+ if (command === "cron" || command === "routine" || command === "routines") {
3378
+ const subArgs = args.slice(args.indexOf(command) + 1);
3718
3379
  const sub = subArgs[0];
3719
3380
  // Help is a no-auth path so users can read the surface without a
3720
3381
  // session — mirrors `dench crm help`.
@@ -3733,7 +3394,10 @@ async function main() {
3733
3394
  // bearer prefix and resolves the org for both. The Bearer token
3734
3395
  // must carry `crons:read` (queries) or `crons:write` (mutations)
3735
3396
  // scope, or the wildcard `*`.
3736
- const authedRuntime = requireAuthenticatedRuntime(runtime, "dench cron");
3397
+ const authedRuntime = requireAuthenticatedRuntime(
3398
+ runtime,
3399
+ `dench ${command}`,
3400
+ );
3737
3401
  await runCronCommand({
3738
3402
  convex: authedRuntime.client,
3739
3403
  args: subArgs,
@@ -3742,6 +3406,85 @@ async function main() {
3742
3406
  return;
3743
3407
  }
3744
3408
 
3409
+ // ── OpenClaw-parity self-update CLI surface ──────────────────────────
3410
+ //
3411
+ // Dispatch all `dench identity / organisation / user / tools / mem /
3412
+ // heartbeat / bootstrap / model / daily` traffic through the shared
3413
+ // agent-config CLI module. Each command requires the same auth shape
3414
+ // as `dench cron` (Bearer DENCH_API_KEY or `dench login` session
3415
+ // token) so server-side `requireCronAccess` accepts both.
3416
+ const AGENT_CONFIG_COMMANDS = new Set([
3417
+ "identity",
3418
+ "organisation",
3419
+ "user",
3420
+ "tools",
3421
+ "mem",
3422
+ "heartbeat",
3423
+ "bootstrap",
3424
+ "model",
3425
+ "daily",
3426
+ ]);
3427
+ if (AGENT_CONFIG_COMMANDS.has(command)) {
3428
+ const subArgs = args.slice(args.indexOf(command) + 1);
3429
+ if (
3430
+ subArgs[0] === "help" ||
3431
+ subArgs.includes("--help") ||
3432
+ subArgs.includes("-h")
3433
+ ) {
3434
+ const { printAgentConfigCliHelp } = await import("./agent-config");
3435
+ printAgentConfigCliHelp();
3436
+ return;
3437
+ }
3438
+ const runtime = await getRuntime();
3439
+ const authedRuntime = requireAuthenticatedRuntime(
3440
+ runtime,
3441
+ `dench ${command}`,
3442
+ );
3443
+ const ctxBase = {
3444
+ convex: authedRuntime.client,
3445
+ args: subArgs,
3446
+ jsonOutput: json,
3447
+ sessionToken: authedRuntime.sessionToken,
3448
+ };
3449
+ const mod = await import("./agent-config");
3450
+ if (command === "identity") {
3451
+ await mod.runIdentityCommand(ctxBase);
3452
+ return;
3453
+ }
3454
+ if (command === "organisation") {
3455
+ await mod.runOrganisationCommand(ctxBase);
3456
+ return;
3457
+ }
3458
+ if (command === "user") {
3459
+ await mod.runUserCommand(ctxBase);
3460
+ return;
3461
+ }
3462
+ if (command === "tools") {
3463
+ await mod.runToolsCommand(ctxBase);
3464
+ return;
3465
+ }
3466
+ if (command === "mem") {
3467
+ await mod.runMemoryAggregateCommand(ctxBase);
3468
+ return;
3469
+ }
3470
+ if (command === "heartbeat") {
3471
+ await mod.runHeartbeatCommand(ctxBase);
3472
+ return;
3473
+ }
3474
+ if (command === "bootstrap") {
3475
+ await mod.runBootstrapCommand(ctxBase);
3476
+ return;
3477
+ }
3478
+ if (command === "model") {
3479
+ await mod.runModelCommand(ctxBase);
3480
+ return;
3481
+ }
3482
+ if (command === "daily") {
3483
+ await mod.runDailyCommand(ctxBase);
3484
+ return;
3485
+ }
3486
+ }
3487
+
3745
3488
  if (command === "login") {
3746
3489
  await login();
3747
3490
  return;
@@ -3786,11 +3529,6 @@ async function main() {
3786
3529
  return;
3787
3530
  }
3788
3531
 
3789
- if (command === "apps") {
3790
- await printToolStatus(runtime);
3791
- return;
3792
- }
3793
-
3794
3532
  if (command === "context") {
3795
3533
  const context = await buildContext(runtime);
3796
3534
  print(json ? context : formatContext(context));
@@ -3943,9 +3681,7 @@ async function main() {
3943
3681
  return;
3944
3682
  }
3945
3683
  if (runtime.mode === "apiKey") {
3946
- throw loginRequiredError(
3947
- "dench tasks (agent-scoped task list)",
3948
- );
3684
+ throw loginRequiredError("dench tasks (agent-scoped task list)");
3949
3685
  }
3950
3686
  const data = await devOverview(runtime);
3951
3687
  print(data.tasks);
@@ -4235,79 +3971,6 @@ async function main() {
4235
3971
  return;
4236
3972
  }
4237
3973
 
4238
- if (command === "tool") {
4239
- const sessionRuntime = requireSessionRuntime(runtime, "dench tool");
4240
-
4241
- if (subcommand === "status") {
4242
- await printToolStatus(sessionRuntime, positional(2));
4243
- return;
4244
- }
4245
-
4246
- if (subcommand === "connect") {
4247
- const toolkit = positional(2);
4248
- if (!toolkit) throw new Error("Missing toolkit");
4249
- const result = await sessionRuntime.client.action(
4250
- api.functions.agentToolsNode.agentToolConnect,
4251
- {
4252
- sessionToken: sessionRuntime.sessionToken,
4253
- toolkit,
4254
- callbackUrl: option("--callback-url"),
4255
- },
4256
- );
4257
- print(
4258
- json ? withToolConnectSummary(result) : formatToolConnectResult(result),
4259
- );
4260
- return;
4261
- }
4262
-
4263
- if (subcommand === "search") {
4264
- const query = positional(2);
4265
- if (!query) throw new Error("Missing search query");
4266
- try {
4267
- printToolSearchResult(
4268
- await sessionRuntime.client.action(
4269
- api.functions.agentToolsNode.agentToolSearch,
4270
- {
4271
- sessionToken: sessionRuntime.sessionToken,
4272
- query,
4273
- toolkit: option("--toolkit"),
4274
- limit: parseNumberOption("--limit"),
4275
- },
4276
- ),
4277
- );
4278
- } catch (error) {
4279
- const unavailable = toolGatewayUnavailable(error, {
4280
- query,
4281
- toolkit: option("--toolkit"),
4282
- });
4283
- if (!unavailable) throw error;
4284
- print(json ? unavailable : formatToolUnavailable(unavailable));
4285
- }
4286
- return;
4287
- }
4288
-
4289
- if (subcommand === "run") {
4290
- const toolSlug = positional(2);
4291
- if (!toolSlug) throw new Error("Missing Composio tool slug");
4292
- printToolRunResult(
4293
- sessionRuntime,
4294
- await sessionRuntime.client.action(
4295
- api.functions.agentToolsNode.agentToolRun,
4296
- {
4297
- sessionToken: sessionRuntime.sessionToken,
4298
- toolSlug,
4299
- arguments: parseJsonObjectOption("--args"),
4300
- connectedAccountId: option("--account"),
4301
- approvalId: option("--approval") as never,
4302
- },
4303
- ),
4304
- );
4305
- return;
4306
- }
4307
-
4308
- throw new Error(`Unknown tool command: ${filteredArgs.join(" ")}`);
4309
- }
4310
-
4311
3974
  if (
4312
3975
  command === "approval" &&
4313
3976
  (subcommand === "approve" || subcommand === "reject")