@ait-co/devtools 0.1.100 → 0.1.101

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.
package/dist/mcp/cli.js CHANGED
@@ -4858,7 +4858,7 @@ async function readMcpSdkVersion() {
4858
4858
  * some test environments that skip the build step).
4859
4859
  */
4860
4860
  function readDevtoolsVersion() {
4861
- return "0.1.100";
4861
+ return "0.1.101";
4862
4862
  }
4863
4863
  /**
4864
4864
  * Derives the next recommended action from a completed diagnostics snapshot.
@@ -5313,6 +5313,52 @@ function makeTunnelStatus(up, wssUrl, droppedAt = null, reissueAttempts = 0) {
5313
5313
  * Node-only.
5314
5314
  */
5315
5315
  /**
5316
+ * Maximum age (ms) of a page's `lastSeenAt` before it is treated as a ghost
5317
+ * and excluded from the `wait_for_attach` short-circuit in `build_attach_url`
5318
+ * (issue #610).
5319
+ *
5320
+ * Rationale: the env-2 relay is owned by the dev server (unplugin), so every
5321
+ * `dev:phone:cdp` restart produces a new quick-tunnel. The old relay goes
5322
+ * offline immediately, but the daemon's warm `ChiiCdpConnection` still lists
5323
+ * the last-seen target — its `lastSeenAt` freezes at the moment the old relay
5324
+ * died. A 5-minute threshold is large enough to be invisible in normal usage
5325
+ * (active CDP sessions see a message every few seconds) while being small
5326
+ * enough to catch a relay that went down before the daemon was re-entered.
5327
+ *
5328
+ * Injectable for tests via {@link DebugServerDeps.stalePageThresholdMs}.
5329
+ */
5330
+ const RELAY_SANDBOX_STALE_PAGE_MS = 300 * 1e3;
5331
+ /**
5332
+ * Predicate used by `build_attach_url`'s `wait_for_attach` loop to decide
5333
+ * whether the relay-sandbox connection has a genuinely fresh page attached.
5334
+ *
5335
+ * Stale-ghost gating (issue #610): when the dev server restarts with a new
5336
+ * quick-tunnel, the warm `ChiiCdpConnection` still lists the last-seen target
5337
+ * but its `lastSeenAt` is frozen. A page whose `lastSeenAt` exceeds
5338
+ * `stalePageThresholdMs` is a ghost from the dead relay — it must NOT
5339
+ * short-circuit `wait_for_attach`.
5340
+ *
5341
+ * Rules:
5342
+ * - `pages.length === 0` → false (nothing attached).
5343
+ * - Connection has no `getLastSeenAt` (test fakes, local-browser) → falls back
5344
+ * to `pages.length > 0` (regression-safe).
5345
+ * - `seenMs === null` → treat as fresh (no CDP message received yet, first
5346
+ * message pending — the connection is alive).
5347
+ * - Otherwise: at least one page must satisfy `nowMs - seenMs <=
5348
+ * stalePageThresholdMs`.
5349
+ *
5350
+ * Exported for unit testing.
5351
+ */
5352
+ function isSandboxPageFresh(pages, getLastSeenAt, nowMs, stalePageThresholdMs) {
5353
+ if (pages.length === 0) return false;
5354
+ if (getLastSeenAt === null) return true;
5355
+ return pages.some((p) => {
5356
+ const seenMs = getLastSeenAt(p.id);
5357
+ if (seenMs === null) return true;
5358
+ return nowMs - seenMs <= stalePageThresholdMs;
5359
+ });
5360
+ }
5361
+ /**
5316
5362
  * Parses `_deploymentId` from the query string of a scheme URL.
5317
5363
  *
5318
5364
  * Returns `null` when the param is absent or empty — callers treat that as
@@ -5395,7 +5441,7 @@ function waitForAttachWithEvents(connection, filterFn, timeoutMs, pollIntervalMs
5395
5441
  * naturally via `enableDomains`). The tier only controls visibility.
5396
5442
  */
