@monster-orange/den 0.4.18 → 0.4.20
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 +160 -0
- package/README.md +14 -13
- package/docs/distributions.md +148 -30
- 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/shared/secrets/index.ts +71 -3
- package/src/shared/secrets/onepassword.ts +87 -14
- package/src/shared/secrets/op-token.ts +270 -0
- package/src/shared/secrets/secret-env.ts +19 -6
- package/src/shared/secrets/strategies.ts +6 -0
- package/src/shared/secrets/types.ts +8 -0
- package/src/ui/cli/agent-ops.ts +41 -6
- package/src/ui/cli/create.ts +2 -2
- package/src/ui/cli/doctor-command.ts +54 -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 +1 -1
- package/src/ui/server/held-actions.ts +35 -7
- package/src/ui/server/index.ts +25 -6
- package/src/ui/server/public/agents-page.js +10 -1
- package/src/ui/server/public/error-page.js +39 -1
- package/src/ui/server/public/graph.css +21 -0
- package/src/ui/server/public/graph.js +49 -6
- package/src/ui/server/public/header.js +29 -7
- package/src/ui/server/public/i18n/es.js +38 -0
- package/src/ui/server/public/i18n/ja.js +40 -0
- package/src/ui/server/public/local-dev.html +22 -1
- 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/settings-registry.js +22 -0
- package/src/ui/server/public/shell.js +67 -6
- package/src/ui/server/public/svc-panel.js +27 -14
- 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 +2 -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
|
|
|
@@ -1573,7 +1606,7 @@ export async function startAgent(
|
|
|
1573
1606
|
if (!d.workspaceExists(ws))
|
|
1574
1607
|
throw usage(`${where}: workspace "${ws}" does not exist`, 404);
|
|
1575
1608
|
const cfg = agentsConfig(d);
|
|
1576
|
-
const all = personas(d, cfg);
|
|
1609
|
+
const all = personas(d, cfg, ws);
|
|
1577
1610
|
const persona = all.get(req.persona);
|
|
1578
1611
|
if (!persona)
|
|
1579
1612
|
throw usage(
|
|
@@ -2108,7 +2141,7 @@ export async function resumeAgent(
|
|
|
2108
2141
|
);
|
|
2109
2142
|
const cfg = agentsConfig(d);
|
|
2110
2143
|
const grants = resumeGrants(rec, cfg);
|
|
2111
|
-
const persona = personas(d, cfg).get(rec.persona);
|
|
2144
|
+
const persona = personas(d, cfg, rec.workspace).get(rec.persona);
|
|
2112
2145
|
if (!persona)
|
|
2113
2146
|
throw refused(`agent ${rec.id}'s persona "${rec.persona}" is gone`);
|
|
2114
2147
|
const det = await availableOrRefuse(d, rt, rec.workspace);
|
|
@@ -2312,7 +2345,7 @@ export async function changePersona(
|
|
|
2312
2345
|
const where = "agents persona";
|
|
2313
2346
|
const rec = agentRecord(d, id, ws);
|
|
2314
2347
|
const cfg = agentsConfig(d);
|
|
2315
|
-
const all = personas(d, cfg);
|
|
2348
|
+
const all = personas(d, cfg, rec.workspace);
|
|
2316
2349
|
if (!all.get(req.persona))
|
|
2317
2350
|
throw usage(
|
|
2318
2351
|
`${where}: no persona "${req.persona}" (personas: ${[...all.keys()].sort().join(", ")})`,
|
|
@@ -2747,7 +2780,9 @@ export async function markInterrupted(
|
|
|
2747
2780
|
return found;
|
|
2748
2781
|
}
|
|
2749
2782
|
for (const rec of marked) {
|
|
2750
|
-
const p =
|
|
2783
|
+
const p = isServerScope(rec.workspace)
|
|
2784
|
+
? serverPersona()
|
|
2785
|
+
: all.get(rec.persona);
|
|
2751
2786
|
if (!p || !(p.standing || p.resumeOnRestart)) continue;
|
|
2752
2787
|
if (resumeRefusal(rec)) continue;
|
|
2753
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}`);
|
|
@@ -37,8 +37,13 @@ import { readBrand } from "../../shared/brand.js";
|
|
|
37
37
|
import { hasEdgeBlock, readEdgeConfig } from "../../shared/edge/config.js";
|
|
38
38
|
import { edgeTlsWarnings, tlsStrategyFor } from "../../shared/edge/tls.js";
|
|
39
39
|
import {
|
|
40
|
+
describeOpTokenSource,
|
|
41
|
+
opTokenSourceProblem,
|
|
42
|
+
parseOpTokenSource,
|
|
40
43
|
readSecretsConfig,
|
|
44
|
+
resolveProviderName,
|
|
41
45
|
secretsStatus,
|
|
46
|
+
type SecretsConfig,
|
|
42
47
|
} from "../../shared/secrets/index.js";
|
|
43
48
|
import {
|
|
44
49
|
DEN_PTY_ID,
|
|
@@ -77,7 +82,22 @@ type Selector = (env: NodeJS.ProcessEnv) => DoctorRow;
|
|
|
77
82
|
|
|
78
83
|
const SELECTORS: Record<string, Selector> = {
|
|
79
84
|
secrets(env) {
|
|
80
|
-
|
|
85
|
+
let cfg: SecretsConfig;
|
|
86
|
+
try {
|
|
87
|
+
cfg = readSecretsConfig();
|
|
88
|
+
} catch (err) {
|
|
89
|
+
// A bad secrets.* key (naming the key): a problem, not a crash.
|
|
90
|
+
return {
|
|
91
|
+
kind: "secrets",
|
|
92
|
+
selected: "",
|
|
93
|
+
implicit: false,
|
|
94
|
+
available: false,
|
|
95
|
+
reason: (err as Error).message,
|
|
96
|
+
problem: true,
|
|
97
|
+
warnings: [],
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
const st = secretsStatus(cfg, env);
|
|
81
101
|
return {
|
|
82
102
|
kind: "secrets",
|
|
83
103
|
selected: st.backend,
|
|
@@ -172,7 +192,7 @@ export function doctorRows(env: NodeJS.ProcessEnv = process.env): DoctorRow[] {
|
|
|
172
192
|
...Object.keys(SELECTORS).filter((k) => kinds.includes(k)),
|
|
173
193
|
...kinds.filter((k) => !(k in SELECTORS)),
|
|
174
194
|
];
|
|
175
|
-
|
|
195
|
+
const rows = ordered.map(
|
|
176
196
|
(kind) =>
|
|
177
197
|
SELECTORS[kind]?.(env) ?? {
|
|
178
198
|
kind,
|
|
@@ -184,6 +204,38 @@ export function doctorRows(env: NodeJS.ProcessEnv = process.env): DoctorRow[] {
|
|
|
184
204
|
warnings: [],
|
|
185
205
|
},
|
|
186
206
|
);
|
|
207
|
+
const op = onePasswordTokenRow(env);
|
|
208
|
+
if (op) rows.splice(rows.findIndex((r) => r.kind === "secrets") + 1, 0, op);
|
|
209
|
+
return rows;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* The 1password row: shown only when 1Password is the configured provider
|
|
214
|
+
* (secret_env refs and the engine's own secrets resolve through it) and the
|
|
215
|
+
* configured token source yields no token. A warning, not a failure: a
|
|
216
|
+
* signed-in desktop app needs none. Checks the source without reading a
|
|
217
|
+
* keychain secret, so nothing prompts.
|
|
218
|
+
*/
|
|
219
|
+
function onePasswordTokenRow(env: NodeJS.ProcessEnv): DoctorRow | null {
|
|
220
|
+
try {
|
|
221
|
+
const cfg = readSecretsConfig();
|
|
222
|
+
if (resolveProviderName(cfg, env) !== "1password") return null;
|
|
223
|
+
const src = parseOpTokenSource(cfg.onepassword?.token);
|
|
224
|
+
const why = opTokenSourceProblem(src, { env });
|
|
225
|
+
if (!why) return null;
|
|
226
|
+
return {
|
|
227
|
+
kind: "1password",
|
|
228
|
+
selected: describeOpTokenSource(src),
|
|
229
|
+
implicit: false,
|
|
230
|
+
available: false,
|
|
231
|
+
reason:
|
|
232
|
+
"this process has no service-account token. Set secrets.onepassword.token.",
|
|
233
|
+
problem: false,
|
|
234
|
+
warnings: [why],
|
|
235
|
+
};
|
|
236
|
+
} catch {
|
|
237
|
+
return null;
|
|
238
|
+
}
|
|
187
239
|
}
|
|
188
240
|
|
|
189
241
|
export function formatDoctor(rows: DoctorRow[], title: string): string[] {
|
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,
|