@juspay/neurolink 12.29.2 → 12.30.1

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.
@@ -0,0 +1,74 @@
1
+ /**
2
+ * Line-level TOML reading for the proxy's client-config writers.
3
+ *
4
+ * Grok and Codex keep their settings in TOML files the user and the CLI
5
+ * itself also edit, and the writers must find what the proxy wrote there
6
+ * without trusting its comment markers: Grok's own save drops every comment.
7
+ * No TOML parser is a dependency, and a parse-and-serialize round trip would
8
+ * reformat the user's file anyway, so this reads just enough structure to
9
+ * edit text in place: table headers, strings, keys and assignments. Every
10
+ * line keeps its own terminator, so text a writer does not own round-trips
11
+ * byte for byte.
12
+ */
13
+ import type { CliTomlSection } from "../../types/index.js";
14
+ export declare function splitTomlSections(text: string): CliTomlSection[];
15
+ /**
16
+ * A line of TOML with every string replaced by `@<n>@` and any comment
17
+ * dropped, so text inside a string can never read as a key or an assignment.
18
+ * `strings[n]` is the decoded value. `@` cannot occur outside a string in
19
+ * valid TOML.
20
+ */
21
+ export declare function maskTomlStrings(line: string): {
22
+ masked: string;
23
+ strings: string[];
24
+ };
25
+ export declare function parseMaskedAssignment(masked: string, strings: readonly string[]): {
26
+ path: string[];
27
+ value: string;
28
+ } | null;
29
+ /**
30
+ * The string a masked value starts with. Only the start counts, so a value
31
+ * with damage after it (a literal `\n` left by a bad paste) is still read.
32
+ */
33
+ export declare function maskedStringValue(value: string, strings: readonly string[]): string | undefined;
34
+ /** The pairs directly inside a one-line inline table. */
35
+ export declare function inlineTablePairs(value: string, strings: readonly string[]): {
36
+ path: string[];
37
+ value: string;
38
+ }[];
39
+ /** The dotted key a statement assigns, and its value when that is a string. */
40
+ export declare function readTomlAssignment(line: string): {
41
+ path: string[];
42
+ value: string | undefined;
43
+ } | null;
44
+ /**
45
+ * The lines of a section that are TOML rather than the inside of a
46
+ * multi-line string; `statement` marks those that start a key/value pair
47
+ * rather than continue an array or inline table.
48
+ */
49
+ export declare function tomlCodeLines(section: CliTomlSection): {
50
+ line: string;
51
+ statement: boolean;
52
+ }[];
53
+ export declare function readStringValue(section: CliTomlSection, key: string): string | undefined;
54
+ /**
55
+ * Names of the `<parent>.<name>` tables defined by dotted keys instead of a
56
+ * `[<parent>.<name>]` header: `claude-opus-4-6.base_url = ...` under
57
+ * `[model]`, or `model.claude-opus-4-6.base_url = ...` before the first
58
+ * header.
59
+ */
60
+ export declare function dottedTableIds(sections: readonly CliTomlSection[], parent: string): string[];
61
+ /**
62
+ * How many of an owned section's lines are the writer's. Comments after its
63
+ * last key belong to what follows, so a note the user wrote above their own
64
+ * next table survives. `managedLines` are the writer's own comment lines,
65
+ * which are dropped wherever they sit.
66
+ */
67
+ export declare function ownedLineCount(section: CliTomlSection, managedLines: ReadonlySet<string>): number;
68
+ /**
69
+ * A table declared twice is a TOML error, and neither Grok nor Codex will
70
+ * start on one.
71
+ * Each `[[array]]` element opens a fresh scope for everything under it, so
72
+ * `[mcp_servers.env]` under two elements is not a repeat.
73
+ */
74
+ export declare function findDuplicateTable(text: string): string | undefined;
@@ -0,0 +1,365 @@
1
+ /**
2
+ * Line-level TOML reading for the proxy's client-config writers.
3
+ *
4
+ * Grok and Codex keep their settings in TOML files the user and the CLI
5
+ * itself also edit, and the writers must find what the proxy wrote there
6
+ * without trusting its comment markers: Grok's own save drops every comment.
7
+ * No TOML parser is a dependency, and a parse-and-serialize round trip would
8
+ * reformat the user's file anyway, so this reads just enough structure to
9
+ * edit text in place: table headers, strings, keys and assignments. Every
10
+ * line keeps its own terminator, so text a writer does not own round-trips
11
+ * byte for byte.
12
+ */
13
+ function splitLinesKeepingEnds(text) {
14
+ return text.match(/[^\n]*\n|[^\n]+$/g) ?? [];
15
+ }
16
+ function readTomlKey(text) {
17
+ const bare = text.match(/^[A-Za-z0-9_-]+/);
18
+ if (bare) {
19
+ return { value: bare[0], length: bare[0].length };
20
+ }
21
+ const basic = text.match(/^"(?:[^"\\]|\\.)*"/);
22
+ if (basic) {
23
+ let value;
24
+ try {
25
+ value = JSON.parse(basic[0]);
26
+ }
27
+ catch {
28
+ value = undefined;
29
+ }
30
+ return {
31
+ value: typeof value === "string" ? value : basic[0].slice(1, -1),
32
+ length: basic[0].length,
33
+ };
34
+ }
35
+ const literal = text.match(/^'[^']*'/);
36
+ if (literal) {
37
+ return { value: literal[0].slice(1, -1), length: literal[0].length };
38
+ }
39
+ return null;
40
+ }
41
+ function parseTomlTableHeader(line) {
42
+ const text = line.trim();
43
+ if (!text.startsWith("[")) {
44
+ return null;
45
+ }
46
+ const arrayTable = text.startsWith("[[");
47
+ const path = [];
48
+ let rest = text.slice(arrayTable ? 2 : 1);
49
+ for (;;) {
50
+ rest = rest.trimStart();
51
+ const key = readTomlKey(rest);
52
+ if (key === null) {
53
+ return null;
54
+ }
55
+ path.push(key.value);
56
+ rest = rest.slice(key.length).trimStart();
57
+ if (!rest.startsWith(".")) {
58
+ break;
59
+ }
60
+ rest = rest.slice(1);
61
+ }
62
+ const close = arrayTable ? "]]" : "]";
63
+ if (!rest.startsWith(close)) {
64
+ return null;
65
+ }
66
+ const tail = rest.slice(close.length).trim();
67
+ return tail.length === 0 || tail.startsWith("#")
68
+ ? { path, arrayTable }
69
+ : null;
70
+ }
71
+ function advanceTomlScan(line, state) {
72
+ let { multiline, depth } = state;
73
+ let i = 0;
74
+ while (i < line.length) {
75
+ if (multiline !== null) {
76
+ if (multiline === '"""' && line[i] === "\\") {
77
+ i += 2;
78
+ }
79
+ else if (line.startsWith(multiline, i)) {
80
+ // Up to two quotes of content may sit against the closing delimiter.
81
+ const quote = multiline[0];
82
+ i += 3;
83
+ while (line[i] === quote) {
84
+ i += 1;
85
+ }
86
+ multiline = null;
87
+ }
88
+ else {
89
+ i += 1;
90
+ }
91
+ continue;
92
+ }
93
+ const ch = line[i];
94
+ if (ch === "#") {
95
+ break;
96
+ }
97
+ if (line.startsWith('"""', i) || line.startsWith("'''", i)) {
98
+ multiline = line.startsWith('"""', i) ? '"""' : "'''";
99
+ i += 3;
100
+ }
101
+ else if (ch === '"') {
102
+ i += 1;
103
+ while (i < line.length && line[i] !== '"') {
104
+ i += line[i] === "\\" ? 2 : 1;
105
+ }
106
+ i += 1;
107
+ }
108
+ else if (ch === "'") {
109
+ const end = line.indexOf("'", i + 1);
110
+ i = end === -1 ? line.length : end + 1;
111
+ }
112
+ else {
113
+ if (ch === "[" || ch === "{") {
114
+ depth += 1;
115
+ }
116
+ else if (ch === "]" || ch === "}") {
117
+ depth = Math.max(0, depth - 1);
118
+ }
119
+ i += 1;
120
+ }
121
+ }
122
+ return { multiline, depth };
123
+ }
124
+ export function splitTomlSections(text) {
125
+ const sections = [];
126
+ let current = { path: null, arrayTable: false, lines: [] };
127
+ let state = { multiline: null, depth: 0 };
128
+ for (const line of splitLinesKeepingEnds(text)) {
129
+ const header = state.multiline === null && state.depth === 0
130
+ ? parseTomlTableHeader(line)
131
+ : null;
132
+ if (header) {
133
+ sections.push(current);
134
+ current = { ...header, lines: [line] };
135
+ continue;
136
+ }
137
+ current.lines.push(line);
138
+ state = advanceTomlScan(line, state);
139
+ }
140
+ sections.push(current);
141
+ return sections;
142
+ }
143
+ function decodeBasicString(inner) {
144
+ try {
145
+ const value = JSON.parse(`"${inner}"`);
146
+ return typeof value === "string" ? value : inner;
147
+ }
148
+ catch {
149
+ return inner;
150
+ }
151
+ }
152
+ /**
153
+ * A line of TOML with every string replaced by `@<n>@` and any comment
154
+ * dropped, so text inside a string can never read as a key or an assignment.
155
+ * `strings[n]` is the decoded value. `@` cannot occur outside a string in
156
+ * valid TOML.
157
+ */
158
+ export function maskTomlStrings(line) {
159
+ const strings = [];
160
+ let masked = "";
161
+ let i = 0;
162
+ while (i < line.length) {
163
+ const ch = line[i];
164
+ if (ch === "#") {
165
+ break;
166
+ }
167
+ const triple = line.startsWith('"""', i)
168
+ ? '"""'
169
+ : line.startsWith("'''", i)
170
+ ? "'''"
171
+ : null;
172
+ if (triple !== null || ch === '"' || ch === "'") {
173
+ const delimiter = triple ?? ch;
174
+ let end = i + delimiter.length;
175
+ while (end < line.length && !line.startsWith(delimiter, end)) {
176
+ end += delimiter.startsWith('"') && line[end] === "\\" ? 2 : 1;
177
+ }
178
+ // Up to two quotes of content may sit against a triple closer.
179
+ for (let extra = 0; triple !== null &&
180
+ extra < 2 &&
181
+ line[end + delimiter.length] === delimiter[0]; extra += 1) {
182
+ end += 1;
183
+ }
184
+ const inner = line.slice(i + delimiter.length, Math.min(end, line.length));
185
+ strings.push(delimiter.startsWith("'") ? inner : decodeBasicString(inner));
186
+ masked += `@${strings.length - 1}@`;
187
+ i = end + delimiter.length;
188
+ continue;
189
+ }
190
+ masked += ch;
191
+ i += 1;
192
+ }
193
+ return { masked, strings };
194
+ }
195
+ const TOML_KEY_PART = String.raw `(?:[A-Za-z0-9_-]+|@\d+@)`;
196
+ const TOML_ASSIGNMENT = new RegExp(String.raw `^\s*(${TOML_KEY_PART}(?:\s*\.\s*${TOML_KEY_PART})*)\s*=\s*(.*?)\s*$`);
197
+ function unmaskKeyPart(part, strings) {
198
+ const token = part.match(/^@(\d+)@$/);
199
+ return token ? strings[Number(token[1])] : part;
200
+ }
201
+ export function parseMaskedAssignment(masked, strings) {
202
+ const match = masked.match(TOML_ASSIGNMENT);
203
+ return match
204
+ ? {
205
+ path: match[1]
206
+ .split(".")
207
+ .map((part) => unmaskKeyPart(part.trim(), strings)),
208
+ value: match[2],
209
+ }
210
+ : null;
211
+ }
212
+ /**
213
+ * The string a masked value starts with. Only the start counts, so a value
214
+ * with damage after it (a literal `\n` left by a bad paste) is still read.
215
+ */
216
+ export function maskedStringValue(value, strings) {
217
+ const token = value.match(/^@(\d+)@/);
218
+ return token ? strings[Number(token[1])] : undefined;
219
+ }
220
+ /** The pairs directly inside a one-line inline table. */
221
+ export function inlineTablePairs(value, strings) {
222
+ if (!value.startsWith("{") || !value.endsWith("}")) {
223
+ return [];
224
+ }
225
+ const parts = [];
226
+ let depth = 0;
227
+ let start = 1;
228
+ for (let i = 1; i < value.length - 1; i += 1) {
229
+ if (value[i] === "{" || value[i] === "[") {
230
+ depth += 1;
231
+ }
232
+ else if (value[i] === "}" || value[i] === "]") {
233
+ depth -= 1;
234
+ }
235
+ else if (value[i] === "," && depth === 0) {
236
+ parts.push(value.slice(start, i));
237
+ start = i + 1;
238
+ }
239
+ }
240
+ parts.push(value.slice(start, value.length - 1));
241
+ return parts.flatMap((part) => {
242
+ const pair = parseMaskedAssignment(part, strings);
243
+ return pair ? [pair] : [];
244
+ });
245
+ }
246
+ /** The dotted key a statement assigns, and its value when that is a string. */
247
+ export function readTomlAssignment(line) {
248
+ const { masked, strings } = maskTomlStrings(line);
249
+ const assignment = parseMaskedAssignment(masked, strings);
250
+ return assignment
251
+ ? {
252
+ path: assignment.path,
253
+ value: maskedStringValue(assignment.value, strings),
254
+ }
255
+ : null;
256
+ }
257
+ function isTomlContentLine(line) {
258
+ const text = line.trim();
259
+ return text.length > 0 && !text.startsWith("#");
260
+ }
261
+ /**
262
+ * The lines of a section that are TOML rather than the inside of a
263
+ * multi-line string; `statement` marks those that start a key/value pair
264
+ * rather than continue an array or inline table.
265
+ */
266
+ export function tomlCodeLines(section) {
267
+ const code = [];
268
+ let state = { multiline: null, depth: 0 };
269
+ section.lines.forEach((line, index) => {
270
+ const isHeader = index === 0 && section.path !== null;
271
+ if (!isHeader && state.multiline === null) {
272
+ code.push({ line, statement: state.depth === 0 });
273
+ }
274
+ if (!isHeader) {
275
+ state = advanceTomlScan(line, state);
276
+ }
277
+ });
278
+ return code;
279
+ }
280
+ export function readStringValue(section, key) {
281
+ for (const { line, statement } of tomlCodeLines(section)) {
282
+ const assignment = statement ? readTomlAssignment(line) : null;
283
+ if (assignment?.path.length === 1 && assignment.path[0] === key) {
284
+ return assignment.value;
285
+ }
286
+ }
287
+ return undefined;
288
+ }
289
+ /**
290
+ * Names of the `<parent>.<name>` tables defined by dotted keys instead of a
291
+ * `[<parent>.<name>]` header: `claude-opus-4-6.base_url = ...` under
292
+ * `[model]`, or `model.claude-opus-4-6.base_url = ...` before the first
293
+ * header.
294
+ */
295
+ export function dottedTableIds(sections, parent) {
296
+ return sections.flatMap((section) => {
297
+ const underParent = !section.arrayTable &&
298
+ section.path?.length === 1 &&
299
+ section.path[0] === parent;
300
+ if (!underParent && section.path !== null) {
301
+ return [];
302
+ }
303
+ return tomlCodeLines(section).flatMap(({ line, statement }) => {
304
+ const path = statement ? readTomlAssignment(line)?.path : undefined;
305
+ if (!path) {
306
+ return [];
307
+ }
308
+ if (underParent) {
309
+ return [path[0]];
310
+ }
311
+ return path.length >= 2 && path[0] === parent ? [path[1]] : [];
312
+ });
313
+ });
314
+ }
315
+ /**
316
+ * How many of an owned section's lines are the writer's. Comments after its
317
+ * last key belong to what follows, so a note the user wrote above their own
318
+ * next table survives. `managedLines` are the writer's own comment lines,
319
+ * which are dropped wherever they sit.
320
+ */
321
+ export function ownedLineCount(section, managedLines) {
322
+ const lastKey = section.lines.findLastIndex(isTomlContentLine);
323
+ const firstNote = section.lines.findIndex((line, index) => index > lastKey &&
324
+ line.trim().length > 0 &&
325
+ !managedLines.has(line.trim()));
326
+ return firstNote === -1 ? section.lines.length : firstNote;
327
+ }
328
+ /**
329
+ * A table declared twice is a TOML error, and neither Grok nor Codex will
330
+ * start on one.
331
+ * Each `[[array]]` element opens a fresh scope for everything under it, so
332
+ * `[mcp_servers.env]` under two elements is not a repeat.
333
+ */
334
+ export function findDuplicateTable(text) {
335
+ const tables = new Map();
336
+ const arrays = new Map();
337
+ const isUnder = (candidate, prefix) => candidate.length > prefix.length &&
338
+ prefix.every((part, index) => candidate[index] === part);
339
+ for (const { path, arrayTable } of splitTomlSections(text)) {
340
+ if (path === null) {
341
+ continue;
342
+ }
343
+ const key = JSON.stringify(path);
344
+ if (arrayTable) {
345
+ if (tables.has(key)) {
346
+ return path.join(".");
347
+ }
348
+ for (const scope of [tables, arrays]) {
349
+ for (const [seenKey, seenPath] of scope) {
350
+ if (isUnder(seenPath, path)) {
351
+ scope.delete(seenKey);
352
+ }
353
+ }
354
+ }
355
+ arrays.set(key, path);
356
+ continue;
357
+ }
358
+ if (tables.has(key) || arrays.has(key)) {
359
+ return path.join(".");
360
+ }
361
+ tables.set(key, path);
362
+ }
363
+ return undefined;
364
+ }
365
+ //# sourceMappingURL=tomlSections.js.map
@@ -936,6 +936,7 @@ export class BaseProvider {
936
936
  context: options.context,
937
937
  csvOptions: options.csvOptions,
938
938
  pdfOptions: options.pdfOptions,
939
+ videoOptions: options.videoOptions,
939
940
  // Forward abort, tool filtering, and timeout options to prevent
940
941
  // silent bypass when falling back from real streaming to fake streaming
941
942
  abortSignal: options.abortSignal,
@@ -105,6 +105,10 @@ export class MessageBuilder {
105
105
  csvOptions: options.csvOptions,
106
106
  pdfOptions: options.pdfOptions,
107
107
  imageOptions: options.imageOptions,
108
+ // Same reason as csvOptions: the keyframe budget and the
109
+ // transcription flag are read downstream by the detector, and
110
+ // a reconstruction that omits them silently restores defaults.
111
+ videoOptions: options.videoOptions,
108
112
  provider: options.provider,
109
113
  model: options.model,
110
114
  temperature: options.temperature,
@@ -236,6 +240,10 @@ export class MessageBuilder {
236
240
  csvOptions: options.csvOptions,
237
241
  pdfOptions: options.pdfOptions,
238
242
  imageOptions: options.imageOptions,
243
+ // Same reason as csvOptions: the keyframe budget and the
244
+ // transcription flag are read downstream by the detector, and
245
+ // a reconstruction that omits them silently restores defaults.
246
+ videoOptions: options.videoOptions,
239
247
  provider: options.provider,
240
248
  model: options.model,
241
249
  temperature: options.temperature,
@@ -142,6 +142,47 @@ export declare class VideoProcessor extends BaseFileProcessor<ProcessedVideo> {
142
142
  * falling back to the default when absent or non-numeric (#478).
143
143
  */
144
144
  private static resolveFrameQuality;
145
+ /**
146
+ * Extract the clip's audio track and transcribe the speech in it (#433).
147
+ *
148
+ * ## Why this exists at all, now that Gemini hears video directly
149
+ *
150
+ * It does not help Gemini — a native provider receives the clip and its
151
+ * audio together. It is for everyone else. A provider on the frame path
152
+ * gets stills and nothing else, so a recorded standup, a support call or a
153
+ * narrated demo arrives with its entire spoken content missing, and
154
+ * "summarise this meeting" is answered from four screenshots.
155
+ *
156
+ * ## Why it is not a stub
157
+ *
158
+ * The issue asked for one, on the grounds that no transcription backend
159
+ * existed yet. One does: `AudioProcessor` has shipped Whisper transcription
160
+ * for standalone audio files for some time. A method that logged "not yet
161
+ * implemented" and returned undefined would be dead code sitting next to a
162
+ * working implementation of the same thing, and `--transcribe-audio` would
163
+ * still do nothing.
164
+ *
165
+ * The Whisper call is reproduced here rather than shared with
166
+ * `AudioProcessor`: extracting it into a common module is the better
167
+ * long-term shape, but that file is being edited concurrently, and a
168
+ * merge conflict in the audio pipeline is a worse outcome than fifty
169
+ * duplicated lines. The duplication is worth removing once both land.
170
+ *
171
+ * ## Failure behaviour
172
+ *
173
+ * Best-effort throughout, and never throws: a clip with no audio track, a
174
+ * machine without ffmpeg, a missing key, an oversized track or a failed
175
+ * request all return a *reason* and leave the rest of the pipeline intact.
176
+ * Each reason is distinct, because from the outside every one of them
177
+ * looks the same — no transcript — while the remedies differ completely.
178
+ *
179
+ * @param videoPath - Temp path to the video, already written by the caller
180
+ * @param tempDir - Caller-owned temp directory; removed in its `finally`
181
+ * @param metadata - Probed metadata, consulted for an audio track
182
+ * @param filename - Display name, for logs
183
+ * @param options - Caller settings; transcription runs only when asked
184
+ */
185
+ private extractAndTranscribeAudio;
145
186
  /**
146
187
  * Extract keyframes from a video at calculated intervals.
147
188
  *
@@ -166,18 +207,28 @@ export declare class VideoProcessor extends BaseFileProcessor<ProcessedVideo> {
166
207
  * @param tempDir - Temp directory for frame output
167
208
  * @param durationSec - Video duration in seconds
168
209
  * @param options - Caller frame budget / encoder settings
169
- * @returns Array of encoded frame buffers (JPEG unless png was requested)
210
+ * @returns Each kept frame with the second it was sampled at, ascending
170
211
  */
171
212
  private extractKeyframes;
172
213
  /**
173
214
  * Run ffmpeg to extract frames at specified timestamps.
174
215
  *
175
- * Uses the `-vf select` filter to pick frames at exact timestamps,
176
- * which is more efficient than seeking for each frame individually.
216
+ * Uses the `-vf select` filter to pick frames at least `intervalSec` apart,
217
+ * which is more efficient than seeking for each frame individually. This is
218
+ * a minimum-gap filter, not a seek to an exact time: on a source whose real
219
+ * frame spacing exceeds the requested interval, the frame actually selected
220
+ * can land well after the ideal schedule. A `showinfo` stage between
221
+ * `select` and `scale` reports each selected frame's true `pts_time` to
222
+ * stderr so the caller can label frames with when they were really
223
+ * sampled, not the idealized request.
177
224
  *
178
225
  * @param videoPath - Path to the video file
179
226
  * @param outputDir - Directory to write frame files
180
227
  * @param timestamps - Array of timestamps in seconds
228
+ * @param intervalSec - Minimum gap enforced between selected frames
229
+ * @returns The real `pts_time` (seconds) ffmpeg reported for each selected
230
+ * frame, in output-file order; may be shorter than `timestamps` if fewer
231
+ * real frames were available to select
181
232
  */
182
233
  private runFfmpegFrameExtraction;
183
234
  /**
@@ -217,9 +268,10 @@ export declare class VideoProcessor extends BaseFileProcessor<ProcessedVideo> {
217
268
  * - Subtitle text (if available)
218
269
  *
219
270
  * @param metadata - Extracted video metadata
220
- * @param frameCount - Number of keyframes extracted
271
+ * @param keyframeTimestampsSec - The real second each kept keyframe was sampled at
221
272
  * @param subtitleText - Extracted subtitle text (if any)
222
273
  * @param filename - Original filename
274
+ * @param transcript - Machine transcription of the spoken audio (if requested and available)
223
275
  * @returns Formatted text content for the LLM
224
276
  */
225
277
  private buildTextContent;