@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.
- package/CHANGELOG.md +151 -0
- package/README.md +7 -7
- package/docs/distributions.md +189 -11
- package/package.json +1 -1
- package/src/shared/agents/conversations.ts +227 -0
- package/src/shared/agents/default-pack/be-dev.md +1 -0
- package/src/shared/agents/default-pack/devops.md +1 -0
- package/src/shared/agents/default-pack/fe-dev.md +1 -0
- package/src/shared/agents/default-pack/orchestrator.md +3 -1
- package/src/shared/agents/default-pack/planner.md +1 -0
- package/src/shared/agents/default-pack/reviewer.md +1 -0
- package/src/shared/agents/default-pack/tester.md +16 -0
- package/src/shared/agents/default-pack/ux-designer.md +1 -0
- package/src/shared/agents/den-claude.ts +128 -13
- package/src/shared/agents/permissions.ts +108 -0
- package/src/shared/agents/persona-store.ts +286 -0
- package/src/shared/agents/personas.ts +116 -12
- package/src/shared/agents/records.ts +33 -0
- package/src/shared/agents/roles.ts +288 -0
- package/src/shared/agents/runtime.ts +50 -0
- package/src/shared/den-config.ts +22 -1
- package/src/shared/environment-manager/service-runner.ts +31 -12
- package/src/shared/environment-manager/workspaces.ts +11 -0
- package/src/ui/cli/agent-ops.ts +796 -29
- package/src/ui/cli/agents-command.ts +97 -3
- package/src/ui/cli/env-probe.ts +4 -1
- package/src/ui/mcp/api.ts +12 -1
- package/src/ui/mcp/tools.ts +60 -1
- package/src/ui/server/agent-resume-grant.ts +35 -0
- package/src/ui/server/agent-routes.ts +412 -22
- package/src/ui/server/public/agent-start.js +3 -2
- package/src/ui/server/public/agents-page.js +693 -49
- package/src/ui/server/public/agents.html +76 -15
- package/src/ui/server/public/i18n/es.js +143 -0
- package/src/ui/server/public/i18n/ja.js +144 -0
- package/src/ui/server/public/persona-ui.css +563 -0
- package/src/ui/server/public/persona-ui.js +796 -0
- package/src/ui/server/public/settings-page.js +4 -9
- package/src/ui/server/public/settings-registry.js +22 -0
- package/src/ui/server/public/settings.html +1 -0
- package/src/ui/server/public/shell.js +140 -0
- package/src/ui/server/public/topbar.js +2 -2
- package/src/ui/server/request-guard.ts +17 -4
- package/src/ui/server/route-classes.ts +31 -0
- package/src/ui/server/session-routes.ts +21 -5
- 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
|
-
[
|
|
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);
|
package/src/ui/cli/env-probe.ts
CHANGED
|
@@ -112,7 +112,10 @@ function probeInput(d: ProbeDeps, dir: string): LaunchInput {
|
|
|
112
112
|
return {
|
|
113
113
|
persona: {
|
|
114
114
|
id: PROBE_ID,
|
|
115
|
-
|
|
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
|
-
/**
|
|
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
|
/**
|
package/src/ui/mcp/tools.ts
CHANGED
|
@@ -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
|
+
}
|