oh-my-opencode-slim 0.9.9 → 0.9.11
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/cli/index.js +2 -1
- package/dist/config/schema.d.ts +4 -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 +30 -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 +1687 -121
- package/oh-my-opencode-slim.schema.json +14 -0
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -502,7 +502,8 @@ var AgentOverrideConfigSchema = z2.object({
|
|
|
502
502
|
temperature: z2.number().min(0).max(2).optional(),
|
|
503
503
|
variant: z2.string().optional().catch(undefined),
|
|
504
504
|
skills: z2.array(z2.string()).optional(),
|
|
505
|
-
mcps: z2.array(z2.string()).optional()
|
|
505
|
+
mcps: z2.array(z2.string()).optional(),
|
|
506
|
+
options: z2.record(z2.string(), z2.unknown()).optional()
|
|
506
507
|
});
|
|
507
508
|
var MultiplexerTypeSchema = z2.enum(["auto", "tmux", "zellij", "none"]);
|
|
508
509
|
var MultiplexerLayoutSchema = z2.enum([
|
|
@@ -1247,6 +1248,12 @@ function applyOverrides(agent, override) {
|
|
|
1247
1248
|
agent.config.variant = override.variant;
|
|
1248
1249
|
if (override.temperature !== undefined)
|
|
1249
1250
|
agent.config.temperature = override.temperature;
|
|
1251
|
+
if (override.options) {
|
|
1252
|
+
agent.config.options = {
|
|
1253
|
+
...agent.config.options,
|
|
1254
|
+
...override.options
|
|
1255
|
+
};
|
|
1256
|
+
}
|
|
1250
1257
|
}
|
|
1251
1258
|
function applyDefaultPermissions(agent, configuredSkills) {
|
|
1252
1259
|
const existing = agent.config.permission ?? {};
|
|
@@ -2956,34 +2963,1505 @@ ${bestResult.result}` : undefined,
|
|
|
2956
2963
|
};
|
|
2957
2964
|
}
|
|
2958
2965
|
}
|
|
2966
|
+
// src/hooks/apply-patch/errors.ts
|
|
2967
|
+
var APPLY_PATCH_ERROR_PREFIX = {
|
|
2968
|
+
blocked: "apply_patch blocked",
|
|
2969
|
+
validation: "apply_patch validation failed",
|
|
2970
|
+
verification: "apply_patch verification failed",
|
|
2971
|
+
internal: "apply_patch internal error"
|
|
2972
|
+
};
|
|
2973
|
+
|
|
2974
|
+
class ApplyPatchError extends Error {
|
|
2975
|
+
kind;
|
|
2976
|
+
code;
|
|
2977
|
+
cause;
|
|
2978
|
+
constructor(kind, code, message, options) {
|
|
2979
|
+
super(`${APPLY_PATCH_ERROR_PREFIX[kind]}: ${message}`);
|
|
2980
|
+
this.kind = kind;
|
|
2981
|
+
this.code = code;
|
|
2982
|
+
this.name = "ApplyPatchError";
|
|
2983
|
+
this.cause = options?.cause;
|
|
2984
|
+
}
|
|
2985
|
+
}
|
|
2986
|
+
function getErrorMessage(error) {
|
|
2987
|
+
return error instanceof Error ? error.message : String(error);
|
|
2988
|
+
}
|
|
2989
|
+
function createApplyPatchBlockedError(message, cause) {
|
|
2990
|
+
return new ApplyPatchError("blocked", "outside_workspace", message, {
|
|
2991
|
+
cause
|
|
2992
|
+
});
|
|
2993
|
+
}
|
|
2994
|
+
function createApplyPatchValidationError(message, cause) {
|
|
2995
|
+
return new ApplyPatchError("validation", "malformed_patch", message, {
|
|
2996
|
+
cause
|
|
2997
|
+
});
|
|
2998
|
+
}
|
|
2999
|
+
function createApplyPatchVerificationError(message, cause) {
|
|
3000
|
+
return new ApplyPatchError("verification", "verification_failed", message, {
|
|
3001
|
+
cause
|
|
3002
|
+
});
|
|
3003
|
+
}
|
|
3004
|
+
function createApplyPatchInternalError(message, cause) {
|
|
3005
|
+
return new ApplyPatchError("internal", "internal_unexpected", message, {
|
|
3006
|
+
cause
|
|
3007
|
+
});
|
|
3008
|
+
}
|
|
3009
|
+
function isApplyPatchError(error) {
|
|
3010
|
+
return error instanceof ApplyPatchError;
|
|
3011
|
+
}
|
|
3012
|
+
function isApplyPatchVerificationError(error) {
|
|
3013
|
+
return isApplyPatchError(error) && error.kind === "verification";
|
|
3014
|
+
}
|
|
3015
|
+
function getApplyPatchErrorDetails(error) {
|
|
3016
|
+
if (!isApplyPatchError(error)) {
|
|
3017
|
+
return;
|
|
3018
|
+
}
|
|
3019
|
+
return {
|
|
3020
|
+
kind: error.kind,
|
|
3021
|
+
code: error.code,
|
|
3022
|
+
message: error.message
|
|
3023
|
+
};
|
|
3024
|
+
}
|
|
3025
|
+
function ensureApplyPatchError(error, context) {
|
|
3026
|
+
if (isApplyPatchError(error)) {
|
|
3027
|
+
return error;
|
|
3028
|
+
}
|
|
3029
|
+
return createApplyPatchInternalError(`${context}: ${getErrorMessage(error)}`, error);
|
|
3030
|
+
}
|
|
3031
|
+
|
|
3032
|
+
// src/hooks/apply-patch/execution-context.ts
|
|
3033
|
+
import * as fs3 from "fs/promises";
|
|
3034
|
+
import path3 from "path";
|
|
3035
|
+
|
|
3036
|
+
// src/hooks/apply-patch/codec.ts
|
|
3037
|
+
function normalizeLineEndings(text) {
|
|
3038
|
+
return text.replace(/\r\n/g, `
|
|
3039
|
+
`).replace(/\r/g, `
|
|
3040
|
+
`);
|
|
3041
|
+
}
|
|
3042
|
+
function normalizeUnicode(text) {
|
|
3043
|
+
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, " ");
|
|
3044
|
+
}
|
|
3045
|
+
function stripHeredoc(input) {
|
|
3046
|
+
const normalized = normalizeLineEndings(input);
|
|
3047
|
+
const match = normalized.match(/^(?:cat\s+)?<<['"]?(\w+)['"]?\s*\n([\s\S]*?)\n\1\s*$/);
|
|
3048
|
+
return match ? match[2] : normalized;
|
|
3049
|
+
}
|
|
3050
|
+
function normalizePatchText(patchText) {
|
|
3051
|
+
return stripHeredoc(normalizeLineEndings(patchText).trim());
|
|
3052
|
+
}
|
|
3053
|
+
function parseHeader(lines, index) {
|
|
3054
|
+
const line = lines[index];
|
|
3055
|
+
if (line.startsWith("*** Add File:")) {
|
|
3056
|
+
const file = line.slice("*** Add File:".length).trim();
|
|
3057
|
+
return file ? { file, next: index + 1 } : null;
|
|
3058
|
+
}
|
|
3059
|
+
if (line.startsWith("*** Delete File:")) {
|
|
3060
|
+
const file = line.slice("*** Delete File:".length).trim();
|
|
3061
|
+
return file ? { file, next: index + 1 } : null;
|
|
3062
|
+
}
|
|
3063
|
+
if (line.startsWith("*** Update File:")) {
|
|
3064
|
+
const file = line.slice("*** Update File:".length).trim();
|
|
3065
|
+
let move;
|
|
3066
|
+
let next = index + 1;
|
|
3067
|
+
if (next < lines.length && lines[next].startsWith("*** Move to:")) {
|
|
3068
|
+
const moveTarget = lines[next].slice("*** Move to:".length).trim();
|
|
3069
|
+
if (!moveTarget) {
|
|
3070
|
+
return null;
|
|
3071
|
+
}
|
|
3072
|
+
move = moveTarget;
|
|
3073
|
+
next += 1;
|
|
3074
|
+
}
|
|
3075
|
+
return file ? { file, move, next } : null;
|
|
3076
|
+
}
|
|
3077
|
+
return null;
|
|
3078
|
+
}
|
|
3079
|
+
function unexpectedPatchLine(context, line) {
|
|
3080
|
+
const rendered = line.length === 0 ? "<empty>" : line;
|
|
3081
|
+
throw new Error(`Invalid patch format: unexpected line ${context}: ${rendered}`);
|
|
3082
|
+
}
|
|
3083
|
+
function parseChunks(lines, index, mode) {
|
|
3084
|
+
const chunks = [];
|
|
3085
|
+
let at = index;
|
|
3086
|
+
while (at < lines.length && !lines[at].startsWith("***")) {
|
|
3087
|
+
if (!lines[at].startsWith("@@")) {
|
|
3088
|
+
if (mode === "strict") {
|
|
3089
|
+
unexpectedPatchLine("in update body", lines[at]);
|
|
3090
|
+
}
|
|
3091
|
+
at += 1;
|
|
3092
|
+
continue;
|
|
3093
|
+
}
|
|
3094
|
+
const context = lines[at].slice(2).trim() || undefined;
|
|
3095
|
+
at += 1;
|
|
3096
|
+
const old_lines = [];
|
|
3097
|
+
const new_lines = [];
|
|
3098
|
+
let eof = false;
|
|
3099
|
+
while (at < lines.length && !lines[at].startsWith("@@") && (!lines[at].startsWith("***") || lines[at] === "*** End of File")) {
|
|
3100
|
+
const line = lines[at];
|
|
3101
|
+
if (line === "*** End of File") {
|
|
3102
|
+
eof = true;
|
|
3103
|
+
at += 1;
|
|
3104
|
+
break;
|
|
3105
|
+
}
|
|
3106
|
+
if (line.startsWith(" ")) {
|
|
3107
|
+
old_lines.push(line.slice(1));
|
|
3108
|
+
new_lines.push(line.slice(1));
|
|
3109
|
+
at += 1;
|
|
3110
|
+
continue;
|
|
3111
|
+
}
|
|
3112
|
+
if (line.startsWith("-")) {
|
|
3113
|
+
old_lines.push(line.slice(1));
|
|
3114
|
+
at += 1;
|
|
3115
|
+
continue;
|
|
3116
|
+
}
|
|
3117
|
+
if (line.startsWith("+")) {
|
|
3118
|
+
new_lines.push(line.slice(1));
|
|
3119
|
+
at += 1;
|
|
3120
|
+
continue;
|
|
3121
|
+
}
|
|
3122
|
+
if (mode === "strict") {
|
|
3123
|
+
unexpectedPatchLine("in patch chunk", line);
|
|
3124
|
+
}
|
|
3125
|
+
at += 1;
|
|
3126
|
+
}
|
|
3127
|
+
chunks.push({
|
|
3128
|
+
old_lines,
|
|
3129
|
+
new_lines,
|
|
3130
|
+
change_context: context,
|
|
3131
|
+
is_end_of_file: eof || undefined
|
|
3132
|
+
});
|
|
3133
|
+
}
|
|
3134
|
+
return { chunks, next: at };
|
|
3135
|
+
}
|
|
3136
|
+
function parseAdd(lines, index, mode) {
|
|
3137
|
+
let contents = "";
|
|
3138
|
+
let at = index;
|
|
3139
|
+
while (at < lines.length && !lines[at].startsWith("***")) {
|
|
3140
|
+
if (lines[at].startsWith("+")) {
|
|
3141
|
+
contents += `${lines[at].slice(1)}
|
|
3142
|
+
`;
|
|
3143
|
+
at += 1;
|
|
3144
|
+
continue;
|
|
3145
|
+
}
|
|
3146
|
+
if (mode === "strict") {
|
|
3147
|
+
unexpectedPatchLine("in Add File body", lines[at]);
|
|
3148
|
+
}
|
|
3149
|
+
at += 1;
|
|
3150
|
+
}
|
|
3151
|
+
if (contents.endsWith(`
|
|
3152
|
+
`)) {
|
|
3153
|
+
contents = contents.slice(0, -1);
|
|
3154
|
+
}
|
|
3155
|
+
return { content: contents, next: at };
|
|
3156
|
+
}
|
|
3157
|
+
function parsePatchInternal(patchText, mode) {
|
|
3158
|
+
const clean = normalizePatchText(patchText);
|
|
3159
|
+
const lines = clean.split(`
|
|
3160
|
+
`);
|
|
3161
|
+
const begin = lines.findIndex((line) => line.trim() === "*** Begin Patch");
|
|
3162
|
+
const end = lines.findIndex((line) => line.trim() === "*** End Patch");
|
|
3163
|
+
if (begin === -1 || end === -1 || begin >= end) {
|
|
3164
|
+
throw new Error("Invalid patch format: missing Begin/End markers");
|
|
3165
|
+
}
|
|
3166
|
+
if (mode === "strict") {
|
|
3167
|
+
for (const line of lines.slice(0, begin)) {
|
|
3168
|
+
unexpectedPatchLine("before Begin Patch", line);
|
|
3169
|
+
}
|
|
3170
|
+
for (const line of lines.slice(end + 1)) {
|
|
3171
|
+
unexpectedPatchLine("after End Patch", line);
|
|
3172
|
+
}
|
|
3173
|
+
}
|
|
3174
|
+
const hunks = [];
|
|
3175
|
+
let index = begin + 1;
|
|
3176
|
+
while (index < end) {
|
|
3177
|
+
const header = parseHeader(lines, index);
|
|
3178
|
+
if (!header) {
|
|
3179
|
+
if (mode === "strict") {
|
|
3180
|
+
unexpectedPatchLine("between hunks", lines[index]);
|
|
3181
|
+
}
|
|
3182
|
+
index += 1;
|
|
3183
|
+
continue;
|
|
3184
|
+
}
|
|
3185
|
+
if (lines[index].startsWith("*** Add File:")) {
|
|
3186
|
+
const next2 = parseAdd(lines, header.next, mode);
|
|
3187
|
+
hunks.push({
|
|
3188
|
+
type: "add",
|
|
3189
|
+
path: header.file,
|
|
3190
|
+
contents: next2.content
|
|
3191
|
+
});
|
|
3192
|
+
index = next2.next;
|
|
3193
|
+
continue;
|
|
3194
|
+
}
|
|
3195
|
+
if (lines[index].startsWith("*** Delete File:")) {
|
|
3196
|
+
hunks.push({ type: "delete", path: header.file });
|
|
3197
|
+
index = header.next;
|
|
3198
|
+
continue;
|
|
3199
|
+
}
|
|
3200
|
+
const next = parseChunks(lines, header.next, mode);
|
|
3201
|
+
if (mode === "strict" && next.chunks.length === 0) {
|
|
3202
|
+
throw new Error(`Invalid patch format: Update File is missing @@ chunk body: ${header.file}`);
|
|
3203
|
+
}
|
|
3204
|
+
hunks.push({
|
|
3205
|
+
type: "update",
|
|
3206
|
+
path: header.file,
|
|
3207
|
+
move_path: header.move,
|
|
3208
|
+
chunks: next.chunks
|
|
3209
|
+
});
|
|
3210
|
+
index = next.next;
|
|
3211
|
+
}
|
|
3212
|
+
return { hunks };
|
|
3213
|
+
}
|
|
3214
|
+
function parsePatchStrict(patchText) {
|
|
3215
|
+
return parsePatchInternal(patchText, "strict");
|
|
3216
|
+
}
|
|
3217
|
+
function diffMatrix(old_lines, new_lines) {
|
|
3218
|
+
const dp = Array.from({ length: old_lines.length + 1 }, () => Array(new_lines.length + 1).fill(0));
|
|
3219
|
+
for (let oldIndex = 1;oldIndex <= old_lines.length; oldIndex += 1) {
|
|
3220
|
+
for (let newIndex = 1;newIndex <= new_lines.length; newIndex += 1) {
|
|
3221
|
+
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]);
|
|
3222
|
+
}
|
|
3223
|
+
}
|
|
3224
|
+
return dp;
|
|
3225
|
+
}
|
|
3226
|
+
function renderChunk(chunk) {
|
|
3227
|
+
const lines = [chunk.change_context ? `@@ ${chunk.change_context}` : "@@"];
|
|
3228
|
+
const dp = diffMatrix(chunk.old_lines, chunk.new_lines);
|
|
3229
|
+
const body = [];
|
|
3230
|
+
let oldIndex = chunk.old_lines.length;
|
|
3231
|
+
let newIndex = chunk.new_lines.length;
|
|
3232
|
+
while (oldIndex > 0 && newIndex > 0) {
|
|
3233
|
+
if (chunk.old_lines[oldIndex - 1] === chunk.new_lines[newIndex - 1]) {
|
|
3234
|
+
body.push(` ${chunk.old_lines[oldIndex - 1]}`);
|
|
3235
|
+
oldIndex -= 1;
|
|
3236
|
+
newIndex -= 1;
|
|
3237
|
+
continue;
|
|
3238
|
+
}
|
|
3239
|
+
if (dp[oldIndex - 1][newIndex] >= dp[oldIndex][newIndex - 1]) {
|
|
3240
|
+
body.push(`-${chunk.old_lines[oldIndex - 1]}`);
|
|
3241
|
+
oldIndex -= 1;
|
|
3242
|
+
continue;
|
|
3243
|
+
}
|
|
3244
|
+
body.push(`+${chunk.new_lines[newIndex - 1]}`);
|
|
3245
|
+
newIndex -= 1;
|
|
3246
|
+
}
|
|
3247
|
+
while (oldIndex > 0) {
|
|
3248
|
+
body.push(`-${chunk.old_lines[oldIndex - 1]}`);
|
|
3249
|
+
oldIndex -= 1;
|
|
3250
|
+
}
|
|
3251
|
+
while (newIndex > 0) {
|
|
3252
|
+
body.push(`+${chunk.new_lines[newIndex - 1]}`);
|
|
3253
|
+
newIndex -= 1;
|
|
3254
|
+
}
|
|
3255
|
+
lines.push(...body.reverse());
|
|
3256
|
+
if (chunk.is_end_of_file) {
|
|
3257
|
+
lines.push("*** End of File");
|
|
3258
|
+
}
|
|
3259
|
+
return lines;
|
|
3260
|
+
}
|
|
3261
|
+
function renderAddContents(contents) {
|
|
3262
|
+
if (contents.length === 0) {
|
|
3263
|
+
return [];
|
|
3264
|
+
}
|
|
3265
|
+
return contents.split(`
|
|
3266
|
+
`).map((line) => `+${line}`);
|
|
3267
|
+
}
|
|
3268
|
+
function formatPatch(patch) {
|
|
3269
|
+
const lines = ["*** Begin Patch"];
|
|
3270
|
+
for (const hunk of patch.hunks) {
|
|
3271
|
+
if (hunk.type === "add") {
|
|
3272
|
+
lines.push(`*** Add File: ${hunk.path}`);
|
|
3273
|
+
lines.push(...renderAddContents(hunk.contents));
|
|
3274
|
+
continue;
|
|
3275
|
+
}
|
|
3276
|
+
if (hunk.type === "delete") {
|
|
3277
|
+
lines.push(`*** Delete File: ${hunk.path}`);
|
|
3278
|
+
continue;
|
|
3279
|
+
}
|
|
3280
|
+
lines.push(`*** Update File: ${hunk.path}`);
|
|
3281
|
+
if (hunk.move_path) {
|
|
3282
|
+
lines.push(`*** Move to: ${hunk.move_path}`);
|
|
3283
|
+
}
|
|
3284
|
+
for (const chunk of hunk.chunks) {
|
|
3285
|
+
lines.push(...renderChunk(chunk));
|
|
3286
|
+
}
|
|
3287
|
+
}
|
|
3288
|
+
lines.push("*** End Patch");
|
|
3289
|
+
return lines.join(`
|
|
3290
|
+
`);
|
|
3291
|
+
}
|
|
3292
|
+
|
|
3293
|
+
// src/hooks/apply-patch/matching.ts
|
|
3294
|
+
var AUTO_RESCUE_COMPARATOR_NAMES = new Set([
|
|
3295
|
+
"exact",
|
|
3296
|
+
"unicode",
|
|
3297
|
+
"trim-end",
|
|
3298
|
+
"unicode-trim-end"
|
|
3299
|
+
]);
|
|
3300
|
+
function equalExact(a, b) {
|
|
3301
|
+
return a === b;
|
|
3302
|
+
}
|
|
3303
|
+
function equalUnicodeExact(a, b) {
|
|
3304
|
+
return normalizeUnicode(a) === normalizeUnicode(b);
|
|
3305
|
+
}
|
|
3306
|
+
function equalTrimEnd(a, b) {
|
|
3307
|
+
return a.trimEnd() === b.trimEnd();
|
|
3308
|
+
}
|
|
3309
|
+
function equalUnicodeTrimEnd(a, b) {
|
|
3310
|
+
return normalizeUnicode(a.trimEnd()) === normalizeUnicode(b.trimEnd());
|
|
3311
|
+
}
|
|
3312
|
+
function equalTrim(a, b) {
|
|
3313
|
+
return a.trim() === b.trim();
|
|
3314
|
+
}
|
|
3315
|
+
function equalUnicodeTrim(a, b) {
|
|
3316
|
+
return normalizeUnicode(a.trim()) === normalizeUnicode(b.trim());
|
|
3317
|
+
}
|
|
3318
|
+
var comparatorEntries = [
|
|
3319
|
+
{ name: "exact", exact: true, same: equalExact },
|
|
3320
|
+
{ name: "unicode", exact: false, same: equalUnicodeExact },
|
|
3321
|
+
{ name: "trim-end", exact: false, same: equalTrimEnd },
|
|
3322
|
+
{
|
|
3323
|
+
name: "unicode-trim-end",
|
|
3324
|
+
exact: false,
|
|
3325
|
+
same: equalUnicodeTrimEnd
|
|
3326
|
+
},
|
|
3327
|
+
{ name: "trim", exact: false, same: equalTrim },
|
|
3328
|
+
{ name: "unicode-trim", exact: false, same: equalUnicodeTrim }
|
|
3329
|
+
];
|
|
3330
|
+
var autoRescueComparatorEntries = comparatorEntries.filter((entry) => AUTO_RESCUE_COMPARATOR_NAMES.has(entry.name));
|
|
3331
|
+
var MAX_LCS_CHUNK_LINES = 48;
|
|
3332
|
+
var MAX_LCS_CANDIDATES = 64;
|
|
3333
|
+
var autoRescueComparators = autoRescueComparatorEntries.map((entry) => entry.same);
|
|
3334
|
+
var permissiveComparators = comparatorEntries.map((entry) => entry.same);
|
|
3335
|
+
function tryMatch(lines, pattern, start, comparator, eof) {
|
|
3336
|
+
if (eof) {
|
|
3337
|
+
const at = lines.length - pattern.length;
|
|
3338
|
+
if (at >= start) {
|
|
3339
|
+
let ok = true;
|
|
3340
|
+
for (let index = 0;index < pattern.length; index += 1) {
|
|
3341
|
+
if (!comparator.same(lines[at + index], pattern[index])) {
|
|
3342
|
+
ok = false;
|
|
3343
|
+
break;
|
|
3344
|
+
}
|
|
3345
|
+
}
|
|
3346
|
+
if (ok) {
|
|
3347
|
+
return {
|
|
3348
|
+
index: at,
|
|
3349
|
+
comparator: comparator.name,
|
|
3350
|
+
exact: comparator.exact
|
|
3351
|
+
};
|
|
3352
|
+
}
|
|
3353
|
+
}
|
|
3354
|
+
}
|
|
3355
|
+
for (let index = start;index <= lines.length - pattern.length; index += 1) {
|
|
3356
|
+
let ok = true;
|
|
3357
|
+
for (let inner = 0;inner < pattern.length; inner += 1) {
|
|
3358
|
+
if (!comparator.same(lines[index + inner], pattern[inner])) {
|
|
3359
|
+
ok = false;
|
|
3360
|
+
break;
|
|
3361
|
+
}
|
|
3362
|
+
}
|
|
3363
|
+
if (ok) {
|
|
3364
|
+
return {
|
|
3365
|
+
index,
|
|
3366
|
+
comparator: comparator.name,
|
|
3367
|
+
exact: comparator.exact
|
|
3368
|
+
};
|
|
3369
|
+
}
|
|
3370
|
+
}
|
|
3371
|
+
return;
|
|
3372
|
+
}
|
|
3373
|
+
function seekMatch(lines, pattern, start, eof = false) {
|
|
3374
|
+
if (pattern.length === 0) {
|
|
3375
|
+
return;
|
|
3376
|
+
}
|
|
3377
|
+
for (const comparator of autoRescueComparatorEntries) {
|
|
3378
|
+
const hit = tryMatch(lines, pattern, start, comparator, eof);
|
|
3379
|
+
if (hit) {
|
|
3380
|
+
return hit;
|
|
3381
|
+
}
|
|
3382
|
+
}
|
|
3383
|
+
return;
|
|
3384
|
+
}
|
|
3385
|
+
function seek(lines, pattern, start, eof = false) {
|
|
3386
|
+
return seekMatch(lines, pattern, start, eof)?.index ?? -1;
|
|
3387
|
+
}
|
|
3388
|
+
function list(lines, pattern, start, same) {
|
|
3389
|
+
if (pattern.length === 0) {
|
|
3390
|
+
return [];
|
|
3391
|
+
}
|
|
3392
|
+
const out = [];
|
|
3393
|
+
for (let index = start;index <= lines.length - pattern.length; index += 1) {
|
|
3394
|
+
let ok = true;
|
|
3395
|
+
for (let inner = 0;inner < pattern.length; inner += 1) {
|
|
3396
|
+
if (!same(lines[index + inner], pattern[inner])) {
|
|
3397
|
+
ok = false;
|
|
3398
|
+
break;
|
|
3399
|
+
}
|
|
3400
|
+
}
|
|
3401
|
+
if (ok) {
|
|
3402
|
+
out.push(index);
|
|
3403
|
+
}
|
|
3404
|
+
}
|
|
3405
|
+
return out;
|
|
3406
|
+
}
|
|
3407
|
+
function sameRescueLine(a, b) {
|
|
3408
|
+
return equalExact(a, b) || equalUnicodeExact(a, b);
|
|
3409
|
+
}
|
|
3410
|
+
function prefix(old_lines, new_lines) {
|
|
3411
|
+
let index = 0;
|
|
3412
|
+
while (index < old_lines.length && index < new_lines.length && sameRescueLine(old_lines[index], new_lines[index])) {
|
|
3413
|
+
index += 1;
|
|
3414
|
+
}
|
|
3415
|
+
return index;
|
|
3416
|
+
}
|
|
3417
|
+
function suffix(old_lines, new_lines, prefixLength) {
|
|
3418
|
+
let index = 0;
|
|
3419
|
+
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])) {
|
|
3420
|
+
index += 1;
|
|
3421
|
+
}
|
|
3422
|
+
return index;
|
|
3423
|
+
}
|
|
3424
|
+
function rescueByPrefixSuffix(lines, old_lines, new_lines, start) {
|
|
3425
|
+
const prefixLength = prefix(old_lines, new_lines);
|
|
3426
|
+
const suffixLength = suffix(old_lines, new_lines, prefixLength);
|
|
3427
|
+
if (prefixLength === 0 || suffixLength === 0) {
|
|
3428
|
+
return { kind: "miss" };
|
|
3429
|
+
}
|
|
3430
|
+
const left = old_lines.slice(0, prefixLength);
|
|
3431
|
+
const right = old_lines.slice(old_lines.length - suffixLength);
|
|
3432
|
+
const middle = new_lines.slice(prefixLength, new_lines.length - suffixLength);
|
|
3433
|
+
const hits = new Map;
|
|
3434
|
+
for (const same of autoRescueComparators) {
|
|
3435
|
+
for (const leftIndex of list(lines, left, start, same)) {
|
|
3436
|
+
const from = leftIndex + left.length;
|
|
3437
|
+
for (const rightIndex of list(lines, right, from, same)) {
|
|
3438
|
+
const key = `${from}:${rightIndex}`;
|
|
3439
|
+
hits.set(key, {
|
|
3440
|
+
start: from,
|
|
3441
|
+
del: rightIndex - from,
|
|
3442
|
+
add: [...middle]
|
|
3443
|
+
});
|
|
3444
|
+
}
|
|
3445
|
+
}
|
|
3446
|
+
}
|
|
3447
|
+
if (hits.size === 0) {
|
|
3448
|
+
return { kind: "miss" };
|
|
3449
|
+
}
|
|
3450
|
+
if (hits.size > 1) {
|
|
3451
|
+
return { kind: "ambiguous", phase: "prefix_suffix" };
|
|
3452
|
+
}
|
|
3453
|
+
return { kind: "match", hit: [...hits.values()][0] };
|
|
3454
|
+
}
|
|
3455
|
+
function score(a, b) {
|
|
3456
|
+
const dp = Array.from({ length: a.length + 1 }, () => Array(b.length + 1).fill(0));
|
|
3457
|
+
for (let i = 1;i <= a.length; i += 1) {
|
|
3458
|
+
for (let j = 1;j <= b.length; j += 1) {
|
|
3459
|
+
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]);
|
|
3460
|
+
}
|
|
3461
|
+
}
|
|
3462
|
+
return dp[a.length][b.length];
|
|
3463
|
+
}
|
|
3464
|
+
function normalizeLcsLine(line) {
|
|
3465
|
+
return normalizeUnicode(line).trim();
|
|
3466
|
+
}
|
|
3467
|
+
function countLcsUpperBound(a, b) {
|
|
3468
|
+
const counts = new Map;
|
|
3469
|
+
for (const line of a) {
|
|
3470
|
+
const key = normalizeLcsLine(line);
|
|
3471
|
+
counts.set(key, (counts.get(key) ?? 0) + 1);
|
|
3472
|
+
}
|
|
3473
|
+
let shared = 0;
|
|
3474
|
+
for (const line of b) {
|
|
3475
|
+
const key = normalizeLcsLine(line);
|
|
3476
|
+
const available = counts.get(key) ?? 0;
|
|
3477
|
+
if (available === 0) {
|
|
3478
|
+
continue;
|
|
3479
|
+
}
|
|
3480
|
+
shared += 1;
|
|
3481
|
+
if (available === 1) {
|
|
3482
|
+
counts.delete(key);
|
|
3483
|
+
continue;
|
|
3484
|
+
}
|
|
3485
|
+
counts.set(key, available - 1);
|
|
3486
|
+
}
|
|
3487
|
+
return shared;
|
|
3488
|
+
}
|
|
3489
|
+
function hasStableBorders(oldLines, candidate) {
|
|
3490
|
+
if (oldLines.length === 0 || candidate.length !== oldLines.length) {
|
|
3491
|
+
return false;
|
|
3492
|
+
}
|
|
3493
|
+
const same = autoRescueComparators.some((compare) => compare(oldLines[0], candidate[0]));
|
|
3494
|
+
if (!same) {
|
|
3495
|
+
return false;
|
|
3496
|
+
}
|
|
3497
|
+
if (oldLines.length === 1) {
|
|
3498
|
+
return true;
|
|
3499
|
+
}
|
|
3500
|
+
return autoRescueComparators.some((compare) => compare(oldLines[oldLines.length - 1], candidate[candidate.length - 1]));
|
|
3501
|
+
}
|
|
3502
|
+
function collectBorderAnchoredStarts(lines, oldLines, start) {
|
|
3503
|
+
if (oldLines.length === 0) {
|
|
3504
|
+
return [];
|
|
3505
|
+
}
|
|
3506
|
+
const firstHits = new Set;
|
|
3507
|
+
const lastHits = new Set;
|
|
3508
|
+
const lastLine = oldLines[oldLines.length - 1];
|
|
3509
|
+
for (const same of autoRescueComparators) {
|
|
3510
|
+
for (const index of list(lines, [oldLines[0]], start, same)) {
|
|
3511
|
+
firstHits.add(index);
|
|
3512
|
+
}
|
|
3513
|
+
for (const index of list(lines, [lastLine], start, same)) {
|
|
3514
|
+
lastHits.add(index);
|
|
3515
|
+
}
|
|
3516
|
+
}
|
|
3517
|
+
const candidates = [];
|
|
3518
|
+
for (const index of [...firstHits].sort((a, b) => a - b)) {
|
|
3519
|
+
const end = index + oldLines.length - 1;
|
|
3520
|
+
if (end >= lines.length || !lastHits.has(end)) {
|
|
3521
|
+
continue;
|
|
3522
|
+
}
|
|
3523
|
+
const candidate = lines.slice(index, index + oldLines.length);
|
|
3524
|
+
if (!hasStableBorders(oldLines, candidate)) {
|
|
3525
|
+
continue;
|
|
3526
|
+
}
|
|
3527
|
+
candidates.push(index);
|
|
3528
|
+
}
|
|
3529
|
+
return candidates;
|
|
3530
|
+
}
|
|
3531
|
+
function rescueByLcs(lines, old_lines, new_lines, start) {
|
|
3532
|
+
if (old_lines.length === 0 || lines.length === 0) {
|
|
3533
|
+
return { kind: "miss" };
|
|
3534
|
+
}
|
|
3535
|
+
const from = start;
|
|
3536
|
+
const to = lines.length - old_lines.length;
|
|
3537
|
+
if (to < from) {
|
|
3538
|
+
return { kind: "miss" };
|
|
3539
|
+
}
|
|
3540
|
+
if (old_lines.length > MAX_LCS_CHUNK_LINES) {
|
|
3541
|
+
return { kind: "miss" };
|
|
3542
|
+
}
|
|
3543
|
+
const needed = old_lines.length <= 2 ? old_lines.length : Math.max(2, Math.ceil(old_lines.length * 0.7));
|
|
3544
|
+
const candidates = collectBorderAnchoredStarts(lines, old_lines, start);
|
|
3545
|
+
if (candidates.length === 0 || candidates.length > MAX_LCS_CANDIDATES) {
|
|
3546
|
+
return { kind: "miss" };
|
|
3547
|
+
}
|
|
3548
|
+
let best;
|
|
3549
|
+
let bestScore = 0;
|
|
3550
|
+
let ties = 0;
|
|
3551
|
+
for (const index of candidates) {
|
|
3552
|
+
if (index < from || index > to) {
|
|
3553
|
+
continue;
|
|
3554
|
+
}
|
|
3555
|
+
const window = lines.slice(index, index + old_lines.length);
|
|
3556
|
+
if (countLcsUpperBound(old_lines, window) < needed) {
|
|
3557
|
+
continue;
|
|
3558
|
+
}
|
|
3559
|
+
const current = score(old_lines, window);
|
|
3560
|
+
if (current > bestScore) {
|
|
3561
|
+
bestScore = current;
|
|
3562
|
+
ties = 1;
|
|
3563
|
+
best = {
|
|
3564
|
+
start: index,
|
|
3565
|
+
del: old_lines.length,
|
|
3566
|
+
add: [...new_lines]
|
|
3567
|
+
};
|
|
3568
|
+
continue;
|
|
3569
|
+
}
|
|
3570
|
+
if (current === bestScore && current > 0) {
|
|
3571
|
+
ties += 1;
|
|
3572
|
+
}
|
|
3573
|
+
}
|
|
3574
|
+
if (!best || bestScore < needed) {
|
|
3575
|
+
return { kind: "miss" };
|
|
3576
|
+
}
|
|
3577
|
+
if (ties > 1) {
|
|
3578
|
+
return { kind: "ambiguous", phase: "lcs" };
|
|
3579
|
+
}
|
|
3580
|
+
return { kind: "match", hit: best };
|
|
3581
|
+
}
|
|
3582
|
+
|
|
3583
|
+
// src/hooks/apply-patch/resolution.ts
|
|
3584
|
+
function splitFileLines(text) {
|
|
3585
|
+
const eol = text.match(/\r\n|\n|\r/)?.[0] === `\r
|
|
3586
|
+
` ? `\r
|
|
3587
|
+
` : `
|
|
3588
|
+
`;
|
|
3589
|
+
const normalized = text.replace(/\r\n/g, `
|
|
3590
|
+
`).replace(/\r/g, `
|
|
3591
|
+
`);
|
|
3592
|
+
const hasFinalNewline = normalized.endsWith(`
|
|
3593
|
+
`);
|
|
3594
|
+
const lines = normalized.split(`
|
|
3595
|
+
`);
|
|
3596
|
+
if (hasFinalNewline) {
|
|
3597
|
+
lines.pop();
|
|
3598
|
+
}
|
|
3599
|
+
return { lines, eol, hasFinalNewline };
|
|
3600
|
+
}
|
|
3601
|
+
function resolveChunkStart(lines, chunk, start) {
|
|
3602
|
+
if (!chunk.change_context) {
|
|
3603
|
+
return start;
|
|
3604
|
+
}
|
|
3605
|
+
const at = seek(lines, [chunk.change_context], start);
|
|
3606
|
+
return at === -1 ? start : at + 1;
|
|
3607
|
+
}
|
|
3608
|
+
function resolveUniqueAnchor(lines, changeContext, start) {
|
|
3609
|
+
const hits = new Set;
|
|
3610
|
+
for (const same of autoRescueComparators) {
|
|
3611
|
+
for (const index2 of list(lines, [changeContext], start, same)) {
|
|
3612
|
+
hits.add(index2);
|
|
3613
|
+
}
|
|
3614
|
+
}
|
|
3615
|
+
if (hits.size === 0) {
|
|
3616
|
+
return { kind: "missing" };
|
|
3617
|
+
}
|
|
3618
|
+
if (hits.size > 1) {
|
|
3619
|
+
return { kind: "ambiguous" };
|
|
3620
|
+
}
|
|
3621
|
+
const index = [...hits][0];
|
|
3622
|
+
const canonicalLine = lines[index];
|
|
3623
|
+
const comparator = seekMatch(lines, [changeContext], index)?.comparator;
|
|
3624
|
+
return {
|
|
3625
|
+
kind: "match",
|
|
3626
|
+
index,
|
|
3627
|
+
exact: canonicalLine === changeContext,
|
|
3628
|
+
comparator: comparator ?? "exact",
|
|
3629
|
+
canonicalLine
|
|
3630
|
+
};
|
|
3631
|
+
}
|
|
3632
|
+
function locateChunk(lines, file, chunk, start, cfg) {
|
|
3633
|
+
const old_lines = chunk.old_lines;
|
|
3634
|
+
const new_lines = chunk.new_lines;
|
|
3635
|
+
const match = seekMatch(lines, old_lines, start, chunk.is_end_of_file ?? false);
|
|
3636
|
+
if (match) {
|
|
3637
|
+
const canonical_old_lines = lines.slice(match.index, match.index + old_lines.length);
|
|
3638
|
+
const rewritten = !match.exact;
|
|
3639
|
+
return {
|
|
3640
|
+
hit: { start: match.index, del: old_lines.length, add: [...new_lines] },
|
|
3641
|
+
old_lines,
|
|
3642
|
+
canonical_old_lines,
|
|
3643
|
+
canonical_new_lines: [...chunk.new_lines],
|
|
3644
|
+
resolved_is_end_of_file: match.index + canonical_old_lines.length === lines.length,
|
|
3645
|
+
rewritten,
|
|
3646
|
+
strategy: undefined,
|
|
3647
|
+
matchComparator: match.comparator
|
|
3648
|
+
};
|
|
3649
|
+
}
|
|
3650
|
+
if (cfg.prefixSuffix) {
|
|
3651
|
+
const rescued = rescueByPrefixSuffix(lines, old_lines, new_lines, start);
|
|
3652
|
+
if (rescued.kind === "ambiguous") {
|
|
3653
|
+
throw new Error(`Prefix/suffix rescue was ambiguous in ${file}:
|
|
3654
|
+
${chunk.old_lines.join(`
|
|
3655
|
+
`)}`);
|
|
3656
|
+
}
|
|
3657
|
+
if (rescued.kind === "match") {
|
|
3658
|
+
const prefixLength = prefix(old_lines, new_lines);
|
|
3659
|
+
const suffixLength = suffix(old_lines, new_lines, prefixLength);
|
|
3660
|
+
const canonicalStart = rescued.hit.start - prefixLength;
|
|
3661
|
+
const canonicalEnd = rescued.hit.start + rescued.hit.del + suffixLength;
|
|
3662
|
+
return {
|
|
3663
|
+
hit: rescued.hit,
|
|
3664
|
+
old_lines,
|
|
3665
|
+
canonical_old_lines: lines.slice(canonicalStart, canonicalEnd),
|
|
3666
|
+
canonical_new_lines: [...chunk.new_lines],
|
|
3667
|
+
resolved_is_end_of_file: canonicalEnd === lines.length,
|
|
3668
|
+
rewritten: true,
|
|
3669
|
+
strategy: "prefix/suffix",
|
|
3670
|
+
matchComparator: "exact"
|
|
3671
|
+
};
|
|
3672
|
+
}
|
|
3673
|
+
}
|
|
3674
|
+
if (cfg.lcsRescue) {
|
|
3675
|
+
const rescued = rescueByLcs(lines, old_lines, new_lines, start);
|
|
3676
|
+
if (rescued.kind === "ambiguous") {
|
|
3677
|
+
throw new Error(`LCS rescue was ambiguous in ${file}:
|
|
3678
|
+
${chunk.old_lines.join(`
|
|
3679
|
+
`)}`);
|
|
3680
|
+
}
|
|
3681
|
+
if (rescued.kind === "match") {
|
|
3682
|
+
return {
|
|
3683
|
+
hit: rescued.hit,
|
|
3684
|
+
old_lines,
|
|
3685
|
+
canonical_old_lines: lines.slice(rescued.hit.start, rescued.hit.start + rescued.hit.del),
|
|
3686
|
+
canonical_new_lines: [...chunk.new_lines],
|
|
3687
|
+
resolved_is_end_of_file: rescued.hit.start + rescued.hit.del === lines.length,
|
|
3688
|
+
rewritten: true,
|
|
3689
|
+
strategy: "lcs",
|
|
3690
|
+
matchComparator: "exact"
|
|
3691
|
+
};
|
|
3692
|
+
}
|
|
3693
|
+
}
|
|
3694
|
+
throw new Error(`Failed to find expected lines in ${file}:
|
|
3695
|
+
${chunk.old_lines.join(`
|
|
3696
|
+
`)}`);
|
|
3697
|
+
}
|
|
3698
|
+
function applyHits(lines, hits, eol = `
|
|
3699
|
+
`, hasFinalNewline = true) {
|
|
3700
|
+
const out = [...lines];
|
|
3701
|
+
for (let index = hits.length - 1;index >= 0; index -= 1) {
|
|
3702
|
+
out.splice(hits[index].start, hits[index].del, ...hits[index].add);
|
|
3703
|
+
}
|
|
3704
|
+
if (out.length === 0) {
|
|
3705
|
+
return "";
|
|
3706
|
+
}
|
|
3707
|
+
const rendered = out.join(eol);
|
|
3708
|
+
return hasFinalNewline ? `${rendered}${eol}` : rendered;
|
|
3709
|
+
}
|
|
3710
|
+
function resolveUpdateChunksFromFileLines(file, state, chunks, cfg) {
|
|
3711
|
+
const lines = [...state.lines];
|
|
3712
|
+
const resolved = [];
|
|
3713
|
+
let start = 0;
|
|
3714
|
+
for (const chunk of chunks) {
|
|
3715
|
+
const chunkStart = resolveChunkStart(lines, chunk, start);
|
|
3716
|
+
let strategy;
|
|
3717
|
+
if (chunk.old_lines.length === 0) {
|
|
3718
|
+
if (chunk.is_end_of_file) {
|
|
3719
|
+
resolved.push({
|
|
3720
|
+
hit: {
|
|
3721
|
+
start: lines.length,
|
|
3722
|
+
del: 0,
|
|
3723
|
+
add: [...chunk.new_lines]
|
|
3724
|
+
},
|
|
3725
|
+
old_lines: [],
|
|
3726
|
+
canonical_old_lines: [],
|
|
3727
|
+
canonical_new_lines: [...chunk.new_lines],
|
|
3728
|
+
resolved_is_end_of_file: true,
|
|
3729
|
+
rewritten: false,
|
|
3730
|
+
strategy,
|
|
3731
|
+
matchComparator: "exact"
|
|
3732
|
+
});
|
|
3733
|
+
start = lines.length;
|
|
3734
|
+
continue;
|
|
3735
|
+
}
|
|
3736
|
+
if (!chunk.change_context) {
|
|
3737
|
+
throw new Error(`Missing insertion anchor in ${file}`);
|
|
3738
|
+
}
|
|
3739
|
+
const anchorMatch = resolveUniqueAnchor(lines, chunk.change_context, start);
|
|
3740
|
+
if (anchorMatch.kind === "missing") {
|
|
3741
|
+
throw new Error(`Failed to find insertion anchor in ${file}:
|
|
3742
|
+
${chunk.change_context}`);
|
|
3743
|
+
}
|
|
3744
|
+
if (anchorMatch.kind === "ambiguous") {
|
|
3745
|
+
throw new Error(`Insertion anchor was ambiguous in ${file}:
|
|
3746
|
+
${chunk.change_context}`);
|
|
3747
|
+
}
|
|
3748
|
+
const insertAt = anchorMatch.index + 1;
|
|
3749
|
+
if (insertAt === lines.length) {
|
|
3750
|
+
resolved.push({
|
|
3751
|
+
hit: {
|
|
3752
|
+
start: insertAt,
|
|
3753
|
+
del: 0,
|
|
3754
|
+
add: [...chunk.new_lines]
|
|
3755
|
+
},
|
|
3756
|
+
old_lines: [],
|
|
3757
|
+
canonical_old_lines: [],
|
|
3758
|
+
canonical_new_lines: [...chunk.new_lines],
|
|
3759
|
+
canonical_change_context: anchorMatch.exact ? undefined : anchorMatch.canonicalLine,
|
|
3760
|
+
resolved_is_end_of_file: insertAt === lines.length,
|
|
3761
|
+
rewritten: !anchorMatch.exact,
|
|
3762
|
+
strategy: anchorMatch.exact ? strategy : "anchor",
|
|
3763
|
+
matchComparator: anchorMatch.comparator
|
|
3764
|
+
});
|
|
3765
|
+
start = insertAt;
|
|
3766
|
+
continue;
|
|
3767
|
+
}
|
|
3768
|
+
const anchor = lines[insertAt];
|
|
3769
|
+
strategy = "anchor";
|
|
3770
|
+
resolved.push({
|
|
3771
|
+
hit: {
|
|
3772
|
+
start: insertAt,
|
|
3773
|
+
del: 0,
|
|
3774
|
+
add: [...chunk.new_lines]
|
|
3775
|
+
},
|
|
3776
|
+
old_lines: [],
|
|
3777
|
+
canonical_old_lines: [anchor],
|
|
3778
|
+
canonical_new_lines: [...chunk.new_lines, anchor],
|
|
3779
|
+
resolved_is_end_of_file: insertAt + 1 === lines.length,
|
|
3780
|
+
rewritten: true,
|
|
3781
|
+
strategy,
|
|
3782
|
+
matchComparator: "exact"
|
|
3783
|
+
});
|
|
3784
|
+
start = insertAt;
|
|
3785
|
+
continue;
|
|
3786
|
+
}
|
|
3787
|
+
const found = locateChunk(lines, file, chunk, chunkStart, cfg);
|
|
3788
|
+
resolved.push(found);
|
|
3789
|
+
start = found.hit.start + found.hit.del;
|
|
3790
|
+
}
|
|
3791
|
+
resolved.sort((a, b) => a.hit.start - b.hit.start);
|
|
3792
|
+
for (let index = 1;index < resolved.length; index += 1) {
|
|
3793
|
+
const previous = resolved[index - 1].hit;
|
|
3794
|
+
const current = resolved[index].hit;
|
|
3795
|
+
if (previous.start + previous.del > current.start) {
|
|
3796
|
+
throw new Error(`Overlapping patch chunks in ${file}`);
|
|
3797
|
+
}
|
|
3798
|
+
}
|
|
3799
|
+
return {
|
|
3800
|
+
lines,
|
|
3801
|
+
resolved,
|
|
3802
|
+
eol: state.eol,
|
|
3803
|
+
hasFinalNewline: state.hasFinalNewline
|
|
3804
|
+
};
|
|
3805
|
+
}
|
|
3806
|
+
function deriveNewContentFromText(file, text, chunks, cfg) {
|
|
3807
|
+
const { lines, resolved, eol, hasFinalNewline } = resolveUpdateChunksFromFileLines(file, splitFileLines(text), chunks, cfg);
|
|
3808
|
+
return applyHits(lines, resolved.map((chunk) => chunk.hit), eol, hasFinalNewline);
|
|
3809
|
+
}
|
|
3810
|
+
function resolveUpdateChunksFromText(file, text, chunks, cfg) {
|
|
3811
|
+
return resolveUpdateChunksFromFileLines(file, splitFileLines(text), chunks, cfg);
|
|
3812
|
+
}
|
|
3813
|
+
|
|
3814
|
+
// src/hooks/apply-patch/execution-context.ts
|
|
3815
|
+
function isMissingPathError(error) {
|
|
3816
|
+
return !!error && typeof error === "object" && "code" in error && (error.code === "ENOENT" || error.code === "ENOTDIR");
|
|
3817
|
+
}
|
|
3818
|
+
async function real(target) {
|
|
3819
|
+
const parts = [];
|
|
3820
|
+
let current = path3.resolve(target);
|
|
3821
|
+
while (true) {
|
|
3822
|
+
const exact = await fs3.realpath(current).catch((error) => {
|
|
3823
|
+
if (isMissingPathError(error)) {
|
|
3824
|
+
return null;
|
|
3825
|
+
}
|
|
3826
|
+
throw createApplyPatchInternalError(`Failed to resolve real path: ${current}`, error);
|
|
3827
|
+
});
|
|
3828
|
+
if (exact) {
|
|
3829
|
+
return parts.length === 0 ? exact : path3.join(exact, ...parts.reverse());
|
|
3830
|
+
}
|
|
3831
|
+
const parent = path3.dirname(current);
|
|
3832
|
+
if (parent === current) {
|
|
3833
|
+
return parts.length === 0 ? current : path3.join(current, ...parts.reverse());
|
|
3834
|
+
}
|
|
3835
|
+
parts.push(path3.basename(current));
|
|
3836
|
+
current = parent;
|
|
3837
|
+
}
|
|
3838
|
+
}
|
|
3839
|
+
function inside(root, target) {
|
|
3840
|
+
const rel = path3.relative(root, target);
|
|
3841
|
+
return rel === "" || !rel.startsWith("..") && !path3.isAbsolute(rel);
|
|
3842
|
+
}
|
|
3843
|
+
function createPathGuardContext(root, worktree) {
|
|
3844
|
+
return {
|
|
3845
|
+
rootReal: real(root),
|
|
3846
|
+
worktreeReal: worktree && worktree !== "/" ? real(worktree) : undefined,
|
|
3847
|
+
realCache: new Map
|
|
3848
|
+
};
|
|
3849
|
+
}
|
|
3850
|
+
async function realCached(ctx, target) {
|
|
3851
|
+
const resolvedTarget = path3.resolve(target);
|
|
3852
|
+
let pending = ctx.realCache.get(resolvedTarget);
|
|
3853
|
+
if (!pending) {
|
|
3854
|
+
pending = real(resolvedTarget);
|
|
3855
|
+
ctx.realCache.set(resolvedTarget, pending);
|
|
3856
|
+
}
|
|
3857
|
+
return await pending;
|
|
3858
|
+
}
|
|
3859
|
+
async function guard(ctx, target) {
|
|
3860
|
+
const [targetReal, rootReal] = await Promise.all([
|
|
3861
|
+
realCached(ctx, target),
|
|
3862
|
+
ctx.rootReal
|
|
3863
|
+
]);
|
|
3864
|
+
if (inside(rootReal, targetReal)) {
|
|
3865
|
+
return;
|
|
3866
|
+
}
|
|
3867
|
+
if (!ctx.worktreeReal) {
|
|
3868
|
+
throw createApplyPatchBlockedError(`patch contains path outside workspace root: ${target}`);
|
|
3869
|
+
}
|
|
3870
|
+
const treeReal = await ctx.worktreeReal;
|
|
3871
|
+
if (inside(treeReal, targetReal)) {
|
|
3872
|
+
return;
|
|
3873
|
+
}
|
|
3874
|
+
throw createApplyPatchBlockedError(`patch contains path outside workspace root: ${target}`);
|
|
3875
|
+
}
|
|
3876
|
+
function createFileCacheContext() {
|
|
3877
|
+
return { stats: new Map };
|
|
3878
|
+
}
|
|
3879
|
+
async function statCached(ctx, filePath) {
|
|
3880
|
+
let pending = ctx.stats.get(filePath);
|
|
3881
|
+
if (!pending) {
|
|
3882
|
+
const nextPending = fs3.stat(filePath).catch((error) => {
|
|
3883
|
+
if (isMissingPathError(error)) {
|
|
3884
|
+
return null;
|
|
3885
|
+
}
|
|
3886
|
+
throw createApplyPatchInternalError(`Failed to stat file for patch verification: ${filePath}`, error);
|
|
3887
|
+
});
|
|
3888
|
+
ctx.stats.set(filePath, nextPending);
|
|
3889
|
+
pending = nextPending;
|
|
3890
|
+
}
|
|
3891
|
+
return await pending;
|
|
3892
|
+
}
|
|
3893
|
+
async function assertRegularFile(ctx, filePath, verb) {
|
|
3894
|
+
const stat2 = await statCached(ctx, filePath);
|
|
3895
|
+
if (!stat2 || stat2.isDirectory()) {
|
|
3896
|
+
throw createApplyPatchVerificationError(`Failed to read file to ${verb}: ${filePath}`);
|
|
3897
|
+
}
|
|
3898
|
+
}
|
|
3899
|
+
function collectPatchTargets(root, hunks) {
|
|
3900
|
+
const targets = new Set;
|
|
3901
|
+
for (const hunk of hunks) {
|
|
3902
|
+
targets.add(path3.resolve(root, hunk.path));
|
|
3903
|
+
if (hunk.type === "update" && hunk.move_path) {
|
|
3904
|
+
targets.add(path3.resolve(root, hunk.move_path));
|
|
3905
|
+
}
|
|
3906
|
+
}
|
|
3907
|
+
return [...targets];
|
|
3908
|
+
}
|
|
3909
|
+
function validatePatchPaths(hunks) {
|
|
3910
|
+
for (const hunk of hunks) {
|
|
3911
|
+
if (path3.isAbsolute(hunk.path)) {
|
|
3912
|
+
throw createApplyPatchValidationError(`absolute patch paths are not allowed: ${hunk.path}`);
|
|
3913
|
+
}
|
|
3914
|
+
if (hunk.type === "update" && hunk.move_path && path3.isAbsolute(hunk.move_path)) {
|
|
3915
|
+
throw createApplyPatchValidationError(`absolute patch paths are not allowed: ${hunk.move_path}`);
|
|
3916
|
+
}
|
|
3917
|
+
}
|
|
3918
|
+
}
|
|
3919
|
+
function toPortablePatchPath(filePath) {
|
|
3920
|
+
return filePath.split(path3.sep).join("/");
|
|
3921
|
+
}
|
|
3922
|
+
function toRelativePatchPath(root, target) {
|
|
3923
|
+
const relative = path3.relative(root, target);
|
|
3924
|
+
return toPortablePatchPath(relative.length === 0 ? path3.basename(target) : relative);
|
|
3925
|
+
}
|
|
3926
|
+
async function normalizeAbsolutePatchPath(root, worktree, value) {
|
|
3927
|
+
if (!path3.isAbsolute(value)) {
|
|
3928
|
+
return value;
|
|
3929
|
+
}
|
|
3930
|
+
const guardContext = createPathGuardContext(root, worktree);
|
|
3931
|
+
const target = path3.resolve(value);
|
|
3932
|
+
await guard(guardContext, target);
|
|
3933
|
+
const [rootReal, targetReal] = await Promise.all([
|
|
3934
|
+
guardContext.rootReal,
|
|
3935
|
+
realCached(guardContext, target)
|
|
3936
|
+
]);
|
|
3937
|
+
if (!inside(rootReal, targetReal)) {
|
|
3938
|
+
throw createApplyPatchBlockedError(`patch contains path outside workspace root: ${target}`);
|
|
3939
|
+
}
|
|
3940
|
+
return toRelativePatchPath(root, target);
|
|
3941
|
+
}
|
|
3942
|
+
async function normalizeAbsolutePatchPaths(root, worktree, hunks) {
|
|
3943
|
+
const normalized = [];
|
|
3944
|
+
let changed = false;
|
|
3945
|
+
for (const hunk of hunks) {
|
|
3946
|
+
const normalizedPath = await normalizeAbsolutePatchPath(root, worktree, hunk.path);
|
|
3947
|
+
if (hunk.type !== "update") {
|
|
3948
|
+
changed ||= normalizedPath !== hunk.path;
|
|
3949
|
+
normalized.push(normalizedPath === hunk.path ? hunk : {
|
|
3950
|
+
...hunk,
|
|
3951
|
+
path: normalizedPath
|
|
3952
|
+
});
|
|
3953
|
+
continue;
|
|
3954
|
+
}
|
|
3955
|
+
const normalizedMovePath = hunk.move_path ? await normalizeAbsolutePatchPath(root, worktree, hunk.move_path) : undefined;
|
|
3956
|
+
changed ||= normalizedPath !== hunk.path || normalizedMovePath !== hunk.move_path;
|
|
3957
|
+
normalized.push(normalizedPath === hunk.path && normalizedMovePath === hunk.move_path ? hunk : {
|
|
3958
|
+
...hunk,
|
|
3959
|
+
path: normalizedPath,
|
|
3960
|
+
move_path: normalizedMovePath
|
|
3961
|
+
});
|
|
3962
|
+
}
|
|
3963
|
+
return { hunks: normalized, changed };
|
|
3964
|
+
}
|
|
3965
|
+
async function guardPatchTargets(root, worktree, targets) {
|
|
3966
|
+
const guardContext = createPathGuardContext(root, worktree);
|
|
3967
|
+
for (const target of targets) {
|
|
3968
|
+
await guard(guardContext, target);
|
|
3969
|
+
}
|
|
3970
|
+
return targets.length;
|
|
3971
|
+
}
|
|
3972
|
+
async function parseValidatedPatch(root, patchText, worktree) {
|
|
3973
|
+
let hunks;
|
|
3974
|
+
try {
|
|
3975
|
+
hunks = parsePatchStrict(patchText).hunks;
|
|
3976
|
+
} catch (error) {
|
|
3977
|
+
throw createApplyPatchValidationError(getErrorMessage(error));
|
|
3978
|
+
}
|
|
3979
|
+
if (hunks.length === 0) {
|
|
3980
|
+
const clean = patchText.replace(/\r\n/g, `
|
|
3981
|
+
`).replace(/\r/g, `
|
|
3982
|
+
`).trim();
|
|
3983
|
+
if (clean === `*** Begin Patch
|
|
3984
|
+
*** End Patch`) {
|
|
3985
|
+
throw createApplyPatchValidationError("empty patch");
|
|
3986
|
+
}
|
|
3987
|
+
throw createApplyPatchValidationError("no hunks found");
|
|
3988
|
+
}
|
|
3989
|
+
const normalizedPatch = await normalizeAbsolutePatchPaths(root, worktree, hunks);
|
|
3990
|
+
validatePatchPaths(normalizedPatch.hunks);
|
|
3991
|
+
return {
|
|
3992
|
+
hunks: normalizedPatch.hunks,
|
|
3993
|
+
pathsNormalized: normalizedPatch.changed
|
|
3994
|
+
};
|
|
3995
|
+
}
|
|
3996
|
+
async function readPreparedFileText(filePath, verb) {
|
|
3997
|
+
try {
|
|
3998
|
+
return await fs3.readFile(filePath, "utf-8");
|
|
3999
|
+
} catch (error) {
|
|
4000
|
+
if (isMissingPathError(error)) {
|
|
4001
|
+
throw createApplyPatchVerificationError(`Failed to read file to ${verb}: ${filePath}`);
|
|
4002
|
+
}
|
|
4003
|
+
throw createApplyPatchInternalError(`Failed to read file for patch verification: ${filePath}`, error);
|
|
4004
|
+
}
|
|
4005
|
+
}
|
|
4006
|
+
async function createPatchExecutionContext(root, patchText, worktree) {
|
|
4007
|
+
const { hunks, pathsNormalized } = await parseValidatedPatch(root, patchText, worktree);
|
|
4008
|
+
await guardPatchTargets(root, worktree, collectPatchTargets(root, hunks));
|
|
4009
|
+
const files = createFileCacheContext();
|
|
4010
|
+
const staged = new Map;
|
|
4011
|
+
async function assertPreparedPathMissing(filePath, verb) {
|
|
4012
|
+
const existing = staged.get(filePath);
|
|
4013
|
+
if (existing) {
|
|
4014
|
+
if (!existing.exists) {
|
|
4015
|
+
return;
|
|
4016
|
+
}
|
|
4017
|
+
throw createApplyPatchVerificationError(verb === "add" ? `Add File target already exists: ${filePath}` : `Move destination already exists: ${filePath}`);
|
|
4018
|
+
}
|
|
4019
|
+
const stat2 = await statCached(files, filePath);
|
|
4020
|
+
if (!stat2) {
|
|
4021
|
+
return;
|
|
4022
|
+
}
|
|
4023
|
+
throw createApplyPatchVerificationError(verb === "add" ? `Add File target already exists: ${filePath}` : `Move destination already exists: ${filePath}`);
|
|
4024
|
+
}
|
|
4025
|
+
async function getPreparedFileState(filePath, verb) {
|
|
4026
|
+
const existing = staged.get(filePath);
|
|
4027
|
+
if (existing) {
|
|
4028
|
+
if (!existing.exists) {
|
|
4029
|
+
throw createApplyPatchVerificationError(`Failed to read file to ${verb}: ${filePath}`);
|
|
4030
|
+
}
|
|
4031
|
+
return existing;
|
|
4032
|
+
}
|
|
4033
|
+
await assertRegularFile(files, filePath, verb);
|
|
4034
|
+
const stat2 = await statCached(files, filePath);
|
|
4035
|
+
const text = await readPreparedFileText(filePath, verb);
|
|
4036
|
+
const state = {
|
|
4037
|
+
exists: true,
|
|
4038
|
+
text,
|
|
4039
|
+
mode: stat2 ? stat2.mode & 4095 : undefined,
|
|
4040
|
+
derived: false
|
|
4041
|
+
};
|
|
4042
|
+
staged.set(filePath, state);
|
|
4043
|
+
return state;
|
|
4044
|
+
}
|
|
4045
|
+
return {
|
|
4046
|
+
hunks,
|
|
4047
|
+
pathsNormalized,
|
|
4048
|
+
staged,
|
|
4049
|
+
getPreparedFileState,
|
|
4050
|
+
assertPreparedPathMissing
|
|
4051
|
+
};
|
|
4052
|
+
}
|
|
4053
|
+
function resolvePreparedUpdate(filePath, currentText, hunk, cfg) {
|
|
4054
|
+
try {
|
|
4055
|
+
const { lines, resolved, eol, hasFinalNewline } = resolveUpdateChunksFromText(filePath, currentText, hunk.chunks, cfg);
|
|
4056
|
+
return {
|
|
4057
|
+
resolved,
|
|
4058
|
+
nextText: applyHits(lines, resolved.map((chunk) => chunk.hit), eol, hasFinalNewline)
|
|
4059
|
+
};
|
|
4060
|
+
} catch (error) {
|
|
4061
|
+
throw createApplyPatchVerificationError(getErrorMessage(error), error);
|
|
4062
|
+
}
|
|
4063
|
+
}
|
|
4064
|
+
function stageAddedText(contents) {
|
|
4065
|
+
return contents.length === 0 || contents.endsWith(`
|
|
4066
|
+
`) ? contents : `${contents}
|
|
4067
|
+
`;
|
|
4068
|
+
}
|
|
4069
|
+
// src/hooks/apply-patch/rewrite.ts
|
|
4070
|
+
import path4 from "path";
|
|
4071
|
+
function normalizeTextLineEndings(text) {
|
|
4072
|
+
return text.replace(/\r\n/g, `
|
|
4073
|
+
`).replace(/\r/g, `
|
|
4074
|
+
`);
|
|
4075
|
+
}
|
|
4076
|
+
function splitPatchTextLines(text) {
|
|
4077
|
+
const normalized = normalizeTextLineEndings(text);
|
|
4078
|
+
const lines = normalized.split(`
|
|
4079
|
+
`);
|
|
4080
|
+
if (normalized.endsWith(`
|
|
4081
|
+
`)) {
|
|
4082
|
+
lines.pop();
|
|
4083
|
+
}
|
|
4084
|
+
return lines;
|
|
4085
|
+
}
|
|
4086
|
+
function createCollapsedUpdateHunk(pathValue, filePath, baseText, finalText, cfg, movePath) {
|
|
4087
|
+
const collapsedChunk = {
|
|
4088
|
+
old_lines: splitPatchTextLines(baseText),
|
|
4089
|
+
new_lines: splitPatchTextLines(finalText),
|
|
4090
|
+
change_context: undefined,
|
|
4091
|
+
is_end_of_file: true
|
|
4092
|
+
};
|
|
4093
|
+
const minimizedChunk = minimizeMergedChunk(collapsedChunk);
|
|
4094
|
+
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 : (() => {
|
|
4095
|
+
try {
|
|
4096
|
+
return deriveNewContentFromText(filePath, baseText, [minimizedChunk], cfg) === finalText ? minimizedChunk : collapsedChunk;
|
|
4097
|
+
} catch {
|
|
4098
|
+
return collapsedChunk;
|
|
4099
|
+
}
|
|
4100
|
+
})();
|
|
4101
|
+
return {
|
|
4102
|
+
type: "update",
|
|
4103
|
+
path: pathValue,
|
|
4104
|
+
move_path: movePath,
|
|
4105
|
+
chunks: [chunk]
|
|
4106
|
+
};
|
|
4107
|
+
}
|
|
4108
|
+
function clonePatchChunks(chunks) {
|
|
4109
|
+
return chunks.map((chunk) => ({
|
|
4110
|
+
old_lines: [...chunk.old_lines],
|
|
4111
|
+
new_lines: [...chunk.new_lines],
|
|
4112
|
+
change_context: chunk.change_context,
|
|
4113
|
+
is_end_of_file: chunk.is_end_of_file
|
|
4114
|
+
}));
|
|
4115
|
+
}
|
|
4116
|
+
function minimizeMergedChunk(chunk) {
|
|
4117
|
+
if (chunk.old_lines.length === 0 && chunk.new_lines.length === 0) {
|
|
4118
|
+
return {
|
|
4119
|
+
old_lines: [],
|
|
4120
|
+
new_lines: [],
|
|
4121
|
+
change_context: chunk.change_context,
|
|
4122
|
+
is_end_of_file: chunk.is_end_of_file
|
|
4123
|
+
};
|
|
4124
|
+
}
|
|
4125
|
+
let prefixLength = 0;
|
|
4126
|
+
while (prefixLength < chunk.old_lines.length && prefixLength < chunk.new_lines.length && chunk.old_lines[prefixLength] === chunk.new_lines[prefixLength]) {
|
|
4127
|
+
prefixLength += 1;
|
|
4128
|
+
}
|
|
4129
|
+
let suffixLength = 0;
|
|
4130
|
+
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]) {
|
|
4131
|
+
suffixLength += 1;
|
|
4132
|
+
}
|
|
4133
|
+
if (prefixLength === 0 && suffixLength === 0) {
|
|
4134
|
+
return {
|
|
4135
|
+
old_lines: [...chunk.old_lines],
|
|
4136
|
+
new_lines: [...chunk.new_lines],
|
|
4137
|
+
change_context: chunk.change_context,
|
|
4138
|
+
is_end_of_file: chunk.is_end_of_file
|
|
4139
|
+
};
|
|
4140
|
+
}
|
|
4141
|
+
return {
|
|
4142
|
+
old_lines: chunk.old_lines.slice(prefixLength, chunk.old_lines.length - suffixLength),
|
|
4143
|
+
new_lines: chunk.new_lines.slice(prefixLength, chunk.new_lines.length - suffixLength),
|
|
4144
|
+
change_context: prefixLength > 0 ? chunk.old_lines[prefixLength - 1] : chunk.change_context,
|
|
4145
|
+
is_end_of_file: chunk.is_end_of_file && suffixLength === 0 ? true : undefined
|
|
4146
|
+
};
|
|
4147
|
+
}
|
|
4148
|
+
function createUpdateHunk(pathValue, chunks, movePath) {
|
|
4149
|
+
return {
|
|
4150
|
+
type: "update",
|
|
4151
|
+
path: pathValue,
|
|
4152
|
+
move_path: movePath,
|
|
4153
|
+
chunks: clonePatchChunks(chunks)
|
|
4154
|
+
};
|
|
4155
|
+
}
|
|
4156
|
+
function mergeSameFileUpdateGroupChunks(filePath, group, nextChunks, finalText, cfg) {
|
|
4157
|
+
if (!group.chunks) {
|
|
4158
|
+
return;
|
|
4159
|
+
}
|
|
4160
|
+
const mergedChunks = [
|
|
4161
|
+
...clonePatchChunks(group.chunks).map(minimizeMergedChunk),
|
|
4162
|
+
...clonePatchChunks(nextChunks).map(minimizeMergedChunk)
|
|
4163
|
+
];
|
|
4164
|
+
try {
|
|
4165
|
+
const mergedText = deriveNewContentFromText(filePath, group.baseText, mergedChunks, cfg);
|
|
4166
|
+
return mergedText === finalText ? mergedChunks : undefined;
|
|
4167
|
+
} catch {
|
|
4168
|
+
return;
|
|
4169
|
+
}
|
|
4170
|
+
}
|
|
4171
|
+
function addContentsFromFinalText(text) {
|
|
4172
|
+
return text.endsWith(`
|
|
4173
|
+
`) ? text.slice(0, -1) : text;
|
|
4174
|
+
}
|
|
4175
|
+
function renderRewriteDependencyGroup(group, cfg) {
|
|
4176
|
+
if (group.kind === "add") {
|
|
4177
|
+
return {
|
|
4178
|
+
type: "add",
|
|
4179
|
+
path: group.group.outputPath,
|
|
4180
|
+
contents: addContentsFromFinalText(group.group.finalText)
|
|
4181
|
+
};
|
|
4182
|
+
}
|
|
4183
|
+
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);
|
|
4184
|
+
}
|
|
4185
|
+
function rewriteModeForDependentUpdate(group) {
|
|
4186
|
+
if (group.kind === "add") {
|
|
4187
|
+
return "collapse:add-followed-by-update";
|
|
4188
|
+
}
|
|
4189
|
+
if (group.group.outputPath !== group.group.sourcePath) {
|
|
4190
|
+
return "collapse:move-followed-by-update";
|
|
4191
|
+
}
|
|
4192
|
+
return "merge:same-file-updates";
|
|
4193
|
+
}
|
|
4194
|
+
function combineDependentUpdateGroup(filePath, group, nextChunks, finalText, nextOutputPath, nextOutputFilePath, cfg) {
|
|
4195
|
+
if (group.kind === "add") {
|
|
4196
|
+
return {
|
|
4197
|
+
kind: "add",
|
|
4198
|
+
group: {
|
|
4199
|
+
...group.group,
|
|
4200
|
+
outputPath: nextOutputPath,
|
|
4201
|
+
outputFilePath: nextOutputFilePath,
|
|
4202
|
+
finalText
|
|
4203
|
+
}
|
|
4204
|
+
};
|
|
4205
|
+
}
|
|
4206
|
+
const mergedChunks = group.group.outputFilePath === filePath && group.group.sourceFilePath === filePath && nextOutputFilePath === filePath ? mergeSameFileUpdateGroupChunks(filePath, group.group, nextChunks, finalText, cfg) : undefined;
|
|
4207
|
+
return {
|
|
4208
|
+
kind: "update",
|
|
4209
|
+
group: {
|
|
4210
|
+
...group.group,
|
|
4211
|
+
outputPath: nextOutputPath,
|
|
4212
|
+
outputFilePath: nextOutputFilePath,
|
|
4213
|
+
finalText,
|
|
4214
|
+
chunks: mergedChunks
|
|
4215
|
+
}
|
|
4216
|
+
};
|
|
4217
|
+
}
|
|
4218
|
+
async function rewritePatch(root, patchText, cfg, worktree) {
|
|
4219
|
+
try {
|
|
4220
|
+
let clearDependencyGroup = function(filePath) {
|
|
4221
|
+
dependencyGroups.delete(filePath);
|
|
4222
|
+
};
|
|
4223
|
+
const {
|
|
4224
|
+
hunks,
|
|
4225
|
+
pathsNormalized,
|
|
4226
|
+
staged,
|
|
4227
|
+
getPreparedFileState,
|
|
4228
|
+
assertPreparedPathMissing
|
|
4229
|
+
} = await createPatchExecutionContext(root, patchText, worktree);
|
|
4230
|
+
const normalizedPatchText = normalizePatchText(patchText);
|
|
4231
|
+
const rewritten = [];
|
|
4232
|
+
let changed = false;
|
|
4233
|
+
let rewrittenChunks = 0;
|
|
4234
|
+
const rewriteModes = new Set;
|
|
4235
|
+
const totalChunks = hunks.reduce((count, hunk) => count + (hunk.type === "update" ? hunk.chunks.length : 0), 0);
|
|
4236
|
+
const dependencyGroups = new Map;
|
|
4237
|
+
for (const hunk of hunks) {
|
|
4238
|
+
if (hunk.type === "add") {
|
|
4239
|
+
const filePath2 = path4.resolve(root, hunk.path);
|
|
4240
|
+
await assertPreparedPathMissing(filePath2, "add");
|
|
4241
|
+
rewritten.push(hunk);
|
|
4242
|
+
clearDependencyGroup(filePath2);
|
|
4243
|
+
const finalText = stageAddedText(hunk.contents);
|
|
4244
|
+
staged.set(filePath2, {
|
|
4245
|
+
exists: true,
|
|
4246
|
+
text: finalText,
|
|
4247
|
+
derived: true
|
|
4248
|
+
});
|
|
4249
|
+
dependencyGroups.set(filePath2, {
|
|
4250
|
+
kind: "add",
|
|
4251
|
+
group: {
|
|
4252
|
+
index: rewritten.length - 1,
|
|
4253
|
+
outputPath: hunk.path,
|
|
4254
|
+
outputFilePath: filePath2,
|
|
4255
|
+
finalText
|
|
4256
|
+
}
|
|
4257
|
+
});
|
|
4258
|
+
continue;
|
|
4259
|
+
}
|
|
4260
|
+
if (hunk.type === "delete") {
|
|
4261
|
+
const filePath2 = path4.resolve(root, hunk.path);
|
|
4262
|
+
await getPreparedFileState(filePath2, "delete");
|
|
4263
|
+
clearDependencyGroup(filePath2);
|
|
4264
|
+
rewritten.push(hunk);
|
|
4265
|
+
staged.set(filePath2, { exists: false, derived: true });
|
|
4266
|
+
continue;
|
|
4267
|
+
}
|
|
4268
|
+
const filePath = path4.resolve(root, hunk.path);
|
|
4269
|
+
const currentDependency = dependencyGroups.get(filePath);
|
|
4270
|
+
const current = await getPreparedFileState(filePath, "update");
|
|
4271
|
+
if (!current.exists) {
|
|
4272
|
+
throw createApplyPatchVerificationError(`Failed to read file to update: ${filePath}`);
|
|
4273
|
+
}
|
|
4274
|
+
const movePath = hunk.move_path ? path4.resolve(root, hunk.move_path) : undefined;
|
|
4275
|
+
if (movePath && movePath !== filePath) {
|
|
4276
|
+
await assertPreparedPathMissing(movePath, "move");
|
|
4277
|
+
}
|
|
4278
|
+
const { resolved, nextText } = resolvePreparedUpdate(filePath, current.text, hunk, cfg);
|
|
4279
|
+
const next = resolved.map((chunk, index) => ({
|
|
4280
|
+
old_lines: [...chunk.canonical_old_lines],
|
|
4281
|
+
new_lines: [...chunk.canonical_new_lines],
|
|
4282
|
+
change_context: chunk.canonical_change_context ?? hunk.chunks[index].change_context,
|
|
4283
|
+
is_end_of_file: hunk.chunks[index].is_end_of_file && chunk.resolved_is_end_of_file ? true : undefined
|
|
4284
|
+
}));
|
|
4285
|
+
for (const chunk of resolved) {
|
|
4286
|
+
if (!chunk.rewritten) {
|
|
4287
|
+
continue;
|
|
4288
|
+
}
|
|
4289
|
+
changed = true;
|
|
4290
|
+
rewrittenChunks += 1;
|
|
4291
|
+
if (chunk.strategy) {
|
|
4292
|
+
rewriteModes.add(chunk.strategy);
|
|
4293
|
+
continue;
|
|
4294
|
+
}
|
|
4295
|
+
if (chunk.matchComparator && chunk.matchComparator !== "exact") {
|
|
4296
|
+
rewriteModes.add(`match:${chunk.matchComparator}`);
|
|
4297
|
+
}
|
|
4298
|
+
}
|
|
4299
|
+
const nextOutputPath = hunk.move_path ?? hunk.path;
|
|
4300
|
+
const nextOutputFilePath = movePath ?? filePath;
|
|
4301
|
+
if (current.derived && currentDependency) {
|
|
4302
|
+
const nextGroup = combineDependentUpdateGroup(filePath, currentDependency, next, nextText, nextOutputPath, nextOutputFilePath, cfg);
|
|
4303
|
+
rewritten[currentDependency.group.index] = renderRewriteDependencyGroup(nextGroup, cfg);
|
|
4304
|
+
changed = true;
|
|
4305
|
+
rewriteModes.add(rewriteModeForDependentUpdate(currentDependency));
|
|
4306
|
+
clearDependencyGroup(filePath);
|
|
4307
|
+
if (movePath && movePath !== filePath) {
|
|
4308
|
+
clearDependencyGroup(movePath);
|
|
4309
|
+
}
|
|
4310
|
+
dependencyGroups.set(nextOutputFilePath, nextGroup);
|
|
4311
|
+
} else {
|
|
4312
|
+
rewritten.push(createUpdateHunk(hunk.path, next, hunk.move_path));
|
|
4313
|
+
clearDependencyGroup(filePath);
|
|
4314
|
+
if (movePath && movePath !== filePath) {
|
|
4315
|
+
clearDependencyGroup(movePath);
|
|
4316
|
+
}
|
|
4317
|
+
dependencyGroups.set(nextOutputFilePath, {
|
|
4318
|
+
kind: "update",
|
|
4319
|
+
group: {
|
|
4320
|
+
index: rewritten.length - 1,
|
|
4321
|
+
sourcePath: hunk.path,
|
|
4322
|
+
outputPath: nextOutputPath,
|
|
4323
|
+
sourceFilePath: filePath,
|
|
4324
|
+
outputFilePath: nextOutputFilePath,
|
|
4325
|
+
baseText: current.text,
|
|
4326
|
+
finalText: nextText,
|
|
4327
|
+
chunks: clonePatchChunks(next)
|
|
4328
|
+
}
|
|
4329
|
+
});
|
|
4330
|
+
}
|
|
4331
|
+
if (movePath && movePath !== filePath) {
|
|
4332
|
+
staged.set(filePath, { exists: false, derived: true });
|
|
4333
|
+
staged.set(movePath, {
|
|
4334
|
+
exists: true,
|
|
4335
|
+
text: nextText,
|
|
4336
|
+
mode: current.mode,
|
|
4337
|
+
derived: true
|
|
4338
|
+
});
|
|
4339
|
+
} else {
|
|
4340
|
+
staged.set(filePath, {
|
|
4341
|
+
exists: true,
|
|
4342
|
+
text: nextText,
|
|
4343
|
+
mode: current.mode,
|
|
4344
|
+
derived: true
|
|
4345
|
+
});
|
|
4346
|
+
}
|
|
4347
|
+
}
|
|
4348
|
+
if (!changed) {
|
|
4349
|
+
if (pathsNormalized) {
|
|
4350
|
+
return {
|
|
4351
|
+
patchText: formatPatch({ hunks }),
|
|
4352
|
+
changed: true,
|
|
4353
|
+
rewrittenChunks: 0,
|
|
4354
|
+
totalChunks,
|
|
4355
|
+
rewriteModes: ["normalize:patch-paths"]
|
|
4356
|
+
};
|
|
4357
|
+
}
|
|
4358
|
+
if (normalizedPatchText !== patchText) {
|
|
4359
|
+
return {
|
|
4360
|
+
patchText: normalizedPatchText,
|
|
4361
|
+
changed: true,
|
|
4362
|
+
rewrittenChunks: 0,
|
|
4363
|
+
totalChunks,
|
|
4364
|
+
rewriteModes: ["normalize:patch-text"]
|
|
4365
|
+
};
|
|
4366
|
+
}
|
|
4367
|
+
return {
|
|
4368
|
+
patchText,
|
|
4369
|
+
changed: false,
|
|
4370
|
+
rewrittenChunks: 0,
|
|
4371
|
+
totalChunks,
|
|
4372
|
+
rewriteModes: []
|
|
4373
|
+
};
|
|
4374
|
+
}
|
|
4375
|
+
return {
|
|
4376
|
+
patchText: formatPatch({ hunks: rewritten }),
|
|
4377
|
+
changed: true,
|
|
4378
|
+
rewrittenChunks,
|
|
4379
|
+
totalChunks,
|
|
4380
|
+
rewriteModes: [...rewriteModes].sort()
|
|
4381
|
+
};
|
|
4382
|
+
} catch (error) {
|
|
4383
|
+
throw ensureApplyPatchError(error, "Unexpected rewrite failure");
|
|
4384
|
+
}
|
|
4385
|
+
}
|
|
4386
|
+
// src/hooks/apply-patch/index.ts
|
|
4387
|
+
var APPLY_PATCH_RESCUE_OPTIONS = {
|
|
4388
|
+
prefixSuffix: true,
|
|
4389
|
+
lcsRescue: true
|
|
4390
|
+
};
|
|
4391
|
+
function createApplyPatchHook(ctx) {
|
|
4392
|
+
function logHookStatus(state, data) {
|
|
4393
|
+
log(`apply-patch hook ${state}`, data);
|
|
4394
|
+
}
|
|
4395
|
+
return {
|
|
4396
|
+
"tool.execute.before": async (input, output) => {
|
|
4397
|
+
if (input.tool !== "apply_patch") {
|
|
4398
|
+
return;
|
|
4399
|
+
}
|
|
4400
|
+
if (typeof output.args?.patchText !== "string") {
|
|
4401
|
+
return;
|
|
4402
|
+
}
|
|
4403
|
+
const root = input.directory || ctx.directory || process.cwd();
|
|
4404
|
+
const worktree = ctx.worktree || root;
|
|
4405
|
+
try {
|
|
4406
|
+
const result = await rewritePatch(root, output.args.patchText, APPLY_PATCH_RESCUE_OPTIONS, worktree);
|
|
4407
|
+
if (result.changed) {
|
|
4408
|
+
output.args.patchText = result.patchText;
|
|
4409
|
+
logHookStatus("rewrite", {
|
|
4410
|
+
rewrittenChunks: result.rewrittenChunks,
|
|
4411
|
+
totalChunks: result.totalChunks,
|
|
4412
|
+
strategies: result.rewriteModes
|
|
4413
|
+
});
|
|
4414
|
+
return;
|
|
4415
|
+
}
|
|
4416
|
+
logHookStatus("unchanged", {
|
|
4417
|
+
rewrittenChunks: 0,
|
|
4418
|
+
totalChunks: result.totalChunks
|
|
4419
|
+
});
|
|
4420
|
+
return;
|
|
4421
|
+
} catch (error) {
|
|
4422
|
+
const normalizedError = isApplyPatchError(error) ? error : createApplyPatchInternalError(`Unexpected hook failure before native apply: ${error instanceof Error ? error.message : String(error)}`, error);
|
|
4423
|
+
const details = getApplyPatchErrorDetails(normalizedError);
|
|
4424
|
+
logHookStatus(isApplyPatchVerificationError(normalizedError) ? "verification" : normalizedError.kind === "validation" ? "validation" : normalizedError.kind === "internal" ? "internal" : "blocked", {
|
|
4425
|
+
kind: details?.kind ?? "internal",
|
|
4426
|
+
code: details?.code ?? "internal_unexpected",
|
|
4427
|
+
reason: normalizedError.message,
|
|
4428
|
+
failOpen: false,
|
|
4429
|
+
rescueOptions: APPLY_PATCH_RESCUE_OPTIONS,
|
|
4430
|
+
rewriteStage: "before-native"
|
|
4431
|
+
});
|
|
4432
|
+
throw normalizedError;
|
|
4433
|
+
}
|
|
4434
|
+
}
|
|
4435
|
+
};
|
|
4436
|
+
}
|
|
2959
4437
|
// src/hooks/auto-update-checker/cache.ts
|
|
2960
|
-
import * as
|
|
2961
|
-
import * as
|
|
4438
|
+
import * as fs4 from "fs";
|
|
4439
|
+
import * as path6 from "path";
|
|
2962
4440
|
// src/hooks/auto-update-checker/constants.ts
|
|
2963
4441
|
import * as os2 from "os";
|
|
2964
|
-
import * as
|
|
4442
|
+
import * as path5 from "path";
|
|
2965
4443
|
var PACKAGE_NAME = "oh-my-opencode-slim";
|
|
2966
4444
|
var NPM_REGISTRY_URL = `https://registry.npmjs.org/-/package/${PACKAGE_NAME}/dist-tags`;
|
|
2967
4445
|
var NPM_FETCH_TIMEOUT = 5000;
|
|
2968
4446
|
function getCacheDir() {
|
|
2969
4447
|
if (process.platform === "win32") {
|
|
2970
|
-
return
|
|
4448
|
+
return path5.join(process.env.LOCALAPPDATA ?? os2.homedir(), "opencode");
|
|
2971
4449
|
}
|
|
2972
|
-
return
|
|
4450
|
+
return path5.join(os2.homedir(), ".cache", "opencode");
|
|
2973
4451
|
}
|
|
2974
4452
|
var CACHE_DIR = getCacheDir();
|
|
2975
|
-
var INSTALLED_PACKAGE_JSON =
|
|
4453
|
+
var INSTALLED_PACKAGE_JSON = path5.join(CACHE_DIR, "node_modules", PACKAGE_NAME, "package.json");
|
|
2976
4454
|
var configPaths = getOpenCodeConfigPaths();
|
|
2977
4455
|
var USER_OPENCODE_CONFIG = configPaths[0];
|
|
2978
4456
|
var USER_OPENCODE_CONFIG_JSONC = configPaths[1];
|
|
2979
4457
|
|
|
2980
4458
|
// src/hooks/auto-update-checker/cache.ts
|
|
2981
4459
|
function removeFromBunLock(packageName) {
|
|
2982
|
-
const lockPath =
|
|
2983
|
-
if (!
|
|
4460
|
+
const lockPath = path6.join(CACHE_DIR, "bun.lock");
|
|
4461
|
+
if (!fs4.existsSync(lockPath))
|
|
2984
4462
|
return false;
|
|
2985
4463
|
try {
|
|
2986
|
-
const content =
|
|
4464
|
+
const content = fs4.readFileSync(lockPath, "utf-8");
|
|
2987
4465
|
let lock;
|
|
2988
4466
|
try {
|
|
2989
4467
|
lock = JSON.parse(stripJsonComments(content));
|
|
@@ -3000,7 +4478,7 @@ function removeFromBunLock(packageName) {
|
|
|
3000
4478
|
modified = true;
|
|
3001
4479
|
}
|
|
3002
4480
|
if (modified) {
|
|
3003
|
-
|
|
4481
|
+
fs4.writeFileSync(lockPath, JSON.stringify(lock, null, 2));
|
|
3004
4482
|
log(`[auto-update-checker] Removed from bun.lock: ${packageName}`);
|
|
3005
4483
|
}
|
|
3006
4484
|
return modified;
|
|
@@ -3011,23 +4489,23 @@ function removeFromBunLock(packageName) {
|
|
|
3011
4489
|
}
|
|
3012
4490
|
function invalidatePackage(packageName = PACKAGE_NAME) {
|
|
3013
4491
|
try {
|
|
3014
|
-
const pkgDir =
|
|
3015
|
-
const pkgJsonPath =
|
|
4492
|
+
const pkgDir = path6.join(CACHE_DIR, "node_modules", packageName);
|
|
4493
|
+
const pkgJsonPath = path6.join(CACHE_DIR, "package.json");
|
|
3016
4494
|
let packageRemoved = false;
|
|
3017
4495
|
let dependencyRemoved = false;
|
|
3018
4496
|
let lockRemoved = false;
|
|
3019
|
-
if (
|
|
3020
|
-
|
|
4497
|
+
if (fs4.existsSync(pkgDir)) {
|
|
4498
|
+
fs4.rmSync(pkgDir, { recursive: true, force: true });
|
|
3021
4499
|
log(`[auto-update-checker] Package removed: ${pkgDir}`);
|
|
3022
4500
|
packageRemoved = true;
|
|
3023
4501
|
}
|
|
3024
|
-
if (
|
|
4502
|
+
if (fs4.existsSync(pkgJsonPath)) {
|
|
3025
4503
|
try {
|
|
3026
|
-
const content =
|
|
4504
|
+
const content = fs4.readFileSync(pkgJsonPath, "utf-8");
|
|
3027
4505
|
const pkgJson = JSON.parse(stripJsonComments(content));
|
|
3028
4506
|
if (pkgJson.dependencies?.[packageName]) {
|
|
3029
4507
|
delete pkgJson.dependencies[packageName];
|
|
3030
|
-
|
|
4508
|
+
fs4.writeFileSync(pkgJsonPath, JSON.stringify(pkgJson, null, 2));
|
|
3031
4509
|
log(`[auto-update-checker] Dependency removed from package.json: ${packageName}`);
|
|
3032
4510
|
dependencyRemoved = true;
|
|
3033
4511
|
}
|
|
@@ -3048,8 +4526,8 @@ function invalidatePackage(packageName = PACKAGE_NAME) {
|
|
|
3048
4526
|
}
|
|
3049
4527
|
|
|
3050
4528
|
// src/hooks/auto-update-checker/checker.ts
|
|
3051
|
-
import * as
|
|
3052
|
-
import * as
|
|
4529
|
+
import * as fs5 from "fs";
|
|
4530
|
+
import * as path7 from "path";
|
|
3053
4531
|
import { fileURLToPath } from "url";
|
|
3054
4532
|
function isPrereleaseVersion(version) {
|
|
3055
4533
|
return version.includes("-");
|
|
@@ -3074,8 +4552,8 @@ function extractChannel(version) {
|
|
|
3074
4552
|
}
|
|
3075
4553
|
function getConfigPaths(directory) {
|
|
3076
4554
|
return [
|
|
3077
|
-
|
|
3078
|
-
|
|
4555
|
+
path7.join(directory, ".opencode", "opencode.json"),
|
|
4556
|
+
path7.join(directory, ".opencode", "opencode.jsonc"),
|
|
3079
4557
|
USER_OPENCODE_CONFIG,
|
|
3080
4558
|
USER_OPENCODE_CONFIG_JSONC
|
|
3081
4559
|
];
|
|
@@ -3083,9 +4561,9 @@ function getConfigPaths(directory) {
|
|
|
3083
4561
|
function getLocalDevPath(directory) {
|
|
3084
4562
|
for (const configPath of getConfigPaths(directory)) {
|
|
3085
4563
|
try {
|
|
3086
|
-
if (!
|
|
4564
|
+
if (!fs5.existsSync(configPath))
|
|
3087
4565
|
continue;
|
|
3088
|
-
const content =
|
|
4566
|
+
const content = fs5.readFileSync(configPath, "utf-8");
|
|
3089
4567
|
const config = JSON.parse(stripJsonComments(content));
|
|
3090
4568
|
const plugins = config.plugin ?? [];
|
|
3091
4569
|
for (const entry of plugins) {
|
|
@@ -3103,19 +4581,19 @@ function getLocalDevPath(directory) {
|
|
|
3103
4581
|
}
|
|
3104
4582
|
function findPackageJsonUp(startPath) {
|
|
3105
4583
|
try {
|
|
3106
|
-
const
|
|
3107
|
-
let dir =
|
|
4584
|
+
const stat2 = fs5.statSync(startPath);
|
|
4585
|
+
let dir = stat2.isDirectory() ? startPath : path7.dirname(startPath);
|
|
3108
4586
|
for (let i = 0;i < 10; i++) {
|
|
3109
|
-
const pkgPath =
|
|
3110
|
-
if (
|
|
4587
|
+
const pkgPath = path7.join(dir, "package.json");
|
|
4588
|
+
if (fs5.existsSync(pkgPath)) {
|
|
3111
4589
|
try {
|
|
3112
|
-
const content =
|
|
4590
|
+
const content = fs5.readFileSync(pkgPath, "utf-8");
|
|
3113
4591
|
const pkg = JSON.parse(content);
|
|
3114
4592
|
if (pkg.name === PACKAGE_NAME)
|
|
3115
4593
|
return pkgPath;
|
|
3116
4594
|
} catch {}
|
|
3117
4595
|
}
|
|
3118
|
-
const parent =
|
|
4596
|
+
const parent = path7.dirname(dir);
|
|
3119
4597
|
if (parent === dir)
|
|
3120
4598
|
break;
|
|
3121
4599
|
dir = parent;
|
|
@@ -3131,7 +4609,7 @@ function getLocalDevVersion(directory) {
|
|
|
3131
4609
|
const pkgPath = findPackageJsonUp(localPath);
|
|
3132
4610
|
if (!pkgPath)
|
|
3133
4611
|
return null;
|
|
3134
|
-
const content =
|
|
4612
|
+
const content = fs5.readFileSync(pkgPath, "utf-8");
|
|
3135
4613
|
const pkg = JSON.parse(content);
|
|
3136
4614
|
return pkg.version ?? null;
|
|
3137
4615
|
} catch {
|
|
@@ -3141,9 +4619,9 @@ function getLocalDevVersion(directory) {
|
|
|
3141
4619
|
function findPluginEntry(directory) {
|
|
3142
4620
|
for (const configPath of getConfigPaths(directory)) {
|
|
3143
4621
|
try {
|
|
3144
|
-
if (!
|
|
4622
|
+
if (!fs5.existsSync(configPath))
|
|
3145
4623
|
continue;
|
|
3146
|
-
const content =
|
|
4624
|
+
const content = fs5.readFileSync(configPath, "utf-8");
|
|
3147
4625
|
const config = JSON.parse(stripJsonComments(content));
|
|
3148
4626
|
const plugins = config.plugin ?? [];
|
|
3149
4627
|
for (const entry of plugins) {
|
|
@@ -3170,8 +4648,8 @@ function getCachedVersion() {
|
|
|
3170
4648
|
if (cachedPackageVersion)
|
|
3171
4649
|
return cachedPackageVersion;
|
|
3172
4650
|
try {
|
|
3173
|
-
if (
|
|
3174
|
-
const content =
|
|
4651
|
+
if (fs5.existsSync(INSTALLED_PACKAGE_JSON)) {
|
|
4652
|
+
const content = fs5.readFileSync(INSTALLED_PACKAGE_JSON, "utf-8");
|
|
3175
4653
|
const pkg = JSON.parse(content);
|
|
3176
4654
|
if (pkg.version) {
|
|
3177
4655
|
cachedPackageVersion = pkg.version;
|
|
@@ -3180,10 +4658,10 @@ function getCachedVersion() {
|
|
|
3180
4658
|
}
|
|
3181
4659
|
} catch {}
|
|
3182
4660
|
try {
|
|
3183
|
-
const currentDir =
|
|
4661
|
+
const currentDir = path7.dirname(fileURLToPath(import.meta.url));
|
|
3184
4662
|
const pkgPath = findPackageJsonUp(currentDir);
|
|
3185
4663
|
if (pkgPath) {
|
|
3186
|
-
const content =
|
|
4664
|
+
const content = fs5.readFileSync(pkgPath, "utf-8");
|
|
3187
4665
|
const pkg = JSON.parse(content);
|
|
3188
4666
|
if (pkg.version) {
|
|
3189
4667
|
cachedPackageVersion = pkg.version;
|
|
@@ -3858,17 +5336,35 @@ ${originalText}`;
|
|
|
3858
5336
|
};
|
|
3859
5337
|
}
|
|
3860
5338
|
// src/hooks/post-file-tool-nudge/index.ts
|
|
3861
|
-
var
|
|
3862
|
-
|
|
3863
|
-
|
|
3864
|
-
|
|
3865
|
-
function createPostFileToolNudgeHook() {
|
|
5339
|
+
var POST_FILE_TOOL_NUDGE = PHASE_REMINDER_TEXT;
|
|
5340
|
+
var FILE_TOOLS = new Set(["Read", "read", "Write", "write"]);
|
|
5341
|
+
function createPostFileToolNudgeHook(options = {}) {
|
|
5342
|
+
const pendingSessionIds = new Set;
|
|
3866
5343
|
return {
|
|
3867
|
-
"tool.execute.after": async (input,
|
|
3868
|
-
if (
|
|
5344
|
+
"tool.execute.after": async (input, _output) => {
|
|
5345
|
+
if (!FILE_TOOLS.has(input.tool) || !input.sessionID) {
|
|
5346
|
+
return;
|
|
5347
|
+
}
|
|
5348
|
+
pendingSessionIds.add(input.sessionID);
|
|
5349
|
+
},
|
|
5350
|
+
"experimental.chat.system.transform": async (input, output) => {
|
|
5351
|
+
if (!input.sessionID || !pendingSessionIds.delete(input.sessionID)) {
|
|
5352
|
+
return;
|
|
5353
|
+
}
|
|
5354
|
+
if (options.shouldInject && !options.shouldInject(input.sessionID)) {
|
|
5355
|
+
return;
|
|
5356
|
+
}
|
|
5357
|
+
output.system.push(POST_FILE_TOOL_NUDGE);
|
|
5358
|
+
},
|
|
5359
|
+
event: async (input) => {
|
|
5360
|
+
if (input.event.type !== "session.deleted") {
|
|
3869
5361
|
return;
|
|
3870
5362
|
}
|
|
3871
|
-
|
|
5363
|
+
const sessionID = input.event.properties?.sessionID ?? input.event.properties?.info?.id;
|
|
5364
|
+
if (!sessionID) {
|
|
5365
|
+
return;
|
|
5366
|
+
}
|
|
5367
|
+
pendingSessionIds.delete(sessionID);
|
|
3872
5368
|
}
|
|
3873
5369
|
};
|
|
3874
5370
|
}
|
|
@@ -3878,6 +5374,7 @@ var HOOK_NAME = "todo-continuation";
|
|
|
3878
5374
|
var COMMAND_NAME = "auto-continue";
|
|
3879
5375
|
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
5376
|
var SUPPRESS_AFTER_ABORT_MS = 5000;
|
|
5377
|
+
var NOTIFICATION_BUSY_GRACE_MS = 250;
|
|
3881
5378
|
var QUESTION_PHRASES = [
|
|
3882
5379
|
"would you like",
|
|
3883
5380
|
"should i",
|
|
@@ -3903,12 +5400,15 @@ function cancelPendingTimer(state) {
|
|
|
3903
5400
|
clearTimeout(state.pendingTimer);
|
|
3904
5401
|
state.pendingTimer = null;
|
|
3905
5402
|
}
|
|
5403
|
+
state.pendingTimerSessionId = null;
|
|
3906
5404
|
}
|
|
3907
5405
|
function resetState(state) {
|
|
3908
5406
|
cancelPendingTimer(state);
|
|
3909
5407
|
state.consecutiveContinuations = 0;
|
|
3910
5408
|
state.suppressUntil = 0;
|
|
3911
5409
|
state.isAutoInjecting = false;
|
|
5410
|
+
state.notifyingSessionIds.clear();
|
|
5411
|
+
state.notificationBusyUntilBySession.clear();
|
|
3912
5412
|
}
|
|
3913
5413
|
function createTodoContinuationHook(ctx, config) {
|
|
3914
5414
|
const maxContinuations = config?.maxContinuations ?? 5;
|
|
@@ -3919,10 +5419,51 @@ function createTodoContinuationHook(ctx, config) {
|
|
|
3919
5419
|
enabled: false,
|
|
3920
5420
|
consecutiveContinuations: 0,
|
|
3921
5421
|
pendingTimer: null,
|
|
5422
|
+
pendingTimerSessionId: null,
|
|
3922
5423
|
suppressUntil: 0,
|
|
3923
|
-
|
|
3924
|
-
|
|
5424
|
+
orchestratorSessionIds: new Set,
|
|
5425
|
+
sawChatMessage: false,
|
|
5426
|
+
isAutoInjecting: false,
|
|
5427
|
+
notifyingSessionIds: new Set,
|
|
5428
|
+
notificationBusyUntilBySession: new Map
|
|
3925
5429
|
};
|
|
5430
|
+
function markNotificationStarted(sessionID) {
|
|
5431
|
+
state.notifyingSessionIds.add(sessionID);
|
|
5432
|
+
}
|
|
5433
|
+
function markNotificationFinished(sessionID) {
|
|
5434
|
+
state.notifyingSessionIds.delete(sessionID);
|
|
5435
|
+
state.notificationBusyUntilBySession.set(sessionID, Date.now() + NOTIFICATION_BUSY_GRACE_MS);
|
|
5436
|
+
}
|
|
5437
|
+
function clearNotificationState(sessionID) {
|
|
5438
|
+
state.notifyingSessionIds.delete(sessionID);
|
|
5439
|
+
state.notificationBusyUntilBySession.delete(sessionID);
|
|
5440
|
+
}
|
|
5441
|
+
function isNotificationBusy(sessionID) {
|
|
5442
|
+
if (state.notifyingSessionIds.has(sessionID)) {
|
|
5443
|
+
return true;
|
|
5444
|
+
}
|
|
5445
|
+
const until = state.notificationBusyUntilBySession.get(sessionID) ?? 0;
|
|
5446
|
+
if (until <= Date.now()) {
|
|
5447
|
+
state.notificationBusyUntilBySession.delete(sessionID);
|
|
5448
|
+
return false;
|
|
5449
|
+
}
|
|
5450
|
+
return true;
|
|
5451
|
+
}
|
|
5452
|
+
function isOrchestratorSession(sessionID) {
|
|
5453
|
+
return state.orchestratorSessionIds.has(sessionID);
|
|
5454
|
+
}
|
|
5455
|
+
function registerOrchestratorSession(sessionID) {
|
|
5456
|
+
state.orchestratorSessionIds.add(sessionID);
|
|
5457
|
+
}
|
|
5458
|
+
function handleChatMessage(input) {
|
|
5459
|
+
if (!input.agent) {
|
|
5460
|
+
return;
|
|
5461
|
+
}
|
|
5462
|
+
state.sawChatMessage = true;
|
|
5463
|
+
if (input.agent === "orchestrator") {
|
|
5464
|
+
registerOrchestratorSession(input.sessionID);
|
|
5465
|
+
}
|
|
5466
|
+
}
|
|
3926
5467
|
const autoContinue = tool({
|
|
3927
5468
|
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
5469
|
args: { enabled: tool.schema.boolean() },
|
|
@@ -3943,19 +5484,19 @@ function createTodoContinuationHook(ctx, config) {
|
|
|
3943
5484
|
async function handleEvent(input) {
|
|
3944
5485
|
const { event } = input;
|
|
3945
5486
|
const properties = event.properties ?? {};
|
|
3946
|
-
if (event.type === "session.idle") {
|
|
5487
|
+
if (event.type === "session.idle" || event.type === "session.status" && properties.status?.type === "idle") {
|
|
3947
5488
|
const sessionID = properties.sessionID;
|
|
3948
5489
|
if (!sessionID) {
|
|
3949
5490
|
return;
|
|
3950
5491
|
}
|
|
3951
5492
|
log(`[${HOOK_NAME}] Session idle`, { sessionID });
|
|
3952
|
-
if (!state.
|
|
3953
|
-
|
|
5493
|
+
if (!state.sawChatMessage && state.orchestratorSessionIds.size === 0) {
|
|
5494
|
+
registerOrchestratorSession(sessionID);
|
|
3954
5495
|
log(`[${HOOK_NAME}] Tracked orchestrator session`, {
|
|
3955
5496
|
sessionID
|
|
3956
5497
|
});
|
|
3957
5498
|
}
|
|
3958
|
-
if (
|
|
5499
|
+
if (!isOrchestratorSession(sessionID)) {
|
|
3959
5500
|
log(`[${HOOK_NAME}] Skipped: not orchestrator session`, {
|
|
3960
5501
|
sessionID
|
|
3961
5502
|
});
|
|
@@ -4068,6 +5609,7 @@ function createTodoContinuationHook(ctx, config) {
|
|
|
4068
5609
|
sessionID,
|
|
4069
5610
|
delayMs: cooldownMs
|
|
4070
5611
|
});
|
|
5612
|
+
markNotificationStarted(sessionID);
|
|
4071
5613
|
ctx.client.session.prompt({
|
|
4072
5614
|
path: { id: sessionID },
|
|
4073
5615
|
body: {
|
|
@@ -4084,9 +5626,14 @@ function createTodoContinuationHook(ctx, config) {
|
|
|
4084
5626
|
}
|
|
4085
5627
|
]
|
|
4086
5628
|
}
|
|
4087
|
-
}).catch(() => {})
|
|
5629
|
+
}).catch(() => {}).finally(() => {
|
|
5630
|
+
markNotificationFinished(sessionID);
|
|
5631
|
+
});
|
|
5632
|
+
state.pendingTimerSessionId = sessionID;
|
|
4088
5633
|
state.pendingTimer = setTimeout(async () => {
|
|
4089
5634
|
state.pendingTimer = null;
|
|
5635
|
+
state.pendingTimerSessionId = null;
|
|
5636
|
+
clearNotificationState(sessionID);
|
|
4090
5637
|
if (!state.enabled) {
|
|
4091
5638
|
log(`[${HOOK_NAME}] Cancelled: disabled during cooldown`, {
|
|
4092
5639
|
sessionID
|
|
@@ -4119,11 +5666,12 @@ function createTodoContinuationHook(ctx, config) {
|
|
|
4119
5666
|
const status = properties.status;
|
|
4120
5667
|
const sessionID = properties.sessionID;
|
|
4121
5668
|
if (status?.type === "busy") {
|
|
4122
|
-
const isOrchestrator = sessionID
|
|
4123
|
-
|
|
5669
|
+
const isOrchestrator = isOrchestratorSession(sessionID);
|
|
5670
|
+
const isNotification = isNotificationBusy(sessionID);
|
|
5671
|
+
if (isOrchestrator && !isNotification && state.pendingTimerSessionId === sessionID) {
|
|
4124
5672
|
cancelPendingTimer(state);
|
|
4125
5673
|
}
|
|
4126
|
-
if (!state.isAutoInjecting && isOrchestrator && state.consecutiveContinuations > 0) {
|
|
5674
|
+
if (!state.isAutoInjecting && !isNotification && isOrchestrator && state.consecutiveContinuations > 0) {
|
|
4127
5675
|
state.consecutiveContinuations = 0;
|
|
4128
5676
|
log(`[${HOOK_NAME}] Reset consecutive count on user activity`, {
|
|
4129
5677
|
sessionID
|
|
@@ -4134,7 +5682,7 @@ function createTodoContinuationHook(ctx, config) {
|
|
|
4134
5682
|
const error = properties.error;
|
|
4135
5683
|
const sessionID = properties.sessionID;
|
|
4136
5684
|
const errorName = error?.name;
|
|
4137
|
-
const isOrchestrator = sessionID
|
|
5685
|
+
const isOrchestrator = isOrchestratorSession(sessionID);
|
|
4138
5686
|
if (isOrchestrator && (errorName === "MessageAbortedError" || errorName === "AbortError")) {
|
|
4139
5687
|
state.suppressUntil = Date.now() + SUPPRESS_AFTER_ABORT_MS;
|
|
4140
5688
|
log(`[${HOOK_NAME}] Suppressed continuation after abort`, {
|
|
@@ -4150,13 +5698,19 @@ function createTodoContinuationHook(ctx, config) {
|
|
|
4150
5698
|
}
|
|
4151
5699
|
} else if (event.type === "session.deleted") {
|
|
4152
5700
|
const deletedSessionId = properties.info?.id ?? properties.sessionID;
|
|
4153
|
-
if (
|
|
4154
|
-
|
|
4155
|
-
|
|
4156
|
-
|
|
4157
|
-
|
|
4158
|
-
|
|
4159
|
-
|
|
5701
|
+
if (deletedSessionId && isOrchestratorSession(deletedSessionId)) {
|
|
5702
|
+
if (state.pendingTimerSessionId === deletedSessionId) {
|
|
5703
|
+
cancelPendingTimer(state);
|
|
5704
|
+
log(`[${HOOK_NAME}] Cancelled pending timer on orchestrator delete`, {
|
|
5705
|
+
sessionID: deletedSessionId
|
|
5706
|
+
});
|
|
5707
|
+
}
|
|
5708
|
+
state.orchestratorSessionIds.delete(deletedSessionId);
|
|
5709
|
+
clearNotificationState(deletedSessionId);
|
|
5710
|
+
if (state.orchestratorSessionIds.size === 0) {
|
|
5711
|
+
resetState(state);
|
|
5712
|
+
state.sawChatMessage = false;
|
|
5713
|
+
}
|
|
4160
5714
|
log(`[${HOOK_NAME}] Reset orchestrator session on delete`, {
|
|
4161
5715
|
sessionID: deletedSessionId
|
|
4162
5716
|
});
|
|
@@ -4167,9 +5721,7 @@ function createTodoContinuationHook(ctx, config) {
|
|
|
4167
5721
|
if (input.command !== COMMAND_NAME) {
|
|
4168
5722
|
return;
|
|
4169
5723
|
}
|
|
4170
|
-
|
|
4171
|
-
state.orchestratorSessionId = input.sessionID;
|
|
4172
|
-
}
|
|
5724
|
+
registerOrchestratorSession(input.sessionID);
|
|
4173
5725
|
output.parts.length = 0;
|
|
4174
5726
|
const arg = input.arguments.trim().toLowerCase();
|
|
4175
5727
|
let newEnabled;
|
|
@@ -4214,6 +5766,7 @@ function createTodoContinuationHook(ctx, config) {
|
|
|
4214
5766
|
return {
|
|
4215
5767
|
tool: { auto_continue: autoContinue },
|
|
4216
5768
|
handleEvent,
|
|
5769
|
+
handleChatMessage,
|
|
4217
5770
|
handleCommandExecuteBefore
|
|
4218
5771
|
};
|
|
4219
5772
|
}
|
|
@@ -5077,8 +6630,8 @@ function createInterviewServer(deps) {
|
|
|
5077
6630
|
// src/interview/service.ts
|
|
5078
6631
|
import { spawn as spawn4 } from "child_process";
|
|
5079
6632
|
import * as fsSync from "fs";
|
|
5080
|
-
import * as
|
|
5081
|
-
import * as
|
|
6633
|
+
import * as fs6 from "fs/promises";
|
|
6634
|
+
import * as path8 from "path";
|
|
5082
6635
|
|
|
5083
6636
|
// src/interview/parser.ts
|
|
5084
6637
|
var INTERVIEW_BLOCK_REGEX = /<interview_state>\s*([\s\S]*?)\s*<\/interview_state>/i;
|
|
@@ -5271,14 +6824,14 @@ function normalizeOutputFolder(outputFolder) {
|
|
|
5271
6824
|
return normalized || DEFAULT_OUTPUT_FOLDER;
|
|
5272
6825
|
}
|
|
5273
6826
|
function createInterviewDirectoryPath(directory, outputFolder) {
|
|
5274
|
-
return
|
|
6827
|
+
return path8.join(directory, normalizeOutputFolder(outputFolder));
|
|
5275
6828
|
}
|
|
5276
6829
|
function createInterviewFilePath(directory, outputFolder, idea) {
|
|
5277
6830
|
const fileName = `${slugify(idea) || "interview"}.md`;
|
|
5278
|
-
return
|
|
6831
|
+
return path8.join(createInterviewDirectoryPath(directory, outputFolder), fileName);
|
|
5279
6832
|
}
|
|
5280
6833
|
function relativeInterviewPath(directory, filePath) {
|
|
5281
|
-
return
|
|
6834
|
+
return path8.relative(directory, filePath) || path8.basename(filePath);
|
|
5282
6835
|
}
|
|
5283
6836
|
function extractHistorySection(document) {
|
|
5284
6837
|
const marker = `## Q&A history
|
|
@@ -5324,31 +6877,31 @@ function buildInterviewDocument(idea, summary, history) {
|
|
|
5324
6877
|
`);
|
|
5325
6878
|
}
|
|
5326
6879
|
async function ensureInterviewFile(record) {
|
|
5327
|
-
await
|
|
6880
|
+
await fs6.mkdir(path8.dirname(record.markdownPath), { recursive: true });
|
|
5328
6881
|
try {
|
|
5329
|
-
await
|
|
6882
|
+
await fs6.access(record.markdownPath);
|
|
5330
6883
|
} catch {
|
|
5331
|
-
await
|
|
6884
|
+
await fs6.writeFile(record.markdownPath, buildInterviewDocument(record.idea, "", ""), "utf8");
|
|
5332
6885
|
}
|
|
5333
6886
|
}
|
|
5334
6887
|
async function readInterviewDocument(record) {
|
|
5335
6888
|
try {
|
|
5336
|
-
return await
|
|
6889
|
+
return await fs6.readFile(record.markdownPath, "utf8");
|
|
5337
6890
|
} catch (error) {
|
|
5338
6891
|
if (error.code === "ENOENT") {
|
|
5339
6892
|
try {
|
|
5340
|
-
return await
|
|
6893
|
+
return await fs6.readFile(record.markdownPath, "utf8");
|
|
5341
6894
|
} catch {}
|
|
5342
6895
|
}
|
|
5343
6896
|
}
|
|
5344
6897
|
await ensureInterviewFile(record);
|
|
5345
|
-
return
|
|
6898
|
+
return fs6.readFile(record.markdownPath, "utf8");
|
|
5346
6899
|
}
|
|
5347
6900
|
async function rewriteInterviewDocument(record, summary) {
|
|
5348
6901
|
const existing = await readInterviewDocument(record);
|
|
5349
6902
|
const history = extractHistorySection(existing);
|
|
5350
6903
|
const next = buildInterviewDocument(record.idea, summary, history);
|
|
5351
|
-
await
|
|
6904
|
+
await fs6.writeFile(record.markdownPath, next, "utf8");
|
|
5352
6905
|
return next;
|
|
5353
6906
|
}
|
|
5354
6907
|
async function appendInterviewAnswers(record, questions, answers) {
|
|
@@ -5366,7 +6919,7 @@ A: ${answer.answer.trim()}` : null;
|
|
|
5366
6919
|
const nextHistory = [history === "No answers yet." ? "" : history, appended].filter(Boolean).join(`
|
|
5367
6920
|
|
|
5368
6921
|
`);
|
|
5369
|
-
await
|
|
6922
|
+
await fs6.writeFile(record.markdownPath, buildInterviewDocument(record.idea, summary, nextHistory), "utf8");
|
|
5370
6923
|
}
|
|
5371
6924
|
function resolveExistingInterviewPath(directory, outputFolder, value) {
|
|
5372
6925
|
const trimmed = value.trim();
|
|
@@ -5375,22 +6928,22 @@ function resolveExistingInterviewPath(directory, outputFolder, value) {
|
|
|
5375
6928
|
}
|
|
5376
6929
|
const outputDir = createInterviewDirectoryPath(directory, outputFolder);
|
|
5377
6930
|
const candidates = new Set;
|
|
5378
|
-
const resolvedRoot =
|
|
5379
|
-
if (
|
|
6931
|
+
const resolvedRoot = path8.resolve(directory);
|
|
6932
|
+
if (path8.isAbsolute(trimmed)) {
|
|
5380
6933
|
candidates.add(trimmed);
|
|
5381
6934
|
} else {
|
|
5382
|
-
candidates.add(
|
|
5383
|
-
candidates.add(
|
|
6935
|
+
candidates.add(path8.resolve(directory, trimmed));
|
|
6936
|
+
candidates.add(path8.join(outputDir, trimmed));
|
|
5384
6937
|
if (!trimmed.endsWith(".md")) {
|
|
5385
|
-
candidates.add(
|
|
6938
|
+
candidates.add(path8.join(outputDir, `${trimmed}.md`));
|
|
5386
6939
|
}
|
|
5387
6940
|
}
|
|
5388
6941
|
for (const candidate of candidates) {
|
|
5389
|
-
if (
|
|
6942
|
+
if (path8.extname(candidate) !== ".md") {
|
|
5390
6943
|
continue;
|
|
5391
6944
|
}
|
|
5392
|
-
const resolved =
|
|
5393
|
-
if (!resolved.startsWith(resolvedRoot +
|
|
6945
|
+
const resolved = path8.resolve(candidate);
|
|
6946
|
+
if (!resolved.startsWith(resolvedRoot + path8.sep) && resolved !== resolvedRoot) {
|
|
5394
6947
|
continue;
|
|
5395
6948
|
}
|
|
5396
6949
|
if (fsSync.existsSync(candidate)) {
|
|
@@ -5437,18 +6990,18 @@ function createInterviewService(ctx, config, deps) {
|
|
|
5437
6990
|
if (!newSlug) {
|
|
5438
6991
|
return;
|
|
5439
6992
|
}
|
|
5440
|
-
const currentFileName =
|
|
6993
|
+
const currentFileName = path8.basename(interview.markdownPath, ".md");
|
|
5441
6994
|
if (currentFileName === newSlug) {
|
|
5442
6995
|
return;
|
|
5443
6996
|
}
|
|
5444
|
-
const dir =
|
|
5445
|
-
const newPath =
|
|
6997
|
+
const dir = path8.dirname(interview.markdownPath);
|
|
6998
|
+
const newPath = path8.join(dir, `${newSlug}.md`);
|
|
5446
6999
|
try {
|
|
5447
|
-
await
|
|
7000
|
+
await fs6.access(newPath);
|
|
5448
7001
|
return;
|
|
5449
7002
|
} catch {}
|
|
5450
7003
|
try {
|
|
5451
|
-
await
|
|
7004
|
+
await fs6.rename(interview.markdownPath, newPath);
|
|
5452
7005
|
interview.markdownPath = newPath;
|
|
5453
7006
|
log("[interview] renamed file with assistant title:", {
|
|
5454
7007
|
from: currentFileName,
|
|
@@ -5510,13 +7063,13 @@ function createInterviewService(ctx, config, deps) {
|
|
|
5510
7063
|
active.status = "abandoned";
|
|
5511
7064
|
}
|
|
5512
7065
|
}
|
|
5513
|
-
const document = await
|
|
7066
|
+
const document = await fs6.readFile(markdownPath, "utf8");
|
|
5514
7067
|
const messages = await loadMessages(sessionID);
|
|
5515
7068
|
const title = extractTitle(document);
|
|
5516
7069
|
const record = {
|
|
5517
|
-
id: `${Date.now()}-${++idCounter}-${slugify(
|
|
7070
|
+
id: `${Date.now()}-${++idCounter}-${slugify(path8.basename(markdownPath, ".md")) || "interview"}`,
|
|
5518
7071
|
sessionID,
|
|
5519
|
-
idea: title ||
|
|
7072
|
+
idea: title || path8.basename(markdownPath, ".md"),
|
|
5520
7073
|
markdownPath,
|
|
5521
7074
|
createdAt: nowIso(),
|
|
5522
7075
|
status: "active",
|
|
@@ -5658,7 +7211,7 @@ function createInterviewService(ctx, config, deps) {
|
|
|
5658
7211
|
const resumePath = resolveExistingInterviewPath(ctx.directory, outputFolder, idea);
|
|
5659
7212
|
if (resumePath) {
|
|
5660
7213
|
const interview2 = await resumeInterview(input.sessionID, resumePath);
|
|
5661
|
-
const document = await
|
|
7214
|
+
const document = await fs6.readFile(interview2.markdownPath, "utf8");
|
|
5662
7215
|
await notifyInterviewUrl(input.sessionID, interview2);
|
|
5663
7216
|
output.parts.push(createInternalAgentTextPart(buildResumePrompt(document, maxQuestions)));
|
|
5664
7217
|
return;
|
|
@@ -5969,9 +7522,9 @@ function findSgCliPathSync() {
|
|
|
5969
7522
|
}
|
|
5970
7523
|
if (process.platform === "darwin") {
|
|
5971
7524
|
const homebrewPaths = ["/opt/homebrew/bin/sg", "/usr/local/bin/sg"];
|
|
5972
|
-
for (const
|
|
5973
|
-
if (existsSync6(
|
|
5974
|
-
return
|
|
7525
|
+
for (const path9 of homebrewPaths) {
|
|
7526
|
+
if (existsSync6(path9) && isValidBinary(path9)) {
|
|
7527
|
+
return path9;
|
|
5975
7528
|
}
|
|
5976
7529
|
}
|
|
5977
7530
|
}
|
|
@@ -5988,8 +7541,8 @@ function getSgCliPath() {
|
|
|
5988
7541
|
}
|
|
5989
7542
|
return "sg";
|
|
5990
7543
|
}
|
|
5991
|
-
function setSgCliPath(
|
|
5992
|
-
resolvedCliPath =
|
|
7544
|
+
function setSgCliPath(path9) {
|
|
7545
|
+
resolvedCliPath = path9;
|
|
5993
7546
|
}
|
|
5994
7547
|
var DEFAULT_TIMEOUT_MS2 = 300000;
|
|
5995
7548
|
var DEFAULT_MAX_OUTPUT_BYTES = 1 * 1024 * 1024;
|
|
@@ -8232,14 +9785,14 @@ var BINARY_PREFIXES = [
|
|
|
8232
9785
|
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
9786
|
// src/tools/smartfetch/tool.ts
|
|
8234
9787
|
import os3 from "os";
|
|
8235
|
-
import
|
|
9788
|
+
import path12 from "path";
|
|
8236
9789
|
import {
|
|
8237
9790
|
tool as tool6
|
|
8238
9791
|
} from "@opencode-ai/plugin";
|
|
8239
9792
|
|
|
8240
9793
|
// src/tools/smartfetch/binary.ts
|
|
8241
9794
|
import { mkdir as mkdir2, writeFile as writeFile2 } from "fs/promises";
|
|
8242
|
-
import
|
|
9795
|
+
import path9 from "path";
|
|
8243
9796
|
function extensionForMime(contentType) {
|
|
8244
9797
|
const mime = contentType.split(";")[0]?.trim().toLowerCase();
|
|
8245
9798
|
const map = {
|
|
@@ -8260,10 +9813,10 @@ function buildBinaryResultMessage(fetchResult, savedPath) {
|
|
|
8260
9813
|
async function saveBinary(binaryDir, data, contentType, filename) {
|
|
8261
9814
|
await mkdir2(binaryDir, { recursive: true });
|
|
8262
9815
|
const initialName = filename || `webfetch-${Date.now()}.${extensionForMime(contentType)}`;
|
|
8263
|
-
const parsed =
|
|
9816
|
+
const parsed = path9.parse(initialName);
|
|
8264
9817
|
for (let attempt = 0;attempt < 1000; attempt++) {
|
|
8265
9818
|
const candidateName = attempt === 0 ? initialName : `${parsed.name}-${attempt}${parsed.ext || `.${extensionForMime(contentType)}`}`;
|
|
8266
|
-
const file =
|
|
9819
|
+
const file = path9.join(binaryDir, candidateName);
|
|
8267
9820
|
try {
|
|
8268
9821
|
await writeFile2(file, data, { flag: "wx" });
|
|
8269
9822
|
return file;
|
|
@@ -8281,7 +9834,7 @@ async function saveBinary(binaryDir, data, contentType, filename) {
|
|
|
8281
9834
|
import { LRUCache } from "lru-cache";
|
|
8282
9835
|
|
|
8283
9836
|
// src/tools/smartfetch/network.ts
|
|
8284
|
-
import
|
|
9837
|
+
import path10 from "path";
|
|
8285
9838
|
|
|
8286
9839
|
// src/tools/smartfetch/utils.ts
|
|
8287
9840
|
import { Readability } from "@mozilla/readability";
|
|
@@ -8665,7 +10218,7 @@ function normalizeUrl(input) {
|
|
|
8665
10218
|
}
|
|
8666
10219
|
function isDocsLikeUrl(url) {
|
|
8667
10220
|
const host = url.hostname.toLowerCase();
|
|
8668
|
-
return DOCS_HOST_SUFFIXES.some((
|
|
10221
|
+
return DOCS_HOST_SUFFIXES.some((suffix2) => host.endsWith(suffix2)) || DOCS_HOST_PREFIXES.some((prefix2) => host.startsWith(prefix2));
|
|
8669
10222
|
}
|
|
8670
10223
|
function buildPermissionPatterns(normalized, shouldProbeLlmsTxt) {
|
|
8671
10224
|
const patterns = new Set([normalized.url]);
|
|
@@ -8721,7 +10274,7 @@ function isPermittedRedirect(from, to, allowedOrigins) {
|
|
|
8721
10274
|
}
|
|
8722
10275
|
function isBinaryContentType(contentType) {
|
|
8723
10276
|
const mime = contentType.split(";")[0]?.trim().toLowerCase() || "";
|
|
8724
|
-
return BINARY_PREFIXES.some((
|
|
10277
|
+
return BINARY_PREFIXES.some((prefix2) => mime.startsWith(prefix2));
|
|
8725
10278
|
}
|
|
8726
10279
|
function getBinaryKind(contentType) {
|
|
8727
10280
|
const mime = contentType.split(";")[0]?.trim().toLowerCase() || "";
|
|
@@ -9000,7 +10553,7 @@ function inferFilenameFromUrl(url) {
|
|
|
9000
10553
|
function truncateFilename(name, maxLength = 180) {
|
|
9001
10554
|
if (name.length <= maxLength)
|
|
9002
10555
|
return name;
|
|
9003
|
-
const parsed =
|
|
10556
|
+
const parsed = path10.parse(name);
|
|
9004
10557
|
const ext = parsed.ext || "";
|
|
9005
10558
|
const baseLimit = Math.max(1, maxLength - ext.length);
|
|
9006
10559
|
return `${parsed.name.slice(0, baseLimit)}${ext}`;
|
|
@@ -9171,8 +10724,8 @@ function isInvalidLlmsResult(fetchResult) {
|
|
|
9171
10724
|
|
|
9172
10725
|
// src/tools/smartfetch/secondary-model.ts
|
|
9173
10726
|
import { existsSync as existsSync11 } from "fs";
|
|
9174
|
-
import { readFile as
|
|
9175
|
-
import
|
|
10727
|
+
import { readFile as readFile3 } from "fs/promises";
|
|
10728
|
+
import path11 from "path";
|
|
9176
10729
|
function parseModelRef(value) {
|
|
9177
10730
|
if (!value)
|
|
9178
10731
|
return;
|
|
@@ -9198,7 +10751,7 @@ function pickAgentModelRef(value) {
|
|
|
9198
10751
|
}
|
|
9199
10752
|
function findPreferredOpenCodeConfigPath(baseDir) {
|
|
9200
10753
|
for (const file of ["opencode.jsonc", "opencode.json"]) {
|
|
9201
|
-
const fullPath =
|
|
10754
|
+
const fullPath = path11.join(baseDir, file);
|
|
9202
10755
|
if (existsSync11(fullPath))
|
|
9203
10756
|
return fullPath;
|
|
9204
10757
|
}
|
|
@@ -9208,14 +10761,14 @@ async function readOpenCodeConfigFile(configPath) {
|
|
|
9208
10761
|
if (!configPath)
|
|
9209
10762
|
return;
|
|
9210
10763
|
try {
|
|
9211
|
-
const content = await
|
|
10764
|
+
const content = await readFile3(configPath, "utf8");
|
|
9212
10765
|
return JSON.parse(stripJsonComments(content));
|
|
9213
10766
|
} catch {
|
|
9214
10767
|
return;
|
|
9215
10768
|
}
|
|
9216
10769
|
}
|
|
9217
10770
|
async function readEffectiveOpenCodeConfig(directory) {
|
|
9218
|
-
const projectDir =
|
|
10771
|
+
const projectDir = path11.join(directory, ".opencode");
|
|
9219
10772
|
const userDirs = getConfigSearchDirs();
|
|
9220
10773
|
const projectPath = findPreferredOpenCodeConfigPath(projectDir);
|
|
9221
10774
|
const userPath = userDirs.map((configDir) => findPreferredOpenCodeConfigPath(configDir)).find(Boolean);
|
|
@@ -9376,7 +10929,7 @@ async function runSecondaryModelWithFallback(client, directory, models, prompt,
|
|
|
9376
10929
|
// src/tools/smartfetch/tool.ts
|
|
9377
10930
|
var z5 = tool6.schema;
|
|
9378
10931
|
function createWebfetchTool(pluginCtx, options = {}) {
|
|
9379
|
-
const binaryDir = options.binaryDir ||
|
|
10932
|
+
const binaryDir = options.binaryDir || path12.join(os3.tmpdir(), "opencode-smartfetch");
|
|
9380
10933
|
return tool6({
|
|
9381
10934
|
description: WEBFETCH_DESCRIPTION,
|
|
9382
10935
|
args: {
|
|
@@ -9917,12 +11470,15 @@ var OhMyOpenCodeLite = async (ctx) => {
|
|
|
9917
11470
|
});
|
|
9918
11471
|
const phaseReminderHook = createPhaseReminderHook();
|
|
9919
11472
|
const filterAvailableSkillsHook = createFilterAvailableSkillsHook(ctx, config);
|
|
9920
|
-
const
|
|
11473
|
+
const sessionAgentMap = new Map;
|
|
11474
|
+
const postFileToolNudgeHook = createPostFileToolNudgeHook({
|
|
11475
|
+
shouldInject: (sessionID) => sessionAgentMap.get(sessionID) === "orchestrator"
|
|
11476
|
+
});
|
|
9921
11477
|
const chatHeadersHook = createChatHeadersHook(ctx);
|
|
9922
11478
|
const delegateTaskRetryHook = createDelegateTaskRetryHook(ctx);
|
|
11479
|
+
const applyPatchHook = createApplyPatchHook(ctx);
|
|
9923
11480
|
const jsonErrorRecoveryHook = createJsonErrorRecoveryHook(ctx);
|
|
9924
11481
|
const foregroundFallback = new ForegroundFallbackManager(ctx.client, runtimeChains, config.fallback?.enabled !== false && Object.keys(runtimeChains).length > 0);
|
|
9925
|
-
const sessionAgentMap = new Map;
|
|
9926
11482
|
const todoContinuationHook = createTodoContinuationHook(ctx, {
|
|
9927
11483
|
maxContinuations: config.todoContinuation?.maxContinuations ?? 5,
|
|
9928
11484
|
cooldownMs: config.todoContinuation?.cooldownMs ?? 3000,
|
|
@@ -10061,16 +11617,25 @@ var OhMyOpenCodeLite = async (ctx) => {
|
|
|
10061
11617
|
await backgroundManager.handleSessionDeleted(input.event);
|
|
10062
11618
|
await multiplexerSessionManager.onSessionDeleted(input.event);
|
|
10063
11619
|
await interviewManager.handleEvent(input);
|
|
11620
|
+
await postFileToolNudgeHook.event(input);
|
|
11621
|
+
},
|
|
11622
|
+
"tool.execute.before": async (input, output) => {
|
|
11623
|
+
await applyPatchHook["tool.execute.before"](input, output);
|
|
10064
11624
|
},
|
|
10065
11625
|
"command.execute.before": async (input, output) => {
|
|
10066
11626
|
await todoContinuationHook.handleCommandExecuteBefore(input, output);
|
|
10067
11627
|
await interviewManager.handleCommandExecuteBefore(input, output);
|
|
10068
11628
|
},
|
|
10069
11629
|
"chat.headers": chatHeadersHook["chat.headers"],
|
|
10070
|
-
"chat.message": async (input) => {
|
|
10071
|
-
|
|
10072
|
-
|
|
11630
|
+
"chat.message": async (input, output) => {
|
|
11631
|
+
const agent = input.agent ?? output?.message?.agent;
|
|
11632
|
+
if (agent) {
|
|
11633
|
+
sessionAgentMap.set(input.sessionID, agent);
|
|
10073
11634
|
}
|
|
11635
|
+
todoContinuationHook.handleChatMessage({
|
|
11636
|
+
sessionID: input.sessionID,
|
|
11637
|
+
agent
|
|
11638
|
+
});
|
|
10074
11639
|
},
|
|
10075
11640
|
"experimental.chat.system.transform": async (input, output) => {
|
|
10076
11641
|
const agentName = input.sessionID ? sessionAgentMap.get(input.sessionID) : undefined;
|
|
@@ -10080,9 +11645,10 @@ var OhMyOpenCodeLite = async (ctx) => {
|
|
|
10080
11645
|
const { ORCHESTRATOR_PROMPT: ORCHESTRATOR_PROMPT2 } = await Promise.resolve().then(() => exports_orchestrator);
|
|
10081
11646
|
output.system[0] = ORCHESTRATOR_PROMPT2 + (output.system[0] ? `
|
|
10082
11647
|
|
|
10083
|
-
|
|
11648
|
+
${output.system[0]}` : "");
|
|
10084
11649
|
}
|
|
10085
11650
|
}
|
|
11651
|
+
await postFileToolNudgeHook["experimental.chat.system.transform"](input, output);
|
|
10086
11652
|
},
|
|
10087
11653
|
"experimental.chat.messages.transform": async (input, output) => {
|
|
10088
11654
|
const typedOutput = output;
|