@dench.com/cli 0.3.7 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +5 -2
- package/agent.ts +95 -13
- package/chat-spawn.ts +65 -4
- package/chat.ts +349 -9
- package/cron.ts +8 -4
- package/dench.ts +200 -20
- 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
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* `dench chat <subcommand>` — CLI surface for browsing
|
|
3
|
-
* in the user's workspace.
|
|
2
|
+
* `dench chat <subcommand>` — CLI surface for browsing and managing
|
|
3
|
+
* past chat threads in the user's workspace.
|
|
4
4
|
*
|
|
5
5
|
* Mirrors the `dench crm` pattern: a thin command dispatcher on top of
|
|
6
|
-
* Convex public queries (`functions/chat:listPastChats`,
|
|
7
|
-
* `searchPastChats`, `readPastChat`)
|
|
8
|
-
* "own + shared in this org" visibility
|
|
6
|
+
* Convex public queries/mutations (`functions/chat:listPastChats`,
|
|
7
|
+
* `searchPastChats`, `readPastChat`, `renameThreads`, `deleteThreads`)
|
|
8
|
+
* which themselves enforce the "own + shared in this org" visibility
|
|
9
|
+
* rule (`requireThreadAccess`).
|
|
9
10
|
*
|
|
10
11
|
* Auth: every call forwards a single bearer token via `sessionToken`
|
|
11
12
|
* (either a `dch_agent_*` agent session minted by `dench login` or a
|
|
@@ -16,9 +17,13 @@
|
|
|
16
17
|
* inside a sandbox without extra flags.
|
|
17
18
|
*
|
|
18
19
|
* Subcommands:
|
|
19
|
-
* chat list
|
|
20
|
-
* chat search
|
|
21
|
-
* chat read
|
|
20
|
+
* chat list [--query <substr>] [--limit N] [--include-archived]
|
|
21
|
+
* chat search <query> [--limit N] [--include-current]
|
|
22
|
+
* chat read <thread-id> [--limit N]
|
|
23
|
+
* chat rename <thread-id> <new-title> # single rename
|
|
24
|
+
* chat rename --from-stdin # batch via JSON {items:[...]}
|
|
25
|
+
* chat delete <thread-id> [<thread-id>...] # 1–25 ids, --yes to skip prompt
|
|
26
|
+
* chat delete --ids id1,id2,id3 # comma-separated form
|
|
22
27
|
*/
|
|
23
28
|
import type { ConvexHttpClient } from "convex/browser";
|
|
24
29
|
import { makeFunctionReference } from "convex/server";
|
|
@@ -67,6 +72,12 @@ const api = {
|
|
|
67
72
|
readPastChat: makeFunctionReference<"query">(
|
|
68
73
|
"functions/chat:readPastChat",
|
|
69
74
|
),
|
|
75
|
+
renameThreads: makeFunctionReference<"mutation">(
|
|
76
|
+
"functions/chat:renameThreads",
|
|
77
|
+
),
|
|
78
|
+
deleteThreads: makeFunctionReference<"mutation">(
|
|
79
|
+
"functions/chat:deleteThreads",
|
|
80
|
+
),
|
|
70
81
|
};
|
|
71
82
|
|
|
72
83
|
function commonArgs(ctx: ChatCliContext) {
|
|
@@ -84,6 +95,14 @@ async function callQuery(
|
|
|
84
95
|
return ctx.convex.query(fn, { ...args, ...commonArgs(ctx) } as never);
|
|
85
96
|
}
|
|
86
97
|
|
|
98
|
+
async function callMutation(
|
|
99
|
+
ctx: ChatCliContext,
|
|
100
|
+
fn: Parameters<ConvexHttpClient["mutation"]>[0],
|
|
101
|
+
args: Record<string, unknown> = {},
|
|
102
|
+
): Promise<unknown> {
|
|
103
|
+
return ctx.convex.mutation(fn, { ...args, ...commonArgs(ctx) } as never);
|
|
104
|
+
}
|
|
105
|
+
|
|
87
106
|
function out(ctx: ChatCliContext, value: unknown): void {
|
|
88
107
|
if (ctx.jsonOutput) {
|
|
89
108
|
console.log(JSON.stringify(value, null, 2));
|
|
@@ -108,10 +127,28 @@ Full-text search across past chat messages (own + shared):
|
|
|
108
127
|
Read messages from a specific past chat thread:
|
|
109
128
|
dench chat read <thread-id> [--limit N] [--json]
|
|
110
129
|
|
|
130
|
+
Rename one or more past chat threads (own + shared):
|
|
131
|
+
dench chat rename <thread-id> <new-title> [--json]
|
|
132
|
+
dench chat rename --from-stdin [--json]
|
|
133
|
+
Reads JSON {"items":[{"threadId":"...","title":"..."}, ...]} from stdin.
|
|
134
|
+
Up to 50 items per call. Titles are trimmed and clamped to 200 chars.
|
|
135
|
+
|
|
136
|
+
PERMANENTLY delete one or more past chat threads (and all their messages):
|
|
137
|
+
dench chat delete <thread-id> [<thread-id>...] [--yes] [--json]
|
|
138
|
+
dench chat delete --ids id1,id2,id3 [--yes] [--json]
|
|
139
|
+
Up to 25 ids per call. IRREVERSIBLE — the messages are removed too.
|
|
140
|
+
Without --yes, the CLI prompts for confirmation when stdin is a TTY.
|
|
141
|
+
In non-interactive contexts (sandboxes, scripts), --yes is required.
|
|
142
|
+
|
|
111
143
|
Spawn a new chat-turn workflow (same agent loop as the web UI):
|
|
112
144
|
dench chat new "<prompt>" [--model <m>] [--file-context <path>] [--visibility private|shared] [--title T] [--yolo] [--follow] [--json]
|
|
113
145
|
dench chat send <thread-id> "<message>" [--model <m>] [--yolo] [--follow] [--json]
|
|
114
|
-
|
|
146
|
+
dench chat follow <thread-id> [--json]
|
|
147
|
+
dench chat tree [<root-thread-id>] [--limit N] [--json]
|
|
148
|
+
Aliased as: dench agent new / dench agent send / dench agent follow.
|
|
149
|
+
Use \`follow\` to tail any live thread by id (e.g. a subagent
|
|
150
|
+
spawned by another chat). Use \`tree\` to print the parent-child
|
|
151
|
+
thread hierarchy from a root thread.
|
|
115
152
|
|
|
116
153
|
Approval policy:
|
|
117
154
|
Without --yolo the workflow uses the workspace policy from
|
|
@@ -156,6 +193,13 @@ export async function runChatCommand(opts: {
|
|
|
156
193
|
return await runChatSearchCommand(ctx);
|
|
157
194
|
case "read":
|
|
158
195
|
return await runChatReadCommand(ctx);
|
|
196
|
+
case "tree":
|
|
197
|
+
return await runChatTreeCommand(ctx);
|
|
198
|
+
case "rename":
|
|
199
|
+
return await runChatRenameCommand(ctx);
|
|
200
|
+
case "delete":
|
|
201
|
+
case "rm":
|
|
202
|
+
return await runChatDeleteCommand(ctx);
|
|
159
203
|
default:
|
|
160
204
|
throw new ChatCliError(`Unknown chat subcommand: ${subcommand}`);
|
|
161
205
|
}
|
|
@@ -214,3 +258,299 @@ async function runChatReadCommand(ctx: ChatCliContext): Promise<void> {
|
|
|
214
258
|
}),
|
|
215
259
|
);
|
|
216
260
|
}
|
|
261
|
+
|
|
262
|
+
async function readStdin(): Promise<string> {
|
|
263
|
+
if (process.stdin.isTTY) {
|
|
264
|
+
throw new ChatCliError(
|
|
265
|
+
"--from-stdin requires JSON piped on stdin (got TTY).",
|
|
266
|
+
);
|
|
267
|
+
}
|
|
268
|
+
const chunks: Buffer[] = [];
|
|
269
|
+
for await (const chunk of process.stdin as AsyncIterable<Buffer>) {
|
|
270
|
+
chunks.push(chunk);
|
|
271
|
+
}
|
|
272
|
+
return Buffer.concat(chunks).toString("utf8");
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
async function runChatRenameCommand(ctx: ChatCliContext): Promise<void> {
|
|
276
|
+
const fromStdin = hasFlag(ctx.args, "--from-stdin");
|
|
277
|
+
let items: Array<{ threadId: string; title: string }>;
|
|
278
|
+
if (fromStdin) {
|
|
279
|
+
const raw = (await readStdin()).trim();
|
|
280
|
+
if (!raw) {
|
|
281
|
+
throw new ChatCliError(
|
|
282
|
+
'Empty --from-stdin payload; expected {"items":[...]}.',
|
|
283
|
+
);
|
|
284
|
+
}
|
|
285
|
+
let parsed: unknown;
|
|
286
|
+
try {
|
|
287
|
+
parsed = JSON.parse(raw);
|
|
288
|
+
} catch (error) {
|
|
289
|
+
throw new ChatCliError(
|
|
290
|
+
`Invalid JSON on stdin: ${
|
|
291
|
+
error instanceof Error ? error.message : String(error)
|
|
292
|
+
}`,
|
|
293
|
+
);
|
|
294
|
+
}
|
|
295
|
+
if (
|
|
296
|
+
!parsed ||
|
|
297
|
+
typeof parsed !== "object" ||
|
|
298
|
+
!Array.isArray((parsed as { items?: unknown }).items)
|
|
299
|
+
) {
|
|
300
|
+
throw new ChatCliError(
|
|
301
|
+
'Stdin JSON must be {"items":[{"threadId":"...","title":"..."}]}.',
|
|
302
|
+
);
|
|
303
|
+
}
|
|
304
|
+
const rawItems = (parsed as { items: unknown[] }).items;
|
|
305
|
+
items = rawItems.map((entry, index) => {
|
|
306
|
+
if (
|
|
307
|
+
!entry ||
|
|
308
|
+
typeof entry !== "object" ||
|
|
309
|
+
typeof (entry as { threadId?: unknown }).threadId !== "string" ||
|
|
310
|
+
typeof (entry as { title?: unknown }).title !== "string"
|
|
311
|
+
) {
|
|
312
|
+
throw new ChatCliError(
|
|
313
|
+
`items[${index}] must be { threadId: string, title: string }.`,
|
|
314
|
+
);
|
|
315
|
+
}
|
|
316
|
+
const item = entry as { threadId: string; title: string };
|
|
317
|
+
if (!item.threadId.trim()) {
|
|
318
|
+
throw new ChatCliError(`items[${index}].threadId is empty.`);
|
|
319
|
+
}
|
|
320
|
+
if (!item.title.trim()) {
|
|
321
|
+
throw new ChatCliError(`items[${index}].title is empty.`);
|
|
322
|
+
}
|
|
323
|
+
return { threadId: item.threadId.trim(), title: item.title };
|
|
324
|
+
});
|
|
325
|
+
} else {
|
|
326
|
+
const threadId = shift(ctx.args, "thread id").trim();
|
|
327
|
+
const titleParts: string[] = [];
|
|
328
|
+
while (ctx.args.length > 0) {
|
|
329
|
+
const next = ctx.args.shift();
|
|
330
|
+
if (next === undefined) break;
|
|
331
|
+
if (next.startsWith("--")) {
|
|
332
|
+
ctx.args.unshift(next);
|
|
333
|
+
break;
|
|
334
|
+
}
|
|
335
|
+
titleParts.push(next);
|
|
336
|
+
}
|
|
337
|
+
const title = titleParts.join(" ").trim();
|
|
338
|
+
if (!title) {
|
|
339
|
+
throw new ChatCliError(
|
|
340
|
+
"Usage: dench chat rename <thread-id> <new-title>",
|
|
341
|
+
);
|
|
342
|
+
}
|
|
343
|
+
items = [{ threadId, title }];
|
|
344
|
+
}
|
|
345
|
+
if (items.length === 0) {
|
|
346
|
+
throw new ChatCliError("Nothing to rename.");
|
|
347
|
+
}
|
|
348
|
+
if (items.length > 50) {
|
|
349
|
+
throw new ChatCliError(
|
|
350
|
+
`Too many items (${items.length}); the server caps each call at 50.`,
|
|
351
|
+
);
|
|
352
|
+
}
|
|
353
|
+
out(ctx, await callMutation(ctx, api.renameThreads, { items }));
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
async function promptYesNo(question: string): Promise<boolean> {
|
|
357
|
+
if (!process.stdin.isTTY) return false;
|
|
358
|
+
process.stdout.write(`${question} [y/N] `);
|
|
359
|
+
return new Promise<boolean>((resolve) => {
|
|
360
|
+
const onData = (chunk: Buffer) => {
|
|
361
|
+
const answer = chunk.toString("utf8").trim().toLowerCase();
|
|
362
|
+
cleanup();
|
|
363
|
+
resolve(answer === "y" || answer === "yes");
|
|
364
|
+
};
|
|
365
|
+
const cleanup = () => {
|
|
366
|
+
process.stdin.off("data", onData);
|
|
367
|
+
try {
|
|
368
|
+
process.stdin.pause();
|
|
369
|
+
} catch {
|
|
370
|
+
// ignore: stdin may already be closed.
|
|
371
|
+
}
|
|
372
|
+
};
|
|
373
|
+
process.stdin.resume();
|
|
374
|
+
process.stdin.once("data", onData);
|
|
375
|
+
});
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
async function runChatDeleteCommand(ctx: ChatCliContext): Promise<void> {
|
|
379
|
+
const idsFlag = getFlag(ctx.args, "--ids");
|
|
380
|
+
const skipConfirm = hasFlag(ctx.args, "--yes") || hasFlag(ctx.args, "-y");
|
|
381
|
+
const collected: string[] = [];
|
|
382
|
+
if (idsFlag) {
|
|
383
|
+
for (const part of idsFlag.split(",")) {
|
|
384
|
+
const trimmed = part.trim();
|
|
385
|
+
if (trimmed) collected.push(trimmed);
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
while (ctx.args.length > 0) {
|
|
389
|
+
const next = ctx.args.shift();
|
|
390
|
+
if (next === undefined) break;
|
|
391
|
+
if (next.startsWith("--")) {
|
|
392
|
+
ctx.args.unshift(next);
|
|
393
|
+
break;
|
|
394
|
+
}
|
|
395
|
+
if (next.trim()) collected.push(next.trim());
|
|
396
|
+
}
|
|
397
|
+
const seen = new Set<string>();
|
|
398
|
+
const threadIds = collected.filter((id) => {
|
|
399
|
+
if (seen.has(id)) return false;
|
|
400
|
+
seen.add(id);
|
|
401
|
+
return true;
|
|
402
|
+
});
|
|
403
|
+
if (threadIds.length === 0) {
|
|
404
|
+
throw new ChatCliError(
|
|
405
|
+
"Usage: dench chat delete <thread-id> [<thread-id>...] (or --ids id1,id2,...)",
|
|
406
|
+
);
|
|
407
|
+
}
|
|
408
|
+
if (threadIds.length > 25) {
|
|
409
|
+
throw new ChatCliError(
|
|
410
|
+
`Too many thread ids (${threadIds.length}); the server caps each call at 25. Run multiple invocations.`,
|
|
411
|
+
);
|
|
412
|
+
}
|
|
413
|
+
if (!skipConfirm) {
|
|
414
|
+
if (!process.stdin.isTTY) {
|
|
415
|
+
throw new ChatCliError(
|
|
416
|
+
"Refusing to delete without --yes in non-interactive mode (no TTY).",
|
|
417
|
+
);
|
|
418
|
+
}
|
|
419
|
+
const ok = await promptYesNo(
|
|
420
|
+
`PERMANENTLY delete ${threadIds.length} chat${
|
|
421
|
+
threadIds.length === 1 ? "" : "s"
|
|
422
|
+
} and all their messages?`,
|
|
423
|
+
);
|
|
424
|
+
if (!ok) {
|
|
425
|
+
console.error("Aborted.");
|
|
426
|
+
return;
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
out(ctx, await callMutation(ctx, api.deleteThreads, { threadIds }));
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
/**
|
|
433
|
+
* `dench chat tree [<root-thread-id>] [--limit N] [--json]` — print the
|
|
434
|
+
* parent-child chat thread hierarchy rooted at the given thread (or
|
|
435
|
+
* roots = top-level chats if no id is passed).
|
|
436
|
+
*
|
|
437
|
+
* Uses `listPastChats` (which already includes nested threads in its
|
|
438
|
+
* results — `topLevelOnly` is a `listThreads`-only knob) and groups
|
|
439
|
+
* client-side by `parentThreadId`. With `--json`, prints the raw flat
|
|
440
|
+
* array; otherwise prints an indented tree with status + a short
|
|
441
|
+
* summary excerpt for each row.
|
|
442
|
+
*/
|
|
443
|
+
async function runChatTreeCommand(ctx: ChatCliContext): Promise<void> {
|
|
444
|
+
// Strip flag values from positionals so e.g. `tree --limit 30` doesn't
|
|
445
|
+
// misinterpret "30" as the rootThreadId. We treat "--key value" as a
|
|
446
|
+
// pair iff value doesn't itself start with "--".
|
|
447
|
+
const positionals: string[] = [];
|
|
448
|
+
for (let i = 0; i < ctx.args.length; i++) {
|
|
449
|
+
const arg = ctx.args[i];
|
|
450
|
+
if (arg.startsWith("--")) {
|
|
451
|
+
const next = ctx.args[i + 1];
|
|
452
|
+
if (next !== undefined && !next.startsWith("--")) {
|
|
453
|
+
i += 1; // skip the flag value
|
|
454
|
+
}
|
|
455
|
+
continue;
|
|
456
|
+
}
|
|
457
|
+
positionals.push(arg);
|
|
458
|
+
}
|
|
459
|
+
const rootThreadId = positionals[0]?.trim() || undefined;
|
|
460
|
+
const limit = parseLimit(getFlag(ctx.args, "--limit")) ?? 200;
|
|
461
|
+
|
|
462
|
+
const flat = (await callQuery(ctx, api.listPastChats, {
|
|
463
|
+
limit,
|
|
464
|
+
includeArchived: false,
|
|
465
|
+
})) as Array<{
|
|
466
|
+
threadId: string;
|
|
467
|
+
title: string;
|
|
468
|
+
status: string;
|
|
469
|
+
parentThreadId: string | null;
|
|
470
|
+
nestingDepth: number | null;
|
|
471
|
+
summary: string | null;
|
|
472
|
+
lastMessageAt: number;
|
|
473
|
+
createdAt: number;
|
|
474
|
+
activeRunId: string | null;
|
|
475
|
+
}>;
|
|
476
|
+
|
|
477
|
+
if (ctx.jsonOutput) {
|
|
478
|
+
out(ctx, flat);
|
|
479
|
+
return;
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
// Group by parent. Top-level rows = either no parent, or parent
|
|
483
|
+
// outside the loaded set (so we don't silently swallow children
|
|
484
|
+
// whose parent fell off the limit window).
|
|
485
|
+
const ids = new Set(flat.map((t) => t.threadId));
|
|
486
|
+
const childrenByParent = new Map<string, typeof flat>();
|
|
487
|
+
const roots: typeof flat = [];
|
|
488
|
+
for (const thread of flat) {
|
|
489
|
+
if (thread.parentThreadId && ids.has(thread.parentThreadId)) {
|
|
490
|
+
let list = childrenByParent.get(thread.parentThreadId);
|
|
491
|
+
if (!list) {
|
|
492
|
+
list = [];
|
|
493
|
+
childrenByParent.set(thread.parentThreadId, list);
|
|
494
|
+
}
|
|
495
|
+
list.push(thread);
|
|
496
|
+
continue;
|
|
497
|
+
}
|
|
498
|
+
if (rootThreadId) {
|
|
499
|
+
if (thread.threadId === rootThreadId) roots.push(thread);
|
|
500
|
+
} else {
|
|
501
|
+
roots.push(thread);
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
// When the user passed an explicit `rootThreadId` but it didn't
|
|
505
|
+
// appear in the loaded set (e.g. it sits beyond `--limit`), pull
|
|
506
|
+
// it on its own so we still surface it as the single root.
|
|
507
|
+
if (rootThreadId && roots.length === 0) {
|
|
508
|
+
const explicit = flat.find((t) => t.threadId === rootThreadId);
|
|
509
|
+
if (explicit) roots.push(explicit);
|
|
510
|
+
}
|
|
511
|
+
// Sort children oldest-first within each parent (spawn order),
|
|
512
|
+
// roots newest-first (most recent root at the top).
|
|
513
|
+
for (const list of childrenByParent.values()) {
|
|
514
|
+
list.sort((a, b) => (a.createdAt || a.lastMessageAt) - (b.createdAt || b.lastMessageAt));
|
|
515
|
+
}
|
|
516
|
+
roots.sort((a, b) => b.lastMessageAt - a.lastMessageAt);
|
|
517
|
+
|
|
518
|
+
if (roots.length === 0) {
|
|
519
|
+
if (rootThreadId) {
|
|
520
|
+
console.error(
|
|
521
|
+
`No thread found with id ${rootThreadId} (or it's outside the limit ` +
|
|
522
|
+
`${limit} window — increase --limit).`,
|
|
523
|
+
);
|
|
524
|
+
} else {
|
|
525
|
+
console.error("No chat threads in this workspace yet.");
|
|
526
|
+
}
|
|
527
|
+
return;
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
const stripe = (s: string, n: number) =>
|
|
531
|
+
s.length > n ? `${s.slice(0, n - 1)}…` : s;
|
|
532
|
+
const print = (thread: (typeof flat)[number], depth: number): void => {
|
|
533
|
+
const indent = " ".repeat(depth);
|
|
534
|
+
const branch = depth > 0 ? "└─ " : "";
|
|
535
|
+
const status =
|
|
536
|
+
thread.status === "streaming"
|
|
537
|
+
? "▸ running"
|
|
538
|
+
: thread.status === "error"
|
|
539
|
+
? "✗ error"
|
|
540
|
+
: thread.summary
|
|
541
|
+
? "✓ done"
|
|
542
|
+
: "·";
|
|
543
|
+
process.stdout.write(
|
|
544
|
+
`${indent}${branch}[${status}] ${stripe(thread.title || "Untitled", 60)} ` +
|
|
545
|
+
`(${thread.threadId})\n`,
|
|
546
|
+
);
|
|
547
|
+
if (thread.summary) {
|
|
548
|
+
process.stdout.write(
|
|
549
|
+
`${indent}${depth > 0 ? " " : ""}${stripe(thread.summary.replace(/\s+/g, " "), 80)}\n`,
|
|
550
|
+
);
|
|
551
|
+
}
|
|
552
|
+
const direct = childrenByParent.get(thread.threadId) ?? [];
|
|
553
|
+
for (const child of direct) print(child, depth + 1);
|
|
554
|
+
};
|
|
555
|
+
for (const root of roots) print(root, 0);
|
|
556
|
+
}
|
package/cron.ts
CHANGED
|
@@ -5,10 +5,12 @@
|
|
|
5
5
|
* Each cron is a row in the Convex `triggers` table that, when due,
|
|
6
6
|
* spawns a fresh chat thread (the `target: "chat_turn"` path — new
|
|
7
7
|
* default) running in YOLO mode and seeded with the cron's prompt as
|
|
8
|
-
* the first user message.
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
8
|
+
* the first user message. Scheduling is owned by the
|
|
9
|
+
* `@convex-dev/crons` component: each create/update/enable mutation
|
|
10
|
+
* registers a component cron under name=triggerId; the component fires
|
|
11
|
+
* `internal.functions.triggersNode.fireDenchTrigger` at the precise
|
|
12
|
+
* scheduled time, which then POSTs to `/api/runs/spawn-{chat,child}`
|
|
13
|
+
* to start the workflow.
|
|
12
14
|
*
|
|
13
15
|
* Auth: prefers the unified Dench API key from `DENCH_API_KEY`
|
|
14
16
|
* (sandbox envVar baked at create time). Falls back to `dench login`
|
|
@@ -504,6 +506,8 @@ async function runHistory(ctx: CronCliContext): Promise<void> {
|
|
|
504
506
|
|
|
505
507
|
function cronHelp(): void {
|
|
506
508
|
console.log(`Usage: dench cron <subcommand>
|
|
509
|
+
dench routine <subcommand> (alias)
|
|
510
|
+
dench routines <subcommand> (alias)
|
|
507
511
|
|
|
508
512
|
Browse:
|
|
509
513
|
dench cron list [--enabled-only] [--json]
|
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 {};
|
|
@@ -457,12 +492,20 @@ Past chat threads (own + shared in this workspace):
|
|
|
457
492
|
dench chat list [--query <substr>] [--limit N] [--include-archived] [--json]
|
|
458
493
|
dench chat search "<query>" [--limit N] [--include-current] [--json]
|
|
459
494
|
dench chat read <thread-id> [--limit N] [--json]
|
|
495
|
+
dench chat rename <thread-id> <new-title> [--json]
|
|
496
|
+
dench chat rename --from-stdin [--json] (batch via JSON {items:[...]})
|
|
497
|
+
dench chat delete <thread-id> [<thread-id>...] [--yes] [--json]
|
|
498
|
+
dench chat delete --ids id1,id2 [--yes] [--json] (PERMANENTLY deletes threads)
|
|
460
499
|
|
|
461
500
|
Spawn a new chat-turn workflow (same as the web UI):
|
|
462
501
|
dench chat new "<prompt>" [--model <m>] [--file-context <path>] [--visibility private|shared] [--title T] [--yolo] [--follow] [--json]
|
|
463
502
|
dench chat send <thread-id> "<message>" [--model <m>] [--yolo] [--follow] [--json]
|
|
464
|
-
dench
|
|
465
|
-
dench
|
|
503
|
+
dench chat follow <thread-id> [--json] (tail a live thread by id)
|
|
504
|
+
dench chat tree [<root-thread-id>] [--limit N] [--json]
|
|
505
|
+
(parent-child thread tree)
|
|
506
|
+
dench agent new ... (alias for dench chat new)
|
|
507
|
+
dench agent send ... (alias for dench chat send)
|
|
508
|
+
dench agent follow ... (alias for dench chat follow)
|
|
466
509
|
|
|
467
510
|
Help: dench chat help
|
|
468
511
|
|
|
@@ -495,6 +538,20 @@ Subagents (Daytona-native rewrite):
|
|
|
495
538
|
|
|
496
539
|
dench --version
|
|
497
540
|
|
|
541
|
+
Self-updating agent harness (OpenClaw parity):
|
|
542
|
+
dench identity show | edit IDENTITY.md (org-wide)
|
|
543
|
+
dench organisation show | edit ORGANISATION.md
|
|
544
|
+
dench user show | edit USER.md (per-member)
|
|
545
|
+
dench tools show | edit TOOLS.md notes section
|
|
546
|
+
dench mem show | append "<text>" | set MEMORY.md aggregate
|
|
547
|
+
dench heartbeat status | enable | disable
|
|
548
|
+
| interval <30m|1h|…> | instructions
|
|
549
|
+
dench bootstrap status | complete | reopen | template
|
|
550
|
+
dench model default <id|clear>
|
|
551
|
+
dench model thread <threadId> <id|clear>
|
|
552
|
+
dench daily today | show <YYYY-MM-DD>
|
|
553
|
+
Help: dench identity help
|
|
554
|
+
|
|
498
555
|
External tools:
|
|
499
556
|
dench apps is an alias for dench tool status -- it lists connected apps.
|
|
500
557
|
dench tool with no subcommand prints tool help.
|
|
@@ -1781,10 +1838,12 @@ async function getRuntime() {
|
|
|
1781
1838
|
: undefined,
|
|
1782
1839
|
};
|
|
1783
1840
|
}
|
|
1841
|
+
|
|
1842
|
+
throw loginRequiredError();
|
|
1784
1843
|
}
|
|
1785
1844
|
|
|
1786
1845
|
if (!hasDevEnv()) {
|
|
1787
|
-
throw
|
|
1846
|
+
throw missingDevEnvError();
|
|
1788
1847
|
}
|
|
1789
1848
|
|
|
1790
1849
|
return {
|
|
@@ -1799,6 +1858,22 @@ async function getRuntime() {
|
|
|
1799
1858
|
|
|
1800
1859
|
type Runtime = Awaited<ReturnType<typeof getRuntime>>;
|
|
1801
1860
|
|
|
1861
|
+
function stripRuntimeFlags(args: string[]) {
|
|
1862
|
+
const booleanFlags = new Set(["--dev", "--prod", "--staging"]);
|
|
1863
|
+
const valueFlags = new Set(["--convex-url", "--host"]);
|
|
1864
|
+
const stripped: string[] = [];
|
|
1865
|
+
for (let i = 0; i < args.length; i++) {
|
|
1866
|
+
const arg = args[i];
|
|
1867
|
+
if (booleanFlags.has(arg)) continue;
|
|
1868
|
+
if (valueFlags.has(arg)) {
|
|
1869
|
+
i++;
|
|
1870
|
+
continue;
|
|
1871
|
+
}
|
|
1872
|
+
stripped.push(arg);
|
|
1873
|
+
}
|
|
1874
|
+
return stripped;
|
|
1875
|
+
}
|
|
1876
|
+
|
|
1802
1877
|
/**
|
|
1803
1878
|
* `session` mode is required for `agentWorkspace.*` calls because they
|
|
1804
1879
|
* gate on `validateAgentSession` server-side, which only accepts the
|
|
@@ -1837,6 +1912,13 @@ function requireAuthenticatedRuntime(
|
|
|
1837
1912
|
throw loginRequiredError(command);
|
|
1838
1913
|
}
|
|
1839
1914
|
|
|
1915
|
+
function encodeDevCrmSessionToken(args: {
|
|
1916
|
+
workspaceSlug: string;
|
|
1917
|
+
devKey: string;
|
|
1918
|
+
}) {
|
|
1919
|
+
return `dch_dev_${encodeURIComponent(args.workspaceSlug)}:${encodeURIComponent(args.devKey)}`;
|
|
1920
|
+
}
|
|
1921
|
+
|
|
1840
1922
|
function agentSessionRequiredError(command: string) {
|
|
1841
1923
|
return new CliError(
|
|
1842
1924
|
`${command} requires an agent session — DENCH_API_KEY is org-scoped and cannot impersonate a specific agent`,
|
|
@@ -3458,7 +3540,7 @@ async function main() {
|
|
|
3458
3540
|
}
|
|
3459
3541
|
|
|
3460
3542
|
if (command === "crm") {
|
|
3461
|
-
const subArgs = args.slice(args.indexOf("crm") + 1);
|
|
3543
|
+
const subArgs = stripRuntimeFlags(args.slice(args.indexOf("crm") + 1));
|
|
3462
3544
|
const sub = subArgs[0];
|
|
3463
3545
|
// Short-circuit help so users don't need a session to read the
|
|
3464
3546
|
// surface. The crm dispatcher's own help resolves --json + the
|
|
@@ -3478,6 +3560,14 @@ async function main() {
|
|
|
3478
3560
|
// (`DENCH_API_KEY` baked into sandbox envVars). Server-side
|
|
3479
3561
|
// `requireCrmAccess` (convex/lib/crm/access.ts) dispatches on the
|
|
3480
3562
|
// bearer prefix and resolves the org for both.
|
|
3563
|
+
if (runtime.mode === "dev") {
|
|
3564
|
+
await runCrmCommand({
|
|
3565
|
+
convex: runtime.client,
|
|
3566
|
+
args: subArgs,
|
|
3567
|
+
sessionToken: encodeDevCrmSessionToken(runtime.workspaceArgs),
|
|
3568
|
+
});
|
|
3569
|
+
return;
|
|
3570
|
+
}
|
|
3481
3571
|
const authedRuntime = requireAuthenticatedRuntime(runtime, "dench crm");
|
|
3482
3572
|
await runCrmCommand({
|
|
3483
3573
|
convex: authedRuntime.client,
|
|
@@ -3534,12 +3624,13 @@ async function main() {
|
|
|
3534
3624
|
await runChatCommand({ convex: {} as any, args: subArgs });
|
|
3535
3625
|
return;
|
|
3536
3626
|
}
|
|
3537
|
-
// `chat new` / `chat send`
|
|
3538
|
-
// /api/chat/cli
|
|
3539
|
-
//
|
|
3540
|
-
//
|
|
3541
|
-
// queries and keep going
|
|
3542
|
-
|
|
3627
|
+
// `chat new` / `chat send` / `chat follow` use HTTP routes
|
|
3628
|
+
// (/api/chat/cli for spawn, /api/chat/[runId]/stream and
|
|
3629
|
+
// /api/chat/stream for follow) so they need an authenticated host
|
|
3630
|
+
// + bearer token, not a Convex client. The remaining subcommands
|
|
3631
|
+
// (list / search / read / tree) are Convex queries and keep going
|
|
3632
|
+
// through `runChatCommand`.
|
|
3633
|
+
if (sub === "new" || sub === "send" || sub === "follow") {
|
|
3543
3634
|
const { runChatSpawnCommand } = await import("./chat-spawn");
|
|
3544
3635
|
const runtime = await getRuntime();
|
|
3545
3636
|
const authedRuntime = requireAuthenticatedRuntime(
|
|
@@ -3583,11 +3674,12 @@ async function main() {
|
|
|
3583
3674
|
await runAgentCommand({ convex: {} as any, args: subArgs });
|
|
3584
3675
|
return;
|
|
3585
3676
|
}
|
|
3586
|
-
// Aliases: `dench agent new` / `dench agent send`
|
|
3587
|
-
// implementation. Surfaced under
|
|
3588
|
-
//
|
|
3589
|
-
//
|
|
3590
|
-
|
|
3677
|
+
// Aliases: `dench agent new` / `dench agent send` / `dench agent
|
|
3678
|
+
// follow` -> the chat-spawn implementation. Surfaced under
|
|
3679
|
+
// `agent` so users who associate chat threads with the agent
|
|
3680
|
+
// surface still find them; the actual workflow is identical to
|
|
3681
|
+
// `dench chat new` / `dench chat send` / `dench chat follow`.
|
|
3682
|
+
if (sub === "new" || sub === "send" || sub === "follow") {
|
|
3591
3683
|
const { runChatSpawnCommand } = await import("./chat-spawn");
|
|
3592
3684
|
const runtime = await getRuntime();
|
|
3593
3685
|
const authedRuntime = requireAuthenticatedRuntime(
|
|
@@ -3609,12 +3701,17 @@ async function main() {
|
|
|
3609
3701
|
await runAgentCommand({
|
|
3610
3702
|
convex: runtime.client,
|
|
3611
3703
|
args: subArgs,
|
|
3704
|
+
sessionToken: runtime.sessionToken,
|
|
3705
|
+
callerRunId: process.env.DENCH_RUN_ID?.trim() || undefined,
|
|
3612
3706
|
});
|
|
3613
3707
|
return;
|
|
3614
3708
|
}
|
|
3615
3709
|
|
|
3616
|
-
|
|
3617
|
-
|
|
3710
|
+
// `dench routine` / `dench routines` are UI-friendly aliases for
|
|
3711
|
+
// `dench cron`. Same dispatch, same auth, same handler — only the
|
|
3712
|
+
// top-level command name differs to match the workspace UI rename.
|
|
3713
|
+
if (command === "cron" || command === "routine" || command === "routines") {
|
|
3714
|
+
const subArgs = args.slice(args.indexOf(command) + 1);
|
|
3618
3715
|
const sub = subArgs[0];
|
|
3619
3716
|
// Help is a no-auth path so users can read the surface without a
|
|
3620
3717
|
// session — mirrors `dench crm help`.
|
|
@@ -3633,7 +3730,10 @@ async function main() {
|
|
|
3633
3730
|
// bearer prefix and resolves the org for both. The Bearer token
|
|
3634
3731
|
// must carry `crons:read` (queries) or `crons:write` (mutations)
|
|
3635
3732
|
// scope, or the wildcard `*`.
|
|
3636
|
-
const authedRuntime = requireAuthenticatedRuntime(
|
|
3733
|
+
const authedRuntime = requireAuthenticatedRuntime(
|
|
3734
|
+
runtime,
|
|
3735
|
+
`dench ${command}`,
|
|
3736
|
+
);
|
|
3637
3737
|
await runCronCommand({
|
|
3638
3738
|
convex: authedRuntime.client,
|
|
3639
3739
|
args: subArgs,
|
|
@@ -3642,6 +3742,85 @@ async function main() {
|
|
|
3642
3742
|
return;
|
|
3643
3743
|
}
|
|
3644
3744
|
|
|
3745
|
+
// ── OpenClaw-parity self-update CLI surface ──────────────────────────
|
|
3746
|
+
//
|
|
3747
|
+
// Dispatch all `dench identity / organisation / user / tools / mem /
|
|
3748
|
+
// heartbeat / bootstrap / model / daily` traffic through the shared
|
|
3749
|
+
// agent-config CLI module. Each command requires the same auth shape
|
|
3750
|
+
// as `dench cron` (Bearer DENCH_API_KEY or `dench login` session
|
|
3751
|
+
// token) so server-side `requireCronAccess` accepts both.
|
|
3752
|
+
const AGENT_CONFIG_COMMANDS = new Set([
|
|
3753
|
+
"identity",
|
|
3754
|
+
"organisation",
|
|
3755
|
+
"user",
|
|
3756
|
+
"tools",
|
|
3757
|
+
"mem",
|
|
3758
|
+
"heartbeat",
|
|
3759
|
+
"bootstrap",
|
|
3760
|
+
"model",
|
|
3761
|
+
"daily",
|
|
3762
|
+
]);
|
|
3763
|
+
if (AGENT_CONFIG_COMMANDS.has(command)) {
|
|
3764
|
+
const subArgs = args.slice(args.indexOf(command) + 1);
|
|
3765
|
+
if (
|
|
3766
|
+
subArgs[0] === "help" ||
|
|
3767
|
+
subArgs.includes("--help") ||
|
|
3768
|
+
subArgs.includes("-h")
|
|
3769
|
+
) {
|
|
3770
|
+
const { printAgentConfigCliHelp } = await import("./agent-config");
|
|
3771
|
+
printAgentConfigCliHelp();
|
|
3772
|
+
return;
|
|
3773
|
+
}
|
|
3774
|
+
const runtime = await getRuntime();
|
|
3775
|
+
const authedRuntime = requireAuthenticatedRuntime(
|
|
3776
|
+
runtime,
|
|
3777
|
+
`dench ${command}`,
|
|
3778
|
+
);
|
|
3779
|
+
const ctxBase = {
|
|
3780
|
+
convex: authedRuntime.client,
|
|
3781
|
+
args: subArgs,
|
|
3782
|
+
jsonOutput: json,
|
|
3783
|
+
sessionToken: authedRuntime.sessionToken,
|
|
3784
|
+
};
|
|
3785
|
+
const mod = await import("./agent-config");
|
|
3786
|
+
if (command === "identity") {
|
|
3787
|
+
await mod.runIdentityCommand(ctxBase);
|
|
3788
|
+
return;
|
|
3789
|
+
}
|
|
3790
|
+
if (command === "organisation") {
|
|
3791
|
+
await mod.runOrganisationCommand(ctxBase);
|
|
3792
|
+
return;
|
|
3793
|
+
}
|
|
3794
|
+
if (command === "user") {
|
|
3795
|
+
await mod.runUserCommand(ctxBase);
|
|
3796
|
+
return;
|
|
3797
|
+
}
|
|
3798
|
+
if (command === "tools") {
|
|
3799
|
+
await mod.runToolsCommand(ctxBase);
|
|
3800
|
+
return;
|
|
3801
|
+
}
|
|
3802
|
+
if (command === "mem") {
|
|
3803
|
+
await mod.runMemoryAggregateCommand(ctxBase);
|
|
3804
|
+
return;
|
|
3805
|
+
}
|
|
3806
|
+
if (command === "heartbeat") {
|
|
3807
|
+
await mod.runHeartbeatCommand(ctxBase);
|
|
3808
|
+
return;
|
|
3809
|
+
}
|
|
3810
|
+
if (command === "bootstrap") {
|
|
3811
|
+
await mod.runBootstrapCommand(ctxBase);
|
|
3812
|
+
return;
|
|
3813
|
+
}
|
|
3814
|
+
if (command === "model") {
|
|
3815
|
+
await mod.runModelCommand(ctxBase);
|
|
3816
|
+
return;
|
|
3817
|
+
}
|
|
3818
|
+
if (command === "daily") {
|
|
3819
|
+
await mod.runDailyCommand(ctxBase);
|
|
3820
|
+
return;
|
|
3821
|
+
}
|
|
3822
|
+
}
|
|
3823
|
+
|
|
3645
3824
|
if (command === "login") {
|
|
3646
3825
|
await login();
|
|
3647
3826
|
return;
|
|
@@ -4237,11 +4416,12 @@ async function main() {
|
|
|
4237
4416
|
}
|
|
4238
4417
|
|
|
4239
4418
|
main().catch((error) => {
|
|
4240
|
-
const
|
|
4419
|
+
const normalized = normalizeCliError(error);
|
|
4420
|
+
const extra = normalized.payload;
|
|
4241
4421
|
const payload: JsonRecord = {
|
|
4242
4422
|
ok: false,
|
|
4243
4423
|
...extra,
|
|
4244
|
-
error:
|
|
4424
|
+
error: normalized.message,
|
|
4245
4425
|
};
|
|
4246
4426
|
if (json) {
|
|
4247
4427
|
console.error(JSON.stringify(payload, null, 2));
|