@dench.com/cli 0.3.7 → 0.3.8
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 +5 -2
- package/agent.ts +95 -13
- package/chat-spawn.ts +65 -4
- package/chat.ts +134 -1
- package/dench.ts +118 -17
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -195,11 +195,14 @@ dench crm entries list people
|
|
|
195
195
|
|
|
196
196
|
Auth precedence:
|
|
197
197
|
|
|
198
|
-
1. `--dev` flag → use the dev workspace env (`NEXT_PUBLIC_CONVEX_URL`,
|
|
198
|
+
1. Explicit `--dev` flag → use the dev workspace env (`NEXT_PUBLIC_CONVEX_URL`,
|
|
199
199
|
`DENCH_WORKSPACE`, `DENCH_DEV_AGENT_KEY`).
|
|
200
200
|
2. A locally-stored `dench login` session for the current host.
|
|
201
201
|
3. `DENCH_API_KEY` + `CONVEX_URL` (sandbox / automation path).
|
|
202
|
-
|
|
202
|
+
|
|
203
|
+
Plain terminal usage does not silently fall back to dev env vars. Without
|
|
204
|
+
`--dev`, a saved session, or `DENCH_API_KEY` + `CONVEX_URL`, live commands
|
|
205
|
+
return `login_required`.
|
|
203
206
|
|
|
204
207
|
Commands that need to impersonate a specific agent (`dench memory`,
|
|
205
208
|
`dench task`, `dench claim`, `dench log`, `dench approval request`,
|
package/agent.ts
CHANGED
|
@@ -34,6 +34,20 @@ type CliCtx = {
|
|
|
34
34
|
convex: ConvexHttpClient;
|
|
35
35
|
args: string[];
|
|
36
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;
|
|
37
51
|
};
|
|
38
52
|
|
|
39
53
|
// Args helpers moved to cli/lib/cli-args.ts; we adapt shift to throw
|
|
@@ -137,10 +151,18 @@ async function postJson(
|
|
|
137
151
|
export async function runAgentCommand(opts: {
|
|
138
152
|
convex: ConvexHttpClient;
|
|
139
153
|
args: string[];
|
|
154
|
+
sessionToken?: string;
|
|
155
|
+
callerRunId?: string;
|
|
140
156
|
}): Promise<void> {
|
|
141
157
|
const args = [...opts.args];
|
|
142
158
|
const jsonOutput = hasFlag(args, "--json");
|
|
143
|
-
const ctx: CliCtx = {
|
|
159
|
+
const ctx: CliCtx = {
|
|
160
|
+
convex: opts.convex,
|
|
161
|
+
args,
|
|
162
|
+
jsonOutput,
|
|
163
|
+
sessionToken: opts.sessionToken,
|
|
164
|
+
callerRunId: opts.callerRunId,
|
|
165
|
+
};
|
|
144
166
|
const subcommand = args.shift();
|
|
145
167
|
if (!subcommand || subcommand === "help" || subcommand === "--help") {
|
|
146
168
|
agentHelp();
|
|
@@ -243,27 +265,81 @@ async function runAwait(ctx: CliCtx): Promise<void> {
|
|
|
243
265
|
"dench agent await requires --hook <token> or --children <csv>",
|
|
244
266
|
);
|
|
245
267
|
}
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
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
|
+
}
|
|
250
282
|
const start = Date.now();
|
|
251
283
|
const timeoutMs = parseInt(
|
|
252
284
|
getFlag(ctx.args, "--timeout-ms") ?? "3600000",
|
|
253
285
|
10,
|
|
254
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> = {};
|
|
255
308
|
while (Object.keys(completed).length < childIds.length) {
|
|
256
309
|
if (Date.now() - start > timeoutMs) {
|
|
257
|
-
throw new AgentCliError(
|
|
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
|
+
);
|
|
258
315
|
}
|
|
259
316
|
for (const childId of childIds) {
|
|
260
317
|
if (completed[childId]) continue;
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
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));
|
|
264
342
|
}
|
|
265
|
-
await new Promise((resolve) => setTimeout(resolve, 2000));
|
|
266
|
-
break; // v1 polling stub — placeholder
|
|
267
343
|
}
|
|
268
344
|
out(ctx, { children: completed });
|
|
269
345
|
}
|
|
@@ -367,12 +443,18 @@ function agentHelp(): void {
|
|
|
367
443
|
Spawn a chat-turn workflow (same as the web UI's chat panel):
|
|
368
444
|
dench agent new "<prompt>" [--model <m>] [--file-context <path>] [--visibility private|shared] [--title T] [--yolo] [--follow] [--json]
|
|
369
445
|
dench agent send <thread-id> "<message>" [--model <m>] [--yolo] [--follow] [--json]
|
|
370
|
-
|
|
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.
|
|
371
450
|
|
|
372
451
|
Subagents (autonomous Long Sessions, NOT chat threads):
|
|
373
452
|
dench agent spawn '<goal>' [--parent-run-id <id>] [--sandbox own|share_parent] [--time-budget-ms N]
|
|
374
453
|
dench agent await --hook <token>
|
|
375
|
-
dench agent await --children id1,id2,id3 [--timeout-ms N]
|
|
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)
|
|
376
458
|
|
|
377
459
|
Peer messaging:
|
|
378
460
|
dench agent message <toRunId> '<text...>' [--from-run-id <id>]
|
package/chat-spawn.ts
CHANGED
|
@@ -87,21 +87,47 @@ function buildSpawnUrl(host: string): string {
|
|
|
87
87
|
return `${host.replace(/\/+$/, "")}/api/chat/cli`;
|
|
88
88
|
}
|
|
89
89
|
|
|
90
|
-
function
|
|
90
|
+
function buildStreamUrlByRunId(host: string, runId: string): string {
|
|
91
91
|
return `${host.replace(/\/+$/, "")}/api/chat/${encodeURIComponent(runId)}/stream`;
|
|
92
92
|
}
|
|
93
93
|
|
|
94
|
+
function buildStreamUrlByThreadId(host: string, threadId: string): string {
|
|
95
|
+
return `${host.replace(/\/+$/, "")}/api/chat/stream?sessionId=${encodeURIComponent(threadId)}`;
|
|
96
|
+
}
|
|
97
|
+
|
|
94
98
|
/**
|
|
95
99
|
* Subscribe to the durable chat stream and pretty-print useful chunks
|
|
96
100
|
* until the stream closes. Mirrors the chunk types the chat panel
|
|
97
101
|
* cares about (text deltas, reasoning, tool input/output, finish).
|
|
102
|
+
*
|
|
103
|
+
* Resolves the stream URL from EITHER:
|
|
104
|
+
* - `runId` (Workflow SDK run id, returned in `x-workflow-run-id`
|
|
105
|
+
* by `POST /api/chat/cli`), via `/api/chat/[runId]/stream`, OR
|
|
106
|
+
* - `threadId` (Convex `chatThreads._id`), via
|
|
107
|
+
* `/api/chat/stream?sessionId=...` which resolves the active run
|
|
108
|
+
* by following `chatThreads.activeRunId → runs.workflowRunId`.
|
|
109
|
+
*
|
|
110
|
+
* The thread-id path is what `dench chat follow <threadId>` uses, so
|
|
111
|
+
* we can tail any live thread (including a subagent created by
|
|
112
|
+
* `spawn_agent`) without needing to know its workflow run id ahead of
|
|
113
|
+
* time.
|
|
98
114
|
*/
|
|
99
115
|
async function followStream(args: {
|
|
100
116
|
host: string;
|
|
101
117
|
bearerToken: string;
|
|
102
|
-
runId
|
|
118
|
+
runId?: string;
|
|
119
|
+
threadId?: string;
|
|
103
120
|
}): Promise<void> {
|
|
104
|
-
const url =
|
|
121
|
+
const url = args.runId
|
|
122
|
+
? buildStreamUrlByRunId(args.host, args.runId)
|
|
123
|
+
: args.threadId
|
|
124
|
+
? buildStreamUrlByThreadId(args.host, args.threadId)
|
|
125
|
+
: null;
|
|
126
|
+
if (!url) {
|
|
127
|
+
throw new ChatSpawnError(
|
|
128
|
+
"followStream requires either { runId } or { threadId }",
|
|
129
|
+
);
|
|
130
|
+
}
|
|
105
131
|
const response = await fetch(url, {
|
|
106
132
|
method: "GET",
|
|
107
133
|
headers: {
|
|
@@ -342,6 +368,37 @@ async function runChatSend(ctx: ChatSpawnContext): Promise<void> {
|
|
|
342
368
|
}
|
|
343
369
|
}
|
|
344
370
|
|
|
371
|
+
/**
|
|
372
|
+
* `dench chat follow <threadId>` — tail the live stream of an
|
|
373
|
+
* existing chat thread by Convex thread id.
|
|
374
|
+
*
|
|
375
|
+
* Useful for watching a subagent (a child thread spawned by a parent
|
|
376
|
+
* via `spawn_agent`) live, or for resuming the stream of any chat
|
|
377
|
+
* thread that's currently in flight. Resolves the active workflow run
|
|
378
|
+
* server-side via `chatThreads.activeRunId → runs.workflowRunId`, so
|
|
379
|
+
* the caller doesn't need to know any workflow ids.
|
|
380
|
+
*
|
|
381
|
+
* Exits when the stream closes (workflow finishes) or when the user
|
|
382
|
+
* hits Ctrl-C.
|
|
383
|
+
*/
|
|
384
|
+
async function runChatFollow(ctx: ChatSpawnContext): Promise<void> {
|
|
385
|
+
const positionals = ctx.args.filter((arg) => !arg.startsWith("--"));
|
|
386
|
+
const threadId = positionals.shift();
|
|
387
|
+
if (!threadId) {
|
|
388
|
+
throw new ChatSpawnError(
|
|
389
|
+
"Usage: dench chat follow <threadId> [--json]",
|
|
390
|
+
);
|
|
391
|
+
}
|
|
392
|
+
if (!ctx.jsonOutput) {
|
|
393
|
+
console.error(`--- following stream for thread ${threadId} ---`);
|
|
394
|
+
}
|
|
395
|
+
await followStream({
|
|
396
|
+
host: ctx.runtime.host,
|
|
397
|
+
bearerToken: ctx.runtime.bearerToken,
|
|
398
|
+
threadId,
|
|
399
|
+
});
|
|
400
|
+
}
|
|
401
|
+
|
|
345
402
|
/**
|
|
346
403
|
* Public entry point invoked by the chat dispatcher in cli/dench.ts
|
|
347
404
|
* (and the agent dispatcher when the user runs `dench agent new` /
|
|
@@ -351,7 +408,7 @@ async function runChatSend(ctx: ChatSpawnContext): Promise<void> {
|
|
|
351
408
|
export async function runChatSpawnCommand(opts: {
|
|
352
409
|
runtime: RuntimeBundle;
|
|
353
410
|
args: string[];
|
|
354
|
-
subcommand: "new" | "send";
|
|
411
|
+
subcommand: "new" | "send" | "follow";
|
|
355
412
|
}): Promise<void> {
|
|
356
413
|
const args = [...opts.args];
|
|
357
414
|
const jsonOutput = hasFlag(args, "--json");
|
|
@@ -364,6 +421,10 @@ export async function runChatSpawnCommand(opts: {
|
|
|
364
421
|
await runChatNew(ctx);
|
|
365
422
|
return;
|
|
366
423
|
}
|
|
424
|
+
if (opts.subcommand === "follow") {
|
|
425
|
+
await runChatFollow(ctx);
|
|
426
|
+
return;
|
|
427
|
+
}
|
|
367
428
|
await runChatSend(ctx);
|
|
368
429
|
}
|
|
369
430
|
|
package/chat.ts
CHANGED
|
@@ -111,7 +111,12 @@ Read messages from a specific past chat thread:
|
|
|
111
111
|
Spawn a new chat-turn workflow (same agent loop as the web UI):
|
|
112
112
|
dench chat new "<prompt>" [--model <m>] [--file-context <path>] [--visibility private|shared] [--title T] [--yolo] [--follow] [--json]
|
|
113
113
|
dench chat send <thread-id> "<message>" [--model <m>] [--yolo] [--follow] [--json]
|
|
114
|
-
|
|
114
|
+
dench chat follow <thread-id> [--json]
|
|
115
|
+
dench chat tree [<root-thread-id>] [--limit N] [--json]
|
|
116
|
+
Aliased as: dench agent new / dench agent send / dench agent follow.
|
|
117
|
+
Use \`follow\` to tail any live thread by id (e.g. a subagent
|
|
118
|
+
spawned by another chat). Use \`tree\` to print the parent-child
|
|
119
|
+
thread hierarchy from a root thread.
|
|
115
120
|
|
|
116
121
|
Approval policy:
|
|
117
122
|
Without --yolo the workflow uses the workspace policy from
|
|
@@ -156,6 +161,8 @@ export async function runChatCommand(opts: {
|
|
|
156
161
|
return await runChatSearchCommand(ctx);
|
|
157
162
|
case "read":
|
|
158
163
|
return await runChatReadCommand(ctx);
|
|
164
|
+
case "tree":
|
|
165
|
+
return await runChatTreeCommand(ctx);
|
|
159
166
|
default:
|
|
160
167
|
throw new ChatCliError(`Unknown chat subcommand: ${subcommand}`);
|
|
161
168
|
}
|
|
@@ -214,3 +221,129 @@ async function runChatReadCommand(ctx: ChatCliContext): Promise<void> {
|
|
|
214
221
|
}),
|
|
215
222
|
);
|
|
216
223
|
}
|
|
224
|
+
|
|
225
|
+
/**
|
|
226
|
+
* `dench chat tree [<root-thread-id>] [--limit N] [--json]` — print the
|
|
227
|
+
* parent-child chat thread hierarchy rooted at the given thread (or
|
|
228
|
+
* roots = top-level chats if no id is passed).
|
|
229
|
+
*
|
|
230
|
+
* Uses `listPastChats` (which already includes nested threads in its
|
|
231
|
+
* results — `topLevelOnly` is a `listThreads`-only knob) and groups
|
|
232
|
+
* client-side by `parentThreadId`. With `--json`, prints the raw flat
|
|
233
|
+
* array; otherwise prints an indented tree with status + a short
|
|
234
|
+
* summary excerpt for each row.
|
|
235
|
+
*/
|
|
236
|
+
async function runChatTreeCommand(ctx: ChatCliContext): Promise<void> {
|
|
237
|
+
// Strip flag values from positionals so e.g. `tree --limit 30` doesn't
|
|
238
|
+
// misinterpret "30" as the rootThreadId. We treat "--key value" as a
|
|
239
|
+
// pair iff value doesn't itself start with "--".
|
|
240
|
+
const positionals: string[] = [];
|
|
241
|
+
for (let i = 0; i < ctx.args.length; i++) {
|
|
242
|
+
const arg = ctx.args[i];
|
|
243
|
+
if (arg.startsWith("--")) {
|
|
244
|
+
const next = ctx.args[i + 1];
|
|
245
|
+
if (next !== undefined && !next.startsWith("--")) {
|
|
246
|
+
i += 1; // skip the flag value
|
|
247
|
+
}
|
|
248
|
+
continue;
|
|
249
|
+
}
|
|
250
|
+
positionals.push(arg);
|
|
251
|
+
}
|
|
252
|
+
const rootThreadId = positionals[0]?.trim() || undefined;
|
|
253
|
+
const limit = parseLimit(getFlag(ctx.args, "--limit")) ?? 200;
|
|
254
|
+
|
|
255
|
+
const flat = (await callQuery(ctx, api.listPastChats, {
|
|
256
|
+
limit,
|
|
257
|
+
includeArchived: false,
|
|
258
|
+
})) as Array<{
|
|
259
|
+
threadId: string;
|
|
260
|
+
title: string;
|
|
261
|
+
status: string;
|
|
262
|
+
parentThreadId: string | null;
|
|
263
|
+
nestingDepth: number | null;
|
|
264
|
+
summary: string | null;
|
|
265
|
+
lastMessageAt: number;
|
|
266
|
+
createdAt: number;
|
|
267
|
+
activeRunId: string | null;
|
|
268
|
+
}>;
|
|
269
|
+
|
|
270
|
+
if (ctx.jsonOutput) {
|
|
271
|
+
out(ctx, flat);
|
|
272
|
+
return;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
// Group by parent. Top-level rows = either no parent, or parent
|
|
276
|
+
// outside the loaded set (so we don't silently swallow children
|
|
277
|
+
// whose parent fell off the limit window).
|
|
278
|
+
const ids = new Set(flat.map((t) => t.threadId));
|
|
279
|
+
const childrenByParent = new Map<string, typeof flat>();
|
|
280
|
+
const roots: typeof flat = [];
|
|
281
|
+
for (const thread of flat) {
|
|
282
|
+
if (thread.parentThreadId && ids.has(thread.parentThreadId)) {
|
|
283
|
+
let list = childrenByParent.get(thread.parentThreadId);
|
|
284
|
+
if (!list) {
|
|
285
|
+
list = [];
|
|
286
|
+
childrenByParent.set(thread.parentThreadId, list);
|
|
287
|
+
}
|
|
288
|
+
list.push(thread);
|
|
289
|
+
continue;
|
|
290
|
+
}
|
|
291
|
+
if (rootThreadId) {
|
|
292
|
+
if (thread.threadId === rootThreadId) roots.push(thread);
|
|
293
|
+
} else {
|
|
294
|
+
roots.push(thread);
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
// When the user passed an explicit `rootThreadId` but it didn't
|
|
298
|
+
// appear in the loaded set (e.g. it sits beyond `--limit`), pull
|
|
299
|
+
// it on its own so we still surface it as the single root.
|
|
300
|
+
if (rootThreadId && roots.length === 0) {
|
|
301
|
+
const explicit = flat.find((t) => t.threadId === rootThreadId);
|
|
302
|
+
if (explicit) roots.push(explicit);
|
|
303
|
+
}
|
|
304
|
+
// Sort children oldest-first within each parent (spawn order),
|
|
305
|
+
// roots newest-first (most recent root at the top).
|
|
306
|
+
for (const list of childrenByParent.values()) {
|
|
307
|
+
list.sort((a, b) => (a.createdAt || a.lastMessageAt) - (b.createdAt || b.lastMessageAt));
|
|
308
|
+
}
|
|
309
|
+
roots.sort((a, b) => b.lastMessageAt - a.lastMessageAt);
|
|
310
|
+
|
|
311
|
+
if (roots.length === 0) {
|
|
312
|
+
if (rootThreadId) {
|
|
313
|
+
console.error(
|
|
314
|
+
`No thread found with id ${rootThreadId} (or it's outside the limit ` +
|
|
315
|
+
`${limit} window — increase --limit).`,
|
|
316
|
+
);
|
|
317
|
+
} else {
|
|
318
|
+
console.error("No chat threads in this workspace yet.");
|
|
319
|
+
}
|
|
320
|
+
return;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
const stripe = (s: string, n: number) =>
|
|
324
|
+
s.length > n ? `${s.slice(0, n - 1)}…` : s;
|
|
325
|
+
const print = (thread: (typeof flat)[number], depth: number): void => {
|
|
326
|
+
const indent = " ".repeat(depth);
|
|
327
|
+
const branch = depth > 0 ? "└─ " : "";
|
|
328
|
+
const status =
|
|
329
|
+
thread.status === "streaming"
|
|
330
|
+
? "▸ running"
|
|
331
|
+
: thread.status === "error"
|
|
332
|
+
? "✗ error"
|
|
333
|
+
: thread.summary
|
|
334
|
+
? "✓ done"
|
|
335
|
+
: "·";
|
|
336
|
+
process.stdout.write(
|
|
337
|
+
`${indent}${branch}[${status}] ${stripe(thread.title || "Untitled", 60)} ` +
|
|
338
|
+
`(${thread.threadId})\n`,
|
|
339
|
+
);
|
|
340
|
+
if (thread.summary) {
|
|
341
|
+
process.stdout.write(
|
|
342
|
+
`${indent}${depth > 0 ? " " : ""}${stripe(thread.summary.replace(/\s+/g, " "), 80)}\n`,
|
|
343
|
+
);
|
|
344
|
+
}
|
|
345
|
+
const direct = childrenByParent.get(thread.threadId) ?? [];
|
|
346
|
+
for (const child of direct) print(child, depth + 1);
|
|
347
|
+
};
|
|
348
|
+
for (const root of roots) print(root, 0);
|
|
349
|
+
}
|
package/dench.ts
CHANGED
|
@@ -327,6 +327,41 @@ function throwIfCliError(value: unknown) {
|
|
|
327
327
|
}
|
|
328
328
|
}
|
|
329
329
|
|
|
330
|
+
function normalizeCliError(error: unknown): CliError {
|
|
331
|
+
if (error instanceof CliError) return error;
|
|
332
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
333
|
+
if (message.includes("Invalid dev agent key")) {
|
|
334
|
+
return new CliError("Invalid dev agent key", {
|
|
335
|
+
code: "invalid_dev_agent_key",
|
|
336
|
+
nextActions: [
|
|
337
|
+
"Check that DENCH_DEV_AGENT_KEY in your shell matches the Convex deployment env.",
|
|
338
|
+
"Use --dev only for a local/dev Convex deployment configured with that key.",
|
|
339
|
+
"For normal terminal usage, run dench login or set DENCH_API_KEY + CONVEX_URL.",
|
|
340
|
+
],
|
|
341
|
+
});
|
|
342
|
+
}
|
|
343
|
+
if (message.includes("Dev CRM access is disabled in production")) {
|
|
344
|
+
return new CliError("Dev CRM access is disabled in production", {
|
|
345
|
+
code: "dev_access_disabled",
|
|
346
|
+
nextActions: [
|
|
347
|
+
"Remove --dev and run dench login for terminal access.",
|
|
348
|
+
"Or use DENCH_API_KEY + CONVEX_URL for sandbox/automation access.",
|
|
349
|
+
],
|
|
350
|
+
});
|
|
351
|
+
}
|
|
352
|
+
if (message.includes("Invalid Dench API key")) {
|
|
353
|
+
return new CliError("Invalid Dench API key", {
|
|
354
|
+
code: "invalid_api_key",
|
|
355
|
+
nextActions: [
|
|
356
|
+
"Check DENCH_API_KEY and CONVEX_URL point to the same organization/deployment.",
|
|
357
|
+
"Unset DENCH_API_KEY to use a saved dench login session instead.",
|
|
358
|
+
"Or run dench login to create a new terminal session.",
|
|
359
|
+
],
|
|
360
|
+
});
|
|
361
|
+
}
|
|
362
|
+
return new CliError(message);
|
|
363
|
+
}
|
|
364
|
+
|
|
330
365
|
function parseJsonObjectOption(name: string) {
|
|
331
366
|
const raw = option(name);
|
|
332
367
|
if (!raw) return {};
|
|
@@ -461,8 +496,12 @@ Past chat threads (own + shared in this workspace):
|
|
|
461
496
|
Spawn a new chat-turn workflow (same as the web UI):
|
|
462
497
|
dench chat new "<prompt>" [--model <m>] [--file-context <path>] [--visibility private|shared] [--title T] [--yolo] [--follow] [--json]
|
|
463
498
|
dench chat send <thread-id> "<message>" [--model <m>] [--yolo] [--follow] [--json]
|
|
464
|
-
dench
|
|
465
|
-
dench
|
|
499
|
+
dench chat follow <thread-id> [--json] (tail a live thread by id)
|
|
500
|
+
dench chat tree [<root-thread-id>] [--limit N] [--json]
|
|
501
|
+
(parent-child thread tree)
|
|
502
|
+
dench agent new ... (alias for dench chat new)
|
|
503
|
+
dench agent send ... (alias for dench chat send)
|
|
504
|
+
dench agent follow ... (alias for dench chat follow)
|
|
466
505
|
|
|
467
506
|
Help: dench chat help
|
|
468
507
|
|
|
@@ -1755,6 +1794,30 @@ async function getRuntime() {
|
|
|
1755
1794
|
});
|
|
1756
1795
|
}
|
|
1757
1796
|
|
|
1797
|
+
// Sandbox session-mode path: workflow minting provisions a real
|
|
1798
|
+
// dch_agent_* token at sandbox-create time. Prefer it over apiKey so
|
|
1799
|
+
// agentWorkspace.* commands (Composio tools) work inside sandboxes.
|
|
1800
|
+
const sandboxAgentToken = process.env.DENCH_AGENT_SESSION_TOKEN?.trim();
|
|
1801
|
+
const sandboxConvexUrl = resolveApiKeyConvexUrl();
|
|
1802
|
+
if (sandboxAgentToken && sandboxConvexUrl) {
|
|
1803
|
+
const orgId = process.env.DENCH_ORG_ID?.trim();
|
|
1804
|
+
const orgSlug = process.env.DENCH_ORG_SLUG?.trim();
|
|
1805
|
+
const apiHost =
|
|
1806
|
+
process.env.DENCH_API_URL?.trim() ||
|
|
1807
|
+
process.env.DENCH_INTERNAL_BASE?.trim() ||
|
|
1808
|
+
host;
|
|
1809
|
+
return {
|
|
1810
|
+
mode: "session" as const,
|
|
1811
|
+
host: apiHost,
|
|
1812
|
+
client: new ConvexHttpClient(sandboxConvexUrl),
|
|
1813
|
+
sessionToken: sandboxAgentToken,
|
|
1814
|
+
organization:
|
|
1815
|
+
orgId && orgSlug
|
|
1816
|
+
? { id: orgId, name: orgSlug, slug: orgSlug }
|
|
1817
|
+
: undefined,
|
|
1818
|
+
};
|
|
1819
|
+
}
|
|
1820
|
+
|
|
1758
1821
|
// Sandbox / automation path: the unified Dench API key + Convex URL
|
|
1759
1822
|
// baked into sandbox envVars (see
|
|
1760
1823
|
// src/lib/secrets/sandbox-tokens.ts) is enough to drive every
|
|
@@ -1781,10 +1844,12 @@ async function getRuntime() {
|
|
|
1781
1844
|
: undefined,
|
|
1782
1845
|
};
|
|
1783
1846
|
}
|
|
1847
|
+
|
|
1848
|
+
throw loginRequiredError();
|
|
1784
1849
|
}
|
|
1785
1850
|
|
|
1786
1851
|
if (!hasDevEnv()) {
|
|
1787
|
-
throw
|
|
1852
|
+
throw missingDevEnvError();
|
|
1788
1853
|
}
|
|
1789
1854
|
|
|
1790
1855
|
return {
|
|
@@ -1799,6 +1864,22 @@ async function getRuntime() {
|
|
|
1799
1864
|
|
|
1800
1865
|
type Runtime = Awaited<ReturnType<typeof getRuntime>>;
|
|
1801
1866
|
|
|
1867
|
+
function stripRuntimeFlags(args: string[]) {
|
|
1868
|
+
const booleanFlags = new Set(["--dev", "--prod", "--staging"]);
|
|
1869
|
+
const valueFlags = new Set(["--convex-url", "--host"]);
|
|
1870
|
+
const stripped: string[] = [];
|
|
1871
|
+
for (let i = 0; i < args.length; i++) {
|
|
1872
|
+
const arg = args[i];
|
|
1873
|
+
if (booleanFlags.has(arg)) continue;
|
|
1874
|
+
if (valueFlags.has(arg)) {
|
|
1875
|
+
i++;
|
|
1876
|
+
continue;
|
|
1877
|
+
}
|
|
1878
|
+
stripped.push(arg);
|
|
1879
|
+
}
|
|
1880
|
+
return stripped;
|
|
1881
|
+
}
|
|
1882
|
+
|
|
1802
1883
|
/**
|
|
1803
1884
|
* `session` mode is required for `agentWorkspace.*` calls because they
|
|
1804
1885
|
* gate on `validateAgentSession` server-side, which only accepts the
|
|
@@ -1837,6 +1918,13 @@ function requireAuthenticatedRuntime(
|
|
|
1837
1918
|
throw loginRequiredError(command);
|
|
1838
1919
|
}
|
|
1839
1920
|
|
|
1921
|
+
function encodeDevCrmSessionToken(args: {
|
|
1922
|
+
workspaceSlug: string;
|
|
1923
|
+
devKey: string;
|
|
1924
|
+
}) {
|
|
1925
|
+
return `dch_dev_${encodeURIComponent(args.workspaceSlug)}:${encodeURIComponent(args.devKey)}`;
|
|
1926
|
+
}
|
|
1927
|
+
|
|
1840
1928
|
function agentSessionRequiredError(command: string) {
|
|
1841
1929
|
return new CliError(
|
|
1842
1930
|
`${command} requires an agent session — DENCH_API_KEY is org-scoped and cannot impersonate a specific agent`,
|
|
@@ -3458,7 +3546,7 @@ async function main() {
|
|
|
3458
3546
|
}
|
|
3459
3547
|
|
|
3460
3548
|
if (command === "crm") {
|
|
3461
|
-
const subArgs = args.slice(args.indexOf("crm") + 1);
|
|
3549
|
+
const subArgs = stripRuntimeFlags(args.slice(args.indexOf("crm") + 1));
|
|
3462
3550
|
const sub = subArgs[0];
|
|
3463
3551
|
// Short-circuit help so users don't need a session to read the
|
|
3464
3552
|
// surface. The crm dispatcher's own help resolves --json + the
|
|
@@ -3478,6 +3566,14 @@ async function main() {
|
|
|
3478
3566
|
// (`DENCH_API_KEY` baked into sandbox envVars). Server-side
|
|
3479
3567
|
// `requireCrmAccess` (convex/lib/crm/access.ts) dispatches on the
|
|
3480
3568
|
// bearer prefix and resolves the org for both.
|
|
3569
|
+
if (runtime.mode === "dev") {
|
|
3570
|
+
await runCrmCommand({
|
|
3571
|
+
convex: runtime.client,
|
|
3572
|
+
args: subArgs,
|
|
3573
|
+
sessionToken: encodeDevCrmSessionToken(runtime.workspaceArgs),
|
|
3574
|
+
});
|
|
3575
|
+
return;
|
|
3576
|
+
}
|
|
3481
3577
|
const authedRuntime = requireAuthenticatedRuntime(runtime, "dench crm");
|
|
3482
3578
|
await runCrmCommand({
|
|
3483
3579
|
convex: authedRuntime.client,
|
|
@@ -3534,12 +3630,13 @@ async function main() {
|
|
|
3534
3630
|
await runChatCommand({ convex: {} as any, args: subArgs });
|
|
3535
3631
|
return;
|
|
3536
3632
|
}
|
|
3537
|
-
// `chat new` / `chat send`
|
|
3538
|
-
// /api/chat/cli
|
|
3539
|
-
//
|
|
3540
|
-
//
|
|
3541
|
-
// queries and keep going
|
|
3542
|
-
|
|
3633
|
+
// `chat new` / `chat send` / `chat follow` use HTTP routes
|
|
3634
|
+
// (/api/chat/cli for spawn, /api/chat/[runId]/stream and
|
|
3635
|
+
// /api/chat/stream for follow) so they need an authenticated host
|
|
3636
|
+
// + bearer token, not a Convex client. The remaining subcommands
|
|
3637
|
+
// (list / search / read / tree) are Convex queries and keep going
|
|
3638
|
+
// through `runChatCommand`.
|
|
3639
|
+
if (sub === "new" || sub === "send" || sub === "follow") {
|
|
3543
3640
|
const { runChatSpawnCommand } = await import("./chat-spawn");
|
|
3544
3641
|
const runtime = await getRuntime();
|
|
3545
3642
|
const authedRuntime = requireAuthenticatedRuntime(
|
|
@@ -3583,11 +3680,12 @@ async function main() {
|
|
|
3583
3680
|
await runAgentCommand({ convex: {} as any, args: subArgs });
|
|
3584
3681
|
return;
|
|
3585
3682
|
}
|
|
3586
|
-
// Aliases: `dench agent new` / `dench agent send`
|
|
3587
|
-
// implementation. Surfaced under
|
|
3588
|
-
//
|
|
3589
|
-
//
|
|
3590
|
-
|
|
3683
|
+
// Aliases: `dench agent new` / `dench agent send` / `dench agent
|
|
3684
|
+
// follow` -> the chat-spawn implementation. Surfaced under
|
|
3685
|
+
// `agent` so users who associate chat threads with the agent
|
|
3686
|
+
// surface still find them; the actual workflow is identical to
|
|
3687
|
+
// `dench chat new` / `dench chat send` / `dench chat follow`.
|
|
3688
|
+
if (sub === "new" || sub === "send" || sub === "follow") {
|
|
3591
3689
|
const { runChatSpawnCommand } = await import("./chat-spawn");
|
|
3592
3690
|
const runtime = await getRuntime();
|
|
3593
3691
|
const authedRuntime = requireAuthenticatedRuntime(
|
|
@@ -3609,6 +3707,8 @@ async function main() {
|
|
|
3609
3707
|
await runAgentCommand({
|
|
3610
3708
|
convex: runtime.client,
|
|
3611
3709
|
args: subArgs,
|
|
3710
|
+
sessionToken: runtime.sessionToken,
|
|
3711
|
+
callerRunId: process.env.DENCH_RUN_ID?.trim() || undefined,
|
|
3612
3712
|
});
|
|
3613
3713
|
return;
|
|
3614
3714
|
}
|
|
@@ -4237,11 +4337,12 @@ async function main() {
|
|
|
4237
4337
|
}
|
|
4238
4338
|
|
|
4239
4339
|
main().catch((error) => {
|
|
4240
|
-
const
|
|
4340
|
+
const normalized = normalizeCliError(error);
|
|
4341
|
+
const extra = normalized.payload;
|
|
4241
4342
|
const payload: JsonRecord = {
|
|
4242
4343
|
ok: false,
|
|
4243
4344
|
...extra,
|
|
4244
|
-
error:
|
|
4345
|
+
error: normalized.message,
|
|
4245
4346
|
};
|
|
4246
4347
|
if (json) {
|
|
4247
4348
|
console.error(JSON.stringify(payload, null, 2));
|