@cortexkit/aft-opencode 0.58.1 → 0.58.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/config-live-reload.d.ts +8 -0
- package/dist/config-live-reload.d.ts.map +1 -1
- package/dist/config.d.ts +13 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/entry/server.js +1603 -839
- package/dist/hashline-tag-sources.d.ts +18 -0
- package/dist/hashline-tag-sources.d.ts.map +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +980 -605
- package/dist/lsp-auto-install.d.ts +30 -0
- package/dist/lsp-auto-install.d.ts.map +1 -1
- package/dist/lsp-github-table.d.ts +4 -3
- package/dist/lsp-github-table.d.ts.map +1 -1
- package/dist/lsp-npm-table.d.ts +10 -0
- package/dist/lsp-npm-table.d.ts.map +1 -1
- package/dist/lsp-registry-probe.d.ts +8 -4
- package/dist/lsp-registry-probe.d.ts.map +1 -1
- package/dist/permissions/v2-service.d.ts +110 -5
- package/dist/permissions/v2-service.d.ts.map +1 -1
- package/dist/rpc/register.d.ts +0 -4
- package/dist/rpc/register.d.ts.map +1 -1
- package/dist/shared/session-directory.d.ts +7 -0
- package/dist/shared/session-directory.d.ts.map +1 -1
- package/dist/shared/subagent-detect.d.ts +11 -4
- package/dist/shared/subagent-detect.d.ts.map +1 -1
- package/dist/tool-registration.d.ts +8 -5
- package/dist/tool-registration.d.ts.map +1 -1
- package/dist/tools/_shared.d.ts +16 -15
- package/dist/tools/_shared.d.ts.map +1 -1
- package/dist/tools/bash.d.ts +24 -1
- package/dist/tools/bash.d.ts.map +1 -1
- package/dist/tools/bash_watch.d.ts +4 -4
- package/dist/tools/bash_watch.d.ts.map +1 -1
- package/dist/tools/bash_write.d.ts.map +1 -1
- package/dist/tools/definitions/v2.d.ts +2 -0
- package/dist/tools/definitions/v2.d.ts.map +1 -1
- package/dist/tools/hoisted/v2.d.ts +8 -1
- package/dist/tools/hoisted/v2.d.ts.map +1 -1
- package/dist/tools/hoisted.d.ts +9 -7
- package/dist/tools/hoisted.d.ts.map +1 -1
- package/dist/tools/reading.d.ts +0 -3
- package/dist/tools/reading.d.ts.map +1 -1
- package/dist/v2-prompt-detach.d.ts +69 -0
- package/dist/v2-prompt-detach.d.ts.map +1 -0
- package/dist/wakes/runtime-consumer.d.ts +2 -0
- package/dist/wakes/runtime-consumer.d.ts.map +1 -1
- package/dist/wakes/session-delivery.d.ts +9 -0
- package/dist/wakes/session-delivery.d.ts.map +1 -1
- package/dist/workflow-hints.d.ts +20 -7
- package/dist/workflow-hints.d.ts.map +1 -1
- package/package.json +8 -8
- package/src/entry/server-runtime.mjs +21 -5
- package/src/shared/session-directory.ts +92 -0
- package/src/shared/subagent-detect.ts +89 -28
package/dist/entry/server.js
CHANGED
|
@@ -9574,29 +9574,84 @@ function isTerminalStatus(status) {
|
|
|
9574
9574
|
function sleep(ms) {
|
|
9575
9575
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
9576
9576
|
}
|
|
9577
|
-
function monotonicNowMs() {
|
|
9578
|
-
return performance.now();
|
|
9579
|
-
}
|
|
9580
9577
|
// ../aft-bridge/dist/bash-hints.js
|
|
9581
9578
|
import * as fs from "node:fs";
|
|
9582
9579
|
import * as os from "node:os";
|
|
9583
9580
|
import * as path from "node:path";
|
|
9584
9581
|
var DEFAULT_PRIMARY_WATCH_TIMEOUT_MS = 30000;
|
|
9585
|
-
var
|
|
9582
|
+
var MAX_WATCH_TIMEOUT_MS = 1800000;
|
|
9583
|
+
var LONGEST_TIMER_DELAY_MS = 2147483647;
|
|
9584
|
+
var WATCH_SYNC_DEFAULTS_DESCRIPTION = "Sync waits default to 30s in a main session (max `bash.watch_sync_max_ms`, 120s by default); in a delegated session a sync wait without a timeout waits until the command finishes";
|
|
9585
|
+
var WATCH_TIMEOUT_PARAM_DESCRIPTION = "Sync-only timeout in milliseconds. In a main session: default 30000, max `bash.watch_sync_max_ms` (120000 by default). In a delegated session: omit it to wait until the command finishes; a value you pass is used as given.";
|
|
9586
9586
|
function resolveWatchTimeoutMs(requestedMs, role, capMs) {
|
|
9587
|
-
|
|
9588
|
-
|
|
9587
|
+
if (role === "worker")
|
|
9588
|
+
return requestedMs;
|
|
9589
|
+
return Math.min(requestedMs ?? DEFAULT_PRIMARY_WATCH_TIMEOUT_MS, capMs);
|
|
9590
|
+
}
|
|
9591
|
+
function maxWatchTimeoutMs(role, capMs) {
|
|
9592
|
+
return role === "worker" ? MAX_WATCH_TIMEOUT_MS : capMs;
|
|
9593
|
+
}
|
|
9594
|
+
function watchPollDelayMs(elapsedMs) {
|
|
9595
|
+
if (elapsedMs < 5000)
|
|
9596
|
+
return 100;
|
|
9597
|
+
if (elapsedMs < 30000)
|
|
9598
|
+
return 250;
|
|
9599
|
+
if (elapsedMs < 120000)
|
|
9600
|
+
return 500;
|
|
9601
|
+
return 1000;
|
|
9602
|
+
}
|
|
9603
|
+
var watchClock = {
|
|
9604
|
+
now: () => performance.now(),
|
|
9605
|
+
sleep: abortableSleep
|
|
9606
|
+
};
|
|
9607
|
+
function abortableSleep(ms, signal) {
|
|
9608
|
+
if (signal?.aborted)
|
|
9609
|
+
return Promise.resolve();
|
|
9610
|
+
return new Promise((resolve) => {
|
|
9611
|
+
const done = () => {
|
|
9612
|
+
clearTimeout(timer);
|
|
9613
|
+
signal?.removeEventListener("abort", done);
|
|
9614
|
+
resolve();
|
|
9615
|
+
};
|
|
9616
|
+
const timer = setTimeout(done, ms);
|
|
9617
|
+
signal?.addEventListener("abort", done, { once: true });
|
|
9618
|
+
});
|
|
9589
9619
|
}
|
|
9590
9620
|
function formatWatchWaited(elapsedMs, limitMs, capMs) {
|
|
9591
|
-
|
|
9621
|
+
let limit;
|
|
9622
|
+
if (limitMs === undefined)
|
|
9623
|
+
limit = "no limit: waits until the command finishes";
|
|
9624
|
+
else if (capMs !== undefined && limitMs >= capMs)
|
|
9625
|
+
limit = `limit ${limitMs}ms, the bash.watch_sync_max_ms cap`;
|
|
9626
|
+
else
|
|
9627
|
+
limit = `limit ${limitMs}ms`;
|
|
9592
9628
|
return `Waited ${Math.round(elapsedMs)}ms (${limit})`;
|
|
9593
9629
|
}
|
|
9594
|
-
function watchTimeoutSteer(role,
|
|
9630
|
+
function watchTimeoutSteer(role, timeoutParam = "timeoutMs") {
|
|
9595
9631
|
if (role === "worker") {
|
|
9596
|
-
return `The command is still running; this is not a failure. Watch again
|
|
9632
|
+
return `The command is still running; this is not a failure. Watch again and don't report a result until it finishes; a bash_watch without ${timeoutParam} waits until the command finishes.`;
|
|
9597
9633
|
}
|
|
9598
9634
|
return "The command is still running; this is not a failure. Watch again, do other work, or end your turn: the completion reminder wakes you.";
|
|
9599
9635
|
}
|
|
9636
|
+
var WATCH_UNAVAILABLE_GIVE_UP_MS = 120000;
|
|
9637
|
+
var WORKER_KEEP_WAITING = "Call bash_watch again to keep waiting; don't report a result until the command finishes.";
|
|
9638
|
+
function interruptedWatchTail(role, primaryTail) {
|
|
9639
|
+
return role === "worker" ? `The task is still running. ${WORKER_KEEP_WAITING}` : primaryTail;
|
|
9640
|
+
}
|
|
9641
|
+
function workerBackgroundTaskNote(taskId, taskIdArg = "taskId") {
|
|
9642
|
+
return `
|
|
9643
|
+
|
|
9644
|
+
NOTE (subagent session): Continue with other work if you have it. If you don't, call bash_watch({ ${taskIdArg}: "${taskId}" }) to wait for completion before returning to the parent; without a timeout it waits until the command finishes. Subagents don't survive turn-end and won't be woken when the command finishes.`;
|
|
9645
|
+
}
|
|
9646
|
+
function runningTaskStatusHint(role) {
|
|
9647
|
+
return role === "worker" ? "To wait for it, call bash_watch; don't poll." : "A completion reminder will be delivered automatically; don't poll.";
|
|
9648
|
+
}
|
|
9649
|
+
function watchUnavailableSteer(role) {
|
|
9650
|
+
const unknown = "the bridge was busy, so task state is unknown.";
|
|
9651
|
+
if (role === "worker")
|
|
9652
|
+
return `${unknown} ${WORKER_KEEP_WAITING}`;
|
|
9653
|
+
return `${unknown} Do not poll; let the task's completion notification wake the session, or use one bash_status snapshot on the next normal tool call.`;
|
|
9654
|
+
}
|
|
9600
9655
|
var CONFLICT_HINT = `
|
|
9601
9656
|
|
|
9602
9657
|
[Hint] Use aft_conflicts to see all conflict regions across files in a single call.`;
|
|
@@ -10455,6 +10510,9 @@ function isPassiveCommand(command) {
|
|
|
10455
10510
|
return PASSIVE_COMMANDS.has(command);
|
|
10456
10511
|
}
|
|
10457
10512
|
|
|
10513
|
+
// ../aft-bridge/dist/transport.js
|
|
10514
|
+
var WORKER_SESSION_FIELD = "worker_session";
|
|
10515
|
+
|
|
10458
10516
|
// ../aft-bridge/dist/bridge.js
|
|
10459
10517
|
var DEFAULT_BRIDGE_TIMEOUT_MS = 30000;
|
|
10460
10518
|
var BRIDGE_HANG_TIMEOUT_THRESHOLD = 2;
|
|
@@ -10873,9 +10931,11 @@ class BinaryBridge {
|
|
|
10873
10931
|
params.edit_slot_survives = editSlotSurvives;
|
|
10874
10932
|
this.logHashlineRegistrationCarrier("tool_call", sessionId, editSlotSurvives);
|
|
10875
10933
|
}
|
|
10876
|
-
const { preview, ...sendOptions } = options ?? {};
|
|
10934
|
+
const { preview, workerSession, ...sendOptions } = options ?? {};
|
|
10877
10935
|
if (preview === true)
|
|
10878
10936
|
params.preview = true;
|
|
10937
|
+
if (workerSession === true)
|
|
10938
|
+
params[WORKER_SESSION_FIELD] = true;
|
|
10879
10939
|
return await this.send("tool_call", params, Object.keys(sendOptions).length > 0 ? sendOptions : undefined);
|
|
10880
10940
|
}
|
|
10881
10941
|
async sendWithVersionMismatchRetry(command, params, options, canRetryAfterVersionSwap) {
|
|
@@ -10944,7 +11004,7 @@ class BinaryBridge {
|
|
|
10944
11004
|
if (command === "cancel_request" || Object.hasOwn(params, "command") || Object.hasOwn(params, "method")) {
|
|
10945
11005
|
const nested = { ...params };
|
|
10946
11006
|
const reserved = {};
|
|
10947
|
-
for (const key of ["session_id", "lsp_hints"]) {
|
|
11007
|
+
for (const key of ["session_id", "lsp_hints", WORKER_SESSION_FIELD]) {
|
|
10948
11008
|
if (Object.hasOwn(nested, key)) {
|
|
10949
11009
|
reserved[key] = nested[key];
|
|
10950
11010
|
delete nested[key];
|
|
@@ -11722,6 +11782,35 @@ function coerceJsonCollectionParam(value, field) {
|
|
|
11722
11782
|
}
|
|
11723
11783
|
throw new Error(`${field} must be ${shape} (got an invalid type)`);
|
|
11724
11784
|
}
|
|
11785
|
+
function isBlankParam(value) {
|
|
11786
|
+
if (typeof value === "string")
|
|
11787
|
+
return value.trim().length === 0;
|
|
11788
|
+
return isEmptyParam(value);
|
|
11789
|
+
}
|
|
11790
|
+
function usableZoomTargets(targets) {
|
|
11791
|
+
if (isEmptyParam(targets))
|
|
11792
|
+
return [];
|
|
11793
|
+
const entries = Array.isArray(targets) ? targets : [targets];
|
|
11794
|
+
return entries.filter((entry) => {
|
|
11795
|
+
if (entry === null || entry === undefined)
|
|
11796
|
+
return false;
|
|
11797
|
+
if (typeof entry !== "object")
|
|
11798
|
+
return true;
|
|
11799
|
+
const record = entry;
|
|
11800
|
+
return !(isBlankParam(record.path) && isBlankParam(record.filePath) && isBlankParam(record.symbol));
|
|
11801
|
+
});
|
|
11802
|
+
}
|
|
11803
|
+
function isFindReplaceOnlyEdit(args) {
|
|
11804
|
+
const absent = (value) => value === undefined || value === null || value === "";
|
|
11805
|
+
const absentLine = (value) => absent(value) || value === 0;
|
|
11806
|
+
const edits = args.edits;
|
|
11807
|
+
return Array.isArray(edits) && edits.length > 0 && absent(args.appendContent) && absent(args.symbol) && absent(args.content) && edits.every((entry) => {
|
|
11808
|
+
if (entry === null || typeof entry !== "object")
|
|
11809
|
+
return false;
|
|
11810
|
+
const item = entry;
|
|
11811
|
+
return typeof item.oldString === "string" && (absent(item.newString) || typeof item.newString === "string") && absentLine(item.startLine) && absentLine(item.endLine) && absent(item.content);
|
|
11812
|
+
});
|
|
11813
|
+
}
|
|
11725
11814
|
// ../aft-bridge/dist/config-error.js
|
|
11726
11815
|
import { homedir as homedir5 } from "node:os";
|
|
11727
11816
|
import { isAbsolute as isAbsolute2, join as join6 } from "node:path";
|
|
@@ -14984,6 +15073,13 @@ class SubcTransportShuttingDownError extends SubcCallError {
|
|
|
14984
15073
|
this.name = "SubcTransportShuttingDownError";
|
|
14985
15074
|
}
|
|
14986
15075
|
}
|
|
15076
|
+
|
|
15077
|
+
class ReconnectKeyChangedError extends Error {
|
|
15078
|
+
constructor() {
|
|
15079
|
+
super("the subc daemon restarted and its connection key changed; the reconnect did not complete within the deadline, retry");
|
|
15080
|
+
this.name = "ReconnectKeyChangedError";
|
|
15081
|
+
}
|
|
15082
|
+
}
|
|
14987
15083
|
function isSubcClientClosedError(error) {
|
|
14988
15084
|
return error instanceof SubcError && error.message === "client closed";
|
|
14989
15085
|
}
|
|
@@ -15486,6 +15582,8 @@ class SubcTransport {
|
|
|
15486
15582
|
body.edit_slot_survives = editSlotSurvives;
|
|
15487
15583
|
if (preview === true)
|
|
15488
15584
|
body.preview = true;
|
|
15585
|
+
if (options?.workerSession === true)
|
|
15586
|
+
body[WORKER_SESSION_FIELD] = true;
|
|
15489
15587
|
const reply = await this.pool.routeRequest(this.identityFor(sessionId), body, timeoutMs, onProgress, this.generation, abortSignal);
|
|
15490
15588
|
return reliftReply(reply);
|
|
15491
15589
|
}
|
|
@@ -15496,7 +15594,10 @@ class SubcTransport {
|
|
|
15496
15594
|
}
|
|
15497
15595
|
const { timeoutMs, onProgress, abortSignal } = this.splitOptions(options);
|
|
15498
15596
|
const session = typeof params.session_id === "string" ? params.session_id : undefined;
|
|
15499
|
-
const
|
|
15597
|
+
const { [WORKER_SESSION_FIELD]: workerSession, ...args } = params;
|
|
15598
|
+
const body = { name: command, arguments: args };
|
|
15599
|
+
if (workerSession === true)
|
|
15600
|
+
body[WORKER_SESSION_FIELD] = true;
|
|
15500
15601
|
const editSlotSurvives = this.pool.getEditSlotSurvives();
|
|
15501
15602
|
if (editSlotSurvives !== undefined)
|
|
15502
15603
|
body.edit_slot_survives = editSlotSurvives;
|
|
@@ -15533,6 +15634,7 @@ class SubcTransportPool {
|
|
|
15533
15634
|
lifecycleRegistration = null;
|
|
15534
15635
|
outerFacadeEvictor;
|
|
15535
15636
|
client = null;
|
|
15637
|
+
hadWorkingClient = false;
|
|
15536
15638
|
connecting = null;
|
|
15537
15639
|
routeReopenRetryDelayMs = RECONNECT_RETRY_FLOOR_MS;
|
|
15538
15640
|
routeReopenRetry = null;
|
|
@@ -15977,6 +16079,10 @@ class SubcTransportPool {
|
|
|
15977
16079
|
}
|
|
15978
16080
|
async routeRequest(identity, body, timeoutMs, onProgress, expectedGeneration, abortSignal) {
|
|
15979
16081
|
const callStartedAt = performance.now();
|
|
16082
|
+
const callDeadlineMs = timeoutMs ?? SUBC_DEFAULT_REQUEST_TIMEOUT_MS;
|
|
16083
|
+
let scheduledDelayMs = 0;
|
|
16084
|
+
const spentMs = () => Math.max(performance.now() - callStartedAt, scheduledDelayMs);
|
|
16085
|
+
const remainingMs = () => callDeadlineMs - spentMs();
|
|
15980
16086
|
const root = asCanonicalRootPath(identity.project_root);
|
|
15981
16087
|
let generation = expectedGeneration;
|
|
15982
16088
|
if (this.lifecycleEnabled()) {
|
|
@@ -15992,7 +16098,49 @@ class SubcTransportPool {
|
|
|
15992
16098
|
try {
|
|
15993
16099
|
let client;
|
|
15994
16100
|
try {
|
|
15995
|
-
|
|
16101
|
+
let keyChanged;
|
|
16102
|
+
while (true) {
|
|
16103
|
+
const remaining = remainingMs();
|
|
16104
|
+
if (remaining <= 0)
|
|
16105
|
+
throw keyChanged ?? callDeadlinePassedError(undefined, callDeadlineMs);
|
|
16106
|
+
let timer;
|
|
16107
|
+
try {
|
|
16108
|
+
const acquired = await Promise.race([
|
|
16109
|
+
this.ensureClient(),
|
|
16110
|
+
new Promise((resolve) => {
|
|
16111
|
+
timer = setTimeout(() => resolve(CALL_DEADLINE_EXPIRED), remaining);
|
|
16112
|
+
})
|
|
16113
|
+
]);
|
|
16114
|
+
if (acquired === CALL_DEADLINE_EXPIRED) {
|
|
16115
|
+
throw keyChanged ?? callDeadlinePassedError(undefined, callDeadlineMs);
|
|
16116
|
+
}
|
|
16117
|
+
client = acquired;
|
|
16118
|
+
break;
|
|
16119
|
+
} catch (error) {
|
|
16120
|
+
if (!(error instanceof ReconnectKeyChangedError))
|
|
16121
|
+
throw error;
|
|
16122
|
+
keyChanged = error;
|
|
16123
|
+
if (spentMs() + this.nextRouteReopenDelayMs() >= callDeadlineMs)
|
|
16124
|
+
throw error;
|
|
16125
|
+
} finally {
|
|
16126
|
+
clearTimeout(timer);
|
|
16127
|
+
}
|
|
16128
|
+
const { delayMs, wait } = this.waitForRouteReopenBackoff();
|
|
16129
|
+
scheduledDelayMs = spentMs() + delayMs;
|
|
16130
|
+
let retryTimer;
|
|
16131
|
+
try {
|
|
16132
|
+
const waited = await Promise.race([
|
|
16133
|
+
wait,
|
|
16134
|
+
new Promise((resolve) => {
|
|
16135
|
+
retryTimer = setTimeout(() => resolve(CALL_DEADLINE_EXPIRED), callDeadlineMs - (performance.now() - callStartedAt));
|
|
16136
|
+
})
|
|
16137
|
+
]);
|
|
16138
|
+
if (waited === CALL_DEADLINE_EXPIRED)
|
|
16139
|
+
throw keyChanged;
|
|
16140
|
+
} finally {
|
|
16141
|
+
clearTimeout(retryTimer);
|
|
16142
|
+
}
|
|
16143
|
+
}
|
|
15996
16144
|
this.assertRecordLive(record);
|
|
15997
16145
|
} catch (error) {
|
|
15998
16146
|
throw this.annotateReapError(error, record);
|
|
@@ -16027,12 +16175,8 @@ class SubcTransportPool {
|
|
|
16027
16175
|
throw error;
|
|
16028
16176
|
}
|
|
16029
16177
|
};
|
|
16030
|
-
const callDeadlineMs = timeoutMs ?? SUBC_DEFAULT_REQUEST_TIMEOUT_MS;
|
|
16031
16178
|
const reloadWaitBudgetMs = Math.min(callDeadlineMs, ROUTE_OPEN_RELOAD_WAIT_CEILING_MS);
|
|
16032
|
-
let scheduledDelayMs = 0;
|
|
16033
16179
|
let lastRefusal;
|
|
16034
|
-
const spentMs = () => Math.max(performance.now() - callStartedAt, scheduledDelayMs);
|
|
16035
|
-
const remainingMs = () => callDeadlineMs - spentMs();
|
|
16036
16180
|
const openRouteWithinDeadline = async () => {
|
|
16037
16181
|
const remaining = remainingMs();
|
|
16038
16182
|
if (remaining <= 0)
|
|
@@ -16235,10 +16379,14 @@ class SubcTransportPool {
|
|
|
16235
16379
|
throw new SubcTransportShuttingDownError;
|
|
16236
16380
|
}
|
|
16237
16381
|
this.client = client;
|
|
16382
|
+
this.hadWorkingClient = true;
|
|
16238
16383
|
this.transportFailures = 0;
|
|
16239
16384
|
return client;
|
|
16240
16385
|
}).catch((error) => {
|
|
16241
16386
|
this.connecting = null;
|
|
16387
|
+
if (this.hadWorkingClient && error instanceof AuthError) {
|
|
16388
|
+
throw new ReconnectKeyChangedError;
|
|
16389
|
+
}
|
|
16242
16390
|
throw error;
|
|
16243
16391
|
});
|
|
16244
16392
|
return this.connecting;
|
|
@@ -17930,6 +18078,7 @@ async function acquireBridge(locationDirectory, options, dependencies = {}) {
|
|
|
17930
18078
|
let owner = state.standalone;
|
|
17931
18079
|
if (!owner) {
|
|
17932
18080
|
const configByRoot = new Map;
|
|
18081
|
+
const versionMismatchHandlers = new Map;
|
|
17933
18082
|
const configuredLoader = options.poolOptions.projectConfigLoader;
|
|
17934
18083
|
const pool = await createPool({
|
|
17935
18084
|
...options,
|
|
@@ -17939,21 +18088,29 @@ async function acquireBridge(locationDirectory, options, dependencies = {}) {
|
|
|
17939
18088
|
projectConfigLoader: (projectRoot) => ({
|
|
17940
18089
|
...configuredLoader?.(projectRoot) ?? {},
|
|
17941
18090
|
...configByRoot.get(canonicalizeProjectRoot(projectRoot)) ?? {}
|
|
17942
|
-
})
|
|
18091
|
+
}),
|
|
18092
|
+
onVersionMismatch: (binaryVersion, minVersion) => [...versionMismatchHandlers.values()].at(-1)?.(binaryVersion, minVersion) ?? null
|
|
17943
18093
|
}
|
|
17944
18094
|
});
|
|
17945
|
-
owner = {
|
|
18095
|
+
owner = {
|
|
18096
|
+
pool,
|
|
18097
|
+
configByRoot,
|
|
18098
|
+
versionMismatchHandlers,
|
|
18099
|
+
referencesByRoot: new Map,
|
|
18100
|
+
references: 0
|
|
18101
|
+
};
|
|
17946
18102
|
state.standalone = owner;
|
|
17947
18103
|
}
|
|
17948
18104
|
owner.references += 1;
|
|
17949
18105
|
owner.referencesByRoot.set(directory, (owner.referencesByRoot.get(directory) ?? 0) + 1);
|
|
17950
18106
|
owner.configByRoot.set(directory, { ...options.configOverrides });
|
|
17951
18107
|
const acquiredOwner = owner;
|
|
17952
|
-
|
|
18108
|
+
const lease = new LocationBridgeLease(directory, mode, owner.pool, async () => {
|
|
17953
18109
|
await withLock(directory, () => withLock(OWNER_LOCK, async () => {
|
|
17954
18110
|
const current = lifecycleState().standalone;
|
|
17955
18111
|
if (current !== acquiredOwner)
|
|
17956
18112
|
return;
|
|
18113
|
+
current.versionMismatchHandlers?.delete(lease);
|
|
17957
18114
|
current.references -= 1;
|
|
17958
18115
|
const rootReferences = (current.referencesByRoot.get(directory) ?? 1) - 1;
|
|
17959
18116
|
if (rootReferences > 0)
|
|
@@ -17968,6 +18125,10 @@ async function acquireBridge(locationDirectory, options, dependencies = {}) {
|
|
|
17968
18125
|
await current.pool.shutdown();
|
|
17969
18126
|
}));
|
|
17970
18127
|
});
|
|
18128
|
+
const onVersionMismatch = options.poolOptions.onVersionMismatch;
|
|
18129
|
+
if (onVersionMismatch)
|
|
18130
|
+
owner.versionMismatchHandlers?.set(lease, onVersionMismatch);
|
|
18131
|
+
return lease;
|
|
17971
18132
|
});
|
|
17972
18133
|
});
|
|
17973
18134
|
}
|
|
@@ -17988,6 +18149,14 @@ var STANDALONE_DETACH_KEYWORDS_PATTERN = new RegExp(STANDALONE_DETACH_KEYWORD_SO
|
|
|
17988
18149
|
function containsStandaloneDetachKeyword(messageText) {
|
|
17989
18150
|
return STANDALONE_DETACH_KEYWORD_PATTERN.test(messageText);
|
|
17990
18151
|
}
|
|
18152
|
+
function standaloneDetachKeywordRanges(messageText) {
|
|
18153
|
+
const ranges = [];
|
|
18154
|
+
for (const match of messageText.matchAll(STANDALONE_DETACH_KEYWORDS_PATTERN)) {
|
|
18155
|
+
const start = (match.index ?? 0) + (match[1]?.length ?? 0);
|
|
18156
|
+
ranges.push([start, start + BASH_WAIT_DETACH_MAGIC_KEYWORD.length]);
|
|
18157
|
+
}
|
|
18158
|
+
return ranges;
|
|
18159
|
+
}
|
|
17991
18160
|
function stripStandaloneDetachKeywords(messageText) {
|
|
17992
18161
|
return messageText.replace(STANDALONE_DETACH_KEYWORDS_PATTERN, "$1");
|
|
17993
18162
|
}
|
|
@@ -19379,31 +19548,36 @@ var ORT_PLATFORM_MAP = {
|
|
|
19379
19548
|
arm64: {
|
|
19380
19549
|
assetName: `onnxruntime-osx-arm64-${ORT_VERSION}`,
|
|
19381
19550
|
libName: "libonnxruntime.dylib",
|
|
19382
|
-
archiveType: "tgz"
|
|
19551
|
+
archiveType: "tgz",
|
|
19552
|
+
librarySha256: "872533f130f1839a5bc01788ddb4f75c83a189763441ba1178788ed965449289"
|
|
19383
19553
|
}
|
|
19384
19554
|
},
|
|
19385
19555
|
linux: {
|
|
19386
19556
|
x64: {
|
|
19387
19557
|
assetName: `onnxruntime-linux-x64-${ORT_VERSION}`,
|
|
19388
19558
|
libName: "libonnxruntime.so",
|
|
19389
|
-
archiveType: "tgz"
|
|
19559
|
+
archiveType: "tgz",
|
|
19560
|
+
librarySha256: "d132535d051344ff5c64c9c200004150559049a81ed330eb4422c1962fb6b7e4"
|
|
19390
19561
|
},
|
|
19391
19562
|
arm64: {
|
|
19392
19563
|
assetName: `onnxruntime-linux-aarch64-${ORT_VERSION}`,
|
|
19393
19564
|
libName: "libonnxruntime.so",
|
|
19394
|
-
archiveType: "tgz"
|
|
19565
|
+
archiveType: "tgz",
|
|
19566
|
+
librarySha256: "52b2a0e75e79468404284fec38ad5ee1a7a996232274f5e1b84f3e793fb07554"
|
|
19395
19567
|
}
|
|
19396
19568
|
},
|
|
19397
19569
|
win32: {
|
|
19398
19570
|
x64: {
|
|
19399
19571
|
assetName: `onnxruntime-win-x64-${ORT_VERSION}`,
|
|
19400
19572
|
libName: "onnxruntime.dll",
|
|
19401
|
-
archiveType: "zip"
|
|
19573
|
+
archiveType: "zip",
|
|
19574
|
+
librarySha256: "b95efb2113b603bbbf3f191061c5516a871ed546893c820e4f3b7b6c358dbf2a"
|
|
19402
19575
|
},
|
|
19403
19576
|
arm64: {
|
|
19404
19577
|
assetName: `onnxruntime-win-arm64-${ORT_VERSION}`,
|
|
19405
19578
|
libName: "onnxruntime.dll",
|
|
19406
|
-
archiveType: "zip"
|
|
19579
|
+
archiveType: "zip",
|
|
19580
|
+
librarySha256: "724d81ac50b11bfaa01ab3ce01b99fb6734a4b762259a8be4395ca662ce99fe6"
|
|
19407
19581
|
}
|
|
19408
19582
|
}
|
|
19409
19583
|
};
|
|
@@ -19893,6 +20067,9 @@ async function downloadOnnxRuntime(info, targetDir) {
|
|
|
19893
20067
|
copyOnnxLibraries(info, extractedDir, stagedInstallDir, realFiles, symlinks);
|
|
19894
20068
|
const libPath = join14(stagedInstallDir, info.libName);
|
|
19895
20069
|
const libHash = sha256File2(libPath);
|
|
20070
|
+
if (libHash !== info.librarySha256) {
|
|
20071
|
+
throw new Error(`onnx runtime library hash mismatch at ${libPath}: sha256 ${libHash}, ` + `expected the pinned ${info.librarySha256} for ${info.assetName}`);
|
|
20072
|
+
}
|
|
19896
20073
|
writeOnnxInstalledMeta(stagedInstallDir, ORT_VERSION, libHash, archiveSha256);
|
|
19897
20074
|
publishOnnxRuntime(stagedInstallDir, targetDir);
|
|
19898
20075
|
rmSync3(tmpDir, { recursive: true, force: true });
|
|
@@ -20235,12 +20412,13 @@ function normalizeZoomTargets(record) {
|
|
|
20235
20412
|
throw new InvalidRequestError(`'targets[${index}].path' must be a non-empty string`);
|
|
20236
20413
|
}
|
|
20237
20414
|
const source = target;
|
|
20238
|
-
const
|
|
20415
|
+
const blank = (value) => value === undefined || value === null || typeof value === "string" && !value.trim();
|
|
20416
|
+
const emptyTarget = blank(source.symbol) && blank(source.path) && blank(source.filePath);
|
|
20239
20417
|
if (emptyTarget)
|
|
20240
20418
|
return { ...source };
|
|
20241
20419
|
const normalized = { ...source };
|
|
20242
20420
|
try {
|
|
20243
|
-
normalizeAliasPair(normalized, "path", "filePath",
|
|
20421
|
+
normalizeAliasPair(normalized, "path", "filePath", false);
|
|
20244
20422
|
} catch (error) {
|
|
20245
20423
|
if (error instanceof InvalidRequestError) {
|
|
20246
20424
|
throw new InvalidRequestError(error.message.replace("'filePath'", `'targets[${index}].filePath'`).replace("'path'", `'targets[${index}].path'`));
|
|
@@ -25686,6 +25864,10 @@ var SubcConfigSchema = object({
|
|
|
25686
25864
|
connection_file: string2().optional(),
|
|
25687
25865
|
client_reaper: boolean2().optional()
|
|
25688
25866
|
});
|
|
25867
|
+
var OpenCodeHostConfigSchema = object({
|
|
25868
|
+
server_url: string2().optional(),
|
|
25869
|
+
server_password_env: string2().optional()
|
|
25870
|
+
});
|
|
25689
25871
|
var GhShimConfigSchema = object({
|
|
25690
25872
|
binary_path: string2().trim().refine(isAbsolute7, "gh_shim.binary_path must be absolute").optional()
|
|
25691
25873
|
});
|
|
@@ -25775,6 +25957,7 @@ var AftConfigFieldsSchema = object({
|
|
|
25775
25957
|
auto_update: boolean2().optional(),
|
|
25776
25958
|
bridge: BridgeConfigSchema.optional(),
|
|
25777
25959
|
subc: SubcConfigSchema.optional(),
|
|
25960
|
+
opencode: OpenCodeHostConfigSchema.optional(),
|
|
25778
25961
|
github: GithubConfigSchema.optional(),
|
|
25779
25962
|
gh_shim: GhShimConfigSchema.optional(),
|
|
25780
25963
|
git: GitConfigSchema.optional(),
|
|
@@ -26364,6 +26547,8 @@ function getStrippedTopLevelKeys(override) {
|
|
|
26364
26547
|
stripped.push("sandbox.write_allow");
|
|
26365
26548
|
if (override.subc !== undefined)
|
|
26366
26549
|
stripped.push("subc");
|
|
26550
|
+
if (override.opencode !== undefined)
|
|
26551
|
+
stripped.push("opencode");
|
|
26367
26552
|
if (override.github !== undefined)
|
|
26368
26553
|
stripped.push("github");
|
|
26369
26554
|
if (override.gh_shim !== undefined)
|
|
@@ -40223,6 +40408,14 @@ function formatDuration(completion) {
|
|
|
40223
40408
|
}
|
|
40224
40409
|
|
|
40225
40410
|
// src/shared/session-directory.ts
|
|
40411
|
+
import { Effect } from "effect";
|
|
40412
|
+
function isV2PluginContext(client) {
|
|
40413
|
+
if (!client || typeof client !== "object")
|
|
40414
|
+
return false;
|
|
40415
|
+
const candidate = client;
|
|
40416
|
+
return typeof candidate.location?.directory === "string" && typeof candidate.session?.get === "function";
|
|
40417
|
+
}
|
|
40418
|
+
var V2_SESSION_LOOKUP_TIMEOUT_MS = 3000;
|
|
40226
40419
|
var CACHE_MAX_ENTRIES = 200;
|
|
40227
40420
|
var cache = new Map;
|
|
40228
40421
|
async function getSessionDirectory(client, sessionId, fallbackDirectory) {
|
|
@@ -40234,6 +40427,8 @@ async function getSessionDirectory(client, sessionId, fallbackDirectory) {
|
|
|
40234
40427
|
cache.set(sessionId, cached);
|
|
40235
40428
|
return cached.directory;
|
|
40236
40429
|
}
|
|
40430
|
+
if (isV2PluginContext(client))
|
|
40431
|
+
return lookupV2SessionDirectory(client, sessionId);
|
|
40237
40432
|
const c = client;
|
|
40238
40433
|
const sessionApi = c?.session;
|
|
40239
40434
|
if (!sessionApi || typeof sessionApi.get !== "function") {
|
|
@@ -40254,6 +40449,25 @@ async function getSessionDirectory(client, sessionId, fallbackDirectory) {
|
|
|
40254
40449
|
setCache(sessionId, dir);
|
|
40255
40450
|
return dir;
|
|
40256
40451
|
}
|
|
40452
|
+
async function lookupV2SessionDirectory(context, sessionId) {
|
|
40453
|
+
let dir = null;
|
|
40454
|
+
let failure;
|
|
40455
|
+
try {
|
|
40456
|
+
const session = await Effect.runPromise(Effect.timeout(context.session.get({ sessionID: sessionId }), V2_SESSION_LOOKUP_TIMEOUT_MS));
|
|
40457
|
+
const directory = session?.location?.directory;
|
|
40458
|
+
if (typeof directory === "string" && directory.length > 0)
|
|
40459
|
+
dir = directory;
|
|
40460
|
+
else
|
|
40461
|
+
failure = "the session record has no location.directory";
|
|
40462
|
+
} catch (err) {
|
|
40463
|
+
failure = err instanceof Error ? err.message || err.name : String(err);
|
|
40464
|
+
}
|
|
40465
|
+
if (failure !== undefined) {
|
|
40466
|
+
sessionWarn(sessionId, `[aft-plugin] OpenCode 2 session lookup failed (${failure}); tool paths and bash use the Location directory ${context.location.directory} for this session`);
|
|
40467
|
+
}
|
|
40468
|
+
setCache(sessionId, dir);
|
|
40469
|
+
return dir;
|
|
40470
|
+
}
|
|
40257
40471
|
function setCache(sessionId, directory) {
|
|
40258
40472
|
if (cache.has(sessionId))
|
|
40259
40473
|
cache.delete(sessionId);
|
|
@@ -40312,6 +40526,108 @@ async function verifySessionDirectory(client, sessionId) {
|
|
|
40312
40526
|
return dir;
|
|
40313
40527
|
}
|
|
40314
40528
|
|
|
40529
|
+
// src/shared/subagent-detect.ts
|
|
40530
|
+
import { Effect as Effect2 } from "effect";
|
|
40531
|
+
var CACHE_MAX_ENTRIES2 = 200;
|
|
40532
|
+
var LOOKUP_TIMEOUT_MS = 1000;
|
|
40533
|
+
var FAILED_LOOKUP_RETRY_MS = 30000;
|
|
40534
|
+
var cache2 = new Map;
|
|
40535
|
+
var inflight = new Map;
|
|
40536
|
+
var now = () => Date.now();
|
|
40537
|
+
async function resolveIsSubagent(client, sessionId, _fallbackDirectory) {
|
|
40538
|
+
if (!sessionId) {
|
|
40539
|
+
sessionLog(undefined, "[subagent-detect] no sessionId provided → primary");
|
|
40540
|
+
return false;
|
|
40541
|
+
}
|
|
40542
|
+
const cached = cache2.get(sessionId);
|
|
40543
|
+
if (cached && (cached.retryAfter === undefined || now() < cached.retryAfter)) {
|
|
40544
|
+
cache2.delete(sessionId);
|
|
40545
|
+
cache2.set(sessionId, cached);
|
|
40546
|
+
return cached.isSubagent;
|
|
40547
|
+
}
|
|
40548
|
+
const pending = inflight.get(sessionId);
|
|
40549
|
+
if (pending)
|
|
40550
|
+
return pending;
|
|
40551
|
+
const lookup = boundedLookup(client, sessionId).finally(() => {
|
|
40552
|
+
inflight.delete(sessionId);
|
|
40553
|
+
});
|
|
40554
|
+
inflight.set(sessionId, lookup);
|
|
40555
|
+
return lookup;
|
|
40556
|
+
}
|
|
40557
|
+
async function boundedLookup(client, sessionId) {
|
|
40558
|
+
let timer;
|
|
40559
|
+
const timedOut = new Promise((resolve) => {
|
|
40560
|
+
timer = setTimeout(() => resolve("timeout"), LOOKUP_TIMEOUT_MS);
|
|
40561
|
+
});
|
|
40562
|
+
let answer;
|
|
40563
|
+
try {
|
|
40564
|
+
answer = await Promise.race([lookupSession(client, sessionId), timedOut]);
|
|
40565
|
+
} finally {
|
|
40566
|
+
clearTimeout(timer);
|
|
40567
|
+
}
|
|
40568
|
+
if (typeof answer === "boolean") {
|
|
40569
|
+
setCache2(sessionId, answer);
|
|
40570
|
+
return answer;
|
|
40571
|
+
}
|
|
40572
|
+
if (answer === "timeout") {
|
|
40573
|
+
sessionWarn(sessionId, `[subagent-detect] session lookup did not answer within ${LOOKUP_TIMEOUT_MS}ms → primary for now`);
|
|
40574
|
+
}
|
|
40575
|
+
setCache2(sessionId, false, now() + FAILED_LOOKUP_RETRY_MS);
|
|
40576
|
+
return false;
|
|
40577
|
+
}
|
|
40578
|
+
async function lookupSession(client, sessionId) {
|
|
40579
|
+
if (isV2PluginContext2(client))
|
|
40580
|
+
return resolveV2(client, sessionId);
|
|
40581
|
+
const c = client;
|
|
40582
|
+
const sessionApi = c?.session;
|
|
40583
|
+
if (!sessionApi || typeof sessionApi.get !== "function") {
|
|
40584
|
+
sessionLog(sessionId, `[subagent-detect] client.session.get unavailable (client=${typeof client}, session=${typeof sessionApi}, get=${typeof sessionApi?.get}) → caching as primary`);
|
|
40585
|
+
return false;
|
|
40586
|
+
}
|
|
40587
|
+
sessionLog(sessionId, `[subagent-detect] cache miss, calling client.session.get(id=${sessionId})`);
|
|
40588
|
+
try {
|
|
40589
|
+
const result = await sessionApi.get({
|
|
40590
|
+
path: { id: sessionId }
|
|
40591
|
+
});
|
|
40592
|
+
const session = result?.data ?? result;
|
|
40593
|
+
const parentIdRaw = session?.parentID;
|
|
40594
|
+
const isSubagent = session !== undefined && typeof session.parentID === "string" && session.parentID.length > 0;
|
|
40595
|
+
sessionLog(sessionId, `[subagent-detect] SDK returned session=${session !== undefined ? "present" : "undefined"}, parentID=${JSON.stringify(parentIdRaw)} → isSubagent=${isSubagent}`);
|
|
40596
|
+
return isSubagent;
|
|
40597
|
+
} catch (err) {
|
|
40598
|
+
sessionWarn(sessionId, `[subagent-detect] SDK lookup failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
40599
|
+
return "failed";
|
|
40600
|
+
}
|
|
40601
|
+
}
|
|
40602
|
+
function isV2PluginContext2(client) {
|
|
40603
|
+
if (!client || typeof client !== "object")
|
|
40604
|
+
return false;
|
|
40605
|
+
const candidate = client;
|
|
40606
|
+
return typeof candidate.location?.directory === "string" && typeof candidate.session?.get === "function";
|
|
40607
|
+
}
|
|
40608
|
+
async function resolveV2(context, sessionId) {
|
|
40609
|
+
let session;
|
|
40610
|
+
try {
|
|
40611
|
+
session = await Effect2.runPromise(context.session.get({ sessionID: sessionId }));
|
|
40612
|
+
} catch (err) {
|
|
40613
|
+
sessionWarn(sessionId, `[subagent-detect] OpenCode 2 session lookup failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
40614
|
+
return "failed";
|
|
40615
|
+
}
|
|
40616
|
+
const isSubagent = typeof session?.parentID === "string" && session.parentID.length > 0;
|
|
40617
|
+
sessionLog(sessionId, `[subagent-detect] OpenCode 2 session parentID=${JSON.stringify(session?.parentID)} → isSubagent=${isSubagent}`);
|
|
40618
|
+
return isSubagent;
|
|
40619
|
+
}
|
|
40620
|
+
function setCache2(sessionId, isSubagent, retryAfter) {
|
|
40621
|
+
if (cache2.has(sessionId))
|
|
40622
|
+
cache2.delete(sessionId);
|
|
40623
|
+
cache2.set(sessionId, retryAfter === undefined ? { isSubagent } : { isSubagent, retryAfter });
|
|
40624
|
+
if (cache2.size > CACHE_MAX_ENTRIES2) {
|
|
40625
|
+
const oldest = cache2.keys().next().value;
|
|
40626
|
+
if (oldest !== undefined)
|
|
40627
|
+
cache2.delete(oldest);
|
|
40628
|
+
}
|
|
40629
|
+
}
|
|
40630
|
+
|
|
40315
40631
|
// src/tool-perf.ts
|
|
40316
40632
|
import { AsyncLocalStorage } from "node:async_hooks";
|
|
40317
40633
|
var perfStore = new AsyncLocalStorage;
|
|
@@ -40387,7 +40703,7 @@ function projectRootFor(runtime) {
|
|
|
40387
40703
|
if (typeof cached === "string" && cached.length > 0) {
|
|
40388
40704
|
return canonicalizeDirectory(cached);
|
|
40389
40705
|
}
|
|
40390
|
-
const raw = runtime.worktree ?? runtime.directory;
|
|
40706
|
+
const raw = runtime.directoryIsSessionRoot ? runtime.directory : runtime.worktree ?? runtime.directory;
|
|
40391
40707
|
return canonicalizeDirectory(raw);
|
|
40392
40708
|
}
|
|
40393
40709
|
async function resolveProjectRoot(ctx, runtime) {
|
|
@@ -40423,6 +40739,11 @@ function bridgeFor(ctx, runtime) {
|
|
|
40423
40739
|
}
|
|
40424
40740
|
return ctx.pool.getBridge(projectRoot);
|
|
40425
40741
|
}
|
|
40742
|
+
async function isWorkerRuntime(ctx, runtime) {
|
|
40743
|
+
if (!runtime.sessionID)
|
|
40744
|
+
return false;
|
|
40745
|
+
return resolveIsSubagent(ctx.client, runtime.sessionID, runtime.directory);
|
|
40746
|
+
}
|
|
40426
40747
|
async function callBridge(ctx, runtime, command, params = {}, options) {
|
|
40427
40748
|
if (runtime.sessionID && getSessionDirectoryCached(runtime.sessionID) === undefined) {
|
|
40428
40749
|
await getSessionDirectory(ctx.client, runtime.sessionID, runtime.directory);
|
|
@@ -40431,6 +40752,9 @@ async function callBridge(ctx, runtime, command, params = {}, options) {
|
|
|
40431
40752
|
if (runtime.sessionID) {
|
|
40432
40753
|
merged.session_id = runtime.sessionID;
|
|
40433
40754
|
}
|
|
40755
|
+
if (await isWorkerRuntime(ctx, runtime)) {
|
|
40756
|
+
merged[WORKER_SESSION_FIELD] = true;
|
|
40757
|
+
}
|
|
40434
40758
|
const timeoutMs = timeoutForCommand(command);
|
|
40435
40759
|
const sendOptions = {
|
|
40436
40760
|
...timeoutMs !== undefined ? { timeoutMs } : {},
|
|
@@ -40457,10 +40781,12 @@ async function callToolCall(ctx, runtime, name, rawArgs = {}, options) {
|
|
|
40457
40781
|
await getSessionDirectory(ctx.client, runtime.sessionID, runtime.directory);
|
|
40458
40782
|
}
|
|
40459
40783
|
const timeoutMs = timeoutForCommand(name);
|
|
40784
|
+
const workerSession = await isWorkerRuntime(ctx, runtime);
|
|
40460
40785
|
const sendOptions = {
|
|
40461
40786
|
...timeoutMs !== undefined ? { timeoutMs } : {},
|
|
40462
40787
|
configureWarningClient: ctx.client,
|
|
40463
40788
|
...runtime.effectAbort ? { abortSignal: runtime.effectAbort } : {},
|
|
40789
|
+
...workerSession ? { workerSession: true } : {},
|
|
40464
40790
|
...options
|
|
40465
40791
|
};
|
|
40466
40792
|
markBridgeStart();
|
|
@@ -41011,7 +41337,8 @@ var NPM_LSP_TABLE = [
|
|
|
41011
41337
|
npm: "typescript",
|
|
41012
41338
|
binary: "tsserver",
|
|
41013
41339
|
extensions: ["ts", "tsx", "js", "jsx", "mjs", "cjs", "mts", "cts"],
|
|
41014
|
-
rootMarkers: ["tsconfig.json", "jsconfig.json", "package.json"]
|
|
41340
|
+
rootMarkers: ["tsconfig.json", "jsconfig.json", "package.json"],
|
|
41341
|
+
supportedMajor: 5
|
|
41015
41342
|
},
|
|
41016
41343
|
{
|
|
41017
41344
|
id: "python",
|
|
@@ -41085,6 +41412,15 @@ var NPM_LSP_TABLE = [
|
|
|
41085
41412
|
rootMarkers: ["biome.json", "biome.jsonc"]
|
|
41086
41413
|
}
|
|
41087
41414
|
];
|
|
41415
|
+
function majorVersionOf(version) {
|
|
41416
|
+
const match = /^v?(\d+)(?:\.|$)/.exec(version.trim());
|
|
41417
|
+
return match ? Number(match[1]) : null;
|
|
41418
|
+
}
|
|
41419
|
+
function isVersionSupported(spec, version) {
|
|
41420
|
+
if (spec.supportedMajor === undefined)
|
|
41421
|
+
return true;
|
|
41422
|
+
return majorVersionOf(version) === spec.supportedMajor;
|
|
41423
|
+
}
|
|
41088
41424
|
|
|
41089
41425
|
// src/lsp-project-relevance.ts
|
|
41090
41426
|
import { existsSync as existsSync14, readdirSync as readdirSync4, readFileSync as readFileSync12 } from "node:fs";
|
|
@@ -41181,10 +41517,11 @@ function extensionOf(fileName) {
|
|
|
41181
41517
|
|
|
41182
41518
|
// src/lsp-registry-probe.ts
|
|
41183
41519
|
var NPM_REGISTRY_BASE = "https://registry.npmjs.org";
|
|
41184
|
-
function pickEligibleVersion(response, graceDays, now = Date.now()) {
|
|
41520
|
+
function pickEligibleVersion(response, graceDays, now = Date.now(), accept = () => true) {
|
|
41185
41521
|
const times = response.time || {};
|
|
41186
41522
|
const cutoff = now - graceDays * 24 * 60 * 60 * 1000;
|
|
41187
41523
|
const candidates = [];
|
|
41524
|
+
let releaseCount = 0;
|
|
41188
41525
|
for (const [version, publishedAt] of Object.entries(times)) {
|
|
41189
41526
|
if (version === "created" || version === "modified")
|
|
41190
41527
|
continue;
|
|
@@ -41195,6 +41532,9 @@ function pickEligibleVersion(response, graceDays, now = Date.now()) {
|
|
|
41195
41532
|
const ts = Date.parse(publishedAt);
|
|
41196
41533
|
if (Number.isNaN(ts))
|
|
41197
41534
|
continue;
|
|
41535
|
+
releaseCount += 1;
|
|
41536
|
+
if (!accept(version))
|
|
41537
|
+
continue;
|
|
41198
41538
|
candidates.push({ version, publishedAt, ts });
|
|
41199
41539
|
}
|
|
41200
41540
|
candidates.sort((a, b) => b.ts - a.ts);
|
|
@@ -41203,10 +41543,11 @@ function pickEligibleVersion(response, graceDays, now = Date.now()) {
|
|
|
41203
41543
|
return {
|
|
41204
41544
|
version: eligible[0]?.version ?? null,
|
|
41205
41545
|
blockedByGrace,
|
|
41546
|
+
outsideSupportedRange: releaseCount > 0 && candidates.length === 0,
|
|
41206
41547
|
eligible: eligible.map(({ version, publishedAt }) => ({ version, publishedAt }))
|
|
41207
41548
|
};
|
|
41208
41549
|
}
|
|
41209
|
-
async function probeRegistry(npmPackage, graceDays, fetchImpl = fetch) {
|
|
41550
|
+
async function probeRegistry(npmPackage, graceDays, fetchImpl = fetch, accept) {
|
|
41210
41551
|
const encoded = encodeURIComponent(npmPackage).replace(/^%40/, "@");
|
|
41211
41552
|
const url = `${NPM_REGISTRY_BASE}/${encoded}`;
|
|
41212
41553
|
try {
|
|
@@ -41219,7 +41560,7 @@ async function probeRegistry(npmPackage, graceDays, fetchImpl = fetch) {
|
|
|
41219
41560
|
return null;
|
|
41220
41561
|
}
|
|
41221
41562
|
const json = await res.json();
|
|
41222
|
-
return pickEligibleVersion(json, graceDays);
|
|
41563
|
+
return pickEligibleVersion(json, graceDays, Date.now(), accept);
|
|
41223
41564
|
} catch (err) {
|
|
41224
41565
|
warn2(`[lsp] registry probe failed for ${npmPackage}: ${err}`);
|
|
41225
41566
|
return null;
|
|
@@ -41270,11 +41611,11 @@ async function resolveTargetVersion(spec, config, fetchImpl = fetch) {
|
|
|
41270
41611
|
}
|
|
41271
41612
|
const cached = readVersionCheck(spec.npm);
|
|
41272
41613
|
const weeklyMs = config.graceDays * 24 * 60 * 60 * 1000;
|
|
41273
|
-
const cachedSafe = isSafeVersion(cached?.latest_eligible ?? null);
|
|
41614
|
+
const cachedSafe = isSafeVersion(cached?.latest_eligible ?? null) && isVersionSupported(spec, cached?.latest_eligible);
|
|
41274
41615
|
if (cached && !shouldRecheckVersion(cached, weeklyMs) && cachedSafe) {
|
|
41275
41616
|
return { version: cached.latest_eligible, pinned: false, probe: null };
|
|
41276
41617
|
}
|
|
41277
|
-
const probe = await probeRegistry(spec.npm, config.graceDays, fetchImpl);
|
|
41618
|
+
const probe = await probeRegistry(spec.npm, config.graceDays, fetchImpl, (candidate) => isVersionSupported(spec, candidate));
|
|
41278
41619
|
if (!probe) {
|
|
41279
41620
|
return {
|
|
41280
41621
|
version: cachedSafe ? cached?.latest_eligible ?? null : null,
|
|
@@ -41409,11 +41750,16 @@ async function ensureServerInstalled(spec, config, fetchImpl, signal) {
|
|
|
41409
41750
|
const { version, probe } = await resolveTargetVersion(spec, config, fetchImpl);
|
|
41410
41751
|
if (!version) {
|
|
41411
41752
|
const installed = isInstalled(spec.npm, spec.binary);
|
|
41412
|
-
if (installed) {
|
|
41753
|
+
if (installed && installedVersionAllowed(spec, config)) {
|
|
41413
41754
|
warn2(`[lsp] no eligible version of ${spec.npm} (grace=${config.graceDays}d); keeping existing install`);
|
|
41414
41755
|
return { started: false, reason: "kept existing install" };
|
|
41415
41756
|
}
|
|
41416
|
-
const blocked = probe?.blockedByGrace ? `all versions are within ${config.graceDays}-day grace window` : "registry probe failed";
|
|
41757
|
+
const blocked = probe?.blockedByGrace ? `all versions are within ${config.graceDays}-day grace window` : probe?.outsideSupportedRange ? `no ${spec.supportedMajor}.x release on the registry` : "registry probe failed";
|
|
41758
|
+
if (installed) {
|
|
41759
|
+
const reason = `cached ${spec.npm} ${readInstalledMeta(spec.npm)?.version ?? "(unknown version)"} is outside the supported ${spec.supportedMajor}.x line and no replacement is available: ${blocked}`;
|
|
41760
|
+
warn2(`[lsp] ${reason}`);
|
|
41761
|
+
return { started: false, reason };
|
|
41762
|
+
}
|
|
41417
41763
|
warn2(`[lsp] skipping ${spec.npm}: ${blocked}`);
|
|
41418
41764
|
return { started: false, reason: blocked };
|
|
41419
41765
|
}
|
|
@@ -41538,6 +41884,15 @@ function validateCachedNpmInstall(spec) {
|
|
|
41538
41884
|
}
|
|
41539
41885
|
return true;
|
|
41540
41886
|
}
|
|
41887
|
+
function installedVersionAllowed(spec, config) {
|
|
41888
|
+
const installed = readInstalledMeta(spec.npm)?.version;
|
|
41889
|
+
if (!installed)
|
|
41890
|
+
return spec.supportedMajor === undefined;
|
|
41891
|
+
return isVersionSupported(spec, installed) || config.versions[spec.npm] === installed;
|
|
41892
|
+
}
|
|
41893
|
+
function cachedInstallUsable(spec, config) {
|
|
41894
|
+
return isInstalled(spec.npm, spec.binary) && validateCachedNpmInstall(spec) && installedVersionAllowed(spec, config);
|
|
41895
|
+
}
|
|
41541
41896
|
function runAutoInstall(projectRoot, config, fetchImpl = fetch) {
|
|
41542
41897
|
const cachedBinDirs = [];
|
|
41543
41898
|
const skipped = [];
|
|
@@ -41550,7 +41905,7 @@ function runAutoInstall(projectRoot, config, fetchImpl = fetch) {
|
|
|
41550
41905
|
return projectExtensions;
|
|
41551
41906
|
};
|
|
41552
41907
|
for (const spec of NPM_LSP_TABLE) {
|
|
41553
|
-
if (
|
|
41908
|
+
if (cachedInstallUsable(spec, config)) {
|
|
41554
41909
|
cachedBinDirs.push(lspBinDir(spec.npm));
|
|
41555
41910
|
}
|
|
41556
41911
|
if (config.disabled.has(spec.id) || spec.id === "typescript-sdk" && config.disabled.has("typescript")) {
|
|
@@ -41590,7 +41945,7 @@ function runAutoInstall(projectRoot, config, fetchImpl = fetch) {
|
|
|
41590
41945
|
},
|
|
41591
41946
|
skipped,
|
|
41592
41947
|
installsComplete: Promise.all(installPromises).then(() => {}),
|
|
41593
|
-
getCachedBinDirs: () => NPM_LSP_TABLE.filter((spec) =>
|
|
41948
|
+
getCachedBinDirs: () => NPM_LSP_TABLE.filter((spec) => cachedInstallUsable(spec, config)).map((spec) => lspBinDir(spec.npm))
|
|
41594
41949
|
};
|
|
41595
41950
|
}
|
|
41596
41951
|
async function pushLspPathsAfterAutoInstall(pool, projectRoot, cachedBinDirs) {
|
|
@@ -43141,391 +43496,8 @@ function astTools(ctx) {
|
|
|
43141
43496
|
};
|
|
43142
43497
|
}
|
|
43143
43498
|
|
|
43144
|
-
// src/tools/conflicts.ts
|
|
43145
|
-
var z3 = tool.schema;
|
|
43146
|
-
function conflictTools(ctx) {
|
|
43147
|
-
return {
|
|
43148
|
-
aft_conflicts: {
|
|
43149
|
-
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).",
|
|
43150
|
-
args: {
|
|
43151
|
-
path: z3.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()
|
|
43152
|
-
},
|
|
43153
|
-
execute: async (args, context) => {
|
|
43154
|
-
const rawArgs = {};
|
|
43155
|
-
if (!isEmptyParam(args?.path)) {
|
|
43156
|
-
const expanded = expandTilde2(String(args.path));
|
|
43157
|
-
const projectRoot = await resolveProjectRoot(ctx, context);
|
|
43158
|
-
const resolved = resolvePathFromProjectRoot(projectRoot, expanded);
|
|
43159
|
-
const denied = await assertExternalDirectoryPermission(ctx, context, resolved, {
|
|
43160
|
-
kind: "directory"
|
|
43161
|
-
});
|
|
43162
|
-
if (denied)
|
|
43163
|
-
return permissionDeniedResponse(denied);
|
|
43164
|
-
rawArgs.path = resolved;
|
|
43165
|
-
}
|
|
43166
|
-
const response = await callToolCall(ctx, context, "conflicts", rawArgs);
|
|
43167
|
-
if (response.success === false) {
|
|
43168
|
-
throw new Error(response.message || "git_conflicts failed");
|
|
43169
|
-
}
|
|
43170
|
-
return response.text;
|
|
43171
|
-
}
|
|
43172
|
-
}
|
|
43173
|
-
};
|
|
43174
|
-
}
|
|
43175
|
-
|
|
43176
|
-
// src/tools/definitions/v2.ts
|
|
43177
|
-
import { Effect } from "effect";
|
|
43178
|
-
var V2_TOOL_NAME = /^[A-Za-z][A-Za-z0-9_-]{0,63}$/;
|
|
43179
|
-
var V2_PATH_HEADER_TOOLS = new Set(["read", "write", "edit"]);
|
|
43180
|
-
var V2_BASH_TOOLS = new Set(["bash"]);
|
|
43181
|
-
var V2_SHELL_PERMISSION = "shell";
|
|
43182
|
-
var ROOT_COMBINATORS = ["anyOf", "oneOf", "allOf"];
|
|
43183
|
-
function failure(error) {
|
|
43184
|
-
if (error instanceof Error)
|
|
43185
|
-
return error;
|
|
43186
|
-
const detail = (() => {
|
|
43187
|
-
try {
|
|
43188
|
-
return JSON.stringify(error) ?? String(error);
|
|
43189
|
-
} catch {
|
|
43190
|
-
return String(error);
|
|
43191
|
-
}
|
|
43192
|
-
})();
|
|
43193
|
-
return new Error(`V2 tool execution rejected with a non-Error value: ${detail}`);
|
|
43194
|
-
}
|
|
43195
|
-
function isPlainObject3(value) {
|
|
43196
|
-
if (typeof value !== "object" || value === null)
|
|
43197
|
-
return false;
|
|
43198
|
-
const prototype = Object.getPrototypeOf(value);
|
|
43199
|
-
return prototype === Object.prototype || prototype === null;
|
|
43200
|
-
}
|
|
43201
|
-
function jsonSafeMetadata(value) {
|
|
43202
|
-
if (Array.isArray(value)) {
|
|
43203
|
-
return value.map((entry) => entry === undefined ? null : jsonSafeMetadata(entry));
|
|
43204
|
-
}
|
|
43205
|
-
if (!isPlainObject3(value))
|
|
43206
|
-
return value;
|
|
43207
|
-
const cleaned = {};
|
|
43208
|
-
for (const [key, entry] of Object.entries(value)) {
|
|
43209
|
-
if (entry === undefined)
|
|
43210
|
-
continue;
|
|
43211
|
-
cleaned[key] = jsonSafeMetadata(entry);
|
|
43212
|
-
}
|
|
43213
|
-
return cleaned;
|
|
43214
|
-
}
|
|
43215
|
-
function resultContent(result) {
|
|
43216
|
-
if (typeof result === "string")
|
|
43217
|
-
return { content: result };
|
|
43218
|
-
const attachments = (result.attachments ?? []).map((attachment) => ({
|
|
43219
|
-
type: "file",
|
|
43220
|
-
uri: attachment.url,
|
|
43221
|
-
mime: attachment.mime,
|
|
43222
|
-
...attachment.filename ? { name: attachment.filename } : {}
|
|
43223
|
-
}));
|
|
43224
|
-
const content = attachments.length ? [{ type: "text", text: result.output }, ...attachments] : result.output;
|
|
43225
|
-
const metadata = jsonSafeMetadata({
|
|
43226
|
-
...result.metadata ?? {},
|
|
43227
|
-
...result.title ? { title: result.title } : {}
|
|
43228
|
-
});
|
|
43229
|
-
return {
|
|
43230
|
-
content,
|
|
43231
|
-
...Object.keys(metadata).length ? { metadata } : {}
|
|
43232
|
-
};
|
|
43233
|
-
}
|
|
43234
|
-
function failureEnvelopeMessage(result) {
|
|
43235
|
-
const output = typeof result === "string" ? result : result.output;
|
|
43236
|
-
if (typeof output !== "string")
|
|
43237
|
-
return;
|
|
43238
|
-
const trimmed = output.trim();
|
|
43239
|
-
if (!trimmed.startsWith("{") || !trimmed.includes('"success"'))
|
|
43240
|
-
return;
|
|
43241
|
-
let parsed;
|
|
43242
|
-
try {
|
|
43243
|
-
parsed = JSON.parse(trimmed);
|
|
43244
|
-
} catch {
|
|
43245
|
-
return;
|
|
43246
|
-
}
|
|
43247
|
-
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed))
|
|
43248
|
-
return;
|
|
43249
|
-
const envelope = parsed;
|
|
43250
|
-
if (envelope.success !== false)
|
|
43251
|
-
return;
|
|
43252
|
-
for (const field of [envelope.message, envelope.error, envelope.code]) {
|
|
43253
|
-
if (typeof field === "string" && field.length > 0)
|
|
43254
|
-
return field;
|
|
43255
|
-
}
|
|
43256
|
-
return "The tool call failed.";
|
|
43257
|
-
}
|
|
43258
|
-
function bareToolName3(name) {
|
|
43259
|
-
return name.startsWith("aft_") ? name.slice(4) : name;
|
|
43260
|
-
}
|
|
43261
|
-
function projectArguments(name, definition) {
|
|
43262
|
-
if (!V2_PATH_HEADER_TOOLS.has(bareToolName3(name)) || !("filePath" in definition.args)) {
|
|
43263
|
-
return { ...definition.args };
|
|
43264
|
-
}
|
|
43265
|
-
const { filePath, ...rest } = definition.args;
|
|
43266
|
-
return { path: filePath, ...rest };
|
|
43267
|
-
}
|
|
43268
|
-
function executionArguments(name, input) {
|
|
43269
|
-
if (!V2_PATH_HEADER_TOOLS.has(bareToolName3(name)) || !("path" in input))
|
|
43270
|
-
return input;
|
|
43271
|
-
const { path, ...rest } = input;
|
|
43272
|
-
return { filePath: path, ...rest };
|
|
43273
|
-
}
|
|
43274
|
-
function hostPermission(name) {
|
|
43275
|
-
const bare = bareToolName3(name);
|
|
43276
|
-
if (new Set(["read", "glob", "grep"]).has(bare))
|
|
43277
|
-
return bare;
|
|
43278
|
-
if (V2_BASH_TOOLS.has(name))
|
|
43279
|
-
return V2_SHELL_PERMISSION;
|
|
43280
|
-
if (new Set([
|
|
43281
|
-
"write",
|
|
43282
|
-
"edit",
|
|
43283
|
-
"apply_patch",
|
|
43284
|
-
"delete",
|
|
43285
|
-
"move",
|
|
43286
|
-
"ast_grep_replace",
|
|
43287
|
-
"import",
|
|
43288
|
-
"safety"
|
|
43289
|
-
]).has(bare)) {
|
|
43290
|
-
return "edit";
|
|
43291
|
-
}
|
|
43292
|
-
return;
|
|
43293
|
-
}
|
|
43294
|
-
function assertV2Contract(name, input) {
|
|
43295
|
-
if (!V2_TOOL_NAME.test(name)) {
|
|
43296
|
-
throw new Error(`Invalid V2 tool name ${JSON.stringify(name)}; expected ${V2_TOOL_NAME.source}`);
|
|
43297
|
-
}
|
|
43298
|
-
const schema = tool.schema.toJSONSchema(input, { io: "input" });
|
|
43299
|
-
if (schema.type !== "object") {
|
|
43300
|
-
throw new Error(`V2 tool ${name} must have an object-rooted input schema`);
|
|
43301
|
-
}
|
|
43302
|
-
for (const keyword of ROOT_COMBINATORS) {
|
|
43303
|
-
if (keyword in schema) {
|
|
43304
|
-
throw new Error(`V2 tool ${name} input schema cannot use root ${keyword}`);
|
|
43305
|
-
}
|
|
43306
|
-
}
|
|
43307
|
-
}
|
|
43308
|
-
function v2PermissionRequest(name, request) {
|
|
43309
|
-
if (!V2_BASH_TOOLS.has(name) || request.permission !== "bash")
|
|
43310
|
-
return request;
|
|
43311
|
-
return { ...request, permission: V2_SHELL_PERMISSION };
|
|
43312
|
-
}
|
|
43313
|
-
function runtimeFor(name, location, context, signal, consumers) {
|
|
43314
|
-
const directory = location.directory;
|
|
43315
|
-
const worktree = location.project?.canonical ?? location.project?.directory ?? directory;
|
|
43316
|
-
return {
|
|
43317
|
-
sessionID: context.sessionID,
|
|
43318
|
-
messageID: context.messageID,
|
|
43319
|
-
agent: context.agent,
|
|
43320
|
-
directory,
|
|
43321
|
-
worktree,
|
|
43322
|
-
abort: signal,
|
|
43323
|
-
effectAbort: signal,
|
|
43324
|
-
metadata: (update) => {
|
|
43325
|
-
Effect.runPromise(context.progress(jsonSafeMetadata({
|
|
43326
|
-
...update.metadata ?? {},
|
|
43327
|
-
...update.title ? { title: update.title } : {}
|
|
43328
|
-
})));
|
|
43329
|
-
},
|
|
43330
|
-
ask: (asked) => {
|
|
43331
|
-
const request = v2PermissionRequest(name, asked);
|
|
43332
|
-
if (consumers.requestPermission)
|
|
43333
|
-
return consumers.requestPermission(request, context);
|
|
43334
|
-
return Promise.reject(new Error(`The "${request.permission}" operation was refused because this AFT runtime has no permission evaluator bound, so the host's permission rules for it could not be consulted.`));
|
|
43335
|
-
},
|
|
43336
|
-
progress: context.progress
|
|
43337
|
-
};
|
|
43338
|
-
}
|
|
43339
|
-
function projectV2Tool(name, definition, location, consumers = {}) {
|
|
43340
|
-
const input = tool.schema.object(projectArguments(name, definition));
|
|
43341
|
-
assertV2Contract(name, input);
|
|
43342
|
-
const sharedOptions = definition.options;
|
|
43343
|
-
const permission = hostPermission(name);
|
|
43344
|
-
return {
|
|
43345
|
-
name,
|
|
43346
|
-
description: definition.description,
|
|
43347
|
-
input,
|
|
43348
|
-
options: {
|
|
43349
|
-
...sharedOptions,
|
|
43350
|
-
...permission ? { permission } : {},
|
|
43351
|
-
codemode: false
|
|
43352
|
-
},
|
|
43353
|
-
execute: (rawInput, context) => Effect.tryPromise({
|
|
43354
|
-
try: async (signal) => {
|
|
43355
|
-
const runtime = runtimeFor(name, location, context, signal, consumers);
|
|
43356
|
-
const input = executionArguments(name, rawInput);
|
|
43357
|
-
const result = consumers.executeBash && V2_BASH_TOOLS.has(name) ? await consumers.executeBash({
|
|
43358
|
-
name,
|
|
43359
|
-
input,
|
|
43360
|
-
context: runtime,
|
|
43361
|
-
definition
|
|
43362
|
-
}) : await definition.execute(input, runtime);
|
|
43363
|
-
const refusal = failureEnvelopeMessage(result);
|
|
43364
|
-
if (refusal)
|
|
43365
|
-
throw new Error(refusal);
|
|
43366
|
-
return resultContent(result);
|
|
43367
|
-
},
|
|
43368
|
-
catch: failure
|
|
43369
|
-
})
|
|
43370
|
-
};
|
|
43371
|
-
}
|
|
43372
|
-
|
|
43373
|
-
// src/tools/hoisted.ts
|
|
43374
|
-
import { stat as stat2 } from "node:fs/promises";
|
|
43375
|
-
import * as path4 from "node:path";
|
|
43376
|
-
|
|
43377
|
-
// src/shared/read-vision.ts
|
|
43378
|
-
var VISION_HOST_TIMEOUT_MS = 1500;
|
|
43379
|
-
var states = new WeakMap;
|
|
43380
|
-
function stateFor2(client) {
|
|
43381
|
-
let state = states.get(client);
|
|
43382
|
-
if (!state) {
|
|
43383
|
-
state = { models: new Map, history: new Map, capabilities: new Map };
|
|
43384
|
-
states.set(client, state);
|
|
43385
|
-
}
|
|
43386
|
-
return state;
|
|
43387
|
-
}
|
|
43388
|
-
function rememberReadModel(client, sessionID, model) {
|
|
43389
|
-
stateFor2(client).models.set(sessionID, model);
|
|
43390
|
-
}
|
|
43391
|
-
async function bounded(call) {
|
|
43392
|
-
let timer;
|
|
43393
|
-
try {
|
|
43394
|
-
return await Promise.race([
|
|
43395
|
-
Promise.resolve().then(call),
|
|
43396
|
-
new Promise((resolve) => {
|
|
43397
|
-
timer = setTimeout(() => resolve(undefined), VISION_HOST_TIMEOUT_MS);
|
|
43398
|
-
})
|
|
43399
|
-
]);
|
|
43400
|
-
} catch {
|
|
43401
|
-
return;
|
|
43402
|
-
} finally {
|
|
43403
|
-
clearTimeout(timer);
|
|
43404
|
-
}
|
|
43405
|
-
}
|
|
43406
|
-
function record3(value) {
|
|
43407
|
-
return typeof value === "object" && value !== null && !Array.isArray(value) ? value : undefined;
|
|
43408
|
-
}
|
|
43409
|
-
async function currentSessionVisionCapability(client, sessionID) {
|
|
43410
|
-
if (!sessionID)
|
|
43411
|
-
return;
|
|
43412
|
-
const state = stateFor2(client);
|
|
43413
|
-
let model = state.models.get(sessionID);
|
|
43414
|
-
if (!model) {
|
|
43415
|
-
let pending = state.history.get(sessionID);
|
|
43416
|
-
if (!pending) {
|
|
43417
|
-
pending = bounded(() => resolvePromptContext(client, sessionID)).then((context) => context?.model);
|
|
43418
|
-
state.history.set(sessionID, pending);
|
|
43419
|
-
pending.finally(() => state.history.delete(sessionID));
|
|
43420
|
-
}
|
|
43421
|
-
const historical = await pending;
|
|
43422
|
-
model = state.models.get(sessionID) ?? historical;
|
|
43423
|
-
}
|
|
43424
|
-
if (!model)
|
|
43425
|
-
return;
|
|
43426
|
-
const currentModel = model;
|
|
43427
|
-
const key = JSON.stringify([model.providerID, model.modelID]);
|
|
43428
|
-
let capability = state.capabilities.get(key);
|
|
43429
|
-
if (!capability) {
|
|
43430
|
-
capability = (async () => {
|
|
43431
|
-
const api = client.provider;
|
|
43432
|
-
if (!api?.list)
|
|
43433
|
-
return;
|
|
43434
|
-
const list = api.list.bind(api);
|
|
43435
|
-
const listed = record3(await bounded(list));
|
|
43436
|
-
const catalog = record3(listed?.data) ?? listed;
|
|
43437
|
-
const providers = catalog?.all ?? catalog?.providers;
|
|
43438
|
-
if (!Array.isArray(providers))
|
|
43439
|
-
return;
|
|
43440
|
-
const provider = providers.map(record3).find((entry) => entry?.id === currentModel.providerID);
|
|
43441
|
-
const models = provider?.models;
|
|
43442
|
-
const entry = record3(Array.isArray(models) ? models.find((entry) => record3(entry)?.id === currentModel.modelID) : record3(models)?.[currentModel.modelID]);
|
|
43443
|
-
const inputs = record3(entry?.modalities)?.input;
|
|
43444
|
-
return Array.isArray(inputs) ? inputs.includes("image") : typeof entry?.attachment === "boolean" ? entry.attachment : undefined;
|
|
43445
|
-
})();
|
|
43446
|
-
state.capabilities.set(key, capability);
|
|
43447
|
-
capability.then((value) => {
|
|
43448
|
-
if (value === undefined)
|
|
43449
|
-
state.capabilities.delete(key);
|
|
43450
|
-
});
|
|
43451
|
-
}
|
|
43452
|
-
return capability;
|
|
43453
|
-
}
|
|
43454
|
-
|
|
43455
|
-
// src/shared/subagent-detect.ts
|
|
43456
|
-
import { Effect as Effect2 } from "effect";
|
|
43457
|
-
var CACHE_MAX_ENTRIES2 = 200;
|
|
43458
|
-
var cache2 = new Map;
|
|
43459
|
-
async function resolveIsSubagent(client, sessionId, _fallbackDirectory) {
|
|
43460
|
-
if (!sessionId) {
|
|
43461
|
-
sessionLog(undefined, "[subagent-detect] no sessionId provided → primary");
|
|
43462
|
-
return false;
|
|
43463
|
-
}
|
|
43464
|
-
const cached = cache2.get(sessionId);
|
|
43465
|
-
if (cached) {
|
|
43466
|
-
cache2.delete(sessionId);
|
|
43467
|
-
cache2.set(sessionId, cached);
|
|
43468
|
-
return cached.isSubagent;
|
|
43469
|
-
}
|
|
43470
|
-
if (isV2PluginContext(client))
|
|
43471
|
-
return resolveV2(client, sessionId);
|
|
43472
|
-
const c = client;
|
|
43473
|
-
const sessionApi = c?.session;
|
|
43474
|
-
if (!sessionApi || typeof sessionApi.get !== "function") {
|
|
43475
|
-
sessionLog(sessionId, `[subagent-detect] client.session.get unavailable (client=${typeof client}, session=${typeof sessionApi}, get=${typeof sessionApi?.get}) → caching as primary`);
|
|
43476
|
-
setCache2(sessionId, false);
|
|
43477
|
-
return false;
|
|
43478
|
-
}
|
|
43479
|
-
sessionLog(sessionId, `[subagent-detect] cache miss, calling client.session.get(id=${sessionId})`);
|
|
43480
|
-
let isSubagent = false;
|
|
43481
|
-
let parentIdRaw;
|
|
43482
|
-
try {
|
|
43483
|
-
const result = await sessionApi.get({
|
|
43484
|
-
path: { id: sessionId }
|
|
43485
|
-
});
|
|
43486
|
-
const session = result?.data ?? result;
|
|
43487
|
-
parentIdRaw = session?.parentID;
|
|
43488
|
-
isSubagent = session !== undefined && typeof session.parentID === "string" && session.parentID.length > 0;
|
|
43489
|
-
sessionLog(sessionId, `[subagent-detect] SDK returned session=${session !== undefined ? "present" : "undefined"}, parentID=${JSON.stringify(parentIdRaw)} → isSubagent=${isSubagent}`);
|
|
43490
|
-
} catch (err) {
|
|
43491
|
-
sessionWarn(sessionId, `[subagent-detect] SDK lookup failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
43492
|
-
return false;
|
|
43493
|
-
}
|
|
43494
|
-
setCache2(sessionId, isSubagent);
|
|
43495
|
-
return isSubagent;
|
|
43496
|
-
}
|
|
43497
|
-
function isV2PluginContext(client) {
|
|
43498
|
-
if (!client || typeof client !== "object")
|
|
43499
|
-
return false;
|
|
43500
|
-
const candidate = client;
|
|
43501
|
-
return typeof candidate.location?.directory === "string" && typeof candidate.session?.get === "function";
|
|
43502
|
-
}
|
|
43503
|
-
async function resolveV2(context, sessionId) {
|
|
43504
|
-
let session;
|
|
43505
|
-
try {
|
|
43506
|
-
session = await Effect2.runPromise(context.session.get({ sessionID: sessionId }));
|
|
43507
|
-
} catch (err) {
|
|
43508
|
-
sessionWarn(sessionId, `[subagent-detect] OpenCode 2 session lookup failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
43509
|
-
return false;
|
|
43510
|
-
}
|
|
43511
|
-
const isSubagent = typeof session?.parentID === "string" && session.parentID.length > 0;
|
|
43512
|
-
sessionLog(sessionId, `[subagent-detect] OpenCode 2 session parentID=${JSON.stringify(session?.parentID)} → isSubagent=${isSubagent}`);
|
|
43513
|
-
setCache2(sessionId, isSubagent);
|
|
43514
|
-
return isSubagent;
|
|
43515
|
-
}
|
|
43516
|
-
function setCache2(sessionId, isSubagent) {
|
|
43517
|
-
if (cache2.has(sessionId))
|
|
43518
|
-
cache2.delete(sessionId);
|
|
43519
|
-
cache2.set(sessionId, { isSubagent, recordedAt: Date.now() });
|
|
43520
|
-
if (cache2.size > CACHE_MAX_ENTRIES2) {
|
|
43521
|
-
const oldest = cache2.keys().next().value;
|
|
43522
|
-
if (oldest !== undefined)
|
|
43523
|
-
cache2.delete(oldest);
|
|
43524
|
-
}
|
|
43525
|
-
}
|
|
43526
|
-
|
|
43527
43499
|
// src/tools/bash.ts
|
|
43528
|
-
var
|
|
43500
|
+
var z3 = tool.schema;
|
|
43529
43501
|
var METADATA_PREVIEW_LIMIT = 30 * 1024;
|
|
43530
43502
|
var DEFAULT_HARD_TIMEOUT_MS = 30 * 60 * 1000;
|
|
43531
43503
|
var BASH_TRANSPORT_MARGIN_MS = 1e4;
|
|
@@ -43541,7 +43513,9 @@ function resolveForegroundWaitMs(configured) {
|
|
|
43541
43513
|
}
|
|
43542
43514
|
return configured;
|
|
43543
43515
|
}
|
|
43544
|
-
function orchestratedTransportTimeoutMs(blockToCompletion, wait, effectiveTimeout, foregroundWaitMs) {
|
|
43516
|
+
function orchestratedTransportTimeoutMs(blockToCompletion, wait, effectiveTimeout, foregroundWaitMs, noHardKill = false) {
|
|
43517
|
+
if (noHardKill)
|
|
43518
|
+
return LONGEST_TIMER_DELAY_MS;
|
|
43545
43519
|
const waitBudget = blockToCompletion || wait ? effectiveTimeout ?? DEFAULT_HARD_TIMEOUT_MS : foregroundWaitMs;
|
|
43546
43520
|
return waitBudget + BASH_TRANSPORT_MARGIN_MS;
|
|
43547
43521
|
}
|
|
@@ -43618,13 +43592,38 @@ function listenForForegroundAbort(ctx, runtime, enabled) {
|
|
|
43618
43592
|
function userMessageDetachDescription(detachOnUserMessage) {
|
|
43619
43593
|
return detachOnUserMessage ? "Any new message detaches this wait. Set `bash.detach_on_user_message: false` to keep it blocking; even then, a message containing the literal `&detach` forces detachment, and the token is stripped before delivery and the rest of the message is preserved; a token-only message becomes `(requested background detach)`." : "Because `bash.detach_on_user_message` is false, a new message leaves this wait blocking; include the literal `&detach` anywhere to force detachment, and the token is stripped before delivery and the rest of the message is preserved; a token-only message becomes `(requested background detach)`.";
|
|
43620
43594
|
}
|
|
43621
|
-
function
|
|
43622
|
-
return
|
|
43595
|
+
function bashCompanionRegistered(config, companion) {
|
|
43596
|
+
return resolveBashConfig(config).background && toolEnabled(config, companion);
|
|
43623
43597
|
}
|
|
43624
|
-
function
|
|
43625
|
-
|
|
43598
|
+
function backgroundWaitDescription(watchToolRegistered, statusRegistered) {
|
|
43599
|
+
if (watchToolRegistered) {
|
|
43600
|
+
const noPolling = statusRegistered ? ", and never loop bash_status to wait" : "";
|
|
43601
|
+
return `then bash_watch handles only a short remaining wait (in a main session a watch defaults to 30s, max bash.watch_sync_max_ms, 120s by default; in a delegated session a watch without a timeout waits until the command finishes); for anything longer end the turn and let the completion reminder wake you, or use bash({wait:true}) when the result is needed before anything else — never background a command and immediately bash_watch it (that wastes a turn for what foreground returns in one)${noPolling}.`;
|
|
43602
|
+
}
|
|
43603
|
+
return statusRegistered ? "the task keeps running after the call returns, a completion reminder arrives when it exits, and bash_status reports its state and output. Use bash({wait:true}) instead when the result is needed before anything else." : "the task keeps running after the call returns and a completion reminder arrives when it exits. Use bash({wait:true}) instead when the result is needed before anything else.";
|
|
43604
|
+
}
|
|
43605
|
+
function ptyDriveClause(statusRegistered, writeRegistered) {
|
|
43606
|
+
if (statusRegistered && writeRegistered) {
|
|
43607
|
+
return ', and is driven with bash_status({ outputMode: "screen" }) plus bash_write';
|
|
43608
|
+
}
|
|
43609
|
+
if (statusRegistered)
|
|
43610
|
+
return ', and its screen is read with bash_status({ outputMode: "screen" })';
|
|
43611
|
+
if (writeRegistered)
|
|
43612
|
+
return ", and is driven with bash_write";
|
|
43613
|
+
return "";
|
|
43614
|
+
}
|
|
43615
|
+
function bashToolDescription(aftSearchRegistered, compressionOn, backgroundOn, detachOnUserMessage = true, zoomEnabled = true, watchToolRegistered = true, surface = {}) {
|
|
43616
|
+
const outline = surface.outline !== false;
|
|
43617
|
+
const status = surface.status !== false;
|
|
43618
|
+
const write = surface.write !== false;
|
|
43619
|
+
const steerTools = [
|
|
43620
|
+
aftSearchRegistered ? "aft_search (concepts, identifiers, regex, literals)" : "the grep tool",
|
|
43621
|
+
"read",
|
|
43622
|
+
...outline ? ["aft_outline"] : []
|
|
43623
|
+
].join(", ");
|
|
43624
|
+
const searchSteer = `use ${steerTools}${zoomEnabled ? ", or aft_zoom" : ""} instead`;
|
|
43626
43625
|
const compression = compressionOn ? " Output is compressed by default; pass compressed: false for raw output. Piped commands run verbatim and show the pipeline's output; for AFT's test/build summary, run the runner without | head, | tail, or | grep. Pipeline-failure notes cover single top-level pipelines only; multi-statement commands (`a; b | c; d`) are not instrumented, so masked failures inside them still need explicit exit-code checks." : "";
|
|
43627
|
-
const tasks = backgroundOn ? ` Commands run in the foreground and return inline; wait: true blocks until a long command finishes instead of auto-promoting; ${userMessageDetachDescription(detachOnUserMessage)} Use it when you need the result before doing anything else; keep it off otherwise so auto-promote can remind you while you work. Use background: true yourself ONLY when you have other useful work to do while it runs; ${backgroundWaitDescription(watchToolRegistered)} A \`nohup … &\` launch still holds the call if the child keeps stdout/stderr; redirect both or use background:true. pty: true runs interactive programs (REPLs, TUIs), implies background,
|
|
43626
|
+
const tasks = backgroundOn ? ` Commands run in the foreground and return inline; wait: true blocks until a long command finishes instead of auto-promoting; ${userMessageDetachDescription(detachOnUserMessage)} Use it when you need the result before doing anything else; keep it off otherwise so auto-promote can remind you while you work. Use background: true yourself ONLY when you have other useful work to do while it runs; ${backgroundWaitDescription(watchToolRegistered, status)} A \`nohup … &\` launch still holds the call if the child keeps stdout/stderr; redirect both or use background:true. pty: true runs interactive programs (REPLs, TUIs), implies background${ptyDriveClause(status, write)}.` : " Commands run in the foreground to completion; timeout is the hard kill cap (default 30 minutes).";
|
|
43628
43627
|
return `Execute shell commands.${compression}${tasks}
|
|
43629
43628
|
|
|
43630
43629
|
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}. When a list is cut, the reply ends with \`shown N of M <unit> (<reason>) · narrow: <knobs>\`; absence of that line means the list is complete.`;
|
|
@@ -43701,30 +43700,63 @@ async function withPermissionLoop(ctx, runtime, params, bridgeCall, options) {
|
|
|
43701
43700
|
}
|
|
43702
43701
|
throw new Error("bash permission retry failed: too many rounds");
|
|
43703
43702
|
}
|
|
43703
|
+
function ptyDriveParamSentence(statusRegistered, writeRegistered) {
|
|
43704
|
+
const writeInput = 'its input accepts either a string OR an array like [ "iHello", { key: "esc" }, ":wq", { key: "enter" } ] for atomic text+key sequences.';
|
|
43705
|
+
if (statusRegistered && writeRegistered) {
|
|
43706
|
+
return ` Inspect with bash_status({ taskId, outputMode: "screen" }) and drive interactively with bash_write — ${writeInput}`;
|
|
43707
|
+
}
|
|
43708
|
+
if (statusRegistered)
|
|
43709
|
+
return ' Inspect with bash_status({ taskId, outputMode: "screen" }).';
|
|
43710
|
+
if (writeRegistered)
|
|
43711
|
+
return ` Drive interactively with bash_write — ${writeInput}`;
|
|
43712
|
+
return "";
|
|
43713
|
+
}
|
|
43714
|
+
function nativeSandboxEnabled(config) {
|
|
43715
|
+
return config.sandbox?.enabled === true;
|
|
43716
|
+
}
|
|
43704
43717
|
function createBashTool(ctx, aftSearchRegisteredOverride) {
|
|
43705
43718
|
const initialBashCfg = resolveBashConfig(ctx.config);
|
|
43719
|
+
const statusRegistered = bashCompanionRegistered(ctx.config, "bash_status");
|
|
43720
|
+
const writeRegistered = bashCompanionRegistered(ctx.config, "bash_write");
|
|
43721
|
+
const taskControls = [
|
|
43722
|
+
...statusRegistered ? ["bash_status"] : [],
|
|
43723
|
+
...bashCompanionRegistered(ctx.config, "bash_kill") ? ["bash_kill"] : []
|
|
43724
|
+
];
|
|
43725
|
+
const waitArg = initialBashCfg.background ? {
|
|
43726
|
+
wait: z3.boolean().optional().describe(`When true, run in the foreground without auto-promoting and wait until the command finishes or reaches its timeout; ${userMessageDetachDescription(initialBashCfg.detach_on_user_message)} Use only when you know the result is required before doing anything else.`)
|
|
43727
|
+
} : {};
|
|
43728
|
+
const sandboxArg = nativeSandboxEnabled(ctx.config) ? {
|
|
43729
|
+
sandbox: z3.literal("host").optional().describe("Request one-command approval to run unsandboxed on the host; use only when native sandboxing blocks required work, and note that it is a no-op when sandboxing is disabled.")
|
|
43730
|
+
} : {};
|
|
43731
|
+
const compressedArg = initialBashCfg.compress ? {
|
|
43732
|
+
compressed: z3.boolean().optional().describe("When true or omitted, return compressed output with noisy terminal control sequences reduced. Set to false for raw output.")
|
|
43733
|
+
} : {};
|
|
43706
43734
|
const backgroundFlagArg = initialBashCfg.background ? {
|
|
43707
|
-
background:
|
|
43735
|
+
background: z3.boolean().optional().describe(`When true, spawn the command in the background and return a taskId${taskControls.length > 0 ? ` for ${taskControls.join("/")}` : ""} instead of waiting for completion. Defaults to false.`)
|
|
43708
43736
|
} : {};
|
|
43709
43737
|
const ptyArgs = initialBashCfg.background ? {
|
|
43710
|
-
pty:
|
|
43738
|
+
pty: z3.boolean().optional().describe(`When true, spawn the command in a real PTY for interactive programs (python/node/bash REPLs, vim). Implies background: true automatically.${initialBashCfg.subagent_background ? "" : " Unavailable in subagent sessions because bash.subagent_background is false."}${ptyDriveParamSentence(statusRegistered, writeRegistered)}`),
|
|
43711
43739
|
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."),
|
|
43712
43740
|
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.")
|
|
43713
43741
|
} : {};
|
|
43714
43742
|
const args = {
|
|
43715
|
-
command:
|
|
43743
|
+
command: z3.string().describe("Shell command to execute. Supports pipes, redirection, and normal shell syntax."),
|
|
43716
43744
|
timeout: optionalInt(1, Number.MAX_SAFE_INTEGER).describe(initialBashCfg.background ? "Hard kill cap in milliseconds (positive integer). In the default foreground mode when wait is false, a command that exceeds the configured wait window is promoted to background and gets a completion reminder when it exits; wait:true disables promotion and remains inline until completion or timeout." : "Hard kill cap in milliseconds (positive integer). When omitted, the foreground command can run up to 30 minutes and returns inline when it finishes."),
|
|
43717
|
-
workdir:
|
|
43718
|
-
description:
|
|
43719
|
-
|
|
43720
|
-
|
|
43745
|
+
workdir: z3.string().optional().describe("Working directory for command execution. Relative paths resolve through the bridge; defaults to the current tool context/project root when omitted."),
|
|
43746
|
+
description: z3.string().optional().describe("Short 5-10 word human-readable summary shown in OpenCode UI metadata instead of raw shell syntax."),
|
|
43747
|
+
...waitArg,
|
|
43748
|
+
...sandboxArg,
|
|
43721
43749
|
...backgroundFlagArg,
|
|
43722
|
-
|
|
43750
|
+
...compressedArg,
|
|
43723
43751
|
...ptyArgs
|
|
43724
43752
|
};
|
|
43725
43753
|
let hostFallbackActive = false;
|
|
43726
43754
|
return {
|
|
43727
|
-
description: bashToolDescription(false, initialBashCfg.compress, initialBashCfg.background, true, toolEnabled(ctx.config, "aft_zoom")),
|
|
43755
|
+
description: bashToolDescription(false, initialBashCfg.compress, initialBashCfg.background, true, toolEnabled(ctx.config, "aft_zoom"), bashCompanionRegistered(ctx.config, "bash_watch"), {
|
|
43756
|
+
outline: toolEnabled(ctx.config, "aft_outline"),
|
|
43757
|
+
status: statusRegistered,
|
|
43758
|
+
write: writeRegistered
|
|
43759
|
+
}),
|
|
43728
43760
|
args,
|
|
43729
43761
|
execute: async (args, context) => {
|
|
43730
43762
|
const bashCfg = resolveBashConfig(ctx.config);
|
|
@@ -43738,7 +43770,7 @@ function createBashTool(ctx, aftSearchRegisteredOverride) {
|
|
|
43738
43770
|
const cwd = args.workdir ?? context.directory;
|
|
43739
43771
|
const isSubagent = await resolveIsSubagent(ctx.client, context.sessionID, context.directory);
|
|
43740
43772
|
const backgroundDisabled = !bashCfg.background;
|
|
43741
|
-
const requestedWait = coerceBoolean(args.wait);
|
|
43773
|
+
const requestedWait = !backgroundDisabled && coerceBoolean(args.wait);
|
|
43742
43774
|
const rawRequestedPty = coerceBoolean(args.pty);
|
|
43743
43775
|
const rawRequestedBackground = coerceBoolean(args.background);
|
|
43744
43776
|
if (requestedWait && rawRequestedPty) {
|
|
@@ -43749,14 +43781,15 @@ function createBashTool(ctx, aftSearchRegisteredOverride) {
|
|
|
43749
43781
|
}
|
|
43750
43782
|
const requestedPty = !backgroundDisabled && rawRequestedPty;
|
|
43751
43783
|
const requestedBackground = !backgroundDisabled && (rawRequestedBackground || requestedPty);
|
|
43752
|
-
if (requestedPty && isSubagent) {
|
|
43753
|
-
throw new Error("PTY mode is not available in subagent sessions; subagents cannot drive interactive terminals.");
|
|
43754
|
-
}
|
|
43755
43784
|
const allowSubagentBg = bashCfg.subagent_background;
|
|
43756
43785
|
const subagentForcedForeground = isSubagent && !allowSubagentBg;
|
|
43786
|
+
if (requestedPty && subagentForcedForeground) {
|
|
43787
|
+
throw new Error("pty:true is unavailable in this subagent session because bash.subagent_background is false; run the command without pty.");
|
|
43788
|
+
}
|
|
43757
43789
|
const blockToCompletion = subagentForcedForeground || backgroundDisabled || requestedWait;
|
|
43758
43790
|
const effectiveBackground = blockToCompletion ? false : requestedBackground;
|
|
43759
43791
|
const rawTimeout = coerceOptionalInt(args.timeout, "timeout", 1, Number.MAX_SAFE_INTEGER);
|
|
43792
|
+
const workerUnboundedWait = isSubagent && requestedWait && rawTimeout === undefined;
|
|
43760
43793
|
const ptyRows = coerceOptionalInt(args.ptyRows, "ptyRows", 1, 60);
|
|
43761
43794
|
const ptyCols = coerceOptionalInt(args.ptyCols, "ptyCols", 1, 140);
|
|
43762
43795
|
const compressed = coerceBoolean(args.compressed, true);
|
|
@@ -43787,7 +43820,7 @@ function createBashTool(ctx, aftSearchRegisteredOverride) {
|
|
|
43787
43820
|
wait: requestedWait,
|
|
43788
43821
|
sandbox: args.sandbox
|
|
43789
43822
|
}, callBashBridge, {
|
|
43790
|
-
transportTimeoutMs: orchestratedTransportTimeoutMs(blockToCompletion, requestedWait, rawTimeout, foregroundWaitMs),
|
|
43823
|
+
transportTimeoutMs: orchestratedTransportTimeoutMs(blockToCompletion, requestedWait, rawTimeout, foregroundWaitMs, workerUnboundedWait),
|
|
43791
43824
|
onProgress: ({ text }) => {
|
|
43792
43825
|
accumulatedOutput = preview(accumulatedOutput + text);
|
|
43793
43826
|
metadata?.({ output: accumulatedOutput, description });
|
|
@@ -43797,10 +43830,10 @@ function createBashTool(ctx, aftSearchRegisteredOverride) {
|
|
|
43797
43830
|
const fallbackCause = classifyBashHostFallbackError(error);
|
|
43798
43831
|
if (!bashCfg.host_fallback || fallbackCause === undefined)
|
|
43799
43832
|
throw error;
|
|
43800
|
-
if (rawRequestedBackground) {
|
|
43833
|
+
if (!backgroundDisabled && rawRequestedBackground) {
|
|
43801
43834
|
throw new Error(`${BASH_HOST_FALLBACK_REFUSAL}; background:true is unsupported.`);
|
|
43802
43835
|
}
|
|
43803
|
-
if (
|
|
43836
|
+
if (requestedPty) {
|
|
43804
43837
|
throw new Error(`${BASH_HOST_FALLBACK_REFUSAL}; pty:true is unsupported.`);
|
|
43805
43838
|
}
|
|
43806
43839
|
const projectRoot = projectRootFor(context);
|
|
@@ -43834,7 +43867,7 @@ function createBashTool(ctx, aftSearchRegisteredOverride) {
|
|
|
43834
43867
|
trackBgTask(context.sessionID, taskId);
|
|
43835
43868
|
let rendered = data.output ?? "";
|
|
43836
43869
|
if (isSubagent && allowSubagentBg) {
|
|
43837
|
-
rendered +=
|
|
43870
|
+
rendered += workerBackgroundTaskNote(taskId);
|
|
43838
43871
|
}
|
|
43839
43872
|
const metadataPayload = { description, output: rendered, status: "running", taskId };
|
|
43840
43873
|
metadata?.(metadataPayload);
|
|
@@ -43853,17 +43886,19 @@ function createBashTool(ctx, aftSearchRegisteredOverride) {
|
|
|
43853
43886
|
};
|
|
43854
43887
|
}
|
|
43855
43888
|
function createBashStatusTool(ctx) {
|
|
43889
|
+
const waitSteer = bashCompanionRegistered(ctx.config, "bash_watch") ? " To wait, use bash_watch." : "";
|
|
43856
43890
|
return {
|
|
43857
|
-
description:
|
|
43891
|
+
description: `Read-only snapshot of a background or PTY bash task's current state and output. Returns immediately. Never waits. One look to check on a task is fine — never loop it to wait for completion.${waitSteer}`,
|
|
43858
43892
|
args: {
|
|
43859
|
-
taskId:
|
|
43860
|
-
outputMode:
|
|
43893
|
+
taskId: z3.string().describe("Background task ID returned by bash({ background: true }), e.g. bash-6b454047a1c39ded."),
|
|
43894
|
+
outputMode: z3.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.")
|
|
43861
43895
|
},
|
|
43862
43896
|
execute: async (args, context) => {
|
|
43863
43897
|
const taskId = args.taskId;
|
|
43864
43898
|
const outputMode = args.outputMode;
|
|
43865
43899
|
const data = await bashStatusSnapshot(ctx, context, taskId, outputMode);
|
|
43866
|
-
|
|
43900
|
+
const isSubagent = await resolveIsSubagent(ctx.client, context.sessionID, context.directory);
|
|
43901
|
+
return await formatBashStatusText(context, taskId, data, outputMode, isSubagent ? "worker" : "primary");
|
|
43867
43902
|
}
|
|
43868
43903
|
};
|
|
43869
43904
|
}
|
|
@@ -43871,7 +43906,7 @@ function createBashKillTool(ctx) {
|
|
|
43871
43906
|
return {
|
|
43872
43907
|
description: "Terminate a running background bash task spawned with bash({ background: true }). Returns confirmation of kill or an error if the task already finished.",
|
|
43873
43908
|
args: {
|
|
43874
|
-
taskId:
|
|
43909
|
+
taskId: z3.string().describe("Background task ID returned by bash({ background: true }), e.g. bash-6b454047a1c39ded.")
|
|
43875
43910
|
},
|
|
43876
43911
|
execute: async (args, context) => {
|
|
43877
43912
|
const data = await callBashBridge(ctx, context, "bash_kill", {
|
|
@@ -43894,7 +43929,7 @@ async function bashStatusSnapshot(ctx, runtime, taskId, outputMode, options) {
|
|
|
43894
43929
|
}
|
|
43895
43930
|
return data;
|
|
43896
43931
|
}
|
|
43897
|
-
async function formatBashStatusText(runtime, taskId, data, requestedOutputMode) {
|
|
43932
|
+
async function formatBashStatusText(runtime, taskId, data, requestedOutputMode, role) {
|
|
43898
43933
|
const status = data.status;
|
|
43899
43934
|
const exit = typeof data.exit_code === "number" ? ` (exit ${data.exit_code})` : "";
|
|
43900
43935
|
const dur = typeof data.duration_ms === "number" ? ` ${Math.round(data.duration_ms / 1000)}s` : "";
|
|
@@ -43912,7 +43947,7 @@ ${preview}`;
|
|
|
43912
43947
|
}
|
|
43913
43948
|
if (status === "running") {
|
|
43914
43949
|
text += `
|
|
43915
|
-
|
|
43950
|
+
${runningTaskStatusHint(role)}`;
|
|
43916
43951
|
}
|
|
43917
43952
|
}
|
|
43918
43953
|
return text;
|
|
@@ -43941,11 +43976,6 @@ PTY task is still running. Use bash_status({ taskId: "${taskId}", outputMode: "s
|
|
|
43941
43976
|
function preview(output) {
|
|
43942
43977
|
return output.length <= METADATA_PREVIEW_LIMIT ? output : output.slice(-METADATA_PREVIEW_LIMIT);
|
|
43943
43978
|
}
|
|
43944
|
-
function subagentGuidance(taskId, watchSyncMaxMs) {
|
|
43945
|
-
return `
|
|
43946
|
-
|
|
43947
|
-
NOTE (subagent session): Continue with other work if you have it. If you don't, call bash_watch({ taskId: "${taskId}" }) to wait for completion before returning to the parent; without timeoutMs it waits up to ${watchSyncMaxMs} ms, the maximum. Subagents don't survive turn-end and won't receive the completion reminder.`;
|
|
43948
|
-
}
|
|
43949
43979
|
function foregroundMetadata(description, data, rendered) {
|
|
43950
43980
|
const outputPath = data.output_path;
|
|
43951
43981
|
const truncated = typeof data.truncated === "boolean" ? data.truncated : data.output_truncated;
|
|
@@ -43966,24 +43996,355 @@ function shortenCommand(command) {
|
|
|
43966
43996
|
return collapsed.length <= 80 ? collapsed : `${collapsed.slice(0, 77)}...`;
|
|
43967
43997
|
}
|
|
43968
43998
|
|
|
43999
|
+
// src/tools/conflicts.ts
|
|
44000
|
+
var z4 = tool.schema;
|
|
44001
|
+
function conflictTools(ctx) {
|
|
44002
|
+
return {
|
|
44003
|
+
aft_conflicts: {
|
|
44004
|
+
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).",
|
|
44005
|
+
args: {
|
|
44006
|
+
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()
|
|
44007
|
+
},
|
|
44008
|
+
execute: async (args, context) => {
|
|
44009
|
+
const rawArgs = {};
|
|
44010
|
+
if (!isEmptyParam(args?.path)) {
|
|
44011
|
+
const expanded = expandTilde2(String(args.path));
|
|
44012
|
+
const projectRoot = await resolveProjectRoot(ctx, context);
|
|
44013
|
+
const resolved = resolvePathFromProjectRoot(projectRoot, expanded);
|
|
44014
|
+
const denied = await assertExternalDirectoryPermission(ctx, context, resolved, {
|
|
44015
|
+
kind: "directory"
|
|
44016
|
+
});
|
|
44017
|
+
if (denied)
|
|
44018
|
+
return permissionDeniedResponse(denied);
|
|
44019
|
+
rawArgs.path = resolved;
|
|
44020
|
+
}
|
|
44021
|
+
const response = await callToolCall(ctx, context, "conflicts", rawArgs);
|
|
44022
|
+
if (response.success === false) {
|
|
44023
|
+
throw new Error(response.message || "git_conflicts failed");
|
|
44024
|
+
}
|
|
44025
|
+
return response.text;
|
|
44026
|
+
}
|
|
44027
|
+
}
|
|
44028
|
+
};
|
|
44029
|
+
}
|
|
44030
|
+
|
|
44031
|
+
// src/tools/definitions/v2.ts
|
|
44032
|
+
import { Effect as Effect3 } from "effect";
|
|
44033
|
+
var V2_TOOL_NAME = /^[A-Za-z][A-Za-z0-9_-]{0,63}$/;
|
|
44034
|
+
var V2_PATH_HEADER_TOOLS = new Set(["read", "write", "edit"]);
|
|
44035
|
+
var V2_BASH_TOOLS = new Set(["bash"]);
|
|
44036
|
+
var V2_SHELL_PERMISSION = "shell";
|
|
44037
|
+
var ROOT_COMBINATORS = ["anyOf", "oneOf", "allOf"];
|
|
44038
|
+
function failure(error) {
|
|
44039
|
+
if (error instanceof Error)
|
|
44040
|
+
return error;
|
|
44041
|
+
const detail = (() => {
|
|
44042
|
+
try {
|
|
44043
|
+
return JSON.stringify(error) ?? String(error);
|
|
44044
|
+
} catch {
|
|
44045
|
+
return String(error);
|
|
44046
|
+
}
|
|
44047
|
+
})();
|
|
44048
|
+
return new Error(`V2 tool execution rejected with a non-Error value: ${detail}`);
|
|
44049
|
+
}
|
|
44050
|
+
function isPlainObject3(value) {
|
|
44051
|
+
if (typeof value !== "object" || value === null)
|
|
44052
|
+
return false;
|
|
44053
|
+
const prototype = Object.getPrototypeOf(value);
|
|
44054
|
+
return prototype === Object.prototype || prototype === null;
|
|
44055
|
+
}
|
|
44056
|
+
function jsonSafeMetadata(value) {
|
|
44057
|
+
if (Array.isArray(value)) {
|
|
44058
|
+
return value.map((entry) => entry === undefined ? null : jsonSafeMetadata(entry));
|
|
44059
|
+
}
|
|
44060
|
+
if (!isPlainObject3(value))
|
|
44061
|
+
return value;
|
|
44062
|
+
const cleaned = {};
|
|
44063
|
+
for (const [key, entry] of Object.entries(value)) {
|
|
44064
|
+
if (entry === undefined)
|
|
44065
|
+
continue;
|
|
44066
|
+
cleaned[key] = jsonSafeMetadata(entry);
|
|
44067
|
+
}
|
|
44068
|
+
return cleaned;
|
|
44069
|
+
}
|
|
44070
|
+
function resultContent(result) {
|
|
44071
|
+
if (typeof result === "string")
|
|
44072
|
+
return { content: result };
|
|
44073
|
+
const attachments = (result.attachments ?? []).map((attachment) => ({
|
|
44074
|
+
type: "file",
|
|
44075
|
+
uri: attachment.url,
|
|
44076
|
+
mime: attachment.mime,
|
|
44077
|
+
...attachment.filename ? { name: attachment.filename } : {}
|
|
44078
|
+
}));
|
|
44079
|
+
const content = attachments.length ? [{ type: "text", text: result.output }, ...attachments] : result.output;
|
|
44080
|
+
const metadata = jsonSafeMetadata({
|
|
44081
|
+
...result.metadata ?? {},
|
|
44082
|
+
...result.title ? { title: result.title } : {}
|
|
44083
|
+
});
|
|
44084
|
+
return {
|
|
44085
|
+
content,
|
|
44086
|
+
...Object.keys(metadata).length ? { metadata } : {}
|
|
44087
|
+
};
|
|
44088
|
+
}
|
|
44089
|
+
function failureEnvelopeMessage(result) {
|
|
44090
|
+
const output = typeof result === "string" ? result : result.output;
|
|
44091
|
+
if (typeof output !== "string")
|
|
44092
|
+
return;
|
|
44093
|
+
const trimmed = output.trim();
|
|
44094
|
+
if (!trimmed.startsWith("{") || !trimmed.includes('"success"'))
|
|
44095
|
+
return;
|
|
44096
|
+
let parsed;
|
|
44097
|
+
try {
|
|
44098
|
+
parsed = JSON.parse(trimmed);
|
|
44099
|
+
} catch {
|
|
44100
|
+
return;
|
|
44101
|
+
}
|
|
44102
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed))
|
|
44103
|
+
return;
|
|
44104
|
+
const envelope = parsed;
|
|
44105
|
+
if (envelope.success !== false)
|
|
44106
|
+
return;
|
|
44107
|
+
for (const field of [envelope.message, envelope.error, envelope.code]) {
|
|
44108
|
+
if (typeof field === "string" && field.length > 0)
|
|
44109
|
+
return field;
|
|
44110
|
+
}
|
|
44111
|
+
return "The tool call failed.";
|
|
44112
|
+
}
|
|
44113
|
+
function bareToolName3(name) {
|
|
44114
|
+
return name.startsWith("aft_") ? name.slice(4) : name;
|
|
44115
|
+
}
|
|
44116
|
+
function projectArguments(name, definition) {
|
|
44117
|
+
if (!V2_PATH_HEADER_TOOLS.has(bareToolName3(name)) || !("filePath" in definition.args)) {
|
|
44118
|
+
return { ...definition.args };
|
|
44119
|
+
}
|
|
44120
|
+
const { filePath, ...rest } = definition.args;
|
|
44121
|
+
return { path: filePath, ...rest };
|
|
44122
|
+
}
|
|
44123
|
+
function executionArguments(name, input) {
|
|
44124
|
+
if (!V2_PATH_HEADER_TOOLS.has(bareToolName3(name)) || !("path" in input))
|
|
44125
|
+
return input;
|
|
44126
|
+
const { path, ...rest } = input;
|
|
44127
|
+
return { filePath: path, ...rest };
|
|
44128
|
+
}
|
|
44129
|
+
function hostPermission(name) {
|
|
44130
|
+
const bare = bareToolName3(name);
|
|
44131
|
+
if (new Set(["read", "glob", "grep"]).has(bare))
|
|
44132
|
+
return bare;
|
|
44133
|
+
if (V2_BASH_TOOLS.has(name))
|
|
44134
|
+
return V2_SHELL_PERMISSION;
|
|
44135
|
+
if (new Set([
|
|
44136
|
+
"write",
|
|
44137
|
+
"edit",
|
|
44138
|
+
"apply_patch",
|
|
44139
|
+
"delete",
|
|
44140
|
+
"move",
|
|
44141
|
+
"ast_grep_replace",
|
|
44142
|
+
"import",
|
|
44143
|
+
"safety"
|
|
44144
|
+
]).has(bare)) {
|
|
44145
|
+
return "edit";
|
|
44146
|
+
}
|
|
44147
|
+
return;
|
|
44148
|
+
}
|
|
44149
|
+
function assertV2Contract(name, input) {
|
|
44150
|
+
if (!V2_TOOL_NAME.test(name)) {
|
|
44151
|
+
throw new Error(`Invalid V2 tool name ${JSON.stringify(name)}; expected ${V2_TOOL_NAME.source}`);
|
|
44152
|
+
}
|
|
44153
|
+
const schema = tool.schema.toJSONSchema(input, { io: "input" });
|
|
44154
|
+
if (schema.type !== "object") {
|
|
44155
|
+
throw new Error(`V2 tool ${name} must have an object-rooted input schema`);
|
|
44156
|
+
}
|
|
44157
|
+
for (const keyword of ROOT_COMBINATORS) {
|
|
44158
|
+
if (keyword in schema) {
|
|
44159
|
+
throw new Error(`V2 tool ${name} input schema cannot use root ${keyword}`);
|
|
44160
|
+
}
|
|
44161
|
+
}
|
|
44162
|
+
}
|
|
44163
|
+
function v2PermissionRequest(name, request) {
|
|
44164
|
+
if (!V2_BASH_TOOLS.has(name) || request.permission !== "bash")
|
|
44165
|
+
return request;
|
|
44166
|
+
return { ...request, permission: V2_SHELL_PERMISSION };
|
|
44167
|
+
}
|
|
44168
|
+
function runtimeFor(name, location, context, signal, consumers) {
|
|
44169
|
+
const directory = location.directory;
|
|
44170
|
+
const worktree = location.project?.canonical ?? location.project?.directory ?? directory;
|
|
44171
|
+
return {
|
|
44172
|
+
sessionID: context.sessionID,
|
|
44173
|
+
messageID: context.messageID,
|
|
44174
|
+
agent: context.agent,
|
|
44175
|
+
directory,
|
|
44176
|
+
directoryIsSessionRoot: true,
|
|
44177
|
+
worktree,
|
|
44178
|
+
abort: signal,
|
|
44179
|
+
effectAbort: signal,
|
|
44180
|
+
metadata: (update) => {
|
|
44181
|
+
Effect3.runPromise(context.progress(jsonSafeMetadata({
|
|
44182
|
+
...update.metadata ?? {},
|
|
44183
|
+
...update.title ? { title: update.title } : {}
|
|
44184
|
+
})));
|
|
44185
|
+
},
|
|
44186
|
+
ask: (asked) => {
|
|
44187
|
+
const request = v2PermissionRequest(name, asked);
|
|
44188
|
+
if (consumers.requestPermission)
|
|
44189
|
+
return consumers.requestPermission(request, context);
|
|
44190
|
+
return Promise.reject(new Error(`The "${request.permission}" operation was refused because this AFT runtime has no permission evaluator bound, so the host's permission rules for it could not be consulted.`));
|
|
44191
|
+
},
|
|
44192
|
+
progress: context.progress
|
|
44193
|
+
};
|
|
44194
|
+
}
|
|
44195
|
+
function projectV2Tool(name, definition, location, consumers = {}) {
|
|
44196
|
+
const input = tool.schema.object(projectArguments(name, definition));
|
|
44197
|
+
assertV2Contract(name, input);
|
|
44198
|
+
const sharedOptions = definition.options;
|
|
44199
|
+
const permission = hostPermission(name);
|
|
44200
|
+
return {
|
|
44201
|
+
name,
|
|
44202
|
+
description: definition.description,
|
|
44203
|
+
input,
|
|
44204
|
+
options: {
|
|
44205
|
+
...sharedOptions,
|
|
44206
|
+
...permission ? { permission } : {},
|
|
44207
|
+
codemode: false
|
|
44208
|
+
},
|
|
44209
|
+
execute: (rawInput, context) => Effect3.tryPromise({
|
|
44210
|
+
try: async (signal) => {
|
|
44211
|
+
const runtime = runtimeFor(name, location, context, signal, consumers);
|
|
44212
|
+
const input = executionArguments(name, rawInput);
|
|
44213
|
+
const result = consumers.executeBash && V2_BASH_TOOLS.has(name) ? await consumers.executeBash({
|
|
44214
|
+
name,
|
|
44215
|
+
input,
|
|
44216
|
+
context: runtime,
|
|
44217
|
+
definition
|
|
44218
|
+
}) : await definition.execute(input, runtime);
|
|
44219
|
+
const refusal = failureEnvelopeMessage(result);
|
|
44220
|
+
if (refusal)
|
|
44221
|
+
throw new Error(refusal);
|
|
44222
|
+
return resultContent(result);
|
|
44223
|
+
},
|
|
44224
|
+
catch: failure
|
|
44225
|
+
})
|
|
44226
|
+
};
|
|
44227
|
+
}
|
|
44228
|
+
|
|
44229
|
+
// src/tools/hoisted.ts
|
|
44230
|
+
import { stat as stat2 } from "node:fs/promises";
|
|
44231
|
+
import * as path4 from "node:path";
|
|
44232
|
+
|
|
44233
|
+
// src/hashline-tag-sources.ts
|
|
44234
|
+
function hashlineTagSourceSentence(bashRewritesMintTags, isRegistered = () => true) {
|
|
44235
|
+
const rewrites = bashRewritesMintTags ? " (and accepted AFT `cat`/`head`/`tail` rewrites)" : "";
|
|
44236
|
+
const lookAlikes = [
|
|
44237
|
+
...["aft_zoom", "aft_outline"].filter(isRegistered),
|
|
44238
|
+
"grep",
|
|
44239
|
+
...isRegistered("aft_search") ? ["aft_search"] : []
|
|
44240
|
+
].map((name) => `\`${name}\``);
|
|
44241
|
+
lookAlikes.push("conflict snippets");
|
|
44242
|
+
return `Only \`read\`${rewrites} mint hashline tags. ${joinList(lookAlikes)} do not. After navigation, call \`read\` on every file and range the patch addresses.`;
|
|
44243
|
+
}
|
|
44244
|
+
function joinList(items) {
|
|
44245
|
+
if (items.length <= 2)
|
|
44246
|
+
return items.join(" and ");
|
|
44247
|
+
return `${items.slice(0, -1).join(", ")}, and ${items[items.length - 1]}`;
|
|
44248
|
+
}
|
|
44249
|
+
|
|
44250
|
+
// src/shared/read-vision.ts
|
|
44251
|
+
var VISION_HOST_TIMEOUT_MS = 1500;
|
|
44252
|
+
var states = new WeakMap;
|
|
44253
|
+
function stateFor2(client) {
|
|
44254
|
+
let state = states.get(client);
|
|
44255
|
+
if (!state) {
|
|
44256
|
+
state = { models: new Map, history: new Map, capabilities: new Map };
|
|
44257
|
+
states.set(client, state);
|
|
44258
|
+
}
|
|
44259
|
+
return state;
|
|
44260
|
+
}
|
|
44261
|
+
function rememberReadModel(client, sessionID, model) {
|
|
44262
|
+
stateFor2(client).models.set(sessionID, model);
|
|
44263
|
+
}
|
|
44264
|
+
async function bounded(call) {
|
|
44265
|
+
let timer;
|
|
44266
|
+
try {
|
|
44267
|
+
return await Promise.race([
|
|
44268
|
+
Promise.resolve().then(call),
|
|
44269
|
+
new Promise((resolve) => {
|
|
44270
|
+
timer = setTimeout(() => resolve(undefined), VISION_HOST_TIMEOUT_MS);
|
|
44271
|
+
})
|
|
44272
|
+
]);
|
|
44273
|
+
} catch {
|
|
44274
|
+
return;
|
|
44275
|
+
} finally {
|
|
44276
|
+
clearTimeout(timer);
|
|
44277
|
+
}
|
|
44278
|
+
}
|
|
44279
|
+
function record3(value) {
|
|
44280
|
+
return typeof value === "object" && value !== null && !Array.isArray(value) ? value : undefined;
|
|
44281
|
+
}
|
|
44282
|
+
async function currentSessionVisionCapability(client, sessionID) {
|
|
44283
|
+
if (!sessionID)
|
|
44284
|
+
return;
|
|
44285
|
+
const state = stateFor2(client);
|
|
44286
|
+
let model = state.models.get(sessionID);
|
|
44287
|
+
if (!model) {
|
|
44288
|
+
let pending = state.history.get(sessionID);
|
|
44289
|
+
if (!pending) {
|
|
44290
|
+
pending = bounded(() => resolvePromptContext(client, sessionID)).then((context) => context?.model);
|
|
44291
|
+
state.history.set(sessionID, pending);
|
|
44292
|
+
pending.finally(() => state.history.delete(sessionID));
|
|
44293
|
+
}
|
|
44294
|
+
const historical = await pending;
|
|
44295
|
+
model = state.models.get(sessionID) ?? historical;
|
|
44296
|
+
}
|
|
44297
|
+
if (!model)
|
|
44298
|
+
return;
|
|
44299
|
+
const currentModel = model;
|
|
44300
|
+
const key = JSON.stringify([model.providerID, model.modelID]);
|
|
44301
|
+
let capability = state.capabilities.get(key);
|
|
44302
|
+
if (!capability) {
|
|
44303
|
+
capability = (async () => {
|
|
44304
|
+
const api = client.provider;
|
|
44305
|
+
if (!api?.list)
|
|
44306
|
+
return;
|
|
44307
|
+
const list = api.list.bind(api);
|
|
44308
|
+
const listed = record3(await bounded(list));
|
|
44309
|
+
const catalog = record3(listed?.data) ?? listed;
|
|
44310
|
+
const providers = catalog?.all ?? catalog?.providers;
|
|
44311
|
+
if (!Array.isArray(providers))
|
|
44312
|
+
return;
|
|
44313
|
+
const provider = providers.map(record3).find((entry) => entry?.id === currentModel.providerID);
|
|
44314
|
+
const models = provider?.models;
|
|
44315
|
+
const entry = record3(Array.isArray(models) ? models.find((entry) => record3(entry)?.id === currentModel.modelID) : record3(models)?.[currentModel.modelID]);
|
|
44316
|
+
const inputs = record3(entry?.modalities)?.input;
|
|
44317
|
+
return Array.isArray(inputs) ? inputs.includes("image") : typeof entry?.attachment === "boolean" ? entry.attachment : undefined;
|
|
44318
|
+
})();
|
|
44319
|
+
state.capabilities.set(key, capability);
|
|
44320
|
+
capability.then((value) => {
|
|
44321
|
+
if (value === undefined)
|
|
44322
|
+
state.capabilities.delete(key);
|
|
44323
|
+
});
|
|
44324
|
+
}
|
|
44325
|
+
return capability;
|
|
44326
|
+
}
|
|
44327
|
+
|
|
43969
44328
|
// src/tools/bash_watch.ts
|
|
43970
44329
|
var z5 = tool.schema;
|
|
43971
|
-
var BASH_WAIT_POLL_INTERVAL_MS = 100;
|
|
43972
44330
|
var REGEX_WAIT_SCAN_WINDOW_BYTES = 64 * 1024;
|
|
43973
44331
|
function unavailableSnapshot() {
|
|
43974
44332
|
return { status: "unknown" };
|
|
43975
44333
|
}
|
|
43976
|
-
function coerceConfiguredWatchTimeout(value, cap) {
|
|
44334
|
+
function coerceConfiguredWatchTimeout(value, role, cap) {
|
|
43977
44335
|
try {
|
|
43978
|
-
return coerceOptionalInt(value, "timeoutMs", 1, cap);
|
|
44336
|
+
return coerceOptionalInt(value, "timeoutMs", 1, maxWatchTimeoutMs(role, cap));
|
|
43979
44337
|
} catch (error) {
|
|
43980
44338
|
const message = error instanceof Error ? error.message : String(error);
|
|
44339
|
+
if (role === "worker")
|
|
44340
|
+
throw error;
|
|
43981
44341
|
throw new Error(`${message} (bash.watch_sync_max_ms)`);
|
|
43982
44342
|
}
|
|
43983
44343
|
}
|
|
43984
44344
|
function createBashWatchTool(ctx) {
|
|
44345
|
+
const noPolling = bashCompanionRegistered(ctx.config, "bash_status") ? " Never loop bash_status to wait." : "";
|
|
43985
44346
|
return {
|
|
43986
|
-
description:
|
|
44347
|
+
description: `Watch a background bash task. ${WATCH_SYNC_DEFAULTS_DESCRIPTION}. In a main session sync waits are for a short remaining wait on a task; for anything longer end the turn on \`bash({background:true})\` and let the completion reminder wake you, or use \`bash({wait:true})\` when the result is needed before anything else. The user can interrupt anytime; the wait auto-converts to an async notification. Async (background:true, requires pattern) registers a non-blocking notification and returns immediately — use when you have parallel work or want to end your turn.${noPolling}`,
|
|
43987
44348
|
args: {
|
|
43988
44349
|
taskId: z5.string().describe("Background task ID returned by bash({ background: true })."),
|
|
43989
44350
|
pattern: z5.union([z5.string(), z5.object({ regex: z5.string() })]).optional().describe("Substring or regex pattern. Optional in sync mode; required with background:true. Sync substring watches keep only the overlap tail needed for boundary matches; sync regex watches use a 64 KB rolling output window."),
|
|
@@ -44034,11 +44395,11 @@ A notification will fire when the pattern matches or the task exits.`;
|
|
|
44034
44395
|
}
|
|
44035
44396
|
const syncWaitCap = bashCfg.watch_sync_max_ms;
|
|
44036
44397
|
const role = isSubagent ? "worker" : "primary";
|
|
44037
|
-
const effectiveWaitMs = subagentForcedSync ?
|
|
44398
|
+
const effectiveWaitMs = subagentForcedSync ? undefined : resolveWatchTimeoutMs(coerceConfiguredWatchTimeout(args.timeoutMs, role, syncWaitCap), role, syncWaitCap);
|
|
44038
44399
|
const data = await waitForBashStatus(ctx, context, taskId, undefined, waitFor, effectiveWaitMs);
|
|
44039
44400
|
const waited = data.waited;
|
|
44040
44401
|
if (waited?.reason === "user_message") {
|
|
44041
|
-
const convertedText = await convertToAsyncWatchOnAbort(ctx, context, taskId, waitFor, coerceBoolean(args.once, true), waited.elapsed_ms);
|
|
44402
|
+
const convertedText = await convertToAsyncWatchOnAbort(ctx, context, taskId, waitFor, coerceBoolean(args.once, true), waited.elapsed_ms, role);
|
|
44042
44403
|
const metadata = context.metadata;
|
|
44043
44404
|
metadata?.({ taskId, status: data.status, waited, convertedToAsync: true });
|
|
44044
44405
|
return convertedText;
|
|
@@ -44050,10 +44411,11 @@ A notification will fire when the pattern matches or the task exits.`;
|
|
|
44050
44411
|
}
|
|
44051
44412
|
};
|
|
44052
44413
|
}
|
|
44053
|
-
async function convertToAsyncWatchOnAbort(ctx, context, taskId, waitFor, once, elapsedMs) {
|
|
44414
|
+
async function convertToAsyncWatchOnAbort(ctx, context, taskId, waitFor, once, elapsedMs, role) {
|
|
44054
44415
|
const interrupted = `Sync watch for task ${taskId} was interrupted because you sent a message after ${elapsedMs}ms of waiting. `;
|
|
44416
|
+
const reminderFallback = interruptedWatchTail(role, `The task is still running in the background. A completion reminder will be ` + `delivered automatically when the task exits.`);
|
|
44055
44417
|
if (!waitFor) {
|
|
44056
|
-
return interrupted + `The task is still running in the background. A completion reminder will be ` + `delivered automatically when the task exits; don't poll bash_status
|
|
44418
|
+
return interrupted + interruptedWatchTail(role, `The task is still running in the background. A completion reminder will be ` + `delivered automatically when the task exits; don't poll bash_status.`);
|
|
44057
44419
|
}
|
|
44058
44420
|
const notifyParams = {
|
|
44059
44421
|
task_id: taskId,
|
|
@@ -44068,12 +44430,12 @@ async function convertToAsyncWatchOnAbort(ctx, context, taskId, waitFor, once, e
|
|
|
44068
44430
|
const registered = await callBashBridge(ctx, context, "bash_notify", notifyParams);
|
|
44069
44431
|
if (registered.success === false) {
|
|
44070
44432
|
unmarkExplicitControl(context.sessionID, taskId);
|
|
44071
|
-
return interrupted + `Auto-registering an async watch failed (${String(registered.message ?? "unknown error")}). ` +
|
|
44433
|
+
return interrupted + `Auto-registering an async watch failed (${String(registered.message ?? "unknown error")}). ` + reminderFallback;
|
|
44072
44434
|
}
|
|
44073
|
-
return interrupted + `The wait has been converted to an async watch (${registered.watch_id}). ` + `A notification will fire when the pattern matches or the task exits
|
|
44435
|
+
return interrupted + `The wait has been converted to an async watch (${registered.watch_id}). ` + interruptedWatchTail(role, `A notification will fire when the pattern matches or the task exits.`);
|
|
44074
44436
|
} catch (err) {
|
|
44075
44437
|
unmarkExplicitControl(context.sessionID, taskId);
|
|
44076
|
-
return interrupted + `Auto-registering an async watch failed (${err instanceof Error ? err.message : String(err)}). ` +
|
|
44438
|
+
return interrupted + `Auto-registering an async watch failed (${err instanceof Error ? err.message : String(err)}). ` + reminderFallback;
|
|
44077
44439
|
}
|
|
44078
44440
|
}
|
|
44079
44441
|
function formatWatchResultText(taskId, data, waited, role, syncWaitCap) {
|
|
@@ -44082,17 +44444,20 @@ function formatWatchResultText(taskId, data, waited, role, syncWaitCap) {
|
|
|
44082
44444
|
const dur = typeof data.duration_ms === "number" ? ` ${Math.round(data.duration_ms / 1000)}s` : "";
|
|
44083
44445
|
let text = `Task ${taskId}: ${status}${exit}${dur}`;
|
|
44084
44446
|
if (waited) {
|
|
44085
|
-
const waitedText = formatWatchWaited(waited.elapsed_ms, waited.limit_ms, syncWaitCap);
|
|
44447
|
+
const waitedText = formatWatchWaited(waited.elapsed_ms, waited.limit_ms, role === "primary" ? syncWaitCap : undefined);
|
|
44086
44448
|
if (waited.reason === "matched") {
|
|
44087
44449
|
const stream = waited.match_stream ? ` in ${waited.match_stream}` : "";
|
|
44088
44450
|
text += `
|
|
44089
44451
|
${waitedText}; matched ${JSON.stringify(waited.match ?? "")}${stream} at offset ${waited.match_offset ?? 0}.`;
|
|
44090
44452
|
} else if (waited.reason === "timeout") {
|
|
44091
44453
|
text += `
|
|
44092
|
-
${waitedText}; timeout reached without match. ${watchTimeoutSteer(role
|
|
44454
|
+
${waitedText}; timeout reached without match. ${watchTimeoutSteer(role)}`;
|
|
44093
44455
|
} else if (waited.reason === "unavailable") {
|
|
44094
44456
|
text += `
|
|
44095
|
-
${waitedText};
|
|
44457
|
+
${waitedText}; ${watchUnavailableSteer(role)}`;
|
|
44458
|
+
} else if (waited.reason === "aborted") {
|
|
44459
|
+
text += `
|
|
44460
|
+
${waitedText}; the watch was cancelled. The task keeps running.`;
|
|
44096
44461
|
} else {
|
|
44097
44462
|
const stat = String(data.status ?? "unknown");
|
|
44098
44463
|
const e = typeof data.exit_code === "number" ? `, exit ${data.exit_code}` : "";
|
|
@@ -44119,9 +44484,17 @@ async function bashStatusSnapshot2(ctx, runtime, taskId, outputMode, cursor, opt
|
|
|
44119
44484
|
return data;
|
|
44120
44485
|
}
|
|
44121
44486
|
async function waitForBashStatus(ctx, runtime, taskId, outputMode, waitFor, effectiveWaitMs) {
|
|
44122
|
-
const startedAt =
|
|
44123
|
-
const deadline = startedAt + effectiveWaitMs;
|
|
44124
|
-
const elapsedMs = () => Math.round(
|
|
44487
|
+
const startedAt = watchClock.now();
|
|
44488
|
+
const deadline = effectiveWaitMs === undefined ? Number.POSITIVE_INFINITY : startedAt + effectiveWaitMs;
|
|
44489
|
+
const elapsedMs = () => Math.round(watchClock.now() - startedAt);
|
|
44490
|
+
const abortSignal = runtime.abort;
|
|
44491
|
+
const pause = () => watchClock.sleep(Math.min(watchPollDelayMs(elapsedMs()), Math.max(0, deadline - watchClock.now())), abortSignal);
|
|
44492
|
+
const waited = (reason, extra = {}) => ({
|
|
44493
|
+
reason,
|
|
44494
|
+
elapsed_ms: elapsedMs(),
|
|
44495
|
+
...effectiveWaitMs === undefined ? {} : { limit_ms: effectiveWaitMs },
|
|
44496
|
+
...extra
|
|
44497
|
+
});
|
|
44125
44498
|
let spillCursor = { output: 0, stderr: 0 };
|
|
44126
44499
|
const scanState = {
|
|
44127
44500
|
output: { text: "", baseOffset: 0 },
|
|
@@ -44135,31 +44508,29 @@ async function waitForBashStatus(ctx, runtime, taskId, outputMode, waitFor, effe
|
|
|
44135
44508
|
markTaskWaiting(runtime.sessionID, taskId);
|
|
44136
44509
|
let sawTerminal = false;
|
|
44137
44510
|
let lastData;
|
|
44511
|
+
let busySince;
|
|
44138
44512
|
try {
|
|
44139
44513
|
while (true) {
|
|
44514
|
+
if (abortSignal?.aborted) {
|
|
44515
|
+
return withWaited(lastData ?? unavailableSnapshot(), waited("aborted"));
|
|
44516
|
+
}
|
|
44140
44517
|
let data;
|
|
44141
44518
|
try {
|
|
44142
44519
|
data = await bashStatusSnapshot2(ctx, runtime, taskId, outputMode, waitFor ? spillCursor : undefined, bridgeOptions);
|
|
44143
44520
|
} catch (err) {
|
|
44144
44521
|
if (!isBridgeTransportTimeout(err))
|
|
44145
44522
|
throw err;
|
|
44523
|
+
busySince ??= watchClock.now();
|
|
44146
44524
|
if (isSyncWatchAborted(runtime.sessionID)) {
|
|
44147
|
-
return withWaited(lastData ?? unavailableSnapshot(),
|
|
44148
|
-
reason: "user_message",
|
|
44149
|
-
elapsed_ms: elapsedMs(),
|
|
44150
|
-
limit_ms: effectiveWaitMs
|
|
44151
|
-
});
|
|
44525
|
+
return withWaited(lastData ?? unavailableSnapshot(), waited("user_message"));
|
|
44152
44526
|
}
|
|
44153
|
-
if (
|
|
44154
|
-
return withWaited(lastData ?? unavailableSnapshot(),
|
|
44155
|
-
reason: "unavailable",
|
|
44156
|
-
elapsed_ms: elapsedMs(),
|
|
44157
|
-
limit_ms: effectiveWaitMs
|
|
44158
|
-
});
|
|
44527
|
+
if (watchClock.now() >= deadline || watchClock.now() - busySince >= WATCH_UNAVAILABLE_GIVE_UP_MS) {
|
|
44528
|
+
return withWaited(lastData ?? unavailableSnapshot(), waited("unavailable"));
|
|
44159
44529
|
}
|
|
44160
|
-
await
|
|
44530
|
+
await pause();
|
|
44161
44531
|
continue;
|
|
44162
44532
|
}
|
|
44533
|
+
busySince = undefined;
|
|
44163
44534
|
lastData = data;
|
|
44164
44535
|
const terminal = isTerminalStatus(data.status);
|
|
44165
44536
|
if (waitFor) {
|
|
@@ -44184,14 +44555,11 @@ async function waitForBashStatus(ctx, runtime, taskId, outputMode, waitFor, effe
|
|
|
44184
44555
|
await markBgCompletionDelivered({ ctx, directory: projectRootFor(runtime), sessionID: runtime.sessionID }, taskId);
|
|
44185
44556
|
}
|
|
44186
44557
|
const matchStream = data.mode === "pty" ? undefined : chunk.stream === "output" ? "stdout" : "stderr";
|
|
44187
|
-
return withWaited(data, {
|
|
44188
|
-
reason: "matched",
|
|
44189
|
-
elapsed_ms: elapsedMs(),
|
|
44190
|
-
limit_ms: effectiveWaitMs,
|
|
44558
|
+
return withWaited(data, waited("matched", {
|
|
44191
44559
|
match: match.text,
|
|
44192
44560
|
match_offset: state.baseOffset + match.byteOffset,
|
|
44193
44561
|
match_stream: matchStream
|
|
44194
|
-
});
|
|
44562
|
+
}));
|
|
44195
44563
|
}
|
|
44196
44564
|
if (waitFor.kind === "substring") {
|
|
44197
44565
|
const trimmed = trimWaitScanBuffer(state.text, state.baseOffset, waitFor);
|
|
@@ -44205,27 +44573,15 @@ async function waitForBashStatus(ctx, runtime, taskId, outputMode, waitFor, effe
|
|
|
44205
44573
|
sawTerminal = true;
|
|
44206
44574
|
consumeBgCompletion(runtime.sessionID, taskId);
|
|
44207
44575
|
await markBgCompletionDelivered({ ctx, directory: projectRootFor(runtime), sessionID: runtime.sessionID }, taskId);
|
|
44208
|
-
return withWaited(data,
|
|
44209
|
-
reason: "exited",
|
|
44210
|
-
elapsed_ms: elapsedMs(),
|
|
44211
|
-
limit_ms: effectiveWaitMs
|
|
44212
|
-
});
|
|
44576
|
+
return withWaited(data, waited("exited"));
|
|
44213
44577
|
}
|
|
44214
44578
|
if (isSyncWatchAborted(runtime.sessionID)) {
|
|
44215
|
-
return withWaited(data,
|
|
44216
|
-
reason: "user_message",
|
|
44217
|
-
elapsed_ms: elapsedMs(),
|
|
44218
|
-
limit_ms: effectiveWaitMs
|
|
44219
|
-
});
|
|
44579
|
+
return withWaited(data, waited("user_message"));
|
|
44220
44580
|
}
|
|
44221
|
-
if (
|
|
44222
|
-
return withWaited(data,
|
|
44223
|
-
reason: "timeout",
|
|
44224
|
-
elapsed_ms: elapsedMs(),
|
|
44225
|
-
limit_ms: effectiveWaitMs
|
|
44226
|
-
});
|
|
44581
|
+
if (watchClock.now() >= deadline) {
|
|
44582
|
+
return withWaited(data, waited("timeout"));
|
|
44227
44583
|
}
|
|
44228
|
-
await
|
|
44584
|
+
await pause();
|
|
44229
44585
|
}
|
|
44230
44586
|
} finally {
|
|
44231
44587
|
if (!sawTerminal)
|
|
@@ -44335,8 +44691,9 @@ function withWaited(data, waited) {
|
|
|
44335
44691
|
// src/tools/bash_write.ts
|
|
44336
44692
|
var z6 = tool.schema;
|
|
44337
44693
|
function createBashWriteTool(ctx) {
|
|
44694
|
+
const modeCheck = bashCompanionRegistered(ctx.config, "bash_status") ? '; check bash_status reports mode: "pty" first.' : ".";
|
|
44338
44695
|
return {
|
|
44339
|
-
description:
|
|
44696
|
+
description: `Write input bytes to a running PTY bash task. PTY-only${modeCheck} ` + 'Input is either a string (verbatim bytes) or an array mixing strings and { key: "esc" | "enter" | "up" | "ctrl-c" | ... } objects ' + 'for atomic text+key sequences such as [ "iHello", { key: "esc" }, ":wq", { key: "enter" } ]. ' + "Named keys cover enter/return/tab/space/backspace/esc/escape, arrows, home/end/page-up/page-down/delete/insert, f1..f12, and ctrl-a..ctrl-z. " + "Maximum 1 MiB per call (post-expansion).",
|
|
44340
44697
|
args: {
|
|
44341
44698
|
taskId: z6.string().describe("Background PTY task ID returned by bash({ pty: true, background: true })."),
|
|
44342
44699
|
input: z6.union([
|
|
@@ -44604,25 +44961,19 @@ function createReadTool(ctx) {
|
|
|
44604
44961
|
const failure = classifyPermissionError(error);
|
|
44605
44962
|
return permissionDeniedResponse(failure.kind === "rule_denied" ? permissionRuleDenial("read") : failure.message);
|
|
44606
44963
|
}
|
|
44607
|
-
const
|
|
44608
|
-
const
|
|
44609
|
-
const
|
|
44610
|
-
const
|
|
44611
|
-
let startLine = rawStartLine;
|
|
44612
|
-
let endLine = rawEndLine;
|
|
44613
|
-
if (startLine === undefined && rawOffset !== undefined) {
|
|
44614
|
-
startLine = rawOffset;
|
|
44615
|
-
if (rawLimit !== undefined) {
|
|
44616
|
-
endLine = rawOffset + rawLimit - 1;
|
|
44617
|
-
}
|
|
44618
|
-
}
|
|
44964
|
+
const startLine = coerceOptionalInt(args.startLine, "startLine", 1, Number.MAX_SAFE_INTEGER);
|
|
44965
|
+
const endLine = coerceOptionalInt(args.endLine, "endLine", 1, Number.MAX_SAFE_INTEGER);
|
|
44966
|
+
const limit = coerceOptionalInt(args.limit, "limit", 1, Number.MAX_SAFE_INTEGER);
|
|
44967
|
+
const offset = coerceOptionalInt(args.offset, "offset", 1, Number.MAX_SAFE_INTEGER);
|
|
44619
44968
|
const rawArgs = { filePath: file };
|
|
44620
44969
|
if (startLine !== undefined)
|
|
44621
44970
|
rawArgs.startLine = startLine;
|
|
44622
44971
|
if (endLine !== undefined)
|
|
44623
44972
|
rawArgs.endLine = endLine;
|
|
44624
|
-
if (
|
|
44625
|
-
rawArgs.
|
|
44973
|
+
if (offset !== undefined)
|
|
44974
|
+
rawArgs.offset = offset;
|
|
44975
|
+
if (limit !== undefined)
|
|
44976
|
+
rawArgs.limit = limit;
|
|
44626
44977
|
const mediaCandidate = isGithubResourcePath(file) || /\.(png|jpe?g|gif|webp|pdf)$/i.test(filePath) && await stat2(filePath).then((info) => info.isFile(), () => false);
|
|
44627
44978
|
const visionCapability = mediaCandidate ? await measurePreStage("capability", () => currentSessionVisionCapability(ctx.client, context.sessionID)) : undefined;
|
|
44628
44979
|
if (visionCapability !== undefined)
|
|
@@ -44678,7 +45029,7 @@ function createWriteTool(ctx, editToolName = "edit") {
|
|
|
44678
45029
|
description: getWriteDescription(ctx, editToolName),
|
|
44679
45030
|
args: {
|
|
44680
45031
|
filePath: z7.string().describe("Path to the file to write (absolute or relative to project root)"),
|
|
44681
|
-
content: z7.string().describe("The full content to write to the file")
|
|
45032
|
+
content: z7.string().describe("The full content to write to the file; for issue://N / pr://N, the comment body to post")
|
|
44682
45033
|
},
|
|
44683
45034
|
execute: async (args, context) => {
|
|
44684
45035
|
const argsRecord = args;
|
|
@@ -44795,7 +45146,7 @@ ${backupBehavior}
|
|
|
44795
45146
|
- Symbol replace includes decorators, attributes, and doc comments in range
|
|
44796
45147
|
- Response is a compact server-rendered summary; before/after diff details are attached as UI metadata when available.`;
|
|
44797
45148
|
}
|
|
44798
|
-
var
|
|
45149
|
+
var HASHLINE_EDIT_GRAMMAR = [
|
|
44799
45150
|
"Apply a hashline patch. Arguments are exactly `{patch}` where `patch` is a non-empty string. Server-owned preview control is outside this schema.",
|
|
44800
45151
|
"",
|
|
44801
45152
|
"Quick reference:",
|
|
@@ -44803,13 +45154,18 @@ var HASHLINE_EDIT_DESCRIPTION = [
|
|
|
44803
45154
|
"- Same canonical path: multiple sections compose in patch order against pre-request coordinates.",
|
|
44804
45155
|
"- Addresses: `0` (BOF), `N` (one line), `N.=M` (range; `N..=M`/`N..M` also work), `<N`/`>N` (gap before/after), `N*`/`<N*`/`>N*` (block), and `$`/`$-K` (EOF-relative). A plain `N` PUT replaces; use `<N` or `>N` to insert.",
|
|
44805
45156
|
"- PUT text: `PUT <address>:` followed by one or more `+` body rows (`+` alone is blank). A final patch newline is allowed. PUT without `:` copies `@name` (or the anonymous register) and takes no body; names use `@` plus ASCII letters, digits, `_`, or `-`.",
|
|
44806
|
-
"- CUT: `CUT <address> [@name]`. REM: bare `REM` only, removing the whole file. MV: `MV <destination>` (one whitespace-free path, optional matching quotes), once and after any line operations. `*** Begin Patch`/`*** End Patch` is an optional envelope."
|
|
44807
|
-
"- Only `read` (and accepted AFT `cat`/`head`/`tail` rewrites) mint hashline tags. `aft_zoom`, `aft_outline`, `grep`, `aft_search`, and conflict snippets do not. After navigation, call `read` on every file and range the patch addresses."
|
|
45157
|
+
"- CUT: `CUT <address> [@name]`. REM: bare `REM` only, removing the whole file. MV: `MV <destination>` (one whitespace-free path, optional matching quotes), once and after any line operations. `*** Begin Patch`/`*** End Patch` is an optional envelope."
|
|
44808
45158
|
].join(`
|
|
44809
45159
|
`);
|
|
45160
|
+
function hashlineEditDescription(config) {
|
|
45161
|
+
const bashRewrites = toolEnabled(config, "bash") && resolveBashConfig(config).rewrite;
|
|
45162
|
+
const tagSources = hashlineTagSourceSentence(bashRewrites, (name) => toolEnabled(config, name));
|
|
45163
|
+
return `${HASHLINE_EDIT_GRAMMAR}
|
|
45164
|
+
- ${tagSources}`;
|
|
45165
|
+
}
|
|
44810
45166
|
function createHashlineEditTool(ctx) {
|
|
44811
45167
|
return {
|
|
44812
|
-
description:
|
|
45168
|
+
description: hashlineEditDescription(ctx.config),
|
|
44813
45169
|
args: {
|
|
44814
45170
|
patch: z7.string().min(1).describe("Hashline patch text with one or more [path#TAG] sections and PUT/CUT/REM/MV operations")
|
|
44815
45171
|
},
|
|
@@ -44884,8 +45240,7 @@ function createEditTool(ctx, writeToolName = "write") {
|
|
|
44884
45240
|
throw new Error("'path' parameter is required");
|
|
44885
45241
|
if (isGithubResourcePath(file)) {
|
|
44886
45242
|
const edits = argsRecord.edits;
|
|
44887
|
-
|
|
44888
|
-
if (!onlyFindReplace) {
|
|
45243
|
+
if (!isFindReplaceOnlyEdit(argsRecord)) {
|
|
44889
45244
|
throw new Error("edit: GitHub resources support only edits[] find/replace entries with oldString and optional newString");
|
|
44890
45245
|
}
|
|
44891
45246
|
const rawArgs = { path: file, edits };
|
|
@@ -45172,6 +45527,12 @@ function createMoveTool(ctx) {
|
|
|
45172
45527
|
};
|
|
45173
45528
|
}
|
|
45174
45529
|
function hoistedTools(ctx) {
|
|
45530
|
+
const backgroundCompanions = resolveBashConfig(ctx.config).background ? {
|
|
45531
|
+
bash_status: createBashStatusTool(ctx),
|
|
45532
|
+
bash_write: createBashWriteTool(ctx),
|
|
45533
|
+
bash_watch: createBashWatchTool(ctx),
|
|
45534
|
+
bash_kill: createBashKillTool(ctx)
|
|
45535
|
+
} : {};
|
|
45175
45536
|
const tools = {
|
|
45176
45537
|
read: createReadTool(ctx),
|
|
45177
45538
|
write: createWriteTool(ctx, "edit"),
|
|
@@ -45180,10 +45541,7 @@ function hoistedTools(ctx) {
|
|
|
45180
45541
|
aft_delete: createDeleteTool(ctx),
|
|
45181
45542
|
aft_move: createMoveTool(ctx),
|
|
45182
45543
|
bash: createBashTool(ctx),
|
|
45183
|
-
|
|
45184
|
-
bash_write: createBashWriteTool(ctx),
|
|
45185
|
-
bash_watch: createBashWatchTool(ctx),
|
|
45186
|
-
bash_kill: createBashKillTool(ctx)
|
|
45544
|
+
...backgroundCompanions
|
|
45187
45545
|
};
|
|
45188
45546
|
return prepareToolMap(tools, { hashlineEffective: ctx.hashlineEffective });
|
|
45189
45547
|
}
|
|
@@ -45579,6 +45937,9 @@ function navigationTools(ctx) {
|
|
|
45579
45937
|
|
|
45580
45938
|
// src/tools/reading.ts
|
|
45581
45939
|
var z11 = tool.schema;
|
|
45940
|
+
function isGithubPathArg(value) {
|
|
45941
|
+
return typeof value === "string" && (value.startsWith("issue://") || value.startsWith("pr://"));
|
|
45942
|
+
}
|
|
45582
45943
|
function buildZoomTitle(args) {
|
|
45583
45944
|
const targets = args.targets;
|
|
45584
45945
|
if (!isEmptyParam(targets)) {
|
|
@@ -45602,6 +45963,13 @@ function buildZoomTitle(args) {
|
|
|
45602
45963
|
}
|
|
45603
45964
|
return path || "(no target)";
|
|
45604
45965
|
}
|
|
45966
|
+
function outlineFocusSteer(config) {
|
|
45967
|
+
const zoomEnabled = toolEnabled(config, "aft_zoom");
|
|
45968
|
+
const reader = zoomEnabled ? "aft_zoom" : "read";
|
|
45969
|
+
const locate = toolEnabled(config, "aft_search") ? `aft_search + ${reader}` : reader;
|
|
45970
|
+
const callgraph = zoomEnabled ? toolEnabled(config, "aft_callgraph") ? " aft_zoom with `callgraph:true` gives one-level forward calls-out; use aft_callgraph only for reverse callers or multi-level traces." : " aft_zoom with `callgraph:true` gives one-level forward calls-out." : "";
|
|
45971
|
+
return `For understanding a specific feature, prefer ${locate} on named symbols; use aft_outline on a whole directory only for high-level structure mapping.${callgraph}`;
|
|
45972
|
+
}
|
|
45605
45973
|
function readingTools(ctx) {
|
|
45606
45974
|
const zoomEnabled = toolEnabled(ctx.config, "aft_zoom");
|
|
45607
45975
|
const ghReadEnabled = resolveGithubConfig(ctx.config).read;
|
|
@@ -45609,9 +45977,9 @@ function readingTools(ctx) {
|
|
|
45609
45977
|
const githubZoomDescription = whenGhReadEnabled(ghReadEnabled, "GitHub issue and pull-request discussion ordinals can be zoomed with `path: issue://…` or `path: pr://…` and `symbols`.");
|
|
45610
45978
|
const tools = prepareToolMap({
|
|
45611
45979
|
aft_outline: {
|
|
45612
|
-
description: "Structural outline of source code, documentation files, or remote URLs. For code, returns symbols (functions, classes, types) with line ranges. For Markdown and HTML, returns heading hierarchy. Use this to explore structure before reading specific sections with " + (zoomEnabled ? "aft_zoom" : "read") + ". With `files: true`, the outline is breadth-first with directory rollups; drill in by outlining a subdirectory. Rows show language, symbol count, and line count.\n\n" + (
|
|
45980
|
+
description: "Structural outline of source code, documentation files, or remote URLs. For code, returns symbols (functions, classes, types) with line ranges. For Markdown and HTML, returns heading hierarchy. Use this to explore structure before reading specific sections with " + (zoomEnabled ? "aft_zoom" : "read") + ". With `files: true`, the outline is breadth-first with directory rollups; drill in by outlining a subdirectory. Rows show language, symbol count, and line count.\n\n" + outlineFocusSteer(ctx.config) + `
|
|
45613
45981
|
|
|
45614
|
-
`
|
|
45982
|
+
` + "Pass a single `target`:\n" + ` • file path → outline that file (with signatures)
|
|
45615
45983
|
` + ` • directory path → outline source files under it
|
|
45616
45984
|
` + ` • URL (http:// or https://) → fetch and outline a remote HTML/Markdown document
|
|
45617
45985
|
` + " • array of paths → outline multiple files in one call; with files:true, every path must be a directory" + (githubOutlineDescription ? `
|
|
@@ -45660,7 +46028,7 @@ ${githubOutlineDescription}` : "") + "\n\nWhen a list is cut, the reply ends wit
|
|
|
45660
46028
|
}
|
|
45661
46029
|
},
|
|
45662
46030
|
aft_zoom: {
|
|
45663
|
-
description: "Inspect code symbols or documentation sections. For code, returns the full source of a symbol. Pass `callgraph: true` to also include call-graph annotations (calls-out / called-by within the same file). For Markdown and HTML, returns the section content under the given heading.\n\nUse
|
|
46031
|
+
description: "Inspect code symbols or documentation sections. For code, returns the full source of a symbol. Pass `callgraph: true` to also include call-graph annotations (calls-out / called-by within the same file). For Markdown and HTML, returns the section content under the given heading.\n\nUse `{ path, symbols }` or `{ url, symbols }` for one file/URL. `symbols` can be a string or array (one or many lookups in the same file/URL). Use `targets` for cross-file batches: `{ path, symbol }` or an array of them. Sending both merges them into one batch; a lookup that fails reports its own error line." + (githubZoomDescription ? `
|
|
45664
46032
|
|
|
45665
46033
|
${githubZoomDescription}` : ""),
|
|
45666
46034
|
args: {
|
|
@@ -45676,37 +46044,22 @@ ${githubZoomDescription}` : ""),
|
|
|
45676
46044
|
path: z11.string().describe("Path to file (absolute or relative to project root)"),
|
|
45677
46045
|
symbol: z11.string().describe("Symbol name in that file")
|
|
45678
46046
|
}))
|
|
45679
|
-
]).optional().describe("Cross-file batch: `{ path, symbol }` or an array of them.
|
|
46047
|
+
]).optional().describe("Cross-file batch: `{ path, symbol }` or an array of them. May be combined with path/url + symbols; all lookups are answered in one batch."),
|
|
45680
46048
|
contextLines: optionalInt(1, Number.MAX_SAFE_INTEGER).describe("Lines of context before/after the symbol (default: 3)"),
|
|
45681
46049
|
callgraph: z11.boolean().optional().describe("Include call-graph annotations (calls-out / called-by within the same file). Default false; off keeps zoom output minimal.")
|
|
45682
46050
|
},
|
|
45683
46051
|
execute: async (args, context) => {
|
|
45684
|
-
const hasTargetsProvided = (t) => {
|
|
45685
|
-
if (isEmptyParam(t))
|
|
45686
|
-
return false;
|
|
45687
|
-
const entryEmpty = (entry) => {
|
|
45688
|
-
if (!entry || typeof entry !== "object")
|
|
45689
|
-
return true;
|
|
45690
|
-
const fp = entry.path;
|
|
45691
|
-
const sym = entry.symbol;
|
|
45692
|
-
const fpEmpty = typeof fp !== "string" || fp.length === 0;
|
|
45693
|
-
const symEmpty = typeof sym !== "string" || sym.length === 0;
|
|
45694
|
-
return fpEmpty && symEmpty;
|
|
45695
|
-
};
|
|
45696
|
-
if (Array.isArray(t))
|
|
45697
|
-
return !t.every(entryEmpty);
|
|
45698
|
-
return !entryEmpty(t);
|
|
45699
|
-
};
|
|
45700
46052
|
const targetsInput = coerceJsonCollectionParam(args.targets, "targets");
|
|
45701
|
-
const
|
|
45702
|
-
const
|
|
45703
|
-
const
|
|
45704
|
-
const
|
|
46053
|
+
const targetEntries = usableZoomTargets(targetsInput);
|
|
46054
|
+
const hasFilePath = !isBlankParam(args.path);
|
|
46055
|
+
const hasUrl = !isBlankParam(args.url);
|
|
46056
|
+
const hasTargets = targetEntries.length > 0;
|
|
46057
|
+
const hasSymbols = !isBlankParam(args.symbols);
|
|
45705
46058
|
const wantCallgraph = coerceBoolean(args.callgraph);
|
|
45706
46059
|
const contextLines = coerceOptionalInt(args.contextLines, "contextLines", 1, Number.MAX_SAFE_INTEGER);
|
|
45707
46060
|
const zoomTitle = buildZoomTitle({
|
|
45708
46061
|
...args,
|
|
45709
|
-
targets:
|
|
46062
|
+
targets: hasTargets ? targetEntries : undefined
|
|
45710
46063
|
});
|
|
45711
46064
|
const zoomDisplay = { title: zoomTitle };
|
|
45712
46065
|
if (hasFilePath)
|
|
@@ -45717,7 +46070,7 @@ ${githubZoomDescription}` : ""),
|
|
|
45717
46070
|
zoomDisplay.symbols = typeof args.symbols === "string" ? args.symbols : JSON.stringify(args.symbols);
|
|
45718
46071
|
}
|
|
45719
46072
|
if (hasTargets)
|
|
45720
|
-
zoomDisplay.targets = JSON.stringify(
|
|
46073
|
+
zoomDisplay.targets = JSON.stringify(targetEntries);
|
|
45721
46074
|
if (contextLines !== undefined)
|
|
45722
46075
|
zoomDisplay.contextLines = contextLines;
|
|
45723
46076
|
if (wantCallgraph)
|
|
@@ -45728,23 +46081,24 @@ ${githubZoomDescription}` : ""),
|
|
|
45728
46081
|
metadata: zoomDisplay
|
|
45729
46082
|
});
|
|
45730
46083
|
if (hasTargets) {
|
|
45731
|
-
if (hasFilePath
|
|
45732
|
-
throw new Error("
|
|
46084
|
+
if (hasFilePath && hasUrl) {
|
|
46085
|
+
throw new Error("Provide exactly ONE of 'path' or 'url' — not both");
|
|
45733
46086
|
}
|
|
45734
|
-
const targets =
|
|
45735
|
-
|
|
45736
|
-
throw new Error("'targets' must be a non-empty object or array");
|
|
45737
|
-
}
|
|
45738
|
-
for (const [i, entry] of targets.entries()) {
|
|
45739
|
-
const targetPath = entry?.path;
|
|
45740
|
-
if (typeof targetPath !== "string" || targetPath.length === 0) {
|
|
46087
|
+
const targets = targetEntries.map((entry, i) => {
|
|
46088
|
+
if (!entry || typeof entry !== "object" || Array.isArray(entry)) {
|
|
45741
46089
|
throw new Error(`targets[${i}].path must be a non-empty string`);
|
|
45742
46090
|
}
|
|
45743
|
-
|
|
45744
|
-
|
|
45745
|
-
|
|
45746
|
-
|
|
45747
|
-
|
|
46091
|
+
const record = entry;
|
|
46092
|
+
const targetPath = isBlankParam(record.path) ? record.filePath : record.path;
|
|
46093
|
+
return {
|
|
46094
|
+
path: typeof targetPath === "string" ? targetPath : "",
|
|
46095
|
+
symbol: typeof record.symbol === "string" ? record.symbol : ""
|
|
46096
|
+
};
|
|
46097
|
+
});
|
|
46098
|
+
const localPaths = targets.map((target) => target.path).filter((path) => path.trim().length > 0 && !isGithubPathArg(path));
|
|
46099
|
+
if (hasFilePath && !isGithubPathArg(args.path))
|
|
46100
|
+
localPaths.push(args.path);
|
|
46101
|
+
const resolvedTargets = await Promise.all(localPaths.map((path) => resolvePathArg(ctx, context, path)));
|
|
45748
46102
|
const permissionDenied = await assertPathExternalPermissions(ctx, context, resolvedTargets);
|
|
45749
46103
|
if (permissionDenied)
|
|
45750
46104
|
return permissionDeniedResponse(permissionDenied);
|
|
@@ -45754,6 +46108,12 @@ ${githubZoomDescription}` : ""),
|
|
|
45754
46108
|
symbol: target.symbol
|
|
45755
46109
|
}))
|
|
45756
46110
|
};
|
|
46111
|
+
if (hasFilePath)
|
|
46112
|
+
rawArgs.filePath = args.path;
|
|
46113
|
+
else if (hasUrl)
|
|
46114
|
+
rawArgs.url = args.url;
|
|
46115
|
+
if (hasSymbols)
|
|
46116
|
+
rawArgs.symbols = args.symbols;
|
|
45757
46117
|
if (contextLines !== undefined)
|
|
45758
46118
|
rawArgs.contextLines = contextLines;
|
|
45759
46119
|
if (wantCallgraph)
|
|
@@ -45770,7 +46130,7 @@ ${githubZoomDescription}` : ""),
|
|
|
45770
46130
|
if (hasFilePath && hasUrl) {
|
|
45771
46131
|
throw new Error("Provide exactly ONE of 'path' or 'url' — not both");
|
|
45772
46132
|
}
|
|
45773
|
-
const githubPath = hasFilePath && (
|
|
46133
|
+
const githubPath = hasFilePath && isGithubPathArg(args.path) || hasUrl && isGithubPathArg(args.url);
|
|
45774
46134
|
if (!hasUrl && !githubPath) {
|
|
45775
46135
|
const file = await resolvePathArg(ctx, context, args.path);
|
|
45776
46136
|
const permissionDenied = await assertPathExternalPermissions(ctx, context, file);
|
|
@@ -45861,7 +46221,7 @@ function safetyTools(ctx) {
|
|
|
45861
46221
|
` + "Use checkpoint before risky multi-file changes. Use undo for quick single-file rollback.",
|
|
45862
46222
|
args: {
|
|
45863
46223
|
op: z12.enum(["undo", "history", "checkpoint", "restore", "list"]).describe("Safety operation"),
|
|
45864
|
-
path: z12.string().optional().describe("File path (required for history, optional for undo, optional for restore — restores only that file). Absolute or relative to project root"),
|
|
46224
|
+
path: z12.string().optional().describe("File path (required for history, optional for undo, optional for checkpoint — snapshots only that file when files is omitted, optional for restore — restores only that file). Absolute or relative to project root"),
|
|
45865
46225
|
name: z12.string().optional().describe("Checkpoint name (required for checkpoint, restore)"),
|
|
45866
46226
|
files: z12.array(z12.string()).optional().describe("Specific files to include in checkpoint, or to restore from a checkpoint (optional; restore without path/files restores the whole checkpoint; explicit checkpoint files may be untracked or gitignored)")
|
|
45867
46227
|
},
|
|
@@ -46297,8 +46657,20 @@ function buildAftToolDefinitions(ctx, config, onUnknownDisabled) {
|
|
|
46297
46657
|
const unknown = unknownDisabledTools(disabled);
|
|
46298
46658
|
if (unknown.length > 0)
|
|
46299
46659
|
onUnknownDisabled?.(unknown);
|
|
46660
|
+
describeBashForRegisteredSurface(allTools, config);
|
|
46300
46661
|
return allTools;
|
|
46301
46662
|
}
|
|
46663
|
+
function describeBashForRegisteredSurface(allTools, config) {
|
|
46664
|
+
const bash = allTools.bash;
|
|
46665
|
+
if (!bash)
|
|
46666
|
+
return;
|
|
46667
|
+
const bashCfg = resolveBashConfig(config);
|
|
46668
|
+
bash.description = bashToolDescription("aft_search" in allTools, bashCfg.compress, bashCfg.background, bashCfg.detach_on_user_message, "aft_zoom" in allTools, "bash_watch" in allTools, {
|
|
46669
|
+
outline: "aft_outline" in allTools,
|
|
46670
|
+
status: "bash_status" in allTools,
|
|
46671
|
+
write: "bash_write" in allTools
|
|
46672
|
+
});
|
|
46673
|
+
}
|
|
46302
46674
|
function registerAftTools(context, location, definitions, consumers = {}) {
|
|
46303
46675
|
const projected = Object.entries(definitions).map(([name, definition]) => projectV2Tool(name, definition, location, consumers));
|
|
46304
46676
|
return context.tool.transform((editor) => {
|
|
@@ -49009,7 +49381,10 @@ function registerShutdownCleanup(fn) {
|
|
|
49009
49381
|
|
|
49010
49382
|
// src/workflow-hints.ts
|
|
49011
49383
|
var HEADING = "## IMPORTANT NOTICE about your tools";
|
|
49012
|
-
|
|
49384
|
+
function hashlineTagSourceHint(bashRewritesMintTags, isRegistered = () => true) {
|
|
49385
|
+
return `**Hashline edit tags**: ${hashlineTagSourceSentence(bashRewritesMintTags, isRegistered)}`;
|
|
49386
|
+
}
|
|
49387
|
+
var HASHLINE_TAG_SOURCE_HINT = hashlineTagSourceHint(true);
|
|
49013
49388
|
function buildWorkflowHints(opts) {
|
|
49014
49389
|
const sections = [];
|
|
49015
49390
|
const grepName = "grep";
|
|
@@ -49026,6 +49401,8 @@ function buildWorkflowHints(opts) {
|
|
|
49026
49401
|
const hasInspect = !opts.disabledTools.has("aft_inspect");
|
|
49027
49402
|
const hasBash = !opts.disabledTools.has(bashName);
|
|
49028
49403
|
const hasBgBash = hasBash && opts.bashBackgroundEnabled && !opts.disabledTools.has(bashStatusName);
|
|
49404
|
+
const hasWatch = !opts.disabledTools.has("bash_watch");
|
|
49405
|
+
const hasWrite = !opts.disabledTools.has(bashWriteName);
|
|
49029
49406
|
if (hasBash && opts.bashCompressionEnabled) {
|
|
49030
49407
|
sections.push([
|
|
49031
49408
|
"**Test/build output**: bash output is auto-compressed for non-piped commands. Piped commands run verbatim and show the pipeline's output. For AFT's test/build summary, run the runner without filters:",
|
|
@@ -49067,17 +49444,19 @@ function buildWorkflowHints(opts) {
|
|
|
49067
49444
|
].join(`
|
|
49068
49445
|
`));
|
|
49069
49446
|
}
|
|
49070
|
-
if (
|
|
49447
|
+
if (hasBgBash && hasWatch) {
|
|
49071
49448
|
sections.push([
|
|
49072
49449
|
`**Long-running commands** (builds, installs, full test suites): run them in the FOREGROUND — use \`${bashName}({ command, wait: true })\` when you know it is long and need the result before anything else; if you send a new message, the wait detaches to background; otherwise omit \`wait\` so auto-promote can hand you a reminder while you work.`,
|
|
49073
49450
|
"- `background: true` is ONLY for when you have OTHER useful work to do while it runs: start it, do the other work, and the completion reminder delivers the result (or spawn a subagent for the side work). Do NOT background a command and then immediately `bash_watch` it — that spends a whole extra turn waiting for something foreground returns in one.",
|
|
49074
49451
|
"- `bash_watch` is for blocking on an ALREADY-backgrounded task once you've run out of parallel work (sync — the user can interrupt), or reacting to a specific early output line (async: background:true + pattern). Never loop `bash_status` to wait — it's a one-shot inspector."
|
|
49075
49452
|
].join(`
|
|
49076
49453
|
`));
|
|
49454
|
+
}
|
|
49455
|
+
if (hasBgBash && hasWrite) {
|
|
49077
49456
|
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: "..." })\`.`);
|
|
49078
49457
|
}
|
|
49079
49458
|
if (opts.hashlineEffective === true) {
|
|
49080
|
-
sections.push(
|
|
49459
|
+
sections.push(hashlineTagSourceHint(hasBash && opts.bashRewriteEnabled !== false, (name) => !opts.disabledTools.has(name)));
|
|
49081
49460
|
}
|
|
49082
49461
|
if (sections.length === 0) {
|
|
49083
49462
|
return null;
|
|
@@ -49092,13 +49471,32 @@ ${sections.join(`
|
|
|
49092
49471
|
`)}`;
|
|
49093
49472
|
}
|
|
49094
49473
|
function buildHintsFromConfig(config, disabledTools, hashlineEffective = false) {
|
|
49474
|
+
const bashCfg = resolveBashConfig(config);
|
|
49095
49475
|
return buildWorkflowHints({
|
|
49096
|
-
bashBackgroundEnabled:
|
|
49097
|
-
bashCompressionEnabled:
|
|
49476
|
+
bashBackgroundEnabled: bashCfg.background,
|
|
49477
|
+
bashCompressionEnabled: bashCfg.compress,
|
|
49478
|
+
bashRewriteEnabled: bashCfg.rewrite,
|
|
49098
49479
|
disabledTools,
|
|
49099
49480
|
hashlineEffective
|
|
49100
49481
|
});
|
|
49101
49482
|
}
|
|
49483
|
+
var WORKFLOW_HINT_TOOL_NAMES = [
|
|
49484
|
+
"aft_outline",
|
|
49485
|
+
"aft_zoom",
|
|
49486
|
+
"aft_search",
|
|
49487
|
+
"aft_callgraph",
|
|
49488
|
+
"aft_inspect",
|
|
49489
|
+
"grep",
|
|
49490
|
+
"read",
|
|
49491
|
+
"bash",
|
|
49492
|
+
"bash_status",
|
|
49493
|
+
"bash_watch",
|
|
49494
|
+
"bash_write"
|
|
49495
|
+
];
|
|
49496
|
+
function buildHintsForRegisteredTools(config, registeredTools, hashlineEffective = false) {
|
|
49497
|
+
const absent = new Set(WORKFLOW_HINT_TOOL_NAMES.filter((name) => !registeredTools.has(name)));
|
|
49498
|
+
return buildHintsFromConfig(config, absent, hashlineEffective);
|
|
49499
|
+
}
|
|
49102
49500
|
function appendHintsToSystem(system, hintsBlock) {
|
|
49103
49501
|
if (!hintsBlock)
|
|
49104
49502
|
return;
|
|
@@ -49469,36 +49867,13 @@ Install: ${getManualInstallHint()}`);
|
|
|
49469
49867
|
signal: autoUpdateAbort.signal,
|
|
49470
49868
|
storageDir: ctx.storageDir
|
|
49471
49869
|
});
|
|
49472
|
-
const HINTS_TOOL_NAMES = [
|
|
49473
|
-
"aft_outline",
|
|
49474
|
-
"aft_zoom",
|
|
49475
|
-
"aft_search",
|
|
49476
|
-
"aft_callgraph",
|
|
49477
|
-
"aft_inspect",
|
|
49478
|
-
"grep",
|
|
49479
|
-
"read",
|
|
49480
|
-
"bash",
|
|
49481
|
-
"bash_status"
|
|
49482
|
-
];
|
|
49483
49870
|
const registeredTools = new Set(Object.keys(allTools));
|
|
49484
49871
|
const { hashlineEditRegistered, aftSearchRegistered } = applyToolSurfaceOverrides(pool, aftConfig, registeredTools);
|
|
49485
49872
|
ctx.hashlineEffective = hashlineEditRegistered;
|
|
49486
49873
|
const hashlineDowngrade = reportHashlineDowngrade(aftConfig, registeredTools, (message) => deliverConfigMigrationWarnings(registrationRoot, [message]));
|
|
49487
49874
|
log2(`hashline activation decision requested=${aftConfig.edit_mode === "hashline"} ` + `edit_slot_survives=${hashlineEditRegistered} effective=${ctx.hashlineEffective}` + (hashlineDowngrade ? ` downgraded=${hashlineDowngrade.code}` : ""));
|
|
49488
49875
|
ctx.aftSearchRegistered = aftSearchRegistered;
|
|
49489
|
-
|
|
49490
|
-
const def = allTools[name];
|
|
49491
|
-
if (def) {
|
|
49492
|
-
const bashCfg = resolveBashConfig(aftConfig);
|
|
49493
|
-
def.description = bashToolDescription(aftSearchRegistered, bashCfg.compress, bashCfg.background, bashCfg.detach_on_user_message);
|
|
49494
|
-
}
|
|
49495
|
-
}
|
|
49496
|
-
const hintsAbsentTools = new Set;
|
|
49497
|
-
for (const name of HINTS_TOOL_NAMES) {
|
|
49498
|
-
if (!registeredTools.has(name))
|
|
49499
|
-
hintsAbsentTools.add(name);
|
|
49500
|
-
}
|
|
49501
|
-
const hintsBlock = buildHintsFromConfig(aftConfig, hintsAbsentTools, hashlineEditRegistered);
|
|
49876
|
+
const hintsBlock = buildHintsForRegisteredTools(aftConfig, registeredTools, hashlineEditRegistered);
|
|
49502
49877
|
if (hintsBlock) {
|
|
49503
49878
|
log2(`Workflow hints injected (${hintsBlock.length} chars)`);
|
|
49504
49879
|
}
|
|
@@ -49644,10 +50019,10 @@ Install: ${getManualInstallHint()}`);
|
|
|
49644
50019
|
var src_default = plugin;
|
|
49645
50020
|
|
|
49646
50021
|
// src/entry/server-runtime.mjs
|
|
49647
|
-
import { Effect as
|
|
50022
|
+
import { Effect as Effect9 } from "effect";
|
|
49648
50023
|
|
|
49649
50024
|
// src/rpc/register.ts
|
|
49650
|
-
import { Effect as
|
|
50025
|
+
import { Effect as Effect4 } from "effect";
|
|
49651
50026
|
|
|
49652
50027
|
// src/tui/aft-rpc.ts
|
|
49653
50028
|
import { Schema } from "effect";
|
|
@@ -49726,7 +50101,7 @@ function isStatusSubscribable(bridge) {
|
|
|
49726
50101
|
return typeof bridge.subscribeStatus === "function";
|
|
49727
50102
|
}
|
|
49728
50103
|
function locationRoot(location) {
|
|
49729
|
-
return location.
|
|
50104
|
+
return location.directory;
|
|
49730
50105
|
}
|
|
49731
50106
|
function placeholderStatus() {
|
|
49732
50107
|
return {
|
|
@@ -49743,7 +50118,7 @@ function restoreOwnProperty(target, key, descriptor) {
|
|
|
49743
50118
|
Reflect.deleteProperty(target, key);
|
|
49744
50119
|
}
|
|
49745
50120
|
function registerAftRpc(context, location, pool) {
|
|
49746
|
-
return
|
|
50121
|
+
return Effect4.gen(function* () {
|
|
49747
50122
|
const root = locationRoot(location);
|
|
49748
50123
|
const bridgeUnsubscribes = new Map;
|
|
49749
50124
|
let disposed = false;
|
|
@@ -49751,7 +50126,7 @@ function registerAftRpc(context, location, pool) {
|
|
|
49751
50126
|
const emit = async (name, payload) => {
|
|
49752
50127
|
if (disposed)
|
|
49753
50128
|
return;
|
|
49754
|
-
await
|
|
50129
|
+
await Effect4.runPromise(registration.events.emit(name, payload));
|
|
49755
50130
|
};
|
|
49756
50131
|
const publishStatus = (snapshot) => {
|
|
49757
50132
|
const sessionID = sessionFromSnapshot(snapshot);
|
|
@@ -49765,21 +50140,21 @@ function registerAftRpc(context, location, pool) {
|
|
|
49765
50140
|
return;
|
|
49766
50141
|
bridgeUnsubscribes.set(bridge, bridge.subscribeStatus(publishStatus));
|
|
49767
50142
|
};
|
|
49768
|
-
const getStatus = (input) =>
|
|
50143
|
+
const getStatus = (input) => Effect4.suspend(() => {
|
|
49769
50144
|
const sessionID = input.sessionID || "rpc";
|
|
49770
50145
|
const bridge = pool.getActiveBridgeForRoot(root);
|
|
49771
50146
|
if (!bridge)
|
|
49772
|
-
return
|
|
50147
|
+
return Effect4.succeed(placeholderStatus());
|
|
49773
50148
|
observeBridge(bridge);
|
|
49774
50149
|
const cached = bridge.getCachedStatus();
|
|
49775
50150
|
const cachedSessionID = cached ? sessionFromSnapshot(cached) : undefined;
|
|
49776
50151
|
if (cached && cachedSessionID === sessionID) {
|
|
49777
|
-
return
|
|
50152
|
+
return Effect4.succeed({ success: true, ...cached });
|
|
49778
50153
|
}
|
|
49779
|
-
return
|
|
50154
|
+
return Effect4.tryPromise({
|
|
49780
50155
|
try: (signal) => bridge.send("status", { session_id: sessionID }, { abortSignal: signal }),
|
|
49781
50156
|
catch: (error) => error
|
|
49782
|
-
}).pipe(
|
|
50157
|
+
}).pipe(Effect4.tap((response) => Effect4.sync(() => {
|
|
49783
50158
|
if (response.success !== false)
|
|
49784
50159
|
bridge.cacheStatusSnapshot(response);
|
|
49785
50160
|
})));
|
|
@@ -49840,21 +50215,21 @@ function registerAftRpc(context, location, pool) {
|
|
|
49840
50215
|
bridgeUnsubscribes.clear();
|
|
49841
50216
|
restoreOwnProperty(pool, "getBridge", ownGetBridge);
|
|
49842
50217
|
restoreOwnProperty(pool, "toolCall", ownToolCall);
|
|
49843
|
-
await
|
|
50218
|
+
await Effect4.runPromise(registration.dispose);
|
|
49844
50219
|
}
|
|
49845
50220
|
};
|
|
49846
50221
|
});
|
|
49847
50222
|
}
|
|
49848
50223
|
function registerAftConfigErrorRpc(context, message) {
|
|
49849
|
-
return
|
|
50224
|
+
return Effect4.gen(function* () {
|
|
49850
50225
|
let disposed = false;
|
|
49851
50226
|
const registration = yield* context.rpc.register(AftRpc, {
|
|
49852
|
-
getStatus: () =>
|
|
50227
|
+
getStatus: () => Effect4.succeed(configErrorStatusSnapshot(message))
|
|
49853
50228
|
});
|
|
49854
50229
|
const emit = async (name, payload) => {
|
|
49855
50230
|
if (disposed)
|
|
49856
50231
|
return;
|
|
49857
|
-
await
|
|
50232
|
+
await Effect4.runPromise(registration.events.emit(name, payload));
|
|
49858
50233
|
};
|
|
49859
50234
|
return {
|
|
49860
50235
|
emitStatusInvalidated: (payload = {}) => emit("statusInvalidated", payload),
|
|
@@ -49864,14 +50239,14 @@ function registerAftConfigErrorRpc(context, message) {
|
|
|
49864
50239
|
if (disposed)
|
|
49865
50240
|
return;
|
|
49866
50241
|
disposed = true;
|
|
49867
|
-
await
|
|
50242
|
+
await Effect4.runPromise(registration.dispose);
|
|
49868
50243
|
}
|
|
49869
50244
|
};
|
|
49870
50245
|
});
|
|
49871
50246
|
}
|
|
49872
50247
|
|
|
49873
50248
|
// src/permissions/v2.ts
|
|
49874
|
-
import { Effect as
|
|
50249
|
+
import { Effect as Effect5 } from "effect";
|
|
49875
50250
|
|
|
49876
50251
|
class PermissionDeniedError extends Error {
|
|
49877
50252
|
name = "PermissionDeniedError";
|
|
@@ -49927,7 +50302,7 @@ function detail(error) {
|
|
|
49927
50302
|
return error instanceof Error ? error.message : String(error);
|
|
49928
50303
|
}
|
|
49929
50304
|
async function runHostEffect(effect) {
|
|
49930
|
-
return await
|
|
50305
|
+
return await Effect5.runPromise(effect);
|
|
49931
50306
|
}
|
|
49932
50307
|
async function sessionRecord(host, sessionID) {
|
|
49933
50308
|
if (!sessionID)
|
|
@@ -49968,7 +50343,7 @@ function deniedMessage(request, decision) {
|
|
|
49968
50343
|
}
|
|
49969
50344
|
function promptUnavailableMessage(request, decision, observed) {
|
|
49970
50345
|
const resource = decision.resource ?? "*";
|
|
49971
|
-
return `The ${JSON.stringify(request.permission)} operation needs interactive approval: this ` + `session's permission rules resolve it to "ask" for ${JSON.stringify(resource)}, and AFT ` + `could not reach the OpenCode service to raise a prompt (${observed}).
|
|
50346
|
+
return `The ${JSON.stringify(request.permission)} operation needs interactive approval: this ` + `session's permission rules resolve it to "ask" for ${JSON.stringify(resource)}, and AFT ` + `could not reach the OpenCode service to raise a prompt (${observed}). Once the server is ` + "reachable, retry; or run it without a prompt by adding " + `{"action": ${JSON.stringify(request.permission)}, "resource": ${JSON.stringify(resource)}, ` + '"effect": "allow"} to the "permissions" list of the agent or session in your OpenCode config.';
|
|
49972
50347
|
}
|
|
49973
50348
|
function promptUnansweredMessage(request, decision, observed) {
|
|
49974
50349
|
const resource = decision.resource ?? "*";
|
|
@@ -50176,6 +50551,9 @@ async function requestPermission(host, request, context, prompt, limits = {}) {
|
|
|
50176
50551
|
}
|
|
50177
50552
|
|
|
50178
50553
|
// src/permissions/v2-service.ts
|
|
50554
|
+
import { readdir, readFile as readFile3, readlink } from "node:fs/promises";
|
|
50555
|
+
import { homedir as homedir15 } from "node:os";
|
|
50556
|
+
import { join as join26 } from "node:path";
|
|
50179
50557
|
init_promise();
|
|
50180
50558
|
|
|
50181
50559
|
// ../../node_modules/.bun/@opencode+client@2.0.11+bdc4bbd92cece36b/node_modules/@opencode/client/dist/chunks/service-txckps9j.js
|
|
@@ -50183,26 +50561,6 @@ var stderrLimit = 8 * 1024;
|
|
|
50183
50561
|
|
|
50184
50562
|
// ../../node_modules/.bun/@opencode+client@2.0.11+bdc4bbd92cece36b/node_modules/@opencode/client/dist/chunks/service-xgnj2w4v.js
|
|
50185
50563
|
var timings = new WeakMap;
|
|
50186
|
-
var defaultEnsureTiming2 = {
|
|
50187
|
-
pollInterval: 25,
|
|
50188
|
-
requestTimeout: 2000,
|
|
50189
|
-
spawnDelay: 5000,
|
|
50190
|
-
maxSpawnDelay: 30000,
|
|
50191
|
-
promiseTimeout: 120000,
|
|
50192
|
-
stopPollInterval: 50,
|
|
50193
|
-
stopPollAttempts: 100
|
|
50194
|
-
};
|
|
50195
|
-
|
|
50196
|
-
// ../../node_modules/.bun/@opencode+client@2.0.11+bdc4bbd92cece36b/node_modules/@opencode/client/dist/chunks/service-sxt88q69.js
|
|
50197
|
-
function matchesVersion2(version, options) {
|
|
50198
|
-
if (options.version === undefined)
|
|
50199
|
-
return true;
|
|
50200
|
-
if (version === undefined)
|
|
50201
|
-
return false;
|
|
50202
|
-
if (typeof options.version === "function")
|
|
50203
|
-
return options.version(version);
|
|
50204
|
-
return version === options.version;
|
|
50205
|
-
}
|
|
50206
50564
|
|
|
50207
50565
|
// ../../node_modules/.bun/@opencode+client@2.0.11+bdc4bbd92cece36b/node_modules/@opencode/client/dist/chunks/service-rg0h87gt.js
|
|
50208
50566
|
init_service_9rqn6x4v();
|
|
@@ -50312,22 +50670,6 @@ function isHandoff(value) {
|
|
|
50312
50670
|
return typeof value === "object" && value !== null && "directory" in value && typeof value.directory === "string" && "instanceID" in value && typeof value.instanceID === "string" && "ticket" in value && typeof value.ticket === "string" && "expiresAt" in value && typeof value.expiresAt === "number" && Number.isFinite(value.expiresAt);
|
|
50313
50671
|
}
|
|
50314
50672
|
// ../../node_modules/.bun/@opencode+client@2.0.11+bdc4bbd92cece36b/node_modules/@opencode/client/dist/promise/service.js
|
|
50315
|
-
import { readFile as readFile3, rm as rm3 } from "node:fs/promises";
|
|
50316
|
-
import { homedir as homedir15 } from "node:os";
|
|
50317
|
-
import { join as join26 } from "node:path";
|
|
50318
|
-
async function discover(options = {}) {
|
|
50319
|
-
const found = (await registered(options.file)).service;
|
|
50320
|
-
if (found?.state !== "ready")
|
|
50321
|
-
return;
|
|
50322
|
-
if (!found.compatible)
|
|
50323
|
-
return;
|
|
50324
|
-
if (!matchesVersion2(found.version, options))
|
|
50325
|
-
return;
|
|
50326
|
-
return found.endpoint;
|
|
50327
|
-
}
|
|
50328
|
-
function fallback() {
|
|
50329
|
-
return join26(process.env["XDG_STATE_HOME"] ?? join26(homedir15(), ".local", "state"), "opencode", "service.json");
|
|
50330
|
-
}
|
|
50331
50673
|
function headers(endpoint) {
|
|
50332
50674
|
if (endpoint.auth === undefined)
|
|
50333
50675
|
return;
|
|
@@ -50335,113 +50677,385 @@ function headers(endpoint) {
|
|
|
50335
50677
|
authorization: "Basic " + Buffer.from(endpoint.auth.username + ":" + endpoint.auth.password).toString("base64")
|
|
50336
50678
|
};
|
|
50337
50679
|
}
|
|
50338
|
-
|
|
50339
|
-
|
|
50340
|
-
|
|
50341
|
-
|
|
50342
|
-
|
|
50680
|
+
|
|
50681
|
+
// src/permissions/v2-service.ts
|
|
50682
|
+
var OPENCODE_BASIC_AUTH_USERNAME = "opencode";
|
|
50683
|
+
var DEFAULT_PROBE_TIMEOUT_MS2 = 2000;
|
|
50684
|
+
var LISTEN_LOOKUP_TIMEOUT_MS = 5000;
|
|
50685
|
+
var HOW_TO_FIX = `set "opencode.server_url" in your user AFT config to this server's URL, with ` + '"opencode.server_password_env" naming the environment variable that holds its password ' + "(for OpenChamber, the host and port given as OPENCODE_HOST / OPENCODE_PORT), or start " + "OpenCode with OPENCODE_SERVER_PASSWORD set so AFT can authenticate to it";
|
|
50686
|
+
function detail2(error) {
|
|
50687
|
+
return error instanceof Error ? error.message : String(error);
|
|
50688
|
+
}
|
|
50689
|
+
function promptClient(endpoint) {
|
|
50690
|
+
const client = exports_client.make({ baseUrl: endpoint.url, headers: headers(endpoint) });
|
|
50691
|
+
return {
|
|
50692
|
+
permission: {
|
|
50693
|
+
create: (input) => client.permission.create(input)
|
|
50694
|
+
},
|
|
50695
|
+
event: {
|
|
50696
|
+
subscribe: async () => ({ stream: client.event.subscribe() })
|
|
50697
|
+
}
|
|
50698
|
+
};
|
|
50699
|
+
}
|
|
50700
|
+
function basicAuth(password) {
|
|
50701
|
+
if (!password)
|
|
50343
50702
|
return;
|
|
50703
|
+
return { type: "basic", username: OPENCODE_BASIC_AUTH_USERNAME, password };
|
|
50704
|
+
}
|
|
50705
|
+
function ownServerPassword(env) {
|
|
50706
|
+
return env.OPENCODE_PASSWORD || env.OPENCODE_SERVER_PASSWORD || undefined;
|
|
50707
|
+
}
|
|
50708
|
+
async function probe(server, endpoint, timeoutMs) {
|
|
50709
|
+
let response;
|
|
50344
50710
|
try {
|
|
50345
|
-
|
|
50711
|
+
response = await server.fetch(`${endpoint.url.replace(/\/+$/, "")}/api/info`, {
|
|
50712
|
+
headers: headers(endpoint) ?? {},
|
|
50713
|
+
signal: AbortSignal.timeout(timeoutMs)
|
|
50714
|
+
});
|
|
50715
|
+
} catch (error) {
|
|
50716
|
+
return { kind: "failed", detail: detail2(error) };
|
|
50717
|
+
}
|
|
50718
|
+
if (response.status === 401 || response.status === 403) {
|
|
50719
|
+
await response.body?.cancel().catch(() => {});
|
|
50720
|
+
return { kind: "unauthorized", status: response.status };
|
|
50721
|
+
}
|
|
50722
|
+
if (!response.ok && response.status !== 503) {
|
|
50723
|
+
await response.body?.cancel().catch(() => {});
|
|
50724
|
+
return { kind: "failed", detail: `it answered HTTP ${response.status}` };
|
|
50725
|
+
}
|
|
50726
|
+
let body;
|
|
50727
|
+
try {
|
|
50728
|
+
body = await response.json();
|
|
50346
50729
|
} catch {
|
|
50347
|
-
|
|
50730
|
+
body = undefined;
|
|
50348
50731
|
}
|
|
50732
|
+
const pid = body?.pid;
|
|
50733
|
+
if (typeof pid === "number" && Number.isInteger(pid))
|
|
50734
|
+
return { kind: "answered", pid };
|
|
50735
|
+
return response.ok ? { kind: "answered" } : { kind: "failed", detail: `it answered HTTP ${response.status}` };
|
|
50349
50736
|
}
|
|
50350
|
-
async function
|
|
50737
|
+
async function configuredServer(settings, server, timeoutMs) {
|
|
50738
|
+
const raw = settings.server_url.trim();
|
|
50739
|
+
let url;
|
|
50740
|
+
try {
|
|
50741
|
+
url = new URL(raw);
|
|
50742
|
+
} catch {
|
|
50743
|
+
return {
|
|
50744
|
+
reason: "server_url_invalid",
|
|
50745
|
+
unavailable: `"opencode.server_url" ${JSON.stringify(raw)} in your AFT config is not a URL; set it to the server's base URL, for example "http://127.0.0.1:4096"`
|
|
50746
|
+
};
|
|
50747
|
+
}
|
|
50748
|
+
if (url.protocol !== "http:" && url.protocol !== "https:" || url.username || url.password) {
|
|
50749
|
+
return {
|
|
50750
|
+
reason: "server_url_invalid",
|
|
50751
|
+
unavailable: `"opencode.server_url" ${JSON.stringify(raw)} must be an http(s) URL without credentials; ` + 'put the password in an environment variable and name it in "opencode.server_password_env"'
|
|
50752
|
+
};
|
|
50753
|
+
}
|
|
50754
|
+
const variable = settings.server_password_env?.trim();
|
|
50755
|
+
let password;
|
|
50756
|
+
if (variable) {
|
|
50757
|
+
password = server.env[variable] || undefined;
|
|
50758
|
+
if (!password) {
|
|
50759
|
+
return {
|
|
50760
|
+
reason: "server_url_invalid",
|
|
50761
|
+
unavailable: `the environment variable ${variable} named by "opencode.server_password_env" is not ` + "set in the OpenCode process; set it to the server's password and restart OpenCode"
|
|
50762
|
+
};
|
|
50763
|
+
}
|
|
50764
|
+
} else {
|
|
50765
|
+
password = ownServerPassword(server.env);
|
|
50766
|
+
}
|
|
50351
50767
|
const endpoint = {
|
|
50352
|
-
url:
|
|
50353
|
-
auth:
|
|
50354
|
-
};
|
|
50355
|
-
const
|
|
50356
|
-
|
|
50357
|
-
response,
|
|
50358
|
-
body: response.status === 404 ? undefined : await response.json()
|
|
50359
|
-
})).then((value) => ({ value }), (cause) => ({ cause }));
|
|
50360
|
-
if ("cause" in result)
|
|
50361
|
-
return { service: undefined, timedOut: signal.aborted };
|
|
50362
|
-
const response = result.value.response;
|
|
50363
|
-
if (response.status === 404)
|
|
50768
|
+
url: url.href.replace(/\/+$/, ""),
|
|
50769
|
+
auth: basicAuth(password)
|
|
50770
|
+
};
|
|
50771
|
+
const result = await probe(server, endpoint, timeoutMs);
|
|
50772
|
+
if (result.kind === "unauthorized") {
|
|
50364
50773
|
return {
|
|
50365
|
-
|
|
50366
|
-
|
|
50367
|
-
endpoint,
|
|
50368
|
-
version: info.version,
|
|
50369
|
-
state: "ready",
|
|
50370
|
-
compatible: false
|
|
50371
|
-
},
|
|
50372
|
-
timedOut: false
|
|
50774
|
+
reason: "server_url_unreachable",
|
|
50775
|
+
unavailable: `the server at "opencode.server_url" ${endpoint.url} refused AFT's credentials ` + `(HTTP ${result.status}); set "opencode.server_password_env" to the name of the ` + "environment variable holding that server's password"
|
|
50373
50776
|
};
|
|
50374
|
-
|
|
50375
|
-
if (
|
|
50376
|
-
if (serverInfo.pid !== info.pid)
|
|
50377
|
-
return { service: undefined, timedOut: false };
|
|
50378
|
-
if (info.version !== undefined && serverInfo.version !== info.version)
|
|
50379
|
-
return { service: undefined, timedOut: false };
|
|
50777
|
+
}
|
|
50778
|
+
if (result.kind === "failed") {
|
|
50380
50779
|
return {
|
|
50381
|
-
|
|
50382
|
-
|
|
50383
|
-
endpoint,
|
|
50384
|
-
version: serverInfo.version,
|
|
50385
|
-
state: response.ok ? "ready" : response.status === 500 ? "failed" : "waiting",
|
|
50386
|
-
compatible: true
|
|
50387
|
-
},
|
|
50388
|
-
timedOut: false
|
|
50780
|
+
reason: "server_url_unreachable",
|
|
50781
|
+
unavailable: `the server at "opencode.server_url" ${endpoint.url} did not answer (${result.detail})`
|
|
50389
50782
|
};
|
|
50390
50783
|
}
|
|
50391
|
-
|
|
50784
|
+
if (result.pid !== undefined && result.pid !== server.pid) {
|
|
50785
|
+
warn2(`[permissions] opencode.server_url ${endpoint.url} is served by process ${result.pid}, not ` + `by this OpenCode process (${server.pid}); permission prompts are raised there`);
|
|
50786
|
+
}
|
|
50787
|
+
return { client: promptClient(endpoint) };
|
|
50392
50788
|
}
|
|
50393
|
-
function
|
|
50394
|
-
|
|
50395
|
-
|
|
50396
|
-
|
|
50397
|
-
|
|
50398
|
-
|
|
50789
|
+
function flagValue(argv, name) {
|
|
50790
|
+
let value;
|
|
50791
|
+
for (let index = 0;index < argv.length; index += 1) {
|
|
50792
|
+
const arg = argv[index];
|
|
50793
|
+
if (arg === `--${name}`)
|
|
50794
|
+
value = argv[index + 1];
|
|
50795
|
+
else if (arg?.startsWith(`--${name}=`))
|
|
50796
|
+
value = arg.slice(name.length + 3);
|
|
50797
|
+
}
|
|
50798
|
+
return value;
|
|
50799
|
+
}
|
|
50800
|
+
function ownListenAddress(argv) {
|
|
50801
|
+
const rawHost = flagValue(argv, "hostname")?.trim();
|
|
50802
|
+
let hostname = rawHost ? rawHost.replace(/^\[(.*)\]$/, "$1") : "127.0.0.1";
|
|
50803
|
+
if (hostname === "0.0.0.0")
|
|
50804
|
+
hostname = "127.0.0.1";
|
|
50805
|
+
if (hostname === "::")
|
|
50806
|
+
hostname = "::1";
|
|
50807
|
+
const rawPort = flagValue(argv, "port")?.trim();
|
|
50808
|
+
const parsed = rawPort !== undefined && /^\d+$/.test(rawPort) ? Number(rawPort) : undefined;
|
|
50809
|
+
const port = parsed !== undefined && parsed <= 65535 ? parsed : undefined;
|
|
50810
|
+
return { hostname, port, stdio: argv.includes("--stdio") };
|
|
50811
|
+
}
|
|
50812
|
+
function serverURL(hostname, port) {
|
|
50813
|
+
return `http://${hostname.includes(":") ? `[${hostname}]` : hostname}:${port}`;
|
|
50814
|
+
}
|
|
50815
|
+
async function ownRegistration(server, timeoutMs, observed) {
|
|
50816
|
+
let registrations;
|
|
50817
|
+
try {
|
|
50818
|
+
registrations = await server.registrations();
|
|
50819
|
+
} catch (error) {
|
|
50820
|
+
observed.push(`reading OpenCode service registrations failed: ${detail2(error)}`);
|
|
50399
50821
|
return;
|
|
50400
|
-
|
|
50822
|
+
}
|
|
50823
|
+
for (const registration of registrations) {
|
|
50824
|
+
let info;
|
|
50825
|
+
try {
|
|
50826
|
+
info = JSON.parse(registration.text);
|
|
50827
|
+
} catch {
|
|
50828
|
+
continue;
|
|
50829
|
+
}
|
|
50830
|
+
if (typeof info?.url !== "string" || typeof info.pid !== "number")
|
|
50831
|
+
continue;
|
|
50832
|
+
if (info.pid !== server.pid) {
|
|
50833
|
+
observed.push(`the service registered in ${registration.path} is process ${info.pid}, not this one (${server.pid}), so it was not used`);
|
|
50834
|
+
continue;
|
|
50835
|
+
}
|
|
50836
|
+
const endpoint = {
|
|
50837
|
+
url: info.url.replace(/\/+$/, ""),
|
|
50838
|
+
auth: basicAuth(typeof info.password === "string" ? info.password : undefined)
|
|
50839
|
+
};
|
|
50840
|
+
const result = await probe(server, endpoint, timeoutMs);
|
|
50841
|
+
if (result.kind === "answered" && result.pid === server.pid) {
|
|
50842
|
+
return { client: promptClient(endpoint) };
|
|
50843
|
+
}
|
|
50844
|
+
observed.push(`this process's own registration ${registration.path} did not verify at ${endpoint.url}`);
|
|
50845
|
+
}
|
|
50846
|
+
return;
|
|
50401
50847
|
}
|
|
50402
|
-
|
|
50403
|
-
const
|
|
50404
|
-
if (
|
|
50405
|
-
return
|
|
50406
|
-
|
|
50848
|
+
function reachableHost(address) {
|
|
50849
|
+
const host = address.replace(/^\[(.*)\]$/, "$1");
|
|
50850
|
+
if (host === "*" || host === "0.0.0.0" || host === "")
|
|
50851
|
+
return "127.0.0.1";
|
|
50852
|
+
if (/^[0:]+$/.test(host))
|
|
50853
|
+
return "::1";
|
|
50854
|
+
return host;
|
|
50855
|
+
}
|
|
50856
|
+
async function ownCandidates(server, listen, observed) {
|
|
50857
|
+
const declared = listen.port !== undefined && listen.port !== 0 ? listen.port : undefined;
|
|
50858
|
+
let sockets;
|
|
50859
|
+
try {
|
|
50860
|
+
sockets = await server.listeningSockets();
|
|
50861
|
+
} catch (error) {
|
|
50862
|
+
const failure = `looking up the ports this process (${server.pid}) listens on failed: ${detail2(error)}`;
|
|
50863
|
+
if (declared === undefined) {
|
|
50864
|
+
observed.push(`${failure}, and its command line names no --port`);
|
|
50865
|
+
return;
|
|
50866
|
+
}
|
|
50867
|
+
return [serverURL(listen.hostname, declared)];
|
|
50868
|
+
}
|
|
50869
|
+
const own = declared === undefined ? sockets : sockets.filter((socket) => socket.port === declared);
|
|
50870
|
+
if (own.length === 0) {
|
|
50871
|
+
const found = sockets.map((socket) => socket.port).join(", ") || "none";
|
|
50872
|
+
observed.push(declared === undefined ? `this process (${server.pid}) listens on no TCP port` : `--port ${declared} is not one of the ports this process (${server.pid}) listens on (${found})`);
|
|
50873
|
+
return;
|
|
50874
|
+
}
|
|
50875
|
+
return [...new Set(own.map((socket) => serverURL(reachableHost(socket.address), socket.port)))];
|
|
50876
|
+
}
|
|
50877
|
+
async function ownServer(server, options) {
|
|
50878
|
+
const observed = [];
|
|
50879
|
+
const registered = await ownRegistration(server, options.timeoutMs, observed);
|
|
50880
|
+
if (registered)
|
|
50881
|
+
return registered;
|
|
50882
|
+
const unaddressable = () => ({
|
|
50883
|
+
reason: "own_server_unaddressable",
|
|
50884
|
+
unavailable: `AFT could not find the OpenCode server it runs in (${observed.join("; ")}); to fix it, ${HOW_TO_FIX}`
|
|
50885
|
+
});
|
|
50886
|
+
const listen = ownListenAddress(server.argv);
|
|
50887
|
+
const password = ownServerPassword(server.env);
|
|
50888
|
+
if (listen.stdio) {
|
|
50889
|
+
observed.push("this is a private server (opencode --standalone) whose password is not readable by plugins");
|
|
50890
|
+
return unaddressable();
|
|
50891
|
+
}
|
|
50892
|
+
if (!password) {
|
|
50893
|
+
observed.push("this server was started without OPENCODE_SERVER_PASSWORD, so it generated a password AFT cannot read");
|
|
50894
|
+
return unaddressable();
|
|
50895
|
+
}
|
|
50896
|
+
const candidates = await ownCandidates(server, listen, observed);
|
|
50897
|
+
if (!candidates)
|
|
50898
|
+
return unaddressable();
|
|
50899
|
+
let refused = 0;
|
|
50900
|
+
let others = 0;
|
|
50901
|
+
for (const url of candidates) {
|
|
50902
|
+
const endpoint = { url, auth: basicAuth(password) };
|
|
50903
|
+
const result = await probe(server, endpoint, options.timeoutMs);
|
|
50904
|
+
if (result.kind === "answered" && result.pid === server.pid) {
|
|
50905
|
+
return { client: promptClient(endpoint) };
|
|
50906
|
+
}
|
|
50907
|
+
if (result.kind === "unauthorized")
|
|
50908
|
+
refused += 1;
|
|
50909
|
+
if (result.kind === "answered")
|
|
50910
|
+
others += 1;
|
|
50911
|
+
}
|
|
50912
|
+
observed.push(`none of ${candidates.join(", ")} answered as this process (${server.pid})` + (refused > 0 ? `; ${refused} refused the password from OPENCODE_SERVER_PASSWORD` : "") + (others > 0 ? `; ${others} belonged to another process` : ""));
|
|
50913
|
+
return unaddressable();
|
|
50914
|
+
}
|
|
50915
|
+
async function discoverPromptServer(settings, server = currentServerProcess(), options = {}) {
|
|
50916
|
+
const resolved = {
|
|
50917
|
+
timeoutMs: options.timeoutMs ?? DEFAULT_PROBE_TIMEOUT_MS2
|
|
50918
|
+
};
|
|
50919
|
+
const serverUrl = settings?.server_url;
|
|
50920
|
+
if (serverUrl !== undefined && serverUrl.trim() !== "") {
|
|
50921
|
+
return await configuredServer({ ...settings, server_url: serverUrl }, server, resolved.timeoutMs);
|
|
50922
|
+
}
|
|
50923
|
+
return await ownServer(server, resolved);
|
|
50407
50924
|
}
|
|
50408
|
-
|
|
50409
|
-
|
|
50410
|
-
function detail2(error) {
|
|
50411
|
-
return error instanceof Error ? error.message : String(error);
|
|
50925
|
+
function registrationDirectory(env) {
|
|
50926
|
+
return join26(env.XDG_STATE_HOME || join26(homedir15(), ".local", "state"), "opencode");
|
|
50412
50927
|
}
|
|
50413
|
-
function
|
|
50414
|
-
const
|
|
50928
|
+
function currentServerProcess() {
|
|
50929
|
+
const env = globalThis.process.env;
|
|
50415
50930
|
return {
|
|
50416
|
-
|
|
50417
|
-
|
|
50931
|
+
pid: globalThis.process.pid,
|
|
50932
|
+
argv: globalThis.process.argv,
|
|
50933
|
+
env,
|
|
50934
|
+
async registrations() {
|
|
50935
|
+
const directory = registrationDirectory(env);
|
|
50936
|
+
const names = await readdir(directory).catch(() => []);
|
|
50937
|
+
const files = names.filter((name) => /^service(-[^/\\]+)?\.json$/.test(name)).sort();
|
|
50938
|
+
const read = await Promise.all(files.map(async (name) => {
|
|
50939
|
+
const path = join26(directory, name);
|
|
50940
|
+
const text = await readFile3(path, "utf8").catch(() => {
|
|
50941
|
+
return;
|
|
50942
|
+
});
|
|
50943
|
+
return text === undefined ? undefined : { path, text };
|
|
50944
|
+
}));
|
|
50945
|
+
return read.filter((entry) => entry !== undefined);
|
|
50418
50946
|
},
|
|
50419
|
-
|
|
50420
|
-
|
|
50421
|
-
}
|
|
50947
|
+
listeningSockets: () => listeningSocketsOf(globalThis.process.pid),
|
|
50948
|
+
fetch: (url, init) => fetch(url, init)
|
|
50422
50949
|
};
|
|
50423
50950
|
}
|
|
50424
|
-
|
|
50425
|
-
|
|
50951
|
+
function runCommand(file, args) {
|
|
50952
|
+
return new Promise((resolve, reject) => {
|
|
50953
|
+
execFile(file, [...args], { timeout: LISTEN_LOOKUP_TIMEOUT_MS, maxBuffer: 8 * 1024 * 1024, encoding: "utf8" }, (error, stdout) => {
|
|
50954
|
+
const code = error?.code;
|
|
50955
|
+
if (error && !(code === 1 && file === "lsof"))
|
|
50956
|
+
reject(error);
|
|
50957
|
+
else
|
|
50958
|
+
resolve(String(stdout ?? ""));
|
|
50959
|
+
});
|
|
50960
|
+
});
|
|
50961
|
+
}
|
|
50962
|
+
function splitHostPort(value) {
|
|
50963
|
+
const separator = value.lastIndexOf(":");
|
|
50964
|
+
if (separator < 0)
|
|
50965
|
+
return;
|
|
50966
|
+
const port = Number(value.slice(separator + 1));
|
|
50967
|
+
if (!Number.isInteger(port) || port <= 0 || port > 65535)
|
|
50968
|
+
return;
|
|
50969
|
+
return { address: value.slice(0, separator).replace(/^\[(.*)\]$/, "$1"), port };
|
|
50970
|
+
}
|
|
50971
|
+
function parseLsofListening(output) {
|
|
50972
|
+
return output.split(/\r?\n/).filter((line) => line.startsWith("n")).map((line) => splitHostPort(line.slice(1))).filter((socket) => socket !== undefined);
|
|
50973
|
+
}
|
|
50974
|
+
function parseNetstatListening(output, pid) {
|
|
50975
|
+
const sockets = [];
|
|
50976
|
+
for (const line of output.split(/\r?\n/)) {
|
|
50977
|
+
const columns = line.trim().split(/\s+/);
|
|
50978
|
+
if (columns.length < 5 || !columns[0]?.toUpperCase().startsWith("TCP"))
|
|
50979
|
+
continue;
|
|
50980
|
+
if (Number(columns[columns.length - 1]) !== pid)
|
|
50981
|
+
continue;
|
|
50982
|
+
if (!/:0$/.test(columns[2] ?? ""))
|
|
50983
|
+
continue;
|
|
50984
|
+
const socket = splitHostPort(columns[1] ?? "");
|
|
50985
|
+
if (socket)
|
|
50986
|
+
sockets.push(socket);
|
|
50987
|
+
}
|
|
50988
|
+
return sockets;
|
|
50989
|
+
}
|
|
50990
|
+
function procAddress(hex) {
|
|
50991
|
+
const bytes = [];
|
|
50992
|
+
for (let word = 0;word < hex.length; word += 8) {
|
|
50993
|
+
const chunk = hex.slice(word, word + 8);
|
|
50994
|
+
for (let index = 6;index >= 0; index -= 2) {
|
|
50995
|
+
bytes.push(Number.parseInt(chunk.slice(index, index + 2), 16));
|
|
50996
|
+
}
|
|
50997
|
+
}
|
|
50998
|
+
if (bytes.length === 4)
|
|
50999
|
+
return bytes.join(".");
|
|
51000
|
+
const groups = [];
|
|
51001
|
+
for (let index = 0;index < bytes.length; index += 2) {
|
|
51002
|
+
groups.push(((bytes[index] ?? 0) << 8 | (bytes[index + 1] ?? 0)).toString(16));
|
|
51003
|
+
}
|
|
51004
|
+
return groups.join(":");
|
|
51005
|
+
}
|
|
51006
|
+
function parseProcNetListening(table, inodes) {
|
|
51007
|
+
const sockets = [];
|
|
51008
|
+
for (const line of table.split(/\r?\n/).slice(1)) {
|
|
51009
|
+
const columns = line.trim().split(/\s+/);
|
|
51010
|
+
if (columns[3] !== "0A" || !inodes.has(columns[9] ?? ""))
|
|
51011
|
+
continue;
|
|
51012
|
+
const [address, port] = (columns[1] ?? "").split(":");
|
|
51013
|
+
if (!address || !port)
|
|
51014
|
+
continue;
|
|
51015
|
+
sockets.push({ address: procAddress(address), port: Number.parseInt(port, 16) });
|
|
51016
|
+
}
|
|
51017
|
+
return sockets;
|
|
51018
|
+
}
|
|
51019
|
+
async function procListening(pid) {
|
|
51020
|
+
const descriptors = await readdir(`/proc/${pid}/fd`);
|
|
51021
|
+
const inodes = new Set;
|
|
51022
|
+
await Promise.all(descriptors.map(async (fd) => {
|
|
51023
|
+
const target = await readlink(`/proc/${pid}/fd/${fd}`).catch(() => "");
|
|
51024
|
+
const inode = /^socket:\[(\d+)\]$/.exec(target)?.[1];
|
|
51025
|
+
if (inode)
|
|
51026
|
+
inodes.add(inode);
|
|
51027
|
+
}));
|
|
51028
|
+
const tables = await Promise.all(["/proc/net/tcp", "/proc/net/tcp6"].map((path) => readFile3(path, "utf8").catch(() => "")));
|
|
51029
|
+
return tables.flatMap((table) => parseProcNetListening(table, inodes));
|
|
51030
|
+
}
|
|
51031
|
+
async function listeningSocketsOf(pid) {
|
|
51032
|
+
if (globalThis.process.platform === "win32") {
|
|
51033
|
+
return parseNetstatListening(await runCommand("netstat", ["-ano", "-p", "TCP"]), pid).concat(parseNetstatListening(await runCommand("netstat", ["-ano", "-p", "TCPv6"]), pid));
|
|
51034
|
+
}
|
|
50426
51035
|
try {
|
|
50427
|
-
|
|
51036
|
+
return parseLsofListening(await runCommand("lsof", ["-nP", "-a", "-p", String(pid), "-iTCP", "-sTCP:LISTEN", "-Fn"]));
|
|
50428
51037
|
} catch (error) {
|
|
50429
|
-
|
|
50430
|
-
|
|
50431
|
-
|
|
50432
|
-
return
|
|
51038
|
+
const missing = error.code === "ENOENT";
|
|
51039
|
+
if (!missing || globalThis.process.platform !== "linux")
|
|
51040
|
+
throw error;
|
|
51041
|
+
return await procListening(pid);
|
|
50433
51042
|
}
|
|
50434
|
-
return { client: promptClient(endpoint) };
|
|
50435
51043
|
}
|
|
50436
|
-
function createV2PromptChannel(discovery =
|
|
51044
|
+
function createV2PromptChannel(discovery = () => discoverPromptServer(undefined)) {
|
|
50437
51045
|
let cached;
|
|
51046
|
+
let failureLogged = false;
|
|
50438
51047
|
return {
|
|
50439
51048
|
async client() {
|
|
50440
51049
|
if (cached)
|
|
50441
51050
|
return { client: cached };
|
|
50442
51051
|
const result = await discovery();
|
|
50443
|
-
if (result.client)
|
|
51052
|
+
if (result.client) {
|
|
50444
51053
|
cached = result.client;
|
|
51054
|
+
log2("[permissions] OpenCode server for permission prompts found");
|
|
51055
|
+
} else if (!failureLogged) {
|
|
51056
|
+
failureLogged = true;
|
|
51057
|
+
warn2(`[permissions] permission prompts are unavailable${result.reason ? ` (${result.reason})` : ""}: ${result.unavailable}`);
|
|
51058
|
+
}
|
|
50445
51059
|
return result;
|
|
50446
51060
|
}
|
|
50447
51061
|
};
|
|
@@ -50461,9 +51075,149 @@ function hoistedV2ToolConsumers(host, prompt = createV2PromptChannel()) {
|
|
|
50461
51075
|
requestPermission: (request, context) => requestPermission(host, request, context, prompt)
|
|
50462
51076
|
};
|
|
50463
51077
|
}
|
|
51078
|
+
function v2PromptChannelFor(settings) {
|
|
51079
|
+
return createV2PromptChannel(() => discoverPromptServer(settings));
|
|
51080
|
+
}
|
|
50464
51081
|
|
|
50465
|
-
// src/
|
|
51082
|
+
// src/v2-prompt-detach.ts
|
|
51083
|
+
import { Effect as Effect7 } from "effect";
|
|
51084
|
+
|
|
51085
|
+
// src/wakes/session-delivery.ts
|
|
50466
51086
|
import { Effect as Effect6 } from "effect";
|
|
51087
|
+
var AFT_PROMPT_SOURCE = "aft";
|
|
51088
|
+
function isAftOriginatedPrompt(metadata) {
|
|
51089
|
+
return typeof metadata === "object" && metadata !== null && metadata.source === AFT_PROMPT_SOURCE;
|
|
51090
|
+
}
|
|
51091
|
+
|
|
51092
|
+
class V2SessionDelivery {
|
|
51093
|
+
session;
|
|
51094
|
+
admittedCompletions = new Set;
|
|
51095
|
+
disposed = false;
|
|
51096
|
+
constructor(session) {
|
|
51097
|
+
this.session = session;
|
|
51098
|
+
}
|
|
51099
|
+
completion(input) {
|
|
51100
|
+
return Effect6.suspend(() => {
|
|
51101
|
+
if (this.disposed)
|
|
51102
|
+
return Effect6.succeed(false);
|
|
51103
|
+
const keys = [...new Set(input.taskIDs)].map((taskID) => `${input.sessionID}\x00${taskID}`);
|
|
51104
|
+
if (keys.length === 0 || keys.every((key) => this.admittedCompletions.has(key))) {
|
|
51105
|
+
return Effect6.succeed(false);
|
|
51106
|
+
}
|
|
51107
|
+
for (const key of keys)
|
|
51108
|
+
this.admittedCompletions.add(key);
|
|
51109
|
+
return this.session.prompt({
|
|
51110
|
+
sessionID: input.sessionID,
|
|
51111
|
+
text: input.text,
|
|
51112
|
+
delivery: "steer",
|
|
51113
|
+
metadata: {
|
|
51114
|
+
...input.metadata,
|
|
51115
|
+
source: AFT_PROMPT_SOURCE,
|
|
51116
|
+
task_ids: [...new Set(input.taskIDs)]
|
|
51117
|
+
}
|
|
51118
|
+
}).pipe(Effect6.map(() => true));
|
|
51119
|
+
});
|
|
51120
|
+
}
|
|
51121
|
+
status(input) {
|
|
51122
|
+
return Effect6.suspend(() => {
|
|
51123
|
+
if (this.disposed)
|
|
51124
|
+
return Effect6.succeed(undefined);
|
|
51125
|
+
return this.session.synthetic({
|
|
51126
|
+
sessionID: input.sessionID,
|
|
51127
|
+
text: input.text,
|
|
51128
|
+
description: input.description,
|
|
51129
|
+
metadata: input.metadata,
|
|
51130
|
+
resume: false
|
|
51131
|
+
}).pipe(Effect6.asVoid);
|
|
51132
|
+
});
|
|
51133
|
+
}
|
|
51134
|
+
dispose() {
|
|
51135
|
+
if (this.disposed)
|
|
51136
|
+
return;
|
|
51137
|
+
this.disposed = true;
|
|
51138
|
+
this.admittedCompletions.clear();
|
|
51139
|
+
}
|
|
51140
|
+
}
|
|
51141
|
+
|
|
51142
|
+
// src/v2-prompt-detach.ts
|
|
51143
|
+
function mapOffset(offset, edits) {
|
|
51144
|
+
let shift = 0;
|
|
51145
|
+
for (const edit of edits) {
|
|
51146
|
+
if (offset <= edit.start)
|
|
51147
|
+
break;
|
|
51148
|
+
if (offset >= edit.end) {
|
|
51149
|
+
shift += edit.length - (edit.end - edit.start);
|
|
51150
|
+
continue;
|
|
51151
|
+
}
|
|
51152
|
+
return edit.start + shift + Math.min(offset - edit.start, edit.length);
|
|
51153
|
+
}
|
|
51154
|
+
return offset + shift;
|
|
51155
|
+
}
|
|
51156
|
+
function offsetMapper(original, finalText) {
|
|
51157
|
+
const tokenEdits = standaloneDetachKeywordRanges(original).map(([start, end]) => ({
|
|
51158
|
+
start,
|
|
51159
|
+
end,
|
|
51160
|
+
length: 0
|
|
51161
|
+
}));
|
|
51162
|
+
const withoutTokens = stripStandaloneDetachKeywords(original);
|
|
51163
|
+
const whitespaceEdits = [...withoutTokens.matchAll(/[ \t]{2,}/g)].map((match) => ({
|
|
51164
|
+
start: match.index ?? 0,
|
|
51165
|
+
end: (match.index ?? 0) + match[0].length,
|
|
51166
|
+
length: 1
|
|
51167
|
+
}));
|
|
51168
|
+
return (offset) => Math.min(mapOffset(mapOffset(offset, tokenEdits), whitespaceEdits), finalText.length);
|
|
51169
|
+
}
|
|
51170
|
+
function mentionCarriers(prompt) {
|
|
51171
|
+
return [...prompt.files ?? [], ...prompt.agents ?? [], ...prompt.skills ?? []].filter((item) => typeof item?.mention?.start === "number" && typeof item.mention.end === "number");
|
|
51172
|
+
}
|
|
51173
|
+
function interruptBashWaitsForV2Prompt(runtime, event) {
|
|
51174
|
+
const prompt = event.prompt;
|
|
51175
|
+
const original = prompt.text;
|
|
51176
|
+
const synthetic = isAftOriginatedPrompt(event.metadata);
|
|
51177
|
+
const output = { parts: [{ type: "text", text: original, synthetic }] };
|
|
51178
|
+
const interrupted = interruptBashWaitsForChatMessage(runtime.pool, runtime.getConfig(), runtime.projectRoot, event.sessionID, output);
|
|
51179
|
+
const finalText = output.parts[0]?.text ?? original;
|
|
51180
|
+
if (synthetic || finalText === original)
|
|
51181
|
+
return interrupted;
|
|
51182
|
+
const map = offsetMapper(original, finalText);
|
|
51183
|
+
const moved = mentionCarriers(prompt).map((item) => {
|
|
51184
|
+
const mention = item.mention;
|
|
51185
|
+
return { item, mention: { ...mention, start: map(mention.start), end: map(mention.end) } };
|
|
51186
|
+
});
|
|
51187
|
+
prompt.text = finalText;
|
|
51188
|
+
for (const { item, mention } of moved)
|
|
51189
|
+
item.mention = mention;
|
|
51190
|
+
return interrupted;
|
|
51191
|
+
}
|
|
51192
|
+
function failureReason(error) {
|
|
51193
|
+
return error instanceof Error ? error.message : String(error);
|
|
51194
|
+
}
|
|
51195
|
+
function createV2PromptDetachHook(runtime) {
|
|
51196
|
+
const loggedReasons = new Set;
|
|
51197
|
+
return (event) => Effect7.sync(() => {
|
|
51198
|
+
try {
|
|
51199
|
+
interruptBashWaitsForV2Prompt(runtime, event);
|
|
51200
|
+
} catch (error) {
|
|
51201
|
+
const reason = failureReason(error);
|
|
51202
|
+
if (loggedReasons.has(reason))
|
|
51203
|
+
return;
|
|
51204
|
+
loggedReasons.add(reason);
|
|
51205
|
+
warn2(`[bash_wait_detach] prompt hook failed for session ${String(event?.sessionID)}; the prompt passes through unchanged: ${reason}`);
|
|
51206
|
+
}
|
|
51207
|
+
});
|
|
51208
|
+
}
|
|
51209
|
+
function registerV2PromptDetachHook(context, runtime) {
|
|
51210
|
+
const session = context?.session;
|
|
51211
|
+
const hook = session?.hook;
|
|
51212
|
+
if (typeof hook !== "function") {
|
|
51213
|
+
warn2("[bash_wait_detach] host has no session prompt hook; a new message cannot detach a waiting bash here");
|
|
51214
|
+
return Effect7.succeed(false);
|
|
51215
|
+
}
|
|
51216
|
+
return hook.call(session, "prompt", createV2PromptDetachHook(runtime)).pipe(Effect7.as(true));
|
|
51217
|
+
}
|
|
51218
|
+
|
|
51219
|
+
// src/wakes/runtime-consumer.ts
|
|
51220
|
+
import { Effect as Effect8 } from "effect";
|
|
50467
51221
|
|
|
50468
51222
|
// src/cancellation/effect-abort.ts
|
|
50469
51223
|
function forwardEffectAbort(signal, send) {
|
|
@@ -50511,96 +51265,64 @@ async function executeV2Bash(execution) {
|
|
|
50511
51265
|
}
|
|
50512
51266
|
}
|
|
50513
51267
|
|
|
50514
|
-
// src/wakes/session-delivery.ts
|
|
50515
|
-
import { Effect as Effect5 } from "effect";
|
|
50516
|
-
|
|
50517
|
-
class V2SessionDelivery {
|
|
50518
|
-
session;
|
|
50519
|
-
admittedCompletions = new Set;
|
|
50520
|
-
disposed = false;
|
|
50521
|
-
constructor(session) {
|
|
50522
|
-
this.session = session;
|
|
50523
|
-
}
|
|
50524
|
-
completion(input) {
|
|
50525
|
-
return Effect5.suspend(() => {
|
|
50526
|
-
if (this.disposed)
|
|
50527
|
-
return Effect5.succeed(false);
|
|
50528
|
-
const keys = [...new Set(input.taskIDs)].map((taskID) => `${input.sessionID}\x00${taskID}`);
|
|
50529
|
-
if (keys.length === 0 || keys.every((key) => this.admittedCompletions.has(key))) {
|
|
50530
|
-
return Effect5.succeed(false);
|
|
50531
|
-
}
|
|
50532
|
-
for (const key of keys)
|
|
50533
|
-
this.admittedCompletions.add(key);
|
|
50534
|
-
return this.session.prompt({
|
|
50535
|
-
sessionID: input.sessionID,
|
|
50536
|
-
text: input.text,
|
|
50537
|
-
delivery: "steer",
|
|
50538
|
-
metadata: {
|
|
50539
|
-
...input.metadata,
|
|
50540
|
-
task_ids: [...new Set(input.taskIDs)]
|
|
50541
|
-
}
|
|
50542
|
-
}).pipe(Effect5.map(() => true));
|
|
50543
|
-
});
|
|
50544
|
-
}
|
|
50545
|
-
status(input) {
|
|
50546
|
-
return Effect5.suspend(() => {
|
|
50547
|
-
if (this.disposed)
|
|
50548
|
-
return Effect5.succeed(undefined);
|
|
50549
|
-
return this.session.synthetic({
|
|
50550
|
-
sessionID: input.sessionID,
|
|
50551
|
-
text: input.text,
|
|
50552
|
-
description: input.description,
|
|
50553
|
-
metadata: input.metadata,
|
|
50554
|
-
resume: false
|
|
50555
|
-
}).pipe(Effect5.asVoid);
|
|
50556
|
-
});
|
|
50557
|
-
}
|
|
50558
|
-
dispose() {
|
|
50559
|
-
if (this.disposed)
|
|
50560
|
-
return;
|
|
50561
|
-
this.disposed = true;
|
|
50562
|
-
this.admittedCompletions.clear();
|
|
50563
|
-
}
|
|
50564
|
-
}
|
|
50565
|
-
|
|
50566
51268
|
// src/wakes/runtime-consumer.ts
|
|
50567
|
-
var
|
|
50568
|
-
|
|
50569
|
-
const
|
|
50570
|
-
|
|
50571
|
-
|
|
50572
|
-
|
|
50573
|
-
|
|
50574
|
-
|
|
50575
|
-
|
|
50576
|
-
|
|
50577
|
-
|
|
50578
|
-
|
|
50579
|
-
|
|
50580
|
-
|
|
50581
|
-
|
|
50582
|
-
|
|
50583
|
-
if (!
|
|
51269
|
+
var WAKE_ROUTE_STATE = Symbol.for("@cortexkit/aft-opencode/wake-routes/v1");
|
|
51270
|
+
function wakeRouteState() {
|
|
51271
|
+
const host = globalThis;
|
|
51272
|
+
return host[WAKE_ROUTE_STATE] ??= {
|
|
51273
|
+
routes: new Map,
|
|
51274
|
+
unrouted: new Map,
|
|
51275
|
+
warnedUnrouted: new Set
|
|
51276
|
+
};
|
|
51277
|
+
}
|
|
51278
|
+
var MAX_UNROUTED_COMPLETIONS = 256;
|
|
51279
|
+
function holdUnrouted(completion, bridge) {
|
|
51280
|
+
const { unrouted } = wakeRouteState();
|
|
51281
|
+
let held = 0;
|
|
51282
|
+
for (const tasks of unrouted.values())
|
|
51283
|
+
held += tasks.size;
|
|
51284
|
+
const sessionTasks = unrouted.get(completion.session_id);
|
|
51285
|
+
if (!sessionTasks?.has(completion.task_id) && held >= MAX_UNROUTED_COMPLETIONS) {
|
|
51286
|
+
const oldest = unrouted.keys().next();
|
|
51287
|
+
if (!oldest.done)
|
|
51288
|
+
unrouted.delete(oldest.value);
|
|
51289
|
+
}
|
|
51290
|
+
const tasks = unrouted.get(completion.session_id) ?? new Map;
|
|
51291
|
+
tasks.set(completion.task_id, { completion, bridge });
|
|
51292
|
+
unrouted.set(completion.session_id, tasks);
|
|
51293
|
+
}
|
|
51294
|
+
function warnUnroutedOnce(sessionID, message) {
|
|
51295
|
+
const { warnedUnrouted } = wakeRouteState();
|
|
51296
|
+
if (warnedUnrouted.has(sessionID))
|
|
50584
51297
|
return;
|
|
50585
|
-
|
|
50586
|
-
|
|
50587
|
-
|
|
50588
|
-
|
|
51298
|
+
warnedUnrouted.add(sessionID);
|
|
51299
|
+
warn2(message);
|
|
51300
|
+
}
|
|
51301
|
+
async function routeCompletion(completion, bridge) {
|
|
51302
|
+
const route = wakeRouteState().routes.get(completion.session_id);
|
|
51303
|
+
if (route)
|
|
51304
|
+
return route.completion(completion, bridge);
|
|
51305
|
+
holdUnrouted(completion, bridge);
|
|
51306
|
+
warnUnroutedOnce(completion.session_id, `[bash_completion] no OpenCode Location has session ${completion.session_id} registered; completion of task ${completion.task_id} is held unacknowledged until the session registers`);
|
|
50589
51307
|
}
|
|
50590
51308
|
async function routeLongRunning(reminder, _bridge) {
|
|
50591
|
-
const route =
|
|
50592
|
-
if (
|
|
51309
|
+
const route = wakeRouteState().routes.get(reminder.session_id);
|
|
51310
|
+
if (route)
|
|
51311
|
+
return route.longRunning(reminder);
|
|
51312
|
+
warnUnroutedOnce(reminder.session_id, `[bash_long_running] no OpenCode Location has session ${reminder.session_id} registered; long-running reminder for task ${reminder.task_id} was not delivered`);
|
|
51313
|
+
}
|
|
51314
|
+
function deliverUnrouted(sessionID, route) {
|
|
51315
|
+
const state = wakeRouteState();
|
|
51316
|
+
state.warnedUnrouted.delete(sessionID);
|
|
51317
|
+
const held = state.unrouted.get(sessionID);
|
|
51318
|
+
if (!held)
|
|
50593
51319
|
return;
|
|
50594
|
-
|
|
50595
|
-
|
|
50596
|
-
|
|
50597
|
-
|
|
50598
|
-
|
|
50599
|
-
|
|
50600
|
-
kind: "bash_long_running",
|
|
50601
|
-
task_id: reminder.task_id
|
|
50602
|
-
}
|
|
50603
|
-
}));
|
|
51320
|
+
state.unrouted.delete(sessionID);
|
|
51321
|
+
for (const { completion, bridge } of held.values()) {
|
|
51322
|
+
route.completion(completion, bridge).catch((error) => {
|
|
51323
|
+
warn2(`[bash_completion] late delivery of task ${completion.task_id} to session ${sessionID} failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
51324
|
+
});
|
|
51325
|
+
}
|
|
50604
51326
|
}
|
|
50605
51327
|
var sharedBridgeOptions = {
|
|
50606
51328
|
onBashCompletion: routeCompletion,
|
|
@@ -50608,12 +51330,47 @@ var sharedBridgeOptions = {
|
|
|
50608
51330
|
};
|
|
50609
51331
|
function createV2RuntimeConsumer(context) {
|
|
50610
51332
|
const delivery = new V2SessionDelivery(context.session);
|
|
50611
|
-
const
|
|
51333
|
+
const sessions = new Set;
|
|
51334
|
+
const route = {
|
|
51335
|
+
completion: async (completion, bridge) => {
|
|
51336
|
+
const admitted = await Effect8.runPromise(delivery.completion({
|
|
51337
|
+
sessionID: completion.session_id,
|
|
51338
|
+
taskIDs: [completion.task_id],
|
|
51339
|
+
text: formatSystemReminder([completion]),
|
|
51340
|
+
metadata: {
|
|
51341
|
+
source: "aft",
|
|
51342
|
+
kind: "bash_completion",
|
|
51343
|
+
task_ids: [completion.task_id],
|
|
51344
|
+
status: completion.status
|
|
51345
|
+
}
|
|
51346
|
+
}));
|
|
51347
|
+
if (!admitted)
|
|
51348
|
+
return;
|
|
51349
|
+
await bridge.send("bash_ack_completions", {
|
|
51350
|
+
session_id: completion.session_id,
|
|
51351
|
+
task_ids: [completion.task_id]
|
|
51352
|
+
});
|
|
51353
|
+
},
|
|
51354
|
+
longRunning: (reminder) => Effect8.runPromise(delivery.status({
|
|
51355
|
+
sessionID: reminder.session_id,
|
|
51356
|
+
text: formatLongRunningReminder([reminder]),
|
|
51357
|
+
description: "Background bash status",
|
|
51358
|
+
metadata: {
|
|
51359
|
+
source: "aft",
|
|
51360
|
+
kind: "bash_long_running",
|
|
51361
|
+
task_id: reminder.task_id
|
|
51362
|
+
}
|
|
51363
|
+
}))
|
|
51364
|
+
};
|
|
50612
51365
|
const registerSession = (sessionID) => {
|
|
50613
51366
|
if (!sessionID)
|
|
50614
51367
|
return;
|
|
50615
|
-
|
|
50616
|
-
|
|
51368
|
+
sessions.add(sessionID);
|
|
51369
|
+
const { routes } = wakeRouteState();
|
|
51370
|
+
if (routes.get(sessionID) === route)
|
|
51371
|
+
return;
|
|
51372
|
+
routes.set(sessionID, route);
|
|
51373
|
+
deliverUnrouted(sessionID, route);
|
|
50617
51374
|
};
|
|
50618
51375
|
return {
|
|
50619
51376
|
bridgeOptions: sharedBridgeOptions,
|
|
@@ -50623,11 +51380,12 @@ function createV2RuntimeConsumer(context) {
|
|
|
50623
51380
|
},
|
|
50624
51381
|
dispose: () => {
|
|
50625
51382
|
delivery.dispose();
|
|
50626
|
-
|
|
50627
|
-
|
|
50628
|
-
|
|
51383
|
+
const { routes } = wakeRouteState();
|
|
51384
|
+
for (const sessionID of sessions) {
|
|
51385
|
+
if (routes.get(sessionID) === route)
|
|
51386
|
+
routes.delete(sessionID);
|
|
50629
51387
|
}
|
|
50630
|
-
|
|
51388
|
+
sessions.clear();
|
|
50631
51389
|
}
|
|
50632
51390
|
};
|
|
50633
51391
|
}
|
|
@@ -50639,8 +51397,9 @@ var defaults = {
|
|
|
50639
51397
|
registerTools: registerAftTools,
|
|
50640
51398
|
registerRpc: registerAftRpc,
|
|
50641
51399
|
registerConfigErrorRpc: registerAftConfigErrorRpc,
|
|
50642
|
-
|
|
50643
|
-
|
|
51400
|
+
registerPromptHook: registerV2PromptDetachHook,
|
|
51401
|
+
toolConsumers: (context, config) => ({
|
|
51402
|
+
...hoistedV2ToolConsumers(context, v2PromptChannelFor(config?.opencode)),
|
|
50644
51403
|
...createV2RuntimeConsumer(context)
|
|
50645
51404
|
}),
|
|
50646
51405
|
acquireBridge,
|
|
@@ -50666,7 +51425,7 @@ async function bootLocation(context, location, dependencies) {
|
|
|
50666
51425
|
const pluginVersion = dependencies.resolveVersion();
|
|
50667
51426
|
const environment = await prepareBridgeEnvironment({ configRoot: directory, lspDirectory: directory, config, pluginVersion, notify }, dependencies);
|
|
50668
51427
|
const canonicalDirectory = location.project?.canonical ?? directory;
|
|
50669
|
-
const consumers = dependencies.toolConsumers(context);
|
|
51428
|
+
const consumers = dependencies.toolConsumers(context, config);
|
|
50670
51429
|
const isProjectEnabled = createProjectAcceptance(directory, dependencies);
|
|
50671
51430
|
const pool = await dependencies.acquireBridge(canonicalDirectory, {
|
|
50672
51431
|
harness: "opencode",
|
|
@@ -50709,7 +51468,7 @@ async function bootLocation(context, location, dependencies) {
|
|
|
50709
51468
|
},
|
|
50710
51469
|
notify
|
|
50711
51470
|
});
|
|
50712
|
-
return { consumers, pool, tools, liveConfigReload };
|
|
51471
|
+
return { consumers, pool, tools, liveConfigReload, toolContext, directory };
|
|
50713
51472
|
}
|
|
50714
51473
|
var runningByDirectory = new Map;
|
|
50715
51474
|
var startsInProcess = 0;
|
|
@@ -50736,27 +51495,32 @@ function makeServerEffect(overrides = {}) {
|
|
|
50736
51495
|
const location = context.location;
|
|
50737
51496
|
if (typeof location?.directory !== "string") {
|
|
50738
51497
|
debug2("V2 effect skipped: host context has no location");
|
|
50739
|
-
return
|
|
51498
|
+
return Effect9.void;
|
|
50740
51499
|
}
|
|
50741
|
-
return
|
|
51500
|
+
return Effect9.gen(function* () {
|
|
50742
51501
|
const logRuntimeStop = logRuntimeStart(location);
|
|
50743
|
-
yield*
|
|
50744
|
-
const runtime = yield*
|
|
51502
|
+
yield* Effect9.addFinalizer(() => Effect9.sync(logRuntimeStop));
|
|
51503
|
+
const runtime = yield* Effect9.promise(() => bootLocation(context, location, dependencies));
|
|
50745
51504
|
if (!runtime)
|
|
50746
51505
|
return;
|
|
50747
51506
|
if (runtime.configError !== undefined) {
|
|
50748
51507
|
const rpc = yield* dependencies.registerConfigErrorRpc(context, runtime.configError);
|
|
50749
|
-
yield*
|
|
51508
|
+
yield* Effect9.addFinalizer(() => Effect9.promise(() => rpc.dispose()));
|
|
50750
51509
|
yield* dependencies.registerTools(context, location, runtime.tools, runtime.consumers);
|
|
50751
51510
|
return;
|
|
50752
51511
|
}
|
|
50753
|
-
yield*
|
|
51512
|
+
yield* Effect9.addFinalizer(() => Effect9.promise(async () => {
|
|
50754
51513
|
runtime.liveConfigReload?.stop();
|
|
50755
51514
|
runtime.consumers.dispose?.();
|
|
50756
51515
|
await dependencies.releaseBridge(runtime.pool);
|
|
50757
51516
|
}));
|
|
50758
51517
|
const rpc = yield* dependencies.registerRpc(context, location, runtime.pool);
|
|
50759
|
-
yield*
|
|
51518
|
+
yield* Effect9.addFinalizer(() => Effect9.promise(() => rpc.dispose()));
|
|
51519
|
+
yield* dependencies.registerPromptHook(context, {
|
|
51520
|
+
pool: runtime.pool,
|
|
51521
|
+
projectRoot: runtime.directory,
|
|
51522
|
+
getConfig: () => runtime.toolContext.config
|
|
51523
|
+
});
|
|
50760
51524
|
yield* dependencies.registerTools(context, location, runtime.tools, runtime.consumers);
|
|
50761
51525
|
});
|
|
50762
51526
|
};
|