@vanillagreen/pi-claude-bridge 3.2.2 → 4.0.0

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.md CHANGED
@@ -2,8 +2,8 @@
2
2
 
3
3
  Works with OAuth subscription, no API key, no errors.
4
4
 
5
- ![Claude bridge demo response](https://raw.githubusercontent.com/vanillagreencom/vstack/main/pi-extensions/pi-claude-bridge/assets/bridge-demo.png)
6
- ![Pi Claude settings panel](https://raw.githubusercontent.com/vanillagreencom/vstack/main/pi-extensions/pi-claude-bridge/assets/settings-panel.png)
5
+ ![Claude bridge demo response](https://raw.githubusercontent.com/vanillagreencom/kendex/main/pi-extensions/pi-claude-bridge/assets/bridge-demo.png)
6
+ ![Pi Claude settings panel](https://raw.githubusercontent.com/vanillagreencom/kendex/main/pi-extensions/pi-claude-bridge/assets/settings-panel.png)
7
7
 
8
8
  Run Claude Code as the `pi-claude` Pi provider while keeping Pi's tools and TUI.
9
9
 
@@ -33,11 +33,11 @@ Via [npm](https://www.npmjs.com/package/@vanillagreen/pi-claude-bridge):
33
33
  pi install npm:@vanillagreen/pi-claude-bridge
34
34
  ```
35
35
 
36
- Via [vstack](https://github.com/vanillagreencom/vstack):
36
+ Via [kendex](https://github.com/vanillagreencom/kendex):
37
37
 
38
38
  ```bash
39
- cargo install --git https://github.com/vanillagreencom/vstack.git vstack
40
- vstack add vanillagreencom/vstack --pi-extension pi-claude-bridge --harness pi -y
39
+ cargo install --git https://github.com/vanillagreencom/kendex.git kendex
40
+ kendex add vanillagreencom/kendex --pi-extension pi-claude-bridge --harness pi -y
41
41
  ```
42
42
 
43
43
  Restart Pi after installation.
@@ -50,7 +50,7 @@ Extra Pi context is off by default. Enable per item in the extension manager whe
50
50
 
51
51
  ## Settings
52
52
 
53
- Open `/extensions:settings`; settings appear under the **Pi Claude** tab. Project settings in `.pi/settings.json` apply only after Pi marks the workspace trusted; before trust, vstack Pi extensions read user/global settings only. The bridge also reads `claude-bridge.json` (`~/.pi/agent/claude-bridge.json`, and `.pi/claude-bridge.json` in a trusted project). Settings the bridge takes from one of those files are shown with the file that supplies them, so the editor reports the value the bridge resolves rather than the default. Changing the setting in the editor writes Pi settings, which take precedence over `claude-bridge.json`.
53
+ Open `/extensions:settings`; settings appear under the **Pi Claude** tab. Project settings in `.pi/settings.json` apply only after Pi marks the workspace trusted; before trust, kendex Pi extensions read user/global settings only. The bridge also reads `claude-bridge.json` (`~/.pi/agent/claude-bridge.json`, and `.pi/claude-bridge.json` in a trusted project). Settings the bridge takes from one of those files are shown with the file that supplies them, so the editor reports the value the bridge resolves rather than the default. Changing the setting in the editor writes Pi settings, which take precedence over `claude-bridge.json`.
54
54
 
55
55
  | Group | Setting | What it does |
56
56
  | --- | --- | --- |
@@ -80,7 +80,7 @@ Connector tools run inside Claude Code rather than in Pi, so Pi shows the model'
80
80
 
81
81
  `/pi-claude:connectors` lists the Claude account's installed claude.ai connectors by asking the account, not the model, so the answer is complete by construction.
82
82
 
83
- Extension-manager settings use flat package-scoped keys under `vstack.extensionManager.config["@vanillagreen/pi-claude-bridge"]` in `settings.json`. Legacy `claude-bridge.json` configuration nests these options under `provider` (prompt-context flags under `promptContext`); the flat keys are accepted there too. Environment variables work with either format. Connectors remain off by default so Pi owns tool execution.
83
+ Extension-manager settings use flat package-scoped keys under `kendex.extensionManager.config["@vanillagreen/pi-claude-bridge"]` in `settings.json`. Legacy `claude-bridge.json` configuration nests these options under `provider` (prompt-context flags under `promptContext`); the flat keys are accepted there too. Environment variables work with either format. Connectors remain off by default so Pi owns tool execution.
84
84
 
85
85
  | Extension-manager key | Env var | Values | Default | What it does |
86
86
  | --- | --- | --- | --- | --- |
package/bundle/index.js CHANGED
@@ -27046,7 +27046,7 @@ function diagDump(label, data) {
27046
27046
 
27047
27047
  // src/config.ts
27048
27048
  var PACKAGE_ID = "@vanillagreen/pi-claude-bridge";
27049
- var EXTERNAL_CONFIG_RESOLVER_SYMBOL = /* @__PURE__ */ Symbol.for("vstack.pi.extension-config-resolver");
27049
+ var EXTERNAL_CONFIG_RESOLVER_SYMBOL = /* @__PURE__ */ Symbol.for("kendex.pi.extension-config-resolver");
27050
27050
  var VALID_EFFORT_LEVELS = /* @__PURE__ */ new Set(["low", "medium", "high", "xhigh", "max"]);
27051
27051
  function expandHome(input) {
27052
27052
  if (input === "~") return homedir();
@@ -27077,13 +27077,13 @@ function projectSettingsPath(cwd) {
27077
27077
  while (true) {
27078
27078
  const candidate = join3(current, ".pi", "settings.json");
27079
27079
  if (existsSync2(candidate)) return candidate;
27080
- if (existsSync2(join3(current, ".pi")) || existsSync2(join3(current, ".git")) || existsSync2(join3(current, ".vstack-lock.json"))) return candidate;
27080
+ if (existsSync2(join3(current, ".pi")) || existsSync2(join3(current, ".git")) || existsSync2(join3(current, ".kendex-lock.json"))) return candidate;
27081
27081
  const parent = dirname2(current);
27082
27082
  if (parent === current) return join3(resolve2(cwd), ".pi", "settings.json");
27083
27083
  current = parent;
27084
27084
  }
27085
27085
  }
27086
- var PROJECT_TRUST_SYMBOL = /* @__PURE__ */ Symbol.for("vstack.pi.project-trust");
27086
+ var PROJECT_TRUST_SYMBOL = /* @__PURE__ */ Symbol.for("kendex.pi.project-trust");
27087
27087
  function projectTrustRegistry() {
27088
27088
  const host = globalThis;
27089
27089
  const existing = host[PROJECT_TRUST_SYMBOL];
@@ -27130,7 +27130,7 @@ function readManagerConfig(cwd) {
27130
27130
  if (!existsSync2(path)) continue;
27131
27131
  try {
27132
27132
  const parsed = JSON.parse(readFileSync2(path, "utf8"));
27133
- const configRoot = asRecord(asRecord(asRecord(parsed?.vstack)?.extensionManager)?.config);
27133
+ const configRoot = asRecord(asRecord(asRecord(parsed?.kendex)?.extensionManager)?.config);
27134
27134
  const config2 = asRecord(configRoot?.[PACKAGE_ID]);
27135
27135
  if (config2) mergeDeep(merged, path === userPath ? config2 : withoutUserScopeOnlyKeys(config2));
27136
27136
  } catch (error51) {
@@ -28118,7 +28118,7 @@ function extractAllToolResults(messages) {
28118
28118
 
28119
28119
  // src/request-lane.ts
28120
28120
  import { AsyncLocalStorage } from "node:async_hooks";
28121
- var REQUEST_LANE_SYMBOL = /* @__PURE__ */ Symbol.for("vstack.pi.claude-bridge.request-lane.v1");
28121
+ var REQUEST_LANE_SYMBOL = /* @__PURE__ */ Symbol.for("kendex.pi.claude-bridge.request-lane.v1");
28122
28122
  function requestLaneStorage() {
28123
28123
  const host = globalThis;
28124
28124
  let storage = host[REQUEST_LANE_SYMBOL];
@@ -28262,7 +28262,7 @@ var QueryContext = class {
28262
28262
  * set endToolUseTurn stamps from the turn's content. A forwarded id is one Pi
28263
28263
  * will execute and answer; it must never be emitted again (a lagging stream
28264
28264
  * replays the same tool_use into the NEXT turn, and per-message turnBlocks
28265
- * dedup cannot see across turns — vstack#1469's duplicate executions), and a
28265
+ * dedup cannot see across turns — kendex#1469's duplicate executions), and a
28266
28266
  * handler waiting on it must be left waiting at the stranded-handler drains.
28267
28267
  * Query-scoped, never reset per message. */
28268
28268
  forwardedToolCallIds = /* @__PURE__ */ new Set();
@@ -28294,7 +28294,7 @@ var QueryContext = class {
28294
28294
  * same reason: a late handler firing after resetToolTracking wiped the
28295
28295
  * per-message records must still be able to exact-match the parked/queued
28296
28296
  * result of ITS OWN call — without stored args the only fallback is
28297
- * sole-same-name, which can hand it a LIVE sibling's id (vstack#1469). */
28297
+ * sole-same-name, which can hand it a LIVE sibling's id (kendex#1469). */
28298
28298
  queryToolArgs = /* @__PURE__ */ new Map();
28299
28299
  claimedToolCallIds = /* @__PURE__ */ new Set();
28300
28300
  deliveredToolResultIds = /* @__PURE__ */ new Set();
@@ -28310,7 +28310,7 @@ var QueryContext = class {
28310
28310
  committedOutput = false;
28311
28311
  /** True when this query holds NO claim on the module-level shared session
28312
28312
  * record: a reentrant (subagent) query, or a foreign-conversation one-shot
28313
- * (vstack#1001). Every shared-record mutation reachable from this context —
28313
+ * (kendex#1001). Every shared-record mutation reachable from this context —
28314
28314
  * reportToolResultMismatch's needsRebuild/forceRotate mark, the cursor
28315
28315
  * advances on the tool-result-delivery and orphaned-result paths — must
28316
28316
  * no-op so the PARENT's record stays untouched. Assigned at fresh-query
@@ -28535,7 +28535,7 @@ var QueryContext = class {
28535
28535
  * mismatch report for the whole query (queued>0 with 0/0 counters and no tool
28536
28536
  * names) and forces a session rebuild per turn. But the boundary does NOT
28537
28537
  * prove the handler gave up — the SDK staggers handler invocations, and the
28538
- * 2026-08-17 deadlock session (vstack#1469) had three of five parallel
28538
+ * 2026-08-17 deadlock session (kendex#1469) had three of five parallel
28539
28539
  * handlers fire after this reap destroyed their results. So the reap parks
28540
28540
  * instead of dropping: reports stay clean, and a late handler still gets its
28541
28541
  * real result through takeQueuedOrParkedResult.
@@ -28601,7 +28601,7 @@ var QueryContext = class {
28601
28601
  };
28602
28602
  }
28603
28603
  };
28604
- var QUERY_LANES_SYMBOL = /* @__PURE__ */ Symbol.for("vstack.pi.claude-bridge.query-lanes.v1");
28604
+ var QUERY_LANES_SYMBOL = /* @__PURE__ */ Symbol.for("kendex.pi.claude-bridge.query-lanes.v1");
28605
28605
  function queryLaneStore() {
28606
28606
  const host = globalThis;
28607
28607
  let store = host[QUERY_LANES_SYMBOL];
@@ -28764,7 +28764,7 @@ function summarizeMissingToolNames(missing) {
28764
28764
  }
28765
28765
 
28766
28766
  // src/bridge-state.ts
28767
- var SHARED_SESSION_LANES_SYMBOL = /* @__PURE__ */ Symbol.for("vstack.pi.claude-bridge.shared-session-lanes.v1");
28767
+ var SHARED_SESSION_LANES_SYMBOL = /* @__PURE__ */ Symbol.for("kendex.pi.claude-bridge.shared-session-lanes.v1");
28768
28768
  function sharedSessionLaneStore() {
28769
28769
  const host = globalThis;
28770
28770
  let store = host[SHARED_SESSION_LANES_SYMBOL];
@@ -28797,7 +28797,7 @@ function clearSharedSessionLanes() {
28797
28797
  store.sessions.clear();
28798
28798
  store.defaultSession = null;
28799
28799
  }
28800
- var STARTED_LANES_SYMBOL = /* @__PURE__ */ Symbol.for("vstack.pi.claude-bridge.started-lanes.v1");
28800
+ var STARTED_LANES_SYMBOL = /* @__PURE__ */ Symbol.for("kendex.pi.claude-bridge.started-lanes.v1");
28801
28801
  function startedLaneStore() {
28802
28802
  const host = globalThis;
28803
28803
  let store = host[STARTED_LANES_SYMBOL];
@@ -44570,7 +44570,7 @@ import { homedir as homedir4 } from "node:os";
44570
44570
  import { join as join8 } from "node:path";
44571
44571
 
44572
44572
  // src/rate-limit.ts
44573
- var RATE_LIMIT_AUTO_RESUME_EVENT = "vstack:rate-limit";
44573
+ var RATE_LIMIT_AUTO_RESUME_EVENT = "kendex:rate-limit";
44574
44574
  var RATE_LIMIT_TOKEN = "\x1B[31m[rate-limit]\x1B[39m";
44575
44575
  var USAGE_LIMIT_PREFIXES = Array.isArray(qO) ? qO : [];
44576
44576
  function coerceMessageText(value) {
@@ -44640,8 +44640,8 @@ function formatAllowedRateLimitWarning(info) {
44640
44640
  }
44641
44641
 
44642
44642
  // src/account-router.ts
44643
- var CLAUDE_ACCOUNT_ROUTER_SYMBOL = /* @__PURE__ */ Symbol.for("vstack.pi.claude-account-router.v1");
44644
- var CLAUDE_BRIDGE_ACCOUNT_HOST_SYMBOL = /* @__PURE__ */ Symbol.for("vstack.pi.claude-bridge.account-host.v1");
44643
+ var CLAUDE_ACCOUNT_ROUTER_SYMBOL = /* @__PURE__ */ Symbol.for("kendex.pi.claude-account-router.v1");
44644
+ var CLAUDE_BRIDGE_ACCOUNT_HOST_SYMBOL = /* @__PURE__ */ Symbol.for("kendex.pi.claude-bridge.account-host.v1");
44645
44645
  function resolveClaudeAccountRouter() {
44646
44646
  const host = globalThis;
44647
44647
  const candidate = host[CLAUDE_ACCOUNT_ROUTER_SYMBOL];
@@ -44920,7 +44920,7 @@ function restoreSharedSessionFromPi(ctx2) {
44920
44920
  });
44921
44921
  debug(`restoreSharedSession: restored ${persisted.sessionId.slice(0, 8)}, cursor=${cursor}, account=${accountProfileId ?? "default"}`);
44922
44922
  }
44923
- var SCHEDULED_PERSISTENCE_SYMBOL = /* @__PURE__ */ Symbol.for("vstack.pi.claude-bridge.scheduled-persistence.v1");
44923
+ var SCHEDULED_PERSISTENCE_SYMBOL = /* @__PURE__ */ Symbol.for("kendex.pi.claude-bridge.scheduled-persistence.v1");
44924
44924
  function scheduledPersistenceTimers() {
44925
44925
  const host = globalThis;
44926
44926
  let store = host[SCHEDULED_PERSISTENCE_SYMBOL];
@@ -45037,7 +45037,7 @@ function verifyWrittenSession2(jsonlPath, expectedSessionId, expectedRecordCount
45037
45037
  safeNotify(
45038
45038
  `Session file issue: ${msg}
45039
45039
  cwd=${displayPath(cwd)} realpath=${displayPath(safeRealpath(cwd))}
45040
- Please copy and paste this message into a new issue at https://github.com/vanillagreencom/vstack/issues/new` + (DEBUG ? ` and attach ${DEBUG_LOG_PATH}` : ` (rerun with CLAUDE_BRIDGE_DEBUG=1 to capture a debug log)`),
45040
+ Please copy and paste this message into a new issue at https://github.com/vanillagreencom/kendex/issues/new` + (DEBUG ? ` and attach ${DEBUG_LOG_PATH}` : ` (rerun with CLAUDE_BRIDGE_DEBUG=1 to capture a debug log)`),
45041
45041
  "warning"
45042
45042
  );
45043
45043
  diagDump("session_verify_fail", { msg, jsonlPath, cwd, realpath: safeRealpath(cwd), claudeConfigDir: claudeDir ?? null });
@@ -45818,7 +45818,7 @@ function commandCwd(ctx2) {
45818
45818
  }
45819
45819
  async function tryOpenExtensionManagerSettings(ctx2) {
45820
45820
  const host = globalThis;
45821
- const openQuickSettings = host[/* @__PURE__ */ Symbol.for("vstack.pi.extension-manager.open-quick-settings")];
45821
+ const openQuickSettings = host[/* @__PURE__ */ Symbol.for("kendex.pi.extension-manager.open-quick-settings")];
45822
45822
  if (typeof openQuickSettings !== "function") return false;
45823
45823
  try {
45824
45824
  await openQuickSettings(ctx2, "@vanillagreen/pi-claude-bridge");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vanillagreen/pi-claude-bridge",
3
- "version": "3.2.2",
3
+ "version": "4.0.0",
4
4
  "description": "Pi provider bridge that runs Claude Code through the Claude Agent SDK, with opt-in forwarding for Pi prompt context.",
5
5
  "type": "module",
6
6
  "keywords": [
@@ -15,9 +15,9 @@
15
15
  "extensions": [
16
16
  "./bundle/index.js"
17
17
  ],
18
- "image": "https://raw.githubusercontent.com/vanillagreencom/vstack/main/pi-extensions/pi-claude-bridge/assets/bridge-demo.png"
18
+ "image": "https://raw.githubusercontent.com/vanillagreencom/kendex/main/pi-extensions/pi-claude-bridge/assets/bridge-demo.png"
19
19
  },
20
- "vstack": {
20
+ "kendex": {
21
21
  "extensionManager": {
22
22
  "displayName": "Pi Claude",
23
23
  "settings": [
@@ -168,12 +168,12 @@
168
168
  ],
169
169
  "repository": {
170
170
  "type": "git",
171
- "url": "git+https://github.com/vanillagreencom/vstack.git",
171
+ "url": "git+https://github.com/vanillagreencom/kendex.git",
172
172
  "directory": "pi-extensions/pi-claude-bridge"
173
173
  },
174
- "homepage": "https://github.com/vanillagreencom/vstack/tree/main/pi-extensions/pi-claude-bridge",
174
+ "homepage": "https://github.com/vanillagreencom/kendex/tree/main/pi-extensions/pi-claude-bridge",
175
175
  "bugs": {
176
- "url": "https://github.com/vanillagreencom/vstack/issues"
176
+ "url": "https://github.com/vanillagreencom/kendex/issues"
177
177
  },
178
178
  "author": "vanillagreen",
179
179
  "publishConfig": {
@@ -13,8 +13,8 @@ import { join } from "node:path";
13
13
  import { debug } from "./debug.js";
14
14
  import { resetTimestampMs } from "./rate-limit.js";
15
15
 
16
- export const CLAUDE_ACCOUNT_ROUTER_SYMBOL = Symbol.for("vstack.pi.claude-account-router.v1");
17
- export const CLAUDE_BRIDGE_ACCOUNT_HOST_SYMBOL = Symbol.for("vstack.pi.claude-bridge.account-host.v1");
16
+ export const CLAUDE_ACCOUNT_ROUTER_SYMBOL = Symbol.for("kendex.pi.claude-account-router.v1");
17
+ export const CLAUDE_BRIDGE_ACCOUNT_HOST_SYMBOL = Symbol.for("kendex.pi.claude-bridge.account-host.v1");
18
18
 
19
19
  export interface ClaudeAccountRoute {
20
20
  profileId: string;
@@ -88,7 +88,7 @@ const TOOL_USE_END_GRACE_MS = 1500;
88
88
 
89
89
  /** End the current pi stream as a tool_use turn boundary. Safe to call when the
90
90
  * turn already ended (no-op). Every end path funnels here, so this is where
91
- * two invariants are enforced by construction (vstack#1469): a block still
91
+ * two invariants are enforced by construction (kendex#1469): a block still
92
92
  * carrying partialJson never ships — Pi executes the done message's content,
93
93
  * and truncated arguments must never execute — and every call that DOES ship
94
94
  * is stamped forwarded so no lagging replay can dispatch it again. */
@@ -105,7 +105,7 @@ export function endToolUseTurn(c: QueryContext): void {
105
105
  }
106
106
  // Every tool call Pi is about to execute from this turn is owed a result and
107
107
  // must never be dispatched again: a lagging stream replays the same tool_use
108
- // into the NEXT turn, whose per-message dedup cannot see it (vstack#1469).
108
+ // into the NEXT turn, whose per-message dedup cannot see it (kendex#1469).
109
109
  for (const block of c.turnOutput.content as Array<{ type?: string; id?: unknown }>) {
110
110
  if (block?.type === "toolCall" && typeof block.id === "string") c.forwardedToolCallIds.add(block.id);
111
111
  }
@@ -148,7 +148,7 @@ export function scheduleToolUseTurnEnd(c: QueryContext, action: () => void, sour
148
148
  * entries would start poisoning mismatch reports — but it does NOT prove the
149
149
  * handler gave up: the SDK staggers handler invocations, and in the 2026-08-17
150
150
  * deadlock session three of five parallel handlers fired after this point
151
- * (vstack#1469). Parked results stay consumable through
151
+ * (kendex#1469). Parked results stay consumable through
152
152
  * takeQueuedOrParkedResult; one that is never consumed belongs to a call the
153
153
  * SDK abandoned client-side (permission denial), which is exactly what the
154
154
  * notice describes.
@@ -187,7 +187,7 @@ export const FINALIZE_MAX_REARMS = 3;
187
187
  * so end the pi stream like the `message_stop` path — with this handler's
188
188
  * schema-validated arguments, never a partial parse — after settling every
189
189
  * sibling whose handler has fired and giving a merely-lagging stream up to
190
- * FINALIZE_MAX_REARMS extra grace periods for the rest (vstack#1469).
190
+ * FINALIZE_MAX_REARMS extra grace periods for the rest (kendex#1469).
191
191
  *
192
192
  * The dead-stream guard is a backstop: the grace timer's own stream-identity
193
193
  * check means this normally never runs after the turn ended. The primary
@@ -205,7 +205,7 @@ export function finalizeToolUseTurnFromMcpInvocation(
205
205
  // be answered — the yield that could have replayed its call was consumed
206
206
  // against a null stream, and the dead-mark below suppresses any replay
207
207
  // that has not happened yet. Failing it now is what turns the observed
208
- // multi-hour session deadlock into one retryable error (vstack#1469).
208
+ // multi-hour session deadlock into one retryable error (kendex#1469).
209
209
  if (failStrandedToolCall(queryCtx, toolCallId)) {
210
210
  debug(`mcp handler: ${toolName} [${toolCallId}] stranded — turn ended before its call reached Pi; resolved with error`);
211
211
  diagDump("tool_handler_stranded", { toolCallId, toolName, site: "finalize-no-stream" });
@@ -221,7 +221,7 @@ export function finalizeToolUseTurnFromMcpInvocation(
221
221
  // with the COMPLETE schema-validated input, so the handler's copy is
222
222
  // authoritative — the streamed partial JSON is by definition behind it.
223
223
  // Settling from the partial is what forwarded `{}`-argument calls that
224
- // Pi then executed and errored (vstack#1469), exactly the divergence
224
+ // Pi then executed and errored (kendex#1469), exactly the divergence
225
225
  // the synthesize branch below never had.
226
226
  block.arguments = mappedArgs;
227
227
  queryCtx.updateToolCallArgs(block.id, block.arguments);
@@ -355,7 +355,7 @@ export function processStreamEvent(
355
355
  if (typeof streamedId === "string" && (c.forwardedToolCallIds.has(streamedId) || c.deadToolCallIds.has(streamedId))) {
356
356
  // A lagging stream replaying a call Pi already executed — or one whose
357
357
  // handler was already failed as stranded — into a later turn. Mirroring
358
- // it would make Pi dispatch it a second time (vstack#1469).
358
+ // it would make Pi dispatch it a second time (kendex#1469).
359
359
  c.suppressedStreamIndexes.add(event.index);
360
360
  debug(`processStreamEvent: tool_use ${streamedId} already ${c.forwardedToolCallIds.has(streamedId) ? "forwarded" : "dead"} — suppressing duplicate stream block`);
361
361
  return;
@@ -364,7 +364,7 @@ export function processStreamEvent(
364
364
  // Same turn, same id: the completed-message yield beat the stream (its
365
365
  // block is already recorded with complete arguments). A second
366
366
  // partialJson copy would ship the id twice in one done message — and
367
- // if its stop never arrives, ship it truncated (vstack#1469).
367
+ // if its stop never arrives, ship it truncated (kendex#1469).
368
368
  c.suppressedStreamIndexes.add(event.index);
369
369
  debug(`processStreamEvent: tool_use ${streamedId} already recorded in this turn — suppressing duplicate stream block`);
370
370
  return;
@@ -494,7 +494,7 @@ function appendMissingToolUsesFromAssistant(
494
494
  // array of a turnOutput that endToolUseTurn already handed Pi BY REFERENCE
495
495
  // in its done event, so a push here appends calls into a delivered message
496
496
  // behind Pi's back — whether Pi's dispatch enumerates before or after the
497
- // push is a microtask race (vstack#1469).
497
+ // push is a microtask race (kendex#1469).
498
498
  const streamLive = Boolean(c.currentPiStream && c.turnOutput);
499
499
  let sawToolUse = false;
500
500
  for (const block of assistantMsg.content) {
@@ -511,7 +511,7 @@ function appendMissingToolUsesFromAssistant(
511
511
  if (existingIdx < 0 && (c.forwardedToolCallIds.has(block.id) || c.deadToolCallIds.has(block.id))) {
512
512
  // Completed-message replay of a call Pi already executed in an earlier
513
513
  // turn (or one already failed as stranded). Not a live Pi turn boundary:
514
- // sawToolUse stays false for it, and no block is emitted (vstack#1469).
514
+ // sawToolUse stays false for it, and no block is emitted (kendex#1469).
515
515
  debug(`assistant message: tool_use ${block.id} already ${c.forwardedToolCallIds.has(block.id) ? "forwarded" : "dead"} — skipping duplicate`);
516
516
  continue;
517
517
  }
@@ -665,7 +665,7 @@ export function processAssistantMessage(message: SDKMessage, model: Model<any>,
665
665
  // handler was already failed as stranded). This exact path produced
666
666
  // the observed duplicate dispatches: the completed-message yield lands
667
667
  // in the callback AFTER a grace finalize already ended the call's
668
- // turn, and per-message dedup cannot see across turns (vstack#1469).
668
+ // turn, and per-message dedup cannot see across turns (kendex#1469).
669
669
  // Not recorded either — a forwarded call must not be claimable again.
670
670
  debug(`processAssistantMessage fallback: tool_use ${block.id} already ${c.forwardedToolCallIds.has(block.id) ? "forwarded" : "dead"} — skipping duplicate`);
671
671
  continue;
@@ -17,7 +17,7 @@ function commandCwd(ctx: unknown): string {
17
17
 
18
18
  async function tryOpenExtensionManagerSettings(ctx: { ui: ExtensionUIContext }): Promise<boolean> {
19
19
  const host = globalThis as unknown as Record<PropertyKey, unknown>;
20
- const openQuickSettings = host[Symbol.for("vstack.pi.extension-manager.open-quick-settings")];
20
+ const openQuickSettings = host[Symbol.for("kendex.pi.extension-manager.open-quick-settings")];
21
21
  if (typeof openQuickSettings !== "function") return false;
22
22
  try {
23
23
  await (openQuickSettings as (ctx: unknown, hint?: string) => Promise<void>)(ctx, "@vanillagreen/pi-claude-bridge");
@@ -35,7 +35,7 @@ function showBridgeStatus(ctx: { ui: ExtensionUIContext; cwd?: string }): void {
35
35
  ].join("\n"), "info");
36
36
  }
37
37
 
38
- // Deterministic connector enumeration for the host app (vstack#838). Reports the
38
+ // Deterministic connector enumeration for the host app (kendex#838). Reports the
39
39
  // failure reason rather than an empty list, so "no connectors" and "could not
40
40
  // check" stay distinguishable.
41
41
  async function reportConnectorInventory(ctx: {
@@ -23,7 +23,7 @@ export interface SessionState {
23
23
  // never rewrite those opening messages — compact/tree-nav mutations set
24
24
  // needsRebuild instead — so a component mismatch marks a FOREIGN
25
25
  // conversation (a subagent-shaped query arriving while the parent is idle,
26
- // vstack#1001) that must run as a clean one-shot without touching this
26
+ // kendex#1001) that must run as a clean one-shot without touching this
27
27
  // record. The user component must always match; the assistant component is
28
28
  // compared only when BOTH sides carry one, so a record stamped on turn 1
29
29
  // (no assistant yet) still matches its own grown conversation and upgrades
@@ -56,7 +56,7 @@ interface SharedSessionLaneStoreV1 {
56
56
  sessions: Map<string, SessionState | null>;
57
57
  }
58
58
 
59
- const SHARED_SESSION_LANES_SYMBOL = Symbol.for("vstack.pi.claude-bridge.shared-session-lanes.v1");
59
+ const SHARED_SESSION_LANES_SYMBOL = Symbol.for("kendex.pi.claude-bridge.shared-session-lanes.v1");
60
60
 
61
61
  function sharedSessionLaneStore(): SharedSessionLaneStoreV1 {
62
62
  const host = globalThis as Record<symbol, unknown>;
@@ -107,7 +107,7 @@ export function clearSharedSessionLanes(): void {
107
107
  // registry above because session_start and session_shutdown can reach different
108
108
  // module instances (`/reload` mid-session, a child agent's own copy) and both
109
109
  // must see the same entry.
110
- const STARTED_LANES_SYMBOL = Symbol.for("vstack.pi.claude-bridge.started-lanes.v1");
110
+ const STARTED_LANES_SYMBOL = Symbol.for("kendex.pi.claude-bridge.started-lanes.v1");
111
111
 
112
112
  function startedLaneStore(): WeakMap<object, string> {
113
113
  const host = globalThis as Record<symbol, unknown>;
@@ -242,7 +242,7 @@ export function reportToolResultMismatch(
242
242
  queryCtx.reportedToolResultMismatch = true;
243
243
  // The single choke point every mismatch path funnels through (abort,
244
244
  // unmatched result, stream-idle, teardown). A context with no claim on
245
- // the shared record (reentrant subagent or foreign one-shot, vstack#1001)
245
+ // the shared record (reentrant subagent or foreign one-shot, kendex#1001)
246
246
  // still gets the full diagnostics below, but its unresolved tool state is
247
247
  // its own — marking the PARENT's record needsRebuild/forceRotate here
248
248
  // would flush the parent's prompt cache for a query that never touched
package/src/config.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  // User-facing extension config. Legacy config is loaded from
2
- // ~/.pi/agent/claude-bridge.json and .pi/claude-bridge.json. vstack extension
2
+ // ~/.pi/agent/claude-bridge.json and .pi/claude-bridge.json. kendex extension
3
3
  // manager config is loaded from settings.json and overrides legacy files in
4
4
  // normal Pi sessions. Isolated embedding hosts consume only the authoritative
5
5
  // user claude-bridge.json and never read extension-manager settings.
@@ -13,11 +13,11 @@ import { debug } from "./debug.js";
13
13
  export const PACKAGE_ID = "@vanillagreen/pi-claude-bridge";
14
14
 
15
15
  /**
16
- * Registry the vstack extension manager reads to display the value an
16
+ * Registry the kendex extension manager reads to display the value an
17
17
  * extension actually resolves for a manifest key, for extensions whose config
18
18
  * has channels the manager does not own. Keyed by package id.
19
19
  */
20
- const EXTERNAL_CONFIG_RESOLVER_SYMBOL = Symbol.for("vstack.pi.extension-config-resolver");
20
+ const EXTERNAL_CONFIG_RESOLVER_SYMBOL = Symbol.for("kendex.pi.extension-config-resolver");
21
21
 
22
22
  export type BridgeEffortLevel = "low" | "medium" | "high" | "xhigh" | "max";
23
23
 
@@ -138,14 +138,14 @@ function projectSettingsPath(cwd: string): string {
138
138
  while (true) {
139
139
  const candidate = join(current, ".pi", "settings.json");
140
140
  if (existsSync(candidate)) return candidate;
141
- if (existsSync(join(current, ".pi")) || existsSync(join(current, ".git")) || existsSync(join(current, ".vstack-lock.json"))) return candidate;
141
+ if (existsSync(join(current, ".pi")) || existsSync(join(current, ".git")) || existsSync(join(current, ".kendex-lock.json"))) return candidate;
142
142
  const parent = dirname(current);
143
143
  if (parent === current) return join(resolve(cwd), ".pi", "settings.json");
144
144
  current = parent;
145
145
  }
146
146
  }
147
147
 
148
- const PROJECT_TRUST_SYMBOL = Symbol.for("vstack.pi.project-trust");
148
+ const PROJECT_TRUST_SYMBOL = Symbol.for("kendex.pi.project-trust");
149
149
 
150
150
  interface ProjectTrustRegistry {
151
151
  projectSettings?: Map<string, boolean>;
@@ -212,7 +212,7 @@ function readManagerConfig(cwd: string): SettingsRecord {
212
212
  if (!existsSync(path)) continue;
213
213
  try {
214
214
  const parsed = JSON.parse(readFileSync(path, "utf8"));
215
- const configRoot = asRecord(asRecord(asRecord(parsed?.vstack)?.extensionManager)?.config);
215
+ const configRoot = asRecord(asRecord(asRecord(parsed?.kendex)?.extensionManager)?.config);
216
216
  const config = asRecord(configRoot?.[PACKAGE_ID]);
217
217
  if (config) mergeDeep(merged, path === userPath ? config : withoutUserScopeOnlyKeys(config));
218
218
  } catch (error) {
@@ -1,4 +1,4 @@
1
- // Cross-PROCESS cache of the connector inventory (vstack#870).
1
+ // Cross-PROCESS cache of the connector inventory (kendex#870).
2
2
  //
3
3
  // #868 primes the inventory at provider registration, but the fetch takes ~1.5s
4
4
  // while the first query is built at ~0.5-0.8s, so turn 1 of a cold sidecar goes
@@ -18,7 +18,7 @@
18
18
  // wrong-version cache returns undefined and the caller falls back to today's
19
19
  // behaviour — the same fail-open contract as the inventory call itself.
20
20
  //
21
- // The ON-DISK FORMAT HAS AN EXTERNAL READER (vstack#892). drovr quarantines this
21
+ // The ON-DISK FORMAT HAS AN EXTERNAL READER (kendex#892). drovr quarantines this
22
22
  // bundle to its sidecar process, so rather than calling `listAccountConnectors`
23
23
  // in-process it re-implements the reader half — path
24
24
  // `<piUserDir()>/connector-cache/<sha256(CLAUDE_CONFIG_DIR).hex[0..16]>.json`,
@@ -5,7 +5,7 @@
5
5
  // nothing in the result distinguishes "these are the connectors" from "these are
6
6
  // the connectors the search happened to return this time". Downstream then stored
7
7
  // that lower bound as authoritative, so an account with Slack attached could
8
- // report an inventory without Slack and no failure signal (vstack#838).
8
+ // report an inventory without Slack and no failure signal (kendex#838).
9
9
  //
10
10
  // This module asks the account instead of the model. Verified live against a
11
11
  // personal claude_max org: the endpoint is POST (a GET returns 405) and each
@@ -23,7 +23,7 @@
23
23
  // conservatively — it under-reported, which fails safe). They are now separately
24
24
  // observable and can legitimately disagree: a correct `complete: true` inventory
25
25
  // can name Slack while `mcp__claude_ai_Slack__*` is not yet callable in this
26
- // process (vstack#832). Treat an inventory as NECESSARY BUT NOT SUFFICIENT for
26
+ // process (kendex#832). Treat an inventory as NECESSARY BUT NOT SUFFICIENT for
27
27
  // availability and keep an attach-time check on the call path; do not derive
28
28
  // "can I call this tool right now" from this result.
29
29
  //
@@ -1,5 +1,5 @@
1
1
  // Connector prime/snapshot host: the per-credential-scope inventory cache the
2
- // query path reads synchronously (vstack#832/#870). Extracted from index.ts —
2
+ // query path reads synchronously (kendex#832/#870). Extracted from index.ts —
3
3
  // this is process-lifetime runtime state, not provider streaming logic.
4
4
  //
5
5
  // Connector declarations for the query path, cached per credential scope. The
@@ -124,7 +124,7 @@ export function primeConnectorServers(claudeConfigDir?: string, overrides: Prime
124
124
  connectorServerFailureAt.delete(key);
125
125
  // Persist so the NEXT cold process has this synchronously. Priming always
126
126
  // loses the race against turn 1 in its own process; a cache written by an
127
- // earlier run is the only thing turn 1 can read in time (vstack#870).
127
+ // earlier run is the only thing turn 1 can read in time (kendex#870).
128
128
  if (writeCachedConnectors(inventory.connectors, key)) {
129
129
  debug(`connectors: cached ${inventory.connectors.length} entries`);
130
130
  }
@@ -148,7 +148,7 @@ export function connectorServersSnapshot(claudeConfigDir?: string): Record<strin
148
148
  primeConnectorServers(claudeConfigDir);
149
149
  // Fall back to the previous run's inventory, read synchronously. This is the
150
150
  // only thing that can populate turn 1 of a cold process, because priming
151
- // cannot finish before the first query is built (vstack#870).
151
+ // cannot finish before the first query is built (kendex#870).
152
152
  const cached = readCachedConnectors(key);
153
153
  if (!cached) return {};
154
154
  const servers = connectorMcpServers({ ok: true, complete: true, connectors: cached });
package/src/connectors.ts CHANGED
@@ -57,7 +57,7 @@ export function connectorsEnabledFor(config?: Config): boolean {
57
57
  // isolation (no sources), which drops the connectors even with
58
58
  // ENABLE_CLAUDEAI_MCP_SERVERS=1. So connectors mode must pass SOME source list.
59
59
  //
60
- // It must be `["user"]` and nothing more (vstack#990). Connector state lives in
60
+ // It must be `["user"]` and nothing more (kendex#990). Connector state lives in
61
61
  // USER scope — the account's config dir (CLAUDE_CONFIG_DIR for managed router
62
62
  // profiles) — so user scope is sufficient for connectors to surface. Claude
63
63
  // Code settings files can also carry an `env` map and `apiKeyHelper`; including
@@ -98,7 +98,7 @@ export const CLAUDE_AI_CONNECTOR_TOOL_PATTERNS = [
98
98
  "mcp__claude_ai_Atlassian__*",
99
99
  ];
100
100
 
101
- // --- The SDK's two-name trap for built-in tools (vstack#1007, vstack#1011) ---
101
+ // --- The SDK's two-name trap for built-in tools (kendex#1007, kendex#1011) ---
102
102
  //
103
103
  // The CLI gives some built-ins TWO spellings, and which one you see depends on
104
104
  // which SURFACE the name crosses:
@@ -118,9 +118,9 @@ export const CLAUDE_AI_CONNECTOR_TOOL_PATTERNS = [
118
118
  // This map declares each alias pair ONCE; every delivered-side membership set
119
119
  // derives its spellings from it instead of hand-copying names. Both prior
120
120
  // instances of the trap were exactly that hand-copy: CHILD_INTERNAL_TOOLS held
121
- // request-side spellings that no stream name ever matched (vstack#1007), and
121
+ // request-side spellings that no stream name ever matched (kendex#1007), and
122
122
  // the connectors allowlist hook DENIED the two discovery tools it exists to
123
- // permit (vstack#1011). Delivered-side sets accept BOTH spellings, so a CLI
123
+ // permit (kendex#1011). Delivered-side sets accept BOTH spellings, so a CLI
124
124
  // version that drops the aliasing cannot reintroduce the bug in either
125
125
  // direction.
126
126
  const SDK_TOOL_ALIASES: Record<string, string> = {
@@ -150,7 +150,7 @@ export const CONNECTOR_DISCOVERY_TOOLS = ["ToolSearch", "ListMcpResources", "Rea
150
150
  // each entry, derived from SDK_TOOL_ALIASES. A PreToolUse hook's `tool_name`
151
151
  // carries the canonical spelling (`ListMcpResourcesTool`), so testing the
152
152
  // request-side list directly made the fail-closed allowlist hook DENY the two
153
- // resource tools it exists to permit (vstack#1011).
153
+ // resource tools it exists to permit (kendex#1011).
154
154
  const CONNECTOR_DISCOVERY_TOOL_NAMES = new Set(CONNECTOR_DISCOVERY_TOOLS.flatMap(deliveredSpellings));
155
155
 
156
156
  // --- Connector WRITE tool control (read-inline / write-by-approval) ---
@@ -243,7 +243,7 @@ function connectorNameWords(segment: string): string[] {
243
243
  // exact tool id (the CLI matcher only supports exact ids or a whole-server glob).
244
244
  //
245
245
  // PUBLIC CONTRACT — this list and `isConnectorWriteTool` have downstream
246
- // dependents that gate real user-facing approvals on them (vstack#892):
246
+ // dependents that gate real user-facing approvals on them (kendex#892):
247
247
  //
248
248
  // memsira routes connector writes through its own gated approval flow
249
249
  // drovr keeps its chat sidecar permanently write-`deny` and runs an
@@ -330,7 +330,7 @@ export function isConnectorTool(name: string | undefined): boolean {
330
330
  // processStreamEvent/processAssistantMessage read. Stream names are a
331
331
  // DELIVERED surface (see SDK_TOOL_ALIASES): the two MCP-resource built-ins'
332
332
  // request-side spellings used to sit in this set and never matched anything
333
- // (vstack#1007). If an aliased name is ever added here, derive its spellings
333
+ // (kendex#1007). If an aliased name is ever added here, derive its spellings
334
334
  // via deliveredSpellings rather than hand-copying them.
335
335
  //
336
336
  // The MCP-resource tools are now EXCLUDED deliberately, under BOTH spellings:
@@ -338,14 +338,14 @@ export function isConnectorTool(name: string | undefined): boolean {
338
338
  // audit it through the Pi mirror (an out-of-process sidecar has no view of the
339
339
  // bridge's own connector-call entries or the child transcript), so it stays
340
340
  // mirrored into Pi. Do not re-add either spelling without revisiting that
341
- // decision in vstack#1007. The allowlist hook is what lets those calls run at
342
- // all — see isAllowlistedConnectorSessionTool (vstack#1011).
341
+ // decision in kendex#1007. The allowlist hook is what lets those calls run at
342
+ // all — see isAllowlistedConnectorSessionTool (kendex#1011).
343
343
  const CHILD_INTERNAL_TOOLS = new Set(["ToolSearch", "ScheduleWakeup"]);
344
344
 
345
345
  /**
346
346
  * True for a Claude Code built-in meta-tool the child resolves in-process.
347
347
  *
348
- * Mirroring one into the Pi stream (vstack#980) made Pi's agent loop dispatch a
348
+ * Mirroring one into the Pi stream (kendex#980) made Pi's agent loop dispatch a
349
349
  * tool it does not have and deliver an error result for an id no MCP handler
350
350
  * ever claimed. The result queued in `pendingResults` until the reaper dropped
351
351
  * it — one "dropped 1 tool result(s) whose handler never matched (ToolSearch)"
@@ -381,7 +381,7 @@ export function isChildInternalTool(name: string | undefined): boolean {
381
381
  * how a lie in a mirror becomes a lie in the conversation of record.
382
382
  *
383
383
  * 2. Claude Code's own in-process meta-tools (`isChildInternalTool`), which the
384
- * child resolves without any dispatcher at all (vstack#980).
384
+ * child resolves without any dispatcher at all (kendex#980).
385
385
  *
386
386
  * Takes the RAW SDK tool name, before `mapToolName` — child-executed names have
387
387
  * no Pi-side counterpart, so mapping them is meaningless. Accepts a missing
@@ -507,7 +507,7 @@ export function connectorWriteDenyHook(): HookCallback {
507
507
  // stay PRODUCT-NEUTRAL: this is shared source and every consuming app shows it.
508
508
  // Naming one host told a different app's model to use a product it has never
509
509
  // heard of, which is confusing at exactly the moment someone is debugging a
510
- // refused write (vstack#892). Each host describes its own approval flow in its
510
+ // refused write (kendex#892). Each host describes its own approval flow in its
511
511
  // own prompt; this string only has to say that one exists.
512
512
  function connectorWriteDenyOutput(toolName: string) {
513
513
  return {
@@ -534,7 +534,7 @@ function connectorWriteDenyOutput(toolName: string) {
534
534
  // This is a DELIVERED-side check — `name` is a hook's `input.tool_name`, which
535
535
  // carries the canonical spelling — so membership is tested against
536
536
  // CONNECTOR_DISCOVERY_TOOL_NAMES (both spellings), not the request-side list
537
- // (vstack#1011). That also carries vstack#1007's mirroring decision: the
537
+ // (kendex#1011). That also carries kendex#1007's mirroring decision: the
538
538
  // MCP-resource tools are deliberately NOT child-internal so every resource
539
539
  // read mirrors into Pi as the consumers' audit surface — a mirror that can
540
540
  // only exist if this allowlist lets the call execute. Denying the canonical
@@ -669,7 +669,7 @@ export function toolIsolationForQuery(connectorsEnabled: boolean, writeMode: Con
669
669
  /**
670
670
  * Explicit `mcpServers` declarations for the account's CONNECTED connectors.
671
671
  *
672
- * Why this exists (vstack#832): claude.ai connectors load async and non-blocking,
672
+ * Why this exists (kendex#832): claude.ai connectors load async and non-blocking,
673
673
  * and the turn-1 tool manifest is built at +410-665ms — roughly 300ms BEFORE the
674
674
  * CLI has even fetched the connector list. The model therefore composes its first
675
675
  * answer against a manifest containing no connectors and says it has no access,
package/src/index.ts CHANGED
@@ -239,18 +239,18 @@ export interface DeferredUserReplayPlan {
239
239
  // with no image blocks).
240
240
  prompt: string | null;
241
241
  // Present when the run carries image blocks — the replay must send these
242
- // (via wrapPromptStream) or the images are silently lost (vstack#993).
242
+ // (via wrapPromptStream) or the images are silently lost (kendex#993).
243
243
  blocks: ContentBlockParam[] | null;
244
244
  }
245
245
 
246
246
  /** Plan replay of user messages pi injected mid-query (steer drain, followUp).
247
247
  * Captures the ENTIRE trailing consecutive user run, not just the last
248
- * message — dropping the earlier ones was silent input loss (vstack#967) —
248
+ * message — dropping the earlier ones was silent input loss (kendex#967) —
249
249
  * but never walks below `capturedThrough`, the position an earlier callback
250
250
  * of the SAME query already captured (or deliberately held at, for an
251
251
  * all-empty run). Without that lower bound a second mid-query steer re-planned
252
252
  * the whole run from scratch and the first steer was queued — and delivered to
253
- * Claude — twice (vstack#1009). */
253
+ * Claude — twice (kendex#1009). */
254
254
  export function planDeferredUserReplay(messages: Context["messages"], capturedThrough = 0): DeferredUserReplayPlan {
255
255
  let runStart = messages.length;
256
256
  while (runStart > capturedThrough && messages[runStart - 1]?.role === "user") runStart--;
@@ -502,7 +502,7 @@ function applyProviderRegistration(trigger: string): void {
502
502
  debug(`${trigger}: native registration upsert, credentialed=${credentialed} (module=${moduleInstanceId})`);
503
503
  // Start the connector inventory now, not on the first turn: the query path
504
504
  // can only read a synchronous snapshot, so priming here is what gets the
505
- // declarations in place before turn 1 (vstack#832). Fire and forget —
505
+ // declarations in place before turn 1 (kendex#832). Fire and forget —
506
506
  // registration must not wait on the network. Primes the DEFAULT credential
507
507
  // scope only; managed profiles are primed per request in their own scope.
508
508
  if (hasClaudeCredentials() && connectorsEnabledFor(loadConfig(process.cwd()))) primeConnectorServers();
@@ -619,7 +619,7 @@ function streamClaudeAgentSdkInLane(model: Model<any>, context: Context, options
619
619
  if (queryCtx.pendingToolCalls.size > 0) {
620
620
  // A waiting handler whose call never reached Pi can never be answered —
621
621
  // fail it now with a retryable error instead of letting the SDK await it
622
- // forever (the 2026-08-17 five-and-a-half-hour deadlock, vstack#1469).
622
+ // forever (the 2026-08-17 five-and-a-half-hour deadlock, kendex#1469).
623
623
  // Forwarded-but-unanswered handlers stay: steer-split batches legitimately
624
624
  // deliver their results in a later callback.
625
625
  const stranded = drainStrandedToolCalls(queryCtx);
@@ -646,7 +646,7 @@ function streamClaudeAgentSdkInLane(model: Model<any>, context: Context, options
646
646
  // so we save them for replay as continuation queries after consumeQuery ends.
647
647
  // The cursor may only advance over messages actually captured for replay:
648
648
  // claiming Claude owns a user message that was never deferred is permanent
649
- // silent input loss (vstack#967 — only the LAST of several trailing user
649
+ // silent input loss (kendex#967 — only the LAST of several trailing user
650
650
  // messages was captured while the cursor skipped them all).
651
651
  let capturedThrough = context.messages.length;
652
652
  if (lastMsgRole === "user") {
@@ -654,13 +654,13 @@ function streamClaudeAgentSdkInLane(model: Model<any>, context: Context, options
654
654
  // Math.max-advances with every callback's capturedThrough below), so a
655
655
  // second steer callback only queues messages BEYOND what the first one
656
656
  // already owns — re-planning the whole trailing run queued the earlier
657
- // steer twice (vstack#1009). latestCursor, not the shared record's
657
+ // steer twice (kendex#1009). latestCursor, not the shared record's
658
658
  // cursor, deliberately: it lives on this QueryContext, so it is correct
659
659
  // for reentrant and detached foreign queries too, whose contexts the
660
- // shared cursor does not index (vstack#1001).
660
+ // shared cursor does not index (kendex#1001).
661
661
  const replay = planDeferredUserReplay(context.messages, queryCtx.latestCursor);
662
662
  // Image-only runs have no usable text but must still replay — capture
663
- // whenever EITHER form has content (vstack#993).
663
+ // whenever EITHER form has content (kendex#993).
664
664
  if (replay.prompt || replay.blocks) {
665
665
  ctx().deferredUserMessages.push({ text: replay.prompt ?? "", blocks: replay.blocks ?? undefined });
666
666
  debug(`provider: deferred ${replay.userMessageCount} user message(s) for replay after query${replay.blocks ? ` (${replay.blocks.length} blocks incl. images)` : ""}: ${(replay.prompt ?? "[image-only]").slice(0, 60)}`);
@@ -683,7 +683,7 @@ function streamClaudeAgentSdkInLane(model: Model<any>, context: Context, options
683
683
  // The stackDepth guard covers a pushed subagent context; the detached
684
684
  // flag covers a foreign one-shot on the top-level ctx, whose GROWN
685
685
  // mid-query context could otherwise out-length the parent's cursor and
686
- // advance it past history Claude never saw (vstack#1001); Math.max
686
+ // advance it past history Claude never saw (kendex#1001); Math.max
687
687
  // remains the backstop for a legacy-record foreign context the
688
688
  // fingerprint guard could not classify.
689
689
  const activeSession = getSharedSession();
@@ -702,7 +702,7 @@ function streamClaudeAgentSdkInLane(model: Model<any>, context: Context, options
702
702
  debug(`provider: orphaned tool result after abort, emitting end_turn`);
703
703
  // The detached flag deliberately survives query end: an orphaned result
704
704
  // from a foreign one-shot indexes ITS conversation, and writing that
705
- // length here would move (even shrink) the parent's cursor (vstack#1001).
705
+ // length here would move (even shrink) the parent's cursor (kendex#1001).
706
706
  const activeSession = getSharedSession();
707
707
  if (activeSession && stackDepth() === 0 && !ctx().detachedFromSharedSession) setSharedSession({ ...activeSession, cursor: context.messages.length });
708
708
  const c = ctx(); // capture current context for the microtask
@@ -883,7 +883,7 @@ function streamClaudeAgentSdkInLane(model: Model<any>, context: Context, options
883
883
  const { sessionId: resumeSessionId, promptStart } = syncResult;
884
884
  // A FOREIGN-conversation query (conversation-fingerprint mismatch against
885
885
  // the shared record — a subagent-shaped request arriving while the parent
886
- // is IDLE, vstack#1001) also runs as a clean one-shot and gets the same
886
+ // is IDLE, kendex#1001) also runs as a clean one-shot and gets the same
887
887
  // hands-off treatment as a reentrant one below: never persist over the
888
888
  // module-level record, never mark it for rebuild. The flag also rides the
889
889
  // QueryContext so the mismatch/abort/teardown paths that mutate the record
@@ -967,7 +967,7 @@ function streamClaudeAgentSdkInLane(model: Model<any>, context: Context, options
967
967
  // A reentrant (subagent) query must never write the module-level shared
968
968
  // session: its completion/failure handlers would overwrite the PARENT's
969
969
  // record with the child's session id and cursor. A foreign-conversation
970
- // one-shot (vstack#1001) has exactly the same non-claim on the record.
970
+ // one-shot (kendex#1001) has exactly the same non-claim on the record.
971
971
  const persistSession = (next: SessionState | null): void => {
972
972
  if (isReentrant || foreignContext) return;
973
973
  setSharedSession(next && conversationFp ? { conversationFingerprint: conversationFp, ...next } : next);
@@ -1235,7 +1235,7 @@ function streamClaudeAgentSdkInLane(model: Model<any>, context: Context, options
1235
1235
 
1236
1236
  const contOptions = { ...queryOptions, resume: resumeId, ...makeCliDebugOptions("continuation") };
1237
1237
  // Runs carrying image blocks replay as blocks (wrapPromptStream) so
1238
- // the images survive; text-only runs stay plain strings (vstack#993).
1238
+ // the images survive; text-only runs stay plain strings (kendex#993).
1239
1239
  const contQuery = sdkQueryFactory({ prompt: steer.blocks ? wrapPromptStream(steer.blocks) : steer.text, options: contOptions });
1240
1240
  abortCtx.activeQuery = contQuery;
1241
1241
 
@@ -83,7 +83,7 @@ export function buildClaudeQueryOptions(input: BuildClaudeQueryOptionsInput): Bu
83
83
  const connectorWriteMode = connectorWriteModeFor(bridgeConfig);
84
84
  // Declare the account's connected connectors explicitly so `alwaysLoad` can
85
85
  // hold startup until they attach — otherwise the turn-1 manifest is built
86
- // before the CLI has fetched them (vstack#832).
86
+ // before the CLI has fetched them (kendex#832).
87
87
  const connectorServers = enableCloudMcp ? connectorServersSnapshot(accountScope.claudeConfigDir) : {};
88
88
  const appendSystemPrompt = providerSettings.appendSystemPrompt !== false;
89
89
  const agentsAppend = appendSystemPrompt ? extractAgentsAppend() : undefined;
@@ -98,7 +98,7 @@ export function buildClaudeQueryOptions(input: BuildClaudeQueryOptionsInput): Bu
98
98
  // ignores auto-discovered filesystem MCP servers while Pi owns tool execution.
99
99
  // Connectors mode needs settings resolution ON but restricted to USER scope
100
100
  // only — project/local settings files can smuggle `env`/`apiKeyHelper` from
101
- // a hostile checkout (vstack#990). Full rationale on settingSourcesForQuery.
101
+ // a hostile checkout (kendex#990). Full rationale on settingSourcesForQuery.
102
102
  const settingSources: SettingSource[] | undefined = settingSourcesForQuery(
103
103
  enableCloudMcp, appendSystemPrompt, providerSettings.settingSources);
104
104
  const strictMcpConfigEnabled = !appendSystemPrompt && providerSettings.strictMcpConfig !== false;
@@ -16,7 +16,7 @@ import { currentRequestLaneId } from "./request-lane.js";
16
16
  * `text` is the joined text form (previews, and the replay prompt when no
17
17
  * image blocks were captured). `blocks` is present when the run carried
18
18
  * images — the replay must send the blocks or the images are silently lost
19
- * (vstack#993). */
19
+ * (kendex#993). */
20
20
  export interface DeferredUserMessage {
21
21
  text: string;
22
22
  blocks?: ContentBlockParam[];
@@ -40,7 +40,7 @@ export interface PendingToolCall {
40
40
  /** The MCP invocation's schema-validated arguments. The SDK hands the handler
41
41
  * the COMPLETE input, so this is the authoritative copy — the grace-timer
42
42
  * finalize settles a still-partial streamed block from here instead of from
43
- * its truncated partial JSON (vstack#1469: a `{}` settle made Pi execute
43
+ * its truncated partial JSON (kendex#1469: a `{}` settle made Pi execute
44
44
  * empty-argument calls). */
45
45
  args: Record<string, unknown>;
46
46
  /** `QueryContext.callbackGeneration` at registration. A handler from an older
@@ -106,7 +106,7 @@ export function strandedToolCallResult(): McpResult {
106
106
  * forwarded (Pi owes it a result — steer-split deliveries arrive turns later)
107
107
  * or nothing is waiting. Marks the id dead so a lagging stream replay can
108
108
  * never forward it AFTER the model was told it failed — that late forward
109
- * would execute the call a second time behind the model's back (vstack#1469).
109
+ * would execute the call a second time behind the model's back (kendex#1469).
110
110
  * Returns true when a handler was failed. */
111
111
  export function failStrandedToolCall(queryCtx: QueryContext, id: string): boolean {
112
112
  if (queryCtx.forwardedToolCallIds.has(id)) return false;
@@ -263,7 +263,7 @@ export class QueryContext {
263
263
  * set endToolUseTurn stamps from the turn's content. A forwarded id is one Pi
264
264
  * will execute and answer; it must never be emitted again (a lagging stream
265
265
  * replays the same tool_use into the NEXT turn, and per-message turnBlocks
266
- * dedup cannot see across turns — vstack#1469's duplicate executions), and a
266
+ * dedup cannot see across turns — kendex#1469's duplicate executions), and a
267
267
  * handler waiting on it must be left waiting at the stranded-handler drains.
268
268
  * Query-scoped, never reset per message. */
269
269
  forwardedToolCallIds = new Set<string>();
@@ -295,7 +295,7 @@ export class QueryContext {
295
295
  * same reason: a late handler firing after resetToolTracking wiped the
296
296
  * per-message records must still be able to exact-match the parked/queued
297
297
  * result of ITS OWN call — without stored args the only fallback is
298
- * sole-same-name, which can hand it a LIVE sibling's id (vstack#1469). */
298
+ * sole-same-name, which can hand it a LIVE sibling's id (kendex#1469). */
299
299
  queryToolArgs = new Map<string, Record<string, unknown>>();
300
300
  claimedToolCallIds = new Set<string>();
301
301
  deliveredToolResultIds = new Set<string>();
@@ -311,7 +311,7 @@ export class QueryContext {
311
311
  committedOutput = false;
312
312
  /** True when this query holds NO claim on the module-level shared session
313
313
  * record: a reentrant (subagent) query, or a foreign-conversation one-shot
314
- * (vstack#1001). Every shared-record mutation reachable from this context —
314
+ * (kendex#1001). Every shared-record mutation reachable from this context —
315
315
  * reportToolResultMismatch's needsRebuild/forceRotate mark, the cursor
316
316
  * advances on the tool-result-delivery and orphaned-result paths — must
317
317
  * no-op so the PARENT's record stays untouched. Assigned at fresh-query
@@ -517,7 +517,7 @@ export class QueryContext {
517
517
  // Ids whose RESULT already sits queued or parked. A handler can fire after
518
518
  // the message boundary wiped the per-message records — by then Pi has
519
519
  // executed its call and only these query-scoped stores still know it
520
- // (vstack#1469: the boundary reap used to make such a handler error out
520
+ // (kendex#1469: the boundary reap used to make such a handler error out
521
521
  // and the model re-run an already-executed side-effectful call). An
522
522
  // exact-args match here outranks the live sole-same-name fallback below,
523
523
  // so a late handler can never steal a live sibling's id while its own
@@ -586,7 +586,7 @@ export class QueryContext {
586
586
  * mismatch report for the whole query (queued>0 with 0/0 counters and no tool
587
587
  * names) and forces a session rebuild per turn. But the boundary does NOT
588
588
  * prove the handler gave up — the SDK staggers handler invocations, and the
589
- * 2026-08-17 deadlock session (vstack#1469) had three of five parallel
589
+ * 2026-08-17 deadlock session (kendex#1469) had three of five parallel
590
590
  * handlers fire after this reap destroyed their results. So the reap parks
591
591
  * instead of dropping: reports stay clean, and a late handler still gets its
592
592
  * real result through takeQueuedOrParkedResult.
@@ -675,7 +675,7 @@ interface QueryLaneStoreV1 {
675
675
  sessionLanes: Map<string, QueryLaneState>;
676
676
  }
677
677
 
678
- const QUERY_LANES_SYMBOL = Symbol.for("vstack.pi.claude-bridge.query-lanes.v1");
678
+ const QUERY_LANES_SYMBOL = Symbol.for("kendex.pi.claude-bridge.query-lanes.v1");
679
679
 
680
680
  function queryLaneStore(): QueryLaneStoreV1 {
681
681
  const host = globalThis as Record<symbol, unknown>;
package/src/rate-limit.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import { USAGE_LIMIT_ERROR_PREFIXES } from "@anthropic-ai/claude-agent-sdk";
2
2
 
3
- export const RATE_LIMIT_AUTO_RESUME_EVENT = "vstack:rate-limit";
3
+ export const RATE_LIMIT_AUTO_RESUME_EVENT = "kendex:rate-limit";
4
4
  export const RATE_LIMIT_TOKEN = "\x1b[31m[rate-limit]\x1b[39m";
5
5
 
6
6
  // The SDK export is @alpha — degrade to "no match" (pre-0.3.220 behavior) if a
@@ -10,7 +10,7 @@ import { AsyncLocalStorage } from "node:async_hooks";
10
10
  * keeps that identity attached to every promise, SDK iterator, and timer born
11
11
  * during the provider call without threading the id through every helper.
12
12
  */
13
- const REQUEST_LANE_SYMBOL = Symbol.for("vstack.pi.claude-bridge.request-lane.v1");
13
+ const REQUEST_LANE_SYMBOL = Symbol.for("kendex.pi.claude-bridge.request-lane.v1");
14
14
 
15
15
  function requestLaneStorage(): AsyncLocalStorage<string> {
16
16
  const host = globalThis as Record<symbol, unknown>;
@@ -226,7 +226,7 @@ export function restoreSharedSessionFromPi(ctx: { sessionManager?: unknown; cwd?
226
226
  // instance can differ. Scheduling again for the same manager REPLACES the
227
227
  // pending timer: each fire appends the record's state as of its schedule and
228
228
  // restore reads the last marker, so the superseded entry is a stale duplicate.
229
- const SCHEDULED_PERSISTENCE_SYMBOL = Symbol.for("vstack.pi.claude-bridge.scheduled-persistence.v1");
229
+ const SCHEDULED_PERSISTENCE_SYMBOL = Symbol.for("kendex.pi.claude-bridge.scheduled-persistence.v1");
230
230
 
231
231
  type PersistenceTimer = ReturnType<typeof setTimeout>;
232
232
 
@@ -438,7 +438,7 @@ function verifyWrittenSession(
438
438
  safeNotify(
439
439
  `Session file issue: ${msg}\n` +
440
440
  `cwd=${displayPath(cwd)} realpath=${displayPath(safeRealpath(cwd))}\n` +
441
- `Please copy and paste this message into a new issue at https://github.com/vanillagreencom/vstack/issues/new` +
441
+ `Please copy and paste this message into a new issue at https://github.com/vanillagreencom/kendex/issues/new` +
442
442
  (DEBUG ? ` and attach ${DEBUG_LOG_PATH}` : ` (rerun with CLAUDE_BRIDGE_DEBUG=1 to capture a debug log)`),
443
443
  "warning",
444
444
  );
@@ -523,7 +523,7 @@ export function syncSharedSession(
523
523
  );
524
524
  const incomingFingerprint = conversationFingerprint(messages);
525
525
 
526
- // FOREIGN-CONVERSATION guard (Case 6, vstack#1001). A subagent-shaped query
526
+ // FOREIGN-CONVERSATION guard (Case 6, kendex#1001). A subagent-shaped query
527
527
  // arriving while the parent is IDLE is not reentrant, so it lands here as an
528
528
  // outermost query. Without an identity check its short foreign context takes
529
529
  // the REBUILD path — rewriting the PARENT's session file from foreign
@@ -568,7 +568,7 @@ export function syncSharedSession(
568
568
  // Read the pre-update cursor first: setSharedSession reassigns the live
569
569
  // binding, so comparing against sharedSession.cursor afterwards would
570
570
  // always be equal and the "advanced past trailing assistant" debug
571
- // branch could never print (vstack#993).
571
+ // branch could never print (kendex#993).
572
572
  const cursorBeforeUpdate = sharedSession.cursor;
573
573
  // A REUSE match proves identity, so the anchor may only strengthen here:
574
574
  // a pre-3.1.1 record adopts it outright, and a turn-1 user-only anchor