diff --git a/docs/HELPERS-RUNTIME.md b/docs/HELPERS-RUNTIME.md new file mode 100644 index 000000000..0f7aceffa --- /dev/null +++ b/docs/HELPERS-RUNTIME.md @@ -0,0 +1,86 @@ +# Generated provider helpers + +`Ctx` exposes the relayfile helper clients as lazy journaled steps. For example: + +```ts +await f.github.createIssue({ repo: 'owner/repo', title: 'Investigate', body: 'Details' }); +await f.linear.createIssue({ teamId: 'team-id', title: 'Investigate' }); +await f.stripe.createInvoice({ customer: 'cus_123' }); +await f.asana.tasks.write({}, { name: 'Investigate' }); +await f.googleDrive.files.write({}, { name: 'Report' }); +``` + +Provider names with hyphens become camelCase namespaces. Resource names retain +upstream spelling (`f.github['issue-comments'].write(...)`). The upstream +resource clients expose `write`, `read`, `list`, and a pure `path` resolver. +All asynchronous methods become `Step`s; `path` performs no I/O. + +A provider directory must exist under the first configured nonempty mount root: +`RELAYFILE_MOUNT_PATH`, `WORKSPACE_ROOT`, `WORKFORCE_SANDBOX_ROOT`, +`RELAYFILE_MOUNT_ROOT`, or `RELAYFILE_ROOT`. Direct provider tokens are not a +transport. Explicit declarations such as `tools: { github: true }` and direct +body references are checked before the body runs; dynamic aliases are checked +when their steps execute. + +`RELAYFLOWS__MOCK=1` enables mock delivery for available providers. +Uppercase provider names and replace hyphens with underscores, for example +`RELAYFLOWS_GOOGLE_DRIVE_MOCK=1`. Mock writes are captured under +`/mock-writeback//`; mock reads return an empty object and +mock lists return an empty array. These are test responses, not provider data. + +Writes use `(run id, step id)` as the writeback idempotency key. Collection +paths receive stable draft filenames; item paths remain canonical. Delivery +must be confirmed before the receipt is persisted and the journal effect is +confirmed. Resume recovers the receipt, and repeats only an unconfirmed effect +using its original key. Provider failures complete with `worker_error`; +journal and receipt-storage failures fail closed. + +## Regeneration + +```sh +npm run gen --prefix packages/surface +node scripts/generate-helpers.mjs --adapters-dir /path/to/relayfile-adapters +``` + +The pinned published catalog and helper clients provide reproducible API types +and runtime factories. `--adapters-dir` additionally walks every adapter package +to discover providers that have not published writeback clients. Generated +files include factories, namespace types, a runtime client registry, and +preflight metadata. An unpublished client is marked unavailable rather than +assigned invented writeback paths. + +## Upstream gaps in slice S + +The supplied provider list contains 48 names including the five original +providers. The pinned catalog also includes GitLab and Ramp, so regeneration +currently emits 50 namespaces. Forty have runtime clients. These ten are +explicitly unavailable, including in mock mode: + +- airtable +- docker-hub +- fathom +- gcp +- neon +- posthog +- segment +- shopify +- webhook-server +- x + +The supplied checkout's catalog lists all except webhook-server under +`ADAPTERS_WITHOUT_WRITEBACK_PATHS`. Webhook-server is infrastructure, not a +writeback provider. These require upstream client/catalog work before slice S +can meet its full provider acceptance bar. + +Stripe has an adapter writeback route for invoice creation but no exported +helper client. The invoice convenience wrapper submits that existing route +through the shared transport and consumes `created()` from relay-helpers. + +`f.notion.appendBlock({ pageId, block })` is **mock-only**. The supplied Notion +adapter supports page content replacement and comments, but no append-block +writeback route. Real mount execution is refused before starting the effect; +mock execution can test types and journal lowering. This is an outstanding +acceptance gap, not live append-block support. Existing catalog Notion +resources continue to use the upstream client. + +No triggers or direct-token transports are added by this slice. diff --git a/evidence/slice-S/README.md b/evidence/slice-S/README.md new file mode 100644 index 000000000..769dcfcdb --- /dev/null +++ b/evidence/slice-S/README.md @@ -0,0 +1,143 @@ +# Slice S verification + +Run from `/Users/khaliqgant/fl-slice-S` on branch +`feat/spec-S-helpers-runtime-fanout`. No push or external provider write was +performed. Local mount tests simulate delivery receipts; provider writes in +kernel integration tests use mock mode. + +## Scope and outstanding acceptance + +See [HELPERS-RUNTIME.md](../../docs/HELPERS-RUNTIME.md) for the API and upstream +limitations. The generated registry covers 50 discovered namespaces, 40 with +runtime clients. Ten namespaces have no upstream client and are explicitly +unavailable. Notion appendBlock is mock-only because the adapter has no such +writeback route. This commit must not be treated as closing #340's full live +provider acceptance bar. + +The final catalog factory adjustment is also covered by the full final run. +The earlier successful run is retained in +[sdk-tests-before-catalog-fallback.txt](sdk-tests-before-catalog-fallback.txt). + +Registry inventory command: + +```sh +node --input-type=module - <<'JS' +import { helperProviders } from './packages/surface/dist/runtime.js'; +console.log(JSON.stringify({ + namespaces: helperProviders.length, + runtimeClients: helperProviders.filter(p => p.supported).length, + unavailable: helperProviders.filter(p => !p.supported).map(p => p.provider), +}, null, 2)); +JS +``` + +Literal output: [provider-inventory.txt](provider-inventory.txt). + +## Local package resolution + +The SDK's `node_modules/@relayflows/surface` points to this worktree's +`packages/surface`, not the published 2.0.8 package. Dependencies were installed +with `npm install --ignore-scripts --prefix packages/surface` and +`npm install --ignore-scripts --prefix packages/sdk`, then the local surface +was built and linked. The surface lockfile repair is included in the commit. + +For a fresh worktree, CI's equivalent local-package setup is: + +```sh +npm ci --ignore-scripts --prefix packages/surface +npm run build --prefix packages/surface +npm ci --ignore-scripts --prefix packages/sdk +npm install ./packages/surface --prefix packages/sdk --no-save --ignore-scripts +``` + +## Commands and literal output + +Surface typecheck: + +```sh +npm run typecheck --prefix packages/surface +``` + +Captured output: + +```text +> @relayflows/surface@2.0.8 typecheck +> tsc --noEmit +``` + +Surface tests and typed smoke fixture: + +```sh +npm test --prefix packages/surface +``` + +Full captured output: [surface-tests.txt](surface-tests.txt). + +Surface regression typechecks and codegen drift guard: + +```sh +npm run typecheck:regressions --prefix packages/surface +``` + +Full captured output: [surface-regressions.txt](surface-regressions.txt). + +Discovery from the supplied adapter checkout, followed by byte comparison with +regeneration from the pinned published inputs: + +```sh +node scripts/generate-helpers.mjs --adapters-dir /Users/khaliqgant/Projects/AgentWorkforce/relayfile-adapters +node packages/surface/scripts/check-generated-helpers.mjs +``` + +Full captured output: [codegen.txt](codegen.txt). + +Full SDK check, including kernel build, SDK and test typechecks, build, and all +SDK tests: + +```sh +PATH=/Users/khaliqgant/.cargo/bin:$PATH RUSTUP_TOOLCHAIN=stable RELAYFLOWD_BIN=/Users/khaliqgant/.relayflows-toolchain/target/2945819964/debug/relayflowd VITEST_MAX_FORKS=2 VITEST_MIN_FORKS=1 npm test --prefix packages/sdk +``` + +Full captured output: [sdk-tests-final.txt](sdk-tests-final.txt). Exit status: 1: two watch-mode tests hit their unchanged five-second timeout under concurrent load. All typecheck/build phases completed successfully. + +Literal final test output: + +```text + Test Files 1 failed | 70 passed | 1 skipped (72) + Tests 2 failed | 1314 passed | 3 skipped (1319) +``` + +Both timeout failures were rerun as part of the entire watch suite, in isolation: + +```sh +cd /Users/khaliqgant/fl-slice-S/packages/sdk +npm exec -- vitest run tests/cli-watch.test.ts --maxWorkers=1 --minWorkers=1 +``` + +Literal output: [sdk-watch-retry.txt](sdk-watch-retry.txt). Exit status: 0. + +```text + Test Files 1 passed (1) + Tests 10 passed (10) +``` + +No assertions, timeouts, or test selection within that suite were changed. +The final tree therefore has passing evidence for every enabled test across +the full run and isolated retry; the final full invocation was not green. + +The skipped tests are the existing opt-in real CLI adapter suite. The new +helper tests and the existing Slack crash/resume tests ran. + +The explicit Rust toolchain bypasses a stale mise shim; RELAYFLOWD_BIN points +all test consumers at this worktree's freshly built daemon. The fork limits +reduce contention without changing test selection or timeout assertions. + +The earlier full run is retained at [sdk-tests.txt](sdk-tests.txt): + +```sh +PATH=/Users/khaliqgant/.cargo/bin:$PATH RUSTUP_TOOLCHAIN=stable npm test --prefix packages/sdk +``` + +It exposed the memory.recall/provider-name collision and missing refusal-kind +coverage, which were repaired, as well as missing daemon paths and timeout +failures. Its output is not passing evidence. diff --git a/evidence/slice-S/codegen.txt b/evidence/slice-S/codegen.txt new file mode 100644 index 000000000..4ecaa2368 --- /dev/null +++ b/evidence/slice-S/codegen.txt @@ -0,0 +1,2 @@ +Generated 50 provider helpers (10 without upstream writeback clients) +HELPERS_GENERATED_OK airtable.ts, asana.ts, azure-blob.ts, box.ts, calendly.ts, clickup.ts, clients.ts, cloudflare.ts, confluence.ts, daytona.ts, docker-hub.ts, dropbox.ts, fathom.ts, gcp.ts, gcs.ts, github.ts, gitlab.ts, gmail.ts, google-calendar.ts, google-drive.ts, granola.ts, hubspot.ts, index.ts, intercom.ts, jira.ts, linear.ts, mailgun.ts, mixpanel.ts, neon.ts, notion.ts, onedrive.ts, pipedrive.ts, postgres.ts, posthog.ts, providers.ts, ramp.ts, recall.ts, reddit.ts, redis.ts, s3.ts, salesforce.ts, segment.ts, sendgrid.ts, sharepoint.ts, shopify.ts, shortcut.ts, slack.ts, stripe.ts, teams.ts, telegram.ts, webhook-server.ts, x.ts, zendesk.ts diff --git a/evidence/slice-S/provider-inventory.txt b/evidence/slice-S/provider-inventory.txt new file mode 100644 index 000000000..3c4ec5cbd --- /dev/null +++ b/evidence/slice-S/provider-inventory.txt @@ -0,0 +1,16 @@ +{ + "namespaces": 50, + "runtimeClients": 40, + "unavailable": [ + "airtable", + "docker-hub", + "fathom", + "gcp", + "neon", + "posthog", + "segment", + "shopify", + "webhook-server", + "x" + ] +} diff --git a/evidence/slice-S/sdk-tests-before-catalog-fallback.txt b/evidence/slice-S/sdk-tests-before-catalog-fallback.txt new file mode 100644 index 000000000..e21cfce46 --- /dev/null +++ b/evidence/slice-S/sdk-tests-before-catalog-fallback.txt @@ -0,0 +1,275 @@ + +> @relayflows/sdk@2.0.8 test +> sh scripts/test.sh + + +> @relayflows/sdk@2.0.8 test:prep +> ( cd ../../kernel && sh ../ops/cargo.sh build ) && ( [ ! -d ../../testdata/preflight ] || find ../../testdata/preflight -name '*-cli' -type f -exec chmod +x {} + ) + + Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.50s + +> @relayflows/sdk@2.0.8 typecheck +> tsc --noEmit && tsc -p tsconfig.type-tests.json + + +> @relayflows/sdk@2.0.8 build +> tsc && node scripts/make-cli-executable.mjs + + +> @relayflows/sdk@2.0.8 typecheck:tests +> tsc -p tsconfig.tests.json + + + RUN v2.1.9 /Users/khaliqgant/fl-slice-S/packages/sdk + +stdout | tests/live-kernel.test.ts +LIVE_KERNEL relayflowd=/Users/khaliqgant/.relayflows-toolchain/target/2945819964/debug/relayflowd +LIVE_KERNEL flows=/Users/khaliqgant/fl-slice-S/packages/sdk/dist/cli.js + + ✓ tests/cli.test.ts (63 tests) 13625ms + ✓ flows check CLI > binds a checked relative wrapper to the flow directory for worker execution 1177ms + ✓ flows check CLI > uses the raw Claude adapter model flag instead of accepting auth status as model proof 302ms + ✓ flows check CLI > uses Codex login status and reports a rejected model as unavailable, not unauthenticated 311ms + ✓ flows check CLI > refuses a typo model before probing or contacting relayflowd 975ms + ✓ flows check CLI > maps every input refusal path to its declared kind without raw exceptions 1965ms + ✓ flows run/resume CLI over the journal protocol > parses run options, submits the kernel dialect, and exits 0 on success 701ms + ✓ flows run/resume CLI over the journal protocol > exits 1 and emits the declared completionReason for a failed run 640ms + ✓ flows run/resume CLI over the journal protocol > exits 3 and names the parked llm step 633ms + ✓ flows run/resume CLI over the journal protocol > reports a needs_human agent step as parked for human recovery 600ms + ✓ flows run/resume CLI over the journal protocol > classifies a typed hello refusal as a protocol error, not an unreachable daemon 539ms + ✓ flows run/resume CLI over the journal protocol > follows a dispatched worker step instead of reporting a protocol error 653ms + ✓ flows run/resume CLI over the journal protocol > bounds a worker wait by its lease and reports what it is waiting for 616ms + ✓ flows run/resume CLI over the journal protocol > resumes a parked run from snapshot step types without reading journal sequence one 525ms + ✓ flows run/resume CLI over the journal protocol > maps only run_not_found resumes to exit 2 1570ms + ✓ tests/observer-link.test.ts (39 tests) 370ms + ✓ tests/preflight.test.ts (27 tests) 143ms + ✓ tests/daemon-lifecycle.test.ts (42 tests) 83ms + ✓ tests/authored-flow.test.ts (25 tests) 846ms + ✓ tests/worker-cli.test.ts (13 tests) 20932ms + ✓ custom wrapper execution identity > refuses a wrapper symlink retarget before delivering private values 381ms + ✓ custom wrapper execution identity > bounds wrapper execution after acknowledgement 307ms + ✓ custom wrapper execution identity > refuses a duplicate execute protocol frame 328ms + ✓ custom wrapper execution bounds are reader-owned > resolves when a conforming wrapper leaks a stdio pipe to a background helper 1803ms + ✓ custom wrapper execution bounds are reader-owned > resolves when the leaked helper inherits stderr only 1819ms + ✓ custom wrapper execution bounds are reader-owned > resolves when a wrapper leaks a stdio pipe and exits before identifying 3257ms + ✓ custom wrapper execution bounds are reader-owned > journals a completionReason at the default bound when a wrapper leaks a stdio pipe 11259ms + ✓ custom wrapper execution bounds are reader-owned > accepts the same over-8KiB payload whether or not it coalesces with the execute token 499ms + ✓ tests/mcp.test.ts (30 tests) 10469ms + ✓ MCP preflight and transports > flows check refuses an undeclared server with exit 2 and no daemon 813ms + ✓ MCP preflight and transports > flows check reports a refusing server and leaves no PID 930ms + ✓ MCP preflight and transports > kills a SIGTERM-resistant silent child after a parent-owned handshake deadline 1312ms + ✓ MCP preflight and transports > reaps a SIGTERM-resistant descendant with inherit stdio before cleanup finishes 1295ms + ✓ MCP preflight and transports > reaps a SIGTERM-resistant descendant with ignore stdio before cleanup finishes 2062ms + ✓ MCP preflight and transports > reports malformed connection configuration as config_invalid 840ms + ✓ authored MCP effects against the real kernel > journals one MCP receipt per call with args, result, stable logical key, and a confirmed effect 663ms + ✓ authored MCP effects against the real kernel > reports a dropped tool connection as a failed CLI run 1196ms + ✓ authored MCP effects against the real kernel > journals drop/echo failure as worker_error with diagnostic mcp_disconnected 328ms + ✓ tests/validate.test.ts (68 tests) 27ms + ✓ tests/journal-client.test.ts (14 tests) 105ms + ✓ tests/tick-source.test.ts (33 tests) 91ms + ✓ tests/verb-field-lint.test.ts (78 tests) 510ms + ✓ tests/cloud-run.test.ts (47 tests) 237ms + ✓ tests/authored-flow-lifecycle-executor.test.ts (27 tests) 675ms + ✓ tests/cli-replay.test.ts (36 tests) 2709ms + ✓ flows replay > --json is byte-identical across two CLI invocations (diff) 2121ms +stdout | tests/live-kernel.test.ts > built flows CLI against live relayflowd > hn-monitor analyze-story reaches done through the real Claude analyzer CLI +LIVE_ANALYZER ready: claude -p --model claude-haiku-4-5-20251001 round-trip OK + + ✓ tests/tick-runner.test.ts (22 tests) 5119ms + ✓ CLI argument parsing refuses coercion rather than accepting it > refuses --interval-ms fractional as an invocation error 802ms + ✓ CLI argument parsing refuses coercion rather than accepting it > refuses --interval-ms exponent notation as an invocation error 848ms + ✓ CLI argument parsing refuses coercion rather than accepting it > refuses --interval-ms hex as an invocation error 1211ms + ✓ CLI argument parsing refuses coercion rather than accepting it > refuses --interval-ms trailing text as an invocation error 745ms + ✓ CLI argument parsing refuses coercion rather than accepting it > refuses --interval-ms empty as an invocation error 700ms + ✓ CLI argument parsing refuses coercion rather than accepting it > accepts an exact integer and proceeds past parsing 713ms +stdout | tests/live-kernel.test.ts > built flows CLI against live relayflowd > hn-monitor analyze-story reaches done through the real Claude analyzer CLI +LIVE_ANALYZER analysis: {"reasoning":"This story directly demonstrates a practical AI agent implementation performing autonomous software development tasks—opening and reviewing pull requests—which is a core application of AI agents and automation in development workflows.","relevance_score":9,"story_title":"Show HN: an agent that opens and reviews its own pull requests [wake-nonce-7f3a91c4]"} + +stdout | tests/live-kernel.test.ts > surface resume after a real daemon kill > resumes a three-step run with each successful completion exactly once +LIVE_KERNEL kill -9 pid=59295 run=01M290DZP9HM8B2MXH72ZW18F1 while step=two state=Running + + ✓ tests/stop-process-group.test.ts (6 tests) 7736ms + ✓ every stop reaches the process group, not just the direct child > exits the run after an execution-timeout stop 927ms + ✓ every stop reaches the process group, not just the direct child > exits the run after a protocol terminate stop 709ms + ✓ every stop reaches the process group, not just the direct child > kills a SIGTERM-deaf grandchild after a protocol terminate stop 2050ms + ✓ every stop reaches the process group, not just the direct child > kills a SIGTERM-deaf grandchild after an execution-timeout stop 2179ms + ✓ every stop reaches the process group, not just the direct child > holds the loop open long enough for the escalation to run 1389ms + ✓ every stop reaches the process group, not just the direct child > terminate() forces a group that outlives SIGTERM 479ms + ✓ tests/live-kernel.test.ts (31 tests) 83164ms + ✓ built flows CLI against live relayflowd > twenty-six-step reuses 25 durable completions after editing the failed final step 6446ms + ✓ built flows CLI against live relayflowd > runs rung (a), parks rung (b), and keeps JSON report-shaped 9141ms + ✓ built flows CLI against live relayflowd > allows a deterministic run to exceed the bounded request timeout 33450ms + ✓ built flows CLI against live relayflowd > follows a live worker dispatch through flows run 1801ms + ✓ built flows CLI against live relayflowd > runs an agent CLI end to end through the SDK worker 369ms + ✓ built flows CLI against live relayflowd > f.agent lowers to a real agent step and dispatches through a live worker 698ms + ✓ built flows CLI against live relayflowd > f.agent's default flowPath anchors on cwd, not cwd's parent 761ms + ✓ built flows CLI against live relayflowd > can always get a parked run to a late-attaching worker 5680ms + ✓ built flows CLI against live relayflowd > reports a real manual-recovery NeedsHuman state as parked 2454ms + ✓ built flows CLI against live relayflowd > runs hn-monitor analyze-story end-to-end via a stub agent CLI (gate 2 clause 2 demo) 438ms + ✓ built flows CLI against live relayflowd > hn-monitor analyze-story FAILS verification when the CLI omits required schema fields 390ms + ✓ built flows CLI against live relayflowd > agent step preserves the CliResult wrapper as output when the CLI emits non-JSON text 302ms + ✓ built flows CLI against live relayflowd > AgentWorker exposes wake_context to the CLI via RELAYFLOW_WAKE_CONTEXT env var (real analyzer prerequisite) 371ms + ✓ built flows CLI against live relayflowd > AgentWorker passes a declared model to an identified wrapper as RELAYFLOW_MODEL 603ms + ✓ built flows CLI against live relayflowd > AgentWorker refuses a nonconforming journal-submitted wrapper before exposing RELAYFLOW_MODEL 390ms + ✓ built flows CLI against live relayflowd > hn-monitor analyze-story reaches done through the real Claude analyzer CLI 10337ms + ✓ built flows CLI against live relayflowd > preflights before journaling and names an unreachable socket 2501ms + ✓ built flows CLI against live relayflowd > starts exactly one daemon when two runs race for one empty data dir 1360ms + ✓ surface resume after a real daemon kill > resumes a three-step run with each successful completion exactly once 3014ms + ✓ a relayflow can be scheduled: tick source against live relayflowd > a tick spawns a real run whose step reports the SCHEDULED instant 471ms + ✓ tests/gate-contract.test.ts (20 tests) 493ms + ✓ tests/cli-hn-monitor.test.ts (16 tests) 142ms + ✓ tests/authored-flow-slack.test.ts (6 tests) 3818ms + ✓ authored Slack helper effects > journals exactly one effect with the authored params and typed receipt, without network 353ms + ✓ authored Slack helper effects > replays after SIGKILL before confirm with the same token and one successful completion 1232ms + ✓ authored Slack helper effects > replays after SIGKILL before complete with the same token and one successful completion 1204ms + ✓ authored Slack helper effects > writes two files for two calls and supports dm, reply, and react 992ms + ✓ tests/bundle.test.ts (21 tests) 16282ms + ✓ immutable bundles > returns exit 2 naming a byte-flipped payload and refuses to reuse corruption 921ms + ✓ immutable bundles > builds and verifies the canonical YAML fixture through the compiled CLI 2446ms + ✓ immutable bundles > emits the ephemeral warning on CLI stderr and uses the default output directory 1710ms + ✓ immutable bundles > refuses build-provable CLI resolution errors without environment probes 813ms + ✓ immutable bundles > builds a standalone TS fixture twice with identical executable hashes 5288ms + ✓ immutable bundles > refuses to label installed dependency drift with lockfile pins 812ms + ✓ immutable bundles > refuses invalid CLI arguments %j 787ms + ✓ immutable bundles > refuses invalid CLI arguments "--out" 797ms + ✓ immutable bundles > refuses invalid CLI arguments "--verify" 766ms + ✓ immutable bundles > refuses invalid CLI arguments "--verify" 757ms + ✓ immutable bundles > refuses invalid CLI arguments "--out" 797ms + ✓ tests/daemon-lifecycle-live.test.ts (9 tests) 18407ms + ✓ flows run against a data dir with no daemon (§6 test 7) > cold start spawns exactly one daemon, the run succeeds, and the daemon outlives the CLI 1512ms + ✓ flows run against a data dir with no daemon (§6 test 7) > polls, bounded, for a daemon that holds the lock before it binds 2187ms + ✓ flows run against a data dir with no daemon (§6 test 7) > attaches to a serving daemon that has not published a connection file 1607ms + ✓ flows run against a data dir with no daemon (§6 test 7) > a second run attaches to the daemon the first one started, spawning nothing 3468ms + ✓ flows run against a data dir with no daemon (§6 test 7) > detects a stale connection file left by a hard kill and starts a fresh daemon 3192ms + ✓ concurrent invocations against one empty data dir (§6 test 15) > ends with exactly one daemon owning the socket, and both runs succeed 1994ms + ✓ refusals from a spawn that cannot produce a daemon > names relayflowd_not_found rather than falling through to PATH 1398ms + ✓ refusals from a spawn that cannot produce a daemon > names daemon_start_failed and quotes the daemon log when startup dies 1541ms + ✓ refusals from a spawn that cannot produce a daemon > refuses a daemon speaking another protocol version instead of binding over it 1504ms + ✓ tests/authored-helpers.test.ts (6 tests) 11265ms + ✓ lowers the named acceptance helpers and Slack to confirmed journal effects 1031ms + ✓ runs every available provider through the real kernel and resumes completed effects without a second write 7285ms + ✓ replays after SIGKILL before confirm with the same token and one successful completion 1224ms + ✓ replays after SIGKILL before complete with the same token and one successful completion 1142ms + ✓ journals provider failure with worker_error and never confirms the effect 572ms + ✓ tests/backlog-picker.test.ts (14 tests) 191ms + ✓ tests/backlog-picker-flow.test.ts (6 tests) 1239ms + ✓ backlog-picker flow > emit-package describes the entry select-entry chose, even if the backlog changes between them 308ms + ✓ backlog-picker flow > does not emit a stale entry left by a previous run 626ms + ✓ tests/flow-executor-chain.test.ts (12 tests) 16027ms + ✓ flow executor LLM and output-binding chain > runs f.llm -> f.agent -> f.run with schema-verified journal output and the exact allowed model 1775ms + ✓ flow executor LLM and output-binding chain > fails invalid LLM output before the next step: not JSON 1027ms + ✓ flow executor LLM and output-binding chain > fails invalid LLM output before the next step: {"message":7} 1107ms + ✓ flow executor LLM and output-binding chain > retains tagged-template text output 882ms + ✓ flow executor LLM and output-binding chain > preserves JSON values without promoting them to process wrappers: null 1058ms + ✓ flow executor LLM and output-binding chain > preserves JSON values without promoting them to process wrappers: [1,2] 1043ms + ✓ flow executor LLM and output-binding chain > preserves JSON values without promoting them to process wrappers: "hello" 1053ms + ✓ flow executor LLM and output-binding chain > runs the authored LLM path through the built flows CLI 2283ms + ✓ flow executor LLM and output-binding chain > passes a declarative verified value through an agent into a deterministic artifact 1419ms + ✓ flow executor LLM and output-binding chain > journals a missing optional field as a failure before the consuming command executes 1080ms + ✓ flow executor LLM and output-binding chain > flows run consumes YAML bindings and resume reuses the original journal output 3226ms + ✓ tests/authored-flow-operation.test.ts (23 tests) 656ms + ✓ completes the gate in linear time over a body with 30000 ordinary awaits 485ms + ✓ tests/direct-input.test.ts (4 tests) 13192ms + ✓ direct .flow.ts input through the built CLI and live runtime > executes inline and file JSON input through relayflowd 4688ms + ✓ direct .flow.ts input through the built CLI and live runtime > refuses missing and malformed input before contacting relayflowd 5518ms + ✓ direct .flow.ts input through the built CLI and live runtime > does not run the authored body before daemon availability 1517ms + ✓ direct .flow.ts input through the built CLI and live runtime > refuses oversized file input before contacting relayflowd 1466ms + ✓ tests/work-package-consumer.test.ts (13 tests) 529ms + ✓ the Garden join: picker output feeds the consumer > accepts a package the picker actually emits, and refuses one lacking a definition of done 516ms + ✓ tests/spec-parity.test.ts (31 tests) 947ms + ✓ tests/helpers-fanout.test.ts (95 tests) 848ms + ✓ tests/cli-watch.test.ts (10 tests) 30440ms + ✓ flows check --watch > rechecks syntax errors, clears once, and returns the last refusal on Ctrl-C 2713ms + ✓ flows check --watch > streams JSON lines without ANSI, recovers after atomic saves, and exits zero after repair 3606ms + ✓ flows check --watch > coalesces 20 concurrent saves into at most two rechecks 3247ms + ✓ flows check --watch > watches transitive relative use imports, cycles, and nearest config changes 4749ms + ✓ flows check --watch > refreshes the import graph and notices missing imports being created 4715ms + ✓ flows check --watch > reloads authored TypeScript instead of reusing the first imported definition 2892ms + ✓ flows check --watch > detects a nearer config appearing and falls back after it is deleted 3930ms + ✓ flows check --watch > keeps watching after the target is deleted and recreated 3779ms + ✓ flows check --watch > queues changes during a slow check without overlapping checks 801ms + ✓ tests/worker-lease.test.ts (7 tests) 31ms + ✓ tests/typed-output.test.ts (14 tests) 554ms + ✓ tests/webhook-live.test.ts (3 tests) 7522ms + ✓ flows serve-webhook writes JSON before the daemon starts, then journals and archives exactly once 2205ms + ✓ replays a dropped file after SIGKILL before spawn 1004ms + ✓ resumes the same journal after SIGKILL after spawn and before acknowledgement 4311ms + ✓ tests/effect-channel.test.ts (5 tests) 676ms + ✓ tests/mcp-lifecycle.test.ts (4 tests) 28ms + ✓ tests/model-selection.test.ts (10 tests) 45ms + ✓ tests/json-schema-bound.test.ts (71 tests) 7492ms + ✓ JSON Schema termination bound > walks a deep schema with an explicit stack rather than recursion 6380ms + ✓ tests/relayflowd-path.test.ts (10 tests) 11ms + ✓ tests/f-memory.test.ts (7 tests) 2782ms + ✓ reads the seeded local SQLite database with cloud unused and fallback disabled 551ms + ✓ cannot widen script scope using a raw project option or another flow name 427ms + ✓ attaches read helpers without journaling read steps 396ms + ✓ refuses missing, corrupt, and directory database paths before body or journal activity 361ms + ✓ detects direct memory use before running earlier body effects 369ms + ✓ maps an unavailable provider probe to memory_unreachable 323ms + ✓ fails closed for deferred writes, agent scope, disabled script memory, and reads after done 353ms + ✓ tests/webhook.test.ts (6 tests) 171ms + ✓ tests/yaml-local-agent-live.test.ts (7 tests) 8993ms + ✓ YAML --local-agent through the built CLI and real daemon > runs with the checked step CLI and model and journals done 1348ms + ✓ YAML --local-agent through the built CLI and real daemon > runs with the checked named CLI and model and journals done 1330ms + ✓ YAML --local-agent through the built CLI and real daemon > runs with the checked flow CLI and model and journals done 1363ms + ✓ YAML --local-agent through the built CLI and real daemon > runs with the checked project CLI and model and journals done 1325ms + ✓ YAML --local-agent through the built CLI and real daemon > still parks without --local-agent 1150ms + ✓ YAML --local-agent through the built CLI and real daemon > reports the agent process failure 1323ms + ✓ YAML --local-agent through the built CLI and real daemon > preserves declared workspace surfaces that the local worker cannot pin 1153ms + ✓ tests/local-dev-ux.test.ts (8 tests) 50ms + ✓ tests/dependency-validation.test.ts (6 tests) 1465ms + ✓ dependency validation > accepts a valid 10,000-step reverse chain through every direct public boundary 824ms + ✓ dependency validation > bounds the author-facing path for a 10,000-step cycle 544ms + ✓ tests/input-binding.test.ts (12 tests) 529ms + ✓ tests/deterministic-llm.test.ts (5 tests) 170ms + ✓ tests/bin.test.ts (7 tests) 4695ms + ✓ built flows binary > refuses through a symlink to the built artifact 778ms + ✓ built flows binary > refuses through a symlinked directory component 808ms + ✓ built flows binary > classifies a signal-terminated auth probe as probe_failed 815ms + ✓ built flows binary > classifies an unavailable PATH resolver as probe_failed 700ms + ✓ built flows binary > does not describe a present non-executable CLI as missing 793ms + ✓ built flows binary > runs one auth probe for three steps sharing a flow CLI 798ms + ✓ tests/scope-compiler.test.ts (25 tests) 28ms + ✓ tests/budget-attribution.test.ts (4 tests) 11ms + ✓ tests/hn-poller.test.ts (6 tests) 10ms + ✓ tests/dir-watcher-poller.test.ts (6 tests) 13ms + ✓ tests/hello-deterministic.test.ts (5 tests) 37ms + ✓ tests/budget-preflight.test.ts (16 tests) 20ms + ✓ tests/work-package-validator.test.ts (7 tests) 14ms + ✓ tests/classify-outcome.test.ts (2 tests) 2233ms + ✓ classifyOutcome > gives up and reports when a running run never becomes classifiable 2072ms + ↓ tests/real-cli-adapters.test.ts (3 tests | 3 skipped) + ✓ tests/model-pricing.test.ts (8 tests) 8ms + ✓ tests/authored-use-loader.test.ts (5 tests) 117ms + ✓ tests/memoization.test.ts (57 tests) 1170ms + ✓ refuses invalid reuse invocation "run" 1068ms + ✓ tests/parse-json-output.test.ts (7 tests) 8ms + ✓ tests/cli-adapter.test.ts (3 tests) 11ms + ✓ tests/journal-client-completion.test.ts (4 tests) 116ms + ✓ tests/worker-cli-abort.test.ts (2 tests) 2951ms + ✓ stops claude and its process group when lease ownership is lost 1466ms + ✓ stops wrapper.mjs and its process group when lease ownership is lost 1483ms + ✓ tests/direct-run-failure.test.ts (6 tests) 14ms + ✓ tests/budget-authored-live.test.ts (2 tests) 670ms + ✓ authored budgets through the live kernel > retains the first completion and journals refusal of the next authored step 326ms + ✓ authored budgets through the live kernel > permits done after the last valid step crosses the limit 343ms + ✓ tests/slack-writeback.test.ts (1 test) 275ms + ✓ tests/local-agent-live.test.ts (5 tests) 45923ms + ✓ built CLI local agent against a real daemon > dispatches through the wrapper and keeps --json stdout report-shaped 1917ms + ✓ built CLI local agent against a real daemon > runs beyond the initial 30-second lease without a second invocation 36937ms + ✓ built CLI local agent against a real daemon > renders actual agent completion in text output 3824ms + ✓ built CLI local agent against a real daemon > returns a failed run when the agent process fails 1753ms + ✓ built CLI local agent against a real daemon > refuses a workspace it cannot pin before invoking the agent 1489ms + ✓ tests/placement.test.ts (54 tests) 41ms + ✓ tests/memory.test.ts (18 tests) 10ms + ✓ tests/cli-progress-wait.test.ts (2 tests) 1229ms + ✓ run starts the wait clock on its first observed lease 683ms + ✓ resume starts the wait clock on its first observed lease 545ms + ✓ tests/worker-platform.test.ts (1 test) 8ms + + Test Files 71 passed | 1 skipped (72) + Tests 1315 passed | 3 skipped (1318) + Start at 21:50:45 + Duration 214.13s (transform 3.28s, setup 0ms, collect 45.07s, tests 351.45s, environment 24ms, prepare 8.01s) + diff --git a/evidence/slice-S/sdk-tests-final.txt b/evidence/slice-S/sdk-tests-final.txt new file mode 100644 index 000000000..c0ff6f569 --- /dev/null +++ b/evidence/slice-S/sdk-tests-final.txt @@ -0,0 +1,293 @@ + +> @relayflows/sdk@2.0.8 test +> sh scripts/test.sh + + +> @relayflows/sdk@2.0.8 test:prep +> ( cd ../../kernel && sh ../ops/cargo.sh build ) && ( [ ! -d ../../testdata/preflight ] || find ../../testdata/preflight -name '*-cli' -type f -exec chmod +x {} + ) + + Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.64s + +> @relayflows/sdk@2.0.8 typecheck +> tsc --noEmit && tsc -p tsconfig.type-tests.json + + +> @relayflows/sdk@2.0.8 build +> tsc && node scripts/make-cli-executable.mjs + + +> @relayflows/sdk@2.0.8 typecheck:tests +> tsc -p tsconfig.tests.json + + + RUN v2.1.9 /Users/khaliqgant/fl-slice-S/packages/sdk + +stdout | tests/live-kernel.test.ts +LIVE_KERNEL relayflowd=/Users/khaliqgant/.relayflows-toolchain/target/2945819964/debug/relayflowd +LIVE_KERNEL flows=/Users/khaliqgant/fl-slice-S/packages/sdk/dist/cli.js + + ✓ tests/cli.test.ts (63 tests) 12160ms + ✓ flows check CLI > binds a checked relative wrapper to the flow directory for worker execution 1247ms + ✓ flows check CLI > uses the raw Claude adapter model flag instead of accepting auth status as model proof 333ms + ✓ flows check CLI > refuses a typo model before probing or contacting relayflowd 667ms + ✓ flows check CLI > maps every input refusal path to its declared kind without raw exceptions 1598ms + ✓ flows run/resume CLI over the journal protocol > parses run options, submits the kernel dialect, and exits 0 on success 651ms + ✓ flows run/resume CLI over the journal protocol > exits 1 and emits the declared completionReason for a failed run 484ms + ✓ flows run/resume CLI over the journal protocol > exits 3 and names the parked llm step 657ms + ✓ flows run/resume CLI over the journal protocol > reports a needs_human agent step as parked for human recovery 644ms + ✓ flows run/resume CLI over the journal protocol > follows a dispatched worker step instead of reporting a protocol error 548ms + ✓ flows run/resume CLI over the journal protocol > bounds a worker wait by its lease and reports what it is waiting for 550ms + ✓ flows run/resume CLI over the journal protocol > resumes a parked run from snapshot step types without reading journal sequence one 459ms + ✓ flows run/resume CLI over the journal protocol > maps only run_not_found resumes to exit 2 1517ms + ✓ tests/observer-link.test.ts (39 tests) 332ms + ✓ tests/preflight.test.ts (27 tests) 144ms + ✓ tests/daemon-lifecycle.test.ts (42 tests) 84ms + ✓ tests/authored-flow.test.ts (25 tests) 840ms + ✓ tests/worker-cli.test.ts (13 tests) 22490ms + ✓ custom wrapper execution identity > passes an explicit safe environment at identification and execution 383ms + ✓ custom wrapper execution identity > refuses a wrapper symlink retarget before delivering private values 365ms + ✓ custom wrapper execution identity > bounds wrapper execution after acknowledgement 419ms + ✓ custom wrapper execution identity > bounds captured wrapper output 433ms + ✓ custom wrapper execution identity > refuses a duplicate execute protocol frame 435ms + ✓ custom wrapper execution bounds are reader-owned > resolves when a conforming wrapper leaks a stdio pipe to a background helper 1954ms + ✓ custom wrapper execution bounds are reader-owned > resolves when the leaked helper inherits stderr only 1929ms + ✓ custom wrapper execution bounds are reader-owned > resolves when a wrapper leaks a stdio pipe and exits before identifying 3257ms + ✓ custom wrapper execution bounds are reader-owned > journals a completionReason at the default bound when a wrapper leaks a stdio pipe 11263ms + ✓ custom wrapper execution bounds are reader-owned > accepts an execute token and an over-8KiB payload flushed in one write 391ms + ✓ custom wrapper execution bounds are reader-owned > accepts the same over-8KiB payload whether or not it coalesces with the execute token 865ms + ✓ custom wrapper execution bounds are reader-owned > still bounds an un-terminated handshake buffer and names the bound 375ms + ✓ delivers the journaled memory pack to the real wrapper and excludes its charge from completion usage 418ms + ✓ tests/mcp.test.ts (30 tests) 10785ms + ✓ MCP preflight and transports > flows check refuses an undeclared server with exit 2 and no daemon 878ms + ✓ MCP preflight and transports > flows check reports a refusing server and leaves no PID 994ms + ✓ MCP preflight and transports > kills a SIGTERM-resistant silent child after a parent-owned handshake deadline 1316ms + ✓ MCP preflight and transports > reaps a SIGTERM-resistant descendant with inherit stdio before cleanup finishes 1327ms + ✓ MCP preflight and transports > reaps a SIGTERM-resistant descendant with ignore stdio before cleanup finishes 2063ms + ✓ MCP preflight and transports > reports malformed connection configuration as config_invalid 864ms + ✓ authored MCP effects against the real kernel > journals one MCP receipt per call with args, result, stable logical key, and a confirmed effect 680ms + ✓ authored MCP effects against the real kernel > reports a dropped tool connection as a failed CLI run 1078ms + ✓ authored MCP effects against the real kernel > journals drop/echo failure as worker_error with diagnostic mcp_disconnected 387ms + ✓ tests/validate.test.ts (68 tests) 50ms + ✓ tests/journal-client.test.ts (14 tests) 120ms + ✓ tests/tick-source.test.ts (33 tests) 58ms + ✓ tests/verb-field-lint.test.ts (78 tests) 812ms + ✓ tests/cloud-run.test.ts (47 tests) 203ms + ✓ tests/authored-flow-lifecycle-executor.test.ts (27 tests) 703ms + ✓ tests/cli-replay.test.ts (36 tests) 2245ms + ✓ flows replay > --json is byte-identical across two CLI invocations (diff) 1629ms +stdout | tests/live-kernel.test.ts > built flows CLI against live relayflowd > hn-monitor analyze-story reaches done through the real Claude analyzer CLI +LIVE_ANALYZER ready: claude -p --model claude-haiku-4-5-20251001 round-trip OK + + ✓ tests/tick-runner.test.ts (22 tests) 5740ms + ✓ CLI argument parsing refuses coercion rather than accepting it > refuses --interval-ms fractional as an invocation error 987ms + ✓ CLI argument parsing refuses coercion rather than accepting it > refuses --interval-ms exponent notation as an invocation error 782ms + ✓ CLI argument parsing refuses coercion rather than accepting it > refuses --interval-ms hex as an invocation error 792ms + ✓ CLI argument parsing refuses coercion rather than accepting it > refuses --interval-ms trailing text as an invocation error 888ms + ✓ CLI argument parsing refuses coercion rather than accepting it > refuses --interval-ms empty as an invocation error 1153ms + ✓ CLI argument parsing refuses coercion rather than accepting it > accepts an exact integer and proceeds past parsing 819ms +stdout | tests/live-kernel.test.ts > built flows CLI against live relayflowd > hn-monitor analyze-story reaches done through the real Claude analyzer CLI +LIVE_ANALYZER analysis: {"reasoning":"This story is directly relevant to AI agents and automation, describing an agent system that autonomously performs software development tasks (opening and reviewing pull requests), which is a core application of agentic AI systems.","relevance_score":10,"story_title":"Show HN: an agent that opens and reviews its own pull requests [wake-nonce-7f3a91c4]"} + + ✓ tests/stop-process-group.test.ts (6 tests) 7833ms + ✓ every stop reaches the process group, not just the direct child > exits the run after an execution-timeout stop 968ms + ✓ every stop reaches the process group, not just the direct child > exits the run after a protocol terminate stop 776ms + ✓ every stop reaches the process group, not just the direct child > kills a SIGTERM-deaf grandchild after a protocol terminate stop 2006ms + ✓ every stop reaches the process group, not just the direct child > kills a SIGTERM-deaf grandchild after an execution-timeout stop 2203ms + ✓ every stop reaches the process group, not just the direct child > holds the loop open long enough for the escalation to run 1393ms + ✓ every stop reaches the process group, not just the direct child > terminate() forces a group that outlives SIGTERM 487ms + ✓ tests/gate-contract.test.ts (20 tests) 385ms +stdout | tests/live-kernel.test.ts > surface resume after a real daemon kill > resumes a three-step run with each successful completion exactly once +LIVE_KERNEL kill -9 pid=88815 run=01M290SXJQ9EFTBB2HFXT2C6HT while step=two state=Running + + ✓ tests/cli-hn-monitor.test.ts (16 tests) 155ms + ✓ tests/live-kernel.test.ts (31 tests) 85264ms + ✓ built flows CLI against live relayflowd > twenty-six-step reuses 25 durable completions after editing the failed final step 6153ms + ✓ built flows CLI against live relayflowd > runs rung (a), parks rung (b), and keeps JSON report-shaped 8140ms + ✓ built flows CLI against live relayflowd > allows a deterministic run to exceed the bounded request timeout 33383ms + ✓ built flows CLI against live relayflowd > follows a live worker dispatch through flows run 1687ms + ✓ built flows CLI against live relayflowd > runs an agent CLI end to end through the SDK worker 492ms + ✓ built flows CLI against live relayflowd > f.agent lowers to a real agent step and dispatches through a live worker 848ms + ✓ built flows CLI against live relayflowd > f.agent's default flowPath anchors on cwd, not cwd's parent 835ms + ✓ built flows CLI against live relayflowd > can always get a parked run to a late-attaching worker 5712ms + ✓ built flows CLI against live relayflowd > reports a real manual-recovery NeedsHuman state as parked 1701ms + ✓ built flows CLI against live relayflowd > runs hn-monitor analyze-story end-to-end via a stub agent CLI (gate 2 clause 2 demo) 401ms + ✓ built flows CLI against live relayflowd > hn-monitor analyze-story FAILS verification when the CLI omits required schema fields 388ms + ✓ built flows CLI against live relayflowd > AgentWorker exposes wake_context to the CLI via RELAYFLOW_WAKE_CONTEXT env var (real analyzer prerequisite) 422ms + ✓ built flows CLI against live relayflowd > AgentWorker passes a declared model to an identified wrapper as RELAYFLOW_MODEL 814ms + ✓ built flows CLI against live relayflowd > AgentWorker refuses a nonconforming journal-submitted wrapper before exposing RELAYFLOW_MODEL 581ms + ✓ built flows CLI against live relayflowd > AgentWorker executes the raw claude adapter with its real model flag 446ms + ✓ built flows CLI against live relayflowd > AgentWorker executes the raw codex adapter with its real model flag 402ms + ✓ built flows CLI against live relayflowd > hn-monitor analyze-story reaches done through the real Claude analyzer CLI 11818ms + ✓ built flows CLI against live relayflowd > preflights before journaling and names an unreachable socket 3033ms + ✓ built flows CLI against live relayflowd > starts exactly one daemon when two runs race for one empty data dir 2186ms + ✓ surface resume after a real daemon kill > resumes a three-step run with each successful completion exactly once 3305ms + ✓ a relayflow can be scheduled: tick source against live relayflowd > a tick spawns a real run whose step reports the SCHEDULED instant 385ms + ✓ tests/authored-flow-slack.test.ts (6 tests) 4011ms + ✓ authored Slack helper effects > journals exactly one effect with the authored params and typed receipt, without network 385ms + ✓ authored Slack helper effects > replays after SIGKILL before confirm with the same token and one successful completion 1378ms + ✓ authored Slack helper effects > replays after SIGKILL before complete with the same token and one successful completion 1216ms + ✓ authored Slack helper effects > writes two files for two calls and supports dm, reply, and react 990ms + ✓ tests/bundle.test.ts (21 tests) 15537ms + ✓ immutable bundles > returns exit 2 naming a byte-flipped payload and refuses to reuse corruption 792ms + ✓ immutable bundles > builds and verifies the canonical YAML fixture through the compiled CLI 2598ms + ✓ immutable bundles > emits the ephemeral warning on CLI stderr and uses the default output directory 1737ms + ✓ immutable bundles > preserves quoted asset words and executable permissions 346ms + ✓ immutable bundles > refuses build-provable CLI resolution errors without environment probes 745ms + ✓ immutable bundles > builds a standalone TS fixture twice with identical executable hashes 4762ms + ✓ immutable bundles > refuses to label installed dependency drift with lockfile pins 701ms + ✓ immutable bundles > refuses invalid CLI arguments %j 710ms + ✓ immutable bundles > refuses invalid CLI arguments "--out" 772ms + ✓ immutable bundles > refuses invalid CLI arguments "--verify" 688ms + ✓ immutable bundles > refuses invalid CLI arguments "--verify" 744ms + ✓ immutable bundles > refuses invalid CLI arguments "--out" 704ms + ✓ tests/daemon-lifecycle-live.test.ts (9 tests) 17465ms + ✓ flows run against a data dir with no daemon (§6 test 7) > cold start spawns exactly one daemon, the run succeeds, and the daemon outlives the CLI 1853ms + ✓ flows run against a data dir with no daemon (§6 test 7) > polls, bounded, for a daemon that holds the lock before it binds 2114ms + ✓ flows run against a data dir with no daemon (§6 test 7) > attaches to a serving daemon that has not published a connection file 1366ms + ✓ flows run against a data dir with no daemon (§6 test 7) > a second run attaches to the daemon the first one started, spawning nothing 2949ms + ✓ flows run against a data dir with no daemon (§6 test 7) > detects a stale connection file left by a hard kill and starts a fresh daemon 3104ms + ✓ concurrent invocations against one empty data dir (§6 test 15) > ends with exactly one daemon owning the socket, and both runs succeed 1833ms + ✓ refusals from a spawn that cannot produce a daemon > names relayflowd_not_found rather than falling through to PATH 1358ms + ✓ refusals from a spawn that cannot produce a daemon > names daemon_start_failed and quotes the daemon log when startup dies 1426ms + ✓ refusals from a spawn that cannot produce a daemon > refuses a daemon speaking another protocol version instead of binding over it 1459ms + ✓ tests/authored-helpers.test.ts (6 tests) 11228ms + ✓ lowers the named acceptance helpers and Slack to confirmed journal effects 1060ms + ✓ runs every available provider through the real kernel and resumes completed effects without a second write 7221ms + ✓ replays after SIGKILL before confirm with the same token and one successful completion 1232ms + ✓ replays after SIGKILL before complete with the same token and one successful completion 1157ms + ✓ journals provider failure with worker_error and never confirms the effect 546ms + ✓ tests/backlog-picker.test.ts (14 tests) 184ms + ✓ tests/backlog-picker-flow.test.ts (6 tests) 969ms + ✓ backlog-picker flow > emit-package describes the entry select-entry chose, even if the backlog changes between them 359ms + ✓ backlog-picker flow > does not emit a stale entry left by a previous run 434ms + ✓ tests/flow-executor-chain.test.ts (12 tests) 14212ms + ✓ flow executor LLM and output-binding chain > runs f.llm -> f.agent -> f.run with schema-verified journal output and the exact allowed model 1724ms + ✓ flow executor LLM and output-binding chain > fails invalid LLM output before the next step: not JSON 995ms + ✓ flow executor LLM and output-binding chain > fails invalid LLM output before the next step: {"message":7} 1011ms + ✓ flow executor LLM and output-binding chain > retains tagged-template text output 818ms + ✓ flow executor LLM and output-binding chain > preserves JSON values without promoting them to process wrappers: null 809ms + ✓ flow executor LLM and output-binding chain > preserves JSON values without promoting them to process wrappers: [1,2] 721ms + ✓ flow executor LLM and output-binding chain > preserves JSON values without promoting them to process wrappers: "hello" 931ms + ✓ flow executor LLM and output-binding chain > runs the authored LLM path through the built flows CLI 1920ms + ✓ flow executor LLM and output-binding chain > passes a declarative verified value through an agent into a deterministic artifact 1065ms + ✓ flow executor LLM and output-binding chain > journals a missing optional field as a failure before the consuming command executes 844ms + ✓ flow executor LLM and output-binding chain > flows run consumes YAML bindings and resume reuses the original journal output 3318ms + ✓ tests/authored-flow-operation.test.ts (23 tests) 643ms + ✓ completes the gate in linear time over a body with 30000 ordinary awaits 473ms + ✓ tests/direct-input.test.ts (4 tests) 15281ms + ✓ direct .flow.ts input through the built CLI and live runtime > executes inline and file JSON input through relayflowd 4115ms + ✓ direct .flow.ts input through the built CLI and live runtime > refuses missing and malformed input before contacting relayflowd 7872ms + ✓ direct .flow.ts input through the built CLI and live runtime > does not run the authored body before daemon availability 1883ms + ✓ direct .flow.ts input through the built CLI and live runtime > refuses oversized file input before contacting relayflowd 1409ms + ✓ tests/work-package-consumer.test.ts (13 tests) 755ms + ✓ the Garden join: picker output feeds the consumer > accepts a package the picker actually emits, and refuses one lacking a definition of done 742ms + ❯ tests/cli-watch.test.ts (10 tests | 2 failed) 32431ms + ✓ flows check --watch > rechecks syntax errors, clears once, and returns the last refusal on Ctrl-C 1796ms + × flows check --watch > streams JSON lines without ANSI, recovers after atomic saves, and exits zero after repair 5163ms + → Test timed out in 5000ms. +If this is a long-running test, pass a timeout value as the last argument or configure it globally with "testTimeout". + ✓ flows check --watch > coalesces 20 concurrent saves into at most two rechecks 2982ms + ✓ flows check --watch > watches transitive relative use imports, cycles, and nearest config changes 3819ms + × flows check --watch > refreshes the import graph and notices missing imports being created 5143ms + → Test timed out in 5000ms. +If this is a long-running test, pass a timeout value as the last argument or configure it globally with "testTimeout". + ✓ flows check --watch > reloads authored TypeScript instead of reusing the first imported definition 5160ms + ✓ flows check --watch > detects a nearer config appearing and falls back after it is deleted 3870ms + ✓ flows check --watch > keeps watching after the target is deleted and recreated 3688ms + ✓ flows check --watch > queues changes during a slow check without overlapping checks 804ms + ✓ tests/spec-parity.test.ts (31 tests) 881ms + ✓ tests/helpers-fanout.test.ts (96 tests) 877ms + ✓ tests/worker-lease.test.ts (7 tests) 16ms + ✓ tests/typed-output.test.ts (14 tests) 535ms + ✓ tests/webhook-live.test.ts (3 tests) 7722ms + ✓ flows serve-webhook writes JSON before the daemon starts, then journals and archives exactly once 2284ms + ✓ replays a dropped file after SIGKILL before spawn 999ms + ✓ resumes the same journal after SIGKILL after spawn and before acknowledgement 4437ms + ✓ tests/effect-channel.test.ts (5 tests) 670ms + ✓ tests/mcp-lifecycle.test.ts (4 tests) 14ms + ✓ tests/model-selection.test.ts (10 tests) 39ms + ✓ tests/json-schema-bound.test.ts (71 tests) 7573ms + ✓ JSON Schema termination bound > walks a deep schema with an explicit stack rather than recursion 6347ms + ✓ tests/relayflowd-path.test.ts (10 tests) 11ms + ✓ tests/f-memory.test.ts (7 tests) 2782ms + ✓ reads the seeded local SQLite database with cloud unused and fallback disabled 561ms + ✓ cannot widen script scope using a raw project option or another flow name 397ms + ✓ attaches read helpers without journaling read steps 375ms + ✓ refuses missing, corrupt, and directory database paths before body or journal activity 363ms + ✓ detects direct memory use before running earlier body effects 348ms + ✓ maps an unavailable provider probe to memory_unreachable 359ms + ✓ fails closed for deferred writes, agent scope, disabled script memory, and reads after done 377ms + ✓ tests/webhook.test.ts (6 tests) 181ms + ✓ tests/yaml-local-agent-live.test.ts (7 tests) 9924ms + ✓ YAML --local-agent through the built CLI and real daemon > runs with the checked step CLI and model and journals done 1254ms + ✓ YAML --local-agent through the built CLI and real daemon > runs with the checked named CLI and model and journals done 1435ms + ✓ YAML --local-agent through the built CLI and real daemon > runs with the checked flow CLI and model and journals done 1398ms + ✓ YAML --local-agent through the built CLI and real daemon > runs with the checked project CLI and model and journals done 1681ms + ✓ YAML --local-agent through the built CLI and real daemon > still parks without --local-agent 1354ms + ✓ YAML --local-agent through the built CLI and real daemon > reports the agent process failure 1507ms + ✓ YAML --local-agent through the built CLI and real daemon > preserves declared workspace surfaces that the local worker cannot pin 1294ms + ✓ tests/local-dev-ux.test.ts (8 tests) 87ms + ✓ tests/dependency-validation.test.ts (6 tests) 1609ms + ✓ dependency validation > accepts a valid 10,000-step reverse chain through every direct public boundary 760ms + ✓ dependency validation > bounds the author-facing path for a 10,000-step cycle 767ms + ✓ tests/input-binding.test.ts (12 tests) 860ms + ✓ declarative output binding > compiles YAML selectors into durable input and implicit dependency edges 381ms + ✓ tests/deterministic-llm.test.ts (5 tests) 81ms + ✓ tests/bin.test.ts (7 tests) 3148ms + ✓ built flows binary > refuses through a symlink to the built artifact 516ms + ✓ built flows binary > refuses through a symlinked directory component 410ms + ✓ built flows binary > classifies a signal-terminated auth probe as probe_failed 378ms + ✓ built flows binary > classifies an unavailable PATH resolver as probe_failed 415ms + ✓ built flows binary > does not describe a present non-executable CLI as missing 503ms + ✓ built flows binary > runs one auth probe for three steps sharing a flow CLI 923ms + ✓ tests/scope-compiler.test.ts (25 tests) 38ms + ✓ tests/budget-attribution.test.ts (4 tests) 11ms + ✓ tests/hn-poller.test.ts (6 tests) 15ms + ✓ tests/dir-watcher-poller.test.ts (6 tests) 5ms + ✓ tests/hello-deterministic.test.ts (5 tests) 53ms + ✓ tests/budget-preflight.test.ts (16 tests) 26ms + ✓ tests/work-package-validator.test.ts (7 tests) 14ms + ✓ tests/classify-outcome.test.ts (2 tests) 2242ms + ✓ classifyOutcome > gives up and reports when a running run never becomes classifiable 2081ms + ↓ tests/real-cli-adapters.test.ts (3 tests | 3 skipped) + ✓ tests/model-pricing.test.ts (8 tests) 10ms + ✓ tests/authored-use-loader.test.ts (5 tests) 127ms + ✓ tests/memoization.test.ts (57 tests) 821ms + ✓ refuses invalid reuse invocation "run" 686ms + ✓ tests/parse-json-output.test.ts (7 tests) 8ms + ✓ tests/cli-adapter.test.ts (3 tests) 11ms + ✓ tests/journal-client-completion.test.ts (4 tests) 124ms + ✓ tests/worker-cli-abort.test.ts (2 tests) 2777ms + ✓ stops claude and its process group when lease ownership is lost 1397ms + ✓ stops wrapper.mjs and its process group when lease ownership is lost 1379ms + ✓ tests/direct-run-failure.test.ts (6 tests) 17ms + ✓ tests/budget-authored-live.test.ts (2 tests) 754ms + ✓ authored budgets through the live kernel > retains the first completion and journals refusal of the next authored step 365ms + ✓ authored budgets through the live kernel > permits done after the last valid step crosses the limit 387ms + ✓ tests/local-agent-live.test.ts (5 tests) 45262ms + ✓ built CLI local agent against a real daemon > dispatches through the wrapper and keeps --json stdout report-shaped 1983ms + ✓ built CLI local agent against a real daemon > runs beyond the initial 30-second lease without a second invocation 36925ms + ✓ built CLI local agent against a real daemon > renders actual agent completion in text output 2472ms + ✓ built CLI local agent against a real daemon > returns a failed run when the agent process fails 2334ms + ✓ built CLI local agent against a real daemon > refuses a workspace it cannot pin before invoking the agent 1545ms + ✓ tests/slack-writeback.test.ts (1 test) 271ms + ✓ tests/placement.test.ts (54 tests) 42ms + ✓ tests/memory.test.ts (18 tests) 19ms + ✓ tests/cli-progress-wait.test.ts (2 tests) 1368ms + ✓ run starts the wait clock on its first observed lease 866ms + ✓ resume starts the wait clock on its first observed lease 501ms + ✓ tests/worker-platform.test.ts (1 test) 7ms + +⎯⎯⎯⎯⎯⎯⎯ Failed Tests 2 ⎯⎯⎯⎯⎯⎯⎯ + + FAIL tests/cli-watch.test.ts > flows check --watch > streams JSON lines without ANSI, recovers after atomic saves, and exits zero after repair + FAIL tests/cli-watch.test.ts > flows check --watch > refreshes the import graph and notices missing imports being created +Error: Test timed out in 5000ms. +If this is a long-running test, pass a timeout value as the last argument or configure it globally with "testTimeout". +⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[1/2]⎯ + + Test Files 1 failed | 70 passed | 1 skipped (72) + Tests 2 failed | 1314 passed | 3 skipped (1319) + Start at 21:57:14 + Duration 210.15s (transform 2.98s, setup 0ms, collect 31.33s, tests 354.15s, environment 24ms, prepare 9.76s) + diff --git a/evidence/slice-S/sdk-tests.txt b/evidence/slice-S/sdk-tests.txt new file mode 100644 index 000000000..b0fdf0163 --- /dev/null +++ b/evidence/slice-S/sdk-tests.txt @@ -0,0 +1,513 @@ + +> @relayflows/sdk@2.0.8 test +> sh scripts/test.sh + + +> @relayflows/sdk@2.0.8 test:prep +> ( cd ../../kernel && sh ../ops/cargo.sh build ) && ( [ ! -d ../../testdata/preflight ] || find ../../testdata/preflight -name '*-cli' -type f -exec chmod +x {} + ) + + Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.68s + +> @relayflows/sdk@2.0.8 typecheck +> tsc --noEmit && tsc -p tsconfig.type-tests.json + + +> @relayflows/sdk@2.0.8 build +> tsc && node scripts/make-cli-executable.mjs + + +> @relayflows/sdk@2.0.8 typecheck:tests +> tsc -p tsconfig.tests.json + + + RUN v2.1.9 /Users/khaliqgant/fl-slice-S/packages/sdk + +stdout | tests/live-kernel.test.ts +LIVE_KERNEL relayflowd=/Users/khaliqgant/.relayflows-toolchain/target/2945819964/debug/relayflowd +LIVE_KERNEL flows=/Users/khaliqgant/fl-slice-S/packages/sdk/dist/cli.js + + ✓ tests/validate.test.ts (68 tests) 135ms + ❯ tests/preflight.test.ts (27 tests | 1 failed) 306ms + × preflight: CLI resolution and refusal predicates > reaches every declared refusal kind, with the converse held by the type 69ms + → expected Set{ …(17) } to deeply equal Set{ 'invalid_spec', …(18) } + ✓ tests/daemon-lifecycle.test.ts (42 tests) 231ms + ✓ tests/observer-link.test.ts (39 tests) 656ms + ✓ tests/authored-flow.test.ts (25 tests) 1041ms + ✓ tests/journal-client.test.ts (14 tests) 226ms + ✓ tests/tick-source.test.ts (33 tests) 208ms + ✓ tests/cloud-run.test.ts (47 tests) 412ms + ✓ tests/authored-flow-lifecycle-executor.test.ts (27 tests) 838ms + ✓ tests/verb-field-lint.test.ts (78 tests) 1530ms + ✓ closed per-verb step fields > preserves a valid v0.1.0 ladder with every declared per-verb field 361ms + ✓ tests/cli-hn-monitor.test.ts (16 tests) 170ms + ✓ tests/gate-contract.test.ts (20 tests) 666ms + ✓ tests/cli-replay.test.ts (36 tests) 4351ms + ✓ flows replay > --json is byte-identical across two CLI invocations (diff) 3296ms + ✓ tests/tick-runner.test.ts (22 tests) 9034ms + ✓ CLI argument parsing refuses coercion rather than accepting it > refuses --interval-ms fractional as an invocation error 1557ms + ✓ CLI argument parsing refuses coercion rather than accepting it > refuses --interval-ms exponent notation as an invocation error 1485ms + ✓ CLI argument parsing refuses coercion rather than accepting it > refuses --interval-ms hex as an invocation error 1623ms + ✓ CLI argument parsing refuses coercion rather than accepting it > refuses --interval-ms trailing text as an invocation error 1254ms + ✓ CLI argument parsing refuses coercion rather than accepting it > refuses --interval-ms empty as an invocation error 1393ms + ✓ CLI argument parsing refuses coercion rather than accepting it > accepts an exact integer and proceeds past parsing 1087ms + ❯ tests/mcp.test.ts (30 tests | 4 skipped) 14281ms + ✓ MCP preflight and transports > flows check refuses an undeclared server with exit 2 and no daemon 2153ms + ✓ MCP preflight and transports > flows check reports a refusing server and leaves no PID 1711ms + ✓ MCP preflight and transports > kills a SIGTERM-resistant silent child after a parent-owned handshake deadline 1316ms + ✓ MCP preflight and transports > reaps a SIGTERM-resistant descendant with inherit stdio before cleanup finishes 1498ms + ✓ MCP preflight and transports > reaps a SIGTERM-resistant descendant with ignore stdio before cleanup finishes 2069ms + ✓ MCP preflight and transports > reports malformed connection configuration as config_invalid 1435ms + ✓ tests/stop-process-group.test.ts (6 tests) 9161ms + ✓ every stop reaches the process group, not just the direct child > exits the run after an execution-timeout stop 1283ms + ✓ every stop reaches the process group, not just the direct child > exits the run after a protocol terminate stop 930ms + ✓ every stop reaches the process group, not just the direct child > kills a SIGTERM-deaf grandchild after a protocol terminate stop 2505ms + ✓ every stop reaches the process group, not just the direct child > kills a SIGTERM-deaf grandchild after an execution-timeout stop 2366ms + ✓ every stop reaches the process group, not just the direct child > holds the loop open long enough for the escalation to run 1528ms + ✓ every stop reaches the process group, not just the direct child > terminate() forces a group that outlives SIGTERM 547ms + ✓ tests/authored-flow-slack.test.ts (6 tests) 5065ms + ✓ authored Slack helper effects > journals exactly one effect with the authored params and typed receipt, without network 420ms + ✓ authored Slack helper effects > replays after SIGKILL before confirm with the same token and one successful completion 1713ms + ✓ authored Slack helper effects > replays after SIGKILL before complete with the same token and one successful completion 1598ms + ✓ authored Slack helper effects > writes two files for two calls and supports dm, reply, and react 1276ms + ✓ tests/backlog-picker.test.ts (14 tests) 282ms + ✓ tests/backlog-picker-flow.test.ts (6 tests) 2288ms + ✓ backlog-picker flow > emit-package describes the entry select-entry chose, even if the backlog changes between them 649ms + ✓ backlog-picker flow > does not emit a stale entry left by a previous run 1119ms + ✓ backlog-picker canonical spec > keeps directories and extensionless paths, and rejects prose 505ms + ✓ tests/cli.test.ts (63 tests) 17725ms + ✓ flows check CLI > binds a checked relative wrapper to the flow directory for worker execution 1004ms + ✓ flows check CLI > uses Codex login status and reports a rejected model as unavailable, not unauthenticated 549ms + ✓ flows check CLI > refuses a typo model before probing or contacting relayflowd 745ms + ✓ flows check CLI > passes all three canonical ladder flows and prints their resolved CLI 748ms + ✓ flows check CLI > resolves a project CLI path relative to the flows.json that declares it 494ms + ✓ flows check CLI > maps every input refusal path to its declared kind without raw exceptions 2005ms + ✓ flows run/resume CLI over the journal protocol > parses run options, submits the kernel dialect, and exits 0 on success 620ms + ✓ flows run/resume CLI over the journal protocol > exits 1 and emits the declared completionReason for a failed run 937ms + ✓ flows run/resume CLI over the journal protocol > exits 3 and names the parked llm step 1156ms + ✓ flows run/resume CLI over the journal protocol > reports a needs_human agent step as parked for human recovery 659ms + ✓ flows run/resume CLI over the journal protocol > classifies a typed hello refusal as a protocol error, not an unreachable daemon 600ms + ✓ flows run/resume CLI over the journal protocol > follows a dispatched worker step instead of reporting a protocol error 973ms + ✓ flows run/resume CLI over the journal protocol > bounds a worker wait by its lease and reports what it is waiting for 738ms + ✓ flows run/resume CLI over the journal protocol > resumes a parked run from snapshot step types without reading journal sequence one 693ms + ✓ flows run/resume CLI over the journal protocol > maps only run_not_found resumes to exit 2 1901ms + ✓ tests/authored-flow-operation.test.ts (23 tests) 1554ms + ✓ completes the gate in linear time over a body with 30000 ordinary awaits 1329ms + ✓ tests/worker-cli.test.ts (13 tests) 25059ms + ✓ custom wrapper execution identity > passes an explicit safe environment at identification and execution 534ms + ✓ custom wrapper execution identity > refuses a wrapper symlink retarget before delivering private values 475ms + ✓ custom wrapper execution identity > bounds wrapper execution after acknowledgement 1367ms + ✓ custom wrapper execution identity > bounds captured wrapper output 552ms + ✓ custom wrapper execution identity > refuses a duplicate execute protocol frame 575ms + ✓ custom wrapper execution bounds are reader-owned > resolves when a conforming wrapper leaks a stdio pipe to a background helper 2062ms + ✓ custom wrapper execution bounds are reader-owned > resolves when the leaked helper inherits stderr only 2044ms + ✓ custom wrapper execution bounds are reader-owned > resolves when a wrapper leaks a stdio pipe and exits before identifying 3261ms + ✓ custom wrapper execution bounds are reader-owned > journals a completionReason at the default bound when a wrapper leaks a stdio pipe 11272ms + ✓ custom wrapper execution bounds are reader-owned > accepts an execute token and an over-8KiB payload flushed in one write 546ms + ✓ custom wrapper execution bounds are reader-owned > accepts the same over-8KiB payload whether or not it coalesces with the execute token 1230ms + ✓ custom wrapper execution bounds are reader-owned > still bounds an un-terminated handshake buffer and names the bound 523ms + ✓ delivers the journaled memory pack to the real wrapper and excludes its charge from completion usage 611ms + ✓ tests/work-package-consumer.test.ts (13 tests) 1179ms + ✓ the Garden join: picker output feeds the consumer > accepts a package the picker actually emits, and refuses one lacking a definition of done 1141ms + ✓ tests/spec-parity.test.ts (31 tests) 1947ms + ✓ tests/helpers-fanout.test.ts (94 tests) 2578ms + ✓ tests/worker-lease.test.ts (7 tests) 69ms + ✓ tests/authored-helpers.test.ts (6 tests) 17292ms + ✓ lowers the named acceptance helpers and Slack to confirmed journal effects 1598ms + ✓ runs every available provider through the real kernel and resumes completed effects without a second write 10020ms + ✓ replays after SIGKILL before confirm with the same token and one successful completion 3046ms + ✓ replays after SIGKILL before complete with the same token and one successful completion 2005ms + ✓ journals provider failure with worker_error and never confirms the effect 610ms + ✓ tests/typed-output.test.ts (14 tests) 1231ms + ✓ typed llm and agent outputs > compiles llm output sugar to the existing json_schema primitive 343ms + ✓ tests/effect-channel.test.ts (5 tests) 960ms + ✓ tests/mcp-lifecycle.test.ts (4 tests) 53ms + ✓ tests/model-selection.test.ts (10 tests) 119ms + ✓ tests/bundle.test.ts (21 tests) 31809ms + ✓ immutable bundles > returns exit 2 naming a byte-flipped payload and refuses to reuse corruption 1492ms + ✓ immutable bundles > builds and verifies the canonical YAML fixture through the compiled CLI 3516ms + ✓ immutable bundles > emits the ephemeral warning on CLI stderr and uses the default output directory 3478ms + ✓ immutable bundles > preserves quoted asset words and executable permissions 328ms + ✓ immutable bundles > refuses build-provable CLI resolution errors without environment probes 2096ms + ✓ immutable bundles > builds a standalone TS fixture twice with identical executable hashes 10113ms + ✓ immutable bundles > refuses to label installed dependency drift with lockfile pins 1650ms + ✓ immutable bundles > refuses invalid CLI arguments %j 1523ms + ✓ immutable bundles > refuses invalid CLI arguments "--out" 1449ms + ✓ immutable bundles > refuses invalid CLI arguments "--verify" 2186ms + ✓ immutable bundles > refuses invalid CLI arguments "--verify" 1543ms + ✓ immutable bundles > refuses invalid CLI arguments "--out" 1910ms + ✓ tests/daemon-lifecycle-live.test.ts (9 tests) 32061ms + ✓ flows run against a data dir with no daemon (§6 test 7) > cold start spawns exactly one daemon, the run succeeds, and the daemon outlives the CLI 2355ms + ✓ flows run against a data dir with no daemon (§6 test 7) > polls, bounded, for a daemon that holds the lock before it binds 2742ms + ✓ flows run against a data dir with no daemon (§6 test 7) > attaches to a serving daemon that has not published a connection file 2448ms + ✓ flows run against a data dir with no daemon (§6 test 7) > a second run attaches to the daemon the first one started, spawning nothing 6032ms + ✓ flows run against a data dir with no daemon (§6 test 7) > detects a stale connection file left by a hard kill and starts a fresh daemon 7211ms + ✓ concurrent invocations against one empty data dir (§6 test 15) > ends with exactly one daemon owning the socket, and both runs succeed 2908ms + ✓ refusals from a spawn that cannot produce a daemon > names relayflowd_not_found rather than falling through to PATH 2356ms + ✓ refusals from a spawn that cannot produce a daemon > names daemon_start_failed and quotes the daemon log when startup dies 3116ms + ✓ refusals from a spawn that cannot produce a daemon > refuses a daemon speaking another protocol version instead of binding over it 2887ms + ✓ tests/relayflowd-path.test.ts (10 tests) 15ms + ✓ tests/direct-input.test.ts (4 tests) 25404ms + ✓ direct .flow.ts input through the built CLI and live runtime > executes inline and file JSON input through relayflowd 9848ms + ✓ direct .flow.ts input through the built CLI and live runtime > refuses missing and malformed input before contacting relayflowd 10133ms + ✓ direct .flow.ts input through the built CLI and live runtime > does not run the authored body before daemon availability 2889ms + ✓ direct .flow.ts input through the built CLI and live runtime > refuses oversized file input before contacting relayflowd 2527ms + ✓ tests/webhook.test.ts (6 tests) 427ms + ❯ tests/f-memory.test.ts (7 tests | 3 failed) 4753ms + ✓ reads the seeded local SQLite database with cloud unused and fallback disabled 977ms + ✓ cannot widen script scope using a raw project option or another flow name 589ms + × attaches read helpers without journaling read steps 726ms + → helper_provider.mount_required: f.recall requires a relayfile recall mount; direct-token transport is not implemented. + ✓ refuses missing, corrupt, and directory database paths before body or journal activity 584ms + × detects direct memory use before running earlier body effects 635ms + → expected AuthoredFlowExecutionError: helper_provid… { …(3) } to match object { code: 'memory_unreachable' } +(3 matching properties omitted from actual) + ✓ maps an unavailable provider probe to memory_unreachable 687ms + × fails closed for deferred writes, agent scope, disabled script memory, and reads after done 552ms + → expected AuthoredFlowExecutionError: helper_provid… { …(3) } to match object { code: 'operation_after_completion' } +(3 matching properties omitted from actual) + ✓ tests/local-dev-ux.test.ts (8 tests) 129ms + ✓ tests/json-schema-bound.test.ts (71 tests) 14395ms + ✓ JSON Schema termination bound > walks a deep schema with an explicit stack rather than recursion 12244ms + ✓ tests/flow-executor-chain.test.ts (12 tests) 30965ms + ✓ flow executor LLM and output-binding chain > runs f.llm -> f.agent -> f.run with schema-verified journal output and the exact allowed model 3404ms + ✓ flow executor LLM and output-binding chain > fails invalid LLM output before the next step: not JSON 2282ms + ✓ flow executor LLM and output-binding chain > fails invalid LLM output before the next step: {"message":7} 2539ms + ✓ flow executor LLM and output-binding chain > retains tagged-template text output 2036ms + ✓ flow executor LLM and output-binding chain > preserves JSON values without promoting them to process wrappers: null 2417ms + ✓ flow executor LLM and output-binding chain > preserves JSON values without promoting them to process wrappers: [1,2] 1931ms + ✓ flow executor LLM and output-binding chain > preserves JSON values without promoting them to process wrappers: "hello" 1996ms + ✓ flow executor LLM and output-binding chain > refuses a model outside flows.json before a run or adapter execution 452ms + ✓ flow executor LLM and output-binding chain > runs the authored LLM path through the built flows CLI 4610ms + ✓ flow executor LLM and output-binding chain > passes a declarative verified value through an agent into a deterministic artifact 2725ms + ✓ flow executor LLM and output-binding chain > journals a missing optional field as a failure before the consuming command executes 1918ms + ✓ flow executor LLM and output-binding chain > flows run consumes YAML bindings and resume reuses the original journal output 4651ms + ✓ tests/deterministic-llm.test.ts (5 tests) 283ms + ✓ tests/input-binding.test.ts (12 tests) 1274ms + ✓ declarative output binding > compiles YAML selectors into durable input and implicit dependency edges 331ms + ✓ tests/scope-compiler.test.ts (25 tests) 51ms + ✓ tests/hn-poller.test.ts (6 tests) 31ms + ✓ tests/budget-attribution.test.ts (4 tests) 16ms + ✓ tests/dependency-validation.test.ts (6 tests) 2709ms + ✓ dependency validation > accepts a valid 10,000-step reverse chain through every direct public boundary 1593ms + ✓ dependency validation > bounds the author-facing path for a 10,000-step cycle 944ms + ✓ tests/dir-watcher-poller.test.ts (6 tests) 34ms + ✓ tests/hello-deterministic.test.ts (5 tests) 85ms + ✓ tests/work-package-validator.test.ts (7 tests) 15ms + ✓ tests/budget-preflight.test.ts (16 tests) 50ms + ✓ tests/model-pricing.test.ts (8 tests) 19ms + ↓ tests/real-cli-adapters.test.ts (3 tests | 3 skipped) + ✓ tests/authored-use-loader.test.ts (5 tests) 200ms + ✓ tests/parse-json-output.test.ts (7 tests) 11ms + ✓ tests/cli-adapter.test.ts (3 tests) 14ms + ✓ tests/classify-outcome.test.ts (2 tests) 2260ms + ✓ classifyOutcome > gives up and reports when a running run never becomes classifiable 2094ms + ✓ tests/memoization.test.ts (57 tests) 883ms + ✓ refuses invalid reuse invocation "run" 634ms + ✓ tests/journal-client-completion.test.ts (4 tests) 133ms + ✓ tests/bin.test.ts (7 tests) 8330ms + ✓ built flows binary > refuses through a symlink to the built artifact 1466ms + ✓ built flows binary > refuses through a symlinked directory component 1508ms + ✓ built flows binary > classifies a signal-terminated auth probe as probe_failed 1866ms + ✓ built flows binary > classifies an unavailable PATH resolver as probe_failed 910ms + ✓ built flows binary > does not describe a present non-executable CLI as missing 1100ms + ✓ built flows binary > runs one auth probe for three steps sharing a flow CLI 1469ms + ✓ tests/direct-run-failure.test.ts (6 tests) 45ms + ✓ tests/worker-cli-abort.test.ts (2 tests) 3045ms + ✓ stops claude and its process group when lease ownership is lost 1453ms + ✓ stops wrapper.mjs and its process group when lease ownership is lost 1590ms + ✓ tests/budget-authored-live.test.ts (2 tests) 1513ms + ✓ authored budgets through the live kernel > retains the first completion and journals refusal of the next authored step 765ms + ✓ authored budgets through the live kernel > permits done after the last valid step crosses the limit 745ms + ✓ tests/slack-writeback.test.ts (1 test) 291ms + ✓ tests/memory.test.ts (18 tests) 25ms + ✓ tests/placement.test.ts (54 tests) 57ms + ✓ tests/worker-platform.test.ts (1 test) 11ms + ✓ tests/cli-progress-wait.test.ts (2 tests) 1248ms + ✓ run starts the wait clock on its first observed lease 752ms + ✓ resume starts the wait clock on its first observed lease 494ms + ✓ tests/yaml-local-agent-live.test.ts (7 tests) 15889ms + ✓ YAML --local-agent through the built CLI and real daemon > runs with the checked step CLI and model and journals done 2363ms + ✓ YAML --local-agent through the built CLI and real daemon > runs with the checked named CLI and model and journals done 2667ms + ✓ YAML --local-agent through the built CLI and real daemon > runs with the checked flow CLI and model and journals done 2658ms + ✓ YAML --local-agent through the built CLI and real daemon > runs with the checked project CLI and model and journals done 1740ms + ✓ YAML --local-agent through the built CLI and real daemon > still parks without --local-agent 1829ms + ✓ YAML --local-agent through the built CLI and real daemon > reports the agent process failure 2848ms + ✓ YAML --local-agent through the built CLI and real daemon > preserves declared workspace surfaces that the local worker cannot pin 1781ms + ❯ tests/webhook-live.test.ts (3 tests | 3 failed) 35448ms + × flows serve-webhook writes JSON before the daemon starts, then journals and archives exactly once 11955ms + → webhook integration timed out: spawn /Users/khaliqgant/fl-slice-S/kernel/target/debug/relayflowd ENOENT + × replays a dropped file after SIGKILL before spawn 11895ms + → webhook integration timed out: spawn /Users/khaliqgant/fl-slice-S/kernel/target/debug/relayflowd ENOENT + × resumes the same journal after SIGKILL after spawn and before acknowledgement 11593ms + → webhook integration timed out: spawn /Users/khaliqgant/fl-slice-S/kernel/target/debug/relayflowd ENOENT + ❯ tests/cli-watch.test.ts (10 tests | 7 failed) 46106ms + × flows check --watch > rechecks syntax errors, clears once, and returns the last refusal on Ctrl-C 7383ms + → Test timed out in 5000ms. +If this is a long-running test, pass a timeout value as the last argument or configure it globally with "testTimeout". + × flows check --watch > streams JSON lines without ANSI, recovers after atomic saves, and exits zero after repair 5628ms + → Test timed out in 5000ms. +If this is a long-running test, pass a timeout value as the last argument or configure it globally with "testTimeout". + × flows check --watch > coalesces 20 concurrent saves into at most two rechecks 6078ms + → Test timed out in 5000ms. +If this is a long-running test, pass a timeout value as the last argument or configure it globally with "testTimeout". + × flows check --watch > watches transitive relative use imports, cycles, and nearest config changes 5633ms + → Test timed out in 5000ms. +If this is a long-running test, pass a timeout value as the last argument or configure it globally with "testTimeout". + × flows check --watch > refreshes the import graph and notices missing imports being created 5063ms + → Test timed out in 5000ms. +If this is a long-running test, pass a timeout value as the last argument or configure it globally with "testTimeout". + × flows check --watch > reloads authored TypeScript instead of reusing the first imported definition 5130ms + → Test timed out in 5000ms. +If this is a long-running test, pass a timeout value as the last argument or configure it globally with "testTimeout". + × flows check --watch > detects a nearer config appearing and falls back after it is deleted 5479ms + → Test timed out in 5000ms. +If this is a long-running test, pass a timeout value as the last argument or configure it globally with "testTimeout". + ✓ flows check --watch > keeps watching after the target is deleted and recreated 4895ms + ✓ flows check --watch > queues changes during a slow check without overlapping checks 799ms +stdout | tests/live-kernel.test.ts > built flows CLI against live relayflowd > hn-monitor analyze-story reaches done through the real Claude analyzer CLI +LIVE_ANALYZER ready: claude -p --model claude-haiku-4-5-20251001 round-trip OK + +stdout | tests/live-kernel.test.ts > built flows CLI against live relayflowd > hn-monitor analyze-story reaches done through the real Claude analyzer CLI +LIVE_ANALYZER analysis: {"reasoning":"This story is directly relevant to AI agents and automation as it describes an autonomous agent performing software development workflows including opening and reviewing pull requests, exemplifying practical agent-driven automation in development operations.","relevance_score":9,"story_title":"Show HN: an agent that opens and reviews its own pull requests [wake-nonce-7f3a91c4]"} + + ✓ tests/local-agent-live.test.ts (5 tests) 46375ms + ✓ built CLI local agent against a real daemon > dispatches through the wrapper and keeps --json stdout report-shaped 3445ms + ✓ built CLI local agent against a real daemon > runs beyond the initial 30-second lease without a second invocation 37410ms + ✓ built CLI local agent against a real daemon > renders actual agent completion in text output 2066ms + ✓ built CLI local agent against a real daemon > returns a failed run when the agent process fails 2027ms + ✓ built CLI local agent against a real daemon > refuses a workspace it cannot pin before invoking the agent 1423ms +stdout | tests/live-kernel.test.ts > surface resume after a real daemon kill > resumes a three-step run with each successful completion exactly once +LIVE_KERNEL kill -9 pid=44112 run=01M29084XKR3WMXDY5FDX3Z49H while step=two state=Running + + ❯ tests/live-kernel.test.ts (31 tests | 2 failed) 95283ms + × built flows CLI against live relayflowd > twenty-six-step reuses 25 durable completions after editing the failed final step 8531ms + → Test timed out in 5000ms. +If this is a long-running test, pass a timeout value as the last argument or configure it globally with "testTimeout". + ✓ built flows CLI against live relayflowd > runs rung (a), parks rung (b), and keeps JSON report-shaped 12969ms + ✓ built flows CLI against live relayflowd > allows a deterministic run to exceed the bounded request timeout 35120ms + ✓ built flows CLI against live relayflowd > follows a live worker dispatch through flows run 2669ms + ✓ built flows CLI against live relayflowd > runs an agent CLI end to end through the SDK worker 706ms + ✓ built flows CLI against live relayflowd > f.agent lowers to a real agent step and dispatches through a live worker 986ms + ✓ built flows CLI against live relayflowd > f.agent's default flowPath anchors on cwd, not cwd's parent 1028ms + ✓ built flows CLI against live relayflowd > can always get a parked run to a late-attaching worker 5683ms + ✓ built flows CLI against live relayflowd > reports a real manual-recovery NeedsHuman state as parked 1494ms + ✓ built flows CLI against live relayflowd > runs hn-monitor analyze-story end-to-end via a stub agent CLI (gate 2 clause 2 demo) 616ms + ✓ built flows CLI against live relayflowd > hn-monitor analyze-story FAILS verification when the CLI omits required schema fields 657ms + ✓ built flows CLI against live relayflowd > agent step preserves the CliResult wrapper as output when the CLI emits non-JSON text 528ms + ✓ built flows CLI against live relayflowd > AgentWorker exposes wake_context to the CLI via RELAYFLOW_WAKE_CONTEXT env var (real analyzer prerequisite) 588ms + ✓ built flows CLI against live relayflowd > AgentWorker leaves RELAYFLOW_WAKE_CONTEXT UNSET when the run has no wake_context (undefined-vs-null pin) 479ms + ✓ built flows CLI against live relayflowd > AgentWorker passes a declared model to an identified wrapper as RELAYFLOW_MODEL 795ms + ✓ built flows CLI against live relayflowd > AgentWorker refuses a nonconforming journal-submitted wrapper before exposing RELAYFLOW_MODEL 517ms + ✓ built flows CLI against live relayflowd > AgentWorker executes the raw claude adapter with its real model flag 350ms + ✓ built flows CLI against live relayflowd > AgentWorker executes the raw codex adapter with its real model flag 340ms + ✓ built flows CLI against live relayflowd > AgentWorker leaves RELAYFLOW_MODEL UNSET when the step declares no model 342ms + ✓ built flows CLI against live relayflowd > hn-monitor analyze-story reaches done through the real Claude analyzer CLI 11871ms + ✓ built flows CLI against live relayflowd > preflights before journaling and names an unreachable socket 2788ms + × built flows CLI against live relayflowd > starts exactly one daemon when two runs race for one empty data dir 1531ms + → WARNING [unprovable_effects] Step "greet" command "echo" resolves, but its effects cannot be proven before execution. +WARNING [unprovable_effects] Step "shout" command "echo" resolves, but its effects cannot be proven before execution. +WARNING [editor_schema_missing] For editor validation, add this first line: # yaml-language-server: $schema=https://schema.relayflows.dev/v0.1/flows.schema.json +REFUSED [relayflowd_not_found] No relayflowd binary could be found. Install the runtime package for this host (@relayflows/runtime-darwin-arm64), or set RELAYFLOWD_BIN to a relayflowd executable. Tried: /Users/khaliqgant/fl-slice-S/packages/sdk/dist/relayflowd. +: expected 2 to be +0 // Object.is equality + ✓ surface resume after a real daemon kill > resumes a three-step run with each successful completion exactly once 2851ms + ✓ a relayflow can be scheduled: tick source against live relayflowd > a tick spawns a real run whose step reports the SCHEDULED instant 603ms + +⎯⎯⎯⎯⎯⎯ Failed Suites 1 ⎯⎯⎯⎯⎯⎯⎯ + + FAIL tests/mcp.test.ts > authored MCP effects against the real kernel +Error: journal client: connect failed: connect ENOENT /var/folders/yv/nbp9l2c55wlbj1x0gml37s7c0000gn/T/relayflowd-edbd934b342d.sock + ❯ Socket.onError src/journal-client.ts:99:16 + 97| socket.removeAllListeners(); + 98| this.failAll(err); + 99| reject(new Error(`journal client: connect failed: ${err.messag… + | ^ + 100| }; + 101| socket.once('error', onError); + +⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[1/17]⎯ + +⎯⎯⎯⎯⎯⎯ Failed Tests 16 ⎯⎯⎯⎯⎯⎯⎯ + + FAIL tests/cli-watch.test.ts > flows check --watch > rechecks syntax errors, clears once, and returns the last refusal on Ctrl-C + FAIL tests/cli-watch.test.ts > flows check --watch > streams JSON lines without ANSI, recovers after atomic saves, and exits zero after repair + FAIL tests/cli-watch.test.ts > flows check --watch > coalesces 20 concurrent saves into at most two rechecks + FAIL tests/cli-watch.test.ts > flows check --watch > watches transitive relative use imports, cycles, and nearest config changes + FAIL tests/cli-watch.test.ts > flows check --watch > refreshes the import graph and notices missing imports being created + FAIL tests/cli-watch.test.ts > flows check --watch > reloads authored TypeScript instead of reusing the first imported definition + FAIL tests/cli-watch.test.ts > flows check --watch > detects a nearer config appearing and falls back after it is deleted + FAIL tests/live-kernel.test.ts > built flows CLI against live relayflowd > twenty-six-step reuses 25 durable completions after editing the failed final step +Error: Test timed out in 5000ms. +If this is a long-running test, pass a timeout value as the last argument or configure it globally with "testTimeout". +⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[2/17]⎯ + + FAIL tests/f-memory.test.ts > attaches read helpers without journaling read steps +AuthoredFlowExecutionError: helper_provider.mount_required: f.recall requires a relayfile recall mount; direct-token transport is not implemented. + ❯ Module.executeAuthoredFlow src/authored-flow-executor.ts:145:11 + 143| if (!helperPreflight.ok) { + 144| const diagnostic = helperPreflight.diagnostics[0]!; + 145| throw new AuthoredFlowExecutionError(diagnostic.kind as AuthoredFl… + | ^ + 146| } + 147| if (headerFields.length > 0) { + ❯ tests/f-memory.test.ts:62:24 + +⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[3/17]⎯ + + FAIL tests/f-memory.test.ts > detects direct memory use before running earlier body effects +AssertionError: expected AuthoredFlowExecutionError: helper_provid… { …(3) } to match object { code: 'memory_unreachable' } +(3 matching properties omitted from actual) + +- Expected ++ Received + +- Object { +- "code": "memory_unreachable", ++ AuthoredFlowExecutionError { ++ "code": "helper_provider.mount_required", + } + +⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[4/17]⎯ + + FAIL tests/f-memory.test.ts > fails closed for deferred writes, agent scope, disabled script memory, and reads after done +AssertionError: expected AuthoredFlowExecutionError: helper_provid… { …(3) } to match object { code: 'operation_after_completion' } +(3 matching properties omitted from actual) + +- Expected ++ Received + +- Object { +- "code": "operation_after_completion", ++ AuthoredFlowExecutionError { ++ "code": "helper_provider.mount_required", + } + +⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[5/17]⎯ + + FAIL tests/live-kernel.test.ts > built flows CLI against live relayflowd > starts exactly one daemon when two runs race for one empty data dir +AssertionError: WARNING [unprovable_effects] Step "greet" command "echo" resolves, but its effects cannot be proven before execution. +WARNING [unprovable_effects] Step "shout" command "echo" resolves, but its effects cannot be proven before execution. +WARNING [editor_schema_missing] For editor validation, add this first line: # yaml-language-server: $schema=https://schema.relayflows.dev/v0.1/flows.schema.json +REFUSED [relayflowd_not_found] No relayflowd binary could be found. Install the runtime package for this host (@relayflows/runtime-darwin-arm64), or set RELAYFLOWD_BIN to a relayflowd executable. Tried: /Users/khaliqgant/fl-slice-S/packages/sdk/dist/relayflowd. +: expected 2 to be +0 // Object.is equality + +- Expected ++ Received + +- 0 ++ 2 + + ❯ tests/live-kernel.test.ts:1388:40 + 1386| ]); + 1387| + 1388| expect(first.status, first.stderr).toBe(0); + | ^ + 1389| expect(second.status, second.stderr).toBe(0); + 1390| expect(first.stdout).toContain('completionReason: success'); + +⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[6/17]⎯ + + FAIL tests/preflight.test.ts > preflight: CLI resolution and refusal predicates > reaches every declared refusal kind, with the converse held by the type +AssertionError: expected Set{ …(17) } to deeply equal Set{ 'invalid_spec', …(18) } + +- Expected ++ Received + + Set { + "budget_missing_price", + "budget_syntax_invalid", + "cli_missing", + "cli_unauthenticated", + "cli_unresolved", + "cli_unsupported", + "command_missing", +- "helper_provider.mount_required", +- "helper_provider.unsupported", + "helper_slack.credential_missing", + "helper_slack.mount_required", + "invalid_spec", + "mcp_undeclared_server", + "mcp_unreachable", + "memory_unreachable", + "model_unavailable", + "model_unknown", + "no_executor", + "probe_failed", + } + + ❯ tests/preflight.test.ts:398:35 + 396| refusalKinds.push(memoryRefusal!.kind); + 397| expect(JSON.stringify(memoryRefusal)).not.toContain('raw secret'); + 398| expect(new Set(refusalKinds)).toEqual(new Set(PREFLIGHT_FAILURE_KI… + | ^ + 399| expect(JSON.stringify(scenarios)).not.toContain('raw secret'); + 400| }); + +⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[7/17]⎯ + + FAIL tests/webhook-live.test.ts > flows serve-webhook writes JSON before the daemon starts, then journals and archives exactly once +Error: webhook integration timed out: spawn /Users/khaliqgant/fl-slice-S/kernel/target/debug/relayflowd ENOENT + ❯ until tests/webhook-live.test.ts:36:9 + 34| const deadline = Date.now() + 10_000; + 35| while (Date.now() < deadline) { if (await predicate()) return; await… + 36| throw new Error(`webhook integration timed out: ${detail()}`); + | ^ + 37| } + 38| async function daemon(dir: string): Promise { + ❯ daemon tests/webhook-live.test.ts:40:3 + ❯ tests/webhook-live.test.ts:82:3 + +⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[8/17]⎯ + + FAIL tests/webhook-live.test.ts > replays a dropped file after SIGKILL before spawn +Error: webhook integration timed out: spawn /Users/khaliqgant/fl-slice-S/kernel/target/debug/relayflowd ENOENT + ❯ until tests/webhook-live.test.ts:36:9 + 34| const deadline = Date.now() + 10_000; + 35| while (Date.now() < deadline) { if (await predicate()) return; await… + 36| throw new Error(`webhook integration timed out: ${detail()}`); + | ^ + 37| } + 38| async function daemon(dir: string): Promise { + ❯ daemon tests/webhook-live.test.ts:40:3 + ❯ tests/webhook-live.test.ts:98:17 + +⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[9/17]⎯ + + FAIL tests/webhook-live.test.ts > resumes the same journal after SIGKILL after spawn and before acknowledgement +Error: webhook integration timed out: spawn /Users/khaliqgant/fl-slice-S/kernel/target/debug/relayflowd ENOENT + ❯ until tests/webhook-live.test.ts:36:9 + 34| const deadline = Date.now() + 10_000; + 35| while (Date.now() < deadline) { if (await predicate()) return; await… + 36| throw new Error(`webhook integration timed out: ${detail()}`); + | ^ + 37| } + 38| async function daemon(dir: string): Promise { + ❯ daemon tests/webhook-live.test.ts:40:3 + ❯ tests/webhook-live.test.ts:111:17 + +⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[10/17]⎯ + +⎯⎯⎯⎯⎯⎯ Unhandled Errors ⎯⎯⎯⎯⎯⎯ + +Vitest caught 1 unhandled error during the test run. +This might cause false positive tests. Resolve unhandled errors to make sure your tests are not affected. + +⎯⎯⎯⎯⎯ Uncaught Exception ⎯⎯⎯⎯⎯ +Error: spawn /Users/khaliqgant/fl-slice-S/kernel/target/release/relayflowd ENOENT + ❯ Process.ChildProcess._handle.onexit node:internal/child_process:286:19 + ❯ onErrorNT node:internal/child_process:524:16 + ❯ processTicksAndRejections node:internal/process/task_queues:90:21 + +⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯ +Serialized Error: { errno: -2, code: 'ENOENT', syscall: 'spawn /Users/khaliqgant/fl-slice-S/kernel/target/release/relayflowd', path: '/Users/khaliqgant/fl-slice-S/kernel/target/release/relayflowd', spawnargs: [ '--data-dir', '/var/folders/yv/nbp9l2c55wlbj1x0gml37s7c0000gn/T/flows-mcp-daemon-l2Zu58', 'serve' ] } +This error originated in "tests/mcp.test.ts" test file. It doesn't mean the error was thrown inside the file itself, but while it was running. +The latest test that might've caused the error is "authored MCP effects against the real kernel". It might mean one of the following: +- The error was thrown, while Vitest was running this test. +- If the error occurred after the test had been completed, this was the last documented test before it was thrown. +⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯ + + Test Files 6 failed | 65 passed | 1 skipped (72) + Tests 16 failed | 1294 passed | 7 skipped (1317) + Errors 1 error + Start at 21:47:18 + Duration 101.03s (transform 9.25s, setup 0ms, collect 84.94s, tests 522.34s, environment 44ms, prepare 17.39s) + diff --git a/evidence/slice-S/sdk-watch-retry.txt b/evidence/slice-S/sdk-watch-retry.txt new file mode 100644 index 000000000..720aadd44 --- /dev/null +++ b/evidence/slice-S/sdk-watch-retry.txt @@ -0,0 +1,19 @@ + + RUN v2.1.9 /Users/khaliqgant/fl-slice-S/packages/sdk + + ✓ tests/cli-watch.test.ts (10 tests) 28592ms + ✓ flows check --watch > rechecks syntax errors, clears once, and returns the last refusal on Ctrl-C 2560ms + ✓ flows check --watch > streams JSON lines without ANSI, recovers after atomic saves, and exits zero after repair 3579ms + ✓ flows check --watch > coalesces 20 concurrent saves into at most two rechecks 2996ms + ✓ flows check --watch > watches transitive relative use imports, cycles, and nearest config changes 4517ms + ✓ flows check --watch > refreshes the import graph and notices missing imports being created 4436ms + ✓ flows check --watch > reloads authored TypeScript instead of reusing the first imported definition 2660ms + ✓ flows check --watch > detects a nearer config appearing and falls back after it is deleted 3600ms + ✓ flows check --watch > keeps watching after the target is deleted and recreated 3442ms + ✓ flows check --watch > queues changes during a slow check without overlapping checks 796ms + + Test Files 1 passed (1) + Tests 10 passed (10) + Start at 22:02:45 + Duration 31.19s (transform 1.07s, setup 0ms, collect 2.14s, tests 28.59s, environment 0ms, prepare 127ms) + diff --git a/evidence/slice-S/surface-regressions.txt b/evidence/slice-S/surface-regressions.txt new file mode 100644 index 000000000..a8f1eede4 --- /dev/null +++ b/evidence/slice-S/surface-regressions.txt @@ -0,0 +1,5 @@ + +> @relayflows/surface@2.0.8 typecheck:regressions +> tsc -p ../../regressions/tsconfig.json && tsc -p tsconfig.test.json && node scripts/check-generated-helpers.mjs + +HELPERS_GENERATED_OK airtable.ts, asana.ts, azure-blob.ts, box.ts, calendly.ts, clickup.ts, clients.ts, cloudflare.ts, confluence.ts, daytona.ts, docker-hub.ts, dropbox.ts, fathom.ts, gcp.ts, gcs.ts, github.ts, gitlab.ts, gmail.ts, google-calendar.ts, google-drive.ts, granola.ts, hubspot.ts, index.ts, intercom.ts, jira.ts, linear.ts, mailgun.ts, mixpanel.ts, neon.ts, notion.ts, onedrive.ts, pipedrive.ts, postgres.ts, posthog.ts, providers.ts, ramp.ts, recall.ts, reddit.ts, redis.ts, s3.ts, salesforce.ts, segment.ts, sendgrid.ts, sharepoint.ts, shopify.ts, shortcut.ts, slack.ts, stripe.ts, teams.ts, telegram.ts, webhook-server.ts, x.ts, zendesk.ts diff --git a/evidence/slice-S/surface-tests.txt b/evidence/slice-S/surface-tests.txt new file mode 100644 index 000000000..ae23729b3 --- /dev/null +++ b/evidence/slice-S/surface-tests.txt @@ -0,0 +1,18 @@ + +> @relayflows/surface@2.0.8 test +> bun run build && tsc -p tsconfig.test.json && vitest run + +$ tsc + + RUN v2.1.9 /Users/khaliqgant/fl-slice-S/packages/surface + + ✓ tests/triggers.test.ts (4 tests) 21ms + ✓ tests/flow.test.ts (20 tests) 26ms + ✓ tests/helpers.snapshot.test.ts (1 test) 994ms + ✓ regenerates helpers byte-identically from the pinned adapter 993ms + + Test Files 3 passed (3) + Tests 25 passed (25) + Start at 21:56:57 + Duration 2.09s (transform 474ms, setup 0ms, collect 1.41s, tests 1.04s, environment 1ms, prepare 747ms) + diff --git a/packages/sdk/src/authored-flow-error.ts b/packages/sdk/src/authored-flow-error.ts index 664f0d071..7f38eef25 100644 --- a/packages/sdk/src/authored-flow-error.ts +++ b/packages/sdk/src/authored-flow-error.ts @@ -4,6 +4,8 @@ import type { } from './protocol.js'; export type AuthoredFlowExecutionErrorCode = + | 'helper_provider.mount_required' + | 'helper_provider.unsupported' | 'helper_slack.credential_missing' | 'helper_slack.mount_required' | 'budget_syntax_invalid' diff --git a/packages/sdk/src/authored-flow-executor.ts b/packages/sdk/src/authored-flow-executor.ts index 9a4cfc034..f04b868b8 100644 --- a/packages/sdk/src/authored-flow-executor.ts +++ b/packages/sdk/src/authored-flow-executor.ts @@ -2,7 +2,7 @@ import { pluginHelpers } from './plugin-loader.js'; import { runPluginEffect } from './authored-plugin-effect.js'; import { randomUUID } from 'node:crypto'; import { dirname } from 'node:path'; -import { assertSlackCredentials, runSlackEffect } from './authored-slack-effect.js'; +import { runHelperEffect } from './authored-helper-effect.js'; import { checkSlackHelpers } from './slack-preflight.js'; import { snapshotJsonValue } from './json-value.js'; import type { SlackCall } from './slack-writeback.js'; @@ -21,7 +21,7 @@ import { type RunCompletionReason as SurfaceRunCompletionReason, type Step, } from '@relayflows/surface'; -import type { FlowHandle } from '@relayflows/surface/runtime'; +import { createHelpers, helperProviders, type HelperCall, type FlowHandle } from '@relayflows/surface/runtime'; import { join } from 'node:path'; import { observeStep, type ProgressEvent } from './progress.js'; import { parseStepTimeout } from './compile.js'; @@ -139,10 +139,13 @@ export async function executeAuthoredFlow( }; const definition = getDefinition(handle); const headerFields = Object.keys(definition.header).filter(key => key !== 'tools' && key !== 'budget' && key !== 'memory'); - if (definition.header.tools && Object.keys(definition.header.tools).some(key => !['slack', 'mcp'].includes(key))) headerFields.push('tools'); + if (definition.header.tools && Object.keys(definition.header.tools).some(key => !['mcp', ...helperProviders.map(p => p.namespace)].includes(key))) headerFields.push('tools'); if (definition.header.tools?.relayfile !== undefined) headerFields.push('tools.relayfile'); const helperPreflight = checkSlackHelpers(definition); - if (!helperPreflight.ok) assertSlackCredentials(); + if (!helperPreflight.ok) { + const diagnostic = helperPreflight.diagnostics[0]!; + throw new AuthoredFlowExecutionError(diagnostic.kind as AuthoredFlowExecutionErrorCode, diagnostic.message); + } if (headerFields.length > 0) { throw new AuthoredFlowExecutionError( 'unsupported_header', @@ -205,7 +208,7 @@ export async function executeAuthoredFlow( id, `slack.${call.verb}`, () => assertOperationAllowed(`slack.${call.verb}`, definition.name, requestedCompletion), async () => { - const receipt = await runSlackEffect(journal, definition.name, id, snapshot, + const receipt = await runHelperEffect(journal, definition.name, id, snapshot, options.dataDir ?? dirname(journal.socketPath), journalSteps); return (call.verb === 'react' ? undefined : receipt) as T; }, @@ -214,6 +217,17 @@ export async function executeAuthoredFlow( } const context: Ctx = { + ...createHelpers((call: HelperCall): Step => { + const verb = `${call.provider}.${call.verb}`; + assertOperationAllowed(verb, definition.name, requestedCompletion); + const snapshot = snapshotJsonValue(call, `f.${verb} call`) as unknown as HelperCall; + const id = `${call.provider}-${slackRun}-${nextStep++}`; + return trackStep(authoredSteps, new AuthoredFlowOperation(id, verb, + () => assertOperationAllowed(verb, definition.name, requestedCompletion), + async () => await runHelperEffect(journal, definition.name, id, snapshot, + options.dataDir ?? dirname(journal.socketPath), journalSteps) as T, + lifecycle)); + }), slack: { post: (channel, text, opts) => slackOperation({ type: 'effect', provider: 'slack', verb: 'post', params: { channel, text, ...(opts === undefined ? {} : { opts }) } }), dm: (user, text) => slackOperation({ type: 'effect', provider: 'slack', verb: 'dm', params: { user, text } }), diff --git a/packages/sdk/src/authored-helper-effect.ts b/packages/sdk/src/authored-helper-effect.ts new file mode 100644 index 000000000..22f84d9db --- /dev/null +++ b/packages/sdk/src/authored-helper-effect.ts @@ -0,0 +1,143 @@ +import { existsSync } from 'node:fs'; +import { join } from 'node:path'; +import { randomUUID } from 'node:crypto'; +import { JournalClient } from './journal-client.js'; +import { AuthoredFlowExecutionError, type AuthoredFlowExecutionErrorCode } from './authored-flow-error.js'; +import { readCompletedStepOutput } from './authored-step-output.js'; +import type { AuthoredFlowJournalStep } from './authored-flow-executor.js'; +import { compileSpec, toKernelSpec } from './compile.js'; +import { SPEC_SCHEMA_VERSION, type KernelAgentStep } from './spec.js'; +import type { StepDispatchEvent } from './protocol.js'; +import { withWorkerLease } from './worker-lease.js'; +import { helperProviders, type HelperCall } from '@relayflows/surface/runtime'; +import { helperWriteback, HelperDeliveryError } from './helper-writeback.js'; +import { checkSlackHelpers } from './slack-preflight.js'; +import { atomicJson, readSlackReceipt, receiptPath, slackWriteback, type SlackCall } from './slack-writeback.js'; + +type ProviderCall = HelperCall | SlackCall; + +export function assertHelperCredentials(provider: string): void { + const namespace = helperProviders.find(p => p.provider === provider)?.namespace; + if (!namespace) throw new AuthoredFlowExecutionError('helper_provider.unsupported', `Unknown helper ${provider}`); + const report = checkSlackHelpers({ header: { tools: { [namespace]: true } }, body() {} }); + if (!report.ok) { + const diagnostic = report.diagnostics[0]!; + throw new AuthoredFlowExecutionError( + diagnostic.kind as AuthoredFlowExecutionErrorCode, diagnostic.message); + } +} + +/** Each helper uses the existing agent lease + effect protocol; no new kernel verb. */ +export async function runHelperEffect( + journal: JournalClient, name: string, stepId: string, call: ProviderCall, + dataDir: string, journalSteps: AuthoredFlowJournalStep[], +): Promise { + assertHelperCredentials(call.provider); + if (call.provider === 'notion' && call.verb === 'appendBlock' && process.env.RELAYFLOWS_NOTION_MOCK !== '1') { + throw new AuthoredFlowExecutionError('helper_provider.unsupported', 'Notion appendBlock has no upstream mount writeback route'); + } + const stream = `${call.provider}-helper-${randomUUID()}`; + const spec = toKernelSpec(compileSpec({ + version: SPEC_SCHEMA_VERSION, name: `${name}/${stepId}`, + steps: [{ id: stepId, type: 'agent', instruction: JSON.stringify(call), + maxIterations: 3, recoveryMode: 'reset', + surfaces: { streams: [{ stream }], external: [`/${call.provider}`] } }], + })); + const outcome = await journal.runStart(spec); + await atomicJson(join(dataDir, 'helper-runs', `${outcome.run_id}.json`), { provider: call.provider }); + await driveHelperEffect(journal, outcome.run_id, spec.steps[0] as KernelAgentStep, call, dataDir); + const output = await readCompletedStepOutput(journal, outcome.run_id, stepId, journalSteps); + return (output as { receipt: unknown }).receipt; +} + +/** Recognize only the journaled helper envelope, so ordinary agent runs keep their worker path. */ +export async function resumeHelperEffect(journal: JournalClient, runId: string, dataDir: string): Promise { + if (!/^[A-Za-z0-9_-]+$/.test(runId) || !existsSync(join(dataDir, 'helper-runs', `${runId}.json`))) return false; + const entries = (await journal.journalRead(runId, 1)).entries as Array<{ + entry_type: string; payload: { spec?: { steps?: KernelAgentStep[] } }; + }>; + const steps = entries.find(entry => entry.entry_type === 'run.spawned')?.payload.spec?.steps; + if (steps?.length !== 1) return false; + const step = steps[0]!; + if (step.type !== 'agent' || !step.surfaces?.streams?.[0]?.stream.includes('-helper-')) return false; + const call = JSON.parse(step.instruction) as ProviderCall; + if (call.type !== 'effect' || !helperProviders.some(p => p.provider === call.provider && p.supported)) return false; + const snapshot = await journal.runGet(runId); + if (snapshot.status === 'completed' || snapshot.status === 'failed') return true; + assertHelperCredentials(call.provider); + await driveHelperEffect(journal, runId, step, call, dataDir); + return true; +} + +async function driveHelperEffect( + journal: JournalClient, runId: string, step: KernelAgentStep, call: ProviderCall, dataDir: string, +): Promise { + const client = new JournalClient(journal.socketPath); + const stream = step.surfaces!.streams![0]!.stream; + const pins = { workspace: [], streams: [{ stream, read_offset: 0 }] }; + let resolve!: () => void; + let reject!: (error: unknown) => void; + const completed = new Promise((yes, no) => { resolve = yes; reject = no; }); + // Observe early dispatch failures even while attach/run.resume is still pending. + void completed.catch(() => undefined); + let executing = false; + client.on('step.dispatch', (dispatch: StepDispatchEvent) => { + if (executing || dispatch.run_id !== runId || dispatch.step_id !== step.id) return; + executing = true; + void completeHelperDispatch(client, dispatch, call, dataDir).then(resolve, reject); + }); + client.on('error', reject); + try { + await client.connect(); + await client.hello('flows-provider-helper'); + await client.workerAttach(`${call.provider}-${randomUUID()}`, ['agent'], pins, 1); + const outcome = await journal.runResume(runId); + if (outcome.status === 'completed') return; + if (outcome.status === 'failed') { + throw new AuthoredFlowExecutionError('step_failed', `Helper run ${runId} ${outcome.status}`, undefined, runId); + } + await completed; + } catch (error) { + if (error instanceof AuthoredFlowExecutionError) throw error; + throw new AuthoredFlowExecutionError('step_failed', error instanceof Error ? error.message : 'Helper effect failed', 'worker_error', runId); + } finally { client.close(); } +} + +async function completeHelperDispatch(client: JournalClient, dispatch: StepDispatchEvent, call: ProviderCall, dataDir: string): Promise { + let output: unknown; + try { + output = await withWorkerLease(client, dispatch, async signal => { + const file = receiptPath(dataDir, dispatch.run_id, dispatch.step_id); + let receipt: unknown; + await client.performEffect({ + runId: dispatch.run_id, stepId: dispatch.step_id, attempt: dispatch.attempt, + idempotencyKey: dispatch.idempotency_key, surfacePath: `/${call.provider}`, + revisionBefore: 'pending', revisionAfter: `${dispatch.run_id}:${dispatch.step_id}`, + }, async () => { + try { receipt = await readSlackReceipt(file); } + catch (error) { + if ((error as NodeJS.ErrnoException).code !== 'ENOENT') throw error; + receipt = await (call.provider === 'slack' && 'params' in call + ? slackWriteback(call, dataDir, dispatch.run_id, dispatch.step_id, signal) + : helperWriteback(call as HelperCall, dataDir, dispatch.run_id, dispatch.step_id, signal)); + await atomicJson(file, receipt); + } + signal.throwIfAborted(); + }); + // Also required after a confirmed election followed by a crash before step.complete. + if (receipt === undefined) receipt = await readSlackReceipt(file); + return { ...call, idempotencyKey: `${dispatch.run_id}:${dispatch.step_id}`, receipt }; + }); + } catch (error) { + if (!(error instanceof HelperDeliveryError)) throw error; + await client.stepComplete(dispatch.run_id, dispatch.step_id, dispatch.attempt, + dispatch.idempotency_key, 'worker_error', { + output: { ...call, diagnostic: error.message }, + started_pins: dispatch.pins, end_pins: dispatch.pins, effects: [], + }); + return; + } + await client.stepComplete(dispatch.run_id, dispatch.step_id, dispatch.attempt, + dispatch.idempotency_key, 'success', { output, started_pins: dispatch.pins, end_pins: dispatch.pins, + effects: [{ surface_path: `/${call.provider}`, idempotency_key: dispatch.idempotency_key }] }); +} diff --git a/packages/sdk/src/authored-slack-effect.ts b/packages/sdk/src/authored-slack-effect.ts deleted file mode 100644 index 0c4fd0e76..000000000 --- a/packages/sdk/src/authored-slack-effect.ts +++ /dev/null @@ -1,95 +0,0 @@ -import { existsSync } from 'node:fs'; -import { join } from 'node:path'; -import { randomUUID } from 'node:crypto'; -import { JournalClient } from './journal-client.js'; -import { AuthoredFlowExecutionError } from './authored-flow-error.js'; -import { readCompletedStepOutput } from './authored-step-output.js'; -import type { AuthoredFlowJournalStep } from './authored-flow-executor.js'; -import { compileSpec, toKernelSpec } from './compile.js'; -import { SPEC_SCHEMA_VERSION, type KernelAgentStep } from './spec.js'; -import type { StepDispatchEvent } from './protocol.js'; -import { completeHelperDispatch } from './yaml-helper-effect.js'; -import { checkSlackHelpers } from './slack-preflight.js'; -import { atomicJson, type SlackCall } from './slack-writeback.js'; - -export function assertSlackCredentials(): void { - const report = checkSlackHelpers({ header: { tools: { slack: true } }, body() {} }); - if (!report.ok) { - const diagnostic = report.diagnostics[0]!; - throw new AuthoredFlowExecutionError( - diagnostic.kind === 'helper_slack.mount_required' ? diagnostic.kind : 'helper_slack.credential_missing', diagnostic.message); - } -} - -/** Each helper uses the existing agent lease + effect protocol; no new kernel verb. */ -export async function runSlackEffect( - journal: JournalClient, name: string, stepId: string, call: SlackCall, - dataDir: string, journalSteps: AuthoredFlowJournalStep[], -): Promise { - assertSlackCredentials(); - const stream = `slack-helper-${randomUUID()}`; - const spec = toKernelSpec(compileSpec({ - version: SPEC_SCHEMA_VERSION, name: `${name}/${stepId}`, - steps: [{ id: stepId, type: 'agent', instruction: JSON.stringify(call), - maxIterations: 3, recoveryMode: 'reset', - surfaces: { streams: [{ stream }], external: ['/slack'] } }], - })); - const outcome = await journal.runStart(spec); - await atomicJson(join(dataDir, 'helper-runs', `${outcome.run_id}.json`), { provider: 'slack' }); - await driveSlackEffect(journal, outcome.run_id, spec.steps[0] as KernelAgentStep, call, dataDir); - const output = await readCompletedStepOutput(journal, outcome.run_id, stepId, journalSteps); - return (output as { receipt: unknown }).receipt; -} - -/** Recognize only the journaled helper envelope, so ordinary agent runs keep their worker path. */ -export async function resumeSlackEffect(journal: JournalClient, runId: string, dataDir: string): Promise { - if (!/^[A-Za-z0-9_-]+$/.test(runId) || !existsSync(join(dataDir, 'helper-runs', `${runId}.json`))) return false; - const entries = (await journal.journalRead(runId, 1)).entries as Array<{ - entry_type: string; payload: { spec?: { steps?: KernelAgentStep[] } }; - }>; - const steps = entries.find(entry => entry.entry_type === 'run.spawned')?.payload.spec?.steps; - if (steps?.length !== 1) return false; - const step = steps[0]!; - if (step.type !== 'agent' || !step.surfaces?.streams?.[0]?.stream.startsWith('slack-helper-')) return false; - const call = JSON.parse(step.instruction) as SlackCall; - if (call.type !== 'effect' || call.provider !== 'slack' || !['post', 'dm', 'reply', 'react'].includes(call.verb)) return false; - const snapshot = await journal.runGet(runId); - if (snapshot.status === 'completed' || snapshot.status === 'failed') return true; - assertSlackCredentials(); - await driveSlackEffect(journal, runId, step, call, dataDir); - return true; -} - -async function driveSlackEffect( - journal: JournalClient, runId: string, step: KernelAgentStep, call: SlackCall, dataDir: string, -): Promise { - const client = new JournalClient(journal.socketPath); - const stream = step.surfaces!.streams![0]!.stream; - const pins = { workspace: [], streams: [{ stream, read_offset: 0 }] }; - let resolve!: () => void; - let reject!: (error: unknown) => void; - const completed = new Promise((yes, no) => { resolve = yes; reject = no; }); - // Observe early dispatch failures even while attach/run.resume is still pending. - void completed.catch(() => undefined); - let executing = false; - client.on('step.dispatch', (dispatch: StepDispatchEvent) => { - if (executing || dispatch.run_id !== runId || dispatch.step_id !== step.id) return; - executing = true; - void completeHelperDispatch(client, dispatch, call, dataDir).then(resolve, reject); - }); - client.on('error', reject); - try { - await client.connect(); - await client.hello('flows-slack-helper'); - await client.workerAttach(`slack-${randomUUID()}`, ['agent'], pins, 1); - const outcome = await journal.runResume(runId); - if (outcome.status === 'completed') return; - if (outcome.status === 'failed') { - throw new AuthoredFlowExecutionError('step_failed', `Slack helper run ${runId} ${outcome.status}`, undefined, runId); - } - await completed; - } catch (error) { - if (error instanceof AuthoredFlowExecutionError) throw error; - throw new AuthoredFlowExecutionError('step_failed', error instanceof Error ? error.message : 'Slack effect failed', 'worker_error', runId); - } finally { client.close(); } -} diff --git a/packages/sdk/src/cli/run.ts b/packages/sdk/src/cli/run.ts index c68bbd227..baabbf83e 100644 --- a/packages/sdk/src/cli/run.ts +++ b/packages/sdk/src/cli/run.ts @@ -1,7 +1,7 @@ import { parseDigestReference } from '../bundle-transport.js'; import { prepareDigestRun } from './run-digest.js'; import { reuseSummary } from './reuse.js'; -import { resumeSlackEffect } from '../authored-slack-effect.js'; +import { resumeHelperEffect } from '../authored-helper-effect.js'; import { AuthoredFlowExecutionError } from '../authored-flow-error.js'; import { join, resolve } from 'node:path'; import type { ProgressEvent } from '../progress.js'; @@ -151,8 +151,12 @@ export async function resumeFlow( if (connected !== undefined) return connected; try { + // resumeHelperEffect subsumes the old resumeSlackEffect: it handles the + // slack effect resume plus every other provider from N's codegen. The + // second call the earlier rebase left is a stale reference from before + // the helper fanout renamed the API. let outcome = await client.runResume(runId, options.allowHumanInfluenced); - if (await resumeSlackEffect(client, runId, dataDir)) { + if (await resumeHelperEffect(client, runId, dataDir)) { outcome = await client.runResume(runId, options.allowHumanInfluenced); } return await classifyOutcome(client, 'resume', outcome, base, socketPath, options); @@ -162,7 +166,8 @@ export async function resumeFlow( diagnostics: [{ severity: 'refusal', kind: 'human_influenced_run', message: error.message.replace(/^human_influenced_run: /, '') }] } }; } if (error instanceof AuthoredFlowExecutionError - && (error.code === 'helper_slack.credential_missing' || error.code === 'helper_slack.mount_required')) { + && (error.code === 'helper_slack.credential_missing' || error.code === 'helper_slack.mount_required' + || error.code === 'helper_provider.mount_required' || error.code === 'helper_provider.unsupported')) { return { exitCode: 2, report: { ...base, runId, socketPath, diagnostics: [{ severity: 'refusal', kind: error.code, message: error.message }] } }; } diff --git a/packages/sdk/src/failure-kinds.ts b/packages/sdk/src/failure-kinds.ts index 7b2917d9c..fd46d5f1c 100644 --- a/packages/sdk/src/failure-kinds.ts +++ b/packages/sdk/src/failure-kinds.ts @@ -6,6 +6,8 @@ const SHARED_SPEC_FAILURE_KINDS = ['invalid_spec'] as const; const PREFLIGHT_ENVIRONMENT_FAILURE_KINDS = [ ...NAMED_GATE_FAILURE_KINDS, ...PLUGIN_FAILURE_KINDS, + 'helper_provider.mount_required', + 'helper_provider.unsupported', 'helper_slack.credential_missing', 'helper_slack.mount_required', 'helper_mount_required', diff --git a/packages/sdk/src/helper-preflight.ts b/packages/sdk/src/helper-preflight.ts new file mode 100644 index 000000000..3f22d264a --- /dev/null +++ b/packages/sdk/src/helper-preflight.ts @@ -0,0 +1,34 @@ +import { helperProviders } from '@relayflows/surface/runtime'; +import type { PreflightResult, PreflightDiagnostic } from './preflight.js'; + +/** Static discovery never executes the body; dynamic aliases are checked at call time. */ +export function preflightHelpers( + definition: { header?: { tools?: Readonly> }; body?: Function }, + facts: { slackToken?: string; slackMount?: boolean; slackMock?: boolean; + providers?: Readonly> }, +): PreflightResult { + const body = typeof definition.body === 'function' ? Function.prototype.toString.call(definition.body) : ''; + const parameter = body.match(/^(?:async\s+)?(?:function(?:\s+[\w$]+)?\s*)?(?:\(\s*([\w$]+)|([\w$]+)\s*=>)/); + const root = (parameter?.[1] ?? parameter?.[2])?.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); + const diagnostics: PreflightDiagnostic[] = []; + for (const { provider, namespace, supported } of helperProviders) { + const used = definition.header?.tools?.[namespace] === true + || (root !== undefined && new RegExp(`(?:^|[^\\w$.])${root}\\s*(?:\\.\\s*${namespace}\\b|\\[\\s*['"]${namespace}['"]\\s*\\])`).test(body)); + if (!used) continue; + const fact = facts.providers?.[provider] ?? (provider === 'slack' + ? { mount: facts.slackMount, mock: facts.slackMock, token: facts.slackToken } + : { mount: false, mock: false, token: undefined }); + if (!supported) { + diagnostics.push({ severity: 'refusal', kind: 'helper_provider.unsupported', + message: `f.${namespace} has no upstream relayfile writeback client.` }); + } else if (!fact.mock && !fact.mount) { + diagnostics.push({ severity: 'refusal', + kind: provider === 'slack' ? (fact.token?.trim() ? 'helper_slack.mount_required' : 'helper_slack.credential_missing') : 'helper_provider.mount_required', + message: `f.${namespace} requires a relayfile ${provider} mount; direct-token transport is not implemented.` }); + } else if (provider === 'notion' && !fact.mock && /\.\s*appendBlock\b/.test(body)) { + diagnostics.push({ severity: 'refusal', kind: 'helper_provider.unsupported', + message: 'f.notion.appendBlock is mock-only: the Notion adapter has no append-block writeback route.' }); + } + } + return { ok: diagnostics.length === 0, gates: [], resolutions: [], diagnostics }; +} diff --git a/packages/sdk/src/helper-storage.ts b/packages/sdk/src/helper-storage.ts new file mode 100644 index 000000000..99c394c2d --- /dev/null +++ b/packages/sdk/src/helper-storage.ts @@ -0,0 +1,23 @@ +import { createHash, randomUUID } from 'node:crypto'; +import { mkdir, open, readFile, rename } from 'node:fs/promises'; +import { dirname, join } from 'node:path'; + +/** Durable receipt precedes effect.confirm, so a confirmed replay can recover it. */ +export async function atomicJson(path: string, value: unknown): Promise { + await mkdir(dirname(path), { recursive: true }); + const temporary = `${path}.${randomUUID()}.tmp`; + const file = await open(temporary, 'wx', 0o600); + try { await file.writeFile(JSON.stringify(value)); await file.sync(); } + finally { await file.close(); } + await rename(temporary, path); + const directory = await open(dirname(path), 'r'); + try { await directory.sync(); } finally { await directory.close(); } +} + +export function receiptPath(dataDir: string, runId: string, stepId: string): string { + return join(dataDir, 'helper-receipts', createHash('sha256').update(`${runId}:${stepId}`).digest('hex') + '.json'); +} + +export async function readHelperReceipt(path: string): Promise { + return JSON.parse(await readFile(path, 'utf8')) as unknown; +} diff --git a/packages/sdk/src/helper-writeback.ts b/packages/sdk/src/helper-writeback.ts new file mode 100644 index 000000000..d5fe108f6 --- /dev/null +++ b/packages/sdk/src/helper-writeback.ts @@ -0,0 +1,83 @@ +import { createHash } from 'node:crypto'; +import { join } from 'node:path'; +import { writeJsonFile, readJsonFile, listJsonFiles, type WritebackResult } from '@relayfile/adapter-core/vfs-client'; +import type { RelayTransport, RelayTransportRequest } from '@relayfile/relay-helpers/transport'; +import { helperClients, helperProviders, invokeHelper, type HelperCall } from '@relayflows/surface/runtime'; +import { providerMount } from './slack-preflight.js'; +import { atomicJson } from './helper-storage.js'; +import type { SlackCall } from './slack-writeback.js'; + +export class HelperDeliveryError extends Error {} + +export async function helperWriteback(call: HelperCall, dataDir: string, runId: string, stepId: string, signal: AbortSignal): Promise { + const factory = helperClients[call.provider]; + if (!factory) throw new Error(`No writeback client for ${call.provider}`); + const { transport } = helperTransport(call, dataDir, runId, stepId, signal); + try { return await invokeHelper(factory, call, transport); } + catch (cause) { throw new HelperDeliveryError(cause instanceof Error ? cause.message : String(cause), { cause }); } +} + +/** One transport for every provider, including Slack. Only confirmed writes succeed. */ +export function helperTransport(call: HelperCall | SlackCall, dataDir: string, runId: string, stepId: string, signal: AbortSignal) { + const provider = helperProviders.find(p => p.provider === call.provider); + if (!provider) throw new Error(`Unknown helper provider ${call.provider}`); + const mock = process.env[provider.mockEnv] === '1'; + const mount = providerMount(provider.provider); + const options = () => { + signal.throwIfAborted(); + if (!mount) throw new Error(`${provider.provider} requires a relayfile mount`); + return { relayfileMountRoot: mount }; + }; + const idempotencyKey = `${runId}:${stepId}`; + let deliveredRef = ''; + const transport: RelayTransport = { + async read(request: RelayTransportRequest): Promise { + signal.throwIfAborted(); + if (mock) return {} as T; + return readJsonFile(options(), call.provider, `read.${request.resource}`, request.path); + }, + async list(request: RelayTransportRequest): Promise { + signal.throwIfAborted(); + if (mock) return []; + return (await listJsonFiles(options(), call.provider, `list.${request.resource}`, request.path)).map(file => file.value); + }, + async write(request) { + signal.throwIfAborted(); + if (request.provider !== call.provider) throw new Error('Helper transport provider mismatch'); + if (!mock && call.provider === 'notion' && call.verb === 'appendBlock') { + throw new Error('Notion appendBlock has no upstream mount writeback route'); + } + if (typeof request.body !== 'object' || request.body === null || Array.isArray(request.body)) { + throw new Error('Helper writeback requires a JSON object'); + } + const body: Record = { ...request.body as Record, idempotencyKey }; + // Item paths must remain canonical; only collections receive a draft filename. + const draft = /\.[a-z]+$/i.test(request.path) ? request.path + : `${request.path}/draft-${createHash('sha256').update(idempotencyKey).digest('hex')}.json`; + let result: WritebackResult; + if (mock) { + deliveredRef = `mock-ref-${stepId}`; + result = { path: deliveredRef, absolutePath: draft, deliveryStatus: 'confirmed', receipt: { externalId: `mock-${stepId}` } }; + await atomicJson(join(dataDir, 'mock-writeback', call.provider, `${stepId}.json`), { + ...call, ...body, + ...(call.provider === 'slack' ? { channel: request.parameters.channelId, + ...(body.parentRef === undefined ? {} : { replyTo: body.parentRef }) } : {}), + runId, stepId, request: { ...request, body }, receipt: result.receipt, + }); + } else { + try { result = await writeJsonFile(options(), call.provider, `write.${request.resource}`, draft, body); } + catch (cause) { + // Upstream created() converts pending/terminal adapter errors into values. + // A journal effect must fail instead of confirming an undelivered write. + throw new Error(`${call.provider} writeback failed: ${cause instanceof Error ? cause.message : String(cause)}`, { cause }); + } + if (result.deliveryStatus !== 'confirmed' || !result.receipt) throw new Error(`${call.provider} writeback is pending; no delivery receipt`); + if (call.provider === 'slack' && call.verb !== 'react' && !result.receipt.externalId && !result.receipt.ts) throw new Error('Slack writeback has no delivered timestamp'); + deliveredRef = result.path; + } + signal.throwIfAborted(); + return result; + }, + }; + return { transport, deliveredRef: () => deliveredRef }; +} diff --git a/packages/sdk/src/preflight.ts b/packages/sdk/src/preflight.ts index 069bd2135..9561e5a37 100644 --- a/packages/sdk/src/preflight.ts +++ b/packages/sdk/src/preflight.ts @@ -607,24 +607,7 @@ function probeNamedGate(step: StepSpec, probes: PreflightProbes, diagnostics: Pr } } -/** Helper preflight never evaluates the authored body. Dynamic uses are checked at call time. */ -export function preflightHelpers( - definition: { header?: { tools?: { slack?: boolean } }; body?: Function }, - facts: { slackToken?: string; slackMount: boolean; slackMock: boolean }, -): PreflightResult { - const usesSlack = definition.header?.tools?.slack === true - || (typeof definition.body === 'function' - && /(?:\.\s*slack\b|\[\s*['"]slack['"]\s*\])/.test(Function.prototype.toString.call(definition.body))); - const diagnostics: PreflightDiagnostic[] = usesSlack && !facts.slackMock - && !facts.slackToken?.trim() && !facts.slackMount - ? [{ severity: 'refusal', kind: 'helper_slack.credential_missing', - message: 'f.slack requires SLACK_BOT_TOKEN or a relayfile Slack mount.' }] - : usesSlack && !facts.slackMock && !facts.slackMount - ? [{ severity: 'refusal', kind: 'helper_slack.mount_required', - message: 'f.slack direct bot-token transport is not implemented; configure a relayfile Slack mount.' }] - : []; - return { ok: diagnostics.length === 0, gates: [], resolutions: [], diagnostics }; -} +export { preflightHelpers } from './helper-preflight.js'; /** Authored memory probes run before invoking the body or contacting the journal. */ export async function preflightMemory( diff --git a/packages/sdk/src/slack-preflight.ts b/packages/sdk/src/slack-preflight.ts index 18a7e0cd7..051d2195d 100644 --- a/packages/sdk/src/slack-preflight.ts +++ b/packages/sdk/src/slack-preflight.ts @@ -1,19 +1,25 @@ import { statSync } from 'node:fs'; import { join } from 'node:path'; +import { helperProviders } from '@relayflows/surface/runtime'; import { preflightHelpers } from './preflight.js'; -export function slackMount(env: NodeJS.ProcessEnv = process.env): string | undefined { +export function providerMount(provider: string, env: NodeJS.ProcessEnv = process.env): string | undefined { const root = [env.RELAYFILE_MOUNT_PATH, env.WORKSPACE_ROOT, env.WORKFORCE_SANDBOX_ROOT, env.RELAYFILE_MOUNT_ROOT, env.RELAYFILE_ROOT].find(value => value?.trim()); if (!root?.trim()) return undefined; - try { return statSync(join(root, 'slack')).isDirectory() ? root : undefined; } + try { return statSync(join(root, provider)).isDirectory() ? root : undefined; } catch { return undefined; } } -export function checkSlackHelpers(definition: Parameters[0]) { +export const slackMount = (env: NodeJS.ProcessEnv = process.env) => providerMount('slack', env); + +export function checkProviderHelpers(definition: Parameters[0]) { return preflightHelpers(definition, { - slackToken: process.env.SLACK_BOT_TOKEN, - slackMount: slackMount() !== undefined, - slackMock: process.env.RELAYFLOWS_SLACK_MOCK === '1', + providers: Object.fromEntries(helperProviders.map(p => [p.provider, { + mount: providerMount(p.provider) !== undefined, + mock: process.env[p.mockEnv] === '1', + token: p.provider === 'slack' ? process.env.SLACK_BOT_TOKEN : undefined, + }])), }); } +export const checkSlackHelpers = checkProviderHelpers; diff --git a/packages/sdk/src/slack-writeback.ts b/packages/sdk/src/slack-writeback.ts index daa2d2162..308133a35 100644 --- a/packages/sdk/src/slack-writeback.ts +++ b/packages/sdk/src/slack-writeback.ts @@ -1,12 +1,8 @@ -import { createHash, randomUUID } from 'node:crypto'; -import { mkdir, open, readFile, rename } from 'node:fs/promises'; -import { dirname, join } from 'node:path'; import { slackClient } from '@relayfile/relay-helpers'; -import { flowRunWritebackIdempotency, type SlackHelper } from '@relayflows/surface'; -import { slackPostBody } from '@relayflows/surface/runtime'; import type { RelayTransport } from '@relayfile/relay-helpers/transport'; -import { writeJsonFile, type WritebackResult } from '@relayfile/adapter-core/vfs-client'; -import { slackMount } from './slack-preflight.js'; +import type { SlackHelper } from '@relayflows/surface'; +import { helperTransport } from './helper-writeback.js'; +export { atomicJson, receiptPath, readHelperReceipt as readSlackReceipt } from './helper-storage.js'; export type SlackCall = | { type: 'effect'; provider: 'slack'; verb: 'post'; params: { channel: string; text: Parameters[1]; opts?: Parameters[2] } } @@ -14,72 +10,54 @@ export type SlackCall = | { type: 'effect'; provider: 'slack'; verb: 'reply'; params: { channel: string; threadTs: string; text: string } } | { type: 'effect'; provider: 'slack'; verb: 'react'; params: { channel: string; messageTs: string; emoji: string } }; -/** Durable receipt precedes effect.confirm, so a confirmed replay can recover it. */ -export async function atomicJson(path: string, value: unknown): Promise { - await mkdir(dirname(path), { recursive: true }); - const temporary = `${path}.${randomUUID()}.tmp`; - const file = await open(temporary, 'wx', 0o600); - try { await file.writeFile(JSON.stringify(value)); await file.sync(); } - finally { await file.close(); } - await rename(temporary, path); - const directory = await open(dirname(path), 'r'); - try { await directory.sync(); } finally { await directory.close(); } +interface SlackPostBodyExtras { text?: string; dropText: boolean; blocks?: unknown; attachments?: unknown } + +// Slice Y widened SlackPostMessage to carry blocks/attachments; the transport +// body is otherwise built by slackClient, which only sees the flattened text. +// Reconstruct the intended body by projecting from the original call.params. +function slackPostBodyExtras(params: Extract['params']): SlackPostBodyExtras { + const extras: SlackPostBodyExtras = { dropText: false }; + const t = params.text; + const opts = params.opts as { blocks?: unknown; attachments?: unknown } | undefined; + if (typeof t === 'object' && t !== null) { + if ('text' in t && typeof t.text === 'string') extras.text = t.text; + else extras.dropText = true; + if ('blocks' in t && t.blocks !== undefined) extras.blocks = t.blocks; + if ('attachments' in t && t.attachments !== undefined) extras.attachments = t.attachments; + } + if (opts?.blocks !== undefined) extras.blocks = opts.blocks; + if (opts?.attachments !== undefined) extras.attachments = opts.attachments; + return extras; } -export function receiptPath(dataDir: string, runId: string, stepId: string): string { - return join(dataDir, 'helper-receipts', createHash('sha256').update(`${runId}:${stepId}`).digest('hex') + '.json'); +function wrapSlackPostTransport(transport: RelayTransport, extras: SlackPostBodyExtras): RelayTransport { + return { + read: transport.read.bind(transport), + list: transport.list.bind(transport), + async write(request) { + const body = { ...request.body as Record }; + if (extras.dropText) delete body.text; + else if (extras.text !== undefined) body.text = extras.text; + if (extras.blocks !== undefined) body.blocks = extras.blocks; + if (extras.attachments !== undefined) body.attachments = extras.attachments; + return transport.write({ ...request, body }); + }, + }; } export async function slackWriteback( call: SlackCall, dataDir: string, runId: string, stepId: string, signal: AbortSignal, ): Promise { - const idempotencyKey = flowRunWritebackIdempotency(runId, stepId); - let deliveredRef = ''; - const transport: RelayTransport = { - async read() { throw new Error('Slack effect transport is write-only'); }, - async list() { throw new Error('Slack effect transport is write-only'); }, - async write(request) { - signal.throwIfAborted(); - // The pinned adapter's ergonomic post accepts text only. Preserve structured - // content at its transport boundary, retaining its paths and receipt handling. - const content = call.verb === 'post' - ? slackPostBody(call.params.text, call.params.opts) - : request.body as Record; - const body: Record = { ...content, idempotencyKey }; - const stamped = { ...request, body }; - const draft = `${request.path}/draft-${createHash('sha256').update(idempotencyKey).digest('hex')}.json`; - if (process.env.RELAYFLOWS_SLACK_MOCK === '1') { - const ts = `mock-${stepId}`; - deliveredRef = `mock-ref-${stepId}`; - const result: WritebackResult = { path: deliveredRef, absolutePath: draft, deliveryStatus: 'confirmed', receipt: { externalId: ts } }; - await atomicJson(join(dataDir, 'mock-writeback', 'slack', `${stepId}.json`), { - ...call, ...body, channel: request.parameters.channelId, - ...(body.parentRef === undefined ? {} : { replyTo: body.parentRef }), - runId, stepId, request: stamped, receipt: result.receipt, - }); - return result; - } - const mount = slackMount(); - if (mount !== undefined) { - const result = await writeJsonFile({ relayfileMountRoot: mount }, 'slack', `write.${request.resource}`, draft, body); - if (result.deliveryStatus !== 'confirmed' || !result.receipt) throw new Error('Slack writeback is pending; no delivery receipt'); - if (call.verb !== 'react' && !result.receipt.externalId && !result.receipt.ts) throw new Error('Slack writeback has no delivered timestamp'); - deliveredRef = result.path; - return result; - } - throw new Error('Slack effect requires a relayfile mount; direct bot-token transport is not implemented'); - }, - }; - const client = slackClient({ transport }); + const { transport, deliveredRef } = helperTransport(call, dataDir, runId, stepId, signal); + const effectiveTransport = call.verb === 'post' + ? wrapSlackPostTransport(transport, slackPostBodyExtras(call.params)) + : transport; + const client = slackClient({ transport: effectiveTransport }); switch (call.verb) { case 'post': return client.post(call.params.channel, typeof call.params.text === 'string' ? call.params.text : call.params.text.text ?? '', call.params.opts); case 'dm': return client.dm(call.params.user, call.params.text); - case 'reply': return { ...await client.reply(call.params.channel, call.params.threadTs, call.params.text), ref: deliveredRef }; + case 'reply': return { ...await client.reply(call.params.channel, call.params.threadTs, call.params.text), ref: deliveredRef() }; case 'react': await client.react(call.params.channel, call.params.messageTs, call.params.emoji); return null; } } - -export async function readSlackReceipt(path: string): Promise { - return JSON.parse(await readFile(path, 'utf8')) as unknown; -} diff --git a/packages/sdk/tests/authored-helpers.test.ts b/packages/sdk/tests/authored-helpers.test.ts new file mode 100644 index 000000000..7d23e2b28 --- /dev/null +++ b/packages/sdk/tests/authored-helpers.test.ts @@ -0,0 +1,190 @@ +import { spawn, spawnSync, type ChildProcess } from 'node:child_process'; +import { once } from 'node:events'; +import { existsSync, mkdirSync, symlinkSync, mkdtempSync, readdirSync, readFileSync, realpathSync, rmSync, writeFileSync } from 'node:fs'; +import { homedir, tmpdir } from 'node:os'; +import { dirname, join, resolve } from 'node:path'; +import { fileURLToPath } from 'node:url'; +import { flow } from '@relayflows/surface'; +import { afterEach, describe, expect, it, vi } from 'vitest'; +import { executeAuthoredFlow } from '../src/authored-flow-executor.js'; +import { JournalClient } from '../src/journal-client.js'; +import { socketPathFor } from '../src/daemon-connection.js'; +import { resumeFlow } from '../src/cli/run.js'; +import { runCli } from '../src/cli.js'; +import { helperProviders, type HelperCall } from '@relayflows/surface/runtime'; +import { WRITEBACK_PATH_CATALOG } from '@relayfile/adapter-core/writeback-paths'; +import { runHelperEffect, resumeHelperEffect } from '../src/authored-helper-effect.js'; + +const root = resolve(dirname(fileURLToPath(import.meta.url)), '../../..'); +// Match ops/cargo.sh's per-worktree build output for a plain `npm test` too. +const worktreeKey = spawnSync('cksum', { input: realpathSync(root), encoding: 'utf8' }).stdout.trim().split(' ')[0]!; +const target = process.env.CARGO_TARGET_DIR ?? join(process.env.RELAYFLOWS_TOOLCHAIN_HOME ?? join(homedir(), '.relayflows-toolchain'), 'target', worktreeKey); +const binary = process.env.RELAYFLOWD_BIN ?? join(target, 'debug/relayflowd'); +const directories: string[] = []; +const children: ChildProcess[] = []; +const clients: JournalClient[] = []; +const delay = (ms: number) => new Promise(resolve => setTimeout(resolve, ms)); + +afterEach(async () => { + for (const client of clients.splice(0)) client.close(); + for (const child of children.splice(0)) await kill(child); + for (const dir of directories.splice(0)) rmSync(dir, { recursive: true, force: true }); + vi.unstubAllEnvs(); + vi.restoreAllMocks(); +}); + +function temporary(): string { + const dir = mkdtempSync(join(tmpdir(), 'f-helpers-')); + directories.push(dir); + return dir; +} + +async function kill(child: ChildProcess): Promise { + if (child.exitCode !== null || child.signalCode !== null) return; + const exited = once(child, 'exit'); + child.kill('SIGKILL'); + await exited; +} + +async function start(dataDir: string): Promise<{ daemon: ChildProcess; client: JournalClient }> { + expect(existsSync(binary), `Build relayflowd first: ${binary}`).toBe(true); + const daemon = spawn(binary, ['--data-dir', dataDir, 'serve'], { stdio: ['ignore', 'pipe', 'pipe'] }); + children.push(daemon); + let stderr = ''; + daemon.stderr!.on('data', chunk => { stderr += String(chunk); }); + for (let attempt = 0; attempt < 100; attempt++) { + const client = new JournalClient(socketPathFor(dataDir), { connectTimeoutMs: 100 }); + try { + await client.connect(); await client.hello('slack-test'); clients.push(client); + return { daemon, client }; + } catch { client.close(); } + if (daemon.exitCode !== null) throw new Error(stderr); + await delay(20); + } + throw new Error(`Daemon did not start: ${stderr}`); +} + +it('lowers the named acceptance helpers and Slack to confirmed journal effects', async () => { + for (const p of helperProviders) vi.stubEnv(p.mockEnv, '1'); + const dataDir = temporary(); + const { client } = await start(dataDir); + const result = await executeAuthoredFlow(flow('helper-smoke', async f => { + const github = await f.github.createIssue({ repo: 'owner/repo', title: 'Smoke', body: 'body' }); + expect(github).toMatchObject({ status: 'confirmed' }); + await f.linear.createIssue({ teamId: 'team', title: 'Smoke' }); + await f.notion.appendBlock({ pageId: 'page', block: { type: 'paragraph' } }); + await f.stripe.createInvoice({ customer: 'cus_fixture' }); + await f.slack.post('#test', 'hello'); + f.done('success'); + }), client, undefined, { dataDir }); + expect(result.completionReason).toBe('success'); + expect(result.journalSteps).toHaveLength(6); + for (const step of result.journalSteps.slice(0, -1)) { + const entries = (await client.journalRead(step.runId, 1)).entries as any[]; + expect(entries.filter(e => e.entry_type === 'effect.confirmed')).toHaveLength(1); + expect(entries.find(e => e.entry_type === 'step.completed').payload).toMatchObject({ completionReason: 'success', output: { type: 'effect' } }); + } +}, 30_000); + +it('runs every available provider through the real kernel and resumes completed effects without a second write', async () => { + const dataDir = temporary(); + const { client } = await start(dataDir); + const catalog = WRITEBACK_PATH_CATALOG as Record>; + for (const p of helperProviders.filter(p => p.supported && p.provider !== 'slack')) { + vi.stubEnv(p.mockEnv, '1'); + const [resource, variants] = Object.entries(catalog[p.provider] ?? {})[0] ?? []; + const params = Object.fromEntries((variants?.[0]?.params ?? []).map(param => [param, 'fixture'])); + const call: HelperCall = p.provider === 'stripe' + ? { type: 'effect', provider: p.provider, verb: 'createInvoice', args: [{ customer: 'cus_fixture' }] } + : { type: 'effect', provider: p.provider, verb: `${resource}.write`, args: [params, { text: 'smoke' }] }; + const steps: import('../src/authored-flow-executor.js').AuthoredFlowJournalStep[] = []; + await runHelperEffect(client, 'fanout', p.provider, call, dataDir, steps); + expect(steps).toHaveLength(1); + const runId = steps[0]!.runId; + expect(await resumeHelperEffect(client, runId, dataDir)).toBe(true); + const entries = (await client.journalRead(runId, 1)).entries as any[]; + expect(entries.filter(e => e.entry_type === 'effect.confirmed'), p.provider).toHaveLength(1); + expect(entries.filter(e => e.entry_type === 'step.completed'), p.provider).toHaveLength(1); + expect(readdirSync(join(dataDir, 'mock-writeback', p.provider))).toEqual([`${p.provider}.json`]); + } +}, 60_000); + +it('rejects malformed arguments before a journal write, without evaluating getters', async () => { + vi.stubEnv('RELAYFLOWS_GITHUB_MOCK', '1'); + let evaluated = false; + const args = { repo: 'a/b', get title() { evaluated = true; return 'bad'; }, body: '' }; + const client = new JournalClient('/must-not-connect'); + await expect(executeAuthoredFlow(flow('bad-args', async f => { + await f.github.createIssue(args); f.done('success'); + }), client)).rejects.toThrow('accessors'); + expect(evaluated).toBe(false); +}); + +function mockFiles(dataDir: string): string[] { + return readdirSync(join(dataDir, "mock-writeback/github")).filter(file => file.endsWith(".json")); +} + + it.each(['confirm', 'complete'] as const)('replays after SIGKILL before %s with the same token and one successful completion', async boundary => { + vi.stubEnv('RELAYFLOWS_GITHUB_MOCK', '1'); + const dataDir = temporary(); + const first = await start(dataDir); + const script = join(dataDir, 'crash.mjs'); + writeFileSync(script, ` + import { flow } from ${JSON.stringify(join(root, 'packages/sdk/node_modules/@relayflows/surface/dist/index.js'))}; + import { JournalClient } from ${JSON.stringify(join(root, 'packages/sdk/dist/journal-client.js'))}; + import { executeAuthoredFlow } from ${JSON.stringify(join(root, 'packages/sdk/dist/authored-flow-executor.js'))}; + JournalClient.prototype.${boundary === 'confirm' ? 'effectConfirm' : 'stepComplete'} = async function() { process.stdout.write('CRASH_BOUNDARY\\n'); await new Promise(() => {}); }; + const client = new JournalClient(${JSON.stringify(socketPathFor(dataDir))}); + await client.connect(); await client.hello('crash-child'); + await executeAuthoredFlow(flow('crash-github', async f => { await f.github.createIssue({ repo: 'owner/repo', title: 'hi', body: '' }); f.done('success'); }), client, undefined, { dataDir: ${JSON.stringify(dataDir)} }); + `); + const child = spawn(process.execPath, [script], { stdio: ['ignore', 'pipe', 'pipe'] }); + children.push(child); + await new Promise((resolve, reject) => { + const timer = setTimeout(() => reject(new Error('Did not reach crash window')), 10_000); + child.stdout!.on('data', chunk => { if (String(chunk).includes('CRASH_BOUNDARY')) { clearTimeout(timer); resolve(); } }); + child.stderr!.on('data', chunk => { clearTimeout(timer); reject(new Error(String(chunk))); }); + }); + const file = join(dataDir, 'mock-writeback/github', mockFiles(dataDir)[0]!); + const before = JSON.parse(readFileSync(file, 'utf8')); + await kill(child); first.client.close(); await kill(first.daemon); + const second = await start(dataDir); + // Unconfirmed: lose the cache too to prove the provider token is stable. + // Confirmed: the persisted receipt must suffice without another provider write. + if (boundary === 'confirm') { + for (const cache of readdirSync(join(dataDir, 'helper-receipts'))) rmSync(join(dataDir, 'helper-receipts', cache)); + } + for (const key of ['GITHUB_BOT_TOKEN', 'RELAYFLOWS_GITHUB_MOCK', 'RELAYFILE_MOUNT_PATH', 'WORKSPACE_ROOT', 'WORKFORCE_SANDBOX_ROOT', 'RELAYFILE_MOUNT_ROOT', 'RELAYFILE_ROOT']) vi.stubEnv(key, ''); + const refused = await resumeFlow(before.runId, dataDir, { daemon: { spawn: false } }); + expect(refused.exitCode).toBe(2); + expect(refused.report.diagnostics).toContainEqual(expect.objectContaining({ kind: 'helper_provider.mount_required' })); + vi.stubEnv('RELAYFLOWS_GITHUB_MOCK', '1'); + const resumed = await resumeFlow(before.runId, dataDir, { daemon: { spawn: false } }); + expect(resumed.exitCode, JSON.stringify(resumed.report)).toBe(0); + const after = JSON.parse(readFileSync(file, 'utf8')); + expect(after.idempotencyKey).toBe(before.idempotencyKey); + expect(after.receipt).toEqual(before.receipt); + const entries = (await second.client.journalRead(before.runId, 1)).entries as any[]; + // The kernel records abandoned attempts as step.completed(crashed); one successful + // completion is the exactly-once result, while retaining honest crash history. + expect(entries.filter(entry => entry.entry_type === 'step.completed' && entry.payload.completionReason === 'success')).toHaveLength(1); + expect(entries.filter(entry => entry.entry_type === 'step.completed' && entry.payload.completionReason === 'crashed')).toHaveLength(1); + expect(entries.filter(entry => entry.entry_type === 'effect.recorded').length).toBeGreaterThan(1); + const again = await resumeFlow(before.runId, dataDir, { daemon: { spawn: false } }); + expect(again.exitCode).toBe(0); + expect(mockFiles(dataDir)).toHaveLength(1); + }, 30_000); + + +it('journals provider failure with worker_error and never confirms the effect', async () => { + vi.stubEnv('RELAYFLOWS_GITHUB_MOCK', '1'); + const dataDir = temporary(); const { client } = await start(dataDir); + const steps: import('../src/authored-flow-executor.js').AuthoredFlowJournalStep[] = []; + await expect(runHelperEffect(client, 'bad-helper', 'invalid', { + type: 'effect', provider: 'github', verb: 'createIssue', args: [{ repo: 'missing-owner', title: 'hi', body: '' }], + }, dataDir, steps)).rejects.toMatchObject({ code: 'step_failed', completionReason: 'worker_error' }); + expect(steps).toHaveLength(1); + const entries = (await client.journalRead(steps[0]!.runId, 1)).entries as any[]; + expect(entries.filter(e => e.entry_type === 'effect.confirmed')).toHaveLength(0); + expect(entries.find(e => e.entry_type === 'step.completed').payload.completionReason).toBe('worker_error'); +}); diff --git a/packages/sdk/tests/helpers-fanout.test.ts b/packages/sdk/tests/helpers-fanout.test.ts new file mode 100644 index 000000000..013787f6b --- /dev/null +++ b/packages/sdk/tests/helpers-fanout.test.ts @@ -0,0 +1,112 @@ +import { mkdtempSync, mkdirSync, readFileSync, rmSync } from 'node:fs'; +import { tmpdir } from 'node:os'; +import { join } from 'node:path'; +import { afterEach, expect, it, vi } from 'vitest'; +import { flow } from '@relayflows/surface'; +import { createHelpers, helperProviders, type HelperCall } from '@relayflows/surface/runtime'; +import { WRITEBACK_PATH_CATALOG } from '@relayfile/adapter-core/writeback-paths'; +import * as vfs from '@relayfile/adapter-core/vfs-client'; +import { helperWriteback } from '../src/helper-writeback.js'; +import { checkProviderHelpers, providerMount } from '../src/slack-preflight.js'; +import { executeAuthoredFlow } from '../src/authored-flow-executor.js'; +import { JournalClient } from '../src/journal-client.js'; + +vi.mock('@relayfile/adapter-core/vfs-client', async () => { + const actual = await vi.importActual('@relayfile/adapter-core/vfs-client'); + return { ...actual, writeJsonFile: vi.fn() }; +}); + +const dirs: string[] = []; +function temporary() { const dir = mkdtempSync(join(tmpdir(), 'helper-fanout-')); dirs.push(dir); return dir; } +afterEach(() => { vi.restoreAllMocks(); vi.unstubAllEnvs(); for (const dir of dirs.splice(0)) rmSync(dir, { recursive: true, force: true }); }); +const signal = () => new AbortController().signal; +const github: HelperCall = { type: 'effect', provider: 'github', verb: 'createIssue', args: [{ repo: 'owner/repo', title: 'hi', body: 'body' }] }; + +for (const provider of helperProviders) { + it(`${provider.provider}: checks mount and its uniform mock variable`, () => { + const dir = temporary(); + for (const key of ['RELAYFILE_MOUNT_PATH', 'WORKSPACE_ROOT', 'WORKFORCE_SANDBOX_ROOT', 'RELAYFILE_MOUNT_ROOT', 'RELAYFILE_ROOT']) vi.stubEnv(key, ''); + vi.stubEnv(provider.mockEnv, ''); + const definition = { header: { tools: { [provider.namespace]: true } } }; + expect(checkProviderHelpers(definition).ok).toBe(false); + vi.stubEnv(provider.mockEnv, '1'); + expect(checkProviderHelpers(definition).ok).toBe(provider.supported); + vi.stubEnv(provider.mockEnv, ''); + vi.stubEnv('WORKSPACE_ROOT', dir); + mkdirSync(join(dir, provider.provider)); + expect(providerMount(provider.provider)).toBe(dir); + expect(checkProviderHelpers(definition).ok).toBe(provider.supported); + }); + if (!provider.supported || provider.provider === 'slack') continue; + it(`${provider.provider}: consumes its upstream client through the mock transport`, async () => { + vi.stubEnv(provider.mockEnv, '1'); + const dir = temporary(); + const catalog = WRITEBACK_PATH_CATALOG as Record>; + const [resource, variants] = Object.entries(catalog[provider.provider] ?? {})[0] ?? []; + const params = Object.fromEntries((variants?.[0]?.params ?? []).map(param => [param, 'fixture'])); + const call: HelperCall = provider.provider === 'stripe' + ? { type: 'effect', provider: 'stripe', verb: 'createInvoice', args: [{ customer: 'cus_fixture' }] } + : { type: 'effect', provider: provider.provider, verb: `${resource}.write`, args: [params, { text: 'smoke' }] }; + await helperWriteback(call, dir, 'run', 'step', signal()); + const record = JSON.parse(readFileSync(join(dir, 'mock-writeback', provider.provider, 'step.json'), 'utf8')); + expect(record.request.provider).toBe(provider.provider); + expect(record.request.body.idempotencyKey).toBe('run:step'); + expect(record.receipt.externalId).toBe('mock-step'); + }); +} + +it('binds named verbs lazily with intact argument snapshots and synchronous paths', () => { + const calls: HelperCall[] = []; + const helpers = createHelpers(call => { calls.push(call); return {} as never; }); + const args = { repo: 'owner/repo', title: 'title', body: 'body' }; + helpers.github.createIssue(args); + expect(calls).toEqual([{ type: 'effect', provider: 'github', verb: 'createIssue', args: [args] }]); + expect(helpers.github.issues.path({ owner: 'owner', repo: 'repo' })).toBe('/github/repos/owner/repo/issues'); +}); + +it('refuses body and bracket uses before any body or journal work', async () => { + vi.stubEnv('RELAYFLOWS_GITHUB_MOCK', ''); + for (const key of ['RELAYFILE_MOUNT_PATH', 'WORKSPACE_ROOT', 'WORKFORCE_SANDBOX_ROOT', 'RELAYFILE_MOUNT_ROOT', 'RELAYFILE_ROOT']) vi.stubEnv(key, ''); + let entered = false; + const handle = flow('missing-github', async f => { entered = true; await f['github'].createIssue({ repo: 'a/b', title: 'x', body: '' }); f.done('success'); }); + await expect(executeAuthoredFlow(handle, new JournalClient('/must-not-connect'))).rejects.toMatchObject({ code: 'helper_provider.mount_required' }); + expect(entered).toBe(false); +}); + +it('preserves collection drafts, item paths, and confirmed receipts in mount mode', async () => { + const dir = temporary(); mkdirSync(join(dir, 'github')); vi.stubEnv('RELAYFILE_MOUNT_PATH', dir); vi.stubEnv('RELAYFLOWS_GITHUB_MOCK', ''); + const write = vi.mocked(vfs.writeJsonFile).mockResolvedValue({ path: 'delivered', absolutePath: 'delivered', deliveryStatus: 'confirmed', receipt: { externalId: '123' } }); + await expect(helperWriteback(github, dir, 'run', 'issue', signal())).resolves.toMatchObject({ status: 'confirmed', id: '123' }); + expect(write.mock.calls[0]?.[3]).toMatch(/^\/github\/repos\/owner\/repo\/issues\/draft-[a-f0-9]+\.json$/); + expect(write.mock.calls[0]?.[4]).toEqual({ title: 'hi', body: 'body', idempotencyKey: 'run:issue' }); + await helperWriteback({ ...github, verb: 'updateRef', args: [{ owner: 'owner', repo: 'repo', ref: 'branch', sha: '123' }] }, dir, 'run', 'ref', signal()); + expect(write.mock.calls[1]?.[3]).toMatch(/refs\/refs%2Fheads%2Fbranch\.json$/); +}); + +it('never confirms pending delivery or permits upstream created() to swallow a pending error', async () => { + const dir = temporary(); mkdirSync(join(dir, 'github')); vi.stubEnv('RELAYFILE_MOUNT_PATH', dir); vi.stubEnv('RELAYFLOWS_GITHUB_MOCK', ''); + const write = vi.mocked(vfs.writeJsonFile).mockResolvedValue({ path: 'pending', absolutePath: 'pending', deliveryStatus: 'pending' }); + await expect(helperWriteback(github, dir, 'run', 'step', signal())).rejects.toThrow('pending'); + write.mockRejectedValue(new vfs.RelayfileWritebackPendingError({ provider: 'github', operation: 'write.issues', path: 'pending', opId: 'op', status: 'pending', timeoutMs: 1 })); + await expect(helperWriteback(github, dir, 'run', 'step', signal())).rejects.toThrow(); +}); + +it('blocks Notion append in mount mode and tests its lowering in mock mode', async () => { + const dir = temporary(); mkdirSync(join(dir, 'notion')); vi.stubEnv('RELAYFILE_MOUNT_PATH', dir); vi.stubEnv('RELAYFLOWS_NOTION_MOCK', ''); + const call: HelperCall = { type: 'effect', provider: 'notion', verb: 'appendBlock', args: [{ pageId: 'page', block: { type: 'paragraph' } }] }; + await expect(helperWriteback(call, dir, 'run', 'step', signal())).rejects.toThrow('no upstream'); + vi.stubEnv('RELAYFLOWS_NOTION_MOCK', '1'); + await expect(helperWriteback(call, dir, 'run', 'step', signal())).resolves.toMatchObject({ status: 'confirmed' }); +}); + +it('rejects cancellation and prototype method invocation', async () => { + const dir = temporary(); vi.stubEnv('RELAYFLOWS_GITHUB_MOCK', '1'); + const controller = new AbortController(); controller.abort(new Error('cancelled')); + await expect(helperWriteback(github, dir, 'run', 'step', controller.signal)).rejects.toThrow('cancelled'); + await expect(helperWriteback({ ...github, verb: 'constructor' }, dir, 'run', 'step', signal())).rejects.toThrow('Unknown helper verb'); +}); + +it('does not mistake memory.recall or unrelated object properties for helper namespaces', () => { + const body = async (f: import('@relayflows/surface').Ctx) => { await f.memory.recall('query'); const item = { x: 1 }; return item.x; }; + expect(checkProviderHelpers({ body }).ok).toBe(true); +}); diff --git a/packages/sdk/tests/preflight.test.ts b/packages/sdk/tests/preflight.test.ts index 501a0d9c3..a263b1487 100644 --- a/packages/sdk/tests/preflight.test.ts +++ b/packages/sdk/tests/preflight.test.ts @@ -367,6 +367,8 @@ describe('preflight: CLI resolution and refusal predicates', () => { // not read as coming from this test alone. it('reaches every declared refusal kind, with the converse held by the type', async () => { const scenarios = [ + preflightHelpers({ header: { tools: { github: true } } }, {}), + preflightHelpers({ header: { tools: { airtable: true } } }, {}), preflightHelpers({ header: { tools: { slack: true } }, body() {} }, { slackMount: false, slackMock: false }), preflightHelpers({ header: { tools: { slack: true } }, body() {} }, { slackToken: 'present', slackMount: false, slackMock: false }), await preflight(flow({ id: 'a', type: 'deterministic', command: 'x' }), { diff --git a/packages/sdk/tsconfig.tests.json b/packages/sdk/tsconfig.tests.json index b20bf50f0..5ef64519e 100644 --- a/packages/sdk/tsconfig.tests.json +++ b/packages/sdk/tsconfig.tests.json @@ -15,6 +15,8 @@ "tests/deploy.test.ts", "tests/run-from-digest.test.ts", "tests/run-digest-live.test.ts", + "tests/helpers-fanout.test.ts", + "tests/authored-helpers.test.ts", "tests/fixtures/build.flow.ts", "tests/typed-output.test.ts", "tests/mcp.test.ts", diff --git a/packages/surface/package-lock.json b/packages/surface/package-lock.json index 448dab2b8..e1867bee8 100644 --- a/packages/surface/package-lock.json +++ b/packages/surface/package-lock.json @@ -18,6 +18,9 @@ }, "engines": { "node": ">=20.19.0 || >=22.12.0" + }, + "peerDependencies": { + "@relayfile/relay-helpers": "0.4.11" } }, "node_modules/@esbuild/aix-ppc64": { @@ -423,6 +426,19 @@ "hono": "^4" } }, + "node_modules/@isaacs/fs-minipass": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@isaacs/fs-minipass/-/fs-minipass-4.0.1.tgz", + "integrity": "sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w==", + "license": "ISC", + "peer": true, + "dependencies": { + "minipass": "^7.0.4" + }, + "engines": { + "node": ">=18.0.0" + } + }, "node_modules/@jridgewell/sourcemap-codec": { "version": "1.6.0", "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.6.0.tgz", @@ -490,6 +506,159 @@ "node": "^22.20 || ^24.12 || >=25" } }, + "node_modules/@relayfile/adapter-core": { + "version": "0.5.24", + "resolved": "https://registry.npmjs.org/@relayfile/adapter-core/-/adapter-core-0.5.24.tgz", + "integrity": "sha512-bOQRuBoAw2RlYs30RtKsOvXlXzcRx4owhHdj384hPrznBIY3U4ZYcb4pVfzreW9TStHEiX7EguUyqhD9g8DJmA==", + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "@scalar/postman-to-openapi": "^0.6.0", + "cheerio": "^1.2.0", + "minimatch": "^10.0.3", + "yaml": "^2.8.1" + }, + "bin": { + "adapter-core": "dist/src/cli.js" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@relayfile/sdk": ">=0.6.0 <1" + } + }, + "node_modules/@relayfile/adapter-linear": { + "version": "0.4.12", + "resolved": "https://registry.npmjs.org/@relayfile/adapter-linear/-/adapter-linear-0.4.12.tgz", + "integrity": "sha512-obICrTmIkVKXX0vAjGiezOK/cu3iUH6i67fuKLe7Fqcp/Upwrsr70Ap68YxyMvpNOvE4TdbbM1vU+2uGXwM5sg==", + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "@relayfile/adapter-core": "^0.5.18" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@relayfile/sdk": ">=0.6.0 <1" + } + }, + "node_modules/@relayfile/adapter-reddit": { + "version": "0.2.9", + "resolved": "https://registry.npmjs.org/@relayfile/adapter-reddit/-/adapter-reddit-0.2.9.tgz", + "integrity": "sha512-/ZWkr4SguRRCk4fY6DEtP5NVUHSMWP66GtMou4NJOcf5PaxYSa0ceSVxAN/IcTBG6JqdtbqB4IoWn8V9d/JC3g==", + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "@relayfile/adapter-core": "^0.5.15" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@relayfile/sdk": ">=0.6.0 <1" + } + }, + "node_modules/@relayfile/core": { + "version": "0.10.57", + "resolved": "https://registry.npmjs.org/@relayfile/core/-/core-0.10.57.tgz", + "integrity": "sha512-oKwNJKGovzdl/6IF3sSVZFsiG2ITH7jXnmFgxbCIYPZ+J7QsQ2zvBhT35sPGiNcD4FfiMpyX9OB8LJXOefNdcQ==", + "license": "Apache-2.0", + "peer": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/@relayfile/mount-darwin-arm64": { + "version": "0.10.57", + "resolved": "https://registry.npmjs.org/@relayfile/mount-darwin-arm64/-/mount-darwin-arm64-0.10.57.tgz", + "integrity": "sha512-yCcjQ7HoCJECnjtZeG7eZwLXa4RAT3RAVAAymdIqKZbMe3wbkF1kg3YNQHuRqYsDEIQzGkA4k5AsITmG1j2PfQ==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "peer": true + }, + "node_modules/@relayfile/mount-darwin-x64": { + "version": "0.10.57", + "resolved": "https://registry.npmjs.org/@relayfile/mount-darwin-x64/-/mount-darwin-x64-0.10.57.tgz", + "integrity": "sha512-5Ze1a16Mxc2PTj2Q+AdtRZIMKQ4sxJIIXwPjdWD+9rTMEQeqs7xzSZ1WRIjI89FzyNHBjx/+gzD9FK8Kjuc7Yg==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "peer": true + }, + "node_modules/@relayfile/mount-linux-arm64": { + "version": "0.10.57", + "resolved": "https://registry.npmjs.org/@relayfile/mount-linux-arm64/-/mount-linux-arm64-0.10.57.tgz", + "integrity": "sha512-+2MhcDwzb7KSC3qjKWOW+9EnZ8mFKJiVns5fiJwO1oThiAd3CnnCfJzYkbEWRKIL0dH5wg4npdayF23DiBtB5w==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "peer": true + }, + "node_modules/@relayfile/mount-linux-x64": { + "version": "0.10.57", + "resolved": "https://registry.npmjs.org/@relayfile/mount-linux-x64/-/mount-linux-x64-0.10.57.tgz", + "integrity": "sha512-Y0OGuGEpglQsUrrR0uzjBzqFyrM8wU5jG1tELJ8LBNhedNUEUupGKD7ixs+i4k9kR8Q68voVsQTfzdgqzW3M+Q==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "peer": true + }, + "node_modules/@relayfile/relay-helpers": { + "version": "0.4.11", + "resolved": "https://registry.npmjs.org/@relayfile/relay-helpers/-/relay-helpers-0.4.11.tgz", + "integrity": "sha512-J9S2L+dVRQcxv32BxMq+xEqpBJoG1mNzo1E/CCXY4LYUSl0euZD0ZayaFMcpBhGqx+2IDNiSlGjFun/YQk9OqA==", + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "@relayfile/adapter-core": "^0.5.15", + "@relayfile/adapter-linear": "^0.4.11", + "@relayfile/adapter-reddit": "^0.2.9" + } + }, + "node_modules/@relayfile/sdk": { + "version": "0.10.57", + "resolved": "https://registry.npmjs.org/@relayfile/sdk/-/sdk-0.10.57.tgz", + "integrity": "sha512-9AJIEyyruqVogbdqVfY7q0pjuXJy8IhzxR/1pvwWJiybU1BZO8LFNurlFePTAo/uhLG9cshr0YJRBxv2WbG0Sg==", + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "@relayfile/core": "0.10.57", + "ignore": "^7.0.5", + "tar": "^7.5.10" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@relayfile/mount-darwin-arm64": "0.10.57", + "@relayfile/mount-darwin-x64": "0.10.57", + "@relayfile/mount-linux-arm64": "0.10.57", + "@relayfile/mount-linux-x64": "0.10.57" + } + }, "node_modules/@rollup/rollup-android-arm-eabi": { "version": "4.63.1", "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.63.1.tgz", @@ -879,6 +1048,40 @@ "win32" ] }, + "node_modules/@scalar/helpers": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/@scalar/helpers/-/helpers-0.5.1.tgz", + "integrity": "sha512-9VvPfv8b+YZVIFwR3SWeq4Y8ij/kU3/kf2M6NKcbf2iVyh63d8s0ssap5m/nOhiz/Puidv/29MAJlJCA0LRssA==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=22" + } + }, + "node_modules/@scalar/openapi-types": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/@scalar/openapi-types/-/openapi-types-0.7.0.tgz", + "integrity": "sha512-kN0PwlJW0de4bwQ4ib+mBHzKJUvBCyR/gwU4zLEq6SCbj+GfgYUh+2a0/yl1WYVUiSkkwFsHjfmQ8KjhR3HK0Q==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=22" + } + }, + "node_modules/@scalar/postman-to-openapi": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/@scalar/postman-to-openapi/-/postman-to-openapi-0.6.3.tgz", + "integrity": "sha512-Y/tMuRZG34wEfpTxDfXFp5o2X3ibb5ojGWupGJ9ZxkThCx7rOGydnszJPzEbgDK3eF6nJ6UuE7bCTpIEutYnPw==", + "license": "MIT", + "peer": true, + "dependencies": { + "@scalar/helpers": "0.5.1", + "@scalar/openapi-types": "0.7.0" + }, + "engines": { + "node": ">=22" + } + }, "node_modules/@types/estree": { "version": "1.0.9", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", @@ -1082,6 +1285,16 @@ "node": ">=12" } }, + "node_modules/balanced-match": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "license": "MIT", + "peer": true, + "engines": { + "node": "18 || 20 || >=22" + } + }, "node_modules/body-parser": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.3.0.tgz", @@ -1119,6 +1332,26 @@ "url": "https://opencollective.com/express" } }, + "node_modules/boolbase": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", + "integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==", + "license": "ISC", + "peer": true + }, + "node_modules/brace-expansion": { + "version": "5.0.9", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.9.tgz", + "integrity": "sha512-ScQ4IuvIEF1TMlP7Zt+vjJ//9zlPb2SDcxWxM3bk8s6t6GGdJ7KO1dCcTidOPJKePW30LE/2cT7wCyPho9/Wxg==", + "license": "MIT", + "peer": true, + "dependencies": { + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "20 || >=22" + } + }, "node_modules/bytes": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", @@ -1194,6 +1427,60 @@ "node": ">= 16" } }, + "node_modules/cheerio": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/cheerio/-/cheerio-1.2.0.tgz", + "integrity": "sha512-WDrybc/gKFpTYQutKIK6UvfcuxijIZfMfXaYm8NMsPQxSYvf+13fXUJ4rztGGbJcBQ/GF55gvrZ0Bc0bj/mqvg==", + "license": "MIT", + "peer": true, + "dependencies": { + "cheerio-select": "^2.1.0", + "dom-serializer": "^2.0.0", + "domhandler": "^5.0.3", + "domutils": "^3.2.2", + "encoding-sniffer": "^0.2.1", + "htmlparser2": "^10.1.0", + "parse5": "^7.3.0", + "parse5-htmlparser2-tree-adapter": "^7.1.0", + "parse5-parser-stream": "^7.1.2", + "undici": "^7.19.0", + "whatwg-mimetype": "^4.0.0" + }, + "engines": { + "node": ">=20.18.1" + }, + "funding": { + "url": "https://github.com/cheeriojs/cheerio?sponsor=1" + } + }, + "node_modules/cheerio-select": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/cheerio-select/-/cheerio-select-2.1.0.tgz", + "integrity": "sha512-9v9kG0LvzrlcungtnJtpGNxY+fzECQKhK4EGJX2vByejiMX84MFNQw4UxPJl3bFbTMw+Dfs37XaIkCwTZfLh4g==", + "license": "BSD-2-Clause", + "peer": true, + "dependencies": { + "boolbase": "^1.0.0", + "css-select": "^5.1.0", + "css-what": "^6.1.0", + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3", + "domutils": "^3.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, + "node_modules/chownr": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-3.0.0.tgz", + "integrity": "sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g==", + "license": "BlueOak-1.0.0", + "peer": true, + "engines": { + "node": ">=18" + } + }, "node_modules/content-disposition": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.1.0.tgz", @@ -1265,6 +1552,36 @@ "node": ">= 8" } }, + "node_modules/css-select": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/css-select/-/css-select-5.2.2.tgz", + "integrity": "sha512-TizTzUddG/xYLA3NXodFM0fSbNizXjOKhqiQQwvhlspadZokn1KDy0NZFS0wuEubIYAV5/c1/lAr0TaaFXEXzw==", + "license": "BSD-2-Clause", + "peer": true, + "dependencies": { + "boolbase": "^1.0.0", + "css-what": "^6.1.0", + "domhandler": "^5.0.2", + "domutils": "^3.0.1", + "nth-check": "^2.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, + "node_modules/css-what": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/css-what/-/css-what-6.2.2.tgz", + "integrity": "sha512-u/O3vwbptzhMs3L1fQE82ZSLHQQfto5gyZzwteVIEyeaY5Fc7R4dapF/BvRoSYFeqfBk4m0V1Vafq5Pjv25wvA==", + "license": "BSD-2-Clause", + "peer": true, + "engines": { + "node": ">= 6" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, "node_modules/debug": { "version": "4.4.3", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", @@ -1301,6 +1618,65 @@ "node": ">= 0.8" } }, + "node_modules/dom-serializer": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz", + "integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==", + "license": "MIT", + "peer": true, + "dependencies": { + "domelementtype": "^2.3.0", + "domhandler": "^5.0.2", + "entities": "^4.2.0" + }, + "funding": { + "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1" + } + }, + "node_modules/domelementtype": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz", + "integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ], + "license": "BSD-2-Clause", + "peer": true + }, + "node_modules/domhandler": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz", + "integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==", + "license": "BSD-2-Clause", + "peer": true, + "dependencies": { + "domelementtype": "^2.3.0" + }, + "engines": { + "node": ">= 4" + }, + "funding": { + "url": "https://github.com/fb55/domhandler?sponsor=1" + } + }, + "node_modules/domutils": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-3.2.2.tgz", + "integrity": "sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==", + "license": "BSD-2-Clause", + "peer": true, + "dependencies": { + "dom-serializer": "^2.0.0", + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3" + }, + "funding": { + "url": "https://github.com/fb55/domutils?sponsor=1" + } + }, "node_modules/dunder-proto": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", @@ -1330,6 +1706,46 @@ "node": ">= 0.8" } }, + "node_modules/encoding-sniffer": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/encoding-sniffer/-/encoding-sniffer-0.2.1.tgz", + "integrity": "sha512-5gvq20T6vfpekVtqrYQsSCFZ1wEg5+wW0/QaZMWkFr6BqD3NfKs0rLCx4rrVlSWJeZb5NBJgVLswK/w2MWU+Gw==", + "license": "MIT", + "peer": true, + "dependencies": { + "iconv-lite": "^0.6.3", + "whatwg-encoding": "^3.1.1" + }, + "funding": { + "url": "https://github.com/fb55/encoding-sniffer?sponsor=1" + } + }, + "node_modules/encoding-sniffer/node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "license": "MIT", + "peer": true, + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/entities": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", + "license": "BSD-2-Clause", + "peer": true, + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, "node_modules/es-define-property": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", @@ -1691,6 +2107,39 @@ "node": ">=16.9.0" } }, + "node_modules/htmlparser2": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-10.1.0.tgz", + "integrity": "sha512-VTZkM9GWRAtEpveh7MSF6SjjrpNVNNVJfFup7xTY3UpFtm67foy9HDVXneLtFVt4pMz5kZtgNcvCniNFb1hlEQ==", + "funding": [ + "https://github.com/fb55/htmlparser2?sponsor=1", + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ], + "license": "MIT", + "peer": true, + "dependencies": { + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3", + "domutils": "^3.2.2", + "entities": "^7.0.1" + } + }, + "node_modules/htmlparser2/node_modules/entities": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/entities/-/entities-7.0.1.tgz", + "integrity": "sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA==", + "license": "BSD-2-Clause", + "peer": true, + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, "node_modules/http-errors": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", @@ -1727,6 +2176,16 @@ "url": "https://opencollective.com/express" } }, + "node_modules/ignore": { + "version": "7.0.9", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.9.tgz", + "integrity": "sha512-brTTsvFRt5C1gGHtPst/281UjPD5t9fBqbgoMPlVWy11ZLTPfu7HxK4ZYqO9H7o/yC9rSTCI85EaQ4OoY12qYw==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 4" + } + }, "node_modules/inherits": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", @@ -1860,6 +2319,45 @@ "url": "https://opencollective.com/express" } }, + "node_modules/minimatch": { + "version": "10.2.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.6.tgz", + "integrity": "sha512-vpLQEs+VLCr1nU0BXS07maYoFwlDAH0gngQuuttxIwutDFEMHq2blX+8vpgxDdK3J1PwjCJiep77OitTZ4Ll1A==", + "license": "BlueOak-1.0.0", + "peer": true, + "dependencies": { + "brace-expansion": "^5.0.8" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/minipass": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.3.tgz", + "integrity": "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==", + "license": "BlueOak-1.0.0", + "peer": true, + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/minizlib": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-3.1.0.tgz", + "integrity": "sha512-KZxYo1BUkWD2TVFLr0MQoM8vUUigWD3LlD83a/75BqC+4qE0Hb1Vo5v1FgcfaNXvfXzr+5EhQ6ing/CaBijTlw==", + "license": "MIT", + "peer": true, + "dependencies": { + "minipass": "^7.1.2" + }, + "engines": { + "node": ">= 18" + } + }, "node_modules/ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", @@ -1914,6 +2412,19 @@ "url": "https://opencollective.com/express" } }, + "node_modules/nth-check": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz", + "integrity": "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==", + "license": "BSD-2-Clause", + "peer": true, + "dependencies": { + "boolbase": "^1.0.0" + }, + "funding": { + "url": "https://github.com/fb55/nth-check?sponsor=1" + } + }, "node_modules/object-assign": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", @@ -1956,6 +2467,59 @@ "wrappy": "1" } }, + "node_modules/parse5": { + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.3.0.tgz", + "integrity": "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==", + "license": "MIT", + "peer": true, + "dependencies": { + "entities": "^6.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/parse5-htmlparser2-tree-adapter": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/parse5-htmlparser2-tree-adapter/-/parse5-htmlparser2-tree-adapter-7.1.0.tgz", + "integrity": "sha512-ruw5xyKs6lrpo9x9rCZqZZnIUntICjQAd0Wsmp396Ul9lN/h+ifgVV1x1gZHi8euej6wTfpqX8j+BFQxF0NS/g==", + "license": "MIT", + "peer": true, + "dependencies": { + "domhandler": "^5.0.3", + "parse5": "^7.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/parse5-parser-stream": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/parse5-parser-stream/-/parse5-parser-stream-7.1.2.tgz", + "integrity": "sha512-JyeQc9iwFLn5TbvvqACIF/VXG6abODeB3Fwmv/TGdLk2LfbWkaySGY72at4+Ty7EkPZj854u4CrICqNk2qIbow==", + "license": "MIT", + "peer": true, + "dependencies": { + "parse5": "^7.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/parse5/node_modules/entities": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz", + "integrity": "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==", + "license": "BSD-2-Clause", + "peer": true, + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, "node_modules/parseurl": { "version": "1.3.3", "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", @@ -2370,6 +2934,23 @@ "dev": true, "license": "MIT" }, + "node_modules/tar": { + "version": "7.5.22", + "resolved": "https://registry.npmjs.org/tar/-/tar-7.5.22.tgz", + "integrity": "sha512-MFO/QzvtAOmJbkhOaCTvbGcFN9L9b+JunIsDwaKljSOdcLMea3NJ1k9Usz/rjdfSXTq4dfzfeS7W4p4YOAAHeA==", + "license": "BlueOak-1.0.0", + "peer": true, + "dependencies": { + "@isaacs/fs-minipass": "^4.0.0", + "chownr": "^3.0.0", + "minipass": "^7.1.2", + "minizlib": "^3.1.0", + "yallist": "^5.0.0" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/tinybench": { "version": "2.9.0", "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", @@ -2468,6 +3049,23 @@ "node": ">=14.17" } }, + "node_modules/undici": { + "version": "7.29.1", + "resolved": "https://registry.npmjs.org/undici/-/undici-7.29.1.tgz", + "integrity": "sha512-RYONW2MeafgYlkVOKYKkA/Ag7BmXqgIWCa8t1m0JcxrQg9pI9lEqRhAOruOBCbAohOa/gkCF+iPi9hrgvTzu6Q==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=20.18.1" + } + }, + "node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "dev": true, + "license": "MIT" + }, "node_modules/unpipe": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", @@ -2486,13 +3084,6 @@ "node": ">= 0.8" } }, - "node_modules/undici-types": { - "version": "6.21.0", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", - "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", - "dev": true, - "license": "MIT" - }, "node_modules/vite": { "version": "5.4.21", "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.21.tgz", @@ -2642,6 +3233,43 @@ } } }, + "node_modules/whatwg-encoding": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-3.1.1.tgz", + "integrity": "sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==", + "deprecated": "Use @exodus/bytes instead for a more spec-conformant and faster implementation", + "license": "MIT", + "peer": true, + "dependencies": { + "iconv-lite": "0.6.3" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/whatwg-encoding/node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "license": "MIT", + "peer": true, + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/whatwg-mimetype": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-4.0.0.tgz", + "integrity": "sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=18" + } + }, "node_modules/which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", @@ -2680,6 +3308,32 @@ "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", "license": "ISC" }, + "node_modules/yallist": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-5.0.0.tgz", + "integrity": "sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==", + "license": "BlueOak-1.0.0", + "peer": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/yaml": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.9.0.tgz", + "integrity": "sha512-2AvhNX3mb8zd6Zy7INTtSpl1F15HW6Wnqj0srWlkKLcpYl/gMIMJiyuGq2KeI2YFxUPjdlB+3Lc10seMLtL4cA==", + "license": "ISC", + "peer": true, + "bin": { + "yaml": "bin.mjs" + }, + "engines": { + "node": ">= 14.6" + }, + "funding": { + "url": "https://github.com/sponsors/eemeli" + } + }, "node_modules/zod": { "version": "4.6.2", "resolved": "https://registry.npmjs.org/zod/-/zod-4.6.2.tgz", diff --git a/packages/surface/src/effect-transport.ts b/packages/surface/src/effect-transport.ts new file mode 100644 index 000000000..3caaeb463 --- /dev/null +++ b/packages/surface/src/effect-transport.ts @@ -0,0 +1,59 @@ +import type { RelayClientOptions, RelayTransport } from '@relayfile/relay-helpers/transport'; +import type { Step } from './step.js'; + +/** Promise-returning client verbs become lazy, journal-owned steps. */ +export type JournalHelper = { + [K in keyof T]: T[K] extends (...args: infer A) => Promise + ? (...args: A) => Step + : T[K] extends (...args: infer A) => infer R ? (...args: A) => R + : T[K] extends object ? JournalHelper : T[K]; +}; +export interface HelperCall { + type: 'effect'; + provider: string; + verb: string; + args: unknown[]; +} +export type EffectDispatcher = (call: HelperCall) => Step; +export interface UnavailableHelper { readonly available: false } +export type HelperFactory = (options: RelayClientOptions) => object; + +const unavailableTransport: RelayTransport = { + async read() { throw new Error('Helper I/O requires a journaled step'); }, + async list() { throw new Error('Helper I/O requires a journaled step'); }, + async write() { throw new Error('Helper I/O requires a journaled step'); }, +}; + +export function bindHelper(provider: string, factory: (options: RelayClientOptions) => T, dispatch: EffectDispatcher): JournalHelper; +export function bindHelper(provider: string, factory: undefined, dispatch: EffectDispatcher): UnavailableHelper; +export function bindHelper(provider: string, factory: HelperFactory | undefined, dispatch: EffectDispatcher): object { + if (!factory) return Object.freeze({ available: false }); + function wrap(client: object, prefix = ''): object { + return Object.fromEntries(Object.entries(client).map(([key, value]) => { + const verb = `${prefix}${key}`; + if (typeof value === 'function') return [key, key === 'path' + ? value.bind(client) + : (...args: unknown[]) => { + while (args.length && args.at(-1) === undefined) args.pop(); + return dispatch({ type: 'effect', provider, verb, args }); + }]; + return [key, value && typeof value === 'object' ? wrap(value, `${verb}.`) : value]; + })); + } + return wrap(factory({ transport: unavailableTransport })); +} + +/** Resolve only own, generated client methods; never arbitrary prototype members. */ +export async function invokeHelper(factory: HelperFactory, call: HelperCall, transport: RelayTransport): Promise { + let target: unknown = factory({ transport }); + const parts = call.verb.split('.'); + for (const part of parts.slice(0, -1)) { + if (!target || typeof target !== 'object' || !Object.hasOwn(target, part)) throw new Error('Unknown helper resource'); + target = (target as Record)[part]; + } + const verb = parts.at(-1)!; + if (!target || typeof target !== 'object' || !Object.hasOwn(target, verb) || verb === 'path') throw new Error('Unknown helper verb'); + const method = (target as Record)[verb]; + if (typeof method !== 'function') throw new Error('Unknown helper verb'); + return await method.apply(target, call.args) ?? null; +} diff --git a/packages/surface/src/flow.ts b/packages/surface/src/flow.ts index 4eec2c712..10193c7d7 100644 --- a/packages/surface/src/flow.ts +++ b/packages/surface/src/flow.ts @@ -1,3 +1,6 @@ +import { helperProviders } from "./helpers/providers.js"; +import type { Helpers } from "./helpers/index.js"; +type HelperTools = Partial>; import type { Ctx } from "./context.js"; import { webhook, type TriggerSource } from "./triggers.js"; @@ -8,7 +11,7 @@ export interface FlowHeader { identity?: string; memory?: { script?: boolean; agent?: boolean }; budget?: string | { tokens?: number; dollars?: number; wallclock?: string }; - tools?: { slack?: boolean; relayfile?: string[]; mcp?: string[] }; + tools?: HelperTools & { relayfile?: string[]; mcp?: string[] }; workspace?: string; } @@ -19,7 +22,7 @@ export interface ReadonlyFlowHeader { readonly identity?: string; readonly memory?: Readonly<{ script?: boolean; agent?: boolean }>; readonly budget?: string | Readonly<{ tokens?: number; dollars?: number; wallclock?: string }>; - readonly tools?: Readonly<{ + readonly tools?: Readonly header.tools?.[p.namespace] !== undefined) + .map(p => [p.namespace, header.tools![p.namespace]])), ...(header.tools.relayfile === undefined ? {} : { relayfile: Object.freeze([...header.tools.relayfile]) }), @@ -224,10 +228,10 @@ function assertFlowHeader(value: unknown, flowName: string): asserts value is Fl assertHeaderObject(value.tools, `${at}.tools`); assertKnownKeys( value.tools, - ["relayfile", "mcp", "slack"], + ["relayfile", "mcp", ...helperProviders.map(p => p.namespace)], `${at}.tools`, ); - assertOptionalBoolean(value.tools, "slack", `${at}.tools`); + for (const { namespace } of helperProviders) assertOptionalBoolean(value.tools, namespace, `${at}.tools`); assertOptionalStringArray(value.tools, "relayfile", `${at}.tools`); assertOptionalStringArray(value.tools, "mcp", `${at}.tools`); } diff --git a/packages/surface/src/helper-clients.ts b/packages/surface/src/helper-clients.ts new file mode 100644 index 000000000..03a54fdf2 --- /dev/null +++ b/packages/surface/src/helper-clients.ts @@ -0,0 +1,30 @@ +import { githubClient as upstreamGithub, notionClient as upstreamNotion, created } from '@relayfile/relay-helpers'; +import type { RelayClientOptions } from '@relayfile/relay-helpers/transport'; + +/** Accept the conventional owner/repo shorthand as well as upstream arguments. */ +export function githubClient(options: RelayClientOptions) { + const client = upstreamGithub(options); + return { ...client, createIssue(args: { owner?: string; repo: string; title: string; body: string; labels?: string[] }) { + const [owner, repo, extra] = args.repo.split('/'); + if (!args.owner && (!owner || !repo || extra)) throw new Error('github.createIssue requires repo: "owner/repo" or an explicit owner'); + return client.createIssue({ ...args, owner: args.owner ?? owner!, repo: args.owner ? args.repo : repo! }); + } }; +} + +export function stripeClient(options: RelayClientOptions) { + return { createInvoice(args: { customer: string; auto_advance?: boolean; collection_method?: 'charge_automatically' | 'send_invoice'; days_until_due?: number; description?: string; metadata?: Record }) { + if (!options.transport) throw new Error('Stripe requires a journal transport'); + return created(options.transport.write({ provider: 'stripe', resource: 'invoices', + parameters: {}, path: '/stripe/invoices', body: args })); + } }; +} + +export function notionClient(options: RelayClientOptions) { + return { ...upstreamNotion(options), async appendBlock(args: { pageId: string; block: Record }) { + if (!options.transport) throw new Error('Notion requires a journal transport'); + // The adapter does not yet support this route. The runtime refuses it in + // mount mode, while mock mode can exercise authoring and effect lowering. + return created(options.transport.write({ provider: 'notion', resource: 'blocks', + parameters: { pageId: args.pageId }, path: `/notion/pages/${encodeURIComponent(args.pageId)}/blocks`, body: { children: [args.block] } })); + } }; +} diff --git a/packages/surface/src/helpers/airtable.ts b/packages/surface/src/helpers/airtable.ts new file mode 100644 index 000000000..e62767d26 --- /dev/null +++ b/packages/surface/src/helpers/airtable.ts @@ -0,0 +1,9 @@ +// GENERATED by scripts/generate-helpers.mjs — do not edit. +// Run `npm run gen --prefix packages/surface` from the repository root. + +import { bindHelper, type EffectDispatcher, type JournalHelper, type UnavailableHelper } from "../effect-transport.js"; + +export type AirtableHelper = UnavailableHelper; + +export const createAirtableHelper = (dispatch: EffectDispatcher): AirtableHelper => + bindHelper("airtable", undefined, dispatch); diff --git a/packages/surface/src/helpers/asana.ts b/packages/surface/src/helpers/asana.ts new file mode 100644 index 000000000..35e6d2860 --- /dev/null +++ b/packages/surface/src/helpers/asana.ts @@ -0,0 +1,10 @@ +// GENERATED by scripts/generate-helpers.mjs — do not edit. +// Run `npm run gen --prefix packages/surface` from the repository root. + +import { asanaClient } from "@relayfile/relay-helpers"; +import { bindHelper, type EffectDispatcher, type JournalHelper } from "../effect-transport.js"; + +export type AsanaHelper = JournalHelper>; + +export const createAsanaHelper = (dispatch: EffectDispatcher): AsanaHelper => + bindHelper("asana", asanaClient, dispatch); diff --git a/packages/surface/src/helpers/azure-blob.ts b/packages/surface/src/helpers/azure-blob.ts new file mode 100644 index 000000000..508d36d04 --- /dev/null +++ b/packages/surface/src/helpers/azure-blob.ts @@ -0,0 +1,10 @@ +// GENERATED by scripts/generate-helpers.mjs — do not edit. +// Run `npm run gen --prefix packages/surface` from the repository root. + +import { azureBlobClient } from "@relayfile/relay-helpers"; +import { bindHelper, type EffectDispatcher, type JournalHelper } from "../effect-transport.js"; + +export type AzureBlobHelper = JournalHelper>; + +export const createAzureBlobHelper = (dispatch: EffectDispatcher): AzureBlobHelper => + bindHelper("azure-blob", azureBlobClient, dispatch); diff --git a/packages/surface/src/helpers/box.ts b/packages/surface/src/helpers/box.ts new file mode 100644 index 000000000..8ed424fac --- /dev/null +++ b/packages/surface/src/helpers/box.ts @@ -0,0 +1,10 @@ +// GENERATED by scripts/generate-helpers.mjs — do not edit. +// Run `npm run gen --prefix packages/surface` from the repository root. + +import { boxClient } from "@relayfile/relay-helpers"; +import { bindHelper, type EffectDispatcher, type JournalHelper } from "../effect-transport.js"; + +export type BoxHelper = JournalHelper>; + +export const createBoxHelper = (dispatch: EffectDispatcher): BoxHelper => + bindHelper("box", boxClient, dispatch); diff --git a/packages/surface/src/helpers/calendly.ts b/packages/surface/src/helpers/calendly.ts new file mode 100644 index 000000000..8287923d9 --- /dev/null +++ b/packages/surface/src/helpers/calendly.ts @@ -0,0 +1,10 @@ +// GENERATED by scripts/generate-helpers.mjs — do not edit. +// Run `npm run gen --prefix packages/surface` from the repository root. + +import { calendlyClient } from "@relayfile/relay-helpers"; +import { bindHelper, type EffectDispatcher, type JournalHelper } from "../effect-transport.js"; + +export type CalendlyHelper = JournalHelper>; + +export const createCalendlyHelper = (dispatch: EffectDispatcher): CalendlyHelper => + bindHelper("calendly", calendlyClient, dispatch); diff --git a/packages/surface/src/helpers/clickup.ts b/packages/surface/src/helpers/clickup.ts new file mode 100644 index 000000000..707f0c8c9 --- /dev/null +++ b/packages/surface/src/helpers/clickup.ts @@ -0,0 +1,10 @@ +// GENERATED by scripts/generate-helpers.mjs — do not edit. +// Run `npm run gen --prefix packages/surface` from the repository root. + +import { clickupClient } from "@relayfile/relay-helpers"; +import { bindHelper, type EffectDispatcher, type JournalHelper } from "../effect-transport.js"; + +export type ClickupHelper = JournalHelper>; + +export const createClickupHelper = (dispatch: EffectDispatcher): ClickupHelper => + bindHelper("clickup", clickupClient, dispatch); diff --git a/packages/surface/src/helpers/clients.ts b/packages/surface/src/helpers/clients.ts new file mode 100644 index 000000000..f74167f7a --- /dev/null +++ b/packages/surface/src/helpers/clients.ts @@ -0,0 +1,48 @@ +// GENERATED by scripts/generate-helpers.mjs — do not edit. +// Run `npm run gen --prefix packages/surface` from the repository root. + +import * as upstream from "@relayfile/relay-helpers"; +import * as custom from "../helper-clients.js"; +import type { HelperFactory } from "../effect-transport.js"; + +export const helperClients: Readonly> = { + "asana": upstream.asanaClient, + "azure-blob": upstream.azureBlobClient, + "box": upstream.boxClient, + "calendly": upstream.calendlyClient, + "clickup": upstream.clickupClient, + "cloudflare": upstream.cloudflareClient, + "confluence": upstream.confluenceClient, + "daytona": upstream.daytonaClient, + "dropbox": upstream.dropboxClient, + "gcs": upstream.gcsClient, + "github": custom.githubClient, + "gitlab": upstream.gitlabClient, + "gmail": upstream.gmailClient, + "google-calendar": upstream.googleCalendarClient, + "google-drive": upstream.googleDriveClient, + "granola": upstream.granolaClient, + "hubspot": upstream.hubspotClient, + "intercom": upstream.intercomClient, + "jira": upstream.jiraClient, + "linear": upstream.linearClient, + "mailgun": upstream.mailgunClient, + "mixpanel": upstream.mixpanelClient, + "notion": custom.notionClient, + "onedrive": upstream.onedriveClient, + "pipedrive": upstream.pipedriveClient, + "postgres": upstream.postgresClient, + "ramp": (options) => upstream.providerClient("ramp", options), + "recall": upstream.recallClient, + "reddit": upstream.redditClient, + "redis": upstream.redisClient, + "s3": upstream.s3Client, + "salesforce": upstream.salesforceClient, + "sendgrid": upstream.sendgridClient, + "sharepoint": upstream.sharepointClient, + "shortcut": upstream.shortcutClient, + "stripe": custom.stripeClient, + "teams": upstream.teamsClient, + "telegram": upstream.telegramClient, + "zendesk": upstream.zendeskClient, +}; diff --git a/packages/surface/src/helpers/cloudflare.ts b/packages/surface/src/helpers/cloudflare.ts new file mode 100644 index 000000000..bafbec1a6 --- /dev/null +++ b/packages/surface/src/helpers/cloudflare.ts @@ -0,0 +1,10 @@ +// GENERATED by scripts/generate-helpers.mjs — do not edit. +// Run `npm run gen --prefix packages/surface` from the repository root. + +import { cloudflareClient } from "@relayfile/relay-helpers"; +import { bindHelper, type EffectDispatcher, type JournalHelper } from "../effect-transport.js"; + +export type CloudflareHelper = JournalHelper>; + +export const createCloudflareHelper = (dispatch: EffectDispatcher): CloudflareHelper => + bindHelper("cloudflare", cloudflareClient, dispatch); diff --git a/packages/surface/src/helpers/confluence.ts b/packages/surface/src/helpers/confluence.ts new file mode 100644 index 000000000..4afbe977d --- /dev/null +++ b/packages/surface/src/helpers/confluence.ts @@ -0,0 +1,10 @@ +// GENERATED by scripts/generate-helpers.mjs — do not edit. +// Run `npm run gen --prefix packages/surface` from the repository root. + +import { confluenceClient } from "@relayfile/relay-helpers"; +import { bindHelper, type EffectDispatcher, type JournalHelper } from "../effect-transport.js"; + +export type ConfluenceHelper = JournalHelper>; + +export const createConfluenceHelper = (dispatch: EffectDispatcher): ConfluenceHelper => + bindHelper("confluence", confluenceClient, dispatch); diff --git a/packages/surface/src/helpers/daytona.ts b/packages/surface/src/helpers/daytona.ts new file mode 100644 index 000000000..af220cbb6 --- /dev/null +++ b/packages/surface/src/helpers/daytona.ts @@ -0,0 +1,10 @@ +// GENERATED by scripts/generate-helpers.mjs — do not edit. +// Run `npm run gen --prefix packages/surface` from the repository root. + +import { daytonaClient } from "@relayfile/relay-helpers"; +import { bindHelper, type EffectDispatcher, type JournalHelper } from "../effect-transport.js"; + +export type DaytonaHelper = JournalHelper>; + +export const createDaytonaHelper = (dispatch: EffectDispatcher): DaytonaHelper => + bindHelper("daytona", daytonaClient, dispatch); diff --git a/packages/surface/src/helpers/docker-hub.ts b/packages/surface/src/helpers/docker-hub.ts new file mode 100644 index 000000000..82fe1b7d5 --- /dev/null +++ b/packages/surface/src/helpers/docker-hub.ts @@ -0,0 +1,9 @@ +// GENERATED by scripts/generate-helpers.mjs — do not edit. +// Run `npm run gen --prefix packages/surface` from the repository root. + +import { bindHelper, type EffectDispatcher, type JournalHelper, type UnavailableHelper } from "../effect-transport.js"; + +export type DockerHubHelper = UnavailableHelper; + +export const createDockerHubHelper = (dispatch: EffectDispatcher): DockerHubHelper => + bindHelper("docker-hub", undefined, dispatch); diff --git a/packages/surface/src/helpers/dropbox.ts b/packages/surface/src/helpers/dropbox.ts new file mode 100644 index 000000000..2c6b9f50e --- /dev/null +++ b/packages/surface/src/helpers/dropbox.ts @@ -0,0 +1,10 @@ +// GENERATED by scripts/generate-helpers.mjs — do not edit. +// Run `npm run gen --prefix packages/surface` from the repository root. + +import { dropboxClient } from "@relayfile/relay-helpers"; +import { bindHelper, type EffectDispatcher, type JournalHelper } from "../effect-transport.js"; + +export type DropboxHelper = JournalHelper>; + +export const createDropboxHelper = (dispatch: EffectDispatcher): DropboxHelper => + bindHelper("dropbox", dropboxClient, dispatch); diff --git a/packages/surface/src/helpers/fathom.ts b/packages/surface/src/helpers/fathom.ts new file mode 100644 index 000000000..59ae6742f --- /dev/null +++ b/packages/surface/src/helpers/fathom.ts @@ -0,0 +1,9 @@ +// GENERATED by scripts/generate-helpers.mjs — do not edit. +// Run `npm run gen --prefix packages/surface` from the repository root. + +import { bindHelper, type EffectDispatcher, type JournalHelper, type UnavailableHelper } from "../effect-transport.js"; + +export type FathomHelper = UnavailableHelper; + +export const createFathomHelper = (dispatch: EffectDispatcher): FathomHelper => + bindHelper("fathom", undefined, dispatch); diff --git a/packages/surface/src/helpers/gcp.ts b/packages/surface/src/helpers/gcp.ts new file mode 100644 index 000000000..2d8970d22 --- /dev/null +++ b/packages/surface/src/helpers/gcp.ts @@ -0,0 +1,9 @@ +// GENERATED by scripts/generate-helpers.mjs — do not edit. +// Run `npm run gen --prefix packages/surface` from the repository root. + +import { bindHelper, type EffectDispatcher, type JournalHelper, type UnavailableHelper } from "../effect-transport.js"; + +export type GcpHelper = UnavailableHelper; + +export const createGcpHelper = (dispatch: EffectDispatcher): GcpHelper => + bindHelper("gcp", undefined, dispatch); diff --git a/packages/surface/src/helpers/gcs.ts b/packages/surface/src/helpers/gcs.ts new file mode 100644 index 000000000..dc0609a9e --- /dev/null +++ b/packages/surface/src/helpers/gcs.ts @@ -0,0 +1,10 @@ +// GENERATED by scripts/generate-helpers.mjs — do not edit. +// Run `npm run gen --prefix packages/surface` from the repository root. + +import { gcsClient } from "@relayfile/relay-helpers"; +import { bindHelper, type EffectDispatcher, type JournalHelper } from "../effect-transport.js"; + +export type GcsHelper = JournalHelper>; + +export const createGcsHelper = (dispatch: EffectDispatcher): GcsHelper => + bindHelper("gcs", gcsClient, dispatch); diff --git a/packages/surface/src/helpers/github.ts b/packages/surface/src/helpers/github.ts new file mode 100644 index 000000000..2814756ca --- /dev/null +++ b/packages/surface/src/helpers/github.ts @@ -0,0 +1,10 @@ +// GENERATED by scripts/generate-helpers.mjs — do not edit. +// Run `npm run gen --prefix packages/surface` from the repository root. + +import { githubClient } from "../helper-clients.js"; +import { bindHelper, type EffectDispatcher, type JournalHelper } from "../effect-transport.js"; + +export type GithubHelper = JournalHelper>; + +export const createGithubHelper = (dispatch: EffectDispatcher): GithubHelper => + bindHelper("github", githubClient, dispatch); diff --git a/packages/surface/src/helpers/gitlab.ts b/packages/surface/src/helpers/gitlab.ts new file mode 100644 index 000000000..3d338f0c8 --- /dev/null +++ b/packages/surface/src/helpers/gitlab.ts @@ -0,0 +1,10 @@ +// GENERATED by scripts/generate-helpers.mjs — do not edit. +// Run `npm run gen --prefix packages/surface` from the repository root. + +import { gitlabClient } from "@relayfile/relay-helpers"; +import { bindHelper, type EffectDispatcher, type JournalHelper } from "../effect-transport.js"; + +export type GitlabHelper = JournalHelper>; + +export const createGitlabHelper = (dispatch: EffectDispatcher): GitlabHelper => + bindHelper("gitlab", gitlabClient, dispatch); diff --git a/packages/surface/src/helpers/gmail.ts b/packages/surface/src/helpers/gmail.ts new file mode 100644 index 000000000..87bebc8c5 --- /dev/null +++ b/packages/surface/src/helpers/gmail.ts @@ -0,0 +1,10 @@ +// GENERATED by scripts/generate-helpers.mjs — do not edit. +// Run `npm run gen --prefix packages/surface` from the repository root. + +import { gmailClient } from "@relayfile/relay-helpers"; +import { bindHelper, type EffectDispatcher, type JournalHelper } from "../effect-transport.js"; + +export type GmailHelper = JournalHelper>; + +export const createGmailHelper = (dispatch: EffectDispatcher): GmailHelper => + bindHelper("gmail", gmailClient, dispatch); diff --git a/packages/surface/src/helpers/google-calendar.ts b/packages/surface/src/helpers/google-calendar.ts new file mode 100644 index 000000000..750a77853 --- /dev/null +++ b/packages/surface/src/helpers/google-calendar.ts @@ -0,0 +1,10 @@ +// GENERATED by scripts/generate-helpers.mjs — do not edit. +// Run `npm run gen --prefix packages/surface` from the repository root. + +import { googleCalendarClient } from "@relayfile/relay-helpers"; +import { bindHelper, type EffectDispatcher, type JournalHelper } from "../effect-transport.js"; + +export type GoogleCalendarHelper = JournalHelper>; + +export const createGoogleCalendarHelper = (dispatch: EffectDispatcher): GoogleCalendarHelper => + bindHelper("google-calendar", googleCalendarClient, dispatch); diff --git a/packages/surface/src/helpers/google-drive.ts b/packages/surface/src/helpers/google-drive.ts new file mode 100644 index 000000000..8ca401245 --- /dev/null +++ b/packages/surface/src/helpers/google-drive.ts @@ -0,0 +1,10 @@ +// GENERATED by scripts/generate-helpers.mjs — do not edit. +// Run `npm run gen --prefix packages/surface` from the repository root. + +import { googleDriveClient } from "@relayfile/relay-helpers"; +import { bindHelper, type EffectDispatcher, type JournalHelper } from "../effect-transport.js"; + +export type GoogleDriveHelper = JournalHelper>; + +export const createGoogleDriveHelper = (dispatch: EffectDispatcher): GoogleDriveHelper => + bindHelper("google-drive", googleDriveClient, dispatch); diff --git a/packages/surface/src/helpers/granola.ts b/packages/surface/src/helpers/granola.ts new file mode 100644 index 000000000..317069328 --- /dev/null +++ b/packages/surface/src/helpers/granola.ts @@ -0,0 +1,10 @@ +// GENERATED by scripts/generate-helpers.mjs — do not edit. +// Run `npm run gen --prefix packages/surface` from the repository root. + +import { granolaClient } from "@relayfile/relay-helpers"; +import { bindHelper, type EffectDispatcher, type JournalHelper } from "../effect-transport.js"; + +export type GranolaHelper = JournalHelper>; + +export const createGranolaHelper = (dispatch: EffectDispatcher): GranolaHelper => + bindHelper("granola", granolaClient, dispatch); diff --git a/packages/surface/src/helpers/hubspot.ts b/packages/surface/src/helpers/hubspot.ts new file mode 100644 index 000000000..95b8f44de --- /dev/null +++ b/packages/surface/src/helpers/hubspot.ts @@ -0,0 +1,10 @@ +// GENERATED by scripts/generate-helpers.mjs — do not edit. +// Run `npm run gen --prefix packages/surface` from the repository root. + +import { hubspotClient } from "@relayfile/relay-helpers"; +import { bindHelper, type EffectDispatcher, type JournalHelper } from "../effect-transport.js"; + +export type HubspotHelper = JournalHelper>; + +export const createHubspotHelper = (dispatch: EffectDispatcher): HubspotHelper => + bindHelper("hubspot", hubspotClient, dispatch); diff --git a/packages/surface/src/helpers/index.ts b/packages/surface/src/helpers/index.ts index c62a5cba8..0dcbe20c9 100644 --- a/packages/surface/src/helpers/index.ts +++ b/packages/surface/src/helpers/index.ts @@ -2,10 +2,211 @@ // Run `npm run gen --prefix packages/surface` from the repository root. import type { SlackHelper } from "./slack.js"; +import { createAirtableHelper, type AirtableHelper } from "./airtable.js"; +import { createAsanaHelper, type AsanaHelper } from "./asana.js"; +import { createAzureBlobHelper, type AzureBlobHelper } from "./azure-blob.js"; +import { createBoxHelper, type BoxHelper } from "./box.js"; +import { createCalendlyHelper, type CalendlyHelper } from "./calendly.js"; +import { createClickupHelper, type ClickupHelper } from "./clickup.js"; +import { createCloudflareHelper, type CloudflareHelper } from "./cloudflare.js"; +import { createConfluenceHelper, type ConfluenceHelper } from "./confluence.js"; +import { createDaytonaHelper, type DaytonaHelper } from "./daytona.js"; +import { createDockerHubHelper, type DockerHubHelper } from "./docker-hub.js"; +import { createDropboxHelper, type DropboxHelper } from "./dropbox.js"; +import { createFathomHelper, type FathomHelper } from "./fathom.js"; +import { createGcpHelper, type GcpHelper } from "./gcp.js"; +import { createGcsHelper, type GcsHelper } from "./gcs.js"; +import { createGithubHelper, type GithubHelper } from "./github.js"; +import { createGitlabHelper, type GitlabHelper } from "./gitlab.js"; +import { createGmailHelper, type GmailHelper } from "./gmail.js"; +import { createGoogleCalendarHelper, type GoogleCalendarHelper } from "./google-calendar.js"; +import { createGoogleDriveHelper, type GoogleDriveHelper } from "./google-drive.js"; +import { createGranolaHelper, type GranolaHelper } from "./granola.js"; +import { createHubspotHelper, type HubspotHelper } from "./hubspot.js"; +import { createIntercomHelper, type IntercomHelper } from "./intercom.js"; +import { createJiraHelper, type JiraHelper } from "./jira.js"; +import { createLinearHelper, type LinearHelper } from "./linear.js"; +import { createMailgunHelper, type MailgunHelper } from "./mailgun.js"; +import { createMixpanelHelper, type MixpanelHelper } from "./mixpanel.js"; +import { createNeonHelper, type NeonHelper } from "./neon.js"; +import { createNotionHelper, type NotionHelper } from "./notion.js"; +import { createOnedriveHelper, type OnedriveHelper } from "./onedrive.js"; +import { createPipedriveHelper, type PipedriveHelper } from "./pipedrive.js"; +import { createPostgresHelper, type PostgresHelper } from "./postgres.js"; +import { createPosthogHelper, type PosthogHelper } from "./posthog.js"; +import { createRampHelper, type RampHelper } from "./ramp.js"; +import { createRecallHelper, type RecallHelper } from "./recall.js"; +import { createRedditHelper, type RedditHelper } from "./reddit.js"; +import { createRedisHelper, type RedisHelper } from "./redis.js"; +import { createS3Helper, type S3Helper } from "./s3.js"; +import { createSalesforceHelper, type SalesforceHelper } from "./salesforce.js"; +import { createSegmentHelper, type SegmentHelper } from "./segment.js"; +import { createSendgridHelper, type SendgridHelper } from "./sendgrid.js"; +import { createSharepointHelper, type SharepointHelper } from "./sharepoint.js"; +import { createShopifyHelper, type ShopifyHelper } from "./shopify.js"; +import { createShortcutHelper, type ShortcutHelper } from "./shortcut.js"; +import { createStripeHelper, type StripeHelper } from "./stripe.js"; +import { createTeamsHelper, type TeamsHelper } from "./teams.js"; +import { createTelegramHelper, type TelegramHelper } from "./telegram.js"; +import { createWebhookServerHelper, type WebhookServerHelper } from "./webhook-server.js"; +import { createXHelper, type XHelper } from "./x.js"; +import { createZendeskHelper, type ZendeskHelper } from "./zendesk.js"; +import type { EffectDispatcher } from "../effect-transport.js"; +export type { AirtableHelper } from "./airtable.js"; +export type { AsanaHelper } from "./asana.js"; +export type { AzureBlobHelper } from "./azure-blob.js"; +export type { BoxHelper } from "./box.js"; +export type { CalendlyHelper } from "./calendly.js"; +export type { ClickupHelper } from "./clickup.js"; +export type { CloudflareHelper } from "./cloudflare.js"; +export type { ConfluenceHelper } from "./confluence.js"; +export type { DaytonaHelper } from "./daytona.js"; +export type { DockerHubHelper } from "./docker-hub.js"; +export type { DropboxHelper } from "./dropbox.js"; +export type { FathomHelper } from "./fathom.js"; +export type { GcpHelper } from "./gcp.js"; +export type { GcsHelper } from "./gcs.js"; +export type { GithubHelper } from "./github.js"; +export type { GitlabHelper } from "./gitlab.js"; +export type { GmailHelper } from "./gmail.js"; +export type { GoogleCalendarHelper } from "./google-calendar.js"; +export type { GoogleDriveHelper } from "./google-drive.js"; +export type { GranolaHelper } from "./granola.js"; +export type { HubspotHelper } from "./hubspot.js"; +export type { IntercomHelper } from "./intercom.js"; +export type { JiraHelper } from "./jira.js"; +export type { LinearHelper } from "./linear.js"; +export type { MailgunHelper } from "./mailgun.js"; +export type { MixpanelHelper } from "./mixpanel.js"; +export type { NeonHelper } from "./neon.js"; +export type { NotionHelper } from "./notion.js"; +export type { OnedriveHelper } from "./onedrive.js"; +export type { PipedriveHelper } from "./pipedrive.js"; +export type { PostgresHelper } from "./postgres.js"; +export type { PosthogHelper } from "./posthog.js"; +export type { RampHelper } from "./ramp.js"; +export type { RecallHelper } from "./recall.js"; +export type { RedditHelper } from "./reddit.js"; +export type { RedisHelper } from "./redis.js"; +export type { S3Helper } from "./s3.js"; +export type { SalesforceHelper } from "./salesforce.js"; +export type { SegmentHelper } from "./segment.js"; +export type { SendgridHelper } from "./sendgrid.js"; +export type { SharepointHelper } from "./sharepoint.js"; +export type { ShopifyHelper } from "./shopify.js"; +export type { ShortcutHelper } from "./shortcut.js"; export type { SlackHelper } from "./slack.js"; +export type { StripeHelper } from "./stripe.js"; +export type { TeamsHelper } from "./teams.js"; +export type { TelegramHelper } from "./telegram.js"; +export type { WebhookServerHelper } from "./webhook-server.js"; +export type { XHelper } from "./x.js"; +export type { ZendeskHelper } from "./zendesk.js"; -/** Supported helper namespaces composed into Ctx. */ export interface Helpers { + airtable: AirtableHelper; + asana: AsanaHelper; + azureBlob: AzureBlobHelper; + box: BoxHelper; + calendly: CalendlyHelper; + clickup: ClickupHelper; + cloudflare: CloudflareHelper; + confluence: ConfluenceHelper; + daytona: DaytonaHelper; + dockerHub: DockerHubHelper; + dropbox: DropboxHelper; + fathom: FathomHelper; + gcp: GcpHelper; + gcs: GcsHelper; + github: GithubHelper; + gitlab: GitlabHelper; + gmail: GmailHelper; + googleCalendar: GoogleCalendarHelper; + googleDrive: GoogleDriveHelper; + granola: GranolaHelper; + hubspot: HubspotHelper; + intercom: IntercomHelper; + jira: JiraHelper; + linear: LinearHelper; + mailgun: MailgunHelper; + mixpanel: MixpanelHelper; + neon: NeonHelper; + notion: NotionHelper; + onedrive: OnedriveHelper; + pipedrive: PipedriveHelper; + postgres: PostgresHelper; + posthog: PosthogHelper; + ramp: RampHelper; + recall: RecallHelper; + reddit: RedditHelper; + redis: RedisHelper; + s3: S3Helper; + salesforce: SalesforceHelper; + segment: SegmentHelper; + sendgrid: SendgridHelper; + sharepoint: SharepointHelper; + shopify: ShopifyHelper; + shortcut: ShortcutHelper; slack: SlackHelper; + stripe: StripeHelper; + teams: TeamsHelper; + telegram: TelegramHelper; + webhookServer: WebhookServerHelper; + x: XHelper; + zendesk: ZendeskHelper; +} + +export function createHelpers(dispatch: EffectDispatcher): Omit { + return { + airtable: createAirtableHelper(dispatch), + asana: createAsanaHelper(dispatch), + azureBlob: createAzureBlobHelper(dispatch), + box: createBoxHelper(dispatch), + calendly: createCalendlyHelper(dispatch), + clickup: createClickupHelper(dispatch), + cloudflare: createCloudflareHelper(dispatch), + confluence: createConfluenceHelper(dispatch), + daytona: createDaytonaHelper(dispatch), + dockerHub: createDockerHubHelper(dispatch), + dropbox: createDropboxHelper(dispatch), + fathom: createFathomHelper(dispatch), + gcp: createGcpHelper(dispatch), + gcs: createGcsHelper(dispatch), + github: createGithubHelper(dispatch), + gitlab: createGitlabHelper(dispatch), + gmail: createGmailHelper(dispatch), + googleCalendar: createGoogleCalendarHelper(dispatch), + googleDrive: createGoogleDriveHelper(dispatch), + granola: createGranolaHelper(dispatch), + hubspot: createHubspotHelper(dispatch), + intercom: createIntercomHelper(dispatch), + jira: createJiraHelper(dispatch), + linear: createLinearHelper(dispatch), + mailgun: createMailgunHelper(dispatch), + mixpanel: createMixpanelHelper(dispatch), + neon: createNeonHelper(dispatch), + notion: createNotionHelper(dispatch), + onedrive: createOnedriveHelper(dispatch), + pipedrive: createPipedriveHelper(dispatch), + postgres: createPostgresHelper(dispatch), + posthog: createPosthogHelper(dispatch), + ramp: createRampHelper(dispatch), + recall: createRecallHelper(dispatch), + reddit: createRedditHelper(dispatch), + redis: createRedisHelper(dispatch), + s3: createS3Helper(dispatch), + salesforce: createSalesforceHelper(dispatch), + segment: createSegmentHelper(dispatch), + sendgrid: createSendgridHelper(dispatch), + sharepoint: createSharepointHelper(dispatch), + shopify: createShopifyHelper(dispatch), + shortcut: createShortcutHelper(dispatch), + stripe: createStripeHelper(dispatch), + teams: createTeamsHelper(dispatch), + telegram: createTelegramHelper(dispatch), + webhookServer: createWebhookServerHelper(dispatch), + x: createXHelper(dispatch), + zendesk: createZendeskHelper(dispatch), + }; } diff --git a/packages/surface/src/helpers/intercom.ts b/packages/surface/src/helpers/intercom.ts new file mode 100644 index 000000000..d718193a1 --- /dev/null +++ b/packages/surface/src/helpers/intercom.ts @@ -0,0 +1,10 @@ +// GENERATED by scripts/generate-helpers.mjs — do not edit. +// Run `npm run gen --prefix packages/surface` from the repository root. + +import { intercomClient } from "@relayfile/relay-helpers"; +import { bindHelper, type EffectDispatcher, type JournalHelper } from "../effect-transport.js"; + +export type IntercomHelper = JournalHelper>; + +export const createIntercomHelper = (dispatch: EffectDispatcher): IntercomHelper => + bindHelper("intercom", intercomClient, dispatch); diff --git a/packages/surface/src/helpers/jira.ts b/packages/surface/src/helpers/jira.ts new file mode 100644 index 000000000..571316453 --- /dev/null +++ b/packages/surface/src/helpers/jira.ts @@ -0,0 +1,10 @@ +// GENERATED by scripts/generate-helpers.mjs — do not edit. +// Run `npm run gen --prefix packages/surface` from the repository root. + +import { jiraClient } from "@relayfile/relay-helpers"; +import { bindHelper, type EffectDispatcher, type JournalHelper } from "../effect-transport.js"; + +export type JiraHelper = JournalHelper>; + +export const createJiraHelper = (dispatch: EffectDispatcher): JiraHelper => + bindHelper("jira", jiraClient, dispatch); diff --git a/packages/surface/src/helpers/linear.ts b/packages/surface/src/helpers/linear.ts new file mode 100644 index 000000000..9896b3d95 --- /dev/null +++ b/packages/surface/src/helpers/linear.ts @@ -0,0 +1,10 @@ +// GENERATED by scripts/generate-helpers.mjs — do not edit. +// Run `npm run gen --prefix packages/surface` from the repository root. + +import { linearClient } from "@relayfile/relay-helpers"; +import { bindHelper, type EffectDispatcher, type JournalHelper } from "../effect-transport.js"; + +export type LinearHelper = JournalHelper>; + +export const createLinearHelper = (dispatch: EffectDispatcher): LinearHelper => + bindHelper("linear", linearClient, dispatch); diff --git a/packages/surface/src/helpers/mailgun.ts b/packages/surface/src/helpers/mailgun.ts new file mode 100644 index 000000000..e9dfad15f --- /dev/null +++ b/packages/surface/src/helpers/mailgun.ts @@ -0,0 +1,10 @@ +// GENERATED by scripts/generate-helpers.mjs — do not edit. +// Run `npm run gen --prefix packages/surface` from the repository root. + +import { mailgunClient } from "@relayfile/relay-helpers"; +import { bindHelper, type EffectDispatcher, type JournalHelper } from "../effect-transport.js"; + +export type MailgunHelper = JournalHelper>; + +export const createMailgunHelper = (dispatch: EffectDispatcher): MailgunHelper => + bindHelper("mailgun", mailgunClient, dispatch); diff --git a/packages/surface/src/helpers/mixpanel.ts b/packages/surface/src/helpers/mixpanel.ts new file mode 100644 index 000000000..6fe290233 --- /dev/null +++ b/packages/surface/src/helpers/mixpanel.ts @@ -0,0 +1,10 @@ +// GENERATED by scripts/generate-helpers.mjs — do not edit. +// Run `npm run gen --prefix packages/surface` from the repository root. + +import { mixpanelClient } from "@relayfile/relay-helpers"; +import { bindHelper, type EffectDispatcher, type JournalHelper } from "../effect-transport.js"; + +export type MixpanelHelper = JournalHelper>; + +export const createMixpanelHelper = (dispatch: EffectDispatcher): MixpanelHelper => + bindHelper("mixpanel", mixpanelClient, dispatch); diff --git a/packages/surface/src/helpers/neon.ts b/packages/surface/src/helpers/neon.ts new file mode 100644 index 000000000..34d2529c7 --- /dev/null +++ b/packages/surface/src/helpers/neon.ts @@ -0,0 +1,9 @@ +// GENERATED by scripts/generate-helpers.mjs — do not edit. +// Run `npm run gen --prefix packages/surface` from the repository root. + +import { bindHelper, type EffectDispatcher, type JournalHelper, type UnavailableHelper } from "../effect-transport.js"; + +export type NeonHelper = UnavailableHelper; + +export const createNeonHelper = (dispatch: EffectDispatcher): NeonHelper => + bindHelper("neon", undefined, dispatch); diff --git a/packages/surface/src/helpers/notion.ts b/packages/surface/src/helpers/notion.ts new file mode 100644 index 000000000..09eb698a0 --- /dev/null +++ b/packages/surface/src/helpers/notion.ts @@ -0,0 +1,10 @@ +// GENERATED by scripts/generate-helpers.mjs — do not edit. +// Run `npm run gen --prefix packages/surface` from the repository root. + +import { notionClient } from "../helper-clients.js"; +import { bindHelper, type EffectDispatcher, type JournalHelper } from "../effect-transport.js"; + +export type NotionHelper = JournalHelper>; + +export const createNotionHelper = (dispatch: EffectDispatcher): NotionHelper => + bindHelper("notion", notionClient, dispatch); diff --git a/packages/surface/src/helpers/onedrive.ts b/packages/surface/src/helpers/onedrive.ts new file mode 100644 index 000000000..9513a5a68 --- /dev/null +++ b/packages/surface/src/helpers/onedrive.ts @@ -0,0 +1,10 @@ +// GENERATED by scripts/generate-helpers.mjs — do not edit. +// Run `npm run gen --prefix packages/surface` from the repository root. + +import { onedriveClient } from "@relayfile/relay-helpers"; +import { bindHelper, type EffectDispatcher, type JournalHelper } from "../effect-transport.js"; + +export type OnedriveHelper = JournalHelper>; + +export const createOnedriveHelper = (dispatch: EffectDispatcher): OnedriveHelper => + bindHelper("onedrive", onedriveClient, dispatch); diff --git a/packages/surface/src/helpers/pipedrive.ts b/packages/surface/src/helpers/pipedrive.ts new file mode 100644 index 000000000..5c4e78a85 --- /dev/null +++ b/packages/surface/src/helpers/pipedrive.ts @@ -0,0 +1,10 @@ +// GENERATED by scripts/generate-helpers.mjs — do not edit. +// Run `npm run gen --prefix packages/surface` from the repository root. + +import { pipedriveClient } from "@relayfile/relay-helpers"; +import { bindHelper, type EffectDispatcher, type JournalHelper } from "../effect-transport.js"; + +export type PipedriveHelper = JournalHelper>; + +export const createPipedriveHelper = (dispatch: EffectDispatcher): PipedriveHelper => + bindHelper("pipedrive", pipedriveClient, dispatch); diff --git a/packages/surface/src/helpers/postgres.ts b/packages/surface/src/helpers/postgres.ts new file mode 100644 index 000000000..b5f7a86b3 --- /dev/null +++ b/packages/surface/src/helpers/postgres.ts @@ -0,0 +1,10 @@ +// GENERATED by scripts/generate-helpers.mjs — do not edit. +// Run `npm run gen --prefix packages/surface` from the repository root. + +import { postgresClient } from "@relayfile/relay-helpers"; +import { bindHelper, type EffectDispatcher, type JournalHelper } from "../effect-transport.js"; + +export type PostgresHelper = JournalHelper>; + +export const createPostgresHelper = (dispatch: EffectDispatcher): PostgresHelper => + bindHelper("postgres", postgresClient, dispatch); diff --git a/packages/surface/src/helpers/posthog.ts b/packages/surface/src/helpers/posthog.ts new file mode 100644 index 000000000..b0b6fb453 --- /dev/null +++ b/packages/surface/src/helpers/posthog.ts @@ -0,0 +1,9 @@ +// GENERATED by scripts/generate-helpers.mjs — do not edit. +// Run `npm run gen --prefix packages/surface` from the repository root. + +import { bindHelper, type EffectDispatcher, type JournalHelper, type UnavailableHelper } from "../effect-transport.js"; + +export type PosthogHelper = UnavailableHelper; + +export const createPosthogHelper = (dispatch: EffectDispatcher): PosthogHelper => + bindHelper("posthog", undefined, dispatch); diff --git a/packages/surface/src/helpers/providers.ts b/packages/surface/src/helpers/providers.ts new file mode 100644 index 000000000..9d29e4d52 --- /dev/null +++ b/packages/surface/src/helpers/providers.ts @@ -0,0 +1,305 @@ +// GENERATED by scripts/generate-helpers.mjs — do not edit. +// Run `npm run gen --prefix packages/surface` from the repository root. + +export const helperProviders = [ + { + "provider": "airtable", + "namespace": "airtable", + "mockEnv": "RELAYFLOWS_AIRTABLE_MOCK", + "supported": false + }, + { + "provider": "asana", + "namespace": "asana", + "mockEnv": "RELAYFLOWS_ASANA_MOCK", + "supported": true + }, + { + "provider": "azure-blob", + "namespace": "azureBlob", + "mockEnv": "RELAYFLOWS_AZURE_BLOB_MOCK", + "supported": true + }, + { + "provider": "box", + "namespace": "box", + "mockEnv": "RELAYFLOWS_BOX_MOCK", + "supported": true + }, + { + "provider": "calendly", + "namespace": "calendly", + "mockEnv": "RELAYFLOWS_CALENDLY_MOCK", + "supported": true + }, + { + "provider": "clickup", + "namespace": "clickup", + "mockEnv": "RELAYFLOWS_CLICKUP_MOCK", + "supported": true + }, + { + "provider": "cloudflare", + "namespace": "cloudflare", + "mockEnv": "RELAYFLOWS_CLOUDFLARE_MOCK", + "supported": true + }, + { + "provider": "confluence", + "namespace": "confluence", + "mockEnv": "RELAYFLOWS_CONFLUENCE_MOCK", + "supported": true + }, + { + "provider": "daytona", + "namespace": "daytona", + "mockEnv": "RELAYFLOWS_DAYTONA_MOCK", + "supported": true + }, + { + "provider": "docker-hub", + "namespace": "dockerHub", + "mockEnv": "RELAYFLOWS_DOCKER_HUB_MOCK", + "supported": false + }, + { + "provider": "dropbox", + "namespace": "dropbox", + "mockEnv": "RELAYFLOWS_DROPBOX_MOCK", + "supported": true + }, + { + "provider": "fathom", + "namespace": "fathom", + "mockEnv": "RELAYFLOWS_FATHOM_MOCK", + "supported": false + }, + { + "provider": "gcp", + "namespace": "gcp", + "mockEnv": "RELAYFLOWS_GCP_MOCK", + "supported": false + }, + { + "provider": "gcs", + "namespace": "gcs", + "mockEnv": "RELAYFLOWS_GCS_MOCK", + "supported": true + }, + { + "provider": "github", + "namespace": "github", + "mockEnv": "RELAYFLOWS_GITHUB_MOCK", + "supported": true + }, + { + "provider": "gitlab", + "namespace": "gitlab", + "mockEnv": "RELAYFLOWS_GITLAB_MOCK", + "supported": true + }, + { + "provider": "gmail", + "namespace": "gmail", + "mockEnv": "RELAYFLOWS_GMAIL_MOCK", + "supported": true + }, + { + "provider": "google-calendar", + "namespace": "googleCalendar", + "mockEnv": "RELAYFLOWS_GOOGLE_CALENDAR_MOCK", + "supported": true + }, + { + "provider": "google-drive", + "namespace": "googleDrive", + "mockEnv": "RELAYFLOWS_GOOGLE_DRIVE_MOCK", + "supported": true + }, + { + "provider": "granola", + "namespace": "granola", + "mockEnv": "RELAYFLOWS_GRANOLA_MOCK", + "supported": true + }, + { + "provider": "hubspot", + "namespace": "hubspot", + "mockEnv": "RELAYFLOWS_HUBSPOT_MOCK", + "supported": true + }, + { + "provider": "intercom", + "namespace": "intercom", + "mockEnv": "RELAYFLOWS_INTERCOM_MOCK", + "supported": true + }, + { + "provider": "jira", + "namespace": "jira", + "mockEnv": "RELAYFLOWS_JIRA_MOCK", + "supported": true + }, + { + "provider": "linear", + "namespace": "linear", + "mockEnv": "RELAYFLOWS_LINEAR_MOCK", + "supported": true + }, + { + "provider": "mailgun", + "namespace": "mailgun", + "mockEnv": "RELAYFLOWS_MAILGUN_MOCK", + "supported": true + }, + { + "provider": "mixpanel", + "namespace": "mixpanel", + "mockEnv": "RELAYFLOWS_MIXPANEL_MOCK", + "supported": true + }, + { + "provider": "neon", + "namespace": "neon", + "mockEnv": "RELAYFLOWS_NEON_MOCK", + "supported": false + }, + { + "provider": "notion", + "namespace": "notion", + "mockEnv": "RELAYFLOWS_NOTION_MOCK", + "supported": true + }, + { + "provider": "onedrive", + "namespace": "onedrive", + "mockEnv": "RELAYFLOWS_ONEDRIVE_MOCK", + "supported": true + }, + { + "provider": "pipedrive", + "namespace": "pipedrive", + "mockEnv": "RELAYFLOWS_PIPEDRIVE_MOCK", + "supported": true + }, + { + "provider": "postgres", + "namespace": "postgres", + "mockEnv": "RELAYFLOWS_POSTGRES_MOCK", + "supported": true + }, + { + "provider": "posthog", + "namespace": "posthog", + "mockEnv": "RELAYFLOWS_POSTHOG_MOCK", + "supported": false + }, + { + "provider": "ramp", + "namespace": "ramp", + "mockEnv": "RELAYFLOWS_RAMP_MOCK", + "supported": true + }, + { + "provider": "recall", + "namespace": "recall", + "mockEnv": "RELAYFLOWS_RECALL_MOCK", + "supported": true + }, + { + "provider": "reddit", + "namespace": "reddit", + "mockEnv": "RELAYFLOWS_REDDIT_MOCK", + "supported": true + }, + { + "provider": "redis", + "namespace": "redis", + "mockEnv": "RELAYFLOWS_REDIS_MOCK", + "supported": true + }, + { + "provider": "s3", + "namespace": "s3", + "mockEnv": "RELAYFLOWS_S3_MOCK", + "supported": true + }, + { + "provider": "salesforce", + "namespace": "salesforce", + "mockEnv": "RELAYFLOWS_SALESFORCE_MOCK", + "supported": true + }, + { + "provider": "segment", + "namespace": "segment", + "mockEnv": "RELAYFLOWS_SEGMENT_MOCK", + "supported": false + }, + { + "provider": "sendgrid", + "namespace": "sendgrid", + "mockEnv": "RELAYFLOWS_SENDGRID_MOCK", + "supported": true + }, + { + "provider": "sharepoint", + "namespace": "sharepoint", + "mockEnv": "RELAYFLOWS_SHAREPOINT_MOCK", + "supported": true + }, + { + "provider": "shopify", + "namespace": "shopify", + "mockEnv": "RELAYFLOWS_SHOPIFY_MOCK", + "supported": false + }, + { + "provider": "shortcut", + "namespace": "shortcut", + "mockEnv": "RELAYFLOWS_SHORTCUT_MOCK", + "supported": true + }, + { + "provider": "slack", + "namespace": "slack", + "mockEnv": "RELAYFLOWS_SLACK_MOCK", + "supported": true + }, + { + "provider": "stripe", + "namespace": "stripe", + "mockEnv": "RELAYFLOWS_STRIPE_MOCK", + "supported": true + }, + { + "provider": "teams", + "namespace": "teams", + "mockEnv": "RELAYFLOWS_TEAMS_MOCK", + "supported": true + }, + { + "provider": "telegram", + "namespace": "telegram", + "mockEnv": "RELAYFLOWS_TELEGRAM_MOCK", + "supported": true + }, + { + "provider": "webhook-server", + "namespace": "webhookServer", + "mockEnv": "RELAYFLOWS_WEBHOOK_SERVER_MOCK", + "supported": false + }, + { + "provider": "x", + "namespace": "x", + "mockEnv": "RELAYFLOWS_X_MOCK", + "supported": false + }, + { + "provider": "zendesk", + "namespace": "zendesk", + "mockEnv": "RELAYFLOWS_ZENDESK_MOCK", + "supported": true + } +] as const; diff --git a/packages/surface/src/helpers/ramp.ts b/packages/surface/src/helpers/ramp.ts new file mode 100644 index 000000000..9c42a021e --- /dev/null +++ b/packages/surface/src/helpers/ramp.ts @@ -0,0 +1,12 @@ +// GENERATED by scripts/generate-helpers.mjs — do not edit. +// Run `npm run gen --prefix packages/surface` from the repository root. + +import { providerClient } from "@relayfile/relay-helpers"; +import type { RelayClientOptions } from "@relayfile/relay-helpers/transport"; +const rampClient = (options: RelayClientOptions) => providerClient("ramp", options); +import { bindHelper, type EffectDispatcher, type JournalHelper } from "../effect-transport.js"; + +export type RampHelper = JournalHelper>; + +export const createRampHelper = (dispatch: EffectDispatcher): RampHelper => + bindHelper("ramp", rampClient, dispatch); diff --git a/packages/surface/src/helpers/recall.ts b/packages/surface/src/helpers/recall.ts new file mode 100644 index 000000000..a6ea32ad2 --- /dev/null +++ b/packages/surface/src/helpers/recall.ts @@ -0,0 +1,10 @@ +// GENERATED by scripts/generate-helpers.mjs — do not edit. +// Run `npm run gen --prefix packages/surface` from the repository root. + +import { recallClient } from "@relayfile/relay-helpers"; +import { bindHelper, type EffectDispatcher, type JournalHelper } from "../effect-transport.js"; + +export type RecallHelper = JournalHelper>; + +export const createRecallHelper = (dispatch: EffectDispatcher): RecallHelper => + bindHelper("recall", recallClient, dispatch); diff --git a/packages/surface/src/helpers/reddit.ts b/packages/surface/src/helpers/reddit.ts new file mode 100644 index 000000000..7876dea78 --- /dev/null +++ b/packages/surface/src/helpers/reddit.ts @@ -0,0 +1,10 @@ +// GENERATED by scripts/generate-helpers.mjs — do not edit. +// Run `npm run gen --prefix packages/surface` from the repository root. + +import { redditClient } from "@relayfile/relay-helpers"; +import { bindHelper, type EffectDispatcher, type JournalHelper } from "../effect-transport.js"; + +export type RedditHelper = JournalHelper>; + +export const createRedditHelper = (dispatch: EffectDispatcher): RedditHelper => + bindHelper("reddit", redditClient, dispatch); diff --git a/packages/surface/src/helpers/redis.ts b/packages/surface/src/helpers/redis.ts new file mode 100644 index 000000000..75486fe1c --- /dev/null +++ b/packages/surface/src/helpers/redis.ts @@ -0,0 +1,10 @@ +// GENERATED by scripts/generate-helpers.mjs — do not edit. +// Run `npm run gen --prefix packages/surface` from the repository root. + +import { redisClient } from "@relayfile/relay-helpers"; +import { bindHelper, type EffectDispatcher, type JournalHelper } from "../effect-transport.js"; + +export type RedisHelper = JournalHelper>; + +export const createRedisHelper = (dispatch: EffectDispatcher): RedisHelper => + bindHelper("redis", redisClient, dispatch); diff --git a/packages/surface/src/helpers/s3.ts b/packages/surface/src/helpers/s3.ts new file mode 100644 index 000000000..85b14d741 --- /dev/null +++ b/packages/surface/src/helpers/s3.ts @@ -0,0 +1,10 @@ +// GENERATED by scripts/generate-helpers.mjs — do not edit. +// Run `npm run gen --prefix packages/surface` from the repository root. + +import { s3Client } from "@relayfile/relay-helpers"; +import { bindHelper, type EffectDispatcher, type JournalHelper } from "../effect-transport.js"; + +export type S3Helper = JournalHelper>; + +export const createS3Helper = (dispatch: EffectDispatcher): S3Helper => + bindHelper("s3", s3Client, dispatch); diff --git a/packages/surface/src/helpers/salesforce.ts b/packages/surface/src/helpers/salesforce.ts new file mode 100644 index 000000000..c47536fe1 --- /dev/null +++ b/packages/surface/src/helpers/salesforce.ts @@ -0,0 +1,10 @@ +// GENERATED by scripts/generate-helpers.mjs — do not edit. +// Run `npm run gen --prefix packages/surface` from the repository root. + +import { salesforceClient } from "@relayfile/relay-helpers"; +import { bindHelper, type EffectDispatcher, type JournalHelper } from "../effect-transport.js"; + +export type SalesforceHelper = JournalHelper>; + +export const createSalesforceHelper = (dispatch: EffectDispatcher): SalesforceHelper => + bindHelper("salesforce", salesforceClient, dispatch); diff --git a/packages/surface/src/helpers/segment.ts b/packages/surface/src/helpers/segment.ts new file mode 100644 index 000000000..05d000d05 --- /dev/null +++ b/packages/surface/src/helpers/segment.ts @@ -0,0 +1,9 @@ +// GENERATED by scripts/generate-helpers.mjs — do not edit. +// Run `npm run gen --prefix packages/surface` from the repository root. + +import { bindHelper, type EffectDispatcher, type JournalHelper, type UnavailableHelper } from "../effect-transport.js"; + +export type SegmentHelper = UnavailableHelper; + +export const createSegmentHelper = (dispatch: EffectDispatcher): SegmentHelper => + bindHelper("segment", undefined, dispatch); diff --git a/packages/surface/src/helpers/sendgrid.ts b/packages/surface/src/helpers/sendgrid.ts new file mode 100644 index 000000000..7a26aa033 --- /dev/null +++ b/packages/surface/src/helpers/sendgrid.ts @@ -0,0 +1,10 @@ +// GENERATED by scripts/generate-helpers.mjs — do not edit. +// Run `npm run gen --prefix packages/surface` from the repository root. + +import { sendgridClient } from "@relayfile/relay-helpers"; +import { bindHelper, type EffectDispatcher, type JournalHelper } from "../effect-transport.js"; + +export type SendgridHelper = JournalHelper>; + +export const createSendgridHelper = (dispatch: EffectDispatcher): SendgridHelper => + bindHelper("sendgrid", sendgridClient, dispatch); diff --git a/packages/surface/src/helpers/sharepoint.ts b/packages/surface/src/helpers/sharepoint.ts new file mode 100644 index 000000000..47e746bdb --- /dev/null +++ b/packages/surface/src/helpers/sharepoint.ts @@ -0,0 +1,10 @@ +// GENERATED by scripts/generate-helpers.mjs — do not edit. +// Run `npm run gen --prefix packages/surface` from the repository root. + +import { sharepointClient } from "@relayfile/relay-helpers"; +import { bindHelper, type EffectDispatcher, type JournalHelper } from "../effect-transport.js"; + +export type SharepointHelper = JournalHelper>; + +export const createSharepointHelper = (dispatch: EffectDispatcher): SharepointHelper => + bindHelper("sharepoint", sharepointClient, dispatch); diff --git a/packages/surface/src/helpers/shopify.ts b/packages/surface/src/helpers/shopify.ts new file mode 100644 index 000000000..2f5bf559f --- /dev/null +++ b/packages/surface/src/helpers/shopify.ts @@ -0,0 +1,9 @@ +// GENERATED by scripts/generate-helpers.mjs — do not edit. +// Run `npm run gen --prefix packages/surface` from the repository root. + +import { bindHelper, type EffectDispatcher, type JournalHelper, type UnavailableHelper } from "../effect-transport.js"; + +export type ShopifyHelper = UnavailableHelper; + +export const createShopifyHelper = (dispatch: EffectDispatcher): ShopifyHelper => + bindHelper("shopify", undefined, dispatch); diff --git a/packages/surface/src/helpers/shortcut.ts b/packages/surface/src/helpers/shortcut.ts new file mode 100644 index 000000000..987e89907 --- /dev/null +++ b/packages/surface/src/helpers/shortcut.ts @@ -0,0 +1,10 @@ +// GENERATED by scripts/generate-helpers.mjs — do not edit. +// Run `npm run gen --prefix packages/surface` from the repository root. + +import { shortcutClient } from "@relayfile/relay-helpers"; +import { bindHelper, type EffectDispatcher, type JournalHelper } from "../effect-transport.js"; + +export type ShortcutHelper = JournalHelper>; + +export const createShortcutHelper = (dispatch: EffectDispatcher): ShortcutHelper => + bindHelper("shortcut", shortcutClient, dispatch); diff --git a/packages/surface/src/helpers/stripe.ts b/packages/surface/src/helpers/stripe.ts new file mode 100644 index 000000000..9d58f6d2e --- /dev/null +++ b/packages/surface/src/helpers/stripe.ts @@ -0,0 +1,10 @@ +// GENERATED by scripts/generate-helpers.mjs — do not edit. +// Run `npm run gen --prefix packages/surface` from the repository root. + +import { stripeClient } from "../helper-clients.js"; +import { bindHelper, type EffectDispatcher, type JournalHelper } from "../effect-transport.js"; + +export type StripeHelper = JournalHelper>; + +export const createStripeHelper = (dispatch: EffectDispatcher): StripeHelper => + bindHelper("stripe", stripeClient, dispatch); diff --git a/packages/surface/src/helpers/teams.ts b/packages/surface/src/helpers/teams.ts new file mode 100644 index 000000000..4b3ae3d6c --- /dev/null +++ b/packages/surface/src/helpers/teams.ts @@ -0,0 +1,10 @@ +// GENERATED by scripts/generate-helpers.mjs — do not edit. +// Run `npm run gen --prefix packages/surface` from the repository root. + +import { teamsClient } from "@relayfile/relay-helpers"; +import { bindHelper, type EffectDispatcher, type JournalHelper } from "../effect-transport.js"; + +export type TeamsHelper = JournalHelper>; + +export const createTeamsHelper = (dispatch: EffectDispatcher): TeamsHelper => + bindHelper("teams", teamsClient, dispatch); diff --git a/packages/surface/src/helpers/telegram.ts b/packages/surface/src/helpers/telegram.ts new file mode 100644 index 000000000..2389f6f75 --- /dev/null +++ b/packages/surface/src/helpers/telegram.ts @@ -0,0 +1,10 @@ +// GENERATED by scripts/generate-helpers.mjs — do not edit. +// Run `npm run gen --prefix packages/surface` from the repository root. + +import { telegramClient } from "@relayfile/relay-helpers"; +import { bindHelper, type EffectDispatcher, type JournalHelper } from "../effect-transport.js"; + +export type TelegramHelper = JournalHelper>; + +export const createTelegramHelper = (dispatch: EffectDispatcher): TelegramHelper => + bindHelper("telegram", telegramClient, dispatch); diff --git a/packages/surface/src/helpers/webhook-server.ts b/packages/surface/src/helpers/webhook-server.ts new file mode 100644 index 000000000..e2f695dc7 --- /dev/null +++ b/packages/surface/src/helpers/webhook-server.ts @@ -0,0 +1,9 @@ +// GENERATED by scripts/generate-helpers.mjs — do not edit. +// Run `npm run gen --prefix packages/surface` from the repository root. + +import { bindHelper, type EffectDispatcher, type JournalHelper, type UnavailableHelper } from "../effect-transport.js"; + +export type WebhookServerHelper = UnavailableHelper; + +export const createWebhookServerHelper = (dispatch: EffectDispatcher): WebhookServerHelper => + bindHelper("webhook-server", undefined, dispatch); diff --git a/packages/surface/src/helpers/x.ts b/packages/surface/src/helpers/x.ts new file mode 100644 index 000000000..d3721bbc8 --- /dev/null +++ b/packages/surface/src/helpers/x.ts @@ -0,0 +1,9 @@ +// GENERATED by scripts/generate-helpers.mjs — do not edit. +// Run `npm run gen --prefix packages/surface` from the repository root. + +import { bindHelper, type EffectDispatcher, type JournalHelper, type UnavailableHelper } from "../effect-transport.js"; + +export type XHelper = UnavailableHelper; + +export const createXHelper = (dispatch: EffectDispatcher): XHelper => + bindHelper("x", undefined, dispatch); diff --git a/packages/surface/src/helpers/zendesk.ts b/packages/surface/src/helpers/zendesk.ts new file mode 100644 index 000000000..00e102d80 --- /dev/null +++ b/packages/surface/src/helpers/zendesk.ts @@ -0,0 +1,10 @@ +// GENERATED by scripts/generate-helpers.mjs — do not edit. +// Run `npm run gen --prefix packages/surface` from the repository root. + +import { zendeskClient } from "@relayfile/relay-helpers"; +import { bindHelper, type EffectDispatcher, type JournalHelper } from "../effect-transport.js"; + +export type ZendeskHelper = JournalHelper>; + +export const createZendeskHelper = (dispatch: EffectDispatcher): ZendeskHelper => + bindHelper("zendesk", zendeskClient, dispatch); diff --git a/packages/surface/src/runtime.ts b/packages/surface/src/runtime.ts index dec331e46..85813bfd9 100644 --- a/packages/surface/src/runtime.ts +++ b/packages/surface/src/runtime.ts @@ -8,3 +8,8 @@ export { type ReadonlyFlowHeader, } from "./flow.js"; export { slackPostBody } from "./slack.js"; + +export { createHelpers } from "./helpers/index.js"; +export { helperProviders } from "./helpers/providers.js"; +export { helperClients } from "./helpers/clients.js"; +export { invokeHelper, type HelperCall } from "./effect-transport.js"; diff --git a/packages/surface/tests/fixtures/helpers.flow.ts b/packages/surface/tests/fixtures/helpers.flow.ts new file mode 100644 index 000000000..2fb0d956d --- /dev/null +++ b/packages/surface/tests/fixtures/helpers.flow.ts @@ -0,0 +1,26 @@ +import { flow, type Step } from '../../src/index.js'; + +/** Smoke fixture: these four operations must compile to journal-backed steps. */ +export default flow('helpers-smoke', async f => { + const issue = f.github.createIssue({ repo: 'owner/repo', title: 'Smoke', body: 'hello' }); + const check: Step<{ status: string }> = issue; + await check; + await f.linear.createIssue({ teamId: 'team', title: 'Smoke' }); + await f.notion.appendBlock({ pageId: 'page', block: { type: 'paragraph', paragraph: { rich_text: [] } } }); + await f.stripe.createInvoice({ customer: 'cus_123' }); + await f.slack.post('#test', 'hello'); + await f.googleDrive.files.write({}, { name: 'Smoke' }); + f.done('success'); +}); + +export const invalid = flow('invalid-helper-args', async f => { + // @ts-expect-error upstream requires teamId + await f.linear.createIssue({ title: 'Missing team' }); + // @ts-expect-error repo must be a string + await f.github.createIssue({ repo: 42, title: 'Bad repo', body: '' }); + // @ts-expect-error invoice requires a customer + await f.stripe.createInvoice({}); + // @ts-expect-error nonexistent provider verb + await f.asana.noSuchVerb({}); + f.done('success'); +}); diff --git a/packages/surface/tests/helpers.snapshot.test.ts b/packages/surface/tests/helpers.snapshot.test.ts index 5e871fd4e..3f19e44c4 100644 --- a/packages/surface/tests/helpers.snapshot.test.ts +++ b/packages/surface/tests/helpers.snapshot.test.ts @@ -5,5 +5,5 @@ import { expect, it } from 'vitest'; it('regenerates helpers byte-identically from the pinned adapter', () => { const guard = fileURLToPath(new URL('../scripts/check-generated-helpers.mjs', import.meta.url)); expect(execFileSync(process.execPath, [guard], { encoding: 'utf8' })) - .toContain('HELPERS_GENERATED_OK index.ts, slack.ts'); + .toContain('HELPERS_GENERATED_OK airtable.ts, asana.ts, azure-blob.ts'); }); diff --git a/scripts/generate-helpers.mjs b/scripts/generate-helpers.mjs index 0b5173114..c33a41a33 100644 --- a/scripts/generate-helpers.mjs +++ b/scripts/generate-helpers.mjs @@ -1,5 +1,5 @@ import assert from 'node:assert/strict'; -import { mkdirSync, readFileSync, writeFileSync } from 'node:fs'; +import { mkdirSync, readFileSync, readdirSync, writeFileSync } from 'node:fs'; import { createRequire } from 'node:module'; import { dirname, join, resolve } from 'node:path'; import { fileURLToPath } from 'node:url'; @@ -8,33 +8,41 @@ import { parseArgs } from 'node:util'; const surface = fileURLToPath(new URL('../packages/surface/', import.meta.url)); const require = createRequire(join(surface, 'package.json')); const ts = require('typescript'); +const upstream = require('@relayfile/relay-helpers'); +const { WRITEBACK_PATH_CATALOG: catalog, ADAPTERS_WITHOUT_WRITEBACK_PATHS: missing } = require('@relayfile/adapter-core/writeback-paths'); const { values } = parseArgs({ options: { 'out-dir': { type: 'string', default: join(surface, 'src/helpers') }, 'adapters-dir': { type: 'string' }, } }); -// The pinned published declaration makes regeneration reproducible in CI. -// An explicit checkout lets adapter authors check source changes before release. -const input = values['adapters-dir'] - ? join(resolve(values['adapters-dir']), 'packages/relay-helpers/src/slack.ts') - : join(dirname(require.resolve('@relayfile/relay-helpers/package.json')), 'dist/slack.d.ts'); -const source = ts.createSourceFile(input, readFileSync(input, 'utf8'), ts.ScriptTarget.Latest, true); -assert.equal(source.parseDiagnostics.length, 0, 'Invalid Slack adapter source'); -const client = source.statements.find(node => ts.isInterfaceDeclaration(node) && node.name.text === 'SlackClient'); -assert(client, 'Missing upstream SlackClient interface'); -const verbs = ['post', 'dm', 'reply', 'react']; -assert.deepEqual(client.members.map(member => member.name?.getText(source)), verbs, - 'Slack adapter verbs changed; review dispatcher support before regenerating'); +const names = new Set([...Object.keys(catalog), ...missing.map(item => item.provider), 'webhook-server']); +if (values['adapters-dir']) { + const packages = join(resolve(values['adapters-dir']), 'packages'); + for (const entry of readdirSync(packages, { withFileTypes: true })) { + if (!entry.isDirectory()) continue; + const pkg = JSON.parse(readFileSync(join(packages, entry.name, 'package.json'), 'utf8')); + if (pkg.name.startsWith('@relayfile/adapter-') && entry.name !== 'core') names.add(entry.name); + } +} +const camel = name => name.replace(/-([a-z])/g, (_, c) => c.toUpperCase()); +const title = name => camel(name).replace(/^./, c => c.toUpperCase()); +const header = '// GENERATED by scripts/generate-helpers.mjs — do not edit.\n' + + '// Run `npm run gen --prefix packages/surface` from the repository root.\n'; +const destination = resolve(values['out-dir']); +mkdirSync(destination, { recursive: true }); +const files = {}; +// Keep Slack's established receipt and argument contract, copied from its AST. +const slackSource = join(dirname(require.resolve('@relayfile/relay-helpers/package.json')), 'dist/slack.d.ts'); +const source = ts.createSourceFile(slackSource, readFileSync(slackSource, 'utf8'), ts.ScriptTarget.Latest, true); +const slack = source.statements.find(node => ts.isInterfaceDeclaration(node) && node.name.text === 'SlackClient'); +assert(slack, 'Missing upstream SlackClient interface'); +assert.deepEqual(slack.members.map(member => member.name?.getText(source)), ['post', 'dm', 'reply', 'react']); const printer = ts.createPrinter({ newLine: ts.NewLineKind.LineFeed, removeComments: true }); -const methods = client.members.map(member => { - assert(ts.isMethodSignature(member) && !member.typeParameters?.length, - 'Expected a non-generic Slack method'); - // Copy the parameter AST unchanged: no hand-maintained approximation of args. - const parameters = member.parameters.map(parameter => - printer.printNode(ts.EmitHint.Unspecified, parameter, source)).join(', '); +const methods = slack.members.map(member => { const name = member.name.getText(source); // The surface adds structured posts while the pinned adapter accepts text only. if (name === 'post') return ' post(...args: Parameters): ReturnType;'; - return ` ${name}(${parameters}): ReturnType;`.replaceAll('\n', '\n '); + const params = member.parameters.map(p => printer.printNode(ts.EmitHint.Unspecified, p, source)).join(', '); + return ` ${name}(${params}): ReturnType;`.replaceAll('\n', '\n '); }); // Pinned OpenAPI fragments fill the gap until relay-helpers exports message types. // Slack's schema deliberately leaves block contents and attachment fields open. @@ -49,20 +57,37 @@ function schemaType(schema) { } const messageTypes = Object.entries(messageSchema.schemas).map(([name, { schema }]) => `export type ${name} = ${schemaType(schema)};`).join('\n'); -const header = '// GENERATED by scripts/generate-helpers.mjs — do not edit.\n' - + '// Run `npm run gen --prefix packages/surface` from the repository root.\n'; -const files = { - 'slack.ts': `${header}\nimport type { SlackHelper as RuntimeSlackHelper } from "../slack.js";\n\n` - + '/** Generated from the pinned Slack OpenAPI fragments in scripts/slack-message-schema.json. */\n' - + `${messageTypes}\n\n` - + '/** Adapter argument shapes over the journal-backed Slack dispatcher. */\n' - + `export interface SlackHelper {\n${methods.join('\n')}\n}\n`, - 'index.ts': `${header}\nimport type { SlackHelper } from "./slack.js";\n\n` - + 'export type { SlackHelper } from "./slack.js";\n\n' - + '/** Supported helper namespaces composed into Ctx. */\n' - + 'export interface Helpers {\n slack: SlackHelper;\n}\n', -}; -const destination = resolve(values['out-dir']); -mkdirSync(destination, { recursive: true }); +files['slack.ts'] = `${header}\nimport type { SlackHelper as RuntimeSlackHelper } from "../slack.js";\n\n` + + '/** Generated from the pinned Slack OpenAPI fragments in scripts/slack-message-schema.json. */\n' + + `${messageTypes}\n\n` + + '/** Adapter argument shapes over the journal-backed Slack dispatcher. */\n' + + `export interface SlackHelper {\n${methods.join('\n')}\n}\n`; +const providers = [...names].sort(); +for (const provider of providers.filter(p => p !== 'slack')) { + const factory = `${camel(provider)}Client`; + const supported = catalog[provider] !== undefined; + const special = ['github', 'notion', 'stripe'].includes(provider); + files[`${provider}.ts`] = header + '\n' + + (special ? `import { ${factory} } from "../helper-clients.js";\n` + : typeof upstream[factory] === 'function' ? `import { ${factory} } from "@relayfile/relay-helpers";\n` + : supported ? `import { providerClient } from "@relayfile/relay-helpers";\nimport type { RelayClientOptions } from "@relayfile/relay-helpers/transport";\nconst ${factory} = (options: RelayClientOptions) => providerClient("${provider}", options);\n` : '') + + `import { bindHelper, type EffectDispatcher, type JournalHelper${!supported && !special ? ', type UnavailableHelper' : ''} } from "../effect-transport.js";\n\n` + + `export type ${title(provider)}Helper = ${supported || special ? `JournalHelper>` : 'UnavailableHelper'};\n\n` + + `export const create${title(provider)}Helper = (dispatch: EffectDispatcher): ${title(provider)}Helper =>\n` + + ` bindHelper(${JSON.stringify(provider)}, ${supported || special ? factory : 'undefined'}, dispatch);\n`; +} +files['index.ts'] = header + '\nimport type { SlackHelper } from "./slack.js";\n' + + providers.filter(p => p !== 'slack').map(p => `import { create${title(p)}Helper, type ${title(p)}Helper } from "./${p}.js";`).join('\n') + + '\nimport type { EffectDispatcher } from "../effect-transport.js";\n\n' + + providers.map(p => `export type { ${title(p)}Helper } from "./${p}.js";`).join('\n') + + '\n\nexport interface Helpers {\n' + providers.map(p => ` ${camel(p)}: ${title(p)}Helper;`).join('\n') + '\n}\n\n' + + 'export function createHelpers(dispatch: EffectDispatcher): Omit {\n return {\n' + + providers.filter(p => p !== 'slack').map(p => ` ${camel(p)}: create${title(p)}Helper(dispatch),`).join('\n') + '\n };\n}\n'; +files['clients.ts'] = header + '\nimport * as upstream from "@relayfile/relay-helpers";\nimport * as custom from "../helper-clients.js";\nimport type { HelperFactory } from "../effect-transport.js";\n\nexport const helperClients: Readonly> = {\n' + + providers.filter(p => p !== 'slack' && (p === 'stripe' || catalog[p])).map(p => ` "${p}": ${['github', 'notion', 'stripe'].includes(p) ? `custom.${camel(p)}Client` : typeof upstream[`${camel(p)}Client`] === 'function' ? `upstream.${camel(p)}Client` : `(options) => upstream.providerClient("${p}", options)`},`).join('\n') + '\n};\n'; +files['providers.ts'] = header + '\nexport const helperProviders = ' + JSON.stringify(providers.map(p => ({ + provider: p, namespace: camel(p), mockEnv: `RELAYFLOWS_${p.replaceAll('-', '_').toUpperCase()}_MOCK`, + supported: p === 'stripe' || catalog[p] !== undefined, +})), null, 2) + ' as const;\n'; for (const [name, content] of Object.entries(files)) writeFileSync(join(destination, name), content); -console.log(`Generated ${Object.keys(files).join(', ')}`); +console.log(`Generated ${providers.length} provider helpers (${providers.filter(p => !catalog[p] && p !== 'stripe').length} without upstream writeback clients)`);