@solidactions/cli 1.12.0 → 1.14.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/README.md +18 -0
- package/dist/commands/role-push.js +168 -0
- package/dist/commands/run-list.js +32 -4
- package/dist/commands/skill-delete.js +54 -0
- package/dist/commands/skill-list.js +60 -0
- package/dist/commands/skill-pull.js +120 -0
- package/dist/commands/skill-push.js +289 -46
- package/dist/commands/skill-view.js +59 -0
- package/dist/index.js +49 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -118,6 +118,24 @@ Use `solidactions <command> --help` for full flag details on any command.
|
|
|
118
118
|
|---------|-----------|-------------|
|
|
119
119
|
| `webhook list <project>` | `-e`, `--show-secrets` | List webhook URLs |
|
|
120
120
|
|
|
121
|
+
### skill
|
|
122
|
+
|
|
123
|
+
Manage agent skills on the crews SOP surface. `push` is an idempotent upsert (create, or update on name collision).
|
|
124
|
+
|
|
125
|
+
| Command | Key Flags | Description |
|
|
126
|
+
|---------|-----------|-------------|
|
|
127
|
+
| `skill push <dir>` | `--role <name>`, `--dry-run`, `--json` | Push a skill folder, or a whole plugin dir — recursive: pushes every `skills/*/SKILL.md`, converts `commands/*.md` → skills, and ingests each skill's `references/`. `--role` scopes to a role instead of the shared library |
|
|
128
|
+
| `skill list` | `--json`, `--limit <n>` | List skills in the library |
|
|
129
|
+
| `skill view <name>` | `--json` | Show one skill |
|
|
130
|
+
| `skill pull <name> [dest]` | `--json` | Fetch a skill to a local folder for editing (inverse of push) |
|
|
131
|
+
| `skill delete <name>` | `--json` | Delete a skill (Admin only) |
|
|
132
|
+
|
|
133
|
+
### role
|
|
134
|
+
|
|
135
|
+
| Command | Key Flags | Description |
|
|
136
|
+
|---------|-----------|-------------|
|
|
137
|
+
| `role push <dir>` | `--dry-run`, `--json` | Push a role definition (create or update) |
|
|
138
|
+
|
|
121
139
|
### workspace
|
|
122
140
|
|
|
123
141
|
| Command | Description |
|
|
@@ -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
|
+
}
|
|
@@ -4,6 +4,8 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
4
4
|
};
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
6
|
exports.runs = runs;
|
|
7
|
+
exports.summaryStatusLabel = summaryStatusLabel;
|
|
8
|
+
exports.detailedOutcomeTag = detailedOutcomeTag;
|
|
7
9
|
const axios_1 = __importDefault(require("axios"));
|
|
8
10
|
const chalk_1 = __importDefault(require("chalk"));
|
|
9
11
|
const api_1 = require("../utils/api");
|
|
@@ -70,6 +72,32 @@ async function runs(projectName, options = {}) {
|
|
|
70
72
|
}
|
|
71
73
|
}
|
|
72
74
|
// ─── Display modes ─────────────────────────────────────────────────────────
|
|
75
|
+
/**
|
|
76
|
+
* Summary-table status label for a run. Recovered/degraded runs share SUCCESS's
|
|
77
|
+
* execution_status, so without this they'd be indistinguishable from a clean success in
|
|
78
|
+
* the summary view. Returns the label to display and whether it's an "attention" outcome
|
|
79
|
+
* (rendered yellow). Falls back to the raw status for servers that don't send `outcome`.
|
|
80
|
+
*/
|
|
81
|
+
function summaryStatusLabel(run) {
|
|
82
|
+
if (run.outcome === 'recovered')
|
|
83
|
+
return { label: 'recovered', attention: true };
|
|
84
|
+
if (run.outcome === 'degraded')
|
|
85
|
+
return { label: 'degraded', attention: true };
|
|
86
|
+
return { label: run.execution_status || run.status || '?', attention: false };
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Detailed-view tag appended to a run header ('' when none). Prefers the API `outcome`;
|
|
90
|
+
* falls back to the local hasRunErrors heuristic for servers that don't send `outcome`.
|
|
91
|
+
*/
|
|
92
|
+
function detailedOutcomeTag(run) {
|
|
93
|
+
if (run.outcome === 'recovered')
|
|
94
|
+
return ' [RECOVERED]';
|
|
95
|
+
if (run.outcome === 'degraded')
|
|
96
|
+
return ' [DEGRADED]';
|
|
97
|
+
if (hasRunErrors(run) && isSuccessStatus(run.execution_status || run.status || ''))
|
|
98
|
+
return ' [DEGRADED]';
|
|
99
|
+
return '';
|
|
100
|
+
}
|
|
73
101
|
function displaySummaryTable(runsList, projectName) {
|
|
74
102
|
const header = projectName ? `Recent runs for "${projectName}":` : 'Recent runs:';
|
|
75
103
|
console.log(chalk_1.default.blue(header));
|
|
@@ -79,8 +107,8 @@ function displaySummaryTable(runsList, projectName) {
|
|
|
79
107
|
for (const run of runsList) {
|
|
80
108
|
const id = String(run.id || '?').padEnd(8);
|
|
81
109
|
const workflow = truncate(run.workflow_name || '?', 24).padEnd(25);
|
|
82
|
-
const status
|
|
83
|
-
const statusColor = getStatusColor(status);
|
|
110
|
+
const { label: status, attention } = summaryStatusLabel(run);
|
|
111
|
+
const statusColor = attention ? chalk_1.default.yellow : getStatusColor(status);
|
|
84
112
|
const triggeredAt = run.triggered_at ? new Date(run.triggered_at).toLocaleString() : '-';
|
|
85
113
|
const triggeredBy = run.triggered_by || '-';
|
|
86
114
|
console.log(chalk_1.default.gray(id) +
|
|
@@ -99,9 +127,9 @@ function displayDetailedList(runsList, projectName) {
|
|
|
99
127
|
const status = run.execution_status || run.status || '?';
|
|
100
128
|
const statusColor = getStatusColor(status);
|
|
101
129
|
const exitStr = run.exit_code !== null && run.exit_code !== undefined ? ` (exit ${run.exit_code})` : '';
|
|
102
|
-
const isSilentFailure = hasRunErrors(run) && isSuccessStatus(status);
|
|
103
130
|
console.log('');
|
|
104
|
-
const
|
|
131
|
+
const outcomeTag = detailedOutcomeTag(run);
|
|
132
|
+
const silentTag = outcomeTag ? chalk_1.default.yellow(outcomeTag) : '';
|
|
105
133
|
console.log(chalk_1.default.bold(` Run #${run.id}`) + chalk_1.default.gray(` — ${run.workflow_name || '?'} (${run.project_name || '?'})`) + silentTag);
|
|
106
134
|
console.log(` Status: ${statusColor(status)}${chalk_1.default.gray(exitStr)}`);
|
|
107
135
|
console.log(` Trigger: ${chalk_1.default.gray(run.triggered_by || '-')}`);
|
|
@@ -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
|
+
}
|
|
@@ -4,13 +4,20 @@
|
|
|
4
4
|
*
|
|
5
5
|
* Pushes a local skill folder into the crews library via the crews MCP server.
|
|
6
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.
|
|
7
11
|
*/
|
|
8
12
|
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
9
13
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
10
14
|
};
|
|
11
15
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
12
16
|
exports.parseSkillFile = parseSkillFile;
|
|
13
|
-
exports.
|
|
17
|
+
exports.readReferences = readReferences;
|
|
18
|
+
exports.readSkillDir = readSkillDir;
|
|
19
|
+
exports.parseCommandFile = parseCommandFile;
|
|
20
|
+
exports.pushParsedSkill = pushParsedSkill;
|
|
14
21
|
exports.skillPushWithConfig = skillPushWithConfig;
|
|
15
22
|
exports.skillPush = skillPush;
|
|
16
23
|
const fs_1 = __importDefault(require("fs"));
|
|
@@ -65,55 +72,141 @@ function parseSkillFile(content) {
|
|
|
65
72
|
return { name, description, properties, body };
|
|
66
73
|
}
|
|
67
74
|
/**
|
|
68
|
-
*
|
|
69
|
-
* Returns a map of
|
|
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).
|
|
70
82
|
*/
|
|
71
|
-
function
|
|
72
|
-
const entries = fs_1.default.readdirSync(dir, { withFileTypes: true });
|
|
83
|
+
function readReferences(dir) {
|
|
73
84
|
const references = {};
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
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);
|
|
82
102
|
return references;
|
|
83
103
|
}
|
|
84
104
|
/**
|
|
85
|
-
*
|
|
86
|
-
*
|
|
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.
|
|
87
109
|
*/
|
|
88
|
-
|
|
110
|
+
function readSkillDir(dir) {
|
|
89
111
|
const absDir = path_1.default.resolve(dir);
|
|
90
|
-
// 1. Check directory exists
|
|
91
112
|
if (!fs_1.default.existsSync(absDir) || !fs_1.default.statSync(absDir).isDirectory()) {
|
|
92
|
-
|
|
93
|
-
process.exit(1);
|
|
113
|
+
throw new Error(`"${dir}" is not a directory.`);
|
|
94
114
|
}
|
|
95
|
-
// 2. Read SKILL.md
|
|
96
115
|
const skillMdPath = path_1.default.join(absDir, 'SKILL.md');
|
|
97
116
|
if (!fs_1.default.existsSync(skillMdPath)) {
|
|
98
|
-
|
|
99
|
-
process.exit(1);
|
|
117
|
+
throw new Error(`"${skillMdPath}" not found. The skill directory must contain a SKILL.md file.`);
|
|
100
118
|
}
|
|
101
119
|
const skillMdContent = fs_1.default.readFileSync(skillMdPath, 'utf8');
|
|
102
|
-
|
|
103
|
-
|
|
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;
|
|
104
149
|
try {
|
|
105
|
-
|
|
150
|
+
fm = js_yaml_1.default.load(yamlBlock);
|
|
106
151
|
}
|
|
107
152
|
catch (e) {
|
|
108
|
-
|
|
109
|
-
|
|
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).`);
|
|
110
160
|
}
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
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;
|
|
115
180
|
const isRole = !!options.role;
|
|
116
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
|
+
}
|
|
117
210
|
const createArgs = isRole
|
|
118
211
|
? { action: 'create_skill', role: options.role, name, description, body, properties, references }
|
|
119
212
|
: { action: 'create', name, description, body, properties, references };
|
|
@@ -122,11 +215,9 @@ async function skillPushWithConfig(dir, options, config) {
|
|
|
122
215
|
result = await (0, mcp_1.callCrewsTool)(config, tool, createArgs);
|
|
123
216
|
}
|
|
124
217
|
catch (e) {
|
|
125
|
-
|
|
126
|
-
process.exit(1);
|
|
218
|
+
throw new Error(`MCP request failed — ${e.message}`);
|
|
127
219
|
}
|
|
128
|
-
|
|
129
|
-
// 6. On name collision, switch to the edit path (the upsert). properties → properties_patch.
|
|
220
|
+
// On name collision, switch to the edit path. properties → properties_patch.
|
|
130
221
|
if (!result.ok && result.data?.code === 'name_collision') {
|
|
131
222
|
const editArgs = isRole
|
|
132
223
|
? { action: 'edit_skill', role: options.role, name, description, body, properties_patch: properties, references }
|
|
@@ -135,34 +226,186 @@ async function skillPushWithConfig(dir, options, config) {
|
|
|
135
226
|
result = await (0, mcp_1.callCrewsTool)(config, tool, editArgs);
|
|
136
227
|
}
|
|
137
228
|
catch (e) {
|
|
138
|
-
|
|
139
|
-
process.exit(1);
|
|
229
|
+
throw new Error(`MCP request failed — ${e.message}`);
|
|
140
230
|
}
|
|
141
|
-
|
|
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 ?? {} };
|
|
142
238
|
}
|
|
143
|
-
// 7. Handle result.
|
|
144
239
|
if (!result.ok) {
|
|
145
240
|
const errData = result.data;
|
|
146
241
|
const errCode = errData?.code ?? 'unknown_error';
|
|
147
242
|
const errMsg = errData?.message ?? 'MCP returned an error with no message';
|
|
148
|
-
|
|
149
|
-
process.exit(1);
|
|
243
|
+
throw new Error(`${errCode}: ${errMsg}`);
|
|
150
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) {
|
|
151
251
|
if (options.json) {
|
|
152
252
|
console.log(JSON.stringify(result.data));
|
|
153
|
-
|
|
253
|
+
return;
|
|
154
254
|
}
|
|
155
|
-
const data = result
|
|
156
|
-
if (
|
|
157
|
-
|
|
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') {
|
|
158
263
|
console.log(chalk_1.default.green(`updated skill '${name}' (version ${data.version_id ?? '?'})`));
|
|
159
264
|
}
|
|
160
265
|
else {
|
|
161
|
-
// create shape: {skill_doc_id, folder_id, reference_doc_ids}
|
|
162
266
|
const skillDocId = data.skill_doc_id ?? data.doc_id ?? data.id ?? '?';
|
|
163
267
|
const refCount = Object.keys(data.reference_doc_ids ?? {}).length;
|
|
164
268
|
console.log(chalk_1.default.green(`created skill '${name}' (doc ${skillDocId}, ${refCount} refs)`));
|
|
165
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
|
+
}
|
|
166
409
|
process.exit(0);
|
|
167
410
|
}
|
|
168
411
|
/**
|
|
@@ -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
|
@@ -33,6 +33,11 @@ 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
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");
|
|
36
41
|
const config_1 = require("./utils/config");
|
|
37
42
|
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
|
38
43
|
const pkg = require('../package.json');
|
|
@@ -402,9 +407,53 @@ skill
|
|
|
402
407
|
.argument('<dir>', 'Path to the skill directory (must contain SKILL.md)')
|
|
403
408
|
.option('--role <name>', 'Scope the skill to a role instead of the shared library')
|
|
404
409
|
.option('--json', 'Output result as JSON')
|
|
410
|
+
.option('--dry-run', 'Preview create vs update without writing')
|
|
405
411
|
.action(async (dir, options) => {
|
|
406
412
|
await (0, skill_push_1.skillPush)(dir, options);
|
|
407
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
|
+
});
|
|
408
457
|
program.parseAsync().catch((err) => {
|
|
409
458
|
console.error(chalk_1.default.red(err.message ?? String(err)));
|
|
410
459
|
process.exit(1);
|