@monster-orange/den 0.3.0 → 0.4.1

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 (79) hide show
  1. package/CHANGELOG.md +593 -0
  2. package/docs/distributions.md +429 -18
  3. package/package.json +6 -1
  4. package/src/shared/actor-token.ts +275 -0
  5. package/src/shared/agent-actor.ts +12 -2
  6. package/src/shared/agents/default-pack/be-dev.md +20 -0
  7. package/src/shared/agents/default-pack/devops.md +14 -0
  8. package/src/shared/agents/default-pack/fe-dev.md +20 -0
  9. package/src/shared/agents/default-pack/orchestrator.md +16 -0
  10. package/src/shared/agents/default-pack/planner.md +17 -0
  11. package/src/shared/agents/default-pack/reviewer.md +14 -0
  12. package/src/shared/agents/default-pack/ux-designer.md +15 -0
  13. package/src/shared/agents/den-claude.ts +271 -0
  14. package/src/shared/agents/generic.ts +52 -0
  15. package/src/shared/agents/personas.ts +218 -0
  16. package/src/shared/agents/records.ts +118 -0
  17. package/src/shared/agents/runtime.ts +199 -0
  18. package/src/shared/agents/runtimes.ts +54 -0
  19. package/src/shared/den-config.ts +116 -0
  20. package/src/shared/engine.ts +23 -0
  21. package/src/shared/environment-manager/scope.ts +22 -0
  22. package/src/shared/environment-manager/service-runner.ts +94 -2
  23. package/src/shared/environment-manager/workspaces.ts +19 -0
  24. package/src/shared/profile.ts +15 -4
  25. package/src/shared/secret-env.ts +70 -0
  26. package/src/shared/sessions/client.ts +284 -0
  27. package/src/shared/sessions/den-pty.ts +97 -0
  28. package/src/shared/sessions/env.ts +89 -0
  29. package/src/shared/sessions/host.ts +690 -0
  30. package/src/shared/sessions/names.ts +85 -0
  31. package/src/shared/sessions/paths.ts +27 -0
  32. package/src/shared/sessions/protocol.ts +72 -0
  33. package/src/shared/sessions/pty.ts +141 -0
  34. package/src/shared/sessions/ring-buffer.ts +160 -0
  35. package/src/shared/sessions/types.ts +81 -0
  36. package/src/shared/web-secret.ts +181 -0
  37. package/src/shared/web-session.ts +491 -0
  38. package/src/ui/cli/agent-ops.ts +843 -0
  39. package/src/ui/cli/agents-command.ts +294 -0
  40. package/src/ui/cli/doctor-command.ts +138 -9
  41. package/src/ui/cli/env-probe.ts +266 -0
  42. package/src/ui/cli/flag-parser.ts +32 -3
  43. package/src/ui/cli/index.ts +128 -12
  44. package/src/ui/cli/sessions-command.ts +750 -0
  45. package/src/ui/cli/target-workspace.ts +105 -0
  46. package/src/ui/cli/web-command.ts +536 -14
  47. package/src/ui/mcp/api.ts +7 -0
  48. package/src/ui/mcp/scope.ts +123 -19
  49. package/src/ui/mcp/server.ts +100 -7
  50. package/src/ui/mcp/tools.ts +44 -5
  51. package/src/ui/server/agent-routes.ts +323 -0
  52. package/src/ui/server/audit.ts +111 -22
  53. package/src/ui/server/http-errors.ts +21 -3
  54. package/src/ui/server/index.ts +820 -65
  55. package/src/ui/server/public/agents-page.js +648 -0
  56. package/src/ui/server/public/agents-section.js +299 -0
  57. package/src/ui/server/public/agents.html +439 -0
  58. package/src/ui/server/public/appearance.js +3 -0
  59. package/src/ui/server/public/error-page.js +103 -0
  60. package/src/ui/server/public/header.js +45 -11
  61. package/src/ui/server/public/i18n/es.js +90 -1
  62. package/src/ui/server/public/i18n/ja.js +86 -1
  63. package/src/ui/server/public/local-dev.html +53 -0
  64. package/src/ui/server/public/logs.html +3 -0
  65. package/src/ui/server/public/panel.js +1 -1
  66. package/src/ui/server/public/release.html +8 -0
  67. package/src/ui/server/public/settings-page.js +5 -0
  68. package/src/ui/server/public/settings-registry.js +33 -0
  69. package/src/ui/server/public/shell.js +248 -5
  70. package/src/ui/server/public/topbar.js +51 -2
  71. package/src/ui/server/readonly-routes.ts +25 -2
  72. package/src/ui/server/request-guard.ts +670 -0
  73. package/src/ui/server/route-classes.ts +317 -0
  74. package/src/ui/server/session-routes.ts +445 -0
  75. package/src/ui/server/shutdown-guard.ts +91 -0
  76. package/src/ui/server/sudo-store.ts +29 -3
  77. package/src/ui/server/ui-assets.ts +10 -1
  78. package/src/ui/server/websocket.ts +326 -0
  79. package/src/ui/server/workspace-routes.ts +16 -22
