@monster-orange/den 0.3.0 → 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 (43) hide show
  1. package/CHANGELOG.md +184 -0
  2. package/docs/distributions.md +229 -8
  3. package/package.json +4 -1
  4. package/src/shared/agents/default-pack/be-dev.md +20 -0
  5. package/src/shared/agents/default-pack/devops.md +14 -0
  6. package/src/shared/agents/default-pack/fe-dev.md +20 -0
  7. package/src/shared/agents/default-pack/orchestrator.md +16 -0
  8. package/src/shared/agents/default-pack/planner.md +17 -0
  9. package/src/shared/agents/default-pack/reviewer.md +14 -0
  10. package/src/shared/agents/default-pack/ux-designer.md +15 -0
  11. package/src/shared/agents/den-claude.ts +173 -0
  12. package/src/shared/agents/generic.ts +52 -0
  13. package/src/shared/agents/personas.ts +218 -0
  14. package/src/shared/agents/records.ts +118 -0
  15. package/src/shared/agents/runtime.ts +192 -0
  16. package/src/shared/agents/runtimes.ts +47 -0
  17. package/src/shared/den-config.ts +116 -0
  18. package/src/shared/engine.ts +23 -0
  19. package/src/shared/environment-manager/workspaces.ts +19 -0
  20. package/src/shared/secret-env.ts +70 -0
  21. package/src/shared/sessions/client.ts +277 -0
  22. package/src/shared/sessions/den-pty.ts +97 -0
  23. package/src/shared/sessions/env.ts +79 -0
  24. package/src/shared/sessions/host.ts +667 -0
  25. package/src/shared/sessions/names.ts +85 -0
  26. package/src/shared/sessions/paths.ts +27 -0
  27. package/src/shared/sessions/protocol.ts +72 -0
  28. package/src/shared/sessions/pty.ts +141 -0
  29. package/src/shared/sessions/ring-buffer.ts +160 -0
  30. package/src/shared/sessions/types.ts +81 -0
  31. package/src/ui/cli/agents-command.ts +812 -0
  32. package/src/ui/cli/doctor-command.ts +138 -9
  33. package/src/ui/cli/env-probe.ts +266 -0
  34. package/src/ui/cli/flag-parser.ts +11 -3
  35. package/src/ui/cli/index.ts +34 -2
  36. package/src/ui/cli/sessions-command.ts +718 -0
  37. package/src/ui/mcp/scope.ts +123 -19
  38. package/src/ui/mcp/server.ts +27 -2
  39. package/src/ui/mcp/tools.ts +44 -5
  40. package/src/ui/server/audit.ts +35 -0
  41. package/src/ui/server/index.ts +7 -0
  42. package/src/ui/server/readonly-routes.ts +25 -2
  43. package/src/ui/server/workspace-routes.ts +3 -15
@@ -1,13 +1,26 @@
1
1
  /**
2
- * `doctor`: every strategy kind in the registry (secrets, tls), the
3
- * strategy this distribution selects for it, whether nothing chose it
4
- * (implicit), and its detect() result with the reason.
2
+ * `doctor`: every strategy kind in the registry (secrets, tls,
3
+ * session-backend, runtime), the strategy this distribution selects for it, whether
4
+ * nothing chose it (implicit), and its detect() result with the reason.
5
5
  *
6
- * Read-only: detect() is a PATH check, so doctor never spawns a backend
7
- * CLI, never prompts, and never prints a secret value. Exit 1 when a
8
- * strategy that will be used is unavailable or its config is invalid.
6
+ * Read-only: detect() is a PATH check (session-backend loads the optional
7
+ * PTY module, which runs nothing; runtime reads a CLI's version from its
8
+ * install, never by running it), so doctor never spawns a backend CLI,
9
+ * never prompts, and never prints a secret value. Exit 1 when a strategy
10
+ * that will be used is unavailable or its config is invalid. When agents
11
+ * are configured, a note says to run `<cli> doctor --probe`.
12
+ *
13
+ * `--probe` (opt-in, AG spec §5a-env) is the one exception to never
14
+ * spawning: it runs a short probe session through the session host for a
15
+ * plain session and each available runtime's agent env (env-probe.ts), and
16
+ * exits 1 when a runtime's scrubEnv name or a credential-shaped variable
17
+ * reaches one. Without the PTY module the probe is unavailable, not failed.
9
18
  */
