@dench.com/cli 0.2.0

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 ADDED
@@ -0,0 +1,3305 @@
1
+ #!/usr/bin/env tsx
2
+ import { Buffer } from "node:buffer";
3
+ import { spawn } from "node:child_process";
4
+ import { chmod, mkdir, readFile, writeFile } from "node:fs/promises";
5
+ import { homedir, hostname, userInfo } from "node:os";
6
+ import { join } from "node:path";
7
+ import { fileURLToPath } from "node:url";
8
+ import { ConvexHttpClient } from "convex/browser";
9
+ import { makeFunctionReference } from "convex/server";
10
+ import { agentKindLabel, normalizeAgentKind } from "./agentKind";
11
+ import {
12
+ DEFAULT_HOST,
13
+ isLocalHost,
14
+ normalizeHost,
15
+ resolveHostFromArgs,
16
+ STAGING_HOST,
17
+ } from "./host";
18
+ import { formatApprovalOpenMessage, openUrl } from "./openUrl";
19
+ import {
20
+ explicitSessionKeyInput,
21
+ findStoredSessionEntries,
22
+ listStoredSessionEntries,
23
+ removeAllStoredSessions,
24
+ removeStoredSession,
25
+ resolveConfigHost,
26
+ resolveSessionScope,
27
+ type SessionScope,
28
+ type StoredSessionEntry,
29
+ selectStoredSession,
30
+ selectStoredSessionEntry,
31
+ withCurrentSessionSelection,
32
+ withSavedSession,
33
+ } from "./session";
34
+
35
+ type JsonRecord = Record<string, unknown>;
36
+
37
+ class CliError extends Error {
38
+ payload: JsonRecord;
39
+
40
+ constructor(message: string, payload: JsonRecord = {}) {
41
+ super(message);
42
+ this.name = "CliError";
43
+ this.payload = payload;
44
+ }
45
+ }
46
+
47
+ type StoredSession = {
48
+ host: string;
49
+ convexUrl: string;
50
+ sessionToken: string;
51
+ organization?: { id: string; name: string; slug?: string };
52
+ agent?: { id: string; name: string; kind: string };
53
+ sessionScope?: string;
54
+ sessionScopeLabel?: string;
55
+ sessionExpiresAt?: number;
56
+ savedAt: number;
57
+ };
58
+
59
+ type ConfigFile = {
60
+ currentHost?: string;
61
+ currentSessionKey?: string;
62
+ currentHosts?: Record<string, string>;
63
+ currentSessionKeys?: Record<string, string>;
64
+ sessions?: Record<string, StoredSession>;
65
+ };
66
+
67
+ type WorkspaceOverview = {
68
+ agents: Array<{ _id: string } & JsonRecord>;
69
+ projects: JsonRecord[];
70
+ tasks: JsonRecord[];
71
+ approvals: Array<{ status?: string } & JsonRecord>;
72
+ rules: JsonRecord[];
73
+ recentArtifacts?: JsonRecord[];
74
+ activeRuns?: JsonRecord[];
75
+ comments?: JsonRecord[];
76
+ dependencies?: JsonRecord[];
77
+ memories?: JsonRecord[];
78
+ };
79
+
80
+ type AgentStatus = {
81
+ workspace?: string;
82
+ workspaceSlug?: string;
83
+ agent?: { id?: string } & JsonRecord;
84
+ counts?: JsonRecord;
85
+ rules?: JsonRecord[];
86
+ approvals?: Array<{ status?: string; agentId?: string } & JsonRecord>;
87
+ };
88
+
89
+ const api = {
90
+ functions: {
91
+ agentWorkspace: {
92
+ agentAppendLog: makeFunctionReference<"mutation">(
93
+ "functions/agentWorkspace:agentAppendLog",
94
+ ),
95
+ agentClaimTask: makeFunctionReference<"mutation">(
96
+ "functions/agentWorkspace:agentClaimTask",
97
+ ),
98
+ agentCreateTask: makeFunctionReference<"mutation">(
99
+ "functions/agentWorkspace:agentCreateTask",
100
+ ),
101
+ agentAddTaskComment: makeFunctionReference<"mutation">(
102
+ "functions/agentWorkspace:agentAddTaskComment",
103
+ ),
104
+ agentAddTaskDependency: makeFunctionReference<"mutation">(
105
+ "functions/agentWorkspace:agentAddTaskDependency",
106
+ ),
107
+ agentConvertSuggestionToTask: makeFunctionReference<"mutation">(
108
+ "functions/agentWorkspace:agentConvertSuggestionToTask",
109
+ ),
110
+ agentDecideApproval: makeFunctionReference<"mutation">(
111
+ "functions/agentWorkspace:agentDecideApproval",
112
+ ),
113
+ agentListArtifacts: makeFunctionReference<"query">(
114
+ "functions/agentWorkspace:agentListArtifacts",
115
+ ),
116
+ agentListTasks: makeFunctionReference<"query">(
117
+ "functions/agentWorkspace:agentListTasks",
118
+ ),
119
+ agentRequestApproval: makeFunctionReference<"mutation">(
120
+ "functions/agentWorkspace:agentRequestApproval",
121
+ ),
122
+ agentSaveMemory: makeFunctionReference<"mutation">(
123
+ "functions/agentWorkspace:agentSaveMemory",
124
+ ),
125
+ agentSearchMemory: makeFunctionReference<"query">(
126
+ "functions/agentWorkspace:agentSearchMemory",
127
+ ),
128
+ agentStatus: makeFunctionReference<"query">(
129
+ "functions/agentWorkspace:agentStatus",
130
+ ),
131
+ getAgentBillingStatus: makeFunctionReference<"query">(
132
+ "functions/agentWorkspace:getAgentBillingStatus",
133
+ ),
134
+ agentUpdateTaskStatus: makeFunctionReference<"mutation">(
135
+ "functions/agentWorkspace:agentUpdateTaskStatus",
136
+ ),
137
+ createAgentLoginRequest: makeFunctionReference<"mutation">(
138
+ "functions/agentWorkspace:createAgentLoginRequest",
139
+ ),
140
+ devAppendLog: makeFunctionReference<"mutation">(
141
+ "functions/agentWorkspace:devAppendLog",
142
+ ),
143
+ devClaimTask: makeFunctionReference<"mutation">(
144
+ "functions/agentWorkspace:devClaimTask",
145
+ ),
146
+ devCreateProject: makeFunctionReference<"mutation">(
147
+ "functions/agentWorkspace:devCreateProject",
148
+ ),
149
+ devCreateTask: makeFunctionReference<"mutation">(
150
+ "functions/agentWorkspace:devCreateTask",
151
+ ),
152
+ devListWorkspaceOverview: makeFunctionReference<"query">(
153
+ "functions/agentWorkspace:devListWorkspaceOverview",
154
+ ),
155
+ devRegisterAgent: makeFunctionReference<"mutation">(
156
+ "functions/agentWorkspace:devRegisterAgent",
157
+ ),
158
+ devRequestApproval: makeFunctionReference<"mutation">(
159
+ "functions/agentWorkspace:devRequestApproval",
160
+ ),
161
+ pollAgentLoginRequest: makeFunctionReference<"mutation">(
162
+ "functions/agentWorkspace:pollAgentLoginRequest",
163
+ ),
164
+ whatCanIDoHere: makeFunctionReference<"query">(
165
+ "functions/agentWorkspace:whatCanIDoHere",
166
+ ),
167
+ },
168
+ agentToolsNode: {
169
+ agentToolConnect: makeFunctionReference<"action">(
170
+ "functions/agentToolsNode:agentToolConnect",
171
+ ),
172
+ agentToolRun: makeFunctionReference<"action">(
173
+ "functions/agentToolsNode:agentToolRun",
174
+ ),
175
+ agentToolSearch: makeFunctionReference<"action">(
176
+ "functions/agentToolsNode:agentToolSearch",
177
+ ),
178
+ agentToolStatus: makeFunctionReference<"action">(
179
+ "functions/agentToolsNode:agentToolStatus",
180
+ ),
181
+ },
182
+ },
183
+ };
184
+ const CONFIG_DIR = join(homedir(), ".dench");
185
+ const CONFIG_PATH = join(CONFIG_DIR, "config.json");
186
+ const AUTONOMOUS_DEFAULT_API_BASE = "http://localhost:3000";
187
+ const SAFE_LONG_SESSION_CONSTRAINTS = [
188
+ "do not edit files",
189
+ "publish",
190
+ "deploy",
191
+ "spend money",
192
+ "send external messages",
193
+ "access secrets",
194
+ "or change production data unless the user explicitly approves it",
195
+ ];
196
+ const args = process.argv.slice(2);
197
+ const json = args.includes("--json");
198
+ const filteredArgs = args.filter((arg) => arg !== "--json");
199
+ const scopedStatus = args.includes("--mine") || args.includes("--self");
200
+
201
+ async function cliVersion() {
202
+ try {
203
+ const packageJson = JSON.parse(
204
+ await readFile(new URL("./package.json", import.meta.url), "utf8"),
205
+ ) as { version?: unknown };
206
+ return typeof packageJson.version === "string"
207
+ ? packageJson.version
208
+ : "unknown";
209
+ } catch {
210
+ return "unknown";
211
+ }
212
+ }
213
+
214
+ function readEnv(name: string) {
215
+ const value = process.env[name]?.trim();
216
+ if (!value) {
217
+ throw new Error(`Missing ${name}`);
218
+ }
219
+ return value;
220
+ }
221
+
222
+ function option(name: string) {
223
+ const index = filteredArgs.indexOf(name);
224
+ if (index === -1) return undefined;
225
+ return filteredArgs[index + 1];
226
+ }
227
+
228
+ function hasFlag(name: string) {
229
+ return filteredArgs.includes(name);
230
+ }
231
+
232
+ const BOOLEAN_FLAGS = new Set([
233
+ "--all",
234
+ "--compact",
235
+ "--dev",
236
+ "--help",
237
+ "--mine",
238
+ "--self",
239
+ "--staging",
240
+ "--prod",
241
+ "--no-open",
242
+ "--version",
243
+ "--ask-before-publishing",
244
+ "--ask-before-spending",
245
+ "--ask-before-external-messages",
246
+ "--ask-before-production-changes",
247
+ "--ask-before-secrets",
248
+ ]);
249
+
250
+ function positionalArgs() {
251
+ const positionals: string[] = [];
252
+ for (let i = 0; i < filteredArgs.length; i++) {
253
+ if (filteredArgs[i].startsWith("--")) {
254
+ if (!BOOLEAN_FLAGS.has(filteredArgs[i])) {
255
+ i++;
256
+ }
257
+ } else {
258
+ positionals.push(filteredArgs[i]);
259
+ }
260
+ }
261
+ return positionals;
262
+ }
263
+
264
+ function positional(index: number) {
265
+ return positionalArgs()[index];
266
+ }
267
+
268
+ function positionalsFrom(index: number) {
269
+ return positionalArgs().slice(index);
270
+ }
271
+
272
+ function print(value: unknown) {
273
+ if (json) {
274
+ console.log(JSON.stringify(value, null, 2));
275
+ return;
276
+ }
277
+ if (typeof value === "string") {
278
+ console.log(value);
279
+ return;
280
+ }
281
+ console.log(JSON.stringify(value, null, 2));
282
+ }
283
+
284
+ function stringArrayField(record: JsonRecord | undefined, name: string) {
285
+ const value = record?.[name];
286
+ return Array.isArray(value)
287
+ ? value.filter((item): item is string => typeof item === "string")
288
+ : [];
289
+ }
290
+
291
+ function throwIfCliError(value: unknown) {
292
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
293
+ return;
294
+ }
295
+ const record = value as JsonRecord;
296
+ if (record.ok === false && typeof record.error === "string") {
297
+ throw new Error(record.error);
298
+ }
299
+ }
300
+
301
+ function parseJsonObjectOption(name: string) {
302
+ const raw = option(name);
303
+ if (!raw) return {};
304
+ let parsed: unknown;
305
+ try {
306
+ parsed = JSON.parse(raw) as unknown;
307
+ } catch {
308
+ throw new CliError(`${name} must be valid JSON`, {
309
+ code: "malformed_json",
310
+ option: name,
311
+ nextActions: [
312
+ `Pass ${name} as a quoted JSON object, for example: ${name} '{"key":"value"}'`,
313
+ "Use --json for structured CLI output.",
314
+ ],
315
+ });
316
+ }
317
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
318
+ throw new CliError(`${name} must be a JSON object`, {
319
+ code: "malformed_json",
320
+ option: name,
321
+ nextActions: [
322
+ `Pass ${name} as an object, for example: ${name} '{"key":"value"}'`,
323
+ ],
324
+ });
325
+ }
326
+ return parsed as JsonRecord;
327
+ }
328
+
329
+ function parseNumberOption(name: string) {
330
+ const raw = option(name);
331
+ if (!raw) return undefined;
332
+ const parsed = Number(raw);
333
+ if (!Number.isFinite(parsed)) {
334
+ throw new CliError(`${name} must be a number`, {
335
+ code: "invalid_number",
336
+ option: name,
337
+ nextActions: [`Retry with a numeric value for ${name}.`],
338
+ });
339
+ }
340
+ return parsed;
341
+ }
342
+
343
+ function parseCommaOption(name: string) {
344
+ return (
345
+ option(name)
346
+ ?.split(",")
347
+ .map((item) => item.trim())
348
+ .filter(Boolean) ?? []
349
+ );
350
+ }
351
+
352
+ function logHuman(message: string) {
353
+ if (!json) {
354
+ console.error(message);
355
+ }
356
+ }
357
+
358
+ function help() {
359
+ console.log(`Dench CLI
360
+
361
+ Usage:
362
+ dench onboard [--kind <kind>] [--name "AI Agent - Project"] [--no-open] [--json]
363
+ dench setup [--kind <kind>] [--name "AI Agent - Project"] [--no-open] [--json]
364
+ dench what-can-i-do [--kind <kind>] [--name "AI Agent - Project"] [--no-open] [--json]
365
+ dench login [--host ${DEFAULT_HOST}] [--name "Claude Code Agent"] [--kind <kind>] [--no-open]
366
+ dench sessions [--host <host>] [--json]
367
+ dench use <session-key-or-workspace-slug> [--host <host>] [--json]
368
+ dench logout [session-key-or-workspace-slug] [--session <key-or-scope>] [--host <host>] [--all] [--json]
369
+ dench context [--json]
370
+ dench status [--mine|--self] [--json]
371
+ dench tasks [--json]
372
+ dench task create "title" [--description "..."] [--priority low|medium|high] [--risk low|medium|high] [--json]
373
+ dench task status <taskId> <open|claimed|in_progress|waiting_approval|completed|canceled> [--note "..."] [--json]
374
+ dench task comment <taskId> "message" [--json]
375
+ dench task handoff <taskId> "message" --agent <agentId> [--json]
376
+ dench task block <taskId> "message" [--json]
377
+ dench claim <taskId> [--json]
378
+ dench log "message" [--task <taskId>] [--json]
379
+ dench memory search "query" [--limit 8] [--json]
380
+ dench memory save <key> "text" [--kind fact|decision|preference|goal|tool_note|other] [--tags a,b] [--sensitivity normal|broad|sensitive] [--json]
381
+ dench artifacts [--limit 25] [--json]
382
+ dench suggested-work [--json]
383
+ dench suggested-work task <artifactId> [--json]
384
+ dench approval request "message" [--task <taskId>] [--json]
385
+ dench approval approve <approvalId> [--evidence "User said yes in chat"] [--json]
386
+ dench approval reject <approvalId> [--evidence "User said no in chat"] [--json]
387
+ dench billing status [--json]
388
+ dench billing topup --amount 5 [--no-open] [--json]
389
+ dench apps [--json]
390
+ dench tool status [toolkit] [--json]
391
+ dench tool connect <toolkit> [--json]
392
+ dench tool search "create github issue" [--toolkit github] [--limit 20] [--compact] [--json]
393
+ dench tool run <composio_tool_slug> --args '{"key":"value"}' [--account <connected_account_id>] [--approval <approvalId>] [--json]
394
+ dench autonomous run "Inspect this repo and summarize the relevant feature. Do not edit files, publish, deploy, spend money, send external messages, access secrets, or change production data." [--duration 1h] [--model <model>] [--api-base http://localhost:3000] [--json]
395
+
396
+ CRM (Daytona-native rewrite):
397
+ dench crm objects <list|get|create|update|rename|delete> ...
398
+ dench crm fields <list|create|update|delete|reorder> <object> ...
399
+ dench crm entries <list|get|create|update|delete|bulk-delete> <object> ...
400
+ dench crm cells <get|set|append> <object> <entryId> <field> [value]
401
+ dench crm query <object> --where '...' --select ... --sort ... --limit N
402
+ dench crm sql 'SELECT "Name" FROM lead WHERE ... LIMIT 10'
403
+ dench crm aggregate <object> --field <name>
404
+ dench crm search [--object <name>] [--limit N] <text>
405
+ dench crm import <object> --csv <path> [--map '{...}'] [--on-conflict skip|update|error]
406
+ dench crm export <object> --format csv|jsonl [--limit N]
407
+ dench crm transaction <begin|add|commit|abort|inspect>
408
+ dench crm people|companies <search|upsert|enrich> ...
409
+ dench crm enrich <cell|object> ...
410
+ dench crm reports generate <name> --object <obj> --type pie|bar|line|table --group-by <field>
411
+ dench crm docs <list|create|link>
412
+ dench crm actions <list|run> ...
413
+ dench crm batch --file ops.jsonl
414
+ Help: dench crm help
415
+
416
+ File sync (Daytona-native rewrite):
417
+ dench fs sync --initial --org-id <orgId> [--workspace /workspace]
418
+ dench fs stream-open <path>
419
+ dench fs stream-append <token> <chunk>
420
+ dench fs stream-close <token>
421
+
422
+ Subagents (Daytona-native rewrite):
423
+ dench agent spawn '<goal>' [--parent-run-id <id>] [--sandbox own|share_parent] [--time-budget-ms N]
424
+ dench agent await --hook <token> | --children id1,id2,id3
425
+ dench agent message <toRunId> '<text>' [--from-run-id <id>]
426
+ dench agent wait-message
427
+ dench agent pause <runId>
428
+ dench agent resume <runId> ['optional prompt']
429
+ dench agent continue <prevRunId> '<prompt>'
430
+ dench agent tree [<rootRunId>]
431
+ Help: dench agent help
432
+
433
+ dench --version
434
+
435
+ External tools:
436
+ dench apps is an alias for dench tool status -- it lists connected apps.
437
+ dench tool with no subcommand prints tool help.
438
+ Read-only FETCH, GET, LIST, SEARCH, READ, and FIND tools do not need manual approval.
439
+
440
+ Billing:
441
+ dench billing status shows current AI credits.
442
+ dench billing topup --amount 5 creates a Stripe Checkout link for the human to pay.
443
+
444
+ Agent setup:
445
+ dench onboard wraps login (when needed), workspace orientation, tasks, tools,
446
+ suggested work, memory, and next recommended actions.
447
+ --kind accepts any string. Suggested values: claude_code, codex, cursor,
448
+ hermes, openclaw, or any custom kind (e.g. aider, goose, internal_orchestrator).
449
+ Defaults to "other" when omitted. Values are normalized to lowercase
450
+ snake_case before send (e.g. "Claude Code" -> claude_code).
451
+ After login approval, run dench status --mine --json and dench tasks --json.
452
+ Use dench memory and dench artifacts to continue from durable context.
453
+ Do not create, claim, or log a setup task by default.
454
+
455
+ Hosts:
456
+ Default production host: ${DEFAULT_HOST}
457
+ Staging: dench login --staging
458
+ Staging with explicit host: dench login --host ${STAGING_HOST}
459
+
460
+ Dev fallback:
461
+ DENCH_DEV_AGENT_KEY=... DENCH_WORKSPACE=... NEXT_PUBLIC_CONVEX_URL=... dench status --dev
462
+
463
+ Advanced:
464
+ For long-lived agents or when the human asks:
465
+ DENCH_SESSION_KEY=stable-agent-id dench login
466
+ DENCH_SESSION_KEY=stable-agent-id dench logout
467
+ DENCH_SESSION_KEY=stable-agent-id dench status
468
+ `);
469
+ }
470
+
471
+ function toolHelp() {
472
+ console.log(`Dench tool commands
473
+
474
+ Usage:
475
+ dench apps [--json]
476
+ dench tool status [toolkit] [--json]
477
+ dench tool connect <toolkit> [--json]
478
+ dench tool search "create github issue" [--toolkit github] [--limit 20] [--compact] [--json]
479
+ dench tool run <composio_tool_slug> --args '{"key":"value"}' [--account <connected_account_id>] [--approval <approvalId>] [--json]
480
+
481
+ Notes:
482
+ dench apps lists all connected apps. It returns the same shape as dench tool status --json.
483
+ dench tool search prints compact ranked results by default. Add --json only when you need full schemas.
484
+ Run clear read-only FETCH, GET, LIST, SEARCH, READ, and FIND tools through Dench without manual approval.
485
+ Non-JSON tool run output redacts likely OTP codes, secrets, and tokens for display.
486
+ Dench enforces the final policy and returns requiresApproval when approval is needed.
487
+ `);
488
+ }
489
+
490
+ function autonomousHelp() {
491
+ console.log(`Dench Long Session commands
492
+
493
+ Usage:
494
+ dench autonomous run "Inspect this repo and summarize the relevant feature. Do not edit files, publish, deploy, spend money, send external messages, access secrets, or change production data." [--duration 1h] [--model <model>] [--api-base http://localhost:3000] [--json]
495
+
496
+ Notes:
497
+ Starts a Long Session through /api/runs.
498
+ Duration supports 30m, 1h, 3h, 5h, or until-done.
499
+ API base defaults to ${AUTONOMOUS_DEFAULT_API_BASE}.
500
+ Override with --api-base, --host, DENCH_AUTONOMOUS_API_BASE, or DENCH_HOST.
501
+ If needed, run dench login --host <api-base> once so the CLI can authenticate.
502
+ Omit --model to use the server default.
503
+ Approval flags: --ask-before-publishing --ask-before-spending --ask-before-external-messages --ask-before-production-changes --ask-before-secrets.
504
+ Safe goals should explicitly say: ${SAFE_LONG_SESSION_CONSTRAINTS.join(", ")}.
505
+ `);
506
+ }
507
+
508
+ function contextHelp() {
509
+ console.log(`Dench context
510
+
511
+ Usage:
512
+ dench context [--json]
513
+
514
+ Shows the current workspace, current agent, assigned tasks, pending approvals
515
+ requested by this agent, connected apps when available, and useful next commands.
516
+ `);
517
+ }
518
+
519
+ function memoryHelp() {
520
+ console.log(`Dench memory
521
+
522
+ Usage:
523
+ dench memory search "query" [--limit 8] [--json]
524
+ dench memory save <key> "text" [--kind fact|decision|preference|goal|tool_note|other] [--tags a,b] [--sensitivity normal|broad|sensitive] [--json]
525
+
526
+ Use memory only for stable facts, decisions, preferences, recurring goals, and
527
+ tool notes. Do not store secrets or scratch notes.
528
+ `);
529
+ }
530
+
531
+ function artifactsHelp() {
532
+ console.log(`Dench artifacts
533
+
534
+ Usage:
535
+ dench artifacts [--limit 25] [--json]
536
+ dench suggested-work [--json]
537
+ dench suggested-work task <artifactId> [--json]
538
+
539
+ Artifacts are durable Long Session outputs for human or agent review.
540
+ Suggested work can be converted into a workspace task.
541
+ `);
542
+ }
543
+
544
+ function onboardHelp() {
545
+ console.log(`Dench onboard
546
+
547
+ Usage:
548
+ dench onboard [--kind <kind>] [--name "AI Agent - Project"] [--no-open] [--json]
549
+ dench setup [--kind <kind>] [--name "AI Agent - Project"] [--no-open] [--json]
550
+ dench what-can-i-do [--kind <kind>] [--name "AI Agent - Project"] [--no-open] [--json]
551
+
552
+ Logs in if needed, then shows what this agent can do here: workspace rules,
553
+ open work, suggested work, Long Session options, connected-tool commands,
554
+ memory, approvals, and next actions.
555
+ `);
556
+ }
557
+
558
+ function loginHelp() {
559
+ console.log(`Dench login
560
+
561
+ Usage:
562
+ dench login [--name "AI Agent - Billing Repo"] [--kind <kind>] [--host <host>] [--no-open]
563
+
564
+ Agent kind:
565
+ --kind accepts any string. Defaults to "other" when omitted.
566
+ Suggested values: claude_code, codex, cursor, hermes, openclaw, or any
567
+ custom kind such as aider, goose, or some_custom_agent. Values are
568
+ normalized to lowercase snake_case before send (e.g. "Claude Code"
569
+ becomes claude_code, "my agent" becomes my_agent).
570
+
571
+ Agent setup:
572
+ Claude Code: dench login --kind claude_code --name "Claude Code Agent - Project"
573
+ Codex: dench login --kind codex --name "Codex Agent - Project"
574
+ Cursor: dench login --kind cursor --name "Cursor Agent - Project"
575
+ Custom: dench login --kind some_custom_agent --name "My Agent - Project"
576
+ After approval, run dench status --mine --json and dench tasks --json.
577
+ `);
578
+ }
579
+
580
+ function statusHelp() {
581
+ console.log(`Dench status
582
+
583
+ Usage:
584
+ dench status [--mine|--self] [--json]
585
+
586
+ Notes:
587
+ --mine and --self return the current workspace, current agent, counts, rules,
588
+ assigned tasks, and pending approvals requested by this agent.
589
+ Use dench status --mine --json during setup verification.
590
+ `);
591
+ }
592
+
593
+ function tasksHelp() {
594
+ console.log(`Dench tasks
595
+
596
+ Usage:
597
+ dench tasks [--json]
598
+
599
+ Notes:
600
+ List workspace tasks before creating new ones.
601
+ Do not create, claim, or log a setup task by default.
602
+ Only create or claim a task if the human assigns work, an open matching task
603
+ exists, or coordination benefits from creating one.
604
+ `);
605
+ }
606
+
607
+ function logoutHelp() {
608
+ console.log(`Dench logout
609
+
610
+ Usage:
611
+ dench logout [session-key-or-workspace-slug] [--session <key-or-scope>] [--host <host>] [--all] [--json]
612
+
613
+ Notes:
614
+ dench logout removes only the current selected session for this local agent context.
615
+ Other saved sessions can still be reused by future chats until removed.
616
+ Use dench logout --all to clear every saved Dench session on this machine.
617
+ `);
618
+ }
619
+
620
+ function appsHelp() {
621
+ console.log(`Dench apps
622
+
623
+ Usage:
624
+ dench apps [--json]
625
+
626
+ Lists all connected external apps for the current Dench session.
627
+ This is an alias for dench tool status [--json].
628
+ `);
629
+ }
630
+
631
+ function billingHelp() {
632
+ console.log(`Dench billing
633
+
634
+ Usage:
635
+ dench billing status [--json]
636
+ dench billing topup --amount 5 [--no-open] [--json]
637
+
638
+ Notes:
639
+ status shows the current AI credit balance for this workspace.
640
+ topup creates a Stripe Checkout link. The human opens the link and pays.
641
+ `);
642
+ }
643
+
644
+ function resolveHost(config: ConfigFile | undefined, scope: SessionScope) {
645
+ return resolveHostFromArgs(filteredArgs, resolveConfigHost(config, scope));
646
+ }
647
+
648
+ async function loadConfig(): Promise<ConfigFile> {
649
+ try {
650
+ return JSON.parse(await readFile(CONFIG_PATH, "utf8")) as ConfigFile;
651
+ } catch {
652
+ return {};
653
+ }
654
+ }
655
+
656
+ async function saveConfig(nextConfig: ConfigFile) {
657
+ await mkdir(CONFIG_DIR, { recursive: true });
658
+ await writeFile(CONFIG_PATH, `${JSON.stringify(nextConfig, null, 2)}\n`, {
659
+ mode: 0o600,
660
+ });
661
+ await chmod(CONFIG_PATH, 0o600).catch(() => {});
662
+ }
663
+
664
+ async function saveSession(session: StoredSession, scope: SessionScope) {
665
+ const config = await loadConfig();
666
+ await saveConfig(withSavedSession(config, session, scope).config);
667
+ }
668
+
669
+ async function getStoredSession(host: string, scope: SessionScope) {
670
+ const config = await loadConfig();
671
+ return selectStoredSession(config, host, scope);
672
+ }
673
+
674
+ function formatTime(value: number | undefined) {
675
+ return value ? new Date(value).toISOString() : null;
676
+ }
677
+
678
+ function workspaceLabel(session: StoredSession) {
679
+ if (!session.organization) return "unknown workspace";
680
+ const slug = session.organization.slug;
681
+ return slug
682
+ ? `${session.organization.name} (${slug})`
683
+ : session.organization.name;
684
+ }
685
+
686
+ function loginNextCommandsMessage(
687
+ session: StoredSession,
688
+ stableSessionKey: string | undefined,
689
+ ) {
690
+ const workspaceName = session.organization?.name ?? "unknown workspace";
691
+ const workspaceSlug = session.organization?.slug ?? "unknown";
692
+ const agentName = session.agent?.name ?? "unknown agent";
693
+ const stableKeyText = stableSessionKey
694
+ ? ` Stable session key: ${stableSessionKey}.`
695
+ : "";
696
+ return `Next commands will use workspace ${workspaceName} (${workspaceSlug}) as ${agentName}.${stableKeyText}`;
697
+ }
698
+
699
+ function ambiguousSessionMessage(
700
+ config: ConfigFile,
701
+ scope: SessionScope,
702
+ host: string,
703
+ ) {
704
+ return `Multiple Dench sessions exist for ${host}, but none match this local agent context.
705
+ ${formatSessionChoices(config, scope, host)}
706
+ Run dench sessions to list sessions.
707
+ Run dench use <session-key-or-workspace-slug> to select one.
708
+ For long-lived agents or when the human asks, use DENCH_SESSION_KEY=<stable-human-readable-id> at login time.`;
709
+ }
710
+
711
+ function summarizeSessionEntry(
712
+ entry: StoredSessionEntry<StoredSession>,
713
+ currentKey: string | undefined,
714
+ ) {
715
+ return {
716
+ sessionKey: entry.key,
717
+ current: entry.key === currentKey,
718
+ host: entry.host,
719
+ workspace: entry.session.organization
720
+ ? {
721
+ name: entry.session.organization.name,
722
+ slug: entry.session.organization.slug ?? null,
723
+ id: entry.session.organization.id,
724
+ }
725
+ : null,
726
+ agent: entry.session.agent
727
+ ? {
728
+ name: entry.session.agent.name,
729
+ kind: entry.session.agent.kind,
730
+ id: entry.session.agent.id,
731
+ }
732
+ : null,
733
+ scope: {
734
+ label: entry.session.sessionScopeLabel ?? null,
735
+ key: entry.session.sessionScope ?? null,
736
+ },
737
+ savedAt: entry.session.savedAt ?? null,
738
+ savedAtIso: formatTime(entry.session.savedAt),
739
+ expiresAt: entry.session.sessionExpiresAt ?? null,
740
+ expiresAtIso: formatTime(entry.session.sessionExpiresAt),
741
+ };
742
+ }
743
+
744
+ function formatSessionLine(
745
+ entry: StoredSessionEntry<StoredSession>,
746
+ currentKey: string | undefined,
747
+ ) {
748
+ const marker = entry.key === currentKey ? "* " : " ";
749
+ const agent = entry.session.agent
750
+ ? `${entry.session.agent.name} [${entry.session.agent.kind}]`
751
+ : "unknown agent";
752
+ const scope = entry.session.sessionScopeLabel
753
+ ? `${entry.session.sessionScopeLabel}: ${entry.session.sessionScope ?? "unknown"}`
754
+ : (entry.session.sessionScope ?? "unknown scope");
755
+ const saved = formatTime(entry.session.savedAt) ?? "unknown";
756
+ const expires = formatTime(entry.session.sessionExpiresAt) ?? "unknown";
757
+ return `${marker}${entry.key}
758
+ host: ${entry.host}
759
+ workspace: ${workspaceLabel(entry.session)}
760
+ agent: ${agent}
761
+ scope: ${scope}
762
+ saved: ${saved}
763
+ expires: ${expires}`;
764
+ }
765
+
766
+ function sessionLines(config: ConfigFile, scope: SessionScope, host?: string) {
767
+ const resolvedHost = host ?? resolveHost(config, scope);
768
+ const selected = selectStoredSessionEntry(config, resolvedHost, scope);
769
+ const currentKey = selected.status === "found" ? selected.key : undefined;
770
+ return listStoredSessionEntries(config, host).map((entry) =>
771
+ formatSessionLine(entry, currentKey),
772
+ );
773
+ }
774
+
775
+ function formatSessionChoices(
776
+ config: ConfigFile,
777
+ scope: SessionScope,
778
+ host?: string,
779
+ ) {
780
+ const lines = sessionLines(config, scope, host);
781
+ return lines.length > 0 ? lines.join("\n") : " No Dench sessions found.";
782
+ }
783
+
784
+ async function listSessions() {
785
+ const config = await loadConfig();
786
+ const scope = resolveSessionScope({ args: filteredArgs });
787
+ const host = option("--host") ? resolveHostFromArgs(filteredArgs) : undefined;
788
+ const selected = selectStoredSessionEntry(
789
+ config,
790
+ host ?? resolveHost(config, scope),
791
+ scope,
792
+ );
793
+ const currentKey = selected.status === "found" ? selected.key : undefined;
794
+ const entries = listStoredSessionEntries(config, host);
795
+
796
+ if (json) {
797
+ print(entries.map((entry) => summarizeSessionEntry(entry, currentKey)));
798
+ return;
799
+ }
800
+
801
+ if (entries.length === 0) {
802
+ print("No Dench sessions found.");
803
+ return;
804
+ }
805
+
806
+ print(
807
+ entries.map((entry) => formatSessionLine(entry, currentKey)).join("\n"),
808
+ );
809
+ }
810
+
811
+ function removeSessionEntries(
812
+ config: ConfigFile,
813
+ entries: Array<StoredSessionEntry<StoredSession>>,
814
+ ) {
815
+ const removedKeys = new Set(entries.map((entry) => entry.key));
816
+ const removedHosts = new Set(entries.map((entry) => entry.host));
817
+ const sessions = { ...(config.sessions ?? {}) };
818
+ for (const key of removedKeys) {
819
+ delete sessions[key];
820
+ }
821
+
822
+ const currentSessionKeys = { ...(config.currentSessionKeys ?? {}) };
823
+ const removedScopeKeys = new Set<string>();
824
+ for (const [scopeKey, key] of Object.entries(currentSessionKeys)) {
825
+ if (removedKeys.has(key)) {
826
+ delete currentSessionKeys[scopeKey];
827
+ removedScopeKeys.add(scopeKey);
828
+ }
829
+ }
830
+
831
+ const currentHosts = { ...(config.currentHosts ?? {}) };
832
+ for (const [scopeKey, host] of Object.entries(currentHosts)) {
833
+ if (removedScopeKeys.has(scopeKey)) {
834
+ delete currentHosts[scopeKey];
835
+ continue;
836
+ }
837
+ const hasRemainingForHost = Object.values(sessions).some(
838
+ (session) => normalizeHost(session.host) === normalizeHost(host),
839
+ );
840
+ if (!hasRemainingForHost) {
841
+ delete currentHosts[scopeKey];
842
+ }
843
+ }
844
+
845
+ const nextConfig: ConfigFile = { ...config };
846
+ if (
847
+ nextConfig.currentSessionKey &&
848
+ removedKeys.has(nextConfig.currentSessionKey)
849
+ ) {
850
+ delete nextConfig.currentSessionKey;
851
+ }
852
+ if (Object.keys(sessions).length > 0) {
853
+ nextConfig.sessions = sessions;
854
+ } else {
855
+ delete nextConfig.sessions;
856
+ }
857
+ if (Object.keys(currentSessionKeys).length > 0) {
858
+ nextConfig.currentSessionKeys = currentSessionKeys;
859
+ } else {
860
+ delete nextConfig.currentSessionKeys;
861
+ }
862
+ if (Object.keys(currentHosts).length > 0) {
863
+ nextConfig.currentHosts = currentHosts;
864
+ } else {
865
+ delete nextConfig.currentHosts;
866
+ }
867
+ if (
868
+ nextConfig.currentHost &&
869
+ removedHosts.has(normalizeHost(nextConfig.currentHost)) &&
870
+ !Object.values(sessions).some(
871
+ (session) =>
872
+ normalizeHost(session.host) ===
873
+ normalizeHost(nextConfig.currentHost ?? ""),
874
+ )
875
+ ) {
876
+ delete nextConfig.currentHost;
877
+ }
878
+ return nextConfig;
879
+ }
880
+
881
+ function plural(count: number, singular: string, pluralForm = `${singular}s`) {
882
+ return `${count} ${count === 1 ? singular : pluralForm}`;
883
+ }
884
+
885
+ function logoutRemainingSummary(config: ConfigFile, host?: string) {
886
+ return {
887
+ total: listStoredSessionEntries(config).length,
888
+ forHost: host ? listStoredSessionEntries(config, host).length : undefined,
889
+ };
890
+ }
891
+
892
+ function logoutNextActions(
893
+ remaining: { total: number; forHost?: number },
894
+ host?: string,
895
+ ) {
896
+ if (remaining.total === 0) return [];
897
+ const actions = [
898
+ "Run dench sessions --json to inspect remaining saved sessions.",
899
+ ];
900
+ if (host && (remaining.forHost ?? 0) > 0) {
901
+ actions.push(
902
+ `Run dench logout --all --host ${host} to clear saved sessions for this host.`,
903
+ );
904
+ }
905
+ actions.push(
906
+ "Run dench logout --all to clear every saved Dench session on this machine.",
907
+ );
908
+ return actions;
909
+ }
910
+
911
+ function formatLogoutMessage(
912
+ primary: string,
913
+ remaining: { total: number; forHost?: number },
914
+ host?: string,
915
+ ) {
916
+ const lines = [primary];
917
+ if (host && (remaining.forHost ?? 0) > 0) {
918
+ lines.push(
919
+ `There ${remaining.forHost === 1 ? "is" : "are"} still ${plural(
920
+ remaining.forHost ?? 0,
921
+ "saved Dench session",
922
+ )} for ${host}. A future chat can reuse one if it is selected.`,
923
+ );
924
+ } else if (remaining.total > 0) {
925
+ lines.push(
926
+ `There ${remaining.total === 1 ? "is" : "are"} still ${plural(
927
+ remaining.total,
928
+ "saved Dench session",
929
+ )} on this machine.`,
930
+ );
931
+ }
932
+ const actions = logoutNextActions(remaining, host);
933
+ if (actions.length > 0) {
934
+ lines.push("Next actions:", ...actions.map((action) => ` - ${action}`));
935
+ }
936
+ return lines.join("\n");
937
+ }
938
+
939
+ async function selectSession() {
940
+ const selector = positional(1);
941
+ if (!selector) {
942
+ throw new Error(
943
+ "Missing session key or workspace slug. Run: dench sessions",
944
+ );
945
+ }
946
+
947
+ const config = await loadConfig();
948
+ const scope = resolveSessionScope({ args: filteredArgs });
949
+ const host = option("--host") ? resolveHostFromArgs(filteredArgs) : undefined;
950
+ const matches = findStoredSessionEntries(config, selector, host);
951
+ if (matches.length === 0) {
952
+ throw new Error(
953
+ `No Dench session matched "${selector}". Run dench sessions to see available sessions.`,
954
+ );
955
+ }
956
+ if (matches.length > 1) {
957
+ throw new Error(
958
+ `More than one Dench session matched "${selector}". Choose the exact session key:\n${matches
959
+ .map((entry) => formatSessionLine(entry, undefined))
960
+ .join("\n")}`,
961
+ );
962
+ }
963
+
964
+ const [entry] = matches;
965
+ await saveConfig({
966
+ ...withCurrentSessionSelection(config, {
967
+ host: entry.host,
968
+ scope,
969
+ sessionKey: entry.key,
970
+ }),
971
+ });
972
+
973
+ print(
974
+ json
975
+ ? { ok: true, session: summarizeSessionEntry(entry, entry.key) }
976
+ : `Now using Dench session ${entry.key} for ${workspaceLabel(entry.session)}.`,
977
+ );
978
+ }
979
+
980
+ async function logout() {
981
+ const config = await loadConfig();
982
+ const scope = resolveSessionScope({ args: filteredArgs });
983
+ const host = option("--host") ? resolveHostFromArgs(filteredArgs) : undefined;
984
+ const selector = option("--session") ?? positional(1);
985
+
986
+ if (hasFlag("--all")) {
987
+ if (selector) {
988
+ throw new Error("Use either --all or a specific session, not both.");
989
+ }
990
+ const entries = listStoredSessionEntries(config, host);
991
+ const removed = entries.length;
992
+ const nextConfig = host
993
+ ? removeSessionEntries(config, entries)
994
+ : removeAllStoredSessions(config);
995
+ const remaining = logoutRemainingSummary(nextConfig, host);
996
+ if (
997
+ removed > 0 ||
998
+ config.currentHost ||
999
+ config.currentSessionKey ||
1000
+ config.currentHosts ||
1001
+ config.currentSessionKeys
1002
+ ) {
1003
+ await saveConfig(nextConfig);
1004
+ }
1005
+ print(
1006
+ json
1007
+ ? {
1008
+ ok: true,
1009
+ removed,
1010
+ all: true,
1011
+ host: host ?? null,
1012
+ remainingSessions: remaining.total,
1013
+ remainingHostSessions: remaining.forHost ?? null,
1014
+ nextActions: logoutNextActions(remaining, host),
1015
+ }
1016
+ : removed > 0
1017
+ ? formatLogoutMessage(
1018
+ `Logged out of ${plural(removed, "Dench session")}.`,
1019
+ remaining,
1020
+ host,
1021
+ )
1022
+ : "No Dench sessions found.",
1023
+ );
1024
+ return;
1025
+ }
1026
+
1027
+ if (selector) {
1028
+ const matches = findStoredSessionEntries(config, selector, host);
1029
+ if (matches.length === 0) {
1030
+ throw new Error(
1031
+ `No Dench session matched "${selector}". Run dench sessions to see available sessions.`,
1032
+ );
1033
+ }
1034
+ if (matches.length > 1) {
1035
+ throw new Error(
1036
+ `More than one Dench session matched "${selector}". Choose the exact session key:\n${matches
1037
+ .map((entry) => formatSessionLine(entry, undefined))
1038
+ .join("\n")}`,
1039
+ );
1040
+ }
1041
+
1042
+ const nextConfig = removeSessionEntries(config, matches);
1043
+ const remaining = logoutRemainingSummary(nextConfig, matches[0].host);
1044
+ await saveConfig(nextConfig);
1045
+ print(
1046
+ json
1047
+ ? {
1048
+ ok: true,
1049
+ removed: 1,
1050
+ all: false,
1051
+ sessionKey: matches[0].key,
1052
+ remainingSessions: remaining.total,
1053
+ remainingHostSessions: remaining.forHost ?? null,
1054
+ nextActions: logoutNextActions(remaining, matches[0].host),
1055
+ }
1056
+ : formatLogoutMessage(
1057
+ `Logged out of Dench session ${matches[0].key}.`,
1058
+ remaining,
1059
+ matches[0].host,
1060
+ ),
1061
+ );
1062
+ return;
1063
+ }
1064
+
1065
+ const resolvedHost = host ?? resolveHost(config, scope);
1066
+ const result = removeStoredSession(config, resolvedHost, scope);
1067
+
1068
+ if (result.status === "ambiguous") {
1069
+ throw new Error(ambiguousSessionMessage(config, scope, resolvedHost));
1070
+ }
1071
+
1072
+ if (result.status === "missing") {
1073
+ print(
1074
+ json
1075
+ ? {
1076
+ ok: true,
1077
+ removed: 0,
1078
+ all: false,
1079
+ host: resolvedHost,
1080
+ sessionScope: scope.label,
1081
+ }
1082
+ : `No Dench session found for ${resolvedHost} in this local agent context.`,
1083
+ );
1084
+ return;
1085
+ }
1086
+
1087
+ await saveConfig(result.config);
1088
+ const remaining = logoutRemainingSummary(result.config, resolvedHost);
1089
+ print(
1090
+ json
1091
+ ? {
1092
+ ok: true,
1093
+ removed: 1,
1094
+ all: false,
1095
+ host: resolvedHost,
1096
+ sessionScope: scope.label,
1097
+ sessionKey: result.removedKey,
1098
+ remainingSessions: remaining.total,
1099
+ remainingHostSessions: remaining.forHost ?? null,
1100
+ nextActions: logoutNextActions(remaining, resolvedHost),
1101
+ }
1102
+ : formatLogoutMessage(
1103
+ `Logged out of Dench session ${result.removedKey}.`,
1104
+ remaining,
1105
+ resolvedHost,
1106
+ ),
1107
+ );
1108
+ }
1109
+
1110
+ function randomUrlSafe(bytes = 32) {
1111
+ const values = new Uint8Array(bytes);
1112
+ crypto.getRandomValues(values);
1113
+ return Buffer.from(values)
1114
+ .toString("base64")
1115
+ .replaceAll("+", "-")
1116
+ .replaceAll("/", "_")
1117
+ .replaceAll("=", "");
1118
+ }
1119
+
1120
+ function cleanNameSegment(value: string) {
1121
+ return value
1122
+ .trim()
1123
+ .replace(/[^a-zA-Z0-9._-]+/g, "-")
1124
+ .replace(/^-+|-+$/g, "")
1125
+ .slice(0, 24);
1126
+ }
1127
+
1128
+ function localUsername() {
1129
+ const fromEnv = process.env.USER ?? process.env.USERNAME;
1130
+ if (fromEnv?.trim()) return fromEnv;
1131
+
1132
+ try {
1133
+ return userInfo().username;
1134
+ } catch {
1135
+ return undefined;
1136
+ }
1137
+ }
1138
+
1139
+ function defaultAgentName(kind: string) {
1140
+ const username = cleanNameSegment(localUsername() ?? "");
1141
+ const machine = cleanNameSegment(hostname().split(".")[0] ?? "");
1142
+ const localPart = [username, machine].filter(Boolean).join("@");
1143
+ const uniquePart = randomUrlSafe(3).slice(0, 4).toLowerCase();
1144
+ const suffix = [localPart, uniquePart].filter(Boolean).join("-");
1145
+
1146
+ return `${agentKindLabel(kind)} (${suffix})`;
1147
+ }
1148
+
1149
+ function resolveAgentName(kind: string) {
1150
+ return option("--name")?.trim() || defaultAgentName(kind);
1151
+ }
1152
+
1153
+ async function sha256Hex(input: string) {
1154
+ const bytes = new TextEncoder().encode(input);
1155
+ const digest = await crypto.subtle.digest("SHA-256", bytes);
1156
+ return [...new Uint8Array(digest)]
1157
+ .map((byte) => byte.toString(16).padStart(2, "0"))
1158
+ .join("");
1159
+ }
1160
+
1161
+ function sleep(ms: number) {
1162
+ return new Promise((resolve) => setTimeout(resolve, ms));
1163
+ }
1164
+
1165
+ async function discoverConvexUrl(host: string) {
1166
+ const explicit = option("--convex-url");
1167
+ if (explicit) return explicit.trim();
1168
+ if (isLocalHost(host) && process.env.NEXT_PUBLIC_CONVEX_URL) {
1169
+ return process.env.NEXT_PUBLIC_CONVEX_URL.trim();
1170
+ }
1171
+
1172
+ const response = await fetch(`${host}/api/agent-config`);
1173
+ if (!response.ok) {
1174
+ throw new Error(`Could not load Dench config from ${host}`);
1175
+ }
1176
+ const payload = (await response.json()) as { convexUrl?: string };
1177
+ if (!payload.convexUrl) {
1178
+ throw new Error(`Dench config at ${host} is missing convexUrl`);
1179
+ }
1180
+ return payload.convexUrl;
1181
+ }
1182
+
1183
+ async function login() {
1184
+ const scope = resolveSessionScope({ args: filteredArgs });
1185
+ const explicitKey = explicitSessionKeyInput({ args: filteredArgs });
1186
+ const host = resolveHostFromArgs(filteredArgs, undefined);
1187
+ const convexUrl = await discoverConvexUrl(host);
1188
+ const client = new ConvexHttpClient(convexUrl);
1189
+ const code = randomUrlSafe(18);
1190
+ const sessionToken = `dch_agent_${randomUrlSafe(32)}`;
1191
+ const codeHash = await sha256Hex(code);
1192
+ const sessionTokenHash = await sha256Hex(sessionToken);
1193
+ const agentKind = normalizeAgentKind(option("--kind"));
1194
+ const agentName = resolveAgentName(agentKind);
1195
+ const request = await client.mutation(
1196
+ api.functions.agentWorkspace.createAgentLoginRequest,
1197
+ {
1198
+ codeHash,
1199
+ sessionTokenHash,
1200
+ agentName,
1201
+ agentKind,
1202
+ userAgent: "dench-cli",
1203
+ },
1204
+ );
1205
+ const approvalUrl = `${host}/agent-login/${encodeURIComponent(code)}`;
1206
+
1207
+ const openResult = await openUrl(approvalUrl, {
1208
+ noOpen: hasFlag("--no-open"),
1209
+ json,
1210
+ env: process.env,
1211
+ });
1212
+ logHuman(formatApprovalOpenMessage(approvalUrl, openResult));
1213
+ logHuman(
1214
+ "Before approving, switch Dench in your browser to the intended workspace.",
1215
+ );
1216
+ logHuman(
1217
+ `This request expires at ${new Date(request.expiresAt).toLocaleString()}.`,
1218
+ );
1219
+
1220
+ const timeoutMs = Number(option("--timeout-ms") ?? 15 * 60 * 1000);
1221
+ const deadline = Date.now() + timeoutMs;
1222
+ while (Date.now() < deadline) {
1223
+ const poll = await client.mutation(
1224
+ api.functions.agentWorkspace.pollAgentLoginRequest,
1225
+ { codeHash, sessionTokenHash },
1226
+ );
1227
+
1228
+ if (poll.status === "approved") {
1229
+ const storedSession: StoredSession = {
1230
+ host,
1231
+ convexUrl,
1232
+ sessionToken,
1233
+ organization: poll.organization,
1234
+ agent: poll.agent,
1235
+ sessionExpiresAt: poll.sessionExpiresAt,
1236
+ savedAt: Date.now(),
1237
+ };
1238
+ await saveSession(storedSession, scope);
1239
+ const message = loginNextCommandsMessage(
1240
+ storedSession,
1241
+ explicitKey?.source === "DENCH_SESSION_KEY"
1242
+ ? explicitKey.value
1243
+ : undefined,
1244
+ );
1245
+ print(
1246
+ json
1247
+ ? {
1248
+ ok: true,
1249
+ host,
1250
+ organization: poll.organization,
1251
+ agent: poll.agent,
1252
+ sessionExpiresAt: poll.sessionExpiresAt,
1253
+ message,
1254
+ }
1255
+ : message,
1256
+ );
1257
+ return;
1258
+ }
1259
+ if (poll.status === "rejected" || poll.status === "expired") {
1260
+ throw new Error(`Login ${poll.status}`);
1261
+ }
1262
+
1263
+ await sleep(2000);
1264
+ }
1265
+
1266
+ throw new Error(`Login timed out. Open ${approvalUrl} and run login again.`);
1267
+ }
1268
+
1269
+ function normalizeApiBase(input: string) {
1270
+ const trimmed = input.trim();
1271
+ const withProtocol = /^https?:\/\//.test(trimmed)
1272
+ ? trimmed
1273
+ : /^(localhost|127\.0\.0\.1|\[::1\])(?::|$)/.test(trimmed)
1274
+ ? `http://${trimmed}`
1275
+ : `https://${trimmed}`;
1276
+ return new URL(withProtocol).origin;
1277
+ }
1278
+
1279
+ function resolveAutonomousApiBase() {
1280
+ const explicit =
1281
+ option("--api-base") ??
1282
+ option("--host") ??
1283
+ process.env.DENCH_AUTONOMOUS_API_BASE ??
1284
+ process.env.DENCH_HOST;
1285
+ return explicit ? normalizeApiBase(explicit) : AUTONOMOUS_DEFAULT_API_BASE;
1286
+ }
1287
+
1288
+ async function resolveAutonomousSessionToken(apiBase: string) {
1289
+ const config = await loadConfig();
1290
+ const scope = resolveSessionScope({ args: filteredArgs });
1291
+ const stored = await getStoredSession(apiBase, scope);
1292
+ if (stored.status === "found") {
1293
+ return stored.session.sessionToken;
1294
+ }
1295
+ if (stored.status === "ambiguous") {
1296
+ throw new Error(ambiguousSessionMessage(config, scope, apiBase));
1297
+ }
1298
+ return undefined;
1299
+ }
1300
+
1301
+ function formatAutonomousRunResult(value: unknown) {
1302
+ const record = asRecord(value);
1303
+ if (!record) {
1304
+ return typeof value === "string" ? value : JSON.stringify(value, null, 2);
1305
+ }
1306
+
1307
+ const runId = stringField(record, "runId", "id");
1308
+ const workflowRunId = stringField(record, "workflowRunId");
1309
+ const status = stringField(record, "status");
1310
+ const url = stringField(record, "url", "runUrl");
1311
+
1312
+ if (!runId && !workflowRunId && !status && !url) {
1313
+ return JSON.stringify(value, null, 2);
1314
+ }
1315
+
1316
+ const lines = ["Started Long Session"];
1317
+ if (runId) lines.push(`Run ID: ${runId}`);
1318
+ if (workflowRunId) lines.push(`Workflow Run ID: ${workflowRunId}`);
1319
+ if (status) lines.push(`Status: ${status}`);
1320
+ if (url) lines.push(`URL: ${url}`);
1321
+ return lines.join("\n");
1322
+ }
1323
+
1324
+ function autonomousRunUrl(apiBase: string, runId: string) {
1325
+ return `${apiBase.replace(/\/+$/, "")}/labs/autonomous/${encodeURIComponent(runId)}`;
1326
+ }
1327
+
1328
+ function withAutonomousRunUrl(payload: unknown, apiBase: string) {
1329
+ const record = asRecord(payload);
1330
+ const runId = stringField(record, "runId", "id");
1331
+ if (!record || !runId || stringField(record, "runUrl", "url")) {
1332
+ return payload;
1333
+ }
1334
+ return { ...record, runUrl: autonomousRunUrl(apiBase, runId) };
1335
+ }
1336
+
1337
+ function formatAutonomousRunError(args: {
1338
+ status: number;
1339
+ payload: unknown;
1340
+ apiBase: string;
1341
+ hasSession: boolean;
1342
+ statusText: string;
1343
+ }) {
1344
+ const record = asRecord(args.payload);
1345
+ const error =
1346
+ stringField(record, "error", "message") ??
1347
+ (typeof args.payload === "string" && args.payload.trim()
1348
+ ? args.payload.trim()
1349
+ : args.statusText);
1350
+ const nextActions = stringArrayField(record, "nextActions");
1351
+ if (args.status === 401 && !args.hasSession) {
1352
+ nextActions.unshift(`Run: dench login --host ${args.apiBase}`);
1353
+ } else if (args.status === 401) {
1354
+ nextActions.push(
1355
+ "Run dench sessions --json to inspect saved sessions.",
1356
+ `Run dench login --host ${args.apiBase} if this session is expired.`,
1357
+ );
1358
+ } else if (args.status === 402 && nextActions.length === 0) {
1359
+ nextActions.push(
1360
+ "Top up AI credits in Dench usage/billing.",
1361
+ "Retry the same Long Session command after credits update.",
1362
+ );
1363
+ }
1364
+ if (stringField(record, "topUpUrl")) {
1365
+ nextActions.unshift(`Top up: ${stringField(record, "topUpUrl")}`);
1366
+ }
1367
+ return new CliError(`Long Session failed (${args.status}): ${error}`, {
1368
+ status: args.status,
1369
+ code: stringField(record, "code") ?? `http_${args.status}`,
1370
+ apiBase: args.apiBase,
1371
+ topUpUrl: stringField(record, "topUpUrl"),
1372
+ nextActions: [...new Set(nextActions)],
1373
+ });
1374
+ }
1375
+
1376
+ function safeLongSessionExample() {
1377
+ return 'dench autonomous run "Inspect this repo and summarize the relevant feature. Do not edit files, publish, deploy, spend money, send external messages, access secrets, or change production data." --duration 30m';
1378
+ }
1379
+
1380
+ function parseDurationMs(rawDuration?: string) {
1381
+ const raw = rawDuration?.trim().toLowerCase();
1382
+ if (!raw) return undefined;
1383
+ if (["until-done", "until_done", "done", "max"].includes(raw)) {
1384
+ return 24 * 60 * 60 * 1000;
1385
+ }
1386
+ const match = raw.match(
1387
+ /^(\d+(?:\.\d+)?)(m|min|mins|minute|minutes|h|hr|hrs|hour|hours)$/,
1388
+ );
1389
+ if (!match) {
1390
+ throw new CliError(
1391
+ "--duration must look like 30m, 1h, 3h, 5h, or until-done",
1392
+ {
1393
+ code: "invalid_duration",
1394
+ nextActions: [
1395
+ "Use one of: 30m, 1h, 3h, 5h, until-done.",
1396
+ safeLongSessionExample(),
1397
+ ],
1398
+ },
1399
+ );
1400
+ }
1401
+ const value = Number(match[1]);
1402
+ const unit = match[2];
1403
+ if (!Number.isFinite(value) || value <= 0) {
1404
+ throw new CliError("--duration must be positive", {
1405
+ code: "invalid_duration",
1406
+ nextActions: ["Use a positive duration like 30m or 1h."],
1407
+ });
1408
+ }
1409
+ const minutes = unit.startsWith("h") ? value * 60 : value;
1410
+ return Math.round(minutes * 60 * 1000);
1411
+ }
1412
+
1413
+ function durationPreset(rawDuration?: string) {
1414
+ const raw = rawDuration?.trim().toLowerCase();
1415
+ if (!raw) return undefined;
1416
+ return raw.replace("-", "_");
1417
+ }
1418
+
1419
+ function parseApprovalRules() {
1420
+ const explicit = option("--approval-rules")
1421
+ ?.split(",")
1422
+ .map((rule) => rule.trim())
1423
+ .filter(Boolean);
1424
+ if (explicit && explicit.length > 0) return explicit;
1425
+ const rules: string[] = [];
1426
+ if (hasFlag("--ask-before-publishing")) rules.push("publishing");
1427
+ if (hasFlag("--ask-before-spending")) rules.push("spending");
1428
+ if (hasFlag("--ask-before-external-messages")) {
1429
+ rules.push("external_messages");
1430
+ }
1431
+ if (hasFlag("--ask-before-production-changes")) {
1432
+ rules.push("production_changes");
1433
+ }
1434
+ if (hasFlag("--ask-before-secrets")) rules.push("secrets");
1435
+ return rules.length > 0
1436
+ ? rules
1437
+ : [
1438
+ "publishing",
1439
+ "spending",
1440
+ "external_messages",
1441
+ "production_changes",
1442
+ "secrets",
1443
+ ];
1444
+ }
1445
+
1446
+ async function parseResponseBody(response: Response) {
1447
+ const text = await response.text();
1448
+ if (!text) return {};
1449
+ try {
1450
+ return JSON.parse(text) as unknown;
1451
+ } catch {
1452
+ return text;
1453
+ }
1454
+ }
1455
+
1456
+ async function startAutonomousRun() {
1457
+ const goal = positionalsFrom(2).join(" ").trim();
1458
+ if (!goal) {
1459
+ throw new CliError("Missing goal", {
1460
+ code: "missing_goal",
1461
+ nextActions: [
1462
+ safeLongSessionExample(),
1463
+ "Keep the goal specific and include safety constraints for risky actions.",
1464
+ ],
1465
+ });
1466
+ }
1467
+
1468
+ const model = option("--model")?.trim();
1469
+ const duration = option("--duration");
1470
+ const body: JsonRecord = {
1471
+ goal,
1472
+ metadata: {
1473
+ sessionKind: "long_session",
1474
+ approvalRules: parseApprovalRules(),
1475
+ safetyConstraints: SAFE_LONG_SESSION_CONSTRAINTS,
1476
+ ...(duration ? { durationPreset: durationPreset(duration) } : {}),
1477
+ },
1478
+ };
1479
+ if (model) body.model = model;
1480
+ const timeBudgetMs = parseDurationMs(duration);
1481
+ if (timeBudgetMs) body.timeBudgetMs = timeBudgetMs;
1482
+
1483
+ const apiBase = resolveAutonomousApiBase();
1484
+ const agentSessionToken = await resolveAutonomousSessionToken(apiBase);
1485
+ const headers: Record<string, string> = {
1486
+ accept: "application/json",
1487
+ "content-type": "application/json",
1488
+ };
1489
+ if (agentSessionToken) {
1490
+ headers.authorization = `Bearer ${agentSessionToken}`;
1491
+ }
1492
+
1493
+ const response = await fetch(`${apiBase}/api/runs`, {
1494
+ method: "POST",
1495
+ headers,
1496
+ body: JSON.stringify(body),
1497
+ });
1498
+ const payload = await parseResponseBody(response);
1499
+
1500
+ if (!response.ok) {
1501
+ throw formatAutonomousRunError({
1502
+ status: response.status,
1503
+ payload,
1504
+ apiBase,
1505
+ hasSession: Boolean(agentSessionToken),
1506
+ statusText: response.statusText,
1507
+ });
1508
+ }
1509
+
1510
+ const output = withAutonomousRunUrl(payload, apiBase);
1511
+ throwIfCliError(output);
1512
+ print(json ? output : formatAutonomousRunResult(output));
1513
+ }
1514
+
1515
+ function formatUsd(cents: number | undefined) {
1516
+ const value = typeof cents === "number" && Number.isFinite(cents) ? cents : 0;
1517
+ return `$${(value / 100).toFixed(2)}`;
1518
+ }
1519
+
1520
+ function formatBillingStatus(value: unknown) {
1521
+ const record = asRecord(value);
1522
+ if (!record) return JSON.stringify(value, null, 2);
1523
+ const workspace = asRecord(record.workspace);
1524
+ const workspaceName = stringField(workspace, "name") ?? "Workspace";
1525
+ const workspaceSlug = stringField(workspace, "slug");
1526
+ const lines = [
1527
+ `Workspace: ${workspaceName}${workspaceSlug ? ` (${workspaceSlug})` : ""}`,
1528
+ `AI credits: ${formatUsd(numberField(record, "availableCreditsCents"))} available`,
1529
+ `Used credits: ${formatUsd(numberField(record, "creditConsumedCents"))}`,
1530
+ ];
1531
+ const status = stringField(record, "subscriptionStatus");
1532
+ if (status) lines.push(`Billing status: ${status}`);
1533
+ lines.push("", "Top up: dench billing topup --amount 5");
1534
+ return lines.join("\n");
1535
+ }
1536
+
1537
+ function parseBillingTopupAmountUsd() {
1538
+ const raw = option("--amount") ?? positional(2);
1539
+ const amount = Number(raw);
1540
+ if (!raw || !Number.isFinite(amount) || amount <= 0) {
1541
+ throw new CliError("Missing or invalid top-up amount", {
1542
+ code: "invalid_topup_amount",
1543
+ nextActions: ["Run dench billing topup --amount 5."],
1544
+ });
1545
+ }
1546
+ return amount;
1547
+ }
1548
+
1549
+ async function billingStatus(runtime: Runtime) {
1550
+ const sessionRuntime = requireSessionRuntime(runtime, "dench billing status");
1551
+ const status = await sessionRuntime.client.query(
1552
+ api.functions.agentWorkspace.getAgentBillingStatus,
1553
+ { sessionToken: sessionRuntime.sessionToken },
1554
+ );
1555
+ print(json ? status : formatBillingStatus(status));
1556
+ }
1557
+
1558
+ async function billingTopup(runtime: Runtime) {
1559
+ const sessionRuntime = requireSessionRuntime(runtime, "dench billing topup");
1560
+ const amountUsd = parseBillingTopupAmountUsd();
1561
+ const response = await fetch(
1562
+ `${sessionRuntime.host.replace(/\/+$/, "")}/api/stripe/create-ai-topup`,
1563
+ {
1564
+ method: "POST",
1565
+ headers: {
1566
+ accept: "application/json",
1567
+ "content-type": "application/json",
1568
+ authorization: `Bearer ${sessionRuntime.sessionToken}`,
1569
+ },
1570
+ body: JSON.stringify({
1571
+ amountUsd,
1572
+ requestId: crypto.randomUUID(),
1573
+ }),
1574
+ },
1575
+ );
1576
+ const payload = await parseResponseBody(response);
1577
+ if (!response.ok) {
1578
+ const errorRecord = asRecord(payload);
1579
+ throw new CliError(
1580
+ stringField(errorRecord, "error", "message") ??
1581
+ `Could not create top-up checkout (${response.status})`,
1582
+ {
1583
+ code: stringField(errorRecord, "code") ?? "topup_failed",
1584
+ status: response.status,
1585
+ },
1586
+ );
1587
+ }
1588
+
1589
+ const url = stringField(asRecord(payload), "url");
1590
+ const openResult = url
1591
+ ? await openUrl(url, {
1592
+ noOpen: hasFlag("--no-open"),
1593
+ json,
1594
+ env: process.env,
1595
+ })
1596
+ : ({ status: "skipped", reason: "missing-url" } as const);
1597
+ const openMessage =
1598
+ url && openResult.status === "opened"
1599
+ ? `Attempted to open Stripe Checkout in your browser. If it did not open, use this link: ${url}`
1600
+ : url && openResult.status === "failed"
1601
+ ? `Could not open browser automatically. Open this Stripe Checkout link: ${url}`
1602
+ : url
1603
+ ? `Open this Stripe Checkout link: ${url}`
1604
+ : "Stripe Checkout link created.";
1605
+ const output = {
1606
+ ok: true,
1607
+ amountUsd,
1608
+ url,
1609
+ openResult,
1610
+ message: openMessage,
1611
+ };
1612
+ print(json ? output : output.message);
1613
+ }
1614
+
1615
+ function hasDevEnv() {
1616
+ return Boolean(
1617
+ process.env.NEXT_PUBLIC_CONVEX_URL &&
1618
+ process.env.DENCH_WORKSPACE &&
1619
+ process.env.DENCH_DEV_AGENT_KEY,
1620
+ );
1621
+ }
1622
+
1623
+ function loginRequiredError(command = "this command") {
1624
+ return new CliError(`${command} requires dench login`, {
1625
+ code: "login_required",
1626
+ nextActions: [
1627
+ 'Run dench onboard --kind <kind> --name "AI Agent - Project".',
1628
+ "If already approved, run dench sessions --json, then dench use <session-key-or-workspace-slug>.",
1629
+ ],
1630
+ });
1631
+ }
1632
+
1633
+ function missingDevEnvError() {
1634
+ const required = [
1635
+ "NEXT_PUBLIC_CONVEX_URL",
1636
+ "DENCH_WORKSPACE",
1637
+ "DENCH_DEV_AGENT_KEY",
1638
+ ];
1639
+ return new CliError("Missing Dench dev environment", {
1640
+ code: "missing_dev_env",
1641
+ missingEnv: required.filter((name) => !process.env[name]?.trim()),
1642
+ nextActions: [
1643
+ "Set NEXT_PUBLIC_CONVEX_URL, DENCH_WORKSPACE, and DENCH_DEV_AGENT_KEY.",
1644
+ "Or remove --dev and run dench onboard to use a real agent session.",
1645
+ ],
1646
+ });
1647
+ }
1648
+
1649
+ async function getRuntime() {
1650
+ const scope = resolveSessionScope({ args: filteredArgs });
1651
+ if (!hasFlag("--dev")) {
1652
+ const config = await loadConfig();
1653
+ const host = resolveHost(config, scope);
1654
+ const stored = await getStoredSession(host, scope);
1655
+ if (stored.status === "found") {
1656
+ return {
1657
+ mode: "session" as const,
1658
+ host,
1659
+ client: new ConvexHttpClient(stored.session.convexUrl),
1660
+ sessionToken: stored.session.sessionToken,
1661
+ organization: stored.session.organization,
1662
+ };
1663
+ }
1664
+ if (stored.status === "ambiguous") {
1665
+ throw new CliError(ambiguousSessionMessage(config, scope, host), {
1666
+ code: "session_ambiguous",
1667
+ nextActions: [
1668
+ "Run dench sessions --json to inspect saved sessions.",
1669
+ "Run dench use <session-key-or-workspace-slug> to select one.",
1670
+ ],
1671
+ });
1672
+ }
1673
+ }
1674
+
1675
+ if (!hasDevEnv()) {
1676
+ throw hasFlag("--dev") ? missingDevEnvError() : loginRequiredError();
1677
+ }
1678
+
1679
+ return {
1680
+ mode: "dev" as const,
1681
+ client: new ConvexHttpClient(readEnv("NEXT_PUBLIC_CONVEX_URL")),
1682
+ workspaceArgs: {
1683
+ devKey: readEnv("DENCH_DEV_AGENT_KEY"),
1684
+ workspaceSlug: readEnv("DENCH_WORKSPACE"),
1685
+ },
1686
+ };
1687
+ }
1688
+
1689
+ type Runtime = Awaited<ReturnType<typeof getRuntime>>;
1690
+
1691
+ function requireSessionRuntime(
1692
+ runtime: Runtime,
1693
+ command: string,
1694
+ ): Extract<Runtime, { mode: "session" }> {
1695
+ if (runtime.mode !== "session") {
1696
+ throw loginRequiredError(command);
1697
+ }
1698
+ return runtime;
1699
+ }
1700
+
1701
+ async function printToolStatus(runtime: Runtime, toolkit?: string) {
1702
+ const sessionRuntime = requireSessionRuntime(runtime, "dench tool status");
1703
+ try {
1704
+ print(
1705
+ await sessionRuntime.client.action(
1706
+ api.functions.agentToolsNode.agentToolStatus,
1707
+ {
1708
+ sessionToken: sessionRuntime.sessionToken,
1709
+ toolkit,
1710
+ },
1711
+ ),
1712
+ );
1713
+ } catch (error) {
1714
+ const unavailable = toolGatewayUnavailable(error, { toolkit });
1715
+ if (!unavailable) throw error;
1716
+ print(json ? unavailable : formatToolUnavailable(unavailable));
1717
+ }
1718
+ }
1719
+
1720
+ function asRecord(value: unknown): JsonRecord | undefined {
1721
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
1722
+ return undefined;
1723
+ }
1724
+ return value as JsonRecord;
1725
+ }
1726
+
1727
+ function asRecordArray(value: unknown): JsonRecord[] {
1728
+ return Array.isArray(value)
1729
+ ? value.flatMap<JsonRecord>((item) => asRecord(item) ?? [])
1730
+ : [];
1731
+ }
1732
+
1733
+ function stringValue(value: unknown) {
1734
+ return typeof value === "string" && value.trim() ? value.trim() : undefined;
1735
+ }
1736
+
1737
+ function stringField(record: JsonRecord | undefined, ...names: string[]) {
1738
+ if (!record) return undefined;
1739
+ for (const name of names) {
1740
+ const value = stringValue(record[name]);
1741
+ if (value) return value;
1742
+ }
1743
+ return undefined;
1744
+ }
1745
+
1746
+ function numberField(record: JsonRecord | undefined, ...names: string[]) {
1747
+ if (!record) return undefined;
1748
+ for (const name of names) {
1749
+ const value = record[name];
1750
+ if (typeof value === "number" && Number.isFinite(value)) return value;
1751
+ }
1752
+ return undefined;
1753
+ }
1754
+
1755
+ function booleanField(record: JsonRecord | undefined, ...names: string[]) {
1756
+ if (!record) return undefined;
1757
+ for (const name of names) {
1758
+ const value = record[name];
1759
+ if (typeof value === "boolean") return value;
1760
+ }
1761
+ return undefined;
1762
+ }
1763
+
1764
+ function compactLine(value: unknown, maxLength = 140) {
1765
+ const text = stringValue(value)?.replace(/\s+/g, " ");
1766
+ if (!text) return undefined;
1767
+ return text.length > maxLength ? `${text.slice(0, maxLength - 1)}...` : text;
1768
+ }
1769
+
1770
+ function errorMessage(error: unknown) {
1771
+ return error instanceof Error ? error.message : String(error);
1772
+ }
1773
+
1774
+ function isMissingConvexFunction(error: unknown) {
1775
+ return /Could not find public function/i.test(errorMessage(error));
1776
+ }
1777
+
1778
+ function toolGatewayUnavailable(error: unknown, context: JsonRecord = {}) {
1779
+ const message = errorMessage(error);
1780
+ if (!/gateway API key|callGateway|Composio/i.test(message)) {
1781
+ return null;
1782
+ }
1783
+ const invalidKey = /Invalid gateway API key/i.test(message);
1784
+ const query = stringField(context, "query");
1785
+ const toolkit = stringField(context, "toolkit");
1786
+ const retryCommand = query
1787
+ ? `Retry dench tool search ${JSON.stringify(query)}${toolkit ? ` --toolkit ${toolkit}` : ""} after the gateway key is fixed.`
1788
+ : "Retry dench apps --json after the gateway key is fixed.";
1789
+ return {
1790
+ ok: false,
1791
+ available: false,
1792
+ code: invalidKey ? "invalid_gateway_api_key" : "tool_gateway_unavailable",
1793
+ reason: invalidKey
1794
+ ? "The workspace gateway key is invalid or out of sync."
1795
+ : "The connected-app gateway is unavailable.",
1796
+ ...context,
1797
+ nextActions: [
1798
+ "Ask a workspace admin to repair or rotate the Dench gateway key.",
1799
+ retryCommand,
1800
+ ],
1801
+ };
1802
+ }
1803
+
1804
+ function formatToolUnavailable(payload: JsonRecord) {
1805
+ return [
1806
+ "Connected apps unavailable.",
1807
+ `Reason: ${stringField(payload, "reason") ?? "tool gateway unavailable"}`,
1808
+ "",
1809
+ "Next actions:",
1810
+ ...(
1811
+ (Array.isArray(payload.nextActions)
1812
+ ? payload.nextActions
1813
+ : []) as unknown[]
1814
+ )
1815
+ .filter((item): item is string => typeof item === "string")
1816
+ .map((item) => ` - ${item}`),
1817
+ ].join("\n");
1818
+ }
1819
+
1820
+ const READ_ONLY_TOOL_ACTIONS = new Set([
1821
+ "FETCH",
1822
+ "GET",
1823
+ "LIST",
1824
+ "SEARCH",
1825
+ "READ",
1826
+ "FIND",
1827
+ ]);
1828
+
1829
+ function toolSlugTokens(toolSlug: string) {
1830
+ return toolSlug
1831
+ .toUpperCase()
1832
+ .split(/[^A-Z0-9]+/)
1833
+ .filter(Boolean);
1834
+ }
1835
+
1836
+ function toolApprovalHint(toolSlug: string | undefined) {
1837
+ if (!toolSlug) return "approval unknown";
1838
+ const readOnly = toolSlugTokens(toolSlug).find((token) =>
1839
+ READ_ONLY_TOOL_ACTIONS.has(token),
1840
+ );
1841
+ return readOnly ? `read-only (${readOnly})` : "approval likely";
1842
+ }
1843
+
1844
+ function compactConnection(connection: JsonRecord) {
1845
+ return {
1846
+ toolkit: stringField(connection, "toolkit") ?? "unknown",
1847
+ status: stringField(connection, "status") ?? "unknown",
1848
+ connectedAccountId:
1849
+ stringField(connection, "connectedAccountId", "connected_account_id") ??
1850
+ stringField(connection, "id"),
1851
+ accountLabel:
1852
+ stringField(connection, "accountLabel", "account_label", "label") ?? null,
1853
+ };
1854
+ }
1855
+
1856
+ function summarizeConnectedApps(value: unknown) {
1857
+ const record = asRecord(value);
1858
+ const connections = asRecordArray(record?.connections).map(compactConnection);
1859
+ return {
1860
+ available: true,
1861
+ count: connections.length,
1862
+ connections,
1863
+ };
1864
+ }
1865
+
1866
+ function workspaceSlugFromContext(context: JsonRecord) {
1867
+ const workspace = asRecord(context.workspace);
1868
+ return (
1869
+ stringField(workspace, "slug") ?? stringField(context, "workspaceSlug")
1870
+ );
1871
+ }
1872
+
1873
+ function workspaceUrls(host: string, workspaceSlug: string | undefined) {
1874
+ if (!workspaceSlug) return {};
1875
+ const base = normalizeHost(host);
1876
+ return {
1877
+ dashboardUrl: `${base}/${workspaceSlug}`,
1878
+ approvalsUrl: `${base}/${workspaceSlug}/approvals`,
1879
+ };
1880
+ }
1881
+
1882
+ function nextContextCommands() {
1883
+ return [
1884
+ "dench onboard --json",
1885
+ "dench tasks --json",
1886
+ 'dench log "Brief progress update"',
1887
+ 'dench tool search "what you need" --toolkit <app>',
1888
+ 'dench approval request "Risky action to approve"',
1889
+ ];
1890
+ }
1891
+
1892
+ async function connectedAppsContext(runtime: Runtime) {
1893
+ if (runtime.mode !== "session") {
1894
+ return {
1895
+ available: false,
1896
+ reason: "Connected apps require a logged-in Dench agent session.",
1897
+ count: 0,
1898
+ connections: [],
1899
+ };
1900
+ }
1901
+
1902
+ try {
1903
+ return summarizeConnectedApps(
1904
+ await runtime.client.action(
1905
+ api.functions.agentToolsNode.agentToolStatus,
1906
+ {
1907
+ sessionToken: runtime.sessionToken,
1908
+ },
1909
+ ),
1910
+ );
1911
+ } catch (error) {
1912
+ const unavailable = toolGatewayUnavailable(error);
1913
+ if (unavailable) {
1914
+ return {
1915
+ ...unavailable,
1916
+ count: 0,
1917
+ connections: [],
1918
+ };
1919
+ }
1920
+ return {
1921
+ available: false,
1922
+ error: errorMessage(error),
1923
+ count: 0,
1924
+ connections: [],
1925
+ };
1926
+ }
1927
+ }
1928
+
1929
+ async function buildContext(runtime: Runtime) {
1930
+ const mine =
1931
+ runtime.mode === "session"
1932
+ ? ((await runtime.client.query(
1933
+ api.functions.agentWorkspace.whatCanIDoHere,
1934
+ {
1935
+ sessionToken: runtime.sessionToken,
1936
+ },
1937
+ )) as JsonRecord)
1938
+ : summarizeDevMine(
1939
+ runtime.workspaceArgs.workspaceSlug,
1940
+ await devOverview(runtime),
1941
+ );
1942
+
1943
+ const urls =
1944
+ runtime.mode === "session"
1945
+ ? workspaceUrls(runtime.host, workspaceSlugFromContext(mine))
1946
+ : {};
1947
+
1948
+ return {
1949
+ ...mine,
1950
+ ...urls,
1951
+ connectedApps: await connectedAppsContext(runtime),
1952
+ nextCommands: nextContextCommands(),
1953
+ };
1954
+ }
1955
+
1956
+ async function runOnboarding() {
1957
+ let runtime: Runtime;
1958
+ try {
1959
+ runtime = await getRuntime();
1960
+ } catch (error) {
1961
+ const message = error instanceof Error ? error.message : String(error);
1962
+ const isLoginRequired =
1963
+ error instanceof CliError && error.payload.code === "login_required";
1964
+ if (!isLoginRequired && !message.includes("Not logged in")) {
1965
+ throw error;
1966
+ }
1967
+ logHuman("No Dench session found. Starting login first.");
1968
+ await login();
1969
+ runtime = await getRuntime();
1970
+ }
1971
+
1972
+ const context = await buildContext(runtime);
1973
+ print(
1974
+ json
1975
+ ? { ok: true, context }
1976
+ : [
1977
+ "Dench onboarding complete.",
1978
+ "",
1979
+ formatContext(context),
1980
+ "",
1981
+ "Setup is complete when the workspace and agent above are correct.",
1982
+ ].join("\n"),
1983
+ );
1984
+ }
1985
+
1986
+ function formatCount(value: unknown) {
1987
+ return typeof value === "number" ? String(value) : "0";
1988
+ }
1989
+
1990
+ function agentDisplayName(agent: JsonRecord | undefined) {
1991
+ return (
1992
+ stringField(agent, "name") ??
1993
+ stringField(agent, "slug") ??
1994
+ stringField(agent, "id") ??
1995
+ stringField(agent, "_id") ??
1996
+ "unknown agent"
1997
+ );
1998
+ }
1999
+
2000
+ function formatContext(context: JsonRecord) {
2001
+ const counts = asRecord(context.counts);
2002
+ const agent = asRecord(context.agent);
2003
+ const workspace = asRecord(context.workspace);
2004
+ const connectedApps = asRecord(context.connectedApps);
2005
+ const connections = asRecordArray(connectedApps?.connections);
2006
+ const tasksRecord = asRecord(context.tasks);
2007
+ const tasks =
2008
+ asRecordArray(tasksRecord?.assignedToMe).length > 0 ||
2009
+ asRecordArray(tasksRecord?.open).length > 0
2010
+ ? [
2011
+ ...asRecordArray(tasksRecord?.assignedToMe),
2012
+ ...asRecordArray(tasksRecord?.open),
2013
+ ]
2014
+ : asRecordArray(context.tasks);
2015
+ const approvals = asRecordArray(context.pendingApprovals);
2016
+ const suggestedWork = asRecordArray(context.suggestedWork);
2017
+ const memories = asRecordArray(context.memory);
2018
+ const nextActions = Array.isArray(context.nextActions)
2019
+ ? context.nextActions.filter(
2020
+ (item): item is string => typeof item === "string",
2021
+ )
2022
+ : [];
2023
+ const lines = [
2024
+ "Dench context",
2025
+ `Workspace: ${
2026
+ stringField(workspace, "name") ??
2027
+ stringField(context, "workspace") ??
2028
+ "unknown"
2029
+ }${
2030
+ (stringField(workspace, "slug") ?? stringField(context, "workspaceSlug"))
2031
+ ? ` (${stringField(workspace, "slug") ?? stringField(context, "workspaceSlug")})`
2032
+ : ""
2033
+ }`,
2034
+ `Agent: ${agentDisplayName(agent)}${
2035
+ stringField(agent, "kind") ? ` [${stringField(agent, "kind")}]` : ""
2036
+ }`,
2037
+ `Counts: ${formatCount(counts?.tasks ?? tasks.length)} visible tasks, ${formatCount(
2038
+ counts?.myAssignedTasks ??
2039
+ asRecordArray(tasksRecord?.assignedToMe).length,
2040
+ )} assigned to me, ${formatCount(
2041
+ counts?.myPendingApprovals ?? approvals.length,
2042
+ )} pending approvals`,
2043
+ ];
2044
+
2045
+ const dashboardUrl = stringField(context, "dashboardUrl");
2046
+ const approvalsUrl = stringField(context, "approvalsUrl");
2047
+ if (dashboardUrl || approvalsUrl) {
2048
+ lines.push(
2049
+ `Dashboard: ${dashboardUrl ?? "unknown"}`,
2050
+ `Approvals: ${approvalsUrl ?? "unknown"}`,
2051
+ );
2052
+ }
2053
+
2054
+ lines.push("", "Assigned tasks:");
2055
+
2056
+ if (tasks.length === 0) {
2057
+ lines.push(" none");
2058
+ } else {
2059
+ lines.push(
2060
+ ...tasks
2061
+ .slice(0, 5)
2062
+ .map(
2063
+ (task) =>
2064
+ ` - ${stringField(task, "title") ?? stringField(task, "id") ?? "untitled"} [${stringField(task, "status") ?? "unknown"}]`,
2065
+ ),
2066
+ );
2067
+ }
2068
+
2069
+ lines.push("", "Pending approvals requested by me:");
2070
+ if (approvals.length === 0) {
2071
+ lines.push(" none");
2072
+ } else {
2073
+ lines.push(
2074
+ ...approvals
2075
+ .slice(0, 5)
2076
+ .map(
2077
+ (approval) =>
2078
+ ` - ${stringField(approval, "title") ?? stringField(approval, "id") ?? "untitled"} [${stringField(approval, "risk") ?? "unknown"}]`,
2079
+ ),
2080
+ );
2081
+ }
2082
+
2083
+ lines.push("", "Connected apps:");
2084
+ if (connectedApps?.available === false) {
2085
+ lines.push(
2086
+ ` unavailable: ${
2087
+ stringField(connectedApps, "error", "reason") ?? "could not check apps"
2088
+ }`,
2089
+ );
2090
+ } else if (connections.length === 0) {
2091
+ lines.push(" none");
2092
+ } else {
2093
+ lines.push(
2094
+ ...connections
2095
+ .slice(0, 8)
2096
+ .map(
2097
+ (connection) =>
2098
+ ` - ${stringField(connection, "toolkit") ?? "unknown"} [${stringField(connection, "status") ?? "unknown"}]${
2099
+ stringField(connection, "accountLabel")
2100
+ ? ` ${stringField(connection, "accountLabel")}`
2101
+ : ""
2102
+ }`,
2103
+ ),
2104
+ );
2105
+ }
2106
+
2107
+ lines.push("", "Suggested work:");
2108
+ if (suggestedWork.length === 0) {
2109
+ lines.push(" none");
2110
+ } else {
2111
+ lines.push(
2112
+ ...suggestedWork
2113
+ .slice(0, 5)
2114
+ .map(
2115
+ (item) =>
2116
+ ` - ${stringField(item, "title") ?? stringField(item, "id") ?? "untitled"}`,
2117
+ ),
2118
+ );
2119
+ }
2120
+
2121
+ lines.push("", "Memory:");
2122
+ if (memories.length === 0) {
2123
+ lines.push(" none");
2124
+ } else {
2125
+ lines.push(
2126
+ ...memories
2127
+ .slice(0, 5)
2128
+ .map(
2129
+ (memory) =>
2130
+ ` - ${stringField(memory, "key") ?? "memory"}: ${compactLine(stringField(memory, "text"), 100) ?? ""}`,
2131
+ ),
2132
+ );
2133
+ }
2134
+
2135
+ if (nextActions.length > 0) {
2136
+ lines.push(
2137
+ "",
2138
+ "Next actions:",
2139
+ ...nextActions.map((action) => ` - ${action}`),
2140
+ );
2141
+ }
2142
+
2143
+ lines.push("", "Useful next commands:");
2144
+ lines.push(
2145
+ ...nextContextCommands().map((command) => ` - ${command}`),
2146
+ "",
2147
+ "Use --json for structured output.",
2148
+ );
2149
+ return lines.join("\n");
2150
+ }
2151
+
2152
+ function toolkitSlugFromTool(tool: JsonRecord) {
2153
+ const toolkit = tool.toolkit;
2154
+ if (typeof toolkit === "string") return toolkit;
2155
+ const toolkitRecord = asRecord(toolkit);
2156
+ return (
2157
+ stringField(toolkitRecord, "slug", "name") ??
2158
+ stringField(tool, "toolkit_slug")
2159
+ );
2160
+ }
2161
+
2162
+ function connectedToolkitSet(data: JsonRecord | undefined) {
2163
+ const connected = new Set<string>();
2164
+ const connectedToolkits = Array.isArray(data?.connected_toolkits)
2165
+ ? data?.connected_toolkits
2166
+ : [];
2167
+ for (const toolkit of connectedToolkits) {
2168
+ const value = stringValue(toolkit);
2169
+ if (value) connected.add(value.toLowerCase());
2170
+ }
2171
+ for (const status of asRecordArray(data?.toolkit_connection_statuses)) {
2172
+ const toolkit = stringField(status, "toolkit");
2173
+ const isConnected = booleanField(status, "has_active_connection");
2174
+ if (toolkit && isConnected) connected.add(toolkit.toLowerCase());
2175
+ }
2176
+ return connected;
2177
+ }
2178
+
2179
+ function searchToolItems(searchResult: unknown) {
2180
+ const root = asRecord(searchResult);
2181
+ const data = asRecord(root?.data) ?? root;
2182
+ const directItems = asRecordArray(data?.items);
2183
+ if (directItems.length > 0) {
2184
+ return { data, items: directItems };
2185
+ }
2186
+
2187
+ const schemas = asRecord(data?.tool_schemas);
2188
+ if (!schemas) return { data, items: [] };
2189
+ return {
2190
+ data,
2191
+ items: Object.entries(schemas).flatMap<JsonRecord>(([slug, value]) => {
2192
+ const tool = asRecord(value);
2193
+ return tool
2194
+ ? [{ ...tool, tool_slug: stringField(tool, "tool_slug") ?? slug }]
2195
+ : [];
2196
+ }),
2197
+ };
2198
+ }
2199
+
2200
+ function compactToolSearchResult(searchResult: unknown) {
2201
+ const root = asRecord(searchResult);
2202
+ const { data, items } = searchToolItems(searchResult);
2203
+ const connected = connectedToolkitSet(data);
2204
+ const tools = items.map((tool) => {
2205
+ const toolkit = toolkitSlugFromTool(tool);
2206
+ const connectionStatus = asRecord(tool.connection_status);
2207
+ const isConnected =
2208
+ booleanField(connectionStatus, "is_connected") ??
2209
+ (toolkit ? connected.has(toolkit.toLowerCase()) : undefined);
2210
+ const slug = stringField(tool, "slug", "tool_slug") ?? "unknown_tool";
2211
+ return {
2212
+ slug,
2213
+ name: stringField(tool, "display_name", "name") ?? slug,
2214
+ toolkit: toolkit ?? "unknown",
2215
+ approval: toolApprovalHint(slug),
2216
+ connected:
2217
+ isConnected === undefined
2218
+ ? "connection unknown"
2219
+ : isConnected
2220
+ ? "connected"
2221
+ : "not connected",
2222
+ description: compactLine(tool.description) ?? null,
2223
+ };
2224
+ });
2225
+
2226
+ return {
2227
+ ok: root?.ok ?? true,
2228
+ query: stringField(root, "query") ?? stringField(data, "query") ?? null,
2229
+ returned: tools.length,
2230
+ total:
2231
+ numberField(data, "total_items", "total", "count") ??
2232
+ numberField(root, "total_items", "total", "count") ??
2233
+ tools.length,
2234
+ tools,
2235
+ };
2236
+ }
2237
+
2238
+ function formatToolSearchResult(searchResult: unknown) {
2239
+ const summary = compactToolSearchResult(searchResult);
2240
+ const query = summary.query ? ` for "${summary.query}"` : "";
2241
+ if (summary.tools.length === 0) {
2242
+ return `No matching tools found${query}.\nUse --json if you need the raw provider response.`;
2243
+ }
2244
+
2245
+ const lines = [`Top tools${query}:`];
2246
+ for (const [index, tool] of summary.tools.entries()) {
2247
+ lines.push(
2248
+ `${index + 1}. ${tool.slug} - ${tool.name}`,
2249
+ ` toolkit: ${tool.toolkit} | ${tool.approval} | ${tool.connected}`,
2250
+ );
2251
+ if (tool.description) {
2252
+ lines.push(` ${tool.description}`);
2253
+ }
2254
+ }
2255
+ lines.push(
2256
+ "",
2257
+ "Use dench tool run <slug> --args '{...}' when ready.",
2258
+ "Use --json only when you need full schemas or raw arguments.",
2259
+ );
2260
+ return lines.join("\n");
2261
+ }
2262
+
2263
+ function printToolSearchResult(searchResult: unknown) {
2264
+ print(json ? searchResult : formatToolSearchResult(searchResult));
2265
+ }
2266
+
2267
+ const SENSITIVE_KEY_RE =
2268
+ /(?:otp|oneTime|verification|security|passcode|password|secret|api[_-]?key|access[_-]?token|refresh[_-]?token|authorization)/i;
2269
+
2270
+ function redactSensitiveString(value: string) {
2271
+ return value
2272
+ .replace(
2273
+ /\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,
2274
+ "$1[redacted-code]",
2275
+ )
2276
+ .replace(
2277
+ /\b((?:api[_\s-]?key|secret|token|password)[^.\n\r]{0,60}?)([A-Za-z0-9_=-]{12,})\b/gi,
2278
+ "$1[redacted-secret]",
2279
+ );
2280
+ }
2281
+
2282
+ function redactSensitiveDisplay(value: unknown, key?: string): unknown {
2283
+ if (typeof value === "string") {
2284
+ return key && SENSITIVE_KEY_RE.test(key)
2285
+ ? "[redacted]"
2286
+ : redactSensitiveString(value);
2287
+ }
2288
+ if (typeof value === "number" && key && SENSITIVE_KEY_RE.test(key)) {
2289
+ return "[redacted]";
2290
+ }
2291
+ if (Array.isArray(value)) {
2292
+ return value.map((item) => redactSensitiveDisplay(item));
2293
+ }
2294
+ const record = asRecord(value);
2295
+ if (!record) return value;
2296
+
2297
+ return Object.fromEntries(
2298
+ Object.entries(record).map(([entryKey, entryValue]) => [
2299
+ entryKey,
2300
+ redactSensitiveDisplay(entryValue, entryKey),
2301
+ ]),
2302
+ );
2303
+ }
2304
+
2305
+ function connectionUrlFromToolConnect(result: unknown) {
2306
+ const record = asRecord(result);
2307
+ const data = asRecord(record?.data);
2308
+ return (
2309
+ stringField(record, "connectUrl", "redirect_url", "redirectUrl", "url") ??
2310
+ stringField(data, "redirect_url", "redirectUrl", "url")
2311
+ );
2312
+ }
2313
+
2314
+ function withToolConnectSummary(result: unknown) {
2315
+ const record = asRecord(result);
2316
+ if (!record) return result;
2317
+ const toolkit = stringField(record, "toolkit") ?? "app";
2318
+ const connectUrl = connectionUrlFromToolConnect(record);
2319
+ if (!connectUrl) return record;
2320
+ return {
2321
+ ...record,
2322
+ connectUrl,
2323
+ linkText: `Connect ${toolkit} here`,
2324
+ message: `Connect ${toolkit} here: ${connectUrl}`,
2325
+ };
2326
+ }
2327
+
2328
+ function formatToolConnectResult(result: unknown) {
2329
+ const record = asRecord(withToolConnectSummary(result));
2330
+ if (!record) return String(result);
2331
+ const toolkit = stringField(record, "toolkit") ?? "app";
2332
+ const connectUrl = stringField(record, "connectUrl");
2333
+ if (connectUrl) {
2334
+ return `Connect ${toolkit} here: ${connectUrl}`;
2335
+ }
2336
+ return JSON.stringify(record, null, 2);
2337
+ }
2338
+
2339
+ function withToolRunGuidance(runtime: Runtime, result: unknown) {
2340
+ const record = asRecord(result);
2341
+ if (!record || record.requiresApproval !== true) return result;
2342
+ const approvalId = stringField(record, "approvalId");
2343
+ const statusContext =
2344
+ runtime.mode === "session"
2345
+ ? workspaceUrls(runtime.host, runtime.organization?.slug)
2346
+ : {};
2347
+ const approvalCommand = approvalId
2348
+ ? `dench approval approve ${approvalId} --evidence "User said yes in chat" --json`
2349
+ : undefined;
2350
+ return {
2351
+ ...record,
2352
+ ...statusContext,
2353
+ ...(approvalCommand ? { approvalCommand } : {}),
2354
+ nextActions: [
2355
+ "Ask the human for an explicit yes/no in chat.",
2356
+ ...(approvalCommand ? [`If yes, run: ${approvalCommand}`] : []),
2357
+ approvalId
2358
+ ? `Rerun the exact same dench tool run command with --approval ${approvalId}.`
2359
+ : "Rerun the exact same dench tool run command after approval.",
2360
+ ],
2361
+ };
2362
+ }
2363
+
2364
+ function printToolRunResult(runtime: Runtime, result: unknown) {
2365
+ const guided = withToolRunGuidance(runtime, result);
2366
+ print(json ? guided : redactSensitiveDisplay(guided));
2367
+ }
2368
+
2369
+ async function devOverview(runtime: Awaited<ReturnType<typeof getRuntime>>) {
2370
+ if (runtime.mode !== "dev") {
2371
+ throw new Error("Not in dev mode");
2372
+ }
2373
+ return (await runtime.client.query(
2374
+ api.functions.agentWorkspace.devListWorkspaceOverview,
2375
+ runtime.workspaceArgs,
2376
+ )) as WorkspaceOverview;
2377
+ }
2378
+
2379
+ function summarizeDevOverview(workspaceSlug: string, data: WorkspaceOverview) {
2380
+ return {
2381
+ workspace: workspaceSlug,
2382
+ agents: data.agents.length,
2383
+ projects: data.projects.length,
2384
+ tasks: data.tasks.length,
2385
+ approvals: data.approvals.filter(
2386
+ (approval) => approval.status === "pending",
2387
+ ).length,
2388
+ rules: data.rules.length,
2389
+ };
2390
+ }
2391
+
2392
+ function compactTask(task: JsonRecord) {
2393
+ return {
2394
+ id: task._id ?? task.id,
2395
+ title: task.title,
2396
+ status: task.status,
2397
+ priority: task.priority,
2398
+ risk: task.risk,
2399
+ assignedAgentId: task.assignedAgentId,
2400
+ };
2401
+ }
2402
+
2403
+ function compactApproval(approval: JsonRecord) {
2404
+ return {
2405
+ id: approval.id ?? approval._id,
2406
+ title: approval.title,
2407
+ risk: approval.risk,
2408
+ status: approval.status,
2409
+ taskId: approval.taskId,
2410
+ createdAt: approval.createdAt,
2411
+ };
2412
+ }
2413
+
2414
+ function summarizeAgentStatus(
2415
+ status: AgentStatus,
2416
+ tasks: JsonRecord[],
2417
+ host?: string,
2418
+ ) {
2419
+ const agentId = status.agent?.id;
2420
+ const assignedTasks = agentId
2421
+ ? tasks.filter((task) => task.assignedAgentId === agentId)
2422
+ : [];
2423
+ const pendingApprovals = (status.approvals ?? []).filter(
2424
+ (approval) => approval.status === "pending" && approval.agentId === agentId,
2425
+ );
2426
+ const urls = host ? workspaceUrls(host, status.workspaceSlug) : {};
2427
+
2428
+ return {
2429
+ workspace: status.workspace,
2430
+ workspaceSlug: status.workspaceSlug,
2431
+ ...urls,
2432
+ agent: status.agent,
2433
+ counts: {
2434
+ ...(status.counts ?? {}),
2435
+ myAssignedTasks: assignedTasks.length,
2436
+ myPendingApprovals: pendingApprovals.length,
2437
+ },
2438
+ rules: status.rules ?? [],
2439
+ tasks: assignedTasks.map(compactTask),
2440
+ pendingApprovals: pendingApprovals.map(compactApproval),
2441
+ };
2442
+ }
2443
+
2444
+ function summarizeDevMine(workspaceSlug: string, data: WorkspaceOverview) {
2445
+ const agentId = option("--agent") ?? data.agents[0]?._id;
2446
+ const agent = data.agents.find((item) => item._id === agentId) ?? null;
2447
+ const tasks = data.tasks.filter((task) => task.assignedAgentId === agentId);
2448
+ const pendingApprovals = data.approvals.filter(
2449
+ (approval) => approval.status === "pending" && approval.agentId === agentId,
2450
+ );
2451
+
2452
+ return {
2453
+ workspace: workspaceSlug,
2454
+ agent,
2455
+ counts: {
2456
+ agents: data.agents.length,
2457
+ projects: data.projects.length,
2458
+ tasks: data.tasks.length,
2459
+ pendingApprovals: data.approvals.filter(
2460
+ (approval) => approval.status === "pending",
2461
+ ).length,
2462
+ rules: data.rules.length,
2463
+ myAssignedTasks: tasks.length,
2464
+ myPendingApprovals: pendingApprovals.length,
2465
+ },
2466
+ rules: data.rules,
2467
+ tasks: tasks.map(compactTask),
2468
+ pendingApprovals: pendingApprovals.map(compactApproval),
2469
+ };
2470
+ }
2471
+
2472
+ async function defaultDevAgentId(
2473
+ runtime: Awaited<ReturnType<typeof getRuntime>>,
2474
+ ) {
2475
+ const data = await devOverview(runtime);
2476
+ const agent = data.agents[0];
2477
+ if (!agent) {
2478
+ throw new Error(
2479
+ 'No agent registered yet. Run: dench register --name "AI Agent" --kind other --dev',
2480
+ );
2481
+ }
2482
+ return agent._id;
2483
+ }
2484
+
2485
+ async function listArtifacts(runtime: Runtime, limit?: number) {
2486
+ if (runtime.mode === "session") {
2487
+ try {
2488
+ return (await runtime.client.query(
2489
+ api.functions.agentWorkspace.agentListArtifacts,
2490
+ {
2491
+ sessionToken: runtime.sessionToken,
2492
+ limit,
2493
+ },
2494
+ )) as JsonRecord[];
2495
+ } catch (error) {
2496
+ if (!isMissingConvexFunction(error)) throw error;
2497
+ const context = await buildContext(runtime);
2498
+ return asRecordArray(asRecord(context)?.recentArtifacts).slice(
2499
+ 0,
2500
+ limit ?? 25,
2501
+ );
2502
+ }
2503
+ }
2504
+ const data = await devOverview(runtime);
2505
+ return asRecordArray(data.recentArtifacts);
2506
+ }
2507
+
2508
+ async function listSuggestedWork(runtime: Runtime) {
2509
+ if (runtime.mode === "session") {
2510
+ const context = await buildContext(runtime);
2511
+ return asRecordArray(asRecord(context)?.suggestedWork);
2512
+ }
2513
+ return suggestedWorkFromArtifacts(await listArtifacts(runtime, 50));
2514
+ }
2515
+
2516
+ function suggestedWorkFromArtifacts(artifacts: JsonRecord[]) {
2517
+ return artifacts.filter(
2518
+ (artifact) =>
2519
+ stringField(artifact, "type") === "task_suggestion" &&
2520
+ stringField(artifact, "status") === "draft",
2521
+ );
2522
+ }
2523
+
2524
+ async function runFsCommand() {
2525
+ const subArgs = args.slice(args.indexOf("fs") + 1);
2526
+ const configuredBinary = process.env.DENCH_FS_DAEMON_BIN?.trim();
2527
+ const binary = configuredBinary || "dench-fs-daemon";
2528
+ let exitCode: number;
2529
+ try {
2530
+ exitCode = await spawnInherited(binary, subArgs);
2531
+ } catch (error) {
2532
+ if (configuredBinary || !isCommandNotFound(error)) {
2533
+ throw error;
2534
+ }
2535
+ const localDaemon = fileURLToPath(
2536
+ new URL("./fs-daemon.ts", import.meta.url),
2537
+ );
2538
+ exitCode = await spawnInherited("bun", [localDaemon, ...subArgs]);
2539
+ }
2540
+ if (exitCode !== 0) {
2541
+ process.exitCode = exitCode;
2542
+ }
2543
+ }
2544
+
2545
+ async function spawnInherited(binary: string, commandArgs: string[]) {
2546
+ return await new Promise<number>((resolve, reject) => {
2547
+ const child = spawn(binary, commandArgs, {
2548
+ stdio: "inherit",
2549
+ env: process.env,
2550
+ });
2551
+ child.on("error", reject);
2552
+ child.on("close", (code) => resolve(code ?? 1));
2553
+ });
2554
+ }
2555
+
2556
+ function isCommandNotFound(error: unknown) {
2557
+ return (
2558
+ error instanceof Error &&
2559
+ "code" in error &&
2560
+ (error as NodeJS.ErrnoException).code === "ENOENT"
2561
+ );
2562
+ }
2563
+
2564
+ async function main() {
2565
+ const command = positional(0);
2566
+ const subcommand = positional(1);
2567
+
2568
+ if (command === "version" || hasFlag("--version")) {
2569
+ console.log(await cliVersion());
2570
+ return;
2571
+ }
2572
+
2573
+ if (!command || command === "help") {
2574
+ help();
2575
+ return;
2576
+ }
2577
+
2578
+ if (
2579
+ command === "tool" &&
2580
+ (!subcommand || subcommand === "help" || hasFlag("--help"))
2581
+ ) {
2582
+ toolHelp();
2583
+ return;
2584
+ }
2585
+
2586
+ if (command === "apps" && (subcommand === "help" || hasFlag("--help"))) {
2587
+ appsHelp();
2588
+ return;
2589
+ }
2590
+
2591
+ if (
2592
+ command === "billing" &&
2593
+ (!subcommand || subcommand === "help" || hasFlag("--help"))
2594
+ ) {
2595
+ billingHelp();
2596
+ return;
2597
+ }
2598
+
2599
+ if (command === "login" && hasFlag("--help")) {
2600
+ loginHelp();
2601
+ return;
2602
+ }
2603
+
2604
+ if (
2605
+ (command === "onboard" ||
2606
+ command === "setup" ||
2607
+ command === "what-can-i-do") &&
2608
+ (subcommand === "help" || hasFlag("--help"))
2609
+ ) {
2610
+ onboardHelp();
2611
+ return;
2612
+ }
2613
+
2614
+ if (command === "context" && hasFlag("--help")) {
2615
+ contextHelp();
2616
+ return;
2617
+ }
2618
+
2619
+ if (
2620
+ command === "memory" &&
2621
+ (!subcommand || subcommand === "help" || hasFlag("--help"))
2622
+ ) {
2623
+ memoryHelp();
2624
+ return;
2625
+ }
2626
+
2627
+ if (
2628
+ (command === "artifacts" || command === "suggested-work") &&
2629
+ (subcommand === "help" || hasFlag("--help"))
2630
+ ) {
2631
+ artifactsHelp();
2632
+ return;
2633
+ }
2634
+
2635
+ if (command === "status" && hasFlag("--help")) {
2636
+ statusHelp();
2637
+ return;
2638
+ }
2639
+
2640
+ if (command === "tasks" && hasFlag("--help")) {
2641
+ tasksHelp();
2642
+ return;
2643
+ }
2644
+
2645
+ if (command === "logout" && hasFlag("--help")) {
2646
+ logoutHelp();
2647
+ return;
2648
+ }
2649
+
2650
+ if (command === "autonomous") {
2651
+ if (!subcommand || subcommand === "help" || hasFlag("--help")) {
2652
+ autonomousHelp();
2653
+ return;
2654
+ }
2655
+ if (subcommand === "run") {
2656
+ await startAutonomousRun();
2657
+ return;
2658
+ }
2659
+ throw new Error(`Unknown autonomous command: ${filteredArgs.join(" ")}`);
2660
+ }
2661
+
2662
+ if (command === "crm") {
2663
+ const subArgs = args.slice(args.indexOf("crm") + 1);
2664
+ const sub = subArgs[0];
2665
+ // Short-circuit help so users don't need a session to read the
2666
+ // surface. The crm dispatcher's own help resolves --json + the
2667
+ // subcommand list inline.
2668
+ if (!sub || sub === "help" || sub === "--help") {
2669
+ const { runCrmCommand } = await import("./crm");
2670
+ // runCrmCommand only needs `convex` for actual queries; help
2671
+ // doesn't touch it. Pass a minimal stub.
2672
+ // biome-ignore lint/suspicious/noExplicitAny: harmless help-only stub
2673
+ await runCrmCommand({ convex: {} as any, args: subArgs });
2674
+ return;
2675
+ }
2676
+ const { runCrmCommand } = await import("./crm");
2677
+ const runtime = await getRuntime();
2678
+ const sessionRuntime = requireSessionRuntime(runtime, "dench crm");
2679
+ await runCrmCommand({
2680
+ convex: sessionRuntime.client,
2681
+ args: subArgs,
2682
+ // Server-side `requireCrmAccess` (convex/lib/crm/access.ts) accepts
2683
+ // this token and resolves the agent's organization without a Convex
2684
+ // Auth cookie. Mirrors the pattern used by every agentWorkspace fn.
2685
+ sessionToken: sessionRuntime.sessionToken,
2686
+ });
2687
+ return;
2688
+ }
2689
+
2690
+ if (command === "fs") {
2691
+ await runFsCommand();
2692
+ return;
2693
+ }
2694
+
2695
+ if (command === "agent") {
2696
+ const subArgs = args.slice(args.indexOf("agent") + 1);
2697
+ const sub = subArgs[0];
2698
+ if (!sub || sub === "help" || sub === "--help") {
2699
+ const { runAgentCommand } = await import("./agent");
2700
+ // biome-ignore lint/suspicious/noExplicitAny: harmless help-only stub
2701
+ await runAgentCommand({ convex: {} as any, args: subArgs });
2702
+ return;
2703
+ }
2704
+ const { runAgentCommand } = await import("./agent");
2705
+ const runtime = await getRuntime();
2706
+ await runAgentCommand({
2707
+ convex: runtime.client,
2708
+ args: subArgs,
2709
+ });
2710
+ return;
2711
+ }
2712
+
2713
+ if (command === "login") {
2714
+ await login();
2715
+ return;
2716
+ }
2717
+
2718
+ if (
2719
+ command === "onboard" ||
2720
+ command === "setup" ||
2721
+ command === "what-can-i-do"
2722
+ ) {
2723
+ await runOnboarding();
2724
+ return;
2725
+ }
2726
+
2727
+ if (command === "sessions") {
2728
+ await listSessions();
2729
+ return;
2730
+ }
2731
+
2732
+ if (command === "use") {
2733
+ await selectSession();
2734
+ return;
2735
+ }
2736
+
2737
+ if (command === "logout") {
2738
+ await logout();
2739
+ return;
2740
+ }
2741
+
2742
+ const runtime = await getRuntime();
2743
+
2744
+ if (command === "billing") {
2745
+ if (subcommand === "status") {
2746
+ await billingStatus(runtime);
2747
+ return;
2748
+ }
2749
+ if (subcommand === "topup") {
2750
+ await billingTopup(runtime);
2751
+ return;
2752
+ }
2753
+ billingHelp();
2754
+ return;
2755
+ }
2756
+
2757
+ if (command === "apps") {
2758
+ await printToolStatus(runtime);
2759
+ return;
2760
+ }
2761
+
2762
+ if (command === "context") {
2763
+ const context = await buildContext(runtime);
2764
+ print(json ? context : formatContext(context));
2765
+ return;
2766
+ }
2767
+
2768
+ if (command === "memory") {
2769
+ const sessionRuntime = requireSessionRuntime(runtime, "dench memory");
2770
+ if (subcommand === "search") {
2771
+ const query = positionalsFrom(2).join(" ").trim();
2772
+ if (!query) throw new Error('Usage: dench memory search "query"');
2773
+ print(
2774
+ await sessionRuntime.client.query(
2775
+ api.functions.agentWorkspace.agentSearchMemory,
2776
+ {
2777
+ sessionToken: sessionRuntime.sessionToken,
2778
+ query,
2779
+ limit: parseNumberOption("--limit"),
2780
+ },
2781
+ ),
2782
+ );
2783
+ return;
2784
+ }
2785
+ if (subcommand === "save") {
2786
+ const key = positional(2);
2787
+ const text = positionalsFrom(3).join(" ").trim();
2788
+ if (!key || !text) {
2789
+ throw new Error('Usage: dench memory save <key> "text"');
2790
+ }
2791
+ print(
2792
+ await sessionRuntime.client.mutation(
2793
+ api.functions.agentWorkspace.agentSaveMemory,
2794
+ {
2795
+ sessionToken: sessionRuntime.sessionToken,
2796
+ key,
2797
+ kind: (option("--kind") ?? "fact") as never,
2798
+ text,
2799
+ tags: parseCommaOption("--tags"),
2800
+ sensitivity: (option("--sensitivity") ?? "normal") as never,
2801
+ },
2802
+ ),
2803
+ );
2804
+ return;
2805
+ }
2806
+ throw new Error(`Unknown memory command: ${filteredArgs.join(" ")}`);
2807
+ }
2808
+
2809
+ if (command === "artifacts") {
2810
+ print(await listArtifacts(runtime, parseNumberOption("--limit")));
2811
+ return;
2812
+ }
2813
+
2814
+ if (command === "suggested-work") {
2815
+ if (subcommand === "task") {
2816
+ const sessionRuntime = requireSessionRuntime(
2817
+ runtime,
2818
+ "dench suggested-work task",
2819
+ );
2820
+ const artifactId = positional(2);
2821
+ if (!artifactId) {
2822
+ throw new Error("Usage: dench suggested-work task <artifactId>");
2823
+ }
2824
+ print(
2825
+ await sessionRuntime.client
2826
+ .mutation(api.functions.agentWorkspace.agentConvertSuggestionToTask, {
2827
+ sessionToken: sessionRuntime.sessionToken,
2828
+ artifactId: artifactId as never,
2829
+ })
2830
+ .catch(async (error) => {
2831
+ if (!isMissingConvexFunction(error)) throw error;
2832
+ const suggestion = (await listSuggestedWork(runtime)).find(
2833
+ (item) => stringField(item, "id") === artifactId,
2834
+ );
2835
+ if (!suggestion) throw error;
2836
+ const title = stringField(suggestion, "title") ?? "Suggested work";
2837
+ const created = (await sessionRuntime.client.mutation(
2838
+ api.functions.agentWorkspace.agentCreateTask,
2839
+ {
2840
+ sessionToken: sessionRuntime.sessionToken,
2841
+ title,
2842
+ description: stringField(suggestion, "content"),
2843
+ priority: "medium",
2844
+ risk: "medium",
2845
+ },
2846
+ )) as JsonRecord;
2847
+ return {
2848
+ ok: true,
2849
+ fallback: true,
2850
+ artifactId,
2851
+ title,
2852
+ taskId: created.taskId,
2853
+ note: "Created a task from suggested work. Artifact status will update after the backend conversion function is deployed.",
2854
+ };
2855
+ }),
2856
+ );
2857
+ return;
2858
+ }
2859
+ print(await listSuggestedWork(runtime));
2860
+ return;
2861
+ }
2862
+
2863
+ if (command === "status") {
2864
+ if (runtime.mode === "session") {
2865
+ const status = (await runtime.client.query(
2866
+ api.functions.agentWorkspace.agentStatus,
2867
+ {
2868
+ sessionToken: runtime.sessionToken,
2869
+ },
2870
+ )) as AgentStatus;
2871
+ if (scopedStatus) {
2872
+ const tasks = (await runtime.client.query(
2873
+ api.functions.agentWorkspace.agentListTasks,
2874
+ {
2875
+ sessionToken: runtime.sessionToken,
2876
+ },
2877
+ )) as JsonRecord[];
2878
+ print(summarizeAgentStatus(status, tasks, runtime.host));
2879
+ return;
2880
+ }
2881
+ print(status);
2882
+ return;
2883
+ }
2884
+ const data = await devOverview(runtime);
2885
+ const output = scopedStatus
2886
+ ? summarizeDevMine(runtime.workspaceArgs.workspaceSlug, data)
2887
+ : json
2888
+ ? data
2889
+ : summarizeDevOverview(runtime.workspaceArgs.workspaceSlug, data);
2890
+ print(output);
2891
+ return;
2892
+ }
2893
+
2894
+ if (command === "tasks") {
2895
+ if (runtime.mode === "session") {
2896
+ print(
2897
+ await runtime.client.query(
2898
+ api.functions.agentWorkspace.agentListTasks,
2899
+ {
2900
+ sessionToken: runtime.sessionToken,
2901
+ },
2902
+ ),
2903
+ );
2904
+ return;
2905
+ }
2906
+ const data = await devOverview(runtime);
2907
+ print(data.tasks);
2908
+ return;
2909
+ }
2910
+
2911
+ if (command === "agents") {
2912
+ if (runtime.mode === "session") {
2913
+ const status = await runtime.client.query(
2914
+ api.functions.agentWorkspace.agentStatus,
2915
+ {
2916
+ sessionToken: runtime.sessionToken,
2917
+ },
2918
+ );
2919
+ print(status.agents);
2920
+ return;
2921
+ }
2922
+ const data = await devOverview(runtime);
2923
+ print(data.agents);
2924
+ return;
2925
+ }
2926
+
2927
+ if (command === "approvals") {
2928
+ if (runtime.mode === "session") {
2929
+ const status = await runtime.client.query(
2930
+ api.functions.agentWorkspace.agentStatus,
2931
+ {
2932
+ sessionToken: runtime.sessionToken,
2933
+ },
2934
+ );
2935
+ print(status.approvals);
2936
+ return;
2937
+ }
2938
+ const data = await devOverview(runtime);
2939
+ print(data.approvals);
2940
+ return;
2941
+ }
2942
+
2943
+ if (command === "register") {
2944
+ if (runtime.mode !== "dev") {
2945
+ throw new Error(
2946
+ "register is only needed in dev mode. Real sessions use dench login.",
2947
+ );
2948
+ }
2949
+ const agentId = await runtime.client.mutation(
2950
+ api.functions.agentWorkspace.devRegisterAgent,
2951
+ {
2952
+ ...runtime.workspaceArgs,
2953
+ name: option("--name") ?? "AI Agent",
2954
+ kind: normalizeAgentKind(option("--kind")),
2955
+ },
2956
+ );
2957
+ print({ ok: true, agentId });
2958
+ return;
2959
+ }
2960
+
2961
+ if (command === "project" && subcommand === "create") {
2962
+ if (runtime.mode !== "dev") {
2963
+ throw new Error(
2964
+ "project create is not available for agent sessions yet.",
2965
+ );
2966
+ }
2967
+ const name = positional(2);
2968
+ if (!name) throw new Error("Missing project name");
2969
+ const projectId = await runtime.client.mutation(
2970
+ api.functions.agentWorkspace.devCreateProject,
2971
+ {
2972
+ ...runtime.workspaceArgs,
2973
+ name,
2974
+ repositoryUrl: option("--repo"),
2975
+ rootPath: option("--root"),
2976
+ },
2977
+ );
2978
+ print({ ok: true, projectId });
2979
+ return;
2980
+ }
2981
+
2982
+ if (command === "task" && subcommand === "create") {
2983
+ const title = positional(2);
2984
+ if (!title) throw new Error("Missing task title");
2985
+ const taskArgs = {
2986
+ title,
2987
+ description: option("--description"),
2988
+ priority: (option("--priority") ?? "medium") as "low" | "medium" | "high",
2989
+ risk: (option("--risk") ?? "medium") as "low" | "medium" | "high",
2990
+ };
2991
+ if (runtime.mode === "session") {
2992
+ print(
2993
+ await runtime.client.mutation(
2994
+ api.functions.agentWorkspace.agentCreateTask,
2995
+ {
2996
+ sessionToken: runtime.sessionToken,
2997
+ ...taskArgs,
2998
+ },
2999
+ ),
3000
+ );
3001
+ return;
3002
+ }
3003
+ const taskId = await runtime.client.mutation(
3004
+ api.functions.agentWorkspace.devCreateTask,
3005
+ {
3006
+ ...runtime.workspaceArgs,
3007
+ ...taskArgs,
3008
+ },
3009
+ );
3010
+ print({ ok: true, taskId });
3011
+ return;
3012
+ }
3013
+
3014
+ if (command === "task" && subcommand === "status") {
3015
+ const sessionRuntime = requireSessionRuntime(runtime, "dench task status");
3016
+ const taskId = positional(2);
3017
+ const status = positional(3);
3018
+ if (!taskId || !status) {
3019
+ throw new Error("Usage: dench task status <taskId> <status>");
3020
+ }
3021
+ print(
3022
+ await sessionRuntime.client.mutation(
3023
+ api.functions.agentWorkspace.agentUpdateTaskStatus,
3024
+ {
3025
+ sessionToken: sessionRuntime.sessionToken,
3026
+ taskId: taskId as never,
3027
+ status: status as never,
3028
+ note: option("--note"),
3029
+ },
3030
+ ),
3031
+ );
3032
+ return;
3033
+ }
3034
+
3035
+ if (
3036
+ command === "task" &&
3037
+ (subcommand === "comment" ||
3038
+ subcommand === "handoff" ||
3039
+ subcommand === "block")
3040
+ ) {
3041
+ const sessionRuntime = requireSessionRuntime(
3042
+ runtime,
3043
+ `dench task ${subcommand}`,
3044
+ );
3045
+ const taskId = positional(2);
3046
+ const body = positionalsFrom(3).join(" ").trim();
3047
+ if (!taskId || !body) {
3048
+ throw new Error(`Usage: dench task ${subcommand} <taskId> "message"`);
3049
+ }
3050
+ print(
3051
+ await sessionRuntime.client.mutation(
3052
+ api.functions.agentWorkspace.agentAddTaskComment,
3053
+ {
3054
+ sessionToken: sessionRuntime.sessionToken,
3055
+ taskId: taskId as never,
3056
+ kind:
3057
+ subcommand === "handoff"
3058
+ ? "handoff"
3059
+ : subcommand === "block"
3060
+ ? "blocker"
3061
+ : "comment",
3062
+ body,
3063
+ targetAgentId: option("--agent") as never,
3064
+ },
3065
+ ),
3066
+ );
3067
+ return;
3068
+ }
3069
+
3070
+ if (command === "task" && subcommand === "depends-on") {
3071
+ const sessionRuntime = requireSessionRuntime(
3072
+ runtime,
3073
+ "dench task depends-on",
3074
+ );
3075
+ const taskId = positional(2);
3076
+ const dependsOnTaskId = positional(3);
3077
+ if (!taskId || !dependsOnTaskId) {
3078
+ throw new Error(
3079
+ "Usage: dench task depends-on <taskId> <dependsOnTaskId>",
3080
+ );
3081
+ }
3082
+ print(
3083
+ await sessionRuntime.client.mutation(
3084
+ api.functions.agentWorkspace.agentAddTaskDependency,
3085
+ {
3086
+ sessionToken: sessionRuntime.sessionToken,
3087
+ taskId: taskId as never,
3088
+ dependsOnTaskId: dependsOnTaskId as never,
3089
+ note: option("--note"),
3090
+ },
3091
+ ),
3092
+ );
3093
+ return;
3094
+ }
3095
+
3096
+ if (command === "claim") {
3097
+ const taskId = positional(1);
3098
+ if (!taskId) throw new Error("Missing task id");
3099
+ if (runtime.mode === "session") {
3100
+ print(
3101
+ await runtime.client.mutation(
3102
+ api.functions.agentWorkspace.agentClaimTask,
3103
+ {
3104
+ sessionToken: runtime.sessionToken,
3105
+ taskId: taskId as never,
3106
+ },
3107
+ ),
3108
+ );
3109
+ return;
3110
+ }
3111
+ const agentId = option("--agent") ?? (await defaultDevAgentId(runtime));
3112
+ print(
3113
+ await runtime.client.mutation(api.functions.agentWorkspace.devClaimTask, {
3114
+ ...runtime.workspaceArgs,
3115
+ taskId: taskId as never,
3116
+ agentId: agentId as never,
3117
+ }),
3118
+ );
3119
+ return;
3120
+ }
3121
+
3122
+ if (command === "log") {
3123
+ const message = positional(1);
3124
+ if (!message) throw new Error("Missing log message");
3125
+ if (runtime.mode === "session") {
3126
+ print(
3127
+ await runtime.client.mutation(
3128
+ api.functions.agentWorkspace.agentAppendLog,
3129
+ {
3130
+ sessionToken: runtime.sessionToken,
3131
+ message,
3132
+ taskId: option("--task") as never,
3133
+ },
3134
+ ),
3135
+ );
3136
+ return;
3137
+ }
3138
+ print(
3139
+ await runtime.client.mutation(api.functions.agentWorkspace.devAppendLog, {
3140
+ ...runtime.workspaceArgs,
3141
+ message,
3142
+ agentId: option("--agent") as never,
3143
+ taskId: option("--task") as never,
3144
+ }),
3145
+ );
3146
+ return;
3147
+ }
3148
+
3149
+ if (command === "approval" && subcommand === "request") {
3150
+ const title = positional(2);
3151
+ if (!title) throw new Error("Missing approval title");
3152
+ if (runtime.mode === "session") {
3153
+ print(
3154
+ await runtime.client.mutation(
3155
+ api.functions.agentWorkspace.agentRequestApproval,
3156
+ {
3157
+ sessionToken: runtime.sessionToken,
3158
+ title,
3159
+ reason: option("--reason"),
3160
+ risk: (option("--risk") ?? "high") as "low" | "medium" | "high",
3161
+ taskId: option("--task") as never,
3162
+ },
3163
+ ),
3164
+ );
3165
+ return;
3166
+ }
3167
+ const approvalId = await runtime.client.mutation(
3168
+ api.functions.agentWorkspace.devRequestApproval,
3169
+ {
3170
+ ...runtime.workspaceArgs,
3171
+ title,
3172
+ reason:
3173
+ option("--reason") ??
3174
+ "Agent requests human approval before a risky action.",
3175
+ risk: (option("--risk") ?? "high") as "low" | "medium" | "high",
3176
+ agentId: option("--agent") as never,
3177
+ taskId: option("--task") as never,
3178
+ },
3179
+ );
3180
+ print({ ok: true, approvalId });
3181
+ return;
3182
+ }
3183
+
3184
+ if (command === "tool") {
3185
+ const sessionRuntime = requireSessionRuntime(runtime, "dench tool");
3186
+
3187
+ if (subcommand === "status") {
3188
+ await printToolStatus(sessionRuntime, positional(2));
3189
+ return;
3190
+ }
3191
+
3192
+ if (subcommand === "connect") {
3193
+ const toolkit = positional(2);
3194
+ if (!toolkit) throw new Error("Missing toolkit");
3195
+ const result = await sessionRuntime.client.action(
3196
+ api.functions.agentToolsNode.agentToolConnect,
3197
+ {
3198
+ sessionToken: sessionRuntime.sessionToken,
3199
+ toolkit,
3200
+ callbackUrl: option("--callback-url"),
3201
+ },
3202
+ );
3203
+ print(
3204
+ json ? withToolConnectSummary(result) : formatToolConnectResult(result),
3205
+ );
3206
+ return;
3207
+ }
3208
+
3209
+ if (subcommand === "search") {
3210
+ const query = positional(2);
3211
+ if (!query) throw new Error("Missing search query");
3212
+ try {
3213
+ printToolSearchResult(
3214
+ await sessionRuntime.client.action(
3215
+ api.functions.agentToolsNode.agentToolSearch,
3216
+ {
3217
+ sessionToken: sessionRuntime.sessionToken,
3218
+ query,
3219
+ toolkit: option("--toolkit"),
3220
+ limit: parseNumberOption("--limit"),
3221
+ },
3222
+ ),
3223
+ );
3224
+ } catch (error) {
3225
+ const unavailable = toolGatewayUnavailable(error, {
3226
+ query,
3227
+ toolkit: option("--toolkit"),
3228
+ });
3229
+ if (!unavailable) throw error;
3230
+ print(json ? unavailable : formatToolUnavailable(unavailable));
3231
+ }
3232
+ return;
3233
+ }
3234
+
3235
+ if (subcommand === "run") {
3236
+ const toolSlug = positional(2);
3237
+ if (!toolSlug) throw new Error("Missing Composio tool slug");
3238
+ printToolRunResult(
3239
+ sessionRuntime,
3240
+ await sessionRuntime.client.action(
3241
+ api.functions.agentToolsNode.agentToolRun,
3242
+ {
3243
+ sessionToken: sessionRuntime.sessionToken,
3244
+ toolSlug,
3245
+ arguments: parseJsonObjectOption("--args"),
3246
+ connectedAccountId: option("--account"),
3247
+ approvalId: option("--approval") as never,
3248
+ },
3249
+ ),
3250
+ );
3251
+ return;
3252
+ }
3253
+
3254
+ throw new Error(`Unknown tool command: ${filteredArgs.join(" ")}`);
3255
+ }
3256
+
3257
+ if (
3258
+ command === "approval" &&
3259
+ (subcommand === "approve" || subcommand === "reject")
3260
+ ) {
3261
+ const approvalId = positional(2);
3262
+ if (!approvalId) throw new Error("Missing approval id");
3263
+ const sessionRuntime = requireSessionRuntime(
3264
+ runtime,
3265
+ "dench approval approve/reject",
3266
+ );
3267
+ const decision = subcommand === "approve" ? "approved" : "rejected";
3268
+ const result = await sessionRuntime.client.mutation(
3269
+ api.functions.agentWorkspace.agentDecideApproval,
3270
+ {
3271
+ sessionToken: sessionRuntime.sessionToken,
3272
+ approvalId: approvalId as never,
3273
+ decision,
3274
+ evidence: option("--evidence"),
3275
+ },
3276
+ );
3277
+ throwIfCliError(result);
3278
+ print(result);
3279
+ return;
3280
+ }
3281
+
3282
+ throw new Error(`Unknown command: ${filteredArgs.join(" ")}`);
3283
+ }
3284
+
3285
+ main().catch((error) => {
3286
+ const extra = error instanceof CliError ? error.payload : {};
3287
+ const payload: JsonRecord = {
3288
+ ok: false,
3289
+ ...extra,
3290
+ error: error instanceof Error ? error.message : String(error),
3291
+ };
3292
+ if (json) {
3293
+ console.error(JSON.stringify(payload, null, 2));
3294
+ } else {
3295
+ console.error(`Error: ${payload.error}`);
3296
+ const nextActions = stringArrayField(payload, "nextActions");
3297
+ if (nextActions.length > 0) {
3298
+ console.error("Next actions:");
3299
+ for (const action of nextActions) {
3300
+ console.error(` - ${action}`);
3301
+ }
3302
+ }
3303
+ }
3304
+ process.exit(1);
3305
+ });