@oh-my-pi/pi-ai 18.4.0 → 18.4.2
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 +26 -0
- package/README.md +12 -0
- package/dist/types/auth/oauth.d.ts +2 -2
- package/dist/types/auth/refresh.d.ts +3 -3
- package/dist/types/auth/store.d.ts +4 -2
- package/dist/types/auth/types.d.ts +20 -3
- package/dist/types/auth/usage-cache.d.ts +9 -2
- package/dist/types/auth/usage.d.ts +6 -5
- package/dist/types/auth-broker/client.d.ts +2 -2
- package/dist/types/auth-broker/remote-store.d.ts +2 -2
- package/dist/types/error/flags.d.ts +9 -2
- package/dist/types/error/rate-limit.d.ts +3 -2
- package/dist/types/providers/cursor/exec-modern.d.ts +1 -1
- package/dist/types/providers/cursor-pi-args.d.ts +13 -18
- package/dist/types/stream.d.ts +7 -0
- package/dist/types/usage/cursor.d.ts +3 -1
- package/dist/types/usage/zai.d.ts +2 -0
- package/dist/types/usage.d.ts +6 -0
- package/package.json +6 -6
- package/src/auth/cascade.ts +1 -0
- package/src/auth/oauth.ts +4 -3
- package/src/auth/refresh.ts +105 -16
- package/src/auth/resets.ts +6 -2
- package/src/auth/select.ts +11 -1
- package/src/auth/store.ts +4 -0
- package/src/auth/types.ts +22 -2
- package/src/auth/usage-cache.ts +12 -8
- package/src/auth/usage.ts +45 -19
- package/src/auth-broker/client.ts +8 -3
- package/src/auth-broker/remote-store.ts +4 -2
- package/src/auth-broker/server.ts +7 -1
- package/src/auth-gateway/dispatch.ts +1 -0
- package/src/auth-retry.ts +5 -1
- package/src/error/flags.ts +10 -3
- package/src/error/rate-limit.ts +23 -3
- package/src/providers/anthropic.ts +62 -53
- package/src/providers/cowork-fetch.ts +45 -3
- package/src/providers/cursor/exec-modern.ts +1 -1
- package/src/providers/cursor-pi-args.ts +13 -18
- package/src/providers/cursor.ts +179 -57
- package/src/providers/devin.ts +43 -13
- package/src/providers/openai-completions.ts +122 -30
- package/src/stream.ts +77 -16
- package/src/usage/claude.ts +5 -0
- package/src/usage/cursor.ts +43 -1
- package/src/usage/devin.ts +8 -7
- package/src/usage/registry.ts +2 -1
- package/src/usage/zai.ts +20 -0
- package/src/usage.ts +6 -0
package/src/error/rate-limit.ts
CHANGED
|
@@ -110,6 +110,17 @@ export function isDashScopeTokenLimitText(errorMessage: string): boolean {
|
|
|
110
110
|
);
|
|
111
111
|
}
|
|
112
112
|
|
|
113
|
+
// Rolling per-minute token/request throttles (TPM/RPM). Providers report these
|
|
114
|
+
// with quota wording — "tpm exhausted (type=quota_exceeded_error)",
|
|
115
|
+
// "inference exceeds tpm/rpm limit", "RateLimitExceeded.EndpointTPMExceeded" —
|
|
116
|
+
// but the window self-heals within the minute, so they belong in the transient
|
|
117
|
+
// backoff lane, not the 30-minute credential-blocking quota lane (#13253).
|
|
118
|
+
// Deliberately subordinate to the account-scoped arms of
|
|
119
|
+
// {@link parseRateLimitReason}: a message that also carries a plan/spend/
|
|
120
|
+
// account-quota signal classifies there first and keeps its quota verdict.
|
|
121
|
+
const TPM_RPM_THROTTLE_PATTERN =
|
|
122
|
+
/\b(?:tpm|rpm)\b[^\n]{0,40}\b(?:exhaust\w*|exceed\w*|limit\w*|throttl\w*|reach\w*)\b|\b(?:exhaust\w*|exceed\w*|limit\w*|throttl\w*|reach\w*)\b[^\n]{0,40}\b(?:tpm|rpm)\b|\bRateLimitExceeded\.(?:Endpoint)?(?:TPM|RPM)\w*/i;
|
|
123
|
+
|
|
113
124
|
const GOOGLE_RPC_ERROR_INFO_TYPE = "type.googleapis.com/google.rpc.ErrorInfo";
|
|
114
125
|
const ANTIGRAVITY_MODEL_QUOTA_PATTERN = /\bexhausted your capacity on this model\b/i;
|
|
115
126
|
const LONG_RATE_LIMIT_DELAY_MS = 5 * 60 * 1000;
|
|
@@ -180,8 +191,9 @@ function isQuotaExhaustedReason(reason: RateLimitReason): boolean {
|
|
|
180
191
|
* Classify a rate-limit error message into a reason category.
|
|
181
192
|
* Priority order: explicit details in a resource-exhausted error > QUOTA
|
|
182
193
|
* (Antigravity "quota will reset") > CN quota > DASHSCOPE_TOKEN_LIMIT (TPM/TPS
|
|
183
|
-
* throttle) > CONCURRENT_LIMIT > MODEL_CAPACITY > QUOTA (account) > RATE_LIMIT
|
|
184
|
-
* QUOTA (generic) > SERVER_ERROR > bare
|
|
194
|
+
* throttle) > CONCURRENT_LIMIT > MODEL_CAPACITY > QUOTA (account) > RATE_LIMIT
|
|
195
|
+
* (including TPM/RPM rolling windows) > QUOTA (generic) > SERVER_ERROR > bare
|
|
196
|
+
* resource-exhausted > UNKNOWN.
|
|
185
197
|
*
|
|
186
198
|
* Bare "resource exhausted" / "resource_exhausted" maps to MODEL_CAPACITY (transient, short wait).
|
|
187
199
|
* Explicit details such as "quota exceeded" retain their normal classification.
|
|
@@ -252,7 +264,8 @@ export function parseRateLimitReason(errorMessage: string): RateLimitReason {
|
|
|
252
264
|
lower.includes("per minute") ||
|
|
253
265
|
lower.includes("rate limit") ||
|
|
254
266
|
lower.includes("too many requests") ||
|
|
255
|
-
lower.includes("presque")
|
|
267
|
+
lower.includes("presque") ||
|
|
268
|
+
TPM_RPM_THROTTLE_PATTERN.test(errorMessage)
|
|
256
269
|
) {
|
|
257
270
|
return "RATE_LIMIT_EXCEEDED";
|
|
258
271
|
}
|
|
@@ -411,6 +424,13 @@ export function matchesUsageLimitText(errorMessage: string): boolean {
|
|
|
411
424
|
const structuredReason = parseGoogleRpcRateLimitReason(errorMessage);
|
|
412
425
|
if (structuredReason !== undefined) return isQuotaExhaustedReason(structuredReason);
|
|
413
426
|
if (isDashScopeTokenLimitText(errorMessage)) return false;
|
|
427
|
+
// Rolling TPM/RPM windows self-heal, so they never rotate a credential. The
|
|
428
|
+
// reason re-check is the precedence guard: an account-scoped cap that merely
|
|
429
|
+
// quotes a TPM number resolves to QUOTA_EXHAUSTED earlier in that ladder and
|
|
430
|
+
// keeps its usage-limit verdict.
|
|
431
|
+
if (TPM_RPM_THROTTLE_PATTERN.test(errorMessage) && parseRateLimitReason(errorMessage) === "RATE_LIMIT_EXCEEDED") {
|
|
432
|
+
return false;
|
|
433
|
+
}
|
|
414
434
|
return (
|
|
415
435
|
USAGE_LIMIT_PATTERN.test(errorMessage) ||
|
|
416
436
|
ANTHROPIC_CREDITS_REQUIRED_PATTERN.test(errorMessage) ||
|
|
@@ -15,6 +15,7 @@ import {
|
|
|
15
15
|
parseStreamingJsonThrottled,
|
|
16
16
|
readSseEvents,
|
|
17
17
|
} from "@oh-my-pi/pi-utils";
|
|
18
|
+
import { NO_AUTH_SENTINEL } from "../auth-retry";
|
|
18
19
|
import { renderDemotedThinking } from "../dialect/demotion";
|
|
19
20
|
import * as AIError from "../error";
|
|
20
21
|
import { getEnvApiKey, OUTPUT_FALLBACK_BUFFER } from "../stream";
|
|
@@ -404,10 +405,17 @@ export function buildAnthropicHeaders(options: AnthropicHeaderOptions): Record<s
|
|
|
404
405
|
};
|
|
405
406
|
return allowAnthropicHeaderOverrides ? mergeHeaders(headers, anthropicHeaderOverrides) : headers;
|
|
406
407
|
} else if (!isOfficialAnthropicApiUrl(options.baseUrl)) {
|
|
408
|
+
// A keyless provider (`auth: none`) resolves to the `N/A` sentinel
|
|
409
|
+
// rather than a real key; custom endpoints that authenticate via their
|
|
410
|
+
// own headers may reject a bogus bearer, so send no Authorization —
|
|
411
|
+
// same sentinel guard as the openai transports. A caller-supplied
|
|
412
|
+
// Authorization in `model.headers` still wins.
|
|
413
|
+
const bearer =
|
|
414
|
+
incomingAuthorization ?? (options.apiKey !== NO_AUTH_SENTINEL ? `Bearer ${options.apiKey}` : undefined);
|
|
407
415
|
return {
|
|
408
416
|
...modelHeaders,
|
|
409
417
|
Accept: acceptHeader,
|
|
410
|
-
Authorization:
|
|
418
|
+
...(bearer ? { Authorization: bearer } : {}),
|
|
411
419
|
...sharedHeaders,
|
|
412
420
|
...(incomingUserAgent ? { "User-Agent": incomingUserAgent } : {}),
|
|
413
421
|
...(betaHeader ? { "anthropic-beta": betaHeader } : {}),
|
|
@@ -3723,8 +3731,13 @@ export function buildAnthropicClientOptions(args: AnthropicClientOptionsArgs): A
|
|
|
3723
3731
|
// the proxy to deal with two competing credentials when the user explicitly
|
|
3724
3732
|
// asked for one.
|
|
3725
3733
|
const authorizationHeader = getHeaderCaseInsensitive(defaultHeaders, "Authorization");
|
|
3734
|
+
// A keyless provider resolves to the `N/A` sentinel, for which no
|
|
3735
|
+
// Authorization was built above; the client would otherwise inject a
|
|
3736
|
+
// bogus `X-Api-Key: N/A` of its own.
|
|
3726
3737
|
const shouldSuppressClientApiKey =
|
|
3727
|
-
!oauthToken &&
|
|
3738
|
+
!oauthToken &&
|
|
3739
|
+
!model.compat.officialEndpoint &&
|
|
3740
|
+
(typeof authorizationHeader === "string" || apiKey === NO_AUTH_SENTINEL);
|
|
3728
3741
|
|
|
3729
3742
|
return {
|
|
3730
3743
|
isOAuthToken: oauthToken,
|
|
@@ -4004,34 +4017,33 @@ function applyPromptCaching(params: MessageCreateParamsStreaming, cacheControl?:
|
|
|
4004
4017
|
}
|
|
4005
4018
|
|
|
4006
4019
|
/**
|
|
4007
|
-
*
|
|
4008
|
-
*
|
|
4009
|
-
*
|
|
4010
|
-
*
|
|
4011
|
-
*
|
|
4012
|
-
*
|
|
4013
|
-
*
|
|
4020
|
+
* System-prompt segments whose bytes differ between sessions or turns of the
|
|
4021
|
+
* same agent: per-turn memory recall (`<memories>`) and the coding agent's
|
|
4022
|
+
* working-directory context (`<project-context>`: context files with their
|
|
4023
|
+
* paths, workspace tree, workspace roots, session append text; advisors use
|
|
4024
|
+
* the same tag for their context-file block). The coding agent renders them
|
|
4025
|
+
* as their own `systemPrompt` array elements after the large static prompt.
|
|
4026
|
+
* The system cache breakpoint anchors on the block right before the first
|
|
4027
|
+
* such segment, so the static head is shared byte-for-byte across sessions in
|
|
4028
|
+
* different directories (e.g. one git worktree per task) and a recall refresh
|
|
4029
|
+
* re-bills only the suffix and the message tail.
|
|
4014
4030
|
*
|
|
4015
|
-
*
|
|
4016
|
-
*
|
|
4017
|
-
*
|
|
4018
|
-
*
|
|
4019
|
-
*
|
|
4031
|
+
* Everything from the first volatile segment on sits after the head
|
|
4032
|
+
* breakpoint, including stable blocks appended behind it (per-spawn subagent
|
|
4033
|
+
* role text, `before_agent_start` extension policy): prefix caching is
|
|
4034
|
+
* positional, so bytes after a changing segment can never extend the cached
|
|
4035
|
+
* head anyway; the rolling message breakpoints still cover them.
|
|
4020
4036
|
*
|
|
4021
|
-
* Detection is by our own markup, not model identity:
|
|
4022
|
-
*
|
|
4023
|
-
* (e.g. quoted in conversation) are unaffected — only a leading tag counts.
|
|
4037
|
+
* Detection is by our own markup, not model identity: only a leading tag
|
|
4038
|
+
* counts, so stable segments quoting these tags elsewhere are unaffected.
|
|
4024
4039
|
*/
|
|
4025
|
-
const VOLATILE_SYSTEM_SEGMENT_MARKERS = ["<memories>"];
|
|
4040
|
+
const VOLATILE_SYSTEM_SEGMENT_MARKERS = ["<memories>", "<project-context>"];
|
|
4026
4041
|
|
|
4027
|
-
function
|
|
4028
|
-
|
|
4029
|
-
|
|
4030
|
-
|
|
4031
|
-
|
|
4032
|
-
start--;
|
|
4033
|
-
}
|
|
4034
|
-
return start;
|
|
4042
|
+
function volatileSystemSuffixStart(systemBlocks: readonly AnthropicSystemBlock[]): number {
|
|
4043
|
+
const start = systemBlocks.findIndex(block =>
|
|
4044
|
+
VOLATILE_SYSTEM_SEGMENT_MARKERS.some(marker => block.text.startsWith(marker)),
|
|
4045
|
+
);
|
|
4046
|
+
return start === -1 ? systemBlocks.length : start;
|
|
4035
4047
|
}
|
|
4036
4048
|
|
|
4037
4049
|
/**
|
|
@@ -4045,10 +4057,10 @@ function stableSystemSuffixStart(systemBlocks: readonly AnthropicSystemBlock[]):
|
|
|
4045
4057
|
* (Claude Code, Pi) use. Without it, the general API-key path anchors only the
|
|
4046
4058
|
* moving message tail, so tail churn re-writes the whole head uncached.
|
|
4047
4059
|
*
|
|
4048
|
-
* Volatile
|
|
4049
|
-
* recall refresh re-bills only the suffix
|
|
4050
|
-
* the whole head. When every system block is volatile
|
|
4051
|
-
* boundary and the breakpoint stays on the array tail
|
|
4060
|
+
* Volatile segments (memory recall, working-directory context) sit after the
|
|
4061
|
+
* breakpoint, so a recall refresh or a different cwd re-bills only the suffix
|
|
4062
|
+
* and the tail instead of the whole head. When every system block is volatile
|
|
4063
|
+
* there is no stable boundary and the breakpoint stays on the array tail.
|
|
4052
4064
|
*
|
|
4053
4065
|
* Anthropic allows at most 4 cache breakpoints per request. At most one is
|
|
4054
4066
|
* spent on tools and one on system here, leaving the remaining budget for
|
|
@@ -4058,9 +4070,12 @@ function stableSystemSuffixStart(systemBlocks: readonly AnthropicSystemBlock[]):
|
|
|
4058
4070
|
* sit first in wire order and survive message rewrites, and sibling subagents of
|
|
4059
4071
|
* the same definition share this prefix byte for byte.
|
|
4060
4072
|
*
|
|
4061
|
-
*
|
|
4062
|
-
* buildAnthropicSystemBlocks
|
|
4063
|
-
*
|
|
4073
|
+
* The OAuth Claude Code path pre-decorates its identity system block in
|
|
4074
|
+
* buildAnthropicSystemBlocks. That breakpoint moves to the anchor block instead
|
|
4075
|
+
* of a second one being added: the identity block is a prefix of the anchored
|
|
4076
|
+
* head, so the move keeps the budget at last tool + last stable system block +
|
|
4077
|
+
* 2 message breakpoints while the agent's static system prompt, not just the
|
|
4078
|
+
* identity line, becomes a cached prefix of its own.
|
|
4064
4079
|
*
|
|
4065
4080
|
* Runs on the fresh system blocks and wire tools built for this request, after
|
|
4066
4081
|
* the declared tool list was derived from the transcript's request controls.
|
|
@@ -4084,26 +4099,20 @@ function applyHeadCaching(
|
|
|
4084
4099
|
}
|
|
4085
4100
|
|
|
4086
4101
|
if (systemBlocks && systemBlocks.length > 0) {
|
|
4087
|
-
// Anchor on the last
|
|
4088
|
-
// re-bills only the suffix,
|
|
4089
|
-
//
|
|
4090
|
-
//
|
|
4091
|
-
//
|
|
4092
|
-
//
|
|
4093
|
-
//
|
|
4094
|
-
//
|
|
4095
|
-
|
|
4096
|
-
|
|
4097
|
-
const
|
|
4098
|
-
if (
|
|
4099
|
-
|
|
4100
|
-
|
|
4101
|
-
if (lastBlock) lastBlock.cache_control = cloneAnthropicCacheControl(cacheControl);
|
|
4102
|
-
}
|
|
4103
|
-
} else {
|
|
4104
|
-
const anchorIndex = suffixStart === 0 ? systemBlocks.length - 1 : suffixStart - 1;
|
|
4105
|
-
const anchor = systemBlocks[anchorIndex];
|
|
4106
|
-
if (anchor && anchor.cache_control == null) anchor.cache_control = cloneAnthropicCacheControl(cacheControl);
|
|
4102
|
+
// Anchor on the last block before the volatile suffix so a recall
|
|
4103
|
+
// refresh or a different working directory re-bills only the suffix,
|
|
4104
|
+
// not the whole head. An earlier system breakpoint (the OAuth identity
|
|
4105
|
+
// block) moves to the anchor rather than staying as a second one: a
|
|
4106
|
+
// breakpoint left on the identity block caches only tools + identity,
|
|
4107
|
+
// and keeping both would take a rolling message breakpoint from
|
|
4108
|
+
// `applyPromptCaching` (4 minus head breakpoints). All-volatile falls
|
|
4109
|
+
// back to tail anchoring.
|
|
4110
|
+
const suffixStart = volatileSystemSuffixStart(systemBlocks);
|
|
4111
|
+
const anchorIndex = suffixStart === 0 ? systemBlocks.length - 1 : suffixStart - 1;
|
|
4112
|
+
const anchor = systemBlocks[anchorIndex];
|
|
4113
|
+
if (anchor && anchor.cache_control == null) {
|
|
4114
|
+
for (const block of systemBlocks) delete block.cache_control;
|
|
4115
|
+
anchor.cache_control = cloneAnthropicCacheControl(cacheControl);
|
|
4107
4116
|
}
|
|
4108
4117
|
}
|
|
4109
4118
|
}
|
|
@@ -125,11 +125,53 @@ function decodedResponseStream(message: IncomingMessage): stream.Readable {
|
|
|
125
125
|
return stream.pipeline(message, decoder, () => {});
|
|
126
126
|
}
|
|
127
127
|
|
|
128
|
-
|
|
128
|
+
/**
|
|
129
|
+
* Bun's `node:http` shim reports a response body cut off mid-stream (peer reset,
|
|
130
|
+
* dead connection) as a bare `Error("aborted")` with code `ECONNRESET` — wording
|
|
131
|
+
* indistinguishable from a cancellation, so retry classification treated every
|
|
132
|
+
* mid-stream drop on this transport as terminal. Native `fetch` reports the same
|
|
133
|
+
* failure as "The socket connection was closed unexpectedly", a recognized
|
|
134
|
+
* transient. Re-raise with that wording, keeping the original as `cause`; a
|
|
135
|
+
* caller abort keeps its own error.
|
|
136
|
+
*/
|
|
137
|
+
function withFetchParityErrors(
|
|
138
|
+
body: ReadableStream<Uint8Array>,
|
|
139
|
+
signal: AbortSignal | undefined,
|
|
140
|
+
): ReadableStream<Uint8Array> {
|
|
141
|
+
const reader = body.getReader();
|
|
142
|
+
return new ReadableStream<Uint8Array>({
|
|
143
|
+
async pull(controller) {
|
|
144
|
+
try {
|
|
145
|
+
const chunk = await reader.read();
|
|
146
|
+
if (chunk.done) controller.close();
|
|
147
|
+
else controller.enqueue(chunk.value);
|
|
148
|
+
} catch (error) {
|
|
149
|
+
const prematureClose =
|
|
150
|
+
!signal?.aborted &&
|
|
151
|
+
error instanceof Error &&
|
|
152
|
+
error.message === "aborted" &&
|
|
153
|
+
(error as NodeJS.ErrnoException).code === "ECONNRESET";
|
|
154
|
+
controller.error(
|
|
155
|
+
prematureClose
|
|
156
|
+
? Object.assign(
|
|
157
|
+
new Error("The socket connection was closed unexpectedly before the response completed", {
|
|
158
|
+
cause: error,
|
|
159
|
+
}),
|
|
160
|
+
{ code: "ECONNRESET" },
|
|
161
|
+
)
|
|
162
|
+
: error,
|
|
163
|
+
);
|
|
164
|
+
}
|
|
165
|
+
},
|
|
166
|
+
cancel: reason => reader.cancel(reason),
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function createResponse(message: IncomingMessage, method: string, signal: AbortSignal | undefined): Response {
|
|
129
171
|
const status = message.statusCode;
|
|
130
172
|
if (status === undefined) throw new Error("Cowork transport received a response without an HTTP status.");
|
|
131
173
|
const hasBody = method !== "HEAD" && status !== 204 && status !== 304;
|
|
132
|
-
const body = hasBody ? stream.Readable.toWeb(decodedResponseStream(message)) : null;
|
|
174
|
+
const body = hasBody ? withFetchParityErrors(stream.Readable.toWeb(decodedResponseStream(message)), signal) : null;
|
|
133
175
|
return new Response(body, {
|
|
134
176
|
status,
|
|
135
177
|
statusText: message.statusMessage,
|
|
@@ -190,7 +232,7 @@ async function sendCoworkRequest(
|
|
|
190
232
|
});
|
|
191
233
|
}
|
|
192
234
|
try {
|
|
193
|
-
result.resolve(createResponse(message, method));
|
|
235
|
+
result.resolve(createResponse(message, method, signal));
|
|
194
236
|
} catch (error) {
|
|
195
237
|
message.destroy();
|
|
196
238
|
release();
|
|
@@ -25,18 +25,14 @@ import * as path from "node:path";
|
|
|
25
25
|
* A `pi_read` range composed onto the path as `read`'s inline `:raw:N+K`
|
|
26
26
|
* selector.
|
|
27
27
|
*
|
|
28
|
-
* `read` exposes no range kwargs
|
|
29
|
-
*
|
|
30
|
-
*
|
|
31
|
-
*
|
|
32
|
-
*
|
|
33
|
-
*
|
|
34
|
-
*
|
|
35
|
-
*
|
|
36
|
-
* wrong for a caller that asked for exactly `limit` lines from `offset`. The
|
|
37
|
-
* wire result is an opaque `output` string, so the hashline and line-number
|
|
38
|
-
* gutter that `raw` also drops carry nothing the frame's contract needs.
|
|
39
|
-
* A range-free read keeps the ordinary form — whole-file reads want them.
|
|
28
|
+
* `read` exposes no range kwargs; `offset` and `limit` are composed onto the
|
|
29
|
+
* path. A negative offset needs the source line count and is resolved by the
|
|
30
|
+
* coding-agent bridge before calling this helper. `limit: 0` has no selector
|
|
31
|
+
* representation and returns `null`.
|
|
32
|
+
*
|
|
33
|
+
* Range selectors are raw because plain ranges add context lines. Cursor
|
|
34
|
+
* numbers the returned text itself, so it cannot use read's hashline gutter.
|
|
35
|
+
* Use [`cursorExecReadPath`] for a range-free read, which also needs `:raw`.
|
|
40
36
|
*/
|
|
41
37
|
export function piReadPath(readPath: string, offset?: number, limit?: number): string | null {
|
|
42
38
|
if (limit !== undefined && Math.floor(limit) <= 0) return null;
|
|
@@ -100,14 +96,13 @@ export function cursorRawReadPath(readPath: string): string {
|
|
|
100
96
|
}
|
|
101
97
|
|
|
102
98
|
/**
|
|
103
|
-
*
|
|
99
|
+
* Raw selector for a Cursor exec read, including edit-owned materialization.
|
|
104
100
|
*
|
|
105
|
-
*
|
|
106
|
-
*
|
|
107
|
-
*
|
|
108
|
-
* second `:raw` onto the already-composed selector.
|
|
101
|
+
* Compose a requested window before forcing `:raw` on whole-file reads. The
|
|
102
|
+
* caller drops `offset`/`limit` after composing so the handler cannot append
|
|
103
|
+
* another selector.
|
|
109
104
|
*/
|
|
110
|
-
export function
|
|
105
|
+
export function cursorExecReadPath(readPath: string, offset?: number, limit?: number): string | null {
|
|
111
106
|
const ranged = piReadPath(readPath, offset, limit);
|
|
112
107
|
if (ranged === null) return null;
|
|
113
108
|
return cursorRawReadPath(ranged);
|