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.
@@ -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, parseObjectResponse } from './llm.js';
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
- const snapshot = {};
57
- for (const [k, v] of this.globals.vars)
58
- if (!isFunction(v))
59
- snapshot[k] = v;
60
- this.store.save(snapshot);
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
- await this.execLet(s.target, s.op, s.value, env);
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, { vars: new Map(), parent: env });
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 execLet(target, op, valueExpr, env) {
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
- // Binds in the current scope, which is what lets a nested block shadow an outer variable.
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
- const prompt = str(await this.eval(e.prompt, env));
321
- const text = await this.callLlm({ kind: 'ask', prompt, mods: e.mods });
322
- return e.mods.fmt === 'object' ? parseObjectResponse(text) : text;
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(c) {
489
+ async callLlm(req) {
485
490
  const { logger, backend } = this.opts;
486
- const req = {
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 ${c.kind} call: ${JSON.stringify(res.effective)} (${Date.now() - started} ms)`);
507
- logger.debug(`llm response:\n${res.text}`);
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 evalTool(tool, paramExprs, instrExpr, env) {
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
- for (const [k, ex] of Object.entries(paramExprs)) {
521
- const v = await this.eval(ex, env);
522
- if (k !== 'content' && typeof v !== 'string')
523
- throw runtimeError(`${tool}: parameter '${k}' must be a string, got ${typeName(v)}`);
524
- params[k] = str(v);
525
- }
526
- const instructions = str(await this.eval(instrExpr, env));
527
- const host = new ToolHost(this.restrictions, this.cwd);
528
- // When the script leaves a required parameter out, the LLM calls the tool itself with the fixed ones pinned.
529
- if (TOOL_SPECS[tool].required.some((p) => !(p in params))) {
530
- if (tool === 'write_file' && params.path !== undefined)
531
- this.restrictions.checkPath(params.path, 'write');
532
- const fixed = Object.keys(params).length ? ` with these fixed parameters (set by the script): ${JSON.stringify(params)}` : '';
533
- return this.callLlm({
534
- kind: 'tool',
535
- prompt: `Use the ${tool} tool${fixed}. Instructions: ${instructions}`,
536
- pinned: { tool, params },
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, ToolName } from './ast.js';
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: 'enforce' | 'forbid';
7
+ kind: 'instruct' | 'avoid';
8
8
  text: string;
9
9
  }
10
10
  export interface LlmRequest {
11
- kind: 'ask' | 'think' | 'tool';
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
- /** Parses an `fmt='object'` response; anything that is not a JSON object or array is `invalid_format`. */
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 FMT_OBJECT_HINT = 'Respond with ONLY a valid JSON value (an object unless the task clearly asks for a list). No prose, no markdown, no code fences.';
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('Rules you must follow strictly:\n' +
17
- opts.constraints.map((c) => `- You ${c.kind === 'enforce' ? 'MUST' : 'MUST NOT'}: ${c.text}`).join('\n'));
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(FMT_OBJECT_HINT);
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
- /** Parses an `fmt='object'` response; anything that is not a JSON object or array is `invalid_format`. */
32
- export function parseObjectResponse(text) {
33
- const candidates = [text.trim()];
34
- const fenced = /^```[a-zA-Z]*\s*\n([\s\S]*?)\n?```$/.exec(text.trim());
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
- throw new Failure('invalid_format', `LLM response is not valid JSON: ${lastError}`);
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, ToolName } from './ast.js';
2
- export declare const TOOL_NAMES: readonly ToolName[];
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;