@modootoday/datalab-extension-mcp 1.8.10 → 1.8.11

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/README.en.md CHANGED
@@ -55,10 +55,10 @@ Avoid sharing a screenshot or the copied setup. If it may have leaked, press **[
55
55
  One command finds **every** AI app on your computer and connects them automatically. Nothing to do per app.
56
56
 
57
57
  1. **Open a terminal** — on Windows type `cmd` in the search box → "Command Prompt"; on Mac press `⌘+Space` and type `Terminal`; on Linux open your terminal app (often `Ctrl+Alt+T`).
58
- 2. **Paste this command and press Enter.** (`@1.8.10` is the pinned version, so the connector never changes behind your back.)
58
+ 2. **Paste this command and press Enter.** (`@1.8.11` is the pinned version, so the connector never changes behind your back.)
59
59
 
60
60
  ```
61
- npx -y @modootoday/datalab-extension-mcp@1.8.10 install
61
+ npx -y @modootoday/datalab-extension-mcp@1.8.11 install
62
62
  ```
63
63
 
64
64
  Lines of English scrolling by is normal. This only downloads and runs the connector — it touches nothing else.
@@ -130,10 +130,10 @@ Apps it **only prints instructions for** (follow the on-screen guide): VS Code,
130
130
  Installing also places the bundled **writing skills**. An AI app reads the name and description of every installed skill on every start, so the more are installed the less room the conversation has. The default therefore places only the ones that work right away.
131
131
 
132
132
  ```
133
- npx -y @modootoday/datalab-extension-mcp skills list
134
- npx -y @modootoday/datalab-extension-mcp skills status
135
- npx -y @modootoday/datalab-extension-mcp skills attach <name>
136
- npx -y @modootoday/datalab-extension-mcp skills detach <name>
133
+ npx -y @modootoday/datalab-extension-mcp@1.8.11 skills list
134
+ npx -y @modootoday/datalab-extension-mcp@1.8.11 skills status
135
+ npx -y @modootoday/datalab-extension-mcp@1.8.11 skills attach <name>
136
+ npx -y @modootoday/datalab-extension-mcp@1.8.11 skills detach <name>
137
137
  ```
138
138
 
139
139
  - `list` shows everything in this version, name and description.
@@ -152,9 +152,9 @@ The **workbench** is this computer, where the connector runs. If you use more th
152
152
  Make the code from the panel with [등록 코드 만들기], or from the terminal.
153
153
 
154
154
  ```
155
- npx -y @modootoday/datalab-extension-mcp browsers add "Work Chrome"
156
- npx -y @modootoday/datalab-extension-mcp browsers list
157
- npx -y @modootoday/datalab-extension-mcp browsers revoke <handle>
155
+ npx -y @modootoday/datalab-extension-mcp@1.8.11 browsers add "Work Chrome"
156
+ npx -y @modootoday/datalab-extension-mcp@1.8.11 browsers list
157
+ npx -y @modootoday/datalab-extension-mcp@1.8.11 browsers revoke <handle>
158
158
  ```
159
159
 
160
160
  `add` **asks you to paste the pairing token** — the same value you used to
@@ -252,7 +252,7 @@ Start it again after a full quit and it picks the tools up. If they still aren't
252
252
  - **When the connection disappears** — run the very command you used at the start once more; it safely re-registers. (Some AI apps clean up their settings file and drop the connection.)
253
253
 
254
254
  ```
255
- npx -y @modootoday/datalab-extension-mcp@1.8.10 install
255
+ npx -y @modootoday/datalab-extension-mcp@1.8.11 install
256
256
  ```
257
257
 
258
258
  - **`'npx' is not recognized...`** — Node.js isn't installed. The "No Node.js?" one-liner in Step 2 is the simplest fix.
@@ -282,7 +282,7 @@ Start it again after a full quit and it picks the tools up. If they still aren't
282
282
  1. Run this in a terminal (it removes the entries registered in your AI apps).
283
283
 
284
284
  ```
285
- npx -y @modootoday/datalab-extension-mcp@1.8.10 uninstall
285
+ npx -y @modootoday/datalab-extension-mcp@1.8.11 uninstall
286
286
  ```
287
287
 
288
288
  2. Turn **MCP 연결** off in the extension settings. The panel is the bridge, so with it off a leftover config line can look nothing up.
package/README.md CHANGED
@@ -49,10 +49,10 @@ Claude 같은 AI 앱을 데이터랩툴즈에 연결해 주는 작은 무료 프
49
49
  명령 한 줄이면 컴퓨터에 있는 AI 앱을 **모두 자동으로** 찾아 연결해 줘요. 앱마다 따로 할 일은 없어요.
50
50
 
51
51
  1. **터미널을 열어요** — Windows는 검색창에 `cmd` → "명령 프롬프트", Mac은 `⌘+스페이스`로 `터미널`, Linux는 터미널 앱(보통 `Ctrl+Alt+T`)이에요.
52
- 2. **아래 명령을 붙여넣고 Enter를 눌러요.** (`@1.8.10`은 커넥터가 나도 모르게 바뀌지 않도록 고정한 버전이에요.)
52
+ 2. **아래 명령을 붙여넣고 Enter를 눌러요.** (`@1.8.11`은 커넥터가 나도 모르게 바뀌지 않도록 고정한 버전이에요.)
53
53
 
54
54
  ```
55
- npx -y @modootoday/datalab-extension-mcp@1.8.10 install
55
+ npx -y @modootoday/datalab-extension-mcp@1.8.11 install
56
56
  ```
57
57
 
58
58
  영어 글자가 여러 줄 올라오는 건 정상이에요. 이 명령은 커넥터를 내려받아 실행할 뿐, 다른 건 건드리지 않아요.
@@ -118,10 +118,10 @@ Claude 같은 AI 앱을 데이터랩툴즈에 연결해 주는 작은 무료 프
118
118
  설치할 때 **글쓰기 도움말(스킬)** 도 함께 놓입니다. AI 앱은 켜질 때마다 설치된 도움말의 이름과 설명을 전부 읽기 때문에, 많이 깔수록 대화에 쓸 자리가 줄어들어요. 그래서 기본은 **바로 쓸 수 있는 것만** 놓습니다.
119
119
 
120
120
  ```
121
- npx -y @modootoday/datalab-extension-mcp skills list
122
- npx -y @modootoday/datalab-extension-mcp skills status
123
- npx -y @modootoday/datalab-extension-mcp skills attach <이름>
124
- npx -y @modootoday/datalab-extension-mcp skills detach <이름>
121
+ npx -y @modootoday/datalab-extension-mcp@1.8.11 skills list
122
+ npx -y @modootoday/datalab-extension-mcp@1.8.11 skills status
123
+ npx -y @modootoday/datalab-extension-mcp@1.8.11 skills attach <이름>
124
+ npx -y @modootoday/datalab-extension-mcp@1.8.11 skills detach <이름>
125
125
  ```
126
126
 
127
127
  - `list` 는 이 버전에 들어 있는 전부를 이름과 설명으로 보여 줘요.
@@ -140,9 +140,9 @@ npx -y @modootoday/datalab-extension-mcp skills detach <이름>
140
140
  패널에서 [등록 코드 만들기]를 눌러도 되고, 터미널을 쓰셔도 돼요.
141
141
 
142
142
  ```
143
- npx -y @modootoday/datalab-extension-mcp browsers add "회사 크롬"
144
- npx -y @modootoday/datalab-extension-mcp browsers list
145
- npx -y @modootoday/datalab-extension-mcp browsers revoke <handle>
143
+ npx -y @modootoday/datalab-extension-mcp@1.8.11 browsers add "회사 크롬"
144
+ npx -y @modootoday/datalab-extension-mcp@1.8.11 browsers list
145
+ npx -y @modootoday/datalab-extension-mcp@1.8.11 browsers revoke <handle>
146
146
  ```
147
147
 
148
148
  `add` 를 실행하면 **연결 토큰을 붙여넣으라고 물어봐요** — 설치할 때와 같은 값이고, 패널의 `복사` 버튼으로 얻습니다. 그러면 코드가 **한 번만** 출력돼요. 커넥터는 그 코드를 저장하지 않고 지문만 남기므로 다시 보여 줄 수 없어요.
@@ -220,7 +220,7 @@ AI가 "그런 기능은 없다" 고 답하면, **"도구 목록을 확인해 보
220
220
  - **연결이 사라졌을 때** — 처음 실행했던 그 명령을 한 번 더 실행하면 안전하게 다시 등록돼요. (일부 AI 앱이 설정 파일을 정리하며 연결을 지우기도 해요.)
221
221
 
222
222
  ```
223
- npx -y @modootoday/datalab-extension-mcp@1.8.10 install
223
+ npx -y @modootoday/datalab-extension-mcp@1.8.11 install
224
224
  ```
225
225
 
226
226
  - **`'npx'을(는) 내부 또는 외부 명령...`** — Node.js가 없는 거예요. 위 2단계의 "Node.js가 없다면" 한 줄 설치가 가장 간단해요.
@@ -250,7 +250,7 @@ AI가 "그런 기능은 없다" 고 답하면, **"도구 목록을 확인해 보
250
250
  1. 터미널에서 아래 명령을 실행해요(AI 앱에 등록된 설정을 지워 줘요).
251
251
 
252
252
  ```
253
- npx -y @modootoday/datalab-extension-mcp@1.8.10 uninstall
253
+ npx -y @modootoday/datalab-extension-mcp@1.8.11 uninstall
254
254
  ```
255
255
 
256
256
  2. 확장 설정에서 **MCP 연결을 꺼요.** 패널이 브릿지라, 꺼 두면 어딘가 설정 줄이 남아 있어도 AI 앱이 아무것도 조회할 수 없어요.
@@ -1,12 +1,12 @@
1
1
  import {
2
2
  DEFAULT_HOST,
3
3
  DEFAULT_PORT
4
- } from "./chunk-DOPJ4OZ4.js";
4
+ } from "./chunk-Q6SOGGKF.js";
5
5
  import {
6
6
  TOKEN_PROMPT_GUIDE,
7
7
  TOKEN_PROMPT_QUESTION,
8
8
  TOKEN_REQUIRED_NON_INTERACTIVE
9
- } from "./chunk-5S7RFXIU.js";
9
+ } from "./chunk-S6D7YVBB.js";
10
10
  import "./chunk-MLKGABMK.js";
