deepcodex 0.1.0
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/.codex-plugin/plugin.json +24 -0
- package/LICENSE +21 -0
- package/README.md +178 -0
- package/bin/opencodex.js +49 -0
- package/config/desktop.json +9 -0
- package/config/pilot.json +16 -0
- package/config/worker.json +78 -0
- package/node_modules/smol-toml/LICENSE +24 -0
- package/node_modules/smol-toml/README.md +418 -0
- package/node_modules/smol-toml/dist/date.d.ts +41 -0
- package/node_modules/smol-toml/dist/date.js +127 -0
- package/node_modules/smol-toml/dist/error.d.ts +38 -0
- package/node_modules/smol-toml/dist/error.js +63 -0
- package/node_modules/smol-toml/dist/extract.js +69 -0
- package/node_modules/smol-toml/dist/index.cjs +734 -0
- package/node_modules/smol-toml/dist/index.d.ts +43 -0
- package/node_modules/smol-toml/dist/index.js +33 -0
- package/node_modules/smol-toml/dist/parse.d.ts +36 -0
- package/node_modules/smol-toml/dist/parse.js +149 -0
- package/node_modules/smol-toml/dist/primitive.js +238 -0
- package/node_modules/smol-toml/dist/stringify.d.ts +31 -0
- package/node_modules/smol-toml/dist/stringify.js +181 -0
- package/node_modules/smol-toml/dist/struct.js +179 -0
- package/node_modules/smol-toml/dist/util.d.ts +38 -0
- package/node_modules/smol-toml/dist/util.js +89 -0
- package/node_modules/smol-toml/package.json +68 -0
- package/package.json +47 -0
- package/prompts/worker.md +20 -0
- package/scripts/credentials.js +102 -0
- package/scripts/desktop.js +199 -0
- package/scripts/pilot-router.js +241 -0
- package/scripts/pilot.js +242 -0
- package/scripts/toml.js +6 -0
- package/scripts/worker.js +637 -0
- package/skills/delegate-flash/SKILL.md +63 -0
- package/vendor/codex-router/LICENSE +21 -0
- package/vendor/codex-router/deepseek-responses.js +55 -0
- package/vendor/codex-router/json-number-rewrite.js +58 -0
- package/vendor/codex-router/namespace-relay.js +4294 -0
- package/vendor/codex-router/sse-prefix.js +115 -0
- package/vendor/codex-router/subagent-completion.js +261 -0
- package/vendor/codex-router/tool-arguments.js +111 -0
- package/vendor/codex-router/tool-schema-root.js +1008 -0
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
// Vendored from duolahypercho/codex-router at 63ec1f3602c28f2a28ccb7e9edaf7b4f7d191c6c.
|
|
2
|
+
// Source: src/sse-prefix.js; MIT license in LICENSE.
|
|
3
|
+
const UTF8_BOM = Buffer.from([0xef, 0xbb, 0xbf]);
|
|
4
|
+
const SSE_FIELD = /^(?:event|data|id|retry)(?::.*)?$/;
|
|
5
|
+
const SSE_FIELDS = ["event", "data", "id", "retry"];
|
|
6
|
+
|
|
7
|
+
export const HEADERLESS_SSE_SNIFF_BYTES = 512;
|
|
8
|
+
export const HEADERLESS_SSE_SNIFF_MS = 30_000;
|
|
9
|
+
|
|
10
|
+
function startsWithPartialBom(bytes) {
|
|
11
|
+
if (bytes.length >= UTF8_BOM.length) return false;
|
|
12
|
+
for (let index = 0; index < bytes.length; index += 1) {
|
|
13
|
+
if (bytes[index] !== UTF8_BOM[index]) return false;
|
|
14
|
+
}
|
|
15
|
+
return bytes.length > 0;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
// Returns `event-stream` only after the prefix proves SSE framing, `other`
|
|
19
|
+
// after it proves a different body shape, and `pending` while more bytes can
|
|
20
|
+
// still settle the question. Leading BOM, comments, and blank dispatches are
|
|
21
|
+
// valid SSE prelude and are intentionally handled before the first field.
|
|
22
|
+
export function classifySsePrefix(value, { end = false } = {}) {
|
|
23
|
+
let bytes = Buffer.isBuffer(value) ? value : Buffer.from(value || "");
|
|
24
|
+
if (startsWithPartialBom(bytes)) return end ? "other" : "pending";
|
|
25
|
+
if (bytes.subarray(0, UTF8_BOM.length).equals(UTF8_BOM)) {
|
|
26
|
+
bytes = bytes.subarray(UTF8_BOM.length);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const text = bytes.toString("utf8");
|
|
30
|
+
let offset = 0;
|
|
31
|
+
while (offset < text.length) {
|
|
32
|
+
const nextCr = text.indexOf("\r", offset);
|
|
33
|
+
const nextLf = text.indexOf("\n", offset);
|
|
34
|
+
let newline;
|
|
35
|
+
if (nextCr === -1) newline = nextLf;
|
|
36
|
+
else if (nextLf === -1) newline = nextCr;
|
|
37
|
+
else newline = Math.min(nextCr, nextLf);
|
|
38
|
+
if (newline === -1) {
|
|
39
|
+
const line = text.slice(offset);
|
|
40
|
+
if (line.startsWith(":")) return "event-stream";
|
|
41
|
+
if (SSE_FIELDS.some((field) => line.startsWith(`${field}:`))) {
|
|
42
|
+
return "event-stream";
|
|
43
|
+
}
|
|
44
|
+
if (!end && SSE_FIELDS.some((field) => field.startsWith(line))) {
|
|
45
|
+
return "pending";
|
|
46
|
+
}
|
|
47
|
+
return end && SSE_FIELD.test(line) ? "event-stream" : "other";
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const line = text.slice(offset, newline);
|
|
51
|
+
offset =
|
|
52
|
+
text[newline] === "\r" && text[newline + 1] === "\n"
|
|
53
|
+
? newline + 2
|
|
54
|
+
: newline + 1;
|
|
55
|
+
if (!line) continue;
|
|
56
|
+
if (line.startsWith(":") || SSE_FIELD.test(line)) return "event-stream";
|
|
57
|
+
return "other";
|
|
58
|
+
}
|
|
59
|
+
return end ? "other" : "pending";
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Holds only the undecided prefix. Once framing is known, the exact buffered
|
|
63
|
+
// bytes and any remainder are returned in order for the caller to process
|
|
64
|
+
// normally. A body that cannot prove SSE within the byte limit falls back to
|
|
65
|
+
// the non-SSE path rather than growing memory without bound.
|
|
66
|
+
export class HeaderlessSseDetector {
|
|
67
|
+
#buffer = Buffer.alloc(0);
|
|
68
|
+
#decision;
|
|
69
|
+
#maxBytes;
|
|
70
|
+
|
|
71
|
+
constructor({ maxBytes = HEADERLESS_SSE_SNIFF_BYTES } = {}) {
|
|
72
|
+
this.#maxBytes =
|
|
73
|
+
Number.isInteger(maxBytes) && maxBytes >= 0
|
|
74
|
+
? maxBytes
|
|
75
|
+
: HEADERLESS_SSE_SNIFF_BYTES;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
write(chunk) {
|
|
79
|
+
const bytes = Buffer.from(chunk);
|
|
80
|
+
if (this.#decision) return { decision: this.#decision, chunks: [bytes] };
|
|
81
|
+
|
|
82
|
+
const capacity = Math.max(0, this.#maxBytes - this.#buffer.length);
|
|
83
|
+
const prefix = bytes.subarray(0, capacity);
|
|
84
|
+
const remainder = bytes.subarray(prefix.length);
|
|
85
|
+
if (prefix.length) {
|
|
86
|
+
this.#buffer = this.#buffer.length
|
|
87
|
+
? Buffer.concat([this.#buffer, prefix])
|
|
88
|
+
: Buffer.from(prefix);
|
|
89
|
+
}
|
|
90
|
+
let decision = classifySsePrefix(this.#buffer);
|
|
91
|
+
if (
|
|
92
|
+
decision === "pending" &&
|
|
93
|
+
(this.#buffer.length >= this.#maxBytes || remainder.length)
|
|
94
|
+
) {
|
|
95
|
+
decision = "other";
|
|
96
|
+
}
|
|
97
|
+
if (decision === "pending") return { decision, chunks: [] };
|
|
98
|
+
return this.#settle(decision, remainder);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
end() {
|
|
102
|
+
if (this.#decision) return { decision: this.#decision, chunks: [] };
|
|
103
|
+
const decision = classifySsePrefix(this.#buffer, { end: true });
|
|
104
|
+
return this.#settle(decision === "event-stream" ? decision : "other");
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
#settle(decision, remainder = Buffer.alloc(0)) {
|
|
108
|
+
this.#decision = decision;
|
|
109
|
+
const chunks = [];
|
|
110
|
+
if (this.#buffer.length) chunks.push(this.#buffer);
|
|
111
|
+
if (remainder.length) chunks.push(Buffer.from(remainder));
|
|
112
|
+
this.#buffer = Buffer.alloc(0);
|
|
113
|
+
return { decision, chunks };
|
|
114
|
+
}
|
|
115
|
+
}
|
|
@@ -0,0 +1,261 @@
|
|
|
1
|
+
// Vendored from duolahypercho/codex-router at 63ec1f3602c28f2a28ccb7e9edaf7b4f7d191c6c.
|
|
2
|
+
// Source: src/subagent-completion.js; MIT license in LICENSE.
|
|
3
|
+
import { randomUUID } from "node:crypto";
|
|
4
|
+
|
|
5
|
+
// Codex 0.147 keeps a finished child visually Working after FINAL_ANSWER while
|
|
6
|
+
// the parent turn is still live. close_agent is not in the v2 toolset;
|
|
7
|
+
// interrupt_agent is the only model-callable close path. Parents frequently
|
|
8
|
+
// ignore the usage-hint text on long multi-agent turns (San Francisco is the
|
|
9
|
+
// pathological case), so the router injects the missing interrupts itself.
|
|
10
|
+
//
|
|
11
|
+
// This module only decides *what* to inject. The response transform in
|
|
12
|
+
// namespace-relay.js is what splices the calls into the stream without shifting
|
|
13
|
+
// sequence numbers of model-authored items.
|
|
14
|
+
|
|
15
|
+
const FINAL_ANSWER_HEADER =
|
|
16
|
+
/Message Type:\s*FINAL_ANSWER\b[\s\S]*?\nSender:\s*(\S+)/gi;
|
|
17
|
+
const NATIVE_ENCRYPTED_TOKEN = /^gAAAAA[A-Za-z0-9_-]+={0,2}$/;
|
|
18
|
+
|
|
19
|
+
// A close must always name a child. "/root" (and its bare and slashed forms)
|
|
20
|
+
// is the parent itself, and interrupting it would cancel the turn that is
|
|
21
|
+
// still running -- so a sender that resolves to the root is never a target.
|
|
22
|
+
function isRootTarget(target) {
|
|
23
|
+
if (typeof target !== "string") return true;
|
|
24
|
+
const normalized = target.replace(/^\/+/, "").replace(/^root\/?/, "");
|
|
25
|
+
return normalized === "";
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function contentPartsText(content) {
|
|
29
|
+
if (typeof content === "string") return content;
|
|
30
|
+
if (!Array.isArray(content)) return "";
|
|
31
|
+
const parts = [];
|
|
32
|
+
for (const part of content) {
|
|
33
|
+
if (!part || typeof part !== "object") continue;
|
|
34
|
+
if (
|
|
35
|
+
(part.type === "input_text" ||
|
|
36
|
+
part.type === "output_text" ||
|
|
37
|
+
part.type === "text") &&
|
|
38
|
+
typeof part.text === "string"
|
|
39
|
+
) {
|
|
40
|
+
parts.push(part.text);
|
|
41
|
+
continue;
|
|
42
|
+
}
|
|
43
|
+
// Routed children store the plaintext handoff under encrypted_content when
|
|
44
|
+
// the parent never touched the native backend. Native Fernet tokens are
|
|
45
|
+
// skipped: we cannot read them, and they are not FINAL_ANSWER text.
|
|
46
|
+
if (
|
|
47
|
+
part.type === "encrypted_content" &&
|
|
48
|
+
typeof part.encrypted_content === "string" &&
|
|
49
|
+
part.encrypted_content.length > 0 &&
|
|
50
|
+
!NATIVE_ENCRYPTED_TOKEN.test(part.encrypted_content)
|
|
51
|
+
) {
|
|
52
|
+
parts.push(part.encrypted_content);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
return parts.join("");
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function itemText(item) {
|
|
59
|
+
if (!item || typeof item !== "object") return "";
|
|
60
|
+
if (typeof item.content === "string") return item.content;
|
|
61
|
+
return contentPartsText(item.content);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export function extractFinalAnswerTargetsFromText(text) {
|
|
65
|
+
if (typeof text !== "string" || !text) return [];
|
|
66
|
+
const targets = [];
|
|
67
|
+
FINAL_ANSWER_HEADER.lastIndex = 0;
|
|
68
|
+
for (const match of text.matchAll(FINAL_ANSWER_HEADER)) {
|
|
69
|
+
const sender = match[1]?.trim();
|
|
70
|
+
if (sender) targets.push(sender);
|
|
71
|
+
}
|
|
72
|
+
return targets;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function targetsFromAgentMessage(item) {
|
|
76
|
+
if (item?.type !== "agent_message") return [];
|
|
77
|
+
const text = itemText(item);
|
|
78
|
+
const fromText = extractFinalAnswerTargetsFromText(text);
|
|
79
|
+
if (fromText.length) return fromText;
|
|
80
|
+
// Structured author is enough when the envelope declares FINAL_ANSWER, even
|
|
81
|
+
// if Sender was stripped during relay.
|
|
82
|
+
if (
|
|
83
|
+
typeof item.author === "string" &&
|
|
84
|
+
item.author &&
|
|
85
|
+
/Message Type:\s*FINAL_ANSWER\b/i.test(text)
|
|
86
|
+
) {
|
|
87
|
+
return [item.author];
|
|
88
|
+
}
|
|
89
|
+
return [];
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function parseFunctionCallArgs(item) {
|
|
93
|
+
if (typeof item?.arguments !== "string" || !item.arguments) return undefined;
|
|
94
|
+
try {
|
|
95
|
+
const args = JSON.parse(item.arguments);
|
|
96
|
+
if (args && typeof args === "object" && !Array.isArray(args)) return args;
|
|
97
|
+
} catch {
|
|
98
|
+
// Malformed arguments stay unparsed.
|
|
99
|
+
}
|
|
100
|
+
return undefined;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export function isInterruptAgentCall(item) {
|
|
104
|
+
if (!item || item.type !== "function_call") return false;
|
|
105
|
+
if (item.namespace === "collaboration" && item.name === "interrupt_agent") {
|
|
106
|
+
return true;
|
|
107
|
+
}
|
|
108
|
+
return item.name === "collaboration__interrupt_agent" || item.name === "interrupt_agent";
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export function interruptTargetFromCall(item) {
|
|
112
|
+
if (!isInterruptAgentCall(item)) return undefined;
|
|
113
|
+
const args = parseFunctionCallArgs(item);
|
|
114
|
+
const target = args?.target;
|
|
115
|
+
return typeof target === "string" && target.trim() ? target.trim() : undefined;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
export function collaborationToolAvailable(namespaces) {
|
|
119
|
+
if (!(namespaces instanceof Map)) return false;
|
|
120
|
+
const names = namespaces.get("collaboration");
|
|
121
|
+
return names instanceof Set && names.has("interrupt_agent");
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// Walk the request input once. Returns every child that has already finished
|
|
125
|
+
// (FINAL_ANSWER seen) and every child the parent has already interrupted, so
|
|
126
|
+
// the response path only injects the missing closes.
|
|
127
|
+
//
|
|
128
|
+
// Evidence of a finished child comes from `agent_message` items only -- the
|
|
129
|
+
// envelope type Codex uses for collaboration traffic on both the native and
|
|
130
|
+
// routed paths. Ordinary `message` items are the operator's and the model's
|
|
131
|
+
// own prose; scanning them meant a turn that merely *quoted* a FINAL_ANSWER
|
|
132
|
+
// envelope (docs, a changelog, this very feature under discussion) had a
|
|
133
|
+
// fabricated interrupt_agent call spliced into its response.
|
|
134
|
+
export function collectFinishedSubagentState(input) {
|
|
135
|
+
const finished = new Set();
|
|
136
|
+
const interrupted = new Set();
|
|
137
|
+
if (!Array.isArray(input)) {
|
|
138
|
+
return { finished, interrupted, pending: [] };
|
|
139
|
+
}
|
|
140
|
+
for (const item of input) {
|
|
141
|
+
if (!item || typeof item !== "object") continue;
|
|
142
|
+
if (item.type === "function_call" || item.type === "custom_tool_call") {
|
|
143
|
+
const target = interruptTargetFromCall(item);
|
|
144
|
+
if (target) interrupted.add(target);
|
|
145
|
+
continue;
|
|
146
|
+
}
|
|
147
|
+
if (item.type === "agent_message") {
|
|
148
|
+
for (const target of targetsFromAgentMessage(item)) {
|
|
149
|
+
if (!isRootTarget(target)) finished.add(target);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
const pending = [...finished].filter((target) => !interrupted.has(target));
|
|
154
|
+
return { finished, interrupted, pending };
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
export function pendingInterruptTargets(
|
|
158
|
+
input,
|
|
159
|
+
{
|
|
160
|
+
namespaces,
|
|
161
|
+
// Only enforce the tool check when the request actually advertised a
|
|
162
|
+
// non-empty inventory. Empty/unknown inventories (native deferred tools)
|
|
163
|
+
// still queue closes for finished children. That bypass is safe only
|
|
164
|
+
// because detection is scoped to `agent_message` envelopes: an ordinary
|
|
165
|
+
// turn cannot contain one, so an empty inventory plus quoted envelope
|
|
166
|
+
// text can no longer manufacture an interrupt.
|
|
167
|
+
requireCollaborationTool = namespaces instanceof Map && namespaces.size > 0,
|
|
168
|
+
} = {},
|
|
169
|
+
) {
|
|
170
|
+
if (
|
|
171
|
+
requireCollaborationTool &&
|
|
172
|
+
namespaces &&
|
|
173
|
+
!collaborationToolAvailable(namespaces)
|
|
174
|
+
) {
|
|
175
|
+
return [];
|
|
176
|
+
}
|
|
177
|
+
return collectFinishedSubagentState(input).pending;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function sameTarget(a, b) {
|
|
181
|
+
if (a === b) return true;
|
|
182
|
+
if (typeof a !== "string" || typeof b !== "string") return false;
|
|
183
|
+
// Codex accepts both "/root/child" and "child" forms for interrupt_agent.
|
|
184
|
+
const normalize = (value) => value.replace(/^\/+/, "").replace(/^root\//, "");
|
|
185
|
+
return normalize(a) === normalize(b);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
export function filterAlreadyInterrupted(pending, interruptedTargets) {
|
|
189
|
+
if (!Array.isArray(pending) || pending.length === 0) return [];
|
|
190
|
+
if (!interruptedTargets || interruptedTargets.size === 0) return [...pending];
|
|
191
|
+
return pending.filter(
|
|
192
|
+
(target) => ![...interruptedTargets].some((done) => sameTarget(done, target)),
|
|
193
|
+
);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
export function buildInterruptAgentCall(target, { callId, flattened = false } = {}) {
|
|
197
|
+
if (typeof target !== "string" || !target.trim()) {
|
|
198
|
+
throw new Error("interrupt_agent target is required");
|
|
199
|
+
}
|
|
200
|
+
const id =
|
|
201
|
+
typeof callId === "string" && callId
|
|
202
|
+
? callId
|
|
203
|
+
: `call_router_interrupt_${randomUUID().replaceAll("-", "")}`;
|
|
204
|
+
if (flattened) {
|
|
205
|
+
return {
|
|
206
|
+
type: "function_call",
|
|
207
|
+
name: "collaboration__interrupt_agent",
|
|
208
|
+
call_id: id,
|
|
209
|
+
arguments: JSON.stringify({ target }),
|
|
210
|
+
};
|
|
211
|
+
}
|
|
212
|
+
return {
|
|
213
|
+
type: "function_call",
|
|
214
|
+
name: "interrupt_agent",
|
|
215
|
+
namespace: "collaboration",
|
|
216
|
+
call_id: id,
|
|
217
|
+
arguments: JSON.stringify({ target }),
|
|
218
|
+
};
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// Build the SSE events for one injected interrupt. Sequence numbers are filled
|
|
222
|
+
// by the stream transform once it knows the last model-emitted sequence.
|
|
223
|
+
export function interruptAgentSseEvents(target, { callId, sequenceStart = 1 } = {}) {
|
|
224
|
+
const item = buildInterruptAgentCall(target, { callId, flattened: false });
|
|
225
|
+
const addedSeq = sequenceStart;
|
|
226
|
+
const doneSeq = sequenceStart + 1;
|
|
227
|
+
return [
|
|
228
|
+
{
|
|
229
|
+
event: "response.output_item.added",
|
|
230
|
+
data: {
|
|
231
|
+
type: "response.output_item.added",
|
|
232
|
+
sequence_number: addedSeq,
|
|
233
|
+
item: {
|
|
234
|
+
type: "function_call",
|
|
235
|
+
name: item.name,
|
|
236
|
+
namespace: item.namespace,
|
|
237
|
+
call_id: item.call_id,
|
|
238
|
+
arguments: "",
|
|
239
|
+
},
|
|
240
|
+
},
|
|
241
|
+
},
|
|
242
|
+
{
|
|
243
|
+
event: "response.output_item.done",
|
|
244
|
+
data: {
|
|
245
|
+
type: "response.output_item.done",
|
|
246
|
+
sequence_number: doneSeq,
|
|
247
|
+
item: {
|
|
248
|
+
type: "function_call",
|
|
249
|
+
name: item.name,
|
|
250
|
+
namespace: item.namespace,
|
|
251
|
+
call_id: item.call_id,
|
|
252
|
+
arguments: item.arguments,
|
|
253
|
+
},
|
|
254
|
+
},
|
|
255
|
+
},
|
|
256
|
+
];
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
export function formatSseBlock(eventName, data) {
|
|
260
|
+
return `event: ${eventName}\ndata: ${JSON.stringify(data)}\n\n`;
|
|
261
|
+
}
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
// Vendored from duolahypercho/codex-router at 63ec1f3602c28f2a28ccb7e9edaf7b4f7d191c6c.
|
|
2
|
+
// Source: src/tool-arguments.js; MIT license in LICENSE.
|
|
3
|
+
// Codex tool schemas use integer/u64 fields. Some routed models (Grok in
|
|
4
|
+
// particular) emit whole numbers as JSON floats (`20000.0`). Serde then
|
|
5
|
+
// rejects the call before the tool runs.
|
|
6
|
+
//
|
|
7
|
+
// JSON.parse cannot see the difference between 20000 and 20000.0, so this
|
|
8
|
+
// rewrites number tokens in the raw argument string.
|
|
9
|
+
|
|
10
|
+
const JSON_NUMBER = /^-?(?:0|[1-9]\d*)(?:\.\d+)?(?:[eE][+-]?\d+)?/;
|
|
11
|
+
const JSON_NUMBER_PARTS = /^(-)?(0|[1-9]\d*)(?:\.(\d+))?(?:[eE]([+-]?\d+))?$/;
|
|
12
|
+
const PLAIN_INTEGER = /^-?(?:0|[1-9]\d*)$/;
|
|
13
|
+
|
|
14
|
+
// Decide integrality from the token spelling. JS Number rounds 1e-324 to 0
|
|
15
|
+
// and cannot represent every u64, so it must not be the judge.
|
|
16
|
+
function integerSpelling(token) {
|
|
17
|
+
const parts = token.match(JSON_NUMBER_PARTS);
|
|
18
|
+
if (!parts) return undefined;
|
|
19
|
+
const sign = parts[1] || "";
|
|
20
|
+
const intPart = parts[2];
|
|
21
|
+
const fracPart = parts[3] || "";
|
|
22
|
+
const exp = parts[4] === undefined ? 0 : Number.parseInt(parts[4], 10);
|
|
23
|
+
if (!Number.isSafeInteger(exp)) return undefined;
|
|
24
|
+
const digits = intPart + fracPart;
|
|
25
|
+
const point = intPart.length + exp;
|
|
26
|
+
if (point > 40) return undefined;
|
|
27
|
+
for (let i = Math.max(0, point); i < digits.length; i += 1) {
|
|
28
|
+
if (digits[i] !== "0") return undefined;
|
|
29
|
+
}
|
|
30
|
+
let integerDigits;
|
|
31
|
+
if (point <= 0) integerDigits = "0";
|
|
32
|
+
else if (point >= digits.length) integerDigits = digits + "0".repeat(point - digits.length);
|
|
33
|
+
else integerDigits = digits.slice(0, point);
|
|
34
|
+
integerDigits = integerDigits.replace(/^0+(?=\d)/, "") || "0";
|
|
35
|
+
if (integerDigits === "0") return "0";
|
|
36
|
+
return `${sign}${integerDigits}`;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function integerToken(token) {
|
|
40
|
+
if (PLAIN_INTEGER.test(token) && token !== "-0") return token;
|
|
41
|
+
return integerSpelling(token) ?? token;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function rewriteWholeNumberTokens(raw) {
|
|
45
|
+
if (typeof raw !== "string" || raw.length === 0) return raw;
|
|
46
|
+
let out = "";
|
|
47
|
+
let inString = false;
|
|
48
|
+
let escaped = false;
|
|
49
|
+
for (let i = 0; i < raw.length; ) {
|
|
50
|
+
const ch = raw[i];
|
|
51
|
+
if (inString) {
|
|
52
|
+
out += ch;
|
|
53
|
+
if (escaped) escaped = false;
|
|
54
|
+
else if (ch === "\\") escaped = true;
|
|
55
|
+
else if (ch === '"') inString = false;
|
|
56
|
+
i += 1;
|
|
57
|
+
continue;
|
|
58
|
+
}
|
|
59
|
+
if (ch === '"') {
|
|
60
|
+
inString = true;
|
|
61
|
+
out += ch;
|
|
62
|
+
i += 1;
|
|
63
|
+
continue;
|
|
64
|
+
}
|
|
65
|
+
if (ch === "-" || (ch >= "0" && ch <= "9")) {
|
|
66
|
+
const match = raw.slice(i).match(JSON_NUMBER);
|
|
67
|
+
if (match) {
|
|
68
|
+
out += integerToken(match[0]);
|
|
69
|
+
i += match[0].length;
|
|
70
|
+
continue;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
out += ch;
|
|
74
|
+
i += 1;
|
|
75
|
+
}
|
|
76
|
+
return out;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export function coerceWholeNumberJson(value) {
|
|
80
|
+
if (Array.isArray(value)) {
|
|
81
|
+
return value.map((entry) => coerceWholeNumberJson(entry));
|
|
82
|
+
}
|
|
83
|
+
if (!value || typeof value !== "object") {
|
|
84
|
+
if (typeof value === "number" && Number.isSafeInteger(value)) {
|
|
85
|
+
return Object.is(value, -0) ? 0 : value;
|
|
86
|
+
}
|
|
87
|
+
return value;
|
|
88
|
+
}
|
|
89
|
+
const next = {};
|
|
90
|
+
for (const [key, entry] of Object.entries(value)) {
|
|
91
|
+
next[key] = coerceWholeNumberJson(entry);
|
|
92
|
+
}
|
|
93
|
+
return next;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export function coerceFunctionCallArguments(raw) {
|
|
97
|
+
if (typeof raw !== "string") return raw;
|
|
98
|
+
try {
|
|
99
|
+
JSON.parse(raw);
|
|
100
|
+
} catch {
|
|
101
|
+
return raw;
|
|
102
|
+
}
|
|
103
|
+
const rewritten = rewriteWholeNumberTokens(raw);
|
|
104
|
+
if (rewritten === raw) return raw;
|
|
105
|
+
try {
|
|
106
|
+
JSON.parse(rewritten);
|
|
107
|
+
} catch {
|
|
108
|
+
return raw;
|
|
109
|
+
}
|
|
110
|
+
return rewritten;
|
|
111
|
+
}
|