@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
@@ -10,11 +10,15 @@ import * as path from "path";
10
10
  import chalk from "chalk";
11
11
  import {
12
12
  MCP_CLIENTS,
13
+ RUNTIME_CLIENTS,
14
+ clientForRuntime,
13
15
  findClient,
14
16
  genericSnippet,
15
17
  jsonHasEntry,
16
18
  mergeJsonConfig,
19
+ mergeTomlConfig,
17
20
  serverSpec,
21
+ tomlHasEntry,
18
22
  type ConnectAction,
19
23
  type ConnectEnv,
20
24
  type McpClient,
@@ -33,6 +37,7 @@ import {
33
37
  type OwnServer,
34
38
  } from "./server-identity.js";
35
39
  import { readDenConfig, readMcpConfig } from "../../shared/den-config.js";
40
+ import { printJson } from "./json-out.js";
36
41
 
37
42
  export const MCP_CONNECT_SUBCOMMANDS = [
38
43
  "connect",
@@ -77,15 +82,28 @@ interface Flags {
77
82
  project: string | null;
78
83
  port?: number;
79
84
  dryRun: boolean;
85
+ json: boolean;
86
+ runtime?: string;
80
87
  }
81
88
 
82
89
  function parseFlags(argv: string[]): Flags {
83
- const f: Flags = { clients: [], all: false, project: null, dryRun: false };
90
+ const f: Flags = {
91
+ clients: [],
92
+ all: false,
93
+ project: null,
94
+ dryRun: false,
95
+ json: false,
96
+ };
84
97
  for (let i = 0; i < argv.length; i++) {
85
98
  const a = argv[i];
86
99
  if (a === "--all" || a === "-a") f.all = true;
87
100
  else if (a === "--dry-run" || a === "-n") f.dryRun = true;
88
- else if (a === "--project" || a === "-p") {
101
+ else if (a === "--json") f.json = true;
102
+ else if (a === "--runtime" || a.startsWith("--runtime=")) {
103
+ const v = a.includes("=") ? a.slice(a.indexOf("=") + 1) : argv[++i];
104
+ if (!v) throw new Error("--runtime needs a value");
105
+ f.runtime = v;
106
+ } else if (a === "--project" || a === "-p") {
89
107
  const next = argv[i + 1];
90
108
  if (next && !next.startsWith("-") && !findClient(next)) {
91
109
  f.project = path.resolve(next);
@@ -150,19 +168,21 @@ function apply(
150
168
  a: ConnectAction,
151
169
  remove: boolean,
152
170
  dryRun: boolean,
153
- ): { ok: boolean; said: string } {
154
- if (a.kind === "json") {
171
+ ): { ok: boolean; said: string; file?: string; changed?: boolean } {
172
+ if (a.kind === "json" || a.kind === "toml") {
155
173
  const before = readText(a.file);
156
- const after = mergeJsonConfig(
157
- before,
158
- a.key,
159
- a.name,
160
- remove ? null : a.entry,
161
- );
162
- if (!dryRun) writeConfig(a.file, after);
174
+ const after =
175
+ a.kind === "json"
176
+ ? mergeJsonConfig(before, a.key, a.name, remove ? null : a.entry)
177
+ : mergeTomlConfig(before, a.name, remove ? null : a.entry);
178
+ const changed = after !== (before ?? "");
179
+ // Unchanged is not rewritten: a re-run leaves the file (and its .bak) be.
180
+ if (!dryRun && changed) writeConfig(a.file, after);
163
181
  return {
164
182
  ok: true,
165
- said: `${dryRun ? "would write" : remove ? "removed from" : "wrote"} ${a.file}`,
183
+ file: a.file,
184
+ changed,
185
+ said: `${dryRun ? "would write" : !changed ? "already in" : remove ? "removed from" : "wrote"} ${a.file}`,
166
186
  };
167
187
  }
168
188
  if (remove) {
@@ -192,8 +212,11 @@ function status(
192
212
  env: ConnectEnv,
193
213
  ): string {
194
214
  const a = c.plan(s, scope, env);
195
- if (a.kind === "json") {
196
- const st = jsonHasEntry(readText(a.file), a.key, a.name, a.entry);
215
+ if (a.kind === "json" || a.kind === "toml") {
216
+ const st =
217
+ a.kind === "json"
218
+ ? jsonHasEntry(readText(a.file), a.key, a.name, a.entry)
219
+ : tomlHasEntry(readText(a.file), a.name, a.entry);
197
220
  return st === "connected"
198
221
  ? chalk.green("connected")
199
222
  : st === "different"
@@ -314,7 +337,8 @@ function usage(): string {
314
337
  ` ${cli} mcp clients which agents are installed and connected`,
315
338
  ` ${cli} mcp connect [<client>...|--all] register ${cli}'s MCP server (user scope)`,
316
339
  ` [--project [dir]] in a project's config instead (default: here)`,
317
- ` [--port <n>] [--dry-run]`,
340
+ ` [--port <n>] [--dry-run] [--json]`,
341
+ ` ${cli} mcp connect --runtime <runtime> by the agent's own name: ${Object.keys(RUNTIME_CLIENTS).join(", ")}`,
318
342
  ` ${cli} mcp disconnect <client>... [--project [dir]]`,
319
343
  ` ${cli} mcp config [--port <n>] JSON for any other MCP client`,
320
344
  ``,
@@ -332,9 +356,31 @@ export async function runMcpConnect(argv: string[]): Promise<number> {
332
356
  try {
333
357
  flags = parseFlags(rest);
334
358
  } catch (e) {
359
+ if (rest.includes("--json")) {
360
+ printJson({
361
+ ok: false,
362
+ error: (e as Error).message,
363
+ fix: `${product().cli} mcp connect --runtime <${Object.keys(RUNTIME_CLIENTS).join("|")}> --json`,
364
+ });
365
+ return 1;
366
+ }
335
367
  console.error(chalk.red(`${(e as Error).message}\n`) + usage());
336
368
  return 1;
337
369
  }
370
+ if (flags.json && (sub === "connect" || sub === "disconnect"))
371
+ return runMcpConnectJson(sub, flags);
372
+ if (flags.runtime) {
373
+ const c = clientForRuntime(flags.runtime);
374
+ if (!c) {
375
+ console.error(
376
+ chalk.red(
377
+ `unknown runtime: ${flags.runtime} (${Object.keys(RUNTIME_CLIENTS).join(", ")})\n`,
378
+ ) + usage(),
379
+ );
380
+ return 1;
381
+ }
382
+ flags.clients.push(c.id);
383
+ }
338
384
  const s = spec(flags.port);
339
385
  const scope: Scope = flags.project ? "project" : "user";
340
386
  const env = connectEnv(
@@ -441,3 +487,121 @@ export async function runMcpConnect(argv: string[]): Promise<number> {
441
487
  }
442
488
  return failed ? 1 : 0;
443
489
  }
490
+
491
+ /** One client's result in `mcp connect --json`. */
492
+ export interface ConnectJsonClient {
493
+ id: string;
494
+ title: string;
495
+ ok: boolean;
496
+ said: string;
497
+ file?: string;
498
+ /** For a config file: whether this run changed it (false on a re-run). */
499
+ changed?: boolean;
500
+ }
501
+
502
+ /**
503
+ * `mcp connect|disconnect … --json` (ON-2): one JSON object, `{ ok, server,
504
+ * clients, restart, web, fix? }`. Safe to re-run: a config file that
505
+ * already has the entry is left as it is (changed: false), and a client's
506
+ * own `mcp add` is preceded by its `mcp remove`.
507
+ */
508
+ async function runMcpConnectJson(
509
+ sub: "connect" | "disconnect",
510
+ flags: Flags,
511
+ ): Promise<number> {
512
+ const cli = product().cli;
513
+ const fixAll = `./bin/${cli} mcp connect --runtime <${Object.keys(RUNTIME_CLIENTS).join("|")}> --json`;
514
+ const fail = (error: string, extra: Record<string, unknown> = {}) => {
515
+ printJson({ ok: false, error, ...extra });
516
+ return 1;
517
+ };
518
+ let s: ServerSpec;
519
+ try {
520
+ s = spec(flags.port);
521
+ } catch (e) {
522
+ return fail((e as Error).message, {
523
+ ask: "Run this from your Den folder (the one with bin/ in it)?",
524
+ });
525
+ }
526
+ const scope: Scope = flags.project ? "project" : "user";
527
+ const env = connectEnv(
528
+ flags.project ?? process.env.DEN_INVOKE_CWD ?? process.cwd(),
529
+ );
530
+ const ids = [...flags.clients];
531
+ if (flags.runtime) {
532
+ const c = clientForRuntime(flags.runtime);
533
+ if (!c)
534
+ return fail(
535
+ `unknown runtime "${flags.runtime}": Den connects ${Object.keys(RUNTIME_CLIENTS).join(", ")}`,
536
+ {
537
+ fix: `./bin/${cli} mcp config`,
538
+ ask: "Your agent is not one Den knows. Add the JSON that `mcp config` prints to its MCP settings by hand?",
539
+ },
540
+ );
541
+ ids.push(c.id);
542
+ }
543
+ const unknown = ids.filter((id) => !findClient(id));
544
+ if (unknown.length)
545
+ return fail(`unknown client: ${unknown.join(", ")}`, { fix: fixAll });
546
+ const targets = flags.all
547
+ ? MCP_CLIENTS.filter((c) => c.detect(env) && c.scopes.includes(scope))
548
+ : ids.map((id) => findClient(id)!);
549
+ if (targets.length === 0) return fail("no client named", { fix: fixAll });
550
+
551
+ const url = webUrl(flags.port);
552
+ const probe = await probeWebServer(url);
553
+ if (sub === "connect" && probe.kind === "foreign")
554
+ return fail(
555
+ `${probe.who} answers at ${url}, not this distribution's web server: agents would read it`,
556
+ { ask: pickAnotherPortHint(cli) },
557
+ );
558
+
559
+ const clients: ConnectJsonClient[] = [];
560
+ for (const c of targets) {
561
+ if (!c.scopes.includes(scope)) {
562
+ clients.push({
563
+ id: c.id,
564
+ title: c.title,
565
+ ok: false,
566
+ said: `no ${scope} scope`,
567
+ });
568
+ continue;
569
+ }
570
+ try {
571
+ const r = apply(
572
+ c.plan(s, scope, env),
573
+ sub === "disconnect",
574
+ flags.dryRun,
575
+ );
576
+ clients.push({ id: c.id, title: c.title, ...r });
577
+ } catch (e) {
578
+ clients.push({
579
+ id: c.id,
580
+ title: c.title,
581
+ ok: false,
582
+ said: (e as Error).message,
583
+ });
584
+ }
585
+ }
586
+ const ok = clients.every((c) => c.ok);
587
+ const failed = clients.find((c) => !c.ok);
588
+ printJson({
589
+ ok,
590
+ server: { name: s.name, command: s.command, args: s.args },
591
+ clients,
592
+ dryRun: flags.dryRun,
593
+ // Most agents load a new MCP server only when they start.
594
+ restart: sub === "connect" && !flags.dryRun && ok,
595
+ web: { url, up: probe.kind === "up" },
596
+ ...(failed
597
+ ? {
598
+ error: `${failed.title}: ${failed.said}`,
599
+ fix: `./bin/${cli} mcp config`,
600
+ ask: `Add Den's MCP server to ${failed.title} by hand, with the JSON \`mcp config\` prints?`,
601
+ }
602
+ : probe.kind !== "up" && sub === "connect"
603
+ ? { next: [`./bin/${cli} web up --no-open`] }
604
+ : {}),
605
+ });
606
+ return ok ? 0 : 1;
607
+ }