@dench.com/cli 0.3.8 → 0.4.1
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/chat.ts +215 -8
- package/cron.ts +8 -4
- package/dench.ts +166 -503
- package/package.json +2 -1
- package/tools.ts +808 -0
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,6 +127,19 @@ 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]
|
|
@@ -163,6 +195,11 @@ export async function runChatCommand(opts: {
|
|
|
163
195
|
return await runChatReadCommand(ctx);
|
|
164
196
|
case "tree":
|
|
165
197
|
return await runChatTreeCommand(ctx);
|
|
198
|
+
case "rename":
|
|
199
|
+
return await runChatRenameCommand(ctx);
|
|
200
|
+
case "delete":
|
|
201
|
+
case "rm":
|
|
202
|
+
return await runChatDeleteCommand(ctx);
|
|
166
203
|
default:
|
|
167
204
|
throw new ChatCliError(`Unknown chat subcommand: ${subcommand}`);
|
|
168
205
|
}
|
|
@@ -222,6 +259,176 @@ async function runChatReadCommand(ctx: ChatCliContext): Promise<void> {
|
|
|
222
259
|
);
|
|
223
260
|
}
|
|
224
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
|
+
|
|
225
432
|
/**
|
|
226
433
|
* `dench chat tree [<root-thread-id>] [--limit N] [--json]` — print the
|
|
227
434
|
* parent-child chat thread hierarchy rooted at the given thread (or
|
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]
|