@monster-orange/den 0.4.17 → 0.4.19

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 (49) hide show
  1. package/CHANGELOG.md +177 -0
  2. package/README.md +14 -13
  3. package/docs/distributions.md +138 -25
  4. package/docs/reference.md +1 -1
  5. package/package.json +2 -1
  6. package/skills/install-den/SKILL.md +35 -75
  7. package/src/shared/agents/den-claude.ts +40 -6
  8. package/src/shared/agents/guardrails/stop-hook.ts +30 -4
  9. package/src/shared/agents/guardrails.ts +40 -4
  10. package/src/shared/agents/handback.ts +22 -4
  11. package/src/shared/agents/personas.ts +32 -5
  12. package/src/shared/agents/runtime.ts +8 -0
  13. package/src/shared/agents/server-agent.ts +57 -0
  14. package/src/shared/agents/server-pack/server.md +45 -0
  15. package/src/shared/environment-manager/workspaces.ts +4 -0
  16. package/src/ui/cli/agent-ops.ts +84 -6
  17. package/src/ui/cli/create.ts +2 -2
  18. package/src/ui/cli/index.ts +1 -1
  19. package/src/ui/cli/repo-command.ts +8 -1
  20. package/src/ui/cli/repo-detect.ts +58 -0
  21. package/src/ui/cli/server-agent-ops.ts +411 -0
  22. package/src/ui/mcp/server.ts +29 -12
  23. package/src/ui/mcp/tools.ts +4 -2
  24. package/src/ui/server/agent-routes.ts +10 -1
  25. package/src/ui/server/held-actions.ts +35 -7
  26. package/src/ui/server/index.ts +20 -5
  27. package/src/ui/server/public/agents-page.js +33 -1
  28. package/src/ui/server/public/agents.html +13 -0
  29. package/src/ui/server/public/error-page.js +39 -1
  30. package/src/ui/server/public/graph.css +43 -0
  31. package/src/ui/server/public/graph.js +68 -1
  32. package/src/ui/server/public/header.js +36 -7
  33. package/src/ui/server/public/i18n/es.js +72 -0
  34. package/src/ui/server/public/i18n/ja.js +74 -0
  35. package/src/ui/server/public/local-dev.html +33 -3
  36. package/src/ui/server/public/log-source.js +26 -0
  37. package/src/ui/server/public/logs.html +7 -20
  38. package/src/ui/server/public/persona-ui.css +6 -0
  39. package/src/ui/server/public/server-agent-page.js +415 -0
  40. package/src/ui/server/public/server-agent.html +222 -0
  41. package/src/ui/server/public/services.html +18 -3
  42. package/src/ui/server/public/shell.js +274 -31
  43. package/src/ui/server/public/svc-panel.js +685 -0
  44. package/src/ui/server/public/topbar.js +13 -1
  45. package/src/ui/server/route-classes.ts +21 -0
  46. package/src/ui/server/server-agent-routes.ts +176 -0
  47. package/src/ui/server/session-routes.ts +12 -1
  48. package/src/ui/server/ui-assets.ts +6 -0
  49. package/src/ui/server/workspace-routes.ts +3 -0
@@ -78,6 +78,7 @@ import { envKey } from "../../shared/product.js";
78
78
  import { redactSecrets } from "../../shared/redact.js";
79
79
  import {
80
80
  loadPersonas,
81
+ serverPersona,
81
82
  resolvePackDirs,
82
83
  type Persona,
83
84
  } from "../../shared/agents/personas.js";
@@ -167,6 +168,11 @@ import {
167
168
  type BinSource,
168
169
  } from "../../shared/agents/runtime-search.js";
169
170
  import { WORKSPACE_NAME_RE } from "../../shared/agent-actor.js";
