pi-soly 1.13.3 β†’ 1.13.5

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
@@ -39,6 +39,26 @@ Restart pi (`/reload`), and you have:
39
39
 
40
40
  The LLM drives execution; `plan`/`execute` delegate to a `worker` subagent when one is available (via pi-subagents), with first-party delegation on the roadmap. You focus on the work.
41
41
 
42
+ ### Known install issue (upstream `pi install`)
43
+
44
+ `pi install` currently does **not** install transitive `peerDependencies` (it skips them the way `--omit=optional` would). pi-soly's MCP stack depends on `@modelcontextprotocol/ext-apps`, which in turn peer-requires `@modelcontextprotocol/sdk` (declared non-optional upstream). After `pi install npm:pi-soly` you may see:
45
+
46
+ ```
47
+ Error: Cannot find module '@modelcontextprotocol/sdk/types.js'
48
+ Require stack:
49
+ - ~/.pi/agent/npm/node_modules/@modelcontextprotocol/ext-apps/dist/src/app-bridge.js
50
+ ```
51
+
52
+ **Workaround** (one-time, after each `pi install` of pi-soly or any pi-soly-related upgrade):
53
+
54
+ ```bash
55
+ cd ~/.pi/agent/npm && npm install
56
+ ```
57
+
58
+ This makes plain npm resolve the transitive peer deps that `pi install` skipped. After this, restart pi (`/reload`) and the MCP features work.
59
+
60
+ Tracked upstream β€” fix is expected on the pi side, not here.
61
+
42
62
  ---
43
63
 
44
64
  ## 🎯 Why pi-soly?
@@ -69,7 +89,7 @@ soly status # current position + progress (no LLM round-trip)
69
89
  ### State inspection (`/soly`)
70
90
 
