@solidactions/cli 1.10.3 → 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.
@@ -1,49 +1,385 @@
1
1
  "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
2
35
  var __importDefault = (this && this.__importDefault) || function (mod) {
3
36
  return (mod && mod.__esModule) ? mod : { "default": mod };
4
37
  };
5
38
  Object.defineProperty(exports, "__esModule", { value: true });
39
+ exports.resolveProjectSlug = resolveProjectSlug;
40
+ exports.runDev = runDev;
6
41
  exports.dev = dev;
7
42
  const child_process_1 = require("child_process");
8
43
  const path_1 = __importDefault(require("path"));
9
44
  const fs_1 = __importDefault(require("fs"));
10
45
  const chalk_1 = __importDefault(require("chalk"));
11
- const dotenv_1 = __importDefault(require("dotenv"));
12
- async function dev(file, options) {
13
- // Resolve the workflow file path
14
- const filePath = path_1.default.resolve(file);
15
- if (!fs_1.default.existsSync(filePath)) {
16
- console.error(chalk_1.default.red(`File not found: ${filePath}`));
17
- process.exit(1);
46
+ const js_yaml_1 = __importDefault(require("js-yaml"));
47
+ const crypto_1 = require("crypto");
48
+ const module_1 = require("module");
49
+ /**
50
+ * Resolve the SA project slug for a workflow entry file and a target env.
51
+ *
52
+ * Walks up from the entry file to the project root (the directory containing
53
+ * `solidactions.yaml`), reads the declared `project:` name, and applies the
54
+ * SAME env→slug rule used by `deploy`: production keeps the bare name, every
55
+ * other environment appends `-<env>` (e.g. `sdk-test` → `sdk-test-dev`).
56
+ *
57
+ * Throws when the project root or the `project:` field cannot be found, so the
58
+ * caller surfaces a clear error instead of fetching against a bogus slug.
59
+ */
60
+ function resolveProjectSlug(entryPath, env) {
61
+ const root = findSolidActionsRoot(path_1.default.resolve(entryPath));
62
+ if (!root) {
63
+ throw new Error(`could not find solidactions.yaml in any parent of ${entryPath}`);
18
64
  }
19
- // Find the project root (directory containing package.json)
20
- const projectDir = findProjectRoot(filePath);
21
- if (!projectDir) {
22
- console.error(chalk_1.default.red('Could not find package.json in parent directories.'));
23
- process.exit(1);
65
+ const configPath = path_1.default.join(root, 'solidactions.yaml');
66
+ const config = js_yaml_1.default.load(fs_1.default.readFileSync(configPath, 'utf8'));
67
+ const projectName = config?.project;
68
+ if (!projectName || typeof projectName !== 'string') {
69
+ throw new Error(`solidactions.yaml at ${configPath} is missing a top-level "project:" name`);
24
70
  }
25
- // Check that @solidactions/sdk is installed
26
- const sdkPath = path_1.default.join(projectDir, 'node_modules', '@solidactions', 'sdk');
27
- if (!fs_1.default.existsSync(sdkPath)) {
28
- console.error(chalk_1.default.red('@solidactions/sdk is not installed in this project.'));
29
- console.log(chalk_1.default.gray('Run: npm install @solidactions/sdk'));
30
- process.exit(1);
71
+ return env === 'production' ? projectName : `${projectName}-${env}`;
72
+ }
73
+ /**
74
+ * Walk up from a starting path looking for the directory that holds
75
+ * `solidactions.yaml` (the project root). Returns null if none is found.
76
+ */
77
+ function findSolidActionsRoot(startPath) {
78
+ let dir = fs_1.default.existsSync(startPath) && fs_1.default.statSync(startPath).isDirectory()
79
+ ? startPath
80
+ : path_1.default.dirname(startPath);
81
+ for (let i = 0; i < 10; i++) {
82
+ if (fs_1.default.existsSync(path_1.default.join(dir, 'solidactions.yaml'))) {
83
+ return dir;
84
+ }
85
+ const parent = path_1.default.dirname(dir);
86
+ if (parent === dir)
87
+ break;
88
+ dir = parent;
31
89
  }
32
- // Check that the testing export exists
33
- const testingPath = path_1.default.join(sdkPath, 'dist', 'src', 'testing');
34
- if (!fs_1.default.existsSync(testingPath)) {
35
- console.error(chalk_1.default.red('@solidactions/sdk version does not include testing utilities.'));
36
- console.log(chalk_1.default.gray('Update to the latest SDK version: npm install @solidactions/sdk@latest'));
37
- process.exit(1);
90
+ return null;
91
+ }
92
+ /**
93
+ * Run a workflow entry file in local-dev mode:
94
+ * 1. Fetch declared vars + connections from the platform (via api seam).
95
+ * 2. Apply any varsOverride, warning when a key shadows a platform var.
96
+ * 3. Spin up an SDK mock backend (createMockServer) for durable-primitive support.
97
+ * 4. import() the workflow module to populate the registry.
98
+ * 5. invoke() the first registered workflow descriptor.
99
+ * 6. Return { stdout, stderr, result }.
100
+ *
101
+ * Never calls process.exit; never lets exceptions escape (returns failed result).
102
+ */
103
+ async function runDev(opts) {
104
+ const stdoutLines = [];
105
+ const stderrLines = [];
106
+ function out(msg) {
107
+ stdoutLines.push(msg);
108
+ }
109
+ function err(msg) {
110
+ stderrLines.push(msg);
111
+ }
112
+ // 1. Determine the API client (only when an env was requested). With no
113
+ // --env, we run fully locally: NO platform fetch, ctx.vars starts empty.
114
+ let apiClient;
115
+ if (opts.env) {
116
+ if (opts.api) {
117
+ apiClient = opts.api;
118
+ }
119
+ else {
120
+ // Production path: build from CLI config.
121
+ // Lazy-import to keep tests fast (no config resolution needed).
122
+ const { requireConfigWithWorkspace } = await Promise.resolve().then(() => __importStar(require('../utils/api')));
123
+ const { getApiHeaders } = await Promise.resolve().then(() => __importStar(require('../utils/api')));
124
+ const axios = (await Promise.resolve().then(() => __importStar(require('axios')))).default;
125
+ const config = await requireConfigWithWorkspace();
126
+ // Real project resolution: read the project name from the workflow
127
+ // file's solidactions.yaml and apply the deploy env→slug rule.
128
+ const projectSlug = resolveProjectSlug(opts.entry, opts.env);
129
+ apiClient = {
130
+ projectSlug,
131
+ async fetchVarsAndConnections(_env) {
132
+ const response = await axios.get(`${config.host}/api/v1/projects/${projectSlug}/variable-mappings?resolve_oauth=true`, { headers: getApiHeaders(config) });
133
+ return response.data || [];
134
+ },
135
+ };
136
+ }
137
+ }
138
+ // 2. Fetch vars from platform (only when an env + client are present).
139
+ let platformVars = [];
140
+ if (apiClient && opts.env) {
141
+ try {
142
+ platformVars = await apiClient.fetchVarsAndConnections(opts.env);
143
+ }
144
+ catch (e) {
145
+ err(`failed to fetch platform vars: ${e?.message ?? e}`);
146
+ }
147
+ }
148
+ // 3. Build ctx.vars from platform vars. A declared mapping is only readable
149
+ // on ctx.vars when it is an OAuth connection (with proxy fields) OR has a
150
+ // non-null resolved_value. Mappings with no value in this env are DROPPED
151
+ // — and must NOT be counted in the summary (BUG #1).
152
+ const vars = {};
153
+ let connectionCount = 0;
154
+ let droppedCount = 0;
155
+ for (const pv of platformVars) {
156
+ if (pv.source_type === 'oauth_connection' && pv.proxy_url && pv.proxy_token && pv.connection_key) {
157
+ vars[pv.env_name] = {
158
+ key: pv.connection_key,
159
+ proxyUrl: pv.proxy_url,
160
+ proxyToken: pv.proxy_token,
161
+ };
162
+ connectionCount++;
163
+ }
164
+ else if (pv.resolved_value != null) {
165
+ vars[pv.env_name] = pv.resolved_value;
166
+ }
167
+ else {
168
+ droppedCount++;
169
+ }
170
+ }
171
+ // 4. Apply overrides, warning on shadows.
172
+ if (opts.varsOverride) {
173
+ for (const [key, value] of Object.entries(opts.varsOverride)) {
174
+ if (key in vars) {
175
+ err(`override shadows platform var: ${key}`);
176
+ }
177
+ vars[key] = value;
178
+ }
179
+ }
180
+ // 5. Print summary line. Report what is ACTUALLY readable on ctx.vars — the
181
+ // plain-var count is the number of plain vars actually placed in `vars`
182
+ // (total keys minus connection entries), never the raw mapping count.
183
+ if (opts.env) {
184
+ const plainVarCount = Object.keys(vars).length - connectionCount;
185
+ let summary = `Loaded ${plainVarCount} vars + ${connectionCount} connections from ${apiClient.projectSlug} / env ${opts.env}`;
186
+ if (droppedCount > 0) {
187
+ summary += ` (${droppedCount} declared ${droppedCount === 1 ? 'var' : 'vars'} had no value in this env and ${droppedCount === 1 ? 'was' : 'were'} skipped)`;
188
+ }
189
+ out(summary);
190
+ }
191
+ else {
192
+ out('Loaded 0 platform vars (no --env) — running locally');
193
+ }
194
+ // 6. Spin up SDK mock backend.
195
+ // Require createMockServer from the SDK's testing subpath.
196
+ //
197
+ // CRITICAL: root the require at the ENTRY FILE, not at __filename. The
198
+ // workflow module's own `import '@solidactions/sdk'` resolves the SDK
199
+ // relative to the entry file's project (e.g. examples/sdk-test/sdk), and
200
+ // it registers its workflow into THAT copy's registry singleton. If we
201
+ // resolved the SDK from the CLI's own node_modules instead (a different,
202
+ // npm-linked copy), we'd read an empty registry. Resolving from the entry's
203
+ // directory guarantees we hit the same SDK instance the workflow used.
204
+ const entryPath = path_1.default.resolve(opts.entry);
205
+ const _require = (0, module_1.createRequire)(entryPath);
206
+ // Resolve via the installed (or linked) @solidactions/sdk package.
207
+ const sdkTestingMain = _require.resolve('@solidactions/sdk/testing');
208
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
209
+ const { createMockServer } = _require(sdkTestingMain);
210
+ const mockServer = await createMockServer();
211
+ try {
212
+ // 7. Import the workflow entry file. Prefer the module's default export
213
+ // (a WorkflowDescriptor returned by defineWorkflow) so that repeated
214
+ // runDev() calls in the same test process don't need to clear and
215
+ // re-register from a cached module (cached CJS modules don't re-run
216
+ // their top-level code). If no default export, fall back to the registry.
217
+ const mod = await Promise.resolve(`${entryPath}`).then(s => __importStar(require(s)));
218
+ let descriptor;
219
+ if (mod?.default && typeof mod.default?.run === 'function') {
220
+ // Module exported a WorkflowDescriptor as its default — use it directly.
221
+ descriptor = mod.default;
222
+ }
223
+ else {
224
+ // Fall back: read from the SDK registry (populated by defineWorkflow side-effects).
225
+ const sdkMain = _require.resolve('@solidactions/sdk');
226
+ const registryPath = path_1.default.resolve(sdkMain, '..', 'invoke', 'registry.js');
227
+ const registry = _require(registryPath);
228
+ const all = registry.__getRegisteredWorkflows();
229
+ descriptor = all[0];
230
+ }
231
+ if (!descriptor) {
232
+ err('runDev: no workflows registered after importing the file.');
233
+ return {
234
+ stdout: stdoutLines.join('\n'),
235
+ stderr: stderrLines.join('\n'),
236
+ result: { status: 'failed', error: new Error('no workflow registered'), phase: 'run' },
237
+ };
238
+ }
239
+ // 8. Build InvokeCtx.
240
+ const runUuid = (0, crypto_1.randomUUID)();
241
+ const ctx = {
242
+ input: JSON.parse(opts.input || '{}'),
243
+ vars: Object.freeze(vars),
244
+ run: {
245
+ triggerId: 'local-dev',
246
+ runUuid,
247
+ runSecret: 'local-dev',
248
+ workerSessionId: (0, crypto_1.randomUUID)(),
249
+ },
250
+ app: {
251
+ appVersion: '0',
252
+ appId: 'local-dev',
253
+ tenantId: 'local-dev',
254
+ },
255
+ api: {
256
+ url: mockServer.baseUrl,
257
+ key: 'local-dev',
258
+ },
259
+ mode: 'local',
260
+ };
261
+ // 9. Create the run-row BEFORE invoking. invoke() itself never creates
262
+ // the durable run record — the production launcher (SolidActions.run
263
+ // → #initOneShotStatusRow) does that first, and step/sleep/recv
264
+ // sub-routes (`/runs/status/<id>/...`) 404 ("Workflow not found")
265
+ // against an absent row. A no-step workflow (the echo fixture) never
266
+ // hits those routes so it survives without this; any workflow with a
267
+ // runStep does not. Mirror #initOneShotStatusRow's CREATE shape.
268
+ const sdkMainForInvoke = _require.resolve('@solidactions/sdk');
269
+ const httpClientPath = path_1.default.resolve(sdkMainForInvoke, '..', 'http_client.js');
270
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
271
+ const { HttpClient } = _require(httpClientPath);
272
+ try {
273
+ const client = new HttpClient({ baseUrl: ctx.api.url, apiKey: ctx.api.key });
274
+ await client.post('/runs/status', {
275
+ workflowUUID: ctx.run.runUuid,
276
+ status: 'PENDING',
277
+ workflowName: descriptor.name ?? '',
278
+ workflowClassName: '',
279
+ workflowConfigName: '',
280
+ output: null,
281
+ error: null,
282
+ authenticatedUser: '',
283
+ assumedRole: '',
284
+ authenticatedRoles: [],
285
+ request: {},
286
+ executorId: String(ctx.run.triggerId),
287
+ applicationVersion: ctx.app.appVersion,
288
+ applicationID: ctx.app.appId,
289
+ createdAt: Date.now(),
290
+ priority: 0,
291
+ ownerXid: (0, crypto_1.randomUUID)(),
292
+ options: {},
293
+ });
294
+ }
295
+ catch (e) {
296
+ // Best-effort, symmetric with #initOneShotStatusRow's swallow.
297
+ err(`failed to create local run-row: ${e?.message ?? e}`);
298
+ }
299
+ // 10. Invoke via internal SDK path (invoke() is not in the public index).
300
+ const invokePath = path_1.default.resolve(sdkMainForInvoke, '..', 'invoke', 'invoke.js');
301
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
302
+ const { invoke } = _require(invokePath);
303
+ const result = await invoke(descriptor, ctx);
304
+ return {
305
+ stdout: stdoutLines.join('\n'),
306
+ stderr: stderrLines.join('\n'),
307
+ result,
308
+ };
309
+ }
310
+ finally {
311
+ await mockServer.stop();
38
312
  }
39
- // Load .env file from project root
40
- const envPath = path_1.default.join(projectDir, '.env');
41
- if (fs_1.default.existsSync(envPath)) {
42
- dotenv_1.default.config({ path: envPath });
43
- console.log(chalk_1.default.gray(`Loaded .env from ${projectDir}`));
313
+ }
314
+ /**
315
+ * Env var set on the tsx re-exec child so it does NOT re-exec again. Without
316
+ * this guard the `--env` path would fork itself forever.
317
+ */
318
+ const TSX_REEXEC_GUARD = 'SOLIDACTIONS_DEV_TSX_REEXEC';
319
+ /**
320
+ * Detect whether the current Node process already has a TypeScript loader
321
+ * registered (tsx / ts-node). When true, `await import('<file>.ts')` works
322
+ * in-process and we can run `runDev` directly without re-exec.
323
+ */
324
+ function hasTsLoader() {
325
+ if (process.env[TSX_REEXEC_GUARD] === '1') {
326
+ return true;
327
+ }
328
+ const execArgv = process.execArgv.join(' ');
329
+ return /tsx|ts-node/.test(execArgv) || Boolean(process._preload_modules);
330
+ }
331
+ /**
332
+ * Re-exec the CLI under `npx tsx` so that `runDev`'s `await import()` of a
333
+ * `.ts` workflow file resolves against tsx's TypeScript loader. The child runs
334
+ * the SAME command (`dev <file> [--env <env>] --input <json>`) with the re-exec
335
+ * guard set, so it falls straight through to {@link runDev}.
336
+ *
337
+ * tsx is resolved via `npx`; the child's cwd is the project root so its
338
+ * node_modules tsx is preferred. `env` is optional — when omitted the child
339
+ * runs the bare local path (no platform fetch, empty ctx.vars).
340
+ */
341
+ function reexecUnderTsx(file, env, input, projectDir) {
342
+ // dist/src/commands/dev.js → CLI entry is dist/index.js (two dirs up).
343
+ const cliEntry = path_1.default.resolve(__dirname, '..', 'index.js');
344
+ const args = ['tsx', cliEntry, 'dev', file, '--input', input];
345
+ if (env) {
346
+ args.push('--env', env);
44
347
  }
45
- // Validate input JSON if provided
348
+ const result = (0, child_process_1.spawnSync)('npx', args, {
349
+ stdio: 'inherit',
350
+ cwd: projectDir,
351
+ env: { ...process.env, [TSX_REEXEC_GUARD]: '1' },
352
+ });
353
+ if (result.error) {
354
+ const code = result.error.code;
355
+ if (code === 'ENOENT') {
356
+ console.error(chalk_1.default.red('npx not found. Make sure Node.js is installed.'));
357
+ }
358
+ else {
359
+ console.error(chalk_1.default.red(`Failed to start tsx loader: ${result.error.message}`));
360
+ }
361
+ return 1;
362
+ }
363
+ return result.status ?? 1;
364
+ }
365
+ /**
366
+ * Handler for `solidactions dev <file> [--env <env>]`: run the workflow LOCALLY
367
+ * through the in-process invoke() engine.
368
+ *
369
+ * With `--env`: pulls declared variable-mappings from the SA API and builds
370
+ * `ctx.vars` for the chosen environment. WITHOUT `--env`: NO platform fetch and
371
+ * `ctx.vars` starts empty `{}` — the host `process.env` is NEVER leaked into the
372
+ * workflow. Either way the workflow runs against the SDK mock backend via
373
+ * {@link runDev}. `.ts` entries are loaded by re-exec'ing under `npx tsx`.
374
+ */
375
+ async function dev(file, options) {
376
+ const env = options.env;
46
377
  const input = options.input || '{}';
378
+ const filePath = path_1.default.resolve(file);
379
+ if (!fs_1.default.existsSync(filePath)) {
380
+ console.error(chalk_1.default.red(`File not found: ${filePath}`));
381
+ process.exit(1);
382
+ }
47
383
  try {
48
384
  JSON.parse(input);
49
385
  }
@@ -51,65 +387,55 @@ async function dev(file, options) {
51
387
  console.error(chalk_1.default.red('Invalid JSON input. Use -i \'{"key": "value"}\''));
52
388
  process.exit(1);
53
389
  }
54
- // Write temp bootstrap file
55
- const bootstrapPath = path_1.default.join(projectDir, '.solidactions-dev-bootstrap.mjs');
56
- const bootstrapContent = generateBootstrap(filePath, input);
57
- try {
58
- fs_1.default.writeFileSync(bootstrapPath, bootstrapContent);
59
- console.log(chalk_1.default.blue('SolidActions local dev mode'));
60
- console.log(chalk_1.default.gray(`File: ${path_1.default.relative(projectDir, filePath)}`));
61
- console.log(chalk_1.default.gray(`Input: ${input}`));
62
- console.log(chalk_1.default.gray('---'));
63
- // Spawn npx tsx with the bootstrap file
64
- const child = (0, child_process_1.spawn)('npx', ['tsx', bootstrapPath], {
65
- stdio: 'inherit',
66
- cwd: projectDir,
67
- });
68
- child.on('error', (err) => {
69
- cleanup(bootstrapPath);
70
- if (err.code === 'ENOENT') {
71
- console.error(chalk_1.default.red('npx not found. Make sure Node.js is installed.'));
390
+ // .ts entry under plain node has no loader — re-exec under tsx, then return.
391
+ if (!hasTsLoader() && /\.(ts|tsx|mts|cts)$/.test(filePath)) {
392
+ const projectDir = findProjectRoot(filePath) ?? process.cwd();
393
+ // Surface a clear slug-resolution error here (before forking) rather
394
+ // than letting the child fail with an opaque exit code. Only relevant
395
+ // when an env was requested (bare runs do no platform fetch).
396
+ if (env) {
397
+ try {
398
+ resolveProjectSlug(filePath, env);
72
399
  }
73
- else {
74
- console.error(chalk_1.default.red(`Failed to start: ${err.message}`));
400
+ catch (err) {
401
+ console.error(chalk_1.default.red(`Failed: ${err.message}`));
402
+ process.exit(1);
75
403
  }
76
- process.exit(1);
77
- });
78
- child.on('exit', (code) => {
79
- cleanup(bootstrapPath);
80
- process.exit(code ?? 1);
81
- });
82
- // Also clean up on signals
83
- process.on('SIGINT', () => {
84
- cleanup(bootstrapPath);
85
- child.kill('SIGINT');
86
- });
87
- process.on('SIGTERM', () => {
88
- cleanup(bootstrapPath);
89
- child.kill('SIGTERM');
90
- });
404
+ }
405
+ process.exit(reexecUnderTsx(file, env, input, projectDir));
406
+ }
407
+ // In-process path (already under a TS loader, or a .js/.mjs entry).
408
+ let result;
409
+ try {
410
+ result = await runDev({ entry: filePath, input, env });
91
411
  }
92
412
  catch (err) {
93
- cleanup(bootstrapPath);
94
413
  console.error(chalk_1.default.red(`Failed: ${err.message}`));
95
414
  process.exit(1);
96
415
  }
97
- }
98
- function generateBootstrap(workflowFile, input) {
99
- // Use forward slashes for the import path (works cross-platform in ESM)
100
- const importPath = workflowFile.replace(/\\/g, '/');
101
- return `// Auto-generated by solidactions dev - do not commit
102
- import { createMockServer } from '@solidactions/sdk/testing';
103
-
104
- const server = await createMockServer();
105
-
106
- process.env.SOLIDACTIONS_API_URL = server.baseUrl;
107
- process.env.SOLIDACTIONS_API_KEY = 'local-dev';
108
- process.env.WORKFLOW_INPUT = ${JSON.stringify(input)};
109
-
110
- // Import the workflow file - this triggers SolidActions.run() at module level
111
- await import(${JSON.stringify(importPath)});
112
- `;
416
+ if (result.stdout) {
417
+ console.log(result.stdout);
418
+ }
419
+ if (result.stderr) {
420
+ console.error(chalk_1.default.yellow(result.stderr));
421
+ }
422
+ const r = result.result;
423
+ if (r.status === 'completed') {
424
+ console.log(chalk_1.default.green('✓ completed'));
425
+ console.log(chalk_1.default.gray('Output:'), JSON.stringify(r.output));
426
+ process.exit(0);
427
+ }
428
+ if (r.status === 'suspended') {
429
+ console.log(chalk_1.default.yellow(`Workflow suspended: ${r.reason ?? 'unknown'}`));
430
+ process.exit(0);
431
+ }
432
+ if (r.status === 'cancelled') {
433
+ console.log(chalk_1.default.yellow('Workflow cancelled'));
434
+ process.exit(1);
435
+ }
436
+ // failed
437
+ console.error(chalk_1.default.red(`✗ failed (${r.phase ?? 'run'}):`), r.error?.message ?? String(r.error));
438
+ process.exit(1);
113
439
  }
114
440
  function findProjectRoot(startPath) {
115
441
  let dir = path_1.default.dirname(startPath);
@@ -124,13 +450,3 @@ function findProjectRoot(startPath) {
124
450
  }
125
451
  return null;
126
452
  }
127
- function cleanup(filePath) {
128
- try {
129
- if (fs_1.default.existsSync(filePath)) {
130
- fs_1.default.unlinkSync(filePath);
131
- }
132
- }
133
- catch {
134
- // Ignore cleanup errors
135
- }
136
- }
@@ -4,6 +4,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.oauthActionsShow = oauthActionsShow;
7
+ exports.buildSnippet = buildSnippet;
7
8
  const axios_1 = __importDefault(require("axios"));
8
9
  const chalk_1 = __importDefault(require("chalk"));
9
10
  const api_1 = require("../utils/api");
@@ -18,7 +19,7 @@ async function oauthActionsShow(platform, actionId, options) {
18
19
  process.stdout.write(JSON.stringify(action, null, 2) + '\n');
19
20
  return;
20
21
  }
21
- renderHumanReadable(action);
22
+ renderHumanReadable(action, options);
22
23
  }
23
24
  catch (error) {
24
25
  if (error.response?.status === 404) {
@@ -36,7 +37,7 @@ async function oauthActionsShow(platform, actionId, options) {
36
37
  process.exit(1);
37
38
  }
38
39
  }
39
- function renderHumanReadable(action) {
40
+ function renderHumanReadable(action, options) {
40
41
  const method = (action.method || 'GET').toUpperCase();
41
42
  const ioSchema = action.io_schema || {};
42
43
  const example = ioSchema.ioExample?.input;
@@ -73,15 +74,125 @@ function renderHumanReadable(action) {
73
74
  console.log('');
74
75
  }
75
76
  console.log(chalk_1.default.bold('Paste-ready snippet:'));
76
- console.log(chalk_1.default.gray(indent(buildSnippet(action))));
77
+ if (options.legacyEnv) {
78
+ console.log(chalk_1.default.yellow(' (--legacy-env: deprecated form, will be removed in a future release)'));
79
+ }
80
+ console.log(chalk_1.default.gray(indent(buildSnippet(action, options.var, options.legacyEnv))));
81
+ }
82
+ /**
83
+ * Render a fetch() call against the SA proxy using the ctx.vars contract.
84
+ *
85
+ * The proxy requires:
86
+ * - Authorization: Bearer <proxyToken> (per-trigger scoped token from ctx.vars)
87
+ * - X-OAuth-Connection-Key: <key> (connection identifier from ctx.vars)
88
+ * - X-OAuth-Action-Id: <action_id> (the action being called)
89
+ *
90
+ * For modifier actions the body MUST include `connectionKey` alongside user params.
91
+ *
92
+ * Pass --var <NAME> to set the ctx.vars variable name (defaults to YOUR_CONNECTION).
93
+ * Pass --legacy-env to emit the old process.env form (deprecated, will be removed).
94
+ */
95
+ function buildSnippet(action, varName, legacyEnv) {
96
+ if (legacyEnv) {
97
+ return buildLegacySnippet(action);
98
+ }
99
+ return buildCtxVarsSnippet(action, varName);
100
+ }
101
+ /**
102
+ * New ctx.vars-based snippet (the live proxy contract).
103
+ *
104
+ * proxyUrl — the SA proxy base URL injected by the runtime at workflow start
105
+ * proxyToken — a per-trigger scoped Bearer token, NOT a long-lived secret
106
+ * key — the connection key used by the proxy to look up the OAuth credentials
107
+ *
108
+ * Headers sent to the proxy:
109
+ * Authorization: Bearer ${ctx.vars.<VAR>.proxyToken}
110
+ * X-OAuth-Connection-Key: ${ctx.vars.<VAR>.key}
111
+ * X-OAuth-Action-Id: '<action_id>'
112
+ *
113
+ * Do NOT include X-SA-Connection — that header is stripped by the proxy (STRIPPED_INBOUND).
114
+ */
115
+ function buildCtxVarsSnippet(action, varName) {
116
+ const method = (action.method || 'GET').toUpperCase();
117
+ const platform = action.platform;
118
+ const varRef = varName || 'YOUR_CONNECTION';
119
+ const example = action.io_schema?.ioExample?.input;
120
+ const pathTemplate = (action.path || '').replace(/\{\{\s*([^}]+?)\s*\}\}/g, (_m, name) => '${' + name.trim() + '}');
121
+ const queryEntries = example?.query ? Object.entries(example.query) : [];
122
+ const queryString = queryEntries.length
123
+ ? '?' + queryEntries.flatMap(([k, v]) => {
124
+ const encodedKey = encodeURIComponent(k);
125
+ const items = Array.isArray(v) ? v : [v];
126
+ return items.map((item) => `${encodedKey}=\${encodeURIComponent(${JSON.stringify(item)})}`);
127
+ }).join('&')
128
+ : '';
129
+ // Build the headers block: Authorization, X-OAuth-Connection-Key, X-OAuth-Action-Id,
130
+ // then any non-proxy-managed example headers, then Content-Type if body is present.
131
+ const proxyHeaderLines = [
132
+ ` 'Authorization': \`Bearer \${ctx.vars.${varRef}.proxyToken}\`,`,
133
+ ` 'X-OAuth-Connection-Key': ctx.vars.${varRef}.key,`,
134
+ ` 'X-OAuth-Action-Id': ${JSON.stringify(action.action_id)},`,
135
+ ];
136
+ const exampleHeaders = example?.headers || {};
137
+ for (const [k, v] of Object.entries(exampleHeaders)) {
138
+ if (isProxyManagedHeader(k))
139
+ continue;
140
+ proxyHeaderLines.push(` ${JSON.stringify(k)}: ${JSON.stringify(v)},`);
141
+ }
142
+ if (example?.body !== undefined && example.body !== null) {
143
+ const hasContentType = Object.keys(exampleHeaders).some(k => k.toLowerCase() === 'content-type');
144
+ if (!hasContentType) {
145
+ proxyHeaderLines.push(` 'Content-Type': 'application/json',`);
146
+ }
147
+ }
148
+ const headersBlock = proxyHeaderLines.join('\n');
149
+ const lines = [];
150
+ lines.push('const res = await fetch(');
151
+ lines.push(` \`\${ctx.vars.${varRef}.proxyUrl}/${platform}${pathTemplate}${queryString}\`,`);
152
+ lines.push(' {');
153
+ lines.push(` method: '${method}',`);
154
+ lines.push(' headers: {');
155
+ lines.push(headersBlock);
156
+ lines.push(' },');
157
+ if (example?.body !== undefined && example.body !== null) {
158
+ // Modifier actions: body already contains connectionKey from the API example.
159
+ // In the new contract the proxy uses X-OAuth-Connection-Key for routing, but
160
+ // modifier actions additionally require connectionKey IN the body so the
161
+ // upstream provider receives it. Substitute with a live ctx.vars reference.
162
+ const bodyObj = example.body;
163
+ const isModifier = typeof bodyObj === 'object' && bodyObj !== null && 'connectionKey' in bodyObj;
164
+ let bodyExpr;
165
+ if (isModifier) {
166
+ // Build the body object manually so we can emit `ctx.vars.<VAR>.key`
167
+ // (an unquoted JS expression) as the connectionKey value rather than
168
+ // a string literal.
169
+ const otherFields = Object.entries(bodyObj)
170
+ .filter(([k]) => k !== 'connectionKey')
171
+ .map(([k, v]) => ` ${JSON.stringify(k)}: ${JSON.stringify(v)}`)
172
+ .join(',\n');
173
+ const otherPart = otherFields ? `,\n${otherFields}` : '';
174
+ bodyExpr = `{\n connectionKey: ctx.vars.${varRef}.key${otherPart}\n}`;
175
+ }
176
+ else {
177
+ bodyExpr = JSON.stringify(bodyObj, null, 2);
178
+ }
179
+ const bodyLines = bodyExpr
180
+ .split('\n')
181
+ .map((l, i) => (i === 0 ? l : ' ' + l))
182
+ .join('\n');
183
+ lines.push(` body: JSON.stringify(${bodyLines}),`);
184
+ }
185
+ lines.push(' }');
186
+ lines.push(');');
187
+ lines.push('const data = await res.json();');
188
+ return lines.join('\n');
77
189
  }
78
190
  /**
79
- * Render a fetch() call against the SA proxy with `{{name}}` path placeholders
80
- * rewritten to `${name}` template-literal slots, query parameters appended,
81
- * and the example body inlined verbatim (so the AI substitutes values into the
82
- * known-good shape rather than inferring it from JSON Schema).
191
+ * Legacy process.env-based snippet (deprecated).
192
+ * Emitted only when --legacy-env is passed.
193
+ * This form no longer matches the live proxy contract use the ctx.vars form.
83
194
  */
84
- function buildSnippet(action) {
195
+ function buildLegacySnippet(action) {
85
196
  const method = (action.method || 'GET').toUpperCase();
86
197
  const platform = action.platform;
87
198
  const platformEnv = platform.toUpperCase().replace(/-/g, '_');
@@ -153,5 +264,8 @@ function isProxyManagedHeader(name) {
153
264
  const lower = name.toLowerCase();
154
265
  return lower === 'authorization'
155
266
  || lower.startsWith('x-pica-')
156
- || lower.startsWith('x-one-');
267
+ || lower.startsWith('x-one-')
268
+ || lower === 'x-sa-connection'
269
+ || lower === 'x-oauth-connection-key'
270
+ || lower === 'x-oauth-action-id';
157
271
  }
@@ -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');
@@ -123,7 +124,11 @@ program
123
124
  .description('Run a workflow locally using an in-memory mock server (no deploy needed)')
124
125
  .argument('<file>', 'Workflow file to run (e.g., src/simple-steps.ts)')
125
126
  .option('-i, --input <json>', 'JSON input for the workflow', '{}')
127
+ .option('-e, --env <env>', 'Pull platform variables for this environment (e.g. dev, staging, production)')
126
128
  .action((file, options) => {
129
+ // Unified in-process invoke path. With --env: fetch declared vars, build
130
+ // ctx.vars, invoke locally. Without --env: NO platform fetch, ctx.vars is
131
+ // empty {} (the host process.env is never leaked into the workflow).
127
132
  (0, dev_1.dev)(file, options);
128
133
  });
129
134
  // =============================================================================
@@ -365,6 +370,8 @@ oauthActionsCmd
365
370
  .command('show <platform> <action-id>')
366
371
  .description('Show full schema, example body, and a paste-ready fetch snippet for one action')
367
372
  .option('--json', 'Emit raw JSON for AI/script consumption')
373
+ .option('--var <NAME>', 'ctx.vars variable name for the connection (default: YOUR_CONNECTION)')
374
+ .option('--legacy-env', 'Emit the deprecated process.env-based snippet (will be removed in a future release)')
368
375
  .action((platform, actionId, options) => {
369
376
  (0, oauth_actions_show_1.oauthActionsShow)(platform, actionId, options);
370
377
  });
@@ -385,6 +392,19 @@ ai
385
392
  .option('--all', 'Install all available examples')
386
393
  .option('--overwrite', 'Overwrite existing examples without warning')
387
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
+ });
388
408
  program.parseAsync().catch((err) => {
389
409
  console.error(chalk_1.default.red(err.message ?? String(err)));
390
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.10.3",
3
+ "version": "1.12.0",
4
4
  "description": "SolidActions CLI - Deploy and manage workflow automation",
5
5
  "main": "dist/index.js",
6
6
  "bin": {