@solidactions/cli 1.11.0 → 1.12.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,174 @@
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
+ var __importDefault = (this && this.__importDefault) || function (mod) {
9
+ return (mod && mod.__esModule) ? mod : { "default": mod };
10
+ };
11
+ Object.defineProperty(exports, "__esModule", { value: true });
12
+ exports.parseSkillFile = parseSkillFile;
13
+ exports.readTopLevelReferences = readTopLevelReferences;
14
+ exports.skillPushWithConfig = skillPushWithConfig;
15
+ exports.skillPush = skillPush;
16
+ const fs_1 = __importDefault(require("fs"));
17
+ const path_1 = __importDefault(require("path"));
18
+ const chalk_1 = __importDefault(require("chalk"));
19
+ const js_yaml_1 = __importDefault(require("js-yaml"));
20
+ const api_1 = require("../utils/api");
21
+ const mcp_1 = require("../utils/mcp");
22
+ /**
23
+ * Parse a SKILL.md file that begins with a YAML frontmatter block.
24
+ *
25
+ * Returns { name, description, properties, body } where:
26
+ * - name and description come from the frontmatter
27
+ * - properties is the rest of the frontmatter (excluding name and description)
28
+ * - body is everything after the closing --- delimiter
29
+ *
30
+ * Throws with a descriptive message if the file is malformed or missing
31
+ * required fields.
32
+ */
33
+ function parseSkillFile(content) {
34
+ if (!content.startsWith('---')) {
35
+ throw new Error('SKILL.md must begin with a YAML frontmatter block (---).');
36
+ }
37
+ // Find the closing --- delimiter (must be on its own line after the opening ---)
38
+ const afterOpen = content.slice(3);
39
+ const closeIdx = afterOpen.indexOf('\n---');
40
+ if (closeIdx === -1) {
41
+ throw new Error('SKILL.md frontmatter is not closed (missing closing ---).');
42
+ }
43
+ const yamlBlock = afterOpen.slice(0, closeIdx);
44
+ // Body is everything after the closing ---\n
45
+ const body = afterOpen.slice(closeIdx + 4).replace(/^\n/, '');
46
+ let fm;
47
+ try {
48
+ fm = js_yaml_1.default.load(yamlBlock);
49
+ }
50
+ catch (e) {
51
+ throw new Error(`Failed to parse SKILL.md frontmatter YAML: ${e.message}`);
52
+ }
53
+ if (!fm || typeof fm !== 'object') {
54
+ throw new Error('SKILL.md frontmatter is empty or not a YAML object.');
55
+ }
56
+ if (!fm.name || typeof fm.name !== 'string') {
57
+ throw new Error('SKILL.md frontmatter must contain a "name" field (string).');
58
+ }
59
+ if (!fm.description || typeof fm.description !== 'string') {
60
+ throw new Error('SKILL.md frontmatter must contain a "description" field (string).');
61
+ }
62
+ const { name, description, ...rest } = fm;
63
+ // Remove 'type' if present — the server sets it
64
+ const { type: _type, ...properties } = rest;
65
+ return { name, description, properties, body };
66
+ }
67
+ /**
68
+ * Read top-level (non-recursive) files in dir, excluding SKILL.md.
69
+ * Returns a map of filename → utf8 contents.
70
+ */
71
+ function readTopLevelReferences(dir) {
72
+ const entries = fs_1.default.readdirSync(dir, { withFileTypes: true });
73
+ const references = {};
74
+ for (const entry of entries) {
75
+ if (!entry.isFile())
76
+ continue;
77
+ if (entry.name === 'SKILL.md')
78
+ continue;
79
+ const filePath = path_1.default.join(dir, entry.name);
80
+ references[entry.name] = fs_1.default.readFileSync(filePath, 'utf8');
81
+ }
82
+ return references;
83
+ }
84
+ /**
85
+ * Core implementation — accepts an injected config so tests can point at a
86
+ * stub server without touching the filesystem config.
87
+ */
88
+ async function skillPushWithConfig(dir, options, config) {
89
+ const absDir = path_1.default.resolve(dir);
90
+ // 1. Check directory exists
91
+ if (!fs_1.default.existsSync(absDir) || !fs_1.default.statSync(absDir).isDirectory()) {
92
+ process.stderr.write(chalk_1.default.red(`error: "${dir}" is not a directory.\n`));
93
+ process.exit(1);
94
+ }
95
+ // 2. Read SKILL.md
96
+ const skillMdPath = path_1.default.join(absDir, 'SKILL.md');
97
+ if (!fs_1.default.existsSync(skillMdPath)) {
98
+ process.stderr.write(chalk_1.default.red(`error: "${skillMdPath}" not found. The skill directory must contain a SKILL.md file.\n`));
99
+ process.exit(1);
100
+ }
101
+ const skillMdContent = fs_1.default.readFileSync(skillMdPath, 'utf8');
102
+ // 3. Parse frontmatter + body
103
+ let parsed;
104
+ try {
105
+ parsed = parseSkillFile(skillMdContent);
106
+ }
107
+ catch (e) {
108
+ process.stderr.write(chalk_1.default.red(`error: ${e.message}\n`));
109
+ process.exit(1);
110
+ }
111
+ const { name, description, properties, body } = parsed;
112
+ // 4. Read sibling reference files
113
+ const references = readTopLevelReferences(absDir);
114
+ // 5. Compose an idempotent upsert: try create; on name_collision, edit.
115
+ const isRole = !!options.role;
116
+ const tool = isRole ? 'roles' : 'skills';
117
+ const createArgs = isRole
118
+ ? { action: 'create_skill', role: options.role, name, description, body, properties, references }
119
+ : { action: 'create', name, description, body, properties, references };
120
+ let result;
121
+ try {
122
+ result = await (0, mcp_1.callCrewsTool)(config, tool, createArgs);
123
+ }
124
+ catch (e) {
125
+ process.stderr.write(chalk_1.default.red(`error: MCP request failed — ${e.message}\n`));
126
+ process.exit(1);
127
+ }
128
+ let didUpdate = false;
129
+ // 6. On name collision, switch to the edit path (the upsert). properties → properties_patch.
130
+ if (!result.ok && result.data?.code === 'name_collision') {
131
+ const editArgs = isRole
132
+ ? { action: 'edit_skill', role: options.role, name, description, body, properties_patch: properties, references }
133
+ : { action: 'edit', identifier: name, description, body, properties_patch: properties, references };
134
+ try {
135
+ result = await (0, mcp_1.callCrewsTool)(config, tool, editArgs);
136
+ }
137
+ catch (e) {
138
+ process.stderr.write(chalk_1.default.red(`error: MCP request failed — ${e.message}\n`));
139
+ process.exit(1);
140
+ }
141
+ didUpdate = true;
142
+ }
143
+ // 7. Handle result.
144
+ if (!result.ok) {
145
+ const errData = result.data;
146
+ const errCode = errData?.code ?? 'unknown_error';
147
+ const errMsg = errData?.message ?? 'MCP returned an error with no message';
148
+ process.stderr.write(chalk_1.default.red(`${errCode}: ${errMsg}\n`));
149
+ process.exit(1);
150
+ }
151
+ if (options.json) {
152
+ console.log(JSON.stringify(result.data));
153
+ process.exit(0);
154
+ }
155
+ const data = result.data;
156
+ if (didUpdate) {
157
+ // edit shape: {version_id, body_blob_sha}
158
+ console.log(chalk_1.default.green(`updated skill '${name}' (version ${data.version_id ?? '?'})`));
159
+ }
160
+ else {
161
+ // create shape: {skill_doc_id, folder_id, reference_doc_ids}
162
+ const skillDocId = data.skill_doc_id ?? data.doc_id ?? data.id ?? '?';
163
+ const refCount = Object.keys(data.reference_doc_ids ?? {}).length;
164
+ console.log(chalk_1.default.green(`created skill '${name}' (doc ${skillDocId}, ${refCount} refs)`));
165
+ }
166
+ process.exit(0);
167
+ }
168
+ /**
169
+ * Entry point called from index.ts.
170
+ */
171
+ async function skillPush(dir, options) {
172
+ const config = await (0, api_1.requireConfigWithWorkspace)();
173
+ await skillPushWithConfig(dir, options, config);
174
+ }
package/dist/index.js CHANGED
@@ -32,6 +32,7 @@ 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");
35
36
  const config_1 = require("./utils/config");
