@monster-orange/den 0.3.0 → 0.4.1
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 +593 -0
- package/docs/distributions.md +429 -18
- package/package.json +6 -1
- package/src/shared/actor-token.ts +275 -0
- package/src/shared/agent-actor.ts +12 -2
- package/src/shared/agents/default-pack/be-dev.md +20 -0
- package/src/shared/agents/default-pack/devops.md +14 -0
- package/src/shared/agents/default-pack/fe-dev.md +20 -0
- package/src/shared/agents/default-pack/orchestrator.md +16 -0
- package/src/shared/agents/default-pack/planner.md +17 -0
- package/src/shared/agents/default-pack/reviewer.md +14 -0
- package/src/shared/agents/default-pack/ux-designer.md +15 -0
- package/src/shared/agents/den-claude.ts +271 -0
- package/src/shared/agents/generic.ts +52 -0
- package/src/shared/agents/personas.ts +218 -0
- package/src/shared/agents/records.ts +118 -0
- package/src/shared/agents/runtime.ts +199 -0
- package/src/shared/agents/runtimes.ts +54 -0
- package/src/shared/den-config.ts +116 -0
- package/src/shared/engine.ts +23 -0
- package/src/shared/environment-manager/scope.ts +22 -0
- package/src/shared/environment-manager/service-runner.ts +94 -2
- package/src/shared/environment-manager/workspaces.ts +19 -0
- package/src/shared/profile.ts +15 -4
- package/src/shared/secret-env.ts +70 -0
- package/src/shared/sessions/client.ts +284 -0
- package/src/shared/sessions/den-pty.ts +97 -0
- package/src/shared/sessions/env.ts +89 -0
- package/src/shared/sessions/host.ts +690 -0
- package/src/shared/sessions/names.ts +85 -0
- package/src/shared/sessions/paths.ts +27 -0
- package/src/shared/sessions/protocol.ts +72 -0
- package/src/shared/sessions/pty.ts +141 -0
- package/src/shared/sessions/ring-buffer.ts +160 -0
- package/src/shared/sessions/types.ts +81 -0
- package/src/shared/web-secret.ts +181 -0
- package/src/shared/web-session.ts +491 -0
- package/src/ui/cli/agent-ops.ts +843 -0
- package/src/ui/cli/agents-command.ts +294 -0
- package/src/ui/cli/doctor-command.ts +138 -9
- package/src/ui/cli/env-probe.ts +266 -0
- package/src/ui/cli/flag-parser.ts +32 -3
- package/src/ui/cli/index.ts +128 -12
- package/src/ui/cli/sessions-command.ts +750 -0
- package/src/ui/cli/target-workspace.ts +105 -0
- package/src/ui/cli/web-command.ts +536 -14
- package/src/ui/mcp/api.ts +7 -0
- package/src/ui/mcp/scope.ts +123 -19
- package/src/ui/mcp/server.ts +100 -7
- package/src/ui/mcp/tools.ts +44 -5
- package/src/ui/server/agent-routes.ts +323 -0
- package/src/ui/server/audit.ts +111 -22
- package/src/ui/server/http-errors.ts +21 -3
- package/src/ui/server/index.ts +820 -65
- package/src/ui/server/public/agents-page.js +648 -0
- package/src/ui/server/public/agents-section.js +299 -0
- package/src/ui/server/public/agents.html +439 -0
- package/src/ui/server/public/appearance.js +3 -0
- package/src/ui/server/public/error-page.js +103 -0
- package/src/ui/server/public/header.js +45 -11
- package/src/ui/server/public/i18n/es.js +90 -1
- package/src/ui/server/public/i18n/ja.js +86 -1
- package/src/ui/server/public/local-dev.html +53 -0
- package/src/ui/server/public/logs.html +3 -0
- package/src/ui/server/public/panel.js +1 -1
- package/src/ui/server/public/release.html +8 -0
- package/src/ui/server/public/settings-page.js +5 -0
- package/src/ui/server/public/settings-registry.js +33 -0
- package/src/ui/server/public/shell.js +248 -5
- package/src/ui/server/public/topbar.js +51 -2
- package/src/ui/server/readonly-routes.ts +25 -2
- package/src/ui/server/request-guard.ts +670 -0
- package/src/ui/server/route-classes.ts +317 -0
- package/src/ui/server/session-routes.ts +445 -0
- package/src/ui/server/shutdown-guard.ts +91 -0
- package/src/ui/server/sudo-store.ts +29 -3
- package/src/ui/server/ui-assets.ts +10 -1
- package/src/ui/server/websocket.ts +326 -0
- package/src/ui/server/workspace-routes.ts +16 -22
|
@@ -0,0 +1,294 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `<cli> agents list|start|stop|resume|personas` (AG-2a, AG spec §5c):
|
|
3
|
+
* an agent is a persona and a workspace, run by a runtime in a session of
|
|
4
|
+
* the AG-3a host. This is the CLI's front: it parses the arguments and
|
|
5
|
+
* prints; what each operation does is agent-ops.ts, which the Workspaces
|
|
6
|
+
* page's routes (server/agent-routes.ts) call too.
|
|
7
|
+
*
|
|
8
|
+
* start resolves the persona (Den's default pack, then
|
|
9
|
+
* agents.persona_packs) and the runtime, asks the runtime for argv,
|
|
10
|
+
* env and files (its MCP config: `<cli> mcp --workspace <ws>
|
|
11
|
+
* --actor agent:<id>`, plus a `--grant` per agents.<id>.grants),
|
|
12
|
+
* starts `<dist>-<ws>:<id>` in the workspace's dir, and writes the
|
|
13
|
+
* agent record (<stateDir>/agents/<id>.json, 0600; no env, no argv).
|
|
14
|
+
* The agent's env is an allowlist plus what its runtime declares,
|
|
15
|
+
* less every runtime's scrubEnv (sessions/env.ts), never the
|
|
16
|
+
* caller's whole environment; no --keep-env. A
|
|
17
|
+
* session that exits non-zero in its first moments (exitWatchMs)
|
|
18
|
+
* is reported with its last lines, exit 1.
|
|
19
|
+
* list the workspace's agents (sessions with an agent record), with
|
|
20
|
+
* persona, runtime and model; a record whose session is gone is
|
|
21
|
+
* listed as stopped or ended. A plain session is not an agent.
|
|
22
|
+
* stop ends its session (TERM) and marks the record stopped.
|
|
23
|
+
* resume only when its runtime declares resume and its session is not
|
|
24
|
+
* running: a new session, in the same cwd, continuing the last run;
|
|
25
|
+
* when that session was never prompted, a fresh start under the
|
|
26
|
+
* same conversation id (there is nothing to continue).
|
|
27
|
+
*
|
|
28
|
+
* Nothing here takes a grant: grants come from the org config only. The
|
|
29
|
+
* host enforces agents.max_concurrent on every start.
|
|
30
|
+
*/
|
|
31
|
+
import { DEFAULT_RUNTIME_ID } from "../../shared/agents/runtimes.js";
|
|
32
|
+
import { HostError } from "../../shared/sessions/client.js";
|
|
33
|
+
import {
|
|
34
|
+
AgentError,
|
|
35
|
+
listAgents,
|
|
36
|
+
listPersonas as personaList,
|
|
37
|
+
loadAgentWorkspaces,
|
|
38
|
+
resolveAgentsDeps,
|
|
39
|
+
resumeAgent,
|
|
40
|
+
startAgent,
|
|
41
|
+
stopAgent,
|
|
42
|
+
type AgentsDeps,
|
|
43
|
+
type AgentsResolved,
|
|
44
|
+
type EarlyExit,
|
|
45
|
+
type Started,
|
|
46
|
+
} from "./agent-ops.js";
|
|
47
|
+
import {
|
|
48
|
+
columns,
|
|
49
|
+
guarded,
|
|
50
|
+
noExtra,
|
|
51
|
+
parseArgs,
|
|
52
|
+
UsageError,
|
|
53
|
+
workspaceFlag,
|
|
54
|
+
} from "./sessions-command.js";
|
|
55
|
+
|
|
56
|
+
export {
|
|
57
|
+
agentMcp,
|
|
58
|
+
agentRows,
|
|
59
|
+
EXIT_WATCH_MS,
|
|
60
|
+
plainLines,
|
|
61
|
+
type AgentRow,
|
|
62
|
+
type AgentsDeps,
|
|
63
|
+
} from "./agent-ops.js";
|
|
64
|
+
|
|
65
|
+
export const AGENTS_HELP = {
|
|
66
|
+
usage: "agents <list|start|stop|resume|personas> ...",
|
|
67
|
+
lines: [
|
|
68
|
+
"Agents: a persona and a workspace, run by a runtime in a session that",
|
|
69
|
+
"outlives the web server (see `{cli} sessions`).",
|
|
70
|
+
"",
|
|
71
|
+
"{cli} agents list -w <ws> [--json]",
|
|
72
|
+
"{cli} agents start -w <ws> --persona <p> [--runtime <r>] [--model <m>] [--id <id>]",
|
|
73
|
+
" runs in the workspace's dir as <dist>-<ws>:<id>; its MCP is pinned to <ws>",
|
|
74
|
+
" (plus agents.<id>.grants); capped by agents.max_concurrent",
|
|
75
|
+
"{cli} agents stop <id> [-w <ws>]",
|
|
76
|
+
"{cli} agents resume <id> [-w <ws>]",
|
|
77
|
+
" only when its runtime declares resume and its session has ended;",
|
|
78
|
+
" a session never prompted starts fresh under the same conversation id",
|
|
79
|
+
"{cli} agents personas [--json]",
|
|
80
|
+
" Den's default pack, then agents.persona_packs (a later pack wins by id)",
|
|
81
|
+
"",
|
|
82
|
+
"Runtimes: den-claude (default; needs claude on PATH) and generic (the",
|
|
83
|
+
"persona's argv template). Attach with `{cli} sessions attach <ws>:<id>`.",
|
|
84
|
+
"An agent's env is PATH, HOME, USER, LOGNAME, SHELL, TERM, COLORTERM, LANG,",
|
|
85
|
+
"LC_*, TZ and TMPDIR, plus what its runtime declares: nothing else of yours.",
|
|
86
|
+
],
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
async function list(args: string[], d: AgentsResolved): Promise<number> {
|
|
90
|
+
const where = "agents list";
|
|
91
|
+
const p = parseArgs(where, args, { "--workspace": true, "--json": false });
|
|
92
|
+
noExtra(where, p);
|
|
93
|
+
const ws = workspaceFlag(where, p);
|
|
94
|
+
if (!ws) throw new UsageError(`${where} needs -w <workspace>`);
|
|
95
|
+
const rows = await listAgents(d, ws);
|
|
96
|
+
if (p.flags["--json"]) {
|
|
97
|
+
d.out(JSON.stringify(rows, null, 2));
|
|
98
|
+
return 0;
|
|
99
|
+
}
|
|
100
|
+
if (!rows.length) {
|
|
101
|
+
d.out(`no agents in ${ws}`);
|
|
102
|
+
return 0;
|
|
103
|
+
}
|
|
104
|
+
for (const l of columns([
|
|
105
|
+
[
|
|
106
|
+
"AGENT",
|
|
107
|
+
"PERSONA",
|
|
108
|
+
"RUNTIME",
|
|
109
|
+
"STATE",
|
|
110
|
+
"MODEL",
|
|
111
|
+
"PID",
|
|
112
|
+
"STARTED",
|
|
113
|
+
"SESSION",
|
|
114
|
+
],
|
|
115
|
+
...rows.map((r) => [
|
|
116
|
+
r.agent,
|
|
117
|
+
r.persona ?? "-",
|
|
118
|
+
r.runtime ?? "-",
|
|
119
|
+
r.state,
|
|
120
|
+
r.model ?? "-",
|
|
121
|
+
r.session && !r.session.exited ? String(r.session.pid) : "-",
|
|
122
|
+
r.session?.startedAt ?? "-",
|
|
123
|
+
r.session?.name ?? "-",
|
|
124
|
+
]),
|
|
125
|
+
]))
|
|
126
|
+
d.out(l);
|
|
127
|
+
return 0;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/** An early exit, as the terminal shows it: its code and last lines. */
|
|
131
|
+
function printEarlyExit(d: AgentsResolved, id: string, e: EarlyExit): void {
|
|
132
|
+
const code = e.exitCode === undefined ? "" : ` (exit ${e.exitCode})`;
|
|
133
|
+
d.err(`✗ agent ${id} exited at once${code}`);
|
|
134
|
+
if (e.tail.length) {
|
|
135
|
+
d.err(` its last ${e.tail.length === 1 ? "line" : "lines"}:`);
|
|
136
|
+
for (const l of e.tail) d.err(` | ${l}`);
|
|
137
|
+
}
|
|
138
|
+
d.err(` All of it: ${d.cli} sessions attach ${e.sessionName}`);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/** A start's or resume's warnings and outcome; its exit code. */
|
|
142
|
+
function report(d: AgentsResolved, s: Started, line: string): number {
|
|
143
|
+
for (const w of s.warnings) d.err(w);
|
|
144
|
+
if (s.early) {
|
|
145
|
+
printEarlyExit(d, s.rec.id, s.early);
|
|
146
|
+
return 1;
|
|
147
|
+
}
|
|
148
|
+
d.out(line);
|
|
149
|
+
d.out(` attach: ${d.cli} sessions attach ${s.rec.sessionName}`);
|
|
150
|
+
return 0;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/** A flag's string value, or undefined. */
|
|
154
|
+
function str(
|
|
155
|
+
p: ReturnType<typeof parseArgs>,
|
|
156
|
+
flag: string,
|
|
157
|
+
): string | undefined {
|
|
158
|
+
const v = p.flags[flag];
|
|
159
|
+
return typeof v === "string" ? v : undefined;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
async function start(args: string[], d: AgentsResolved): Promise<number> {
|
|
163
|
+
const where = "agents start";
|
|
164
|
+
const p = parseArgs(where, args, {
|
|
165
|
+
"--workspace": true,
|
|
166
|
+
"--persona": true,
|
|
167
|
+
"--runtime": true,
|
|
168
|
+
"--model": true,
|
|
169
|
+
"--id": true,
|
|
170
|
+
});
|
|
171
|
+
noExtra(where, p);
|
|
172
|
+
const ws = workspaceFlag(where, p);
|
|
173
|
+
if (!ws) throw new UsageError(`${where} needs -w <workspace>`);
|
|
174
|
+
const persona = str(p, "--persona");
|
|
175
|
+
if (!persona) throw new UsageError(`${where} needs --persona <p>`);
|
|
176
|
+
const s = await startAgent(d, {
|
|
177
|
+
workspace: ws,
|
|
178
|
+
persona,
|
|
179
|
+
runtime: str(p, "--runtime"),
|
|
180
|
+
model: str(p, "--model"),
|
|
181
|
+
id: str(p, "--id"),
|
|
182
|
+
});
|
|
183
|
+
return report(
|
|
184
|
+
d,
|
|
185
|
+
s,
|
|
186
|
+
`started agent ${s.rec.id} (${s.rec.persona}, ${s.rec.runtime}) as ${s.rec.sessionName} in ${s.rec.cwd}`,
|
|
187
|
+
);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/** `<id> [-w <ws>]`. */
|
|
191
|
+
function idAndWorkspace(
|
|
192
|
+
where: string,
|
|
193
|
+
args: string[],
|
|
194
|
+
): { id: string; ws?: string } {
|
|
195
|
+
const p = parseArgs(where, args, { "--workspace": true });
|
|
196
|
+
noExtra(where, p, 1);
|
|
197
|
+
const id = p.positional[0];
|
|
198
|
+
if (!id) throw new UsageError(`${where} needs <id>`);
|
|
199
|
+
return { id, ws: workspaceFlag(where, p) };
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
async function stop(args: string[], d: AgentsResolved): Promise<number> {
|
|
203
|
+
const { id, ws } = idAndWorkspace("agents stop", args);
|
|
204
|
+
const { rec, session: s } = await stopAgent(d, id, ws);
|
|
205
|
+
d.out(
|
|
206
|
+
s
|
|
207
|
+
? `stopped agent ${rec.id} (sent TERM to ${s.name}, pid ${s.pid})`
|
|
208
|
+
: `agent ${rec.id} was not running; marked stopped`,
|
|
209
|
+
);
|
|
210
|
+
return 0;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
async function resume(args: string[], d: AgentsResolved): Promise<number> {
|
|
214
|
+
const { id, ws } = idAndWorkspace("agents resume", args);
|
|
215
|
+
const s = await resumeAgent(d, id, ws);
|
|
216
|
+
if (s.fresh)
|
|
217
|
+
d.out(
|
|
218
|
+
`agent ${s.rec.id}: starting fresh: it was never prompted, so there is no conversation to resume`,
|
|
219
|
+
);
|
|
220
|
+
return report(
|
|
221
|
+
d,
|
|
222
|
+
s,
|
|
223
|
+
`${s.fresh ? "started" : "resumed"} agent ${s.rec.id} as ${s.rec.sessionName} in ${s.rec.cwd}`,
|
|
224
|
+
);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
async function listPersonas(
|
|
228
|
+
args: string[],
|
|
229
|
+
d: AgentsResolved,
|
|
230
|
+
): Promise<number> {
|
|
231
|
+
const where = "agents personas";
|
|
232
|
+
const p = parseArgs(where, args, { "--json": false });
|
|
233
|
+
noExtra(where, p);
|
|
234
|
+
const all = personaList(d);
|
|
235
|
+
if (p.flags["--json"]) {
|
|
236
|
+
d.out(JSON.stringify(all, null, 2));
|
|
237
|
+
return 0;
|
|
238
|
+
}
|
|
239
|
+
for (const l of columns([
|
|
240
|
+
["PERSONA", "STANDING", "RUNTIME", "MODEL", "ROLE"],
|
|
241
|
+
...all.map((x) => [
|
|
242
|
+
x.id,
|
|
243
|
+
x.standing ? "yes" : "-",
|
|
244
|
+
x.runtime ?? DEFAULT_RUNTIME_ID,
|
|
245
|
+
x.model ?? "-",
|
|
246
|
+
x.role,
|
|
247
|
+
]),
|
|
248
|
+
]))
|
|
249
|
+
d.out(l);
|
|
250
|
+
return 0;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
/** `<cli> agents ...`; returns the exit code. */
|
|
254
|
+
export async function cmdAgents(
|
|
255
|
+
args: string[],
|
|
256
|
+
deps: AgentsDeps = {},
|
|
257
|
+
): Promise<number> {
|
|
258
|
+
const d = resolveAgentsDeps(deps);
|
|
259
|
+
const sub =
|
|
260
|
+
args[0] === undefined || args[0].startsWith("-") ? "list" : args[0];
|
|
261
|
+
const rest = sub === args[0] ? args.slice(1) : args;
|
|
262
|
+
return guarded(d, AGENTS_HELP, "agents", async () => {
|
|
263
|
+
try {
|
|
264
|
+
switch (sub) {
|
|
265
|
+
case "list":
|
|
266
|
+
return await list(rest, d);
|
|
267
|
+
case "start":
|
|
268
|
+
await loadAgentWorkspaces(deps);
|
|
269
|
+
return await start(rest, d);
|
|
270
|
+
case "stop":
|
|
271
|
+
return await stop(rest, d);
|
|
272
|
+
case "resume":
|
|
273
|
+
await loadAgentWorkspaces(deps);
|
|
274
|
+
return await resume(rest, d);
|
|
275
|
+
case "personas":
|
|
276
|
+
return await listPersonas(rest, d);
|
|
277
|
+
default:
|
|
278
|
+
throw new UsageError(`unknown agents command "${sub}"`);
|
|
279
|
+
}
|
|
280
|
+
} catch (e) {
|
|
281
|
+
if (e instanceof AgentError) {
|
|
282
|
+
if (e.usage) throw new UsageError(e.message);
|
|
283
|
+
d.err(`✗ ${e.message}`);
|
|
284
|
+
for (const h of e.hints) d.err(` ${h}`);
|
|
285
|
+
return 1;
|
|
286
|
+
}
|
|
287
|
+
if (e instanceof HostError) {
|
|
288
|
+
d.err(`✗ ${e.message}`);
|
|
289
|
+
return 1;
|
|
290
|
+
}
|
|
291
|
+
throw e;
|
|
292
|
+
}
|
|
293
|
+
});
|
|
294
|
+
}
|
|
@@ -1,13 +1,26 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* `doctor`: every strategy kind in the registry (secrets, tls
|
|
3
|
-
* strategy this distribution selects for it, whether
|
|
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
|
|
7
|
-
*
|
|
8
|
-
*
|
|
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 {
|
|
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
|
-
|
|
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
|
-
|
|
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
|
}
|