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/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 fs3 from "fs";
2961
- import * as path4 from "path";
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 path3 from "path";
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 path3.join(process.env.LOCALAPPDATA ?? os2.homedir(), "opencode");
4448
+ return path5.join(process.env.LOCALAPPDATA ?? os2.homedir(), "opencode");
2971
4449
  }
2972
- return path3.join(os2.homedir(), ".cache", "opencode");
4450
+ return path5.join(os2.homedir(), ".cache", "opencode");
2973
4451
  }
2974
4452
  var CACHE_DIR = getCacheDir();
2975
- var INSTALLED_PACKAGE_JSON = path3.join(CACHE_DIR, "node_modules", PACKAGE_NAME, "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 = path4.join(CACHE_DIR, "bun.lock");
2983
- if (!fs3.existsSync(lockPath))
4460
+ const lockPath = path6.join(CACHE_DIR, "bun.lock");
4461
+ if (!fs4.existsSync(lockPath))
2984
4462
  return false;
2985
4463
  try {
2986
- const content = fs3.readFileSync(lockPath, "utf-8");
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
- fs3.writeFileSync(lockPath, JSON.stringify(lock, null, 2));
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 = path4.join(CACHE_DIR, "node_modules", packageName);
3015
- const pkgJsonPath = path4.join(CACHE_DIR, "package.json");
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 (fs3.existsSync(pkgDir)) {
3020
- fs3.rmSync(pkgDir, { recursive: true, force: true });
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 (fs3.existsSync(pkgJsonPath)) {
4502
+ if (fs4.existsSync(pkgJsonPath)) {
3025
4503
  try {
3026
- const content = fs3.readFileSync(pkgJsonPath, "utf-8");
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
- fs3.writeFileSync(pkgJsonPath, JSON.stringify(pkgJson, null, 2));
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 fs4 from "fs";
3052
- import * as path5 from "path";
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
- path5.join(directory, ".opencode", "opencode.json"),
3078
- path5.join(directory, ".opencode", "opencode.jsonc"),
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 (!fs4.existsSync(configPath))
4564
+ if (!fs5.existsSync(configPath))
3087
4565
  continue;
3088
- const content = fs4.readFileSync(configPath, "utf-8");
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 stat = fs4.statSync(startPath);
3107
- let dir = stat.isDirectory() ? startPath : path5.dirname(startPath);
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 = path5.join(dir, "package.json");
3110
- if (fs4.existsSync(pkgPath)) {
4587
+ const pkgPath = path7.join(dir, "package.json");
4588
+ if (fs5.existsSync(pkgPath)) {
3111
4589
  try {
3112
- const content = fs4.readFileSync(pkgPath, "utf-8");
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 = path5.dirname(dir);
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 = fs4.readFileSync(pkgPath, "utf-8");
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 (!fs4.existsSync(configPath))
4622
+ if (!fs5.existsSync(configPath))
3145
4623
  continue;
3146
- const content = fs4.readFileSync(configPath, "utf-8");
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 (fs4.existsSync(INSTALLED_PACKAGE_JSON)) {
3174
- const content = fs4.readFileSync(INSTALLED_PACKAGE_JSON, "utf-8");
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 = path5.dirname(fileURLToPath(import.meta.url));
4661
+ const currentDir = path7.dirname(fileURLToPath(import.meta.url));
3184
4662
  const pkgPath = findPackageJsonUp(currentDir);
3185
4663
  if (pkgPath) {
3186
- const content = fs4.readFileSync(pkgPath, "utf-8");
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 NUDGE = `
3862
-
3863
- ---
3864
- ${PHASE_REMINDER_TEXT}`;
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, output) => {
3868
- if (input.tool !== "Read" && input.tool !== "read" && input.tool !== "Write" && input.tool !== "write") {
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
- output.output = output.output + NUDGE;
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
- orchestratorSessionId: null,
3924
- isAutoInjecting: false
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.orchestratorSessionId) {
3953
- state.orchestratorSessionId = sessionID;
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 (state.orchestratorSessionId !== sessionID) {
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 === state.orchestratorSessionId;
4123
- if (isOrchestrator) {
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 === state.orchestratorSessionId;
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 (state.orchestratorSessionId === deletedSessionId) {
4154
- cancelPendingTimer(state);
4155
- log(`[${HOOK_NAME}] Cancelled pending timer on orchestrator delete`, {
4156
- sessionID: deletedSessionId
4157
- });
4158
- resetState(state);
4159
- state.orchestratorSessionId = null;
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
- if (!state.orchestratorSessionId) {
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 fs5 from "fs/promises";
5081
- import * as path6 from "path";
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 path6.join(directory, normalizeOutputFolder(outputFolder));
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 path6.join(createInterviewDirectoryPath(directory, outputFolder), fileName);
6831
+ return path8.join(createInterviewDirectoryPath(directory, outputFolder), fileName);
5279
6832
  }
5280
6833
  function relativeInterviewPath(directory, filePath) {
5281
- return path6.relative(directory, filePath) || path6.basename(filePath);
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 fs5.mkdir(path6.dirname(record.markdownPath), { recursive: true });
6880
+ await fs6.mkdir(path8.dirname(record.markdownPath), { recursive: true });
5328
6881
  try {
5329
- await fs5.access(record.markdownPath);
6882
+ await fs6.access(record.markdownPath);
5330
6883
  } catch {
5331
- await fs5.writeFile(record.markdownPath, buildInterviewDocument(record.idea, "", ""), "utf8");
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 fs5.readFile(record.markdownPath, "utf8");
6889
+ return await fs6.readFile(record.markdownPath, "utf8");
5337
6890
  } catch (error) {
5338
6891
  if (error.code === "ENOENT") {
5339
6892
  try {
5340
- return await fs5.readFile(record.markdownPath, "utf8");
6893
+ return await fs6.readFile(record.markdownPath, "utf8");
5341
6894
  } catch {}
5342
6895
  }
5343
6896
  }
5344
6897
  await ensureInterviewFile(record);
5345
- return fs5.readFile(record.markdownPath, "utf8");
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 fs5.writeFile(record.markdownPath, next, "utf8");
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 fs5.writeFile(record.markdownPath, buildInterviewDocument(record.idea, summary, nextHistory), "utf8");
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 = path6.resolve(directory);
5379
- if (path6.isAbsolute(trimmed)) {
6931
+ const resolvedRoot = path8.resolve(directory);
6932
+ if (path8.isAbsolute(trimmed)) {
5380
6933
  candidates.add(trimmed);
5381
6934
  } else {
5382
- candidates.add(path6.resolve(directory, trimmed));
5383
- candidates.add(path6.join(outputDir, trimmed));
6935
+ candidates.add(path8.resolve(directory, trimmed));
6936
+ candidates.add(path8.join(outputDir, trimmed));
5384
6937
  if (!trimmed.endsWith(".md")) {
5385
- candidates.add(path6.join(outputDir, `${trimmed}.md`));
6938
+ candidates.add(path8.join(outputDir, `${trimmed}.md`));
5386
6939
  }
5387
6940
  }
5388
6941
  for (const candidate of candidates) {
5389
- if (path6.extname(candidate) !== ".md") {
6942
+ if (path8.extname(candidate) !== ".md") {
5390
6943
  continue;
5391
6944
  }
5392
- const resolved = path6.resolve(candidate);
5393
- if (!resolved.startsWith(resolvedRoot + path6.sep) && resolved !== 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 = path6.basename(interview.markdownPath, ".md");
6993
+ const currentFileName = path8.basename(interview.markdownPath, ".md");
5441
6994
  if (currentFileName === newSlug) {
5442
6995
  return;
5443
6996
  }
5444
- const dir = path6.dirname(interview.markdownPath);
5445
- const newPath = path6.join(dir, `${newSlug}.md`);
6997
+ const dir = path8.dirname(interview.markdownPath);
6998
+ const newPath = path8.join(dir, `${newSlug}.md`);
5446
6999
  try {
5447
- await fs5.access(newPath);
7000
+ await fs6.access(newPath);
5448
7001
  return;
5449
7002
  } catch {}
5450
7003
  try {
5451
- await fs5.rename(interview.markdownPath, newPath);
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 fs5.readFile(markdownPath, "utf8");
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(path6.basename(markdownPath, ".md")) || "interview"}`,
7070
+ id: `${Date.now()}-${++idCounter}-${slugify(path8.basename(markdownPath, ".md")) || "interview"}`,
5518
7071
  sessionID,
5519
- idea: title || path6.basename(markdownPath, ".md"),
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 fs5.readFile(interview2.markdownPath, "utf8");
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 path7 of homebrewPaths) {
5973
- if (existsSync6(path7) && isValidBinary(path7)) {
5974
- return path7;
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(path7) {
5992
- resolvedCliPath = path7;
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 path10 from "path";
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 path7 from "path";
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 = path7.parse(initialName);
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 = path7.join(binaryDir, candidateName);
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 path8 from "path";
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((suffix) => host.endsWith(suffix)) || DOCS_HOST_PREFIXES.some((prefix) => host.startsWith(prefix));
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((prefix) => mime.startsWith(prefix));
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 = path8.parse(name);
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 readFile2 } from "fs/promises";
9175
- import path9 from "path";
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 = path9.join(baseDir, file);
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 readFile2(configPath, "utf8");
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 = path9.join(directory, ".opencode");
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 || path10.join(os3.tmpdir(), "opencode-smartfetch");
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 postFileToolNudgeHook = createPostFileToolNudgeHook();
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
- if (input.agent) {
10072
- sessionAgentMap.set(input.sessionID, input.agent);
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
- ` + output.system[0] : "");
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;