36
37
  // eslint-disable-next-line @typescript-eslint/no-var-requires
37
38
  const pkg = require('../package.json');
@@ -391,6 +392,19 @@ ai
391
392
  .option('--all', 'Install all available examples')
392
393
  .option('--overwrite', 'Overwrite existing examples without warning')
393
394
  .action((names, options) => { (0, ai_examples_1.aiExamples)(names, options); });
395
+ // =============================================================================
396
+ // skill <subcommand> (SOP surface — flat top-level noun per cli#34)
397
+ // =============================================================================
398
+ const skill = program.command('skill').description('Manage agent skills (crews SOP surface)');
399
+ skill
400
+ .command('push')
401
+ .description('Push a local skill folder into the library (create, or update if it already exists)')
402
+ .argument('<dir>', 'Path to the skill directory (must contain SKILL.md)')
403
+ .option('--role <name>', 'Scope the skill to a role instead of the shared library')
404
+ .option('--json', 'Output result as JSON')
405
+ .action(async (dir, options) => {
406
+ await (0, skill_push_1.skillPush)(dir, options);
407
+ });
394
408
  program.parseAsync().catch((err) => {
395
409
  console.error(chalk_1.default.red(err.message ?? String(err)));
396
410
  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.12.0",
4
4
  "description": "SolidActions CLI - Deploy and manage workflow automation",
5
5
  "main": "dist/index.js",
6
6
  "bin": {