@geohar/pi-mcp-combiner 0.14.3 → 0.16.0-dev.1
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 +71 -48
- package/dist/client/config-ladder.d.ts +1 -1
- package/dist/client/config-ladder.d.ts.map +1 -1
- package/dist/client/control.d.ts +1 -1
- package/dist/client/control.d.ts.map +1 -1
- package/dist/client/elicitation.d.ts +4 -2
- package/dist/client/elicitation.d.ts.map +1 -1
- package/dist/client/elicitation.js +8 -2
- package/dist/client/elicitation.js.map +1 -1
- package/dist/client/footer.d.ts +1 -1
- package/dist/client/footer.d.ts.map +1 -1
- package/dist/client/panel.d.ts +1 -1
- package/dist/client/panel.d.ts.map +1 -1
- package/dist/client/panel.js +1 -1
- package/dist/client/panel.js.map +1 -1
- package/dist/client/prompts.d.ts +1 -1
- package/dist/client/prompts.d.ts.map +1 -1
- package/dist/client/proxy-tool.d.ts +1 -1
- package/dist/client/proxy-tool.d.ts.map +1 -1
- package/dist/client/resource-naming.d.ts +8 -0
- package/dist/client/resource-naming.d.ts.map +1 -0
- package/dist/client/resource-naming.js +33 -0
- package/dist/client/resource-naming.js.map +1 -0
- package/dist/client/settings.d.ts +5 -4
- package/dist/client/settings.d.ts.map +1 -1
- package/dist/client/settings.js +3 -3
- package/dist/client/settings.js.map +1 -1
- package/dist/client/tool-matching.d.ts +32 -0
- package/dist/client/tool-matching.d.ts.map +1 -0
- package/dist/client/tool-matching.js +43 -0
- package/dist/client/tool-matching.js.map +1 -0
- package/dist/client/types.d.ts +71 -0
- package/dist/client/types.d.ts.map +1 -0
- package/dist/client/types.js +16 -0
- package/dist/client/types.js.map +1 -0
- package/dist/client/widget-support.d.ts +17 -0
- package/dist/client/widget-support.d.ts.map +1 -0
- package/dist/client/widget-support.js +89 -0
- package/dist/client/widget-support.js.map +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +109 -53
- package/dist/index.js.map +1 -1
- package/dist/{client/connection.d.ts → native/combiner-connection.d.ts} +46 -38
- package/dist/native/combiner-connection.d.ts.map +1 -0
- package/dist/native/combiner-connection.js +412 -0
- package/dist/native/combiner-connection.js.map +1 -0
- package/dist/native/index.d.ts +30 -0
- package/dist/native/index.d.ts.map +1 -0
- package/dist/native/index.js +61 -0
- package/dist/native/index.js.map +1 -0
- package/dist/native/resources.d.ts +18 -0
- package/dist/native/resources.d.ts.map +1 -0
- package/dist/native/resources.js +97 -0
- package/dist/native/resources.js.map +1 -0
- package/dist/native/tool-surface.d.ts +26 -0
- package/dist/native/tool-surface.d.ts.map +1 -0
- package/dist/native/tool-surface.js +113 -0
- package/dist/native/tool-surface.js.map +1 -0
- package/dist/pi.d.ts +19 -0
- package/dist/pi.d.ts.map +1 -1
- package/package.json +3 -5
- package/src/client/config-ladder.ts +1 -1
- package/src/client/control.ts +1 -1
- package/src/client/elicitation.ts +10 -2
- package/src/client/footer.ts +1 -1
- package/src/client/panel.ts +2 -2
- package/src/client/prompts.ts +1 -1
- package/src/client/proxy-tool.ts +1 -1
- package/src/client/resource-naming.ts +36 -0
- package/src/client/settings.ts +7 -6
- package/src/client/tool-matching.ts +69 -0
- package/src/client/types.ts +80 -0
- package/src/client/widget-support.ts +86 -0
- package/src/index.ts +109 -65
- package/src/native/combiner-connection.ts +483 -0
- package/src/native/index.ts +100 -0
- package/src/native/resources.ts +112 -0
- package/src/native/tool-surface.ts +147 -0
- package/src/pi.ts +16 -0
- package/dist/client/connection.d.ts.map +0 -1
- package/dist/client/connection.js +0 -332
- package/dist/client/connection.js.map +0 -1
- package/dist/client/direct-tools.d.ts +0 -41
- package/dist/client/direct-tools.d.ts.map +0 -1
- package/dist/client/direct-tools.js +0 -122
- package/dist/client/direct-tools.js.map +0 -1
- package/dist/client/resources.d.ts +0 -20
- package/dist/client/resources.d.ts.map +0 -1
- package/dist/client/resources.js +0 -119
- package/dist/client/resources.js.map +0 -1
- package/dist/client/script.d.ts +0 -10
- package/dist/client/script.d.ts.map +0 -1
- package/dist/client/script.js +0 -130
- package/dist/client/script.js.map +0 -1
- package/src/client/connection.ts +0 -373
- package/src/client/direct-tools.ts +0 -146
- package/src/client/resources.ts +0 -137
- package/src/client/script.ts +0 -150
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/** A combined-tool allowlist spec from the ladder's `combiner.directTools` key:
|
|
2
|
+
* a glob allowlist, or "search" (legacy adapter semantics — deprecated in the
|
|
3
|
+
* native mode, where tool_search replaces it). */
|
|
4
|
+
export type DirectToolsSpec = string[] | "search";
|
|
5
|
+
/** Names that must never be shadowed by a promoted/registered tool (pi builtins).
|
|
6
|
+
* Callers add their own tool names on top. */
|
|
7
|
+
export declare const RESERVED_TOOL_NAMES: readonly ["read", "bash", "edit", "write", "grep", "find", "ls", "mcp", "glob"];
|
|
8
|
+
/** Apply a server allow/deny filter to a combined tool list. Server names may
|
|
9
|
+
* contain underscores (gws_georgeharker), so matching is by PREFIX — a tool
|
|
10
|
+
* belongs to entry E iff its name starts `E_` (subsumes first-segment equality). */
|
|
11
|
+
export declare function toolServerMatches(toolName: string, serverEntry: string): boolean;
|
|
12
|
+
/** Apply a server allow/deny filter to a combined tool list (`<server>_` prefix,
|
|
13
|
+
* underscore-safe via toolServerMatches). */
|
|
14
|
+
export declare function applyServerFilter(tools: ToolSummary[], filter: {
|
|
15
|
+
allow?: string[];
|
|
16
|
+
deny?: string[];
|
|
17
|
+
} | undefined): ToolSummary[];
|
|
18
|
+
/** fnmatch-lite: `*` → any run, `?` → one char, everything else literal. */
|
|
19
|
+
export declare function globToRegExp(pattern: string): RegExp;
|
|
20
|
+
export declare function matchesGlob(name: string, pattern: string): boolean;
|
|
21
|
+
export type ToolSummary = {
|
|
22
|
+
name: string;
|
|
23
|
+
description?: string;
|
|
24
|
+
inputSchema?: unknown;
|
|
25
|
+
annotations?: {
|
|
26
|
+
readOnlyHint?: boolean;
|
|
27
|
+
destructiveHint?: boolean;
|
|
28
|
+
idempotentHint?: boolean;
|
|
29
|
+
openWorldHint?: boolean;
|
|
30
|
+
};
|
|
31
|
+
};
|
|
32
|
+
//# sourceMappingURL=tool-matching.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"tool-matching.d.ts","sourceRoot":"","sources":["../../src/client/tool-matching.ts"],"names":[],"mappings":"AAUA;;mDAEmD;AACnD,MAAM,MAAM,eAAe,GAAG,MAAM,EAAE,GAAG,QAAQ,CAAA;AAEjD;+CAC+C;AAC/C,eAAO,MAAM,mBAAmB,iFAAkF,CAAA;AAElH;;qFAEqF;AACrF,wBAAgB,iBAAiB,CAAC,QAAQ,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,GAAG,OAAO,CAEhF;AAED;8CAC8C;AAC9C,wBAAgB,iBAAiB,CAC7B,KAAK,EAAE,WAAW,EAAE,EACpB,MAAM,EAAE;IAAE,KAAK,CAAC,EAAE,MAAM,EAAE,CAAC;IAAC,IAAI,CAAC,EAAE,MAAM,EAAE,CAAA;CAAE,GAAG,SAAS,GAC1D,WAAW,EAAE,CASf;AAED,4EAA4E;AAC5E,wBAAgB,YAAY,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAMpD;AAED,wBAAgB,WAAW,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,OAAO,CAElE;AAKD,MAAM,MAAM,WAAW,GAAG;IACtB,IAAI,EAAE,MAAM,CAAA;IACZ,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,WAAW,CAAC,EAAE,OAAO,CAAA;IACrB,WAAW,CAAC,EAAE;QACV,YAAY,CAAC,EAAE,OAAO,CAAA;QACtB,eAAe,CAAC,EAAE,OAAO,CAAA;QACzB,cAAc,CAAC,EAAE,OAAO,CAAA;QACxB,aAAa,CAAC,EAAE,OAAO,CAAA;KAC1B,CAAA;CACJ,CAAA"}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
// Shared tool-name matching helpers — extracted from legacy-client/direct-tools.ts
|
|
2
|
+
// during the legacy-client factoring so the NATIVE tool surface (src/native/) and the
|
|
3
|
+
// client half (src/client/: config-ladder, panel) can use them without importing the
|
|
4
|
+
// deprecated legacy-client/ directory. When legacy-client/ is deleted, this file
|
|
5
|
+
// survives untouched.
|
|
6
|
+
//
|
|
7
|
+
// These are the stable contract:
|
|
8
|
+
// - tool names arrive with the combiner's baked `<server>_` prefix
|
|
9
|
+
// - server attribution is PREFIX-based (underscore-safe), not string equality
|
|
10
|
+
/** Names that must never be shadowed by a promoted/registered tool (pi builtins).
|
|
11
|
+
* Callers add their own tool names on top. */
|
|
12
|
+
export const RESERVED_TOOL_NAMES = ["read", "bash", "edit", "write", "grep", "find", "ls", "mcp", "glob"];
|
|
13
|
+
/** Apply a server allow/deny filter to a combined tool list. Server names may
|
|
14
|
+
* contain underscores (gws_georgeharker), so matching is by PREFIX — a tool
|
|
15
|
+
* belongs to entry E iff its name starts `E_` (subsumes first-segment equality). */
|
|
16
|
+
export function toolServerMatches(toolName, serverEntry) {
|
|
17
|
+
return toolName.startsWith(`${serverEntry}_`);
|
|
18
|
+
}
|
|
19
|
+
/** Apply a server allow/deny filter to a combined tool list (`<server>_` prefix,
|
|
20
|
+
* underscore-safe via toolServerMatches). */
|
|
21
|
+
export function applyServerFilter(tools, filter) {
|
|
22
|
+
if (!filter)
|
|
23
|
+
return tools;
|
|
24
|
+
if (filter.allow?.length) {
|
|
25
|
+
return tools.filter((t) => filter.allow.some((e) => toolServerMatches(t.name, e)));
|
|
26
|
+
}
|
|
27
|
+
if (filter.deny?.length) {
|
|
28
|
+
return tools.filter((t) => !filter.deny.some((e) => toolServerMatches(t.name, e)));
|
|
29
|
+
}
|
|
30
|
+
return tools;
|
|
31
|
+
}
|
|
32
|
+
/** fnmatch-lite: `*` → any run, `?` → one char, everything else literal. */
|
|
33
|
+
export function globToRegExp(pattern) {
|
|
34
|
+
const escaped = pattern
|
|
35
|
+
.replace(/[.+^${}()|[\]\\]/g, "\\$&")
|
|
36
|
+
.replace(/\*/g, ".*")
|
|
37
|
+
.replace(/\?/g, ".");
|
|
38
|
+
return new RegExp(`^${escaped}$`, "i");
|
|
39
|
+
}
|
|
40
|
+
export function matchesGlob(name, pattern) {
|
|
41
|
+
return globToRegExp(pattern).test(name);
|
|
42
|
+
}
|
|
43
|
+
//# sourceMappingURL=tool-matching.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"tool-matching.js","sourceRoot":"","sources":["../../src/client/tool-matching.ts"],"names":[],"mappings":"AAAA,mFAAmF;AACnF,sFAAsF;AACtF,qFAAqF;AACrF,iFAAiF;AACjF,sBAAsB;AACtB,EAAE;AACF,iCAAiC;AACjC,qEAAqE;AACrE,gFAAgF;AAOhF;+CAC+C;AAC/C,MAAM,CAAC,MAAM,mBAAmB,GAAG,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,KAAK,EAAE,MAAM,CAAU,CAAA;AAElH;;qFAEqF;AACrF,MAAM,UAAU,iBAAiB,CAAC,QAAgB,EAAE,WAAmB;IACnE,OAAO,QAAQ,CAAC,UAAU,CAAC,GAAG,WAAW,GAAG,CAAC,CAAA;AACjD,CAAC;AAED;8CAC8C;AAC9C,MAAM,UAAU,iBAAiB,CAC7B,KAAoB,EACpB,MAAyD;IAEzD,IAAI,CAAC,MAAM;QAAE,OAAO,KAAK,CAAA;IACzB,IAAI,MAAM,CAAC,KAAK,EAAE,MAAM,EAAE,CAAC;QACvB,OAAO,KAAK,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,MAAM,CAAC,KAAM,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,iBAAiB,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC,CAAC,CAAA;IACvF,CAAC;IACD,IAAI,MAAM,CAAC,IAAI,EAAE,MAAM,EAAE,CAAC;QACtB,OAAO,KAAK,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,MAAM,CAAC,IAAK,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,iBAAiB,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC,CAAC,CAAA;IACvF,CAAC;IACD,OAAO,KAAK,CAAA;AAChB,CAAC;AAED,4EAA4E;AAC5E,MAAM,UAAU,YAAY,CAAC,OAAe;IACxC,MAAM,OAAO,GAAG,OAAO;SAClB,OAAO,CAAC,mBAAmB,EAAE,MAAM,CAAC;SACpC,OAAO,CAAC,KAAK,EAAE,IAAI,CAAC;SACpB,OAAO,CAAC,KAAK,EAAE,GAAG,CAAC,CAAA;IACxB,OAAO,IAAI,MAAM,CAAC,IAAI,OAAO,GAAG,EAAE,GAAG,CAAC,CAAA;AAC1C,CAAC;AAED,MAAM,UAAU,WAAW,CAAC,IAAY,EAAE,OAAe;IACrD,OAAO,YAAY,CAAC,OAAO,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAA;AAC3C,CAAC"}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import type { ServerFilter } from "./config-ladder.js";
|
|
2
|
+
import type { ElicitUi } from "./elicitation.js";
|
|
3
|
+
export type LogFn = (level: "info" | "warn" | "error", message: string) => void;
|
|
4
|
+
export type ResolvedConnection = {
|
|
5
|
+
/** Base URL including /mcp, WITHOUT any token path (e.g. http://127.0.0.1:9741/mcp). */
|
|
6
|
+
baseUrl: string;
|
|
7
|
+
/** Env var holding the inbound bearer token (read at connect time), if any. */
|
|
8
|
+
bearerTokenEnv?: string;
|
|
9
|
+
/** Explicit token already present in the configured URL path (user override). */
|
|
10
|
+
urlToken?: string;
|
|
11
|
+
};
|
|
12
|
+
export type ConnectionState = "disconnected" | "connecting" | "connected" | "failed";
|
|
13
|
+
export type ToolSummary = {
|
|
14
|
+
name: string;
|
|
15
|
+
description?: string;
|
|
16
|
+
inputSchema?: unknown;
|
|
17
|
+
};
|
|
18
|
+
export type PromptArgument = {
|
|
19
|
+
name: string;
|
|
20
|
+
description?: string;
|
|
21
|
+
required?: boolean;
|
|
22
|
+
};
|
|
23
|
+
export type PromptSummary = {
|
|
24
|
+
name: string;
|
|
25
|
+
title?: string;
|
|
26
|
+
description?: string;
|
|
27
|
+
arguments?: PromptArgument[];
|
|
28
|
+
};
|
|
29
|
+
export type ResourceSummary = {
|
|
30
|
+
uri: string;
|
|
31
|
+
name?: string;
|
|
32
|
+
description?: string;
|
|
33
|
+
mimeType?: string;
|
|
34
|
+
_meta?: Record<string, unknown>;
|
|
35
|
+
};
|
|
36
|
+
/** Optional callbacks fired by the connection (set from index.ts wiring). */
|
|
37
|
+
export type ConnectionHooks = {
|
|
38
|
+
/** tools/list_changed arrived — caches invalidated; refresh derived surfaces. */
|
|
39
|
+
onToolsChanged?: () => void;
|
|
40
|
+
/** connection state transitioned — refresh footer/status surfaces. */
|
|
41
|
+
onStateChange?: (state: ConnectionState) => void;
|
|
42
|
+
/** Stage 2: the combiner announced a widget UI URL while an agent tool call
|
|
43
|
+
* is held in flight — the client should open it (gated by uiAutoOpen). */
|
|
44
|
+
onWidgetUrl?: (url: string) => void;
|
|
45
|
+
};
|
|
46
|
+
/** The connection surface every module types against — the duck-parity contract
|
|
47
|
+
* both the retired SDK connection and the native pi-mcp connection implemented. */
|
|
48
|
+
export interface CombinerConnection {
|
|
49
|
+
state: ConnectionState;
|
|
50
|
+
lastError: string | undefined;
|
|
51
|
+
sessionToken: string | undefined;
|
|
52
|
+
setToken(token: string): void;
|
|
53
|
+
rebind(conn: ResolvedConnection): boolean;
|
|
54
|
+
reset(reason: string): Promise<void>;
|
|
55
|
+
ensureConnected(): Promise<unknown>;
|
|
56
|
+
listTools(force?: boolean): Promise<ToolSummary[]>;
|
|
57
|
+
listPrompts(force?: boolean): Promise<PromptSummary[]>;
|
|
58
|
+
getPrompt(name: string, args: Record<string, string>): Promise<unknown>;
|
|
59
|
+
listResources(force?: boolean): Promise<ResourceSummary[]>;
|
|
60
|
+
readResource(uri: string): Promise<unknown>;
|
|
61
|
+
callTool(name: string, args: Record<string, unknown> | undefined): Promise<unknown>;
|
|
62
|
+
controlOrigin(): string;
|
|
63
|
+
uiUrlFor(resourceUri: string): string;
|
|
64
|
+
applyFilter(filter: ServerFilter): Promise<void>;
|
|
65
|
+
health(): Promise<unknown>;
|
|
66
|
+
setHooks(hooks: ConnectionHooks): void;
|
|
67
|
+
bindUi(ui: ElicitUi): void;
|
|
68
|
+
}
|
|
69
|
+
/** Build the tokened endpoint URL: <origin>/mcp/<token> (combiner's URL form). */
|
|
70
|
+
export declare function tokenedUrl(baseUrl: string, token: string): string;
|
|
71
|
+
//# sourceMappingURL=types.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/client/types.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
// Shared connection contract: the types every surface (router, panel, footer,
|
|
2
|
+
// prompts, resources, the extension wiring) types against. The concrete class
|
|
3
|
+
// lives in native/combiner-connection.ts (the pi-mcp transport); this module
|
|
4
|
+
// owns the INTERFACE so no surface depends on the implementation.
|
|
5
|
+
/** Build the tokened endpoint URL: <origin>/mcp/<token> (combiner's URL form). */
|
|
6
|
+
export function tokenedUrl(baseUrl, token) {
|
|
7
|
+
try {
|
|
8
|
+
const u = new URL(baseUrl);
|
|
9
|
+
u.pathname = `${u.pathname.replace(/\/+$/, "")}/${token}`;
|
|
10
|
+
return u.toString();
|
|
11
|
+
}
|
|
12
|
+
catch {
|
|
13
|
+
return `${baseUrl.replace(/\/+$/, "")}/${token}`;
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
//# sourceMappingURL=types.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.js","sourceRoot":"","sources":["../../src/client/types.ts"],"names":[],"mappings":"AAAA,8EAA8E;AAC9E,8EAA8E;AAC9E,6EAA6E;AAC7E,kEAAkE;AAmElE,kFAAkF;AAClF,MAAM,UAAU,UAAU,CAAC,OAAe,EAAE,KAAa;IACrD,IAAI,CAAC;QACD,MAAM,CAAC,GAAG,IAAI,GAAG,CAAC,OAAO,CAAC,CAAA;QAC1B,CAAC,CAAC,QAAQ,GAAG,GAAG,CAAC,CAAC,QAAQ,CAAC,OAAO,CAAC,MAAM,EAAE,EAAE,CAAC,IAAI,KAAK,EAAE,CAAA;QACzD,OAAO,CAAC,CAAC,QAAQ,EAAE,CAAA;IACvB,CAAC;IAAC,MAAM,CAAC;QACL,OAAO,GAAG,OAAO,CAAC,OAAO,CAAC,MAAM,EAAE,EAAE,CAAC,IAAI,KAAK,EAAE,CAAA;IACpD,CAAC;AACL,CAAC"}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { ResourceSummary } from "./types.js";
|
|
2
|
+
export declare const MAX_RESULT_CHARS: number;
|
|
3
|
+
export declare const MCP_APP_MIME = "text/html;profile=mcp-app";
|
|
4
|
+
/** True for interactive ext-apps resources: read_* returns their markup AND
|
|
5
|
+
* surfaces the combiner UI-host URL (which serves the live widget). */
|
|
6
|
+
export declare function isInteractiveResource(r: ResourceSummary): boolean;
|
|
7
|
+
/** Platform browser open — best-effort; failures are fine (the URL is in the
|
|
8
|
+
* result text anyway). Spawned detached so pi退出 doesn't kill the browser. */
|
|
9
|
+
export declare function openInBrowser(url: string): void;
|
|
10
|
+
/** The ui:// resource a tool result carries (port of the adapter's
|
|
11
|
+
* getToolUiResourceUri): `_meta["ui/resourceUri"]` or `_meta.ui.resourceUri`. */
|
|
12
|
+
export declare function toolUiResourceUri(result: unknown): string | undefined;
|
|
13
|
+
/** MCP readResource result → guarded plain text: text contents pass through
|
|
14
|
+
* (pretty-printed when small JSON; with the shared size guard), images become
|
|
15
|
+
* markers, base64 blobs become byte notes. */
|
|
16
|
+
export declare function renderResourceResult(result: unknown): string;
|
|
17
|
+
//# sourceMappingURL=widget-support.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"widget-support.d.ts","sourceRoot":"","sources":["../../src/client/widget-support.ts"],"names":[],"mappings":"AAWA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,YAAY,CAAA;AAEjD,eAAO,MAAM,gBAAgB,QAAY,CAAA;AAEzC,eAAO,MAAM,YAAY,8BAA8B,CAAA;AAEvD;wEACwE;AACxE,wBAAgB,qBAAqB,CAAC,CAAC,EAAE,eAAe,GAAG,OAAO,CAEjE;AAED;8EAC8E;AAC9E,wBAAgB,aAAa,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,CAO/C;AAED;iFACiF;AACjF,wBAAgB,iBAAiB,CAAC,MAAM,EAAE,OAAO,GAAG,MAAM,GAAG,SAAS,CAOrE;AAiBD;;+CAE+C;AAC/C,wBAAgB,oBAAoB,CAAC,MAAM,EAAE,OAAO,GAAG,MAAM,CAsB5D"}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
// Widget/interactive-resource support — extracted from legacy-client/render.ts,
|
|
2
|
+
// proxy-tool.ts, and resources.ts during the legacy-client factoring.
|
|
3
|
+
//
|
|
4
|
+
// These pieces are NOT proxy-surface specific: they are the combiner UI-host,
|
|
5
|
+
// mcp-app-resource, and browser-open mechanics that SURVIVE into the native mode
|
|
6
|
+
// (widget holds via the connection's onWidgetUrl log-tap; read_* tools for ui://
|
|
7
|
+
// resources, which pi's built-in read_mcp_resource deliberately does NOT cover).
|
|
8
|
+
// When legacy-client/ is deleted, this file keeps them alive; legacy/resources.ts
|
|
9
|
+
// imports them from here in the meantime.
|
|
10
|
+
import { spawn } from "node:child_process";
|
|
11
|
+
export const MAX_RESULT_CHARS = 16 * 1024;
|
|
12
|
+
export const MCP_APP_MIME = "text/html;profile=mcp-app";
|
|
13
|
+
/** True for interactive ext-apps resources: read_* returns their markup AND
|
|
14
|
+
* surfaces the combiner UI-host URL (which serves the live widget). */
|
|
15
|
+
export function isInteractiveResource(r) {
|
|
16
|
+
return r.mimeType === MCP_APP_MIME || r.uri.startsWith("ui://");
|
|
17
|
+
}
|
|
18
|
+
/** Platform browser open — best-effort; failures are fine (the URL is in the
|
|
19
|
+
* result text anyway). Spawned detached so pi退出 doesn't kill the browser. */
|
|
20
|
+
export function openInBrowser(url) {
|
|
21
|
+
const cmd = process.platform === "darwin" ? ["open"] : ["xdg-open"];
|
|
22
|
+
try {
|
|
23
|
+
spawn(cmd[0], [...cmd.slice(1), url], { stdio: "ignore", detached: true }).unref?.();
|
|
24
|
+
}
|
|
25
|
+
catch {
|
|
26
|
+
// no opener available — the URL is in the result text anyway
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
/** The ui:// resource a tool result carries (port of the adapter's
|
|
30
|
+
* getToolUiResourceUri): `_meta["ui/resourceUri"]` or `_meta.ui.resourceUri`. */
|
|
31
|
+
export function toolUiResourceUri(result) {
|
|
32
|
+
const meta = result?._meta;
|
|
33
|
+
if (typeof meta !== "object" || meta === null)
|
|
34
|
+
return undefined;
|
|
35
|
+
const m = meta;
|
|
36
|
+
const nested = m.ui?.resourceUri;
|
|
37
|
+
const uri = typeof nested === "string" ? nested : m["ui/resourceUri"];
|
|
38
|
+
return typeof uri === "string" && uri.startsWith("ui://") ? uri : undefined;
|
|
39
|
+
}
|
|
40
|
+
const maxPrettyCharsDefault = 2048;
|
|
41
|
+
function prettifyIfSmallJson(text, maxPrettyChars = maxPrettyCharsDefault) {
|
|
42
|
+
const trimmed = text.trim();
|
|
43
|
+
if (trimmed.length === 0 || trimmed.length > maxPrettyChars)
|
|
44
|
+
return text;
|
|
45
|
+
if (!(trimmed.startsWith("{") || trimmed.startsWith("[")))
|
|
46
|
+
return text;
|
|
47
|
+
try {
|
|
48
|
+
const parsed = JSON.parse(trimmed);
|
|
49
|
+
if (typeof parsed !== "object" || parsed === null)
|
|
50
|
+
return text;
|
|
51
|
+
return JSON.stringify(parsed, null, 2);
|
|
52
|
+
}
|
|
53
|
+
catch {
|
|
54
|
+
return text;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
/** MCP readResource result → guarded plain text: text contents pass through
|
|
58
|
+
* (pretty-printed when small JSON; with the shared size guard), images become
|
|
59
|
+
* markers, base64 blobs become byte notes. */
|
|
60
|
+
export function renderResourceResult(result) {
|
|
61
|
+
const contents = result?.contents;
|
|
62
|
+
if (!Array.isArray(contents) || contents.length === 0)
|
|
63
|
+
return "(empty resource)";
|
|
64
|
+
const parts = [];
|
|
65
|
+
for (const c of contents) {
|
|
66
|
+
if (typeof c !== "object" || c === null)
|
|
67
|
+
continue;
|
|
68
|
+
const item = c;
|
|
69
|
+
const mime = typeof item.mimeType === "string" ? item.mimeType : "unknown";
|
|
70
|
+
if (typeof item.text === "string") {
|
|
71
|
+
parts.push(prettifyIfSmallJson(item.text));
|
|
72
|
+
}
|
|
73
|
+
else if (typeof item.blob === "string") {
|
|
74
|
+
const bytes = Math.floor((item.blob.length * 3) / 4); // base64 → approx decoded size
|
|
75
|
+
parts.push(`[binary ${mime}, ~${bytes} bytes — uri ${item.uri ?? "?"}]`);
|
|
76
|
+
}
|
|
77
|
+
else if (mime.startsWith("image/")) {
|
|
78
|
+
parts.push(`[image: ${mime} — uri ${item.uri ?? "?"}]`);
|
|
79
|
+
}
|
|
80
|
+
else {
|
|
81
|
+
parts.push(`[unreadable content: ${mime} — uri ${item.uri ?? "?"}]`);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
const text = parts.join("\n").trim() || "(empty resource)";
|
|
85
|
+
if (text.length <= MAX_RESULT_CHARS)
|
|
86
|
+
return text;
|
|
87
|
+
return `${text.slice(0, MAX_RESULT_CHARS)}\n\n… (resource truncated at ${MAX_RESULT_CHARS} chars)`;
|
|
88
|
+
}
|
|
89
|
+
//# sourceMappingURL=widget-support.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"widget-support.js","sourceRoot":"","sources":["../../src/client/widget-support.ts"],"names":[],"mappings":"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"}
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAiCA,OAAO,KAAK,EAER,YAAY,
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAiCA,OAAO,KAAK,EAER,YAAY,EAMf,MAAM,SAAS,CAAA;AAyPhB,MAAM,CAAC,OAAO,UAAU,WAAW,CAAC,EAAE,EAAE,YAAY,GAAG,IAAI,CA4e1D"}
|
package/dist/index.js
CHANGED
|
@@ -31,17 +31,20 @@ import { homedir } from "node:os";
|
|
|
31
31
|
import { basename, dirname, join } from "node:path";
|
|
32
32
|
import { fileURLToPath } from "node:url";
|
|
33
33
|
import { resolveSharedserver } from "./sharedserver-resolve.js";
|
|
34
|
-
import {
|
|
35
|
-
|
|
34
|
+
import { NativeCombinerConnection, activateNativeMode } from "./native/index.js";
|
|
35
|
+
// ── TOOL SURFACE ───────────────────────────────────────────────────────────────
|
|
36
|
+
// The mcp router (search/describe/call on the owned connection) + the native
|
|
37
|
+
// per-tool registration (pi-1.0 exposure/namespace/annotations) + interactive
|
|
38
|
+
// read_<resource> tools. Shared surface modules live in src/client/; the transport
|
|
39
|
+
// is src/native/combiner-connection.ts (pi-mcp, peer dep).
|
|
40
|
+
import { createMcpTool } from "./client/proxy-tool.js";
|
|
41
|
+
import { openInBrowser } from "./client/widget-support.js";
|
|
36
42
|
import { agentDir, loadSettings, settingsPath } from "./client/settings.js";
|
|
37
43
|
import { resolveSessionConfig } from "./client/config-ladder.js";
|
|
38
44
|
import { metaToolCall, statusText as controlStatusText } from "./client/control.js";
|
|
39
45
|
import { openCombinerPanel } from "./client/panel.js";
|
|
40
46
|
import { syncPromptCommands } from "./client/prompts.js";
|
|
41
|
-
import { syncResourceTools } from "./client/resources.js";
|
|
42
|
-
import { activateFromSearch, applyServerFilter, RESERVED_TOOL_NAMES, syncAllowlistTools, } from "./client/direct-tools.js";
|
|
43
47
|
import { updateFooter } from "./client/footer.js";
|
|
44
|
-
import { createScriptTool } from "./client/script.js";
|
|
45
48
|
const DEFAULT_PORT = 9741;
|
|
46
49
|
const DEFAULT_NAME = "mcp-combiner";
|
|
47
50
|
const DEFAULT_GRACE = "30m";
|
|
@@ -288,13 +291,25 @@ export default function mcpCombiner(pi) {
|
|
|
288
291
|
uiLevel = "warn";
|
|
289
292
|
else if (level === "error")
|
|
290
293
|
uiLevel = "error";
|
|
291
|
-
|
|
294
|
+
try {
|
|
295
|
+
uiNotify(line, uiLevel);
|
|
296
|
+
}
|
|
297
|
+
catch {
|
|
298
|
+
// The captured ctx went stale (reload/session replacement kills
|
|
299
|
+
// extension ctxs — pi 1.0 asserts on use). Degrade to stderr and
|
|
300
|
+
// drop the capture so later logs skip the dead path entirely:
|
|
301
|
+
// an async log line (e.g. a superseded connection's reset) must
|
|
302
|
+
// NEVER crash the process.
|
|
303
|
+
uiNotify = undefined;
|
|
304
|
+
process.stderr.write(`${line}\n`);
|
|
305
|
+
}
|
|
292
306
|
}
|
|
293
307
|
else if (level !== "info") {
|
|
294
308
|
process.stderr.write(`${line}\n`);
|
|
295
309
|
}
|
|
296
310
|
};
|
|
297
311
|
let connection;
|
|
312
|
+
let nativeSurface;
|
|
298
313
|
// Session-config HOLDER: everything the ladder shapes, resolved against a SESSION
|
|
299
314
|
// cwd (worktree subagents / project switches get their own project layers).
|
|
300
315
|
// Factory-time resolution with process.cwd() is the pre-session default; every
|
|
@@ -306,17 +321,6 @@ export default function mcpCombiner(pi) {
|
|
|
306
321
|
bearerTokenEnv: cfg.bearerTokenEnv ?? "MCP_COMBINER_AUTH_TOKEN",
|
|
307
322
|
urlToken: cfg.urlToken,
|
|
308
323
|
});
|
|
309
|
-
const registeredDirect = new Set();
|
|
310
|
-
const directReserved = new Set(RESERVED_TOOL_NAMES);
|
|
311
|
-
directReserved.add(toolName);
|
|
312
|
-
directReserved.add(`${toolName}Script`);
|
|
313
|
-
const directDepsFor = (conn) => ({
|
|
314
|
-
connection: conn,
|
|
315
|
-
registered: registeredDirect,
|
|
316
|
-
reserved: directReserved,
|
|
317
|
-
register: (tool) => pi.registerTool(tool),
|
|
318
|
-
log: clientLog,
|
|
319
|
-
});
|
|
320
324
|
if (adapterEnabled) {
|
|
321
325
|
sessionCfg = resolveSessionConfig({
|
|
322
326
|
cwd: process.cwd(),
|
|
@@ -324,17 +328,47 @@ export default function mcpCombiner(pi) {
|
|
|
324
328
|
settingsUrl: settings.url,
|
|
325
329
|
defaultUrl: defaultCombinerUrl(),
|
|
326
330
|
});
|
|
327
|
-
|
|
328
|
-
|
|
331
|
+
// Zombie teardown: pi re-imports the extension module on /reload WITHOUT
|
|
332
|
+
// firing session_shutdown (the session persists, no unload hook exists), so
|
|
333
|
+
// the previous instance's McpClient + SSE stream would live on and compete
|
|
334
|
+
// for the token's elicit routing. A process-global registry survives module
|
|
335
|
+
// re-imports: the fresh factory closes every prior live connection first.
|
|
336
|
+
const g = globalThis;
|
|
337
|
+
// The teardown closures below (from prior factory runs, orphaned by pi's
|
|
338
|
+
// reload-without-session_shutdown) know how to fully retire their instance:
|
|
339
|
+
// clear the footer timer, drop the captured UI ctx (stale-ctx reads on it
|
|
340
|
+
// are a hard pi-1.0 assertion), and close the connection.
|
|
341
|
+
for (const dispose of g.__piMcpCombinerDispose ?? []) {
|
|
342
|
+
try {
|
|
343
|
+
dispose();
|
|
344
|
+
}
|
|
345
|
+
catch {
|
|
346
|
+
// teardown must never break the fresh instance's load
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
g.__piMcpCombinerDispose = [];
|
|
350
|
+
connection = new NativeCombinerConnection(connInputsFor(sessionCfg), clientLog);
|
|
351
|
+
// The mcp router registers alongside the per-tool native surface (the same
|
|
352
|
+
// owned connection — not a second proxy): tool_call gates such as
|
|
353
|
+
// pi-permission-system and pi-subagents recognize MCP calls by the tool
|
|
354
|
+
// name "mcp", and the router keeps a discovery/search surface.
|
|
329
355
|
pi.registerTool(createMcpTool({
|
|
330
356
|
connection,
|
|
331
357
|
toolName,
|
|
332
358
|
getServerFilter: () => sessionCfg?.serverFilter,
|
|
333
|
-
onSearchMatches: (tools) => sessionCfg?.directSpec === "search" ? activateFromSearch(tools, directDepsFor(activeConn)) : [],
|
|
334
359
|
}));
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
360
|
+
// Per-tool surface + interactive read_*. The FIRST registration runs at
|
|
361
|
+
// session_start (post-setToken — the connection refuses to connect
|
|
362
|
+
// without a grouping token); list_changed re-syncs ride resyncAll below.
|
|
363
|
+
nativeSurface = activateNativeMode(pi, {
|
|
364
|
+
connection,
|
|
365
|
+
directSpec: sessionCfg?.directSpec ?? [],
|
|
366
|
+
serverFilter: sessionCfg?.serverFilter,
|
|
367
|
+
exposeResources: settings.exposeResources !== false,
|
|
368
|
+
uiAutoOpen: settings.uiAutoOpen !== false,
|
|
369
|
+
warnLargeDirectExposure: settings.warnLargeDirectExposure,
|
|
370
|
+
log: clientLog,
|
|
371
|
+
});
|
|
338
372
|
if (sessionCfg.otherServers.length && !adapterPackageInstalled()) {
|
|
339
373
|
clientLog("info", `note: ${sessionCfg.otherServers.length} non-combiner server(s) in the mcp.json ladder are not ` +
|
|
340
374
|
`connected by this extension (${sessionCfg.otherServers.join(", ")})`);
|
|
@@ -350,9 +384,22 @@ export default function mcpCombiner(pi) {
|
|
|
350
384
|
if (connection) {
|
|
351
385
|
const conn = connection; // captured non-optional for the handlers below
|
|
352
386
|
const registeredPrompts = new Set();
|
|
353
|
-
const registeredResources = new Set();
|
|
354
387
|
let footerTimer;
|
|
355
388
|
let uiCtx;
|
|
389
|
+
// Full instance teardown, callable from a LATER factory run (pi re-imports
|
|
390
|
+
// the module on /reload without firing session_shutdown): clears this
|
|
391
|
+
// instance's footer timer, drops its captured UI ctx (stale-ctx reads are
|
|
392
|
+
// a hard pi-1.0 assertion), and closes its connection. The registry lives
|
|
393
|
+
// on globalThis — the one thing that survives a module re-import.
|
|
394
|
+
const teardownGlobal = globalThis;
|
|
395
|
+
(teardownGlobal.__piMcpCombinerDispose ??= []).push(() => {
|
|
396
|
+
if (footerTimer)
|
|
397
|
+
clearInterval(footerTimer);
|
|
398
|
+
footerTimer = undefined;
|
|
399
|
+
uiCtx = undefined;
|
|
400
|
+
uiNotify = undefined;
|
|
401
|
+
void conn.reset("superseded by a newer extension instance (reload)");
|
|
402
|
+
});
|
|
356
403
|
const refreshFooter = () => {
|
|
357
404
|
if (uiCtx)
|
|
358
405
|
void updateFooter(conn, uiCtx, settings.mcpFooterKey, settings.mcpFooterStatus).catch(() => undefined);
|
|
@@ -362,31 +409,27 @@ export default function mcpCombiner(pi) {
|
|
|
362
409
|
return;
|
|
363
410
|
void syncPromptCommands(pi, conn, toolName, registeredPrompts, (m, l) => clientLog(l, m)).catch(() => undefined);
|
|
364
411
|
};
|
|
365
|
-
const
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
if (!Array.isArray(spec) || spec.length === 0)
|
|
374
|
-
return;
|
|
375
|
-
void conn
|
|
376
|
-
.listTools()
|
|
377
|
-
.then((tools) => {
|
|
378
|
-
syncAllowlistTools(applyServerFilter(tools, sessionCfg?.serverFilter), spec, directDepsFor(conn));
|
|
379
|
-
})
|
|
380
|
-
.catch(() => undefined);
|
|
412
|
+
const resyncAll = () => {
|
|
413
|
+
// Prompts + footer re-sync on: list_changed notifications, reconnect
|
|
414
|
+
// catch-up (fired by the connection itself), AND session_start — a
|
|
415
|
+
// reload/re-enter with an UNCHANGED token skips any reconnect, so
|
|
416
|
+
// without this the surfaces would only ever update on notifications.
|
|
417
|
+
nativeSurface?.run();
|
|
418
|
+
syncPrompts();
|
|
419
|
+
refreshFooter();
|
|
381
420
|
};
|
|
382
421
|
conn.setHooks({
|
|
383
|
-
onToolsChanged:
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
422
|
+
onToolsChanged: resyncAll,
|
|
423
|
+
onStateChange: (state) => {
|
|
424
|
+
// Re-attach on (re)connect: a combiner bounce kills the instance our
|
|
425
|
+
// sharedserver reference was attached to, and nothing else re-takes
|
|
426
|
+
// ownership until the next session_start — an orphaned combiner then
|
|
427
|
+
// grace-stops 30m later (sharedserver `use` is idempotent per pid, so
|
|
428
|
+
// re-attaching on every connect is safe and cheap).
|
|
429
|
+
if (state === "connected")
|
|
430
|
+
attachCombiner(clientLog);
|
|
387
431
|
refreshFooter();
|
|
388
432
|
},
|
|
389
|
-
onStateChange: () => refreshFooter(),
|
|
390
433
|
// Stage 2: the widget hold announces its UI URL mid-call — open it.
|
|
391
434
|
onWidgetUrl: (url) => {
|
|
392
435
|
if (settings.uiAutoOpen !== false)
|
|
@@ -408,6 +451,11 @@ export default function mcpCombiner(pi) {
|
|
|
408
451
|
});
|
|
409
452
|
conn.rebind(connInputsFor(sessionCfg));
|
|
410
453
|
conn.setToken(sessionToken(event, ctx));
|
|
454
|
+
// The native per-tool/resource registration runs HERE — post-setToken,
|
|
455
|
+
// the first moment the grouping token exists (a factory-time run
|
|
456
|
+
// would fail with "no grouping token set"). Subsequent re-syncs ride
|
|
457
|
+
// resyncAll via onToolsChanged.
|
|
458
|
+
nativeSurface?.run();
|
|
411
459
|
// Per-project exposure: pending filters apply server-side before first connect.
|
|
412
460
|
const filter = sessionCfg.serverFilter;
|
|
413
461
|
if (filter) {
|
|
@@ -431,8 +479,6 @@ export default function mcpCombiner(pi) {
|
|
|
431
479
|
.then(() => {
|
|
432
480
|
refreshFooter();
|
|
433
481
|
syncPrompts();
|
|
434
|
-
syncResources();
|
|
435
|
-
syncDirect();
|
|
436
482
|
})
|
|
437
483
|
.catch(() => undefined);
|
|
438
484
|
}
|
|
@@ -475,7 +521,7 @@ export default function mcpCombiner(pi) {
|
|
|
475
521
|
// restart-server <srv> (combiner meta-tools through our client), system-prompt
|
|
476
522
|
// (show the injected directive), install-config [path] (write the shared mcp.json
|
|
477
523
|
// combiner entry — legacy pairing with pi-mcp-adapter, now also read by our ladder).
|
|
478
|
-
|
|
524
|
+
const combinerCommandSpec = {
|
|
479
525
|
description: "mcp-combiner — verbs: status, enable <srv>, disable <srv>, restart-server <srv>, system-prompt, install-config [path]",
|
|
480
526
|
getArgumentCompletions: (prefix) => completeVerbs(prefix),
|
|
481
527
|
handler: (args, ctx) => {
|
|
@@ -530,7 +576,15 @@ export default function mcpCombiner(pi) {
|
|
|
530
576
|
}
|
|
531
577
|
ctx.ui?.notify?.(`mcp-combiner: unknown verb "${v}". Try: ${COMMAND_VERBS.join(", ")}`, "warn");
|
|
532
578
|
},
|
|
533
|
-
}
|
|
579
|
+
};
|
|
580
|
+
pi.registerCommand("mcp-combiner", combinerCommandSpec);
|
|
581
|
+
// NOTE: we deliberately do NOT register "/mcp" — pi's built-in MCP extension
|
|
582
|
+
// owns that command and stays the native-mode transport; our surfaces ("mcp"
|
|
583
|
+
// router tool, direct <server>_<tool>, read_*) don't collide with the builtin's
|
|
584
|
+
// (codemode / tool-search / read_mcp_resource). Registering "/mcp" would
|
|
585
|
+
// displace the builtin at load (pi's loader drops replaceable built-ins on
|
|
586
|
+
// command-name collision) and with it the native support we're migrating
|
|
587
|
+
// toward.
|
|
534
588
|
if (hostOwned || !manage) {
|
|
535
589
|
// Registration is static (mcp.json) and the combiner is someone else's to run:
|
|
536
590
|
// nothing to launch. The instructions handler above still applies.
|
|
@@ -540,10 +594,7 @@ export default function mcpCombiner(pi) {
|
|
|
540
594
|
// Pi forbids starting background resources from the factory, so all of this is
|
|
541
595
|
// deferred to session_start. session_start fires again on reload/new/resume/fork
|
|
542
596
|
// within the same process; the `attachment` guard makes re-entry a no-op attach.
|
|
543
|
-
|
|
544
|
-
if (attachment)
|
|
545
|
-
return; // already attached in this process
|
|
546
|
-
const log = makeLog(ctx, notify);
|
|
597
|
+
const attachCombiner = (log) => {
|
|
547
598
|
const binary = resolveSharedserver({
|
|
548
599
|
label: "mcp-combiner",
|
|
549
600
|
minVersion: SHAREDSERVER_MIN_VERSION,
|
|
@@ -620,6 +671,11 @@ export default function mcpCombiner(pi) {
|
|
|
620
671
|
}
|
|
621
672
|
attachment = { binary, name };
|
|
622
673
|
log("info", `combiner "${name}" attached on port ${port} (${combiner.cmd} ${wrappedArgs.join(" ")})`);
|
|
674
|
+
};
|
|
675
|
+
pi.on("session_start", (_event, ctx) => {
|
|
676
|
+
if (attachment)
|
|
677
|
+
return; // already attached in this process
|
|
678
|
+
attachCombiner(makeLog(ctx, notify));
|
|
623
679
|
});
|
|
624
680
|
pi.on("session_shutdown", (event) => {
|
|
625
681
|
// Only "quit" means the Pi process is actually leaving. reload/new/resume/fork
|