@oh-my-pi/pi-coding-agent 15.1.2 → 15.1.4

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.
Files changed (155) hide show
  1. package/CHANGELOG.md +60 -0
  2. package/dist/types/async/job-manager.d.ts +3 -2
  3. package/dist/types/cli/auth-broker-cli.d.ts +25 -0
  4. package/dist/types/cli/auth-gateway-cli.d.ts +18 -0
  5. package/dist/types/cli/grievances-cli.d.ts +12 -0
  6. package/dist/types/commands/auth-broker.d.ts +54 -0
  7. package/dist/types/commands/auth-gateway.d.ts +32 -0
  8. package/dist/types/commands/grievances.d.ts +1 -1
  9. package/dist/types/commit/agentic/tools/propose-commit.d.ts +9 -1
  10. package/dist/types/commit/agentic/tools/schemas.d.ts +9 -1
  11. package/dist/types/commit/agentic/tools/split-commit.d.ts +9 -1
  12. package/dist/types/config/model-registry.d.ts +3 -0
  13. package/dist/types/config/models-config-schema.d.ts +1 -0
  14. package/dist/types/config/settings-schema.d.ts +46 -0
  15. package/dist/types/discovery/agents.d.ts +12 -1
  16. package/dist/types/edit/renderer.d.ts +3 -0
  17. package/dist/types/eval/index.d.ts +0 -2
  18. package/dist/types/goals/tools/goal-tool.d.ts +10 -2
  19. package/dist/types/index.d.ts +0 -1
  20. package/dist/types/internal-urls/index.d.ts +1 -1
  21. package/dist/types/internal-urls/{pi-protocol.d.ts → omp-protocol.d.ts} +3 -3
  22. package/dist/types/internal-urls/types.d.ts +1 -1
  23. package/dist/types/main.d.ts +11 -2
  24. package/dist/types/modes/acp/acp-agent.d.ts +2 -1
  25. package/dist/types/modes/acp/acp-event-mapper.d.ts +13 -1
  26. package/dist/types/modes/acp/acp-mode.d.ts +3 -1
  27. package/dist/types/modes/emoji-autocomplete.d.ts +16 -0
  28. package/dist/types/modes/interactive-mode.d.ts +1 -1
  29. package/dist/types/modes/prompt-action-autocomplete.d.ts +4 -0
  30. package/dist/types/plan-mode/approved-plan.d.ts +10 -4
  31. package/dist/types/sdk.d.ts +10 -3
  32. package/dist/types/session/agent-session.d.ts +7 -3
  33. package/dist/types/session/auth-broker-config.d.ts +13 -0
  34. package/dist/types/session/auth-storage.d.ts +1 -1
  35. package/dist/types/session/client-bridge.d.ts +3 -0
  36. package/dist/types/tools/eval.d.ts +41 -7
  37. package/dist/types/tools/irc.d.ts +8 -2
  38. package/dist/types/tools/report-tool-issue.d.ts +118 -1
  39. package/dist/types/tools/resolve.d.ts +8 -2
  40. package/examples/custom-tools/README.md +3 -12
  41. package/examples/extensions/README.md +2 -15
  42. package/examples/extensions/api-demo.ts +1 -7
  43. package/package.json +7 -7
  44. package/src/async/job-manager.ts +111 -13
  45. package/src/autoresearch/tools/init-experiment.ts +11 -33
  46. package/src/autoresearch/tools/log-experiment.ts +10 -24
  47. package/src/autoresearch/tools/run-experiment.ts +1 -1
  48. package/src/autoresearch/tools/update-notes.ts +2 -9
  49. package/src/cli/auth-broker-cli.ts +746 -0
  50. package/src/cli/auth-gateway-cli.ts +342 -0
  51. package/src/cli/grievances-cli.ts +109 -16
  52. package/src/cli/update-cli.ts +1 -5
  53. package/src/cli.ts +4 -2
  54. package/src/commands/auth-broker.ts +96 -0
  55. package/src/commands/auth-gateway.ts +61 -0
  56. package/src/commands/grievances.ts +13 -8
  57. package/src/commands/launch.ts +1 -1
  58. package/src/commit/agentic/agent.ts +2 -0
  59. package/src/commit/agentic/tools/analyze-file.ts +2 -2
  60. package/src/commit/agentic/tools/git-file-diff.ts +2 -2
  61. package/src/commit/agentic/tools/git-hunk.ts +3 -3
  62. package/src/commit/agentic/tools/git-overview.ts +2 -2
  63. package/src/commit/agentic/tools/propose-changelog.ts +1 -3
  64. package/src/commit/agentic/tools/recent-commits.ts +1 -1
  65. package/src/commit/agentic/tools/schemas.ts +1 -9
  66. package/src/config/model-equivalence.ts +279 -174
  67. package/src/config/model-registry.ts +37 -6
  68. package/src/config/model-resolver.ts +13 -8
  69. package/src/config/models-config-schema.ts +8 -0
  70. package/src/config/settings-schema.ts +52 -0
  71. package/src/cursor.ts +1 -1
  72. package/src/debug/log-formatting.ts +1 -1
  73. package/src/debug/log-viewer.ts +1 -1
  74. package/src/debug/profiler.ts +4 -0
  75. package/src/debug/raw-sse-buffer.ts +100 -59
  76. package/src/debug/raw-sse.ts +1 -1
  77. package/src/discovery/agents.ts +15 -4
  78. package/src/edit/modes/apply-patch.ts +1 -5
  79. package/src/edit/modes/patch.ts +5 -5
  80. package/src/edit/modes/replace.ts +5 -5
  81. package/src/edit/renderer.ts +2 -1
  82. package/src/edit/streaming.ts +1 -1
  83. package/src/eval/index.ts +0 -2
  84. package/src/eval/js/shared/runtime.ts +107 -2
  85. package/src/eval/py/kernel.ts +1 -1
  86. package/src/exa/researcher.ts +4 -4
  87. package/src/exa/search.ts +10 -22
  88. package/src/exa/websets.ts +33 -33
  89. package/src/extensibility/typebox.ts +44 -17
  90. package/src/goals/tools/goal-tool.ts +3 -3
  91. package/src/index.ts +0 -3
  92. package/src/internal-urls/docs-index.generated.ts +21 -18
  93. package/src/internal-urls/index.ts +1 -1
  94. package/src/internal-urls/{pi-protocol.ts → omp-protocol.ts} +10 -10
  95. package/src/internal-urls/router.ts +3 -3
  96. package/src/internal-urls/types.ts +1 -1
  97. package/src/lsp/types.ts +8 -11
  98. package/src/main.ts +216 -146
  99. package/src/mcp/tool-bridge.ts +3 -3
  100. package/src/modes/acp/acp-agent.ts +203 -57
  101. package/src/modes/acp/acp-client-bridge.ts +2 -1
  102. package/src/modes/acp/acp-event-mapper.ts +208 -32
  103. package/src/modes/acp/acp-mode.ts +11 -3
  104. package/src/modes/components/bash-execution.ts +1 -1
  105. package/src/modes/components/diff.ts +1 -2
  106. package/src/modes/components/eval-execution.ts +1 -1
  107. package/src/modes/components/oauth-selector.ts +38 -2
  108. package/src/modes/components/tool-execution.ts +1 -2
  109. package/src/modes/components/tree-selector.ts +26 -7
  110. package/src/modes/controllers/command-controller.ts +95 -34
  111. package/src/modes/controllers/input-controller.ts +4 -3
  112. package/src/modes/data/emojis.json +1 -0
  113. package/src/modes/emoji-autocomplete.ts +285 -0
  114. package/src/modes/interactive-mode.ts +92 -19
  115. package/src/modes/print-mode.ts +3 -3
  116. package/src/modes/prompt-action-autocomplete.ts +14 -0
  117. package/src/plan-mode/approved-plan.ts +30 -9
  118. package/src/prompts/system/system-prompt.md +1 -1
  119. package/src/prompts/system/ttsr-tool-reminder.md +5 -0
  120. package/src/prompts/tools/ask.md +4 -3
  121. package/src/prompts/tools/eval.md +25 -26
  122. package/src/prompts/tools/read.md +1 -1
  123. package/src/prompts/tools/resolve.md +1 -1
  124. package/src/prompts/tools/search.md +1 -1
  125. package/src/prompts/tools/web-search.md +1 -1
  126. package/src/sdk.ts +81 -8
  127. package/src/session/agent-session.ts +362 -131
  128. package/src/session/agent-storage.ts +7 -2
  129. package/src/session/auth-broker-config.ts +102 -0
  130. package/src/session/auth-storage.ts +7 -1
  131. package/src/session/client-bridge.ts +3 -0
  132. package/src/session/streaming-output.ts +1 -1
  133. package/src/task/types.ts +10 -35
  134. package/src/tools/bash-interactive.ts +4 -1
  135. package/src/tools/bash-pty-selection.ts +2 -2
  136. package/src/tools/browser.ts +12 -20
  137. package/src/tools/eval.ts +77 -100
  138. package/src/tools/gh.ts +21 -45
  139. package/src/tools/hindsight-recall.ts +1 -1
  140. package/src/tools/hindsight-reflect.ts +2 -2
  141. package/src/tools/hindsight-retain.ts +3 -7
  142. package/src/tools/index.ts +8 -1
  143. package/src/tools/inspect-image.ts +4 -1
  144. package/src/tools/irc.ts +4 -12
  145. package/src/tools/job.ts +3 -11
  146. package/src/tools/report-tool-issue.ts +462 -17
  147. package/src/tools/resolve.ts +2 -7
  148. package/src/tools/todo-write.ts +8 -15
  149. package/src/utils/title-generator.ts +3 -0
  150. package/src/web/search/index.ts +6 -6
  151. package/dist/types/eval/parse.d.ts +0 -28
  152. package/dist/types/eval/sniff.d.ts +0 -11
  153. package/src/eval/eval.lark +0 -36
  154. package/src/eval/parse.ts +0 -407
  155. package/src/eval/sniff.ts +0 -28
