@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.
@@ -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
- const built = path.join(engine, "dist/src/ui/cli/index.js");
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, "src/ui/cli/index.ts");
112
+ const src = path.join(engine, rel);
101
113
  if (loader && fs.existsSync(src))
102
114
  return {
103
115
  command: process.execPath,
@@ -437,20 +437,24 @@ function personas(
437
437
  cfg: ResolvedAgentsConfig,
438
438
  ): Map<string, Persona> {
439
439
  try {
440
- return loadPersonas(resolvePackDirs(cfg.personaPacks, d.distRoot()));
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(
@@ -215,6 +215,7 @@ const COMMAND_FLAGS: Record<string, readonly string[]> = {
215
215
  "--json",
216
216
  ],
217
217
  down: [
218
+ "--json",
218
219
  "--manifest",
219
220
  "--no-derive",
220
221
  "-a",
@@ -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
- await this.cmdServiceDown(args.slice(1));
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
- " --json (up, restart) log on stderr; stdout: { ok, url, port, version? }",
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",