@dench.com/cli 0.4.4 → 0.4.6

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/agent.ts CHANGED
@@ -1,473 +1,37 @@
1
- /**
2
- * `dench agent <subcommand>` — CLI surface for the subagent + peer
3
- * messaging system.
4
- *
5
- * Used inside sandboxes (where DENCH_API_KEY + DENCH_INTERNAL_BASE +
6
- * DENCH_INTERNAL_TOKEN are already present in env) to:
7
- * - spawn parallel subagents
8
- * - await their completion
9
- * - send / wait on peer messages
10
- * - pause / resume / continue runs
11
- * - list run trees
12
- *
13
- * Routes hit:
14
- * POST /api/runs/spawn-child (internal token)
15
- * POST /api/runs/notify-parent (internal token)
16
- * POST /api/runs/send-message (internal token)
17
- * POST /api/runs/continue (Bearer Dench API key)
18
- * POST /api/runs (existing legacy route — kept for
19
- * compatibility; new runs prefer /start)
20
- * POST /api/runs/start (Bearer Dench API key, the new flow)
21
- */
22
1
  import type { ConvexHttpClient } from "convex/browser";
23
- import { makeFunctionReference } from "convex/server";
24
- import {
25
- CliArgError,
26
- getFlag,
27
- hasFlag,
28
- shift as shiftRaw,
29
- } from "./lib/cli-args";
30
2
 
31
3
  class AgentCliError extends Error {}
32
4
 
33
- type CliCtx = {
34
- convex: ConvexHttpClient;
35
- args: string[];
36
- jsonOutput: boolean;
37
- /**
38
- * Bearer token forwarded by the dispatcher in `cli/dench.ts`. Either a
39
- * `dch_agent_*` agent session minted by `dench login` or a unified
40
- * Dench API key (`DENCH_API_KEY`) when the CLI is running inside a
41
- * sandbox. Used by `runAwait` to authenticate against
42
- * `runs:getChildRunStatusForCli`.
43
- */
44
- sessionToken?: string;
45
- /**
46
- * Convex `runs._id` of the caller's own run, when authenticated via
47
- * an API key (sandbox path). Required by `getChildRunStatusForCli`
48
- * to scope the API key's reach to a specific organization.
49
- */
50
- callerRunId?: string;
51
- };
52
-
53
- // Args helpers moved to cli/lib/cli-args.ts; we adapt shift to throw
54
- // AgentCliError so existing catch blocks don't need updating.
55
- function shift(args: string[], expected: string): string {
56
- try {
57
- return shiftRaw(args, expected);
58
- } catch (error) {
59
- if (error instanceof CliArgError) throw new AgentCliError(error.message);
60
- throw error;
61
- }
62
- }
63
-
64
- function out(ctx: CliCtx, value: unknown): void {
65
- if (ctx.jsonOutput) {
66
- console.log(JSON.stringify(value, null, 2));
67
- return;
68
- }
69
- if (value === null || value === undefined) {
70
- console.log("(empty)");
71
- return;
72
- }
73
- console.log(JSON.stringify(value, null, 2));
74
- }
75
-
76
- function getInternalBase(): string {
77
- const base = (
78
- process.env.DENCH_INTERNAL_BASE ??
79
- process.env.DENCH_API_URL ??
80
- ""
81
- ).replace(/\/+$/, "");
82
- if (!base) {
83
- throw new AgentCliError(
84
- "DENCH_INTERNAL_BASE (or DENCH_API_URL) is not configured in this sandbox",
85
- );
86
- }
87
- return base;
88
- }
89
-
90
- function getApiBase(): string {
91
- const base = (process.env.DENCH_API_URL ?? "").replace(/\/+$/, "");
92
- if (!base) {
93
- throw new AgentCliError("DENCH_API_URL is not configured in this sandbox");
94
- }
95
- return base;
96
- }
5
+ function agentHelp(): void {
6
+ console.log(`Usage: dench agent <subcommand>
97
7
 
98
- function internalHeaders(): Record<string, string> {
99
- const token = process.env.DENCH_INTERNAL_TOKEN?.trim();
100
- if (!token) {
101
- throw new AgentCliError(
102
- "DENCH_INTERNAL_TOKEN is not configured in this sandbox",
103
- );
104
- }
105
- return {
106
- "content-type": "application/json",
107
- "x-dench-internal-token": token,
108
- };
109
- }
8
+ Agent sessions are normal chat-turn workflows, the same path used by the web UI:
9
+ dench agent new "<prompt>" [--model <m>] [--file-context <path>] [--visibility private|shared] [--title T] [--yolo] [--follow] [--json]
10
+ dench agent send <thread-id> "<message>" [--model <m>] [--yolo] [--follow] [--json]
11
+ dench agent follow <thread-id> [--json]
110
12
 
111
- function apiKeyHeaders(): Record<string, string> {
112
- const key = process.env.DENCH_API_KEY?.trim();
113
- if (!key) {
114
- throw new AgentCliError(
115
- "DENCH_API_KEY is not configured in this sandbox",
116
- );
117
- }
118
- return {
119
- "content-type": "application/json",
120
- authorization: `Bearer ${key}`,
121
- };
122
- }
13
+ These are aliases for:
14
+ dench chat new
15
+ dench chat send
16
+ dench chat follow
123
17
 
124
- async function postJson(
125
- url: string,
126
- headers: Record<string, string>,
127
- body: unknown,
128
- ): Promise<unknown> {
129
- const response = await fetch(url, {
130
- method: "POST",
131
- headers,
132
- body: JSON.stringify(body ?? {}),
133
- });
134
- const text = await response.text();
135
- let json: unknown;
136
- try {
137
- json = text ? JSON.parse(text) : null;
138
- } catch {
139
- json = { raw: text };
140
- }
141
- if (!response.ok) {
142
- throw new AgentCliError(
143
- `${url} failed with ${response.status}: ${
144
- typeof json === "object" ? JSON.stringify(json) : String(json)
145
- }`,
146
- );
147
- }
148
- return json;
18
+ Use spawn_agent from inside a running chat to create child agents. Child agents
19
+ are also normal chat threads and can be followed with dench agent follow.
20
+ `);
149
21
  }