@@ -0,0 +1,299 @@
1
+ /**
2
+ * The Workspaces page's Agents section (AG-4a, AG spec §5c): under the
3
+ * repo table, one row per agent of the selected workspace (persona and id,
4
+ * runtime, state, cwd, started at), Stop and, where the record allows it,
5
+ * Resume; "+ Start agent" picks a persona and a runtime. The workspace
6
+ * header counts them ("n agents", R24c).
7
+ *
8
+ * Every call is the server's agent routes (agent-routes.ts), which are the
9
+ * CLI's own operations: nothing here keeps an agent's state. Each action
10
+ * goes through denShell.api() (a refusal is its error toast) after the
11
+ * lock rule (requireUnlock, R8), as a service action does. A row with a
12
+ * session has Open (its terminal on the Agents page, AG-4a-term), and the
13
+ * header links to the Agents page for the workspace.
14
+ *
15
+ * The page's table styles (table.svc, .tablewrap, .tbl-*) and the shell's
16
+ * (.den-sec, .den-btn, .den-dot) are reused as they are.
17
+ */
18
+ import { requireUnlock } from "/topbar.js";
19
+
20
+ const t = (key, vars) => window.denShell?.t?.(key, vars) ?? key;
21
+ const esc = (s) =>
22
+ String(s ?? "").replace(
23
+ /[&<>"']/g,
24
+ (c) =>
25
+ ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#39;" })[
26
+ c
27
+ ],
28
+ );
29
+ const api = (method, url, body) => window.denShell.api(method, url, body);
30
+ const base = (ws) => `/api/workspaces/${encodeURIComponent(ws)}/agents`;
31
+
32
+ /** How often the list is asked again (faster while one is stopping). */
33
+ const REFRESH_MS = 15_000;
34
+ const STOPPING_MS = 1_000;
35
+
36
+ let current = null; // { ws, rows, timer }
37
+ let personasAsked = null;
38
+
39
+ /** The section's frame, drawn with the page; mountAgents fills it. */
40
+ export function agentsSectionHtml(ws) {
41
+ return `<section class="agents" id="agents-section" data-ws="${esc(ws)}" aria-labelledby="agents-h">
42
+ <div class="ag-head">
43
+ <h2 class="den-sec" id="agents-h">${esc(t("ag.title"))}</h2>
44
+ <a href="/agents?ws=${encodeURIComponent(ws)}" id="agents-open-all">${esc(t("ws.openAgents"))}</a>
45
+ </div>
46
+ <div class="tablewrap">
47
+ <table class="svc" id="agent-table">
48
+ <caption>${esc(t("ag.caption", { ws }))}</caption>
49
+ <thead><tr>
50
+ <th scope="col">${esc(t("ag.col.agent"))}</th>
51
+ <th scope="col">${esc(t("ag.col.runtime"))}</th>
52
+ <th scope="col">${esc(t("ag.col.state"))}</th>
53
+ <th scope="col">${esc(t("ag.col.cwd"))}</th>
54
+ <th scope="col">${esc(t("ag.col.started"))}</th>
55
+ <th scope="col"><span class="tbl-sr">${esc(t("ag.col.actions"))}</span></th>
56
+ </tr></thead>
57
+ <tbody><tr class="tbl-none"><td colspan="6">${esc(t("ag.loading"))}</td></tr></tbody>
58
+ </table>
59
+ </div>
60
+ <div class="tbl-foot">
61
+ <button type="button" class="add-svc-open-btn" id="agent-start-open" aria-expanded="false" aria-controls="agent-start-form">${esc(t("ag.start"))}</button>
62
+ <form class="agent-form" id="agent-start-form" hidden>
63
+ <label>${esc(t("ag.persona"))} <select class="form-sel" name="persona" required></select></label>
64
+ <label>${esc(t("ag.runtime"))} <select class="form-sel" name="runtime"></select></label>
65
+ <button type="submit" class="den-btn sm primary" id="agent-start-go">${esc(t("ag.go"))}</button>
66
+ <button type="button" class="den-btn sm" data-agent-cancel>${esc(t("ag.cancel"))}</button>
67
+ </form>
68
+ </div>
69
+ </section>`;
70
+ }
71
+
72
+ /** A state word in the page's language (the server's word is the key). */
73
+ function stateText(state) {
74
+ const m = /^exited(?: \((-?\d+)\))?$/.exec(state);
75
+ if (m)
76
+ return m[1] === undefined
77
+ ? t("ag.state.exited")
78
+ : t("ag.state.exitedCode", { code: m[1] });
79
+ return ["running", "stopping", "stopped", "ended"].includes(state)
80
+ ? t(`ag.state.${state}`)
81
+ : state;
82
+ }
83
+
84
+ /** The state cell: the repo table's health look (dot, then the word). */
85
+ function stateCell(state) {
86
+ const cls =
87
+ state === "running"
88
+ ? { tone: " ok", dot: "" }
89
+ : state === "stopping"
90
+ ? { tone: "", dot: "pending" }
91
+ : /^exited \((?!0\))/.test(state)
92
+ ? { tone: " bad", dot: "bad" }
93
+ : { tone: "", dot: "off" };
94
+ return `<span class="tbl-health${cls.tone}" data-state="${esc(state)}"><span class="den-dot ${cls.dot}" aria-hidden="true"></span><span class="tbl-ht">${esc(stateText(state))}</span></span>`;
95
+ }
96
+
97
+ function when(iso) {
98
+ if (!iso) return "—";
99
+ const d = new Date(iso);
100
+ if (Number.isNaN(d.getTime())) return "—";
101
+ try {
102
+ return new Intl.DateTimeFormat(window.denShell?.lang?.() || undefined, {
103
+ dateStyle: "short",
104
+ timeStyle: "short",
105
+ }).format(d);
106
+ } catch {
107
+ return d.toISOString();
108
+ }
109
+ }
110
+
111
+ function rowHtml(r) {
112
+ const running = r.state === "running" || r.state === "stopping";
113
+ const btn = (act, label, name) =>
114
+ `<button type="button" class="den-btn sm tbl-btn" data-agent-act="${act}" data-id="${esc(r.agent)}" aria-label="${esc(name)}">${esc(label)}</button>`;
115
+ // Open: the session's terminal on the Agents page (AG-4a-term).
116
+ const sid = r.session?.ref?.id;
117
+ const open = sid
118
+ ? `<a class="den-btn sm tbl-btn" data-agent-open href="/agents?open=${encodeURIComponent(sid)}" aria-label="${esc(t("ag.openName", { id: r.agent }))}">${esc(t("ag.open"))}</a>`
119
+ : "";
120
+ const acts =
121
+ open +
122
+ (running
123
+ ? btn("stop", t("ag.stop"), t("ag.stopName", { id: r.agent }))
124
+ : "") +
125
+ (r.resumable
126
+ ? btn("resume", t("ag.resume"), t("ag.resumeName", { id: r.agent }))
127
+ : "");
128
+ // Every row names its persona, an id that is the persona too.
129
+ const persona = r.persona
130
+ ? ` <span class="tbl-muted">${esc(t("ag.personaOf", { persona: r.persona }))}</span>`
131
+ : "";
132
+ const model = r.model
133
+ ? ` <span class="tbl-muted">· ${esc(r.model)}</span>`
134
+ : "";
135
+ return `<tr class="agent" data-agent="${esc(r.agent)}" data-state="${esc(r.state)}">
136
+ <th scope="row"><span class="repo-name">${esc(r.agent)}</span>${persona}</th>
137
+ <td class="tbl-mono">${esc(r.runtime ?? "—")}${model}</td>
138
+ <td>${stateCell(r.state)}</td>
139
+ <td class="tbl-mono tbl-muted agent-cwd" title="${esc(r.cwd ?? "")}">${esc(r.cwd ?? "—")}</td>
140
+ <td class="tbl-muted">${esc(when(r.startedAt))}</td>
141
+ <td><div class="tbl-acts">${acts}</div></td>
142
+ </tr>`;
143
+ }
144
+
145
+ function paint(ws, rows) {
146
+ const sec = document.getElementById("agents-section");
147
+ if (!sec || sec.dataset.ws !== ws) return;
148
+ const body = sec.querySelector("#agent-table tbody");
149
+ body.innerHTML = rows.length
150
+ ? rows.map(rowHtml).join("")
151
+ : `<tr class="tbl-none"><td colspan="6">${esc(t("ag.none"))}</td></tr>`;
152
+ const count = document.getElementById("ws-agents-count");
153
+ if (count) {
154
+ count.textContent = t("ag.count", { n: rows.length });
155
+ count.hidden = false;
156
+ }
157
+ }
158
+
159
+ /** Ask the list again, and keep asking while the section is shown. */
160
+ async function refresh(ws) {
161
+ if (!current || current.ws !== ws) return;
162
+ clearTimeout(current.timer);
163
+ let rows = current.rows;
164
+ try {
165
+ const data = await api("GET", base(ws));
166
+ rows = Array.isArray(data?.agents) ? data.agents : [];
167
+ } catch (err) {
168
+ // A failed list keeps the last one; the first failure says so.
169
+ if (!current.failed) window.denShell?.errorToast?.(err);
170
+ if (current) current.failed = true;
171
+ }
172
+ if (!current || current.ws !== ws) return;
173
+ current.rows = rows;
174
+ paint(ws, rows);
175
+ if (!document.getElementById("agents-section")) return;
176
+ const soon = rows.some((r) => r.state === "stopping");
177
+ current.timer = setTimeout(
178
+ () => {
179
+ if (!document.hidden) void refresh(ws);
180
+ else if (current)
181
+ current.timer = setTimeout(() => refresh(ws), REFRESH_MS);
182
+ },
183
+ soon ? STOPPING_MS : REFRESH_MS,
184
+ );
185
+ }
186
+
187
+ function fillForm(form, data) {
188
+ const ps = form.querySelector('select[name="persona"]');
189
+ ps.innerHTML = (data.personas ?? [])
190
+ .map(
191
+ (p) =>
192
+ `<option value="${esc(p.id)}">${esc(p.id)}${p.role ? ` — ${esc(p.role)}` : ""}</option>`,
193
+ )
194
+ .join("");
195
+ const rs = form.querySelector('select[name="runtime"]');
196
+ rs.innerHTML =
197
+ `<option value="">${esc(t("ag.runtime.auto"))}</option>` +
198
+ (data.runtimes ?? [])
199
+ .map(
200
+ (r) =>
201
+ `<option value="${esc(r.id)}"${r.available ? "" : " disabled"}>${esc(r.id)}${r.available ? "" : ` — ${esc(t("ag.unavailable"))}`}</option>`,
202
+ )
203
+ .join("");
204
+ }
205
+
206
+ async function openForm(sec) {
207
+ const form = sec.querySelector("#agent-start-form");
208
+ const open = sec.querySelector("#agent-start-open");
209
+ if (!form.hidden) {
210
+ form.hidden = true;
211
+ open.setAttribute("aria-expanded", "false");
212
+ return;
213
+ }
214
+ personasAsked ??= api("GET", "/api/personas").catch((err) => {
215
+ personasAsked = null;
216
+ throw err;
217
+ });
218
+ let data;
219
+ try {
220
+ data = await personasAsked;
221
+ } catch (err) {
222
+ window.denShell?.errorToast?.(err);
223
+ return;
224
+ }
225
+ fillForm(form, data);
226
+ form.hidden = false;
227
+ open.setAttribute("aria-expanded", "true");
228
+ form.querySelector('select[name="persona"]').focus();
229
+ }
230
+
231
+ async function act(ws, verb, id, button) {
232
+ if (!requireUnlock()) return;
233
+ button.disabled = true;
234
+ try {
235
+ await api("POST", `${base(ws)}/${encodeURIComponent(id)}/${verb}`);
236
+ window.denShell?.toast?.(
237
+ t(verb === "stop" ? "ag.stopped" : "ag.resumed", { id }),
238
+ );
239
+ } catch (err) {
240
+ window.denShell?.errorToast?.(err);
241
+ }
242
+ await refresh(ws);
243
+ }
244
+
245
+ async function start(ws, form) {
246
+ if (!requireUnlock()) return;
247
+ const go = form.querySelector("#agent-start-go");
248
+ const persona = form.querySelector('select[name="persona"]').value;
249
+ const runtime = form.querySelector('select[name="runtime"]').value;
250
+ if (!persona) return;
251
+ go.disabled = true;
252
+ go.textContent = t("ag.starting");
253
+ try {
254
+ const r = await api("POST", base(ws), {
255
+ persona,
256
+ ...(runtime ? { runtime } : {}),
257
+ });
258
+ window.denShell?.toast?.(t("ag.started", { id: r?.agent?.id ?? persona }));
259
+ form.hidden = true;
260
+ form
261
+ .closest("section")
262
+ ?.querySelector("#agent-start-open")
263
+ ?.setAttribute("aria-expanded", "false");
264
+ } catch (err) {
265
+ window.denShell?.errorToast?.(err);
266
+ } finally {
267
+ go.disabled = false;
268
+ go.textContent = t("ag.go");
269
+ }
270
+ await refresh(ws);
271
+ }
272
+
273
+ /** Fill the section drawn by agentsSectionHtml(ws) and wire its controls. */
274
+ export function mountAgents(ws) {
275
+ const sec = document.getElementById("agents-section");
276
+ if (!sec || sec.dataset.ws !== ws) return;
277
+ if (current) clearTimeout(current.timer);
278
+ current = { ws, rows: [], timer: null, failed: false };
279
+ sec.addEventListener("click", (e) => {
280
+ const b = e.target.closest("button");
281
+ if (!b || !sec.contains(b)) return;
282
+ if (b.id === "agent-start-open") return void openForm(sec);
283
+ if (b.hasAttribute("data-agent-cancel")) {
284
+ sec.querySelector("#agent-start-form").hidden = true;
285
+ sec
286
+ .querySelector("#agent-start-open")
287
+ .setAttribute("aria-expanded", "false");
288
+ return;
289
+ }
290
+ const verb = b.dataset.agentAct;
291
+ if (verb === "stop" || verb === "resume")
292
+ void act(ws, verb, b.dataset.id, b);
293
+ });
294
+ sec.querySelector("#agent-start-form").addEventListener("submit", (e) => {
295
+ e.preventDefault();
296
+ void start(ws, e.currentTarget);
297
+ });
298
+ void refresh(ws);
299
+ }