@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.
- package/CHANGELOG.md +177 -0
- package/README.md +14 -13
- package/docs/distributions.md +138 -25
- package/docs/reference.md +1 -1
- package/package.json +2 -1
- package/skills/install-den/SKILL.md +35 -75
- package/src/shared/agents/den-claude.ts +40 -6
- package/src/shared/agents/guardrails/stop-hook.ts +30 -4
- package/src/shared/agents/guardrails.ts +40 -4
- package/src/shared/agents/handback.ts +22 -4
- package/src/shared/agents/personas.ts +32 -5
- package/src/shared/agents/runtime.ts +8 -0
- package/src/shared/agents/server-agent.ts +57 -0
- package/src/shared/agents/server-pack/server.md +45 -0
- package/src/shared/environment-manager/workspaces.ts +4 -0
- package/src/ui/cli/agent-ops.ts +84 -6
- package/src/ui/cli/create.ts +2 -2
- package/src/ui/cli/index.ts +1 -1
- package/src/ui/cli/repo-command.ts +8 -1
- package/src/ui/cli/repo-detect.ts +58 -0
- package/src/ui/cli/server-agent-ops.ts +411 -0
- package/src/ui/mcp/server.ts +29 -12
- package/src/ui/mcp/tools.ts +4 -2
- package/src/ui/server/agent-routes.ts +10 -1
- package/src/ui/server/held-actions.ts +35 -7
- package/src/ui/server/index.ts +20 -5
- package/src/ui/server/public/agents-page.js +33 -1
- package/src/ui/server/public/agents.html +13 -0
- package/src/ui/server/public/error-page.js +39 -1
- package/src/ui/server/public/graph.css +43 -0
- package/src/ui/server/public/graph.js +68 -1
- package/src/ui/server/public/header.js +36 -7
- package/src/ui/server/public/i18n/es.js +72 -0
- package/src/ui/server/public/i18n/ja.js +74 -0
- package/src/ui/server/public/local-dev.html +33 -3
- package/src/ui/server/public/log-source.js +26 -0
- package/src/ui/server/public/logs.html +7 -20
- package/src/ui/server/public/persona-ui.css +6 -0
- package/src/ui/server/public/server-agent-page.js +415 -0
- package/src/ui/server/public/server-agent.html +222 -0
- package/src/ui/server/public/services.html +18 -3
- package/src/ui/server/public/shell.js +274 -31
- package/src/ui/server/public/svc-panel.js +685 -0
- package/src/ui/server/public/topbar.js +13 -1
- package/src/ui/server/route-classes.ts +21 -0
- package/src/ui/server/server-agent-routes.ts +176 -0
- package/src/ui/server/session-routes.ts +12 -1
- package/src/ui/server/ui-assets.ts +6 -0
- package/src/ui/server/workspace-routes.ts +3 -0
package/src/ui/cli/agent-ops.ts
CHANGED
|
@@ -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
|
-
|
|
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 =
|
|
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 {
|
package/src/ui/cli/create.ts
CHANGED
|
@@ -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}`);
|
package/src/ui/cli/index.ts
CHANGED
|
@@ -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
|
-
|
|
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,
|