@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 +1 -1
- package/agent-config.ts +645 -0
- package/crm.ts +74 -8
- package/dench.ts +662 -171
- package/host.ts +3 -3
- package/lib/enrichment-gateway.ts +1080 -42
- package/members.ts +192 -0
- package/package.json +4 -2
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.
|
|
26
|
+
The default backend is `https://dench.com` (production). Use `dench backend`
|
|
27
27
|
to switch:
|
|
28
28
|
|
|
29
29
|
```bash
|
package/agent-config.ts
ADDED
|
@@ -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 };
|