@monster-orange/den 0.4.2 → 0.4.4
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 +273 -0
- package/README.md +9 -9
- package/docs/distributions.md +313 -50
- package/package.json +1 -1
- package/src/shared/agents/catalog-runtime.ts +178 -0
- package/src/shared/agents/catalog.ts +343 -0
- package/src/shared/agents/den-claude.ts +53 -12
- package/src/shared/agents/generic.ts +1 -0
- package/src/shared/agents/runtime-search.ts +543 -0
- package/src/shared/agents/runtime.ts +43 -1
- package/src/shared/agents/runtimes.ts +53 -4
- package/src/shared/auth/sudo-vault.ts +16 -0
- package/src/shared/den-config.ts +92 -2
- package/src/shared/edge/render.ts +31 -0
- package/src/shared/edge/status.ts +134 -0
- package/src/shared/environment-manager/health.ts +82 -8
- package/src/shared/environment-manager/workspaces.ts +54 -0
- package/src/shared/health/not-checked.ts +42 -0
- package/src/shared/health/rollup.ts +4 -0
- package/src/shared/process/spawn-streaming.ts +8 -3
- package/src/shared/sessions/client.ts +5 -1
- package/src/shared/sessions/host.ts +29 -1
- package/src/shared/sessions/protocol.ts +6 -1
- package/src/shared/sessions/types.ts +6 -0
- package/src/shared/stacks/status.ts +24 -0
- package/src/shared/strategies/registry.ts +5 -0
- package/src/shared/types/index.ts +6 -0
- package/src/ui/cli/agent-ops.ts +298 -19
- package/src/ui/cli/agents-command.ts +129 -4
- package/src/ui/cli/edge-command.ts +16 -3
- package/src/ui/cli/health-display.ts +4 -1
- package/src/ui/cli/sessions-command.ts +9 -4
- package/src/ui/cli/web-command.ts +53 -8
- package/src/ui/mcp/scope.ts +23 -3
- package/src/ui/mcp/server.ts +31 -7
- package/src/ui/mcp/tools.ts +327 -20
- package/src/ui/mcp/views.ts +450 -0
- package/src/ui/server/agent-routes.ts +123 -26
- package/src/ui/server/edge-exposure.ts +143 -0
- package/src/ui/server/index.ts +171 -25
- package/src/ui/server/public/agent-start.js +241 -0
- package/src/ui/server/public/agents-page.js +246 -45
- package/src/ui/server/public/agents-section.js +7 -21
- package/src/ui/server/public/agents.html +55 -1
- package/src/ui/server/public/graph-model.js +2 -0
- package/src/ui/server/public/header.js +2 -3
- package/src/ui/server/public/health-rollup.js +20 -8
- package/src/ui/server/public/i18n/es.js +26 -13
- package/src/ui/server/public/i18n/ja.js +25 -13
- package/src/ui/server/public/index.html +17 -4
- package/src/ui/server/public/local-dev.html +62 -22
- package/src/ui/server/public/services.html +21 -5
- package/src/ui/server/public/settings-page.js +3 -2
- package/src/ui/server/public/settings-registry.js +226 -1
- package/src/ui/server/public/settings.html +90 -1
- package/src/ui/server/public/shell.js +67 -22
- package/src/ui/server/public/topbar.js +217 -158
- package/src/ui/server/readonly-routes.ts +29 -5
- package/src/ui/server/request-guard.ts +123 -8
- package/src/ui/server/route-classes.ts +14 -2
- package/src/ui/server/session-routes.ts +9 -1
- package/src/ui/server/sudo-store.ts +170 -6
- package/src/ui/server/ui-assets.ts +2 -0
- package/src/ui/server/workspace-routes.ts +10 -3
|
@@ -1371,6 +1371,34 @@
|
|
|
1371
1371
|
}
|
|
1372
1372
|
.agent-form select {
|
|
1373
1373
|
max-width: 320px;
|
|
1374
|
+
min-width: 0;
|
|
1375
|
+
}
|
|
1376
|
+
.agent-form-help {
|
|
1377
|
+
flex-basis: 100%;
|
|
1378
|
+
margin: 0;
|
|
1379
|
+
font-size: 12px;
|
|
1380
|
+
color: var(--muted);
|
|
1381
|
+
overflow-wrap: anywhere;
|
|
1382
|
+
}
|
|
1383
|
+
.agent-form-help[hidden],
|
|
1384
|
+
.agent-form-ready[hidden] {
|
|
1385
|
+
display: none;
|
|
1386
|
+
}
|
|
1387
|
+
/* AG-0.2: the runtime it will run, or why it can't (the mock's .helpl). */
|
|
1388
|
+
.agent-form-ready {
|
|
1389
|
+
flex-basis: 100%;
|
|
1390
|
+
order: 1;
|
|
1391
|
+
margin: 0;
|
|
1392
|
+
font-size: 12px;
|
|
1393
|
+
line-height: 1.4;
|
|
1394
|
+
color: var(--faint);
|
|
1395
|
+
overflow-wrap: anywhere;
|
|
1396
|
+
}
|
|
1397
|
+
.agent-form-ready[data-tone="warn"] {
|
|
1398
|
+
color: var(--warn);
|
|
1399
|
+
}
|
|
1400
|
+
.agent-form-ready .mono {
|
|
1401
|
+
font-family: var(--mono);
|
|
1374
1402
|
}
|
|
1375
1403
|
.agent-cwd {
|
|
1376
1404
|
max-width: 280px;
|
|
@@ -1459,8 +1487,8 @@
|
|
|
1459
1487
|
initChangelog,
|
|
1460
1488
|
copyToClipboard,
|
|
1461
1489
|
computePrimaryByRepo,
|
|
1462
|
-
requireUnlock,
|
|
1463
1490
|
setServiceRunner,
|
|
1491
|
+
sudoRun,
|
|
1464
1492
|
takePendingAction,
|
|
1465
1493
|
problemCards,
|
|
1466
1494
|
problemFix,
|
|
@@ -1517,13 +1545,12 @@
|
|
|
1517
1545
|
.replace(/>/g, ">")
|
|
1518
1546
|
.replace(/"/g, """);
|
|
1519
1547
|
|
|
1520
|
-
// ── The
|
|
1521
|
-
//
|
|
1522
|
-
//
|
|
1523
|
-
//
|
|
1524
|
-
|
|
1525
|
-
|
|
1526
|
-
}
|
|
1548
|
+
// ── The admin password (R8, LK-0) ─────────────────────────────────────────
|
|
1549
|
+
// It is for sudo, and nothing else: no action here asks for it up
|
|
1550
|
+
// front. A run whose command is about to run sudo without it gets a
|
|
1551
|
+
// needs-sudo event from the server (sudoRun().see); the shell's
|
|
1552
|
+
// prompt then says what needs it, and after the password the action
|
|
1553
|
+
// runs again (sudoRun().done).
|
|
1527
1554
|
|
|
1528
1555
|
// ── Bootstrap ─────────────────────────────────────────────────────────────
|
|
1529
1556
|
// Systems with auto-start toggled OFF in the viewed workspace (`autostart:
|
|
@@ -1543,7 +1570,7 @@
|
|
|
1543
1570
|
]);
|
|
1544
1571
|
initLogBadge();
|
|
1545
1572
|
// Start and Restart from the status drawer and the ⌘K palette run
|
|
1546
|
-
// here, with this page's
|
|
1573
|
+
// here, with this page's output (R15, R17).
|
|
1547
1574
|
setServiceRunner((cmd, s) =>
|
|
1548
1575
|
window.runCmd(s.address, s.repo, s.service, cmd),
|
|
1549
1576
|
);
|
|
@@ -3021,10 +3048,10 @@
|
|
|
3021
3048
|
|
|
3022
3049
|
// ── Workspace-level command (SSE) ─────────────────────────────────────────
|
|
3023
3050
|
// Routes output to each service's own tab as `label` events arrive.
|
|
3024
|
-
window.runWorkspaceSync = function () {
|
|
3025
|
-
if (!requireSudo()) return;
|
|
3051
|
+
window.runWorkspaceSync = async function () {
|
|
3026
3052
|
setAllButtons(true);
|
|
3027
3053
|
setWsStatus("syncing…");
|
|
3054
|
+
const sudo = sudoRun();
|
|
3028
3055
|
const es = new EventSource("/api/workspace/sync");
|
|
3029
3056
|
// Clear previous sync output then open/activate the tab (explicit action).
|
|
3030
3057
|
clearDismissed("⟳ sync");
|
|
@@ -3033,6 +3060,7 @@
|
|
|
3033
3060
|
setBusy("⟳ sync", true);
|
|
3034
3061
|
es.onmessage = (e) => {
|
|
3035
3062
|
const msg = JSON.parse(e.data);
|
|
3063
|
+
sudo.see(msg);
|
|
3036
3064
|
if (msg.type === "line") appendToTab("⟳ sync", msg.text);
|
|
3037
3065
|
if (msg.type === "error")
|
|
3038
3066
|
appendToTab("⟳ sync", "Error: " + msg.text, "err");
|
|
@@ -3044,6 +3072,7 @@
|
|
|
3044
3072
|
setTimeout(() => setWsStatus(""), 3000);
|
|
3045
3073
|
// See refreshAfterSync — a sync that clones brings new commands with it.
|
|
3046
3074
|
refreshAfterSync();
|
|
3075
|
+
void sudo.done(() => window.runWorkspaceSync());
|
|
3047
3076
|
}
|
|
3048
3077
|
};
|
|
3049
3078
|
es.onerror = () => {
|
|
@@ -3073,10 +3102,10 @@
|
|
|
3073
3102
|
// rather than a shared pane, so it sits with everything else for that
|
|
3074
3103
|
// system. Buttons are not globally disabled: it is one repo.
|
|
3075
3104
|
const _syncing = new Set();
|
|
3076
|
-
window.runRepoSync = function (repo) {
|
|
3077
|
-
if (!requireSudo()) return;
|
|
3105
|
+
window.runRepoSync = async function (repo) {
|
|
3078
3106
|
if (_syncing.has(repo)) return; // already in flight — ignore re-entry
|
|
3079
3107
|
_syncing.add(repo);
|
|
3108
|
+
const sudo = sudoRun();
|
|
3080
3109
|
|
|
3081
3110
|
const route = tabRouteFor(repo);
|
|
3082
3111
|
clearDismissed(route); // explicit action — undo any prior dismissal
|
|
@@ -3101,12 +3130,14 @@
|
|
|
3101
3130
|
);
|
|
3102
3131
|
es.onmessage = (e) => {
|
|
3103
3132
|
const msg = JSON.parse(e.data);
|
|
3133
|
+
sudo.see(msg);
|
|
3104
3134
|
if (msg.type === "line") appendToTab(route, msg.text);
|
|
3105
3135
|
if (msg.type === "error")
|
|
3106
3136
|
appendToTab(route, "Error: " + msg.text, "err");
|
|
3107
3137
|
if (msg.type === "done") {
|
|
3108
3138
|
es.close();
|
|
3109
3139
|
finish("sync complete");
|
|
3140
|
+
void sudo.done(() => window.runRepoSync(repo));
|
|
3110
3141
|
// A pull can replace this repo's health probes on disk. The health
|
|
3111
3142
|
// response is cached ~8s, so force a fresh poll rather than leaving
|
|
3112
3143
|
// dots that reflect the code we just replaced.
|
|
@@ -3126,8 +3157,8 @@
|
|
|
3126
3157
|
};
|
|
3127
3158
|
};
|
|
3128
3159
|
|
|
3129
|
-
window.runWorkspaceCmd = function (cmd, addresses) {
|
|
3130
|
-
|
|
3160
|
+
window.runWorkspaceCmd = async function (cmd, addresses) {
|
|
3161
|
+
const sudo = sudoRun();
|
|
3131
3162
|
const only =
|
|
3132
3163
|
Array.isArray(addresses) && addresses.length
|
|
3133
3164
|
? `&addresses=${encodeURIComponent(addresses.join(","))}`
|
|
@@ -3145,6 +3176,7 @@
|
|
|
3145
3176
|
|
|
3146
3177
|
esWs.onmessage = (e) => {
|
|
3147
3178
|
const msg = JSON.parse(e.data);
|
|
3179
|
+
sudo.see(msg);
|
|
3148
3180
|
if (msg.type === "label") {
|
|
3149
3181
|
// New service section — route to the SYSTEM's primary tab (one tab
|
|
3150
3182
|
// per system; sidecar output lands in its repo's tab with a header).
|
|
@@ -3210,6 +3242,7 @@
|
|
|
3210
3242
|
idle();
|
|
3211
3243
|
// Lifecycle changed run-state — sync dots now, don't wait for the poll.
|
|
3212
3244
|
syncHealthNow();
|
|
3245
|
+
void sudo.done(() => window.runWorkspaceCmd(cmd, addresses));
|
|
3213
3246
|
}
|
|
3214
3247
|
};
|
|
3215
3248
|
esWs.onerror = () => {
|
|
@@ -3240,8 +3273,7 @@
|
|
|
3240
3273
|
// is derived on the SERVER (see the `seed` param in /api/workspace/exec):
|
|
3241
3274
|
// its rules are load-bearing, and the already-healthy skip it relies on is
|
|
3242
3275
|
// suppressed whenever an explicit address list is sent.
|
|
3243
|
-
window.runUpWithDeps = function (repo) {
|
|
3244
|
-
if (!requireSudo()) return;
|
|
3276
|
+
window.runUpWithDeps = async function (repo) {
|
|
3245
3277
|
void runWorkspaceCmdAsync("up", null, {
|
|
3246
3278
|
seed: repo,
|
|
3247
3279
|
undismiss: true,
|
|
@@ -3252,16 +3284,14 @@
|
|
|
3252
3284
|
// same closure as "up with deps", reversed. Derived server-side for the
|
|
3253
3285
|
// same reason as its counterpart: the already-stopped skip is suppressed
|
|
3254
3286
|
// when an explicit address list is sent.
|
|
3255
|
-
window.runDownWithDeps = function (repo) {
|
|
3256
|
-
if (!requireSudo()) return;
|
|
3287
|
+
window.runDownWithDeps = async function (repo) {
|
|
3257
3288
|
void runWorkspaceCmdAsync("down", null, {
|
|
3258
3289
|
seed: repo,
|
|
3259
3290
|
undismiss: true,
|
|
3260
3291
|
});
|
|
3261
3292
|
};
|
|
3262
3293
|
|
|
3263
|
-
window.runCmd = function (address, repo, service, cmd) {
|
|
3264
|
-
if (!requireSudo()) return;
|
|
3294
|
+
window.runCmd = async function (address, repo, service, cmd) {
|
|
3265
3295
|
// Explicit single-service action — undo any prior dismissal so its pane returns.
|
|
3266
3296
|
clearDismissed(address);
|
|
3267
3297
|
// up/restart/down go through the workspace-exec runner: it uses the full
|
|
@@ -3290,9 +3320,12 @@
|
|
|
3290
3320
|
setTabStatus(address, { es: esCmd });
|
|
3291
3321
|
const _cmdTab = _tabs.get(address);
|
|
3292
3322
|
if (_cmdTab) _cmdTab.esKind = "exec";
|
|
3323
|
+
const sudo = sudoRun();
|
|
3324
|
+
const again = () => window.runCmd(address, repo, service, cmd);
|
|
3293
3325
|
|
|
3294
3326
|
esCmd.onmessage = (e) => {
|
|
3295
3327
|
const msg = JSON.parse(e.data);
|
|
3328
|
+
sudo.see(msg);
|
|
3296
3329
|
if (msg.type === "cmd") {
|
|
3297
3330
|
appendToTab(address, msg.text, "cmd");
|
|
3298
3331
|
} else if (msg.type === "started") {
|
|
@@ -3320,6 +3353,7 @@
|
|
|
3320
3353
|
"done-fail",
|
|
3321
3354
|
);
|
|
3322
3355
|
setTabStatus(address, { live: false, errored: true, es: null });
|
|
3356
|
+
void sudo.done(again);
|
|
3323
3357
|
}
|
|
3324
3358
|
} else if (msg.type === "line") {
|
|
3325
3359
|
appendToTab(address, msg.text, "");
|
|
@@ -3340,6 +3374,7 @@
|
|
|
3340
3374
|
);
|
|
3341
3375
|
_tabs.get(address) && (_tabs.get(address).busy = false);
|
|
3342
3376
|
setTabStatus(address, { live: false, errored: !ok, es: null });
|
|
3377
|
+
void sudo.done(again);
|
|
3343
3378
|
}
|
|
3344
3379
|
};
|
|
3345
3380
|
esCmd.onerror = () => {
|
|
@@ -3824,9 +3859,11 @@
|
|
|
3824
3859
|
// address filter — the filter suppresses the already-healthy skip and
|
|
3825
3860
|
// the auto-start-off handling that make "with deps" behave.
|
|
3826
3861
|
if (opts.seed) params.set("seed", opts.seed);
|
|
3862
|
+
const sudo = sudoRun();
|
|
3827
3863
|
const es = new EventSource(`/api/workspace/exec?${params}`);
|
|
3828
3864
|
es.onmessage = (e) => {
|
|
3829
3865
|
const msg = JSON.parse(e.data);
|
|
3866
|
+
sudo.see(msg);
|
|
3830
3867
|
if (msg.type === "label") {
|
|
3831
3868
|
currentAddress = msg.text;
|
|
3832
3869
|
// Route to the system's primary tab; skip dismissed panes on
|
|
@@ -3895,7 +3932,10 @@
|
|
|
3895
3932
|
idle();
|
|
3896
3933
|
// Lifecycle changed run-state — sync dots now, don't wait for the poll.
|
|
3897
3934
|
syncHealthNow();
|
|
3898
|
-
|
|
3935
|
+
// LK-0: after the admin password, the same run again.
|
|
3936
|
+
void sudo
|
|
3937
|
+
.done(() => runWorkspaceCmdAsync(cmd, addresses, opts))
|
|
3938
|
+
.then((again) => resolve(again ?? ok));
|
|
3899
3939
|
}
|
|
3900
3940
|
};
|
|
3901
3941
|
es.onerror = () => {
|
|
@@ -333,6 +333,7 @@
|
|
|
333
333
|
initChangelog,
|
|
334
334
|
computePrimaryByRepo,
|
|
335
335
|
envReady,
|
|
336
|
+
sudoRun,
|
|
336
337
|
} from "/header.js";
|
|
337
338
|
import { ownersLabel } from "/workspace-picker.js";
|
|
338
339
|
|
|
@@ -613,10 +614,16 @@
|
|
|
613
614
|
const usableCmds = isSub
|
|
614
615
|
? s.commands.filter((cmd) => !LIFECYCLE_CMDS.has(cmd))
|
|
615
616
|
: s.commands;
|
|
617
|
+
// The workspace the row was built from (the first in view that
|
|
618
|
+
// defines it, focus first): its command runs there, not in
|
|
619
|
+
// whichever workspace is focused.
|
|
620
|
+
const wsAttr = s.inWorkspaces?.[0]
|
|
621
|
+
? ` data-workspace="${esc(s.inWorkspaces[0])}"`
|
|
622
|
+
: "";
|
|
616
623
|
const cmdButtons = usableCmds
|
|
617
624
|
.map(
|
|
618
625
|
(cmd) =>
|
|
619
|
-
`<button class="cmd-btn" data-repo="${s.repo}" data-service="${s.service}" data-cmd="${cmd}" data-type="exec">${cmd}</button>`,
|
|
626
|
+
`<button class="cmd-btn" data-repo="${s.repo}" data-service="${s.service}" data-cmd="${cmd}" data-type="exec"${wsAttr}>${cmd}</button>`,
|
|
620
627
|
)
|
|
621
628
|
.join("");
|
|
622
629
|
const logsButton = s.logs
|
|
@@ -743,8 +750,9 @@
|
|
|
743
750
|
// Attach click handlers
|
|
744
751
|
pane.querySelectorAll(".cmd-btn").forEach((btn) => {
|
|
745
752
|
btn.addEventListener("click", () => {
|
|
746
|
-
const { repo, service, cmd, type } = btn.dataset;
|
|
747
|
-
if (type === "exec")
|
|
753
|
+
const { repo, service, cmd, type, workspace } = btn.dataset;
|
|
754
|
+
if (type === "exec")
|
|
755
|
+
runExec(repo, service, cmd, btn, workspace);
|
|
748
756
|
else runLogs(repo, service, btn);
|
|
749
757
|
});
|
|
750
758
|
});
|
|
@@ -765,7 +773,7 @@
|
|
|
765
773
|
}
|
|
766
774
|
}
|
|
767
775
|
|
|
768
|
-
function runExec(repo, service, cmd, btn) {
|
|
776
|
+
function runExec(repo, service, cmd, btn, workspace) {
|
|
769
777
|
endStream();
|
|
770
778
|
const env = document.getElementById("env-sel").value;
|
|
771
779
|
|
|
@@ -781,8 +789,13 @@
|
|
|
781
789
|
appendLine(`\n▶ ${repo}/${service} · ${cmd} · ${env}\n`, "cmd");
|
|
782
790
|
|
|
783
791
|
const params = new URLSearchParams({ repo, service, cmd, env });
|
|
792
|
+
if (workspace) params.set("workspace", workspace);
|
|
784
793
|
es = new EventSource(`/api/service/exec?${params}`);
|
|
785
|
-
|
|
794
|
+
// LK-0: a command that needs sudo without the password asks,
|
|
795
|
+
// then runs again.
|
|
796
|
+
handleSSE(es, repo, service, {
|
|
797
|
+
again: () => runExec(repo, service, cmd, btn, workspace),
|
|
798
|
+
});
|
|
786
799
|
}
|
|
787
800
|
|
|
788
801
|
function runLogs(repo, service, btn) {
|
|
@@ -819,8 +832,10 @@
|
|
|
819
832
|
const isStream = opts.stream === true;
|
|
820
833
|
const id = runId;
|
|
821
834
|
const appendLine = (text, cls) => runs().append(id, text, cls);
|
|
835
|
+
const sudo = sudoRun();
|
|
822
836
|
evtSrc.onmessage = (e) => {
|
|
823
837
|
const msg = JSON.parse(e.data);
|
|
838
|
+
sudo.see(msg);
|
|
824
839
|
if (msg.type === "cmd") {
|
|
825
840
|
appendLine(msg.text, "cmd");
|
|
826
841
|
} else if (msg.type === "line") {
|
|
@@ -842,6 +857,7 @@
|
|
|
842
857
|
} else runs().set(id, { running: false });
|
|
843
858
|
evtSrc.close();
|
|
844
859
|
es = null;
|
|
860
|
+
if (opts.again) void sudo.done(opts.again);
|
|
845
861
|
}
|
|
846
862
|
};
|
|
847
863
|
evtSrc.onerror = () => {
|
|
@@ -300,7 +300,8 @@ export async function mountSettings(deps) {
|
|
|
300
300
|
const did = `d-${id}`;
|
|
301
301
|
const v = value(r);
|
|
302
302
|
const t = r.control?.type || "readonly";
|
|
303
|
-
|
|
303
|
+
// A custom control draws (and saves) itself: no write to read it by.
|
|
304
|
+
const ro = !r.write && t !== "custom";
|
|
304
305
|
const single = ["number", "select", "toggle", "text"].includes(t);
|
|
305
306
|
const label = single
|
|
306
307
|
? `<label class="l" id="${lid}" for="${cid}">${esc(r.label)}</label>`
|
|
@@ -499,7 +500,7 @@ export async function mountSettings(deps) {
|
|
|
499
500
|
}, 2000);
|
|
500
501
|
}
|
|
501
502
|
function wireRow(r) {
|
|
502
|
-
if (!r.write) return;
|
|
503
|
+
if (!r.write && r.control?.type !== "custom") return;
|
|
503
504
|
const row = $(`setting-${slug(r.id)}`);
|
|
504
505
|
if (!row) return;
|
|
505
506
|
const t = r.control?.type;
|
|
@@ -64,6 +64,13 @@ export const DEN_SETTING_CATEGORIES = [
|
|
|
64
64
|
description:
|
|
65
65
|
"Settings for one workspace. They follow the Workspace you view in the top bar.",
|
|
66
66
|
},
|
|
67
|
+
{
|
|
68
|
+
id: "agents",
|
|
69
|
+
label: "Agents",
|
|
70
|
+
order: 55,
|
|
71
|
+
description:
|
|
72
|
+
"The runtimes agents run on, installed first. Paths you set are this machine's only: everywhere (config/local/user.yml) or for one workspace (config/local/workspaces.yml), which wins.",
|
|
73
|
+
},
|
|
67
74
|
{
|
|
68
75
|
id: "cli",
|
|
69
76
|
label: "Command line",
|
|
@@ -416,6 +423,29 @@ export const DEN_SETTINGS = [
|
|
|
416
423
|
write: (v, ctx) => ctx.saveWorkspace({ editor: v || null }),
|
|
417
424
|
},
|
|
418
425
|
|
|
426
|
+
// ── Agents ─────────────────────────────────────────────────────────────
|
|
427
|
+
{
|
|
428
|
+
id: "agents.runtimes",
|
|
429
|
+
category: "agents",
|
|
430
|
+
label: "Runtimes",
|
|
431
|
+
description:
|
|
432
|
+
"Each runtime's status and version, the path in use and where it came from. Set path… runs its version command first and refuses a path that isn't that runtime.",
|
|
433
|
+
keywords: [
|
|
434
|
+
"runtime",
|
|
435
|
+
"claude",
|
|
436
|
+
"codex",
|
|
437
|
+
"gemini",
|
|
438
|
+
"grok",
|
|
439
|
+
"path",
|
|
440
|
+
"bin",
|
|
441
|
+
"cli",
|
|
442
|
+
"agent",
|
|
443
|
+
],
|
|
444
|
+
scope: "user",
|
|
445
|
+
control: { type: "custom", render: renderRuntimes },
|
|
446
|
+
read: () => null,
|
|
447
|
+
},
|
|
448
|
+
|
|
419
449
|
// ── Command line ───────────────────────────────────────────────────────
|
|
420
450
|
{
|
|
421
451
|
id: "cli.local_dev",
|
|
@@ -472,7 +502,7 @@ export const DEN_SETTINGS = [
|
|
|
472
502
|
category: "cli",
|
|
473
503
|
label: "Browser sign-in",
|
|
474
504
|
description:
|
|
475
|
-
"Signing in is needed only when this server is reached
|
|
505
|
+
"Signing in is needed only when this server is reached from another machine (through the edge, or a LAN address); on localhost, and through the edge from this machine, nothing asks for it. __DEN_CLI__ web login prints a sign-in link; __DEN_CLI__ web sessions lists signed-in browsers.",
|
|
476
506
|
keywords: ["sign in", "sign out", "login", "logout", "session", "browser"],
|
|
477
507
|
scope: "user",
|
|
478
508
|
control: {
|
|
@@ -498,6 +528,53 @@ export const DEN_SETTINGS = [
|
|
|
498
528
|
},
|
|
499
529
|
read: () => null,
|
|
500
530
|
},
|
|
531
|
+
{
|
|
532
|
+
// R8, LK-0: the admin password is for sudo, and nothing else. Den asks
|
|
533
|
+
// for it only when a command it runs needs sudo; here is whether it is
|
|
534
|
+
// saved, and Forget. Reading never wakes the vault.
|
|
535
|
+
id: "cli.sudo",
|
|
536
|
+
category: "cli",
|
|
537
|
+
label: "Admin password",
|
|
538
|
+
description:
|
|
539
|
+
"Used only when a command Den runs needs sudo, and asked for only then. It is no sign-in: nothing else needs it.",
|
|
540
|
+
keywords: ["admin", "password", "sudo", "forget", "vault", "1password"],
|
|
541
|
+
scope: "user",
|
|
542
|
+
control: {
|
|
543
|
+
type: "custom",
|
|
544
|
+
render(host, ctx) {
|
|
545
|
+
const draw = (st) => {
|
|
546
|
+
const saved = st?.saved;
|
|
547
|
+
const where =
|
|
548
|
+
saved === "vault"
|
|
549
|
+
? `Saved in ${st.vault?.label || "the vault"}`
|
|
550
|
+
: saved === "session"
|
|
551
|
+
? "Saved for this session"
|
|
552
|
+
: "Not saved";
|
|
553
|
+
host.innerHTML =
|
|
554
|
+
'<output class="set-ro" id="set-sudo"></output>' +
|
|
555
|
+
(saved
|
|
556
|
+
? ' <button type="button" class="btn" id="set-sudo-forget">Forget</button>'
|
|
557
|
+
: "");
|
|
558
|
+
host.querySelector("output").textContent = where;
|
|
559
|
+
host
|
|
560
|
+
.querySelector("#set-sudo-forget")
|
|
561
|
+
?.addEventListener("click", () =>
|
|
562
|
+
ctx.shell
|
|
563
|
+
.api("POST", "/api/sudo-password/forget", {})
|
|
564
|
+
.then(load)
|
|
565
|
+
.catch(load),
|
|
566
|
+
);
|
|
567
|
+
};
|
|
568
|
+
const load = () =>
|
|
569
|
+
fetch("/api/sudo-password/status", { cache: "no-store" })
|
|
570
|
+
.then((r) => (r.ok ? r.json() : null))
|
|
571
|
+
.then(draw)
|
|
572
|
+
.catch(() => draw(null));
|
|
573
|
+
void load();
|
|
574
|
+
},
|
|
575
|
+
},
|
|
576
|
+
read: () => null,
|
|
577
|
+
},
|
|
501
578
|
|
|
502
579
|
// ── Distribution (read-only) ───────────────────────────────────────────
|
|
503
580
|
distRow(
|
|
@@ -547,6 +624,154 @@ export const DEN_SETTINGS = [
|
|
|
547
624
|
),
|
|
548
625
|
];
|
|
549
626
|
|
|
627
|
+
// ── Settings › Agents › Runtimes (AG-0.5) ────────────────────────────────
|
|
628
|
+
|
|
629
|
+
const escR = (s) =>
|
|
630
|
+
String(s ?? "").replace(
|
|
631
|
+
/[&<>"']/g,
|
|
632
|
+
(c) =>
|
|
633
|
+
({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" })[
|
|
634
|
+
c
|
|
635
|
+
],
|
|
636
|
+
);
|
|
637
|
+
|
|
638
|
+
/** Where a runtime's path came from, as the row says it. */
|
|
639
|
+
export const RUNTIME_SOURCE = {
|
|
640
|
+
workspace: "workspace",
|
|
641
|
+
setting: "global",
|
|
642
|
+
"login-shell": "detected · login shell",
|
|
643
|
+
PATH: "detected · PATH",
|
|
644
|
+
"well-known": "detected · install directory",
|
|
645
|
+
"interactive-shell": "detected · interactive shell",
|
|
646
|
+
};
|
|
647
|
+
|
|
648
|
+
/** One runtime's row (r: GET /api/agents/runtimes' entry). */
|
|
649
|
+
export function runtimeRowHtml(r, scopeWs) {
|
|
650
|
+
const id = escR(r.id);
|
|
651
|
+
const name =
|
|
652
|
+
`<b>${escR(r.label || r.id)}</b>` +
|
|
653
|
+
(r.vendor ? ` <span class="rt-v">${escR(r.vendor)}</span>` : "") +
|
|
654
|
+
(r.denTools === false
|
|
655
|
+
? ` <span class="set-tag" data-den-tools="coming">Den tools: coming</span>`
|
|
656
|
+
: "");
|
|
657
|
+
const set = r.source === "workspace" || r.source === "setting";
|
|
658
|
+
const status = r.available
|
|
659
|
+
? `<span class="rt-ok">✓ ${escR(r.version ? r.version : "installed")}</span>`
|
|
660
|
+
: `<span class="rt-bad">${escR(set ? r.why : "Not installed")}</span>`;
|
|
661
|
+
const where = r.path
|
|
662
|
+
? `<span class="den-mono rt-p">${escR(r.path)}</span> <span class="set-tag" data-src="${escR(r.source || "")}">${escR(RUNTIME_SOURCE[r.source] || r.source || "")}</span>`
|
|
663
|
+
: set
|
|
664
|
+
? `<span class="set-tag" data-src="${escR(r.source)}">${escR(RUNTIME_SOURCE[r.source])}</span>`
|
|
665
|
+
: "";
|
|
666
|
+
// A path set at the scope shown can be cleared from here.
|
|
667
|
+
const mine = scopeWs ? r.source === "workspace" : r.source === "setting";
|
|
668
|
+
return (
|
|
669
|
+
`<div class="rt" data-runtime="${id}" data-available="${r.available ? "true" : "false"}">` +
|
|
670
|
+
`<div class="rt-n">${name}</div>` +
|
|
671
|
+
`<div class="rt-s">${status}</div>` +
|
|
672
|
+
`<div class="rt-w">${where}</div>` +
|
|
673
|
+
`<div class="rt-a"><button type="button" class="btn sm" data-set-path="${id}">Set path…</button>` +
|
|
674
|
+
(mine
|
|
675
|
+
? ` <button type="button" class="btn sm" data-clear-path="${id}">Clear</button>`
|
|
676
|
+
: "") +
|
|
677
|
+
`</div>` +
|
|
678
|
+
`<form class="rt-f" data-path-form="${id}" hidden>` +
|
|
679
|
+
`<label class="rt-fl" for="rt-in-${id}">Path to ${escR(r.label || r.id)}${scopeWs ? ` for ${escR(scopeWs)}` : " (every workspace)"}</label>` +
|
|
680
|
+
`<input type="text" id="rt-in-${id}" class="den-mono" spellcheck="false" autocomplete="off" placeholder="/absolute/path/to/${escR(r.id === "den-claude" ? "claude" : r.id)}" value="${escR(mine ? r.path || "" : "")}">` +
|
|
681
|
+
`<button type="submit" class="btn sm primary">Save</button> <button type="button" class="btn sm" data-cancel-path>Cancel</button>` +
|
|
682
|
+
`<div class="rt-err" role="alert" hidden></div></form>` +
|
|
683
|
+
`</div>`
|
|
684
|
+
);
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
/** The Runtimes row: the list, a workspace selector, Set path… and Clear. */
|
|
688
|
+
function renderRuntimes(host, ctx) {
|
|
689
|
+
const wss = ctx.workspaces || [];
|
|
690
|
+
let ws = "";
|
|
691
|
+
const draw = (rows, note) => {
|
|
692
|
+
host.innerHTML =
|
|
693
|
+
`<div class="rt-scope"><label for="rt-ws">Paths for</label> <select id="rt-ws" data-focus-key="rt-ws">` +
|
|
694
|
+
`<option value="">every workspace (user.yml)</option>` +
|
|
695
|
+
wss
|
|
696
|
+
.map(
|
|
697
|
+
(w) =>
|
|
698
|
+
`<option value="${escR(w)}"${w === ws ? " selected" : ""}>${escR(w)} (workspaces.yml)</option>`,
|
|
699
|
+
)
|
|
700
|
+
.join("") +
|
|
701
|
+
`</select></div>` +
|
|
702
|
+
(note ? `<div class="set-note">${escR(note)}</div>` : "") +
|
|
703
|
+
`<div class="rt-list" role="list">${rows.map((r) => runtimeRowHtml(r, ws)).join("")}</div>`;
|
|
704
|
+
};
|
|
705
|
+
const load = () => {
|
|
706
|
+
// Under an invalid config the runtimes answer 422: say why, ask nothing.
|
|
707
|
+
if (ctx.shell?.configInvalid?.() === true)
|
|
708
|
+
return draw([], "Fix the config first: the runtimes are read from it.");
|
|
709
|
+
return ctx.shell
|
|
710
|
+
.api(
|
|
711
|
+
"GET",
|
|
712
|
+
`/api/agents/runtimes${ws ? `?workspace=${encodeURIComponent(ws)}` : ""}`,
|
|
713
|
+
)
|
|
714
|
+
.then((b) =>
|
|
715
|
+
// Only what has a CLI to find: generic is the persona's argv.
|
|
716
|
+
draw((b?.runtimes || []).filter((r) => r.setting)),
|
|
717
|
+
)
|
|
718
|
+
.catch((e) => draw([], e?.message || "The runtimes did not load."));
|
|
719
|
+
};
|
|
720
|
+
host.addEventListener("change", (e) => {
|
|
721
|
+
if (e.target.id !== "rt-ws") return;
|
|
722
|
+
ws = e.target.value;
|
|
723
|
+
void load();
|
|
724
|
+
});
|
|
725
|
+
host.addEventListener("click", async (e) => {
|
|
726
|
+
const b = e.target.closest("button");
|
|
727
|
+
if (!b) return;
|
|
728
|
+
const row = b.closest(".rt");
|
|
729
|
+
const form = row?.querySelector(".rt-f");
|
|
730
|
+
if (b.dataset.setPath && form) {
|
|
731
|
+
form.hidden = false;
|
|
732
|
+
form.querySelector("input").focus();
|
|
733
|
+
} else if (b.hasAttribute("data-cancel-path") && form) {
|
|
734
|
+
form.hidden = true;
|
|
735
|
+
} else if (b.dataset.clearPath) {
|
|
736
|
+
await savePath(b.dataset.clearPath, null, form);
|
|
737
|
+
}
|
|
738
|
+
});
|
|
739
|
+
host.addEventListener("submit", async (e) => {
|
|
740
|
+
e.preventDefault();
|
|
741
|
+
const form = e.target.closest(".rt-f");
|
|
742
|
+
if (!form) return;
|
|
743
|
+
const bin = form.querySelector("input").value.trim();
|
|
744
|
+
if (!bin) return;
|
|
745
|
+
await savePath(form.dataset.pathForm, bin, form);
|
|
746
|
+
});
|
|
747
|
+
async function savePath(id, bin, form) {
|
|
748
|
+
const err = form?.querySelector(".rt-err");
|
|
749
|
+
const btns = form ? [...form.querySelectorAll("button")] : [];
|
|
750
|
+
btns.forEach((x) => (x.disabled = true));
|
|
751
|
+
try {
|
|
752
|
+
await ctx.shell.api(
|
|
753
|
+
"POST",
|
|
754
|
+
`/api/agents/runtimes/${encodeURIComponent(id)}/path`,
|
|
755
|
+
{
|
|
756
|
+
scope: ws ? "workspace" : "global",
|
|
757
|
+
...(ws ? { workspace: ws } : {}),
|
|
758
|
+
bin,
|
|
759
|
+
},
|
|
760
|
+
);
|
|
761
|
+
await load();
|
|
762
|
+
} catch (x) {
|
|
763
|
+
// Its refusal is shown here, under the field, not as a toast.
|
|
764
|
+
if (err) {
|
|
765
|
+
err.textContent = x?.message || "Not saved";
|
|
766
|
+
err.hidden = false;
|
|
767
|
+
}
|
|
768
|
+
} finally {
|
|
769
|
+
btns.forEach((x) => (x.disabled = false));
|
|
770
|
+
}
|
|
771
|
+
}
|
|
772
|
+
void load();
|
|
773
|
+
}
|
|
774
|
+
|
|
550
775
|
/** Register Den's categories and rows with the shell's SETTINGS registry. */
|
|
551
776
|
export function registerDenSettings(shell) {
|
|
552
777
|
for (const c of DEN_SETTING_CATEGORIES) shell.registerSettingsCategory(c);
|