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 +3 -3
- package/mcp/config.ts +3 -2
- package/mcp/direct-tools.ts +5 -4
- package/mcp/ext-apps-bridge.ts +6 -3
- package/mcp/host-html-template.ts +2 -1
- package/mcp/index.ts +6 -6
- package/mcp/init.ts +1 -1
- package/mcp/logger.ts +12 -7
- package/mcp/mcp-auth-flow.ts +7 -6
- package/mcp/mcp-auth.ts +3 -2
- package/package.json +1 -1
- package/quota/zai.ts +45 -0
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
|
-
|
|
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
|
-
|
|
980
|
+
emit("[soly] MCP adapter unavailable (load failed):" + ": " + (err instanceof Error ? err.message : String(err)), "error");
|
|
981
981
|
});
|
|
982
982
|
} catch (err) {
|
|
983
|
-
|
|
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
|
-
|
|
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
|
-
|
|
320
|
+
emit(`Failed to load ${label}: ${error instanceof Error ? error.message : String(error)}`, "warning");
|
|
320
321
|
return null;
|
|
321
322
|
}
|
|
322
323
|
}
|
package/mcp/direct-tools.ts
CHANGED
|
@@ -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
|
-
|
|
137
|
+
emit(`MCP: skipping direct tool "${prefixedName}" (collides with builtin)`, "warning");
|
|
137
138
|
continue;
|
|
138
139
|
}
|
|
139
140
|
if (seenNames.has(prefixedName)) {
|
|
140
|
-
|
|
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
|
-
|
|
163
|
+
emit(`MCP: skipping direct resource tool "${prefixedName}" (collides with builtin)`, "warning");
|
|
163
164
|
continue;
|
|
164
165
|
}
|
|
165
166
|
if (seenNames.has(prefixedName)) {
|
|
166
|
-
|
|
167
|
+
emit(`MCP: skipping duplicate direct resource tool "${prefixedName}" from "${serverName}"`, "warning");
|
|
167
168
|
continue;
|
|
168
169
|
}
|
|
169
170
|
seenNames.add(prefixedName);
|
package/mcp/ext-apps-bridge.ts
CHANGED
|
@@ -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
|
-
|
|
47
|
-
"
|
|
48
|
-
"(upstream ext-apps/sdk version mismatch, not a soly bug). Core MCP
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
77
|
-
|
|
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
|
|
82
|
+
const errStr = error ? `: ${error.message}` : "";
|
|
83
|
+
const text = contextStr ? `${message}${errStr} ${contextStr}` : `${message}${errStr}`;
|
|
80
84
|
|
|
81
85
|
if (level === "error") {
|
|
82
|
-
|
|
86
|
+
emitEvent(text, "error");
|
|
83
87
|
} else if (level === "warn") {
|
|
84
|
-
|
|
88
|
+
emitEvent(text, "warning");
|
|
85
89
|
} else if (level === "debug") {
|
|
86
|
-
|
|
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
|
-
|
|
93
|
+
emitEvent(text, "info");
|
|
89
94
|
}
|
|
90
95
|
|
|
91
96
|
// Custom handlers
|
package/mcp/mcp-auth-flow.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
+
};
|