@monster-orange/den 0.4.4 → 0.4.6

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 (57) hide show
  1. package/CHANGELOG.md +243 -0
  2. package/README.md +24 -9
  3. package/docs/distributions.md +320 -26
  4. package/docs/reference.md +8 -3
  5. package/package.json +4 -3
  6. package/skills/install-den/SKILL.md +114 -0
  7. package/src/shared/agent-setup.ts +9 -0
  8. package/src/shared/auth/sudo-vault.ts +115 -11
  9. package/src/shared/cli-command.ts +186 -0
  10. package/src/shared/confirm-code.ts +34 -0
  11. package/src/shared/constants.ts +7 -0
  12. package/src/shared/den-config.ts +7 -0
  13. package/src/shared/dev-metrics/emit.ts +59 -2
  14. package/src/shared/edge/config.ts +39 -0
  15. package/src/shared/edge/render.ts +92 -18
  16. package/src/shared/edge/runtime.ts +57 -1
  17. package/src/shared/edge/types.ts +11 -1
  18. package/src/shared/github-app.ts +94 -0
  19. package/src/shared/github-device.ts +286 -0
  20. package/src/shared/port-pick.ts +60 -0
  21. package/src/shared/secrets/onepassword.ts +27 -4
  22. package/src/shared/web-owner.ts +91 -0
  23. package/src/shared/web-session.ts +135 -1
  24. package/src/ui/cli/create-main.ts +1 -1
  25. package/src/ui/cli/create.ts +263 -30
  26. package/src/ui/cli/doctor-command.ts +67 -4
  27. package/src/ui/cli/doctor-onboard.ts +291 -0
  28. package/src/ui/cli/flag-parser.ts +5 -1
  29. package/src/ui/cli/index.ts +246 -4
  30. package/src/ui/cli/json-out.ts +49 -0
  31. package/src/ui/cli/mcp-connect.ts +179 -15
  32. package/src/ui/cli/repo-command.ts +490 -0
  33. package/src/ui/cli/repo-detect.ts +192 -0
  34. package/src/ui/cli/web-command.ts +513 -33
  35. package/src/ui/mcp/connect.ts +134 -0
  36. package/src/ui/server/device-flows.ts +274 -0
  37. package/src/ui/server/edge-exposure.ts +16 -4
  38. package/src/ui/server/github-routes.ts +321 -0
  39. package/src/ui/server/http-errors.ts +10 -0
  40. package/src/ui/server/index.ts +463 -27
  41. package/src/ui/server/login-waits.ts +203 -0
  42. package/src/ui/server/pair-requests.ts +237 -0
  43. package/src/ui/server/pair-routes.ts +196 -0
  44. package/src/ui/server/public/error-page.js +452 -40
  45. package/src/ui/server/public/error.html +59 -0
  46. package/src/ui/server/public/i18n/es.js +74 -8
  47. package/src/ui/server/public/i18n/ja.js +74 -8
  48. package/src/ui/server/public/local-dev.html +16 -4
  49. package/src/ui/server/public/qr.js +317 -0
  50. package/src/ui/server/public/settings-page.js +10 -3
  51. package/src/ui/server/public/settings-registry.js +392 -1
  52. package/src/ui/server/public/shell.js +123 -10
  53. package/src/ui/server/public/topbar.js +7 -3
  54. package/src/ui/server/public/workspace-picker.js +1 -1
  55. package/src/ui/server/request-guard.ts +103 -36
  56. package/src/ui/server/route-classes.ts +102 -2
  57. package/src/ui/server/ui-assets.ts +2 -0
