@pipelex/sdk 0.32.0 → 0.34.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 +40 -5
- package/dist/artifacts.js +23 -9
- package/dist/artifacts.js.map +1 -1
- package/dist/cli/args.d.ts +41 -0
- package/dist/cli/args.js +96 -0
- package/dist/cli/args.js.map +1 -0
- package/dist/cli/bundle.d.ts +45 -0
- package/dist/cli/bundle.js +148 -0
- package/dist/cli/bundle.js.map +1 -0
- package/dist/cli/environment.d.ts +36 -0
- package/dist/cli/environment.js +72 -0
- package/dist/cli/environment.js.map +1 -0
- package/dist/cli/help.d.ts +4 -0
- package/dist/cli/help.js +56 -0
- package/dist/cli/help.js.map +1 -0
- package/dist/cli/inputs.d.ts +13 -0
- package/dist/cli/inputs.js +70 -0
- package/dist/cli/inputs.js.map +1 -0
- package/dist/cli/io.d.ts +79 -0
- package/dist/cli/io.js +96 -0
- package/dist/cli/io.js.map +1 -0
- package/dist/cli/main.d.ts +18 -0
- package/dist/cli/main.js +58 -0
- package/dist/cli/main.js.map +1 -0
- package/dist/cli/method.d.ts +56 -0
- package/dist/cli/method.js +108 -0
- package/dist/cli/method.js.map +1 -0
- package/dist/cli/present.d.ts +43 -0
- package/dist/cli/present.js +191 -0
- package/dist/cli/present.js.map +1 -0
- package/dist/cli/run.d.ts +17 -0
- package/dist/cli/run.js +290 -0
- package/dist/cli/run.js.map +1 -0
- package/dist/cli/script.d.ts +23 -0
- package/dist/cli/script.js +212 -0
- package/dist/cli/script.js.map +1 -0
- package/dist/cli/source.d.ts +49 -0
- package/dist/cli/source.js +71 -0
- package/dist/cli/source.js.map +1 -0
- package/dist/cli.d.ts +10 -0
- package/dist/cli.js +65 -0
- package/dist/cli.js.map +1 -0
- package/dist/client.d.ts +125 -17
- package/dist/client.js +383 -109
- package/dist/client.js.map +1 -1
- package/dist/codegen-check.d.ts +9 -0
- package/dist/codegen-check.js +12 -6
- package/dist/codegen-check.js.map +1 -1
- package/dist/error-models.d.ts +60 -2
- package/dist/error-models.js +169 -3
- package/dist/error-models.js.map +1 -1
- package/dist/error-verdicts.d.ts +34 -0
- package/dist/error-verdicts.js +69 -0
- package/dist/error-verdicts.js.map +1 -0
- package/dist/errors.d.ts +250 -48
- package/dist/errors.js +330 -64
- package/dist/errors.js.map +1 -1
- package/dist/hooks/validate-client.d.ts +3 -3
- package/dist/hooks/validate-client.js +3 -3
- package/dist/index.d.ts +4 -4
- package/dist/index.js +3 -2
- package/dist/index.js.map +1 -1
- package/dist/prepare-inputs.d.ts +6 -0
- package/dist/prepare-inputs.js +46 -14
- package/dist/prepare-inputs.js.map +1 -1
- package/dist/runs.d.ts +28 -1
- package/dist/runs.js +2 -1
- package/dist/runs.js.map +1 -1
- package/dist/upload-grant.d.ts +15 -10
- package/dist/upload-grant.js +36 -17
- package/dist/upload-grant.js.map +1 -1
- package/dist/upload.d.ts +6 -0
- package/dist/upload.js +25 -7
- package/dist/upload.js.map +1 -1
- package/dist/user-agent.d.ts +3 -2
- package/dist/user-agent.js +3 -2
- package/dist/user-agent.js.map +1 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/docs/architecture.md +15 -9
- package/docs/artifact-download.md +3 -3
- package/docs/cli.md +123 -0
- package/docs/client-identification.md +1 -1
- package/docs/errors.md +96 -11
- package/docs/input-preparation.md +16 -8
- package/docs/run-results.md +20 -0
- package/package.json +5 -1
package/dist/upload.d.ts
CHANGED
|
@@ -28,6 +28,12 @@ export interface UploadRecord {
|
|
|
28
28
|
export interface UploadFileOptions {
|
|
29
29
|
filename?: string;
|
|
30
30
|
contentType?: string;
|
|
31
|
+
/**
|
|
32
|
+
* Read once the asset's bytes are in hand, right before the upload request: once it has
|
|
33
|
+
* aborted, nothing is uploaded and `uploadFile` throws the abort, however long the read took.
|
|
34
|
+
* A request already sent runs to its end. It is not sent to the server.
|
|
35
|
+
*/
|
|
36
|
+
signal?: AbortSignal;
|
|
31
37
|
}
|
|
32
38
|
/** The subset of the client `uploadFile` needs — the raw base64 `upload` wire call. */
|
|
33
39
|
export interface UploadCapableClient {
|
package/dist/upload.js
CHANGED
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
*/
|
|
10
10
|
import { InvalidLocalSourceError, RejectedAssetError, UploadAuthenticationError, UploadTransportError, UnsupportedUploadCapabilityError, } from "./errors.js";
|
|
11
11
|
import { ApiResponseError, ApiUnreachableError } from "./errors.js";
|
|
12
|
+
import { throwIfAborted } from "./runs.js";
|
|
12
13
|
const DEFAULT_CONTENT_TYPE = "application/octet-stream";
|
|
13
14
|
const DEFAULT_FILENAME = "upload.bin";
|
|
14
15
|
const EXTENSION_MIME = {
|
|
@@ -112,6 +113,8 @@ function assetBytesFromBuffer(bytes, options) {
|
|
|
112
113
|
*/
|
|
113
114
|
export async function uploadFile(client, asset, options = {}) {
|
|
114
115
|
const { bytes, filename, contentType } = await toAssetBytes(asset, options);
|
|
116
|
+
// A slow read, or a Blob's `arrayBuffer()`, may outlast the caller's patience.
|
|
117
|
+
throwIfAborted(options.signal);
|
|
115
118
|
const data = bytesToBase64(bytes);
|
|
116
119
|
let uploaded;
|
|
117
120
|
try {
|
|
@@ -120,42 +123,57 @@ export async function uploadFile(client, asset, options = {}) {
|
|
|
120
123
|
catch (error) {
|
|
121
124
|
throw mapUploadError(error, filename);
|
|
122
125
|
}
|
|
123
|
-
|
|
126
|
+
// The record guarantees a filename: an answer that names none keeps the one sent.
|
|
127
|
+
const stored = uploaded.filename;
|
|
128
|
+
return {
|
|
129
|
+
uri: uploaded.uri,
|
|
130
|
+
filename: typeof stored === "string" ? stored : filename,
|
|
131
|
+
contentType,
|
|
132
|
+
size: bytes.length,
|
|
133
|
+
};
|
|
124
134
|
}
|
|
125
135
|
/** Translate a raw `upload()` transport error into the matching preparation error. */
|
|
126
136
|
function mapUploadError(error, filename) {
|
|
127
137
|
if (error instanceof ApiResponseError) {
|
|
138
|
+
// A 2xx the client could not read: storage may hold the file, but under no reference the SDK
|
|
139
|
+
// can return.
|
|
140
|
+
if (error.status >= 200 && error.status < 300) {
|
|
141
|
+
return new UploadTransportError(`Upload of "${filename}" was answered (${error.status}) with a body the SDK could not ` +
|
|
142
|
+
"read, so whether and where the file was stored is unknown.", { cause: error, status: error.status, code: "unexpected", filename });
|
|
143
|
+
}
|
|
128
144
|
switch (error.status) {
|
|
129
145
|
case 413:
|
|
130
146
|
return new RejectedAssetError(`The server rejected "${filename}": ${error.serverMessage ?? "asset exceeds the service size limit"}.`, filename, error.status, { cause: error, code: "too_large" });
|
|
131
147
|
case 401:
|
|
132
148
|
case 403:
|
|
133
|
-
return new UploadAuthenticationError(`Upload of "${filename}" was not authorized (${error.status}). Check the configured Pipelex API key.`, error.status, { cause: error });
|
|
149
|
+
return new UploadAuthenticationError(`Upload of "${filename}" was not authorized (${error.status}). Check the configured Pipelex API key.`, error.status, { cause: error, filename });
|
|
134
150
|
case 404:
|
|
135
151
|
return new UnsupportedUploadCapabilityError("The configured Pipelex deployment does not support file upload (no /v1/upload route). " +
|
|
136
|
-
"Upload is a hosted Pipelex capability.", { cause: error });
|
|
152
|
+
"Upload is a hosted Pipelex capability.", { cause: error, filename });
|
|
137
153
|
default:
|
|
138
154
|
return new UploadTransportError(`Upload of "${filename}" failed (${error.status}): ${error.serverMessage ?? error.statusText}.`, {
|
|
139
155
|
cause: error,
|
|
140
156
|
status: error.status,
|
|
141
157
|
code: error.status >= 500 ? "server_error" : "unexpected",
|
|
158
|
+
filename,
|
|
142
159
|
});
|
|
143
160
|
}
|
|
144
161
|
}
|
|
145
162
|
if (error instanceof ApiUnreachableError) {
|
|
146
163
|
return new UploadTransportError(`Upload of "${filename}" could not reach the Pipelex API (${error.code ?? "unreachable"}).`,
|
|
147
164
|
// The client's own request timeout surfaces as an unreachable host with this code.
|
|
148
|
-
{ cause: error, code: error.code === "ABORT_TIMEOUT" ? "timeout" : "unreachable" });
|
|
165
|
+
{ cause: error, code: error.code === "ABORT_TIMEOUT" ? "timeout" : "unreachable", filename });
|
|
149
166
|
}
|
|
150
167
|
// Only errors thrown by the `client.upload()` call reach here — the local-source
|
|
151
168
|
// and asset-type failures are raised in `toAssetBytes`, before this try block. Any
|
|
152
|
-
// error that is neither of the two mapped types (a
|
|
153
|
-
//
|
|
154
|
-
//
|
|
169
|
+
// error that is neither of the two mapped types (a custom client throwing a plain
|
|
170
|
+
// Error or a SyntaxError of its own) is wrapped so every upload failure stays
|
|
171
|
+
// catchable as an InputPreparationError, never a raw escape.
|
|
155
172
|
const detail = error instanceof Error ? error.message : String(error);
|
|
156
173
|
return new UploadTransportError(`Upload of "${filename}" failed unexpectedly: ${detail}.`, {
|
|
157
174
|
cause: error,
|
|
158
175
|
code: "unexpected",
|
|
176
|
+
filename,
|
|
159
177
|
});
|
|
160
178
|
}
|
|
161
179
|
//# sourceMappingURL=upload.js.map
|
package/dist/upload.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"upload.js","sourceRoot":"","sources":["../src/upload.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,EACL,uBAAuB,EACvB,kBAAkB,EAClB,yBAAyB,EACzB,oBAAoB,EACpB,gCAAgC,GACjC,MAAM,aAAa,CAAC;AACrB,OAAO,EAAE,gBAAgB,EAAE,mBAAmB,EAAE,MAAM,aAAa,CAAC;
|
|
1
|
+
{"version":3,"file":"upload.js","sourceRoot":"","sources":["../src/upload.ts"],"names":[],"mappings":"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"}
|
package/dist/user-agent.d.ts
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Client identification: the `User-Agent` this SDK sends to the Pipelex API.
|
|
3
3
|
*
|
|
4
|
-
* Implements the
|
|
5
|
-
* value is a sequence of RFC 9110 product tokens,
|
|
4
|
+
* Implements the spec `conformance/specs/client-identification.md`, in the `conformance`
|
|
5
|
+
* repository beside the specs' tests. The value is a sequence of RFC 9110 product tokens,
|
|
6
|
+
* outermost first:
|
|
6
7
|
*
|
|
7
8
|
* [<appInfo>] pipelex-sdk-js/<SDK_VERSION> [<runtime>/<version> (<os>; <arch>)]
|
|
8
9
|
*
|
package/dist/user-agent.js
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Client identification: the `User-Agent` this SDK sends to the Pipelex API.
|
|
3
3
|
*
|
|
4
|
-
* Implements the
|
|
5
|
-
* value is a sequence of RFC 9110 product tokens,
|
|
4
|
+
* Implements the spec `conformance/specs/client-identification.md`, in the `conformance`
|
|
5
|
+
* repository beside the specs' tests. The value is a sequence of RFC 9110 product tokens,
|
|
6
|
+
* outermost first:
|
|
6
7
|
*
|
|
7
8
|
* [<appInfo>] pipelex-sdk-js/<SDK_VERSION> [<runtime>/<version> (<os>; <arch>)]
|
|
8
9
|
*
|
package/dist/user-agent.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"user-agent.js","sourceRoot":"","sources":["../src/user-agent.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"user-agent.js","sourceRoot":"","sources":["../src/user-agent.ts"],"names":[],"mappings":"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"}
|
package/dist/version.d.ts
CHANGED
package/dist/version.js
CHANGED
package/docs/architecture.md
CHANGED
|
@@ -32,9 +32,11 @@
|
|
|
32
32
|
|
|
33
33
|
The package is ESM-only (`"type": "module"`, `dist/index.js` built by `tsc` with `module: NodeNext`) — there is no separate CJS build. CJS consumers can still load it: the `exports` map declares a `default` condition alongside `import`, both pointing at the same ESM `dist/index.js`, so `require("@pipelex/sdk")` resolves through Node's `require(ESM)` support rather than a transpiled dual build. That support needs Node 20.19+ or 22.12+ (unflagged); `engines.node` is pinned to `>=22.12.0` to match the workspace's Node 22 floor. This only works because `dist/` has no top-level await — if that ever changes, `require()` of the module will throw and a real dual ESM+CJS build (or a `require`-only CJS entrypoint) would be needed instead.
|
|
34
34
|
|
|
35
|
+
The package also declares one `bin`, `pipelex-sdk`, which is `dist/cli.js` with a `#!/usr/bin/env node` first line, so that `npx @pipelex/sdk` runs it ([`cli.md`](cli.md)). It is not in the `exports` map, so no consumer can import it, and it uses no top-level `await` either. `make test-pack` packs the package and runs the command from the tarball through `npx`, from a directory outside the package, which is how CI proves the published artifact exposes it.
|
|
36
|
+
|
|
35
37
|
Condition order in `exports` matters: `types` is listed first (TS/Node resolution convention — the type-checker looks at the first matching condition), then `import`, then `default` as the CJS fallback.
|
|
36
38
|
|
|
37
|
-
The map has a second entry, **`@pipelex/sdk/upload`** (`dist/upload-grant.js`), the browser-safe one. The main entry is Node-first — `upload.ts` names `node:fs/promises` and `artifacts.ts` names `undici`, so a browser-targeting bundler must mark them external — because the client holds an API key and belongs on a server. The subpath carries only what a page needs to store a file with an upload grant: `uploadWithGrant`, the grant types and the three error classes it raises. Its runtime import graph is `errors.ts
|
|
39
|
+
The map has a second entry, **`@pipelex/sdk/upload`** (`dist/upload-grant.js`), the browser-safe one. The main entry is Node-first — `upload.ts` names `node:fs/promises` and `artifacts.ts` names `undici`, so a browser-targeting bundler must mark them external — because the client holds an API key and belongs on a server. The subpath carries only what a page needs to store a file with an upload grant: `uploadWithGrant`, the grant types and the three error classes it raises. Its runtime import graph is `errors.ts` (with the fallback table it reads, `error-verdicts.ts`), `timers.ts` and `mthds/protocol`, nothing else, and the unit suite bundles it for the browser with esbuild, nothing marked external, so a Node-only module becoming reachable fails a test rather than a consumer's build. Both entries load the same `dist/errors.js` and `dist/upload-grant.js`, so the function and the error classes are identical whichever entry a consumer imports them through. Whether the main entry should become browser-safe as a whole is a separate question, because it touches how path strings are read.
|
|
38
40
|
|
|
39
41
|
## Wire conventions
|
|
40
42
|
|
|
@@ -86,18 +88,20 @@ Flat `src/` (mirrors `mthds-js`'s `runners/api` flatness — the SDK has one cli
|
|
|
86
88
|
- `client.ts` — `PipelexApiClient`: the `request()` pipeline (auth, base URL, timeouts/abort, problem-details parsing) and every route. `implements MTHDSProtocol<DictPipeOutput>` so the protocol-execution methods stay shaped like the standard.
|
|
87
89
|
- `models.ts` — Dict-serialized concretes (`DictStuff` / `DictWorkingMemory` / `DictPipeOutput` / `DictRunResultExecute`), the `/v1/validate` surface (`PipelexValidationResult`, `ValidationErrorItem`, and the `SuggestedFix` / `FixOp` repair vocabulary its items carry — the standard's declarations, restated because `mthds` exports them only from its package root and pinned to it by a type-level test, see [errors.md](./errors.md#a-validation-item--validationerroritem)), the tools surface (`Diagnostic` / `LintResponse` / `FormatResponse`), and the crate-route request/response models — the shared `MthdsFileItem` / `CrateRequestBase` / `CrateInvalidReport` vocabulary plus the `/v1/resolve`, `/v1/codegen` and `/v1/pipe-io` shapes built on it, `GeneratedArtifact` among them.
|
|
88
90
|
- `product-models.ts` — the snake_case wire shapes for the hosted-product management routes (profile, methods catalog, organizations, billing, API keys, onboarding, storage, runs list/detail/update). Each interface models only the fields the product actually consumes — no speculative mirrors.
|
|
89
|
-
- `error-models.ts` — the error-report wire models: `RunErrorReport`, the runner's `ErrorReport` typed with every field it carries (with `UserAction`, `ProviderErrorMetadata` and `MigrationErrorBlock`), which is the one type for a failed run's report on `RunPublic.error`, `PipelineRun.error`, `RunHistoryItem.error`, the failed arm of `RunResultState` and `RunFailedError.error`; and `ProblemDetails` / `FieldError`, the typed members of a problem document that `ApiResponseError` exposes. Every field is optional and every interface keeps an index signature, because the runner owns the shape and adds fields without asking.
|
|
91
|
+
- `error-models.ts` — the error-report wire models: `RunErrorReport`, the runner's `ErrorReport` typed with every field it carries (with `UserAction`, `ProviderErrorMetadata` and `MigrationErrorBlock`), which is the one type for a failed run's report on `RunPublic.error`, `PipelineRun.error`, `RunHistoryItem.error`, the failed arm of `RunResultState` and `RunFailedError.error`; and `ProblemDetails` / `FieldError`, the typed members of a problem document that `ApiResponseError` exposes. Every field is optional and every interface keeps an index signature, because the runner owns the shape and adds fields without asking. It also holds the one function that checks a stored report, the internal `readRunErrorReport`, which every read handing a report back applies: each named field is kept only when it has its declared type, and members the SDK does not name are relayed as sent. It imports the `ValidationErrorItem` type and nothing else. See [errors.md](./errors.md#a-failed-run--runerrorreport).
|
|
90
92
|
- `method-source.ts` — `methodSourceToContents`, the canonical parser turning a stored method's polymorphic `mthds` source (raw single-bundle text XOR a JSON `{name, content}[]` file array) into a flat list of file contents. A verbatim port of the platform's `method_source_to_contents` so the SDK and runtime read a stored source identically; exported from the barrel and consumed by `getMethodClosure`. See [input-preparation.md](./input-preparation.md#getmethodclosure--the-explicit-expansion-utility).
|
|
91
|
-
- `codegen-check.ts` — `runCodegenCheck`, the **pure** offline drift check over a committed codegen tree: the stamp grammar, the `codegen.lock` parser, the hash, and the drift taxonomy, ported from pipelex's `codegen/check.py` so a verdict computed here equals the one `pipelex codegen check` computes over the same bytes (the drift `detail` sentences included, verbatim). It touches neither the client nor `mthds/protocol` — its only
|
|
93
|
+
- `codegen-check.ts` — `runCodegenCheck`, the **pure** offline drift check over a committed codegen tree: the stamp grammar, the `codegen.lock` parser, the hash, and the drift taxonomy, ported from pipelex's `codegen/check.py` so a verdict computed here equals the one `pipelex codegen check` computes over the same bytes (the drift `detail` sentences included, verbatim). It touches neither the client nor `mthds/protocol` — its only runtime imports are `smol-toml` and the plain-object guard of the pure `error-models.ts` — and it hashes through WebCrypto rather than `node:crypto` so it adds no Node builtin to the barrel's import graph (`upload.ts` still names `node:fs/promises`, so a browser-targeting bundler must mark `node:*` external). It raises `CodegenLockError`, which extends `Error` rather than the protocol base and declares the error verdict itself (`input`, not retryable). No filesystem, no fetch, no API key — the caller walks its own tree and hands in the text. See [crate-routes.md](./crate-routes.md#the-offline-check--runcodegencheck).
|
|
92
94
|
- `upload-grant.ts` — `uploadWithGrant`, the sender of an upload grant (`requestUploadGrant`, `POST /v1/upload/grant`): the file as the raw body of a `PUT` to the grant's URL with its signed headers unchanged, redirects refused, the whole exchange bounded by a time limit scaled by the file's size unless the caller passes `timeoutMs`, only the first 16 KiB of storage's error body read, and storage's refusals mapped onto the input-preparation errors (`RejectedAssetError` with a `code` for a `4xx` such as the `412` of a used grant or the `403` of a signature mismatch, `UploadTransportError` with a `code` for the time limit running out, a `5xx`, storage's `RequestTimeout`, a `409` conflict, a redirect or no response). It is the browser-safe `@pipelex/sdk/upload` entry, so it imports the error classes, the timer bound in `timers.ts` and the wire types only, never the client. See [input-preparation.md](./input-preparation.md#uploading-with-a-grant--requestuploadgrant--uploadwithgrant).
|
|
93
95
|
- `artifacts.ts` — the artifact stack, the download twin of `prepareInputs`, in layers: `locateArtifacts` (a pure, exported walk for `pipelex-storage://` references, each with every `$`-rooted path it sits at) and `collectArtifacts` (the same walk's bare references), `resolveArtifacts` (the bulk resolve route over a whole list, chunked at its bound), `fetchArtifact` (one reference as a bounded `Response`) and `downloadArtifacts` (Node-only, a run's produced files saved under a directory as a produced verdict), plus `artifactFilename`, the rule that names a saved file after the field it fills. The three network operations are client methods over a small structural seam (`ArtifactCapableClient`: the raw `resolveStorageUrls` call and `getRunResult`); the module's static import graph stays free of Node builtins, because `node:fs/promises`, `node:path` and `undici` are loaded by dynamic import behind `isNodeRuntime()` — a browser-targeting bundler still has to mark `node:*` and `undici` external, as it already must for `upload.ts`. `undici` is a runtime dependency for one reason: the fetch runs on an `Agent` carrying `headersTimeout` and `bodyTimeout`, the per-stall bounds an `AbortSignal` budget does not give, and Node exposes its bundled undici's `fetch` but not its `Agent`. See [artifact-download.md](./artifact-download.md).
|
|
94
96
|
- `runs.ts` — the run-lifecycle types (`RunStatus`, `RunRead`, `RunResults`, `RunResultState`) and the single poll loop (`pollUntilResult`). `RunResults.main_stuff` (the resolved main output content) is always present for a completed run: on the hosted path it is the `main_stuff.json` S3 artifact; on the bare-runner blocking path the SDK resolves it from the returned working memory via the response's `main_stuff_name`, so both paths deliver the same shape. Consumers read `main_stuff` directly. `working_memory` — every named stuff of the run, `{ root, aliases }`, typed `DictWorkingMemory` from `models.ts` — reads the same on both paths: the hosted path relays the `working_memory.json` artifact, and the blocking path lifts it off `pipe_output`, which still rides whole (blocking path only). `graph_spec` — the executed graph — also reaches the client on both paths: the hosted path relays the `graphspec.json` artifact, and the blocking path lifts it off `pipe_output` beside `graph_assembly_error`, the graph's twin of `usage_assembly_error`. That error field is declared ahead of the wire on the hosted side, where the platform's results body relays no such key and the field is therefore absent, reading `undefined` rather than `null`. A completed run that cannot deliver a main stuff throws `MissingMainStuffError` when the read asked for it. `runs.ts` also declares `RUN_RESULT_ARTIFACTS` / `RunResultArtifact`, the artifacts a results read can be narrowed to with `getRunResult`'s `artifacts` option; with a selection an unselected artifact is absent while a selected unwritten one is `null`. `runs.ts` also owns `TokensUsageRecord` — one client-facing usage record per inference call, carried by `RunResults.tokens_usages` on both paths. That is a mirror of a wire contract this SDK does not own (inference accounting is a Pipelex runtime extension rather than part of the MTHDS standard): every field is optional and the interface carries an index signature so pre-contract artifacts (relayed verbatim, never migrated) still type-check. See [`run-usage.md`](./run-usage.md) for the field reference, the cost/null semantics, and the old-artifact rules, and [`run-results.md`](./run-results.md) for the field-by-field account of `RunResults` — the durable handle, the output and how to read it, the run's working memory, the executed graph and how to render or keep it, and the produced files with their short-lived links.
|
|
95
97
|
- `usage.ts` — `summarizeUsage`, the **pure** fold of a run's usage pair (`tokens_usages` / `usage_assembly_error`) into one `UsageSummary`: a `state` (`records`, `no_inference` or `unavailable`), the null-aware total cost with its `cost_partial` flag, the `input` and `output` token totals, the call count, the relayed assembly error, and a `by_pipe` rollup sorted by cost with the unattributed calls grouped under a `null` `pipe_code`. It applies the rules [`run-usage.md`](./run-usage.md#summarizing-a-run--summarizeusage) states, so that every consumer reads one computation rather than re-deriving those rules by hand. It imports types from `runs.ts` and nothing else: no client, no fetch, no `mthds/protocol`.
|
|
96
98
|
- `execute-result.ts` — `PipelexExecuteResult`, a `DictRunResultExecute` subtype that adds a resolved `.main_stuff` accessor (dug out of `pipe_output`'s working memory via the response's `main_stuff_name`, throwing `MissingMainStuffError` if unlocatable). `execute()` returns it, giving blocking and durable results the **same output accessor** — `result.main_stuff` — so callers never branch on which path ran. The neutral `mthds` wire model leaves `main_stuff_name` in its extension index signature; this Pipelex-branded subtype declares it as a typed field (Pipelex owns that concept). `resultsFromExecute` (the public lift from that result onto `RunResults`, in the same module) reads that accessor too, keeping the resolution single-sourced; the `executeBlocking` fallback is its only caller inside the SDK, and a consumer driving `execute()` directly calls it by name.
|
|
97
|
-
- `errors.ts` — the transport and run-lifecycle
|
|
98
|
-
- `
|
|
99
|
+
- `errors.ts` — the error verdict and the typed errors. The verdict is `ErrorDomain` (`input`, `config`, `runtime`: who can fix the failure) and `ErrorVerdict` (that domain and `retryable`), carried as two readonly own properties by the abstract `PipelexRequestError`, which refines the protocol-base `PipelineRequestError` (re-exported from `mthds/protocol`) and takes the verdict as a constructor argument, so every subclass declares its verdict where it is defined and one that forgets it does not compile; `errorVerdictOf` reads the pair structurally from anything a `catch` holds. Under that base sit `RequestArgumentError` (the client refusing a call's arguments before sending any request, `input` unless the site declares otherwise, as the invalid base URL declares `config`), the transport and run-lifecycle errors, `PagingNotTerminatingError`, the input-preparation family under `InputPreparationError`, and the artifact family under `ArtifactOperationError` (`ScopeUnavailableError`, `ArtifactFetchError`, `ArtifactAuthenticationError`, the last carrying the download verdict so far); both family bases take an optional verdict for the sites, and the consumer subclasses, whose verdict is not the family's. `ApiResponseError` carries every member of the problem document it was built from, its nested members checked one level down by the readers a stored report goes through, and also stands for a 2xx answer the SDK could not read — the branch fields `errorDomain` and `type`, `retryable`, `userAction`, `requestId` (the body's, else the `X-Request-ID` header's), the native `code` and `errorType`, the platform's `errors[]`, the inference members, and the decoded document whole as `problemDocument` — and resolves `retryable` and `errorDomain` from the document's values when they are valid and the fallback's otherwise, except that a valid `input` or `config` domain sent without `retryable` is not retryable. Its member names match `mthds`'s own `ApiResponseError` wherever the two overlap, and so do their types except the verdict, which this SDK narrows to always decided; a type-level test pins the shared `ProblemDetails` and `UserAction` because this package may not import `mthds/errors`. `RunFailedError` carries the run's `status` and its stored report as `error`, and takes its verdict from that report. It is not the whole error surface: `codegen-check.ts` exports `CodegenLockError`, which extends plain `Error` because nothing was requested over the wire, and declares the two verdict members itself. See [errors.md](./errors.md#the-verdict-every-error-carries).
|
|
100
|
+
- `error-verdicts.ts` — `fallbackVerdict`, the table an `ApiResponseError` falls back to for a member the server did not send: the HTTP status, the platform `code`, and whether the body is named, which decides a `404`. Internal and pure. `pipelex-sdk` (Python) must apply the same table: `tests/fixtures/error-verdicts.json`, the case file this package's suite drives, is the contract, and the Python suite is to drive its own code through an identical copy of it once that copy is added.
|
|
101
|
+
- `user-agent.ts` — client identification per the spec `conformance/specs/client-identification.md`, in the `conformance` repository beside the specs' tests: the exported `AppInfo` option, its RFC 9110 token validation (a `TypeError` on a bad field, a `RangeError` past the 512-character ceiling), its rendering, the runtime detection (Node, Bun, Deno, or a browser, where no header is set) and `buildUserAgent`, which the client calls once at construction. The barrel re-exports `buildUserAgent`, `validateAppInfo`, `MAX_USER_AGENT_LENGTH` and the `RuntimeInfo` type, so a consumer's raw fetches send the same value. The client's private `requestHeaders` is the one place API request headers are built, so every route carries the value; the artifact fetch of a presigned link never goes through it. See [client-identification.md](./client-identification.md).
|
|
99
102
|
- `timers.ts` — `MAX_TIMER_DELAY_MS`, the longest delay `setTimeout` honours, and `isTimerDelay`, which every caller-supplied `timeoutMs` is checked against before it reaches a timer: a longer delay overflows and fires at once. It imports nothing, so the browser-safe upload entry uses it too.
|
|
100
103
|
- `version.ts` — `SDK_VERSION`, in its own module so the client stamps it on the `User-Agent` without importing the barrel, which re-exports it.
|
|
104
|
+
- `cli.ts` and `cli/` — the `pipelex-sdk` command, `run` and `script` ([`cli.md`](cli.md)): `cli.ts` is the executable `package.json` declares as the package's one `bin`, handing the process to `cli/main.ts`'s `runCommand`, and `cli/` holds the flags, the bundle reader, the inputs, the environment, the two subcommands and what they print. It sits at the TOP of the graph, the other way round from every other module: nothing imports it, the barrel does not export it, and it reaches the SDK only through `index.ts`, which `dependency-cruiser` enforces in both directions.
|
|
101
105
|
- `index.ts` — the public barrel: `export * from "mthds/protocol"` (single import source for the standard surface) plus the client, models, run types, the standalone pure helpers (`runCodegenCheck`, `summarizeUsage`, `locateArtifacts`, `collectArtifacts`, `artifactFilename`), and errors.
|
|
102
106
|
|
|
103
107
|
## Client surface (current)
|
|
@@ -109,15 +113,15 @@ Flat `src/` (mirrors `mthds-js`'s `runners/api` flatness — the SDK has one cli
|
|
|
109
113
|
- **Method selectors on `validate`:** the first argument is inline contents (`string[]`) or a `ValidateMethodSelector` — `{ method_ref }` (runner-resolved by address) or `{ method_id }` (hosted, platform-resolved) — under the strict tooling XOR: exactly one selector, no linkage exception, `mthds_sources` legal only beside inline contents. A selector-resolution failure is a non-2xx `ApiResponseError`, never an `is_valid: false` verdict.
|
|
110
114
|
- **Crate routes (`/v1/resolve`, `/v1/codegen`, `/v1/pipe-io`):** `resolve` emits the **normalized library crate** (the MTHDS standard's Library Crate Format, fingerprint riding inside the payload); `codegen` projects that crate through two explicit axes — `kind` (`types`) × `target` (`ts-zod` / `python-pydantic` / `python-structures`) — into stamped artifacts plus their `codegen.lock`. All three share one closure envelope (`files: [{content, source?}]` XOR a `method_ref`, the address form server-resolved and the registry form a `501`) and return a **discriminated 200 verdict** (`is_valid`, with `CrateInvalidReport` as the shared invalid arm) rather than a bare payload, the same discipline as `validate`. They also take the hosted `method_id` (`PipelexHostedToolingExtensions`) as a third closure form under the strict three-way XOR — a pass-through the platform resolves, meaningless against a bare runner. Writing the artifacts and lock verbatim reproduces a local `pipelex codegen types` run byte-for-byte, so the offline `pipelex codegen check` passes — the SDK stays transport-only and does not write files. Verifying such a tree afterwards is `runCodegenCheck`, a standalone pure helper rather than a client method, because it needs no server at all. `pipeIo` derives a method's three I/O artifacts — the standard's `PipeIOContracts`, `InputForm` and `OutputForm`, imported from `mthds/protocol` — for the pipe the route selects (the request's qualified `pipe_ref`, else the method's own entry pipe) or for every pipe under `all_pipes`, with the runnability facts beside them and the closure's files echoed on `include_files`, and it runs no dry run; a selection it refuses is a `422` typed `EntryPipeNotFoundError` or `EntryPipeAmbiguousError`. See [crate-routes.md](./crate-routes.md).
|
|
111
115
|
- **Tools (`/v1/lint`, `/v1/format`):** `lint`, `format` — single-file static diagnostics and canonical formatting, served by any pipelex-api runner (no inference, no bundle load). Both are **diagnostic endpoints** like `validate`: a malformed `.mthds` file is a produced verdict on a **200** carrying `diagnostics[]`, never a thrown error — `format` additionally echoes the content back unchanged with `changed: false`. Only a no-verdict condition is non-2xx: a malformed body, **malformed formatter `options`** (e.g. a non-numeric `column_width` ⇒ 422), auth, or a server fault, all mapped to the typed `ApiResponseError`. `lint` is the cheap static check (syntax / schema / semantic); it does not replace `validate`, which loads the bundle, resolves across files, and dry-runs the pipes.
|
|
112
|
-
- **Durable run lifecycle (hosted extension — NOT protocol):** `getRunStatus`, `getRunResult` (with an optional `artifacts` selection sent as `?artifacts=`, see [run-results.md](./run-results.md#reading-only-some-artifacts--artifacts)), `waitForResult`, `startAndWaitForResult` (handshakes `/v1/version`, takes the durable start+poll path on a hosted deployment, and self-heals to the blocking `execute` against a bare runner). A run that ended without completing answers the results read with a `409` whose `run_status` and `error` members carry its status and its stored error report; `getRunResult` turns it into the `failed` arm (`status`, `message` from `detail`, `error`) and the waiting calls on the durable path into a `RunFailedError` carrying the same (the blocking fallback's failure is the runner's `ApiResponseError`), the status read from the member, or from the `detail` sentence on a platform that does not send it yet. The status read serves the same report as `RunRead.error`. See [errors.md](./errors.md#a-failed-run--runerrorreport). Reading what a completed run consumed is `summarizeUsage(results)`, a standalone pure helper rather than a client method, because it folds the usage pair the results already carry and needs no server. See [run-usage.md](./run-usage.md#summarizing-a-run--summarizeusage).
|
|
113
|
-
- **Health:** `health` (origin-level `/health`).
|
|
116
|
+
- **Durable run lifecycle (hosted extension — NOT protocol):** `getRunStatus`, `getRunResult` (with an optional `artifacts` selection sent as `?artifacts=`, see [run-results.md](./run-results.md#reading-only-some-artifacts--artifacts)), `waitForResult`, `startAndWaitForResult` (handshakes `/v1/version`, takes the durable start+poll path on a hosted deployment, and self-heals to the blocking `execute` against a bare runner). The handshake's outcome is cached for the client's lifetime, under the rule the Python SDK follows too: an answer that is not a usable version assumes hosted and lets the start surface the real error, while no answer at all throws its `ApiUnreachableError` at once, caches nothing and sends no start. An answer is anything the server sent back: an error status, a body that is not JSON, not UTF-8 or no version, and a body that arrived but could not be decoded, such as a broken gzip stream, which fetch fails with the decompressor's code (`Z_DATA_ERROR` and the other zlib codes, brotli's `ERR__ERROR_…`, zstd's `ZSTD_error_…`). A run that ended without completing answers the results read with a `409` whose `run_status` and `error` members carry its status and its stored error report; `getRunResult` turns it into the `failed` arm (`status`, `message` from `detail`, `error`) and the waiting calls on the durable path into a `RunFailedError` carrying the same (the blocking fallback's failure is the runner's `ApiResponseError`), the status read from the member, or from the `detail` sentence on a platform that does not send it yet. The status read serves the same report as `RunRead.error`. See [errors.md](./errors.md#a-failed-run--runerrorreport). Reading what a completed run consumed is `summarizeUsage(results)`, a standalone pure helper rather than a client method, because it folds the usage pair the results already carry and needs no server. See [run-usage.md](./run-usage.md#summarizing-a-run--summarizeusage).
|
|
117
|
+
- **Health:** `health` (origin-level `/health`), on the same transport as every route: an unreachable origin is an `ApiUnreachableError` and a non-2xx answer an `ApiResponseError` naming `/health`, whose verdict the fallback reads from the status. A 2xx whose body is not a JSON object is an `ApiResponseError` too, as on every protocol, extension and run-lifecycle route.
|
|
114
118
|
|
|
115
119
|
### Product routes (hosted management surface)
|
|
116
120
|
|
|
117
|
-
The hosted catalog/account routes the webapp drives. All ride the same `{base}/v1/*` + `Authorization: Bearer` + org-from-JWT contract as the protocol routes, go through the shared `requestProduct<T>` helper (30s management timeout, empty-body tolerant), and map a non-2xx RFC 9457 `problem+json` to a typed `ApiResponseError`. Consumers branch on **`type` and `errorDomain`**, as the
|
|
121
|
+
The hosted catalog/account routes the webapp drives. All ride the same `{base}/v1/*` + `Authorization: Bearer` + org-from-JWT contract as the protocol routes, go through the shared `requestProduct<T>` helper (30s management timeout, empty-body tolerant), and map a non-2xx RFC 9457 `problem+json` to a typed `ApiResponseError`. Every route parses a 2xx body through one private helper, so an answer that is not JSON (a gateway's HTML page, an empty body where one was due) is an `ApiResponseError` carrying its status and raw text, `runtime` and not retryable, rather than a bare `SyntaxError`. A product route whose answer the SDK or its caller reads before anything else (`listMethods`, `listRuns`, `getRunDetail`, `upload`, `requestUploadGrant`, `resolveStorageUrls`, and the methods routes answering one stored method) goes through `requestProductObject` or a helper built on the same read instead, which also refuses an empty body and JSON that is not an object; the two paged routes then refuse, the same way, through one page reader, a page whose `items` is not an array or whose `next_cursor` is neither a string nor `null`, `upload` an answer with no non-empty string `uri`, and the methods routes a stored `mthds` field that is neither a string nor absent and a stored `python` field that is not the serialized file list. Consumers branch on **`type` and `errorDomain`**, as the hosted-envelope spec (`conformance/specs/pipelex-hosted-envelope.md`, in the `conformance` repository) says, never on the HTTP status. The platform's `type` is `https://pipelex.com/errors/<code>`, one-to-one with its native `code` (`conflict`, `not_found`, `pipelex_api_key_limit_reached`, …), which stays on the error as the finer surface-specific code. The platform does not emit `error_domain` or `retryable` on its own refusals yet, so their `errorDomain` and `retryable` are the SDK's fallback, read from the status; every error carries both, decided. See [errors.md](./errors.md).
|
|
118
122
|
|
|
119
123
|
- **User profile:** `getMe` (`GET /v1/me`).
|
|
120
|
-
- **Methods catalog:** `listMethods`, `getMethod`, `createMethod`, `updateMethod` (rename = changed `name`), `deleteMethod`. **`deleteMethod` is asynchronous and its return type says so:** `DELETE /v1/methods/{id}` answers `202` with a `MethodDeletionAccepted` (`method_id`, `deletion_state`, `deletion_job_id`) the moment the platform has claimed the method and terminated its in-flight workflows — the rest of the cascade (runs, events, S3 objects) is enqueued. A resolved promise means "accepted", never "gone"; completion is the row disappearing from `listMethods`, not any field of that body. The claim is a conditional write, so a double-clicked delete is a `409 conflict` rather than a second cascade over the same runs, and an unknown or foreign-org id is a `404`. (This doc previously stated the SDK had no `delete` because the platform had no such route — the route has always existed; the claim was simply false.) `listMethods` returns **one page**, not the whole catalog — a `MethodPage` (`{items, nextCursor}`) served from a narrow index projection, so its cost is the page size rather than the size of the catalog. It used to return every method with its full `.mthds` bundle attached, which hit DynamoDB's 1 MB page cap at a couple hundred methods and came back truncated with no error and no flag, so methods silently vanished from the UI. Rows are therefore `MethodSummary`, **not** `MethodData`: no `mthds`, no `python`, no `updated_at`, because none of them is in the projection and putting `mthds` back is what restored the truncation. Reach for `getMethod(id)` when you need a method's source. `query.q` is applied server-side across the whole catalog, not over the page — filtering one page client-side would be searching 50 rows of 10,000 and calling it a search. `iterateMethods` is the async iterator that follows the cursor for callers who genuinely want everything (`for await (const m of client.iterateMethods())`); unlike `iterateRuns` it must keep going **past empty pages**, because `q` is a post-read filter over a bounded index slice per request, so `{items: [], nextCursor: "…"}` means "nothing matched in the slice I just read, keep going" — the difference is the server, not the client. It gives up only when the server stops advancing its cursor, and **throws** rather than returning a partial answer; like `iterateRuns` it is deliberately not a `listAllMethods(): Promise<MethodSummary[]>`, because an all-at-once helper needs a page cap and a cap means silently returning a truncated list. `MethodSummary.deletion_state` rides the list rows: a method mid-erasure stays listed so the UI can render it as "Deleting…", while `getMethod` refuses it with a 409. Full read responses (`MethodData`, from `getMethod`) carry `org_id`, `created_by_user_id`, and a server-derived `description` (parsed read-side from the bundle's top-level `description`) on top of the write contract's `name` / `mthds` / `input_data`. `getMethodClosure` is a client-side semantic layer over `getMethod` (not a route) that parses the stored method's polymorphic `mthds` source into a runnable, provenance-labelled closure — see [input-preparation.md](./input-preparation.md#getmethodclosure--the-explicit-expansion-utility).
|
|
124
|
+
- **Methods catalog:** `listMethods`, `getMethod`, `createMethod`, `updateMethod` (rename = changed `name`), `deleteMethod`. **`deleteMethod` is asynchronous and its return type says so:** `DELETE /v1/methods/{id}` answers `202` with a `MethodDeletionAccepted` (`method_id`, `deletion_state`, `deletion_job_id`) the moment the platform has claimed the method and terminated its in-flight workflows — the rest of the cascade (runs, events, S3 objects) is enqueued. A resolved promise means "accepted", never "gone"; completion is the row disappearing from `listMethods`, not any field of that body. The claim is a conditional write, so a double-clicked delete is a `409 conflict` rather than a second cascade over the same runs, and an unknown or foreign-org id is a `404`. (This doc previously stated the SDK had no `delete` because the platform had no such route — the route has always existed; the claim was simply false.) `listMethods` returns **one page**, not the whole catalog — a `MethodPage` (`{items, nextCursor}`) served from a narrow index projection, so its cost is the page size rather than the size of the catalog. It used to return every method with its full `.mthds` bundle attached, which hit DynamoDB's 1 MB page cap at a couple hundred methods and came back truncated with no error and no flag, so methods silently vanished from the UI. Rows are therefore `MethodSummary`, **not** `MethodData`: no `mthds`, no `python`, no `updated_at`, because none of them is in the projection and putting `mthds` back is what restored the truncation. Reach for `getMethod(id)` when you need a method's source. `query.q` is applied server-side across the whole catalog, not over the page — filtering one page client-side would be searching 50 rows of 10,000 and calling it a search. `iterateMethods` is the async iterator that follows the cursor for callers who genuinely want everything (`for await (const m of client.iterateMethods())`); unlike `iterateRuns` it must keep going **past empty pages**, because `q` is a post-read filter over a bounded index slice per request, so `{items: [], nextCursor: "…"}` means "nothing matched in the slice I just read, keep going" — the difference is the server, not the client. A server that keeps minting fresh cursors forever stops it at a page ceiling far past any real catalog with a `PagingNotTerminatingError` carrying `pageLimit`, never a silently truncated stream. It gives up only when the server stops advancing its cursor, and **throws** rather than returning a partial answer; like `iterateRuns` it is deliberately not a `listAllMethods(): Promise<MethodSummary[]>`, because an all-at-once helper needs a page cap and a cap means silently returning a truncated list. `MethodSummary.deletion_state` rides the list rows: a method mid-erasure stays listed so the UI can render it as "Deleting…", while `getMethod` refuses it with a 409. Full read responses (`MethodData`, from `getMethod`) carry `org_id`, `created_by_user_id`, and a server-derived `description` (parsed read-side from the bundle's top-level `description`) on top of the write contract's `name` / `mthds` / `input_data`. `getMethodClosure` is a client-side semantic layer over `getMethod` (not a route) that parses the stored method's polymorphic `mthds` source into a runnable, provenance-labelled closure — see [input-preparation.md](./input-preparation.md#getmethodclosure--the-explicit-expansion-utility).
|
|
121
125
|
- **Organizations:** `listMemberships`, `createOrganization`, `renameOrganization` (`PATCH`). The org "switch" is a WorkOS-session op (`/api/active-org`), not a `/v1` route — out of SDK scope.
|
|
122
126
|
- **Billing:** `getSubscription`, `listPlans`, `listInvoices`, `createCheckout`, `changePlan` (409 `conflict` ⇒ no subscription), `getBillingPortal` (409 `conflict` ⇒ no subscription).
|
|
123
127
|
- **Pipelex API keys (`plx_sk_…`):** `listPipelexApiKeys`, `createPipelexApiKey` (409 `pipelex_api_key_limit_reached`), `revokePipelexApiKey`, `rotatePipelexApiKey`. The plaintext `api_key` is returned once, on create/rotate.
|
|
@@ -134,4 +138,6 @@ The hosted catalog/account routes the webapp drives. All ride the same `{base}/v
|
|
|
134
138
|
Two layers, split by whether a server is in the loop:
|
|
135
139
|
|
|
136
140
|
- **Unit suites (`tests/*.test.ts`, `make test`):** mock the fetch boundary (`vi.spyOn(globalThis, "fetch")`) and pin the client's wire behavior — URLs, request bodies, status→error mapping, poll semantics. These run everywhere, including CI. The codegen-check suite is the exception that proves the rule: it has no server to mock, so it runs against **vendored real codegen output** under `tests/fixtures/codegen/` — artifacts and lock emitted by an actual `pipelex codegen types` run, committed beside the `.mthds` they came from. A hand-written stamp would only prove the port agrees with itself. The artifacts are vendored with a trailing `.txt` and their real names restored at load time, because `format:check`, `lint`, and `typecheck:test` all glob `tests/**/*.ts` — and a single reformatted byte would invalidate the very hash the fixture exists to pin.
|
|
141
|
+
- **The command's case table (`tests/cli.test.ts`):** runs every case of `tests/fixtures/cli-cases.json` through the `pipelex-sdk` command in-process, in a fresh working directory, with `fetch` answering from the case's recorded routes, so the command runs on the real client. The table is written for the Python SDK's command of the same name to run too; [`cli.md`](cli.md) describes the format.
|
|
142
|
+
- **The packed command (`make test-pack`):** packs the package with `npm pack` and runs `pipelex-sdk --help` and `--version` from the tarball through `npx`, from a directory outside the package. It needs the registry for the dependencies, so CI's `js (make all)` job runs it and `make test` does not.
|
|
137
143
|
- **E2E suites (`tests/e2e/*.e2e.ts`, `make test-e2e`):** no mocks — they drive a **live pipelex-api server** (`PIPELEX_E2E_BASE_URL`, default `http://localhost:8081`) and verify the diagnostic-endpoint contract end-to-end with correct and malformed MTHDS bundles: clean content lints with zero diagnostics and formats to itself (idempotently), malformed content comes back as a 200 verdict (`diagnostics[]` on lint; `diagnostics[]` plus the content echoed back unchanged with `changed: false` on format; `is_valid: false` + `validation_errors[]` on validate), and malformed formatter options surface as a 422 `ApiResponseError`. The crate suite adds the case only a live server can settle: the `kind` / `target` vocabulary is a mirror of a Python `StrEnum` in another repo, so nothing but a real call proves the two still agree — and it cross-checks that `codegen`'s `crate_fingerprint` equals the `fingerprint` `resolve` returns for the same closure. It then runs `runCodegenCheck` over the artifacts and lock the server just emitted — verbatim first (current, with the lock header's fingerprint matching the response's), then mutated in memory once per drift category, for a `.ts` target and a `.py` one. The unit suite pins the port against vendored bytes; only this pins it against the stamp and lock bytes the server writes **today**, which is the pairing that matters when a consumer commits the server's artifacts and gates CI on the SDK's verdict about them. The same suite drives `pipeIo` across all three selectors: the artifacts under the resolved ref, their equality with `validate`'s views for the same closure, the `include_files` echo, `all_pipes` on a method with no entry pipe, a `method_ref` package's manifest entry pipe, the invalid arm, and the typed selection `422`. Its `method_id` case, like the `prepareInputs` suite's, runs only when `PIPELEX_E2E_METHOD_ID` names a stored method the key's organization owns, since only a hosted origin can resolve one, and is skipped otherwise. The distinct `.e2e.ts` suffix + `vitest.e2e.config.ts` keep them out of the default `vitest run`, so `make test` / CI never require a server. An unreachable server is an error, never a silent skip, and it is reported once: `make test-e2e` preflights the target with a single `curl` and bails with a one-line message, instead of letting every suite fail in `beforeAll` and print a socket-error dump each. The per-suite `beforeAll` guard stays as the backstop for a direct `npm run test:e2e` run. Both probe **`/v1/version`**, deliberately: a bare runner serves the origin-level `/health` and a hosted origin does not (it answers `/v1/health` instead), so `/v1/version` is the one unauthenticated route both answer — probing anything else reports a perfectly reachable hosted origin as down. The target itself is resolved once, in the Makefile, and exported along with `PIPELEX_API_KEY`, so the URL the preflight checks is always the URL the suites call: the shell environment (or a `make test-e2e PIPELEX_E2E_BASE_URL=...` override) wins, then `.env`, then `http://localhost:8081`. The run echoes the resolved target before it starts, since `.env` can now supply it invisibly. Note that a hosted origin cannot run the whole suite today — `lint` and `format` are not exposed there (gateway `403`), while the protocol routes and the `resolve` and `codegen` crate routes answer, and `pipe-io` answers only once the platform's proxy and the gateway list it — so a fully green run means a local runner. The upload-grant suite needs the same hosted platform: it requests a grant, sends a small PDF with `uploadWithGrant`, reads the bytes back equal through `fetchArtifact`, and proves the refusals the error mapping reads — the `412` of a used grant, the `403 SignatureDoesNotMatch` of a file one byte longer than declared, and the `413` of a declared size over the cap. It runs in Node, whose `fetch` sends no CORS preflight, so the bucket's CORS rule is proven by a page, not by it. The artifact suite is the mirror image: it uploads a file with `prepareInputs`, runs a pass-through and brings the file back down with `downloadArtifacts` over `working_memory`, bytes equal, so it needs a hosted platform carrying `POST /v1/pipe-io` (which `prepareInputs` reads), the upload, run-lifecycle and bulk resolve routes, and a key for it, and it fails honestly rather than skipping against a bare runner.
|
|
@@ -162,11 +162,11 @@ Per-item `error.code` is the fetch vocabulary above plus the download's own: `re
|
|
|
162
162
|
|
|
163
163
|
- `RunStillRunningError` (with the retry hint) or `RunFailedError` (with the run's status and its stored error report as `error`) — a `run_id` naming a run that has not completed;
|
|
164
164
|
- `ScopeUnavailableError` — the requested scope's artifact is `null` or missing from the body (`scope` and `runId` on the error). Reading by `run_id`, a null `main_stuff` is already `MissingMainStuffError` from `getRunResult`;
|
|
165
|
-
- `ArtifactAuthenticationError` — the resolve route refused the credential (`401` / `403`), on the first resolve or on a re-resolve part-way through. It carries `verdict`, the result as it stood: the refusal stops the workers taking new items but lets the fetches already running finish, since they are on presigned links that do not carry the credential, so every file saved is real and listed and the rest are marked `aborted` with a detail naming the credential failure;
|
|
166
|
-
- `ArtifactOperationError` — outside Node, an unusable `dir`, both selectors or neither, an unknown `scope`, or nonsense bounds;
|
|
165
|
+
- `ArtifactAuthenticationError` — the resolve route refused the credential (`401` / `403`), on the first resolve or on a re-resolve part-way through. It carries `verdict`, the result as it stood: the refusal stops the workers taking new items but lets the fetches already running finish, since they are on presigned links that do not carry the credential, so every file saved is real and listed and the rest are marked `aborted` with a detail naming the credential failure. Its `verdict` member is the download's result so far, not the error's: that is `retryable` and `errorDomain`, `config` and not retryable;
|
|
166
|
+
- `ArtifactOperationError` — outside Node, an unusable `dir`, both selectors or neither, an unknown `scope`, or nonsense bounds; its verdict is `config` for the first two, since the environment must change, and `input` for the rest, the caller's arguments;
|
|
167
167
|
- and the transport and lifecycle errors of the reads it makes, unchanged: `ApiResponseError` for a deployment without the bulk route, `RunLifecycleUnavailableError` for a bare runner asked by `run_id`, `ApiUnreachableError`.
|
|
168
168
|
|
|
169
|
-
Everything else that can go wrong with one reference is that reference's `error`.
|
|
169
|
+
Everything else that can go wrong with one reference is that reference's `error`. Each thrown error carries the verdict every SDK error does, `retryable` and `errorDomain`, listed per class on [errors.md](./errors.md#each-classs-verdict); a per-reference `error` is a value with a `code`, and carries none.
|
|
170
170
|
|
|
171
171
|
**Options and defaults.**
|
|
172
172
|
|
package/docs/cli.md
ADDED
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
# The `pipelex-sdk` command
|
|
2
|
+
|
|
3
|
+
`@pipelex/sdk` publishes one command, `pipelex-sdk`, which runs a method on the Pipelex API with nothing to install but Node 22.12 or later. Its subcommands are `run`, which runs a method and prints its main output as JSON, and `script`, which writes a short shell script that runs one method with this SDK's version pinned, which is the lightest way to give a method a command of its own.
|
|
4
|
+
|
|
5
|
+
```bash
|
|
6
|
+
npx @pipelex/sdk run --method github.com/acme/methods/receipt-review@v1.0.0 --inputs inputs.json
|
|
7
|
+
npx @pipelex/sdk run --method ./receipt-review --inputs-template > inputs.json
|
|
8
|
+
npx @pipelex/sdk script --method github.com/acme/methods/receipt-review@v1.0.0 --pipe receipts.review_receipt
|
|
9
|
+
./receipt-review --inputs inputs.json
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
`npx` runs a package's only command whatever its name, so `npx @pipelex/sdk …` is enough. In a project that has the SDK installed, the command is `npx pipelex-sdk …`. The Python SDK, `pipelex-sdk` on PyPI, publishes the same command under the same name (`uvx pipelex-sdk …`, its `docs/cli.md`), with the same flags and the same output, held to this one by the recorded case table described at the end of this page.
|
|
13
|
+
|
|
14
|
+
The command lives in `src/cli.ts`, the executable, and `src/cli/`. It sits at the top of the SDK's dependency graph: nothing in the SDK imports it, the package entry does not export it, and it reaches the SDK only through the public barrel, as any other caller would (`.dependency-cruiser.cjs` holds both rules). It adds no runtime dependency: its flags are read with `node:util`'s `parseArgs`.
|
|
15
|
+
|
|
16
|
+
## `run`
|
|
17
|
+
|
|
18
|
+
```text
|
|
19
|
+
pipelex-sdk run --method <method> [--pipe <domain.pipe_code>] [--inputs <file> | --inputs-template]
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
- **`--method`**, required, names the method in one of the forms below, told apart by shape alone. A value starting with `mt_` is a catalog id, sent as `method_id`. A value starting with `github.com/` is a published method's address, `github.com/<owner>/<repo>[/<package>][@<tag>]`, sent as `method_ref`. Anything else is a path to a `.mthds` file or a bundle directory, which must exist and is read from disk and sent inline (see "Reading a bundle" below). A file or directory that happens to be named `mt_…` is reached as `./mt_…`, so what a value names never depends on what the current directory holds, and an address is written without its scheme. An `mt_` value holding a character no catalog id holds, anything but letters, digits, `_` and `-` (the ids are minted as `mt_` and a UUID, and the run route takes no other character), is a usage error made before any request, which says to write `./` to name a file: `mt_review.mthds` is refused, `./mt_review.mthds` is a path.
|
|
23
|
+
- **`--pipe`** names the pipe by its qualified ref, `domain.pipe_code`. It is sent as `pipe_ref` to the input preparation and as `pipe_code` to the run. A bare code, and an `alias->domain.pipe_code` ref naming a dependency's pipe, are usage errors. Without it, the run takes the method's main pipe, as the API resolves it.
|
|
24
|
+
- **`--inputs`** names a JSON file holding one object, one entry per input, the shape `mthds run --inputs` and `cli-python`'s `--inputs` take; `-` reads it from stdin. The text is UTF-8, and a byte-order mark at its start is dropped, as RFC 8259 lets a JSON reader do, since Windows PowerShell 5.1 writes one (`Out-File -Encoding utf8`); a bundle's mark, by contrast, is sent as it is on disk. A relative path resolves against the current directory. A file or stdin that cannot be read is a usage error, `cannot read stdin.` with the system's reason under it (`Reason: EIO`), and a stdin closed when the command starts reads as empty, so it is not valid JSON: Node opens the null device on a standard stream a process starts without, and the Python command reads the missing stream the same way. A `process.stdin` destroyed before the command reads it is unreadable too, with the reason `ERR_STREAM_PREMATURE_CLOSE`. Without it the run gets no inputs. When the object has entries, they first go through the SDK's input preparation (`prepareInputs`, one `POST /v1/pipe-io` call): a local path or a `data:` URL at a file input is uploaded and replaced by its `pipelex-storage://` reference, each upload is reported on stderr, and a URL passes through. An empty object or no `--inputs` makes no preparation request. A value at a file input that is no file, a `data:` URL that does not decode or a value of another type such as a number, is a usage error, as is a local path that cannot be read; a method that does not load, met during the preparation, is reported as `--inputs-template` reports it ("Exit codes, errors and Ctrl-C" below). The command tells the two apart by the SDK's error classes, `InvalidLocalSourceError` and `InvalidInputValueError` for the inputs' mistakes and `MethodLoadError` for the method's ([`input-preparation.md`](input-preparation.md)).
|
|
25
|
+
- **`--inputs-template`** prints the pipe's inputs template, the object `--inputs` takes with a placeholder for every input, and runs nothing. The template is rendered by `mthds`' `renderInputsTemplate`, in its compact JSON shape, from the input-form descriptor one `POST /v1/pipe-io` call returns, which spends no inference. It takes no `--inputs`.
|
|
26
|
+
|
|
27
|
+
The run goes through the SDK's `startAndWaitForResult`: on the hosted API a durable start, then polls until the run ends, however long it takes, and on a bare runner the blocking execute. Its `onStarted` callback prints `Run started: <id>` on stderr as soon as the run exists ([`run-results.md`](run-results.md)). The poll reads only the `main_stuff` artifact.
|
|
28
|
+
|
|
29
|
+
**stdout carries the run's main output and nothing else**, as indented JSON (two spaces), followed by a newline: exactly what `JSON.stringify(value, null, 2)` prints, non-ASCII characters as they are. It is printed as the method produced it, unvalidated and unrepaired: a list output arrives either bare or wrapped as `{"items": […]}` depending on the runtime path, and is printed as it arrived. An output the method left absent, which the run delivers as the runtime's absence document (matched by its whole shape, as `cli-python` matches it) or as nothing, prints `null`, and stderr says `The method left its output "<name>" absent (<reason>), so the result is null.` A produced file is not downloaded: its short-lived signed URL is in the printed JSON. A reader that stops early and closes either stream, as `| head` or `2>&1 | head -1` does, ends nothing: the lines it no longer reads are dropped, and the command goes on waiting for the run it started and exits as it would have.
|
|
30
|
+
|
|
31
|
+
## `script`
|
|
32
|
+
|
|
33
|
+
```text
|
|
34
|
+
pipelex-sdk script --method <address | catalog id> [--pipe <domain.pipe_code>] [--name <name>] [--dir <dir>]
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
`script` checks the method with one `POST /v1/pipe-io` call, which selects the pipe as the run will and spends no inference, so a typo in the address or the pipe is caught when the script is written rather than when it first runs. Then it writes an executable file and prints its path on stdout:
|
|
38
|
+
|
|
39
|
+
```sh
|
|
40
|
+
#!/bin/sh
|
|
41
|
+
# receipt-review: runs github.com/acme/methods/receipt-review@v1.0.0 on the Pipelex API.
|
|
42
|
+
# Written by @pipelex/sdk X.Y.Z. Needs Node 22.12 or later, and PIPELEX_API_KEY in the environment.
|
|
43
|
+
# Usage: ./receipt-review --inputs inputs.json, or ./receipt-review --inputs-template to see what to fill in.
|
|
44
|
+
exec npx --yes @pipelex/sdk@X.Y.Z run --method 'github.com/acme/methods/receipt-review@v1.0.0' --pipe 'receipts.review_receipt' "$@"
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
- **The method is an address or a catalog id.** A local bundle is refused, since a script naming a path breaks as soon as the script or the bundle moves; the refusal names both ways to get one, publishing the method or saving it to the catalog. An address without a tag is accepted, with a warning that the script follows the repository's default branch.
|
|
48
|
+
- **The name** defaults to the address's last segment without its tag, or, for a catalog id, to the method's name read from its catalog entry (`GET /v1/methods/{id}`) and kebab-cased: decomposed (NFKD), stripped of combining marks, lowercased, every run of characters outside `a-z0-9` turned into one `-`, trimmed of `-`. A name that comes out empty asks for `--name`, and so does a catalog entry whose name is missing or not a string. A name is a file name: it may not be empty, `.` or `..`, or hold a `/`, a `\` or a control character. The `\` is refused on every platform, since Windows reads it as a `/` and `..\outside` would leave `--dir`.
|
|
49
|
+
- **The file** is written in `--dir`, by default the current directory, which must exist. It is created with the permissions of an executable (`0755`, less the umask) and never over an existing file, a dangling link included. A write that fails once the file exists removes it, so a script is whole or absent, never a truncated one that would run half a command and make the next attempt say it already exists; a Ctrl-C waits for the file to be whole ("Exit codes, errors and Ctrl-C" below). The path printed is `--dir` as given without its trailing `/`, or `.`, then `/` and the name.
|
|
50
|
+
- **The script's one line** is `exec npx --yes @pipelex/sdk@<version> run --method '<method>' [--pipe '<pipe>'] "$@"`: every value single-quoted, an inner `'` spelled `'\''`, and no value may hold a control character. The `"$@"` passes everything else on, `--inputs`, `--inputs-template` and stdin included, and since `run` refuses a flag given twice, a second `--method` cannot quietly run another method. The version is the one of the SDK that wrote the script, so the script keeps running the command it was written with; `npx` caches the package, so after the first run only the API needs the network. There is no option to write the Python SDK's line, since this SDK cannot know which version of the other package exists and was tested.
|
|
51
|
+
- **On Windows** the script needs a POSIX shell, such as Git Bash or WSL; otherwise type the `npx` line directly.
|
|
52
|
+
|
|
53
|
+
## The environment
|
|
54
|
+
|
|
55
|
+
The key comes from `PIPELEX_API_KEY` and nowhere else, since a flag would put it in the shell's history and the process list; its absence is a usage error that says where to get a key, and the command never prints it. The API's address comes from `PIPELEX_BASE_URL`, `https://api.pipelex.com` when unset; a refused base URL is reported without its credentials, path or query. An empty variable counts as unset, except `PIPELEX_BASE_URL`: an empty base URL is refused like any other the SDK cannot use, since it is most often a variable that was meant to name another API, as in `PIPELEX_BASE_URL=$STAGING_URL` with `STAGING_URL` unset, and counting it as unset would send the key to the default address. Every request carries `pipelex-sdk-cli/<version>` before the SDK's own token in its `User-Agent` ([`client-identification.md`](client-identification.md)).
|
|
56
|
+
|
|
57
|
+
**No `.env` file is read.** A command started through `npx` from wherever the person stands would otherwise pick up a credential from whichever file it found there. To load one into the shell: `set -a; . ./.env; set +a`.
|
|
58
|
+
|
|
59
|
+
**`PIPELEX_SDK_POLL_INTERVAL_MS` is for tests only.** It sets the poll interval in milliseconds, a whole number; the case table sets it to `0` so that a run answered "still running" is polled again at once, and serves its `202` answers with `Retry-After: 0`, since the SDK waits at least as long as the server's `Retry-After` and five seconds when a `202` gives none. Unset, the SDK's own interval applies. The Python command reads the same variable.
|
|
60
|
+
|
|
61
|
+
## Exit codes, errors and Ctrl-C
|
|
62
|
+
|
|
63
|
+
The exit code is presentation, following the workspace's surface-output rule: a script reads stdout.
|
|
64
|
+
|
|
65
|
+
| Code | When |
|
|
66
|
+
| --- | --- |
|
|
67
|
+
| `0` | The result was printed, or the script written, or the help or version shown |
|
|
68
|
+
| `1` | The run failed, the API refused a request, could not be reached, or answered what the command cannot read |
|
|
69
|
+
| `2` | A usage error: a bad flag, an unreadable or invalid inputs file, a selector of no known form, a path that names no bundle, a missing key, a refused base URL, an `mt_` value that cannot be a catalog id, a local file the inputs name that cannot be read, a value at a file input that is no file, a script that would overwrite a file |
|
|
70
|
+
| `130` | Interrupted: on Linux and macOS the process ends by the interrupt signal, which a shell reports as `130` |
|
|
71
|
+
|
|
72
|
+
Every error goes to stderr, its first line `Error: <sentence>` and the lines under it giving the details. The command words a failure from the error's typed fields rather than from an SDK message, wherever the fields carry what a person needs, so that the JavaScript and Python commands print the same lines for the same answer:
|
|
73
|
+
|
|
74
|
+
- **A refused request**: `Error: the API refused the request (status <n>).`, then `Reason: <the problem's detail>`, each validation error as ` - <file>: <message>`, `Next step: <the advised action>` and `Request id: <id>` when the answer carries them.
|
|
75
|
+
- **A failed run**: `Error: run <id> ended with status <STATUS>.`, then `Reason: <the report's message>` (the problem's own sentence when the run has no report) and `Next step: <the report's advised action>`.
|
|
76
|
+
- **A method that does not load**: `Error: the method does not load.`, then one line per validation error that carries a string category and message, as ` - <file>: <message>`, or, when there is none, ` - <the answer's own message>`. It reads the same whichever request found it: the pipe I/O call of `--inputs-template` and `script`, or the input preparation of a run with inputs.
|
|
77
|
+
- **A value at a file input that is no file**: `Error: the inputs hold a value at a file input that cannot be read as a file.`, then `Reason: <the SDK's message>` and the forms a file input takes. An unreadable local path reads `Error: Local file cannot be read: "<path>" (<reason>).`
|
|
78
|
+
- **An unreachable API**: `Error: could not reach the Pipelex API at <base URL> (<code>).`
|
|
79
|
+
- **A failed upload** of a file the inputs name: `Error: the upload of "<file>" failed (status <n>).`, the status left out when no answer came back, then `Reason: <the SDK's message>`, which says what the status means for an upload (a file past the size limit, a deployment without file upload, a credential refused), and `Next step:` and `Request id:` when the API's answer carries them. It covers the SDK's `RejectedAssetError`, `UploadAuthenticationError`, `UnsupportedUploadCapabilityError` and `UploadTransportError`, each of which names the file it was sending. It exits `1`, and no run starts.
|
|
80
|
+
- **A wait that fails once the run exists**, on an unreachable API, a refused poll or an answer the command cannot read: the failure's own lines, then `Run <id> may still be going on the server, so do not start it again.`, since the run was started and nothing says it ended. A run that ended in failure (`RunFailedError`) and a run that ended without its main output (`MissingMainStuffError`) get no such line. It exits `1`.
|
|
81
|
+
- **A start whose outcome is unknown**: once a request that may create a run has left, the start or a bare runner's blocking execute, and before the API names the run, a failure that leaves it unknown whether a run was created adds `A run may have started on the server without the command learning of it, so check before starting it again.` under its own lines. That is an answer lost to a time limit or to a connection that closed once the request had left, a request the gateway cut off, a gateway's `502` or `504`, which says it lost or never got the server's answer (RFC 9110), and an answer the command cannot read. The hosted gateway cuts off a request it waits on at about 30 seconds and answers `503` or `504`, while the server may still be handling it: a blocking execute, or a start whose server is fetching a `method_ref`'s package. The command times the request from the moment the SDK's `onStarting` announces it, and the SDK's `isGatewayCutOff` reads a `503` that came back past its threshold, about 28 seconds, as that cut-off. Any other answer from the API, a `503` that came back sooner, saying the request was not handled, included, and a transport failure that proves the request never left, add nothing, since no run was created. Such a failure names a step of the connection's set-up, which no request follows: an unknown host (`ENOTFOUND`, `EAI_AGAIN`), a refused connection (`ECONNREFUSED`, from every address the host has included), no route to the host or to the network (`EHOSTUNREACH`, `ENETUNREACH`, from the `connect` call), the connection given up by the system's or undici's own connect time limit (`ETIMEDOUT` from `connect`, `UND_ERR_CONNECT_TIMEOUT`), a server certificate the TLS handshake refused (`CERT_HAS_EXPIRED`, `DEPTH_ZERO_SELF_SIGNED_CERT`, `ERR_TLS_CERT_ALTNAME_INVALID` and the other verdicts of the certificate's check Node names, then `UNSPECIFIED`, its code for every verdict it does not name, such as an unhandled critical extension). Both commands read the same failures the same way, so where one command cannot tell whether the request had left, both take the reading that a run may exist: a connection dropped during the TLS handshake, which Node reports as the reset (`ECONNRESET`) of a connection dropped once the request had left, and another TLS failure. A host that drops packets is a connection that never comes, and each command meets its transport's connect time limit first, which proves nothing was sent: here undici's own, ten seconds for the TCP connect and the TLS handshake together (`UND_ERR_CONNECT_TIMEOUT`), which runs out before the SDK's own time limit (`ABORT_TIMEOUT`), at least 30 seconds on every request the command sends; in the Python command, httpx's connect timeout, which the one time limit the Python client passes bounds. Fetch cannot say whether the SDK's own time limit ran out before the connection was made or once the request had left, so the command reads it as an answer lost to a time limit; with Node's own dispatcher it is one, since undici gives up a connection that does not come sooner. It exits `1`.
|
|
82
|
+
|
|
83
|
+
The checks that need no request come first, so a mistake costs nothing. `run` checks, in this order, its flags, `--pipe`'s form, the method (reading a path from disk), the inputs, the poll interval, the key and the base URL. `script` checks its flags, `--pipe`'s form, a control character in any value, the method's form, `--name`, `--dir`, the target file when its name is already known, the key and the base URL. The flags are read in command-line order and the first problem is the one reported: `--help` or `-h` anywhere before the `--` that ends the options asks for the help whatever else is there, and an argument that is exactly one of the two is never taken as a flag's value, so `run --method --help` prints the help, and so does `run --pipe -- --help`, where the `--` is `--pipe`'s value and ends nothing; an unknown option or a positional argument is refused; a flag given twice is refused rather than the last one winning; a string flag needs a non-empty value, given as the next argument or after `=`, and a next argument starting with `-` is not taken as one, except `-` alone, which `--inputs` reads as stdin. Before a subcommand, `--help` prints the command's help whatever follows it, as a subcommand's does, while `--version` must stand alone: `pipelex-sdk --version run …` is a usage error, since printing the version would leave the rest of the command line unanswered.
|
|
84
|
+
|
|
85
|
+
**Ctrl-C does not stop the run.** The command stops waiting, says where the run stood and exits `130`: until a request that may create a run is about to leave, the version check included, `Interrupted. No run was started.`; from then until the API answers with the run's id, `Interrupted before the API answered with a run id. A run may or may not have started on the server.`; once the run exists, `Interrupted. Run <id> keeps going on the server.` The moment between the first two is the one the SDK announces through `startAndWaitForResult`'s `onStarting`, right before it sends the start or a bare runner's blocking execute ([`run-results.md`](run-results.md)). Fetching that run later is not built into the command: the SDK's `waitForResult(id)` does it. A second Ctrl-C ends the process at once.
|
|
86
|
+
|
|
87
|
+
**An interrupt sends nothing more.** Every request the command makes starts only if no interrupt has landed, so a Ctrl-C during the local work (reading the bundle or the inputs, a read from stdin or a named pipe included) sends no request at all, not the pipe I/O call, the version check nor the start, and the command says `Interrupted. No run was started.` A local read that never completes, such as `--inputs` naming a pipe no one writes to or a bundle on a stalled network mount, does not hold the command either: it stops waiting for the read. An upload under way when the interrupt lands finishes, but no further upload starts, one whose file was being read included, and neither does the run. `script` interrupted before it starts writing its file, during its last check that the name is free included, writes nothing and says `Interrupted. Nothing was written.`; once it has started, the write is not raced with the interrupt, which waits for the file to be whole, then says `Interrupted. ./<name> was written.`, so that the message is true wherever the interrupt lands. A second Ctrl-C ends the process at once, as it does anywhere in the command, printing nothing more; should it land between the file's creation and the end of its write, which on a local disk takes no time but can last where the write blocks, on a stalled mount for instance, that file stays behind, empty or partial, for the person to remove. The Python command stops the write instead and removes what it wrote. Node puts every signal back to its default action as it starts, so this command takes Ctrl-C even when it was started with SIGINT ignored, as a background job of a non-interactive shell is, where the Python command keeps it ignored; and `--inputs-template` says `Interrupted.`
|
|
88
|
+
|
|
89
|
+
**How the executable ends after Ctrl-C.** On Linux and macOS, once it has said where the run stood, the process ends by the interrupt signal itself, which a shell reports as status `130` and which tells a calling loop that the person interrupted. It does not call `exit(130)`, because Node's exit waits for its worker threads, and one can stay blocked for good in a read the command stopped waiting for. On Windows it exits with `130`. `runCommand`, the function the executable calls, returns `130` in every case.
|
|
90
|
+
|
|
91
|
+
## Reading a bundle
|
|
92
|
+
|
|
93
|
+
A `--method` path is read by the command's own reader (`src/cli/bundle.ts`), whose rules are recorded as cases so that the Python command reads a bundle the same way:
|
|
94
|
+
|
|
95
|
+
- **A `.mthds` file** is a one-file bundle, sent under its own file name. A file of any other name is refused.
|
|
96
|
+
- **A directory** is every `.mthds` file under it, at any depth, each sent under its path relative to the directory, with `/` between segments.
|
|
97
|
+
- **The order** is the order of those names compared code point by code point, which is the order of their UTF-8 bytes: `Zeta.mthds` before `main.mthds`, `steps/score.mthds` before `steps2.mthds`, and `~.mthds` (U+FF5E) before `🧾.mthds` (U+1F9FE), where UTF-16 order would put them the other way round. A run sends the contents in this order as `mthds_contents`, and the pipe I/O route gets them as `files`, each with its name as `source`.
|
|
98
|
+
- **A hidden entry**, a file or a directory whose name starts with `.`, is skipped, so a `.venv/` or a `.git/` inside a bundle is never read.
|
|
99
|
+
- **Any file not named `.mthds`** is ignored, Python files included: the command sends only the MTHDS sources.
|
|
100
|
+
- **A symbolic link** is judged by its own name. One named `.mthds` that leads to a file is read through, under the link's name; one that leads to a directory is not descended into, which keeps a link cycle from looping; one that leads nowhere is refused. The `--method` path itself may be a link, and is followed.
|
|
101
|
+
- **A directory with no `.mthds` file** under it is refused, and so is a file that is not UTF-8 text and a directory that cannot be listed. A byte-order mark is kept as the file's first character, as it is on disk.
|
|
102
|
+
|
|
103
|
+
## The case table
|
|
104
|
+
|
|
105
|
+
The JavaScript and Python commands share no code, so they are held together by one recorded table of cases written for both: `tests/fixtures/cli-cases.json` is its source, and `python/tests/fixtures/cli-cases.json` its copy, which `make shared-files` at the repository root writes and `make check-shared-files` holds byte for byte (`docs/ci.md` at the root). A behaviour of the command changes here, in the table and in `src/cli/`, and the Python command follows it. `tests/cli.test.ts` runs every case through `runCommand` in-process, and the Python SDK's `tests/unit/test_cli.py` runs every case through its own `run_command`; a case that uses a field a suite does not know fails rather than being skipped, so a case added for one language reaches the other.
|
|
106
|
+
|
|
107
|
+
The table holds:
|
|
108
|
+
|
|
109
|
+
- **`base_url`**, the origin every recorded route is served on, and **`env`**, the environment every case starts from: a key, that base URL and the poll interval `0`.
|
|
110
|
+
- **`answers`**, named answers a route's exchange can refer to rather than repeat.
|
|
111
|
+
- **`placeholders`**, each with its JavaScript and Python value: `WRITER` (`@pipelex/sdk`), `NEEDS` (`Node 22.12 or later`), `RUNNER` (`npx --yes @pipelex/sdk@{{SDK_VERSION}}`), and `SDK_VERSION`, which each suite fills with its own SDK's version. They appear as `{{NAME}}` in a case's expected outputs only, and each suite substitutes the language values first, then `{{SDK_VERSION}}`.
|
|
112
|
+
- **`cases`**, each with these fields:
|
|
113
|
+
- `name`, unique, and `summary`, one sentence saying what the case holds.
|
|
114
|
+
- `argv`, the arguments after the command's name.
|
|
115
|
+
- `env`, merged over the table's `env`; a `null` value unsets the variable. The command sees only this environment.
|
|
116
|
+
- `files`, created in a fresh, empty working directory before the command runs, parents included: `{path, text}`, `{path, base64}` for bytes that are not UTF-8, `{path, symlink}` whose value is the link's target as written, relative to the link's directory, and `{path, directory: true}`.
|
|
117
|
+
- `stdin`, the text stdin holds; empty when absent.
|
|
118
|
+
- `stdin_error`, the system error code reading stdin fails with, such as `EIO`, instead of the text.
|
|
119
|
+
- `routes`, keyed `"<METHOD> <path>[?<query>]"` on the base URL, each a list of exchanges answering that route's calls in order, each once. An exchange gives the answer's `status`, `headers` and JSON `body` (or raw `text`, or `base64`, raw bytes such as a body that is not UTF-8, or none), or `answer`, the name of a table-level answer whose fields it takes, or `unreachable`, a request that never got a connection, of a kind: `"refused"`, `"refused-every-address"` (a host with several addresses, each refusing), `"unknown-host"`, `"no-route"`, `"no-network"`, `"connect-timeout"` (the transport's own connect time limit, which a host that drops packets meets first: undici's ten seconds in Node, httpx's connect timeout in Python), `"system-connect-timeout"`, `"certificate"` (the server's, refused), `"certificate-unnamed"` (refused for a reason Node gives no code of its own), `"handshake-dropped"` (during the TLS handshake); or `lost`, a request sent whose answer never comes back: `"timeout"` when the time limit runs out first, `"closed"` when the connection closes first, `"reset"` when it is reset, `"no-route"` when it loses its route to the host. This suite throws each kind as Node's fetch reports it, read off the real library against local listeners and certificates, but for a missing route, which takes the shape every failed `connect` call takes: a system error with its `code`, `errno` and `syscall` under fetch's `TypeError` (`ECONNREFUSED` from `connect`, an `AggregateError` of those for every address, `ENOTFOUND` from `getaddrinfo`, `EHOSTUNREACH` and `ENETUNREACH` from `connect` but from `read` once lost, `ETIMEDOUT` from `connect`, `ECONNRESET` from `read` for a dropped handshake and a reset alike), undici's `UND_ERR_CONNECT_TIMEOUT` and `UND_ERR_SOCKET`, the certificate check's `CERT_HAS_EXPIRED` and `UNSPECIFIED`, and the client's own `TimeoutError` for a `timeout`. An answer with `Content-Encoding: gzip`, which the table records only broken, fails as undici fails it, while its body is read (`Z_DATA_ERROR`), since a fetch mock decodes nothing. `elapsed_ms`, on any exchange, is how long it takes to come back, as the clock the command and the SDK time a request with sees it: the suite moves that clock forward by as much while it serves the exchange, rather than waiting, which is how a case holds an answer that came back past the gateway's ~30-second cut-off. This suite runs `performance.now`, the monotonic clock both read, as it does, plus the time the case's exchanges have taken. `request_body`, when given, is what the command must send: the sent JSON body, without its top-level keys whose value is `null`, which mean absent, must equal it. Every request must carry `Authorization: Bearer <PIPELEX_API_KEY>`. A request the case did not record, and a recorded exchange the command never asked for, fail the case.
|
|
120
|
+
- `interrupt`, optional, `{route, call}`: when that call of that route arrives, the suite interrupts the command, as Ctrl-C would, instead of answering; the route lists no exchange for it.
|
|
121
|
+
- `expect`: `exit_code`; either `stdout`, exact, or `stdout_includes`, substrings that must appear in order; `stderr`, substrings that must appear in stderr in this order, each found after the end of the one before, so a case pins the command's own sentences and the data they carry without pinning every line an SDK adds; `stderr_excludes`, substrings that must not appear anywhere, such as a key or a password; `files`, each `{path, text, executable}` that must exist with exactly that text, and with its owner's execute bit when `executable`; and `absent_files`, paths that must not exist.
|
|
122
|
+
|
|
123
|
+
**Writing a case.** A number in a recorded answer is an integer below 2^53 or a decimal whose shortest form both languages print alike, never `1.0` nor `1e16`, and an object's keys are never integer-like, since JavaScript would print those first; the two commands then print a recorded output byte for byte alike. A needle that ends with `\n` pins a whole line's end. A failure only one runtime can meet or tell apart, such as httpx's pool timeout, has no kind in the table, and that runtime's own suite tests it. Add the case at the source, run `make shared-files` at the root, and make it pass in both suites.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Client identification (`User-Agent` and `appInfo`)
|
|
2
2
|
|
|
3
|
-
Every request `PipelexApiClient` sends to the Pipelex API carries a `User-Agent` header that says which program made it. The hosted platform parses that header into the client surface it reports in product analytics and in its access log, which is how it tells a run started by an SDK script from one started by the web app, the MCP server, the CLI or the coding-agent hook. The convention is shared by every first-party client and is fixed by the
|
|
3
|
+
Every request `PipelexApiClient` sends to the Pipelex API carries a `User-Agent` header that says which program made it. The hosted platform parses that header into the client surface it reports in product analytics and in its access log, which is how it tells a run started by an SDK script from one started by the web app, the MCP server, the CLI or the coding-agent hook. The convention is shared by every first-party client and is fixed by the spec `conformance/specs/client-identification.md`, in the `conformance` repository, where the cross-repo specs sit beside the tests that verify them; this page describes how this SDK implements it.
|
|
4
4
|
|
|
5
5
|
## What the SDK sends
|
|
6
6
|
|