spl-lang 0.1.1 → 0.3.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/README.md +73 -129
- package/dist/ast.d.ts +12 -19
- package/dist/cli.js +58 -13
- package/dist/copilot.d.ts +6 -3
- package/dist/copilot.js +23 -10
- package/dist/errors.d.ts +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.js +2 -0
- package/dist/interpreter.js +106 -73
- package/dist/llm.d.ts +5 -10
- package/dist/llm.js +17 -26
- package/dist/logger.d.ts +3 -0
- package/dist/logger.js +7 -0
- package/dist/mcp.d.ts +7 -0
- package/dist/mcp.js +108 -0
- package/dist/parser.d.ts +2 -12
- package/dist/parser.js +36 -105
- package/dist/recording.d.ts +17 -0
- package/dist/recording.js +93 -0
- package/dist/restrict.d.ts +3 -0
- package/dist/restrict.js +27 -1
- package/dist/schema.d.ts +2 -0
- package/dist/schema.js +40 -0
- package/dist/tools.d.ts +9 -1
- package/dist/tools.js +223 -28
- package/dist/values.d.ts +1 -1
- package/docs/syntax.ebnf +53 -69
- package/package.json +3 -2
package/dist/parser.js
CHANGED
|
@@ -1,33 +1,11 @@
|
|
|
1
1
|
import { SplSyntaxError } from './errors.js';
|
|
2
2
|
import { tokenize } from './lexer.js';
|
|
3
|
-
export const TOOL_NAMES = ['read_file', 'write_file', 'web_fetch', 'web_search', 'shell'];
|
|
4
3
|
const KEYWORDS = new Set([
|
|
5
4
|
'let', 'if', 'elif', 'else', 'for', 'in', 'while', 'def', 'return', 'try', 'except', 'assert', 'fail',
|
|
6
|
-
'success', '
|
|
7
|
-
'True', 'False', 'None',
|
|
5
|
+
'success', 'instruct', 'avoid', 'restrict', 'think', 'agent', 'break', 'continue', 'pass', 'and', 'or', 'not',
|
|
6
|
+
'True', 'False', 'None',
|
|
8
7
|
]);
|
|
9
|
-
|
|
10
|
-
export const MODIFIER_SCHEMA = {
|
|
11
|
-
'?': {
|
|
12
|
-
len: ['word', 'sentence', 'paragraph', 'long'],
|
|
13
|
-
fmt: ['text', 'object'],
|
|
14
|
-
},
|
|
15
|
-
think: {
|
|
16
|
-
level: ['low', 'medium', 'high'],
|
|
17
|
-
show: [true, false],
|
|
18
|
-
},
|
|
19
|
-
};
|
|
20
|
-
/**
|
|
21
|
-
* Tool parameter schema: the allowed names, and the `required` ones that let the runtime run the tool itself.
|
|
22
|
-
* Every parameter is optional in a script; the LLM supplies the missing required ones when it calls the tool.
|
|
23
|
-
*/
|
|
24
|
-
export const TOOL_SCHEMA = {
|
|
25
|
-
read_file: { params: ['path'], required: ['path'] },
|
|
26
|
-
write_file: { params: ['path', 'content'], required: ['path', 'content'] },
|
|
27
|
-
web_fetch: { params: ['url'], required: ['url'] },
|
|
28
|
-
web_search: { params: ['query'], required: ['query'] },
|
|
29
|
-
shell: { params: ['command'], required: ['command'] },
|
|
30
|
-
};
|
|
8
|
+
export const MODIFIER_NAMES = ['len', 'fmt', 'reasoning', 'show', 'schema', 'repairs', 'model'];
|
|
31
9
|
const DIRECTIVE_SCHEMA = { model: 'string' };
|
|
32
10
|
const ASSIGN_OPS = new Set(['=', '+=', '-=', '*=', '/=', '//=', '%=']);
|
|
33
11
|
export function parse(source) {
|
|
@@ -264,14 +242,17 @@ class Parser {
|
|
|
264
242
|
this.next();
|
|
265
243
|
return { t: 'success', line, value: this.atStatementEnd() ? undefined : this.parseExpr() };
|
|
266
244
|
}
|
|
267
|
-
case '
|
|
268
|
-
case '
|
|
245
|
+
case 'instruct':
|
|
246
|
+
case 'avoid': {
|
|
269
247
|
const kind = t.value;
|
|
270
248
|
this.next();
|
|
271
249
|
const text = this.parseExpr();
|
|
272
250
|
return { t: 'constraint', line, kind, text, body: this.parseBlock() };
|
|
273
251
|
}
|
|
274
252
|
case 'restrict': return this.parseRestrict();
|
|
253
|
+
case 'enforce':
|
|
254
|
+
case 'forbid':
|
|
255
|
+
this.fail(`'${t.value}' was removed; use '${t.value === 'enforce' ? 'instruct' : 'avoid'}' for prompt guidance, or assert/schema for validation`);
|
|
275
256
|
default:
|
|
276
257
|
break;
|
|
277
258
|
}
|
|
@@ -283,18 +264,9 @@ class Parser {
|
|
|
283
264
|
parseLet() {
|
|
284
265
|
const line = this.tok.line;
|
|
285
266
|
this.next();
|
|
286
|
-
const
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
}
|
|
290
|
-
if (startTok.type !== 'name')
|
|
291
|
-
this.fail(`expected a variable name but found ${this.describe(startTok)}`);
|
|
292
|
-
const target = this.toTarget(this.parsePostfix(), startTok);
|
|
293
|
-
const opTok = this.tok;
|
|
294
|
-
if (opTok.type !== 'op' || !ASSIGN_OPS.has(opTok.value))
|
|
295
|
-
this.fail(`expected '=' after the target of 'let' but found ${this.describe(opTok)}`);
|
|
296
|
-
this.next();
|
|
297
|
-
return { t: 'let', line, target, op: opTok.value, value: this.parseExpr() };
|
|
267
|
+
const name = this.expectIdent('a variable name');
|
|
268
|
+
this.expectOp('=');
|
|
269
|
+
return { t: 'let', line, name, value: this.parseExpr() };
|
|
298
270
|
}
|
|
299
271
|
parseIf() {
|
|
300
272
|
const line = this.tok.line;
|
|
@@ -355,21 +327,31 @@ class Parser {
|
|
|
355
327
|
parseRestrict() {
|
|
356
328
|
const line = this.tok.line;
|
|
357
329
|
this.next();
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
330
|
+
const clauses = [];
|
|
331
|
+
for (;;) {
|
|
332
|
+
let kind = 'fs';
|
|
333
|
+
const t = this.tok;
|
|
334
|
+
if (t.type === 'name' && (t.value === 'fs' || t.value === 'net' || t.value === 'exec' || t.value === 'mcp')) {
|
|
335
|
+
kind = t.value;
|
|
336
|
+
this.next();
|
|
337
|
+
}
|
|
338
|
+
clauses.push({ kind, arg: this.parseExpr() });
|
|
339
|
+
if (!this.isOp(','))
|
|
340
|
+
break;
|
|
362
341
|
this.next();
|
|
342
|
+
this.skipNl();
|
|
363
343
|
}
|
|
364
|
-
|
|
365
|
-
return { t: 'restrict', line, kind, arg, body: this.parseBlock() };
|
|
344
|
+
return { t: 'restrict', line, clauses, body: this.parseBlock() };
|
|
366
345
|
}
|
|
367
346
|
parseSimpleStatement() {
|
|
368
|
-
const
|
|
347
|
+
const start = this.tok;
|
|
348
|
+
const line = start.line;
|
|
369
349
|
const e = this.parseExpr();
|
|
370
350
|
const t = this.tok;
|
|
371
351
|
if (t.type === 'op' && ASSIGN_OPS.has(t.value)) {
|
|
372
|
-
|
|
352
|
+
const target = this.toTarget(e, start);
|
|
353
|
+
this.next();
|
|
354
|
+
return { t: 'assign', line, target, op: t.value, value: this.parseExpr() };
|
|
373
355
|
}
|
|
374
356
|
return { t: 'expr', line, e };
|
|
375
357
|
}
|
|
@@ -597,17 +579,12 @@ class Parser {
|
|
|
597
579
|
case 'None':
|
|
598
580
|
this.next();
|
|
599
581
|
return { t: 'none' };
|
|
600
|
-
case 'think':
|
|
582
|
+
case 'think':
|
|
583
|
+
case 'agent': {
|
|
601
584
|
this.next();
|
|
602
|
-
const mods = this.isOp('{') ? this.parseMods(
|
|
603
|
-
return { t:
|
|
585
|
+
const mods = this.isOp('{') ? this.parseMods(t.value) : {};
|
|
586
|
+
return { t: t.value, mods, prompt: this.parsePrompt(t.value) };
|
|
604
587
|
}
|
|
605
|
-
case 'read_file':
|
|
606
|
-
case 'write_file':
|
|
607
|
-
case 'web_fetch':
|
|
608
|
-
case 'web_search':
|
|
609
|
-
case 'shell':
|
|
610
|
-
return this.parseToolCall(t.value);
|
|
611
588
|
default:
|
|
612
589
|
if (KEYWORDS.has(t.value))
|
|
613
590
|
this.fail(`unexpected keyword '${t.value}'`);
|
|
@@ -621,7 +598,6 @@ class Parser {
|
|
|
621
598
|
return this.parsePostfix();
|
|
622
599
|
}
|
|
623
600
|
parseMods(cmd) {
|
|
624
|
-
const schema = MODIFIER_SCHEMA[cmd];
|
|
625
601
|
this.expectOp('{');
|
|
626
602
|
this.skipNl();
|
|
627
603
|
const out = {};
|
|
@@ -631,25 +607,13 @@ class Parser {
|
|
|
631
607
|
this.fail('expected a modifier name');
|
|
632
608
|
const name = nameTok.value;
|
|
633
609
|
this.next();
|
|
634
|
-
if (!(
|
|
635
|
-
this.fail(`unknown modifier '${name}' for '${cmd}' (allowed: ${
|
|
610
|
+
if (!MODIFIER_NAMES.some((allowed) => allowed === name)) {
|
|
611
|
+
this.fail(`unknown modifier '${name}' for '${cmd}' (allowed: ${MODIFIER_NAMES.join(', ')})`, nameTok);
|
|
636
612
|
}
|
|
637
613
|
if (name in out)
|
|
638
614
|
this.fail(`duplicate modifier '${name}'`, nameTok);
|
|
639
615
|
this.expectOp('=');
|
|
640
|
-
|
|
641
|
-
let value;
|
|
642
|
-
if (valTok.type === 'str')
|
|
643
|
-
value = this.parseLiteralString(`modifier '${name}'`);
|
|
644
|
-
else if (this.isName('True') || this.isName('False'))
|
|
645
|
-
value = this.next().value === 'True';
|
|
646
|
-
else
|
|
647
|
-
return this.fail(`modifier '${name}' requires a quoted string or True/False`);
|
|
648
|
-
if (!schema[name].includes(value)) {
|
|
649
|
-
const allowed = schema[name].map((v) => (typeof v === 'string' ? `'${v}'` : v ? 'True' : 'False'));
|
|
650
|
-
this.fail(`invalid value ${typeof value === 'string' ? `'${value}'` : value ? 'True' : 'False'} for modifier '${name}' (allowed: ${allowed.join(', ')})`, valTok);
|
|
651
|
-
}
|
|
652
|
-
out[name] = value;
|
|
616
|
+
out[name] = this.parseExpr();
|
|
653
617
|
this.skipNl();
|
|
654
618
|
if (this.isOp(',')) {
|
|
655
619
|
this.next();
|
|
@@ -662,39 +626,6 @@ class Parser {
|
|
|
662
626
|
this.next();
|
|
663
627
|
return out;
|
|
664
628
|
}
|
|
665
|
-
parseToolCall(tool) {
|
|
666
|
-
this.next();
|
|
667
|
-
const schema = TOOL_SCHEMA[tool];
|
|
668
|
-
const params = {};
|
|
669
|
-
if (this.isOp('{')) {
|
|
670
|
-
this.next();
|
|
671
|
-
this.skipNl();
|
|
672
|
-
while (!this.isOp('}')) {
|
|
673
|
-
const nameTok = this.tok;
|
|
674
|
-
if (nameTok.type !== 'name')
|
|
675
|
-
this.fail('expected a parameter name');
|
|
676
|
-
const name = nameTok.value;
|
|
677
|
-
this.next();
|
|
678
|
-
if (!schema.params.includes(name)) {
|
|
679
|
-
this.fail(`unknown parameter '${name}' for '${tool}' (allowed: ${schema.params.join(', ')})`, nameTok);
|
|
680
|
-
}
|
|
681
|
-
if (name in params)
|
|
682
|
-
this.fail(`duplicate parameter '${name}'`, nameTok);
|
|
683
|
-
this.expectOp('=');
|
|
684
|
-
params[name] = this.parseExpr();
|
|
685
|
-
this.skipNl();
|
|
686
|
-
if (this.isOp(',')) {
|
|
687
|
-
this.next();
|
|
688
|
-
this.skipNl();
|
|
689
|
-
}
|
|
690
|
-
else if (!this.isOp('}')) {
|
|
691
|
-
this.fail("expected ',' or '}' in tool parameters");
|
|
692
|
-
}
|
|
693
|
-
}
|
|
694
|
-
this.next();
|
|
695
|
-
}
|
|
696
|
-
return { t: 'tool', tool, params, instructions: this.parsePrompt(tool) };
|
|
697
|
-
}
|
|
698
629
|
convertParts(parts) {
|
|
699
630
|
return parts.map((p) => {
|
|
700
631
|
if ('text' in p)
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { type LlmBackend, type LlmRequest, type LlmResponse } from './llm.js';
|
|
2
|
+
export declare class RecordingBackend implements LlmBackend {
|
|
3
|
+
private readonly backend;
|
|
4
|
+
private readonly file;
|
|
5
|
+
readonly mode = "record";
|
|
6
|
+
constructor(backend: LlmBackend, file: string);
|
|
7
|
+
call(req: LlmRequest): Promise<LlmResponse>;
|
|
8
|
+
close(): Promise<void>;
|
|
9
|
+
}
|
|
10
|
+
export declare class ReplayBackend implements LlmBackend {
|
|
11
|
+
readonly mode = "replay";
|
|
12
|
+
private readonly entries;
|
|
13
|
+
private position;
|
|
14
|
+
constructor(file: string);
|
|
15
|
+
call(req: LlmRequest): Promise<LlmResponse>;
|
|
16
|
+
close(): Promise<void>;
|
|
17
|
+
}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import * as fs from 'node:fs';
|
|
2
|
+
import { LlmError } from './llm.js';
|
|
3
|
+
function requestKey(req) {
|
|
4
|
+
return JSON.stringify({
|
|
5
|
+
kind: req.kind, prompt: req.prompt, model: req.model ?? null,
|
|
6
|
+
reasoningEffort: req.reasoningEffort ?? null, showReasoning: req.showReasoning,
|
|
7
|
+
});
|
|
8
|
+
}
|
|
9
|
+
function isRecord(value) {
|
|
10
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
11
|
+
}
|
|
12
|
+
function parseLine(line, number) {
|
|
13
|
+
try {
|
|
14
|
+
return JSON.parse(line);
|
|
15
|
+
}
|
|
16
|
+
catch (e) {
|
|
17
|
+
if (!(e instanceof SyntaxError))
|
|
18
|
+
throw e;
|
|
19
|
+
throw new LlmError(`invalid recording JSON at line ${number}: ${e.message}`);
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
export class RecordingBackend {
|
|
23
|
+
backend;
|
|
24
|
+
file;
|
|
25
|
+
mode = 'record';
|
|
26
|
+
constructor(backend, file) {
|
|
27
|
+
this.backend = backend;
|
|
28
|
+
this.file = file;
|
|
29
|
+
fs.writeFileSync(file, JSON.stringify({ format: 'spl-generation', version: 1 }) + '\n', { flag: 'wx', mode: 0o600 });
|
|
30
|
+
}
|
|
31
|
+
async call(req) {
|
|
32
|
+
if (req.kind === 'agent')
|
|
33
|
+
throw new LlmError('agent actions cannot be recorded; record generation-only workflows');
|
|
34
|
+
const request = requestKey(req);
|
|
35
|
+
const response = await this.backend.call(req);
|
|
36
|
+
try {
|
|
37
|
+
fs.appendFileSync(this.file, JSON.stringify({ request, response }) + '\n');
|
|
38
|
+
}
|
|
39
|
+
catch (e) {
|
|
40
|
+
if (!(e instanceof Error))
|
|
41
|
+
throw e;
|
|
42
|
+
throw new LlmError(`could not save recording: ${e.message}`);
|
|
43
|
+
}
|
|
44
|
+
return response;
|
|
45
|
+
}
|
|
46
|
+
close() {
|
|
47
|
+
return this.backend.close();
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
export class ReplayBackend {
|
|
51
|
+
mode = 'replay';
|
|
52
|
+
entries;
|
|
53
|
+
position = 0;
|
|
54
|
+
constructor(file) {
|
|
55
|
+
const lines = fs.readFileSync(file, 'utf8').trimEnd().split(/\r?\n/);
|
|
56
|
+
const header = parseLine(lines.shift() ?? '', 1);
|
|
57
|
+
if (!isRecord(header) || header.format !== 'spl-generation' || header.version !== 1) {
|
|
58
|
+
throw new LlmError('unsupported recording format/version');
|
|
59
|
+
}
|
|
60
|
+
this.entries = lines.map((line, i) => {
|
|
61
|
+
const entry = parseLine(line, i + 2);
|
|
62
|
+
if (!isRecord(entry) || typeof entry.request !== 'string' || !isRecord(entry.response)
|
|
63
|
+
|| typeof entry.response.text !== 'string' || !isRecord(entry.response.effective)
|
|
64
|
+
|| (entry.response.reasoning !== undefined && typeof entry.response.reasoning !== 'string')) {
|
|
65
|
+
throw new LlmError(`invalid recording entry at line ${i + 2}`);
|
|
66
|
+
}
|
|
67
|
+
return {
|
|
68
|
+
request: entry.request,
|
|
69
|
+
response: {
|
|
70
|
+
text: entry.response.text,
|
|
71
|
+
effective: entry.response.effective,
|
|
72
|
+
reasoning: entry.response.reasoning,
|
|
73
|
+
},
|
|
74
|
+
};
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
async call(req) {
|
|
78
|
+
if (req.kind === 'agent')
|
|
79
|
+
throw new LlmError('agent actions cannot be replayed');
|
|
80
|
+
const entry = this.entries[this.position];
|
|
81
|
+
if (!entry)
|
|
82
|
+
throw new LlmError(`recording exhausted at call ${this.position + 1}`);
|
|
83
|
+
if (entry.request !== requestKey(req))
|
|
84
|
+
throw new LlmError(`replay request mismatch at call ${this.position + 1}`);
|
|
85
|
+
this.position++;
|
|
86
|
+
return structuredClone(entry.response);
|
|
87
|
+
}
|
|
88
|
+
async close() {
|
|
89
|
+
if (this.position !== this.entries.length) {
|
|
90
|
+
throw new LlmError(`replay ended with ${this.entries.length - this.position} unused recorded response(s)`);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}
|
package/dist/restrict.d.ts
CHANGED
|
@@ -20,6 +20,9 @@ export declare class Restrictions {
|
|
|
20
20
|
checkUrl(url: string): URL;
|
|
21
21
|
checkHost(host: string): void;
|
|
22
22
|
checkExec(name: string): void;
|
|
23
|
+
checkMcp(server: string, tool: string): void;
|
|
24
|
+
/** Narrows configured tools before the SDK starts servers or advertises their tools. */
|
|
25
|
+
mcpTools(server: string, configured?: readonly string[]): string[];
|
|
23
26
|
/** Human-readable guidance for the LLM (the first enforcement layer). */
|
|
24
27
|
guidance(): string[];
|
|
25
28
|
}
|
package/dist/restrict.js
CHANGED
|
@@ -127,6 +127,24 @@ export class Restrictions {
|
|
|
127
127
|
}
|
|
128
128
|
}
|
|
129
129
|
}
|
|
130
|
+
checkMcp(server, tool) {
|
|
131
|
+
for (const r of this.list) {
|
|
132
|
+
if (r.kind === 'mcp' && !r.values.some((v) => v === server || v === `${server}/*` || v === `${server}/${tool}`)) {
|
|
133
|
+
throw violation(`MCP tool '${server}/${tool}' blocked: allowed MCP servers/tools are ${r.values.join(', ')}`);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
/** Narrows configured tools before the SDK starts servers or advertises their tools. */
|
|
138
|
+
mcpTools(server, configured = ['*']) {
|
|
139
|
+
let tools = [...configured];
|
|
140
|
+
for (const r of this.list) {
|
|
141
|
+
if (r.kind !== 'mcp' || r.values.includes(server) || r.values.includes(`${server}/*`))
|
|
142
|
+
continue;
|
|
143
|
+
const allowed = r.values.filter((v) => v.startsWith(`${server}/`)).map((v) => v.slice(server.length + 1));
|
|
144
|
+
tools = tools.includes('*') ? allowed : tools.filter((t) => allowed.includes(t));
|
|
145
|
+
}
|
|
146
|
+
return [...new Set(tools)];
|
|
147
|
+
}
|
|
130
148
|
/** Human-readable guidance for the LLM (the first enforcement layer). */
|
|
131
149
|
guidance() {
|
|
132
150
|
return this.list.map((r) => {
|
|
@@ -134,7 +152,9 @@ export class Restrictions {
|
|
|
134
152
|
return `File system: you may read and write only inside: ${r.values.join(', ')}. Any other path will be blocked.`;
|
|
135
153
|
if (r.kind === 'net')
|
|
136
154
|
return `Network: you may only access these hosts: ${r.values.join(', ')}. Any other host will be blocked.`;
|
|
137
|
-
|
|
155
|
+
if (r.kind === 'mcp')
|
|
156
|
+
return `MCP: you may only call these servers/tools: ${r.values.join(', ')}. Anything else will be blocked.`;
|
|
157
|
+
return `Shell: you may only run these executables: ${r.values.join(', ')}. Anything else will be blocked.`;
|
|
138
158
|
});
|
|
139
159
|
}
|
|
140
160
|
}
|
|
@@ -154,5 +174,11 @@ export function makeRestriction(kind, raw, cwd) {
|
|
|
154
174
|
return { kind, values: [...new Set(items.map((i) => canonicalPath(i, cwd)))] };
|
|
155
175
|
if (kind === 'net')
|
|
156
176
|
return { kind, values: [...new Set(items.map(normalizeHost))] };
|
|
177
|
+
if (kind === 'mcp') {
|
|
178
|
+
if (items.some((v) => !/^[A-Za-z0-9_.-]+(?:\/(?:[A-Za-z0-9_.-]+|\*))?$/.test(v))) {
|
|
179
|
+
throw new Failure('runtime_error', 'restrict mcp expects server names or server/tool selectors');
|
|
180
|
+
}
|
|
181
|
+
return { kind, values: [...new Set(items)] };
|
|
182
|
+
}
|
|
157
183
|
return { kind, values: [...new Set(items.map(normalizeExec))] };
|
|
158
184
|
}
|
package/dist/schema.d.ts
ADDED
package/dist/schema.js
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { Ajv } from 'ajv';
|
|
2
|
+
import { Failure } from './errors.js';
|
|
3
|
+
import { isDict, runtimeError, toJson } from './values.js';
|
|
4
|
+
const ajv = new Ajv({ allErrors: true, strict: true });
|
|
5
|
+
function errorPath(error) {
|
|
6
|
+
const parts = error.instancePath.split('/').slice(1).map((p) => p.replace(/~1/g, '/').replace(/~0/g, '~'));
|
|
7
|
+
if (error.keyword === 'required')
|
|
8
|
+
parts.push(String(error.params.missingProperty));
|
|
9
|
+
if (error.keyword === 'additionalProperties')
|
|
10
|
+
parts.push(String(error.params.additionalProperty));
|
|
11
|
+
return parts.reduce((path, part) => {
|
|
12
|
+
if (/^(0|[1-9]\d*)$/.test(part))
|
|
13
|
+
return `${path}[${part}]`;
|
|
14
|
+
return /^[a-zA-Z_]\w*$/.test(part) ? `${path ? `${path}.` : ''}${part}` : `${path}[${JSON.stringify(part)}]`;
|
|
15
|
+
}, '') || '$';
|
|
16
|
+
}
|
|
17
|
+
export function compileSchema(schema) {
|
|
18
|
+
if (typeof schema !== 'boolean' && !isDict(schema))
|
|
19
|
+
throw runtimeError('schema must be a JSON Schema object or boolean');
|
|
20
|
+
const json = toJson(schema);
|
|
21
|
+
if (typeof json !== 'boolean' && (typeof json !== 'object' || json === null))
|
|
22
|
+
throw runtimeError('invalid schema');
|
|
23
|
+
let validate;
|
|
24
|
+
try {
|
|
25
|
+
validate = ajv.compile(json);
|
|
26
|
+
if ('$async' in validate && validate.$async)
|
|
27
|
+
throw new Error('asynchronous schemas are not supported');
|
|
28
|
+
}
|
|
29
|
+
catch (e) {
|
|
30
|
+
if (!(e instanceof Error))
|
|
31
|
+
throw e;
|
|
32
|
+
throw runtimeError(`invalid schema: ${e.message}`);
|
|
33
|
+
}
|
|
34
|
+
return (value) => {
|
|
35
|
+
if (!validate(value)) {
|
|
36
|
+
const details = (validate.errors ?? []).map((error) => `${errorPath(error)}: ${error.message}`).join('; ');
|
|
37
|
+
throw new Failure('validation_failed', details);
|
|
38
|
+
}
|
|
39
|
+
};
|
|
40
|
+
}
|
package/dist/tools.d.ts
CHANGED
|
@@ -12,6 +12,10 @@ export interface ToolSpec {
|
|
|
12
12
|
export declare const TOOL_SPECS: Record<ToolName, ToolSpec>;
|
|
13
13
|
/** Splits a simple command line, honoring '...' and "..." quoting. No escapes, no expansion. */
|
|
14
14
|
export declare function splitCommand(command: string): string[];
|
|
15
|
+
/** Splits a command string by chain/pipeline separators (&&, ||, |, ;, &, \n) outside quotes. */
|
|
16
|
+
export declare function splitChainedCommands(command: string): string[];
|
|
17
|
+
/** Finds nested $(...) or `...` command substitutions outside single quotes. */
|
|
18
|
+
export declare function extractSubstitutions(command: string): string[];
|
|
15
19
|
export interface SearchResult {
|
|
16
20
|
title: string;
|
|
17
21
|
url: string;
|
|
@@ -26,7 +30,9 @@ export declare function parseSearchResults(html: string): SearchResult[];
|
|
|
26
30
|
export declare class ToolHost {
|
|
27
31
|
private readonly restrictions;
|
|
28
32
|
private readonly cwd;
|
|
29
|
-
|
|
33
|
+
private readonly replay;
|
|
34
|
+
private readonly raw;
|
|
35
|
+
constructor(restrictions: Restrictions, cwd: string, replay?: boolean, raw?: boolean);
|
|
30
36
|
invoke(name: ToolName, params: Record<string, unknown>): Promise<string>;
|
|
31
37
|
private readFile;
|
|
32
38
|
private writeFile;
|
|
@@ -34,5 +40,7 @@ export declare class ToolHost {
|
|
|
34
40
|
/** Searches DuckDuckGo's HTML endpoint (no API key); it is a network call, so `restrict net` applies to it. */
|
|
35
41
|
private webSearch;
|
|
36
42
|
private shell;
|
|
43
|
+
private checkCommandRestrictions;
|
|
44
|
+
private checkSubcommand;
|
|
37
45
|
private run;
|
|
38
46
|
}
|