rainskills 0.1.0 → 0.1.2

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.
Files changed (116) hide show
  1. package/README.md +156 -420
  2. package/SKILL.md +37 -7
  3. package/agents/openai.yaml +2 -2
  4. package/bin/rainskills-tools.js +712 -0
  5. package/bin/rainskills.js +1109 -4
  6. package/install.sh +536 -63
  7. package/marketplace/rainskills/.claude-plugin/plugin.json +2 -2
  8. package/marketplace/rainskills/.codex-plugin/plugin.json +5 -5
  9. package/marketplace/rainskills/skills/rainskills/SKILL.md +38 -8
  10. package/package.json +14 -5
  11. package/pi/rainskills-mcp.ts +214 -1850
  12. package/rainbond-app-assistant/SKILL.md +123 -49
  13. package/rainbond-app-assistant/evals/01-linked-topology-missing.response.md +3 -3
  14. package/rainbond-app-assistant/evals/02-linked-topology-building.response.md +3 -3
  15. package/rainbond-app-assistant/evals/03-runtime-healthy-delivery-unknown.response.md +3 -3
  16. package/rainbond-app-assistant/evals/04-delivered-stop-without-promotion.expected.yaml +1 -1
  17. package/rainbond-app-assistant/evals/04-delivered-stop-without-promotion.response.md +1 -1
  18. package/rainbond-app-assistant/evals/05-delivered-explicit-dev-to-test.response.md +3 -3
  19. package/rainbond-app-assistant/evals/06-manual-validation-stop.expected.yaml +1 -1
  20. package/rainbond-app-assistant/evals/06-manual-validation-stop.response.md +3 -3
  21. package/rainbond-app-assistant/evals/07-cluster-capacity-blocked.response.md +4 -4
  22. package/rainbond-app-assistant/evals/08-code-build-handoff-stop.response.md +2 -2
  23. package/rainbond-app-assistant/evals/09-source-backed-failure-no-fallback.response.md +2 -2
  24. package/rainbond-app-assistant/evals/10-identity-ambiguous-stop.response.md +3 -3
  25. package/rainbond-app-assistant/evals/12-complex-suite-needs-descriptor-stop.response.md +2 -2
  26. package/rainbond-app-assistant/evals/13-explicit-git-url-locks-source.response.md +2 -2
  27. package/rainbond-app-assistant/evals/14-strategy-switch-cleans-residue.response.md +2 -2
  28. package/rainbond-app-assistant/evals/15-turn-discipline-reuse-alias.response.md +2 -2
  29. package/rainbond-app-assistant/references/operational-reference.md +77 -0
  30. package/rainbond-app-assistant/references/output-contract.md +454 -0
  31. package/rainbond-app-assistant/references/product-object-model.md +1558 -0
  32. package/rainbond-app-assistant/references/workflow-rules.md +343 -0
  33. package/rainbond-app-assistant/schemas/app-assistant-result.schema.yaml +6 -1
  34. package/rainbond-app-version-assistant/SKILL.md +57 -18
  35. package/rainbond-delivery-verifier/SKILL.md +86 -13
  36. package/rainbond-env-sync/SKILL.md +52 -11
  37. package/rainbond-fullstack-bootstrap/SKILL.md +79 -11
  38. package/rainbond-fullstack-bootstrap/evals/01-reuse-only.response.md +1 -1
  39. package/rainbond-fullstack-bootstrap/evals/02-deferred-dependency.response.md +1 -1
  40. package/rainbond-fullstack-bootstrap/evals/03-capacity-blocked.response.md +1 -1
  41. package/rainbond-fullstack-bootstrap/evals/04-template-skip.response.md +1 -1
  42. package/rainbond-fullstack-bootstrap/evals/05-source-ambiguity-stop.response.md +1 -1
  43. package/rainbond-fullstack-bootstrap/evals/06-external-artifact-unreachable.response.md +1 -1
  44. package/rainbond-fullstack-bootstrap/evals/07-provider-connection-contract.response.md +1 -1
  45. package/rainbond-fullstack-bootstrap/evals/08-explicit-dependency-after-manual-components.response.md +1 -1
  46. package/rainbond-fullstack-bootstrap/evals/09-inferred-provider-consumer-gate.response.md +1 -1
  47. package/rainbond-fullstack-bootstrap/evals/10-compose-service-name-not-hostname.response.md +1 -1
  48. package/rainbond-fullstack-bootstrap/evals/11-reverse-proxy-not-dropped.response.md +1 -1
  49. package/rainbond-fullstack-bootstrap/evals/12-optional-services-disclose-not-deploy.response.md +1 -1
  50. package/rainbond-fullstack-bootstrap/evals/13-env-reference-dependency-completeness.response.md +1 -1
  51. package/rainbond-fullstack-bootstrap/evals/14-client-package-upload.expected.yaml +1 -1
  52. package/rainbond-fullstack-bootstrap/evals/14-client-package-upload.response.md +3 -3
  53. package/rainbond-fullstack-bootstrap/evals/15-unfinished-dockerfile-recovery.expected.yaml +25 -0
  54. package/rainbond-fullstack-bootstrap/evals/15-unfinished-dockerfile-recovery.response.md +44 -0
  55. package/rainbond-fullstack-bootstrap/evals/16-complete-cnb-recreate-confirmed.expected.yaml +25 -0
  56. package/rainbond-fullstack-bootstrap/evals/16-complete-cnb-recreate-confirmed.response.md +44 -0
  57. package/rainbond-fullstack-bootstrap/modules/10-context-loading.md +8 -8
  58. package/rainbond-fullstack-bootstrap/modules/30-creation-rules.md +10 -10
  59. package/rainbond-fullstack-bootstrap/modules/40-source-and-package-rules.md +13 -9
  60. package/rainbond-fullstack-bootstrap/modules/50-workflow-and-convergence.md +6 -6
  61. package/rainbond-fullstack-bootstrap/modules/60-verification-and-handoffs.md +4 -4
  62. package/rainbond-fullstack-bootstrap/modules/70-output-contract.md +3 -3
  63. package/rainbond-fullstack-bootstrap/references/quick-reference.md +6 -6
  64. package/rainbond-fullstack-bootstrap/references/source-build-parameter-guide.md +6 -6
  65. package/rainbond-fullstack-bootstrap/references/sql-init-recipe.md +3 -3
  66. package/rainbond-fullstack-bootstrap/schemas/bootstrap-result.schema.yaml +3 -2
  67. package/rainbond-fullstack-bootstrap/scripts/upload_local_package.py +1 -1
  68. package/rainbond-fullstack-bootstrap/scripts/validate_bootstrap_output.py +2 -2
  69. package/rainbond-fullstack-troubleshooter/SKILL.md +103 -13
  70. package/rainbond-fullstack-troubleshooter/evals/10-config-file-configmap-missing.expected.yaml +21 -0
  71. package/rainbond-fullstack-troubleshooter/evals/10-config-file-configmap-missing.response.md +51 -0
  72. package/rainbond-fullstack-troubleshooter/references/operational-reference.md +70 -0
  73. package/rainbond-fullstack-troubleshooter/references/output-contract.md +259 -0
  74. package/rainbond-fullstack-troubleshooter/references/root-cause-rules.md +210 -0
  75. package/rainbond-fullstack-troubleshooter/schemas/troubleshoot-result.schema.yaml +3 -1
  76. package/rainbond-fullstack-troubleshooter/scripts/validate_troubleshoot_output.py +22 -1
  77. package/rainbond-platform-installer/SKILL.md +73 -9
  78. package/rainbond-platform-installer/agents/openai.yaml +2 -2
  79. package/rainbond-platform-installer/references/installation-policy.json +35 -1
  80. package/rainbond-platform-installer/references/installation-policy.md +33 -8
  81. package/rainbond-platform-installer/references/troubleshooting.md +3 -2
  82. package/rainbond-platform-installer/scripts/auto-update.js +495 -0
  83. package/rainbond-platform-installer/scripts/console-origin.js +159 -0
  84. package/rainbond-platform-installer/scripts/environment-credentials.js +225 -0
  85. package/rainbond-platform-installer/scripts/environment-registry.js +349 -0
  86. package/rainbond-platform-installer/scripts/existing-kubernetes-installer.js +1370 -0
  87. package/rainbond-platform-installer/scripts/host-cluster-installer.js +2143 -0
  88. package/rainbond-platform-installer/scripts/local-runtime-commands.js +180 -0
  89. package/rainbond-platform-installer/scripts/local-runtime.js +18 -0
  90. package/rainbond-platform-installer/scripts/mcp-router.js +212 -0
  91. package/rainbond-platform-installer/scripts/mcp-server.js +79 -0
  92. package/rainbond-platform-installer/scripts/platform-installer.js +694 -255
  93. package/rainbond-platform-installer/scripts/platform-routing.js +326 -0
  94. package/rainbond-platform-installer/scripts/runtime-credentials.js +163 -0
  95. package/rainbond-platform-installer/scripts/runtime-intents.js +251 -0
  96. package/rainbond-platform-installer/scripts/runtime-operations.js +383 -0
  97. package/rainbond-platform-installer/scripts/runtime-state.js +656 -0
  98. package/rainbond-platform-installer/scripts/secure-state.js +59 -13
  99. package/rainbond-platform-installer/scripts/ssh-key-setup.js +242 -0
  100. package/rainbond-platform-installer/scripts/telemetry.js +244 -0
  101. package/rainbond-platform-installer/scripts/user-message.js +125 -0
  102. package/rainbond-platform-installer/scripts/windows-auth.js +1 -1
  103. package/rainbond-platform-installer/scripts/windows-client-config.js +101 -22
  104. package/rainbond-platform-installer/scripts/windows-onboarding.js +179 -83
  105. package/rainbond-platform-installer/scripts/windows-platform.js +3 -3
  106. package/rainbond-platform-installer/scripts/windows-platform.ps1 +264 -28
  107. package/rainbond-platform-installer/scripts/windows-read-user-environment.ps1 +16 -0
  108. package/rainbond-platform-query/SKILL.md +88 -0
  109. package/rainbond-platform-query/agents/openai.yaml +4 -0
  110. package/rainbond-platform-query/evals/01-current-enterprise.expected.yaml +10 -0
  111. package/rainbond-project-init/SKILL.md +68 -9
  112. package/rainbond-project-init/references/manifest-rules.md +487 -0
  113. package/rainbond-project-init/references/operational-reference.md +74 -0
  114. package/rainbond-project-init/references/output-contract.md +272 -0
  115. package/rainbond-project-init/references/workflow-and-verification.md +85 -0
  116. package/rainbond-template-installer/SKILL.md +80 -16