5397
5443
  function createDebugServer(deps) {
5398
- const { connection, router: routerDep, aitSource, getTunnelStatus, waitForAttachTimeoutMs = 6e4, qrHttpServer, getEnvironment: getEnvDep, getEnvironmentReason: getEnvReasonDep, diagnosticsCollector: collectorDep, totpSecret, onAttachUrlBuilt, getTunnelChildPid, readLock: readLockDep } = deps;
5444
+ const { connection, router: routerDep, aitSource, getTunnelStatus, waitForAttachTimeoutMs = 6e4, qrHttpServer, getEnvironment: getEnvDep, getEnvironmentReason: getEnvReasonDep, diagnosticsCollector: collectorDep, totpSecret, onAttachUrlBuilt, getTunnelChildPid, readLock: readLockDep, stalePageThresholdMs = RELAY_SANDBOX_STALE_PAGE_MS, nowMs = () => Date.now() } = deps;
5399
5445
  const getTotpSecret = deps.getTotpSecret ?? (() => totpSecret);
5400
5446
  const readLockFn = readLockDep ?? readServerLock;
5401
5447
  const router = routerDep ?? makeSingleConnectionRouter(connection);
@@ -5404,7 +5450,7 @@ function createDebugServer(deps) {
5404
5450
  const collector = collectorDep ?? new InMemoryDiagnosticsCollector();
5405
5451
  const server = new Server({
5406
5452
  name: "ait-debug",
5407
- version: "0.1.100"
5453
+ version: "0.1.101"
5408
5454
  }, { capabilities: { tools: { listChanged: true } } });
5409
5455
  server.setRequestHandler(ListToolsRequestSchema, () => {
5410
5456
  const conn = router.active;
@@ -5533,7 +5579,10 @@ function createDebugServer(deps) {
5533
5579
  });
5534
5580
  const relayUrl = tunnelStatus.wssUrl;
5535
5581
  const totp = totpMeta;
5536
- const isMatchingPage = (pages) => pages.length > 0;
5582
+ const connAsAny = conn;
5583
+ const getLastSeenAt = typeof connAsAny.getTargetLastSeenAt === "function" ? (id) => connAsAny.getTargetLastSeenAt(id) : null;
5584
+ const callNow = nowMs();
5585
+ const isMatchingPage = (pages) => isSandboxPageFresh(pages, getLastSeenAt, callNow, stalePageThresholdMs);
5537
5586
  const buildTimeoutError = (baseText, timeoutSec, observed) => {
5538
5587
  const observedUrls = observed.slice(0, 3).map((p) => p.url.slice(0, 80)).join(", ");
5539
5588
  return `${baseText}\n\nNo page attached within ${timeoutSec}s${observed.length > 0 ? ` — previously attached pages: [${observedUrls}]` : ""} — launcher QR을 폰 카메라로 스캔한 뒤 call list_pages를 다시 호출하세요.`;
@@ -6459,10 +6508,39 @@ var DualConnectionRouter = class {
6459
6508
  * `projectRoot` is forwarded to `bootLazyFor` so `relay-sandbox` boot can
6460
6509
  * fall back to `.ait_urls` file discovery (#424) when `AIT_RELAY_BASE_URL` is
6461
6510
  * not set in the environment.
6511
+ *
6512
+ * **Relay-sandbox stale-URL rebuild (issue #610):** when the `relay-sandbox`
6513
+ * family is already warm, reads the current relay URL via
6514
+ * `deps.readSandboxRelayUrl` and compares it against the cached
6515
+ * `relayHttpUrl`. If they differ (dev server was restarted → new tunnel),
6516
+ * the stale family is torn down, evicted from the map, and a fresh one is
6517
+ * booted. If they match, or if the URL cannot be read, the warm family is
6518
+ * reused (fail-open — no unnecessary teardown on transient read errors).
6519
+ *
6520
+ * SECRET-HANDLING: fresh and cached relay URLs carry the tunnel host. The
6521
+ * comparison result (same/different) is the only thing surfaced — URLs are
6522
+ * never logged.
6462
6523
  */
6463
6524
  async familyFor(key, projectRoot) {
6464
6525
  const warm = this.lazyFamilies.get(key);
6465
- if (warm) return warm;
6526
+ if (warm) {
6527
+ if (key === "relay-sandbox" && this.deps.readSandboxRelayUrl !== void 0) {
6528
+ let freshUrl = null;
6529
+ try {
6530
+ freshUrl = await this.deps.readSandboxRelayUrl(projectRoot);
6531
+ } catch {
6532
+ freshUrl = null;
6533
+ }
6534
+ if (freshUrl !== null && freshUrl !== warm.relayHttpUrl) {
6535
+ warm.stop();
6536
+ this.lazyFamilies.delete(key);
6537
+ const booted = await this.deps.bootLazyFor(key, projectRoot);
6538
+ this.lazyFamilies.set(key, booted);
6539
+ return booted;
6540
+ }
6541
+ }
6542
+ return warm;
6543
+ }
6466
6544
  const booted = await this.deps.bootLazyFor(key, projectRoot);
6467
6545
  this.lazyFamilies.set(key, booted);
6468
6546
  return booted;
@@ -6522,7 +6600,8 @@ async function runDebugServer(options = {}) {
6522
6600
  diagnosticsCollector,
6523
6601
  devtoolsOpener,
6524
6602
  onPageAttach: () => qrServer?.notifyStateChange(),
6525
- getInspectorStableUrl: () => qrServer?.inspectorStableUrl ?? null
6603
+ getInspectorStableUrl: () => qrServer?.inspectorStableUrl ?? null,
6604
+ readSandboxRelayUrl: (pr) => readMobileRelayBaseUrl(process.env, pr).catch(() => null)
6526
6605
  });
6527
6606
  const aitSource = new RoutingAitSource(() => {
6528
6607
  return router.active;
@@ -6734,7 +6813,8 @@ async function runLocalDebugServer(options = {}) {
6734
6813
  diagnosticsCollector,
6735
6814
  devtoolsOpener,
6736
6815
  onPageAttach: () => qrServer?.notifyStateChange(),
6737
- getInspectorStableUrl: () => qrServer?.inspectorStableUrl ?? null
6816
+ getInspectorStableUrl: () => qrServer?.inspectorStableUrl ?? null,
6817
+ readSandboxRelayUrl: (pr) => readMobileRelayBaseUrl(process.env, pr).catch(() => null)
6738
6818
  });
6739
6819
  const aitSource = new RoutingAitSource(() => {
6740
6820
  return router.active;
@@ -6929,7 +7009,8 @@ async function runMobileDebugServer(options = {}) {
6929
7009
  diagnosticsCollector,
6930
7010
  devtoolsOpener,
6931
7011
  onPageAttach: () => qrServer?.notifyStateChange(),
6932
- getInspectorStableUrl: () => qrServer?.inspectorStableUrl ?? null
7012
+ getInspectorStableUrl: () => qrServer?.inspectorStableUrl ?? null,
7013
+ readSandboxRelayUrl: (pr) => readMobileRelayBaseUrl(process.env, pr ?? options.projectRoot ?? process.cwd()).catch(() => null)
6933
7014
  });
6934
7015
  const aitSource = new RoutingAitSource(() => {
6935
7016
  return router.active;
@@ -7503,7 +7584,7 @@ function createDevServer(deps = {}) {
7503
7584
  const aitSource = deps.aitSource ?? new HttpAitSource({ stateEndpoint });
7504
7585
  const server = new Server({
7505
7586
  name: "ait-devtools",
7506
- version: "0.1.100"
7587
+ version: "0.1.101"
7507
7588
  }, { capabilities: { tools: {} } });
7508
7589
  server.setRequestHandler(ListToolsRequestSchema, () => ({ tools: DEV_TOOL_DEFINITIONS.map((tool) => ({ ...tool })) }));
7509
7590
  server.setRequestHandler(CallToolRequestSchema, async (request) => {