@monster-orange/den 0.4.5 → 0.4.7
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 +140 -0
- package/README.md +21 -7
- package/docs/distributions.md +221 -0
- package/package.json +4 -3
- package/skills/install-den/SKILL.md +114 -0
- package/src/shared/agent-setup.ts +9 -0
- package/src/shared/agents/den-claude.ts +19 -5
- package/src/shared/agents/guardrails/hooks.json +27 -0
- package/src/shared/agents/guardrails/instructions.md +39 -0
- package/src/shared/agents/guardrails/pretooluse.mjs +430 -0
- package/src/shared/agents/guardrails/stop-hook.ts +66 -0
- package/src/shared/agents/guardrails.ts +114 -0
- package/src/shared/agents/handback.ts +241 -0
- package/src/shared/agents/personas.ts +55 -6
- package/src/shared/constants.ts +7 -0
- package/src/shared/den-config.ts +67 -6
- package/src/shared/dev-metrics/emit.ts +59 -5
- package/src/shared/dev-metrics/polling.ts +135 -0
- package/src/shared/engine.ts +14 -2
- package/src/shared/port-pick.ts +60 -0
- package/src/ui/cli/agent-ops.ts +7 -3
- package/src/ui/cli/agents-command.ts +51 -1
- package/src/ui/cli/create-main.ts +1 -1
- package/src/ui/cli/create.ts +263 -30
- package/src/ui/cli/doctor-command.ts +87 -4
- package/src/ui/cli/doctor-onboard.ts +291 -0
- package/src/ui/cli/flag-parser.ts +5 -0
- package/src/ui/cli/index.ts +352 -5
- package/src/ui/cli/json-out.ts +49 -0
- package/src/ui/cli/mcp-connect.ts +179 -15
- package/src/ui/cli/repo-command.ts +490 -0
- package/src/ui/cli/repo-detect.ts +192 -0
- package/src/ui/cli/web-command.ts +3 -0
- package/src/ui/mcp/connect.ts +134 -0
- package/src/ui/server/public/settings-registry.js +3 -3
- package/src/ui/server/request-guard.ts +1 -1
package/src/ui/mcp/connect.ts
CHANGED
|
@@ -46,6 +46,13 @@ export type ConnectAction =
|
|
|
46
46
|
name: string;
|
|
47
47
|
entry: Record<string, unknown>;
|
|
48
48
|
}
|
|
49
|
+
| {
|
|
50
|
+
/** One `[mcp_servers.<name>]` table in a TOML config (Grok CLI). */
|
|
51
|
+
kind: "toml";
|
|
52
|
+
file: string;
|
|
53
|
+
name: string;
|
|
54
|
+
entry: Record<string, unknown>;
|
|
55
|
+
}
|
|
49
56
|
| {
|
|
50
57
|
kind: "exec";
|
|
51
58
|
/** Run first, failure ignored: makes the add idempotent. */
|
|
@@ -209,12 +216,49 @@ export const MCP_CLIENTS: readonly McpClient[] = [
|
|
|
209
216
|
s,
|
|
210
217
|
),
|
|
211
218
|
},
|
|
219
|
+
{
|
|
220
|
+
// Grok CLI (xAI). Its MCP servers live in ~/.grok/config.toml (user)
|
|
221
|
+
// or .grok/config.toml (project), one [mcp_servers.<name>] table each,
|
|
222
|
+
// with command, args and env keys; `grok mcp add` documents no env flag,
|
|
223
|
+
// so Den writes the table itself, PATH and all.
|
|
224
|
+
// https://docs.x.ai/build/features/mcp-servers
|
|
225
|
+
id: "grok",
|
|
226
|
+
title: "Grok CLI",
|
|
227
|
+
scopes: ["user", "project"],
|
|
228
|
+
detect: (e) => e.which("grok") || e.exists(path.join(e.home, ".grok")),
|
|
229
|
+
plan: (s, scope, e) => ({
|
|
230
|
+
kind: "toml",
|
|
231
|
+
file: path.join(
|
|
232
|
+
scope === "project" ? e.projectDir : e.home,
|
|
233
|
+
".grok",
|
|
234
|
+
"config.toml",
|
|
235
|
+
),
|
|
236
|
+
name: s.name,
|
|
237
|
+
entry: stdioEntry(s),
|
|
238
|
+
}),
|
|
239
|
+
},
|
|
212
240
|
];
|
|
213
241
|
|
|
214
242
|
export function findClient(id: string): McpClient | undefined {
|
|
215
243
|
return MCP_CLIENTS.find((c) => c.id === id);
|
|
216
244
|
}
|
|
217
245
|
|
|
246
|
+
/**
|
|
247
|
+
* `mcp connect --runtime <runtime>` (ON-2): the agent runtime's own name
|
|
248
|
+
* (AG-0.5's catalog: claude, codex, gemini, grok) to its MCP client id.
|
|
249
|
+
*/
|
|
250
|
+
export const RUNTIME_CLIENTS: Readonly<Record<string, string>> = {
|
|
251
|
+
claude: "claude-code",
|
|
252
|
+
codex: "codex",
|
|
253
|
+
gemini: "gemini",
|
|
254
|
+
grok: "grok",
|
|
255
|
+
};
|
|
256
|
+
|
|
257
|
+
export function clientForRuntime(runtime: string): McpClient | undefined {
|
|
258
|
+
const id = RUNTIME_CLIENTS[runtime];
|
|
259
|
+
return id ? findClient(id) : undefined;
|
|
260
|
+
}
|
|
261
|
+
|
|
218
262
|
/** The PATH a launched server needs: node's own directory first. */
|
|
219
263
|
export function serverPath(nodeBinDir: string, platform: NodeJS.Platform) {
|
|
220
264
|
const base =
|
|
@@ -304,3 +348,93 @@ export function genericSnippet(spec: ServerSpec): string {
|
|
|
304
348
|
2,
|
|
305
349
|
);
|
|
306
350
|
}
|
|
351
|
+
|
|
352
|
+
// ── TOML configs (Grok CLI) ─────────────────────────────────────────────────
|
|
353
|
+
|
|
354
|
+
/** A TOML basic string: JSON's escaping is a subset TOML reads the same. */
|
|
355
|
+
const tomlString = (v: string): string => JSON.stringify(v);
|
|
356
|
+
|
|
357
|
+
function tomlValue(v: unknown): string {
|
|
358
|
+
if (typeof v === "string") return tomlString(v);
|
|
359
|
+
if (Array.isArray(v)) return `[${v.map(tomlValue).join(", ")}]`;
|
|
360
|
+
if (v && typeof v === "object")
|
|
361
|
+
return `{ ${Object.entries(v as Record<string, unknown>)
|
|
362
|
+
.map(
|
|
363
|
+
([k, x]) =>
|
|
364
|
+
`${/^[A-Za-z0-9_-]+$/.test(k) ? k : tomlString(k)} = ${tomlValue(x)}`,
|
|
365
|
+
)
|
|
366
|
+
.join(", ")} }`;
|
|
367
|
+
return String(v);
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
/** The `[mcp_servers.<name>]` table for `entry`, newline-terminated. */
|
|
371
|
+
export function tomlTable(
|
|
372
|
+
name: string,
|
|
373
|
+
entry: Record<string, unknown>,
|
|
374
|
+
): string {
|
|
375
|
+
return [
|
|
376
|
+
`[mcp_servers.${name}]`,
|
|
377
|
+
...Object.entries(entry).map(([k, v]) => `${k} = ${tomlValue(v)}`),
|
|
378
|
+
"",
|
|
379
|
+
].join("\n");
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
/** Whether a header line opens `name`'s table or one of its sub-tables. */
|
|
383
|
+
function ownsHeader(line: string, name: string): boolean {
|
|
384
|
+
const m =
|
|
385
|
+
/^\s*\[\s*mcp_servers\.(?:"([^"]+)"|([A-Za-z0-9_-]+))\s*(\]|\.)/.exec(line);
|
|
386
|
+
return !!m && (m[1] ?? m[2]) === name;
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
/** `text` split into [before, name's table, after] (the table may be ""). */
|
|
390
|
+
function splitTable(text: string, name: string): [string, string, string] {
|
|
391
|
+
const lines = text.split("\n");
|
|
392
|
+
const start = lines.findIndex((l) => ownsHeader(l, name));
|
|
393
|
+
if (start === -1) return [text, "", ""];
|
|
394
|
+
let end = start + 1;
|
|
395
|
+
while (
|
|
396
|
+
end < lines.length &&
|
|
397
|
+
!(/^\s*\[/.test(lines[end]) && !ownsHeader(lines[end], name))
|
|
398
|
+
)
|
|
399
|
+
end++;
|
|
400
|
+
return [
|
|
401
|
+
lines.slice(0, start).join("\n"),
|
|
402
|
+
lines.slice(start, end).join("\n"),
|
|
403
|
+
lines.slice(end).join("\n"),
|
|
404
|
+
];
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
/**
|
|
408
|
+
* `text` (a TOML config, or null) with `name`'s `[mcp_servers.<name>]`
|
|
409
|
+
* table replaced by `entry` (or removed, for null). Everything else in the
|
|
410
|
+
* file is kept as written.
|
|
411
|
+
*/
|
|
412
|
+
export function mergeTomlConfig(
|
|
413
|
+
text: string | null,
|
|
414
|
+
name: string,
|
|
415
|
+
entry: Record<string, unknown> | null,
|
|
416
|
+
): string {
|
|
417
|
+
const [before, , after] = splitTable(text ?? "", name);
|
|
418
|
+
const head = before.replace(/\n*$/, "");
|
|
419
|
+
const tail = after.replace(/^\n*/, "").replace(/\n*$/, "");
|
|
420
|
+
const parts = [
|
|
421
|
+
head,
|
|
422
|
+
entry ? tomlTable(name, entry).trimEnd() : "",
|
|
423
|
+
tail,
|
|
424
|
+
].filter(Boolean);
|
|
425
|
+
return parts.length ? `${parts.join("\n\n")}\n` : "";
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
/** Whether a TOML config already has `name`'s table, as `entry` writes it. */
|
|
429
|
+
export function tomlHasEntry(
|
|
430
|
+
text: string | null,
|
|
431
|
+
name: string,
|
|
432
|
+
entry: Record<string, unknown>,
|
|
433
|
+
): "connected" | "different" | "absent" {
|
|
434
|
+
if (!text) return "absent";
|
|
435
|
+
const [, table] = splitTable(text, name);
|
|
436
|
+
if (!table) return "absent";
|
|
437
|
+
return table.trim() === tomlTable(name, entry).trim()
|
|
438
|
+
? "connected"
|
|
439
|
+
: "different";
|
|
440
|
+
}
|
|
@@ -156,17 +156,17 @@ export function seenAgo(iso, now = Date.now()) {
|
|
|
156
156
|
export function deviceLine(d, now = Date.now()) {
|
|
157
157
|
const how =
|
|
158
158
|
d.via === "github"
|
|
159
|
-
? `GitHub${d.login ? ` (@${d.login})` : ""}`
|
|
159
|
+
? `signed in with GitHub${d.login ? ` (@${d.login})` : ""}`
|
|
160
160
|
: d.via === "code"
|
|
161
161
|
? "approved with a code"
|
|
162
|
-
: "one-time link";
|
|
162
|
+
: "signed in with a one-time link";
|
|
163
163
|
const name = d.name || "Unknown browser";
|
|
164
164
|
const from = d.client || d.host;
|
|
165
165
|
return {
|
|
166
166
|
title: name,
|
|
167
167
|
current: !!d.current,
|
|
168
168
|
detail: [
|
|
169
|
-
|
|
169
|
+
how,
|
|
170
170
|
`created ${seenAgo(d.createdAt, now)}`,
|
|
171
171
|
from ? `from ${from} (${name})` : "",
|
|
172
172
|
`last seen ${seenAgo(d.lastSeenAt, now)}`,
|
|
@@ -695,7 +695,7 @@ export function agentRefusal(
|
|
|
695
695
|
method !== "HEAD" &&
|
|
696
696
|
AGENT_READ_ONLY.some((r) => pathMatches(r, pathname)))
|
|
697
697
|
)
|
|
698
|
-
return `${p.actor} can't use ${pathname}: signing in, web.secret and the
|
|
698
|
+
return `${p.actor} can't use ${pathname}: signing in, web.secret and the admin password are a person's.`;
|
|
699
699
|
const m = SESSION_ROUTE.exec(pathname);
|
|
700
700
|
if (m) {
|
|
701
701
|
let id = m[1];
|