@ait-co/devtools 0.1.74 → 0.1.76

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 (40) hide show
  1. package/README.en.md +22 -1
  2. package/README.md +22 -1
  3. package/dist/devtools-opener-BbUXBzgA.js.map +1 -1
  4. package/dist/devtools-opener-Bp671YXu.cjs.map +1 -1
  5. package/dist/devtools-opener-D84kZFtR.js.map +1 -1
  6. package/dist/devtools-opener-h6A-UjzC.cjs.map +1 -1
  7. package/dist/in-app/auto.d.ts +86 -0
  8. package/dist/in-app/auto.d.ts.map +1 -0
  9. package/dist/in-app/auto.js +549 -0
  10. package/dist/in-app/auto.js.map +1 -0
  11. package/dist/mcp/cli.js +77 -37
  12. package/dist/mcp/cli.js.map +1 -1
  13. package/dist/mcp/server.js +1 -1
  14. package/dist/mock/index.d.ts +50 -2
  15. package/dist/mock/index.d.ts.map +1 -1
  16. package/dist/mock/index.js +1210 -1110
  17. package/dist/mock/index.js.map +1 -1
  18. package/dist/panel/index.js +822 -820
  19. package/dist/panel/index.js.map +1 -1
  20. package/dist/{qr-http-server-Bn2ciFuC.js → qr-http-server-CDO6o2nr.js} +19 -7
  21. package/dist/qr-http-server-CDO6o2nr.js.map +1 -0
  22. package/dist/{qr-http-server-DNGVwI0P.cjs → qr-http-server-D0v9ooAD.cjs} +19 -7
  23. package/dist/qr-http-server-D0v9ooAD.cjs.map +1 -0
  24. package/dist/{qr-http-server-Cpc4jfTA.js → qr-http-server-DznDIcJF.js} +19 -7
  25. package/dist/qr-http-server-DznDIcJF.js.map +1 -0
  26. package/dist/{qr-http-server-BqZ8c0Bp.cjs → qr-http-server-jMC1nVqY.cjs} +19 -7
  27. package/dist/qr-http-server-jMC1nVqY.cjs.map +1 -0
  28. package/dist/{tunnel-CAxygywQ.cjs → tunnel-D7f-0enB.cjs} +2 -2
  29. package/dist/{tunnel-CAxygywQ.cjs.map → tunnel-D7f-0enB.cjs.map} +1 -1
  30. package/dist/{tunnel-BuymAS3O.js → tunnel-km3KkZrF.js} +2 -2
  31. package/dist/{tunnel-BuymAS3O.js.map → tunnel-km3KkZrF.js.map} +1 -1
  32. package/dist/unplugin/index.cjs +1 -1
  33. package/dist/unplugin/index.js +1 -1
  34. package/dist/unplugin/tunnel.cjs +1 -1
  35. package/dist/unplugin/tunnel.js +1 -1
  36. package/package.json +6 -1
  37. package/dist/qr-http-server-Bn2ciFuC.js.map +0 -1
  38. package/dist/qr-http-server-BqZ8c0Bp.cjs.map +0 -1
  39. package/dist/qr-http-server-Cpc4jfTA.js.map +0 -1
  40. package/dist/qr-http-server-DNGVwI0P.cjs.map +0 -1
package/dist/mcp/cli.js CHANGED
@@ -1169,9 +1169,8 @@ function buildDeepLinkAttachUrl(schemeUrl, wssUrl, totpCode) {
1169
1169
  * Chii serves its own DevTools frontend at
1170
1170
  * `<relayHttpBaseUrl>/front_end/chii_app.html`. The `ws=` (plain HTTP relay)
1171
1171
  * or `wss=` (HTTPS relay) query parameter is a URL-encoded string of the form
1172
- * `<relay-host>/client/<uuid>?target=<id>` (and optionally `&at=<totp>`)
1173
- * the same format used by Chii's own target list page (derived from
1174
- * `chii/public/index.js`).
1172
+ * `<relay-host>/client/<uuid>?target=<id>&at=<totp>` — the same format used
1173
+ * by Chii's own target list page (derived from `chii/public/index.js`).
1175
1174
  *
1176
1175
  * The `at=` TOTP code is minted at call time via `mintTotp()`. It is valid
