pi-soly 2.4.0 → 2.4.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/index.ts CHANGED
@@ -973,13 +973,13 @@ export default function solyExtension(pi: ExtensionAPI) {
973
973
  try {
974
974
  m.default(pi);
975
975
  } catch (err) {
976
- console.error("[soly] MCP adapter failed to initialize:", err);
976
+ emit("[soly] MCP adapter failed to initialize:" + ": " + (err instanceof Error ? err.message : String(err)), "error");
977
977
  }
978
978
  })
979
979
  .catch((err) => {
980
- console.error("[soly] MCP adapter unavailable (load failed):", err);
980
+ emit("[soly] MCP adapter unavailable (load failed):" + ": " + (err instanceof Error ? err.message : String(err)), "error");
981
981
  });
982
982
  } catch (err) {
983
- console.error("[soly] MCP adapter unavailable (load threw):", err);
983
+ emit("[soly] MCP adapter unavailable (load threw):" + ": " + (err instanceof Error ? err.message : String(err)), "error");
984
984
  }
985
985
  }
package/mcp/config.ts CHANGED
@@ -4,6 +4,7 @@ import { homedir } from "node:os";
4
4
  import { dirname, join, resolve } from "node:path";
5
5
  import { getAgentPath } from "./agent-dir.ts";
6
6
  import type { McpConfig, ServerEntry, McpSettings, ImportKind, ServerProvenance } from "./types.ts";
7
+ import { emit } from "../visual/event-sink.ts";
7
8
 
8
9
  const GENERIC_GLOBAL_CONFIG_PATH = join(homedir(), ".config", "mcp", "mcp.json");
9
10
  const PROJECT_CONFIG_NAME = ".mcp.json";
@@ -279,7 +280,7 @@ function expandImports(config: McpConfig, cwd = process.cwd()): McpConfig {
279
280
  }
280
281
  }
281
282
  } catch (error) {
282
- console.warn(`Failed to import MCP config from ${importKind}:`, error);
283
+ emit(`Failed to import MCP config from ${importKind}: ${error instanceof Error ? error.message : String(error)}`, "warning");
283
284
  }
284
285
  }
285
286
 
@@ -316,7 +317,7 @@ function readValidatedConfig(path: string, label: string): McpConfig | null {
316
317
  try {
317
318
  return validateConfig(JSON.parse(readFileSync(path, "utf-8")));
318
319
  } catch (error) {
319
- console.warn(`Failed to load ${label}:`, error);
320
+ emit(`Failed to load ${label}: ${error instanceof Error ? error.message : String(error)}`, "warning");
320
321
  return null;
321
322
  }
322
323
  }
@@ -13,6 +13,7 @@ import { resourceNameToToolName } from "./resource-tools.ts";
13
13
  import { authenticate, supportsOAuth } from "./mcp-auth-flow.ts";
14
14
  import { formatAuthRequiredMessage } from "./utils.ts";
15
15
  import { ToolCache, cacheKey } from "./tool-cache.ts";
16
+ import { emit } from "../visual/event-sink.ts";
16
17
 
17
18
  const BUILTIN_NAMES = new Set(["read", "bash", "edit", "write", "grep", "find", "ls", "mcp"]);
18
19
 
