experimental-ash 0.18.2 → 0.18.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +7 -0
- package/dist/src/chunks/{host-DkTSR6YJ.js → host-C19hLVqS.js} +1 -1
- package/dist/src/cli/run.js +1 -1
- package/dist/src/evals/cli/eval.js +1 -1
- package/dist/src/internal/application/package.js +1 -1
- package/dist/src/internal/workflow-bundle/vercel-workflow-output.js +0 -2
- package/dist/src/public/channels/slack/api.d.ts +2 -27
- package/dist/src/public/channels/slack/api.js +6 -82
- package/dist/src/public/channels/slack/defaults.js +2 -2
- package/dist/src/public/channels/slack/hitl.js +6 -3
- package/dist/src/public/channels/slack/inbound.js +1 -1
- package/dist/src/public/channels/slack/limits.d.ts +19 -0
- package/dist/src/public/channels/slack/limits.js +23 -0
- package/dist/src/public/channels/slack/mrkdwn.d.ts +38 -0
- package/dist/src/public/channels/slack/mrkdwn.js +89 -0
- package/dist/src/public/channels/slack/slackChannel.js +1 -7
- package/dist/src/public/definitions/defineChannel.d.ts +1 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
# experimental-ash
|
|
2
2
|
|
|
3
|
+
## 0.18.3
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- 41a45f2: Remove unnecessary NODE_OPTIONS env
|
|
8
|
+
- 42e4f64: fix(slack): truncate HITL prompt before it hits Slack's section / message-text caps so long `ask_question` prompts no longer fail `chat.postMessage` with `invalid_blocks`
|
|
9
|
+
|
|
3
10
|
## 0.18.2
|
|
4
11
|
|
|
5
12
|
### Patch Changes
|
|
@@ -10,7 +10,7 @@ const workflowCode = \`${(e.code.endsWith(`
|
|
|
10
10
|
`)?e.code:`${e.code}\n`).replace(/[\\`$]/g,`\\$&`)}\`;
|
|
11
11
|
|
|
12
12
|
export const POST = workflowEntrypoint(workflowCode);`;if(!e.bundleFinalOutput){await Ct(e.outfile,t);return}let n=s(await l({cwd:e.workingDir,input:ct,external:e=>e===`@aws-sdk/credential-provider-web-identity`,platform:`node`,plugins:[mt(t)],write:!1,output:{comments:!1,format:e.format,sourcemap:!1}}),`final workflow bundle for "${e.outfile}"`);await Ct(e.outfile,n.code)}function yt(e){let t={};for(let[n,r]of Object.entries(e??{})){t[n]={};for(let[e,i]of Object.entries(r))t[n][e]={stepId:i.stepId}}return t}function bt(e){let t={};for(let[n,r]of Object.entries(e??{})){t[n]={};for(let[e,i]of Object.entries(r))t[n][e]={graph:{edges:[],nodes:[]},workflowId:i.workflowId}}return t}function xt(e){let t={};for(let[n,r]of Object.entries(e??{})){t[n]={};for(let[e,i]of Object.entries(r))t[n][e]={classId:i.classId}}return t}function St(e,t){let n=y(e,t).replaceAll(`\\`,`/`);return n.startsWith(`./`)||n.startsWith(`../`)?n:`./${n}`}function P(e){for(let t of e)if(ge(t))return{id:b(t)}}async function Ct(e,t){await S(_(e),{recursive:!0});let n=`${e}.${process.pid}.${Date.now()}.tmp`;await T(n,t),await me(n,e)}function wt(e,t){e.steps=F(e.steps,t.steps),e.workflows=F(e.workflows,t.workflows),e.classes=F(e.classes,t.classes)}function F(e,t){if(t===void 0)return e;let n={...e};for(let[e,r]of Object.entries(t))n[e]={...n[e],...r};return n}function Tt(e,t){let n=t.replaceAll(`\\`,`/`),r=y(e.replaceAll(`\\`,`/`),n).replaceAll(`\\`,`/`);return r.startsWith(`../`)&&(r=r.split(`/`).filter(e=>e!==`..`).join(`/`)),r}function Et(e){return/\.(?:[cm]?[jt]sx?)$/.test(e)}async function Dt(e){let t=[...e.discoveredEntries.discoveredSteps].sort(),n=new Set(t),r=[...e.discoveredEntries.discoveredSerdeFiles].sort().filter(e=>!n.has(e)),i=await kt({projectRoot:e.projectRoot,stepFiles:t,serdeOnlyFiles:r,workingDir:e.workingDir}),a=_(e.outfile),o=[`// Generated by Ash. Do not edit by hand.`,...Ot({builtinsImportSpecifier:e.builtinsPath===void 0?`workflow/internal/builtins`:I({outfileDirectory:a,preferAbsoluteFileImports:e.preferAbsoluteFileImports??!1,targetPath:e.builtinsPath}),outfileDirectory:a,preferAbsoluteFileImports:e.preferAbsoluteFileImports??!1,serdeOnlyFiles:r,stepFiles:t}),`export const __steps_registered = true;`,``].join(`
|
|
13
|
-
`);return await S(a,{recursive:!0}),await Nt(e.outfile)!==o&&await T(e.outfile,o),i}function Ot(e){return[e.builtinsImportSpecifier,...e.stepFiles.map(t=>I({outfileDirectory:e.outfileDirectory,preferAbsoluteFileImports:e.preferAbsoluteFileImports,targetPath:t})),...e.serdeOnlyFiles.map(t=>I({outfileDirectory:e.outfileDirectory,preferAbsoluteFileImports:e.preferAbsoluteFileImports,targetPath:t}))].map(e=>`import ${JSON.stringify(e)};`)}function I(e){return e.preferAbsoluteFileImports?t(e.targetPath):Mt(e.outfileDirectory,e.targetPath)}async function kt(e){let t={},n=[...e.stepFiles,...e.serdeOnlyFiles];for(let r of n){let n=await C(r,`utf8`);At(t,(await k(jt(e.workingDir,r),n,`step`,r,e.projectRoot)).workflowManifest)}return t}function At(e,t){e.steps=L(e.steps,t.steps),e.workflows=L(e.workflows,t.workflows),e.classes=L(e.classes,t.classes)}function L(e,t){if(t===void 0)return e;let n={...e};for(let[e,r]of Object.entries(t))n[e]={...n[e],...r};return n}function jt(e,t){let n=t.replaceAll(`\\`,`/`),r=y(e.replaceAll(`\\`,`/`),n).replaceAll(`\\`,`/`);return r.startsWith(`../`)&&(r=r.split(`/`).filter(e=>e!==`..`).join(`/`)),r}function Mt(e,t){let n=y(e,t).replaceAll(`\\`,`/`);return n.startsWith(`.`)?n:`./${n}`}async function Nt(e){try{return await C(e,`utf8`)}catch(e){if(e instanceof Error&&`code`in e&&e.code===`ENOENT`)return null;throw e}}const Pt=[`@mongodb-js/zstd`,`node-liblzma`],Ft=[`@chat-adapter/slack`,`chat`];function It(e){let t={};return Lt(e)&&Object.assign(t,e),t
|
|
13
|
+
`);return await S(a,{recursive:!0}),await Nt(e.outfile)!==o&&await T(e.outfile,o),i}function Ot(e){return[e.builtinsImportSpecifier,...e.stepFiles.map(t=>I({outfileDirectory:e.outfileDirectory,preferAbsoluteFileImports:e.preferAbsoluteFileImports,targetPath:t})),...e.serdeOnlyFiles.map(t=>I({outfileDirectory:e.outfileDirectory,preferAbsoluteFileImports:e.preferAbsoluteFileImports,targetPath:t}))].map(e=>`import ${JSON.stringify(e)};`)}function I(e){return e.preferAbsoluteFileImports?t(e.targetPath):Mt(e.outfileDirectory,e.targetPath)}async function kt(e){let t={},n=[...e.stepFiles,...e.serdeOnlyFiles];for(let r of n){let n=await C(r,`utf8`);At(t,(await k(jt(e.workingDir,r),n,`step`,r,e.projectRoot)).workflowManifest)}return t}function At(e,t){e.steps=L(e.steps,t.steps),e.workflows=L(e.workflows,t.workflows),e.classes=L(e.classes,t.classes)}function L(e,t){if(t===void 0)return e;let n={...e};for(let[e,r]of Object.entries(t))n[e]={...n[e],...r};return n}function jt(e,t){let n=t.replaceAll(`\\`,`/`),r=y(e.replaceAll(`\\`,`/`),n).replaceAll(`\\`,`/`);return r.startsWith(`../`)&&(r=r.split(`/`).filter(e=>e!==`..`).join(`/`)),r}function Mt(e,t){let n=y(e,t).replaceAll(`\\`,`/`);return n.startsWith(`.`)?n:`./${n}`}async function Nt(e){try{return await C(e,`utf8`)}catch(e){if(e instanceof Error&&`code`in e&&e.code===`ENOENT`)return null;throw e}}const Pt=[`@mongodb-js/zstd`,`node-liblzma`],Ft=[`@chat-adapter/slack`,`chat`];function It(e){let t={};return Lt(e)&&Object.assign(t,e),t}function Lt(e){return typeof e==`object`&&!!e&&!Array.isArray(e)}async function Rt(e){await w(e,{force:!0,recursive:!0}),await S(e,{recursive:!0})}async function zt(e){try{return await pe(e.sourcePath)}catch{return await pe(e.fallbackPath)}}async function Bt(e){let t=await zt({fallbackPath:e.fallbackPath,sourcePath:e.sourcePath});await Rt(e.targetPath),await x(t,e.targetPath,{dereference:!0,recursive:!0})}function Vt(e){let t=y(e.fromDirectoryPath,e.toFilePath).replaceAll(`\\`,`/`);return t.startsWith(`.`)?t:`./${t}`}function Ht(e){return[`import nitroHandler from ${JSON.stringify(e.delegateImportPath)};`,``,`function invokeNitroHandler(request, context) {`,` if (typeof nitroHandler === "function") {`,` return nitroHandler(request, context);`,` }`,``,` if (nitroHandler !== null && typeof nitroHandler === "object" && "fetch" in nitroHandler) {`,` const fetch = nitroHandler.fetch;`,` if (typeof fetch === "function") {`,` return fetch.call(nitroHandler, request, context);`,` }`,` }`,``,` throw new TypeError("Expected Nitro handler to export a function or an object with fetch(request, context).");`,`}`,``,`const workflowRoutePath = ${JSON.stringify(e.workflowRoutePath)};`,``,`function rewriteRequestToWorkflowRoute(request) {`,` const sourceUrl = new URL(request.url);`,` const routedUrl = new URL(workflowRoutePath, sourceUrl);`,` routedUrl.search = sourceUrl.search;`,` return new Request(routedUrl, request);`,`}`,``,`export default {`,` fetch(request, context) {`,` return invokeNitroHandler(rewriteRequestToWorkflowRoute(request), context);`,` },`,`};`,``].join(`
|
|
14
14
|
`)}async function Ut(e){try{let t=JSON.parse(await C(v(e,`.vc-config.json`),`utf8`));if(typeof t.handler==`string`&&t.handler.length>0)return t.handler}catch{}return`index.mjs`}async function Wt(e){let t=await Ut(e.functionDirectoryPath),n=v(e.functionDirectoryPath,t),r=_(n),i=le(t),a=v(r,`__ash_nitro_handler__${i.length>0?i:`.mjs`}`),o=Vt({fromDirectoryPath:r,toFilePath:a});await me(n,a),await T(n,Ht({delegateImportPath:o,workflowRoutePath:e.workflowRoutePath}))}const Gt=new Map;var Kt=class{#e;#t;config;#n=new WeakMap;constructor(e){this.config={buildTarget:`standalone`,dirs:[m(`src/execution`)],externalPackages:[...Pt,...Ft],projectRoot:e.appRoot,watch:e.watch,workingDir:e.rootDir},this.#e=e.compiledArtifactsBootstrapPath,this.#t=e.outDir}async build(e={}){let t=(Gt.get(this.#t)??Promise.resolve()).then(()=>this.#r(e));Gt.set(this.#t,t.catch(()=>{})),await t}async#r(e){await E(this.#t);let t=await this.#i();if(t.length===0)throw Error(`Expected the execution workflow source file under "${m(`src/execution`)}".`);let n=await this.findTsConfigPath();await S(this.#t,{recursive:!0});let r=await this.discoverEntries(t,this.#t,n),i=v(this.#t,`workflows.mjs`),{manifest:a}=await this.createWorkflowsBundle({discoveredEntries:r,keepInterimBundleContext:!1,outfile:i,bundleFinalOutput:!1,format:`esm`,inputFiles:t,tsconfigPath:n}),o=v(this.#t,`steps.mjs`),s=await Dt({builtinsPath:u(`workflow/internal/builtins`),discoveredEntries:r,outfile:o,preferAbsoluteFileImports:!0,projectRoot:this.config.projectRoot??this.config.workingDir,workingDir:this.config.workingDir}),c=e.nitroStepOutfile;c!==void 0&&c!==o&&await Dt({builtinsPath:u(`workflow/internal/builtins`),discoveredEntries:r,outfile:c,preferAbsoluteFileImports:!0,projectRoot:this.config.projectRoot??this.config.workingDir,workingDir:this.config.workingDir}),await qt(i,o),await Jt(i),await Yt(i);let l=e.nitroWorkflowOutfile;l!==void 0&&l!==i&&(await S(_(l),{recursive:!0}),await nn(i,l),c!==void 0&&(await qt(l,c),await Jt(l),await Yt(l))),await this.createManifest({workflowBundlePath:v(this.#t,`workflows.mjs`),manifestDir:this.#t,manifest:{steps:{...s.steps,...a.steps},workflows:{...s.workflows,...a.workflows},classes:{...s.classes,...a.classes}}}),await De(this.#t)}get transformProjectRoot(){return this.config.projectRoot??this.config.workingDir}async findTsConfigPath(){let e=this.config.workingDir;for(;;){for(let t of[`tsconfig.json`,`jsconfig.json`]){let n=v(e,t);try{return await C(n),n}catch(e){if(!(e instanceof Error&&`code`in e&&e.code===`ENOENT`))throw e}}let t=_(e);if(t===e)return;e=t}}async getInputFiles(){let e=this.config.dirs.map(e=>b(this.config.workingDir,e));return(await Promise.all(e.map(e=>ft(e)))).flat()}async discoverEntries(e,t,n){let r=this.#n.get(e);if(r!==void 0)return r;let i={discoveredSerdeFiles:[],discoveredSteps:[],discoveredWorkflows:[]};for(let t of e){let e=Qe(await C(t,`utf8`));e.hasUseStep&&i.discoveredSteps.push(t),e.hasUseWorkflow&&i.discoveredWorkflows.push(t),e.hasSerde&&i.discoveredSerdeFiles.push(t)}return this.#n.set(e,i),i}async createWorkflowsBundle({bundleFinalOutput:e=!0,discoveredEntries:t,format:n=`cjs`,inputFiles:r,keepInterimBundleContext:i=this.config.watch,outfile:a,tsconfigPath:o}){let c=t??await this.discoverEntries(r,_(a),o),u=[...c.discoveredWorkflows].sort(),d=new Set(u),f=[...c.discoveredSerdeFiles].sort().filter(e=>!d.has(e)),p={},m=[...u.map(e=>pt(e,this.config.workingDir)),...f.map(e=>pt(e,this.config.workingDir))].join(`
|
|
15
15
|
`);return await vt({bundleFinalOutput:e,code:s(await l({cwd:this.config.workingDir,input:ct,platform:`neutral`,plugins:[mt(m),ht(),gt(this.config.workingDir,{workflowCondition:!0}),_t({manifest:p,projectRoot:this.transformProjectRoot,sideEffectFiles:[...u,...f],workingDir:this.config.workingDir})],resolve:{conditionNames:[`ash-source`,`workflow`,`node`,`import`,`default`],extensions:[`.ts`,`.tsx`,`.mts`,`.cts`,`.js`,`.jsx`,`.mjs`,`.cjs`],mainFields:[`module`,`main`]},tsconfig:o??!1,write:!1,output:{banner:`globalThis.__private_workflows = new Map();`,codeSplitting:!1,comments:!1,format:`cjs`,sourcemap:`inline`}}),`intermediate workflow bundle for "${a}"`).code,format:n,outfile:a,workingDir:this.config.workingDir}),i?{bundleFinal:async t=>{await vt({bundleFinalOutput:e,code:t,format:n,outfile:a,workingDir:this.config.workingDir})},interimBundleCtx:void 0,manifest:p}:{manifest:p}}async createManifest({manifest:e,manifestDir:t}){let n={version:`1.0.0`,steps:yt(e.steps),workflows:bt(e.workflows),classes:xt(e.classes)},r=JSON.stringify(n,null,2);return await S(t,{recursive:!0}),await T(v(t,`manifest.json`),r),r}async buildVercelOutput(e){await this.build();let t=v(this.#t,`vercel-build-output`,`functions`,`.well-known`,`workflow`,`v1`),n=v(t,`flow.func`),r=v(e.outputDir,`functions`,`.well-known`,`workflow`,`v1`),i=v(e.flowNitroOutputDir,`functions`,`__server.func`),a=v(e.flowNitroOutputDir,`functions`,`.well-known`,`workflow`,`v1`,`flow.func`),o=v(r,`flow.func`),s=v(r,`step.func`),c=v(r,`webhook`,`[token].func`);await Bt({fallbackPath:i,sourcePath:a,targetPath:n}),await Promise.all([this.#a(n,{experimentalTriggers:Array.from([Xe]),maxDuration:`max`,runtime:e.runtime??null,shouldAddHelpers:!1}),x(v(this.#t,`manifest.json`),v(t,`manifest.json`))]),await Wt({functionDirectoryPath:n,workflowRoutePath:`/.well-known/workflow/v1/flow`}),await Promise.all([w(o,{force:!0,recursive:!0}),w(s,{force:!0,recursive:!0}),w(c,{force:!0,recursive:!0})]),await S(r,{recursive:!0}),await Promise.all([x(n,o,{recursive:!0}),x(v(t,`manifest.json`),v(r,`manifest.json`))])}async#i(){return[...await this.getInputFiles(),this.#e]}async#a(e,t){let n=v(e,`.vc-config.json`),r=await this.#o(n),i={...r};i.environment=It(r.environment),t.runtime!==null&&(i.runtime=t.runtime),t.maxDuration!==void 0&&(i.maxDuration=t.maxDuration),t.shouldAddHelpers!==void 0&&(i.shouldAddHelpers=t.shouldAddHelpers),t.shouldAddSourcemapSupport!==void 0&&(i.shouldAddSourcemapSupport=t.shouldAddSourcemapSupport),t.experimentalTriggers!==void 0&&(i.experimentalTriggers=[...t.experimentalTriggers]),await T(n,`${JSON.stringify(i,null,2)}\n`)}async#o(e){try{let t=JSON.parse(await C(e,`utf8`));if(typeof t==`object`&&t)return t}catch{}return{}}};async function qt(e,t){let n=await R(e);if(n===null||n.includes(`__ashWorkflowStepsRegistered`))return;let r=en(_(e),t),i=[`import { __steps_registered as __ashWorkflowStepsRegistered } from ${JSON.stringify(r)};`,`void __ashWorkflowStepsRegistered;`,``].join(`
|
|
16
16
|
`),a=n.match(/^import\s.+?;\n/m);if(a===null||a.index===void 0){await T(e,`${i}${n}`);return}let o=a.index+a[0].length;await T(e,`${n.slice(0,o)}${i}${n.slice(o)}`)}async function Jt(e){let t=await R(e);if(t===null)return;let n=t;for(let e of[`workflow`,`workflow/api`,`workflow/internal/builtins`,`workflow/internal/private`,`workflow/runtime`]){let t=$t(u(e));n=Qt(n,e,t)}n!==t&&await T(e,n)}async function Yt(e){let t=await R(e);if(t===null)return;let n=t.indexOf(`const workflowCode = `),r=t.lastIndexOf(`;
|
package/dist/src/cli/run.js
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
import{t as e}from"../chunks/package-DmsQgn4v.js";import{createCliTheme as t,renderCliTaggedLine as n}from"./ui/output.js";import{i as r,n as i,r as a,t as o}from"../chunks/url-BVRhVE2O.js";import{resolve as s}from"node:path";async function c(){return(await import(`../chunks/host-
|
|
1
|
+
import{t as e}from"../chunks/package-DmsQgn4v.js";import{createCliTheme as t,renderCliTaggedLine as n}from"./ui/output.js";import{i as r,n as i,r as a,t as o}from"../chunks/url-BVRhVE2O.js";import{resolve as s}from"node:path";async function c(){return(await import(`../chunks/host-C19hLVqS.js`).then(e=>e.t)).buildHost}async function l(){return(await import(`./commands/info.js`)).printApplicationInfo}async function u(){return(await import(`./dev/repl.js`)).runDevelopmentRepl}async function d(){return(await import(`../evals/cli/eval.js`)).runEvalCommand}async function f(){return(await import(`../chunks/host-C19hLVqS.js`).then(e=>e.t)).startHost}function p(e=process.cwd()){return s(e)}function m(e){return`Ash (v${e})`}function h(e){return e.name()===`info`||e.name()===`dev`}async function g(e){await new Promise((t,n)=>{let r=!1,i=()=>{process.off(`SIGINT`,a),process.off(`SIGTERM`,a)},a=()=>{r||(r=!0,i(),e.close().then(t,n))};process.once(`SIGINT`,a),process.once(`SIGTERM`,a)})}function _(e){if(!/^-?\d+$/.test(e))throw new r(`Expected a numeric port, received "${e}".`);let t=Number(e);if(!Number.isInteger(t))throw new r(`Expected a numeric port, received "${e}".`);if(t<0||t>65535)throw new r(`Expected a port between 0 and 65535, received "${e}".`);return t}function v(){return!!(process.stdin.isTTY&&process.stdout.isTTY)}function y(e){let t=e[1];return e[0]!==`dev`||e.length!==2||t===void 0||t.startsWith(`-`)?[...e]:[`dev`,`--url`,t]}function b(e){if(e.url){if(e.host!==void 0)throw new r(`The --host option cannot be used with --url.`);if(e.port!==void 0)throw new r(`The --port option cannot be used with --url.`);if(e.repl===!1)throw new r(`The --no-repl option cannot be used with --url.`);return e.url}}function x(r,a){let s=p(),y=e().version,x=new i,S=t();return x.name(`ash`).description(`Build and run an Ash application.`).version(y).showHelpAfterError().exitOverride().hook(`preAction`,(e,t)=>{h(t)&&r.log(m(y))}).configureOutput({writeErr:e=>{r.error(e.trimEnd())},writeOut:e=>{r.log(e.trimEnd())}}),x.command(`build`).description(`Build the current Ash application.`).action(async()=>{let{loadDevelopmentEnvironmentFiles:e}=await import(`./dev/environment.js`);e(s);let t=await(a.buildHost??await c())(s);r.log(n(S,{message:`built output at ${t}`,tag:`build`,tone:`success`}))}),x.command(`dev`).description(`Start the Ash development server or connect the REPL to an existing URL.`).option(`--host <host>`,`Host interface to bind`).option(`--no-repl`,`Start the server without the interactive REPL`).option(`--port <port>`,`Port to listen on (defaults to $PORT, then 3000)`,_).option(`--schedules`,`Run scheduled tasks during development (off by default)`).option(`-u, --url <url>`,`Connect the REPL to an existing server URL`,o).addHelpText(`after`,`
|
|
2
2
|
You can also pass a bare URL as the only argument, for example: ash dev https://example.com
|
|
3
3
|
`).action(async e=>{let t=b(e),{loadDevelopmentEnvironmentFiles:i}=await import(`./dev/environment.js`);if(i(s),t){if(r.log(n(S,{message:`REPL connecting to ${t}`,tag:`dev`,tone:`info`})),!v()){r.log(n(S,{message:`Interactive REPL disabled because the current terminal is not a TTY.`,tag:`dev`,tone:`warning`}));return}r.log(``),await(a.runDevelopmentRepl??await u())({serverUrl:t});return}let o=await(a.startHost??await f())(s,{host:e.host,port:e.port,schedules:e.schedules===!0}),c=!1,l=async()=>{c||(c=!0,await o.close())};try{if(r.log(n(S,{message:`server listening at ${o.url}`,tag:`dev`,tone:`success`})),e.repl===!1)return await g({close:l});if(!v())return r.log(n(S,{message:`Interactive REPL disabled because the current terminal is not a TTY.`,tag:`dev`,tone:`warning`})),await g({close:l});r.log(``),await(a.runDevelopmentRepl??await u())({serverUrl:o.url})}finally{await l()}}),x.command(`info`).description(`Print resolved application information.`).action(async()=>{await(a.printApplicationInfo??await l())(r,s)}),x.command(`eval`).description(`Run eval suites against an Ash agent.`).option(`--suite <id...>`,`Suite IDs to run (repeatable)`).option(`--all`,`Run all discovered suites`).option(`--url <url>`,`Remote agent URL (skip local host startup)`).option(`--timeout <ms>`,`Per-case timeout in milliseconds`).option(`--max-concurrency <n>`,`Max concurrent case executions per suite`).option(`--json`,`Output results as JSON`).option(`--list-suites`,`List discovered suites and exit`).option(`--skip-report`,`Skip suite-defined reporters (e.g. Braintrust)`).action(async e=>{await(a.runEvalCommand??await d())(e,r)}),x}async function S(e=process.argv.slice(2),t=console,n={}){let r=x(t,n),i=e.length===0?[`info`]:y(e);try{await r.parseAsync(i,{from:`user`})}catch(e){if(e instanceof a){if(e.exitCode===0)return;throw Error(e.message)}throw e}}export{S as runCli};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{n as e}from"../../chunks/paths-Dwv0Eash.js";import{loadDevelopmentEnvironmentFiles as t}from"../../cli/dev/environment.js";import{a as n,n as r,t as i}from"../../chunks/client-CKsU8Li3.js";import{n as a}from"../../chunks/host-
|
|
1
|
+
import{n as e}from"../../chunks/paths-Dwv0Eash.js";import{loadDevelopmentEnvironmentFiles as t}from"../../cli/dev/environment.js";import{a as n,n as r,t as i}from"../../chunks/client-CKsU8Li3.js";import{n as a}from"../../chunks/host-C19hLVqS.js";import{discoverAndImportSuites as o,discoverSuiteFiles as s,importSuiteFile as c}from"../runner/discover.js";import{executeSuite as l}from"../runner/execute-suite.js";import{ConsoleReporter as u}from"../runner/reporters/console.js";var d=n();function f(e,t){e.command(`eval`).description(`Run eval suites against an Ash agent.`).option(`--suite <id...>`,`Suite IDs to run (repeatable)`).option(`--all`,`Run all discovered suites`).option(`--url <url>`,`Remote agent URL (skip local host startup)`).option(`--timeout <ms>`,`Per-case timeout in milliseconds`).option(`--max-concurrency <n>`,`Max concurrent case executions per suite`).option(`--json`,`Output results as JSON`).option(`--list-suites`,`List discovered suites and exit`).option(`--skip-report`,`Skip suite-defined reporters (e.g. Braintrust)`).action(async e=>{await p(e,t)})}async function p(n,r){let i=e();if(t(i),n.listSuites){await y(i,r);return}let s=n.suite,c=await o(i,s);if(c.length===0){s&&s.length>0?r.error(`No suites found matching: ${s.join(`, `)}`):r.error(`No eval suites found. Create suite files under evals/ with the *.eval.ts extension.`),process.exitCode=1;return}let u,d;n.url?d={kind:`remote`,url:n.url}:(u=await a(i,{host:`127.0.0.1`,port:0}),d={kind:`local`,url:u.url});let f=m(d);try{let e=[];for(let t of c){let r=_(t,n),a=v(r,{json:n.json===!0,skipReport:n.skipReport===!0}),o=await l({suite:r,target:d,reporters:a,appRoot:i,client:f});e.push(o)}n.json&&r.log(JSON.stringify(e,null,2)),e.some(e=>e.errored>0)&&(process.exitCode=1)}finally{u&&await u.close()}process.exit(process.exitCode??0)}function m(e){if(e.kind===`local`)return new i({host:e.url});let t={},n=process.env.VERCEL_AUTOMATION_BYPASS_SECRET?.trim();return n&&(t[r]=n),new i({auth:h(),headers:Object.keys(t).length>0?t:void 0,host:e.url})}function h(){let e=process.env.ASH_EVAL_AUTH_TOKEN?.trim();return e?{bearer:e}:{bearer:g}}async function g(){try{let e=(await(0,d.getVercelOidcToken)()).trim();if(e.length>0)return e}catch{}return process.env.VERCEL_OIDC_TOKEN?.trim()??``}function _(e,t){let n=t.maxConcurrency?Number.parseInt(t.maxConcurrency,10):void 0,r=t.timeout?Number.parseInt(t.timeout,10):void 0;if(n===void 0&&r===void 0)return e;let i={...e};return n!==void 0&&(i.maxConcurrency=n),r!==void 0&&(i.timeoutMs=r),i}function v(e,t){let n=t.json?[]:[new u];return!t.skipReport&&e.reporters&&n.push(...e.reporters),n}async function y(e,t){let n=await s(e);if(n.length===0){t.log(`No eval suites found.`);return}t.log(`Found ${n.length} eval suite file(s):\n`);for(let r of n){let n=await c(e,r);t.log(` ${n.id}${n.description?` - ${n.description}`:``}`)}}export{f as registerEvalCommand,p as runEvalCommand};
|
|
@@ -6,7 +6,7 @@ import { ASH_PACKAGE_NAME } from "#package-name.js";
|
|
|
6
6
|
let cachedPackageInfo;
|
|
7
7
|
// The package build stamps the published version into `dist` so bundled
|
|
8
8
|
// deployments can still report package metadata without resolving package.json.
|
|
9
|
-
const BUNDLED_FALLBACK_PACKAGE_VERSION = "0.18.
|
|
9
|
+
const BUNDLED_FALLBACK_PACKAGE_VERSION = "0.18.3";
|
|
10
10
|
const BUNDLED_FALLBACK_PACKAGE_VERSION_PLACEHOLDER = "__ASH_PACKAGE_VERSION_PLACEHOLDER__";
|
|
11
11
|
const WORKFLOW_MODULE_ALIASES = {
|
|
12
12
|
"workflow/api": "src/compiled/@workflow/core/runtime.js",
|
|
@@ -11,7 +11,6 @@ export const WORKFLOW_STEP_EXTERNAL_PACKAGES = ["@mongodb-js/zstd", "node-liblzm
|
|
|
11
11
|
* Nitro performs the final bundling/tracing pass for hosted output.
|
|
12
12
|
*/
|
|
13
13
|
export const WORKFLOW_BUILDER_DEFERRED_PACKAGES = ["@chat-adapter/slack", "chat"];
|
|
14
|
-
const WORKFLOW_FUNCTION_NODE_OPTIONS = "--experimental-require-module";
|
|
15
14
|
/**
|
|
16
15
|
* Builds the environment block every generated Vercel workflow function needs.
|
|
17
16
|
*/
|
|
@@ -20,7 +19,6 @@ export function createWorkflowFunctionEnvironment(environment) {
|
|
|
20
19
|
if (isRecord(environment)) {
|
|
21
20
|
Object.assign(nextEnvironment, environment);
|
|
22
21
|
}
|
|
23
|
-
nextEnvironment.NODE_OPTIONS = WORKFLOW_FUNCTION_NODE_OPTIONS;
|
|
24
22
|
return nextEnvironment;
|
|
25
23
|
}
|
|
26
24
|
function isRecord(value) {
|
|
@@ -272,7 +272,7 @@ export interface SlackBinding {
|
|
|
272
272
|
*
|
|
273
273
|
* Auto-anchor: when the binding starts without a `threadTs`, the first
|
|
274
274
|
* `chat.postMessage` adopts its own `ts` as the thread root; the live
|
|
275
|
-
* `threadTs` is updated and `
|
|
275
|
+
* `threadTs` is updated and `onThreadTsChanged` fires so the caller can persist
|
|
276
276
|
* the anchor. Ephemerals and files-only posts do not anchor.
|
|
277
277
|
*/
|
|
278
278
|
export declare function buildSlackBinding(input: {
|
|
@@ -280,31 +280,6 @@ export declare function buildSlackBinding(input: {
|
|
|
280
280
|
readonly channelId: string;
|
|
281
281
|
readonly threadTs: string;
|
|
282
282
|
readonly teamId: string | undefined;
|
|
283
|
-
readonly
|
|
283
|
+
readonly onThreadTsChanged?: (ts: string) => void;
|
|
284
284
|
}): SlackBinding;
|
|
285
|
-
/**
|
|
286
|
-
* Best-effort GFM → Slack mrkdwn converter used only in contexts that
|
|
287
|
-
* do not support `markdown_text` (e.g. `files.completeUploadExternal`'s
|
|
288
|
-
* `initial_comment` field).
|
|
289
|
-
*
|
|
290
|
-
* The main `{ markdown }` post path sends `markdown_text` directly
|
|
291
|
-
* to `chat.postMessage` and does not go through this converter.
|
|
292
|
-
*/
|
|
293
|
-
export declare function gfmToSlackMrkdwn(input: string): string;
|
|
294
|
-
/**
|
|
295
|
-
* Best-effort Slack mrkdwn → GFM converter applied to the text of
|
|
296
|
-
* every inbound Slack message before the harness sees it.
|
|
297
|
-
*
|
|
298
|
-
* - `<@U123>` → `@U123`
|
|
299
|
-
* - `<#C123|name>` → `#name` (or `#C123` when no name)
|
|
300
|
-
* - `<!channel>` etc. → `@channel`
|
|
301
|
-
* - `<https://x|label>` → `[label](https://x)`
|
|
302
|
-
* - `<https://x>` → `https://x`
|
|
303
|
-
* - `*bold*` (paired) → `**bold**`
|
|
304
|
-
* - `~strike~` (paired) → `~~strike~~`
|
|
305
|
-
*
|
|
306
|
-
* Inline `_italic_` and code spans pass through unchanged because both
|
|
307
|
-
* formats render them identically.
|
|
308
|
-
*/
|
|
309
|
-
export declare function slackMrkdwnToGfm(input: string): string;
|
|
310
285
|
export {};
|
|
@@ -18,6 +18,7 @@ import { isCardElement } from "#compiled/chat/index.js";
|
|
|
18
18
|
import { createLogger } from "#internal/logging.js";
|
|
19
19
|
import { encodeSlackApiBody } from "#public/channels/slack/api-encoding.js";
|
|
20
20
|
import { cardToBlocks, cardToFallbackText } from "#public/channels/slack/blocks.js";
|
|
21
|
+
import { gfmToSlackMrkdwn, rewriteBareMentions, slackMrkdwnToGfm, } from "#public/channels/slack/mrkdwn.js";
|
|
21
22
|
const log = createLogger("slack.api");
|
|
22
23
|
/**
|
|
23
24
|
* Builds the Slack channel-local continuation token
|
|
@@ -72,18 +73,18 @@ export function createSlackRequester(botToken) {
|
|
|
72
73
|
*
|
|
73
74
|
* Auto-anchor: when the binding starts without a `threadTs`, the first
|
|
74
75
|
* `chat.postMessage` adopts its own `ts` as the thread root; the live
|
|
75
|
-
* `threadTs` is updated and `
|
|
76
|
+
* `threadTs` is updated and `onThreadTsChanged` fires so the caller can persist
|
|
76
77
|
* the anchor. Ephemerals and files-only posts do not anchor.
|
|
77
78
|
*/
|
|
78
79
|
export function buildSlackBinding(input) {
|
|
79
80
|
const request = createSlackRequester(input.botToken);
|
|
80
81
|
const messages = [];
|
|
81
82
|
let currentThreadTs = input.threadTs;
|
|
82
|
-
function
|
|
83
|
-
if (currentThreadTs
|
|
83
|
+
function handleMessageTs(ts) {
|
|
84
|
+
if (currentThreadTs || ts === currentThreadTs)
|
|
84
85
|
return;
|
|
85
86
|
currentThreadTs = ts;
|
|
86
|
-
input.
|
|
87
|
+
input.onThreadTsChanged?.(ts);
|
|
87
88
|
}
|
|
88
89
|
async function uploadFiles(files, options) {
|
|
89
90
|
if (files.length === 0) {
|
|
@@ -168,7 +169,7 @@ export function buildSlackBinding(input) {
|
|
|
168
169
|
throw new Error(`Slack chat.postMessage failed: ${response.error ?? "unknown_error"}`);
|
|
169
170
|
}
|
|
170
171
|
const id = typeof response.ts === "string" ? response.ts : "";
|
|
171
|
-
|
|
172
|
+
handleMessageTs(id);
|
|
172
173
|
// blocks / card + files: structured message lands first, then upload
|
|
173
174
|
// files as a follow-up post in the same thread.
|
|
174
175
|
if (files.length > 0 && hasStructured) {
|
|
@@ -313,70 +314,6 @@ function parseThreadMessage(raw, threadRootTs) {
|
|
|
313
314
|
raw,
|
|
314
315
|
};
|
|
315
316
|
}
|
|
316
|
-
const BARE_MENTION_RE = /(?<![<\w])@(\w+)/gu;
|
|
317
|
-
function rewriteBareMentions(text) {
|
|
318
|
-
return text.replace(BARE_MENTION_RE, "<@$1>");
|
|
319
|
-
}
|
|
320
|
-
/**
|
|
321
|
-
* Best-effort GFM → Slack mrkdwn converter used only in contexts that
|
|
322
|
-
* do not support `markdown_text` (e.g. `files.completeUploadExternal`'s
|
|
323
|
-
* `initial_comment` field).
|
|
324
|
-
*
|
|
325
|
-
* The main `{ markdown }` post path sends `markdown_text` directly
|
|
326
|
-
* to `chat.postMessage` and does not go through this converter.
|
|
327
|
-
*/
|
|
328
|
-
export function gfmToSlackMrkdwn(input) {
|
|
329
|
-
const segments = splitCodeFences(input);
|
|
330
|
-
return segments
|
|
331
|
-
.map((segment) => (segment.kind === "code" ? segment.text : convertInline(segment.text)))
|
|
332
|
-
.join("");
|
|
333
|
-
}
|
|
334
|
-
/**
|
|
335
|
-
* Best-effort Slack mrkdwn → GFM converter applied to the text of
|
|
336
|
-
* every inbound Slack message before the harness sees it.
|
|
337
|
-
*
|
|
338
|
-
* - `<@U123>` → `@U123`
|
|
339
|
-
* - `<#C123|name>` → `#name` (or `#C123` when no name)
|
|
340
|
-
* - `<!channel>` etc. → `@channel`
|
|
341
|
-
* - `<https://x|label>` → `[label](https://x)`
|
|
342
|
-
* - `<https://x>` → `https://x`
|
|
343
|
-
* - `*bold*` (paired) → `**bold**`
|
|
344
|
-
* - `~strike~` (paired) → `~~strike~~`
|
|
345
|
-
*
|
|
346
|
-
* Inline `_italic_` and code spans pass through unchanged because both
|
|
347
|
-
* formats render them identically.
|
|
348
|
-
*/
|
|
349
|
-
export function slackMrkdwnToGfm(input) {
|
|
350
|
-
const segments = splitCodeFences(input);
|
|
351
|
-
return segments
|
|
352
|
-
.map((segment) => (segment.kind === "code" ? segment.text : decodeInline(segment.text)))
|
|
353
|
-
.join("");
|
|
354
|
-
}
|
|
355
|
-
function splitCodeFences(input) {
|
|
356
|
-
const segments = [];
|
|
357
|
-
const fenceRe = /```[\s\S]*?```|`[^`\n]+`/gu;
|
|
358
|
-
let lastIndex = 0;
|
|
359
|
-
for (const match of input.matchAll(fenceRe)) {
|
|
360
|
-
const start = match.index ?? 0;
|
|
361
|
-
if (start > lastIndex) {
|
|
362
|
-
segments.push({ kind: "text", text: input.slice(lastIndex, start) });
|
|
363
|
-
}
|
|
364
|
-
segments.push({ kind: "code", text: match[0] });
|
|
365
|
-
lastIndex = start + match[0].length;
|
|
366
|
-
}
|
|
367
|
-
if (lastIndex < input.length) {
|
|
368
|
-
segments.push({ kind: "text", text: input.slice(lastIndex) });
|
|
369
|
-
}
|
|
370
|
-
return segments;
|
|
371
|
-
}
|
|
372
|
-
function convertInline(input) {
|
|
373
|
-
let out = input;
|
|
374
|
-
out = out.replace(/\*\*([^*\n]+)\*\*/gu, "*$1*");
|
|
375
|
-
out = out.replace(/__([^_\n]+)__/gu, "*$1*");
|
|
376
|
-
out = out.replace(/~~([^~\n]+)~~/gu, "~$1~");
|
|
377
|
-
out = out.replace(/\[([^\]\n]+)\]\(([^)\s]+)\)/gu, "<$2|$1>");
|
|
378
|
-
return out;
|
|
379
|
-
}
|
|
380
317
|
/**
|
|
381
318
|
* Normalize a {@link FileUpload.data} value (`Buffer | Blob | ArrayBuffer`) to
|
|
382
319
|
* a contiguous `Buffer` we can both POST and length-prefix without
|
|
@@ -394,16 +331,3 @@ async function readFileBytes(data) {
|
|
|
394
331
|
}
|
|
395
332
|
throw new Error("FileUpload.data must be a Buffer, ArrayBuffer, or Blob.");
|
|
396
333
|
}
|
|
397
|
-
function decodeInline(input) {
|
|
398
|
-
let out = input;
|
|
399
|
-
out = out.replace(/<!(channel|here|everyone)>/gu, "@$1");
|
|
400
|
-
out = out.replace(/<@([A-Z0-9]+)\|([^>]+)>/gu, "@$2");
|
|
401
|
-
out = out.replace(/<@([A-Z0-9]+)>/gu, "@$1");
|
|
402
|
-
out = out.replace(/<#([A-Z0-9]+)\|([^>]+)>/gu, "#$2");
|
|
403
|
-
out = out.replace(/<#([A-Z0-9]+)>/gu, "#$1");
|
|
404
|
-
out = out.replace(/<(https?:\/\/[^|>\s]+)\|([^>]+)>/gu, "[$2]($1)");
|
|
405
|
-
out = out.replace(/<(https?:\/\/[^>\s]+)>/gu, "$1");
|
|
406
|
-
out = out.replace(/(^|[^*])\*([^*\n]+)\*(?!\*)/gu, "$1**$2**");
|
|
407
|
-
out = out.replace(/(^|[^~])~([^~\n]+)~(?!~)/gu, "$1~~$2~~");
|
|
408
|
-
return out;
|
|
409
|
-
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { createLogger, extractErrorId, formatErrorHint } from "#internal/logging.js";
|
|
2
2
|
import { buildAuthCompletedText, buildAuthEphemeralBlocks, buildAuthRequiredPublicText, formatConnectionDisplayName, } from "#public/channels/slack/connections.js";
|
|
3
3
|
import { renderInputRequestBlocks } from "#public/channels/slack/hitl.js";
|
|
4
|
-
import { truncateTypingStatus } from "#public/channels/slack/limits.js";
|
|
4
|
+
import { truncateMessageText, truncateTypingStatus } from "#public/channels/slack/limits.js";
|
|
5
5
|
const log = createLogger("slack.defaults");
|
|
6
6
|
/**
|
|
7
7
|
* Workspace-scoped projection of the Slack actor that produced
|
|
@@ -84,7 +84,7 @@ export function defaultInputRequestedHandler() {
|
|
|
84
84
|
return async (data, ctx) => {
|
|
85
85
|
if (data.requests.length === 0)
|
|
86
86
|
return;
|
|
87
|
-
const promptText = data.requests.map((r) => r.prompt).join("\n");
|
|
87
|
+
const promptText = truncateMessageText(data.requests.map((r) => r.prompt).join("\n"));
|
|
88
88
|
await ctx.thread.post({
|
|
89
89
|
blocks: data.requests.flatMap(renderInputRequestBlocks),
|
|
90
90
|
text: promptText,
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
* picks whichever is set so the renderer can pick a widget kind on
|
|
12
12
|
* UX grounds without changing the read path.
|
|
13
13
|
*/
|
|
14
|
-
import { truncateModalTitle, truncatePlainText } from "#public/channels/slack/limits.js";
|
|
14
|
+
import { truncateModalTitle, truncatePlainText, truncateSectionText, } from "#public/channels/slack/limits.js";
|
|
15
15
|
/**
|
|
16
16
|
* Wire-format prefix every framework HITL widget mints onto its
|
|
17
17
|
* `action_id`. Exposed so end-user adapters that render their own
|
|
@@ -93,7 +93,10 @@ export function isHitlAction(actionId) {
|
|
|
93
93
|
* Always emits at least the prompt section.
|
|
94
94
|
*/
|
|
95
95
|
export function renderInputRequestBlocks(request) {
|
|
96
|
-
const prompt = {
|
|
96
|
+
const prompt = {
|
|
97
|
+
text: { text: truncateSectionText(request.prompt), type: "mrkdwn" },
|
|
98
|
+
type: "section",
|
|
99
|
+
};
|
|
97
100
|
const actionId = `${HITL_ACTION_PREFIX}${request.requestId}`;
|
|
98
101
|
const options = request.options;
|
|
99
102
|
const acceptsFreeform = request.allowFreeform === true || !options || options.length === 0;
|
|
@@ -146,7 +149,7 @@ export function renderInputRequestBlocks(request) {
|
|
|
146
149
|
export function buildFreeformModalView(input) {
|
|
147
150
|
const title = input.prompt ? truncateModalTitle(input.prompt) : "Your answer";
|
|
148
151
|
const promptBlocks = input.prompt
|
|
149
|
-
? [{ type: "section", text: { type: "mrkdwn", text: input.prompt } }]
|
|
152
|
+
? [{ type: "section", text: { type: "mrkdwn", text: truncateSectionText(input.prompt) } }]
|
|
150
153
|
: [];
|
|
151
154
|
return {
|
|
152
155
|
type: "modal",
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
* naming the actor, channel, and thread so the agent's prompt always
|
|
13
13
|
* knows who and where it is talking.
|
|
14
14
|
*/
|
|
15
|
-
import { slackMrkdwnToGfm } from "#public/channels/slack/
|
|
15
|
+
import { slackMrkdwnToGfm } from "#public/channels/slack/mrkdwn.js";
|
|
16
16
|
/**
|
|
17
17
|
* Parses a Slack `app_mention` event into a {@link SlackMessage}.
|
|
18
18
|
*
|
|
@@ -20,6 +20,15 @@ export declare const SLACK_TYPING_STATUS_MAX_LENGTH = 50;
|
|
|
20
20
|
* options and button labels are capped at 75 characters by Slack.
|
|
21
21
|
*/
|
|
22
22
|
export declare const SLACK_BLOCK_KIT_PLAIN_TEXT_MAX_LENGTH = 75;
|
|
23
|
+
/**
|
|
24
|
+
* Block Kit `section` blocks cap `text.text` at 3000 chars. Anything
|
|
25
|
+
* longer fails the whole post with `invalid_blocks`.
|
|
26
|
+
*/
|
|
27
|
+
export declare const SLACK_SECTION_TEXT_MAX_LENGTH = 3000;
|
|
28
|
+
/**
|
|
29
|
+
* Top-level `text` field on `chat.postMessage` is capped at 40000 chars.
|
|
30
|
+
*/
|
|
31
|
+
export declare const SLACK_MESSAGE_TEXT_MAX_LENGTH = 40000;
|
|
23
32
|
/**
|
|
24
33
|
* `views.open` modal title is capped at 24 characters.
|
|
25
34
|
*/
|
|
@@ -37,6 +46,16 @@ export declare function truncateTypingStatus(status: string): string;
|
|
|
37
46
|
*/
|
|
38
47
|
export declare function truncatePlainText(value: string): string;
|
|
39
48
|
export declare function truncatePlainText(value: string | undefined): string | undefined;
|
|
49
|
+
/**
|
|
50
|
+
* Caps a section block's `text.text` at the Slack limit with a
|
|
51
|
+
* trailing ellipsis.
|
|
52
|
+
*/
|
|
53
|
+
export declare function truncateSectionText(value: string): string;
|
|
54
|
+
/**
|
|
55
|
+
* Caps a `chat.postMessage` `text` field at the Slack limit with a
|
|
56
|
+
* trailing ellipsis.
|
|
57
|
+
*/
|
|
58
|
+
export declare function truncateMessageText(value: string): string;
|
|
40
59
|
/**
|
|
41
60
|
* Caps a modal title at the Slack limit with a trailing ellipsis.
|
|
42
61
|
*/
|
|
@@ -20,6 +20,15 @@ export const SLACK_TYPING_STATUS_MAX_LENGTH = 50;
|
|
|
20
20
|
* options and button labels are capped at 75 characters by Slack.
|
|
21
21
|
*/
|
|
22
22
|
export const SLACK_BLOCK_KIT_PLAIN_TEXT_MAX_LENGTH = 75;
|
|
23
|
+
/**
|
|
24
|
+
* Block Kit `section` blocks cap `text.text` at 3000 chars. Anything
|
|
25
|
+
* longer fails the whole post with `invalid_blocks`.
|
|
26
|
+
*/
|
|
27
|
+
export const SLACK_SECTION_TEXT_MAX_LENGTH = 3000;
|
|
28
|
+
/**
|
|
29
|
+
* Top-level `text` field on `chat.postMessage` is capped at 40000 chars.
|
|
30
|
+
*/
|
|
31
|
+
export const SLACK_MESSAGE_TEXT_MAX_LENGTH = 40000;
|
|
23
32
|
/**
|
|
24
33
|
* `views.open` modal title is capped at 24 characters.
|
|
25
34
|
*/
|
|
@@ -38,6 +47,20 @@ export function truncatePlainText(value) {
|
|
|
38
47
|
return undefined;
|
|
39
48
|
return truncateWithEllipsis(value, SLACK_BLOCK_KIT_PLAIN_TEXT_MAX_LENGTH);
|
|
40
49
|
}
|
|
50
|
+
/**
|
|
51
|
+
* Caps a section block's `text.text` at the Slack limit with a
|
|
52
|
+
* trailing ellipsis.
|
|
53
|
+
*/
|
|
54
|
+
export function truncateSectionText(value) {
|
|
55
|
+
return truncateWithEllipsis(value, SLACK_SECTION_TEXT_MAX_LENGTH);
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Caps a `chat.postMessage` `text` field at the Slack limit with a
|
|
59
|
+
* trailing ellipsis.
|
|
60
|
+
*/
|
|
61
|
+
export function truncateMessageText(value) {
|
|
62
|
+
return truncateWithEllipsis(value, SLACK_MESSAGE_TEXT_MAX_LENGTH);
|
|
63
|
+
}
|
|
41
64
|
/**
|
|
42
65
|
* Caps a modal title at the Slack limit with a trailing ellipsis.
|
|
43
66
|
*/
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Slack mrkdwn ↔ GitHub-flavored markdown converters and the bare
|
|
3
|
+
* `@mention` rewriter used by the outbound post pipeline. These are
|
|
4
|
+
* pure string utilities — no Slack API I/O, no I/O at all — so they
|
|
5
|
+
* live separately from the binding constructor and request shape code
|
|
6
|
+
* in {@link ./api.ts}.
|
|
7
|
+
*/
|
|
8
|
+
/**
|
|
9
|
+
* Rewrites bare `@USER_ID` tokens (the form Slack apps and humans tend
|
|
10
|
+
* to type) into the `<@USER_ID>` mention syntax Slack actually renders.
|
|
11
|
+
* Anything already wrapped in `<...>` is left untouched.
|
|
12
|
+
*/
|
|
13
|
+
export declare function rewriteBareMentions(text: string): string;
|
|
14
|
+
/**
|
|
15
|
+
* Best-effort GFM → Slack mrkdwn converter used only in contexts that
|
|
16
|
+
* do not support `markdown_text` (e.g. `files.completeUploadExternal`'s
|
|
17
|
+
* `initial_comment` field).
|
|
18
|
+
*
|
|
19
|
+
* The main `{ markdown }` post path sends `markdown_text` directly
|
|
20
|
+
* to `chat.postMessage` and does not go through this converter.
|
|
21
|
+
*/
|
|
22
|
+
export declare function gfmToSlackMrkdwn(input: string): string;
|
|
23
|
+
/**
|
|
24
|
+
* Best-effort Slack mrkdwn → GFM converter applied to the text of
|
|
25
|
+
* every inbound Slack message before the harness sees it.
|
|
26
|
+
*
|
|
27
|
+
* - `<@U123>` → `@U123`
|
|
28
|
+
* - `<#C123|name>` → `#name` (or `#C123` when no name)
|
|
29
|
+
* - `<!channel>` etc. → `@channel`
|
|
30
|
+
* - `<https://x|label>` → `[label](https://x)`
|
|
31
|
+
* - `<https://x>` → `https://x`
|
|
32
|
+
* - `*bold*` (paired) → `**bold**`
|
|
33
|
+
* - `~strike~` (paired) → `~~strike~~`
|
|
34
|
+
*
|
|
35
|
+
* Inline `_italic_` and code spans pass through unchanged because both
|
|
36
|
+
* formats render them identically.
|
|
37
|
+
*/
|
|
38
|
+
export declare function slackMrkdwnToGfm(input: string): string;
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Slack mrkdwn ↔ GitHub-flavored markdown converters and the bare
|
|
3
|
+
* `@mention` rewriter used by the outbound post pipeline. These are
|
|
4
|
+
* pure string utilities — no Slack API I/O, no I/O at all — so they
|
|
5
|
+
* live separately from the binding constructor and request shape code
|
|
6
|
+
* in {@link ./api.ts}.
|
|
7
|
+
*/
|
|
8
|
+
const BARE_MENTION_RE = /(?<![<\w])@(\w+)/gu;
|
|
9
|
+
/**
|
|
10
|
+
* Rewrites bare `@USER_ID` tokens (the form Slack apps and humans tend
|
|
11
|
+
* to type) into the `<@USER_ID>` mention syntax Slack actually renders.
|
|
12
|
+
* Anything already wrapped in `<...>` is left untouched.
|
|
13
|
+
*/
|
|
14
|
+
export function rewriteBareMentions(text) {
|
|
15
|
+
return text.replace(BARE_MENTION_RE, "<@$1>");
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Best-effort GFM → Slack mrkdwn converter used only in contexts that
|
|
19
|
+
* do not support `markdown_text` (e.g. `files.completeUploadExternal`'s
|
|
20
|
+
* `initial_comment` field).
|
|
21
|
+
*
|
|
22
|
+
* The main `{ markdown }` post path sends `markdown_text` directly
|
|
23
|
+
* to `chat.postMessage` and does not go through this converter.
|
|
24
|
+
*/
|
|
25
|
+
export function gfmToSlackMrkdwn(input) {
|
|
26
|
+
const segments = splitCodeFences(input);
|
|
27
|
+
return segments
|
|
28
|
+
.map((segment) => (segment.kind === "code" ? segment.text : convertInline(segment.text)))
|
|
29
|
+
.join("");
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Best-effort Slack mrkdwn → GFM converter applied to the text of
|
|
33
|
+
* every inbound Slack message before the harness sees it.
|
|
34
|
+
*
|
|
35
|
+
* - `<@U123>` → `@U123`
|
|
36
|
+
* - `<#C123|name>` → `#name` (or `#C123` when no name)
|
|
37
|
+
* - `<!channel>` etc. → `@channel`
|
|
38
|
+
* - `<https://x|label>` → `[label](https://x)`
|
|
39
|
+
* - `<https://x>` → `https://x`
|
|
40
|
+
* - `*bold*` (paired) → `**bold**`
|
|
41
|
+
* - `~strike~` (paired) → `~~strike~~`
|
|
42
|
+
*
|
|
43
|
+
* Inline `_italic_` and code spans pass through unchanged because both
|
|
44
|
+
* formats render them identically.
|
|
45
|
+
*/
|
|
46
|
+
export function slackMrkdwnToGfm(input) {
|
|
47
|
+
const segments = splitCodeFences(input);
|
|
48
|
+
return segments
|
|
49
|
+
.map((segment) => (segment.kind === "code" ? segment.text : decodeInline(segment.text)))
|
|
50
|
+
.join("");
|
|
51
|
+
}
|
|
52
|
+
function splitCodeFences(input) {
|
|
53
|
+
const segments = [];
|
|
54
|
+
const fenceRe = /```[\s\S]*?```|`[^`\n]+`/gu;
|
|
55
|
+
let lastIndex = 0;
|
|
56
|
+
for (const match of input.matchAll(fenceRe)) {
|
|
57
|
+
const start = match.index ?? 0;
|
|
58
|
+
if (start > lastIndex) {
|
|
59
|
+
segments.push({ kind: "text", text: input.slice(lastIndex, start) });
|
|
60
|
+
}
|
|
61
|
+
segments.push({ kind: "code", text: match[0] });
|
|
62
|
+
lastIndex = start + match[0].length;
|
|
63
|
+
}
|
|
64
|
+
if (lastIndex < input.length) {
|
|
65
|
+
segments.push({ kind: "text", text: input.slice(lastIndex) });
|
|
66
|
+
}
|
|
67
|
+
return segments;
|
|
68
|
+
}
|
|
69
|
+
function convertInline(input) {
|
|
70
|
+
let out = input;
|
|
71
|
+
out = out.replace(/\*\*([^*\n]+)\*\*/gu, "*$1*");
|
|
72
|
+
out = out.replace(/__([^_\n]+)__/gu, "*$1*");
|
|
73
|
+
out = out.replace(/~~([^~\n]+)~~/gu, "~$1~");
|
|
74
|
+
out = out.replace(/\[([^\]\n]+)\]\(([^)\s]+)\)/gu, "<$2|$1>");
|
|
75
|
+
return out;
|
|
76
|
+
}
|
|
77
|
+
function decodeInline(input) {
|
|
78
|
+
let out = input;
|
|
79
|
+
out = out.replace(/<!(channel|here|everyone)>/gu, "@$1");
|
|
80
|
+
out = out.replace(/<@([A-Z0-9]+)\|([^>]+)>/gu, "@$2");
|
|
81
|
+
out = out.replace(/<@([A-Z0-9]+)>/gu, "@$1");
|
|
82
|
+
out = out.replace(/<#([A-Z0-9]+)\|([^>]+)>/gu, "#$2");
|
|
83
|
+
out = out.replace(/<#([A-Z0-9]+)>/gu, "#$1");
|
|
84
|
+
out = out.replace(/<(https?:\/\/[^|>\s]+)\|([^>]+)>/gu, "[$2]($1)");
|
|
85
|
+
out = out.replace(/<(https?:\/\/[^>\s]+)>/gu, "$1");
|
|
86
|
+
out = out.replace(/(^|[^*])\*([^*\n]+)\*(?!\*)/gu, "$1**$2**");
|
|
87
|
+
out = out.replace(/(^|[^~])~([^~\n]+)~(?!~)/gu, "$1~~$2~~");
|
|
88
|
+
return out;
|
|
89
|
+
}
|
|
@@ -14,13 +14,7 @@ function rebuildSlackContext(state, session, credentials) {
|
|
|
14
14
|
channelId: state.channelId ?? "",
|
|
15
15
|
threadTs: state.threadTs ?? "",
|
|
16
16
|
teamId: state.teamId ?? undefined,
|
|
17
|
-
|
|
18
|
-
// thread (programmatic start, schedule firing, etc.) becomes the
|
|
19
|
-
// thread root. We update durable adapter state so subsequent
|
|
20
|
-
// posts thread under it, and re-key the parked session so a
|
|
21
|
-
// follow-up `@mention` reply in the same thread resumes the same
|
|
22
|
-
// workflow.
|
|
23
|
-
onAnchor(ts) {
|
|
17
|
+
onThreadTsChanged(ts) {
|
|
24
18
|
state.threadTs = ts;
|
|
25
19
|
if (state.channelId) {
|
|
26
20
|
session.setContinuationToken(slackContinuationToken(state.channelId, ts));
|
|
@@ -37,7 +37,7 @@ export interface ChannelConfig<TState = undefined, TCtx = void> {
|
|
|
37
37
|
* Builds the per-step channel context handed to `events` and
|
|
38
38
|
* `deliver`. Receives the live {@link SessionHandle} so factories
|
|
39
39
|
* that need to wire late-bound callbacks (e.g. Slack's auto-anchor
|
|
40
|
-
* `
|
|
40
|
+
* `onThreadTsChanged` calling `session.setContinuationToken(...)`) can close
|
|
41
41
|
* over it. State mutations made inside the returned context flow
|
|
42
42
|
* back through `adapter.state` automatically.
|
|
43
43
|
*/
|