@cortexkit/aft-opencode 0.36.0 → 0.37.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/dist/bg-notifications.d.ts +5 -1
- package/dist/bg-notifications.d.ts.map +1 -1
- package/dist/index.d.ts +0 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1594 -1159
- package/dist/shared/rpc-client.d.ts +13 -0
- package/dist/shared/rpc-client.d.ts.map +1 -1
- package/dist/shared/rpc-server.d.ts +7 -0
- package/dist/shared/rpc-server.d.ts.map +1 -1
- package/dist/shared/rpc-utils.d.ts +23 -0
- package/dist/shared/rpc-utils.d.ts.map +1 -1
- package/dist/shared/session-directory.d.ts +1 -0
- package/dist/shared/session-directory.d.ts.map +1 -1
- package/dist/status-bar-inject.d.ts +1 -1
- package/dist/status-bar-inject.d.ts.map +1 -1
- package/dist/tool-perf.d.ts +17 -0
- package/dist/tool-perf.d.ts.map +1 -0
- package/dist/tools/_shared.d.ts +1 -11
- package/dist/tools/_shared.d.ts.map +1 -1
- package/dist/tools/bash.d.ts +21 -1
- package/dist/tools/bash.d.ts.map +1 -1
- package/dist/tools/conflicts.d.ts.map +1 -1
- package/dist/tools/hoisted.d.ts.map +1 -1
- package/dist/tools/reading.d.ts.map +1 -1
- package/dist/tools/refactoring.d.ts.map +1 -1
- package/dist/tools/safety.d.ts.map +1 -1
- package/dist/tools/semantic.d.ts.map +1 -1
- package/dist/tui.js +118 -95
- package/dist/workflow-hints.d.ts.map +1 -1
- package/package.json +8 -8
- package/src/shared/rpc-client.ts +79 -26
- package/src/shared/rpc-server.ts +64 -6
- package/src/shared/rpc-utils.ts +62 -0
- package/src/shared/session-directory.ts +56 -0
- package/src/tui/index.tsx +26 -2
- package/src/tui/sidebar.tsx +64 -1
- package/dist/metadata-store.d.ts +0 -29
- package/dist/metadata-store.d.ts.map +0 -1
- package/dist/tools/structure.d.ts +0 -8
- package/dist/tools/structure.d.ts.map +0 -1
package/dist/index.js
CHANGED
|
@@ -11597,6 +11597,9 @@ function error(message, meta) {
|
|
|
11597
11597
|
var CONFLICT_HINT = `
|
|
11598
11598
|
|
|
11599
11599
|
[Hint] Use aft_conflicts to see all conflict regions across files in a single call.`;
|
|
11600
|
+
var GREP_SEARCH_AFT_SEARCH_HINT = "DO NOT search code by running grep/rg in bash — it is unindexed, unranked, and serial. Use the `aft_search` tool instead (it auto-routes concepts, identifiers, regex, and literals).";
|
|
11601
|
+
var GREP_SEARCH_GREP_HINT = "DO NOT search code by running grep/rg in bash — it is unindexed, unranked, and serial. Use the `grep` tool instead (indexed and ranked).";
|
|
11602
|
+
var GREP_SEARCH_HINT_PREFIX = "DO NOT search code by running grep/rg in bash —";
|
|
11600
11603
|
function maybeAppendConflictsHint(output) {
|
|
11601
11604
|
if (!output.includes("Automatic merge failed; fix conflicts"))
|
|
11602
11605
|
return output;
|
|
@@ -11604,6 +11607,146 @@ function maybeAppendConflictsHint(output) {
|
|
|
11604
11607
|
return output;
|
|
11605
11608
|
return output + CONFLICT_HINT;
|
|
11606
11609
|
}
|
|
11610
|
+
function commandLeadsWithCodeSearch(command) {
|
|
11611
|
+
const trimmed = command.trim();
|
|
11612
|
+
if (!trimmed)
|
|
11613
|
+
return false;
|
|
11614
|
+
const afterCd = peelLeadingCdAnd(trimmed);
|
|
11615
|
+
if (afterCd === null)
|
|
11616
|
+
return false;
|
|
11617
|
+
const firstStage = firstPipelineStage(afterCd);
|
|
11618
|
+
if (firstStage === null)
|
|
11619
|
+
return false;
|
|
11620
|
+
const firstToken = readShellToken(firstStage, skipSpaces(firstStage, 0));
|
|
11621
|
+
if (firstToken === null)
|
|
11622
|
+
return false;
|
|
11623
|
+
return firstToken.token === "grep" || firstToken.token === "rg";
|
|
11624
|
+
}
|
|
11625
|
+
function maybeAppendGrepSearchHint(output, command, aftSearchRegistered) {
|
|
11626
|
+
if (output === "")
|
|
11627
|
+
return output;
|
|
11628
|
+
if (!commandLeadsWithCodeSearch(command))
|
|
11629
|
+
return output;
|
|
11630
|
+
if (output.includes(GREP_SEARCH_HINT_PREFIX))
|
|
11631
|
+
return output;
|
|
11632
|
+
const hint = aftSearchRegistered ? GREP_SEARCH_AFT_SEARCH_HINT : GREP_SEARCH_GREP_HINT;
|
|
11633
|
+
return `${output}
|
|
11634
|
+
|
|
11635
|
+
${hint}`;
|
|
11636
|
+
}
|
|
11637
|
+
function peelLeadingCdAnd(command) {
|
|
11638
|
+
const first = readShellToken(command, skipSpaces(command, 0));
|
|
11639
|
+
if (first === null)
|
|
11640
|
+
return null;
|
|
11641
|
+
if (first.token !== "cd")
|
|
11642
|
+
return command;
|
|
11643
|
+
const dir = readShellToken(command, skipSpaces(command, first.end));
|
|
11644
|
+
if (dir === null)
|
|
11645
|
+
return null;
|
|
11646
|
+
if (!dir.token)
|
|
11647
|
+
return command;
|
|
11648
|
+
const afterDir = skipSpaces(command, dir.end);
|
|
11649
|
+
if (!command.startsWith("&&", afterDir))
|
|
11650
|
+
return command;
|
|
11651
|
+
return command.slice(afterDir + 2).trim();
|
|
11652
|
+
}
|
|
11653
|
+
function firstPipelineStage(command) {
|
|
11654
|
+
let quote = "none";
|
|
11655
|
+
let firstPipeIndex;
|
|
11656
|
+
for (let index = 0;index < command.length; index++) {
|
|
11657
|
+
const ch = command[index];
|
|
11658
|
+
if (quote === "single") {
|
|
11659
|
+
if (ch === "'")
|
|
11660
|
+
quote = "none";
|
|
11661
|
+
continue;
|
|
11662
|
+
}
|
|
11663
|
+
if (quote === "double") {
|
|
11664
|
+
if (ch === '"') {
|
|
11665
|
+
quote = "none";
|
|
11666
|
+
} else if (ch === "\\") {
|
|
11667
|
+
index++;
|
|
11668
|
+
} else if (ch === "`") {
|
|
11669
|
+
return null;
|
|
11670
|
+
}
|
|
11671
|
+
continue;
|
|
11672
|
+
}
|
|
11673
|
+
if (ch === "'") {
|
|
11674
|
+
quote = "single";
|
|
11675
|
+
} else if (ch === '"') {
|
|
11676
|
+
quote = "double";
|
|
11677
|
+
} else if (ch === "\\") {
|
|
11678
|
+
index++;
|
|
11679
|
+
} else if (ch === "`") {
|
|
11680
|
+
return null;
|
|
11681
|
+
} else if (ch === "|") {
|
|
11682
|
+
if (command[index + 1] === "|") {
|
|
11683
|
+
index++;
|
|
11684
|
+
} else if (firstPipeIndex === undefined) {
|
|
11685
|
+
firstPipeIndex = index;
|
|
11686
|
+
}
|
|
11687
|
+
}
|
|
11688
|
+
}
|
|
11689
|
+
if (quote !== "none")
|
|
11690
|
+
return null;
|
|
11691
|
+
return command.slice(0, firstPipeIndex ?? command.length).trim();
|
|
11692
|
+
}
|
|
11693
|
+
function readShellToken(command, start) {
|
|
11694
|
+
let quote = "none";
|
|
11695
|
+
let token = "";
|
|
11696
|
+
let index = start;
|
|
11697
|
+
for (;index < command.length; index++) {
|
|
11698
|
+
const ch = command[index];
|
|
11699
|
+
if (quote === "single") {
|
|
11700
|
+
if (ch === "'") {
|
|
11701
|
+
quote = "none";
|
|
11702
|
+
} else {
|
|
11703
|
+
token += ch;
|
|
11704
|
+
}
|
|
11705
|
+
continue;
|
|
11706
|
+
}
|
|
11707
|
+
if (quote === "double") {
|
|
11708
|
+
if (ch === '"') {
|
|
11709
|
+
quote = "none";
|
|
11710
|
+
} else if (ch === "\\") {
|
|
11711
|
+
index++;
|
|
11712
|
+
token += command[index] ?? "\\";
|
|
11713
|
+
} else if (ch === "`") {
|
|
11714
|
+
return null;
|
|
11715
|
+
} else {
|
|
11716
|
+
token += ch;
|
|
11717
|
+
}
|
|
11718
|
+
continue;
|
|
11719
|
+
}
|
|
11720
|
+
if (/\s/.test(ch))
|
|
11721
|
+
break;
|
|
11722
|
+
if (isTokenBoundary(ch))
|
|
11723
|
+
break;
|
|
11724
|
+
if (ch === "'") {
|
|
11725
|
+
quote = "single";
|
|
11726
|
+
} else if (ch === '"') {
|
|
11727
|
+
quote = "double";
|
|
11728
|
+
} else if (ch === "\\") {
|
|
11729
|
+
index++;
|
|
11730
|
+
token += command[index] ?? "\\";
|
|
11731
|
+
} else if (ch === "`") {
|
|
11732
|
+
return null;
|
|
11733
|
+
} else {
|
|
11734
|
+
token += ch;
|
|
11735
|
+
}
|
|
11736
|
+
}
|
|
11737
|
+
if (quote !== "none")
|
|
11738
|
+
return null;
|
|
11739
|
+
return { token, end: index };
|
|
11740
|
+
}
|
|
11741
|
+
function isTokenBoundary(ch) {
|
|
11742
|
+
return ch === "|" || ch === ";" || ch === "&" || ch === "<" || ch === ">";
|
|
11743
|
+
}
|
|
11744
|
+
function skipSpaces(input, start) {
|
|
11745
|
+
let index = start;
|
|
11746
|
+
while (index < input.length && /\s/.test(input[index]))
|
|
11747
|
+
index++;
|
|
11748
|
+
return index;
|
|
11749
|
+
}
|
|
11607
11750
|
// ../aft-bridge/dist/bash-timeout.js
|
|
11608
11751
|
function resolveBashKillTimeout(modelTimeout, foregroundWaitMs) {
|
|
11609
11752
|
if (modelTimeout !== undefined && modelTimeout >= foregroundWaitMs) {
|
|
@@ -11617,6 +11760,21 @@ import { homedir } from "node:os";
|
|
|
11617
11760
|
import { join } from "node:path";
|
|
11618
11761
|
import { StringDecoder } from "node:string_decoder";
|
|
11619
11762
|
|
|
11763
|
+
// ../aft-bridge/dist/command-timeouts.js
|
|
11764
|
+
var LONG_RUNNING_COMMAND_TIMEOUT_MS = {
|
|
11765
|
+
callers: 60000,
|
|
11766
|
+
trace_to: 60000,
|
|
11767
|
+
trace_to_symbol: 60000,
|
|
11768
|
+
trace_data: 60000,
|
|
11769
|
+
impact: 60000,
|
|
11770
|
+
grep: 60000,
|
|
11771
|
+
glob: 60000,
|
|
11772
|
+
semantic_search: 60000
|
|
11773
|
+
};
|
|
11774
|
+
function timeoutForCommand(command) {
|
|
11775
|
+
return LONG_RUNNING_COMMAND_TIMEOUT_MS[command];
|
|
11776
|
+
}
|
|
11777
|
+
|
|
11620
11778
|
// ../aft-bridge/dist/status-bar.js
|
|
11621
11779
|
var STATUS_BAR_HEARTBEAT_CALLS = 15;
|
|
11622
11780
|
function createStatusBarEmitState() {
|
|
@@ -11669,6 +11827,27 @@ var DEFAULT_BRIDGE_TIMEOUT_MS = 30000;
|
|
|
11669
11827
|
var BRIDGE_HANG_TIMEOUT_THRESHOLD = 2;
|
|
11670
11828
|
var SEMANTIC_TIMEOUT_SAFETY_MARGIN_MS = 5000;
|
|
11671
11829
|
var MAX_STDOUT_BUFFER = 64 * 1024 * 1024;
|
|
11830
|
+
var STDOUT_BUFFER_COMPACT_THRESHOLD = 64 * 1024;
|
|
11831
|
+
var TERMINAL_BASH_STATUSES = new Set([
|
|
11832
|
+
"completed",
|
|
11833
|
+
"failed",
|
|
11834
|
+
"killed",
|
|
11835
|
+
"timed_out",
|
|
11836
|
+
"cancelled",
|
|
11837
|
+
"timeout"
|
|
11838
|
+
]);
|
|
11839
|
+
function isTerminalBashStatus(status) {
|
|
11840
|
+
return typeof status === "string" && TERMINAL_BASH_STATUSES.has(status);
|
|
11841
|
+
}
|
|
11842
|
+
function bashTaskIdFrom(response) {
|
|
11843
|
+
const snakeCase = response.task_id;
|
|
11844
|
+
if (typeof snakeCase === "string" && snakeCase.length > 0)
|
|
11845
|
+
return snakeCase;
|
|
11846
|
+
const camelCase = response.taskId;
|
|
11847
|
+
if (typeof camelCase === "string" && camelCase.length > 0)
|
|
11848
|
+
return camelCase;
|
|
11849
|
+
return;
|
|
11850
|
+
}
|
|
11672
11851
|
function tagStderrLine(line) {
|
|
11673
11852
|
return /^\[aft(-\w+)?\] /.test(line) ? line : `[aft] ${line}`;
|
|
11674
11853
|
}
|
|
@@ -11723,11 +11902,12 @@ function clampSemanticTimeout(configOverrides, bridgeTimeoutMs) {
|
|
|
11723
11902
|
if (typeof timeoutMs !== "number" || !Number.isFinite(timeoutMs)) {
|
|
11724
11903
|
return configOverrides;
|
|
11725
11904
|
}
|
|
11726
|
-
const
|
|
11905
|
+
const semanticTransportBudgetMs = Math.max(bridgeTimeoutMs, LONG_RUNNING_COMMAND_TIMEOUT_MS.semantic_search ?? 0);
|
|
11906
|
+
const maxSemanticTimeoutMs = semanticTransportBudgetMs > SEMANTIC_TIMEOUT_SAFETY_MARGIN_MS ? semanticTransportBudgetMs - SEMANTIC_TIMEOUT_SAFETY_MARGIN_MS : Math.max(1, semanticTransportBudgetMs - 1);
|
|
11727
11907
|
if (timeoutMs <= maxSemanticTimeoutMs) {
|
|
11728
11908
|
return configOverrides;
|
|
11729
11909
|
}
|
|
11730
|
-
warn(`semantic.timeout_ms=${timeoutMs} exceeds
|
|
11910
|
+
warn(`semantic.timeout_ms=${timeoutMs} exceeds the semantic transport budget; clamping to ${maxSemanticTimeoutMs}ms (budget: ${semanticTransportBudgetMs}ms)`);
|
|
11731
11911
|
return {
|
|
11732
11912
|
...configOverrides,
|
|
11733
11913
|
semantic: {
|
|
@@ -11753,8 +11933,10 @@ class BinaryBridge {
|
|
|
11753
11933
|
cwd;
|
|
11754
11934
|
process = null;
|
|
11755
11935
|
pending = new Map;
|
|
11936
|
+
outstandingBackgroundTaskIds = new Set;
|
|
11756
11937
|
nextId = 1;
|
|
11757
11938
|
stdoutBuffer = "";
|
|
11939
|
+
stdoutReadOffset = 0;
|
|
11758
11940
|
stderrBuffer = "";
|
|
11759
11941
|
stderrTail = [];
|
|
11760
11942
|
_restartCount = 0;
|
|
@@ -11864,6 +12046,9 @@ class BinaryBridge {
|
|
|
11864
12046
|
hasPendingRequests() {
|
|
11865
12047
|
return this.pending.size > 0;
|
|
11866
12048
|
}
|
|
12049
|
+
hasOutstandingBackgroundTasks() {
|
|
12050
|
+
return this.outstandingBackgroundTaskIds.size > 0;
|
|
12051
|
+
}
|
|
11867
12052
|
getCwd() {
|
|
11868
12053
|
return this.cwd;
|
|
11869
12054
|
}
|
|
@@ -11985,7 +12170,7 @@ class BinaryBridge {
|
|
|
11985
12170
|
entry.reject(new Error(`${this.errorPrefix} Request "${command}" (id=${id}) timed out after ${effectiveTimeoutMs}ms`));
|
|
11986
12171
|
this.handleTimeout(requestSessionId);
|
|
11987
12172
|
}, effectiveTimeoutMs);
|
|
11988
|
-
this.pending.set(id, { resolve, reject, timer, onProgress: options?.onProgress });
|
|
12173
|
+
this.pending.set(id, { resolve, reject, timer, onProgress: options?.onProgress, command });
|
|
11989
12174
|
if (!this.process?.stdin?.writable) {
|
|
11990
12175
|
this.pending.delete(id);
|
|
11991
12176
|
clearTimeout(timer);
|
|
@@ -12246,6 +12431,7 @@ class BinaryBridge {
|
|
|
12246
12431
|
});
|
|
12247
12432
|
this.process = child;
|
|
12248
12433
|
this.stdoutBuffer = "";
|
|
12434
|
+
this.stdoutReadOffset = 0;
|
|
12249
12435
|
this.stderrBuffer = "";
|
|
12250
12436
|
this.lastChildActivityAt = 0;
|
|
12251
12437
|
this.consecutiveRequestTimeouts = 0;
|
|
@@ -12290,24 +12476,41 @@ class BinaryBridge {
|
|
|
12290
12476
|
${tail}`;
|
|
12291
12477
|
}
|
|
12292
12478
|
onStdoutData(data) {
|
|
12479
|
+
if (this.stdoutReadOffset > STDOUT_BUFFER_COMPACT_THRESHOLD) {
|
|
12480
|
+
this.compactStdoutBuffer();
|
|
12481
|
+
}
|
|
12293
12482
|
this.stdoutBuffer += data;
|
|
12294
|
-
if (this.stdoutBuffer.length > MAX_STDOUT_BUFFER) {
|
|
12483
|
+
if (this.stdoutBuffer.length - this.stdoutReadOffset > MAX_STDOUT_BUFFER) {
|
|
12295
12484
|
this.handleCrash(new Error(`aft bridge stdout buffer exceeded ${MAX_STDOUT_BUFFER} bytes — killing bridge`));
|
|
12296
12485
|
return;
|
|
12297
12486
|
}
|
|
12298
12487
|
let newlineIdx;
|
|
12299
12488
|
while ((newlineIdx = this.stdoutBuffer.indexOf(`
|
|
12300
|
-
|
|
12301
|
-
const line = this.stdoutBuffer.slice(
|
|
12302
|
-
this.
|
|
12303
|
-
if (
|
|
12304
|
-
|
|
12305
|
-
|
|
12489
|
+
`, this.stdoutReadOffset)) !== -1) {
|
|
12490
|
+
const line = this.stdoutBuffer.slice(this.stdoutReadOffset, newlineIdx).trim();
|
|
12491
|
+
this.stdoutReadOffset = newlineIdx + 1;
|
|
12492
|
+
if (line) {
|
|
12493
|
+
this.processStdoutLine(line);
|
|
12494
|
+
}
|
|
12495
|
+
if (this.stdoutReadOffset > STDOUT_BUFFER_COMPACT_THRESHOLD && this.stdoutReadOffset > this.stdoutBuffer.length / 2) {
|
|
12496
|
+
this.compactStdoutBuffer();
|
|
12497
|
+
}
|
|
12306
12498
|
}
|
|
12499
|
+
if (this.stdoutReadOffset === this.stdoutBuffer.length) {
|
|
12500
|
+
this.stdoutBuffer = "";
|
|
12501
|
+
this.stdoutReadOffset = 0;
|
|
12502
|
+
}
|
|
12503
|
+
}
|
|
12504
|
+
compactStdoutBuffer() {
|
|
12505
|
+
if (this.stdoutReadOffset === 0)
|
|
12506
|
+
return;
|
|
12507
|
+
this.stdoutBuffer = this.stdoutBuffer.slice(this.stdoutReadOffset);
|
|
12508
|
+
this.stdoutReadOffset = 0;
|
|
12307
12509
|
}
|
|
12308
12510
|
flushStdoutBuffer() {
|
|
12309
|
-
const line = this.stdoutBuffer.trim();
|
|
12511
|
+
const line = this.stdoutBuffer.slice(this.stdoutReadOffset).trim();
|
|
12310
12512
|
this.stdoutBuffer = "";
|
|
12513
|
+
this.stdoutReadOffset = 0;
|
|
12311
12514
|
if (!line)
|
|
12312
12515
|
return;
|
|
12313
12516
|
this.processStdoutLine(line);
|
|
@@ -12340,6 +12543,9 @@ class BinaryBridge {
|
|
|
12340
12543
|
return;
|
|
12341
12544
|
}
|
|
12342
12545
|
if (response.type === "bash_completed") {
|
|
12546
|
+
const taskId = bashTaskIdFrom(response);
|
|
12547
|
+
if (taskId)
|
|
12548
|
+
this.outstandingBackgroundTaskIds.delete(taskId);
|
|
12343
12549
|
this.onBashCompletion?.(response, this);
|
|
12344
12550
|
return;
|
|
12345
12551
|
}
|
|
@@ -12370,6 +12576,7 @@ class BinaryBridge {
|
|
|
12370
12576
|
clearTimeout(entry.timer);
|
|
12371
12577
|
this.consecutiveRequestTimeouts = 0;
|
|
12372
12578
|
this.scheduleRestartCountReset();
|
|
12579
|
+
this.accountForBashTaskResponse(entry.command, response);
|
|
12373
12580
|
this.captureStatusBar(response);
|
|
12374
12581
|
entry.resolve(response);
|
|
12375
12582
|
} else if (typeof response.type === "string") {
|
|
@@ -12379,6 +12586,18 @@ class BinaryBridge {
|
|
|
12379
12586
|
this.warnVia(`Failed to parse stdout line: ${line}`);
|
|
12380
12587
|
}
|
|
12381
12588
|
}
|
|
12589
|
+
accountForBashTaskResponse(command, response) {
|
|
12590
|
+
const taskId = bashTaskIdFrom(response);
|
|
12591
|
+
if (!taskId)
|
|
12592
|
+
return;
|
|
12593
|
+
if (isTerminalBashStatus(response.status)) {
|
|
12594
|
+
this.outstandingBackgroundTaskIds.delete(taskId);
|
|
12595
|
+
return;
|
|
12596
|
+
}
|
|
12597
|
+
if (command === "bash" && response.success !== false) {
|
|
12598
|
+
this.outstandingBackgroundTaskIds.add(taskId);
|
|
12599
|
+
}
|
|
12600
|
+
}
|
|
12382
12601
|
captureStatusBar(response) {
|
|
12383
12602
|
const parsed = parseStatusBarCounts(response.status_bar);
|
|
12384
12603
|
if (parsed)
|
|
@@ -12390,6 +12609,7 @@ class BinaryBridge {
|
|
|
12390
12609
|
handleTimeout(triggeringSessionId) {
|
|
12391
12610
|
this.consecutiveRequestTimeouts = 0;
|
|
12392
12611
|
this.rejectAllPending(new Error(`${this.errorPrefix} bridge killed during sibling timeout — request aborted`));
|
|
12612
|
+
this.outstandingBackgroundTaskIds.clear();
|
|
12393
12613
|
if (this.process) {
|
|
12394
12614
|
this.process.kill("SIGKILL");
|
|
12395
12615
|
this.process = null;
|
|
@@ -12419,6 +12639,7 @@ class BinaryBridge {
|
|
|
12419
12639
|
}
|
|
12420
12640
|
this.clearRestartResetTimer();
|
|
12421
12641
|
this.configured = false;
|
|
12642
|
+
this.outstandingBackgroundTaskIds.clear();
|
|
12422
12643
|
const tail = this.formatStderrTail();
|
|
12423
12644
|
if (tail) {
|
|
12424
12645
|
this.errorVia(`Binary crashed (restarts: ${this._restartCount})${cause ? `: ${cause.message}` : ""}.${tail}`);
|
|
@@ -12781,6 +13002,11 @@ function formatEditSummary(data) {
|
|
|
12781
13002
|
const n = data.files_modified;
|
|
12782
13003
|
return `Applied edits to ${n} file${n === 1 ? "" : "s"}.`;
|
|
12783
13004
|
}
|
|
13005
|
+
if (typeof data.total_files === "number") {
|
|
13006
|
+
const files = data.total_files;
|
|
13007
|
+
const reps = data.total_replacements ?? 0;
|
|
13008
|
+
return `Edited ${files} file${files === 1 ? "" : "s"} (${reps} replacement${reps === 1 ? "" : "s"}).`;
|
|
13009
|
+
}
|
|
12784
13010
|
const additions = data.diff?.additions ?? 0;
|
|
12785
13011
|
const deletions = data.diff?.deletions ?? 0;
|
|
12786
13012
|
const counts = `+${additions}/-${deletions}`;
|
|
@@ -13994,7 +14220,23 @@ function isProcessAlive(pid) {
|
|
|
13994
14220
|
}
|
|
13995
14221
|
}
|
|
13996
14222
|
// ../aft-bridge/dist/pipe-strip.js
|
|
13997
|
-
var NOISE_FILTERS = new Set([
|
|
14223
|
+
var NOISE_FILTERS = new Set([
|
|
14224
|
+
"grep",
|
|
14225
|
+
"rg",
|
|
14226
|
+
"head",
|
|
14227
|
+
"tail",
|
|
14228
|
+
"cat",
|
|
14229
|
+
"less",
|
|
14230
|
+
"more",
|
|
14231
|
+
"sed",
|
|
14232
|
+
"awk",
|
|
14233
|
+
"cut",
|
|
14234
|
+
"sort",
|
|
14235
|
+
"uniq",
|
|
14236
|
+
"tr",
|
|
14237
|
+
"column",
|
|
14238
|
+
"fold"
|
|
14239
|
+
]);
|
|
13998
14240
|
var GREP_GUARD_FLAGS = new Set([
|
|
13999
14241
|
"c",
|
|
14000
14242
|
"count",
|
|
@@ -14008,6 +14250,8 @@ var GREP_GUARD_FLAGS = new Set([
|
|
|
14008
14250
|
function maybeStripCompressorPipe(command, compressionEnabled) {
|
|
14009
14251
|
if (!compressionEnabled)
|
|
14010
14252
|
return { command, stripped: false };
|
|
14253
|
+
if (containsUnsplittableConstruct(command))
|
|
14254
|
+
return { command, stripped: false };
|
|
14011
14255
|
const chain = splitTopLevelAndChain(command);
|
|
14012
14256
|
if (chain === null)
|
|
14013
14257
|
return { command, stripped: false };
|
|
@@ -14021,7 +14265,7 @@ function maybeStripCompressorPipe(command, compressionEnabled) {
|
|
|
14021
14265
|
return { command, stripped: false };
|
|
14022
14266
|
const filterStages = stages.slice(1).map((stage) => stage.trim());
|
|
14023
14267
|
for (const stage of filterStages) {
|
|
14024
|
-
if (!
|
|
14268
|
+
if (!filterStageIsSafeToDrop(stage))
|
|
14025
14269
|
return { command, stripped: false };
|
|
14026
14270
|
}
|
|
14027
14271
|
const filters = filterStages.join(" | ");
|
|
@@ -14067,6 +14311,14 @@ function splitTopLevelAndChain(command) {
|
|
|
14067
14311
|
return null;
|
|
14068
14312
|
if (char === ";")
|
|
14069
14313
|
return null;
|
|
14314
|
+
if (char === `
|
|
14315
|
+
` || char === "\r")
|
|
14316
|
+
return null;
|
|
14317
|
+
if (char === "&") {
|
|
14318
|
+
const prev = command[index - 1];
|
|
14319
|
+
if (prev !== ">" && next !== ">")
|
|
14320
|
+
return null;
|
|
14321
|
+
}
|
|
14070
14322
|
}
|
|
14071
14323
|
segments.push(command.slice(start));
|
|
14072
14324
|
return segments;
|
|
@@ -14109,45 +14361,262 @@ function isCompressorHandledRunner(stage) {
|
|
|
14109
14361
|
const tokens = tokenizeStage(stage);
|
|
14110
14362
|
if (tokens.length === 0)
|
|
14111
14363
|
return false;
|
|
14112
|
-
const [first, second, third] = tokens;
|
|
14113
|
-
if (!first)
|
|
14114
|
-
return false;
|
|
14115
14364
|
if (tokens.some((token) => token === "&&" || token === "||" || token.includes(";"))) {
|
|
14116
14365
|
return false;
|
|
14117
14366
|
}
|
|
14367
|
+
const first = runnerName(tokens[0]);
|
|
14368
|
+
const second = tokens[1];
|
|
14369
|
+
const third = tokens[2];
|
|
14370
|
+
const rest = tokens.slice(1);
|
|
14371
|
+
if (!first)
|
|
14372
|
+
return false;
|
|
14118
14373
|
if (first === "bun")
|
|
14119
14374
|
return second === "test" || second === "run" && startsWithTest(third);
|
|
14120
|
-
if (first === "
|
|
14121
|
-
return ["test", "build", "check", "clippy"].includes(second ?? "");
|
|
14122
|
-
if (first === "go")
|
|
14123
|
-
return second === "test" || second === "build";
|
|
14124
|
-
if (["npm", "pnpm"].includes(first)) {
|
|
14375
|
+
if (first === "npm" || first === "pnpm") {
|
|
14125
14376
|
return second === "test" || second === "run" && startsWithTest(third);
|
|
14126
14377
|
}
|
|
14127
|
-
if (first === "yarn")
|
|
14128
|
-
return second === "
|
|
14378
|
+
if (first === "yarn") {
|
|
14379
|
+
return startsWithTest(second) || second === "run" && startsWithTest(third);
|
|
14380
|
+
}
|
|
14381
|
+
if (first === "deno")
|
|
14382
|
+
return ["test", "lint", "check", "bench"].includes(second ?? "");
|
|
14383
|
+
if (first === "npx") {
|
|
14384
|
+
return ["tsc", "eslint", "vitest", "jest", "playwright", "biome"].includes(second ?? "");
|
|
14385
|
+
}
|
|
14129
14386
|
if (first === "playwright")
|
|
14130
14387
|
return second === "test";
|
|
14131
|
-
if (first === "
|
|
14132
|
-
return ["
|
|
14133
|
-
|
|
14388
|
+
if (first === "cargo") {
|
|
14389
|
+
return ["test", "build", "check", "clippy", "nextest"].includes(second ?? "");
|
|
14390
|
+
}
|
|
14391
|
+
if (first === "go")
|
|
14392
|
+
return ["test", "build", "vet"].includes(second ?? "");
|
|
14393
|
+
if (first === "gradle" || first === "gradlew") {
|
|
14394
|
+
return hasBuildTask(rest, ["test", "check", "build", "assemble", "clean"]);
|
|
14395
|
+
}
|
|
14396
|
+
if (first === "mvn" || first === "mvnw") {
|
|
14397
|
+
return hasBuildTask(rest, ["test", "verify", "package", "install"]);
|
|
14398
|
+
}
|
|
14399
|
+
if (first === "dotnet")
|
|
14400
|
+
return ["test", "build"].includes(second ?? "");
|
|
14401
|
+
if (first === "rspec")
|
|
14402
|
+
return true;
|
|
14403
|
+
if (first === "rake")
|
|
14404
|
+
return second === "test" || second === "spec";
|
|
14405
|
+
if (first === "phpunit" || first === "pest")
|
|
14406
|
+
return true;
|
|
14407
|
+
if (first === "xcodebuild")
|
|
14408
|
+
return xcodebuildHasBuildAction(rest);
|
|
14409
|
+
if (first === "swift")
|
|
14410
|
+
return second === "test" || second === "build";
|
|
14411
|
+
if (first === "make" || first === "gmake") {
|
|
14412
|
+
return hasBuildTask(rest, ["test", "check", "lint", "clean"]);
|
|
14413
|
+
}
|
|
14414
|
+
return [
|
|
14415
|
+
"vitest",
|
|
14416
|
+
"jest",
|
|
14417
|
+
"pytest",
|
|
14418
|
+
"tsc",
|
|
14419
|
+
"eslint",
|
|
14420
|
+
"biome",
|
|
14421
|
+
"ruff",
|
|
14422
|
+
"mypy",
|
|
14423
|
+
"tox",
|
|
14424
|
+
"nox"
|
|
14425
|
+
].includes(first);
|
|
14426
|
+
}
|
|
14427
|
+
function runnerName(token) {
|
|
14428
|
+
if (!token)
|
|
14429
|
+
return "";
|
|
14430
|
+
const slash = token.lastIndexOf("/");
|
|
14431
|
+
return slash === -1 ? token : token.slice(slash + 1);
|
|
14432
|
+
}
|
|
14433
|
+
function hasBuildTask(args, tasks) {
|
|
14434
|
+
const isAllowedTask = (arg) => tasks.some((task) => arg === task || arg.endsWith(`:${task}`));
|
|
14435
|
+
const isFlagOrProperty = (arg) => arg.startsWith("-") || arg.includes("=");
|
|
14436
|
+
let sawAllowed = false;
|
|
14437
|
+
for (const arg of args) {
|
|
14438
|
+
if (isFlagOrProperty(arg))
|
|
14439
|
+
continue;
|
|
14440
|
+
if (!isAllowedTask(arg))
|
|
14441
|
+
return false;
|
|
14442
|
+
sawAllowed = true;
|
|
14443
|
+
}
|
|
14444
|
+
return sawAllowed;
|
|
14134
14445
|
}
|
|
14135
14446
|
function startsWithTest(token) {
|
|
14136
14447
|
return token?.startsWith("test") === true;
|
|
14137
14448
|
}
|
|
14138
|
-
|
|
14139
|
-
|
|
14140
|
-
|
|
14449
|
+
var XCODEBUILD_VALUE_FLAGS = new Set([
|
|
14450
|
+
"-scheme",
|
|
14451
|
+
"-target",
|
|
14452
|
+
"-project",
|
|
14453
|
+
"-workspace",
|
|
14454
|
+
"-configuration",
|
|
14455
|
+
"-sdk",
|
|
14456
|
+
"-destination",
|
|
14457
|
+
"-arch",
|
|
14458
|
+
"-derivedDataPath",
|
|
14459
|
+
"-resultBundlePath",
|
|
14460
|
+
"-xcconfig",
|
|
14461
|
+
"-toolchain"
|
|
14462
|
+
]);
|
|
14463
|
+
var XCODEBUILD_BUILD_ACTIONS = new Set([
|
|
14464
|
+
"build",
|
|
14465
|
+
"test",
|
|
14466
|
+
"build-for-testing",
|
|
14467
|
+
"test-without-building",
|
|
14468
|
+
"analyze"
|
|
14469
|
+
]);
|
|
14470
|
+
function xcodebuildHasBuildAction(args) {
|
|
14471
|
+
for (let i = 0;i < args.length; i++) {
|
|
14472
|
+
const arg = args[i];
|
|
14473
|
+
if (arg.startsWith("-")) {
|
|
14474
|
+
if (XCODEBUILD_VALUE_FLAGS.has(arg))
|
|
14475
|
+
i++;
|
|
14476
|
+
continue;
|
|
14477
|
+
}
|
|
14478
|
+
if (XCODEBUILD_BUILD_ACTIONS.has(arg))
|
|
14479
|
+
return true;
|
|
14480
|
+
}
|
|
14481
|
+
return false;
|
|
14482
|
+
}
|
|
14483
|
+
function containsUnsplittableConstruct(command) {
|
|
14484
|
+
let quote = null;
|
|
14485
|
+
let escaped = false;
|
|
14486
|
+
for (let i = 0;i < command.length; i++) {
|
|
14487
|
+
const char = command[i];
|
|
14488
|
+
if (escaped) {
|
|
14489
|
+
escaped = false;
|
|
14490
|
+
continue;
|
|
14491
|
+
}
|
|
14492
|
+
if (char === "\\" && quote !== "'") {
|
|
14493
|
+
escaped = true;
|
|
14494
|
+
continue;
|
|
14495
|
+
}
|
|
14496
|
+
if (quote === "'") {
|
|
14497
|
+
if (char === "'")
|
|
14498
|
+
quote = null;
|
|
14499
|
+
continue;
|
|
14500
|
+
}
|
|
14501
|
+
if (quote === '"') {
|
|
14502
|
+
if (char === '"')
|
|
14503
|
+
quote = null;
|
|
14504
|
+
else if (char === "`")
|
|
14505
|
+
return true;
|
|
14506
|
+
else if (char === "$" && command[i + 1] === "(")
|
|
14507
|
+
return true;
|
|
14508
|
+
continue;
|
|
14509
|
+
}
|
|
14510
|
+
if (char === "'" || char === '"') {
|
|
14511
|
+
quote = char;
|
|
14512
|
+
continue;
|
|
14513
|
+
}
|
|
14514
|
+
if (char === "`")
|
|
14515
|
+
return true;
|
|
14516
|
+
if (char === "(" || char === ")")
|
|
14517
|
+
return true;
|
|
14518
|
+
}
|
|
14519
|
+
return false;
|
|
14520
|
+
}
|
|
14521
|
+
var READS_FILE_OPERAND = new Set(["cat", "tac", "nl", "less", "more"]);
|
|
14522
|
+
function filterStageIsSafeToDrop(stage) {
|
|
14523
|
+
const head = tokenizeStage(stage)[0];
|
|
14141
14524
|
if (!head)
|
|
14142
14525
|
return false;
|
|
14143
14526
|
if (head === "wc")
|
|
14144
14527
|
return false;
|
|
14145
14528
|
if (!NOISE_FILTERS.has(head))
|
|
14146
14529
|
return false;
|
|
14147
|
-
if ((
|
|
14530
|
+
if (/[<>]/.test(stage))
|
|
14531
|
+
return false;
|
|
14532
|
+
if (hasUnquotedBackground(stage))
|
|
14533
|
+
return false;
|
|
14534
|
+
const args = tokenizeStage(stage).slice(1);
|
|
14535
|
+
const hasFlag = (...names) => args.some((a) => names.some((n) => a === n || a.startsWith(`${n}=`)));
|
|
14536
|
+
if (head === "grep" || head === "rg") {
|
|
14537
|
+
if (hasIntentChangingGrepFlag(args))
|
|
14538
|
+
return false;
|
|
14539
|
+
if (countBareOperands(args) > 1)
|
|
14540
|
+
return false;
|
|
14541
|
+
return true;
|
|
14542
|
+
}
|
|
14543
|
+
if (head === "head" || head === "tail") {
|
|
14544
|
+
if (bareOperands(args).some((op) => !/^\d+$/.test(op)))
|
|
14545
|
+
return false;
|
|
14546
|
+
return true;
|
|
14547
|
+
}
|
|
14548
|
+
if (READS_FILE_OPERAND.has(head)) {
|
|
14549
|
+
if (countBareOperands(args) > 0)
|
|
14550
|
+
return false;
|
|
14551
|
+
return true;
|
|
14552
|
+
}
|
|
14553
|
+
if (head === "sed") {
|
|
14554
|
+
if (hasFlag("-i", "--in-place"))
|
|
14555
|
+
return false;
|
|
14556
|
+
if (countBareOperands(args) > 1)
|
|
14557
|
+
return false;
|
|
14558
|
+
return true;
|
|
14559
|
+
}
|
|
14560
|
+
if (head === "awk") {
|
|
14561
|
+
if (countBareOperands(args) > 1)
|
|
14562
|
+
return false;
|
|
14563
|
+
return true;
|
|
14564
|
+
}
|
|
14565
|
+
if (head === "sort" && hasFlag("-o", "--output"))
|
|
14566
|
+
return false;
|
|
14567
|
+
if (bareOperands(args).some((op) => op.includes("/") || op.includes(".")))
|
|
14148
14568
|
return false;
|
|
14149
14569
|
return true;
|
|
14150
14570
|
}
|
|
14571
|
+
function bareOperands(args) {
|
|
14572
|
+
const out = [];
|
|
14573
|
+
let afterDoubleDash = false;
|
|
14574
|
+
for (const arg of args) {
|
|
14575
|
+
if (!afterDoubleDash && arg === "--") {
|
|
14576
|
+
afterDoubleDash = true;
|
|
14577
|
+
continue;
|
|
14578
|
+
}
|
|
14579
|
+
if (!afterDoubleDash && arg.startsWith("-") && arg !== "-")
|
|
14580
|
+
continue;
|
|
14581
|
+
out.push(arg);
|
|
14582
|
+
}
|
|
14583
|
+
return out;
|
|
14584
|
+
}
|
|
14585
|
+
function countBareOperands(args) {
|
|
14586
|
+
return bareOperands(args).length;
|
|
14587
|
+
}
|
|
14588
|
+
function hasUnquotedBackground(stage) {
|
|
14589
|
+
let quote = null;
|
|
14590
|
+
let escaped = false;
|
|
14591
|
+
for (let i = 0;i < stage.length; i++) {
|
|
14592
|
+
const char = stage[i];
|
|
14593
|
+
if (escaped) {
|
|
14594
|
+
escaped = false;
|
|
14595
|
+
continue;
|
|
14596
|
+
}
|
|
14597
|
+
if (char === "\\" && quote !== "'") {
|
|
14598
|
+
escaped = true;
|
|
14599
|
+
continue;
|
|
14600
|
+
}
|
|
14601
|
+
if (quote) {
|
|
14602
|
+
if (char === quote)
|
|
14603
|
+
quote = null;
|
|
14604
|
+
continue;
|
|
14605
|
+
}
|
|
14606
|
+
if (char === "'" || char === '"') {
|
|
14607
|
+
quote = char;
|
|
14608
|
+
continue;
|
|
14609
|
+
}
|
|
14610
|
+
if (char === "&") {
|
|
14611
|
+
const prev = stage[i - 1];
|
|
14612
|
+
const next = stage[i + 1];
|
|
14613
|
+
if (prev === "&" || next === "&" || prev === ">" || next === ">")
|
|
14614
|
+
continue;
|
|
14615
|
+
return true;
|
|
14616
|
+
}
|
|
14617
|
+
}
|
|
14618
|
+
return false;
|
|
14619
|
+
}
|
|
14151
14620
|
function hasIntentChangingGrepFlag(args) {
|
|
14152
14621
|
for (const arg of args) {
|
|
14153
14622
|
if (arg === "--")
|
|
@@ -14210,7 +14679,7 @@ function tokenizeStage(stage) {
|
|
|
14210
14679
|
}
|
|
14211
14680
|
// ../aft-bridge/dist/pool.js
|
|
14212
14681
|
import { realpathSync as realpathSync2 } from "node:fs";
|
|
14213
|
-
var DEFAULT_IDLE_TIMEOUT_MS =
|
|
14682
|
+
var DEFAULT_IDLE_TIMEOUT_MS = 30 * 60 * 1000;
|
|
14214
14683
|
var DEFAULT_MAX_POOL_SIZE = 8;
|
|
14215
14684
|
var CLEANUP_INTERVAL_MS = 60 * 1000;
|
|
14216
14685
|
class BridgePool {
|
|
@@ -14284,7 +14753,7 @@ class BridgePool {
|
|
|
14284
14753
|
cleanup() {
|
|
14285
14754
|
const now = Date.now();
|
|
14286
14755
|
for (const [dir, entry] of this.bridges) {
|
|
14287
|
-
if (entry.bridge.hasPendingRequests())
|
|
14756
|
+
if (entry.bridge.hasPendingRequests() || entry.bridge.hasOutstandingBackgroundTasks())
|
|
14288
14757
|
continue;
|
|
14289
14758
|
if (now - entry.lastUsed > this.idleTimeoutMs) {
|
|
14290
14759
|
entry.bridge.shutdown().catch((err) => this.error("cleanup shutdown failed:", err));
|
|
@@ -14292,7 +14761,7 @@ class BridgePool {
|
|
|
14292
14761
|
}
|
|
14293
14762
|
}
|
|
14294
14763
|
for (const bridge of this.staleBridges) {
|
|
14295
|
-
if (bridge.hasPendingRequests())
|
|
14764
|
+
if (bridge.hasPendingRequests() || bridge.hasOutstandingBackgroundTasks())
|
|
14296
14765
|
continue;
|
|
14297
14766
|
bridge.shutdown().catch((err) => this.error("stale cleanup shutdown failed:", err));
|
|
14298
14767
|
this.staleBridges.delete(bridge);
|
|
@@ -14302,7 +14771,7 @@ class BridgePool {
|
|
|
14302
14771
|
let oldestDir = null;
|
|
14303
14772
|
let oldestTime = Infinity;
|
|
14304
14773
|
for (const [dir, entry] of this.bridges) {
|
|
14305
|
-
if (entry.bridge.hasPendingRequests())
|
|
14774
|
+
if (entry.bridge.hasPendingRequests() || entry.bridge.hasOutstandingBackgroundTasks())
|
|
14306
14775
|
continue;
|
|
14307
14776
|
if (entry.lastUsed < oldestTime) {
|
|
14308
14777
|
oldestTime = entry.lastUsed;
|
|
@@ -32515,38 +32984,6 @@ function discoverRelevantGithubServers(projectRoot) {
|
|
|
32515
32984
|
return relevant;
|
|
32516
32985
|
}
|
|
32517
32986
|
|
|
32518
|
-
// src/metadata-store.ts
|
|
32519
|
-
var pendingStore = new Map;
|
|
32520
|
-
var STALE_TIMEOUT_MS = 15 * 60 * 1000;
|
|
32521
|
-
function makeKey(sessionID, callID) {
|
|
32522
|
-
return `${sessionID}:${callID}`;
|
|
32523
|
-
}
|
|
32524
|
-
function cleanupStaleEntries() {
|
|
32525
|
-
const now = Date.now();
|
|
32526
|
-
for (const [key, entry] of pendingStore) {
|
|
32527
|
-
if (now - entry.storedAt > STALE_TIMEOUT_MS) {
|
|
32528
|
-
pendingStore.delete(key);
|
|
32529
|
-
}
|
|
32530
|
-
}
|
|
32531
|
-
}
|
|
32532
|
-
function storeToolMetadata(sessionID, callID, data) {
|
|
32533
|
-
cleanupStaleEntries();
|
|
32534
|
-
pendingStore.set(makeKey(sessionID, callID), {
|
|
32535
|
-
...data,
|
|
32536
|
-
storedAt: Date.now()
|
|
32537
|
-
});
|
|
32538
|
-
}
|
|
32539
|
-
function consumeToolMetadata(sessionID, callID) {
|
|
32540
|
-
const key = makeKey(sessionID, callID);
|
|
32541
|
-
const stored = pendingStore.get(key);
|
|
32542
|
-
if (stored) {
|
|
32543
|
-
pendingStore.delete(key);
|
|
32544
|
-
const { storedAt: _, ...data } = stored;
|
|
32545
|
-
return data;
|
|
32546
|
-
}
|
|
32547
|
-
return;
|
|
32548
|
-
}
|
|
32549
|
-
|
|
32550
32987
|
// src/normalize-schemas.ts
|
|
32551
32988
|
import { tool } from "@opencode-ai/plugin";
|
|
32552
32989
|
function stripRootJsonSchemaFields(jsonSchema) {
|
|
@@ -32665,7 +33102,14 @@ function writeTerminal(terminal, data) {
|
|
|
32665
33102
|
|
|
32666
33103
|
// src/shared/rpc-server.ts
|
|
32667
33104
|
import { randomBytes as randomBytes2 } from "node:crypto";
|
|
32668
|
-
import {
|
|
33105
|
+
import {
|
|
33106
|
+
mkdirSync as mkdirSync10,
|
|
33107
|
+
readdirSync as readdirSync5,
|
|
33108
|
+
readFileSync as readFileSync13,
|
|
33109
|
+
renameSync as renameSync8,
|
|
33110
|
+
unlinkSync as unlinkSync7,
|
|
33111
|
+
writeFileSync as writeFileSync10
|
|
33112
|
+
} from "node:fs";
|
|
32669
33113
|
import { createServer } from "node:http";
|
|
32670
33114
|
import { dirname as dirname9, join as join19 } from "node:path";
|
|
32671
33115
|
|
|
@@ -32680,6 +33124,41 @@ function rpcPortFileDir(storageDir, directory) {
|
|
|
32680
33124
|
const hash2 = projectHash(directory);
|
|
32681
33125
|
return join18(storageDir, "rpc", hash2, "ports");
|
|
32682
33126
|
}
|
|
33127
|
+
function isPidAlive(pid) {
|
|
33128
|
+
if (typeof pid !== "number" || !Number.isInteger(pid) || pid <= 0)
|
|
33129
|
+
return false;
|
|
33130
|
+
try {
|
|
33131
|
+
process.kill(pid, 0);
|
|
33132
|
+
return true;
|
|
33133
|
+
} catch (err) {
|
|
33134
|
+
return err.code === "EPERM";
|
|
33135
|
+
}
|
|
33136
|
+
}
|
|
33137
|
+
function parseRpcPortRecord(content) {
|
|
33138
|
+
const trimmed = content.trim();
|
|
33139
|
+
if (trimmed.length === 0)
|
|
33140
|
+
return null;
|
|
33141
|
+
if (trimmed.startsWith("{")) {
|
|
33142
|
+
try {
|
|
33143
|
+
const parsed = JSON.parse(trimmed);
|
|
33144
|
+
const port2 = typeof parsed.port === "number" ? parsed.port : Number.NaN;
|
|
33145
|
+
if (!Number.isInteger(port2) || port2 <= 0 || port2 > 65535)
|
|
33146
|
+
return null;
|
|
33147
|
+
return {
|
|
33148
|
+
port: port2,
|
|
33149
|
+
token: typeof parsed.token === "string" ? parsed.token : null,
|
|
33150
|
+
pid: typeof parsed.pid === "number" && Number.isInteger(parsed.pid) ? parsed.pid : undefined,
|
|
33151
|
+
started_at: typeof parsed.started_at === "number" ? parsed.started_at : undefined
|
|
33152
|
+
};
|
|
33153
|
+
} catch {
|
|
33154
|
+
return null;
|
|
33155
|
+
}
|
|
33156
|
+
}
|
|
33157
|
+
const port = Number.parseInt(trimmed, 10);
|
|
33158
|
+
if (!Number.isInteger(port) || port <= 0 || port > 65535)
|
|
33159
|
+
return null;
|
|
33160
|
+
return { port, token: null };
|
|
33161
|
+
}
|
|
32683
33162
|
|
|
32684
33163
|
// src/shared/rpc-server.ts
|
|
32685
33164
|
class AftRpcServer {
|
|
@@ -32718,12 +33197,15 @@ class AftRpcServer {
|
|
|
32718
33197
|
const dir = dirname9(this.portFilePath);
|
|
32719
33198
|
mkdirSync10(dir, { recursive: true, mode: 448 });
|
|
32720
33199
|
const tmpPath = `${this.portFilePath}.tmp`;
|
|
32721
|
-
writeFileSync10(tmpPath, JSON.stringify({
|
|
32722
|
-
|
|
32723
|
-
|
|
32724
|
-
|
|
33200
|
+
writeFileSync10(tmpPath, JSON.stringify({
|
|
33201
|
+
port: this.port,
|
|
33202
|
+
token: this.token,
|
|
33203
|
+
pid: process.pid,
|
|
33204
|
+
started_at: Date.now()
|
|
33205
|
+
}), { encoding: "utf-8", mode: 384 });
|
|
32725
33206
|
renameSync8(tmpPath, this.portFilePath);
|
|
32726
33207
|
log2(`RPC server listening on 127.0.0.1:${this.port}`);
|
|
33208
|
+
this.sweepDeadPortFiles();
|
|
32727
33209
|
} catch (err) {
|
|
32728
33210
|
warn2(`Failed to write RPC port file: ${err}`);
|
|
32729
33211
|
}
|
|
@@ -32732,6 +33214,31 @@ class AftRpcServer {
|
|
|
32732
33214
|
server.unref();
|
|
32733
33215
|
});
|
|
32734
33216
|
}
|
|
33217
|
+
sweepDeadPortFiles() {
|
|
33218
|
+
let entries;
|
|
33219
|
+
try {
|
|
33220
|
+
entries = readdirSync5(this.portsDir);
|
|
33221
|
+
} catch {
|
|
33222
|
+
return;
|
|
33223
|
+
}
|
|
33224
|
+
for (const entry of entries) {
|
|
33225
|
+
if (!entry.endsWith(".json"))
|
|
33226
|
+
continue;
|
|
33227
|
+
const filePath = join19(this.portsDir, entry);
|
|
33228
|
+
if (filePath === this.portFilePath)
|
|
33229
|
+
continue;
|
|
33230
|
+
try {
|
|
33231
|
+
const record2 = parseRpcPortRecord(readFileSync13(filePath, "utf-8"));
|
|
33232
|
+
if (record2 === null) {
|
|
33233
|
+
unlinkSync7(filePath);
|
|
33234
|
+
continue;
|
|
33235
|
+
}
|
|
33236
|
+
if (record2.pid !== undefined && !isPidAlive(record2.pid)) {
|
|
33237
|
+
unlinkSync7(filePath);
|
|
33238
|
+
}
|
|
33239
|
+
} catch {}
|
|
33240
|
+
}
|
|
33241
|
+
}
|
|
32735
33242
|
stop() {
|
|
32736
33243
|
if (this.server) {
|
|
32737
33244
|
this.server.close();
|
|
@@ -32856,15 +33363,47 @@ function warmSessionDirectory(client, sessionId, fallbackDirectory) {
|
|
|
32856
33363
|
return;
|
|
32857
33364
|
getSessionDirectory(client, sessionId, fallbackDirectory);
|
|
32858
33365
|
}
|
|
32859
|
-
|
|
32860
|
-
|
|
32861
|
-
function
|
|
32862
|
-
|
|
32863
|
-
|
|
32864
|
-
|
|
32865
|
-
|
|
32866
|
-
|
|
32867
|
-
|
|
33366
|
+
var VERIFY_TTL_MS = 15000;
|
|
33367
|
+
var verifyCache = new Map;
|
|
33368
|
+
async function verifySessionDirectory(client, sessionId) {
|
|
33369
|
+
if (!sessionId)
|
|
33370
|
+
return null;
|
|
33371
|
+
const hit = verifyCache.get(sessionId);
|
|
33372
|
+
if (hit && Date.now() - hit.verifiedAt < VERIFY_TTL_MS)
|
|
33373
|
+
return hit.directory;
|
|
33374
|
+
const c = client;
|
|
33375
|
+
const sessionApi = c?.session;
|
|
33376
|
+
if (!sessionApi || typeof sessionApi.get !== "function")
|
|
33377
|
+
return null;
|
|
33378
|
+
let dir = null;
|
|
33379
|
+
try {
|
|
33380
|
+
const result = await sessionApi.get({ path: { id: sessionId } });
|
|
33381
|
+
const session = result?.data ?? result;
|
|
33382
|
+
if (session && typeof session.directory === "string" && session.directory.length > 0) {
|
|
33383
|
+
dir = session.directory;
|
|
33384
|
+
}
|
|
33385
|
+
} catch {
|
|
33386
|
+
return null;
|
|
33387
|
+
}
|
|
33388
|
+
verifyCache.set(sessionId, { directory: dir, verifiedAt: Date.now() });
|
|
33389
|
+
if (verifyCache.size > CACHE_MAX_ENTRIES) {
|
|
33390
|
+
const oldest = verifyCache.keys().next().value;
|
|
33391
|
+
if (oldest !== undefined)
|
|
33392
|
+
verifyCache.delete(oldest);
|
|
33393
|
+
}
|
|
33394
|
+
if (dir !== null)
|
|
33395
|
+
setCache(sessionId, dir);
|
|
33396
|
+
return dir;
|
|
33397
|
+
}
|
|
33398
|
+
|
|
33399
|
+
// src/shared/status.ts
|
|
33400
|
+
function asRecord(value) {
|
|
33401
|
+
return typeof value === "object" && value !== null ? value : {};
|
|
33402
|
+
}
|
|
33403
|
+
function readString(value, fallback = "") {
|
|
33404
|
+
return typeof value === "string" ? value : fallback;
|
|
33405
|
+
}
|
|
33406
|
+
function readNullableString(value) {
|
|
32868
33407
|
return typeof value === "string" ? value : null;
|
|
32869
33408
|
}
|
|
32870
33409
|
function readBoolean(value, fallback = false) {
|
|
@@ -33068,7 +33607,7 @@ function formatStatusMarkdown(status) {
|
|
|
33068
33607
|
|
|
33069
33608
|
// src/shared/tui-config.ts
|
|
33070
33609
|
var import_comment_json4 = __toESM(require_src2(), 1);
|
|
33071
|
-
import { existsSync as existsSync14, mkdirSync as mkdirSync11, readFileSync as
|
|
33610
|
+
import { existsSync as existsSync14, mkdirSync as mkdirSync11, readFileSync as readFileSync14, writeFileSync as writeFileSync11 } from "node:fs";
|
|
33072
33611
|
import { dirname as dirname10, join as join21 } from "node:path";
|
|
33073
33612
|
|
|
33074
33613
|
// src/shared/opencode-config-dir.ts
|
|
@@ -33116,7 +33655,7 @@ function ensureTuiPluginEntry() {
|
|
|
33116
33655
|
const configPath = resolveTuiConfigPath();
|
|
33117
33656
|
let config2 = {};
|
|
33118
33657
|
if (existsSync14(configPath)) {
|
|
33119
|
-
config2 = import_comment_json4.parse(
|
|
33658
|
+
config2 = import_comment_json4.parse(readFileSync14(configPath, "utf-8")) ?? {};
|
|
33120
33659
|
}
|
|
33121
33660
|
const plugins = Array.isArray(config2.plugin) ? config2.plugin.filter((value) => typeof value === "string") : [];
|
|
33122
33661
|
if (plugins.some((plugin) => plugin === PLUGIN_NAME || plugin.startsWith(`${PLUGIN_NAME}@`) || plugin.includes("opencode-plugin") || plugin.includes("aft-opencode"))) {
|
|
@@ -33193,7 +33732,7 @@ function registerShutdownCleanup(fn) {
|
|
|
33193
33732
|
|
|
33194
33733
|
// src/status-bar-inject.ts
|
|
33195
33734
|
var emitStateBySession = new Map;
|
|
33196
|
-
function statusBarSuffixForSession(sessionID, counts) {
|
|
33735
|
+
function statusBarSuffixForSession(sessionID, counts, force = false) {
|
|
33197
33736
|
if (!counts)
|
|
33198
33737
|
return "";
|
|
33199
33738
|
let state = emitStateBySession.get(sessionID);
|
|
@@ -33201,12 +33740,67 @@ function statusBarSuffixForSession(sessionID, counts) {
|
|
|
33201
33740
|
state = createStatusBarEmitState();
|
|
33202
33741
|
emitStateBySession.set(sessionID, state);
|
|
33203
33742
|
}
|
|
33743
|
+
if (force) {
|
|
33744
|
+
state.last = counts;
|
|
33745
|
+
state.callsSinceEmit = 0;
|
|
33746
|
+
return statusBarLine(counts);
|
|
33747
|
+
}
|
|
33204
33748
|
return shouldEmitStatusBar(state, counts) ? statusBarLine(counts) : "";
|
|
33205
33749
|
}
|
|
33206
33750
|
function clearStatusBarSession(sessionID) {
|
|
33207
33751
|
emitStateBySession.delete(sessionID);
|
|
33208
33752
|
}
|
|
33209
33753
|
|
|
33754
|
+
// src/tool-perf.ts
|
|
33755
|
+
import { AsyncLocalStorage } from "node:async_hooks";
|
|
33756
|
+
var perfStore = new AsyncLocalStorage;
|
|
33757
|
+
function markBridgeStart() {
|
|
33758
|
+
const slot = perfStore.getStore();
|
|
33759
|
+
if (slot && slot.bridgeStart === undefined) {
|
|
33760
|
+
slot.bridgeStart = performance.now();
|
|
33761
|
+
}
|
|
33762
|
+
}
|
|
33763
|
+
function markBridgeEnd() {
|
|
33764
|
+
const slot = perfStore.getStore();
|
|
33765
|
+
if (slot) {
|
|
33766
|
+
slot.bridgeEnd = performance.now();
|
|
33767
|
+
}
|
|
33768
|
+
}
|
|
33769
|
+
function emit(slot) {
|
|
33770
|
+
const t3 = performance.now();
|
|
33771
|
+
const total = Math.round(t3 - slot.t0);
|
|
33772
|
+
if (slot.bridgeStart !== undefined && slot.bridgeEnd !== undefined) {
|
|
33773
|
+
const pre = Math.round(slot.bridgeStart - slot.t0);
|
|
33774
|
+
const bridge = Math.round(slot.bridgeEnd - slot.bridgeStart);
|
|
33775
|
+
const post = Math.round(t3 - slot.bridgeEnd);
|
|
33776
|
+
sessionLog(slot.sessionID, `perf tool=${slot.tool} total=${total}ms pre=${pre}ms bridge=${bridge}ms post=${post}ms`);
|
|
33777
|
+
} else {
|
|
33778
|
+
sessionLog(slot.sessionID, `perf tool=${slot.tool} total=${total}ms (no bridge call)`);
|
|
33779
|
+
}
|
|
33780
|
+
}
|
|
33781
|
+
function instrumentToolMap(tools) {
|
|
33782
|
+
for (const [name, def] of Object.entries(tools)) {
|
|
33783
|
+
const original = def.execute;
|
|
33784
|
+
if (typeof original !== "function")
|
|
33785
|
+
continue;
|
|
33786
|
+
def.execute = (args, context) => {
|
|
33787
|
+
const slot = {
|
|
33788
|
+
tool: name,
|
|
33789
|
+
sessionID: context?.sessionID,
|
|
33790
|
+
t0: performance.now()
|
|
33791
|
+
};
|
|
33792
|
+
return perfStore.run(slot, async () => {
|
|
33793
|
+
try {
|
|
33794
|
+
return await original(args, context);
|
|
33795
|
+
} finally {
|
|
33796
|
+
emit(slot);
|
|
33797
|
+
}
|
|
33798
|
+
});
|
|
33799
|
+
};
|
|
33800
|
+
}
|
|
33801
|
+
return tools;
|
|
33802
|
+
}
|
|
33803
|
+
|
|
33210
33804
|
// src/tools/ast.ts
|
|
33211
33805
|
import { tool as tool3 } from "@opencode-ai/plugin";
|
|
33212
33806
|
|
|
@@ -33229,19 +33823,6 @@ function isEmptyParam(value) {
|
|
|
33229
33823
|
return false;
|
|
33230
33824
|
}
|
|
33231
33825
|
var BASH_TRANSPORT_TIMEOUT_MS = 30000;
|
|
33232
|
-
var LONG_RUNNING_COMMAND_TIMEOUT_MS = {
|
|
33233
|
-
callers: 60000,
|
|
33234
|
-
trace_to: 60000,
|
|
33235
|
-
trace_to_symbol: 60000,
|
|
33236
|
-
trace_data: 60000,
|
|
33237
|
-
impact: 60000,
|
|
33238
|
-
grep: 60000,
|
|
33239
|
-
glob: 60000,
|
|
33240
|
-
semantic_search: 60000
|
|
33241
|
-
};
|
|
33242
|
-
function timeoutForCommand(command) {
|
|
33243
|
-
return LONG_RUNNING_COMMAND_TIMEOUT_MS[command];
|
|
33244
|
-
}
|
|
33245
33826
|
function asPlainObject(value) {
|
|
33246
33827
|
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
33247
33828
|
return;
|
|
@@ -33360,7 +33941,13 @@ async function callBridge(ctx, runtime, command, params = {}, options) {
|
|
|
33360
33941
|
configureWarningClient: ctx.client,
|
|
33361
33942
|
...options
|
|
33362
33943
|
};
|
|
33363
|
-
|
|
33944
|
+
markBridgeStart();
|
|
33945
|
+
let response;
|
|
33946
|
+
try {
|
|
33947
|
+
response = await bridgeFor(ctx, runtime).send(command, merged, Object.keys(sendOptions).length > 0 ? sendOptions : undefined);
|
|
33948
|
+
} finally {
|
|
33949
|
+
markBridgeEnd();
|
|
33950
|
+
}
|
|
33364
33951
|
ingestBgCompletions(runtime.sessionID, response.bg_completions);
|
|
33365
33952
|
return response;
|
|
33366
33953
|
}
|
|
@@ -33812,760 +34399,762 @@ ${hint}`;
|
|
|
33812
34399
|
};
|
|
33813
34400
|
}
|
|
33814
34401
|
|
|
33815
|
-
// src/tools/
|
|
34402
|
+
// src/tools/bash.ts
|
|
33816
34403
|
import { tool as tool4 } from "@opencode-ai/plugin";
|
|
33817
|
-
var z4 = tool4.schema;
|
|
33818
|
-
function conflictTools(ctx) {
|
|
33819
|
-
return {
|
|
33820
|
-
aft_conflicts: {
|
|
33821
|
-
description: "Show all git merge conflicts across the repository — returns line-numbered conflict regions with context for every conflicted file in a single call. Conflicts are discovered from the git repository's top level. By default it inspects the session's project repository; pass `path` to inspect a different repository or git worktree (e.g. where a rebase/merge is running).",
|
|
33822
|
-
args: {
|
|
33823
|
-
path: z4.string().describe("Optional path inside the git repository or worktree to inspect (absolute or relative to project root). Conflicts are discovered from that repository's top level. Defaults to the session project root.").optional()
|
|
33824
|
-
},
|
|
33825
|
-
execute: async (args, context) => {
|
|
33826
|
-
const params = {};
|
|
33827
|
-
if (!isEmptyParam(args?.path))
|
|
33828
|
-
params.path = args.path;
|
|
33829
|
-
const response = await callBridge(ctx, context, "git_conflicts", params);
|
|
33830
|
-
if (response.success === false) {
|
|
33831
|
-
throw new Error(response.message || "git_conflicts failed");
|
|
33832
|
-
}
|
|
33833
|
-
return response.text;
|
|
33834
|
-
}
|
|
33835
|
-
}
|
|
33836
|
-
};
|
|
33837
|
-
}
|
|
33838
34404
|
|
|
33839
|
-
// src/
|
|
33840
|
-
|
|
33841
|
-
|
|
33842
|
-
|
|
33843
|
-
|
|
33844
|
-
|
|
33845
|
-
|
|
33846
|
-
const heredocMatch = input.match(/^(?:cat\s+)?<<['"]?(\w+)['"]?\s*\n([\s\S]*?)\n\1\s*$/);
|
|
33847
|
-
return heredocMatch ? heredocMatch[2] : input;
|
|
33848
|
-
}
|
|
33849
|
-
function parsePatchHeader(lines, startIdx) {
|
|
33850
|
-
const line = lines[startIdx];
|
|
33851
|
-
if (line.startsWith("*** Add File:")) {
|
|
33852
|
-
const filePath = line.slice("*** Add File:".length).trim();
|
|
33853
|
-
return filePath ? { filePath, nextIdx: startIdx + 1 } : null;
|
|
34405
|
+
// src/shared/subagent-detect.ts
|
|
34406
|
+
var CACHE_MAX_ENTRIES2 = 200;
|
|
34407
|
+
var cache2 = new Map;
|
|
34408
|
+
async function resolveIsSubagent(client, sessionId, _fallbackDirectory) {
|
|
34409
|
+
if (!sessionId) {
|
|
34410
|
+
sessionLog(undefined, "[subagent-detect] no sessionId provided → primary");
|
|
34411
|
+
return false;
|
|
33854
34412
|
}
|
|
33855
|
-
|
|
33856
|
-
|
|
33857
|
-
|
|
34413
|
+
const cached2 = cache2.get(sessionId);
|
|
34414
|
+
if (cached2) {
|
|
34415
|
+
cache2.delete(sessionId);
|
|
34416
|
+
cache2.set(sessionId, cached2);
|
|
34417
|
+
return cached2.isSubagent;
|
|
33858
34418
|
}
|
|
33859
|
-
|
|
33860
|
-
|
|
33861
|
-
|
|
33862
|
-
|
|
33863
|
-
|
|
33864
|
-
|
|
33865
|
-
nextIdx++;
|
|
33866
|
-
}
|
|
33867
|
-
return filePath ? { filePath, movePath, nextIdx } : null;
|
|
34419
|
+
const c = client;
|
|
34420
|
+
const sessionApi = c?.session;
|
|
34421
|
+
if (!sessionApi || typeof sessionApi.get !== "function") {
|
|
34422
|
+
sessionLog(sessionId, `[subagent-detect] client.session.get unavailable (client=${typeof client}, session=${typeof sessionApi}, get=${typeof sessionApi?.get}) → caching as primary`);
|
|
34423
|
+
setCache2(sessionId, false);
|
|
34424
|
+
return false;
|
|
33868
34425
|
}
|
|
33869
|
-
|
|
33870
|
-
|
|
33871
|
-
|
|
33872
|
-
|
|
33873
|
-
|
|
33874
|
-
|
|
33875
|
-
|
|
33876
|
-
|
|
33877
|
-
|
|
33878
|
-
|
|
33879
|
-
|
|
34426
|
+
sessionLog(sessionId, `[subagent-detect] cache miss, calling client.session.get(id=${sessionId})`);
|
|
34427
|
+
let isSubagent = false;
|
|
34428
|
+
let parentIdRaw;
|
|
34429
|
+
try {
|
|
34430
|
+
const result = await sessionApi.get({
|
|
34431
|
+
path: { id: sessionId }
|
|
34432
|
+
});
|
|
34433
|
+
const session = result?.data ?? result;
|
|
34434
|
+
parentIdRaw = session?.parentID;
|
|
34435
|
+
isSubagent = session !== undefined && typeof session.parentID === "string" && session.parentID.length > 0;
|
|
34436
|
+
sessionLog(sessionId, `[subagent-detect] SDK returned session=${session !== undefined ? "present" : "undefined"}, parentID=${JSON.stringify(parentIdRaw)} → isSubagent=${isSubagent}`);
|
|
34437
|
+
} catch (err) {
|
|
34438
|
+
sessionWarn(sessionId, `[subagent-detect] SDK lookup failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
34439
|
+
return false;
|
|
33880
34440
|
}
|
|
33881
|
-
|
|
33882
|
-
|
|
33883
|
-
|
|
34441
|
+
setCache2(sessionId, isSubagent);
|
|
34442
|
+
return isSubagent;
|
|
34443
|
+
}
|
|
34444
|
+
function setCache2(sessionId, isSubagent) {
|
|
34445
|
+
if (cache2.has(sessionId))
|
|
34446
|
+
cache2.delete(sessionId);
|
|
34447
|
+
cache2.set(sessionId, { isSubagent, recordedAt: Date.now() });
|
|
34448
|
+
if (cache2.size > CACHE_MAX_ENTRIES2) {
|
|
34449
|
+
const oldest = cache2.keys().next().value;
|
|
34450
|
+
if (oldest !== undefined)
|
|
34451
|
+
cache2.delete(oldest);
|
|
33884
34452
|
}
|
|
33885
|
-
return { content, nextIdx: i };
|
|
33886
34453
|
}
|
|
33887
|
-
|
|
33888
|
-
|
|
33889
|
-
|
|
33890
|
-
|
|
33891
|
-
|
|
33892
|
-
|
|
33893
|
-
|
|
33894
|
-
|
|
33895
|
-
|
|
33896
|
-
|
|
33897
|
-
|
|
33898
|
-
|
|
33899
|
-
if (changeLine === "*** End of File") {
|
|
33900
|
-
isEndOfFile = true;
|
|
33901
|
-
i++;
|
|
33902
|
-
break;
|
|
33903
|
-
}
|
|
33904
|
-
if (changeLine.startsWith(" ")) {
|
|
33905
|
-
const content = changeLine.substring(1);
|
|
33906
|
-
oldLines.push(content);
|
|
33907
|
-
newLines.push(content);
|
|
33908
|
-
} else if (changeLine.startsWith("-")) {
|
|
33909
|
-
oldLines.push(changeLine.substring(1));
|
|
33910
|
-
} else if (changeLine.startsWith("+")) {
|
|
33911
|
-
newLines.push(changeLine.substring(1));
|
|
33912
|
-
}
|
|
33913
|
-
i++;
|
|
33914
|
-
}
|
|
33915
|
-
chunks.push({
|
|
33916
|
-
old_lines: oldLines,
|
|
33917
|
-
new_lines: newLines,
|
|
33918
|
-
change_context: contextLine || undefined,
|
|
33919
|
-
is_end_of_file: isEndOfFile || undefined
|
|
33920
|
-
});
|
|
33921
|
-
} else {
|
|
33922
|
-
i++;
|
|
33923
|
-
}
|
|
34454
|
+
|
|
34455
|
+
// src/tools/bash.ts
|
|
34456
|
+
var z4 = tool4.schema;
|
|
34457
|
+
var METADATA_PREVIEW_LIMIT = 30 * 1024;
|
|
34458
|
+
var FOREGROUND_POLL_INTERVAL_MS = 100;
|
|
34459
|
+
var DEFAULT_HARD_TIMEOUT_MS = 30 * 60 * 1000;
|
|
34460
|
+
function resolveForegroundWaitMs(configured) {
|
|
34461
|
+
const override = process.env.AFT_TEST_FOREGROUND_WAIT_MS;
|
|
34462
|
+
if (override !== undefined) {
|
|
34463
|
+
const parsed = Number(override);
|
|
34464
|
+
if (Number.isFinite(parsed) && parsed >= 0)
|
|
34465
|
+
return parsed;
|
|
33924
34466
|
}
|
|
33925
|
-
return
|
|
34467
|
+
return configured;
|
|
33926
34468
|
}
|
|
33927
|
-
|
|
33928
|
-
|
|
33929
|
-
|
|
33930
|
-
|
|
33931
|
-
|
|
34469
|
+
function bashToolDescription(aftSearchRegistered, compressionOn, backgroundOn) {
|
|
34470
|
+
const searchSteer = aftSearchRegistered ? "use aft_search (concepts, identifiers, regex, literals), read, aft_outline, or aft_zoom instead" : "use the grep tool, read, aft_outline, or aft_zoom instead";
|
|
34471
|
+
const compression = compressionOn ? " Output is compressed by default; pass compressed: false for raw output." : "";
|
|
34472
|
+
const tasks = backgroundOn ? ' Pass background: true to run in the background and get a taskId for bash_status/bash_kill. Pass pty: true for interactive programs (REPLs, TUIs) and drive them with bash_status({ outputMode: "screen" }) plus bash_write (pty implies background automatically).' : " Commands that outlive the foreground wait window are promoted to background tasks; inspect them with bash_status({ taskId }) or terminate with bash_kill.";
|
|
34473
|
+
return `Execute shell commands.${compression}${tasks} Use bash_watch to wait for output patterns or exit events.
|
|
34474
|
+
|
|
34475
|
+
DO NOT use bash for code search or code exploration. If you are about to run grep, rg, sed, awk, find, or cat through bash to locate or read code: STOP — ${searchSteer}.`;
|
|
34476
|
+
}
|
|
34477
|
+
async function withPermissionLoop(ctx, runtime, params, bridgeCall, options) {
|
|
34478
|
+
const first = await bridgeCall(ctx, runtime, "bash", params, options);
|
|
34479
|
+
if (first.success !== false || first.code !== "permission_required")
|
|
34480
|
+
return first;
|
|
34481
|
+
const asks = Array.isArray(first.asks) ? first.asks : [];
|
|
34482
|
+
const permissionsGranted = [];
|
|
34483
|
+
for (const ask of asks) {
|
|
34484
|
+
const permission = ask.kind === "external_directory" ? "external_directory" : "bash";
|
|
34485
|
+
await runAsk(runtime.ask({
|
|
34486
|
+
permission,
|
|
34487
|
+
patterns: ask.patterns,
|
|
34488
|
+
always: ask.always,
|
|
34489
|
+
metadata: {}
|
|
34490
|
+
}));
|
|
34491
|
+
permissionsGranted.push(...ask.always.length > 0 ? ask.always : ask.patterns);
|
|
33932
34492
|
}
|
|
33933
|
-
const
|
|
33934
|
-
|
|
33935
|
-
|
|
33936
|
-
const hunks = [];
|
|
33937
|
-
const beginIdx = lines.findIndex((line) => line.trim() === "*** Begin Patch");
|
|
33938
|
-
const endIdx = lines.findIndex((line) => line.trim() === "*** End Patch");
|
|
33939
|
-
if (beginIdx === -1 || endIdx === -1 || beginIdx >= endIdx) {
|
|
33940
|
-
throw new Error("Invalid patch format: missing *** Begin Patch / *** End Patch markers");
|
|
34493
|
+
const second = await bridgeCall(ctx, runtime, "bash", { ...params, permissions_granted: permissionsGranted }, options);
|
|
34494
|
+
if (second.success === false && second.code === "permission_required") {
|
|
34495
|
+
throw new Error("bash permission retry failed");
|
|
33941
34496
|
}
|
|
33942
|
-
|
|
33943
|
-
|
|
33944
|
-
|
|
33945
|
-
|
|
33946
|
-
|
|
33947
|
-
|
|
33948
|
-
|
|
33949
|
-
|
|
33950
|
-
|
|
33951
|
-
|
|
33952
|
-
|
|
33953
|
-
|
|
33954
|
-
|
|
33955
|
-
|
|
33956
|
-
|
|
33957
|
-
|
|
33958
|
-
|
|
33959
|
-
|
|
33960
|
-
|
|
33961
|
-
|
|
33962
|
-
|
|
33963
|
-
|
|
33964
|
-
|
|
33965
|
-
|
|
34497
|
+
return second;
|
|
34498
|
+
}
|
|
34499
|
+
function createBashTool(ctx, aftSearchRegisteredOverride) {
|
|
34500
|
+
return {
|
|
34501
|
+
description: (() => {
|
|
34502
|
+
const cfg = resolveBashConfig(ctx.config);
|
|
34503
|
+
return bashToolDescription(false, cfg.compress, cfg.background);
|
|
34504
|
+
})(),
|
|
34505
|
+
args: {
|
|
34506
|
+
command: z4.string().describe("Shell command to execute. Supports pipes, redirection, and normal shell syntax."),
|
|
34507
|
+
timeout: optionalInt(1, Number.MAX_SAFE_INTEGER).describe("Hard kill cap in milliseconds (positive integer). When omitted, the task can run up to 30 minutes. Foreground bash returns inline if the command finishes within ~8s (configurable via bash.foreground_wait_window_ms); otherwise it's automatically promoted to background and a completion reminder is delivered when the task actually finishes."),
|
|
34508
|
+
workdir: z4.string().optional().describe("Working directory for command execution. Relative paths resolve through the bridge; defaults to the current tool context/project root when omitted."),
|
|
34509
|
+
description: z4.string().optional().describe("Short 5-10 word human-readable summary shown in OpenCode UI metadata instead of raw shell syntax."),
|
|
34510
|
+
background: z4.boolean().optional().describe("When true, spawn the command in the background and return a taskId for bash_status/bash_kill instead of waiting for completion. Defaults to false."),
|
|
34511
|
+
compressed: z4.boolean().optional().describe("When true or omitted, return compressed output with noisy terminal control sequences reduced. Set to false for raw output."),
|
|
34512
|
+
pty: z4.boolean().optional().describe('When true, spawn the command in a real PTY for interactive programs (python/node/bash REPLs, vim). Implies background: true automatically. Unavailable in subagent sessions. Inspect with bash_status({ taskId, outputMode: "screen" }) and drive interactively with bash_write — its input accepts either a string OR an array like [ "iHello", { key: "esc" }, ":wq", { key: "enter" } ] for atomic text+key sequences.'),
|
|
34513
|
+
ptyRows: optionalInt(1, 60).describe("PTY terminal height in rows — ignored when pty is false. Defaults to 24 when pty: true. Minimum 1, maximum 60."),
|
|
34514
|
+
ptyCols: optionalInt(1, 140).describe("PTY terminal width in columns — ignored when pty is false. Defaults to 80 when pty: true. Minimum 1, maximum 140.")
|
|
34515
|
+
},
|
|
34516
|
+
execute: async (args, context) => {
|
|
34517
|
+
const bashCfg = resolveBashConfig(ctx.config);
|
|
34518
|
+
const ctxAftSearchRegistered = ctx.aftSearchRegistered === true;
|
|
34519
|
+
const aftSearchRegistered = aftSearchRegisteredOverride ?? ctxAftSearchRegistered;
|
|
34520
|
+
let accumulatedOutput = "";
|
|
34521
|
+
const description = args.description;
|
|
34522
|
+
const metadata = context.metadata;
|
|
34523
|
+
const rawCommand = args.command;
|
|
34524
|
+
const compressionEnabled = bashCfg.compress && args.compressed !== false;
|
|
34525
|
+
const pipeStrip = maybeStripCompressorPipe(rawCommand, compressionEnabled);
|
|
34526
|
+
const command = pipeStrip.command;
|
|
34527
|
+
const cwd = args.workdir ?? context.directory;
|
|
34528
|
+
const isSubagent = await resolveIsSubagent(ctx.client, context.sessionID, context.directory);
|
|
34529
|
+
const requestedPty = args.pty === true;
|
|
34530
|
+
const requestedBackground = args.background === true || requestedPty;
|
|
34531
|
+
if (requestedPty && isSubagent) {
|
|
34532
|
+
throw new Error("PTY mode is not available in subagent sessions; subagents cannot drive interactive terminals.");
|
|
34533
|
+
}
|
|
34534
|
+
const allowSubagentBg = bashCfg.subagent_background;
|
|
34535
|
+
const subagentForcedForeground = isSubagent && !allowSubagentBg;
|
|
34536
|
+
const effectiveBackground = subagentForcedForeground ? false : requestedBackground;
|
|
34537
|
+
const rawTimeout = args.timeout;
|
|
34538
|
+
const effectiveTimeout = effectiveBackground ? rawTimeout : resolveBashKillTimeout(rawTimeout, bashCfg.foreground_wait_window_ms);
|
|
34539
|
+
if (subagentForcedForeground && requestedBackground) {
|
|
34540
|
+
sessionLog(context.sessionID, "[bash] subagent + background:true → converting to foreground (subagent would lose task_id)");
|
|
34541
|
+
}
|
|
34542
|
+
const shellEnv = await ctx.plugin?.trigger?.("shell.env", { cwd, sessionID: context.sessionID, callID: getCallID(context) }, { env: {} });
|
|
34543
|
+
const data = await withPermissionLoop(ctx, context, {
|
|
34544
|
+
command,
|
|
34545
|
+
timeout: effectiveTimeout,
|
|
34546
|
+
workdir: args.workdir,
|
|
34547
|
+
env: shellEnv?.env ?? {},
|
|
34548
|
+
description,
|
|
34549
|
+
background: effectiveBackground,
|
|
34550
|
+
notify_on_completion: effectiveBackground,
|
|
34551
|
+
compressed: args.compressed,
|
|
34552
|
+
pty: requestedPty,
|
|
34553
|
+
pty_rows: args.ptyRows,
|
|
34554
|
+
pty_cols: args.ptyCols,
|
|
34555
|
+
permissions_requested: true
|
|
34556
|
+
}, callBashBridge, {
|
|
34557
|
+
onProgress: ({ text }) => {
|
|
34558
|
+
accumulatedOutput = preview(accumulatedOutput + text);
|
|
34559
|
+
metadata?.({ output: accumulatedOutput, description });
|
|
34560
|
+
}
|
|
33966
34561
|
});
|
|
33967
|
-
|
|
33968
|
-
|
|
33969
|
-
|
|
34562
|
+
if (data.success === false) {
|
|
34563
|
+
throw new Error(data.message || "bash failed");
|
|
34564
|
+
}
|
|
34565
|
+
if (data.status === "running" && typeof data.task_id === "string") {
|
|
34566
|
+
const taskId = data.task_id;
|
|
34567
|
+
const uiTitle = description ?? shortenCommand(command);
|
|
34568
|
+
if (effectiveBackground) {
|
|
34569
|
+
trackBgTask(context.sessionID, taskId);
|
|
34570
|
+
let startedLine = formatBackgroundLaunch(taskId, requestedPty);
|
|
34571
|
+
if (isSubagent && allowSubagentBg)
|
|
34572
|
+
startedLine += subagentGuidance(taskId);
|
|
34573
|
+
startedLine = appendPipeStripNote(startedLine, pipeStrip.note);
|
|
34574
|
+
const metadataPayload2 = { description, output: startedLine, status: "running", taskId };
|
|
34575
|
+
metadata?.(metadataPayload2);
|
|
34576
|
+
return { output: startedLine, title: uiTitle, metadata: metadataPayload2 };
|
|
34577
|
+
}
|
|
34578
|
+
const foregroundWaitMs = resolveForegroundWaitMs(bashCfg.foreground_wait_window_ms);
|
|
34579
|
+
const waitTimeoutMs = subagentForcedForeground ? effectiveTimeout ?? DEFAULT_HARD_TIMEOUT_MS : effectiveTimeout !== undefined ? Math.min(effectiveTimeout, foregroundWaitMs) : foregroundWaitMs;
|
|
34580
|
+
const startedAt = Date.now();
|
|
34581
|
+
while (true) {
|
|
34582
|
+
const status = await callBashBridge(ctx, context, "bash_status", { task_id: taskId });
|
|
34583
|
+
if (status.success === false) {
|
|
34584
|
+
throw new Error(status.message ?? "bash_status failed");
|
|
34585
|
+
}
|
|
34586
|
+
if (isTerminalStatus(status.status)) {
|
|
34587
|
+
const rendered2 = maybeAppendGrepSearchHint(appendPipeStripNote(formatForegroundResult(status), pipeStrip.note), command, aftSearchRegistered);
|
|
34588
|
+
const metadataPayload2 = foregroundMetadata(description, status, rendered2);
|
|
34589
|
+
metadata?.(metadataPayload2);
|
|
34590
|
+
return { output: rendered2, title: uiTitle, metadata: metadataPayload2 };
|
|
34591
|
+
}
|
|
34592
|
+
if (Date.now() - startedAt >= waitTimeoutMs) {
|
|
34593
|
+
if (subagentForcedForeground) {
|
|
34594
|
+
await sleep(FOREGROUND_POLL_INTERVAL_MS);
|
|
34595
|
+
continue;
|
|
34596
|
+
}
|
|
34597
|
+
const promoted = await callBashBridge(ctx, context, "bash_promote", {
|
|
34598
|
+
task_id: taskId
|
|
34599
|
+
});
|
|
34600
|
+
if (promoted.success === false) {
|
|
34601
|
+
throw new Error(promoted.message ?? "bash_promote failed");
|
|
34602
|
+
}
|
|
34603
|
+
trackBgTask(context.sessionID, taskId);
|
|
34604
|
+
let message = formatPromotionMessage(taskId, effectiveTimeout, foregroundWaitMs);
|
|
34605
|
+
if (isSubagent && allowSubagentBg)
|
|
34606
|
+
message += subagentGuidance(taskId);
|
|
34607
|
+
message = appendPipeStripNote(message, pipeStrip.note);
|
|
34608
|
+
const metadataPayload2 = { description, output: message, status: "running", taskId };
|
|
34609
|
+
metadata?.(metadataPayload2);
|
|
34610
|
+
return { output: message, title: uiTitle, metadata: metadataPayload2 };
|
|
34611
|
+
}
|
|
34612
|
+
await sleep(FOREGROUND_POLL_INTERVAL_MS);
|
|
34613
|
+
}
|
|
34614
|
+
}
|
|
34615
|
+
const output = data.output ?? "";
|
|
34616
|
+
const metadataOutput = preview(output);
|
|
34617
|
+
const exit = data.exit_code;
|
|
34618
|
+
const truncated = data.truncated;
|
|
34619
|
+
const outputPath = data.output_path;
|
|
34620
|
+
const timedOut = data.timed_out === true;
|
|
34621
|
+
const metadataPayload = {
|
|
34622
|
+
description,
|
|
34623
|
+
output: metadataOutput,
|
|
34624
|
+
exit,
|
|
34625
|
+
truncated,
|
|
34626
|
+
...outputPath ? { outputPath } : {}
|
|
34627
|
+
};
|
|
34628
|
+
metadata?.(metadataPayload);
|
|
34629
|
+
let rendered = output;
|
|
34630
|
+
if (truncated && outputPath) {
|
|
34631
|
+
rendered += `
|
|
34632
|
+
[output truncated; full output at ${outputPath}]`;
|
|
34633
|
+
}
|
|
34634
|
+
if (timedOut) {
|
|
34635
|
+
rendered += `
|
|
34636
|
+
[command timed out]`;
|
|
34637
|
+
}
|
|
34638
|
+
if (typeof exit === "number" && exit !== 0) {
|
|
34639
|
+
rendered += `
|
|
34640
|
+
[exit code: ${exit}]`;
|
|
34641
|
+
}
|
|
34642
|
+
rendered = appendPipeStripNote(rendered, pipeStrip.note);
|
|
34643
|
+
return {
|
|
34644
|
+
output: rendered,
|
|
34645
|
+
title: description ?? shortenCommand(command),
|
|
34646
|
+
metadata: metadataPayload
|
|
34647
|
+
};
|
|
33970
34648
|
}
|
|
33971
|
-
}
|
|
33972
|
-
return hunks;
|
|
34649
|
+
};
|
|
33973
34650
|
}
|
|
33974
|
-
function
|
|
33975
|
-
return
|
|
34651
|
+
function appendPipeStripNote(output, note) {
|
|
34652
|
+
return note ? `${output}
|
|
34653
|
+
|
|
34654
|
+
${note}` : output;
|
|
33976
34655
|
}
|
|
33977
|
-
function
|
|
33978
|
-
return
|
|
34656
|
+
function createBashStatusTool(ctx) {
|
|
34657
|
+
return {
|
|
34658
|
+
description: "Read-only snapshot of a background or PTY bash task's current state and output. Returns immediately. Never waits. Use bash_watch to block on or register for pattern matches and exit events.",
|
|
34659
|
+
args: {
|
|
34660
|
+
taskId: z4.string().describe("Background task ID returned by bash({ background: true }), e.g. bash-6b454047."),
|
|
34661
|
+
outputMode: z4.enum(["screen", "raw", "both"]).optional().describe("PTY output rendering mode. Defaults to screen for PTY tasks and preserves existing behavior for piped tasks when omitted.")
|
|
34662
|
+
},
|
|
34663
|
+
execute: async (args, context) => {
|
|
34664
|
+
const taskId = args.taskId;
|
|
34665
|
+
const outputMode = args.outputMode;
|
|
34666
|
+
const data = await bashStatusSnapshot(ctx, context, taskId, outputMode);
|
|
34667
|
+
return await formatBashStatusText(context, taskId, data, outputMode);
|
|
34668
|
+
}
|
|
34669
|
+
};
|
|
33979
34670
|
}
|
|
33980
|
-
function
|
|
33981
|
-
|
|
33982
|
-
|
|
33983
|
-
|
|
33984
|
-
|
|
33985
|
-
|
|
33986
|
-
|
|
33987
|
-
|
|
33988
|
-
|
|
33989
|
-
|
|
34671
|
+
function createBashKillTool(ctx) {
|
|
34672
|
+
return {
|
|
34673
|
+
description: "Terminate a running background bash task spawned with bash({ background: true }). Returns confirmation of kill or an error if the task already finished.",
|
|
34674
|
+
args: {
|
|
34675
|
+
taskId: z4.string().describe("Background task ID returned by bash({ background: true }), e.g. bash-6b454047.")
|
|
34676
|
+
},
|
|
34677
|
+
execute: async (args, context) => {
|
|
34678
|
+
const data = await callBashBridge(ctx, context, "bash_kill", {
|
|
34679
|
+
task_id: args.taskId
|
|
34680
|
+
});
|
|
34681
|
+
if (data.success === false) {
|
|
34682
|
+
throw new Error(data.message ?? "bash_kill failed");
|
|
33990
34683
|
}
|
|
33991
|
-
|
|
33992
|
-
|
|
33993
|
-
|
|
33994
|
-
}
|
|
33995
|
-
for (let i = startIndex;i <= lines.length - pattern.length; i++) {
|
|
33996
|
-
let matches = true;
|
|
33997
|
-
for (let j = 0;j < pattern.length; j++) {
|
|
33998
|
-
if (!compare(lines[i + j], pattern[j])) {
|
|
33999
|
-
matches = false;
|
|
34000
|
-
break;
|
|
34684
|
+
await disposePtyTerminal(ptyCacheKey(context, args.taskId));
|
|
34685
|
+
if (data.kill_signaled === true) {
|
|
34686
|
+
return `Task ${args.taskId}: kill_signaled`;
|
|
34001
34687
|
}
|
|
34688
|
+
return `Task ${args.taskId}: ${String(data.status ?? "killed")}`;
|
|
34002
34689
|
}
|
|
34003
|
-
|
|
34004
|
-
|
|
34690
|
+
};
|
|
34691
|
+
}
|
|
34692
|
+
async function bashStatusSnapshot(ctx, runtime, taskId, outputMode, options) {
|
|
34693
|
+
const data = await callBashBridge(ctx, runtime, "bash_status", { task_id: taskId, output_mode: outputMode }, options);
|
|
34694
|
+
if (data.success === false) {
|
|
34695
|
+
throw new Error(data.message ?? "bash_status failed");
|
|
34005
34696
|
}
|
|
34006
|
-
return
|
|
34697
|
+
return data;
|
|
34007
34698
|
}
|
|
34008
|
-
function
|
|
34009
|
-
|
|
34010
|
-
|
|
34011
|
-
const
|
|
34012
|
-
|
|
34013
|
-
|
|
34014
|
-
|
|
34015
|
-
|
|
34016
|
-
|
|
34017
|
-
|
|
34018
|
-
|
|
34019
|
-
|
|
34020
|
-
|
|
34021
|
-
|
|
34022
|
-
|
|
34023
|
-
|
|
34024
|
-
|
|
34025
|
-
|
|
34026
|
-
return
|
|
34699
|
+
async function formatBashStatusText(runtime, taskId, data, requestedOutputMode) {
|
|
34700
|
+
const status = data.status;
|
|
34701
|
+
const exit = typeof data.exit_code === "number" ? ` (exit ${data.exit_code})` : "";
|
|
34702
|
+
const dur = typeof data.duration_ms === "number" ? ` ${Math.round(data.duration_ms / 1000)}s` : "";
|
|
34703
|
+
let text = `Task ${taskId}: ${status}${exit}${dur}`;
|
|
34704
|
+
if (data.mode === "pty") {
|
|
34705
|
+
text += await formatPtyStatus(runtime, taskId, data, requestedOutputMode);
|
|
34706
|
+
} else {
|
|
34707
|
+
const preview = data.output_preview;
|
|
34708
|
+
if (preview && status !== "running") {
|
|
34709
|
+
text += `
|
|
34710
|
+
${preview}`;
|
|
34711
|
+
}
|
|
34712
|
+
if (status === "running") {
|
|
34713
|
+
text += `
|
|
34714
|
+
A completion reminder will be delivered automatically; don't poll.`;
|
|
34715
|
+
}
|
|
34716
|
+
}
|
|
34717
|
+
return text;
|
|
34027
34718
|
}
|
|
34028
|
-
function
|
|
34029
|
-
const
|
|
34030
|
-
|
|
34031
|
-
|
|
34032
|
-
|
|
34033
|
-
|
|
34034
|
-
|
|
34035
|
-
const
|
|
34036
|
-
const
|
|
34037
|
-
|
|
34038
|
-
|
|
34039
|
-
|
|
34719
|
+
async function formatPtyStatus(runtime, taskId, data, requestedOutputMode) {
|
|
34720
|
+
const outputPath = data.output_path;
|
|
34721
|
+
if (!outputPath)
|
|
34722
|
+
return `
|
|
34723
|
+
[PTY output path unavailable]`;
|
|
34724
|
+
const key = ptyCacheKey(runtime, taskId);
|
|
34725
|
+
const { rows, cols } = ptyDimensions(data);
|
|
34726
|
+
const state = await getOrCreatePtyTerminal(key, outputPath, rows, cols);
|
|
34727
|
+
const raw = await readPtyBytes(state);
|
|
34728
|
+
const outputMode = requestedOutputMode ?? "screen";
|
|
34729
|
+
let suffix = "";
|
|
34730
|
+
if (outputMode === "raw") {
|
|
34731
|
+
suffix = raw.length > 0 ? `
|
|
34732
|
+
${raw.toString("utf8")}` : "";
|
|
34733
|
+
} else if (outputMode === "both") {
|
|
34734
|
+
suffix = `
|
|
34735
|
+
${JSON.stringify({ screen: renderScreen(state, rows, cols), raw: raw.toString("utf8") }, null, 2)}`;
|
|
34736
|
+
} else {
|
|
34737
|
+
const screen = renderScreen(state, rows, cols);
|
|
34738
|
+
suffix = screen ? `
|
|
34739
|
+
${screen}` : "";
|
|
34040
34740
|
}
|
|
34041
|
-
if (
|
|
34042
|
-
|
|
34043
|
-
|
|
34044
|
-
|
|
34045
|
-
|
|
34046
|
-
for (let j = 0;j < pattern.length && start + j < lines.length; j++) {
|
|
34047
|
-
if (!compareAny(lines[start + j], pattern[j]))
|
|
34048
|
-
break;
|
|
34049
|
-
matched++;
|
|
34050
|
-
}
|
|
34051
|
-
if (matched > best.matchedLines) {
|
|
34052
|
-
best = {
|
|
34053
|
-
lineNumber: start + 1,
|
|
34054
|
-
matchedLines: matched,
|
|
34055
|
-
firstDivergence: matched
|
|
34056
|
-
};
|
|
34057
|
-
}
|
|
34741
|
+
if (data.status === "running") {
|
|
34742
|
+
suffix += `
|
|
34743
|
+
PTY task is still running. Use bash_status({ taskId: "${taskId}", outputMode: "screen" }) to inspect, bash_write({ taskId: "${taskId}", input: "..." }) to send keystrokes.`;
|
|
34744
|
+
} else if (isTerminalStatus(data.status)) {
|
|
34745
|
+
await disposePtyTerminal(key);
|
|
34058
34746
|
}
|
|
34059
|
-
return
|
|
34747
|
+
return suffix;
|
|
34060
34748
|
}
|
|
34061
|
-
function
|
|
34062
|
-
const
|
|
34063
|
-
|
|
34064
|
-
|
|
34065
|
-
originalLines.pop();
|
|
34066
|
-
}
|
|
34067
|
-
const replacements = [];
|
|
34068
|
-
let lineIndex = 0;
|
|
34069
|
-
for (const chunk of chunks) {
|
|
34070
|
-
if (chunk.change_context) {
|
|
34071
|
-
const contextIdx = seekSequence(originalLines, [chunk.change_context], lineIndex);
|
|
34072
|
-
if (contextIdx === -1) {
|
|
34073
|
-
throw new Error(`Failed to find context '${chunk.change_context}' in ${filePath}`);
|
|
34074
|
-
}
|
|
34075
|
-
lineIndex = contextIdx + 1;
|
|
34076
|
-
}
|
|
34077
|
-
if (chunk.old_lines.length === 0) {
|
|
34078
|
-
let insertionIdx;
|
|
34079
|
-
if (chunk.change_context) {
|
|
34080
|
-
insertionIdx = lineIndex;
|
|
34081
|
-
} else if (originalLines.length > 0 && originalLines[originalLines.length - 1] === "") {
|
|
34082
|
-
insertionIdx = originalLines.length - 1;
|
|
34083
|
-
} else {
|
|
34084
|
-
insertionIdx = originalLines.length;
|
|
34085
|
-
}
|
|
34086
|
-
replacements.push([insertionIdx, 0, chunk.new_lines]);
|
|
34087
|
-
continue;
|
|
34088
|
-
}
|
|
34089
|
-
let pattern = chunk.old_lines;
|
|
34090
|
-
let newSlice = chunk.new_lines;
|
|
34091
|
-
let found = seekSequence(originalLines, pattern, lineIndex, chunk.is_end_of_file);
|
|
34092
|
-
if (found === -1 && pattern.length > 0 && pattern[pattern.length - 1] === "") {
|
|
34093
|
-
pattern = pattern.slice(0, -1);
|
|
34094
|
-
if (newSlice.length > 0 && newSlice[newSlice.length - 1] === "") {
|
|
34095
|
-
newSlice = newSlice.slice(0, -1);
|
|
34096
|
-
}
|
|
34097
|
-
found = seekSequence(originalLines, pattern, lineIndex, chunk.is_end_of_file);
|
|
34098
|
-
}
|
|
34099
|
-
if (found !== -1) {
|
|
34100
|
-
replacements.push([found, pattern.length, newSlice]);
|
|
34101
|
-
lineIndex = found + pattern.length;
|
|
34102
|
-
} else {
|
|
34103
|
-
const newSliceTrimmed = newSlice.filter((line) => line.trim().length > 0);
|
|
34104
|
-
const alreadyApplied = newSliceTrimmed.length > 0 && seekSequence(originalLines, newSliceTrimmed, 0, chunk.is_end_of_file) !== -1;
|
|
34105
|
-
const closest = findClosestPartialMatch(originalLines, pattern);
|
|
34106
|
-
let closestHint = "";
|
|
34107
|
-
if (closest && closest.matchedLines > 0) {
|
|
34108
|
-
const fileLineNo = closest.lineNumber + closest.firstDivergence;
|
|
34109
|
-
const expectedLine = pattern[closest.firstDivergence];
|
|
34110
|
-
const actualLine = fileLineNo - 1 < originalLines.length ? originalLines[fileLineNo - 1] : "<EOF>";
|
|
34111
|
-
closestHint = `
|
|
34112
|
-
|
|
34113
|
-
Closest match starts at line ${closest.lineNumber} ` + `(${closest.matchedLines} of ${pattern.length} lines matched).
|
|
34114
|
-
` + `First divergence at line ${fileLineNo}:
|
|
34115
|
-
` + ` expected: ${JSON.stringify(expectedLine)}
|
|
34116
|
-
` + ` actual: ${JSON.stringify(actualLine)}`;
|
|
34117
|
-
}
|
|
34118
|
-
const triedTiers = "exact, trimEnd, trim, indent (tab/space), unicode";
|
|
34119
|
-
const alreadyAppliedHint = alreadyApplied ? `
|
|
34120
|
-
|
|
34121
|
-
Hint: the replacement content for this hunk already appears in the file. ` + "The patch may have been partially applied in a prior turn — re-read the file " + "to confirm which hunks still need to apply." : "";
|
|
34122
|
-
throw new Error(`Failed to find expected lines in ${filePath}:
|
|
34123
|
-
${chunk.old_lines.join(`
|
|
34124
|
-
`)}
|
|
34125
|
-
|
|
34126
|
-
` + `Tried match tiers: ${triedTiers}.${closestHint}${alreadyAppliedHint}`);
|
|
34127
|
-
}
|
|
34128
|
-
}
|
|
34129
|
-
replacements.sort((a, b) => a[0] - b[0]);
|
|
34130
|
-
const result = [...originalLines];
|
|
34131
|
-
for (let i = replacements.length - 1;i >= 0; i--) {
|
|
34132
|
-
const [startIdx, oldLen, newSegment] = replacements[i];
|
|
34133
|
-
result.splice(startIdx, oldLen, ...newSegment);
|
|
34134
|
-
}
|
|
34135
|
-
if (result.length === 0 || result[result.length - 1] !== "") {
|
|
34136
|
-
result.push("");
|
|
34137
|
-
}
|
|
34138
|
-
return result.join(`
|
|
34139
|
-
`);
|
|
34749
|
+
function ptyDimensions(data) {
|
|
34750
|
+
const rows = typeof data.pty_rows === "number" ? data.pty_rows : 24;
|
|
34751
|
+
const cols = typeof data.pty_cols === "number" ? data.pty_cols : 80;
|
|
34752
|
+
return { rows, cols };
|
|
34140
34753
|
}
|
|
34754
|
+
function ptyCacheKey(runtime, taskId) {
|
|
34755
|
+
return `${projectRootFor(runtime)}::${runtime.sessionID ?? "__default__"}::${taskId}`;
|
|
34756
|
+
}
|
|
34757
|
+
function preview(output) {
|
|
34758
|
+
return output.length <= METADATA_PREVIEW_LIMIT ? output : output.slice(-METADATA_PREVIEW_LIMIT);
|
|
34759
|
+
}
|
|
34760
|
+
function isTerminalStatus(status) {
|
|
34761
|
+
return status === "completed" || status === "failed" || status === "killed" || status === "timed_out";
|
|
34762
|
+
}
|
|
34763
|
+
function subagentGuidance(taskId) {
|
|
34764
|
+
return `
|
|
34141
34765
|
|
|
34142
|
-
|
|
34143
|
-
|
|
34144
|
-
|
|
34145
|
-
|
|
34146
|
-
|
|
34147
|
-
var cache2 = new Map;
|
|
34148
|
-
async function resolveIsSubagent(client, sessionId, _fallbackDirectory) {
|
|
34149
|
-
if (!sessionId) {
|
|
34150
|
-
sessionLog(undefined, "[subagent-detect] no sessionId provided → primary");
|
|
34151
|
-
return false;
|
|
34766
|
+
NOTE (subagent session): Continue with other work if you have it. If you don't, call bash_watch({ taskId: "${taskId}", timeoutMs: 60000 }) to wait for completion before returning to the parent. Subagents don't survive turn-end and won't receive the completion reminder.`;
|
|
34767
|
+
}
|
|
34768
|
+
function formatBackgroundLaunch(taskId, isPty) {
|
|
34769
|
+
if (isPty) {
|
|
34770
|
+
return `PTY task started: ${taskId}. Use bash_status({ taskId: "${taskId}", outputMode: "screen" }) to see the visible terminal, bash_write({ taskId: "${taskId}", input: ... }) to send keystrokes. A completion reminder fires automatically when the task exits.`;
|
|
34152
34771
|
}
|
|
34153
|
-
|
|
34154
|
-
|
|
34155
|
-
|
|
34156
|
-
|
|
34157
|
-
|
|
34772
|
+
return `Background task started: ${taskId}. A completion reminder will be delivered automatically; don't poll bash_status.`;
|
|
34773
|
+
}
|
|
34774
|
+
function formatPromotionMessage(taskId, timeout, waitWindowMs) {
|
|
34775
|
+
const waited = timeout !== undefined ? Math.min(timeout, waitWindowMs) : waitWindowMs;
|
|
34776
|
+
return `Foreground bash didn't finish within ${formatSeconds(waited)} and was promoted to background: ${taskId}. A completion reminder will be delivered automatically; use bash_status({ taskId: "${taskId}" }) to inspect output or bash_kill({ taskId: "${taskId}" }) to terminate.`;
|
|
34777
|
+
}
|
|
34778
|
+
function formatSeconds(ms) {
|
|
34779
|
+
return `${Number((ms / 1000).toFixed(1))}s`;
|
|
34780
|
+
}
|
|
34781
|
+
function formatForegroundResult(data) {
|
|
34782
|
+
const output = data.output_preview ?? "";
|
|
34783
|
+
const outputPath = data.output_path;
|
|
34784
|
+
const truncated = data.output_truncated === true;
|
|
34785
|
+
const status = data.status;
|
|
34786
|
+
const exit = data.exit_code;
|
|
34787
|
+
let rendered = output;
|
|
34788
|
+
if (truncated && outputPath) {
|
|
34789
|
+
rendered += `
|
|
34790
|
+
[output truncated; full output at ${outputPath}]`;
|
|
34158
34791
|
}
|
|
34159
|
-
|
|
34160
|
-
|
|
34161
|
-
|
|
34162
|
-
sessionLog(sessionId, `[subagent-detect] client.session.get unavailable (client=${typeof client}, session=${typeof sessionApi}, get=${typeof sessionApi?.get}) → caching as primary`);
|
|
34163
|
-
setCache2(sessionId, false);
|
|
34164
|
-
return false;
|
|
34792
|
+
if (status === "timed_out") {
|
|
34793
|
+
rendered += `
|
|
34794
|
+
[command timed out]`;
|
|
34165
34795
|
}
|
|
34166
|
-
|
|
34167
|
-
|
|
34168
|
-
|
|
34169
|
-
try {
|
|
34170
|
-
const result = await sessionApi.get({
|
|
34171
|
-
path: { id: sessionId }
|
|
34172
|
-
});
|
|
34173
|
-
const session = result?.data ?? result;
|
|
34174
|
-
parentIdRaw = session?.parentID;
|
|
34175
|
-
isSubagent = session !== undefined && typeof session.parentID === "string" && session.parentID.length > 0;
|
|
34176
|
-
sessionLog(sessionId, `[subagent-detect] SDK returned session=${session !== undefined ? "present" : "undefined"}, parentID=${JSON.stringify(parentIdRaw)} → isSubagent=${isSubagent}`);
|
|
34177
|
-
} catch (err) {
|
|
34178
|
-
sessionWarn(sessionId, `[subagent-detect] SDK lookup failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
34179
|
-
return false;
|
|
34796
|
+
if (typeof exit === "number" && exit !== 0) {
|
|
34797
|
+
rendered += `
|
|
34798
|
+
[exit code: ${exit}]`;
|
|
34180
34799
|
}
|
|
34181
|
-
|
|
34182
|
-
return isSubagent;
|
|
34800
|
+
return rendered;
|
|
34183
34801
|
}
|
|
34184
|
-
function
|
|
34185
|
-
|
|
34186
|
-
|
|
34187
|
-
|
|
34188
|
-
|
|
34189
|
-
|
|
34190
|
-
|
|
34191
|
-
|
|
34192
|
-
}
|
|
34802
|
+
function foregroundMetadata(description, data, rendered) {
|
|
34803
|
+
const outputPath = data.output_path;
|
|
34804
|
+
return {
|
|
34805
|
+
description,
|
|
34806
|
+
output: preview(rendered),
|
|
34807
|
+
exit: data.exit_code,
|
|
34808
|
+
truncated: data.output_truncated,
|
|
34809
|
+
...outputPath ? { outputPath } : {}
|
|
34810
|
+
};
|
|
34811
|
+
}
|
|
34812
|
+
function sleep(ms) {
|
|
34813
|
+
return new Promise((resolve7) => setTimeout(resolve7, ms));
|
|
34814
|
+
}
|
|
34815
|
+
function getCallID(ctx) {
|
|
34816
|
+
const c = ctx;
|
|
34817
|
+
return c.callID ?? c.callId ?? c.call_id;
|
|
34818
|
+
}
|
|
34819
|
+
function shortenCommand(command) {
|
|
34820
|
+
const collapsed = command.replace(/\s+/g, " ").trim();
|
|
34821
|
+
return collapsed.length <= 80 ? collapsed : `${collapsed.slice(0, 77)}...`;
|
|
34193
34822
|
}
|
|
34194
34823
|
|
|
34195
|
-
// src/tools/
|
|
34824
|
+
// src/tools/conflicts.ts
|
|
34825
|
+
import { tool as tool5 } from "@opencode-ai/plugin";
|
|
34196
34826
|
var z5 = tool5.schema;
|
|
34197
|
-
|
|
34198
|
-
|
|
34199
|
-
|
|
34200
|
-
|
|
34201
|
-
|
|
34202
|
-
|
|
34203
|
-
|
|
34204
|
-
|
|
34205
|
-
|
|
34827
|
+
function conflictTools(ctx) {
|
|
34828
|
+
return {
|
|
34829
|
+
aft_conflicts: {
|
|
34830
|
+
description: "Show all git merge conflicts across the repository — returns line-numbered conflict regions with context for every conflicted file in a single call. Conflicts are discovered from the git repository's top level. By default it inspects the session's project repository; pass `path` to inspect a different repository or git worktree (e.g. where a rebase/merge is running).",
|
|
34831
|
+
args: {
|
|
34832
|
+
path: z5.string().describe("Optional path inside the git repository or worktree to inspect (absolute or relative to project root). Conflicts are discovered from that repository's top level. Defaults to the session project root.").optional()
|
|
34833
|
+
},
|
|
34834
|
+
execute: async (args, context) => {
|
|
34835
|
+
const params = {};
|
|
34836
|
+
if (!isEmptyParam(args?.path)) {
|
|
34837
|
+
const expanded = expandTilde(String(args.path));
|
|
34838
|
+
const projectRoot = await resolveProjectRoot(ctx, context);
|
|
34839
|
+
const resolved = resolvePathFromProjectRoot(projectRoot, expanded);
|
|
34840
|
+
const denied = await assertExternalDirectoryPermission(context, resolved, {
|
|
34841
|
+
kind: "directory"
|
|
34842
|
+
});
|
|
34843
|
+
if (denied)
|
|
34844
|
+
return permissionDeniedResponse(denied);
|
|
34845
|
+
params.path = resolved;
|
|
34846
|
+
}
|
|
34847
|
+
const response = await callBridge(ctx, context, "git_conflicts", params);
|
|
34848
|
+
if (response.success === false) {
|
|
34849
|
+
throw new Error(response.message || "git_conflicts failed");
|
|
34850
|
+
}
|
|
34851
|
+
return response.text;
|
|
34852
|
+
}
|
|
34853
|
+
}
|
|
34854
|
+
};
|
|
34855
|
+
}
|
|
34856
|
+
|
|
34857
|
+
// src/tools/hoisted.ts
|
|
34858
|
+
import * as fs6 from "node:fs";
|
|
34859
|
+
import * as path4 from "node:path";
|
|
34860
|
+
import { tool as tool8 } from "@opencode-ai/plugin";
|
|
34861
|
+
|
|
34862
|
+
// src/patch-parser.ts
|
|
34863
|
+
function stripHeredoc(input) {
|
|
34864
|
+
const heredocMatch = input.match(/^(?:cat\s+)?<<['"]?(\w+)['"]?\s*\n([\s\S]*?)\n\1\s*$/);
|
|
34865
|
+
return heredocMatch ? heredocMatch[2] : input;
|
|
34866
|
+
}
|
|
34867
|
+
function parsePatchHeader(lines, startIdx) {
|
|
34868
|
+
const line = lines[startIdx];
|
|
34869
|
+
if (line.startsWith("*** Add File:")) {
|
|
34870
|
+
const filePath = line.slice("*** Add File:".length).trim();
|
|
34871
|
+
return filePath ? { filePath, nextIdx: startIdx + 1 } : null;
|
|
34206
34872
|
}
|
|
34207
|
-
|
|
34873
|
+
if (line.startsWith("*** Delete File:")) {
|
|
34874
|
+
const filePath = line.slice("*** Delete File:".length).trim();
|
|
34875
|
+
return filePath ? { filePath, nextIdx: startIdx + 1 } : null;
|
|
34876
|
+
}
|
|
34877
|
+
if (line.startsWith("*** Update File:")) {
|
|
34878
|
+
const filePath = line.slice("*** Update File:".length).trim();
|
|
34879
|
+
let movePath;
|
|
34880
|
+
let nextIdx = startIdx + 1;
|
|
34881
|
+
if (nextIdx < lines.length && lines[nextIdx].startsWith("*** Move to:")) {
|
|
34882
|
+
movePath = lines[nextIdx].slice("*** Move to:".length).trim();
|
|
34883
|
+
nextIdx++;
|
|
34884
|
+
}
|
|
34885
|
+
return filePath ? { filePath, movePath, nextIdx } : null;
|
|
34886
|
+
}
|
|
34887
|
+
return null;
|
|
34208
34888
|
}
|
|
34209
|
-
|
|
34210
|
-
|
|
34211
|
-
|
|
34212
|
-
|
|
34213
|
-
|
|
34214
|
-
|
|
34215
|
-
|
|
34216
|
-
|
|
34217
|
-
|
|
34218
|
-
await runAsk(runtime.ask({
|
|
34219
|
-
permission,
|
|
34220
|
-
patterns: ask.patterns,
|
|
34221
|
-
always: ask.always,
|
|
34222
|
-
metadata: {}
|
|
34223
|
-
}));
|
|
34224
|
-
permissionsGranted.push(...ask.always.length > 0 ? ask.always : ask.patterns);
|
|
34889
|
+
function parseAddFileContent(lines, startIdx) {
|
|
34890
|
+
let content = "";
|
|
34891
|
+
let i = startIdx;
|
|
34892
|
+
while (i < lines.length && !lines[i].startsWith("***")) {
|
|
34893
|
+
if (lines[i].startsWith("+")) {
|
|
34894
|
+
content += `${lines[i].substring(1)}
|
|
34895
|
+
`;
|
|
34896
|
+
}
|
|
34897
|
+
i++;
|
|
34225
34898
|
}
|
|
34226
|
-
|
|
34227
|
-
|
|
34228
|
-
|
|
34899
|
+
if (content.endsWith(`
|
|
34900
|
+
`)) {
|
|
34901
|
+
content = content.slice(0, -1);
|
|
34229
34902
|
}
|
|
34230
|
-
return
|
|
34903
|
+
return { content, nextIdx: i };
|
|
34231
34904
|
}
|
|
34232
|
-
function
|
|
34233
|
-
|
|
34234
|
-
|
|
34235
|
-
|
|
34236
|
-
|
|
34237
|
-
|
|
34238
|
-
|
|
34239
|
-
|
|
34240
|
-
|
|
34241
|
-
|
|
34242
|
-
|
|
34243
|
-
|
|
34244
|
-
|
|
34245
|
-
|
|
34246
|
-
|
|
34247
|
-
|
|
34248
|
-
let accumulatedOutput = "";
|
|
34249
|
-
const description = args.description;
|
|
34250
|
-
const metadata = context.metadata;
|
|
34251
|
-
const rawCommand = args.command;
|
|
34252
|
-
const compressionEnabled = bashCfg.compress && args.compressed !== false;
|
|
34253
|
-
const pipeStrip = maybeStripCompressorPipe(rawCommand, compressionEnabled);
|
|
34254
|
-
const command = pipeStrip.command;
|
|
34255
|
-
const cwd = args.workdir ?? context.directory;
|
|
34256
|
-
const isSubagent = await resolveIsSubagent(ctx.client, context.sessionID, context.directory);
|
|
34257
|
-
const requestedPty = args.pty === true;
|
|
34258
|
-
const requestedBackground = args.background === true || requestedPty;
|
|
34259
|
-
if (requestedPty && isSubagent) {
|
|
34260
|
-
throw new Error("PTY mode is not available in subagent sessions; subagents cannot drive interactive terminals.");
|
|
34261
|
-
}
|
|
34262
|
-
const allowSubagentBg = bashCfg.subagent_background;
|
|
34263
|
-
const subagentForcedForeground = isSubagent && !allowSubagentBg;
|
|
34264
|
-
const effectiveBackground = subagentForcedForeground ? false : requestedBackground;
|
|
34265
|
-
const rawTimeout = args.timeout;
|
|
34266
|
-
const effectiveTimeout = effectiveBackground ? rawTimeout : resolveBashKillTimeout(rawTimeout, bashCfg.foreground_wait_window_ms);
|
|
34267
|
-
if (subagentForcedForeground && requestedBackground) {
|
|
34268
|
-
sessionLog(context.sessionID, "[bash] subagent + background:true → converting to foreground (subagent would lose task_id)");
|
|
34269
|
-
}
|
|
34270
|
-
const shellEnv = await ctx.plugin?.trigger?.("shell.env", { cwd, sessionID: context.sessionID, callID: getCallID(context) }, { env: {} });
|
|
34271
|
-
const data = await withPermissionLoop(ctx, context, {
|
|
34272
|
-
command,
|
|
34273
|
-
timeout: effectiveTimeout,
|
|
34274
|
-
workdir: args.workdir,
|
|
34275
|
-
env: shellEnv?.env ?? {},
|
|
34276
|
-
description,
|
|
34277
|
-
background: effectiveBackground,
|
|
34278
|
-
notify_on_completion: effectiveBackground,
|
|
34279
|
-
compressed: args.compressed,
|
|
34280
|
-
pty: requestedPty,
|
|
34281
|
-
pty_rows: args.ptyRows,
|
|
34282
|
-
pty_cols: args.ptyCols,
|
|
34283
|
-
permissions_requested: true
|
|
34284
|
-
}, callBashBridge, {
|
|
34285
|
-
onProgress: ({ text }) => {
|
|
34286
|
-
accumulatedOutput = preview(accumulatedOutput + text);
|
|
34287
|
-
metadata?.({ output: accumulatedOutput, description });
|
|
34288
|
-
}
|
|
34289
|
-
});
|
|
34290
|
-
if (data.success === false) {
|
|
34291
|
-
throw new Error(data.message || "bash failed");
|
|
34292
|
-
}
|
|
34293
|
-
if (data.status === "running" && typeof data.task_id === "string") {
|
|
34294
|
-
const callID2 = getCallID(context);
|
|
34295
|
-
const taskId = data.task_id;
|
|
34296
|
-
if (effectiveBackground) {
|
|
34297
|
-
trackBgTask(context.sessionID, taskId);
|
|
34298
|
-
let startedLine = formatBackgroundLaunch(taskId, requestedPty);
|
|
34299
|
-
if (isSubagent && allowSubagentBg)
|
|
34300
|
-
startedLine += subagentGuidance(taskId);
|
|
34301
|
-
const metadataPayload2 = { description, output: startedLine, status: "running", taskId };
|
|
34302
|
-
metadata?.(metadataPayload2);
|
|
34303
|
-
if (callID2) {
|
|
34304
|
-
storeToolMetadata(context.sessionID, callID2, {
|
|
34305
|
-
title: description ?? shortenCommand(command),
|
|
34306
|
-
metadata: metadataPayload2
|
|
34307
|
-
});
|
|
34308
|
-
}
|
|
34309
|
-
return startedLine;
|
|
34310
|
-
}
|
|
34311
|
-
const foregroundWaitMs = resolveForegroundWaitMs(bashCfg.foreground_wait_window_ms);
|
|
34312
|
-
const waitTimeoutMs = subagentForcedForeground ? effectiveTimeout ?? DEFAULT_HARD_TIMEOUT_MS : effectiveTimeout !== undefined ? Math.min(effectiveTimeout, foregroundWaitMs) : foregroundWaitMs;
|
|
34313
|
-
const startedAt = Date.now();
|
|
34314
|
-
while (true) {
|
|
34315
|
-
const status = await callBashBridge(ctx, context, "bash_status", { task_id: taskId });
|
|
34316
|
-
if (status.success === false) {
|
|
34317
|
-
throw new Error(status.message ?? "bash_status failed");
|
|
34318
|
-
}
|
|
34319
|
-
if (isTerminalStatus(status.status)) {
|
|
34320
|
-
const rendered2 = appendPipeStripNote(formatForegroundResult(status), pipeStrip.note);
|
|
34321
|
-
const metadataPayload2 = foregroundMetadata(description, status, rendered2);
|
|
34322
|
-
metadata?.(metadataPayload2);
|
|
34323
|
-
if (callID2) {
|
|
34324
|
-
storeToolMetadata(context.sessionID, callID2, {
|
|
34325
|
-
title: description ?? shortenCommand(command),
|
|
34326
|
-
metadata: metadataPayload2
|
|
34327
|
-
});
|
|
34328
|
-
}
|
|
34329
|
-
return rendered2;
|
|
34330
|
-
}
|
|
34331
|
-
if (Date.now() - startedAt >= waitTimeoutMs) {
|
|
34332
|
-
if (subagentForcedForeground) {
|
|
34333
|
-
await sleep(FOREGROUND_POLL_INTERVAL_MS);
|
|
34334
|
-
continue;
|
|
34335
|
-
}
|
|
34336
|
-
const promoted = await callBashBridge(ctx, context, "bash_promote", {
|
|
34337
|
-
task_id: taskId
|
|
34338
|
-
});
|
|
34339
|
-
if (promoted.success === false) {
|
|
34340
|
-
throw new Error(promoted.message ?? "bash_promote failed");
|
|
34341
|
-
}
|
|
34342
|
-
trackBgTask(context.sessionID, taskId);
|
|
34343
|
-
let message = formatPromotionMessage(taskId, effectiveTimeout, foregroundWaitMs);
|
|
34344
|
-
if (isSubagent && allowSubagentBg)
|
|
34345
|
-
message += subagentGuidance(taskId);
|
|
34346
|
-
const metadataPayload2 = { description, output: message, status: "running", taskId };
|
|
34347
|
-
metadata?.(metadataPayload2);
|
|
34348
|
-
if (callID2) {
|
|
34349
|
-
storeToolMetadata(context.sessionID, callID2, {
|
|
34350
|
-
title: description ?? shortenCommand(command),
|
|
34351
|
-
metadata: metadataPayload2
|
|
34352
|
-
});
|
|
34353
|
-
}
|
|
34354
|
-
return message;
|
|
34355
|
-
}
|
|
34356
|
-
await sleep(FOREGROUND_POLL_INTERVAL_MS);
|
|
34905
|
+
function parseUpdateFileChunks(lines, startIdx) {
|
|
34906
|
+
const chunks = [];
|
|
34907
|
+
let i = startIdx;
|
|
34908
|
+
while (i < lines.length && !lines[i].startsWith("***")) {
|
|
34909
|
+
if (lines[i].startsWith("@@")) {
|
|
34910
|
+
const contextLine = lines[i].substring(2).trim();
|
|
34911
|
+
i++;
|
|
34912
|
+
const oldLines = [];
|
|
34913
|
+
const newLines = [];
|
|
34914
|
+
let isEndOfFile = false;
|
|
34915
|
+
while (i < lines.length && !lines[i].startsWith("@@") && !lines[i].startsWith("***")) {
|
|
34916
|
+
const changeLine = lines[i];
|
|
34917
|
+
if (changeLine === "*** End of File") {
|
|
34918
|
+
isEndOfFile = true;
|
|
34919
|
+
i++;
|
|
34920
|
+
break;
|
|
34357
34921
|
}
|
|
34922
|
+
if (changeLine.startsWith(" ")) {
|
|
34923
|
+
const content = changeLine.substring(1);
|
|
34924
|
+
oldLines.push(content);
|
|
34925
|
+
newLines.push(content);
|
|
34926
|
+
} else if (changeLine.startsWith("-")) {
|
|
34927
|
+
oldLines.push(changeLine.substring(1));
|
|
34928
|
+
} else if (changeLine.startsWith("+")) {
|
|
34929
|
+
newLines.push(changeLine.substring(1));
|
|
34930
|
+
}
|
|
34931
|
+
i++;
|
|
34358
34932
|
}
|
|
34359
|
-
|
|
34360
|
-
|
|
34361
|
-
|
|
34362
|
-
|
|
34363
|
-
|
|
34364
|
-
const timedOut = data.timed_out === true;
|
|
34365
|
-
const callID = getCallID(context);
|
|
34366
|
-
const metadataPayload = {
|
|
34367
|
-
description,
|
|
34368
|
-
output: metadataOutput,
|
|
34369
|
-
exit,
|
|
34370
|
-
truncated,
|
|
34371
|
-
...outputPath ? { outputPath } : {}
|
|
34372
|
-
};
|
|
34373
|
-
metadata?.(metadataPayload);
|
|
34374
|
-
if (callID) {
|
|
34375
|
-
storeToolMetadata(context.sessionID, callID, {
|
|
34376
|
-
title: description ?? shortenCommand(command),
|
|
34377
|
-
metadata: metadataPayload
|
|
34378
|
-
});
|
|
34379
|
-
}
|
|
34380
|
-
let rendered = output;
|
|
34381
|
-
if (truncated && outputPath) {
|
|
34382
|
-
rendered += `
|
|
34383
|
-
[output truncated; full output at ${outputPath}]`;
|
|
34384
|
-
}
|
|
34385
|
-
if (timedOut) {
|
|
34386
|
-
rendered += `
|
|
34387
|
-
[command timed out]`;
|
|
34388
|
-
}
|
|
34389
|
-
if (typeof exit === "number" && exit !== 0) {
|
|
34390
|
-
rendered += `
|
|
34391
|
-
[exit code: ${exit}]`;
|
|
34392
|
-
}
|
|
34393
|
-
rendered = appendPipeStripNote(rendered, pipeStrip.note);
|
|
34394
|
-
return rendered;
|
|
34395
|
-
}
|
|
34396
|
-
};
|
|
34397
|
-
}
|
|
34398
|
-
function appendPipeStripNote(output, note) {
|
|
34399
|
-
return note ? `${output}
|
|
34400
|
-
|
|
34401
|
-
${note}` : output;
|
|
34402
|
-
}
|
|
34403
|
-
function createBashStatusTool(ctx) {
|
|
34404
|
-
return {
|
|
34405
|
-
description: "Read-only snapshot of a background or PTY bash task's current state and output. Returns immediately. Never waits. Use bash_watch to block on or register for pattern matches and exit events.",
|
|
34406
|
-
args: {
|
|
34407
|
-
taskId: z5.string().describe("Background task ID returned by bash({ background: true }), e.g. bash-6b454047."),
|
|
34408
|
-
outputMode: z5.enum(["screen", "raw", "both"]).optional().describe("PTY output rendering mode. Defaults to screen for PTY tasks and preserves existing behavior for piped tasks when omitted.")
|
|
34409
|
-
},
|
|
34410
|
-
execute: async (args, context) => {
|
|
34411
|
-
const taskId = args.taskId;
|
|
34412
|
-
const outputMode = args.outputMode;
|
|
34413
|
-
const data = await bashStatusSnapshot(ctx, context, taskId, outputMode);
|
|
34414
|
-
return await formatBashStatusText(context, taskId, data, outputMode);
|
|
34415
|
-
}
|
|
34416
|
-
};
|
|
34417
|
-
}
|
|
34418
|
-
function createBashKillTool(ctx) {
|
|
34419
|
-
return {
|
|
34420
|
-
description: "Terminate a running background bash task spawned with bash({ background: true }). Returns confirmation of kill or an error if the task already finished.",
|
|
34421
|
-
args: {
|
|
34422
|
-
taskId: z5.string().describe("Background task ID returned by bash({ background: true }), e.g. bash-6b454047.")
|
|
34423
|
-
},
|
|
34424
|
-
execute: async (args, context) => {
|
|
34425
|
-
const data = await callBashBridge(ctx, context, "bash_kill", {
|
|
34426
|
-
task_id: args.taskId
|
|
34933
|
+
chunks.push({
|
|
34934
|
+
old_lines: oldLines,
|
|
34935
|
+
new_lines: newLines,
|
|
34936
|
+
change_context: contextLine || undefined,
|
|
34937
|
+
is_end_of_file: isEndOfFile || undefined
|
|
34427
34938
|
});
|
|
34428
|
-
|
|
34429
|
-
|
|
34430
|
-
}
|
|
34431
|
-
await disposePtyTerminal(ptyCacheKey(context, args.taskId));
|
|
34432
|
-
if (data.kill_signaled === true) {
|
|
34433
|
-
return `Task ${args.taskId}: kill_signaled`;
|
|
34434
|
-
}
|
|
34435
|
-
return `Task ${args.taskId}: ${String(data.status ?? "killed")}`;
|
|
34939
|
+
} else {
|
|
34940
|
+
i++;
|
|
34436
34941
|
}
|
|
34437
|
-
};
|
|
34438
|
-
}
|
|
34439
|
-
async function bashStatusSnapshot(ctx, runtime, taskId, outputMode, options) {
|
|
34440
|
-
const data = await callBashBridge(ctx, runtime, "bash_status", { task_id: taskId, output_mode: outputMode }, options);
|
|
34441
|
-
if (data.success === false) {
|
|
34442
|
-
throw new Error(data.message ?? "bash_status failed");
|
|
34443
34942
|
}
|
|
34444
|
-
return
|
|
34943
|
+
return { chunks, nextIdx: i };
|
|
34445
34944
|
}
|
|
34446
|
-
|
|
34447
|
-
|
|
34448
|
-
|
|
34449
|
-
|
|
34450
|
-
|
|
34451
|
-
|
|
34452
|
-
|
|
34453
|
-
|
|
34454
|
-
|
|
34455
|
-
|
|
34456
|
-
|
|
34457
|
-
|
|
34945
|
+
var MAX_PATCH_SIZE = 1024 * 1024;
|
|
34946
|
+
var MAX_HUNKS = 500;
|
|
34947
|
+
function parsePatch(patchText) {
|
|
34948
|
+
if (patchText.length > MAX_PATCH_SIZE) {
|
|
34949
|
+
throw new Error(`Patch too large: ${patchText.length} bytes exceeds limit of ${MAX_PATCH_SIZE} bytes`);
|
|
34950
|
+
}
|
|
34951
|
+
const cleaned = stripHeredoc(patchText.trim());
|
|
34952
|
+
const lines = cleaned.split(`
|
|
34953
|
+
`);
|
|
34954
|
+
const hunks = [];
|
|
34955
|
+
const beginIdx = lines.findIndex((line) => line.trim() === "*** Begin Patch");
|
|
34956
|
+
const endIdx = lines.findIndex((line) => line.trim() === "*** End Patch");
|
|
34957
|
+
if (beginIdx === -1 || endIdx === -1 || beginIdx >= endIdx) {
|
|
34958
|
+
throw new Error("Invalid patch format: missing *** Begin Patch / *** End Patch markers");
|
|
34959
|
+
}
|
|
34960
|
+
let i = beginIdx + 1;
|
|
34961
|
+
while (i < endIdx) {
|
|
34962
|
+
const header = parsePatchHeader(lines, i);
|
|
34963
|
+
if (!header) {
|
|
34964
|
+
i++;
|
|
34965
|
+
continue;
|
|
34458
34966
|
}
|
|
34459
|
-
if (
|
|
34460
|
-
|
|
34461
|
-
|
|
34967
|
+
if (hunks.length >= MAX_HUNKS) {
|
|
34968
|
+
throw new Error(`Patch exceeds maximum of ${MAX_HUNKS} file operations`);
|
|
34969
|
+
}
|
|
34970
|
+
if (lines[i].startsWith("*** Add File:")) {
|
|
34971
|
+
const { content, nextIdx } = parseAddFileContent(lines, header.nextIdx);
|
|
34972
|
+
hunks.push({ type: "add", path: header.filePath, contents: content });
|
|
34973
|
+
i = nextIdx;
|
|
34974
|
+
} else if (lines[i].startsWith("*** Delete File:")) {
|
|
34975
|
+
hunks.push({ type: "delete", path: header.filePath });
|
|
34976
|
+
i = header.nextIdx;
|
|
34977
|
+
} else if (lines[i].startsWith("*** Update File:")) {
|
|
34978
|
+
const { chunks, nextIdx } = parseUpdateFileChunks(lines, header.nextIdx);
|
|
34979
|
+
hunks.push({
|
|
34980
|
+
type: "update",
|
|
34981
|
+
path: header.filePath,
|
|
34982
|
+
move_path: header.movePath,
|
|
34983
|
+
chunks
|
|
34984
|
+
});
|
|
34985
|
+
i = nextIdx;
|
|
34986
|
+
} else {
|
|
34987
|
+
i++;
|
|
34462
34988
|
}
|
|
34463
34989
|
}
|
|
34464
|
-
return
|
|
34990
|
+
return hunks;
|
|
34465
34991
|
}
|
|
34466
|
-
|
|
34467
|
-
|
|
34468
|
-
|
|
34469
|
-
|
|
34470
|
-
[
|
|
34471
|
-
|
|
34472
|
-
|
|
34473
|
-
|
|
34474
|
-
|
|
34475
|
-
|
|
34476
|
-
|
|
34477
|
-
|
|
34478
|
-
|
|
34479
|
-
|
|
34480
|
-
|
|
34481
|
-
|
|
34482
|
-
|
|
34483
|
-
|
|
34484
|
-
|
|
34485
|
-
|
|
34486
|
-
${screen}` : "";
|
|
34992
|
+
function normalizeUnicode(str) {
|
|
34993
|
+
return str.replace(/[\u2018\u2019\u201A\u201B]/g, "'").replace(/[\u201C\u201D\u201E\u201F]/g, '"').replace(/[\u2010\u2011\u2012\u2013\u2014\u2015]/g, "-").replace(/\u2026/g, "...").replace(/\u00A0/g, " ");
|
|
34994
|
+
}
|
|
34995
|
+
function normalizeIndent(str) {
|
|
34996
|
+
return str.replace(/^[\t ]+/, (m) => " ".repeat(m.length));
|
|
34997
|
+
}
|
|
34998
|
+
function tryMatch(lines, pattern, startIndex, compare, eof) {
|
|
34999
|
+
if (eof) {
|
|
35000
|
+
const fromEnd = lines.length - pattern.length;
|
|
35001
|
+
if (fromEnd >= startIndex) {
|
|
35002
|
+
let matches = true;
|
|
35003
|
+
for (let j = 0;j < pattern.length; j++) {
|
|
35004
|
+
if (!compare(lines[fromEnd + j], pattern[j])) {
|
|
35005
|
+
matches = false;
|
|
35006
|
+
break;
|
|
35007
|
+
}
|
|
35008
|
+
}
|
|
35009
|
+
if (matches)
|
|
35010
|
+
return fromEnd;
|
|
35011
|
+
}
|
|
34487
35012
|
}
|
|
34488
|
-
|
|
34489
|
-
|
|
34490
|
-
|
|
34491
|
-
|
|
34492
|
-
|
|
35013
|
+
for (let i = startIndex;i <= lines.length - pattern.length; i++) {
|
|
35014
|
+
let matches = true;
|
|
35015
|
+
for (let j = 0;j < pattern.length; j++) {
|
|
35016
|
+
if (!compare(lines[i + j], pattern[j])) {
|
|
35017
|
+
matches = false;
|
|
35018
|
+
break;
|
|
35019
|
+
}
|
|
35020
|
+
}
|
|
35021
|
+
if (matches)
|
|
35022
|
+
return i;
|
|
34493
35023
|
}
|
|
34494
|
-
return
|
|
34495
|
-
}
|
|
34496
|
-
function ptyDimensions(data) {
|
|
34497
|
-
const rows = typeof data.pty_rows === "number" ? data.pty_rows : 24;
|
|
34498
|
-
const cols = typeof data.pty_cols === "number" ? data.pty_cols : 80;
|
|
34499
|
-
return { rows, cols };
|
|
35024
|
+
return -1;
|
|
34500
35025
|
}
|
|
34501
|
-
function
|
|
34502
|
-
|
|
35026
|
+
function seekSequenceTiered(lines, pattern, startIndex, eof = false) {
|
|
35027
|
+
if (pattern.length === 0)
|
|
35028
|
+
return null;
|
|
35029
|
+
const exact = tryMatch(lines, pattern, startIndex, (a, b) => a === b, eof);
|
|
35030
|
+
if (exact !== -1)
|
|
35031
|
+
return { found: exact, tier: "exact" };
|
|
35032
|
+
const rstrip = tryMatch(lines, pattern, startIndex, (a, b) => a.trimEnd() === b.trimEnd(), eof);
|
|
35033
|
+
if (rstrip !== -1)
|
|
35034
|
+
return { found: rstrip, tier: "rstrip" };
|
|
35035
|
+
const trim = tryMatch(lines, pattern, startIndex, (a, b) => a.trim() === b.trim(), eof);
|
|
35036
|
+
if (trim !== -1)
|
|
35037
|
+
return { found: trim, tier: "trim" };
|
|
35038
|
+
const indent = tryMatch(lines, pattern, startIndex, (a, b) => normalizeIndent(a).trimEnd() === normalizeIndent(b).trimEnd(), eof);
|
|
35039
|
+
if (indent !== -1)
|
|
35040
|
+
return { found: indent, tier: "indent" };
|
|
35041
|
+
const unicode = tryMatch(lines, pattern, startIndex, (a, b) => normalizeUnicode(a.trim()) === normalizeUnicode(b.trim()), eof);
|
|
35042
|
+
if (unicode !== -1)
|
|
35043
|
+
return { found: unicode, tier: "unicode" };
|
|
35044
|
+
return null;
|
|
34503
35045
|
}
|
|
34504
|
-
function
|
|
34505
|
-
|
|
35046
|
+
function seekSequence(lines, pattern, startIndex, eof = false) {
|
|
35047
|
+
const r = seekSequenceTiered(lines, pattern, startIndex, eof);
|
|
35048
|
+
return r ? r.found : -1;
|
|
34506
35049
|
}
|
|
34507
|
-
function
|
|
34508
|
-
|
|
35050
|
+
function findClosestPartialMatch(lines, pattern) {
|
|
35051
|
+
if (pattern.length === 0 || lines.length === 0)
|
|
35052
|
+
return null;
|
|
35053
|
+
const compareAny = (a, b) => a === b || a.trimEnd() === b.trimEnd() || a.trim() === b.trim() || normalizeIndent(a).trimEnd() === normalizeIndent(b).trimEnd() || normalizeUnicode(a.trim()) === normalizeUnicode(b.trim());
|
|
35054
|
+
const candidates = [];
|
|
35055
|
+
for (let i = 0;i < lines.length && candidates.length < 16; i++) {
|
|
35056
|
+
if (compareAny(lines[i], pattern[0]))
|
|
35057
|
+
candidates.push(i);
|
|
35058
|
+
}
|
|
35059
|
+
if (candidates.length === 0)
|
|
35060
|
+
return null;
|
|
35061
|
+
let best = { lineNumber: -1, matchedLines: 0, firstDivergence: -1 };
|
|
35062
|
+
for (const start of candidates) {
|
|
35063
|
+
let matched = 0;
|
|
35064
|
+
for (let j = 0;j < pattern.length && start + j < lines.length; j++) {
|
|
35065
|
+
if (!compareAny(lines[start + j], pattern[j]))
|
|
35066
|
+
break;
|
|
35067
|
+
matched++;
|
|
35068
|
+
}
|
|
35069
|
+
if (matched > best.matchedLines) {
|
|
35070
|
+
best = {
|
|
35071
|
+
lineNumber: start + 1,
|
|
35072
|
+
matchedLines: matched,
|
|
35073
|
+
firstDivergence: matched
|
|
35074
|
+
};
|
|
35075
|
+
}
|
|
35076
|
+
}
|
|
35077
|
+
return best.lineNumber === -1 ? null : best;
|
|
34509
35078
|
}
|
|
34510
|
-
function
|
|
34511
|
-
|
|
35079
|
+
function applyUpdateChunks(originalContent, filePath, chunks) {
|
|
35080
|
+
const originalLines = originalContent.split(`
|
|
35081
|
+
`);
|
|
35082
|
+
if (originalLines.length > 0 && originalLines[originalLines.length - 1] === "") {
|
|
35083
|
+
originalLines.pop();
|
|
35084
|
+
}
|
|
35085
|
+
const replacements = [];
|
|
35086
|
+
let lineIndex = 0;
|
|
35087
|
+
for (const chunk of chunks) {
|
|
35088
|
+
if (chunk.change_context) {
|
|
35089
|
+
const contextIdx = seekSequence(originalLines, [chunk.change_context], lineIndex);
|
|
35090
|
+
if (contextIdx === -1) {
|
|
35091
|
+
throw new Error(`Failed to find context '${chunk.change_context}' in ${filePath}`);
|
|
35092
|
+
}
|
|
35093
|
+
lineIndex = contextIdx + 1;
|
|
35094
|
+
}
|
|
35095
|
+
if (chunk.old_lines.length === 0) {
|
|
35096
|
+
let insertionIdx;
|
|
35097
|
+
if (chunk.change_context) {
|
|
35098
|
+
insertionIdx = lineIndex;
|
|
35099
|
+
} else if (originalLines.length > 0 && originalLines[originalLines.length - 1] === "") {
|
|
35100
|
+
insertionIdx = originalLines.length - 1;
|
|
35101
|
+
} else {
|
|
35102
|
+
insertionIdx = originalLines.length;
|
|
35103
|
+
}
|
|
35104
|
+
replacements.push([insertionIdx, 0, chunk.new_lines]);
|
|
35105
|
+
continue;
|
|
35106
|
+
}
|
|
35107
|
+
let pattern = chunk.old_lines;
|
|
35108
|
+
let newSlice = chunk.new_lines;
|
|
35109
|
+
let found = seekSequence(originalLines, pattern, lineIndex, chunk.is_end_of_file);
|
|
35110
|
+
if (found === -1 && pattern.length > 0 && pattern[pattern.length - 1] === "") {
|
|
35111
|
+
pattern = pattern.slice(0, -1);
|
|
35112
|
+
if (newSlice.length > 0 && newSlice[newSlice.length - 1] === "") {
|
|
35113
|
+
newSlice = newSlice.slice(0, -1);
|
|
35114
|
+
}
|
|
35115
|
+
found = seekSequence(originalLines, pattern, lineIndex, chunk.is_end_of_file);
|
|
35116
|
+
}
|
|
35117
|
+
if (found !== -1) {
|
|
35118
|
+
replacements.push([found, pattern.length, newSlice]);
|
|
35119
|
+
lineIndex = found + pattern.length;
|
|
35120
|
+
} else {
|
|
35121
|
+
const newSliceTrimmed = newSlice.filter((line) => line.trim().length > 0);
|
|
35122
|
+
const alreadyApplied = newSliceTrimmed.length > 0 && seekSequence(originalLines, newSliceTrimmed, 0, chunk.is_end_of_file) !== -1;
|
|
35123
|
+
const closest = findClosestPartialMatch(originalLines, pattern);
|
|
35124
|
+
let closestHint = "";
|
|
35125
|
+
if (closest && closest.matchedLines > 0) {
|
|
35126
|
+
const fileLineNo = closest.lineNumber + closest.firstDivergence;
|
|
35127
|
+
const expectedLine = pattern[closest.firstDivergence];
|
|
35128
|
+
const actualLine = fileLineNo - 1 < originalLines.length ? originalLines[fileLineNo - 1] : "<EOF>";
|
|
35129
|
+
closestHint = `
|
|
34512
35130
|
|
|
34513
|
-
|
|
34514
|
-
}
|
|
34515
|
-
|
|
34516
|
-
|
|
34517
|
-
|
|
34518
|
-
|
|
34519
|
-
|
|
34520
|
-
|
|
34521
|
-
|
|
34522
|
-
|
|
34523
|
-
|
|
34524
|
-
}
|
|
34525
|
-
|
|
34526
|
-
|
|
34527
|
-
}
|
|
34528
|
-
function formatForegroundResult(data) {
|
|
34529
|
-
const output = data.output_preview ?? "";
|
|
34530
|
-
const outputPath = data.output_path;
|
|
34531
|
-
const truncated = data.output_truncated === true;
|
|
34532
|
-
const status = data.status;
|
|
34533
|
-
const exit = data.exit_code;
|
|
34534
|
-
let rendered = output;
|
|
34535
|
-
if (truncated && outputPath) {
|
|
34536
|
-
rendered += `
|
|
34537
|
-
[output truncated; full output at ${outputPath}]`;
|
|
35131
|
+
Closest match starts at line ${closest.lineNumber} ` + `(${closest.matchedLines} of ${pattern.length} lines matched).
|
|
35132
|
+
` + `First divergence at line ${fileLineNo}:
|
|
35133
|
+
` + ` expected: ${JSON.stringify(expectedLine)}
|
|
35134
|
+
` + ` actual: ${JSON.stringify(actualLine)}`;
|
|
35135
|
+
}
|
|
35136
|
+
const triedTiers = "exact, trimEnd, trim, indent (tab/space), unicode";
|
|
35137
|
+
const alreadyAppliedHint = alreadyApplied ? `
|
|
35138
|
+
|
|
35139
|
+
Hint: the replacement content for this hunk already appears in the file. ` + "The patch may have been partially applied in a prior turn — re-read the file " + "to confirm which hunks still need to apply." : "";
|
|
35140
|
+
throw new Error(`Failed to find expected lines in ${filePath}:
|
|
35141
|
+
${chunk.old_lines.join(`
|
|
35142
|
+
`)}
|
|
35143
|
+
|
|
35144
|
+
` + `Tried match tiers: ${triedTiers}.${closestHint}${alreadyAppliedHint}`);
|
|
35145
|
+
}
|
|
34538
35146
|
}
|
|
34539
|
-
|
|
34540
|
-
|
|
34541
|
-
|
|
35147
|
+
replacements.sort((a, b) => a[0] - b[0]);
|
|
35148
|
+
const result = [...originalLines];
|
|
35149
|
+
for (let i = replacements.length - 1;i >= 0; i--) {
|
|
35150
|
+
const [startIdx, oldLen, newSegment] = replacements[i];
|
|
35151
|
+
result.splice(startIdx, oldLen, ...newSegment);
|
|
34542
35152
|
}
|
|
34543
|
-
if (
|
|
34544
|
-
|
|
34545
|
-
[exit code: ${exit}]`;
|
|
35153
|
+
if (result.length === 0 || result[result.length - 1] !== "") {
|
|
35154
|
+
result.push("");
|
|
34546
35155
|
}
|
|
34547
|
-
return
|
|
34548
|
-
|
|
34549
|
-
function foregroundMetadata(description, data, rendered) {
|
|
34550
|
-
const outputPath = data.output_path;
|
|
34551
|
-
return {
|
|
34552
|
-
description,
|
|
34553
|
-
output: preview(rendered),
|
|
34554
|
-
exit: data.exit_code,
|
|
34555
|
-
truncated: data.output_truncated,
|
|
34556
|
-
...outputPath ? { outputPath } : {}
|
|
34557
|
-
};
|
|
34558
|
-
}
|
|
34559
|
-
function sleep(ms) {
|
|
34560
|
-
return new Promise((resolve7) => setTimeout(resolve7, ms));
|
|
34561
|
-
}
|
|
34562
|
-
function getCallID(ctx) {
|
|
34563
|
-
const c = ctx;
|
|
34564
|
-
return c.callID ?? c.callId ?? c.call_id;
|
|
34565
|
-
}
|
|
34566
|
-
function shortenCommand(command) {
|
|
34567
|
-
const collapsed = command.replace(/\s+/g, " ").trim();
|
|
34568
|
-
return collapsed.length <= 80 ? collapsed : `${collapsed.slice(0, 77)}...`;
|
|
35156
|
+
return result.join(`
|
|
35157
|
+
`);
|
|
34569
35158
|
}
|
|
34570
35159
|
|
|
34571
35160
|
// src/tools/bash_watch.ts
|
|
@@ -34874,10 +35463,6 @@ function createBashWriteTool(ctx) {
|
|
|
34874
35463
|
}
|
|
34875
35464
|
|
|
34876
35465
|
// src/tools/hoisted.ts
|
|
34877
|
-
function getCallID2(ctx) {
|
|
34878
|
-
const c = ctx;
|
|
34879
|
-
return c.callID ?? c.callId ?? c.call_id;
|
|
34880
|
-
}
|
|
34881
35466
|
function relativeToWorktree(fp, worktree) {
|
|
34882
35467
|
return path4.relative(worktree, fp);
|
|
34883
35468
|
}
|
|
@@ -35083,7 +35668,6 @@ function inferBeforeStart(ops, from, beforeLen) {
|
|
|
35083
35668
|
return beforeLen;
|
|
35084
35669
|
}
|
|
35085
35670
|
var z8 = tool8.schema;
|
|
35086
|
-
var DIAGNOSTICS_PARAM_DESCRIPTION = "When true, wait up to 3 seconds for fresh LSP diagnostics on the edited file and include them in the result. Defaults to the configured `lsp.diagnostics_on_edit` value (false unless configured); per-call true/false overrides. Use aft_inspect to check diagnostics across a batch of edits or before tests/commits.";
|
|
35087
35671
|
function diagnosticsOnEditDefault(ctx) {
|
|
35088
35672
|
return ctx.config.lsp?.diagnostics_on_edit ?? false;
|
|
35089
35673
|
}
|
|
@@ -35163,19 +35747,16 @@ function createReadTool(ctx) {
|
|
|
35163
35747
|
} catch {}
|
|
35164
35748
|
const sizeStr = fileSize > 1048576 ? `${(fileSize / 1048576).toFixed(1)}MB` : fileSize > 1024 ? `${(fileSize / 1024).toFixed(0)}KB` : `${fileSize} bytes`;
|
|
35165
35749
|
const msg = `${label} read successfully`;
|
|
35166
|
-
|
|
35167
|
-
|
|
35168
|
-
|
|
35169
|
-
|
|
35170
|
-
|
|
35171
|
-
|
|
35172
|
-
|
|
35173
|
-
|
|
35174
|
-
|
|
35175
|
-
|
|
35176
|
-
});
|
|
35177
|
-
}
|
|
35178
|
-
return `${msg} (${ext.slice(1).toUpperCase()}, ${sizeStr}). File: ${filePath}`;
|
|
35750
|
+
return {
|
|
35751
|
+
output: `${msg} (${ext.slice(1).toUpperCase()}, ${sizeStr}). File: ${filePath}`,
|
|
35752
|
+
title: path4.relative(projectRoot, filePath),
|
|
35753
|
+
metadata: {
|
|
35754
|
+
preview: msg,
|
|
35755
|
+
filepath: filePath,
|
|
35756
|
+
isImage,
|
|
35757
|
+
isPdf: mime === "application/pdf"
|
|
35758
|
+
}
|
|
35759
|
+
};
|
|
35179
35760
|
}
|
|
35180
35761
|
let startLine = args.startLine;
|
|
35181
35762
|
let endLine = args.endLine;
|
|
@@ -35196,60 +35777,36 @@ function createReadTool(ctx) {
|
|
|
35196
35777
|
if (data.success === false) {
|
|
35197
35778
|
throw new Error(data.message || "read failed");
|
|
35198
35779
|
}
|
|
35199
|
-
const
|
|
35780
|
+
const dp = relativeToWorktree(filePath, projectRoot) || file2;
|
|
35200
35781
|
if (data.entries) {
|
|
35201
|
-
|
|
35202
|
-
|
|
35203
|
-
|
|
35204
|
-
|
|
35205
|
-
|
|
35206
|
-
|
|
35782
|
+
return {
|
|
35783
|
+
output: data.entries.join(`
|
|
35784
|
+
`),
|
|
35785
|
+
title: dp,
|
|
35786
|
+
metadata: { title: dp }
|
|
35787
|
+
};
|
|
35207
35788
|
}
|
|
35208
35789
|
if (data.binary) {
|
|
35209
|
-
|
|
35210
|
-
const dp = relativeToWorktree(filePath, projectRoot) || file2;
|
|
35211
|
-
storeToolMetadata(context.sessionID, readCallID, { title: dp, metadata: { title: dp } });
|
|
35212
|
-
}
|
|
35213
|
-
return data.message;
|
|
35214
|
-
}
|
|
35215
|
-
if (readCallID) {
|
|
35216
|
-
const dp = relativeToWorktree(filePath, projectRoot) || file2;
|
|
35217
|
-
storeToolMetadata(context.sessionID, readCallID, { title: dp, metadata: { title: dp } });
|
|
35790
|
+
return { output: data.message, title: dp, metadata: { title: dp } };
|
|
35218
35791
|
}
|
|
35219
35792
|
let output = data.content;
|
|
35220
35793
|
const agentSpecifiedRange = args.startLine !== undefined || args.endLine !== undefined || args.offset !== undefined || args.limit !== undefined;
|
|
35221
35794
|
const footer = formatReadFooter(agentSpecifiedRange, data);
|
|
35222
35795
|
if (footer)
|
|
35223
35796
|
output += footer;
|
|
35224
|
-
return output;
|
|
35797
|
+
return { output, title: dp, metadata: { title: dp } };
|
|
35225
35798
|
}
|
|
35226
35799
|
};
|
|
35227
35800
|
}
|
|
35228
35801
|
function getWriteDescription(editToolName) {
|
|
35229
|
-
return `Write content to a file, creating it
|
|
35230
|
-
|
|
35231
|
-
Automatically creates parent directories. Backs up existing files before overwriting.
|
|
35232
|
-
If the project has a formatter configured (biome, prettier, rustfmt, etc.), the file
|
|
35233
|
-
is auto-formatted after writing. Edits return as soon as the write completes unless
|
|
35234
|
-
the configured \`lsp.diagnostics_on_edit\` default or a per-call \`diagnostics: true\`
|
|
35235
|
-
asks for legacy sync-wait behavior. Call \`aft_inspect\` afterward to check
|
|
35236
|
-
diagnostics across a batch of edits.
|
|
35237
|
-
|
|
35238
|
-
**Behavior:**
|
|
35239
|
-
- Creates parent directories automatically (no need to mkdir first)
|
|
35240
|
-
- Existing files are backed up before overwriting (recoverable via aft_safety undo)
|
|
35241
|
-
- Auto-formats using project formatter if configured (biome.json, .prettierrc, etc.)
|
|
35242
|
-
- LSP diagnostics follow \`lsp.diagnostics_on_edit\` by default; pass \`diagnostics\` to override per call
|
|
35243
|
-
- Use this for creating new files or completely replacing file contents
|
|
35244
|
-
- For partial edits (find/replace), use the \`${editToolName}\` tool instead`;
|
|
35802
|
+
return `Write content to a file, creating it and parent directories automatically. Existing files are backed up before overwriting (undo via aft_safety) and auto-formatted when the project has a formatter configured. Use it to create files or replace whole contents; for partial edits, use the \`${editToolName}\` tool.`;
|
|
35245
35803
|
}
|
|
35246
35804
|
function createWriteTool(ctx, editToolName = "edit") {
|
|
35247
35805
|
return {
|
|
35248
35806
|
description: getWriteDescription(editToolName),
|
|
35249
35807
|
args: {
|
|
35250
35808
|
filePath: z8.string().describe("Path to the file to write (absolute or relative to project root)"),
|
|
35251
|
-
content: z8.string().describe("The full content to write to the file")
|
|
35252
|
-
diagnostics: z8.boolean().optional().describe(DIAGNOSTICS_PARAM_DESCRIPTION)
|
|
35809
|
+
content: z8.string().describe("The full content to write to the file")
|
|
35253
35810
|
},
|
|
35254
35811
|
execute: async (args, context) => {
|
|
35255
35812
|
const file2 = args.filePath;
|
|
@@ -35272,12 +35829,15 @@ function createWriteTool(ctx, editToolName = "edit") {
|
|
|
35272
35829
|
file: filePath,
|
|
35273
35830
|
content,
|
|
35274
35831
|
create_dirs: true,
|
|
35275
|
-
diagnostics:
|
|
35832
|
+
diagnostics: diagnosticsOnEditDefault(ctx),
|
|
35276
35833
|
include_diff_content: true
|
|
35277
35834
|
});
|
|
35278
35835
|
if (data.success === false) {
|
|
35279
35836
|
throw new Error(data.message || "write failed");
|
|
35280
35837
|
}
|
|
35838
|
+
if (data.rolled_back === true) {
|
|
35839
|
+
return "Write rolled back: the content produced invalid syntax, so the file was left unchanged.";
|
|
35840
|
+
}
|
|
35281
35841
|
let output = data.created ? "Created new file." : "File updated.";
|
|
35282
35842
|
if (data.formatted)
|
|
35283
35843
|
output += " Auto-formatted.";
|
|
@@ -35311,27 +35871,24 @@ Note: LSP server(s) did not respond in time: ${pendingServers.join(", ")}. Diagn
|
|
|
35311
35871
|
Note: LSP server(s) exited during this edit: ${exitedServers.join(", ")}. Their diagnostics could not be collected.`;
|
|
35312
35872
|
}
|
|
35313
35873
|
const diff = data.diff;
|
|
35314
|
-
const
|
|
35315
|
-
|
|
35316
|
-
|
|
35317
|
-
|
|
35318
|
-
|
|
35319
|
-
|
|
35320
|
-
|
|
35321
|
-
|
|
35322
|
-
|
|
35323
|
-
|
|
35324
|
-
|
|
35325
|
-
|
|
35326
|
-
|
|
35327
|
-
|
|
35328
|
-
|
|
35329
|
-
|
|
35330
|
-
|
|
35331
|
-
|
|
35332
|
-
});
|
|
35333
|
-
}
|
|
35334
|
-
return output;
|
|
35874
|
+
const dp = relativeToWorktree(filePath, projectRoot);
|
|
35875
|
+
const beforeContent = diff?.before ?? "";
|
|
35876
|
+
const afterContent = diff?.after ?? content;
|
|
35877
|
+
return {
|
|
35878
|
+
output,
|
|
35879
|
+
title: dp,
|
|
35880
|
+
metadata: {
|
|
35881
|
+
diff: buildUnifiedDiff(filePath, beforeContent, afterContent),
|
|
35882
|
+
filediff: {
|
|
35883
|
+
file: filePath,
|
|
35884
|
+
before: beforeContent,
|
|
35885
|
+
after: afterContent,
|
|
35886
|
+
additions: diff?.additions ?? 0,
|
|
35887
|
+
deletions: diff?.deletions ?? 0
|
|
35888
|
+
},
|
|
35889
|
+
diagnostics: {}
|
|
35890
|
+
}
|
|
35891
|
+
};
|
|
35335
35892
|
}
|
|
35336
35893
|
};
|
|
35337
35894
|
}
|
|
@@ -35340,107 +35897,66 @@ function getEditDescription(writeToolName) {
|
|
|
35340
35897
|
|
|
35341
35898
|
**Modes** (determined by which parameters you provide):
|
|
35342
35899
|
|
|
35343
|
-
Mode priority:
|
|
35900
|
+
Mode priority: appendContent > edits > symbol (without oldString) > oldString (find/replace). If none match, the call is rejected — there is no implicit "write" fallback. To edit multiple files, make parallel \`edit\` calls in one response.
|
|
35344
35901
|
|
|
35345
|
-
1. **
|
|
35346
|
-
Edits across multiple files with checkpoint-based rollback on failure.
|
|
35347
|
-
Each operation: \`{ "file": "path", "command": "edit_match" | "write", ... }\`.
|
|
35348
|
-
For \`edit_match\`: include \`match\`, \`replacement\`. For \`write\`: include \`content\`.
|
|
35349
|
-
Example: \`{ "operations": [{ "file": "a.ts", "command": "edit_match", "match": "old", "replacement": "new" }, { "file": "b.ts", "command": "write", "content": "..." }] }\`
|
|
35350
|
-
|
|
35351
|
-
2. **Append** — pass \`filePath\` + \`appendContent\`
|
|
35902
|
+
1. **Append** — pass \`filePath\` + \`appendContent\`
|
|
35352
35903
|
Appends text to the end of a file, creating the file if it does not exist.
|
|
35353
35904
|
Example: \`{ "filePath": "notes.txt", "appendContent": "new line\\n" }\`
|
|
35354
35905
|
|
|
35355
|
-
|
|
35906
|
+
2. **Batch edits** — pass \`filePath\` + \`edits\` array
|
|
35356
35907
|
Multiple edits in one file atomically. Each edit is either:
|
|
35357
35908
|
- \`{ "oldString": "old", "newString": "new" }\` — find/replace
|
|
35358
35909
|
- \`{ "startLine": 5, "endLine": 7, "content": "new lines" }\` — replace line range (1-based, both inclusive)
|
|
35359
35910
|
Set content to empty string to delete lines.
|
|
35360
35911
|
|
|
35361
|
-
|
|
35912
|
+
3. **Symbol replace** — pass \`filePath\` + \`symbol\` + \`content\`
|
|
35362
35913
|
Replaces an entire named symbol (function, class, type) with new content.
|
|
35363
35914
|
Includes decorators, attributes, and doc comments in the replacement range.
|
|
35364
35915
|
**Important:** You must NOT provide \`oldString\` when using symbol mode — if present, the tool silently falls back to find/replace mode.
|
|
35365
35916
|
Example: \`{ "filePath": "src/app.ts", "symbol": "handleRequest", "content": "function handleRequest() { ... }" }\`
|
|
35366
35917
|
|
|
35367
|
-
|
|
35918
|
+
4. **Find and replace** — pass \`filePath\` + \`oldString\` + \`newString\`
|
|
35368
35919
|
Finds the exact text in \`oldString\` and replaces it with \`newString\`.
|
|
35369
35920
|
Supports fuzzy matching (handles whitespace differences automatically).
|
|
35370
35921
|
If multiple matches exist, specify which one with \`occurrence\` or use \`replaceAll: true\`.
|
|
35371
35922
|
Example: \`{ "filePath": "src/app.ts", "oldString": "const x = 1", "newString": "const x = 2" }\`
|
|
35372
35923
|
|
|
35373
|
-
|
|
35924
|
+
5. **Replace all occurrences** — add \`replaceAll: true\`
|
|
35374
35925
|
Replaces every occurrence of \`oldString\` in the file.
|
|
35375
35926
|
Example: \`{ "filePath": "src/app.ts", "oldString": "oldName", "newString": "newName", "replaceAll": true }\`
|
|
35376
35927
|
|
|
35377
|
-
|
|
35928
|
+
6. **Select specific occurrence** — add \`occurrence: N\` (0-indexed)
|
|
35378
35929
|
When multiple matches exist, select the Nth one (0 = first, 1 = second, etc.).
|
|
35379
35930
|
Example: \`{ "filePath": "src/app.ts", "oldString": "TODO", "newString": "DONE", "occurrence": 0 }\`
|
|
35380
35931
|
|
|
35381
|
-
Note: Modes
|
|
35932
|
+
Note: Modes 5 and 6 are options on mode 4 (find/replace) — they require \`oldString\`.
|
|
35382
35933
|
|
|
35383
35934
|
**Behavior:**
|
|
35384
35935
|
- Backs up files before editing (recoverable via aft_safety undo)
|
|
35385
35936
|
- Auto-formats using project formatter if configured
|
|
35386
35937
|
- Tree-sitter syntax validation on all edits
|
|
35387
35938
|
- Symbol replace includes decorators, attributes, and doc comments in range
|
|
35388
|
-
- Edits return as soon as the write completes unless \`lsp.diagnostics_on_edit\` or a per-call \`diagnostics: true\` requests legacy sync-wait behavior. Call \`aft_inspect\` afterward to check diagnostics across a batch of edits.
|
|
35389
35939
|
- Response is a JSON string for the selected edit mode; key fields include success, diff, backup_id, syntax_valid, and mode-specific fields.`;
|
|
35390
35940
|
}
|
|
35391
35941
|
function createEditTool(ctx, writeToolName = "write") {
|
|
35392
35942
|
return {
|
|
35393
35943
|
description: getEditDescription(writeToolName),
|
|
35394
35944
|
args: {
|
|
35395
|
-
filePath: z8.string().optional().describe("Path to the file to edit (absolute or relative to project root)
|
|
35945
|
+
filePath: z8.string().optional().describe("Path to the file to edit (absolute or relative to project root)"),
|
|
35396
35946
|
oldString: z8.string().optional().describe("Text to find (exact match, with fuzzy fallback)"),
|
|
35397
35947
|
newString: z8.string().optional().describe("Text to replace with (omit or set to empty string to delete the matched text)"),
|
|
35398
35948
|
replaceAll: z8.boolean().optional().describe("Replace all occurrences"),
|
|
35399
35949
|
occurrence: optionalInt(0, Number.MAX_SAFE_INTEGER).describe("0-indexed occurrence to replace when multiple matches exist"),
|
|
35400
35950
|
symbol: z8.string().optional().describe("Named symbol to replace (function, class, type)"),
|
|
35401
|
-
content: z8.string().optional().describe("Replacement content for symbol mode
|
|
35951
|
+
content: z8.string().optional().describe("Replacement content for symbol mode. For whole-file writes, use the `write` tool."),
|
|
35402
35952
|
appendContent: z8.string().optional().describe("Text to append to the end of filePath; creates the file if needed"),
|
|
35403
|
-
edits: z8.array(z8.record(z8.string(), z8.unknown())).optional().describe("Batch edits — array of { oldString: string, newString: string } or { startLine: number (1-based), endLine: number (1-based, inclusive), content: string }")
|
|
35404
|
-
operations: z8.array(z8.record(z8.string(), z8.unknown())).optional().describe("Transaction — array of { file: string, command: 'edit_match' | 'write', match?: string, replacement?: string, content?: string } for multi-file edits with rollback. Note: uses 'file'/'match'/'replacement' (not filePath/oldString/newString)"),
|
|
35405
|
-
diagnostics: z8.boolean().optional().describe(DIAGNOSTICS_PARAM_DESCRIPTION)
|
|
35953
|
+
edits: z8.array(z8.record(z8.string(), z8.unknown())).optional().describe("Batch edits — array of { oldString: string, newString: string } or { startLine: number (1-based), endLine: number (1-based, inclusive), content: string }")
|
|
35406
35954
|
},
|
|
35407
35955
|
execute: async (args, context) => {
|
|
35408
35956
|
const argsRecord = args;
|
|
35409
35957
|
if (argsRecord.startLine !== undefined || argsRecord.endLine !== undefined) {
|
|
35410
35958
|
throw new Error("edit: 'startLine'/'endLine' are not top-level parameters. For line-range edits, nest them inside the `edits` array: `edits: [{ startLine: N, endLine: M, content: \"...\" }]`. For find/replace, use `oldString`/`newString` instead.");
|
|
35411
35959
|
}
|
|
35412
|
-
if (Array.isArray(args.operations)) {
|
|
35413
|
-
const ops = args.operations;
|
|
35414
|
-
const files = ops.map((op) => op.file).filter(Boolean);
|
|
35415
|
-
const projectRoot2 = await resolveProjectRoot(ctx, context);
|
|
35416
|
-
{
|
|
35417
|
-
const asked = new Set;
|
|
35418
|
-
for (const file3 of files) {
|
|
35419
|
-
const absPath = resolvePathFromProjectRoot(projectRoot2, file3);
|
|
35420
|
-
if (asked.has(absPath))
|
|
35421
|
-
continue;
|
|
35422
|
-
asked.add(absPath);
|
|
35423
|
-
const denial = await assertExternalDirectoryPermission(context, absPath);
|
|
35424
|
-
if (denial)
|
|
35425
|
-
return permissionDeniedResponse(denial);
|
|
35426
|
-
}
|
|
35427
|
-
}
|
|
35428
|
-
await runAsk(context.ask({
|
|
35429
|
-
permission: "edit",
|
|
35430
|
-
patterns: files.map((f) => path4.relative(projectRoot2, resolvePathFromProjectRoot(projectRoot2, f))),
|
|
35431
|
-
always: ["*"],
|
|
35432
|
-
metadata: {}
|
|
35433
|
-
}));
|
|
35434
|
-
const resolvedOps = ops.map((op) => ({
|
|
35435
|
-
...op,
|
|
35436
|
-
file: resolvePathFromProjectRoot(projectRoot2, op.file)
|
|
35437
|
-
}));
|
|
35438
|
-
const response = await callBridge(ctx, context, "transaction", { operations: resolvedOps });
|
|
35439
|
-
if (response.success === false) {
|
|
35440
|
-
throw new Error(response.message ?? "transaction failed");
|
|
35441
|
-
}
|
|
35442
|
-
return formatEditSummary(response);
|
|
35443
|
-
}
|
|
35444
35960
|
const file2 = args.filePath;
|
|
35445
35961
|
if (!file2)
|
|
35446
35962
|
throw new Error("'filePath' parameter is required");
|
|
@@ -35497,34 +36013,33 @@ function createEditTool(ctx, writeToolName = "write") {
|
|
|
35497
36013
|
if (args.occurrence !== undefined)
|
|
35498
36014
|
params.occurrence = args.occurrence;
|
|
35499
36015
|
} else {
|
|
35500
|
-
const hint = typeof args.content === "string" ? ` To write the whole file, use the '${writeToolName}' tool. To edit existing content, provide 'oldString' (and optionally 'newString'), 'symbol' + 'content', or an 'edits' array.` : " Provide 'oldString' (+ optional 'newString'), 'symbol' + 'content',
|
|
36016
|
+
const hint = typeof args.content === "string" ? ` To write the whole file, use the '${writeToolName}' tool. To edit existing content, provide 'oldString' (and optionally 'newString'), 'symbol' + 'content', or an 'edits' array.` : " Provide 'oldString' (+ optional 'newString'), 'symbol' + 'content', or an 'edits' array.";
|
|
35501
36017
|
throw new Error(`edit: no edit mode resolved from arguments.${hint}`);
|
|
35502
36018
|
}
|
|
35503
|
-
params.diagnostics =
|
|
36019
|
+
params.diagnostics = diagnosticsOnEditDefault(ctx);
|
|
35504
36020
|
params.include_diff_content = true;
|
|
35505
36021
|
const data = await callBridge(ctx, context, command, params);
|
|
36022
|
+
if (data.success === false) {
|
|
36023
|
+
throw new Error(data.message || "edit failed");
|
|
36024
|
+
}
|
|
36025
|
+
let uiMeta;
|
|
36026
|
+
let uiTitle;
|
|
35506
36027
|
if (data.success && data.diff) {
|
|
35507
36028
|
const diff = data.diff;
|
|
35508
|
-
|
|
35509
|
-
|
|
35510
|
-
|
|
35511
|
-
|
|
35512
|
-
|
|
35513
|
-
|
|
35514
|
-
|
|
35515
|
-
|
|
35516
|
-
|
|
35517
|
-
|
|
35518
|
-
|
|
35519
|
-
|
|
35520
|
-
|
|
35521
|
-
|
|
35522
|
-
deletions: diff.deletions ?? 0
|
|
35523
|
-
},
|
|
35524
|
-
diagnostics: {}
|
|
35525
|
-
}
|
|
35526
|
-
});
|
|
35527
|
-
}
|
|
36029
|
+
uiTitle = relativeToWorktree(filePath, projectRoot);
|
|
36030
|
+
const beforeContent = diff.before ?? "";
|
|
36031
|
+
const afterContent = diff.after ?? "";
|
|
36032
|
+
uiMeta = {
|
|
36033
|
+
diff: buildUnifiedDiff(filePath, beforeContent, afterContent),
|
|
36034
|
+
filediff: {
|
|
36035
|
+
file: filePath,
|
|
36036
|
+
before: beforeContent,
|
|
36037
|
+
after: afterContent,
|
|
36038
|
+
additions: diff.additions ?? 0,
|
|
36039
|
+
deletions: diff.deletions ?? 0
|
|
36040
|
+
},
|
|
36041
|
+
diagnostics: {}
|
|
36042
|
+
};
|
|
35528
36043
|
}
|
|
35529
36044
|
let result = formatEditSummary(data);
|
|
35530
36045
|
const globSkipNote = formatGlobSkipReasonsNote(data.format_skip_reasons);
|
|
@@ -35561,6 +36076,9 @@ Note: LSP server(s) did not respond in time: ${pendingServers.join(", ")}. Diagn
|
|
|
35561
36076
|
|
|
35562
36077
|
Note: LSP server(s) exited during this edit: ${exitedServers.join(", ")}. Their diagnostics could not be collected.`;
|
|
35563
36078
|
}
|
|
36079
|
+
if (uiMeta) {
|
|
36080
|
+
return { output: result, title: uiTitle ?? "", metadata: uiMeta };
|
|
36081
|
+
}
|
|
35564
36082
|
return result;
|
|
35565
36083
|
}
|
|
35566
36084
|
};
|
|
@@ -35620,12 +36138,11 @@ function createApplyPatchTool(ctx) {
|
|
|
35620
36138
|
return {
|
|
35621
36139
|
description: APPLY_PATCH_DESCRIPTION,
|
|
35622
36140
|
args: {
|
|
35623
|
-
patchText: z8.string().describe("The full patch text including Begin/End markers")
|
|
35624
|
-
diagnostics: z8.boolean().optional().describe(DIAGNOSTICS_PARAM_DESCRIPTION)
|
|
36141
|
+
patchText: z8.string().describe("The full patch text including Begin/End markers")
|
|
35625
36142
|
},
|
|
35626
36143
|
execute: async (args, context) => {
|
|
35627
36144
|
const patchText = args.patchText;
|
|
35628
|
-
const diagnostics =
|
|
36145
|
+
const diagnostics = diagnosticsOnEditDefault(ctx);
|
|
35629
36146
|
if (!patchText)
|
|
35630
36147
|
throw new Error("'patchText' is required");
|
|
35631
36148
|
let hunks;
|
|
@@ -35710,6 +36227,12 @@ function createApplyPatchTool(ctx) {
|
|
|
35710
36227
|
include_diff_content: true,
|
|
35711
36228
|
multi_file_write_paths: multiFileWritePaths
|
|
35712
36229
|
});
|
|
36230
|
+
if (writeResult.success === false) {
|
|
36231
|
+
throw new Error(writeResult.message ?? "write failed");
|
|
36232
|
+
}
|
|
36233
|
+
if (writeResult.rolled_back === true) {
|
|
36234
|
+
throw new Error("produced invalid syntax (rolled back)");
|
|
36235
|
+
}
|
|
35713
36236
|
const wrDiff = writeResult.diff;
|
|
35714
36237
|
perFileDiffs.push({
|
|
35715
36238
|
filePath,
|
|
@@ -35735,7 +36258,12 @@ function createApplyPatchTool(ctx) {
|
|
|
35735
36258
|
case "delete": {
|
|
35736
36259
|
try {
|
|
35737
36260
|
const before = await fs6.promises.readFile(filePath, "utf-8").catch(() => "");
|
|
35738
|
-
await callBridge(ctx, context, "delete_file", {
|
|
36261
|
+
const deleteResult = await callBridge(ctx, context, "delete_file", {
|
|
36262
|
+
file: filePath
|
|
36263
|
+
});
|
|
36264
|
+
if (deleteResult.success === false) {
|
|
36265
|
+
throw new Error(deleteResult.message ?? "delete failed");
|
|
36266
|
+
}
|
|
35739
36267
|
perFileDiffs.push({
|
|
35740
36268
|
filePath,
|
|
35741
36269
|
before,
|
|
@@ -35763,6 +36291,12 @@ function createApplyPatchTool(ctx) {
|
|
|
35763
36291
|
include_diff_content: true,
|
|
35764
36292
|
multi_file_write_paths: multiFileWritePaths
|
|
35765
36293
|
});
|
|
36294
|
+
if (writeResult.success === false) {
|
|
36295
|
+
throw new Error(writeResult.message ?? "write failed");
|
|
36296
|
+
}
|
|
36297
|
+
if (writeResult.rolled_back === true) {
|
|
36298
|
+
throw new Error("produced invalid syntax (rolled back)");
|
|
36299
|
+
}
|
|
35766
36300
|
const diags = writeResult.lsp_diagnostics;
|
|
35767
36301
|
if (diags && diags.length > 0) {
|
|
35768
36302
|
const errors3 = diags.filter((d) => d.severity === "error");
|
|
@@ -35842,10 +36376,11 @@ ${diagLines}`);
|
|
|
35842
36376
|
`));
|
|
35843
36377
|
}
|
|
35844
36378
|
}
|
|
35845
|
-
|
|
35846
|
-
if (callID) {
|
|
36379
|
+
{
|
|
35847
36380
|
const diffByPath = new Map(perFileDiffs.map((d) => [d.filePath, d]));
|
|
35848
|
-
const
|
|
36381
|
+
const failedPaths = new Set(failures);
|
|
36382
|
+
const appliedHunks = hunks.filter((h) => !failedPaths.has(h.path));
|
|
36383
|
+
const files = appliedHunks.map((h) => {
|
|
35849
36384
|
const filePath = resolvePathFromProjectRoot(projectRoot, h.path);
|
|
35850
36385
|
const rawMovePath = h.type === "update" ? h.move_path : undefined;
|
|
35851
36386
|
const movePath = rawMovePath ? resolvePathFromProjectRoot(projectRoot, rawMovePath) : undefined;
|
|
@@ -35871,20 +36406,21 @@ ${diagLines}`);
|
|
|
35871
36406
|
return `${prefix} ${f.relativePath}`;
|
|
35872
36407
|
}).join(`
|
|
35873
36408
|
`);
|
|
35874
|
-
const title =
|
|
36409
|
+
const title = failures.length > 0 ? `Partially applied (${files.length} of ${hunks.length}). Updated:
|
|
36410
|
+
${fileList}` : `Success. Updated the following files:
|
|
35875
36411
|
${fileList}`;
|
|
35876
36412
|
const diffText = files.map((f) => f.patch).filter(Boolean).join(`
|
|
35877
36413
|
`);
|
|
35878
|
-
|
|
36414
|
+
return {
|
|
36415
|
+
output: results.join(`
|
|
36416
|
+
`),
|
|
35879
36417
|
title,
|
|
35880
36418
|
metadata: {
|
|
35881
36419
|
diff: diffText,
|
|
35882
36420
|
files
|
|
35883
36421
|
}
|
|
35884
|
-
}
|
|
36422
|
+
};
|
|
35885
36423
|
}
|
|
35886
|
-
return results.join(`
|
|
35887
|
-
`);
|
|
35888
36424
|
}
|
|
35889
36425
|
};
|
|
35890
36426
|
}
|
|
@@ -36408,10 +36944,6 @@ function navigationTools(ctx) {
|
|
|
36408
36944
|
// src/tools/reading.ts
|
|
36409
36945
|
import { tool as tool12 } from "@opencode-ai/plugin";
|
|
36410
36946
|
var z12 = tool12.schema;
|
|
36411
|
-
function getCallID3(ctx) {
|
|
36412
|
-
const c = ctx;
|
|
36413
|
-
return c.callID ?? c.callId ?? c.call_id;
|
|
36414
|
-
}
|
|
36415
36947
|
function buildZoomTitle(args) {
|
|
36416
36948
|
if (!isEmptyParam(args.targets)) {
|
|
36417
36949
|
if (Array.isArray(args.targets)) {
|
|
@@ -36574,25 +37106,26 @@ function readingTools(ctx) {
|
|
|
36574
37106
|
const hasTargets = hasTargetsProvided(args.targets);
|
|
36575
37107
|
const hasSymbols = !isEmptyParam(args.symbols);
|
|
36576
37108
|
const wantCallgraph = args.callgraph === true;
|
|
36577
|
-
const
|
|
36578
|
-
|
|
36579
|
-
|
|
36580
|
-
|
|
36581
|
-
|
|
36582
|
-
|
|
36583
|
-
|
|
36584
|
-
|
|
36585
|
-
|
|
36586
|
-
|
|
36587
|
-
|
|
36588
|
-
|
|
36589
|
-
|
|
36590
|
-
|
|
36591
|
-
|
|
36592
|
-
|
|
36593
|
-
|
|
36594
|
-
|
|
36595
|
-
|
|
37109
|
+
const zoomTitle = buildZoomTitle(args);
|
|
37110
|
+
const zoomDisplay = { title: zoomTitle };
|
|
37111
|
+
if (hasFilePath)
|
|
37112
|
+
zoomDisplay.filePath = args.filePath;
|
|
37113
|
+
if (hasUrl)
|
|
37114
|
+
zoomDisplay.url = args.url;
|
|
37115
|
+
if (hasSymbols) {
|
|
37116
|
+
zoomDisplay.symbols = typeof args.symbols === "string" ? args.symbols : JSON.stringify(args.symbols);
|
|
37117
|
+
}
|
|
37118
|
+
if (hasTargets)
|
|
37119
|
+
zoomDisplay.targets = JSON.stringify(args.targets);
|
|
37120
|
+
if (args.contextLines !== undefined)
|
|
37121
|
+
zoomDisplay.contextLines = args.contextLines;
|
|
37122
|
+
if (wantCallgraph)
|
|
37123
|
+
zoomDisplay.callgraph = true;
|
|
37124
|
+
const withMeta = (output) => ({
|
|
37125
|
+
output,
|
|
37126
|
+
title: zoomTitle,
|
|
37127
|
+
metadata: zoomDisplay
|
|
37128
|
+
});
|
|
36596
37129
|
if (hasTargets) {
|
|
36597
37130
|
if (hasFilePath || hasUrl || hasSymbols) {
|
|
36598
37131
|
throw new Error("'targets' is mutually exclusive with 'filePath', 'url', and 'symbols'");
|
|
@@ -36632,7 +37165,7 @@ function readingTools(ctx) {
|
|
|
36632
37165
|
name: t.symbol,
|
|
36633
37166
|
response: responses[i] ?? { success: false, message: "missing zoom response" }
|
|
36634
37167
|
}));
|
|
36635
|
-
return formatZoomMultiTargetResult(entries).text;
|
|
37168
|
+
return withMeta(formatZoomMultiTargetResult(entries).text);
|
|
36636
37169
|
}
|
|
36637
37170
|
if (!hasFilePath && !hasUrl) {
|
|
36638
37171
|
throw new Error("Provide exactly one of 'filePath', 'url', or 'targets'");
|
|
@@ -36665,9 +37198,9 @@ function readingTools(ctx) {
|
|
|
36665
37198
|
if (response.success === false) {
|
|
36666
37199
|
throw new Error(response.message || "zoom failed");
|
|
36667
37200
|
}
|
|
36668
|
-
return formatZoomText(targetLabel, response);
|
|
37201
|
+
return withMeta(formatZoomText(targetLabel, response));
|
|
36669
37202
|
}
|
|
36670
|
-
return formatZoomBatchResult(targetLabel, symbolsArray, results).text;
|
|
37203
|
+
return withMeta(formatZoomBatchResult(targetLabel, symbolsArray, results).text);
|
|
36671
37204
|
}
|
|
36672
37205
|
const params = { file: file2 };
|
|
36673
37206
|
if (args.contextLines !== undefined)
|
|
@@ -36678,7 +37211,7 @@ function readingTools(ctx) {
|
|
|
36678
37211
|
if (data.success === false) {
|
|
36679
37212
|
throw new Error(data.message || "zoom failed");
|
|
36680
37213
|
}
|
|
36681
|
-
return formatZoomText(targetLabel, data);
|
|
37214
|
+
return withMeta(formatZoomText(targetLabel, data));
|
|
36682
37215
|
}
|
|
36683
37216
|
}
|
|
36684
37217
|
};
|
|
@@ -36834,17 +37367,12 @@ var z13 = tool13.schema;
|
|
|
36834
37367
|
function refactoringTools(ctx) {
|
|
36835
37368
|
return {
|
|
36836
37369
|
aft_refactor: {
|
|
36837
|
-
description: `Workspace-wide refactoring
|
|
37370
|
+
description: `Workspace-wide refactoring that updates imports and references across files.
|
|
36838
37371
|
|
|
36839
37372
|
` + `Ops:
|
|
36840
|
-
` + `- 'move':
|
|
36841
|
-
` +
|
|
36842
|
-
` +
|
|
36843
|
-
` + `- 'inline': Replace a function call with the function's body, substituting args for params. Requires 'symbol', 'callSiteLine' (1-based). Validates single-return constraint.
|
|
36844
|
-
|
|
36845
|
-
` + `Each op requires specific parameters — see parameter descriptions for requirements.
|
|
36846
|
-
|
|
36847
|
-
` + "All ops need 'filePath'. Use aft_safety checkpoint/undo before risky refactors.",
|
|
37373
|
+
` + `- 'move': move a top-level symbol (not nested functions or class methods) to another file, rewriting imports workspace-wide. A checkpoint is created first. To move/rename a whole file, use aft_move.
|
|
37374
|
+
` + `- 'extract': extract a line range into a new function with auto-detected parameters (TS/JS/TSX, Python).
|
|
37375
|
+
` + "- 'inline': replace a function call with the function's body.",
|
|
36848
37376
|
args: {
|
|
36849
37377
|
op: z13.enum(["move", "extract", "inline"]).describe("Refactoring operation"),
|
|
36850
37378
|
filePath: z13.string().describe("Path to the source file (absolute or relative to project root)"),
|
|
@@ -37013,9 +37541,10 @@ function safetyTools(ctx) {
|
|
|
37013
37541
|
if (Array.isArray(checkpointFiles)) {
|
|
37014
37542
|
const projectRoot = await resolveProjectRoot(ctx, context);
|
|
37015
37543
|
const uniqueParents = new Set;
|
|
37016
|
-
for (const
|
|
37017
|
-
if (typeof
|
|
37544
|
+
for (const rawFile of checkpointFiles) {
|
|
37545
|
+
if (typeof rawFile !== "string")
|
|
37018
37546
|
continue;
|
|
37547
|
+
const file2 = expandTilde(rawFile);
|
|
37019
37548
|
const abs = path5.isAbsolute(file2) ? file2 : path5.resolve(projectRoot, file2);
|
|
37020
37549
|
const parent = path5.dirname(abs);
|
|
37021
37550
|
if (uniqueParents.has(parent))
|
|
@@ -37055,16 +37584,17 @@ function safetyTools(ctx) {
|
|
|
37055
37584
|
const params = {};
|
|
37056
37585
|
if (args.name !== undefined)
|
|
37057
37586
|
params.name = args.name;
|
|
37058
|
-
const payloadFiles = coerceStringArray(args.files);
|
|
37587
|
+
const payloadFiles = coerceStringArray(args.files).map(expandTilde);
|
|
37588
|
+
const filePathArg = typeof args.filePath === "string" ? expandTilde(args.filePath) : undefined;
|
|
37059
37589
|
if (op === "checkpoint") {
|
|
37060
37590
|
if (payloadFiles.length > 0) {
|
|
37061
37591
|
params.files = payloadFiles;
|
|
37062
|
-
} else if (
|
|
37063
|
-
params.files = [
|
|
37592
|
+
} else if (filePathArg !== undefined) {
|
|
37593
|
+
params.files = [filePathArg];
|
|
37064
37594
|
}
|
|
37065
37595
|
} else {
|
|
37066
|
-
if (
|
|
37067
|
-
params.file =
|
|
37596
|
+
if (filePathArg !== undefined)
|
|
37597
|
+
params.file = filePathArg;
|
|
37068
37598
|
if (payloadFiles.length > 0)
|
|
37069
37599
|
params.files = payloadFiles;
|
|
37070
37600
|
}
|
|
@@ -37092,7 +37622,7 @@ function formatGrepOutput(response) {
|
|
|
37092
37622
|
const totalMatches = response.total_matches ?? matches.length;
|
|
37093
37623
|
const filesWithMatches = response.files_with_matches ?? new Set(matches.map((m) => m.file)).size;
|
|
37094
37624
|
if (matches.length === 0) {
|
|
37095
|
-
return `Found ${totalMatches} match
|
|
37625
|
+
return `Found ${totalMatches} match across ${filesWithMatches} file`;
|
|
37096
37626
|
}
|
|
37097
37627
|
const body = matches.map((match) => {
|
|
37098
37628
|
const file2 = match.file ?? "unknown";
|
|
@@ -37103,7 +37633,7 @@ function formatGrepOutput(response) {
|
|
|
37103
37633
|
`);
|
|
37104
37634
|
return `${body}
|
|
37105
37635
|
|
|
37106
|
-
Found ${totalMatches} match
|
|
37636
|
+
Found ${totalMatches} match across ${filesWithMatches} file`;
|
|
37107
37637
|
}
|
|
37108
37638
|
function normalizeGlob(pattern) {
|
|
37109
37639
|
if (!pattern.includes("/") && !pattern.startsWith("**/")) {
|
|
@@ -37308,21 +37838,9 @@ function arg3(schema) {
|
|
|
37308
37838
|
function semanticTools(ctx) {
|
|
37309
37839
|
const searchTool = {
|
|
37310
37840
|
description: [
|
|
37311
|
-
"
|
|
37312
|
-
"",
|
|
37313
|
-
"When to reach for it:",
|
|
37314
|
-
"- Exploring an unfamiliar area: 'where is rate limiting handled', 'how does auth flow work'",
|
|
37315
|
-
"- Concept doesn't appear as a literal string: 'retry logic', 'cache invalidation', 'graceful shutdown'",
|
|
37316
|
-
"- Filename-shaped concepts: 'the bridge spawn helper', 'the session detection module'",
|
|
37317
|
-
"- Regex-shaped or exact text queries when you want AFT to classify and route automatically",
|
|
37318
|
-
"- You know roughly what the function does but not what it's named",
|
|
37841
|
+
"Search code with one tool: concepts, identifiers, error strings, regex, literals, and filenames are auto-routed to the right engine and returned ranked. Use it for any code search — including when you only know what the code does, not what it's named ('where is rate limiting handled', 'retry logic', '^export', 'Cargo.lock').",
|
|
37319
37842
|
"",
|
|
37320
|
-
"
|
|
37321
|
-
"- You need exhaustive literal enumeration → use grep directly",
|
|
37322
|
-
"- You want the file/module structure → use aft_outline",
|
|
37323
|
-
"- You're following a call chain → use aft_callgraph",
|
|
37324
|
-
"",
|
|
37325
|
-
"Set hint to 'regex', 'literal', 'semantic', or 'auto' to override or document routing intent."
|
|
37843
|
+
"Set hint to 'regex', 'literal', or 'semantic' to force a lane."
|
|
37326
37844
|
].join(`
|
|
37327
37845
|
`),
|
|
37328
37846
|
args: {
|
|
@@ -37366,118 +37884,8 @@ ${note}` : response.text;
|
|
|
37366
37884
|
};
|
|
37367
37885
|
}
|
|
37368
37886
|
|
|
37369
|
-
// src/tools/structure.ts
|
|
37370
|
-
import { tool as tool16 } from "@opencode-ai/plugin";
|
|
37371
|
-
var z16 = tool16.schema;
|
|
37372
|
-
function structureTools(ctx) {
|
|
37373
|
-
return {
|
|
37374
|
-
aft_transform: {
|
|
37375
|
-
description: `Scope-aware structural code transformations with correct indentation.
|
|
37376
|
-
|
|
37377
|
-
` + `Language-specific: add_derive (Rust), add_struct_tags (Go), add_decorator (Python), wrap_try_catch (TS/JS), add_member (any language).
|
|
37378
|
-
|
|
37379
|
-
` + `Ops:
|
|
37380
|
-
` + `- 'add_member': Insert method/field into class, struct, or impl block. Requires 'container' (container name) and 'code'. Optional 'position'.
|
|
37381
|
-
` + `- 'add_derive': Add Rust derive macros to a struct/enum. Requires 'target' and 'derives' array. Deduplicates existing derives.
|
|
37382
|
-
` + `- 'wrap_try_catch': Wrap a TS/JS function body in try/catch. Requires 'target' (function name). Optional 'catchBody'.
|
|
37383
|
-
` + `- 'add_decorator': Add Python decorator to function/class. Requires 'target' and 'decorator' (without @). Optional 'position'.
|
|
37384
|
-
` + `- 'add_struct_tags': Add/update Go struct field tags. Requires 'target' (struct name), 'field', 'tag', 'value'.
|
|
37385
|
-
|
|
37386
|
-
` + "Each op requires specific parameters — see parameter descriptions for requirements. Use aft_safety checkpoint/undo before risky transforms.",
|
|
37387
|
-
args: {
|
|
37388
|
-
op: z16.enum(["add_member", "add_derive", "wrap_try_catch", "add_decorator", "add_struct_tags"]).describe("Transformation operation"),
|
|
37389
|
-
filePath: z16.string().describe("Path to the source file (absolute or relative to project root)"),
|
|
37390
|
-
container: z16.string().optional().describe("Container name for add_member — the class, struct, or impl block to insert into. Appears as 'scope' in the response."),
|
|
37391
|
-
code: z16.string().optional().describe("Member code to insert (add_member)"),
|
|
37392
|
-
position: z16.string().optional().describe("For add_member: 'first', 'last' (default), 'before:name', 'after:name'. For add_decorator: 'first' (default) or 'last' only."),
|
|
37393
|
-
target: z16.string().optional().describe("Target symbol name (add_derive: struct/enum, wrap_try_catch: function, add_decorator: function/class, add_struct_tags: struct)"),
|
|
37394
|
-
derives: z16.array(z16.string()).optional().describe("Derive macro names (add_derive — e.g. ['Clone', 'Debug'])"),
|
|
37395
|
-
catchBody: z16.string().optional().describe("Catch block body (wrap_try_catch — default: 'throw error;')"),
|
|
37396
|
-
decorator: z16.string().optional().describe("Decorator text without @ (add_decorator — e.g. 'staticmethod')"),
|
|
37397
|
-
field: z16.string().optional().describe("Struct field name (add_struct_tags)"),
|
|
37398
|
-
tag: z16.string().optional().describe("Tag key (add_struct_tags — e.g. 'json')"),
|
|
37399
|
-
value: z16.string().optional().describe("Tag value (add_struct_tags — e.g. 'user_name,omitempty')"),
|
|
37400
|
-
validate: z16.enum(["syntax", "full"]).optional().describe("Validation level: 'syntax' (default) or 'full'. Syntax = tree-sitter parse check only. Full = also runs LSP type-checking (slower, catches more errors)")
|
|
37401
|
-
},
|
|
37402
|
-
execute: async (args, context) => {
|
|
37403
|
-
const op = args.op;
|
|
37404
|
-
if (op === "add_member") {
|
|
37405
|
-
if (isEmptyParam(args.container))
|
|
37406
|
-
throw new Error("'container' is required for 'add_member' op");
|
|
37407
|
-
if (isEmptyParam(args.code))
|
|
37408
|
-
throw new Error("'code' is required for 'add_member' op");
|
|
37409
|
-
}
|
|
37410
|
-
if (op === "add_derive" || op === "wrap_try_catch" || op === "add_decorator" || op === "add_struct_tags") {
|
|
37411
|
-
if (isEmptyParam(args.target))
|
|
37412
|
-
throw new Error(`'target' is required for '${op}' op`);
|
|
37413
|
-
}
|
|
37414
|
-
if (op === "add_derive" && isEmptyParam(args.derives)) {
|
|
37415
|
-
throw new Error("'derives' array is required for 'add_derive' op");
|
|
37416
|
-
}
|
|
37417
|
-
if (op === "add_decorator" && isEmptyParam(args.decorator)) {
|
|
37418
|
-
throw new Error("'decorator' is required for 'add_decorator' op");
|
|
37419
|
-
}
|
|
37420
|
-
if (op === "add_struct_tags") {
|
|
37421
|
-
if (isEmptyParam(args.field))
|
|
37422
|
-
throw new Error("'field' is required for 'add_struct_tags' op");
|
|
37423
|
-
if (isEmptyParam(args.tag))
|
|
37424
|
-
throw new Error("'tag' is required for 'add_struct_tags' op");
|
|
37425
|
-
if (isEmptyParam(args.value))
|
|
37426
|
-
throw new Error("'value' is required for 'add_struct_tags' op");
|
|
37427
|
-
}
|
|
37428
|
-
const filePath = await resolvePathArg(ctx, context, args.filePath);
|
|
37429
|
-
{
|
|
37430
|
-
const denial = await assertExternalDirectoryPermission(context, filePath);
|
|
37431
|
-
if (denial)
|
|
37432
|
-
return permissionDeniedResponse(denial);
|
|
37433
|
-
}
|
|
37434
|
-
const permissionError = await askEditPermission(context, [resolveRelativePattern(context, filePath)], { filepath: filePath });
|
|
37435
|
-
if (permissionError)
|
|
37436
|
-
return permissionDeniedResponse(permissionError);
|
|
37437
|
-
const params = { file: filePath };
|
|
37438
|
-
if (args.validate !== undefined)
|
|
37439
|
-
params.validate = args.validate;
|
|
37440
|
-
switch (op) {
|
|
37441
|
-
case "add_member":
|
|
37442
|
-
params.scope = args.container;
|
|
37443
|
-
params.code = args.code;
|
|
37444
|
-
if (args.position !== undefined)
|
|
37445
|
-
params.position = args.position;
|
|
37446
|
-
break;
|
|
37447
|
-
case "add_derive":
|
|
37448
|
-
params.target = args.target;
|
|
37449
|
-
params.derives = args.derives;
|
|
37450
|
-
break;
|
|
37451
|
-
case "wrap_try_catch":
|
|
37452
|
-
params.target = args.target;
|
|
37453
|
-
if (args.catchBody !== undefined)
|
|
37454
|
-
params.catch_body = args.catchBody;
|
|
37455
|
-
break;
|
|
37456
|
-
case "add_decorator":
|
|
37457
|
-
params.target = args.target;
|
|
37458
|
-
params.decorator = args.decorator;
|
|
37459
|
-
if (args.position !== undefined)
|
|
37460
|
-
params.position = args.position;
|
|
37461
|
-
break;
|
|
37462
|
-
case "add_struct_tags":
|
|
37463
|
-
params.target = args.target;
|
|
37464
|
-
params.field = args.field;
|
|
37465
|
-
params.tag = args.tag;
|
|
37466
|
-
params.value = args.value;
|
|
37467
|
-
break;
|
|
37468
|
-
}
|
|
37469
|
-
const response = await callBridge(ctx, context, op, params);
|
|
37470
|
-
if (response.success === false) {
|
|
37471
|
-
throw new Error(response.message || `${op} failed`);
|
|
37472
|
-
}
|
|
37473
|
-
return JSON.stringify(response);
|
|
37474
|
-
}
|
|
37475
|
-
}
|
|
37476
|
-
};
|
|
37477
|
-
}
|
|
37478
|
-
|
|
37479
37887
|
// src/workflow-hints.ts
|
|
37480
|
-
var HEADING = "##
|
|
37888
|
+
var HEADING = "## IMPORTANT NOTICE about your tools";
|
|
37481
37889
|
function buildWorkflowHints(opts) {
|
|
37482
37890
|
const sections = [];
|
|
37483
37891
|
const grepName = opts.hoistBuiltins ? "grep" : "aft_grep";
|
|
@@ -37493,14 +37901,28 @@ function buildWorkflowHints(opts) {
|
|
|
37493
37901
|
const hasBash = !opts.disabledTools.has(bashName);
|
|
37494
37902
|
const hasBgBash = hasBash && opts.bashBackgroundEnabled && !opts.disabledTools.has(bashStatusName);
|
|
37495
37903
|
if (hasBash && opts.bashCompressionEnabled) {
|
|
37496
|
-
sections.push(
|
|
37904
|
+
sections.push([
|
|
37905
|
+
"**Test/build output**: bash output is auto-compressed — failures and the summary are always kept. DO NOT pipe test/build commands through filters to summarize; that hides failures:",
|
|
37906
|
+
"- `bun test | grep fail` → run `bun test`",
|
|
37907
|
+
"- `cargo test 2>&1 | tail -20` → run `cargo test`",
|
|
37908
|
+
"- `npm run build | head -50` → run `npm run build`"
|
|
37909
|
+
].join(`
|
|
37910
|
+
`));
|
|
37497
37911
|
}
|
|
37498
37912
|
if (hasOutline && hasZoom) {
|
|
37499
37913
|
sections.push(`**Web/URL access**: \`aft_outline({ target: url })\` first for structure, then \`aft_zoom({ url, symbols: "<heading>" })\` for the specific section.`);
|
|
37500
37914
|
}
|
|
37501
37915
|
if (hasOutline && hasZoom && (hasGrep || hasSearch)) {
|
|
37916
|
+
const searchName = hasSearch ? "aft_search" : grepName;
|
|
37502
37917
|
const locate = hasSearch ? '`aft_search` is the primary code-search tool: one call auto-routes concepts, identifiers, regex, error strings, and literals (pass `hint: "regex"`/`"literal"`/`"semantic"` to force a lane).' : `\`${grepName}\` (the tool — indexed and ranked) locates code.`;
|
|
37503
|
-
|
|
37918
|
+
const readName = opts.hoistBuiltins ? "read" : "aft_read";
|
|
37919
|
+
sections.push([
|
|
37920
|
+
`**Code exploration**: ${locate} Then \`aft_outline\` for structure → \`aft_zoom\` for symbol(s). DO NOT run \`grep\`/\`rg\`/\`find\`/\`sed\`/\`cat\` through \`bash\` to locate or read code — the bash path is unindexed, unranked, serial, and routinely surfaces the wrong hit. Keep \`bash\` for shell facts (git state, file metadata, processes). Reflex translations:`,
|
|
37921
|
+
`- \`grep -rn "handleAuth" src/\` in bash → \`${searchName}({ query: "handleAuth" })\``,
|
|
37922
|
+
`- \`find . -name "*.ts" | xargs grep watcher\` in bash → \`${searchName}({ query: "watcher invalidation" })\` (concepts work too)`,
|
|
37923
|
+
`- \`sed -n '100,160p' app.ts\` / \`cat app.ts\` in bash → \`${readName}({ filePath: "app.ts", startLine: 100, endLine: 160 })\``
|
|
37924
|
+
].join(`
|
|
37925
|
+
`));
|
|
37504
37926
|
}
|
|
37505
37927
|
if (hasInspect) {
|
|
37506
37928
|
sections.push("**Codebase health & diagnostics**: AFT does not surface compile/type errors automatically after edits — pull them with `aft_inspect`. Run it after a batch of edits and before you run tests or commit, when starting in unfamiliar code, or before a refactor/review. One call summarizes diagnostics (compile/type errors), TODOs, metrics, dead code, unused exports, and duplicates; pass `sections` for focused drill-down and `scope` to actively pull diagnostics for a specific file or directory. Its diagnostics are a fast checkpoint, not the authority — a clean `tsc` / `cargo check` / `pyright` run is the real gate. Treat `stale_categories` as a genuine stale-cache signal while an async Tier 2 refresh catches up.");
|
|
@@ -37518,13 +37940,22 @@ function buildWorkflowHints(opts) {
|
|
|
37518
37940
|
`));
|
|
37519
37941
|
}
|
|
37520
37942
|
if (hasBash && hasBgBash) {
|
|
37521
|
-
sections.push(
|
|
37943
|
+
sections.push([
|
|
37944
|
+
`**Long-running commands** (builds, installs, full test suites): \`${bashName}({ background: true })\` returns a \`taskId\` immediately. Then do ONE of these:`,
|
|
37945
|
+
"1. Keep working on something independent of the result.",
|
|
37946
|
+
"2. End your turn — a completion reminder with the result arrives automatically.",
|
|
37947
|
+
`3. Need to react to a specific output line early? Register \`bash_watch({ taskId, pattern, background: true })\`, then end your turn — it pings you the moment the pattern appears.`,
|
|
37948
|
+
`\`${bashStatusName}({ taskId })\` is for inspecting output AFTER the reminder arrives, or one quick look at a live task — never call it repeatedly to wait: the reminder already delivers the result, and each poll wastes a turn. Sync \`bash_watch\` (without \`background: true\`) blocks your turn and locks the user out — reserve it for waits of a few seconds (a dev server printing its ready line), never for builds or test suites.`
|
|
37949
|
+
].join(`
|
|
37950
|
+
`));
|
|
37522
37951
|
sections.push(`**PTY / interactive commands**: PTY mode is for interactive REPLs and terminal apps (python, node, bash itself, vim). Start with \`${bashName}({ command: "python", pty: true, background: true })\`, read the screen with \`${bashStatusName}({ taskId, outputMode: "screen" })\`, and send input with \`${bashWriteName}({ taskId, input: "..." })\`.`);
|
|
37523
37952
|
}
|
|
37524
37953
|
if (sections.length === 0) {
|
|
37525
37954
|
return null;
|
|
37526
37955
|
}
|
|
37527
|
-
sections.unshift(
|
|
37956
|
+
sections.unshift(`You are equipped with a non-standard tool set: indexed code search, symbol-level reading, structural editing, and code analysis that are faster, more precise, and far cheaper in tokens than stitching together command-line utilities in bash. Always reach for these tools first.
|
|
37957
|
+
|
|
37958
|
+
**Parallel tool calls**: when several read-only operations are independent, emit them in ONE response instead of serializing — file reads, structure and symbol lookups, code search, diagnostics, and git status/diff/log. Sequence only when a call depends on a prior result or when a command mutates state.`);
|
|
37528
37959
|
return `${HEADING}
|
|
37529
37960
|
|
|
37530
37961
|
${sections.join(`
|
|
@@ -37586,12 +38017,12 @@ var PLUGIN_VERSION = (() => {
|
|
|
37586
38017
|
return "0.0.0";
|
|
37587
38018
|
}
|
|
37588
38019
|
})();
|
|
37589
|
-
var ANNOUNCEMENT_VERSION = "0.
|
|
38020
|
+
var ANNOUNCEMENT_VERSION = "0.37.0";
|
|
37590
38021
|
var ANNOUNCEMENT_FEATURES = [
|
|
37591
|
-
"
|
|
37592
|
-
"
|
|
37593
|
-
"
|
|
37594
|
-
"
|
|
38022
|
+
"Much lower background CPU: idle bridges shut down after 30 minutes, deleted project roots stop the watcher instead of spinning it, and background scans reuse warm caches on a bounded thread pool.",
|
|
38023
|
+
"`aft_transform` removed: usage data showed agents never called it — `edit` covers everything it did, and every request now carries a smaller tool surface.",
|
|
38024
|
+
"Leaner tools: `bash`, `write`, `edit`, `apply_patch`, `aft_search`, and `aft_refactor` descriptions trimmed and config-aware; system-prompt guidance rewritten around what to do instead of what to avoid.",
|
|
38025
|
+
"Background bash reminders are right-sized: failures keep head + tail context, successes show a short tail, and compressors never print a success summary for a non-zero exit."
|
|
37595
38026
|
];
|
|
37596
38027
|
var ANNOUNCEMENT_FOOTER = "Join us on Discord: https://discord.gg/DSa65w8wuf";
|
|
37597
38028
|
var plugin = async (input) => initializePluginForDirectory(input);
|
|
@@ -37816,17 +38247,25 @@ ${lines}
|
|
|
37816
38247
|
});
|
|
37817
38248
|
rpcServer.handle("status", async (params) => {
|
|
37818
38249
|
const sessionID = params.sessionID || "rpc";
|
|
37819
|
-
const cachedDir = getSessionDirectoryCached(sessionID);
|
|
37820
|
-
const candidateDirs = new Set;
|
|
37821
|
-
if (typeof cachedDir === "string" && cachedDir.length > 0) {
|
|
37822
|
-
candidateDirs.add(cachedDir);
|
|
37823
|
-
}
|
|
37824
|
-
candidateDirs.add(input.directory);
|
|
37825
38250
|
let bridge = null;
|
|
37826
|
-
|
|
37827
|
-
|
|
37828
|
-
|
|
37829
|
-
|
|
38251
|
+
let servedDirectory = input.directory;
|
|
38252
|
+
const realSessionID = params.sessionID || "";
|
|
38253
|
+
const verifiedDir = realSessionID ? await verifySessionDirectory(input.client, realSessionID) : null;
|
|
38254
|
+
if (verifiedDir) {
|
|
38255
|
+
bridge = pool.getActiveBridgeForRoot(verifiedDir);
|
|
38256
|
+
if (bridge) {
|
|
38257
|
+
servedDirectory = verifiedDir;
|
|
38258
|
+
} else if (verifiedDir !== input.directory) {
|
|
38259
|
+
return {
|
|
38260
|
+
success: true,
|
|
38261
|
+
status: "not_initialized",
|
|
38262
|
+
message: "AFT bridge is now spawned lazily, information here will be populated after first tool call."
|
|
38263
|
+
};
|
|
38264
|
+
}
|
|
38265
|
+
}
|
|
38266
|
+
if (!bridge) {
|
|
38267
|
+
bridge = pool.getActiveBridgeForRoot(input.directory);
|
|
38268
|
+
servedDirectory = input.directory;
|
|
37830
38269
|
}
|
|
37831
38270
|
if (!bridge) {
|
|
37832
38271
|
return {
|
|
@@ -37839,13 +38278,13 @@ ${lines}
|
|
|
37839
38278
|
const cachedSessionId = cached2?.session;
|
|
37840
38279
|
const cachedId = cachedSessionId?.id;
|
|
37841
38280
|
if (cached2 !== null && cachedId === sessionID) {
|
|
37842
|
-
return { success: true, ...cached2 };
|
|
38281
|
+
return { success: true, ...cached2, served_directory: servedDirectory };
|
|
37843
38282
|
}
|
|
37844
38283
|
const response = await bridge.send("status", { session_id: sessionID });
|
|
37845
38284
|
if (response.success !== false) {
|
|
37846
38285
|
bridge.cacheStatusSnapshot(response);
|
|
37847
38286
|
}
|
|
37848
|
-
return response;
|
|
38287
|
+
return { ...response, served_directory: servedDirectory };
|
|
37849
38288
|
});
|
|
37850
38289
|
const storageDir = configOverrides.storage_dir;
|
|
37851
38290
|
rpcServer.handle("get-announcement", async () => {
|
|
@@ -37905,19 +38344,12 @@ Install: ${getManualInstallHint()}`).catch(() => {});
|
|
|
37905
38344
|
cleanupWarnings(notifyOpts).catch(() => {});
|
|
37906
38345
|
}
|
|
37907
38346
|
const surface = aftConfig.tool_surface ?? "recommended";
|
|
37908
|
-
const ALL_ONLY_TOOLS = new Set([
|
|
37909
|
-
"aft_callgraph",
|
|
37910
|
-
"aft_delete",
|
|
37911
|
-
"aft_move",
|
|
37912
|
-
"aft_transform",
|
|
37913
|
-
"aft_refactor"
|
|
37914
|
-
]);
|
|
38347
|
+
const ALL_ONLY_TOOLS = new Set(["aft_callgraph", "aft_delete", "aft_move", "aft_refactor"]);
|
|
37915
38348
|
const allTools = normalizeToolMap({
|
|
37916
38349
|
...surface !== "minimal" && (aftConfig.hoist_builtin_tools !== false ? hoistedTools(ctx) : aftPrefixedTools(ctx)),
|
|
37917
38350
|
...readingTools(ctx),
|
|
37918
38351
|
...safetyTools(ctx),
|
|
37919
38352
|
...surface !== "minimal" && importTools(ctx),
|
|
37920
|
-
...structureTools(ctx),
|
|
37921
38353
|
...navigationTools(ctx),
|
|
37922
38354
|
...surface !== "minimal" && astTools(ctx),
|
|
37923
38355
|
...surface !== "minimal" && aftConfig.semantic_search === true && semanticTools(ctx),
|
|
@@ -37944,6 +38376,7 @@ Install: ${getManualInstallHint()}`).catch(() => {});
|
|
|
37944
38376
|
}
|
|
37945
38377
|
log2(`Disabled ${disabled.size} tool(s): ${[...disabled].join(", ")}`);
|
|
37946
38378
|
}
|
|
38379
|
+
instrumentToolMap(allTools);
|
|
37947
38380
|
const autoUpdateEventHook = createAutoUpdateCheckerHook(input, {
|
|
37948
38381
|
enabled: true,
|
|
37949
38382
|
autoUpdate: aftConfig.auto_update ?? true,
|
|
@@ -37963,7 +38396,16 @@ Install: ${getManualInstallHint()}`).catch(() => {});
|
|
|
37963
38396
|
"bash_status"
|
|
37964
38397
|
];
|
|
37965
38398
|
const registeredTools = new Set(Object.keys(allTools));
|
|
37966
|
-
|
|
38399
|
+
const aftSearchRegistered = registeredTools.has("aft_search");
|
|
38400
|
+
pool.setConfigureOverride("aft_search_registered", aftSearchRegistered);
|
|
38401
|
+
ctx.aftSearchRegistered = aftSearchRegistered;
|
|
38402
|
+
for (const name of ["bash", "aft_bash"]) {
|
|
38403
|
+
const def = allTools[name];
|
|
38404
|
+
if (def) {
|
|
38405
|
+
const bashCfg = resolveBashConfig(aftConfig);
|
|
38406
|
+
def.description = bashToolDescription(aftSearchRegistered, bashCfg.compress, bashCfg.background);
|
|
38407
|
+
}
|
|
38408
|
+
}
|
|
37967
38409
|
const hintsAbsentTools = new Set;
|
|
37968
38410
|
for (const name of HINTS_TOOL_NAMES) {
|
|
37969
38411
|
if (!registeredTools.has(name))
|
|
@@ -38050,13 +38492,6 @@ Install: ${getManualInstallHint()}`).catch(() => {});
|
|
|
38050
38492
|
"tool.execute.after": async (toolInput, output) => {
|
|
38051
38493
|
if (!output)
|
|
38052
38494
|
return;
|
|
38053
|
-
const stored = consumeToolMetadata(toolInput.sessionID, toolInput.callID);
|
|
38054
|
-
if (stored) {
|
|
38055
|
-
if (stored.title)
|
|
38056
|
-
output.title = stored.title;
|
|
38057
|
-
if (stored.metadata)
|
|
38058
|
-
output.metadata = { ...output.metadata, ...stored.metadata };
|
|
38059
|
-
}
|
|
38060
38495
|
if (toolInput.tool === "bash" && output.output) {
|
|
38061
38496
|
output.output = maybeAppendConflictsHint(output.output);
|
|
38062
38497
|
}
|
|
@@ -38064,7 +38499,7 @@ Install: ${getManualInstallHint()}`).catch(() => {});
|
|
|
38064
38499
|
await appendInTurnBgCompletions({ ctx, directory: sessionDir, sessionID: toolInput.sessionID }, output);
|
|
38065
38500
|
if (output.output !== undefined) {
|
|
38066
38501
|
const activeBridge = ctx.pool.getActiveBridgeForRoot(sessionDir);
|
|
38067
|
-
const suffix = statusBarSuffixForSession(toolInput.sessionID, activeBridge?.getStatusBar());
|
|
38502
|
+
const suffix = statusBarSuffixForSession(toolInput.sessionID, activeBridge?.getStatusBar(), toolInput.tool === "aft_inspect");
|
|
38068
38503
|
if (suffix)
|
|
38069
38504
|
output.output += suffix;
|
|
38070
38505
|
}
|