@monster-orange/den 0.4.14 → 0.4.15

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 (54) hide show
  1. package/CHANGELOG.md +127 -0
  2. package/README.md +7 -7
  3. package/docs/distributions.md +144 -8
  4. package/package.json +1 -1
  5. package/src/shared/actor-token.ts +12 -0
  6. package/src/shared/agents/conversations.ts +33 -4
  7. package/src/shared/agents/den-claude.ts +13 -3
  8. package/src/shared/agents/records.ts +18 -0
  9. package/src/shared/agents/runtime.ts +6 -1
  10. package/src/shared/claim-code.ts +163 -0
  11. package/src/shared/cli-command.ts +31 -0
  12. package/src/shared/den-config.ts +12 -0
  13. package/src/shared/environment-manager/workspaces.ts +20 -0
  14. package/src/shared/web-session.ts +64 -5
  15. package/src/ui/cli/agent-ops.ts +75 -22
  16. package/src/ui/cli/agents-command.ts +4 -1
  17. package/src/ui/cli/doctor-command.ts +29 -3
  18. package/src/ui/cli/doctor-onboard.ts +26 -0
  19. package/src/ui/cli/flag-parser.ts +9 -1
  20. package/src/ui/cli/index.ts +29 -3
  21. package/src/ui/cli/onboard-command.ts +180 -0
  22. package/src/ui/cli/onboard-reads.ts +613 -0
  23. package/src/ui/cli/repo-command.ts +156 -10
  24. package/src/ui/cli/web-command.ts +92 -1
  25. package/src/ui/mcp/api.ts +14 -2
  26. package/src/ui/mcp/server.ts +12 -2
  27. package/src/ui/mcp/tools.ts +368 -1
  28. package/src/ui/server/agent-routes.ts +2 -5
  29. package/src/ui/server/approval-routes.ts +390 -0
  30. package/src/ui/server/approvals.ts +416 -0
  31. package/src/ui/server/github-routes.ts +12 -4
  32. package/src/ui/server/held-actions.ts +203 -0
  33. package/src/ui/server/http-helpers.ts +35 -0
  34. package/src/ui/server/index.ts +286 -16
  35. package/src/ui/server/lifecycle-exec.ts +70 -0
  36. package/src/ui/server/onboard-routes.ts +132 -0
  37. package/src/ui/server/pair-routes.ts +7 -4
  38. package/src/ui/server/public/agents-page.js +91 -4
  39. package/src/ui/server/public/error-page.js +27 -3
  40. package/src/ui/server/public/header.js +1 -1
  41. package/src/ui/server/public/i18n/es.js +29 -0
  42. package/src/ui/server/public/i18n/ja.js +31 -0
  43. package/src/ui/server/public/index.html +1 -1
  44. package/src/ui/server/public/local-dev.html +5 -4
  45. package/src/ui/server/public/persona-ui.css +21 -0
  46. package/src/ui/server/public/release.html +0 -1
  47. package/src/ui/server/public/settings-registry.js +3 -1
  48. package/src/ui/server/public/settings.html +9 -1
  49. package/src/ui/server/public/shell.js +313 -0
  50. package/src/ui/server/request-guard.ts +2 -0
  51. package/src/ui/server/route-classes.ts +28 -1
  52. package/src/ui/server/session-routes.ts +11 -3
  53. package/src/ui/server/workspace-routes.ts +21 -10
  54. package/themes/den/den-theme.json +6 -6
@@ -28,6 +28,8 @@ import {
28
28
  import { readWebPort } from "../../shared/edge/config.js";
29
29
  import { DEFAULT_PORT } from "../../shared/constants.js";
30
30
  import { needsDocker } from "./repo-detect.js";
31
+ import { readMcpConfig } from "../../shared/den-config.js";
32
+ import { approvalsState } from "../mcp/tools.js";
31
33
  import type { WebProbe } from "./mcp-connect.js";
32
34
 
33
35
  export type CheckStatus = "ok" | "warn" | "fail";
@@ -59,6 +61,8 @@ export interface OnboardDeps {
59
61
  probeWeb: (url: string) => Promise<WebProbe>;
60
62
  /** The MCP one-shot against the web server: exit 0, or why not. */
61
63
  mcpCall: () => { ok: boolean; detail?: string };
64
+ /** `mcp.approvals` (throws on a bad value): whether the held writes are on. */
65
+ approvals: () => boolean;
62
66
  /** The command an agent runs, e.g. `./bin/acme`. */
63
67
  bin: string;
64
68
  }
@@ -193,9 +197,30 @@ export async function onboardingChecks(
193
197
  },
194
198
  );