10
19
  import chalk from "chalk";
20
+ import { readDenConfig, type DenConfig } from "../../shared/den-config.js";
21
+ import { getStateDir } from "../../shared/paths.js";
22
+ import { product } from "../../shared/product.js";
23
+ import { listRecords } from "../../shared/agents/records.js";
11
24
  import { readBrand } from "../../shared/brand.js";
12
25
  import { hasEdgeBlock, readEdgeConfig } from "../../shared/edge/config.js";
13
26
  import { edgeTlsWarnings, tlsStrategyFor } from "../../shared/edge/tls.js";
@@ -15,7 +28,18 @@ import {
15
28
  readSecretsConfig,
16
29
  secretsStatus,
17
30
  } from "../../shared/secrets/index.js";
18
- import { strategyKinds } from "../../shared/strategies/registry.js";
31
+ import {
32
+ DEN_PTY_ID,
33
+ SESSION_BACKEND_KIND,
34
+ } from "../../shared/sessions/den-pty.js";
35
+ import {
36
+ getStrategy,
37
+ strategyKinds,
38
+ } from "../../shared/strategies/registry.js";
39
+ import {
40
+ DEFAULT_RUNTIME_ID,
41
+ RUNTIME_KIND,
42
+ } from "../../shared/agents/runtimes.js";
19
43
 
20
44
  export interface DoctorRow {
21
45
  kind: string;
@@ -80,6 +104,42 @@ const SELECTORS: Record<string, Selector> = {
80
104
  };
81
105
  }
82
106
  },
107
+ // Agents' terminals (AG-3a). Only den-pty exists, so it is the implicit
108
+ // choice; unavailable is never a problem (nothing needs a session until
109
+ // one is started, and `sessions start` says why).
110
+ [SESSION_BACKEND_KIND](env) {
111
+ const d = getStrategy(SESSION_BACKEND_KIND, DEN_PTY_ID)?.detect(env) ?? {
112
+ available: false,
113
+ reason: "not registered",
114
+ };
115
+ return {
116
+ kind: SESSION_BACKEND_KIND,
117
+ selected: DEN_PTY_ID,
118
+ implicit: true,
119
+ available: d.available,
120
+ reason: d.reason,
121
+ problem: false,
122
+ warnings: [],
123
+ };
124
+ },
125
+ // Agents' CLIs (AG-2a): the default runtime, chosen per persona or agent,
126
+ // so it is implicit and its absence is never a problem (`agents start`
127
+ // says why it can't run).
128
+ [RUNTIME_KIND](env) {
129
+ const d = getStrategy(RUNTIME_KIND, DEFAULT_RUNTIME_ID)?.detect(env) ?? {
130
+ available: false,
131
+ reason: "not registered",
132
+ };
133
+ return {
134
+ kind: RUNTIME_KIND,
135
+ selected: DEFAULT_RUNTIME_ID,
136
+ implicit: true,
137
+ available: d.available,
138
+ reason: d.reason,
139
+ problem: false,
140
+ warnings: [],
141
+ };
142
+ },
83
143
  };
84
144
 
