From 161704e23e3e3d9c20597113d983e9b0bce7b1f2 Mon Sep 17 00:00:00 2001 From: Eric Satterwhite Date: Wed, 3 Jun 2026 07:17:53 -0500 Subject: [PATCH 1/3] feat: Add an agent skill for working with setup chain This adds a skill that confirms to the agentskills.io. It should work with most any agent provider. It can be discovered by the skills.sh cli This includes instructions on adding actions and functions, as well as some guidence on general usage. Resolves: #135 --- lib/actions/map.js | 2 +- skills/setup-chain/SKILL.md | 114 ++++++++++++++++++ .../setup-chain/references/create-action.md | 88 ++++++++++++++ .../setup-chain/references/create-function.md | 71 +++++++++++ .../setup-chain/references/examples/basic.js | 5 + .../references/examples/custom-actions.js | 26 ++++ .../references/examples/lookups.js | 23 ++++ .../references/examples/templates.js | 9 ++ skills/setup-chain/references/usage.md | 98 +++++++++++++++ test/integration/actions/map.js | 2 +- 10 files changed, 436 insertions(+), 2 deletions(-) create mode 100644 skills/setup-chain/SKILL.md create mode 100644 skills/setup-chain/references/create-action.md create mode 100644 skills/setup-chain/references/create-function.md create mode 100644 skills/setup-chain/references/examples/basic.js create mode 100644 skills/setup-chain/references/examples/custom-actions.js create mode 100644 skills/setup-chain/references/examples/lookups.js create mode 100644 skills/setup-chain/references/examples/templates.js create mode 100644 skills/setup-chain/references/usage.md diff --git a/lib/actions/map.js b/lib/actions/map.js index 6d87122..253690e 100644 --- a/lib/actions/map.js +++ b/lib/actions/map.js @@ -9,7 +9,7 @@ const assert = require('assert') const {typeOf} = require('@logdna/stdlib') const NAME = 'SetupChain.map' -const ARRAY_ERR = `${NAME} first param should be an array. Supports dynamic lookups` +const ARRAY_ERR = `${NAME} first param should be an array. Supports dynamic lookups` const FN_ERR = `${NAME} second param should be a function` module.exports = async function map(collection, fn) { diff --git a/skills/setup-chain/SKILL.md b/skills/setup-chain/SKILL.md new file mode 100644 index 0000000..f606bd0 --- /dev/null +++ b/skills/setup-chain/SKILL.md @@ -0,0 +1,114 @@ +--- +name: setup-chain +description: create new actions, functions or workflows for logdna/setup-chain +license: MIT +metadata: + author: Mezmo Inc,. +--- + +# Setup-chain Agent + +Adds new actions and functions to logdna setup-chain + +## Prerequisites + +1. Confirm `node` executable is installed on the host +2. Make sure there is a package.json in the current working directory +3. package.json should have `@logdna/setup-chain` in dependencies or devDependencies + +If any of the above are not met, explain what is missing. Do not proceed with custom action creation. + +## Creating a Custom Action + +Follow these steps to create and wire up a custom action: + +1. **Define the action function** in an actions object + - **Requirement**: Actions MUST be `async` functions or return a `Promise` + ```javascript + const actions = { + myAction: async (opts) => { + return opts.value || 'default' + } + } + ``` + +2. **Create or extend SetupChain class** + ```javascript + class MyChain extends SetupChain { + constructor(state) { + super(state, actions) + } + } + ``` + +3. **Use the action** in your chain + ```javascript + await new MyChain().myAction({value: 'test'}, 'result').execute() + // state: {result: 'test'} + ``` + +4. **Validate** your implementation works as expected + +## Custom Signatures (Advanced) + +Only manually push `this.tasks` when action signature deviates from `(opts, label)`: + +```javascript +class MyChain extends SetupChain { + constructor(state) { + super(state, yourActions) + } + + customAction(arg1, arg2, label) { + this.tasks.push(['customAction', label, arg1, arg2]) + return this + } +} +``` + +See `create-action.md` for detailed patterns and examples. + +## Usage & Workflow + +SetupChain implements chain-of-responsibility pattern. Actions are available as top level functions on a chain instance. +Chain actions, execute, and results are stored in state. + +```javascript +const chain = new MyChain() + +const state = await chain + .set('user', 'alice') + .map('#user', n => n * 2, 'doubled') + .execute() + +// state: {user: 'alice', doubled: [2, 4, 6]} +``` + +Best practices: +- Use labels explicitly to avoid state key collisions +- Chain actions for readability: `.action1().action2().execute()` +- Reuse state across instances for persistence: `new SetupChain(state2)` +- Group Long action chains by use case with comments + +```javascript +const chain = new MyChain() + +// test scenerio 1 +chain + .account({}, 'account_one') + .user({account: '#account_one'}, 'user_one') + +// test scenerio 1 +chain + .account({}, 'account_two') + .user({account: '#account_two'}, 'user_two') + +const state = await chain.execute() +``` + +## Additional Resources + +- [Detailed action patterns](./references/create-action.md) +- [Adding helper functions](./references/create-function.md) +- [Quick reference](./references/QUICKREF.md) +- [Code examples](./references/examples/) diff --git a/skills/setup-chain/references/create-action.md b/skills/setup-chain/references/create-action.md new file mode 100644 index 0000000..951b105 --- /dev/null +++ b/skills/setup-chain/references/create-action.md @@ -0,0 +1,88 @@ +# Creating Custom Actions + + +## Steps to Create and Wire a Custom Action + +1. **Define your action function** + - **Requirement**: Actions MUST be `async` functions or return a `Promise` + - Accept single `opts` object + - Return the result directly + ```javascript + const actions = { + hello: async (opts) => opts.name || 'World' + } + ``` + +2. **Create SetupChain class** + - Pass actions object to constructor + ```javascript + class MyChain extends SetupChain { + constructor(state) { + super(state, actions) + } + } + ``` + +3. **Call action with label** + ```javascript + await new MyChain().hello({name: 'Alice'}, 'result').execute() + ``` + - Label is key in state for this action's result + +4. **Test your implementation** + +## Custom Signature (Use Only When Needed) + +1. Define action function manually +2. Override with custom method signature +3. Manually push task with correct format +4. Return `this` for chaining + +```javascript +const actions = { + printNames: async (opts) => [opts.first, opts.last].join(', ') +} + +class MyChain extends SetupChain { + constructor(state) { + super(state, actions) + } + + printNames(first, last, label) { + this.tasks.push(['printNames', label, first, last]) + return this + } +} +``` + +## Pattern: State-Dependent with Defaults + +1. Define defaults with template placeholders +2. Use `this.lookup()` to merge defaults and opts +3. Use `#this` to reference action's result context +4. Use `assert` module to validate values after lookup resolution + +```javascript +const actions = { + person: async (opts) => { + const defaults = { + first: 'bobby' + , last: 'fischer' + , full: '!template:"{{#this.first}} {{#this.last}}"' + } + const result = this.lookup({...defaults, ...opts}) + + // Validate required fields exist + assert.ok(result.first, 'First name is required') + assert.ok(result.last, 'Last name is required') + + // Validate types + assert.equal(typeof result.first, 'string', 'First name must be a string') + assert.equal(typeof result.last, 'string', 'Last name must be a string') + + return result + } +} +``` + +5. **Validate** after executing with various inputs diff --git a/skills/setup-chain/references/create-function.md b/skills/setup-chain/references/create-function.md new file mode 100644 index 0000000..bdf2a19 --- /dev/null +++ b/skills/setup-chain/references/create-function.md @@ -0,0 +1,71 @@ +# Adding Helper Functions + +## Overview + +Helper functions are synchronous methods defined on a chain class that extend the capabilities of the `lookup` system. They are called when the `lookup` function encounters a value starting with `!`. + +## Creating a Helper Function + +1. **Define the method on your chain class** + - **Prefix**: Method name MUST start with `$` (e.g., `$slugify`) + - **Naming**: Keep names short, single-word, and lowercase (`$`) + - **Synchronous**: Functions MUST be synchronous; they cannot be `async` by design. Use [Actions](./create-action.md) for async operations. + - **Arguments**: They can accept any number of arguments. + + ```javascript + class MyChain extends SetupChain { + $slugify(text) { + return text.toLowerCase().replace(/\s+/g, '-'); + } + } + ``` + +2. **Use the function via `lookup`** + - Call the function by using the `!` prefix in lookup strings or objects. + - The `$` prefix is removed when calling via lookup (e.g., `$slugify` is called as `!slugify`). + +## Usage Patterns + +### Basic Call +Pass arguments directly after the function name, separated by commas. +```javascript +chain.lookup('!slugify:"Hello World"') +// returns: "hello-world" +``` + +### Nested & Complex Lookups +Functions can accept other lookup values (state properties or other functions) as arguments. + +- **Using State Properties**: + ```javascript + await chain.set('title', 'My Page').execute(); + chain.lookup('!slugify:#title') + // returns: "my-page" + ``` + +- **Array of Lookups**: + ```javascript + await chain.set(('one', 'ONE').set('two', 'TWO').execute() + chain.lookup(['!lower:#one', '!lower:#two']) + // returns ['one', 'two'] + ``` + +- **Deeply Nested Functions**: + ```javascript + var chain = new MyChain({bar: 'Hello World}) + chain.lookup({one: '!lower(!slugify(#bar))' }) + // returns {one: 'hello-world'} + ``` + +- **Inside Action Options**: + You can pass function calls as values in action options; they will be resolved by the action's `this.lookup()` call. + ```javascript + await chain.set({title: 'Hello World'}).myAction({ slug: '!slugify(#title)' }).execute() + // returns {slug: 'hello-world'} + ``` + +## Summary Checklist +- [ ] Method starts with `$` +- [ ] Method is synchronous +- [ ] Name is short, lowercase, single-word +- [ ] Called using `!` in `lookup` diff --git a/skills/setup-chain/references/examples/basic.js b/skills/setup-chain/references/examples/basic.js new file mode 100644 index 0000000..e001d4a --- /dev/null +++ b/skills/setup-chain/references/examples/basic.js @@ -0,0 +1,5 @@ +// Basic set() usage +const SetupChain = require('@logdna/setup-chain') +const chain = new SetupChain() +await chain.set('hello', 'world').set('goodbye', 'world').execute() +// state: {hello: 'world', goodbye: 'world'} \ No newline at end of file diff --git a/skills/setup-chain/references/examples/custom-actions.js b/skills/setup-chain/references/examples/custom-actions.js new file mode 100644 index 0000000..0a1443d --- /dev/null +++ b/skills/setup-chain/references/examples/custom-actions.js @@ -0,0 +1,26 @@ +// Custom actions with defaults +const SetupChain = require('@logdna/setup-chain') + +const defaults = { + first: 'bobby' +, last: 'fischer' +, full: '!template:"{{#this.first}} {{#this.last}}"' +} + +const actions = { + person: async function person(opts) { + return this.lookup({...defaults, ...opts}) + } +} + +const chain = new SetupChain(null, actions) +const state = await chain + .person({}, 'bobby') // uses defaults + .person({first: 'fred'}, 'fred') + .person({last: 'williams'}, 'williams') + .execute() +// state: { +// bobby: {first: 'bobby', last: 'fischer', full: 'bobby fischer'}, +// fred: {first: 'fred', last: 'fischer', full: 'fred fischer'}, +// williams: {first: 'bobby', last: 'williams', full: 'bobby williams'} +// } diff --git a/skills/setup-chain/references/examples/lookups.js b/skills/setup-chain/references/examples/lookups.js new file mode 100644 index 0000000..b8e8168 --- /dev/null +++ b/skills/setup-chain/references/examples/lookups.js @@ -0,0 +1,23 @@ +// Lookup syntax examples +const SetupChain = require('@logdna/setup-chain') +const chain = new SetupChain( + {one: 1, two: 2, three: 3, four: ['a', 'b', 'c']} +, { + myAction: async function(opts) { + return this.lookup(opts) + } + } +) + +console.log(chain.lookup('#one')) // 1 +console.log(chain.lookup('#four.1')) // 'b' +console.log(chain.lookup(['#one', '#two'])) // [1, 2] +console.log(chain.lookup({ + one: '#one' +, nested: '#three' +})) + + +console.dir( + await chain.myAction({four: '#four.0'}).execute() +) // {myAction: {four: 'a'}} diff --git a/skills/setup-chain/references/examples/templates.js b/skills/setup-chain/references/examples/templates.js new file mode 100644 index 0000000..302b565 --- /dev/null +++ b/skills/setup-chain/references/examples/templates.js @@ -0,0 +1,9 @@ +// Using templates and random +const SetupChain = require('@logdna/setup-chain') +const chain = new SetupChain() + +chain.set('name', 'Alice').set('email', 'alice@example.com').execute() +chain.lookup('!template:"Hello, {{#name}}!"') // 'Hello, Alice!' +chain.lookup('!template:"User {{#name}} ({{#email}})"') // 'User Alice (alice@example.com)' +chain.lookup('!random:10') // random hex string like 'a7f39c2e4e' +chain.lookup('!template:"ID-{{#this.name}}-{{!random:8}}"') \ No newline at end of file diff --git a/skills/setup-chain/references/usage.md b/skills/setup-chain/references/usage.md new file mode 100644 index 0000000..50f81df --- /dev/null +++ b/skills/setup-chain/references/usage.md @@ -0,0 +1,98 @@ +# Setup-chain Quick Reference + +## Additional Documentation + +- [Action development](./create-action.md) +- [Usage patterns](./SKILL.md) +- [Examples](./examples) + + +| Action | Usage | Returns | +|--------------------------------------|--------------------------------------------|---------------------------------| +| `set(key, value)` | `chain.set('foo', 'bar')` | stores value in state | +| `set({key: value})` | `chain.set('foo', 'bar')` | stores multiple values in state | +| `map(collection, fn, label)` | `chain.map([1,2,3], n=>n*2, 'doubled')` | transformed array | +| `repeat(times, action, opts, label)` | `chain.repeat(5, 'hello', {}, 'res')` | array of results | +| `serial(times, action, opts, label)` | `chain.serial(3, 'delay', {}, 'res')` | sequential results | +| `sort(collection, fn, label)` | `chain.sort('#arr', (a,b)=>b-a, 'sorted')` | sorted array | +| `sleep({ms})` | `chain.sleep({ms:1000})` | waits 1000ms | + +## Lookup Syntax + +| Syntax | Example | Description | +|--------|---------|-------------| +| `#foo` | `chain.lookup('#foo')` | state property | +| `#foo.bar` | `chain.lookup('#user.name')` | nested property | +| `#arr.0` | `chain.lookup('#arr.0')` | array index (dot notation) | +| `['#foo', '#bar']` | `chain.lookup(['#foo','bar'])` | array lookup | +| `!fn:arg1,arg2` | `chain.lookup('!random:5')` | function call | +| `!template:"{{#foo}}"` | `chain.$template('Hello {{#name}}')` | interpolated string | + +## Creating Actions + +**Important**: Only manually push tasks when action signature **deviates** from standard `(opts, label)` pattern. + +### Auto-exposed (simple) - Standard signature +```javascript +const actions = { + hello: async (opts) => opts.name || 'World' +} + +class MyChain extends SetupChain { + constructor(state) { + super(state, actions) + } +} + +new MyChain().hello({name: 'Alice'}, 'result').execute() +// NO .tasks.push needed! +``` + +### Custom Signature - Only when needed +```javascript +printNames(first, last, label) { + this.tasks.push(['printNames', label, first, last]) // Only if signature differs + return this +} +``` + +### State-Dependent with Validation +```javascript +const actions = { + person: async (opts) => { + const defaults = { + first: 'bobby' + , last: 'fischer' + , full: '!template:"{{#this.first}} {{#this.last}}"' + } + const result = this.lookup({...defaults, ...opts}) + + assert.ok(result.first, 'First name is required') + assert.ok(result.last, 'Last name is required') + assert.equal(typeof result.first, 'string', 'First name must be a string') + assert.equal(typeof result.last, 'string', 'Last name must be a string') + + return result + } +} +``` + +## Custom Functions + +```javascript +class MyChain extends SetupChain { + $max(...args) { + return Math.max(...this.lookup(args)) + } +} + +chain.lookup('!max:1,2,3') // '3' +``` + +## Best Practices + +1. Use labels explicitly to avoid collisions +2. Use templates in action defaults for reusability +3. Keep actions simple, use `this.lookup()` for composition +4. Chain operations for readability +5. Reuse state across chains for persistence diff --git a/test/integration/actions/map.js b/test/integration/actions/map.js index 1fbed33..6c34b10 100644 --- a/test/integration/actions/map.js +++ b/test/integration/actions/map.js @@ -73,7 +73,7 @@ test('SetupChain.map() as a builtin action', async (t) => { t.test('Error: first parameter is not an array', async (t) => { const msg = new RegExp( - 'first param should be an array. Supports dynamic lookups' + 'first param should be an array. Supports dynamic lookups' ) t.rejects(chain.map().execute(), { From 485cfa2a1140baed1345dc687d6097fed886dae5 Mon Sep 17 00:00:00 2001 From: Eric Satterwhite Date: Wed, 3 Jun 2026 07:30:56 -0500 Subject: [PATCH 2/3] fix(ci): update node versions to test against update the test matrix to include node 20, 22, 24 --- Jenkinsfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Jenkinsfile b/Jenkinsfile index 880e5c2..7911483 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -45,7 +45,7 @@ pipeline { axes { axis { name 'NODE_VERSION' - values '12', '14', '16' + values '20', '22', '24' } } From 96cb56b876602b5c2e9b7f98e7212c000b36cc45 Mon Sep 17 00:00:00 2001 From: Eric Satterwhite Date: Wed, 3 Jun 2026 07:47:31 -0500 Subject: [PATCH 3/3] chore(dep-dev): eslint-config-logdna@8.0.1 update eslint and bas config to latest version and corrects outstanding linting errors --- eslint.config.js | 23 +++++++++++++++++++ package.json | 20 ++++------------ .../references/examples/lookups.js | 2 +- test/fixtures/actions/error.js | 2 +- test/integration/actions/sort.js | 2 +- test/integration/chain.js | 20 ++-------------- 6 files changed, 32 insertions(+), 37 deletions(-) create mode 100644 eslint.config.js diff --git a/eslint.config.js b/eslint.config.js new file mode 100644 index 0000000..7c7895f --- /dev/null +++ b/eslint.config.js @@ -0,0 +1,23 @@ +'use strict' + +const {defineConfig} = require('eslint/config') +const logdna = require('eslint-config-logdna') + +module.exports = defineConfig([ + { + 'extends': [logdna] + , 'ignores': ['skills/**'] + , 'languageOptions': { + ecmaVersion: 2022 + , sourceType: 'script' + , globals: { + fetch: 'readonly' + } + } + , 'rules': { + 'sensible/check-require': [2, 'always', { + root: __dirname + }] + } + } +]) diff --git a/package.json b/package.json index af6fbef..d910495 100644 --- a/package.json +++ b/package.json @@ -47,25 +47,11 @@ "bugs": { "url": "https://github.com/logdna/setup-chain-node/issues" }, - "eslintConfig": { - "root": true, - "ignorePatterns": [ - "node_modules/", - "coverage/" - ], - "extends": [ - "logdna" - ], - "parserOptions": { - "ecmaVersion": 2022 - }, - "plugins": [] - }, "homepage": "https://github.com/logdna/setup-chain-node", "devDependencies": { "casual": "^1.6.2", - "eslint": "^8.35.0", - "eslint-config-logdna": "^6.1.0", + "eslint": "^10.4.1", + "eslint-config-logdna": "^8.0.1", "luxon": "^3.2.1", "moment": "^2.29.1", "semantic-release": "^17.4.4", @@ -98,6 +84,8 @@ "--exclude=coverage/", "--exclude=scripts/", "--exclude=examples/", + "--exclude=skills/", + "--exclude=eslint.config.js/", "--all" ] }, diff --git a/skills/setup-chain/references/examples/lookups.js b/skills/setup-chain/references/examples/lookups.js index b8e8168..ea1599e 100644 --- a/skills/setup-chain/references/examples/lookups.js +++ b/skills/setup-chain/references/examples/lookups.js @@ -17,7 +17,7 @@ console.log(chain.lookup({ , nested: '#three' })) - +// using lookup in an action console.dir( await chain.myAction({four: '#four.0'}).execute() ) // {myAction: {four: 'a'}} diff --git a/test/fixtures/actions/error.js b/test/fixtures/actions/error.js index 083fae8..de27b0a 100644 --- a/test/fixtures/actions/error.js +++ b/test/fixtures/actions/error.js @@ -1,6 +1,6 @@ 'use strict' const assert = require('assert') -module.exports = async function error(opts) { +module.exports = async function error() { assert.equal(1, 4) } diff --git a/test/integration/actions/sort.js b/test/integration/actions/sort.js index 9fea083..13d6ad8 100644 --- a/test/integration/actions/sort.js +++ b/test/integration/actions/sort.js @@ -58,7 +58,7 @@ test('SetupChain.sort() as a builtin action', async (t) => { t.test('Error: collection is not an array', async (t) => { const msg = new RegExp( - 'first param should be an array. Supports dynamic lookups' + 'first param should be an array. {2}Supports dynamic lookups' ) t.rejects(chain.sort().execute(), { diff --git a/test/integration/chain.js b/test/integration/chain.js index b96b77f..c1b3590 100644 --- a/test/integration/chain.js +++ b/test/integration/chain.js @@ -42,7 +42,7 @@ test('Setup chain', async (t) => { $last(...args) { return args[args.length - 1] } - fake(opts, label) { + fake() { this.tasks.push(null) return this } @@ -111,10 +111,6 @@ test('Setup chain', async (t) => { t.test('extended chain w/ lookup functions (moment)', async (t) => { class MomentChain extends Chain { - constructor(state) { - super(state) - } - $now() { return moment().utc().valueOf() } @@ -144,10 +140,6 @@ test('Setup chain', async (t) => { t.test('extended chain w/ lookup functions (luxon)', async (t) => { class LuxonChain extends Chain { - constructor(state) { - super(state) - } - $now() { return luxon.DateTime.now() } @@ -177,11 +169,7 @@ test('Setup chain', async (t) => { }) t.test('extended chain created with an existing state', async (t) => { - class WithStateChain extends Chain { - constructor(state) { - super(state) - } - } + class WithStateChain extends Chain {} const state_param = {hello: 'there'} const chain = new WithStateChain(state_param) @@ -192,10 +180,6 @@ test('Setup chain', async (t) => { t.test('chain function argument handling', async (t) => { class FunctionChain extends Chain { - constructor(state) { - super(state) - } - $test(...args) { return `test-${args.join('-')}` }