@oh-my-pi/pi-coding-agent 6.2.0 → 6.7.0
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 +46 -0
- package/docs/sdk.md +1 -1
- package/package.json +5 -5
- package/scripts/generate-template.ts +6 -6
- package/src/cli/args.ts +3 -0
- package/src/core/agent-session.ts +39 -0
- package/src/core/bash-executor.ts +3 -3
- package/src/core/cursor/exec-bridge.ts +95 -88
- package/src/core/custom-commands/bundled/review/index.ts +142 -145
- package/src/core/custom-commands/bundled/wt/index.ts +68 -66
- package/src/core/custom-commands/loader.ts +4 -6
- package/src/core/custom-tools/index.ts +2 -2
- package/src/core/custom-tools/loader.ts +66 -61
- package/src/core/custom-tools/types.ts +4 -4
- package/src/core/custom-tools/wrapper.ts +61 -25
- package/src/core/event-bus.ts +19 -47
- package/src/core/extensions/index.ts +8 -4
- package/src/core/extensions/loader.ts +160 -120
- package/src/core/extensions/types.ts +4 -4
- package/src/core/extensions/wrapper.ts +149 -100
- package/src/core/hooks/index.ts +1 -1
- package/src/core/hooks/tool-wrapper.ts +96 -70
- package/src/core/hooks/types.ts +1 -2
- package/src/core/index.ts +1 -0
- package/src/core/mcp/index.ts +6 -2
- package/src/core/mcp/json-rpc.ts +88 -0
- package/src/core/mcp/loader.ts +22 -4
- package/src/core/mcp/manager.ts +202 -48
- package/src/core/mcp/tool-bridge.ts +143 -55
- package/src/core/mcp/tool-cache.ts +122 -0
- package/src/core/python-executor.ts +3 -9
- package/src/core/sdk.ts +33 -32
- package/src/core/session-manager.ts +30 -0
- package/src/core/settings-manager.ts +34 -1
- package/src/core/ssh/ssh-executor.ts +6 -84
- package/src/core/streaming-output.ts +107 -53
- package/src/core/tools/ask.ts +92 -93
- package/src/core/tools/bash.ts +103 -94
- package/src/core/tools/calculator.ts +41 -26
- package/src/core/tools/complete.ts +76 -66
- package/src/core/tools/context.ts +22 -24
- package/src/core/tools/exa/index.ts +1 -1
- package/src/core/tools/exa/mcp-client.ts +56 -101
- package/src/core/tools/find.ts +250 -253
- package/src/core/tools/git.ts +39 -33
- package/src/core/tools/grep.ts +440 -427
- package/src/core/tools/index.ts +62 -61
- package/src/core/tools/ls.ts +119 -114
- package/src/core/tools/lsp/clients/biome-client.ts +5 -7
- package/src/core/tools/lsp/clients/index.ts +4 -4
- package/src/core/tools/lsp/clients/lsp-linter-client.ts +5 -7
- package/src/core/tools/lsp/config.ts +2 -2
- package/src/core/tools/lsp/index.ts +604 -578
- package/src/core/tools/notebook.ts +121 -119
- package/src/core/tools/output.ts +163 -147
- package/src/core/tools/patch/applicator.ts +1100 -0
- package/src/core/tools/patch/diff.ts +362 -0
- package/src/core/tools/patch/fuzzy.ts +647 -0
- package/src/core/tools/patch/index.ts +430 -0
- package/src/core/tools/patch/normalize.ts +220 -0
- package/src/core/tools/patch/normative.ts +49 -0
- package/src/core/tools/patch/parser.ts +528 -0
- package/src/core/tools/patch/shared.ts +228 -0
- package/src/core/tools/patch/types.ts +244 -0
- package/src/core/tools/python.ts +139 -136
- package/src/core/tools/read.ts +237 -216
- package/src/core/tools/render-utils.ts +196 -77
- package/src/core/tools/renderers.ts +1 -1
- package/src/core/tools/ssh.ts +99 -80
- package/src/core/tools/task/executor.ts +11 -7
- package/src/core/tools/task/index.ts +352 -343
- package/src/core/tools/task/worker.ts +13 -23
- package/src/core/tools/todo-write.ts +74 -59
- package/src/core/tools/web-fetch.ts +54 -47
- package/src/core/tools/web-search/index.ts +27 -16
- package/src/core/tools/write.ts +73 -44
- package/src/core/ttsr.ts +106 -152
- package/src/core/voice.ts +49 -39
- package/src/index.ts +16 -12
- package/src/lib/worktree/index.ts +1 -9
- package/src/modes/interactive/components/diff.ts +15 -8
- package/src/modes/interactive/components/settings-defs.ts +24 -0
- package/src/modes/interactive/components/tool-execution.ts +34 -6
- package/src/modes/interactive/controllers/event-controller.ts +6 -19
- package/src/modes/interactive/controllers/input-controller.ts +1 -1
- package/src/modes/interactive/utils/ui-helpers.ts +5 -1
- package/src/modes/rpc/rpc-mode.ts +99 -81
- package/src/prompts/tools/patch.md +76 -0
- package/src/prompts/tools/read.md +1 -1
- package/src/prompts/tools/{edit.md → replace.md} +1 -0
- package/src/utils/shell.ts +0 -40
- package/src/core/tools/edit-diff.ts +0 -574
- package/src/core/tools/edit.ts +0 -345
|
@@ -231,67 +231,104 @@ export interface ToolUITitleOptions {
|
|
|
231
231
|
bold?: boolean;
|
|
232
232
|
}
|
|
233
233
|
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
234
|
+
// =============================================================================
|
|
235
|
+
// Diagnostic Formatting
|
|
236
|
+
// =============================================================================
|
|
237
|
+
|
|
238
|
+
export class ToolUIKit {
|
|
239
|
+
constructor(public theme: Theme) {}
|
|
240
|
+
|
|
241
|
+
title(label: string, options?: ToolUITitleOptions): string {
|
|
242
|
+
const content = options?.bold === false ? label : this.theme.bold(label);
|
|
243
|
+
return this.theme.fg("toolTitle", content);
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
meta(meta: string[]): string {
|
|
247
|
+
return formatMeta(meta, this.theme);
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
count(label: string, count: number): string {
|
|
251
|
+
return formatCount(label, count);
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
moreItems(remaining: number, itemType: string): string {
|
|
255
|
+
return formatMoreItems(remaining, itemType, this.theme);
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
expandHint(expanded: boolean, hasMore: boolean): string {
|
|
259
|
+
return formatExpandHint(this.theme, expanded, hasMore);
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
scope(scopePath?: string): string {
|
|
263
|
+
return formatScope(scopePath, this.theme);
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
truncationSuffix(truncated: boolean): string {
|
|
267
|
+
return formatTruncationSuffix(truncated, this.theme);
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
errorMessage(message: string | undefined): string {
|
|
271
|
+
return formatErrorMessage(message, this.theme);
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
emptyMessage(message: string): string {
|
|
275
|
+
return formatEmptyMessage(message, this.theme);
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
badge(label: string, color: ToolUIColor): string {
|
|
279
|
+
return formatBadge(label, color, this.theme);
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
statusIcon(status: ToolUIStatus, spinnerFrame?: number): string {
|
|
283
|
+
return formatStatusIcon(status, this.theme, spinnerFrame);
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
wrapBrackets(text: string): string {
|
|
287
|
+
return wrapBrackets(text, this.theme);
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
truncate(text: string, maxLen: number): string {
|
|
291
|
+
return truncate(text, maxLen, this.theme.format.ellipsis);
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
previewLines(text: string, maxLines: number, maxLineLen: number): string[] {
|
|
295
|
+
return getPreviewLines(text, maxLines, maxLineLen, this.theme.format.ellipsis);
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
formatBytes(bytes: number): string {
|
|
299
|
+
return formatBytes(bytes);
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
formatTokens(tokens: number): string {
|
|
303
|
+
return formatTokens(tokens);
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
formatDuration(ms: number): string {
|
|
307
|
+
return formatDuration(ms);
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
formatAge(ageSeconds: number | null | undefined): string {
|
|
311
|
+
return formatAge(ageSeconds);
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
formatDiagnostics(
|
|
255
315
|
diag: { errored: boolean; summary: string; messages: string[] },
|
|
256
316
|
expanded: boolean,
|
|
257
317
|
getLangIcon: (filePath: string) => string,
|
|
258
|
-
)
|
|
259
|
-
|
|
318
|
+
): string {
|
|
319
|
+
return formatDiagnostics(diag, expanded, this.theme, getLangIcon);
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
formatDiffStats(added: number, removed: number, hunks: number): string {
|
|
323
|
+
return formatDiffStats(added, removed, hunks, this.theme);
|
|
324
|
+
}
|
|
260
325
|
}
|
|
261
326
|
|
|
327
|
+
/** @deprecated Use `new ToolUIKit(theme)` instead */
|
|
262
328
|
export function createToolUIKit(theme: Theme): ToolUIKit {
|
|
263
|
-
return
|
|
264
|
-
theme,
|
|
265
|
-
title: (label, options) => {
|
|
266
|
-
const content = options?.bold === false ? label : theme.bold(label);
|
|
267
|
-
return theme.fg("toolTitle", content);
|
|
268
|
-
},
|
|
269
|
-
meta: (meta) => formatMeta(meta, theme),
|
|
270
|
-
count: (label, count) => formatCount(label, count),
|
|
271
|
-
moreItems: (remaining, itemType) => formatMoreItems(remaining, itemType, theme),
|
|
272
|
-
expandHint: (expanded, hasMore) => formatExpandHint(theme, expanded, hasMore),
|
|
273
|
-
scope: (scopePath) => formatScope(scopePath, theme),
|
|
274
|
-
truncationSuffix: (truncated) => formatTruncationSuffix(truncated, theme),
|
|
275
|
-
errorMessage: (message) => formatErrorMessage(message, theme),
|
|
276
|
-
emptyMessage: (message) => formatEmptyMessage(message, theme),
|
|
277
|
-
badge: (label, color) => formatBadge(label, color, theme),
|
|
278
|
-
statusIcon: (status, spinnerFrame) => formatStatusIcon(status, theme, spinnerFrame),
|
|
279
|
-
wrapBrackets: (text) => wrapBrackets(text, theme),
|
|
280
|
-
truncate: (text, maxLen) => truncate(text, maxLen, theme.format.ellipsis),
|
|
281
|
-
previewLines: (text, maxLines, maxLineLen) => getPreviewLines(text, maxLines, maxLineLen, theme.format.ellipsis),
|
|
282
|
-
formatBytes,
|
|
283
|
-
formatTokens,
|
|
284
|
-
formatDuration,
|
|
285
|
-
formatAge,
|
|
286
|
-
formatDiagnostics: (diag, expanded, getLangIcon) => formatDiagnostics(diag, expanded, theme, getLangIcon),
|
|
287
|
-
formatDiffStats: (added, removed, hunks) => formatDiffStats(added, removed, hunks, theme),
|
|
288
|
-
};
|
|
329
|
+
return new ToolUIKit(theme);
|
|
289
330
|
}
|
|
290
331
|
|
|
291
|
-
// =============================================================================
|
|
292
|
-
// Diagnostic Formatting
|
|
293
|
-
// =============================================================================
|
|
294
|
-
|
|
295
332
|
interface ParsedDiagnostic {
|
|
296
333
|
filePath: string;
|
|
297
334
|
line: number;
|
|
@@ -477,6 +514,36 @@ export function formatDiffStats(added: number, removed: number, hunks: number, t
|
|
|
477
514
|
return parts.join(theme.fg("dim", " / "));
|
|
478
515
|
}
|
|
479
516
|
|
|
517
|
+
interface DiffSegment {
|
|
518
|
+
lines: string[];
|
|
519
|
+
isChange: boolean;
|
|
520
|
+
isEllipsis: boolean;
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
function parseDiffSegments(lines: string[]): DiffSegment[] {
|
|
524
|
+
const segments: DiffSegment[] = [];
|
|
525
|
+
let current: DiffSegment | null = null;
|
|
526
|
+
|
|
527
|
+
for (const line of lines) {
|
|
528
|
+
const isChange = line.startsWith("+") || line.startsWith("-");
|
|
529
|
+
const isEllipsis = line.trimStart().startsWith("...");
|
|
530
|
+
|
|
531
|
+
if (isEllipsis) {
|
|
532
|
+
if (current) segments.push(current);
|
|
533
|
+
segments.push({ lines: [line], isChange: false, isEllipsis: true });
|
|
534
|
+
current = null;
|
|
535
|
+
} else if (!current || current.isChange !== isChange) {
|
|
536
|
+
if (current) segments.push(current);
|
|
537
|
+
current = { lines: [line], isChange, isEllipsis: false };
|
|
538
|
+
} else {
|
|
539
|
+
current.lines.push(line);
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
if (current) segments.push(current);
|
|
544
|
+
return segments;
|
|
545
|
+
}
|
|
546
|
+
|
|
480
547
|
export function truncateDiffByHunk(
|
|
481
548
|
diffText: string,
|
|
482
549
|
maxHunks: number,
|
|
@@ -484,42 +551,94 @@ export function truncateDiffByHunk(
|
|
|
484
551
|
): { text: string; hiddenHunks: number; hiddenLines: number } {
|
|
485
552
|
const lines = diffText ? diffText.split("\n") : [];
|
|
486
553
|
const totalStats = getDiffStats(diffText);
|
|
487
|
-
const kept: string[] = [];
|
|
488
|
-
let inHunk = false;
|
|
489
|
-
let currentHunks = 0;
|
|
490
|
-
let reachedLimit = false;
|
|
491
554
|
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
currentHunks++;
|
|
496
|
-
inHunk = true;
|
|
497
|
-
}
|
|
498
|
-
if (!isChange) {
|
|
499
|
-
inHunk = false;
|
|
500
|
-
}
|
|
555
|
+
if (lines.length <= maxLines && totalStats.hunks <= maxHunks) {
|
|
556
|
+
return { text: diffText, hiddenHunks: 0, hiddenLines: 0 };
|
|
557
|
+
}
|
|
501
558
|
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
559
|
+
const segments = parseDiffSegments(lines);
|
|
560
|
+
|
|
561
|
+
const changeSegments = segments.filter((s) => s.isChange);
|
|
562
|
+
const changeLineCount = changeSegments.reduce((sum, s) => sum + s.lines.length, 0);
|
|
563
|
+
|
|
564
|
+
if (changeLineCount > maxLines) {
|
|
565
|
+
const kept: string[] = [];
|
|
566
|
+
let keptHunks = 0;
|
|
506
567
|
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
568
|
+
for (const seg of segments) {
|
|
569
|
+
if (seg.isChange) {
|
|
570
|
+
keptHunks++;
|
|
571
|
+
if (keptHunks > maxHunks) break;
|
|
572
|
+
}
|
|
573
|
+
kept.push(...seg.lines);
|
|
574
|
+
if (kept.length >= maxLines) break;
|
|
511
575
|
}
|
|
576
|
+
|
|
577
|
+
const keptStats = getDiffStats(kept.join("\n"));
|
|
578
|
+
return {
|
|
579
|
+
text: kept.join("\n"),
|
|
580
|
+
hiddenHunks: Math.max(0, totalStats.hunks - keptStats.hunks),
|
|
581
|
+
hiddenLines: Math.max(0, lines.length - kept.length),
|
|
582
|
+
};
|
|
512
583
|
}
|
|
513
584
|
|
|
514
|
-
|
|
515
|
-
|
|
585
|
+
const contextBudget = maxLines - changeLineCount;
|
|
586
|
+
const contextSegments = segments.filter((s) => !s.isChange && !s.isEllipsis);
|
|
587
|
+
const totalContextLines = contextSegments.reduce((sum, s) => sum + s.lines.length, 0);
|
|
588
|
+
|
|
589
|
+
const kept: string[] = [];
|
|
590
|
+
let keptHunks = 0;
|
|
591
|
+
|
|
592
|
+
if (totalContextLines <= contextBudget) {
|
|
593
|
+
for (const seg of segments) {
|
|
594
|
+
if (seg.isChange) {
|
|
595
|
+
keptHunks++;
|
|
596
|
+
if (keptHunks > maxHunks) break;
|
|
597
|
+
}
|
|
598
|
+
kept.push(...seg.lines);
|
|
599
|
+
}
|
|
600
|
+
} else {
|
|
601
|
+
const contextRatio = contextSegments.length > 0 ? contextBudget / totalContextLines : 0;
|
|
602
|
+
|
|
603
|
+
for (let i = 0; i < segments.length; i++) {
|
|
604
|
+
const seg = segments[i];
|
|
605
|
+
|
|
606
|
+
if (seg.isChange) {
|
|
607
|
+
keptHunks++;
|
|
608
|
+
if (keptHunks > maxHunks) break;
|
|
609
|
+
kept.push(...seg.lines);
|
|
610
|
+
} else if (seg.isEllipsis) {
|
|
611
|
+
kept.push(...seg.lines);
|
|
612
|
+
} else {
|
|
613
|
+
const allowedLines = Math.max(1, Math.floor(seg.lines.length * contextRatio));
|
|
614
|
+
const isBeforeChange = segments[i + 1]?.isChange;
|
|
615
|
+
const isAfterChange = segments[i - 1]?.isChange;
|
|
616
|
+
|
|
617
|
+
if (isBeforeChange && isAfterChange) {
|
|
618
|
+
const half = Math.ceil(allowedLines / 2);
|
|
619
|
+
if (seg.lines.length > allowedLines) {
|
|
620
|
+
kept.push(...seg.lines.slice(0, half));
|
|
621
|
+
kept.push(seg.lines[0].replace(/^(\s*\d*\s*).*/, "$1..."));
|
|
622
|
+
kept.push(...seg.lines.slice(-half));
|
|
623
|
+
} else {
|
|
624
|
+
kept.push(...seg.lines);
|
|
625
|
+
}
|
|
626
|
+
} else if (isBeforeChange) {
|
|
627
|
+
kept.push(...seg.lines.slice(-allowedLines));
|
|
628
|
+
} else if (isAfterChange) {
|
|
629
|
+
kept.push(...seg.lines.slice(0, allowedLines));
|
|
630
|
+
} else {
|
|
631
|
+
kept.push(...seg.lines.slice(0, Math.min(allowedLines, 2)));
|
|
632
|
+
}
|
|
633
|
+
}
|
|
634
|
+
}
|
|
516
635
|
}
|
|
517
636
|
|
|
518
637
|
const keptStats = getDiffStats(kept.join("\n"));
|
|
519
638
|
return {
|
|
520
639
|
text: kept.join("\n"),
|
|
521
640
|
hiddenHunks: Math.max(0, totalStats.hunks - keptStats.hunks),
|
|
522
|
-
hiddenLines: Math.max(0,
|
|
641
|
+
hiddenLines: Math.max(0, lines.length - kept.length),
|
|
523
642
|
};
|
|
524
643
|
}
|
|
525
644
|
|
|
@@ -10,13 +10,13 @@ import type { RenderResultOptions } from "../custom-tools/types";
|
|
|
10
10
|
import { askToolRenderer } from "./ask";
|
|
11
11
|
import { bashToolRenderer } from "./bash";
|
|
12
12
|
import { calculatorToolRenderer } from "./calculator";
|
|
13
|
-
import { editToolRenderer } from "./edit";
|
|
14
13
|
import { findToolRenderer } from "./find";
|
|
15
14
|
import { grepToolRenderer } from "./grep";
|
|
16
15
|
import { lsToolRenderer } from "./ls";
|
|
17
16
|
import { lspToolRenderer } from "./lsp/render";
|
|
18
17
|
import { notebookToolRenderer } from "./notebook";
|
|
19
18
|
import { outputToolRenderer } from "./output";
|
|
19
|
+
import { editToolRenderer } from "./patch";
|
|
20
20
|
import { pythonToolRenderer } from "./python";
|
|
21
21
|
import { readToolRenderer } from "./read";
|
|
22
22
|
import { sshToolRenderer } from "./ssh";
|
package/src/core/tools/ssh.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { AgentTool, AgentToolContext } from "@oh-my-pi/pi-agent-core";
|
|
1
|
+
import type { AgentTool, AgentToolContext, AgentToolResult, AgentToolUpdateCallback } from "@oh-my-pi/pi-agent-core";
|
|
2
2
|
import type { Component } from "@oh-my-pi/pi-tui";
|
|
3
3
|
import { Text } from "@oh-my-pi/pi-tui";
|
|
4
4
|
import { Type } from "@sinclair/typebox";
|
|
@@ -112,97 +112,116 @@ async function loadHosts(session: ToolSession): Promise<{
|
|
|
112
112
|
return { hostNames, hostsByName };
|
|
113
113
|
}
|
|
114
114
|
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
115
|
+
interface SshToolParams {
|
|
116
|
+
host: string;
|
|
117
|
+
command: string;
|
|
118
|
+
cwd?: string;
|
|
119
|
+
timeout?: number;
|
|
120
|
+
}
|
|
120
121
|
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
return {
|
|
128
|
-
name: "ssh",
|
|
129
|
-
label: "SSH",
|
|
130
|
-
description: formatDescription(descriptionHosts),
|
|
131
|
-
parameters: sshSchema,
|
|
132
|
-
execute: async (
|
|
133
|
-
_toolCallId: string,
|
|
134
|
-
{ host, command, cwd, timeout }: { host: string; command: string; cwd?: string; timeout?: number },
|
|
135
|
-
signal?: AbortSignal,
|
|
136
|
-
onUpdate?,
|
|
137
|
-
_ctx?: AgentToolContext,
|
|
138
|
-
) => {
|
|
139
|
-
if (!allowedHosts.has(host)) {
|
|
140
|
-
throw new Error(`Unknown SSH host: ${host}. Available hosts: ${hostNames.join(", ")}`);
|
|
141
|
-
}
|
|
122
|
+
export class SshTool implements AgentTool<typeof sshSchema, SSHToolDetails> {
|
|
123
|
+
public readonly name = "ssh";
|
|
124
|
+
public readonly label = "SSH";
|
|
125
|
+
public readonly description: string;
|
|
126
|
+
public readonly parameters = sshSchema;
|
|
142
127
|
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
}
|
|
128
|
+
private readonly allowedHosts: Set<string>;
|
|
129
|
+
private readonly hostsByName: Map<string, SSHHost>;
|
|
130
|
+
private readonly hostNames: string[];
|
|
147
131
|
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
const result = await executeSSH(hostConfig, remoteCommand, {
|
|
153
|
-
timeout: timeout ? timeout * 1000 : undefined,
|
|
154
|
-
signal,
|
|
155
|
-
compatEnabled: hostInfo.compatEnabled,
|
|
156
|
-
onChunk: (chunk) => {
|
|
157
|
-
currentOutput += chunk;
|
|
158
|
-
if (onUpdate) {
|
|
159
|
-
const truncation = truncateTail(currentOutput);
|
|
160
|
-
onUpdate({
|
|
161
|
-
content: [{ type: "text", text: truncation.content || "" }],
|
|
162
|
-
details: {
|
|
163
|
-
truncation: truncation.truncated ? truncation : undefined,
|
|
164
|
-
},
|
|
165
|
-
});
|
|
166
|
-
}
|
|
167
|
-
},
|
|
168
|
-
});
|
|
169
|
-
|
|
170
|
-
if (result.cancelled) {
|
|
171
|
-
throw new Error(result.output || "Command aborted");
|
|
172
|
-
}
|
|
132
|
+
constructor(hostNames: string[], hostsByName: Map<string, SSHHost>) {
|
|
133
|
+
this.hostNames = hostNames;
|
|
134
|
+
this.hostsByName = hostsByName;
|
|
135
|
+
this.allowedHosts = new Set(hostNames);
|
|
173
136
|
|
|
174
|
-
|
|
175
|
-
|
|
137
|
+
const descriptionHosts = hostNames
|
|
138
|
+
.map((name) => hostsByName.get(name))
|
|
139
|
+
.filter((host): host is SSHHost => host !== undefined);
|
|
176
140
|
|
|
177
|
-
|
|
141
|
+
this.description = formatDescription(descriptionHosts);
|
|
142
|
+
}
|
|
178
143
|
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
144
|
+
public async execute(
|
|
145
|
+
_toolCallId: string,
|
|
146
|
+
{ host, command, cwd, timeout }: SshToolParams,
|
|
147
|
+
signal?: AbortSignal,
|
|
148
|
+
onUpdate?: AgentToolUpdateCallback<SSHToolDetails>,
|
|
149
|
+
_ctx?: AgentToolContext,
|
|
150
|
+
): Promise<AgentToolResult<SSHToolDetails>> {
|
|
151
|
+
if (!this.allowedHosts.has(host)) {
|
|
152
|
+
throw new Error(`Unknown SSH host: ${host}. Available hosts: ${this.hostNames.join(", ")}`);
|
|
153
|
+
}
|
|
184
154
|
|
|
185
|
-
|
|
186
|
-
|
|
155
|
+
const hostConfig = this.hostsByName.get(host);
|
|
156
|
+
if (!hostConfig) {
|
|
157
|
+
throw new Error(`SSH host not loaded: ${host}`);
|
|
158
|
+
}
|
|
187
159
|
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
160
|
+
const hostInfo = await ensureHostInfo(hostConfig);
|
|
161
|
+
const remoteCommand = buildRemoteCommand(command, cwd, hostInfo);
|
|
162
|
+
let currentOutput = "";
|
|
163
|
+
|
|
164
|
+
const result = await executeSSH(hostConfig, remoteCommand, {
|
|
165
|
+
timeout: timeout ? timeout * 1000 : undefined,
|
|
166
|
+
signal,
|
|
167
|
+
compatEnabled: hostInfo.compatEnabled,
|
|
168
|
+
onChunk: (chunk) => {
|
|
169
|
+
currentOutput += chunk;
|
|
170
|
+
if (onUpdate) {
|
|
171
|
+
const truncation = truncateTail(currentOutput);
|
|
172
|
+
onUpdate({
|
|
173
|
+
content: [{ type: "text", text: truncation.content || "" }],
|
|
174
|
+
details: {
|
|
175
|
+
truncation: truncation.truncated ? truncation : undefined,
|
|
176
|
+
},
|
|
177
|
+
});
|
|
195
178
|
}
|
|
196
|
-
}
|
|
179
|
+
},
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
if (result.cancelled) {
|
|
183
|
+
throw new Error(result.output || "Command aborted");
|
|
184
|
+
}
|
|
197
185
|
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
186
|
+
const truncation = truncateTail(result.output);
|
|
187
|
+
let outputText = truncation.content || "(no output)";
|
|
188
|
+
|
|
189
|
+
let details: SSHToolDetails | undefined;
|
|
190
|
+
|
|
191
|
+
if (truncation.truncated) {
|
|
192
|
+
details = {
|
|
193
|
+
truncation,
|
|
194
|
+
fullOutputPath: result.fullOutputPath,
|
|
195
|
+
};
|
|
196
|
+
|
|
197
|
+
const startLine = truncation.totalLines - truncation.outputLines + 1;
|
|
198
|
+
const endLine = truncation.totalLines;
|
|
199
|
+
|
|
200
|
+
if (truncation.lastLinePartial) {
|
|
201
|
+
const lastLineSize = formatSize(Buffer.byteLength(result.output.split("\n").pop() || "", "utf-8"));
|
|
202
|
+
outputText += `\n\n[Showing last ${formatSize(truncation.outputBytes)} of line ${endLine} (line is ${lastLineSize}). Full output: ${result.fullOutputPath}]`;
|
|
203
|
+
} else if (truncation.truncatedBy === "lines") {
|
|
204
|
+
outputText += `\n\n[Showing lines ${startLine}-${endLine} of ${truncation.totalLines}. Full output: ${result.fullOutputPath}]`;
|
|
205
|
+
} else {
|
|
206
|
+
outputText += `\n\n[Showing lines ${startLine}-${endLine} of ${truncation.totalLines} (${formatSize(DEFAULT_MAX_BYTES)} limit). Full output: ${result.fullOutputPath}]`;
|
|
201
207
|
}
|
|
208
|
+
}
|
|
202
209
|
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
210
|
+
if (result.exitCode !== 0 && result.exitCode !== undefined) {
|
|
211
|
+
outputText += `\n\nCommand exited with code ${result.exitCode}`;
|
|
212
|
+
throw new Error(outputText);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
return { content: [{ type: "text", text: outputText }], details: details ?? {} };
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
export async function loadSshTool(session: ToolSession): Promise<SshTool | null> {
|
|
220
|
+
const { hostNames, hostsByName } = await loadHosts(session);
|
|
221
|
+
if (hostNames.length === 0) {
|
|
222
|
+
return null;
|
|
223
|
+
}
|
|
224
|
+
return new SshTool(hostNames, hostsByName);
|
|
206
225
|
}
|
|
207
226
|
|
|
208
227
|
// =============================================================================
|
|
@@ -12,9 +12,9 @@ import type { MCPManager } from "../../mcp/manager";
|
|
|
12
12
|
import type { ModelRegistry } from "../../model-registry";
|
|
13
13
|
import { checkPythonKernelAvailability } from "../../python-kernel";
|
|
14
14
|
import type { ToolSession } from "..";
|
|
15
|
-
import {
|
|
15
|
+
import { LspTool } from "../lsp/index";
|
|
16
16
|
import type { LspParams } from "../lsp/types";
|
|
17
|
-
import {
|
|
17
|
+
import { PythonTool } from "../python";
|
|
18
18
|
import { ensureArtifactsDir, getArtifactPaths } from "./artifacts";
|
|
19
19
|
import { resolveModelPattern } from "./model-resolver";
|
|
20
20
|
import { subprocessToolRegistry } from "./subprocess-tool-registry";
|
|
@@ -388,7 +388,7 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
|
|
|
388
388
|
settings: options.settingsManager as ToolSession["settings"],
|
|
389
389
|
settingsManager: options.settingsManager,
|
|
390
390
|
};
|
|
391
|
-
const pythonTool = pythonProxyEnabled ?
|
|
391
|
+
const pythonTool = pythonProxyEnabled ? new PythonTool(pythonToolSession) : null;
|
|
392
392
|
const pythonCallControllers = new Map<string, AbortController>();
|
|
393
393
|
|
|
394
394
|
const lspToolSession: ToolSession = {
|
|
@@ -400,7 +400,7 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
|
|
|
400
400
|
settings: options.settingsManager as ToolSession["settings"],
|
|
401
401
|
settingsManager: options.settingsManager,
|
|
402
402
|
};
|
|
403
|
-
const lspTool = lspToolRequested ?
|
|
403
|
+
const lspTool = lspToolRequested ? new LspTool(lspToolSession) : null;
|
|
404
404
|
|
|
405
405
|
// Accumulate usage incrementally from message_end events (no memory for streaming events)
|
|
406
406
|
const accumulatedUsage = {
|
|
@@ -736,9 +736,13 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
|
|
|
736
736
|
try {
|
|
737
737
|
const parsed = parseMCPToolName(request.toolName);
|
|
738
738
|
if (!parsed) throw new Error(`Invalid MCP tool name: ${request.toolName}`);
|
|
739
|
-
const
|
|
740
|
-
|
|
741
|
-
|
|
739
|
+
const result = await withTimeout(
|
|
740
|
+
(async () => {
|
|
741
|
+
const connection = await mcpManager.waitForConnection(parsed.serverName);
|
|
742
|
+
return callTool(connection, parsed.toolName, request.params);
|
|
743
|
+
})(),
|
|
744
|
+
request.timeoutMs,
|
|
745
|
+
);
|
|
742
746
|
worker.postMessage({
|
|
743
747
|
type: "mcp_tool_result",
|
|
744
748
|
callId: request.callId,
|