@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.
Files changed (36) hide show
  1. package/CHANGELOG.md +140 -0
  2. package/README.md +21 -7
  3. package/docs/distributions.md +221 -0
  4. package/package.json +4 -3
  5. package/skills/install-den/SKILL.md +114 -0
  6. package/src/shared/agent-setup.ts +9 -0
  7. package/src/shared/agents/den-claude.ts +19 -5
  8. package/src/shared/agents/guardrails/hooks.json +27 -0
  9. package/src/shared/agents/guardrails/instructions.md +39 -0
  10. package/src/shared/agents/guardrails/pretooluse.mjs +430 -0
  11. package/src/shared/agents/guardrails/stop-hook.ts +66 -0
  12. package/src/shared/agents/guardrails.ts +114 -0
  13. package/src/shared/agents/handback.ts +241 -0
  14. package/src/shared/agents/personas.ts +55 -6
  15. package/src/shared/constants.ts +7 -0
  16. package/src/shared/den-config.ts +67 -6
  17. package/src/shared/dev-metrics/emit.ts +59 -5
  18. package/src/shared/dev-metrics/polling.ts +135 -0
  19. package/src/shared/engine.ts +14 -2
  20. package/src/shared/port-pick.ts +60 -0
  21. package/src/ui/cli/agent-ops.ts +7 -3
  22. package/src/ui/cli/agents-command.ts +51 -1
  23. package/src/ui/cli/create-main.ts +1 -1
  24. package/src/ui/cli/create.ts +263 -30
  25. package/src/ui/cli/doctor-command.ts +87 -4
  26. package/src/ui/cli/doctor-onboard.ts +291 -0
  27. package/src/ui/cli/flag-parser.ts +5 -0
  28. package/src/ui/cli/index.ts +352 -5
  29. package/src/ui/cli/json-out.ts +49 -0
  30. package/src/ui/cli/mcp-connect.ts +179 -15
  31. package/src/ui/cli/repo-command.ts +490 -0
  32. package/src/ui/cli/repo-detect.ts +192 -0
  33. package/src/ui/cli/web-command.ts +3 -0
  34. package/src/ui/mcp/connect.ts +134 -0
  35. package/src/ui/server/public/settings-registry.js +3 -3
  36. package/src/ui/server/request-guard.ts +1 -1
@@ -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
- `signed in with ${how}`,
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 lock are a person's.`;
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];