bitfab-cli 0.2.33 → 0.2.35

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 (2) hide show
  1. package/dist/index.js +117 -66
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -6971,6 +6971,39 @@ async function pollLoginEvents(opts) {
6971
6971
  }
6972
6972
  throw new Error("Login polling aborted");
6973
6973
  }
6974
+ async function fetchStreamTip(client) {
6975
+ const PAGE_LIMIT = 500;
6976
+ const MAX_PAGES = 10;
6977
+ let tip;
6978
+ try {
6979
+ for (let page = 0; page < MAX_PAGES; page++) {
6980
+ const url2 = new URL(`${client.serviceUrl}/api/studio/events`);
6981
+ url2.searchParams.set("session", client.sessionId);
6982
+ url2.searchParams.set("limit", String(PAGE_LIMIT));
6983
+ if (tip) {
6984
+ url2.searchParams.set("since", tip);
6985
+ }
6986
+ const res = await fetch(url2.toString(), {
6987
+ headers: { Authorization: `Bearer ${client.apiKey}` }
6988
+ });
6989
+ if (!res.ok) {
6990
+ return tip;
6991
+ }
6992
+ const body = await res.json();
6993
+ const events = body.events ?? [];
6994
+ const last = events.at(-1)?.id;
6995
+ if (!last) {
6996
+ return tip;
6997
+ }
6998
+ tip = last;
6999
+ if (events.length < PAGE_LIMIT) {
7000
+ return tip;
7001
+ }
7002
+ }
7003
+ } catch {
7004
+ }
7005
+ return tip;
7006
+ }
6974
7007
  async function navigateStudio(client, path15) {
6975
7008
  if (!isValidStudioRoute(path15)) {
6976
7009
  throw new Error(`Studio route not allowed: ${path15}`);
@@ -6982,7 +7015,7 @@ async function navigateStudio(client, path15) {
6982
7015
  }
6983
7016
  var STUDIO_NAVIGATE_ACK_TIMEOUT_MS = 12e3;
6984
7017
  var PING_PONG_TIMEOUT_MS = 5e3;
6985
- function awaitNavigateAck(client, path15, ackTimeoutMs) {
7018
+ function awaitNavigateAck(client, path15, ackTimeoutMs, startCursor) {
6986
7019
  const abortController = new AbortController();
6987
7020
  let timer = null;
6988
7021
  const result = new Promise((resolve) => {
@@ -6995,6 +7028,12 @@ function awaitNavigateAck(client, path15, ackTimeoutMs) {
6995
7028
  serviceUrl: client.serviceUrl,
6996
7029
  apiKey: client.apiKey,
6997
7030
  sessionId: client.sessionId,
7031
+ // Only a FRESH ack counts: polling from the stream start would match a
7032
+ // historical `studio:navigated` for the same path (sessions revisit the
7033
+ // same routes constantly), false-acking a navigation a dead or
7034
+ // disconnected window never processed — masking exactly the
7035
+ // unreachable-window case that must gate.
7036
+ startCursor,
6998
7037
  abortSignal: abortController.signal,
6999
7038
  onEvent: (event) => {
7000
7039
  if (event.type === "studio:navigated" && event.data.path === pathOnly) {
@@ -7017,11 +7056,11 @@ function awaitNavigateAck(client, path15, ackTimeoutMs) {
7017
7056
  abortController.abort();
7018
7057
  return;
7019
7058
  }
7020
- if (event.type === "studio:session-ended" || event.type === "studio:session-closed") {
7059
+ if (event.type === "studio:window-closed") {
7021
7060
  if (timer) {
7022
7061
  clearTimeout(timer);
7023
7062
  }
7024
- resolve({ acked: false, reason: "session-ended" });
7063
+ resolve({ acked: false, reason: "window-closed" });
7025
7064
  abortController.abort();
7026
7065
  }
7027
7066
  },
@@ -7075,12 +7114,13 @@ async function navigateStudioAndAwaitAck(client, path15, opts = {}) {
7075
7114
  if (!isValidStudioRoute(path15)) {
7076
7115
  return { acked: false, reason: "invalid-route" };
7077
7116
  }
7117
+ const startCursor = await fetchStreamTip(client);
7078
7118
  try {
7079
7119
  await navigateStudio(client, path15);
7080
7120
  } catch {
7081
7121
  return { acked: false, reason: "push-failed" };
7082
7122
  }
7083
- const result = await awaitNavigateAck(client, path15, ackTimeoutMs);
7123
+ const result = await awaitNavigateAck(client, path15, ackTimeoutMs, startCursor);
7084
7124
  if (result.acked || result.reason !== "timeout") {
7085
7125
  return result;
7086
7126
  }
@@ -7374,7 +7414,8 @@ function readActiveStudioSessionRaw() {
7374
7414
  sessionId: parsed.sessionId,
7375
7415
  serviceUrl: parsed.serviceUrl,
7376
7416
  pid: parsed.pid,
7377
- startedAt: parsed.startedAt
7417
+ startedAt: parsed.startedAt,
7418
+ ...typeof parsed.windowPid === "number" ? { windowPid: parsed.windowPid } : {}
7378
7419
  };
7379
7420
  }
7380
7421
  return null;
@@ -7601,9 +7642,10 @@ function spawnDetached(command, args) {
7601
7642
  child.on("error", () => {
7602
7643
  });
7603
7644
  child.unref();
7645
+ return child.pid ?? null;
7604
7646
  }
7605
7647
  function openChromiumApp(binaryPath, url2, sizeArgs) {
7606
- spawnDetached(binaryPath, [`--app=${url2}`, ...sizeArgs]);
7648
+ return spawnDetached(binaryPath, [`--app=${url2}`, ...sizeArgs]);
7607
7649
  }
7608
7650
  function openOsDefault(url2) {
7609
7651
  const platform2 = os7.platform();
@@ -7630,7 +7672,7 @@ function shouldDisableChromelessWindows() {
7630
7672
  function openChromelessWindow(url2) {
7631
7673
  if (shouldDisableChromelessWindows()) {
7632
7674
  openOsDefault(url2);
7633
- return;
7675
+ return null;
7634
7676
  }
7635
7677
  const browsers = getChromiumBrowsers();
7636
7678
  const defaultId = getDefaultBrowserId();
@@ -7639,22 +7681,21 @@ function openChromelessWindow(url2) {
7639
7681
  if (defaultChromium) {
7640
7682
  const binary = findExistingBinary(defaultChromium.binaryPaths);
7641
7683
  if (binary) {
7642
- openChromiumApp(binary, url2, sizeArgs);
7643
- return;
7684
+ return openChromiumApp(binary, url2, sizeArgs);
7644
7685
  }
7645
7686
  }
7646
7687
  if (defaultId !== null && defaultChromium === null) {
7647
7688
  openOsDefault(url2);
7648
- return;
7689
+ return null;
7649
7690
  }
7650
7691
  for (const browser of browsers) {
7651
7692
  const binary = findExistingBinary(browser.binaryPaths);
7652
7693
  if (binary) {
7653
- openChromiumApp(binary, url2, sizeArgs);
7654
- return;
7694
+ return openChromiumApp(binary, url2, sizeArgs);
7655
7695
  }
7656
7696
  }
7657
7697
  openOsDefault(url2);
7698
+ return null;
7658
7699
  }
7659
7700
 
7660
7701
  // ../bitfab-plugin-lib/dist/commands/createStudioSession.js
@@ -7678,10 +7719,14 @@ async function createStudioSession({ serviceUrl, apiKey, sessionId, initialPath
7678
7719
  sessionId,
7679
7720
  extraHeaders: clientHeaders
7680
7721
  });
7681
- writeActiveStudioSession({ sessionId, serviceUrl });
7682
7722
  const separator = initialPath.includes("?") ? "&" : "?";
7683
7723
  const url2 = `${serviceUrl}${initialPath}${separator}session=${encodeURIComponent(sessionId)}`;
7684
- openChromelessWindow(url2);
7724
+ const windowPid = openChromelessWindow(url2);
7725
+ writeActiveStudioSession({
7726
+ sessionId,
7727
+ serviceUrl,
7728
+ windowPid: windowPid ?? void 0
7729
+ });
7685
7730
  return { sessionId, serviceUrl };
7686
7731
  }
7687
7732
 
@@ -7887,7 +7932,7 @@ async function waitForSessionReady(opts) {
7887
7932
  }
7888
7933
 
7889
7934
  // ../bitfab-plugin-lib/dist/commands/openStudioTo.js
7890
- var SESSION_CLOSE_GRACE_PERIOD_MS = 1e4;
7935
+ var WINDOW_CLOSE_GRACE_PERIOD_MS = 1e4;
7891
7936
  var LOGIN_TIMEOUT_MS = 10 * 60 * 1e3;
7892
7937
  var StudioNavigationError = class extends Error {
7893
7938
  reason;
@@ -7914,7 +7959,7 @@ async function openStudioTo(path15, opts = {}) {
7914
7959
  const redirectPath = `${path15}${separator}session=${encodeURIComponent(sessionId2)}&pluginLogin=true`;
7915
7960
  const signInPath = `/studio/sign-in?redirect_url=${encodeURIComponent(redirectPath)}`;
7916
7961
  const signInUrl = `${serviceUrl}${signInPath}&session=${encodeURIComponent(sessionId2)}`;
7917
- openChromelessWindow(signInUrl);
7962
+ const windowPid = openChromelessWindow(signInUrl);
7918
7963
  opts.onLoginRequired?.(sessionId2, signInUrl);
7919
7964
  const abortController = new AbortController();
7920
7965
  const timer = setTimeout(() => abortController.abort(), LOGIN_TIMEOUT_MS);
@@ -7941,7 +7986,11 @@ async function openStudioTo(path15, opts = {}) {
7941
7986
  apiKey: loginApiKey,
7942
7987
  sessionId: sessionId2
7943
7988
  });
7944
- writeActiveStudioSession({ sessionId: sessionId2, serviceUrl });
7989
+ writeActiveStudioSession({
7990
+ sessionId: sessionId2,
7991
+ serviceUrl,
7992
+ windowPid: windowPid ?? void 0
7993
+ });
7945
7994
  const poller2 = startEventPoller(serviceUrl, loginApiKey, sessionId2, opts.onEvent, restoreFocus);
7946
7995
  return {
7947
7996
  sessionId: sessionId2,
@@ -8001,71 +8050,73 @@ async function openStudioTo(path15, opts = {}) {
8001
8050
  function startEventPoller(serviceUrl, apiKey, sessionId, onEvent, restoreFocus) {
8002
8051
  const abortController = new AbortController();
8003
8052
  let graceTimer = null;
8004
- let sawSessionEnded = false;
8053
+ let endedNotified = false;
8005
8054
  const clearGraceTimer = () => {
8006
8055
  if (graceTimer) {
8007
8056
  clearTimeout(graceTimer);
8008
8057
  graceTimer = null;
8009
8058
  }
8010
8059
  };
8011
- const terminate = (clear, baseEvent) => {
8012
- graceTimer = null;
8013
- if (clear) {
8014
- clearActiveStudioSession(sessionId);
8060
+ const notifyEnded = (baseEvent) => {
8061
+ if (!endedNotified) {
8062
+ endedNotified = true;
8063
+ restoreFocus();
8064
+ onEvent({ ...baseEvent, type: "studio:session-ended" });
8015
8065
  }
8016
- restoreFocus();
8017
- onEvent({ ...baseEvent, type: "studio:session-ended" });
8018
8066
  abortController.abort();
8019
8067
  };
8020
- const done = pollAgentSessionEvents({
8021
- serviceUrl,
8022
- apiKey,
8023
- sessionId,
8024
- abortSignal: abortController.signal,
8025
- onEvent: (event) => {
8026
- if (event.type === "studio:session-closed") {
8027
- clearGraceTimer();
8028
- if (sawSessionEnded) {
8029
- terminate(true, event);
8030
- } else {
8068
+ const done = (async () => {
8069
+ const startCursor = await fetchStreamTip({ serviceUrl, apiKey, sessionId });
8070
+ if (abortController.signal.aborted) {
8071
+ return;
8072
+ }
8073
+ await pollAgentSessionEvents({
8074
+ serviceUrl,
8075
+ apiKey,
8076
+ sessionId,
8077
+ startCursor,
8078
+ abortSignal: abortController.signal,
8079
+ onEvent: (event) => {
8080
+ if (event.type === "studio:window-closed") {
8081
+ clearGraceTimer();
8031
8082
  graceTimer = setTimeout(() => {
8032
- terminate(true, event);
8033
- }, SESSION_CLOSE_GRACE_PERIOD_MS);
8083
+ graceTimer = null;
8084
+ clearActiveStudioSession(sessionId);
8085
+ notifyEnded(event);
8086
+ }, WINDOW_CLOSE_GRACE_PERIOD_MS);
8087
+ return;
8034
8088
  }
8035
- return;
8036
- }
8037
- if (event.type === "studio:session-started") {
8038
- if (graceTimer) {
8089
+ if (event.type === "studio:session-started") {
8090
+ if (graceTimer) {
8091
+ clearGraceTimer();
8092
+ console.error("Studio reconnected after page refresh");
8093
+ }
8094
+ return;
8095
+ }
8096
+ if (event.type === "studio:session-ended") {
8039
8097
  clearGraceTimer();
8040
- console.error("Studio reconnected after page refresh");
8098
+ notifyEnded(event);
8099
+ return;
8041
8100
  }
8042
- sawSessionEnded = false;
8043
- return;
8044
- }
8045
- if (event.type === "studio:session-ended") {
8046
- sawSessionEnded = true;
8047
- if (!graceTimer) {
8048
- graceTimer = setTimeout(() => {
8049
- terminate(false, event);
8050
- }, SESSION_CLOSE_GRACE_PERIOD_MS);
8101
+ if (event.type === "studio:session-closed") {
8102
+ return;
8103
+ }
8104
+ if (event.type === "studio:not-member") {
8105
+ restoreFocus();
8106
+ onEvent(event);
8107
+ abortController.abort();
8108
+ return;
8109
+ }
8110
+ if (event.type === "studio:return-to-agent" || event.type === "studio:edit-with-agent") {
8111
+ restoreFocus();
8051
8112
  }
8052
- return;
8053
- }
8054
- if (event.type === "studio:not-member") {
8055
- restoreFocus();
8056
8113
  onEvent(event);
8057
- abortController.abort();
8058
- return;
8059
- }
8060
- if (event.type === "studio:return-to-agent" || event.type === "studio:edit-with-agent") {
8061
- restoreFocus();
8114
+ },
8115
+ onError: (err) => {
8116
+ console.error(`studio agent-event poll: ${err.message}`);
8062
8117
  }
8063
- onEvent(event);
8064
- },
8065
- onError: (err) => {
8066
- console.error(`studio agent-event poll: ${err.message}`);
8067
- }
8068
- }).catch((err) => {
8118
+ });
8119
+ })().catch((err) => {
8069
8120
  if (!abortController.signal.aborted) {
8070
8121
  console.error(`studio agent-event poll terminated: ${err.message}`);
8071
8122
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bitfab-cli",
3
- "version": "0.2.33",
3
+ "version": "0.2.35",
4
4
  "description": "Install and configure the Bitfab plugin in Claude Code, Codex, or Cursor.",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",