11
11
 
12
12
  // src/browsers.ts
@@ -105,7 +105,7 @@ async function runBrowsers(argv, deps) {
105
105
  return 0;
106
106
  }
107
107
  for (const row of rows) {
108
- const state = row.browserId === null ? "waiting" : "connected";
108
+ const state = row.browserId === null ? "waiting" : row.connected === true ? "connected" : "detached";
109
109
  deps.out(`${row.handle} ${state.padEnd(9)} ${row.label}`);
110
110
  }
111
111
  return 0;
@@ -4,7 +4,7 @@ import {
4
4
  mediaToolDescriptors,
5
5
  spawnDaemon,
6
6
  tryConnect
7
- } from "./chunk-DOPJ4OZ4.js";
7
+ } from "./chunk-Q6SOGGKF.js";
8
8
  import {
9
9
  __export
10
10
  } from "./chunk-MLKGABMK.js";
@@ -14583,6 +14583,14 @@ var HelloSchema = external_exports.object({
14583
14583
  */
14584
14584
  capabilities: external_exports.array(external_exports.string()).default([])
14585
14585
  });
14586
+ var BRIDGE_AUTH_CAPABILITY = "bridge-auth-bearer";
14587
+ var BROWSER_ROUTING_CAPABILITY = "browser-routing-v1";
14588
+ var LOCAL_FILE_REQUEST_CAPABILITY = "local-file-request-v1";
14589
+ var REQUIRED_BROWSER_CAPABILITIES = Object.freeze([
14590
+ BRIDGE_AUTH_CAPABILITY,
14591
+ BROWSER_ROUTING_CAPABILITY,
14592
+ LOCAL_FILE_REQUEST_CAPABILITY
14593
+ ]);
14586
14594
  var HelloAckSchema = external_exports.object({
14587
14595
  t: external_exports.literal("hello_ack"),
14588
14596
  /** The single version both sides will use for this session. */
@@ -14603,7 +14611,9 @@ var HelloNackReasonSchema = external_exports.enum([
14603
14611
  /** This browser already has an active event stream. */
14604
14612
  "session_active",
14605
14613
  /** Root authority cannot open a browser session. */
14606
- "browser_registration_required"
14614
+ "browser_registration_required",
14615
+ /** This panel predates mandatory browser routing and file permits. */
14616
+ "browser_capability_required"
14607
14617
  ]);
14608
14618
  var BrowserRoutingSchema = external_exports.object({
14609
14619
  targetBrowserId: external_exports.string(),
@@ -15033,6 +15043,7 @@ var REASON_POLICY = Object.freeze({
15033
15043
  // --- The request itself must change ---
15034
15044
  unknown_tool: Object.freeze({ retryable: false }),
15035
15045
  invalid_args: Object.freeze({ retryable: false }),
15046
+ browser_required: Object.freeze({ retryable: false }),
15036
15047
  tool_closed: Object.freeze({ retryable: false }),
15037
15048
  missing_in_build: Object.freeze({ retryable: false }),
15038
15049
  // --- A human must act; re-calling only nags them ---
@@ -15446,7 +15457,7 @@ var BRIDGE_USER_MESSAGES = Object.freeze({
15446
15457
  panelClosed: "\uD06C\uB86C\uC5D0\uC11C \uB370\uC774\uD130\uB7A9\uD234\uC988 \uD328\uB110\uC774 \uB2EB\uD600 \uC788\uC5B4\uC694. \uD06C\uB86C\uC744 \uC5F4\uACE0 \uB370\uC774\uD130\uB7A9\uD234\uC988 \uD328\uB110\uC744 \uC5F4\uC5B4 \uB450\uBA74 \uBC14\uB85C \uB3D9\uC791\uD574\uC694.",
15447
15458
  /** The panel accepted the call but never answered inside the deadline. */
15448
15459
  toolTimeout: "\uBE0C\uB77C\uC6B0\uC800 \uC751\uB2F5\uC774 \uB2A6\uC5B4\uC9C0\uACE0 \uC788\uC5B4\uC694. \uD06C\uB86C\uC5D0\uC11C \uB124\uC774\uBC84\uC5D0 \uB85C\uADF8\uC778\uB418\uC5B4 \uC788\uB294\uC9C0 \uD655\uC778\uD558\uACE0 \uB2E4\uC2DC \uC2DC\uB3C4\uD574 \uC8FC\uC138\uC694.",
15449
- /** The session hit the per-session rate limit. */
15460
+ /** The daemon request budget is empty. */
15450
15461
  rateLimited: "\uC694\uCCAD\uC774 \uB108\uBB34 \uC7A6\uC544\uC694. \uC7A0\uAE50 \uAE30\uB2E4\uB838\uB2E4\uAC00 \uB2E4\uC2DC \uC2DC\uB3C4\uD574 \uC8FC\uC138\uC694.",
15451
15462
  /**
15452
15463
  * The local connector accepted nothing back inside its ceiling. Distinct
@@ -15458,7 +15469,7 @@ var BRIDGE_USER_MESSAGES = Object.freeze({
15458
15469
  var BRIDGE_AGENT_HINT = Object.freeze({
15459
15470
  /** Panel closed / not connected — the panel must be open for any tool to run. */
15460
15471
  notConnected: "The Datalab extension side panel is not open, so this tool cannot run right now. Ask the user to open the extension side panel in their browser, then call this tool again.",
15461
- /** Per-session rate limit tripped — transient. */
15472
+ /** Daemon-wide rate limit tripped — transient. */
15462
15473
  rateLimited: "This tool is temporarily rate-limited. Wait a few seconds, then call it again.",
15463
15474
  /** The local connector did not answer at all — retrying immediately will not help. */
15464
15475
  connectorStuck: "The local connector stopped responding, so this tool cannot run right now. Do not retry immediately; ask the user to turn the MCP connection off and on again in the extension side panel."
@@ -1,4 +1,4 @@
1
- // ../mcp-server/dist/chunk-Q4ALBPO7.js
1
+ // ../mcp-server/dist/chunk-377F4BGN.js
2
2
  import { createHash } from "crypto";
3
3
  import { createHash as createHash2, randomUUID } from "crypto";
4
4
  import { mkdir, readFile, readdir, rm, writeFile } from "fs/promises";
@@ -14439,6 +14439,11 @@ var HelloSchema = external_exports.object({
14439
14439
  var BRIDGE_AUTH_CAPABILITY = "bridge-auth-bearer";
14440
14440
  var BROWSER_ROUTING_CAPABILITY = "browser-routing-v1";
14441
14441
  var LOCAL_FILE_REQUEST_CAPABILITY = "local-file-request-v1";
14442
+ var REQUIRED_BROWSER_CAPABILITIES = Object.freeze([
14443
+ BRIDGE_AUTH_CAPABILITY,
14444
+ BROWSER_ROUTING_CAPABILITY,
14445
+ LOCAL_FILE_REQUEST_CAPABILITY
14446
+ ]);
14442
14447
  function bearerToken(header) {
14443
14448
  if (typeof header !== "string") return null;
14444
14449
  const match = /^Bearer (.+)$/i.exec(header.trim());
@@ -14464,7 +14469,9 @@ var HelloNackReasonSchema = external_exports.enum([
14464
14469
  /** This browser already has an active event stream. */
14465
14470
  "session_active",
14466
14471
  /** Root authority cannot open a browser session. */
14467
- "browser_registration_required"
14472
+ "browser_registration_required",
14473
+ /** This panel predates mandatory browser routing and file permits. */
14474
+ "browser_capability_required"
14468
14475
  ]);
14469
14476
  var BrowserRoutingSchema = external_exports.object({
14470
14477
  targetBrowserId: external_exports.string(),
@@ -15024,6 +15031,7 @@ var REASON_POLICY = Object.freeze({
15024
15031
  // --- The request itself must change ---
15025
15032
  unknown_tool: Object.freeze({ retryable: false }),
15026
15033
  invalid_args: Object.freeze({ retryable: false }),
15034
+ browser_required: Object.freeze({ retryable: false }),
15027
15035
  tool_closed: Object.freeze({ retryable: false }),
15028
15036
  missing_in_build: Object.freeze({ retryable: false }),
15029
15037
  // --- A human must act; re-calling only nags them ---
@@ -15492,7 +15500,7 @@ var BRIDGE_USER_MESSAGES = Object.freeze({
15492
15500
  panelClosed: "\uD06C\uB86C\uC5D0\uC11C \uB370\uC774\uD130\uB7A9\uD234\uC988 \uD328\uB110\uC774 \uB2EB\uD600 \uC788\uC5B4\uC694. \uD06C\uB86C\uC744 \uC5F4\uACE0 \uB370\uC774\uD130\uB7A9\uD234\uC988 \uD328\uB110\uC744 \uC5F4\uC5B4 \uB450\uBA74 \uBC14\uB85C \uB3D9\uC791\uD574\uC694.",
15493
15501
  /** The panel accepted the call but never answered inside the deadline. */
15494
15502
  toolTimeout: "\uBE0C\uB77C\uC6B0\uC800 \uC751\uB2F5\uC774 \uB2A6\uC5B4\uC9C0\uACE0 \uC788\uC5B4\uC694. \uD06C\uB86C\uC5D0\uC11C \uB124\uC774\uBC84\uC5D0 \uB85C\uADF8\uC778\uB418\uC5B4 \uC788\uB294\uC9C0 \uD655\uC778\uD558\uACE0 \uB2E4\uC2DC \uC2DC\uB3C4\uD574 \uC8FC\uC138\uC694.",
15495
- /** The session hit the per-session rate limit. */
15503
+ /** The daemon request budget is empty. */
15496
15504
  rateLimited: "\uC694\uCCAD\uC774 \uB108\uBB34 \uC7A6\uC544\uC694. \uC7A0\uAE50 \uAE30\uB2E4\uB838\uB2E4\uAC00 \uB2E4\uC2DC \uC2DC\uB3C4\uD574 \uC8FC\uC138\uC694.",
15497
15505
  /**
15498
15506
  * The local connector accepted nothing back inside its ceiling. Distinct
@@ -15504,7 +15512,7 @@ var BRIDGE_USER_MESSAGES = Object.freeze({
15504
15512
  var BRIDGE_AGENT_HINT = Object.freeze({
15505
15513
  /** Panel closed / not connected — the panel must be open for any tool to run. */
15506
15514
  notConnected: "The Datalab extension side panel is not open, so this tool cannot run right now. Ask the user to open the extension side panel in their browser, then call this tool again.",
15507
- /** Per-session rate limit tripped — transient. */
15515
+ /** Daemon-wide rate limit tripped — transient. */
15508
15516
  rateLimited: "This tool is temporarily rate-limited. Wait a few seconds, then call it again.",
15509
15517
  /** The local connector did not answer at all — retrying immediately will not help. */
15510
15518
  connectorStuck: "The local connector stopped responding, so this tool cannot run right now. Do not retry immediately; ask the user to turn the MCP connection off and on again in the extension side panel."
@@ -15634,7 +15642,6 @@ var Bridge = class {
15634
15642
  #idTag = daemonIdTag();
15635
15643
  #sessionSeq = 0;
15636
15644
  #defaultKey = null;
15637
- #retireSession = null;
15638
15645
  #bucketTokens = 0;
15639
15646
  #bucketRefilledAt = 0;
15640
15647
  // Last catalog the panel reported, kept across disconnects: many hosts cache
@@ -15643,12 +15650,9 @@ var Bridge = class {
15643
15650
  #cachedTools = [];
15644
15651
  constructor(deps) {
15645
15652
  this.#deps = deps;
15646
- }
15647
- bindRetireSession(retire) {
15648
- if (this.#retireSession !== null) {
15649
- throw new Error("bridge transport retirement is already bound");
15650
- }
15651
- this.#retireSession = retire;
15653
+ const limit = deps.rateLimit ?? DEFAULT_RATE_LIMIT;
15654
+ this.#bucketTokens = limit.capacity;
15655
+ this.#bucketRefilledAt = (deps.now ?? Date.now)();
15652
15656
  }
15653
15657
  /**
15654
15658
  * The session a request with no browser named goes to. Map preserves
@@ -15674,7 +15678,7 @@ var Bridge = class {
15674
15678
  */
15675
15679
  connectedBrowsers() {
15676
15680
  const keys = [...this.#sessions.values()].filter((entry) => entry.streamActive).map((entry) => entry.key);
15677
- const primary = this.#defaultKey === null ? this.#primary()?.key ?? null : this.#defaultKey;
15681
+ const primary = this.#primary()?.key ?? null;
15678
15682
  return keys.map((id) => ({
15679
15683
  id,
15680
15684
  name: this.#deps.browserLabels.get(id) ?? null,
@@ -15946,6 +15950,32 @@ var Bridge = class {
15946
15950
  message: idCheck.message
15947
15951
  };
15948
15952
  }
15953
+ const negotiated = negotiateProtocol(parsed.data.protocolVersions);
15954
+ if (!negotiated.ok) {
15955
+ return {
15956
+ t: "hello_nack",
15957
+ reason: negotiated.reason,
15958
+ supported: negotiated.supported,
15959
+ message: negotiated.message
15960
+ };
15961
+ }
15962
+ const bearerRequired = parsed.data.capabilities.includes(
15963
+ BRIDGE_AUTH_CAPABILITY
15964
+ );
15965
+ const routingRequired = parsed.data.capabilities.includes(
15966
+ BROWSER_ROUTING_CAPABILITY
15967
+ );
15968
+ const fileRequestBound = parsed.data.capabilities.includes(
15969
+ LOCAL_FILE_REQUEST_CAPABILITY
15970
+ );
15971
+ if (!bearerRequired || !routingRequired || !fileRequestBound) {
15972
+ return {
15973
+ t: "hello_nack",
15974
+ reason: "browser_capability_required",
15975
+ supported: [],
15976
+ message: "This browser panel is outdated. Close and reopen the side panel before reconnecting."
15977
+ };
15978
+ }
15949
15979
  const tokenHash = digestToken(parsed.data.token);
15950
15980
  const browserId = parsed.data.browserId;
15951
15981
  if (browserId !== void 0 && browserId.length > 0 && !this.#deps.claimedBrowsers.has(browserId)) {
@@ -15974,19 +16004,15 @@ var Bridge = class {
15974
16004
  message: "The root token is management-only. Register this browser with its own token."
15975
16005
  };
15976
16006
  }
15977
- const negotiated = negotiateProtocol(parsed.data.protocolVersions);
15978
- if (!negotiated.ok) {
15979
- return {
15980
- t: "hello_nack",
15981
- reason: negotiated.reason,
15982
- supported: negotiated.supported,
15983
- message: negotiated.message
15984
- };
15985
- }
15986
16007
  const key = credential.browserId;
15987
16008
  const held = this.#sessions.get(key);
15988
16009
  if (held?.streamActive) {
15989
- this.#retireSession?.(key, held.session.sessionId);
16010
+ return {
16011
+ t: "hello_nack",
16012
+ reason: "session_active",
16013
+ supported: [],
16014
+ message: "This browser already has an active side-panel session."
16015
+ };
15990
16016
  }
15991
16017
  const session = {
15992
16018
  // A counter, not a timestamp: a panel reload produces two handshakes
@@ -15996,28 +16022,17 @@ var Bridge = class {
15996
16022
  protocolVersion: negotiated.version,
15997
16023
  extensionVersion: parsed.data.extensionVersion
15998
16024
  };
15999
- const routingRequired = credential.kind === "claimed" && parsed.data.capabilities.includes(BROWSER_ROUTING_CAPABILITY);
16000
- const fileRequestBound = parsed.data.capabilities.includes(
16001
- LOCAL_FILE_REQUEST_CAPABILITY
16002
- );
16003
16025
  this.#installSession(key, session, {
16004
16026
  tokenHash,
16005
16027
  routingRequired,
16006
16028
  fileRequestBound
16007
16029
  });
16008
- const limit = this.#deps.rateLimit ?? DEFAULT_RATE_LIMIT;
16009
- this.#bucketTokens = limit.capacity;
16010
- this.#bucketRefilledAt = (this.#deps.now ?? Date.now)();
16011
16030
  return {
16012
16031
  t: "hello_ack",
16013
16032
  protocolVersion: negotiated.version,
16014
16033
  serverVersion: this.#deps.serverVersion,
16015
16034
  sessionId: session.sessionId,
16016
- capabilities: [
16017
- BRIDGE_AUTH_CAPABILITY,
16018
- ...routingRequired ? [BROWSER_ROUTING_CAPABILITY] : [],
16019
- ...fileRequestBound ? [LOCAL_FILE_REQUEST_CAPABILITY] : []
16020
- ]
16035
+ capabilities: [...REQUIRED_BROWSER_CAPABILITIES]
16021
16036
  };
16022
16037
  }
16023
16038
  /**
@@ -16209,7 +16224,7 @@ var Bridge = class {
16209
16224
  };
16210
16225
  }
16211
16226
  /**
16212
- * Refill-then-take on the session's token bucket. The refill is computed
16227
+ * Refill-then-take on the daemon's token bucket. The refill is computed
16213
16228
  * lazily on each take, so there is no timer to leak and an injected clock
16214
16229
  * keeps it testable without waiting real seconds.
16215
16230
  */
@@ -17137,13 +17152,6 @@ function createHttpBridge(opts) {
17137
17152
  opts.bridge.disconnect(key);
17138
17153
  if (wasPrimary) scheduleToolsDrop();
17139
17154
  };
17140
- opts.bridge.bindRetireSession((key, sessionId) => {
17141
- const stream = streams.get(key);
17142
- if (!stream || stream.sessionId !== sessionId) return;
17143
- const wasPrimary = key === opts.bridge.sessionKey;
17144
- removeStream(key)?.end();
17145
- if (wasPrimary) scheduleToolsDrop();
17146
- });
17147
17155
  const server = createServer((req, res) => {
17148
17156
  const url2 = new URL(req.url ?? "/", `http://${host}`);
17149
17157
  const origin = req.headers.origin;
@@ -17286,9 +17294,16 @@ function createHttpBridge(opts) {
17286
17294
  }
17287
17295
  const action = b?.["action"];
17288
17296
  if (action === "list") {
17297
+ const active = new Set(
17298
+ opts.bridge.connectedBrowsers().map((row) => row.id)
17299
+ );
17300
+ const browsers = (await registry2.list()).map((row) => ({
17301
+ ...row,
17302
+ connected: row.browserId !== null && active.has(row.browserId)
17303
+ }));
17289
17304
  json2(res, 200, {
17290
17305
  ok: true,
17291
- browsers: await registry2.list(),
17306
+ browsers,
17292
17307
  registryStatus: registry2.status()
17293
17308
  });
17294
17309
  return;
@@ -17854,24 +17869,54 @@ function matches(claim, handle) {
17854
17869
  function createBrowserRegistry(opts) {
17855
17870
  const now = opts.now ?? (() => /* @__PURE__ */ new Date());
17856
17871
  const revoked = /* @__PURE__ */ new Set();
17857
- const readCurrent = () => {
17858
- const read = readLedger(opts.path);
17859
- if (read.state === "unreadable") {
17860
- throw new Error(`browser registry is unreadable: ${read.reason}`);
17861
- }
17862
- const rows = read.state === "ok" ? [...read.rows] : [];
17863
- if (!opts.forbiddenTokenHash) return { rows, quarantined: [] };
17864
- const quarantined = rows.filter(
17872
+ const initial = readLedger(opts.path);
17873
+ const unreadable = initial.state === "unreadable" ? initial.reason : null;
17874
+ let rows = initial.state === "ok" ? initial.rows.map((row) => ({ ...row })) : [];
17875
+ let mutationTail = Promise.resolve();
17876
+ let pendingMutations = 0;
17877
+ const view = (source = rows) => {
17878
+ if (unreadable !== null) {
17879
+ throw new Error(`browser registry is unreadable: ${unreadable}`);
17880
+ }
17881
+ if (!opts.forbiddenTokenHash) return { rows: [...source], quarantined: [] };
17882
+ const quarantined = source.filter(
17865
17883
  (row) => timingSafeEqual(row.tokenHash, opts.forbiddenTokenHash)
17866
17884
  );
17867
17885
  return {
17868
- rows: rows.filter(
17886
+ rows: source.filter(
17869
17887
  (row) => !timingSafeEqual(row.tokenHash, opts.forbiddenTokenHash)
17870
17888
  ),
17871
17889
  quarantined
17872
17890
  };
17873
17891
  };
17874
- const current = () => readCurrent().rows;
17892
+ const current = () => view().rows;
17893
+ const serialize = (work) => {
17894
+ pendingMutations += 1;
17895
+ const result = mutationTail.then(work, work);
17896
+ mutationTail = result.then(
17897
+ () => {
17898
+ pendingMutations -= 1;
17899
+ },
17900
+ () => {
17901
+ pendingMutations -= 1;
17902
+ }
17903
+ );
17904
+ return result;
17905
+ };
17906
+ const transact = (propose) => {
17907
+ if (unreadable !== null) {
17908
+ return Promise.reject(
17909
+ new Error(`browser registry is unreadable: ${unreadable}`)
17910
+ );
17911
+ }
17912
+ return serialize(async () => {
17913
+ const proposal = propose(rows.map((row) => ({ ...row })));
17914
+ await writeLedger(opts.path, proposal.next);
17915
+ rows = proposal.next;
17916
+ proposal.commit?.();
17917
+ return proposal.result;
17918
+ });
17919
+ };
17875
17920
  return {
17876
17921
  async add(label) {
17877
17922
  const token = mintToken();
@@ -17881,8 +17926,10 @@ function createBrowserRegistry(opts) {
17881
17926
  label,
17882
17927
  createdAt: now().toISOString()
17883
17928
  };
17884
- await writeLedger(opts.path, [...current(), claim]);
17885
- return { handle: handleOf(claim), token };
17929
+ return transact((snapshot) => ({
17930
+ next: [...snapshot, claim],
17931
+ result: { handle: handleOf(claim), token }
17932
+ }));
17886
17933
  },
17887
17934
  async list() {
17888
17935
  return current().map((claim) => ({
@@ -17893,6 +17940,7 @@ function createBrowserRegistry(opts) {
17893
17940
  }));
17894
17941
  },
17895
17942
  claim(browserId, tokenHash) {
17943
+ if (pendingMutations > 0) return false;
17896
17944
  if (opts.claimed.has(browserId)) return false;
17897
17945
  for (const held of opts.claimed.values()) {
17898
17946
  if (timingSafeEqual(held, tokenHash)) return false;
@@ -17907,47 +17955,75 @@ function createBrowserRegistry(opts) {
17907
17955
  (claim) => claim.browserId === null && timingSafeEqual(claim.tokenHash, tokenHash)
17908
17956
  );
17909
17957
  if (!row) return false;
17910
- row.browserId = browserId;
17958
+ const stored = rows.find((claim) => claim.tokenHash === row.tokenHash);
17959
+ if (!stored) return false;
17960
+ stored.browserId = browserId;
17911
17961
  opts.claimed.set(browserId, row.tokenHash);
17912
- void writeLedger(opts.path, claims).catch(() => {
17913
- });
17962
+ opts.onClaim?.(browserId, stored.label);
17963
+ const snapshot = rows.map((entry) => ({ ...entry }));
17964
+ void serialize(async () => {
17965
+ try {
17966
+ await writeLedger(opts.path, snapshot);
17967
+ } catch (error51) {
17968
+ if (stored.browserId === browserId) stored.browserId = null;
17969
+ if (opts.claimed.get(browserId) === row.tokenHash) {
17970
+ opts.claimed.delete(browserId);
17971
+ opts.onRevoke?.(browserId);
17972
+ }
17973
+ throw error51;
17974
+ }
17975
+ }).catch(() => void 0);
17914
17976
  return true;
17915
17977
  },
17916
17978
  async revoke(handle) {
17917
- const claims = current();
17918
- const kept = claims.filter((claim) => !matches(claim, handle));
17919
- if (kept.length === claims.length) return false;
17920
- await writeLedger(opts.path, kept);
17921
- for (const claim of claims) {
17922
- if (!matches(claim, handle)) continue;
17923
- revoked.add(claim.tokenHash);
17924
- if (claim.browserId !== null) {
17925
- opts.claimed.delete(claim.browserId);
17926
- opts.onRevoke?.(claim.browserId);
17979
+ return transact((snapshot) => {
17980
+ const claims = view(snapshot).rows;
17981
+ const removed = claims.filter((claim) => matches(claim, handle));
17982
+ if (removed.length === 0) {
17983
+ return { next: snapshot, result: false };
17927
17984
  }
17928
- }
17929
- return true;
17985
+ const removedHashes = new Set(removed.map((claim) => claim.tokenHash));
17986
+ return {
17987
+ next: snapshot.filter((claim) => !removedHashes.has(claim.tokenHash)),
17988
+ result: true,
17989
+ commit: () => {
17990
+ for (const claim of removed) {
17991
+ revoked.add(claim.tokenHash);
17992
+ if (claim.browserId !== null) {
17993
+ opts.claimed.delete(claim.browserId);
17994
+ opts.onRevoke?.(claim.browserId);
17995
+ }
17996
+ }
17997
+ }
17998
+ };
17999
+ });
17930
18000
  },
17931
18001
  async repair() {
17932
- const view = readCurrent();
17933
- if (view.quarantined.length === 0) return { removed: [] };
17934
- await writeLedger(opts.path, view.rows);
17935
- return { removed: view.quarantined.map(handleOf) };
18002
+ return transact((snapshot) => {
18003
+ const currentView = view(snapshot);
18004
+ const removed = currentView.quarantined.map(handleOf);
18005
+ const removedHashes = new Set(
18006
+ currentView.quarantined.map((row) => row.tokenHash)
18007
+ );
18008
+ return {
18009
+ next: snapshot.filter((row) => !removedHashes.has(row.tokenHash)),
18010
+ result: { removed }
18011
+ };
18012
+ });
17936
18013
  },
17937
18014
  status() {
17938
- const read = readLedger(opts.path);
17939
- if (read.state === "unreadable") {
18015
+ if (unreadable !== null) {
17940
18016
  return {
17941
18017
  state: "disabled",
17942
18018
  error: "browser_registry_unreadable",
17943
- detail: read.reason
18019
+ detail: unreadable
17944
18020
  };
17945
18021
  }
17946
- if (read.state === "ok" && opts.forbiddenTokenHash && ledgerOverlapsToken(read.rows, opts.forbiddenTokenHash)) {
18022
+ if (opts.forbiddenTokenHash && ledgerOverlapsToken(rows, opts.forbiddenTokenHash)) {
17947
18023
  return {
17948
18024
  state: "degraded",
17949
18025
  error: "browser_registry_root_authority_overlap",
17950
- quarantinedHandles: read.rows.filter(
18026
+ quarantinedHandles: rows.filter(
17951
18027
  (row) => timingSafeEqual(row.tokenHash, opts.forbiddenTokenHash)
17952
18028
  ).map(handleOf),
17953
18029
  recovery: "browsers repair"
@@ -18016,20 +18092,7 @@ function runDaemon(env, deps = {}) {
18016
18092
  claimedBrowsers,
18017
18093
  browserLabels,
18018
18094
  browserRegistryStatus: () => registry2.status(),
18019
- claimBrowser: (browserId, tokenHash) => {
18020
- const took = registry2.claim(browserId, tokenHash);
18021
- if (took) {
18022
- const now = readLedger(ledgerPath(deps.home));
18023
- if (now.state === "ok") {
18024
- for (const row of now.rows) {
18025
- if (row.browserId === browserId) {
18026
- browserLabels.set(browserId, row.label);
18027
- }
18028
- }
18029
- }
18030
- }
18031
- return took;
18032
- },
18095
+ claimBrowser: (browserId, tokenHash) => registry2.claim(browserId, tokenHash),
18033
18096
  wasRevoked: (tokenHash) => registry2.wasRevoked(tokenHash),
18034
18097
  log
18035
18098
  });
@@ -18038,6 +18101,7 @@ function runDaemon(env, deps = {}) {
18038
18101
  const registry2 = createBrowserRegistry({
18039
18102
  path: ledgerPath(deps.home),
18040
18103
  claimed: claimedBrowsers,
18104
+ onClaim: (browserId, label) => browserLabels.set(browserId, label),
18041
18105
  forbiddenTokenHash: rootTokenHash,
18042
18106
  onRevoke: (browserId) => http.disconnect(browserId)
18043
18107
  });
@@ -1109,7 +1109,7 @@ async function removeSkills(io, detectedIds, slugs) {
1109
1109
  var RESTART_NOTICE = "\uB9C8\uC9C0\uB9C9 \uD55C \uB2E8\uACC4: AI \uC571\uC744 \uC644\uC804\uD788 \uC885\uB8CC\uD588\uB2E4\uAC00 \uB2E4\uC2DC \uC2E4\uD589\uD574 \uC8FC\uC138\uC694.\n(Windows: \uC791\uC5C5 \uD45C\uC2DC\uC904 \uD2B8\uB808\uC774 \uC544\uC774\uCF58\uC5D0\uC11C \uC885\uB8CC)";
1110
1110
  var NOTHING_CHANGED = "\uC544\uBB34\uAC83\uB3C4 \uBC14\uAFB8\uC9C0 \uC54A\uC558\uC5B4\uC694.";
1111
1111
  var UNINSTALL_DONE = "\uC815\uB9AC\uAC00 \uB05D\uB0AC\uC5B4\uC694.";
1112
- var UNINSTALL_TOKEN_REMINDER = "\uC5F0\uACB0 \uD1A0\uD070\uAE4C\uC9C0 \uC5C6\uC560\uB824\uBA74 \uB370\uC774\uD130\uB7A9\uD234\uC988 \uD328\uB110\uC5D0\uC11C '\uC5F0\uB3D9 \uD574\uC81C' \uBC84\uD2BC\uC744 \uB20C\uB7EC \uC8FC\uC138\uC694. \uD3D0\uAE30\uB41C \uD1A0\uD070\uC740 \uC124\uC815\uC5D0 \uB0A8\uC740 \uD56D\uBAA9\uC744 \uC804\uBD80 \uBB34\uB825\uD654\uD574\uC694.";
1112
+ var UNINSTALL_TOKEN_REMINDER = "\uC774 \uBE0C\uB77C\uC6B0\uC800\uC758 \uB85C\uCEEC \uC5F0\uACB0 \uC815\uBCF4\uB3C4 \uC9C0\uC6B0\uB824\uBA74 \uB370\uC774\uD130\uB7A9\uD234\uC988 \uD328\uB110\uC5D0\uC11C '\uC774 \uBE0C\uB77C\uC6B0\uC800 \uC5F0\uACB0 \uB044\uAE30'\uB97C \uB20C\uB7EC \uC8FC\uC138\uC694. \uC6CC\uD06C\uBCA4\uCE58 \uB4F1\uB85D\uC740 \uBE0C\uB77C\uC6B0\uC800 \uBAA9\uB85D\uC5D0\uC11C \uBCC4\uB3C4\uB85C \uB0B4\uB9B4 \uC218 \uC788\uC5B4\uC694.";
1113
1113
  var PERMISSION_DENIED_HINT = "\uD30C\uC77C \uAD8C\uD55C \uB54C\uBB38\uC5D0 \uC124\uC815\uC744 \uC4F8 \uC218 \uC5C6\uC5C8\uC5B4\uC694. \uD30C\uC77C \uAD8C\uD55C\uC744 \uD655\uC778\uD55C \uB4A4 \uAC19\uC740 \uBA85\uB839\uC5B4\uB97C \uB2E4\uC2DC \uC2E4\uD589\uD574 \uC8FC\uC138\uC694.";
1114
1114
  var NO_HOSTS_DETECTED = "\uC5F0\uACB0\uD560 \uC218 \uC788\uB294 AI \uD504\uB85C\uADF8\uB7A8\uC744 \uCC3E\uC9C0 \uBABB\uD588\uC5B4\uC694.";
1115
1115
  var SUPPORTED_APPS_HEADER = "\uC9C0\uC6D0\uD558\uB294 \uD504\uB85C\uADF8\uB7A8\uACFC \uB2E4\uC6B4\uB85C\uB4DC \uC8FC\uC18C\uC608\uC694:";
package/dist/cli.js CHANGED
@@ -2,10 +2,10 @@
2
2
  import {
3
3
  dispatchCli,
4
4
  runAdapter
5
- } from "./chunk-YB7L4SZC.js";
5
+ } from "./chunk-65FDP6LF.js";
6
6
  import {
7
7
  runDaemon
8
- } from "./chunk-DOPJ4OZ4.js";
8
+ } from "./chunk-Q6SOGGKF.js";
9
9
  import "./chunk-MLKGABMK.js";
10
10
 
11
11
  // src/cli.ts
@@ -17,7 +17,7 @@ function log(message) {
17
17
  `);
18
18
  }
19
19
  async function runInstaller(sub, argv) {
20
- const { createNodeIo, runInstall, runUninstall } = await import("./dist-JICXADYR.js");
20
+ const { createNodeIo, runInstall, runUninstall } = await import("./dist-63JYX33Z.js");
21
21
  const opts = { version: VERSION };
22
22
  for (let i = 0; i < argv.length; i += 1) {
23
23
  const flag = argv[i];
@@ -62,8 +62,8 @@ async function main() {
62
62
  // Loaded lazily, like the installer: the adapter path is what an MCP host
63
63
  // spawns and must not pay for a command only an operator types.
64
64
  browsers: async (argv) => {
65
- const { runBrowsers } = await import("./browsers-AGEUPMJA.js");
66
- const { createNodeIo } = await import("./dist-JICXADYR.js");
65
+ const { runBrowsers } = await import("./browsers-EMS5V4M4.js");
66
+ const { createNodeIo } = await import("./dist-63JYX33Z.js");
67
67
  const io = createNodeIo();
68
68
  const code = await runBrowsers(argv, {
69
69
  env: process.env,
@@ -88,7 +88,7 @@ async function main() {
88
88
  attachSkill,
89
89
  detachSkill,
90
90
  skillsPayloadRoot
91
- } = await import("./dist-JICXADYR.js");
91
+ } = await import("./dist-63JYX33Z.js");
92
92
  const io = createNodeIo();
93
93
  const root = skillsPayloadRoot();
94
94
  const code = await runSkills(argv, {
@@ -49,7 +49,7 @@ import {
49
49
  upsertServerKey,
50
50
  validateInstallOptions,
51
51
  validateUninstallOptions
52
- } from "./chunk-5S7RFXIU.js";
52
+ } from "./chunk-S6D7YVBB.js";
53
53
  import "./chunk-MLKGABMK.js";
54
54
  export {
55
55
  BACKUP_KEEP,
package/dist/index.js CHANGED
@@ -3,8 +3,8 @@ import {
3
3
  dispatchCli,
4
4
  ensureDaemonRunning,
5
5
  runAdapter
6
- } from "./chunk-YB7L4SZC.js";
7
- import "./chunk-DOPJ4OZ4.js";
6
+ } from "./chunk-65FDP6LF.js";
7
+ import "./chunk-Q6SOGGKF.js";
8
8
  import "./chunk-MLKGABMK.js";
9
9
  export {
10
10
  createAdapterServer,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@modootoday/datalab-extension-mcp",
3
- "version": "1.8.10",
3
+ "version": "1.8.11",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",