@oh-my-pi/pi-coding-agent 16.2.2 → 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 +49 -0
- package/dist/cli.js +3624 -3568
- 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/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/settings-schema.d.ts +81 -0
- package/dist/types/debug/log-viewer.d.ts +1 -0
- package/dist/types/debug/raw-sse.d.ts +1 -0
- package/dist/types/edit/hashline/diff.d.ts +0 -11
- package/dist/types/extensibility/tool-event-input.d.ts +7 -0
- package/dist/types/extensibility/utils.d.ts +12 -0
- 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 -1
- 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/session/agent-session.d.ts +58 -10
- package/dist/types/session/indexed-session-storage.d.ts +7 -1
- package/dist/types/session/messages.d.ts +26 -0
- 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/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 +242 -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 +12 -2
- package/src/advisor/transcript-recorder.ts +25 -2
- package/src/advisor/watchdog.ts +57 -31
- 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/settings-schema.ts +77 -0
- 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/edit/hashline/diff.ts +57 -4
- package/src/eval/js/shared/local-module-loader.ts +23 -1
- package/src/export/html/template.js +13 -7
- 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/manager.ts +2 -1
- 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/transports/index.ts +1 -0
- package/src/mcp/transports/sse.ts +377 -0
- package/src/memories/index.ts +15 -6
- 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/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 +144 -5
- package/src/modes/components/status-line/segments.ts +46 -21
- 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 +106 -0
- 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 -5
- package/src/modes/skill-command.ts +116 -0
- package/src/modes/types.ts +7 -2
- package/src/modes/utils/ui-helpers.ts +41 -23
- 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 +28 -24
- package/src/session/agent-session.ts +867 -428
- package/src/session/indexed-session-storage.ts +86 -13
- package/src/session/messages.test.ts +125 -0
- package/src/session/messages.ts +172 -9
- 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 +95 -1
- package/src/session/session-storage.ts +36 -0
- package/src/session/session-title-slot.ts +141 -0
- package/src/session/sql-session-storage.ts +71 -11
- package/src/slash-commands/builtin-registry.ts +16 -3
- 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/text.ts +23 -0
- package/src/tools/ask.ts +55 -4
- 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/yield.ts +140 -31
- package/src/utils/thinking-display.ts +15 -0
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") {
|
|
@@ -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
|