@monster-orange/den 0.4.6 → 0.4.8
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/CHANGELOG.md +160 -0
- package/README.md +7 -7
- package/docs/distributions.md +244 -1
- package/package.json +1 -1
- package/schemas/den.plugin.v1.json +3 -2
- package/skills/install-den/SKILL.md +2 -2
- package/src/shared/agents/den-claude.ts +38 -5
- package/src/shared/agents/guardrails/hooks.json +27 -0
- package/src/shared/agents/guardrails/instructions.md +39 -0
- package/src/shared/agents/guardrails/pretooluse.mjs +430 -0
- package/src/shared/agents/guardrails/stop-hook.ts +66 -0
- package/src/shared/agents/guardrails.ts +114 -0
- package/src/shared/agents/handback.ts +241 -0
- package/src/shared/agents/personas.ts +66 -7
- package/src/shared/agents/records.ts +114 -0
- package/src/shared/agents/runtime.ts +12 -0
- package/src/shared/den-config.ts +97 -6
- package/src/shared/dev-metrics/emit.ts +31 -4
- package/src/shared/dev-metrics/polling.ts +135 -0
- package/src/shared/engine.ts +14 -2
- package/src/shared/plugins/manifest.ts +3 -0
- package/src/shared/usage/paths.ts +16 -0
- package/src/shared/usage/report.ts +283 -0
- package/src/shared/usage/sources.ts +127 -0
- package/src/shared/usage/transcript.ts +262 -0
- package/src/ui/cli/agent-ops.ts +495 -53
- package/src/ui/cli/agents-command.ts +170 -14
- package/src/ui/cli/doctor-command.ts +20 -0
- package/src/ui/cli/flag-parser.ts +1 -0
- package/src/ui/cli/index.ts +119 -2
- package/src/ui/cli/usage-command.ts +256 -0
- package/src/ui/cli/web-command.ts +3 -1
- package/src/ui/mcp/api.ts +10 -1
- package/src/ui/mcp/server.ts +34 -0
- package/src/ui/mcp/tools.ts +50 -0
- package/src/ui/server/agent-resume-grant.ts +53 -0
- package/src/ui/server/agent-routes.ts +230 -9
- package/src/ui/server/audit.ts +68 -1
- package/src/ui/server/index.ts +16 -5
- package/src/ui/server/public/agents-page.js +333 -11
- package/src/ui/server/public/agents-section.js +17 -3
- package/src/ui/server/public/agents.html +44 -0
- package/src/ui/server/public/i18n/es.js +28 -0
- package/src/ui/server/public/i18n/ja.js +25 -0
- package/src/ui/server/public/shell.js +66 -0
- package/src/ui/server/request-guard.ts +21 -1
- package/src/ui/server/route-classes.ts +23 -0
- package/src/ui/server/session-routes.ts +52 -4
- package/src/ui/server/sudo-store.ts +36 -7
|
@@ -20,10 +20,17 @@
|
|
|
20
20
|
* persona, runtime and model; a record whose session is gone is
|
|
21
21
|
* listed as stopped or ended. A plain session is not an agent.
|
|
22
22
|
* stop ends its session (TERM) and marks the record stopped.
|
|
23
|
-
* resume only when its runtime declares resume
|
|
24
|
-
*
|
|
23
|
+
* resume only when its runtime declares resume, its record holds the
|
|
24
|
+
* runtime's resume token, and its session is not running: a new
|
|
25
|
+
* session, in the same cwd and workspace with the grants it had,
|
|
26
|
+
* continuing the last conversation (den-claude: --resume <token>);
|
|
25
27
|
* when that session was never prompted, a fresh start under the
|
|
26
28
|
* same conversation id (there is nothing to continue).
|
|
29
|
+
* --all-interrupted (AG-14.3): every agent interrupted when Den or
|
|
30
|
+
* the machine restarted that can resume.
|
|
31
|
+
* rm (AG-14.1b) forgets a stopped, interrupted or exited agent (never
|
|
32
|
+
* a running one); --stopped forgets every one of -w <ws> (or of
|
|
33
|
+
* every workspace). The runtime's own history stays.
|
|
27
34
|
* doctor (AG-0.3, AG-0.5) each runtime of the catalog, installed
|
|
28
35
|
* first: its status, the CLI it runs and where that came from
|
|
29
36
|
* (workspace or user.yml setting, login shell, PATH, install
|
|
@@ -33,21 +40,30 @@
|
|
|
33
40
|
* fix when it is missing. Exit 1 when the default runtime is
|
|
34
41
|
* unavailable. It resolves the login shell's PATH first, as the
|
|
35
42
|
* web server does at start.
|
|
43
|
+
* handback-check <file|-> (den#5) the hand-back check (handback.ts) on a
|
|
44
|
+
* file or stdin: each problem on its own line, exit 1 when there
|
|
45
|
+
* is one, 0 when the hand-back is complete and plain.
|
|
36
46
|
*
|
|
37
47
|
* Nothing here takes a grant: grants come from the org config only. The
|
|
38
48
|
* host enforces agents.max_concurrent on every start, counting agents only.
|
|
39
49
|
*/
|
|
50
|
+
import * as fs from "fs";
|
|
40
51
|
import { DEFAULT_RUNTIME_ID } from "../../shared/agents/runtimes.js";
|
|
41
52
|
import { initLoginPath } from "../../shared/agents/runtime-search.js";
|
|
42
53
|
import { HostError } from "../../shared/sessions/client.js";
|
|
54
|
+
import { checkHandback } from "../../shared/agents/handback.js";
|
|
55
|
+
import { listRecords } from "../../shared/agents/records.js";
|
|
43
56
|
import {
|
|
44
57
|
AgentError,
|
|
45
58
|
listAgents,
|
|
46
59
|
listPersonas as personaList,
|
|
47
60
|
listRuntimes,
|
|
48
61
|
loadAgentWorkspaces,
|
|
62
|
+
removeAgent,
|
|
63
|
+
removeStopped,
|
|
49
64
|
resolveAgentsDeps,
|
|
50
65
|
resumeAgent,
|
|
66
|
+
resumeInterrupted,
|
|
51
67
|
startAgent,
|
|
52
68
|
stopAgent,
|
|
53
69
|
type AgentsDeps,
|
|
@@ -75,25 +91,41 @@ export {
|
|
|
75
91
|
} from "./agent-ops.js";
|
|
76
92
|
|
|
77
93
|
export const AGENTS_HELP = {
|
|
78
|
-
usage:
|
|
94
|
+
usage:
|
|
95
|
+
"agents <list|start|stop|resume|rm|personas|doctor|handback-check> ...",
|
|
79
96
|
lines: [
|
|
80
97
|
"Agents: a persona and a workspace, run by a runtime in a session that",
|
|
81
98
|
"outlives the web server (see `{cli} sessions`).",
|
|
82
99
|
"",
|
|
83
|
-
"{cli} agents list -w <ws> [--json]",
|
|
100
|
+
"{cli} agents list -w <ws> [--stopped] [--json]",
|
|
101
|
+
" --stopped: only the stopped, interrupted and exited ones",
|
|
84
102
|
"{cli} agents start -w <ws> --persona <p> [--runtime <r>] [--model <m>] [--id <id>]",
|
|
85
103
|
" runs in the workspace's dir as <dist>-<ws>:<id>; its MCP is pinned to <ws>",
|
|
86
104
|
" (plus agents.<id>.grants); capped by agents.max_concurrent",
|
|
87
|
-
"{cli} agents stop <id> [-w <ws>]",
|
|
105
|
+
"{cli} agents stop <id> [-w <ws>] [--no-wait]",
|
|
106
|
+
" waits up to 5 s for the session to end (else: still stopping, exit 1);",
|
|
107
|
+
" --no-wait: return once TERM is sent",
|
|
88
108
|
"{cli} agents resume <id> [-w <ws>]",
|
|
89
|
-
"
|
|
90
|
-
"
|
|
109
|
+
"{cli} agents resume --all-interrupted [-w <ws>]",
|
|
110
|
+
" the same conversation, persona, workspace and folder, when its runtime",
|
|
111
|
+
" can resume one and its session has ended; with nothing to resume yet,",
|
|
112
|
+
" it starts fresh under the same conversation id. --all-interrupted: every",
|
|
113
|
+
" agent interrupted when Den or the machine restarted",
|
|
114
|
+
"{cli} agents rm <id> [-w <ws>]",
|
|
115
|
+
"{cli} agents rm --stopped [-w <ws>]",
|
|
116
|
+
" forget a stopped, interrupted or exited agent (never a running one);",
|
|
117
|
+
" the runtime keeps its own history. agents.remove_stopped_after in",
|
|
118
|
+
" user.yml (never, 1d, 7d) forgets stopped ones on its own",
|
|
91
119
|
"{cli} agents personas [--json]",
|
|
92
120
|
" Den's default pack, then agents.persona_packs (a later pack wins by id)",
|
|
93
121
|
"{cli} agents doctor [-w <ws>] [--json]",
|
|
94
122
|
" each runtime, installed first: status, the CLI it runs and where it came",
|
|
95
123
|
" from, its version, the directories searched, and the fix; exit 1 when the",
|
|
96
124
|
" default is missing. -w: as that workspace's agents find them",
|
|
125
|
+
"{cli} agents handback-check <file|-> [--json]",
|
|
126
|
+
" checks a hand-back to a person: URL, machine, branch, what changed, what",
|
|
127
|
+
" to click and decisions are all there, every ID is explained, and each",
|
|
128
|
+
" decision is a numbered yes/no question; exit 1 with what to fix",
|
|
97
129
|
"",
|
|
98
130
|
"Runtimes: den-claude (default; Claude Code's claude), codex (OpenAI's Codex",
|
|
99
131
|
"CLI), gemini (Google's Gemini CLI), grok (xAI's Grok Build), and generic (the",
|
|
@@ -111,17 +143,27 @@ export const AGENTS_HELP = {
|
|
|
111
143
|
|
|
112
144
|
async function list(args: string[], d: AgentsResolved): Promise<number> {
|
|
113
145
|
const where = "agents list";
|
|
114
|
-
const p = parseArgs(where, args, {
|
|
146
|
+
const p = parseArgs(where, args, {
|
|
147
|
+
"--workspace": true,
|
|
148
|
+
"--json": false,
|
|
149
|
+
"--stopped": false,
|
|
150
|
+
});
|
|
115
151
|
noExtra(where, p);
|
|
116
152
|
const ws = workspaceFlag(where, p);
|
|
117
153
|
if (!ws) throw new UsageError(`${where} needs -w <workspace>`);
|
|
118
|
-
const rows = await listAgents(d, ws
|
|
154
|
+
const rows = await listAgents(d, ws, {
|
|
155
|
+
stopped: p.flags["--stopped"] === true,
|
|
156
|
+
});
|
|
119
157
|
if (p.flags["--json"]) {
|
|
120
158
|
d.out(JSON.stringify(rows, null, 2));
|
|
121
159
|
return 0;
|
|
122
160
|
}
|
|
123
161
|
if (!rows.length) {
|
|
124
|
-
d.out(
|
|
162
|
+
d.out(
|
|
163
|
+
p.flags["--stopped"]
|
|
164
|
+
? `no stopped agents in ${ws}`
|
|
165
|
+
: `no agents in ${ws}`,
|
|
166
|
+
);
|
|
125
167
|
return 0;
|
|
126
168
|
}
|
|
127
169
|
for (const l of columns([
|
|
@@ -223,22 +265,40 @@ function idAndWorkspace(
|
|
|
223
265
|
}
|
|
224
266
|
|
|
225
267
|
async function stop(args: string[], d: AgentsResolved): Promise<number> {
|
|
226
|
-
|
|
227
|
-
const
|
|
268
|
+
// --no-wait: return once TERM is sent (before 0.4.8, always).
|
|
269
|
+
const wait = !args.includes("--no-wait");
|
|
270
|
+
const { id, ws } = idAndWorkspace(
|
|
271
|
+
"agents stop",
|
|
272
|
+
args.filter((a) => a !== "--no-wait"),
|
|
273
|
+
);
|
|
274
|
+
const {
|
|
275
|
+
rec,
|
|
276
|
+
session: s,
|
|
277
|
+
stillStopping,
|
|
278
|
+
} = await stopAgent(d, id, ws, {
|
|
279
|
+
wait,
|
|
280
|
+
});
|
|
281
|
+
if (stillStopping) {
|
|
282
|
+
d.err(
|
|
283
|
+
`${rec.id} is still stopping (sent TERM to ${s!.name}, pid ${s!.pid}; not ended after ${d.stopWaitMs / 1000} s)`,
|
|
284
|
+
);
|
|
285
|
+
return 1;
|
|
286
|
+
}
|
|
228
287
|
d.out(
|
|
229
288
|
s
|
|
230
|
-
? `stopped agent ${rec.id} (sent TERM to ${s.name}, pid ${s.pid})`
|
|
289
|
+
? `stopped agent ${rec.id} (sent TERM to ${s.name}, pid ${s.pid}${wait && d.stopWaitMs > 0 ? "; it has ended" : ""})`
|
|
231
290
|
: `agent ${rec.id} was not running; marked stopped`,
|
|
232
291
|
);
|
|
233
292
|
return 0;
|
|
234
293
|
}
|
|
235
294
|
|
|
236
295
|
async function resume(args: string[], d: AgentsResolved): Promise<number> {
|
|
296
|
+
if (args.includes("--all-interrupted")) return resumeAll(args, d);
|
|
237
297
|
const { id, ws } = idAndWorkspace("agents resume", args);
|
|
238
298
|
const s = await resumeAgent(d, id, ws);
|
|
239
299
|
if (s.fresh)
|
|
240
300
|
d.out(
|
|
241
|
-
`agent ${s.rec.id}:
|
|
301
|
+
`agent ${s.rec.id}: nothing to resume yet; started fresh (same conversation id, folder and scope)`,
|
|
242
302
|
);
|
|
243
303
|
return report(
|
|
244
304
|
d,
|
|
@@ -247,6 +307,58 @@ async function resume(args: string[], d: AgentsResolved): Promise<number> {
|
|
|
247
307
|
);
|
|
248
308
|
}
|
|
249
309
|
|
|
310
|
+
/** `agents resume --all-interrupted [-w <ws>]` (AG-14.3). */
|
|
311
|
+
async function resumeAll(args: string[], d: AgentsResolved): Promise<number> {
|
|
312
|
+
const where = "agents resume";
|
|
313
|
+
const p = parseArgs(where, args, {
|
|
314
|
+
"--workspace": true,
|
|
315
|
+
"--all-interrupted": false,
|
|
316
|
+
});
|
|
317
|
+
noExtra(where, p);
|
|
318
|
+
const ws = workspaceFlag(where, p);
|
|
319
|
+
const r = await resumeInterrupted(d, ws ? [ws] : undefined);
|
|
320
|
+
if (!r.resumed.length && !r.failed.length) {
|
|
321
|
+
d.out(`no interrupted agents${ws ? ` in ${ws}` : ""}`);
|
|
322
|
+
return 0;
|
|
323
|
+
}
|
|
324
|
+
for (const x of r.resumed)
|
|
325
|
+
d.out(
|
|
326
|
+
`${x.fresh ? "started" : "resumed"} agent ${x.rec.id} as ${x.rec.sessionName} in ${x.rec.cwd}`,
|
|
327
|
+
);
|
|
328
|
+
for (const f of r.failed) d.err(`✗ agent ${f.agent}: ${f.why}`);
|
|
329
|
+
return r.failed.length ? 1 : 0;
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
/** `agents rm <id> [-w <ws>]`, `agents rm --stopped [-w <ws>]` (AG-14.1b). */
|
|
333
|
+
async function rm(args: string[], d: AgentsResolved): Promise<number> {
|
|
334
|
+
const where = "agents rm";
|
|
335
|
+
const p = parseArgs(where, args, { "--workspace": true, "--stopped": false });
|
|
336
|
+
noExtra(where, p, 1);
|
|
337
|
+
const ws = workspaceFlag(where, p);
|
|
338
|
+
const id = p.positional[0];
|
|
339
|
+
if (p.flags["--stopped"]) {
|
|
340
|
+
if (id) throw new UsageError(`${where}: give <id> or --stopped, not both`);
|
|
341
|
+
if (ws && !d.workspaceExists(ws))
|
|
342
|
+
throw new UsageError(`${where}: workspace "${ws}" does not exist`);
|
|
343
|
+
const wss = ws
|
|
344
|
+
? [ws]
|
|
345
|
+
: [...new Set(listRecords(d.stateDir()).map((r) => r.workspace))];
|
|
346
|
+
const gone = await removeStopped(d, wss);
|
|
347
|
+
d.out(
|
|
348
|
+
gone.length
|
|
349
|
+
? `removed ${gone.length} stopped ${gone.length === 1 ? "agent" : "agents"}: ${gone.map((g) => g.agent).join(", ")}`
|
|
350
|
+
: `no stopped agents${ws ? ` in ${ws}` : ""}`,
|
|
351
|
+
);
|
|
352
|
+
return 0;
|
|
353
|
+
}
|
|
354
|
+
if (!id) throw new UsageError(`${where} needs <id> or --stopped`);
|
|
355
|
+
const [gone] = await removeAgent(d, id, ws);
|
|
356
|
+
d.out(
|
|
357
|
+
`removed agent ${gone?.agent ?? id}; its conversation stays in the runtime's own history`,
|
|
358
|
+
);
|
|
359
|
+
return 0;
|
|
360
|
+
}
|
|
361
|
+
|
|
250
362
|
async function listPersonas(
|
|
251
363
|
args: string[],
|
|
252
364
|
d: AgentsResolved,
|
|
@@ -276,6 +388,45 @@ async function listPersonas(
|
|
|
276
388
|
export interface AgentsDoctorDeps {
|
|
277
389
|
/** Resolve the login shell's PATH first (default: initLoginPath). */
|
|
278
390
|
loginPath?: () => Promise<void>;
|
|
391
|
+
/** Read a hand-back: a file path, or "-" for stdin (handback-check). */
|
|
392
|
+
readHandback?: (src: string) => Promise<string>;
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
async function readFileOrStdin(src: string): Promise<string> {
|
|
396
|
+
if (src !== "-") return fs.promises.readFile(src, "utf-8");
|
|
397
|
+
let text = "";
|
|
398
|
+
for await (const chunk of process.stdin) text += String(chunk);
|
|
399
|
+
return text;
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
/** `agents handback-check <file|-> [--json]`. */
|
|
403
|
+
async function handbackCheck(
|
|
404
|
+
args: string[],
|
|
405
|
+
d: AgentsResolved,
|
|
406
|
+
deps: AgentsDoctorDeps,
|
|
407
|
+
): Promise<number> {
|
|
408
|
+
const where = "agents handback-check";
|
|
409
|
+
const p = parseArgs(where, args, { "--json": false });
|
|
410
|
+
if (p.positional.length !== 1 || p.rest)
|
|
411
|
+
throw new UsageError(`${where} takes one file, or - for stdin`);
|
|
412
|
+
const src = p.positional[0];
|
|
413
|
+
let text: string;
|
|
414
|
+
try {
|
|
415
|
+
text = await (deps.readHandback ?? readFileOrStdin)(src);
|
|
416
|
+
} catch (e) {
|
|
417
|
+
d.err(
|
|
418
|
+
`✗ ${where}: can't read ${src} (${(e as NodeJS.ErrnoException).code ?? "error"})`,
|
|
419
|
+
);
|
|
420
|
+
return 1;
|
|
421
|
+
}
|
|
422
|
+
const r = checkHandback(text);
|
|
423
|
+
if (p.flags["--json"]) d.out(JSON.stringify(r, null, 2));
|
|
424
|
+
else if (r.ok) d.out("✓ the hand-back is complete and plain");
|
|
425
|
+
else {
|
|
426
|
+
d.out("✗ the hand-back is not ready:");
|
|
427
|
+
for (const l of r.problems) d.out(` - ${l}`);
|
|
428
|
+
}
|
|
429
|
+
return r.ok ? 0 : 1;
|
|
279
430
|
}
|
|
280
431
|
|
|
281
432
|
const SOURCE_WORD: Record<string, string> = {
|
|
@@ -393,12 +544,17 @@ export async function cmdAgents(
|
|
|
393
544
|
case "resume":
|
|
394
545
|
await loadAgentWorkspaces(deps);
|
|
395
546
|
return await resume(rest, d);
|
|
547
|
+
case "rm":
|
|
548
|
+
await loadAgentWorkspaces(deps);
|
|
549
|
+
return await rm(rest, d);
|
|
396
550
|
case "personas":
|
|
397
551
|
return await listPersonas(rest, d);
|
|
398
552
|
case "doctor":
|
|
399
553
|
// -w checks the workspace exists, and reads its runtime paths.
|
|
400
554
|
await loadAgentWorkspaces(deps);
|
|
401
555
|
return await doctor(rest, d, deps);
|
|
556
|
+
case "handback-check":
|
|
557
|
+
return await handbackCheck(rest, d, deps);
|
|
402
558
|
default:
|
|
403
559
|
throw new UsageError(`unknown agents command "${sub}"`);
|
|
404
560
|
}
|
|
@@ -15,12 +15,19 @@
|
|
|
15
15
|
* plain session and each available runtime's agent env (env-probe.ts), and
|
|
16
16
|
* exits 1 when a runtime's scrubEnv name or a credential-shaped variable
|
|
17
17
|
* reaches one. Without the PTY module the probe is unavailable, not failed.
|
|
18
|
+
*
|
|
19
|
+
* It also reads the tail of the local .dev-metrics/events.jsonl (bounded,
|
|
20
|
+
* polling.ts) and warns when one command, run from one place, ran more than
|
|
21
|
+
* 600 times in an hour: an agent polling it. A warning, never a failure;
|
|
22
|
+
* nothing is sent.
|
|
18
23
|
*/
|
|
19
24
|
import chalk from "chalk";
|
|
20
25
|
import { readDenConfig, type DenConfig } from "../../shared/den-config.js";
|
|
21
26
|
import { getStateDir } from "../../shared/paths.js";
|
|
22
27
|
import { product } from "../../shared/product.js";
|
|
23
28
|
import { listRecords } from "../../shared/agents/records.js";
|
|
29
|
+
import { eventsFile } from "../../shared/dev-metrics/emit.js";
|
|
30
|
+
import { pollingWarnings } from "../../shared/dev-metrics/polling.js";
|
|
24
31
|
import { readBrand } from "../../shared/brand.js";
|
|
25
32
|
import { hasEdgeBlock, readEdgeConfig } from "../../shared/edge/config.js";
|
|
26
33
|
import { edgeTlsWarnings, tlsStrategyFor } from "../../shared/edge/tls.js";
|
|
@@ -219,6 +226,18 @@ export function doctorNotes(cli: string, configured: boolean): string[] {
|
|
|
219
226
|
: [];
|
|
220
227
|
}
|
|
221
228
|
|
|
229
|
+
/** The polling-loop warnings for this events file, as doctor prints them. */
|
|
230
|
+
export function pollingLines(file: string, title: string): string[] {
|
|
231
|
+
const warnings = pollingWarnings(file);
|
|
232
|
+
if (!warnings.length) return [];
|
|
233
|
+
return [
|
|
234
|
+
"",
|
|
235
|
+
chalk.bold(`${title} doctor: dev-metrics (local, never sent)`),
|
|
236
|
+
"",
|
|
237
|
+
...warnings.map((w) => chalk.yellow(` ⚠ ${w}`)),
|
|
238
|
+
];
|
|
239
|
+
}
|
|
240
|
+
|
|
222
241
|
function readConfigOrNull(): DenConfig | null {
|
|
223
242
|
try {
|
|
224
243
|
return readDenConfig();
|
|
@@ -293,6 +312,7 @@ export async function cmdDoctor(args: string[]): Promise<number> {
|
|
|
293
312
|
}
|
|
294
313
|
const rows = doctorRows();
|
|
295
314
|
for (const l of formatDoctor(rows, readBrand().name)) console.log(l);
|
|
315
|
+
for (const l of pollingLines(eventsFile(), readBrand().name)) console.log(l);
|
|
296
316
|
const cli = product().cli;
|
|
297
317
|
if (!probe) {
|
|
298
318
|
for (const l of doctorNotes(
|
package/src/ui/cli/index.ts
CHANGED
|
@@ -196,6 +196,7 @@ import { cmdEdge } from "./edge-command.js";
|
|
|
196
196
|
import { cmdDoctor } from "./doctor-command.js";
|
|
197
197
|
import { cmdSessions, SESSIONS_HELP } from "./sessions-command.js";
|
|
198
198
|
import { AGENTS_HELP, cmdAgents } from "./agents-command.js";
|
|
199
|
+
import { USAGE_HELP, cmdUsage } from "./usage-command.js";
|
|
199
200
|
import { cmdService } from "./service-command.js";
|
|
200
201
|
import { cmdStack } from "./stack-command.js";
|
|
201
202
|
import { cmdTheme } from "./theme-command.js";
|
|
@@ -536,6 +537,7 @@ const COMMAND_HELP: Record<string, { usage: string; lines: string[] }> = {
|
|
|
536
537
|
},
|
|
537
538
|
sessions: SESSIONS_HELP,
|
|
538
539
|
agents: AGENTS_HELP,
|
|
540
|
+
usage: USAGE_HELP,
|
|
539
541
|
};
|
|
540
542
|
|
|
541
543
|
function resolveCliBaseApiUrl(explicitBaseApiUrl?: string): string | undefined {
|
|
@@ -588,6 +590,7 @@ const TELEMETRY_COMMANDS = new Set<string>([
|
|
|
588
590
|
"mcp",
|
|
589
591
|
"sessions",
|
|
590
592
|
"agents",
|
|
593
|
+
"usage",
|
|
591
594
|
"service",
|
|
592
595
|
"theme",
|
|
593
596
|
"plugin",
|
|
@@ -616,6 +619,8 @@ const CONFIG_FREE_COMMANDS = new Set<string>([
|
|
|
616
619
|
// listing a session is never stranded by a config that fails (AG-3a).
|
|
617
620
|
"sessions",
|
|
618
621
|
"agents",
|
|
622
|
+
// den#6: reads local transcripts and agent records, no org data.
|
|
623
|
+
"usage",
|
|
619
624
|
]);
|
|
620
625
|
|
|
621
626
|
function configCheckedCommand(command: string): boolean {
|
|
@@ -900,6 +905,12 @@ class DenCLI {
|
|
|
900
905
|
|
|
901
906
|
case "web": {
|
|
902
907
|
const sub = args[1];
|
|
908
|
+
if (args.includes("--json") && (sub === "down" || sub === "stop")) {
|
|
909
|
+
process.exitCode = await this.cmdWebDownJson(
|
|
910
|
+
args.slice(1).filter((a) => a !== "--json"),
|
|
911
|
+
);
|
|
912
|
+
break;
|
|
913
|
+
}
|
|
903
914
|
if (
|
|
904
915
|
args.includes("--json") &&
|
|
905
916
|
(sub === undefined ||
|
|
@@ -926,7 +937,11 @@ class DenCLI {
|
|
|
926
937
|
break;
|
|
927
938
|
|
|
928
939
|
case "down":
|
|
929
|
-
|
|
940
|
+
if (args.slice(1).includes("--json"))
|
|
941
|
+
process.exitCode = await this.cmdServiceDownJson(
|
|
942
|
+
args.slice(1).filter((a) => a !== "--json"),
|
|
943
|
+
);
|
|
944
|
+
else await this.cmdServiceDown(args.slice(1));
|
|
930
945
|
break;
|
|
931
946
|
|
|
932
947
|
case "restart":
|
|
@@ -1072,6 +1087,10 @@ class DenCLI {
|
|
|
1072
1087
|
process.exitCode = await cmdAgents(args.slice(1));
|
|
1073
1088
|
break;
|
|
1074
1089
|
|
|
1090
|
+
case "usage":
|
|
1091
|
+
process.exitCode = await cmdUsage(args.slice(1));
|
|
1092
|
+
break;
|
|
1093
|
+
|
|
1075
1094
|
case "mcp": {
|
|
1076
1095
|
// Setup subcommands: register the server with agents.
|
|
1077
1096
|
const { MCP_CONNECT_SUBCOMMANDS, runMcpConnect } =
|
|
@@ -3276,6 +3295,101 @@ class DenCLI {
|
|
|
3276
3295
|
return out.ok ? 0 : 1;
|
|
3277
3296
|
}
|
|
3278
3297
|
|
|
3298
|
+
/**
|
|
3299
|
+
* `down --json`: down as it always runs, its log on stderr, then one JSON
|
|
3300
|
+
* object on stdout, `{ ok, workspace, services, all? }`. Safe to re-run:
|
|
3301
|
+
* stopping what is already stopped is ok. A failure names a fix.
|
|
3302
|
+
*/
|
|
3303
|
+
private async cmdServiceDownJson(args: string[]): Promise<number> {
|
|
3304
|
+
const bin = fs.existsSync(
|
|
3305
|
+
nodePath.join(getDistRoot(), "bin", product().cli),
|
|
3306
|
+
)
|
|
3307
|
+
? `./bin/${product().cli}`
|
|
3308
|
+
: product().cli;
|
|
3309
|
+
const before = process.exitCode;
|
|
3310
|
+
process.exitCode = undefined;
|
|
3311
|
+
let error: string | undefined;
|
|
3312
|
+
try {
|
|
3313
|
+
await quietStdout(() => this.cmdServiceDown(args));
|
|
3314
|
+
} catch (e) {
|
|
3315
|
+
if (!(e instanceof ExitRequested)) error = (e as Error).message;
|
|
3316
|
+
else if (e.code !== 0) process.exitCode = e.code;
|
|
3317
|
+
}
|
|
3318
|
+
const failedExit = process.exitCode !== undefined && process.exitCode !== 0;
|
|
3319
|
+
process.exitCode = before;
|
|
3320
|
+
const terminator = args.indexOf("--");
|
|
3321
|
+
const own = terminator === -1 ? args : args.slice(0, terminator);
|
|
3322
|
+
const all = own.includes("-a") || own.includes("--all");
|
|
3323
|
+
const out: Record<string, unknown> = {
|
|
3324
|
+
ok: !error && !failedExit,
|
|
3325
|
+
workspace:
|
|
3326
|
+
process.env.DEN_WORKSPACE_OVERRIDE ?? getFocusedWorkspaceName() ?? null,
|
|
3327
|
+
services: own.filter((a) => !a.startsWith("-")),
|
|
3328
|
+
...(all ? { all: true } : {}),
|
|
3329
|
+
};
|
|
3330
|
+
if (!out.ok) {
|
|
3331
|
+
out.error = error ?? "down failed (its log is on stderr)";
|
|
3332
|
+
out.fix = `${bin} doctor --json`;
|
|
3333
|
+
}
|
|
3334
|
+
printJson(out);
|
|
3335
|
+
return out.ok ? 0 : 1;
|
|
3336
|
+
}
|
|
3337
|
+
|
|
3338
|
+
/**
|
|
3339
|
+
* `web down --json`: the web server stopped as usual (its log on stderr),
|
|
3340
|
+
* then `{ ok, url, port, running: false, stopped }` once nothing answers
|
|
3341
|
+
* (up to 10s). `stopped` is false when it wasn't running: re-running is ok.
|
|
3342
|
+
*/
|
|
3343
|
+
private async cmdWebDownJson(args: string[]): Promise<number> {
|
|
3344
|
+
const { parseWebCommandArgs } = await import("./web-command.js");
|
|
3345
|
+
const { probeWebServer } = await import("./mcp-connect.js");
|
|
3346
|
+
const bin = fs.existsSync(
|
|
3347
|
+
nodePath.join(getDistRoot(), "bin", product().cli),
|
|
3348
|
+
)
|
|
3349
|
+
? `./bin/${product().cli}`
|
|
3350
|
+
: product().cli;
|
|
3351
|
+
let port: number;
|
|
3352
|
+
try {
|
|
3353
|
+
port = parseWebCommandArgs(args).port;
|
|
3354
|
+
} catch (e) {
|
|
3355
|
+
printJson({
|
|
3356
|
+
ok: false,
|
|
3357
|
+
error: (e as Error).message,
|
|
3358
|
+
fix: `${bin} web down --json`,
|
|
3359
|
+
});
|
|
3360
|
+
return 2;
|
|
3361
|
+
}
|
|
3362
|
+
const url = `http://localhost:${port}`;
|
|
3363
|
+
const was = (await probeWebServer(url)).kind === "up";
|
|
3364
|
+
let error: string | undefined;
|
|
3365
|
+
try {
|
|
3366
|
+
await quietStdout(() => cmdWeb(args));
|
|
3367
|
+
} catch (e) {
|
|
3368
|
+
if (!(e instanceof ExitRequested) || e.code !== 0)
|
|
3369
|
+
error = (e as Error).message;
|
|
3370
|
+
}
|
|
3371
|
+
let probe = await probeWebServer(url);
|
|
3372
|
+
const deadline = Date.now() + 10_000;
|
|
3373
|
+
while (!error && probe.kind === "up" && Date.now() < deadline) {
|
|
3374
|
+
await new Promise((r) => setTimeout(r, 250));
|
|
3375
|
+
probe = await probeWebServer(url);
|
|
3376
|
+
}
|
|
3377
|
+
const running = probe.kind === "up";
|
|
3378
|
+
const out: Record<string, unknown> = {
|
|
3379
|
+
ok: !error && !running,
|
|
3380
|
+
url,
|
|
3381
|
+
port,
|
|
3382
|
+
running,
|
|
3383
|
+
stopped: was && !running,
|
|
3384
|
+
};
|
|
3385
|
+
if (!out.ok) {
|
|
3386
|
+
out.error = error ?? `the web server still answers at ${url}`;
|
|
3387
|
+
out.fix = `${bin} doctor --json`;
|
|
3388
|
+
}
|
|
3389
|
+
printJson(out);
|
|
3390
|
+
return out.ok ? 0 : 1;
|
|
3391
|
+
}
|
|
3392
|
+
|
|
3279
3393
|
/**
|
|
3280
3394
|
* `web up|restart --json` (ON-2): the web server started as usual (its
|
|
3281
3395
|
* log on stderr), then `{ ok, url, port, version? }` once it answers
|
|
@@ -5193,7 +5307,10 @@ class DenCLI {
|
|
|
5193
5307
|
);
|
|
5194
5308
|
console.log(" agents list -w <ws> The workspace's agents");
|
|
5195
5309
|
console.log(
|
|
5196
|
-
" agents start -w <ws> --persona <p> Run a persona in the workspace (stop, resume, personas)",
|
|
5310
|
+
" agents start -w <ws> --persona <p> Run a persona in the workspace (stop, resume, rm, personas)",
|
|
5311
|
+
);
|
|
5312
|
+
console.log(
|
|
5313
|
+
" usage agents|report Agents' tokens and outcomes, from local transcripts (never sent)",
|
|
5197
5314
|
);
|
|
5198
5315
|
console.log();
|
|
5199
5316
|
console.log(
|