@cortexkit/aft-opencode 0.36.1 → 0.37.1
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 +1066 -765
- package/dist/shared/rpc-client.d.ts +11 -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/session-directory.d.ts +1 -0
- package/dist/shared/session-directory.d.ts.map +1 -1
- package/dist/sync-watch-abort.d.ts +26 -0
- package/dist/sync-watch-abort.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/bash_watch.d.ts +1 -1
- package/dist/tools/bash_watch.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/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 +41 -4
- package/dist/workflow-hints.d.ts.map +1 -1
- package/package.json +8 -8
- package/src/shared/rpc-client.ts +16 -0
- package/src/shared/rpc-server.ts +51 -2
- package/src/shared/session-directory.ts +56 -0
- package/src/tui/index.tsx +26 -2
- package/src/tui/sidebar.tsx +64 -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}`);
|
|
@@ -14143,14 +14364,27 @@ function isCompressorHandledRunner(stage) {
|
|
|
14143
14364
|
if (tokens.some((token) => token === "&&" || token === "||" || token.includes(";"))) {
|
|
14144
14365
|
return false;
|
|
14145
14366
|
}
|
|
14146
|
-
|
|
14147
|
-
|
|
14148
|
-
|
|
14149
|
-
|
|
14367
|
+
let tokenOffset = 0;
|
|
14368
|
+
while (tokenOffset < tokens.length && isEnvAssignment(tokens[tokenOffset])) {
|
|
14369
|
+
tokenOffset++;
|
|
14370
|
+
}
|
|
14371
|
+
const first = runnerName(tokens[tokenOffset]);
|
|
14372
|
+
const runnerArgs = tokens.slice(tokenOffset + 1);
|
|
14373
|
+
const second = runnerArgs[0];
|
|
14374
|
+
const third = runnerArgs[1];
|
|
14375
|
+
const rest = runnerArgs;
|
|
14150
14376
|
if (!first)
|
|
14151
14377
|
return false;
|
|
14152
|
-
if (first === "bun")
|
|
14153
|
-
|
|
14378
|
+
if (first === "bun") {
|
|
14379
|
+
let args = rest;
|
|
14380
|
+
if (args[0] === "--cwd")
|
|
14381
|
+
args = args.slice(2);
|
|
14382
|
+
else if (args[0]?.startsWith("--cwd="))
|
|
14383
|
+
args = args.slice(1);
|
|
14384
|
+
const sub = args[0];
|
|
14385
|
+
const subNext = args[1];
|
|
14386
|
+
return sub === "test" || sub === "run" && startsWithTest(subNext);
|
|
14387
|
+
}
|
|
14154
14388
|
if (first === "npm" || first === "pnpm") {
|
|
14155
14389
|
return second === "test" || second === "run" && startsWithTest(third);
|
|
14156
14390
|
}
|
|
@@ -14173,14 +14407,20 @@ function isCompressorHandledRunner(stage) {
|
|
|
14173
14407
|
return hasBuildTask(rest, ["test", "check", "build", "assemble", "clean"]);
|
|
14174
14408
|
}
|
|
14175
14409
|
if (first === "mvn" || first === "mvnw") {
|
|
14176
|
-
return hasBuildTask(rest, ["test", "verify", "package", "install"]);
|
|
14410
|
+
return hasBuildTask(rest, ["test", "verify", "package", "install", "clean"]);
|
|
14177
14411
|
}
|
|
14178
14412
|
if (first === "dotnet")
|
|
14179
14413
|
return ["test", "build"].includes(second ?? "");
|
|
14180
14414
|
if (first === "rspec")
|
|
14181
14415
|
return true;
|
|
14182
|
-
if (first === "rake")
|
|
14183
|
-
|
|
14416
|
+
if (first === "rake") {
|
|
14417
|
+
const positionals = rest.filter((a) => !a.startsWith("-"));
|
|
14418
|
+
if (positionals.length === 0)
|
|
14419
|
+
return false;
|
|
14420
|
+
if (positionals.some((a) => a.includes("/") || a.includes(".") || a.includes("=")))
|
|
14421
|
+
return false;
|
|
14422
|
+
return positionals.some((a) => a === "test" || a === "spec");
|
|
14423
|
+
}
|
|
14184
14424
|
if (first === "phpunit" || first === "pest")
|
|
14185
14425
|
return true;
|
|
14186
14426
|
if (first === "xcodebuild")
|
|
@@ -14203,6 +14443,11 @@ function isCompressorHandledRunner(stage) {
|
|
|
14203
14443
|
"nox"
|
|
14204
14444
|
].includes(first);
|
|
14205
14445
|
}
|
|
14446
|
+
function isEnvAssignment(token) {
|
|
14447
|
+
if (!token)
|
|
14448
|
+
return false;
|
|
14449
|
+
return /^[a-zA-Z_][a-zA-Z0-9_]*=/.test(token);
|
|
14450
|
+
}
|
|
14206
14451
|
function runnerName(token) {
|
|
14207
14452
|
if (!token)
|
|
14208
14453
|
return "";
|
|
@@ -14458,7 +14703,7 @@ function tokenizeStage(stage) {
|
|
|
14458
14703
|
}
|
|
14459
14704
|
// ../aft-bridge/dist/pool.js
|
|
14460
14705
|
import { realpathSync as realpathSync2 } from "node:fs";
|
|
14461
|
-
var DEFAULT_IDLE_TIMEOUT_MS =
|
|
14706
|
+
var DEFAULT_IDLE_TIMEOUT_MS = 30 * 60 * 1000;
|
|
14462
14707
|
var DEFAULT_MAX_POOL_SIZE = 8;
|
|
14463
14708
|
var CLEANUP_INTERVAL_MS = 60 * 1000;
|
|
14464
14709
|
class BridgePool {
|
|
@@ -14532,7 +14777,7 @@ class BridgePool {
|
|
|
14532
14777
|
cleanup() {
|
|
14533
14778
|
const now = Date.now();
|
|
14534
14779
|
for (const [dir, entry] of this.bridges) {
|
|
14535
|
-
if (entry.bridge.hasPendingRequests())
|
|
14780
|
+
if (entry.bridge.hasPendingRequests() || entry.bridge.hasOutstandingBackgroundTasks())
|
|
14536
14781
|
continue;
|
|
14537
14782
|
if (now - entry.lastUsed > this.idleTimeoutMs) {
|
|
14538
14783
|
entry.bridge.shutdown().catch((err) => this.error("cleanup shutdown failed:", err));
|
|
@@ -14540,7 +14785,7 @@ class BridgePool {
|
|
|
14540
14785
|
}
|
|
14541
14786
|
}
|
|
14542
14787
|
for (const bridge of this.staleBridges) {
|
|
14543
|
-
if (bridge.hasPendingRequests())
|
|
14788
|
+
if (bridge.hasPendingRequests() || bridge.hasOutstandingBackgroundTasks())
|
|
14544
14789
|
continue;
|
|
14545
14790
|
bridge.shutdown().catch((err) => this.error("stale cleanup shutdown failed:", err));
|
|
14546
14791
|
this.staleBridges.delete(bridge);
|
|
@@ -14550,7 +14795,7 @@ class BridgePool {
|
|
|
14550
14795
|
let oldestDir = null;
|
|
14551
14796
|
let oldestTime = Infinity;
|
|
14552
14797
|
for (const [dir, entry] of this.bridges) {
|
|
14553
|
-
if (entry.bridge.hasPendingRequests())
|
|
14798
|
+
if (entry.bridge.hasPendingRequests() || entry.bridge.hasOutstandingBackgroundTasks())
|
|
14554
14799
|
continue;
|
|
14555
14800
|
if (entry.lastUsed < oldestTime) {
|
|
14556
14801
|
oldestTime = entry.lastUsed;
|
|
@@ -32881,7 +33126,14 @@ function writeTerminal(terminal, data) {
|
|
|
32881
33126
|
|
|
32882
33127
|
// src/shared/rpc-server.ts
|
|
32883
33128
|
import { randomBytes as randomBytes2 } from "node:crypto";
|
|
32884
|
-
import {
|
|
33129
|
+
import {
|
|
33130
|
+
mkdirSync as mkdirSync10,
|
|
33131
|
+
readdirSync as readdirSync5,
|
|
33132
|
+
readFileSync as readFileSync13,
|
|
33133
|
+
renameSync as renameSync8,
|
|
33134
|
+
unlinkSync as unlinkSync7,
|
|
33135
|
+
writeFileSync as writeFileSync10
|
|
33136
|
+
} from "node:fs";
|
|
32885
33137
|
import { createServer } from "node:http";
|
|
32886
33138
|
import { dirname as dirname9, join as join19 } from "node:path";
|
|
32887
33139
|
|
|
@@ -32896,6 +33148,41 @@ function rpcPortFileDir(storageDir, directory) {
|
|
|
32896
33148
|
const hash2 = projectHash(directory);
|
|
32897
33149
|
return join18(storageDir, "rpc", hash2, "ports");
|
|
32898
33150
|
}
|
|
33151
|
+
function isPidAlive(pid) {
|
|
33152
|
+
if (typeof pid !== "number" || !Number.isInteger(pid) || pid <= 0)
|
|
33153
|
+
return false;
|
|
33154
|
+
try {
|
|
33155
|
+
process.kill(pid, 0);
|
|
33156
|
+
return true;
|
|
33157
|
+
} catch (err) {
|
|
33158
|
+
return err.code === "EPERM";
|
|
33159
|
+
}
|
|
33160
|
+
}
|
|
33161
|
+
function parseRpcPortRecord(content) {
|
|
33162
|
+
const trimmed = content.trim();
|
|
33163
|
+
if (trimmed.length === 0)
|
|
33164
|
+
return null;
|
|
33165
|
+
if (trimmed.startsWith("{")) {
|
|
33166
|
+
try {
|
|
33167
|
+
const parsed = JSON.parse(trimmed);
|
|
33168
|
+
const port2 = typeof parsed.port === "number" ? parsed.port : Number.NaN;
|
|
33169
|
+
if (!Number.isInteger(port2) || port2 <= 0 || port2 > 65535)
|
|
33170
|
+
return null;
|
|
33171
|
+
return {
|
|
33172
|
+
port: port2,
|
|
33173
|
+
token: typeof parsed.token === "string" ? parsed.token : null,
|
|
33174
|
+
pid: typeof parsed.pid === "number" && Number.isInteger(parsed.pid) ? parsed.pid : undefined,
|
|
33175
|
+
started_at: typeof parsed.started_at === "number" ? parsed.started_at : undefined
|
|
33176
|
+
};
|
|
33177
|
+
} catch {
|
|
33178
|
+
return null;
|
|
33179
|
+
}
|
|
33180
|
+
}
|
|
33181
|
+
const port = Number.parseInt(trimmed, 10);
|
|
33182
|
+
if (!Number.isInteger(port) || port <= 0 || port > 65535)
|
|
33183
|
+
return null;
|
|
33184
|
+
return { port, token: null };
|
|
33185
|
+
}
|
|
32899
33186
|
|
|
32900
33187
|
// src/shared/rpc-server.ts
|
|
32901
33188
|
class AftRpcServer {
|
|
@@ -32942,6 +33229,7 @@ class AftRpcServer {
|
|
|
32942
33229
|
}), { encoding: "utf-8", mode: 384 });
|
|
32943
33230
|
renameSync8(tmpPath, this.portFilePath);
|
|
32944
33231
|
log2(`RPC server listening on 127.0.0.1:${this.port}`);
|
|
33232
|
+
this.sweepDeadPortFiles();
|
|
32945
33233
|
} catch (err) {
|
|
32946
33234
|
warn2(`Failed to write RPC port file: ${err}`);
|
|
32947
33235
|
}
|
|
@@ -32950,6 +33238,31 @@ class AftRpcServer {
|
|
|
32950
33238
|
server.unref();
|
|
32951
33239
|
});
|
|
32952
33240
|
}
|
|
33241
|
+
sweepDeadPortFiles() {
|
|
33242
|
+
let entries;
|
|
33243
|
+
try {
|
|
33244
|
+
entries = readdirSync5(this.portsDir);
|
|
33245
|
+
} catch {
|
|
33246
|
+
return;
|
|
33247
|
+
}
|
|
33248
|
+
for (const entry of entries) {
|
|
33249
|
+
if (!entry.endsWith(".json"))
|
|
33250
|
+
continue;
|
|
33251
|
+
const filePath = join19(this.portsDir, entry);
|
|
33252
|
+
if (filePath === this.portFilePath)
|
|
33253
|
+
continue;
|
|
33254
|
+
try {
|
|
33255
|
+
const record2 = parseRpcPortRecord(readFileSync13(filePath, "utf-8"));
|
|
33256
|
+
if (record2 === null) {
|
|
33257
|
+
unlinkSync7(filePath);
|
|
33258
|
+
continue;
|
|
33259
|
+
}
|
|
33260
|
+
if (record2.pid !== undefined && !isPidAlive(record2.pid)) {
|
|
33261
|
+
unlinkSync7(filePath);
|
|
33262
|
+
}
|
|
33263
|
+
} catch {}
|
|
33264
|
+
}
|
|
33265
|
+
}
|
|
32953
33266
|
stop() {
|
|
32954
33267
|
if (this.server) {
|
|
32955
33268
|
this.server.close();
|
|
@@ -33074,6 +33387,38 @@ function warmSessionDirectory(client, sessionId, fallbackDirectory) {
|
|
|
33074
33387
|
return;
|
|
33075
33388
|
getSessionDirectory(client, sessionId, fallbackDirectory);
|
|
33076
33389
|
}
|
|
33390
|
+
var VERIFY_TTL_MS = 15000;
|
|
33391
|
+
var verifyCache = new Map;
|
|
33392
|
+
async function verifySessionDirectory(client, sessionId) {
|
|
33393
|
+
if (!sessionId)
|
|
33394
|
+
return null;
|
|
33395
|
+
const hit = verifyCache.get(sessionId);
|
|
33396
|
+
if (hit && Date.now() - hit.verifiedAt < VERIFY_TTL_MS)
|
|
33397
|
+
return hit.directory;
|
|
33398
|
+
const c = client;
|
|
33399
|
+
const sessionApi = c?.session;
|
|
33400
|
+
if (!sessionApi || typeof sessionApi.get !== "function")
|
|
33401
|
+
return null;
|
|
33402
|
+
let dir = null;
|
|
33403
|
+
try {
|
|
33404
|
+
const result = await sessionApi.get({ path: { id: sessionId } });
|
|
33405
|
+
const session = result?.data ?? result;
|
|
33406
|
+
if (session && typeof session.directory === "string" && session.directory.length > 0) {
|
|
33407
|
+
dir = session.directory;
|
|
33408
|
+
}
|
|
33409
|
+
} catch {
|
|
33410
|
+
return null;
|
|
33411
|
+
}
|
|
33412
|
+
verifyCache.set(sessionId, { directory: dir, verifiedAt: Date.now() });
|
|
33413
|
+
if (verifyCache.size > CACHE_MAX_ENTRIES) {
|
|
33414
|
+
const oldest = verifyCache.keys().next().value;
|
|
33415
|
+
if (oldest !== undefined)
|
|
33416
|
+
verifyCache.delete(oldest);
|
|
33417
|
+
}
|
|
33418
|
+
if (dir !== null)
|
|
33419
|
+
setCache(sessionId, dir);
|
|
33420
|
+
return dir;
|
|
33421
|
+
}
|
|
33077
33422
|
|
|
33078
33423
|
// src/shared/status.ts
|
|
33079
33424
|
function asRecord(value) {
|
|
@@ -33286,7 +33631,7 @@ function formatStatusMarkdown(status) {
|
|
|
33286
33631
|
|
|
33287
33632
|
// src/shared/tui-config.ts
|
|
33288
33633
|
var import_comment_json4 = __toESM(require_src2(), 1);
|
|
33289
|
-
import { existsSync as existsSync14, mkdirSync as mkdirSync11, readFileSync as
|
|
33634
|
+
import { existsSync as existsSync14, mkdirSync as mkdirSync11, readFileSync as readFileSync14, writeFileSync as writeFileSync11 } from "node:fs";
|
|
33290
33635
|
import { dirname as dirname10, join as join21 } from "node:path";
|
|
33291
33636
|
|
|
33292
33637
|
// src/shared/opencode-config-dir.ts
|
|
@@ -33334,7 +33679,7 @@ function ensureTuiPluginEntry() {
|
|
|
33334
33679
|
const configPath = resolveTuiConfigPath();
|
|
33335
33680
|
let config2 = {};
|
|
33336
33681
|
if (existsSync14(configPath)) {
|
|
33337
|
-
config2 = import_comment_json4.parse(
|
|
33682
|
+
config2 = import_comment_json4.parse(readFileSync14(configPath, "utf-8")) ?? {};
|
|
33338
33683
|
}
|
|
33339
33684
|
const plugins = Array.isArray(config2.plugin) ? config2.plugin.filter((value) => typeof value === "string") : [];
|
|
33340
33685
|
if (plugins.some((plugin) => plugin === PLUGIN_NAME || plugin.startsWith(`${PLUGIN_NAME}@`) || plugin.includes("opencode-plugin") || plugin.includes("aft-opencode"))) {
|
|
@@ -33430,6 +33775,21 @@ function clearStatusBarSession(sessionID) {
|
|
|
33430
33775
|
emitStateBySession.delete(sessionID);
|
|
33431
33776
|
}
|
|
33432
33777
|
|
|
33778
|
+
// src/sync-watch-abort.ts
|
|
33779
|
+
var abortFlags = new Map;
|
|
33780
|
+
function signalSyncWatchAbort(sessionID) {
|
|
33781
|
+
const key = sessionID || "__default__";
|
|
33782
|
+
abortFlags.set(key, true);
|
|
33783
|
+
}
|
|
33784
|
+
function clearSyncWatchAbort(sessionID) {
|
|
33785
|
+
const key = sessionID || "__default__";
|
|
33786
|
+
abortFlags.delete(key);
|
|
33787
|
+
}
|
|
33788
|
+
function isSyncWatchAborted(sessionID) {
|
|
33789
|
+
const key = sessionID || "__default__";
|
|
33790
|
+
return abortFlags.get(key) === true;
|
|
33791
|
+
}
|
|
33792
|
+
|
|
33433
33793
|
// src/tool-perf.ts
|
|
33434
33794
|
import { AsyncLocalStorage } from "node:async_hooks";
|
|
33435
33795
|
var perfStore = new AsyncLocalStorage;
|
|
@@ -33502,19 +33862,6 @@ function isEmptyParam(value) {
|
|
|
33502
33862
|
return false;
|
|
33503
33863
|
}
|
|
33504
33864
|
var BASH_TRANSPORT_TIMEOUT_MS = 30000;
|
|
33505
|
-
var LONG_RUNNING_COMMAND_TIMEOUT_MS = {
|
|
33506
|
-
callers: 60000,
|
|
33507
|
-
trace_to: 60000,
|
|
33508
|
-
trace_to_symbol: 60000,
|
|
33509
|
-
trace_data: 60000,
|
|
33510
|
-
impact: 60000,
|
|
33511
|
-
grep: 60000,
|
|
33512
|
-
glob: 60000,
|
|
33513
|
-
semantic_search: 60000
|
|
33514
|
-
};
|
|
33515
|
-
function timeoutForCommand(command) {
|
|
33516
|
-
return LONG_RUNNING_COMMAND_TIMEOUT_MS[command];
|
|
33517
|
-
}
|
|
33518
33865
|
function asPlainObject(value) {
|
|
33519
33866
|
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
33520
33867
|
return;
|
|
@@ -34091,374 +34438,47 @@ ${hint}`;
|
|
|
34091
34438
|
};
|
|
34092
34439
|
}
|
|
34093
34440
|
|
|
34094
|
-
// src/tools/
|
|
34441
|
+
// src/tools/bash.ts
|
|
34095
34442
|
import { tool as tool4 } from "@opencode-ai/plugin";
|
|
34096
|
-
var z4 = tool4.schema;
|
|
34097
|
-
function conflictTools(ctx) {
|
|
34098
|
-
return {
|
|
34099
|
-
aft_conflicts: {
|
|
34100
|
-
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).",
|
|
34101
|
-
args: {
|
|
34102
|
-
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()
|
|
34103
|
-
},
|
|
34104
|
-
execute: async (args, context) => {
|
|
34105
|
-
const params = {};
|
|
34106
|
-
if (!isEmptyParam(args?.path))
|
|
34107
|
-
params.path = args.path;
|
|
34108
|
-
const response = await callBridge(ctx, context, "git_conflicts", params);
|
|
34109
|
-
if (response.success === false) {
|
|
34110
|
-
throw new Error(response.message || "git_conflicts failed");
|
|
34111
|
-
}
|
|
34112
|
-
return response.text;
|
|
34113
|
-
}
|
|
34114
|
-
}
|
|
34115
|
-
};
|
|
34116
|
-
}
|
|
34117
|
-
|
|
34118
|
-
// src/tools/hoisted.ts
|
|
34119
|
-
import * as fs6 from "node:fs";
|
|
34120
|
-
import * as path4 from "node:path";
|
|
34121
|
-
import { tool as tool8 } from "@opencode-ai/plugin";
|
|
34122
34443
|
|
|
34123
|
-
// src/
|
|
34124
|
-
|
|
34125
|
-
|
|
34126
|
-
|
|
34127
|
-
|
|
34128
|
-
|
|
34129
|
-
|
|
34130
|
-
if (line.startsWith("*** Add File:")) {
|
|
34131
|
-
const filePath = line.slice("*** Add File:".length).trim();
|
|
34132
|
-
return filePath ? { filePath, nextIdx: startIdx + 1 } : null;
|
|
34133
|
-
}
|
|
34134
|
-
if (line.startsWith("*** Delete File:")) {
|
|
34135
|
-
const filePath = line.slice("*** Delete File:".length).trim();
|
|
34136
|
-
return filePath ? { filePath, nextIdx: startIdx + 1 } : null;
|
|
34444
|
+
// src/shared/subagent-detect.ts
|
|
34445
|
+
var CACHE_MAX_ENTRIES2 = 200;
|
|
34446
|
+
var cache2 = new Map;
|
|
34447
|
+
async function resolveIsSubagent(client, sessionId, _fallbackDirectory) {
|
|
34448
|
+
if (!sessionId) {
|
|
34449
|
+
sessionLog(undefined, "[subagent-detect] no sessionId provided → primary");
|
|
34450
|
+
return false;
|
|
34137
34451
|
}
|
|
34138
|
-
|
|
34139
|
-
|
|
34140
|
-
|
|
34141
|
-
|
|
34142
|
-
|
|
34143
|
-
movePath = lines[nextIdx].slice("*** Move to:".length).trim();
|
|
34144
|
-
nextIdx++;
|
|
34145
|
-
}
|
|
34146
|
-
return filePath ? { filePath, movePath, nextIdx } : null;
|
|
34452
|
+
const cached2 = cache2.get(sessionId);
|
|
34453
|
+
if (cached2) {
|
|
34454
|
+
cache2.delete(sessionId);
|
|
34455
|
+
cache2.set(sessionId, cached2);
|
|
34456
|
+
return cached2.isSubagent;
|
|
34147
34457
|
}
|
|
34148
|
-
|
|
34149
|
-
|
|
34150
|
-
|
|
34151
|
-
|
|
34152
|
-
|
|
34153
|
-
|
|
34154
|
-
if (lines[i].startsWith("+")) {
|
|
34155
|
-
content += `${lines[i].substring(1)}
|
|
34156
|
-
`;
|
|
34157
|
-
}
|
|
34158
|
-
i++;
|
|
34458
|
+
const c = client;
|
|
34459
|
+
const sessionApi = c?.session;
|
|
34460
|
+
if (!sessionApi || typeof sessionApi.get !== "function") {
|
|
34461
|
+
sessionLog(sessionId, `[subagent-detect] client.session.get unavailable (client=${typeof client}, session=${typeof sessionApi}, get=${typeof sessionApi?.get}) → caching as primary`);
|
|
34462
|
+
setCache2(sessionId, false);
|
|
34463
|
+
return false;
|
|
34159
34464
|
}
|
|
34160
|
-
|
|
34161
|
-
|
|
34162
|
-
|
|
34465
|
+
sessionLog(sessionId, `[subagent-detect] cache miss, calling client.session.get(id=${sessionId})`);
|
|
34466
|
+
let isSubagent = false;
|
|
34467
|
+
let parentIdRaw;
|
|
34468
|
+
try {
|
|
34469
|
+
const result = await sessionApi.get({
|
|
34470
|
+
path: { id: sessionId }
|
|
34471
|
+
});
|
|
34472
|
+
const session = result?.data ?? result;
|
|
34473
|
+
parentIdRaw = session?.parentID;
|
|
34474
|
+
isSubagent = session !== undefined && typeof session.parentID === "string" && session.parentID.length > 0;
|
|
34475
|
+
sessionLog(sessionId, `[subagent-detect] SDK returned session=${session !== undefined ? "present" : "undefined"}, parentID=${JSON.stringify(parentIdRaw)} → isSubagent=${isSubagent}`);
|
|
34476
|
+
} catch (err) {
|
|
34477
|
+
sessionWarn(sessionId, `[subagent-detect] SDK lookup failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
34478
|
+
return false;
|
|
34163
34479
|
}
|
|
34164
|
-
|
|
34165
|
-
|
|
34166
|
-
function parseUpdateFileChunks(lines, startIdx) {
|
|
34167
|
-
const chunks = [];
|
|
34168
|
-
let i = startIdx;
|
|
34169
|
-
while (i < lines.length && !lines[i].startsWith("***")) {
|
|
34170
|
-
if (lines[i].startsWith("@@")) {
|
|
34171
|
-
const contextLine = lines[i].substring(2).trim();
|
|
34172
|
-
i++;
|
|
34173
|
-
const oldLines = [];
|
|
34174
|
-
const newLines = [];
|
|
34175
|
-
let isEndOfFile = false;
|
|
34176
|
-
while (i < lines.length && !lines[i].startsWith("@@") && !lines[i].startsWith("***")) {
|
|
34177
|
-
const changeLine = lines[i];
|
|
34178
|
-
if (changeLine === "*** End of File") {
|
|
34179
|
-
isEndOfFile = true;
|
|
34180
|
-
i++;
|
|
34181
|
-
break;
|
|
34182
|
-
}
|
|
34183
|
-
if (changeLine.startsWith(" ")) {
|
|
34184
|
-
const content = changeLine.substring(1);
|
|
34185
|
-
oldLines.push(content);
|
|
34186
|
-
newLines.push(content);
|
|
34187
|
-
} else if (changeLine.startsWith("-")) {
|
|
34188
|
-
oldLines.push(changeLine.substring(1));
|
|
34189
|
-
} else if (changeLine.startsWith("+")) {
|
|
34190
|
-
newLines.push(changeLine.substring(1));
|
|
34191
|
-
}
|
|
34192
|
-
i++;
|
|
34193
|
-
}
|
|
34194
|
-
chunks.push({
|
|
34195
|
-
old_lines: oldLines,
|
|
34196
|
-
new_lines: newLines,
|
|
34197
|
-
change_context: contextLine || undefined,
|
|
34198
|
-
is_end_of_file: isEndOfFile || undefined
|
|
34199
|
-
});
|
|
34200
|
-
} else {
|
|
34201
|
-
i++;
|
|
34202
|
-
}
|
|
34203
|
-
}
|
|
34204
|
-
return { chunks, nextIdx: i };
|
|
34205
|
-
}
|
|
34206
|
-
var MAX_PATCH_SIZE = 1024 * 1024;
|
|
34207
|
-
var MAX_HUNKS = 500;
|
|
34208
|
-
function parsePatch(patchText) {
|
|
34209
|
-
if (patchText.length > MAX_PATCH_SIZE) {
|
|
34210
|
-
throw new Error(`Patch too large: ${patchText.length} bytes exceeds limit of ${MAX_PATCH_SIZE} bytes`);
|
|
34211
|
-
}
|
|
34212
|
-
const cleaned = stripHeredoc(patchText.trim());
|
|
34213
|
-
const lines = cleaned.split(`
|
|
34214
|
-
`);
|
|
34215
|
-
const hunks = [];
|
|
34216
|
-
const beginIdx = lines.findIndex((line) => line.trim() === "*** Begin Patch");
|
|
34217
|
-
const endIdx = lines.findIndex((line) => line.trim() === "*** End Patch");
|
|
34218
|
-
if (beginIdx === -1 || endIdx === -1 || beginIdx >= endIdx) {
|
|
34219
|
-
throw new Error("Invalid patch format: missing *** Begin Patch / *** End Patch markers");
|
|
34220
|
-
}
|
|
34221
|
-
let i = beginIdx + 1;
|
|
34222
|
-
while (i < endIdx) {
|
|
34223
|
-
const header = parsePatchHeader(lines, i);
|
|
34224
|
-
if (!header) {
|
|
34225
|
-
i++;
|
|
34226
|
-
continue;
|
|
34227
|
-
}
|
|
34228
|
-
if (hunks.length >= MAX_HUNKS) {
|
|
34229
|
-
throw new Error(`Patch exceeds maximum of ${MAX_HUNKS} file operations`);
|
|
34230
|
-
}
|
|
34231
|
-
if (lines[i].startsWith("*** Add File:")) {
|
|
34232
|
-
const { content, nextIdx } = parseAddFileContent(lines, header.nextIdx);
|
|
34233
|
-
hunks.push({ type: "add", path: header.filePath, contents: content });
|
|
34234
|
-
i = nextIdx;
|
|
34235
|
-
} else if (lines[i].startsWith("*** Delete File:")) {
|
|
34236
|
-
hunks.push({ type: "delete", path: header.filePath });
|
|
34237
|
-
i = header.nextIdx;
|
|
34238
|
-
} else if (lines[i].startsWith("*** Update File:")) {
|
|
34239
|
-
const { chunks, nextIdx } = parseUpdateFileChunks(lines, header.nextIdx);
|
|
34240
|
-
hunks.push({
|
|
34241
|
-
type: "update",
|
|
34242
|
-
path: header.filePath,
|
|
34243
|
-
move_path: header.movePath,
|
|
34244
|
-
chunks
|
|
34245
|
-
});
|
|
34246
|
-
i = nextIdx;
|
|
34247
|
-
} else {
|
|
34248
|
-
i++;
|
|
34249
|
-
}
|
|
34250
|
-
}
|
|
34251
|
-
return hunks;
|
|
34252
|
-
}
|
|
34253
|
-
function normalizeUnicode(str) {
|
|
34254
|
-
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, " ");
|
|
34255
|
-
}
|
|
34256
|
-
function normalizeIndent(str) {
|
|
34257
|
-
return str.replace(/^[\t ]+/, (m) => " ".repeat(m.length));
|
|
34258
|
-
}
|
|
34259
|
-
function tryMatch(lines, pattern, startIndex, compare, eof) {
|
|
34260
|
-
if (eof) {
|
|
34261
|
-
const fromEnd = lines.length - pattern.length;
|
|
34262
|
-
if (fromEnd >= startIndex) {
|
|
34263
|
-
let matches = true;
|
|
34264
|
-
for (let j = 0;j < pattern.length; j++) {
|
|
34265
|
-
if (!compare(lines[fromEnd + j], pattern[j])) {
|
|
34266
|
-
matches = false;
|
|
34267
|
-
break;
|
|
34268
|
-
}
|
|
34269
|
-
}
|
|
34270
|
-
if (matches)
|
|
34271
|
-
return fromEnd;
|
|
34272
|
-
}
|
|
34273
|
-
}
|
|
34274
|
-
for (let i = startIndex;i <= lines.length - pattern.length; i++) {
|
|
34275
|
-
let matches = true;
|
|
34276
|
-
for (let j = 0;j < pattern.length; j++) {
|
|
34277
|
-
if (!compare(lines[i + j], pattern[j])) {
|
|
34278
|
-
matches = false;
|
|
34279
|
-
break;
|
|
34280
|
-
}
|
|
34281
|
-
}
|
|
34282
|
-
if (matches)
|
|
34283
|
-
return i;
|
|
34284
|
-
}
|
|
34285
|
-
return -1;
|
|
34286
|
-
}
|
|
34287
|
-
function seekSequenceTiered(lines, pattern, startIndex, eof = false) {
|
|
34288
|
-
if (pattern.length === 0)
|
|
34289
|
-
return null;
|
|
34290
|
-
const exact = tryMatch(lines, pattern, startIndex, (a, b) => a === b, eof);
|
|
34291
|
-
if (exact !== -1)
|
|
34292
|
-
return { found: exact, tier: "exact" };
|
|
34293
|
-
const rstrip = tryMatch(lines, pattern, startIndex, (a, b) => a.trimEnd() === b.trimEnd(), eof);
|
|
34294
|
-
if (rstrip !== -1)
|
|
34295
|
-
return { found: rstrip, tier: "rstrip" };
|
|
34296
|
-
const trim = tryMatch(lines, pattern, startIndex, (a, b) => a.trim() === b.trim(), eof);
|
|
34297
|
-
if (trim !== -1)
|
|
34298
|
-
return { found: trim, tier: "trim" };
|
|
34299
|
-
const indent = tryMatch(lines, pattern, startIndex, (a, b) => normalizeIndent(a).trimEnd() === normalizeIndent(b).trimEnd(), eof);
|
|
34300
|
-
if (indent !== -1)
|
|
34301
|
-
return { found: indent, tier: "indent" };
|
|
34302
|
-
const unicode = tryMatch(lines, pattern, startIndex, (a, b) => normalizeUnicode(a.trim()) === normalizeUnicode(b.trim()), eof);
|
|
34303
|
-
if (unicode !== -1)
|
|
34304
|
-
return { found: unicode, tier: "unicode" };
|
|
34305
|
-
return null;
|
|
34306
|
-
}
|
|
34307
|
-
function seekSequence(lines, pattern, startIndex, eof = false) {
|
|
34308
|
-
const r = seekSequenceTiered(lines, pattern, startIndex, eof);
|
|
34309
|
-
return r ? r.found : -1;
|
|
34310
|
-
}
|
|
34311
|
-
function findClosestPartialMatch(lines, pattern) {
|
|
34312
|
-
if (pattern.length === 0 || lines.length === 0)
|
|
34313
|
-
return null;
|
|
34314
|
-
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());
|
|
34315
|
-
const candidates = [];
|
|
34316
|
-
for (let i = 0;i < lines.length && candidates.length < 16; i++) {
|
|
34317
|
-
if (compareAny(lines[i], pattern[0]))
|
|
34318
|
-
candidates.push(i);
|
|
34319
|
-
}
|
|
34320
|
-
if (candidates.length === 0)
|
|
34321
|
-
return null;
|
|
34322
|
-
let best = { lineNumber: -1, matchedLines: 0, firstDivergence: -1 };
|
|
34323
|
-
for (const start of candidates) {
|
|
34324
|
-
let matched = 0;
|
|
34325
|
-
for (let j = 0;j < pattern.length && start + j < lines.length; j++) {
|
|
34326
|
-
if (!compareAny(lines[start + j], pattern[j]))
|
|
34327
|
-
break;
|
|
34328
|
-
matched++;
|
|
34329
|
-
}
|
|
34330
|
-
if (matched > best.matchedLines) {
|
|
34331
|
-
best = {
|
|
34332
|
-
lineNumber: start + 1,
|
|
34333
|
-
matchedLines: matched,
|
|
34334
|
-
firstDivergence: matched
|
|
34335
|
-
};
|
|
34336
|
-
}
|
|
34337
|
-
}
|
|
34338
|
-
return best.lineNumber === -1 ? null : best;
|
|
34339
|
-
}
|
|
34340
|
-
function applyUpdateChunks(originalContent, filePath, chunks) {
|
|
34341
|
-
const originalLines = originalContent.split(`
|
|
34342
|
-
`);
|
|
34343
|
-
if (originalLines.length > 0 && originalLines[originalLines.length - 1] === "") {
|
|
34344
|
-
originalLines.pop();
|
|
34345
|
-
}
|
|
34346
|
-
const replacements = [];
|
|
34347
|
-
let lineIndex = 0;
|
|
34348
|
-
for (const chunk of chunks) {
|
|
34349
|
-
if (chunk.change_context) {
|
|
34350
|
-
const contextIdx = seekSequence(originalLines, [chunk.change_context], lineIndex);
|
|
34351
|
-
if (contextIdx === -1) {
|
|
34352
|
-
throw new Error(`Failed to find context '${chunk.change_context}' in ${filePath}`);
|
|
34353
|
-
}
|
|
34354
|
-
lineIndex = contextIdx + 1;
|
|
34355
|
-
}
|
|
34356
|
-
if (chunk.old_lines.length === 0) {
|
|
34357
|
-
let insertionIdx;
|
|
34358
|
-
if (chunk.change_context) {
|
|
34359
|
-
insertionIdx = lineIndex;
|
|
34360
|
-
} else if (originalLines.length > 0 && originalLines[originalLines.length - 1] === "") {
|
|
34361
|
-
insertionIdx = originalLines.length - 1;
|
|
34362
|
-
} else {
|
|
34363
|
-
insertionIdx = originalLines.length;
|
|
34364
|
-
}
|
|
34365
|
-
replacements.push([insertionIdx, 0, chunk.new_lines]);
|
|
34366
|
-
continue;
|
|
34367
|
-
}
|
|
34368
|
-
let pattern = chunk.old_lines;
|
|
34369
|
-
let newSlice = chunk.new_lines;
|
|
34370
|
-
let found = seekSequence(originalLines, pattern, lineIndex, chunk.is_end_of_file);
|
|
34371
|
-
if (found === -1 && pattern.length > 0 && pattern[pattern.length - 1] === "") {
|
|
34372
|
-
pattern = pattern.slice(0, -1);
|
|
34373
|
-
if (newSlice.length > 0 && newSlice[newSlice.length - 1] === "") {
|
|
34374
|
-
newSlice = newSlice.slice(0, -1);
|
|
34375
|
-
}
|
|
34376
|
-
found = seekSequence(originalLines, pattern, lineIndex, chunk.is_end_of_file);
|
|
34377
|
-
}
|
|
34378
|
-
if (found !== -1) {
|
|
34379
|
-
replacements.push([found, pattern.length, newSlice]);
|
|
34380
|
-
lineIndex = found + pattern.length;
|
|
34381
|
-
} else {
|
|
34382
|
-
const newSliceTrimmed = newSlice.filter((line) => line.trim().length > 0);
|
|
34383
|
-
const alreadyApplied = newSliceTrimmed.length > 0 && seekSequence(originalLines, newSliceTrimmed, 0, chunk.is_end_of_file) !== -1;
|
|
34384
|
-
const closest = findClosestPartialMatch(originalLines, pattern);
|
|
34385
|
-
let closestHint = "";
|
|
34386
|
-
if (closest && closest.matchedLines > 0) {
|
|
34387
|
-
const fileLineNo = closest.lineNumber + closest.firstDivergence;
|
|
34388
|
-
const expectedLine = pattern[closest.firstDivergence];
|
|
34389
|
-
const actualLine = fileLineNo - 1 < originalLines.length ? originalLines[fileLineNo - 1] : "<EOF>";
|
|
34390
|
-
closestHint = `
|
|
34391
|
-
|
|
34392
|
-
Closest match starts at line ${closest.lineNumber} ` + `(${closest.matchedLines} of ${pattern.length} lines matched).
|
|
34393
|
-
` + `First divergence at line ${fileLineNo}:
|
|
34394
|
-
` + ` expected: ${JSON.stringify(expectedLine)}
|
|
34395
|
-
` + ` actual: ${JSON.stringify(actualLine)}`;
|
|
34396
|
-
}
|
|
34397
|
-
const triedTiers = "exact, trimEnd, trim, indent (tab/space), unicode";
|
|
34398
|
-
const alreadyAppliedHint = alreadyApplied ? `
|
|
34399
|
-
|
|
34400
|
-
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." : "";
|
|
34401
|
-
throw new Error(`Failed to find expected lines in ${filePath}:
|
|
34402
|
-
${chunk.old_lines.join(`
|
|
34403
|
-
`)}
|
|
34404
|
-
|
|
34405
|
-
` + `Tried match tiers: ${triedTiers}.${closestHint}${alreadyAppliedHint}`);
|
|
34406
|
-
}
|
|
34407
|
-
}
|
|
34408
|
-
replacements.sort((a, b) => a[0] - b[0]);
|
|
34409
|
-
const result = [...originalLines];
|
|
34410
|
-
for (let i = replacements.length - 1;i >= 0; i--) {
|
|
34411
|
-
const [startIdx, oldLen, newSegment] = replacements[i];
|
|
34412
|
-
result.splice(startIdx, oldLen, ...newSegment);
|
|
34413
|
-
}
|
|
34414
|
-
if (result.length === 0 || result[result.length - 1] !== "") {
|
|
34415
|
-
result.push("");
|
|
34416
|
-
}
|
|
34417
|
-
return result.join(`
|
|
34418
|
-
`);
|
|
34419
|
-
}
|
|
34420
|
-
|
|
34421
|
-
// src/tools/bash.ts
|
|
34422
|
-
import { tool as tool5 } from "@opencode-ai/plugin";
|
|
34423
|
-
|
|
34424
|
-
// src/shared/subagent-detect.ts
|
|
34425
|
-
var CACHE_MAX_ENTRIES2 = 200;
|
|
34426
|
-
var cache2 = new Map;
|
|
34427
|
-
async function resolveIsSubagent(client, sessionId, _fallbackDirectory) {
|
|
34428
|
-
if (!sessionId) {
|
|
34429
|
-
sessionLog(undefined, "[subagent-detect] no sessionId provided → primary");
|
|
34430
|
-
return false;
|
|
34431
|
-
}
|
|
34432
|
-
const cached2 = cache2.get(sessionId);
|
|
34433
|
-
if (cached2) {
|
|
34434
|
-
cache2.delete(sessionId);
|
|
34435
|
-
cache2.set(sessionId, cached2);
|
|
34436
|
-
return cached2.isSubagent;
|
|
34437
|
-
}
|
|
34438
|
-
const c = client;
|
|
34439
|
-
const sessionApi = c?.session;
|
|
34440
|
-
if (!sessionApi || typeof sessionApi.get !== "function") {
|
|
34441
|
-
sessionLog(sessionId, `[subagent-detect] client.session.get unavailable (client=${typeof client}, session=${typeof sessionApi}, get=${typeof sessionApi?.get}) → caching as primary`);
|
|
34442
|
-
setCache2(sessionId, false);
|
|
34443
|
-
return false;
|
|
34444
|
-
}
|
|
34445
|
-
sessionLog(sessionId, `[subagent-detect] cache miss, calling client.session.get(id=${sessionId})`);
|
|
34446
|
-
let isSubagent = false;
|
|
34447
|
-
let parentIdRaw;
|
|
34448
|
-
try {
|
|
34449
|
-
const result = await sessionApi.get({
|
|
34450
|
-
path: { id: sessionId }
|
|
34451
|
-
});
|
|
34452
|
-
const session = result?.data ?? result;
|
|
34453
|
-
parentIdRaw = session?.parentID;
|
|
34454
|
-
isSubagent = session !== undefined && typeof session.parentID === "string" && session.parentID.length > 0;
|
|
34455
|
-
sessionLog(sessionId, `[subagent-detect] SDK returned session=${session !== undefined ? "present" : "undefined"}, parentID=${JSON.stringify(parentIdRaw)} → isSubagent=${isSubagent}`);
|
|
34456
|
-
} catch (err) {
|
|
34457
|
-
sessionWarn(sessionId, `[subagent-detect] SDK lookup failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
34458
|
-
return false;
|
|
34459
|
-
}
|
|
34460
|
-
setCache2(sessionId, isSubagent);
|
|
34461
|
-
return isSubagent;
|
|
34480
|
+
setCache2(sessionId, isSubagent);
|
|
34481
|
+
return isSubagent;
|
|
34462
34482
|
}
|
|
34463
34483
|
function setCache2(sessionId, isSubagent) {
|
|
34464
34484
|
if (cache2.has(sessionId))
|
|
@@ -34472,7 +34492,7 @@ function setCache2(sessionId, isSubagent) {
|
|
|
34472
34492
|
}
|
|
34473
34493
|
|
|
34474
34494
|
// src/tools/bash.ts
|
|
34475
|
-
var
|
|
34495
|
+
var z4 = tool4.schema;
|
|
34476
34496
|
var METADATA_PREVIEW_LIMIT = 30 * 1024;
|
|
34477
34497
|
var FOREGROUND_POLL_INTERVAL_MS = 100;
|
|
34478
34498
|
var DEFAULT_HARD_TIMEOUT_MS = 30 * 60 * 1000;
|
|
@@ -34485,7 +34505,14 @@ function resolveForegroundWaitMs(configured) {
|
|
|
34485
34505
|
}
|
|
34486
34506
|
return configured;
|
|
34487
34507
|
}
|
|
34488
|
-
|
|
34508
|
+
function bashToolDescription(aftSearchRegistered, compressionOn, backgroundOn) {
|
|
34509
|
+
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";
|
|
34510
|
+
const compression = compressionOn ? " Output is compressed by default; pass compressed: false for raw output." : "";
|
|
34511
|
+
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.";
|
|
34512
|
+
return `Execute shell commands.${compression}${tasks} Use bash_watch to wait for output patterns or exit events.
|
|
34513
|
+
|
|
34514
|
+
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}.`;
|
|
34515
|
+
}
|
|
34489
34516
|
async function withPermissionLoop(ctx, runtime, params, bridgeCall, options) {
|
|
34490
34517
|
const first = await bridgeCall(ctx, runtime, "bash", params, options);
|
|
34491
34518
|
if (first.success !== false || first.code !== "permission_required")
|
|
@@ -34508,22 +34535,27 @@ async function withPermissionLoop(ctx, runtime, params, bridgeCall, options) {
|
|
|
34508
34535
|
}
|
|
34509
34536
|
return second;
|
|
34510
34537
|
}
|
|
34511
|
-
function createBashTool(ctx) {
|
|
34538
|
+
function createBashTool(ctx, aftSearchRegisteredOverride) {
|
|
34512
34539
|
return {
|
|
34513
|
-
description:
|
|
34540
|
+
description: (() => {
|
|
34541
|
+
const cfg = resolveBashConfig(ctx.config);
|
|
34542
|
+
return bashToolDescription(false, cfg.compress, cfg.background);
|
|
34543
|
+
})(),
|
|
34514
34544
|
args: {
|
|
34515
|
-
command:
|
|
34545
|
+
command: z4.string().describe("Shell command to execute. Supports pipes, redirection, and normal shell syntax."),
|
|
34516
34546
|
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."),
|
|
34517
|
-
workdir:
|
|
34518
|
-
description:
|
|
34519
|
-
background:
|
|
34520
|
-
compressed:
|
|
34521
|
-
pty:
|
|
34547
|
+
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."),
|
|
34548
|
+
description: z4.string().optional().describe("Short 5-10 word human-readable summary shown in OpenCode UI metadata instead of raw shell syntax."),
|
|
34549
|
+
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."),
|
|
34550
|
+
compressed: z4.boolean().optional().describe("When true or omitted, return compressed output with noisy terminal control sequences reduced. Set to false for raw output."),
|
|
34551
|
+
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.'),
|
|
34522
34552
|
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."),
|
|
34523
34553
|
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.")
|
|
34524
34554
|
},
|
|
34525
34555
|
execute: async (args, context) => {
|
|
34526
34556
|
const bashCfg = resolveBashConfig(ctx.config);
|
|
34557
|
+
const ctxAftSearchRegistered = ctx.aftSearchRegistered === true;
|
|
34558
|
+
const aftSearchRegistered = aftSearchRegisteredOverride ?? ctxAftSearchRegistered;
|
|
34527
34559
|
let accumulatedOutput = "";
|
|
34528
34560
|
const description = args.description;
|
|
34529
34561
|
const metadata = context.metadata;
|
|
@@ -34591,7 +34623,7 @@ function createBashTool(ctx) {
|
|
|
34591
34623
|
throw new Error(status.message ?? "bash_status failed");
|
|
34592
34624
|
}
|
|
34593
34625
|
if (isTerminalStatus(status.status)) {
|
|
34594
|
-
const rendered2 = appendPipeStripNote(formatForegroundResult(status), pipeStrip.note);
|
|
34626
|
+
const rendered2 = maybeAppendGrepSearchHint(appendPipeStripNote(formatForegroundResult(status), pipeStrip.note), command, aftSearchRegistered);
|
|
34595
34627
|
const metadataPayload2 = foregroundMetadata(description, status, rendered2);
|
|
34596
34628
|
metadata?.(metadataPayload2);
|
|
34597
34629
|
return { output: rendered2, title: uiTitle, metadata: metadataPayload2 };
|
|
@@ -34653,179 +34685,515 @@ function createBashTool(ctx) {
|
|
|
34653
34685
|
metadata: metadataPayload
|
|
34654
34686
|
};
|
|
34655
34687
|
}
|
|
34656
|
-
};
|
|
34688
|
+
};
|
|
34689
|
+
}
|
|
34690
|
+
function appendPipeStripNote(output, note) {
|
|
34691
|
+
return note ? `${output}
|
|
34692
|
+
|
|
34693
|
+
${note}` : output;
|
|
34694
|
+
}
|
|
34695
|
+
function createBashStatusTool(ctx) {
|
|
34696
|
+
return {
|
|
34697
|
+
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.",
|
|
34698
|
+
args: {
|
|
34699
|
+
taskId: z4.string().describe("Background task ID returned by bash({ background: true }), e.g. bash-6b454047."),
|
|
34700
|
+
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.")
|
|
34701
|
+
},
|
|
34702
|
+
execute: async (args, context) => {
|
|
34703
|
+
const taskId = args.taskId;
|
|
34704
|
+
const outputMode = args.outputMode;
|
|
34705
|
+
const data = await bashStatusSnapshot(ctx, context, taskId, outputMode);
|
|
34706
|
+
return await formatBashStatusText(context, taskId, data, outputMode);
|
|
34707
|
+
}
|
|
34708
|
+
};
|
|
34709
|
+
}
|
|
34710
|
+
function createBashKillTool(ctx) {
|
|
34711
|
+
return {
|
|
34712
|
+
description: "Terminate a running background bash task spawned with bash({ background: true }). Returns confirmation of kill or an error if the task already finished.",
|
|
34713
|
+
args: {
|
|
34714
|
+
taskId: z4.string().describe("Background task ID returned by bash({ background: true }), e.g. bash-6b454047.")
|
|
34715
|
+
},
|
|
34716
|
+
execute: async (args, context) => {
|
|
34717
|
+
const data = await callBashBridge(ctx, context, "bash_kill", {
|
|
34718
|
+
task_id: args.taskId
|
|
34719
|
+
});
|
|
34720
|
+
if (data.success === false) {
|
|
34721
|
+
throw new Error(data.message ?? "bash_kill failed");
|
|
34722
|
+
}
|
|
34723
|
+
await disposePtyTerminal(ptyCacheKey(context, args.taskId));
|
|
34724
|
+
if (data.kill_signaled === true) {
|
|
34725
|
+
return `Task ${args.taskId}: kill_signaled`;
|
|
34726
|
+
}
|
|
34727
|
+
return `Task ${args.taskId}: ${String(data.status ?? "killed")}`;
|
|
34728
|
+
}
|
|
34729
|
+
};
|
|
34730
|
+
}
|
|
34731
|
+
async function bashStatusSnapshot(ctx, runtime, taskId, outputMode, options) {
|
|
34732
|
+
const data = await callBashBridge(ctx, runtime, "bash_status", { task_id: taskId, output_mode: outputMode }, options);
|
|
34733
|
+
if (data.success === false) {
|
|
34734
|
+
throw new Error(data.message ?? "bash_status failed");
|
|
34735
|
+
}
|
|
34736
|
+
return data;
|
|
34737
|
+
}
|
|
34738
|
+
async function formatBashStatusText(runtime, taskId, data, requestedOutputMode) {
|
|
34739
|
+
const status = data.status;
|
|
34740
|
+
const exit = typeof data.exit_code === "number" ? ` (exit ${data.exit_code})` : "";
|
|
34741
|
+
const dur = typeof data.duration_ms === "number" ? ` ${Math.round(data.duration_ms / 1000)}s` : "";
|
|
34742
|
+
let text = `Task ${taskId}: ${status}${exit}${dur}`;
|
|
34743
|
+
if (data.mode === "pty") {
|
|
34744
|
+
text += await formatPtyStatus(runtime, taskId, data, requestedOutputMode);
|
|
34745
|
+
} else {
|
|
34746
|
+
const preview = data.output_preview;
|
|
34747
|
+
if (preview && status !== "running") {
|
|
34748
|
+
text += `
|
|
34749
|
+
${preview}`;
|
|
34750
|
+
}
|
|
34751
|
+
if (status === "running") {
|
|
34752
|
+
text += `
|
|
34753
|
+
A completion reminder will be delivered automatically; don't poll.`;
|
|
34754
|
+
}
|
|
34755
|
+
}
|
|
34756
|
+
return text;
|
|
34757
|
+
}
|
|
34758
|
+
async function formatPtyStatus(runtime, taskId, data, requestedOutputMode) {
|
|
34759
|
+
const outputPath = data.output_path;
|
|
34760
|
+
if (!outputPath)
|
|
34761
|
+
return `
|
|
34762
|
+
[PTY output path unavailable]`;
|
|
34763
|
+
const key = ptyCacheKey(runtime, taskId);
|
|
34764
|
+
const { rows, cols } = ptyDimensions(data);
|
|
34765
|
+
const state = await getOrCreatePtyTerminal(key, outputPath, rows, cols);
|
|
34766
|
+
const raw = await readPtyBytes(state);
|
|
34767
|
+
const outputMode = requestedOutputMode ?? "screen";
|
|
34768
|
+
let suffix = "";
|
|
34769
|
+
if (outputMode === "raw") {
|
|
34770
|
+
suffix = raw.length > 0 ? `
|
|
34771
|
+
${raw.toString("utf8")}` : "";
|
|
34772
|
+
} else if (outputMode === "both") {
|
|
34773
|
+
suffix = `
|
|
34774
|
+
${JSON.stringify({ screen: renderScreen(state, rows, cols), raw: raw.toString("utf8") }, null, 2)}`;
|
|
34775
|
+
} else {
|
|
34776
|
+
const screen = renderScreen(state, rows, cols);
|
|
34777
|
+
suffix = screen ? `
|
|
34778
|
+
${screen}` : "";
|
|
34779
|
+
}
|
|
34780
|
+
if (data.status === "running") {
|
|
34781
|
+
suffix += `
|
|
34782
|
+
PTY task is still running. Use bash_status({ taskId: "${taskId}", outputMode: "screen" }) to inspect, bash_write({ taskId: "${taskId}", input: "..." }) to send keystrokes.`;
|
|
34783
|
+
} else if (isTerminalStatus(data.status)) {
|
|
34784
|
+
await disposePtyTerminal(key);
|
|
34785
|
+
}
|
|
34786
|
+
return suffix;
|
|
34787
|
+
}
|
|
34788
|
+
function ptyDimensions(data) {
|
|
34789
|
+
const rows = typeof data.pty_rows === "number" ? data.pty_rows : 24;
|
|
34790
|
+
const cols = typeof data.pty_cols === "number" ? data.pty_cols : 80;
|
|
34791
|
+
return { rows, cols };
|
|
34792
|
+
}
|
|
34793
|
+
function ptyCacheKey(runtime, taskId) {
|
|
34794
|
+
return `${projectRootFor(runtime)}::${runtime.sessionID ?? "__default__"}::${taskId}`;
|
|
34795
|
+
}
|
|
34796
|
+
function preview(output) {
|
|
34797
|
+
return output.length <= METADATA_PREVIEW_LIMIT ? output : output.slice(-METADATA_PREVIEW_LIMIT);
|
|
34798
|
+
}
|
|
34799
|
+
function isTerminalStatus(status) {
|
|
34800
|
+
return status === "completed" || status === "failed" || status === "killed" || status === "timed_out";
|
|
34801
|
+
}
|
|
34802
|
+
function subagentGuidance(taskId) {
|
|
34803
|
+
return `
|
|
34804
|
+
|
|
34805
|
+
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.`;
|
|
34806
|
+
}
|
|
34807
|
+
function formatBackgroundLaunch(taskId, isPty) {
|
|
34808
|
+
if (isPty) {
|
|
34809
|
+
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.`;
|
|
34810
|
+
}
|
|
34811
|
+
return `Background task started: ${taskId}. A completion reminder will be delivered automatically; don't poll bash_status.`;
|
|
34812
|
+
}
|
|
34813
|
+
function formatPromotionMessage(taskId, timeout, waitWindowMs) {
|
|
34814
|
+
const waited = timeout !== undefined ? Math.min(timeout, waitWindowMs) : waitWindowMs;
|
|
34815
|
+
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.`;
|
|
34816
|
+
}
|
|
34817
|
+
function formatSeconds(ms) {
|
|
34818
|
+
return `${Number((ms / 1000).toFixed(1))}s`;
|
|
34819
|
+
}
|
|
34820
|
+
function formatForegroundResult(data) {
|
|
34821
|
+
const output = data.output_preview ?? "";
|
|
34822
|
+
const outputPath = data.output_path;
|
|
34823
|
+
const truncated = data.output_truncated === true;
|
|
34824
|
+
const status = data.status;
|
|
34825
|
+
const exit = data.exit_code;
|
|
34826
|
+
let rendered = output;
|
|
34827
|
+
if (truncated && outputPath) {
|
|
34828
|
+
rendered += `
|
|
34829
|
+
[output truncated; full output at ${outputPath}]`;
|
|
34830
|
+
}
|
|
34831
|
+
if (status === "timed_out") {
|
|
34832
|
+
rendered += `
|
|
34833
|
+
[command timed out]`;
|
|
34834
|
+
}
|
|
34835
|
+
if (typeof exit === "number" && exit !== 0) {
|
|
34836
|
+
rendered += `
|
|
34837
|
+
[exit code: ${exit}]`;
|
|
34838
|
+
}
|
|
34839
|
+
return rendered;
|
|
34840
|
+
}
|
|
34841
|
+
function foregroundMetadata(description, data, rendered) {
|
|
34842
|
+
const outputPath = data.output_path;
|
|
34843
|
+
return {
|
|
34844
|
+
description,
|
|
34845
|
+
output: preview(rendered),
|
|
34846
|
+
exit: data.exit_code,
|
|
34847
|
+
truncated: data.output_truncated,
|
|
34848
|
+
...outputPath ? { outputPath } : {}
|
|
34849
|
+
};
|
|
34850
|
+
}
|
|
34851
|
+
function sleep(ms) {
|
|
34852
|
+
return new Promise((resolve7) => setTimeout(resolve7, ms));
|
|
34853
|
+
}
|
|
34854
|
+
function getCallID(ctx) {
|
|
34855
|
+
const c = ctx;
|
|
34856
|
+
return c.callID ?? c.callId ?? c.call_id;
|
|
34857
|
+
}
|
|
34858
|
+
function shortenCommand(command) {
|
|
34859
|
+
const collapsed = command.replace(/\s+/g, " ").trim();
|
|
34860
|
+
return collapsed.length <= 80 ? collapsed : `${collapsed.slice(0, 77)}...`;
|
|
34861
|
+
}
|
|
34862
|
+
|
|
34863
|
+
// src/tools/conflicts.ts
|
|
34864
|
+
import { tool as tool5 } from "@opencode-ai/plugin";
|
|
34865
|
+
var z5 = tool5.schema;
|
|
34866
|
+
function conflictTools(ctx) {
|
|
34867
|
+
return {
|
|
34868
|
+
aft_conflicts: {
|
|
34869
|
+
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).",
|
|
34870
|
+
args: {
|
|
34871
|
+
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()
|
|
34872
|
+
},
|
|
34873
|
+
execute: async (args, context) => {
|
|
34874
|
+
const params = {};
|
|
34875
|
+
if (!isEmptyParam(args?.path)) {
|
|
34876
|
+
const expanded = expandTilde(String(args.path));
|
|
34877
|
+
const projectRoot = await resolveProjectRoot(ctx, context);
|
|
34878
|
+
const resolved = resolvePathFromProjectRoot(projectRoot, expanded);
|
|
34879
|
+
const denied = await assertExternalDirectoryPermission(context, resolved, {
|
|
34880
|
+
kind: "directory"
|
|
34881
|
+
});
|
|
34882
|
+
if (denied)
|
|
34883
|
+
return permissionDeniedResponse(denied);
|
|
34884
|
+
params.path = resolved;
|
|
34885
|
+
}
|
|
34886
|
+
const response = await callBridge(ctx, context, "git_conflicts", params);
|
|
34887
|
+
if (response.success === false) {
|
|
34888
|
+
throw new Error(response.message || "git_conflicts failed");
|
|
34889
|
+
}
|
|
34890
|
+
return response.text;
|
|
34891
|
+
}
|
|
34892
|
+
}
|
|
34893
|
+
};
|
|
34894
|
+
}
|
|
34895
|
+
|
|
34896
|
+
// src/tools/hoisted.ts
|
|
34897
|
+
import * as fs6 from "node:fs";
|
|
34898
|
+
import * as path4 from "node:path";
|
|
34899
|
+
import { tool as tool8 } from "@opencode-ai/plugin";
|
|
34900
|
+
|
|
34901
|
+
// src/patch-parser.ts
|
|
34902
|
+
function stripHeredoc(input) {
|
|
34903
|
+
const heredocMatch = input.match(/^(?:cat\s+)?<<['"]?(\w+)['"]?\s*\n([\s\S]*?)\n\1\s*$/);
|
|
34904
|
+
return heredocMatch ? heredocMatch[2] : input;
|
|
34905
|
+
}
|
|
34906
|
+
function parsePatchHeader(lines, startIdx) {
|
|
34907
|
+
const line = lines[startIdx];
|
|
34908
|
+
if (line.startsWith("*** Add File:")) {
|
|
34909
|
+
const filePath = line.slice("*** Add File:".length).trim();
|
|
34910
|
+
return filePath ? { filePath, nextIdx: startIdx + 1 } : null;
|
|
34911
|
+
}
|
|
34912
|
+
if (line.startsWith("*** Delete File:")) {
|
|
34913
|
+
const filePath = line.slice("*** Delete File:".length).trim();
|
|
34914
|
+
return filePath ? { filePath, nextIdx: startIdx + 1 } : null;
|
|
34915
|
+
}
|
|
34916
|
+
if (line.startsWith("*** Update File:")) {
|
|
34917
|
+
const filePath = line.slice("*** Update File:".length).trim();
|
|
34918
|
+
let movePath;
|
|
34919
|
+
let nextIdx = startIdx + 1;
|
|
34920
|
+
if (nextIdx < lines.length && lines[nextIdx].startsWith("*** Move to:")) {
|
|
34921
|
+
movePath = lines[nextIdx].slice("*** Move to:".length).trim();
|
|
34922
|
+
nextIdx++;
|
|
34923
|
+
}
|
|
34924
|
+
return filePath ? { filePath, movePath, nextIdx } : null;
|
|
34925
|
+
}
|
|
34926
|
+
return null;
|
|
34657
34927
|
}
|
|
34658
|
-
function
|
|
34659
|
-
|
|
34660
|
-
|
|
34661
|
-
|
|
34928
|
+
function parseAddFileContent(lines, startIdx) {
|
|
34929
|
+
let content = "";
|
|
34930
|
+
let i = startIdx;
|
|
34931
|
+
while (i < lines.length && !lines[i].startsWith("***")) {
|
|
34932
|
+
if (lines[i].startsWith("+")) {
|
|
34933
|
+
content += `${lines[i].substring(1)}
|
|
34934
|
+
`;
|
|
34935
|
+
}
|
|
34936
|
+
i++;
|
|
34937
|
+
}
|
|
34938
|
+
if (content.endsWith(`
|
|
34939
|
+
`)) {
|
|
34940
|
+
content = content.slice(0, -1);
|
|
34941
|
+
}
|
|
34942
|
+
return { content, nextIdx: i };
|
|
34662
34943
|
}
|
|
34663
|
-
function
|
|
34664
|
-
|
|
34665
|
-
|
|
34666
|
-
|
|
34667
|
-
|
|
34668
|
-
|
|
34669
|
-
|
|
34670
|
-
|
|
34671
|
-
const
|
|
34672
|
-
|
|
34673
|
-
|
|
34674
|
-
|
|
34944
|
+
function parseUpdateFileChunks(lines, startIdx) {
|
|
34945
|
+
const chunks = [];
|
|
34946
|
+
let i = startIdx;
|
|
34947
|
+
while (i < lines.length && !lines[i].startsWith("***")) {
|
|
34948
|
+
if (lines[i].startsWith("@@")) {
|
|
34949
|
+
const contextLine = lines[i].substring(2).trim();
|
|
34950
|
+
i++;
|
|
34951
|
+
const oldLines = [];
|
|
34952
|
+
const newLines = [];
|
|
34953
|
+
let isEndOfFile = false;
|
|
34954
|
+
while (i < lines.length && !lines[i].startsWith("@@") && !lines[i].startsWith("***")) {
|
|
34955
|
+
const changeLine = lines[i];
|
|
34956
|
+
if (changeLine === "*** End of File") {
|
|
34957
|
+
isEndOfFile = true;
|
|
34958
|
+
i++;
|
|
34959
|
+
break;
|
|
34960
|
+
}
|
|
34961
|
+
if (changeLine.startsWith(" ")) {
|
|
34962
|
+
const content = changeLine.substring(1);
|
|
34963
|
+
oldLines.push(content);
|
|
34964
|
+
newLines.push(content);
|
|
34965
|
+
} else if (changeLine.startsWith("-")) {
|
|
34966
|
+
oldLines.push(changeLine.substring(1));
|
|
34967
|
+
} else if (changeLine.startsWith("+")) {
|
|
34968
|
+
newLines.push(changeLine.substring(1));
|
|
34969
|
+
}
|
|
34970
|
+
i++;
|
|
34971
|
+
}
|
|
34972
|
+
chunks.push({
|
|
34973
|
+
old_lines: oldLines,
|
|
34974
|
+
new_lines: newLines,
|
|
34975
|
+
change_context: contextLine || undefined,
|
|
34976
|
+
is_end_of_file: isEndOfFile || undefined
|
|
34977
|
+
});
|
|
34978
|
+
} else {
|
|
34979
|
+
i++;
|
|
34675
34980
|
}
|
|
34676
|
-
}
|
|
34981
|
+
}
|
|
34982
|
+
return { chunks, nextIdx: i };
|
|
34677
34983
|
}
|
|
34678
|
-
|
|
34679
|
-
|
|
34680
|
-
|
|
34681
|
-
|
|
34682
|
-
|
|
34683
|
-
|
|
34684
|
-
|
|
34685
|
-
|
|
34686
|
-
|
|
34984
|
+
var MAX_PATCH_SIZE = 1024 * 1024;
|
|
34985
|
+
var MAX_HUNKS = 500;
|
|
34986
|
+
function parsePatch(patchText) {
|
|
34987
|
+
if (patchText.length > MAX_PATCH_SIZE) {
|
|
34988
|
+
throw new Error(`Patch too large: ${patchText.length} bytes exceeds limit of ${MAX_PATCH_SIZE} bytes`);
|
|
34989
|
+
}
|
|
34990
|
+
const cleaned = stripHeredoc(patchText.trim());
|
|
34991
|
+
const lines = cleaned.split(`
|
|
34992
|
+
`);
|
|
34993
|
+
const hunks = [];
|
|
34994
|
+
const beginIdx = lines.findIndex((line) => line.trim() === "*** Begin Patch");
|
|
34995
|
+
const endIdx = lines.findIndex((line) => line.trim() === "*** End Patch");
|
|
34996
|
+
if (beginIdx === -1 || endIdx === -1 || beginIdx >= endIdx) {
|
|
34997
|
+
throw new Error("Invalid patch format: missing *** Begin Patch / *** End Patch markers");
|
|
34998
|
+
}
|
|
34999
|
+
let i = beginIdx + 1;
|
|
35000
|
+
while (i < endIdx) {
|
|
35001
|
+
const header = parsePatchHeader(lines, i);
|
|
35002
|
+
if (!header) {
|
|
35003
|
+
i++;
|
|
35004
|
+
continue;
|
|
35005
|
+
}
|
|
35006
|
+
if (hunks.length >= MAX_HUNKS) {
|
|
35007
|
+
throw new Error(`Patch exceeds maximum of ${MAX_HUNKS} file operations`);
|
|
35008
|
+
}
|
|
35009
|
+
if (lines[i].startsWith("*** Add File:")) {
|
|
35010
|
+
const { content, nextIdx } = parseAddFileContent(lines, header.nextIdx);
|
|
35011
|
+
hunks.push({ type: "add", path: header.filePath, contents: content });
|
|
35012
|
+
i = nextIdx;
|
|
35013
|
+
} else if (lines[i].startsWith("*** Delete File:")) {
|
|
35014
|
+
hunks.push({ type: "delete", path: header.filePath });
|
|
35015
|
+
i = header.nextIdx;
|
|
35016
|
+
} else if (lines[i].startsWith("*** Update File:")) {
|
|
35017
|
+
const { chunks, nextIdx } = parseUpdateFileChunks(lines, header.nextIdx);
|
|
35018
|
+
hunks.push({
|
|
35019
|
+
type: "update",
|
|
35020
|
+
path: header.filePath,
|
|
35021
|
+
move_path: header.movePath,
|
|
35022
|
+
chunks
|
|
34687
35023
|
});
|
|
34688
|
-
|
|
34689
|
-
|
|
35024
|
+
i = nextIdx;
|
|
35025
|
+
} else {
|
|
35026
|
+
i++;
|
|
35027
|
+
}
|
|
35028
|
+
}
|
|
35029
|
+
return hunks;
|
|
35030
|
+
}
|
|
35031
|
+
function normalizeUnicode(str) {
|
|
35032
|
+
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, " ");
|
|
35033
|
+
}
|
|
35034
|
+
function normalizeIndent(str) {
|
|
35035
|
+
return str.replace(/^[\t ]+/, (m) => " ".repeat(m.length));
|
|
35036
|
+
}
|
|
35037
|
+
function tryMatch(lines, pattern, startIndex, compare, eof) {
|
|
35038
|
+
if (eof) {
|
|
35039
|
+
const fromEnd = lines.length - pattern.length;
|
|
35040
|
+
if (fromEnd >= startIndex) {
|
|
35041
|
+
let matches = true;
|
|
35042
|
+
for (let j = 0;j < pattern.length; j++) {
|
|
35043
|
+
if (!compare(lines[fromEnd + j], pattern[j])) {
|
|
35044
|
+
matches = false;
|
|
35045
|
+
break;
|
|
35046
|
+
}
|
|
34690
35047
|
}
|
|
34691
|
-
|
|
34692
|
-
|
|
34693
|
-
|
|
35048
|
+
if (matches)
|
|
35049
|
+
return fromEnd;
|
|
35050
|
+
}
|
|
35051
|
+
}
|
|
35052
|
+
for (let i = startIndex;i <= lines.length - pattern.length; i++) {
|
|
35053
|
+
let matches = true;
|
|
35054
|
+
for (let j = 0;j < pattern.length; j++) {
|
|
35055
|
+
if (!compare(lines[i + j], pattern[j])) {
|
|
35056
|
+
matches = false;
|
|
35057
|
+
break;
|
|
34694
35058
|
}
|
|
34695
|
-
return `Task ${args.taskId}: ${String(data.status ?? "killed")}`;
|
|
34696
35059
|
}
|
|
34697
|
-
|
|
34698
|
-
|
|
34699
|
-
async function bashStatusSnapshot(ctx, runtime, taskId, outputMode, options) {
|
|
34700
|
-
const data = await callBashBridge(ctx, runtime, "bash_status", { task_id: taskId, output_mode: outputMode }, options);
|
|
34701
|
-
if (data.success === false) {
|
|
34702
|
-
throw new Error(data.message ?? "bash_status failed");
|
|
35060
|
+
if (matches)
|
|
35061
|
+
return i;
|
|
34703
35062
|
}
|
|
34704
|
-
return
|
|
35063
|
+
return -1;
|
|
34705
35064
|
}
|
|
34706
|
-
|
|
34707
|
-
|
|
34708
|
-
|
|
34709
|
-
const
|
|
34710
|
-
|
|
34711
|
-
|
|
34712
|
-
|
|
34713
|
-
|
|
34714
|
-
|
|
34715
|
-
|
|
34716
|
-
|
|
34717
|
-
|
|
35065
|
+
function seekSequenceTiered(lines, pattern, startIndex, eof = false) {
|
|
35066
|
+
if (pattern.length === 0)
|
|
35067
|
+
return null;
|
|
35068
|
+
const exact = tryMatch(lines, pattern, startIndex, (a, b) => a === b, eof);
|
|
35069
|
+
if (exact !== -1)
|
|
35070
|
+
return { found: exact, tier: "exact" };
|
|
35071
|
+
const rstrip = tryMatch(lines, pattern, startIndex, (a, b) => a.trimEnd() === b.trimEnd(), eof);
|
|
35072
|
+
if (rstrip !== -1)
|
|
35073
|
+
return { found: rstrip, tier: "rstrip" };
|
|
35074
|
+
const trim = tryMatch(lines, pattern, startIndex, (a, b) => a.trim() === b.trim(), eof);
|
|
35075
|
+
if (trim !== -1)
|
|
35076
|
+
return { found: trim, tier: "trim" };
|
|
35077
|
+
const indent = tryMatch(lines, pattern, startIndex, (a, b) => normalizeIndent(a).trimEnd() === normalizeIndent(b).trimEnd(), eof);
|
|
35078
|
+
if (indent !== -1)
|
|
35079
|
+
return { found: indent, tier: "indent" };
|
|
35080
|
+
const unicode = tryMatch(lines, pattern, startIndex, (a, b) => normalizeUnicode(a.trim()) === normalizeUnicode(b.trim()), eof);
|
|
35081
|
+
if (unicode !== -1)
|
|
35082
|
+
return { found: unicode, tier: "unicode" };
|
|
35083
|
+
return null;
|
|
35084
|
+
}
|
|
35085
|
+
function seekSequence(lines, pattern, startIndex, eof = false) {
|
|
35086
|
+
const r = seekSequenceTiered(lines, pattern, startIndex, eof);
|
|
35087
|
+
return r ? r.found : -1;
|
|
35088
|
+
}
|
|
35089
|
+
function findClosestPartialMatch(lines, pattern) {
|
|
35090
|
+
if (pattern.length === 0 || lines.length === 0)
|
|
35091
|
+
return null;
|
|
35092
|
+
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());
|
|
35093
|
+
const candidates = [];
|
|
35094
|
+
for (let i = 0;i < lines.length && candidates.length < 16; i++) {
|
|
35095
|
+
if (compareAny(lines[i], pattern[0]))
|
|
35096
|
+
candidates.push(i);
|
|
35097
|
+
}
|
|
35098
|
+
if (candidates.length === 0)
|
|
35099
|
+
return null;
|
|
35100
|
+
let best = { lineNumber: -1, matchedLines: 0, firstDivergence: -1 };
|
|
35101
|
+
for (const start of candidates) {
|
|
35102
|
+
let matched = 0;
|
|
35103
|
+
for (let j = 0;j < pattern.length && start + j < lines.length; j++) {
|
|
35104
|
+
if (!compareAny(lines[start + j], pattern[j]))
|
|
35105
|
+
break;
|
|
35106
|
+
matched++;
|
|
34718
35107
|
}
|
|
34719
|
-
if (
|
|
34720
|
-
|
|
34721
|
-
|
|
35108
|
+
if (matched > best.matchedLines) {
|
|
35109
|
+
best = {
|
|
35110
|
+
lineNumber: start + 1,
|
|
35111
|
+
matchedLines: matched,
|
|
35112
|
+
firstDivergence: matched
|
|
35113
|
+
};
|
|
34722
35114
|
}
|
|
34723
35115
|
}
|
|
34724
|
-
return
|
|
35116
|
+
return best.lineNumber === -1 ? null : best;
|
|
34725
35117
|
}
|
|
34726
|
-
|
|
34727
|
-
const
|
|
34728
|
-
|
|
34729
|
-
|
|
34730
|
-
|
|
34731
|
-
const key = ptyCacheKey(runtime, taskId);
|
|
34732
|
-
const { rows, cols } = ptyDimensions(data);
|
|
34733
|
-
const state = await getOrCreatePtyTerminal(key, outputPath, rows, cols);
|
|
34734
|
-
const raw = await readPtyBytes(state);
|
|
34735
|
-
const outputMode = requestedOutputMode ?? "screen";
|
|
34736
|
-
let suffix = "";
|
|
34737
|
-
if (outputMode === "raw") {
|
|
34738
|
-
suffix = raw.length > 0 ? `
|
|
34739
|
-
${raw.toString("utf8")}` : "";
|
|
34740
|
-
} else if (outputMode === "both") {
|
|
34741
|
-
suffix = `
|
|
34742
|
-
${JSON.stringify({ screen: renderScreen(state, rows, cols), raw: raw.toString("utf8") }, null, 2)}`;
|
|
34743
|
-
} else {
|
|
34744
|
-
const screen = renderScreen(state, rows, cols);
|
|
34745
|
-
suffix = screen ? `
|
|
34746
|
-
${screen}` : "";
|
|
34747
|
-
}
|
|
34748
|
-
if (data.status === "running") {
|
|
34749
|
-
suffix += `
|
|
34750
|
-
PTY task is still running. Use bash_status({ taskId: "${taskId}", outputMode: "screen" }) to inspect, bash_write({ taskId: "${taskId}", input: "..." }) to send keystrokes.`;
|
|
34751
|
-
} else if (isTerminalStatus(data.status)) {
|
|
34752
|
-
await disposePtyTerminal(key);
|
|
35118
|
+
function applyUpdateChunks(originalContent, filePath, chunks) {
|
|
35119
|
+
const originalLines = originalContent.split(`
|
|
35120
|
+
`);
|
|
35121
|
+
if (originalLines.length > 0 && originalLines[originalLines.length - 1] === "") {
|
|
35122
|
+
originalLines.pop();
|
|
34753
35123
|
}
|
|
34754
|
-
|
|
34755
|
-
|
|
34756
|
-
|
|
34757
|
-
|
|
34758
|
-
|
|
34759
|
-
|
|
34760
|
-
}
|
|
34761
|
-
|
|
34762
|
-
|
|
34763
|
-
}
|
|
34764
|
-
|
|
34765
|
-
|
|
34766
|
-
|
|
34767
|
-
|
|
34768
|
-
|
|
34769
|
-
|
|
34770
|
-
|
|
34771
|
-
|
|
35124
|
+
const replacements = [];
|
|
35125
|
+
let lineIndex = 0;
|
|
35126
|
+
for (const chunk of chunks) {
|
|
35127
|
+
if (chunk.change_context) {
|
|
35128
|
+
const contextIdx = seekSequence(originalLines, [chunk.change_context], lineIndex);
|
|
35129
|
+
if (contextIdx === -1) {
|
|
35130
|
+
throw new Error(`Failed to find context '${chunk.change_context}' in ${filePath}`);
|
|
35131
|
+
}
|
|
35132
|
+
lineIndex = contextIdx + 1;
|
|
35133
|
+
}
|
|
35134
|
+
if (chunk.old_lines.length === 0) {
|
|
35135
|
+
let insertionIdx;
|
|
35136
|
+
if (chunk.change_context) {
|
|
35137
|
+
insertionIdx = lineIndex;
|
|
35138
|
+
} else if (originalLines.length > 0 && originalLines[originalLines.length - 1] === "") {
|
|
35139
|
+
insertionIdx = originalLines.length - 1;
|
|
35140
|
+
} else {
|
|
35141
|
+
insertionIdx = originalLines.length;
|
|
35142
|
+
}
|
|
35143
|
+
replacements.push([insertionIdx, 0, chunk.new_lines]);
|
|
35144
|
+
continue;
|
|
35145
|
+
}
|
|
35146
|
+
let pattern = chunk.old_lines;
|
|
35147
|
+
let newSlice = chunk.new_lines;
|
|
35148
|
+
let found = seekSequence(originalLines, pattern, lineIndex, chunk.is_end_of_file);
|
|
35149
|
+
if (found === -1 && pattern.length > 0 && pattern[pattern.length - 1] === "") {
|
|
35150
|
+
pattern = pattern.slice(0, -1);
|
|
35151
|
+
if (newSlice.length > 0 && newSlice[newSlice.length - 1] === "") {
|
|
35152
|
+
newSlice = newSlice.slice(0, -1);
|
|
35153
|
+
}
|
|
35154
|
+
found = seekSequence(originalLines, pattern, lineIndex, chunk.is_end_of_file);
|
|
35155
|
+
}
|
|
35156
|
+
if (found !== -1) {
|
|
35157
|
+
replacements.push([found, pattern.length, newSlice]);
|
|
35158
|
+
lineIndex = found + pattern.length;
|
|
35159
|
+
} else {
|
|
35160
|
+
const newSliceTrimmed = newSlice.filter((line) => line.trim().length > 0);
|
|
35161
|
+
const alreadyApplied = newSliceTrimmed.length > 0 && seekSequence(originalLines, newSliceTrimmed, 0, chunk.is_end_of_file) !== -1;
|
|
35162
|
+
const closest = findClosestPartialMatch(originalLines, pattern);
|
|
35163
|
+
let closestHint = "";
|
|
35164
|
+
if (closest && closest.matchedLines > 0) {
|
|
35165
|
+
const fileLineNo = closest.lineNumber + closest.firstDivergence;
|
|
35166
|
+
const expectedLine = pattern[closest.firstDivergence];
|
|
35167
|
+
const actualLine = fileLineNo - 1 < originalLines.length ? originalLines[fileLineNo - 1] : "<EOF>";
|
|
35168
|
+
closestHint = `
|
|
34772
35169
|
|
|
34773
|
-
|
|
34774
|
-
}
|
|
34775
|
-
|
|
34776
|
-
|
|
34777
|
-
|
|
34778
|
-
|
|
34779
|
-
|
|
34780
|
-
|
|
34781
|
-
|
|
34782
|
-
|
|
34783
|
-
|
|
34784
|
-
}
|
|
34785
|
-
|
|
34786
|
-
|
|
34787
|
-
}
|
|
34788
|
-
function formatForegroundResult(data) {
|
|
34789
|
-
const output = data.output_preview ?? "";
|
|
34790
|
-
const outputPath = data.output_path;
|
|
34791
|
-
const truncated = data.output_truncated === true;
|
|
34792
|
-
const status = data.status;
|
|
34793
|
-
const exit = data.exit_code;
|
|
34794
|
-
let rendered = output;
|
|
34795
|
-
if (truncated && outputPath) {
|
|
34796
|
-
rendered += `
|
|
34797
|
-
[output truncated; full output at ${outputPath}]`;
|
|
35170
|
+
Closest match starts at line ${closest.lineNumber} ` + `(${closest.matchedLines} of ${pattern.length} lines matched).
|
|
35171
|
+
` + `First divergence at line ${fileLineNo}:
|
|
35172
|
+
` + ` expected: ${JSON.stringify(expectedLine)}
|
|
35173
|
+
` + ` actual: ${JSON.stringify(actualLine)}`;
|
|
35174
|
+
}
|
|
35175
|
+
const triedTiers = "exact, trimEnd, trim, indent (tab/space), unicode";
|
|
35176
|
+
const alreadyAppliedHint = alreadyApplied ? `
|
|
35177
|
+
|
|
35178
|
+
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." : "";
|
|
35179
|
+
throw new Error(`Failed to find expected lines in ${filePath}:
|
|
35180
|
+
${chunk.old_lines.join(`
|
|
35181
|
+
`)}
|
|
35182
|
+
|
|
35183
|
+
` + `Tried match tiers: ${triedTiers}.${closestHint}${alreadyAppliedHint}`);
|
|
35184
|
+
}
|
|
34798
35185
|
}
|
|
34799
|
-
|
|
34800
|
-
|
|
34801
|
-
|
|
35186
|
+
replacements.sort((a, b) => a[0] - b[0]);
|
|
35187
|
+
const result = [...originalLines];
|
|
35188
|
+
for (let i = replacements.length - 1;i >= 0; i--) {
|
|
35189
|
+
const [startIdx, oldLen, newSegment] = replacements[i];
|
|
35190
|
+
result.splice(startIdx, oldLen, ...newSegment);
|
|
34802
35191
|
}
|
|
34803
|
-
if (
|
|
34804
|
-
|
|
34805
|
-
[exit code: ${exit}]`;
|
|
35192
|
+
if (result.length === 0 || result[result.length - 1] !== "") {
|
|
35193
|
+
result.push("");
|
|
34806
35194
|
}
|
|
34807
|
-
return
|
|
34808
|
-
|
|
34809
|
-
function foregroundMetadata(description, data, rendered) {
|
|
34810
|
-
const outputPath = data.output_path;
|
|
34811
|
-
return {
|
|
34812
|
-
description,
|
|
34813
|
-
output: preview(rendered),
|
|
34814
|
-
exit: data.exit_code,
|
|
34815
|
-
truncated: data.output_truncated,
|
|
34816
|
-
...outputPath ? { outputPath } : {}
|
|
34817
|
-
};
|
|
34818
|
-
}
|
|
34819
|
-
function sleep(ms) {
|
|
34820
|
-
return new Promise((resolve7) => setTimeout(resolve7, ms));
|
|
34821
|
-
}
|
|
34822
|
-
function getCallID(ctx) {
|
|
34823
|
-
const c = ctx;
|
|
34824
|
-
return c.callID ?? c.callId ?? c.call_id;
|
|
34825
|
-
}
|
|
34826
|
-
function shortenCommand(command) {
|
|
34827
|
-
const collapsed = command.replace(/\s+/g, " ").trim();
|
|
34828
|
-
return collapsed.length <= 80 ? collapsed : `${collapsed.slice(0, 77)}...`;
|
|
35195
|
+
return result.join(`
|
|
35196
|
+
`);
|
|
34829
35197
|
}
|
|
34830
35198
|
|
|
34831
35199
|
// src/tools/bash_watch.ts
|
|
@@ -34890,6 +35258,12 @@ A notification will fire when the pattern matches or the task exits.`;
|
|
|
34890
35258
|
const effectiveWaitMs = subagentForcedSync ? MAX_BASH_STATUS_WAIT_TIMEOUT_MS : Math.min(args.timeoutMs ?? DEFAULT_BASH_STATUS_WAIT_TIMEOUT_MS, MAX_BASH_STATUS_WAIT_TIMEOUT_MS);
|
|
34891
35259
|
const data = await waitForBashStatus(ctx, context, taskId, undefined, waitFor, effectiveWaitMs);
|
|
34892
35260
|
const waited = data.waited;
|
|
35261
|
+
if (waited?.reason === "user_message") {
|
|
35262
|
+
const convertedText = await convertToAsyncWatchOnAbort(ctx, context, taskId, waitFor, args.once !== false);
|
|
35263
|
+
const metadata2 = context.metadata;
|
|
35264
|
+
metadata2?.({ taskId, status: data.status, waited, convertedToAsync: true });
|
|
35265
|
+
return convertedText;
|
|
35266
|
+
}
|
|
34893
35267
|
const metadata = context.metadata;
|
|
34894
35268
|
if (waited)
|
|
34895
35269
|
metadata?.({ taskId, status: data.status, waited });
|
|
@@ -34897,6 +35271,31 @@ A notification will fire when the pattern matches or the task exits.`;
|
|
|
34897
35271
|
}
|
|
34898
35272
|
};
|
|
34899
35273
|
}
|
|
35274
|
+
async function convertToAsyncWatchOnAbort(ctx, context, taskId, waitFor, once) {
|
|
35275
|
+
if (!waitFor) {
|
|
35276
|
+
return `Sync watch for task ${taskId} was interrupted because you sent a message. ` + `The task is still running in the background. A completion reminder will be ` + `delivered automatically when the task exits; don't poll bash_status.`;
|
|
35277
|
+
}
|
|
35278
|
+
const notifyParams = {
|
|
35279
|
+
task_id: taskId,
|
|
35280
|
+
once
|
|
35281
|
+
};
|
|
35282
|
+
if (waitFor.kind === "regex")
|
|
35283
|
+
notifyParams.regex = waitFor.source;
|
|
35284
|
+
else
|
|
35285
|
+
notifyParams.pattern = waitFor.value;
|
|
35286
|
+
markExplicitControl(context.sessionID, taskId, false);
|
|
35287
|
+
try {
|
|
35288
|
+
const registered = await callBashBridge(ctx, context, "bash_notify", notifyParams);
|
|
35289
|
+
if (registered.success === false) {
|
|
35290
|
+
unmarkExplicitControl(context.sessionID, taskId);
|
|
35291
|
+
return `Sync watch for task ${taskId} was interrupted because you sent a message. ` + `Auto-registering an async watch failed (${String(registered.message ?? "unknown error")}). ` + `The task is still running in the background. A completion reminder will be ` + `delivered automatically when the task exits.`;
|
|
35292
|
+
}
|
|
35293
|
+
return `Sync watch for task ${taskId} was interrupted because you sent a message. ` + `The wait has been converted to an async watch (${registered.watch_id}). ` + `A notification will fire when the pattern matches or the task exits.`;
|
|
35294
|
+
} catch (err) {
|
|
35295
|
+
unmarkExplicitControl(context.sessionID, taskId);
|
|
35296
|
+
return `Sync watch for task ${taskId} was interrupted because you sent a message. ` + `Auto-registering an async watch failed (${err instanceof Error ? err.message : String(err)}). ` + `The task is still running in the background. A completion reminder will be ` + `delivered automatically when the task exits.`;
|
|
35297
|
+
}
|
|
35298
|
+
}
|
|
34900
35299
|
function formatWatchResultText(taskId, data, waited) {
|
|
34901
35300
|
const status = data.status;
|
|
34902
35301
|
const exit = typeof data.exit_code === "number" ? ` (exit ${data.exit_code})` : "";
|
|
@@ -34936,6 +35335,7 @@ async function waitForBashStatus(ctx, runtime, taskId, outputMode, waitFor, effe
|
|
|
34936
35335
|
let scanText = "";
|
|
34937
35336
|
let scanBaseOffset = 0;
|
|
34938
35337
|
const bridgeOptions = {};
|
|
35338
|
+
clearSyncWatchAbort(runtime.sessionID);
|
|
34939
35339
|
markTaskWaiting(runtime.sessionID, taskId);
|
|
34940
35340
|
let sawTerminal = false;
|
|
34941
35341
|
try {
|
|
@@ -34974,6 +35374,9 @@ async function waitForBashStatus(ctx, runtime, taskId, outputMode, waitFor, effe
|
|
|
34974
35374
|
await markBgCompletionDelivered({ ctx, directory: projectRootFor(runtime), sessionID: runtime.sessionID }, taskId);
|
|
34975
35375
|
return withWaited(data, { reason: "exited", elapsed_ms: Date.now() - startedAt });
|
|
34976
35376
|
}
|
|
35377
|
+
if (isSyncWatchAborted(runtime.sessionID)) {
|
|
35378
|
+
return withWaited(data, { reason: "user_message", elapsed_ms: Date.now() - startedAt });
|
|
35379
|
+
}
|
|
34977
35380
|
if (Date.now() >= deadline) {
|
|
34978
35381
|
return withWaited(data, { reason: "timeout", elapsed_ms: Date.now() - startedAt });
|
|
34979
35382
|
}
|
|
@@ -35339,7 +35742,6 @@ function inferBeforeStart(ops, from, beforeLen) {
|
|
|
35339
35742
|
return beforeLen;
|
|
35340
35743
|
}
|
|
35341
35744
|
var z8 = tool8.schema;
|
|
35342
|
-
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.";
|
|
35343
35745
|
function diagnosticsOnEditDefault(ctx) {
|
|
35344
35746
|
return ctx.config.lsp?.diagnostics_on_edit ?? false;
|
|
35345
35747
|
}
|
|
@@ -35471,30 +35873,14 @@ function createReadTool(ctx) {
|
|
|
35471
35873
|
};
|
|
35472
35874
|
}
|
|
35473
35875
|
function getWriteDescription(editToolName) {
|
|
35474
|
-
return `Write content to a file, creating it
|
|
35475
|
-
|
|
35476
|
-
Automatically creates parent directories. Backs up existing files before overwriting.
|
|
35477
|
-
If the project has a formatter configured (biome, prettier, rustfmt, etc.), the file
|
|
35478
|
-
is auto-formatted after writing. Edits return as soon as the write completes unless
|
|
35479
|
-
the configured \`lsp.diagnostics_on_edit\` default or a per-call \`diagnostics: true\`
|
|
35480
|
-
asks for legacy sync-wait behavior. Call \`aft_inspect\` afterward to check
|
|
35481
|
-
diagnostics across a batch of edits.
|
|
35482
|
-
|
|
35483
|
-
**Behavior:**
|
|
35484
|
-
- Creates parent directories automatically (no need to mkdir first)
|
|
35485
|
-
- Existing files are backed up before overwriting (recoverable via aft_safety undo)
|
|
35486
|
-
- Auto-formats using project formatter if configured (biome.json, .prettierrc, etc.)
|
|
35487
|
-
- LSP diagnostics follow \`lsp.diagnostics_on_edit\` by default; pass \`diagnostics\` to override per call
|
|
35488
|
-
- Use this for creating new files or completely replacing file contents
|
|
35489
|
-
- For partial edits (find/replace), use the \`${editToolName}\` tool instead`;
|
|
35876
|
+
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.`;
|
|
35490
35877
|
}
|
|
35491
35878
|
function createWriteTool(ctx, editToolName = "edit") {
|
|
35492
35879
|
return {
|
|
35493
35880
|
description: getWriteDescription(editToolName),
|
|
35494
35881
|
args: {
|
|
35495
35882
|
filePath: z8.string().describe("Path to the file to write (absolute or relative to project root)"),
|
|
35496
|
-
content: z8.string().describe("The full content to write to the file")
|
|
35497
|
-
diagnostics: z8.boolean().optional().describe(DIAGNOSTICS_PARAM_DESCRIPTION)
|
|
35883
|
+
content: z8.string().describe("The full content to write to the file")
|
|
35498
35884
|
},
|
|
35499
35885
|
execute: async (args, context) => {
|
|
35500
35886
|
const file2 = args.filePath;
|
|
@@ -35517,12 +35903,15 @@ function createWriteTool(ctx, editToolName = "edit") {
|
|
|
35517
35903
|
file: filePath,
|
|
35518
35904
|
content,
|
|
35519
35905
|
create_dirs: true,
|
|
35520
|
-
diagnostics:
|
|
35906
|
+
diagnostics: diagnosticsOnEditDefault(ctx),
|
|
35521
35907
|
include_diff_content: true
|
|
35522
35908
|
});
|
|
35523
35909
|
if (data.success === false) {
|
|
35524
35910
|
throw new Error(data.message || "write failed");
|
|
35525
35911
|
}
|
|
35912
|
+
if (data.rolled_back === true) {
|
|
35913
|
+
return "Write rolled back: the content produced invalid syntax, so the file was left unchanged.";
|
|
35914
|
+
}
|
|
35526
35915
|
let output = data.created ? "Created new file." : "File updated.";
|
|
35527
35916
|
if (data.formatted)
|
|
35528
35917
|
output += " Auto-formatted.";
|
|
@@ -35621,7 +36010,6 @@ Note: Modes 5 and 6 are options on mode 4 (find/replace) — they require \`oldS
|
|
|
35621
36010
|
- Auto-formats using project formatter if configured
|
|
35622
36011
|
- Tree-sitter syntax validation on all edits
|
|
35623
36012
|
- Symbol replace includes decorators, attributes, and doc comments in range
|
|
35624
|
-
- 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.
|
|
35625
36013
|
- Response is a JSON string for the selected edit mode; key fields include success, diff, backup_id, syntax_valid, and mode-specific fields.`;
|
|
35626
36014
|
}
|
|
35627
36015
|
function createEditTool(ctx, writeToolName = "write") {
|
|
@@ -35636,8 +36024,7 @@ function createEditTool(ctx, writeToolName = "write") {
|
|
|
35636
36024
|
symbol: z8.string().optional().describe("Named symbol to replace (function, class, type)"),
|
|
35637
36025
|
content: z8.string().optional().describe("Replacement content for symbol mode. For whole-file writes, use the `write` tool."),
|
|
35638
36026
|
appendContent: z8.string().optional().describe("Text to append to the end of filePath; creates the file if needed"),
|
|
35639
|
-
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 }")
|
|
35640
|
-
diagnostics: z8.boolean().optional().describe(DIAGNOSTICS_PARAM_DESCRIPTION)
|
|
36027
|
+
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 }")
|
|
35641
36028
|
},
|
|
35642
36029
|
execute: async (args, context) => {
|
|
35643
36030
|
const argsRecord = args;
|
|
@@ -35703,7 +36090,7 @@ function createEditTool(ctx, writeToolName = "write") {
|
|
|
35703
36090
|
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.";
|
|
35704
36091
|
throw new Error(`edit: no edit mode resolved from arguments.${hint}`);
|
|
35705
36092
|
}
|
|
35706
|
-
params.diagnostics =
|
|
36093
|
+
params.diagnostics = diagnosticsOnEditDefault(ctx);
|
|
35707
36094
|
params.include_diff_content = true;
|
|
35708
36095
|
const data = await callBridge(ctx, context, command, params);
|
|
35709
36096
|
if (data.success === false) {
|
|
@@ -35825,12 +36212,11 @@ function createApplyPatchTool(ctx) {
|
|
|
35825
36212
|
return {
|
|
35826
36213
|
description: APPLY_PATCH_DESCRIPTION,
|
|
35827
36214
|
args: {
|
|
35828
|
-
patchText: z8.string().describe("The full patch text including Begin/End markers")
|
|
35829
|
-
diagnostics: z8.boolean().optional().describe(DIAGNOSTICS_PARAM_DESCRIPTION)
|
|
36215
|
+
patchText: z8.string().describe("The full patch text including Begin/End markers")
|
|
35830
36216
|
},
|
|
35831
36217
|
execute: async (args, context) => {
|
|
35832
36218
|
const patchText = args.patchText;
|
|
35833
|
-
const diagnostics =
|
|
36219
|
+
const diagnostics = diagnosticsOnEditDefault(ctx);
|
|
35834
36220
|
if (!patchText)
|
|
35835
36221
|
throw new Error("'patchText' is required");
|
|
35836
36222
|
let hunks;
|
|
@@ -35918,6 +36304,9 @@ function createApplyPatchTool(ctx) {
|
|
|
35918
36304
|
if (writeResult.success === false) {
|
|
35919
36305
|
throw new Error(writeResult.message ?? "write failed");
|
|
35920
36306
|
}
|
|
36307
|
+
if (writeResult.rolled_back === true) {
|
|
36308
|
+
throw new Error("produced invalid syntax (rolled back)");
|
|
36309
|
+
}
|
|
35921
36310
|
const wrDiff = writeResult.diff;
|
|
35922
36311
|
perFileDiffs.push({
|
|
35923
36312
|
filePath,
|
|
@@ -35979,6 +36368,9 @@ function createApplyPatchTool(ctx) {
|
|
|
35979
36368
|
if (writeResult.success === false) {
|
|
35980
36369
|
throw new Error(writeResult.message ?? "write failed");
|
|
35981
36370
|
}
|
|
36371
|
+
if (writeResult.rolled_back === true) {
|
|
36372
|
+
throw new Error("produced invalid syntax (rolled back)");
|
|
36373
|
+
}
|
|
35982
36374
|
const diags = writeResult.lsp_diagnostics;
|
|
35983
36375
|
if (diags && diags.length > 0) {
|
|
35984
36376
|
const errors3 = diags.filter((d) => d.severity === "error");
|
|
@@ -37049,17 +37441,12 @@ var z13 = tool13.schema;
|
|
|
37049
37441
|
function refactoringTools(ctx) {
|
|
37050
37442
|
return {
|
|
37051
37443
|
aft_refactor: {
|
|
37052
|
-
description: `Workspace-wide refactoring
|
|
37444
|
+
description: `Workspace-wide refactoring that updates imports and references across files.
|
|
37053
37445
|
|
|
37054
37446
|
` + `Ops:
|
|
37055
|
-
` + `- 'move':
|
|
37056
|
-
` +
|
|
37057
|
-
` +
|
|
37058
|
-
` + `- 'inline': Replace a function call with the function's body, substituting args for params. Requires 'symbol', 'callSiteLine' (1-based). Validates single-return constraint.
|
|
37059
|
-
|
|
37060
|
-
` + `Each op requires specific parameters — see parameter descriptions for requirements.
|
|
37061
|
-
|
|
37062
|
-
` + "All ops need 'filePath'. Use aft_safety checkpoint/undo before risky refactors.",
|
|
37447
|
+
` + `- '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.
|
|
37448
|
+
` + `- 'extract': extract a line range into a new function with auto-detected parameters (TS/JS/TSX, Python).
|
|
37449
|
+
` + "- 'inline': replace a function call with the function's body.",
|
|
37063
37450
|
args: {
|
|
37064
37451
|
op: z13.enum(["move", "extract", "inline"]).describe("Refactoring operation"),
|
|
37065
37452
|
filePath: z13.string().describe("Path to the source file (absolute or relative to project root)"),
|
|
@@ -37228,9 +37615,10 @@ function safetyTools(ctx) {
|
|
|
37228
37615
|
if (Array.isArray(checkpointFiles)) {
|
|
37229
37616
|
const projectRoot = await resolveProjectRoot(ctx, context);
|
|
37230
37617
|
const uniqueParents = new Set;
|
|
37231
|
-
for (const
|
|
37232
|
-
if (typeof
|
|
37618
|
+
for (const rawFile of checkpointFiles) {
|
|
37619
|
+
if (typeof rawFile !== "string")
|
|
37233
37620
|
continue;
|
|
37621
|
+
const file2 = expandTilde(rawFile);
|
|
37234
37622
|
const abs = path5.isAbsolute(file2) ? file2 : path5.resolve(projectRoot, file2);
|
|
37235
37623
|
const parent = path5.dirname(abs);
|
|
37236
37624
|
if (uniqueParents.has(parent))
|
|
@@ -37270,16 +37658,17 @@ function safetyTools(ctx) {
|
|
|
37270
37658
|
const params = {};
|
|
37271
37659
|
if (args.name !== undefined)
|
|
37272
37660
|
params.name = args.name;
|
|
37273
|
-
const payloadFiles = coerceStringArray(args.files);
|
|
37661
|
+
const payloadFiles = coerceStringArray(args.files).map(expandTilde);
|
|
37662
|
+
const filePathArg = typeof args.filePath === "string" ? expandTilde(args.filePath) : undefined;
|
|
37274
37663
|
if (op === "checkpoint") {
|
|
37275
37664
|
if (payloadFiles.length > 0) {
|
|
37276
37665
|
params.files = payloadFiles;
|
|
37277
|
-
} else if (
|
|
37278
|
-
params.files = [
|
|
37666
|
+
} else if (filePathArg !== undefined) {
|
|
37667
|
+
params.files = [filePathArg];
|
|
37279
37668
|
}
|
|
37280
37669
|
} else {
|
|
37281
|
-
if (
|
|
37282
|
-
params.file =
|
|
37670
|
+
if (filePathArg !== undefined)
|
|
37671
|
+
params.file = filePathArg;
|
|
37283
37672
|
if (payloadFiles.length > 0)
|
|
37284
37673
|
params.files = payloadFiles;
|
|
37285
37674
|
}
|
|
@@ -37523,21 +37912,9 @@ function arg3(schema) {
|
|
|
37523
37912
|
function semanticTools(ctx) {
|
|
37524
37913
|
const searchTool = {
|
|
37525
37914
|
description: [
|
|
37526
|
-
"
|
|
37527
|
-
"",
|
|
37528
|
-
"When to reach for it:",
|
|
37529
|
-
"- Exploring an unfamiliar area: 'where is rate limiting handled', 'how does auth flow work'",
|
|
37530
|
-
"- Concept doesn't appear as a literal string: 'retry logic', 'cache invalidation', 'graceful shutdown'",
|
|
37531
|
-
"- Filename-shaped concepts: 'the bridge spawn helper', 'the session detection module'",
|
|
37532
|
-
"- Regex-shaped or exact text queries when you want AFT to classify and route automatically",
|
|
37533
|
-
"- You know roughly what the function does but not what it's named",
|
|
37534
|
-
"",
|
|
37535
|
-
"When NOT to use:",
|
|
37536
|
-
"- You need exhaustive literal enumeration → use grep directly",
|
|
37537
|
-
"- You want the file/module structure → use aft_outline",
|
|
37538
|
-
"- You're following a call chain → use aft_callgraph",
|
|
37915
|
+
"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').",
|
|
37539
37916
|
"",
|
|
37540
|
-
"Set hint to 'regex', 'literal',
|
|
37917
|
+
"Set hint to 'regex', 'literal', or 'semantic' to force a lane."
|
|
37541
37918
|
].join(`
|
|
37542
37919
|
`),
|
|
37543
37920
|
args: {
|
|
@@ -37581,118 +37958,8 @@ ${note}` : response.text;
|
|
|
37581
37958
|
};
|
|
37582
37959
|
}
|
|
37583
37960
|
|
|
37584
|
-
// src/tools/structure.ts
|
|
37585
|
-
import { tool as tool16 } from "@opencode-ai/plugin";
|
|
37586
|
-
var z16 = tool16.schema;
|
|
37587
|
-
function structureTools(ctx) {
|
|
37588
|
-
return {
|
|
37589
|
-
aft_transform: {
|
|
37590
|
-
description: `Scope-aware structural code transformations with correct indentation.
|
|
37591
|
-
|
|
37592
|
-
` + `Language-specific: add_derive (Rust), add_struct_tags (Go), add_decorator (Python), wrap_try_catch (TS/JS), add_member (any language).
|
|
37593
|
-
|
|
37594
|
-
` + `Ops:
|
|
37595
|
-
` + `- 'add_member': Insert method/field into class, struct, or impl block. Requires 'container' (container name) and 'code'. Optional 'position'.
|
|
37596
|
-
` + `- 'add_derive': Add Rust derive macros to a struct/enum. Requires 'target' and 'derives' array. Deduplicates existing derives.
|
|
37597
|
-
` + `- 'wrap_try_catch': Wrap a TS/JS function body in try/catch. Requires 'target' (function name). Optional 'catchBody'.
|
|
37598
|
-
` + `- 'add_decorator': Add Python decorator to function/class. Requires 'target' and 'decorator' (without @). Optional 'position'.
|
|
37599
|
-
` + `- 'add_struct_tags': Add/update Go struct field tags. Requires 'target' (struct name), 'field', 'tag', 'value'.
|
|
37600
|
-
|
|
37601
|
-
` + "Each op requires specific parameters — see parameter descriptions for requirements. Use aft_safety checkpoint/undo before risky transforms.",
|
|
37602
|
-
args: {
|
|
37603
|
-
op: z16.enum(["add_member", "add_derive", "wrap_try_catch", "add_decorator", "add_struct_tags"]).describe("Transformation operation"),
|
|
37604
|
-
filePath: z16.string().describe("Path to the source file (absolute or relative to project root)"),
|
|
37605
|
-
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."),
|
|
37606
|
-
code: z16.string().optional().describe("Member code to insert (add_member)"),
|
|
37607
|
-
position: z16.string().optional().describe("For add_member: 'first', 'last' (default), 'before:name', 'after:name'. For add_decorator: 'first' (default) or 'last' only."),
|
|
37608
|
-
target: z16.string().optional().describe("Target symbol name (add_derive: struct/enum, wrap_try_catch: function, add_decorator: function/class, add_struct_tags: struct)"),
|
|
37609
|
-
derives: z16.array(z16.string()).optional().describe("Derive macro names (add_derive — e.g. ['Clone', 'Debug'])"),
|
|
37610
|
-
catchBody: z16.string().optional().describe("Catch block body (wrap_try_catch — default: 'throw error;')"),
|
|
37611
|
-
decorator: z16.string().optional().describe("Decorator text without @ (add_decorator — e.g. 'staticmethod')"),
|
|
37612
|
-
field: z16.string().optional().describe("Struct field name (add_struct_tags)"),
|
|
37613
|
-
tag: z16.string().optional().describe("Tag key (add_struct_tags — e.g. 'json')"),
|
|
37614
|
-
value: z16.string().optional().describe("Tag value (add_struct_tags — e.g. 'user_name,omitempty')"),
|
|
37615
|
-
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)")
|
|
37616
|
-
},
|
|
37617
|
-
execute: async (args, context) => {
|
|
37618
|
-
const op = args.op;
|
|
37619
|
-
if (op === "add_member") {
|
|
37620
|
-
if (isEmptyParam(args.container))
|
|
37621
|
-
throw new Error("'container' is required for 'add_member' op");
|
|
37622
|
-
if (isEmptyParam(args.code))
|
|
37623
|
-
throw new Error("'code' is required for 'add_member' op");
|
|
37624
|
-
}
|
|
37625
|
-
if (op === "add_derive" || op === "wrap_try_catch" || op === "add_decorator" || op === "add_struct_tags") {
|
|
37626
|
-
if (isEmptyParam(args.target))
|
|
37627
|
-
throw new Error(`'target' is required for '${op}' op`);
|
|
37628
|
-
}
|
|
37629
|
-
if (op === "add_derive" && isEmptyParam(args.derives)) {
|
|
37630
|
-
throw new Error("'derives' array is required for 'add_derive' op");
|
|
37631
|
-
}
|
|
37632
|
-
if (op === "add_decorator" && isEmptyParam(args.decorator)) {
|
|
37633
|
-
throw new Error("'decorator' is required for 'add_decorator' op");
|
|
37634
|
-
}
|
|
37635
|
-
if (op === "add_struct_tags") {
|
|
37636
|
-
if (isEmptyParam(args.field))
|
|
37637
|
-
throw new Error("'field' is required for 'add_struct_tags' op");
|
|
37638
|
-
if (isEmptyParam(args.tag))
|
|
37639
|
-
throw new Error("'tag' is required for 'add_struct_tags' op");
|
|
37640
|
-
if (isEmptyParam(args.value))
|
|
37641
|
-
throw new Error("'value' is required for 'add_struct_tags' op");
|
|
37642
|
-
}
|
|
37643
|
-
const filePath = await resolvePathArg(ctx, context, args.filePath);
|
|
37644
|
-
{
|
|
37645
|
-
const denial = await assertExternalDirectoryPermission(context, filePath);
|
|
37646
|
-
if (denial)
|
|
37647
|
-
return permissionDeniedResponse(denial);
|
|
37648
|
-
}
|
|
37649
|
-
const permissionError = await askEditPermission(context, [resolveRelativePattern(context, filePath)], { filepath: filePath });
|
|
37650
|
-
if (permissionError)
|
|
37651
|
-
return permissionDeniedResponse(permissionError);
|
|
37652
|
-
const params = { file: filePath };
|
|
37653
|
-
if (args.validate !== undefined)
|
|
37654
|
-
params.validate = args.validate;
|
|
37655
|
-
switch (op) {
|
|
37656
|
-
case "add_member":
|
|
37657
|
-
params.scope = args.container;
|
|
37658
|
-
params.code = args.code;
|
|
37659
|
-
if (args.position !== undefined)
|
|
37660
|
-
params.position = args.position;
|
|
37661
|
-
break;
|
|
37662
|
-
case "add_derive":
|
|
37663
|
-
params.target = args.target;
|
|
37664
|
-
params.derives = args.derives;
|
|
37665
|
-
break;
|
|
37666
|
-
case "wrap_try_catch":
|
|
37667
|
-
params.target = args.target;
|
|
37668
|
-
if (args.catchBody !== undefined)
|
|
37669
|
-
params.catch_body = args.catchBody;
|
|
37670
|
-
break;
|
|
37671
|
-
case "add_decorator":
|
|
37672
|
-
params.target = args.target;
|
|
37673
|
-
params.decorator = args.decorator;
|
|
37674
|
-
if (args.position !== undefined)
|
|
37675
|
-
params.position = args.position;
|
|
37676
|
-
break;
|
|
37677
|
-
case "add_struct_tags":
|
|
37678
|
-
params.target = args.target;
|
|
37679
|
-
params.field = args.field;
|
|
37680
|
-
params.tag = args.tag;
|
|
37681
|
-
params.value = args.value;
|
|
37682
|
-
break;
|
|
37683
|
-
}
|
|
37684
|
-
const response = await callBridge(ctx, context, op, params);
|
|
37685
|
-
if (response.success === false) {
|
|
37686
|
-
throw new Error(response.message || `${op} failed`);
|
|
37687
|
-
}
|
|
37688
|
-
return JSON.stringify(response);
|
|
37689
|
-
}
|
|
37690
|
-
}
|
|
37691
|
-
};
|
|
37692
|
-
}
|
|
37693
|
-
|
|
37694
37961
|
// src/workflow-hints.ts
|
|
37695
|
-
var HEADING = "##
|
|
37962
|
+
var HEADING = "## IMPORTANT NOTICE about your tools";
|
|
37696
37963
|
function buildWorkflowHints(opts) {
|
|
37697
37964
|
const sections = [];
|
|
37698
37965
|
const grepName = opts.hoistBuiltins ? "grep" : "aft_grep";
|
|
@@ -37708,14 +37975,28 @@ function buildWorkflowHints(opts) {
|
|
|
37708
37975
|
const hasBash = !opts.disabledTools.has(bashName);
|
|
37709
37976
|
const hasBgBash = hasBash && opts.bashBackgroundEnabled && !opts.disabledTools.has(bashStatusName);
|
|
37710
37977
|
if (hasBash && opts.bashCompressionEnabled) {
|
|
37711
|
-
sections.push(
|
|
37978
|
+
sections.push([
|
|
37979
|
+
"**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:",
|
|
37980
|
+
"- `bun test | grep fail` → run `bun test`",
|
|
37981
|
+
"- `cargo test 2>&1 | tail -20` → run `cargo test`",
|
|
37982
|
+
"- `npm run build | head -50` → run `npm run build`"
|
|
37983
|
+
].join(`
|
|
37984
|
+
`));
|
|
37712
37985
|
}
|
|
37713
37986
|
if (hasOutline && hasZoom) {
|
|
37714
37987
|
sections.push(`**Web/URL access**: \`aft_outline({ target: url })\` first for structure, then \`aft_zoom({ url, symbols: "<heading>" })\` for the specific section.`);
|
|
37715
37988
|
}
|
|
37716
37989
|
if (hasOutline && hasZoom && (hasGrep || hasSearch)) {
|
|
37990
|
+
const searchName = hasSearch ? "aft_search" : grepName;
|
|
37717
37991
|
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.`;
|
|
37718
|
-
|
|
37992
|
+
const readName = opts.hoistBuiltins ? "read" : "aft_read";
|
|
37993
|
+
sections.push([
|
|
37994
|
+
`**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:`,
|
|
37995
|
+
`- \`grep -rn "handleAuth" src/\` in bash → \`${searchName}({ query: "handleAuth" })\``,
|
|
37996
|
+
`- \`find . -name "*.ts" | xargs grep watcher\` in bash → \`${searchName}({ query: "watcher invalidation" })\` (concepts work too)`,
|
|
37997
|
+
`- \`sed -n '100,160p' app.ts\` / \`cat app.ts\` in bash → \`${readName}({ filePath: "app.ts", startLine: 100, endLine: 160 })\``
|
|
37998
|
+
].join(`
|
|
37999
|
+
`));
|
|
37719
38000
|
}
|
|
37720
38001
|
if (hasInspect) {
|
|
37721
38002
|
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.");
|
|
@@ -37733,13 +38014,22 @@ function buildWorkflowHints(opts) {
|
|
|
37733
38014
|
`));
|
|
37734
38015
|
}
|
|
37735
38016
|
if (hasBash && hasBgBash) {
|
|
37736
|
-
sections.push(
|
|
38017
|
+
sections.push([
|
|
38018
|
+
`**Long-running commands** (builds, installs, full test suites): \`${bashName}({ background: true })\` returns a \`taskId\` immediately. Then do ONE of these:`,
|
|
38019
|
+
"1. Keep working on something independent of the result.",
|
|
38020
|
+
"2. End your turn — a completion reminder with the result arrives automatically.",
|
|
38021
|
+
`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.`,
|
|
38022
|
+
`\`${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.`
|
|
38023
|
+
].join(`
|
|
38024
|
+
`));
|
|
37737
38025
|
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: "..." })\`.`);
|
|
37738
38026
|
}
|
|
37739
38027
|
if (sections.length === 0) {
|
|
37740
38028
|
return null;
|
|
37741
38029
|
}
|
|
37742
|
-
sections.unshift(
|
|
38030
|
+
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.
|
|
38031
|
+
|
|
38032
|
+
**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.`);
|
|
37743
38033
|
return `${HEADING}
|
|
37744
38034
|
|
|
37745
38035
|
${sections.join(`
|
|
@@ -37801,12 +38091,12 @@ var PLUGIN_VERSION = (() => {
|
|
|
37801
38091
|
return "0.0.0";
|
|
37802
38092
|
}
|
|
37803
38093
|
})();
|
|
37804
|
-
var ANNOUNCEMENT_VERSION = "0.
|
|
38094
|
+
var ANNOUNCEMENT_VERSION = "0.37.0";
|
|
37805
38095
|
var ANNOUNCEMENT_FEATURES = [
|
|
37806
|
-
"
|
|
37807
|
-
"
|
|
37808
|
-
"
|
|
37809
|
-
"
|
|
38096
|
+
"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.",
|
|
38097
|
+
"`aft_transform` removed: usage data showed agents never called it — `edit` covers everything it did, and every request now carries a smaller tool surface.",
|
|
38098
|
+
"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.",
|
|
38099
|
+
"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."
|
|
37810
38100
|
];
|
|
37811
38101
|
var ANNOUNCEMENT_FOOTER = "Join us on Discord: https://discord.gg/DSa65w8wuf";
|
|
37812
38102
|
var plugin = async (input) => initializePluginForDirectory(input);
|
|
@@ -38031,17 +38321,25 @@ ${lines}
|
|
|
38031
38321
|
});
|
|
38032
38322
|
rpcServer.handle("status", async (params) => {
|
|
38033
38323
|
const sessionID = params.sessionID || "rpc";
|
|
38034
|
-
const cachedDir = getSessionDirectoryCached(sessionID);
|
|
38035
|
-
const candidateDirs = new Set;
|
|
38036
|
-
candidateDirs.add(input.directory);
|
|
38037
|
-
if (typeof cachedDir === "string" && cachedDir.length > 0) {
|
|
38038
|
-
candidateDirs.add(cachedDir);
|
|
38039
|
-
}
|
|
38040
38324
|
let bridge = null;
|
|
38041
|
-
|
|
38042
|
-
|
|
38043
|
-
|
|
38044
|
-
|
|
38325
|
+
let servedDirectory = input.directory;
|
|
38326
|
+
const realSessionID = params.sessionID || "";
|
|
38327
|
+
const verifiedDir = realSessionID ? await verifySessionDirectory(input.client, realSessionID) : null;
|
|
38328
|
+
if (verifiedDir) {
|
|
38329
|
+
bridge = pool.getActiveBridgeForRoot(verifiedDir);
|
|
38330
|
+
if (bridge) {
|
|
38331
|
+
servedDirectory = verifiedDir;
|
|
38332
|
+
} else if (verifiedDir !== input.directory) {
|
|
38333
|
+
return {
|
|
38334
|
+
success: true,
|
|
38335
|
+
status: "not_initialized",
|
|
38336
|
+
message: "AFT bridge is now spawned lazily, information here will be populated after first tool call."
|
|
38337
|
+
};
|
|
38338
|
+
}
|
|
38339
|
+
}
|
|
38340
|
+
if (!bridge) {
|
|
38341
|
+
bridge = pool.getActiveBridgeForRoot(input.directory);
|
|
38342
|
+
servedDirectory = input.directory;
|
|
38045
38343
|
}
|
|
38046
38344
|
if (!bridge) {
|
|
38047
38345
|
return {
|
|
@@ -38054,13 +38352,13 @@ ${lines}
|
|
|
38054
38352
|
const cachedSessionId = cached2?.session;
|
|
38055
38353
|
const cachedId = cachedSessionId?.id;
|
|
38056
38354
|
if (cached2 !== null && cachedId === sessionID) {
|
|
38057
|
-
return { success: true, ...cached2 };
|
|
38355
|
+
return { success: true, ...cached2, served_directory: servedDirectory };
|
|
38058
38356
|
}
|
|
38059
38357
|
const response = await bridge.send("status", { session_id: sessionID });
|
|
38060
38358
|
if (response.success !== false) {
|
|
38061
38359
|
bridge.cacheStatusSnapshot(response);
|
|
38062
38360
|
}
|
|
38063
|
-
return response;
|
|
38361
|
+
return { ...response, served_directory: servedDirectory };
|
|
38064
38362
|
});
|
|
38065
38363
|
const storageDir = configOverrides.storage_dir;
|
|
38066
38364
|
rpcServer.handle("get-announcement", async () => {
|
|
@@ -38120,19 +38418,12 @@ Install: ${getManualInstallHint()}`).catch(() => {});
|
|
|
38120
38418
|
cleanupWarnings(notifyOpts).catch(() => {});
|
|
38121
38419
|
}
|
|
38122
38420
|
const surface = aftConfig.tool_surface ?? "recommended";
|
|
38123
|
-
const ALL_ONLY_TOOLS = new Set([
|
|
38124
|
-
"aft_callgraph",
|
|
38125
|
-
"aft_delete",
|
|
38126
|
-
"aft_move",
|
|
38127
|
-
"aft_transform",
|
|
38128
|
-
"aft_refactor"
|
|
38129
|
-
]);
|
|
38421
|
+
const ALL_ONLY_TOOLS = new Set(["aft_callgraph", "aft_delete", "aft_move", "aft_refactor"]);
|
|
38130
38422
|
const allTools = normalizeToolMap({
|
|
38131
38423
|
...surface !== "minimal" && (aftConfig.hoist_builtin_tools !== false ? hoistedTools(ctx) : aftPrefixedTools(ctx)),
|
|
38132
38424
|
...readingTools(ctx),
|
|
38133
38425
|
...safetyTools(ctx),
|
|
38134
38426
|
...surface !== "minimal" && importTools(ctx),
|
|
38135
|
-
...structureTools(ctx),
|
|
38136
38427
|
...navigationTools(ctx),
|
|
38137
38428
|
...surface !== "minimal" && astTools(ctx),
|
|
38138
38429
|
...surface !== "minimal" && aftConfig.semantic_search === true && semanticTools(ctx),
|
|
@@ -38179,7 +38470,16 @@ Install: ${getManualInstallHint()}`).catch(() => {});
|
|
|
38179
38470
|
"bash_status"
|
|
38180
38471
|
];
|
|
38181
38472
|
const registeredTools = new Set(Object.keys(allTools));
|
|
38182
|
-
|
|
38473
|
+
const aftSearchRegistered = registeredTools.has("aft_search");
|
|
38474
|
+
pool.setConfigureOverride("aft_search_registered", aftSearchRegistered);
|
|
38475
|
+
ctx.aftSearchRegistered = aftSearchRegistered;
|
|
38476
|
+
for (const name of ["bash", "aft_bash"]) {
|
|
38477
|
+
const def = allTools[name];
|
|
38478
|
+
if (def) {
|
|
38479
|
+
const bashCfg = resolveBashConfig(aftConfig);
|
|
38480
|
+
def.description = bashToolDescription(aftSearchRegistered, bashCfg.compress, bashCfg.background);
|
|
38481
|
+
}
|
|
38482
|
+
}
|
|
38183
38483
|
const hintsAbsentTools = new Set;
|
|
38184
38484
|
for (const name of HINTS_TOOL_NAMES) {
|
|
38185
38485
|
if (!registeredTools.has(name))
|
|
@@ -38237,6 +38537,7 @@ Install: ${getManualInstallHint()}`).catch(() => {});
|
|
|
38237
38537
|
"chat.message": async (messageInput) => {
|
|
38238
38538
|
const sid = messageInput.sessionID ?? messageInput.sessionId ?? messageInput.id;
|
|
38239
38539
|
warmSessionDirectory(input.client, sid, input.directory);
|
|
38540
|
+
signalSyncWatchAbort(sid);
|
|
38240
38541
|
},
|
|
38241
38542
|
"tool.execute.before": async (toolInput) => {
|
|
38242
38543
|
if (toolInput.sessionID)
|