195
199
  }
200
+ out.push(approvalsCheck(d));
196
201
  return out;
197
202
  }
198
203
 
204
+ /** `mcp:approvals`: which state the held writes are in. Never needs the server. */
205
+ function approvalsCheck(d: OnboardDeps): OnboardCheck {
206
+ let on: boolean;
207
+ try {
208
+ on = d.approvals();
209
+ } catch (err) {
210
+ return {
211
+ id: "mcp:approvals",
212
+ status: "fail",
213
+ detail: (err as Error).message,
214
+ ask: "Set mcp.approvals in the distribution config to true or false.",
215
+ };
216
+ }
217
+ return {
218
+ id: "mcp:approvals",
219
+ status: "ok",
220
+ detail: approvalsState({ approvals: on }),
221
+ };
222
+ }
223
+
199
224
  function onPath(bin: string): boolean {
200
225
  for (const dir of (process.env.PATH ?? "").split(path.delimiter)) {
201
226
  if (!dir) continue;
@@ -286,6 +311,7 @@ export function defaultOnboardDeps(
286
311
  `exit ${r.status ?? "timeout"}`,
287
312
  };
288
313
  },
314
+ approvals: () => readMcpConfig().approvals,
289
315
  bin: runnableCli(),
290
316
  };
291
317
  }
