@dench.com/cli 0.3.8 → 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.
Files changed (4) hide show
  1. package/chat.ts +215 -8
  2. package/cron.ts +8 -4
  3. package/dench.ts +106 -27
  4. package/package.json +1 -1
package/chat.ts CHANGED
@@ -1,11 +1,12 @@
1
1
  /**
2
- * `dench chat <subcommand>` — CLI surface for browsing past chat threads
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`) which themselves enforce the
8
- * "own + shared in this org" visibility rule.
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 [--query <substr>] [--limit N] [--include-archived]
20
- * chat search <query> [--limit N] [--include-current]
21
- * chat read <thread-id> [--limit N]
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. The Vercel cron dispatcher (`/api/cron/dispatch`,
9
- * scheduled via `vercel.json` at every minute) walks the
10
- * `by_enabled_nextFireAt` index, claims due rows, and fires the
11
- * workflow.
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
@@ -492,6 +492,10 @@ Past chat threads (own + shared in this workspace):
492
492
  dench chat list [--query <substr>] [--limit N] [--include-archived] [--json]
493
493
  dench chat search "<query>" [--limit N] [--include-current] [--json]
494
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)
495
499
 
496
500
  Spawn a new chat-turn workflow (same as the web UI):
497
501
  dench chat new "<prompt>" [--model <m>] [--file-context <path>] [--visibility private|shared] [--title T] [--yolo] [--follow] [--json]
@@ -534,6 +538,20 @@ Subagents (Daytona-native rewrite):
534
538
 
535
539
  dench --version
536
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
+
537
555
  External tools:
538
556
  dench apps is an alias for dench tool status -- it lists connected apps.
539
557
  dench tool with no subcommand prints tool help.
@@ -1794,30 +1812,6 @@ async function getRuntime() {
1794
1812
  });
1795
1813
  }
1796
1814
 
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
-
1821
1815
  // Sandbox / automation path: the unified Dench API key + Convex URL
1822
1816
  // baked into sandbox envVars (see
1823
1817
  // src/lib/secrets/sandbox-tokens.ts) is enough to drive every
@@ -3713,8 +3707,11 @@ async function main() {
3713
3707
  return;
3714
3708
  }
3715
3709
 
3716
- if (command === "cron") {
3717
- const subArgs = args.slice(args.indexOf("cron") + 1);
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);
3718
3715
  const sub = subArgs[0];
3719
3716
  // Help is a no-auth path so users can read the surface without a
3720
3717
  // session — mirrors `dench crm help`.
@@ -3733,7 +3730,10 @@ async function main() {
3733
3730
  // bearer prefix and resolves the org for both. The Bearer token
3734
3731
  // must carry `crons:read` (queries) or `crons:write` (mutations)
3735
3732
  // scope, or the wildcard `*`.
3736
- const authedRuntime = requireAuthenticatedRuntime(runtime, "dench cron");
3733
+ const authedRuntime = requireAuthenticatedRuntime(
3734
+ runtime,
3735
+ `dench ${command}`,
3736
+ );
3737
3737
  await runCronCommand({
3738
3738
  convex: authedRuntime.client,
3739
3739
  args: subArgs,
@@ -3742,6 +3742,85 @@ async function main() {
3742
3742
  return;
3743
3743
  }
3744
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
+
3745
3824
  if (command === "login") {
3746
3825
  await login();
3747
3826
  return;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dench.com/cli",
3
- "version": "0.3.8",
3
+ "version": "0.4.0",
4
4
  "description": "Dench agent workspace CLI.",
5
5
  "type": "module",
6
6
  "bin": {