@ait-co/devtools 0.1.72 → 0.1.73

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 (51) hide show
  1. package/dist/{deeplink-CaO6hZVG.js → deeplink-B-94XmWA.js} +19 -3
  2. package/dist/deeplink-B-94XmWA.js.map +1 -0
  3. package/dist/{deeplink-BONXxWEO.cjs → deeplink-BLU2_hg6.cjs} +19 -3
  4. package/dist/deeplink-BLU2_hg6.cjs.map +1 -0
  5. package/dist/{deeplink-CCGiyoHq.cjs → deeplink-CU6opogq.cjs} +19 -3
  6. package/dist/deeplink-CU6opogq.cjs.map +1 -0
  7. package/dist/{deeplink-Cqli4qzm.js → deeplink-CYqDwVYs.js} +19 -3
  8. package/dist/deeplink-CYqDwVYs.js.map +1 -0
  9. package/dist/mcp/cli.js +48 -15
  10. package/dist/mcp/cli.js.map +1 -1
  11. package/dist/mcp/server.js +2 -2
  12. package/dist/mcp/server.js.map +1 -1
  13. package/dist/panel/index.js +16 -8
  14. package/dist/panel/index.js.map +1 -1
  15. package/dist/{qr-http-server-DPOOrh6y.cjs → qr-http-server-0uN5jxLW.cjs} +15 -7
  16. package/dist/qr-http-server-0uN5jxLW.cjs.map +1 -0
  17. package/dist/{qr-http-server-CG198dpc.cjs → qr-http-server-BTjpFS3p.cjs} +15 -7
  18. package/dist/qr-http-server-BTjpFS3p.cjs.map +1 -0
  19. package/dist/{qr-http-server-Bh9qkiRm.js → qr-http-server-Ditd2ndz.js} +15 -7
  20. package/dist/qr-http-server-Ditd2ndz.js.map +1 -0
  21. package/dist/{qr-http-server-CwkTFbMV.js → qr-http-server-TQG61eI4.js} +15 -7
  22. package/dist/qr-http-server-TQG61eI4.js.map +1 -0
  23. package/dist/{tunnel-BS6Td6f4.cjs → tunnel-BXAWl2tI.cjs} +21 -10
  24. package/dist/tunnel-BXAWl2tI.cjs.map +1 -0
  25. package/dist/{tunnel-CQdoKopR.js → tunnel-BxGnLAat.js} +21 -10
  26. package/dist/tunnel-BxGnLAat.js.map +1 -0
  27. package/dist/unplugin/index.cjs +13 -3
  28. package/dist/unplugin/index.cjs.map +1 -1
  29. package/dist/unplugin/index.d.cts.map +1 -1
  30. package/dist/unplugin/index.d.ts.map +1 -1
  31. package/dist/unplugin/index.js +13 -3
  32. package/dist/unplugin/index.js.map +1 -1
  33. package/dist/unplugin/tunnel.cjs +20 -9
  34. package/dist/unplugin/tunnel.cjs.map +1 -1
  35. package/dist/unplugin/tunnel.d.cts +36 -3
  36. package/dist/unplugin/tunnel.d.cts.map +1 -1
  37. package/dist/unplugin/tunnel.d.ts +36 -3
  38. package/dist/unplugin/tunnel.d.ts.map +1 -1
  39. package/dist/unplugin/tunnel.js +20 -9
  40. package/dist/unplugin/tunnel.js.map +1 -1
  41. package/package.json +1 -1
  42. package/dist/deeplink-BONXxWEO.cjs.map +0 -1
  43. package/dist/deeplink-CCGiyoHq.cjs.map +0 -1
  44. package/dist/deeplink-CaO6hZVG.js.map +0 -1
  45. package/dist/deeplink-Cqli4qzm.js.map +0 -1
  46. package/dist/qr-http-server-Bh9qkiRm.js.map +0 -1
  47. package/dist/qr-http-server-CG198dpc.cjs.map +0 -1
  48. package/dist/qr-http-server-CwkTFbMV.js.map +0 -1
  49. package/dist/qr-http-server-DPOOrh6y.cjs.map +0 -1
  50. package/dist/tunnel-BS6Td6f4.cjs.map +0 -1
  51. package/dist/tunnel-CQdoKopR.js.map +0 -1
@@ -16,6 +16,11 @@ const LAUNCHER_URL = "https://devtools.aitc.dev/launcher/";
16
16
  * is injected. `&at=<totpCode>` is added only when a code is provided (same
17
17
  * conditional as {@link buildDeepLinkAttachUrl}).
18
18
  *
