@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.
- package/dist/commands/skill-push.js +174 -0
- package/dist/index.js +14 -0
- package/dist/utils/mcp.js +117 -0
- package/package.json +1 -1
|
@@ -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
|
+
}
|