@@ -0,0 +1,712 @@
1
+ #!/usr/bin/env node
2
+
3
+ const fs = require("node:fs");
4
+ const http = require("node:http");
5
+ const https = require("node:https");
6
+ const os = require("node:os");
7
+ const path = require("node:path");
8
+ const { createHash, randomUUID } = require("node:crypto");
9
+
10
+ const REQUEST_TIMEOUT_MS = 180_000;
11
+ const INPUT_MAX_BYTES = 1024 * 1024;
12
+ const RESPONSE_MAX_BYTES = 10 * 1024 * 1024;
13
+ const STDOUT_MAX_BYTES = 128 * 1024;
14
+ const CATALOG_TTL_MS = 5 * 60 * 1000;
15
+ const PROTOCOL_VERSION = "2025-03-26";
16
+ const ENDPOINT_PATH = "/console/mcp/rainskills/api/query";
17
+ const CLI_VERSION = "2.1.0";
18
+ const CONFIG_DIRECTORY = ".rainbond";
19
+ const CREDENTIALS_FILENAME = "credentials.env";
20
+ const LEGACY_CREDENTIALS_FILENAME = "mcp.env";
21
+ const CATALOG_FILENAME = "capabilities.json";
22
+ const OPERATIONS_DIRECTORY = "operations";
23
+ const OPERATION_ID_PATTERN = /^[a-f0-9]{8}-[a-f0-9-]{27,}$/;
24
+ const SENSITIVE_RESPONSE_KEY_PATTERN = /(?:authorization|jwt|token|password|secret|credential|private[_-]?key|key[_-]?file|certificate|cert[_-]?file|ssl[_-]?ca[_-]?cert)/i;
25
+ const RISK_POLICY = Object.freeze({
26
+ version: "1",
27
+ readPrefixes: [
28
+ "rainbond_query_", "rainbond_get_", "rainbond_list_", "rainbond_check_",
29
+ "rainbond_describe_", "rainbond_validate_", "rainbond_verify_", "rainbond_search_",
30
+ ],
31
+ destructiveFragments: ["delete", "remove", "purge", "destroy"],
32
+ });
33
+
34
+ const EXIT = Object.freeze({
35
+ USAGE: 2,
36
+ CONFIG: 3,
37
+ TRANSPORT: 4,
38
+ TOOL: 5,
39
+ });
40
+
41
+ class BridgeError extends Error {
42
+ constructor(message, exitCode, payload) {
43
+ super(message);
44
+ this.exitCode = exitCode;
45
+ this.payload = payload;
46
+ }
47
+ }
48
+
49
+ function parseShellValue(value) {
50
+ let offset = 0;
51
+ let result = "";
52
+ while (offset < value.length) {
53
+ const quote = value[offset];
54
+ if (quote === "'") {
55
+ const end = value.indexOf("'", offset + 1);
56
+ if (end === -1) return null;
57
+ result += value.slice(offset + 1, end);
58
+ offset = end + 1;
59
+ continue;
60
+ }
61
+ if (quote === '"') {
62
+ const end = value.indexOf('"', offset + 1);
63
+ if (end === -1) return null;
64
+ result += value.slice(offset + 1, end).replace(/\\([\\"$`])/g, "$1");
65
+ offset = end + 1;
66
+ continue;
67
+ }
68
+ const match = value.slice(offset).match(/^[^\s'"#]+/);
69
+ if (!match) return null;
70
+ result += match[0];
71
+ offset += match[0].length;
72
+ }
73
+ return result;
74
+ }
75
+
76
+ function parseEnvFile(contents) {
77
+ const values = {};
78
+ for (const line of contents.split(/\r?\n/)) {
79
+ const match = line.match(/^\s*(?:export\s+)?(RAINBOND_URL|RAINBOND_JWT|RAINBOND_ALLOW_INSECURE_HTTP)\s*=\s*(.*?)\s*$/);
80
+ if (!match) continue;
81
+ const parsed = parseShellValue(match[2]);
82
+ if (parsed !== null) values[match[1]] = parsed;
83
+ }
84
+ return values;
85
+ }
86
+
87
+ function readPrivateEnvFile(configPath) {
88
+ let stat;
89
+ try {
90
+ stat = fs.lstatSync(configPath);
91
+ } catch (error) {
92
+ if (error.code === "ENOENT") return {};
93
+ throw new BridgeError("unable to inspect Rainbond configuration", EXIT.CONFIG);
94
+ }
95
+ if (!stat.isFile() || stat.isSymbolicLink() || (stat.mode & 0o077) !== 0) {
96
+ throw new BridgeError("Rainbond configuration must be a private regular file", EXIT.CONFIG);
97
+ }
98
+ try {
99
+ return parseEnvFile(fs.readFileSync(configPath, "utf8"));
100
+ } catch (_error) {
101
+ throw new BridgeError("unable to read Rainbond configuration", EXIT.CONFIG);
102
+ }
103
+ }
104
+
105
+ function loadConfig({ env = process.env, homeDir = os.homedir(), includeRuntime = false } = {}) {
106
+ const configDirectory = path.join(homeDir, CONFIG_DIRECTORY);
107
+ const credentialsPath = path.join(configDirectory, CREDENTIALS_FILENAME);
108
+ const legacyCredentialsPath = path.join(configDirectory, LEGACY_CREDENTIALS_FILENAME);
109
+ const fileValues = readPrivateEnvFile(credentialsPath);
110
+ const legacyValues = Object.keys(fileValues).length === 0
111
+ ? readPrivateEnvFile(legacyCredentialsPath)
112
+ : {};
113
+
114
+ const baseUrl = env.RAINBOND_URL || fileValues.RAINBOND_URL || legacyValues.RAINBOND_URL;
115
+ const jwt = env.RAINBOND_JWT || fileValues.RAINBOND_JWT || legacyValues.RAINBOND_JWT;
116
+ if (!baseUrl || !jwt) {
117
+ throw new BridgeError("RAINBOND_URL and RAINBOND_JWT are required", EXIT.CONFIG);
118
+ }
119
+
120
+ let parsed;
121
+ try {
122
+ parsed = new URL(baseUrl);
123
+ } catch (_error) {
124
+ throw new BridgeError("RAINBOND_URL must be a valid HTTP(S) URL", EXIT.CONFIG);
125
+ }
126
+ if (!["http:", "https:"].includes(parsed.protocol) || parsed.username ||
127
+ parsed.password || parsed.search || parsed.hash) {
128
+ throw new BridgeError("RAINBOND_URL must be a safe HTTP(S) URL", EXIT.CONFIG);
129
+ }
130
+ const allowInsecureHttp = (env.RAINBOND_ALLOW_INSECURE_HTTP ||
131
+ fileValues.RAINBOND_ALLOW_INSECURE_HTTP ||
132
+ legacyValues.RAINBOND_ALLOW_INSECURE_HTTP) === "true";
133
+ if (parsed.protocol === "http:" && !allowInsecureHttp) {
134
+ throw new BridgeError(
135
+ "RAINBOND_URL must use HTTPS; explicitly authorize insecure HTTP during installation",
136
+ EXIT.CONFIG
137
+ );
138
+ }
139
+ const config = {
140
+ baseUrl: parsed.toString().replace(/\/$/, ""),
141
+ jwt,
142
+ };
143
+ if (includeRuntime) {
144
+ config.homeDir = homeDir;
145
+ config.allowInsecureHttp = allowInsecureHttp;
146
+ config.isInsecureHttp = parsed.protocol === "http:";
147
+ }
148
+ return config;
149
+ }
150
+
151
+ function parseCommand(args) {
152
+ const command = args[0];
153
+ if (command === "status" && args.length === 1) return { command };
154
+ if (command === "list") {
155
+ if (args.length === 1) return { command };
156
+ if (args.length === 3 && args[1] === "--prefix" && args[2]) {
157
+ return { command, prefix: args[2] };
158
+ }
159
+ }
160
+ if (command === "describe" && args.length === 2 && args[1]) {
161
+ return { command, toolName: args[1] };
162
+ }
163
+ if (command === "read" && args.length === 4 && args[1] && args[2] === "--input" && args[3] === "-") {
164
+ return { command, toolName: args[1], input: args[3] };
165
+ }
166
+ if (command === "call" && args.length >= 4 && args[1] && args[2] === "--input" && args[3] === "-") {
167
+ if (args.length === 4) return { command, toolName: args[1], input: args[3] };
168
+ if (args.length === 6 && args[4] === "--confirm" && OPERATION_ID_PATTERN.test(args[5])) {
169
+ return { command, toolName: args[1], input: args[3], confirmation: args[5] };
170
+ }
171
+ }
172
+ throw new BridgeError("invalid command; use status, list, describe, read, or call", EXIT.USAGE);
173
+ }
174
+
175
+ function readArguments(input) {
176
+ if (input !== "-") {
177
+ throw new BridgeError("read and call accept JSON only through --input - (stdin)", EXIT.USAGE);
178
+ }
179
+ let contentsBuffer;
180
+ try {
181
+ const chunks = [];
182
+ let totalBytes = 0;
183
+ while (true) {
184
+ const chunk = Buffer.allocUnsafe(64 * 1024);
185
+ const bytesRead = fs.readSync(0, chunk, 0, chunk.length, null);
186
+ if (bytesRead === 0) break;
187
+ totalBytes += bytesRead;
188
+ if (totalBytes > INPUT_MAX_BYTES) {
189
+ throw new BridgeError("JSON input is too large", EXIT.USAGE);
190
+ }
191
+ chunks.push(chunk.subarray(0, bytesRead));
192
+ }
193
+ contentsBuffer = Buffer.concat(chunks, totalBytes);
194
+ } catch (_error) {
195
+ if (_error instanceof BridgeError) throw _error;
196
+ throw new BridgeError("unable to read JSON input", EXIT.USAGE);
197
+ }
198
+ let parsed;
199
+ try {
200
+ parsed = JSON.parse(contentsBuffer.toString("utf8"));
201
+ } catch (_error) {
202
+ throw new BridgeError("input must contain valid JSON", EXIT.USAGE);
203
+ }
204
+ if (!parsed || Array.isArray(parsed) || typeof parsed !== "object") {
205
+ throw new BridgeError("input JSON must be an object", EXIT.USAGE);
206
+ }
207
+ return parsed;
208
+ }
209
+
210
+ function assertPrivateDirectory(directory) {
211
+ try {
212
+ const stat = fs.lstatSync(directory);
213
+ if (!stat.isDirectory() || stat.isSymbolicLink()) return false;
214
+ if ((stat.mode & 0o077) !== 0) fs.chmodSync(directory, 0o700);
215
+ return true;
216
+ } catch (error) {
217
+ if (error.code !== "ENOENT") return false;
218
+ }
219
+ try {
220
+ fs.mkdirSync(directory, { recursive: true, mode: 0o700 });
221
+ fs.chmodSync(directory, 0o700);
222
+ return true;
223
+ } catch (_error) {
224
+ return false;
225
+ }
226
+ }
227
+
228
+ function catalogPath(config) {
229
+ return path.join(config.homeDir || os.homedir(), CONFIG_DIRECTORY, CATALOG_FILENAME);
230
+ }
231
+
232
+ function operationsPath(config) {
233
+ return path.join(config.homeDir || os.homedir(), CONFIG_DIRECTORY, OPERATIONS_DIRECTORY);
234
+ }
235
+
236
+ function classifyTool(toolName) {
237
+ const normalized = String(toolName || "").toLowerCase();
238
+ if (RISK_POLICY.destructiveFragments.some((fragment) => normalized.includes(fragment))) {
239
+ return "destructive";
240
+ }
241
+ if (RISK_POLICY.readPrefixes.some((prefix) => normalized.startsWith(prefix))) return "read";
242
+ return "write";
243
+ }
244
+
245
+ function operationFile(config, operationId) {
246
+ return path.join(operationsPath(config), `${operationId}.json`);
247
+ }
248
+
249
+ function operationClaimFile(config, operationId) {
250
+ return path.join(operationsPath(config), `${operationId}.claim`);
251
+ }
252
+
253
+ function canonicalJson(value) {
254
+ if (Array.isArray(value)) {
255
+ return `[${value.map(canonicalJson).join(",")}]`;
256
+ }
257
+ if (value && typeof value === "object") {
258
+ return `{${Object.keys(value)
259
+ .sort()
260
+ .map((key) => `${JSON.stringify(key)}:${canonicalJson(value[key])}`)
261
+ .join(",")}}`;
262
+ }
263
+ return JSON.stringify(value);
264
+ }
265
+
266
+ function argumentsDigest(value) {
267
+ return createHash("sha256").update(canonicalJson(value), "utf8").digest("hex");
268
+ }
269
+
270
+ function writeOperation(config, record) {
271
+ const directory = operationsPath(config);
272
+ if (!assertPrivateDirectory(directory)) {
273
+ throw new BridgeError("unable to write the local operation journal", EXIT.CONFIG);
274
+ }
275
+ const target = operationFile(config, record.operation_id);
276
+ try {
277
+ if (fs.existsSync(target) && fs.lstatSync(target).isSymbolicLink()) {
278
+ throw new Error("symbolic link");
279
+ }
280
+ const temporary = path.join(directory, `.${record.operation_id}.${process.pid}.tmp`);
281
+ fs.writeFileSync(temporary, `${JSON.stringify(record)}\n`, { mode: 0o600 });
282
+ fs.chmodSync(temporary, 0o600);
283
+ fs.renameSync(temporary, target);
284
+ } catch (_error) {
285
+ throw new BridgeError("unable to write the local operation journal", EXIT.CONFIG);
286
+ }
287
+ }
288
+
289
+ function readOperation(config, operationId) {
290
+ const target = operationFile(config, operationId);
291
+ try {
292
+ const stat = fs.lstatSync(target);
293
+ if (!stat.isFile() || stat.isSymbolicLink() || (stat.mode & 0o077) !== 0) return null;
294
+ const record = JSON.parse(fs.readFileSync(target, "utf8"));
295
+ return record && typeof record === "object" ? record : null;
296
+ } catch (_error) {
297
+ return null;
298
+ }
299
+ }
300
+
301
+ function prepareOperation(config, toolName, operationClass, argumentsValue) {
302
+ const operationId = randomUUID();
303
+ const record = {
304
+ operation_id: operationId,
305
+ tool_name: toolName,
306
+ operation_class: operationClass,
307
+ arguments_digest: argumentsDigest(argumentsValue),
308
+ policy_version: RISK_POLICY.version,
309
+ created_at: new Date().toISOString(),
310
+ status: "awaiting_confirmation",
311
+ };
312
+ writeOperation(config, record);
313
+ return record;
314
+ }
315
+
316
+ function confirmOperation(
317
+ config,
318
+ operationId,
319
+ toolName,
320
+ operationClass,
321
+ argumentsValue
322
+ ) {
323
+ const record = readOperation(config, operationId);
324
+ if (!record || record.status !== "awaiting_confirmation" || record.tool_name !== toolName ||
325
+ record.operation_class !== operationClass ||
326
+ record.arguments_digest !== argumentsDigest(argumentsValue)) {
327
+ throw new BridgeError("operation confirmation is missing or does not match this tool", EXIT.USAGE);
328
+ }
329
+ let claim;
330
+ try {
331
+ claim = fs.openSync(operationClaimFile(config, operationId), "wx", 0o600);
332
+ fs.writeFileSync(claim, `${JSON.stringify({ claimed_at: new Date().toISOString() })}\n`);
333
+ } catch (_error) {
334
+ throw new BridgeError("operation confirmation was already claimed", EXIT.USAGE);
335
+ } finally {
336
+ if (claim !== undefined) fs.closeSync(claim);
337
+ }
338
+ updateOperation(config, record, "executing");
339
+ return record;
340
+ }
341
+
342
+ function updateOperation(config, record, status) {
343
+ writeOperation(config, { ...record, status, updated_at: new Date().toISOString() });
344
+ }
345
+
346
+ function loadCachedCatalog(config) {
347
+ const target = catalogPath(config);
348
+ try {
349
+ const stat = fs.lstatSync(target);
350
+ if (!stat.isFile() || stat.isSymbolicLink() || (stat.mode & 0o077) !== 0) return null;
351
+ const payload = JSON.parse(fs.readFileSync(target, "utf8"));
352
+ if (!payload || payload.endpoint !== config.baseUrl || payload.protocol_version !== PROTOCOL_VERSION ||
353
+ !Array.isArray(payload.tools) || !Number.isFinite(payload.fetched_at)) return null;
354
+ const age = Date.now() - payload.fetched_at;
355
+ if (age < 0 || age > CATALOG_TTL_MS) return null;
356
+ return { tools: payload.tools, age };
357
+ } catch (_error) {
358
+ return null;
359
+ }
360
+ }
361
+
362
+ function writeCachedCatalog(config, tools) {
363
+ const directory = path.dirname(catalogPath(config));
364
+ if (!assertPrivateDirectory(directory)) return;
365
+ const target = catalogPath(config);
366
+ try {
367
+ if (fs.existsSync(target) && fs.lstatSync(target).isSymbolicLink()) return;
368
+ const temporary = path.join(directory, `.capabilities.${process.pid}.${Date.now()}.tmp`);
369
+ fs.writeFileSync(temporary, JSON.stringify({
370
+ endpoint: config.baseUrl,
371
+ protocol_version: PROTOCOL_VERSION,
372
+ fetched_at: Date.now(),
373
+ tools,
374
+ }), { mode: 0o600 });
375
+ fs.chmodSync(temporary, 0o600);
376
+ fs.renameSync(temporary, target);
377
+ } catch (_error) {
378
+ // Capability caching is an optimization and must never block a valid API call.
379
+ }
380
+ }
381
+
382
+ function fitOutput(value) {
383
+ const serialized = JSON.stringify(value);
384
+ if (Buffer.byteLength(serialized, "utf8") <= STDOUT_MAX_BYTES) return value;
385
+ const nextCursor = value && typeof value === "object" && !Array.isArray(value)
386
+ ? value.next_cursor || value.nextCursor || null
387
+ : null;
388
+ return {
389
+ truncated: true,
390
+ summary: "RainSkills CLI result exceeded the local output budget; request a narrower page, time range, or log tail.",
391
+ ...(nextCursor ? { next_cursor: nextCursor } : {}),
392
+ };
393
+ }
394
+
395
+ function sanitizeToolResult(value) {
396
+ if (Array.isArray(value)) return value.map(sanitizeToolResult);
397
+ if (!value || typeof value !== "object") return value;
398
+ const safe = {};
399
+ for (const [key, item] of Object.entries(value)) {
400
+ // Console destructive operations may return this one-time control value.
401
+ // It is not a credential and must remain available for the confirmation flow.
402
+ if (key !== "confirmation_token" && SENSITIVE_RESPONSE_KEY_PATTERN.test(key)) continue;
403
+ safe[key] = sanitizeToolResult(item);
404
+ }
405
+ return safe;
406
+ }
407
+
408
+ function collectArgumentRedactions(value, output = []) {
409
+ if (typeof value === "string" && value.length >= 4) output.push(value);
410
+ if (Array.isArray(value)) {
411
+ for (const item of value) collectArgumentRedactions(item, output);
412
+ } else if (value && typeof value === "object") {
413
+ for (const item of Object.values(value)) collectArgumentRedactions(item, output);
414
+ }
415
+ return output;
416
+ }
417
+
418
+ function redactPayload(value) {
419
+ if (Array.isArray(value)) return value.map(redactPayload);
420
+ if (!value || typeof value !== "object") return value;
421
+ const result = {};
422
+ for (const [key, item] of Object.entries(value)) {
423
+ if (/arguments|authorization|jwt|token|password|secret|credential/i.test(key)) {
424
+ result[key] = "[REDACTED]";
425
+ } else {
426
+ result[key] = redactPayload(item);
427
+ }
428
+ }
429
+ return result;
430
+ }
431
+
432
+ function safeJson(value, secrets = []) {
433
+ let output = JSON.stringify(redactPayload(value));
434
+ for (const secret of secrets) {
435
+ if (secret) output = output.split(secret).join("[REDACTED]");
436
+ }
437
+ return output;
438
+ }
439
+
440
+ function rpcRequest(config, method, params = {}, {
441
+ timeoutMs = REQUEST_TIMEOUT_MS,
442
+ maxResponseBytes = RESPONSE_MAX_BYTES,
443
+ } = {}) {
444
+ const endpoint = new URL(`${config.baseUrl.replace(/\/+$/, "")}${ENDPOINT_PATH}`);
445
+ const payload = JSON.stringify({
446
+ jsonrpc: "2.0",
447
+ id: 1,
448
+ method,
449
+ params,
450
+ });
451
+ const client = endpoint.protocol === "https:" ? https : http;
452
+
453
+ return new Promise((resolve, reject) => {
454
+ let settled = false;
455
+ let responseRef;
456
+ let wallTimer;
457
+
458
+ const settle = (callback, value) => {
459
+ if (settled) return;
460
+ settled = true;
461
+ clearTimeout(wallTimer);
462
+ callback(value);
463
+ };
464
+ const fail = (error, abort = false) => {
465
+ if (settled) return;
466
+ settle(reject, error);
467
+ if (abort) {
468
+ if (responseRef && !responseRef.destroyed) responseRef.destroy();
469
+ if (!request.destroyed) request.destroy();
470
+ }
471
+ };
472
+
473
+ const request = client.request(endpoint, {
474
+ method: "POST",
475
+ headers: {
476
+ Authorization: `GRJWT ${config.jwt}`,
477
+ Accept: "application/json",
478
+ "Content-Type": "application/json",
479
+ "Content-Length": Buffer.byteLength(payload),
480
+ "MCP-Protocol-Version": PROTOCOL_VERSION,
481
+ },
482
+ }, (response) => {
483
+ responseRef = response;
484
+ const chunks = [];
485
+ let responseBytes = 0;
486
+ const interrupted = () => fail(
487
+ new BridgeError("Rainbond API response was interrupted", EXIT.TRANSPORT),
488
+ true
489
+ );
490
+ response.on("aborted", interrupted);
491
+ response.on("error", interrupted);
492
+ response.on("close", () => {
493
+ if (!settled && !response.complete) interrupted();
494
+ });
495
+ response.on("data", (chunk) => {
496
+ if (settled) return;
497
+ responseBytes += chunk.length;
498
+ if (responseBytes > maxResponseBytes) {
499
+ fail(new BridgeError("Rainbond API response is too large", EXIT.TRANSPORT), true);
500
+ return;
501
+ }
502
+ chunks.push(chunk);
503
+ });
504
+ response.on("end", () => {
505
+ if (settled) return;
506
+ if (!response.complete) {
507
+ interrupted();
508
+ return;
509
+ }
510
+ const body = Buffer.concat(chunks, responseBytes).toString("utf8");
511
+ if (response.statusCode === 401 || response.statusCode === 403) {
512
+ fail(new BridgeError("Rainbond authentication failed", EXIT.CONFIG));
513
+ return;
514
+ }
515
+ if (!response.statusCode || response.statusCode < 200 || response.statusCode >= 300) {
516
+ fail(new BridgeError(
517
+ response.statusCode === 404
518
+ ? "Rainskills API endpoint is unavailable; upgrade Rainbond Console"
519
+ : "Rainbond API request failed",
520
+ EXIT.TRANSPORT
521
+ ));
522
+ return;
523
+ }
524
+ let rpc;
525
+ try {
526
+ rpc = JSON.parse(body);
527
+ } catch (_error) {
528
+ fail(new BridgeError("Rainbond returned invalid JSON", EXIT.TRANSPORT));
529
+ return;
530
+ }
531
+ if (!rpc || rpc.jsonrpc !== "2.0" || rpc.id !== 1) {
532
+ fail(new BridgeError("Rainbond returned an invalid JSON-RPC response", EXIT.TRANSPORT));
533
+ return;
534
+ }
535
+ if (rpc.error) {
536
+ const protocolErrors = new Set([-32700, -32600, -32601]);
537
+ fail(new BridgeError(
538
+ "Rainbond rejected the tool request",
539
+ protocolErrors.has(rpc.error.code) ? EXIT.TRANSPORT : EXIT.TOOL,
540
+ {
541
+ code: rpc.error.code,
542
+ message: rpc.error.message,
543
+ }
544
+ ));
545
+ return;
546
+ }
547
+ if (!("result" in rpc)) {
548
+ fail(new BridgeError("Rainbond JSON-RPC result is missing", EXIT.TRANSPORT));
549
+ return;
550
+ }
551
+ settle(resolve, rpc.result);
552
+ });
553
+ });
554
+ request.on("error", (error) => {
555
+ fail(error instanceof BridgeError
556
+ ? error
557
+ : new BridgeError("unable to reach Rainbond API", EXIT.TRANSPORT));
558
+ });
559
+ wallTimer = setTimeout(() => {
560
+ fail(new BridgeError("Rainbond API request timed out", EXIT.TRANSPORT), true);
561
+ }, timeoutMs);
562
+ request.end(payload);
563
+ });
564
+ }
565
+
566
+ async function listTools(config) {
567
+ const cached = loadCachedCatalog(config);
568
+ if (cached) return cached.tools;
569
+ const result = await rpcRequest(config, "tools/list");
570
+ if (!result || !Array.isArray(result.tools)) {
571
+ throw new BridgeError("Rainbond tool catalog is invalid", EXIT.TRANSPORT);
572
+ }
573
+ writeCachedCatalog(config, result.tools);
574
+ return result.tools;
575
+ }
576
+
577
+ async function execute(command, config) {
578
+ if (command.command === "status") {
579
+ const tools = await listTools(config);
580
+ const cached = loadCachedCatalog(config);
581
+ return {
582
+ status: "ok",
583
+ cli_version: CLI_VERSION,
584
+ tool_count: tools.length,
585
+ catalog_age_ms: cached ? Math.max(0, Math.round(cached.age)) : 0,
586
+ };
587
+ }
588
+ if (command.command === "list") {
589
+ const tools = await listTools(config);
590
+ return tools
591
+ .map((tool) => tool && tool.name)
592
+ .filter((name) => typeof name === "string" && (!command.prefix || name.startsWith(command.prefix)));
593
+ }
594
+ if (command.command === "describe") {
595
+ const tools = await listTools(config);
596
+ const tool = tools.find((candidate) => candidate && candidate.name === command.toolName);
597
+ if (!tool) throw new BridgeError("Rainbond tool was not found", EXIT.TOOL);
598
+ return {
599
+ name: tool.name,
600
+ description: tool.description,
601
+ inputSchema: tool.inputSchema,
602
+ };
603
+ }
604
+
605
+ const operationClass = classifyTool(command.toolName);
606
+ if (command.command === "read" && operationClass !== "read") {
607
+ throw new BridgeError(
608
+ "read is read-only and rejects tools classified as write or destructive",
609
+ EXIT.USAGE
610
+ );
611
+ }
612
+ const argumentsValue = command.argumentsValue || readArguments(command.input);
613
+ let operation = null;
614
+ if (operationClass !== "read") {
615
+ if (!command.confirmation) {
616
+ operation = prepareOperation(
617
+ config,
618
+ command.toolName,
619
+ operationClass,
620
+ argumentsValue
621
+ );
622
+ return {
623
+ requires_confirmation: true,
624
+ operation_id: operation.operation_id,
625
+ operation_class: operation.operation_class,
626
+ summary: "Review the target and repeat this exact call with --confirm <operation_id> to execute it once.",
627
+ };
628
+ }
629
+ operation = confirmOperation(
630
+ config,
631
+ command.confirmation,
632
+ command.toolName,
633
+ operationClass,
634
+ argumentsValue
635
+ );
636
+ }
637
+
638
+ try {
639
+ const result = await rpcRequest(config, "tools/call", {
640
+ name: command.toolName,
641
+ arguments: argumentsValue,
642
+ });
643
+ if (!result || typeof result !== "object") {
644
+ throw new BridgeError("Rainbond tool result is invalid", EXIT.TRANSPORT);
645
+ }
646
+ if (result.isError) {
647
+ const statusCode = result.structuredContent && (
648
+ result.structuredContent.status_code || result.structuredContent.error_code
649
+ );
650
+ throw new BridgeError(
651
+ "Rainbond tool call failed",
652
+ statusCode === 401 || statusCode === 403 ? EXIT.CONFIG : EXIT.TOOL,
653
+ result.structuredContent || { error: "tool call failed" }
654
+ );
655
+ }
656
+ if (!("structuredContent" in result)) {
657
+ throw new BridgeError("Rainbond tool response lacks structuredContent", EXIT.TRANSPORT);
658
+ }
659
+ if (operation) updateOperation(config, operation, "succeeded");
660
+ return fitOutput(sanitizeToolResult(result.structuredContent));
661
+ } catch (error) {
662
+ if (operation) {
663
+ updateOperation(config, operation, error.exitCode === EXIT.TRANSPORT ? "unknown" : "failed");
664
+ }
665
+ throw error;
666
+ }
667
+ }
668
+
669
+ async function main(args = process.argv.slice(2)) {
670
+ let config;
671
+ let command;
672
+ let argumentRedactions = [];
673
+ try {
674
+ command = parseCommand(args);
675
+ if (command.command === "read" || command.command === "call") {
676
+ const argumentsValue = readArguments(command.input);
677
+ command.argumentsValue = argumentsValue;
678
+ argumentRedactions = collectArgumentRedactions(argumentsValue);
679
+ }
680
+ config = loadConfig({ includeRuntime: true });
681
+ if (config.isInsecureHttp) {
682
+ process.stderr.write('{"warning":"using insecure HTTP transport"}\n');
683
+ }
684
+ const output = await execute(command, config);
685
+ process.stdout.write(`${JSON.stringify(fitOutput(output))}\n`);
686
+ } catch (error) {
687
+ const bridgeError = error instanceof BridgeError
688
+ ? error
689
+ : new BridgeError("unexpected API bridge error", EXIT.TRANSPORT);
690
+ const secrets = [config && config.jwt, config && config.baseUrl, ...argumentRedactions];
691
+ const output = bridgeError.payload || { error: bridgeError.message };
692
+ process.stderr.write(`${safeJson(output, secrets)}\n`);
693
+ process.exitCode = bridgeError.exitCode;
694
+ }
695
+ }
696
+
697
+ module.exports = {
698
+ REQUEST_TIMEOUT_MS,
699
+ INPUT_MAX_BYTES,
700
+ RESPONSE_MAX_BYTES,
701
+ STDOUT_MAX_BYTES,
702
+ CATALOG_TTL_MS,
703
+ CLI_VERSION,
704
+ loadConfig,
705
+ parseEnvFile,
706
+ parseCommand,
707
+ rpcRequest,
708
+ };
709
+
710
+ if (require.main === module) {
711
+ main();
712
+ }