@solidactions/cli 1.11.0 → 1.13.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.
@@ -0,0 +1,168 @@
1
+ "use strict";
2
+ /**
3
+ * solidactions role push <dir>
4
+ *
5
+ * Pushes a role definition to the crews library (idempotent upsert).
6
+ * Roles are flat peers of skills (per cli#34) — NOT nested under a role.
7
+ *
8
+ * The role def is a SKILL.md-shaped file (frontmatter name+description, body).
9
+ * Roles carry NO references.
10
+ *
11
+ * Supports --dry-run: pre-flight a read to detect existence; prints
12
+ * "[dry-run] would create/update '<name>'" without making any create/edit call.
13
+ *
14
+ * Upsert: create → on name_collision → edit.
15
+ * Roles use 'name' (NOT 'identifier') for read and edit.
16
+ */
17
+ var __importDefault = (this && this.__importDefault) || function (mod) {
18
+ return (mod && mod.__esModule) ? mod : { "default": mod };
19
+ };
20
+ Object.defineProperty(exports, "__esModule", { value: true });
21
+ exports.rolePushWithConfig = rolePushWithConfig;
22
+ exports.rolePush = rolePush;
23
+ const fs_1 = __importDefault(require("fs"));
24
+ const path_1 = __importDefault(require("path"));
25
+ const chalk_1 = __importDefault(require("chalk"));
26
+ const api_1 = require("../utils/api");
27
+ const mcp_1 = require("../utils/mcp");
28
+ const skill_push_1 = require("./skill-push");
29
+ /**
30
+ * Core implementation — accepts an injected config so tests can point at a
31
+ * stub server without touching the filesystem config.
32
+ *
33
+ * Reads <dir>/SKILL.md (errors if missing), parses it via parseSkillFile,
34
+ * then upserts the role via the crews 'roles' MCP tool.
35
+ *
36
+ * Roles carry NO references. The 'roles' tool uses 'name' for read and edit
37
+ * (NOT 'identifier'). Dry-run pre-flight uses {action:'read', name}.
38
+ */
39
+ async function rolePushWithConfig(dir, options, config) {
40
+ const absDir = path_1.default.resolve(dir);
41
+ // Verify the directory exists
42
+ if (!fs_1.default.existsSync(absDir) || !fs_1.default.statSync(absDir).isDirectory()) {
43
+ process.stderr.write(chalk_1.default.red(`error: "${dir}" is not a directory.\n`));
44
+ process.exit(1);
45
+ }
46
+ // Read and parse SKILL.md (roles use the same SKILL.md filename convention)
47
+ const skillMdPath = path_1.default.join(absDir, 'SKILL.md');
48
+ if (!fs_1.default.existsSync(skillMdPath)) {
49
+ process.stderr.write(chalk_1.default.red(`error: "${skillMdPath}" not found. The role directory must contain a SKILL.md file.\n`));
50
+ process.exit(1);
51
+ }
52
+ const skillMdContent = fs_1.default.readFileSync(skillMdPath, 'utf8');
53
+ let name;
54
+ let description;
55
+ let properties;
56
+ let body;
57
+ try {
58
+ ({ name, description, properties, body } = (0, skill_push_1.parseSkillFile)(skillMdContent));
59
+ }
60
+ catch (e) {
61
+ process.stderr.write(chalk_1.default.red(`error: ${e.message}\n`));
62
+ process.exit(1);
63
+ }
64
+ // --dry-run: pre-flight a read to detect existence; NO create or edit.
65
+ // Roles use {action:'read', name} (NOT identifier).
66
+ if (options.dryRun) {
67
+ let readResult;
68
+ try {
69
+ readResult = await (0, mcp_1.callCrewsTool)(config, 'roles', { action: 'read', name });
70
+ }
71
+ catch (e) {
72
+ process.stderr.write(chalk_1.default.red(`error: MCP request failed — ${e.message}\n`));
73
+ process.exit(1);
74
+ }
75
+ if (!readResult.ok) {
76
+ const code = readResult.data?.code;
77
+ if (code === 'role_not_found') {
78
+ if (options.json) {
79
+ console.log(JSON.stringify({}));
80
+ }
81
+ else {
82
+ console.log(chalk_1.default.cyan(`[dry-run] would create '${name}'`));
83
+ }
84
+ process.exit(0);
85
+ }
86
+ // Any other error is unexpected — surface it
87
+ const errMsg = readResult.data?.message ?? 'MCP returned an error with no message';
88
+ process.stderr.write(chalk_1.default.red(`error: ${code ?? 'unknown_error'}: ${errMsg}\n`));
89
+ process.exit(1);
90
+ }
91
+ // Read succeeded → role exists → would update
92
+ if (options.json) {
93
+ console.log(JSON.stringify({}));
94
+ }
95
+ else {
96
+ console.log(chalk_1.default.cyan(`[dry-run] would update '${name}'`));
97
+ }
98
+ process.exit(0);
99
+ }
100
+ // Upsert: try create first; on name_collision switch to edit.
101
+ // Roles carry NO references — do not send references key.
102
+ let result;
103
+ try {
104
+ result = await (0, mcp_1.callCrewsTool)(config, 'roles', {
105
+ action: 'create',
106
+ name,
107
+ description,
108
+ body,
109
+ properties,
110
+ });
111
+ }
112
+ catch (e) {
113
+ process.stderr.write(chalk_1.default.red(`error: MCP request failed — ${e.message}\n`));
114
+ process.exit(1);
115
+ }
116
+ // On name collision, switch to the edit path.
117
+ // Roles edit uses 'name' (NOT 'identifier'), and properties → properties_patch.
118
+ if (!result.ok && result.data?.code === 'name_collision') {
119
+ try {
120
+ result = await (0, mcp_1.callCrewsTool)(config, 'roles', {
121
+ action: 'edit',
122
+ name,
123
+ description,
124
+ body,
125
+ properties_patch: properties,
126
+ });
127
+ }
128
+ catch (e) {
129
+ process.stderr.write(chalk_1.default.red(`error: MCP request failed — ${e.message}\n`));
130
+ process.exit(1);
131
+ }
132
+ if (!result.ok) {
133
+ const errCode = result.data?.code ?? 'unknown_error';
134
+ const errMsg = result.data?.message ?? 'MCP returned an error with no message';
135
+ process.stderr.write(chalk_1.default.red(`error: ${errCode}: ${errMsg}\n`));
136
+ process.exit(1);
137
+ }
138
+ if (options.json) {
139
+ console.log(JSON.stringify(result.data ?? {}));
140
+ }
141
+ else {
142
+ console.log(chalk_1.default.green(`updated role '${name}'`));
143
+ }
144
+ process.exit(0);
145
+ }
146
+ if (!result.ok) {
147
+ const errCode = result.data?.code ?? 'unknown_error';
148
+ const errMsg = result.data?.message ?? 'MCP returned an error with no message';
149
+ process.stderr.write(chalk_1.default.red(`error: ${errCode}: ${errMsg}\n`));
150
+ process.exit(1);
151
+ }
152
+ if (options.json) {
153
+ console.log(JSON.stringify(result.data ?? {}));
154
+ }
155
+ else {
156
+ const data = result.data ?? {};
157
+ const roleDocId = data.role_doc_id ?? data.doc_id ?? data.id ?? '?';
158
+ console.log(chalk_1.default.green(`created role '${name}' (doc ${roleDocId})`));
159
+ }
160
+ process.exit(0);
161
+ }
162
+ /**
163
+ * Entry point called from index.ts.
164
+ */
165
+ async function rolePush(dir, options) {
166
+ const config = await (0, api_1.requireConfigWithWorkspace)();
167
+ await rolePushWithConfig(dir, options, config);
168
+ }
@@ -0,0 +1,54 @@
1
+ "use strict";
2
+ /**
3
+ * solidactions skill delete <name>
4
+ *
5
+ * Deletes a skill from the crews library. Admin only.
6
+ */
7
+ var __importDefault = (this && this.__importDefault) || function (mod) {
8
+ return (mod && mod.__esModule) ? mod : { "default": mod };
9
+ };
10
+ Object.defineProperty(exports, "__esModule", { value: true });
11
+ exports.skillDeleteWithConfig = skillDeleteWithConfig;
12
+ exports.skillDelete = skillDelete;
13
+ const chalk_1 = __importDefault(require("chalk"));
14
+ const api_1 = require("../utils/api");
15
+ const mcp_1 = require("../utils/mcp");
16
+ /**
17
+ * Core implementation — accepts an injected config so tests can point at a
18
+ * stub server without touching the filesystem config.
19
+ */
20
+ async function skillDeleteWithConfig(name, options, config) {
21
+ let result;
22
+ try {
23
+ result = await (0, mcp_1.callCrewsTool)(config, 'skills', { action: 'delete', identifier: name });
24
+ }
25
+ catch (e) {
26
+ process.stderr.write(chalk_1.default.red(`error: ${e.message}\n`));
27
+ process.exit(1);
28
+ }
29
+ if (!result.ok) {
30
+ const code = result.data?.code ?? 'unknown_error';
31
+ const message = result.data?.message ?? 'MCP returned an error with no message';
32
+ if (code === 'permission_denied') {
33
+ process.stderr.write(chalk_1.default.red(`error: permission denied — deleting a skill requires Admin access.\n`));
34
+ }
35
+ else {
36
+ process.stderr.write(chalk_1.default.red(`error: ${code}: ${message}\n`));
37
+ }
38
+ process.exit(1);
39
+ }
40
+ if (options.json) {
41
+ console.log(JSON.stringify(result.data));
42
+ process.exit(0);
43
+ }
44
+ const deleteToken = result.data?.delete_token ?? '';
45
+ console.log(chalk_1.default.green(`deleted skill '${name}'`) + (deleteToken ? chalk_1.default.gray(` (token: ${deleteToken})`) : ''));
46
+ process.exit(0);
47
+ }
48
+ /**
49
+ * Entry point called from index.ts.
50
+ */
51
+ async function skillDelete(name, options) {
52
+ const config = await (0, api_1.requireConfigWithWorkspace)();
53
+ await skillDeleteWithConfig(name, options, config);
54
+ }
@@ -0,0 +1,60 @@
1
+ "use strict";
2
+ /**
3
+ * solidactions skill list
4
+ *
5
+ * Lists skills in the crews library.
6
+ */
7
+ var __importDefault = (this && this.__importDefault) || function (mod) {
8
+ return (mod && mod.__esModule) ? mod : { "default": mod };
9
+ };
10
+ Object.defineProperty(exports, "__esModule", { value: true });
11
+ exports.skillListWithConfig = skillListWithConfig;
12
+ exports.skillList = skillList;
13
+ const chalk_1 = __importDefault(require("chalk"));
14
+ const api_1 = require("../utils/api");
15
+ const mcp_1 = require("../utils/mcp");
16
+ /**
17
+ * Core implementation — accepts an injected config so tests can point at a
18
+ * stub server without touching the filesystem config.
19
+ */
20
+ async function skillListWithConfig(options, config) {
21
+ const args = { action: 'list' };
22
+ if (options.limit !== undefined) {
23
+ args.limit = options.limit;
24
+ }
25
+ let result;
26
+ try {
27
+ result = await (0, mcp_1.callCrewsTool)(config, 'skills', args);
28
+ }
29
+ catch (e) {
30
+ process.stderr.write(chalk_1.default.red(`error: ${e.message}\n`));
31
+ process.exit(1);
32
+ }
33
+ if (!result.ok) {
34
+ const code = result.data?.code ?? 'unknown_error';
35
+ const message = result.data?.message ?? 'MCP returned an error with no message';
36
+ process.stderr.write(chalk_1.default.red(`error: ${code}: ${message}\n`));
37
+ process.exit(1);
38
+ }
39
+ const skills = result.data?.skills ?? [];
40
+ if (options.json) {
41
+ console.log(JSON.stringify(result.data));
42
+ process.exit(0);
43
+ }
44
+ if (skills.length === 0) {
45
+ console.log(chalk_1.default.gray('No skills found.'));
46
+ process.exit(0);
47
+ }
48
+ for (const skill of skills) {
49
+ const advertisedMarker = skill.catalog_advertised ? chalk_1.default.gray(' (advertised)') : '';
50
+ console.log(`${chalk_1.default.green(skill.name)} — ${chalk_1.default.gray(skill.description)}${advertisedMarker}`);
51
+ }
52
+ process.exit(0);
53
+ }
54
+ /**
55
+ * Entry point called from index.ts.
56
+ */
57
+ async function skillList(options) {
58
+ const config = await (0, api_1.requireConfigWithWorkspace)();
59
+ await skillListWithConfig(options, config);
60
+ }
@@ -0,0 +1,120 @@
1
+ "use strict";
2
+ /**
3
+ * solidactions skill pull <name> [dest]
4
+ *
5
+ * Fetches a skill from the crews library and reconstructs a local skill folder
6
+ * (the inverse of `skill push`): writes <dest>/SKILL.md + any bundled reference
7
+ * files under <dest>/. Default dest = ./<name>/.
8
+ *
9
+ * --json mode: prints the raw read result from the server and exits WITHOUT
10
+ * writing any files. This is useful for scripting/inspection. The default
11
+ * (no --json) writes the folder.
12
+ */
13
+ var __importDefault = (this && this.__importDefault) || function (mod) {
14
+ return (mod && mod.__esModule) ? mod : { "default": mod };
15
+ };
16
+ Object.defineProperty(exports, "__esModule", { value: true });
17
+ exports.skillPullWithConfig = skillPullWithConfig;
18
+ exports.skillPull = skillPull;
19
+ const fs_1 = __importDefault(require("fs"));
20
+ const path_1 = __importDefault(require("path"));
21
+ const chalk_1 = __importDefault(require("chalk"));
22
+ const js_yaml_1 = __importDefault(require("js-yaml"));
23
+ const api_1 = require("../utils/api");
24
+ const mcp_1 = require("../utils/mcp");
25
+ /**
26
+ * Core implementation — accepts an injected config so tests can point at a
27
+ * stub server without touching the filesystem config.
28
+ *
29
+ * Steps:
30
+ * 1. Call skills.read to fetch the skill from the library.
31
+ * 2. If --json: print the raw result, exit 0 (no file writes).
32
+ * 3. Otherwise: reconstruct SKILL.md from the returned `properties` (which
33
+ * contains name, description, and any extra props), OMITTING `type` (the
34
+ * server sets it; stripping mirrors what parseSkillFile does on push so a
35
+ * push→pull→push round-trip is idempotent). Write <dest>/SKILL.md, then
36
+ * write each reference file under <dest>/ with a path-traversal guard.
37
+ */
38
+ async function skillPullWithConfig(name, dest, options, config) {
39
+ // Call the crews MCP server for a skills.read
40
+ let result;
41
+ try {
42
+ result = await (0, mcp_1.callCrewsTool)(config, 'skills', { action: 'read', identifier: name });
43
+ }
44
+ catch (e) {
45
+ process.stderr.write(chalk_1.default.red(`error: MCP request failed — ${e.message}\n`));
46
+ process.exit(1);
47
+ }
48
+ if (!result.ok) {
49
+ const code = result.data?.code ?? 'unknown_error';
50
+ const message = result.data?.message ?? 'MCP returned an error with no message';
51
+ process.stderr.write(chalk_1.default.red(`error: ${code}: ${message}\n`));
52
+ process.exit(1);
53
+ }
54
+ const data = result.data;
55
+ // --json mode: print raw result and exit WITHOUT writing files.
56
+ // This is intentionally non-destructive: inspect the raw server payload
57
+ // without side-effects. File reconstruction is the default (no --json) mode.
58
+ if (options.json) {
59
+ console.log(JSON.stringify(data));
60
+ process.exit(0);
61
+ }
62
+ // Resolve the output directory (default: ./<name>/)
63
+ const out = dest ?? './' + name;
64
+ const absOut = path_1.default.resolve(out);
65
+ // Build the frontmatter object from `properties`, OMITTING `type`.
66
+ // The server stores a `type` field but it is stripped on push (parseSkillFile
67
+ // does `const { type: _type, ...properties } = rest`). We mirror that here
68
+ // so that pull → push is idempotent.
69
+ const { type: _type, name: propName, description: propDescription, ...extraProps } = data.properties;
70
+ // Reconstruct the full frontmatter: name + description (from properties)
71
+ // followed by all remaining extra props (minus type).
72
+ const frontmatterObj = {
73
+ name: propName,
74
+ description: propDescription,
75
+ ...extraProps,
76
+ };
77
+ // yaml.dump produces "key: value\n" lines; we wrap in ---\n...\n---\n
78
+ const yamlBlock = js_yaml_1.default.dump(frontmatterObj, { lineWidth: -1 });
79
+ const body = data.body ?? '';
80
+ // Ensure the body starts on its own line after the closing ---
81
+ const skillMdContent = `---\n${yamlBlock}---\n${body.startsWith('\n') ? body : '\n' + body}`;
82
+ // Create the output directory (and parents) if needed
83
+ fs_1.default.mkdirSync(absOut, { recursive: true });
84
+ // Write SKILL.md
85
+ fs_1.default.writeFileSync(path_1.default.join(absOut, 'SKILL.md'), skillMdContent, 'utf8');
86
+ // Write reference files, guarding against path traversal
87
+ const reference = data.reference ?? {};
88
+ const absOutNorm = path_1.default.resolve(absOut) + path_1.default.sep;
89
+ let writtenCount = 0;
90
+ let skippedCount = 0;
91
+ for (const [key, content] of Object.entries(reference)) {
92
+ // Guard 1: reject absolute paths
93
+ if (path_1.default.isAbsolute(key)) {
94
+ process.stderr.write(chalk_1.default.yellow(`warn: skipping unsafe reference key (absolute path): ${key}\n`));
95
+ skippedCount++;
96
+ continue;
97
+ }
98
+ // Guard 2: reject path traversal (any key that resolves outside absOut)
99
+ const target = path_1.default.resolve(absOut, key);
100
+ if (!target.startsWith(absOutNorm)) {
101
+ process.stderr.write(chalk_1.default.yellow(`warn: skipping unsafe reference key (traversal): ${key}\n`));
102
+ skippedCount++;
103
+ continue;
104
+ }
105
+ // Safe: create parent dirs and write the file
106
+ fs_1.default.mkdirSync(path_1.default.dirname(target), { recursive: true });
107
+ fs_1.default.writeFileSync(target, content, 'utf8');
108
+ writtenCount++;
109
+ }
110
+ const refSummary = writtenCount === 1 ? '1 reference' : `${writtenCount} references`;
111
+ console.log(chalk_1.default.green(`pulled skill '${name}' → ${out} (${refSummary})`));
112
+ process.exit(0);
113
+ }
114
+ /**
115
+ * Entry point called from index.ts.
116
+ */
117
+ async function skillPull(name, dest, options) {
118
+ const config = await (0, api_1.requireConfigWithWorkspace)();
119
+ await skillPullWithConfig(name, dest, options, config);
120
+ }
@@ -0,0 +1,417 @@
1
+ "use strict";
2
+ /**
3
+ * solidactions skill push <dir>
4
+ *
5
+ * Pushes a local skill folder into the crews library via the crews MCP server.
6
+ * Idempotent upsert: create, or update on name collision.
7
+ *
8
+ * Supports two modes:
9
+ * - Single-skill: <dir>/SKILL.md exists → push that one skill.
10
+ * - Plugin: <dir>/skills/<name>/SKILL.md and/or <dir>/commands/<name>.md → push all.
11
+ */
12
+ var __importDefault = (this && this.__importDefault) || function (mod) {
13
+ return (mod && mod.__esModule) ? mod : { "default": mod };
14
+ };
15
+ Object.defineProperty(exports, "__esModule", { value: true });
16
+ exports.parseSkillFile = parseSkillFile;
17
+ exports.readReferences = readReferences;
18
+ exports.readSkillDir = readSkillDir;
19
+ exports.parseCommandFile = parseCommandFile;
20
+ exports.pushParsedSkill = pushParsedSkill;
21
+ exports.skillPushWithConfig = skillPushWithConfig;
22
+ exports.skillPush = skillPush;
23
+ const fs_1 = __importDefault(require("fs"));
24
+ const path_1 = __importDefault(require("path"));
25
+ const chalk_1 = __importDefault(require("chalk"));
26
+ const js_yaml_1 = __importDefault(require("js-yaml"));
27
+ const api_1 = require("../utils/api");
28
+ const mcp_1 = require("../utils/mcp");
29
+ /**
30
+ * Parse a SKILL.md file that begins with a YAML frontmatter block.
31
+ *
32
+ * Returns { name, description, properties, body } where:
33
+ * - name and description come from the frontmatter
34
+ * - properties is the rest of the frontmatter (excluding name and description)
35
+ * - body is everything after the closing --- delimiter
36
+ *
37
+ * Throws with a descriptive message if the file is malformed or missing
38
+ * required fields.
39
+ */
40
+ function parseSkillFile(content) {
41
+ if (!content.startsWith('---')) {
42
+ throw new Error('SKILL.md must begin with a YAML frontmatter block (---).');
43
+ }
44
+ // Find the closing --- delimiter (must be on its own line after the opening ---)
45
+ const afterOpen = content.slice(3);
46
+ const closeIdx = afterOpen.indexOf('\n---');
47
+ if (closeIdx === -1) {
48
+ throw new Error('SKILL.md frontmatter is not closed (missing closing ---).');
49
+ }
50
+ const yamlBlock = afterOpen.slice(0, closeIdx);
51
+ // Body is everything after the closing ---\n
52
+ const body = afterOpen.slice(closeIdx + 4).replace(/^\n/, '');
53
+ let fm;
54
+ try {
55
+ fm = js_yaml_1.default.load(yamlBlock);
56
+ }
57
+ catch (e) {
58
+ throw new Error(`Failed to parse SKILL.md frontmatter YAML: ${e.message}`);
59
+ }
60
+ if (!fm || typeof fm !== 'object') {
61
+ throw new Error('SKILL.md frontmatter is empty or not a YAML object.');
62
+ }
63
+ if (!fm.name || typeof fm.name !== 'string') {
64
+ throw new Error('SKILL.md frontmatter must contain a "name" field (string).');
65
+ }
66
+ if (!fm.description || typeof fm.description !== 'string') {
67
+ throw new Error('SKILL.md frontmatter must contain a "description" field (string).');
68
+ }
69
+ const { name, description, ...rest } = fm;
70
+ // Remove 'type' if present — the server sets it
71
+ const { type: _type, ...properties } = rest;
72
+ return { name, description, properties, body };
73
+ }
74
+ /**
75
+ * Recursively read every bundled file under a skill dir, excluding the
76
+ * top-level SKILL.md. Returns a map of reference-key → utf8 contents.
77
+ *
78
+ * Keys are the file's POSIX path relative to the skill dir: top-level files
79
+ * keep a bare-filename key (e.g. "helper.py"), and files in subfolders keep
80
+ * their relative path (e.g. "references/member-roles.md") — matching how
81
+ * SKILL.md cites them, so bundled reference docs land complete (#247).
82
+ */
83
+ function readReferences(dir) {
84
+ const references = {};
85
+ const walk = (current) => {
86
+ for (const entry of fs_1.default.readdirSync(current, { withFileTypes: true })) {
87
+ const abs = path_1.default.join(current, entry.name);
88
+ if (entry.isDirectory()) {
89
+ walk(abs);
90
+ continue;
91
+ }
92
+ if (!entry.isFile())
93
+ continue;
94
+ // POSIX-normalised path relative to the skill dir, used as the key.
95
+ const key = path_1.default.relative(dir, abs).split(path_1.default.sep).join('/');
96
+ if (key === 'SKILL.md')
97
+ continue; // exclude only the top-level skill file
98
+ references[key] = fs_1.default.readFileSync(abs, 'utf8');
99
+ }
100
+ };
101
+ walk(dir);
102
+ return references;
103
+ }
104
+ /**
105
+ * Read a skill directory: validates the dir exists, reads SKILL.md, parses it,
106
+ * and reads bundled reference files. Returns the full payload for pushParsedSkill.
107
+ *
108
+ * Throws with a descriptive message on any filesystem or parse error.
109
+ */
110
+ function readSkillDir(dir) {
111
+ const absDir = path_1.default.resolve(dir);
112
+ if (!fs_1.default.existsSync(absDir) || !fs_1.default.statSync(absDir).isDirectory()) {
113
+ throw new Error(`"${dir}" is not a directory.`);
114
+ }
115
+ const skillMdPath = path_1.default.join(absDir, 'SKILL.md');
116
+ if (!fs_1.default.existsSync(skillMdPath)) {
117
+ throw new Error(`"${skillMdPath}" not found. The skill directory must contain a SKILL.md file.`);
118
+ }
119
+ const skillMdContent = fs_1.default.readFileSync(skillMdPath, 'utf8');
120
+ const { name, description, properties, body } = parseSkillFile(skillMdContent);
121
+ const references = readReferences(absDir);
122
+ return { name, description, properties, body, references };
123
+ }
124
+ /**
125
+ * Parse a commands/<name>.md file (plugin command format).
126
+ *
127
+ * Derives the skill name from the filename (kebab-case: lowercase, spaces and
128
+ * underscores replaced with hyphens). The frontmatter must contain `description`;
129
+ * `name`, `allowed-tools`, and `argument-hint` are not carried forward.
130
+ *
131
+ * Returns a SkillPayload with properties={} and references={}.
132
+ * Throws with a descriptive message if description is missing or frontmatter malformed.
133
+ */
134
+ function parseCommandFile(filename, content) {
135
+ // Derive name from filename (basename without extension, converted to kebab-case)
136
+ const base = path_1.default.basename(filename, '.md');
137
+ const name = base.toLowerCase().replace(/[\s_]+/g, '-');
138
+ if (!content.startsWith('---')) {
139
+ throw new Error(`${filename}: command file must begin with a YAML frontmatter block (---).`);
140
+ }
141
+ const afterOpen = content.slice(3);
142
+ const closeIdx = afterOpen.indexOf('\n---');
143
+ if (closeIdx === -1) {
144
+ throw new Error(`${filename}: command file frontmatter is not closed (missing closing ---).`);
145
+ }
146
+ const yamlBlock = afterOpen.slice(0, closeIdx);
147
+ const body = afterOpen.slice(closeIdx + 4).replace(/^\n/, '');
148
+ let fm;
149
+ try {
150
+ fm = js_yaml_1.default.load(yamlBlock);
151
+ }
152
+ catch (e) {
153
+ throw new Error(`${filename}: failed to parse command file frontmatter YAML: ${e.message}`);
154
+ }
155
+ if (!fm || typeof fm !== 'object') {
156
+ throw new Error(`${filename}: command file frontmatter is empty or not a YAML object.`);
157
+ }
158
+ if (!fm.description || typeof fm.description !== 'string') {
159
+ throw new Error(`${filename}: command file frontmatter must contain a "description" field (string).`);
160
+ }
161
+ return {
162
+ name,
163
+ description: fm.description,
164
+ properties: {},
165
+ body,
166
+ references: {},
167
+ };
168
+ }
169
+ /**
170
+ * Core upsert: try create; on name_collision, switch to edit.
171
+ * Returns a result object — does NOT call process.exit and does NOT print.
172
+ * Throws on non-collision MCP errors.
173
+ *
174
+ * When options.dryRun is true, performs only a skills.read pre-flight and
175
+ * returns {status:'would-create'} or {status:'would-update'} without any
176
+ * create or edit call.
177
+ */
178
+ async function pushParsedSkill(payload, options, config) {
179
+ const { name, description, properties, body, references } = payload;
180
+ const isRole = !!options.role;
181
+ const tool = isRole ? 'roles' : 'skills';
182
+ // --dry-run: pre-flight a read to detect existence; make NO create/edit calls.
183
+ if (options.dryRun) {
184
+ // Role-scoped skills are read via the roles tool's read_skill {role, name};
185
+ // shared skills via the skills tool's read {identifier}. Both return
186
+ // `skill_not_found` when absent. (read does NOT accept `identifier` on the
187
+ // roles tool, so the action/args must branch on isRole.)
188
+ const readArgs = isRole
189
+ ? { action: 'read_skill', role: options.role, name }
190
+ : { action: 'read', identifier: name };
191
+ let readResult;
192
+ try {
193
+ readResult = await (0, mcp_1.callCrewsTool)(config, tool, readArgs);
194
+ }
195
+ catch (e) {
196
+ throw new Error(`MCP request failed — ${e.message}`);
197
+ }
198
+ if (!readResult.ok) {
199
+ const code = readResult.data?.code;
200
+ if (code === 'skill_not_found') {
201
+ return { status: 'would-create', name, data: {} };
202
+ }
203
+ // Any other error is unexpected — surface it
204
+ const errMsg = readResult.data?.message ?? 'MCP returned an error with no message';
205
+ throw new Error(`${code ?? 'unknown_error'}: ${errMsg}`);
206
+ }
207
+ // Read succeeded → skill exists → would update
208
+ return { status: 'would-update', name, data: {} };
209
+ }
210
+ const createArgs = isRole
211
+ ? { action: 'create_skill', role: options.role, name, description, body, properties, references }
212
+ : { action: 'create', name, description, body, properties, references };
213
+ let result;
214
+ try {
215
+ result = await (0, mcp_1.callCrewsTool)(config, tool, createArgs);
216
+ }
217
+ catch (e) {
218
+ throw new Error(`MCP request failed — ${e.message}`);
219
+ }
220
+ // On name collision, switch to the edit path. properties → properties_patch.
221
+ if (!result.ok && result.data?.code === 'name_collision') {
222
+ const editArgs = isRole
223
+ ? { action: 'edit_skill', role: options.role, name, description, body, properties_patch: properties, references }
224
+ : { action: 'edit', identifier: name, description, body, properties_patch: properties, references };
225
+ try {
226
+ result = await (0, mcp_1.callCrewsTool)(config, tool, editArgs);
227
+ }
228
+ catch (e) {
229
+ throw new Error(`MCP request failed — ${e.message}`);
230
+ }
231
+ if (!result.ok) {
232
+ const errData = result.data;
233
+ const errCode = errData?.code ?? 'unknown_error';
234
+ const errMsg = errData?.message ?? 'MCP returned an error with no message';
235
+ throw new Error(`${errCode}: ${errMsg}`);
236
+ }
237
+ return { status: 'updated', name, data: result.data ?? {} };
238
+ }
239
+ if (!result.ok) {
240
+ const errData = result.data;
241
+ const errCode = errData?.code ?? 'unknown_error';
242
+ const errMsg = errData?.message ?? 'MCP returned an error with no message';
243
+ throw new Error(`${errCode}: ${errMsg}`);
244
+ }
245
+ return { status: 'created', name, data: result.data ?? {} };
246
+ }
247
+ /**
248
+ * Print a single skill push result line (shared between single and plugin modes).
249
+ */
250
+ function printPushResult(result, options) {
251
+ if (options.json) {
252
+ console.log(JSON.stringify(result.data));
253
+ return;
254
+ }
255
+ const { status, name, data } = result;
256
+ if (status === 'would-create') {
257
+ console.log(chalk_1.default.cyan(`[dry-run] would create '${name}'`));
258
+ }
259
+ else if (status === 'would-update') {
260
+ console.log(chalk_1.default.cyan(`[dry-run] would update '${name}'`));
261
+ }
262
+ else if (status === 'updated') {
263
+ console.log(chalk_1.default.green(`updated skill '${name}' (version ${data.version_id ?? '?'})`));
264
+ }
265
+ else {
266
+ const skillDocId = data.skill_doc_id ?? data.doc_id ?? data.id ?? '?';
267
+ const refCount = Object.keys(data.reference_doc_ids ?? {}).length;
268
+ console.log(chalk_1.default.green(`created skill '${name}' (doc ${skillDocId}, ${refCount} refs)`));
269
+ }
270
+ }
271
+ /**
272
+ * Core implementation — accepts an injected config so tests can point at a
273
+ * stub server without touching the filesystem config.
274
+ *
275
+ * Routes between single-skill mode (top-level SKILL.md) and plugin mode
276
+ * (skills/ + commands/ subdirectories) based on directory structure.
277
+ */
278
+ async function skillPushWithConfig(dir, options, config) {
279
+ const absDir = path_1.default.resolve(dir);
280
+ if (!fs_1.default.existsSync(absDir) || !fs_1.default.statSync(absDir).isDirectory()) {
281
+ process.stderr.write(chalk_1.default.red(`error: "${dir}" is not a directory.\n`));
282
+ process.exit(1);
283
+ }
284
+ const topLevelSkillMd = path_1.default.join(absDir, 'SKILL.md');
285
+ // -------------------------------------------------------------------------
286
+ // Single-skill mode: <dir>/SKILL.md exists
287
+ // -------------------------------------------------------------------------
288
+ if (fs_1.default.existsSync(topLevelSkillMd)) {
289
+ let payload;
290
+ try {
291
+ payload = readSkillDir(dir);
292
+ }
293
+ catch (e) {
294
+ process.stderr.write(chalk_1.default.red(`error: ${e.message}\n`));
295
+ process.exit(1);
296
+ }
297
+ let pushResult;
298
+ try {
299
+ pushResult = await pushParsedSkill(payload, options, config);
300
+ }
301
+ catch (e) {
302
+ process.stderr.write(chalk_1.default.red(`error: ${e.message}\n`));
303
+ process.exit(1);
304
+ }
305
+ if (options.json) {
306
+ console.log(JSON.stringify(pushResult.data));
307
+ }
308
+ else {
309
+ printPushResult(pushResult, options);
310
+ }
311
+ process.exit(0);
312
+ }
313
+ // -------------------------------------------------------------------------
314
+ // Plugin mode: push skills/<name>/SKILL.md + commands/<name>.md
315
+ // -------------------------------------------------------------------------
316
+ const payloads = [];
317
+ // One-level glob: skills/*/SKILL.md
318
+ const skillsDir = path_1.default.join(absDir, 'skills');
319
+ if (fs_1.default.existsSync(skillsDir) && fs_1.default.statSync(skillsDir).isDirectory()) {
320
+ for (const entry of fs_1.default.readdirSync(skillsDir, { withFileTypes: true })) {
321
+ if (!entry.isDirectory())
322
+ continue;
323
+ const skillSubdir = path_1.default.join(skillsDir, entry.name);
324
+ const skillMd = path_1.default.join(skillSubdir, 'SKILL.md');
325
+ if (!fs_1.default.existsSync(skillMd))
326
+ continue;
327
+ try {
328
+ payloads.push(readSkillDir(skillSubdir));
329
+ }
330
+ catch (e) {
331
+ process.stderr.write(chalk_1.default.red(`error: skills/${entry.name}/SKILL.md: ${e.message}\n`));
332
+ process.exit(1);
333
+ }
334
+ }
335
+ }
336
+ // One-level glob: commands/*.md
337
+ const commandsDir = path_1.default.join(absDir, 'commands');
338
+ if (fs_1.default.existsSync(commandsDir) && fs_1.default.statSync(commandsDir).isDirectory()) {
339
+ for (const entry of fs_1.default.readdirSync(commandsDir, { withFileTypes: true })) {
340
+ if (!entry.isFile())
341
+ continue;
342
+ if (!entry.name.endsWith('.md'))
343
+ continue;
344
+ const cmdPath = path_1.default.join(commandsDir, entry.name);
345
+ const content = fs_1.default.readFileSync(cmdPath, 'utf8');
346
+ try {
347
+ payloads.push(parseCommandFile(entry.name, content));
348
+ }
349
+ catch (e) {
350
+ process.stderr.write(chalk_1.default.red(`error: commands/${entry.name}: ${e.message}\n`));
351
+ process.exit(1);
352
+ }
353
+ }
354
+ }
355
+ if (payloads.length === 0) {
356
+ process.stderr.write(chalk_1.default.red(`error: no skills/ or commands/ under "${dir}" — nothing to push.\n`));
357
+ process.exit(1);
358
+ }
359
+ // Detect duplicate names BEFORE any push
360
+ const seen = new Map();
361
+ for (const p of payloads) {
362
+ seen.set(p.name, (seen.get(p.name) ?? 0) + 1);
363
+ }
364
+ const conflicts = [...seen.entries()].filter(([, count]) => count > 1).map(([name]) => name);
365
+ if (conflicts.length > 0) {
366
+ process.stderr.write(chalk_1.default.red(`error: duplicate skill names detected — aborting before any push.\n`) +
367
+ chalk_1.default.red(` conflicts: ${conflicts.join(', ')}\n`) +
368
+ chalk_1.default.red(` Rename the conflicting skills/commands so all names are unique.\n`));
369
+ process.exit(1);
370
+ }
371
+ // Push each skill, print a summary line per item
372
+ let createdCount = 0;
373
+ let updatedCount = 0;
374
+ for (const payload of payloads) {
375
+ let pushResult;
376
+ try {
377
+ pushResult = await pushParsedSkill(payload, options, config);
378
+ }
379
+ catch (e) {
380
+ process.stderr.write(chalk_1.default.red(`error: ${payload.name}: ${e.message}\n`));
381
+ process.exit(1);
382
+ }
383
+ printPushResult(pushResult, options);
384
+ if (pushResult.status === 'created' || pushResult.status === 'would-create') {
385
+ createdCount++;
386
+ }
387
+ else {
388
+ updatedCount++;
389
+ }
390
+ }
391
+ // Final tally
392
+ if (!options.json) {
393
+ const parts = [];
394
+ if (options.dryRun) {
395
+ if (createdCount > 0)
396
+ parts.push(`${createdCount} would create`);
397
+ if (updatedCount > 0)
398
+ parts.push(`${updatedCount} would update`);
399
+ console.log(chalk_1.default.cyan(`\n[dry-run] ${payloads.length} skill(s) (${parts.join(', ')})`));
400
+ }
401
+ else {
402
+ if (createdCount > 0)
403
+ parts.push(`${createdCount} created`);
404
+ if (updatedCount > 0)
405
+ parts.push(`${updatedCount} updated`);
406
+ console.log(chalk_1.default.cyan(`\ndone: ${payloads.length} skill(s) pushed (${parts.join(', ')})`));
407
+ }
408
+ }
409
+ process.exit(0);
410
+ }
411
+ /**
412
+ * Entry point called from index.ts.
413
+ */
414
+ async function skillPush(dir, options) {
415
+ const config = await (0, api_1.requireConfigWithWorkspace)();
416
+ await skillPushWithConfig(dir, options, config);
417
+ }
@@ -0,0 +1,59 @@
1
+ "use strict";
2
+ /**
3
+ * solidactions skill view <name>
4
+ *
5
+ * Shows the full details of a single skill from the crews library.
6
+ */
7
+ var __importDefault = (this && this.__importDefault) || function (mod) {
8
+ return (mod && mod.__esModule) ? mod : { "default": mod };
9
+ };
10
+ Object.defineProperty(exports, "__esModule", { value: true });
11
+ exports.skillViewWithConfig = skillViewWithConfig;
12
+ exports.skillView = skillView;
13
+ const chalk_1 = __importDefault(require("chalk"));
14
+ const api_1 = require("../utils/api");
15
+ const mcp_1 = require("../utils/mcp");
16
+ /**
17
+ * Core implementation — accepts an injected config so tests can point at a
18
+ * stub server without touching the filesystem config.
19
+ */
20
+ async function skillViewWithConfig(name, options, config) {
21
+ let result;
22
+ try {
23
+ result = await (0, mcp_1.callCrewsTool)(config, 'skills', { action: 'read', identifier: name });
24
+ }
25
+ catch (e) {
26
+ process.stderr.write(chalk_1.default.red(`error: ${e.message}\n`));
27
+ process.exit(1);
28
+ }
29
+ if (!result.ok) {
30
+ const code = result.data?.code ?? 'unknown_error';
31
+ const message = result.data?.message ?? 'MCP returned an error with no message';
32
+ process.stderr.write(chalk_1.default.red(`error: ${code}: ${message}\n`));
33
+ process.exit(1);
34
+ }
35
+ if (options.json) {
36
+ console.log(JSON.stringify(result.data));
37
+ process.exit(0);
38
+ }
39
+ const data = result.data ?? {};
40
+ const identifier = data.identifier ?? name;
41
+ const description = data.properties?.description ?? '';
42
+ const body = data.body ?? '';
43
+ console.log(chalk_1.default.green(identifier));
44
+ if (description) {
45
+ console.log(chalk_1.default.gray(description));
46
+ }
47
+ if (body) {
48
+ console.log('');
49
+ console.log(body);
50
+ }
51
+ process.exit(0);
52
+ }
53
+ /**
54
+ * Entry point called from index.ts.
55
+ */
56
+ async function skillView(name, options) {
57
+ const config = await (0, api_1.requireConfigWithWorkspace)();
58
+ await skillViewWithConfig(name, options, config);
59
+ }
package/dist/index.js CHANGED
@@ -32,6 +32,12 @@ const oauth_actions_search_1 = require("./commands/oauth-actions-search");
32
32
  const oauth_actions_list_1 = require("./commands/oauth-actions-list");
