modelmix 5.0.2 → 5.0.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +109 -7
- package/RLM_PLUGIN_SPEC.md +465 -0
- package/demo/gemini.js +3 -4
- package/demo/short.js +1 -1
- package/effort.js +2 -0
- package/index.d.ts +61 -1
- package/index.js +333 -45
- package/package.json +7 -4
- package/plugins/rlm/index.d.ts +194 -0
- package/plugins/rlm/index.js +25 -0
- package/plugins/rlm/lib/budget.js +153 -0
- package/plugins/rlm/lib/isolated-vm-sandbox.js +90 -0
- package/plugins/rlm/lib/markdown.js +156 -0
- package/plugins/rlm/lib/planner-prompt.js +137 -0
- package/plugins/rlm/lib/plugin.js +203 -0
- package/plugins/rlm/lib/runtime.js +146 -0
- package/plugins/rlm/lib/variable-descriptors.js +228 -0
- package/plugins/rlm/lib/worker-catalog.js +70 -0
- package/plugins/rlm/package.json +32 -0
- package/plugins/rlm/prompts/partials/processing-rules.md +8 -0
- package/plugins/rlm/prompts/planner.md +53 -0
- package/plugins/rlm/test/budget.test.js +86 -0
- package/plugins/rlm/test/fixtures/book.md +24 -0
- package/plugins/rlm/test/isolated-vm-sandbox.test.js +114 -0
- package/plugins/rlm/test/markdown.test.js +64 -0
- package/plugins/rlm/test/planner-template.test.js +140 -0
- package/plugins/rlm/test/plugin-contract.test.js +182 -0
- package/plugins/rlm/test/rlm-e2e.test.js +338 -0
- package/plugins/rlm/test/variable-descriptors.test.js +170 -0
- package/plugins/rlm/test/worker-catalog.test.js +104 -0
- package/pnpm-workspace.yaml +6 -0
- package/skills/modelmix/SKILL.md +22 -3
- package/test/effort.test.js +14 -1
- package/test/live.mcp.js +6 -6
- package/test/live.test.js +2 -2
- package/test/plugins.test.js +356 -0
- package/test/tokens.test.js +37 -5
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
export interface NumericStats {
|
|
2
|
+
min: number;
|
|
3
|
+
max: number;
|
|
4
|
+
average: number;
|
|
5
|
+
total: number;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export interface StringSizeStats {
|
|
9
|
+
characters: NumericStats;
|
|
10
|
+
utf8Bytes: NumericStats;
|
|
11
|
+
lines: NumericStats;
|
|
12
|
+
paragraphs: NumericStats;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface VariableDescriptor {
|
|
16
|
+
path: string;
|
|
17
|
+
type: string;
|
|
18
|
+
estimatedBytes: number;
|
|
19
|
+
characters?: number;
|
|
20
|
+
utf16CodeUnits?: number;
|
|
21
|
+
utf8Bytes?: number;
|
|
22
|
+
lines?: number;
|
|
23
|
+
paragraphs?: number;
|
|
24
|
+
items?: number;
|
|
25
|
+
itemSize?: NumericStats;
|
|
26
|
+
itemShape?: Record<string, unknown>;
|
|
27
|
+
properties?: number;
|
|
28
|
+
children?: Record<string, VariableDescriptor>;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export interface VariableManifest {
|
|
32
|
+
sizeBasis: 'serialized-json-utf8';
|
|
33
|
+
variables: number;
|
|
34
|
+
estimatedBytes: number;
|
|
35
|
+
descriptors: Record<string, VariableDescriptor>;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export interface PlannerLimits {
|
|
39
|
+
maxQueryBytes: number;
|
|
40
|
+
sandboxMemoryBytes: number;
|
|
41
|
+
maxConcurrentQueries: number;
|
|
42
|
+
maxCalls: number;
|
|
43
|
+
maxOutputBytes: number;
|
|
44
|
+
maxGeneratedTokens: number;
|
|
45
|
+
maxWallTimeMs: number;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export interface MarkdownListItem {
|
|
49
|
+
text: string;
|
|
50
|
+
source: string;
|
|
51
|
+
lists: MarkdownList[];
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export interface MarkdownList {
|
|
55
|
+
ordered: boolean;
|
|
56
|
+
start: number | null;
|
|
57
|
+
items: MarkdownListItem[];
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export interface MarkdownSection {
|
|
61
|
+
id: string;
|
|
62
|
+
path: string[];
|
|
63
|
+
title: string;
|
|
64
|
+
depth: number;
|
|
65
|
+
order: number;
|
|
66
|
+
heading: string;
|
|
67
|
+
body: string;
|
|
68
|
+
lists: MarkdownList[];
|
|
69
|
+
children: MarkdownSection[];
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export interface MarkdownDocument {
|
|
73
|
+
format: 'markdown';
|
|
74
|
+
preamble: {
|
|
75
|
+
source: string;
|
|
76
|
+
lists: MarkdownList[];
|
|
77
|
+
};
|
|
78
|
+
sections: MarkdownSection[];
|
|
79
|
+
stats: {
|
|
80
|
+
characters: number;
|
|
81
|
+
utf16CodeUnits: number;
|
|
82
|
+
utf8Bytes: number;
|
|
83
|
+
lines: number;
|
|
84
|
+
sectionCount: number;
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export declare function parseMarkdownDocument(source: string): Promise<MarkdownDocument>;
|
|
89
|
+
|
|
90
|
+
export declare function reconstructMarkdownDocument(document: MarkdownDocument): string;
|
|
91
|
+
|
|
92
|
+
export declare function describeVariables(
|
|
93
|
+
variables: Record<string, unknown>
|
|
94
|
+
): VariableManifest;
|
|
95
|
+
|
|
96
|
+
export interface PlannerTemplateData {
|
|
97
|
+
variableManifest: string;
|
|
98
|
+
processingLimits: string;
|
|
99
|
+
planningHints: string;
|
|
100
|
+
workerManifest: string;
|
|
101
|
+
outputRequirements: string;
|
|
102
|
+
maxQueryBytes: number;
|
|
103
|
+
maxConcurrentQueries: number;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export interface PlannerInvocation {
|
|
107
|
+
systemFile: string;
|
|
108
|
+
assign: PlannerTemplateData;
|
|
109
|
+
messages: Array<{
|
|
110
|
+
role: 'user';
|
|
111
|
+
content: Array<{ type: 'text'; text: string }>;
|
|
112
|
+
}>;
|
|
113
|
+
plugins: { exclude: ['rlm'] };
|
|
114
|
+
history: false;
|
|
115
|
+
outputMode: 'raw';
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
export declare function plannerTemplateData(input: {
|
|
119
|
+
variables: Record<string, unknown>;
|
|
120
|
+
limits: PlannerLimits;
|
|
121
|
+
workerManifest: Record<string, unknown>;
|
|
122
|
+
outputMode?: 'message' | 'json' | 'block' | 'raw';
|
|
123
|
+
outputSchema?: Record<string, unknown> | null;
|
|
124
|
+
}): PlannerTemplateData;
|
|
125
|
+
|
|
126
|
+
export declare function createPlannerInvocation(input: {
|
|
127
|
+
task: string;
|
|
128
|
+
variables: Record<string, unknown>;
|
|
129
|
+
limits: PlannerLimits;
|
|
130
|
+
workerManifest: Record<string, unknown>;
|
|
131
|
+
outputMode?: 'message' | 'json' | 'block' | 'raw';
|
|
132
|
+
outputSchema?: Record<string, unknown> | null;
|
|
133
|
+
}): PlannerInvocation;
|
|
134
|
+
|
|
135
|
+
export interface RlmWorkerMetadata {
|
|
136
|
+
intelligence: number;
|
|
137
|
+
cost: number;
|
|
138
|
+
speed: number;
|
|
139
|
+
description: string;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
export type RlmWorker = RlmWorkerMetadata & (
|
|
143
|
+
| { model: object; useParent?: never }
|
|
144
|
+
| { model?: never; useParent: true }
|
|
145
|
+
);
|
|
146
|
+
|
|
147
|
+
export interface RlmSandbox {
|
|
148
|
+
execute(input: {
|
|
149
|
+
code: string;
|
|
150
|
+
variables: Record<string, unknown>;
|
|
151
|
+
query: (input: {
|
|
152
|
+
worker: string;
|
|
153
|
+
system: string;
|
|
154
|
+
message: string;
|
|
155
|
+
}) => Promise<string>;
|
|
156
|
+
limits: PlannerLimits;
|
|
157
|
+
execution: {
|
|
158
|
+
executionId: string;
|
|
159
|
+
parentExecutionId: string | null;
|
|
160
|
+
depth: number;
|
|
161
|
+
};
|
|
162
|
+
timeoutMs: number;
|
|
163
|
+
}): Promise<unknown>;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
export declare function createIsolatedVmSandbox(): RlmSandbox;
|
|
167
|
+
|
|
168
|
+
export interface RlmDocumentInput {
|
|
169
|
+
format: 'markdown';
|
|
170
|
+
content: string;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
export interface RlmOptions {
|
|
174
|
+
maxDepth: number;
|
|
175
|
+
variables?: Record<string, unknown>;
|
|
176
|
+
documents?: Record<string, RlmDocumentInput>;
|
|
177
|
+
workers: Record<string, RlmWorker>;
|
|
178
|
+
limits: PlannerLimits;
|
|
179
|
+
sandbox?: RlmSandbox;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
export declare class RlmLimitError extends Error {
|
|
183
|
+
limit: string;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
export declare function createWorkerCatalog(workers: Record<string, RlmWorker>): {
|
|
187
|
+
get(name: string): object | undefined;
|
|
188
|
+
manifest: Record<string, RlmWorkerMetadata>;
|
|
189
|
+
};
|
|
190
|
+
|
|
191
|
+
export declare function rlm(options: RlmOptions): {
|
|
192
|
+
name: 'rlm';
|
|
193
|
+
execute(context: unknown): Promise<Record<string, unknown>>;
|
|
194
|
+
};
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
const { describeVariables } = require('./lib/variable-descriptors');
|
|
2
|
+
const {
|
|
3
|
+
createPlannerInvocation,
|
|
4
|
+
plannerTemplateData
|
|
5
|
+
} = require('./lib/planner-prompt');
|
|
6
|
+
const { rlm } = require('./lib/plugin');
|
|
7
|
+
const { RlmLimitError } = require('./lib/budget');
|
|
8
|
+
const { createIsolatedVmSandbox } = require('./lib/isolated-vm-sandbox');
|
|
9
|
+
const {
|
|
10
|
+
parseMarkdownDocument,
|
|
11
|
+
reconstructMarkdownDocument
|
|
12
|
+
} = require('./lib/markdown');
|
|
13
|
+
const { createWorkerCatalog } = require('./lib/worker-catalog');
|
|
14
|
+
|
|
15
|
+
module.exports = {
|
|
16
|
+
RlmLimitError,
|
|
17
|
+
createIsolatedVmSandbox,
|
|
18
|
+
createWorkerCatalog,
|
|
19
|
+
describeVariables,
|
|
20
|
+
createPlannerInvocation,
|
|
21
|
+
plannerTemplateData,
|
|
22
|
+
parseMarkdownDocument,
|
|
23
|
+
reconstructMarkdownDocument,
|
|
24
|
+
rlm
|
|
25
|
+
};
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
class RlmLimitError extends Error {
|
|
2
|
+
constructor(limit, message) {
|
|
3
|
+
super(message);
|
|
4
|
+
this.name = 'RlmLimitError';
|
|
5
|
+
this.limit = limit;
|
|
6
|
+
}
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
function positiveInteger(value, name) {
|
|
10
|
+
if (!Number.isInteger(value) || value <= 0) {
|
|
11
|
+
throw new TypeError(`${name} must be a positive integer.`);
|
|
12
|
+
}
|
|
13
|
+
return value;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function validateRuntimeLimits(limits) {
|
|
17
|
+
if (!limits || typeof limits !== 'object' || Array.isArray(limits)) {
|
|
18
|
+
throw new TypeError('limits must be a plain object.');
|
|
19
|
+
}
|
|
20
|
+
return Object.freeze({
|
|
21
|
+
maxQueryBytes: positiveInteger(limits.maxQueryBytes, 'limits.maxQueryBytes'),
|
|
22
|
+
sandboxMemoryBytes: positiveInteger(limits.sandboxMemoryBytes, 'limits.sandboxMemoryBytes'),
|
|
23
|
+
maxConcurrentQueries: positiveInteger(
|
|
24
|
+
limits.maxConcurrentQueries,
|
|
25
|
+
'limits.maxConcurrentQueries'
|
|
26
|
+
),
|
|
27
|
+
maxCalls: positiveInteger(limits.maxCalls, 'limits.maxCalls'),
|
|
28
|
+
maxOutputBytes: positiveInteger(limits.maxOutputBytes, 'limits.maxOutputBytes'),
|
|
29
|
+
maxGeneratedTokens: positiveInteger(
|
|
30
|
+
limits.maxGeneratedTokens,
|
|
31
|
+
'limits.maxGeneratedTokens'
|
|
32
|
+
),
|
|
33
|
+
maxWallTimeMs: positiveInteger(limits.maxWallTimeMs, 'limits.maxWallTimeMs')
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function createRuntimeBudget(limits, now = Date.now) {
|
|
38
|
+
const validated = validateRuntimeLimits(limits);
|
|
39
|
+
const startedAt = now();
|
|
40
|
+
const queue = [];
|
|
41
|
+
let calls = 0;
|
|
42
|
+
let active = 0;
|
|
43
|
+
let peakConcurrency = 0;
|
|
44
|
+
let outputBytes = 0;
|
|
45
|
+
let generatedTokens = 0;
|
|
46
|
+
|
|
47
|
+
const checkTime = () => {
|
|
48
|
+
if (now() - startedAt > validated.maxWallTimeMs) {
|
|
49
|
+
throw new RlmLimitError('maxWallTimeMs', 'RLM wall-time limit exceeded.');
|
|
50
|
+
}
|
|
51
|
+
};
|
|
52
|
+
const acquire = () => new Promise(resolve => {
|
|
53
|
+
if (active < validated.maxConcurrentQueries) {
|
|
54
|
+
active += 1;
|
|
55
|
+
peakConcurrency = Math.max(peakConcurrency, active);
|
|
56
|
+
resolve();
|
|
57
|
+
} else {
|
|
58
|
+
queue.push(resolve);
|
|
59
|
+
}
|
|
60
|
+
});
|
|
61
|
+
const release = () => {
|
|
62
|
+
const next = queue.shift();
|
|
63
|
+
if (next) {
|
|
64
|
+
next();
|
|
65
|
+
} else {
|
|
66
|
+
active -= 1;
|
|
67
|
+
}
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
const accountResult = result => {
|
|
71
|
+
const message = result?.message ?? '';
|
|
72
|
+
const resultBytes = Buffer.byteLength(
|
|
73
|
+
typeof message === 'string' ? message : JSON.stringify(message),
|
|
74
|
+
'utf8'
|
|
75
|
+
);
|
|
76
|
+
outputBytes += resultBytes;
|
|
77
|
+
generatedTokens += Number.isFinite(result?.tokens?.output)
|
|
78
|
+
? Math.max(0, result.tokens.output)
|
|
79
|
+
: 0;
|
|
80
|
+
if (outputBytes > validated.maxOutputBytes) {
|
|
81
|
+
throw new RlmLimitError('maxOutputBytes', 'RLM output byte limit exceeded.');
|
|
82
|
+
}
|
|
83
|
+
if (generatedTokens > validated.maxGeneratedTokens) {
|
|
84
|
+
throw new RlmLimitError(
|
|
85
|
+
'maxGeneratedTokens',
|
|
86
|
+
'RLM generated-token limit exceeded.'
|
|
87
|
+
);
|
|
88
|
+
}
|
|
89
|
+
};
|
|
90
|
+
const runCall = async ({ payloadBytes, enforcePayload }, operation) => {
|
|
91
|
+
checkTime();
|
|
92
|
+
if (enforcePayload && payloadBytes > validated.maxQueryBytes) {
|
|
93
|
+
throw new RlmLimitError(
|
|
94
|
+
'maxQueryBytes',
|
|
95
|
+
`RLM query payload is ${payloadBytes} bytes; limit is ${validated.maxQueryBytes}.`
|
|
96
|
+
);
|
|
97
|
+
}
|
|
98
|
+
if (calls >= validated.maxCalls) {
|
|
99
|
+
throw new RlmLimitError('maxCalls', 'RLM call limit exceeded.');
|
|
100
|
+
}
|
|
101
|
+
calls += 1;
|
|
102
|
+
await acquire();
|
|
103
|
+
try {
|
|
104
|
+
checkTime();
|
|
105
|
+
const result = await operation();
|
|
106
|
+
checkTime();
|
|
107
|
+
accountResult(result);
|
|
108
|
+
return result;
|
|
109
|
+
} finally {
|
|
110
|
+
release();
|
|
111
|
+
}
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
return {
|
|
115
|
+
limits: validated,
|
|
116
|
+
assertQueryPayload(payloadBytes) {
|
|
117
|
+
checkTime();
|
|
118
|
+
if (payloadBytes > validated.maxQueryBytes) {
|
|
119
|
+
throw new RlmLimitError(
|
|
120
|
+
'maxQueryBytes',
|
|
121
|
+
`RLM query payload is ${payloadBytes} bytes; limit is ${validated.maxQueryBytes}.`
|
|
122
|
+
);
|
|
123
|
+
}
|
|
124
|
+
},
|
|
125
|
+
async runQuery({ payloadBytes }, operation) {
|
|
126
|
+
return runCall({ payloadBytes, enforcePayload: true }, operation);
|
|
127
|
+
},
|
|
128
|
+
async runPlanner(operation) {
|
|
129
|
+
return runCall({ payloadBytes: 0, enforcePayload: false }, operation);
|
|
130
|
+
},
|
|
131
|
+
accountFinalOutput(value) {
|
|
132
|
+
const message = typeof value === 'string' ? value : JSON.stringify(value);
|
|
133
|
+
accountResult({ message });
|
|
134
|
+
checkTime();
|
|
135
|
+
},
|
|
136
|
+
snapshot() {
|
|
137
|
+
return {
|
|
138
|
+
calls,
|
|
139
|
+
active,
|
|
140
|
+
peakConcurrency,
|
|
141
|
+
outputBytes,
|
|
142
|
+
generatedTokens,
|
|
143
|
+
elapsedMs: now() - startedAt
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
module.exports = {
|
|
150
|
+
RlmLimitError,
|
|
151
|
+
createRuntimeBudget,
|
|
152
|
+
validateRuntimeLimits
|
|
153
|
+
};
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
const ivm = require('isolated-vm');
|
|
2
|
+
const { RlmLimitError } = require('./budget');
|
|
3
|
+
|
|
4
|
+
const MINIMUM_ISOLATE_MEMORY_BYTES = 8 * 1024 * 1024;
|
|
5
|
+
|
|
6
|
+
function timeoutError() {
|
|
7
|
+
return new RlmLimitError('maxWallTimeMs', 'RLM wall-time limit exceeded.');
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
function memoryError(error) {
|
|
11
|
+
return /memory|heap|array buffer allocation/i.test(error.message);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function executionTimeoutError(error) {
|
|
15
|
+
return /timed out|execution terminated/i.test(error.message);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function sandboxSource(code) {
|
|
19
|
+
return `'use strict';\n${code}`;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function createIsolatedVmSandbox() {
|
|
23
|
+
return {
|
|
24
|
+
async execute({ code, variables, query, limits, timeoutMs }) {
|
|
25
|
+
if (limits.sandboxMemoryBytes < MINIMUM_ISOLATE_MEMORY_BYTES) {
|
|
26
|
+
throw new TypeError(
|
|
27
|
+
`limits.sandboxMemoryBytes must be at least ${MINIMUM_ISOLATE_MEMORY_BYTES}.`
|
|
28
|
+
);
|
|
29
|
+
}
|
|
30
|
+
const isolate = new ivm.Isolate({
|
|
31
|
+
memoryLimit: Math.ceil(limits.sandboxMemoryBytes / (1024 * 1024))
|
|
32
|
+
});
|
|
33
|
+
let timedOut = false;
|
|
34
|
+
const timeout = setTimeout(() => {
|
|
35
|
+
timedOut = true;
|
|
36
|
+
if (!isolate.isDisposed) isolate.dispose();
|
|
37
|
+
}, timeoutMs);
|
|
38
|
+
|
|
39
|
+
try {
|
|
40
|
+
const context = await isolate.createContext();
|
|
41
|
+
const jail = context.global;
|
|
42
|
+
await jail.set(
|
|
43
|
+
'variables',
|
|
44
|
+
new ivm.ExternalCopy(variables).copyInto()
|
|
45
|
+
);
|
|
46
|
+
await jail.set('__queryReference', new ivm.Reference(query));
|
|
47
|
+
await context.eval(`
|
|
48
|
+
(() => {
|
|
49
|
+
const queryReference = globalThis.__queryReference;
|
|
50
|
+
Object.defineProperty(globalThis, 'query', {
|
|
51
|
+
configurable: false,
|
|
52
|
+
enumerable: true,
|
|
53
|
+
writable: false,
|
|
54
|
+
value(input) {
|
|
55
|
+
return queryReference.apply(undefined, [input], {
|
|
56
|
+
arguments: { copy: true },
|
|
57
|
+
result: { promise: true, copy: true }
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
});
|
|
61
|
+
delete globalThis.__queryReference;
|
|
62
|
+
})();
|
|
63
|
+
`, { timeout: timeoutMs });
|
|
64
|
+
return await context.eval(sandboxSource(code), {
|
|
65
|
+
copy: true,
|
|
66
|
+
filename: 'rlm-planner.js',
|
|
67
|
+
promise: true,
|
|
68
|
+
timeout: timeoutMs
|
|
69
|
+
});
|
|
70
|
+
} catch (error) {
|
|
71
|
+
if (memoryError(error)) {
|
|
72
|
+
throw new RlmLimitError(
|
|
73
|
+
'sandboxMemoryBytes',
|
|
74
|
+
'RLM sandbox memory limit exceeded.'
|
|
75
|
+
);
|
|
76
|
+
}
|
|
77
|
+
if (timedOut || executionTimeoutError(error)) throw timeoutError();
|
|
78
|
+
throw error;
|
|
79
|
+
} finally {
|
|
80
|
+
clearTimeout(timeout);
|
|
81
|
+
if (!isolate.isDisposed) isolate.dispose();
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
module.exports = {
|
|
88
|
+
MINIMUM_ISOLATE_MEMORY_BYTES,
|
|
89
|
+
createIsolatedVmSandbox
|
|
90
|
+
};
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
let parserPromise;
|
|
2
|
+
|
|
3
|
+
function loadParser() {
|
|
4
|
+
if (!parserPromise) {
|
|
5
|
+
parserPromise = import('mdast-util-from-markdown')
|
|
6
|
+
.then(module => module.fromMarkdown);
|
|
7
|
+
}
|
|
8
|
+
return parserPromise;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function nodeText(node) {
|
|
12
|
+
if (typeof node.value === 'string') return node.value;
|
|
13
|
+
if (!Array.isArray(node.children)) return '';
|
|
14
|
+
return node.children.map(nodeText).join('');
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function sourceSlice(source, node) {
|
|
18
|
+
const start = node.position?.start?.offset;
|
|
19
|
+
const end = node.position?.end?.offset;
|
|
20
|
+
if (!Number.isInteger(start) || !Number.isInteger(end)) return '';
|
|
21
|
+
return source.slice(start, end);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function listValue(node, source) {
|
|
25
|
+
return {
|
|
26
|
+
ordered: node.ordered,
|
|
27
|
+
start: node.ordered ? (node.start || 1) : null,
|
|
28
|
+
items: node.children.map(item => {
|
|
29
|
+
const nestedLists = (item.children || [])
|
|
30
|
+
.filter(child => child.type === 'list')
|
|
31
|
+
.map(child => listValue(child, source));
|
|
32
|
+
const text = (item.children || [])
|
|
33
|
+
.filter(child => child.type !== 'list')
|
|
34
|
+
.map(nodeText)
|
|
35
|
+
.join('\n');
|
|
36
|
+
return {
|
|
37
|
+
text,
|
|
38
|
+
source: sourceSlice(source, item),
|
|
39
|
+
lists: nestedLists
|
|
40
|
+
};
|
|
41
|
+
})
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function listsBetween(children, startIndex, endIndex, source) {
|
|
46
|
+
return children
|
|
47
|
+
.slice(startIndex, endIndex)
|
|
48
|
+
.filter(node => node.type === 'list')
|
|
49
|
+
.map(node => listValue(node, source));
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function slugify(value) {
|
|
53
|
+
const slug = value
|
|
54
|
+
.normalize('NFKD')
|
|
55
|
+
.replace(/[\u0300-\u036f]/g, '')
|
|
56
|
+
.toLowerCase()
|
|
57
|
+
.replace(/[^a-z0-9]+/g, '-')
|
|
58
|
+
.replace(/^-+|-+$/g, '');
|
|
59
|
+
return slug || 'section';
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function uniqueId(title, counts) {
|
|
63
|
+
const base = slugify(title);
|
|
64
|
+
const count = (counts.get(base) || 0) + 1;
|
|
65
|
+
counts.set(base, count);
|
|
66
|
+
return count === 1 ? base : `${base}-${count}`;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function markdownStats(source, sectionCount) {
|
|
70
|
+
return {
|
|
71
|
+
characters: Array.from(source).length,
|
|
72
|
+
utf16CodeUnits: source.length,
|
|
73
|
+
utf8Bytes: Buffer.byteLength(source, 'utf8'),
|
|
74
|
+
lines: source.length === 0 ? 0 : source.split(/\r\n|\n|\r/).length,
|
|
75
|
+
sectionCount
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
async function parseMarkdownDocument(source) {
|
|
80
|
+
if (typeof source !== 'string') {
|
|
81
|
+
throw new TypeError('Markdown document content must be a string.');
|
|
82
|
+
}
|
|
83
|
+
const fromMarkdown = await loadParser();
|
|
84
|
+
const tree = fromMarkdown(source);
|
|
85
|
+
const headings = [];
|
|
86
|
+
for (let index = 0; index < tree.children.length; index += 1) {
|
|
87
|
+
const node = tree.children[index];
|
|
88
|
+
if (node.type === 'heading') headings.push({ index, node });
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const firstHeadingOffset = headings[0]?.node.position?.start?.offset ?? source.length;
|
|
92
|
+
const preambleEndIndex = headings[0]?.index ?? tree.children.length;
|
|
93
|
+
const document = {
|
|
94
|
+
format: 'markdown',
|
|
95
|
+
preamble: {
|
|
96
|
+
source: source.slice(0, firstHeadingOffset),
|
|
97
|
+
lists: listsBetween(tree.children, 0, preambleEndIndex, source)
|
|
98
|
+
},
|
|
99
|
+
sections: [],
|
|
100
|
+
stats: markdownStats(source, headings.length)
|
|
101
|
+
};
|
|
102
|
+
const stack = [];
|
|
103
|
+
const counts = new Map();
|
|
104
|
+
|
|
105
|
+
for (let headingIndex = 0; headingIndex < headings.length; headingIndex += 1) {
|
|
106
|
+
const { index, node } = headings[headingIndex];
|
|
107
|
+
const next = headings[headingIndex + 1];
|
|
108
|
+
const headingStart = node.position.start.offset;
|
|
109
|
+
const headingEnd = node.position.end.offset;
|
|
110
|
+
const bodyEnd = next?.node.position.start.offset ?? source.length;
|
|
111
|
+
const bodyEndIndex = next?.index ?? tree.children.length;
|
|
112
|
+
const title = nodeText(node);
|
|
113
|
+
const id = uniqueId(title, counts);
|
|
114
|
+
|
|
115
|
+
while (stack.length > 0 && stack[stack.length - 1].depth >= node.depth) {
|
|
116
|
+
stack.pop();
|
|
117
|
+
}
|
|
118
|
+
const parent = stack[stack.length - 1] || null;
|
|
119
|
+
const section = {
|
|
120
|
+
id,
|
|
121
|
+
path: parent ? [...parent.path, id] : [id],
|
|
122
|
+
title,
|
|
123
|
+
depth: node.depth,
|
|
124
|
+
order: headingIndex,
|
|
125
|
+
heading: source.slice(headingStart, headingEnd),
|
|
126
|
+
body: source.slice(headingEnd, bodyEnd),
|
|
127
|
+
lists: listsBetween(tree.children, index + 1, bodyEndIndex, source),
|
|
128
|
+
children: []
|
|
129
|
+
};
|
|
130
|
+
if (parent) parent.children.push(section);
|
|
131
|
+
else document.sections.push(section);
|
|
132
|
+
stack.push(section);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
return document;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function renderSections(sections) {
|
|
139
|
+
return sections.map(section => (
|
|
140
|
+
section.heading
|
|
141
|
+
+ section.body
|
|
142
|
+
+ renderSections(section.children)
|
|
143
|
+
)).join('');
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function reconstructMarkdownDocument(document) {
|
|
147
|
+
if (!document || document.format !== 'markdown' || !Array.isArray(document.sections)) {
|
|
148
|
+
throw new TypeError('document must be a parsed Markdown document.');
|
|
149
|
+
}
|
|
150
|
+
return `${document.preamble?.source || ''}${renderSections(document.sections)}`;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
module.exports = {
|
|
154
|
+
parseMarkdownDocument,
|
|
155
|
+
reconstructMarkdownDocument
|
|
156
|
+
};
|