skillscript-runtime 0.5.0 → 0.7.2
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 +105 -35
- package/dist/bootstrap.d.ts.map +1 -1
- package/dist/bootstrap.js +15 -0
- package/dist/bootstrap.js.map +1 -1
- package/dist/compile.d.ts.map +1 -1
- package/dist/compile.js +42 -2
- package/dist/compile.js.map +1 -1
- package/dist/connectors/config.d.ts.map +1 -1
- package/dist/connectors/config.js +10 -0
- package/dist/connectors/config.js.map +1 -1
- package/dist/connectors/local-model-mcp.d.ts +9 -0
- package/dist/connectors/local-model-mcp.d.ts.map +1 -0
- package/dist/connectors/local-model-mcp.js +73 -0
- package/dist/connectors/local-model-mcp.js.map +1 -0
- package/dist/connectors/memory-store-mcp.d.ts +9 -0
- package/dist/connectors/memory-store-mcp.d.ts.map +1 -0
- package/dist/connectors/memory-store-mcp.js +94 -0
- package/dist/connectors/memory-store-mcp.js.map +1 -0
- package/dist/help-content.d.ts.map +1 -1
- package/dist/help-content.js +316 -238
- package/dist/help-content.js.map +1 -1
- package/dist/lint.d.ts.map +1 -1
- package/dist/lint.js +296 -53
- package/dist/lint.js.map +1 -1
- package/dist/parser.d.ts +48 -3
- package/dist/parser.d.ts.map +1 -1
- package/dist/parser.js +377 -31
- package/dist/parser.js.map +1 -1
- package/dist/runtime.d.ts.map +1 -1
- package/dist/runtime.js +93 -16
- package/dist/runtime.js.map +1 -1
- package/examples/classify-support-ticket.skill.md +13 -13
- package/examples/cut-release-tag.skill.md +14 -14
- package/examples/doc-qa-with-citations.skill.md +3 -3
- package/examples/feedback-sentiment-scan.skill.md +13 -13
- package/examples/hello.skill.md +2 -2
- package/examples/hello.skill.provenance.json +1 -1
- package/examples/morning-brief.skill.md +6 -6
- package/examples/queue-length-monitor.skill.md +4 -4
- package/examples/service-health-watch.skill.md +7 -7
- package/examples/youtrack-morning-sweep.skill.md +4 -4
- package/package.json +1 -1
- package/scaffold/connectors.json +15 -13
package/dist/parser.js
CHANGED
|
@@ -1,6 +1,23 @@
|
|
|
1
1
|
// Source text → AST. The parser recognizes the full v1 grammar but performs
|
|
2
2
|
// no resolution against external state. Semantic analysis (variable resolution,
|
|
3
3
|
// data-skill inlining, topo-sort) lives in compile.ts.
|
|
4
|
+
/**
|
|
5
|
+
* v0.7.0 — runtime-intrinsic function-call names. Closed set of ops the
|
|
6
|
+
* language implements directly (no MCP dispatch). Function-call grammar:
|
|
7
|
+
* `verb(kwarg=value, ...) [-> BINDING]`.
|
|
8
|
+
*
|
|
9
|
+
* Anything else with function-call shape is rejected by parser with a
|
|
10
|
+
* remediation pointing at `$ tool args -> R` for MCP dispatch.
|
|
11
|
+
*/
|
|
12
|
+
export const RUNTIME_INTRINSIC_FN_NAMES = [
|
|
13
|
+
"emit", // → ! (output to skill consumer)
|
|
14
|
+
"ask", // → ?? (prompt user)
|
|
15
|
+
"inline", // → & (compile-time skill composition)
|
|
16
|
+
"execute_skill", // → $ execute_skill (runtime skill invocation)
|
|
17
|
+
"shell", // → @ (local subprocess)
|
|
18
|
+
"file_read", // new — read file contents at runtime
|
|
19
|
+
"file_write", // new — write file contents at runtime
|
|
20
|
+
];
|
|
4
21
|
/**
|
|
5
22
|
* Case-insensitive accept, canonical-form return. The `allowed` list defines
|
|
6
23
|
* canonical form (the first match for any case-folded input). Returns `null`
|
|
@@ -21,12 +38,17 @@ const REQUIRES_LINE = /^(user-var|system-var):([A-Za-z0-9_-]+)\s*(?:→|->)\s*([
|
|
|
21
38
|
const CAPABILITY_TOKEN = /^[a-z_][a-z0-9_]*\.[a-z_][a-z0-9_]*$/;
|
|
22
39
|
/** `&` op: `& skill-name [arg=value ...] [-> VARNAME]`. Skill names follow the same charset as filesystem-safe identifiers (alphanumeric, hyphen, underscore). */
|
|
23
40
|
const AMPERSAND_OP_REGEX = /^&\s+([A-Za-z0-9][\w-]*)\s*(.*?)(?:\s*->\s*([A-Za-z_]\w*))?(?:\s+\(fallback\s*:\s*(.+?)\))?\s*$/s;
|
|
24
|
-
|
|
41
|
+
// v0.7.2 — `(.*)` widened to `([\s\S]*)` so multi-line triple-quote
|
|
42
|
+
// (`"""..."""`) values fold into a single $set value capture. Without the
|
|
43
|
+
// dotall-equivalent, the `.` excludes newlines and the regex stops at the
|
|
44
|
+
// first line's `"""` opening.
|
|
45
|
+
const SET_OP_REGEX = /^\$set\s+([A-Za-z_]\w*)\s*=\s*([\s\S]*)$/;
|
|
25
46
|
// v0.3.0 accumulator. `$append VAR <value>` — single-value append to a
|
|
26
47
|
// list-typed VAR. Form: `$append IDENT <space> <value>`. Mirrors $set
|
|
27
48
|
// in shape (var name + value) but the runtime mutates an outer-scope
|
|
28
49
|
// list rather than overwriting. See spec memory `9d6079bb` + `442cf4bb`.
|
|
29
|
-
|
|
50
|
+
// v0.7.2 — `(.+)` widened to `([\s\S]+)` for same multi-line reason.
|
|
51
|
+
const APPEND_OP_REGEX = /^\$append\s+([A-Za-z_]\w*)\s+([\s\S]+)$/;
|
|
30
52
|
const FOREACH_OP_REGEX = /^foreach\s+([A-Za-z_]\w*)\s+in\s+(.+?):\s*$/;
|
|
31
53
|
const IF_OP_REGEX = /^if\s+(.+?):\s*$/;
|
|
32
54
|
const ELIF_OP_REGEX = /^elif\s+(.+?):\s*$/;
|
|
@@ -53,20 +75,21 @@ const LOCAL_MODEL_OP_REGEX = /^~\s+(.+?)\s+->\s+([A-Za-z_]\w*)(?:\s+\(fallback\s
|
|
|
53
75
|
const MCP_CONNECTOR_PREFIX = /^([a-z_][a-z0-9_-]*)\.(?=[A-Za-z_])([\s\S]*)$/;
|
|
54
76
|
// Narrow v1 condition grammar.
|
|
55
77
|
// v0.3.4: filter chain support — each `(REF)(|filter)?` became `(REF)(|filter)*`
|
|
56
|
-
// to match `substituteRuntime`'s chain capture
|
|
57
|
-
//
|
|
58
|
-
//
|
|
59
|
-
|
|
78
|
+
// to match `substituteRuntime`'s chain capture. Closes the recurring "filter
|
|
79
|
+
// chain works in substitution but not conditions" gap named in dev-log §14.
|
|
80
|
+
// v0.7.0: REF_PATTERN accepts both `$(REF)` (legacy) and `${REF}` (canonical).
|
|
81
|
+
// Both forms have identical semantics; migration tool rewrites old → new.
|
|
82
|
+
const REF_PATTERN = "\\$(?:\\([A-Za-z_]\\w*(?:\\.[A-Za-z_]\\w*)*(?:\\s*\\|\\s*[A-Za-z_]\\w*(?:\\s*:\\s*\"[^\"]*\")?)*\\)|\\{[A-Za-z_]\\w*(?:\\.[A-Za-z_]\\w*)*(?:\\s*\\|\\s*[A-Za-z_]\\w*(?:\\s*:\\s*\"[^\"]*\")?)*\\})";
|
|
83
|
+
const REF_PATTERN_NO_FILTER = "\\$(?:\\([A-Za-z_]\\w*(?:\\.[A-Za-z_]\\w*)*\\)|\\{[A-Za-z_]\\w*(?:\\.[A-Za-z_]\\w*)*\\})";
|
|
84
|
+
const COND_TRUTHY = new RegExp(`^\\s*${REF_PATTERN}\\s*$`);
|
|
60
85
|
/** `$(REF) ==/!= "literal"` — ref-vs-string equality. Filter chain on the ref side. */
|
|
61
|
-
const COND_EQ =
|
|
86
|
+
const COND_EQ = new RegExp(`^\\s*${REF_PATTERN}\\s*(?:==|!=)\\s*"[^"]*"\\s*$`);
|
|
62
87
|
/**
|
|
63
88
|
* `$(REF) ==/!= $(REF)` — ref-vs-ref equality. Extended 2026-05-21 per
|
|
64
|
-
* language reference §5
|
|
65
|
-
*
|
|
66
|
-
* the natural change-detection pattern). Filter chain + dotted field
|
|
67
|
-
* access permitted on either side.
|
|
89
|
+
* language reference §5. Filter chain + dotted field access permitted on
|
|
90
|
+
* either side.
|
|
68
91
|
*/
|
|
69
|
-
const COND_EQ_REF =
|
|
92
|
+
const COND_EQ_REF = new RegExp(`^\\s*${REF_PATTERN}\\s*(?:==|!=)\\s*${REF_PATTERN}\\s*$`);
|
|
70
93
|
/**
|
|
71
94
|
* `$(REF) </>/<=/>= "literal"` and `$(REF) </>/<=/>= $(REF)` — numeric
|
|
72
95
|
* comparison. v0.2.5 addition per the orchestration carve-out: comparison
|
|
@@ -75,9 +98,9 @@ const COND_EQ_REF = /^\s*\$\([A-Za-z_]\w*(?:\.[A-Za-z_]\w*)*(?:\s*\|\s*[A-Za-z_]
|
|
|
75
98
|
* chain + dotted field access permitted on either side, matching
|
|
76
99
|
* EQ/EQ_REF shape.
|
|
77
100
|
*/
|
|
78
|
-
const COND_CMP =
|
|
79
|
-
const COND_CMP_REF =
|
|
80
|
-
const COND_IN =
|
|
101
|
+
const COND_CMP = new RegExp(`^\\s*${REF_PATTERN}\\s*(?:<=|>=|<|>)\\s*"[^"]*"\\s*$`);
|
|
102
|
+
const COND_CMP_REF = new RegExp(`^\\s*${REF_PATTERN}\\s*(?:<=|>=|<|>)\\s*${REF_PATTERN}\\s*$`);
|
|
103
|
+
const COND_IN = new RegExp(`^\\s*${REF_PATTERN}\\s+(?:not\\s+)?in\\s+${REF_PATTERN_NO_FILTER}\\s*$`);
|
|
81
104
|
function validateCondition(cond) {
|
|
82
105
|
return validateCompoundCondition(cond.trim());
|
|
83
106
|
}
|
|
@@ -161,20 +184,20 @@ function stripOuterCondParens(cond) {
|
|
|
161
184
|
}
|
|
162
185
|
return trimmed.slice(1, -1).trim();
|
|
163
186
|
}
|
|
164
|
-
/** Detects `$(REF) = "literal"` —
|
|
165
|
-
const SINGLE_EQ_IN_COND =
|
|
187
|
+
/** Detects `$(REF) = "literal"` or `${REF} = "literal"` — single `=` in condition position. */
|
|
188
|
+
const SINGLE_EQ_IN_COND = /\$(?:\([^)]+\)|\{[^}]+\})\s*=(?!=)\s*"[^"]*"/;
|
|
166
189
|
/**
|
|
167
|
-
* If the condition contains `$(REF) = "..."` (single `=`),
|
|
168
|
-
* diagnostic suggesting `==`. Returns the diagnostic string
|
|
169
|
-
* `null` otherwise.
|
|
170
|
-
* this surfaces the JS-shaped-bug pattern as a specific error rather than
|
|
171
|
-
* the generic "unsupported condition" fallback.
|
|
190
|
+
* If the condition contains `$(REF) = "..."` or `${REF} = "..."` (single `=`),
|
|
191
|
+
* emit a specific diagnostic suggesting `==`. Returns the diagnostic string
|
|
192
|
+
* when matched, `null` otherwise.
|
|
172
193
|
*/
|
|
173
194
|
function detectSingleEqualsInCondition(cond) {
|
|
174
195
|
const m = SINGLE_EQ_IN_COND.exec(cond);
|
|
175
196
|
if (m === null)
|
|
176
197
|
return null;
|
|
177
|
-
const fixed = cond
|
|
198
|
+
const fixed = cond
|
|
199
|
+
.replace(/\$\(([^)]+)\)\s*=(?!=)\s*"([^"]*)"/, '$($1) == "$2"')
|
|
200
|
+
.replace(/\$\{([^}]+)\}\s*=(?!=)\s*"([^"]*)"/, '${$1} == "$2"');
|
|
178
201
|
return `\`=\` is not valid in a condition; use \`==\` for equality. rewrite as: \`${fixed}\``;
|
|
179
202
|
}
|
|
180
203
|
/**
|
|
@@ -299,10 +322,22 @@ function splitVarsLine(value) {
|
|
|
299
322
|
function foldQuotedContinuations(lines) {
|
|
300
323
|
const out = [];
|
|
301
324
|
let buffer = null;
|
|
325
|
+
// v0.7.2 — triple-quote folding engages regardless of op kind. Three
|
|
326
|
+
// consecutive `"` chars don't accidentally appear in natural English
|
|
327
|
+
// prose, so the "phantom-scope from apostrophe" risk that gates single-
|
|
328
|
+
// quote folding to kwarg-bearing ops doesn't apply. inTripleAccum tracks
|
|
329
|
+
// which fold mode the buffer is in so we know which closing condition
|
|
330
|
+
// to test against.
|
|
331
|
+
let inTripleAccum = false;
|
|
302
332
|
for (const line of lines) {
|
|
303
333
|
if (buffer === null) {
|
|
304
|
-
if (
|
|
334
|
+
if (hasUnclosedTriple(line)) {
|
|
335
|
+
buffer = line;
|
|
336
|
+
inTripleAccum = true;
|
|
337
|
+
}
|
|
338
|
+
else if (isKwargBearingLine(line) && hasUnclosedQuote(line)) {
|
|
305
339
|
buffer = line;
|
|
340
|
+
inTripleAccum = false;
|
|
306
341
|
}
|
|
307
342
|
else {
|
|
308
343
|
out.push(line);
|
|
@@ -310,9 +345,11 @@ function foldQuotedContinuations(lines) {
|
|
|
310
345
|
}
|
|
311
346
|
else {
|
|
312
347
|
buffer = buffer + "\n" + line;
|
|
313
|
-
|
|
348
|
+
const closed = inTripleAccum ? !hasUnclosedTriple(buffer) : !hasUnclosedQuote(buffer);
|
|
349
|
+
if (closed) {
|
|
314
350
|
out.push(buffer);
|
|
315
351
|
buffer = null;
|
|
352
|
+
inTripleAccum = false;
|
|
316
353
|
}
|
|
317
354
|
}
|
|
318
355
|
}
|
|
@@ -344,6 +381,24 @@ function hasUnclosedQuote(text) {
|
|
|
344
381
|
}
|
|
345
382
|
return inDouble || inSingle;
|
|
346
383
|
}
|
|
384
|
+
/**
|
|
385
|
+
* v0.7.2 — true if `text` contains an odd number of `"""` triple-quote
|
|
386
|
+
* delimiters (i.e., an unterminated triple-quote literal). Scans the
|
|
387
|
+
* string counting non-overlapping `"""` occurrences.
|
|
388
|
+
*/
|
|
389
|
+
function hasUnclosedTriple(text) {
|
|
390
|
+
let count = 0;
|
|
391
|
+
for (let i = 0; i <= text.length - 3;) {
|
|
392
|
+
if (text[i] === '"' && text[i + 1] === '"' && text[i + 2] === '"') {
|
|
393
|
+
count++;
|
|
394
|
+
i += 3;
|
|
395
|
+
}
|
|
396
|
+
else {
|
|
397
|
+
i++;
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
return count % 2 === 1;
|
|
401
|
+
}
|
|
347
402
|
/**
|
|
348
403
|
* Split a `# Triggers:` header value into separate trigger entries.
|
|
349
404
|
*
|
|
@@ -366,21 +421,56 @@ function splitTriggersLine(value) {
|
|
|
366
421
|
return value.split(splitRegex);
|
|
367
422
|
}
|
|
368
423
|
/**
|
|
369
|
-
* `$set`
|
|
370
|
-
* - Matching outer `"..."
|
|
424
|
+
* `$set` / `>` / `~` / kwarg arg-value quote-strip rules:
|
|
425
|
+
* - Matching outer `"..."`: stripped + interpret \n/\t/\\/\" escapes (v0.7.2).
|
|
426
|
+
* - Matching outer `'...'`: stripped, no escape interpretation (literal).
|
|
371
427
|
* - Mismatched / unquoted: verbatim, trailing whitespace trimmed.
|
|
428
|
+
*
|
|
429
|
+
* v0.7.2 — escape interpretation in double-quoted strings closes the R4
|
|
430
|
+
* cold-author footgun where `$set X = "line1\nline2"` stored literal
|
|
431
|
+
* `\n` bytes. Bash/Python/JS/Go all interpret these escapes; skillscript
|
|
432
|
+
* not interpreting was the surprise. Single-quoted strings reserved for
|
|
433
|
+
* v0.8+ explicit literal-pass-through semantics if a real use case
|
|
434
|
+
* surfaces.
|
|
372
435
|
*/
|
|
373
436
|
export function processSetValue(raw) {
|
|
374
437
|
const trimmed = raw.replace(/\s+$/, "");
|
|
438
|
+
// v0.7.2 — triple-quote `"""..."""` multi-line literal. Check before the
|
|
439
|
+
// single-quote pair check; a value like `"""abc"""` shouldn't be shortened
|
|
440
|
+
// to `""abc""` via the `"..."` branch.
|
|
441
|
+
if (trimmed.length >= 6 && trimmed.startsWith('"""') && trimmed.endsWith('"""')) {
|
|
442
|
+
return interpretDoubleQuotedEscapes(trimmed.slice(3, -3));
|
|
443
|
+
}
|
|
375
444
|
if (trimmed.length >= 2) {
|
|
376
445
|
const first = trimmed[0];
|
|
377
446
|
const last = trimmed[trimmed.length - 1];
|
|
378
|
-
if (
|
|
447
|
+
if (first === '"' && last === '"') {
|
|
448
|
+
return interpretDoubleQuotedEscapes(trimmed.slice(1, -1));
|
|
449
|
+
}
|
|
450
|
+
if (first === "'" && last === "'") {
|
|
379
451
|
return trimmed.slice(1, -1);
|
|
380
452
|
}
|
|
381
453
|
}
|
|
382
454
|
return trimmed;
|
|
383
455
|
}
|
|
456
|
+
/**
|
|
457
|
+
* v0.7.2 — interpret common escape sequences in double-quoted string
|
|
458
|
+
* literals: `\n` → newline, `\t` → tab, `\\` → literal backslash,
|
|
459
|
+
* `\"` → literal quote. Other `\X` sequences pass through verbatim
|
|
460
|
+
* (no over-eager interpretation; future v0.8+ may add `\r` / `\0` /
|
|
461
|
+
* etc. if cold-author demand surfaces).
|
|
462
|
+
*/
|
|
463
|
+
function interpretDoubleQuotedEscapes(s) {
|
|
464
|
+
return s.replace(/\\(["\\nt])/g, (match, ch) => {
|
|
465
|
+
switch (ch) {
|
|
466
|
+
case '"': return '"';
|
|
467
|
+
case "\\": return "\\";
|
|
468
|
+
case "n": return "\n";
|
|
469
|
+
case "t": return "\t";
|
|
470
|
+
default: return match;
|
|
471
|
+
}
|
|
472
|
+
});
|
|
473
|
+
}
|
|
384
474
|
/**
|
|
385
475
|
* Tokenize whitespace-separated `key=value` pairs, respecting matching
|
|
386
476
|
* single/double quotes and `[...]` brackets.
|
|
@@ -389,15 +479,35 @@ export function tokenizeKeywordArgs(input) {
|
|
|
389
479
|
const tokens = [];
|
|
390
480
|
let current = "";
|
|
391
481
|
let inQuote = null;
|
|
482
|
+
let inTriple = false; // v0.7.2 — triple-quote `"""..."""` state
|
|
392
483
|
let bracketDepth = 0;
|
|
393
484
|
for (let i = 0; i < input.length; i++) {
|
|
394
485
|
const ch = input[i];
|
|
486
|
+
// Triple-quote state takes precedence: inside `"""..."""`, single `"`
|
|
487
|
+
// and whitespace are content, not delimiters.
|
|
488
|
+
if (inTriple) {
|
|
489
|
+
current += ch;
|
|
490
|
+
if (ch === '"' && input[i + 1] === '"' && input[i + 2] === '"') {
|
|
491
|
+
current += input[i + 1];
|
|
492
|
+
current += input[i + 2];
|
|
493
|
+
i += 2;
|
|
494
|
+
inTriple = false;
|
|
495
|
+
}
|
|
496
|
+
continue;
|
|
497
|
+
}
|
|
395
498
|
if (inQuote) {
|
|
396
499
|
current += ch;
|
|
397
500
|
if (ch === inQuote)
|
|
398
501
|
inQuote = null;
|
|
399
502
|
continue;
|
|
400
503
|
}
|
|
504
|
+
// Check for triple-quote OPEN before single-quote (greedy match).
|
|
505
|
+
if (ch === '"' && input[i + 1] === '"' && input[i + 2] === '"') {
|
|
506
|
+
current += '"""';
|
|
507
|
+
i += 2;
|
|
508
|
+
inTriple = true;
|
|
509
|
+
continue;
|
|
510
|
+
}
|
|
401
511
|
if (ch === '"' || ch === "'") {
|
|
402
512
|
current += ch;
|
|
403
513
|
inQuote = ch;
|
|
@@ -425,6 +535,99 @@ export function tokenizeKeywordArgs(input) {
|
|
|
425
535
|
tokens.push(current);
|
|
426
536
|
return tokens;
|
|
427
537
|
}
|
|
538
|
+
/**
|
|
539
|
+
* v0.7.0 — paren-balanced extraction. Given text and the index of an opening
|
|
540
|
+
* `(`, return the substring between matched parens plus the index of the
|
|
541
|
+
* closing `)`. Quote-aware (skips parens inside `"..."`/`'...'`). Returns
|
|
542
|
+
* null on unbalanced parens.
|
|
543
|
+
*/
|
|
544
|
+
function extractParenBody(text, openIdx) {
|
|
545
|
+
if (text[openIdx] !== "(")
|
|
546
|
+
return null;
|
|
547
|
+
let depth = 1;
|
|
548
|
+
let inQuote = null;
|
|
549
|
+
let inTriple = false; // v0.7.2 — `"""..."""` state
|
|
550
|
+
for (let i = openIdx + 1; i < text.length; i++) {
|
|
551
|
+
const ch = text[i];
|
|
552
|
+
if (inTriple) {
|
|
553
|
+
if (ch === '"' && text[i + 1] === '"' && text[i + 2] === '"') {
|
|
554
|
+
i += 2;
|
|
555
|
+
inTriple = false;
|
|
556
|
+
}
|
|
557
|
+
continue;
|
|
558
|
+
}
|
|
559
|
+
if (inQuote !== null) {
|
|
560
|
+
if (ch === inQuote)
|
|
561
|
+
inQuote = null;
|
|
562
|
+
continue;
|
|
563
|
+
}
|
|
564
|
+
if (ch === '"' && text[i + 1] === '"' && text[i + 2] === '"') {
|
|
565
|
+
i += 2;
|
|
566
|
+
inTriple = true;
|
|
567
|
+
continue;
|
|
568
|
+
}
|
|
569
|
+
if (ch === '"' || ch === "'") {
|
|
570
|
+
inQuote = ch;
|
|
571
|
+
continue;
|
|
572
|
+
}
|
|
573
|
+
if (ch === "(")
|
|
574
|
+
depth++;
|
|
575
|
+
else if (ch === ")") {
|
|
576
|
+
depth--;
|
|
577
|
+
if (depth === 0)
|
|
578
|
+
return { body: text.slice(openIdx + 1, i), endIdx: i };
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
return null;
|
|
582
|
+
}
|
|
583
|
+
/**
|
|
584
|
+
* v0.7.0 — split a function-call argument list on top-level commas.
|
|
585
|
+
* Respects matched single/double quotes and `[...]`/`{...}`/`(...)` nesting.
|
|
586
|
+
*/
|
|
587
|
+
function splitTopLevelCommas(text) {
|
|
588
|
+
const parts = [];
|
|
589
|
+
let cur = "";
|
|
590
|
+
let depth = 0;
|
|
591
|
+
let inQuote = null;
|
|
592
|
+
for (let i = 0; i < text.length; i++) {
|
|
593
|
+
const ch = text[i];
|
|
594
|
+
if (inQuote !== null) {
|
|
595
|
+
cur += ch;
|
|
596
|
+
if (ch === inQuote)
|
|
597
|
+
inQuote = null;
|
|
598
|
+
continue;
|
|
599
|
+
}
|
|
600
|
+
if (ch === '"' || ch === "'") {
|
|
601
|
+
cur += ch;
|
|
602
|
+
inQuote = ch;
|
|
603
|
+
continue;
|
|
604
|
+
}
|
|
605
|
+
if (ch === "(" || ch === "[" || ch === "{") {
|
|
606
|
+
depth++;
|
|
607
|
+
cur += ch;
|
|
608
|
+
continue;
|
|
609
|
+
}
|
|
610
|
+
if (ch === ")" || ch === "]" || ch === "}") {
|
|
611
|
+
depth = Math.max(0, depth - 1);
|
|
612
|
+
cur += ch;
|
|
613
|
+
continue;
|
|
614
|
+
}
|
|
615
|
+
if (ch === "," && depth === 0) {
|
|
616
|
+
const t = cur.trim();
|
|
617
|
+
if (t !== "")
|
|
618
|
+
parts.push(t);
|
|
619
|
+
cur = "";
|
|
620
|
+
continue;
|
|
621
|
+
}
|
|
622
|
+
cur += ch;
|
|
623
|
+
}
|
|
624
|
+
const t = cur.trim();
|
|
625
|
+
if (t !== "")
|
|
626
|
+
parts.push(t);
|
|
627
|
+
return parts;
|
|
628
|
+
}
|
|
629
|
+
/** v0.7.0 — prefix probe for function-call shape: `name(`. */
|
|
630
|
+
const FN_CALL_PREFIX = /^([a-z_][\w]*)\s*\(/;
|
|
428
631
|
function splitMcpConnectorPrefix(body) {
|
|
429
632
|
const m = MCP_CONNECTOR_PREFIX.exec(body);
|
|
430
633
|
if (m === null)
|
|
@@ -455,7 +658,7 @@ function parseRetrievalArgs(argsStr, targetName) {
|
|
|
455
658
|
// validate at parse time.
|
|
456
659
|
let limit = 0;
|
|
457
660
|
const rawLimit = map["limit"] ?? "";
|
|
458
|
-
if (
|
|
661
|
+
if (/\$[(\{]/.test(rawLimit)) {
|
|
459
662
|
limit = rawLimit;
|
|
460
663
|
}
|
|
461
664
|
else {
|
|
@@ -512,7 +715,7 @@ function parseLocalModelArgs(argsStr, targetName) {
|
|
|
512
715
|
if (!(key in map))
|
|
513
716
|
return undefined;
|
|
514
717
|
const raw = map[key];
|
|
515
|
-
if (
|
|
718
|
+
if (/\$[(\{]/.test(raw))
|
|
516
719
|
return raw;
|
|
517
720
|
const n = parseInt(raw, 10);
|
|
518
721
|
if (!Number.isFinite(n) || n <= 0) {
|
|
@@ -676,7 +879,7 @@ export function parse(source) {
|
|
|
676
879
|
else if (key === "timeout") {
|
|
677
880
|
// Per lesson ab6c19db: defer integer validation when value contains
|
|
678
881
|
// `$(VAR)` ref. Runtime resolves via resolveIntParam at op dispatch.
|
|
679
|
-
if (
|
|
882
|
+
if (/\$[(\{]/.test(value)) {
|
|
680
883
|
result.timeout = value;
|
|
681
884
|
}
|
|
682
885
|
else {
|
|
@@ -1177,6 +1380,149 @@ export function parse(source) {
|
|
|
1177
1380
|
});
|
|
1178
1381
|
continue;
|
|
1179
1382
|
}
|
|
1383
|
+
// v0.7.0 — function-call op grammar: `verb(kwarg=value, ...) [-> VAR] [(fallback: "...")]`
|
|
1384
|
+
// Closed runtime-intrinsic op set in RUNTIME_INTRINSIC_FN_NAMES. Unknown
|
|
1385
|
+
// function-call names are parse-errors with remediation pointing at `$`.
|
|
1386
|
+
{
|
|
1387
|
+
const fnPrefix = FN_CALL_PREFIX.exec(stripped0);
|
|
1388
|
+
if (fnPrefix !== null) {
|
|
1389
|
+
const fnName = fnPrefix[1];
|
|
1390
|
+
const parenOpenIdx = fnPrefix[0].length - 1;
|
|
1391
|
+
const parsed = extractParenBody(stripped0, parenOpenIdx);
|
|
1392
|
+
if (parsed === null) {
|
|
1393
|
+
result.parseErrors.push(`Malformed function-call op '${fnName}(...)' in target '${currentTarget.name}' — unbalanced parens.`);
|
|
1394
|
+
continue;
|
|
1395
|
+
}
|
|
1396
|
+
// Parse comma-separated kwargs.
|
|
1397
|
+
const kwArgs = {};
|
|
1398
|
+
let argErr = false;
|
|
1399
|
+
for (const arg of splitTopLevelCommas(parsed.body)) {
|
|
1400
|
+
const eq = arg.indexOf("=");
|
|
1401
|
+
if (eq === -1) {
|
|
1402
|
+
result.parseErrors.push(`Malformed function-call arg '${arg}' in '${fnName}(...)' (target '${currentTarget.name}') — expected name=value.`);
|
|
1403
|
+
argErr = true;
|
|
1404
|
+
continue;
|
|
1405
|
+
}
|
|
1406
|
+
const k = arg.slice(0, eq).trim();
|
|
1407
|
+
const v = arg.slice(eq + 1).trim();
|
|
1408
|
+
kwArgs[k] = processSetValue(v);
|
|
1409
|
+
}
|
|
1410
|
+
if (argErr)
|
|
1411
|
+
continue;
|
|
1412
|
+
// Trailing `-> VAR` and optional `(fallback: "...")`.
|
|
1413
|
+
const tail = stripped0.slice(parsed.endIdx + 1).trim();
|
|
1414
|
+
let outputVar;
|
|
1415
|
+
let fallback;
|
|
1416
|
+
if (tail !== "") {
|
|
1417
|
+
const tailMatch = /^(?:->\s*([A-Za-z_]\w*))?(?:\s*\(fallback\s*:\s*(.+?)\))?\s*$/.exec(tail);
|
|
1418
|
+
if (tailMatch !== null) {
|
|
1419
|
+
if (tailMatch[1] !== undefined)
|
|
1420
|
+
outputVar = tailMatch[1];
|
|
1421
|
+
if (tailMatch[2] !== undefined)
|
|
1422
|
+
fallback = processSetValue(tailMatch[2]);
|
|
1423
|
+
}
|
|
1424
|
+
else {
|
|
1425
|
+
result.parseErrors.push(`Malformed function-call op '${fnName}(...)' trailer in target '${currentTarget.name}': '${tail}' — expected '-> VAR' and/or '(fallback: "value")'.`);
|
|
1426
|
+
continue;
|
|
1427
|
+
}
|
|
1428
|
+
}
|
|
1429
|
+
const approved = kwArgs["approved"];
|
|
1430
|
+
// Per-op dispatch — map function-call form to canonical AST shapes.
|
|
1431
|
+
if (fnName === "emit") {
|
|
1432
|
+
const text = kwArgs["text"] ?? "";
|
|
1433
|
+
opBucket.push({
|
|
1434
|
+
kind: "!",
|
|
1435
|
+
body: text,
|
|
1436
|
+
...(outputVar !== undefined ? { outputVar } : {}),
|
|
1437
|
+
...(approved !== undefined ? { approved } : {}),
|
|
1438
|
+
sourceForm: "function-call",
|
|
1439
|
+
});
|
|
1440
|
+
continue;
|
|
1441
|
+
}
|
|
1442
|
+
if (fnName === "ask") {
|
|
1443
|
+
const prompt = kwArgs["prompt"] ?? "";
|
|
1444
|
+
opBucket.push({
|
|
1445
|
+
kind: "??",
|
|
1446
|
+
body: prompt,
|
|
1447
|
+
...(outputVar !== undefined ? { outputVar } : {}),
|
|
1448
|
+
...(approved !== undefined ? { approved } : {}),
|
|
1449
|
+
sourceForm: "function-call",
|
|
1450
|
+
});
|
|
1451
|
+
continue;
|
|
1452
|
+
}
|
|
1453
|
+
if (fnName === "inline") {
|
|
1454
|
+
const skill = kwArgs["skill"] ?? "";
|
|
1455
|
+
opBucket.push({
|
|
1456
|
+
kind: "&",
|
|
1457
|
+
body: stripped0,
|
|
1458
|
+
ampParams: { skillName: skill, args: {} },
|
|
1459
|
+
...(outputVar !== undefined ? { outputVar } : {}),
|
|
1460
|
+
...(fallback !== undefined ? { fallback } : {}),
|
|
1461
|
+
...(approved !== undefined ? { approved } : {}),
|
|
1462
|
+
sourceForm: "function-call",
|
|
1463
|
+
});
|
|
1464
|
+
continue;
|
|
1465
|
+
}
|
|
1466
|
+
if (fnName === "execute_skill") {
|
|
1467
|
+
const skillName = kwArgs["skill_name"] ?? "";
|
|
1468
|
+
const rest = Object.entries(kwArgs).filter(([k]) => k !== "skill_name" && k !== "approved");
|
|
1469
|
+
const inner = rest.map(([k, v]) => /\s/.test(v) || v.startsWith("{") || v.startsWith("[") ? `${k}=${v}` : `${k}="${v}"`).join(" ");
|
|
1470
|
+
opBucket.push({
|
|
1471
|
+
kind: "$",
|
|
1472
|
+
body: `execute_skill skill_name="${skillName}"${inner ? " " + inner : ""}`,
|
|
1473
|
+
...(outputVar !== undefined ? { outputVar } : {}),
|
|
1474
|
+
...(fallback !== undefined ? { fallback } : {}),
|
|
1475
|
+
...(approved !== undefined ? { approved } : {}),
|
|
1476
|
+
sourceForm: "function-call",
|
|
1477
|
+
});
|
|
1478
|
+
continue;
|
|
1479
|
+
}
|
|
1480
|
+
if (fnName === "shell") {
|
|
1481
|
+
const command = kwArgs["command"] ?? "";
|
|
1482
|
+
const unsafe = kwArgs["unsafe"] === "true";
|
|
1483
|
+
opBucket.push({
|
|
1484
|
+
kind: "@",
|
|
1485
|
+
body: command,
|
|
1486
|
+
...(unsafe ? { policy: "unsafe" } : {}),
|
|
1487
|
+
...(outputVar !== undefined ? { outputVar } : {}),
|
|
1488
|
+
...(fallback !== undefined ? { fallback } : {}),
|
|
1489
|
+
...(approved !== undefined ? { approved } : {}),
|
|
1490
|
+
sourceForm: "function-call",
|
|
1491
|
+
});
|
|
1492
|
+
continue;
|
|
1493
|
+
}
|
|
1494
|
+
if (fnName === "file_read") {
|
|
1495
|
+
const path = kwArgs["path"] ?? "";
|
|
1496
|
+
opBucket.push({
|
|
1497
|
+
kind: "file_read",
|
|
1498
|
+
body: stripped0,
|
|
1499
|
+
fileParams: { path },
|
|
1500
|
+
...(outputVar !== undefined ? { outputVar } : {}),
|
|
1501
|
+
...(fallback !== undefined ? { fallback } : {}),
|
|
1502
|
+
sourceForm: "function-call",
|
|
1503
|
+
});
|
|
1504
|
+
continue;
|
|
1505
|
+
}
|
|
1506
|
+
if (fnName === "file_write") {
|
|
1507
|
+
const path = kwArgs["path"] ?? "";
|
|
1508
|
+
const content = kwArgs["content"] ?? "";
|
|
1509
|
+
opBucket.push({
|
|
1510
|
+
kind: "file_write",
|
|
1511
|
+
body: stripped0,
|
|
1512
|
+
fileParams: { path, content },
|
|
1513
|
+
...(outputVar !== undefined ? { outputVar } : {}),
|
|
1514
|
+
...(approved !== undefined ? { approved } : {}),
|
|
1515
|
+
sourceForm: "function-call",
|
|
1516
|
+
});
|
|
1517
|
+
continue;
|
|
1518
|
+
}
|
|
1519
|
+
// Unknown function-call name — runtime-intrinsic set is closed.
|
|
1520
|
+
result.parseErrors.push(`Unknown function-call op '${fnName}(...)' in target '${currentTarget.name}'. ` +
|
|
1521
|
+
`Runtime-intrinsic ops are: ${RUNTIME_INTRINSIC_FN_NAMES.join(", ")}. ` +
|
|
1522
|
+
`If this is an MCP tool, use \`$ ${fnName} args -> R\` shape instead.`);
|
|
1523
|
+
continue;
|
|
1524
|
+
}
|
|
1525
|
+
}
|
|
1180
1526
|
const stripped = line.replace(/^\s+/, "");
|
|
1181
1527
|
let kind = null;
|
|
1182
1528
|
let body = "";
|