diff --git a/docs/SLACK-HELPER.md b/docs/SLACK-HELPER.md new file mode 100644 index 000000000..84fb65f21 --- /dev/null +++ b/docs/SLACK-HELPER.md @@ -0,0 +1,57 @@ +# Slack helper namespace + +`f.slack` is the first provider helper on the authored TypeScript surface. +Calls return a `Step` immediately and must be awaited like `f.run` and `f.llm`. + +```ts +import { flow } from '@relayflows/surface'; + +export default flow('notify', async f => { + const posted = await f.slack.post('#releases', 'Release shipped'); + await f.slack.reply('#releases', posted.ts, 'Details follow'); + await f.slack.react('#releases', posted.ts, 'rocket'); + await f.slack.dm('U123', 'Release shipped'); + f.done('success'); +}); +``` + +Provide a relayfile mount with a `slack/` directory. +`flows check notify.flow.ts` refuses visible Slack calls when neither a mount +nor a bot token is available, with `REFUSED [helper_slack.credential_missing]` +and exit status 2. +Direct bot-token transport is deferred: a token without a mount is refused +as `helper_slack.mount_required`, before running the body. A header can declare +`tools: { slack: true }` for calls hidden behind another +function. The body is not evaluated during check; dynamic calls also check +credentials when they are awaited. + +`RELAYFLOWS_SLACK_MOCK=1` bypasses credentials and network. Each call captures +its stamped body in `/mock-writeback/slack/.json` and returns +a synthetic receipt. Message timestamps are `mock-` and message +references are `mock-ref-`. + +The runtime uses the published `@relayfile/relay-helpers` Slack client with an +explicit transport. Relayfile receives the adapter's canonical write path. +Direct-token idempotency needs Slack-server timestamp deduplication, which +mount writeback already handles; direct-token transport is deferred. +`flowRunWritebackIdempotency(runId, stepId)` always returns `runId:stepId`. +The process-tick stamper cannot replace this token, including for reactions. + +The existing kernel agent lease carries the helper's external write. Its +instruction and successful `step.completed.payload.output` carry +`{ type: 'effect', provider: 'slack', verb, params }`; output also carries the +receipt and flow idempotency token. `effect.record`/`effect.confirm` retain the +kernel's own lease key. No kernel vocabulary or protocol change is required. +Other providers can follow this transport and effect-worker pattern. + +Receipts are persisted under `/helper-receipts/` before confirmation. +`flows resume --data-dir ` reattaches the helper worker +and recovers the receipt. Retain the whole data directory across restarts. +The kernel retains a crashed attempt's `step.completed(crashed)` and records +exactly one successful completion on recovery. A confirmed effect is not sent +again. An unconfirmed effect is retried with its original provider token. + +The authored executor still creates separate runs per step; it does not yet +have a durable journal for the whole TypeScript body. Resuming a helper run +finishes that effect, not the remainder of the authored body. Postfix callback +gates remain unsupported by this executor, as for its other current verbs. diff --git a/packages/sdk/package-lock.json b/packages/sdk/package-lock.json index 46d619c46..1a6e52500 100644 --- a/packages/sdk/package-lock.json +++ b/packages/sdk/package-lock.json @@ -9,6 +9,8 @@ "version": "2.0.8", "license": "Apache-2.0", "dependencies": { + "@relayfile/adapter-core": "0.5.24", + "@relayfile/relay-helpers": "0.4.11", "@relayflows/surface": "2.0.8", "@types/js-yaml": "^4.0.9", "ajv": "^8.17.1", @@ -416,6 +418,19 @@ "node": ">=12" } }, + "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.5.5", "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", @@ -440,6 +455,155 @@ "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", + "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", + "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", + "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", + "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/@relayflows/surface": { "version": "2.0.8", "resolved": "https://registry.npmjs.org/@relayflows/surface/-/surface-2.0.8.tgz", @@ -799,6 +963,37 @@ "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", + "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", + "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", + "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", @@ -981,6 +1176,33 @@ "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", + "engines": { + "node": "18 || 20 || >=22" + } + }, + "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" + }, + "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", + "dependencies": { + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "20 || >=22" + } + }, "node_modules/cac": { "version": "6.7.14", "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz", @@ -1018,6 +1240,86 @@ "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", + "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": "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/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", + "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": "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/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/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", + "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": "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/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", + "engines": { + "node": ">= 6" + }, + "funding": { + "url": "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/sponsors/fb55" + } + }, "node_modules/debug": { "version": "4.4.3", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", @@ -1046,6 +1348,86 @@ "node": ">=6" } }, + "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", + "dependencies": { + "domelementtype": "^2.3.0", + "domhandler": "^5.0.2", + "entities": "^4.2.0" + }, + "funding": { + "url": "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/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": "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/sponsors/fb55" + } + ], + "license": "BSD-2-Clause" + }, + "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", + "dependencies": { + "domelementtype": "^2.3.0" + }, + "engines": { + "node": ">= 4" + }, + "funding": { + "url": "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/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", + "dependencies": { + "dom-serializer": "^2.0.0", + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3" + }, + "funding": { + "url": "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/fb55/domutils?sponsor=1" + } + }, + "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", + "dependencies": { + "iconv-lite": "^0.6.3", + "whatwg-encoding": "^3.1.1" + }, + "funding": { + "url": "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/fb55/encoding-sniffer?sponsor=1" + } + }, + "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", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "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/fb55/entities?sponsor=1" + } + }, "node_modules/es-module-lexer": { "version": "1.7.0", "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz", @@ -1149,6 +1531,59 @@ "node": "^8.16.0 || ^10.6.0 || >=11.0.0" } }, + "node_modules/htmlparser2": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-10.1.0.tgz", + "integrity": "sha512-VTZkM9GWRAtEpveh7MSF6SjjrpNVNNVJfFup7xTY3UpFtm67foy9HDVXneLtFVt4pMz5kZtgNcvCniNFb1hlEQ==", + "funding": [ + "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/fb55/htmlparser2?sponsor=1", + { + "type": "github", + "url": "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/sponsors/fb55" + } + ], + "license": "MIT", + "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", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "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/fb55/entities?sponsor=1" + } + }, + "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", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "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/js-yaml": { "version": "5.4.1", "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-5.4.1.tgz", @@ -1194,6 +1629,44 @@ "@jridgewell/sourcemap-codec": "^1.5.5" } }, + "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", + "dependencies": { + "brace-expansion": "^5.0.8" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "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/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", @@ -1220,6 +1693,67 @@ "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" } }, + "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", + "dependencies": { + "boolbase": "^1.0.0" + }, + "funding": { + "url": "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/fb55/nth-check?sponsor=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", + "dependencies": { + "entities": "^6.0.0" + }, + "funding": { + "url": "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/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", + "dependencies": { + "domhandler": "^5.0.3", + "parse5": "^7.0.0" + }, + "funding": { + "url": "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/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", + "dependencies": { + "parse5": "^7.0.0" + }, + "funding": { + "url": "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/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", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "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/fb55/entities?sponsor=1" + } + }, "node_modules/pathe": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/pathe/-/pathe-1.1.2.tgz", @@ -1328,6 +1862,12 @@ "fsevents": "~2.3.2" } }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "license": "MIT" + }, "node_modules/siginfo": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", @@ -1359,6 +1899,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", @@ -1417,6 +1974,15 @@ "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", + "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", @@ -1573,6 +2139,28 @@ } } }, + "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", + "dependencies": { + "iconv-lite": "0.6.3" + }, + "engines": { + "node": ">=18" + } + }, + "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", + "engines": { + "node": ">=18" + } + }, "node_modules/why-is-node-running": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", @@ -1590,6 +2178,16 @@ "node": ">=8" } }, + "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", diff --git a/packages/sdk/package.json b/packages/sdk/package.json index 2d5966718..4d4759075 100644 --- a/packages/sdk/package.json +++ b/packages/sdk/package.json @@ -42,6 +42,8 @@ }, "license": "Apache-2.0", "dependencies": { + "@relayfile/adapter-core": "0.5.24", + "@relayfile/relay-helpers": "0.4.11", "@relayflows/surface": "2.0.8", "@types/js-yaml": "^4.0.9", "ajv": "^8.17.1", diff --git a/packages/sdk/src/authored-flow-error.ts b/packages/sdk/src/authored-flow-error.ts index d68fff3f5..728a24c46 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_slack.credential_missing' + | 'helper_slack.mount_required' | 'agent_cli_unresolved' | 'agent_parked' | 'llm_cli_unresolved' diff --git a/packages/sdk/src/authored-flow-executor.ts b/packages/sdk/src/authored-flow-executor.ts index 23fa7ff18..2b1a54bbd 100644 --- a/packages/sdk/src/authored-flow-executor.ts +++ b/packages/sdk/src/authored-flow-executor.ts @@ -1,3 +1,9 @@ +import { randomUUID } from 'node:crypto'; +import { dirname } from 'node:path'; +import { assertSlackCredentials, runSlackEffect } from './authored-slack-effect.js'; +import { checkSlackHelpers } from './slack-preflight.js'; +import { snapshotJsonValue } from './json-value.js'; +import type { SlackCall } from './slack-writeback.js'; import { authoredWorkerRunner } from './authored-worker-step.js'; import { readSuccessfulOutput, isSurfaceRunCompletionReason } from './authored-step-output.js'; import { @@ -90,6 +96,8 @@ export interface ExecuteAuthoredFlowOptions { * `getDefinition` instead; see authored-flow-loader.ts's comment on why. */ readonly getDefinition?: GetFlowDefinition; + /** Helper receipt and mock-writeback storage; CLI passes its daemon data directory. */ + readonly dataDir?: string; /** * The flow file's own path — passed straight to `checkAuthoredFlow` * (cli/check.ts) so `f.agent` resolves a CLI the same way a declarative @@ -124,7 +132,10 @@ export async function executeAuthoredFlow( ...(options.onWait !== undefined ? { onWait: options.onWait } : {}), }; const definition = getDefinition(handle); - const headerFields = Object.keys(definition.header); + const headerFields = Object.keys(definition.header).filter(key => key !== 'tools'); + if (definition.header.tools && Object.keys(definition.header.tools).some(key => key !== 'slack')) headerFields.push('tools'); + const helperPreflight = checkSlackHelpers(definition); + if (!helperPreflight.ok) assertSlackCredentials(); if (headerFields.length > 0) { throw new AuthoredFlowExecutionError( 'unsupported_header', @@ -176,7 +187,30 @@ export async function executeAuthoredFlow( )); } + const slackRun = randomUUID(); + function slackOperation(call: SlackCall): Step { + assertOperationAllowed(`slack.${call.verb}`, definition.name, requestedCompletion); + const snapshot = snapshotJsonValue(call, 'f.slack call') as unknown as SlackCall; + const id = `slack-${slackRun}-${nextStep++}`; + return trackStep(authoredSteps, new AuthoredFlowOperation( + id, `slack.${call.verb}`, + () => assertOperationAllowed(`slack.${call.verb}`, definition.name, requestedCompletion), + async () => { + const receipt = await runSlackEffect(journal, definition.name, id, snapshot, + options.dataDir ?? dirname(journal.socketPath), journalSteps); + return (call.verb === 'react' ? undefined : receipt) as T; + }, + lifecycle, + )); + } + const context: Ctx = { + 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 } }), + reply: (channel, threadTs, text) => slackOperation({ type: 'effect', provider: 'slack', verb: 'reply', params: { channel, threadTs, text } }), + react: (channel, messageTs, emoji) => slackOperation({ type: 'effect', provider: 'slack', verb: 'react', params: { channel, messageTs, emoji } }), + }, run(command) { assertOperationAllowed('run', definition.name, requestedCompletion); const id = `run-${nextStep++}`; diff --git a/packages/sdk/src/authored-slack-effect.ts b/packages/sdk/src/authored-slack-effect.ts new file mode 100644 index 000000000..992fbc6d6 --- /dev/null +++ b/packages/sdk/src/authored-slack-effect.ts @@ -0,0 +1,121 @@ +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 { withWorkerLease } from './worker-lease.js'; +import { checkSlackHelpers } from './slack-preflight.js'; +import { atomicJson, readSlackReceipt, receiptPath, slackWriteback, 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 completeSlackDispatch(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(); } +} + +async function completeSlackDispatch(client: JournalClient, dispatch: StepDispatchEvent, call: SlackCall, dataDir: string): Promise { + const 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: '/slack', + 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 slackWriteback(call, 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 }; + }); + 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: '/slack', idempotency_key: dispatch.idempotency_key }] }); +} diff --git a/packages/sdk/src/cli.ts b/packages/sdk/src/cli.ts index 4458b515a..47e5d6ea6 100644 --- a/packages/sdk/src/cli.ts +++ b/packages/sdk/src/cli.ts @@ -1,4 +1,5 @@ #!/usr/bin/env node +import { checkHelperBody } from './cli/check-helper-body.js'; import { renderProgress, type ProgressEvent } from './progress.js'; import { realpathSync } from 'node:fs'; @@ -97,7 +98,8 @@ export async function runCli( // refuses `--data-dir` on `check`, so there is no data dir to attach to. // `flows check` keeps working with no daemon, no relayflowd binary and no // data directory at all -- a property worth keeping, not an omission. - const checked = checkFlow(parsed.value); + const checked = /\.(?:[cm]?[jt]s)$/.test(parsed.value) + ? await checkHelperBody(parsed.value) : checkFlow(parsed.value); emitCheckReport(checked.report, parsed.json, io); return checked.report.ok ? 0 : 2; } diff --git a/packages/sdk/src/cli/check-helper-body.ts b/packages/sdk/src/cli/check-helper-body.ts new file mode 100644 index 000000000..febc5dcde --- /dev/null +++ b/packages/sdk/src/cli/check-helper-body.ts @@ -0,0 +1,14 @@ +import { loadAuthoredFlow } from '../authored-flow-loader.js'; +import { checkSlackHelpers } from '../slack-preflight.js'; +import { inputFailureReport, type CheckExecution } from './check.js'; + +/** Imports the definition, but never executes arbitrary authored body code. */ +export async function checkHelperBody(path: string): Promise { + try { + const { handle, getDefinition } = await loadAuthoredFlow(path); + return { report: { ...checkSlackHelpers(getDefinition(handle)), path } }; + } catch (error) { + return { report: inputFailureReport({ kind: 'invalid_spec', + message: error instanceof Error ? error.message : 'Could not import authored flow.' }, path) }; + } +} diff --git a/packages/sdk/src/cli/direct-run.ts b/packages/sdk/src/cli/direct-run.ts index 1762d1741..3f22d9f12 100644 --- a/packages/sdk/src/cli/direct-run.ts +++ b/packages/sdk/src/cli/direct-run.ts @@ -64,6 +64,7 @@ export async function runDirectFlow( } const result = await executeAuthoredFlow(handle, client, input, { getDefinition, + dataDir, flowPath: path, onProgress: options.onProgress, localAgentStream: localAgent?.stream, @@ -105,7 +106,9 @@ export async function runDirectFlow( // the same limitation already documented elsewhere). if (error instanceof AuthoredFlowLoadError || (error instanceof AuthoredFlowExecutionError - && (error.code === 'unsupported_header' + && (error.code === 'helper_slack.credential_missing' + || error.code === 'helper_slack.mount_required' + || error.code === 'unsupported_header' || error.code === 'agent_cli_unresolved' || error.code === 'llm_cli_unresolved' || error.code === 'unsupported_workspace_permission'))) { @@ -113,7 +116,7 @@ export async function runDirectFlow( exitCode: 2, report: { ...fromCheckReport('run', inputFailureReport({ - kind: 'invalid_spec', + kind: error instanceof AuthoredFlowExecutionError && (error.code === 'helper_slack.credential_missing' || error.code === 'helper_slack.mount_required') ? error.code : 'invalid_spec', message: error.message, }, path)), socketPath, diff --git a/packages/sdk/src/cli/run.ts b/packages/sdk/src/cli/run.ts index a3ca7356f..495d63486 100644 --- a/packages/sdk/src/cli/run.ts +++ b/packages/sdk/src/cli/run.ts @@ -1,3 +1,5 @@ +import { resumeSlackEffect } from '../authored-slack-effect.js'; +import { AuthoredFlowExecutionError } from '../authored-flow-error.js'; import { join, resolve } from 'node:path'; import type { ProgressEvent } from '../progress.js'; import { toKernelSpec } from '../compile.js'; @@ -128,9 +130,15 @@ export async function resumeFlow( if (connected !== undefined) return connected; try { + await resumeSlackEffect(client, runId, dataDir); const outcome = await client.runResume(runId); return await classifyOutcome(client, 'resume', outcome, base, socketPath, options); } catch (error) { + if (error instanceof AuthoredFlowExecutionError + && (error.code === 'helper_slack.credential_missing' || error.code === 'helper_slack.mount_required')) { + return { exitCode: 2, report: { ...base, runId, socketPath, + diagnostics: [{ severity: 'refusal', kind: error.code, message: error.message }] } }; + } if (!(error instanceof JournalProtocolError) || error.code !== 'run_not_found') { return protocolFailure('resume', base, socketPath, error, runId); } diff --git a/packages/sdk/src/failure-kinds.ts b/packages/sdk/src/failure-kinds.ts index 6eec36b84..fbb2b7139 100644 --- a/packages/sdk/src/failure-kinds.ts +++ b/packages/sdk/src/failure-kinds.ts @@ -2,6 +2,8 @@ const SHARED_SPEC_FAILURE_KINDS = ['invalid_spec'] as const; /** Environment refusal kinds produced after spec validation succeeds. */ const PREFLIGHT_ENVIRONMENT_FAILURE_KINDS = [ + 'helper_slack.credential_missing', + 'helper_slack.mount_required', 'cli_missing', 'cli_unauthenticated', 'cli_unresolved', diff --git a/packages/sdk/src/index.ts b/packages/sdk/src/index.ts index 64decefed..d29b5324d 100644 --- a/packages/sdk/src/index.ts +++ b/packages/sdk/src/index.ts @@ -70,6 +70,7 @@ export { } from './authored-flow.js'; export { preflight, + preflightHelpers, type CliResolution, type CliResolutionSource, type CliProbeResult, diff --git a/packages/sdk/src/journal-client.ts b/packages/sdk/src/journal-client.ts index ff6aeb184..e85ac6d2f 100644 --- a/packages/sdk/src/journal-client.ts +++ b/packages/sdk/src/journal-client.ts @@ -65,7 +65,7 @@ export class JournalClient extends EventEmitter { private readonly connectTimeoutMs: number; constructor( - private readonly socketPath: string, + readonly socketPath: string, options: JournalClientOptions = {}, ) { super(); diff --git a/packages/sdk/src/preflight.ts b/packages/sdk/src/preflight.ts index 30169f196..8b0b38436 100644 --- a/packages/sdk/src/preflight.ts +++ b/packages/sdk/src/preflight.ts @@ -490,3 +490,21 @@ function firstCommandWord(command: string): string | undefined { const match = rest.match(/^(?:"([^"]+)"|'([^']+)'|([^\s]+))/); return match?.[1] ?? match?.[2] ?? match?.[3]; } + +/** 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 + || /(?:\.\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 }; +} diff --git a/packages/sdk/src/slack-preflight.ts b/packages/sdk/src/slack-preflight.ts new file mode 100644 index 000000000..18a7e0cd7 --- /dev/null +++ b/packages/sdk/src/slack-preflight.ts @@ -0,0 +1,19 @@ +import { statSync } from 'node:fs'; +import { join } from 'node:path'; +import { preflightHelpers } from './preflight.js'; + +export function slackMount(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; } + catch { return undefined; } +} + +export function checkSlackHelpers(definition: Parameters[0]) { + return preflightHelpers(definition, { + slackToken: process.env.SLACK_BOT_TOKEN, + slackMount: slackMount() !== undefined, + slackMock: process.env.RELAYFLOWS_SLACK_MOCK === '1', + }); +} diff --git a/packages/sdk/src/slack-writeback.ts b/packages/sdk/src/slack-writeback.ts new file mode 100644 index 000000000..63983a14c --- /dev/null +++ b/packages/sdk/src/slack-writeback.ts @@ -0,0 +1,78 @@ +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 } from '@relayflows/surface'; +import type { RelayTransport } from '@relayfile/relay-helpers/transport'; +import { writeJsonFile, type WritebackResult } from '@relayfile/adapter-core/vfs-client'; +import { slackMount } from './slack-preflight.js'; + +export type SlackCall = + | { type: 'effect'; provider: 'slack'; verb: 'post'; params: { channel: string; text: string; opts?: { replyTo?: string } } } + | { type: 'effect'; provider: 'slack'; verb: 'dm'; params: { user: string; text: string } } + | { 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(); } +} + +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 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(); + const body: Record = { ...request.body as Record, 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 }); + switch (call.verb) { + case 'post': return client.post(call.params.channel, call.params.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 '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-flow-slack.test.ts b/packages/sdk/tests/authored-flow-slack.test.ts new file mode 100644 index 000000000..d4250cf34 --- /dev/null +++ b/packages/sdk/tests/authored-flow-slack.test.ts @@ -0,0 +1,186 @@ +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 { preflightHelpers } from '../src/preflight.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-slack-')); + 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}`); +} + +function mockFiles(dataDir: string): string[] { + return readdirSync(join(dataDir, 'mock-writeback/slack')).filter(file => file.endsWith('.json')); +} + +describe('authored Slack helper effects', () => { + it('journals exactly one effect with the authored params and typed receipt, without network', async () => { + vi.stubEnv('RELAYFLOWS_SLACK_MOCK', '1'); + const network = vi.spyOn(globalThis, 'fetch').mockRejectedValue(new Error('network forbidden')); + const dataDir = temporary(); + const { client } = await start(dataDir); + const result = await executeAuthoredFlow(flow('slack-post', async f => { + const receipt = await f.slack.post('#test', 'hi'); + expect(receipt).toMatchObject({ channel: '#test', ts: expect.stringMatching(/^mock-slack-/), ref: expect.stringMatching(/^mock-ref-/) }); + f.done('success'); + }), client, undefined, { dataDir }); + const step = result.journalSteps[0]!; + const entries = (await client.journalRead(step.runId, 1)).entries as any[]; + const completed = entries.filter(entry => entry.entry_type === 'step.completed'); + expect(completed).toHaveLength(1); + expect(completed[0]).toMatchObject({ step_id: step.id, payload: { completionReason: 'success', output: { + type: 'effect', provider: 'slack', verb: 'post', params: { channel: '#test', text: 'hi' }, + idempotencyKey: `${step.runId}:${step.id}`, receipt: { channel: '#test', ts: expect.any(String), ref: expect.any(String) }, + } } }); + expect(entries.filter(entry => entry.entry_type === 'effect.confirmed')).toHaveLength(1); + expect(mockFiles(dataDir)).toEqual([`${step.id}.json`]); + expect(network).not.toHaveBeenCalled(); + }); + + it('refuses missing credentials before running the body and flows check exits 2', async () => { + for (const key of ['SLACK_BOT_TOKEN', 'RELAYFLOWS_SLACK_MOCK', 'RELAYFILE_MOUNT_PATH', 'WORKSPACE_ROOT', 'WORKFORCE_SANDBOX_ROOT', 'RELAYFILE_MOUNT_ROOT', 'RELAYFILE_ROOT']) vi.stubEnv(key, ''); + let executed = false; + const handle = flow('no-credential', async f => { executed = true; await f.slack.post('#test', 'hi'); f.done('success'); }); + await expect(executeAuthoredFlow(handle, new JournalClient('/must-not-connect'))).rejects.toMatchObject({ code: 'helper_slack.credential_missing' }); + expect(executed).toBe(false); + const dataDir = temporary(); + mkdirSync(join(dataDir, 'node_modules/@relayflows'), { recursive: true }); + symlinkSync(join(root, 'packages/sdk/node_modules/@relayflows/surface'), join(dataDir, 'node_modules/@relayflows/surface')); + const fixture = join(dataDir, 'slack.mjs'); + writeFileSync(fixture, `import { flow } from ${JSON.stringify(join(root, 'packages/sdk/node_modules/@relayflows/surface/dist/index.js'))};\nexport default flow('slack', async f => { await f.slack.post('#test', 'hi'); f.done('success'); });`); + const output: string[] = []; + expect(await runCli(['check', '--json', fixture], { stdout: line => output.push(line), stderr: line => output.push(line) })).toBe(2); + expect(output.join('')).toContain('helper_slack.credential_missing'); + vi.stubEnv('SLACK_BOT_TOKEN', 'configured-token'); + await expect(executeAuthoredFlow(handle, new JournalClient('/must-not-connect'))).rejects.toMatchObject({ code: 'helper_slack.mount_required' }); + expect(executed).toBe(false); + expect(preflightHelpers({ header: {}, body: async (f: any) => f.slack.post('#test', 'hi') }, { + slackMount: true, slackMock: false, + }).ok).toBe(true); + }); + + it.each(['confirm', 'complete'] as const)('replays after SIGKILL before %s with the same token and one successful completion', async boundary => { + vi.stubEnv('RELAYFLOWS_SLACK_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-slack', async f => { await f.slack.post('#test', 'hi'); 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/slack', 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 ['SLACK_BOT_TOKEN', 'RELAYFLOWS_SLACK_MOCK', 'RELAYFILE_MOUNT_PATH', 'WORKSPACE_ROOT', 'WORKFORCE_SANDBOX_ROOT', 'RELAYFILE_MOUNT_ROOT', 'RELAYFILE_ROOT']) vi.stubEnv(key, ''); + const refused = await resumeFlow(before.runId, dataDir, { spawn: false }); + expect(refused.exitCode).toBe(2); + expect(refused.report.diagnostics).toContainEqual(expect.objectContaining({ kind: 'helper_slack.credential_missing' })); + vi.stubEnv('RELAYFLOWS_SLACK_MOCK', '1'); + const resumed = await resumeFlow(before.runId, dataDir, { 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, { spawn: false }); + expect(again.exitCode).toBe(0); + expect(mockFiles(dataDir)).toHaveLength(1); + }, 30_000); + + it('writes two files for two calls and supports dm, reply, and react', async () => { + vi.stubEnv('RELAYFLOWS_SLACK_MOCK', '1'); + const dataDir = temporary(); + const { client } = await start(dataDir); + await executeAuthoredFlow(flow('two-posts', async f => { + await f.slack.post('#test', 'one'); await f.slack.post('#test', 'two'); f.done('success'); + }), client, undefined, { dataDir }); + const files = mockFiles(dataDir); + expect(files).toHaveLength(2); + expect(new Set(files).size).toBe(2); + await executeAuthoredFlow(flow('other-verbs', async f => { + expect(await f.slack.dm('U123', 'hello')).toMatchObject({ user: 'U123', ts: expect.any(String) }); + expect(await f.slack.reply('#test', '123.456', 'reply')).toMatchObject({ channel: '#test', ts: expect.any(String) }); + expect(await f.slack.react('#test', '123.456', 'thumbsup')).toBeUndefined(); + f.done('success'); + }), client, undefined, { dataDir }); + const captures = mockFiles(dataDir).map(file => JSON.parse(readFileSync(join(dataDir, 'mock-writeback/slack', file), 'utf8'))); + expect(captures.find(call => call.verb === 'reply').request.body.thread_ts).toBe('123.456'); + expect(captures.find(call => call.verb === 'react').request.body).toMatchObject({ emoji: 'thumbsup', idempotencyKey: expect.any(String) }); + }); +}); diff --git a/packages/sdk/tests/preflight.test.ts b/packages/sdk/tests/preflight.test.ts index 327dce33f..80666bb26 100644 --- a/packages/sdk/tests/preflight.test.ts +++ b/packages/sdk/tests/preflight.test.ts @@ -1,3 +1,4 @@ +import { preflightHelpers } from '../src/preflight.js'; import { describe, expect, it } from 'vitest'; import { PREFLIGHT_FAILURE_KINDS, @@ -360,6 +361,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', () => { const scenarios = [ + preflightHelpers({ header: { tools: { slack: true } }, body() {} }, { slackMount: false, slackMock: false }), + preflightHelpers({ header: { tools: { slack: true } }, body() {} }, { slackToken: 'present', slackMount: false, slackMock: false }), preflight({ version: '0.1.0', steps: [{ id: 'a', type: 'deterministic', command: 'x', prompt: 'cross-verb' }], diff --git a/packages/sdk/tests/slack-writeback.test.ts b/packages/sdk/tests/slack-writeback.test.ts new file mode 100644 index 000000000..42886543f --- /dev/null +++ b/packages/sdk/tests/slack-writeback.test.ts @@ -0,0 +1,41 @@ +import { mkdtempSync, mkdirSync, readdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs'; +import { tmpdir } from 'node:os'; +import { join } from 'node:path'; +import { afterEach, expect, it, vi } from 'vitest'; +import { slackWriteback, type SlackCall } from '../src/slack-writeback.js'; +import { slackMount } from '../src/slack-preflight.js'; + +const dirs: string[] = []; +afterEach(() => { + for (const dir of dirs.splice(0)) rmSync(dir, { recursive: true, force: true }); + vi.unstubAllEnvs(); vi.restoreAllMocks(); +}); +function setup(): string { + for (const key of ['RELAYFLOWS_SLACK_MOCK', 'RELAYFILE_MOUNT_PATH', 'WORKSPACE_ROOT', 'WORKFORCE_SANDBOX_ROOT', 'RELAYFILE_MOUNT_ROOT', 'RELAYFILE_ROOT']) vi.stubEnv(key, ''); + vi.stubEnv('SLACK_BOT_TOKEN', 'test-token'); + const dir = mkdtempSync(join(tmpdir(), 'slack-write-')); dirs.push(dir); return dir; +} +const post: SlackCall = { type: 'effect', provider: 'slack', verb: 'post', params: { channel: 'C1', text: 'hi' } }; +const signal = () => new AbortController().signal; + +it('stamps a real mount draft and waits for its delivered receipt', async () => { + const dir = setup(); + mkdirSync(join(dir, 'slack')); + vi.stubEnv('WORKSPACE_ROOT', dir); + vi.stubEnv('WORKFORCE_TICK_DELIVERY_ID', 'ambient-tick'); + expect(slackMount()).toBe(dir); // Empty higher-precedence env must not mask a mount. + const network = vi.spyOn(globalThis, 'fetch').mockRejectedValue(new Error('no network')); + const request = slackWriteback(post, dir, 'run', 'step', signal()); + const messages = join(dir, 'slack/channels/C1/messages'); + let draft: string | undefined; + for (let i = 0; i < 100 && !draft; i++) { + try { draft = readdirSync(messages).find(file => file.endsWith('.json')); } catch {} + if (!draft) await new Promise(resolve => setTimeout(resolve, 10)); + } + expect(draft).toBeDefined(); + const path = join(messages, draft!); + expect(JSON.parse(readFileSync(path, 'utf8'))).toEqual({ text: 'hi', idempotencyKey: 'run:step' }); + writeFileSync(path, JSON.stringify({ externalId: '123.456' })); + expect(await request).toMatchObject({ channel: 'C1', ts: '123.456', ref: expect.any(String) }); + expect(network).not.toHaveBeenCalled(); +}); diff --git a/packages/surface/bun.lock b/packages/surface/bun.lock index 3298f4ead..743cefb02 100644 --- a/packages/surface/bun.lock +++ b/packages/surface/bun.lock @@ -8,6 +8,9 @@ "typescript": "^5.6.0", "vitest": "^2.1.0", }, + "peerDependencies": { + "@relayfile/relay-helpers": "0.4.11", + }, }, }, "packages": { @@ -57,10 +60,32 @@ "@esbuild/win32-x64": ["@esbuild/win32-x64@0.21.5", "", { "os": "win32", "cpu": "x64" }, "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw=="], + "@isaacs/fs-minipass": ["@isaacs/fs-minipass@4.0.1", "", { "dependencies": { "minipass": "^7.0.4" } }, "sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w=="], + "@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.6.0", "", {}, "sha512-T7jf+5zgsZHwNJ4lvQ7/aezbyk0nNX+zJVWpmHA7VYsEx7a7qr5Rg5IbtJFqkgze5Y2sruq1RUY8Q837Od7iFw=="], "@napi-rs/lzma-linux-x64-gnu": ["@napi-rs/lzma-linux-x64-gnu@1.5.1", "", { "os": "linux", "cpu": "x64" }, "sha512-oTXEIha4SsuXdTA4Iyskj0kpdx2yVXdhd75c2v3xGrHFfVMsbhTPZU/nMPL4sWKo4pBHm3aucLaqGlF696dTyQ=="], + "@relayfile/adapter-core": ["@relayfile/adapter-core@0.5.24", "", { "dependencies": { "@scalar/postman-to-openapi": "^0.6.0", "cheerio": "^1.2.0", "minimatch": "^10.0.3", "yaml": "^2.8.1" }, "peerDependencies": { "@relayfile/sdk": ">=0.6.0 <1" }, "bin": { "adapter-core": "dist/src/cli.js" } }, "sha512-bOQRuBoAw2RlYs30RtKsOvXlXzcRx4owhHdj384hPrznBIY3U4ZYcb4pVfzreW9TStHEiX7EguUyqhD9g8DJmA=="], + + "@relayfile/adapter-linear": ["@relayfile/adapter-linear@0.4.12", "", { "dependencies": { "@relayfile/adapter-core": "^0.5.18" }, "peerDependencies": { "@relayfile/sdk": ">=0.6.0 <1" } }, "sha512-obICrTmIkVKXX0vAjGiezOK/cu3iUH6i67fuKLe7Fqcp/Upwrsr70Ap68YxyMvpNOvE4TdbbM1vU+2uGXwM5sg=="], + + "@relayfile/adapter-reddit": ["@relayfile/adapter-reddit@0.2.9", "", { "dependencies": { "@relayfile/adapter-core": "^0.5.15" }, "peerDependencies": { "@relayfile/sdk": ">=0.6.0 <1" } }, "sha512-/ZWkr4SguRRCk4fY6DEtP5NVUHSMWP66GtMou4NJOcf5PaxYSa0ceSVxAN/IcTBG6JqdtbqB4IoWn8V9d/JC3g=="], + + "@relayfile/core": ["@relayfile/core@0.10.57", "", {}, "sha512-oKwNJKGovzdl/6IF3sSVZFsiG2ITH7jXnmFgxbCIYPZ+J7QsQ2zvBhT35sPGiNcD4FfiMpyX9OB8LJXOefNdcQ=="], + + "@relayfile/mount-darwin-arm64": ["@relayfile/mount-darwin-arm64@0.10.57", "", { "os": "darwin", "cpu": "arm64" }, "sha512-yCcjQ7HoCJECnjtZeG7eZwLXa4RAT3RAVAAymdIqKZbMe3wbkF1kg3YNQHuRqYsDEIQzGkA4k5AsITmG1j2PfQ=="], + + "@relayfile/mount-darwin-x64": ["@relayfile/mount-darwin-x64@0.10.57", "", { "os": "darwin", "cpu": "x64" }, "sha512-5Ze1a16Mxc2PTj2Q+AdtRZIMKQ4sxJIIXwPjdWD+9rTMEQeqs7xzSZ1WRIjI89FzyNHBjx/+gzD9FK8Kjuc7Yg=="], + + "@relayfile/mount-linux-arm64": ["@relayfile/mount-linux-arm64@0.10.57", "", { "os": "linux", "cpu": "arm64" }, "sha512-+2MhcDwzb7KSC3qjKWOW+9EnZ8mFKJiVns5fiJwO1oThiAd3CnnCfJzYkbEWRKIL0dH5wg4npdayF23DiBtB5w=="], + + "@relayfile/mount-linux-x64": ["@relayfile/mount-linux-x64@0.10.57", "", { "os": "linux", "cpu": "x64" }, "sha512-Y0OGuGEpglQsUrrR0uzjBzqFyrM8wU5jG1tELJ8LBNhedNUEUupGKD7ixs+i4k9kR8Q68voVsQTfzdgqzW3M+Q=="], + + "@relayfile/relay-helpers": ["@relayfile/relay-helpers@0.4.11", "", { "dependencies": { "@relayfile/adapter-core": "^0.5.15", "@relayfile/adapter-linear": "^0.4.11", "@relayfile/adapter-reddit": "^0.2.9" } }, "sha512-J9S2L+dVRQcxv32BxMq+xEqpBJoG1mNzo1E/CCXY4LYUSl0euZD0ZayaFMcpBhGqx+2IDNiSlGjFun/YQk9OqA=="], + + "@relayfile/sdk": ["@relayfile/sdk@0.10.57", "", { "dependencies": { "@relayfile/core": "0.10.57", "ignore": "^7.0.5", "tar": "^7.5.10" }, "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" } }, "sha512-9AJIEyyruqVogbdqVfY7q0pjuXJy8IhzxR/1pvwWJiybU1BZO8LFNurlFePTAo/uhLG9cshr0YJRBxv2WbG0Sg=="], + "@rollup/rollup-android-arm-eabi": ["@rollup/rollup-android-arm-eabi@4.63.1", "", { "os": "android", "cpu": "arm" }, "sha512-UZ8sUxPTiHWYX9QNdJedb1kDZSpS1t/VPWBWGSgqHNi9w3Cu6IXvu2mzbhiTiPvtrqgTQJ+zqiAq2iPIPilpaQ=="], "@rollup/rollup-android-arm64": ["@rollup/rollup-android-arm64@4.63.1", "", { "os": "android", "cpu": "arm64" }, "sha512-cQ4nFQABN5cDvDpbvJ7bMStCpnaVxynZrRMfUJYgxcIk9Sh54FIO1vtfkg0B69REjER77ioZ/ov+eAApx/KmLQ=="], @@ -111,6 +136,12 @@ "@rollup/rollup-win32-x64-msvc": ["@rollup/rollup-win32-x64-msvc@4.63.1", "", { "os": "win32", "cpu": "x64" }, "sha512-at8QVep6S3h5Y6gSbdGU06bRY5WJkf6WUduM9YtvYMbYhB1MOFfUgc6kehitQXzOtMSaT70q7f9ydPhpqu821w=="], + "@scalar/helpers": ["@scalar/helpers@0.5.1", "", {}, "sha512-9VvPfv8b+YZVIFwR3SWeq4Y8ij/kU3/kf2M6NKcbf2iVyh63d8s0ssap5m/nOhiz/Puidv/29MAJlJCA0LRssA=="], + + "@scalar/openapi-types": ["@scalar/openapi-types@0.7.0", "", {}, "sha512-kN0PwlJW0de4bwQ4ib+mBHzKJUvBCyR/gwU4zLEq6SCbj+GfgYUh+2a0/yl1WYVUiSkkwFsHjfmQ8KjhR3HK0Q=="], + + "@scalar/postman-to-openapi": ["@scalar/postman-to-openapi@0.6.3", "", { "dependencies": { "@scalar/helpers": "0.5.1", "@scalar/openapi-types": "0.7.0" } }, "sha512-Y/tMuRZG34wEfpTxDfXFp5o2X3ibb5ojGWupGJ9ZxkThCx7rOGydnszJPzEbgDK3eF6nJ6UuE7bCTpIEutYnPw=="], + "@types/estree": ["@types/estree@1.0.9", "", {}, "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg=="], "@vitest/expect": ["@vitest/expect@2.1.9", "", { "dependencies": { "@vitest/spy": "2.1.9", "@vitest/utils": "2.1.9", "chai": "^5.1.2", "tinyrainbow": "^1.2.0" } }, "sha512-UJCIkTBenHeKT1TTlKMJWy1laZewsRIzYighyYiJKZreqtdxSos/S1t+ktRMQWu2CKqaarrkeszJx1cgC5tGZw=="], @@ -129,16 +160,44 @@ "assertion-error": ["assertion-error@2.0.1", "", {}, "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA=="], + "balanced-match": ["balanced-match@4.0.4", "", {}, "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA=="], + + "boolbase": ["boolbase@1.0.0", "", {}, "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww=="], + + "brace-expansion": ["brace-expansion@5.0.9", "", { "dependencies": { "balanced-match": "^4.0.2" } }, "sha512-ScQ4IuvIEF1TMlP7Zt+vjJ//9zlPb2SDcxWxM3bk8s6t6GGdJ7KO1dCcTidOPJKePW30LE/2cT7wCyPho9/Wxg=="], + "cac": ["cac@6.7.14", "", {}, "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ=="], "chai": ["chai@5.3.3", "", { "dependencies": { "assertion-error": "^2.0.1", "check-error": "^2.1.1", "deep-eql": "^5.0.1", "loupe": "^3.1.0", "pathval": "^2.0.0" } }, "sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw=="], "check-error": ["check-error@2.1.3", "", {}, "sha512-PAJdDJusoxnwm1VwW07VWwUN1sl7smmC3OKggvndJFadxxDRyFJBX/ggnu/KE4kQAB7a3Dp8f/YXC1FlUprWmA=="], + "cheerio": ["cheerio@1.2.0", "", { "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" } }, "sha512-WDrybc/gKFpTYQutKIK6UvfcuxijIZfMfXaYm8NMsPQxSYvf+13fXUJ4rztGGbJcBQ/GF55gvrZ0Bc0bj/mqvg=="], + + "cheerio-select": ["cheerio-select@2.1.0", "", { "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" } }, "sha512-9v9kG0LvzrlcungtnJtpGNxY+fzECQKhK4EGJX2vByejiMX84MFNQw4UxPJl3bFbTMw+Dfs37XaIkCwTZfLh4g=="], + + "chownr": ["chownr@3.0.0", "", {}, "sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g=="], + + "css-select": ["css-select@5.2.2", "", { "dependencies": { "boolbase": "^1.0.0", "css-what": "^6.1.0", "domhandler": "^5.0.2", "domutils": "^3.0.1", "nth-check": "^2.0.1" } }, "sha512-TizTzUddG/xYLA3NXodFM0fSbNizXjOKhqiQQwvhlspadZokn1KDy0NZFS0wuEubIYAV5/c1/lAr0TaaFXEXzw=="], + + "css-what": ["css-what@6.2.2", "", {}, "sha512-u/O3vwbptzhMs3L1fQE82ZSLHQQfto5gyZzwteVIEyeaY5Fc7R4dapF/BvRoSYFeqfBk4m0V1Vafq5Pjv25wvA=="], + "debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" }, "peerDependencies": { "supports-color": "*" }, "optionalPeers": ["supports-color"] }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="], "deep-eql": ["deep-eql@5.0.2", "", {}, "sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q=="], + "dom-serializer": ["dom-serializer@2.0.0", "", { "dependencies": { "domelementtype": "^2.3.0", "domhandler": "^5.0.2", "entities": "^4.2.0" } }, "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg=="], + + "domelementtype": ["domelementtype@2.3.0", "", {}, "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw=="], + + "domhandler": ["domhandler@5.0.3", "", { "dependencies": { "domelementtype": "^2.3.0" } }, "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w=="], + + "domutils": ["domutils@3.2.2", "", { "dependencies": { "dom-serializer": "^2.0.0", "domelementtype": "^2.3.0", "domhandler": "^5.0.3" } }, "sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw=="], + + "encoding-sniffer": ["encoding-sniffer@0.2.1", "", { "dependencies": { "iconv-lite": "^0.6.3", "whatwg-encoding": "^3.1.1" } }, "sha512-5gvq20T6vfpekVtqrYQsSCFZ1wEg5+wW0/QaZMWkFr6BqD3NfKs0rLCx4rrVlSWJeZb5NBJgVLswK/w2MWU+Gw=="], + + "entities": ["entities@4.5.0", "", {}, "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw=="], + "es-module-lexer": ["es-module-lexer@1.7.0", "", {}, "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA=="], "esbuild": ["esbuild@0.21.5", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.21.5", "@esbuild/android-arm": "0.21.5", "@esbuild/android-arm64": "0.21.5", "@esbuild/android-x64": "0.21.5", "@esbuild/darwin-arm64": "0.21.5", "@esbuild/darwin-x64": "0.21.5", "@esbuild/freebsd-arm64": "0.21.5", "@esbuild/freebsd-x64": "0.21.5", "@esbuild/linux-arm": "0.21.5", "@esbuild/linux-arm64": "0.21.5", "@esbuild/linux-ia32": "0.21.5", "@esbuild/linux-loong64": "0.21.5", "@esbuild/linux-mips64el": "0.21.5", "@esbuild/linux-ppc64": "0.21.5", "@esbuild/linux-riscv64": "0.21.5", "@esbuild/linux-s390x": "0.21.5", "@esbuild/linux-x64": "0.21.5", "@esbuild/netbsd-x64": "0.21.5", "@esbuild/openbsd-x64": "0.21.5", "@esbuild/sunos-x64": "0.21.5", "@esbuild/win32-arm64": "0.21.5", "@esbuild/win32-ia32": "0.21.5", "@esbuild/win32-x64": "0.21.5" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw=="], @@ -149,14 +208,34 @@ "fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="], + "htmlparser2": ["htmlparser2@10.1.0", "", { "dependencies": { "domelementtype": "^2.3.0", "domhandler": "^5.0.3", "domutils": "^3.2.2", "entities": "^7.0.1" } }, "sha512-VTZkM9GWRAtEpveh7MSF6SjjrpNVNNVJfFup7xTY3UpFtm67foy9HDVXneLtFVt4pMz5kZtgNcvCniNFb1hlEQ=="], + + "iconv-lite": ["iconv-lite@0.6.3", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw=="], + + "ignore": ["ignore@7.0.9", "", {}, "sha512-brTTsvFRt5C1gGHtPst/281UjPD5t9fBqbgoMPlVWy11ZLTPfu7HxK4ZYqO9H7o/yC9rSTCI85EaQ4OoY12qYw=="], + "loupe": ["loupe@3.2.1", "", {}, "sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ=="], "magic-string": ["magic-string@0.30.21", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ=="], + "minimatch": ["minimatch@10.2.6", "", { "dependencies": { "brace-expansion": "^5.0.8" } }, "sha512-vpLQEs+VLCr1nU0BXS07maYoFwlDAH0gngQuuttxIwutDFEMHq2blX+8vpgxDdK3J1PwjCJiep77OitTZ4Ll1A=="], + + "minipass": ["minipass@7.1.3", "", {}, "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A=="], + + "minizlib": ["minizlib@3.1.0", "", { "dependencies": { "minipass": "^7.1.2" } }, "sha512-KZxYo1BUkWD2TVFLr0MQoM8vUUigWD3LlD83a/75BqC+4qE0Hb1Vo5v1FgcfaNXvfXzr+5EhQ6ing/CaBijTlw=="], + "ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], "nanoid": ["nanoid@3.3.18", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-DTg4MJbGMWkfi6VZFdNt2/caMbQy4Ou+Op/hJQvGEWcnVfoA1QA+xzRKAzw9jD6+GVOOeYr/mIcuDSdug6F6+w=="], + "nth-check": ["nth-check@2.1.1", "", { "dependencies": { "boolbase": "^1.0.0" } }, "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w=="], + + "parse5": ["parse5@7.3.0", "", { "dependencies": { "entities": "^6.0.0" } }, "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw=="], + + "parse5-htmlparser2-tree-adapter": ["parse5-htmlparser2-tree-adapter@7.1.0", "", { "dependencies": { "domhandler": "^5.0.3", "parse5": "^7.0.0" } }, "sha512-ruw5xyKs6lrpo9x9rCZqZZnIUntICjQAd0Wsmp396Ul9lN/h+ifgVV1x1gZHi8euej6wTfpqX8j+BFQxF0NS/g=="], + + "parse5-parser-stream": ["parse5-parser-stream@7.1.2", "", { "dependencies": { "parse5": "^7.0.0" } }, "sha512-JyeQc9iwFLn5TbvvqACIF/VXG6abODeB3Fwmv/TGdLk2LfbWkaySGY72at4+Ty7EkPZj854u4CrICqNk2qIbow=="], + "pathe": ["pathe@1.1.2", "", {}, "sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ=="], "pathval": ["pathval@2.0.1", "", {}, "sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ=="], @@ -167,6 +246,8 @@ "rollup": ["rollup@4.63.1", "", { "dependencies": { "@types/estree": "1.0.9" }, "optionalDependencies": { "@napi-rs/lzma-linux-x64-gnu": "1.5.1", "@rollup/rollup-android-arm-eabi": "4.63.1", "@rollup/rollup-android-arm64": "4.63.1", "@rollup/rollup-darwin-arm64": "4.63.1", "@rollup/rollup-darwin-x64": "4.63.1", "@rollup/rollup-freebsd-arm64": "4.63.1", "@rollup/rollup-freebsd-x64": "4.63.1", "@rollup/rollup-linux-arm-gnueabihf": "4.63.1", "@rollup/rollup-linux-arm-musleabihf": "4.63.1", "@rollup/rollup-linux-arm64-gnu": "4.63.1", "@rollup/rollup-linux-arm64-musl": "4.63.1", "@rollup/rollup-linux-loong64-gnu": "4.63.1", "@rollup/rollup-linux-loong64-musl": "4.63.1", "@rollup/rollup-linux-ppc64-gnu": "4.63.1", "@rollup/rollup-linux-ppc64-musl": "4.63.1", "@rollup/rollup-linux-riscv64-gnu": "4.63.1", "@rollup/rollup-linux-riscv64-musl": "4.63.1", "@rollup/rollup-linux-s390x-gnu": "4.63.1", "@rollup/rollup-linux-x64-gnu": "4.63.1", "@rollup/rollup-linux-x64-musl": "4.63.1", "@rollup/rollup-openbsd-x64": "4.63.1", "@rollup/rollup-openharmony-arm64": "4.63.1", "@rollup/rollup-win32-arm64-msvc": "4.63.1", "@rollup/rollup-win32-ia32-msvc": "4.63.1", "@rollup/rollup-win32-x64-gnu": "4.63.1", "@rollup/rollup-win32-x64-msvc": "4.63.1", "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-3Df9jsstwhccuEfmAMi9l8XUh/GOkVObmFTU7CCVBysEbcOZLl84jCtaAZMcPiMz2EGKsATzQcU+Xr3n/wU6cg=="], + "safer-buffer": ["safer-buffer@2.1.2", "", {}, "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="], + "siginfo": ["siginfo@2.0.0", "", {}, "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g=="], "source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="], @@ -175,6 +256,8 @@ "std-env": ["std-env@3.10.0", "", {}, "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg=="], + "tar": ["tar@7.5.22", "", { "dependencies": { "@isaacs/fs-minipass": "^4.0.0", "chownr": "^3.0.0", "minipass": "^7.1.2", "minizlib": "^3.1.0", "yallist": "^5.0.0" } }, "sha512-MFO/QzvtAOmJbkhOaCTvbGcFN9L9b+JunIsDwaKljSOdcLMea3NJ1k9Usz/rjdfSXTq4dfzfeS7W4p4YOAAHeA=="], + "tinybench": ["tinybench@2.9.0", "", {}, "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg=="], "tinyexec": ["tinyexec@0.3.2", "", {}, "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA=="], @@ -187,12 +270,26 @@ "typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="], + "undici": ["undici@7.29.1", "", {}, "sha512-RYONW2MeafgYlkVOKYKkA/Ag7BmXqgIWCa8t1m0JcxrQg9pI9lEqRhAOruOBCbAohOa/gkCF+iPi9hrgvTzu6Q=="], + "vite": ["vite@5.4.21", "", { "dependencies": { "esbuild": "^0.21.3", "postcss": "^8.4.43", "rollup": "^4.20.0" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^18.0.0 || >=20.0.0", "less": "*", "lightningcss": "^1.21.0", "sass": "*", "sass-embedded": "*", "stylus": "*", "sugarss": "*", "terser": "^5.4.0" }, "optionalPeers": ["@types/node", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser"], "bin": { "vite": "bin/vite.js" } }, "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw=="], "vite-node": ["vite-node@2.1.9", "", { "dependencies": { "cac": "^6.7.14", "debug": "^4.3.7", "es-module-lexer": "^1.5.4", "pathe": "^1.1.2", "vite": "^5.0.0" }, "bin": { "vite-node": "vite-node.mjs" } }, "sha512-AM9aQ/IPrW/6ENLQg3AGY4K1N2TGZdR5e4gu/MmmR2xR3Ll1+dib+nook92g4TV3PXVyeyxdWwtaCAiUL0hMxA=="], "vitest": ["vitest@2.1.9", "", { "dependencies": { "@vitest/expect": "2.1.9", "@vitest/mocker": "2.1.9", "@vitest/pretty-format": "^2.1.9", "@vitest/runner": "2.1.9", "@vitest/snapshot": "2.1.9", "@vitest/spy": "2.1.9", "@vitest/utils": "2.1.9", "chai": "^5.1.2", "debug": "^4.3.7", "expect-type": "^1.1.0", "magic-string": "^0.30.12", "pathe": "^1.1.2", "std-env": "^3.8.0", "tinybench": "^2.9.0", "tinyexec": "^0.3.1", "tinypool": "^1.0.1", "tinyrainbow": "^1.2.0", "vite": "^5.0.0", "vite-node": "2.1.9", "why-is-node-running": "^2.3.0" }, "peerDependencies": { "@edge-runtime/vm": "*", "@types/node": "^18.0.0 || >=20.0.0", "@vitest/browser": "2.1.9", "@vitest/ui": "2.1.9", "happy-dom": "*", "jsdom": "*" }, "optionalPeers": ["@edge-runtime/vm", "@types/node", "@vitest/browser", "@vitest/ui", "happy-dom", "jsdom"], "bin": { "vitest": "vitest.mjs" } }, "sha512-MSmPM9REYqDGBI8439mA4mWhV5sKmDlBKWIYbA3lRb2PTHACE0mgKwA8yQ2xq9vxDTuk4iPrECBAEW2aoFXY0Q=="], + "whatwg-encoding": ["whatwg-encoding@3.1.1", "", { "dependencies": { "iconv-lite": "0.6.3" } }, "sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ=="], + + "whatwg-mimetype": ["whatwg-mimetype@4.0.0", "", {}, "sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg=="], + "why-is-node-running": ["why-is-node-running@2.3.0", "", { "dependencies": { "siginfo": "^2.0.0", "stackback": "0.0.2" }, "bin": { "why-is-node-running": "cli.js" } }, "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w=="], + + "yallist": ["yallist@5.0.0", "", {}, "sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw=="], + + "yaml": ["yaml@2.9.0", "", { "bin": { "yaml": "bin.mjs" } }, "sha512-2AvhNX3mb8zd6Zy7INTtSpl1F15HW6Wnqj0srWlkKLcpYl/gMIMJiyuGq2KeI2YFxUPjdlB+3Lc10seMLtL4cA=="], + + "htmlparser2/entities": ["entities@7.0.1", "", {}, "sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA=="], + + "parse5/entities": ["entities@6.0.1", "", {}, "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g=="], } } diff --git a/packages/surface/package-lock.json b/packages/surface/package-lock.json index ec47c375b..d699ec4a9 100644 --- a/packages/surface/package-lock.json +++ b/packages/surface/package-lock.json @@ -14,6 +14,9 @@ }, "engines": { "node": ">=20.19.0 || >=22.12.0" + }, + "peerDependencies": { + "@relayfile/relay-helpers": "0.4.11" } }, "node_modules/@esbuild/aix-ppc64": { @@ -407,6 +410,19 @@ "node": ">=12" } }, + "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", @@ -434,6 +450,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", @@ -823,6 +992,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", @@ -953,6 +1156,36 @@ "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/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/cac": { "version": "6.7.14", "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz", @@ -990,6 +1223,90 @@ "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": "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/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": "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/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/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": "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/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": "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/sponsors/fb55" + } + }, "node_modules/debug": { "version": "4.4.3", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", @@ -1018,6 +1335,92 @@ "node": ">=6" } }, + "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": "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/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": "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/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": "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/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": "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/fb55/domutils?sponsor=1" + } + }, + "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": "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/fb55/encoding-sniffer?sponsor=1" + } + }, + "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": "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/fb55/entities?sponsor=1" + } + }, "node_modules/es-module-lexer": { "version": "1.7.0", "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz", @@ -1099,6 +1502,62 @@ "node": "^8.16.0 || ^10.6.0 || >=11.0.0" } }, + "node_modules/htmlparser2": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-10.1.0.tgz", + "integrity": "sha512-VTZkM9GWRAtEpveh7MSF6SjjrpNVNNVJfFup7xTY3UpFtm67foy9HDVXneLtFVt4pMz5kZtgNcvCniNFb1hlEQ==", + "funding": [ + "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/fb55/htmlparser2?sponsor=1", + { + "type": "github", + "url": "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/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": "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/fb55/entities?sponsor=1" + } + }, + "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/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/loupe": { "version": "3.2.1", "resolved": "https://registry.npmjs.org/loupe/-/loupe-3.2.1.tgz", @@ -1116,6 +1575,45 @@ "@jridgewell/sourcemap-codec": "^1.5.5" } }, + "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": "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/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", @@ -1142,6 +1640,72 @@ "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" } }, + "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": "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/fb55/nth-check?sponsor=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": "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/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": "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/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": "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/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": "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/fb55/entities?sponsor=1" + } + }, "node_modules/pathe": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/pathe/-/pathe-1.1.2.tgz", @@ -1241,6 +1805,13 @@ "fsevents": "~2.3.2" } }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "license": "MIT", + "peer": true + }, "node_modules/siginfo": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", @@ -1272,6 +1843,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", @@ -1330,6 +1918,16 @@ "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/vite": { "version": "5.4.21", "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.21.tgz", @@ -1479,6 +2077,30 @@ } } }, + "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-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/why-is-node-running": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", @@ -1495,6 +2117,32 @@ "engines": { "node": ">=8" } + }, + "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": "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/sponsors/eemeli" + } } } } diff --git a/packages/surface/package.json b/packages/surface/package.json index 27ec996fd..9dd8c0854 100644 --- a/packages/surface/package.json +++ b/packages/surface/package.json @@ -41,5 +41,8 @@ "type": "git", "url": "git+https://github.com/AgentWorkforce/flows.git", "directory": "packages/surface" + }, + "peerDependencies": { + "@relayfile/relay-helpers": "0.4.11" } } diff --git a/packages/surface/src/context.ts b/packages/surface/src/context.ts index 2594266f8..69d3229a1 100644 --- a/packages/surface/src/context.ts +++ b/packages/surface/src/context.ts @@ -1,3 +1,4 @@ +import type { SlackHelper } from "./slack.js"; import type { CloudHelper } from "./cloud.js"; import type { RunCompletionReason } from "./completion.js"; import type { Step } from "./step.js"; @@ -37,4 +38,5 @@ export interface Ctx { dispatch(flow: string, input: unknown): Promise; done(reason: RunCompletionReason): void; cloud: CloudHelper; + slack: SlackHelper; } diff --git a/packages/surface/src/flow.ts b/packages/surface/src/flow.ts index fa1f358d8..cb0f4a3d5 100644 --- a/packages/surface/src/flow.ts +++ b/packages/surface/src/flow.ts @@ -5,7 +5,7 @@ export interface FlowHeader { identity?: string; memory?: { script?: boolean; agent?: boolean }; budget?: string; - tools?: { relayfile?: string[]; mcp?: string[] }; + tools?: { slack?: boolean; relayfile?: string[]; mcp?: string[] }; workspace?: string; } @@ -16,6 +16,7 @@ export interface ReadonlyFlowHeader { readonly memory?: Readonly<{ script?: boolean; agent?: boolean }>; readonly budget?: string; readonly tools?: Readonly<{ + slack?: boolean; relayfile?: readonly string[]; mcp?: readonly string[]; }>; @@ -129,6 +130,7 @@ function freezeHeader(header: FlowHeader): ReadonlyFlowHeader { const tools = header.tools === undefined ? undefined : Object.freeze({ + ...(header.tools.slack === undefined ? {} : { slack: header.tools.slack }), ...(header.tools.relayfile === undefined ? {} : { relayfile: Object.freeze([...header.tools.relayfile]) }), @@ -172,9 +174,10 @@ function assertFlowHeader(value: unknown, flowName: string): asserts value is Fl assertHeaderObject(value.tools, `${at}.tools`); assertKnownKeys( value.tools, - ["relayfile", "mcp"], + ["relayfile", "mcp", "slack"], `${at}.tools`, ); + assertOptionalBoolean(value.tools, "slack", `${at}.tools`); assertOptionalStringArray(value.tools, "relayfile", `${at}.tools`); assertOptionalStringArray(value.tools, "mcp", `${at}.tools`); } diff --git a/packages/surface/src/index.ts b/packages/surface/src/index.ts index 437d99f60..a8409d411 100644 --- a/packages/surface/src/index.ts +++ b/packages/surface/src/index.ts @@ -20,3 +20,4 @@ export { type FlowHandle, type FlowHeader, } from "./flow.js"; +export { flowRunWritebackIdempotency, type SlackHelper, type SlackReceipt } from "./slack.js"; diff --git a/packages/surface/src/slack.ts b/packages/surface/src/slack.ts new file mode 100644 index 000000000..1654d1735 --- /dev/null +++ b/packages/surface/src/slack.ts @@ -0,0 +1,20 @@ +import type { Step } from "./step.js"; + +export interface SlackReceipt { + channel: string; + ts: string; + ref: string; +} + +/** Slack writeback helpers; every call is a journal-backed effect. */ +export interface SlackHelper { + post(channel: string, text: string, opts?: { replyTo?: string }): Step; + dm(user: string, text: string): Step<{ user: string; ts: string }>; + reply(channel: string, threadTs: string, text: string): Step; + react(channel: string, messageTs: string, emoji: string): Step; +} + +/** Stable across attempts; deliberately independent of process-tick ordinals. */ +export function flowRunWritebackIdempotency(runId: string, stepId: string): string { + return `${runId}:${stepId}`; +} diff --git a/scripts/surface-package-gate.sh b/scripts/surface-package-gate.sh index 1d74d5a06..6be884857 100755 --- a/scripts/surface-package-gate.sh +++ b/scripts/surface-package-gate.sh @@ -113,6 +113,8 @@ import { type AgentOptions, type AgentResult, type CloudHelper, + type SlackHelper, + type SlackReceipt, type CompletionReason, type Ctx, type FlowHandle, @@ -131,6 +133,10 @@ const body = async (f: Ctx): Promise => { const options: AgentOptions = { task: 'review' }; const agent: Step = f.agent('reviewer', options); await agent; + const slack: SlackHelper = f.slack; + const post: Step = slack.post('#test', 'hi'); + await post; + await f.slack.post('#test', 'typed'); f.done('success'); }; const header: FlowHeader = { identity: 'packed-type-consumer' };