spl-lang 0.2.0 → 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 -170
- package/dist/ast.d.ts +7 -16
- package/dist/cli.js +46 -12
- package/dist/copilot.d.ts +1 -0
- package/dist/copilot.js +10 -6
- package/dist/errors.d.ts +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/interpreter.js +99 -69
- package/dist/llm.d.ts +5 -10
- package/dist/llm.js +17 -26
- package/dist/parser.d.ts +2 -12
- package/dist/parser.js +23 -99
- package/dist/recording.d.ts +17 -0
- package/dist/recording.js +93 -0
- package/dist/schema.d.ts +2 -0
- package/dist/schema.js +40 -0
- package/dist/tools.d.ts +3 -1
- package/dist/tools.js +29 -9
- package/dist/values.d.ts +1 -1
- package/docs/syntax.ebnf +53 -70
- package/package.json +3 -2
package/README.md
CHANGED
|
@@ -1,188 +1,91 @@
|
|
|
1
|
-
# spl-lang
|
|
1
|
+
# spl-lang 0.3
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
calls go through the GitHub Copilot SDK. See [../LANG-PRD.md](../LANG-PRD.md) for the full spec,
|
|
5
|
-
[docs/syntax.ebnf](docs/syntax.ebnf) for the grammar and the [documentation site](../docs/index.html) (open it in a
|
|
6
|
-
browser, or publish `docs/` with GitHub Pages) for guides and reference.
|
|
3
|
+
The Semantic Prompt Language interpreter. Requires Node.js 20+.
|
|
7
4
|
|
|
8
|
-
|
|
5
|
+
See the [project guide](../README.md), [language specification](../LANG-PRD.md),
|
|
6
|
+
[EBNF](docs/syntax.ebnf), and [documentation site](../docs/index.html).
|
|
9
7
|
|
|
10
|
-
|
|
11
|
-
npm install && npm run build
|
|
12
|
-
node dist/cli.js login # once: authenticate with GitHub Copilot
|
|
13
|
-
node dist/cli.js examples/live.spl
|
|
14
|
-
npx file:./lang examples/live.spl # npx from a checkout (or `npx spl-lang` once published)
|
|
15
|
-
```
|
|
16
|
-
|
|
17
|
-
Requires Node 20+ and an authenticated GitHub Copilot.
|
|
8
|
+
## Run from a checkout
|
|
18
9
|
|
|
10
|
+
```sh
|
|
11
|
+
npm install
|
|
12
|
+
node dist/cli.js login
|
|
13
|
+
node dist/cli.js examples/hi.spl
|
|
19
14
|
```
|
|
20
|
-
spl <script.spl> [--log-level error|info|debug] [--silent] [--model <id>]
|
|
21
|
-
[--store <file.json> | --store-module <module>]
|
|
22
|
-
[--timeout <s>] [--cwd <dir>] [--mcp <file.json>] [--check]
|
|
23
|
-
spl login # GitHub device flow: prints a code, you confirm it in the browser
|
|
24
|
-
spl logout # removes the credentials saved by `spl login`
|
|
25
|
-
```
|
|
26
|
-
|
|
27
|
-
* stdout: the value of `success` or a top-level `return` (strings raw, everything else as JSON) or the failure payload.
|
|
28
|
-
* stderr: logs, including full LLM requests and raw responses (`[llm]`) and executed statement traces with line numbers (`[deterministic]`) at every non-silent log level (`error`, `info`, `debug`). Model and reasoning effort are included at `info` and `debug`. Logs may contain sensitive data; use `--silent` to suppress them.
|
|
29
|
-
* Exit code: `0` success, `1` uncaught failure, `2` syntax/usage error.
|
|
30
|
-
|
|
31
|
-
`spl login` does nothing if the runtime is already authenticated (an earlier login, a Copilot CLI / `gh` login or
|
|
32
|
-
`COPILOT_GITHUB_TOKEN`/`GH_TOKEN`/`GITHUB_TOKEN`). Otherwise it runs the device flow and saves the token to
|
|
33
|
-
`~/.spl/credentials.json` (override the folder with `SPL_HOME`); scripts use that token automatically. A script that
|
|
34
|
-
needs an LLM and finds no credentials fails with `llm_error` and tells you to run `spl login`.
|
|
35
|
-
|
|
36
|
-
## Language at a glance
|
|
37
15
|
|
|
16
|
+
`npm install` runs the TypeScript build. Installed packages expose `spl` and `spl-lang`.
|
|
17
|
+
Authentication is needed only for live generation and explicit agents; raw tools and replay
|
|
18
|
+
work without Copilot. Existing Copilot/CLI credentials and supported GitHub token environment
|
|
19
|
+
variables are reused. `spl login` otherwise runs device authentication and saves credentials
|
|
20
|
+
under `~/.spl` (override with `SPL_HOME`); `spl logout` removes those saved credentials.
|
|
21
|
+
|
|
22
|
+
## Language contract
|
|
23
|
+
|
|
24
|
+
- **Exact tools:** `read_file(path)`, `write_file(path, content)`, `web_fetch(url)`,
|
|
25
|
+
`web_search(query)`, `shell(command)` never call a model.
|
|
26
|
+
- **Generation:** `?` and `think` have no tools or MCP access.
|
|
27
|
+
- **Agency:** `agent` explicitly enables model-directed runtime and configured MCP tools.
|
|
28
|
+
- **Validation:** expression-valued `schema` uses strict synchronous JSON Schema draft-07;
|
|
29
|
+
`fmt='json'` accepts any JSON value, `fmt='object'` requires an object/array.
|
|
30
|
+
Prose and code fences are not silently extracted.
|
|
31
|
+
- **Repairs:** `repairs=0..5` retries only malformed/invalid generation, not transport errors
|
|
32
|
+
or external actions. Agents require `repairs=0`. Use `assert` for business rules.
|
|
33
|
+
- **Guidance:** `instruct`, `avoid`, and `len` influence prompts, not runtime guarantees.
|
|
34
|
+
- **Capabilities:** nested `restrict fs/net/exec/mcp` clauses intersect. Shell command
|
|
35
|
+
inspection is best effort; MCP servers are trusted services, not OS-sandboxed.
|
|
36
|
+
- **Bindings:** `let` declares/redeclares function/script locals; `x = ...` and `x += ...`
|
|
37
|
+
update existing locals. Every block shares that scope. Enclosing names are readable but
|
|
38
|
+
cannot be rebound by a function; shared collections remain mutable.
|
|
39
|
+
|
|
40
|
+
Common generation modifiers are `len`, `fmt`, `reasoning`, `show`, `schema`, `repairs`,
|
|
41
|
+
and `model`. Values are evaluated once, then validated before calling the model.
|
|
42
|
+
`think` and `agent` default to medium reasoning; `?` uses the model default.
|
|
43
|
+
CLI `--model` overrides per-call `model`, which overrides the header directive.
|
|
44
|
+
|
|
45
|
+
## Record/replay
|
|
46
|
+
|
|
47
|
+
```sh
|
|
48
|
+
node dist/cli.js examples/triage-issues.spl --record triage.jsonl
|
|
49
|
+
node dist/cli.js examples/triage-issues.spl --replay triage.jsonl --silent
|
|
38
50
|
```
|
|
39
|
-
@{model='gpt-5.5'}
|
|
40
|
-
|
|
41
|
-
def fib(n) {
|
|
42
|
-
if n < 2 { return n }
|
|
43
|
-
return fib(n - 1) + fib(n - 2)
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
let joke = ?{len='sentence', fmt='object'} 'A joke about programmers' # JSON validated and parsed
|
|
47
|
-
let why = think{level='high', show=True} 'Why is {joke.setup} funny?'
|
|
48
51
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
52
|
+
Record creates a new file and rejects agents. Replay performs exact, ordered request matching
|
|
53
|
+
without starting Copilot/MCP. Local reads are permitted; writes, shell, network, agents,
|
|
54
|
+
and persistent stores are blocked. Recordings include prompts and raw responses and must
|
|
55
|
+
be protected like logs. Mismatched, missing, or unused entries fail explicitly.
|
|
53
56
|
|
|
54
|
-
|
|
55
|
-
let color = 'Red' # shadows the outer variable
|
|
56
|
-
log(color)
|
|
57
|
-
}
|
|
57
|
+
## CLI
|
|
58
58
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
success {'joke': joke, 'why': why, 'color': color}
|
|
59
|
+
```text
|
|
60
|
+
spl script.spl [--model id] [--mcp file.json] [--cwd dir] [--timeout seconds]
|
|
61
|
+
[--store file.json | --store-module module]
|
|
62
|
+
[--record file.jsonl | --replay file.jsonl]
|
|
63
|
+
[--log-level error|info|debug] [--silent] [--check]
|
|
65
64
|
```
|
|
66
65
|
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
A bare `x = 1` is a syntax error.
|
|
72
|
-
* **Scoping.** `let NAME = ...` binds in the current scope. Bare `{ ... }` blocks and function bodies open a new scope,
|
|
73
|
-
so `let` there shadows an outer variable and the outer one is untouched. `if`/`elif`/`else`, `for`, `while`,
|
|
74
|
-
`try`/`except`, `enforce`, `forbid` and `restrict` bodies share the enclosing scope, so `let` inside them updates the
|
|
75
|
-
enclosing variable: this is how the answer of an LLM call leaves an `enforce`/`forbid`/`restrict` block. A function
|
|
76
|
-
cannot rebind its caller's variables (use a list or dict to share state); names are read through all enclosing scopes.
|
|
77
|
-
* **`return` exits immediately** from any scope: the innermost function, or the whole script when used at the top level
|
|
78
|
-
(like `success`). `break`/`continue` still belong to loops.
|
|
79
|
-
* **`?` is a prefix operator**: `?'a' + b` is `(?'a') + b`, `-?'n'` is `-(?'n')`.
|
|
80
|
-
* **LLM commands have defaults and braces are optional**: `?'q'`, `think 'q'` (level `medium`), and tools without
|
|
81
|
-
parameters, e.g. `read_file 'Read notes.txt and summarize it'`.
|
|
82
|
-
* **Every string interpolates `{expr}`**. Write `{{`/`}}` or `\{`/`\}` for literal braces, or use a raw string
|
|
83
|
-
`r'{"a": 1}'`. Values are substituted as is: strings raw, `True`/`False`/`None`, lists and dicts as JSON.
|
|
84
|
-
* Tool calls (`read_file`, `write_file`, `web_fetch`, `web_search`, `shell`): when the script supplies every parameter
|
|
85
|
-
the tool needs, the runtime runs it first (restriction-checked), and the output plus your instructions go to the LLM
|
|
86
|
-
whose answer is the value. When a parameter is missing (`read_file 'summarize notes.txt'`,
|
|
87
|
-
`write_file{path='a.txt'} 'write a haiku'`, `web_search 'latest Node.js LTS'`) the LLM calls the tool itself with the
|
|
88
|
-
parameters you gave kept fixed. `web_search` queries DuckDuckGo and counts as network access for `restrict net`.
|
|
89
|
-
* Builtins: `len str int float bool type list range abs round min max sum sorted reversed enumerate zip any all keys
|
|
90
|
-
values items json_parse json_stringify log`, plus Python-style string/list/dict methods. `log(...)` writes to stderr.
|
|
91
|
-
* Unknown modifiers/values, `break` outside loops, `try` without `except`, a modification without `let`, etc. are
|
|
92
|
-
syntax errors (exit code 2).
|
|
93
|
-
|
|
94
|
-
## Restrictions
|
|
95
|
-
|
|
96
|
-
Two layers: the LLM is told about active restrictions in the prompt, and the runtime enforces them. The LLM only gets
|
|
97
|
-
the five runtime tools (`read_file`, `write_file`, `web_fetch`, `web_search`, `shell`) plus configured MCP tools; every call is checked against
|
|
98
|
-
the active `restrict` scopes and any other Copilot permission request is rejected. A violation raises a
|
|
99
|
-
`restriction_violation` failure (catchable with `try/except`).
|
|
100
|
-
|
|
101
|
-
* Paths are canonicalized (symlinks resolved, `..` removed) relative to `--cwd` before the check.
|
|
102
|
-
* `net` matches exact hosts (and `*.example.com` wildcards); redirects are re-checked on every hop. `web_search`
|
|
103
|
-
needs `html.duckduckgo.com` to be allowed.
|
|
104
|
-
* `exec` matches the executable name. Executables in pipelines and chains are checked against `restrict exec`,
|
|
105
|
-
and path-like and URL arguments and redirection targets are checked against `fs`/`net`. This is best effort for
|
|
106
|
-
programs that touch files or the network on their own.
|
|
107
|
-
* Nested restrictions of one kind intersect and can never widen.
|
|
108
|
-
* Clauses can be combined: `restrict './src', net 'example.com', exec 'git', mcp 'docs/search' { ... }`.
|
|
109
|
-
Repeating a kind intersects its clauses, just like nesting.
|
|
110
|
-
* `mcp` allows configured servers (`'docs'` or `'docs/*'`) or individual tools (`'docs/search'`).
|
|
111
|
-
Selectors are case-sensitive; nested scopes cannot widen the configuration's tool allowlist.
|
|
112
|
-
|
|
113
|
-
On Windows, `dir` switches such as `/s`, `/b` and `/a:-d` are recognized as options,
|
|
114
|
-
not filesystem paths. Paths and redirection targets remain subject to filesystem restrictions.
|
|
115
|
-
|
|
116
|
-
## MCP tools
|
|
117
|
-
|
|
118
|
-
Pass `--mcp .vscode/mcp.json` to load MCP servers in VS Code's JSON format:
|
|
119
|
-
|
|
120
|
-
See the [MCP configuration reference](../docs/cli.html#mcp) for HTTP/SSE and stdio examples,
|
|
121
|
-
all supported fields, authentication, variable expansion and validation commands.
|
|
122
|
-
Remote servers must explicitly set `"type": "http"` or `"type": "sse"`; omitting it defaults to stdio.
|
|
123
|
-
|
|
124
|
-
```json
|
|
125
|
-
{
|
|
126
|
-
"servers": {
|
|
127
|
-
"docs": {
|
|
128
|
-
"type": "stdio",
|
|
129
|
-
"command": "node",
|
|
130
|
-
"args": ["${workspaceFolder}/tools/docs-server.js"],
|
|
131
|
-
"tools": ["search"]
|
|
132
|
-
}
|
|
133
|
-
}
|
|
134
|
-
}
|
|
135
|
-
```
|
|
66
|
+
Results are printed on stdout, logs on stderr. Full prompts/responses and statement traces
|
|
67
|
+
appear at every non-silent log level. Exit codes are `0` success, `1` script failure,
|
|
68
|
+
`2` syntax/usage error, and `70` internal error. `--check` does not execute modifier expressions
|
|
69
|
+
or compile schemas. See [CLI and MCP configuration](../docs/cli.html).
|
|
136
70
|
|
|
137
|
-
|
|
138
|
-
`args`, `env`, and `cwd`; `http`/`sse` servers use `url` and optional `headers`. All support optional `tools`
|
|
139
|
-
(`["*"]` or omitted means all, `[]` disables the server) and `timeout` in milliseconds.
|
|
140
|
-
`${workspaceFolder}` expands to `--cwd`; `${env:NAME}` expands environment variables. Server working
|
|
141
|
-
directories default to `--cwd`; relative server `cwd` values resolve against it. The config filename is
|
|
142
|
-
relative to the invoking directory. `--check` also validates this config without starting servers.
|
|
143
|
-
Interactive `inputs`, unsupported fields/variables, JSON comments and trailing commas are rejected.
|
|
71
|
+
## Tests and embedding
|
|
144
72
|
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
## Reproducibility
|
|
150
|
-
|
|
151
|
-
Control flow, arithmetic and interpolation are deterministic. The Copilot SDK has no `temperature` or `seed` setting,
|
|
152
|
-
so the language has none either. `think` levels map to the SDK reasoning effort; if the selected model does not support
|
|
153
|
-
it a warning is logged and the model default is used. Guard against variance with `assert` and `fmt='object'`.
|
|
154
|
-
|
|
155
|
-
## Variable store
|
|
156
|
-
|
|
157
|
-
`--store vars.json` loads top-level variables from the file at start and writes them back at exit (also on failure).
|
|
158
|
-
`--store-module ./mystore.mjs` plugs in your own store: export `createStore()` returning `{ load(): Record, save(vars) }`.
|
|
159
|
-
|
|
160
|
-
## Examples
|
|
161
|
-
|
|
162
|
-
All in [examples/](examples); run from the project root, `--cwd <dir>` points a script at another project. The example
|
|
163
|
-
syntax is checked by `tests/examples.test.ts`; `triage-issues` is also run on Copilot there.
|
|
164
|
-
|
|
165
|
-
| Script | Shows |
|
|
166
|
-
|--------|-------|
|
|
167
|
-
| `commit-message.spl` | `shell` under `restrict exec 'git'`, validation helper, retry loop |
|
|
168
|
-
| `mcp.spl` | MCP search via `--mcp`, combined filesystem/exec/MCP restrictions |
|
|
169
|
-
| `triage-issues.spl` | `fmt='object'` + `assert` validation with feedback retries, aggregation, summary |
|
|
170
|
-
| `extract-invoice.spl` | structured extraction cross-checked with arithmetic before it is accepted |
|
|
171
|
-
| `code-review.spl` | `think` then structure, `restrict` on read and on write, `fail` as a CI gate |
|
|
172
|
-
| `release-notes.spl` | nested `forbid`/`enforce`, git history to a file inside `./out` |
|
|
173
|
-
| `web-digest.spl` | `restrict net`, `web_fetch`, catching a blocked second host |
|
|
174
|
-
| `fix-failing-tests.spl` | agent loop: run `npm test`, let the LLM fix the source, repeat |
|
|
175
|
-
| `translate-docs.spl` | `write_file` without `content` (LLM writes, path pinned), dict iteration |
|
|
176
|
-
| `journal.spl` | state across runs with `--store` |
|
|
177
|
-
| `basics.spl`, `live.spl`, `restrict-live.spl`, `tools-live.spl` | small feature tours |
|
|
73
|
+
```sh
|
|
74
|
+
npm test
|
|
75
|
+
```
|
|
178
76
|
|
|
179
|
-
|
|
77
|
+
The default suite is offline. In PowerShell, opt into authenticated live checks with
|
|
78
|
+
`$env:SPL_LIVE_TESTS='1'; npm test`, then remove the environment variable.
|
|
180
79
|
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
80
|
+
The package exports `parse`, `run`, `Logger`, `CopilotBackend`, `RecordingBackend`,
|
|
81
|
+
`ReplayBackend`, variable stores, authentication helpers, and related types.
|
|
82
|
+
Call `close()` on the backend after running; replay also checks for unused entries there.
|
|
83
|
+
Custom backends must honor the request kind: only `agent` may execute tools.
|
|
84
|
+
See [embedding](../docs/internals.html#embedding).
|
|
184
85
|
|
|
185
|
-
|
|
186
|
-
are skipped automatically when the runtime is not authenticated: run `spl login` first to include them.
|
|
86
|
+
## Migration
|
|
187
87
|
|
|
188
|
-
|
|
88
|
+
0.3 intentionally removes `enforce`/`forbid`, `think{level=...}`, tool-command syntax,
|
|
89
|
+
and `let` before compound/member/index assignments. Use `instruct`/`avoid`,
|
|
90
|
+
`reasoning`, ordinary raw functions, and explicit agents instead.
|
|
91
|
+
Bare blocks no longer create scopes. See the [migration table](../README.md#breaking-changes-in-03).
|
package/dist/ast.d.ts
CHANGED
|
@@ -61,27 +61,14 @@ export type Expr = {
|
|
|
61
61
|
lo?: Expr;
|
|
62
62
|
hi?: Expr;
|
|
63
63
|
} | {
|
|
64
|
-
t: 'ask';
|
|
65
|
-
mods:
|
|
64
|
+
t: 'ask' | 'think' | 'agent';
|
|
65
|
+
mods: Record<string, Expr>;
|
|
66
66
|
prompt: Expr;
|
|
67
|
-
} | {
|
|
68
|
-
t: 'think';
|
|
69
|
-
mods: ThinkMods;
|
|
70
|
-
prompt: Expr;
|
|
71
|
-
} | {
|
|
72
|
-
t: 'tool';
|
|
73
|
-
tool: ToolName;
|
|
74
|
-
params: Record<string, Expr>;
|
|
75
|
-
instructions: Expr;
|
|
76
67
|
};
|
|
77
68
|
export interface AskMods {
|
|
78
69
|
len?: 'word' | 'sentence' | 'paragraph' | 'long';
|
|
79
70
|
fmt?: 'text' | 'object';
|
|
80
71
|
}
|
|
81
|
-
export interface ThinkMods {
|
|
82
|
-
level?: 'low' | 'medium' | 'high';
|
|
83
|
-
show?: boolean;
|
|
84
|
-
}
|
|
85
72
|
export type ToolName = 'read_file' | 'write_file' | 'web_fetch' | 'web_search' | 'shell';
|
|
86
73
|
export type RestrictKind = 'fs' | 'net' | 'exec' | 'mcp';
|
|
87
74
|
export type AssignTarget = {
|
|
@@ -101,6 +88,10 @@ interface Base {
|
|
|
101
88
|
}
|
|
102
89
|
export type Stmt = Base & ({
|
|
103
90
|
t: 'let';
|
|
91
|
+
name: string;
|
|
92
|
+
value: Expr;
|
|
93
|
+
} | {
|
|
94
|
+
t: 'assign';
|
|
104
95
|
target: AssignTarget;
|
|
105
96
|
op: '=' | '+=' | '-=' | '*=' | '/=' | '//=' | '%=';
|
|
106
97
|
value: Expr;
|
|
@@ -157,7 +148,7 @@ export type Stmt = Base & ({
|
|
|
157
148
|
value?: Expr;
|
|
158
149
|
} | {
|
|
159
150
|
t: 'constraint';
|
|
160
|
-
kind: '
|
|
151
|
+
kind: 'instruct' | 'avoid';
|
|
161
152
|
text: Expr;
|
|
162
153
|
body: Stmt[];
|
|
163
154
|
} | {
|
package/dist/cli.js
CHANGED
|
@@ -4,13 +4,15 @@ import * as path from 'node:path';
|
|
|
4
4
|
import { parseArgs } from 'node:util';
|
|
5
5
|
import { CopilotBackend } from './copilot.js';
|
|
6
6
|
import { AuthError, login, logout } from './auth.js';
|
|
7
|
-
import { SplSyntaxError } from './errors.js';
|
|
7
|
+
import { Failure, SplSyntaxError } from './errors.js';
|
|
8
|
+
import { LlmError } from './llm.js';
|
|
8
9
|
import { Logger } from './logger.js';
|
|
9
10
|
import { parse } from './parser.js';
|
|
10
11
|
import { run } from './interpreter.js';
|
|
11
12
|
import { JsonFileStore, MemoryStore, loadStoreModule } from './store.js';
|
|
12
13
|
import { toJson } from './values.js';
|
|
13
14
|
import { loadMcpConfig } from './mcp.js';
|
|
15
|
+
import { RecordingBackend, ReplayBackend } from './recording.js';
|
|
14
16
|
const HELP = `spl - Semantic Prompt Language interpreter
|
|
15
17
|
|
|
16
18
|
Usage: spl <script.spl> [options]
|
|
@@ -26,6 +28,8 @@ Options:
|
|
|
26
28
|
--store <file.json> persist top-level variables in a JSON file
|
|
27
29
|
--store-module <path> custom variable store: module exporting createStore() -> { load(), save(vars) }
|
|
28
30
|
--timeout <seconds> per LLM call timeout (default: 300)
|
|
31
|
+
--record <file.jsonl> record generation responses (new file; no agents)
|
|
32
|
+
--replay <file.jsonl> offline generation replay; only local reads allowed
|
|
29
33
|
--cwd <dir> base directory for relative paths and restrictions (default: current directory)
|
|
30
34
|
--check validate script syntax and any --mcp config without executing
|
|
31
35
|
-v, --version print version
|
|
@@ -79,6 +83,8 @@ async function main() {
|
|
|
79
83
|
store: { type: 'string' },
|
|
80
84
|
'store-module': { type: 'string' },
|
|
81
85
|
timeout: { type: 'string' },
|
|
86
|
+
record: { type: 'string' },
|
|
87
|
+
replay: { type: 'string' },
|
|
82
88
|
cwd: { type: 'string' },
|
|
83
89
|
check: { type: 'boolean' },
|
|
84
90
|
version: { type: 'boolean', short: 'v' },
|
|
@@ -114,6 +120,10 @@ async function main() {
|
|
|
114
120
|
usageError('--timeout must be a positive number');
|
|
115
121
|
if (values.store && values['store-module'])
|
|
116
122
|
usageError('--store and --store-module are mutually exclusive');
|
|
123
|
+
if (values.record && values.replay)
|
|
124
|
+
usageError('--record and --replay are mutually exclusive');
|
|
125
|
+
if (values.replay && (values.store || values['store-module']))
|
|
126
|
+
usageError('--replay cannot use a persistent variable store');
|
|
117
127
|
const cwd = path.resolve(values.cwd ?? process.cwd());
|
|
118
128
|
const file = positionals[0];
|
|
119
129
|
let source;
|
|
@@ -150,22 +160,46 @@ async function main() {
|
|
|
150
160
|
catch (e) {
|
|
151
161
|
return usageError(e.message);
|
|
152
162
|
}
|
|
153
|
-
|
|
163
|
+
let backend;
|
|
154
164
|
try {
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
165
|
+
if (values.replay)
|
|
166
|
+
backend = new ReplayBackend(values.replay);
|
|
167
|
+
else {
|
|
168
|
+
backend = new CopilotBackend({ logger, cwd, timeoutMs: timeoutSec * 1000, mcpServers });
|
|
169
|
+
if (values.record)
|
|
170
|
+
backend = new RecordingBackend(backend, values.record);
|
|
159
171
|
}
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
return
|
|
172
|
+
}
|
|
173
|
+
catch (e) {
|
|
174
|
+
if (!(e instanceof Error))
|
|
175
|
+
throw e;
|
|
176
|
+
return usageError(`cannot open recording: ${e.message}`);
|
|
177
|
+
}
|
|
178
|
+
let result;
|
|
179
|
+
try {
|
|
180
|
+
result = await run(program, { backend, logger, store: values.replay ? undefined : store, cwd, model: values.model });
|
|
165
181
|
}
|
|
166
182
|
finally {
|
|
167
|
-
|
|
183
|
+
try {
|
|
184
|
+
await backend.close();
|
|
185
|
+
}
|
|
186
|
+
catch (e) {
|
|
187
|
+
if (!(e instanceof LlmError))
|
|
188
|
+
throw e;
|
|
189
|
+
logger.error(e.message);
|
|
190
|
+
if (result?.status !== 'failure')
|
|
191
|
+
result = { status: 'failure', failure: new Failure('llm_error', e.message).payload() };
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
if (result.status === 'failure') {
|
|
195
|
+
process.stdout.write(JSON.stringify(result.failure, null, 4) + '\n');
|
|
196
|
+
return 1;
|
|
197
|
+
}
|
|
198
|
+
if (result.hasValue) {
|
|
199
|
+
const v = result.value;
|
|
200
|
+
process.stdout.write((typeof v === 'string' ? v : JSON.stringify(toJson(v), null, 2)) + '\n');
|
|
168
201
|
}
|
|
202
|
+
return 0;
|
|
169
203
|
}
|
|
170
204
|
main().then((code) => process.exit(code), (e) => {
|
|
171
205
|
process.stderr.write(`spl: internal error: ${e.stack ?? e}\n`);
|
package/dist/copilot.d.ts
CHANGED
|
@@ -16,6 +16,7 @@ export declare class CopilotBackend implements LlmBackend {
|
|
|
16
16
|
private models?;
|
|
17
17
|
constructor(opts: CopilotBackendOptions);
|
|
18
18
|
private getClient;
|
|
19
|
+
private denyGenerationPermission;
|
|
19
20
|
private listModels;
|
|
20
21
|
/** Maps the `think` level to the SDK reasoning effort, warning when the model cannot honor it. */
|
|
21
22
|
private resolveEffort;
|
package/dist/copilot.js
CHANGED
|
@@ -36,6 +36,11 @@ export class CopilotBackend {
|
|
|
36
36
|
}
|
|
37
37
|
return this.client;
|
|
38
38
|
}
|
|
39
|
+
denyGenerationPermission(block) {
|
|
40
|
+
const failure = new Failure('restriction_violation', 'generation cannot use tools; use an explicit agent operation');
|
|
41
|
+
block(failure);
|
|
42
|
+
return { kind: 'reject', feedback: failure.reason };
|
|
43
|
+
}
|
|
39
44
|
listModels(client) {
|
|
40
45
|
this.models ??= client.listModels().catch((e) => {
|
|
41
46
|
this.opts.logger.debug(`could not list models: ${e.message}`);
|
|
@@ -76,7 +81,8 @@ export class CopilotBackend {
|
|
|
76
81
|
violation ??= failure;
|
|
77
82
|
void session?.abort().catch(() => undefined);
|
|
78
83
|
};
|
|
79
|
-
const
|
|
84
|
+
const agent = req.kind === 'agent';
|
|
85
|
+
const tools = (agent ? TOOL_NAMES : []).map((name) => {
|
|
80
86
|
const spec = TOOL_SPECS[name];
|
|
81
87
|
return defineTool(name, {
|
|
82
88
|
description: spec.description,
|
|
@@ -84,8 +90,6 @@ export class CopilotBackend {
|
|
|
84
90
|
parameters: { type: 'object', properties: spec.properties, required: spec.required, additionalProperties: false },
|
|
85
91
|
handler: async (args) => {
|
|
86
92
|
const params = { ...args };
|
|
87
|
-
if (req.pinned?.tool === name)
|
|
88
|
-
Object.assign(params, req.pinned.params);
|
|
89
93
|
try {
|
|
90
94
|
return await req.tools.invoke(name, params);
|
|
91
95
|
}
|
|
@@ -101,15 +105,15 @@ export class CopilotBackend {
|
|
|
101
105
|
},
|
|
102
106
|
});
|
|
103
107
|
});
|
|
104
|
-
const mcpServers = filterMcpServers(this.opts.mcpServers ?? {}, req.restrictions);
|
|
108
|
+
const mcpServers = agent ? filterMcpServers(this.opts.mcpServers ?? {}, req.restrictions) : {};
|
|
105
109
|
const baseConfig = {
|
|
106
110
|
model: req.model,
|
|
107
111
|
tools,
|
|
108
|
-
availableTools: [...TOOL_NAMES, ...Object.keys(mcpServers).map((name) => `mcp:${name}-*`)],
|
|
112
|
+
availableTools: [...(agent ? TOOL_NAMES : []), ...Object.keys(mcpServers).map((name) => `mcp:${name}-*`)],
|
|
109
113
|
mcpServers,
|
|
110
114
|
systemMessage: { mode: 'append', content: SYSTEM_MESSAGE },
|
|
111
115
|
workingDirectory: this.opts.cwd,
|
|
112
|
-
onPermissionRequest: (r) => decidePermission(r, req.restrictions, block, mcpServers),
|
|
116
|
+
onPermissionRequest: (r) => agent ? decidePermission(r, req.restrictions, block, mcpServers) : this.denyGenerationPermission(block),
|
|
113
117
|
};
|
|
114
118
|
try {
|
|
115
119
|
try {
|
package/dist/errors.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export type StatusCode = 'assertion_failed' | 'explicit_failure' | 'invalid_format' | 'restriction_violation' | 'llm_error' | 'tool_error' | 'runtime_error';
|
|
1
|
+
export type StatusCode = 'assertion_failed' | 'explicit_failure' | 'invalid_format' | 'validation_failed' | 'restriction_violation' | 'llm_error' | 'tool_error' | 'runtime_error';
|
|
2
2
|
export interface FailurePayload {
|
|
3
3
|
status: 'failure';
|
|
4
4
|
reason: string;
|
package/dist/index.d.ts
CHANGED
|
@@ -3,6 +3,7 @@ export { run, type RunOptions, type RunResult } from './interpreter.js';
|
|
|
3
3
|
export { Failure, SplSyntaxError, type FailurePayload, type StatusCode } from './errors.js';
|
|
4
4
|
export { Logger, type LogLevel } from './logger.js';
|
|
5
5
|
export { CopilotBackend } from './copilot.js';
|
|
6
|
+
export { RecordingBackend, ReplayBackend } from './recording.js';
|
|
6
7
|
export { loadMcpConfig, type McpServers } from './mcp.js';
|
|
7
8
|
export { login, logout, authStatus, AuthError } from './auth.js';
|
|
8
9
|
export { JsonFileStore, MemoryStore, loadStoreModule, type VariableStore } from './store.js';
|
package/dist/index.js
CHANGED
|
@@ -3,6 +3,7 @@ export { run } from './interpreter.js';
|
|
|
3
3
|
export { Failure, SplSyntaxError } from './errors.js';
|
|
4
4
|
export { Logger } from './logger.js';
|
|
5
5
|
export { CopilotBackend } from './copilot.js';
|
|
6
|
+
export { RecordingBackend, ReplayBackend } from './recording.js';
|
|
6
7
|
export { loadMcpConfig } from './mcp.js';
|
|
7
8
|
export { login, logout, authStatus, AuthError } from './auth.js';
|
|
8
9
|
export { JsonFileStore, MemoryStore, loadStoreModule } from './store.js';
|