@dench.com/cli 0.4.8 → 2.0.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 CHANGED
@@ -23,7 +23,7 @@ dench login --name "AI Agent - Billing Repo"
23
23
  to lowercase snake_case before send (e.g. `--kind "Claude Code"` becomes
24
24
  `claude_code`).
25
25
 
26
- The default backend is `https://dench.dev` (production). Use `dench backend`
26
+ The default backend is `https://dench.com` (production). Use `dench backend`
27
27
  to switch:
28
28
 
29
29
  ```bash
@@ -0,0 +1,645 @@
1
+ /**
2
+ * `dench identity / organisation / user / heartbeat / bootstrap /
3
+ * model / daily / mem aggregate` — CLI surface for the OpenClaw-
4
+ * parity self-updating harness.
5
+ *
6
+ * Mirrors the auth pattern in `cli/cron.ts`:
7
+ * • Convex calls go through the shared `ConvexHttpClient`.
8
+ * • Auth resolves via `sessionToken` — accepts either a
9
+ * `dch_agent_*` agent session token (from `dench login`) or a
10
+ * unified Dench API key (`DENCH_API_KEY` baked into sandbox
11
+ * envVars). Server-side `requireCronAccess` dispatches on
12
+ * bearer prefix.
13
+ *
14
+ * Subcommands:
15
+ * identity show | edit
16
+ * organisation show | edit
17
+ * user show | edit
18
+ * tools show | notes-edit
19
+ * memory show | append <text> | set (MEMORY.md aggregate)
20
+ * heartbeat status | enable | disable | interval <duration> |
21
+ * instructions
22
+ * bootstrap status | complete | reopen | template
23
+ * model default <id> | model default clear |
24
+ * model thread <threadId> <id> | model thread <threadId> clear
25
+ * daily today | daily show <YYYY-MM-DD> | daily append <text>
26
+ *
27
+ * `edit` / `notes-edit` / `instructions` / `template` open the
28
+ * user's `$EDITOR` (default `vi`) to capture the new body. The
29
+ * stdin path is also supported via `--stdin`, useful in scripts.
30
+ */
31
+
32
+ import { execSync } from "node:child_process";
33
+ import { mkdirSync, readFileSync, unlinkSync, writeFileSync } from "node:fs";
34
+ import { tmpdir } from "node:os";
35
+ import { join } from "node:path";
36
+ import type { ConvexHttpClient } from "convex/browser";
37
+ import { api } from "@/../convex/_generated/api";
38
+ import { CliArgError, getFlag, hasFlag } from "./lib/cli-args";
39
+
40
+ class AgentConfigCliError extends Error {}
41
+
42
+ type CliCtx = {
43
+ convex: ConvexHttpClient;
44
+ args: string[];
45
+ jsonOutput: boolean;
46
+ sessionToken?: string;
47
+ };
48
+
49
+ function out(ctx: CliCtx, value: unknown): void {
50
+ if (ctx.jsonOutput) {
51
+ console.log(JSON.stringify(value, null, 2));
52
+ return;
53
+ }
54
+ if (typeof value === "string") {
55
+ console.log(value);
56
+ return;
57
+ }
58
+ console.log(JSON.stringify(value, null, 2));
59
+ }
60
+
61
+ async function runQuery(
62
+ ctx: CliCtx,
63
+ fn: Parameters<ConvexHttpClient["query"]>[0],
64
+ args: Record<string, unknown> = {},
65
+ ): Promise<unknown> {
66
+ return ctx.convex.query(fn, {
67
+ ...args,
68
+ ...(ctx.sessionToken ? { sessionToken: ctx.sessionToken } : {}),
69
+ } as never);
70
+ }
71
+
72
+ async function runMutation(
73
+ ctx: CliCtx,
74
+ fn: Parameters<ConvexHttpClient["mutation"]>[0],
75
+ args: Record<string, unknown> = {},
76
+ ): Promise<unknown> {
77
+ return ctx.convex.mutation(fn, {
78
+ ...args,
79
+ ...(ctx.sessionToken ? { sessionToken: ctx.sessionToken } : {}),
80
+ } as never);
81
+ }
82
+
83
+ // ── Editor helpers ────────────────────────────────────────────────────────
84
+
85
+ async function readContentFromStdin(): Promise<string> {
86
+ return new Promise((resolve, reject) => {
87
+ const chunks: Buffer[] = [];
88
+ process.stdin.on("data", (chunk) => chunks.push(Buffer.from(chunk)));
89
+ process.stdin.on("end", () => resolve(Buffer.concat(chunks).toString("utf8")));
90
+ process.stdin.on("error", reject);
91
+ });
92
+ }
93
+
94
+ function openEditor(initial: string, suffix: string): string {
95
+ const editor = process.env.EDITOR?.trim() || "vi";
96
+ const dir = join(tmpdir(), "dench-agent-cli");
97
+ mkdirSync(dir, { recursive: true });
98
+ const file = join(dir, `${Date.now()}-${suffix}.md`);
99
+ writeFileSync(file, initial, "utf8");
100
+ try {
101
+ execSync(`${editor} ${file}`, { stdio: "inherit" });
102
+ return readFileSync(file, "utf8");
103
+ } finally {
104
+ try {
105
+ unlinkSync(file);
106
+ } catch {
107
+ // best-effort cleanup
108
+ }
109
+ }
110
+ }
111
+
112
+ async function resolveContent(args: string[]): Promise<string> {
113
+ if (hasFlag(args, "--stdin")) {
114
+ const text = await readContentFromStdin();
115
+ if (!text.trim().length) {
116
+ throw new AgentConfigCliError("--stdin received empty input");
117
+ }
118
+ return text;
119
+ }
120
+ const inline = getFlag(args, "--content");
121
+ if (inline) return inline;
122
+ // Otherwise launch the editor with the existing content as the
123
+ // starting point. Caller is expected to call this AFTER fetching
124
+ // the current value.
125
+ throw new AgentConfigCliError(
126
+ "No content provided. Use --content '<markdown>' or --stdin or run without flags to launch $EDITOR.",
127
+ );
128
+ }
129
+
130
+ // ── Loader (one round-trip for all reads + display) ──────────────────────
131
+
132
+ async function loadAgentConfig(ctx: CliCtx): Promise<{
133
+ agentConfig: {
134
+ identity: { content: string; updatedAt: number } | null;
135
+ organisation: { content: string; updatedAt: number } | null;
136
+ memoryAggregate: { content: string; updatedAt: number } | null;
137
+ toolsNotes: { content: string; updatedAt: number } | null;
138
+ bootstrap: {
139
+ completed: boolean;
140
+ completedAt?: number;
141
+ template?: string;
142
+ };
143
+ heartbeat: {
144
+ enabled: boolean;
145
+ intervalMs: number;
146
+ instructions: string;
147
+ lastFiredAt?: number;
148
+ };
149
+ };
150
+ userProfile: { content: string; updatedAt: number } | null;
151
+ organizationId: string;
152
+ }> {
153
+ const result = (await runQuery(ctx, api.functions.agentConfig.getAgentConfig)) as {
154
+ agentConfig: {
155
+ identity: { content: string; updatedAt: number } | null;
156
+ organisation: { content: string; updatedAt: number } | null;
157
+ memoryAggregate: { content: string; updatedAt: number } | null;
158
+ toolsNotes: { content: string; updatedAt: number } | null;
159
+ bootstrap: {
160
+ completed: boolean;
161
+ completedAt?: number;
162
+ template?: string;
163
+ };
164
+ heartbeat: {
165
+ enabled: boolean;
166
+ intervalMs: number;
167
+ instructions: string;
168
+ lastFiredAt?: number;
169
+ };
170
+ };
171
+ userProfile: { content: string; updatedAt: number } | null;
172
+ organizationId: string;
173
+ };
174
+ return result;
175
+ }
176
+
177
+ // ── Per-section handlers ─────────────────────────────────────────────────
178
+
179
+ async function handleSimpleMagicFile(
180
+ ctx: CliCtx,
181
+ args: string[],
182
+ opts: {
183
+ label: "identity" | "organisation" | "tools" | "user" | "memory";
184
+ field: "identity" | "organisation" | "toolsNotes" | "memoryAggregate";
185
+ perMember?: boolean;
186
+ },
187
+ ): Promise<void> {
188
+ const sub = args[0];
189
+ if (!sub || sub === "show") {
190
+ const config = await loadAgentConfig(ctx);
191
+ const value =
192
+ opts.field === "memoryAggregate"
193
+ ? config.agentConfig.memoryAggregate
194
+ : opts.field === "toolsNotes"
195
+ ? config.agentConfig.toolsNotes
196
+ : opts.field === "identity"
197
+ ? config.agentConfig.identity
198
+ : opts.field === "organisation"
199
+ ? config.agentConfig.organisation
200
+ : null;
201
+ if (opts.perMember) {
202
+ out(ctx, config.userProfile?.content ?? "(empty)");
203
+ return;
204
+ }
205
+ out(ctx, value?.content ?? "(empty)");
206
+ return;
207
+ }
208
+ if (sub === "edit" || sub === "set") {
209
+ let content: string;
210
+ if (hasFlag(args, "--stdin") || getFlag(args, "--content")) {
211
+ content = await resolveContent(args);
212
+ } else {
213
+ const config = await loadAgentConfig(ctx);
214
+ const initial = opts.perMember
215
+ ? config.userProfile?.content ?? ""
216
+ : opts.field === "memoryAggregate"
217
+ ? config.agentConfig.memoryAggregate?.content ?? ""
218
+ : opts.field === "toolsNotes"
219
+ ? config.agentConfig.toolsNotes?.content ?? ""
220
+ : opts.field === "identity"
221
+ ? config.agentConfig.identity?.content ?? ""
222
+ : config.agentConfig.organisation?.content ?? "";
223
+ content = openEditor(initial, opts.label);
224
+ }
225
+ if (!content.trim().length) {
226
+ throw new AgentConfigCliError(`${opts.label} content cannot be empty`);
227
+ }
228
+ const fn = (() => {
229
+ switch (opts.field) {
230
+ case "identity":
231
+ return api.functions.agentConfig.updateIdentity;
232
+ case "organisation":
233
+ return api.functions.agentConfig.updateOrganisation;
234
+ case "toolsNotes":
235
+ return api.functions.agentConfig.updateToolsNotes;
236
+ case "memoryAggregate":
237
+ return api.functions.agentConfig.updateMemory;
238
+ }
239
+ })();
240
+ if (opts.perMember) {
241
+ await runMutation(ctx, api.functions.agentConfig.updateUserProfile, {
242
+ content,
243
+ });
244
+ out(ctx, { ok: true, kind: "user_profile" });
245
+ return;
246
+ }
247
+ if (opts.field === "memoryAggregate") {
248
+ await runMutation(ctx, api.functions.agentConfig.updateMemory, {
249
+ content,
250
+ append: hasFlag(args, "--append"),
251
+ });
252
+ out(ctx, { ok: true, kind: "memory" });
253
+ return;
254
+ }
255
+ await runMutation(ctx, fn, { content });
256
+ out(ctx, { ok: true, kind: opts.label });
257
+ return;
258
+ }
259
+ throw new AgentConfigCliError(
260
+ `Unknown ${opts.label} subcommand: "${sub}". Try "show" or "edit".`,
261
+ );
262
+ }
263
+
264
+ function formatInterval(ms: number): string {
265
+ if (ms < 60_000) return `${Math.round(ms / 1000)}s`;
266
+ if (ms < 60 * 60_000) return `${Math.round(ms / 60_000)}m`;
267
+ if (ms < 24 * 60 * 60_000) {
268
+ const hours = ms / (60 * 60_000);
269
+ return Number.isInteger(hours) ? `${hours}h` : `${hours.toFixed(1)}h`;
270
+ }
271
+ return `${(ms / (24 * 60 * 60_000)).toFixed(1)}d`;
272
+ }
273
+
274
+ const DURATION_RE = /^(\d+)\s*(ms|s|m|h|d)$/i;
275
+ function parseDurationFlagToMs(input: string): number | null {
276
+ const m = DURATION_RE.exec(input.trim());
277
+ if (!m) return null;
278
+ const n = parseInt(m[1], 10);
279
+ if (!Number.isFinite(n) || n <= 0) return null;
280
+ switch (m[2].toLowerCase()) {
281
+ case "ms":
282
+ return n;
283
+ case "s":
284
+ return n * 1000;
285
+ case "m":
286
+ return n * 60_000;
287
+ case "h":
288
+ return n * 3_600_000;
289
+ case "d":
290
+ return n * 86_400_000;
291
+ }
292
+ return null;
293
+ }
294
+
295
+ async function handleHeartbeat(ctx: CliCtx, args: string[]): Promise<void> {
296
+ const sub = args[0];
297
+ if (!sub || sub === "status" || sub === "show") {
298
+ const config = await loadAgentConfig(ctx);
299
+ const hb = config.agentConfig.heartbeat;
300
+ if (ctx.jsonOutput) {
301
+ out(ctx, hb);
302
+ return;
303
+ }
304
+ out(
305
+ ctx,
306
+ `enabled=${hb.enabled} interval=${formatInterval(hb.intervalMs)}\n\n${hb.instructions}`,
307
+ );
308
+ return;
309
+ }
310
+ if (sub === "enable") {
311
+ await runMutation(ctx, api.functions.agentConfig.updateHeartbeat, {
312
+ enabled: true,
313
+ });
314
+ out(ctx, { ok: true, enabled: true });
315
+ return;
316
+ }
317
+ if (sub === "disable") {
318
+ await runMutation(ctx, api.functions.agentConfig.updateHeartbeat, {
319
+ enabled: false,
320
+ });
321
+ out(ctx, { ok: true, enabled: false });
322
+ return;
323
+ }
324
+ if (sub === "interval") {
325
+ const raw = args[1];
326
+ if (!raw) {
327
+ throw new AgentConfigCliError(
328
+ "Usage: dench heartbeat interval <duration> (e.g. 30m, 2h, 1d)",
329
+ );
330
+ }
331
+ const ms = parseDurationFlagToMs(raw);
332
+ if (!ms) throw new AgentConfigCliError(`Invalid duration: ${raw}`);
333
+ await runMutation(ctx, api.functions.agentConfig.updateHeartbeat, {
334
+ intervalMs: ms,
335
+ });
336
+ out(ctx, { ok: true, intervalMs: ms, label: formatInterval(ms) });
337
+ return;
338
+ }
339
+ if (sub === "instructions" || sub === "edit") {
340
+ let content: string;
341
+ if (hasFlag(args, "--stdin") || getFlag(args, "--content")) {
342
+ content = await resolveContent(args);
343
+ } else {
344
+ const config = await loadAgentConfig(ctx);
345
+ content = openEditor(
346
+ config.agentConfig.heartbeat.instructions,
347
+ "heartbeat",
348
+ );
349
+ }
350
+ if (!content.trim().length) {
351
+ throw new AgentConfigCliError("heartbeat instructions cannot be empty");
352
+ }
353
+ await runMutation(ctx, api.functions.agentConfig.updateHeartbeat, {
354
+ instructions: content,
355
+ });
356
+ out(ctx, { ok: true, kind: "heartbeat" });
357
+ return;
358
+ }
359
+ throw new AgentConfigCliError(
360
+ `Unknown heartbeat subcommand: "${sub}". Try "status", "enable", "disable", "interval <duration>", "instructions".`,
361
+ );
362
+ }
363
+
364
+ async function handleBootstrap(ctx: CliCtx, args: string[]): Promise<void> {
365
+ const sub = args[0];
366
+ if (!sub || sub === "status" || sub === "show") {
367
+ const config = await loadAgentConfig(ctx);
368
+ out(
369
+ ctx,
370
+ ctx.jsonOutput
371
+ ? config.agentConfig.bootstrap
372
+ : `completed=${config.agentConfig.bootstrap.completed}${
373
+ config.agentConfig.bootstrap.completedAt
374
+ ? ` at ${new Date(config.agentConfig.bootstrap.completedAt).toISOString()}`
375
+ : ""
376
+ }`,
377
+ );
378
+ return;
379
+ }
380
+ if (sub === "complete") {
381
+ await runMutation(ctx, api.functions.agentConfig.completeBootstrap, {
382
+ completed: true,
383
+ });
384
+ out(ctx, { ok: true, completed: true });
385
+ return;
386
+ }
387
+ if (sub === "reopen") {
388
+ await runMutation(ctx, api.functions.agentConfig.completeBootstrap, {
389
+ completed: false,
390
+ });
391
+ out(ctx, { ok: true, completed: false });
392
+ return;
393
+ }
394
+ if (sub === "template") {
395
+ let content: string;
396
+ if (hasFlag(args, "--stdin") || getFlag(args, "--content")) {
397
+ content = await resolveContent(args);
398
+ } else {
399
+ const config = await loadAgentConfig(ctx);
400
+ content = openEditor(
401
+ config.agentConfig.bootstrap.template ?? "",
402
+ "bootstrap",
403
+ );
404
+ }
405
+ if (!content.trim().length) {
406
+ throw new AgentConfigCliError("bootstrap template cannot be empty");
407
+ }
408
+ await runMutation(ctx, api.functions.agentConfig.updateBootstrapTemplate, {
409
+ content,
410
+ });
411
+ out(ctx, { ok: true, kind: "bootstrap_template" });
412
+ return;
413
+ }
414
+ throw new AgentConfigCliError(
415
+ `Unknown bootstrap subcommand: "${sub}". Try "status", "complete", "reopen", "template".`,
416
+ );
417
+ }
418
+
419
+ async function handleModel(ctx: CliCtx, args: string[]): Promise<void> {
420
+ const sub = args[0];
421
+ if (sub === "default") {
422
+ const arg = args[1];
423
+ if (!arg || arg === "show") {
424
+ const config = await loadAgentConfig(ctx);
425
+ // org default lives outside agentConfig — use the same query
426
+ // but pull from a separate getMyOrganization call. For now we
427
+ // surface "(use the dashboard for the current value)".
428
+ out(
429
+ ctx,
430
+ ctx.jsonOutput
431
+ ? { organizationId: config.organizationId }
432
+ : "Use `dench identity show` and your settings page to view the active default; this command sets the value.",
433
+ );
434
+ return;
435
+ }
436
+ if (arg === "clear") {
437
+ await runMutation(ctx, api.functions.agentConfig.setDefaultChatModelV2, {
438
+ modelId: null,
439
+ });
440
+ out(ctx, { ok: true, defaultChatModelId: null });
441
+ return;
442
+ }
443
+ await runMutation(ctx, api.functions.agentConfig.setDefaultChatModelV2, {
444
+ modelId: arg,
445
+ });
446
+ out(ctx, { ok: true, defaultChatModelId: arg });
447
+ return;
448
+ }
449
+ if (sub === "thread") {
450
+ const threadId = args[1];
451
+ const target = args[2];
452
+ if (!threadId || !target) {
453
+ throw new AgentConfigCliError(
454
+ "Usage: dench model thread <threadId> <modelId|clear>",
455
+ );
456
+ }
457
+ if (target === "clear") {
458
+ await runMutation(ctx, api.functions.agentConfig.setThreadModelOverride, {
459
+ threadId: threadId as never,
460
+ modelId: null,
461
+ });
462
+ out(ctx, { ok: true, threadId, modelOverride: null });
463
+ return;
464
+ }
465
+ await runMutation(ctx, api.functions.agentConfig.setThreadModelOverride, {
466
+ threadId: threadId as never,
467
+ modelId: target,
468
+ });
469
+ out(ctx, { ok: true, threadId, modelOverride: target });
470
+ return;
471
+ }
472
+ throw new AgentConfigCliError(
473
+ `Unknown model subcommand: "${sub}". Try "default <id|clear>" or "thread <threadId> <id|clear>".`,
474
+ );
475
+ }
476
+
477
+ // ── Entry points consumed by cli/dench.ts ────────────────────────────────
478
+
479
+ export async function runIdentityCommand(ctx: CliCtx): Promise<void> {
480
+ await handleSimpleMagicFile(ctx, ctx.args, {
481
+ label: "identity",
482
+ field: "identity",
483
+ });
484
+ }
485
+
486
+ export async function runOrganisationCommand(ctx: CliCtx): Promise<void> {
487
+ await handleSimpleMagicFile(ctx, ctx.args, {
488
+ label: "organisation",
489
+ field: "organisation",
490
+ });
491
+ }
492
+
493
+ export async function runUserCommand(ctx: CliCtx): Promise<void> {
494
+ await handleSimpleMagicFile(ctx, ctx.args, {
495
+ label: "user",
496
+ field: "identity", // unused (perMember=true)
497
+ perMember: true,
498
+ });
499
+ }
500
+
501
+ export async function runToolsCommand(ctx: CliCtx): Promise<void> {
502
+ await handleSimpleMagicFile(ctx, ctx.args, {
503
+ label: "tools",
504
+ field: "toolsNotes",
505
+ });
506
+ }
507
+
508
+ /**
509
+ * Aggregate-MEMORY operations (separate from `dench memory
510
+ * search/save` which still hit `workspaceMemories`). Subcommands:
511
+ * show | append <text> | set
512
+ */
513
+ export async function runMemoryAggregateCommand(ctx: CliCtx): Promise<void> {
514
+ const sub = ctx.args[0];
515
+ if (!sub || sub === "show") {
516
+ const config = await loadAgentConfig(ctx);
517
+ out(ctx, config.agentConfig.memoryAggregate?.content ?? "(empty)");
518
+ return;
519
+ }
520
+ if (sub === "append") {
521
+ const text = ctx.args.slice(1).join(" ").trim();
522
+ if (!text)
523
+ throw new AgentConfigCliError("Usage: dench mem append <text>");
524
+ await runMutation(ctx, api.functions.agentConfig.updateMemory, {
525
+ content: text,
526
+ append: true,
527
+ });
528
+ out(ctx, { ok: true, kind: "memory_aggregate", appended: true });
529
+ return;
530
+ }
531
+ if (sub === "set" || sub === "edit") {
532
+ let content: string;
533
+ if (hasFlag(ctx.args, "--stdin") || getFlag(ctx.args, "--content")) {
534
+ content = await resolveContent(ctx.args);
535
+ } else {
536
+ const config = await loadAgentConfig(ctx);
537
+ content = openEditor(
538
+ config.agentConfig.memoryAggregate?.content ?? "",
539
+ "memory",
540
+ );
541
+ }
542
+ if (!content.trim().length) {
543
+ throw new AgentConfigCliError("memory cannot be empty");
544
+ }
545
+ await runMutation(ctx, api.functions.agentConfig.updateMemory, {
546
+ content,
547
+ append: false,
548
+ });
549
+ out(ctx, { ok: true, kind: "memory_aggregate" });
550
+ return;
551
+ }
552
+ throw new AgentConfigCliError(
553
+ `Unknown mem subcommand: "${sub}". Try "show", "append <text>", "set".`,
554
+ );
555
+ }
556
+
557
+ export async function runHeartbeatCommand(ctx: CliCtx): Promise<void> {
558
+ await handleHeartbeat(ctx, ctx.args);
559
+ }
560
+
561
+ export async function runBootstrapCommand(ctx: CliCtx): Promise<void> {
562
+ await handleBootstrap(ctx, ctx.args);
563
+ }
564
+
565
+ export async function runModelCommand(ctx: CliCtx): Promise<void> {
566
+ await handleModel(ctx, ctx.args);
567
+ }
568
+
569
+ /**
570
+ * Daily activity log subcommands (real fileTree files under
571
+ * `/workspace/daily/`). Today / show <YYYY-MM-DD> read the inline
572
+ * cache; append delegates to the agent action so timestamps and
573
+ * heading wrappers stay consistent.
574
+ */
575
+ export async function runDailyCommand(ctx: CliCtx): Promise<void> {
576
+ const sub = ctx.args[0];
577
+ if (!sub || sub === "today") {
578
+ const today = new Date();
579
+ const yyyy = today.getUTCFullYear();
580
+ const mm = String(today.getUTCMonth() + 1).padStart(2, "0");
581
+ const dd = String(today.getUTCDate()).padStart(2, "0");
582
+ const path = `/daily/${yyyy}-${mm}-${dd}.md`;
583
+ // Daily logs are real files — fetch via the workspace file API
584
+ // hop so the same auth (Convex Auth or API key) works.
585
+ out(
586
+ ctx,
587
+ ctx.jsonOutput
588
+ ? { path }
589
+ : `Today's daily log lives at ${path}. Use \`dench fs cat ${path}\` to read it, or open it in the workspace UI.`,
590
+ );
591
+ return;
592
+ }
593
+ if (sub === "show") {
594
+ const date = ctx.args[1];
595
+ if (!date) {
596
+ throw new AgentConfigCliError(
597
+ "Usage: dench daily show <YYYY-MM-DD>",
598
+ );
599
+ }
600
+ out(
601
+ ctx,
602
+ ctx.jsonOutput
603
+ ? { path: `/daily/${date}.md` }
604
+ : `Daily log for ${date} lives at /daily/${date}.md. Use \`dench fs cat\` to read it.`,
605
+ );
606
+ return;
607
+ }
608
+ if (sub === "append") {
609
+ throw new AgentConfigCliError(
610
+ "Daily append is currently only available via the agent's `append_daily_log` tool. Use the chat to log entries with timestamps.",
611
+ );
612
+ }
613
+ throw new AgentConfigCliError(
614
+ `Unknown daily subcommand: "${sub}". Try "today", "show <YYYY-MM-DD>".`,
615
+ );
616
+ }
617
+
618
+ export function printAgentConfigCliHelp(): void {
619
+ console.log(
620
+ `Self-updating agent harness:
621
+
622
+ dench identity show | edit IDENTITY.md (org-wide)
623
+ dench organisation show | edit ORGANISATION.md
624
+ dench user show | edit USER.md (per-member)
625
+ dench tools show | notes-edit TOOLS.md notes section
626
+ dench mem show | append <text> | set MEMORY.md aggregate
627
+ dench heartbeat status | enable | disable Heartbeat cron
628
+ | interval <30m|1h|…> | instructions
629
+ dench bootstrap status | complete | reopen | template
630
+ dench model default <id|clear> | model thread <threadId> <id|clear>
631
+ dench daily today | show <YYYY-MM-DD> Activity log paths
632
+
633
+ Editing flags:
634
+ --content "<markdown>" Inline body
635
+ --stdin Read body from stdin
636
+ (default) Launch $EDITOR ($EDITOR or vi)
637
+
638
+ Auth:
639
+ --json Machine-readable output
640
+ Bearer token via DENCH_API_KEY or \`dench login\` session.`,
641
+ );
642
+ }
643
+
644
+ // Re-export error so callers can branch on it
645
+ export { AgentConfigCliError, CliArgError };