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/interpreter.js
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import { callMethod, iterate, makeBuiltins } from './builtins.js';
|
|
2
2
|
import { Failure, SuccessSignal } from './errors.js';
|
|
3
|
-
import { LlmError, buildPrompt,
|
|
3
|
+
import { LlmError, buildPrompt, parseJsonResponse } from './llm.js';
|
|
4
|
+
import { compileSchema } from './schema.js';
|
|
4
5
|
import { Restrictions, makeRestriction } from './restrict.js';
|
|
5
6
|
import { MemoryStore } from './store.js';
|
|
6
7
|
import { ToolHost, TOOL_SPECS } from './tools.js';
|
|
7
|
-
import { compareValues, equals, hasKey, isDict, isFunction, isList, newDict, runtimeError, setKey, str, truthy, typeName, } from './values.js';
|
|
8
|
+
import { compareValues, equals, hasKey, isDict, isFunction, isList, newDict, runtimeError, setKey, str, toJson, truthy, typeName, } from './values.js';
|
|
8
9
|
export async function run(program, opts) {
|
|
9
10
|
return new Interpreter(program, opts).run();
|
|
10
11
|
}
|
|
@@ -29,10 +30,16 @@ class Interpreter {
|
|
|
29
30
|
this.maxDepth = opts.maxCallDepth ?? 200;
|
|
30
31
|
this.restrictions = new Restrictions(this.cwd);
|
|
31
32
|
this.builtins = makeBuiltins({ log: (m) => opts.logger.info(`log: ${m}`) });
|
|
33
|
+
for (const tool of Object.keys(TOOL_SPECS)) {
|
|
34
|
+
this.builtins.set(tool, { __fn: 'builtin', name: tool, call: (args) => this.callTool(tool, args) });
|
|
35
|
+
}
|
|
32
36
|
}
|
|
33
37
|
async run() {
|
|
34
38
|
const { logger } = this.opts;
|
|
35
39
|
try {
|
|
40
|
+
if (this.opts.backend.mode === 'replay' && this.opts.store) {
|
|
41
|
+
throw new Failure('restriction_violation', 'replay does not allow a persistent variable store');
|
|
42
|
+
}
|
|
36
43
|
for (const [k, v] of Object.entries(this.store.load()))
|
|
37
44
|
this.globals.vars.set(k, v);
|
|
38
45
|
const signal = await this.execBlock(this.program.body, this.globals);
|
|
@@ -53,11 +60,13 @@ class Interpreter {
|
|
|
53
60
|
}
|
|
54
61
|
finally {
|
|
55
62
|
try {
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
63
|
+
if (this.opts.backend.mode !== 'replay') {
|
|
64
|
+
const snapshot = {};
|
|
65
|
+
for (const [k, v] of this.globals.vars)
|
|
66
|
+
if (!isFunction(v))
|
|
67
|
+
snapshot[k] = v;
|
|
68
|
+
this.store.save(snapshot);
|
|
69
|
+
}
|
|
61
70
|
}
|
|
62
71
|
catch (e) {
|
|
63
72
|
logger.error(`could not save variable store: ${e.message}`);
|
|
@@ -86,6 +95,7 @@ class Interpreter {
|
|
|
86
95
|
}
|
|
87
96
|
async execStmt(s, env) {
|
|
88
97
|
try {
|
|
98
|
+
this.opts.logger.deterministic(`line ${s.line} (${s.t}): ${this.program.lines[s.line - 1]?.trim() ?? ''}`);
|
|
89
99
|
return await this.execStmtInner(s, env);
|
|
90
100
|
}
|
|
91
101
|
catch (e) {
|
|
@@ -100,10 +110,13 @@ class Interpreter {
|
|
|
100
110
|
async execStmtInner(s, env) {
|
|
101
111
|
switch (s.t) {
|
|
102
112
|
case 'let':
|
|
103
|
-
|
|
113
|
+
env.vars.set(s.name, await this.eval(s.value, env));
|
|
114
|
+
return;
|
|
115
|
+
case 'assign':
|
|
116
|
+
await this.execAssign(s.target, s.op, s.value, env);
|
|
104
117
|
return;
|
|
105
118
|
case 'block':
|
|
106
|
-
return this.execBlock(s.body,
|
|
119
|
+
return this.execBlock(s.body, env);
|
|
107
120
|
case 'expr':
|
|
108
121
|
await this.eval(s.e, env);
|
|
109
122
|
return;
|
|
@@ -189,11 +202,13 @@ class Interpreter {
|
|
|
189
202
|
}
|
|
190
203
|
}
|
|
191
204
|
case 'restrict': {
|
|
192
|
-
const restriction = makeRestriction(s.kind, await this.eval(s.arg, env), this.cwd);
|
|
193
205
|
const saved = this.restrictions;
|
|
194
|
-
this.restrictions = saved.with(restriction);
|
|
195
|
-
this.opts.logger.debug(`restrict ${s.kind}: ${restriction.values.join(', ')}`);
|
|
196
206
|
try {
|
|
207
|
+
for (const clause of s.clauses) {
|
|
208
|
+
const restriction = makeRestriction(clause.kind, await this.eval(clause.arg, env), this.cwd);
|
|
209
|
+
this.restrictions = this.restrictions.with(restriction);
|
|
210
|
+
this.opts.logger.debug(`restrict ${clause.kind}: ${restriction.values.join(', ')}`);
|
|
211
|
+
}
|
|
197
212
|
return await this.execBlock(s.body, env);
|
|
198
213
|
}
|
|
199
214
|
finally {
|
|
@@ -212,13 +227,14 @@ class Interpreter {
|
|
|
212
227
|
}
|
|
213
228
|
vars.forEach((v, i) => env.vars.set(v, item[i]));
|
|
214
229
|
}
|
|
215
|
-
async
|
|
230
|
+
async execAssign(target, op, valueExpr, env) {
|
|
216
231
|
const compute = async (current) => {
|
|
217
232
|
const rhs = await this.eval(valueExpr, env);
|
|
218
233
|
return op === '=' ? rhs : this.binary(op.slice(0, -1), current(), rhs);
|
|
219
234
|
};
|
|
220
235
|
if (target.t === 'name') {
|
|
221
|
-
|
|
236
|
+
if (!env.vars.has(target.name))
|
|
237
|
+
throw runtimeError(`cannot assign '${target.name}': declare a local with let first`);
|
|
222
238
|
env.vars.set(target.name, await compute(() => this.lookup(target.name, env)));
|
|
223
239
|
return;
|
|
224
240
|
}
|
|
@@ -316,21 +332,10 @@ class Interpreter {
|
|
|
316
332
|
const hi = e.hi ? await this.eval(e.hi, env) : undefined;
|
|
317
333
|
return this.slice(obj, lo, hi);
|
|
318
334
|
}
|
|
319
|
-
case 'ask':
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
return e.mods.
|
|
323
|
-
}
|
|
324
|
-
case 'think': {
|
|
325
|
-
const prompt = str(await this.eval(e.prompt, env));
|
|
326
|
-
return this.callLlm({
|
|
327
|
-
kind: 'think',
|
|
328
|
-
prompt,
|
|
329
|
-
effort: e.mods.level ?? 'medium',
|
|
330
|
-
show: e.mods.show ?? false,
|
|
331
|
-
});
|
|
332
|
-
}
|
|
333
|
-
case 'tool': return this.evalTool(e.tool, e.params, e.instructions, env);
|
|
335
|
+
case 'ask':
|
|
336
|
+
case 'think':
|
|
337
|
+
case 'agent':
|
|
338
|
+
return this.evalGeneration(e.t, e.mods, e.prompt, env);
|
|
334
339
|
}
|
|
335
340
|
}
|
|
336
341
|
getKey(d, k) {
|
|
@@ -481,30 +486,14 @@ class Interpreter {
|
|
|
481
486
|
return typeof obj === 'string' ? part.join('') : part;
|
|
482
487
|
}
|
|
483
488
|
// ---- LLM and tools -------------------------------------------------
|
|
484
|
-
async callLlm(
|
|
489
|
+
async callLlm(req) {
|
|
485
490
|
const { logger, backend } = this.opts;
|
|
486
|
-
|
|
487
|
-
kind: c.kind,
|
|
488
|
-
prompt: buildPrompt({
|
|
489
|
-
prompt: c.prompt,
|
|
490
|
-
mods: c.mods,
|
|
491
|
-
thinking: c.kind === 'think',
|
|
492
|
-
constraints: this.constraints,
|
|
493
|
-
restrictions: this.restrictions,
|
|
494
|
-
}),
|
|
495
|
-
model: this.model,
|
|
496
|
-
reasoningEffort: c.effort ?? (c.kind === 'tool' ? 'medium' : undefined),
|
|
497
|
-
showReasoning: c.show ?? false,
|
|
498
|
-
restrictions: this.restrictions,
|
|
499
|
-
tools: new ToolHost(this.restrictions, this.cwd),
|
|
500
|
-
pinned: c.pinned,
|
|
501
|
-
};
|
|
502
|
-
logger.debug(`llm request (${c.kind}):\n${req.prompt}`);
|
|
491
|
+
logger.llm(`llm request (${req.kind}):\n${req.prompt}`);
|
|
503
492
|
const started = Date.now();
|
|
504
493
|
try {
|
|
505
494
|
const res = await backend.call(req);
|
|
506
|
-
logger.info(`llm ${
|
|
507
|
-
logger.
|
|
495
|
+
logger.info(`llm ${req.kind} call: ${JSON.stringify(res.effective)} (${Date.now() - started} ms)`);
|
|
496
|
+
logger.llm(`llm response:\n${res.text}`);
|
|
508
497
|
return res.text.trim();
|
|
509
498
|
}
|
|
510
499
|
catch (e) {
|
|
@@ -515,32 +504,76 @@ class Interpreter {
|
|
|
515
504
|
throw new Failure('llm_error', e.message ?? String(e));
|
|
516
505
|
}
|
|
517
506
|
}
|
|
518
|
-
async
|
|
507
|
+
async callTool(tool, args) {
|
|
508
|
+
const names = TOOL_SPECS[tool].required;
|
|
509
|
+
if (args.length !== names.length)
|
|
510
|
+
throw runtimeError(`${tool}() takes ${names.length} argument(s) but ${args.length} were given`);
|
|
519
511
|
const params = {};
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
512
|
+
names.forEach((name, i) => {
|
|
513
|
+
if (typeof args[i] !== 'string')
|
|
514
|
+
throw runtimeError(`${tool}(): ${name} must be a string`);
|
|
515
|
+
params[name] = args[i];
|
|
516
|
+
});
|
|
517
|
+
return new ToolHost(this.restrictions, this.cwd, this.opts.backend.mode === 'replay', true).invoke(tool, params);
|
|
518
|
+
}
|
|
519
|
+
async evalGeneration(kind, mods, promptExpr, env) {
|
|
520
|
+
if (kind === 'agent' && this.opts.backend.mode) {
|
|
521
|
+
throw new Failure('restriction_violation', 'agents cannot run in record/replay mode; record generation-only workflows');
|
|
522
|
+
}
|
|
523
|
+
const options = {};
|
|
524
|
+
for (const [name, expr] of Object.entries(mods))
|
|
525
|
+
setKey(options, name, await this.eval(expr, env));
|
|
526
|
+
const { len, reasoning = kind === 'ask' ? undefined : 'medium', show = false, repairs = 0, model } = options;
|
|
527
|
+
const fmt = options.fmt ?? ('schema' in options ? 'json' : 'text');
|
|
528
|
+
if (len !== undefined && len !== 'word' && len !== 'sentence' && len !== 'paragraph' && len !== 'long')
|
|
529
|
+
throw runtimeError('len must be word, sentence, paragraph, or long');
|
|
530
|
+
if (fmt !== 'text' && fmt !== 'json' && fmt !== 'object')
|
|
531
|
+
throw runtimeError('fmt must be text, json, or object');
|
|
532
|
+
if (reasoning !== undefined && reasoning !== 'low' && reasoning !== 'medium' && reasoning !== 'high')
|
|
533
|
+
throw runtimeError('reasoning must be low, medium, or high');
|
|
534
|
+
if (typeof show !== 'boolean')
|
|
535
|
+
throw runtimeError('show must be True or False');
|
|
536
|
+
if (model !== undefined && (typeof model !== 'string' || !model.trim()))
|
|
537
|
+
throw runtimeError('model must be a non-empty string');
|
|
538
|
+
if (typeof repairs !== 'number' || !Number.isInteger(repairs) || repairs < 0 || repairs > 5)
|
|
539
|
+
throw runtimeError('repairs must be an integer from 0 to 5');
|
|
540
|
+
if (kind === 'agent' && repairs !== 0)
|
|
541
|
+
throw runtimeError('agents cannot use repairs: external actions must not be retried automatically');
|
|
542
|
+
if (fmt === 'text' && ('schema' in options || repairs !== 0))
|
|
543
|
+
throw runtimeError('schema and repairs require JSON output');
|
|
544
|
+
const validate = 'schema' in options ? compileSchema(options.schema) : undefined;
|
|
545
|
+
let prompt = buildPrompt({
|
|
546
|
+
prompt: str(await this.eval(promptExpr, env)),
|
|
547
|
+
mods: { len, fmt: fmt === 'object' ? 'object' : undefined },
|
|
548
|
+
thinking: reasoning !== undefined,
|
|
549
|
+
constraints: this.constraints,
|
|
550
|
+
restrictions: this.restrictions,
|
|
551
|
+
});
|
|
552
|
+
if (fmt === 'json')
|
|
553
|
+
prompt += '\n\nReturn ONLY valid JSON, without prose or code fences.';
|
|
554
|
+
if (validate)
|
|
555
|
+
prompt += `\n\nThe result must satisfy this JSON Schema:\n${JSON.stringify(toJson(options.schema))}`;
|
|
556
|
+
const basePrompt = prompt;
|
|
557
|
+
for (let attempt = 0;; attempt++) {
|
|
558
|
+
const text = await this.callLlm({
|
|
559
|
+
kind, prompt,
|
|
560
|
+
model: this.opts.model ?? model ?? this.model,
|
|
561
|
+
reasoningEffort: reasoning,
|
|
562
|
+
showReasoning: show,
|
|
563
|
+
restrictions: this.restrictions,
|
|
564
|
+
tools: new ToolHost(this.restrictions, this.cwd, this.opts.backend.mode === 'replay'),
|
|
537
565
|
});
|
|
566
|
+
try {
|
|
567
|
+
const value = fmt === 'text' ? text : parseJsonResponse(text, fmt === 'object');
|
|
568
|
+
validate?.(value);
|
|
569
|
+
return value;
|
|
570
|
+
}
|
|
571
|
+
catch (e) {
|
|
572
|
+
if (!(e instanceof Failure) || (e.statusCode !== 'invalid_format' && e.statusCode !== 'validation_failed') || attempt >= repairs)
|
|
573
|
+
throw e;
|
|
574
|
+
this.opts.logger.info(`repair ${attempt + 1}/${repairs}: ${e.reason}`);
|
|
575
|
+
prompt = `${basePrompt}\n\nPrevious response:\n${text}\n\nValidation failed: ${e.reason}\nReturn a corrected JSON value.`;
|
|
576
|
+
}
|
|
538
577
|
}
|
|
539
|
-
const output = await host.invoke(tool, params);
|
|
540
|
-
this.opts.logger.debug(`tool ${tool} ${JSON.stringify(params)} -> ${output.length} chars`);
|
|
541
|
-
return this.callLlm({
|
|
542
|
-
kind: 'tool',
|
|
543
|
-
prompt: `The tool \`${tool}\` was run with parameters ${JSON.stringify(params)}. Its output is between the markers.\n<tool_output>\n${output}\n</tool_output>\n\nInstructions: ${instructions}`,
|
|
544
|
-
});
|
|
545
578
|
}
|
|
546
579
|
}
|
package/dist/llm.d.ts
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
|
-
import type { AskMods
|
|
1
|
+
import type { AskMods } from './ast.js';
|
|
2
2
|
import type { Restrictions } from './restrict.js';
|
|
3
3
|
import type { ToolHost } from './tools.js';
|
|
4
4
|
import { type Value } from './values.js';
|
|
5
5
|
export type ReasoningLevel = 'low' | 'medium' | 'high';
|
|
6
6
|
export interface Constraint {
|
|
7
|
-
kind: '
|
|
7
|
+
kind: 'instruct' | 'avoid';
|
|
8
8
|
text: string;
|
|
9
9
|
}
|
|
10
10
|
export interface LlmRequest {
|
|
11
|
-
kind: 'ask' | 'think' | '
|
|
11
|
+
kind: 'ask' | 'think' | 'agent';
|
|
12
12
|
/** The fully assembled query: constraints, restriction guidance and hints are already prepended. */
|
|
13
13
|
prompt: string;
|
|
14
14
|
model?: string;
|
|
@@ -16,11 +16,6 @@ export interface LlmRequest {
|
|
|
16
16
|
showReasoning: boolean;
|
|
17
17
|
restrictions: Restrictions;
|
|
18
18
|
tools: ToolHost;
|
|
19
|
-
/** For tools the script left parameters out of: the LLM calls the tool itself, these params are fixed by the script. */
|
|
20
|
-
pinned?: {
|
|
21
|
-
tool: ToolName;
|
|
22
|
-
params: Record<string, string>;
|
|
23
|
-
};
|
|
24
19
|
}
|
|
25
20
|
export interface LlmResponse {
|
|
26
21
|
text: string;
|
|
@@ -29,6 +24,7 @@ export interface LlmResponse {
|
|
|
29
24
|
effective: Record<string, unknown>;
|
|
30
25
|
}
|
|
31
26
|
export interface LlmBackend {
|
|
27
|
+
readonly mode?: 'record' | 'replay';
|
|
32
28
|
call(req: LlmRequest): Promise<LlmResponse>;
|
|
33
29
|
close(): Promise<void>;
|
|
34
30
|
}
|
|
@@ -42,5 +38,4 @@ export declare function buildPrompt(opts: {
|
|
|
42
38
|
constraints: readonly Constraint[];
|
|
43
39
|
restrictions: Restrictions;
|
|
44
40
|
}): string;
|
|
45
|
-
|
|
46
|
-
export declare function parseObjectResponse(text: string): Value;
|
|
41
|
+
export declare function parseJsonResponse(text: string, objectOnly?: boolean): Value;
|
package/dist/llm.js
CHANGED
|
@@ -9,12 +9,12 @@ const LEN_HINTS = {
|
|
|
9
9
|
paragraph: 'Answer in a single paragraph.',
|
|
10
10
|
long: 'Give a long, detailed answer.',
|
|
11
11
|
};
|
|
12
|
-
const
|
|
12
|
+
const JSON_HINT = 'Respond with ONLY valid JSON. No prose, no markdown, no code fences.';
|
|
13
13
|
export function buildPrompt(opts) {
|
|
14
14
|
const sections = [];
|
|
15
15
|
if (opts.constraints.length) {
|
|
16
|
-
sections.push('
|
|
17
|
-
opts.constraints.map((c) => `-
|
|
16
|
+
sections.push('Response guidance (not runtime validation):\n' +
|
|
17
|
+
opts.constraints.map((c) => `- ${c.kind === 'instruct' ? 'Follow' : 'Avoid'}: ${c.text}`).join('\n'));
|
|
18
18
|
}
|
|
19
19
|
const guidance = opts.restrictions.guidance();
|
|
20
20
|
if (guidance.length)
|
|
@@ -24,31 +24,22 @@ export function buildPrompt(opts) {
|
|
|
24
24
|
if (opts.mods?.len)
|
|
25
25
|
sections.push(LEN_HINTS[opts.mods.len]);
|
|
26
26
|
if (opts.mods?.fmt === 'object')
|
|
27
|
-
sections.push(
|
|
27
|
+
sections.push(`${JSON_HINT} Return an object or array.`);
|
|
28
28
|
sections.push(opts.prompt);
|
|
29
29
|
return sections.join('\n\n');
|
|
30
30
|
}
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
if (fenced)
|
|
36
|
-
candidates.push(fenced[1].trim());
|
|
37
|
-
const open = text.search(/[{[]/);
|
|
38
|
-
const close = Math.max(text.lastIndexOf('}'), text.lastIndexOf(']'));
|
|
39
|
-
if (open >= 0 && close > open)
|
|
40
|
-
candidates.push(text.slice(open, close + 1));
|
|
41
|
-
let lastError = 'empty response';
|
|
42
|
-
for (const c of candidates) {
|
|
43
|
-
try {
|
|
44
|
-
const v = fromJson(JSON.parse(c));
|
|
45
|
-
if (isDict(v) || isList(v))
|
|
46
|
-
return v;
|
|
47
|
-
lastError = 'response is JSON but not an object or array';
|
|
48
|
-
}
|
|
49
|
-
catch (e) {
|
|
50
|
-
lastError = e.message;
|
|
51
|
-
}
|
|
31
|
+
export function parseJsonResponse(text, objectOnly = false) {
|
|
32
|
+
let value;
|
|
33
|
+
try {
|
|
34
|
+
value = fromJson(JSON.parse(text));
|
|
52
35
|
}
|
|
53
|
-
|
|
36
|
+
catch (e) {
|
|
37
|
+
if (!(e instanceof SyntaxError))
|
|
38
|
+
throw e;
|
|
39
|
+
throw new Failure('invalid_format', `LLM response is not valid JSON: ${e.message}`);
|
|
40
|
+
}
|
|
41
|
+
if (objectOnly && !isDict(value) && !isList(value)) {
|
|
42
|
+
throw new Failure('invalid_format', 'LLM response must be a JSON object or array');
|
|
43
|
+
}
|
|
44
|
+
return value;
|
|
54
45
|
}
|
package/dist/logger.d.ts
CHANGED
|
@@ -9,4 +9,7 @@ export declare class Logger {
|
|
|
9
9
|
warn(msg: string): void;
|
|
10
10
|
info(msg: string): void;
|
|
11
11
|
debug(msg: string): void;
|
|
12
|
+
/** LLM transcripts are visible at every non-silent level, without labeling them as errors. */
|
|
13
|
+
llm(msg: string): void;
|
|
14
|
+
deterministic(msg: string): void;
|
|
12
15
|
}
|
package/dist/logger.js
CHANGED
|
@@ -24,4 +24,11 @@ export class Logger {
|
|
|
24
24
|
debug(msg) {
|
|
25
25
|
this.emit('debug', 'debug', msg);
|
|
26
26
|
}
|
|
27
|
+
/** LLM transcripts are visible at every non-silent level, without labeling them as errors. */
|
|
28
|
+
llm(msg) {
|
|
29
|
+
this.emit('error', 'llm', msg);
|
|
30
|
+
}
|
|
31
|
+
deterministic(msg) {
|
|
32
|
+
this.emit('error', 'deterministic', msg);
|
|
33
|
+
}
|
|
27
34
|
}
|
package/dist/mcp.d.ts
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { MCPServerConfig } from '@github/copilot-sdk';
|
|
2
|
+
import type { Restrictions } from './restrict.js';
|
|
3
|
+
export type McpServers = Record<string, MCPServerConfig>;
|
|
4
|
+
/** Loads the supported VS Code mcp.json subset; never executes commands or connects to servers. */
|
|
5
|
+
export declare function loadMcpConfig(file: string, cwd: string): McpServers;
|
|
6
|
+
export declare function filterMcpServers(servers: McpServers, restrictions: Restrictions): McpServers;
|
|
7
|
+
export declare function configuredMcpTool(servers: McpServers, server: string, tool: string): boolean;
|
package/dist/mcp.js
ADDED
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import * as fs from 'node:fs';
|
|
2
|
+
import * as path from 'node:path';
|
|
3
|
+
const object = (v) => typeof v === 'object' && v !== null && !Array.isArray(v);
|
|
4
|
+
/** Loads the supported VS Code mcp.json subset; never executes commands or connects to servers. */
|
|
5
|
+
export function loadMcpConfig(file, cwd) {
|
|
6
|
+
const fail = (message) => { throw new Error(`MCP config '${file}': ${message}`); };
|
|
7
|
+
let config;
|
|
8
|
+
try {
|
|
9
|
+
config = JSON.parse(fs.readFileSync(file, 'utf8'));
|
|
10
|
+
}
|
|
11
|
+
catch (e) {
|
|
12
|
+
return fail(`cannot read JSON: ${e.message}`);
|
|
13
|
+
}
|
|
14
|
+
if (!object(config) || !object(config.servers))
|
|
15
|
+
return fail("expected an object with a 'servers' object");
|
|
16
|
+
if (config.inputs !== undefined)
|
|
17
|
+
return fail("'inputs' are not supported; use environment variables instead");
|
|
18
|
+
const expand = (value) => value.replace(/\$\{([^}]+)\}/g, (_, key) => {
|
|
19
|
+
if (key === 'workspaceFolder')
|
|
20
|
+
return cwd;
|
|
21
|
+
if (key.startsWith('env:')) {
|
|
22
|
+
const name = key.slice(4);
|
|
23
|
+
const value = process.env[name];
|
|
24
|
+
if (value !== undefined)
|
|
25
|
+
return value;
|
|
26
|
+
return fail(`environment variable '${name}' is not set`);
|
|
27
|
+
}
|
|
28
|
+
return fail(`unsupported variable '\${${key}}'`);
|
|
29
|
+
});
|
|
30
|
+
const strings = (value, field) => {
|
|
31
|
+
if (value === undefined)
|
|
32
|
+
return undefined;
|
|
33
|
+
if (!object(value))
|
|
34
|
+
return fail(`${field} must be an object of strings`);
|
|
35
|
+
const entries = Object.entries(value).map(([key, val]) => {
|
|
36
|
+
if (typeof val !== 'string')
|
|
37
|
+
return fail(`${field}.${key} must be a string`);
|
|
38
|
+
return [key, expand(val)];
|
|
39
|
+
});
|
|
40
|
+
return Object.fromEntries(entries);
|
|
41
|
+
};
|
|
42
|
+
const servers = Object.create(null);
|
|
43
|
+
for (const [name, raw] of Object.entries(config.servers)) {
|
|
44
|
+
if (!/^[A-Za-z0-9_.-]+$/.test(name))
|
|
45
|
+
return fail(`invalid server name '${name}'`);
|
|
46
|
+
if (!object(raw))
|
|
47
|
+
return fail(`server '${name}' must be an object`);
|
|
48
|
+
const type = raw.type ?? 'stdio';
|
|
49
|
+
const local = type === 'stdio';
|
|
50
|
+
if (!local && type !== 'http' && type !== 'sse')
|
|
51
|
+
return fail(`server '${name}' has unsupported type '${String(type)}'`);
|
|
52
|
+
const fields = local ? ['type', 'command', 'args', 'env', 'cwd', 'tools', 'timeout'] : ['type', 'url', 'headers', 'tools', 'timeout'];
|
|
53
|
+
for (const field of Object.keys(raw)) {
|
|
54
|
+
if (!fields.includes(field))
|
|
55
|
+
return fail(`server '${name}' has unsupported field '${field}'`);
|
|
56
|
+
}
|
|
57
|
+
if (raw.tools !== undefined && (!Array.isArray(raw.tools) || raw.tools.some((t) => typeof t !== 'string' || !/^(?:\*|[A-Za-z0-9_.-]+)$/.test(t)))) {
|
|
58
|
+
return fail(`server '${name}': tools must be an array of tool names or '*'`);
|
|
59
|
+
}
|
|
60
|
+
if (raw.timeout !== undefined && (typeof raw.timeout !== 'number' || !Number.isFinite(raw.timeout) || raw.timeout <= 0)) {
|
|
61
|
+
return fail(`server '${name}': timeout must be a positive number of milliseconds`);
|
|
62
|
+
}
|
|
63
|
+
const common = { tools: raw.tools, timeout: raw.timeout };
|
|
64
|
+
if (local) {
|
|
65
|
+
if (typeof raw.command !== 'string' || raw.command.trim() === '')
|
|
66
|
+
return fail(`server '${name}': command must be a non-empty string`);
|
|
67
|
+
if (raw.args !== undefined && (!Array.isArray(raw.args) || raw.args.some((v) => typeof v !== 'string'))) {
|
|
68
|
+
return fail(`server '${name}': args must be an array of strings`);
|
|
69
|
+
}
|
|
70
|
+
if (raw.cwd !== undefined && typeof raw.cwd !== 'string')
|
|
71
|
+
return fail(`server '${name}': cwd must be a string`);
|
|
72
|
+
servers[name] = {
|
|
73
|
+
...common, type: 'stdio', command: expand(raw.command),
|
|
74
|
+
args: raw.args?.map(expand),
|
|
75
|
+
env: strings(raw.env, `${name}.env`),
|
|
76
|
+
workingDirectory: raw.cwd === undefined ? cwd : path.resolve(cwd, expand(raw.cwd)),
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
else {
|
|
80
|
+
if (typeof raw.url !== 'string')
|
|
81
|
+
return fail(`server '${name}': url must be a string`);
|
|
82
|
+
const url = expand(raw.url);
|
|
83
|
+
let parsed;
|
|
84
|
+
try {
|
|
85
|
+
parsed = new URL(url);
|
|
86
|
+
}
|
|
87
|
+
catch {
|
|
88
|
+
return fail(`server '${name}': invalid URL`);
|
|
89
|
+
}
|
|
90
|
+
if (!['http:', 'https:'].includes(parsed.protocol))
|
|
91
|
+
return fail(`server '${name}': URL must use http or https`);
|
|
92
|
+
servers[name] = { ...common, type, url, headers: strings(raw.headers, `${name}.headers`) };
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
return servers;
|
|
96
|
+
}
|
|
97
|
+
export function filterMcpServers(servers, restrictions) {
|
|
98
|
+
return Object.fromEntries(Object.entries(servers).flatMap(([name, config]) => {
|
|
99
|
+
const tools = restrictions.mcpTools(name, config.tools);
|
|
100
|
+
return tools.length ? [[name, { ...config, tools }]] : [];
|
|
101
|
+
}));
|
|
102
|
+
}
|
|
103
|
+
export function configuredMcpTool(servers, server, tool) {
|
|
104
|
+
if (!Object.hasOwn(servers, server))
|
|
105
|
+
return false;
|
|
106
|
+
const tools = servers[server].tools;
|
|
107
|
+
return tools === undefined || tools.includes('*') || tools.includes(tool);
|
|
108
|
+
}
|
package/dist/parser.d.ts
CHANGED
|
@@ -1,13 +1,3 @@
|
|
|
1
|
-
import type { Program
|
|
2
|
-
export declare const
|
|
3
|
-
/** Fixed modifier schema per command; anything else is a syntax error. */
|
|
4
|
-
export declare const MODIFIER_SCHEMA: Record<'?' | 'think', Record<string, readonly (string | boolean)[]>>;
|
|
5
|
-
/**
|
|
6
|
-
* Tool parameter schema: the allowed names, and the `required` ones that let the runtime run the tool itself.
|
|
7
|
-
* Every parameter is optional in a script; the LLM supplies the missing required ones when it calls the tool.
|
|
8
|
-
*/
|
|
9
|
-
export declare const TOOL_SCHEMA: Record<ToolName, {
|
|
10
|
-
params: string[];
|
|
11
|
-
required: string[];
|
|
12
|
-
}>;
|
|
1
|
+
import type { Program } from './ast.js';
|
|
2
|
+
export declare const MODIFIER_NAMES: readonly ['len', 'fmt', 'reasoning', 'show', 'schema', 'repairs', 'model'];
|
|
13
3
|
export declare function parse(source: string): Program;
|