@@ -133,11 +134,11 @@ export function resolveDirectTools(
133
134
  if (isToolExcluded(tool.name, serverName, prefix, definition.excludeTools)) continue;
134
135
  const prefixedName = formatToolName(tool.name, serverName, prefix);
135
136
  if (BUILTIN_NAMES.has(prefixedName)) {
136
- console.warn(`MCP: skipping direct tool "${prefixedName}" (collides with builtin)`);
137
+ emit(`MCP: skipping direct tool "${prefixedName}" (collides with builtin)`, "warning");
137
138
  continue;
138
139
  }
139
140
  if (seenNames.has(prefixedName)) {
140
- console.warn(`MCP: skipping duplicate direct tool "${prefixedName}" from "${serverName}"`);
141
+ emit(`MCP: skipping duplicate direct tool "${prefixedName}" from "${serverName}"`, "warning");
141
142
  continue;
142
143
  }
143
144
  seenNames.add(prefixedName);
@@ -159,11 +160,11 @@ export function resolveDirectTools(
159
160
  if (isToolExcluded(baseName, serverName, prefix, definition.excludeTools)) continue;
160
161
  const prefixedName = formatToolName(baseName, serverName, prefix);
161
162
  if (BUILTIN_NAMES.has(prefixedName)) {
162
- console.warn(`MCP: skipping direct resource tool "${prefixedName}" (collides with builtin)`);
163
+ emit(`MCP: skipping direct resource tool "${prefixedName}" (collides with builtin)`, "warning");
163
164
  continue;
164
165
  }
165
166
  if (seenNames.has(prefixedName)) {
166
- console.warn(`MCP: skipping duplicate direct resource tool "${prefixedName}" from "${serverName}"`);
167
+ emit(`MCP: skipping duplicate direct resource tool "${prefixedName}" from "${serverName}"`, "warning");
167
168
  continue;
168
169
  }
169
170
  seenNames.add(prefixedName);
@@ -17,6 +17,8 @@
17
17
  // resolve ext-apps' types (which themselves reference the missing sdk subpath).
18
18
  // =============================================================================
19
19
 
20
+ import { emit } from "../visual/event-sink.ts";
21
+
20
22
  /** Minimal shape of the bits of ext-apps/app-bridge we use. */
21
23
  interface AppBridge {
22
24
  RESOURCE_MIME_TYPE: string;
@@ -43,9 +45,10 @@ export async function preloadAppBridge(): Promise<void> {
43
45
  cached = (await import(APP_BRIDGE_SPECIFIER)) as unknown as AppBridge;
44
46
  } catch {
45
47
  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.",
48
+ emit(
49
+ "MCP UI (ext-apps/app-bridge) could not load — app-bridge features disabled " +
50
+ "(upstream ext-apps/sdk version mismatch, not a soly bug). Core MCP unaffected.",
51
+ "warning",
49
52
  );
50
53
  }
51
54
  }
@@ -1,4 +1,5 @@
1
1
  import type { UiHostContext, UiResourceContent, UiResourceCsp } from "./types.ts";
2
+ import { emit } from "../visual/event-sink.ts";
2
3
 
3
4
  // Use locally bundled AppBridge to avoid CDN Zod bundling issues
4
5
  const DEFAULT_APP_BRIDGE_MODULE_URL = "/app-bridge.bundle.js";
@@ -268,7 +269,7 @@ export function buildHostHtmlTemplate(input: HostHtmlTemplateInput): string {
268
269
  const transport = new PostMessageTransport(iframe.contentWindow, null);
269
270
  await bridge.connect(transport);
270
271
  } catch (error) {
271
- console.error("[host] Bridge connection failed:", error);
272
+ emit("[host] Bridge connection failed:" + ": " + (error instanceof Error ? error.message : String(error)), "error");
272
273
  showError("Failed to initialize AppBridge: " + String(error));
273
274
  }
274
275
 
package/mcp/index.ts CHANGED
@@ -58,7 +58,7 @@ export default function mcpAdapter(pi: ExtensionAPI) {
58
58
  await currentState.lifecycle.gracefulShutdown();
59
59
  } catch (error) {
60
60
  if (flushError) {
61
- console.error("MCP: graceful shutdown failed after metadata flush error", error);
61
+ emit("MCP: graceful shutdown failed after metadata flush error" + ": " + (error instanceof Error ? error.message : String(error)), "error");
62
62
  } else {
63
63
  throw error;
64
64
  }
@@ -122,7 +122,7 @@ export default function mcpAdapter(pi: ExtensionAPI) {
122
122
  shutdownOAuth(),
123
123
  ]);
124
124
  } catch (error) {
125
- console.error("MCP: failed to shut down previous session state", error);
125
+ emit("MCP: failed to shut down previous session state" + ": " + (error instanceof Error ? error.message : String(error)), "error");
126
126
  }
127
127
 
128
128
  if (generation !== lifecycleGeneration) {
@@ -130,7 +130,7 @@ export default function mcpAdapter(pi: ExtensionAPI) {
130
130
  }
131
131
 
132
132
  await initializeOAuth().catch(err => {
133
- console.error("MCP OAuth initialization failed:", err);
133
+ emit("MCP OAuth initialization failed:" + ": " + (err instanceof Error ? err.message : String(err)), "error");
134
134
  });
135
135
 
136
136
  // Load the (optional, sometimes-broken upstream) ext-apps UI bridge once,
@@ -145,7 +145,7 @@ export default function mcpAdapter(pi: ExtensionAPI) {
145
145
  try {
146
146
  await shutdownState(nextState, "stale_session_start");
147
147
  } catch (error) {
148
- console.error("MCP: failed to clean stale session state", error);
148
+ emit("MCP: failed to clean stale session state" + ": " + (error instanceof Error ? error.message : String(error)), "error");
149
149
  }
150
150
  return;
151
151
  }
@@ -161,7 +161,7 @@ export default function mcpAdapter(pi: ExtensionAPI) {
161
161
  if (initPromise !== promise && initPromise !== null) {
162
162
  return;
163
163
  }
164
- console.error("MCP initialization failed:", err);
164
+ emit("MCP initialization failed:" + ": " + (err instanceof Error ? err.message : String(err)), "error");
165
165
  initPromise = null;
166
166
  });
167
167
  });