@@ -1,36 +0,0 @@
1
- // Canonical Eval input. Each cell is introduced by a single header line:
2
- //
3
- // *** Cell <LANG>:"<title>" [t:<duration>] [rst]
4
- //
5
- // Attribute order is fixed: language+title, then optional timeout, then
6
- // optional reset flag. Title may be empty (`py:""`).
7
- //
8
- // Tokens:
9
- //
10
- // py:"..." | js:"..." language plus title (required)
11
- // t:<digits>(ms|s|m)? per-cell timeout (default 30s)
12
- // rst reset this language's kernel before running
13
- //
14
- // Everything between one header line and the next (or the optional trailing
15
- // `*** End`, or end of input) is the cell's code, verbatim. The runtime
16
- // parser additionally accepts content before the first header as an implicit
17
- // default-language cell, but that is lenient fallback and MUST NOT be relied
18
- // on.
19
-
20
- start: cell+ end_marker
21
-
22
- cell: cell_header code_line*
23
-
24
- cell_header: "*** Cell" WS_INLINE LANG_TITLE (WS_INLINE T_ATTR)? (WS_INLINE RST_FLAG)? LF
25
-
26
- end_marker: "*** End" LF?
27
-
28
- code_line: CODE_TEXT LF | LF
29
- CODE_TEXT: /([^*\r\n]|\*\*?[^*\r\n])+\*{0,2}|\*{1,2}/
30
-
31
- LANG_TITLE: ("py" | "js") ":\"" /[^"\r\n]*/ "\""
32
- T_ATTR: "t:" /\d+(ms|s|m)?/
33
- RST_FLAG: "rst"
34
-
35
- %import common.LF
36
- %import common.WS_INLINE
package/src/eval/parse.ts DELETED
@@ -1,407 +0,0 @@
1
- import { sniffEvalLanguage } from "./sniff";
2
- import type { EvalLanguage } from "./types";
3
-
4
- export type EvalLanguageOrigin = "default" | "header";
5
-
6
- export interface ParsedEvalCell {
7
- index: number;
8
- title?: string;
9
- code: string;
10
- language: EvalLanguage;
11
- languageOrigin: EvalLanguageOrigin;
12
- timeoutMs: number;
13
- reset: boolean;
14
- }
15
-
16
- export interface ParsedEvalInput {
17
- cells: ParsedEvalCell[];
18
- /**
19
- * True when the parser encountered `*** Abort` (recovery sentinel emitted
20
- * by the agent loop's harmony-leak mitigation; see
21
- * `docs/ERRATA-GPT5-HARMONY.md`). The cell containing the marker, if any,
22
- * is dropped — its body is incomplete and unsafe to execute.
23
- */
24
- aborted?: boolean;
25
- }
26
-
27
- const DEFAULT_TIMEOUT_MS = 30_000;
28
- const DEFAULT_LANGUAGE: EvalLanguage = "python";
29
-
30
- /**
31
- * Canonical language tokens plus common long-form aliases. The grammar
32
- * advertises only `PY` / `JS` / `TS`, but unconstrained models reach for
33
- * `Python` / `JavaScript` / `TypeScript` often enough that we accept them.
34
- */
35
- const LANGUAGE_MAP: Record<string, EvalLanguage> = {
36
- PY: "python",
37
- PYTHON: "python",
38
- IPY: "python",
39
- IPYTHON: "python",
40
- JS: "js",
41
- JAVASCRIPT: "js",
42
- TS: "js",
43
- TYPESCRIPT: "js",
44
- };
45
-
46
- // Markers are case-insensitive, accept ≥2 leading stars (so `**Cell` and
47
- // `*** Cell` both work), and tolerate any whitespace (including tabs)
48
- // between tokens. Models that can't constrain-sample frequently emit minor
49
- // variations like `**End` or `*** cell py`.
50
- const STARS = String.raw`\*{2,}`;
51
- // Cell header: `*** Cell <attrs...>`. The remainder of the line is captured
52
- // and tokenized separately so we can handle quoted values.
53
- const CELL_RE = new RegExp(`^${STARS}\\s*Cell\\b\\s*(.*)$`, "i");
54
- // `*** End` is a tolerated cell/file terminator. Documented as required at
55
- // the file level in the lark grammar (the trailing `*** End` quirks GPT-
56
- // trained models naturally produce), but optional at the parser level.
57
- const END_RE = new RegExp(`^${STARS}\\s*End\\b.*$`, "i");
58
- // `*** Abort` is the harmony-leak recovery sentinel; see ABORT_WARNING.
59
- const ABORT_RE = new RegExp(`^${STARS}\\s*Abort\\s*$`, "i");
60
-
61
- /**
62
- * Warning text appended to the eval tool result when parsing terminated on
63
- * `*** Abort`. Tells the model that earlier cells (if any) ran normally and
64
- * that any aborted cell needs to be re-issued.
65
- */
66
- export const ABORT_WARNING =
67
- "Tool stream truncated mid-call due to detected output corruption. Earlier cells (if any) executed normally; their state persists. Re-issue the aborted cell.";
68
-
69
- const DURATION_RE = /^(\d+)(ms|s|m)?$/i;
70
-
71
- function resolveLang(token: string | undefined): EvalLanguage | undefined {
72
- return token ? LANGUAGE_MAP[token.toUpperCase()] : undefined;
73
- }
74
-
75
- function parseDurationMs(raw: string, lineNumber: number): number {
76
- const match = DURATION_RE.exec(raw.trim());
77
- if (!match) {
78
- throw new Error(
79
- `Eval line ${lineNumber}: invalid duration \`${raw}\`; use a number with optional ms, s, or m units.`,
80
- );
81
- }
82
- const value = Number.parseInt(match[1], 10);
83
- const unit = (match[2] ?? "s").toLowerCase();
84
- if (unit === "ms") return value;
85
- if (unit === "s") return value * 1000;
86
- return value * 60_000;
87
- }
88
-
89
- // Markdown fence wrapping a single bare cell, e.g. "```py\n...\n```" or
90
- // "```\n...\n```". Used by models that wrap eval input in code fences.
91
- const FENCE_OPEN_RE = /^```\s*([A-Za-z]\w*)?\s*$/;
92
- const FENCE_CLOSE_RE = /^```\s*$/;
93
-
94
- /**
95
- * Last-resort fallback when the input has no recognizable `*** Cell` header.
96
- * Models that can't constrain-sample sometimes pass bare code or wrap it in
97
- * a markdown fence (```py / ```python / bare ```). Treat the whole input as
98
- * a single implicit cell, sniffing the language from the body.
99
- */
100
- function parseImplicitCell(lines: string[]): ParsedEvalCell {
101
- let body = lines.slice();
102
- while (body.length > 0 && body[0].trim() === "") body.shift();
103
- while (body.length > 0 && body[body.length - 1].trim() === "") body.pop();
104
-
105
- let fenceLang: string | undefined;
106
- if (body.length >= 2) {
107
- const open = FENCE_OPEN_RE.exec(body[0]);
108
- const closeIdx = body.length - 1;
109
- if (open && FENCE_CLOSE_RE.test(body[closeIdx])) {
110
- fenceLang = open[1];
111
- body = body.slice(1, closeIdx);
112
- }
113
- }
114
-
115
- const code = body.join("\n");
116
- const explicitLanguage = resolveLang(fenceLang);
117
- const language = explicitLanguage ?? sniffEvalLanguage(code) ?? DEFAULT_LANGUAGE;
118
- return {
119
- index: 0,
120
- title: undefined,
121
- code,
122
- language,
123
- languageOrigin: explicitLanguage ? "header" : "default",
124
- timeoutMs: DEFAULT_TIMEOUT_MS,
125
- reset: false,
126
- };
127
- }
128
-
129
- /**
130
- * Tokenize a `*** Cell` header's attribute list while preserving quoted
131
- * segments (`id:"some title"`, `py:"hi"`, single quotes too) as single
132
- * tokens. Outer whitespace separates tokens; the quote characters
133
- * themselves are kept verbatim so attribute parsing can strip them later.
134
- */
135
- function tokenizeCellAttrs(input: string): string[] {
136
- const tokens: string[] = [];
137
- let i = 0;
138
- while (i < input.length) {
139
- while (i < input.length && /\s/.test(input[i])) i++;
140
- if (i >= input.length) break;
141
- let token = "";
142
- while (i < input.length && !/\s/.test(input[i])) {
143
- const ch = input[i];
144
- if (ch === '"' || ch === "'") {
145
- token += ch;
146
- i++;
147
- while (i < input.length && input[i] !== ch) {
148
- token += input[i];
149
- i++;
150
- }
151
- if (i < input.length) {
152
- token += input[i];
153
- i++;
154
- }
155
- } else {
156
- token += ch;
157
- i++;
158
- }
159
- }
160
- tokens.push(token);
161
- }
162
- return tokens;
163
- }
164
-
165
- interface CellHeader {
166
- language: EvalLanguage | undefined;
167
- languageOrigin: EvalLanguageOrigin;
168
- title: string | undefined;
169
- timeoutMs: number | undefined;
170
- reset: boolean;
171
- }
172
-
173
- /**
174
- * Map an attribute key (from `key:value` or bare `key`) to one of the three
175
- * canonical roles. Canonical keys: `id`, `t`, `rst`. Fallback aliases —
176
- * accepted but not advertised in the prompt — cover common synonyms LLMs
177
- * reach for instead of the short canonical.
178
- */
179
- const ID_KEYS = new Set(["id", "title", "name", "cell", "file", "label"]);
180
- const T_KEYS = new Set(["t", "timeout", "duration", "time"]);
181
- const RST_KEYS = new Set(["rst", "reset"]);
182
-
183
- function classifyAttrKey(key: string): "id" | "t" | "rst" | null {
184
- if (ID_KEYS.has(key)) return "id";
185
- if (T_KEYS.has(key)) return "t";
186
- if (RST_KEYS.has(key)) return "rst";
187
- return null;
188
- }
189
-
190
- // `key:value` form. `value` may be `"..."`, `'...'`, or a bare run.
191
- const ATTR_TOKEN_RE = /^([a-zA-Z][\w-]*)(?::(?:"([^"]*)"|'([^']*)'|(.*)))?$/;
192
- // Bare positional duration (lenient — `t:` is canonical).
193
- const DURATION_TOKEN_RE = /^\d+(?:ms|s|m)?$/;
194
-
195
- function parseBooleanFlag(value: string): boolean | undefined {
196
- const v = value.trim().toLowerCase();
197
- if (v === "true" || v === "1" || v === "yes" || v === "on") return true;
198
- if (v === "false" || v === "0" || v === "no" || v === "off") return false;
199
- return undefined;
200
- }
201
-
202
- /**
203
- * Decode a `*** Cell` header's attribute list into language, title,
204
- * timeout, and reset flag.
205
- *
206
- * Token forms (all optional, any order):
207
- * - `py` / `js` / `ts` bare language
208
- * - `py:"..."` / `js:"..."` / `ts:"..."` language + title shorthand
209
- * - `id:"..."` cell title (canonical)
210
- * - `t:<duration>` per-cell timeout (canonical)
211
- * - `<duration>` (e.g. `30s`) bare positional duration
212
- * - `rst` reset flag (canonical)
213
- * - `rst:true|false|1|0|yes|no|on|off` reset flag with explicit value
214
- *
215
- * Fallback aliases (accepted but not advertised in the prompt):
216
- * - id: title, name, cell, file, label
217
- * - t: timeout, duration, time
218
- * - rst: reset
219
- *
220
- * Quotes may be `"` or `'`. Truly unknown keys are silently dropped. First
221
- * occurrence wins when a key is repeated (canonical or alias). Anything
222
- * that doesn't classify accumulates as a positional title fragment joined
223
- * by spaces.
224
- */
225
- function parseCellHeader(rest: string, lineNumber: number): CellHeader {
226
- const tokens = tokenizeCellAttrs(rest);
227
- let language: EvalLanguage | undefined;
228
- let titleAttr: string | undefined;
229
- let positionalDurationMs: number | undefined;
230
- let tAttr: string | undefined;
231
- let rstAttr: string | undefined;
232
- let bareReset = false;
233
- const titleParts: string[] = [];
234
-
235
- for (const token of tokens) {
236
- // Bare reset flag (canonical or alias).
237
- if (RST_KEYS.has(token.toLowerCase())) {
238
- bareReset = true;
239
- continue;
240
- }
241
-
242
- const attrMatch = ATTR_TOKEN_RE.exec(token);
243
- if (attrMatch && token.includes(":")) {
244
- const key = attrMatch[1].toLowerCase();
245
- const value = attrMatch[2] ?? attrMatch[3] ?? attrMatch[4] ?? "";
246
-
247
- // Language-with-title shorthand: `py:"foo"`, `js:'bar'`, etc.
248
- const langCandidate = resolveLang(key);
249
- if (langCandidate) {
250
- if (language === undefined) language = langCandidate;
251
- if (titleAttr === undefined && value !== "") titleAttr = value;
252
- continue;
253
- }
254
-
255
- const role = classifyAttrKey(key);
256
- if (role === "id" && titleAttr === undefined) titleAttr = value;
257
- else if (role === "t" && tAttr === undefined) tAttr = value;
258
- else if (role === "rst" && rstAttr === undefined) rstAttr = value;
259
- // unknown / repeated keys silently dropped
260
- continue;
261
- }
262
-
263
- // Bare language token (no colon).
264
- const lang = resolveLang(token);
265
- if (lang && language === undefined) {
266
- language = lang;
267
- continue;
268
- }
269
-
270
- // Bare positional duration (lenient — `t:` is canonical).
271
- if (positionalDurationMs === undefined && DURATION_TOKEN_RE.test(token)) {
272
- positionalDurationMs = parseDurationMs(token, lineNumber);
273
- continue;
274
- }
275
-
276
- titleParts.push(token);
277
- }
278
-
279
- const explicitTitle = (titleAttr ?? "").trim();
280
- const positionalTitle = titleParts.join(" ").trim();
281
- const title = explicitTitle.length > 0 ? explicitTitle : positionalTitle.length > 0 ? positionalTitle : undefined;
282
-
283
- let timeoutMs: number | undefined;
284
- if (tAttr !== undefined) {
285
- timeoutMs = parseDurationMs(tAttr, lineNumber);
286
- } else if (positionalDurationMs !== undefined) {
287
- timeoutMs = positionalDurationMs;
288
- }
289
-
290
- let reset = false;
291
- if (rstAttr !== undefined) {
292
- const parsed = parseBooleanFlag(rstAttr);
293
- if (parsed === undefined) {
294
- throw new Error(`Eval line ${lineNumber}: invalid rst value \`${rstAttr}\`; use true or false.`);
295
- }
296
- reset = parsed;
297
- } else if (bareReset) {
298
- reset = true;
299
- }
300
-
301
- return {
302
- language,
303
- languageOrigin: language ? "header" : "default",
304
- title,
305
- timeoutMs,
306
- reset,
307
- };
308
- }
309
-
310
- export function parseEvalInput(input: string): ParsedEvalInput {
311
- const normalized = input.replace(/\r\n?/g, "\n");
312
- const lines = normalized.split("\n");
313
- if (lines.length > 0 && lines[lines.length - 1] === "") lines.pop();
314
-
315
- const cells: ParsedEvalCell[] = [];
316
- let aborted = false;
317
- let i = 0;
318
-
319
- // Skip leading blank lines.
320
- while (i < lines.length && lines[i].trim() === "") i++;
321
-
322
- // Lenient fallback: if the input has no recognizable cell header, treat
323
- // the entire input as one implicit cell — unless that content contains
324
- // `*** Abort`, in which case the body is incomplete/unsafe and we drop it.
325
- if (i < lines.length && !CELL_RE.test(lines[i])) {
326
- const tail = lines.slice(i);
327
- if (tail.some(line => ABORT_RE.test(line))) {
328
- return { cells, aborted: true };
329
- }
330
- const cell = parseImplicitCell(tail);
331
- if (cell.code.length > 0) cells.push(cell);
332
- return { cells };
333
- }
334
-
335
- while (i < lines.length) {
336
- const headerLine = lines[i];
337
- const cellMatch = CELL_RE.exec(headerLine);
338
- if (!cellMatch) {
339
- // Stray content between/after cells (blank lines were already
340
- // consumed). `*** Abort` here terminates parsing; `*** End` is
341
- // the optional file-level terminator (silently consumed). Anything
342
- // else — typically a harmony-leak fragment — is skipped.
343
- if (ABORT_RE.test(headerLine)) {
344
- aborted = true;
345
- break;
346
- }
347
- i++;
348
- continue;
349
- }
350
- const header = parseCellHeader(cellMatch[1] ?? "", i + 1);
351
- i++;
352
-
353
- // Collect cell body. Close on `*** End` (any form), the next
354
- // `*** Cell` header, or `*** Abort` (which drops the in-progress
355
- // cell as its body is partial and unsafe to run).
356
- const codeLines: string[] = [];
357
- let cellAborted = false;
358
- while (i < lines.length) {
359
- const line = lines[i];
360
- if (ABORT_RE.test(line)) {
361
- cellAborted = true;
362
- aborted = true;
363
- i++;
364
- break;
365
- }
366
- if (END_RE.test(line)) {
367
- i++;
368
- break;
369
- }
370
- if (CELL_RE.test(line)) break;
371
- codeLines.push(line);
372
- i++;
373
- }
374
-
375
- if (cellAborted) break;
376
-
377
- // Strip trailing blank lines so visual spacing between cells doesn't
378
- // leak into the preceding cell's code.
379
- while (codeLines.length > 0 && codeLines[codeLines.length - 1].trim() === "") {
380
- codeLines.pop();
381
- }
382
- const code = codeLines.join("\n");
383
-
384
- const language = header.language ?? sniffEvalLanguage(code) ?? DEFAULT_LANGUAGE;
385
- const languageOrigin: EvalLanguageOrigin = header.language ? "header" : "default";
386
-
387
- cells.push({
388
- index: cells.length,
389
- title: header.title,
390
- code,
391
- language,
392
- languageOrigin,
393
- timeoutMs: header.timeoutMs ?? DEFAULT_TIMEOUT_MS,
394
- reset: header.reset,
395
- });
396
-
397
- // Skip blank separator lines between cells; an `*** Abort` here
398
- // terminates parsing while keeping previously-collected cells.
399
- while (i < lines.length && lines[i].trim() === "") i++;
400
- if (i < lines.length && ABORT_RE.test(lines[i])) {
401
- aborted = true;
402
- break;
403
- }
404
- }
405
-
406
- return aborted ? { cells, aborted: true } : { cells };
407
- }
package/src/eval/sniff.ts DELETED
@@ -1,28 +0,0 @@
1
- import type { EvalLanguage } from "./types";
2
-
3
- /**
4
- * Best-effort language sniff for cells with no explicit `language`.
5
- *
6
- * Order:
7
- * 1. Shebang on first line (`#!/usr/bin/env python`, `#!/usr/bin/env node`, etc.)
8
- * 2. Strong syntactic markers unique to one language. Bias false negatives over
9
- * false positives — anything ambiguous returns `undefined` and the caller
10
- * falls back to the default-backend rules.
11
- */
12
- export function sniffEvalLanguage(code: string): EvalLanguage | undefined {
13
- const stripped = code.replace(/^\s+/, "");
14
- if (stripped.startsWith("#!")) {
15
- const firstLine = stripped.split("\n", 1)[0]!.toLowerCase();
16
- if (/(\bpython\d?\b|\bipython\b)/.test(firstLine)) return "python";
17
- if (/(\bnode\b|\bbun\b|\bdeno\b|\bjavascript\b|\bjs\b)/.test(firstLine)) return "js";
18
- }
19
- const jsMarkers =
20
- /(^|\n)\s*(const|let|var|async\s+function|function\s*\*?\s*[\w$]*\s*\(|import\s+[^\n]+\sfrom\s|export\s+(default|const|let|function|class|async)|require\s*\(|console\.\w+\s*\(|=>|;\s*$)/m;
21
- const pyMarkers =
22
- /(^|\n)\s*(def\s+\w+\s*\(|from\s+[\w.]+\s+import|import\s+\w+(\s+as\s+\w+)?\s*$|class\s+\w+\s*[(:]|print\s*\(|elif\s+[^\n]*:|with\s+[^\n]+:\s*$|@[\w.]+\s*$)/m;
23
- const hasJs = jsMarkers.test(code);
24
- const hasPy = pyMarkers.test(code);
25
- if (hasJs && !hasPy) return "js";
26
- if (hasPy && !hasJs) return "python";
27
- return undefined;
28
- }