@solidactions/cli 1.13.0 → 1.15.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 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,220 @@
1
+ "use strict";
2
+ /**
3
+ * solidactions docs push <dir>
4
+ *
5
+ * Recursively uploads a local markdown tree into SA-Docs via the docs MCP
6
+ * server's `docs_vault bulk_create` tool, mirroring folder structure.
7
+ *
8
+ * Prints a report distinguishing fully-published docs from "properties pending"
9
+ * ones (docs whose frontmatter properties couldn't be validated yet).
10
+ */
11
+ var __importDefault = (this && this.__importDefault) || function (mod) {
12
+ return (mod && mod.__esModule) ? mod : { "default": mod };
13
+ };
14
+ Object.defineProperty(exports, "__esModule", { value: true });
15
+ exports.docsPushWithConfig = docsPushWithConfig;
16
+ exports.docsPush = docsPush;
17
+ const fs_1 = __importDefault(require("fs"));
18
+ const path_1 = __importDefault(require("path"));
19
+ const chalk_1 = __importDefault(require("chalk"));
20
+ const api_1 = require("../utils/api");
21
+ const mcp_1 = require("../utils/mcp");
22
+ const CHUNK_SIZE = 50;
23
+ /**
24
+ * Recursively walk `dir` and collect all *.md files.
25
+ * Returns a list of absolute paths.
26
+ */
27
+ function walkMarkdownFiles(dir) {
28
+ const results = [];
29
+ const walk = (current) => {
30
+ for (const entry of fs_1.default.readdirSync(current, { withFileTypes: true })) {
31
+ const abs = path_1.default.join(current, entry.name);
32
+ if (entry.isDirectory()) {
33
+ walk(abs);
34
+ }
35
+ else if (entry.isFile() && entry.name.endsWith('.md')) {
36
+ results.push(abs);
37
+ }
38
+ }
39
+ };
40
+ walk(dir);
41
+ return results;
42
+ }
43
+ /**
44
+ * Convert an absolute file path to a bulk_create item, relative to `rootDir`.
45
+ */
46
+ function fileToItem(absPath, rootDir) {
47
+ const title = path_1.default.basename(absPath, '.md');
48
+ const body = fs_1.default.readFileSync(absPath, 'utf8');
49
+ const relDir = path_1.default.relative(rootDir, path_1.default.dirname(absPath));
50
+ // Use POSIX separators; omit if file is in the root dir
51
+ const relative_folder_path = relDir === '' ? undefined : relDir.split(path_1.default.sep).join('/');
52
+ const item = { title, body, _filePath: absPath };
53
+ if (relative_folder_path !== undefined) {
54
+ item.relative_folder_path = relative_folder_path;
55
+ }
56
+ return item;
57
+ }
58
+ /**
59
+ * Merge two BulkCreateSummary objects by summing all numeric fields.
60
+ */
61
+ function mergeSummaries(a, b) {
62
+ const keys = [
63
+ 'created', 'overwritten', 'skipped', 'renamed', 'errors', 'folders_created',
64
+ 'planned_create', 'planned_overwrite', 'planned_skip', 'planned_rename',
65
+ ];
66
+ const result = {};
67
+ for (const key of keys) {
68
+ const aVal = a[key] ?? 0;
69
+ const bVal = b[key] ?? 0;
70
+ if (aVal !== 0 || bVal !== 0) {
71
+ result[key] = aVal + bVal;
72
+ }
73
+ }
74
+ return result;
75
+ }
76
+ /**
77
+ * Format the merged summary into human-readable totals line.
78
+ * Normal run: created/overwritten/skipped/renamed. Dry-run: planned_*.
79
+ */
80
+ function formatSummaryLine(summary, dryRun) {
81
+ const parts = [];
82
+ if (dryRun) {
83
+ if (summary.planned_create)
84
+ parts.push(`${summary.planned_create} planned create`);
85
+ if (summary.planned_overwrite)
86
+ parts.push(`${summary.planned_overwrite} planned overwrite`);
87
+ if (summary.planned_skip)
88
+ parts.push(`${summary.planned_skip} planned skip`);
89
+ if (summary.planned_rename)
90
+ parts.push(`${summary.planned_rename} planned rename`);
91
+ }
92
+ else {
93
+ if (summary.created)
94
+ parts.push(`${summary.created} created`);
95
+ if (summary.overwritten)
96
+ parts.push(`${summary.overwritten} updated`);
97
+ if (summary.skipped)
98
+ parts.push(`${summary.skipped} skipped`);
99
+ if (summary.renamed)
100
+ parts.push(`${summary.renamed} renamed`);
101
+ if (summary.errors)
102
+ parts.push(`${summary.errors} errors`);
103
+ }
104
+ return parts.join(', ') || '0 docs';
105
+ }
106
+ /**
107
+ * Core implementation — accepts an injected config so tests can point at a
108
+ * stub server without touching the filesystem config.
109
+ */
110
+ async function docsPushWithConfig(dir, options, config) {
111
+ const absDir = path_1.default.resolve(dir);
112
+ if (!fs_1.default.existsSync(absDir) || !fs_1.default.statSync(absDir).isDirectory()) {
113
+ process.stderr.write(chalk_1.default.red(`error: "${dir}" is not a directory.\n`));
114
+ process.exit(1);
115
+ }
116
+ const allFiles = walkMarkdownFiles(absDir);
117
+ if (allFiles.length === 0) {
118
+ process.stderr.write(chalk_1.default.red(`error: no .md files found under "${dir}" — nothing to push.\n`));
119
+ process.exit(1);
120
+ }
121
+ const items = allFiles.map((f) => fileToItem(f, absDir));
122
+ // Chunk into groups of CHUNK_SIZE
123
+ const chunks = [];
124
+ for (let i = 0; i < items.length; i += CHUNK_SIZE) {
125
+ chunks.push(items.slice(i, i + CHUNK_SIZE));
126
+ }
127
+ const onConflict = options.onConflict ?? 'skip';
128
+ const allResultRows = [];
129
+ let mergedSummary = {};
130
+ for (const chunk of chunks) {
131
+ const callArgs = {
132
+ action: 'bulk_create',
133
+ on_conflict: onConflict,
134
+ items: chunk.map(({ title, body, relative_folder_path }) => {
135
+ const item = { title, body };
136
+ if (relative_folder_path !== undefined) {
137
+ item.relative_folder_path = relative_folder_path;
138
+ }
139
+ return item;
140
+ }),
141
+ };
142
+ if (options.type) {
143
+ callArgs.type = options.type;
144
+ }
145
+ if (options.dryRun) {
146
+ callArgs.dry_run = true;
147
+ }
148
+ let mcpResult;
149
+ try {
150
+ mcpResult = await (0, mcp_1.callDocsTool)(config, 'docs_vault', callArgs);
151
+ }
152
+ catch (e) {
153
+ process.stderr.write(chalk_1.default.red(`error: MCP request failed — ${e.message}\n`));
154
+ process.exit(1);
155
+ }
156
+ if (!mcpResult.ok) {
157
+ const code = mcpResult.data?.code ?? 'unknown_error';
158
+ const message = mcpResult.data?.message ?? 'MCP returned an error with no message';
159
+ process.stderr.write(chalk_1.default.red(`error: ${code}: ${message}\n`));
160
+ process.exit(1);
161
+ }
162
+ const data = mcpResult.data;
163
+ const rows = data?.results ?? [];
164
+ const summary = data?.summary ?? {};
165
+ // Correlate rows back to source files using chunk index
166
+ for (const row of rows) {
167
+ const sourceFile = chunk[row.index]?._filePath ?? '(unknown)';
168
+ allResultRows.push({
169
+ ...row,
170
+ file: path_1.default.relative(absDir, sourceFile),
171
+ });
172
+ }
173
+ mergedSummary = mergeSummaries(mergedSummary, summary);
174
+ }
175
+ // Collect pending items (results that have property_validation)
176
+ const pendingItems = allResultRows
177
+ .filter((r) => r.property_validation != null)
178
+ .map((r) => ({
179
+ file: r.file,
180
+ missing: r.property_validation.missing ?? [],
181
+ invalid: r.property_validation.invalid ?? [],
182
+ }));
183
+ // --json: output single JSON object
184
+ if (options.json) {
185
+ console.log(JSON.stringify({
186
+ summary: mergedSummary,
187
+ pending: pendingItems,
188
+ results: allResultRows,
189
+ }));
190
+ process.exit(0);
191
+ }
192
+ // Human-readable output
193
+ const summaryLine = formatSummaryLine(mergedSummary, !!options.dryRun);
194
+ if (options.dryRun) {
195
+ console.log(chalk_1.default.cyan(`[dry-run preview] ${summaryLine}`));
196
+ }
197
+ else {
198
+ console.log(chalk_1.default.green(`done: ${summaryLine}`));
199
+ }
200
+ if (pendingItems.length > 0) {
201
+ console.log(chalk_1.default.yellow(`\nProperties pending (${pendingItems.length} doc${pendingItems.length === 1 ? '' : 's'}):`));
202
+ for (const item of pendingItems) {
203
+ console.log(chalk_1.default.yellow(` ${item.file}`));
204
+ if (item.missing.length > 0) {
205
+ console.log(chalk_1.default.yellow(` missing: ${item.missing.join(', ')}`));
206
+ }
207
+ for (const inv of item.invalid) {
208
+ console.log(chalk_1.default.yellow(` invalid: ${inv.key} — ${inv.reason}`));
209
+ }
210
+ }
211
+ }
212
+ process.exit(0);
213
+ }
214
+ /**
215
+ * Entry point called from index.ts.
216
+ */
217
+ async function docsPush(dir, options) {
218
+ const config = await (0, api_1.requireConfigWithWorkspace)();
219
+ await docsPushWithConfig(dir, options, config);
220
+ }
@@ -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 = run.execution_status || run.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 silentTag = isSilentFailure ? chalk_1.default.yellow(' [DEGRADED]') : '';
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 || '-')}`);
package/dist/index.js CHANGED
@@ -38,6 +38,7 @@ const skill_list_1 = require("./commands/skill-list");
38
38
  const skill_view_1 = require("./commands/skill-view");
39
39
  const skill_delete_1 = require("./commands/skill-delete");
40
40
  const role_push_1 = require("./commands/role-push");
41
+ const docs_push_1 = require("./commands/docs-push");
41
42
  const config_1 = require("./utils/config");
42
43
  // eslint-disable-next-line @typescript-eslint/no-var-requires
43
44
  const pkg = require('../package.json');
@@ -454,6 +455,21 @@ role
454
455
  .action(async (dir, options) => {
455
456
  await (0, role_push_1.rolePush)(dir, options);
456
457
  });
458
+ // =============================================================================
459
+ // docs <subcommand> (SA-Docs surface)
460
+ // =============================================================================
461
+ const docs = program.command('docs').description('Manage docs in SA-Docs');
462
+ docs
463
+ .command('push')
464
+ .description('Recursively upload a local markdown tree into SA-Docs')
465
+ .argument('<dir>', 'Path to the directory containing markdown files')
466
+ .option('--on-conflict <mode>', 'Conflict resolution: skip|overwrite|rename (default: skip)', 'skip')
467
+ .option('--type <slug>', 'Doc-type slug to apply to all uploaded docs')
468
+ .option('--dry-run', 'Preview what would be created without writing')
469
+ .option('--json', 'Output result as JSON')
470
+ .action(async (dir, options) => {
471
+ await (0, docs_push_1.docsPush)(dir, { onConflict: options.onConflict, type: options.type, dryRun: options.dryRun, json: options.json });
472
+ });
457
473
  program.parseAsync().catch((err) => {
458
474
  console.error(chalk_1.default.red(err.message ?? String(err)));
459
475
  process.exit(1);
package/dist/utils/mcp.js CHANGED
@@ -1,10 +1,10 @@
1
1
  "use strict";
2
2
  /**
3
- * Minimal MCP client for the SolidActions crews MCP server.
3
+ * Minimal MCP client for the SolidActions in-app MCP servers.
4
4
  *
5
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).
6
+ * required — the streamable-HTTP transport accepts single stateless POSTs
7
+ * (confirmed live).
8
8
  */
9
9
  var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
10
10
  if (k2 === undefined) k2 = k;
@@ -41,16 +41,17 @@ var __importStar = (this && this.__importStar) || (function () {
41
41
  })();
42
42
  Object.defineProperty(exports, "__esModule", { value: true });
43
43
  exports.callCrewsTool = callCrewsTool;
44
+ exports.callDocsTool = callDocsTool;
44
45
  const http = __importStar(require("http"));
45
46
  const https = __importStar(require("https"));
46
47
  const url_1 = require("url");
47
48
  const api_1 = require("./api");
48
49
  /**
49
- * Call a single MCP tool on /mcp/crews.
50
+ * Internal: call a single MCP tool on the given endpoint path.
50
51
  *
51
52
  * Returns { ok: true, data: <success shape> } or { ok: false, data: { code, message } }.
52
53
  */
53
- async function callCrewsTool(config, toolName, args) {
54
+ async function callMcpTool(config, endpointPath, toolName, args) {
54
55
  const baseHeaders = (0, api_1.getApiHeaders)(config, 'application/json');
55
56
  // Override Accept to include text/event-stream for streamable-HTTP transport
56
57
  const headers = {
@@ -66,7 +67,7 @@ async function callCrewsTool(config, toolName, args) {
66
67
  arguments: args,
67
68
  },
68
69
  });
69
- const parsed = new url_1.URL(`${config.host}/mcp/crews`);
70
+ const parsed = new url_1.URL(`${config.host}${endpointPath}`);
70
71
  const isHttps = parsed.protocol === 'https:';
71
72
  const transport = isHttps ? https : http;
72
73
  const responseData = await new Promise((resolve, reject) => {
@@ -115,3 +116,15 @@ async function callCrewsTool(config, toolName, args) {
115
116
  }
116
117
  return { ok: !isError, data: toolData };
117
118
  }
119
+ /**
120
+ * Call a single MCP tool on /mcp/crews.
121
+ */
122
+ async function callCrewsTool(config, toolName, args) {
123
+ return callMcpTool(config, '/mcp/crews', toolName, args);
124
+ }
125
+ /**
126
+ * Call a single MCP tool on /mcp/docs.
127
+ */
128
+ async function callDocsTool(config, toolName, args) {
129
+ return callMcpTool(config, '/mcp/docs', toolName, args);
130
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@solidactions/cli",
3
- "version": "1.13.0",
3
+ "version": "1.15.0",
4
4
  "description": "SolidActions CLI - Deploy and manage workflow automation",
5
5
  "main": "dist/index.js",
6
6
  "bin": {