@kontextmind/kxm 0.7.16 → 0.7.18

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (39) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/docs/README.md +1 -1
  3. package/docs/browser-automation.md +24 -1
  4. package/docs/prompts/browser-annotate-feedback.md +17 -0
  5. package/docs/prompts/browser-diagnose-recover.md +17 -0
  6. package/docs/prompts/browser-explore.md +17 -0
  7. package/docs/prompts/browser-repro-fix.md +17 -0
  8. package/docs/prompts/browser-start.md +17 -0
  9. package/docs/prompts/browser-takeover.md +17 -0
  10. package/docs/templates/README.md +1 -1
  11. package/package.json +8 -6
  12. package/plugins/kxm/.claude-plugin/plugin.json +1 -1
  13. package/plugins/kxm/dist/cli.js +43 -39
  14. package/plugins/kxm/dist/core.js +3 -2
  15. package/plugins/kxm/dist/extension.js +34472 -11328
  16. package/plugins/kxm/dist/mcp-server.js +1 -1
  17. package/plugins/kxm/dist/runtime.js +43 -39
  18. package/plugins/kxm/dist/server.js +43 -39
  19. package/plugins/kxm/dist/vnext-runtime-supervisor.js +40 -37
  20. package/plugins/kxm/package.json +4 -1
  21. package/plugins/kxm/src/extension.ts +9 -0
  22. package/plugins/kxm/src/mcp-server.ts +1 -1
  23. package/plugins/kxm/src/providers/claude-bridge/LICENSE +21 -0
  24. package/plugins/kxm/src/providers/claude-bridge/README.md +18 -0
  25. package/plugins/kxm/src/providers/claude-bridge/config.ts +53 -0
  26. package/plugins/kxm/src/providers/claude-bridge/convert.ts +180 -0
  27. package/plugins/kxm/src/providers/claude-bridge/index.ts +28 -0
  28. package/plugins/kxm/src/providers/claude-bridge/mcp-server.ts +59 -0
  29. package/plugins/kxm/src/providers/claude-bridge/models.ts +166 -0
  30. package/plugins/kxm/src/providers/claude-bridge/pi-compat.ts +215 -0
  31. package/plugins/kxm/src/providers/claude-bridge/prompt-stream.ts +89 -0
  32. package/plugins/kxm/src/providers/claude-bridge/provider.ts +286 -0
  33. package/plugins/kxm/src/providers/claude-bridge/query-state.ts +72 -0
  34. package/plugins/kxm/src/providers/claude-bridge/redact.ts +26 -0
  35. package/plugins/kxm/src/providers/claude-bridge/register.ts +124 -0
  36. package/plugins/kxm/src/providers/claude-bridge/stream.ts +390 -0
  37. package/plugins/kxm/src/providers/claude-bridge/tools.ts +140 -0
  38. package/plugins/kxm/src/redact.ts +3 -2
  39. package/scripts/build-runtime.mjs +5 -1
