@oh-my-pi/pi-coding-agent 6.8.0 → 6.8.2
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 +23 -0
- package/examples/extensions/plan-mode.ts +8 -8
- package/examples/extensions/tools.ts +7 -7
- package/package.json +6 -6
- package/src/cli/session-picker.ts +5 -2
- package/src/core/agent-session.ts +18 -5
- package/src/core/auth-storage.ts +13 -1
- package/src/core/bash-executor.ts +5 -4
- package/src/core/exec.ts +4 -2
- package/src/core/extensions/types.ts +1 -1
- package/src/core/hooks/types.ts +4 -3
- package/src/core/mcp/transports/http.ts +35 -27
- package/src/core/prompt-templates.ts +1 -1
- package/src/core/python-gateway-coordinator.ts +5 -4
- package/src/core/ssh/ssh-executor.ts +1 -1
- package/src/core/tools/lsp/client.ts +1 -1
- package/src/core/tools/patch/applicator.ts +38 -24
- package/src/core/tools/patch/diff.ts +7 -3
- package/src/core/tools/patch/fuzzy.ts +19 -1
- package/src/core/tools/patch/index.ts +4 -1
- package/src/core/tools/patch/types.ts +4 -0
- package/src/core/tools/python.ts +1 -0
- package/src/core/tools/task/executor.ts +100 -64
- package/src/core/tools/task/worker.ts +44 -14
- package/src/core/tools/web-fetch.ts +47 -7
- package/src/core/tools/web-scrapers/youtube.ts +6 -49
- package/src/lib/worktree/collapse.ts +3 -3
- package/src/lib/worktree/git.ts +6 -40
- package/src/lib/worktree/index.ts +1 -1
- package/src/main.ts +7 -5
- package/src/modes/interactive/components/login-dialog.ts +6 -2
- package/src/modes/interactive/components/tool-execution.ts +4 -0
- package/src/modes/interactive/interactive-mode.ts +3 -0
- package/src/utils/clipboard.ts +3 -5
- package/src/core/tools/task/model-resolver.ts +0 -206
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,29 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [6.8.2] - 2026-01-21
|
|
6
|
+
|
|
7
|
+
### Fixed
|
|
8
|
+
|
|
9
|
+
- Improved error messages when multiple text occurrences are found by showing line previews and context
|
|
10
|
+
- Enhanced patch application to better handle duplicate content in context lines
|
|
11
|
+
- Added occurrence previews to help users disambiguate between multiple matches
|
|
12
|
+
- Fixed cache invalidation for streaming edits to prevent stale data
|
|
13
|
+
- Fixed file existence check for prompt templates directory
|
|
14
|
+
- Fixed bash output streaming to prevent premature stream closure
|
|
15
|
+
- Fixed LSP client request handling when signal is already aborted
|
|
16
|
+
- Fixed git apply operations with stdin input handling
|
|
17
|
+
|
|
18
|
+
### Security
|
|
19
|
+
|
|
20
|
+
- Updated Anthropic authentication to handle manual code input securely
|
|
21
|
+
|
|
22
|
+
## [6.8.1] - 2026-01-20
|
|
23
|
+
|
|
24
|
+
### Fixed
|
|
25
|
+
|
|
26
|
+
- Fixed unhandled promise rejection when tool execution fails by adding missing `.catch()` to floating `.finally()` chain in `createAbortablePromise`
|
|
27
|
+
|
|
5
28
|
## [6.8.0] - 2026-01-20
|
|
6
29
|
|
|
7
30
|
### Added
|
|
@@ -247,16 +247,16 @@ export default function planModeExtension(pi: ExtensionAPI) {
|
|
|
247
247
|
}
|
|
248
248
|
}
|
|
249
249
|
|
|
250
|
-
function togglePlanMode(ctx: ExtensionContext) {
|
|
250
|
+
async function togglePlanMode(ctx: ExtensionContext) {
|
|
251
251
|
planModeEnabled = !planModeEnabled;
|
|
252
252
|
executionMode = false;
|
|
253
253
|
todoItems = [];
|
|
254
254
|
|
|
255
255
|
if (planModeEnabled) {
|
|
256
|
-
pi.setActiveTools(PLAN_MODE_TOOLS);
|
|
256
|
+
await pi.setActiveTools(PLAN_MODE_TOOLS);
|
|
257
257
|
ctx.ui.notify(`Plan mode enabled. Tools: ${PLAN_MODE_TOOLS.join(", ")}`);
|
|
258
258
|
} else {
|
|
259
|
-
pi.setActiveTools(NORMAL_MODE_TOOLS);
|
|
259
|
+
await pi.setActiveTools(NORMAL_MODE_TOOLS);
|
|
260
260
|
ctx.ui.notify("Plan mode disabled. Full access restored.");
|
|
261
261
|
}
|
|
262
262
|
updateStatus(ctx);
|
|
@@ -266,7 +266,7 @@ export default function planModeExtension(pi: ExtensionAPI) {
|
|
|
266
266
|
pi.registerCommand("plan", {
|
|
267
267
|
description: "Toggle plan mode (read-only exploration)",
|
|
268
268
|
handler: async (_args, ctx) => {
|
|
269
|
-
togglePlanMode(ctx);
|
|
269
|
+
await togglePlanMode(ctx);
|
|
270
270
|
},
|
|
271
271
|
});
|
|
272
272
|
|
|
@@ -294,7 +294,7 @@ export default function planModeExtension(pi: ExtensionAPI) {
|
|
|
294
294
|
pi.registerShortcut(Key.shift("p"), {
|
|
295
295
|
description: "Toggle plan mode",
|
|
296
296
|
handler: async (ctx) => {
|
|
297
|
-
togglePlanMode(ctx);
|
|
297
|
+
await togglePlanMode(ctx);
|
|
298
298
|
},
|
|
299
299
|
});
|
|
300
300
|
|
|
@@ -417,7 +417,7 @@ Execute each step in order.`,
|
|
|
417
417
|
|
|
418
418
|
executionMode = false;
|
|
419
419
|
todoItems = [];
|
|
420
|
-
pi.setActiveTools(NORMAL_MODE_TOOLS);
|
|
420
|
+
await pi.setActiveTools(NORMAL_MODE_TOOLS);
|
|
421
421
|
updateStatus(ctx);
|
|
422
422
|
}
|
|
423
423
|
return;
|
|
@@ -470,7 +470,7 @@ Execute each step in order.`,
|
|
|
470
470
|
if (choice?.startsWith("Execute")) {
|
|
471
471
|
planModeEnabled = false;
|
|
472
472
|
executionMode = hasTodos;
|
|
473
|
-
pi.setActiveTools(NORMAL_MODE_TOOLS);
|
|
473
|
+
await pi.setActiveTools(NORMAL_MODE_TOOLS);
|
|
474
474
|
updateStatus(ctx);
|
|
475
475
|
|
|
476
476
|
// Simple execution message - context event filters old plan mode messages
|
|
@@ -519,7 +519,7 @@ Execute each step in order.`,
|
|
|
519
519
|
}
|
|
520
520
|
|
|
521
521
|
if (planModeEnabled) {
|
|
522
|
-
pi.setActiveTools(PLAN_MODE_TOOLS);
|
|
522
|
+
await pi.setActiveTools(PLAN_MODE_TOOLS);
|
|
523
523
|
}
|
|
524
524
|
updateStatus(ctx);
|
|
525
525
|
});
|
|
@@ -31,12 +31,12 @@ export default function toolsExtension(pi: ExtensionAPI) {
|
|
|
31
31
|
}
|
|
32
32
|
|
|
33
33
|
// Apply current tool selection
|
|
34
|
-
function applyTools() {
|
|
35
|
-
pi.setActiveTools(Array.from(enabledTools));
|
|
34
|
+
async function applyTools() {
|
|
35
|
+
await pi.setActiveTools(Array.from(enabledTools));
|
|
36
36
|
}
|
|
37
37
|
|
|
38
38
|
// Find the last tools-config entry in the current branch
|
|
39
|
-
function restoreFromBranch(ctx: ExtensionContext) {
|
|
39
|
+
async function restoreFromBranch(ctx: ExtensionContext) {
|
|
40
40
|
allTools = pi.getAllTools();
|
|
41
41
|
|
|
42
42
|
// Get entries in current branch only
|
|
@@ -55,7 +55,7 @@ export default function toolsExtension(pi: ExtensionAPI) {
|
|
|
55
55
|
if (savedTools) {
|
|
56
56
|
// Restore saved tool selection (filter to only tools that still exist)
|
|
57
57
|
enabledTools = new Set(savedTools.filter((t: string) => allTools.includes(t)));
|
|
58
|
-
applyTools();
|
|
58
|
+
await applyTools();
|
|
59
59
|
} else {
|
|
60
60
|
// No saved state - sync with currently active tools
|
|
61
61
|
enabledTools = new Set(pi.getActiveTools());
|
|
@@ -130,16 +130,16 @@ export default function toolsExtension(pi: ExtensionAPI) {
|
|
|
130
130
|
|
|
131
131
|
// Restore state on session start
|
|
132
132
|
pi.on("session_start", async (_event, ctx) => {
|
|
133
|
-
restoreFromBranch(ctx);
|
|
133
|
+
await restoreFromBranch(ctx);
|
|
134
134
|
});
|
|
135
135
|
|
|
136
136
|
// Restore state when navigating the session tree
|
|
137
137
|
pi.on("session_tree", async (_event, ctx) => {
|
|
138
|
-
restoreFromBranch(ctx);
|
|
138
|
+
await restoreFromBranch(ctx);
|
|
139
139
|
});
|
|
140
140
|
|
|
141
141
|
// Restore state after branching
|
|
142
142
|
pi.on("session_branch", async (_event, ctx) => {
|
|
143
|
-
restoreFromBranch(ctx);
|
|
143
|
+
await restoreFromBranch(ctx);
|
|
144
144
|
});
|
|
145
145
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@oh-my-pi/pi-coding-agent",
|
|
3
|
-
"version": "6.8.
|
|
3
|
+
"version": "6.8.2",
|
|
4
4
|
"description": "Coding agent CLI with read, bash, edit, write tools and session management",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"ompConfig": {
|
|
@@ -40,11 +40,11 @@
|
|
|
40
40
|
"prepublishOnly": "bun run generate-template && bun run clean && bun run build"
|
|
41
41
|
},
|
|
42
42
|
"dependencies": {
|
|
43
|
-
"@oh-my-pi/pi-agent-core": "6.8.
|
|
44
|
-
"@oh-my-pi/pi-ai": "6.8.
|
|
45
|
-
"@oh-my-pi/pi-git-tool": "6.8.
|
|
46
|
-
"@oh-my-pi/pi-tui": "6.8.
|
|
47
|
-
"@oh-my-pi/pi-utils": "6.8.
|
|
43
|
+
"@oh-my-pi/pi-agent-core": "6.8.2",
|
|
44
|
+
"@oh-my-pi/pi-ai": "6.8.2",
|
|
45
|
+
"@oh-my-pi/pi-git-tool": "6.8.2",
|
|
46
|
+
"@oh-my-pi/pi-tui": "6.8.2",
|
|
47
|
+
"@oh-my-pi/pi-utils": "6.8.2",
|
|
48
48
|
"@openai/agents": "^0.3.7",
|
|
49
49
|
"@sinclair/typebox": "^0.34.46",
|
|
50
50
|
"ajv": "^8.17.1",
|
|
@@ -454,15 +454,19 @@ export class AgentSession {
|
|
|
454
454
|
}
|
|
455
455
|
|
|
456
456
|
if (event.message.role === "toolResult") {
|
|
457
|
-
const { $normative, toolCallId } = event.message as {
|
|
457
|
+
const { toolName, $normative, toolCallId, details } = event.message as {
|
|
458
458
|
toolName?: string;
|
|
459
459
|
toolCallId?: string;
|
|
460
|
-
details?:
|
|
460
|
+
details?: { path?: string };
|
|
461
461
|
$normative?: Record<string, unknown>;
|
|
462
462
|
};
|
|
463
463
|
if ($normative && toolCallId && this.settingsManager.getNormativeRewrite()) {
|
|
464
464
|
await this._rewriteToolCallArgs(toolCallId, $normative);
|
|
465
465
|
}
|
|
466
|
+
// Invalidate streaming edit cache when edit tool completes to prevent stale data
|
|
467
|
+
if (toolName === "edit" && details?.path) {
|
|
468
|
+
this._invalidateFileCacheForPath(details.path);
|
|
469
|
+
}
|
|
466
470
|
}
|
|
467
471
|
}
|
|
468
472
|
|
|
@@ -579,11 +583,16 @@ export class AgentSession {
|
|
|
579
583
|
this._streamingEditFileCache.set(resolvedPath, normalizeToLF(text));
|
|
580
584
|
}
|
|
581
585
|
} catch {
|
|
582
|
-
//
|
|
583
|
-
this._streamingEditFileCache.set(resolvedPath, "");
|
|
586
|
+
// Don't cache on read errors - let the edit tool handle them
|
|
584
587
|
}
|
|
585
588
|
}
|
|
586
589
|
|
|
590
|
+
/** Invalidate cache for a file after an edit completes to prevent stale data */
|
|
591
|
+
private _invalidateFileCacheForPath(path: string): void {
|
|
592
|
+
const resolvedPath = resolveToCwd(path, this.sessionManager.getCwd());
|
|
593
|
+
this._streamingEditFileCache.delete(resolvedPath);
|
|
594
|
+
}
|
|
595
|
+
|
|
587
596
|
private _maybeAbortStreamingEdit(event: AgentEvent): void {
|
|
588
597
|
if (!this.settingsManager.getEditStreamingAbort()) return;
|
|
589
598
|
if (this._streamingEditAbortTriggered) return;
|
|
@@ -2226,7 +2235,7 @@ export class AgentSession {
|
|
|
2226
2235
|
error: message,
|
|
2227
2236
|
model: `${candidate.provider}/${candidate.id}`,
|
|
2228
2237
|
});
|
|
2229
|
-
await
|
|
2238
|
+
await abortableSleep(delayMs, this._autoCompactionAbortController.signal);
|
|
2230
2239
|
}
|
|
2231
2240
|
}
|
|
2232
2241
|
|
|
@@ -2291,6 +2300,10 @@ export class AgentSession {
|
|
|
2291
2300
|
}, 100);
|
|
2292
2301
|
}
|
|
2293
2302
|
} catch (error) {
|
|
2303
|
+
if (this._autoCompactionAbortController?.signal.aborted) {
|
|
2304
|
+
this._emit({ type: "auto_compaction_end", result: undefined, aborted: true, willRetry: false });
|
|
2305
|
+
return;
|
|
2306
|
+
}
|
|
2294
2307
|
const errorMessage = error instanceof Error ? error.message : "compaction failed";
|
|
2295
2308
|
this._emit({
|
|
2296
2309
|
type: "auto_compaction_end",
|
package/src/core/auth-storage.ts
CHANGED
|
@@ -107,10 +107,12 @@ function toBoolean(value: unknown): boolean | undefined {
|
|
|
107
107
|
export class AuthStorage {
|
|
108
108
|
private static readonly codexUsageCacheTtlMs = 60_000; // Cache usage data for 1 minute
|
|
109
109
|
private static readonly defaultBackoffMs = 60_000; // Default backoff when no reset time available
|
|
110
|
+
private static readonly cacheCleanupIntervalMs = 300_000; // Clean expired cache every 5 minutes
|
|
110
111
|
|
|
111
112
|
/** Provider -> credentials cache, populated from agent.db on reload(). */
|
|
112
113
|
private data: Map<string, StoredCredential[]> = new Map();
|
|
113
114
|
private storage: AgentStorage;
|
|
115
|
+
private lastCacheCleanup = 0;
|
|
114
116
|
/** Resolved path to agent.db (derived from authPath or used directly if .db). */
|
|
115
117
|
private dbPath: string;
|
|
116
118
|
private runtimeOverrides: Map<string, string> = new Map();
|
|
@@ -153,6 +155,7 @@ export class AuthStorage {
|
|
|
153
155
|
instance.sessionLastCredential = new Map();
|
|
154
156
|
instance.credentialBackoff = new Map();
|
|
155
157
|
instance.codexUsageCache = new Map();
|
|
158
|
+
instance.lastCacheCleanup = 0;
|
|
156
159
|
|
|
157
160
|
for (const [provider, creds] of Object.entries(data.credentials)) {
|
|
158
161
|
instance.data.set(
|
|
@@ -557,7 +560,11 @@ export class AuthStorage {
|
|
|
557
560
|
|
|
558
561
|
switch (provider) {
|
|
559
562
|
case "anthropic":
|
|
560
|
-
credentials = await loginAnthropic(
|
|
563
|
+
credentials = await loginAnthropic({
|
|
564
|
+
...ctrl,
|
|
565
|
+
onManualCodeInput: async () =>
|
|
566
|
+
ctrl.onPrompt({ message: "Paste the authorization code (or full redirect URL):" }),
|
|
567
|
+
});
|
|
561
568
|
break;
|
|
562
569
|
case "github-copilot":
|
|
563
570
|
credentials = await loginGitHubCopilot({
|
|
@@ -748,6 +755,11 @@ export class AuthStorage {
|
|
|
748
755
|
const cacheKey = this.getCodexUsageCacheKey(accountId, normalizedBase);
|
|
749
756
|
const now = Date.now();
|
|
750
757
|
|
|
758
|
+
if (now - this.lastCacheCleanup > AuthStorage.cacheCleanupIntervalMs) {
|
|
759
|
+
this.lastCacheCleanup = now;
|
|
760
|
+
this.storage.cleanExpiredCache();
|
|
761
|
+
}
|
|
762
|
+
|
|
751
763
|
// Check in-memory cache first (fastest)
|
|
752
764
|
const memCached = this.codexUsageCache.get(cacheKey);
|
|
753
765
|
if (memCached && memCached.expiresAt > now) {
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* Provides unified bash execution for AgentSession.executeBash() and direct calls.
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
-
import { cspawn, Exception } from "@oh-my-pi/pi-utils";
|
|
7
|
+
import { cspawn, Exception, ptree } from "@oh-my-pi/pi-utils";
|
|
8
8
|
import { getShellConfig } from "../utils/shell";
|
|
9
9
|
import { getOrCreateSnapshot, getSnapshotSourceCommand } from "../utils/shell-snapshot";
|
|
10
10
|
import { OutputSink } from "./streaming-output";
|
|
@@ -34,7 +34,7 @@ export async function executeBash(command: string, options?: BashExecutorOptions
|
|
|
34
34
|
const prefixedCommand = prefix ? `${prefix} ${command}` : command;
|
|
35
35
|
const finalCommand = `${snapshotPrefix}${prefixedCommand}`;
|
|
36
36
|
|
|
37
|
-
const stream = new OutputSink({
|
|
37
|
+
const stream = new OutputSink({ onChunk: options?.onChunk });
|
|
38
38
|
|
|
39
39
|
const child = cspawn([shell, ...args, finalCommand], {
|
|
40
40
|
cwd: options?.cwd,
|
|
@@ -44,6 +44,7 @@ export async function executeBash(command: string, options?: BashExecutorOptions
|
|
|
44
44
|
});
|
|
45
45
|
|
|
46
46
|
// Pump streams - errors during abort/timeout are expected
|
|
47
|
+
// Use preventClose to avoid closing the shared sink when either stream finishes
|
|
47
48
|
await Promise.allSettled([
|
|
48
49
|
child.stdout.pipeTo(stream.createWritable()),
|
|
49
50
|
child.stderr.pipeTo(stream.createWritable()),
|
|
@@ -63,7 +64,7 @@ export async function executeBash(command: string, options?: BashExecutorOptions
|
|
|
63
64
|
// Exception covers NonZeroExitError, AbortError, TimeoutError
|
|
64
65
|
if (err instanceof Exception) {
|
|
65
66
|
if (err.aborted) {
|
|
66
|
-
const isTimeout = err.message.includes("timed out");
|
|
67
|
+
const isTimeout = err instanceof ptree.TimeoutError || err.message.toLowerCase().includes("timed out");
|
|
67
68
|
const annotation = isTimeout
|
|
68
69
|
? `Command timed out after ${Math.round((options?.timeout ?? 0) / 1000)} seconds`
|
|
69
70
|
: undefined;
|
|
@@ -92,7 +93,7 @@ export async function executeBashWithOperations(
|
|
|
92
93
|
operations: BashOperations,
|
|
93
94
|
options?: BashExecutorOptions,
|
|
94
95
|
): Promise<BashResult> {
|
|
95
|
-
const stream = new OutputSink({
|
|
96
|
+
const stream = new OutputSink({ onChunk: options?.onChunk });
|
|
96
97
|
const writable = stream.createWritable();
|
|
97
98
|
const writer = writable.getWriter();
|
|
98
99
|
|
package/src/core/exec.ts
CHANGED
|
@@ -41,14 +41,16 @@ export async function execCommand(
|
|
|
41
41
|
signal: options?.signal,
|
|
42
42
|
timeout: options?.timeout,
|
|
43
43
|
});
|
|
44
|
+
// Read streams before awaiting exit to avoid data loss if streams close
|
|
45
|
+
const [stdoutText, stderrText] = await Promise.all([proc.stdout.text(), proc.stderr.text()]);
|
|
44
46
|
try {
|
|
45
47
|
await proc.exited;
|
|
46
48
|
} catch {
|
|
47
49
|
// ChildProcess rejects on non-zero exit; we handle it below
|
|
48
50
|
}
|
|
49
51
|
return {
|
|
50
|
-
stdout:
|
|
51
|
-
stderr:
|
|
52
|
+
stdout: stdoutText,
|
|
53
|
+
stderr: stderrText,
|
|
52
54
|
code: proc.exitCode ?? 0,
|
|
53
55
|
killed: proc.exitReason instanceof ptree.AbortError,
|
|
54
56
|
};
|
|
@@ -757,7 +757,7 @@ export interface ExtensionAPI {
|
|
|
757
757
|
getAllTools(): string[];
|
|
758
758
|
|
|
759
759
|
/** Set the active tools by name. */
|
|
760
|
-
setActiveTools(toolNames: string[]): void
|
|
760
|
+
setActiveTools(toolNames: string[]): Promise<void>;
|
|
761
761
|
|
|
762
762
|
/** Set the current model. Returns false if no API key available. */
|
|
763
763
|
setModel(model: Model<any>): Promise<boolean>;
|
package/src/core/hooks/types.ts
CHANGED
|
@@ -685,12 +685,13 @@ export interface HookAPI {
|
|
|
685
685
|
* @param message.content - Message content (string or TextContent/ImageContent array)
|
|
686
686
|
* @param message.display - Whether to show in TUI (true = styled display, false = hidden)
|
|
687
687
|
* @param message.details - Optional hook-specific metadata (not sent to LLM)
|
|
688
|
-
* @param triggerTurn - If true and agent is idle, triggers a new LLM turn. Default: false.
|
|
689
|
-
*
|
|
688
|
+
* @param options.triggerTurn - If true and agent is idle, triggers a new LLM turn. Default: false.
|
|
689
|
+
* If agent is streaming, message is queued and triggerTurn is ignored.
|
|
690
|
+
* @param options.deliverAs - How to deliver the message: "steer" or "followUp".
|
|
690
691
|
*/
|
|
691
692
|
sendMessage<T = unknown>(
|
|
692
693
|
message: Pick<HookMessage<T>, "customType" | "content" | "display" | "details">,
|
|
693
|
-
triggerTurn?: boolean,
|
|
694
|
+
options?: { triggerTurn?: boolean; deliverAs?: "steer" | "followUp" },
|
|
694
695
|
): void;
|
|
695
696
|
|
|
696
697
|
/**
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* Based on MCP spec 2025-03-26.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
-
import type { JsonRpcResponse, MCPHttpServerConfig, MCPSseServerConfig, MCPTransport } from "../types";
|
|
8
|
+
import type { JsonRpcMessage, JsonRpcResponse, MCPHttpServerConfig, MCPSseServerConfig, MCPTransport } from "../types";
|
|
9
9
|
|
|
10
10
|
/** Generate unique request ID */
|
|
11
11
|
function generateId(): string {
|
|
@@ -167,39 +167,47 @@ export class HttpTransport implements MCPTransport {
|
|
|
167
167
|
throw new Error("No response body");
|
|
168
168
|
}
|
|
169
169
|
|
|
170
|
-
|
|
170
|
+
const timeout = this.config.timeout ?? 30000;
|
|
171
171
|
|
|
172
|
-
|
|
173
|
-
const
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
const message = JSON.parse(data) as JsonRpcResponse;
|
|
172
|
+
const parse = async (): Promise<T> => {
|
|
173
|
+
for await (const event of readSseEvents(response.body!)) {
|
|
174
|
+
const data = event.data?.trim();
|
|
175
|
+
if (!data || data === "[DONE]") continue;
|
|
177
176
|
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
177
|
+
try {
|
|
178
|
+
const message = JSON.parse(data) as JsonRpcMessage;
|
|
179
|
+
|
|
180
|
+
if (
|
|
181
|
+
"id" in message &&
|
|
182
|
+
(message as JsonRpcResponse).id === expectedId &&
|
|
183
|
+
("result" in message || "error" in message)
|
|
184
|
+
) {
|
|
185
|
+
const response = message as JsonRpcResponse;
|
|
186
|
+
if (response.error) {
|
|
187
|
+
throw new Error(`MCP error ${response.error.code}: ${response.error.message}`);
|
|
188
|
+
}
|
|
189
|
+
return response.result as T;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
if ("method" in message && !("id" in message)) {
|
|
193
|
+
this.onNotification?.(message.method, message.params);
|
|
194
|
+
}
|
|
195
|
+
} catch (error) {
|
|
196
|
+
if (error instanceof Error && error.message.startsWith("MCP error")) {
|
|
197
|
+
throw error;
|
|
182
198
|
}
|
|
183
|
-
result = message.result as T;
|
|
184
|
-
}
|
|
185
|
-
// Handle notifications
|
|
186
|
-
else if ("method" in message && !("id" in message)) {
|
|
187
|
-
const notification = message as { method: string; params?: unknown };
|
|
188
|
-
this.onNotification?.(notification.method, notification.params);
|
|
189
|
-
}
|
|
190
|
-
} catch (error) {
|
|
191
|
-
if (error instanceof Error && error.message.startsWith("MCP error")) {
|
|
192
|
-
throw error;
|
|
193
199
|
}
|
|
194
|
-
// Ignore other parse errors
|
|
195
200
|
}
|
|
196
|
-
}
|
|
197
201
|
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
}
|
|
202
|
+
throw new Error(`No response received for request ID ${expectedId}`);
|
|
203
|
+
};
|
|
201
204
|
|
|
202
|
-
return
|
|
205
|
+
return Promise.race([
|
|
206
|
+
parse(),
|
|
207
|
+
new Promise<never>((_, reject) =>
|
|
208
|
+
setTimeout(() => reject(new Error(`SSE response timeout after ${timeout}ms`)), timeout),
|
|
209
|
+
),
|
|
210
|
+
]);
|
|
203
211
|
}
|
|
204
212
|
|
|
205
213
|
async notify(method: string, params?: Record<string, unknown>): Promise<void> {
|
|
@@ -432,7 +432,7 @@ async function loadTemplatesFromDir(
|
|
|
432
432
|
}
|
|
433
433
|
}
|
|
434
434
|
} catch (error) {
|
|
435
|
-
if (!Bun.file(dir).exists()) {
|
|
435
|
+
if (!(await Bun.file(dir).exists())) {
|
|
436
436
|
return [];
|
|
437
437
|
}
|
|
438
438
|
logger.warn("Failed to scan prompt templates directory", { dir, error: String(error) });
|
|
@@ -116,8 +116,8 @@ const DEFAULT_ENV_DENYLIST = new Set([
|
|
|
116
116
|
const CASE_INSENSITIVE_ENV = process.platform === "win32";
|
|
117
117
|
const ACTIVE_ENV_ALLOWLIST = CASE_INSENSITIVE_ENV ? WINDOWS_ENV_ALLOWLIST : DEFAULT_ENV_ALLOWLIST;
|
|
118
118
|
|
|
119
|
-
const NORMALIZED_ALLOWLIST = new
|
|
120
|
-
Array.from(ACTIVE_ENV_ALLOWLIST, (key) =>
|
|
119
|
+
const NORMALIZED_ALLOWLIST = new Map(
|
|
120
|
+
Array.from(ACTIVE_ENV_ALLOWLIST, (key) => [CASE_INSENSITIVE_ENV ? key.toUpperCase() : key, key] as const),
|
|
121
121
|
);
|
|
122
122
|
const NORMALIZED_DENYLIST = new Set(
|
|
123
123
|
Array.from(DEFAULT_ENV_DENYLIST, (key) => (CASE_INSENSITIVE_ENV ? key.toUpperCase() : key)),
|
|
@@ -168,8 +168,9 @@ function filterEnv(env: Record<string, string | undefined>): Record<string, stri
|
|
|
168
168
|
if (value === undefined) continue;
|
|
169
169
|
const normalizedKey = normalizeEnvKey(key);
|
|
170
170
|
if (NORMALIZED_DENYLIST.has(normalizedKey)) continue;
|
|
171
|
-
|
|
172
|
-
|
|
171
|
+
const canonicalKey = NORMALIZED_ALLOWLIST.get(normalizedKey);
|
|
172
|
+
if (canonicalKey !== undefined) {
|
|
173
|
+
filtered[canonicalKey] = value;
|
|
173
174
|
continue;
|
|
174
175
|
}
|
|
175
176
|
if (NORMALIZED_ALLOW_PREFIXES.some((prefix) => normalizedKey.startsWith(prefix))) {
|
|
@@ -95,7 +95,7 @@ export async function executeSSH(
|
|
|
95
95
|
return {
|
|
96
96
|
exitCode: undefined,
|
|
97
97
|
cancelled: true,
|
|
98
|
-
...sink.dump(`SSH
|
|
98
|
+
...sink.dump(`SSH: ${err.message}`),
|
|
99
99
|
};
|
|
100
100
|
}
|
|
101
101
|
if (err.aborted) {
|
|
@@ -92,17 +92,17 @@ function adjustLinesIndentation(patternLines: string[], actualLines: string[], n
|
|
|
92
92
|
}
|
|
93
93
|
}
|
|
94
94
|
|
|
95
|
-
// Build a map from trimmed content to
|
|
96
|
-
// This
|
|
97
|
-
const
|
|
98
|
-
for (
|
|
99
|
-
const trimmed =
|
|
95
|
+
// Build a map from trimmed content to actual lines (by content, not position)
|
|
96
|
+
// This handles fuzzy matches where pattern and actual may not be positionally aligned
|
|
97
|
+
const contentToActualLines = new Map<string, string[]>();
|
|
98
|
+
for (const line of actualLines) {
|
|
99
|
+
const trimmed = line.trim();
|
|
100
100
|
if (trimmed.length === 0) continue;
|
|
101
|
-
const arr =
|
|
101
|
+
const arr = contentToActualLines.get(trimmed);
|
|
102
102
|
if (arr) {
|
|
103
|
-
arr.push(
|
|
103
|
+
arr.push(line);
|
|
104
104
|
} else {
|
|
105
|
-
|
|
105
|
+
contentToActualLines.set(trimmed, [line]);
|
|
106
106
|
}
|
|
107
107
|
}
|
|
108
108
|
|
|
@@ -119,8 +119,8 @@ function adjustLinesIndentation(patternLines: string[], actualLines: string[], n
|
|
|
119
119
|
}
|
|
120
120
|
const avgDelta = deltaCount > 0 ? Math.round(totalDelta / deltaCount) : 0;
|
|
121
121
|
|
|
122
|
-
// Track which
|
|
123
|
-
const
|
|
122
|
+
// Track which actual lines we've used to handle duplicate content correctly
|
|
123
|
+
const usedActualLines = new Map<string, number>(); // trimmed content -> count used
|
|
124
124
|
|
|
125
125
|
return newLines.map((newLine) => {
|
|
126
126
|
if (newLine.trim().length === 0) {
|
|
@@ -128,16 +128,15 @@ function adjustLinesIndentation(patternLines: string[], actualLines: string[], n
|
|
|
128
128
|
}
|
|
129
129
|
|
|
130
130
|
const trimmed = newLine.trim();
|
|
131
|
-
const
|
|
132
|
-
|
|
133
|
-
// Check if this is a context line (same trimmed content exists in
|
|
134
|
-
if (
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
}
|
|
131
|
+
const matchingActualLines = contentToActualLines.get(trimmed);
|
|
132
|
+
|
|
133
|
+
// Check if this is a context line (same trimmed content exists in actual)
|
|
134
|
+
if (matchingActualLines && matchingActualLines.length > 0) {
|
|
135
|
+
const usedCount = usedActualLines.get(trimmed) ?? 0;
|
|
136
|
+
if (usedCount < matchingActualLines.length) {
|
|
137
|
+
usedActualLines.set(trimmed, usedCount + 1);
|
|
138
|
+
// Use actual file content directly for context lines
|
|
139
|
+
return matchingActualLines[usedCount];
|
|
141
140
|
}
|
|
142
141
|
}
|
|
143
142
|
|
|
@@ -599,9 +598,11 @@ function applyCharacterMatch(
|
|
|
599
598
|
|
|
600
599
|
// Check for multiple exact occurrences
|
|
601
600
|
if (matchOutcome.occurrences && matchOutcome.occurrences > 1) {
|
|
601
|
+
const previews = matchOutcome.occurrencePreviews?.join("\n\n") ?? "";
|
|
602
|
+
const moreMsg = matchOutcome.occurrences > 5 ? ` (showing first 5 of ${matchOutcome.occurrences})` : "";
|
|
602
603
|
throw new ApplyPatchError(
|
|
603
|
-
`Found ${matchOutcome.occurrences} occurrences
|
|
604
|
-
`
|
|
604
|
+
`Found ${matchOutcome.occurrences} occurrences in ${path}${moreMsg}:\n\n${previews}\n\n` +
|
|
605
|
+
`Add more context lines to disambiguate.`,
|
|
605
606
|
);
|
|
606
607
|
}
|
|
607
608
|
|
|
@@ -857,9 +858,22 @@ function computeReplacements(
|
|
|
857
858
|
if (hunk.changeContext === undefined && !hunk.hasContextLines && !hunk.isEndOfFile && lineHint === undefined) {
|
|
858
859
|
const secondMatch = seekSequence(originalLines, pattern, found + 1, false, { allowFuzzy });
|
|
859
860
|
if (secondMatch.index !== undefined) {
|
|
861
|
+
// Extract 3-line previews for each match
|
|
862
|
+
const formatPreview = (startIdx: number) => {
|
|
863
|
+
const lines = originalLines.slice(startIdx, startIdx + 3);
|
|
864
|
+
return lines
|
|
865
|
+
.map((line, i) => {
|
|
866
|
+
const num = startIdx + i + 1;
|
|
867
|
+
const truncated = line.length > 60 ? `${line.slice(0, 57)}...` : line;
|
|
868
|
+
return ` ${num} | ${truncated}`;
|
|
869
|
+
})
|
|
870
|
+
.join("\n");
|
|
871
|
+
};
|
|
872
|
+
const preview1 = formatPreview(found);
|
|
873
|
+
const preview2 = formatPreview(secondMatch.index);
|
|
860
874
|
throw new ApplyPatchError(
|
|
861
|
-
`Found 2 occurrences
|
|
862
|
-
`
|
|
875
|
+
`Found 2 occurrences in ${path}:\n\n${preview1}\n\n${preview2}\n\n` +
|
|
876
|
+
`Add more context lines to disambiguate.`,
|
|
863
877
|
);
|
|
864
878
|
}
|
|
865
879
|
}
|
|
@@ -228,9 +228,11 @@ export function replaceText(content: string, oldText: string, newText: string, o
|
|
|
228
228
|
});
|
|
229
229
|
|
|
230
230
|
if (matchOutcome.occurrences && matchOutcome.occurrences > 1) {
|
|
231
|
+
const previews = matchOutcome.occurrencePreviews?.join("\n\n") ?? "";
|
|
232
|
+
const moreMsg = matchOutcome.occurrences > 5 ? ` (showing first 5 of ${matchOutcome.occurrences})` : "";
|
|
231
233
|
throw new Error(
|
|
232
|
-
`Found ${matchOutcome.occurrences} occurrences
|
|
233
|
-
`
|
|
234
|
+
`Found ${matchOutcome.occurrences} occurrences${moreMsg}:\n\n${previews}\n\n` +
|
|
235
|
+
`Add more context lines to disambiguate.`,
|
|
234
236
|
);
|
|
235
237
|
}
|
|
236
238
|
|
|
@@ -307,8 +309,10 @@ export async function computeEditDiff(
|
|
|
307
309
|
});
|
|
308
310
|
|
|
309
311
|
if (matchOutcome.occurrences && matchOutcome.occurrences > 1) {
|
|
312
|
+
const previews = matchOutcome.occurrencePreviews?.join("\n\n") ?? "";
|
|
313
|
+
const moreMsg = matchOutcome.occurrences > 5 ? ` (showing first 5 of ${matchOutcome.occurrences})` : "";
|
|
310
314
|
return {
|
|
311
|
-
error: `Found ${matchOutcome.occurrences} occurrences
|
|
315
|
+
error: `Found ${matchOutcome.occurrences} occurrences in ${path}${moreMsg}:\n\n${previews}\n\nAdd more context lines to disambiguate.`,
|
|
312
316
|
};
|
|
313
317
|
}
|
|
314
318
|
|