150
22
 
151
- export async function runAgentCommand(opts: {
23
+ export async function runAgentCommand(_opts: {
152
24
  convex: ConvexHttpClient;
153
25
  args: string[];
154
26
  sessionToken?: string;
155
27
  callerRunId?: string;
156
28
  }): Promise<void> {
157
- const args = [...opts.args];
158
- const jsonOutput = hasFlag(args, "--json");
159
- const ctx: CliCtx = {
160
- convex: opts.convex,
161
- args,
162
- jsonOutput,
163
- sessionToken: opts.sessionToken,
164
- callerRunId: opts.callerRunId,
165
- };
166
- const subcommand = args.shift();
29
+ const subcommand = _opts.args[0];
167
30
  if (!subcommand || subcommand === "help" || subcommand === "--help") {
168
31
  agentHelp();
169
32
  return;
170
33
  }
171
- switch (subcommand) {
172
- case "spawn":
173
- return await runSpawn(ctx);
174
- case "await":
175
- return await runAwait(ctx);
176
- case "message":
177
- return await runMessage(ctx);
178
- case "wait-message":
179
- return await runWaitMessage(ctx);
180
- case "pause":
181
- return await runPause(ctx);
182
- case "resume":
183
- return await runResume(ctx);
184
- case "continue":
185
- return await runContinue(ctx);
186
- case "tree":
187
- return await runTree(ctx);
188
- default:
189
- throw new AgentCliError(
190
- `Unknown agent subcommand: ${subcommand}. Run 'dench agent help'.`,
191
- );
192
- }
193
- }
194
-
195
- async function runPause(ctx: CliCtx): Promise<void> {
196
- const runId = shift(ctx.args, "run id");
197
- const result = await postJson(
198
- `${getApiBase()}/api/runs/pause`,
199
- apiKeyHeaders(),
200
- { runId },
201
- );
202
- out(ctx, result);
203
- }
204
-
205
- async function runResume(ctx: CliCtx): Promise<void> {
206
- const runId = shift(ctx.args, "run id");
207
- const prompt = ctx.args.length > 0 ? ctx.args.join(" ").trim() : undefined;
208
- const result = await postJson(
209
- `${getApiBase()}/api/runs/resume`,
210
- apiKeyHeaders(),
211
- { runId, prompt },
212
- );
213
- out(ctx, result);
214
- }
215
-
216
- async function runSpawn(ctx: CliCtx): Promise<void> {
217
- const goal = ctx.args.filter((arg) => !arg.startsWith("--")).join(" ").trim();
218
- if (!goal) throw new AgentCliError("Goal required: dench agent spawn '...'");
219
- const parentRunId =
220
- getFlag(ctx.args, "--parent-run-id") ?? process.env.DENCH_RUN_ID;
221
- if (!parentRunId) {
222
- throw new AgentCliError(
223
- "--parent-run-id required (or set DENCH_RUN_ID inside a sandbox)",
224
- );
225
- }
226
- const sandboxStrategy = getFlag(ctx.args, "--sandbox") as
227
- | "own"
228
- | "share_parent"
229
- | undefined;
230
- const timeBudgetMs = parseInt(
231
- getFlag(ctx.args, "--time-budget-ms") ?? "0",
232
- 10,
233
- );
234
-
235
- // Step 1: createChildRun via Convex (mutation requires admin auth, so we
236
- // hop through the spawn-child route which has CONVEX_DEPLOY_KEY).
237
- // For the v1 plan, we ship a single internal-route hop: the route owns
238
- // the createChildRun + setRunLinkAwaitToken + start() sequence.
239
- const result = await postJson(
240
- `${getInternalBase()}/api/runs/spawn-child`,
241
- internalHeaders(),
242
- {
243
- // The route currently expects a pre-created childRunId. To support
244
- // the CLI flow we'll pass the raw goal and let the route create
245
- // the child row + start the workflow. (Route extension lands in
246
- // the next commit.)
247
- parentRunId,
248
- goal,
249
- sandboxStrategy,
250
- timeBudgetMs: timeBudgetMs > 0 ? timeBudgetMs : undefined,
251
- },
34
+ throw new AgentCliError(
35
+ `Unknown agent subcommand: ${subcommand}. Use dench agent new, send, or follow.`,
252
36
  );
253
- out(ctx, result);
254
- }
255
-
256
- async function runAwait(ctx: CliCtx): Promise<void> {
257
- const hookToken = getFlag(ctx.args, "--hook");
258
- if (hookToken) {
259
- out(ctx, { hookToken, status: "registered" });
260
- return;
261
- }
262
- const childIdsCsv = getFlag(ctx.args, "--children");
263
- if (!childIdsCsv) {
264
- throw new AgentCliError(
265
- "dench agent await requires --hook <token> or --children <csv>",
266
- );
267
- }
268
- if (!ctx.sessionToken) {
269
- throw new AgentCliError(
270
- "dench agent await --children needs an authenticated session " +
271
- "(run `dench login`, or set DENCH_API_KEY in a sandbox)",
272
- );
273
- }
274
-
275
- const childIds = childIdsCsv
276
- .split(",")
277
- .map((id) => id.trim())
278
- .filter(Boolean);
279
- if (childIds.length === 0) {
280
- throw new AgentCliError("--children requires at least one run id");
281
- }
282
- const start = Date.now();
283
- const timeoutMs = parseInt(
284
- getFlag(ctx.args, "--timeout-ms") ?? "3600000",
285
- 10,
286
- );
287
- const intervalMs = parseInt(
288
- getFlag(ctx.args, "--interval-ms") ?? "2000",
289
- 10,
290
- );
291
-
292
- type ChildSnapshot = {
293
- runId: string;
294
- status: string;
295
- summary: string | null;
296
- threadId: string | null;
297
- terminal: boolean;
298
- };
299
- const getStatus = makeFunctionReference<"query">(
300
- "functions/runs:getChildRunStatusForCli",
301
- );
302
-
303
- // Poll each child's status until it reaches a terminal state. The
304
- // server uses sessionToken auth (mirrors `dench chat list`) so this
305
- // works both for `dch_agent_*` sessions (local dev) and for unified
306
- // `DENCH_API_KEY`s (sandbox, must include `--caller-run-id`).
307
- const completed: Record<string, ChildSnapshot> = {};
308
- while (Object.keys(completed).length < childIds.length) {
309
- if (Date.now() - start > timeoutMs) {
310
- throw new AgentCliError(
311
- `dench agent await timed out after ${timeoutMs}ms with ${
312
- Object.keys(completed).length
313
- }/${childIds.length} children completed`,
314
- );
315
- }
316
- for (const childId of childIds) {
317
- if (completed[childId]) continue;
318
- try {
319
- const snap = (await ctx.convex.query(getStatus, {
320
- sessionToken: ctx.sessionToken,
321
- runId: ctx.callerRunId ?? undefined,
322
- childRunId: childId as never,
323
- })) as ChildSnapshot | null;
324
- if (snap?.terminal) {
325
- completed[childId] = snap;
326
- }
327
- } catch (error) {
328
- const msg = error instanceof Error ? error.message : String(error);
329
- // Treat "not found" / access errors as terminal-with-error so
330
- // we don't poll forever on a typo'd id.
331
- completed[childId] = {
332
- runId: childId,
333
- status: "error",
334
- summary: msg,
335
- threadId: null,
336
- terminal: true,
337
- };
338
- }
339
- }
340
- if (Object.keys(completed).length < childIds.length) {
341
- await new Promise((resolve) => setTimeout(resolve, intervalMs));
342
- }
343
- }
344
- out(ctx, { children: completed });
345
- }
346
-
347
- async function runMessage(ctx: CliCtx): Promise<void> {
348
- const toRunId = shift(ctx.args, "target run id");
349
- const fromRunId =
350
- getFlag(ctx.args, "--from-run-id") ?? process.env.DENCH_RUN_ID;
351
- if (!fromRunId) {
352
- throw new AgentCliError(
353
- "--from-run-id required (or set DENCH_RUN_ID inside a sandbox)",
354
- );
355
- }
356
- const text = ctx.args.join(" ").trim();
357
- if (!text) {
358
- throw new AgentCliError(
359
- "Message body required: dench agent message <toRunId> 'text...'",
360
- );
361
- }
362
- const result = await postJson(
363
- `${getInternalBase()}/api/runs/send-message`,
364
- internalHeaders(),
365
- { fromRunId, toRunId, payload: { text } },
366
- );
367
- out(ctx, result);
368
- }
369
-
370
- async function runWaitMessage(ctx: CliCtx): Promise<void> {
371
- // v1 stub: print a message + exit. The real implementation lives in
372
- // the agent loop's `wait_for_message` tool, which calls the workflow
373
- // step that registers an await-message hook and durably sleeps.
374
- out(ctx, {
375
- note:
376
- "wait-message is intended to be called by the agent loop, not from a one-shot CLI. Use the agent's wait_for_message tool inside a Long Session.",
377
- });
378
- }
379
-
380
- async function runContinue(ctx: CliCtx): Promise<void> {
381
- const prevRunId = shift(ctx.args, "previous run id");
382
- const prompt = ctx.args.join(" ").trim();
383
- if (!prompt) {
384
- throw new AgentCliError(
385
- "Continuation prompt required: dench agent continue <prevRunId> '...'",
386
- );
387
- }
388
- const result = await postJson(
389
- `${getApiBase()}/api/runs/continue`,
390
- apiKeyHeaders(),
391
- { prevRunId, prompt },
392
- );
393
- out(ctx, result);
394
- }
395
-
396
- async function runTree(ctx: CliCtx): Promise<void> {
397
- const rootRunId =
398
- ctx.args[0] ??
399
- process.env.DENCH_ROOT_RUN_ID ??
400
- process.env.DENCH_RUN_ID;
401
- if (!rootRunId) {
402
- throw new AgentCliError(
403
- "dench agent tree <rootRunId> (or set DENCH_ROOT_RUN_ID inside a sandbox)",
404
- );
405
- }
406
- const getRunTree = makeFunctionReference<"query">(
407
- "functions/runs:getRunTree",
408
- );
409
- const flat = (await ctx.convex.query(getRunTree, {
410
- rootRunId: rootRunId as never,
411
- })) as Array<{
412
- _id: string;
413
- parentRunId?: string;
414
- goal: string;
415
- status: string;
416
- }>;
417
- if (ctx.jsonOutput) {
418
- out(ctx, flat);
419
- return;
420
- }
421
- // Tree-pretty-print, depth-first.
422
- const byParent = new Map<string | null, typeof flat>();
423
- for (const node of flat) {
424
- const key = node.parentRunId ?? null;
425
- const list = byParent.get(key) ?? [];
426
- list.push(node);
427
- byParent.set(key, list);
428
- }
429
- function print(node: (typeof flat)[number], depth: number): void {
430
- const indent = " ".repeat(depth);
431
- const goalLine = node.goal.split("\n")[0].slice(0, 60);
432
- process.stdout.write(
433
- `${indent}- [${node.status}] ${node._id} ${goalLine}\n`,
434
- );
435
- for (const child of byParent.get(node._id) ?? []) print(child, depth + 1);
436
- }
437
- for (const root of byParent.get(null) ?? []) print(root, 0);
438
- }
439
-
440
- function agentHelp(): void {
441
- console.log(`Usage: dench agent <subcommand>
442
-
443
- Spawn a chat-turn workflow (same as the web UI's chat panel):
444
- dench agent new "<prompt>" [--model <m>] [--file-context <path>] [--visibility private|shared] [--title T] [--yolo] [--follow] [--json]
445
- dench agent send <thread-id> "<message>" [--model <m>] [--yolo] [--follow] [--json]
446
- dench agent follow <thread-id> [--json]
447
- These are aliases for \`dench chat new\` / \`dench chat send\` / \`dench chat follow\`.
448
- Use \`follow\` to tail the live stream of an existing thread (e.g.
449
- a subagent spawned by \`spawn_agent\`) without sending a new message.
450
-
451
- Subagents (autonomous Long Sessions, NOT chat threads):
452
- dench agent spawn '<goal>' [--parent-run-id <id>] [--sandbox own|share_parent] [--time-budget-ms N]
453
- dench agent await --hook <token>
454
- dench agent await --children id1,id2,id3 [--timeout-ms N] [--interval-ms N]
455
- (polls runs:getChildRunStatusForCli
456
- with the active session token until
457
- every child is terminal)
458
-
459
- Peer messaging:
460
- dench agent message <toRunId> '<text...>' [--from-run-id <id>]
461
- dench agent wait-message (intended for use inside the agent loop)
462
-
463
- Lifecycle:
464
- dench agent pause <runId>
465
- dench agent resume <runId> ['optional prompt to inject on resume']
466
- dench agent continue <prevRunId> '<prompt>' (creates a new run from a completed one)
467
- dench agent tree (see <RunTreeView> in dench.com UI)
468
-
469
- Global flags:
470
- --json Emit raw JSON instead of pretty-printed text.
471
- --follow After spawning a chat (new/send), stream the response to stdout.
472
- `);
473
37
  }
package/crm.ts CHANGED
@@ -1082,56 +1082,9 @@ async function runActionsCommand(ctx: CrmCliContext): Promise<void> {
1082
1082
  return;
1083
1083
  }
1084
1084
  case "run": {
1085
- // Two-step happy path: insert crmActionRuns row → POST run-action
1086
- // route which creates the workflow run + starts agentRunWorkflow.
1087
- const actionId = shift(ctx.args, "action id");
1088
- const fieldId = getFlag(ctx.args, "--field-id");
1089
- const entryId = getFlag(ctx.args, "--entry-id");
1090
- const goal = getFlag(ctx.args, "--goal") ?? "";
1091
- const wait = hasFlag(ctx.args, "--wait");
1092
- assertNoUnknownFlags(ctx.args, "crm actions run");
1093
- if (!fieldId || !entryId) {
1094
- throw new CrmCliError(
1095
- "dench crm actions run requires --field-id and --entry-id",
1096
- );
1097
- }
1098
- const created = (await callMutation(ctx, api.actions.startActionRun, {
1099
- actionId,
1100
- fieldId: fieldId as any,
1101
- entryId: entryId as any,
1102
- })) as { id: string };
1103
- // Hop through /api/crm/run-action so the workflow actually starts.
1104
- const apiBase = (
1105
- process.env.DENCH_API_URL ?? "http://localhost:3000"
1106
- ).replace(/\/+$/, "");
1107
- const apiKey =
1108
- process.env.DENCH_API_KEY?.trim() ??
1109
- process.env.DENCH_FALLBACK_API_KEY?.trim() ??
1110
- "";
1111
- if (!apiKey) {
1112
- throw new CrmCliError(
1113
- "DENCH_API_KEY required to fire the action workflow",
1114
- );
1115
- }
1116
- const response = await fetch(`${apiBase}/api/crm/run-action`, {
1117
- method: "POST",
1118
- headers: {
1119
- "content-type": "application/json",
1120
- authorization: `Bearer ${apiKey}`,
1121
- },
1122
- body: JSON.stringify({ actionRunId: created.id, goal }),
1123
- });
1124
- const json = (await response.json().catch(() => ({}))) as {
1125
- runId?: string;
1126
- error?: string;
1127
- };
1128
- if (!response.ok) {
1129
- throw new CrmCliError(
1130
- `run-action failed (${response.status}): ${json.error ?? ""}`,
1131
- );
1132
- }
1133
- out(ctx, { actionRunId: created.id, runId: json.runId, wait });
1134
- return;
1085
+ throw new CrmCliError(
1086
+ "dench crm actions run has been removed. Start a normal agent with `dench agent new --follow` and ask it to perform the CRM action.",
1087
+ );
1135
1088
  }
1136
1089
  default:
1137
1090
  throw new CrmCliError(`Unknown crm actions verb: ${verb ?? "<none>"}`);
package/cron.ts CHANGED
@@ -356,14 +356,9 @@ async function runCreate(ctx: CronCliContext): Promise<void> {
356
356
  }
357
357
  const disabled = hasFlag(ctx.args, "--disabled");
358
358
  const deleteAfterRun = hasFlag(ctx.args, "--delete-after-run");
359
- const target = getFlag(ctx.args, "--target") as
360
- | "chat_turn"
361
- | "agent_run"
362
- | undefined;
363
- if (target && !["chat_turn", "agent_run"].includes(target)) {
364
- throw new CronCliError(
365
- `--target must be chat_turn|agent_run (got "${target}")`,
366
- );
359
+ const target = getFlag(ctx.args, "--target") as "chat_turn" | undefined;
360
+ if (target && target !== "chat_turn") {
361
+ throw new CronCliError(`--target must be chat_turn (got "${target}")`);
367
362
  }
368
363
  const schedule = consumeScheduleFlags(ctx.args);
369
364
  if (!schedule) {
@@ -523,7 +518,7 @@ Create / update (one of --every / --cron / --at is required for create):
523
518
  [--overlap skip|queue|kill_old] # default: skip
524
519
  [--disabled] # default: enabled
525
520
  [--delete-after-run] # one-shot at-schedule; default false
526
- [--target chat_turn|agent_run] # default: chat_turn (fires a fresh chat thread)
521
+ [--target chat_turn] # default: chat_turn (fires a fresh chat thread)
527
522
  [--json]
528
523
 
529
524
  dench cron update <jobId> [--name N] [--prompt P] [--description D] \\
package/dench.ts CHANGED
@@ -197,16 +197,6 @@ const api = {
197
197
  };
198
198
  const CONFIG_DIR = join(homedir(), ".dench");
199
199
  const CONFIG_PATH = join(CONFIG_DIR, "config.json");
200
- const AUTONOMOUS_DEFAULT_API_BASE = "http://localhost:3000";
201
- const SAFE_LONG_SESSION_CONSTRAINTS = [
202
- "do not edit files",
203
- "publish",
204
- "deploy",
205
- "spend money",
206
- "send external messages",
207
- "access secrets",
208
- "or change production data unless the user explicitly approves it",
209
- ];
210
200
  const args = process.argv.slice(2);
211
201
  const json = args.includes("--json");
212
202
  const filteredArgs = args.filter((arg) => arg !== "--json");
@@ -442,8 +432,6 @@ Usage:
442
432
  dench tool connect <toolkit> [--json]
443
433
  dench tool search "create github issue" [--toolkit github] [--limit 20] [--compact] [--json]
444
434
  dench tool run <composio_tool_slug> --args '{"key":"value"}' [--account <connected_account_id>] [--approval <approvalId>] [--json]
445
- 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] [--yolo] [--json]
446
-
447
435
  CRM (Daytona-native rewrite):
448
436
  dench crm objects <list|get|create|update|rename|delete> ...
449
437
  dench crm fields <list|create|update|delete|reorder> <object> ...
@@ -602,26 +590,6 @@ Notes:
602
590
  `);
603
591
  }
604
592
 
605
- function autonomousHelp() {
606
- console.log(`Dench Long Session commands
607
-
608
- Usage:
609
- 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] [--yolo] [--json]
610
-
611
- Notes:
612
- Starts a Long Session through /api/runs.
613
- Duration supports 30m, 1h, 3h, 5h, or until-done.
614
- API base defaults to ${AUTONOMOUS_DEFAULT_API_BASE}.
615
- Override with --api-base, --host, DENCH_AUTONOMOUS_API_BASE, or DENCH_HOST.
616
- If needed, run dench login --host <api-base> once so the CLI can authenticate.
617
- Omit --model to use the server default.
618
- Approval flags: --ask-before-publishing --ask-before-spending --ask-before-external-messages --ask-before-production-changes --ask-before-secrets.
619
- --yolo: skip every approval gate (overrides --ask-before-* / --approval-rules). Equivalent to the workspace's YOLO mode in /<slug>/settings -> Approvals.
620
- Without any approval flags, the run inherits the workspace's Approvals policy from /<slug>/settings.
621
- Safe goals should explicitly say: ${SAFE_LONG_SESSION_CONSTRAINTS.join(", ")}.
622
- `);
623
- }
624
-
625
593
  function contextHelp() {
626
594
  console.log(`Dench context
627
595
 
@@ -653,8 +621,8 @@ Usage:
653
621
  dench suggested-work [--json]
654
622
  dench suggested-work task <artifactId> [--json]
655
623
 
656
- Artifacts are durable Long Session outputs for human or agent review.
657
- Suggested work can be converted into a workspace task.
624
+ Artifacts are durable outputs for human or agent review. Suggested work can
625
+ be converted into a workspace task.
658
626
  `);
659
627
  }
660
628
 
@@ -667,8 +635,8 @@ Usage:
667
635
  dench what-can-i-do [--kind <kind>] [--name "AI Agent - Project"] [--no-open] [--json]
668
636
 
669
637
  Logs in if needed, then shows what this agent can do here: workspace rules,
670
- open work, suggested work, Long Session options, connected-tool commands,
671
- memory, approvals, and next actions.
638
+ open work, suggested work, connected-tool commands, memory, approvals, and
639
+ next actions.
672
640
  `);
673
641
  }
674
642
 
@@ -1395,176 +1363,6 @@ function normalizeApiBase(input: string) {
1395
1363
  return new URL(withProtocol).origin;
1396
1364
  }
1397
1365
 
1398
- function resolveAutonomousApiBase() {
1399
- const explicit =
1400
- option("--api-base") ??
1401
- option("--host") ??
1402
- process.env.DENCH_AUTONOMOUS_API_BASE ??
1403
- process.env.DENCH_HOST;
1404
- return explicit ? normalizeApiBase(explicit) : AUTONOMOUS_DEFAULT_API_BASE;
1405
- }
1406
-
1407
- async function resolveAutonomousSessionToken(apiBase: string) {
1408
- const config = await loadConfig();
1409
- const scope = resolveSessionScope({ args: filteredArgs });
1410
- const stored = await getStoredSession(apiBase, scope);
1411
- if (stored.status === "found") {
1412
- return stored.session.sessionToken;
1413
- }
1414
- if (stored.status === "ambiguous") {
1415
- throw new Error(ambiguousSessionMessage(config, scope, apiBase));
1416
- }
1417
- return undefined;
1418
- }
1419
-
1420
- function formatAutonomousRunResult(value: unknown) {
1421
- const record = asRecord(value);
1422
- if (!record) {
1423
- return typeof value === "string" ? value : JSON.stringify(value, null, 2);
1424
- }
1425
-
1426
- const runId = stringField(record, "runId", "id");
1427
- const workflowRunId = stringField(record, "workflowRunId");
1428
- const status = stringField(record, "status");
1429
- const url = stringField(record, "url", "runUrl");
1430
-
1431
- if (!runId && !workflowRunId && !status && !url) {
1432
- return JSON.stringify(value, null, 2);
1433
- }
1434
-
1435
- const lines = ["Started Long Session"];
1436
- if (runId) lines.push(`Run ID: ${runId}`);
1437
- if (workflowRunId) lines.push(`Workflow Run ID: ${workflowRunId}`);
1438
- if (status) lines.push(`Status: ${status}`);
1439
- if (url) lines.push(`URL: ${url}`);
1440
- return lines.join("\n");
1441
- }
1442
-
1443
- function autonomousRunUrl(apiBase: string, runId: string) {
1444
- return `${apiBase.replace(/\/+$/, "")}/labs/autonomous/${encodeURIComponent(runId)}`;
1445
- }
1446
-
1447
- function withAutonomousRunUrl(payload: unknown, apiBase: string) {
1448
- const record = asRecord(payload);
1449
- const runId = stringField(record, "runId", "id");
1450
- if (!record || !runId || stringField(record, "runUrl", "url")) {
1451
- return payload;
1452
- }
1453
- return { ...record, runUrl: autonomousRunUrl(apiBase, runId) };
1454
- }
1455
-
1456
- function formatAutonomousRunError(args: {
1457
- status: number;
1458
- payload: unknown;
1459
- apiBase: string;
1460
- hasSession: boolean;
1461
- statusText: string;
1462
- }) {
1463
- const record = asRecord(args.payload);
1464
- const error =
1465
- stringField(record, "error", "message") ??
1466
- (typeof args.payload === "string" && args.payload.trim()
1467
- ? args.payload.trim()
1468
- : args.statusText);
1469
- const nextActions = stringArrayField(record, "nextActions");
1470
- if (args.status === 401 && !args.hasSession) {
1471
- nextActions.unshift(`Run: dench login --host ${args.apiBase}`);
1472
- } else if (args.status === 401) {
1473
- nextActions.push(
1474
- "Run dench sessions --json to inspect saved sessions.",
1475
- `Run dench login --host ${args.apiBase} if this session is expired.`,
1476
- );
1477
- } else if (args.status === 402 && nextActions.length === 0) {
1478
- nextActions.push(
1479
- "Top up AI credits in Dench usage/billing.",
1480
- "Retry the same Long Session command after credits update.",
1481
- );
1482
- }
1483
- if (stringField(record, "topUpUrl")) {
1484
- nextActions.unshift(`Top up: ${stringField(record, "topUpUrl")}`);
1485
- }
1486
- return new CliError(`Long Session failed (${args.status}): ${error}`, {
1487
- status: args.status,
1488
- code: stringField(record, "code") ?? `http_${args.status}`,
1489
- apiBase: args.apiBase,
1490
- topUpUrl: stringField(record, "topUpUrl"),
1491
- nextActions: [...new Set(nextActions)],
1492
- });
1493
- }
1494
-
1495
- function safeLongSessionExample() {
1496
- 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';
1497
- }
1498
-
1499
- function parseDurationMs(rawDuration?: string) {
1500
- const raw = rawDuration?.trim().toLowerCase();
1501
- if (!raw) return undefined;
1502
- if (["until-done", "until_done", "done", "max"].includes(raw)) {
1503
- return 24 * 60 * 60 * 1000;
1504
- }
1505
- const match = raw.match(
1506
- /^(\d+(?:\.\d+)?)(m|min|mins|minute|minutes|h|hr|hrs|hour|hours)$/,
1507
- );
1508
- if (!match) {
1509
- throw new CliError(
1510
- "--duration must look like 30m, 1h, 3h, 5h, or until-done",
1511
- {
1512
- code: "invalid_duration",
1513
- nextActions: [
1514
- "Use one of: 30m, 1h, 3h, 5h, until-done.",
1515
- safeLongSessionExample(),
1516
- ],
1517
- },
1518
- );
1519
- }
1520
- const value = Number(match[1]);
1521
- const unit = match[2];
1522
- if (!Number.isFinite(value) || value <= 0) {
1523
- throw new CliError("--duration must be positive", {
1524
- code: "invalid_duration",
1525
- nextActions: ["Use a positive duration like 30m or 1h."],
1526
- });
1527
- }
1528
- const minutes = unit.startsWith("h") ? value * 60 : value;
1529
- return Math.round(minutes * 60 * 1000);
1530
- }
1531
-
1532
- function durationPreset(rawDuration?: string) {
1533
- const raw = rawDuration?.trim().toLowerCase();
1534
- if (!raw) return undefined;
1535
- return raw.replace("-", "_");
1536
- }
1537
-
1538
- function parseApprovalRules() {
1539
- // --yolo wins over everything else: every approval gate is off,
1540
- // mirroring the org-level YOLO mode (settings -> Approvals).
1541
- if (hasFlag("--yolo")) return [];
1542
- const explicit = option("--approval-rules")
1543
- ?.split(",")
1544
- .map((rule) => rule.trim())
1545
- .filter(Boolean);
1546
- if (explicit && explicit.length > 0) return explicit;
1547
- const rules: string[] = [];
1548
- if (hasFlag("--ask-before-publishing")) rules.push("publishing");
1549
- if (hasFlag("--ask-before-spending")) rules.push("spending");
1550
- if (hasFlag("--ask-before-external-messages")) {
1551
- rules.push("external_messages");
1552
- }
1553
- if (hasFlag("--ask-before-production-changes")) {
1554
- rules.push("production_changes");
1555
- }
1556
- if (hasFlag("--ask-before-secrets")) rules.push("secrets");
1557
- return rules.length > 0
1558
- ? rules
1559
- : [
1560
- "publishing",
1561
- "spending",
1562
- "external_messages",
1563
- "production_changes",
1564
- "secrets",
1565
- ];
1566
- }
1567
-
1568
1366
  async function parseResponseBody(response: Response) {
1569
1367
  const text = await response.text();
1570
1368
  if (!text) return {};
@@ -1575,65 +1373,6 @@ async function parseResponseBody(response: Response) {
1575
1373
  }
1576
1374
  }
1577
1375
 
1578
- async function startAutonomousRun() {
1579
- const goal = positionalsFrom(2).join(" ").trim();
1580
- if (!goal) {
1581
- throw new CliError("Missing goal", {
1582
- code: "missing_goal",
1583
- nextActions: [
1584
- safeLongSessionExample(),
1585
- "Keep the goal specific and include safety constraints for risky actions.",
1586
- ],
1587
- });
1588
- }
1589
-
1590
- const model = option("--model")?.trim();
1591
- const duration = option("--duration");
1592
- const body: JsonRecord = {
1593
- goal,
1594
- metadata: {
1595
- sessionKind: "long_session",
1596
- approvalRules: parseApprovalRules(),
1597
- safetyConstraints: SAFE_LONG_SESSION_CONSTRAINTS,
1598
- ...(duration ? { durationPreset: durationPreset(duration) } : {}),
1599
- },
1600
- };
1601
- if (model) body.model = model;
1602
- const timeBudgetMs = parseDurationMs(duration);
1603
- if (timeBudgetMs) body.timeBudgetMs = timeBudgetMs;
1604
-
1605
- const apiBase = resolveAutonomousApiBase();
1606
- const agentSessionToken = await resolveAutonomousSessionToken(apiBase);
1607
- const headers: Record<string, string> = {
1608
- accept: "application/json",
1609
- "content-type": "application/json",
1610
- };
1611
- if (agentSessionToken) {
1612
- headers.authorization = `Bearer ${agentSessionToken}`;
1613
- }
1614
-
1615
- const response = await fetch(`${apiBase}/api/runs`, {
1616
- method: "POST",
1617
- headers,
1618
- body: JSON.stringify(body),
1619
- });
1620
- const payload = await parseResponseBody(response);
1621
-
1622
- if (!response.ok) {
1623
- throw formatAutonomousRunError({
1624
- status: response.status,
1625
- payload,
1626
- apiBase,
1627
- hasSession: Boolean(agentSessionToken),
1628
- statusText: response.statusText,
1629
- });
1630
- }
1631
-
1632
- const output = withAutonomousRunUrl(payload, apiBase);
1633
- throwIfCliError(output);
1634
- print(json ? output : formatAutonomousRunResult(output));
1635
- }
1636
-
1637
1376
  function formatUsd(cents: number | undefined) {
1638
1377
  const value = typeof cents === "number" && Number.isFinite(cents) ? cents : 0;
1639
1378
  return `$${(value / 100).toFixed(2)}`;
@@ -3198,18 +2937,6 @@ async function main() {
3198
2937
  return;
3199
2938
  }
3200
2939
 
3201
- if (command === "autonomous") {
3202
- if (!subcommand || subcommand === "help" || hasFlag("--help")) {
3203
- autonomousHelp();
3204
- return;
3205
- }
3206
- if (subcommand === "run") {
3207
- await startAutonomousRun();
3208
- return;
3209
- }
3210
- throw new Error(`Unknown autonomous command: ${filteredArgs.join(" ")}`);
3211
- }
3212
-
3213
2940
  if (command === "crm") {
3214
2941
  const subArgs = stripRuntimeFlags(args.slice(args.indexOf("crm") + 1));
3215
2942
  const sub = subArgs[0];
package/fs-daemon.ts CHANGED
@@ -68,6 +68,12 @@ import {
68
68
  readMountInfo,
69
69
  } from "./fs-daemon-mount";
70
70
 
71
+ type FileTreeSyncRow = {
72
+ path: string;
73
+ contentHash?: string;
74
+ isDir: boolean;
75
+ };
76
+
71
77
  type Args = {
72
78
  orgId?: string;
73
79
  workspace: string;
@@ -302,6 +308,9 @@ const api = {
302
308
  ),
303
309
  deleteFile: makeFunctionReference<"mutation">("functions/files:deleteFile"),
304
310
  listTree: makeFunctionReference<"query">("functions/files:listTree"),
311
+ listTreeSnapshot: makeFunctionReference<"query">(
312
+ "functions/files:listTreeSnapshot",
313
+ ),
305
314
  resolveApiKeyOrganization: makeFunctionReference<"query">(
306
315
  "functions/files:resolveApiKeyOrganization",
307
316
  ),
@@ -404,13 +413,11 @@ class DenchFileClient {
404
413
  * fs status` to recursively crawl the canonical tree and diff
405
414
  * against the local FS snapshot.
406
415
  */
407
- async listTree(
408
- prefix: string,
409
- ): Promise<Array<{ path: string; contentHash?: string; isDir: boolean }>> {
416
+ async listTree(prefix: string): Promise<FileTreeSyncRow[]> {
410
417
  const rows = (await this.http.query(api.files.listTree, {
411
418
  prefix,
412
419
  apiKey: this.apiKey,
413
- })) as Array<{ path: string; contentHash?: string; isDir: boolean }>;
420
+ })) as FileTreeSyncRow[];
414
421
  return Array.isArray(rows) ? rows : [];
415
422
  }
416
423
 
@@ -468,25 +475,26 @@ class DenchFileClient {
468
475
  */
469
476
  subscribeFileTree(
470
477
  convexUrl: string,
471
- onChange: (
472
- rows: Array<{ path: string; contentHash?: string; isDir: boolean }>,
473
- ) => void,
478
+ onChange: (rows: FileTreeSyncRow[]) => void,
474
479
  ): () => void {
475
480
  if (!this.realtime) {
476
481
  this.realtime = new ConvexClient(convexUrl);
477
482
  }
478
483
  const unsubscribe = this.realtime.onUpdate(
479
- api.files.listTree,
480
- { prefix: "/", apiKey: this.apiKey },
481
- (rows: unknown) => {
482
- const list = Array.isArray(rows)
483
- ? (rows as Array<{
484
- path: string;
485
- contentHash?: string;
486
- isDir: boolean;
487
- }>)
488
- : [];
489
- onChange(list);
484
+ api.files.listTreeSnapshot,
485
+ { apiKey: this.apiKey },
486
+ (snapshot: unknown) => {
487
+ // Remote writes may land deep in the tree (for example seeded
488
+ // `/skills/<slug>/SKILL.md` files). A root-only `listTree`
489
+ // subscription only sees the `/skills` directory row, so it
490
+ // never pulls the actual file bytes down to the volume.
491
+ const rows =
492
+ snapshot &&
493
+ typeof snapshot === "object" &&
494
+ Array.isArray((snapshot as { rows?: unknown }).rows)
495
+ ? (snapshot as { rows: FileTreeSyncRow[] }).rows
496
+ : [];
497
+ onChange(rows);
490
498
  },
491
499
  );
492
500
  return unsubscribe;
package/home-sync.ts CHANGED
@@ -9,9 +9,11 @@
9
9
  * symlink/rename limitations while keeping future sandboxes restorable.
10
10
  */
11
11
  import { spawn } from "node:child_process";
12
- import { cp, mkdtemp, rm, writeFile } from "node:fs/promises";
12
+ import { createReadStream, createWriteStream } from "node:fs";
13
+ import { mkdtemp, rm, writeFile } from "node:fs/promises";
13
14
  import { tmpdir } from "node:os";
14
15
  import { join } from "node:path";
16
+ import { pipeline } from "node:stream/promises";
15
17
  import {
16
18
  buildSnapshotName,
17
19
  buildTarCreateArgs,
@@ -61,6 +63,16 @@ function runCommand(command: string, args: string[]): Promise<void> {
61
63
  });
62
64
  }
63
65
 
66
+ async function copyBytesOnly(
67
+ source: string,
68
+ destination: string,
69
+ ): Promise<void> {
70
+ await pipeline(
71
+ createReadStream(source),
72
+ createWriteStream(destination, { flags: "wx" }),
73
+ );
74
+ }
75
+
64
76
  async function writeStatus(
65
77
  path: string,
66
78
  status: HomeSyncStatus,
@@ -178,7 +190,7 @@ class HomeSyncDaemon {
178
190
  "tar",
179
191
  buildTarCreateArgs({ liveHome: this.args.liveHome, archivePath }),
180
192
  );
181
- await cp(archivePath, durablePath, { force: false });
193
+ await copyBytesOnly(archivePath, durablePath);
182
194
  await pruneHomeSnapshots({
183
195
  snapshotDir: this.args.snapshotDir,
184
196
  keepSnapshots: this.args.keepSnapshots,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dench.com/cli",
3
- "version": "0.4.4",
3
+ "version": "0.4.6",
4
4
  "description": "Dench agent workspace CLI.",
5
5
  "type": "module",
6
6
  "bin": {