@dench.com/cli 0.4.5 → 0.4.7
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/README.md +40 -2
- package/agent.ts +17 -453
- package/crm.ts +4 -51
- package/cron.ts +4 -9
- package/dench.ts +324 -295
- package/fs-daemon.ts +26 -18
- package/host.ts +60 -1
- package/lib/enrichment-gateway.ts +7 -4
- package/lib/load-dev-env.mjs +90 -15
- package/lib/load-dev-env.test.mjs +44 -0
- package/package.json +1 -1
- package/tools.ts +6 -2
package/README.md
CHANGED
|
@@ -23,7 +23,27 @@ dench login --name "AI Agent - Billing Repo"
|
|
|
23
23
|
to lowercase snake_case before send (e.g. `--kind "Claude Code"` becomes
|
|
24
24
|
`claude_code`).
|
|
25
25
|
|
|
26
|
-
The default
|
|
26
|
+
The default backend is `https://dench.dev` (production). Use `dench backend`
|
|
27
|
+
to switch:
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
dench backend show
|
|
31
|
+
dench backend set production
|
|
32
|
+
dench backend set staging
|
|
33
|
+
dench backend set local # http://localhost:3000
|
|
34
|
+
dench backend set https://example.com # custom URL
|
|
35
|
+
dench backend clear # revert to production
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
For one-shot overrides (no config change):
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
dench --prod <cmd>
|
|
42
|
+
dench --staging <cmd>
|
|
43
|
+
dench --backend local <cmd>
|
|
44
|
+
dench --host https://example.com <cmd>
|
|
45
|
+
DENCH_HOST=https://example.com dench <cmd>
|
|
46
|
+
```
|
|
27
47
|
|
|
28
48
|
The smooth setup flow is:
|
|
29
49
|
|
|
@@ -197,13 +217,31 @@ Auth precedence:
|
|
|
197
217
|
|
|
198
218
|
1. Explicit `--dev` flag → use the dev workspace env (`NEXT_PUBLIC_CONVEX_URL`,
|
|
199
219
|
`DENCH_WORKSPACE`, `DENCH_DEV_AGENT_KEY`).
|
|
200
|
-
2. A locally-stored `dench login` session for the current
|
|
220
|
+
2. A locally-stored `dench login` session for the current backend (set via
|
|
221
|
+
`dench backend`, `--host`, `--prod`, or `--staging`).
|
|
201
222
|
3. `DENCH_API_KEY` + `CONVEX_URL` (sandbox / automation path).
|
|
202
223
|
|
|
203
224
|
Plain terminal usage does not silently fall back to dev env vars. Without
|
|
204
225
|
`--dev`, a saved session, or `DENCH_API_KEY` + `CONVEX_URL`, live commands
|
|
205
226
|
return `login_required`.
|
|
206
227
|
|
|
228
|
+
### `.env` auto-load is opt-in
|
|
229
|
+
|
|
230
|
+
When `dench` runs from inside the `dench.com` repo it does **not**
|
|
231
|
+
auto-load workspace `.env` / `.env.local` files by default — that used
|
|
232
|
+
to silently flip `NEXT_PUBLIC_CONVEX_URL` to the dev Convex deployment
|
|
233
|
+
and made the CLI talk to local Convex even when the user expected
|
|
234
|
+
production.
|
|
235
|
+
|
|
236
|
+
Auto-load now turns on only when one of:
|
|
237
|
+
|
|
238
|
+
- `--dev` (also switches the runtime to dev-workspace mode)
|
|
239
|
+
- `--use-dev-env` (load `.env` files but keep the rest of the runtime)
|
|
240
|
+
- `DENCH_USE_DEV_ENV=1` in the shell env (e.g. via `direnv`)
|
|
241
|
+
- `dench backend set local` (a stored local backend implies dev env)
|
|
242
|
+
|
|
243
|
+
Explicit shell exports (`GATEWAY_URL=… ./dench …`) always win regardless.
|
|
244
|
+
|
|
207
245
|
Commands that need to impersonate a specific agent (`dench memory`,
|
|
208
246
|
`dench task`, `dench claim`, `dench log`, `dench approval request`,
|
|
209
247
|
etc.) still require an agent session (`dench login`) — the unified API
|
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>"}`);
|
|
@@ -1700,7 +1653,7 @@ async function callEnrichmentGatewayForEntry(args: {
|
|
|
1700
1653
|
|
|
1701
1654
|
async function enrichOneEntry(
|
|
1702
1655
|
ctx: CrmCliContext,
|
|
1703
|
-
args: CellEnrichmentArgs & {
|
|
1656
|
+
args: Omit<CellEnrichmentArgs, "entryId"> & {
|
|
1704
1657
|
target: EnrichmentTarget;
|
|
1705
1658
|
entry: CrmEntryForEnrichment;
|
|
1706
1659
|
},
|
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] \\
|