@thenavidm/slipway 0.1.4 → 0.1.6
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/CHANGELOG.md +19 -0
- package/README.md +22 -7
- package/SKILL.md +1 -1
- package/dist/app.d.ts +50 -3
- package/dist/check.js +10 -1
- package/dist/cli/completion.js +12 -10
- package/dist/cli/context.d.ts +10 -0
- package/dist/cli/context.js +13 -0
- package/dist/cli/data.js +1 -1
- package/dist/cli/help.js +27 -17
- package/dist/cli/run.js +18 -8
- package/dist/doctor.js +3 -2
- package/dist/errors.js +5 -0
- package/dist/install.js +1 -1
- package/dist/schema.js +20 -3
- package/dist/serve.js +1 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,25 @@
|
|
|
2
2
|
|
|
3
3
|
What changed in Slipway, newest first.
|
|
4
4
|
|
|
5
|
+
## 0.1.6, 2026-10-05: what Mastodon's move needed
|
|
6
|
+
|
|
7
|
+
- **`login` runs the app's own sign-in, with the words after it.** `mastodon-cli login mastodon.social --oob` hands `mastodon.social --oob` to Mastodon's flow, which registers an app on that instance and signs in. A flow given with `usage` and `help` shows them in help, `login --help` and `agent-context`, so nobody has to guess that it takes an instance.
|
|
8
|
+
- **Terminal commands beside the tools.** `commands` adds commands such as `logout` to the CLI only. Each is listed in help, `agent-context` and tab completion, `slipway check` fails one named like a built-in or a tool, and an MCP client never sees it.
|
|
9
|
+
- **Tuning stays out of the way.** A setting marked `tuning: true`, such as a timeout with a working default, is named on one line of help and left out of what `install` writes. On Mastodon, `install claude-code` told people to set 13 variables and now names the 6 that connect an account, and the general help is 64 tokens shorter.
|
|
10
|
+
- **`httpPort` sets the default port for `--http`.** A server that shipped another default, such as 8000, keeps it after the move; `<PREFIX>_HTTP_PORT` and `--port` still override it.
|
|
11
|
+
- **`doctorNetwork` calls the service on every `doctor`.** Doctor stays local unless `--network` is passed, which keeps it quick and spends no requests. Mastodon's most common failure is a token without the `write` scope, which only a request finds, and its docs have always told people to run plain `doctor` for it.
|
|
12
|
+
- **The MCP binary's help names the CLI binary for the command list.** `mastodon-mcp --help` said a bare `mastodon-mcp` lists the commands, when it starts the server. Every hint that says where to list the commands now names the CLI binary.
|
|
13
|
+
- **Advertised schemas leave out Zod 4's safe-integer bounds.** Zod 4 gives every whole number `maximum: 9007199254740991` and its negative unless the schema sets its own. They tell a client nothing, so they are left out of what it receives; three were in Mastodon's tool list. Validation still runs on the full schema.
|
|
14
|
+
|
|
15
|
+
## 0.1.5, 2026-10-04: what Bluesky's move found
|
|
16
|
+
|
|
17
|
+
- **A request that never got an answer exits 5.** A failed fetch, a refused connection or a DNS failure mapped to exit 1, "unexpected error", so a script that retries on 5 gave up instead. Bluesky 1.2.3 exited 5 for an unreachable host, 0.1.4 made it 1, and it is 5 again.
|
|
18
|
+
- **`--help` and `agent-context` list every variable Slipway reads**, `<PREFIX>_HTTP_PORT`, `_HOST`, `_TOKEN` and `_DEBUG` included. Bluesky's own help listed the HTTP ones before it moved.
|
|
19
|
+
- **A shorter general help.** An agent often reads it first and pays for it again on every later step. The header drops the description, the rarely needed commands share one line, and Slipway's own settings say only what they do: Bluesky's went from 780 tokens to 667.
|
|
20
|
+
- **The command list says `!` needs `--confirm`** when that is true of every command it lists.
|
|
21
|
+
- **The entry turns on Node's compile cache.** The README's `src/index.ts` loads the app after `module.enableCompileCache()`, so every launch after the first skips compiling it: Bluesky answers a client in 183 ms instead of 204. Node before 22.8 starts as before.
|
|
22
|
+
- **The README says what happens to a piped request after stdin closes.** The server stops without answering, as the MCP stdio binding asks; keep stdin open until you read the answer.
|
|
23
|
+
|
|
5
24
|
## 0.1.4, 2026-10-04: faster starts, cheaper results in Codex
|
|
6
25
|
|
|
7
26
|
- **A JSON Schema compiles on its tool's first call.** `jsonSchema()` compiled its validator as soon as a tool was defined, so a server paid for every schema before it could answer. On Teachable's 123 contract tools that held the first answer back by 118 ms. Building Stripe's 611 OpenAPI tools took 1,745 ms and now takes 69; GitHub's 1,230 took 646 ms and now take 60 (medians of three runs on one Mac). A tool's first call now compiles its own schema, a median of 3 ms on Stripe's and under 1 ms on GitHub's.
|
package/README.md
CHANGED
|
@@ -174,11 +174,15 @@ export const app = slipway<Context>({
|
|
|
174
174
|
|
|
175
175
|
```ts
|
|
176
176
|
#!/usr/bin/env node
|
|
177
|
-
import
|
|
177
|
+
import * as nodeModule from "node:module";
|
|
178
178
|
|
|
179
|
+
nodeModule.enableCompileCache?.();
|
|
180
|
+
const { app } = await import("./app.js");
|
|
179
181
|
await app.main();
|
|
180
182
|
```
|
|
181
183
|
|
|
184
|
+
The app loads after Node's compile cache goes on, so every launch after the first skips compiling it again: Bluesky answers a client in 183 ms instead of 204. Node before 22.8 has no compile cache and starts as before, and `NODE_DISABLE_COMPILE_CACHE=1` turns it off.
|
|
185
|
+
|
|
182
186
|
**`src/npx.ts`** is what `npx -y @you/notes-mcp-cli` runs:
|
|
183
187
|
|
|
184
188
|
```ts
|
|
@@ -384,10 +388,11 @@ Override any operation's name or risk with `names` and `risk`, keep a subset wit
|
|
|
384
388
|
| `<cli> which <words>` | Find the command for a task, by what it does |
|
|
385
389
|
| `<cli> schema <command>` | The JSON Schema an MCP client receives. `--output` for the result's |
|
|
386
390
|
| `<cli> agent-context` | Commands, flags, risk, examples, exit codes and settings as JSON. `--brief` for just the commands, which ones write or need `--confirm`, and the exit codes |
|
|
387
|
-
| `<cli> doctor` | Check the setup. `--network` also calls the service |
|
|
388
|
-
| `<cli> login` | How to connect an account |
|
|
391
|
+
| `<cli> doctor` | Check the setup. `--network` also calls the service, which an app with `doctorNetwork` does every time |
|
|
392
|
+
| `<cli> login` | How to connect an account: printed steps, or the app's own sign-in flow with the words after `login` |
|
|
389
393
|
| `<cli> install <client>` | Add the MCP server to a client. See [Add it to a client](#10-add-it-to-a-client) |
|
|
390
394
|
| `<cli> data` | The local cache and synced lists: `sync`, `search`, `sql`, `clear` |
|
|
395
|
+
| `<cli> <command>` from `commands` | A terminal command the app adds, such as `logout`. Listed in help and `agent-context`, never sent to an MCP client |
|
|
391
396
|
| `<cli> completion bash` | Tab completion for bash, zsh or fish |
|
|
392
397
|
|
|
393
398
|
Flags come from the schema: `--flag value`, `--flag=value`, the underscore spelling, `--no-flag` for a boolean, repeated or comma-separated lists of numbers and choices, and JSON or `@file.json` for an object. `--input` takes every argument as one JSON object, from the flag, a file or stdin, and flags on the same line override it.
|
|
@@ -422,7 +427,7 @@ Errors are JSON on stderr, always, with `error`, `code` and a `hint` that names
|
|
|
422
427
|
| Run | Serves |
|
|
423
428
|
|---|---|
|
|
424
429
|
| `<mcp>` | MCP over stdio, what a client launches |
|
|
425
|
-
| `<mcp> --http [--port 8787]` | Streamable HTTP at `/mcp`, with `/health` |
|
|
430
|
+
| `<mcp> --http [--port 8787]` | Streamable HTTP at `/mcp`, with `/health`. The app's `httpPort` replaces 8787, for a server that shipped another default |
|
|
426
431
|
|
|
427
432
|
HTTP binds `127.0.0.1` and checks the Host header, so a web page cannot reach it through a name that resolves to localhost. It refuses to listen on any other address without `<PREFIX>_HTTP_TOKEN`, because anyone who reached the port would act as your account.
|
|
428
433
|
|
|
@@ -453,7 +458,7 @@ notes-cli install cursor --dry-run
|
|
|
453
458
|
| `vscode` | `.vscode/mcp.json` | VS Code asks for each credential once and stores it securely |
|
|
454
459
|
| `gemini` | `~/.gemini/settings.json`, or `.gemini/settings.json` | `${NAME}` references, which Gemini CLI needs to pass anything named like a key |
|
|
455
460
|
|
|
456
|
-
A published server is started with `npx --package=<package>@latest <name>-mcp`, so a client picks up every release on its next start, with Codex's startup timeout raised for the download. The binary is named, because npx alone starts whichever binary a package lists first. Without `package`, or with `--local`, the client starts this copy on disk. Installing again updates the entry in place: anything you added to it by hand stays, and the old file is kept as a backup.
|
|
461
|
+
A published server is started with `npx --package=<package>@latest <name>-mcp`, so a client picks up every release on its next start, with Codex's startup timeout raised for the download. The binary is named, because npx alone starts whichever binary a package lists first. Without `package`, or with `--local`, the client starts this copy on disk. Installing again updates the entry in place: anything you added to it by hand stays, and the old file is kept as a backup. A setting marked `tuning: true`, such as a timeout with a working default, stays out of the entry, so it carries only what connects an account.
|
|
457
462
|
|
|
458
463
|
## 11. Large catalogs
|
|
459
464
|
|
|
@@ -530,11 +535,12 @@ const mcp = await connect(app, { era: "modern", elicit: () => ({ action: "accept
|
|
|
530
535
|
| Codex shows the server as failed at startup | The first npx download outlasted 10 seconds | `install codex` sets `startup_timeout_sec = 60`; add it by hand to an older entry |
|
|
531
536
|
| `slipway check` warns about schema size | One tool's schema is large or repeats its definitions | Send the body schema once, or advertise a short one and validate the full one in the handler |
|
|
532
537
|
| `slipway check` cannot load the app | The module starts the server when imported | Export the app from `app.ts` and call `app.main()` only in `index.ts` |
|
|
538
|
+
| A request piped to the server gets no answer | Stdin closed before the answer, and the MCP stdio binding stops a server when its input ends | Keep stdin open until you read the answer, as clients do, or run the command from the CLI |
|
|
533
539
|
| `npx slipway` prints something unexpected | Slipway is not installed in this folder, so npx fetched an unrelated package called `slipway` | Run `npm install @thenavidm/slipway`, or `npx -p @thenavidm/slipway slipway <command>` |
|
|
534
540
|
|
|
535
541
|
## Environment variables
|
|
536
542
|
|
|
537
|
-
Every server reads these, under its own prefix: the app name in capitals, `NOTES` for `notes`, unless `envPrefix` says otherwise. A server's own settings, declared with `settings`, are listed in its help, its `agent-context` and its generated docs
|
|
543
|
+
Every server reads these, under its own prefix: the app name in capitals, `NOTES` for `notes`, unless `envPrefix` says otherwise. A server's own settings, declared with `settings`, are listed in its help, its `agent-context` and its generated docs, and `install` passes on every one not marked `tuning`.
|
|
538
544
|
|
|
539
545
|
| Variable | Default | What it does |
|
|
540
546
|
|---|---|---|
|
|
@@ -547,7 +553,7 @@ Every server reads these, under its own prefix: the app name in capitals, `NOTES
|
|
|
547
553
|
| `<PREFIX>_TOOLSETS` | `all` | Comma-separated toolsets to turn on |
|
|
548
554
|
| `<PREFIX>_SURFACE` | `full` | `search` lists three tools that find, describe and run the rest |
|
|
549
555
|
| `<PREFIX>_TOOL_TIMEOUT_MS` | none | Give up on any tool after this long |
|
|
550
|
-
| `<PREFIX>_HTTP_PORT` | `8787` | For `--http` |
|
|
556
|
+
| `<PREFIX>_HTTP_PORT` | `8787`, or the app's `httpPort` | For `--http` |
|
|
551
557
|
| `<PREFIX>_HTTP_HOST` | `127.0.0.1` | For `--http`. Any other address needs a token |
|
|
552
558
|
| `<PREFIX>_HTTP_TOKEN` | none | Bearer token required by `--http` |
|
|
553
559
|
| `<PREFIX>_DEBUG` | `0` | `1` prints debug lines on stderr |
|
|
@@ -556,6 +562,15 @@ Every server reads these, under its own prefix: the app name in capitals, `NOTES
|
|
|
556
562
|
|
|
557
563
|
See [CHANGELOG.md](CHANGELOG.md).
|
|
558
564
|
|
|
565
|
+
## Servers built on Slipway
|
|
566
|
+
|
|
567
|
+
| Server | Package | Covers |
|
|
568
|
+
| --- | --- | --- |
|
|
569
|
+
| [Bluesky](https://github.com/thenavidm/bluesky-mcp-cli) | [`@thenavidm/bluesky-mcp-cli`](https://www.npmjs.com/package/@thenavidm/bluesky-mcp-cli) 2.0.0 | Posting, threads, replies, the timeline, search, feeds, lists, notifications and the social graph |
|
|
570
|
+
| [Teachable](https://github.com/thenavidm/teachable-mcp-cli) | [`@thenavidm/teachable-mcp-cli`](https://www.npmjs.com/package/@thenavidm/teachable-mcp-cli) 3.0.0 | Courses, users, enrollments, pricing, coupons and transactions |
|
|
571
|
+
|
|
572
|
+
Each server was measured against its previous release before it moved: startup, what a client receives, CLI exit codes, and tokens in Claude Code and Codex. Its README has the numbers.
|
|
573
|
+
|
|
559
574
|
## 15. FAQ ❓
|
|
560
575
|
|
|
561
576
|
<details>
|
package/SKILL.md
CHANGED
|
@@ -22,7 +22,7 @@ Run `npm ls @thenavidm/slipway` in the repo. If it does not list a version, STOP
|
|
|
22
22
|
|---|---|---|
|
|
23
23
|
| `src/tools.ts` | The tools, from `toolkit<Context>().defineTool` | One `defineTool` per action |
|
|
24
24
|
| `src/app.ts` | `export const app = slipway({...})` | Describes only. Never calls `main()`, so checks and tests can import it |
|
|
25
|
-
| `src/index.ts` | `await app.main()` | The only file that starts anything. Both binaries point at it |
|
|
25
|
+
| `src/index.ts` | `nodeModule.enableCompileCache?.()`, then `await import("./app.js")` and `app.main()` | The only file that starts anything. Both binaries point at it. The cache goes on before the app loads, so later launches skip compiling it |
|
|
26
26
|
| `src/npx.ts` | `import "./index.js";` | What `npx -y <package>` runs. Its binary is named after the package |
|
|
27
27
|
|
|
28
28
|
`package.json` declares `"<name>-mcp"` and `"<name>-cli"` on `dist/index.js`, and a third binary named after the package (`"<name>-mcp-cli"`) on `dist/npx.js`. npx only picks a binary by name when they point to different files; otherwise it takes whichever one the registry lists first, which may be the CLI.
|
package/dist/app.d.ts
CHANGED
|
@@ -48,6 +48,13 @@ export type ServiceSetting = {
|
|
|
48
48
|
description: string;
|
|
49
49
|
/** A credential: shown as set or unset, never printed. */
|
|
50
50
|
secret?: boolean;
|
|
51
|
+
/**
|
|
52
|
+
* Tuning with a working default, such as a timeout or a retry count, or a
|
|
53
|
+
* second name for another setting. Listed in help and `agent-context`, but
|
|
54
|
+
* `install` leaves it out, so a client entry and its instructions hold only
|
|
55
|
+
* what connects an account.
|
|
56
|
+
*/
|
|
57
|
+
tuning?: boolean;
|
|
51
58
|
};
|
|
52
59
|
export type CliIO = {
|
|
53
60
|
stdout: (text: string) => void;
|
|
@@ -62,6 +69,18 @@ export type CliIO = {
|
|
|
62
69
|
/** The folder a project-scoped `install` writes into. Defaults to the current one. */
|
|
63
70
|
cwd?: string;
|
|
64
71
|
};
|
|
72
|
+
/** An interactive sign-in. It gets the words after `login` and returns an exit code. */
|
|
73
|
+
export type LoginFlow = (io: CliIO, args: string[]) => number | Promise<number>;
|
|
74
|
+
/** A terminal-only command an app adds beside its tools. */
|
|
75
|
+
export type CliCommand = {
|
|
76
|
+
/** The word typed after the binary: `logout`. Must not be a tool's command or a built-in. */
|
|
77
|
+
name: string;
|
|
78
|
+
/** What it takes, as help shows it: `logout [<handle>]`. Defaults to the name. */
|
|
79
|
+
usage?: string;
|
|
80
|
+
/** One line: what it does. */
|
|
81
|
+
help: string;
|
|
82
|
+
run: (io: CliIO, args: string[]) => number | Promise<number>;
|
|
83
|
+
};
|
|
65
84
|
export type AppDefinition<Ctx> = {
|
|
66
85
|
/** The service slug: "bluesky". Binaries default to bluesky-mcp and bluesky-cli. */
|
|
67
86
|
name: string;
|
|
@@ -83,6 +102,12 @@ export type AppDefinition<Ctx> = {
|
|
|
83
102
|
};
|
|
84
103
|
/** The npm package that ships the binaries, so `install` can have a client start it with npx. */
|
|
85
104
|
package?: string;
|
|
105
|
+
/**
|
|
106
|
+
* The port `--http` listens on when neither `--port` nor `<PREFIX>_HTTP_PORT`
|
|
107
|
+
* names one. 8787 when unset; a server that already shipped another default
|
|
108
|
+
* keeps it here.
|
|
109
|
+
*/
|
|
110
|
+
httpPort?: number;
|
|
86
111
|
/**
|
|
87
112
|
* Builds what handlers need: an API client, config, accounts. Called once,
|
|
88
113
|
* on the first call that needs it, so `--help` works with nothing configured.
|
|
@@ -93,12 +118,34 @@ export type AppDefinition<Ctx> = {
|
|
|
93
118
|
prompts?: readonly PromptDefinition<Ctx>[];
|
|
94
119
|
/** Whether any credentials are set. False makes `doctor` exit 10 and the server warn at startup. */
|
|
95
120
|
configured?: (ctx: Ctx) => boolean | Promise<boolean>;
|
|
96
|
-
/** Service checks for `doctor`. `network` is true only when the person passed --network. */
|
|
121
|
+
/** Service checks for `doctor`. `network` is true only when the person passed --network, or `doctorNetwork` is set. */
|
|
97
122
|
doctor?: (ctx: Ctx, options: {
|
|
98
123
|
network: boolean;
|
|
99
124
|
}) => DoctorCheck[] | Promise<DoctorCheck[]>;
|
|
100
|
-
/**
|
|
101
|
-
|
|
125
|
+
/**
|
|
126
|
+
* Call the service on every `doctor`, not only with --network. Off by
|
|
127
|
+
* default, so doctor stays quick and spends no requests; on for a service
|
|
128
|
+
* whose most common failure only a request finds, such as a token missing
|
|
129
|
+
* a scope.
|
|
130
|
+
*/
|
|
131
|
+
doctorNetwork?: boolean;
|
|
132
|
+
/**
|
|
133
|
+
* How to sign in: printed instructions, or an interactive flow that returns an
|
|
134
|
+
* exit code. A flow gets the words after `login`: `mastodon-cli login mastodon.social`.
|
|
135
|
+
* A flow given with `usage` and `help` shows them in help, `login --help` and
|
|
136
|
+
* `agent-context`, so nobody has to guess that it takes an instance.
|
|
137
|
+
*/
|
|
138
|
+
login?: string | LoginFlow | {
|
|
139
|
+
usage?: string;
|
|
140
|
+
help: string;
|
|
141
|
+
run: LoginFlow;
|
|
142
|
+
};
|
|
143
|
+
/**
|
|
144
|
+
* Terminal commands beyond the tools, such as `logout`, `auth` or `refresh`.
|
|
145
|
+
* An MCP client never sees them. Each is listed in help and `agent-context`,
|
|
146
|
+
* and gets the words after its name.
|
|
147
|
+
*/
|
|
148
|
+
commands?: readonly CliCommand[];
|
|
102
149
|
/** Values to mask in every result: API keys, tokens. */
|
|
103
150
|
secrets?: (ctx: Ctx) => Array<string | undefined | null>;
|
|
104
151
|
/**
|
package/dist/check.js
CHANGED
|
@@ -50,6 +50,15 @@ export async function checkApp(app, options = {}) {
|
|
|
50
50
|
add("warn", "schema", "ajv is not installed, so schemas were not checked against JSON Schema 2020-12. Add ajv as a dev dependency.");
|
|
51
51
|
let totalBytes = 0;
|
|
52
52
|
let largest;
|
|
53
|
+
for (const command of app.definition.commands ?? []) {
|
|
54
|
+
if (BUILTINS.includes(command.name) || app.find(command.name)) {
|
|
55
|
+
add("error", "names", `The terminal command '${command.name}' has the name of a built-in or a tool. Rename it.`);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
const httpPort = app.definition.httpPort;
|
|
59
|
+
if (httpPort !== undefined && (!Number.isInteger(httpPort) || httpPort < 1 || httpPort > 65535)) {
|
|
60
|
+
add("error", "http", `httpPort is ${httpPort}, which is not a port number from 1 to 65535.`);
|
|
61
|
+
}
|
|
53
62
|
for (const tool of app.allTools) {
|
|
54
63
|
if (BUILTINS.includes(tool.command)) {
|
|
55
64
|
add("error", "names", `'${tool.command}' is a built-in CLI command. Rename the tool.`, tool.name);
|
|
@@ -288,7 +297,7 @@ function checkDocs(app, file, add) {
|
|
|
288
297
|
// The match may begin on the character before the binary, a newline included.
|
|
289
298
|
const at = segment.offset + (match.index ?? 0) + (match[0].length - match[0].trimStart().length);
|
|
290
299
|
const line = text.slice(0, at).split("\n").length;
|
|
291
|
-
if (BUILTINS.includes(command))
|
|
300
|
+
if (BUILTINS.includes(command) || app.definition.commands?.some((custom) => custom.name === command))
|
|
292
301
|
continue;
|
|
293
302
|
const tool = app.find(command);
|
|
294
303
|
if (!tool) {
|
package/dist/cli/completion.js
CHANGED
|
@@ -10,9 +10,9 @@ function globalFlags() {
|
|
|
10
10
|
function toolFlags(tool) {
|
|
11
11
|
return flagsFor(tool.jsonSchema).map((flag) => flag.flag);
|
|
12
12
|
}
|
|
13
|
-
function bash(bin, tools) {
|
|
13
|
+
function bash(bin, tools, words) {
|
|
14
14
|
const fn = `_${bin.replace(/[^A-Za-z0-9]/g, "_")}`;
|
|
15
|
-
const commands = [...tools.map((tool) => tool.command), ...
|
|
15
|
+
const commands = [...tools.map((tool) => tool.command), ...words].join(" ");
|
|
16
16
|
const globals = globalFlags().join(" ");
|
|
17
17
|
const cases = tools
|
|
18
18
|
.map((tool) => ` ${tool.command}) COMPREPLY=( $(compgen -W "${[...toolFlags(tool), ...(tool.paginate ? ["--all", "--max-items"] : [])].join(" ")} ${globals}" -- "$cur") ) ;;`)
|
|
@@ -35,16 +35,16 @@ ${cases}
|
|
|
35
35
|
complete -F ${fn} ${bin}
|
|
36
36
|
`;
|
|
37
37
|
}
|
|
38
|
-
function zsh(bin, tools) {
|
|
38
|
+
function zsh(bin, tools, words) {
|
|
39
39
|
return `#compdef ${bin}
|
|
40
40
|
# ${bin} completion for zsh, through zsh's bash compatibility layer.
|
|
41
41
|
autoload -U +X bashcompinit && bashcompinit
|
|
42
|
-
${bash(bin, tools)}`;
|
|
42
|
+
${bash(bin, tools, words)}`;
|
|
43
43
|
}
|
|
44
44
|
function fishEscape(text) {
|
|
45
45
|
return text.replace(/\\/g, "\\\\").replace(/'/g, "\\'");
|
|
46
46
|
}
|
|
47
|
-
function fish(bin, tools) {
|
|
47
|
+
function fish(bin, tools, words) {
|
|
48
48
|
const lines = [`# ${bin} completion for fish`, `complete -c ${bin} -f`];
|
|
49
49
|
for (const tool of tools) {
|
|
50
50
|
lines.push(`complete -c ${bin} -n '__fish_use_subcommand' -a '${tool.command}' -d '${fishEscape(tool.title)}'`);
|
|
@@ -52,20 +52,22 @@ function fish(bin, tools) {
|
|
|
52
52
|
lines.push(`complete -c ${bin} -n '__fish_seen_subcommand_from ${tool.command}' -l '${flag.flag.slice(2)}' -d '${fishEscape(flag.help.slice(0, 80))}'`);
|
|
53
53
|
}
|
|
54
54
|
}
|
|
55
|
-
for (const
|
|
56
|
-
lines.push(`complete -c ${bin} -n '__fish_use_subcommand' -a '${
|
|
55
|
+
for (const word of words)
|
|
56
|
+
lines.push(`complete -c ${bin} -n '__fish_use_subcommand' -a '${word}'`);
|
|
57
57
|
for (const flag of globalFlags())
|
|
58
58
|
lines.push(`complete -c ${bin} -l '${flag.slice(2)}'`);
|
|
59
59
|
return `${lines.join("\n")}\n`;
|
|
60
60
|
}
|
|
61
61
|
export function completionScript(app, shell, bin, env) {
|
|
62
62
|
const tools = app.tools(env);
|
|
63
|
+
// The built-ins, then any terminal commands the app adds, such as logout.
|
|
64
|
+
const words = [...BUILTINS, ...(app.definition.commands ?? []).map((command) => command.name)];
|
|
63
65
|
if (shell === "bash")
|
|
64
|
-
return bash(bin, tools);
|
|
66
|
+
return bash(bin, tools, words);
|
|
65
67
|
if (shell === "zsh")
|
|
66
|
-
return zsh(bin, tools);
|
|
68
|
+
return zsh(bin, tools, words);
|
|
67
69
|
if (shell === "fish")
|
|
68
|
-
return fish(bin, tools);
|
|
70
|
+
return fish(bin, tools, words);
|
|
69
71
|
throw new UsageError(`completion expects bash, zsh or fish${shell ? `, got '${shell}'` : ""}.`, {
|
|
70
72
|
hint: `Add \`source <(${bin} completion bash)\` to your shell's startup file.`,
|
|
71
73
|
});
|
package/dist/cli/context.d.ts
CHANGED
|
@@ -24,6 +24,11 @@ export declare function agentContext(app: App, env: NodeJS.ProcessEnv, bin: stri
|
|
|
24
24
|
exit_codes: Record<number, string>;
|
|
25
25
|
hidden_commands?: number | undefined;
|
|
26
26
|
toolsets?: Record<string, string> | undefined;
|
|
27
|
+
extra_commands?: {
|
|
28
|
+
command: string;
|
|
29
|
+
usage: string;
|
|
30
|
+
description: string;
|
|
31
|
+
}[] | undefined;
|
|
27
32
|
commands: {
|
|
28
33
|
command: string;
|
|
29
34
|
title: string;
|
|
@@ -79,6 +84,11 @@ export declare function agentContext(app: App, env: NodeJS.ProcessEnv, bin: stri
|
|
|
79
84
|
})[];
|
|
80
85
|
toolsets?: Record<string, string> | undefined;
|
|
81
86
|
hidden_commands: number;
|
|
87
|
+
extra_commands?: {
|
|
88
|
+
command: string;
|
|
89
|
+
usage: string;
|
|
90
|
+
description: string;
|
|
91
|
+
}[] | undefined;
|
|
82
92
|
commands: {
|
|
83
93
|
command: string;
|
|
84
94
|
tool: string;
|
package/dist/cli/context.js
CHANGED
|
@@ -28,6 +28,13 @@ export function agentContext(app, env, bin, options = {}) {
|
|
|
28
28
|
const names = policyEnvNames(app.envPrefix);
|
|
29
29
|
const tools = app.tools(env);
|
|
30
30
|
const hidden = app.allTools.length - tools.length;
|
|
31
|
+
// Terminal commands the app adds beside its tools, such as logout, and a sign-in flow that says what it takes.
|
|
32
|
+
const login = app.definition.login;
|
|
33
|
+
const extra = [...(typeof login === "object" ? [{ name: "login", ...login }] : []), ...(app.definition.commands ?? [])].map((command) => ({
|
|
34
|
+
command: command.name,
|
|
35
|
+
usage: `${bin} ${command.usage ?? command.name}`,
|
|
36
|
+
description: command.help,
|
|
37
|
+
}));
|
|
31
38
|
const note = "--agent never confirms a write. A command that requires --confirm runs only when it is passed explicitly.";
|
|
32
39
|
if (options.brief) {
|
|
33
40
|
// Enough to pick a command: what each one is, and which write or need --confirm. The full read adds flags and settings.
|
|
@@ -38,6 +45,7 @@ export function agentContext(app, env, bin, options = {}) {
|
|
|
38
45
|
usage: { run: `${bin} <command> [flags]`, help: `${bin} <command> --help`, note },
|
|
39
46
|
exit_codes: EXIT_MEANINGS,
|
|
40
47
|
...(hidden ? { hidden_commands: hidden, ...(app.definition.toolsets ? { toolsets: app.definition.toolsets } : {}) } : {}),
|
|
48
|
+
...(extra.length ? { extra_commands: extra } : {}),
|
|
41
49
|
commands: tools.map((tool) => ({
|
|
42
50
|
command: tool.command,
|
|
43
51
|
title: tool.title,
|
|
@@ -76,9 +84,14 @@ export function agentContext(app, env, bin, options = {}) {
|
|
|
76
84
|
{ env: names.auditLog, value: policy.auditLog ?? null, description: "file that records every attempted write" },
|
|
77
85
|
{ env: names.toolTimeoutMs, value: policy.toolTimeoutMs ?? null, description: "deadline for any tool" },
|
|
78
86
|
{ env: names.confirm, value: policy.confirm, description: "who confirms a confirmed call over MCP: human asks a person where the client can, model accepts confirm: true" },
|
|
87
|
+
{ env: `${app.envPrefix}_HTTP_PORT`, value: env[`${app.envPrefix}_HTTP_PORT`] ?? null, description: `port for --http, ${app.definition.httpPort ?? 8787} when unset` },
|
|
88
|
+
{ env: `${app.envPrefix}_HTTP_HOST`, value: env[`${app.envPrefix}_HTTP_HOST`] ?? null, description: "address for --http, 127.0.0.1 when unset; any other needs a token" },
|
|
89
|
+
{ env: `${app.envPrefix}_HTTP_TOKEN`, set: Boolean(env[`${app.envPrefix}_HTTP_TOKEN`]), secret: true, description: "bearer token --http requires" },
|
|
90
|
+
{ env: `${app.envPrefix}_DEBUG`, value: /^(1|true|yes)$/i.test(env[`${app.envPrefix}_DEBUG`] ?? ""), description: "print debug lines on stderr" },
|
|
79
91
|
],
|
|
80
92
|
...(app.definition.toolsets ? { toolsets: app.definition.toolsets } : {}),
|
|
81
93
|
hidden_commands: hidden,
|
|
94
|
+
...(extra.length ? { extra_commands: extra } : {}),
|
|
82
95
|
commands: tools.map((tool) => ({
|
|
83
96
|
command: tool.command,
|
|
84
97
|
tool: tool.name,
|
package/dist/cli/data.js
CHANGED
|
@@ -59,7 +59,7 @@ export async function runData(app, io, tokens, options) {
|
|
|
59
59
|
throw new UsageError("data sync expects the command to copy: data sync <command>.");
|
|
60
60
|
const tool = app.find(command);
|
|
61
61
|
if (!tool)
|
|
62
|
-
throw new UsageError(`Unknown command '${command}'.`, { hint: `Run \`${
|
|
62
|
+
throw new UsageError(`Unknown command '${command}'.`, { hint: `Run \`${app.bins.cli}\` to list commands.` });
|
|
63
63
|
if (!tool.sync) {
|
|
64
64
|
const lists = app.allTools.filter((candidate) => candidate.sync).map((candidate) => candidate.command);
|
|
65
65
|
throw new UsageError(`${tool.command} is not a list that can be synced.`, { hint: `Lists that can: ${lists.join(", ") || "none"}.` });
|
package/dist/cli/help.js
CHANGED
|
@@ -94,7 +94,9 @@ export function renderList(app, tools, bin, env = process.env) {
|
|
|
94
94
|
for (const tool of members)
|
|
95
95
|
lines.push(` ${riskMark(tool.risk)} ${tool.command.padEnd(width)}${tool.title}`);
|
|
96
96
|
}
|
|
97
|
-
|
|
97
|
+
// The legend says `!` needs --confirm only when that holds for every listed command.
|
|
98
|
+
const confirmByRisk = tools.every((tool) => tool.requireConfirm === (tool.risk === "destructive"));
|
|
99
|
+
lines.push(``, ` * writes ! public or irreversible${confirmByRisk ? ", needs --confirm" : ""}`, ``, ` ${bin} <command> --help what one takes, with examples`, ` ${bin} which <words> find the command for a task`, ` ${bin} --help flags, settings and setup`, ``, ...hiddenNote(app, env));
|
|
98
100
|
return lines.join("\n");
|
|
99
101
|
}
|
|
100
102
|
function shellQuote(value) {
|
|
@@ -186,16 +188,18 @@ export function renderGeneralHelp(app, bin) {
|
|
|
186
188
|
const cache = app.allTools.some((tool) => tool.cache);
|
|
187
189
|
const sync = app.allTools.some((tool) => tool.sync);
|
|
188
190
|
const jobs = app.allTools.some((tool) => tool.job);
|
|
191
|
+
// An agent often reads this first and pays for it again on every later step, so the
|
|
192
|
+
// rarely needed commands share one line and Slipway's own settings say only what they do.
|
|
189
193
|
const commands = [
|
|
190
|
-
[
|
|
194
|
+
[app.bins.cli, "list the commands"],
|
|
191
195
|
[`${bin} <command> --help`, "what one takes, with examples"],
|
|
192
196
|
[`${bin} which <words>`, "find the command for a task"],
|
|
193
|
-
[`${bin}
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
[`${bin}
|
|
198
|
-
[`${bin}
|
|
197
|
+
[`${bin} doctor${app.definition.doctorNetwork ? "" : " [--network]"}`, "check the setup and say what is wrong"],
|
|
198
|
+
typeof app.definition.login === "object"
|
|
199
|
+
? [`${bin} ${app.definition.login.usage ?? "login"}`, app.definition.login.help]
|
|
200
|
+
: [`${bin} login`, "how to connect an account"],
|
|
201
|
+
...(app.definition.commands ?? []).map((command) => [`${bin} ${command.usage ?? command.name}`, command.help]),
|
|
202
|
+
[`${bin} install <client>`, "add the server to an MCP client; install --help lists them"],
|
|
199
203
|
...(cache || sync ? [[`${bin} data`, "what is kept on this machine; data clear [<command>] deletes it"]] : []),
|
|
200
204
|
...(sync
|
|
201
205
|
? [
|
|
@@ -206,17 +210,21 @@ export function renderGeneralHelp(app, bin) {
|
|
|
206
210
|
: []),
|
|
207
211
|
[app.bins.mcp, "the MCP server over stdio; --http [--port N] for HTTP"],
|
|
208
212
|
];
|
|
213
|
+
// Tuning keeps a working default, so it is named on one line; agent-context says what each does.
|
|
214
|
+
const tuning = (app.definition.settings ?? []).filter((setting) => setting.tuning).map((setting) => setting.env);
|
|
209
215
|
const settings = [
|
|
210
|
-
...(app.definition.settings ?? []).map((setting) => [setting.env, setting.description]),
|
|
216
|
+
...(app.definition.settings ?? []).filter((setting) => !setting.tuning).map((setting) => [setting.env, setting.description]),
|
|
211
217
|
[`${names.readOnly}=1`, "hide and refuse every write"],
|
|
212
|
-
[`${names.allowDestructive}=0`, "
|
|
218
|
+
[`${names.allowDestructive}=0`, "refuse the irreversible writes"],
|
|
213
219
|
[`${names.toolsets}=a,b`, "only these toolsets, or all"],
|
|
214
|
-
[`${names.surface}=search`, "MCP
|
|
215
|
-
[`${names.auditLog}=<file>`, "log every attempted write
|
|
216
|
-
[`${names.toolTimeoutMs}=<ms>`, "
|
|
217
|
-
[`${names.confirm}=model`, "confirm: true alone confirms
|
|
220
|
+
[`${names.surface}=search`, "MCP lists three finder tools instead"],
|
|
221
|
+
[`${names.auditLog}=<file>`, "log every attempted write"],
|
|
222
|
+
[`${names.toolTimeoutMs}=<ms>`, "deadline for any tool"],
|
|
223
|
+
[`${names.confirm}=model`, "confirm: true alone confirms over MCP"],
|
|
218
224
|
...(cache ? [[`${names.cache}=0`, "never answer from the local cache"]] : []),
|
|
219
225
|
...(cache || sync ? [[`${names.dataDir}=<dir>`, "keep local data in this folder"]] : []),
|
|
226
|
+
[`${app.envPrefix}_HTTP_PORT / _HOST / _TOKEN`, "for --http"],
|
|
227
|
+
[`${app.envPrefix}_DEBUG=1`, "debug lines on stderr"],
|
|
220
228
|
];
|
|
221
229
|
// Flags that cannot apply here (jobs, the cache) are left out; agent-context lists every one.
|
|
222
230
|
const flags = GLOBAL_FLAGS.map(([flag]) => flag).filter((flag) => flag !== "--agent" && (flag !== "--wait" || jobs) && (flag !== "--refresh" || cache));
|
|
@@ -224,16 +232,18 @@ export function renderGeneralHelp(app, bin) {
|
|
|
224
232
|
const row = ([left, help]) => ` ${left.padEnd(width)}${help}`;
|
|
225
233
|
const lines = [
|
|
226
234
|
``,
|
|
227
|
-
`${app.title} ${app.version}
|
|
235
|
+
`${app.title} ${app.version}`,
|
|
228
236
|
``,
|
|
229
237
|
...commands.map(row),
|
|
238
|
+
` Also: schema <command>, agent-context [--brief] (all of this as JSON), completion <shell>.`,
|
|
230
239
|
``,
|
|
231
|
-
`Flags: ${flags.join(", ")}, and --agent
|
|
240
|
+
`Flags: ${flags.join(", ")}, and --agent: compact JSON, no prompts, never confirms a write.`,
|
|
232
241
|
``,
|
|
233
242
|
`Settings:`,
|
|
234
243
|
...settings.map(row),
|
|
244
|
+
...(tuning.length ? [` Also: ${tuning.join(", ")}, described in agent-context.`] : []),
|
|
235
245
|
``,
|
|
236
|
-
`Exit codes: ${EXIT.ok} ok, ${EXIT.error} unexpected
|
|
246
|
+
`Exit codes: ${EXIT.ok} ok, ${EXIT.error} unexpected, ${EXIT.usage} usage or refused, ${EXIT.notFound} not found, ${EXIT.auth} auth, ${EXIT.api} API, ${EXIT.rateLimited} rate limited, ${EXIT.notConfigured} not configured`,
|
|
237
247
|
``,
|
|
238
248
|
];
|
|
239
249
|
if (app.definition.links?.repository)
|
package/dist/cli/run.js
CHANGED
|
@@ -140,7 +140,8 @@ export async function runCli(app, argv, partial = {}) {
|
|
|
140
140
|
const at = findCommand(argv);
|
|
141
141
|
const command = at === -1 ? undefined : argv[at];
|
|
142
142
|
const builtin = command !== undefined && BUILTINS.includes(command);
|
|
143
|
-
const
|
|
143
|
+
const custom = command !== undefined && !builtin ? app.definition.commands?.find((candidate) => candidate.name === command) : undefined;
|
|
144
|
+
const tool = command !== undefined && !builtin && !custom ? app.find(command) : undefined;
|
|
144
145
|
const reserved = new Set(tool ? flagsFor(tool.jsonSchema).map((flag) => flag.flag) : []);
|
|
145
146
|
let agent = argv.includes("--agent");
|
|
146
147
|
try {
|
|
@@ -155,11 +156,16 @@ export async function runCli(app, argv, partial = {}) {
|
|
|
155
156
|
}
|
|
156
157
|
if (builtin)
|
|
157
158
|
return await runBuiltin(app, io, command, rest, globals);
|
|
159
|
+
if (custom) {
|
|
160
|
+
if (globals.help)
|
|
161
|
+
return print(io, `\nUsage: ${io.bin} ${custom.usage ?? custom.name}\n\n${custom.help}\n`);
|
|
162
|
+
return await custom.run(io, rest);
|
|
163
|
+
}
|
|
158
164
|
if (!tool) {
|
|
159
|
-
const candidates = [...app.tools(io.env).map((t) => t.command), ...BUILTINS];
|
|
165
|
+
const candidates = [...app.tools(io.env).map((t) => t.command), ...BUILTINS, ...(app.definition.commands ?? []).map((c) => c.name)];
|
|
160
166
|
const guess = didYouMean(command, candidates);
|
|
161
167
|
throw new UsageError(`Unknown command '${command}'.${guess ? ` Did you mean '${guess}'?` : ""}`, {
|
|
162
|
-
hint: `Run \`${
|
|
168
|
+
hint: `Run \`${app.bins.cli}\` to list commands, or \`${io.bin} which <words>\` to find one.`,
|
|
163
169
|
});
|
|
164
170
|
}
|
|
165
171
|
const seen = visibility(tool, app.policy(io.env));
|
|
@@ -196,6 +202,9 @@ async function runBuiltin(app, io, command, rest, globals) {
|
|
|
196
202
|
// `<built-in> --help` explains the command instead of running it.
|
|
197
203
|
if (globals.help && command === "install")
|
|
198
204
|
return print(io, (await import("./install.js")).installHelp(app, io.bin));
|
|
205
|
+
const login = app.definition.login;
|
|
206
|
+
if (globals.help && command === "login" && typeof login === "object")
|
|
207
|
+
return print(io, `\nUsage: ${io.bin} ${login.usage ?? "login"}\n\n${login.help}\n`);
|
|
199
208
|
if (globals.help && command !== "help")
|
|
200
209
|
return print(io, renderGeneralHelp(app, io.bin));
|
|
201
210
|
const target = rest.find((token) => !token.startsWith("-"));
|
|
@@ -209,13 +218,13 @@ async function runBuiltin(app, io, command, rest, globals) {
|
|
|
209
218
|
return print(io, renderGeneralHelp(app, io.bin));
|
|
210
219
|
const tool = app.find(target);
|
|
211
220
|
if (!tool)
|
|
212
|
-
throw new UsageError(`Unknown command '${target}'.`, { hint: `Run \`${
|
|
221
|
+
throw new UsageError(`Unknown command '${target}'.`, { hint: `Run \`${app.bins.cli}\` to list commands.` });
|
|
213
222
|
return print(io, renderToolHelp(tool, io.bin));
|
|
214
223
|
}
|
|
215
224
|
case "schema": {
|
|
216
225
|
const tool = target ? app.find(target) : undefined;
|
|
217
226
|
if (!tool)
|
|
218
|
-
throw new UsageError(`schema expects a command${target ? `; '${target}' is not one` : ""}.`, { hint: `Run \`${
|
|
227
|
+
throw new UsageError(`schema expects a command${target ? `; '${target}' is not one` : ""}.`, { hint: `Run \`${app.bins.cli}\` to list commands.` });
|
|
219
228
|
if (rest.includes("--output")) {
|
|
220
229
|
if (!tool.output)
|
|
221
230
|
throw new UsageError(`${tool.command} declares no output schema.`);
|
|
@@ -234,15 +243,16 @@ async function runBuiltin(app, io, command, rest, globals) {
|
|
|
234
243
|
return print(io, json(globals, matches.map(({ tool, score }) => ({ command: tool.command, title: tool.title, risk: tool.risk, score: Number(score.toFixed(2)) }))));
|
|
235
244
|
}
|
|
236
245
|
if (!matches.length)
|
|
237
|
-
return print(io, `No command matches '${query}'. Run \`${
|
|
246
|
+
return print(io, `No command matches '${query}'. Run \`${app.bins.cli}\` to see them all.`);
|
|
238
247
|
return print(io, matches.map(({ tool }) => toolLine(tool)).join("\n"));
|
|
239
248
|
}
|
|
240
249
|
case "doctor":
|
|
241
250
|
return runDoctor(app, io, { network: rest.includes("--network"), json: globals.format !== "auto" });
|
|
242
251
|
case "login": {
|
|
243
|
-
const login = app.definition.login;
|
|
244
252
|
if (typeof login === "function")
|
|
245
|
-
return await login(io);
|
|
253
|
+
return await login(io, rest);
|
|
254
|
+
if (typeof login === "object")
|
|
255
|
+
return await login.run(io, rest);
|
|
246
256
|
if (typeof login === "string")
|
|
247
257
|
return print(io, login);
|
|
248
258
|
return print(io, `${app.title} reads its credentials from the environment. Run \`${io.bin} doctor\` to see what is missing.`);
|
package/dist/doctor.js
CHANGED
|
@@ -68,15 +68,16 @@ export async function runDoctor(app, io, options) {
|
|
|
68
68
|
...(configured ? {} : { fix: `Run \`${app.bins.cli} login\` to see how to connect an account.` }),
|
|
69
69
|
});
|
|
70
70
|
}
|
|
71
|
+
const network = options.network || app.definition.doctorNetwork === true;
|
|
71
72
|
if (ctx !== undefined && app.definition.doctor) {
|
|
72
73
|
try {
|
|
73
|
-
checks.push(...(await app.definition.doctor(ctx, { network
|
|
74
|
+
checks.push(...(await app.definition.doctor(ctx, { network })));
|
|
74
75
|
}
|
|
75
76
|
catch (error) {
|
|
76
77
|
checks.push({ name: "Service check", ok: false, detail: app.secrets.redact(error?.message ?? String(error)) });
|
|
77
78
|
}
|
|
78
79
|
}
|
|
79
|
-
if (!
|
|
80
|
+
if (!network && app.definition.doctor) {
|
|
80
81
|
checks.push({ name: "Network", ok: true, warn: true, detail: "not checked; run with --network to call the service" });
|
|
81
82
|
}
|
|
82
83
|
const failed = checks.filter((check) => !check.ok && !check.warn);
|
package/dist/errors.js
CHANGED
|
@@ -149,5 +149,10 @@ export function toSlipwayError(error) {
|
|
|
149
149
|
return new AuthError(message, { cause: error });
|
|
150
150
|
if (/\b404\b|not found|does not exist/.test(text))
|
|
151
151
|
return new NotFoundError(message, { cause: error });
|
|
152
|
+
// A request that never got an answer is the service's failure, not a bug here: exit 5, which a script may retry.
|
|
153
|
+
const codes = [error?.code, error?.cause?.code];
|
|
154
|
+
const network = codes.some((code) => typeof code === "string" && /^(ECONN(REFUSED|RESET|ABORTED)|ENOTFOUND|EAI_AGAIN|ETIMEDOUT|E(HOST|NET)UNREACH|EPIPE|UND_ERR_\w+)$/.test(code));
|
|
155
|
+
if (network || /fetch failed|could not reach|network error|socket hang up/.test(text))
|
|
156
|
+
return new ApiError(message, { cause: error });
|
|
152
157
|
return new SlipwayError(message, "internal", EXIT.error, { cause: error });
|
|
153
158
|
}
|
package/dist/install.js
CHANGED
|
@@ -207,7 +207,7 @@ export function planInstall(app, options, context) {
|
|
|
207
207
|
}
|
|
208
208
|
const launch = launchFor(app, { local: options.local, ...(context.entry ? { entry: context.entry } : {}), ...(context.platform ? { platform: context.platform } : {}) });
|
|
209
209
|
const usesNpx = launch.command === "npx" || launch.args.includes("npx");
|
|
210
|
-
const settings = app.definition.settings ?? [];
|
|
210
|
+
const settings = (app.definition.settings ?? []).filter((setting) => !setting.tuning);
|
|
211
211
|
const variables = settings.map((setting) => setting.env);
|
|
212
212
|
const env = { forwarded: [], copied: [], toAdd: [] };
|
|
213
213
|
const notes = [];
|
package/dist/schema.js
CHANGED
|
@@ -83,6 +83,25 @@ export const CONTROL_NAMES = ["confirm", "wait_seconds"];
|
|
|
83
83
|
export const CONFIRM_DESCRIPTION = "Set true only when the user asked for exactly this action.";
|
|
84
84
|
/** The schema already carries the range and the default, so the words only say what the number is for. */
|
|
85
85
|
export const WAIT_DESCRIPTION = "Seconds to wait for the job to finish before returning it to check later.";
|
|
86
|
+
/**
|
|
87
|
+
* Zod 4 gives every whole number the safe-integer bounds, `maximum:
|
|
88
|
+
* 9007199254740991` and its negative, unless the schema sets its own. They say
|
|
89
|
+
* nothing a client can use, so they are left out of what it receives.
|
|
90
|
+
* Validation still runs on the schema itself.
|
|
91
|
+
*/
|
|
92
|
+
function withoutSafeIntegerBounds(node) {
|
|
93
|
+
if (Array.isArray(node))
|
|
94
|
+
return node.map(withoutSafeIntegerBounds);
|
|
95
|
+
if (node === null || typeof node !== "object")
|
|
96
|
+
return node;
|
|
97
|
+
const out = {};
|
|
98
|
+
for (const [key, value] of Object.entries(node)) {
|
|
99
|
+
if ((key === "maximum" && value === Number.MAX_SAFE_INTEGER) || (key === "minimum" && value === Number.MIN_SAFE_INTEGER))
|
|
100
|
+
continue;
|
|
101
|
+
out[key] = withoutSafeIntegerBounds(value);
|
|
102
|
+
}
|
|
103
|
+
return out;
|
|
104
|
+
}
|
|
86
105
|
/**
|
|
87
106
|
* A schema as clients receive it, without the `$schema` line that names its
|
|
88
107
|
* dialect. A client reads JSON Schema 2020-12 when no dialect is named, so
|
|
@@ -91,10 +110,8 @@ export const WAIT_DESCRIPTION = "Seconds to wait for the job to finish before re
|
|
|
91
110
|
export function advertised(schema) {
|
|
92
111
|
const std = schema["~standard"];
|
|
93
112
|
const plain = (json) => {
|
|
94
|
-
if (!("$schema" in json))
|
|
95
|
-
return json;
|
|
96
113
|
const { $schema: _dialect, ...rest } = json;
|
|
97
|
-
return rest;
|
|
114
|
+
return withoutSafeIntegerBounds(rest);
|
|
98
115
|
};
|
|
99
116
|
return {
|
|
100
117
|
"~standard": {
|
package/dist/serve.js
CHANGED
|
@@ -43,7 +43,7 @@ async function warnIfUnconfigured(app, env, warn) {
|
|
|
43
43
|
export function httpOptions(app, env, argv) {
|
|
44
44
|
const at = argv.findIndex((token) => token === "--port" || token.startsWith("--port="));
|
|
45
45
|
const raw = at === -1 ? env[`${app.envPrefix}_HTTP_PORT`] : argv[at].includes("=") ? argv[at].split("=")[1] : argv[at + 1];
|
|
46
|
-
const port = Number(raw ?? 8787);
|
|
46
|
+
const port = Number(raw ?? app.definition.httpPort ?? 8787);
|
|
47
47
|
if (!Number.isInteger(port) || port < 1 || port > 65535)
|
|
48
48
|
throw new UsageError(`--port expects a port number, got '${raw}'.`);
|
|
49
49
|
return {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@thenavidm/slipway",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.6",
|
|
4
4
|
"description": "Slipway, the TypeScript framework for MCP servers and agent-native CLIs. One tool definition ships an MCP server and a CLI, with write safety, typed results and release checks built in.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "Apache-2.0",
|