@dadado/agent-kit-cli 4.8.9 → 5.1.0

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.
@@ -0,0 +1,213 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="utf-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
6
+ <meta name="robots" content="noindex,nofollow" />
7
+ <title>Mission Control share</title>
8
+ <style>
9
+ :root {
10
+ color-scheme: dark light;
11
+ --bg: #0f1419;
12
+ --fg: #e7ecf1;
13
+ --muted: #8b9aab;
14
+ --accent: #3b82f6;
15
+ --err: #f87171;
16
+ --card: #1a222c;
17
+ }
18
+ @media (prefers-color-scheme: light) {
19
+ :root {
20
+ --bg: #f4f6f8;
21
+ --fg: #12202e;
22
+ --muted: #5b6b7c;
23
+ --card: #ffffff;
24
+ }
25
+ }
26
+ * {
27
+ box-sizing: border-box;
28
+ }
29
+ body {
30
+ margin: 0;
31
+ min-height: 100vh;
32
+ font-family: ui-sans-serif, system-ui, -apple-system, Segoe UI, sans-serif;
33
+ background: var(--bg);
34
+ color: var(--fg);
35
+ display: grid;
36
+ place-items: center;
37
+ padding: 1.5rem;
38
+ }
39
+ main {
40
+ width: min(28rem, 100%);
41
+ background: var(--card);
42
+ border-radius: 12px;
43
+ padding: 1.5rem 1.35rem;
44
+ box-shadow: 0 12px 40px rgb(0 0 0 / 18%);
45
+ }
46
+ h1 {
47
+ font-size: 1.15rem;
48
+ margin: 0 0 0.5rem;
49
+ font-weight: 650;
50
+ }
51
+ p {
52
+ margin: 0 0 1rem;
53
+ color: var(--muted);
54
+ font-size: 0.95rem;
55
+ line-height: 1.45;
56
+ }
57
+ .err {
58
+ color: var(--err);
59
+ }
60
+ a.btn,
61
+ button.btn {
62
+ display: inline-flex;
63
+ align-items: center;
64
+ justify-content: center;
65
+ width: 100%;
66
+ border: 0;
67
+ border-radius: 8px;
68
+ padding: 0.75rem 1rem;
69
+ font: inherit;
70
+ font-weight: 600;
71
+ text-decoration: none;
72
+ cursor: pointer;
73
+ background: var(--accent);
74
+ color: #fff;
75
+ }
76
+ a.btn[hidden],
77
+ button.btn[hidden] {
78
+ display: none;
79
+ }
80
+ .note {
81
+ margin-top: 1rem;
82
+ font-size: 0.8rem;
83
+ }
84
+ </style>
85
+ </head>
86
+ <body>
87
+ <main>
88
+ <h1>Mission Control</h1>
89
+ <p id="status">Opening your local Mission Control…</p>
90
+ <a id="open" class="btn" hidden href="#">Open Mission Control</a>
91
+ <p class="note">
92
+ Cosmetic share link only. You must be on the same trusted LAN (or VPN) as the host.
93
+ Treat the full Share URL as a secret (it embeds the session token in the fragment).
94
+ Only private/loopback LAN targets are accepted.
95
+ </p>
96
+ </main>
97
+ <script>
98
+ (function () {
99
+ var statusEl = document.getElementById("status");
100
+ var openEl = document.getElementById("open");
101
+
102
+ function fail(msg) {
103
+ statusEl.textContent = msg;
104
+ statusEl.className = "err";
105
+ openEl.hidden = true;
106
+ }
107
+
108
+ function isPrivateOrLoopbackHostname(hostname) {
109
+ var host = String(hostname || "")
110
+ .trim()
111
+ .toLowerCase()
112
+ .replace(/^\[|\]$/g, "");
113
+ if (!host) return false;
114
+ if (host === "localhost" || host.slice(-10) === ".localhost" || host.slice(-6) === ".local") {
115
+ return true;
116
+ }
117
+ if (host === "::1" || host === "0:0:0:0:0:0:0:1") return true;
118
+ var m = /^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/.exec(host);
119
+ if (m) {
120
+ var a = +m[1];
121
+ var b = +m[2];
122
+ var c = +m[3];
123
+ var d = +m[4];
124
+ if ([a, b, c, d].some(function (n) {
125
+ return !Number.isInteger(n) || n < 0 || n > 255;
126
+ })) {
127
+ return false;
128
+ }
129
+ if (a === 127) return true;
130
+ if (a === 10) return true;
131
+ if (a === 192 && b === 168) return true;
132
+ if (a === 172 && b >= 16 && b <= 31) return true;
133
+ if (a === 169 && b === 254) return true;
134
+ return false;
135
+ }
136
+ if (host.indexOf(":") !== -1) {
137
+ if (host.slice(0, 2) === "fc" || host.slice(0, 2) === "fd") return true;
138
+ if (/^fe[89ab]/.test(host)) return true;
139
+ }
140
+ return false;
141
+ }
142
+
143
+ function validateTarget(url) {
144
+ var raw = typeof url === "string" ? url.trim() : "";
145
+ if (!raw) return { ok: false, error: "invalid-target" };
146
+ var parsed;
147
+ try {
148
+ parsed = new URL(raw);
149
+ } catch (_) {
150
+ return { ok: false, error: "invalid-target" };
151
+ }
152
+ if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
153
+ return { ok: false, error: "invalid-target" };
154
+ }
155
+ if (!isPrivateOrLoopbackHostname(parsed.hostname)) {
156
+ return { ok: false, error: "non-private-target" };
157
+ }
158
+ return { ok: true, url: raw };
159
+ }
160
+
161
+ function decodeFragment(fragment) {
162
+ var raw = String(fragment || "").replace(/^#/, "").trim();
163
+ if (!raw) return { ok: false, error: "missing-fragment" };
164
+ var m = /^v1\.([A-Za-z0-9_-]+)$/.exec(raw);
165
+ if (!m) return { ok: false, error: "unsupported-version" };
166
+ try {
167
+ var b64 = m[1].replace(/-/g, "+").replace(/_/g, "/");
168
+ while (b64.length % 4) b64 += "=";
169
+ var json = atob(b64);
170
+ var parsed = JSON.parse(json);
171
+ if (!parsed || parsed.v !== 1 || typeof parsed.u !== "string") {
172
+ return { ok: false, error: "invalid-payload" };
173
+ }
174
+ var target = validateTarget(parsed.u.trim());
175
+ if (!target.ok) return target;
176
+ if (parsed.e != null) {
177
+ var e = Number(parsed.e);
178
+ if (!isFinite(e)) return { ok: false, error: "invalid-expiry" };
179
+ if (Math.floor(Date.now() / 1000) > e) return { ok: false, error: "expired" };
180
+ }
181
+ return { ok: true, url: target.url };
182
+ } catch (err) {
183
+ return { ok: false, error: "invalid-payload" };
184
+ }
185
+ }
186
+
187
+ var decoded = decodeFragment(location.hash);
188
+ if (!decoded.ok) {
189
+ var map = {
190
+ "missing-fragment": "This share link is incomplete. Ask the operator to re-run dashboard-broadcast.",
191
+ "unsupported-version": "This share link uses an unsupported format.",
192
+ "invalid-payload": "This share link could not be decoded.",
193
+ "invalid-target": "This share link has an invalid Mission Control URL.",
194
+ "non-private-target": "This share link targets a non-private host and was blocked.",
195
+ expired: "This share link expired. Ask the operator for a fresh link.",
196
+ };
197
+ fail(map[decoded.error] || "Could not open Mission Control.");
198
+ return;
199
+ }
200
+
201
+ openEl.href = decoded.url;
202
+ openEl.hidden = false;
203
+ statusEl.textContent = "Ready. Continue to Mission Control on your LAN.";
204
+ // Auto-navigate only for validated private/loopback targets; button remains if blocked.
205
+ try {
206
+ location.replace(decoded.url);
207
+ } catch (_) {
208
+ /* keep button */
209
+ }
210
+ })();
211
+ </script>
212
+ </body>
213
+ </html>
@@ -10,6 +10,7 @@ import { existsSync, mkdirSync, readFileSync, realpathSync, watch, writeFileSync
10
10
  import { createServer } from "node:http";
11
11
  import { dirname, extname, join } from "node:path";
12
12
  import { fileURLToPath } from "node:url";
13
+ import { shareShellTokenRequired } from "./lib/broadcast-share.mjs";
13
14
  import {
14
15
  DEFAULT_HOST,
15
16
  REPO_ROOT_ENV,
@@ -423,8 +424,10 @@ const server = createServer((req, res) => {
423
424
  return;
424
425
  }
425
426
 
427
+ // Cosmetic share resolver shell (fragment holds LAN+token client-side).
428
+ // ADR: 2026-08-11_mission-control-broadcast-url-mask.md
426
429
  const auth = authorizeMissionControlRequest(req, url, {
427
- tokenRequired: TOKEN_REQUIRED,
430
+ tokenRequired: shareShellTokenRequired(TOKEN_REQUIRED, req.method || "GET", path),
428
431
  expectedToken: AUTH_TOKEN,
429
432
  });
430
433
  if (!auth.ok) {
@@ -496,7 +499,8 @@ const server = createServer((req, res) => {
496
499
  return;
497
500
  }
498
501
 
499
- const staticPath = resolveStaticPath(path);
502
+ const staticLookup = path === "/open" ? "/open.html" : path;
503
+ const staticPath = resolveStaticPath(staticLookup);
500
504
  if (!staticPath) {
501
505
  res.writeHead(404);
502
506
  res.end("Not found");
@@ -9,9 +9,14 @@
9
9
 
10
10
  import { execFileSync, execSync, spawn } from "node:child_process";
11
11
  import { existsSync, openSync } from "node:fs";
12
- import { platform } from "node:os";
13
- import { dirname, join } from "node:path";
12
+ import { basename, dirname, join } from "node:path";
14
13
  import { fileURLToPath } from "node:url";
14
+ import {
15
+ buildBroadcastShareUrl,
16
+ resolveShareBase,
17
+ resolveShareShowLan,
18
+ resolveShareTtlSec,
19
+ } from "./lib/broadcast-share.mjs";
15
20
  import {
16
21
  BROADCAST_TOKEN_ENV,
17
22
  escapePerlDoubleQuoted,
@@ -21,10 +26,16 @@ import {
21
26
  listLanIPv4Addresses,
22
27
  normalizeAuthToken,
23
28
  resolveBindHost,
29
+ resolveContextConfigPath,
30
+ resolveSnapshotRepoRoot,
24
31
  } from "./lib/guards.mjs";
32
+ import { openBrowser, readPreferredBrowserFromConfig } from "./lib/open-browser.mjs";
25
33
 
26
34
  const __dirname = dirname(fileURLToPath(import.meta.url));
27
- const ROOT = join(__dirname, "..");
35
+ const KIT_ROOT = join(__dirname, "..");
36
+ /** Workspace snapshots / preference config. Defaults to KIT_ROOT. */
37
+ const ROOT = resolveSnapshotRepoRoot(process.env, KIT_ROOT);
38
+ process.title = `Mission Control · ${basename(ROOT) || "workspace"}`;
28
39
  const SERVE = join(__dirname, "serve.mjs");
29
40
  const LOG = process.env.MISSION_CONTROL_LOG || "/tmp/mission-control-broadcast.log";
30
41
  const PORT = Number.parseInt(process.env.PORT || "3333", 10);
@@ -97,7 +108,7 @@ function detachStart(env) {
97
108
  if (hasSetsid()) {
98
109
  const out = openSync(LOG, "a");
99
110
  const child = spawn("setsid", ["node", SERVE], {
100
- cwd: ROOT,
111
+ cwd: KIT_ROOT,
101
112
  detached: true,
102
113
  stdio: ["ignore", out, out],
103
114
  env,
@@ -107,7 +118,7 @@ function detachStart(env) {
107
118
  }
108
119
 
109
120
  // Escape @/$ so scoped package paths (node_modules/@scope/...) survive Perl qq.
110
- const rootEsc = escapePerlDoubleQuoted(ROOT);
121
+ const rootEsc = escapePerlDoubleQuoted(KIT_ROOT);
111
122
  const serveEsc = escapePerlDoubleQuoted(SERVE);
112
123
  const logEsc = escapePerlDoubleQuoted(LOG);
113
124
  const hostEsc = escapePerlDoubleQuoted(String(env.HOST));
@@ -129,7 +140,7 @@ function detachStart(env) {
129
140
  ].join(" ");
130
141
 
131
142
  const child = spawn("perl", ["-e", perl], {
132
- cwd: ROOT,
143
+ cwd: KIT_ROOT,
133
144
  detached: true,
134
145
  stdio: "ignore",
135
146
  env,
@@ -148,24 +159,6 @@ async function waitReady(urls) {
148
159
  return null;
149
160
  }
150
161
 
151
- function openBrowser(url) {
152
- const os = platform();
153
- try {
154
- if (os === "darwin") {
155
- spawn("open", [url], { detached: true, stdio: "ignore" }).unref();
156
- return true;
157
- }
158
- if (os === "win32") {
159
- spawn("cmd", ["/c", "start", "", url], { detached: true, stdio: "ignore" }).unref();
160
- return true;
161
- }
162
- spawn("xdg-open", [url], { detached: true, stdio: "ignore" }).unref();
163
- return true;
164
- } catch {
165
- return false;
166
- }
167
- }
168
-
169
162
  async function main() {
170
163
  const { env, host, token } = resolveBroadcastEnv();
171
164
  if (isLoopbackBindHost(host)) {
@@ -203,14 +196,43 @@ async function main() {
203
196
  console.log(`Mission Control broadcast already listening on port ${PORT}`);
204
197
  }
205
198
 
199
+ const shareBase = resolveShareBase(process.env);
200
+ let shareUrl = null;
201
+ if (shareBase != null) {
202
+ try {
203
+ shareUrl = buildBroadcastShareUrl(displayUrl, {
204
+ base: shareBase,
205
+ ttlSec: resolveShareTtlSec(process.env),
206
+ });
207
+ } catch (err) {
208
+ // Non-RFC1918 primary LAN (Tailscale 100.64/10, public/DMZ) cannot encode into
209
+ // the share fragment allowlist — degrade to LAN/token print instead of exit 1.
210
+ const msg = err instanceof Error ? err.message : String(err);
211
+ console.warn(`Share URL skipped (${msg}). Printing LAN/token only.`);
212
+ shareUrl = null;
213
+ }
214
+ }
215
+ const showLan = resolveShareShowLan(process.env);
216
+ const openTarget = shareUrl || displayUrl;
217
+
206
218
  console.log("");
207
219
  console.log(" Mission Control (LAN broadcast)");
208
220
  console.log(` Bind: ${host}:${PORT}`);
221
+ if (shareUrl) {
222
+ console.log(` Share: ${shareUrl}`);
223
+ }
209
224
  console.log(` Token: ${token}`);
210
- for (const ip of listLanIPv4Addresses()) {
211
- console.log(` LAN: http://${ip}:${PORT}/?token=${encodeURIComponent(token)}`);
225
+ if (showLan || !shareUrl) {
226
+ for (const ip of listLanIPv4Addresses()) {
227
+ console.log(` LAN: http://${ip}:${PORT}/?token=${encodeURIComponent(token)}`);
228
+ }
229
+ console.log(` Local: http://127.0.0.1:${PORT}/?token=${encodeURIComponent(token)}`);
230
+ }
231
+ if (shareUrl) {
232
+ console.log(
233
+ " Share is a cosmetic Mission Kit (or BYO) link; phone must still reach this LAN.",
234
+ );
212
235
  }
213
- console.log(` Local: http://127.0.0.1:${PORT}/?token=${encodeURIComponent(token)}`);
214
236
  console.log(" Config writes stay loopback-only. Stop: kill the LISTEN pid on this port.");
215
237
  console.log(" Firewall: allow inbound TCP on this port for your LAN profile if needed.");
216
238
  console.log("");
@@ -218,10 +240,32 @@ async function main() {
218
240
  if (process.env.MISSION_CONTROL_NO_OPEN === "1") {
219
241
  return;
220
242
  }
221
- if (openBrowser(displayUrl)) {
222
- console.log("Opened primary URL in the default browser.");
223
- } else {
224
- console.log("Open a LAN URL above on your phone/tablet browser.");
243
+ let configValue = null;
244
+ const cfg = resolveContextConfigPath(ROOT, { existsSync });
245
+ if (cfg.ok) {
246
+ configValue = readPreferredBrowserFromConfig(cfg.path);
247
+ }
248
+ const result = openBrowser(openTarget, { configValue });
249
+ if (result.opened) {
250
+ if (result.reason === "preferred-fallback") {
251
+ console.log(
252
+ shareUrl
253
+ ? "Preferred browser failed; opened share URL with the OS default."
254
+ : "Preferred browser failed; opened primary URL with the OS default.",
255
+ );
256
+ } else {
257
+ console.log(
258
+ shareUrl
259
+ ? "Opened share URL in the preferred browser (or OS default)."
260
+ : "Opened primary URL in the preferred browser (or OS default).",
261
+ );
262
+ }
263
+ } else if (result.reason !== "no-open") {
264
+ console.log(
265
+ shareUrl
266
+ ? "Open the Share URL above on your phone/tablet browser."
267
+ : "Open a LAN URL above on your phone/tablet browser.",
268
+ );
225
269
  }
226
270
  }
227
271
 
@@ -4,7 +4,8 @@
4
4
  *
5
5
  * Allocates a stable per-workspace listen port (hash of snapshot root in the
6
6
  * 3333–3588 range unless PORT is set), detach-starts `serve.mjs` when needed,
7
- * waits until HTTP 200, prints the URL, and opens the default browser.
7
+ * waits until HTTP 200, prints the URL, and opens one preferred browser
8
+ * (or OS default). Never opens more than one browser process.
8
9
  *
9
10
  * Never kills a listener whose system.repoRoot belongs to another workspace.
10
11
  *
@@ -17,21 +18,23 @@
17
18
 
18
19
  import { execFileSync, execSync, spawn } from "node:child_process";
19
20
  import { existsSync, openSync } from "node:fs";
20
- import { platform } from "node:os";
21
- import { dirname, join, resolve } from "node:path";
21
+ import { basename, dirname, join, resolve } from "node:path";
22
22
  import { fileURLToPath } from "node:url";
23
23
  import {
24
24
  REPO_ROOT_ENV,
25
25
  escapePerlDoubleQuoted,
26
26
  repoRootLogId,
27
+ resolveContextConfigPath,
27
28
  resolveMissionControlPort,
28
29
  resolveSnapshotRepoRoot,
29
30
  sameRepoRoot,
30
31
  } from "./lib/guards.mjs";
32
+ import { openBrowser, readPreferredBrowserFromConfig } from "./lib/open-browser.mjs";
31
33
 
32
34
  const __dirname = dirname(fileURLToPath(import.meta.url));
33
35
  const KIT_ROOT = join(__dirname, "..");
34
36
  const ROOT = resolveSnapshotRepoRoot(process.env, KIT_ROOT);
37
+ process.title = `Mission Control · ${basename(ROOT) || "workspace"}`;
35
38
  const SERVE = join(__dirname, "serve.mjs");
36
39
  const HOST = process.env.HOST || "127.0.0.1";
37
40
  const DISPLAY_HOST = HOST === "0.0.0.0" ? "127.0.0.1" : HOST;
@@ -188,24 +191,6 @@ async function waitReady() {
188
191
  return false;
189
192
  }
190
193
 
191
- function openBrowser(url) {
192
- const os = platform();
193
- try {
194
- if (os === "darwin") {
195
- spawn("open", [url], { detached: true, stdio: "ignore" }).unref();
196
- return true;
197
- }
198
- if (os === "win32") {
199
- spawn("cmd", ["/c", "start", "", url], { detached: true, stdio: "ignore" }).unref();
200
- return true;
201
- }
202
- spawn("xdg-open", [url], { detached: true, stdio: "ignore" }).unref();
203
- return true;
204
- } catch {
205
- return false;
206
- }
207
- }
208
-
209
194
  async function ensureServer() {
210
195
  const allocation = resolveMissionControlPort({
211
196
  repoRoot: ROOT,
@@ -271,11 +256,23 @@ async function main() {
271
256
  if (process.env.MISSION_CONTROL_NO_OPEN === "1") {
272
257
  return;
273
258
  }
274
- if (openBrowser(URL)) {
275
- console.log(
276
- "Opened in the default browser. In Cursor, Simple Browser or /dashboard also works.",
277
- );
278
- } else {
259
+ let configValue = null;
260
+ const cfg = resolveContextConfigPath(ROOT, { existsSync });
261
+ if (cfg.ok) {
262
+ configValue = readPreferredBrowserFromConfig(cfg.path);
263
+ }
264
+ const result = openBrowser(URL, { configValue });
265
+ if (result.opened) {
266
+ if (result.reason === "preferred-fallback") {
267
+ console.log(
268
+ "Preferred browser failed; opened with the OS default. In Cursor, Simple Browser or /dashboard also works.",
269
+ );
270
+ } else {
271
+ console.log(
272
+ "Opened in the preferred browser (or OS default). In Cursor, Simple Browser or /dashboard also works.",
273
+ );
274
+ }
275
+ } else if (result.reason !== "no-open") {
279
276
  console.log("Open that URL in a browser (Cursor: Simple Browser, or run /dashboard in chat).");
280
277
  }
281
278
  }