85
145
  /**
@@ -124,8 +184,77 @@ export function formatDoctor(rows: DoctorRow[], title: string): string[] {
124
184
  return lines;
125
185
  }
126
186
 
127
- export function cmdDoctor(_args: string[]): number {
187
+ /**
188
+ * Whether agents are configured here: an `agents:` block with anything in
189
+ * it (max_concurrent, persona_packs, an agent id), or an agent record.
190
+ */
191
+ export function agentsConfigured(
192
+ config: DenConfig | null,
193
+ stateDir: string,
194
+ ): boolean {
195
+ const raw = config?.agents as unknown;
196
+ if (raw && typeof raw === "object" && Object.keys(raw).length) return true;
197
+ try {
198
+ return listRecords(stateDir).length > 0;
199
+ } catch {
200
+ return false;
201
+ }
202
+ }
203
+
204
+ /** Informational lines after the rows: never a failure. */
205
+ export function doctorNotes(cli: string, configured: boolean): string[] {
206
+ return configured
207
+ ? [
208
+ "",
209
+ ` ℹ agents are configured or have run here: run \`${cli} doctor --probe\` to verify agent env isolation`,
210
+ ]
211
+ : [];
212
+ }
213
+
214
+ function readConfigOrNull(): DenConfig | null {
215
+ try {
216
+ return readDenConfig();
217
+ } catch {
218
+ return null;
219
+ }
220
+ }
221
+
222
+ export async function cmdDoctor(args: string[]): Promise<number> {
223
+ const probe = args.includes("--probe");
224
+ const unknown = args.filter((a) => a !== "--probe");
225
+ if (unknown.length) {
226
+ console.error(
227
+ `✗ doctor: unknown argument ${unknown[0]} (doctor [--probe])`,
228
+ );
229
+ return 2;
230
+ }
128
231
  const rows = doctorRows();
129
232
  for (const l of formatDoctor(rows, readBrand().name)) console.log(l);
130
- return rows.some((r) => r.problem) ? 1 : 0;
233
+ const cli = product().cli;
234
+ if (!probe) {
235
+ for (const l of doctorNotes(
236
+ cli,
237
+ agentsConfigured(readConfigOrNull(), getStateDir()),
238
+ ))
239
+ console.log(l);
240
+ return rows.some((r) => r.problem) ? 1 : 0;
241
+ }
242
+ const { runEnvProbe, formatProbe } = await import("./env-probe.js");
243
+ const { resolveDeps, plainSessionEnv } =
244
+ await import("./sessions-command.js");
245
+ const d = resolveDeps({});
246
+ const results = await runEnvProbe({
247
+ dist: d.dist,
248
+ cli: d.cli,
249
+ env: d.env,
250
+ pty: d.pty,
251
+ connect: d.connect,
252
+ plainEnv: (caller) => plainSessionEnv(caller),
253
+ });
254
+ console.log("");
255
+ console.log(chalk.bold(`${readBrand().name} doctor: session env probe`));
256
+ console.log("");
257
+ for (const l of formatProbe(results)) console.log(l);
258
+ const found = results.some((r) => r.findings.length);
259
+ return rows.some((r) => r.problem) || found ? 1 : 0;
131
260
  }
