@cortexkit/aft-opencode 0.18.2 → 0.18.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/bg-notifications.d.ts +13 -0
- package/dist/bg-notifications.d.ts.map +1 -1
- package/dist/bridge.d.ts +26 -0
- package/dist/bridge.d.ts.map +1 -1
- package/dist/config.d.ts +1 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +450 -73
- package/dist/logger.d.ts +3 -3
- package/dist/logger.d.ts.map +1 -1
- package/dist/lsp.d.ts +1 -1
- package/dist/lsp.d.ts.map +1 -1
- package/dist/notifications.d.ts.map +1 -1
- package/dist/pool.d.ts +2 -0
- package/dist/pool.d.ts.map +1 -1
- package/dist/shared/last-user-model.d.ts +38 -0
- package/dist/shared/last-user-model.d.ts.map +1 -0
- package/dist/shared/rpc-client.d.ts +2 -0
- package/dist/shared/rpc-client.d.ts.map +1 -1
- package/dist/shared/rpc-server.d.ts.map +1 -1
- package/dist/shared/url-fetch.d.ts +16 -0
- package/dist/shared/url-fetch.d.ts.map +1 -1
- package/dist/tools/bash.d.ts +2 -0
- package/dist/tools/bash.d.ts.map +1 -1
- package/dist/tools/hoisted.d.ts.map +1 -1
- package/dist/tools/reading.d.ts +14 -0
- package/dist/tools/reading.d.ts.map +1 -1
- package/dist/tools/refactoring.d.ts.map +1 -1
- package/dist/tui.js +38 -12
- package/dist/workflow-hints.d.ts +25 -0
- package/dist/workflow-hints.d.ts.map +1 -0
- package/package.json +8 -6
- package/src/logger.ts +9 -4
- package/src/shared/last-user-model.ts +133 -0
- package/src/shared/rpc-client.ts +38 -5
- package/src/shared/rpc-server.ts +3 -2
- package/src/shared/url-fetch.ts +53 -8
package/dist/index.js
CHANGED
|
@@ -7813,7 +7813,8 @@ import * as fs from "fs";
|
|
|
7813
7813
|
import * as os from "os";
|
|
7814
7814
|
import * as path from "path";
|
|
7815
7815
|
var TAG = "[aft-plugin]";
|
|
7816
|
-
var
|
|
7816
|
+
var isTestEnv = process.env.BUN_TEST === "1" || false;
|
|
7817
|
+
var logFile = path.join(os.tmpdir(), isTestEnv ? "aft-plugin-test.log" : "aft-plugin.log");
|
|
7817
7818
|
var useStderr = process.env.AFT_LOG_STDERR === "1";
|
|
7818
7819
|
var buffer = [];
|
|
7819
7820
|
var flushTimer = null;
|
|
@@ -7871,6 +7872,9 @@ function warn(message, data) {
|
|
|
7871
7872
|
function error(message, data) {
|
|
7872
7873
|
write("ERROR", message, data);
|
|
7873
7874
|
}
|
|
7875
|
+
function sessionLog(sessionId, message, data) {
|
|
7876
|
+
write("INFO", message, data, sessionId);
|
|
7877
|
+
}
|
|
7874
7878
|
function sessionWarn(sessionId, message, data) {
|
|
7875
7879
|
write("WARN", message, data, sessionId);
|
|
7876
7880
|
}
|
|
@@ -7878,15 +7882,90 @@ function getLogFilePath() {
|
|
|
7878
7882
|
return logFile;
|
|
7879
7883
|
}
|
|
7880
7884
|
|
|
7885
|
+
// src/shared/last-user-model.ts
|
|
7886
|
+
var CACHE_TTL_MS = 5000;
|
|
7887
|
+
var CACHE_MAX_ENTRIES = 100;
|
|
7888
|
+
var cache = new Map;
|
|
7889
|
+
async function getLastUserModel(client, sessionId) {
|
|
7890
|
+
const now = Date.now();
|
|
7891
|
+
const cached = cache.get(sessionId);
|
|
7892
|
+
if (cached && cached.expiresAt > now) {
|
|
7893
|
+
cache.delete(sessionId);
|
|
7894
|
+
cache.set(sessionId, cached);
|
|
7895
|
+
return cached.model;
|
|
7896
|
+
}
|
|
7897
|
+
if (cached)
|
|
7898
|
+
cache.delete(sessionId);
|
|
7899
|
+
const c = client;
|
|
7900
|
+
const fetcher = c?.session?.messages;
|
|
7901
|
+
if (typeof fetcher !== "function") {
|
|
7902
|
+
return null;
|
|
7903
|
+
}
|
|
7904
|
+
let messages;
|
|
7905
|
+
try {
|
|
7906
|
+
const result = await fetcher({ path: { id: sessionId } });
|
|
7907
|
+
messages = result?.data ?? [];
|
|
7908
|
+
} catch {
|
|
7909
|
+
return null;
|
|
7910
|
+
}
|
|
7911
|
+
for (let i = messages.length - 1;i >= 0; i--) {
|
|
7912
|
+
const info = messages[i]?.info;
|
|
7913
|
+
if (info?.role !== "user")
|
|
7914
|
+
continue;
|
|
7915
|
+
if (isSyntheticIgnoredMessage(messages[i]))
|
|
7916
|
+
continue;
|
|
7917
|
+
const model = info.model;
|
|
7918
|
+
if (!model?.providerID || !model?.modelID)
|
|
7919
|
+
continue;
|
|
7920
|
+
const resolved = {
|
|
7921
|
+
providerID: model.providerID,
|
|
7922
|
+
modelID: model.modelID,
|
|
7923
|
+
...model.variant ? { variant: model.variant } : {}
|
|
7924
|
+
};
|
|
7925
|
+
setCache(sessionId, resolved);
|
|
7926
|
+
return resolved;
|
|
7927
|
+
}
|
|
7928
|
+
return null;
|
|
7929
|
+
}
|
|
7930
|
+
function setCache(sessionId, model) {
|
|
7931
|
+
if (cache.has(sessionId))
|
|
7932
|
+
cache.delete(sessionId);
|
|
7933
|
+
cache.set(sessionId, { expiresAt: Date.now() + CACHE_TTL_MS, model });
|
|
7934
|
+
while (cache.size > CACHE_MAX_ENTRIES) {
|
|
7935
|
+
const oldest = cache.keys().next().value;
|
|
7936
|
+
if (typeof oldest !== "string")
|
|
7937
|
+
break;
|
|
7938
|
+
cache.delete(oldest);
|
|
7939
|
+
}
|
|
7940
|
+
}
|
|
7941
|
+
function isSyntheticIgnoredMessage(message) {
|
|
7942
|
+
const parts = message?.info?.parts ?? message?.parts;
|
|
7943
|
+
return Array.isArray(parts) && parts.length > 0 && parts.every((part) => part?.ignored === true);
|
|
7944
|
+
}
|
|
7945
|
+
|
|
7881
7946
|
// src/bg-notifications.ts
|
|
7882
7947
|
var sessionBgStates = new Map;
|
|
7883
7948
|
var SESSION_BG_STATE_IDLE_TTL_MS = 60 * 60 * 1000;
|
|
7884
7949
|
var DEBOUNCE_STEP_MS = 200;
|
|
7885
7950
|
var DEBOUNCE_CAP_MS = 1000;
|
|
7951
|
+
var UNKNOWN_COMPLETION_TTL_MS = 5000;
|
|
7952
|
+
var UNKNOWN_COMPLETION_CAP = 32;
|
|
7886
7953
|
var DEFAULT_SESSION_ID = "__default__";
|
|
7887
7954
|
var LOG_PREFIX = "[aft-plugin] bg-notifications:";
|
|
7888
7955
|
function trackBgTask(sessionID, taskId) {
|
|
7889
|
-
stateFor(sessionID)
|
|
7956
|
+
const state = stateFor(sessionID);
|
|
7957
|
+
pruneUnknownCompletions(state, Date.now());
|
|
7958
|
+
const buffered = state.unknownCompletions.filter((entry) => entry.completion.task_id === taskId);
|
|
7959
|
+
state.unknownCompletions = state.unknownCompletions.filter((entry) => entry.completion.task_id !== taskId);
|
|
7960
|
+
if (buffered.length > 0) {
|
|
7961
|
+
for (const entry of buffered) {
|
|
7962
|
+
if (!state.pendingCompletions.some((pending) => pending.task_id === taskId)) {
|
|
7963
|
+
state.pendingCompletions.push(entry.completion);
|
|
7964
|
+
}
|
|
7965
|
+
}
|
|
7966
|
+
return;
|
|
7967
|
+
}
|
|
7968
|
+
state.outstandingTaskIds.add(taskId);
|
|
7890
7969
|
}
|
|
7891
7970
|
function ingestBgCompletions(sessionID, completions) {
|
|
7892
7971
|
if (!Array.isArray(completions) || completions.length === 0)
|
|
@@ -7896,8 +7975,10 @@ function ingestBgCompletions(sessionID, completions) {
|
|
|
7896
7975
|
for (const completion of completions) {
|
|
7897
7976
|
if (!isBgCompletion(completion))
|
|
7898
7977
|
continue;
|
|
7899
|
-
if (!state.outstandingTaskIds.has(completion.task_id))
|
|
7978
|
+
if (!state.outstandingTaskIds.has(completion.task_id)) {
|
|
7979
|
+
bufferUnknownCompletion(state, completion);
|
|
7900
7980
|
continue;
|
|
7981
|
+
}
|
|
7901
7982
|
state.outstandingTaskIds.delete(completion.task_id);
|
|
7902
7983
|
if (!state.pendingCompletions.some((pending) => pending.task_id === completion.task_id) && !accepted.some((pending) => pending.task_id === completion.task_id)) {
|
|
7903
7984
|
accepted.push(completion);
|
|
@@ -7906,6 +7987,10 @@ function ingestBgCompletions(sessionID, completions) {
|
|
|
7906
7987
|
state.pendingCompletions.push(...accepted);
|
|
7907
7988
|
return accepted;
|
|
7908
7989
|
}
|
|
7990
|
+
async function handlePushedBgCompletion(drainContext, completion) {
|
|
7991
|
+
ingestBgCompletions(drainContext.sessionID, [completion]);
|
|
7992
|
+
await triggerWakeIfPending(drainContext, true);
|
|
7993
|
+
}
|
|
7909
7994
|
async function appendInTurnBgCompletions(drainContext, output) {
|
|
7910
7995
|
if (!output)
|
|
7911
7996
|
return;
|
|
@@ -7922,43 +8007,55 @@ async function appendInTurnBgCompletions(drainContext, output) {
|
|
|
7922
8007
|
state.pendingCompletions = [];
|
|
7923
8008
|
}
|
|
7924
8009
|
async function handleIdleBgCompletions(drainContext) {
|
|
8010
|
+
await triggerWakeIfPending(drainContext, false);
|
|
8011
|
+
}
|
|
8012
|
+
async function triggerWakeIfPending(drainContext, skipDrain) {
|
|
7925
8013
|
const state = stateFor(drainContext.sessionID);
|
|
7926
8014
|
if (state.wakeFiredThisIdle)
|
|
7927
8015
|
return;
|
|
7928
|
-
if (
|
|
8016
|
+
if (drainContext.isActive?.())
|
|
8017
|
+
return;
|
|
8018
|
+
if (!skipDrain && state.outstandingTaskIds.size > 0) {
|
|
7929
8019
|
await drainCompletions(drainContext);
|
|
7930
8020
|
}
|
|
7931
8021
|
if (state.pendingCompletions.length === 0)
|
|
7932
8022
|
return;
|
|
7933
8023
|
scheduleWake(state, async (reminder) => {
|
|
7934
|
-
|
|
7935
|
-
|
|
7936
|
-
|
|
7937
|
-
|
|
7938
|
-
|
|
7939
|
-
|
|
7940
|
-
|
|
7941
|
-
|
|
7942
|
-
|
|
7943
|
-
|
|
7944
|
-
|
|
7945
|
-
|
|
7946
|
-
|
|
7947
|
-
|
|
7948
|
-
|
|
8024
|
+
const client = drainContext.client;
|
|
8025
|
+
if (typeof client.session?.promptAsync !== "function") {
|
|
8026
|
+
throw new Error("client.session.promptAsync is unavailable");
|
|
8027
|
+
}
|
|
8028
|
+
const lastModel = await getLastUserModel(client, drainContext.sessionID);
|
|
8029
|
+
const body = {
|
|
8030
|
+
noReply: false,
|
|
8031
|
+
parts: [{ type: "text", text: reminder }]
|
|
8032
|
+
};
|
|
8033
|
+
if (lastModel) {
|
|
8034
|
+
body.model = { providerID: lastModel.providerID, modelID: lastModel.modelID };
|
|
8035
|
+
if (lastModel.variant)
|
|
8036
|
+
body.variant = lastModel.variant;
|
|
8037
|
+
}
|
|
8038
|
+
await client.session.promptAsync({
|
|
8039
|
+
path: { id: drainContext.sessionID },
|
|
8040
|
+
body
|
|
8041
|
+
});
|
|
8042
|
+
}, (err) => {
|
|
8043
|
+
sessionWarn(drainContext.sessionID, `${LOG_PREFIX} wake send failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
7949
8044
|
});
|
|
7950
8045
|
}
|
|
7951
8046
|
function resetBgWake(sessionID) {
|
|
7952
8047
|
stateFor(sessionID).wakeFiredThisIdle = false;
|
|
7953
8048
|
}
|
|
7954
8049
|
function formatSystemReminder(completions) {
|
|
7955
|
-
const bullets = completions.map((completion) =>
|
|
8050
|
+
const bullets = completions.map((completion) => formatCompletion(completion)).join(`
|
|
7956
8051
|
`);
|
|
8052
|
+
const anyTruncated = completions.some((c) => c.output_truncated === true);
|
|
8053
|
+
const tail = anyTruncated ? `
|
|
8054
|
+
|
|
8055
|
+
For truncated tasks, use bash_status({ taskId: "..." }) to retrieve full output.` : "";
|
|
7957
8056
|
return `<system-reminder>
|
|
7958
8057
|
[BACKGROUND BASH COMPLETED]
|
|
7959
|
-
${bullets}
|
|
7960
|
-
|
|
7961
|
-
Use bash_status({ task_id: "..." }) to retrieve full output.
|
|
8058
|
+
${bullets}${tail}
|
|
7962
8059
|
</system-reminder>`;
|
|
7963
8060
|
}
|
|
7964
8061
|
function extractSessionID(value) {
|
|
@@ -7984,15 +8081,15 @@ async function drainCompletions({ ctx, directory, sessionID }) {
|
|
|
7984
8081
|
const bridge = ctx.pool.getAnyActiveBridge(directory) ?? ctx.pool.getBridge(directory);
|
|
7985
8082
|
const response = await bridge.send("bash_drain_completions", { session_id: sessionID });
|
|
7986
8083
|
if (response.success === false) {
|
|
7987
|
-
|
|
8084
|
+
sessionWarn(sessionID, `${LOG_PREFIX} drain failed: ${String(response.message ?? "unknown error")}`);
|
|
7988
8085
|
return;
|
|
7989
8086
|
}
|
|
7990
8087
|
ingestBgCompletions(sessionID, response.bg_completions);
|
|
7991
8088
|
} catch (err) {
|
|
7992
|
-
|
|
8089
|
+
sessionWarn(sessionID, `${LOG_PREFIX} drain failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
7993
8090
|
}
|
|
7994
8091
|
}
|
|
7995
|
-
function scheduleWake(state, sendWake) {
|
|
8092
|
+
function scheduleWake(state, sendWake, onSendFailure) {
|
|
7996
8093
|
const now = Date.now();
|
|
7997
8094
|
if (state.debounceTimer && state.pendingCompletions.length <= state.scheduledCompletionCount) {
|
|
7998
8095
|
return;
|
|
@@ -8007,16 +8104,24 @@ function scheduleWake(state, sendWake) {
|
|
|
8007
8104
|
state.scheduledCompletionCount = state.pendingCompletions.length;
|
|
8008
8105
|
if (state.debounceTimer)
|
|
8009
8106
|
clearTimeout(state.debounceTimer);
|
|
8010
|
-
const delay = Math.max(0, (state.scheduledFireAt ?? now) - now);
|
|
8107
|
+
const delay = state.retryDelayMs ?? Math.max(0, (state.scheduledFireAt ?? now) - now);
|
|
8011
8108
|
state.debounceTimer = setTimeout(() => {
|
|
8012
|
-
const
|
|
8109
|
+
const pending = state.pendingCompletions;
|
|
8110
|
+
const reminder = formatSystemReminder(pending);
|
|
8013
8111
|
state.pendingCompletions = [];
|
|
8014
8112
|
state.debounceTimer = null;
|
|
8015
|
-
state.wakeFiredThisIdle = true;
|
|
8016
8113
|
state.firstCompletionAt = null;
|
|
8017
8114
|
state.scheduledFireAt = null;
|
|
8018
8115
|
state.scheduledCompletionCount = 0;
|
|
8019
|
-
sendWake(reminder)
|
|
8116
|
+
sendWake(reminder).then(() => {
|
|
8117
|
+
state.retryDelayMs = null;
|
|
8118
|
+
state.wakeFiredThisIdle = true;
|
|
8119
|
+
}).catch((err) => {
|
|
8120
|
+
state.pendingCompletions = [...pending, ...state.pendingCompletions];
|
|
8121
|
+
state.retryDelayMs = Math.min((delay || DEBOUNCE_STEP_MS) * 2, DEBOUNCE_CAP_MS);
|
|
8122
|
+
onSendFailure(err);
|
|
8123
|
+
scheduleWake(state, sendWake, onSendFailure);
|
|
8124
|
+
});
|
|
8020
8125
|
}, delay);
|
|
8021
8126
|
state.debounceTimer.unref?.();
|
|
8022
8127
|
}
|
|
@@ -8034,6 +8139,8 @@ function stateFor(sessionID) {
|
|
|
8034
8139
|
firstCompletionAt: null,
|
|
8035
8140
|
scheduledFireAt: null,
|
|
8036
8141
|
scheduledCompletionCount: 0,
|
|
8142
|
+
retryDelayMs: null,
|
|
8143
|
+
unknownCompletions: [],
|
|
8037
8144
|
lastSeenAt: now
|
|
8038
8145
|
};
|
|
8039
8146
|
sessionBgStates.set(key, state);
|
|
@@ -8054,6 +8161,18 @@ function cleanupIdleSessionStates(now) {
|
|
|
8054
8161
|
sessionBgStates.delete(sessionID);
|
|
8055
8162
|
}
|
|
8056
8163
|
}
|
|
8164
|
+
function bufferUnknownCompletion(state, completion) {
|
|
8165
|
+
const now = Date.now();
|
|
8166
|
+
pruneUnknownCompletions(state, now);
|
|
8167
|
+
state.unknownCompletions = state.unknownCompletions.filter((entry) => entry.completion.task_id !== completion.task_id);
|
|
8168
|
+
state.unknownCompletions.push({ completion, receivedAt: now });
|
|
8169
|
+
if (state.unknownCompletions.length > UNKNOWN_COMPLETION_CAP) {
|
|
8170
|
+
state.unknownCompletions.splice(0, state.unknownCompletions.length - UNKNOWN_COMPLETION_CAP);
|
|
8171
|
+
}
|
|
8172
|
+
}
|
|
8173
|
+
function pruneUnknownCompletions(state, now) {
|
|
8174
|
+
state.unknownCompletions = state.unknownCompletions.filter((entry) => now - entry.receivedAt <= UNKNOWN_COMPLETION_TTL_MS);
|
|
8175
|
+
}
|
|
8057
8176
|
function isBgCompletion(value) {
|
|
8058
8177
|
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
8059
8178
|
return false;
|
|
@@ -8068,10 +8187,26 @@ ${reminder}` : reminder;
|
|
|
8068
8187
|
function formatCompletion(completion) {
|
|
8069
8188
|
const status = formatStatus(completion);
|
|
8070
8189
|
const duration = formatDuration(completion);
|
|
8071
|
-
|
|
8190
|
+
const header = `- task ${completion.task_id} (${status}${duration ? `, ${duration}` : ""})`;
|
|
8191
|
+
const previewBlock = formatOutputPreview(completion);
|
|
8192
|
+
return previewBlock ? `${header}
|
|
8193
|
+
${previewBlock}` : header;
|
|
8194
|
+
}
|
|
8195
|
+
function formatOutputPreview(completion) {
|
|
8196
|
+
const ansiRegex = /\x1b\[[0-9;]*[a-zA-Z]/g;
|
|
8197
|
+
const raw = (completion.output_preview ?? "").replace(ansiRegex, "");
|
|
8198
|
+
if (!raw.trim())
|
|
8199
|
+
return "";
|
|
8200
|
+
const trimmed = raw.replace(/\n+$/, "");
|
|
8201
|
+
const ellipsis = completion.output_truncated ? "\u2026" : "";
|
|
8202
|
+
const indented = trimmed.split(`
|
|
8203
|
+
`).map((line) => ` ${line}`).join(`
|
|
8204
|
+
`);
|
|
8205
|
+
return ellipsis ? ` ${ellipsis}
|
|
8206
|
+
${indented}` : indented;
|
|
8072
8207
|
}
|
|
8073
8208
|
function formatStatus(completion) {
|
|
8074
|
-
if (completion.status === "timeout")
|
|
8209
|
+
if (completion.status === "timed_out" || completion.status === "timeout")
|
|
8075
8210
|
return "timed out";
|
|
8076
8211
|
if (completion.status === "killed")
|
|
8077
8212
|
return "killed";
|
|
@@ -21693,6 +21828,7 @@ var ExperimentalConfigSchema = exports_external.object({
|
|
|
21693
21828
|
});
|
|
21694
21829
|
var AftConfigSchema = exports_external.object({
|
|
21695
21830
|
format_on_edit: exports_external.boolean().optional(),
|
|
21831
|
+
formatter_timeout_secs: exports_external.number().int().min(1).max(600).optional(),
|
|
21696
21832
|
validate_on_edit: exports_external.enum(["syntax", "full"]).optional(),
|
|
21697
21833
|
formatter: exports_external.record(exports_external.string(), FormatterEnum).optional(),
|
|
21698
21834
|
checker: exports_external.record(exports_external.string(), CheckerEnum).optional(),
|
|
@@ -24332,13 +24468,17 @@ async function getSessionMessages(client, sessionId) {
|
|
|
24332
24468
|
async function sendIgnoredMessage(client, sessionId, text) {
|
|
24333
24469
|
try {
|
|
24334
24470
|
const c = client;
|
|
24335
|
-
const
|
|
24336
|
-
|
|
24337
|
-
|
|
24338
|
-
|
|
24339
|
-
parts: [{ type: "text", text, ignored: true }]
|
|
24340
|
-
}
|
|
24471
|
+
const lastModel = await getLastUserModel(client, sessionId);
|
|
24472
|
+
const body = {
|
|
24473
|
+
noReply: true,
|
|
24474
|
+
parts: [{ type: "text", text, ignored: true }]
|
|
24341
24475
|
};
|
|
24476
|
+
if (lastModel) {
|
|
24477
|
+
body.model = { providerID: lastModel.providerID, modelID: lastModel.modelID };
|
|
24478
|
+
if (lastModel.variant)
|
|
24479
|
+
body.variant = lastModel.variant;
|
|
24480
|
+
}
|
|
24481
|
+
const promptInput = { path: { id: sessionId }, body };
|
|
24342
24482
|
if (typeof c.session?.prompt === "function") {
|
|
24343
24483
|
await Promise.resolve(c.session.prompt(promptInput));
|
|
24344
24484
|
return true;
|
|
@@ -24348,7 +24488,7 @@ async function sendIgnoredMessage(client, sessionId, text) {
|
|
|
24348
24488
|
return true;
|
|
24349
24489
|
}
|
|
24350
24490
|
} catch (err) {
|
|
24351
|
-
|
|
24491
|
+
sessionLog(sessionId, `[aft-plugin] notification send failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
24352
24492
|
}
|
|
24353
24493
|
return false;
|
|
24354
24494
|
}
|
|
@@ -24374,7 +24514,7 @@ async function sendWarning(opts, message) {
|
|
|
24374
24514
|
if (!sessionId)
|
|
24375
24515
|
return;
|
|
24376
24516
|
const text = `${WARNING_MARKER} ${message}`;
|
|
24377
|
-
|
|
24517
|
+
sessionLog(sessionId, `[aft-plugin] sending warning to session ${sessionId}`);
|
|
24378
24518
|
await sendIgnoredMessage(opts.client, sessionId, text);
|
|
24379
24519
|
}
|
|
24380
24520
|
async function sendFeatureAnnouncement(opts, version2, features, storageDir) {
|
|
@@ -24397,7 +24537,7 @@ async function sendFeatureAnnouncement(opts, version2, features, storageDir) {
|
|
|
24397
24537
|
return;
|
|
24398
24538
|
const text = [`${FEATURE_MARKER} v${version2}:`, ...features.map((f) => ` \u2022 ${f}`)].join(`
|
|
24399
24539
|
`);
|
|
24400
|
-
|
|
24540
|
+
sessionLog(sessionId, `[aft-plugin] sending feature announcement for v${version2}`);
|
|
24401
24541
|
await sendIgnoredMessage(opts.client, sessionId, text);
|
|
24402
24542
|
}
|
|
24403
24543
|
if (storageDir) {
|
|
@@ -24515,7 +24655,7 @@ async function cleanupWarnings(opts) {
|
|
|
24515
24655
|
}
|
|
24516
24656
|
if (warningIds.length === 0)
|
|
24517
24657
|
return;
|
|
24518
|
-
|
|
24658
|
+
sessionLog(sessionId, `[aft-plugin] cleaning up ${warningIds.length} stale warning(s)`);
|
|
24519
24659
|
for (const id of warningIds) {
|
|
24520
24660
|
await deleteMessage(effectiveServerUrl, sessionId, id);
|
|
24521
24661
|
}
|
|
@@ -24968,6 +25108,9 @@ function isProcessAlive2(pid) {
|
|
|
24968
25108
|
}
|
|
24969
25109
|
}
|
|
24970
25110
|
|
|
25111
|
+
// src/pool.ts
|
|
25112
|
+
import { realpathSync as realpathSync3 } from "fs";
|
|
25113
|
+
|
|
24971
25114
|
// src/bridge.ts
|
|
24972
25115
|
import { spawn as spawn3 } from "child_process";
|
|
24973
25116
|
import { homedir as homedir7 } from "os";
|
|
@@ -25060,6 +25203,7 @@ class BinaryBridge {
|
|
|
25060
25203
|
minVersion;
|
|
25061
25204
|
onVersionMismatch;
|
|
25062
25205
|
onConfigureWarnings;
|
|
25206
|
+
onBashCompletion;
|
|
25063
25207
|
restartResetTimer = null;
|
|
25064
25208
|
constructor(binaryPath, cwd, options, configOverrides) {
|
|
25065
25209
|
this.binaryPath = binaryPath;
|
|
@@ -25070,6 +25214,7 @@ class BinaryBridge {
|
|
|
25070
25214
|
this.minVersion = options?.minVersion;
|
|
25071
25215
|
this.onVersionMismatch = options?.onVersionMismatch;
|
|
25072
25216
|
this.onConfigureWarnings = options?.onConfigureWarnings;
|
|
25217
|
+
this.onBashCompletion = options?.onBashCompletion;
|
|
25073
25218
|
}
|
|
25074
25219
|
get restartCount() {
|
|
25075
25220
|
return this._restartCount;
|
|
@@ -25077,6 +25222,9 @@ class BinaryBridge {
|
|
|
25077
25222
|
isAlive() {
|
|
25078
25223
|
return this.process !== null && this.process.exitCode === null && !this.process.killed;
|
|
25079
25224
|
}
|
|
25225
|
+
hasPendingRequests() {
|
|
25226
|
+
return this.pending.size > 0;
|
|
25227
|
+
}
|
|
25080
25228
|
async send(command, params = {}, options) {
|
|
25081
25229
|
if (this._shuttingDown) {
|
|
25082
25230
|
throw new Error(`[aft-plugin] Bridge is shutting down, cannot send "${command}"`);
|
|
@@ -25130,17 +25278,21 @@ class BinaryBridge {
|
|
|
25130
25278
|
`;
|
|
25131
25279
|
const effectiveTimeoutMs = options?.transportTimeoutMs ?? options?.timeoutMs ?? this.timeoutMs;
|
|
25132
25280
|
const requestSessionId = typeof params.session_id === "string" && params.session_id.length > 0 ? params.session_id : undefined;
|
|
25281
|
+
const keepBridgeOnTimeout = options?.keepBridgeOnTimeout === true;
|
|
25133
25282
|
return new Promise((resolve4, reject) => {
|
|
25134
25283
|
const timer = setTimeout(() => {
|
|
25135
25284
|
this.pending.delete(id);
|
|
25136
|
-
const
|
|
25285
|
+
const restartSuffix = keepBridgeOnTimeout ? "" : " \u2014 restarting bridge";
|
|
25286
|
+
const timeoutMsg = `Request "${command}" (id=${id}) timed out after ${effectiveTimeoutMs}ms${restartSuffix}`;
|
|
25137
25287
|
if (requestSessionId) {
|
|
25138
25288
|
sessionWarn(requestSessionId, timeoutMsg);
|
|
25139
25289
|
} else {
|
|
25140
25290
|
warn(timeoutMsg);
|
|
25141
25291
|
}
|
|
25142
25292
|
reject(new Error(`[aft-plugin] Request "${command}" (id=${id}) timed out after ${effectiveTimeoutMs}ms`));
|
|
25143
|
-
|
|
25293
|
+
if (!keepBridgeOnTimeout) {
|
|
25294
|
+
this.handleTimeout();
|
|
25295
|
+
}
|
|
25144
25296
|
}, effectiveTimeoutMs);
|
|
25145
25297
|
this.pending.set(id, { resolve: resolve4, reject, timer, onProgress: options?.onProgress });
|
|
25146
25298
|
if (!this.process?.stdin?.writable) {
|
|
@@ -25345,6 +25497,10 @@ class BinaryBridge {
|
|
|
25345
25497
|
}
|
|
25346
25498
|
continue;
|
|
25347
25499
|
}
|
|
25500
|
+
if (response.type === "bash_completed") {
|
|
25501
|
+
this.onBashCompletion?.(response, this);
|
|
25502
|
+
continue;
|
|
25503
|
+
}
|
|
25348
25504
|
const id = response.id;
|
|
25349
25505
|
if (id && this.pending.has(id)) {
|
|
25350
25506
|
const entry = this.pending.get(id);
|
|
@@ -25354,6 +25510,8 @@ class BinaryBridge {
|
|
|
25354
25510
|
clearTimeout(entry.timer);
|
|
25355
25511
|
this.scheduleRestartCountReset();
|
|
25356
25512
|
entry.resolve(response);
|
|
25513
|
+
} else if (typeof response.type === "string") {
|
|
25514
|
+
log(`Ignoring unknown stdout push frame type: ${response.type}`);
|
|
25357
25515
|
}
|
|
25358
25516
|
} catch (_err) {
|
|
25359
25517
|
warn(`Failed to parse stdout line: ${line}`);
|
|
@@ -25451,7 +25609,8 @@ class BridgePool {
|
|
|
25451
25609
|
maxRestarts: options.maxRestarts,
|
|
25452
25610
|
minVersion: options.minVersion,
|
|
25453
25611
|
onVersionMismatch: options.onVersionMismatch,
|
|
25454
|
-
onConfigureWarnings: options.onConfigureWarnings
|
|
25612
|
+
onConfigureWarnings: options.onConfigureWarnings,
|
|
25613
|
+
onBashCompletion: options.onBashCompletion
|
|
25455
25614
|
};
|
|
25456
25615
|
this.configOverrides = configOverrides;
|
|
25457
25616
|
if (Number.isFinite(this.idleTimeoutMs)) {
|
|
@@ -25474,6 +25633,14 @@ class BridgePool {
|
|
|
25474
25633
|
}
|
|
25475
25634
|
return null;
|
|
25476
25635
|
}
|
|
25636
|
+
getActiveBridgeForRoot(projectRoot) {
|
|
25637
|
+
const key = normalizeKey(projectRoot);
|
|
25638
|
+
const entry = this.bridges.get(key);
|
|
25639
|
+
if (!entry?.bridge.isAlive())
|
|
25640
|
+
return null;
|
|
25641
|
+
entry.lastUsed = Date.now();
|
|
25642
|
+
return entry.bridge;
|
|
25643
|
+
}
|
|
25477
25644
|
getBridge(projectRoot) {
|
|
25478
25645
|
const key = normalizeKey(projectRoot);
|
|
25479
25646
|
const existing = this.bridges.get(key);
|
|
@@ -25533,7 +25700,12 @@ class BridgePool {
|
|
|
25533
25700
|
}
|
|
25534
25701
|
}
|
|
25535
25702
|
function normalizeKey(projectRoot) {
|
|
25536
|
-
|
|
25703
|
+
const stripped = projectRoot.replace(/[/\\]+$/, "");
|
|
25704
|
+
try {
|
|
25705
|
+
return realpathSync3(stripped);
|
|
25706
|
+
} catch {
|
|
25707
|
+
return stripped;
|
|
25708
|
+
}
|
|
25537
25709
|
}
|
|
25538
25710
|
|
|
25539
25711
|
// src/resolver.ts
|
|
@@ -25771,9 +25943,7 @@ class AftRpcServer {
|
|
|
25771
25943
|
return;
|
|
25772
25944
|
}
|
|
25773
25945
|
const { token: _token, ...handlerParams } = params;
|
|
25774
|
-
log(`RPC call: ${method} params=${JSON.stringify(handlerParams).slice(0, 200)}`);
|
|
25775
25946
|
handler(handlerParams).then((result) => {
|
|
25776
|
-
log(`RPC result: ${method} => ${JSON.stringify(result).slice(0, 200)}`);
|
|
25777
25947
|
res.writeHead(200, { "Content-Type": "application/json" });
|
|
25778
25948
|
res.end(JSON.stringify(result));
|
|
25779
25949
|
}).catch((err) => {
|
|
@@ -26025,8 +26195,9 @@ import {
|
|
|
26025
26195
|
} from "fs";
|
|
26026
26196
|
import { isIP } from "net";
|
|
26027
26197
|
import { join as join17 } from "path";
|
|
26198
|
+
import { Agent, fetch as undiciFetch } from "undici";
|
|
26028
26199
|
var MAX_RESPONSE_BYTES = 10 * 1024 * 1024;
|
|
26029
|
-
var
|
|
26200
|
+
var CACHE_TTL_MS2 = 24 * 60 * 60 * 1000;
|
|
26030
26201
|
var FETCH_TIMEOUT_MS = 30000;
|
|
26031
26202
|
var MAX_REDIRECTS = 5;
|
|
26032
26203
|
function cacheDir(storageDir) {
|
|
@@ -26116,7 +26287,7 @@ function isPrivateIp(address) {
|
|
|
26116
26287
|
}
|
|
26117
26288
|
return true;
|
|
26118
26289
|
}
|
|
26119
|
-
async function assertPublicUrl(url2, allowPrivate) {
|
|
26290
|
+
async function assertPublicUrl(url2, allowPrivate, dnsLookup = lookup) {
|
|
26120
26291
|
if (url2.protocol !== "http:" && url2.protocol !== "https:") {
|
|
26121
26292
|
throw new Error(`Only http:// and https:// URLs are supported, got: ${url2.protocol}`);
|
|
26122
26293
|
}
|
|
@@ -26127,14 +26298,27 @@ async function assertPublicUrl(url2, allowPrivate) {
|
|
|
26127
26298
|
if (isPrivateIp(hostname3)) {
|
|
26128
26299
|
throw new Error(`Blocked private URL host ${url2.hostname} (${hostname3})`);
|
|
26129
26300
|
}
|
|
26130
|
-
return;
|
|
26301
|
+
return hostname3;
|
|
26131
26302
|
}
|
|
26132
|
-
const addresses = await
|
|
26303
|
+
const addresses = await dnsLookup(hostname3, { all: true, verbatim: true });
|
|
26133
26304
|
for (const { address } of addresses) {
|
|
26134
26305
|
if (isPrivateIp(address)) {
|
|
26135
26306
|
throw new Error(`Blocked private URL host ${url2.hostname} (${address})`);
|
|
26136
26307
|
}
|
|
26137
26308
|
}
|
|
26309
|
+
if (addresses.length === 0) {
|
|
26310
|
+
throw new Error(`Failed to resolve URL host ${url2.hostname}`);
|
|
26311
|
+
}
|
|
26312
|
+
return addresses[0].address;
|
|
26313
|
+
}
|
|
26314
|
+
function createPinnedDispatcher(validatedIp) {
|
|
26315
|
+
return new Agent({
|
|
26316
|
+
connect: {
|
|
26317
|
+
lookup: (_hostname, _opts, callback) => {
|
|
26318
|
+
callback(null, validatedIp, validatedIp.includes(":") ? 6 : 4);
|
|
26319
|
+
}
|
|
26320
|
+
}
|
|
26321
|
+
});
|
|
26138
26322
|
}
|
|
26139
26323
|
function resolveRedirectUrl(currentUrl, location) {
|
|
26140
26324
|
if (!location) {
|
|
@@ -26142,17 +26326,21 @@ function resolveRedirectUrl(currentUrl, location) {
|
|
|
26142
26326
|
}
|
|
26143
26327
|
return new URL(location, currentUrl);
|
|
26144
26328
|
}
|
|
26145
|
-
async function fetchWithRedirects(startUrl, allowPrivate) {
|
|
26329
|
+
async function fetchWithRedirects(startUrl, allowPrivate, deps = {}) {
|
|
26146
26330
|
let currentUrl = startUrl;
|
|
26331
|
+
const fetchImpl = deps.fetchImpl ?? undiciFetch;
|
|
26332
|
+
const dnsLookup = deps.lookup ?? lookup;
|
|
26147
26333
|
for (let redirectCount = 0;redirectCount <= MAX_REDIRECTS; redirectCount++) {
|
|
26148
|
-
await assertPublicUrl(currentUrl, allowPrivate);
|
|
26334
|
+
const validatedIp = await assertPublicUrl(currentUrl, allowPrivate, dnsLookup);
|
|
26335
|
+
const dispatcher = validatedIp ? (deps.dispatcherFactory ?? createPinnedDispatcher)(validatedIp) : undefined;
|
|
26149
26336
|
const controller = new AbortController;
|
|
26150
26337
|
const timer = setTimeout(() => controller.abort(), FETCH_TIMEOUT_MS);
|
|
26151
26338
|
let response;
|
|
26152
26339
|
try {
|
|
26153
|
-
response = await
|
|
26340
|
+
response = await fetchImpl(currentUrl.href, {
|
|
26154
26341
|
signal: controller.signal,
|
|
26155
26342
|
redirect: "manual",
|
|
26343
|
+
dispatcher,
|
|
26156
26344
|
headers: {
|
|
26157
26345
|
"user-agent": "aft-opencode-plugin",
|
|
26158
26346
|
accept: "application/vnd.github.raw, text/markdown, text/x-markdown, text/html;q=0.9, text/plain;q=0.5"
|
|
@@ -26206,14 +26394,14 @@ async function fetchUrlToTempFile(url2, storageDir, options = {}) {
|
|
|
26206
26394
|
const meta4 = JSON.parse(readFileSync8(metaFile, "utf8"));
|
|
26207
26395
|
const age = Date.now() - meta4.fetchedAt;
|
|
26208
26396
|
const cached2 = contentPath(storageDir, hash2, meta4.extension);
|
|
26209
|
-
if (age <
|
|
26397
|
+
if (age < CACHE_TTL_MS2 && existsSync11(cached2)) {
|
|
26210
26398
|
log(`URL cache hit: ${url2} (${Math.round(age / 1000)}s old)`);
|
|
26211
26399
|
return cached2;
|
|
26212
26400
|
}
|
|
26213
26401
|
} catch {}
|
|
26214
26402
|
}
|
|
26215
26403
|
log(`Fetching URL: ${url2}`);
|
|
26216
|
-
const response = await fetchWithRedirects(parsed, allowPrivate);
|
|
26404
|
+
const response = await fetchWithRedirects(parsed, allowPrivate, options);
|
|
26217
26405
|
if (!response.ok) {
|
|
26218
26406
|
throw new Error(`HTTP ${response.status} ${response.statusText} fetching ${url2}`);
|
|
26219
26407
|
}
|
|
@@ -26279,7 +26467,7 @@ function cleanupUrlCache(storageDir) {
|
|
|
26279
26467
|
try {
|
|
26280
26468
|
const meta3 = JSON.parse(readFileSync8(metaFile, "utf8"));
|
|
26281
26469
|
const age = Date.now() - meta3.fetchedAt;
|
|
26282
|
-
if (age >
|
|
26470
|
+
if (age > CACHE_TTL_MS2) {
|
|
26283
26471
|
const hash2 = entry.slice(0, -".meta.json".length);
|
|
26284
26472
|
const content = contentPath(storageDir, hash2, meta3.extension);
|
|
26285
26473
|
if (existsSync11(content))
|
|
@@ -27079,6 +27267,7 @@ function createBashTool(ctx) {
|
|
|
27079
27267
|
permissions_requested: true
|
|
27080
27268
|
}, callBridge, {
|
|
27081
27269
|
transportTimeoutMs: bashTransportTimeoutMs(args.timeout),
|
|
27270
|
+
keepBridgeOnTimeout: true,
|
|
27082
27271
|
onProgress: ({ text }) => {
|
|
27083
27272
|
accumulatedOutput = preview(accumulatedOutput + text);
|
|
27084
27273
|
metadata?.({ output: accumulatedOutput, description });
|
|
@@ -27140,6 +27329,49 @@ function createBashTool(ctx) {
|
|
|
27140
27329
|
}
|
|
27141
27330
|
};
|
|
27142
27331
|
}
|
|
27332
|
+
function createBashStatusTool(ctx) {
|
|
27333
|
+
return {
|
|
27334
|
+
description: "Check the status and captured output of a background bash task spawned with bash({ background: true }). Returns status (running | completed | failed | killed | timed_out), exit code, duration, and a preview of captured output.",
|
|
27335
|
+
args: {
|
|
27336
|
+
taskId: z3.string().describe("Background task ID returned by bash({ background: true }), e.g. bgb-6b454047.")
|
|
27337
|
+
},
|
|
27338
|
+
execute: async (args, context) => {
|
|
27339
|
+
const data = await callBridge(ctx, context, "bash_status", {
|
|
27340
|
+
task_id: args.taskId
|
|
27341
|
+
});
|
|
27342
|
+
if (data.success === false) {
|
|
27343
|
+
throw new Error(data.message ?? "bash_status failed");
|
|
27344
|
+
}
|
|
27345
|
+
const status = data.status;
|
|
27346
|
+
const exit = typeof data.exit_code === "number" ? ` (exit ${data.exit_code})` : "";
|
|
27347
|
+
const dur = typeof data.duration_ms === "number" ? ` ${Math.round(data.duration_ms / 1000)}s` : "";
|
|
27348
|
+
let text = `Task ${args.taskId}: ${status}${exit}${dur}`;
|
|
27349
|
+
const preview = data.output_preview;
|
|
27350
|
+
if (preview && status !== "running") {
|
|
27351
|
+
text += `
|
|
27352
|
+
${preview.slice(0, 2000)}`;
|
|
27353
|
+
}
|
|
27354
|
+
return text;
|
|
27355
|
+
}
|
|
27356
|
+
};
|
|
27357
|
+
}
|
|
27358
|
+
function createBashKillTool(ctx) {
|
|
27359
|
+
return {
|
|
27360
|
+
description: "Terminate a running background bash task spawned with bash({ background: true }). Returns confirmation of kill or an error if the task already finished.",
|
|
27361
|
+
args: {
|
|
27362
|
+
taskId: z3.string().describe("Background task ID returned by bash({ background: true }), e.g. bgb-6b454047.")
|
|
27363
|
+
},
|
|
27364
|
+
execute: async (args, context) => {
|
|
27365
|
+
const data = await callBridge(ctx, context, "bash_kill", {
|
|
27366
|
+
task_id: args.taskId
|
|
27367
|
+
});
|
|
27368
|
+
if (data.success === false) {
|
|
27369
|
+
throw new Error(data.message ?? "bash_kill failed");
|
|
27370
|
+
}
|
|
27371
|
+
return `Task ${args.taskId}: ${String(data.status ?? "killed")}`;
|
|
27372
|
+
}
|
|
27373
|
+
};
|
|
27374
|
+
}
|
|
27143
27375
|
function bashTransportTimeoutMs(timeout) {
|
|
27144
27376
|
const bashTimeout = timeout ?? DEFAULT_BASH_TIMEOUT_MS;
|
|
27145
27377
|
return Math.max(30000, bashTimeout + BASH_TRANSPORT_TIMEOUT_OVERHEAD_MS);
|
|
@@ -27770,6 +28002,11 @@ function createEditTool(ctx, writeToolName = "write") {
|
|
|
27770
28002
|
}
|
|
27771
28003
|
}
|
|
27772
28004
|
let result = JSON.stringify(data);
|
|
28005
|
+
const globSkipNote = formatGlobSkipReasonsNote(data.format_skip_reasons);
|
|
28006
|
+
if (globSkipNote)
|
|
28007
|
+
result += `
|
|
28008
|
+
|
|
28009
|
+
${globSkipNote}`;
|
|
27773
28010
|
if (!args.dryRun) {
|
|
27774
28011
|
const diags = data.lsp_diagnostics;
|
|
27775
28012
|
if (diags && diags.length > 0) {
|
|
@@ -27800,6 +28037,14 @@ Note: LSP server(s) exited during this edit: ${exitedServers.join(", ")}. Their
|
|
|
27800
28037
|
}
|
|
27801
28038
|
};
|
|
27802
28039
|
}
|
|
28040
|
+
function formatGlobSkipReasonsNote(reasons) {
|
|
28041
|
+
if (!Array.isArray(reasons))
|
|
28042
|
+
return;
|
|
28043
|
+
const actionable = reasons.filter((reason) => typeof reason === "string").filter((reason) => ["formatter_not_installed", "formatter_excluded_path", "timeout", "error"].includes(reason));
|
|
28044
|
+
if (actionable.length === 0)
|
|
28045
|
+
return;
|
|
28046
|
+
return `Note: formatter skipped some glob edit result file(s): ${[...new Set(actionable)].sort().join(", ")}. See per-file format_skipped_reason values for details.`;
|
|
28047
|
+
}
|
|
27803
28048
|
var APPLY_PATCH_DESCRIPTION = `Use the \`apply_patch\` tool to edit files. Your patch language is a stripped\u2011down, file\u2011oriented diff format designed to be easy to parse and safe to apply. You can think of it as a high\u2011level envelope:
|
|
27804
28049
|
|
|
27805
28050
|
*** Begin Patch
|
|
@@ -28051,8 +28296,8 @@ ${diagLines}`);
|
|
|
28051
28296
|
const summary = partial2 ? `Patch partially applied \u2014 ${hunks.length - failures.length} of ${hunks.length} hunk(s) succeeded. Failed: ${failures.join(", ")}. Successful changes are kept; use \`aft_safety\` to revert if you want to abort.` : `Patch failed \u2014 none of the ${hunks.length} hunk(s) applied: ${failures.join(", ")}.`;
|
|
28052
28297
|
results.push(summary);
|
|
28053
28298
|
if (!partial2) {
|
|
28054
|
-
|
|
28055
|
-
`);
|
|
28299
|
+
throw new Error(results.join(`
|
|
28300
|
+
`));
|
|
28056
28301
|
}
|
|
28057
28302
|
}
|
|
28058
28303
|
const callID = getCallID2(context);
|
|
@@ -28158,6 +28403,8 @@ function createMoveTool(ctx) {
|
|
|
28158
28403
|
function hoistedTools(ctx) {
|
|
28159
28404
|
return {
|
|
28160
28405
|
bash: createBashTool(ctx),
|
|
28406
|
+
bash_status: createBashStatusTool(ctx),
|
|
28407
|
+
bash_kill: createBashKillTool(ctx),
|
|
28161
28408
|
read: createReadTool(ctx),
|
|
28162
28409
|
write: createWriteTool(ctx, "edit"),
|
|
28163
28410
|
edit: createEditTool(ctx, "write"),
|
|
@@ -28529,9 +28776,12 @@ Provide exactly ONE of 'filePath' or 'url'. Pass either 'symbol' for a single lo
|
|
|
28529
28776
|
const params2 = { file: file2, symbol: sym };
|
|
28530
28777
|
if (args.contextLines !== undefined)
|
|
28531
28778
|
params2.context_lines = args.contextLines;
|
|
28532
|
-
return callBridge(ctx, context, "zoom", params2)
|
|
28779
|
+
return callBridge(ctx, context, "zoom", params2).catch((err) => ({
|
|
28780
|
+
success: false,
|
|
28781
|
+
message: err instanceof Error ? err.message : String(err)
|
|
28782
|
+
}));
|
|
28533
28783
|
}));
|
|
28534
|
-
return JSON.stringify(results);
|
|
28784
|
+
return JSON.stringify(formatZoomBatchResult(args.symbols, results), null, 2);
|
|
28535
28785
|
}
|
|
28536
28786
|
const params = { file: file2 };
|
|
28537
28787
|
if (typeof args.symbol === "string")
|
|
@@ -28547,6 +28797,40 @@ Provide exactly ONE of 'filePath' or 'url'. Pass either 'symbol' for a single lo
|
|
|
28547
28797
|
}
|
|
28548
28798
|
};
|
|
28549
28799
|
}
|
|
28800
|
+
function formatZoomBatchResult(symbols, responses) {
|
|
28801
|
+
const entries = symbols.map((name, index) => {
|
|
28802
|
+
const response = responses[index] ?? { success: false, message: "missing zoom response" };
|
|
28803
|
+
if (response.success === false) {
|
|
28804
|
+
const message = typeof response.message === "string" && response.message.length > 0 ? response.message : "zoom failed";
|
|
28805
|
+
return { name, success: false, error: message };
|
|
28806
|
+
}
|
|
28807
|
+
return { name, success: true, content: zoomResponseContent(response) };
|
|
28808
|
+
});
|
|
28809
|
+
const complete = entries.every((entry) => entry.success);
|
|
28810
|
+
const lines = [];
|
|
28811
|
+
if (!complete) {
|
|
28812
|
+
lines.push("Incomplete zoom results: one or more symbols failed.");
|
|
28813
|
+
}
|
|
28814
|
+
for (const entry of entries) {
|
|
28815
|
+
if (entry.success) {
|
|
28816
|
+
lines.push(`Symbol "${entry.name}":
|
|
28817
|
+
${entry.content ?? ""}`.trimEnd());
|
|
28818
|
+
} else {
|
|
28819
|
+
lines.push(`Symbol "${entry.name}" not found: ${entry.error ?? "zoom failed"}`);
|
|
28820
|
+
}
|
|
28821
|
+
}
|
|
28822
|
+
return { complete, symbols: entries, text: lines.join(`
|
|
28823
|
+
|
|
28824
|
+
`) };
|
|
28825
|
+
}
|
|
28826
|
+
function zoomResponseContent(response) {
|
|
28827
|
+
if (typeof response.content === "string")
|
|
28828
|
+
return response.content;
|
|
28829
|
+
if (typeof response.text === "string")
|
|
28830
|
+
return response.text;
|
|
28831
|
+
const { success: _success2, ...rest } = response;
|
|
28832
|
+
return JSON.stringify(rest, null, 2);
|
|
28833
|
+
}
|
|
28550
28834
|
var SKIP_DIRS = new Set([
|
|
28551
28835
|
"node_modules",
|
|
28552
28836
|
".git",
|
|
@@ -28622,7 +28906,7 @@ var LSP_SYMBOL_KIND_MAP = {
|
|
|
28622
28906
|
12: "function",
|
|
28623
28907
|
23: "struct"
|
|
28624
28908
|
};
|
|
28625
|
-
async function queryLspHints(client, symbolName, directory) {
|
|
28909
|
+
async function queryLspHints(client, symbolName, directory, sessionId) {
|
|
28626
28910
|
try {
|
|
28627
28911
|
const statusResult = await client.lsp.status();
|
|
28628
28912
|
const servers = statusResult.data;
|
|
@@ -28664,7 +28948,7 @@ async function queryLspHints(client, symbolName, directory) {
|
|
|
28664
28948
|
}
|
|
28665
28949
|
return { symbols: hints };
|
|
28666
28950
|
} catch (err) {
|
|
28667
|
-
|
|
28951
|
+
sessionWarn(sessionId, `LSP query failed for "${symbolName}": ${err.message}`);
|
|
28668
28952
|
return;
|
|
28669
28953
|
}
|
|
28670
28954
|
}
|
|
@@ -28753,7 +29037,7 @@ function refactoringTools(ctx) {
|
|
|
28753
29037
|
params.call_site_line = Number(args.callSiteLine);
|
|
28754
29038
|
break;
|
|
28755
29039
|
}
|
|
28756
|
-
const hints = await queryLspHints(ctx.client, args.symbol ?? args.name);
|
|
29040
|
+
const hints = await queryLspHints(ctx.client, args.symbol ?? args.name, undefined, context.sessionID);
|
|
28757
29041
|
if (hints)
|
|
28758
29042
|
params.lsp_hints = hints;
|
|
28759
29043
|
const response = await callBridge(ctx, context, commandMap[op], params);
|
|
@@ -29109,6 +29393,58 @@ function structureTools(ctx) {
|
|
|
29109
29393
|
};
|
|
29110
29394
|
}
|
|
29111
29395
|
|
|
29396
|
+
// src/workflow-hints.ts
|
|
29397
|
+
var HEADING = "## Prefer AFT tools for token efficiency";
|
|
29398
|
+
function buildWorkflowHints(opts) {
|
|
29399
|
+
const sections = [];
|
|
29400
|
+
const grepName = opts.hoistBuiltins ? "grep" : "aft_grep";
|
|
29401
|
+
const bashName = opts.hoistBuiltins ? "bash" : "aft_bash";
|
|
29402
|
+
const bashStatusName = "bash_status";
|
|
29403
|
+
const hasOutline = !opts.disabledTools.has("aft_outline");
|
|
29404
|
+
const hasZoom = !opts.disabledTools.has("aft_zoom");
|
|
29405
|
+
const hasGrep = opts.toolSurface !== "minimal" && !opts.disabledTools.has(grepName);
|
|
29406
|
+
const hasSearch = opts.toolSurface !== "minimal" && opts.semanticEnabled && !opts.disabledTools.has("aft_search");
|
|
29407
|
+
const hasNavigate = opts.toolSurface === "all" && !opts.disabledTools.has("aft_navigate");
|
|
29408
|
+
const hasBgBash = opts.bashBackgroundEnabled && !opts.disabledTools.has(bashName) && !opts.disabledTools.has(bashStatusName);
|
|
29409
|
+
if (hasOutline && hasZoom) {
|
|
29410
|
+
sections.push(`**Web/URL access**: \`aft_outline({ url })\` first for structure, then \`aft_zoom({ url, symbol: "<heading>" })\` for the specific section.`);
|
|
29411
|
+
}
|
|
29412
|
+
if (hasOutline && hasZoom && (hasGrep || hasSearch)) {
|
|
29413
|
+
const locator = hasGrep && hasSearch ? `\`${grepName}\` or \`aft_search\`` : hasGrep ? `\`${grepName}\`` : "`aft_search`";
|
|
29414
|
+
sections.push(`**Code exploration**: ${locator} to locate \u2192 \`aft_outline\` for structure \u2192 \`aft_zoom\` for symbol(s).`);
|
|
29415
|
+
}
|
|
29416
|
+
if (hasNavigate) {
|
|
29417
|
+
sections.push([
|
|
29418
|
+
"Use `aft_navigate` instead of grep + read chains for relationship questions:",
|
|
29419
|
+
"- `callers` \u2014 find all call sites before changing a function signature",
|
|
29420
|
+
"- `impact` \u2014 blast radius (which functions/files will need updates)",
|
|
29421
|
+
"- `trace_to` \u2014 how execution reaches this code from entry points (routes, exports, main)",
|
|
29422
|
+
"- `trace_data` \u2014 follow a value through assignments and parameters across files"
|
|
29423
|
+
].join(`
|
|
29424
|
+
`));
|
|
29425
|
+
}
|
|
29426
|
+
if (hasBgBash) {
|
|
29427
|
+
sections.push(`**Long-running commands** (builds, installs, full test suites): \`${bashName}({ background: true })\` returns immediately with a \`taskId\`. Check progress with \`${bashStatusName}({ taskId })\`.`);
|
|
29428
|
+
}
|
|
29429
|
+
if (sections.length === 0) {
|
|
29430
|
+
return null;
|
|
29431
|
+
}
|
|
29432
|
+
return `${HEADING}
|
|
29433
|
+
|
|
29434
|
+
${sections.join(`
|
|
29435
|
+
|
|
29436
|
+
`)}`;
|
|
29437
|
+
}
|
|
29438
|
+
function buildHintsFromConfig(config2, disabledTools) {
|
|
29439
|
+
return buildWorkflowHints({
|
|
29440
|
+
toolSurface: config2.tool_surface ?? "recommended",
|
|
29441
|
+
hoistBuiltins: config2.hoist_builtin_tools !== false,
|
|
29442
|
+
semanticEnabled: config2.semantic_search === true,
|
|
29443
|
+
bashBackgroundEnabled: config2.experimental?.bash?.background === true,
|
|
29444
|
+
disabledTools
|
|
29445
|
+
});
|
|
29446
|
+
}
|
|
29447
|
+
|
|
29112
29448
|
// src/index.ts
|
|
29113
29449
|
var STATUS_COMMAND = "aft-status";
|
|
29114
29450
|
var SENTINEL_PREFIX = "__AFT_STATUS_";
|
|
@@ -29129,13 +29465,17 @@ function coerceConfigureWarnings(warnings) {
|
|
|
29129
29465
|
}
|
|
29130
29466
|
async function sendIgnoredMessage2(client, sessionID, text) {
|
|
29131
29467
|
const typedClient = client;
|
|
29132
|
-
const
|
|
29133
|
-
|
|
29134
|
-
|
|
29135
|
-
|
|
29136
|
-
parts: [{ type: "text", text, ignored: true }]
|
|
29137
|
-
}
|
|
29468
|
+
const lastModel = await getLastUserModel(client, sessionID);
|
|
29469
|
+
const body = {
|
|
29470
|
+
noReply: true,
|
|
29471
|
+
parts: [{ type: "text", text, ignored: true }]
|
|
29138
29472
|
};
|
|
29473
|
+
if (lastModel) {
|
|
29474
|
+
body.model = { providerID: lastModel.providerID, modelID: lastModel.modelID };
|
|
29475
|
+
if (lastModel.variant)
|
|
29476
|
+
body.variant = lastModel.variant;
|
|
29477
|
+
}
|
|
29478
|
+
const promptInput = { path: { id: sessionID }, body };
|
|
29139
29479
|
if (typeof typedClient.session?.prompt === "function") {
|
|
29140
29480
|
await Promise.resolve(typedClient.session.prompt(promptInput));
|
|
29141
29481
|
return;
|
|
@@ -29171,6 +29511,8 @@ var plugin = async (input) => {
|
|
|
29171
29511
|
const configOverrides = {};
|
|
29172
29512
|
if (aftConfig.format_on_edit !== undefined)
|
|
29173
29513
|
configOverrides.format_on_edit = aftConfig.format_on_edit;
|
|
29514
|
+
if (aftConfig.formatter_timeout_secs !== undefined)
|
|
29515
|
+
configOverrides.formatter_timeout_secs = aftConfig.formatter_timeout_secs;
|
|
29174
29516
|
if (aftConfig.validate_on_edit !== undefined)
|
|
29175
29517
|
configOverrides.validate_on_edit = aftConfig.validate_on_edit;
|
|
29176
29518
|
if (aftConfig.formatter !== undefined)
|
|
@@ -29306,6 +29648,15 @@ ${lines}
|
|
|
29306
29648
|
pluginVersion: PLUGIN_VERSION,
|
|
29307
29649
|
projectRoot
|
|
29308
29650
|
}, validWarnings);
|
|
29651
|
+
},
|
|
29652
|
+
onBashCompletion: (completion, bridge) => {
|
|
29653
|
+
handlePushedBgCompletion({
|
|
29654
|
+
ctx,
|
|
29655
|
+
directory: input.directory,
|
|
29656
|
+
sessionID: completion.session_id,
|
|
29657
|
+
client: input.client,
|
|
29658
|
+
isActive: () => bridge.hasPendingRequests()
|
|
29659
|
+
}, completion);
|
|
29309
29660
|
}
|
|
29310
29661
|
}, configOverrides);
|
|
29311
29662
|
const ctx = {
|
|
@@ -29326,7 +29677,7 @@ ${lines}
|
|
|
29326
29677
|
});
|
|
29327
29678
|
rpcServer.handle("status", async (params) => {
|
|
29328
29679
|
const sessionID = params.sessionID || "rpc";
|
|
29329
|
-
const bridge = pool.
|
|
29680
|
+
const bridge = pool.getActiveBridgeForRoot(input.directory) ?? pool.getBridge(input.directory);
|
|
29330
29681
|
return await bridge.send("status", { session_id: sessionID });
|
|
29331
29682
|
});
|
|
29332
29683
|
const storageDir = configOverrides.storage_dir;
|
|
@@ -29436,8 +29787,34 @@ Install: ${getManualInstallHint()}`).catch(() => {});
|
|
|
29436
29787
|
autoUpdate: aftConfig.auto_update ?? true,
|
|
29437
29788
|
signal: autoUpdateAbort.signal
|
|
29438
29789
|
});
|
|
29790
|
+
const HINTS_TOOL_NAMES = [
|
|
29791
|
+
"aft_outline",
|
|
29792
|
+
"aft_zoom",
|
|
29793
|
+
"aft_search",
|
|
29794
|
+
"aft_navigate",
|
|
29795
|
+
"grep",
|
|
29796
|
+
"aft_grep",
|
|
29797
|
+
"bash",
|
|
29798
|
+
"aft_bash",
|
|
29799
|
+
"bash_status"
|
|
29800
|
+
];
|
|
29801
|
+
const registeredTools = new Set(Object.keys(allTools));
|
|
29802
|
+
const hintsAbsentTools = new Set;
|
|
29803
|
+
for (const name of HINTS_TOOL_NAMES) {
|
|
29804
|
+
if (!registeredTools.has(name))
|
|
29805
|
+
hintsAbsentTools.add(name);
|
|
29806
|
+
}
|
|
29807
|
+
const hintsBlock = buildHintsFromConfig(aftConfig, hintsAbsentTools);
|
|
29808
|
+
if (hintsBlock) {
|
|
29809
|
+
log(`Workflow hints injected (${hintsBlock.length} chars)`);
|
|
29810
|
+
}
|
|
29439
29811
|
return {
|
|
29440
29812
|
tool: allTools,
|
|
29813
|
+
"experimental.chat.system.transform": async (_input, output) => {
|
|
29814
|
+
if (hintsBlock) {
|
|
29815
|
+
output.system.push(hintsBlock);
|
|
29816
|
+
}
|
|
29817
|
+
},
|
|
29441
29818
|
event: async (eventInput) => {
|
|
29442
29819
|
await autoUpdateEventHook(eventInput);
|
|
29443
29820
|
if (eventInput.event.type !== "session.idle")
|
|
@@ -29459,7 +29836,7 @@ Install: ${getManualInstallHint()}`).catch(() => {});
|
|
|
29459
29836
|
if (isTuiMode2() || commandInput.command !== STATUS_COMMAND) {
|
|
29460
29837
|
return;
|
|
29461
29838
|
}
|
|
29462
|
-
const bridge = ctx.pool.
|
|
29839
|
+
const bridge = ctx.pool.getActiveBridgeForRoot(input.directory) ?? ctx.pool.getBridge(input.directory);
|
|
29463
29840
|
const response = await bridge.send("status", { session_id: commandInput.sessionID });
|
|
29464
29841
|
if (response.success === false) {
|
|
29465
29842
|
throw new Error(response.message || "status failed");
|