@@ -0,0 +1,89 @@
1
+ // Long-lived streaming-input prompt for query().
2
+ // Vendored from pi-claude-bridge 0.7.0 (provider path only).
3
+
4
+ export type PromptUserMessage = {
5
+ type: "user";
6
+ message: { role: "user"; content: unknown };
7
+ parent_tool_use_id: null;
8
+ priority?: string;
9
+ };
10
+
11
+ export interface PromptStream {
12
+ stream: AsyncGenerator<PromptUserMessage>;
13
+ push: (msg: PromptUserMessage) => Promise<void>;
14
+ end: () => void;
15
+ fail: (error: Error) => void;
16
+ }
17
+
18
+ export function makePromptStream(): PromptStream {
19
+ type Item = { msg: PromptUserMessage; resolve: () => void; reject: (e: Error) => void };
20
+ const queue: Item[] = [];
21
+ let inflight: Item | null = null;
22
+ let wake: (() => void) | null = null;
23
+ let done = false;
24
+ let failure: Error | null = null;
25
+
26
+ const kick = () => {
27
+ wake?.();
28
+ wake = null;
29
+ };
30
+
31
+ async function* gen(): AsyncGenerator<PromptUserMessage> {
32
+ try {
33
+ while (true) {
34
+ while (queue.length === 0 && !done && !failure) {
35
+ await new Promise<void>((resolve) => {
36
+ wake = resolve;
37
+ });
38
+ }
39
+ if (failure) throw failure;
40
+ const item = queue.shift();
41
+ if (!item) return;
42
+ inflight = item;
43
+ try {
44
+ yield item.msg;
45
+ item.resolve();
46
+ } finally {
47
+ item.reject(new Error("prompt stream closed"));
48
+ inflight = null;
49
+ }
50
+ }
51
+ } finally {
52
+ done = true;
53
+ }
54
+ }
55
+
56
+ return {
57
+ stream: gen(),
58
+ push: (msg) =>
59
+ failure || done
60
+ ? Promise.reject(failure ?? new Error("prompt stream closed"))
61
+ : new Promise<void>((resolve, reject) => {
62
+ queue.push({ msg, resolve, reject });
63
+ kick();
64
+ }),
65
+ end: () => {
66
+ done = true;
67
+ kick();
68
+ },
69
+ fail: (error) => {
70
+ if (failure) return;
71
+ failure = error;
72
+ queue.splice(0).forEach((item) => item.reject(error));
73
+ inflight?.reject(error);
74
+ kick();
75
+ },
76
+ };
77
+ }
78
+
79
+ export function userMessage(
80
+ content: unknown,
81
+ priority?: string,
82
+ ): PromptUserMessage {
83
+ return {
84
+ type: "user",
85
+ message: { role: "user", content },
86
+ parent_tool_use_id: null,
87
+ ...(priority ? { priority } : {}),
88
+ };
89
+ }
@@ -0,0 +1,286 @@
1
+ // @ts-nocheck — vendored snapshot; host tsconfig is stricter than upstream.
2
+ import { createRequire } from "node:module";
3
+ import type { Context, Model, SimpleStreamOptions } from "./pi-compat.ts";
4
+ import { createAssistantMessageEventStream, type AssistantMessageEventStream } from "./pi-compat.ts";
5
+ import {
6
+ claudeCodeModelId,
7
+ DEFAULT_LONG_CONTEXT,
8
+ resolveEffort,
9
+ type LongContextSettings,
10
+ } from "./models.ts";
11
+ import { claudeCodeSettings, loadConfig, longContextFromConfig, type ClaudeBridgeConfig } from "./config.ts";
12
+ import { extractUserPrompt, extractUserPromptBlocks } from "./convert.ts";
13
+ import { createToolServer } from "./mcp-server.ts";
14
+ import { makePromptStream, userMessage } from "./prompt-stream.ts";
15
+ import { QueryContext, ctx } from "./query-state.ts";
16
+ import {
17
+ claimCurrentPiStream,
18
+ consumeQuery,
19
+ finalizeCurrentStream,
20
+ markStreamComplete,
21
+ } from "./stream.ts";
22
+ import {
23
+ extractAllToolResults,
24
+ MCP_SERVER_NAME,
25
+ resolveMcpTools,
26
+ type McpResult,
27
+ } from "./tools.ts";
28
+
29
+ export const CC_CHILD_ENV = {
30
+ ENABLE_CLAUDEAI_MCP_SERVERS: "0",
31
+ DISABLE_AUTO_COMPACT: "1",
32
+ } as const;
33
+
34
+ export const CLAUDE_MD_EXCLUDES = ["**/CLAUDE.md", "**/.claude/rules/**"];
35
+
36
+ const require = createRequire(import.meta.url);
37
+ const activeQueryContexts = new Set<QueryContext>();
38
+
39
+ export type QueryHandle = AsyncIterable<unknown> & {
40
+ interrupt?: () => Promise<unknown> | unknown;
41
+ close?: () => void;
42
+ };
43
+
44
+ export type QueryFactory = (args: { prompt: unknown; options: Record<string, unknown> }) => QueryHandle;
45
+
46
+ export type StreamDeps = {
47
+ query?: QueryFactory;
48
+ config?: ClaudeBridgeConfig;
49
+ longContext?: LongContextSettings;
50
+ nowCwd?: () => string;
51
+ };
52
+
53
+ let lastSessionId: string | undefined;
54
+
55
+ export function resetClaudeBridgeSession(): void {
56
+ lastSessionId = undefined;
57
+ activeQueryContexts.clear();
58
+ }
59
+
60
+ export function getClaudeBridgeSessionId(): string | undefined {
61
+ return lastSessionId;
62
+ }
63
+
64
+ function buildMcpServers(tools: Context["tools"], queryCtx: QueryContext) {
65
+ if (!tools?.length) return undefined;
66
+ const mcpTools = tools.map((tool) => ({
67
+ name: tool.name,
68
+ description: tool.description,
69
+ inputSchema: tool.parameters,
70
+ handler: async (toolCallId: string) => {
71
+ if (queryCtx.pendingResults.has(toolCallId)) {
72
+ const result = queryCtx.pendingResults.get(toolCallId)!;
73
+ queryCtx.pendingResults.delete(toolCallId);
74
+ return result;
75
+ }
76
+ return new Promise<McpResult>((resolve) => {
77
+ queryCtx.pendingToolCalls.set(toolCallId, { toolName: tool.name, resolve });
78
+ });
79
+ },
80
+ }));
81
+ return { [MCP_SERVER_NAME]: createToolServer(MCP_SERVER_NAME, mcpTools) };
82
+ }
83
+
84
+ export function deliverToolResults(c: QueryContext, results: McpResult[]): void {
85
+ for (const result of results) {
86
+ const id = result.toolCallId;
87
+ if (id && c.pendingToolCalls.has(id)) {
88
+ const pending = c.pendingToolCalls.get(id)!;
89
+ c.pendingToolCalls.delete(id);
90
+ pending.resolve(result);
91
+ } else if (id) {
92
+ c.pendingResults.set(id, result);
93
+ }
94
+ }
95
+ }
96
+
97
+ function contextForToolResults(results: McpResult[]): QueryContext | undefined {
98
+ for (const result of results) {
99
+ const id = result.toolCallId;
100
+ if (!id) continue;
101
+ for (const queryCtx of activeQueryContexts) {
102
+ if (
103
+ queryCtx.pendingToolCalls.has(id) ||
104
+ queryCtx.pendingResults.has(id) ||
105
+ queryCtx.turnToolCallIds.includes(id)
106
+ ) {
107
+ return queryCtx;
108
+ }
109
+ }
110
+ }
111
+ return undefined;
112
+ }
113
+
114
+ let cachedQuery: QueryFactory | undefined;
115
+
116
+ function loadAgentSdkQuery(): QueryFactory {
117
+ if (cachedQuery) return cachedQuery;
118
+ const mod = require("@anthropic-ai/claude-agent-sdk") as { query: QueryFactory };
119
+ cachedQuery = (args) => mod.query(args);
120
+ return cachedQuery;
121
+ }
122
+
123
+ export function setAgentSdkQueryForTests(factory: QueryFactory | undefined): void {
124
+ cachedQuery = factory;
125
+ }
126
+
127
+ export function streamClaudeBridge(
128
+ model: Model,
129
+ context: Context,
130
+ options?: SimpleStreamOptions,
131
+ deps: StreamDeps = {},
132
+ ): AssistantMessageEventStream {
133
+ const stream = createAssistantMessageEventStream();
134
+ const lastMsg = context.messages[context.messages.length - 1];
135
+ const allResults = activeQueryContexts.size > 0 ? extractAllToolResults(context.messages).results : [];
136
+ const resultCtx = allResults.length > 0 ? contextForToolResults(allResults) : undefined;
137
+
138
+ if (resultCtx) {
139
+ claimCurrentPiStream(stream, "tool-result", resultCtx);
140
+ resultCtx.resetTurnState(model);
141
+ deliverToolResults(resultCtx, allResults);
142
+ resultCtx.latestCursor = Math.max(resultCtx.latestCursor, context.messages.length);
143
+ return stream;
144
+ }
145
+
146
+ if (lastMsg?.role === "toolResult") {
147
+ const c = new QueryContext();
148
+ c.resetTurnState(model);
149
+ queueMicrotask(() => {
150
+ stream.push({ type: "done", reason: "stop", message: c.turnOutput });
151
+ markStreamComplete(stream);
152
+ stream.end();
153
+ });
154
+ return stream;
155
+ }
156
+
157
+ const queryCtx = ctx().activeQuery !== null ? new QueryContext() : ctx();
158
+ const { mcpTools, customToolNameToPi } = resolveMcpTools(context);
159
+ claimCurrentPiStream(stream, "fresh-query", queryCtx);
160
+ queryCtx.pendingToolCalls.clear();
161
+ queryCtx.pendingResults.clear();
162
+ queryCtx.turnToolCallIds = [];
163
+ queryCtx.resetTurnState(model);
164
+
165
+ const cwd = options?.cwd ?? deps.nowCwd?.() ?? process.cwd();
166
+ const config = deps.config ?? loadConfig(cwd);
167
+ const longContext = deps.longContext ?? longContextFromConfig(config);
168
+ const cliModel = claudeCodeModelId(model, longContext);
169
+ const promptBlocks = extractUserPromptBlocks(context.messages);
170
+ let promptText = extractUserPrompt(context.messages) ?? "";
171
+ if (!promptText && !promptBlocks) promptText = "[continue]";
172
+
173
+ const promptStream = makePromptStream();
174
+ void promptStream.push(userMessage(promptBlocks ?? [{ type: "text", text: promptText }])).catch(() => {});
175
+ queryCtx.promptStream = promptStream;
176
+ const mcpServers = buildMcpServers(mcpTools, queryCtx);
177
+ const effort = resolveEffort(options?.reasoning, model.thinkingLevelMap);
178
+ const extraArgs: Record<string, string | null> = { model: cliModel };
179
+ if (config.provider?.strictMcpConfig !== false) extraArgs["strict-mcp-config"] = null;
180
+ if (effort) extraArgs["thinking-display"] = "summarized";
181
+
182
+ const queryOptions: Record<string, unknown> = {
183
+ cwd,
184
+ env: { ...process.env, ...CC_CHILD_ENV },
185
+ tools: [],
186
+ permissionMode: "bypassPermissions",
187
+ includePartialMessages: true,
188
+ settings: {
189
+ ...claudeCodeSettings(config.provider),
190
+ claudeMdExcludes: CLAUDE_MD_EXCLUDES,
191
+ includeGitInstructions: false,
192
+ },
193
+ systemPrompt: {
194
+ type: "preset",
195
+ preset: "claude_code",
196
+ append: context.systemPrompt,
197
+ },
198
+ extraArgs,
199
+ ...(effort ? { effort } : {}),
200
+ ...(mcpServers ? { mcpServers } : {}),
201
+ ...(lastSessionId ? { resume: lastSessionId } : {}),
202
+ ...(config.provider?.pathToClaudeCodeExecutable
203
+ ? { pathToClaudeCodeExecutable: config.provider.pathToClaudeCodeExecutable }
204
+ : {}),
205
+ };
206
+
207
+ const queryFn = deps.query ?? loadAgentSdkQuery();
208
+ let wasAborted = false;
209
+ const sdkQuery = queryFn({ prompt: promptStream.stream, options: queryOptions });
210
+ queryCtx.activeQuery = sdkQuery;
211
+ activeQueryContexts.add(queryCtx);
212
+
213
+ const onAbort = () => {
214
+ wasAborted = true;
215
+ promptStream.fail(new Error("Operation aborted"));
216
+ queryCtx.releasePendingToolCalls("Operation aborted");
217
+ void sdkQuery.interrupt?.();
218
+ try {
219
+ sdkQuery.close?.();
220
+ } catch {
221
+ /* ignore */
222
+ }
223
+ };
224
+ if (options?.signal) {
225
+ if (options.signal.aborted) onAbort();
226
+ else options.signal.addEventListener("abort", onAbort, { once: true });
227
+ }
228
+
229
+ consumeQuery(sdkQuery as AsyncIterable<any>, customToolNameToPi, model, () => wasAborted, queryCtx)
230
+ .then(({ capturedSessionId }) => {
231
+ if (wasAborted || options?.signal?.aborted) {
232
+ if (queryCtx.turnOutput) {
233
+ queryCtx.turnOutput.stopReason = "aborted";
234
+ queryCtx.turnOutput.errorMessage = "Operation aborted";
235
+ }
236
+ const current = queryCtx.currentPiStream;
237
+ current?.push({ type: "error", reason: "aborted", error: queryCtx.turnOutput });
238
+ markStreamComplete(current);
239
+ current?.end();
240
+ queryCtx.currentPiStream = null;
241
+ return;
242
+ }
243
+ if (capturedSessionId) lastSessionId = capturedSessionId;
244
+ if (queryCtx.activeQuery === sdkQuery) queryCtx.activeQuery = null;
245
+ finalizeCurrentStream(queryCtx, queryCtx.turnOutput?.stopReason);
246
+ })
247
+ .catch((error) => {
248
+ promptStream.fail(error instanceof Error ? error : new Error(String(error)));
249
+ if (queryCtx.turnOutput) {
250
+ queryCtx.turnOutput.stopReason = options?.signal?.aborted ? "aborted" : "error";
251
+ queryCtx.turnOutput.errorMessage ??= error instanceof Error ? error.message : String(error);
252
+ }
253
+ if (queryCtx.activeQuery === sdkQuery) {
254
+ queryCtx.releasePendingToolCalls("Query ended");
255
+ queryCtx.activeQuery = null;
256
+ }
257
+ const current = queryCtx.currentPiStream;
258
+ current?.push({
259
+ type: "error",
260
+ reason: queryCtx.turnOutput?.stopReason ?? "error",
261
+ error: queryCtx.turnOutput,
262
+ });
263
+ markStreamComplete(current);
264
+ current?.end();
265
+ queryCtx.currentPiStream = null;
266
+ })
267
+ .finally(() => {
268
+ options?.signal?.removeEventListener("abort", onAbort);
269
+ promptStream.fail(new Error("query ended"));
270
+ if (queryCtx.promptStream === promptStream) queryCtx.promptStream = null;
271
+ if (queryCtx.activeQuery === sdkQuery || queryCtx.activeQuery === null) {
272
+ queryCtx.releasePendingToolCalls("Query ended");
273
+ queryCtx.activeQuery = null;
274
+ activeQueryContexts.delete(queryCtx);
275
+ }
276
+ try {
277
+ sdkQuery.close?.();
278
+ } catch {
279
+ /* ignore */
280
+ }
281
+ });
282
+
283
+ return stream;
284
+ }
285
+
286
+ export { DEFAULT_LONG_CONTEXT };
@@ -0,0 +1,72 @@
1
+ // @ts-nocheck — vendored snapshot; host tsconfig is stricter than upstream.
2
+ import {
3
+ emptyUsage,
4
+ type AssistantMessage,
5
+ type AssistantMessageEventStream,
6
+ type Model,
7
+ } from "./pi-compat.ts";
8
+ import type { McpResult } from "./tools.ts";
9
+ import type { PromptStream } from "./prompt-stream.ts";
10
+
11
+ export interface PendingToolCall {
12
+ toolName: string;
13
+ resolve: (result: McpResult) => void;
14
+ }
15
+
16
+ export class QueryContext {
17
+ activeQuery: unknown | null = null;
18
+ currentPiStream: AssistantMessageEventStream | null = null;
19
+ latestCursor = 0;
20
+ pendingToolCalls = new Map<string, PendingToolCall>();
21
+ pendingResults = new Map<string, McpResult>();
22
+ turnToolCallIds: string[] = [];
23
+ promptStream: PromptStream | null = null;
24
+ rateLimitRejection: { rateLimitType?: string; resetsAt?: number } | null = null;
25
+ lastRateLimitWarnStep: number | null = null;
26
+ lastRateLimitWarnThreshold: number | undefined;
27
+ sessionId: string | undefined;
28
+
29
+ turnOutput: AssistantMessage | null = null;
30
+ turnStarted = false;
31
+ turnSawStreamEvent = false;
32
+ turnSawToolCall = false;
33
+
34
+ get turnBlocks(): Array<any> {
35
+ if (!this.turnOutput) throw new Error("turnBlocks accessed before resetTurnState");
36
+ return this.turnOutput.content;
37
+ }
38
+
39
+ releasePendingToolCalls(reason: string): void {
40
+ for (const pending of this.pendingToolCalls.values()) {
41
+ pending.resolve({ content: [{ type: "text", text: reason }] });
42
+ }
43
+ this.pendingToolCalls.clear();
44
+ this.pendingResults.clear();
45
+ }
46
+
47
+ resetTurnState(model: Model): void {
48
+ this.turnOutput = {
49
+ role: "assistant",
50
+ content: [],
51
+ api: model.api,
52
+ provider: model.provider,
53
+ model: model.id,
54
+ usage: emptyUsage(),
55
+ stopReason: "stop",
56
+ timestamp: Date.now(),
57
+ };
58
+ this.turnStarted = false;
59
+ this.turnSawStreamEvent = false;
60
+ this.turnSawToolCall = false;
61
+ }
62
+ }
63
+
64
+ let _ctx = new QueryContext();
65
+
66
+ export function ctx(): QueryContext {
67
+ return _ctx;
68
+ }
69
+
70
+ export function resetCtx(): void {
71
+ _ctx = new QueryContext();
72
+ }
@@ -0,0 +1,26 @@
1
+ /**
2
+ * Anthropic session / Keychain material that must not reach logs or telemetry.
3
+ * Complements plugins/kxm/src/redact.ts.
4
+ */
5
+
6
+ const PATTERNS: Array<{ re: RegExp; replacement: string }> = [
7
+ { re: /\bsk-ant-[A-Za-z0-9_-]{8,}\b/g, replacement: "[redacted]" },
8
+ { re: /\bANTHROPIC_(?:API_KEY|AUTH_TOKEN|SESSION_KEY)=\S+/gi, replacement: "ANTHROPIC_[redacted]" },
9
+ { re: /\bCLAUDE_(?:API_KEY|SESSION_KEY)=\S+/gi, replacement: "CLAUDE_[redacted]" },
10
+ {
11
+ re: /("?(?:sessionKey|session_key|claude_oauth_token|anthropicApiKey|sk-ant)"?\s*[:=]\s*")[^"]*(")/gi,
12
+ replacement: "$1[redacted]$2",
13
+ },
14
+ ];
15
+
16
+ export function redactClaudeBridgeSecrets(text: string): string {
17
+ let result = text;
18
+ for (const { re, replacement } of PATTERNS) {
19
+ result = result.replace(re, replacement);
20
+ }
21
+ return result;
22
+ }
23
+
24
+ export function looksLikeClaudeBridgeSecret(value: string): boolean {
25
+ return redactClaudeBridgeSecrets(value) !== value;
26
+ }
@@ -0,0 +1,124 @@
1
+ /**
2
+ * Register the Claude Bridge provider on a Pi extension host.
3
+ * Provider id stays `claude-bridge` so `claude login` credentials carry over.
4
+ */
5
+
6
+ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
7
+ import {
8
+ CLAUDE_BRIDGE_API,
9
+ PROVIDER_ID,
10
+ PROVIDER_NAME,
11
+ registeredClaudeBridgeModels,
12
+ } from "./models.ts";
13
+ import { streamClaudeBridge } from "./provider.ts";
14
+
15
+ export const CLAUDE_BRIDGE_DOUBLE_REGISTRATION_WARNING =
16
+ "kxm: standalone pi-claude-bridge is still installed (AskClaude tool is present). Remove that extension and reload. KXM will not register provider id claude-bridge.";
17
+
18
+ export const CLAUDE_BRIDGE_ALREADY_REGISTERED_WARNING =
19
+ "kxm: standalone pi-claude-bridge is still installed (AskClaude tool is present). KXM already registered provider id claude-bridge. Remove that extension and reload to avoid override or ambiguity.";
20
+
21
+ /** Tools that only the standalone extension registers. */
22
+ export const STANDALONE_CLAUDE_BRIDGE_TOOLS = Object.freeze(["AskClaude"]);
23
+
24
+ /** Real ExtensionAPI probes that can observe the standalone AskClaude tool. */
25
+ export const CLAUDE_BRIDGE_HOST_PROBE_METHODS = Object.freeze([
26
+ "getAllTools",
27
+ "getActiveTools",
28
+ "getCommands",
29
+ ] as const);
30
+
31
+ export type ClaudeBridgeRegistration = {
32
+ registered: boolean;
33
+ conflict: boolean;
34
+ warning?: string;
35
+ };
36
+
37
+ type HostProbe = Pick<ExtensionAPI, (typeof CLAUDE_BRIDGE_HOST_PROBE_METHODS)[number]>;
38
+
39
+ function asProbe(pi: object): Partial<HostProbe> {
40
+ return pi as Partial<HostProbe>;
41
+ }
42
+
43
+ function collectHostSignalNames(pi: object): string[] {
44
+ const probe = asProbe(pi);
45
+ const names: string[] = [];
46
+ try {
47
+ for (const tool of probe.getAllTools?.() ?? []) {
48
+ if (typeof tool.name === "string") names.push(tool.name);
49
+ }
50
+ } catch {
51
+ /* getAllTools may throw before the runner is bound */
52
+ }
53
+ try {
54
+ for (const name of probe.getActiveTools?.() ?? []) {
55
+ if (typeof name === "string") names.push(name);
56
+ }
57
+ } catch {
58
+ /* getActiveTools may throw before the runner is bound */
59
+ }
60
+ try {
61
+ for (const command of probe.getCommands?.() ?? []) {
62
+ if (typeof command.name === "string") names.push(command.name);
63
+ }
64
+ } catch {
65
+ /* getCommands may throw before the session is ready */
66
+ }
67
+ return names;
68
+ }
69
+
70
+ export function claudeBridgeStandaloneToolsPresent(pi: object): boolean {
71
+ const names = collectHostSignalNames(pi);
72
+ return STANDALONE_CLAUDE_BRIDGE_TOOLS.some((name) => names.includes(name));
73
+ }
74
+
75
+ export function shouldSkipClaudeBridgeRegistration(pi: object): boolean {
76
+ return claudeBridgeStandaloneToolsPresent(pi);
77
+ }
78
+
79
+ function boundedRegistrationWarning(message: string): string {
80
+ return message.slice(0, 400);
81
+ }
82
+
83
+ export function claudeBridgeConflictWarning(registered: boolean): string {
84
+ return boundedRegistrationWarning(
85
+ registered
86
+ ? CLAUDE_BRIDGE_ALREADY_REGISTERED_WARNING
87
+ : CLAUDE_BRIDGE_DOUBLE_REGISTRATION_WARNING,
88
+ );
89
+ }
90
+
91
+ export function claudeBridgeRegistrationNotice(
92
+ report: ClaudeBridgeRegistration | undefined,
93
+ pi?: object,
94
+ ): { message: string; type: "warning" } | undefined {
95
+ const conflict = Boolean(report?.conflict) || (pi ? claudeBridgeStandaloneToolsPresent(pi) : false);
96
+ if (!conflict) return undefined;
97
+ return {
98
+ message: claudeBridgeConflictWarning(Boolean(report?.registered)),
99
+ type: "warning",
100
+ };
101
+ }
102
+
103
+ export function registerClaudeBridgeProvider(pi: ExtensionAPI): ClaudeBridgeRegistration {
104
+ if (shouldSkipClaudeBridgeRegistration(pi)) {
105
+ return {
106
+ registered: false,
107
+ conflict: true,
108
+ warning: CLAUDE_BRIDGE_DOUBLE_REGISTRATION_WARNING,
109
+ };
110
+ }
111
+ if (typeof pi.registerProvider !== "function") {
112
+ return { registered: false, conflict: false };
113
+ }
114
+
115
+ pi.registerProvider(PROVIDER_ID, {
116
+ name: PROVIDER_NAME,
117
+ baseUrl: PROVIDER_ID,
118
+ api: CLAUDE_BRIDGE_API,
119
+ models: registeredClaudeBridgeModels(),
120
+ streamSimple: streamClaudeBridge,
121
+ } as unknown as Parameters<ExtensionAPI["registerProvider"]>[1]);
122
+
123
+ return { registered: true, conflict: false };
124
+ }