33
33
  const oauth_actions_show_1 = require("./commands/oauth-actions-show");
34
34
  const workspaces_1 = require("./commands/workspaces");
35
+ const skill_push_1 = require("./commands/skill-push");
36
+ const skill_pull_1 = require("./commands/skill-pull");
37
+ const skill_list_1 = require("./commands/skill-list");
38
+ const skill_view_1 = require("./commands/skill-view");
39
+ const skill_delete_1 = require("./commands/skill-delete");
40
+ const role_push_1 = require("./commands/role-push");
35
41
  const config_1 = require("./utils/config");
36
42
  // eslint-disable-next-line @typescript-eslint/no-var-requires
37
43
  const pkg = require('../package.json');
@@ -391,6 +397,63 @@ ai
391
397
  .option('--all', 'Install all available examples')
392
398
  .option('--overwrite', 'Overwrite existing examples without warning')
393
399
  .action((names, options) => { (0, ai_examples_1.aiExamples)(names, options); });
400
+ // =============================================================================
401
+ // skill <subcommand> (SOP surface — flat top-level noun per cli#34)
402
+ // =============================================================================
403
+ const skill = program.command('skill').description('Manage agent skills (crews SOP surface)');
404
+ skill
405
+ .command('push')
406
+ .description('Push a local skill folder into the library (create, or update if it already exists)')
407
+ .argument('<dir>', 'Path to the skill directory (must contain SKILL.md)')
408
+ .option('--role <name>', 'Scope the skill to a role instead of the shared library')
409
+ .option('--json', 'Output result as JSON')
410
+ .option('--dry-run', 'Preview create vs update without writing')
411
+ .action(async (dir, options) => {
412
+ await (0, skill_push_1.skillPush)(dir, options);
413
+ });
414
+ skill
415
+ .command('pull')
416
+ .description('Fetch a skill from the library to a local folder for editing (inverse of push)')
417
+ .argument('<name>', 'Skill name or identifier')
418
+ .argument('[dest]', 'Destination directory (defaults to ./<name>/)')
419
+ .option('--json', 'Output raw read result as JSON (no file writes)')
420
+ .action(async (name, dest, options) => {
421
+ await (0, skill_pull_1.skillPull)(name, dest, options);
422
+ });
423
+ skill
424
+ .command('list')
425
+ .description('List skills in the library')
426
+ .option('--json', 'Output as JSON')
427
+ .option('--limit <n>', 'Maximum number of skills to return', (v) => parseInt(v, 10))
428
+ .action(async (options) => {
429
+ await (0, skill_list_1.skillList)(options);
430
+ });
431
+ skill
432
+ .command('view <name>')
433
+ .description('Show one skill')
434
+ .option('--json', 'Output as JSON')
435
+ .action(async (name, options) => {
436
+ await (0, skill_view_1.skillView)(name, options);
437
+ });
438
+ skill
439
+ .command('delete <name>')
440
+ .description('Delete a skill (Admin only)')
441
+ .option('--json', 'Output as JSON')
442
+ .action(async (name, options) => {
443
+ await (0, skill_delete_1.skillDelete)(name, options);
444
+ });
445
+ // =============================================================================
446
+ // role <subcommand> (SOP surface — flat peer of skill, per cli#34)
447
+ // =============================================================================
448
+ const role = program.command('role').description('Manage roles (crews SOP surface)');
449
+ role
450
+ .command('push <dir>')
451
+ .description('Push a role definition (create or update)')
452
+ .option('--dry-run', 'Preview create vs update without writing')
453
+ .option('--json', 'Output result as JSON')
454
+ .action(async (dir, options) => {
455
+ await (0, role_push_1.rolePush)(dir, options);
456
+ });
394
457
  program.parseAsync().catch((err) => {
395
458
  console.error(chalk_1.default.red(err.message ?? String(err)));
396
459
  process.exit(1);
@@ -0,0 +1,117 @@
1
+ "use strict";
2
+ /**
3
+ * Minimal MCP client for the SolidActions crews MCP server.
4
+ *
5
+ * Sends a single stateless JSON-RPC tools/call POST. No initialize handshake
6
+ * required — the streamable-HTTP transport at /mcp/crews accepts single
7
+ * stateless POSTs (confirmed live).
8
+ */
9
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ var desc = Object.getOwnPropertyDescriptor(m, k);
12
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
13
+ desc = { enumerable: true, get: function() { return m[k]; } };
14
+ }
15
+ Object.defineProperty(o, k2, desc);
16
+ }) : (function(o, m, k, k2) {
17
+ if (k2 === undefined) k2 = k;
18
+ o[k2] = m[k];
19
+ }));
20
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
21
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
22
+ }) : function(o, v) {
23
+ o["default"] = v;
24
+ });
25
+ var __importStar = (this && this.__importStar) || (function () {
26
+ var ownKeys = function(o) {
27
+ ownKeys = Object.getOwnPropertyNames || function (o) {
28
+ var ar = [];
29
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
30
+ return ar;
31
+ };
32
+ return ownKeys(o);
33
+ };
34
+ return function (mod) {
35
+ if (mod && mod.__esModule) return mod;
36
+ var result = {};
37
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
38
+ __setModuleDefault(result, mod);
39
+ return result;
40
+ };
41
+ })();
42
+ Object.defineProperty(exports, "__esModule", { value: true });
43
+ exports.callCrewsTool = callCrewsTool;
44
+ const http = __importStar(require("http"));
45
+ const https = __importStar(require("https"));
46
+ const url_1 = require("url");
47
+ const api_1 = require("./api");
48
+ /**
49
+ * Call a single MCP tool on /mcp/crews.
50
+ *
51
+ * Returns { ok: true, data: <success shape> } or { ok: false, data: { code, message } }.
52
+ */
53
+ async function callCrewsTool(config, toolName, args) {
54
+ const baseHeaders = (0, api_1.getApiHeaders)(config, 'application/json');
55
+ // Override Accept to include text/event-stream for streamable-HTTP transport
56
+ const headers = {
57
+ ...baseHeaders,
58
+ 'Accept': 'application/json, text/event-stream',
59
+ };
60
+ const body = JSON.stringify({
61
+ jsonrpc: '2.0',
62
+ id: 1,
63
+ method: 'tools/call',
64
+ params: {
65
+ name: toolName,
66
+ arguments: args,
67
+ },
68
+ });
69
+ const parsed = new url_1.URL(`${config.host}/mcp/crews`);
70
+ const isHttps = parsed.protocol === 'https:';
71
+ const transport = isHttps ? https : http;
72
+ const responseData = await new Promise((resolve, reject) => {
73
+ const options = {
74
+ hostname: parsed.hostname,
75
+ port: parsed.port || (isHttps ? 443 : 80),
76
+ path: parsed.pathname + (parsed.search || ''),
77
+ method: 'POST',
78
+ headers: {
79
+ ...headers,
80
+ 'Content-Length': Buffer.byteLength(body),
81
+ },
82
+ };
83
+ const req = transport.request(options, (res) => {
84
+ let raw = '';
85
+ res.on('data', (chunk) => { raw += chunk; });
86
+ res.on('end', () => {
87
+ if (res.statusCode && res.statusCode >= 400) {
88
+ reject(new Error(`MCP request failed with HTTP ${res.statusCode}: ${raw}`));
89
+ }
90
+ else {
91
+ resolve(raw);
92
+ }
93
+ });
94
+ });
95
+ req.on('error', reject);
96
+ req.write(body);
97
+ req.end();
98
+ });
99
+ let parsed2;
100
+ try {
101
+ parsed2 = JSON.parse(responseData);
102
+ }
103
+ catch {
104
+ throw new Error(`MCP server returned non-JSON response: ${responseData}`);
105
+ }
106
+ const result = parsed2?.result;
107
+ const isError = result?.isError === true;
108
+ const textContent = result?.content?.[0]?.text ?? '{}';
109
+ let toolData;
110
+ try {
111
+ toolData = JSON.parse(textContent);
112
+ }
113
+ catch {
114
+ throw new Error(`MCP tool result content is not valid JSON: ${textContent}`);
115
+ }
116
+ return { ok: !isError, data: toolData };
117
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@solidactions/cli",
3
- "version": "1.11.0",
3
+ "version": "1.13.0",
4
4
  "description": "SolidActions CLI - Deploy and manage workflow automation",
5
5
  "main": "dist/index.js",
6
6
  "bin": {