@oh-my-pi/pi-coding-agent 16.2.1 → 16.2.3
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 +72 -0
- package/dist/cli.js +3621 -3579
- package/dist/types/advisor/__tests__/config.test.d.ts +1 -0
- package/dist/types/advisor/advise-tool.d.ts +8 -4
- package/dist/types/advisor/config.d.ts +88 -0
- package/dist/types/advisor/index.d.ts +1 -0
- package/dist/types/advisor/runtime.d.ts +15 -1
- package/dist/types/advisor/transcript-recorder.d.ts +13 -2
- package/dist/types/advisor/watchdog.d.ts +20 -0
- package/dist/types/collab/guest.d.ts +29 -0
- package/dist/types/config/model-roles.d.ts +1 -1
- package/dist/types/config/settings-schema.d.ts +113 -12
- package/dist/types/debug/log-viewer.d.ts +1 -0
- package/dist/types/debug/raw-sse.d.ts +1 -0
- package/dist/types/discovery/omp-extension-roots.d.ts +3 -3
- package/dist/types/edit/hashline/diff.d.ts +0 -11
- package/dist/types/edit/index.d.ts +18 -0
- package/dist/types/edit/streaming.d.ts +30 -0
- package/dist/types/extensibility/custom-tools/types.d.ts +1 -0
- package/dist/types/extensibility/shared-events.d.ts +1 -0
- package/dist/types/extensibility/tool-event-input.d.ts +7 -0
- package/dist/types/extensibility/utils.d.ts +12 -0
- package/dist/types/mcp/oauth-discovery.d.ts +0 -11
- package/dist/types/mcp/transports/index.d.ts +1 -0
- package/dist/types/mcp/transports/sse.d.ts +20 -0
- package/dist/types/modes/components/advisor-config.d.ts +59 -0
- package/dist/types/modes/components/index.d.ts +1 -0
- package/dist/types/modes/components/model-selector.d.ts +9 -1
- package/dist/types/modes/components/settings-selector.d.ts +1 -0
- package/dist/types/modes/components/status-line/component.d.ts +30 -3
- package/dist/types/modes/components/status-line/types.d.ts +13 -1
- package/dist/types/modes/controllers/selector-controller.d.ts +1 -0
- package/dist/types/modes/interactive-mode.d.ts +10 -4
- package/dist/types/modes/skill-command.d.ts +32 -0
- package/dist/types/modes/types.d.ts +7 -2
- package/dist/types/sdk.d.ts +1 -1
- package/dist/types/session/agent-session.d.ts +84 -12
- package/dist/types/session/indexed-session-storage.d.ts +7 -1
- package/dist/types/session/messages.d.ts +32 -7
- package/dist/types/session/messages.test.d.ts +1 -0
- package/dist/types/session/session-entries.d.ts +31 -3
- package/dist/types/session/session-history-format.d.ts +6 -0
- package/dist/types/session/session-loader.d.ts +9 -1
- package/dist/types/session/session-manager.d.ts +6 -4
- package/dist/types/session/session-storage.d.ts +11 -0
- package/dist/types/session/session-title-slot.d.ts +19 -0
- package/dist/types/session/settings-stream-fn.d.ts +21 -0
- package/dist/types/session/turn-persistence.d.ts +88 -0
- package/dist/types/ssh/connection-manager.d.ts +47 -0
- package/dist/types/ssh/utils.d.ts +16 -0
- package/dist/types/task/executor.d.ts +3 -16
- package/dist/types/task/render.d.ts +0 -5
- package/dist/types/task/renderer.d.ts +13 -0
- package/dist/types/task/types.d.ts +16 -0
- package/dist/types/task/yield-assembly.d.ts +28 -0
- package/dist/types/tiny/text.d.ts +8 -0
- package/dist/types/tools/render-utils.d.ts +2 -0
- package/dist/types/tools/review.d.ts +6 -4
- package/dist/types/tools/ssh.d.ts +1 -1
- package/dist/types/tools/todo.d.ts +6 -0
- package/dist/types/tools/yield.d.ts +8 -3
- package/dist/types/utils/thinking-display.d.ts +4 -0
- package/package.json +12 -12
- package/src/advisor/__tests__/advisor.test.ts +438 -10
- package/src/advisor/__tests__/config.test.ts +173 -0
- package/src/advisor/advise-tool.ts +11 -6
- package/src/advisor/config.ts +256 -0
- package/src/advisor/index.ts +1 -0
- package/src/advisor/runtime.ts +77 -4
- package/src/advisor/transcript-recorder.ts +25 -2
- package/src/advisor/watchdog.ts +57 -31
- package/src/auto-thinking/classifier.ts +2 -2
- package/src/autoresearch/index.ts +7 -2
- package/src/cli/gc-cli.ts +17 -10
- package/src/collab/guest.ts +43 -7
- package/src/config/model-registry.ts +80 -18
- package/src/config/model-resolver.ts +5 -1
- package/src/config/model-roles.ts +3 -3
- package/src/config/settings-schema.ts +107 -8
- package/src/debug/index.ts +32 -7
- package/src/debug/log-viewer.ts +111 -53
- package/src/debug/raw-sse.ts +68 -48
- package/src/discovery/codex.ts +13 -5
- package/src/discovery/omp-extension-roots.ts +38 -13
- package/src/edit/hashline/diff.ts +57 -4
- package/src/edit/index.ts +21 -0
- package/src/edit/streaming.ts +170 -0
- package/src/eval/js/shared/local-module-loader.ts +23 -1
- package/src/export/html/template.js +13 -7
- package/src/extensibility/custom-tools/types.ts +1 -0
- package/src/extensibility/extensions/loader.ts +5 -3
- package/src/extensibility/extensions/wrapper.ts +9 -3
- package/src/extensibility/hooks/loader.ts +3 -3
- package/src/extensibility/hooks/tool-wrapper.ts +13 -4
- package/src/extensibility/plugins/legacy-pi-bundled-keys.ts +18 -1
- package/src/extensibility/plugins/legacy-pi-bundled-registry.ts +59 -2
- package/src/extensibility/plugins/manager.ts +76 -5
- package/src/extensibility/shared-events.ts +1 -0
- package/src/extensibility/tool-event-input.ts +23 -0
- package/src/extensibility/utils.ts +74 -0
- package/src/internal-urls/docs-index.generated.txt +1 -1
- package/src/mcp/client.ts +3 -1
- package/src/mcp/manager.ts +12 -5
- package/src/mcp/oauth-discovery.ts +5 -29
- package/src/mcp/transports/http.ts +3 -1
- package/src/mcp/transports/index.ts +1 -0
- package/src/mcp/transports/sse.ts +377 -0
- package/src/memories/index.ts +15 -6
- package/src/mnemopi/backend.ts +2 -2
- package/src/modes/acp/acp-agent.ts +1 -1
- package/src/modes/components/advisor-config.ts +555 -0
- package/src/modes/components/advisor-message.ts +9 -2
- package/src/modes/components/agent-hub.ts +9 -4
- package/src/modes/components/assistant-message.ts +5 -5
- package/src/modes/components/index.ts +2 -0
- package/src/modes/components/model-selector.ts +79 -48
- package/src/modes/components/settings-selector.ts +1 -0
- package/src/modes/components/status-line/component.ts +145 -14
- package/src/modes/components/status-line/segments.ts +47 -22
- package/src/modes/components/status-line/types.ts +13 -1
- package/src/modes/components/tool-execution.ts +47 -6
- package/src/modes/controllers/command-controller.ts +23 -2
- package/src/modes/controllers/event-controller.ts +114 -11
- package/src/modes/controllers/extension-ui-controller.ts +1 -1
- package/src/modes/controllers/input-controller.ts +61 -61
- package/src/modes/controllers/selector-controller.ts +100 -9
- package/src/modes/interactive-mode.ts +65 -10
- package/src/modes/print-mode.ts +1 -1
- package/src/modes/skill-command.ts +116 -0
- package/src/modes/types.ts +7 -2
- package/src/modes/utils/transcript-render-helpers.ts +2 -2
- package/src/modes/utils/ui-helpers.ts +46 -27
- package/src/prompts/agents/reviewer.md +11 -10
- package/src/prompts/review-custom-request.md +1 -2
- package/src/prompts/review-request.md +1 -2
- package/src/prompts/system/interrupted-thinking.md +7 -0
- package/src/prompts/system/recap-user.md +9 -0
- package/src/prompts/system/subagent-system-prompt.md +8 -5
- package/src/prompts/system/subagent-yield-reminder.md +6 -5
- package/src/prompts/system/system-prompt.md +0 -1
- package/src/prompts/tools/irc.md +2 -2
- package/src/prompts/tools/read.md +2 -2
- package/src/sdk.ts +40 -50
- package/src/session/agent-session.ts +1139 -600
- package/src/session/indexed-session-storage.ts +86 -13
- package/src/session/messages.test.ts +125 -0
- package/src/session/messages.ts +192 -21
- package/src/session/redis-session-storage.ts +49 -2
- package/src/session/session-entries.ts +39 -2
- package/src/session/session-history-format.ts +29 -2
- package/src/session/session-listing.ts +54 -24
- package/src/session/session-loader.ts +66 -3
- package/src/session/session-manager.ts +113 -19
- package/src/session/session-persistence.ts +96 -3
- package/src/session/session-storage.ts +36 -0
- package/src/session/session-title-slot.ts +141 -0
- package/src/session/settings-stream-fn.ts +49 -0
- package/src/session/sql-session-storage.ts +71 -11
- package/src/session/turn-persistence.ts +142 -0
- package/src/session/unexpected-stop-classifier.ts +2 -2
- package/src/slash-commands/builtin-registry.ts +16 -3
- package/src/slash-commands/helpers/mcp.ts +2 -1
- package/src/ssh/__tests__/connection-manager-args.test.ts +123 -1
- package/src/ssh/__tests__/file-transfer-posix-guard.test.ts +55 -18
- package/src/ssh/connection-manager.ts +139 -12
- package/src/ssh/file-transfer.ts +23 -18
- package/src/ssh/ssh-executor.ts +2 -13
- package/src/ssh/utils.ts +19 -0
- package/src/task/executor.ts +21 -23
- package/src/task/render.ts +162 -20
- package/src/task/renderer.ts +14 -0
- package/src/task/types.ts +17 -0
- package/src/task/yield-assembly.ts +207 -0
- package/src/tiny/models.ts +8 -6
- package/src/tiny/text.ts +37 -7
- package/src/tools/ask.ts +55 -4
- package/src/tools/image-gen.ts +2 -1
- package/src/tools/render-utils.ts +2 -0
- package/src/tools/renderers.ts +8 -2
- package/src/tools/review.ts +17 -7
- package/src/tools/ssh.ts +8 -4
- package/src/tools/todo.ts +17 -1
- package/src/tools/tts.ts +2 -1
- package/src/tools/yield.ts +140 -31
- package/src/utils/thinking-display.ts +15 -0
- package/src/utils/title-generator.ts +1 -1
- package/src/web/search/providers/tavily.ts +36 -19
package/src/mcp/client.ts
CHANGED
|
@@ -8,6 +8,7 @@ import * as url from "node:url";
|
|
|
8
8
|
import { getProjectDir, logger, withTimeout } from "@oh-my-pi/pi-utils";
|
|
9
9
|
import { describeMCPTimeout, isMCPTimeoutEnabled, resolveMCPTimeoutMs } from "./timeout";
|
|
10
10
|
import { createHttpTransport } from "./transports/http";
|
|
11
|
+
import { createSseTransport } from "./transports/sse";
|
|
11
12
|
import { createStdioTransport } from "./transports/stdio";
|
|
12
13
|
import type {
|
|
13
14
|
MCPGetPromptParams,
|
|
@@ -77,8 +78,9 @@ async function createTransport(config: MCPServerConfig): Promise<MCPTransport> {
|
|
|
77
78
|
case "stdio":
|
|
78
79
|
return createStdioTransport(config as MCPStdioServerConfig);
|
|
79
80
|
case "http":
|
|
81
|
+
return createHttpTransport(config as MCPHttpServerConfig);
|
|
80
82
|
case "sse":
|
|
81
|
-
return
|
|
83
|
+
return createSseTransport(config as MCPSseServerConfig);
|
|
82
84
|
default:
|
|
83
85
|
throw new Error(`Unknown server type: ${serverType}`);
|
|
84
86
|
}
|
package/src/mcp/manager.ts
CHANGED
|
@@ -37,7 +37,6 @@ import type { McpConnectionStatusEvent } from "./startup-events";
|
|
|
37
37
|
import type { MCPToolDetails } from "./tool-bridge";
|
|
38
38
|
import { DeferredMCPTool, MCPTool } from "./tool-bridge";
|
|
39
39
|
import type { MCPToolCache } from "./tool-cache";
|
|
40
|
-
import { HttpTransport } from "./transports/http";
|
|
41
40
|
import type {
|
|
42
41
|
MCPGetPromptResult,
|
|
43
42
|
MCPPrompt,
|
|
@@ -48,6 +47,7 @@ import type {
|
|
|
48
47
|
MCPServerConfig,
|
|
49
48
|
MCPServerConnection,
|
|
50
49
|
MCPToolDefinition,
|
|
50
|
+
MCPTransport,
|
|
51
51
|
} from "./types";
|
|
52
52
|
import { MCPNotificationMethods } from "./types";
|
|
53
53
|
|
|
@@ -56,6 +56,13 @@ type ToolLoadResult = {
|
|
|
56
56
|
serverTools: MCPToolDefinition[];
|
|
57
57
|
};
|
|
58
58
|
|
|
59
|
+
interface AuthRefreshableMCPTransport extends MCPTransport {
|
|
60
|
+
onAuthError?: () => Promise<Record<string, string> | null>;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function isAuthRefreshableMCPTransport(transport: MCPTransport): transport is AuthRefreshableMCPTransport {
|
|
64
|
+
return "onAuthError" in transport;
|
|
65
|
+
}
|
|
59
66
|
type TrackedPromise<T> = {
|
|
60
67
|
promise: Promise<T>;
|
|
61
68
|
status: "pending" | "fulfilled" | "rejected";
|
|
@@ -419,12 +426,12 @@ export class MCPManager {
|
|
|
419
426
|
this.#connections.set(name, connection);
|
|
420
427
|
}
|
|
421
428
|
|
|
422
|
-
// Wire auth refresh for HTTP transports so 401s trigger token refresh.
|
|
429
|
+
// Wire auth refresh for HTTP-like transports so 401s trigger token refresh.
|
|
423
430
|
// Gate on a resolvable managed credential, not on the auth block:
|
|
424
431
|
// definition-only configs (url-keyed fallback) get Bearer injection
|
|
425
432
|
// too and need the same mid-session refresh hook.
|
|
426
433
|
if (
|
|
427
|
-
connection.transport
|
|
434
|
+
isAuthRefreshableMCPTransport(connection.transport) &&
|
|
428
435
|
lookupMcpOAuthCredential(this.#authStorage, config)
|
|
429
436
|
) {
|
|
430
437
|
connection.transport.onAuthError = async () => {
|
|
@@ -957,9 +964,9 @@ export class MCPManager {
|
|
|
957
964
|
|
|
958
965
|
this.#connections.set(name, connection);
|
|
959
966
|
|
|
960
|
-
// Wire auth refresh for HTTP transports, and reconnect for any transport.
|
|
967
|
+
// Wire auth refresh for HTTP-like transports, and reconnect for any transport.
|
|
961
968
|
// Same gate as connectServers: any resolvable managed credential.
|
|
962
|
-
if (connection.transport
|
|
969
|
+
if (isAuthRefreshableMCPTransport(connection.transport) && lookupMcpOAuthCredential(this.#authStorage, config)) {
|
|
963
970
|
connection.transport.onAuthError = async () => {
|
|
964
971
|
const refreshed = await this.#resolveAuthConfig(config, { forceRefresh: true });
|
|
965
972
|
if (refreshed.type === "http" || refreshed.type === "sse") {
|
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
* Automatically detects OAuth requirements from MCP server responses
|
|
5
5
|
* and extracts authentication endpoints.
|
|
6
6
|
*/
|
|
7
|
+
import * as AIError from "@oh-my-pi/pi-ai/error";
|
|
7
8
|
import type { FetchImpl } from "@oh-my-pi/pi-ai/types";
|
|
8
9
|
|
|
9
10
|
export interface OAuthEndpoints {
|
|
@@ -23,8 +24,8 @@ export interface AuthDetectionResult {
|
|
|
23
24
|
message?: string;
|
|
24
25
|
}
|
|
25
26
|
|
|
26
|
-
function
|
|
27
|
-
const match =
|
|
27
|
+
export function extractMcpAuthServerUrl(error: Error, serverUrl?: string): string | undefined {
|
|
28
|
+
const match = error.message.match(/Mcp-Auth-Server:\s*([^;\]\s]+)/i);
|
|
28
29
|
if (!match?.[1]) return undefined;
|
|
29
30
|
|
|
30
31
|
try {
|
|
@@ -34,32 +35,6 @@ function parseMcpAuthServerUrl(errorMessage: string, serverUrl?: string): string
|
|
|
34
35
|
}
|
|
35
36
|
}
|
|
36
37
|
|
|
37
|
-
export function extractMcpAuthServerUrl(error: Error, serverUrl?: string): string | undefined {
|
|
38
|
-
return parseMcpAuthServerUrl(error.message, serverUrl);
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
/**
|
|
42
|
-
* Detect if an error indicates authentication is required.
|
|
43
|
-
* Checks for common auth error patterns.
|
|
44
|
-
*/
|
|
45
|
-
export function detectAuthError(error: Error): boolean {
|
|
46
|
-
const errorMsg = error.message.toLowerCase();
|
|
47
|
-
|
|
48
|
-
// Check for HTTP auth status codes
|
|
49
|
-
if (
|
|
50
|
-
errorMsg.includes("401") ||
|
|
51
|
-
errorMsg.includes("403") ||
|
|
52
|
-
errorMsg.includes("unauthorized") ||
|
|
53
|
-
errorMsg.includes("forbidden") ||
|
|
54
|
-
errorMsg.includes("authentication required") ||
|
|
55
|
-
errorMsg.includes("authentication failed")
|
|
56
|
-
) {
|
|
57
|
-
return true;
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
return false;
|
|
61
|
-
}
|
|
62
|
-
|
|
63
38
|
/**
|
|
64
39
|
* Extract OAuth endpoints from error response.
|
|
65
40
|
* Looks for WWW-Authenticate header format or JSON error bodies.
|
|
@@ -200,7 +175,8 @@ export function extractOAuthEndpoints(error: Error): OAuthEndpoints | null {
|
|
|
200
175
|
* Returns structured info about what auth is needed.
|
|
201
176
|
*/
|
|
202
177
|
export function analyzeAuthError(error: Error, serverUrl?: string): AuthDetectionResult {
|
|
203
|
-
|
|
178
|
+
// No auth required unless the error carries an HTTP auth status / auth-failure phrasing.
|
|
179
|
+
if (!AIError.is(AIError.classify(error), AIError.Flag.AuthFailed)) {
|
|
204
180
|
return { requiresAuth: false };
|
|
205
181
|
}
|
|
206
182
|
|
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
* Implements JSON-RPC 2.0 over HTTP POST with optional SSE streaming.
|
|
5
5
|
* Based on MCP spec 2025-03-26.
|
|
6
6
|
*/
|
|
7
|
+
import * as AIError from "@oh-my-pi/pi-ai/error";
|
|
7
8
|
import { logger, readSseJson, Snowflake } from "@oh-my-pi/pi-utils";
|
|
8
9
|
import type {
|
|
9
10
|
JsonRpcError,
|
|
@@ -186,7 +187,8 @@ export class HttpTransport implements MCPTransport {
|
|
|
186
187
|
return await this.#executeRequest<T>(method, params, options);
|
|
187
188
|
} catch (error) {
|
|
188
189
|
// Retry once on auth failure if onAuthError is wired
|
|
189
|
-
|
|
190
|
+
const status = error instanceof Error ? AIError.status(error) : undefined;
|
|
191
|
+
if (this.onAuthError && (status === 401 || status === 403)) {
|
|
190
192
|
const newHeaders = await this.onAuthError();
|
|
191
193
|
if (newHeaders) {
|
|
192
194
|
// Persist refreshed headers so subsequent requests use them directly
|
|
@@ -0,0 +1,377 @@
|
|
|
1
|
+
import * as AIError from "@oh-my-pi/pi-ai/error";
|
|
2
|
+
import { logger, readSseEvents, Snowflake } from "@oh-my-pi/pi-utils";
|
|
3
|
+
import type {
|
|
4
|
+
JsonRpcError,
|
|
5
|
+
JsonRpcMessage,
|
|
6
|
+
JsonRpcRequest,
|
|
7
|
+
JsonRpcResponse,
|
|
8
|
+
MCPRequestOptions,
|
|
9
|
+
MCPSseServerConfig,
|
|
10
|
+
MCPTransport,
|
|
11
|
+
} from "../../mcp/types";
|
|
12
|
+
import { toJsonRpcError } from "../../mcp/types";
|
|
13
|
+
import { createMCPTimeout, getNeverAbortSignal, resolveMCPTimeoutMs } from "../timeout";
|
|
14
|
+
|
|
15
|
+
interface MCPTimeoutOperation {
|
|
16
|
+
signal?: AbortSignal;
|
|
17
|
+
clear: () => void;
|
|
18
|
+
isTimeoutAbort: (error: unknown) => boolean;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
interface PendingLegacySseRequest {
|
|
22
|
+
resolve: (value: unknown) => void;
|
|
23
|
+
reject: (reason?: unknown) => void;
|
|
24
|
+
operation: MCPTimeoutOperation;
|
|
25
|
+
abortHandler?: () => void;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** Legacy MCP HTTP+SSE transport from protocol revision 2024-11-05. */
|
|
29
|
+
export class LegacySseTransport implements MCPTransport {
|
|
30
|
+
#connected = false;
|
|
31
|
+
#endpointUrl: string | null = null;
|
|
32
|
+
#sseConnection: AbortController | null = null;
|
|
33
|
+
#pending = new Map<string | number, PendingLegacySseRequest>();
|
|
34
|
+
#config: MCPSseServerConfig;
|
|
35
|
+
|
|
36
|
+
onClose?: () => void;
|
|
37
|
+
onError?: (error: Error) => void;
|
|
38
|
+
onNotification?: (method: string, params: unknown) => void;
|
|
39
|
+
onRequest?: (method: string, params: unknown) => Promise<unknown>;
|
|
40
|
+
/** Called on 401/403 to attempt token refresh. Returns updated headers or null. */
|
|
41
|
+
onAuthError?: () => Promise<Record<string, string> | null>;
|
|
42
|
+
|
|
43
|
+
constructor(config: MCPSseServerConfig) {
|
|
44
|
+
this.#config = config;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
get connected(): boolean {
|
|
48
|
+
return this.#connected;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
get url(): string {
|
|
52
|
+
return this.#config.url;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
async connect(): Promise<void> {
|
|
56
|
+
if (this.#connected) return;
|
|
57
|
+
if (this.#sseConnection) return;
|
|
58
|
+
|
|
59
|
+
const connection = new AbortController();
|
|
60
|
+
const timeout = resolveMCPTimeoutMs(this.#config.timeout);
|
|
61
|
+
const operation = createMCPTimeout(timeout, connection.signal);
|
|
62
|
+
const endpointReady = Promise.withResolvers<void>();
|
|
63
|
+
this.#sseConnection = connection;
|
|
64
|
+
|
|
65
|
+
try {
|
|
66
|
+
const response = await fetch(this.#config.url, {
|
|
67
|
+
method: "GET",
|
|
68
|
+
headers: {
|
|
69
|
+
Accept: "text/event-stream",
|
|
70
|
+
...this.#config.headers,
|
|
71
|
+
},
|
|
72
|
+
signal: operation.signal,
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
if (!response.ok) {
|
|
76
|
+
const text = await response.text();
|
|
77
|
+
throw new Error(`HTTP ${response.status}: ${text}`);
|
|
78
|
+
}
|
|
79
|
+
if (!response.body) {
|
|
80
|
+
throw new Error("Legacy SSE response did not include a body");
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
void this.#readSSEStream(response.body, operation, endpointReady).finally(() => {
|
|
84
|
+
const wasConnected = this.#connected;
|
|
85
|
+
if (this.#sseConnection === connection) this.#sseConnection = null;
|
|
86
|
+
if (wasConnected) this.onClose?.();
|
|
87
|
+
});
|
|
88
|
+
await endpointReady.promise;
|
|
89
|
+
} catch (error) {
|
|
90
|
+
operation.clear();
|
|
91
|
+
if (this.#sseConnection === connection) this.#sseConnection = null;
|
|
92
|
+
connection.abort();
|
|
93
|
+
if (operation.isTimeoutAbort(error)) {
|
|
94
|
+
throw new Error(`Legacy SSE endpoint timeout after ${timeout}ms`);
|
|
95
|
+
}
|
|
96
|
+
throw error;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
async #readSSEStream(
|
|
101
|
+
body: ReadableStream<Uint8Array>,
|
|
102
|
+
operation: MCPTimeoutOperation,
|
|
103
|
+
endpointReady: PromiseWithResolvers<void>,
|
|
104
|
+
): Promise<void> {
|
|
105
|
+
const signal = operation.signal ?? getNeverAbortSignal();
|
|
106
|
+
let endpointReceived = false;
|
|
107
|
+
try {
|
|
108
|
+
for await (const event of readSseEvents(body, signal)) {
|
|
109
|
+
if (event.event === "endpoint") {
|
|
110
|
+
if (!this.#endpointUrl) {
|
|
111
|
+
const endpointUrl = new URL(event.data, this.#config.url);
|
|
112
|
+
const configuredUrl = new URL(this.#config.url);
|
|
113
|
+
if (endpointUrl.origin !== configuredUrl.origin) {
|
|
114
|
+
throw new Error(
|
|
115
|
+
`Legacy SSE endpoint origin mismatch: expected ${configuredUrl.origin}, received ${endpointUrl.origin}`,
|
|
116
|
+
);
|
|
117
|
+
}
|
|
118
|
+
this.#endpointUrl = endpointUrl.href;
|
|
119
|
+
this.#connected = true;
|
|
120
|
+
endpointReceived = true;
|
|
121
|
+
operation.clear();
|
|
122
|
+
endpointReady.resolve();
|
|
123
|
+
}
|
|
124
|
+
continue;
|
|
125
|
+
}
|
|
126
|
+
if (event.data === "" || event.data === "[DONE]") continue;
|
|
127
|
+
|
|
128
|
+
let payload: unknown;
|
|
129
|
+
try {
|
|
130
|
+
payload = JSON.parse(event.data) as unknown;
|
|
131
|
+
} catch (error) {
|
|
132
|
+
if (error instanceof SyntaxError) {
|
|
133
|
+
throw new Error(`Legacy SSE message event contained non-JSON data: ${event.data}`);
|
|
134
|
+
}
|
|
135
|
+
throw error;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
const messages = Array.isArray(payload) ? payload : [payload];
|
|
139
|
+
for (const message of messages) {
|
|
140
|
+
if (typeof message !== "object" || message === null) continue;
|
|
141
|
+
this.#dispatchMessage(message as JsonRpcMessage);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
if (!endpointReceived) {
|
|
145
|
+
endpointReady.reject(new Error("Legacy SSE endpoint event not received"));
|
|
146
|
+
}
|
|
147
|
+
} catch (error) {
|
|
148
|
+
if (!endpointReceived) {
|
|
149
|
+
endpointReady.reject(error);
|
|
150
|
+
} else if (error instanceof Error && error.name !== "AbortError") {
|
|
151
|
+
logger.debug("Legacy SSE stream error", { url: this.#config.url, error: error.message });
|
|
152
|
+
this.onError?.(error);
|
|
153
|
+
this.#rejectPending(error);
|
|
154
|
+
}
|
|
155
|
+
} finally {
|
|
156
|
+
operation.clear();
|
|
157
|
+
if (endpointReceived) {
|
|
158
|
+
this.#rejectPending(new Error("Transport closed: legacy SSE stream closed"));
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
#dispatchMessage(message: JsonRpcMessage): void {
|
|
164
|
+
if ("id" in message && ("result" in message || "error" in message)) {
|
|
165
|
+
const pending = this.#pending.get(message.id);
|
|
166
|
+
if (pending) {
|
|
167
|
+
this.#pending.delete(message.id);
|
|
168
|
+
pending.operation.clear();
|
|
169
|
+
if (pending.abortHandler) pending.operation.signal?.removeEventListener("abort", pending.abortHandler);
|
|
170
|
+
const response = message as JsonRpcResponse;
|
|
171
|
+
if (response.error) {
|
|
172
|
+
pending.reject(new Error(`MCP error ${response.error.code}: ${response.error.message}`));
|
|
173
|
+
} else {
|
|
174
|
+
pending.resolve(response.result);
|
|
175
|
+
}
|
|
176
|
+
return;
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
if ("method" in message && "id" in message && message.id != null) {
|
|
180
|
+
void this.#handleServerRequest(message as JsonRpcRequest);
|
|
181
|
+
return;
|
|
182
|
+
}
|
|
183
|
+
if ("method" in message && !("id" in message)) {
|
|
184
|
+
this.onNotification?.(message.method, message.params);
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
async request<T = unknown>(
|
|
189
|
+
method: string,
|
|
190
|
+
params?: Record<string, unknown>,
|
|
191
|
+
options?: MCPRequestOptions,
|
|
192
|
+
): Promise<T> {
|
|
193
|
+
if (!this.#connected || !this.#endpointUrl) {
|
|
194
|
+
throw new Error("Transport not connected");
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
const id = Snowflake.next();
|
|
198
|
+
const body = {
|
|
199
|
+
jsonrpc: "2.0" as const,
|
|
200
|
+
id,
|
|
201
|
+
method,
|
|
202
|
+
params: params ?? {},
|
|
203
|
+
};
|
|
204
|
+
const timeout = resolveMCPTimeoutMs(this.#config.timeout);
|
|
205
|
+
const operation = createMCPTimeout(timeout, options?.signal);
|
|
206
|
+
const deferred = Promise.withResolvers<unknown>();
|
|
207
|
+
// Observe the response promise synchronously so a stream-close rejection
|
|
208
|
+
// from `#rejectPending` that lands while `request()` is still awaiting the
|
|
209
|
+
// POST round-trip is never flagged as an unhandled rejection. The real
|
|
210
|
+
// `await deferred.promise` below still receives and propagates the error.
|
|
211
|
+
void deferred.promise.catch(() => undefined);
|
|
212
|
+
const pending: PendingLegacySseRequest = {
|
|
213
|
+
resolve: deferred.resolve,
|
|
214
|
+
reject: deferred.reject,
|
|
215
|
+
operation,
|
|
216
|
+
};
|
|
217
|
+
if (operation.signal) {
|
|
218
|
+
pending.abortHandler = () => {
|
|
219
|
+
this.#pending.delete(id);
|
|
220
|
+
operation.clear();
|
|
221
|
+
deferred.reject(
|
|
222
|
+
options?.signal?.aborted && options.signal.reason instanceof Error
|
|
223
|
+
? options.signal.reason
|
|
224
|
+
: new Error(`Legacy SSE response timeout after ${timeout}ms`),
|
|
225
|
+
);
|
|
226
|
+
};
|
|
227
|
+
operation.signal.addEventListener("abort", pending.abortHandler, { once: true });
|
|
228
|
+
}
|
|
229
|
+
this.#pending.set(id, pending);
|
|
230
|
+
|
|
231
|
+
try {
|
|
232
|
+
const response = await this.#postJson(body, operation.signal);
|
|
233
|
+
if (!response.ok) {
|
|
234
|
+
const text = await response.text();
|
|
235
|
+
throw new Error(`HTTP ${response.status}: ${text}`);
|
|
236
|
+
}
|
|
237
|
+
await response.body?.cancel();
|
|
238
|
+
return (await deferred.promise) as T;
|
|
239
|
+
} catch (error) {
|
|
240
|
+
this.#pending.delete(id);
|
|
241
|
+
operation.clear();
|
|
242
|
+
if (pending.abortHandler) operation.signal?.removeEventListener("abort", pending.abortHandler);
|
|
243
|
+
if (operation.isTimeoutAbort(error)) {
|
|
244
|
+
throw new Error(`Request timeout after ${timeout}ms`);
|
|
245
|
+
}
|
|
246
|
+
throw error;
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
async notify(method: string, params?: Record<string, unknown>): Promise<void> {
|
|
251
|
+
if (!this.#connected || !this.#endpointUrl) {
|
|
252
|
+
throw new Error("Transport not connected");
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
const timeout = resolveMCPTimeoutMs(this.#config.timeout);
|
|
256
|
+
const operation = createMCPTimeout(timeout);
|
|
257
|
+
try {
|
|
258
|
+
const response = await this.#postJson(
|
|
259
|
+
{
|
|
260
|
+
jsonrpc: "2.0" as const,
|
|
261
|
+
method,
|
|
262
|
+
params: params ?? {},
|
|
263
|
+
},
|
|
264
|
+
operation.signal,
|
|
265
|
+
);
|
|
266
|
+
operation.clear();
|
|
267
|
+
if (!response.ok) {
|
|
268
|
+
const text = await response.text();
|
|
269
|
+
throw new Error(`HTTP ${response.status}: ${text}`);
|
|
270
|
+
}
|
|
271
|
+
await response.body?.cancel();
|
|
272
|
+
} catch (error) {
|
|
273
|
+
operation.clear();
|
|
274
|
+
if (operation.isTimeoutAbort(error)) {
|
|
275
|
+
throw new Error(`Notify timeout after ${timeout}ms`);
|
|
276
|
+
}
|
|
277
|
+
throw error;
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
async #postJson(
|
|
282
|
+
body: JsonRpcRequest | JsonRpcResponse | { jsonrpc: "2.0"; method: string; params: Record<string, unknown> },
|
|
283
|
+
signal?: AbortSignal,
|
|
284
|
+
): Promise<Response> {
|
|
285
|
+
const endpointUrl = this.#endpointUrl;
|
|
286
|
+
if (!endpointUrl) throw new Error("Transport not connected");
|
|
287
|
+
let headers: Record<string, string> = {
|
|
288
|
+
"Content-Type": "application/json",
|
|
289
|
+
Accept: "application/json, text/event-stream",
|
|
290
|
+
...this.#config.headers,
|
|
291
|
+
};
|
|
292
|
+
let response = await fetch(endpointUrl, {
|
|
293
|
+
method: "POST",
|
|
294
|
+
headers,
|
|
295
|
+
body: JSON.stringify(body),
|
|
296
|
+
signal,
|
|
297
|
+
});
|
|
298
|
+
const status = AIError.status(response);
|
|
299
|
+
if (!this.onAuthError || (status !== 401 && status !== 403)) return response;
|
|
300
|
+
|
|
301
|
+
const refreshedHeaders = await this.onAuthError();
|
|
302
|
+
if (!refreshedHeaders) return response;
|
|
303
|
+
await response.body?.cancel();
|
|
304
|
+
this.#config.headers = refreshedHeaders;
|
|
305
|
+
headers = {
|
|
306
|
+
"Content-Type": "application/json",
|
|
307
|
+
Accept: "application/json, text/event-stream",
|
|
308
|
+
...this.#config.headers,
|
|
309
|
+
};
|
|
310
|
+
response = await fetch(endpointUrl, {
|
|
311
|
+
method: "POST",
|
|
312
|
+
headers,
|
|
313
|
+
body: JSON.stringify(body),
|
|
314
|
+
signal,
|
|
315
|
+
});
|
|
316
|
+
return response;
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
async #handleServerRequest(request: JsonRpcRequest): Promise<void> {
|
|
320
|
+
if (!this.onRequest) {
|
|
321
|
+
await this.#sendServerResponse(request.id, undefined, { code: -32601, message: "Method not found" });
|
|
322
|
+
return;
|
|
323
|
+
}
|
|
324
|
+
try {
|
|
325
|
+
const result = await this.onRequest(request.method, request.params);
|
|
326
|
+
await this.#sendServerResponse(request.id, result);
|
|
327
|
+
} catch (error) {
|
|
328
|
+
await this.#sendServerResponse(request.id, undefined, toJsonRpcError(error));
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
async #sendServerResponse(id: string | number, result?: unknown, error?: JsonRpcError): Promise<void> {
|
|
333
|
+
if (!this.#connected) return;
|
|
334
|
+
const timeout = resolveMCPTimeoutMs(this.#config.timeout);
|
|
335
|
+
const operation = createMCPTimeout(timeout);
|
|
336
|
+
try {
|
|
337
|
+
const response = await this.#postJson(
|
|
338
|
+
error ? { jsonrpc: "2.0" as const, id, error } : { jsonrpc: "2.0" as const, id, result: result ?? {} },
|
|
339
|
+
operation.signal,
|
|
340
|
+
);
|
|
341
|
+
operation.clear();
|
|
342
|
+
await response.body?.cancel();
|
|
343
|
+
} catch {
|
|
344
|
+
operation.clear();
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
#rejectPending(error: Error): void {
|
|
349
|
+
for (const [id, pending] of this.#pending) {
|
|
350
|
+
this.#pending.delete(id);
|
|
351
|
+
pending.operation.clear();
|
|
352
|
+
if (pending.abortHandler) pending.operation.signal?.removeEventListener("abort", pending.abortHandler);
|
|
353
|
+
pending.reject(error);
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
async close(): Promise<void> {
|
|
358
|
+
if (!this.#connected && !this.#sseConnection) return;
|
|
359
|
+
const wasConnected = this.#connected;
|
|
360
|
+
this.#connected = false;
|
|
361
|
+
this.#endpointUrl = null;
|
|
362
|
+
if (this.#sseConnection) {
|
|
363
|
+
this.#sseConnection.abort();
|
|
364
|
+
this.#sseConnection = null;
|
|
365
|
+
}
|
|
366
|
+
this.#rejectPending(new Error("Transport closed"));
|
|
367
|
+
if (wasConnected) this.onClose?.();
|
|
368
|
+
this.onClose = undefined;
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
/** Create and connect a legacy HTTP+SSE transport. */
|
|
373
|
+
export async function createSseTransport(config: MCPSseServerConfig): Promise<LegacySseTransport> {
|
|
374
|
+
const transport = new LegacySseTransport(config);
|
|
375
|
+
await transport.connect();
|
|
376
|
+
return transport;
|
|
377
|
+
}
|
package/src/memories/index.ts
CHANGED
|
@@ -529,12 +529,21 @@ async function collectThreads(session: AgentSession, currentThreadId?: string):
|
|
|
529
529
|
let id = name.slice(0, -6);
|
|
530
530
|
try {
|
|
531
531
|
const fileText = await Bun.file(fullPath).text();
|
|
532
|
-
|
|
533
|
-
const
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
532
|
+
let sawTitleSlot = false;
|
|
533
|
+
for (const rawLine of fileText.split(/\r?\n/)) {
|
|
534
|
+
const line = rawLine.trim();
|
|
535
|
+
if (!line) continue;
|
|
536
|
+
const parsed = parseJsonlLenient<Record<string, unknown>>(line);
|
|
537
|
+
const header = Array.isArray(parsed) && parsed.length > 0 ? parsed[0] : undefined;
|
|
538
|
+
if (!sawTitleSlot && header?.type === "title") {
|
|
539
|
+
sawTitleSlot = true;
|
|
540
|
+
continue;
|
|
541
|
+
}
|
|
542
|
+
if (header?.type === "session") {
|
|
543
|
+
if (typeof header.cwd === "string") cwd = header.cwd;
|
|
544
|
+
if (typeof header.id === "string") id = header.id;
|
|
545
|
+
}
|
|
546
|
+
break;
|
|
538
547
|
}
|
|
539
548
|
} catch {
|
|
540
549
|
// ignore malformed session files
|
package/src/mnemopi/backend.ts
CHANGED
|
@@ -511,10 +511,10 @@ async function resolveMnemopiProviderOptions(
|
|
|
511
511
|
}
|
|
512
512
|
|
|
513
513
|
try {
|
|
514
|
-
const resolved = resolveRoleSelection(["smol"], settings, modelRegistry.getAvailable(), modelRegistry);
|
|
514
|
+
const resolved = resolveRoleSelection(["tiny", "smol"], settings, modelRegistry.getAvailable(), modelRegistry);
|
|
515
515
|
const model = resolved?.model;
|
|
516
516
|
if (!model) {
|
|
517
|
-
logger.warn("Mnemopi: llmMode=smol but no smol model resolved; continuing without LLM.");
|
|
517
|
+
logger.warn("Mnemopi: llmMode=smol but no tiny/smol model resolved; continuing without LLM.");
|
|
518
518
|
return base;
|
|
519
519
|
}
|
|
520
520
|
return {
|
|
@@ -2014,7 +2014,7 @@ export class AcpAgent implements Agent {
|
|
|
2014
2014
|
}
|
|
2015
2015
|
}
|
|
2016
2016
|
}
|
|
2017
|
-
if (notifications.length === 0 && message.errorMessage && !isSilentAbort(message
|
|
2017
|
+
if (notifications.length === 0 && message.errorMessage && !isSilentAbort(message)) {
|
|
2018
2018
|
notifications.push({
|
|
2019
2019
|
sessionId,
|
|
2020
2020
|
update: {
|