1177
1176
  * for ~3 minutes (relay gate accepts ±RELAY_VERIFY_SKEW_STEPS=6 steps =
@@ -1179,6 +1178,15 @@ function buildDeepLinkAttachUrl(schemeUrl, wssUrl, totpCode) {
1179
1178
  * If the window expires before the browser connects, the relay will reject the
1180
1179
  * WebSocket upgrade with close code 4401.
1181
1180
  *
1181
+ * FAIL-CLOSED (issue #509): `mintTotp` is REQUIRED. When omitted (i.e.
1182
+ * `undefined`), this function returns `null` — the caller must treat `null` as
1183
+ * "inspector not yet available" and show a waiting hint instead of a broken
1184
+ * link. Relay sessions gate every WS upgrade with TOTP (#452), so a URL built
1185
+ * without `at=` would be rejected with WS 4401 immediately — there is no
1186
+ * non-TOTP relay path in production. Returning `null` surfaces this cleanly as
1187
+ * a "TOTP not yet configured" state rather than silently producing a URL that
1188
+ * will always fail at the WS handshake.
1189
+ *
1182
1190
  * SECRET-HANDLING: `mintTotp` returns a code, not a secret. The code is
1183
1191
  * embedded in the `wss=` parameter (inside the `at=` param) of the returned
1184
1192
  * URL. Callers MUST NOT log the returned URL to stdout (stderr is OK — it is
@@ -1187,11 +1195,14 @@ function buildDeepLinkAttachUrl(schemeUrl, wssUrl, totpCode) {
1187
1195
  * @param relayHttpBaseUrl - Local HTTP base URL of the Chii relay, e.g.
1188
1196
  * `http://127.0.0.1:9100`. No trailing slash.
1189
1197
  * @param targetId - Chii target id (from `GET <relay>/targets`).
1190
- * @param mintTotp - Optional function that returns a fresh 6-digit TOTP code
1191
- * string. Called at most once. When omitted (TOTP disabled) no `at=` param
1192
- * is added.
1198
+ * @param mintTotp - Function that returns a fresh 6-digit TOTP code string.
1199
+ * Called at most once. **Required** when `undefined`, the function returns
1200
+ * `null` (fail-closed: no `at=` param means the relay WS gate rejects the
1201
+ * handshake, so a null result is safer than a URL that always 404s).
1193
1202
  * @param panel - Initial panel. Defaults to `"console"`.
1194
1203
  *
1204
+ * @returns The inspector URL string, or `null` when `mintTotp` is absent.
1205
+ *
1195
1206
  * @example
1196
1207
  * buildChiiInspectorUrl(
1197
1208
  * 'http://127.0.0.1:9100',
@@ -1201,6 +1212,7 @@ function buildDeepLinkAttachUrl(schemeUrl, wssUrl, totpCode) {
1201
1212
  * // → 'http://127.0.0.1:9100/front_end/chii_app.html?ws=127.0.0.1%3A9100%2Fclient%2F<uuid>%3Ftarget%3Dabc123%26at%3D<code>'
1202
1213
  */
1203
1214
  function buildChiiInspectorUrl(relayHttpBaseUrl, targetId, mintTotp, panel = "console") {
1215
+ if (!mintTotp) return null;
1204
1216
  let relayHost;
1205
1217
  let wsParamName;
1206
1218
  try {
@@ -1212,11 +1224,8 @@ function buildChiiInspectorUrl(relayHttpBaseUrl, targetId, mintTotp, panel = "co
1212
1224
  wsParamName = /^https:/i.test(relayHttpBaseUrl) ? "wss" : "ws";
1213
1225
  }
1214
1226
  const clientId = `devtools-opener-${Date.now().toString(36)}`;
1215
- let wsPath = `${relayHost}/client/${clientId}?target=${encodeURIComponent(targetId)}`;
1216
- if (mintTotp) {
1217
- const code = mintTotp();
1218
- wsPath += `&at=${encodeURIComponent(code)}`;
1219
- }
1227
+ const code = mintTotp();
1228
+ const wsPath = `${relayHost}/client/${clientId}?target=${encodeURIComponent(targetId)}&at=${encodeURIComponent(code)}`;
1220
1229
  const params = new URLSearchParams({
1221
1230
  [wsParamName]: wsPath,
1222
1231
  panel
@@ -1325,6 +1334,10 @@ var AutoDevtoolsOpener = class {
1325
1334
  if (!options.targetId) return;
1326
1335
  this._opened = true;
1327
1336
  const inspectorUrl = buildChiiInspectorUrl(options.relayHttpBaseUrl, options.targetId, options.mintTotp);
1337
+ if (inspectorUrl === null) {
1338
+ process.stderr.write("[ait-debug] 기기가 연결됐습니다 — TOTP secret 미설정으로 인스펙터 URL을 생성할 수 없습니다.\n[ait-debug] relay 세션은 AIT_DEBUG_TOTP_SECRET 설정이 필요합니다.\n");
1339
+ return;
1340
+ }
1328
1341
  process.stderr.write(`[ait-debug] 기기가 연결됐습니다 — Chii DevTools를 자동으로 엽니다.
1329
1342
  [ait-debug] DevTools URL: ${inspectorUrl}\n[ait-debug] (AIT_AUTO_DEVTOOLS=0 으로 자동 열기를 끌 수 있습니다)
1330
1343
  [ait-debug] 주의: URL의 at= 코드는 ~3분 안에서만 유효합니다.
@@ -2142,6 +2155,7 @@ const en = {
2142
2155
  "launcher.diagNo": "no",
2143
2156
  "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.",
2144
2157
  "launcher.navbar.defaultTitle": "Mini App",
2158
+ "launcher.navbar.back": "Back",
2145
2159
  "launcher.navbar.menu": "Menu",
2146
2160
  "launcher.navbar.close": "Close",
2147
2161
  "launcher.navbar.menuRescan": "Rescan",
@@ -2393,6 +2407,7 @@ const tables = {
2393
2407
  "launcher.diagNo": "아니요",
2394
2408
  "launcher.letterboxDetected": "표시 영역이 {pt}pt 부족합니다 — iOS standalone letterbox로 보입니다. 런처를 홈 화면에서 제거 후 다시 설치하면 해소될 수 있어요.",
2395
2409
  "launcher.navbar.defaultTitle": "미니앱",
2410
+ "launcher.navbar.back": "뒤로가기",
2396
2411
  "launcher.navbar.menu": "메뉴",
2397
2412
  "launcher.navbar.close": "닫기",
2398
2413
  "launcher.navbar.menuRescan": "다시 스캔",
@@ -3100,8 +3115,9 @@ function buildAttachHtml(qrDataUrl, safeLabel, safeAttachUrl, locale, path = "/a
3100
3115
  *
3101
3116
  * @param getDashboardState - dashboard 상태를 반환하는 클로저. 주입 시 `GET /` dashboard와
3102
3117
  * `GET /events` SSE 스트림이 활성화된다. 미주입 시 두 라우트는 204/서비스 없음으로 응답.
3118
+ * @param options - 서버 옵션. `sseRefreshIntervalMs`로 idle 탭 TOTP 만료 방지 주기를 조정.
3103
3119
  */
3104
- async function startQrHttpServer(getDashboardState) {
3120
+ async function startQrHttpServer(getDashboardState, options) {
3105
3121
  const { default: QRCode } = await import("qrcode");
3106
3122
  /** SSE 활성 연결 목록 — `notifyStateChange()` 시 전체 push. */
3107
3123
  const sseClients = [];
@@ -3234,19 +3250,28 @@ async function startQrHttpServer(getDashboardState) {
3234
3250
  const address = server.address();
3235
3251
  if (!address || typeof address === "string") throw new Error("qr-http-server: server.address()가 예상하지 못한 형태입니다.");
3236
3252
  const port = address.port;
3253
+ /** idle 탭 TOTP 만료 방지용 주기 SSE 갱신 interval. */
3254
+ function notifyStateChangeInternal() {
3255
+ if (!getDashboardState) return;
3256
+ const state = getDashboardState();
3257
+ for (const client of sseClients) try {
3258
+ pushStateToClient(client, state);
3259
+ } catch {}
3260
+ }
3261
+ const refreshIntervalMs = options?.sseRefreshIntervalMs ?? 9e4;
3262
+ const refreshHandle = setInterval(() => {
3263
+ if (sseClients.length > 0 && getDashboardState) notifyStateChangeInternal();
3264
+ }, refreshIntervalMs).unref();
3237
3265
  return {
3238
3266
  port,
3239
3267
  buildAttachPageUrl(attachUrl) {
3240
3268
  return `http://127.0.0.1:${port}/attach?u=${encodeURIComponent(attachUrl)}`;
3241
3269
  },
3242
3270
  notifyStateChange() {
3243
- if (!getDashboardState) return;
3244
- const state = getDashboardState();
3245
- for (const client of sseClients) try {
3246
- pushStateToClient(client, state);
3247
- } catch {}
3271
+ notifyStateChangeInternal();
3248
3272
  },
3249
3273
  close() {
3274
+ clearInterval(refreshHandle);
3250
3275
  return new Promise((resolve, reject) => {
3251
3276
  server.close((err) => err ? reject(err) : resolve());
3252
3277
  });
@@ -4635,7 +4660,7 @@ async function readMcpSdkVersion() {
4635
4660
  * some test environments that skip the build step).
4636
4661
  */
4637
4662
  function readDevtoolsVersion() {
4638
- return "0.1.74";
4663
+ return "0.1.76";
4639
4664
  }
4640
4665
  /**
4641
4666
  * Derives the next recommended action from a completed diagnostics snapshot.
@@ -5139,7 +5164,7 @@ function createDebugServer(deps) {
5139
5164
  const collector = collectorDep ?? new InMemoryDiagnosticsCollector();
5140
5165
  const server = new Server({
5141
5166
  name: "ait-debug",
5142
- version: "0.1.74"
5167
+ version: "0.1.76"
5143
5168
  }, { capabilities: { tools: { listChanged: true } } });
5144
5169
  server.setRequestHandler(ListToolsRequestSchema, () => {
5145
5170
  const conn = router.active;
@@ -5738,37 +5763,52 @@ function errorResult(err, name, isLocal = false) {
5738
5763
  return classifyToolError(err, name, isLocal);
5739
5764
  }
5740
5765
  /**
5741
- * Starts a polling watcher that detects the first 0→N target transition on
5766
+ * Starts a polling watcher that detects target-set changes on
5742
5767
  * `connection.listTargets()` and sends a `notifications/tools/list_changed`
5743
5768
  * notification on the given server.
5744
5769
  *
5745
5770
  * The watcher polls every `intervalMs` (default 1 000 ms). It fires
5746
- * `server.sendToolListChanged()` exactly once on the first transition — then
5747
- * clears itself. Shutdown calls `stop()` to clear the interval.
5771
+ * `server.sendToolListChanged()` + `onAttach()` whenever the sorted target-id
5772
+ * signature changes AND the new target set is non-empty. This covers:
5773
+ * - 0→N first attach
5774
+ * - 1→1 target replacement (same count, different id — e.g. rescan)
5775
+ * - N→M any change where the result is still non-empty
5776
+ *
5777
+ * Full detach (→ empty) updates the stored signature but does NOT fire the
5778
+ * callback — `onAttach` semantics are about a live target being present.
5779
+ *
5780
+ * The interval is **never cleared automatically** — it keeps running until
5781
+ * `stop()` is called during shutdown. This ensures that a target replacement
5782
+ * after the first attach is always detected.
5748
5783
  *
5749
- * `onFirstAttach` is called once on the 0→N transition (or immediately when
5750
- * already attached). Use this to trigger side-effects such as auto-opening
5751
- * Chrome DevTools (issue #282). The callback is optional; omitting it preserves
5752
- * the previous behaviour exactly.
5784
+ * `onAttach` is called on every non-empty signature change (or immediately when
5785
+ * already attached). Use this to trigger side-effects such as pushing a fresh
5786
+ * SSE state to open dashboard tabs (issue #509). The callback is optional;
5787
+ * omitting it preserves the previous behaviour exactly.
5753
5788
  *
5754
5789
  * SECRET-HANDLING: target `id`/`title`/`url` are not written to any log here.
5755
5790
  * Only an attach-detected stderr line is emitted (no target details).
5756
5791
  *
5757
5792
  * @returns `stop` — call this during shutdown to clear the interval.
5758
5793
  */
5759
- function startAttachWatcher(connection, server, intervalMs = 1e3, onFirstAttach) {
5760
- let wasAttached = connection.listTargets().length > 0;
5761
- if (wasAttached) {
5794
+ function startAttachWatcher(connection, server, intervalMs = 1e3, onAttach) {
5795
+ /** Sorted, comma-joined target-id string — '' means no targets attached. */
5796
+ function signature() {
5797
+ return connection.listTargets().map((t) => t.id).sort().join(",");
5798
+ }
5799
+ let lastSignature = signature();
5800
+ if (lastSignature !== "") {
5762
5801
  server.sendToolListChanged();
5763
- onFirstAttach?.();
5802
+ onAttach?.();
5764
5803
  }
5765
5804
  const handle = setInterval(() => {
5766
- const isAttached = connection.listTargets().length > 0;
5767
- if (!wasAttached && isAttached) {
5768
- wasAttached = true;
5769
- server.sendToolListChanged();
5770
- onFirstAttach?.();
5771
- clearInterval(handle);
5805
+ const current = signature();
5806
+ if (current !== lastSignature) {
5807
+ lastSignature = current;
5808
+ if (current !== "") {
5809
+ server.sendToolListChanged();
5810
+ onAttach?.();
5811
+ }
5772
5812
  }
5773
5813
  }, intervalMs);
5774
5814
  return { stop() {
@@ -7065,7 +7105,7 @@ function createDevServer(deps = {}) {
7065
7105
  const aitSource = deps.aitSource ?? new HttpAitSource({ stateEndpoint });
7066
7106
  const server = new Server({
7067
7107
  name: "ait-devtools",
7068
- version: "0.1.74"
7108
+ version: "0.1.76"
7069
7109
  }, { capabilities: { tools: {} } });
7070
7110
  server.setRequestHandler(ListToolsRequestSchema, () => ({ tools: DEV_TOOL_DEFINITIONS.map((tool) => ({ ...tool })) }));
7071
7111
  server.setRequestHandler(CallToolRequestSchema, async (request) => {