@oh-my-pi/pi-coding-agent 16.1.21 → 16.1.22
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/CHANGELOG.md +7 -0
- package/dist/cli.js +2486 -2485
- package/dist/types/mcp/transports/stdio.test.d.ts +1 -0
- package/package.json +12 -12
- package/src/mcp/oauth-discovery.ts +48 -1
- package/src/mcp/transports/stdio.test.ts +28 -0
- package/src/mcp/transports/stdio.ts +5 -2
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"type": "module",
|
|
3
3
|
"name": "@oh-my-pi/pi-coding-agent",
|
|
4
|
-
"version": "16.1.
|
|
4
|
+
"version": "16.1.22",
|
|
5
5
|
"description": "Coding agent CLI with read, bash, edit, write tools and session management",
|
|
6
6
|
"homepage": "https://omp.sh",
|
|
7
7
|
"author": "Can Boluk",
|
|
@@ -48,17 +48,17 @@
|
|
|
48
48
|
"@agentclientprotocol/sdk": "0.25.0",
|
|
49
49
|
"@babel/parser": "^7.29.7",
|
|
50
50
|
"@mozilla/readability": "^0.6.0",
|
|
51
|
-
"@oh-my-pi/hashline": "16.1.
|
|
52
|
-
"@oh-my-pi/omp-stats": "16.1.
|
|
53
|
-
"@oh-my-pi/pi-agent-core": "16.1.
|
|
54
|
-
"@oh-my-pi/pi-ai": "16.1.
|
|
55
|
-
"@oh-my-pi/pi-catalog": "16.1.
|
|
56
|
-
"@oh-my-pi/pi-mnemopi": "16.1.
|
|
57
|
-
"@oh-my-pi/pi-natives": "16.1.
|
|
58
|
-
"@oh-my-pi/pi-tui": "16.1.
|
|
59
|
-
"@oh-my-pi/pi-utils": "16.1.
|
|
60
|
-
"@oh-my-pi/pi-wire": "16.1.
|
|
61
|
-
"@oh-my-pi/snapcompact": "16.1.
|
|
51
|
+
"@oh-my-pi/hashline": "16.1.22",
|
|
52
|
+
"@oh-my-pi/omp-stats": "16.1.22",
|
|
53
|
+
"@oh-my-pi/pi-agent-core": "16.1.22",
|
|
54
|
+
"@oh-my-pi/pi-ai": "16.1.22",
|
|
55
|
+
"@oh-my-pi/pi-catalog": "16.1.22",
|
|
56
|
+
"@oh-my-pi/pi-mnemopi": "16.1.22",
|
|
57
|
+
"@oh-my-pi/pi-natives": "16.1.22",
|
|
58
|
+
"@oh-my-pi/pi-tui": "16.1.22",
|
|
59
|
+
"@oh-my-pi/pi-utils": "16.1.22",
|
|
60
|
+
"@oh-my-pi/pi-wire": "16.1.22",
|
|
61
|
+
"@oh-my-pi/snapcompact": "16.1.22",
|
|
62
62
|
"@opentelemetry/api": "^1.9.1",
|
|
63
63
|
"@opentelemetry/context-async-hooks": "^2.7.1",
|
|
64
64
|
"@opentelemetry/exporter-trace-otlp-proto": "^0.218.0",
|
|
@@ -250,6 +250,45 @@ export function analyzeAuthError(error: Error, serverUrl?: string): AuthDetectio
|
|
|
250
250
|
};
|
|
251
251
|
}
|
|
252
252
|
|
|
253
|
+
/**
|
|
254
|
+
* Normalize an OAuth issuer URL for RFC 8414 §3.3 comparison: lowercase
|
|
255
|
+
* scheme/host (URL parser already does this), drop fragment/query, strip a
|
|
256
|
+
* trailing slash on the path. The path is otherwise case-sensitive.
|
|
257
|
+
*/
|
|
258
|
+
function normalizeIssuerUrl(value: string): string | undefined {
|
|
259
|
+
try {
|
|
260
|
+
const u = new URL(value);
|
|
261
|
+
const path = u.pathname.replace(/\/+$/, "");
|
|
262
|
+
return `${u.protocol}//${u.host}${path}`;
|
|
263
|
+
} catch {
|
|
264
|
+
return undefined;
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
/**
|
|
269
|
+
* RFC 8414 §3.3: an authorization-server metadata document's `issuer` MUST
|
|
270
|
+
* equal the URL the client used to construct the metadata URL. When a server
|
|
271
|
+
* hosts metadata for several issuers under one origin (Plane serves a root
|
|
272
|
+
* issuer `https://mcp.plane.so/` at `/.well-known/oauth-authorization-server`
|
|
273
|
+
* *and* a path-scoped issuer `https://mcp.plane.so/http` at the path-prefixed
|
|
274
|
+
* well-known URL), accepting the first hit silently routes the grant to the
|
|
275
|
+
* wrong `/authorize` endpoint and produces opaque `server_error` redirects.
|
|
276
|
+
*
|
|
277
|
+
* Returns true when the metadata is safe to use:
|
|
278
|
+
* - the document has no `issuer` field (nonstandard / legacy servers — keep
|
|
279
|
+
* today's permissive behavior), or
|
|
280
|
+
* - the issuer matches `baseUrl` after trailing-slash normalization.
|
|
281
|
+
*/
|
|
282
|
+
function issuerMatchesBase(metadataIssuer: unknown, baseUrl: string): boolean {
|
|
283
|
+
if (typeof metadataIssuer !== "string" || !metadataIssuer.trim()) {
|
|
284
|
+
return true;
|
|
285
|
+
}
|
|
286
|
+
const normalizedIssuer = normalizeIssuerUrl(metadataIssuer);
|
|
287
|
+
const normalizedBase = normalizeIssuerUrl(baseUrl);
|
|
288
|
+
if (!normalizedIssuer || !normalizedBase) return true;
|
|
289
|
+
return normalizedIssuer === normalizedBase;
|
|
290
|
+
}
|
|
291
|
+
|
|
253
292
|
/**
|
|
254
293
|
* Try to discover OAuth endpoints by querying the server's well-known endpoints.
|
|
255
294
|
* This is a fallback when error responses don't include OAuth metadata.
|
|
@@ -389,7 +428,15 @@ export async function discoverOAuthEndpoints(
|
|
|
389
428
|
|
|
390
429
|
if (response.ok) {
|
|
391
430
|
const metadata = (await response.json()) as Record<string, unknown>;
|
|
392
|
-
|
|
431
|
+
// Authorization-server / OpenID Connect metadata documents carry an
|
|
432
|
+
// `issuer` field that MUST equal the queried base URL (RFC 8414 §3.3,
|
|
433
|
+
// OIDC Discovery §4.3). Protected-resource and nonstandard paths use a
|
|
434
|
+
// different schema, so the issuer check only gates the two official
|
|
435
|
+
// auth-server documents.
|
|
436
|
+
const requireIssuerMatch =
|
|
437
|
+
path === "/.well-known/oauth-authorization-server" || path === "/.well-known/openid-configuration";
|
|
438
|
+
const issuerOk = requireIssuerMatch ? issuerMatchesBase(metadata.issuer, baseUrl) : true;
|
|
439
|
+
const endpoints = issuerOk ? findEndpoints(metadata) : null;
|
|
393
440
|
if (endpoints) return endpoints;
|
|
394
441
|
|
|
395
442
|
if (path === "/.well-known/oauth-protected-resource") {
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { describe, expect, it } from "bun:test";
|
|
2
|
+
|
|
3
|
+
import { resolveStdioSpawnCommand } from "./stdio";
|
|
4
|
+
|
|
5
|
+
describe("resolveStdioSpawnCommand", () => {
|
|
6
|
+
it("hides direct Windows executable MCP servers", async () => {
|
|
7
|
+
await expect(
|
|
8
|
+
resolveStdioSpawnCommand(
|
|
9
|
+
{ command: "server.exe", args: ["--stdio"] },
|
|
10
|
+
{ cwd: process.cwd(), env: {}, platform: "win32" },
|
|
11
|
+
),
|
|
12
|
+
).resolves.toEqual({
|
|
13
|
+
cmd: ["server.exe", "--stdio"],
|
|
14
|
+
windowsHide: true,
|
|
15
|
+
});
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
it("keeps off-Windows spawn options unchanged", async () => {
|
|
19
|
+
await expect(
|
|
20
|
+
resolveStdioSpawnCommand(
|
|
21
|
+
{ command: "server.exe", args: ["--stdio"] },
|
|
22
|
+
{ cwd: process.cwd(), env: {}, platform: "linux" },
|
|
23
|
+
),
|
|
24
|
+
).resolves.toEqual({
|
|
25
|
+
cmd: ["server.exe", "--stdio"],
|
|
26
|
+
});
|
|
27
|
+
});
|
|
28
|
+
});
|
|
@@ -239,12 +239,15 @@ export async function resolveStdioSpawnCommand(
|
|
|
239
239
|
// Direct-spawn only when we resolved to a concrete file AND its extension
|
|
240
240
|
// is not a batch script. Everything else (resolved .cmd/.bat, or an
|
|
241
241
|
// unresolved extensionless command) goes through cmd.exe so PATHEXT runs.
|
|
242
|
+
// Every Windows stdio server launch hides its console window; otherwise
|
|
243
|
+
// direct .exe servers pop a visible cmd window while the MCP server lives.
|
|
244
|
+
const windowsHide = true;
|
|
242
245
|
const needsCmdExe = resolved === null || isWindowsBatchCommand(resolvedCommand);
|
|
243
|
-
if (!needsCmdExe) return { cmd: [resolvedCommand, ...args] };
|
|
246
|
+
if (!needsCmdExe) return { cmd: [resolvedCommand, ...args], windowsHide };
|
|
244
247
|
|
|
245
248
|
return {
|
|
246
249
|
cmd: [resolveComSpec(options.env), "/d", "/s", "/c", buildCmdExeCommand(resolvedCommand, args)],
|
|
247
|
-
windowsHide
|
|
250
|
+
windowsHide,
|
|
248
251
|
};
|
|
249
252
|
}
|
|
250
253
|
|