@thenavidm/slipway 0.1.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/CHANGELOG.md +21 -0
- package/LICENSE +202 -0
- package/NOTICE +4 -0
- package/README.md +757 -0
- package/SECURITY.md +33 -0
- package/SKILL.md +136 -0
- package/dist/app.d.ts +210 -0
- package/dist/app.js +364 -0
- package/dist/bin.d.ts +14 -0
- package/dist/bin.js +178 -0
- package/dist/check.d.ts +52 -0
- package/dist/check.js +313 -0
- package/dist/cli/completion.d.ts +5 -0
- package/dist/cli/completion.js +72 -0
- package/dist/cli/context.d.ts +97 -0
- package/dist/cli/context.js +94 -0
- package/dist/cli/data.d.ts +17 -0
- package/dist/cli/data.js +120 -0
- package/dist/cli/flags.d.ts +35 -0
- package/dist/cli/flags.js +208 -0
- package/dist/cli/help.d.ts +15 -0
- package/dist/cli/help.js +213 -0
- package/dist/cli/install.d.ts +10 -0
- package/dist/cli/install.js +65 -0
- package/dist/cli/output.d.ts +22 -0
- package/dist/cli/output.js +159 -0
- package/dist/cli/run.d.ts +10 -0
- package/dist/cli/run.js +343 -0
- package/dist/confirm.d.ts +90 -0
- package/dist/confirm.js +166 -0
- package/dist/data.d.ts +109 -0
- package/dist/data.js +324 -0
- package/dist/docs.d.ts +13 -0
- package/dist/docs.js +66 -0
- package/dist/doctor.d.ts +12 -0
- package/dist/doctor.js +100 -0
- package/dist/entry.d.ts +11 -0
- package/dist/entry.js +34 -0
- package/dist/errors.d.ts +96 -0
- package/dist/errors.js +153 -0
- package/dist/guard.d.ts +46 -0
- package/dist/guard.js +89 -0
- package/dist/index.d.ts +27 -0
- package/dist/index.js +16 -0
- package/dist/install.d.ts +98 -0
- package/dist/install.js +325 -0
- package/dist/jobs.d.ts +143 -0
- package/dist/jobs.js +247 -0
- package/dist/openapi.d.ts +127 -0
- package/dist/openapi.js +549 -0
- package/dist/pages.d.ts +22 -0
- package/dist/pages.js +61 -0
- package/dist/policy.d.ts +66 -0
- package/dist/policy.js +74 -0
- package/dist/redact.d.ts +18 -0
- package/dist/redact.js +60 -0
- package/dist/result.d.ts +44 -0
- package/dist/result.js +74 -0
- package/dist/rpc.d.ts +69 -0
- package/dist/rpc.js +120 -0
- package/dist/schema.d.ts +99 -0
- package/dist/schema.js +192 -0
- package/dist/search.d.ts +18 -0
- package/dist/search.js +119 -0
- package/dist/serve.d.ts +30 -0
- package/dist/serve.js +149 -0
- package/dist/server.d.ts +20 -0
- package/dist/server.js +244 -0
- package/dist/sync.d.ts +35 -0
- package/dist/sync.js +119 -0
- package/dist/testing.d.ts +35 -0
- package/dist/testing.js +41 -0
- package/dist/tool.d.ts +194 -0
- package/dist/tool.js +151 -0
- package/dist/util.d.ts +12 -0
- package/dist/util.js +35 -0
- package/package.json +89 -0
package/dist/doctor.d.ts
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `doctor`: what someone types when nothing works yet.
|
|
3
|
+
*
|
|
4
|
+
* It answers in the order a person needs: is the runtime fine, is anything
|
|
5
|
+
* configured, do the settings say what they think, and does the service's own
|
|
6
|
+
* check pass. Each failure names its fix. Nothing it prints is a credential.
|
|
7
|
+
*/
|
|
8
|
+
import type { App, CliIO } from "./app.js";
|
|
9
|
+
export declare function runDoctor(app: App, io: CliIO, options: {
|
|
10
|
+
network: boolean;
|
|
11
|
+
json: boolean;
|
|
12
|
+
}): Promise<number>;
|
package/dist/doctor.js
ADDED
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `doctor`: what someone types when nothing works yet.
|
|
3
|
+
*
|
|
4
|
+
* It answers in the order a person needs: is the runtime fine, is anything
|
|
5
|
+
* configured, do the settings say what they think, and does the service's own
|
|
6
|
+
* check pass. Each failure names its fix. Nothing it prints is a credential.
|
|
7
|
+
*/
|
|
8
|
+
import { accessSync, constants } from "node:fs";
|
|
9
|
+
import { dirname } from "node:path";
|
|
10
|
+
import { EXIT, NotConfiguredError, SlipwayError } from "./errors.js";
|
|
11
|
+
import { policyEnvNames } from "./policy.js";
|
|
12
|
+
export async function runDoctor(app, io, options) {
|
|
13
|
+
const checks = [];
|
|
14
|
+
const names = policyEnvNames(app.envPrefix);
|
|
15
|
+
const policy = app.policy(io.env);
|
|
16
|
+
const major = Number(process.versions.node.split(".")[0]);
|
|
17
|
+
checks.push({ name: "Node.js", ok: major >= 22, detail: `v${process.versions.node}`, ...(major >= 22 ? {} : { fix: "Install Node.js 22 or later." }) });
|
|
18
|
+
checks.push({ name: "Version", ok: true, detail: `${app.name} ${app.version}` });
|
|
19
|
+
const writes = policy.readOnly ? "off (read-only)" : policy.allowDestructive ? "on" : "on, irreversible ones refused";
|
|
20
|
+
checks.push({ name: "Writes", ok: true, detail: writes });
|
|
21
|
+
checks.push({
|
|
22
|
+
name: "Tools",
|
|
23
|
+
ok: true,
|
|
24
|
+
detail: `${app.tools(io.env).length} of ${app.allTools.length} on${policy.toolsets === "all" ? "" : ` (${names.toolsets}=${[...policy.toolsets].join(",")})`}`,
|
|
25
|
+
});
|
|
26
|
+
if (policy.auditLog) {
|
|
27
|
+
let writable = true;
|
|
28
|
+
try {
|
|
29
|
+
accessSync(dirname(policy.auditLog), constants.W_OK);
|
|
30
|
+
}
|
|
31
|
+
catch {
|
|
32
|
+
writable = false;
|
|
33
|
+
}
|
|
34
|
+
checks.push({
|
|
35
|
+
name: "Audit log",
|
|
36
|
+
ok: writable,
|
|
37
|
+
detail: policy.auditLog,
|
|
38
|
+
...(writable ? {} : { fix: `The folder for ${names.auditLog} is missing or not writable.` }),
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
if (app.allTools.some((tool) => tool.cache || tool.sync)) {
|
|
42
|
+
const { dataDir, loadSqlite } = await import("./data.js");
|
|
43
|
+
const sqlite = await loadSqlite();
|
|
44
|
+
checks.push({
|
|
45
|
+
name: "Local data",
|
|
46
|
+
ok: Boolean(sqlite),
|
|
47
|
+
...(sqlite ? {} : { warn: true }),
|
|
48
|
+
detail: sqlite ? `${dataDir(app.name, app.envPrefix, io.env)}${policy.cache ? "" : ` (cache off: ${names.cache}=0)`}` : "unavailable: this Node.js has no SQLite",
|
|
49
|
+
...(sqlite ? {} : { fix: "Install Node.js 22.13 or later for the cache and offline search." }),
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
let configured = true;
|
|
53
|
+
let ctx;
|
|
54
|
+
try {
|
|
55
|
+
ctx = await app.context(io.env);
|
|
56
|
+
}
|
|
57
|
+
catch (error) {
|
|
58
|
+
configured = false;
|
|
59
|
+
const e = error instanceof SlipwayError ? error : new NotConfiguredError(String(error?.message ?? error));
|
|
60
|
+
checks.push({ name: "Setup", ok: false, detail: e.message, fix: e.hint ?? `Run \`${app.bins.cli} login\`.` });
|
|
61
|
+
}
|
|
62
|
+
if (ctx !== undefined && app.definition.configured) {
|
|
63
|
+
configured = await app.definition.configured(ctx);
|
|
64
|
+
checks.push({
|
|
65
|
+
name: "Credentials",
|
|
66
|
+
ok: configured,
|
|
67
|
+
detail: configured ? "configured" : "nothing configured",
|
|
68
|
+
...(configured ? {} : { fix: `Run \`${app.bins.cli} login\` to see how to connect an account.` }),
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
if (ctx !== undefined && app.definition.doctor) {
|
|
72
|
+
try {
|
|
73
|
+
checks.push(...(await app.definition.doctor(ctx, { network: options.network })));
|
|
74
|
+
}
|
|
75
|
+
catch (error) {
|
|
76
|
+
checks.push({ name: "Service check", ok: false, detail: app.secrets.redact(error?.message ?? String(error)) });
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
if (!options.network && app.definition.doctor) {
|
|
80
|
+
checks.push({ name: "Network", ok: true, warn: true, detail: "not checked; run with --network to call the service" });
|
|
81
|
+
}
|
|
82
|
+
const failed = checks.filter((check) => !check.ok && !check.warn);
|
|
83
|
+
const code = !configured ? EXIT.notConfigured : failed.length ? EXIT.error : EXIT.ok;
|
|
84
|
+
if (options.json) {
|
|
85
|
+
io.stdout(`${JSON.stringify(app.secrets.redactDeep({ ok: code === EXIT.ok, exit_code: code, checks }), null, 2)}\n`);
|
|
86
|
+
}
|
|
87
|
+
else {
|
|
88
|
+
const width = Math.max(...checks.map((check) => check.name.length)) + 2;
|
|
89
|
+
const lines = [``, `${app.title} doctor`, ``];
|
|
90
|
+
for (const check of checks) {
|
|
91
|
+
const mark = check.ok ? (check.warn ? "-" : "✓") : check.warn ? "!" : "✗";
|
|
92
|
+
lines.push(` ${mark} ${check.name.padEnd(width)}${app.secrets.redact(check.detail ?? "")}`);
|
|
93
|
+
if (!check.ok && check.fix)
|
|
94
|
+
lines.push(` ${" ".repeat(width)}${check.fix}`);
|
|
95
|
+
}
|
|
96
|
+
lines.push(``, code === EXIT.ok ? " Ready." : code === EXIT.notConfigured ? " Nothing is configured yet." : " Something needs fixing.", ``);
|
|
97
|
+
io.stdout(lines.join("\n"));
|
|
98
|
+
}
|
|
99
|
+
return code;
|
|
100
|
+
}
|
package/dist/entry.d.ts
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* One entry point, two binaries.
|
|
3
|
+
*
|
|
4
|
+
* `<name>-mcp` with no arguments is an MCP client starting a stdio server, and
|
|
5
|
+
* must stay silent on stdout. `<name>-cli` with no arguments is a person who
|
|
6
|
+
* wants to know what they can type. Any argument on either binary is a CLI
|
|
7
|
+
* command, so a typo is reported instead of starting a server that sits
|
|
8
|
+
* waiting on stdin and looks like a hang.
|
|
9
|
+
*/
|
|
10
|
+
import { type App } from "./app.js";
|
|
11
|
+
export declare function main(app: App, argv: string[], invokedAs: string): Promise<void>;
|
package/dist/entry.js
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* One entry point, two binaries.
|
|
3
|
+
*
|
|
4
|
+
* `<name>-mcp` with no arguments is an MCP client starting a stdio server, and
|
|
5
|
+
* must stay silent on stdout. `<name>-cli` with no arguments is a person who
|
|
6
|
+
* wants to know what they can type. Any argument on either binary is a CLI
|
|
7
|
+
* command, so a typo is reported instead of starting a server that sits
|
|
8
|
+
* waiting on stdin and looks like a hang.
|
|
9
|
+
*/
|
|
10
|
+
import { stderrLogger } from "./app.js";
|
|
11
|
+
import { SlipwayError } from "./errors.js";
|
|
12
|
+
export async function main(app, argv, invokedAs) {
|
|
13
|
+
const asCli = invokedAs.startsWith(app.bins.cli);
|
|
14
|
+
if (!asCli && argv.includes("--http")) {
|
|
15
|
+
const { httpOptions, serveHttpApp } = await import("./serve.js");
|
|
16
|
+
try {
|
|
17
|
+
const served = await serveHttpApp(app, process.env, httpOptions(app, process.env, argv));
|
|
18
|
+
const stop = () => void served.close().finally(() => process.exit(0));
|
|
19
|
+
process.on("SIGINT", stop);
|
|
20
|
+
process.on("SIGTERM", stop);
|
|
21
|
+
}
|
|
22
|
+
catch (error) {
|
|
23
|
+
stderrLogger(app.envPrefix, process.env).error(error.message);
|
|
24
|
+
process.exitCode = error instanceof SlipwayError ? error.exitCode : 1;
|
|
25
|
+
}
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
if (!asCli && argv.length === 0) {
|
|
29
|
+
const { serveStdioApp } = await import("./serve.js");
|
|
30
|
+
await serveStdioApp(app, process.env);
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
process.exitCode = await app.runCli(argv, { bin: asCli ? app.bins.cli : app.bins.mcp });
|
|
34
|
+
}
|
package/dist/errors.d.ts
ADDED
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Errors that carry their own exit code.
|
|
3
|
+
*
|
|
4
|
+
* A script branches on the number, a model reads the message and the hint.
|
|
5
|
+
* Mapping errors to exit codes by matching words in the message breaks the day
|
|
6
|
+
* someone rewords a message, so every error Slipway raises says which code it
|
|
7
|
+
* is, and the word matching survives only as a fallback for errors thrown by
|
|
8
|
+
* code that knows nothing about Slipway.
|
|
9
|
+
*/
|
|
10
|
+
/** The exit-code contract. Scripts depend on these numbers, so they never change. */
|
|
11
|
+
export declare const EXIT: {
|
|
12
|
+
readonly ok: 0;
|
|
13
|
+
readonly error: 1;
|
|
14
|
+
readonly usage: 2;
|
|
15
|
+
readonly notFound: 3;
|
|
16
|
+
readonly auth: 4;
|
|
17
|
+
readonly api: 5;
|
|
18
|
+
readonly rateLimited: 7;
|
|
19
|
+
readonly notConfigured: 10;
|
|
20
|
+
readonly interrupted: 130;
|
|
21
|
+
};
|
|
22
|
+
export type ErrorCode = "usage" | "refused" | "not_found" | "auth" | "api" | "rate_limited" | "not_configured" | "timeout" | "canceled" | "internal";
|
|
23
|
+
export type ErrorOptions = {
|
|
24
|
+
hint?: string;
|
|
25
|
+
status?: number;
|
|
26
|
+
retryAfterSeconds?: number;
|
|
27
|
+
details?: unknown;
|
|
28
|
+
cause?: unknown;
|
|
29
|
+
};
|
|
30
|
+
export type ErrorPayload = {
|
|
31
|
+
error: string;
|
|
32
|
+
code: ErrorCode;
|
|
33
|
+
hint?: string;
|
|
34
|
+
status?: number;
|
|
35
|
+
retry_after_seconds?: number;
|
|
36
|
+
details?: unknown;
|
|
37
|
+
};
|
|
38
|
+
export declare class SlipwayError extends Error {
|
|
39
|
+
readonly code: ErrorCode;
|
|
40
|
+
readonly exitCode: number;
|
|
41
|
+
readonly hint?: string;
|
|
42
|
+
readonly status?: number;
|
|
43
|
+
readonly retryAfterSeconds?: number;
|
|
44
|
+
readonly details?: unknown;
|
|
45
|
+
constructor(message: string, code: ErrorCode, exitCode: number, options?: ErrorOptions);
|
|
46
|
+
/** The one shape both surfaces report: JSON on stderr in a terminal, an isError result over MCP. */
|
|
47
|
+
toJSON(): ErrorPayload;
|
|
48
|
+
}
|
|
49
|
+
/** The caller asked for something malformed. Fix the arguments and retry. */
|
|
50
|
+
export declare class UsageError extends SlipwayError {
|
|
51
|
+
constructor(message: string, options?: ErrorOptions);
|
|
52
|
+
}
|
|
53
|
+
/** A write the guard would not run: read-only mode, destructive writes off, or no confirmation. */
|
|
54
|
+
export declare class RefusedError extends SlipwayError {
|
|
55
|
+
constructor(message: string, options?: ErrorOptions);
|
|
56
|
+
}
|
|
57
|
+
export declare class NotFoundError extends SlipwayError {
|
|
58
|
+
constructor(message: string, options?: ErrorOptions);
|
|
59
|
+
}
|
|
60
|
+
export declare class AuthError extends SlipwayError {
|
|
61
|
+
constructor(message: string, options?: ErrorOptions);
|
|
62
|
+
}
|
|
63
|
+
export declare class ApiError extends SlipwayError {
|
|
64
|
+
constructor(message: string, options?: ErrorOptions);
|
|
65
|
+
}
|
|
66
|
+
export declare class RateLimitError extends SlipwayError {
|
|
67
|
+
constructor(message: string, options?: ErrorOptions);
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Nothing is set up yet. This is what someone hits on first run, so it always
|
|
71
|
+
* names the command that fixes it.
|
|
72
|
+
*/
|
|
73
|
+
export declare class NotConfiguredError extends SlipwayError {
|
|
74
|
+
constructor(message: string, options?: ErrorOptions);
|
|
75
|
+
}
|
|
76
|
+
export declare class TimeoutError extends SlipwayError {
|
|
77
|
+
constructor(message: string, options?: ErrorOptions);
|
|
78
|
+
}
|
|
79
|
+
export declare class CanceledError extends SlipwayError {
|
|
80
|
+
constructor(message: string, options?: ErrorOptions);
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* The error for an HTTP status, so an API client needs one line per failure.
|
|
84
|
+
*
|
|
85
|
+
* 400 and 422 are usage errors: the upstream API rejected the arguments, and the
|
|
86
|
+
* caller fixes that by changing them, exactly like a schema failure.
|
|
87
|
+
*/
|
|
88
|
+
export declare function httpError(status: number, message: string, options?: ErrorOptions): SlipwayError;
|
|
89
|
+
/**
|
|
90
|
+
* Turn anything thrown into a SlipwayError.
|
|
91
|
+
*
|
|
92
|
+
* Errors from other libraries arrive with no code, so the fallback reads the
|
|
93
|
+
* shape they usually have (a numeric `status`, an abort) and only then the
|
|
94
|
+
* words in the message.
|
|
95
|
+
*/
|
|
96
|
+
export declare function toSlipwayError(error: unknown): SlipwayError;
|
package/dist/errors.js
ADDED
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Errors that carry their own exit code.
|
|
3
|
+
*
|
|
4
|
+
* A script branches on the number, a model reads the message and the hint.
|
|
5
|
+
* Mapping errors to exit codes by matching words in the message breaks the day
|
|
6
|
+
* someone rewords a message, so every error Slipway raises says which code it
|
|
7
|
+
* is, and the word matching survives only as a fallback for errors thrown by
|
|
8
|
+
* code that knows nothing about Slipway.
|
|
9
|
+
*/
|
|
10
|
+
/** The exit-code contract. Scripts depend on these numbers, so they never change. */
|
|
11
|
+
export const EXIT = {
|
|
12
|
+
ok: 0,
|
|
13
|
+
error: 1,
|
|
14
|
+
usage: 2,
|
|
15
|
+
notFound: 3,
|
|
16
|
+
auth: 4,
|
|
17
|
+
api: 5,
|
|
18
|
+
rateLimited: 7,
|
|
19
|
+
notConfigured: 10,
|
|
20
|
+
interrupted: 130,
|
|
21
|
+
};
|
|
22
|
+
export class SlipwayError extends Error {
|
|
23
|
+
code;
|
|
24
|
+
exitCode;
|
|
25
|
+
hint;
|
|
26
|
+
status;
|
|
27
|
+
retryAfterSeconds;
|
|
28
|
+
details;
|
|
29
|
+
constructor(message, code, exitCode, options = {}) {
|
|
30
|
+
super(message, options.cause === undefined ? undefined : { cause: options.cause });
|
|
31
|
+
this.name = new.target.name;
|
|
32
|
+
this.code = code;
|
|
33
|
+
this.exitCode = exitCode;
|
|
34
|
+
this.hint = options.hint;
|
|
35
|
+
this.status = options.status;
|
|
36
|
+
this.retryAfterSeconds = options.retryAfterSeconds;
|
|
37
|
+
this.details = options.details;
|
|
38
|
+
}
|
|
39
|
+
/** The one shape both surfaces report: JSON on stderr in a terminal, an isError result over MCP. */
|
|
40
|
+
toJSON() {
|
|
41
|
+
const payload = { error: this.message, code: this.code };
|
|
42
|
+
if (this.hint)
|
|
43
|
+
payload.hint = this.hint;
|
|
44
|
+
if (this.status !== undefined)
|
|
45
|
+
payload.status = this.status;
|
|
46
|
+
if (this.retryAfterSeconds !== undefined)
|
|
47
|
+
payload.retry_after_seconds = this.retryAfterSeconds;
|
|
48
|
+
if (this.details !== undefined)
|
|
49
|
+
payload.details = this.details;
|
|
50
|
+
return payload;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
/** The caller asked for something malformed. Fix the arguments and retry. */
|
|
54
|
+
export class UsageError extends SlipwayError {
|
|
55
|
+
constructor(message, options) {
|
|
56
|
+
super(message, "usage", EXIT.usage, options);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
/** A write the guard would not run: read-only mode, destructive writes off, or no confirmation. */
|
|
60
|
+
export class RefusedError extends SlipwayError {
|
|
61
|
+
constructor(message, options) {
|
|
62
|
+
super(message, "refused", EXIT.usage, options);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
export class NotFoundError extends SlipwayError {
|
|
66
|
+
constructor(message, options) {
|
|
67
|
+
super(message, "not_found", EXIT.notFound, options);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
export class AuthError extends SlipwayError {
|
|
71
|
+
constructor(message, options) {
|
|
72
|
+
super(message, "auth", EXIT.auth, options);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
export class ApiError extends SlipwayError {
|
|
76
|
+
constructor(message, options) {
|
|
77
|
+
super(message, "api", EXIT.api, options);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
export class RateLimitError extends SlipwayError {
|
|
81
|
+
constructor(message, options) {
|
|
82
|
+
super(message, "rate_limited", EXIT.rateLimited, options);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Nothing is set up yet. This is what someone hits on first run, so it always
|
|
87
|
+
* names the command that fixes it.
|
|
88
|
+
*/
|
|
89
|
+
export class NotConfiguredError extends SlipwayError {
|
|
90
|
+
constructor(message, options) {
|
|
91
|
+
super(message, "not_configured", EXIT.notConfigured, options);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
export class TimeoutError extends SlipwayError {
|
|
95
|
+
constructor(message, options) {
|
|
96
|
+
super(message, "timeout", EXIT.api, options);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
export class CanceledError extends SlipwayError {
|
|
100
|
+
constructor(message, options) {
|
|
101
|
+
super(message, "canceled", EXIT.interrupted, options);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* The error for an HTTP status, so an API client needs one line per failure.
|
|
106
|
+
*
|
|
107
|
+
* 400 and 422 are usage errors: the upstream API rejected the arguments, and the
|
|
108
|
+
* caller fixes that by changing them, exactly like a schema failure.
|
|
109
|
+
*/
|
|
110
|
+
export function httpError(status, message, options = {}) {
|
|
111
|
+
const withStatus = { ...options, status };
|
|
112
|
+
if (status === 401 || status === 403)
|
|
113
|
+
return new AuthError(message, withStatus);
|
|
114
|
+
if (status === 404 || status === 410)
|
|
115
|
+
return new NotFoundError(message, withStatus);
|
|
116
|
+
if (status === 429)
|
|
117
|
+
return new RateLimitError(message, withStatus);
|
|
118
|
+
if (status === 400 || status === 422)
|
|
119
|
+
return new UsageError(message, withStatus);
|
|
120
|
+
return new ApiError(message, withStatus);
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
* Turn anything thrown into a SlipwayError.
|
|
124
|
+
*
|
|
125
|
+
* Errors from other libraries arrive with no code, so the fallback reads the
|
|
126
|
+
* shape they usually have (a numeric `status`, an abort) and only then the
|
|
127
|
+
* words in the message.
|
|
128
|
+
*/
|
|
129
|
+
export function toSlipwayError(error) {
|
|
130
|
+
if (error instanceof SlipwayError)
|
|
131
|
+
return error;
|
|
132
|
+
const e = error;
|
|
133
|
+
const message = typeof e?.message === "string" && e.message ? e.message : typeof error === "string" ? error : "Unknown error";
|
|
134
|
+
const status = typeof e?.status === "number" ? e.status : typeof e?.statusCode === "number" ? e.statusCode : undefined;
|
|
135
|
+
if (e?.name === "AbortError")
|
|
136
|
+
return new CanceledError(message, { cause: error });
|
|
137
|
+
if (e?.name === "TimeoutError")
|
|
138
|
+
return new TimeoutError(message, { cause: error });
|
|
139
|
+
if (status !== undefined && status >= 400)
|
|
140
|
+
return httpError(status, message, { cause: error });
|
|
141
|
+
const text = message.toLowerCase();
|
|
142
|
+
if (/rate ?limit|too many requests|\b429\b/.test(text))
|
|
143
|
+
return new RateLimitError(message, { cause: error });
|
|
144
|
+
// Before auth: "no token configured" mentions a token, and matching auth first
|
|
145
|
+
// sends someone who configured nothing looking for a bad credential.
|
|
146
|
+
if (/not configured|nothing is configured|no [a-z ]*(account|credential|token|key)s? (is |are )?(set|configured)/.test(text))
|
|
147
|
+
return new NotConfiguredError(message, { cause: error });
|
|
148
|
+
if (/\b401\b|\b403\b|unauthori[sz]ed|forbidden|invalid[_ ]grant|expired token|token (has )?expired/.test(text))
|
|
149
|
+
return new AuthError(message, { cause: error });
|
|
150
|
+
if (/\b404\b|not found|does not exist/.test(text))
|
|
151
|
+
return new NotFoundError(message, { cause: error });
|
|
152
|
+
return new SlipwayError(message, "internal", EXIT.error, { cause: error });
|
|
153
|
+
}
|
package/dist/guard.d.ts
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Decides whether a write is allowed to run.
|
|
3
|
+
*
|
|
4
|
+
* Shipping no writes is not safety: it hands the work back to a person.
|
|
5
|
+
* Shipping them unguarded is worse. So everything works, the irreversible
|
|
6
|
+
* calls need an explicit confirmation, and one switch removes every write for
|
|
7
|
+
* an agent nobody should trust with them.
|
|
8
|
+
*
|
|
9
|
+
* Agent mode never confirms anything. A flag that turns on JSON output must
|
|
10
|
+
* not also say yes to deleting something, because the agent that sets it is
|
|
11
|
+
* exactly the caller the confirmation exists for.
|
|
12
|
+
*/
|
|
13
|
+
import { type Policy } from "./policy.js";
|
|
14
|
+
import type { Surface, Tool } from "./tool.js";
|
|
15
|
+
/**
|
|
16
|
+
* Who confirmed a call.
|
|
17
|
+
*
|
|
18
|
+
* - `flag`: the caller passed `confirm: true` or `--confirm`.
|
|
19
|
+
* - `person`: a person approved it in a form the client showed.
|
|
20
|
+
* - `client`: the client showed its own approval prompt for this exact call.
|
|
21
|
+
*/
|
|
22
|
+
export type ConfirmedBy = "flag" | "person" | "client";
|
|
23
|
+
export type GuardOutcome = "allowed" | "dry-run" | "asked a person" | "blocked: read-only" | "blocked: destructive disabled" | "blocked: no confirm" | "blocked: person declined" | "blocked: no answer" | "blocked: approval invalid";
|
|
24
|
+
export declare class Guard {
|
|
25
|
+
private readonly policy;
|
|
26
|
+
private readonly surface;
|
|
27
|
+
private readonly prefix;
|
|
28
|
+
constructor(policy: Policy, surface: Surface, prefix: string);
|
|
29
|
+
/** What the caller can actually type to confirm. A model reads `confirm: true`, a person types `--confirm`. */
|
|
30
|
+
get confirmFlag(): string;
|
|
31
|
+
/**
|
|
32
|
+
* The checks that do not depend on confirmation: read-only mode and the
|
|
33
|
+
* switch for irreversible writes. Run before asking a person, so nobody is
|
|
34
|
+
* asked to approve a call that would be refused anyway.
|
|
35
|
+
*/
|
|
36
|
+
preflight(tool: Tool, summary: string): void;
|
|
37
|
+
check(tool: Tool, options: {
|
|
38
|
+
confirmedBy?: ConfirmedBy;
|
|
39
|
+
dryRun: boolean;
|
|
40
|
+
summary: string;
|
|
41
|
+
}): void;
|
|
42
|
+
/** Append-only record of every attempted write, when an audit log is configured. `started` is a job still running when its call returned. */
|
|
43
|
+
record(tool: Tool, summary: string, outcome: GuardOutcome | "failed" | "done" | "started", confirmedBy?: ConfirmedBy): void;
|
|
44
|
+
}
|
|
45
|
+
/** Why a tool needs confirming, in the words a refusal and an approval form both use. */
|
|
46
|
+
export declare function consequence(tool: Pick<Tool, "risk">): string;
|
package/dist/guard.js
ADDED
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Decides whether a write is allowed to run.
|
|
3
|
+
*
|
|
4
|
+
* Shipping no writes is not safety: it hands the work back to a person.
|
|
5
|
+
* Shipping them unguarded is worse. So everything works, the irreversible
|
|
6
|
+
* calls need an explicit confirmation, and one switch removes every write for
|
|
7
|
+
* an agent nobody should trust with them.
|
|
8
|
+
*
|
|
9
|
+
* Agent mode never confirms anything. A flag that turns on JSON output must
|
|
10
|
+
* not also say yes to deleting something, because the agent that sets it is
|
|
11
|
+
* exactly the caller the confirmation exists for.
|
|
12
|
+
*/
|
|
13
|
+
import { appendFileSync } from "node:fs";
|
|
14
|
+
import { RefusedError } from "./errors.js";
|
|
15
|
+
import { policyEnvNames } from "./policy.js";
|
|
16
|
+
export class Guard {
|
|
17
|
+
policy;
|
|
18
|
+
surface;
|
|
19
|
+
prefix;
|
|
20
|
+
constructor(policy, surface, prefix) {
|
|
21
|
+
this.policy = policy;
|
|
22
|
+
this.surface = surface;
|
|
23
|
+
this.prefix = prefix;
|
|
24
|
+
}
|
|
25
|
+
/** What the caller can actually type to confirm. A model reads `confirm: true`, a person types `--confirm`. */
|
|
26
|
+
get confirmFlag() {
|
|
27
|
+
return this.surface === "cli" ? "--confirm" : "confirm: true";
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* The checks that do not depend on confirmation: read-only mode and the
|
|
31
|
+
* switch for irreversible writes. Run before asking a person, so nobody is
|
|
32
|
+
* asked to approve a call that would be refused anyway.
|
|
33
|
+
*/
|
|
34
|
+
preflight(tool, summary) {
|
|
35
|
+
if (tool.risk === "read" && !tool.requireConfirm)
|
|
36
|
+
return;
|
|
37
|
+
const names = policyEnvNames(this.prefix);
|
|
38
|
+
if (this.policy.readOnly && tool.risk !== "read") {
|
|
39
|
+
this.record(tool, summary, "blocked: read-only");
|
|
40
|
+
throw new RefusedError(`${tool.name} is unavailable: this server is running with ${names.readOnly}=1.`, {
|
|
41
|
+
hint: `Unset ${names.readOnly} to allow writes.`,
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
if (tool.risk === "destructive" && !this.policy.allowDestructive) {
|
|
45
|
+
this.record(tool, summary, "blocked: destructive disabled");
|
|
46
|
+
throw new RefusedError(`${tool.name} is unavailable: this server is running with ${names.allowDestructive}=0.`, {
|
|
47
|
+
hint: `Unset ${names.allowDestructive} to allow irreversible writes.`,
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
check(tool, options) {
|
|
52
|
+
if (tool.risk === "read" && !tool.requireConfirm)
|
|
53
|
+
return;
|
|
54
|
+
this.preflight(tool, options.summary);
|
|
55
|
+
if (options.dryRun) {
|
|
56
|
+
this.record(tool, options.summary, "dry-run");
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
if (tool.requireConfirm && !options.confirmedBy) {
|
|
60
|
+
this.record(tool, options.summary, "blocked: no confirm");
|
|
61
|
+
throw new RefusedError(`${tool.name} ${consequence(tool)}, so it will not run without ${this.confirmFlag}. About to: ${options.summary}. Call again with ${this.confirmFlag} if that is what was asked for.`, { hint: `Pass ${this.confirmFlag} only when the user asked for this exact action.` });
|
|
62
|
+
}
|
|
63
|
+
this.record(tool, options.summary, "allowed", options.confirmedBy);
|
|
64
|
+
}
|
|
65
|
+
/** Append-only record of every attempted write, when an audit log is configured. `started` is a job still running when its call returned. */
|
|
66
|
+
record(tool, summary, outcome, confirmedBy) {
|
|
67
|
+
if (!this.policy.auditLog)
|
|
68
|
+
return;
|
|
69
|
+
const line = JSON.stringify({
|
|
70
|
+
at: new Date().toISOString(),
|
|
71
|
+
surface: this.surface,
|
|
72
|
+
tool: tool.name,
|
|
73
|
+
risk: tool.risk,
|
|
74
|
+
summary,
|
|
75
|
+
outcome,
|
|
76
|
+
...(confirmedBy ? { confirmed_by: confirmedBy } : {}),
|
|
77
|
+
});
|
|
78
|
+
try {
|
|
79
|
+
appendFileSync(this.policy.auditLog, `${line}\n`, { mode: 0o600 });
|
|
80
|
+
}
|
|
81
|
+
catch {
|
|
82
|
+
// A broken audit log must never take the tool call down with it.
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
/** Why a tool needs confirming, in the words a refusal and an approval form both use. */
|
|
87
|
+
export function consequence(tool) {
|
|
88
|
+
return tool.risk === "destructive" ? "is public or cannot be undone" : "has an effect that cannot be taken back";
|
|
89
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Slipway: one tool definition ships an MCP server and an agent-native CLI.
|
|
3
|
+
*/
|
|
4
|
+
export { slipway, stderrLogger } from "./app.js";
|
|
5
|
+
export type { App, AppDefinition, CliIO, DoctorCheck, DryRun, InvokeOptions, PromptDefinition, ResourceDefinition, ServiceSetting, } from "./app.js";
|
|
6
|
+
export { defineTool, isTool, toolkit } from "./tool.js";
|
|
7
|
+
export type { CacheOptions, Logger, Paginate, Risk, RunContext, Surface, SyncOptions, Tool, ToolContext, ToolDefinition, ToolExample } from "./tool.js";
|
|
8
|
+
export { emptyInput, inputJsonSchema, jsonSchema, outputJsonSchema, validate, CONFIRM_DESCRIPTION } from "./schema.js";
|
|
9
|
+
export type { InferInput, InferOutput, JsonSchema, Schema } from "./schema.js";
|
|
10
|
+
export { ApiError, AuthError, CanceledError, EXIT, NotConfiguredError, NotFoundError, RateLimitError, RefusedError, SlipwayError, TimeoutError, UsageError, httpError, toSlipwayError, } from "./errors.js";
|
|
11
|
+
export type { ErrorCode, ErrorPayload } from "./errors.js";
|
|
12
|
+
export { audio, content, file, image, resourceLink, text } from "./result.js";
|
|
13
|
+
export type { ContentResult } from "./result.js";
|
|
14
|
+
export { readPolicy, policyEnvNames } from "./policy.js";
|
|
15
|
+
export type { ConfirmMode, Policy, PolicyDefaults, ToolSurface } from "./policy.js";
|
|
16
|
+
export { fromOpenAPI, httpExecutor, openapiHash, readOperations, toJsonSchema, toolName } from "./openapi.js";
|
|
17
|
+
export type { Executor, FromOpenAPIOptions, HttpExecutorOptions, Operation, OperationInput, OperationParameter, SkippedOperation } from "./openapi.js";
|
|
18
|
+
export type { BackgroundJob, JobDefinition, JobProgress, JobResult, ServiceJob } from "./jobs.js";
|
|
19
|
+
export type { DataStore, SearchHit, SyncedTool } from "./data.js";
|
|
20
|
+
export type { SyncReport } from "./sync.js";
|
|
21
|
+
export { CLIENTS } from "./install.js";
|
|
22
|
+
export type { ClientId } from "./install.js";
|
|
23
|
+
export { Secrets } from "./redact.js";
|
|
24
|
+
export { annotationsFor, CACHE_META, MAX_RESULT_SIZE_CHARS, REQUIRES_USER_INTERACTION } from "./server.js";
|
|
25
|
+
export { renderDocs, settingsTable, toolReference, toolTable } from "./docs.js";
|
|
26
|
+
/** The Zod instance Slipway is built against. Use it, so every schema in an app comes from one copy. */
|
|
27
|
+
export { z } from "zod";
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Slipway: one tool definition ships an MCP server and an agent-native CLI.
|
|
3
|
+
*/
|
|
4
|
+
export { slipway, stderrLogger } from "./app.js";
|
|
5
|
+
export { defineTool, isTool, toolkit } from "./tool.js";
|
|
6
|
+
export { emptyInput, inputJsonSchema, jsonSchema, outputJsonSchema, validate, CONFIRM_DESCRIPTION } from "./schema.js";
|
|
7
|
+
export { ApiError, AuthError, CanceledError, EXIT, NotConfiguredError, NotFoundError, RateLimitError, RefusedError, SlipwayError, TimeoutError, UsageError, httpError, toSlipwayError, } from "./errors.js";
|
|
8
|
+
export { audio, content, file, image, resourceLink, text } from "./result.js";
|
|
9
|
+
export { readPolicy, policyEnvNames } from "./policy.js";
|
|
10
|
+
export { fromOpenAPI, httpExecutor, openapiHash, readOperations, toJsonSchema, toolName } from "./openapi.js";
|
|
11
|
+
export { CLIENTS } from "./install.js";
|
|
12
|
+
export { Secrets } from "./redact.js";
|
|
13
|
+
export { annotationsFor, CACHE_META, MAX_RESULT_SIZE_CHARS, REQUIRES_USER_INTERACTION } from "./server.js";
|
|
14
|
+
export { renderDocs, settingsTable, toolReference, toolTable } from "./docs.js";
|
|
15
|
+
/** The Zod instance Slipway is built against. Use it, so every schema in an app comes from one copy. */
|
|
16
|
+
export { z } from "zod";
|