@@ -12,6 +12,7 @@ Den's own checkout is itself a distribution, named `den`.
12
12
  - [The bin shim](#the-bin-shim)
13
13
  - [make: den.mk](#make-denmk)
14
14
  - [Two roots](#two-roots)
15
+ - [Dev metrics: local unless you opt in](#dev-metrics-local-unless-you-opt-in)
15
16
  - [Pinning and upgrading](#pinning-and-upgrading)
16
17
  - [Developing Den against a distribution](#developing-den-against-a-distribution)
17
18
 
@@ -31,8 +32,22 @@ From a Den checkout, `./bin/den create acme ../acme` does the same.
31
32
  | `--title <title>` | `<Name>` Title Case | Brand name shown to people |
32
33
  | `--cli <cli>` | `<name>` | The command people type |
33
34
  | `--env-prefix <PREFIX>` | `<NAME>` | Env-var prefix (`ACME` → `ACME_HOME`) |
35
+ | `--yes`, `-y` | | Never prompt (create never does) |
36
+ | `--json` | | One JSON object on stdout |
37
+ | `--port <n>` | `7777`, else free | The web UI's port |
34
38
 
35
39
  `create` refuses a directory that is not empty and never overwrites a file.
40
+ A directory that already holds this distribution (its `den.yml` names the
41
+ same product and command) is not an error: `create` says so and writes
42
+ nothing, so it is safe to re-run.
43
+
44
+ The web UI's port is 7777 unless that port is in use on this machine (another
45
+ Den already running, say). Then `create` picks the next free port above it,
46
+ never a port Den itself reserves (7778, 7791, 7799), writes it to the new
47
+ distribution's `config/local/user.yml` as `web: { port: N }`, and says so.
48
+ `--port <n>` sets it outright. The check only connects and binds; it sends
49
+ nothing to whatever holds the port. `DEN_WEB_PORT_START` moves the starting
50
+ port (tests use it so none touches 7777).
36
51
  [`examples/distribution/`](../examples/distribution) is exactly the output of
37
52
  `den create acme`; a unit test regenerates it, so the two never drift
38
53
  (`npm run example:regen` after changing the scaffolder).
@@ -47,6 +62,85 @@ npm install
47
62
  make help
48
63
  ```
49
64
 
65
+ ## Set up by your agent
66
+
67
+ A developer pastes one line into their agent:
68
+
69
+ ```text
70
+ Set up Den for me by following https://raw.githubusercontent.com/Monster-Orange/den/main/skills/install-den/SKILL.md
71
+ ```
72
+
73
+ That file is the install-den skill, also in the package at
74
+ `skills/install-den/SKILL.md`: plain Markdown any agent can follow, with
75
+ Claude skill front matter. `den --help` prints the line. The skill runs the
76
+ commands below. Each one takes `--json`, never prompts, and is safe to
77
+ re-run. With `--json`, stdout carries exactly one JSON object and the
78
+ command's log goes to stderr. When `ok` is false the object says why
79
+ (`error`) and carries either a command the agent can run (`fix`) or a
80
+ question for the person (`ask`, sometimes with `choices`).
81
+
82
+ | Step | Answers |
83
+ | ---------------------------------------------------------- | ---------------------------------------------------------------------------- |
84
+ | `npx @monster-orange/den create <name> [dir] --yes --json` | `{ ok, dir, cli, existed, files, port, portPicked?, next }` |
85
+ | `<cli> repo add <path\|git-url> --json` | `{ ok, existed, repo, workspace, mode, path, manifest, services, next }` |
86
+ | `<cli> up <repo> --json` | `{ ok, workspace, services: [{ address, result, health, detail? }], next }` |
87
+ | `<cli> web up --no-open --json` | `{ ok, url, port, version? }` |
88
+ | `<cli> doctor --json` | `{ ok, checks: [{ id, status, detail, url?, port?, fix?, ask? }] }` |
89
+ | `<cli> mcp connect --runtime <runtime> --json` | `{ ok, server, clients: [{ id, ok, said, file?, changed? }], restart, web }` |
90
+
91
+ ### repo add
92
+
93
+ `<cli> repo add <path|git-url> [--sidecar] [-w <workspace>] [--name <repo>]
94
+ [--run "<cmd>"] [--down "<cmd>"] [--health <url>] [--branch <branch>]
95
+ [--json]` adds a repo to a workspace without writing into the repo.
96
+
97
+ - A path is the person's own checkout, added dev-managed at that path. A git
98
+ URL is cloned where Den keeps its managed clones
99
+ (`.<name>-services/<repo>/<branch>`, the remote's default branch unless
100
+ `--branch` says otherwise) and added managed. A clone that is already
101
+ there is used as it is.
102
+ - A repo with its own manifest is used as it is. One without, or with
103
+ `--sidecar`, gets a side-car: a held manifest at
104
+ `manifests/<repo>.<name>.yml` ([below](#overriding-a-manifest)), keyed by
105
+ the repo's `origin` name or folder name. It is checked by the manifest
106
+ validator before it is written, and an existing one is never overwritten.
107
+ `--sidecar` on a repo with its own manifest is an error: Den always uses
108
+ the repo's own file (change it with an override instead).
109
+ - The side-car's commands are detected from the repo's files, in this order:
110
+ a Makefile with an `up` target (`make up`, plus `down`, `setup` and
111
+ `restart` when present); otherwise a compose file
112
+ (`docker compose -f <file> up -d` and `down`) or `package.json`'s `dev`,
113
+ else `start`, script. One candidate decides. Two, or none, is a question:
114
+ the answer has `ask` and `choices`, nothing is written, and a re-run names
115
+ the command with `--run "<cmd>"`. A Makefile `test` target or a
116
+ `package.json` `test` script becomes the repo's `tests.unit`.
117
+ `--health <url>` adds an HTTP health check.
118
+ - The workspace is `-w <workspace>` (made and activated if it doesn't exist),
119
+ else the focused one, else a new `default` workspace.
120
+ - The same repo again answers `ok` with `existed: true`. A different repo
121
+ under the same name is an error that offers `relocate` or `--name`.
122
+
123
+ ### up, doctor and mcp connect
124
+
125
+ - `up --json` runs `up` as usual, then reports each service it touched:
126
+ `result` is `started`, `already-running`, `failed` or `skipped`, and
127
+ `health` is the service's health after it started (a service with a
128
+ health check is waited on, up to 30 seconds). A healthy service is
129
+ `already-running` on a re-run.
130
+ - `doctor --json` lists the onboarding checks first: `node` (22.13 or
131
+ later), `git`, `docker` (only when a registered repo has a compose file or
132
+ block, or a docker command), `web` (the web server answers at `url`) and
133
+ `mcp` (`<cli> mcp --call workspaces` gets an answer through it). Then one
134
+ `strategy:<kind>` check per row of the [human doctor](#doctor). `ok` is
135
+ false, and the exit code 1, when any check is `fail`.
136
+ - `mcp connect --runtime <runtime>` names the agent by its runtime: `claude`
137
+ (the `claude-code` client), `codex`, `gemini` or `grok`. Grok CLI keeps its
138
+ MCP servers in `~/.grok/config.toml` (or `.grok/config.toml` with
139
+ `--project`), one `[mcp_servers.<name>]` table each
140
+ ([xAI's docs](https://docs.x.ai/build/features/mcp-servers)); Den writes
141
+ only its own table. A config file that already has the entry is left as
142
+ it is (`changed: false`).
143
+
50
144
  ## Layout
51
145
 
52
146
  | Path | What |
@@ -379,9 +473,23 @@ a ref that is not set is **not configured**, and its feature says so
379
473
  ("not configured: set secrets.provider and secrets.refs.sudo in
380
474
  config/acme.yml") instead of running a CLI. Typing the password always
381
475
  works; only the saved password needs `refs.sudo`.
476
+
477
+ **Saving writes `refs.sudo` for you** (LK-4, 0.4.5). When `secrets.provider`
478
+ is `1password` or `doppler`, its CLI is available, and no `refs.sudo` is
479
+ set, the admin password prompt (shown when a command is about to run sudo)
480
+ still offers "Save to 1Password". Saving creates the item
481
+ (1Password: a Password item "<Product> admin password", field `password`,
482
+ in your default vault; Doppler: the key `<PREFIX>_LOCAL_ADMIN_PWD`) and
483
+ then writes that ref to `config/local/user.yml`, because the password is
484
+ the person's, not the distribution's. From then on it survives restarts.
485
+ A failed save writes nothing, and its error never carries the password.
486
+ `env` keeps nothing past the process, so it offers no save. A `refs.sudo`
487
+ in the distribution's config always wins over the one in user.yml.
488
+
382
489
  `/api/sudo-password/vault-status` reports `backend`, `label`,
383
- `configured`, `backendAvailable`, `reason` and `hasStored` (`opAvailable`
384
- is a deprecated alias of `backendAvailable`, kept for one minor).
490
+ `configured`, `canSave` (saving works with no YAML to edit),
491
+ `backendAvailable`, `reason` and `hasStored` (`opAvailable` is a
492
+ deprecated alias of `backendAvailable`, kept for one minor).
385
493
 
386
494
  The admin password is for sudo, and nothing else (LK-0). It is no sign-in
387
495
  and no access check: starting, stopping and resuming agents, sessions and
@@ -572,6 +680,10 @@ Acme doctor: strategies
572
680
  `session-backend` is never a doctor failure: nothing needs a session until
573
681
  one is started (see Sessions).
574
682
 
683
+ `<cli> doctor --json` adds the onboarding checks (Node, git, Docker, the web
684
+ server, Den's MCP) and answers with one JSON object: see
685
+ [Set up by your agent](#up-doctor-and-mcp-connect).
686
+
575
687
  ## den.yml: the product profile
576
688
 
577
689
  Every runtime name derives from `product.name`. Set only what you want to
@@ -1209,11 +1321,13 @@ are the same directory.
1209
1321
 
1210
1322
  On localhost nothing asks you to sign in: every page, the agents and the
1211
1323
  terminal work as they are, and the admin password (R8) is the only prompt,
1212
- asked only when a command Den runs needs sudo. The edge counts as local too (0.4.3): opening
1213
- `https://acme.acme.localhost` from this machine asks for nothing.
1214
- Signing in is for when the server is reached from another machine: the
1215
- edge for a client the edge says is elsewhere, a LAN address, or a bind
1216
- that isn't loopback (`DEN_BIND_HOST`).
1324
+ asked only when a command Den runs needs sudo. Through the edge, a browser
1325
+ the edge shows is on this machine asks for nothing either. Signing in is
1326
+ for everything else: the edge for a client it can't place here (0.4.5), a
1327
+ LAN address, or a bind that isn't loopback (`DEN_BIND_HOST`). From another
1328
+ device, sign in with GitHub as the Den's owner, approve the computer with
1329
+ the code its page shows, or open a one-time link (below, and
1330
+ [Signing in with GitHub](#signing-in-with-github)).
1217
1331
 
1218
1332
  There is no password. Reading `web.secret` (32 random bytes, mode 0600 in
1219
1333
  the state dir, made at the first `web up`) proves you are this computer's
@@ -1221,7 +1335,10 @@ user, and the CLI does that for you:
1221
1335
 
1222
1336
  ```bash
1223
1337
  ./bin/acme web open # start if needed, and sign this browser in (a new tab)
1224
- ./bin/acme web login # print a one-time sign-in link, for another browser
1338
+ ./bin/acme web login # a one-time sign-in link and its code; waits until it's used
1339
+ ./bin/acme web login --no-wait # the link alone (asks for no code), and return
1340
+ ./bin/acme web approve ABC-123 # approve the computer showing this code
1341
+ ./bin/acme web approve --list # the computers waiting with a code
1225
1342
  ./bin/acme web sessions # the signed-in browsers: id, created, last seen, host, user agent
1226
1343
  ./bin/acme web logout <id> # sign out one browser (an id prefix from `web sessions`)
1227
1344
  ./bin/acme web logout # sign out every browser
@@ -1238,7 +1355,19 @@ user, and the CLI does that for you:
1238
1355
  among them; its URL comes from the edge config, and a name the edge
1239
1356
  doesn't route here is refused, saying what to add), and
1240
1357
  `web login --origin http://<lan name>:<port>` for any other allowed name
1241
- (the signed-out page there shows the exact command).
1358
+ (the signed-out page there shows the exact command, under "Other ways").
1359
+ - **`web login` waits** (0.4.5). Besides the link it prints an
1360
+ 8-character code ("It asks for this code: ABCD-EFGH") and blocks until
1361
+ the link is used, then says who: "Signed in: Safari on macOS
1362
+ (100.64.0.7) at 10:42". Nobody within the minute: "Expired: nobody used
1363
+ the link", exit 1. The link's page asks for the code before it signs in,
1364
+ so the link alone is useless; once opened it waits 5 minutes for the
1365
+ code. A wrong code is refused; five burn the link ("Refused: too many
1366
+ wrong codes", exit 1), more than 20 wrong a minute (any link) are slowed,
1367
+ and the link still works once. The CLI asks the running server through
1368
+ its bearer and never logs the code. `--no-wait` prints the link alone and
1369
+ returns, as before (no code); with no server running `web login` does
1370
+ that too, and says so.
1242
1371
  - **The cookie** (`den_session_<port>`) is HttpOnly, SameSite=Strict and
1243
1372
  Path=/, and Secure when the request came over HTTPS through the edge. The
1244
1373
  session lives server-side (`web-sessions.json`, 0600, in the state dir)
@@ -1256,36 +1385,111 @@ user, and the CLI does that for you:
1256
1385
  The Host allowlist, the Origin check and the CSRF token are what stop
1257
1386
  another site or a rebinding DNS name. From another machine, every page
1258
1387
  and call needs a signed-in browser (or a bearer). A page there shows
1259
- "Sign in to use <name>" with the command to run; it never asks for a
1388
+ "Sign in to <name>" and the ways to sign in (below); it never asks for a
1260
1389
  secret. `/api` answers 401 `signin_required`.
1261
- - **The edge counts as local.** A request for the edge hostname from the
1262
- edge (an edge peer, below) is treated as loopback: no sign-in on any
1263
- route, the agents and the terminal included. Host, Origin, CSRF and the
1264
- agent-token rules still apply. Who the client is comes from the left-most
1265
- `X-Forwarded-For` entry the edge sends:
1390
+ - **The signed-out page says where to act.** It names the machine Den runs
1391
+ on ("You're opening Den from another computer. Den runs on
1392
+ **studio**, so it needs to know this is you."), then:
1393
+ - **Sign in with GitHub**, only when a client id is set (below);
1394
+ - **Approve this computer**: a code (`XXX-XXX`) and the command that
1395
+ approves it, to run on that machine (`<cli> web approve <code>`), or
1396
+ Settings › Access › Waiting devices there. The page signs itself in
1397
+ when it's approved and goes to the page you asked for; it counts down
1398
+ the 10 minutes, and an expired code is a click away from a new one.
1399
+ From a computer with SSH access to it, the page gives the one-liner
1400
+ (`ssh <host> '<cli> web approve <code>'`) and, when the host is a name,
1401
+ an Open Terminal (`ssh://`) link;
1402
+ - **Other ways**, collapsed: the one-time link with
1403
+ `web login --origin`.
1404
+
1405
+ Every command the page prints names the machine it runs on and has a
1406
+ copy button. It is `<cli>` when the first `<cli>` on that machine's PATH
1407
+ is this distribution's, else the absolute path of its bin.
1408
+
1409
+ - **The machine's name** on the page is `web.machine_name` from
1410
+ `config/local/user.yml` (Settings › Access › Machine name), else the short
1411
+ host name (up to the first dot). It is never taken from the browser's
1412
+ Host. The ssh one-liner's target is the Host the browser used, without
1413
+ the port (the machine's host name if it had none).
1414
+ - **Codes.** About 30 bits (Crockford base32: no I, L, O or U; typed in any
1415
+ case, the dash optional); the page polls with a separate 128-bit id. A
1416
+ code expires after 10 minutes and works once; at most five wait at once
1417
+ (a sixth drops the oldest). They live in the server's memory only.
1418
+ Approving is rate-limited (10 a minute) and recorded in the activity log
1419
+ with who approved, never the code. The new session is "approved with a
1420
+ code". **Deny** (Settings › Access, or `web deny` through the API) tells
1421
+ the page it was refused.
1422
+ - **Seeing that you're signed in.** The chip at the foot of the rail says
1423
+ "Local" (this machine), "Signed in · this device" (a code or a one-time
1424
+ link) or "@<login>" (GitHub), and opens Settings › Access. The first page
1425
+ after a sign-in says "This device is signed in to Den on <machine>",
1426
+ once. Linked devices lists every session (GitHub, a code, a one-time
1427
+ link): how it signed in, when, from where (the address and the browser
1428
+ and OS), last seen, and **Revoke**; the one you're using says "This
1429
+ device".
1430
+ - **The edge counts as local only when it shows the client is.** A
1431
+ request for the edge hostname from the edge (an edge peer, below) is
1432
+ treated as loopback (no sign-in on any route, the agents and the
1433
+ terminal included) when the client the edge reports is this machine.
1434
+ Host, Origin, CSRF and the agent-token rules still apply. Who the client
1435
+ is comes from the right-most `X-Forwarded-For` entry, the one the edge
1436
+ itself appended (anything left of it came from the client, and is never
1437
+ believed):
1266
1438
  - loopback, or one of this machine's own interface addresses (a LAN
1267
- address, or a VPN's such as a Tailscale `100.x`): local;
1439
+ address, or a VPN's such as a Tailscale `100.x`): local, no sign-in;
1268
1440
  - missing, or a container/VM network's address (the Docker ranges, or a
1269
1441
  subnet of one of this machine's Docker/OrbStack/VM bridges): Den can't
1270
1442
  tell (Docker Desktop and OrbStack on macOS hand the edge their
1271
1443
  gateway's address, and a TCP passthrough in front of the edge hides the
1272
- client), so local;
1273
- - any other address: another machine, which needs a sign-in.
1274
-
1444
+ client), so it signs in: `web.edge_unattested`, below;
1445
+ - any other address: another machine, which signs in.
1446
+
1447
+ `web.edge_unattested: local` in `config/local/user.yml` counts a client
1448
+ Den can't place as local again (0.4.4's behaviour). It is read only from
1449
+ that file (a choice for that machine); the distribution config's key is
1450
+ ignored with a warning. The default is `signin`. A request straight to
1451
+ the server's port on localhost (loopback Host and peer) never signs in.
1275
1452
  With `DEN_BIND_HOST` beyond loopback (a deliberate exposure, other than
1276
- the Linux edge's Docker bridge address) the edge needs a sign-in as
1277
- before. With `forwardedHeaders.insecure` (the edge's default) Traefik
1278
- keeps an `X-Forwarded-For` the client sent, so this is a convenience,
1279
- not a boundary: whoever can reach the edge can use the terminal.
1453
+ the Linux edge's Docker bridge address) the edge always signs in.
1454
+
1455
+ - **The edge believes forwarded headers only from its own networks.** Den
1456
+ renders each entrypoint with `forwardedHeaders.trustedIPs` set to
1457
+ loopback and the Docker ranges (`127.0.0.1/32`, `::1/128`,
1458
+ `172.16.0.0/12`, `192.168.65.0/24`), never `forwardedHeaders.insecure`,
1459
+ so a client can't hand the edge an `X-Forwarded-For` of its own. An
1460
+ `edge.args` entry that sets `forwardedHeaders` or `proxyProtocol` is
1461
+ dropped, and `edge up` says so.
1462
+ - **A TCP passthrough in front of the edge** (a gateway that publishes
1463
+ `:443` and passes TLS through) hides the client: the edge sees the
1464
+ gateway's address. Have the gateway send the PROXY protocol, and list it
1465
+ in `edge.proxy_protocol.trusted_ips` (empty by default; IP addresses or
1466
+ CIDRs; `config/local/user.yml`'s list replaces the team's):
1467
+
1468
+ ```yaml
1469
+ edge:
1470
+ proxy_protocol:
1471
+ trusted_ips: [172.18.0.5]
1472
+ ```
1473
+
1474
+ Every entrypoint then takes the PROXY protocol from those addresses, and
1475
+ the client it carries is what Den sees. Configuring the gateway is the
1476
+ gateway's job.
1477
+
1478
+ - **`edge up` recreates the edge when its command changed.** It compares
1479
+ the running edge's command with the one it renders and says why ("acme-edge's
1480
+ settings changed (forwarded-header trust): recreating it once. Routes
1481
+ drop only while it restarts."). Upgrading to 0.4.5 does this once.
1280
1482
 
1281
1483
  - **"Your edge is reachable from other machines".** When Den can tell, at
1282
1484
  start, that the edge is reachable beyond this machine (an edge hostname
1283
1485
  that resolves to an address that isn't loopback, such as a LAN or
1284
1486
  Tailscale address, or the edge container publishing a port on every
1285
1487
  interface, from `docker inspect`), it writes "Your edge is reachable from
1286
- other machines; anyone who can reach it can use terminals. Auth is
1287
- coming." once to `web.log`, and every page shows it as a banner (dismissed
1288
- per browser).
1488
+ other machines. A browser it can't place as this one signs in first
1489
+ (Sign in with GitHub, or a pairing link)." once to `web.log`, and every
1490
+ page shows it as a banner (dismissed per browser). With
1491
+ `web.edge_unattested: local` it says instead that anyone it can't place
1492
+ can use terminals, and to remove the key to have them sign in.
1289
1493
  - **localhost is the canonical name.** A cookie set on `localhost` isn't
1290
1494
  sent to `127.0.0.1`, so the links Den prints use `localhost`.
1291
1495
  - **Browsers.** Chromium, Firefox and Safari 16.4 or later: a sign-in link
@@ -1319,6 +1523,76 @@ logout --all` the old token is refused; the CLI reads the new one.
1319
1523
  421 `misdirected`, so a LAN client can't send the edge's name straight to
1320
1524
  the server's port.
1321
1525
 
1526
+ ## Signing in with GitHub
1527
+
1528
+ A Den has one owner: a GitHub account. Once it is linked, that account
1529
+ signs in from any device (a phone, another laptop, a browser through the
1530
+ edge) with **Sign in with GitHub** on the signed-out page. Den stores only
1531
+ the account's id and login, never a token, and no other account gets in.
1532
+
1533
+ ```bash
1534
+ ./bin/acme web owner link # link the owner: GitHub's device flow, in the terminal
1535
+ ./bin/acme web owner # show who it is (also: web owner show, --json)
1536
+ ./bin/acme web owner unlink # forget the owner (signed-in browsers stay signed in)
1537
+ ```
1538
+
1539
+ - **`web owner link`** prints `https://github.com/login/device` and a code.
1540
+ Enter the code there and approve; Den asks GitHub who approved, stores
1541
+ `owner: github:<id>` and the login in `web-owner.json` (0600, in the
1542
+ state dir) and throws the token away. A second link is refused until
1543
+ `web owner unlink`. Settings › Access does the same in the browser
1544
+ (Link your GitHub account, Unlink): shell-grade actions, for a signed-in
1545
+ browser and never an agent's token.
1546
+ - **Sign in with GitHub** on the signed-out page shows a code and
1547
+ `github.com/login/device`. The server runs the flow, and when the owner
1548
+ approves it sets the same never-expiring session cookie a one-time link
1549
+ sets, and the page reloads signed in. Another account sees "@<login>
1550
+ isn't allowed on this Den". The token is thrown away either way. At most
1551
+ three sign-ins wait for GitHub at once; a code expires as GitHub says
1552
+ (15 minutes). With no owner linked the page says to run
1553
+ `web owner link`.
1554
+ - **Settings › Access** lists the linked devices (the browser, how it
1555
+ signed in, the client address when the edge vouched for one, created and
1556
+ last seen; "This device" on yours), each with **Revoke**, which ends its session at once (its
1557
+ open terminals included). **Pair a device** makes a one-time link and a
1558
+ QR code for an edge hostname you pick (`web login --host <name>` in the
1559
+ browser): scan it on the other device within 60 seconds.
1560
+ - **`web.github_client_id`** is the GitHub App's client id (public; no
1561
+ secret). The default is Den's own App, **Monster Orange Den**
1562
+ (`Iv23liKL12gnN6WvRV5b`), so Sign in with GitHub works out of the box. A
1563
+ distribution that ships its own App sets its client id here (below);
1564
+ `config/local/user.yml`'s wins over the team's. An empty value
1565
+ (`github_client_id: ""`) turns Sign in with GitHub off: the page then
1566
+ offers the code and the one-time link only.
1567
+ - **Expiring tokens.** If the App hands out expiring tokens (GitHub's
1568
+ answer carries `expires_in` or a `refresh_token`), sign-in still
1569
+ completes, and Den says once, in the log and on stderr, to switch off
1570
+ token expiry in the App's settings ("Expire user authorization tokens");
1571
+ it never prints the token. `GET /api/web/owner` reports it as
1572
+ `tokenExpiry: true`.
1573
+
1574
+ ### The GitHub App (once, by whoever ships the Den)
1575
+
1576
+ Den's default App is Monster Orange Den. A distribution that wants its own
1577
+ (its name on GitHub's approval page) needs a GitHub App with the device
1578
+ flow on. Create it once (organization or account › Settings › Developer settings › GitHub
1579
+ Apps › New GitHub App):
1580
+
1581
+ 1. No callback URL; untick "Request user authorization during
1582
+ installation". Webhook off.
1583
+ 2. **Enable Device Flow**: ticked. The device flow needs only the client
1584
+ id.
1585
+ 3. **Expire user authorization tokens**: off. Den never asks for
1586
+ `offline_access`, and keeps no token either way.
1587
+ 4. Account permissions: none. Den reads only `GET /user` (the id and the
1588
+ login), which needs none. Repository permissions are only for features
1589
+ that act on a repository (Den's default App has Issues for feedback).
1590
+ 5. **Where can this GitHub App be installed**: Any account. GitHub lets only
1591
+ members of the owning organization authorize a private App, so anyone
1592
+ else couldn't sign in.
1593
+ 6. Put the **Client ID** in `web.github_client_id`. Never generate or
1594
+ share a client secret or a private key for Den: it never uses one.
1595
+
1322
1596
  ## Running the web UI as a service (macOS)
1323
1597
 
1324
1598
  Den ships a launchd installer that runs the web UI at login and restarts it if
@@ -2129,6 +2403,26 @@ with both workspaces named. Any key of `agents:` other than `max_concurrent`
2129
2403
  and `persona_packs` is an agent id, and its only key is `grants`. A grant
2130
2404
  naming a workspace that doesn't exist stops `start`, naming the key.
2131
2405
 
2406
+ ## Dev metrics: local unless you opt in
2407
+
2408
+ Den records each command it runs (its name, surface, duration and exit
2409
+ code, never arguments or values) as one JSON line in
2410
+ `.dev-metrics/events.jsonl` under the distribution root. That file stays
2411
+ on the machine.
2412
+
2413
+ Nothing is sent anywhere unless the engineer opts in, per machine:
2414
+
2415
+ ```yaml
2416
+ # config/local/user.yml (gitignored)
2417
+ metrics:
2418
+ share: true
2419
+ ```
2420
+
2421
+ Only the boolean `true` counts. With it, and a Sentry DSN in
2422
+ `DEV_METRICS_SENTRY_DSN` (or a synced `dev-metrics.config`), each line is
2423
+ also sent, best effort. A DSN on its own never sends. `CI=true` and
2424
+ `DEV_METRICS_DISABLED=1` stop sending even when opted in.
2425
+
2132
2426
  ## Pinning and upgrading
2133
2427
 
2134
2428
  `package.json` pins an exact Den version (`create` writes the version that
package/docs/reference.md CHANGED
@@ -110,9 +110,14 @@ That is `examples/demo/demo-api/.den.yml`; the other demo repos are in
110
110
  workspace's checkout, with an MCP server pinned to that workspace and a
111
111
  token of its own. The web UI's Agents tab lists them and opens their
112
112
  terminals.
113
- - **Browser sign-in beyond loopback.** On localhost no route needs a sign-in.
114
- Through the edge or on a non-loopback bind, every route needs one:
115
- `<cli> web login` prints a one-time link (`web open` opens it).
113
+ - **Browser sign-in beyond loopback.** On localhost no route needs a sign-in,
114
+ nor through the edge when it shows the browser is on this machine.
115
+ Anywhere else, every route needs one: Sign in with GitHub as the Den's
116
+ owner (`<cli> web owner link` links the account once), approving the
117
+ computer with the code its page shows (`<cli> web approve <code>`, or
118
+ Settings › Access › Waiting devices), or a one-time link (`<cli> web
119
+ login`, which waits until it's used; `web open` opens it; Settings ›
120
+ Access pairs a device with a QR code). The rail's chip shows who you are.
116
121
  - **Web UI and CLI** over the same state.
117
122
 
118
123
  ## Strategies
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@monster-orange/den",
3
- "version": "0.4.4",
3
+ "version": "0.4.6",
4
4
  "description": "Den: a local multi-repo dev orchestrator, HTTPS edge and E2E harness",
5
5
  "type": "module",
6
6
  "bin": {
@@ -25,7 +25,8 @@
25
25
  "README.md",
26
26
  "CHANGELOG.md",
27
27
  "docs/",
28
- "!docs/images/"
28
+ "!docs/images/",
29
+ "skills/"
29
30
  ],
30
31
  "scripts": {
31
32
  "cli": "tsx src/ui/cli/index.ts menu",
@@ -44,7 +45,7 @@
44
45
  "gate:org-names": "node scripts/org-name-gate.mjs",
45
46
  "docs:shots": "node scripts/doc-shots.mjs",
46
47
  "docs:links": "node scripts/docs-links.js",
47
- "example:regen": "rm -rf examples/distribution && ./bin/den create acme examples/distribution",
48
+ "example:regen": "rm -rf examples/distribution && ./bin/den create acme examples/distribution --port 7777",
48
49
  "dist-smoke": "bash scripts/dist-smoke.sh"
49
50
  },
50
51
  "keywords": [
@@ -0,0 +1,114 @@
1
+ ---
2
+ name: install-den
3
+ description: Install and set up Den, Monster Orange's local dev orchestrator, for a developer in five minutes. Use when the person says "set up Den", "install Den", "get me started with Den", or pastes a link to this file.
4
+ ---
5
+
6
+ # Install Den
7
+
8
+ You are setting up Den on this person's machine. Den runs their repos locally, shows them in a browser, and gives you
9
+ (the agent) an MCP server to work with. Aim to be done in five minutes. Keep your messages short and plain.
10
+
11
+ ## Hard rules
12
+
13
+ - **Never ask for a token, password or secret in chat.** If a step needs one, tell the person which command to run
14
+ themselves.
15
+ - **Never edit the person's repos.** Den's files go in the Den folder only. Read the repo; don't write to it.
16
+ - **Send nothing off this machine.** No uploads, no telemetry, no pasting their code anywhere. The only downloads are
17
+ Den from npm and a repo the person gave you as a git URL.
18
+ - **Sudo is the person's job.** Never run `sudo` yourself. When a Den step needs it, Den asks for the password in its
19
+ own prompt (in the terminal or the browser), and the person types it there.
20
+ - Every step below is safe to re-run. If you are unsure whether a step ran, run it again.
21
+
22
+ ## 1. Check the prerequisites
23
+
24
+ Run these and read the results. Don't install anything without saying so first.
25
+
26
+ 1. `node --version`: needs **22.13 or later**. If it's older or missing, say so and suggest their usual installer
27
+ (for example `brew install node`, or `nvm install 22`).
28
+ 2. `npm --version`: needs 9 or later. It comes with Node.
29
+ 3. `git --version`: needs to be there.
30
+ 4. `docker info`: only matters if the repo runs in containers (it has a `docker-compose.yml` or `compose.yaml`).
31
+ If Docker is needed and not running, ask them to start it.
32
+
33
+ If anything is missing, stop here, tell them what and how to fix it, and wait.
34
+
35
+ ## 2. Ask at most three questions
36
+
37
+ Ask them together, each with a default, so the person can just say "yes":
38
+
39
+ 1. **Where should your Den live?** Default: `~/den`. It must be a new or empty folder.
40
+ 2. **Which repo should we start with?** Default: the folder you're in now. A local path or a git URL.
41
+ 3. **What should we call your Den?** Default: `den`. Lowercase letters, numbers and dashes. This also becomes the
42
+ command name, so `acme` gives you `./bin/acme`.
43
+
44
+ Below, `<dir>` is the Den folder, `<repo>` the repo path, and `<name>` the name.
45
+
46
+ ## 3. Create the Den
47
+
48
+ Every Den command below takes `--json`. It then prints one JSON object on stdout and its log on stderr. Read `ok`
49
+ first. When `ok` is false, the object says why (`error`) and either a command to run (`fix`) or a question for the
50
+ person (`ask`, sometimes with `choices`).
51
+
52
+ 1. `npx @monster-orange/den create <name> <dir> --yes --json`. It answers `{ok, dir, cli, existed, port, next}`. If
53
+ the folder already holds this Den, it answers `ok` with `existed: true` and changes nothing. `port` is the browser
54
+ UI's port. When another Den already uses the usual one on this machine, Den picks the next free port and says so
55
+ (`portPicked`). Remember the port for the hand-back.
56
+ 2. `cd <dir> && npm install`
57
+ 3. From here on, the Den command is `./bin/<name>`, run from `<dir>`.
58
+
59
+ ## 4. Add the repo without changing it
60
+
61
+ 1. `./bin/<name> repo add <repo> --json`. `<repo>` can be a path or a git URL. Den clones a URL into its own folder.
62
+ - If the repo has its own `.<name>.yml`, Den uses it.
63
+ - If it doesn't, Den works out how the repo starts and writes a side-car manifest into `<dir>/manifests/`. It never
64
+ writes into the repo. It looks for a Makefile `up` target, then a compose file, then the `dev` or `start` script
65
+ in `package.json`. `--sidecar` asks for this explicitly, and fails if the repo has its own manifest.
66
+ - If you can see which port the app listens on, add `--health http://localhost:<port>/`. Den then checks that it
67
+ answers.
68
+ 2. If the answer has `ask` and `choices`, Den couldn't tell how the repo starts, and it wrote nothing. Ask the person
69
+ **one** question: the `ask` text, with the choices numbered. Then re-run with their answer:
70
+ `./bin/<name> repo add <repo> --run "<their command>" --json`.
71
+ 3. The answer names the repo (`repo`), the workspace it joined (`workspace`) and where its manifest is (`manifest`).
72
+ A workspace is Den's name for a set of repos that run together. The first one is called `default`.
73
+
74
+ ## 5. Start it and check it
75
+
76
+ 1. `./bin/<name> up <repo> --json`. `<repo>` is the `repo` value from step 4. Each service comes back with a
77
+ `result` (`started` or `already-running`) and a `health` (`ok`, or `unknown` when it has no health check).
78
+ 2. `./bin/<name> web up --no-open --json`. This starts the browser UI, and answers `{ok, url, port}`. The MCP server
79
+ reads through it.
80
+ 3. `./bin/<name> doctor --json`. Each check comes back as `ok`, `warn` or `fail`. The checks are Node, git, Docker
81
+ (only if a repo needs it), the web server (with its `url` and `port`) and Den's MCP. Fix every `fail` before you hand back.
82
+
83
+ ## 6. Connect yourself to Den's MCP
84
+
85
+ 1. `./bin/<name> mcp connect --runtime <you> --json`. `<you>` is `claude`, `codex`, `gemini` or `grok`. For any other
86
+ agent, `./bin/<name> mcp config` prints the JSON to add by hand.
87
+ 2. `./bin/<name> mcp clients` shows whether you are connected.
88
+ 3. If the answer has `restart: true`, you load the new MCP server only when you start again. Tell the person to
89
+ restart you once.
90
+
91
+ ## 7. Hand back
92
+
93
+ Tell the person, in a few short sentences. Explain every name you use the first time (for example, "a workspace is a
94
+ set of repos that run together").
95
+
96
+ - **Open:** the `url` from `web up` or from the doctor's `web` check, for example `http://localhost:<port>`. Never
97
+ assume a port: use the one Den gave you.
98
+ - **What's running:** each service from `up`, and whether it's healthy.
99
+ - **Your Den:** where it lives (`<dir>`) and its command (`./bin/<name>`).
100
+ - **Decisions, if any:** as numbered yes/no questions, for example "1. Restart me now so I can use Den's tools?"
101
+ - **Three next things we can do together:**
102
+ 1. Add another repo.
103
+ 2. Look at a service's logs, or run its tests.
104
+ 3. Start an agent session on this repo from the browser.
105
+
106
+ ## When something fails
107
+
108
+ 1. Read the JSON. If it has `fix`, run that command, then re-run the step that failed.
109
+ 2. If it has `ask`, the person has to decide or act (sudo, a password, starting Docker, signing in). Ask them the
110
+ question, show any `choices` numbered, and wait.
111
+ 3. If there is neither, ask the person **one** clear question, with your best guess as the default.
112
+ 4. Never work around a failure by editing their repo, turning off a check, or skipping a step without saying so.
113
+ 5. If you're stuck after two tries, stop. Tell them what you ran, what it said, and where Den writes its logs
114
+ (`~/.<name>/`), so they can get help.
@@ -0,0 +1,9 @@
1
+ /**
2
+ * The one line a developer pastes into their agent to have it set Den up
3
+ * (ON-5): the agent follows the install-den skill published at this URL
4
+ * (skills/install-den/SKILL.md in the package).
5
+ */
6
+ export const AGENT_SETUP_URL =
7
+ "https://raw.githubusercontent.com/Monster-Orange/den/main/skills/install-den/SKILL.md";
8
+
9
+ export const AGENT_SETUP_LINE = `Set up Den for me by following ${AGENT_SETUP_URL}`;