@@ -181,7 +181,15 @@ function validateFlagScopes(parsed: ParsedCliArgs): void {
181
181
  */
182
182
  export const SUBCOMMAND_OWN_HELP: Readonly<Record<string, readonly string[]>> =
183
183
  {
184
- web: ["token", "login", "sessions", "logout", "owner", "approve"],
184
+ web: [
185
+ "token",
186
+ "login",
187
+ "sessions",
188
+ "logout",
189
+ "owner",
190
+ "approve",
191
+ "claim-link",
192
+ ],
185
193
  mcp: ["connect", "disconnect", "clients", "config"],
186
194
  repo: ["add"],
187
195
  };
@@ -366,12 +366,22 @@ const COMMAND_HELP: Record<string, { usage: string; lines: string[] }> = {
366
366
  },
367
367
  repo: {
368
368
  usage:
369
- 'repo add <path|git-url> [--sidecar] [-w <workspace>] [--name <repo>] [--run "<cmd>"] [--health <url>] [--json]',
369
+ 'repo add <path|git-url> [--sidecar] [-w <workspace>] [--name <repo>] [--run "<cmd>"] [--health <url>] [--dry-run] [--json] | repo detect <folder> [--json]',
370
370
  lines: [
371
371
  "Add a repo with no prompts, never writing into it. A repo with no",
372
372
  "manifest (or --sidecar) gets a side-car manifest in manifests/, detected",
373
373
  "from its Makefile, compose file or package.json scripts. Safe to re-run.",
374
- "`{cli} repo add --help` says more.",
374
+ "--dry-run prints the side-car it would write, validated, and writes nothing.",
375
+ "detect: the git repos in a folder in your home, how each runs, and",
376
+ "suggested workspaces. `{cli} repo add --help` says more.",
377
+ ],
378
+ },
379
+ onboard: {
380
+ usage: "onboard status [--json]",
381
+ lines: [
382
+ "What's set up (a distribution, the web server, workspaces, repos, a",
383
+ "running service, Den's MCP) and the one next step, with its command.",
384
+ "Reads only; it never runs an agent's CLI.",
375
385
  ],
376
386
  },
377
387
  relocate: {
@@ -595,6 +605,7 @@ const TELEMETRY_COMMANDS = new Set<string>([
595
605
  "theme",
596
606
  "plugin",
597
607
  "repo",
608
+ "onboard",
598
609
  ]);
599
610
 
600
611
  /**
@@ -975,6 +986,15 @@ class DenCLI {
975
986
  break;
976
987
  }
977
988
 
989
+ case "onboard": {
990
+ const { cmdOnboard } = await import("./onboard-command.js");
991
+ const { probeWebServer } = await import("./mcp-connect.js");
992
+ process.exitCode = await cmdOnboard(args.slice(1), (u) =>
993
+ probeWebServer(u),
994
+ );
995
+ break;
996
+ }
997
+
978
998
  case "relocate":
979
999
  await this.cmdRelocate(args.slice(1));
980
1000
  break;
@@ -5271,7 +5291,13 @@ class DenCLI {
5271
5291
  " register [path] Register a repo in the service catalog",
5272
5292
  );
5273
5293
  console.log(
5274
- " repo add <path|url> Add a repo with no prompts, never changing it (--sidecar, --json)",
5294
+ " repo add <path|url> Add a repo with no prompts, never changing it (--sidecar, --dry-run, --json)",
5295
+ );
5296
+ console.log(
5297
+ " repo detect <folder> Find the repos in a folder and suggest workspaces (--json)",
5298
+ );
5299
+ console.log(
5300
+ " onboard status What's set up, and the next step (--json)",
5275
5301
  );
5276
5302
  console.log(
5277
5303
  " relocate <repo> <path> Point a moved checkout's workspace entries at its new path",
@@ -0,0 +1,180 @@
1
+ /**
2
+ * `<cli> onboard status [--json]` (ON-3): what's set up and the one next
3
+ * step, the CLI twin of the MCP tool `onboard_status` and of
4
+ * GET /api/onboard/status. `doctor --json` has the machine checks (Node,
5
+ * git, Docker, the web server, Den's MCP); this adds what onboarding needs
6
+ * on top (a distribution, workspaces, repos, a running service) and picks
7
+ * the next step. Reads only. It never runs an agent's CLI: Den's MCP is
8
+ * checked in the agents' config files (onboard-reads.ts mcpRegistration).
9
+ * Exit 0 whenever it answers, whatever is left to do.
10
+ */
11
+ import { execFile } from "child_process";
12
+ import * as fs from "fs";
13
+ import * as os from "os";
14
+ import * as path from "path";
15
+ import { product } from "../../shared/product.js";
16
+ import { getDistRoot } from "../../shared/paths.js";
17
+ import {
18
+ DEN_SERVICES_DIR,
19
+ readWorkspacesFile,
20
+ } from "../../shared/environment-manager/workspaces.js";
21
+ import { listDaemons } from "../../shared/environment-manager/service-runner.js";
22
+ import { defaultOnboardDeps, runnableCli } from "./doctor-onboard.js";
23
+ import type { WebProbe } from "./mcp-connect.js";
24
+ import {
25
+ mcpRegistration,
26
+ onboardStatus,
27
+ type OnboardStatus,
28
+ type OnboardStatusDeps,
29
+ type RegisteredRepo,
30
+ } from "./onboard-reads.js";
31
+ import { printJson } from "./json-out.js";
32
+
33
+ /** Every repo in a workspace, at the folder Den runs it from. */
34
+ export function registeredRepos(): RegisteredRepo[] {
35
+ let file;
36
+ try {
37
+ file = readWorkspacesFile();
38
+ } catch {
39
+ return [];
40
+ }
41
+ const out: RegisteredRepo[] = [];
42
+ for (const [workspace, ws] of Object.entries(file.workspaces))
43
+ for (const [addr, entry] of Object.entries(ws?.services ?? {})) {
44
+ const repo = addr.split("/")[0];
45
+ const dir =
46
+ entry.mode === "dev-managed"
47
+ ? entry.path
48
+ : path.join(DEN_SERVICES_DIR, repo, entry.branch ?? "main");
49
+ if (dir) out.push({ path: dir, workspace, repo });
50
+ }
51
+ return out;
52
+ }
53
+
54
+ function readText(file: string): string | null {
55
+ try {
56
+ return fs.readFileSync(file, "utf-8");
57
+ } catch {
58
+ return null;
59
+ }
60
+ }
61
+
62
+ /** Whether the Docker daemon answers, without blocking the event loop. */
63
+ function dockerUpAsync(): Promise<boolean> {
64
+ return new Promise((resolve) => {
65
+ execFile(
66
+ "docker",
67
+ ["info", "--format", "{{.ServerVersion}}"],
68
+ { timeout: 10_000 },
69
+ (err) => resolve(!err),
70
+ );
71
+ });
72
+ }
73
+
74
+ /** This machine's deps for onboardStatus. */
75
+ export async function defaultStatusDeps(o: {
76
+ probeWeb: (url: string) => Promise<WebProbe>;
77
+ webUrl?: string;
78
+ viaMcp?: boolean;
79
+ }): Promise<OnboardStatusDeps> {
80
+ const doctor = defaultOnboardDeps(o.probeWeb);
81
+ if (o.webUrl) doctor.webUrl = o.webUrl;
82
+ // Docker is asked only when a registered repo needs it, and asynchronously:
83
+ // the web server answers this route too.
84
+ const dockerRepos = doctor.dockerRepos();
85
+ const dockerUp =
86
+ dockerRepos.length && doctor.which("docker")
87
+ ? await dockerUpAsync()
88
+ : false;
89
+ const bin = runnableCli();
90
+ return {
91
+ bin,
92
+ distribution: fs.existsSync(path.join(getDistRoot(), "bin", product().cli)),
93
+ doctor: {
94
+ ...doctor,
95
+ dockerRepos: () => dockerRepos,
96
+ dockerUp: () => dockerUp,
97
+ },
98
+ workspaces: () => {
99
+ let file;
100
+ try {
101
+ file = readWorkspacesFile();
102
+ } catch {
103
+ return { names: [], active: [], repos: [] };
104
+ }
105
+ const repos = new Set<string>();
106
+ for (const ws of Object.values(file.workspaces))
107
+ for (const addr of Object.keys(ws?.services ?? {}))
108
+ repos.add(addr.split("/")[0]);
109
+ return {
110
+ names: Object.keys(file.workspaces),
111
+ active: file.active.filter((n) => n in file.workspaces),
112
+ repos: [...repos],
113
+ };
114
+ },
115
+ running: () => {
116
+ try {
117
+ return listDaemons().map((d) => d.address);
118
+ } catch {
119
+ return [];
120
+ }
121
+ },
122
+ mcp: () =>
123
+ mcpRegistration({
124
+ cli: bin,
125
+ home: os.homedir(),
126
+ platform: process.platform,
127
+ appData: process.env.APPDATA,
128
+ projectDir: getDistRoot(),
129
+ which: doctor.which,
130
+ read: readText,
131
+ viaMcp: o.viaMcp,
132
+ }),
133
+ };
134
+ }
135
+
136
+ const MARK: Record<string, string> = { ok: "✓", warn: "!", todo: "·" };
137
+
138
+ function printHuman(r: OnboardStatus): void {
139
+ for (const c of r.checks)
140
+ console.log(` ${MARK[c.status] ?? " "} ${c.id.padEnd(12)} ${c.detail}`);
141
+ console.log(`\n Next: ${r.next.step}`);
142
+ if (r.next.command) console.log(` Run: ${r.next.command}`);
143
+ }
144
+
145
+ export const ONBOARD_USAGE = () => `${product().cli} onboard status [--json]`;
146
+
147
+ /** `onboard <sub> …`; returns the exit code. */
148
+ export async function cmdOnboard(
149
+ argv: string[],
150
+ probeWeb: (url: string) => Promise<WebProbe>,
151
+ ): Promise<number> {
152
+ const [sub, ...rest] = argv;
153
+ const json = rest.includes("--json");
154
+ const unknown = rest.filter((a) => a !== "--json");
155
+ if (sub !== "status" || unknown.length) {
156
+ const error =
157
+ sub === undefined || sub === "--help" || sub === "-h"
158
+ ? null
159
+ : sub !== "status"
160
+ ? `unknown onboard subcommand "${sub}"`
161
+ : `unknown flag ${unknown[0]}`;
162
+ if (json && error)
163
+ printJson({
164
+ ok: false,
165
+ error,
166
+ fix: `${runnableCli()} onboard status --json`,
167
+ });
168
+ else {
169
+ if (error) console.error(`✗ ${error}`);
170
+ console.log(
171
+ `\n ${ONBOARD_USAGE()}\n\n What's set up (a distribution, the web server, workspaces, repos,\n a running service, Den's MCP) and the one next step, with its command.\n`,
172
+ );
173
+ }
174
+ return error ? 2 : sub === undefined ? 1 : 0;
175
+ }
176
+ const r = await onboardStatus(await defaultStatusDeps({ probeWeb }));
177
+ if (json) printJson(r);
178
+ else printHuman(r);
179
+ return 0;
180
+ }