openclaw-workflowskill 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +197 -0
- package/index.ts +235 -0
- package/lib/adapters.ts +181 -0
- package/lib/openclaw-context.md +27 -0
- package/lib/storage.ts +123 -0
- package/openclaw.plugin.json +12 -0
- package/package.json +51 -0
- package/tools/run.ts +94 -0
- package/tools/runs.ts +40 -0
- package/tools/validate.ts +20 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Matthew Cromer
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
# OpenClaw WorkflowSkill Plugin
|
|
2
|
+
|
|
3
|
+
[](LICENSE)
|
|
4
|
+
[](https://nodejs.org)
|
|
5
|
+
[](https://github.com/matthew-h-cromer/openclaw-workflowskill/issues)
|
|
6
|
+
|
|
7
|
+
> [!IMPORTANT]
|
|
8
|
+
> **Pre-release.** This plugin tracks the [WorkflowSkill](https://github.com/matthew-h-cromer/workflowskill) spec, which is not yet frozen. Expect breaking changes as the spec evolves.
|
|
9
|
+
|
|
10
|
+
Author, validate, run, and review WorkflowSkill workflows — without leaving the OpenClaw chat.
|
|
11
|
+
|
|
12
|
+
1. Tell the agent what you need
|
|
13
|
+
2. The agent writes, validates, and test-runs the workflow in chat
|
|
14
|
+
3. Schedule it with cron — runs autonomously, no agent session needed
|
|
15
|
+
|
|
16
|
+
## What it looks like
|
|
17
|
+
|
|
18
|
+
> **You:** I want to check Hacker News for AI stories every morning and email me a summary.
|
|
19
|
+
>
|
|
20
|
+
> **Agent:** I'll author a WorkflowSkill for that. *(invokes `/workflowskill-author`, writes a SKILL.md, runs `workflowskill_validate`)*
|
|
21
|
+
>
|
|
22
|
+
> Validated — 3 steps: `fetch`, `filter`, `email`. Running a test now... *(invokes `workflowskill_run`)*
|
|
23
|
+
>
|
|
24
|
+
> Run complete: 4 AI stories found, summary drafted. Ready to schedule — want me to set up a daily cron at 8 AM?
|
|
25
|
+
|
|
26
|
+
## Workflow Lifecycle
|
|
27
|
+
|
|
28
|
+
```
|
|
29
|
+
describe workflow in natural language
|
|
30
|
+
↓
|
|
31
|
+
/workflowskill-author (agent writes YAML)
|
|
32
|
+
↓
|
|
33
|
+
workflowskill_validate (catch errors early)
|
|
34
|
+
↓
|
|
35
|
+
workflowskill_run (test run, review RunLog)
|
|
36
|
+
↓
|
|
37
|
+
workflowskill_runs (diagnose failures, iterate)
|
|
38
|
+
↓
|
|
39
|
+
cron (schedule for automated execution)
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
## Repositories
|
|
43
|
+
|
|
44
|
+
| Repo | Description |
|
|
45
|
+
|------|-------------|
|
|
46
|
+
| [workflowskill](https://github.com/matthew-h-cromer/workflowskill) | Specification and reference runtime |
|
|
47
|
+
| **openclaw-workflowskill** (this repo) | OpenClaw plugin — author, validate, run, and review workflows from the agent |
|
|
48
|
+
|
|
49
|
+
## Quick Start
|
|
50
|
+
|
|
51
|
+
Requires [OpenClaw](https://openclaw.ai).
|
|
52
|
+
|
|
53
|
+
### 1. Install the plugin
|
|
54
|
+
|
|
55
|
+
```bash
|
|
56
|
+
openclaw plugins install openclaw-workflowskill
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
### 2. Configure Anthropic credentials
|
|
60
|
+
|
|
61
|
+
The plugin reads your Anthropic API key from OpenClaw's credential store — no `.env` file needed. Make sure an Anthropic auth profile is configured in OpenClaw (`~/.openclaw/agents/main/agent/auth-profiles.json`).
|
|
62
|
+
|
|
63
|
+
### 3. Restart the gateway
|
|
64
|
+
|
|
65
|
+
```bash
|
|
66
|
+
openclaw gateway restart
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
### 4. Verify
|
|
70
|
+
|
|
71
|
+
```bash
|
|
72
|
+
openclaw plugins list
|
|
73
|
+
# → workflowskill: 4 tools registered
|
|
74
|
+
|
|
75
|
+
openclaw skills list
|
|
76
|
+
# → workflowskill-author (user-invocable)
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
## Tools
|
|
80
|
+
|
|
81
|
+
Registers four tools with the OpenClaw agent:
|
|
82
|
+
|
|
83
|
+
| Tool | Description |
|
|
84
|
+
|------|-------------|
|
|
85
|
+
| `workflowskill_validate` | Parse and validate a SKILL.md or raw YAML workflow |
|
|
86
|
+
| `workflowskill_run` | Execute a workflow and return a compact run summary |
|
|
87
|
+
| `workflowskill_runs` | List and inspect past run logs |
|
|
88
|
+
| `workflowskill_llm` | Call Anthropic directly for inline LLM reasoning in workflows |
|
|
89
|
+
|
|
90
|
+
Also ships the `/workflowskill-author` skill — just say "I want to automate X" and the agent handles the rest: researching, writing, validating, and test-running the workflow in chat.
|
|
91
|
+
|
|
92
|
+
## Workspace Layout
|
|
93
|
+
|
|
94
|
+
```
|
|
95
|
+
<workspace>/
|
|
96
|
+
skills/ # Workflow SKILL.md files (one per subdirectory)
|
|
97
|
+
daily-triage/
|
|
98
|
+
SKILL.md
|
|
99
|
+
workflow-runs/ # RunLog JSON files (auto-created)
|
|
100
|
+
daily-triage-2024-01-15T09-00-00.000Z.json
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
## Cron Scheduling
|
|
104
|
+
|
|
105
|
+
Schedule a workflow to run autonomously via OpenClaw's cron, at `~/.openclaw/cron/jobs.json`:
|
|
106
|
+
|
|
107
|
+
```json
|
|
108
|
+
{
|
|
109
|
+
"payload": {
|
|
110
|
+
"kind": "agentTurn",
|
|
111
|
+
"message": "Run the daily-triage workflow using workflowskill_run\n\nSend results to Slack in the #general channel",
|
|
112
|
+
"model": "haiku"
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
Always set `"model": "haiku"` — cron runs are lightweight orchestration and don't need a powerful model. Put delivery instructions (e.g. Slack channel) in the cron message, not in the workflow, so workflows stay reusable.
|
|
118
|
+
|
|
119
|
+
Review past runs via `workflowskill_runs`.
|
|
120
|
+
|
|
121
|
+
## Architecture
|
|
122
|
+
|
|
123
|
+
### Tool delegation
|
|
124
|
+
|
|
125
|
+
Workflow `tool` steps are forwarded to the **OpenClaw gateway** via `POST /tools/invoke`. Any tool registered with the gateway is available to a workflow — the plugin sends the tool name and args as JSON and returns the result. Gateway auth (`config.gateway.auth.token`) must be configured or the plugin will refuse to start.
|
|
126
|
+
|
|
127
|
+
The `workflowskill_llm` tool is built-in: it calls Anthropic directly using the API key from OpenClaw's credential store, and is always available.
|
|
128
|
+
|
|
129
|
+
The plugin's own four tools (`workflowskill_validate`, `workflowskill_run`, `workflowskill_runs`, `workflowskill_llm`) are blocked from being forwarded to the gateway to prevent infinite recursion.
|
|
130
|
+
|
|
131
|
+
## Tool Reference
|
|
132
|
+
|
|
133
|
+
### `workflowskill_validate`
|
|
134
|
+
|
|
135
|
+
Parse and validate a SKILL.md or raw YAML workflow.
|
|
136
|
+
|
|
137
|
+
| Param | Type | Required | Description |
|
|
138
|
+
|-------|------|----------|-------------|
|
|
139
|
+
| `content` | string | yes | SKILL.md text or raw workflow YAML |
|
|
140
|
+
|
|
141
|
+
Returns `{ valid, errors[], name, stepCount, stepTypes[] }`.
|
|
142
|
+
|
|
143
|
+
### `workflowskill_run`
|
|
144
|
+
|
|
145
|
+
Execute a workflow and return a compact run summary. The full RunLog is persisted to `workflow-runs/` and retrievable via `workflowskill_runs` with `run_id`.
|
|
146
|
+
|
|
147
|
+
| Param | Type | Required | Description |
|
|
148
|
+
|-------|------|----------|-------------|
|
|
149
|
+
| `workflow_name` | string | no* | Name of a skill resolved from skills directories |
|
|
150
|
+
| `content` | string | no* | Inline SKILL.md content (bypasses skill files) |
|
|
151
|
+
| `inputs` | object | no | Override workflow input defaults |
|
|
152
|
+
|
|
153
|
+
*One of `workflow_name` or `content` is required.
|
|
154
|
+
|
|
155
|
+
### `workflowskill_runs`
|
|
156
|
+
|
|
157
|
+
List and inspect past run logs.
|
|
158
|
+
|
|
159
|
+
| Param | Type | Required | Description |
|
|
160
|
+
|-------|------|----------|-------------|
|
|
161
|
+
| `workflow_name` | string | no | Filter by workflow name |
|
|
162
|
+
| `run_id` | string | no | Get full RunLog detail for one run |
|
|
163
|
+
| `status` | string | no | Filter by `"success"` or `"failed"` |
|
|
164
|
+
|
|
165
|
+
No params → 20 most recent runs (summary view).
|
|
166
|
+
|
|
167
|
+
### `workflowskill_llm`
|
|
168
|
+
|
|
169
|
+
Call Anthropic directly and return the text response. Uses the API key from OpenClaw's credential store. Useful in workflow `tool` steps when you need inline LLM reasoning.
|
|
170
|
+
|
|
171
|
+
| Param | Type | Required | Description |
|
|
172
|
+
|-------|------|----------|-------------|
|
|
173
|
+
| `prompt` | string | yes | The prompt to send to the LLM |
|
|
174
|
+
| `model` | string | no | Model alias (`haiku`, `sonnet`, `opus`) or full model ID — omit to use the default |
|
|
175
|
+
|
|
176
|
+
Returns `{ text: string }`.
|
|
177
|
+
|
|
178
|
+
## Development
|
|
179
|
+
|
|
180
|
+
The plugin imports from `workflowskill` (peer dependency), installed from npm. No build step is required for type checking:
|
|
181
|
+
|
|
182
|
+
```bash
|
|
183
|
+
npm install
|
|
184
|
+
npm run typecheck
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
To test changes, link the plugin locally and restart the OpenClaw gateway:
|
|
188
|
+
|
|
189
|
+
```bash
|
|
190
|
+
openclaw plugins install --link "$(pwd)"
|
|
191
|
+
openclaw gateway restart
|
|
192
|
+
openclaw tools invoke workflowskill_validate '{"content": "..."}'
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
## License
|
|
196
|
+
|
|
197
|
+
MIT
|
package/index.ts
ADDED
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
// WorkflowSkill OpenClaw Plugin
|
|
2
|
+
//
|
|
3
|
+
// Entry point called by OpenClaw when the plugin is loaded.
|
|
4
|
+
// Default-exports an object with id + register(api) per the OpenClaw plugin API.
|
|
5
|
+
|
|
6
|
+
import { mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
7
|
+
import { join } from 'node:path';
|
|
8
|
+
import { AUTHORING_SKILL } from 'workflowskill';
|
|
9
|
+
import { validateHandler } from './tools/validate.js';
|
|
10
|
+
import { runHandler } from './tools/run.js';
|
|
11
|
+
import { runsHandler } from './tools/runs.js';
|
|
12
|
+
import { createAdapters, type GatewayConfig } from './lib/adapters.js';
|
|
13
|
+
|
|
14
|
+
// ─── OpenClaw plugin API types ─────────────────────────────────────────────
|
|
15
|
+
|
|
16
|
+
/** JSON Schema (subset) for describing tool parameters. */
|
|
17
|
+
interface JsonSchemaObject {
|
|
18
|
+
type: string;
|
|
19
|
+
description?: string;
|
|
20
|
+
properties?: Record<string, JsonSchemaObject>;
|
|
21
|
+
required?: string[];
|
|
22
|
+
items?: JsonSchemaObject;
|
|
23
|
+
enum?: string[];
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** Content block returned by execute handlers. */
|
|
27
|
+
interface TextContent {
|
|
28
|
+
type: 'text';
|
|
29
|
+
text: string;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** Specification for registering a tool with OpenClaw. */
|
|
33
|
+
interface ToolSpec {
|
|
34
|
+
name: string;
|
|
35
|
+
description: string;
|
|
36
|
+
parameters: {
|
|
37
|
+
type: 'object';
|
|
38
|
+
properties: Record<string, JsonSchemaObject>;
|
|
39
|
+
required?: string[];
|
|
40
|
+
};
|
|
41
|
+
execute: (_id: unknown, params: Record<string, unknown>) => Promise<{ content: TextContent[] }>;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** OpenClaw agent config shape (relevant subset). */
|
|
45
|
+
interface OpenClawConfig {
|
|
46
|
+
agents?: { defaults?: { workspace?: string } };
|
|
47
|
+
gateway?: {
|
|
48
|
+
port?: number;
|
|
49
|
+
bind?: string;
|
|
50
|
+
auth?: { token?: string; password?: string };
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** The API object OpenClaw passes to register(). */
|
|
55
|
+
interface PluginApi {
|
|
56
|
+
/** OpenClaw configuration object. */
|
|
57
|
+
config: OpenClawConfig;
|
|
58
|
+
/** Register a tool with the OpenClaw agent. */
|
|
59
|
+
registerTool(spec: ToolSpec): void;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** Build a GatewayConfig from the OpenClaw config. Throws if auth is missing. */
|
|
63
|
+
function buildGatewayConfig(config: OpenClawConfig): GatewayConfig {
|
|
64
|
+
const token = config?.gateway?.auth?.token;
|
|
65
|
+
if (!token) {
|
|
66
|
+
throw new Error(
|
|
67
|
+
'WorkflowSkill plugin requires gateway auth to be configured. ' +
|
|
68
|
+
'Set config.gateway.auth.token in your OpenClaw configuration.',
|
|
69
|
+
);
|
|
70
|
+
}
|
|
71
|
+
// 'loopback' is an OpenClaw keyword meaning 127.0.0.1 — normalise to localhost.
|
|
72
|
+
const rawBind = config?.gateway?.bind ?? 'localhost';
|
|
73
|
+
const bind = rawBind === 'loopback' ? 'localhost' : rawBind;
|
|
74
|
+
const port = config?.gateway?.port ?? 3000;
|
|
75
|
+
return { baseUrl: `http://${bind}:${port}`, token };
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** Wrap a handler result as an OpenClaw text content response. */
|
|
79
|
+
function toContent(result: unknown): { content: TextContent[] } {
|
|
80
|
+
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// ─── Plugin entry point ────────────────────────────────────────────────────
|
|
84
|
+
|
|
85
|
+
export default {
|
|
86
|
+
id: 'workflowskill',
|
|
87
|
+
|
|
88
|
+
register(api: PluginApi): void {
|
|
89
|
+
const workspace = api?.config?.agents?.defaults?.workspace;
|
|
90
|
+
if (typeof workspace !== 'string' || workspace.length === 0) {
|
|
91
|
+
throw new Error(
|
|
92
|
+
`WorkflowSkill plugin requires a valid workspace path in config.agents.defaults.workspace. Received: ${JSON.stringify(workspace)}`,
|
|
93
|
+
);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// Write the canonical authoring skill from the workflowskill package so
|
|
97
|
+
// resolveSkillContent() finds it at the expected plugin-bundled path.
|
|
98
|
+
// Append OpenClaw-specific context.
|
|
99
|
+
const skillDir = join(import.meta.dirname, 'skills', 'workflowskill-author');
|
|
100
|
+
mkdirSync(skillDir, { recursive: true });
|
|
101
|
+
const contextPath = join(import.meta.dirname, 'lib', 'openclaw-context.md');
|
|
102
|
+
const openclawContext = readFileSync(contextPath, 'utf-8')
|
|
103
|
+
.replace(/\{\{workspace\}\}/g, workspace);
|
|
104
|
+
writeFileSync(join(skillDir, 'SKILL.md'), AUTHORING_SKILL + '\n' + openclawContext, 'utf-8');
|
|
105
|
+
|
|
106
|
+
const gatewayConfig = buildGatewayConfig(api.config);
|
|
107
|
+
const adapters = createAdapters(gatewayConfig);
|
|
108
|
+
const { registerTool } = api;
|
|
109
|
+
|
|
110
|
+
// ── workflowskill_validate ────────────────────────────────────────────
|
|
111
|
+
registerTool({
|
|
112
|
+
name: 'workflowskill_validate',
|
|
113
|
+
description:
|
|
114
|
+
'Parse and validate a WorkflowSkill SKILL.md or raw YAML. ' +
|
|
115
|
+
'Returns { valid, errors[], name, stepCount, stepTypes[] }. ' +
|
|
116
|
+
'Use before running to catch structural and type errors early.',
|
|
117
|
+
parameters: {
|
|
118
|
+
type: 'object',
|
|
119
|
+
properties: {
|
|
120
|
+
content: {
|
|
121
|
+
type: 'string',
|
|
122
|
+
description: 'Full SKILL.md text or raw workflow YAML to validate.',
|
|
123
|
+
},
|
|
124
|
+
},
|
|
125
|
+
required: ['content'],
|
|
126
|
+
},
|
|
127
|
+
execute: async (_id, params) => {
|
|
128
|
+
return toContent(await validateHandler(params as { content: string }, adapters.toolAdapter));
|
|
129
|
+
},
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
// ── workflowskill_run ─────────────────────────────────────────────────
|
|
133
|
+
registerTool({
|
|
134
|
+
name: 'workflowskill_run',
|
|
135
|
+
description:
|
|
136
|
+
'Execute a WorkflowSkill workflow and return a compact run summary. ' +
|
|
137
|
+
'Accepts a skill_name (resolved from skills directories) or inline content. ' +
|
|
138
|
+
'The full RunLog is persisted to workflow-runs/ automatically. ' +
|
|
139
|
+
'Use workflowskill_runs with run_id to retrieve full step-level detail.',
|
|
140
|
+
parameters: {
|
|
141
|
+
type: 'object',
|
|
142
|
+
properties: {
|
|
143
|
+
workflow_name: {
|
|
144
|
+
type: 'string',
|
|
145
|
+
description: 'Name of a workflow skill to resolve from skills directories.',
|
|
146
|
+
},
|
|
147
|
+
content: {
|
|
148
|
+
type: 'string',
|
|
149
|
+
description: 'Inline SKILL.md content (alternative to workflow_name).',
|
|
150
|
+
},
|
|
151
|
+
inputs: {
|
|
152
|
+
type: 'object',
|
|
153
|
+
description: 'Input values to override workflow defaults. Optional.',
|
|
154
|
+
},
|
|
155
|
+
},
|
|
156
|
+
},
|
|
157
|
+
execute: async (_id, params) => {
|
|
158
|
+
return toContent(
|
|
159
|
+
await runHandler(
|
|
160
|
+
params as { workflow_name?: string; content?: string; inputs?: Record<string, unknown> },
|
|
161
|
+
workspace,
|
|
162
|
+
adapters,
|
|
163
|
+
),
|
|
164
|
+
);
|
|
165
|
+
},
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
// ── workflowskill_runs ────────────────────────────────────────────────
|
|
169
|
+
registerTool({
|
|
170
|
+
name: 'workflowskill_runs',
|
|
171
|
+
description:
|
|
172
|
+
'List and inspect past workflow run logs. ' +
|
|
173
|
+
'No params → 20 most recent runs (summary). ' +
|
|
174
|
+
'workflow_name → filter by workflow. ' +
|
|
175
|
+
'run_id → full RunLog detail. ' +
|
|
176
|
+
'status → filter by "success" or "failed". ' +
|
|
177
|
+
'Use for failure diagnosis: find failed run → detail view → explain first failed step.',
|
|
178
|
+
parameters: {
|
|
179
|
+
type: 'object',
|
|
180
|
+
properties: {
|
|
181
|
+
workflow_name: {
|
|
182
|
+
type: 'string',
|
|
183
|
+
description: 'Filter results to a specific workflow by name.',
|
|
184
|
+
},
|
|
185
|
+
run_id: {
|
|
186
|
+
type: 'string',
|
|
187
|
+
description: 'Get the full RunLog for a specific run ID.',
|
|
188
|
+
},
|
|
189
|
+
status: {
|
|
190
|
+
type: 'string',
|
|
191
|
+
enum: ['success', 'failed'],
|
|
192
|
+
description: 'Filter by run status.',
|
|
193
|
+
},
|
|
194
|
+
},
|
|
195
|
+
},
|
|
196
|
+
execute: async (_id, params) => {
|
|
197
|
+
return toContent(
|
|
198
|
+
await runsHandler(
|
|
199
|
+
params as { workflow_name?: string; run_id?: string; status?: string },
|
|
200
|
+
workspace,
|
|
201
|
+
),
|
|
202
|
+
);
|
|
203
|
+
},
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
// ── workflowskill_llm ─────────────────────────────────────────────────────
|
|
207
|
+
registerTool({
|
|
208
|
+
name: 'workflowskill_llm',
|
|
209
|
+
description:
|
|
210
|
+
'Call Anthropic directly and return the text response. ' +
|
|
211
|
+
'Uses the API key from OpenClaw\'s credential store (~/.openclaw/agents/main/agent/auth-profiles.json). ' +
|
|
212
|
+
'Use in workflow tool steps when you need LLM reasoning inline. ' +
|
|
213
|
+
'model is optional (haiku / sonnet / opus or full model ID); omit for the default.',
|
|
214
|
+
parameters: {
|
|
215
|
+
type: 'object',
|
|
216
|
+
properties: {
|
|
217
|
+
prompt: {
|
|
218
|
+
type: 'string',
|
|
219
|
+
description: 'The prompt to send to the LLM.',
|
|
220
|
+
},
|
|
221
|
+
model: {
|
|
222
|
+
type: 'string',
|
|
223
|
+
description: 'Model alias or ID. Optional — omit to use the Anthropic default.',
|
|
224
|
+
},
|
|
225
|
+
},
|
|
226
|
+
required: ['prompt'],
|
|
227
|
+
},
|
|
228
|
+
execute: async (_id, params) => {
|
|
229
|
+
const { prompt, model } = params as { prompt: string; model?: string };
|
|
230
|
+
const result = await adapters.llmAdapter.call(model, prompt);
|
|
231
|
+
return toContent({ text: result.text });
|
|
232
|
+
},
|
|
233
|
+
});
|
|
234
|
+
},
|
|
235
|
+
};
|
package/lib/adapters.ts
ADDED
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
// adapters.ts — host-delegating adapters for the OpenClaw plugin.
|
|
2
|
+
//
|
|
3
|
+
// Tool steps delegate to the Gateway HTTP API via HostToolAdapter (POST /tools/invoke).
|
|
4
|
+
// LLM steps use AnthropicLLMAdapter with the API key read directly from
|
|
5
|
+
// OpenClaw's credential store at ~/.openclaw/agents/main/agent/auth-profiles.json.
|
|
6
|
+
|
|
7
|
+
import { readFileSync } from 'node:fs';
|
|
8
|
+
import { homedir } from 'node:os';
|
|
9
|
+
import { join } from 'node:path';
|
|
10
|
+
import type { LLMAdapter, ToolAdapter, ToolDescriptor, ToolResult } from 'workflowskill';
|
|
11
|
+
import { AnthropicLLMAdapter } from 'workflowskill';
|
|
12
|
+
|
|
13
|
+
export interface GatewayConfig {
|
|
14
|
+
baseUrl: string;
|
|
15
|
+
token: string;
|
|
16
|
+
timeoutMs?: number;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface AdapterSet {
|
|
20
|
+
toolAdapter: ToolAdapter;
|
|
21
|
+
llmAdapter: LLMAdapter;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// Tools this plugin registers — must not be forwarded to the gateway to prevent infinite recursion.
|
|
25
|
+
const SELF_REFERENCING_TOOLS = new Set([
|
|
26
|
+
'workflowskill_validate',
|
|
27
|
+
'workflowskill_run',
|
|
28
|
+
'workflowskill_runs',
|
|
29
|
+
'workflowskill_llm',
|
|
30
|
+
]);
|
|
31
|
+
|
|
32
|
+
/** ToolAdapter that delegates to the Gateway HTTP API via POST /tools/invoke. */
|
|
33
|
+
export class HostToolAdapter implements ToolAdapter {
|
|
34
|
+
private readonly baseUrl: string;
|
|
35
|
+
private readonly token: string;
|
|
36
|
+
private readonly timeoutMs: number;
|
|
37
|
+
|
|
38
|
+
constructor(config: GatewayConfig) {
|
|
39
|
+
this.baseUrl = config.baseUrl.replace(/\/$/, '');
|
|
40
|
+
this.token = config.token;
|
|
41
|
+
this.timeoutMs = config.timeoutMs ?? 30_000;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
has(toolName: string): boolean {
|
|
45
|
+
return !SELF_REFERENCING_TOOLS.has(toolName);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
async invoke(toolName: string, args: Record<string, unknown>): Promise<ToolResult> {
|
|
49
|
+
if (SELF_REFERENCING_TOOLS.has(toolName)) {
|
|
50
|
+
return {
|
|
51
|
+
output: null,
|
|
52
|
+
error: `Tool '${toolName}' cannot be called from within a workflow (self-referencing).`,
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
let response: Response;
|
|
57
|
+
try {
|
|
58
|
+
response = await fetch(`${this.baseUrl}/tools/invoke`, {
|
|
59
|
+
method: 'POST',
|
|
60
|
+
headers: {
|
|
61
|
+
'Content-Type': 'application/json',
|
|
62
|
+
Authorization: `Bearer ${this.token}`,
|
|
63
|
+
},
|
|
64
|
+
body: JSON.stringify({ tool: toolName, args }),
|
|
65
|
+
signal: AbortSignal.timeout(this.timeoutMs),
|
|
66
|
+
});
|
|
67
|
+
} catch (err) {
|
|
68
|
+
return {
|
|
69
|
+
output: null,
|
|
70
|
+
error: `Tool '${toolName}' invocation failed: ${err instanceof Error ? err.message : String(err)}`,
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
if (response.status === 200) {
|
|
75
|
+
const result = (await response.json()) as unknown;
|
|
76
|
+
return { output: result };
|
|
77
|
+
}
|
|
78
|
+
if (response.status === 404) {
|
|
79
|
+
return { output: null, error: `Tool '${toolName}' not found or blocked by the gateway.` };
|
|
80
|
+
}
|
|
81
|
+
if (response.status === 401) {
|
|
82
|
+
return {
|
|
83
|
+
output: null,
|
|
84
|
+
error: `Tool '${toolName}' invocation failed: unauthorized. Check your gateway token.`,
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
if (response.status === 429) {
|
|
88
|
+
return {
|
|
89
|
+
output: null,
|
|
90
|
+
error: `Tool '${toolName}' invocation failed: rate limited by gateway.`,
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
const body = await response.text().catch(() => '');
|
|
94
|
+
return {
|
|
95
|
+
output: null,
|
|
96
|
+
error: `Tool '${toolName}' invocation failed: HTTP ${response.status}${body ? `: ${body}` : ''}`,
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
list(): ToolDescriptor[] {
|
|
101
|
+
return [];
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Read the Anthropic API key from OpenClaw's credential store.
|
|
107
|
+
* Throws a clear error if the file is missing or has no anthropic profile.
|
|
108
|
+
*/
|
|
109
|
+
function readAnthropicApiKey(): string {
|
|
110
|
+
const profilesPath = join(homedir(), '.openclaw', 'agents', 'main', 'agent', 'auth-profiles.json');
|
|
111
|
+
let parsed: {
|
|
112
|
+
profiles?: Record<string, { provider?: string; key?: string }>;
|
|
113
|
+
lastGood?: Record<string, string>;
|
|
114
|
+
};
|
|
115
|
+
try {
|
|
116
|
+
parsed = JSON.parse(readFileSync(profilesPath, 'utf-8')) as typeof parsed;
|
|
117
|
+
} catch (err) {
|
|
118
|
+
throw new Error(
|
|
119
|
+
`WorkflowSkill: could not read OpenClaw auth profiles from ${profilesPath}: ${err instanceof Error ? err.message : String(err)}`,
|
|
120
|
+
);
|
|
121
|
+
}
|
|
122
|
+
const profiles = parsed.profiles ?? {};
|
|
123
|
+
// Prefer the profile OpenClaw last used successfully for anthropic.
|
|
124
|
+
const lastGoodName = parsed.lastGood?.['anthropic'];
|
|
125
|
+
const profile = lastGoodName
|
|
126
|
+
? profiles[lastGoodName]
|
|
127
|
+
: Object.values(profiles).find((p) => p.provider === 'anthropic');
|
|
128
|
+
if (!profile?.key) {
|
|
129
|
+
throw new Error(
|
|
130
|
+
`WorkflowSkill: no anthropic profile found in ${profilesPath}. Add a profile with provider "anthropic" and a key.`,
|
|
131
|
+
);
|
|
132
|
+
}
|
|
133
|
+
return profile.key;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Create host adapters backed by the Gateway HTTP API.
|
|
138
|
+
*
|
|
139
|
+
* HostToolAdapter forwards tool steps to the gateway's POST /tools/invoke endpoint.
|
|
140
|
+
* Self-referencing tools (the plugin's own four tools) are blocked to prevent recursion.
|
|
141
|
+
* LLM steps use AnthropicLLMAdapter with the key read from OpenClaw's credential store.
|
|
142
|
+
*/
|
|
143
|
+
export function createAdapters(gatewayConfig: GatewayConfig): AdapterSet {
|
|
144
|
+
const hostTools = new HostToolAdapter(gatewayConfig);
|
|
145
|
+
const llmAdapter = new AnthropicLLMAdapter(readAnthropicApiKey());
|
|
146
|
+
|
|
147
|
+
const LLM_COMPLETE = 'workflowskill_llm';
|
|
148
|
+
const LLM_COMPLETE_DESCRIPTOR: ToolDescriptor = {
|
|
149
|
+
name: LLM_COMPLETE,
|
|
150
|
+
description: 'Call the host LLM with a prompt; returns { text }.',
|
|
151
|
+
};
|
|
152
|
+
|
|
153
|
+
const toolAdapter: ToolAdapter = {
|
|
154
|
+
has(toolName: string): boolean {
|
|
155
|
+
if (toolName === LLM_COMPLETE) return true;
|
|
156
|
+
return hostTools.has(toolName);
|
|
157
|
+
},
|
|
158
|
+
async invoke(toolName: string, args: Record<string, unknown>): Promise<ToolResult> {
|
|
159
|
+
if (toolName === LLM_COMPLETE) {
|
|
160
|
+
const result = await llmAdapter.call(
|
|
161
|
+
args.model as string | undefined,
|
|
162
|
+
args.prompt as string,
|
|
163
|
+
);
|
|
164
|
+
return { output: { text: result.text } };
|
|
165
|
+
}
|
|
166
|
+
return hostTools.invoke(toolName, args);
|
|
167
|
+
},
|
|
168
|
+
list(): ToolDescriptor[] {
|
|
169
|
+
const hostToolList = hostTools.list();
|
|
170
|
+
return [
|
|
171
|
+
LLM_COMPLETE_DESCRIPTOR,
|
|
172
|
+
...hostToolList.filter((t) => t.name !== LLM_COMPLETE),
|
|
173
|
+
];
|
|
174
|
+
},
|
|
175
|
+
};
|
|
176
|
+
|
|
177
|
+
return {
|
|
178
|
+
toolAdapter,
|
|
179
|
+
llmAdapter,
|
|
180
|
+
};
|
|
181
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
## OpenClaw Integration
|
|
2
|
+
|
|
3
|
+
### Where to Save Skills
|
|
4
|
+
|
|
5
|
+
Save authored workflows to:
|
|
6
|
+
|
|
7
|
+
```
|
|
8
|
+
{{workspace}}/skills/<name>/SKILL.md
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
### Scheduling Workflows via Cron
|
|
12
|
+
|
|
13
|
+
OpenClaw schedules cron jobs at `~/.openclaw/cron/jobs.json`.
|
|
14
|
+
|
|
15
|
+
When a cron triggers, it invokes an agent session. The message should be a short trigger — put delivery instructions (e.g. Slack channel) in the cron, not the workflow, so workflows stay reusable. Do not put business logic in the cron prompt; use `workflowskill_run` to invoke the workflow instead.
|
|
16
|
+
|
|
17
|
+
Always set `"model": "haiku"` on cron payloads — cron runs are lightweight orchestration and don't need a powerful model.
|
|
18
|
+
|
|
19
|
+
```json
|
|
20
|
+
{
|
|
21
|
+
"payload": {
|
|
22
|
+
"kind": "agentTurn",
|
|
23
|
+
"message": "Run the <name> workflow using workflowskill_run\n\nSend results to Slack in the #general channel",
|
|
24
|
+
"model": "haiku"
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
```
|
package/lib/storage.ts
ADDED
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
// storage.ts — RunLog file operations and skill resolution for the OpenClaw plugin.
|
|
2
|
+
|
|
3
|
+
import { existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
4
|
+
import { join } from 'node:path';
|
|
5
|
+
import type { RunLog } from 'workflowskill';
|
|
6
|
+
|
|
7
|
+
/** Path to the run-logs directory inside the workspace. */
|
|
8
|
+
export function runsDir(workspace: string): string {
|
|
9
|
+
return join(workspace, 'workflow-runs');
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Resolve a skill's SKILL.md content by name.
|
|
14
|
+
*
|
|
15
|
+
* Search order:
|
|
16
|
+
* 1. <workspace>/skills/<name>/SKILL.md
|
|
17
|
+
* 2. <plugin-dir>/skills/<name>/SKILL.md (bundled skills)
|
|
18
|
+
*
|
|
19
|
+
* Throws with a descriptive error listing all searched paths if not found.
|
|
20
|
+
*/
|
|
21
|
+
export function resolveSkillContent(workspace: string, name: string): string {
|
|
22
|
+
const workspacePath = join(workspace, 'skills', name, 'SKILL.md');
|
|
23
|
+
if (existsSync(workspacePath)) {
|
|
24
|
+
return readFileSync(workspacePath, 'utf-8');
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const pluginPath = join(import.meta.dirname, '..', 'skills', name, 'SKILL.md');
|
|
28
|
+
if (existsSync(pluginPath)) {
|
|
29
|
+
return readFileSync(pluginPath, 'utf-8');
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
throw new Error(
|
|
33
|
+
`Skill "${name}" not found. Searched:\n ${workspacePath}\n ${pluginPath}`,
|
|
34
|
+
);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** Persist a RunLog to <workspace>/workflow-runs/<name>-<timestamp>.json. */
|
|
38
|
+
export function saveRunLog(workspace: string, log: RunLog): string {
|
|
39
|
+
const dir = runsDir(workspace);
|
|
40
|
+
mkdirSync(dir, { recursive: true });
|
|
41
|
+
const safeTimestamp = log.started_at.replace(/:/g, '-');
|
|
42
|
+
const filename = `${log.workflow}-${safeTimestamp}.json`;
|
|
43
|
+
const path = join(dir, filename);
|
|
44
|
+
writeFileSync(path, JSON.stringify(log, null, 2) + '\n', 'utf-8');
|
|
45
|
+
return path;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export interface RunSummaryEntry {
|
|
49
|
+
id: string;
|
|
50
|
+
workflow: string;
|
|
51
|
+
status: string;
|
|
52
|
+
started_at: string;
|
|
53
|
+
duration_ms: number;
|
|
54
|
+
steps_executed: number;
|
|
55
|
+
steps_skipped: number;
|
|
56
|
+
total_tokens: number;
|
|
57
|
+
error_message?: string;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** Read all RunLog files and return summary entries, newest first. */
|
|
61
|
+
export function listRuns(
|
|
62
|
+
workspace: string,
|
|
63
|
+
filter?: { workflow_name?: string; status?: string },
|
|
64
|
+
): RunSummaryEntry[] {
|
|
65
|
+
const dir = runsDir(workspace);
|
|
66
|
+
let files: string[];
|
|
67
|
+
try {
|
|
68
|
+
files = readdirSync(dir).filter((f) => f.endsWith('.json'));
|
|
69
|
+
} catch {
|
|
70
|
+
return [];
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const entries: RunSummaryEntry[] = [];
|
|
74
|
+
for (const file of files) {
|
|
75
|
+
try {
|
|
76
|
+
const raw = readFileSync(join(dir, file), 'utf-8');
|
|
77
|
+
const log = JSON.parse(raw) as RunLog;
|
|
78
|
+
|
|
79
|
+
if (filter?.workflow_name && log.workflow !== filter.workflow_name) continue;
|
|
80
|
+
if (filter?.status && log.status !== filter.status) continue;
|
|
81
|
+
|
|
82
|
+
entries.push({
|
|
83
|
+
id: log.id,
|
|
84
|
+
workflow: log.workflow,
|
|
85
|
+
status: log.status,
|
|
86
|
+
started_at: log.started_at,
|
|
87
|
+
duration_ms: log.duration_ms,
|
|
88
|
+
steps_executed: log.summary.steps_executed,
|
|
89
|
+
steps_skipped: log.summary.steps_skipped,
|
|
90
|
+
total_tokens: log.summary.total_tokens,
|
|
91
|
+
error_message: log.error?.message,
|
|
92
|
+
});
|
|
93
|
+
} catch {
|
|
94
|
+
// Skip corrupt files
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// Sort newest first
|
|
99
|
+
entries.sort((a, b) => b.started_at.localeCompare(a.started_at));
|
|
100
|
+
return entries;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/** Read a single RunLog by run ID. Returns null if not found. */
|
|
104
|
+
export function getRunLog(workspace: string, runId: string): RunLog | null {
|
|
105
|
+
const dir = runsDir(workspace);
|
|
106
|
+
let files: string[];
|
|
107
|
+
try {
|
|
108
|
+
files = readdirSync(dir).filter((f) => f.endsWith('.json'));
|
|
109
|
+
} catch {
|
|
110
|
+
return null;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
for (const file of files) {
|
|
114
|
+
try {
|
|
115
|
+
const raw = readFileSync(join(dir, file), 'utf-8');
|
|
116
|
+
const log = JSON.parse(raw) as RunLog;
|
|
117
|
+
if (log.id === runId) return log;
|
|
118
|
+
} catch {
|
|
119
|
+
// Skip corrupt files
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
return null;
|
|
123
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
{
|
|
2
|
+
"id": "workflowskill",
|
|
3
|
+
"name": "WorkflowSkill",
|
|
4
|
+
"description": "Author, validate, run, and review WorkflowSkill YAML workflows",
|
|
5
|
+
"version": "0.1.0",
|
|
6
|
+
"skills": ["skills/workflowskill-author"],
|
|
7
|
+
"configSchema": {
|
|
8
|
+
"type": "object",
|
|
9
|
+
"additionalProperties": false,
|
|
10
|
+
"properties": {}
|
|
11
|
+
}
|
|
12
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "openclaw-workflowskill",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "WorkflowSkill plugin for OpenClaw — author, validate, run, and review YAML workflows",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "index.ts",
|
|
7
|
+
"exports": {
|
|
8
|
+
".": "./index.ts"
|
|
9
|
+
},
|
|
10
|
+
"files": [
|
|
11
|
+
"index.ts",
|
|
12
|
+
"lib/",
|
|
13
|
+
"tools/",
|
|
14
|
+
"openclaw.plugin.json",
|
|
15
|
+
"LICENSE"
|
|
16
|
+
],
|
|
17
|
+
"keywords": [
|
|
18
|
+
"openclaw",
|
|
19
|
+
"workflowskill",
|
|
20
|
+
"workflow",
|
|
21
|
+
"automation"
|
|
22
|
+
],
|
|
23
|
+
"license": "MIT",
|
|
24
|
+
"author": "Matthew Cromer",
|
|
25
|
+
"repository": {
|
|
26
|
+
"type": "git",
|
|
27
|
+
"url": "git+https://github.com/matthew-h-cromer/openclaw-workflowskill.git"
|
|
28
|
+
},
|
|
29
|
+
"engines": {
|
|
30
|
+
"node": ">=20.11.0"
|
|
31
|
+
},
|
|
32
|
+
"openclaw": {
|
|
33
|
+
"extensions": [
|
|
34
|
+
"./index.ts"
|
|
35
|
+
]
|
|
36
|
+
},
|
|
37
|
+
"peerDependencies": {
|
|
38
|
+
"workflowskill": ">=0.1.0"
|
|
39
|
+
},
|
|
40
|
+
"devDependencies": {
|
|
41
|
+
"@types/node": "^25.3.0",
|
|
42
|
+
"tsx": "^4.21.0",
|
|
43
|
+
"typescript": "^5.9.3",
|
|
44
|
+
"workflowskill": "^0.1.0"
|
|
45
|
+
},
|
|
46
|
+
"scripts": {
|
|
47
|
+
"typecheck": "tsc --noEmit --project tsconfig.json",
|
|
48
|
+
"reset": "bash scripts/reset.sh",
|
|
49
|
+
"prepublishOnly": "tsc --noEmit --project tsconfig.json"
|
|
50
|
+
}
|
|
51
|
+
}
|
package/tools/run.ts
ADDED
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
// workflowskill_run — execute a workflow and return a compact summary.
|
|
2
|
+
//
|
|
3
|
+
// Accepts either a skill_name (resolved from skills directories)
|
|
4
|
+
// or inline content. Persists the full RunLog to <workspace>/workflow-runs/.
|
|
5
|
+
// Returns a compact summary to avoid blowing up the calling agent's context.
|
|
6
|
+
|
|
7
|
+
import { runWorkflowSkill } from 'workflowskill';
|
|
8
|
+
import type { RunLog, RunSummary } from 'workflowskill';
|
|
9
|
+
import type { AdapterSet } from '../lib/adapters.js';
|
|
10
|
+
import { resolveSkillContent, saveRunLog } from '../lib/storage.js';
|
|
11
|
+
|
|
12
|
+
export interface RunParams {
|
|
13
|
+
workflow_name?: string;
|
|
14
|
+
content?: string;
|
|
15
|
+
inputs?: Record<string, unknown>;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
interface RunSummarySuccess {
|
|
19
|
+
status: 'success';
|
|
20
|
+
id: string;
|
|
21
|
+
workflow: string;
|
|
22
|
+
duration_ms: number;
|
|
23
|
+
summary: RunSummary;
|
|
24
|
+
outputs: Record<string, unknown>;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
interface RunSummaryFailed {
|
|
28
|
+
status: 'failed';
|
|
29
|
+
id: string;
|
|
30
|
+
workflow: string;
|
|
31
|
+
duration_ms: number;
|
|
32
|
+
summary: RunSummary;
|
|
33
|
+
error: RunLog['error'];
|
|
34
|
+
failed_step: {
|
|
35
|
+
id: string;
|
|
36
|
+
executor: string;
|
|
37
|
+
error: string | undefined;
|
|
38
|
+
} | undefined;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function summarizeRunLog(log: RunLog): RunSummarySuccess | RunSummaryFailed {
|
|
42
|
+
const base = {
|
|
43
|
+
id: log.id,
|
|
44
|
+
workflow: log.workflow,
|
|
45
|
+
duration_ms: log.duration_ms,
|
|
46
|
+
summary: log.summary,
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
if (log.status === 'success') {
|
|
50
|
+
return { status: 'success', ...base, outputs: log.outputs };
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const failedStep = log.steps.find((s) => s.status === 'failed');
|
|
54
|
+
return {
|
|
55
|
+
status: 'failed',
|
|
56
|
+
...base,
|
|
57
|
+
error: log.error,
|
|
58
|
+
failed_step: failedStep
|
|
59
|
+
? { id: failedStep.id, executor: failedStep.executor, error: failedStep.error }
|
|
60
|
+
: undefined,
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export async function runHandler(
|
|
65
|
+
params: RunParams,
|
|
66
|
+
workspace: string,
|
|
67
|
+
adapters: AdapterSet,
|
|
68
|
+
): Promise<RunSummarySuccess | RunSummaryFailed> {
|
|
69
|
+
const { workflow_name, content: inlineContent, inputs = {} } = params;
|
|
70
|
+
|
|
71
|
+
let content = inlineContent ?? '';
|
|
72
|
+
if (!content && workflow_name) {
|
|
73
|
+
content = resolveSkillContent(workspace, workflow_name);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const { toolAdapter, llmAdapter } = adapters;
|
|
77
|
+
|
|
78
|
+
const log: RunLog = await runWorkflowSkill({
|
|
79
|
+
content,
|
|
80
|
+
inputs,
|
|
81
|
+
toolAdapter,
|
|
82
|
+
llmAdapter,
|
|
83
|
+
workflowName: workflow_name ?? 'inline',
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
// Persist full log
|
|
87
|
+
try {
|
|
88
|
+
saveRunLog(workspace, log);
|
|
89
|
+
} catch {
|
|
90
|
+
// Persistence failure is non-fatal
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
return summarizeRunLog(log);
|
|
94
|
+
}
|
package/tools/runs.ts
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
// workflowskill_runs — list and inspect past run logs.
|
|
2
|
+
//
|
|
3
|
+
// No params → list 20 most recent runs (summary view)
|
|
4
|
+
// workflow_name → filter by workflow name
|
|
5
|
+
// run_id → get full RunLog detail for one run
|
|
6
|
+
// status → filter by "success" or "failed"
|
|
7
|
+
|
|
8
|
+
import type { RunLog } from 'workflowskill';
|
|
9
|
+
import { getRunLog, listRuns, type RunSummaryEntry } from '../lib/storage.js';
|
|
10
|
+
|
|
11
|
+
export interface RunsParams {
|
|
12
|
+
workflow_name?: string;
|
|
13
|
+
run_id?: string;
|
|
14
|
+
status?: string;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export type RunsResult = RunSummaryEntry[] | RunLog | { error: string };
|
|
18
|
+
|
|
19
|
+
const RECENT_LIMIT = 20;
|
|
20
|
+
|
|
21
|
+
export function runsHandler(params: RunsParams, workspace: string): RunsResult {
|
|
22
|
+
const { workflow_name, run_id, status } = params;
|
|
23
|
+
|
|
24
|
+
// Detail view — return full RunLog for a specific run
|
|
25
|
+
if (run_id) {
|
|
26
|
+
const log = getRunLog(workspace, run_id);
|
|
27
|
+
if (!log) {
|
|
28
|
+
return { error: `No run found with id "${run_id}"` };
|
|
29
|
+
}
|
|
30
|
+
return log;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// Summary view — list and filter
|
|
34
|
+
const entries = listRuns(workspace, {
|
|
35
|
+
workflow_name,
|
|
36
|
+
status,
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
return entries.slice(0, RECENT_LIMIT);
|
|
40
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
// workflowskill_validate — parse and validate SKILL.md or raw YAML.
|
|
2
|
+
|
|
3
|
+
import { validateWorkflowSkill } from 'workflowskill';
|
|
4
|
+
import type { ToolAdapter } from 'workflowskill';
|
|
5
|
+
|
|
6
|
+
export interface ValidateParams {
|
|
7
|
+
content: string;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export interface ValidateResult {
|
|
11
|
+
valid: boolean;
|
|
12
|
+
errors: Array<{ path: string; message: string }>;
|
|
13
|
+
name?: string;
|
|
14
|
+
stepCount?: number;
|
|
15
|
+
stepTypes?: string[];
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export async function validateHandler(params: ValidateParams, toolAdapter: ToolAdapter): Promise<ValidateResult> {
|
|
19
|
+
return validateWorkflowSkill({ content: params.content, toolAdapter });
|
|
20
|
+
}
|