@monster-orange/den 0.4.6 → 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 +66 -0
- package/README.md +7 -7
- package/docs/distributions.md +124 -0
- package/package.json +1 -1
- package/skills/install-den/SKILL.md +2 -2
- 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/den-config.ts +67 -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/ui/cli/agent-ops.ts +7 -3
- package/src/ui/cli/agents-command.ts +51 -1
- package/src/ui/cli/doctor-command.ts +20 -0
- package/src/ui/cli/flag-parser.ts +1 -0
- package/src/ui/cli/index.ts +106 -1
- package/src/ui/cli/web-command.ts +3 -1
package/src/shared/engine.ts
CHANGED
|
@@ -93,11 +93,23 @@ export function playwrightCommand(...args: string[]): EngineCommand {
|
|
|
93
93
|
* or with DEN_USE_DIST=1. Mirrors the web server's resolveServerEntrypoint.
|
|
94
94
|
*/
|
|
95
95
|
export function cliEntrypoint(engine: string = getEngineRoot()): EngineCommand {
|
|
96
|
-
|
|
96
|
+
return scriptEntrypoint("src/ui/cli/index.ts", engine);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* How to run one of Den's TypeScript entry points (`src/….ts`, relative to
|
|
101
|
+
* the engine root) as a node process: cliEntrypoint's rules, for any
|
|
102
|
+
* script (an agent's hook, guardrails/stop-hook.ts).
|
|
103
|
+
*/
|
|
104
|
+
export function scriptEntrypoint(
|
|
105
|
+
rel: string,
|
|
106
|
+
engine: string = getEngineRoot(),
|
|
107
|
+
): EngineCommand {
|
|
108
|
+
const built = path.join(engine, "dist", rel.replace(/\.ts$/, ".js"));
|
|
97
109
|
if (process.env.DEN_USE_DIST === "1" && fs.existsSync(built))
|
|
98
110
|
return { command: process.execPath, args: [built] };
|
|
99
111
|
const loader = engineModuleFile("tsx/dist/loader.mjs", engine);
|
|
100
|
-
const src = path.join(engine,
|
|
112
|
+
const src = path.join(engine, rel);
|
|
101
113
|
if (loader && fs.existsSync(src))
|
|
102
114
|
return {
|
|
103
115
|
command: process.execPath,
|
package/src/ui/cli/agent-ops.ts
CHANGED
|
@@ -437,20 +437,24 @@ function personas(
|
|
|
437
437
|
cfg: ResolvedAgentsConfig,
|
|
438
438
|
): Map<string, Persona> {
|
|
439
439
|
try {
|
|
440
|
-
return loadPersonas(
|
|
440
|
+
return loadPersonas(
|
|
441
|
+
resolvePackDirs(cfg.personaPacks, d.distRoot()),
|
|
442
|
+
undefined,
|
|
443
|
+
{ defaultPack: cfg.defaultPackGuardrails, packs: cfg.packGuardrails },
|
|
444
|
+
);
|
|
441
445
|
} catch (e) {
|
|
442
446
|
throw refused((e as Error).message, [], 422);
|
|
443
447
|
}
|
|
444
448
|
}
|
|
445
449
|
|
|
446
450
|
/** A persona as listed: everything but its role prompt. */
|
|
447
|
-
export type ListedPersona = Omit<Persona, "prompt">;
|
|
451
|
+
export type ListedPersona = Omit<Persona, "prompt" | "guardrailsSet">;
|
|
448
452
|
|
|
449
453
|
/** Den's default pack, then agents.persona_packs, by id. */
|
|
450
454
|
export function listPersonas(d: AgentsResolved): ListedPersona[] {
|
|
451
455
|
return [...personas(d, agentsConfig(d)).values()]
|
|
452
456
|
.sort((a, b) => a.id.localeCompare(b.id))
|
|
453
|
-
.map(({ prompt: _p, ...rest }) => rest);
|
|
457
|
+
.map(({ prompt: _p, guardrailsSet: _g, ...rest }) => rest);
|
|
454
458
|
}
|
|
455
459
|
|
|
456
460
|
export interface RuntimeChoice {
|
|
@@ -33,13 +33,18 @@
|
|
|
33
33
|
* fix when it is missing. Exit 1 when the default runtime is
|
|
34
34
|
* unavailable. It resolves the login shell's PATH first, as the
|
|
35
35
|
* web server does at start.
|
|
36
|
+
* handback-check <file|-> (den#5) the hand-back check (handback.ts) on a
|
|
37
|
+
* file or stdin: each problem on its own line, exit 1 when there
|
|
38
|
+
* is one, 0 when the hand-back is complete and plain.
|
|
36
39
|
*
|
|
37
40
|
* Nothing here takes a grant: grants come from the org config only. The
|
|
38
41
|
* host enforces agents.max_concurrent on every start, counting agents only.
|
|
39
42
|
*/
|
|
43
|
+
import * as fs from "fs";
|
|
40
44
|
import { DEFAULT_RUNTIME_ID } from "../../shared/agents/runtimes.js";
|
|
41
45
|
import { initLoginPath } from "../../shared/agents/runtime-search.js";
|
|
42
46
|
import { HostError } from "../../shared/sessions/client.js";
|
|
47
|
+
import { checkHandback } from "../../shared/agents/handback.js";
|
|
43
48
|
import {
|
|
44
49
|
AgentError,
|
|
45
50
|
listAgents,
|
|
@@ -75,7 +80,7 @@ export {
|
|
|
75
80
|
} from "./agent-ops.js";
|
|
76
81
|
|
|
77
82
|
export const AGENTS_HELP = {
|
|
78
|
-
usage: "agents <list|start|stop|resume|personas|doctor> ...",
|
|
83
|
+
usage: "agents <list|start|stop|resume|personas|doctor|handback-check> ...",
|
|
79
84
|
lines: [
|
|
80
85
|
"Agents: a persona and a workspace, run by a runtime in a session that",
|
|
81
86
|
"outlives the web server (see `{cli} sessions`).",
|
|
@@ -94,6 +99,10 @@ export const AGENTS_HELP = {
|
|
|
94
99
|
" each runtime, installed first: status, the CLI it runs and where it came",
|
|
95
100
|
" from, its version, the directories searched, and the fix; exit 1 when the",
|
|
96
101
|
" default is missing. -w: as that workspace's agents find them",
|
|
102
|
+
"{cli} agents handback-check <file|-> [--json]",
|
|
103
|
+
" checks a hand-back to a person: URL, machine, branch, what changed, what",
|
|
104
|
+
" to click and decisions are all there, every ID is explained, and each",
|
|
105
|
+
" decision is a numbered yes/no question; exit 1 with what to fix",
|
|
97
106
|
"",
|
|
98
107
|
"Runtimes: den-claude (default; Claude Code's claude), codex (OpenAI's Codex",
|
|
99
108
|
"CLI), gemini (Google's Gemini CLI), grok (xAI's Grok Build), and generic (the",
|
|
@@ -276,6 +285,45 @@ async function listPersonas(
|
|
|
276
285
|
export interface AgentsDoctorDeps {
|
|
277
286
|
/** Resolve the login shell's PATH first (default: initLoginPath). */
|
|
278
287
|
loginPath?: () => Promise<void>;
|
|
288
|
+
/** Read a hand-back: a file path, or "-" for stdin (handback-check). */
|
|
289
|
+
readHandback?: (src: string) => Promise<string>;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
async function readFileOrStdin(src: string): Promise<string> {
|
|
293
|
+
if (src !== "-") return fs.promises.readFile(src, "utf-8");
|
|
294
|
+
let text = "";
|
|
295
|
+
for await (const chunk of process.stdin) text += String(chunk);
|
|
296
|
+
return text;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
/** `agents handback-check <file|-> [--json]`. */
|
|
300
|
+
async function handbackCheck(
|
|
301
|
+
args: string[],
|
|
302
|
+
d: AgentsResolved,
|
|
303
|
+
deps: AgentsDoctorDeps,
|
|
304
|
+
): Promise<number> {
|
|
305
|
+
const where = "agents handback-check";
|
|
306
|
+
const p = parseArgs(where, args, { "--json": false });
|
|
307
|
+
if (p.positional.length !== 1 || p.rest)
|
|
308
|
+
throw new UsageError(`${where} takes one file, or - for stdin`);
|
|
309
|
+
const src = p.positional[0];
|
|
310
|
+
let text: string;
|
|
311
|
+
try {
|
|
312
|
+
text = await (deps.readHandback ?? readFileOrStdin)(src);
|
|
313
|
+
} catch (e) {
|
|
314
|
+
d.err(
|
|
315
|
+
`✗ ${where}: can't read ${src} (${(e as NodeJS.ErrnoException).code ?? "error"})`,
|
|
316
|
+
);
|
|
317
|
+
return 1;
|
|
318
|
+
}
|
|
319
|
+
const r = checkHandback(text);
|
|
320
|
+
if (p.flags["--json"]) d.out(JSON.stringify(r, null, 2));
|
|
321
|
+
else if (r.ok) d.out("✓ the hand-back is complete and plain");
|
|
322
|
+
else {
|
|
323
|
+
d.out("✗ the hand-back is not ready:");
|
|
324
|
+
for (const l of r.problems) d.out(` - ${l}`);
|
|
325
|
+
}
|
|
326
|
+
return r.ok ? 0 : 1;
|
|
279
327
|
}
|
|
280
328
|
|
|
281
329
|
const SOURCE_WORD: Record<string, string> = {
|
|
@@ -399,6 +447,8 @@ export async function cmdAgents(
|
|
|
399
447
|
// -w checks the workspace exists, and reads its runtime paths.
|
|
400
448
|
await loadAgentWorkspaces(deps);
|
|
401
449
|
return await doctor(rest, d, deps);
|
|
450
|
+
case "handback-check":
|
|
451
|
+
return await handbackCheck(rest, d, deps);
|
|
402
452
|
default:
|
|
403
453
|
throw new UsageError(`unknown agents command "${sub}"`);
|
|
404
454
|
}
|
|
@@ -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
|
@@ -900,6 +900,12 @@ class DenCLI {
|
|
|
900
900
|
|
|
901
901
|
case "web": {
|
|
902
902
|
const sub = args[1];
|
|
903
|
+
if (args.includes("--json") && (sub === "down" || sub === "stop")) {
|
|
904
|
+
process.exitCode = await this.cmdWebDownJson(
|
|
905
|
+
args.slice(1).filter((a) => a !== "--json"),
|
|
906
|
+
);
|
|
907
|
+
break;
|
|
908
|
+
}
|
|
903
909
|
if (
|
|
904
910
|
args.includes("--json") &&
|
|
905
911
|
(sub === undefined ||
|
|
@@ -926,7 +932,11 @@ class DenCLI {
|
|
|
926
932
|
break;
|
|
927
933
|
|
|
928
934
|
case "down":
|
|
929
|
-
|
|
935
|
+
if (args.slice(1).includes("--json"))
|
|
936
|
+
process.exitCode = await this.cmdServiceDownJson(
|
|
937
|
+
args.slice(1).filter((a) => a !== "--json"),
|
|
938
|
+
);
|
|
939
|
+
else await this.cmdServiceDown(args.slice(1));
|
|
930
940
|
break;
|
|
931
941
|
|
|
932
942
|
case "restart":
|
|
@@ -3276,6 +3286,101 @@ class DenCLI {
|
|
|
3276
3286
|
return out.ok ? 0 : 1;
|
|
3277
3287
|
}
|
|
3278
3288
|
|
|
3289
|
+
/**
|
|
3290
|
+
* `down --json`: down as it always runs, its log on stderr, then one JSON
|
|
3291
|
+
* object on stdout, `{ ok, workspace, services, all? }`. Safe to re-run:
|
|
3292
|
+
* stopping what is already stopped is ok. A failure names a fix.
|
|
3293
|
+
*/
|
|
3294
|
+
private async cmdServiceDownJson(args: string[]): Promise<number> {
|
|
3295
|
+
const bin = fs.existsSync(
|
|
3296
|
+
nodePath.join(getDistRoot(), "bin", product().cli),
|
|
3297
|
+
)
|
|
3298
|
+
? `./bin/${product().cli}`
|
|
3299
|
+
: product().cli;
|
|
3300
|
+
const before = process.exitCode;
|
|
3301
|
+
process.exitCode = undefined;
|
|
3302
|
+
let error: string | undefined;
|
|
3303
|
+
try {
|
|
3304
|
+
await quietStdout(() => this.cmdServiceDown(args));
|
|
3305
|
+
} catch (e) {
|
|
3306
|
+
if (!(e instanceof ExitRequested)) error = (e as Error).message;
|
|
3307
|
+
else if (e.code !== 0) process.exitCode = e.code;
|
|
3308
|
+
}
|
|
3309
|
+
const failedExit = process.exitCode !== undefined && process.exitCode !== 0;
|
|
3310
|
+
process.exitCode = before;
|
|
3311
|
+
const terminator = args.indexOf("--");
|
|
3312
|
+
const own = terminator === -1 ? args : args.slice(0, terminator);
|
|
3313
|
+
const all = own.includes("-a") || own.includes("--all");
|
|
3314
|
+
const out: Record<string, unknown> = {
|
|
3315
|
+
ok: !error && !failedExit,
|
|
3316
|
+
workspace:
|
|
3317
|
+
process.env.DEN_WORKSPACE_OVERRIDE ?? getFocusedWorkspaceName() ?? null,
|
|
3318
|
+
services: own.filter((a) => !a.startsWith("-")),
|
|
3319
|
+
...(all ? { all: true } : {}),
|
|
3320
|
+
};
|
|
3321
|
+
if (!out.ok) {
|
|
3322
|
+
out.error = error ?? "down failed (its log is on stderr)";
|
|
3323
|
+
out.fix = `${bin} doctor --json`;
|
|
3324
|
+
}
|
|
3325
|
+
printJson(out);
|
|
3326
|
+
return out.ok ? 0 : 1;
|
|
3327
|
+
}
|
|
3328
|
+
|
|
3329
|
+
/**
|
|
3330
|
+
* `web down --json`: the web server stopped as usual (its log on stderr),
|
|
3331
|
+
* then `{ ok, url, port, running: false, stopped }` once nothing answers
|
|
3332
|
+
* (up to 10s). `stopped` is false when it wasn't running: re-running is ok.
|
|
3333
|
+
*/
|
|
3334
|
+
private async cmdWebDownJson(args: string[]): Promise<number> {
|
|
3335
|
+
const { parseWebCommandArgs } = await import("./web-command.js");
|
|
3336
|
+
const { probeWebServer } = await import("./mcp-connect.js");
|
|
3337
|
+
const bin = fs.existsSync(
|
|
3338
|
+
nodePath.join(getDistRoot(), "bin", product().cli),
|
|
3339
|
+
)
|
|
3340
|
+
? `./bin/${product().cli}`
|
|
3341
|
+
: product().cli;
|
|
3342
|
+
let port: number;
|
|
3343
|
+
try {
|
|
3344
|
+
port = parseWebCommandArgs(args).port;
|
|
3345
|
+
} catch (e) {
|
|
3346
|
+
printJson({
|
|
3347
|
+
ok: false,
|
|
3348
|
+
error: (e as Error).message,
|
|
3349
|
+
fix: `${bin} web down --json`,
|
|
3350
|
+
});
|
|
3351
|
+
return 2;
|
|
3352
|
+
}
|
|
3353
|
+
const url = `http://localhost:${port}`;
|
|
3354
|
+
const was = (await probeWebServer(url)).kind === "up";
|
|
3355
|
+
let error: string | undefined;
|
|
3356
|
+
try {
|
|
3357
|
+
await quietStdout(() => cmdWeb(args));
|
|
3358
|
+
} catch (e) {
|
|
3359
|
+
if (!(e instanceof ExitRequested) || e.code !== 0)
|
|
3360
|
+
error = (e as Error).message;
|
|
3361
|
+
}
|
|
3362
|
+
let probe = await probeWebServer(url);
|
|
3363
|
+
const deadline = Date.now() + 10_000;
|
|
3364
|
+
while (!error && probe.kind === "up" && Date.now() < deadline) {
|
|
3365
|
+
await new Promise((r) => setTimeout(r, 250));
|
|
3366
|
+
probe = await probeWebServer(url);
|
|
3367
|
+
}
|
|
3368
|
+
const running = probe.kind === "up";
|
|
3369
|
+
const out: Record<string, unknown> = {
|
|
3370
|
+
ok: !error && !running,
|
|
3371
|
+
url,
|
|
3372
|
+
port,
|
|
3373
|
+
running,
|
|
3374
|
+
stopped: was && !running,
|
|
3375
|
+
};
|
|
3376
|
+
if (!out.ok) {
|
|
3377
|
+
out.error = error ?? `the web server still answers at ${url}`;
|
|
3378
|
+
out.fix = `${bin} doctor --json`;
|
|
3379
|
+
}
|
|
3380
|
+
printJson(out);
|
|
3381
|
+
return out.ok ? 0 : 1;
|
|
3382
|
+
}
|
|
3383
|
+
|
|
3279
3384
|
/**
|
|
3280
3385
|
* `web up|restart --json` (ON-2): the web server started as usual (its
|
|
3281
3386
|
* log on stderr), then `{ ok, url, port, version? }` once it answers
|
|
@@ -1196,7 +1196,9 @@ export function displayWebHelp(): void {
|
|
|
1196
1196
|
console.log(" --port, -p <port> Use a non-default web port");
|
|
1197
1197
|
console.log(
|
|
1198
1198
|
" --no-open Start or check the server without opening a browser",
|
|
1199
|
-
|
|
1199
|
+
);
|
|
1200
|
+
console.log(
|
|
1201
|
+
" --json log on stderr; stdout: up, restart { ok, url, port, version? }; down { ok, url, port, running, stopped }",
|
|
1200
1202
|
);
|
|
1201
1203
|
console.log(
|
|
1202
1204
|
" --setup-browser-access Request macOS Automation access for browser tab reuse",
|