@@ -179,7 +179,7 @@ export default function mcpAdapter(pi: ExtensionAPI) {
179
179
  shutdownOAuth(),
180
180
  ]);
181
181
  } catch (error) {
182
- console.error("MCP: session shutdown cleanup failed", error);
182
+ emit("MCP: session shutdown cleanup failed" + ": " + (error instanceof Error ? error.message : String(error)), "error");
183
183
  }
184
184
  });
185
185
 
package/mcp/init.ts CHANGED
@@ -161,7 +161,7 @@ export async function initializeMcp(
161
161
  if (ctx.hasUI) {
162
162
  emit(`MCP: Failed to connect to ${name}: ${error}`, "error");
163
163
  }
164
- console.error(`MCP: Failed to connect to ${name}: ${error}`);
164
+ emit(`MCP: Failed to connect to ${name}: ${error}`, "error");
165
165
  continue;
166
166
  }
167
167
 
package/mcp/logger.ts CHANGED
@@ -3,6 +3,8 @@
3
3
  * Provides structured, contextual logs with levels.
4
4
  */
5
5
 
6
+ import { emit as emitEvent } from "../visual/event-sink.ts";
7
+
6
8
  export type LogLevel = "debug" | "info" | "warn" | "error";
7
9
 
8
10
  export interface LogContext {
@@ -73,19 +75,22 @@ class Logger {
73
75
  timestamp: new Date(),
74
76
  };
75
77
 
76
- // Default console output
77
- const prefix = LEVEL_PREFIX[level];
78
+ // Route through the event sink → Working sub-line (no console output).
79
+ // Context is appended for usefulness; the console-style [MCP-UI:*]
80
+ // prefix is dropped (the sub-line glyph carries the level).
78
81
  const contextStr = formatContext(entry.context);
79
- const fullMessage = contextStr ? `${prefix} ${message} ${contextStr}` : `${prefix} ${message}`;
82
+ const errStr = error ? `: ${error.message}` : "";
83
+ const text = contextStr ? `${message}${errStr} ${contextStr}` : `${message}${errStr}`;
80
84
 
81
85
  if (level === "error") {
82
- console.error(fullMessage, error ?? "");
86
+ emitEvent(text, "error");
83
87
  } else if (level === "warn") {
84
- console.warn(fullMessage);
88
+ emitEvent(text, "warning");
85
89
  } else if (level === "debug") {
86
- console.debug(fullMessage);
90
+ // Debug is too verbose for a single-line toolbar slot; drop unless
91
+ // a custom handler (e.g. file logger) is registered.
87
92
  } else {
88
- console.log(fullMessage);
93
+ emitEvent(text, "info");
89
94
  }
90
95
 
91
96
  // Custom handlers
@@ -32,6 +32,7 @@ import {
32
32
  type StoredTokens,
33
33
  } from "./mcp-auth.ts"
34
34
  import type { ServerEntry } from "./types.ts"
35
+ import { emit } from "../visual/event-sink.ts";
35
36
 
36
37
  /** Auth status for a server */
37
38
  export type AuthStatus = "authenticated" | "expired" | "not_authenticated"
@@ -399,11 +400,11 @@ export async function authenticate(
399
400
  try {
400
401
  // Open browser. Always print the URL first so remote/headless users can copy it
401
402
  // even when the OS browser handoff is unavailable or invisible.
402
- console.log(`MCP Auth: Open this URL to authenticate ${serverName}:\n${authorizationUrl}`)
403
+ emit(`MCP Auth: Open this URL to authenticate ${serverName}:\n${authorizationUrl}`, "info");
403
404
  try {
404
405
  await open(authorizationUrl)
405
406
  } catch (error) {
406
- console.warn(`MCP Auth: Failed to open browser for ${serverName}; waiting for manual callback`, { error })
407
+ emit(`MCP Auth: Failed to open browser for ${serverName}; waiting for manual callback`, "warning");
407
408
  }
408
409
 
409
410
  // Wait for callback
@@ -462,7 +463,7 @@ export async function getValidToken(
462
463
 
463
464
  if (expired === true && entry.tokens.refreshToken) {
464
465
  // Token is expired, try to refresh
465
- console.log(`MCP Auth: Token expired for ${serverName}, attempting refresh`)
466
+ emit(`MCP Auth: Token expired for ${serverName}, attempting refresh`, "info");
466
467
 
467
468
  try {
468
469
  // Create auth provider for token refresh
@@ -472,7 +473,7 @@ export async function getValidToken(
472
473
 
473
474
  const clientInfo = await authProvider.clientInformation()
474
475
  if (!clientInfo) {
475
- console.log(`MCP Auth: No client info for refresh for ${serverName}`)
476
+ emit(`MCP Auth: No client info for refresh for ${serverName}`, "info");
476
477
  return null
477
478
  }
478
479
 
@@ -483,7 +484,7 @@ export async function getValidToken(
483
484
  const refreshed = await getAuthForUrl(serverName, serverUrl)
484
485
  return refreshed?.tokens ?? null
485
486
  } catch (error) {
486
- console.error(`MCP Auth: Token refresh failed for ${serverName}`, { error })
487
+ emit(`MCP Auth: Token refresh failed for ${serverName}`, "error");
487
488
  return null
488
489
  }
489
490
  }
@@ -519,7 +520,7 @@ export async function removeAuth(serverName: string): Promise<void> {
519
520
  await clearPendingAuth(serverName, oauthState)
520
521
  clearAllCredentials(serverName)
521
522
  await clearOAuthState(serverName)
522
- console.log(`MCP Auth: Removed credentials for ${serverName}`)
523
+ emit(`MCP Auth: Removed credentials for ${serverName}`, "info");
523
524
  }
524
525
 
525
526
  /**
package/mcp/mcp-auth.ts CHANGED
@@ -12,6 +12,7 @@ import { createHash } from 'crypto';
12
12
  import { mkdirSync, readFileSync, writeFileSync, existsSync, rmSync } from 'fs';
13
13
  import { join } from 'path';
14
14
  import { getAgentPath } from './agent-dir.ts';
15
+ import { emit } from "../visual/event-sink.ts";
15
16
 
16
17
  /** OAuth token storage format */
17
18
  export interface StoredTokens {
@@ -86,7 +87,7 @@ function readAuthEntry(serverName: string): AuthEntry | undefined {
86
87
  const data = readFileSync(filePath, 'utf-8');
87
88
  return JSON.parse(data) as AuthEntry;
88
89
  } catch (error) {
89
- console.error(`Failed to read auth entry for ${serverName}:`, error);
90
+ emit(`Failed to read auth entry for ${serverName}:` + ": " + (error instanceof Error ? error.message : String(error)), "error");
90
91
  return undefined;
91
92
  }
92
93
  }
@@ -155,7 +156,7 @@ export function removeAuthEntry(serverName: string): void {
155
156
  }
156
157
  }
157
158
  } catch (error) {
158
- console.error(`Failed to remove auth entry for ${serverName}:`, error);
159
+ emit(`Failed to remove auth entry for ${serverName}:` + ": " + (error instanceof Error ? error.message : String(error)), "error");
159
160
  }
160
161
  }
161
162
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-soly",
3
- "version": "2.4.0",
3
+ "version": "2.4.1",
4
4
  "description": "Workflow + project management for pi-coding-agent. Plans, state, MANDATORY rules, self-review, multi-question picker. One npm install, zero config. LLM drives the workflow inline via the soly_workflow tool — no external subagent plugin.",
5
5
  "type": "module",
6
6
  "main": "index.ts",
package/quota/zai.ts ADDED
@@ -0,0 +1,45 @@
1
+ // =============================================================================
2
+ // quota/zai.ts — Zhipu AI (zai / GLM) quota provider
3
+ // =============================================================================
4
+ //
5
+ // Adapter for the "zai" provider id. Implement fetch() to return a
6
+ // QuotaSnapshot — the poller + footer do the rest.
7
+ //
8
+ // TODO(zai): fill in the real fetch. Options:
9
+ // (a) shell out to a CLI (like minimax does with `mmx`) if one exists
10
+ // (b) read the API key from env (ZAI_API_KEY / ZHIPUAI_API_KEY) and
11
+ // call the billing/quota endpoint directly via fetch()
12
+ // The shape below is the contract — replace the body of fetch().
13
+
14
+ import type { QuotaProvider, QuotaSnapshot } from "./types.ts";
15
+
16
+ /** Zhipu AI quota provider.
17
+ *
18
+ * Register in index.ts:
19
+ * registerQuotaProvider(zaiProvider);
20
+ * Then any model with provider="zai" (or "zai-cn") shows the segment. */
21
+ export const zaiProvider: QuotaProvider = {
22
+ id: "zai",
23
+ async fetch(): Promise<QuotaSnapshot | null> {
24
+ // TODO(zai): real implementation. Example shape if using HTTP:
25
+ //
26
+ // const key = process.env.ZAI_API_KEY ?? process.env.ZHIPUAI_API_KEY;
27
+ // if (!key) return null;
28
+ // try {
29
+ // const res = await fetch("https://open.bigmodel.cn/api/paas/v4/billing", {
30
+ // headers: { Authorization: `Bearer ${key}` },
31
+ // });
32
+ // if (!res.ok) return null;
33
+ // const data = await res.json() as { remaining?: number; resetsAt?: string };
34
+ // return {
35
+ // remainingPercent: data.remaining,
36
+ // resetsInMs: data.resetsAt ? Date.parse(data.resetsAt) - Date.now() : null,
37
+ // };
38
+ // } catch {
39
+ // return null; // never throw — poller treats null as "no data"
40
+ // }
41
+ //
42
+ // Until implemented, return null (segment stays hidden for zai models).
43
+ return null;
44
+ },
45
+ };