@monster-orange/den 0.4.12 → 0.4.14

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 (46) hide show
  1. package/CHANGELOG.md +151 -0
  2. package/README.md +7 -7
  3. package/docs/distributions.md +189 -11
  4. package/package.json +1 -1
  5. package/src/shared/agents/conversations.ts +227 -0
  6. package/src/shared/agents/default-pack/be-dev.md +1 -0
  7. package/src/shared/agents/default-pack/devops.md +1 -0
  8. package/src/shared/agents/default-pack/fe-dev.md +1 -0
  9. package/src/shared/agents/default-pack/orchestrator.md +3 -1
  10. package/src/shared/agents/default-pack/planner.md +1 -0
  11. package/src/shared/agents/default-pack/reviewer.md +1 -0
  12. package/src/shared/agents/default-pack/tester.md +16 -0
  13. package/src/shared/agents/default-pack/ux-designer.md +1 -0
  14. package/src/shared/agents/den-claude.ts +128 -13
  15. package/src/shared/agents/permissions.ts +108 -0
  16. package/src/shared/agents/persona-store.ts +286 -0
  17. package/src/shared/agents/personas.ts +116 -12
  18. package/src/shared/agents/records.ts +33 -0
  19. package/src/shared/agents/roles.ts +288 -0
  20. package/src/shared/agents/runtime.ts +50 -0
  21. package/src/shared/den-config.ts +22 -1
  22. package/src/shared/environment-manager/service-runner.ts +31 -12
  23. package/src/shared/environment-manager/workspaces.ts +11 -0
  24. package/src/ui/cli/agent-ops.ts +796 -29
  25. package/src/ui/cli/agents-command.ts +97 -3
  26. package/src/ui/cli/env-probe.ts +4 -1
  27. package/src/ui/mcp/api.ts +12 -1
  28. package/src/ui/mcp/tools.ts +60 -1
  29. package/src/ui/server/agent-resume-grant.ts +35 -0
  30. package/src/ui/server/agent-routes.ts +412 -22
  31. package/src/ui/server/public/agent-start.js +3 -2
  32. package/src/ui/server/public/agents-page.js +693 -49
  33. package/src/ui/server/public/agents.html +76 -15
  34. package/src/ui/server/public/i18n/es.js +143 -0
  35. package/src/ui/server/public/i18n/ja.js +144 -0
  36. package/src/ui/server/public/persona-ui.css +563 -0
  37. package/src/ui/server/public/persona-ui.js +796 -0
  38. package/src/ui/server/public/settings-page.js +4 -9
  39. package/src/ui/server/public/settings-registry.js +22 -0
  40. package/src/ui/server/public/settings.html +1 -0
  41. package/src/ui/server/public/shell.js +140 -0
  42. package/src/ui/server/public/topbar.js +2 -2
  43. package/src/ui/server/request-guard.ts +17 -4
  44. package/src/ui/server/route-classes.ts +31 -0
  45. package/src/ui/server/session-routes.ts +21 -5
  46. package/src/ui/server/ui-assets.ts +3 -0
@@ -28,6 +28,13 @@
28
28
  * same conversation id (there is nothing to continue).
29
29
  * --all-interrupted (AG-14.3): every agent interrupted when Den or
30
30
  * the machine restarted that can resume.
31
+ * import (AG-17.1) bring in a conversation started outside Den: with
32
+ * no --session, the runtime's own conversations in the
33
+ * workspace's folder (newest first); with --session <id> and
34
+ * --persona <p>, the runtime's resume of it (den-claude:
35
+ * --resume <id>) in the folder it ran in, as a new agent under
36
+ * Den's token, hooks and guards. Refused while it is still open
37
+ * in another terminal (written in the last minute).
31
38
  * rm (AG-14.1b) forgets a stopped, interrupted or exited agent (never
32
39
  * a running one); --stopped forgets every one of -w <ws> (or of
33
40
  * every workspace). The runtime's own history stays.
@@ -69,7 +76,9 @@ import {
69
76
  } from "../../shared/agents/records.js";