171
+ import {
172
+ SERVER_PERSONA_ID,
173
+ isServerScope,
174
+ serverAgentDir,
175
+ } from "../../shared/agents/server-agent.js";
170
176
  import {
171
177
  bindActorSession,
172
178
  mintActorToken,
@@ -333,7 +339,7 @@ function defaultMcpServer(cli: string): {
333
339
 
334
340
  export function resolveAgentsDeps(d: AgentsDeps): AgentsResolved {
335
341
  const base = resolveDeps(d);
336
- return {
342
+ return withServerScope({
337
343
  ...base,
338
344
  agentsConfig: d.agentsConfig ?? (() => readAgentsConfig()),
339
345
  distRoot: d.distRoot ?? getDistRoot,
@@ -365,6 +371,26 @@ export function resolveAgentsDeps(d: AgentsDeps): AgentsResolved {
365
371
  ? setWorkspaceRuntimeBin(ws, id, bin)
366
372
  : setUserRuntimeBin(id, bin)),
367
373
  userConfigFile: d.userConfigFile ?? userConfigPath,
374
+ });
375
+ }
376
+
377
+ /**
378
+ * ON-6: the server agent's scope is no workspace, but its agent is a
379
+ * normal one: it "exists", and its folder is an empty one under the state
380
+ * dir (serverAgentDir), with no workspace runtime paths.
381
+ */
382
+ function withServerScope(r: AgentsResolved): AgentsResolved {
383
+ return {
384
+ ...r,
385
+ workspaceExists: (ws) => isServerScope(ws) || r.workspaceExists(ws),
386
+ workspaceDir: (ws) =>
387
+ isServerScope(ws) ? serverAgentDir(r.stateDir()) : r.workspaceDir(ws),
388
+ workspaceFolders: (ws) =>
389
+ isServerScope(ws)
390
+ ? { under: [serverAgentDir(r.stateDir())] }
391
+ : r.workspaceFolders(ws),
392
+ workspaceRuntimeBin: (ws, id) =>
393
+ isServerScope(ws) ? undefined : r.workspaceRuntimeBin(ws, id),
368
394
  };
369
395
  }
370
396
 
@@ -685,14 +711,18 @@ function agentsConfig(d: AgentsResolved): ResolvedAgentsConfig {
685
711
  function personas(
686
712
  d: AgentsResolved,
687
713
  cfg: ResolvedAgentsConfig,
714
+ /** ON-6: the server scope adds Den's own server persona (only there). */
715
+ ws?: string,
688
716
  ): Map<string, Persona> {
689
717
  try {
690
- return loadPersonas(
718
+ const all = loadPersonas(
691
719
  resolvePackDirs(cfg.personaPacks, d.distRoot()),
692
720
  undefined,
693
721
  { defaultPack: cfg.defaultPackGuardrails, packs: cfg.packGuardrails },
694
722
  localPersonasDir(d.stateDir()),
695
723
  );
724
+ if (isServerScope(ws)) all.set(SERVER_PERSONA_ID, serverPersona());
725
+ return all;
696
726
  } catch (e) {
697
727
  throw refused((e as Error).message, [], 422);
698
728
  }
@@ -1337,6 +1367,9 @@ function launchInput(
1337
1367
  rotateAfter: after,
1338
1368
  ...(perms.permission ? { permission: perms.permission } : {}),
1339
1369
  ...(perms.allow?.length ? { allow: [...perms.allow] } : {}),
1370
+ // ON-6: the server agent runs Den's own tools without the runtime's
1371
+ // prompt; its writes wait for the person's Allow in the browser.
1372
+ ...(isServerScope(ws) ? { denToolsAllowed: true } : {}),
1340
1373
  };
1341
1374
  }
1342
1375
 
@@ -1409,6 +1442,12 @@ export interface StartRequest {
1409
1442
  allow?: unknown;
1410
1443
  /** AG-10.10: a display name for this agent (AG-4b's), to tell two apart. */
1411
1444
  name?: unknown;
1445
+ /**
1446
+ * 0.4.18, Start agent here: the folder it runs in, the workspace's own
1447
+ * or one inside it (a repo's), relative to the workspace's folder or
1448
+ * absolute. Default: the workspace's folder.
1449
+ */
1450
+ folder?: string;
1412
1451
  /**
1413
1452
  * The page's start (AG-10.1): a model must be one the runtime offers,
1414
1453
  * never a free string (the CLI's --model for generic stays free).
@@ -1524,6 +1563,39 @@ interface StartMore {
1524
1563
  bringIn?: { token: string; cwd: string };
1525
1564
  }
1526
1565
 
1566
+ /**
1567
+ * 0.4.18: the folder a start runs in. Relative to the workspace's folder,
1568
+ * or absolute; it must be a directory inside the workspace's scope (its
1569
+ * folder, or a by-path repo's), never outside it.
1570
+ */
1571
+ export function startFolder(
1572
+ d: AgentsResolved,
1573
+ ws: string,
1574
+ folder: string,
1575
+ ): string {
1576
+ const where = "agents start";
1577
+ if (typeof folder !== "string" || !folder.trim() || folder.length > 4096)
1578
+ throw usage(`${where}: folder must be a path`);
1579
+ const dir = path.resolve(d.workspaceDir(ws), folder.trim());
1580
+ let real: string;
1581
+ try {
1582
+ real = fs.realpathSync(dir);
1583
+ } catch {
1584
+ throw usage(`${where}: folder ${dir} does not exist`, 404);
1585
+ }
1586
+ if (!fs.statSync(real).isDirectory())
1587
+ throw usage(`${where}: folder ${dir} is not a directory`);
1588
+ // The real folder only: a link inside the workspace that points out of
1589
+ // it is outside it (the scope's own folders are compared real, too).
1590
+ if (!inScope(real, d.workspaceFolders(ws)))
1591
+ throw refused(
1592
+ `${where}: folder ${dir} is not inside workspace ${ws}`,
1593
+ [],
1594
+ 403,
1595
+ );
1596
+ return real;
1597
+ }
1598
+
1527
1599
  export async function startAgent(
1528
1600
  d: AgentsResolved,
1529
1601
  req: StartRequest,
@@ -1534,7 +1606,7 @@ export async function startAgent(
1534
1606
  if (!d.workspaceExists(ws))
1535
1607
  throw usage(`${where}: workspace "${ws}" does not exist`, 404);
1536
1608
  const cfg = agentsConfig(d);
1537
- const all = personas(d, cfg);
1609
+ const all = personas(d, cfg, ws);
1538
1610
  const persona = all.get(req.persona);
1539
1611
  if (!persona)
1540
1612
  throw usage(
@@ -1571,6 +1643,10 @@ export async function startAgent(
1571
1643
  ...(rt.permissions ? { allow } : {}),
1572
1644
  }),
1573
1645
  ...(more.firstPrompt ? { firstPrompt: more.firstPrompt } : {}),
1646
+ // 0.4.18: Start agent here: in a folder of the workspace's.
1647
+ ...(req.folder !== undefined && !more.bringIn
1648
+ ? { worktree: startFolder(d, ws, req.folder) }
1649
+ : {}),
1574
1650
  // AG-17.1: the runtime's resume, in the folder the conversation ran in.
1575
1651
  ...(more.bringIn
1576
1652
  ? { worktree: more.bringIn.cwd, resumeToken: more.bringIn.token }
@@ -2065,7 +2141,7 @@ export async function resumeAgent(
2065
2141
  );
2066
2142
  const cfg = agentsConfig(d);
2067
2143
  const grants = resumeGrants(rec, cfg);
2068
- const persona = personas(d, cfg).get(rec.persona);
2144
+ const persona = personas(d, cfg, rec.workspace).get(rec.persona);
2069
2145
  if (!persona)
2070
2146
  throw refused(`agent ${rec.id}'s persona "${rec.persona}" is gone`);
2071
2147
  const det = await availableOrRefuse(d, rt, rec.workspace);
@@ -2269,7 +2345,7 @@ export async function changePersona(
2269
2345
  const where = "agents persona";
2270
2346
  const rec = agentRecord(d, id, ws);
2271
2347
  const cfg = agentsConfig(d);
2272
- const all = personas(d, cfg);
2348
+ const all = personas(d, cfg, rec.workspace);
2273
2349
  if (!all.get(req.persona))
2274
2350
  throw usage(
2275
2351
  `${where}: no persona "${req.persona}" (personas: ${[...all.keys()].sort().join(", ")})`,
@@ -2704,7 +2780,9 @@ export async function markInterrupted(
2704
2780
  return found;
2705
2781
  }
2706
2782
  for (const rec of marked) {
2707
- const p = all.get(rec.persona);
2783
+ const p = isServerScope(rec.workspace)
2784
+ ? serverPersona()
2785
+ : all.get(rec.persona);
2708
2786
  if (!p || !(p.standing || p.resumeOnRestart)) continue;
2709
2787
  if (resumeRefusal(rec)) continue;
2710
2788
  try {
@@ -545,7 +545,7 @@ export async function createResult(
545
545
  return {
546
546
  ok: false,
547
547
  error,
548
- fix: `npx ${ENGINE_PACKAGE} create ${args.name} ${path.join(path.dirname(args.dir), `${args.name}-den`)} --yes --json`,
548
+ fix: `npx ${ENGINE_PACKAGE}@latest create ${args.name} ${path.join(path.dirname(args.dir), `${args.name}-den`)} --yes --json`,
549
549
  ask: `${args.dir} already has files that are not a ${args.name} distribution. Which new or empty folder should the distribution go in?`,
550
550
  };
551
551
  return {
@@ -576,7 +576,7 @@ export async function runCreate(
576
576
  JSON.stringify({
577
577
  ok: false,
578
578
  error: (e as Error).message.split("\n")[0],
579
- fix: `npx ${ENGINE_PACKAGE} create <name> [dir] --yes --json`,
579
+ fix: `npx ${ENGINE_PACKAGE}@latest create <name> [dir] --yes --json`,
580
580
  } satisfies CreateJson),
581
581
  );
582
582
  } else console.error(`create: ${(e as Error).message}`);
@@ -446,7 +446,7 @@ const COMMAND_HELP: Record<string, { usage: string; lines: string[] }> = {
446
446
  "create <name> [dir] [--title <title>] [--cli <cli>] [--env-prefix <PREFIX>]",
447
447
  lines: [
448
448
  "Scaffold a new distribution (den.yml, config, bin shim, Makefile) in [dir].",
449
- "Also: npx @monster-orange/den create <name>. See docs/distributions.md.",
449
+ "Also: npx @monster-orange/den@latest create <name>. See docs/distributions.md.",
450
450
  ],
451
451
  },
452
452
  validate: {
@@ -60,6 +60,7 @@ import { ensureManagedServiceClone } from "../../shared/environment-manager/work
60
60
  import { getGitHubCoords } from "../../shared/smoke/git-remote.js";
61
61
  import {
62
62
  detectRunCommands,
63
+ draftHealthUrl,
63
64
  repoNameFrom,
64
65
  sidecarManifest,
65
66
  type DetectedCommands,
@@ -358,7 +359,13 @@ export function repoAdd(
358
359
  error: `cannot name a repo from the folder "${key}"`,
359
360
  fix: `${bin} repo add ${a.spec} --name <repo> --json`,
360
361
  };
361
- const manifest = sidecarManifest(mName, d.commands, a.health);
362
+ // No --health: the dev server's own port, when its script names one,
363
+ // so the drafted service has a health check and a Visit link.
364
+ const manifest = sidecarManifest(
365
+ mName,
366
+ d.commands,
367
+ a.health ?? draftHealthUrl(repoPath, d.commands),
368
+ );
362
369
  const v = validate(manifest);
363
370
  const dry = a.dryRun
364
371
  ? {
@@ -157,6 +157,64 @@ export function needsDocker(manifest: unknown, repoPath?: string): boolean {
157
157
  return repoPath ? composeFileOf(repoPath) !== undefined : false;
158
158
  }
159
159
 
160
+ /**
161
+ * Dev servers' own default ports, by the command a package script runs.
162
+ * Only tools whose default is fixed and documented; anything else gets no
163
+ * port, and so no health URL.
164
+ */
165
+ const DEV_SERVER_PORTS: ReadonlyArray<[RegExp, number]> = [
166
+ [/^next(\s+dev)?\b/, 3000],
167
+ [/^(vite|vite\s+dev|vite\s+serve)\b/, 5173],
168
+ [/^(nuxt|nuxi)\s+dev\b/, 3000],
169
+ [/^react-scripts\s+start\b/, 3000],
170
+ [/^ng\s+serve\b/, 4200],
171
+ [/^gatsby\s+develop\b/, 8000],
172
+ [/^remix\s+vite:dev\b/, 5173],
173
+ [/^webpack(-dev-server|\s+serve)\b/, 8080],
174
+ ];
175
+
176
+ /** A port the command names itself: --port N, -p N, PORT=N. */
177
+ function explicitPort(cmd: string): number | undefined {
178
+ const m =
179
+ /(?:^|\s)--port[=\s]+(\d{2,5})\b/.exec(cmd) ??
180
+ /(?:^|\s)-p\s+(\d{2,5})\b/.exec(cmd) ??
181
+ /(?:^|\s)PORT=(\d{2,5})\b/.exec(cmd);
182
+ const n = m ? Number(m[1]) : NaN;
183
+ return n > 0 && n < 65536 ? n : undefined;
184
+ }
185
+
186
+ /**
187
+ * The port a repo's dev server listens on, when its start command is a
188
+ * package script that names one or runs a dev server with a known default.
189
+ * Read-only: the script's text, never the running server.
190
+ */
191
+ export function detectDevPort(
192
+ repoPath: string,
193
+ c: Pick<DetectedCommands, "up">,
194
+ ): number | undefined {
195
+ const scripts = readScripts(repoPath);
196
+ const m = /^npm\s+(?:run\s+(\S+)|(start))$/.exec(c.up.trim());
197
+ const script = m ? scripts[m[1] ?? "start"] : c.up;
198
+ if (!script) return undefined;
199
+ const own = explicitPort(script);
200
+ if (own) return own;
201
+ // The tool the script runs, after any VAR=value prefixes and npx.
202
+ const tool = script
203
+ .trim()
204
+ .replace(/^(?:[A-Z_][A-Z0-9_]*=\S*\s+)+/, "")
205
+ .replace(/^npx\s+(?:--yes\s+)?/, "");
206
+ return DEV_SERVER_PORTS.find(([re]) => re.test(tool))?.[1];
207
+ }
208
+
209
+ /** The health URL to draft: the dev server's own port, at `/`. */
210
+ export function draftHealthUrl(
211
+ repoPath: string,
212
+ c: Pick<DetectedCommands, "up">,
213
+ ): string | undefined {
214
+ const port = detectDevPort(repoPath, c);
215
+ return port ? `http://localhost:${port}/` : undefined;
216
+ }
217
+
160
218
  /** The side-car manifest for a repo Den detected. Validate before writing. */
161
219
  export function sidecarManifest(
162
220
  name: string,