oh-my-opencode-slim 0.9.9 → 0.9.10
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/README.md +2 -0
- package/dist/hooks/apply-patch/codec.d.ts +7 -0
- package/dist/hooks/apply-patch/errors.d.ts +25 -0
- package/dist/hooks/apply-patch/execution-context.d.ts +26 -0
- package/dist/hooks/apply-patch/index.d.ts +15 -0
- package/dist/hooks/apply-patch/matching.d.ts +18 -0
- package/dist/hooks/apply-patch/operations.d.ts +3 -0
- package/dist/hooks/apply-patch/patch.d.ts +2 -0
- package/dist/hooks/apply-patch/prepared-changes.d.ts +17 -0
- package/dist/hooks/apply-patch/resolution.d.ts +19 -0
- package/dist/hooks/apply-patch/rewrite.d.ts +10 -0
- package/dist/hooks/apply-patch/test-helpers.d.ts +6 -0
- package/dist/hooks/apply-patch/types.d.ts +80 -0
- package/dist/hooks/index.d.ts +1 -0
- package/dist/hooks/post-file-tool-nudge/index.d.ts +24 -7
- package/dist/hooks/todo-continuation/index.d.ts +4 -0
- package/dist/index.js +1613 -120
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -2956,34 +2956,1439 @@ ${bestResult.result}` : undefined,
|
|
|
2956
2956
|
};
|
|
2957
2957
|
}
|
|
2958
2958
|
}
|
|
2959
|
+
// src/hooks/apply-patch/errors.ts
|
|
2960
|
+
var APPLY_PATCH_ERROR_PREFIX = {
|
|
2961
|
+
blocked: "apply_patch blocked",
|
|
2962
|
+
validation: "apply_patch validation failed",
|
|
2963
|
+
verification: "apply_patch verification failed",
|
|
2964
|
+
internal: "apply_patch internal error"
|
|
2965
|
+
};
|
|
2966
|
+
|
|
2967
|
+
class ApplyPatchError extends Error {
|
|
2968
|
+
kind;
|
|
2969
|
+
code;
|
|
2970
|
+
cause;
|
|
2971
|
+
constructor(kind, code, message, options) {
|
|
2972
|
+
super(`${APPLY_PATCH_ERROR_PREFIX[kind]}: ${message}`);
|
|
2973
|
+
this.kind = kind;
|
|
2974
|
+
this.code = code;
|
|
2975
|
+
this.name = "ApplyPatchError";
|
|
2976
|
+
this.cause = options?.cause;
|
|
2977
|
+
}
|
|
2978
|
+
}
|
|
2979
|
+
function getErrorMessage(error) {
|
|
2980
|
+
return error instanceof Error ? error.message : String(error);
|
|
2981
|
+
}
|
|
2982
|
+
function createApplyPatchBlockedError(message, cause) {
|
|
2983
|
+
return new ApplyPatchError("blocked", "outside_workspace", message, {
|
|
2984
|
+
cause
|
|
2985
|
+
});
|
|
2986
|
+
}
|
|
2987
|
+
function createApplyPatchValidationError(message, cause) {
|
|
2988
|
+
return new ApplyPatchError("validation", "malformed_patch", message, {
|
|
2989
|
+
cause
|
|
2990
|
+
});
|
|
2991
|
+
}
|
|
2992
|
+
function createApplyPatchVerificationError(message, cause) {
|
|
2993
|
+
return new ApplyPatchError("verification", "verification_failed", message, {
|
|
2994
|
+
cause
|
|
2995
|
+
});
|
|
2996
|
+
}
|
|
2997
|
+
function createApplyPatchInternalError(message, cause) {
|
|
2998
|
+
return new ApplyPatchError("internal", "internal_unexpected", message, {
|
|
2999
|
+
cause
|
|
3000
|
+
});
|
|
3001
|
+
}
|
|
3002
|
+
function isApplyPatchError(error) {
|
|
3003
|
+
return error instanceof ApplyPatchError;
|
|
3004
|
+
}
|
|
3005
|
+
function isApplyPatchVerificationError(error) {
|
|
3006
|
+
return isApplyPatchError(error) && error.kind === "verification";
|
|
3007
|
+
}
|
|
3008
|
+
function getApplyPatchErrorDetails(error) {
|
|
3009
|
+
if (!isApplyPatchError(error)) {
|
|
3010
|
+
return;
|
|
3011
|
+
}
|
|
3012
|
+
return {
|
|
3013
|
+
kind: error.kind,
|
|
3014
|
+
code: error.code,
|
|
3015
|
+
message: error.message
|
|
3016
|
+
};
|
|
3017
|
+
}
|
|
3018
|
+
function ensureApplyPatchError(error, context) {
|
|
3019
|
+
if (isApplyPatchError(error)) {
|
|
3020
|
+
return error;
|
|
3021
|
+
}
|
|
3022
|
+
return createApplyPatchInternalError(`${context}: ${getErrorMessage(error)}`, error);
|
|
3023
|
+
}
|
|
3024
|
+
|
|
3025
|
+
// src/hooks/apply-patch/execution-context.ts
|
|
3026
|
+
import * as fs3 from "fs/promises";
|
|
3027
|
+
import path3 from "path";
|
|
3028
|
+
|
|
3029
|
+
// src/hooks/apply-patch/codec.ts
|
|
3030
|
+
function normalizeLineEndings(text) {
|
|
3031
|
+
return text.replace(/\r\n/g, `
|
|
3032
|
+
`).replace(/\r/g, `
|
|
3033
|
+
`);
|
|
3034
|
+
}
|
|
3035
|
+
function normalizeUnicode(text) {
|
|
3036
|
+
return text.replace(/[\u2018\u2019\u201A\u201B]/g, "'").replace(/[\u201C\u201D\u201E\u201F]/g, '"').replace(/[\u2010\u2011\u2012\u2013\u2014\u2015]/g, "-").replace(/\u2026/g, "...").replace(/\u00A0/g, " ");
|
|
3037
|
+
}
|
|
3038
|
+
function stripHeredoc(input) {
|
|
3039
|
+
const normalized = normalizeLineEndings(input);
|
|
3040
|
+
const match = normalized.match(/^(?:cat\s+)?<<['"]?(\w+)['"]?\s*\n([\s\S]*?)\n\1\s*$/);
|
|
3041
|
+
return match ? match[2] : normalized;
|
|
3042
|
+
}
|
|
3043
|
+
function normalizePatchText(patchText) {
|
|
3044
|
+
return stripHeredoc(normalizeLineEndings(patchText).trim());
|
|
3045
|
+
}
|
|
3046
|
+
function parseHeader(lines, index) {
|
|
3047
|
+
const line = lines[index];
|
|
3048
|
+
if (line.startsWith("*** Add File:")) {
|
|
3049
|
+
const file = line.slice("*** Add File:".length).trim();
|
|
3050
|
+
return file ? { file, next: index + 1 } : null;
|
|
3051
|
+
}
|
|
3052
|
+
if (line.startsWith("*** Delete File:")) {
|
|
3053
|
+
const file = line.slice("*** Delete File:".length).trim();
|
|
3054
|
+
return file ? { file, next: index + 1 } : null;
|
|
3055
|
+
}
|
|
3056
|
+
if (line.startsWith("*** Update File:")) {
|
|
3057
|
+
const file = line.slice("*** Update File:".length).trim();
|
|
3058
|
+
let move;
|
|
3059
|
+
let next = index + 1;
|
|
3060
|
+
if (next < lines.length && lines[next].startsWith("*** Move to:")) {
|
|
3061
|
+
const moveTarget = lines[next].slice("*** Move to:".length).trim();
|
|
3062
|
+
if (!moveTarget) {
|
|
3063
|
+
return null;
|
|
3064
|
+
}
|
|
3065
|
+
move = moveTarget;
|
|
3066
|
+
next += 1;
|
|
3067
|
+
}
|
|
3068
|
+
return file ? { file, move, next } : null;
|
|
3069
|
+
}
|
|
3070
|
+
return null;
|
|
3071
|
+
}
|
|
3072
|
+
function unexpectedPatchLine(context, line) {
|
|
3073
|
+
const rendered = line.length === 0 ? "<empty>" : line;
|
|
3074
|
+
throw new Error(`Invalid patch format: unexpected line ${context}: ${rendered}`);
|
|
3075
|
+
}
|
|
3076
|
+
function parseChunks(lines, index, mode) {
|
|
3077
|
+
const chunks = [];
|
|
3078
|
+
let at = index;
|
|
3079
|
+
while (at < lines.length && !lines[at].startsWith("***")) {
|
|
3080
|
+
if (!lines[at].startsWith("@@")) {
|
|
3081
|
+
if (mode === "strict") {
|
|
3082
|
+
unexpectedPatchLine("in update body", lines[at]);
|
|
3083
|
+
}
|
|
3084
|
+
at += 1;
|
|
3085
|
+
continue;
|
|
3086
|
+
}
|
|
3087
|
+
const context = lines[at].slice(2).trim() || undefined;
|
|
3088
|
+
at += 1;
|
|
3089
|
+
const old_lines = [];
|
|
3090
|
+
const new_lines = [];
|
|
3091
|
+
let eof = false;
|
|
3092
|
+
while (at < lines.length && !lines[at].startsWith("@@") && (!lines[at].startsWith("***") || lines[at] === "*** End of File")) {
|
|
3093
|
+
const line = lines[at];
|
|
3094
|
+
if (line === "*** End of File") {
|
|
3095
|
+
eof = true;
|
|
3096
|
+
at += 1;
|
|
3097
|
+
break;
|
|
3098
|
+
}
|
|
3099
|
+
if (line.startsWith(" ")) {
|
|
3100
|
+
old_lines.push(line.slice(1));
|
|
3101
|
+
new_lines.push(line.slice(1));
|
|
3102
|
+
at += 1;
|
|
3103
|
+
continue;
|
|
3104
|
+
}
|
|
3105
|
+
if (line.startsWith("-")) {
|
|
3106
|
+
old_lines.push(line.slice(1));
|
|
3107
|
+
at += 1;
|
|
3108
|
+
continue;
|
|
3109
|
+
}
|
|
3110
|
+
if (line.startsWith("+")) {
|
|
3111
|
+
new_lines.push(line.slice(1));
|
|
3112
|
+
at += 1;
|
|
3113
|
+
continue;
|
|
3114
|
+
}
|
|
3115
|
+
if (mode === "strict") {
|
|
3116
|
+
unexpectedPatchLine("in patch chunk", line);
|
|
3117
|
+
}
|
|
3118
|
+
at += 1;
|
|
3119
|
+
}
|
|
3120
|
+
chunks.push({
|
|
3121
|
+
old_lines,
|
|
3122
|
+
new_lines,
|
|
3123
|
+
change_context: context,
|
|
3124
|
+
is_end_of_file: eof || undefined
|
|
3125
|
+
});
|
|
3126
|
+
}
|
|
3127
|
+
return { chunks, next: at };
|
|
3128
|
+
}
|
|
3129
|
+
function parseAdd(lines, index, mode) {
|
|
3130
|
+
let contents = "";
|
|
3131
|
+
let at = index;
|
|
3132
|
+
while (at < lines.length && !lines[at].startsWith("***")) {
|
|
3133
|
+
if (lines[at].startsWith("+")) {
|
|
3134
|
+
contents += `${lines[at].slice(1)}
|
|
3135
|
+
`;
|
|
3136
|
+
at += 1;
|
|
3137
|
+
continue;
|
|
3138
|
+
}
|
|
3139
|
+
if (mode === "strict") {
|
|
3140
|
+
unexpectedPatchLine("in Add File body", lines[at]);
|
|
3141
|
+
}
|
|
3142
|
+
at += 1;
|
|
3143
|
+
}
|
|
3144
|
+
if (contents.endsWith(`
|
|
3145
|
+
`)) {
|
|
3146
|
+
contents = contents.slice(0, -1);
|
|
3147
|
+
}
|
|
3148
|
+
return { content: contents, next: at };
|
|
3149
|
+
}
|
|
3150
|
+
function parsePatchInternal(patchText, mode) {
|
|
3151
|
+
const clean = normalizePatchText(patchText);
|
|
3152
|
+
const lines = clean.split(`
|
|
3153
|
+
`);
|
|
3154
|
+
const begin = lines.findIndex((line) => line.trim() === "*** Begin Patch");
|
|
3155
|
+
const end = lines.findIndex((line) => line.trim() === "*** End Patch");
|
|
3156
|
+
if (begin === -1 || end === -1 || begin >= end) {
|
|
3157
|
+
throw new Error("Invalid patch format: missing Begin/End markers");
|
|
3158
|
+
}
|
|
3159
|
+
if (mode === "strict") {
|
|
3160
|
+
for (const line of lines.slice(0, begin)) {
|
|
3161
|
+
unexpectedPatchLine("before Begin Patch", line);
|
|
3162
|
+
}
|
|
3163
|
+
for (const line of lines.slice(end + 1)) {
|
|
3164
|
+
unexpectedPatchLine("after End Patch", line);
|
|
3165
|
+
}
|
|
3166
|
+
}
|
|
3167
|
+
const hunks = [];
|
|
3168
|
+
let index = begin + 1;
|
|
3169
|
+
while (index < end) {
|
|
3170
|
+
const header = parseHeader(lines, index);
|
|
3171
|
+
if (!header) {
|
|
3172
|
+
if (mode === "strict") {
|
|
3173
|
+
unexpectedPatchLine("between hunks", lines[index]);
|
|
3174
|
+
}
|
|
3175
|
+
index += 1;
|
|
3176
|
+
continue;
|
|
3177
|
+
}
|
|
3178
|
+
if (lines[index].startsWith("*** Add File:")) {
|
|
3179
|
+
const next2 = parseAdd(lines, header.next, mode);
|
|
3180
|
+
hunks.push({
|
|
3181
|
+
type: "add",
|
|
3182
|
+
path: header.file,
|
|
3183
|
+
contents: next2.content
|
|
3184
|
+
});
|
|
3185
|
+
index = next2.next;
|
|
3186
|
+
continue;
|
|
3187
|
+
}
|
|
3188
|
+
if (lines[index].startsWith("*** Delete File:")) {
|
|
3189
|
+
hunks.push({ type: "delete", path: header.file });
|
|
3190
|
+
index = header.next;
|
|
3191
|
+
continue;
|
|
3192
|
+
}
|
|
3193
|
+
const next = parseChunks(lines, header.next, mode);
|
|
3194
|
+
if (mode === "strict" && next.chunks.length === 0) {
|
|
3195
|
+
throw new Error(`Invalid patch format: Update File is missing @@ chunk body: ${header.file}`);
|
|
3196
|
+
}
|
|
3197
|
+
hunks.push({
|
|
3198
|
+
type: "update",
|
|
3199
|
+
path: header.file,
|
|
3200
|
+
move_path: header.move,
|
|
3201
|
+
chunks: next.chunks
|
|
3202
|
+
});
|
|
3203
|
+
index = next.next;
|
|
3204
|
+
}
|
|
3205
|
+
return { hunks };
|
|
3206
|
+
}
|
|
3207
|
+
function parsePatchStrict(patchText) {
|
|
3208
|
+
return parsePatchInternal(patchText, "strict");
|
|
3209
|
+
}
|
|
3210
|
+
function diffMatrix(old_lines, new_lines) {
|
|
3211
|
+
const dp = Array.from({ length: old_lines.length + 1 }, () => Array(new_lines.length + 1).fill(0));
|
|
3212
|
+
for (let oldIndex = 1;oldIndex <= old_lines.length; oldIndex += 1) {
|
|
3213
|
+
for (let newIndex = 1;newIndex <= new_lines.length; newIndex += 1) {
|
|
3214
|
+
dp[oldIndex][newIndex] = old_lines[oldIndex - 1] === new_lines[newIndex - 1] ? dp[oldIndex - 1][newIndex - 1] + 1 : Math.max(dp[oldIndex - 1][newIndex], dp[oldIndex][newIndex - 1]);
|
|
3215
|
+
}
|
|
3216
|
+
}
|
|
3217
|
+
return dp;
|
|
3218
|
+
}
|
|
3219
|
+
function renderChunk(chunk) {
|
|
3220
|
+
const lines = [chunk.change_context ? `@@ ${chunk.change_context}` : "@@"];
|
|
3221
|
+
const dp = diffMatrix(chunk.old_lines, chunk.new_lines);
|
|
3222
|
+
const body = [];
|
|
3223
|
+
let oldIndex = chunk.old_lines.length;
|
|
3224
|
+
let newIndex = chunk.new_lines.length;
|
|
3225
|
+
while (oldIndex > 0 && newIndex > 0) {
|
|
3226
|
+
if (chunk.old_lines[oldIndex - 1] === chunk.new_lines[newIndex - 1]) {
|
|
3227
|
+
body.push(` ${chunk.old_lines[oldIndex - 1]}`);
|
|
3228
|
+
oldIndex -= 1;
|
|
3229
|
+
newIndex -= 1;
|
|
3230
|
+
continue;
|
|
3231
|
+
}
|
|
3232
|
+
if (dp[oldIndex - 1][newIndex] >= dp[oldIndex][newIndex - 1]) {
|
|
3233
|
+
body.push(`-${chunk.old_lines[oldIndex - 1]}`);
|
|
3234
|
+
oldIndex -= 1;
|
|
3235
|
+
continue;
|
|
3236
|
+
}
|
|
3237
|
+
body.push(`+${chunk.new_lines[newIndex - 1]}`);
|
|
3238
|
+
newIndex -= 1;
|
|
3239
|
+
}
|
|
3240
|
+
while (oldIndex > 0) {
|
|
3241
|
+
body.push(`-${chunk.old_lines[oldIndex - 1]}`);
|
|
3242
|
+
oldIndex -= 1;
|
|
3243
|
+
}
|
|
3244
|
+
while (newIndex > 0) {
|
|
3245
|
+
body.push(`+${chunk.new_lines[newIndex - 1]}`);
|
|
3246
|
+
newIndex -= 1;
|
|
3247
|
+
}
|
|
3248
|
+
lines.push(...body.reverse());
|
|
3249
|
+
if (chunk.is_end_of_file) {
|
|
3250
|
+
lines.push("*** End of File");
|
|
3251
|
+
}
|
|
3252
|
+
return lines;
|
|
3253
|
+
}
|
|
3254
|
+
function renderAddContents(contents) {
|
|
3255
|
+
if (contents.length === 0) {
|
|
3256
|
+
return [];
|
|
3257
|
+
}
|
|
3258
|
+
return contents.split(`
|
|
3259
|
+
`).map((line) => `+${line}`);
|
|
3260
|
+
}
|
|
3261
|
+
function formatPatch(patch) {
|
|
3262
|
+
const lines = ["*** Begin Patch"];
|
|
3263
|
+
for (const hunk of patch.hunks) {
|
|
3264
|
+
if (hunk.type === "add") {
|
|
3265
|
+
lines.push(`*** Add File: ${hunk.path}`);
|
|
3266
|
+
lines.push(...renderAddContents(hunk.contents));
|
|
3267
|
+
continue;
|
|
3268
|
+
}
|
|
3269
|
+
if (hunk.type === "delete") {
|
|
3270
|
+
lines.push(`*** Delete File: ${hunk.path}`);
|
|
3271
|
+
continue;
|
|
3272
|
+
}
|
|
3273
|
+
lines.push(`*** Update File: ${hunk.path}`);
|
|
3274
|
+
if (hunk.move_path) {
|
|
3275
|
+
lines.push(`*** Move to: ${hunk.move_path}`);
|
|
3276
|
+
}
|
|
3277
|
+
for (const chunk of hunk.chunks) {
|
|
3278
|
+
lines.push(...renderChunk(chunk));
|
|
3279
|
+
}
|
|
3280
|
+
}
|
|
3281
|
+
lines.push("*** End Patch");
|
|
3282
|
+
return lines.join(`
|
|
3283
|
+
`);
|
|
3284
|
+
}
|
|
3285
|
+
|
|
3286
|
+
// src/hooks/apply-patch/matching.ts
|
|
3287
|
+
var AUTO_RESCUE_COMPARATOR_NAMES = new Set([
|
|
3288
|
+
"exact",
|
|
3289
|
+
"unicode",
|
|
3290
|
+
"trim-end",
|
|
3291
|
+
"unicode-trim-end"
|
|
3292
|
+
]);
|
|
3293
|
+
function equalExact(a, b) {
|
|
3294
|
+
return a === b;
|
|
3295
|
+
}
|
|
3296
|
+
function equalUnicodeExact(a, b) {
|
|
3297
|
+
return normalizeUnicode(a) === normalizeUnicode(b);
|
|
3298
|
+
}
|
|
3299
|
+
function equalTrimEnd(a, b) {
|
|
3300
|
+
return a.trimEnd() === b.trimEnd();
|
|
3301
|
+
}
|
|
3302
|
+
function equalUnicodeTrimEnd(a, b) {
|
|
3303
|
+
return normalizeUnicode(a.trimEnd()) === normalizeUnicode(b.trimEnd());
|
|
3304
|
+
}
|
|
3305
|
+
function equalTrim(a, b) {
|
|
3306
|
+
return a.trim() === b.trim();
|
|
3307
|
+
}
|
|
3308
|
+
function equalUnicodeTrim(a, b) {
|
|
3309
|
+
return normalizeUnicode(a.trim()) === normalizeUnicode(b.trim());
|
|
3310
|
+
}
|
|
3311
|
+
var comparatorEntries = [
|
|
3312
|
+
{ name: "exact", exact: true, same: equalExact },
|
|
3313
|
+
{ name: "unicode", exact: false, same: equalUnicodeExact },
|
|
3314
|
+
{ name: "trim-end", exact: false, same: equalTrimEnd },
|
|
3315
|
+
{
|
|
3316
|
+
name: "unicode-trim-end",
|
|
3317
|
+
exact: false,
|
|
3318
|
+
same: equalUnicodeTrimEnd
|
|
3319
|
+
},
|
|
3320
|
+
{ name: "trim", exact: false, same: equalTrim },
|
|
3321
|
+
{ name: "unicode-trim", exact: false, same: equalUnicodeTrim }
|
|
3322
|
+
];
|
|
3323
|
+
var autoRescueComparatorEntries = comparatorEntries.filter((entry) => AUTO_RESCUE_COMPARATOR_NAMES.has(entry.name));
|
|
3324
|
+
var MAX_LCS_CHUNK_LINES = 48;
|
|
3325
|
+
var MAX_LCS_CANDIDATES = 64;
|
|
3326
|
+
var autoRescueComparators = autoRescueComparatorEntries.map((entry) => entry.same);
|
|
3327
|
+
var permissiveComparators = comparatorEntries.map((entry) => entry.same);
|
|
3328
|
+
function tryMatch(lines, pattern, start, comparator, eof) {
|
|
3329
|
+
if (eof) {
|
|
3330
|
+
const at = lines.length - pattern.length;
|
|
3331
|
+
if (at >= start) {
|
|
3332
|
+
let ok = true;
|
|
3333
|
+
for (let index = 0;index < pattern.length; index += 1) {
|
|
3334
|
+
if (!comparator.same(lines[at + index], pattern[index])) {
|
|
3335
|
+
ok = false;
|
|
3336
|
+
break;
|
|
3337
|
+
}
|
|
3338
|
+
}
|
|
3339
|
+
if (ok) {
|
|
3340
|
+
return {
|
|
3341
|
+
index: at,
|
|
3342
|
+
comparator: comparator.name,
|
|
3343
|
+
exact: comparator.exact
|
|
3344
|
+
};
|
|
3345
|
+
}
|
|
3346
|
+
}
|
|
3347
|
+
}
|
|
3348
|
+
for (let index = start;index <= lines.length - pattern.length; index += 1) {
|
|
3349
|
+
let ok = true;
|
|
3350
|
+
for (let inner = 0;inner < pattern.length; inner += 1) {
|
|
3351
|
+
if (!comparator.same(lines[index + inner], pattern[inner])) {
|
|
3352
|
+
ok = false;
|
|
3353
|
+
break;
|
|
3354
|
+
}
|
|
3355
|
+
}
|
|
3356
|
+
if (ok) {
|
|
3357
|
+
return {
|
|
3358
|
+
index,
|
|
3359
|
+
comparator: comparator.name,
|
|
3360
|
+
exact: comparator.exact
|
|
3361
|
+
};
|
|
3362
|
+
}
|
|
3363
|
+
}
|
|
3364
|
+
return;
|
|
3365
|
+
}
|
|
3366
|
+
function seekMatch(lines, pattern, start, eof = false) {
|
|
3367
|
+
if (pattern.length === 0) {
|
|
3368
|
+
return;
|
|
3369
|
+
}
|
|
3370
|
+
for (const comparator of autoRescueComparatorEntries) {
|
|
3371
|
+
const hit = tryMatch(lines, pattern, start, comparator, eof);
|
|
3372
|
+
if (hit) {
|
|
3373
|
+
return hit;
|
|
3374
|
+
}
|
|
3375
|
+
}
|
|
3376
|
+
return;
|
|
3377
|
+
}
|
|
3378
|
+
function seek(lines, pattern, start, eof = false) {
|
|
3379
|
+
return seekMatch(lines, pattern, start, eof)?.index ?? -1;
|
|
3380
|
+
}
|
|
3381
|
+
function list(lines, pattern, start, same) {
|
|
3382
|
+
if (pattern.length === 0) {
|
|
3383
|
+
return [];
|
|
3384
|
+
}
|
|
3385
|
+
const out = [];
|
|
3386
|
+
for (let index = start;index <= lines.length - pattern.length; index += 1) {
|
|
3387
|
+
let ok = true;
|
|
3388
|
+
for (let inner = 0;inner < pattern.length; inner += 1) {
|
|
3389
|
+
if (!same(lines[index + inner], pattern[inner])) {
|
|
3390
|
+
ok = false;
|
|
3391
|
+
break;
|
|
3392
|
+
}
|
|
3393
|
+
}
|
|
3394
|
+
if (ok) {
|
|
3395
|
+
out.push(index);
|
|
3396
|
+
}
|
|
3397
|
+
}
|
|
3398
|
+
return out;
|
|
3399
|
+
}
|
|
3400
|
+
function sameRescueLine(a, b) {
|
|
3401
|
+
return equalExact(a, b) || equalUnicodeExact(a, b);
|
|
3402
|
+
}
|
|
3403
|
+
function prefix(old_lines, new_lines) {
|
|
3404
|
+
let index = 0;
|
|
3405
|
+
while (index < old_lines.length && index < new_lines.length && sameRescueLine(old_lines[index], new_lines[index])) {
|
|
3406
|
+
index += 1;
|
|
3407
|
+
}
|
|
3408
|
+
return index;
|
|
3409
|
+
}
|
|
3410
|
+
function suffix(old_lines, new_lines, prefixLength) {
|
|
3411
|
+
let index = 0;
|
|
3412
|
+
while (old_lines.length - index - 1 >= prefixLength && new_lines.length - index - 1 >= prefixLength && sameRescueLine(old_lines[old_lines.length - index - 1], new_lines[new_lines.length - index - 1])) {
|
|
3413
|
+
index += 1;
|
|
3414
|
+
}
|
|
3415
|
+
return index;
|
|
3416
|
+
}
|
|
3417
|
+
function rescueByPrefixSuffix(lines, old_lines, new_lines, start) {
|
|
3418
|
+
const prefixLength = prefix(old_lines, new_lines);
|
|
3419
|
+
const suffixLength = suffix(old_lines, new_lines, prefixLength);
|
|
3420
|
+
if (prefixLength === 0 || suffixLength === 0) {
|
|
3421
|
+
return { kind: "miss" };
|
|
3422
|
+
}
|
|
3423
|
+
const left = old_lines.slice(0, prefixLength);
|
|
3424
|
+
const right = old_lines.slice(old_lines.length - suffixLength);
|
|
3425
|
+
const middle = new_lines.slice(prefixLength, new_lines.length - suffixLength);
|
|
3426
|
+
const hits = new Map;
|
|
3427
|
+
for (const same of autoRescueComparators) {
|
|
3428
|
+
for (const leftIndex of list(lines, left, start, same)) {
|
|
3429
|
+
const from = leftIndex + left.length;
|
|
3430
|
+
for (const rightIndex of list(lines, right, from, same)) {
|
|
3431
|
+
const key = `${from}:${rightIndex}`;
|
|
3432
|
+
hits.set(key, {
|
|
3433
|
+
start: from,
|
|
3434
|
+
del: rightIndex - from,
|
|
3435
|
+
add: [...middle]
|
|
3436
|
+
});
|
|
3437
|
+
}
|
|
3438
|
+
}
|
|
3439
|
+
}
|
|
3440
|
+
if (hits.size === 0) {
|
|
3441
|
+
return { kind: "miss" };
|
|
3442
|
+
}
|
|
3443
|
+
if (hits.size > 1) {
|
|
3444
|
+
return { kind: "ambiguous", phase: "prefix_suffix" };
|
|
3445
|
+
}
|
|
3446
|
+
return { kind: "match", hit: [...hits.values()][0] };
|
|
3447
|
+
}
|
|
3448
|
+
function score(a, b) {
|
|
3449
|
+
const dp = Array.from({ length: a.length + 1 }, () => Array(b.length + 1).fill(0));
|
|
3450
|
+
for (let i = 1;i <= a.length; i += 1) {
|
|
3451
|
+
for (let j = 1;j <= b.length; j += 1) {
|
|
3452
|
+
dp[i][j] = normalizeUnicode(a[i - 1].trim()) === normalizeUnicode(b[j - 1].trim()) ? dp[i - 1][j - 1] + 1 : Math.max(dp[i - 1][j], dp[i][j - 1]);
|
|
3453
|
+
}
|
|
3454
|
+
}
|
|
3455
|
+
return dp[a.length][b.length];
|
|
3456
|
+
}
|
|
3457
|
+
function normalizeLcsLine(line) {
|
|
3458
|
+
return normalizeUnicode(line).trim();
|
|
3459
|
+
}
|
|
3460
|
+
function countLcsUpperBound(a, b) {
|
|
3461
|
+
const counts = new Map;
|
|
3462
|
+
for (const line of a) {
|
|
3463
|
+
const key = normalizeLcsLine(line);
|
|
3464
|
+
counts.set(key, (counts.get(key) ?? 0) + 1);
|
|
3465
|
+
}
|
|
3466
|
+
let shared = 0;
|
|
3467
|
+
for (const line of b) {
|
|
3468
|
+
const key = normalizeLcsLine(line);
|
|
3469
|
+
const available = counts.get(key) ?? 0;
|
|
3470
|
+
if (available === 0) {
|
|
3471
|
+
continue;
|
|
3472
|
+
}
|
|
3473
|
+
shared += 1;
|
|
3474
|
+
if (available === 1) {
|
|
3475
|
+
counts.delete(key);
|
|
3476
|
+
continue;
|
|
3477
|
+
}
|
|
3478
|
+
counts.set(key, available - 1);
|
|
3479
|
+
}
|
|
3480
|
+
return shared;
|
|
3481
|
+
}
|
|
3482
|
+
function hasStableBorders(oldLines, candidate) {
|
|
3483
|
+
if (oldLines.length === 0 || candidate.length !== oldLines.length) {
|
|
3484
|
+
return false;
|
|
3485
|
+
}
|
|
3486
|
+
const same = autoRescueComparators.some((compare) => compare(oldLines[0], candidate[0]));
|
|
3487
|
+
if (!same) {
|
|
3488
|
+
return false;
|
|
3489
|
+
}
|
|
3490
|
+
if (oldLines.length === 1) {
|
|
3491
|
+
return true;
|
|
3492
|
+
}
|
|
3493
|
+
return autoRescueComparators.some((compare) => compare(oldLines[oldLines.length - 1], candidate[candidate.length - 1]));
|
|
3494
|
+
}
|
|
3495
|
+
function collectBorderAnchoredStarts(lines, oldLines, start) {
|
|
3496
|
+
if (oldLines.length === 0) {
|
|
3497
|
+
return [];
|
|
3498
|
+
}
|
|
3499
|
+
const firstHits = new Set;
|
|
3500
|
+
const lastHits = new Set;
|
|
3501
|
+
const lastLine = oldLines[oldLines.length - 1];
|
|
3502
|
+
for (const same of autoRescueComparators) {
|
|
3503
|
+
for (const index of list(lines, [oldLines[0]], start, same)) {
|
|
3504
|
+
firstHits.add(index);
|
|
3505
|
+
}
|
|
3506
|
+
for (const index of list(lines, [lastLine], start, same)) {
|
|
3507
|
+
lastHits.add(index);
|
|
3508
|
+
}
|
|
3509
|
+
}
|
|
3510
|
+
const candidates = [];
|
|
3511
|
+
for (const index of [...firstHits].sort((a, b) => a - b)) {
|
|
3512
|
+
const end = index + oldLines.length - 1;
|
|
3513
|
+
if (end >= lines.length || !lastHits.has(end)) {
|
|
3514
|
+
continue;
|
|
3515
|
+
}
|
|
3516
|
+
const candidate = lines.slice(index, index + oldLines.length);
|
|
3517
|
+
if (!hasStableBorders(oldLines, candidate)) {
|
|
3518
|
+
continue;
|
|
3519
|
+
}
|
|
3520
|
+
candidates.push(index);
|
|
3521
|
+
}
|
|
3522
|
+
return candidates;
|
|
3523
|
+
}
|
|
3524
|
+
function rescueByLcs(lines, old_lines, new_lines, start) {
|
|
3525
|
+
if (old_lines.length === 0 || lines.length === 0) {
|
|
3526
|
+
return { kind: "miss" };
|
|
3527
|
+
}
|
|
3528
|
+
const from = start;
|
|
3529
|
+
const to = lines.length - old_lines.length;
|
|
3530
|
+
if (to < from) {
|
|
3531
|
+
return { kind: "miss" };
|
|
3532
|
+
}
|
|
3533
|
+
if (old_lines.length > MAX_LCS_CHUNK_LINES) {
|
|
3534
|
+
return { kind: "miss" };
|
|
3535
|
+
}
|
|
3536
|
+
const needed = old_lines.length <= 2 ? old_lines.length : Math.max(2, Math.ceil(old_lines.length * 0.7));
|
|
3537
|
+
const candidates = collectBorderAnchoredStarts(lines, old_lines, start);
|
|
3538
|
+
if (candidates.length === 0 || candidates.length > MAX_LCS_CANDIDATES) {
|
|
3539
|
+
return { kind: "miss" };
|
|
3540
|
+
}
|
|
3541
|
+
let best;
|
|
3542
|
+
let bestScore = 0;
|
|
3543
|
+
let ties = 0;
|
|
3544
|
+
for (const index of candidates) {
|
|
3545
|
+
if (index < from || index > to) {
|
|
3546
|
+
continue;
|
|
3547
|
+
}
|
|
3548
|
+
const window = lines.slice(index, index + old_lines.length);
|
|
3549
|
+
if (countLcsUpperBound(old_lines, window) < needed) {
|
|
3550
|
+
continue;
|
|
3551
|
+
}
|
|
3552
|
+
const current = score(old_lines, window);
|
|
3553
|
+
if (current > bestScore) {
|
|
3554
|
+
bestScore = current;
|
|
3555
|
+
ties = 1;
|
|
3556
|
+
best = {
|
|
3557
|
+
start: index,
|
|
3558
|
+
del: old_lines.length,
|
|
3559
|
+
add: [...new_lines]
|
|
3560
|
+
};
|
|
3561
|
+
continue;
|
|
3562
|
+
}
|
|
3563
|
+
if (current === bestScore && current > 0) {
|
|
3564
|
+
ties += 1;
|
|
3565
|
+
}
|
|
3566
|
+
}
|
|
3567
|
+
if (!best || bestScore < needed) {
|
|
3568
|
+
return { kind: "miss" };
|
|
3569
|
+
}
|
|
3570
|
+
if (ties > 1) {
|
|
3571
|
+
return { kind: "ambiguous", phase: "lcs" };
|
|
3572
|
+
}
|
|
3573
|
+
return { kind: "match", hit: best };
|
|
3574
|
+
}
|
|
3575
|
+
|
|
3576
|
+
// src/hooks/apply-patch/resolution.ts
|
|
3577
|
+
function splitFileLines(text) {
|
|
3578
|
+
const eol = text.match(/\r\n|\n|\r/)?.[0] === `\r
|
|
3579
|
+
` ? `\r
|
|
3580
|
+
` : `
|
|
3581
|
+
`;
|
|
3582
|
+
const normalized = text.replace(/\r\n/g, `
|
|
3583
|
+
`).replace(/\r/g, `
|
|
3584
|
+
`);
|
|
3585
|
+
const hasFinalNewline = normalized.endsWith(`
|
|
3586
|
+
`);
|
|
3587
|
+
const lines = normalized.split(`
|
|
3588
|
+
`);
|
|
3589
|
+
if (hasFinalNewline) {
|
|
3590
|
+
lines.pop();
|
|
3591
|
+
}
|
|
3592
|
+
return { lines, eol, hasFinalNewline };
|
|
3593
|
+
}
|
|
3594
|
+
function resolveChunkStart(lines, chunk, start) {
|
|
3595
|
+
if (!chunk.change_context) {
|
|
3596
|
+
return start;
|
|
3597
|
+
}
|
|
3598
|
+
const at = seek(lines, [chunk.change_context], start);
|
|
3599
|
+
return at === -1 ? start : at + 1;
|
|
3600
|
+
}
|
|
3601
|
+
function resolveUniqueAnchor(lines, changeContext, start) {
|
|
3602
|
+
const hits = new Set;
|
|
3603
|
+
for (const same of autoRescueComparators) {
|
|
3604
|
+
for (const index2 of list(lines, [changeContext], start, same)) {
|
|
3605
|
+
hits.add(index2);
|
|
3606
|
+
}
|
|
3607
|
+
}
|
|
3608
|
+
if (hits.size === 0) {
|
|
3609
|
+
return { kind: "missing" };
|
|
3610
|
+
}
|
|
3611
|
+
if (hits.size > 1) {
|
|
3612
|
+
return { kind: "ambiguous" };
|
|
3613
|
+
}
|
|
3614
|
+
const index = [...hits][0];
|
|
3615
|
+
const canonicalLine = lines[index];
|
|
3616
|
+
const comparator = seekMatch(lines, [changeContext], index)?.comparator;
|
|
3617
|
+
return {
|
|
3618
|
+
kind: "match",
|
|
3619
|
+
index,
|
|
3620
|
+
exact: canonicalLine === changeContext,
|
|
3621
|
+
comparator: comparator ?? "exact",
|
|
3622
|
+
canonicalLine
|
|
3623
|
+
};
|
|
3624
|
+
}
|
|
3625
|
+
function locateChunk(lines, file, chunk, start, cfg) {
|
|
3626
|
+
const old_lines = chunk.old_lines;
|
|
3627
|
+
const new_lines = chunk.new_lines;
|
|
3628
|
+
const match = seekMatch(lines, old_lines, start, chunk.is_end_of_file ?? false);
|
|
3629
|
+
if (match) {
|
|
3630
|
+
const canonical_old_lines = lines.slice(match.index, match.index + old_lines.length);
|
|
3631
|
+
const rewritten = !match.exact;
|
|
3632
|
+
return {
|
|
3633
|
+
hit: { start: match.index, del: old_lines.length, add: [...new_lines] },
|
|
3634
|
+
old_lines,
|
|
3635
|
+
canonical_old_lines,
|
|
3636
|
+
canonical_new_lines: [...chunk.new_lines],
|
|
3637
|
+
resolved_is_end_of_file: match.index + canonical_old_lines.length === lines.length,
|
|
3638
|
+
rewritten,
|
|
3639
|
+
strategy: undefined,
|
|
3640
|
+
matchComparator: match.comparator
|
|
3641
|
+
};
|
|
3642
|
+
}
|
|
3643
|
+
if (cfg.prefixSuffix) {
|
|
3644
|
+
const rescued = rescueByPrefixSuffix(lines, old_lines, new_lines, start);
|
|
3645
|
+
if (rescued.kind === "ambiguous") {
|
|
3646
|
+
throw new Error(`Prefix/suffix rescue was ambiguous in ${file}:
|
|
3647
|
+
${chunk.old_lines.join(`
|
|
3648
|
+
`)}`);
|
|
3649
|
+
}
|
|
3650
|
+
if (rescued.kind === "match") {
|
|
3651
|
+
const prefixLength = prefix(old_lines, new_lines);
|
|
3652
|
+
const suffixLength = suffix(old_lines, new_lines, prefixLength);
|
|
3653
|
+
const canonicalStart = rescued.hit.start - prefixLength;
|
|
3654
|
+
const canonicalEnd = rescued.hit.start + rescued.hit.del + suffixLength;
|
|
3655
|
+
return {
|
|
3656
|
+
hit: rescued.hit,
|
|
3657
|
+
old_lines,
|
|
3658
|
+
canonical_old_lines: lines.slice(canonicalStart, canonicalEnd),
|
|
3659
|
+
canonical_new_lines: [...chunk.new_lines],
|
|
3660
|
+
resolved_is_end_of_file: canonicalEnd === lines.length,
|
|
3661
|
+
rewritten: true,
|
|
3662
|
+
strategy: "prefix/suffix",
|
|
3663
|
+
matchComparator: "exact"
|
|
3664
|
+
};
|
|
3665
|
+
}
|
|
3666
|
+
}
|
|
3667
|
+
if (cfg.lcsRescue) {
|
|
3668
|
+
const rescued = rescueByLcs(lines, old_lines, new_lines, start);
|
|
3669
|
+
if (rescued.kind === "ambiguous") {
|
|
3670
|
+
throw new Error(`LCS rescue was ambiguous in ${file}:
|
|
3671
|
+
${chunk.old_lines.join(`
|
|
3672
|
+
`)}`);
|
|
3673
|
+
}
|
|
3674
|
+
if (rescued.kind === "match") {
|
|
3675
|
+
return {
|
|
3676
|
+
hit: rescued.hit,
|
|
3677
|
+
old_lines,
|
|
3678
|
+
canonical_old_lines: lines.slice(rescued.hit.start, rescued.hit.start + rescued.hit.del),
|
|
3679
|
+
canonical_new_lines: [...chunk.new_lines],
|
|
3680
|
+
resolved_is_end_of_file: rescued.hit.start + rescued.hit.del === lines.length,
|
|
3681
|
+
rewritten: true,
|
|
3682
|
+
strategy: "lcs",
|
|
3683
|
+
matchComparator: "exact"
|
|
3684
|
+
};
|
|
3685
|
+
}
|
|
3686
|
+
}
|
|
3687
|
+
throw new Error(`Failed to find expected lines in ${file}:
|
|
3688
|
+
${chunk.old_lines.join(`
|
|
3689
|
+
`)}`);
|
|
3690
|
+
}
|
|
3691
|
+
function applyHits(lines, hits, eol = `
|
|
3692
|
+
`, hasFinalNewline = true) {
|
|
3693
|
+
const out = [...lines];
|
|
3694
|
+
for (let index = hits.length - 1;index >= 0; index -= 1) {
|
|
3695
|
+
out.splice(hits[index].start, hits[index].del, ...hits[index].add);
|
|
3696
|
+
}
|
|
3697
|
+
if (out.length === 0) {
|
|
3698
|
+
return "";
|
|
3699
|
+
}
|
|
3700
|
+
const rendered = out.join(eol);
|
|
3701
|
+
return hasFinalNewline ? `${rendered}${eol}` : rendered;
|
|
3702
|
+
}
|
|
3703
|
+
function resolveUpdateChunksFromFileLines(file, state, chunks, cfg) {
|
|
3704
|
+
const lines = [...state.lines];
|
|
3705
|
+
const resolved = [];
|
|
3706
|
+
let start = 0;
|
|
3707
|
+
for (const chunk of chunks) {
|
|
3708
|
+
const chunkStart = resolveChunkStart(lines, chunk, start);
|
|
3709
|
+
let strategy;
|
|
3710
|
+
if (chunk.old_lines.length === 0) {
|
|
3711
|
+
if (chunk.is_end_of_file) {
|
|
3712
|
+
resolved.push({
|
|
3713
|
+
hit: {
|
|
3714
|
+
start: lines.length,
|
|
3715
|
+
del: 0,
|
|
3716
|
+
add: [...chunk.new_lines]
|
|
3717
|
+
},
|
|
3718
|
+
old_lines: [],
|
|
3719
|
+
canonical_old_lines: [],
|
|
3720
|
+
canonical_new_lines: [...chunk.new_lines],
|
|
3721
|
+
resolved_is_end_of_file: true,
|
|
3722
|
+
rewritten: false,
|
|
3723
|
+
strategy,
|
|
3724
|
+
matchComparator: "exact"
|
|
3725
|
+
});
|
|
3726
|
+
start = lines.length;
|
|
3727
|
+
continue;
|
|
3728
|
+
}
|
|
3729
|
+
if (!chunk.change_context) {
|
|
3730
|
+
throw new Error(`Missing insertion anchor in ${file}`);
|
|
3731
|
+
}
|
|
3732
|
+
const anchorMatch = resolveUniqueAnchor(lines, chunk.change_context, start);
|
|
3733
|
+
if (anchorMatch.kind === "missing") {
|
|
3734
|
+
throw new Error(`Failed to find insertion anchor in ${file}:
|
|
3735
|
+
${chunk.change_context}`);
|
|
3736
|
+
}
|
|
3737
|
+
if (anchorMatch.kind === "ambiguous") {
|
|
3738
|
+
throw new Error(`Insertion anchor was ambiguous in ${file}:
|
|
3739
|
+
${chunk.change_context}`);
|
|
3740
|
+
}
|
|
3741
|
+
const insertAt = anchorMatch.index + 1;
|
|
3742
|
+
if (insertAt === lines.length) {
|
|
3743
|
+
resolved.push({
|
|
3744
|
+
hit: {
|
|
3745
|
+
start: insertAt,
|
|
3746
|
+
del: 0,
|
|
3747
|
+
add: [...chunk.new_lines]
|
|
3748
|
+
},
|
|
3749
|
+
old_lines: [],
|
|
3750
|
+
canonical_old_lines: [],
|
|
3751
|
+
canonical_new_lines: [...chunk.new_lines],
|
|
3752
|
+
canonical_change_context: anchorMatch.exact ? undefined : anchorMatch.canonicalLine,
|
|
3753
|
+
resolved_is_end_of_file: insertAt === lines.length,
|
|
3754
|
+
rewritten: !anchorMatch.exact,
|
|
3755
|
+
strategy: anchorMatch.exact ? strategy : "anchor",
|
|
3756
|
+
matchComparator: anchorMatch.comparator
|
|
3757
|
+
});
|
|
3758
|
+
start = insertAt;
|
|
3759
|
+
continue;
|
|
3760
|
+
}
|
|
3761
|
+
const anchor = lines[insertAt];
|
|
3762
|
+
strategy = "anchor";
|
|
3763
|
+
resolved.push({
|
|
3764
|
+
hit: {
|
|
3765
|
+
start: insertAt,
|
|
3766
|
+
del: 0,
|
|
3767
|
+
add: [...chunk.new_lines]
|
|
3768
|
+
},
|
|
3769
|
+
old_lines: [],
|
|
3770
|
+
canonical_old_lines: [anchor],
|
|
3771
|
+
canonical_new_lines: [...chunk.new_lines, anchor],
|
|
3772
|
+
resolved_is_end_of_file: insertAt + 1 === lines.length,
|
|
3773
|
+
rewritten: true,
|
|
3774
|
+
strategy,
|
|
3775
|
+
matchComparator: "exact"
|
|
3776
|
+
});
|
|
3777
|
+
start = insertAt;
|
|
3778
|
+
continue;
|
|
3779
|
+
}
|
|
3780
|
+
const found = locateChunk(lines, file, chunk, chunkStart, cfg);
|
|
3781
|
+
resolved.push(found);
|
|
3782
|
+
start = found.hit.start + found.hit.del;
|
|
3783
|
+
}
|
|
3784
|
+
resolved.sort((a, b) => a.hit.start - b.hit.start);
|
|
3785
|
+
for (let index = 1;index < resolved.length; index += 1) {
|
|
3786
|
+
const previous = resolved[index - 1].hit;
|
|
3787
|
+
const current = resolved[index].hit;
|
|
3788
|
+
if (previous.start + previous.del > current.start) {
|
|
3789
|
+
throw new Error(`Overlapping patch chunks in ${file}`);
|
|
3790
|
+
}
|
|
3791
|
+
}
|
|
3792
|
+
return {
|
|
3793
|
+
lines,
|
|
3794
|
+
resolved,
|
|
3795
|
+
eol: state.eol,
|
|
3796
|
+
hasFinalNewline: state.hasFinalNewline
|
|
3797
|
+
};
|
|
3798
|
+
}
|
|
3799
|
+
function deriveNewContentFromText(file, text, chunks, cfg) {
|
|
3800
|
+
const { lines, resolved, eol, hasFinalNewline } = resolveUpdateChunksFromFileLines(file, splitFileLines(text), chunks, cfg);
|
|
3801
|
+
return applyHits(lines, resolved.map((chunk) => chunk.hit), eol, hasFinalNewline);
|
|
3802
|
+
}
|
|
3803
|
+
function resolveUpdateChunksFromText(file, text, chunks, cfg) {
|
|
3804
|
+
return resolveUpdateChunksFromFileLines(file, splitFileLines(text), chunks, cfg);
|
|
3805
|
+
}
|
|
3806
|
+
|
|
3807
|
+
// src/hooks/apply-patch/execution-context.ts
|
|
3808
|
+
function isMissingPathError(error) {
|
|
3809
|
+
return !!error && typeof error === "object" && "code" in error && (error.code === "ENOENT" || error.code === "ENOTDIR");
|
|
3810
|
+
}
|
|
3811
|
+
async function real(target) {
|
|
3812
|
+
const parts = [];
|
|
3813
|
+
let current = path3.resolve(target);
|
|
3814
|
+
while (true) {
|
|
3815
|
+
const exact = await fs3.realpath(current).catch((error) => {
|
|
3816
|
+
if (isMissingPathError(error)) {
|
|
3817
|
+
return null;
|
|
3818
|
+
}
|
|
3819
|
+
throw createApplyPatchInternalError(`Failed to resolve real path: ${current}`, error);
|
|
3820
|
+
});
|
|
3821
|
+
if (exact) {
|
|
3822
|
+
return parts.length === 0 ? exact : path3.join(exact, ...parts.reverse());
|
|
3823
|
+
}
|
|
3824
|
+
const parent = path3.dirname(current);
|
|
3825
|
+
if (parent === current) {
|
|
3826
|
+
return parts.length === 0 ? current : path3.join(current, ...parts.reverse());
|
|
3827
|
+
}
|
|
3828
|
+
parts.push(path3.basename(current));
|
|
3829
|
+
current = parent;
|
|
3830
|
+
}
|
|
3831
|
+
}
|
|
3832
|
+
function inside(root, target) {
|
|
3833
|
+
const rel = path3.relative(root, target);
|
|
3834
|
+
return rel === "" || !rel.startsWith("..") && !path3.isAbsolute(rel);
|
|
3835
|
+
}
|
|
3836
|
+
function createPathGuardContext(root, worktree) {
|
|
3837
|
+
return {
|
|
3838
|
+
rootReal: real(root),
|
|
3839
|
+
worktreeReal: worktree && worktree !== "/" ? real(worktree) : undefined,
|
|
3840
|
+
realCache: new Map
|
|
3841
|
+
};
|
|
3842
|
+
}
|
|
3843
|
+
async function realCached(ctx, target) {
|
|
3844
|
+
const resolvedTarget = path3.resolve(target);
|
|
3845
|
+
let pending = ctx.realCache.get(resolvedTarget);
|
|
3846
|
+
if (!pending) {
|
|
3847
|
+
pending = real(resolvedTarget);
|
|
3848
|
+
ctx.realCache.set(resolvedTarget, pending);
|
|
3849
|
+
}
|
|
3850
|
+
return await pending;
|
|
3851
|
+
}
|
|
3852
|
+
async function guard(ctx, target) {
|
|
3853
|
+
const [targetReal, rootReal] = await Promise.all([
|
|
3854
|
+
realCached(ctx, target),
|
|
3855
|
+
ctx.rootReal
|
|
3856
|
+
]);
|
|
3857
|
+
if (inside(rootReal, targetReal)) {
|
|
3858
|
+
return;
|
|
3859
|
+
}
|
|
3860
|
+
if (!ctx.worktreeReal) {
|
|
3861
|
+
throw createApplyPatchBlockedError(`patch contains path outside workspace root: ${target}`);
|
|
3862
|
+
}
|
|
3863
|
+
const treeReal = await ctx.worktreeReal;
|
|
3864
|
+
if (inside(treeReal, targetReal)) {
|
|
3865
|
+
return;
|
|
3866
|
+
}
|
|
3867
|
+
throw createApplyPatchBlockedError(`patch contains path outside workspace root: ${target}`);
|
|
3868
|
+
}
|
|
3869
|
+
function createFileCacheContext() {
|
|
3870
|
+
return { stats: new Map };
|
|
3871
|
+
}
|
|
3872
|
+
async function statCached(ctx, filePath) {
|
|
3873
|
+
let pending = ctx.stats.get(filePath);
|
|
3874
|
+
if (!pending) {
|
|
3875
|
+
const nextPending = fs3.stat(filePath).catch((error) => {
|
|
3876
|
+
if (isMissingPathError(error)) {
|
|
3877
|
+
return null;
|
|
3878
|
+
}
|
|
3879
|
+
throw createApplyPatchInternalError(`Failed to stat file for patch verification: ${filePath}`, error);
|
|
3880
|
+
});
|
|
3881
|
+
ctx.stats.set(filePath, nextPending);
|
|
3882
|
+
pending = nextPending;
|
|
3883
|
+
}
|
|
3884
|
+
return await pending;
|
|
3885
|
+
}
|
|
3886
|
+
async function assertRegularFile(ctx, filePath, verb) {
|
|
3887
|
+
const stat2 = await statCached(ctx, filePath);
|
|
3888
|
+
if (!stat2 || stat2.isDirectory()) {
|
|
3889
|
+
throw createApplyPatchVerificationError(`Failed to read file to ${verb}: ${filePath}`);
|
|
3890
|
+
}
|
|
3891
|
+
}
|
|
3892
|
+
function collectPatchTargets(root, hunks) {
|
|
3893
|
+
const targets = new Set;
|
|
3894
|
+
for (const hunk of hunks) {
|
|
3895
|
+
targets.add(path3.resolve(root, hunk.path));
|
|
3896
|
+
if (hunk.type === "update" && hunk.move_path) {
|
|
3897
|
+
targets.add(path3.resolve(root, hunk.move_path));
|
|
3898
|
+
}
|
|
3899
|
+
}
|
|
3900
|
+
return [...targets];
|
|
3901
|
+
}
|
|
3902
|
+
function validatePatchPaths(hunks) {
|
|
3903
|
+
for (const hunk of hunks) {
|
|
3904
|
+
if (path3.isAbsolute(hunk.path)) {
|
|
3905
|
+
throw createApplyPatchValidationError(`absolute patch paths are not allowed: ${hunk.path}`);
|
|
3906
|
+
}
|
|
3907
|
+
if (hunk.type === "update" && hunk.move_path && path3.isAbsolute(hunk.move_path)) {
|
|
3908
|
+
throw createApplyPatchValidationError(`absolute patch paths are not allowed: ${hunk.move_path}`);
|
|
3909
|
+
}
|
|
3910
|
+
}
|
|
3911
|
+
}
|
|
3912
|
+
async function guardPatchTargets(root, worktree, targets) {
|
|
3913
|
+
const guardContext = createPathGuardContext(root, worktree);
|
|
3914
|
+
for (const target of targets) {
|
|
3915
|
+
await guard(guardContext, target);
|
|
3916
|
+
}
|
|
3917
|
+
return targets.length;
|
|
3918
|
+
}
|
|
3919
|
+
function parseValidatedPatch(patchText) {
|
|
3920
|
+
let hunks;
|
|
3921
|
+
try {
|
|
3922
|
+
hunks = parsePatchStrict(patchText).hunks;
|
|
3923
|
+
} catch (error) {
|
|
3924
|
+
throw createApplyPatchValidationError(getErrorMessage(error));
|
|
3925
|
+
}
|
|
3926
|
+
if (hunks.length === 0) {
|
|
3927
|
+
const clean = patchText.replace(/\r\n/g, `
|
|
3928
|
+
`).replace(/\r/g, `
|
|
3929
|
+
`).trim();
|
|
3930
|
+
if (clean === `*** Begin Patch
|
|
3931
|
+
*** End Patch`) {
|
|
3932
|
+
throw createApplyPatchValidationError("empty patch");
|
|
3933
|
+
}
|
|
3934
|
+
throw createApplyPatchValidationError("no hunks found");
|
|
3935
|
+
}
|
|
3936
|
+
validatePatchPaths(hunks);
|
|
3937
|
+
return hunks;
|
|
3938
|
+
}
|
|
3939
|
+
async function readPreparedFileText(filePath, verb) {
|
|
3940
|
+
try {
|
|
3941
|
+
return await fs3.readFile(filePath, "utf-8");
|
|
3942
|
+
} catch (error) {
|
|
3943
|
+
if (isMissingPathError(error)) {
|
|
3944
|
+
throw createApplyPatchVerificationError(`Failed to read file to ${verb}: ${filePath}`);
|
|
3945
|
+
}
|
|
3946
|
+
throw createApplyPatchInternalError(`Failed to read file for patch verification: ${filePath}`, error);
|
|
3947
|
+
}
|
|
3948
|
+
}
|
|
3949
|
+
async function createPatchExecutionContext(root, patchText, worktree) {
|
|
3950
|
+
const hunks = parseValidatedPatch(patchText);
|
|
3951
|
+
await guardPatchTargets(root, worktree, collectPatchTargets(root, hunks));
|
|
3952
|
+
const files = createFileCacheContext();
|
|
3953
|
+
const staged = new Map;
|
|
3954
|
+
async function assertPreparedPathMissing(filePath, verb) {
|
|
3955
|
+
const existing = staged.get(filePath);
|
|
3956
|
+
if (existing) {
|
|
3957
|
+
if (!existing.exists) {
|
|
3958
|
+
return;
|
|
3959
|
+
}
|
|
3960
|
+
throw createApplyPatchVerificationError(verb === "add" ? `Add File target already exists: ${filePath}` : `Move destination already exists: ${filePath}`);
|
|
3961
|
+
}
|
|
3962
|
+
const stat2 = await statCached(files, filePath);
|
|
3963
|
+
if (!stat2) {
|
|
3964
|
+
return;
|
|
3965
|
+
}
|
|
3966
|
+
throw createApplyPatchVerificationError(verb === "add" ? `Add File target already exists: ${filePath}` : `Move destination already exists: ${filePath}`);
|
|
3967
|
+
}
|
|
3968
|
+
async function getPreparedFileState(filePath, verb) {
|
|
3969
|
+
const existing = staged.get(filePath);
|
|
3970
|
+
if (existing) {
|
|
3971
|
+
if (!existing.exists) {
|
|
3972
|
+
throw createApplyPatchVerificationError(`Failed to read file to ${verb}: ${filePath}`);
|
|
3973
|
+
}
|
|
3974
|
+
return existing;
|
|
3975
|
+
}
|
|
3976
|
+
await assertRegularFile(files, filePath, verb);
|
|
3977
|
+
const stat2 = await statCached(files, filePath);
|
|
3978
|
+
const text = await readPreparedFileText(filePath, verb);
|
|
3979
|
+
const state = {
|
|
3980
|
+
exists: true,
|
|
3981
|
+
text,
|
|
3982
|
+
mode: stat2 ? stat2.mode & 4095 : undefined,
|
|
3983
|
+
derived: false
|
|
3984
|
+
};
|
|
3985
|
+
staged.set(filePath, state);
|
|
3986
|
+
return state;
|
|
3987
|
+
}
|
|
3988
|
+
return {
|
|
3989
|
+
hunks,
|
|
3990
|
+
staged,
|
|
3991
|
+
getPreparedFileState,
|
|
3992
|
+
assertPreparedPathMissing
|
|
3993
|
+
};
|
|
3994
|
+
}
|
|
3995
|
+
function resolvePreparedUpdate(filePath, currentText, hunk, cfg) {
|
|
3996
|
+
try {
|
|
3997
|
+
const { lines, resolved, eol, hasFinalNewline } = resolveUpdateChunksFromText(filePath, currentText, hunk.chunks, cfg);
|
|
3998
|
+
return {
|
|
3999
|
+
resolved,
|
|
4000
|
+
nextText: applyHits(lines, resolved.map((chunk) => chunk.hit), eol, hasFinalNewline)
|
|
4001
|
+
};
|
|
4002
|
+
} catch (error) {
|
|
4003
|
+
throw createApplyPatchVerificationError(getErrorMessage(error), error);
|
|
4004
|
+
}
|
|
4005
|
+
}
|
|
4006
|
+
function stageAddedText(contents) {
|
|
4007
|
+
return contents.length === 0 || contents.endsWith(`
|
|
4008
|
+
`) ? contents : `${contents}
|
|
4009
|
+
`;
|
|
4010
|
+
}
|
|
4011
|
+
// src/hooks/apply-patch/rewrite.ts
|
|
4012
|
+
import path4 from "path";
|
|
4013
|
+
function normalizeTextLineEndings(text) {
|
|
4014
|
+
return text.replace(/\r\n/g, `
|
|
4015
|
+
`).replace(/\r/g, `
|
|
4016
|
+
`);
|
|
4017
|
+
}
|
|
4018
|
+
function splitPatchTextLines(text) {
|
|
4019
|
+
const normalized = normalizeTextLineEndings(text);
|
|
4020
|
+
const lines = normalized.split(`
|
|
4021
|
+
`);
|
|
4022
|
+
if (normalized.endsWith(`
|
|
4023
|
+
`)) {
|
|
4024
|
+
lines.pop();
|
|
4025
|
+
}
|
|
4026
|
+
return lines;
|
|
4027
|
+
}
|
|
4028
|
+
function createCollapsedUpdateHunk(pathValue, filePath, baseText, finalText, cfg, movePath) {
|
|
4029
|
+
const collapsedChunk = {
|
|
4030
|
+
old_lines: splitPatchTextLines(baseText),
|
|
4031
|
+
new_lines: splitPatchTextLines(finalText),
|
|
4032
|
+
change_context: undefined,
|
|
4033
|
+
is_end_of_file: true
|
|
4034
|
+
};
|
|
4035
|
+
const minimizedChunk = minimizeMergedChunk(collapsedChunk);
|
|
4036
|
+
const chunk = minimizedChunk.old_lines.length === collapsedChunk.old_lines.length && minimizedChunk.new_lines.length === collapsedChunk.new_lines.length && minimizedChunk.change_context === collapsedChunk.change_context && minimizedChunk.is_end_of_file === collapsedChunk.is_end_of_file ? collapsedChunk : (() => {
|
|
4037
|
+
try {
|
|
4038
|
+
return deriveNewContentFromText(filePath, baseText, [minimizedChunk], cfg) === finalText ? minimizedChunk : collapsedChunk;
|
|
4039
|
+
} catch {
|
|
4040
|
+
return collapsedChunk;
|
|
4041
|
+
}
|
|
4042
|
+
})();
|
|
4043
|
+
return {
|
|
4044
|
+
type: "update",
|
|
4045
|
+
path: pathValue,
|
|
4046
|
+
move_path: movePath,
|
|
4047
|
+
chunks: [chunk]
|
|
4048
|
+
};
|
|
4049
|
+
}
|
|
4050
|
+
function clonePatchChunks(chunks) {
|
|
4051
|
+
return chunks.map((chunk) => ({
|
|
4052
|
+
old_lines: [...chunk.old_lines],
|
|
4053
|
+
new_lines: [...chunk.new_lines],
|
|
4054
|
+
change_context: chunk.change_context,
|
|
4055
|
+
is_end_of_file: chunk.is_end_of_file
|
|
4056
|
+
}));
|
|
4057
|
+
}
|
|
4058
|
+
function minimizeMergedChunk(chunk) {
|
|
4059
|
+
if (chunk.old_lines.length === 0 && chunk.new_lines.length === 0) {
|
|
4060
|
+
return {
|
|
4061
|
+
old_lines: [],
|
|
4062
|
+
new_lines: [],
|
|
4063
|
+
change_context: chunk.change_context,
|
|
4064
|
+
is_end_of_file: chunk.is_end_of_file
|
|
4065
|
+
};
|
|
4066
|
+
}
|
|
4067
|
+
let prefixLength = 0;
|
|
4068
|
+
while (prefixLength < chunk.old_lines.length && prefixLength < chunk.new_lines.length && chunk.old_lines[prefixLength] === chunk.new_lines[prefixLength]) {
|
|
4069
|
+
prefixLength += 1;
|
|
4070
|
+
}
|
|
4071
|
+
let suffixLength = 0;
|
|
4072
|
+
while (chunk.old_lines.length - suffixLength - 1 >= prefixLength && chunk.new_lines.length - suffixLength - 1 >= prefixLength && chunk.old_lines[chunk.old_lines.length - suffixLength - 1] === chunk.new_lines[chunk.new_lines.length - suffixLength - 1]) {
|
|
4073
|
+
suffixLength += 1;
|
|
4074
|
+
}
|
|
4075
|
+
if (prefixLength === 0 && suffixLength === 0) {
|
|
4076
|
+
return {
|
|
4077
|
+
old_lines: [...chunk.old_lines],
|
|
4078
|
+
new_lines: [...chunk.new_lines],
|
|
4079
|
+
change_context: chunk.change_context,
|
|
4080
|
+
is_end_of_file: chunk.is_end_of_file
|
|
4081
|
+
};
|
|
4082
|
+
}
|
|
4083
|
+
return {
|
|
4084
|
+
old_lines: chunk.old_lines.slice(prefixLength, chunk.old_lines.length - suffixLength),
|
|
4085
|
+
new_lines: chunk.new_lines.slice(prefixLength, chunk.new_lines.length - suffixLength),
|
|
4086
|
+
change_context: prefixLength > 0 ? chunk.old_lines[prefixLength - 1] : chunk.change_context,
|
|
4087
|
+
is_end_of_file: chunk.is_end_of_file && suffixLength === 0 ? true : undefined
|
|
4088
|
+
};
|
|
4089
|
+
}
|
|
4090
|
+
function createUpdateHunk(pathValue, chunks, movePath) {
|
|
4091
|
+
return {
|
|
4092
|
+
type: "update",
|
|
4093
|
+
path: pathValue,
|
|
4094
|
+
move_path: movePath,
|
|
4095
|
+
chunks: clonePatchChunks(chunks)
|
|
4096
|
+
};
|
|
4097
|
+
}
|
|
4098
|
+
function mergeSameFileUpdateGroupChunks(filePath, group, nextChunks, finalText, cfg) {
|
|
4099
|
+
if (!group.chunks) {
|
|
4100
|
+
return;
|
|
4101
|
+
}
|
|
4102
|
+
const mergedChunks = [
|
|
4103
|
+
...clonePatchChunks(group.chunks).map(minimizeMergedChunk),
|
|
4104
|
+
...clonePatchChunks(nextChunks).map(minimizeMergedChunk)
|
|
4105
|
+
];
|
|
4106
|
+
try {
|
|
4107
|
+
const mergedText = deriveNewContentFromText(filePath, group.baseText, mergedChunks, cfg);
|
|
4108
|
+
return mergedText === finalText ? mergedChunks : undefined;
|
|
4109
|
+
} catch {
|
|
4110
|
+
return;
|
|
4111
|
+
}
|
|
4112
|
+
}
|
|
4113
|
+
function addContentsFromFinalText(text) {
|
|
4114
|
+
return text.endsWith(`
|
|
4115
|
+
`) ? text.slice(0, -1) : text;
|
|
4116
|
+
}
|
|
4117
|
+
function renderRewriteDependencyGroup(group, cfg) {
|
|
4118
|
+
if (group.kind === "add") {
|
|
4119
|
+
return {
|
|
4120
|
+
type: "add",
|
|
4121
|
+
path: group.group.outputPath,
|
|
4122
|
+
contents: addContentsFromFinalText(group.group.finalText)
|
|
4123
|
+
};
|
|
4124
|
+
}
|
|
4125
|
+
return group.group.chunks ? createUpdateHunk(group.group.sourcePath, group.group.chunks, group.group.outputPath !== group.group.sourcePath ? group.group.outputPath : undefined) : createCollapsedUpdateHunk(group.group.sourcePath, group.group.sourceFilePath, group.group.baseText, group.group.finalText, cfg, group.group.outputPath !== group.group.sourcePath ? group.group.outputPath : undefined);
|
|
4126
|
+
}
|
|
4127
|
+
function rewriteModeForDependentUpdate(group) {
|
|
4128
|
+
if (group.kind === "add") {
|
|
4129
|
+
return "collapse:add-followed-by-update";
|
|
4130
|
+
}
|
|
4131
|
+
if (group.group.outputPath !== group.group.sourcePath) {
|
|
4132
|
+
return "collapse:move-followed-by-update";
|
|
4133
|
+
}
|
|
4134
|
+
return "merge:same-file-updates";
|
|
4135
|
+
}
|
|
4136
|
+
function combineDependentUpdateGroup(filePath, group, nextChunks, finalText, nextOutputPath, nextOutputFilePath, cfg) {
|
|
4137
|
+
if (group.kind === "add") {
|
|
4138
|
+
return {
|
|
4139
|
+
kind: "add",
|
|
4140
|
+
group: {
|
|
4141
|
+
...group.group,
|
|
4142
|
+
outputPath: nextOutputPath,
|
|
4143
|
+
outputFilePath: nextOutputFilePath,
|
|
4144
|
+
finalText
|
|
4145
|
+
}
|
|
4146
|
+
};
|
|
4147
|
+
}
|
|
4148
|
+
const mergedChunks = group.group.outputFilePath === filePath && group.group.sourceFilePath === filePath && nextOutputFilePath === filePath ? mergeSameFileUpdateGroupChunks(filePath, group.group, nextChunks, finalText, cfg) : undefined;
|
|
4149
|
+
return {
|
|
4150
|
+
kind: "update",
|
|
4151
|
+
group: {
|
|
4152
|
+
...group.group,
|
|
4153
|
+
outputPath: nextOutputPath,
|
|
4154
|
+
outputFilePath: nextOutputFilePath,
|
|
4155
|
+
finalText,
|
|
4156
|
+
chunks: mergedChunks
|
|
4157
|
+
}
|
|
4158
|
+
};
|
|
4159
|
+
}
|
|
4160
|
+
async function rewritePatch(root, patchText, cfg, worktree) {
|
|
4161
|
+
try {
|
|
4162
|
+
let clearDependencyGroup = function(filePath) {
|
|
4163
|
+
dependencyGroups.delete(filePath);
|
|
4164
|
+
};
|
|
4165
|
+
const { hunks, staged, getPreparedFileState, assertPreparedPathMissing } = await createPatchExecutionContext(root, patchText, worktree);
|
|
4166
|
+
const normalizedPatchText = normalizePatchText(patchText);
|
|
4167
|
+
const rewritten = [];
|
|
4168
|
+
let changed = false;
|
|
4169
|
+
let rewrittenChunks = 0;
|
|
4170
|
+
const rewriteModes = new Set;
|
|
4171
|
+
const totalChunks = hunks.reduce((count, hunk) => count + (hunk.type === "update" ? hunk.chunks.length : 0), 0);
|
|
4172
|
+
const dependencyGroups = new Map;
|
|
4173
|
+
for (const hunk of hunks) {
|
|
4174
|
+
if (hunk.type === "add") {
|
|
4175
|
+
const filePath2 = path4.resolve(root, hunk.path);
|
|
4176
|
+
await assertPreparedPathMissing(filePath2, "add");
|
|
4177
|
+
rewritten.push(hunk);
|
|
4178
|
+
clearDependencyGroup(filePath2);
|
|
4179
|
+
const finalText = stageAddedText(hunk.contents);
|
|
4180
|
+
staged.set(filePath2, {
|
|
4181
|
+
exists: true,
|
|
4182
|
+
text: finalText,
|
|
4183
|
+
derived: true
|
|
4184
|
+
});
|
|
4185
|
+
dependencyGroups.set(filePath2, {
|
|
4186
|
+
kind: "add",
|
|
4187
|
+
group: {
|
|
4188
|
+
index: rewritten.length - 1,
|
|
4189
|
+
outputPath: hunk.path,
|
|
4190
|
+
outputFilePath: filePath2,
|
|
4191
|
+
finalText
|
|
4192
|
+
}
|
|
4193
|
+
});
|
|
4194
|
+
continue;
|
|
4195
|
+
}
|
|
4196
|
+
if (hunk.type === "delete") {
|
|
4197
|
+
const filePath2 = path4.resolve(root, hunk.path);
|
|
4198
|
+
await getPreparedFileState(filePath2, "delete");
|
|
4199
|
+
clearDependencyGroup(filePath2);
|
|
4200
|
+
rewritten.push(hunk);
|
|
4201
|
+
staged.set(filePath2, { exists: false, derived: true });
|
|
4202
|
+
continue;
|
|
4203
|
+
}
|
|
4204
|
+
const filePath = path4.resolve(root, hunk.path);
|
|
4205
|
+
const currentDependency = dependencyGroups.get(filePath);
|
|
4206
|
+
const current = await getPreparedFileState(filePath, "update");
|
|
4207
|
+
if (!current.exists) {
|
|
4208
|
+
throw createApplyPatchVerificationError(`Failed to read file to update: ${filePath}`);
|
|
4209
|
+
}
|
|
4210
|
+
const movePath = hunk.move_path ? path4.resolve(root, hunk.move_path) : undefined;
|
|
4211
|
+
if (movePath && movePath !== filePath) {
|
|
4212
|
+
await assertPreparedPathMissing(movePath, "move");
|
|
4213
|
+
}
|
|
4214
|
+
const { resolved, nextText } = resolvePreparedUpdate(filePath, current.text, hunk, cfg);
|
|
4215
|
+
const next = resolved.map((chunk, index) => ({
|
|
4216
|
+
old_lines: [...chunk.canonical_old_lines],
|
|
4217
|
+
new_lines: [...chunk.canonical_new_lines],
|
|
4218
|
+
change_context: chunk.canonical_change_context ?? hunk.chunks[index].change_context,
|
|
4219
|
+
is_end_of_file: hunk.chunks[index].is_end_of_file && chunk.resolved_is_end_of_file ? true : undefined
|
|
4220
|
+
}));
|
|
4221
|
+
for (const chunk of resolved) {
|
|
4222
|
+
if (!chunk.rewritten) {
|
|
4223
|
+
continue;
|
|
4224
|
+
}
|
|
4225
|
+
changed = true;
|
|
4226
|
+
rewrittenChunks += 1;
|
|
4227
|
+
if (chunk.strategy) {
|
|
4228
|
+
rewriteModes.add(chunk.strategy);
|
|
4229
|
+
continue;
|
|
4230
|
+
}
|
|
4231
|
+
if (chunk.matchComparator && chunk.matchComparator !== "exact") {
|
|
4232
|
+
rewriteModes.add(`match:${chunk.matchComparator}`);
|
|
4233
|
+
}
|
|
4234
|
+
}
|
|
4235
|
+
const nextOutputPath = hunk.move_path ?? hunk.path;
|
|
4236
|
+
const nextOutputFilePath = movePath ?? filePath;
|
|
4237
|
+
if (current.derived && currentDependency) {
|
|
4238
|
+
const nextGroup = combineDependentUpdateGroup(filePath, currentDependency, next, nextText, nextOutputPath, nextOutputFilePath, cfg);
|
|
4239
|
+
rewritten[currentDependency.group.index] = renderRewriteDependencyGroup(nextGroup, cfg);
|
|
4240
|
+
changed = true;
|
|
4241
|
+
rewriteModes.add(rewriteModeForDependentUpdate(currentDependency));
|
|
4242
|
+
clearDependencyGroup(filePath);
|
|
4243
|
+
if (movePath && movePath !== filePath) {
|
|
4244
|
+
clearDependencyGroup(movePath);
|
|
4245
|
+
}
|
|
4246
|
+
dependencyGroups.set(nextOutputFilePath, nextGroup);
|
|
4247
|
+
} else {
|
|
4248
|
+
rewritten.push(createUpdateHunk(hunk.path, next, hunk.move_path));
|
|
4249
|
+
clearDependencyGroup(filePath);
|
|
4250
|
+
if (movePath && movePath !== filePath) {
|
|
4251
|
+
clearDependencyGroup(movePath);
|
|
4252
|
+
}
|
|
4253
|
+
dependencyGroups.set(nextOutputFilePath, {
|
|
4254
|
+
kind: "update",
|
|
4255
|
+
group: {
|
|
4256
|
+
index: rewritten.length - 1,
|
|
4257
|
+
sourcePath: hunk.path,
|
|
4258
|
+
outputPath: nextOutputPath,
|
|
4259
|
+
sourceFilePath: filePath,
|
|
4260
|
+
outputFilePath: nextOutputFilePath,
|
|
4261
|
+
baseText: current.text,
|
|
4262
|
+
finalText: nextText,
|
|
4263
|
+
chunks: clonePatchChunks(next)
|
|
4264
|
+
}
|
|
4265
|
+
});
|
|
4266
|
+
}
|
|
4267
|
+
if (movePath && movePath !== filePath) {
|
|
4268
|
+
staged.set(filePath, { exists: false, derived: true });
|
|
4269
|
+
staged.set(movePath, {
|
|
4270
|
+
exists: true,
|
|
4271
|
+
text: nextText,
|
|
4272
|
+
mode: current.mode,
|
|
4273
|
+
derived: true
|
|
4274
|
+
});
|
|
4275
|
+
} else {
|
|
4276
|
+
staged.set(filePath, {
|
|
4277
|
+
exists: true,
|
|
4278
|
+
text: nextText,
|
|
4279
|
+
mode: current.mode,
|
|
4280
|
+
derived: true
|
|
4281
|
+
});
|
|
4282
|
+
}
|
|
4283
|
+
}
|
|
4284
|
+
if (!changed) {
|
|
4285
|
+
if (normalizedPatchText !== patchText) {
|
|
4286
|
+
return {
|
|
4287
|
+
patchText: normalizedPatchText,
|
|
4288
|
+
changed: true,
|
|
4289
|
+
rewrittenChunks: 0,
|
|
4290
|
+
totalChunks,
|
|
4291
|
+
rewriteModes: ["normalize:patch-text"]
|
|
4292
|
+
};
|
|
4293
|
+
}
|
|
4294
|
+
return {
|
|
4295
|
+
patchText,
|
|
4296
|
+
changed: false,
|
|
4297
|
+
rewrittenChunks: 0,
|
|
4298
|
+
totalChunks,
|
|
4299
|
+
rewriteModes: []
|
|
4300
|
+
};
|
|
4301
|
+
}
|
|
4302
|
+
return {
|
|
4303
|
+
patchText: formatPatch({ hunks: rewritten }),
|
|
4304
|
+
changed: true,
|
|
4305
|
+
rewrittenChunks,
|
|
4306
|
+
totalChunks,
|
|
4307
|
+
rewriteModes: [...rewriteModes].sort()
|
|
4308
|
+
};
|
|
4309
|
+
} catch (error) {
|
|
4310
|
+
throw ensureApplyPatchError(error, "Unexpected rewrite failure");
|
|
4311
|
+
}
|
|
4312
|
+
}
|
|
4313
|
+
// src/hooks/apply-patch/index.ts
|
|
4314
|
+
var APPLY_PATCH_RESCUE_OPTIONS = {
|
|
4315
|
+
prefixSuffix: true,
|
|
4316
|
+
lcsRescue: true
|
|
4317
|
+
};
|
|
4318
|
+
function createApplyPatchHook(ctx) {
|
|
4319
|
+
function logHookStatus(state, data) {
|
|
4320
|
+
log(`apply-patch hook ${state}`, data);
|
|
4321
|
+
}
|
|
4322
|
+
return {
|
|
4323
|
+
"tool.execute.before": async (input, output) => {
|
|
4324
|
+
if (input.tool !== "apply_patch") {
|
|
4325
|
+
return;
|
|
4326
|
+
}
|
|
4327
|
+
if (typeof output.args?.patchText !== "string") {
|
|
4328
|
+
return;
|
|
4329
|
+
}
|
|
4330
|
+
const root = input.directory || ctx.directory || process.cwd();
|
|
4331
|
+
const worktree = ctx.worktree || root;
|
|
4332
|
+
try {
|
|
4333
|
+
const result = await rewritePatch(root, output.args.patchText, APPLY_PATCH_RESCUE_OPTIONS, worktree);
|
|
4334
|
+
if (result.changed) {
|
|
4335
|
+
output.args.patchText = result.patchText;
|
|
4336
|
+
logHookStatus("rewrite", {
|
|
4337
|
+
rewrittenChunks: result.rewrittenChunks,
|
|
4338
|
+
totalChunks: result.totalChunks,
|
|
4339
|
+
strategies: result.rewriteModes
|
|
4340
|
+
});
|
|
4341
|
+
return;
|
|
4342
|
+
}
|
|
4343
|
+
logHookStatus("unchanged", {
|
|
4344
|
+
rewrittenChunks: 0,
|
|
4345
|
+
totalChunks: result.totalChunks
|
|
4346
|
+
});
|
|
4347
|
+
return;
|
|
4348
|
+
} catch (error) {
|
|
4349
|
+
const normalizedError = isApplyPatchError(error) ? error : createApplyPatchInternalError(`Unexpected hook failure before native apply: ${error instanceof Error ? error.message : String(error)}`, error);
|
|
4350
|
+
const details = getApplyPatchErrorDetails(normalizedError);
|
|
4351
|
+
logHookStatus(isApplyPatchVerificationError(normalizedError) ? "verification" : normalizedError.kind === "validation" ? "validation" : normalizedError.kind === "internal" ? "internal" : "blocked", {
|
|
4352
|
+
kind: details?.kind ?? "internal",
|
|
4353
|
+
code: details?.code ?? "internal_unexpected",
|
|
4354
|
+
reason: normalizedError.message,
|
|
4355
|
+
failOpen: false,
|
|
4356
|
+
rescueOptions: APPLY_PATCH_RESCUE_OPTIONS,
|
|
4357
|
+
rewriteStage: "before-native"
|
|
4358
|
+
});
|
|
4359
|
+
throw normalizedError;
|
|
4360
|
+
}
|
|
4361
|
+
}
|
|
4362
|
+
};
|
|
4363
|
+
}
|
|
2959
4364
|
// src/hooks/auto-update-checker/cache.ts
|
|
2960
|
-
import * as
|
|
2961
|
-
import * as
|
|
4365
|
+
import * as fs4 from "fs";
|
|
4366
|
+
import * as path6 from "path";
|
|
2962
4367
|
// src/hooks/auto-update-checker/constants.ts
|
|
2963
4368
|
import * as os2 from "os";
|
|
2964
|
-
import * as
|
|
4369
|
+
import * as path5 from "path";
|
|
2965
4370
|
var PACKAGE_NAME = "oh-my-opencode-slim";
|
|
2966
4371
|
var NPM_REGISTRY_URL = `https://registry.npmjs.org/-/package/${PACKAGE_NAME}/dist-tags`;
|
|
2967
4372
|
var NPM_FETCH_TIMEOUT = 5000;
|
|
2968
4373
|
function getCacheDir() {
|
|
2969
4374
|
if (process.platform === "win32") {
|
|
2970
|
-
return
|
|
4375
|
+
return path5.join(process.env.LOCALAPPDATA ?? os2.homedir(), "opencode");
|
|
2971
4376
|
}
|
|
2972
|
-
return
|
|
4377
|
+
return path5.join(os2.homedir(), ".cache", "opencode");
|
|
2973
4378
|
}
|
|
2974
4379
|
var CACHE_DIR = getCacheDir();
|
|
2975
|
-
var INSTALLED_PACKAGE_JSON =
|
|
4380
|
+
var INSTALLED_PACKAGE_JSON = path5.join(CACHE_DIR, "node_modules", PACKAGE_NAME, "package.json");
|
|
2976
4381
|
var configPaths = getOpenCodeConfigPaths();
|
|
2977
4382
|
var USER_OPENCODE_CONFIG = configPaths[0];
|
|
2978
4383
|
var USER_OPENCODE_CONFIG_JSONC = configPaths[1];
|
|
2979
4384
|
|
|
2980
4385
|
// src/hooks/auto-update-checker/cache.ts
|
|
2981
4386
|
function removeFromBunLock(packageName) {
|
|
2982
|
-
const lockPath =
|
|
2983
|
-
if (!
|
|
4387
|
+
const lockPath = path6.join(CACHE_DIR, "bun.lock");
|
|
4388
|
+
if (!fs4.existsSync(lockPath))
|
|
2984
4389
|
return false;
|
|
2985
4390
|
try {
|
|
2986
|
-
const content =
|
|
4391
|
+
const content = fs4.readFileSync(lockPath, "utf-8");
|
|
2987
4392
|
let lock;
|
|
2988
4393
|
try {
|
|
2989
4394
|
lock = JSON.parse(stripJsonComments(content));
|
|
@@ -3000,7 +4405,7 @@ function removeFromBunLock(packageName) {
|
|
|
3000
4405
|
modified = true;
|
|
3001
4406
|
}
|
|
3002
4407
|
if (modified) {
|
|
3003
|
-
|
|
4408
|
+
fs4.writeFileSync(lockPath, JSON.stringify(lock, null, 2));
|
|
3004
4409
|
log(`[auto-update-checker] Removed from bun.lock: ${packageName}`);
|
|
3005
4410
|
}
|
|
3006
4411
|
return modified;
|
|
@@ -3011,23 +4416,23 @@ function removeFromBunLock(packageName) {
|
|
|
3011
4416
|
}
|
|
3012
4417
|
function invalidatePackage(packageName = PACKAGE_NAME) {
|
|
3013
4418
|
try {
|
|
3014
|
-
const pkgDir =
|
|
3015
|
-
const pkgJsonPath =
|
|
4419
|
+
const pkgDir = path6.join(CACHE_DIR, "node_modules", packageName);
|
|
4420
|
+
const pkgJsonPath = path6.join(CACHE_DIR, "package.json");
|
|
3016
4421
|
let packageRemoved = false;
|
|
3017
4422
|
let dependencyRemoved = false;
|
|
3018
4423
|
let lockRemoved = false;
|
|
3019
|
-
if (
|
|
3020
|
-
|
|
4424
|
+
if (fs4.existsSync(pkgDir)) {
|
|
4425
|
+
fs4.rmSync(pkgDir, { recursive: true, force: true });
|
|
3021
4426
|
log(`[auto-update-checker] Package removed: ${pkgDir}`);
|
|
3022
4427
|
packageRemoved = true;
|
|
3023
4428
|
}
|
|
3024
|
-
if (
|
|
4429
|
+
if (fs4.existsSync(pkgJsonPath)) {
|
|
3025
4430
|
try {
|
|
3026
|
-
const content =
|
|
4431
|
+
const content = fs4.readFileSync(pkgJsonPath, "utf-8");
|
|
3027
4432
|
const pkgJson = JSON.parse(stripJsonComments(content));
|
|
3028
4433
|
if (pkgJson.dependencies?.[packageName]) {
|
|
3029
4434
|
delete pkgJson.dependencies[packageName];
|
|
3030
|
-
|
|
4435
|
+
fs4.writeFileSync(pkgJsonPath, JSON.stringify(pkgJson, null, 2));
|
|
3031
4436
|
log(`[auto-update-checker] Dependency removed from package.json: ${packageName}`);
|
|
3032
4437
|
dependencyRemoved = true;
|
|
3033
4438
|
}
|
|
@@ -3048,8 +4453,8 @@ function invalidatePackage(packageName = PACKAGE_NAME) {
|
|
|
3048
4453
|
}
|
|
3049
4454
|
|
|
3050
4455
|
// src/hooks/auto-update-checker/checker.ts
|
|
3051
|
-
import * as
|
|
3052
|
-
import * as
|
|
4456
|
+
import * as fs5 from "fs";
|
|
4457
|
+
import * as path7 from "path";
|
|
3053
4458
|
import { fileURLToPath } from "url";
|
|
3054
4459
|
function isPrereleaseVersion(version) {
|
|
3055
4460
|
return version.includes("-");
|
|
@@ -3074,8 +4479,8 @@ function extractChannel(version) {
|
|
|
3074
4479
|
}
|
|
3075
4480
|
function getConfigPaths(directory) {
|
|
3076
4481
|
return [
|
|
3077
|
-
|
|
3078
|
-
|
|
4482
|
+
path7.join(directory, ".opencode", "opencode.json"),
|
|
4483
|
+
path7.join(directory, ".opencode", "opencode.jsonc"),
|
|
3079
4484
|
USER_OPENCODE_CONFIG,
|
|
3080
4485
|
USER_OPENCODE_CONFIG_JSONC
|
|
3081
4486
|
];
|
|
@@ -3083,9 +4488,9 @@ function getConfigPaths(directory) {
|
|
|
3083
4488
|
function getLocalDevPath(directory) {
|
|
3084
4489
|
for (const configPath of getConfigPaths(directory)) {
|
|
3085
4490
|
try {
|
|
3086
|
-
if (!
|
|
4491
|
+
if (!fs5.existsSync(configPath))
|
|
3087
4492
|
continue;
|
|
3088
|
-
const content =
|
|
4493
|
+
const content = fs5.readFileSync(configPath, "utf-8");
|
|
3089
4494
|
const config = JSON.parse(stripJsonComments(content));
|
|
3090
4495
|
const plugins = config.plugin ?? [];
|
|
3091
4496
|
for (const entry of plugins) {
|
|
@@ -3103,19 +4508,19 @@ function getLocalDevPath(directory) {
|
|
|
3103
4508
|
}
|
|
3104
4509
|
function findPackageJsonUp(startPath) {
|
|
3105
4510
|
try {
|
|
3106
|
-
const
|
|
3107
|
-
let dir =
|
|
4511
|
+
const stat2 = fs5.statSync(startPath);
|
|
4512
|
+
let dir = stat2.isDirectory() ? startPath : path7.dirname(startPath);
|
|
3108
4513
|
for (let i = 0;i < 10; i++) {
|
|
3109
|
-
const pkgPath =
|
|
3110
|
-
if (
|
|
4514
|
+
const pkgPath = path7.join(dir, "package.json");
|
|
4515
|
+
if (fs5.existsSync(pkgPath)) {
|
|
3111
4516
|
try {
|
|
3112
|
-
const content =
|
|
4517
|
+
const content = fs5.readFileSync(pkgPath, "utf-8");
|
|
3113
4518
|
const pkg = JSON.parse(content);
|
|
3114
4519
|
if (pkg.name === PACKAGE_NAME)
|
|
3115
4520
|
return pkgPath;
|
|
3116
4521
|
} catch {}
|
|
3117
4522
|
}
|
|
3118
|
-
const parent =
|
|
4523
|
+
const parent = path7.dirname(dir);
|
|
3119
4524
|
if (parent === dir)
|
|
3120
4525
|
break;
|
|
3121
4526
|
dir = parent;
|
|
@@ -3131,7 +4536,7 @@ function getLocalDevVersion(directory) {
|
|
|
3131
4536
|
const pkgPath = findPackageJsonUp(localPath);
|
|
3132
4537
|
if (!pkgPath)
|
|
3133
4538
|
return null;
|
|
3134
|
-
const content =
|
|
4539
|
+
const content = fs5.readFileSync(pkgPath, "utf-8");
|
|
3135
4540
|
const pkg = JSON.parse(content);
|
|
3136
4541
|
return pkg.version ?? null;
|
|
3137
4542
|
} catch {
|
|
@@ -3141,9 +4546,9 @@ function getLocalDevVersion(directory) {
|
|
|
3141
4546
|
function findPluginEntry(directory) {
|
|
3142
4547
|
for (const configPath of getConfigPaths(directory)) {
|
|
3143
4548
|
try {
|
|
3144
|
-
if (!
|
|
4549
|
+
if (!fs5.existsSync(configPath))
|
|
3145
4550
|
continue;
|
|
3146
|
-
const content =
|
|
4551
|
+
const content = fs5.readFileSync(configPath, "utf-8");
|
|
3147
4552
|
const config = JSON.parse(stripJsonComments(content));
|
|
3148
4553
|
const plugins = config.plugin ?? [];
|
|
3149
4554
|
for (const entry of plugins) {
|
|
@@ -3170,8 +4575,8 @@ function getCachedVersion() {
|
|
|
3170
4575
|
if (cachedPackageVersion)
|
|
3171
4576
|
return cachedPackageVersion;
|
|
3172
4577
|
try {
|
|
3173
|
-
if (
|
|
3174
|
-
const content =
|
|
4578
|
+
if (fs5.existsSync(INSTALLED_PACKAGE_JSON)) {
|
|
4579
|
+
const content = fs5.readFileSync(INSTALLED_PACKAGE_JSON, "utf-8");
|
|
3175
4580
|
const pkg = JSON.parse(content);
|
|
3176
4581
|
if (pkg.version) {
|
|
3177
4582
|
cachedPackageVersion = pkg.version;
|
|
@@ -3180,10 +4585,10 @@ function getCachedVersion() {
|
|
|
3180
4585
|
}
|
|
3181
4586
|
} catch {}
|
|
3182
4587
|
try {
|
|
3183
|
-
const currentDir =
|
|
4588
|
+
const currentDir = path7.dirname(fileURLToPath(import.meta.url));
|
|
3184
4589
|
const pkgPath = findPackageJsonUp(currentDir);
|
|
3185
4590
|
if (pkgPath) {
|
|
3186
|
-
const content =
|
|
4591
|
+
const content = fs5.readFileSync(pkgPath, "utf-8");
|
|
3187
4592
|
const pkg = JSON.parse(content);
|
|
3188
4593
|
if (pkg.version) {
|
|
3189
4594
|
cachedPackageVersion = pkg.version;
|
|
@@ -3858,17 +5263,35 @@ ${originalText}`;
|
|
|
3858
5263
|
};
|
|
3859
5264
|
}
|
|
3860
5265
|
// src/hooks/post-file-tool-nudge/index.ts
|
|
3861
|
-
var
|
|
3862
|
-
|
|
3863
|
-
|
|
3864
|
-
|
|
3865
|
-
function createPostFileToolNudgeHook() {
|
|
5266
|
+
var POST_FILE_TOOL_NUDGE = PHASE_REMINDER_TEXT;
|
|
5267
|
+
var FILE_TOOLS = new Set(["Read", "read", "Write", "write"]);
|
|
5268
|
+
function createPostFileToolNudgeHook(options = {}) {
|
|
5269
|
+
const pendingSessionIds = new Set;
|
|
3866
5270
|
return {
|
|
3867
|
-
"tool.execute.after": async (input,
|
|
3868
|
-
if (
|
|
5271
|
+
"tool.execute.after": async (input, _output) => {
|
|
5272
|
+
if (!FILE_TOOLS.has(input.tool) || !input.sessionID) {
|
|
5273
|
+
return;
|
|
5274
|
+
}
|
|
5275
|
+
pendingSessionIds.add(input.sessionID);
|
|
5276
|
+
},
|
|
5277
|
+
"experimental.chat.system.transform": async (input, output) => {
|
|
5278
|
+
if (!input.sessionID || !pendingSessionIds.delete(input.sessionID)) {
|
|
3869
5279
|
return;
|
|
3870
5280
|
}
|
|
3871
|
-
|
|
5281
|
+
if (options.shouldInject && !options.shouldInject(input.sessionID)) {
|
|
5282
|
+
return;
|
|
5283
|
+
}
|
|
5284
|
+
output.system.push(POST_FILE_TOOL_NUDGE);
|
|
5285
|
+
},
|
|
5286
|
+
event: async (input) => {
|
|
5287
|
+
if (input.event.type !== "session.deleted") {
|
|
5288
|
+
return;
|
|
5289
|
+
}
|
|
5290
|
+
const sessionID = input.event.properties?.sessionID ?? input.event.properties?.info?.id;
|
|
5291
|
+
if (!sessionID) {
|
|
5292
|
+
return;
|
|
5293
|
+
}
|
|
5294
|
+
pendingSessionIds.delete(sessionID);
|
|
3872
5295
|
}
|
|
3873
5296
|
};
|
|
3874
5297
|
}
|
|
@@ -3878,6 +5301,7 @@ var HOOK_NAME = "todo-continuation";
|
|
|
3878
5301
|
var COMMAND_NAME = "auto-continue";
|
|
3879
5302
|
var CONTINUATION_PROMPT = "[Auto-continue: enabled - there are incomplete todos remaining. Continue with the next uncompleted item. Press Esc to cancel. If you need user input or review for the next item, ask instead of proceeding.]";
|
|
3880
5303
|
var SUPPRESS_AFTER_ABORT_MS = 5000;
|
|
5304
|
+
var NOTIFICATION_BUSY_GRACE_MS = 250;
|
|
3881
5305
|
var QUESTION_PHRASES = [
|
|
3882
5306
|
"would you like",
|
|
3883
5307
|
"should i",
|
|
@@ -3903,12 +5327,15 @@ function cancelPendingTimer(state) {
|
|
|
3903
5327
|
clearTimeout(state.pendingTimer);
|
|
3904
5328
|
state.pendingTimer = null;
|
|
3905
5329
|
}
|
|
5330
|
+
state.pendingTimerSessionId = null;
|
|
3906
5331
|
}
|
|
3907
5332
|
function resetState(state) {
|
|
3908
5333
|
cancelPendingTimer(state);
|
|
3909
5334
|
state.consecutiveContinuations = 0;
|
|
3910
5335
|
state.suppressUntil = 0;
|
|
3911
5336
|
state.isAutoInjecting = false;
|
|
5337
|
+
state.notifyingSessionIds.clear();
|
|
5338
|
+
state.notificationBusyUntilBySession.clear();
|
|
3912
5339
|
}
|
|
3913
5340
|
function createTodoContinuationHook(ctx, config) {
|
|
3914
5341
|
const maxContinuations = config?.maxContinuations ?? 5;
|
|
@@ -3919,10 +5346,51 @@ function createTodoContinuationHook(ctx, config) {
|
|
|
3919
5346
|
enabled: false,
|
|
3920
5347
|
consecutiveContinuations: 0,
|
|
3921
5348
|
pendingTimer: null,
|
|
5349
|
+
pendingTimerSessionId: null,
|
|
3922
5350
|
suppressUntil: 0,
|
|
3923
|
-
|
|
3924
|
-
|
|
5351
|
+
orchestratorSessionIds: new Set,
|
|
5352
|
+
sawChatMessage: false,
|
|
5353
|
+
isAutoInjecting: false,
|
|
5354
|
+
notifyingSessionIds: new Set,
|
|
5355
|
+
notificationBusyUntilBySession: new Map
|
|
3925
5356
|
};
|
|
5357
|
+
function markNotificationStarted(sessionID) {
|
|
5358
|
+
state.notifyingSessionIds.add(sessionID);
|
|
5359
|
+
}
|
|
5360
|
+
function markNotificationFinished(sessionID) {
|
|
5361
|
+
state.notifyingSessionIds.delete(sessionID);
|
|
5362
|
+
state.notificationBusyUntilBySession.set(sessionID, Date.now() + NOTIFICATION_BUSY_GRACE_MS);
|
|
5363
|
+
}
|
|
5364
|
+
function clearNotificationState(sessionID) {
|
|
5365
|
+
state.notifyingSessionIds.delete(sessionID);
|
|
5366
|
+
state.notificationBusyUntilBySession.delete(sessionID);
|
|
5367
|
+
}
|
|
5368
|
+
function isNotificationBusy(sessionID) {
|
|
5369
|
+
if (state.notifyingSessionIds.has(sessionID)) {
|
|
5370
|
+
return true;
|
|
5371
|
+
}
|
|
5372
|
+
const until = state.notificationBusyUntilBySession.get(sessionID) ?? 0;
|
|
5373
|
+
if (until <= Date.now()) {
|
|
5374
|
+
state.notificationBusyUntilBySession.delete(sessionID);
|
|
5375
|
+
return false;
|
|
5376
|
+
}
|
|
5377
|
+
return true;
|
|
5378
|
+
}
|
|
5379
|
+
function isOrchestratorSession(sessionID) {
|
|
5380
|
+
return state.orchestratorSessionIds.has(sessionID);
|
|
5381
|
+
}
|
|
5382
|
+
function registerOrchestratorSession(sessionID) {
|
|
5383
|
+
state.orchestratorSessionIds.add(sessionID);
|
|
5384
|
+
}
|
|
5385
|
+
function handleChatMessage(input) {
|
|
5386
|
+
if (!input.agent) {
|
|
5387
|
+
return;
|
|
5388
|
+
}
|
|
5389
|
+
state.sawChatMessage = true;
|
|
5390
|
+
if (input.agent === "orchestrator") {
|
|
5391
|
+
registerOrchestratorSession(input.sessionID);
|
|
5392
|
+
}
|
|
5393
|
+
}
|
|
3926
5394
|
const autoContinue = tool({
|
|
3927
5395
|
description: "Toggle auto-continuation for incomplete todos. When enabled, the orchestrator will automatically continue working through its todo list when it stops with incomplete items.",
|
|
3928
5396
|
args: { enabled: tool.schema.boolean() },
|
|
@@ -3943,19 +5411,19 @@ function createTodoContinuationHook(ctx, config) {
|
|
|
3943
5411
|
async function handleEvent(input) {
|
|
3944
5412
|
const { event } = input;
|
|
3945
5413
|
const properties = event.properties ?? {};
|
|
3946
|
-
if (event.type === "session.idle") {
|
|
5414
|
+
if (event.type === "session.idle" || event.type === "session.status" && properties.status?.type === "idle") {
|
|
3947
5415
|
const sessionID = properties.sessionID;
|
|
3948
5416
|
if (!sessionID) {
|
|
3949
5417
|
return;
|
|
3950
5418
|
}
|
|
3951
5419
|
log(`[${HOOK_NAME}] Session idle`, { sessionID });
|
|
3952
|
-
if (!state.
|
|
3953
|
-
|
|
5420
|
+
if (!state.sawChatMessage && state.orchestratorSessionIds.size === 0) {
|
|
5421
|
+
registerOrchestratorSession(sessionID);
|
|
3954
5422
|
log(`[${HOOK_NAME}] Tracked orchestrator session`, {
|
|
3955
5423
|
sessionID
|
|
3956
5424
|
});
|
|
3957
5425
|
}
|
|
3958
|
-
if (
|
|
5426
|
+
if (!isOrchestratorSession(sessionID)) {
|
|
3959
5427
|
log(`[${HOOK_NAME}] Skipped: not orchestrator session`, {
|
|
3960
5428
|
sessionID
|
|
3961
5429
|
});
|
|
@@ -4068,6 +5536,7 @@ function createTodoContinuationHook(ctx, config) {
|
|
|
4068
5536
|
sessionID,
|
|
4069
5537
|
delayMs: cooldownMs
|
|
4070
5538
|
});
|
|
5539
|
+
markNotificationStarted(sessionID);
|
|
4071
5540
|
ctx.client.session.prompt({
|
|
4072
5541
|
path: { id: sessionID },
|
|
4073
5542
|
body: {
|
|
@@ -4084,9 +5553,14 @@ function createTodoContinuationHook(ctx, config) {
|
|
|
4084
5553
|
}
|
|
4085
5554
|
]
|
|
4086
5555
|
}
|
|
4087
|
-
}).catch(() => {})
|
|
5556
|
+
}).catch(() => {}).finally(() => {
|
|
5557
|
+
markNotificationFinished(sessionID);
|
|
5558
|
+
});
|
|
5559
|
+
state.pendingTimerSessionId = sessionID;
|
|
4088
5560
|
state.pendingTimer = setTimeout(async () => {
|
|
4089
5561
|
state.pendingTimer = null;
|
|
5562
|
+
state.pendingTimerSessionId = null;
|
|
5563
|
+
clearNotificationState(sessionID);
|
|
4090
5564
|
if (!state.enabled) {
|
|
4091
5565
|
log(`[${HOOK_NAME}] Cancelled: disabled during cooldown`, {
|
|
4092
5566
|
sessionID
|
|
@@ -4119,11 +5593,12 @@ function createTodoContinuationHook(ctx, config) {
|
|
|
4119
5593
|
const status = properties.status;
|
|
4120
5594
|
const sessionID = properties.sessionID;
|
|
4121
5595
|
if (status?.type === "busy") {
|
|
4122
|
-
const isOrchestrator = sessionID
|
|
4123
|
-
|
|
5596
|
+
const isOrchestrator = isOrchestratorSession(sessionID);
|
|
5597
|
+
const isNotification = isNotificationBusy(sessionID);
|
|
5598
|
+
if (isOrchestrator && !isNotification && state.pendingTimerSessionId === sessionID) {
|
|
4124
5599
|
cancelPendingTimer(state);
|
|
4125
5600
|
}
|
|
4126
|
-
if (!state.isAutoInjecting && isOrchestrator && state.consecutiveContinuations > 0) {
|
|
5601
|
+
if (!state.isAutoInjecting && !isNotification && isOrchestrator && state.consecutiveContinuations > 0) {
|
|
4127
5602
|
state.consecutiveContinuations = 0;
|
|
4128
5603
|
log(`[${HOOK_NAME}] Reset consecutive count on user activity`, {
|
|
4129
5604
|
sessionID
|
|
@@ -4134,7 +5609,7 @@ function createTodoContinuationHook(ctx, config) {
|
|
|
4134
5609
|
const error = properties.error;
|
|
4135
5610
|
const sessionID = properties.sessionID;
|
|
4136
5611
|
const errorName = error?.name;
|
|
4137
|
-
const isOrchestrator = sessionID
|
|
5612
|
+
const isOrchestrator = isOrchestratorSession(sessionID);
|
|
4138
5613
|
if (isOrchestrator && (errorName === "MessageAbortedError" || errorName === "AbortError")) {
|
|
4139
5614
|
state.suppressUntil = Date.now() + SUPPRESS_AFTER_ABORT_MS;
|
|
4140
5615
|
log(`[${HOOK_NAME}] Suppressed continuation after abort`, {
|
|
@@ -4150,13 +5625,19 @@ function createTodoContinuationHook(ctx, config) {
|
|
|
4150
5625
|
}
|
|
4151
5626
|
} else if (event.type === "session.deleted") {
|
|
4152
5627
|
const deletedSessionId = properties.info?.id ?? properties.sessionID;
|
|
4153
|
-
if (
|
|
4154
|
-
|
|
4155
|
-
|
|
4156
|
-
|
|
4157
|
-
|
|
4158
|
-
|
|
4159
|
-
|
|
5628
|
+
if (deletedSessionId && isOrchestratorSession(deletedSessionId)) {
|
|
5629
|
+
if (state.pendingTimerSessionId === deletedSessionId) {
|
|
5630
|
+
cancelPendingTimer(state);
|
|
5631
|
+
log(`[${HOOK_NAME}] Cancelled pending timer on orchestrator delete`, {
|
|
5632
|
+
sessionID: deletedSessionId
|
|
5633
|
+
});
|
|
5634
|
+
}
|
|
5635
|
+
state.orchestratorSessionIds.delete(deletedSessionId);
|
|
5636
|
+
clearNotificationState(deletedSessionId);
|
|
5637
|
+
if (state.orchestratorSessionIds.size === 0) {
|
|
5638
|
+
resetState(state);
|
|
5639
|
+
state.sawChatMessage = false;
|
|
5640
|
+
}
|
|
4160
5641
|
log(`[${HOOK_NAME}] Reset orchestrator session on delete`, {
|
|
4161
5642
|
sessionID: deletedSessionId
|
|
4162
5643
|
});
|
|
@@ -4167,9 +5648,7 @@ function createTodoContinuationHook(ctx, config) {
|
|
|
4167
5648
|
if (input.command !== COMMAND_NAME) {
|
|
4168
5649
|
return;
|
|
4169
5650
|
}
|
|
4170
|
-
|
|
4171
|
-
state.orchestratorSessionId = input.sessionID;
|
|
4172
|
-
}
|
|
5651
|
+
registerOrchestratorSession(input.sessionID);
|
|
4173
5652
|
output.parts.length = 0;
|
|
4174
5653
|
const arg = input.arguments.trim().toLowerCase();
|
|
4175
5654
|
let newEnabled;
|
|
@@ -4214,6 +5693,7 @@ function createTodoContinuationHook(ctx, config) {
|
|
|
4214
5693
|
return {
|
|
4215
5694
|
tool: { auto_continue: autoContinue },
|
|
4216
5695
|
handleEvent,
|
|
5696
|
+
handleChatMessage,
|
|
4217
5697
|
handleCommandExecuteBefore
|
|
4218
5698
|
};
|
|
4219
5699
|
}
|
|
@@ -5077,8 +6557,8 @@ function createInterviewServer(deps) {
|
|
|
5077
6557
|
// src/interview/service.ts
|
|
5078
6558
|
import { spawn as spawn4 } from "child_process";
|
|
5079
6559
|
import * as fsSync from "fs";
|
|
5080
|
-
import * as
|
|
5081
|
-
import * as
|
|
6560
|
+
import * as fs6 from "fs/promises";
|
|
6561
|
+
import * as path8 from "path";
|
|
5082
6562
|
|
|
5083
6563
|
// src/interview/parser.ts
|
|
5084
6564
|
var INTERVIEW_BLOCK_REGEX = /<interview_state>\s*([\s\S]*?)\s*<\/interview_state>/i;
|
|
@@ -5271,14 +6751,14 @@ function normalizeOutputFolder(outputFolder) {
|
|
|
5271
6751
|
return normalized || DEFAULT_OUTPUT_FOLDER;
|
|
5272
6752
|
}
|
|
5273
6753
|
function createInterviewDirectoryPath(directory, outputFolder) {
|
|
5274
|
-
return
|
|
6754
|
+
return path8.join(directory, normalizeOutputFolder(outputFolder));
|
|
5275
6755
|
}
|
|
5276
6756
|
function createInterviewFilePath(directory, outputFolder, idea) {
|
|
5277
6757
|
const fileName = `${slugify(idea) || "interview"}.md`;
|
|
5278
|
-
return
|
|
6758
|
+
return path8.join(createInterviewDirectoryPath(directory, outputFolder), fileName);
|
|
5279
6759
|
}
|
|
5280
6760
|
function relativeInterviewPath(directory, filePath) {
|
|
5281
|
-
return
|
|
6761
|
+
return path8.relative(directory, filePath) || path8.basename(filePath);
|
|
5282
6762
|
}
|
|
5283
6763
|
function extractHistorySection(document) {
|
|
5284
6764
|
const marker = `## Q&A history
|
|
@@ -5324,31 +6804,31 @@ function buildInterviewDocument(idea, summary, history) {
|
|
|
5324
6804
|
`);
|
|
5325
6805
|
}
|
|
5326
6806
|
async function ensureInterviewFile(record) {
|
|
5327
|
-
await
|
|
6807
|
+
await fs6.mkdir(path8.dirname(record.markdownPath), { recursive: true });
|
|
5328
6808
|
try {
|
|
5329
|
-
await
|
|
6809
|
+
await fs6.access(record.markdownPath);
|
|
5330
6810
|
} catch {
|
|
5331
|
-
await
|
|
6811
|
+
await fs6.writeFile(record.markdownPath, buildInterviewDocument(record.idea, "", ""), "utf8");
|
|
5332
6812
|
}
|
|
5333
6813
|
}
|
|
5334
6814
|
async function readInterviewDocument(record) {
|
|
5335
6815
|
try {
|
|
5336
|
-
return await
|
|
6816
|
+
return await fs6.readFile(record.markdownPath, "utf8");
|
|
5337
6817
|
} catch (error) {
|
|
5338
6818
|
if (error.code === "ENOENT") {
|
|
5339
6819
|
try {
|
|
5340
|
-
return await
|
|
6820
|
+
return await fs6.readFile(record.markdownPath, "utf8");
|
|
5341
6821
|
} catch {}
|
|
5342
6822
|
}
|
|
5343
6823
|
}
|
|
5344
6824
|
await ensureInterviewFile(record);
|
|
5345
|
-
return
|
|
6825
|
+
return fs6.readFile(record.markdownPath, "utf8");
|
|
5346
6826
|
}
|
|
5347
6827
|
async function rewriteInterviewDocument(record, summary) {
|
|
5348
6828
|
const existing = await readInterviewDocument(record);
|
|
5349
6829
|
const history = extractHistorySection(existing);
|
|
5350
6830
|
const next = buildInterviewDocument(record.idea, summary, history);
|
|
5351
|
-
await
|
|
6831
|
+
await fs6.writeFile(record.markdownPath, next, "utf8");
|
|
5352
6832
|
return next;
|
|
5353
6833
|
}
|
|
5354
6834
|
async function appendInterviewAnswers(record, questions, answers) {
|
|
@@ -5366,7 +6846,7 @@ A: ${answer.answer.trim()}` : null;
|
|
|
5366
6846
|
const nextHistory = [history === "No answers yet." ? "" : history, appended].filter(Boolean).join(`
|
|
5367
6847
|
|
|
5368
6848
|
`);
|
|
5369
|
-
await
|
|
6849
|
+
await fs6.writeFile(record.markdownPath, buildInterviewDocument(record.idea, summary, nextHistory), "utf8");
|
|
5370
6850
|
}
|
|
5371
6851
|
function resolveExistingInterviewPath(directory, outputFolder, value) {
|
|
5372
6852
|
const trimmed = value.trim();
|
|
@@ -5375,22 +6855,22 @@ function resolveExistingInterviewPath(directory, outputFolder, value) {
|
|
|
5375
6855
|
}
|
|
5376
6856
|
const outputDir = createInterviewDirectoryPath(directory, outputFolder);
|
|
5377
6857
|
const candidates = new Set;
|
|
5378
|
-
const resolvedRoot =
|
|
5379
|
-
if (
|
|
6858
|
+
const resolvedRoot = path8.resolve(directory);
|
|
6859
|
+
if (path8.isAbsolute(trimmed)) {
|
|
5380
6860
|
candidates.add(trimmed);
|
|
5381
6861
|
} else {
|
|
5382
|
-
candidates.add(
|
|
5383
|
-
candidates.add(
|
|
6862
|
+
candidates.add(path8.resolve(directory, trimmed));
|
|
6863
|
+
candidates.add(path8.join(outputDir, trimmed));
|
|
5384
6864
|
if (!trimmed.endsWith(".md")) {
|
|
5385
|
-
candidates.add(
|
|
6865
|
+
candidates.add(path8.join(outputDir, `${trimmed}.md`));
|
|
5386
6866
|
}
|
|
5387
6867
|
}
|
|
5388
6868
|
for (const candidate of candidates) {
|
|
5389
|
-
if (
|
|
6869
|
+
if (path8.extname(candidate) !== ".md") {
|
|
5390
6870
|
continue;
|
|
5391
6871
|
}
|
|
5392
|
-
const resolved =
|
|
5393
|
-
if (!resolved.startsWith(resolvedRoot +
|
|
6872
|
+
const resolved = path8.resolve(candidate);
|
|
6873
|
+
if (!resolved.startsWith(resolvedRoot + path8.sep) && resolved !== resolvedRoot) {
|
|
5394
6874
|
continue;
|
|
5395
6875
|
}
|
|
5396
6876
|
if (fsSync.existsSync(candidate)) {
|
|
@@ -5437,18 +6917,18 @@ function createInterviewService(ctx, config, deps) {
|
|
|
5437
6917
|
if (!newSlug) {
|
|
5438
6918
|
return;
|
|
5439
6919
|
}
|
|
5440
|
-
const currentFileName =
|
|
6920
|
+
const currentFileName = path8.basename(interview.markdownPath, ".md");
|
|
5441
6921
|
if (currentFileName === newSlug) {
|
|
5442
6922
|
return;
|
|
5443
6923
|
}
|
|
5444
|
-
const dir =
|
|
5445
|
-
const newPath =
|
|
6924
|
+
const dir = path8.dirname(interview.markdownPath);
|
|
6925
|
+
const newPath = path8.join(dir, `${newSlug}.md`);
|
|
5446
6926
|
try {
|
|
5447
|
-
await
|
|
6927
|
+
await fs6.access(newPath);
|
|
5448
6928
|
return;
|
|
5449
6929
|
} catch {}
|
|
5450
6930
|
try {
|
|
5451
|
-
await
|
|
6931
|
+
await fs6.rename(interview.markdownPath, newPath);
|
|
5452
6932
|
interview.markdownPath = newPath;
|
|
5453
6933
|
log("[interview] renamed file with assistant title:", {
|
|
5454
6934
|
from: currentFileName,
|
|
@@ -5510,13 +6990,13 @@ function createInterviewService(ctx, config, deps) {
|
|
|
5510
6990
|
active.status = "abandoned";
|
|
5511
6991
|
}
|
|
5512
6992
|
}
|
|
5513
|
-
const document = await
|
|
6993
|
+
const document = await fs6.readFile(markdownPath, "utf8");
|
|
5514
6994
|
const messages = await loadMessages(sessionID);
|
|
5515
6995
|
const title = extractTitle(document);
|
|
5516
6996
|
const record = {
|
|
5517
|
-
id: `${Date.now()}-${++idCounter}-${slugify(
|
|
6997
|
+
id: `${Date.now()}-${++idCounter}-${slugify(path8.basename(markdownPath, ".md")) || "interview"}`,
|
|
5518
6998
|
sessionID,
|
|
5519
|
-
idea: title ||
|
|
6999
|
+
idea: title || path8.basename(markdownPath, ".md"),
|
|
5520
7000
|
markdownPath,
|
|
5521
7001
|
createdAt: nowIso(),
|
|
5522
7002
|
status: "active",
|
|
@@ -5658,7 +7138,7 @@ function createInterviewService(ctx, config, deps) {
|
|
|
5658
7138
|
const resumePath = resolveExistingInterviewPath(ctx.directory, outputFolder, idea);
|
|
5659
7139
|
if (resumePath) {
|
|
5660
7140
|
const interview2 = await resumeInterview(input.sessionID, resumePath);
|
|
5661
|
-
const document = await
|
|
7141
|
+
const document = await fs6.readFile(interview2.markdownPath, "utf8");
|
|
5662
7142
|
await notifyInterviewUrl(input.sessionID, interview2);
|
|
5663
7143
|
output.parts.push(createInternalAgentTextPart(buildResumePrompt(document, maxQuestions)));
|
|
5664
7144
|
return;
|
|
@@ -5969,9 +7449,9 @@ function findSgCliPathSync() {
|
|
|
5969
7449
|
}
|
|
5970
7450
|
if (process.platform === "darwin") {
|
|
5971
7451
|
const homebrewPaths = ["/opt/homebrew/bin/sg", "/usr/local/bin/sg"];
|
|
5972
|
-
for (const
|
|
5973
|
-
if (existsSync6(
|
|
5974
|
-
return
|
|
7452
|
+
for (const path9 of homebrewPaths) {
|
|
7453
|
+
if (existsSync6(path9) && isValidBinary(path9)) {
|
|
7454
|
+
return path9;
|
|
5975
7455
|
}
|
|
5976
7456
|
}
|
|
5977
7457
|
}
|
|
@@ -5988,8 +7468,8 @@ function getSgCliPath() {
|
|
|
5988
7468
|
}
|
|
5989
7469
|
return "sg";
|
|
5990
7470
|
}
|
|
5991
|
-
function setSgCliPath(
|
|
5992
|
-
resolvedCliPath =
|
|
7471
|
+
function setSgCliPath(path9) {
|
|
7472
|
+
resolvedCliPath = path9;
|
|
5993
7473
|
}
|
|
5994
7474
|
var DEFAULT_TIMEOUT_MS2 = 300000;
|
|
5995
7475
|
var DEFAULT_MAX_OUTPUT_BYTES = 1 * 1024 * 1024;
|
|
@@ -8232,14 +9712,14 @@ var BINARY_PREFIXES = [
|
|
|
8232
9712
|
var WEBFETCH_DESCRIPTION = "Fetch a URL with better extraction for static/docs pages. Supports llms.txt probing, content-focused HTML extraction, metadata, redirects, and an optional prompt processed by a cheap secondary model.";
|
|
8233
9713
|
// src/tools/smartfetch/tool.ts
|
|
8234
9714
|
import os3 from "os";
|
|
8235
|
-
import
|
|
9715
|
+
import path12 from "path";
|
|
8236
9716
|
import {
|
|
8237
9717
|
tool as tool6
|
|
8238
9718
|
} from "@opencode-ai/plugin";
|
|
8239
9719
|
|
|
8240
9720
|
// src/tools/smartfetch/binary.ts
|
|
8241
9721
|
import { mkdir as mkdir2, writeFile as writeFile2 } from "fs/promises";
|
|
8242
|
-
import
|
|
9722
|
+
import path9 from "path";
|
|
8243
9723
|
function extensionForMime(contentType) {
|
|
8244
9724
|
const mime = contentType.split(";")[0]?.trim().toLowerCase();
|
|
8245
9725
|
const map = {
|
|
@@ -8260,10 +9740,10 @@ function buildBinaryResultMessage(fetchResult, savedPath) {
|
|
|
8260
9740
|
async function saveBinary(binaryDir, data, contentType, filename) {
|
|
8261
9741
|
await mkdir2(binaryDir, { recursive: true });
|
|
8262
9742
|
const initialName = filename || `webfetch-${Date.now()}.${extensionForMime(contentType)}`;
|
|
8263
|
-
const parsed =
|
|
9743
|
+
const parsed = path9.parse(initialName);
|
|
8264
9744
|
for (let attempt = 0;attempt < 1000; attempt++) {
|
|
8265
9745
|
const candidateName = attempt === 0 ? initialName : `${parsed.name}-${attempt}${parsed.ext || `.${extensionForMime(contentType)}`}`;
|
|
8266
|
-
const file =
|
|
9746
|
+
const file = path9.join(binaryDir, candidateName);
|
|
8267
9747
|
try {
|
|
8268
9748
|
await writeFile2(file, data, { flag: "wx" });
|
|
8269
9749
|
return file;
|
|
@@ -8281,7 +9761,7 @@ async function saveBinary(binaryDir, data, contentType, filename) {
|
|
|
8281
9761
|
import { LRUCache } from "lru-cache";
|
|
8282
9762
|
|
|
8283
9763
|
// src/tools/smartfetch/network.ts
|
|
8284
|
-
import
|
|
9764
|
+
import path10 from "path";
|
|
8285
9765
|
|
|
8286
9766
|
// src/tools/smartfetch/utils.ts
|
|
8287
9767
|
import { Readability } from "@mozilla/readability";
|
|
@@ -8665,7 +10145,7 @@ function normalizeUrl(input) {
|
|
|
8665
10145
|
}
|
|
8666
10146
|
function isDocsLikeUrl(url) {
|
|
8667
10147
|
const host = url.hostname.toLowerCase();
|
|
8668
|
-
return DOCS_HOST_SUFFIXES.some((
|
|
10148
|
+
return DOCS_HOST_SUFFIXES.some((suffix2) => host.endsWith(suffix2)) || DOCS_HOST_PREFIXES.some((prefix2) => host.startsWith(prefix2));
|
|
8669
10149
|
}
|
|
8670
10150
|
function buildPermissionPatterns(normalized, shouldProbeLlmsTxt) {
|
|
8671
10151
|
const patterns = new Set([normalized.url]);
|
|
@@ -8721,7 +10201,7 @@ function isPermittedRedirect(from, to, allowedOrigins) {
|
|
|
8721
10201
|
}
|
|
8722
10202
|
function isBinaryContentType(contentType) {
|
|
8723
10203
|
const mime = contentType.split(";")[0]?.trim().toLowerCase() || "";
|
|
8724
|
-
return BINARY_PREFIXES.some((
|
|
10204
|
+
return BINARY_PREFIXES.some((prefix2) => mime.startsWith(prefix2));
|
|
8725
10205
|
}
|
|
8726
10206
|
function getBinaryKind(contentType) {
|
|
8727
10207
|
const mime = contentType.split(";")[0]?.trim().toLowerCase() || "";
|
|
@@ -9000,7 +10480,7 @@ function inferFilenameFromUrl(url) {
|
|
|
9000
10480
|
function truncateFilename(name, maxLength = 180) {
|
|
9001
10481
|
if (name.length <= maxLength)
|
|
9002
10482
|
return name;
|
|
9003
|
-
const parsed =
|
|
10483
|
+
const parsed = path10.parse(name);
|
|
9004
10484
|
const ext = parsed.ext || "";
|
|
9005
10485
|
const baseLimit = Math.max(1, maxLength - ext.length);
|
|
9006
10486
|
return `${parsed.name.slice(0, baseLimit)}${ext}`;
|
|
@@ -9171,8 +10651,8 @@ function isInvalidLlmsResult(fetchResult) {
|
|
|
9171
10651
|
|
|
9172
10652
|
// src/tools/smartfetch/secondary-model.ts
|
|
9173
10653
|
import { existsSync as existsSync11 } from "fs";
|
|
9174
|
-
import { readFile as
|
|
9175
|
-
import
|
|
10654
|
+
import { readFile as readFile3 } from "fs/promises";
|
|
10655
|
+
import path11 from "path";
|
|
9176
10656
|
function parseModelRef(value) {
|
|
9177
10657
|
if (!value)
|
|
9178
10658
|
return;
|
|
@@ -9198,7 +10678,7 @@ function pickAgentModelRef(value) {
|
|
|
9198
10678
|
}
|
|
9199
10679
|
function findPreferredOpenCodeConfigPath(baseDir) {
|
|
9200
10680
|
for (const file of ["opencode.jsonc", "opencode.json"]) {
|
|
9201
|
-
const fullPath =
|
|
10681
|
+
const fullPath = path11.join(baseDir, file);
|
|
9202
10682
|
if (existsSync11(fullPath))
|
|
9203
10683
|
return fullPath;
|
|
9204
10684
|
}
|
|
@@ -9208,14 +10688,14 @@ async function readOpenCodeConfigFile(configPath) {
|
|
|
9208
10688
|
if (!configPath)
|
|
9209
10689
|
return;
|
|
9210
10690
|
try {
|
|
9211
|
-
const content = await
|
|
10691
|
+
const content = await readFile3(configPath, "utf8");
|
|
9212
10692
|
return JSON.parse(stripJsonComments(content));
|
|
9213
10693
|
} catch {
|
|
9214
10694
|
return;
|
|
9215
10695
|
}
|
|
9216
10696
|
}
|
|
9217
10697
|
async function readEffectiveOpenCodeConfig(directory) {
|
|
9218
|
-
const projectDir =
|
|
10698
|
+
const projectDir = path11.join(directory, ".opencode");
|
|
9219
10699
|
const userDirs = getConfigSearchDirs();
|
|
9220
10700
|
const projectPath = findPreferredOpenCodeConfigPath(projectDir);
|
|
9221
10701
|
const userPath = userDirs.map((configDir) => findPreferredOpenCodeConfigPath(configDir)).find(Boolean);
|
|
@@ -9376,7 +10856,7 @@ async function runSecondaryModelWithFallback(client, directory, models, prompt,
|
|
|
9376
10856
|
// src/tools/smartfetch/tool.ts
|
|
9377
10857
|
var z5 = tool6.schema;
|
|
9378
10858
|
function createWebfetchTool(pluginCtx, options = {}) {
|
|
9379
|
-
const binaryDir = options.binaryDir ||
|
|
10859
|
+
const binaryDir = options.binaryDir || path12.join(os3.tmpdir(), "opencode-smartfetch");
|
|
9380
10860
|
return tool6({
|
|
9381
10861
|
description: WEBFETCH_DESCRIPTION,
|
|
9382
10862
|
args: {
|
|
@@ -9917,12 +11397,15 @@ var OhMyOpenCodeLite = async (ctx) => {
|
|
|
9917
11397
|
});
|
|
9918
11398
|
const phaseReminderHook = createPhaseReminderHook();
|
|
9919
11399
|
const filterAvailableSkillsHook = createFilterAvailableSkillsHook(ctx, config);
|
|
9920
|
-
const
|
|
11400
|
+
const sessionAgentMap = new Map;
|
|
11401
|
+
const postFileToolNudgeHook = createPostFileToolNudgeHook({
|
|
11402
|
+
shouldInject: (sessionID) => sessionAgentMap.get(sessionID) === "orchestrator"
|
|
11403
|
+
});
|
|
9921
11404
|
const chatHeadersHook = createChatHeadersHook(ctx);
|
|
9922
11405
|
const delegateTaskRetryHook = createDelegateTaskRetryHook(ctx);
|
|
11406
|
+
const applyPatchHook = createApplyPatchHook(ctx);
|
|
9923
11407
|
const jsonErrorRecoveryHook = createJsonErrorRecoveryHook(ctx);
|
|
9924
11408
|
const foregroundFallback = new ForegroundFallbackManager(ctx.client, runtimeChains, config.fallback?.enabled !== false && Object.keys(runtimeChains).length > 0);
|
|
9925
|
-
const sessionAgentMap = new Map;
|
|
9926
11409
|
const todoContinuationHook = createTodoContinuationHook(ctx, {
|
|
9927
11410
|
maxContinuations: config.todoContinuation?.maxContinuations ?? 5,
|
|
9928
11411
|
cooldownMs: config.todoContinuation?.cooldownMs ?? 3000,
|
|
@@ -10061,16 +11544,25 @@ var OhMyOpenCodeLite = async (ctx) => {
|
|
|
10061
11544
|
await backgroundManager.handleSessionDeleted(input.event);
|
|
10062
11545
|
await multiplexerSessionManager.onSessionDeleted(input.event);
|
|
10063
11546
|
await interviewManager.handleEvent(input);
|
|
11547
|
+
await postFileToolNudgeHook.event(input);
|
|
11548
|
+
},
|
|
11549
|
+
"tool.execute.before": async (input, output) => {
|
|
11550
|
+
await applyPatchHook["tool.execute.before"](input, output);
|
|
10064
11551
|
},
|
|
10065
11552
|
"command.execute.before": async (input, output) => {
|
|
10066
11553
|
await todoContinuationHook.handleCommandExecuteBefore(input, output);
|
|
10067
11554
|
await interviewManager.handleCommandExecuteBefore(input, output);
|
|
10068
11555
|
},
|
|
10069
11556
|
"chat.headers": chatHeadersHook["chat.headers"],
|
|
10070
|
-
"chat.message": async (input) => {
|
|
10071
|
-
|
|
10072
|
-
|
|
11557
|
+
"chat.message": async (input, output) => {
|
|
11558
|
+
const agent = input.agent ?? output?.message?.agent;
|
|
11559
|
+
if (agent) {
|
|
11560
|
+
sessionAgentMap.set(input.sessionID, agent);
|
|
10073
11561
|
}
|
|
11562
|
+
todoContinuationHook.handleChatMessage({
|
|
11563
|
+
sessionID: input.sessionID,
|
|
11564
|
+
agent
|
|
11565
|
+
});
|
|
10074
11566
|
},
|
|
10075
11567
|
"experimental.chat.system.transform": async (input, output) => {
|
|
10076
11568
|
const agentName = input.sessionID ? sessionAgentMap.get(input.sessionID) : undefined;
|
|
@@ -10080,9 +11572,10 @@ var OhMyOpenCodeLite = async (ctx) => {
|
|
|
10080
11572
|
const { ORCHESTRATOR_PROMPT: ORCHESTRATOR_PROMPT2 } = await Promise.resolve().then(() => exports_orchestrator);
|
|
10081
11573
|
output.system[0] = ORCHESTRATOR_PROMPT2 + (output.system[0] ? `
|
|
10082
11574
|
|
|
10083
|
-
|
|
11575
|
+
${output.system[0]}` : "");
|
|
10084
11576
|
}
|
|
10085
11577
|
}
|
|
11578
|
+
await postFileToolNudgeHook["experimental.chat.system.transform"](input, output);
|
|
10086
11579
|
},
|
|
10087
11580
|
"experimental.chat.messages.transform": async (input, output) => {
|
|
10088
11581
|
const typedOutput = output;
|