19
+ * When `opts.name` is given (non-blank), it is added as `&name=` so the
20
+ * launcher partner bar shows the app name instead of the generic default (#498).
21
+ * When `opts.icon` is an absolute https:// URL, it is added as `&icon=` so the
22
+ * launcher can render an icon next to the title (#498).
23
+ *
19
24
  * Unlike `buildDeepLinkAttachUrl` (which splices onto a non-special scheme URL
20
25
  * via raw string manipulation), this function uses WHATWG `encodeURIComponent`
21
26
  * because the target is a standard `https:` URL.
@@ -30,15 +35,26 @@ const LAUNCHER_URL = "https://devtools.aitc.dev/launcher/";
30
35
  * @param totpCode - Optional current TOTP code (6 digits). When provided, it
31
36
  * is appended as `at=<totpCode>`. Must be computed at call time — it rotates
32
37
  * every 30 s. Omit when TOTP is disabled.
38
+ * @param opts - Optional app identity hints: `name` and `icon` (#498).
33
39
  * @returns The launcher deep-link URL with `?url=<enc>&debug=1&relay=<enc>
34
- * [&at=<code>]` params.
40
+ * [&at=<code>][&name=<enc>][&icon=<enc>]` params.
35
41
  */
36
- function buildLauncherAttachUrl(tunnelUrl, wssUrl, totpCode) {
42
+ function buildLauncherAttachUrl(tunnelUrl, wssUrl, totpCode, opts) {
37
43
  let url = `${LAUNCHER_URL}?url=${encodeURIComponent(tunnelUrl)}&debug=1&relay=${encodeURIComponent(wssUrl)}`;
38
44
  if (totpCode !== void 0 && totpCode !== "") url += `&at=${encodeURIComponent(totpCode)}`;
45
+ if (opts?.name !== void 0 && opts.name.trim() !== "") url += `&name=${encodeURIComponent(opts.name.trim())}`;
46
+ if (opts?.icon !== void 0) {
47
+ let iconParsed;
48
+ try {
49
+ iconParsed = new URL(opts.icon);
50
+ } catch {
51
+ iconParsed = null;
52
+ }
53
+ if (iconParsed?.protocol === "https:") url += `&icon=${encodeURIComponent(opts.icon)}`;
54
+ }
39
55
  return url;
40
56
  }
41
57
  //#endregion
42
58
  export { buildLauncherAttachUrl };
43
59
 
44
- //# sourceMappingURL=deeplink-CaO6hZVG.js.map
60
+ //# sourceMappingURL=deeplink-B-94XmWA.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"deeplink-B-94XmWA.js","names":[],"sources":["../src/mcp/deeplink.ts"],"sourcesContent":["/**\n * URL of the AITC Sandbox launcher PWA.\n *\n * Declared here (not imported from `src/unplugin/tunnel.ts`) to respect the\n * mcp → unplugin layering boundary. unplugin/tunnel.ts declares its own copy\n * for the same reason — keep the two in sync when the URL changes.\n */\nconst LAUNCHER_URL = 'https://devtools.aitc.dev/launcher/';\n\n/**\n * Optional metadata that enriches the launcher deep-link (#498).\n *\n * These fields are added as query params so the launcher PWA can display\n * a recognizable identity (name, icon) without the user having to configure\n * anything extra.\n */\nexport interface LauncherAttachUrlOpts {\n /**\n * Human-readable app name shown in the partner nav bar (`name=` param).\n * Blank / whitespace-only values are not added.\n */\n name?: string;\n /**\n * Absolute `https://` icon URL for the partner nav bar icon slot (`icon=`\n * param). Non-https or falsy values are not added.\n */\n icon?: string;\n}\n\n/**\n * Builds a launcher PWA deep-link for env-2 MCP-attach (issue #378).\n *\n * The launcher at {@link LAUNCHER_URL} renders tunnelUrl in a full-viewport\n * iframe. `&debug=1&relay=<wssUrl>` is forwarded onto the iframe src so the\n * framed page's in-app debug gate (Layer C) is satisfied and a Chii target.js\n * is injected. `&at=<totpCode>` is added only when a code is provided (same\n * conditional as {@link buildDeepLinkAttachUrl}).\n *\n * When `opts.name` is given (non-blank), it is added as `&name=` so the\n * launcher partner bar shows the app name instead of the generic default (#498).\n * When `opts.icon` is an absolute https:// URL, it is added as `&icon=` so the\n * launcher can render an icon next to the title (#498).\n *\n * Unlike `buildDeepLinkAttachUrl` (which splices onto a non-special scheme URL\n * via raw string manipulation), this function uses WHATWG `encodeURIComponent`\n * because the target is a standard `https:` URL.\n *\n * SECRET-HANDLING: `totpCode` (when provided) is placed into the `at=` param\n * only — never logged or returned separately. Callers must NOT log the result\n * of this function to stdout/stderr.\n *\n * @param tunnelUrl - The `https://*.trycloudflare.com` app tunnel URL\n * (`AIT_TUNNEL_BASE_URL`). This is the URL the launcher frames.\n * @param wssUrl - The `wss://` relay URL the framed page will attach to.\n * @param totpCode - Optional current TOTP code (6 digits). When provided, it\n * is appended as `at=<totpCode>`. Must be computed at call time — it rotates\n * every 30 s. Omit when TOTP is disabled.\n * @param opts - Optional app identity hints: `name` and `icon` (#498).\n * @returns The launcher deep-link URL with `?url=<enc>&debug=1&relay=<enc>\n * [&at=<code>][&name=<enc>][&icon=<enc>]` params.\n */\nexport function buildLauncherAttachUrl(\n tunnelUrl: string,\n wssUrl: string,\n totpCode?: string,\n opts?: LauncherAttachUrlOpts,\n): string {\n let url =\n `${LAUNCHER_URL}?url=${encodeURIComponent(tunnelUrl)}` +\n `&debug=1&relay=${encodeURIComponent(wssUrl)}`;\n if (totpCode !== undefined && totpCode !== '') {\n url += `&at=${encodeURIComponent(totpCode)}`;\n }\n // App identity hints (#498): add non-blank name and valid https icon.\n if (opts?.name !== undefined && opts.name.trim() !== '') {\n url += `&name=${encodeURIComponent(opts.name.trim())}`;\n }\n if (opts?.icon !== undefined) {\n let iconParsed: URL;\n try {\n iconParsed = new URL(opts.icon);\n } catch {\n iconParsed = null as unknown as URL;\n }\n if (iconParsed?.protocol === 'https:') {\n url += `&icon=${encodeURIComponent(opts.icon)}`;\n }\n }\n return url;\n}\n\n/**\n * Build a self-attaching dogfood deep link.\n *\n * `ait deploy --scheme-only` prints an `intoss-private://…?_deploymentId=<uuid>`\n * URL that opens a dogfood bundle on a phone. The in-app debug gate\n * (`src/in-app/gate.ts`) auto-attaches when the entry URL also carries\n * `debug=1` and `relay=<wss-url>`. This helper splices those params (plus\n * `at=<code>` when TOTP is enabled) into the scheme URL; rendering the result\n * as a QR code and scanning it with the phone camera opens the mini-app and\n * attaches it to the live Chii relay. QR is the single entry path — it needs\n * no USB cable, platform CLI, or driver, and works the same on iOS/Android.\n *\n * The Toss app propagates extra query params from the entry deep link into the\n * mini-app WebView's `location.search` (confirmed behavior), so the gate reads\n * them at attach time.\n *\n * TOTP `at=` param:\n * When a TOTP secret is active, `buildDeepLinkAttachUrl` accepts an optional\n * `totpCode` argument and splices `at=<code>` alongside `debug` and `relay`.\n * The code must be computed by the caller at call time — do NOT pre-compute\n * and cache it, because the 30-second window expires quickly. The in-app gate\n * (`src/in-app/gate.ts` Layer C) validates this code against the baked secret.\n *\n * Why not `URL`/`URLSearchParams`: `intoss-private:` is a non-special scheme.\n * The WHATWG `URL` parser treats such schemes opaquely (no host/path/query\n * decomposition you can rely on across runtimes), so query manipulation via\n * `url.searchParams` is not portable here. We splice the query string directly\n * on the raw string instead, which keeps the scheme, authority, path, and any\n * pre-existing params (notably `_deploymentId`) byte-for-byte intact.\n */\n\n/**\n * Suspicious/generic authority values that indicate a malformed or placeholder\n * scheme URL. These are host strings that will almost certainly cause the Toss\n * app to fail with \"bundle not found\" silently.\n *\n * The expected form from `ait deploy --scheme-only` is:\n * intoss-private://<appName>?_deploymentId=<uuid>\n * where `<appName>` is a non-generic string like `aitc-sdk-example`.\n */\nconst SUSPICIOUS_AUTHORITIES = new Set<string>(['', 'web', 'localhost', '127.0.0.1', 'app']);\n\n/**\n * Validates the authority (host) portion of a scheme URL.\n *\n * Returns a warning message if the authority is missing or looks like a\n * placeholder, or `null` if the authority looks valid.\n *\n * Expected form: `intoss-private://<appName>?_deploymentId=<uuid>`\n * The authority must be a non-empty, non-generic app name (e.g. `aitc-sdk-example`).\n */\nexport function validateSchemeAuthority(schemeUrl: string): string | null {\n // Extract authority from `scheme://authority[/path][?query][#hash]`.\n // We cannot use the WHATWG URL parser for non-special schemes reliably\n // (see the deeplink.ts module comment), so we parse the raw string.\n const afterScheme = schemeUrl.replace(/^[a-zA-Z][a-zA-Z0-9+\\-.]*:\\/\\//, '');\n if (afterScheme === schemeUrl) {\n // No `://` found — not a scheme URL at all.\n return (\n 'scheme_url does not look like a scheme URL (expected `intoss-private://<appName>?_deploymentId=<uuid>`). ' +\n 'Use the URL printed by `ait deploy --scheme-only`.'\n );\n }\n\n // authority ends at the first `/`, `?`, `#`, or end of string.\n const authorityEnd = afterScheme.search(/[/?#]/);\n const authority = authorityEnd === -1 ? afterScheme : afterScheme.slice(0, authorityEnd);\n\n if (SUSPICIOUS_AUTHORITIES.has(authority.toLowerCase())) {\n const displayAuthority = authority === '' ? '(empty)' : `\"${authority}\"`;\n return (\n `scheme_url authority ${displayAuthority} looks like a placeholder. ` +\n 'Expected an app name like `intoss-private://aitc-sdk-example?_deploymentId=<uuid>`. ' +\n 'Use the URL printed by `ait deploy --scheme-only` — it includes the correct app name as the host.'\n );\n }\n\n return null;\n}\n\n/** A param the helper appends. Existing occurrences are replaced, not duplicated. */\ntype AppendParam = readonly [key: string, value: string];\n\nfunction stripExisting(query: string, key: string): string {\n if (query === '') return '';\n return query\n .split('&')\n .filter((pair) => pair !== '' && pair.split('=')[0] !== key)\n .join('&');\n}\n\n/**\n * Splices `debug=1`, `relay=<wssUrl>`, and (optionally) `at=<totpCode>` into a\n * scheme URL's query string, preserving everything else (scheme, authority,\n * path, hash, and the existing `_deploymentId` param). If any of the spliced\n * params is already present it is replaced so the helper is idempotent.\n *\n * @param schemeUrl - The `intoss-private://…?_deploymentId=<uuid>` URL printed\n * by `ait deploy --scheme-only`. Must already carry `_deploymentId` (Layer B\n * of the gate); this helper does not invent one.\n * @param wssUrl - The live relay URL (`wss://…trycloudflare.com`) from the\n * running debug MCP server's quick tunnel.\n * @param totpCode - Optional current TOTP code (6 digits). When provided, it\n * is spliced as `at=<totpCode>`. Must be computed at call time — it rotates\n * every 30 s. Pass `undefined` or omit when TOTP is disabled.\n * @returns The same URL with `debug=1&relay=<encoded wssUrl>[&at=<totpCode>]`\n * appended.\n * @throws If `wssUrl` is not a `wss:` URL (the gate rejects anything else, so\n * producing such a link would be a silent dead end).\n */\nexport function buildDeepLinkAttachUrl(\n schemeUrl: string,\n wssUrl: string,\n totpCode?: string,\n): string {\n let relay: URL;\n try {\n relay = new URL(wssUrl);\n } catch {\n throw new Error(`relay URL is not a valid URL: ${wssUrl}`);\n }\n if (relay.protocol !== 'wss:') {\n throw new Error(`relay URL must use the wss: scheme, got ${relay.protocol} (${wssUrl})`);\n }\n\n const hashIndex = schemeUrl.indexOf('#');\n const hash = hashIndex === -1 ? '' : schemeUrl.slice(hashIndex);\n const beforeHash = hashIndex === -1 ? schemeUrl : schemeUrl.slice(0, hashIndex);\n\n const queryIndex = beforeHash.indexOf('?');\n const base = queryIndex === -1 ? beforeHash : beforeHash.slice(0, queryIndex);\n let query = queryIndex === -1 ? '' : beforeHash.slice(queryIndex + 1);\n\n const appended: AppendParam[] = [\n ['debug', '1'],\n ['relay', wssUrl],\n ];\n // Only splice `at=` when a code is provided (TOTP enabled). Omitting it when\n // TOTP is disabled preserves backward compatibility with gate deployments\n // that do not yet evaluate the `at` param.\n if (totpCode !== undefined && totpCode !== '') {\n appended.push(['at', totpCode]);\n }\n\n // Always strip the `at` key from the existing query so a stale code from a\n // previous run is removed even when the caller does not provide a fresh code.\n query = stripExisting(query, 'at');\n\n for (const [key] of appended) {\n query = stripExisting(query, key);\n }\n for (const [key, value] of appended) {\n const pair = `${key}=${encodeURIComponent(value)}`;\n query = query === '' ? pair : `${query}&${pair}`;\n }\n\n return `${base}?${query}${hash}`;\n}\n"],"mappings":";;;;;;;;AAOA,MAAM,eAAe;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAsDrB,SAAgB,uBACd,WACA,QACA,UACA,MACQ;CACR,IAAI,MACF,GAAG,aAAa,OAAO,mBAAmB,UAAU,CAAA,iBAClC,mBAAmB,OAAO;AAC9C,KAAI,aAAa,KAAA,KAAa,aAAa,GACzC,QAAO,OAAO,mBAAmB,SAAS;AAG5C,KAAI,MAAM,SAAS,KAAA,KAAa,KAAK,KAAK,MAAM,KAAK,GACnD,QAAO,SAAS,mBAAmB,KAAK,KAAK,MAAM,CAAC;AAEtD,KAAI,MAAM,SAAS,KAAA,GAAW;EAC5B,IAAI;AACJ,MAAI;AACF,gBAAa,IAAI,IAAI,KAAK,KAAK;UACzB;AACN,gBAAa;;AAEf,MAAI,YAAY,aAAa,SAC3B,QAAO,SAAS,mBAAmB,KAAK,KAAK;;AAGjD,QAAO"}
@@ -16,6 +16,11 @@ const LAUNCHER_URL = "https://devtools.aitc.dev/launcher/";
16
16
  * is injected. `&at=<totpCode>` is added only when a code is provided (same
17
17
  * conditional as {@link buildDeepLinkAttachUrl}).
18
18
  *
19
+ * When `opts.name` is given (non-blank), it is added as `&name=` so the
20
+ * launcher partner bar shows the app name instead of the generic default (#498).
21
+ * When `opts.icon` is an absolute https:// URL, it is added as `&icon=` so the
22
+ * launcher can render an icon next to the title (#498).
23
+ *
19
24
  * Unlike `buildDeepLinkAttachUrl` (which splices onto a non-special scheme URL
20
25
  * via raw string manipulation), this function uses WHATWG `encodeURIComponent`
21
26
  * because the target is a standard `https:` URL.
@@ -30,15 +35,26 @@ const LAUNCHER_URL = "https://devtools.aitc.dev/launcher/";
30
35
  * @param totpCode - Optional current TOTP code (6 digits). When provided, it
31
36
  * is appended as `at=<totpCode>`. Must be computed at call time — it rotates
32
37
  * every 30 s. Omit when TOTP is disabled.
38
+ * @param opts - Optional app identity hints: `name` and `icon` (#498).
33
39
  * @returns The launcher deep-link URL with `?url=<enc>&debug=1&relay=<enc>
34
- * [&at=<code>]` params.
40
+ * [&at=<code>][&name=<enc>][&icon=<enc>]` params.
35
41
  */
36
- function buildLauncherAttachUrl(tunnelUrl, wssUrl, totpCode) {
42
+ function buildLauncherAttachUrl(tunnelUrl, wssUrl, totpCode, opts) {
37
43
  let url = `${LAUNCHER_URL}?url=${encodeURIComponent(tunnelUrl)}&debug=1&relay=${encodeURIComponent(wssUrl)}`;
38
44
  if (totpCode !== void 0 && totpCode !== "") url += `&at=${encodeURIComponent(totpCode)}`;
45
+ if (opts?.name !== void 0 && opts.name.trim() !== "") url += `&name=${encodeURIComponent(opts.name.trim())}`;
46
+ if (opts?.icon !== void 0) {
47
+ let iconParsed;
48
+ try {
49
+ iconParsed = new URL(opts.icon);
50
+ } catch {
51
+ iconParsed = null;
52
+ }
53
+ if (iconParsed?.protocol === "https:") url += `&icon=${encodeURIComponent(opts.icon)}`;
54
+ }
39
55
  return url;
40
56
  }
41
57
  //#endregion
42
58
  exports.buildLauncherAttachUrl = buildLauncherAttachUrl;
43
59
 
44
- //# sourceMappingURL=deeplink-BONXxWEO.cjs.map
60
+ //# sourceMappingURL=deeplink-BLU2_hg6.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"deeplink-BLU2_hg6.cjs","names":[],"sources":["../src/mcp/deeplink.ts"],"sourcesContent":["/**\n * URL of the AITC Sandbox launcher PWA.\n *\n * Declared here (not imported from `src/unplugin/tunnel.ts`) to respect the\n * mcp → unplugin layering boundary. unplugin/tunnel.ts declares its own copy\n * for the same reason — keep the two in sync when the URL changes.\n */\nconst LAUNCHER_URL = 'https://devtools.aitc.dev/launcher/';\n\n/**\n * Optional metadata that enriches the launcher deep-link (#498).\n *\n * These fields are added as query params so the launcher PWA can display\n * a recognizable identity (name, icon) without the user having to configure\n * anything extra.\n */\nexport interface LauncherAttachUrlOpts {\n /**\n * Human-readable app name shown in the partner nav bar (`name=` param).\n * Blank / whitespace-only values are not added.\n */\n name?: string;\n /**\n * Absolute `https://` icon URL for the partner nav bar icon slot (`icon=`\n * param). Non-https or falsy values are not added.\n */\n icon?: string;\n}\n\n/**\n * Builds a launcher PWA deep-link for env-2 MCP-attach (issue #378).\n *\n * The launcher at {@link LAUNCHER_URL} renders tunnelUrl in a full-viewport\n * iframe. `&debug=1&relay=<wssUrl>` is forwarded onto the iframe src so the\n * framed page's in-app debug gate (Layer C) is satisfied and a Chii target.js\n * is injected. `&at=<totpCode>` is added only when a code is provided (same\n * conditional as {@link buildDeepLinkAttachUrl}).\n *\n * When `opts.name` is given (non-blank), it is added as `&name=` so the\n * launcher partner bar shows the app name instead of the generic default (#498).\n * When `opts.icon` is an absolute https:// URL, it is added as `&icon=` so the\n * launcher can render an icon next to the title (#498).\n *\n * Unlike `buildDeepLinkAttachUrl` (which splices onto a non-special scheme URL\n * via raw string manipulation), this function uses WHATWG `encodeURIComponent`\n * because the target is a standard `https:` URL.\n *\n * SECRET-HANDLING: `totpCode` (when provided) is placed into the `at=` param\n * only — never logged or returned separately. Callers must NOT log the result\n * of this function to stdout/stderr.\n *\n * @param tunnelUrl - The `https://*.trycloudflare.com` app tunnel URL\n * (`AIT_TUNNEL_BASE_URL`). This is the URL the launcher frames.\n * @param wssUrl - The `wss://` relay URL the framed page will attach to.\n * @param totpCode - Optional current TOTP code (6 digits). When provided, it\n * is appended as `at=<totpCode>`. Must be computed at call time — it rotates\n * every 30 s. Omit when TOTP is disabled.\n * @param opts - Optional app identity hints: `name` and `icon` (#498).\n * @returns The launcher deep-link URL with `?url=<enc>&debug=1&relay=<enc>\n * [&at=<code>][&name=<enc>][&icon=<enc>]` params.\n */\nexport function buildLauncherAttachUrl(\n tunnelUrl: string,\n wssUrl: string,\n totpCode?: string,\n opts?: LauncherAttachUrlOpts,\n): string {\n let url =\n `${LAUNCHER_URL}?url=${encodeURIComponent(tunnelUrl)}` +\n `&debug=1&relay=${encodeURIComponent(wssUrl)}`;\n if (totpCode !== undefined && totpCode !== '') {\n url += `&at=${encodeURIComponent(totpCode)}`;\n }\n // App identity hints (#498): add non-blank name and valid https icon.\n if (opts?.name !== undefined && opts.name.trim() !== '') {\n url += `&name=${encodeURIComponent(opts.name.trim())}`;\n }\n if (opts?.icon !== undefined) {\n let iconParsed: URL;\n try {\n iconParsed = new URL(opts.icon);\n } catch {\n iconParsed = null as unknown as URL;\n }\n if (iconParsed?.protocol === 'https:') {\n url += `&icon=${encodeURIComponent(opts.icon)}`;\n }\n }\n return url;\n}\n\n/**\n * Build a self-attaching dogfood deep link.\n *\n * `ait deploy --scheme-only` prints an `intoss-private://…?_deploymentId=<uuid>`\n * URL that opens a dogfood bundle on a phone. The in-app debug gate\n * (`src/in-app/gate.ts`) auto-attaches when the entry URL also carries\n * `debug=1` and `relay=<wss-url>`. This helper splices those params (plus\n * `at=<code>` when TOTP is enabled) into the scheme URL; rendering the result\n * as a QR code and scanning it with the phone camera opens the mini-app and\n * attaches it to the live Chii relay. QR is the single entry path — it needs\n * no USB cable, platform CLI, or driver, and works the same on iOS/Android.\n *\n * The Toss app propagates extra query params from the entry deep link into the\n * mini-app WebView's `location.search` (confirmed behavior), so the gate reads\n * them at attach time.\n *\n * TOTP `at=` param:\n * When a TOTP secret is active, `buildDeepLinkAttachUrl` accepts an optional\n * `totpCode` argument and splices `at=<code>` alongside `debug` and `relay`.\n * The code must be computed by the caller at call time — do NOT pre-compute\n * and cache it, because the 30-second window expires quickly. The in-app gate\n * (`src/in-app/gate.ts` Layer C) validates this code against the baked secret.\n *\n * Why not `URL`/`URLSearchParams`: `intoss-private:` is a non-special scheme.\n * The WHATWG `URL` parser treats such schemes opaquely (no host/path/query\n * decomposition you can rely on across runtimes), so query manipulation via\n * `url.searchParams` is not portable here. We splice the query string directly\n * on the raw string instead, which keeps the scheme, authority, path, and any\n * pre-existing params (notably `_deploymentId`) byte-for-byte intact.\n */\n\n/**\n * Suspicious/generic authority values that indicate a malformed or placeholder\n * scheme URL. These are host strings that will almost certainly cause the Toss\n * app to fail with \"bundle not found\" silently.\n *\n * The expected form from `ait deploy --scheme-only` is:\n * intoss-private://<appName>?_deploymentId=<uuid>\n * where `<appName>` is a non-generic string like `aitc-sdk-example`.\n */\nconst SUSPICIOUS_AUTHORITIES = new Set<string>(['', 'web', 'localhost', '127.0.0.1', 'app']);\n\n/**\n * Validates the authority (host) portion of a scheme URL.\n *\n * Returns a warning message if the authority is missing or looks like a\n * placeholder, or `null` if the authority looks valid.\n *\n * Expected form: `intoss-private://<appName>?_deploymentId=<uuid>`\n * The authority must be a non-empty, non-generic app name (e.g. `aitc-sdk-example`).\n */\nexport function validateSchemeAuthority(schemeUrl: string): string | null {\n // Extract authority from `scheme://authority[/path][?query][#hash]`.\n // We cannot use the WHATWG URL parser for non-special schemes reliably\n // (see the deeplink.ts module comment), so we parse the raw string.\n const afterScheme = schemeUrl.replace(/^[a-zA-Z][a-zA-Z0-9+\\-.]*:\\/\\//, '');\n if (afterScheme === schemeUrl) {\n // No `://` found — not a scheme URL at all.\n return (\n 'scheme_url does not look like a scheme URL (expected `intoss-private://<appName>?_deploymentId=<uuid>`). ' +\n 'Use the URL printed by `ait deploy --scheme-only`.'\n );\n }\n\n // authority ends at the first `/`, `?`, `#`, or end of string.\n const authorityEnd = afterScheme.search(/[/?#]/);\n const authority = authorityEnd === -1 ? afterScheme : afterScheme.slice(0, authorityEnd);\n\n if (SUSPICIOUS_AUTHORITIES.has(authority.toLowerCase())) {\n const displayAuthority = authority === '' ? '(empty)' : `\"${authority}\"`;\n return (\n `scheme_url authority ${displayAuthority} looks like a placeholder. ` +\n 'Expected an app name like `intoss-private://aitc-sdk-example?_deploymentId=<uuid>`. ' +\n 'Use the URL printed by `ait deploy --scheme-only` — it includes the correct app name as the host.'\n );\n }\n\n return null;\n}\n\n/** A param the helper appends. Existing occurrences are replaced, not duplicated. */\ntype AppendParam = readonly [key: string, value: string];\n\nfunction stripExisting(query: string, key: string): string {\n if (query === '') return '';\n return query\n .split('&')\n .filter((pair) => pair !== '' && pair.split('=')[0] !== key)\n .join('&');\n}\n\n/**\n * Splices `debug=1`, `relay=<wssUrl>`, and (optionally) `at=<totpCode>` into a\n * scheme URL's query string, preserving everything else (scheme, authority,\n * path, hash, and the existing `_deploymentId` param). If any of the spliced\n * params is already present it is replaced so the helper is idempotent.\n *\n * @param schemeUrl - The `intoss-private://…?_deploymentId=<uuid>` URL printed\n * by `ait deploy --scheme-only`. Must already carry `_deploymentId` (Layer B\n * of the gate); this helper does not invent one.\n * @param wssUrl - The live relay URL (`wss://…trycloudflare.com`) from the\n * running debug MCP server's quick tunnel.\n * @param totpCode - Optional current TOTP code (6 digits). When provided, it\n * is spliced as `at=<totpCode>`. Must be computed at call time — it rotates\n * every 30 s. Pass `undefined` or omit when TOTP is disabled.\n * @returns The same URL with `debug=1&relay=<encoded wssUrl>[&at=<totpCode>]`\n * appended.\n * @throws If `wssUrl` is not a `wss:` URL (the gate rejects anything else, so\n * producing such a link would be a silent dead end).\n */\nexport function buildDeepLinkAttachUrl(\n schemeUrl: string,\n wssUrl: string,\n totpCode?: string,\n): string {\n let relay: URL;\n try {\n relay = new URL(wssUrl);\n } catch {\n throw new Error(`relay URL is not a valid URL: ${wssUrl}`);\n }\n if (relay.protocol !== 'wss:') {\n throw new Error(`relay URL must use the wss: scheme, got ${relay.protocol} (${wssUrl})`);\n }\n\n const hashIndex = schemeUrl.indexOf('#');\n const hash = hashIndex === -1 ? '' : schemeUrl.slice(hashIndex);\n const beforeHash = hashIndex === -1 ? schemeUrl : schemeUrl.slice(0, hashIndex);\n\n const queryIndex = beforeHash.indexOf('?');\n const base = queryIndex === -1 ? beforeHash : beforeHash.slice(0, queryIndex);\n let query = queryIndex === -1 ? '' : beforeHash.slice(queryIndex + 1);\n\n const appended: AppendParam[] = [\n ['debug', '1'],\n ['relay', wssUrl],\n ];\n // Only splice `at=` when a code is provided (TOTP enabled). Omitting it when\n // TOTP is disabled preserves backward compatibility with gate deployments\n // that do not yet evaluate the `at` param.\n if (totpCode !== undefined && totpCode !== '') {\n appended.push(['at', totpCode]);\n }\n\n // Always strip the `at` key from the existing query so a stale code from a\n // previous run is removed even when the caller does not provide a fresh code.\n query = stripExisting(query, 'at');\n\n for (const [key] of appended) {\n query = stripExisting(query, key);\n }\n for (const [key, value] of appended) {\n const pair = `${key}=${encodeURIComponent(value)}`;\n query = query === '' ? pair : `${query}&${pair}`;\n }\n\n return `${base}?${query}${hash}`;\n}\n"],"mappings":";;;;;;;;AAOA,MAAM,eAAe;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAsDrB,SAAgB,uBACd,WACA,QACA,UACA,MACQ;CACR,IAAI,MACF,GAAG,aAAa,OAAO,mBAAmB,UAAU,CAAA,iBAClC,mBAAmB,OAAO;AAC9C,KAAI,aAAa,KAAA,KAAa,aAAa,GACzC,QAAO,OAAO,mBAAmB,SAAS;AAG5C,KAAI,MAAM,SAAS,KAAA,KAAa,KAAK,KAAK,MAAM,KAAK,GACnD,QAAO,SAAS,mBAAmB,KAAK,KAAK,MAAM,CAAC;AAEtD,KAAI,MAAM,SAAS,KAAA,GAAW;EAC5B,IAAI;AACJ,MAAI;AACF,gBAAa,IAAI,IAAI,KAAK,KAAK;UACzB;AACN,gBAAa;;AAEf,MAAI,YAAY,aAAa,SAC3B,QAAO,SAAS,mBAAmB,KAAK,KAAK;;AAGjD,QAAO"}
@@ -16,6 +16,11 @@ const LAUNCHER_URL = "https://devtools.aitc.dev/launcher/";
16
16
  * is injected. `&at=<totpCode>` is added only when a code is provided (same
17
17
  * conditional as {@link buildDeepLinkAttachUrl}).
18
18
  *
19
+ * When `opts.name` is given (non-blank), it is added as `&name=` so the
20
+ * launcher partner bar shows the app name instead of the generic default (#498).
21
+ * When `opts.icon` is an absolute https:// URL, it is added as `&icon=` so the
22
+ * launcher can render an icon next to the title (#498).
23
+ *
19
24
  * Unlike `buildDeepLinkAttachUrl` (which splices onto a non-special scheme URL
20
25
  * via raw string manipulation), this function uses WHATWG `encodeURIComponent`
21
26
  * because the target is a standard `https:` URL.
@@ -30,15 +35,26 @@ const LAUNCHER_URL = "https://devtools.aitc.dev/launcher/";
30
35
  * @param totpCode - Optional current TOTP code (6 digits). When provided, it
31
36
  * is appended as `at=<totpCode>`. Must be computed at call time — it rotates
32
37
  * every 30 s. Omit when TOTP is disabled.
38
+ * @param opts - Optional app identity hints: `name` and `icon` (#498).
33
39
  * @returns The launcher deep-link URL with `?url=<enc>&debug=1&relay=<enc>
34
- * [&at=<code>]` params.
40
+ * [&at=<code>][&name=<enc>][&icon=<enc>]` params.
35
41
  */
36
- function buildLauncherAttachUrl(tunnelUrl, wssUrl, totpCode) {
42
+ function buildLauncherAttachUrl(tunnelUrl, wssUrl, totpCode, opts) {
37
43
  let url = `${LAUNCHER_URL}?url=${encodeURIComponent(tunnelUrl)}&debug=1&relay=${encodeURIComponent(wssUrl)}`;
38
44
  if (totpCode !== void 0 && totpCode !== "") url += `&at=${encodeURIComponent(totpCode)}`;
45
+ if (opts?.name !== void 0 && opts.name.trim() !== "") url += `&name=${encodeURIComponent(opts.name.trim())}`;
46
+ if (opts?.icon !== void 0) {
47
+ let iconParsed;
48
+ try {
49
+ iconParsed = new URL(opts.icon);
50
+ } catch {
51
+ iconParsed = null;
52
+ }
53
+ if (iconParsed?.protocol === "https:") url += `&icon=${encodeURIComponent(opts.icon)}`;
54
+ }
39
55
  return url;
40
56
  }
41
57
  //#endregion
42
58
  exports.buildLauncherAttachUrl = buildLauncherAttachUrl;
43
59
 
44
- //# sourceMappingURL=deeplink-CCGiyoHq.cjs.map
60
+ //# sourceMappingURL=deeplink-CU6opogq.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"deeplink-CU6opogq.cjs","names":[],"sources":["../src/mcp/deeplink.ts"],"sourcesContent":["/**\n * URL of the AITC Sandbox launcher PWA.\n *\n * Declared here (not imported from `src/unplugin/tunnel.ts`) to respect the\n * mcp → unplugin layering boundary. unplugin/tunnel.ts declares its own copy\n * for the same reason — keep the two in sync when the URL changes.\n */\nconst LAUNCHER_URL = 'https://devtools.aitc.dev/launcher/';\n\n/**\n * Optional metadata that enriches the launcher deep-link (#498).\n *\n * These fields are added as query params so the launcher PWA can display\n * a recognizable identity (name, icon) without the user having to configure\n * anything extra.\n */\nexport interface LauncherAttachUrlOpts {\n /**\n * Human-readable app name shown in the partner nav bar (`name=` param).\n * Blank / whitespace-only values are not added.\n */\n name?: string;\n /**\n * Absolute `https://` icon URL for the partner nav bar icon slot (`icon=`\n * param). Non-https or falsy values are not added.\n */\n icon?: string;\n}\n\n/**\n * Builds a launcher PWA deep-link for env-2 MCP-attach (issue #378).\n *\n * The launcher at {@link LAUNCHER_URL} renders tunnelUrl in a full-viewport\n * iframe. `&debug=1&relay=<wssUrl>` is forwarded onto the iframe src so the\n * framed page's in-app debug gate (Layer C) is satisfied and a Chii target.js\n * is injected. `&at=<totpCode>` is added only when a code is provided (same\n * conditional as {@link buildDeepLinkAttachUrl}).\n *\n * When `opts.name` is given (non-blank), it is added as `&name=` so the\n * launcher partner bar shows the app name instead of the generic default (#498).\n * When `opts.icon` is an absolute https:// URL, it is added as `&icon=` so the\n * launcher can render an icon next to the title (#498).\n *\n * Unlike `buildDeepLinkAttachUrl` (which splices onto a non-special scheme URL\n * via raw string manipulation), this function uses WHATWG `encodeURIComponent`\n * because the target is a standard `https:` URL.\n *\n * SECRET-HANDLING: `totpCode` (when provided) is placed into the `at=` param\n * only — never logged or returned separately. Callers must NOT log the result\n * of this function to stdout/stderr.\n *\n * @param tunnelUrl - The `https://*.trycloudflare.com` app tunnel URL\n * (`AIT_TUNNEL_BASE_URL`). This is the URL the launcher frames.\n * @param wssUrl - The `wss://` relay URL the framed page will attach to.\n * @param totpCode - Optional current TOTP code (6 digits). When provided, it\n * is appended as `at=<totpCode>`. Must be computed at call time — it rotates\n * every 30 s. Omit when TOTP is disabled.\n * @param opts - Optional app identity hints: `name` and `icon` (#498).\n * @returns The launcher deep-link URL with `?url=<enc>&debug=1&relay=<enc>\n * [&at=<code>][&name=<enc>][&icon=<enc>]` params.\n */\nexport function buildLauncherAttachUrl(\n tunnelUrl: string,\n wssUrl: string,\n totpCode?: string,\n opts?: LauncherAttachUrlOpts,\n): string {\n let url =\n `${LAUNCHER_URL}?url=${encodeURIComponent(tunnelUrl)}` +\n `&debug=1&relay=${encodeURIComponent(wssUrl)}`;\n if (totpCode !== undefined && totpCode !== '') {\n url += `&at=${encodeURIComponent(totpCode)}`;\n }\n // App identity hints (#498): add non-blank name and valid https icon.\n if (opts?.name !== undefined && opts.name.trim() !== '') {\n url += `&name=${encodeURIComponent(opts.name.trim())}`;\n }\n if (opts?.icon !== undefined) {\n let iconParsed: URL;\n try {\n iconParsed = new URL(opts.icon);\n } catch {\n iconParsed = null as unknown as URL;\n }\n if (iconParsed?.protocol === 'https:') {\n url += `&icon=${encodeURIComponent(opts.icon)}`;\n }\n }\n return url;\n}\n\n/**\n * Build a self-attaching dogfood deep link.\n *\n * `ait deploy --scheme-only` prints an `intoss-private://…?_deploymentId=<uuid>`\n * URL that opens a dogfood bundle on a phone. The in-app debug gate\n * (`src/in-app/gate.ts`) auto-attaches when the entry URL also carries\n * `debug=1` and `relay=<wss-url>`. This helper splices those params (plus\n * `at=<code>` when TOTP is enabled) into the scheme URL; rendering the result\n * as a QR code and scanning it with the phone camera opens the mini-app and\n * attaches it to the live Chii relay. QR is the single entry path — it needs\n * no USB cable, platform CLI, or driver, and works the same on iOS/Android.\n *\n * The Toss app propagates extra query params from the entry deep link into the\n * mini-app WebView's `location.search` (confirmed behavior), so the gate reads\n * them at attach time.\n *\n * TOTP `at=` param:\n * When a TOTP secret is active, `buildDeepLinkAttachUrl` accepts an optional\n * `totpCode` argument and splices `at=<code>` alongside `debug` and `relay`.\n * The code must be computed by the caller at call time — do NOT pre-compute\n * and cache it, because the 30-second window expires quickly. The in-app gate\n * (`src/in-app/gate.ts` Layer C) validates this code against the baked secret.\n *\n * Why not `URL`/`URLSearchParams`: `intoss-private:` is a non-special scheme.\n * The WHATWG `URL` parser treats such schemes opaquely (no host/path/query\n * decomposition you can rely on across runtimes), so query manipulation via\n * `url.searchParams` is not portable here. We splice the query string directly\n * on the raw string instead, which keeps the scheme, authority, path, and any\n * pre-existing params (notably `_deploymentId`) byte-for-byte intact.\n */\n\n/**\n * Suspicious/generic authority values that indicate a malformed or placeholder\n * scheme URL. These are host strings that will almost certainly cause the Toss\n * app to fail with \"bundle not found\" silently.\n *\n * The expected form from `ait deploy --scheme-only` is:\n * intoss-private://<appName>?_deploymentId=<uuid>\n * where `<appName>` is a non-generic string like `aitc-sdk-example`.\n */\nconst SUSPICIOUS_AUTHORITIES = new Set<string>(['', 'web', 'localhost', '127.0.0.1', 'app']);\n\n/**\n * Validates the authority (host) portion of a scheme URL.\n *\n * Returns a warning message if the authority is missing or looks like a\n * placeholder, or `null` if the authority looks valid.\n *\n * Expected form: `intoss-private://<appName>?_deploymentId=<uuid>`\n * The authority must be a non-empty, non-generic app name (e.g. `aitc-sdk-example`).\n */\nexport function validateSchemeAuthority(schemeUrl: string): string | null {\n // Extract authority from `scheme://authority[/path][?query][#hash]`.\n // We cannot use the WHATWG URL parser for non-special schemes reliably\n // (see the deeplink.ts module comment), so we parse the raw string.\n const afterScheme = schemeUrl.replace(/^[a-zA-Z][a-zA-Z0-9+\\-.]*:\\/\\//, '');\n if (afterScheme === schemeUrl) {\n // No `://` found — not a scheme URL at all.\n return (\n 'scheme_url does not look like a scheme URL (expected `intoss-private://<appName>?_deploymentId=<uuid>`). ' +\n 'Use the URL printed by `ait deploy --scheme-only`.'\n );\n }\n\n // authority ends at the first `/`, `?`, `#`, or end of string.\n const authorityEnd = afterScheme.search(/[/?#]/);\n const authority = authorityEnd === -1 ? afterScheme : afterScheme.slice(0, authorityEnd);\n\n if (SUSPICIOUS_AUTHORITIES.has(authority.toLowerCase())) {\n const displayAuthority = authority === '' ? '(empty)' : `\"${authority}\"`;\n return (\n `scheme_url authority ${displayAuthority} looks like a placeholder. ` +\n 'Expected an app name like `intoss-private://aitc-sdk-example?_deploymentId=<uuid>`. ' +\n 'Use the URL printed by `ait deploy --scheme-only` — it includes the correct app name as the host.'\n );\n }\n\n return null;\n}\n\n/** A param the helper appends. Existing occurrences are replaced, not duplicated. */\ntype AppendParam = readonly [key: string, value: string];\n\nfunction stripExisting(query: string, key: string): string {\n if (query === '') return '';\n return query\n .split('&')\n .filter((pair) => pair !== '' && pair.split('=')[0] !== key)\n .join('&');\n}\n\n/**\n * Splices `debug=1`, `relay=<wssUrl>`, and (optionally) `at=<totpCode>` into a\n * scheme URL's query string, preserving everything else (scheme, authority,\n * path, hash, and the existing `_deploymentId` param). If any of the spliced\n * params is already present it is replaced so the helper is idempotent.\n *\n * @param schemeUrl - The `intoss-private://…?_deploymentId=<uuid>` URL printed\n * by `ait deploy --scheme-only`. Must already carry `_deploymentId` (Layer B\n * of the gate); this helper does not invent one.\n * @param wssUrl - The live relay URL (`wss://…trycloudflare.com`) from the\n * running debug MCP server's quick tunnel.\n * @param totpCode - Optional current TOTP code (6 digits). When provided, it\n * is spliced as `at=<totpCode>`. Must be computed at call time — it rotates\n * every 30 s. Pass `undefined` or omit when TOTP is disabled.\n * @returns The same URL with `debug=1&relay=<encoded wssUrl>[&at=<totpCode>]`\n * appended.\n * @throws If `wssUrl` is not a `wss:` URL (the gate rejects anything else, so\n * producing such a link would be a silent dead end).\n */\nexport function buildDeepLinkAttachUrl(\n schemeUrl: string,\n wssUrl: string,\n totpCode?: string,\n): string {\n let relay: URL;\n try {\n relay = new URL(wssUrl);\n } catch {\n throw new Error(`relay URL is not a valid URL: ${wssUrl}`);\n }\n if (relay.protocol !== 'wss:') {\n throw new Error(`relay URL must use the wss: scheme, got ${relay.protocol} (${wssUrl})`);\n }\n\n const hashIndex = schemeUrl.indexOf('#');\n const hash = hashIndex === -1 ? '' : schemeUrl.slice(hashIndex);\n const beforeHash = hashIndex === -1 ? schemeUrl : schemeUrl.slice(0, hashIndex);\n\n const queryIndex = beforeHash.indexOf('?');\n const base = queryIndex === -1 ? beforeHash : beforeHash.slice(0, queryIndex);\n let query = queryIndex === -1 ? '' : beforeHash.slice(queryIndex + 1);\n\n const appended: AppendParam[] = [\n ['debug', '1'],\n ['relay', wssUrl],\n ];\n // Only splice `at=` when a code is provided (TOTP enabled). Omitting it when\n // TOTP is disabled preserves backward compatibility with gate deployments\n // that do not yet evaluate the `at` param.\n if (totpCode !== undefined && totpCode !== '') {\n appended.push(['at', totpCode]);\n }\n\n // Always strip the `at` key from the existing query so a stale code from a\n // previous run is removed even when the caller does not provide a fresh code.\n query = stripExisting(query, 'at');\n\n for (const [key] of appended) {\n query = stripExisting(query, key);\n }\n for (const [key, value] of appended) {\n const pair = `${key}=${encodeURIComponent(value)}`;\n query = query === '' ? pair : `${query}&${pair}`;\n }\n\n return `${base}?${query}${hash}`;\n}\n"],"mappings":";;;;;;;;AAOA,MAAM,eAAe;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAsDrB,SAAgB,uBACd,WACA,QACA,UACA,MACQ;CACR,IAAI,MACF,GAAG,aAAa,OAAO,mBAAmB,UAAU,CAAA,iBAClC,mBAAmB,OAAO;AAC9C,KAAI,aAAa,KAAA,KAAa,aAAa,GACzC,QAAO,OAAO,mBAAmB,SAAS;AAG5C,KAAI,MAAM,SAAS,KAAA,KAAa,KAAK,KAAK,MAAM,KAAK,GACnD,QAAO,SAAS,mBAAmB,KAAK,KAAK,MAAM,CAAC;AAEtD,KAAI,MAAM,SAAS,KAAA,GAAW;EAC5B,IAAI;AACJ,MAAI;AACF,gBAAa,IAAI,IAAI,KAAK,KAAK;UACzB;AACN,gBAAa;;AAEf,MAAI,YAAY,aAAa,SAC3B,QAAO,SAAS,mBAAmB,KAAK,KAAK;;AAGjD,QAAO"}
@@ -16,6 +16,11 @@ const LAUNCHER_URL = "https://devtools.aitc.dev/launcher/";
16
16
  * is injected. `&at=<totpCode>` is added only when a code is provided (same
17
17
  * conditional as {@link buildDeepLinkAttachUrl}).
18
18
  *
19
+ * When `opts.name` is given (non-blank), it is added as `&name=` so the
20
+ * launcher partner bar shows the app name instead of the generic default (#498).
21
+ * When `opts.icon` is an absolute https:// URL, it is added as `&icon=` so the
22
+ * launcher can render an icon next to the title (#498).
23
+ *
19
24
  * Unlike `buildDeepLinkAttachUrl` (which splices onto a non-special scheme URL
20
25
  * via raw string manipulation), this function uses WHATWG `encodeURIComponent`
21
26
  * because the target is a standard `https:` URL.
@@ -30,15 +35,26 @@ const LAUNCHER_URL = "https://devtools.aitc.dev/launcher/";
30
35
  * @param totpCode - Optional current TOTP code (6 digits). When provided, it
31
36
  * is appended as `at=<totpCode>`. Must be computed at call time — it rotates
32
37
  * every 30 s. Omit when TOTP is disabled.
38
+ * @param opts - Optional app identity hints: `name` and `icon` (#498).
33
39
  * @returns The launcher deep-link URL with `?url=<enc>&debug=1&relay=<enc>
34
- * [&at=<code>]` params.
40
+ * [&at=<code>][&name=<enc>][&icon=<enc>]` params.
35
41
  */
36
- function buildLauncherAttachUrl(tunnelUrl, wssUrl, totpCode) {
42
+ function buildLauncherAttachUrl(tunnelUrl, wssUrl, totpCode, opts) {
37
43
  let url = `${LAUNCHER_URL}?url=${encodeURIComponent(tunnelUrl)}&debug=1&relay=${encodeURIComponent(wssUrl)}`;
38
44
  if (totpCode !== void 0 && totpCode !== "") url += `&at=${encodeURIComponent(totpCode)}`;
45
+ if (opts?.name !== void 0 && opts.name.trim() !== "") url += `&name=${encodeURIComponent(opts.name.trim())}`;
46
+ if (opts?.icon !== void 0) {
47
+ let iconParsed;
48
+ try {
49
+ iconParsed = new URL(opts.icon);
50
+ } catch {
51
+ iconParsed = null;
52
+ }
53
+ if (iconParsed?.protocol === "https:") url += `&icon=${encodeURIComponent(opts.icon)}`;
54
+ }
39
55
  return url;
40
56
  }
41
57
  //#endregion
42
58
  export { buildLauncherAttachUrl };
43
59
 
44
- //# sourceMappingURL=deeplink-Cqli4qzm.js.map
60
+ //# sourceMappingURL=deeplink-CYqDwVYs.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"deeplink-CYqDwVYs.js","names":[],"sources":["../src/mcp/deeplink.ts"],"sourcesContent":["/**\n * URL of the AITC Sandbox launcher PWA.\n *\n * Declared here (not imported from `src/unplugin/tunnel.ts`) to respect the\n * mcp → unplugin layering boundary. unplugin/tunnel.ts declares its own copy\n * for the same reason — keep the two in sync when the URL changes.\n */\nconst LAUNCHER_URL = 'https://devtools.aitc.dev/launcher/';\n\n/**\n * Optional metadata that enriches the launcher deep-link (#498).\n *\n * These fields are added as query params so the launcher PWA can display\n * a recognizable identity (name, icon) without the user having to configure\n * anything extra.\n */\nexport interface LauncherAttachUrlOpts {\n /**\n * Human-readable app name shown in the partner nav bar (`name=` param).\n * Blank / whitespace-only values are not added.\n */\n name?: string;\n /**\n * Absolute `https://` icon URL for the partner nav bar icon slot (`icon=`\n * param). Non-https or falsy values are not added.\n */\n icon?: string;\n}\n\n/**\n * Builds a launcher PWA deep-link for env-2 MCP-attach (issue #378).\n *\n * The launcher at {@link LAUNCHER_URL} renders tunnelUrl in a full-viewport\n * iframe. `&debug=1&relay=<wssUrl>` is forwarded onto the iframe src so the\n * framed page's in-app debug gate (Layer C) is satisfied and a Chii target.js\n * is injected. `&at=<totpCode>` is added only when a code is provided (same\n * conditional as {@link buildDeepLinkAttachUrl}).\n *\n * When `opts.name` is given (non-blank), it is added as `&name=` so the\n * launcher partner bar shows the app name instead of the generic default (#498).\n * When `opts.icon` is an absolute https:// URL, it is added as `&icon=` so the\n * launcher can render an icon next to the title (#498).\n *\n * Unlike `buildDeepLinkAttachUrl` (which splices onto a non-special scheme URL\n * via raw string manipulation), this function uses WHATWG `encodeURIComponent`\n * because the target is a standard `https:` URL.\n *\n * SECRET-HANDLING: `totpCode` (when provided) is placed into the `at=` param\n * only — never logged or returned separately. Callers must NOT log the result\n * of this function to stdout/stderr.\n *\n * @param tunnelUrl - The `https://*.trycloudflare.com` app tunnel URL\n * (`AIT_TUNNEL_BASE_URL`). This is the URL the launcher frames.\n * @param wssUrl - The `wss://` relay URL the framed page will attach to.\n * @param totpCode - Optional current TOTP code (6 digits). When provided, it\n * is appended as `at=<totpCode>`. Must be computed at call time — it rotates\n * every 30 s. Omit when TOTP is disabled.\n * @param opts - Optional app identity hints: `name` and `icon` (#498).\n * @returns The launcher deep-link URL with `?url=<enc>&debug=1&relay=<enc>\n * [&at=<code>][&name=<enc>][&icon=<enc>]` params.\n */\nexport function buildLauncherAttachUrl(\n tunnelUrl: string,\n wssUrl: string,\n totpCode?: string,\n opts?: LauncherAttachUrlOpts,\n): string {\n let url =\n `${LAUNCHER_URL}?url=${encodeURIComponent(tunnelUrl)}` +\n `&debug=1&relay=${encodeURIComponent(wssUrl)}`;\n if (totpCode !== undefined && totpCode !== '') {\n url += `&at=${encodeURIComponent(totpCode)}`;\n }\n // App identity hints (#498): add non-blank name and valid https icon.\n if (opts?.name !== undefined && opts.name.trim() !== '') {\n url += `&name=${encodeURIComponent(opts.name.trim())}`;\n }\n if (opts?.icon !== undefined) {\n let iconParsed: URL;\n try {\n iconParsed = new URL(opts.icon);\n } catch {\n iconParsed = null as unknown as URL;\n }\n if (iconParsed?.protocol === 'https:') {\n url += `&icon=${encodeURIComponent(opts.icon)}`;\n }\n }\n return url;\n}\n\n/**\n * Build a self-attaching dogfood deep link.\n *\n * `ait deploy --scheme-only` prints an `intoss-private://…?_deploymentId=<uuid>`\n * URL that opens a dogfood bundle on a phone. The in-app debug gate\n * (`src/in-app/gate.ts`) auto-attaches when the entry URL also carries\n * `debug=1` and `relay=<wss-url>`. This helper splices those params (plus\n * `at=<code>` when TOTP is enabled) into the scheme URL; rendering the result\n * as a QR code and scanning it with the phone camera opens the mini-app and\n * attaches it to the live Chii relay. QR is the single entry path — it needs\n * no USB cable, platform CLI, or driver, and works the same on iOS/Android.\n *\n * The Toss app propagates extra query params from the entry deep link into the\n * mini-app WebView's `location.search` (confirmed behavior), so the gate reads\n * them at attach time.\n *\n * TOTP `at=` param:\n * When a TOTP secret is active, `buildDeepLinkAttachUrl` accepts an optional\n * `totpCode` argument and splices `at=<code>` alongside `debug` and `relay`.\n * The code must be computed by the caller at call time — do NOT pre-compute\n * and cache it, because the 30-second window expires quickly. The in-app gate\n * (`src/in-app/gate.ts` Layer C) validates this code against the baked secret.\n *\n * Why not `URL`/`URLSearchParams`: `intoss-private:` is a non-special scheme.\n * The WHATWG `URL` parser treats such schemes opaquely (no host/path/query\n * decomposition you can rely on across runtimes), so query manipulation via\n * `url.searchParams` is not portable here. We splice the query string directly\n * on the raw string instead, which keeps the scheme, authority, path, and any\n * pre-existing params (notably `_deploymentId`) byte-for-byte intact.\n */\n\n/**\n * Suspicious/generic authority values that indicate a malformed or placeholder\n * scheme URL. These are host strings that will almost certainly cause the Toss\n * app to fail with \"bundle not found\" silently.\n *\n * The expected form from `ait deploy --scheme-only` is:\n * intoss-private://<appName>?_deploymentId=<uuid>\n * where `<appName>` is a non-generic string like `aitc-sdk-example`.\n */\nconst SUSPICIOUS_AUTHORITIES = new Set<string>(['', 'web', 'localhost', '127.0.0.1', 'app']);\n\n/**\n * Validates the authority (host) portion of a scheme URL.\n *\n * Returns a warning message if the authority is missing or looks like a\n * placeholder, or `null` if the authority looks valid.\n *\n * Expected form: `intoss-private://<appName>?_deploymentId=<uuid>`\n * The authority must be a non-empty, non-generic app name (e.g. `aitc-sdk-example`).\n */\nexport function validateSchemeAuthority(schemeUrl: string): string | null {\n // Extract authority from `scheme://authority[/path][?query][#hash]`.\n // We cannot use the WHATWG URL parser for non-special schemes reliably\n // (see the deeplink.ts module comment), so we parse the raw string.\n const afterScheme = schemeUrl.replace(/^[a-zA-Z][a-zA-Z0-9+\\-.]*:\\/\\//, '');\n if (afterScheme === schemeUrl) {\n // No `://` found — not a scheme URL at all.\n return (\n 'scheme_url does not look like a scheme URL (expected `intoss-private://<appName>?_deploymentId=<uuid>`). ' +\n 'Use the URL printed by `ait deploy --scheme-only`.'\n );\n }\n\n // authority ends at the first `/`, `?`, `#`, or end of string.\n const authorityEnd = afterScheme.search(/[/?#]/);\n const authority = authorityEnd === -1 ? afterScheme : afterScheme.slice(0, authorityEnd);\n\n if (SUSPICIOUS_AUTHORITIES.has(authority.toLowerCase())) {\n const displayAuthority = authority === '' ? '(empty)' : `\"${authority}\"`;\n return (\n `scheme_url authority ${displayAuthority} looks like a placeholder. ` +\n 'Expected an app name like `intoss-private://aitc-sdk-example?_deploymentId=<uuid>`. ' +\n 'Use the URL printed by `ait deploy --scheme-only` — it includes the correct app name as the host.'\n );\n }\n\n return null;\n}\n\n/** A param the helper appends. Existing occurrences are replaced, not duplicated. */\ntype AppendParam = readonly [key: string, value: string];\n\nfunction stripExisting(query: string, key: string): string {\n if (query === '') return '';\n return query\n .split('&')\n .filter((pair) => pair !== '' && pair.split('=')[0] !== key)\n .join('&');\n}\n\n/**\n * Splices `debug=1`, `relay=<wssUrl>`, and (optionally) `at=<totpCode>` into a\n * scheme URL's query string, preserving everything else (scheme, authority,\n * path, hash, and the existing `_deploymentId` param). If any of the spliced\n * params is already present it is replaced so the helper is idempotent.\n *\n * @param schemeUrl - The `intoss-private://…?_deploymentId=<uuid>` URL printed\n * by `ait deploy --scheme-only`. Must already carry `_deploymentId` (Layer B\n * of the gate); this helper does not invent one.\n * @param wssUrl - The live relay URL (`wss://…trycloudflare.com`) from the\n * running debug MCP server's quick tunnel.\n * @param totpCode - Optional current TOTP code (6 digits). When provided, it\n * is spliced as `at=<totpCode>`. Must be computed at call time — it rotates\n * every 30 s. Pass `undefined` or omit when TOTP is disabled.\n * @returns The same URL with `debug=1&relay=<encoded wssUrl>[&at=<totpCode>]`\n * appended.\n * @throws If `wssUrl` is not a `wss:` URL (the gate rejects anything else, so\n * producing such a link would be a silent dead end).\n */\nexport function buildDeepLinkAttachUrl(\n schemeUrl: string,\n wssUrl: string,\n totpCode?: string,\n): string {\n let relay: URL;\n try {\n relay = new URL(wssUrl);\n } catch {\n throw new Error(`relay URL is not a valid URL: ${wssUrl}`);\n }\n if (relay.protocol !== 'wss:') {\n throw new Error(`relay URL must use the wss: scheme, got ${relay.protocol} (${wssUrl})`);\n }\n\n const hashIndex = schemeUrl.indexOf('#');\n const hash = hashIndex === -1 ? '' : schemeUrl.slice(hashIndex);\n const beforeHash = hashIndex === -1 ? schemeUrl : schemeUrl.slice(0, hashIndex);\n\n const queryIndex = beforeHash.indexOf('?');\n const base = queryIndex === -1 ? beforeHash : beforeHash.slice(0, queryIndex);\n let query = queryIndex === -1 ? '' : beforeHash.slice(queryIndex + 1);\n\n const appended: AppendParam[] = [\n ['debug', '1'],\n ['relay', wssUrl],\n ];\n // Only splice `at=` when a code is provided (TOTP enabled). Omitting it when\n // TOTP is disabled preserves backward compatibility with gate deployments\n // that do not yet evaluate the `at` param.\n if (totpCode !== undefined && totpCode !== '') {\n appended.push(['at', totpCode]);\n }\n\n // Always strip the `at` key from the existing query so a stale code from a\n // previous run is removed even when the caller does not provide a fresh code.\n query = stripExisting(query, 'at');\n\n for (const [key] of appended) {\n query = stripExisting(query, key);\n }\n for (const [key, value] of appended) {\n const pair = `${key}=${encodeURIComponent(value)}`;\n query = query === '' ? pair : `${query}&${pair}`;\n }\n\n return `${base}?${query}${hash}`;\n}\n"],"mappings":";;;;;;;;AAOA,MAAM,eAAe;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAsDrB,SAAgB,uBACd,WACA,QACA,UACA,MACQ;CACR,IAAI,MACF,GAAG,aAAa,OAAO,mBAAmB,UAAU,CAAA,iBAClC,mBAAmB,OAAO;AAC9C,KAAI,aAAa,KAAA,KAAa,aAAa,GACzC,QAAO,OAAO,mBAAmB,SAAS;AAG5C,KAAI,MAAM,SAAS,KAAA,KAAa,KAAK,KAAK,MAAM,KAAK,GACnD,QAAO,SAAS,mBAAmB,KAAK,KAAK,MAAM,CAAC;AAEtD,KAAI,MAAM,SAAS,KAAA,GAAW;EAC5B,IAAI;AACJ,MAAI;AACF,gBAAa,IAAI,IAAI,KAAK,KAAK;UACzB;AACN,gBAAa;;AAEf,MAAI,YAAY,aAAa,SAC3B,QAAO,SAAS,mBAAmB,KAAK,KAAK;;AAGjD,QAAO"}
package/dist/mcp/cli.js CHANGED
@@ -1012,6 +1012,11 @@ const LAUNCHER_URL = "https://devtools.aitc.dev/launcher/";
1012
1012
  * is injected. `&at=<totpCode>` is added only when a code is provided (same
1013
1013
  * conditional as {@link buildDeepLinkAttachUrl}).
1014
1014
  *
1015
+ * When `opts.name` is given (non-blank), it is added as `&name=` so the
1016
+ * launcher partner bar shows the app name instead of the generic default (#498).
1017
+ * When `opts.icon` is an absolute https:// URL, it is added as `&icon=` so the
1018
+ * launcher can render an icon next to the title (#498).
1019
+ *
1015
1020
  * Unlike `buildDeepLinkAttachUrl` (which splices onto a non-special scheme URL
1016
1021
  * via raw string manipulation), this function uses WHATWG `encodeURIComponent`
1017
1022
  * because the target is a standard `https:` URL.
@@ -1026,12 +1031,23 @@ const LAUNCHER_URL = "https://devtools.aitc.dev/launcher/";
1026
1031
  * @param totpCode - Optional current TOTP code (6 digits). When provided, it
1027
1032
  * is appended as `at=<totpCode>`. Must be computed at call time — it rotates
1028
1033
  * every 30 s. Omit when TOTP is disabled.
1034
+ * @param opts - Optional app identity hints: `name` and `icon` (#498).
1029
1035
  * @returns The launcher deep-link URL with `?url=<enc>&debug=1&relay=<enc>
1030
- * [&at=<code>]` params.
1036
+ * [&at=<code>][&name=<enc>][&icon=<enc>]` params.
1031
1037
  */
1032
- function buildLauncherAttachUrl(tunnelUrl, wssUrl, totpCode) {
1038
+ function buildLauncherAttachUrl(tunnelUrl, wssUrl, totpCode, opts) {
1033
1039
  let url = `${LAUNCHER_URL}?url=${encodeURIComponent(tunnelUrl)}&debug=1&relay=${encodeURIComponent(wssUrl)}`;
1034
1040
  if (totpCode !== void 0 && totpCode !== "") url += `&at=${encodeURIComponent(totpCode)}`;
1041
+ if (opts?.name !== void 0 && opts.name.trim() !== "") url += `&name=${encodeURIComponent(opts.name.trim())}`;
1042
+ if (opts?.icon !== void 0) {
1043
+ let iconParsed;
1044
+ try {
1045
+ iconParsed = new URL(opts.icon);
1046
+ } catch {
1047
+ iconParsed = null;
1048
+ }
1049
+ if (iconParsed?.protocol === "https:") url += `&icon=${encodeURIComponent(opts.icon)}`;
1050
+ }
1035
1051
  return url;
1036
1052
  }
1037
1053
  /**
@@ -2110,7 +2126,6 @@ const en = {
2110
2126
  "launcher.urlPlaceholder": "https://example.trycloudflare.com",
2111
2127
  "launcher.openBtn": "Open",
2112
2128
  "launcher.scanBtn": "Scan QR with camera",
2113
- "launcher.rescanBtn": "Rescan",
2114
2129
  "launcher.noCamera": "No camera available — paste the URL instead.",
2115
2130
  "launcher.cameraError": "Could not access the camera — paste the URL instead.",
2116
2131
  "launcher.invalidUrlHttps": "Enter a valid https:// URL (the tunnel URL from your terminal).",
@@ -2119,11 +2134,16 @@ const en = {
2119
2134
  "launcher.debugAuthFailedHint": "The QR code may have expired. Scan a fresh QR code.",
2120
2135
  "launcher.debugAuthExpiredHint": "The debug session has expired. Scan a fresh QR from the attach page on your Mac.",
2121
2136
  "launcher.debugAuthRescanCta": "Scan a new QR",
2122
- "launcher.diagFab": "Diag",
2123
2137
  "launcher.diagTitle": "Viewport diagnostics",
2124
2138
  "launcher.diagYes": "yes",
2125
2139
  "launcher.diagNo": "no",
2126
- "launcher.letterboxDetected": "Display area is {pt}pt short — likely an iOS standalone letterbox. Removing and re-adding the launcher to the home screen may fix it."
2140
+ "launcher.letterboxDetected": "Display area is {pt}pt short — likely an iOS standalone letterbox. Removing and re-adding the launcher to the home screen may fix it.",
2141
+ "launcher.navbar.defaultTitle": "Mini App",
2142
+ "launcher.navbar.menu": "Menu",
2143
+ "launcher.navbar.close": "Close",
2144
+ "launcher.navbar.menuRescan": "Rescan",
2145
+ "launcher.navbar.menuDiag": "Viewport diagnostics",
2146
+ "launcher.navbar.menuLanguage": "Language"
2127
2147
  };
2128
2148
  //#endregion
2129
2149
  //#region src/i18n/index.ts
@@ -2354,7 +2374,6 @@ const tables = {
2354
2374
  "launcher.urlPlaceholder": "https://example.trycloudflare.com",
2355
2375
  "launcher.openBtn": "Open",
2356
2376
  "launcher.scanBtn": "QR 카메라로 스캔",
2357
- "launcher.rescanBtn": "Rescan",
2358
2377
  "launcher.noCamera": "카메라를 사용할 수 없습니다 — URL을 직접 붙여넣으세요.",
2359
2378
  "launcher.cameraError": "카메라에 접근할 수 없습니다 — URL을 직접 붙여넣으세요.",
2360
2379
  "launcher.invalidUrlHttps": "올바른 https:// URL을 입력하세요 (터미널의 터널 URL).",
@@ -2363,11 +2382,16 @@ const tables = {
2363
2382
  "launcher.debugAuthFailedHint": "QR 코드가 만료되었을 수 있어요. 새 QR을 다시 스캔하세요.",
2364
2383
  "launcher.debugAuthExpiredHint": "디버그 세션이 만료됐어요. Mac의 attach 페이지에서 새 QR을 스캔하세요.",
2365
2384
  "launcher.debugAuthRescanCta": "새 QR 스캔하기",
2366
- "launcher.diagFab": "진단",
2367
2385
  "launcher.diagTitle": "뷰포트 진단",
2368
2386
  "launcher.diagYes": "예",
2369
2387
  "launcher.diagNo": "아니요",
2370
- "launcher.letterboxDetected": "표시 영역이 {pt}pt 부족합니다 — iOS standalone letterbox로 보입니다. 런처를 홈 화면에서 제거 후 다시 설치하면 해소될 수 있어요."
2388
+ "launcher.letterboxDetected": "표시 영역이 {pt}pt 부족합니다 — iOS standalone letterbox로 보입니다. 런처를 홈 화면에서 제거 후 다시 설치하면 해소될 수 있어요.",
2389
+ "launcher.navbar.defaultTitle": "미니앱",
2390
+ "launcher.navbar.menu": "메뉴",
2391
+ "launcher.navbar.close": "닫기",
2392
+ "launcher.navbar.menuRescan": "다시 스캔",
2393
+ "launcher.navbar.menuDiag": "뷰포트 진단",
2394
+ "launcher.navbar.menuLanguage": "언어"
2371
2395
  },
2372
2396
  en
2373
2397
  };
@@ -3552,7 +3576,7 @@ const DEBUG_TOOL_DEFINITIONS = [
3552
3576
  },
3553
3577
  {
3554
3578
  name: "build_attach_url",
3555
- description: "The tool result already shows the QR to the user directly (Claude Code renders MCP tool output to the user's screen; they press Ctrl+O to expand if it's collapsed). Do NOT re-print or re-render the QR in your reply — that just wastes output tokens. Simply tell the user to scan the QR shown in this tool's output with their phone camera. Builds a self-attaching deep link for the active relay environment and returns a QR code. Scan the QR with the phone camera to open the mini-app and attach it to this debug session (QR is the single entry path — no USB cable or platform CLI needed). Call list_pages first to confirm the relay/tunnel is up. If the tunnel is not up, restart: `npx @ait-co/devtools devtools-mcp`.\n\nEnvironment-specific behaviour:\n • env 3 / relay-staging (start_debug mode=\"relay-staging\"): requires scheme_url — the intoss-private://…?_deploymentId=<uuid> URL from `ait deploy --scheme-only`. Splices debug=1 + relay URL into the scheme URL to produce a self-attach deep link.\n • env 2 / relay-sandbox (start_debug mode=\"relay-sandbox\"): scheme_url is NOT used. Instead, reads AIT_TUNNEL_BASE_URL (the https://*.trycloudflare.com app tunnel from `tunnel:{cdp:true}`) and builds a launcher PWA deep-link (https://devtools.aitc.dev/launcher/?url=…&debug=1&relay=…). Scan the QR with the phone to open the launcher, which frames the tunnel URL and attaches CDP.\n\nSet wait_for_attach=true to block until a page attaches (polls up to 30 s). On timeout, call build_attach_url again to resume polling. When open_in_browser=true (default), saves the QR as a PNG and opens it in the OS default browser — only works when the MCP server runs on a local GUI machine (not headless/remote containers). \n\nTOTP auth: when AIT_DEBUG_TOTP_SECRET is set on the MCP server, the returned attachUrl automatically includes the current one-time code (at=<code>). The code is valid for ~3 minutes (the relay gate accepts ±6 TOTP steps = 180–210 s of backwards acceptance). The response includes a `totp` field with `expiresAt` (ISO timestamp, ~3 min from issuance). If the phone scan happens after expiresAt, the relay will reject the code — just call build_attach_url again to get a fresh URL. Without AIT_DEBUG_TOTP_SECRET, the attachUrl has no expiry.",
3579
+ description: "The tool result already shows the QR to the user directly (Claude Code renders MCP tool output to the user's screen; they press Ctrl+O to expand if it's collapsed). Do NOT re-print or re-render the QR in your reply — that just wastes output tokens. Simply tell the user to scan the QR shown in this tool's output with their phone camera. Builds a self-attaching deep link for the active relay environment and returns a QR code. Scan the QR with the phone camera to open the mini-app and attach it to this debug session (QR is the single entry path — no USB cable or platform CLI needed). Call list_pages first to confirm the relay/tunnel is up. If the tunnel is not up, restart: `npx @ait-co/devtools devtools-mcp`.\n\nEnvironment-specific behaviour:\n • env 3 / relay-staging (start_debug mode=\"relay-staging\"): requires scheme_url — the intoss-private://…?_deploymentId=<uuid> URL from `ait deploy --scheme-only`. Splices debug=1 + relay URL into the scheme URL to produce a self-attach deep link.\n • env 2 / relay-sandbox (start_debug mode=\"relay-sandbox\"): scheme_url is NOT used. Instead, reads AIT_TUNNEL_BASE_URL (the https://*.trycloudflare.com app tunnel from `tunnel:{cdp:true}`) and builds a launcher PWA deep-link (https://devtools.aitc.dev/launcher/?url=…&debug=1&relay=…). When projectRoot is given, the app name from <projectRoot>/package.json is automatically added as name= so the launcher partner bar shows it. Scan the QR with the phone to open the launcher, which frames the tunnel URL and attaches CDP.\n\nSet wait_for_attach=true to block until a page attaches (polls up to 30 s). On timeout, call build_attach_url again to resume polling. When open_in_browser=true (default), saves the QR as a PNG and opens it in the OS default browser — only works when the MCP server runs on a local GUI machine (not headless/remote containers). \n\nTOTP auth: when AIT_DEBUG_TOTP_SECRET is set on the MCP server, the returned attachUrl automatically includes the current one-time code (at=<code>). The code is valid for ~3 minutes (the relay gate accepts ±6 TOTP steps = 180–210 s of backwards acceptance). The response includes a `totp` field with `expiresAt` (ISO timestamp, ~3 min from issuance). If the phone scan happens after expiresAt, the relay will reject the code — just call build_attach_url again to get a fresh URL. Without AIT_DEBUG_TOTP_SECRET, the attachUrl has no expiry.",
3556
3580
  inputSchema: {
3557
3581
  type: "object",
3558
3582
  properties: {
@@ -4566,7 +4590,7 @@ async function readMcpSdkVersion() {
4566
4590
  * some test environments that skip the build step).
4567
4591
  */
4568
4592
  function readDevtoolsVersion() {
4569
- return "0.1.72";
4593
+ return "0.1.73";
4570
4594
  }
4571
4595
  /**
4572
4596
  * Derives the next recommended action from a completed diagnostics snapshot.
@@ -5070,7 +5094,7 @@ function createDebugServer(deps) {
5070
5094
  const collector = collectorDep ?? new InMemoryDiagnosticsCollector();
5071
5095
  const server = new Server({
5072
5096
  name: "ait-debug",
5073
- version: "0.1.72"
5097
+ version: "0.1.73"
5074
5098
  }, { capabilities: { tools: { listChanged: true } } });
5075
5099
  server.setRequestHandler(ListToolsRequestSchema, () => {
5076
5100
  const conn = router.active;
@@ -5170,11 +5194,20 @@ function createDebugServer(deps) {
5170
5194
  expiresAt: new Date(expiresAtMs).toISOString()
5171
5195
  };
5172
5196
  }
5173
- const attachUrl = buildLauncherAttachUrl(tunnelHttpUrl, tunnelStatus.wssUrl, totpCode);
5197
+ let launcherAppName;
5198
+ if (buildProjectRoot !== void 0) try {
5199
+ const { readFileSync } = await import("node:fs");
5200
+ const pkgRaw = readFileSync(`${buildProjectRoot}/package.json`, "utf8");
5201
+ const pkg = JSON.parse(pkgRaw);
5202
+ const rawName = typeof pkg.name === "string" ? pkg.name : "";
5203
+ launcherAppName = (rawName.includes("/") ? rawName.slice(rawName.indexOf("/") + 1) : rawName).trim() || void 0;
5204
+ } catch {}
5205
+ const attachUrl = buildLauncherAttachUrl(tunnelHttpUrl, tunnelStatus.wssUrl, totpCode, { name: launcherAppName });
5174
5206
  onAttachUrlBuilt?.({
5175
5207
  kind: "launcher",
5176
5208
  tunnelHttpUrl,
5177
- wssUrl: tunnelStatus.wssUrl
5209
+ wssUrl: tunnelStatus.wssUrl,
5210
+ appName: launcherAppName
5178
5211
  });
5179
5212
  const relayUrl = tunnelStatus.wssUrl;
5180
5213
  const totp = totpMeta;
@@ -5610,7 +5643,7 @@ function makeSingleConnectionRouter(connection) {
5610
5643
  function rebuildAttachUrl(parts) {
5611
5644
  const secret = process.env.AIT_DEBUG_TOTP_SECRET;
5612
5645
  const code = secret ? generateTotp(secret) : void 0;
5613
- return parts.kind === "launcher" ? buildLauncherAttachUrl(parts.tunnelHttpUrl, parts.wssUrl, code) : buildDeepLinkAttachUrl(parts.schemeUrl, parts.wssUrl, code);
5646
+ return parts.kind === "launcher" ? buildLauncherAttachUrl(parts.tunnelHttpUrl, parts.wssUrl, code, { name: parts.appName }) : buildDeepLinkAttachUrl(parts.schemeUrl, parts.wssUrl, code);
5614
5647
  }
5615
5648
  function jsonResult$1(value) {
5616
5649
  return { content: [{
@@ -6957,7 +6990,7 @@ function createDevServer(deps = {}) {
6957
6990
  const aitSource = deps.aitSource ?? new HttpAitSource({ stateEndpoint });
6958
6991
  const server = new Server({
6959
6992
  name: "ait-devtools",
6960
- version: "0.1.72"
6993
+ version: "0.1.73"
6961
6994
  }, { capabilities: { tools: {} } });
6962
6995
  server.setRequestHandler(ListToolsRequestSchema, () => ({ tools: DEV_TOOL_DEFINITIONS.map((tool) => ({ ...tool })) }));
6963
6996
  server.setRequestHandler(CallToolRequestSchema, async (request) => {