@oh-my-pi/pi-coding-agent 16.2.1 → 16.2.3
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/CHANGELOG.md +72 -0
- package/dist/cli.js +3621 -3579
- package/dist/types/advisor/__tests__/config.test.d.ts +1 -0
- package/dist/types/advisor/advise-tool.d.ts +8 -4
- package/dist/types/advisor/config.d.ts +88 -0
- package/dist/types/advisor/index.d.ts +1 -0
- package/dist/types/advisor/runtime.d.ts +15 -1
- package/dist/types/advisor/transcript-recorder.d.ts +13 -2
- package/dist/types/advisor/watchdog.d.ts +20 -0
- package/dist/types/collab/guest.d.ts +29 -0
- package/dist/types/config/model-roles.d.ts +1 -1
- package/dist/types/config/settings-schema.d.ts +113 -12
- package/dist/types/debug/log-viewer.d.ts +1 -0
- package/dist/types/debug/raw-sse.d.ts +1 -0
- package/dist/types/discovery/omp-extension-roots.d.ts +3 -3
- package/dist/types/edit/hashline/diff.d.ts +0 -11
- package/dist/types/edit/index.d.ts +18 -0
- package/dist/types/edit/streaming.d.ts +30 -0
- package/dist/types/extensibility/custom-tools/types.d.ts +1 -0
- package/dist/types/extensibility/shared-events.d.ts +1 -0
- package/dist/types/extensibility/tool-event-input.d.ts +7 -0
- package/dist/types/extensibility/utils.d.ts +12 -0
- package/dist/types/mcp/oauth-discovery.d.ts +0 -11
- package/dist/types/mcp/transports/index.d.ts +1 -0
- package/dist/types/mcp/transports/sse.d.ts +20 -0
- package/dist/types/modes/components/advisor-config.d.ts +59 -0
- package/dist/types/modes/components/index.d.ts +1 -0
- package/dist/types/modes/components/model-selector.d.ts +9 -1
- package/dist/types/modes/components/settings-selector.d.ts +1 -0
- package/dist/types/modes/components/status-line/component.d.ts +30 -3
- package/dist/types/modes/components/status-line/types.d.ts +13 -1
- package/dist/types/modes/controllers/selector-controller.d.ts +1 -0
- package/dist/types/modes/interactive-mode.d.ts +10 -4
- package/dist/types/modes/skill-command.d.ts +32 -0
- package/dist/types/modes/types.d.ts +7 -2
- package/dist/types/sdk.d.ts +1 -1
- package/dist/types/session/agent-session.d.ts +84 -12
- package/dist/types/session/indexed-session-storage.d.ts +7 -1
- package/dist/types/session/messages.d.ts +32 -7
- package/dist/types/session/messages.test.d.ts +1 -0
- package/dist/types/session/session-entries.d.ts +31 -3
- package/dist/types/session/session-history-format.d.ts +6 -0
- package/dist/types/session/session-loader.d.ts +9 -1
- package/dist/types/session/session-manager.d.ts +6 -4
- package/dist/types/session/session-storage.d.ts +11 -0
- package/dist/types/session/session-title-slot.d.ts +19 -0
- package/dist/types/session/settings-stream-fn.d.ts +21 -0
- package/dist/types/session/turn-persistence.d.ts +88 -0
- package/dist/types/ssh/connection-manager.d.ts +47 -0
- package/dist/types/ssh/utils.d.ts +16 -0
- package/dist/types/task/executor.d.ts +3 -16
- package/dist/types/task/render.d.ts +0 -5
- package/dist/types/task/renderer.d.ts +13 -0
- package/dist/types/task/types.d.ts +16 -0
- package/dist/types/task/yield-assembly.d.ts +28 -0
- package/dist/types/tiny/text.d.ts +8 -0
- package/dist/types/tools/render-utils.d.ts +2 -0
- package/dist/types/tools/review.d.ts +6 -4
- package/dist/types/tools/ssh.d.ts +1 -1
- package/dist/types/tools/todo.d.ts +6 -0
- package/dist/types/tools/yield.d.ts +8 -3
- package/dist/types/utils/thinking-display.d.ts +4 -0
- package/package.json +12 -12
- package/src/advisor/__tests__/advisor.test.ts +438 -10
- package/src/advisor/__tests__/config.test.ts +173 -0
- package/src/advisor/advise-tool.ts +11 -6
- package/src/advisor/config.ts +256 -0
- package/src/advisor/index.ts +1 -0
- package/src/advisor/runtime.ts +77 -4
- package/src/advisor/transcript-recorder.ts +25 -2
- package/src/advisor/watchdog.ts +57 -31
- package/src/auto-thinking/classifier.ts +2 -2
- package/src/autoresearch/index.ts +7 -2
- package/src/cli/gc-cli.ts +17 -10
- package/src/collab/guest.ts +43 -7
- package/src/config/model-registry.ts +80 -18
- package/src/config/model-resolver.ts +5 -1
- package/src/config/model-roles.ts +3 -3
- package/src/config/settings-schema.ts +107 -8
- package/src/debug/index.ts +32 -7
- package/src/debug/log-viewer.ts +111 -53
- package/src/debug/raw-sse.ts +68 -48
- package/src/discovery/codex.ts +13 -5
- package/src/discovery/omp-extension-roots.ts +38 -13
- package/src/edit/hashline/diff.ts +57 -4
- package/src/edit/index.ts +21 -0
- package/src/edit/streaming.ts +170 -0
- package/src/eval/js/shared/local-module-loader.ts +23 -1
- package/src/export/html/template.js +13 -7
- package/src/extensibility/custom-tools/types.ts +1 -0
- package/src/extensibility/extensions/loader.ts +5 -3
- package/src/extensibility/extensions/wrapper.ts +9 -3
- package/src/extensibility/hooks/loader.ts +3 -3
- package/src/extensibility/hooks/tool-wrapper.ts +13 -4
- package/src/extensibility/plugins/legacy-pi-bundled-keys.ts +18 -1
- package/src/extensibility/plugins/legacy-pi-bundled-registry.ts +59 -2
- package/src/extensibility/plugins/manager.ts +76 -5
- package/src/extensibility/shared-events.ts +1 -0
- package/src/extensibility/tool-event-input.ts +23 -0
- package/src/extensibility/utils.ts +74 -0
- package/src/internal-urls/docs-index.generated.txt +1 -1
- package/src/mcp/client.ts +3 -1
- package/src/mcp/manager.ts +12 -5
- package/src/mcp/oauth-discovery.ts +5 -29
- package/src/mcp/transports/http.ts +3 -1
- package/src/mcp/transports/index.ts +1 -0
- package/src/mcp/transports/sse.ts +377 -0
- package/src/memories/index.ts +15 -6
- package/src/mnemopi/backend.ts +2 -2
- package/src/modes/acp/acp-agent.ts +1 -1
- package/src/modes/components/advisor-config.ts +555 -0
- package/src/modes/components/advisor-message.ts +9 -2
- package/src/modes/components/agent-hub.ts +9 -4
- package/src/modes/components/assistant-message.ts +5 -5
- package/src/modes/components/index.ts +2 -0
- package/src/modes/components/model-selector.ts +79 -48
- package/src/modes/components/settings-selector.ts +1 -0
- package/src/modes/components/status-line/component.ts +145 -14
- package/src/modes/components/status-line/segments.ts +47 -22
- package/src/modes/components/status-line/types.ts +13 -1
- package/src/modes/components/tool-execution.ts +47 -6
- package/src/modes/controllers/command-controller.ts +23 -2
- package/src/modes/controllers/event-controller.ts +114 -11
- package/src/modes/controllers/extension-ui-controller.ts +1 -1
- package/src/modes/controllers/input-controller.ts +61 -61
- package/src/modes/controllers/selector-controller.ts +100 -9
- package/src/modes/interactive-mode.ts +65 -10
- package/src/modes/print-mode.ts +1 -1
- package/src/modes/skill-command.ts +116 -0
- package/src/modes/types.ts +7 -2
- package/src/modes/utils/transcript-render-helpers.ts +2 -2
- package/src/modes/utils/ui-helpers.ts +46 -27
- package/src/prompts/agents/reviewer.md +11 -10
- package/src/prompts/review-custom-request.md +1 -2
- package/src/prompts/review-request.md +1 -2
- package/src/prompts/system/interrupted-thinking.md +7 -0
- package/src/prompts/system/recap-user.md +9 -0
- package/src/prompts/system/subagent-system-prompt.md +8 -5
- package/src/prompts/system/subagent-yield-reminder.md +6 -5
- package/src/prompts/system/system-prompt.md +0 -1
- package/src/prompts/tools/irc.md +2 -2
- package/src/prompts/tools/read.md +2 -2
- package/src/sdk.ts +40 -50
- package/src/session/agent-session.ts +1139 -600
- package/src/session/indexed-session-storage.ts +86 -13
- package/src/session/messages.test.ts +125 -0
- package/src/session/messages.ts +192 -21
- package/src/session/redis-session-storage.ts +49 -2
- package/src/session/session-entries.ts +39 -2
- package/src/session/session-history-format.ts +29 -2
- package/src/session/session-listing.ts +54 -24
- package/src/session/session-loader.ts +66 -3
- package/src/session/session-manager.ts +113 -19
- package/src/session/session-persistence.ts +96 -3
- package/src/session/session-storage.ts +36 -0
- package/src/session/session-title-slot.ts +141 -0
- package/src/session/settings-stream-fn.ts +49 -0
- package/src/session/sql-session-storage.ts +71 -11
- package/src/session/turn-persistence.ts +142 -0
- package/src/session/unexpected-stop-classifier.ts +2 -2
- package/src/slash-commands/builtin-registry.ts +16 -3
- package/src/slash-commands/helpers/mcp.ts +2 -1
- package/src/ssh/__tests__/connection-manager-args.test.ts +123 -1
- package/src/ssh/__tests__/file-transfer-posix-guard.test.ts +55 -18
- package/src/ssh/connection-manager.ts +139 -12
- package/src/ssh/file-transfer.ts +23 -18
- package/src/ssh/ssh-executor.ts +2 -13
- package/src/ssh/utils.ts +19 -0
- package/src/task/executor.ts +21 -23
- package/src/task/render.ts +162 -20
- package/src/task/renderer.ts +14 -0
- package/src/task/types.ts +17 -0
- package/src/task/yield-assembly.ts +207 -0
- package/src/tiny/models.ts +8 -6
- package/src/tiny/text.ts +37 -7
- package/src/tools/ask.ts +55 -4
- package/src/tools/image-gen.ts +2 -1
- package/src/tools/render-utils.ts +2 -0
- package/src/tools/renderers.ts +8 -2
- package/src/tools/review.ts +17 -7
- package/src/tools/ssh.ts +8 -4
- package/src/tools/todo.ts +17 -1
- package/src/tools/tts.ts +2 -1
- package/src/tools/yield.ts +140 -31
- package/src/utils/thinking-display.ts +15 -0
- package/src/utils/title-generator.ts +1 -1
- package/src/web/search/providers/tavily.ts +36 -19
package/src/debug/raw-sse.ts
CHANGED
|
@@ -1,13 +1,6 @@
|
|
|
1
|
-
import {
|
|
2
|
-
type Component,
|
|
3
|
-
matchesKey,
|
|
4
|
-
padding,
|
|
5
|
-
replaceTabs,
|
|
6
|
-
ScrollView,
|
|
7
|
-
truncateToWidth,
|
|
8
|
-
visibleWidth,
|
|
9
|
-
} from "@oh-my-pi/pi-tui";
|
|
1
|
+
import { type Component, matchesKey, parseSgrMouse, replaceTabs, ScrollView, truncateToWidth } from "@oh-my-pi/pi-tui";
|
|
10
2
|
import { sanitizeText } from "@oh-my-pi/pi-utils";
|
|
3
|
+
import { bottomBorder, divider, row, topBorder } from "../modes/components/overlay-box";
|
|
11
4
|
import { theme } from "../modes/theme/theme";
|
|
12
5
|
import { copyToClipboard } from "../utils/clipboard";
|
|
13
6
|
import {
|
|
@@ -17,8 +10,8 @@ import {
|
|
|
17
10
|
rawSseRecordLines,
|
|
18
11
|
} from "./raw-sse-buffer";
|
|
19
12
|
|
|
20
|
-
const MIN_VIEWER_WIDTH =
|
|
21
|
-
const
|
|
13
|
+
const MIN_VIEWER_WIDTH = 40;
|
|
14
|
+
const VIEWER_CHROME_LINES = 6;
|
|
22
15
|
// `data:` lines below this width render fine on a single row; anything wider gets pretty-printed
|
|
23
16
|
// across multiple `data:` lines so streamed JSON blobs stop getting clipped by `truncateToWidth`.
|
|
24
17
|
const PRETTY_PRINT_DATA_THRESHOLD = 100;
|
|
@@ -79,6 +72,8 @@ export class RawSseViewerComponent implements Component {
|
|
|
79
72
|
#followTail = true;
|
|
80
73
|
#lastRenderWidth = MIN_VIEWER_WIDTH;
|
|
81
74
|
#statusMessage: string | undefined;
|
|
75
|
+
#bodyRowStart = 0;
|
|
76
|
+
#bodyRowCount = 0;
|
|
82
77
|
// Pretty-printed wire lines keyed by `record.sequence`. Pretty-printing is
|
|
83
78
|
// the JSON.parse + JSON.stringify per `data:` line, so we cache the result —
|
|
84
79
|
// the render path runs on every keypress and from `#maxScrollOffset()`.
|
|
@@ -98,9 +93,17 @@ export class RawSseViewerComponent implements Component {
|
|
|
98
93
|
});
|
|
99
94
|
}
|
|
100
95
|
|
|
96
|
+
dispose(): void {
|
|
97
|
+
this.#unsubscribe();
|
|
98
|
+
}
|
|
99
|
+
|
|
101
100
|
handleInput(keyData: string): void {
|
|
101
|
+
if (keyData.startsWith("\x1b[<") && this.#handleMouse(keyData)) {
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
|
|
102
105
|
if (matchesKey(keyData, "escape") || matchesKey(keyData, "esc")) {
|
|
103
|
-
this
|
|
106
|
+
this.dispose();
|
|
104
107
|
this.#onExit();
|
|
105
108
|
return;
|
|
106
109
|
}
|
|
@@ -145,15 +148,44 @@ export class RawSseViewerComponent implements Component {
|
|
|
145
148
|
}
|
|
146
149
|
}
|
|
147
150
|
|
|
151
|
+
#handleMouse(keyData: string): boolean {
|
|
152
|
+
const event = parseSgrMouse(keyData);
|
|
153
|
+
if (!event) return false;
|
|
154
|
+
|
|
155
|
+
const overBody = event.row >= this.#bodyRowStart && event.row < this.#bodyRowStart + this.#bodyRowCount;
|
|
156
|
+
if (event.wheel !== null && overBody) {
|
|
157
|
+
this.#followTail = false;
|
|
158
|
+
this.#scrollOffset = Math.max(0, Math.min(this.#maxScrollOffset(), this.#scrollOffset + event.wheel * 3));
|
|
159
|
+
this.#onUpdate?.();
|
|
160
|
+
return true;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
if (!event.leftClick) return false;
|
|
164
|
+
if (event.row === 1) {
|
|
165
|
+
this.#followTail = !this.#followTail;
|
|
166
|
+
this.#followIfNeeded();
|
|
167
|
+
this.#onUpdate?.();
|
|
168
|
+
return true;
|
|
169
|
+
}
|
|
170
|
+
if (overBody) {
|
|
171
|
+
this.#followTail = false;
|
|
172
|
+
const clickedOffset = this.#scrollOffset + event.row - this.#bodyRowStart;
|
|
173
|
+
this.#scrollOffset = Math.max(0, Math.min(this.#maxScrollOffset(), clickedOffset));
|
|
174
|
+
this.#onUpdate?.();
|
|
175
|
+
return true;
|
|
176
|
+
}
|
|
177
|
+
return false;
|
|
178
|
+
}
|
|
179
|
+
|
|
148
180
|
invalidate(): void {}
|
|
149
181
|
|
|
150
182
|
render(width: number): readonly string[] {
|
|
151
183
|
this.#lastRenderWidth = Math.max(MIN_VIEWER_WIDTH, width);
|
|
152
184
|
this.#followIfNeeded();
|
|
153
185
|
|
|
154
|
-
const
|
|
186
|
+
const contentWidth = Math.max(1, this.#lastRenderWidth - 4);
|
|
155
187
|
const bodyHeight = this.#bodyHeight();
|
|
156
|
-
const rawLines = this.#renderRawLines(
|
|
188
|
+
const rawLines = this.#renderRawLines(contentWidth);
|
|
157
189
|
const sv = new ScrollView(rawLines.slice(this.#scrollOffset, this.#scrollOffset + bodyHeight), {
|
|
158
190
|
height: bodyHeight,
|
|
159
191
|
scrollbar: "auto",
|
|
@@ -161,15 +193,18 @@ export class RawSseViewerComponent implements Component {
|
|
|
161
193
|
theme: { track: t => theme.fg("muted", t), thumb: t => theme.fg("accent", t) },
|
|
162
194
|
});
|
|
163
195
|
sv.setScrollOffset(this.#scrollOffset);
|
|
164
|
-
const bodyRows = sv.render(
|
|
196
|
+
const bodyRows = sv.render(contentWidth);
|
|
197
|
+
this.#bodyRowStart = 3;
|
|
198
|
+
this.#bodyRowCount = bodyHeight;
|
|
165
199
|
|
|
166
200
|
return [
|
|
167
|
-
this.#
|
|
168
|
-
|
|
169
|
-
this.#
|
|
170
|
-
...bodyRows.map(line =>
|
|
171
|
-
|
|
172
|
-
this.#
|
|
201
|
+
topBorder(this.#lastRenderWidth, "Raw Provider Stream"),
|
|
202
|
+
row(this.#summaryText(), this.#lastRenderWidth),
|
|
203
|
+
divider(this.#lastRenderWidth),
|
|
204
|
+
...bodyRows.map(line => row(line, this.#lastRenderWidth)),
|
|
205
|
+
divider(this.#lastRenderWidth),
|
|
206
|
+
row(this.#statusText(), this.#lastRenderWidth),
|
|
207
|
+
bottomBorder(this.#lastRenderWidth),
|
|
173
208
|
];
|
|
174
209
|
}
|
|
175
210
|
|
|
@@ -221,22 +256,25 @@ export class RawSseViewerComponent implements Component {
|
|
|
221
256
|
if (key < firstSequence) this.#prettyLinesCache.delete(key);
|
|
222
257
|
}
|
|
223
258
|
}
|
|
224
|
-
|
|
225
259
|
#summaryText(): string {
|
|
226
260
|
const snapshot = this.#buffer.snapshot();
|
|
227
|
-
const last = snapshot.lastUpdatedAt
|
|
228
|
-
|
|
229
|
-
|
|
261
|
+
const last = snapshot.lastUpdatedAt
|
|
262
|
+
? `${theme.fg("muted", "last")} ${theme.fg("accent", formatRawSseIsoTime(snapshot.lastUpdatedAt))}`
|
|
263
|
+
: theme.fg("muted", "waiting for first frame");
|
|
264
|
+
const follow = this.#followTail ? theme.fg("success", "follow on") : theme.fg("warning", "follow off");
|
|
265
|
+
return `${theme.fg("muted", "events")} ${theme.fg("accent", String(snapshot.totalEvents))} ${theme.fg("muted", "records")} ${theme.fg("accent", String(snapshot.records.length))} ${last} ${follow}`;
|
|
230
266
|
}
|
|
231
267
|
|
|
232
268
|
#statusText(): string {
|
|
233
|
-
|
|
269
|
+
const help = "Esc close · Ctrl+C copy raw · End follow tail · wheel scroll · click summary toggles follow";
|
|
270
|
+
return this.#statusMessage
|
|
271
|
+
? `${theme.fg("success", this.#statusMessage)} ${theme.fg("dim", help)}`
|
|
272
|
+
: theme.fg("dim", help);
|
|
234
273
|
}
|
|
235
274
|
|
|
236
275
|
#bodyHeight(): number {
|
|
237
|
-
return Math.max(3, this.#terminalRows -
|
|
276
|
+
return Math.max(3, (process.stdout.rows || this.#terminalRows || 24) - VIEWER_CHROME_LINES);
|
|
238
277
|
}
|
|
239
|
-
|
|
240
278
|
#followIfNeeded(): void {
|
|
241
279
|
if (this.#followTail) this.#scrollToTail();
|
|
242
280
|
}
|
|
@@ -246,8 +284,8 @@ export class RawSseViewerComponent implements Component {
|
|
|
246
284
|
}
|
|
247
285
|
|
|
248
286
|
#maxScrollOffset(): number {
|
|
249
|
-
const
|
|
250
|
-
return Math.max(0, this.#renderRawLines(
|
|
287
|
+
const contentWidth = Math.max(1, this.#lastRenderWidth - 4);
|
|
288
|
+
return Math.max(0, this.#renderRawLines(contentWidth).length - this.#bodyHeight());
|
|
251
289
|
}
|
|
252
290
|
|
|
253
291
|
#copyAll(): void {
|
|
@@ -271,22 +309,4 @@ export class RawSseViewerComponent implements Component {
|
|
|
271
309
|
}
|
|
272
310
|
this.#onUpdate?.();
|
|
273
311
|
}
|
|
274
|
-
|
|
275
|
-
#frameTop(innerWidth: number): string {
|
|
276
|
-
return `${theme.boxRound.topLeft}${theme.boxRound.horizontal.repeat(innerWidth)}${theme.boxRound.topRight}`;
|
|
277
|
-
}
|
|
278
|
-
|
|
279
|
-
#frameSeparator(innerWidth: number): string {
|
|
280
|
-
return `${theme.boxRound.teeRight}${theme.boxRound.horizontal.repeat(innerWidth)}${theme.boxRound.teeLeft}`;
|
|
281
|
-
}
|
|
282
|
-
|
|
283
|
-
#frameBottom(innerWidth: number): string {
|
|
284
|
-
return `${theme.boxRound.bottomLeft}${theme.boxRound.horizontal.repeat(innerWidth)}${theme.boxRound.bottomRight}`;
|
|
285
|
-
}
|
|
286
|
-
|
|
287
|
-
#frameLine(content: string, innerWidth: number): string {
|
|
288
|
-
const truncated = truncateToWidth(content, innerWidth);
|
|
289
|
-
const remaining = Math.max(0, innerWidth - visibleWidth(truncated));
|
|
290
|
-
return `${theme.boxRound.vertical}${truncated}${padding(remaining)}${theme.boxRound.vertical}`;
|
|
291
|
-
}
|
|
292
312
|
}
|
package/src/discovery/codex.ts
CHANGED
|
@@ -342,12 +342,20 @@ async function loadHooks(ctx: LoadContext): Promise<LoadResult<Hook>> {
|
|
|
342
342
|
const codexDir = getProjectCodexDir(ctx);
|
|
343
343
|
const projectHooksDir = path.join(codexDir, "hooks");
|
|
344
344
|
|
|
345
|
+
// OMP hooks must be named `pre-<tool>.<ts|js>` or `post-<tool>.<ts|js>`.
|
|
346
|
+
// Files without that prefix are not OMP hooks (e.g. the standalone Codex
|
|
347
|
+
// hook scripts users keep alongside) — silently dropping the prefix and
|
|
348
|
+
// defaulting to `pre:<basename>` caused those scripts to be imported as
|
|
349
|
+
// extension factories and any top-level `process.exit()` killed startup
|
|
350
|
+
// (#3680).
|
|
345
351
|
const transformHook =
|
|
346
|
-
(level: "user" | "project") =>
|
|
352
|
+
(level: "user" | "project") =>
|
|
353
|
+
(name: string, _content: string, path: string, source: SourceMeta): Hook | null => {
|
|
347
354
|
const baseName = name.replace(/\.(ts|js)$/, "");
|
|
348
355
|
const match = baseName.match(/^(pre|post)-(.+)$/);
|
|
349
|
-
|
|
350
|
-
const
|
|
356
|
+
if (!match) return null;
|
|
357
|
+
const hookType = match[1] as "pre" | "post";
|
|
358
|
+
const toolName = match[2];
|
|
351
359
|
return {
|
|
352
360
|
name,
|
|
353
361
|
path,
|
|
@@ -359,11 +367,11 @@ async function loadHooks(ctx: LoadContext): Promise<LoadResult<Hook>> {
|
|
|
359
367
|
};
|
|
360
368
|
|
|
361
369
|
const results = await Promise.all([
|
|
362
|
-
loadFilesFromDir(ctx, userHooksDir, PROVIDER_ID, "user", {
|
|
370
|
+
loadFilesFromDir<Hook>(ctx, userHooksDir, PROVIDER_ID, "user", {
|
|
363
371
|
extensions: ["ts", "js"],
|
|
364
372
|
transform: transformHook("user"),
|
|
365
373
|
}),
|
|
366
|
-
loadFilesFromDir(ctx, projectHooksDir, PROVIDER_ID, "project", {
|
|
374
|
+
loadFilesFromDir<Hook>(ctx, projectHooksDir, PROVIDER_ID, "project", {
|
|
367
375
|
extensions: ["ts", "js"],
|
|
368
376
|
transform: transformHook("project"),
|
|
369
377
|
}),
|
|
@@ -22,6 +22,7 @@ import { readDirEntries, readFile } from "../capability/fs";
|
|
|
22
22
|
import type { LoadContext } from "../capability/types";
|
|
23
23
|
import { getEnabledPlugins } from "../extensibility/plugins/loader";
|
|
24
24
|
import { expandTilde } from "../tools/path-utils";
|
|
25
|
+
import { listClaudePluginRoots } from "./helpers";
|
|
25
26
|
|
|
26
27
|
/** A resolved extension package directory wired into the discovery surfaces. */
|
|
27
28
|
export interface OmpExtensionRoot {
|
|
@@ -123,9 +124,9 @@ async function isDirectory(p: string): Promise<boolean> {
|
|
|
123
124
|
* 1. CLI roots injected via {@link injectOmpExtensionCliRoots}
|
|
124
125
|
* 2. Project `<cwd>/.omp/settings.json#extensions`
|
|
125
126
|
* 3. User `~/.omp/agent/settings.json#extensions`
|
|
126
|
-
* 4. Enabled plugins installed under `<plugins>/node_modules/` (
|
|
127
|
-
* `omp install <pkg>` / `omp plugin install` / `omp plugin link`)
|
|
128
|
-
*
|
|
127
|
+
* 4. Enabled npm/link plugins installed under `<plugins>/node_modules/` (for
|
|
128
|
+
* `omp install <pkg>` / `omp plugin install` / `omp plugin link`). Marketplace
|
|
129
|
+
* installs are loaded by the `claude-plugins` provider and are excluded here.
|
|
129
130
|
* Only entries that resolve to a directory on disk are returned; file
|
|
130
131
|
* entrypoints contribute zero sub-discovery surface and are filtered out.
|
|
131
132
|
* Installed-plugin enumeration failures (missing lockfile, unreadable
|
|
@@ -167,20 +168,44 @@ export async function listOmpExtensionRoots(ctx: LoadContext): Promise<OmpExtens
|
|
|
167
168
|
}
|
|
168
169
|
|
|
169
170
|
/**
|
|
170
|
-
* Enumerate every enabled
|
|
171
|
-
*
|
|
172
|
-
*
|
|
173
|
-
*
|
|
171
|
+
* Enumerate every enabled npm/link plugin's package directory so its conventional
|
|
172
|
+
* `skills/`, `hooks/`, `tools/`, `commands/`, `rules/`, `prompts/`, and
|
|
173
|
+
* `.mcp.json` are wired into discovery — mirrors how `getAllPluginExtensionPaths`
|
|
174
|
+
* already feeds the extension factory loader.
|
|
174
175
|
*
|
|
175
|
-
* Marketplace
|
|
176
|
-
*
|
|
177
|
-
*
|
|
178
|
-
*
|
|
176
|
+
* Marketplace installs also create runtime symlinks for enable-state persistence,
|
|
177
|
+
* but their resources are discovered through the `claude-plugins` provider.
|
|
178
|
+
* Filtering them here prevents `/status` from showing the same plugin under both
|
|
179
|
+
* "Claude Code Marketplace" and "OMP Extension Packages".
|
|
179
180
|
*/
|
|
181
|
+
async function realpathOrResolved(p: string): Promise<string> {
|
|
182
|
+
try {
|
|
183
|
+
return await fs.realpath(p);
|
|
184
|
+
} catch (err) {
|
|
185
|
+
if (isEnoent(err)) return path.resolve(p);
|
|
186
|
+
throw err;
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
180
190
|
async function listInstalledPluginRoots(ctx: LoadContext): Promise<InjectedRoot[]> {
|
|
181
191
|
try {
|
|
182
|
-
const plugins = await
|
|
183
|
-
|
|
192
|
+
const [plugins, marketplaceRoots] = await Promise.all([
|
|
193
|
+
getEnabledPlugins(ctx.cwd, { home: ctx.home }),
|
|
194
|
+
listClaudePluginRoots(ctx.home, ctx.cwd),
|
|
195
|
+
]);
|
|
196
|
+
const marketplaceRealpaths = new Set(
|
|
197
|
+
await Promise.all(marketplaceRoots.roots.map(root => realpathOrResolved(root.path))),
|
|
198
|
+
);
|
|
199
|
+
const installedRoots = await Promise.all(
|
|
200
|
+
plugins.map(async plugin => ({
|
|
201
|
+
path: plugin.path,
|
|
202
|
+
scope: plugin.scope,
|
|
203
|
+
realpath: await realpathOrResolved(plugin.path),
|
|
204
|
+
})),
|
|
205
|
+
);
|
|
206
|
+
return installedRoots
|
|
207
|
+
.filter(root => !marketplaceRealpaths.has(root.realpath))
|
|
208
|
+
.map(({ path: p, scope }) => ({ path: p, level: scope }));
|
|
184
209
|
} catch (err) {
|
|
185
210
|
logger.debug("listInstalledPluginRoots: enumeration failed", { error: String(err) });
|
|
186
211
|
return [];
|
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
* match is accepted even when the tag was minted by a source that did not keep
|
|
10
10
|
* history, and stale tags recover through the session snapshot store when possible.
|
|
11
11
|
*/
|
|
12
|
+
import * as path from "node:path";
|
|
12
13
|
import {
|
|
13
14
|
type ApplyResult,
|
|
14
15
|
applyEdits,
|
|
@@ -30,6 +31,7 @@ import {
|
|
|
30
31
|
} from "@oh-my-pi/hashline";
|
|
31
32
|
import { resolveToCwd } from "../../tools/path-utils";
|
|
32
33
|
import { generateDiffString } from "../diff";
|
|
34
|
+
import { canonicalSnapshotKey } from "../file-snapshot-store";
|
|
33
35
|
import { readEditFileText } from "../read-file";
|
|
34
36
|
import { nativeBlockResolver } from "./block-resolver";
|
|
35
37
|
|
|
@@ -90,6 +92,54 @@ async function readSectionTextCached(absolutePath: string, sectionPath: string):
|
|
|
90
92
|
return rawContent;
|
|
91
93
|
}
|
|
92
94
|
|
|
95
|
+
/**
|
|
96
|
+
* Resolve a missing authored path to a file read this session by matching its
|
|
97
|
+
* basename and snapshot tag, mirroring {@link Patcher}'s apply-time recovery so
|
|
98
|
+
* a bare/wrong-directory `[basename#tag]` header previews against the same file
|
|
99
|
+
* the edit will land on. Returns `undefined` when no unique basename+tag match
|
|
100
|
+
* exists, leaving the caller to surface the original read error.
|
|
101
|
+
*/
|
|
102
|
+
function recoverSectionPathFromTag(
|
|
103
|
+
section: PatchSection,
|
|
104
|
+
authoredAbsolutePath: string,
|
|
105
|
+
snapshots: SnapshotStore,
|
|
106
|
+
): string | undefined {
|
|
107
|
+
if (section.fileHash === undefined) return undefined;
|
|
108
|
+
const authoredName = path.basename(section.path);
|
|
109
|
+
const authoredKey = canonicalSnapshotKey(authoredAbsolutePath);
|
|
110
|
+
const candidates = [
|
|
111
|
+
...new Set(
|
|
112
|
+
snapshots
|
|
113
|
+
.findByHash(section.fileHash)
|
|
114
|
+
.filter(snapshot => path.basename(snapshot.path) === authoredName)
|
|
115
|
+
.map(snapshot => snapshot.path),
|
|
116
|
+
),
|
|
117
|
+
].filter(candidate => candidate !== authoredKey);
|
|
118
|
+
return candidates.length === 1 ? candidates[0] : undefined;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Read the section's target file for a preview, recovering a bare/mis-typed
|
|
123
|
+
* `[basename#tag]` path onto the file its tag uniquely names. Recovery fires
|
|
124
|
+
* only when the authored path is absent — matching {@link Patcher}'s apply-time
|
|
125
|
+
* order — so a permission/parse error on an existing file surfaces against the
|
|
126
|
+
* authored path instead of silently previewing a different tagged file. Returns
|
|
127
|
+
* the path actually read so callers key snapshot lookups off the same file.
|
|
128
|
+
*/
|
|
129
|
+
async function readSectionForPreview(
|
|
130
|
+
section: PatchSection,
|
|
131
|
+
authoredAbsolutePath: string,
|
|
132
|
+
snapshots: SnapshotStore,
|
|
133
|
+
streaming: boolean | undefined,
|
|
134
|
+
): Promise<{ absolutePath: string; rawContent: string }> {
|
|
135
|
+
const read = streaming ? readSectionTextCached : readSectionText;
|
|
136
|
+
const recovered = (await Bun.file(authoredAbsolutePath).exists())
|
|
137
|
+
? undefined
|
|
138
|
+
: recoverSectionPathFromTag(section, authoredAbsolutePath, snapshots);
|
|
139
|
+
const target = recovered ?? authoredAbsolutePath;
|
|
140
|
+
return { absolutePath: target, rawContent: await read(target, section.path) };
|
|
141
|
+
}
|
|
142
|
+
|
|
93
143
|
function hasAnchorScopedEdit(edits: readonly Edit[]): boolean {
|
|
94
144
|
return edits.some(edit => {
|
|
95
145
|
if (edit.kind === "delete") return true;
|
|
@@ -258,10 +308,13 @@ export async function computeHashlineSectionDiff(
|
|
|
258
308
|
options: HashlineDiffOptions = {},
|
|
259
309
|
): Promise<{ diff: string; firstChangedLine: number | undefined } | { error: string }> {
|
|
260
310
|
try {
|
|
261
|
-
const
|
|
262
|
-
const rawContent =
|
|
263
|
-
|
|
264
|
-
|
|
311
|
+
const authoredPath = resolveToCwd(section.path, cwd);
|
|
312
|
+
const { absolutePath, rawContent } = await readSectionForPreview(
|
|
313
|
+
section,
|
|
314
|
+
authoredPath,
|
|
315
|
+
snapshots,
|
|
316
|
+
options.streaming,
|
|
317
|
+
);
|
|
265
318
|
const { text: content } = stripBom(rawContent);
|
|
266
319
|
const normalized = normalizeToLF(content);
|
|
267
320
|
// Streaming favors a stable, monotonic preview over an exact unified
|
package/src/edit/index.ts
CHANGED
|
@@ -397,6 +397,27 @@ export class EditTool implements AgentTool<TInput> {
|
|
|
397
397
|
return EDIT_MODE_STRATEGIES[this.mode].matcherDigest(args);
|
|
398
398
|
}
|
|
399
399
|
|
|
400
|
+
/**
|
|
401
|
+
* Project the streamed args onto their target file paths so path-scoped
|
|
402
|
+
* stream matchers (e.g. TTSR `tool:edit(*.ts)` globs) match hashline and
|
|
403
|
+
* apply_patch edits even though the path lives in the wire payload (a
|
|
404
|
+
* section header / envelope marker) rather than a top-level argument.
|
|
405
|
+
*/
|
|
406
|
+
matcherPaths(args: unknown): readonly string[] | undefined {
|
|
407
|
+
return EDIT_MODE_STRATEGIES[this.mode].matcherPaths(args);
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
/**
|
|
411
|
+
* Per-file projection of the streamed args, splitting multi-section
|
|
412
|
+
* hashline / multi-hunk apply_patch payloads into one (path, digest) entry
|
|
413
|
+
* per touched file. Path-scoped stream matchers (TTSR) then evaluate each
|
|
414
|
+
* file in isolation, so a `tool:edit(*.ts)` rule never fires on text that
|
|
415
|
+
* actually belongs to a sibling Markdown hunk.
|
|
416
|
+
*/
|
|
417
|
+
matcherEntries(args: unknown): readonly { path: string; digest: string }[] | undefined {
|
|
418
|
+
return EDIT_MODE_STRATEGIES[this.mode].matcherEntries(args);
|
|
419
|
+
}
|
|
420
|
+
|
|
400
421
|
async execute(
|
|
401
422
|
_toolCallId: string,
|
|
402
423
|
params: EditParams,
|
package/src/edit/streaming.ts
CHANGED
|
@@ -52,6 +52,17 @@ export interface StreamingDiffContext {
|
|
|
52
52
|
isStreaming?: boolean;
|
|
53
53
|
}
|
|
54
54
|
|
|
55
|
+
/**
|
|
56
|
+
* Per-file projection of a streamed edit payload. Pairs one target file path
|
|
57
|
+
* with the digest of only the lines added to that file, so path-scoped stream
|
|
58
|
+
* matchers (TTSR) evaluate each file in isolation — a `tool:edit(*.ts)` rule
|
|
59
|
+
* never fires on text that actually belongs to a sibling `README.md` hunk.
|
|
60
|
+
*/
|
|
61
|
+
export interface EditMatcherEntry {
|
|
62
|
+
readonly path: string;
|
|
63
|
+
readonly digest: string;
|
|
64
|
+
}
|
|
65
|
+
|
|
55
66
|
export interface EditStreamingStrategy<Args = unknown> {
|
|
56
67
|
/**
|
|
57
68
|
* Return the args restricted to edits that are "complete enough" to
|
|
@@ -77,6 +88,26 @@ export interface EditStreamingStrategy<Args = unknown> {
|
|
|
77
88
|
* args don't yet carry any content.
|
|
78
89
|
*/
|
|
79
90
|
matcherDigest(args: Args): string | undefined;
|
|
91
|
+
/**
|
|
92
|
+
* Surface the target file paths a (potentially partial) call would touch,
|
|
93
|
+
* so path-scoped stream matchers (e.g. TTSR `tool:edit(*.ts)` globs) match
|
|
94
|
+
* even when the path is not a top-level argument but lives inside the wire
|
|
95
|
+
* payload — `hashline` section headers, `apply_patch` envelope markers.
|
|
96
|
+
* Returns `undefined` (or an empty list) when no paths are recoverable.
|
|
97
|
+
*/
|
|
98
|
+
matcherPaths(args: Args): readonly string[] | undefined;
|
|
99
|
+
/**
|
|
100
|
+
* Per-file projection of the (potentially partial) args: one entry per
|
|
101
|
+
* touched file pairing the path with the digest of only the lines added to
|
|
102
|
+
* that file. Multi-file payloads (multi-section hashline / multi-hunk
|
|
103
|
+
* apply_patch) MUST split here so callers can evaluate each file under its
|
|
104
|
+
* own path scope instead of leaking added lines from one file into the
|
|
105
|
+
* other's match context. Same-path sections / hunks are merged into one
|
|
106
|
+
* entry. Returns `undefined` (or empty) when no per-file split is
|
|
107
|
+
* recoverable yet — the caller falls back to {@link matcherDigest} +
|
|
108
|
+
* {@link matcherPaths}.
|
|
109
|
+
*/
|
|
110
|
+
matcherEntries(args: Args): readonly EditMatcherEntry[] | undefined;
|
|
80
111
|
}
|
|
81
112
|
|
|
82
113
|
// -----------------------------------------------------------------------------
|
|
@@ -191,6 +222,103 @@ function extractAddedLines(text: string, fallbackToWhole: boolean): string {
|
|
|
191
222
|
return added;
|
|
192
223
|
}
|
|
193
224
|
|
|
225
|
+
/**
|
|
226
|
+
* Extract hashline `[path#TAG]` (and untagged `[path]`) section-header paths
|
|
227
|
+
* from a (possibly partial) hashline buffer. Tolerant of streaming chunks
|
|
228
|
+
* where `Patch.parse` would still throw on the trailing op — only fully
|
|
229
|
+
* closed header lines are recognised.
|
|
230
|
+
*/
|
|
231
|
+
function extractHashlineHeaderPaths(input: string): string[] {
|
|
232
|
+
const paths: string[] = [];
|
|
233
|
+
const re = /^\s*\[([^\]\r\n]+?)(?:#[0-9a-fA-F]{4})?\]\s*$/gm;
|
|
234
|
+
for (const match of input.matchAll(re)) {
|
|
235
|
+
const candidate = stripApplyPatchPathNoise(match[1]).trim();
|
|
236
|
+
if (candidate.length > 0) paths.push(candidate);
|
|
237
|
+
}
|
|
238
|
+
return paths;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/**
|
|
242
|
+
* Strip the `*** Add/Update/Delete File:` / `*** Move to:` noise that the
|
|
243
|
+
* model sometimes pastes into a hashline header (the hashline tokenizer does
|
|
244
|
+
* the same in its recovery path).
|
|
245
|
+
*/
|
|
246
|
+
function stripApplyPatchPathNoise(value: string): string {
|
|
247
|
+
return value
|
|
248
|
+
.replace(/^\s*\*{3}\s*(?:Add|Update|Delete)\s+File\s*:\s*/i, "")
|
|
249
|
+
.replace(/^\s*\*{3}\s*Move\s+to\s*:\s*/i, "");
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
/** Extract `*** Add/Update/Delete File:` paths from a (possibly partial) apply_patch envelope. */
|
|
253
|
+
function extractApplyPatchEnvelopePaths(input: string): string[] {
|
|
254
|
+
const paths: string[] = [];
|
|
255
|
+
const re = /^\s*\*{3}\s+(?:Add|Update|Delete)\s+File\s*:\s*(\S.*?)\s*$/gm;
|
|
256
|
+
for (const match of input.matchAll(re)) {
|
|
257
|
+
const candidate = match[1].trim();
|
|
258
|
+
if (candidate.length > 0) paths.push(candidate);
|
|
259
|
+
}
|
|
260
|
+
return paths;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
/**
|
|
264
|
+
* Split a (possibly partial) hashline buffer into one matcher entry per
|
|
265
|
+
* touched file: pair the section header path with the added lines from that
|
|
266
|
+
* section's body, merging sections that target the same file into one entry.
|
|
267
|
+
* Header-line regex (not `Patch.parse`) so a mid-typed trailing op still
|
|
268
|
+
* yields entries for completed sections.
|
|
269
|
+
*/
|
|
270
|
+
function splitHashlinePerFile(input: string): EditMatcherEntry[] {
|
|
271
|
+
const headerRe = /^\s*\[([^\]\r\n]+?)(?:#[0-9a-fA-F]{4})?\]\s*$/gm;
|
|
272
|
+
const sections: { path: string; headerStart: number; bodyStart: number }[] = [];
|
|
273
|
+
let match: RegExpExecArray | null = headerRe.exec(input);
|
|
274
|
+
while (match !== null) {
|
|
275
|
+
const candidate = stripApplyPatchPathNoise(match[1]).trim();
|
|
276
|
+
if (candidate.length > 0) {
|
|
277
|
+
sections.push({ path: candidate, headerStart: match.index, bodyStart: headerRe.lastIndex });
|
|
278
|
+
}
|
|
279
|
+
match = headerRe.exec(input);
|
|
280
|
+
}
|
|
281
|
+
if (sections.length === 0) return [];
|
|
282
|
+
|
|
283
|
+
const byPath = new Map<string, string>();
|
|
284
|
+
for (let i = 0; i < sections.length; i++) {
|
|
285
|
+
const { path: sectionPath, bodyStart } = sections[i];
|
|
286
|
+
const bodyEnd = i + 1 < sections.length ? sections[i + 1].headerStart : input.length;
|
|
287
|
+
const added = extractAddedLines(input.slice(bodyStart, bodyEnd), false);
|
|
288
|
+
if (added.length === 0) continue;
|
|
289
|
+
const existing = byPath.get(sectionPath);
|
|
290
|
+
byPath.set(sectionPath, existing === undefined ? added : `${existing}\n${added}`);
|
|
291
|
+
}
|
|
292
|
+
return Array.from(byPath, ([path, digest]) => ({ path, digest }));
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
/**
|
|
296
|
+
* Split a (possibly partial) apply_patch envelope into one matcher entry per
|
|
297
|
+
* touched file. Same-path hunks are merged into one entry. Falls back to the
|
|
298
|
+
* streaming-tolerant parser when the envelope hasn't reached `*** End Patch`.
|
|
299
|
+
*/
|
|
300
|
+
function splitApplyPatchPerFile(input: string): EditMatcherEntry[] {
|
|
301
|
+
let entries: ApplyPatchEntry[];
|
|
302
|
+
try {
|
|
303
|
+
entries = expandApplyPatchToEntries({ input });
|
|
304
|
+
} catch {
|
|
305
|
+
try {
|
|
306
|
+
entries = expandApplyPatchToPreviewEntries({ input });
|
|
307
|
+
} catch {
|
|
308
|
+
return [];
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
const byPath = new Map<string, string>();
|
|
312
|
+
for (const entry of entries) {
|
|
313
|
+
if (typeof entry.diff !== "string") continue;
|
|
314
|
+
const added = extractAddedLines(entry.diff, false);
|
|
315
|
+
if (added.length === 0) continue;
|
|
316
|
+
const existing = byPath.get(entry.path);
|
|
317
|
+
byPath.set(entry.path, existing === undefined ? added : `${existing}\n${added}`);
|
|
318
|
+
}
|
|
319
|
+
return Array.from(byPath, ([path, digest]) => ({ path, digest }));
|
|
320
|
+
}
|
|
321
|
+
|
|
194
322
|
// -----------------------------------------------------------------------------
|
|
195
323
|
// Strategies
|
|
196
324
|
// -----------------------------------------------------------------------------
|
|
@@ -236,6 +364,15 @@ const replaceStrategy: EditStreamingStrategy<ReplaceArgs> = {
|
|
|
236
364
|
}
|
|
237
365
|
return digest;
|
|
238
366
|
},
|
|
367
|
+
matcherPaths(args) {
|
|
368
|
+
return typeof args?.path === "string" && args.path.length > 0 ? [args.path] : undefined;
|
|
369
|
+
},
|
|
370
|
+
matcherEntries(args) {
|
|
371
|
+
const path = args?.path;
|
|
372
|
+
if (typeof path !== "string" || path.length === 0) return undefined;
|
|
373
|
+
const digest = replaceStrategy.matcherDigest(args);
|
|
374
|
+
return digest === undefined ? undefined : [{ path, digest }];
|
|
375
|
+
},
|
|
239
376
|
};
|
|
240
377
|
|
|
241
378
|
interface PatchArgs {
|
|
@@ -278,6 +415,15 @@ const patchStrategy: EditStreamingStrategy<PatchArgs> = {
|
|
|
278
415
|
}
|
|
279
416
|
return digest;
|
|
280
417
|
},
|
|
418
|
+
matcherPaths(args) {
|
|
419
|
+
return typeof args?.path === "string" && args.path.length > 0 ? [args.path] : undefined;
|
|
420
|
+
},
|
|
421
|
+
matcherEntries(args) {
|
|
422
|
+
const path = args?.path;
|
|
423
|
+
if (typeof path !== "string" || path.length === 0) return undefined;
|
|
424
|
+
const digest = patchStrategy.matcherDigest(args);
|
|
425
|
+
return digest === undefined ? undefined : [{ path, digest }];
|
|
426
|
+
},
|
|
281
427
|
};
|
|
282
428
|
|
|
283
429
|
interface HashlineArgs {
|
|
@@ -437,6 +583,18 @@ const hashlineStrategy: EditStreamingStrategy<HashlineArgs> = {
|
|
|
437
583
|
// Body rows are `+TEXT`; headers and op lines are grammar, never content.
|
|
438
584
|
return extractAddedLines(input, false);
|
|
439
585
|
},
|
|
586
|
+
matcherPaths(args) {
|
|
587
|
+
const input = args?.input;
|
|
588
|
+
if (typeof input !== "string" || input.length === 0) return undefined;
|
|
589
|
+
const paths = extractHashlineHeaderPaths(input);
|
|
590
|
+
return paths.length > 0 ? paths : undefined;
|
|
591
|
+
},
|
|
592
|
+
matcherEntries(args) {
|
|
593
|
+
const input = args?.input;
|
|
594
|
+
if (typeof input !== "string" || input.length === 0) return undefined;
|
|
595
|
+
const entries = splitHashlinePerFile(input);
|
|
596
|
+
return entries.length > 0 ? entries : undefined;
|
|
597
|
+
},
|
|
440
598
|
};
|
|
441
599
|
|
|
442
600
|
interface ApplyPatchArgs {
|
|
@@ -495,6 +653,18 @@ const applyPatchStrategy: EditStreamingStrategy<ApplyPatchArgs> = {
|
|
|
495
653
|
// Envelope markers and `@@` hunk headers are grammar, never content.
|
|
496
654
|
return extractAddedLines(input, false);
|
|
497
655
|
},
|
|
656
|
+
matcherPaths(args) {
|
|
657
|
+
const input = args?.input;
|
|
658
|
+
if (typeof input !== "string" || input.length === 0) return undefined;
|
|
659
|
+
const paths = extractApplyPatchEnvelopePaths(input);
|
|
660
|
+
return paths.length > 0 ? paths : undefined;
|
|
661
|
+
},
|
|
662
|
+
matcherEntries(args) {
|
|
663
|
+
const input = args?.input;
|
|
664
|
+
if (typeof input !== "string" || input.length === 0) return undefined;
|
|
665
|
+
const entries = splitApplyPatchPerFile(input);
|
|
666
|
+
return entries.length > 0 ? entries : undefined;
|
|
667
|
+
},
|
|
498
668
|
};
|
|
499
669
|
export const EDIT_MODE_STRATEGIES: Record<EditMode, EditStreamingStrategy<unknown>> = {
|
|
500
670
|
replace: replaceStrategy as EditStreamingStrategy<unknown>,
|