@@ -0,0 +1,266 @@
1
+ /**
2
+ * `<cli> doctor --probe` (AG spec §5a-env): check, through the real session
3
+ * host and backend, what environment a session actually gets. Opt-in: plain
4
+ * `doctor` never spawns anything.
5
+ *
6
+ * For a plain session (as `sessions start` builds it, no --keep-env) and
7
+ * for each available runtime's agent env (as `agents start` builds it, the
8
+ * runtime's launch env included, but running the probe instead of the
9
+ * runtime's CLI), it starts a short session running a tiny node script that
10
+ * prints each key name and one flag: whether the name or value is
11
+ * credential-shaped (secret-env.ts, with the redactor's patterns). Values
12
+ * never leave the probe process.
13
+ *
14
+ * A finding is any runtime's scrubEnv name, or any credential-shaped
15
+ * variable, in either env.
16
+ */
17
+ import * as fs from "fs";
18
+ import * as os from "os";
19
+ import * as path from "path";
20
+ import {
21
+ agentSessionEnv,
22
+ allScrubEnv,
23
+ getRuntime,
24
+ LaunchError,
25
+ runtimeIds,
26
+ type LaunchInput,
27
+ type Runtime,
28
+ } from "../../shared/agents/runtimes.js";
29
+ import { credentialPatternData } from "../../shared/secret-env.js";
30
+ import type { HostClient } from "../../shared/sessions/client.js";
31
+ import { matchesScrub } from "../../shared/sessions/env.js";
32
+ import { sessionName } from "../../shared/sessions/names.js";
33
+ import type { PtyLoad } from "../../shared/sessions/pty.js";
34
+
35
+ const MARK = "DENPROBE";
36
+ const PROBE_ID = "env-probe";
37
+ /** How long one probe session gets to print and exit. */
38
+ const PROBE_TIMEOUT_MS = 10_000;
39
+
40
+ /** The probe: key names and a 0/1 flag per key, then an end mark. No values. */
41
+ export function probeScript(): string {
42
+ const p = JSON.stringify(credentialPatternData());
43
+ return [
44
+ `const P = ${p};`,
45
+ `const re = (l) => l.map((r) => new RegExp(r.source, r.flags));`,
46
+ `const N = re(P.name), V = re(P.value);`,
47
+ `for (const k of Object.keys(process.env).sort()) {`,
48
+ ` const v = process.env[k] || "";`,
49
+ ` const c = N.some((r) => r.test(k)) || (v !== "" && V.some((r) => r.test(v)));`,
50
+ ` process.stdout.write("${MARK} " + k + " " + (c ? 1 : 0) + "\\n");`,
51
+ `}`,
52
+ `process.stdout.write("${MARK}-END\\n");`,
53
+ ].join("\n");
54
+ }
55
+
56
+ export interface ProbeKey {
57
+ name: string;
58
+ credentialShaped: boolean;
59
+ }
60
+
61
+ /** The probe's report from its output; null when it did not finish. */
62
+ export function parseProbe(output: string): ProbeKey[] | null {
63
+ const keys: ProbeKey[] = [];
64
+ let ended = false;
65
+ for (const raw of output.split(/\r?\n/)) {
66
+ const line = raw.replace(/\r/g, "").trim();
67
+ if (line === `${MARK}-END`) ended = true;
68
+ const m = new RegExp(`^${MARK} ([A-Za-z_][A-Za-z0-9_]*) ([01])$`).exec(
69
+ line,
70
+ );
71
+ if (m) keys.push({ name: m[1], credentialShaped: m[2] === "1" });
72
+ }
73
+ return ended ? keys : null;
74
+ }
75
+
76
+ export interface ProbeFinding {
77
+ key: string;
78
+ why: string;
79
+ }
80
+
81
+ export interface ProbeResult {
82
+ /** "plain session", or "agent (<runtime>)". */
83
+ target: string;
84
+ /** False: not probed (the reason says why); never a failure. */
85
+ ran: boolean;
86
+ reason?: string;
87
+ keys?: number;
88
+ findings: ProbeFinding[];
89
+ }
90
+
91
+ export interface ProbeDeps {
92
+ dist: string;
93
+ cli: string;
94
+ env: NodeJS.ProcessEnv;
95
+ pty: () => PtyLoad;
96
+ connect: () => Promise<HostClient>;
97
+ /** A plain session's env, as `sessions start` builds it. */
98
+ plainEnv: (caller: NodeJS.ProcessEnv) => Record<string, string>;
99
+ /** An agent's env, as `agents start` builds it. */
100
+ agentEnv?: (
101
+ caller: NodeJS.ProcessEnv,
102
+ rt: Runtime,
103
+ launch: Record<string, string>,
104
+ agentId: string,
105
+ ) => Record<string, string>;
106
+ /** Runtimes to probe (default: every registered one that is available). */
107
+ runtimes?: () => Runtime[];
108
+ }
109
+
110
+ /** A launch input for the probe: nothing is written, nothing it names runs. */
111
+ function probeInput(d: ProbeDeps, dir: string): LaunchInput {
112
+ return {
113
+ persona: {
114
+ id: PROBE_ID,
115
+ role: "",
116
+ prompt: "",
117
+ standing: false,
118
+ tools: [],
119
+ context: [],
120
+ argv: [PROBE_ID],
121
+ source: "doctor --probe",
122
+ },
123
+ workspace: { name: "doctor", dir },
124
+ agentId: PROBE_ID,
125
+ worktree: dir,
126
+ mcp: { name: d.cli, command: [d.cli], env: {}, scope: "doctor" },
127
+ // Never written: the probe uses only the launch's env.
128
+ filesDir: path.join(dir, PROBE_ID),
129
+ env: d.env,
130
+ };
131
+ }
132
+
133
+ function findings(keys: ProbeKey[]): ProbeFinding[] {
134
+ const scrub = allScrubEnv();
135
+ const out: ProbeFinding[] = [];
136
+ for (const k of keys) {
137
+ if (matchesScrub(k.name, scrub))
138
+ out.push({ key: k.name, why: "a runtime's scrubEnv name" });
139
+ else if (k.credentialShaped)
140
+ out.push({ key: k.name, why: "credential-shaped" });
141
+ }
142
+ return out;
143
+ }
144
+
145
+ /** Run the probe in one session with `env`; its report, or why not. */
146
+ async function probeOnce(
147
+ d: ProbeDeps,
148
+ c: HostClient,
149
+ target: string,
150
+ slug: string,
151
+ env: Record<string, string>,
152
+ cwd: string,
153
+ ): Promise<ProbeResult> {
154
+ const info = await c.start({
155
+ name: sessionName(d.dist, "doctor", `${PROBE_ID}-${slug}`),
156
+ cwd,
157
+ argv: [process.execPath, "-e", probeScript()],
158
+ env,
159
+ cols: 200,
160
+ rows: 24,
161
+ });
162
+ try {
163
+ const deadline = Date.now() + PROBE_TIMEOUT_MS;
164
+ for (;;) {
165
+ const s = (await c.list()).find((x) => x.ref.id === info.ref.id);
166
+ if (!s || s.exited) break;
167
+ if (Date.now() > deadline) {
168
+ await c.kill(info.ref.id, "KILL").catch(() => undefined);
169
+ await new Promise((r) => setTimeout(r, 200));
170
+ break;
171
+ }
172
+ await new Promise((r) => setTimeout(r, 50));
173
+ }
174
+ const keys = parseProbe(
175
+ (await c.scrollback(info.ref.id, 1024 * 1024)).toString("utf-8"),
176
+ );
177
+ if (!keys)
178
+ return {
179
+ target,
180
+ ran: true,
181
+ findings: [{ key: "-", why: "the probe did not finish" }],
182
+ };
183
+ return { target, ran: true, keys: keys.length, findings: findings(keys) };
184
+ } finally {
185
+ await c.forget(info.ref.id).catch(() => undefined);
186
+ }
187
+ }
188
+
189
+ /** Probe a plain session and each available runtime's agent env. */
190
+ export async function runEnvProbe(d: ProbeDeps): Promise<ProbeResult[]> {
191
+ const pty = d.pty();
192
+ if (!pty.available)
193
+ return [
194
+ {
195
+ target: "sessions",
196
+ ran: false,
197
+ reason: `den-pty is unavailable: ${pty.reason}`,
198
+ findings: [],
199
+ },
200
+ ];
201
+ const runtimes =
202
+ d.runtimes?.() ??
203
+ runtimeIds()
204
+ .map((id) => getRuntime(id))
205
+ .filter((rt): rt is Runtime => !!rt);
206
+ const agentEnv = d.agentEnv ?? agentSessionEnv;
207
+ // The probe writes nothing: its cwd is the temp dir, as it is.
208
+ const dir = fs.realpathSync(os.tmpdir());
209
+ const c = await d.connect();
210
+ try {
211
+ const out: ProbeResult[] = [
212
+ await probeOnce(d, c, "plain session", "plain", d.plainEnv(d.env), dir),
213
+ ];
214
+ for (const rt of runtimes) {
215
+ const target = `agent (${rt.id})`;
216
+ const det = rt.detect(d.env);
217
+ if (!det.available) {
218
+ out.push({
219
+ target,
220
+ ran: false,
221
+ reason: `${rt.id} is unavailable: ${det.why ?? "unknown"}`,
222
+ findings: [],
223
+ });
224
+ continue;
225
+ }
226
+ let launch: Record<string, string>;
227
+ try {
228
+ launch = rt.launch(probeInput(d, dir)).env;
229
+ } catch (e) {
230
+ if (!(e instanceof LaunchError)) throw e;
231
+ out.push({ target, ran: false, reason: e.message, findings: [] });
232
+ continue;
233
+ }
234
+ out.push(
235
+ await probeOnce(
236
+ d,
237
+ c,
238
+ target,
239
+ rt.id,
240
+ agentEnv(d.env, rt, launch, PROBE_ID),
241
+ dir,
242
+ ),
243
+ );
244
+ }
245
+ return out;
246
+ } finally {
247
+ c.close();
248
+ }
249
+ }
250
+
251
+ /** One line per result, one per finding; key names only. */
252
+ export function formatProbe(results: ProbeResult[]): string[] {
253
+ const lines: string[] = [];
254
+ const width = Math.max(...results.map((r) => r.target.length));
255
+ for (const r of results) {
256
+ const t = r.target.padEnd(width);
257
+ if (!r.ran)
258
+ lines.push(` ${t} - not probed: ${r.reason ?? "unavailable"}`);
259
+ else if (!r.findings.length)
260
+ lines.push(` ${t} ✓ clean (${r.keys ?? 0} variables)`);
261
+ else
262
+ for (const f of r.findings)
263
+ lines.push(` ${t} ✗ ${f.key} reaches it (${f.why})`);
264
+ }
265
+ return lines;
266
+ }
@@ -77,10 +77,18 @@ const WORKSPACE_COMMANDS = new Set([
77
77
 
78
78
  /**
79
79
  * Commands that read `-w`/`--workspace` themselves, so it is left in their
80
- * args: `setup --plan -w a -w b` takes several, and `mcp --workspace <ws>`
81
- * scopes the MCP server (it never sets the CLI's workspace override).
80
+ * args: `setup --plan -w a -w b` takes several, `mcp --workspace <ws>`
81
+ * scopes the MCP server, and `sessions`/`agents` name a session's
82
+ * workspace. None of them sets the CLI's workspace override.
82
83
  */
83
- const OWN_WORKSPACE_FLAG_COMMANDS = new Set(["setup", "mcp"]);
84
+ const OWN_WORKSPACE_FLAG_COMMANDS = new Set([
85
+ "setup",
86
+ "mcp",
87
+ // A session's workspace is part of its name (AG-3a); -w never sets the
88
+ // CLI's workspace override here.
89
+ "sessions",
90
+ "agents",
91
+ ]);
84
92
 
85
93
  /** Whether `-w <name>` on this command requires `<name>` to be active. */
86
94
  export function workspaceFlagRequiresActive(command: string): boolean {
@@ -193,6 +193,8 @@ import { cleanupStaleTabs } from "../../shared/cli-tabs.js";
193
193
  import { cmdConfig } from "./config-command.js";
194
194
  import { cmdEdge } from "./edge-command.js";
195
195
  import { cmdDoctor } from "./doctor-command.js";
196
+ import { cmdSessions, SESSIONS_HELP } from "./sessions-command.js";
197
+ import { AGENTS_HELP, cmdAgents } from "./agents-command.js";
196
198
  import { cmdService } from "./service-command.js";
197
199
  import { cmdStack } from "./stack-command.js";
198
200
  import { cmdTheme } from "./theme-command.js";
@@ -459,10 +461,14 @@ const COMMAND_HELP: Record<string, { usage: string; lines: string[] }> = {
459
461
  ],
460
462
  },
461
463
  doctor: {
462
- usage: "doctor",
464
+ usage: "doctor [--probe]",
463
465
  lines: [
464
466
  "Each strategy kind (secrets, tls): the one selected, whether it was implicit,",
465
467
  "and whether it can work here (a PATH check; nothing is run or prompted).",
468
+ "--probe: start a short probe session (through the session host) for a",
469
+ "plain session and each available runtime's agent env; key names only.",
470
+ "Exit 1 when a runtime's scrubEnv name or a credential-shaped variable",
471
+ "reaches one. Without the PTY module it says unavailable, not failed.",
466
472
  ],
467
473
  },
468
474
  plugin: {
@@ -508,6 +514,8 @@ const COMMAND_HELP: Record<string, { usage: string; lines: string[] }> = {
508
514
  usage: "menu",
509
515
  lines: ["Open the interactive menu (pillar/module/env picker)."],
510
516
  },
517
+ sessions: SESSIONS_HELP,
518
+ agents: AGENTS_HELP,
511
519
  };
512
520
 
513
521
  function resolveCliBaseApiUrl(explicitBaseApiUrl?: string): string | undefined {
@@ -558,6 +566,8 @@ const TELEMETRY_COMMANDS = new Set<string>([
558
566
  "apps",
559
567
  "applications",
560
568
  "mcp",
569
+ "sessions",
570
+ "agents",
561
571
  "service",
562
572
  "theme",
563
573
  "plugin",
@@ -581,6 +591,10 @@ const CONFIG_FREE_COMMANDS = new Set<string>([
581
591
  "local-dev",
582
592
  "view-results",
583
593
  "down",
594
+ // The session host and its clients read no org data, and stopping or
595
+ // listing a session is never stranded by a config that fails (AG-3a).
596
+ "sessions",
597
+ "agents",
584
598
  ]);
585
599
 
586
600
  function configCheckedCommand(command: string): boolean {
@@ -964,7 +978,7 @@ class DenCLI {
964
978
  break;
965
979
 
966
980
  case "doctor":
967
- process.exitCode = cmdDoctor(args.slice(1));
981
+ process.exitCode = await cmdDoctor(args.slice(1));
968
982
  break;
969
983
 
970
984
  case "service":
@@ -989,6 +1003,14 @@ class DenCLI {
989
1003
  cmdApps(args.slice(1));
990
1004
  break;
991
1005
 
1006
+ case "sessions":
1007
+ process.exitCode = await cmdSessions(args.slice(1));
1008
+ break;
1009
+
1010
+ case "agents":
1011
+ process.exitCode = await cmdAgents(args.slice(1));
1012
+ break;
1013
+
992
1014
  case "mcp": {
993
1015
  // Setup subcommands: register the server with agents.
994
1016
  const { MCP_CONNECT_SUBCOMMANDS, runMcpConnect } =
@@ -4805,6 +4827,16 @@ class DenCLI {
4805
4827
  console.log(
4806
4828
  " plugin list Local and added plugins, checked (validate, add, update, remove)",
4807
4829
  );
4830
+ console.log(
4831
+ " sessions start -w <ws> --name <n> -- <argv…> A terminal that outlives the web server",
4832
+ );
4833
+ console.log(
4834
+ " sessions list|attach|kill List, attach to (ctrl-] detaches) or stop sessions",
4835
+ );
4836
+ console.log(" agents list -w <ws> The workspace's agents");
4837
+ console.log(
4838
+ " agents start -w <ws> --persona <p> Run a persona in the workspace (stop, resume, personas)",
4839
+ );
4808
4840
  console.log();
4809
4841
  console.log(
4810
4842
  " workspace list List workspaces (active set + focus)",