70
77
  import {
71
78
  AgentError,
79
+ importAgent,
72
80
  listAgents,
81
+ listConversations,
73
82
  listPersonas as personaList,
74
83
  listRuntimes,
75
84
  loadAgentWorkspaces,
@@ -107,7 +116,7 @@ export {
107
116
 
108
117
  export const AGENTS_HELP = {
109
118
  usage:
110
- "agents <list|start|stop|resume|relaunch|rm|personas|doctor|handback-check|handoff-check> ...",
119
+ "agents <list|start|stop|resume|relaunch|import|rm|personas|doctor|handback-check|handoff-check> ...",
111
120
  lines: [
112
121
  "Agents: a persona and a workspace, run by a runtime in a session that",
113
122
  "outlives the web server (see `{cli} sessions`).",
@@ -132,6 +141,12 @@ export const AGENTS_HELP = {
132
141
  " compactions (default 3) or its pull request merged, and its handoff.md",
133
142
  " passes the check, a fresh conversation (same persona, workspace, folder,",
134
143
  " grants) whose first prompt is the handoff",
144
+ "{cli} agents import -w <ws> [--runtime <r>] [--json]",
145
+ "{cli} agents import -w <ws> --session <id> --persona <p> [--runtime <r>] [--id <id>]",
146
+ " bring in a conversation started outside Den: the first lists the runtime's",
147
+ " own (default den-claude) in the workspace's folder; the second continues",
148
+ " one as a Den agent (its resume, in the folder it ran in). Refused while it",
149
+ " is still open in another terminal: close it there first",
135
150
  "{cli} agents rm <id> [-w <ws>]",
136
151
  "{cli} agents rm --stopped [-w <ws>]",
137
152
  " forget a stopped, interrupted or exited agent (never a running one);",
@@ -279,6 +294,72 @@ async function start(args: string[], d: AgentsResolved): Promise<number> {
279
294
  );
280
295
  }
281
296
 
297
+ /** `agents import` (AG-17.1): list, or bring one in. */
298
+ async function bringIn(args: string[], d: AgentsResolved): Promise<number> {
299
+ const where = "agents import";
300
+ const p = parseArgs(where, args, {
301
+ "--workspace": true,
302
+ "--runtime": true,
303
+ "--session": true,
304
+ "--persona": true,
305
+ "--id": true,
306
+ "--json": false,
307
+ });
308
+ noExtra(where, p);
309
+ const ws = workspaceFlag(where, p);
310
+ if (!ws) throw new UsageError(`${where} needs -w <workspace>`);
311
+ const runtime = str(p, "--runtime");
312
+ const session = str(p, "--session");
313
+ if (!session) {
314
+ if (str(p, "--persona") || str(p, "--id"))
315
+ throw new UsageError(`${where}: --persona and --id need --session <id>`);
316
+ const { runtimes, conversations } = listConversations(d, ws, runtime);
317
+ if (p.flags["--json"]) {
318
+ d.out(
319
+ JSON.stringify({ workspace: ws, runtimes, conversations }, null, 2),
320
+ );
321
+ return 0;
322
+ }
323
+ for (const r of runtimes)
324
+ if (!r.bringIn && (!runtime || runtime === r.id)) d.err(`${r.why}`);
325
+ if (!conversations.length) {
326
+ d.out(`no conversations to bring in for workspace ${ws}`);
327
+ return 0;
328
+ }
329
+ d.out(
330
+ columns([
331
+ ["ID", "RUNTIME", "LAST", "TITLE", "FOLDER", "NOTE"],
332
+ ...conversations.map((c) => [
333
+ c.id,
334
+ c.runtime,
335
+ c.lastAt.slice(0, 16).replace("T", " "),
336
+ c.title ?? "(untitled)",
337
+ c.cwd,
338
+ c.agent ? `agent ${c.agent}` : c.open ? "open elsewhere" : "",
339
+ ]),
340
+ ]).join("\n"),
341
+ );
342
+ d.out(
343
+ `bring one in: ${d.cli} agents import -w ${ws} --session <id> --persona <p>`,
344
+ );
345
+ return 0;
346
+ }
347
+ const persona = str(p, "--persona");
348
+ if (!persona) throw new UsageError(`${where} needs --persona <p>`);
349
+ const s = await importAgent(d, {
350
+ workspace: ws,
351
+ session,
352
+ persona,
353
+ runtime,
354
+ id: str(p, "--id"),
355
+ });
356
+ return report(
357
+ d,
358
+ s,
359
+ `brought in ${session} as agent ${s.rec.id} (${s.rec.persona}, ${s.rec.runtime}) as ${s.rec.sessionName} in ${s.rec.cwd}`,
360
+ );
361
+ }
362
+
282
363
  /** `<id> [-w <ws>]`. */
283
364
  function idAndWorkspace(
284
365
  where: string,
@@ -410,13 +491,23 @@ async function listPersonas(
410
491
  return 0;
411
492
  }
412
493
  for (const l of columns([
413
- ["PERSONA", "STANDING", "RUNTIME", "MODEL", "ROLE"],
494
+ [
495
+ "PERSONA",
496
+ "SOURCE",
497
+ "STANDING",
498
+ "RUNTIME",
499
+ "MODEL",
500
+ "ROLE",
501
+ "DESCRIPTION",
502
+ ],
414
503
  ...all.map((x) => [
415
504
  x.id,
505
+ x.origin,
416
506
  x.standing ? "yes" : "-",
417
507
  x.runtime ?? DEFAULT_RUNTIME_ID,
418
508
  x.model ?? "-",
419
- x.role,
509
+ x.role ?? "-",
510
+ x.description,
420
511
  ]),
421
512
  ]))
422
513
  d.out(l);
@@ -606,6 +697,9 @@ export async function cmdAgents(
606
697
  case "relaunch":
607
698
  await loadAgentWorkspaces(deps);
608
699
  return await relaunch(rest, d);
700
+ case "import":
701
+ await loadAgentWorkspaces(deps);
702
+ return await bringIn(rest, d);
609
703
  case "rm":
610
704
  await loadAgentWorkspaces(deps);
611
705
  return await rm(rest, d);
@@ -112,7 +112,10 @@ function probeInput(d: ProbeDeps, dir: string): LaunchInput {
112
112
  return {
113
113
  persona: {
114
114
  id: PROBE_ID,
115
- role: "",
115
+ description: "",
116
+ category: "engineering",
117
+ allowedCommands: [],
118
+ origin: "builtin",
116
119
  prompt: "",
117
120
  standing: false,
118
121
  tools: [],
package/src/ui/mcp/api.ts CHANGED
@@ -140,12 +140,23 @@ const ACTION_ROUTES: Record<string, string> = {
140
140
  "/api/workspace/sync": "POST",
141
141
  };
142
142
 
143
- /** Action routes with a parameter: an agent's resume (MCP agent_resume). */
143
+ /**
144
+ * Action routes with a parameter: an agent's resume (MCP agent_resume),
145
+ * and Bring in's list and import (MCP agent_import, AG-17.1).
146
+ */
144
147
  const ACTION_ROUTE_PATTERNS: { re: RegExp; method: string }[] = [
145
148
  {
146
149
  re: /^\/api\/workspaces\/[A-Za-z0-9._-]{1,64}\/agents\/[A-Za-z0-9._-]{1,64}\/resume$/,
147
150
  method: "POST",
148
151
  },
152
+ {
153
+ re: /^\/api\/workspaces\/[A-Za-z0-9._-]{1,64}\/conversations$/,
154
+ method: "GET",
155
+ },
156
+ {
157
+ re: /^\/api\/workspaces\/[A-Za-z0-9._-]{1,64}\/conversations\/[A-Za-z0-9][A-Za-z0-9_-]{0,127}\/import$/,
158
+ method: "POST",
159
+ },
149
160
  ];
150
161
 
151
162
  /**
@@ -740,7 +740,7 @@ const agentResumeTool: DenTool = {
740
740
  name: "agent_resume",
741
741
  title: "Resume an agent",
742
742
  description:
743
- "Resume a stopped or interrupted agent in the same conversation, persona, workspace and folder. Never starts a new conversation: an agent whose runtime can't resume is refused. Never the caller itself.",
743
+ "Resume a stopped or interrupted agent in the same conversation, persona, workspace and folder. Never starts a new conversation: an agent whose runtime can't resume is refused. Never the caller itself. Answers with agent: its id, persona, runtime, workspace, cwd, session (the session handle, <dist>-<ws>:<id>), sessionId, displayName (a person's name for it) and sessionTitle (the name its session shows: Den's rename or the runtime's, the later), when set.",
744
744
  action: true,
745
745
  shape: {
746
746
  agent: z.string().describe("The agent's id"),
@@ -776,6 +776,63 @@ const agentResumeTool: DenTool = {
776
776
  },
777
777
  };
778
778
 
779
+ /** A runtime's conversation id (Claude Code: a uuid). */
780
+ const SESSION_RE = /^[A-Za-z0-9][A-Za-z0-9_-]{0,127}$/;
781
+
782
+ /**
783
+ * MCP agent_import (AG-17.1): bring in a conversation started outside Den
784
+ * as an agent of this workspace (or a granted one). No session: the list.
785
+ * Opt-in through mcp.actions; the web server checks the scope again.
786
+ */
787
+ const agentImportTool: DenTool = {
788
+ name: "agent_import",
789
+ title: "Bring in a conversation",
790
+ description:
791
+ "Bring in a conversation started outside Den (in the workspace's folder) as a Den agent: its runtime's own resume, in the folder it ran in, under Den's token, hooks and guards. Without session: lists the conversations there are (id, title, lastAt, cwd, runtime, open, agent). With session and persona: brings that one in and answers with the agent. Refused while it is still open in another terminal (written in the last minute), or when an agent already continues it.",
792
+ action: true,
793
+ shape: {
794
+ session: z
795
+ .string()
796
+ .optional()
797
+ .describe("The runtime's conversation id (omit to list them)"),
798
+ persona: z.string().optional().describe("The persona to run it as"),
799
+ runtime: z.string().optional().describe("The runtime (default den-claude)"),
800
+ workspace: workspaceArg,
801
+ },
802
+ async run(a, { api, scope }) {
803
+ let ws = str(a.workspace) ?? scope?.workspace;
804
+ if (!ws) {
805
+ const focused = (await api.get("/api/workspaces")) as {
806
+ focus?: unknown;
807
+ };
808
+ ws = typeof focused?.focus === "string" ? focused.focus : undefined;
809
+ }
810
+ if (!ws || !NAME_RE.test(ws))
811
+ throw new Refusal("agent_import: name the workspace");
812
+ if (scope && ws !== scope.workspace && !(scope.grants ?? []).includes(ws))
813
+ throw new ScopeRefusal(
814
+ `agent_import: workspace "${ws}" is outside this agent's scope ("${scope.workspace}")`,
815
+ );
816
+ const runtime = str(a.runtime);
817
+ if (runtime && !NAME_RE.test(runtime))
818
+ throw new Refusal("agent_import: runtime must be a runtime id");
819
+ const session = str(a.session);
820
+ const base = `/api/workspaces/${encodeURIComponent(ws)}/conversations`;
821
+ if (!session) return api.get(base, runtime ? { runtime } : {}, "GET");
822
+ if (!SESSION_RE.test(session))
823
+ throw new Refusal("agent_import: session must be a conversation id");
824
+ const persona = str(a.persona);
825
+ if (!persona || !NAME_RE.test(persona))
826
+ throw new Refusal("agent_import: name the persona to run it as");
827
+ return api.get(
828
+ `${base}/${encodeURIComponent(session)}/import`,
829
+ {},
830
+ "POST",
831
+ { persona, ...(runtime ? { runtime } : {}) },
832
+ );
833
+ },
834
+ };
835
+
779
836
  const ACTION_TOOLS: Record<McpAction, DenTool> = {
780
837
  service_up: lifecycleTool("up"),
781
838
  service_down: lifecycleTool("down"),
@@ -865,6 +922,7 @@ const ACTION_TOOLS: Record<McpAction, DenTool> = {
865
922
  },
866
923
  },
867
924
  agent_resume: agentResumeTool,
925
+ agent_import: agentImportTool,
868
926
  };
869
927
 
870
928
  export const READ_TOOL_NAMES: readonly string[] = READ_TOOLS.map((t) => t.name);
@@ -903,6 +961,7 @@ const WORKSPACE_TOOLS = new Set([
903
961
  "workspace_sync",
904
962
  "run_tests",
905
963
  "agent_resume",
964
+ "agent_import",
906
965
  ]);
907
966
 
908
967
  /**
@@ -51,3 +51,38 @@ export function agentResumeTarget(
51
51
  return null;
52
52
  }
53
53
  }
54
+
55
+ /**
56
+ * MCP `agent_import` (AG-17.1): bring a conversation in as an agent, and
57
+ * list the ones there are. Off unless `mcp.actions` lists it; only in the
58
+ * caller's workspace or one its grants give it. A person's otherwise.
59
+ */
60
+ export const AGENT_IMPORT_ACTION = "agent_import";
61
+
62
+ /** GET /api/workspaces/<ws>/conversations, POST …/<id>/import. */
63
+ export const AGENT_IMPORT_ROUTE =
64
+ /^\/api\/workspaces\/([^/]+)\/conversations(?:\/([^/]+)\/import)?$/;
65
+
66
+ /** Why this agent may not bring in a conversation of `ws`, or null. */
67
+ export function agentImportRefusal(
68
+ p: ResumingAgent,
69
+ ws: string,
70
+ actions: readonly string[],
71
+ ): string | null {
72
+ if (!actions.includes(AGENT_IMPORT_ACTION))
73
+ return `${p.actor} can't bring in conversations: the org config's mcp.actions doesn't list ${AGENT_IMPORT_ACTION}.`;
74
+ if (ws !== p.workspace && !p.grants.includes(ws))
75
+ return `${p.actor} can bring in conversations of workspace ${p.workspace} only${p.grants.length ? ` (or ${p.grants.join(", ")}, its grants)` : ""}, not ${ws}.`;
76
+ return null;
77
+ }
78
+
79
+ /** The import route's workspace, decoded; null when it isn't it. */
80
+ export function agentImportTarget(pathname: string): { ws: string } | null {
81
+ const m = AGENT_IMPORT_ROUTE.exec(pathname);
82
+ if (!m) return null;
83
+ try {
84
+ return { ws: decodeURIComponent(m[1]) };
85
+ } catch {
86
+ return null;
87
+ }
88
+ }