@dench.com/cli 0.4.5 → 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 +17 -453
- package/crm.ts +3 -50
- package/cron.ts +4 -9
- package/dench.ts +4 -277
- package/fs-daemon.ts +26 -18
- package/package.json +1 -1
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
|
-
|
|
34
|
-
|
|
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
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
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
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
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
|
-
|
|
125
|
-
|
|
126
|
-
|
|
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(
|
|
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
|
|
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
|
-
|
|
172
|
-
|
|
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
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
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
|
-
|
|
361
|
-
|
|
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
|
|
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
|
|
657
|
-
|
|
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,
|
|
671
|
-
|
|
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
|
|
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.
|
|
480
|
-
{
|
|
481
|
-
(
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
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;
|