@monster-orange/den 0.4.5 → 0.4.7
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 +140 -0
- package/README.md +21 -7
- package/docs/distributions.md +221 -0
- package/package.json +4 -3
- package/skills/install-den/SKILL.md +114 -0
- package/src/shared/agent-setup.ts +9 -0
- package/src/shared/agents/den-claude.ts +19 -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 +55 -6
- package/src/shared/constants.ts +7 -0
- package/src/shared/den-config.ts +67 -6
- package/src/shared/dev-metrics/emit.ts +59 -5
- package/src/shared/dev-metrics/polling.ts +135 -0
- package/src/shared/engine.ts +14 -2
- package/src/shared/port-pick.ts +60 -0
- package/src/ui/cli/agent-ops.ts +7 -3
- package/src/ui/cli/agents-command.ts +51 -1
- package/src/ui/cli/create-main.ts +1 -1
- package/src/ui/cli/create.ts +263 -30
- package/src/ui/cli/doctor-command.ts +87 -4
- package/src/ui/cli/doctor-onboard.ts +291 -0
- package/src/ui/cli/flag-parser.ts +5 -0
- package/src/ui/cli/index.ts +352 -5
- package/src/ui/cli/json-out.ts +49 -0
- package/src/ui/cli/mcp-connect.ts +179 -15
- package/src/ui/cli/repo-command.ts +490 -0
- package/src/ui/cli/repo-detect.ts +192 -0
- package/src/ui/cli/web-command.ts +3 -0
- package/src/ui/mcp/connect.ts +134 -0
- package/src/ui/server/public/settings-registry.js +3 -3
- package/src/ui/server/request-guard.ts +1 -1
|
@@ -0,0 +1,291 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `doctor --json`'s onboarding checks (ON-2): what an agent setting Den up
|
|
3
|
+
* needs to know, each `ok`, `warn` or `fail`, with a command the agent can
|
|
4
|
+
* run (`fix`) or a question for the person (`ask`).
|
|
5
|
+
*
|
|
6
|
+
* - node: Node 22.13 or later.
|
|
7
|
+
* - git: on PATH.
|
|
8
|
+
* - docker: only when a registered repo needs it (a compose file or block,
|
|
9
|
+
* or a docker command), then installed and its daemon answering.
|
|
10
|
+
* - web: the web server answers at its URL (and is this distribution's).
|
|
11
|
+
* - mcp: Den's MCP one-shot (`<cli> mcp --call workspaces`) gets an answer
|
|
12
|
+
* from that web server: the path every agent's tools take.
|
|
13
|
+
*
|
|
14
|
+
* Every probe is injected (OnboardDeps), so tests never depend on this
|
|
15
|
+
* machine's Docker, Node or web server. defaultOnboardDeps() wires the real
|
|
16
|
+
* ones; only the docker and mcp probes spawn, each bounded by a timeout.
|
|
17
|
+
*/
|
|
18
|
+
import { spawnSync } from "child_process";
|
|
19
|
+
import * as fs from "fs";
|
|
20
|
+
import * as path from "path";
|
|
21
|
+
import { product } from "../../shared/product.js";
|
|
22
|
+
import { getDistRoot } from "../../shared/paths.js";
|
|
23
|
+
import { readManifest } from "../../shared/manifest/read-manifest.js";
|
|
24
|
+
import {
|
|
25
|
+
DEN_SERVICES_DIR,
|
|
26
|
+
readWorkspacesFile,
|
|
27
|
+
} from "../../shared/environment-manager/workspaces.js";
|
|
28
|
+
import { readWebPort } from "../../shared/edge/config.js";
|
|
29
|
+
import { DEFAULT_PORT } from "../../shared/constants.js";
|
|
30
|
+
import { needsDocker } from "./repo-detect.js";
|
|
31
|
+
import type { WebProbe } from "./mcp-connect.js";
|
|
32
|
+
|
|
33
|
+
export type CheckStatus = "ok" | "warn" | "fail";
|
|
34
|
+
|
|
35
|
+
export interface OnboardCheck {
|
|
36
|
+
id: string;
|
|
37
|
+
status: CheckStatus;
|
|
38
|
+
detail: string;
|
|
39
|
+
/** A command the agent can run to fix it. */
|
|
40
|
+
fix?: string;
|
|
41
|
+
/** A question for the person, when only they can fix it. */
|
|
42
|
+
ask?: string;
|
|
43
|
+
/** The web server's URL and port (the web check). */
|
|
44
|
+
url?: string;
|
|
45
|
+
port?: number;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export interface OnboardDeps {
|
|
49
|
+
/** process.versions.node */
|
|
50
|
+
nodeVersion: string;
|
|
51
|
+
platform: NodeJS.Platform;
|
|
52
|
+
/** Whether an executable is on PATH. */
|
|
53
|
+
which: (bin: string) => boolean;
|
|
54
|
+
/** The registered repos that need Docker. */
|
|
55
|
+
dockerRepos: () => string[];
|
|
56
|
+
/** Whether the Docker daemon answers (`docker info`). */
|
|
57
|
+
dockerUp: () => boolean;
|
|
58
|
+
webUrl: string;
|
|
59
|
+
probeWeb: (url: string) => Promise<WebProbe>;
|
|
60
|
+
/** The MCP one-shot against the web server: exit 0, or why not. */
|
|
61
|
+
mcpCall: () => { ok: boolean; detail?: string };
|
|
62
|
+
/** The command an agent runs, e.g. `./bin/acme`. */
|
|
63
|
+
bin: string;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export const MIN_NODE = [22, 13] as const;
|
|
67
|
+
|
|
68
|
+
/** Whether `version` ("22.13.0", "v24.1.0") is at least 22.13. */
|
|
69
|
+
export function nodeOk(version: string): boolean {
|
|
70
|
+
const [maj, min] = version.replace(/^v/, "").split(".").map(Number);
|
|
71
|
+
if (!Number.isFinite(maj)) return false;
|
|
72
|
+
return (
|
|
73
|
+
maj > MIN_NODE[0] || (maj === MIN_NODE[0] && (min ?? 0) >= MIN_NODE[1])
|
|
74
|
+
);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export async function onboardingChecks(
|
|
78
|
+
d: OnboardDeps,
|
|
79
|
+
): Promise<OnboardCheck[]> {
|
|
80
|
+
const out: OnboardCheck[] = [];
|
|
81
|
+
const mac = d.platform === "darwin";
|
|
82
|
+
|
|
83
|
+
out.push(
|
|
84
|
+
nodeOk(d.nodeVersion)
|
|
85
|
+
? { id: "node", status: "ok", detail: `Node ${d.nodeVersion}` }
|
|
86
|
+
: {
|
|
87
|
+
id: "node",
|
|
88
|
+
status: "fail",
|
|
89
|
+
detail: `Node ${d.nodeVersion} is older than ${MIN_NODE.join(".")}`,
|
|
90
|
+
fix: "nvm install 22 && nvm use 22",
|
|
91
|
+
ask: `Den needs Node ${MIN_NODE.join(".")} or later. May I install it with your usual Node installer (nvm, or brew install node)?`,
|
|
92
|
+
},
|
|
93
|
+
);
|
|
94
|
+
|
|
95
|
+
out.push(
|
|
96
|
+
d.which("git")
|
|
97
|
+
? { id: "git", status: "ok", detail: "git is on PATH" }
|
|
98
|
+
: {
|
|
99
|
+
id: "git",
|
|
100
|
+
status: "fail",
|
|
101
|
+
detail: "git is not on PATH",
|
|
102
|
+
...(mac ? { fix: "xcode-select --install" } : {}),
|
|
103
|
+
ask: "Den needs git. May I install it (on macOS: xcode-select --install)?",
|
|
104
|
+
},
|
|
105
|
+
);
|
|
106
|
+
|
|
107
|
+
const repos = d.dockerRepos();
|
|
108
|
+
if (repos.length === 0)
|
|
109
|
+
out.push({
|
|
110
|
+
id: "docker",
|
|
111
|
+
status: "ok",
|
|
112
|
+
detail: "no registered repo needs Docker",
|
|
113
|
+
});
|
|
114
|
+
else if (!d.which("docker"))
|
|
115
|
+
out.push({
|
|
116
|
+
id: "docker",
|
|
117
|
+
status: "fail",
|
|
118
|
+
detail: `${repos.join(", ")} need${repos.length === 1 ? "s" : ""} Docker, and docker is not on PATH`,
|
|
119
|
+
ask: "These repos run in containers. Can you install Docker Desktop (or another Docker engine)?",
|
|
120
|
+
});
|
|
121
|
+
else if (!d.dockerUp())
|
|
122
|
+
out.push({
|
|
123
|
+
id: "docker",
|
|
124
|
+
status: "fail",
|
|
125
|
+
detail: `${repos.join(", ")} need${repos.length === 1 ? "s" : ""} Docker, and the Docker daemon is not answering`,
|
|
126
|
+
...(mac ? { fix: "open -a Docker" } : {}),
|
|
127
|
+
ask: "Docker is installed but not running. Can you start it?",
|
|
128
|
+
});
|
|
129
|
+
else
|
|
130
|
+
out.push({
|
|
131
|
+
id: "docker",
|
|
132
|
+
status: "ok",
|
|
133
|
+
detail: `Docker is running (needed by ${repos.join(", ")})`,
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
const probe = await d.probeWeb(d.webUrl);
|
|
137
|
+
const webPort = Number(new URL(d.webUrl).port) || 80;
|
|
138
|
+
const webFix = `${d.bin} web up --no-open --json`;
|
|
139
|
+
if (probe.kind === "up")
|
|
140
|
+
out.push({
|
|
141
|
+
id: "web",
|
|
142
|
+
status: "ok",
|
|
143
|
+
detail: `web server up${probe.version ? ` (Den ${probe.version})` : ""}`,
|
|
144
|
+
url: d.webUrl,
|
|
145
|
+
port: webPort,
|
|
146
|
+
});
|
|
147
|
+
else if (probe.kind === "foreign")
|
|
148
|
+
out.push({
|
|
149
|
+
id: "web",
|
|
150
|
+
status: "fail",
|
|
151
|
+
detail: `${probe.who} answers at ${d.webUrl}, not this distribution's web server`,
|
|
152
|
+
url: d.webUrl,
|
|
153
|
+
port: webPort,
|
|
154
|
+
ask: `Another Den distribution already uses ${d.webUrl}. Which port should ${product().title} use instead? (web.port in config/local/user.yml)`,
|
|
155
|
+
});
|
|
156
|
+
else
|
|
157
|
+
out.push({
|
|
158
|
+
id: "web",
|
|
159
|
+
status: "fail",
|
|
160
|
+
detail:
|
|
161
|
+
probe.kind === "timeout"
|
|
162
|
+
? `web server at ${d.webUrl} is not answering in time`
|
|
163
|
+
: `web server not running at ${d.webUrl}`,
|
|
164
|
+
url: d.webUrl,
|
|
165
|
+
port: webPort,
|
|
166
|
+
fix:
|
|
167
|
+
probe.kind === "timeout"
|
|
168
|
+
? `${d.bin} web restart --no-open --json`
|
|
169
|
+
: webFix,
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
if (probe.kind !== "up")
|
|
173
|
+
out.push({
|
|
174
|
+
id: "mcp",
|
|
175
|
+
status: "fail",
|
|
176
|
+
detail: "Den's MCP reads through the web server, which is not up",
|
|
177
|
+
fix: webFix,
|
|
178
|
+
});
|
|
179
|
+
else {
|
|
180
|
+
const m = d.mcpCall();
|
|
181
|
+
out.push(
|
|
182
|
+
m.ok
|
|
183
|
+
? {
|
|
184
|
+
id: "mcp",
|
|
185
|
+
status: "ok",
|
|
186
|
+
detail: `${d.bin} mcp --call workspaces answered`,
|
|
187
|
+
}
|
|
188
|
+
: {
|
|
189
|
+
id: "mcp",
|
|
190
|
+
status: "fail",
|
|
191
|
+
detail: `${d.bin} mcp --call workspaces failed${m.detail ? `: ${m.detail}` : ""}`,
|
|
192
|
+
fix: `${d.bin} web restart --no-open --json`,
|
|
193
|
+
},
|
|
194
|
+
);
|
|
195
|
+
}
|
|
196
|
+
return out;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
function onPath(bin: string): boolean {
|
|
200
|
+
for (const dir of (process.env.PATH ?? "").split(path.delimiter)) {
|
|
201
|
+
if (!dir) continue;
|
|
202
|
+
try {
|
|
203
|
+
fs.accessSync(path.join(dir, bin), fs.constants.X_OK);
|
|
204
|
+
return true;
|
|
205
|
+
} catch {
|
|
206
|
+
/* next */
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
return false;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/** The repos in every active workspace whose manifest or files need Docker. */
|
|
213
|
+
export function registeredDockerRepos(): string[] {
|
|
214
|
+
const out: string[] = [];
|
|
215
|
+
let file;
|
|
216
|
+
try {
|
|
217
|
+
file = readWorkspacesFile();
|
|
218
|
+
} catch {
|
|
219
|
+
return out;
|
|
220
|
+
}
|
|
221
|
+
for (const ws of file.active) {
|
|
222
|
+
for (const [addr, entry] of Object.entries(
|
|
223
|
+
file.workspaces[ws]?.services ?? {},
|
|
224
|
+
)) {
|
|
225
|
+
const repo = addr.split("/")[0];
|
|
226
|
+
if (out.includes(repo)) continue;
|
|
227
|
+
const dir =
|
|
228
|
+
entry.mode === "dev-managed"
|
|
229
|
+
? entry.path
|
|
230
|
+
: path.join(DEN_SERVICES_DIR, repo, entry.branch ?? "main");
|
|
231
|
+
if (!dir) continue;
|
|
232
|
+
if (needsDocker(readManifest(dir), fs.existsSync(dir) ? dir : undefined))
|
|
233
|
+
out.push(repo);
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
return out;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/** The command an agent runs from the distribution root. */
|
|
240
|
+
export function runnableCli(): string {
|
|
241
|
+
const cli = product().cli;
|
|
242
|
+
return fs.existsSync(path.join(getDistRoot(), "bin", cli))
|
|
243
|
+
? `./bin/${cli}`
|
|
244
|
+
: cli;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
export function defaultOnboardDeps(
|
|
248
|
+
probeWeb: (url: string) => Promise<WebProbe>,
|
|
249
|
+
): OnboardDeps {
|
|
250
|
+
let port: number;
|
|
251
|
+
try {
|
|
252
|
+
port = readWebPort();
|
|
253
|
+
} catch {
|
|
254
|
+
port = DEFAULT_PORT;
|
|
255
|
+
}
|
|
256
|
+
const cli = product().cli;
|
|
257
|
+
const shim = path.join(getDistRoot(), "bin", cli);
|
|
258
|
+
return {
|
|
259
|
+
nodeVersion: process.versions.node,
|
|
260
|
+
platform: process.platform,
|
|
261
|
+
which: onPath,
|
|
262
|
+
dockerRepos: registeredDockerRepos,
|
|
263
|
+
dockerUp: () =>
|
|
264
|
+
spawnSync("docker", ["info", "--format", "{{.ServerVersion}}"], {
|
|
265
|
+
stdio: "ignore",
|
|
266
|
+
timeout: 10_000,
|
|
267
|
+
}).status === 0,
|
|
268
|
+
webUrl: `http://localhost:${port}`,
|
|
269
|
+
probeWeb,
|
|
270
|
+
mcpCall: () => {
|
|
271
|
+
// The shim when there is one (a distribution), else this Den's bin.
|
|
272
|
+
const cmd = fs.existsSync(shim)
|
|
273
|
+
? shim
|
|
274
|
+
: path.join(getDistRoot(), "bin", "den");
|
|
275
|
+
const r = spawnSync(cmd, ["mcp", "--call", "workspaces"], {
|
|
276
|
+
encoding: "utf-8",
|
|
277
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
278
|
+
timeout: 30_000,
|
|
279
|
+
});
|
|
280
|
+
return r.status === 0
|
|
281
|
+
? { ok: true }
|
|
282
|
+
: {
|
|
283
|
+
ok: false,
|
|
284
|
+
detail:
|
|
285
|
+
(r.stderr ?? "").trim().split("\n").pop()?.slice(0, 200) ||
|
|
286
|
+
`exit ${r.status ?? "timeout"}`,
|
|
287
|
+
};
|
|
288
|
+
},
|
|
289
|
+
bin: runnableCli(),
|
|
290
|
+
};
|
|
291
|
+
}
|
|
@@ -88,6 +88,8 @@ const OWN_WORKSPACE_FLAG_COMMANDS = new Set([
|
|
|
88
88
|
// CLI's workspace override here.
|
|
89
89
|
"sessions",
|
|
90
90
|
"agents",
|
|
91
|
+
// `repo add -w <ws>` names (or creates) the workspace to write.
|
|
92
|
+
"repo",
|
|
91
93
|
]);
|
|
92
94
|
|
|
93
95
|
/** Whether `-w <name>` on this command requires `<name>` to be active. */
|
|
@@ -181,6 +183,7 @@ export const SUBCOMMAND_OWN_HELP: Readonly<Record<string, readonly string[]>> =
|
|
|
181
183
|
{
|
|
182
184
|
web: ["token", "login", "sessions", "logout", "owner", "approve"],
|
|
183
185
|
mcp: ["connect", "disconnect", "clients", "config"],
|
|
186
|
+
repo: ["add"],
|
|
184
187
|
};
|
|
185
188
|
|
|
186
189
|
/** Whether `<command> <sub> --help` is the subcommand's own to answer. */
|
|
@@ -209,8 +212,10 @@ const COMMAND_FLAGS: Record<string, readonly string[]> = {
|
|
|
209
212
|
"--pull",
|
|
210
213
|
"-A",
|
|
211
214
|
"--app",
|
|
215
|
+
"--json",
|
|
212
216
|
],
|
|
213
217
|
down: [
|
|
218
|
+
"--json",
|
|
214
219
|
"--manifest",
|
|
215
220
|
"--no-derive",
|
|
216
221
|
"-a",
|