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 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,242 @@
1
+ # WorkflowSkill
2
+
3
+ [![npm](https://img.shields.io/npm/v/workflowskill.svg)](https://www.npmjs.com/package/workflowskill)
4
+ [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](https://github.com/matthew-h-cromer/workflowskill/blob/main/LICENSE)
5
+ [![Node: >=20](https://img.shields.io/badge/Node-%3E%3D20-green.svg)](https://nodejs.org)
6
+ [![PRs Welcome](https://img.shields.io/badge/PRs-welcome-brightgreen.svg)](https://github.com/matthew-h-cromer/workflowskill/issues)
7
+
8
+ > [!IMPORTANT]
9
+ > **Pre-release.** The spec and reference runtime are complete and tested, but the API is not yet frozen. This is a good time to influence direction — open an issue if something feels wrong, missing, or over-engineered.
10
+
11
+ A declarative workflow language for AI agents.
12
+
13
+ 1. You prompt what you need: "I want to check this website daily"
14
+ 2. Your agent writes a WorkflowSkill — an extension to [Agent Skills](https://agentskills.io/home) — that can be executed deterministically by any compatible runtime.
15
+ 3. The WorkflowSkill runs reliably and cheaply for repetitive tasks — no agent needed.
16
+
17
+ ```yaml
18
+ inputs:
19
+ endpoint:
20
+ type: string
21
+
22
+ outputs:
23
+ summary:
24
+ type: string
25
+ value: $steps.summarize.output.summary
26
+
27
+ steps:
28
+ - id: fetch
29
+ type: tool
30
+ tool: http.request
31
+ inputs:
32
+ url:
33
+ type: string
34
+ value: $inputs.endpoint
35
+ outputs:
36
+ body:
37
+ type: string
38
+
39
+ - id: summarize
40
+ type: llm
41
+ prompt: "Summarize this content: $steps.fetch.output.body"
42
+ outputs:
43
+ summary:
44
+ type: string
45
+ value: $result
46
+ ```
47
+
48
+ ## Install
49
+
50
+ ```bash
51
+ npm install workflowskill
52
+ ```
53
+
54
+ ## CLI
55
+
56
+ ```bash
57
+ # Validate a workflow (no API keys needed)
58
+ workflowskill validate path/to/workflow.md
59
+
60
+ # Run a workflow
61
+ workflowskill run path/to/workflow.md
62
+
63
+ # Pass inputs as JSON
64
+ workflowskill run path/to/workflow.md --input '{"keywords": "rust developer"}'
65
+ ```
66
+
67
+ The CLI shows live progress on stderr and writes a structured [run log](#run-log) as JSON to stdout and to `runs/<name>-<timestamp>.json`.
68
+
69
+ ## Authoring
70
+
71
+ A WorkflowSkill is authored via natural conversation with any agent system that supports the [Agent Skills](https://agentskills.io/home) format.
72
+
73
+ 1. Prompt your agent to create a workflow: "I want to check this website daily"
74
+ - For Claude Code, this package ships `skill/SKILL.md` which teaches it to author valid WorkflowSkill YAML. For other agent tools, provide the contents of `node_modules/workflowskill/skill/SKILL.md` as context.
75
+ 2. Your agent writes a WorkflowSkill.
76
+ 3. Run it deterministically: `workflowskill run <workflow.md>`
77
+
78
+ Your agent will research the task, write the workflow file, and validate it with the CLI. The dev tools available to generated workflows are:
79
+
80
+ | Tool | What it does | Needs credentials |
81
+ | --------------- | ------------------------------------- | ----------------- |
82
+ | `http.request` | HTTP GET/POST/PUT/PATCH/DELETE | No |
83
+ | `html.select` | CSS selector extraction from HTML | No |
84
+ | `gmail.search` | Search Gmail by query | Google OAuth2 |
85
+ | `gmail.read` | Read a Gmail message by ID | Google OAuth2 |
86
+ | `gmail.send` | Send email via Gmail | Google OAuth2 |
87
+ | `sheets.read` | Read a Google Sheets range | Google OAuth2 |
88
+ | `sheets.write` | Write to a Google Sheets range | Google OAuth2 |
89
+ | `sheets.append` | Append rows to a Google Sheets range | Google OAuth2 |
90
+
91
+ `http.request` and `html.select` work with no setup. For Google tools, add credentials to `.env` (see [Configuration](#configuration)).
92
+
93
+ **Evaluate the output:** Check `status` for overall success. If a step failed, its `error` field explains why. For `each` steps, `iterations` shows per-item results. Compare per-step `inputs` and `output` values against your expectations to find where the data flow broke down.
94
+
95
+ ## Language overview
96
+
97
+ WorkflowSkill workflows are YAML documents with five step types:
98
+
99
+ | Step type | Description |
100
+ | ------------- | ------------------------------------------------------------------------------------------ |
101
+ | `tool` | Invoke an MCP server endpoint, dev tool (HTTP, Gmail, Sheets), or any registered function |
102
+ | `llm` | Call a language model with a templated prompt |
103
+ | `transform` | Filter, map, or sort data without side effects |
104
+ | `conditional` | Branch execution based on an expression |
105
+ | `exit` | Terminate early with a status and output |
106
+
107
+ Steps are connected by `$steps.<id>.output.<field>` references. Loops use `each`. Error handling uses `on_error: fail | ignore` (retries are a separate `retry:` field).
108
+
109
+ See the [full language specification](https://github.com/matthew-h-cromer/workflowskill/blob/main/SPEC.md) for details.
110
+
111
+ ## Library API
112
+
113
+ Two entry points collapse parse → validate → run into single calls that accept raw content and never throw.
114
+
115
+ ```typescript
116
+ import {
117
+ runWorkflowSkill,
118
+ validateWorkflowSkill,
119
+ } from "workflowskill";
120
+
121
+ // Validate a workflow (synchronous, never throws)
122
+ const result = validateWorkflowSkill({
123
+ content, // SKILL.md with frontmatter or bare workflow YAML
124
+ toolAdapter, // optional — skips tool availability checks if absent
125
+ });
126
+
127
+ if (!result.valid) {
128
+ console.error(result.errors);
129
+ }
130
+
131
+ // Run a workflow (async, never throws — always returns a RunLog)
132
+ const log = await runWorkflowSkill({
133
+ content, // SKILL.md with frontmatter or bare workflow YAML
134
+ inputs: { ... },
135
+ toolAdapter, // implements ToolAdapter
136
+ llmAdapter, // implements LLMAdapter
137
+ });
138
+
139
+ if (log.status === "success") {
140
+ console.log(log.outputs);
141
+ }
142
+ ```
143
+
144
+ Key ergonomic properties:
145
+
146
+ - **Accepts raw content** — SKILL.md with frontmatter or bare workflow YAML, no parsing step needed
147
+ - **Never throws** — parse, validation, and execution failures are encoded in the return value
148
+ - **`RunLog` is always returned**, with `error?: { phase, message, details }` on failure
149
+
150
+ ## Adapters
151
+
152
+ `ToolAdapter` and `LLMAdapter` are the integration boundaries:
153
+
154
+ ```typescript
155
+ interface ToolAdapter {
156
+ invoke(toolName: string, args: Record<string, unknown>): Promise<ToolResult>;
157
+ has(toolName: string): boolean;
158
+ list?(): ToolDescriptor[];
159
+ }
160
+
161
+ interface LLMAdapter {
162
+ call(
163
+ model: string | undefined,
164
+ prompt: string,
165
+ responseFormat?: Record<string, unknown>,
166
+ ): Promise<LLMResult>;
167
+ }
168
+ ```
169
+
170
+ Built-in implementations:
171
+
172
+ - **`DevToolAdapter`** — provides `http.request`, `html.select`, Gmail, and Google Sheets tools for standalone use
173
+ - **`AnthropicLLMAdapter`** — wraps the Anthropic SDK; reads `ANTHROPIC_API_KEY` from the environment
174
+
175
+ For testing, **`MockToolAdapter`** and **`MockLLMAdapter`** let you supply handler functions without any external dependencies.
176
+
177
+ ## Run log
178
+
179
+ Every call to `runWorkflowSkill` returns a `RunLog`:
180
+
181
+ ```typescript
182
+ interface RunLog {
183
+ id: string;
184
+ workflow: string;
185
+ status: "success" | "failed";
186
+ summary: {
187
+ steps_executed: number;
188
+ steps_skipped: number;
189
+ total_tokens: number;
190
+ total_duration_ms: number;
191
+ };
192
+ started_at: string; // ISO 8601
193
+ completed_at: string; // ISO 8601
194
+ duration_ms: number;
195
+ inputs: Record<string, unknown>;
196
+ steps: StepRecord[];
197
+ outputs: Record<string, unknown>;
198
+ error?: {
199
+ phase: "parse" | "validate" | "execute";
200
+ message: string;
201
+ details?: unknown;
202
+ };
203
+ }
204
+ ```
205
+
206
+ `error` is present only when the run failed. `outputs` is populated on success (and on `exit` steps with `status: failed`).
207
+
208
+ ## Low-level API
209
+
210
+ `parseWorkflowFromMd`, `validateWorkflow`, and `runWorkflow` are still exported for consumers who need fine-grained control over each phase.
211
+
212
+ ## Configuration
213
+
214
+ Create a `.env` file in your project directory (or export env vars in your shell):
215
+
216
+ ```
217
+ ANTHROPIC_API_KEY=sk-ant-... # Required for LLM steps in workflows
218
+ GOOGLE_CLIENT_ID=... # Required for Gmail and Sheets tools
219
+ GOOGLE_CLIENT_SECRET=...
220
+ GOOGLE_REFRESH_TOKEN=...
221
+ ```
222
+
223
+ Missing `ANTHROPIC_API_KEY` causes LLM steps to fail with a clear error (no silent fallback). Missing Google credentials → Google tools not registered (warning if a workflow references them).
224
+
225
+ ### Getting Google OAuth2 credentials
226
+
227
+ 1. Go to [Google Cloud Console](https://console.cloud.google.com/) and create or select a project.
228
+ 2. Enable the APIs you need: **Gmail API** and/or **Google Sheets API** under _APIs & Services > Library_.
229
+ 3. Go to _APIs & Services > OAuth consent screen_. Choose **External** and fill in the app name and your email.
230
+ 4. Go to _APIs & Services > Credentials > Create Credentials > OAuth client ID_. Choose **Desktop app**. Copy the **Client ID** and **Client Secret** into your `.env`.
231
+ 5. Get a refresh token. The easiest way is [Google's OAuth 2.0 Playground](https://developers.google.com/oauthplayground/):
232
+ - Click the gear icon, check **Use your own OAuth credentials**, and enter your Client ID and Client Secret.
233
+ - In the left panel, select the scopes you need: `https://www.googleapis.com/auth/gmail.modify` (under **Gmail API v1**) and/or `https://www.googleapis.com/auth/spreadsheets` (under **Sheets API v4**). Click **Authorize APIs** and sign in.
234
+ - Click **Exchange authorization code for tokens**. Copy the **Refresh token** into your `.env`.
235
+
236
+ ## Documentation
237
+
238
+ Full specification, design rationale, and examples: [github.com/matthew-h-cromer/workflowskill](https://github.com/matthew-h-cromer/workflowskill)
239
+
240
+ ## License
241
+
242
+ [MIT](https://github.com/matthew-h-cromer/workflowskill/blob/main/LICENSE)
@@ -0,0 +1 @@
1
+ export { };
@@ -0,0 +1,187 @@
1
+ #!/usr/bin/env node
2
+ import { S as ParseError, f as loadConfig, h as validateWorkflow, i as runWorkflowSkill, m as AnthropicLLMAdapter, p as DevToolAdapter, w as parseWorkflowFromMd } from "../runtime-BY1CFnew.mjs";
3
+ import { mkdirSync, readFileSync, writeFileSync } from "node:fs";
4
+ import { basename, join } from "node:path";
5
+ import { Command } from "commander";
6
+ import pc from "picocolors";
7
+
8
+ //#region src/cli/validate.ts
9
+ function validateCommand(files) {
10
+ let hasErrors = false;
11
+ for (const file of files) {
12
+ let content;
13
+ try {
14
+ content = readFileSync(file, "utf-8");
15
+ } catch {
16
+ console.error(`✗ ${file}: Cannot read file`);
17
+ hasErrors = true;
18
+ continue;
19
+ }
20
+ let workflow;
21
+ try {
22
+ workflow = parseWorkflowFromMd(content);
23
+ } catch (err) {
24
+ if (err instanceof ParseError) {
25
+ console.error(`✗ ${file}: ${err.message}`);
26
+ for (const detail of err.details) console.error(` ${detail.path}: ${detail.message}`);
27
+ } else console.error(`✗ ${file}: ${err instanceof Error ? err.message : String(err)}`);
28
+ hasErrors = true;
29
+ continue;
30
+ }
31
+ const result = validateWorkflow(workflow);
32
+ if (!result.valid) {
33
+ console.error(`✗ ${file}: Validation errors`);
34
+ for (const error of result.errors) console.error(` ${error.path}: ${error.message}`);
35
+ hasErrors = true;
36
+ continue;
37
+ }
38
+ console.log(`✓ ${file} (${workflow.steps.length} steps)`);
39
+ }
40
+ if (hasErrors) process.exit(1);
41
+ }
42
+
43
+ //#endregion
44
+ //#region src/cli/format.ts
45
+ const runtimeActiveSteps = /* @__PURE__ */ new Map();
46
+ const isTTY = !!process.stderr.isTTY;
47
+ const MOVE_UP = (n) => `\x1b[${n}A`;
48
+ const CLEAR_LINE = "\x1B[2K\r";
49
+ const MOVE_DOWN = (n) => `\x1b[${n}B`;
50
+ let linesSinceStepStart = 0;
51
+ let hasEachProgress = false;
52
+ /**
53
+ * Render a runtime event to stderr with live step-by-step progress output.
54
+ * All CLI live output goes to stderr; stdout is reserved for the JSON run log.
55
+ *
56
+ * On TTY: the yellow "running..." line is overwritten in-place by the green/red
57
+ * completion line, and each_progress updates a single line instead of appending.
58
+ * On non-TTY (pipes, files): append-only fallback — existing behavior.
59
+ */
60
+ function renderRuntimeEvent(event) {
61
+ switch (event.type) {
62
+ case "workflow_start":
63
+ process.stderr.write(`\n${pc.bold("▶")} Running ${pc.cyan(event.workflow)} (${event.totalSteps} step${event.totalSteps !== 1 ? "s" : ""})\n\n`);
64
+ break;
65
+ case "step_start": {
66
+ linesSinceStepStart = 0;
67
+ hasEachProgress = false;
68
+ const typeLabel = event.tool ? `[tool] (${event.tool})` : `[${event.stepType}]`;
69
+ const line = ` ${pc.yellow("●")} ${event.stepId} ${pc.dim(typeLabel)} ${pc.dim("running...")}`;
70
+ runtimeActiveSteps.set(event.stepId, line);
71
+ process.stderr.write(line + "\n");
72
+ break;
73
+ }
74
+ case "step_complete": {
75
+ runtimeActiveSteps.delete(event.stepId);
76
+ const durationStr = formatDuration(event.duration_ms);
77
+ const extras = [];
78
+ if (event.tokens) extras.push(`${event.tokens.input + event.tokens.output} tokens`);
79
+ if (event.iterations !== void 0) extras.push(`${event.iterations} iterations`);
80
+ const extraStr = extras.length > 0 ? pc.dim(` (${extras.join(", ")})`) : "";
81
+ const completionLine = event.status === "success" ? ` ${pc.green("✓")} ${event.stepId} ${pc.dim(durationStr)}${extraStr}` : ` ${pc.red("✗")} ${event.stepId} ${pc.dim(durationStr)} ${pc.red("failed")}${extraStr}`;
82
+ if (isTTY) {
83
+ const up = linesSinceStepStart + 1;
84
+ process.stderr.write(MOVE_UP(up) + CLEAR_LINE + completionLine + "\n" + (linesSinceStepStart > 0 ? MOVE_DOWN(linesSinceStepStart) : ""));
85
+ } else process.stderr.write(completionLine + "\n");
86
+ break;
87
+ }
88
+ case "step_skip":
89
+ runtimeActiveSteps.delete(event.stepId);
90
+ process.stderr.write(` ${pc.dim("○")} ${pc.dim(event.stepId)} ${pc.dim("skipped")}: ${pc.dim(event.reason)}\n`);
91
+ break;
92
+ case "step_retry":
93
+ process.stderr.write(` ${pc.yellow("↻")} ${event.stepId} retry #${event.attempt}: ${pc.dim(event.error)}\n`);
94
+ linesSinceStepStart++;
95
+ break;
96
+ case "step_error":
97
+ process.stderr.write(` ${pc.red("✗")} ${event.stepId} error (${event.onError}): ${pc.dim(event.error)}\n`);
98
+ linesSinceStepStart++;
99
+ break;
100
+ case "each_progress":
101
+ if (isTTY && hasEachProgress) process.stderr.write(MOVE_UP(1) + CLEAR_LINE + ` ${pc.dim(`${event.current}/${event.total}`)}\n`);
102
+ else {
103
+ process.stderr.write(` ${pc.dim(`${event.current}/${event.total}`)}\n`);
104
+ linesSinceStepStart++;
105
+ hasEachProgress = true;
106
+ }
107
+ break;
108
+ case "workflow_complete": {
109
+ const durationStr = formatDuration(event.duration_ms);
110
+ const { steps_executed, steps_skipped, total_tokens } = event.summary;
111
+ const parts = [`${steps_executed} executed`];
112
+ if (steps_skipped > 0) parts.push(`${steps_skipped} skipped`);
113
+ if (total_tokens > 0) parts.push(`${total_tokens} tokens`);
114
+ const summary = parts.join(", ");
115
+ if (event.status === "success") process.stderr.write(`\n${pc.green(pc.bold("✓"))} ${pc.green("Success")} in ${durationStr} ${pc.dim(`(${summary})`)}\n`);
116
+ else process.stderr.write(`\n${pc.red(pc.bold("✗"))} ${pc.red("Failed")} in ${durationStr} ${pc.dim(`(${summary})`)}\n`);
117
+ break;
118
+ }
119
+ }
120
+ }
121
+ /** Format a duration in milliseconds as a human-readable string (e.g. "1.2s", "2m30s"). */
122
+ function formatDuration(ms) {
123
+ if (ms < 1e3) return `${ms}ms`;
124
+ const totalSeconds = ms / 1e3;
125
+ if (totalSeconds < 60) return `${totalSeconds.toFixed(1)}s`;
126
+ return `${Math.floor(totalSeconds / 60)}m${Math.round(totalSeconds % 60)}s`;
127
+ }
128
+
129
+ //#endregion
130
+ //#region src/cli/run.ts
131
+ /** Write a run log to stdout and persist it to disk. */
132
+ function writeRunLog(log, logDir) {
133
+ const json = JSON.stringify(log, null, 2);
134
+ mkdirSync(logDir, { recursive: true });
135
+ const safeTimestamp = log.started_at.replace(/:/g, "-");
136
+ const logFile = join(logDir, `${log.workflow}-${safeTimestamp}.json`);
137
+ writeFileSync(logFile, json + "\n", "utf-8");
138
+ console.error(`Run log written to ${logFile}`);
139
+ console.log(json);
140
+ }
141
+ async function runCommand(file, options) {
142
+ const logDir = options.logDir ?? "runs";
143
+ const workflowName = basename(file, ".md");
144
+ let content;
145
+ try {
146
+ content = readFileSync(file, "utf-8");
147
+ } catch (err) {
148
+ console.error(`Error: Cannot read file "${file}": ${err instanceof Error ? err.message : String(err)}`);
149
+ process.exit(1);
150
+ }
151
+ let inputs = {};
152
+ if (options.input) try {
153
+ inputs = JSON.parse(options.input);
154
+ } catch {
155
+ console.error("Error: --input must be valid JSON");
156
+ process.exit(1);
157
+ }
158
+ const config = loadConfig();
159
+ const toolAdapter = await DevToolAdapter.create(config);
160
+ let llmAdapter;
161
+ if (config.anthropicApiKey) llmAdapter = new AnthropicLLMAdapter(config.anthropicApiKey);
162
+ else llmAdapter = { call() {
163
+ throw new Error("ANTHROPIC_API_KEY not set. This workflow has LLM steps that require it.\nSet it in runtime/.env or export it in your shell: export ANTHROPIC_API_KEY=sk-ant-...");
164
+ } };
165
+ const log = await runWorkflowSkill({
166
+ content,
167
+ inputs,
168
+ toolAdapter,
169
+ llmAdapter,
170
+ workflowName,
171
+ onEvent: renderRuntimeEvent
172
+ });
173
+ writeRunLog(log, logDir);
174
+ process.exit(log.status === "success" ? 0 : 1);
175
+ }
176
+
177
+ //#endregion
178
+ //#region src/cli/index.ts
179
+ const program = new Command();
180
+ program.name("workflowskill").description("WorkflowSkill runtime CLI").version("0.1.0");
181
+ program.command("validate").description("Validate one or more workflow SKILL.md files without executing").argument("<files...>", "Workflow files to validate").action(validateCommand);
182
+ program.command("run <file>").description("Execute a workflow SKILL.md file").option("-i, --input <json>", "Workflow inputs as JSON string", "{}").option("-l, --log-dir <dir>", "Directory to write run logs", "runs").action(runCommand);
183
+ program.parse();
184
+
185
+ //#endregion
186
+ export { };
187
+ //# sourceMappingURL=index.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.mjs","names":[],"sources":["../../src/cli/validate.ts","../../src/cli/format.ts","../../src/cli/run.ts","../../src/cli/index.ts"],"sourcesContent":["// CLI: validate command — check workflows without running them.\n\nimport { readFileSync } from 'node:fs';\nimport { parseWorkflowFromMd } from '../parser/index.js';\nimport { ParseError } from '../parser/index.js';\nimport { validateWorkflow } from '../validator/index.js';\n\nexport function validateCommand(files: string[]): void {\n let hasErrors = false;\n\n for (const file of files) {\n let content: string;\n try {\n content = readFileSync(file, 'utf-8');\n } catch {\n console.error(`✗ ${file}: Cannot read file`);\n hasErrors = true;\n continue;\n }\n\n // Parse\n let workflow;\n try {\n workflow = parseWorkflowFromMd(content);\n } catch (err) {\n if (err instanceof ParseError) {\n console.error(`✗ ${file}: ${err.message}`);\n for (const detail of err.details) {\n console.error(` ${detail.path}: ${detail.message}`);\n }\n } else {\n console.error(`✗ ${file}: ${err instanceof Error ? err.message : String(err)}`);\n }\n hasErrors = true;\n continue;\n }\n\n // Validate\n const result = validateWorkflow(workflow);\n if (!result.valid) {\n console.error(`✗ ${file}: Validation errors`);\n for (const error of result.errors) {\n console.error(` ${error.path}: ${error.message}`);\n }\n hasErrors = true;\n continue;\n }\n\n console.log(`✓ ${file} (${workflow.steps.length} steps)`);\n }\n\n if (hasErrors) {\n process.exit(1);\n }\n}\n","// CLI formatting module — runtime event rendering.\n// Uses picocolors for terminal colors and writes to stderr (keeping stdout clean for output).\n\nimport pc from 'picocolors';\nimport type { RuntimeEvent } from '../types/index.js';\n\n// ─── Runtime event renderer ───────────────────────────────────────────────────\n\n// Track in-progress step names for the \"running...\" line (keyed by stepId).\nconst runtimeActiveSteps = new Map<string, string>();\n\n// Whether stderr is a real TTY (enables in-place cursor movement).\nconst isTTY = !!process.stderr.isTTY;\n\n// ANSI helpers for in-place line updates (only used when isTTY is true).\nconst MOVE_UP = (n: number): string => `\\x1b[${n}A`;\nconst CLEAR_LINE = '\\x1b[2K\\r';\nconst MOVE_DOWN = (n: number): string => `\\x1b[${n}B`;\n\n// Lines printed between step_start and step_complete (retries, each_progress).\nlet linesSinceStepStart = 0;\n// Whether each_progress has been printed at least once for the current step.\nlet hasEachProgress = false;\n\n/**\n * Render a runtime event to stderr with live step-by-step progress output.\n * All CLI live output goes to stderr; stdout is reserved for the JSON run log.\n *\n * On TTY: the yellow \"running...\" line is overwritten in-place by the green/red\n * completion line, and each_progress updates a single line instead of appending.\n * On non-TTY (pipes, files): append-only fallback — existing behavior.\n */\nexport function renderRuntimeEvent(event: RuntimeEvent): void {\n switch (event.type) {\n case 'workflow_start':\n process.stderr.write(`\\n${pc.bold('▶')} Running ${pc.cyan(event.workflow)} (${event.totalSteps} step${event.totalSteps !== 1 ? 's' : ''})\\n\\n`);\n break;\n\n case 'step_start': {\n linesSinceStepStart = 0;\n hasEachProgress = false;\n const typeLabel = event.tool ? `[tool] (${event.tool})` : `[${event.stepType}]`;\n const line = ` ${pc.yellow('●')} ${event.stepId} ${pc.dim(typeLabel)} ${pc.dim('running...')}`;\n runtimeActiveSteps.set(event.stepId, line);\n process.stderr.write(line + '\\n');\n break;\n }\n\n case 'step_complete': {\n runtimeActiveSteps.delete(event.stepId);\n const durationStr = formatDuration(event.duration_ms);\n const extras: string[] = [];\n if (event.tokens) extras.push(`${event.tokens.input + event.tokens.output} tokens`);\n if (event.iterations !== undefined) extras.push(`${event.iterations} iterations`);\n const extraStr = extras.length > 0 ? pc.dim(` (${extras.join(', ')})`) : '';\n const completionLine =\n event.status === 'success'\n ? ` ${pc.green('✓')} ${event.stepId} ${pc.dim(durationStr)}${extraStr}`\n : ` ${pc.red('✗')} ${event.stepId} ${pc.dim(durationStr)} ${pc.red('failed')}${extraStr}`;\n\n if (isTTY) {\n // Move up past any intermediate lines + the \"running...\" line, overwrite it,\n // then move the cursor back down past the intermediate lines.\n const up = linesSinceStepStart + 1;\n process.stderr.write(\n MOVE_UP(up) + CLEAR_LINE + completionLine + '\\n' +\n (linesSinceStepStart > 0 ? MOVE_DOWN(linesSinceStepStart) : ''),\n );\n } else {\n process.stderr.write(completionLine + '\\n');\n }\n break;\n }\n\n case 'step_skip':\n runtimeActiveSteps.delete(event.stepId);\n process.stderr.write(` ${pc.dim('○')} ${pc.dim(event.stepId)} ${pc.dim('skipped')}: ${pc.dim(event.reason)}\\n`);\n break;\n\n case 'step_retry':\n process.stderr.write(` ${pc.yellow('↻')} ${event.stepId} retry #${event.attempt}: ${pc.dim(event.error)}\\n`);\n linesSinceStepStart++;\n break;\n\n case 'step_error':\n process.stderr.write(` ${pc.red('✗')} ${event.stepId} error (${event.onError}): ${pc.dim(event.error)}\\n`);\n linesSinceStepStart++;\n break;\n\n case 'each_progress':\n if (isTTY && hasEachProgress) {\n // Overwrite the previous each_progress line in-place.\n process.stderr.write(MOVE_UP(1) + CLEAR_LINE + ` ${pc.dim(`${event.current}/${event.total}`)}\\n`);\n } else {\n process.stderr.write(` ${pc.dim(`${event.current}/${event.total}`)}\\n`);\n linesSinceStepStart++;\n hasEachProgress = true;\n }\n break;\n\n case 'workflow_complete': {\n const durationStr = formatDuration(event.duration_ms);\n const { steps_executed, steps_skipped, total_tokens } = event.summary;\n const parts: string[] = [`${steps_executed} executed`];\n if (steps_skipped > 0) parts.push(`${steps_skipped} skipped`);\n if (total_tokens > 0) parts.push(`${total_tokens} tokens`);\n const summary = parts.join(', ');\n if (event.status === 'success') {\n process.stderr.write(`\\n${pc.green(pc.bold('✓'))} ${pc.green('Success')} in ${durationStr} ${pc.dim(`(${summary})`)}\\n`);\n } else {\n process.stderr.write(`\\n${pc.red(pc.bold('✗'))} ${pc.red('Failed')} in ${durationStr} ${pc.dim(`(${summary})`)}\\n`);\n }\n break;\n }\n }\n}\n\n/** Reset runtime format state (for testing). */\nexport function resetRuntimeFormatState(): void {\n runtimeActiveSteps.clear();\n linesSinceStepStart = 0;\n hasEachProgress = false;\n}\n\n/** Format a duration in milliseconds as a human-readable string (e.g. \"1.2s\", \"2m30s\"). */\nexport function formatDuration(ms: number): string {\n if (ms < 1000) return `${ms}ms`;\n const totalSeconds = ms / 1000;\n if (totalSeconds < 60) return `${totalSeconds.toFixed(1)}s`;\n const minutes = Math.floor(totalSeconds / 60);\n const seconds = Math.round(totalSeconds % 60);\n return `${minutes}m${seconds}s`;\n}\n","// CLI: run command — execute a workflow and print the run log.\n\nimport { mkdirSync, readFileSync, writeFileSync } from 'node:fs';\nimport { basename, join } from 'node:path';\nimport { runWorkflowSkill } from '../runtime/index.js';\nimport type { RunLog } from '../types/index.js';\nimport { loadConfig } from '../config/index.js';\nimport { AnthropicLLMAdapter } from '../adapters/anthropic-llm-adapter.js';\nimport { DevToolAdapter } from '../dev-tools/dev-tool-adapter.js';\nimport type { LLMAdapter, LLMResult } from '../types/index.js';\nimport { renderRuntimeEvent } from './format.js';\n\n/** Write a run log to stdout and persist it to disk. */\nfunction writeRunLog(log: RunLog, logDir: string): void {\n const json = JSON.stringify(log, null, 2);\n mkdirSync(logDir, { recursive: true });\n const safeTimestamp = log.started_at.replace(/:/g, '-');\n const logFile = join(logDir, `${log.workflow}-${safeTimestamp}.json`);\n writeFileSync(logFile, json + '\\n', 'utf-8');\n console.error(`Run log written to ${logFile}`);\n console.log(json);\n}\n\nexport async function runCommand(\n file: string,\n options: { input?: string; logDir?: string },\n): Promise<void> {\n const logDir = options.logDir ?? 'runs';\n const workflowName = basename(file, '.md');\n\n let content: string;\n try {\n content = readFileSync(file, 'utf-8');\n } catch (err) {\n console.error(`Error: Cannot read file \"${file}\": ${err instanceof Error ? err.message : String(err)}`);\n process.exit(1);\n }\n\n // Parse inputs\n let inputs: Record<string, unknown> = {};\n if (options.input) {\n try {\n inputs = JSON.parse(options.input) as Record<string, unknown>;\n } catch {\n console.error('Error: --input must be valid JSON');\n process.exit(1);\n }\n }\n\n // Load config and create adapters\n const config = loadConfig();\n const toolAdapter = await DevToolAdapter.create(config);\n let llmAdapter: LLMAdapter;\n if (config.anthropicApiKey) {\n llmAdapter = new AnthropicLLMAdapter(config.anthropicApiKey);\n } else {\n // No API key — create an adapter that fails with a clear error on first use.\n // Workflows without LLM steps (e.g., hello-world.md) still work.\n llmAdapter = {\n call(): Promise<LLMResult> {\n throw new Error(\n 'ANTHROPIC_API_KEY not set. This workflow has LLM steps that require it.\\n' +\n 'Set it in runtime/.env or export it in your shell: export ANTHROPIC_API_KEY=sk-ant-...',\n );\n },\n };\n }\n\n const log = await runWorkflowSkill({\n content,\n inputs,\n toolAdapter,\n llmAdapter,\n workflowName,\n onEvent: renderRuntimeEvent,\n });\n\n writeRunLog(log, logDir);\n process.exit(log.status === 'success' ? 0 : 1);\n}\n","#!/usr/bin/env node\n// WorkflowSkill CLI — validate and run workflows.\n\nimport { Command } from 'commander';\nimport { validateCommand } from './validate.js';\nimport { runCommand } from './run.js';\n\nconst program = new Command();\n\nprogram\n .name('workflowskill')\n .description('WorkflowSkill runtime CLI')\n .version('0.1.0');\n\nprogram\n .command('validate')\n .description('Validate one or more workflow SKILL.md files without executing')\n .argument('<files...>', 'Workflow files to validate')\n .action(validateCommand);\n\nprogram\n .command('run <file>')\n .description('Execute a workflow SKILL.md file')\n .option('-i, --input <json>', 'Workflow inputs as JSON string', '{}')\n .option('-l, --log-dir <dir>', 'Directory to write run logs', 'runs')\n .action(runCommand);\n\nprogram.parse();\n"],"mappings":"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"}