71
91
  ```bash
72
- /soly # interactive picker (πŸ“πŸ“„πŸ“‹πŸ’‘πŸ”¬πŸ—ΊοΈπŸ“ŠπŸ“βœ…β­πŸŽ―πŸ”„βš™οΈ)
92
+ /soly # interactive modal picker (live preview per item, ⏎ to open)
73
93
  /soly position # where am I in the plan
74
94
  /soly state # current STATE.md content
75
95
  /soly roadmap # all phases
package/commands.ts CHANGED
@@ -828,11 +828,13 @@ What must the LLM do?
828
828
  },
829
829
  };
830
830
 
831
+ // Single-width BMP glyphs only β€” astral/VS16 emoji are mis-measured by
832
+ // visibleWidth, overflow the panel, and ghost the modal on ↑↓.
831
833
  const ICONS: Record<string, string> = {
832
- position: "πŸ“", state: "πŸ“„", plan: "πŸ“‹", context: "πŸ’‘",
833
- research: "πŸ”¬", roadmap: "πŸ—ΊοΈ", progress: "πŸ“Š",
834
- phases: "πŸ“", tasks: "βœ…", task: "πŸ”Ž",
835
- features: "⭐", milestone: "🎯", reload: "πŸ”„", config: "βš™οΈ",
834
+ position: "β—Ž", state: "β–€", plan: "β–₯", context: "β—Œ",
835
+ research: "βŠ™", roadmap: "≣", progress: "β–°",
836
+ phases: "β–­", tasks: "βœ“", task: "◍",
837
+ features: "β˜…", milestone: "β—ˆ", reload: "↻", config: "β–£",
836
838
  };
837
839
 
838
840
  // Short live preview shown in the modal's preview pane per subcommand.
package/index.ts CHANGED
@@ -892,12 +892,23 @@ export default function solyExtension(pi: ExtensionAPI) {
892
892
  // Bundled into pi-soly as of v1.11.0 with UE5 session-retry fix + framed
893
893
  // notifications + compact per-server status footer.
894
894
  // We import dynamically because mcp/ has heavy deps (modelcontextprotocol/sdk)
895
- // and we want soly to still load if MCP fails for any reason.
896
- void import("./mcp/index.ts").then((m) => {
897
- try {
898
- m.default(pi);
899
- } catch (err) {
900
- console.error("[soly] MCP adapter failed to initialize:", err);
901
- }
902
- });
895
+ // and we want soly to still load if MCP fails for any reason. The import()
896
+ // itself can reject (e.g. `pi install` didn't resolve a transitive peer dep
897
+ // like @modelcontextprotocol/sdk) or throw synchronously under jiti β€” both
898
+ // must be swallowed so a broken MCP never takes down the whole agent.
899
+ try {
900
+ void import("./mcp/index.ts")
901
+ .then((m) => {
902
+ try {
903
+ m.default(pi);
904
+ } catch (err) {
905
+ console.error("[soly] MCP adapter failed to initialize:", err);
906
+ }
907
+ })
908
+ .catch((err) => {
909
+ console.error("[soly] MCP adapter unavailable (load failed):", err);
910
+ });
911
+ } catch (err) {
912
+ console.error("[soly] MCP adapter unavailable (load threw):", err);
913
+ }
903
914
  }
@@ -0,0 +1,76 @@
1
+ // =============================================================================
2
+ // ext-apps-bridge.ts β€” lazy, guarded access to @modelcontextprotocol/ext-apps
3
+ // =============================================================================
4
+ //
5
+ // `@modelcontextprotocol/ext-apps@1.7.4` (its peer is sdk `^1.29.0`, yet its
6
+ // compiled CJS `app-bridge.js` does `require("@modelcontextprotocol/sdk/types.js")`
7
+ // β€” a subpath sdk 1.29 no longer exposes to CJS) is currently broken upstream.
8
+ // Importing it statically threw at module-eval and crashed the whole pi agent.
9
+ //
10
+ // So we load it LAZILY, exactly once, behind a try/catch. If it loads, the MCP-UI
11
+ // (app-bridge) features work; if it can't, the cache is null and those features
12
+ // degrade gracefully (no UI resource URIs, empty iframe allow-list, fallback MIME)
13
+ // while core MCP keeps working. `preloadAppBridge()` is awaited early in MCP init
14
+ // so the synchronous accessors below see a populated (or null) cache.
15
+ //
16
+ // The dynamic specifier is typed as `string` on purpose so TypeScript does not
17
+ // resolve ext-apps' types (which themselves reference the missing sdk subpath).
18
+ // =============================================================================
19
+
20
+ /** Minimal shape of the bits of ext-apps/app-bridge we use. */
21
+ interface AppBridge {
22
+ RESOURCE_MIME_TYPE: string;
23
+ getToolUiResourceUri(meta: { _meta?: unknown }): string | undefined;
24
+ buildAllowAttribute(permissions: unknown): string;
25
+ }
26
+
27
+ /** Mirrors ext-apps' RESOURCE_MIME_TYPE; used when the module can't be loaded. */
28
+ const FALLBACK_RESOURCE_MIME_TYPE = "text/html;profile=mcp-app";
29
+
30
+ // `: string` (not a string literal) so `import()` is typed `Promise<any>` and TS
31
+ // never tries to resolve ext-apps' (broken) types.
32
+ const APP_BRIDGE_SPECIFIER: string = "@modelcontextprotocol/ext-apps/app-bridge";
33
+
34
+ let cached: AppBridge | null = null;
35
+ let attempted = false;
36
+
37
+ /** Load ext-apps once (idempotent), swallowing any failure. Safe to await many
38
+ * times. Call early in MCP init before the sync accessors are used. */
39
+ export async function preloadAppBridge(): Promise<void> {
40
+ if (attempted) return;
41
+ attempted = true;
42
+ try {
43
+ cached = (await import(APP_BRIDGE_SPECIFIER)) as unknown as AppBridge;
44
+ } catch {
45
+ cached = null;
46
+ console.error(
47
+ "[soly] MCP UI (ext-apps/app-bridge) could not load β€” app-bridge features are disabled " +
48
+ "(upstream ext-apps/sdk version mismatch, not a soly bug). Core MCP is unaffected.",
49
+ );
50
+ }
51
+ }
52
+
53
+ /** ext-apps' RESOURCE_MIME_TYPE when loaded, else the fallback literal. */
54
+ export function resourceMimeType(): string {
55
+ return cached?.RESOURCE_MIME_TYPE ?? FALLBACK_RESOURCE_MIME_TYPE;
56
+ }
57
+
58
+ /** ext-apps getToolUiResourceUri, or undefined when the bridge isn't available. */
59
+ export function getToolUiResourceUri(meta: { _meta?: unknown }): string | undefined {
60
+ if (!cached) return undefined;
61
+ try {
62
+ return cached.getToolUiResourceUri(meta);
63
+ } catch {
64
+ return undefined;
65
+ }
66
+ }
67
+
68
+ /** ext-apps buildAllowAttribute, or "" (most restrictive) when not available. */
69
+ export function buildAllowAttribute(permissions: unknown): string {
70
+ if (!cached) return "";
71
+ try {
72
+ return cached.buildAllowAttribute(permissions);
73
+ } catch {
74
+ return "";
75
+ }
76
+ }
package/mcp/index.ts CHANGED
@@ -13,6 +13,7 @@ import { getConfigPathFromArgv, truncateAtWord } from "./utils.ts";
13
13
  import { initializeOAuth, shutdownOAuth } from "./mcp-auth-flow.ts";
14
14
  import { createMcpDirectToolCallRenderer, renderMcpProxyToolCall, renderMcpToolResult } from "./tool-result-renderer.ts";
15
15
  import { ToolCache, cacheKey as makeCacheKey } from "./tool-cache.ts";
16
+ import { preloadAppBridge } from "./ext-apps-bridge.ts";
16
17
 
17
18
  /** Default TTL for cached MCP tool results (60s). Tools that hit a stable
18
19
  * server benefit; volatile ones are penalized for 60s β€” call sites can
@@ -131,6 +132,10 @@ export default function mcpAdapter(pi: ExtensionAPI) {
131
132
  console.error("MCP OAuth initialization failed:", err);
132
133
  });
133
134
 
135
+ // Load the (optional, sometimes-broken upstream) ext-apps UI bridge once,
136
+ // before metadata is built. Guarded β€” a failure disables UI features only.
137
+ await preloadAppBridge();
138
+
134
139
  const promise = initializeMcp(pi, ctx);
135
140
  initPromise = promise;
136
141
 
@@ -3,7 +3,7 @@ import { existsSync, readFileSync, writeFileSync, renameSync, mkdirSync } from "
3
3
  import { dirname } from "node:path";
4
4
  import { getAgentPath } from "./agent-dir.ts";
5
5
  import { createHash } from "node:crypto";
6
- import { getToolUiResourceUri } from "@modelcontextprotocol/ext-apps/app-bridge";
6
+ import { getToolUiResourceUri } from "./ext-apps-bridge.ts";
7
7
  import type { McpTool, McpResource, ServerEntry, ToolMetadata } from "./types.ts";
8
8
  import { formatToolName, isToolExcluded } from "./types.ts";
9
9
  import { resourceNameToToolName } from "./resource-tools.ts";
@@ -1,4 +1,4 @@
1
- import { getToolUiResourceUri } from "@modelcontextprotocol/ext-apps/app-bridge";
1
+ import { getToolUiResourceUri } from "./ext-apps-bridge.ts";
2
2
  import type { McpExtensionState } from "./state.ts";
3
3
  import type { ToolMetadata, McpTool, McpResource, ServerEntry } from "./types.ts";
4
4
  import { formatToolName, isToolExcluded } from "./types.ts";
@@ -1,4 +1,4 @@
1
- import { RESOURCE_MIME_TYPE } from "@modelcontextprotocol/ext-apps/app-bridge";
1
+ import { resourceMimeType } from "./ext-apps-bridge.ts";
2
2
  import { UrlElicitationRequiredError, type ReadResourceResult } from "@modelcontextprotocol/sdk/types.js";
3
3
  import { ResourceFetchError, ResourceParseError } from "./errors.ts";
4
4
  import { logger } from "./logger.ts";
@@ -47,7 +47,7 @@ export class UiResourceHandler {
47
47
  log.warn("Unsupported MIME type", { mimeType });
48
48
  throw new ResourceParseError(
49
49
  uri,
50
- `unsupported MIME type "${mimeType}" (expected text/html or ${RESOURCE_MIME_TYPE})`,
50
+ `unsupported MIME type "${mimeType}" (expected text/html or ${resourceMimeType()})`,
51
51
  { server: serverName, mimeType }
52
52
  );
53
53
  }
@@ -69,7 +69,7 @@ export class UiResourceHandler {
69
69
  return {
70
70
  uri: content.uri ?? uri,
71
71
  html,
72
- mimeType: mimeType ?? RESOURCE_MIME_TYPE,
72
+ mimeType: mimeType ?? resourceMimeType(),
73
73
  meta: {
74
74
  csp: contentMeta.csp ?? listMeta.csp,
75
75
  permissions: contentMeta.permissions ?? listMeta.permissions,
@@ -107,7 +107,7 @@ function selectContent(result: ReadResourceResult, preferredUri: string): Resour
107
107
 
108
108
  function isHtmlMimeType(mimeType: string): boolean {
109
109
  const normalized = mimeType.toLowerCase();
110
- return normalized.startsWith("text/html") || normalized === RESOURCE_MIME_TYPE.toLowerCase();
110
+ return normalized.startsWith("text/html") || normalized === resourceMimeType().toLowerCase();
111
111
  }
112
112
 
113
113
  function toHtml(content: ResourceContentRecord): string {
package/mcp/ui-server.ts CHANGED
@@ -2,7 +2,7 @@ import http, { type IncomingMessage, type ServerResponse } from "node:http";
2
2
  import fs from "node:fs/promises";
3
3
  import path from "node:path";
4
4
  import { randomUUID } from "node:crypto";
5
- import { buildAllowAttribute } from "@modelcontextprotocol/ext-apps/app-bridge";
5
+ import { buildAllowAttribute } from "./ext-apps-bridge.ts";
6
6
  import type {
7
7
  CallToolRequest,
8
8
  CallToolResult,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-soly",
3
- "version": "1.13.3",
3
+ "version": "1.13.5",
4
4
  "description": "Project management + workflow framework for pi-coding-agent. Plans, state, MANDATORY rules, self-review, multi-question picker, native footer + welcome chrome β€” one npm install, zero config. The LLM drives execution and delegates to a worker subagent when available.",
5
5
  "type": "module",
6
6
  "main": "index.ts",